From 335f82d740fa43c11794d2171b452279b212f5de Mon Sep 17 00:00:00 2001
From: Paul Korzhyk
Date: Thu, 28 Aug 2025 20:19:06 +0300
Subject: [PATCH 001/105] Add Mixin1D.discretize
---
src/build123d/topology/one_d.py | 32 +++++++++++++++++++++++++-
tests/test_direct_api/test_mixin1_d.py | 5 ++++
2 files changed, 36 insertions(+), 1 deletion(-)
diff --git a/src/build123d/topology/one_d.py b/src/build123d/topology/one_d.py
index ff75d64..5df771a 100644
--- a/src/build123d/topology/one_d.py
+++ b/src/build123d/topology/one_d.py
@@ -91,7 +91,11 @@ from OCP.BRepPrimAPI import BRepPrimAPI_MakeHalfSpace
from OCP.BRepProj import BRepProj_Projection
from OCP.BRepTools import BRepTools, BRepTools_WireExplorer
from OCP.GC import GC_MakeArcOfCircle, GC_MakeArcOfEllipse
-from OCP.GCPnts import GCPnts_AbscissaPoint
+from OCP.GCPnts import (
+ GCPnts_AbscissaPoint,
+ GCPnts_QuasiUniformDeflection,
+ GCPnts_UniformDeflection,
+)
from OCP.GProp import GProp_GProps
from OCP.Geom import (
Geom_BezierCurve,
@@ -484,6 +488,32 @@ class Mixin1D(Shape):
return result
+ def discretize(self, deflection: float = 0.1, quasi=True) -> list[Vector]:
+ """Discretize the shape into a list of points"""
+ if self.wrapped is None:
+ raise ValueError("Cannot discretize an empty shape")
+ curve = self.geom_adaptor()
+ if quasi:
+ discretizer = GCPnts_QuasiUniformDeflection()
+ else:
+ discretizer = GCPnts_UniformDeflection()
+ discretizer.Initialize(
+ curve,
+ deflection,
+ curve.FirstParameter(),
+ curve.LastParameter(),
+ )
+
+ assert discretizer.IsDone()
+
+ return [
+ Vector(v.X(), v.Y(), v.Z())
+ for v in (
+ curve.Value(discretizer.Parameter(i))
+ for i in range(1, discretizer.NbPoints() + 1)
+ )
+ ]
+
def edge(self) -> Edge | None:
"""Return the Edge"""
return Shape.get_single_shape(self, "Edge")
diff --git a/tests/test_direct_api/test_mixin1_d.py b/tests/test_direct_api/test_mixin1_d.py
index df8f2d4..8e3c61e 100644
--- a/tests/test_direct_api/test_mixin1_d.py
+++ b/tests/test_direct_api/test_mixin1_d.py
@@ -354,6 +354,11 @@ class TestMixin1D(unittest.TestCase):
self.assertAlmostEqual(common.z_dir.Y, 0, 5)
self.assertAlmostEqual(common.z_dir.Z, 0, 5)
+ def test_discretize(self):
+ edge = Edge.make_circle(2, start_angle=0, end_angle=180)
+ points = edge.discretize(0.1)
+ self.assertEqual(len(points), 6)
+
def test_edge_volume(self):
edge = Edge.make_line((0, 0), (1, 1))
self.assertAlmostEqual(edge.volume, 0, 5)
From 3d8bbcc539f3fbe6335fbfb461c269dd3ee65212 Mon Sep 17 00:00:00 2001
From: Jonathan Wagenet
Date: Tue, 9 Sep 2025 23:21:05 -0400
Subject: [PATCH 002/105] Add basic b123d lexer and change pygments style
---
docs/OpenSCAD.rst | 4 +-
docs/advantages.rst | 9 +--
docs/algebra_performance.rst | 6 +-
docs/assemblies.rst | 8 ++-
docs/build123d_lexer.py | 75 ++++++++++++++++++++
docs/build_line.rst | 8 +++
docs/build_part.rst | 2 +
docs/build_sketch.rst | 8 ++-
docs/conf.py | 2 +
docs/debugging_logging.rst | 2 +-
docs/examples_1.rst | 33 +++++++++
docs/import_export.rst | 8 +--
docs/index.rst | 3 +-
docs/introductory_examples.rst | 76 ++++++++++++++++++++-
docs/joints.rst | 7 +-
docs/key_concepts_algebra.rst | 26 +++----
docs/key_concepts_builder.rst | 32 ++++-----
docs/location_arithmetic.rst | 16 ++---
docs/moving_objects.rst | 24 +++----
docs/objects.rst | 9 +--
docs/operations.rst | 4 +-
docs/selectors.rst | 4 +-
docs/tech_drawing_tutorial.rst | 14 ++--
docs/tips.rst | 12 ++--
docs/topology_selection.rst | 24 +++----
docs/topology_selection/filter_examples.rst | 28 ++++----
docs/topology_selection/group_examples.rst | 18 ++---
docs/topology_selection/sort_examples.rst | 22 +++---
docs/tttt.rst | 13 ++++
docs/tutorial_design.rst | 48 ++++++-------
docs/tutorial_joints.rst | 15 ++++
docs/tutorial_lego.rst | 11 +++
docs/tutorial_selectors.rst | 8 ++-
docs/tutorial_surface_modeling.rst | 14 ++--
34 files changed, 421 insertions(+), 172 deletions(-)
create mode 100644 docs/build123d_lexer.py
diff --git a/docs/OpenSCAD.rst b/docs/OpenSCAD.rst
index 899cee9..3c64382 100644
--- a/docs/OpenSCAD.rst
+++ b/docs/OpenSCAD.rst
@@ -124,7 +124,7 @@ build123d of a piece of angle iron:
**build123d Approach**
-.. code-block:: python
+.. code-block:: build123d
# Builder mode
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)
-.. code-block:: python
+.. code-block:: build123d
# Algebra mode
profile = Rectangle(3 * CM, 4 * MM, align=Align.MIN)
diff --git a/docs/advantages.rst b/docs/advantages.rst
index 51a6ac0..15f2b7e 100644
--- a/docs/advantages.rst
+++ b/docs/advantages.rst
@@ -20,7 +20,7 @@ python context manager.
...
)
-.. code-block:: python
+.. code-block:: build123d
# build123d API
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
an instance variable as with standard python programming for direct use.
-.. code-block:: python
+.. code-block:: build123d
with BuildSketch() as plan:
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
features to these points without knowing their numeric values.
-.. code-block:: python
+.. code-block:: build123d
with BuildLine() as outline:
...
@@ -81,6 +81,7 @@ by the last operation and fillets them. Such a selection would be quite difficul
otherwise.
.. literalinclude:: ../examples/intersecting_pipes.py
+ :language: build123d
: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
a fully custom selection:
-.. code-block:: python
+.. code-block:: build123d
top = rail.faces().filter_by(Axis.Z)[-1]
...
diff --git a/docs/algebra_performance.rst b/docs/algebra_performance.rst
index 3ec9a20..12784c3 100644
--- a/docs/algebra_performance.rst
+++ b/docs/algebra_performance.rst
@@ -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
already fused. Overall it takes on an M1 Mac 4.76 sec.
-.. code-block:: python
+.. code-block:: build123d
diam = 80
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
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)
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
an empty ``Part``, ``Sketch`` or ``Curve``:
-.. code-block:: python
+.. code-block:: build123d
polygons = Sketch() + [
loc * RegularPolygon(radius=5, side_count=5)
diff --git a/docs/assemblies.rst b/docs/assemblies.rst
index 8889c28..4fe1ada 100644
--- a/docs/assemblies.rst
+++ b/docs/assemblies.rst
@@ -22,6 +22,7 @@ Here we'll assign labels to all of the components that will be part of the box
assembly:
.. literalinclude:: tutorial_joints.py
+ :language: build123d
:start-after: [Add labels]
: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:
.. literalinclude:: tutorial_joints.py
+ :language: build123d
:start-after: [Create 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:
.. literalinclude:: tutorial_joints.py
+ :language: build123d
:start-after: [Display assembly]
: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.
.. literalinclude:: tutorial_joints.py
+ :language: build123d
: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]
@@ -180,7 +184,7 @@ Compare this to assembly3_volume which only results in the volume of the top lev
assembly2 = Compound(label='Assembly2', children=[assembly1, Box(1, 1, 1)])
assembly3 = Compound(label='Assembly3', children=[assembly2, Box(1, 1, 1)])
total_volume = sum(part.volume for part in assembly3.solids()) # 3
- assembly3_volume = assembly3.volume # 1
+ assembly3_volume = assembly3.volume # 1
******
pack
@@ -269,6 +273,6 @@ If you place the arranged objects into a ``Compound``, you can easily determine
# [bounding box]
print(Compound(xy_pack).bounding_box())
# bbox: 0.0 <= x <= 159.0, 0.0 <= y <= 129.0, -54.0 <= z <= 100.0
-
+
print(Compound(z_pack).bounding_box())
# bbox: 0.0 <= x <= 159.0, 0.0 <= y <= 129.0, 0.0 <= z <= 100.0
diff --git a/docs/build123d_lexer.py b/docs/build123d_lexer.py
new file mode 100644
index 0000000..f01e58f
--- /dev/null
+++ b/docs/build123d_lexer.py
@@ -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"}
\ No newline at end of file
diff --git a/docs/build_line.rst b/docs/build_line.rst
index 70c7f2a..f2f0f93 100644
--- a/docs/build_line.rst
+++ b/docs/build_line.rst
@@ -15,6 +15,7 @@ Basic Functionality
The following is a simple BuildLine example:
.. literalinclude:: objects_1d.py
+ :language: build123d
:start-after: [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:
.. literalinclude:: objects_1d.py
+ :language: build123d
:start-after: [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:
.. literalinclude:: objects_1d.py
+ :language: build123d
:start-after: [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:
.. literalinclude:: objects_1d.py
+ :language: build123d
:start-after: [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:
.. literalinclude:: objects_1d.py
+ :language: build123d
:start-after: [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:
.. literalinclude:: objects_1d.py
+ :language: build123d
:start-after: [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:
.. literalinclude:: objects_1d.py
+ :language: build123d
:start-after: [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.
.. literalinclude:: objects_1d.py
+ :language: build123d
:start-after: [Ex. 8]
:end-before: [Ex. 8]
diff --git a/docs/build_part.rst b/docs/build_part.rst
index 6ea9d11..d5206c8 100644
--- a/docs/build_part.rst
+++ b/docs/build_part.rst
@@ -15,6 +15,7 @@ Basic Functionality
The following is a simple BuildPart example:
.. literalinclude:: general_examples.py
+ :language: build123d
:start-after: [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:
.. literalinclude:: ../examples/tea_cup.py
+ :language: build123d
:start-after: [Code]
:end-before: [End]
:emphasize-lines: 52
diff --git a/docs/build_sketch.rst b/docs/build_sketch.rst
index 803dcb7..76ed6f7 100644
--- a/docs/build_sketch.rst
+++ b/docs/build_sketch.rst
@@ -16,6 +16,7 @@ Basic Functionality
The following is a simple BuildSketch example:
.. literalinclude:: objects_2d.py
+ :language: build123d
:start-after: [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:
.. literalinclude:: objects_2d.py
+ :language: build123d
:start-after: [Ex. 14]
:end-before: [Ex. 14]
: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
above, one would use:
-.. code-block:: python
+.. code-block:: build123d
show_object(display.sketch_local, name="sketch on Plane.XY")
while the sketches as applied to their target workplanes is accessible through
the ``sketch`` property, as follows:
-.. code-block:: python
+.. code-block:: build123d
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
follows:
-.. code-block:: python
+.. code-block:: build123d
reoriented_face = plane.to_local_coords(face)
diff --git a/docs/conf.py b/docs/conf.py
index 5ba9cea..ff46e9f 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -49,6 +49,7 @@ extensions = [
"sphinx_design",
"sphinx_copybutton",
"hoverxref.extension",
+ "build123d_lexer"
]
# Napoleon settings
@@ -99,6 +100,7 @@ exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"]
#
# html_theme = "alabaster"
html_theme = "sphinx_rtd_theme"
+pygments_style = "colorful"
# 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,
diff --git a/docs/debugging_logging.rst b/docs/debugging_logging.rst
index 3e27acd..eb24d68 100644
--- a/docs/debugging_logging.rst
+++ b/docs/debugging_logging.rst
@@ -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
location in memory, as follows:
-.. code-block:: python
+.. code-block:: build123d
plane = Plane.XY.offset(1)
print(f"{plane=}")
diff --git a/docs/examples_1.rst b/docs/examples_1.rst
index 7da07de..32cc77d 100644
--- a/docs/examples_1.rst
+++ b/docs/examples_1.rst
@@ -164,6 +164,7 @@ modify it by replacing chimney with a BREP version.
.. dropdown:: |Builder| Reference Implementation (Builder Mode)
.. literalinclude:: ../examples/benchy.py
+ :language: build123d
:start-after: [Code]
:end-before: [End]
@@ -184,6 +185,7 @@ surface.
.. dropdown:: |Builder| Reference Implementation (Builder Mode)
.. literalinclude:: ../examples/bicycle_tire.py
+ :language: build123d
:start-after: [Code]
:end-before: [End]
@@ -204,12 +206,14 @@ The builder mode example also generates the SVG file `logo.svg`.
.. dropdown:: |Builder| Reference Implementation (Builder Mode)
.. literalinclude:: ../examples/build123d_logo.py
+ :language: build123d
:start-after: [Code]
:end-before: [End]
.. dropdown:: |Algebra| Reference Implementation (Algebra Mode)
.. literalinclude:: ../examples/build123d_logo_algebra.py
+ :language: build123d
:start-after: [Code]
: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)
.. literalinclude:: ../examples/cast_bearing_unit.py
+ :language: build123d
:start-after: [Code]
: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)
.. literalinclude:: ../examples/canadian_flag.py
+ :language: build123d
:start-after: [Code]
:end-before: [End]
.. dropdown:: |Algebra| Reference Implementation (Algebra Mode)
.. literalinclude:: ../examples/canadian_flag_algebra.py
+ :language: build123d
:start-after: [Code]
:end-before: [End]
@@ -293,12 +300,14 @@ This example demonstrates placing holes around a part.
.. dropdown:: |Builder| Reference Implementation (Builder Mode)
.. literalinclude:: ../examples/circuit_board.py
+ :language: build123d
:start-after: [Code]
:end-before: [End]
.. dropdown:: |Algebra| Reference Implementation (Algebra Mode)
.. literalinclude:: ../examples/circuit_board_algebra.py
+ :language: build123d
:start-after: [Code]
:end-before: [End]
@@ -313,12 +322,14 @@ Clock Face
.. dropdown:: |Builder| Reference Implementation (Builder Mode)
.. literalinclude:: ../examples/clock.py
+ :language: build123d
:start-after: [Code]
:end-before: [End]
.. dropdown:: |Algebra| Reference Implementation (Algebra Mode)
.. literalinclude:: ../examples/clock_algebra.py
+ :language: build123d
:start-after: [Code]
:end-before: [End]
@@ -340,6 +351,7 @@ Fast Grid Holes
.. dropdown:: |Algebra| Reference Implementation (Algebra Mode)
.. literalinclude:: ../examples/fast_grid_holes.py
+ :language: build123d
:start-after: [Code]
:end-before: [End]
@@ -367,12 +379,14 @@ Handle
.. dropdown:: |Builder| Reference Implementation (Builder Mode)
.. literalinclude:: ../examples/handle.py
+ :language: build123d
:start-after: [Code]
:end-before: [End]
.. dropdown:: |Algebra| Reference Implementation (Algebra Mode)
.. literalinclude:: ../examples/handle_algebra.py
+ :language: build123d
:start-after: [Code]
:end-before: [End]
@@ -388,12 +402,14 @@ Heat Exchanger
.. dropdown:: |Builder| Reference Implementation (Builder Mode)
.. literalinclude:: ../examples/heat_exchanger.py
+ :language: build123d
:start-after: [Code]
:end-before: [End]
.. dropdown:: |Algebra| Reference Implementation (Algebra Mode)
.. literalinclude:: ../examples/heat_exchanger_algebra.py
+ :language: build123d
:start-after: [Code]
:end-before: [End]
@@ -412,12 +428,14 @@ Key Cap
.. dropdown:: |Builder| Reference Implementation (Builder Mode)
.. literalinclude:: ../examples/key_cap.py
+ :language: build123d
:start-after: [Code]
:end-before: [End]
.. dropdown:: |Algebra| Reference Implementation (Algebra Mode)
.. literalinclude:: ../examples/key_cap_algebra.py
+ :language: build123d
:start-after: [Code]
:end-before: [End]
@@ -444,6 +462,7 @@ YouTube channel. There are two key features:
.. dropdown:: |Builder| Reference Implementation (Builder Mode)
.. literalinclude:: ../examples/maker_coin.py
+ :language: build123d
:start-after: [Code]
:end-before: [End]
@@ -462,12 +481,14 @@ the top and bottom by type, and shelling.
.. dropdown:: |Builder| Reference Implementation (Builder Mode)
.. literalinclude:: ../examples/loft.py
+ :language: build123d
:start-after: [Code]
:end-before: [End]
.. dropdown:: |Algebra| Reference Implementation (Algebra Mode)
.. literalinclude:: ../examples/loft_algebra.py
+ :language: build123d
:start-after: [Code]
:end-before: [End]
@@ -488,6 +509,7 @@ to aid 3D printing.
.. dropdown:: |Builder| Reference Implementation (Builder Mode)
.. literalinclude:: ../examples/pegboard_j_hook.py
+ :language: build123d
:start-after: [Code]
:end-before: [End]
@@ -495,6 +517,7 @@ to aid 3D printing.
.. dropdown:: |Algebra| Reference Implementation (Algebra Mode)
.. literalinclude:: ../examples/pegboard_j_hook_algebra.py
+ :language: build123d
:start-after: [Code]
:end-before: [End]
@@ -521,6 +544,7 @@ embodying ideals of symmetry and balance.
.. dropdown:: |Algebra| Reference Implementation (Algebra Mode)
.. literalinclude:: ../examples/platonic_solids.py
+ :language: build123d
:start-after: [Code]
: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)
.. literalinclude:: ../examples/playing_cards.py
+ :language: build123d
:start-after: [Code]
:end-before: [End]
@@ -558,6 +583,7 @@ are used to position all of objects.
.. dropdown:: |Algebra| Reference Implementation (Algebra Mode)
.. literalinclude:: ../examples/stud_wall.py
+ :language: build123d
:start-after: [Code]
:end-before: [End]
@@ -571,12 +597,14 @@ Tea Cup
.. dropdown:: |Builder| Reference Implementation (Builder Mode)
.. literalinclude:: ../examples/tea_cup.py
+ :language: build123d
:start-after: [Code]
:end-before: [End]
.. dropdown:: |Algebra| Reference Implementation (Algebra Mode)
.. literalinclude:: ../examples/tea_cup_algebra.py
+ :language: build123d
:start-after: [Code]
:end-before: [End]
@@ -610,6 +638,7 @@ Toy Truck
.. dropdown:: |Builder| Reference Implementation (Builder Mode)
.. literalinclude:: ../examples/toy_truck.py
+ :language: build123d
:start-after: [Code]
:end-before: [End]
@@ -630,12 +659,14 @@ Vase
.. dropdown:: |Builder| Reference Implementation (Builder Mode)
.. literalinclude:: ../examples/vase.py
+ :language: build123d
:start-after: [Code]
:end-before: [End]
.. dropdown:: |Algebra| Reference Implementation (Algebra Mode)
.. literalinclude:: ../examples/vase_algebra.py
+ :language: build123d
:start-after: [Code]
: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)
.. literalinclude:: ../examples/boxes_on_faces.py
+ :language: build123d
:start-after: [Code]
:end-before: [End]
.. dropdown:: |Algebra| Reference Implementation (Algebra Mode)
.. literalinclude:: ../examples/boxes_on_faces_algebra.py
+ :language: build123d
:start-after: [Code]
:end-before: [End]
diff --git a/docs/import_export.rst b/docs/import_export.rst
index 73b26b2..53e935f 100644
--- a/docs/import_export.rst
+++ b/docs/import_export.rst
@@ -6,7 +6,7 @@ Methods and functions specific to exporting and importing build123d objects are
For example:
-.. code-block:: python
+.. code-block:: build123d
with BuildPart() as box_builder:
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 this example:
-.. code-block:: python
+.. code-block:: build123d
view_port_origin=(-100, -50, 30)
visible, hidden = part.project_to_viewport(view_port_origin)
@@ -222,7 +222,7 @@ more complex API than the simple Shape exporters.
For example:
-.. code-block:: python
+.. code-block:: build123d
# Create the shapes and assign attributes
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:
-.. code-block:: python
+.. code-block:: build123d
importer = Mesher()
cone, cyl = importer.read("example.3mf")
diff --git a/docs/index.rst b/docs/index.rst
index 0af6014..08f3299 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -66,7 +66,7 @@ file or used in an Assembly. There are three builders available:
The three builders work together in a hierarchy as follows:
-.. code-block:: python
+.. code-block:: build123d
with BuildPart() as my_part:
...
@@ -83,6 +83,7 @@ added to ``my_part`` once the sketch is complete.
As an example, consider the design of a tea cup:
.. literalinclude:: ../examples/tea_cup.py
+ :language: build123d
:start-after: [Code]
:end-before: [End]
diff --git a/docs/introductory_examples.rst b/docs/introductory_examples.rst
index 1399a80..a887bb9 100644
--- a/docs/introductory_examples.rst
+++ b/docs/introductory_examples.rst
@@ -36,12 +36,14 @@ Just about the simplest possible example, a rectangular :class:`~objects_part.Bo
* **Builder mode**
.. literalinclude:: general_examples.py
+ :language: build123d
:start-after: [Ex. 1]
:end-before: [Ex. 1]
* **Algebra mode**
.. literalinclude:: general_examples_algebra.py
+ :language: build123d
:start-after: [Ex. 1]
:end-before: [Ex. 1]
@@ -63,6 +65,7 @@ A rectangular box, but with a hole added.
from the :class:`~objects_part.Box`.
.. literalinclude:: general_examples.py
+ :language: build123d
:start-after: [Ex. 2]
:end-before: [Ex. 2]
@@ -73,6 +76,7 @@ A rectangular box, but with a hole added.
from the :class:`~objects_part.Box`.
.. literalinclude:: general_examples_algebra.py
+ :language: build123d
:start-after: [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.
.. literalinclude:: general_examples.py
+ :language: build123d
:start-after: [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.
.. literalinclude:: general_examples_algebra.py
+ :language: build123d
:start-after: [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.
.. literalinclude:: general_examples.py
+ :language: build123d
:start-after: [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.
.. literalinclude:: general_examples_algebra.py
+ :language: build123d
:start-after: [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.
.. literalinclude:: general_examples.py
+ :language: build123d
:start-after: [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.
.. literalinclude:: general_examples_algebra.py
+ :language: build123d
:start-after: [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.
.. literalinclude:: general_examples.py
+ :language: build123d
:start-after: [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`).
.. literalinclude:: general_examples_algebra.py
+ :language: build123d
:start-after: [Ex. 6]
:end-before: [Ex. 6]
@@ -218,6 +230,7 @@ Sometimes you need to create a number of features at various
you would like.
.. literalinclude:: general_examples.py
+ :language: build123d
:start-after: [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.
.. literalinclude:: general_examples_algebra.py
+ :language: build123d
:start-after: [Ex. 7]
:end-before: [Ex. 7]
@@ -247,12 +261,14 @@ create the final profile.
* **Builder mode**
.. literalinclude:: general_examples.py
+ :language: build123d
:start-after: [Ex. 8]
:end-before: [Ex. 8]
* **Algebra mode**
.. literalinclude:: general_examples_algebra.py
+ :language: build123d
:start-after: [Ex. 8]
:end-before: [Ex. 8]
@@ -273,12 +289,14 @@ edges, you could simply pass in ``ex9.edges()``.
* **Builder mode**
.. literalinclude:: general_examples.py
+ :language: build123d
:start-after: [Ex. 9]
:end-before: [Ex. 9]
* **Algebra mode**
.. literalinclude:: general_examples_algebra.py
+ :language: build123d
:start-after: [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.
.. literalinclude:: general_examples.py
+ :language: build123d
:start-after: [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.
.. literalinclude:: general_examples_algebra.py
+ :language: build123d
:start-after: [Ex. 10]
:end-before: [Ex. 10]
@@ -339,6 +359,7 @@ be the highest z-dimension group.
cut these from the parent.
.. literalinclude:: general_examples.py
+ :language: build123d
:start-after: [Ex. 11]
:end-before: [Ex. 11]
@@ -355,6 +376,7 @@ be the highest z-dimension group.
parent.
.. literalinclude:: general_examples_algebra.py
+ :language: build123d
:start-after: [Ex. 11]
:end-before: [Ex. 11]
@@ -376,12 +398,14 @@ edge that needs a complex profile.
* **Builder mode**
.. literalinclude:: general_examples.py
+ :language: build123d
:start-after: [Ex. 12]
:end-before: [Ex. 12]
* **Algebra mode**
.. literalinclude:: general_examples_algebra.py
+ :language: build123d
:start-after: [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`.
.. literalinclude:: general_examples.py
+ :language: build123d
:start-after: [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.
.. literalinclude:: general_examples_algebra.py
+ :language: build123d
:start-after: [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:
-14. Position on a line with '\@', '\%' and introduce Sweep
+1. Position on a line with '\@', '\%' and introduce Sweep
------------------------------------------------------------
build123d includes a feature for finding the position along a line segment. This
@@ -437,9 +463,10 @@ path, please see example 37 for a way to make this placement easier.
The :meth:`~operations_generic.sweep` method takes any pending faces and sweeps them through the provided
path (in this case the path is taken from the pending edges from ``ex14_ln``).
- :meth:`~operations_part.revolve` requires a single connected wire.
+ :meth:`~operations_part.revolve` requires a single connected wire.
.. literalinclude:: general_examples.py
+ :language: build123d
:start-after: [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``).
.. literalinclude:: general_examples_algebra.py
+ :language: build123d
:start-after: [Ex. 14]
:end-before: [Ex. 14]
@@ -471,6 +499,7 @@ Additionally the '@' operator is used to simplify the line segment commands.
* **Builder mode**
.. literalinclude:: general_examples.py
+ :language: build123d
:start-after: [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]``
.. literalinclude:: general_examples_algebra.py
+ :language: build123d
:start-after: [Ex. 15]
:end-before: [Ex. 15]
@@ -496,12 +526,14 @@ The ``Plane.offset()`` method shifts the plane in the normal direction (positive
* **Builder mode**
.. literalinclude:: general_examples.py
+ :language: build123d
:start-after: [Ex. 16]
:end-before: [Ex. 16]
* **Algebra mode**
.. literalinclude:: general_examples_algebra.py
+ :language: build123d
:start-after: [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**
.. literalinclude:: general_examples.py
+ :language: build123d
:start-after: [Ex. 17]
:end-before: [Ex. 17]
* **Algebra mode**
.. literalinclude:: general_examples_algebra.py
+ :language: build123d
:start-after: [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.
.. literalinclude:: general_examples.py
+ :language: build123d
:start-after: [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.
.. literalinclude:: general_examples_algebra.py
+ :language: build123d
:start-after: [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.
.. literalinclude:: general_examples.py
+ :language: build123d
:start-after: [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.
.. literalinclude:: general_examples_algebra.py
+ :language: build123d
:start-after: [Ex. 19]
:end-before: [Ex. 19]
@@ -606,12 +644,14 @@ negative x-direction. The resulting Plane is offset from the original position.
* **Builder mode**
.. literalinclude:: general_examples.py
+ :language: build123d
:start-after: [Ex. 20]
:end-before: [Ex. 20]
* **Algebra mode**
.. literalinclude:: general_examples_algebra.py
+ :language: build123d
:start-after: [Ex. 20]
:end-before: [Ex. 20]
@@ -630,12 +670,14 @@ positioning another cylinder perpendicular and halfway along the first.
* **Builder mode**
.. literalinclude:: general_examples.py
+ :language: build123d
:start-after: [Ex. 21]
:end-before: [Ex. 21]
* **Algebra mode**
.. literalinclude:: general_examples_algebra.py
+ :language: build123d
:start-after: [Ex. 21]
:end-before: [Ex. 21]
@@ -656,6 +698,7 @@ example.
Use the :meth:`~geometry.Plane.rotated` method to rotate the workplane.
.. literalinclude:: general_examples.py
+ :language: build123d
:start-after: [Ex. 22]
:end-before: [Ex. 22]
@@ -664,6 +707,7 @@ example.
Use the operator ``*`` to relocate the plane (post-multiplication!).
.. literalinclude:: general_examples_algebra.py
+ :language: build123d
:start-after: [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**
.. literalinclude:: general_examples.py
+ :language: build123d
:start-after: [Ex. 23]
:end-before: [Ex. 23]
* **Algebra mode**
.. literalinclude:: general_examples_algebra.py
+ :language: build123d
:start-after: [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**
.. literalinclude:: general_examples.py
+ :language: build123d
:start-after: [Ex. 24]
:end-before: [Ex. 24]
* **Algebra mode**
.. literalinclude:: general_examples_algebra.py
+ :language: build123d
:start-after: [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`.
.. literalinclude:: general_examples.py
+ :language: build123d
:start-after: [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`.
.. literalinclude:: general_examples_algebra.py
+ :language: build123d
:start-after: [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**
.. literalinclude:: general_examples.py
+ :language: build123d
:start-after: [Ex. 26]
:end-before: [Ex. 26]
* **Algebra mode**
.. literalinclude:: general_examples_algebra.py
+ :language: build123d
:start-after: [Ex. 26]
:end-before: [Ex. 26]
@@ -796,12 +848,14 @@ a face and offset half the width of the box.
* **Builder mode**
.. literalinclude:: general_examples.py
+ :language: build123d
:start-after: [Ex. 27]
:end-before: [Ex. 27]
* **Algebra mode**
.. literalinclude:: general_examples_algebra.py
+ :language: build123d
:start-after: [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.
.. literalinclude:: general_examples.py
+ :language: build123d
:start-after: [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.
.. literalinclude:: general_examples_algebra.py
+ :language: build123d
:start-after: [Ex. 28]
:end-before: [Ex. 28]
@@ -849,12 +905,14 @@ the bottle opening.
* **Builder mode**
.. literalinclude:: general_examples.py
+ :language: build123d
:start-after: [Ex. 29]
:end-before: [Ex. 29]
* **Algebra mode**
.. literalinclude:: general_examples_algebra.py
+ :language: build123d
:start-after: [Ex. 29]
:end-before: [Ex. 29]
@@ -874,12 +932,14 @@ create a closed line that is made into a face and extruded.
* **Builder mode**
.. literalinclude:: general_examples.py
+ :language: build123d
:start-after: [Ex. 30]
:end-before: [Ex. 30]
* **Algebra mode**
.. literalinclude:: general_examples_algebra.py
+ :language: build123d
:start-after: [Ex. 30]
:end-before: [Ex. 30]
@@ -899,12 +959,14 @@ rotates any "children" groups by default.
* **Builder mode**
.. literalinclude:: general_examples.py
+ :language: build123d
:start-after: [Ex. 31]
:end-before: [Ex. 31]
* **Algebra mode**
.. literalinclude:: general_examples_algebra.py
+ :language: build123d
:start-after: [Ex. 31]
:end-before: [Ex. 31]
@@ -927,12 +989,14 @@ separate calls to :meth:`~operations_part.extrude`.
adding these faces until the for-loop.
.. literalinclude:: general_examples.py
+ :language: build123d
:start-after: [Ex. 32]
:end-before: [Ex. 32]
* **Algebra mode**
.. literalinclude:: general_examples_algebra.py
+ :language: build123d
:start-after: [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`.
.. literalinclude:: general_examples.py
+ :language: build123d
:start-after: [Ex. 33]
:end-before: [Ex. 33]
@@ -962,6 +1027,7 @@ progressively modify the size of each square.
The function returns a ``Sketch`` object.
.. literalinclude:: general_examples_algebra.py
+ :language: build123d
:start-after: [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.
.. literalinclude:: general_examples.py
+ :language: build123d
:start-after: [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".
.. literalinclude:: general_examples_algebra.py
+ :language: build123d
:start-after: [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`.
.. literalinclude:: general_examples.py
+ :language: build123d
:start-after: [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`.
.. literalinclude:: general_examples_algebra.py
+ :language: build123d
:start-after: [Ex. 35]
:end-before: [Ex. 35]
@@ -1041,11 +1111,13 @@ with ``Until.NEXT`` or ``Until.LAST``.
* **Builder mode**
.. literalinclude:: general_examples.py
+ :language: build123d
:start-after: [Ex. 36]
:end-before: [Ex. 36]
* **Algebra mode**
.. literalinclude:: general_examples_algebra.py
+ :language: build123d
:start-after: [Ex. 36]
:end-before: [Ex. 36]
diff --git a/docs/joints.rst b/docs/joints.rst
index 07cc623..3b117b0 100644
--- a/docs/joints.rst
+++ b/docs/joints.rst
@@ -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
:class:`~geometry.Location`) - as follows:
-.. code-block:: python
+.. code-block:: build123d
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
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"])
@@ -70,6 +70,7 @@ flanges are attached to the ends of a curved pipe:
.. image:: assets/rigid_joints_pipe.png
.. literalinclude:: rigid_joints_pipe.py
+ :language: build123d
:emphasize-lines: 19-20, 23-24
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:
.. literalinclude:: slide_latch.py
+ :language: build123d
:emphasize-lines: 30, 52, 55
.. image:: assets/joint-latch.png
@@ -193,6 +195,7 @@ is found within a rod end as shown here:
.. image:: assets/rod_end.png
.. literalinclude:: rod_end.py
+ :language: build123d
: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
diff --git a/docs/key_concepts_algebra.rst b/docs/key_concepts_algebra.rst
index 655b6c7..76f876a 100644
--- a/docs/key_concepts_algebra.rst
+++ b/docs/key_concepts_algebra.rst
@@ -12,26 +12,26 @@ Object arithmetic
- Creating a box and a cylinder centered at ``(0, 0, 0)``
- .. code-block:: python
+ .. code-block:: build123d
b = Box(1, 2, 3)
c = Cylinder(0.2, 5)
- Fusing a box and a cylinder
- .. code-block:: python
+ .. code-block:: build123d
r = Box(1, 2, 3) + Cylinder(0.2, 5)
- Cutting a cylinder from a box
- .. code-block:: python
+ .. code-block:: build123d
r = Box(1, 2, 3) - Cylinder(0.2, 5)
- Intersecting a box and a cylinder
- .. code-block:: python
+ .. code-block:: build123d
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:
- .. code-block:: python
+ .. code-block:: build123d
plane * 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``
(the location is relative to the local coordinate system of the plane).
- .. code-block:: python
+ .. code-block:: build123d
plane * location * alg_compound
@@ -73,7 +73,7 @@ Examples:
- 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)
@@ -84,7 +84,7 @@ Examples:
- 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)
@@ -96,21 +96,21 @@ Examples:
- Box on plane ``Plane.XZ``:
- .. code-block:: python
+ .. code-block:: build123d
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.,
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)
- 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:
- .. code-block:: python
+ .. code-block:: build123d
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
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)
@@ -131,7 +131,7 @@ Combing both concepts
**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)
diff --git a/docs/key_concepts_builder.rst b/docs/key_concepts_builder.rst
index 20370f3..076882d 100644
--- a/docs/key_concepts_builder.rst
+++ b/docs/key_concepts_builder.rst
@@ -61,7 +61,7 @@ Example Workflow
Here is an example of using a Builder to create a simple part:
-.. code-block:: python
+.. code-block:: build123d
from build123d import *
@@ -117,21 +117,21 @@ class for further processing.
One can access the objects created by these builders by referencing the appropriate
instance variable. For example:
-.. code-block:: python
+.. code-block:: build123d
with BuildPart() as my_part:
...
show_object(my_part.part)
-.. code-block:: python
+.. code-block:: build123d
with BuildSketch() as my_sketch:
...
show_object(my_sketch.sketch)
-.. code-block:: python
+.. code-block:: build123d
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
objects or operations that impact that builder like this:
-.. code-block:: python
+.. code-block:: build123d
with BuildPart() as part_builder:
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
information - as follows:
-.. code-block:: python
+.. code-block:: build123d
with BuildPart() as part_builder:
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.
-.. code-block:: python
+.. code-block:: build123d
with BuildPart(Plane.XY) as example:
... # 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
rotated boxes.
-.. code-block:: python
+.. code-block:: build123d
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
more locations are active within a scope. For example:
-.. code-block:: python
+.. code-block:: build123d
with BuildPart():
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
nested. It's easy for a user to retrieve the global locations:
-.. code-block:: python
+.. code-block:: build123d
with Locations(Plane.XY, Plane.XZ):
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:
-.. code-block:: python
+.. code-block:: build123d
def fillet(
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
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:
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
``Mode`` Enum class as follows:
-.. code-block:: python
+.. code-block:: build123d
class Mode(Enum):
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
will be created at the origin of the given workplane such that:
-.. code-block:: python
+.. code-block:: build123d
with BuildPart() as pipes:
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
objects as follows:
-.. code-block:: python
+.. code-block:: build123d
with BuildPart() as pipes:
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
there was one. Here is an example:
-.. code-block:: python
+.. code-block:: build123d
height, width, thickness, f_rad = 60, 80, 20, 10
diff --git a/docs/location_arithmetic.rst b/docs/location_arithmetic.rst
index f26834e..d5c4b0e 100644
--- a/docs/location_arithmetic.rst
+++ b/docs/location_arithmetic.rst
@@ -9,7 +9,7 @@ Position a shape relative to the XY plane
For the following use the helper function:
-.. code-block:: python
+.. code-block:: build123d
def location_symbol(location: Location, scale: float = 1) -> Compound:
return Compound.make_triad(axes_scale=scale).locate(location)
@@ -22,7 +22,7 @@ For the following use the helper function:
1. **Positioning at a location**
- .. code-block:: python
+ .. code-block:: build123d
loc = Location((0.1, 0.2, 0.3), (10, 20, 30))
@@ -35,7 +35,7 @@ For the following use the helper function:
2) **Positioning on a plane**
- .. code-block:: python
+ .. code-block:: build123d
plane = Plane.XZ
@@ -54,7 +54,7 @@ Relative positioning to a 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))
@@ -77,7 +77,7 @@ Relative positioning to a 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))
@@ -96,7 +96,7 @@ Relative positioning to a plane
More general:
- .. code-block:: python
+ .. code-block:: build123d
loc = Location((0.1, 0.2, 0.3), (10, 20, 30))
@@ -114,7 +114,7 @@ Relative positioning to a plane
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))
@@ -133,7 +133,7 @@ Relative positioning to a plane
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))
diff --git a/docs/moving_objects.rst b/docs/moving_objects.rst
index 088973c..b36dde4 100644
--- a/docs/moving_objects.rst
+++ b/docs/moving_objects.rst
@@ -22,7 +22,7 @@ construction process. The following tools are commonly used to specify locations
Example:
-.. code-block:: python
+.. code-block:: build123d
with Locations((10, 20, 30)):
Box(5, 5, 5)
@@ -42,7 +42,7 @@ an existing one.
Example:
-.. code-block:: python
+.. code-block:: build123d
rotated_box = Rotation(45, 0, 0) * box
@@ -55,13 +55,13 @@ Position
^^^^^^^^
- **Absolute Position:** Set the position directly.
-.. code-block:: python
+.. code-block:: build123d
shape.position = (x, y, z)
- **Relative Position:** Adjust the position incrementally.
-.. code-block:: python
+.. code-block:: build123d
shape.position += (x, y, z)
shape.position -= (x, y, z)
@@ -71,13 +71,13 @@ Orientation
^^^^^^^^^^^
- **Absolute Orientation:** Set the orientation directly.
-.. code-block:: python
+.. code-block:: build123d
shape.orientation = (X, Y, Z)
- **Relative Orientation:** Adjust the orientation incrementally.
-.. code-block:: python
+.. code-block:: build123d
shape.orientation += (X, Y, Z)
shape.orientation -= (X, Y, Z)
@@ -86,25 +86,25 @@ Movement Methods
^^^^^^^^^^^^^^^^
- **Relative Move:**
-.. code-block:: python
+.. code-block:: build123d
shape.move(Location)
- **Relative Move of Copy:**
-.. code-block:: python
+.. code-block:: build123d
relocated_shape = shape.moved(Location)
- **Absolute Move:**
-.. code-block:: python
+.. code-block:: build123d
shape.locate(Location)
- **Absolute Move of Copy:**
-.. code-block:: python
+.. code-block:: build123d
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.
-.. code-block:: python
+.. code-block:: build123d
relocated_shape = shape.translate(x, y, z)
- **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)
diff --git a/docs/objects.rst b/docs/objects.rst
index 0cff926..395d81d 100644
--- a/docs/objects.rst
+++ b/docs/objects.rst
@@ -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
are positioned with the ``*`` operator and shown in these examples:
-.. code-block:: python
+.. code-block:: build123d
with BuildPart() as disk:
with BuildSketch():
@@ -18,7 +18,7 @@ are positioned with the ``*`` operator and shown in these examples:
Circle(d, mode=Mode.SUBTRACT)
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)
disk = extrude(sketch, c)
@@ -36,7 +36,7 @@ right or left of each Axis. The following diagram shows how this alignment works
For example:
-.. code-block:: python
+.. code-block:: build123d
with BuildSketch():
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 -
as shown here:
-.. code-block:: python
+.. code-block:: build123d
with BuildSketch():
Circle(1, align=Align.MIN)
@@ -511,6 +511,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>`):
.. literalinclude:: ../examples/playing_cards.py
+ :language: build123d
:start-after: [Club]
:end-before: [Club]
diff --git a/docs/operations.rst b/docs/operations.rst
index e7532b6..8dedac9 100644
--- a/docs/operations.rst
+++ b/docs/operations.rst
@@ -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:
-.. code-block:: python
+.. code-block:: build123d
with BuildPart() as cylinder:
with BuildSketch():
Circle(radius)
extrude(amount=height)
-.. code-block:: python
+.. code-block:: build123d
cylinder = extrude(Circle(radius), amount=height)
diff --git a/docs/selectors.rst b/docs/selectors.rst
index 189b367..ca41f9b 100644
--- a/docs/selectors.rst
+++ b/docs/selectors.rst
@@ -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
sorts and filters. Here is an example of a custom filters:
-.. code-block:: python
+.. code-block:: build123d
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
selectors as shown in this example:
-.. code-block:: python
+.. code-block:: build123d
obj = Box(1, 1, 1) - Cylinder(0.2, 1)
faces_with_holes = obj.faces().filter_by(lambda f: f.inner_wires())
diff --git a/docs/tech_drawing_tutorial.rst b/docs/tech_drawing_tutorial.rst
index 227caab..b4f9db6 100644
--- a/docs/tech_drawing_tutorial.rst
+++ b/docs/tech_drawing_tutorial.rst
@@ -4,14 +4,14 @@
Technical Drawing Tutorial
##########################
-This example demonstrates how to generate a standard technical drawing of a 3D part
-using `build123d`. It creates orthographic and isometric views of a Nema 23 stepper
+This example demonstrates how to generate a standard technical drawing of a 3D part
+using `build123d`. It creates orthographic and isometric views of a Nema 23 stepper
motor and exports the result as an SVG file suitable for printing or inspection.
Overview
--------
-A technical drawing represents a 3D object in 2D using a series of standardized views.
+A technical drawing represents a 3D object in 2D using a series of standardized views.
These include:
- **Plan (Top View)** – as seen from directly above (Z-axis down)
@@ -24,8 +24,8 @@ Each view is aligned to a position on the page and optionally scaled or annotate
How It Works
------------
-The script uses the `project_to_viewport` method to project the 3D part geometry into 2D.
-A helper function, `project_to_2d`, sets up the viewport (camera origin and up direction)
+The script uses the `project_to_viewport` method to project the 3D part geometry into 2D.
+A helper function, `project_to_2d`, sets up the viewport (camera origin and up direction)
and places the result onto a virtual drawing sheet.
The steps involved are:
@@ -34,7 +34,7 @@ The steps involved are:
2. Define a `TechnicalDrawing` border and title block using A4 page size.
3. Generate each of the standard views and apply transformations to place them.
4. Add dimensions using `ExtensionLine` and labels using `Text`.
-5. Export the drawing using `ExportSVG`, separating visible and hidden edges by layer
+5. Export the drawing using `ExportSVG`, separating visible and hidden edges by layer
and style.
Result
@@ -59,7 +59,7 @@ Code
----
.. literalinclude:: technical_drawing.py
- :language: python
+ :language: build123d
:start-after: [code]
:end-before: [end]
diff --git a/docs/tips.rst b/docs/tips.rst
index ad5c299..2567088 100644
--- a/docs/tips.rst
+++ b/docs/tips.rst
@@ -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
belong to then select the edges as shown here:
-.. code-block:: python
+.. code-block:: build123d
from build123d import *
@@ -118,7 +118,7 @@ a common OpenCascade Python wrapper (`OCP `_) i
interchange objects both from CadQuery to build123d and vice-versa by transferring the ``wrapped``
objects as follows (first from CadQuery to build123d):
-.. code-block:: python
+.. code-block:: build123d
import build123d as b3d
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:
-.. code-block:: python
+.. code-block:: build123d
import build123d as b3d
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
on the workplane / coordinate system provided. For example:
-.. code-block:: python
+.. code-block:: build123d
with BuildSketch(Plane.XZ) as vertical_sketch:
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``
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:
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
is a common issue:
-.. code-block:: python
+.. code-block:: build123d
with BuildSketch() as sketch:
with BuildLine(Plane.XZ):
diff --git a/docs/topology_selection.rst b/docs/topology_selection.rst
index f1ef50e..694c75f 100644
--- a/docs/topology_selection.rst
+++ b/docs/topology_selection.rst
@@ -40,7 +40,7 @@ Overview
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.
-.. code-block:: python
+.. code-block:: build123d
# In 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!
- .. code-block:: python
+ .. code-block:: build123d
# In 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
``Select.ALL``. Specifying ``Select.ALL`` for the selector is not required.
-.. code-block:: python
+.. code-block:: build123d
with BuildPart() as part:
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``.
-.. code-block:: python
+.. code-block:: build123d
with BuildPart() as part:
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
available for a ``ShapeList`` of edges!
-.. code-block:: python
+.. code-block:: build123d
with BuildPart() as part:
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
edges are reused?
-.. code-block:: python
+.. code-block:: build123d
with BuildPart() as part:
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
``Select.NEW``.
- .. code-block:: python
+ .. code-block:: build123d
with BuildPart() as part:
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
Algebra Mode where ``Select.NEW`` is unavailable
-.. code-block:: python
+.. code-block:: build123d
box = Box(5, 5, 1)
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
comparing the object before the operation to the "combined" object.
-.. code-block:: python
+.. code-block:: build123d
box = Box(5, 5, 1)
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
subclass of ``list``, so any list slice can be used.
-.. code-block:: python
+.. code-block:: build123d
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
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())
@@ -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
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))
diff --git a/docs/topology_selection/filter_examples.rst b/docs/topology_selection/filter_examples.rst
index f7233b8..0b0f3fc 100644
--- a/docs/topology_selection/filter_examples.rst
+++ b/docs/topology_selection/filter_examples.rst
@@ -18,11 +18,11 @@ operations, and are sometimes necessary e.g. before sorting or filtering by radi
.. dropdown:: Setup
.. literalinclude:: examples/filter_geomtype.py
- :language: python
+ :language: build123d
:lines: 3, 8-13
.. literalinclude:: examples/filter_geomtype.py
- :language: python
+ :language: build123d
:lines: 15
.. 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
- :language: python
+ :language: build123d
:lines: 17
.. 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
.. literalinclude:: examples/filter_all_edges_circle.py
- :language: python
+ :language: build123d
:lines: 3, 8-41
.. literalinclude:: examples/filter_all_edges_circle.py
- :language: python
+ :language: build123d
:lines: 43-47
.. figure:: ../assets/topology_selection/filter_all_edges_circle.png
@@ -74,14 +74,14 @@ Plane will select faces parallel to the plane.
.. dropdown:: Setup
- .. code-block:: python
+ .. code-block:: build123d
from build123d import *
with BuildPart() as part:
Box(1, 1, 1)
-.. code-block:: python
+.. code-block:: build123d
part.faces().filter_by(Axis.Z)
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
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(Plane.XY.z_dir)) < 1e-6)
@@ -122,11 +122,11 @@ and then filtering for the specific inner wire by radius.
.. dropdown:: Setup
.. literalinclude:: examples/filter_inner_wire_count.py
- :language: python
+ :language: build123d
:lines: 4, 9-16
.. literalinclude:: examples/filter_inner_wire_count.py
- :language: python
+ :language: build123d
:lines: 18-21
.. 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.
.. literalinclude:: examples/filter_inner_wire_count.py
- :language: python
+ :language: build123d
:lines: 25-32
.. figure:: ../assets/topology_selection/filter_inner_wire_count_linear.png
@@ -163,11 +163,11 @@ any line edges.
.. dropdown:: Setup
.. literalinclude:: examples/filter_nested.py
- :language: python
+ :language: build123d
:lines: 4, 9-22
.. literalinclude:: examples/filter_nested.py
- :language: python
+ :language: build123d
:lines: 26-32
.. 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.
.. literalinclude:: examples/filter_shape_properties.py
- :language: python
+ :language: build123d
:lines: 3-4, 8-22
.. figure:: ../assets/topology_selection/filter_shape_properties.png
diff --git a/docs/topology_selection/group_examples.rst b/docs/topology_selection/group_examples.rst
index 3f0057b..d91de21 100644
--- a/docs/topology_selection/group_examples.rst
+++ b/docs/topology_selection/group_examples.rst
@@ -14,7 +14,7 @@ result knowing how many edges to expect.
.. dropdown:: Setup
.. literalinclude:: examples/group_axis.py
- :language: python
+ :language: build123d
:lines: 4, 9-17
.. 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.
.. literalinclude:: examples/group_axis.py
- :language: python
+ :language: build123d
:lines: 21-22
.. figure:: ../assets/topology_selection/group_axis_with.png
@@ -46,11 +46,11 @@ with the largest hole.
.. dropdown:: Setup
.. literalinclude:: examples/group_hole_area.py
- :language: python
+ :language: build123d
:lines: 4, 9-17
.. literalinclude:: examples/group_hole_area.py
- :language: python
+ :language: build123d
:lines: 21-24
.. 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
.. literalinclude:: examples/group_properties_with_keys.py
- :language: python
+ :language: build123d
:lines: 4, 9-26
.. literalinclude:: examples/group_properties_with_keys.py
- :language: python
+ :language: build123d
:lines: 30, 31
.. 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
.. literalinclude:: examples/group_properties_with_keys.py
- :language: python
+ :language: build123d
:lines: 35-43
.. literalinclude:: examples/group_properties_with_keys.py
- :language: python
+ :language: build123d
:lines: 47-50
.. 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
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)
bearing_edges = radius_groups.filter_by(lambda e: e.radius == 8)
diff --git a/docs/topology_selection/sort_examples.rst b/docs/topology_selection/sort_examples.rst
index a4779fc..ecdbf96 100644
--- a/docs/topology_selection/sort_examples.rst
+++ b/docs/topology_selection/sort_examples.rst
@@ -23,11 +23,11 @@ be used with``group_by``.
.. dropdown:: Setup
.. literalinclude:: examples/sort_sortby.py
- :language: python
+ :language: build123d
:lines: 3, 8-13
.. literalinclude:: examples/sort_sortby.py
- :language: python
+ :language: build123d
:lines: 19-22
.. figure:: ../assets/topology_selection/sort_sortby_length.png
@@ -36,7 +36,7 @@ be used with``group_by``.
|
.. literalinclude:: examples/sort_sortby.py
- :language: python
+ :language: build123d
:lines: 24-27
.. figure:: ../assets/topology_selection/sort_sortby_distance.png
@@ -57,11 +57,11 @@ the order is random.
.. dropdown:: Setup
.. literalinclude:: examples/sort_along_wire.py
- :language: python
+ :language: build123d
:lines: 3, 8-12
.. literalinclude:: examples/sort_along_wire.py
- :language: python
+ :language: build123d
:lines: 14-15
.. 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.
.. literalinclude:: examples/sort_along_wire.py
- :language: python
+ :language: build123d
:lines: 26-28
.. figure:: ../assets/topology_selection/sort_along_wire.png
@@ -94,11 +94,11 @@ edge can be found sorting along y-axis.
.. dropdown:: Setup
.. literalinclude:: examples/sort_axis.py
- :language: python
+ :language: build123d
:lines: 4, 9-18
.. literalinclude:: examples/sort_axis.py
- :language: python
+ :language: build123d
:lines: 22-24
.. 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
.. literalinclude:: examples/sort_distance_from.py
- :language: python
+ :language: build123d
:lines: 2-5, 9-13
.. literalinclude:: examples/sort_distance_from.py
- :language: python
+ :language: build123d
:lines: 15-16
.. 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.
.. literalinclude:: examples/sort_distance_from.py
- :language: python
+ :language: build123d
:lines: 19-20
.. figure:: ../assets/topology_selection/sort_distance_from_largest.png
diff --git a/docs/tttt.rst b/docs/tttt.rst
index 8379142..1c1f75f 100644
--- a/docs/tttt.rst
+++ b/docs/tttt.rst
@@ -98,6 +98,7 @@ Party Pack 01-01 Bearing Bracket
.. dropdown:: Reference Implementation
.. literalinclude:: assets/ttt/ttt-ppp0101.py
+ :language: build123d
.. _ttt-ppp0102:
@@ -114,6 +115,7 @@ Party Pack 01-02 Post Cap
.. dropdown:: Reference Implementation
.. literalinclude:: assets/ttt/ttt-ppp0102.py
+ :language: build123d
.. _ttt-ppp0103:
@@ -129,6 +131,7 @@ Party Pack 01-03 C Clamp Base
.. dropdown:: Reference Implementation
.. literalinclude:: assets/ttt/ttt-ppp0103.py
+ :language: build123d
.. _ttt-ppp0104:
@@ -144,6 +147,7 @@ Party Pack 01-04 Angle Bracket
.. dropdown:: Reference Implementation
.. literalinclude:: assets/ttt/ttt-ppp0104.py
+ :language: build123d
.. _ttt-ppp0105:
@@ -159,6 +163,7 @@ Party Pack 01-05 Paste Sleeve
.. dropdown:: Reference Implementation
.. literalinclude:: assets/ttt/ttt-ppp0105.py
+ :language: build123d
.. _ttt-ppp0106:
@@ -174,6 +179,7 @@ Party Pack 01-06 Bearing Jig
.. dropdown:: Reference Implementation
.. literalinclude:: assets/ttt/ttt-ppp0106.py
+ :language: build123d
.. _ttt-ppp0107:
@@ -189,6 +195,7 @@ Party Pack 01-07 Flanged Hub
.. dropdown:: Reference Implementation
.. literalinclude:: assets/ttt/ttt-ppp0107.py
+ :language: build123d
.. _ttt-ppp0108:
@@ -204,6 +211,7 @@ Party Pack 01-08 Tie Plate
.. dropdown:: Reference Implementation
.. literalinclude:: assets/ttt/ttt-ppp0108.py
+ :language: build123d
.. _ttt-ppp0109:
@@ -219,6 +227,7 @@ Party Pack 01-09 Corner Tie
.. dropdown:: Reference Implementation
.. literalinclude:: assets/ttt/ttt-ppp0109.py
+ :language: build123d
.. _ttt-ppp0110:
@@ -234,6 +243,7 @@ Party Pack 01-10 Light Cap
.. dropdown:: Reference Implementation
.. literalinclude:: assets/ttt/ttt-ppp0110.py
+ :language: build123d
.. _ttt-23-02-02-sm_hanger:
@@ -249,6 +259,7 @@ Party Pack 01-10 Light Cap
.. dropdown:: Reference Implementation
.. literalinclude:: assets/ttt/ttt-23-02-02-sm_hanger.py
+ :language: build123d
.. _ttt-23-t-24:
@@ -265,6 +276,7 @@ Party Pack 01-10 Light Cap
.. dropdown:: Reference Implementation
.. literalinclude:: assets/ttt/ttt-23-t-24-curved_support.py
+ :language: build123d
.. _ttt-24-spo-06:
@@ -281,3 +293,4 @@ Party Pack 01-10 Light Cap
.. dropdown:: Reference Implementation
.. literalinclude:: assets/ttt/ttt-24-SPO-06-Buffer_Stand.py
+ :language: build123d
diff --git a/docs/tutorial_design.rst b/docs/tutorial_design.rst
index 3950399..f16afbc 100644
--- a/docs/tutorial_design.rst
+++ b/docs/tutorial_design.rst
@@ -4,8 +4,8 @@
Designing a Part in build123d
#############################
-Designing a part with build123d involves a systematic approach that leverages the power
-of 2D profiles, extrusions, and revolutions. Where possible, always work in the lowest
+Designing a part with build123d involves a systematic approach that leverages the power
+of 2D profiles, extrusions, and revolutions. Where possible, always work in the lowest
possible dimension, 1D lines before 2D sketches before 3D parts. The following guide will
get you started:
@@ -18,8 +18,8 @@ get you started:
Step 1. Examine the Part in All Three Orientations
**************************************************
-Start by visualizing the part from the front, top, and side views. Identify any symmetries
-in these orientations, as symmetries can simplify the design by reducing the number of
+Start by visualizing the part from the front, top, and side views. Identify any symmetries
+in these orientations, as symmetries can simplify the design by reducing the number of
unique features you need to model.
*In the following view of the bracket one can see two planes of symmetry
@@ -31,8 +31,8 @@ so we'll only need to design one quarter of it.*
Step 2. Identify Rotational Symmetries
**************************************
-Look for structures that could be created through the rotation of a 2D shape. For instance,
-cylindrical or spherical features are often the result of revolving a profile around an axis.
+Look for structures that could be created through the rotation of a 2D shape. For instance,
+cylindrical or spherical features are often the result of revolving a profile around an axis.
Identify the axis of rotation and make a note of it.
*There are no rotational structures in the example bracket.*
@@ -40,17 +40,17 @@ Identify the axis of rotation and make a note of it.
Step 3. Select a Convenient Origin
**********************************
-Choose an origin point that minimizes the need to move or transform components later in the
-design process. Ideally, the origin should be placed at a natural center of symmetry or a
+Choose an origin point that minimizes the need to move or transform components later in the
+design process. Ideally, the origin should be placed at a natural center of symmetry or a
critical reference point on the part.
-*The planes of symmetry for the bracket was identified in step 1, making it logical to
-place the origin at the intersection of these planes on the bracket's front face. Additionally,
-we'll define the coordinate system we'll be working in: Plane.XY (the default), where
-the origin is set at the global (0,0,0) position. In this system, the x-axis aligns with
-the front of the bracket, and the z-axis corresponds to its width. It’s important to note
+*The planes of symmetry for the bracket was identified in step 1, making it logical to
+place the origin at the intersection of these planes on the bracket's front face. Additionally,
+we'll define the coordinate system we'll be working in: Plane.XY (the default), where
+the origin is set at the global (0,0,0) position. In this system, the x-axis aligns with
+the front of the bracket, and the z-axis corresponds to its width. It’s important to note
that all coordinate systems/planes in build123d adhere to the*
-`right-hand rule `_ *meaning the y-axis is
+`right-hand rule `_ *meaning the y-axis is
automatically determined by this convention.*
.. image:: assets/bracket_with_origin.png
@@ -58,18 +58,18 @@ automatically determined by this convention.*
Step 4. Create 2D Profiles
**************************
-Design the 2D profiles of your part in the appropriate orientation(s). These profiles are
-the foundation of the part's geometry and can often represent cross-sections of the part.
+Design the 2D profiles of your part in the appropriate orientation(s). These profiles are
+the foundation of the part's geometry and can often represent cross-sections of the part.
Mirror parts of profiles across any axes of symmetry identified earlier.
*The 2D profile of the bracket is as follows:*
.. image:: assets/bracket_sketch.png
:align: center
-
+
*The build123d code to generate this profile is as follows:*
-.. code-block:: python
+.. code-block:: build123d
with BuildSketch() as sketch:
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
a part by extruding it as shown in this code:*
-.. code-block:: python
+.. code-block:: build123d
with BuildPart() as bracket:
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
code block, captures just these edges:*
-.. code-block:: python
+.. code-block:: build123d
corners = bracket.edges().filter_by(Axis.X).group_by(Axis.Y)[-1]
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
the two circular holes:*
-.. code-block:: python
+.. code-block:: build123d
with Locations(bracket.faces().sort_by(Axis.X)[-1]):
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
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]):
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:*
-.. code-block:: python
+.. code-block:: build123d
thickness = 3 * 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:*
-.. code-block:: python
+.. code-block:: build123d
from build123d import *
from ocp_vscode import show_all
diff --git a/docs/tutorial_joints.rst b/docs/tutorial_joints.rst
index 47a4026..d7c7658 100644
--- a/docs/tutorial_joints.rst
+++ b/docs/tutorial_joints.rst
@@ -19,6 +19,7 @@ Before getting to the CAD operations, this selector script needs to import the b
environment.
.. literalinclude:: tutorial_joints.py
+ :language: build123d
:start-after: [import]
: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.
.. literalinclude:: tutorial_joints.py
+ :language: build123d
:start-after: [Hinge Class]
: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.
.. literalinclude:: tutorial_joints.py
+ :language: build123d
:start-after: [Create the Joints]
: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.
.. literalinclude:: tutorial_joints.py
+ :language: build123d
:start-after: [Create the Joints]
:end-before: [Fastener holes]
: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.
.. literalinclude:: tutorial_joints.py
+ :language: build123d
:start-after: [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:
.. literalinclude:: tutorial_joints.py
+ :language: build123d
:start-after: [End Fastener holes]
: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.
.. literalinclude:: tutorial_joints.py
+ :language: build123d
:start-after: [Create instances of the two leaves of 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
.. literalinclude:: tutorial_joints.py
+ :language: build123d
: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]
:emphasize-lines: 13-16
@@ -157,6 +165,7 @@ having to recreate or modify :class:`~topology.Joint`'s. Here is the box is move
property.
.. literalinclude:: tutorial_joints.py
+ :language: build123d
:start-after: [Demonstrate that objects with Joints can be moved and the joints follow]
: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
.. literalinclude:: tutorial_joints.py
+ :language: build123d
:start-after: [The lid with a RigidJoint for the hinge]
:end-before: [A screw to attach the hinge to the box]
:emphasize-lines: 6-9
@@ -191,6 +201,7 @@ screw.
:align: center
.. literalinclude:: tutorial_joints.py
+ :language: build123d
:start-after: [A screw to attach the hinge to the box]
: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:
.. literalinclude:: tutorial_joints.py
+ :language: build123d
:start-after: [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.
.. literalinclude:: tutorial_joints.py
+ :language: build123d
:start-after: [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``:
.. literalinclude:: tutorial_joints.py
+ :language: build123d
:start-after: [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:
.. literalinclude:: tutorial_joints.py
+ :language: build123d
:start-after: [Connect Screw to Hole]
:end-before: [Connect Screw to Hole]
diff --git a/docs/tutorial_lego.rst b/docs/tutorial_lego.rst
index 0ac2ab4..fdd02de 100644
--- a/docs/tutorial_lego.rst
+++ b/docs/tutorial_lego.rst
@@ -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.
.. literalinclude:: ../examples/lego.py
+ :language: build123d
: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``.
.. literalinclude:: ../examples/lego.py
+ :language: build123d
: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:
.. literalinclude:: ../examples/lego.py
+ :language: build123d
:lines: 49-51
: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``.
.. literalinclude:: ../examples/lego.py
+ :language: build123d
:lines: 49-53
: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``.
.. literalinclude:: ../examples/lego.py
+ :language: build123d
:lines: 49-53,58-64
: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.
.. literalinclude:: ../examples/lego.py
+ :language: build123d
:lines: 49-53,58-64,69-73
: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``.
.. literalinclude:: ../examples/lego.py
+ :language: build123d
:lines: 49-53,58-64,69-73,78-83
: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``.
.. literalinclude:: ../examples/lego.py
+ :language: build123d
:lines: 49-53,58-64,69-73,78-83,88-93
: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.
.. literalinclude:: ../examples/lego.py
+ :language: build123d
:lines: 49-53,58-64,69-73,78-83,88-93,98-99
: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.
.. literalinclude:: ../examples/lego.py
+ :language: build123d
:lines: 49-53,58-64,69-73,78-83,88-93,98-99,110-118
: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.
.. literalinclude:: ../examples/lego.py
+ :language: build123d
:lines: 49-53,58-64,69-73,78-83,88-93,98-99,110-118,129-137
:emphasize-lines: 41-49
diff --git a/docs/tutorial_selectors.rst b/docs/tutorial_selectors.rst
index ed974f1..f0ee8db 100644
--- a/docs/tutorial_selectors.rst
+++ b/docs/tutorial_selectors.rst
@@ -11,7 +11,7 @@ this part:
.. note::
One can see any object in the following tutorial by using the ``ocp_vscode`` (or
any other supported viewer) by using the ``show(object_to_be_viewed)`` command.
- Alternatively, the ``show_all()`` command will display all objects that have been
+ Alternatively, the ``show_all()`` command will display all objects that have been
assigned an identifier.
*************
@@ -22,6 +22,7 @@ Before getting to the CAD operations, this selector script needs to import the b
environment.
.. literalinclude:: selector_example.py
+ :language: build123d
:start-after: [Code]
:end-before: [End]
: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`:
.. literalinclude:: selector_example.py
+ :language: build123d
:start-after: [Code]
:end-before: [End]
: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:
.. literalinclude:: selector_example.py
+ :language: build123d
:start-after: [Code]
:end-before: [End]
: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.
.. literalinclude:: selector_example.py
+ :language: build123d
:start-after: [Code]
:end-before: [End]
: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.
.. literalinclude:: selector_example.py
+ :language: build123d
:start-after: [Code]
:end-before: [End]
: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.
.. literalinclude:: selector_example.py
+ :language: build123d
:start-after: [Code]
:end-before: [End]
:lines: 1-9,18-24,33-34
diff --git a/docs/tutorial_surface_modeling.rst b/docs/tutorial_surface_modeling.rst
index 08ed253..79aeada 100644
--- a/docs/tutorial_surface_modeling.rst
+++ b/docs/tutorial_surface_modeling.rst
@@ -31,7 +31,7 @@ the perimeter of the surface and a central point on that surface.
To create the perimeter, we'll use a ``BuildLine`` instance as follows. Since the heart is
symmetric, we'll only create half of its surface here:
-.. code-block:: python
+.. code-block:: build123d
with BuildLine() as heart_half:
l1 = JernArc((0, 0), (1, 1.4), 40, -17)
@@ -48,13 +48,13 @@ of the heart and archs up off ``Plane.XY``.
In preparation for creating the surface, we'll define a point on the surface:
-.. code-block:: python
+.. code-block:: build123d
surface_pnt = l2.edge().arc_center + Vector(0, 0, 1.5)
We will then use this point to create a non-planar ``Face``:
-.. code-block:: python
+.. code-block:: build123d
top_right_surface = -Face.make_surface(heart_half.wire(), [surface_pnt]).locate(
Pos(Z=0.5)
@@ -71,7 +71,7 @@ is up, which isn't necessary but helps with viewing.
Now that one half of the top of the heart has been created, the remainder of the top
and bottom can be created by mirroring:
-.. code-block:: python
+.. code-block:: build123d
top_left_surface = top_right_surface.mirror(Plane.YZ)
bottom_right_surface = top_right_surface.mirror(Plane.XY)
@@ -80,7 +80,7 @@ and bottom can be created by mirroring:
The sides of the heart are going to be created by extruding the outside of the perimeter
as follows:
-.. code-block:: python
+.. code-block:: build123d
left_wire = Wire([l3.edge(), l2.edge(), l1.edge()])
left_side = Face.extrude(left_wire, (0, 0, 1)).locate(Pos(Z=-0.5))
@@ -94,7 +94,7 @@ With the top, bottom, and sides, the complete boundary of the object is defined.
now put them together, first into a :class:`~topology.Shell` and then into a
:class:`~topology.Solid`:
-.. code-block:: python
+.. code-block:: build123d
heart = Solid(
Shell(
@@ -122,7 +122,7 @@ now put them together, first into a :class:`~topology.Shell` and then into a
Finally, we'll create the frame around the heart as a simple extrusion of a planar
shape defined by the perimeter of the heart and merge all of the components together:
- .. code-block:: python
+ .. code-block:: build123d
with BuildPart() as heart_token:
with BuildSketch() as outline:
From f4c79db263d42ec372328cb42fccdc9ef4da2622 Mon Sep 17 00:00:00 2001
From: Jonathan Wagenet
Date: Wed, 24 Sep 2025 23:16:35 -0400
Subject: [PATCH 003/105] Change kwarg capitalization to fix #1026. Unindent
code blocks, fix doublespace + formatting
---
docs/location_arithmetic.rst | 136 +++++++++++++++++------------------
1 file changed, 66 insertions(+), 70 deletions(-)
diff --git a/docs/location_arithmetic.rst b/docs/location_arithmetic.rst
index f26834e..a28c105 100644
--- a/docs/location_arithmetic.rst
+++ b/docs/location_arithmetic.rst
@@ -3,7 +3,6 @@
Location arithmetic for algebra mode
======================================
-
Position a shape relative to the XY plane
---------------------------------------------
@@ -19,134 +18,131 @@ For the following use the helper function:
circle = Circle(scale * .8).edge()
return (triad + circle).locate(plane.location)
-
1. **Positioning at a location**
- .. code-block:: python
+.. code-block:: python
- 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(location_symbol(loc), name="location")
+ show_object(face, name="face")
+ show_object(location_symbol(loc), name="location")
- .. image:: assets/location-example-01.png
+.. image:: assets/location-example-01.png
2) **Positioning on a plane**
- .. code-block:: python
+.. code-block:: python
- plane = Plane.XZ
+ plane = Plane.XZ
- face = plane * Rectangle(1, 2)
+ face = plane * Rectangle(1, 2)
- show_object(face, name="face")
- show_object(plane_symbol(plane), name="plane")
+ show_object(face, name="face")
+ show_object(plane_symbol(plane), name="plane")
- .. 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)
+.. image:: assets/location-example-07.png
+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
------------------------------------
1. **Position an object on a plane relative to the plane**
- .. code-block:: python
+.. code-block:: python
- 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(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 = 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 = loc * Pos(0.2, 0.4, 0.1) * Box(0.2, 0.2, 0.2)
- show_object(face, name="face")
- show_object(location_symbol(loc), name="location")
- show_object(box, name="box")
+ show_object(face, name="face")
+ show_object(location_symbol(loc), name="location")
+ 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
- ``z``-axis of the underlying location ``loc``.
+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``.
- 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**
- .. code-block:: python
+.. code-block:: python
- 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(location_symbol(loc), name="location")
- show_object(box, name="box")
+ show_object(face, name="face")
+ show_object(location_symbol(loc), name="location")
+ 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
- (and not of the z-axis of the world).
+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).
- More general:
+More general:
- .. code-block:: python
+.. code-block:: python
- 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(location_symbol(loc), name="location")
- show_object(box, name="box")
+ show_object(face, name="face")
+ show_object(location_symbol(loc), name="location")
+ 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**
- .. code-block:: python
+.. code-block:: python
- 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(location_symbol(loc), name="location")
- 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(face, name="face")
+ show_object(location_symbol(loc), name="location")
+ show_object(box, name="box")
+ 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**
- .. code-block:: python
+.. code-block:: python
- 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(location_symbol(loc), name="location")
- 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(face, name="face")
+ show_object(location_symbol(loc), name="location")
+ 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")
- .. 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)`
+.. 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)``
From bb9495a821970feaa1b6bbdf403d570e58d024cd Mon Sep 17 00:00:00 2001
From: Jonathan Wagenet
Date: Wed, 24 Sep 2025 23:28:22 -0400
Subject: [PATCH 004/105] Reorder mirror / make_face bot best practice to
resolve #1053
---
docs/assets/ttt/ttt-ppp0106.py | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/docs/assets/ttt/ttt-ppp0106.py b/docs/assets/ttt/ttt-ppp0106.py
index 596a47b..abd6751 100644
--- a/docs/assets/ttt/ttt-ppp0106.py
+++ b/docs/assets/ttt/ttt-ppp0106.py
@@ -21,8 +21,8 @@ with BuildSketch(Location((0, -r1, y3))) as sk_body:
m3 = IntersectingLine(m2 @ 1, m2 % 1, c1)
m4 = Line(m3 @ 1, (r1, r1))
m5 = JernArc(m4 @ 1, m4 % 1, r1, -90)
- m6 = Line(m5 @ 1, m1 @ 0)
- mirror(make_face(l.line), Plane.YZ)
+ mirror(about=Plane.YZ)
+ make_face()
fillet(sk_body.vertices().group_by(Axis.Y)[1], 12)
with Locations((x1 / 2, y_tot - 10), (-x1 / 2, y_tot - 10)):
Circle(r2, mode=Mode.SUBTRACT)
From 640b5300583151eb43860690e46f96983ad0d885 Mon Sep 17 00:00:00 2001
From: Jonathan Wagenet
Date: Wed, 24 Sep 2025 23:48:46 -0400
Subject: [PATCH 005/105] Fix doctrings for sphinx make
---
src/build123d/topology/one_d.py | 20 ++++++++++----------
1 file changed, 10 insertions(+), 10 deletions(-)
diff --git a/src/build123d/topology/one_d.py b/src/build123d/topology/one_d.py
index bac4507..b750697 100644
--- a/src/build123d/topology/one_d.py
+++ b/src/build123d/topology/one_d.py
@@ -531,7 +531,7 @@ class Mixin1D(Shape):
A curvature comb is a set of short line segments (“teeth”) erected
perpendicular to the curve that visualize the signed curvature κ(u).
- Tooth length is proportional to |κ| and the direction encodes the sign
+ Tooth length is proportional to \|κ\| and the direction encodes the sign
(left normal for κ>0, right normal for κ<0). This is useful for inspecting
fairness and continuity (C0/C1/C2) of edges and wires.
@@ -554,7 +554,7 @@ class Mixin1D(Shape):
- On straight segments, κ = 0 so no teeth are drawn.
- At inflection points κ→0 and the tooth flips direction.
- 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:
>>> comb = my_wire.curvature_comb(count=200, max_tooth_size=2.0)
@@ -961,16 +961,16 @@ class Mixin1D(Shape):
The meaning of the returned parameter depends on the type of self:
- **Edge**: Returns the native OCCT curve parameter corresponding to the
- given normalized `position` (0.0 → start, 1.0 → end). For closed/periodic
- edges, OCCT may return a value **outside** the edge's nominal parameter
- 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
- modulo with the parameter span.
+ given normalized `position` (0.0 → start, 1.0 → end). For closed/periodic
+ edges, OCCT may return a value **outside** the edge's nominal parameter
+ 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
+ modulo with the parameter span.
- **Wire**: Returns a *composite* parameter encoding both the edge index
- and the position within that edge: the **integer part** is the zero-based
- count of fully traversed edges, and the **fractional part** is the
- normalized position in `[0.0, 1.0]` along the current edge.
+ and the position within that edge: the **integer part** is the zero-based
+ count of fully traversed edges, and the **fractional part** is the
+ normalized position in `[0.0, 1.0]` along the current edge.
Args:
position (float): Normalized arc-length position along the shape,
From 99da8912df93adfca65b91a180d160aa1bc7c024 Mon Sep 17 00:00:00 2001
From: Jonathan Wagenet
Date: Fri, 17 Oct 2025 11:45:11 -0400
Subject: [PATCH 006/105] Add 2d and 3d intersection tests
---
tests/test_direct_api/test_intersection.py | 131 +++++++++++++++++++++
1 file changed, 131 insertions(+)
diff --git a/tests/test_direct_api/test_intersection.py b/tests/test_direct_api/test_intersection.py
index 6eebc41..6262a23 100644
--- a/tests/test_direct_api/test_intersection.py
+++ b/tests/test_direct_api/test_intersection.py
@@ -152,6 +152,7 @@ def test_shape_0d(obj, target, expected):
run_test(obj, target, expected)
+# 1d Shapes
ed1 = Line((0, 0), (5, 0)).edge()
ed2 = Line((0, -1), (5, 1)).edge()
ed3 = Line((0, 0, 5), (5, 0, 5)).edge()
@@ -220,6 +221,136 @@ def test_shape_1d(obj, target, expected):
run_test(obj, target, expected)
+# 2d Shapes
+fc1 = Rectangle(5, 5).face()
+fc2 = Pos(Z=5) * Rectangle(5, 5).face()
+fc3 = Rot(Y=90) * Rectangle(5, 5).face()
+fc4 = Rot(Z=45) * Rectangle(5, 5).face()
+fc5 = Pos(2.5, 2.5, 2.5) * Rot(0, 90) * Rectangle(5, 5).face()
+fc6 = Pos(2.5, 2.5) * Rot(0, 90, 45, Extrinsic.XYZ) * Rectangle(5, 5).face()
+fc7 = (Rot(90) * Cylinder(2, 4)).faces().filter_by(GeomType.CYLINDER)[0]
+
+fc11 = Rectangle(4, 4).face()
+fc22 = sweep(Rot(90) * CenterArc((0, 0), 2, 0, 180), Line((0, 2), (0, -2)))
+sh1 = Shell([Pos(-4) * fc11, fc22])
+sh2 = Pos(Z=1) * sh1
+sh3 = Shell([Pos(-4) * fc11, fc22, Pos(2, 0, -2) * Rot(0, 90) * fc11])
+sh4 = Shell([Pos(-4) * fc11, fc22, Pos(4) * fc11])
+sh5 = Pos(Z=1) * Shell([Pos(-2, 0, -2) * Rot(0, -90) * fc11, fc22, Pos(2, 0, -2) * Rot(0, 90) * fc11])
+
+shape_2d_matrix = [
+ Case(fc1, vl2, None, "non-coincident", None),
+ Case(fc1, vl1, [Vertex], "coincident", None),
+
+ Case(fc1, lc2, None, "non-coincident", None),
+ Case(fc1, lc1, [Vertex], "coincident", None),
+
+ Case(fc2, ax1, None, "parallel/skew", None),
+ Case(fc3, ax1, [Vertex], "intersecting", None),
+ Case(fc1, ax1, [Edge], "collinear", None),
+ # Case(fc7, ax1, [Vertex, Vertex], "multi intersect", 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(sh5, fc1, [Edge], "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].edge().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", "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),
+]
+
+@pytest.mark.parametrize("obj, target, expected", make_params(shape_3d_matrix))
+def test_shape_3d(obj, target, expected):
+ run_test(obj, target, expected)
+
+
# FreeCAD issue example
c1 = CenterArc((0, 0), 10, 0, 360).edge()
c2 = CenterArc((19, 0), 10, 0, 360).edge()
From c7bf48c80c88320f9160534339740377e280a402 Mon Sep 17 00:00:00 2001
From: Jonathan Wagenet
Date: Mon, 20 Oct 2025 17:59:19 -0400
Subject: [PATCH 007/105] Add intersect methods to Mixin2D and Mixin3D
These methods are very similar using a branching structure to pick intersection method.
---
src/build123d/topology/three_d.py | 131 ++++++++++++++++++++++++++++--
src/build123d/topology/two_d.py | 119 ++++++++++++++++++++++++++-
2 files changed, 243 insertions(+), 7 deletions(-)
diff --git a/src/build123d/topology/three_d.py b/src/build123d/topology/three_d.py
index e4131ce..0331317 100644
--- a/src/build123d/topology/three_d.py
+++ b/src/build123d/topology/three_d.py
@@ -56,13 +56,13 @@ from __future__ import annotations
import platform
import warnings
+from collections.abc import Iterable, Sequence
from math import radians, cos, tan
-from typing import Union, TYPE_CHECKING
-
-from collections.abc import Iterable
+from typing import TYPE_CHECKING
+from typing_extensions import Self
import OCP.TopAbs as ta
-from OCP.BRepAlgoAPI import BRepAlgoAPI_Cut
+from OCP.BRepAlgoAPI import BRepAlgoAPI_Common, BRepAlgoAPI_Cut, BRepAlgoAPI_Section
from OCP.BRepBuilderAPI import BRepBuilderAPI_MakeSolid
from OCP.BRepClass3d import BRepClass3d_SolidClassifier
from OCP.BRepFeat import BRepFeat_MakeDPrism
@@ -95,6 +95,7 @@ from OCP.gp import gp_Ax2, gp_Pnt
from build123d.build_enums import CenterOf, GeomType, Kind, Transition, Until
from build123d.geometry import (
DEG2RAD,
+ TOLERANCE,
Axis,
BoundBox,
Color,
@@ -104,7 +105,6 @@ from build123d.geometry import (
Vector,
VectorLike,
)
-from typing_extensions import Self
from .one_d import Edge, Wire, Mixin1D
from .shape_core import Shape, ShapeList, Joint, downcast, shapetype
@@ -420,6 +420,127 @@ class Mixin3D(Shape):
return return_value
+ def intersect(
+ self, *to_intersect: Shape | Vector | Location | Axis | Plane
+ ) -> None | ShapeList[Vertex | Edge | Face | Shape]:
+ """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 | None:
+ # Wrap Shape._bool_op for corrected output
+ intersections = args[0]._bool_op(args, tools, operation)
+ if isinstance(intersections, ShapeList):
+ return intersections or None
+ if (isinstance(intersections, Shape) and not intersections.is_null):
+ return ShapeList([intersections])
+ return None
+
+ 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] = ShapeList(self.solids())
+ target: ShapeList | Shape
+ for other in to_intersect:
+ # Conform target type
+ # Vertices need to be Vector for set()
+ match other:
+ case Axis():
+ target = Edge(other)
+ case Plane():
+ target = Face.make_plane(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[Vector | Edge | Face] = []
+ result: ShapeList | Shape | None
+ for obj in common_set:
+ match (obj, target):
+ case (Vertex(), Vertex()):
+ result = obj.intersect(target)
+
+ case (Edge(), Edge() | Wire()):
+ result = obj.intersect(target)
+
+ case _ if issubclass(type(target), Shape):
+ if isinstance(target, Wire):
+ targets = target.edges()
+ elif isinstance(target, Shell):
+ targets = target.faces()
+ else:
+ targets = ShapeList([target])
+
+ result = ShapeList()
+ for t in targets:
+ if (
+ not isinstance(obj, Edge) and not isinstance(t, (Edge))
+ ) or (isinstance(obj, Solid) or isinstance(t, 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,), (t,), operation) or [])
+ operation = BRepAlgoAPI_Section()
+ result.extend(bool_op((obj,), (t,), operation) or [])
+
+ if result:
+ common.extend(to_vector(result))
+
+ if common:
+ common_set = to_vertex(set(common))
+ common_set = filter_shapes_by_order(
+ common_set, [Vertex, Edge, Face, Solid]
+ )
+ else:
+ return None
+
+ return ShapeList(common_set)
+
def is_inside(self, point: VectorLike, tolerance: float = 1.0e-6) -> bool:
"""Returns whether or not the point is inside a solid or compound
object within the specified tolerance.
diff --git a/src/build123d/topology/two_d.py b/src/build123d/topology/two_d.py
index 306c5b8..0195c30 100644
--- a/src/build123d/topology/two_d.py
+++ b/src/build123d/topology/two_d.py
@@ -64,9 +64,8 @@ from typing import TYPE_CHECKING, Any, TypeVar, overload
import OCP.TopAbs as ta
from OCP.BRep import BRep_Builder, BRep_Tool
-from OCP.BRepAdaptor import BRepAdaptor_Surface
from OCP.BRepAlgo import BRepAlgo
-from OCP.BRepAlgoAPI import BRepAlgoAPI_Common
+from OCP.BRepAlgoAPI import BRepAlgoAPI_Common, BRepAlgoAPI_Section
from OCP.BRepBuilderAPI import (
BRepBuilderAPI_MakeEdge,
BRepBuilderAPI_MakeFace,
@@ -267,6 +266,122 @@ class Mixin2D(ABC, Shape):
return result
+ def intersect(
+ self, *to_intersect: Shape | Vector | Location | Axis | Plane
+ ) -> None | ShapeList[Vertex | Edge | Face]:
+ """Intersect Face with Shape or geometry object
+
+ Args:
+ to_intersect (Shape | Vector | Location | Axis | Plane): objects to intersect
+
+ Returns:
+ ShapeList[Vertex | Edge | Face] | None: ShapeList of vertices, edges, and/or
+ faces.
+ """
+
+ def to_vector(objs: Iterable) -> ShapeList:
+ return ShapeList([Vector(v) if isinstance(v, Vertex) else v for v in objs])
+
+ def to_vertex(objs: Iterable) -> ShapeList:
+ return ShapeList([Vertex(v) if isinstance(v, Vector) else v for v in objs])
+
+ def bool_op(
+ args: Sequence,
+ tools: Sequence,
+ operation: BRepAlgoAPI_Section | BRepAlgoAPI_Common,
+ ) -> ShapeList | None:
+ # Wrap Shape._bool_op for corrected output
+ intersections = args[0]._bool_op(args, tools, operation)
+ if isinstance(intersections, ShapeList):
+ return intersections or None
+ if isinstance(intersections, Shape) and not intersections.is_null:
+ return ShapeList([intersections])
+ return None
+
+ 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] = ShapeList(self.faces())
+ target: ShapeList | Shape
+ for other in to_intersect:
+ # Conform target type
+ # Vertices need to be Vector for set()
+ match other:
+ case Axis():
+ target = Edge(other)
+ case Plane():
+ target = Face.make_plane(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[Vector | Edge | Face] = []
+ result: ShapeList | Shape | None
+ for obj in common_set:
+ match (obj, target):
+ case (Vertex(), Vertex()):
+ result = obj.intersect(target)
+
+ case (Edge(), Edge() | Wire()):
+ result = obj.intersect(target)
+
+ case _ if issubclass(type(target), Shape):
+ if isinstance(target, Wire):
+ targets = target.edges()
+ elif isinstance(target, Shell):
+ targets = target.faces()
+ else:
+ targets = ShapeList([target])
+
+ result = ShapeList()
+ for t in targets:
+ if not isinstance(obj, Edge) and not isinstance(t, (Edge)):
+ # 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,), (t,), operation) or [])
+ operation = BRepAlgoAPI_Section()
+ result.extend(bool_op((obj,), (t,), operation) or [])
+
+ if result:
+ common.extend(to_vector(result))
+
+ if common:
+ common_set = to_vertex(set(common))
+ common_set = filter_shapes_by_order(common_set, [Vertex, Edge, Face])
+ else:
+ return None
+
+ return ShapeList(common_set)
+
@abstractmethod
def location_at(self, *args: Any, **kwargs: Any) -> Location:
"""A location from a face or shell"""
From 5d485ee705acf3315757bd4cec553d5eec458d30 Mon Sep 17 00:00:00 2001
From: snoyer
Date: Tue, 21 Oct 2025 08:12:29 +0400
Subject: [PATCH 008/105] use `_wrapped: TOPODS | None` member and `wrapped:
TOPODS` property
---
src/build123d/topology/composite.py | 6 +-
src/build123d/topology/one_d.py | 57 ++++++++-------
src/build123d/topology/shape_core.py | 102 +++++++++++++++------------
src/build123d/topology/two_d.py | 14 ++--
4 files changed, 96 insertions(+), 83 deletions(-)
diff --git a/src/build123d/topology/composite.py b/src/build123d/topology/composite.py
index 823eece..a34fa23 100644
--- a/src/build123d/topology/composite.py
+++ b/src/build123d/topology/composite.py
@@ -455,7 +455,7 @@ class Compound(Mixin3D, Shape[TopoDS_Compound]):
will be a Wire, otherwise a Shape.
"""
if self._dim == 1:
- curve = Curve() if self.wrapped is None else Curve(self.wrapped)
+ curve = Curve() if self._wrapped is None else Curve(self.wrapped)
sum1d: Edge | Wire | ShapeList[Edge] = curve + other
if isinstance(sum1d, ShapeList):
result1d: Curve | Wire = Curve(sum1d)
@@ -517,7 +517,7 @@ class Compound(Mixin3D, Shape[TopoDS_Compound]):
Check if empty.
"""
- return TopoDS_Iterator(self.wrapped).More()
+ return self._wrapped is not None and TopoDS_Iterator(self.wrapped).More()
def __iter__(self) -> Iterator[Shape]:
"""
@@ -602,7 +602,7 @@ class Compound(Mixin3D, Shape[TopoDS_Compound]):
def compounds(self) -> ShapeList[Compound]:
"""compounds - all the compounds in this Shape"""
- if self.wrapped is None:
+ if self._wrapped is None:
return ShapeList()
if isinstance(self.wrapped, TopoDS_Compound):
# pylint: disable=not-an-iterable
diff --git a/src/build123d/topology/one_d.py b/src/build123d/topology/one_d.py
index 25b817a..c149f1e 100644
--- a/src/build123d/topology/one_d.py
+++ b/src/build123d/topology/one_d.py
@@ -263,14 +263,14 @@ class Mixin1D(Shape):
@property
def is_closed(self) -> bool:
"""Are the start and end points equal?"""
- if self.wrapped is None:
+ if self._wrapped is None:
raise ValueError("Can't determine if empty Edge or Wire is closed")
return BRep_Tool.IsClosed_s(self.wrapped)
@property
def is_forward(self) -> bool:
"""Does the Edge/Wire loop forward or reverse"""
- if self.wrapped is None:
+ if self._wrapped is None:
raise ValueError("Can't determine direction of empty Edge or Wire")
return self.wrapped.Orientation() == TopAbs_Orientation.TopAbs_FORWARD
@@ -388,8 +388,7 @@ class Mixin1D(Shape):
shape
# for o in (other if isinstance(other, (list, tuple)) else [other])
for o in ([other] if isinstance(other, Shape) else other)
- if o is not None
- for shape in get_top_level_topods_shapes(o.wrapped)
+ for shape in get_top_level_topods_shapes(o.wrapped if o else None)
]
# If there is nothing to add return the original object
if not topods_summands:
@@ -404,7 +403,7 @@ class Mixin1D(Shape):
)
summand_edges = [e for summand in summands for e in summand.edges()]
- if self.wrapped is None: # an empty object
+ if self._wrapped is None: # an empty object
if len(summands) == 1:
sum_shape: Edge | Wire | ShapeList[Edge] = summands[0]
else:
@@ -452,7 +451,7 @@ class Mixin1D(Shape):
Returns:
Vector: center
"""
- if self.wrapped is None:
+ if self._wrapped is None:
raise ValueError("Can't find center of empty edge/wire")
if center_of == CenterOf.GEOMETRY:
@@ -578,7 +577,7 @@ class Mixin1D(Shape):
>>> show(my_wire, Curve(comb))
"""
- if self.wrapped is None:
+ if self._wrapped is None:
raise ValueError("Can't create curvature_comb for empty curve")
pln = self.common_plane()
if pln is None or not isclose(abs(pln.z_dir.Z), 1.0, abs_tol=TOLERANCE):
@@ -991,7 +990,7 @@ class Mixin1D(Shape):
Returns:
"""
- if self.wrapped is None:
+ if self._wrapped is None:
raise ValueError("Can't find normal of empty edge/wire")
curve = self.geom_adaptor()
@@ -1225,7 +1224,7 @@ class Mixin1D(Shape):
Returns:
"""
- if self.wrapped is None or face.wrapped is None:
+ if self._wrapped is None or face.wrapped is None:
raise ValueError("Can't project an empty Edge or Wire onto empty Face")
bldr = BRepProj_Projection(
@@ -1297,7 +1296,7 @@ class Mixin1D(Shape):
return edges
- if self.wrapped is None:
+ if self._wrapped is None:
raise ValueError("Can't project empty edge/wire")
# Setup the projector
@@ -1400,7 +1399,7 @@ class Mixin1D(Shape):
- **Keep.BOTH**: Returns a tuple `(inside, outside)` where each element is
either a `Self` or `list[Self]`, or `None` if no corresponding part is found.
"""
- if self.wrapped is None or tool.wrapped is None:
+ if self._wrapped is None or tool.wrapped is None:
raise ValueError("Can't split an empty edge/wire/tool")
shape_list = TopTools_ListOfShape()
@@ -2538,7 +2537,7 @@ class Edge(Mixin1D, Shape[TopoDS_Edge]):
extension_factor: float = 0.1,
):
"""Helper method to slightly extend an edge that is bound to a surface"""
- if self.wrapped is None:
+ if self._wrapped is None:
raise ValueError("Can't extend empty spline")
if self.geom_type != GeomType.BSPLINE:
raise TypeError("_extend_spline only works with splines")
@@ -2595,7 +2594,7 @@ class Edge(Mixin1D, Shape[TopoDS_Edge]):
Returns:
ShapeList[Vector]: list of intersection points
"""
- if self.wrapped is None:
+ if self._wrapped is None:
raise ValueError("Can't find intersections of empty edge")
# Convert an Axis into an edge at least as large as self and Axis start point
@@ -2723,7 +2722,7 @@ class Edge(Mixin1D, Shape[TopoDS_Edge]):
def geom_adaptor(self) -> BRepAdaptor_Curve:
"""Return the Geom Curve from this Edge"""
- if self.wrapped is None:
+ if self._wrapped is None:
raise ValueError("Can't find adaptor for empty edge")
return BRepAdaptor_Curve(self.wrapped)
@@ -2811,7 +2810,7 @@ class Edge(Mixin1D, Shape[TopoDS_Edge]):
float: Normalized parameter in [0.0, 1.0] corresponding to the point's
closest location on the edge.
"""
- if self.wrapped is None:
+ if self._wrapped is None:
raise ValueError("Can't find param on empty edge")
pnt = Vector(point)
@@ -2945,7 +2944,7 @@ class Edge(Mixin1D, Shape[TopoDS_Edge]):
Returns:
Edge: reversed
"""
- if self.wrapped is None:
+ if self._wrapped is None:
raise ValueError("An empty edge can't be reversed")
assert isinstance(self.wrapped, TopoDS_Edge)
@@ -3025,7 +3024,7 @@ class Edge(Mixin1D, Shape[TopoDS_Edge]):
# if start_u >= end_u:
# raise ValueError(f"start ({start_u}) must be less than end ({end_u})")
- if self.wrapped is None:
+ if self._wrapped is None:
raise ValueError("Can't trim empty edge")
self_copy = copy.deepcopy(self)
@@ -3060,7 +3059,7 @@ class Edge(Mixin1D, Shape[TopoDS_Edge]):
Returns:
Edge: trimmed edge
"""
- if self.wrapped is None:
+ if self._wrapped is None:
raise ValueError("Can't trim empty edge")
start_u = Mixin1D._to_param(self, start, "start")
@@ -3623,7 +3622,7 @@ class Wire(Mixin1D, Shape[TopoDS_Wire]):
Returns:
Wire: chamfered wire
"""
- if self.wrapped is None:
+ if self._wrapped is None:
raise ValueError("Can't chamfer empty wire")
reference_edge = edge
@@ -3695,7 +3694,7 @@ class Wire(Mixin1D, Shape[TopoDS_Wire]):
Returns:
Wire: filleted wire
"""
- if self.wrapped is None:
+ if self._wrapped is None:
raise ValueError("Can't fillet an empty wire")
# Create a face to fillet
@@ -3723,7 +3722,7 @@ class Wire(Mixin1D, Shape[TopoDS_Wire]):
Returns:
Wire: fixed wire
"""
- if self.wrapped is None:
+ if self._wrapped is None:
raise ValueError("Can't fix an empty edge")
sf_w = ShapeFix_Wireframe(self.wrapped)
@@ -3735,7 +3734,7 @@ class Wire(Mixin1D, Shape[TopoDS_Wire]):
def geom_adaptor(self) -> BRepAdaptor_CompCurve:
"""Return the Geom Comp Curve for this Wire"""
- if self.wrapped is None:
+ if self._wrapped is None:
raise ValueError("Can't get geom adaptor of empty wire")
return BRepAdaptor_CompCurve(self.wrapped)
@@ -3779,7 +3778,7 @@ class Wire(Mixin1D, Shape[TopoDS_Wire]):
float: Normalized parameter in [0.0, 1.0] representing the relative
position of the projected point along the wire.
"""
- if self.wrapped is None:
+ if self._wrapped is None:
raise ValueError("Can't find point on empty wire")
point_on_curve = Vector(point)
@@ -3932,7 +3931,7 @@ class Wire(Mixin1D, Shape[TopoDS_Wire]):
"""
# pylint: disable=too-many-branches
- if self.wrapped is None or target_object.wrapped is None:
+ if self._wrapped is None or target_object.wrapped is None:
raise ValueError("Can't project empty Wires or to empty Shapes")
if direction is not None and center is None:
@@ -4021,7 +4020,7 @@ class Wire(Mixin1D, Shape[TopoDS_Wire]):
Returns:
Wire: stitched wires
"""
- if self.wrapped is None or other.wrapped is None:
+ if self._wrapped is None or other.wrapped is None:
raise ValueError("Can't stitch empty wires")
wire_builder = BRepBuilderAPI_MakeWire()
@@ -4065,7 +4064,7 @@ class Wire(Mixin1D, Shape[TopoDS_Wire]):
"""
# Build a single Geom_BSplineCurve from the wire, in *topological order*
builder = GeomConvert_CompCurveToBSplineCurve()
- if self.wrapped is None:
+ if self._wrapped is None:
raise ValueError("Can't convert an empty wire")
wire_explorer = BRepTools_WireExplorer(self.wrapped)
@@ -4217,9 +4216,9 @@ def topo_explore_connected_edges(
parent = parent if parent is not None else edge.topo_parent
if parent is None:
raise ValueError("edge has no valid parent")
- given_topods_edge = edge.wrapped
- if given_topods_edge is None:
+ if not edge:
raise ValueError("edge is empty")
+ given_topods_edge = edge.wrapped
connected_edges = set()
# Find all the TopoDS_Edges for this Shape
@@ -4262,7 +4261,7 @@ def topo_explore_connected_faces(
) -> list[TopoDS_Face]:
"""Given an edge extracted from a Shape, return the topods_faces connected to it"""
- if edge.wrapped is None:
+ if not edge:
raise ValueError("Can't explore from an empty edge")
parent = parent if parent is not None else edge.topo_parent
diff --git a/src/build123d/topology/shape_core.py b/src/build123d/topology/shape_core.py
index 6402c3e..6a270e8 100644
--- a/src/build123d/topology/shape_core.py
+++ b/src/build123d/topology/shape_core.py
@@ -287,7 +287,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
color: ColorLike | None = None,
parent: Compound | None = None,
):
- self.wrapped: TOPODS | None = (
+ self._wrapped: TOPODS | None = (
tcast(Optional[TOPODS], downcast(obj)) if obj is not None else None
)
self.for_construction = False
@@ -304,6 +304,18 @@ class Shape(NodeMixin, Generic[TOPODS]):
# pylint: disable=too-many-instance-attributes, too-many-public-methods
+ @property
+ def wrapped(self):
+ assert self._wrapped
+ return self._wrapped
+
+ @wrapped.setter
+ def wrapped(self, shape: TOPODS):
+ self._wrapped = shape
+
+ def __bool__(self):
+ return self._wrapped is not None
+
@property
@abstractmethod
def _dim(self) -> int | None:
@@ -312,7 +324,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
@property
def area(self) -> float:
"""area -the surface area of all faces in this Shape"""
- if self.wrapped is None:
+ if self._wrapped is None:
return 0.0
properties = GProp_GProps()
BRepGProp.SurfaceProperties_s(self.wrapped, properties)
@@ -351,7 +363,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
GeomType: The geometry type of the shape
"""
- if self.wrapped is None:
+ if self._wrapped is None:
raise ValueError("Cannot determine geometry type of an empty shape")
shape: TopAbs_ShapeEnum = shapetype(self.wrapped)
@@ -380,7 +392,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
bool: is the shape manifold or water tight
"""
# Extract one or more (if a Compound) shape from self
- if self.wrapped is None:
+ if self._wrapped is None:
return False
shape_stack = get_top_level_topods_shapes(self.wrapped)
@@ -431,12 +443,12 @@ class Shape(NodeMixin, Generic[TOPODS]):
underlying shape with the potential to be given a location and an
orientation.
"""
- return self.wrapped is None or self.wrapped.IsNull()
+ return self._wrapped is None or self.wrapped.IsNull()
@property
def is_planar_face(self) -> bool:
"""Is the shape a planar face even though its geom_type may not be PLANE"""
- if self.wrapped is None or not isinstance(self.wrapped, TopoDS_Face):
+ if self._wrapped is None or not isinstance(self.wrapped, TopoDS_Face):
return False
surface = BRep_Tool.Surface_s(self.wrapped)
is_face_planar = GeomLib_IsPlanarSurface(surface, TOLERANCE)
@@ -448,7 +460,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
subshapes. See the OCCT docs on BRepCheck_Analyzer::IsValid for a full
description of what is checked.
"""
- if self.wrapped is None:
+ if self._wrapped is None:
return True
chk = BRepCheck_Analyzer(self.wrapped)
chk.SetParallel(True)
@@ -474,7 +486,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
@property
def location(self) -> Location:
"""Get this Shape's Location"""
- if self.wrapped is None:
+ if self._wrapped is None:
raise ValueError("Can't find the location of an empty shape")
return Location(self.wrapped.Location())
@@ -518,7 +530,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
- It is commonly used in structural analysis, mechanical simulations,
and physics-based motion calculations.
"""
- if self.wrapped is None:
+ if self._wrapped is None:
raise ValueError("Can't calculate matrix for empty shape")
properties = GProp_GProps()
BRepGProp.VolumeProperties_s(self.wrapped, properties)
@@ -546,7 +558,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
@property
def position(self) -> Vector:
"""Get the position component of this Shape's Location"""
- if self.wrapped is None or self.location is None:
+ if self._wrapped is None or self.location is None:
raise ValueError("Can't find the position of an empty shape")
return self.location.position
@@ -575,7 +587,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
(Vector(0, 1, 0), 1000.0),
(Vector(0, 0, 1), 300.0)]
"""
- if self.wrapped is None:
+ if self._wrapped is None:
raise ValueError("Can't calculate properties for empty shape")
properties = GProp_GProps()
@@ -615,7 +627,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
(150.0, 200.0, 50.0)
"""
- if self.wrapped is None:
+ if self._wrapped is None:
raise ValueError("Can't calculate moments for empty shape")
properties = GProp_GProps()
@@ -859,7 +871,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
if not all(summand._dim == addend_dim for summand in summands):
raise ValueError("Only shapes with the same dimension can be added")
- if self.wrapped is None: # an empty object
+ if self._wrapped is None: # an empty object
if len(summands) == 1:
sum_shape = summands[0]
else:
@@ -876,7 +888,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
"""intersect shape with self operator &"""
others = other if isinstance(other, (list, tuple)) else [other]
- if self.wrapped is None or (isinstance(other, Shape) and other.wrapped is None):
+ if not self or (isinstance(other, Shape) and not other):
raise ValueError("Cannot intersect shape with empty compound")
new_shape = self.intersect(*others)
@@ -948,7 +960,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
def __hash__(self) -> int:
"""Return hash code"""
- if self.wrapped is None:
+ if self._wrapped is None:
return 0
return hash(self.wrapped)
@@ -966,7 +978,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
def __sub__(self, other: None | Shape | Iterable[Shape]) -> Self | ShapeList[Self]:
"""cut shape from self operator -"""
- if self.wrapped is None:
+ if self._wrapped is None:
raise ValueError("Cannot subtract shape from empty compound")
# Convert `other` to list of base objects and filter out None values
@@ -1014,7 +1026,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
Returns:
BoundBox: A box sized to contain this Shape
"""
- if self.wrapped is None:
+ if self._wrapped is None:
return BoundBox(Bnd_Box())
tolerance = TOLERANCE if tolerance is None else tolerance
return BoundBox.from_topo_ds(self.wrapped, tolerance=tolerance, optimal=optimal)
@@ -1033,7 +1045,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
Returns:
Shape: Original object with extraneous internal edges removed
"""
- if self.wrapped is None:
+ if self._wrapped is None:
return self
upgrader = ShapeUpgrade_UnifySameDomain(self.wrapped, True, True, True)
upgrader.AllowInternalEdges(False)
@@ -1112,7 +1124,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
Returns:
"""
- if self.wrapped is None or other.wrapped is None:
+ if self._wrapped is None or other.wrapped is None:
raise ValueError("Cannot calculate distance to or from an empty shape")
return BRepExtrema_DistShapeShape(self.wrapped, other.wrapped).Value()
@@ -1125,7 +1137,9 @@ class Shape(NodeMixin, Generic[TOPODS]):
self, other: Shape | VectorLike
) -> tuple[float, Vector, Vector]:
"""Minimal distance between two shapes and the points on each shape"""
- if self.wrapped is None or (isinstance(other, Shape) and other.wrapped is None):
+ if self._wrapped is None or (
+ isinstance(other, Shape) and other.wrapped is None
+ ):
raise ValueError("Cannot calculate distance to or from an empty shape")
if isinstance(other, Shape):
@@ -1155,7 +1169,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
Returns:
"""
- if self.wrapped is None:
+ if self._wrapped is None:
raise ValueError("Cannot calculate distance to or from an empty shape")
dist_calc = BRepExtrema_DistShapeShape()
@@ -1181,7 +1195,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
def entities(self, topo_type: Shapes) -> list[TopoDS_Shape]:
"""Return all of the TopoDS sub entities of the given type"""
- if self.wrapped is None:
+ if self._wrapped is None:
return []
return _topods_entities(self.wrapped, topo_type)
@@ -1209,7 +1223,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
Returns:
list[Face]: A list of intersected faces sorted by distance from axis.position
"""
- if self.wrapped is None:
+ if self._wrapped is None:
return ShapeList()
line = gce_MakeLin(axis.wrapped).Value()
@@ -1239,7 +1253,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
def fix(self) -> Self:
"""fix - try to fix shape if not valid"""
- if self.wrapped is None:
+ if self._wrapped is None:
return self
if not self.is_valid:
shape_copy: Shape = copy.deepcopy(self, None)
@@ -1281,7 +1295,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
# self, child_type: Shapes, parent_type: Shapes
# ) -> Dict[Shape, list[Shape]]:
# """This function is very slow on M1 macs and is currently unused"""
- # if self.wrapped is None:
+ # if self._wrapped is None:
# return {}
# res = TopTools_IndexedDataMapOfShapeListOfShape()
@@ -1319,7 +1333,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
(e.g., edges, vertices) and other compounds, the method returns a list
of only the simple shapes directly contained at the top level.
"""
- if self.wrapped is None:
+ if self._wrapped is None:
return ShapeList()
return ShapeList(
self.__class__.cast(s) for s in get_top_level_topods_shapes(self.wrapped)
@@ -1401,7 +1415,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
Returns:
"""
- if self.wrapped is None or other.wrapped is None:
+ if self._wrapped is None or other.wrapped is None:
return False
return self.wrapped.IsEqual(other.wrapped)
@@ -1416,7 +1430,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
Returns:
"""
- if self.wrapped is None or other.wrapped is None:
+ if self._wrapped is None or other.wrapped is None:
return False
return self.wrapped.IsSame(other.wrapped)
@@ -1429,7 +1443,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
Returns:
"""
- if self.wrapped is None:
+ if self._wrapped is None:
raise ValueError("Cannot locate an empty shape")
if loc.wrapped is None:
raise ValueError("Cannot locate a shape at an empty location")
@@ -1448,7 +1462,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
Returns:
Shape: copy of Shape at location
"""
- if self.wrapped is None:
+ if self._wrapped is None:
raise ValueError("Cannot locate an empty shape")
if loc.wrapped is None:
raise ValueError("Cannot locate a shape at an empty location")
@@ -1466,7 +1480,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
Returns:
"""
- if self.wrapped is None:
+ if self._wrapped is None:
raise ValueError("Cannot mesh an empty shape")
if not BRepTools.Triangulation_s(self.wrapped, tolerance):
@@ -1487,7 +1501,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
if not mirror_plane:
mirror_plane = Plane.XY
- if self.wrapped is None:
+ if self._wrapped is None:
return self
transformation = gp_Trsf()
transformation.SetMirror(
@@ -1505,7 +1519,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
Returns:
"""
- if self.wrapped is None:
+ if self._wrapped is None:
raise ValueError("Cannot move an empty shape")
if loc.wrapped is None:
raise ValueError("Cannot move a shape at an empty location")
@@ -1525,7 +1539,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
Returns:
Shape: copy of Shape moved to relative location
"""
- if self.wrapped is None:
+ if self._wrapped is None:
raise ValueError("Cannot move an empty shape")
if loc.wrapped is None:
raise ValueError("Cannot move a shape at an empty location")
@@ -1539,7 +1553,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
Returns:
OrientedBoundBox: A box oriented and sized to contain this Shape
"""
- if self.wrapped is None:
+ if self._wrapped is None:
return OrientedBoundBox(Bnd_OBB())
return OrientedBoundBox(self)
@@ -1641,7 +1655,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
- The radius of gyration is computed based on the shape’s mass properties.
- It is useful for evaluating structural stability and rotational behavior.
"""
- if self.wrapped is None:
+ if self._wrapped is None:
raise ValueError("Can't calculate radius of gyration for empty shape")
properties = GProp_GProps()
@@ -1660,7 +1674,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
DeprecationWarning,
stacklevel=2,
)
- if self.wrapped is None:
+ if self._wrapped is None:
raise ValueError("Cannot relocate an empty shape")
if loc.wrapped is None:
raise ValueError("Cannot relocate a shape at an empty location")
@@ -1855,7 +1869,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
"keep must be one of Keep.INSIDE, Keep.OUTSIDE, or Keep.BOTH"
)
- if self.wrapped is None:
+ if self._wrapped is None:
raise ValueError("Cannot split an empty shape")
# Process the perimeter
@@ -1900,7 +1914,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
self, tolerance: float, angular_tolerance: float = 0.1
) -> tuple[list[Vector], list[tuple[int, int, int]]]:
"""General triangulated approximation"""
- if self.wrapped is None:
+ if self._wrapped is None:
raise ValueError("Cannot tessellate an empty shape")
self.mesh(tolerance, angular_tolerance)
@@ -1962,7 +1976,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
Returns:
Self: Approximated shape
"""
- if self.wrapped is None:
+ if self._wrapped is None:
raise ValueError("Cannot approximate an empty shape")
params = ShapeCustom_RestrictionParameters()
@@ -1999,7 +2013,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
Returns:
Shape: a copy of the object, but with geometry transformed
"""
- if self.wrapped is None:
+ if self._wrapped is None:
return self
new_shape = copy.deepcopy(self, None)
transformed = downcast(
@@ -2022,7 +2036,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
Returns:
Shape: copy of transformed shape with all objects keeping their type
"""
- if self.wrapped is None:
+ if self._wrapped is None:
return self
new_shape = copy.deepcopy(self, None)
transformed = downcast(
@@ -2095,7 +2109,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
Returns:
Shape: copy of transformed Shape
"""
- if self.wrapped is None:
+ if self._wrapped is None:
return self
shape_copy: Shape = copy.deepcopy(self, None)
transformed_shape = BRepBuilderAPI_Transform(
@@ -2200,7 +2214,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
Returns:
tuple[ShapeList[Vertex], ShapeList[Edge]]: section results
"""
- if self.wrapped is None or other.wrapped is None:
+ if self._wrapped is None or other.wrapped is None:
return (ShapeList(), ShapeList())
section = BRepAlgoAPI_Section(self.wrapped, other.wrapped)
diff --git a/src/build123d/topology/two_d.py b/src/build123d/topology/two_d.py
index 8b8f264..c6102a7 100644
--- a/src/build123d/topology/two_d.py
+++ b/src/build123d/topology/two_d.py
@@ -213,7 +213,7 @@ class Mixin2D(ABC, Shape):
def __neg__(self) -> Self:
"""Reverse normal operator -"""
- if self.wrapped is None:
+ if self._wrapped is None:
raise ValueError("Invalid Shape")
new_surface = copy.deepcopy(self)
new_surface.wrapped = downcast(self.wrapped.Complemented())
@@ -244,7 +244,7 @@ class Mixin2D(ABC, Shape):
Returns:
list[tuple[Vector, Vector]]: Point and normal of intersection
"""
- if self.wrapped is None:
+ if self._wrapped is None:
return []
intersection_line = gce_MakeLin(other.wrapped).Value()
@@ -350,7 +350,7 @@ class Mixin2D(ABC, Shape):
world_point, world_point - target_object_center
)
- if self.wrapped is None:
+ if self._wrapped is None:
raise ValueError("Can't wrap around an empty face")
# Initial setup
@@ -545,7 +545,7 @@ class Face(Mixin2D, Shape[TopoDS_Face]):
float: The total surface area, including the area of holes. Returns 0.0 if
the face is empty.
"""
- if self.wrapped is None:
+ if self._wrapped is None:
return 0.0
return self.without_holes().area
@@ -605,7 +605,7 @@ class Face(Mixin2D, Shape[TopoDS_Face]):
ValueError: If the face or its underlying representation is empty.
ValueError: If the face is not planar.
"""
- if self.wrapped is None:
+ if self._wrapped is None:
raise ValueError("Can't determine axes_of_symmetry of empty face")
if not self.is_planar_face:
@@ -1940,7 +1940,7 @@ class Face(Mixin2D, Shape[TopoDS_Face]):
DeprecationWarning,
stacklevel=2,
)
- if self.wrapped is None:
+ if self._wrapped is None:
raise ValueError("Cannot approximate an empty shape")
return self.__class__.cast(BRepAlgo.ConvertFace_s(self.wrapped, tolerance))
@@ -1953,7 +1953,7 @@ class Face(Mixin2D, Shape[TopoDS_Face]):
Returns:
Face: A new Face instance identical to the original but without any holes.
"""
- if self.wrapped is None:
+ if self._wrapped is None:
raise ValueError("Cannot remove holes from an empty face")
if not (inner_wires := self.inner_wires()):
From 0013b9fa872e3806e533ee31d8115812d8a65911 Mon Sep 17 00:00:00 2001
From: snoyer
Date: Tue, 21 Oct 2025 08:28:24 +0400
Subject: [PATCH 009/105] fix Mixins generic types
---
src/build123d/topology/composite.py | 2 +-
src/build123d/topology/one_d.py | 7 ++++---
src/build123d/topology/three_d.py | 6 +++---
src/build123d/topology/two_d.py | 7 ++++---
4 files changed, 12 insertions(+), 10 deletions(-)
diff --git a/src/build123d/topology/composite.py b/src/build123d/topology/composite.py
index a34fa23..4faeb36 100644
--- a/src/build123d/topology/composite.py
+++ b/src/build123d/topology/composite.py
@@ -130,7 +130,7 @@ from .utils import (
from .zero_d import Vertex
-class Compound(Mixin3D, Shape[TopoDS_Compound]):
+class Compound(Mixin3D[TopoDS_Compound]):
"""A Compound in build123d is a topological entity representing a collection of
geometric shapes grouped together within a single structure. It serves as a
container for organizing diverse shapes like edges, faces, or solids. This
diff --git a/src/build123d/topology/one_d.py b/src/build123d/topology/one_d.py
index c149f1e..8e9939f 100644
--- a/src/build123d/topology/one_d.py
+++ b/src/build123d/topology/one_d.py
@@ -217,6 +217,7 @@ from build123d.geometry import (
)
from .shape_core import (
+ TOPODS,
Shape,
ShapeList,
SkipClean,
@@ -250,7 +251,7 @@ if TYPE_CHECKING: # pragma: no cover
from .two_d import Face, Shell # pylint: disable=R0801
-class Mixin1D(Shape):
+class Mixin1D(Shape[TOPODS]):
"""Methods to add to the Edge and Wire classes"""
# ---- Properties ----
@@ -1565,7 +1566,7 @@ class Mixin1D(Shape):
return Shape.get_shape_list(self, "Wire")
-class Edge(Mixin1D, Shape[TopoDS_Edge]):
+class Edge(Mixin1D[TopoDS_Edge]):
"""An Edge in build123d is a fundamental element in the topological data structure
representing a one-dimensional geometric entity within a 3D model. It encapsulates
information about a curve, which could be a line, arc, or other parametrically
@@ -3088,7 +3089,7 @@ class Edge(Mixin1D, Shape[TopoDS_Edge]):
return Edge(new_edge)
-class Wire(Mixin1D, Shape[TopoDS_Wire]):
+class Wire(Mixin1D[TopoDS_Wire]):
"""A Wire in build123d is a topological entity representing a connected sequence
of edges forming a continuous curve or path in 3D space. Wires are essential
components in modeling complex objects, defining boundaries for surfaces or
diff --git a/src/build123d/topology/three_d.py b/src/build123d/topology/three_d.py
index e4131ce..ed3ba34 100644
--- a/src/build123d/topology/three_d.py
+++ b/src/build123d/topology/three_d.py
@@ -107,7 +107,7 @@ from build123d.geometry import (
from typing_extensions import Self
from .one_d import Edge, Wire, Mixin1D
-from .shape_core import Shape, ShapeList, Joint, downcast, shapetype
+from .shape_core import TOPODS, Shape, ShapeList, Joint, downcast, shapetype
from .two_d import sort_wires_by_build_order, Mixin2D, Face, Shell
from .utils import (
_extrude_topods_shape,
@@ -122,7 +122,7 @@ if TYPE_CHECKING: # pragma: no cover
from .composite import Compound, Curve, Sketch, Part # pylint: disable=R0801
-class Mixin3D(Shape):
+class Mixin3D(Shape[TOPODS]):
"""Additional methods to add to 3D Shape classes"""
project_to_viewport = Mixin1D.project_to_viewport
@@ -590,7 +590,7 @@ class Mixin3D(Shape):
return Shape.get_shape_list(self, "Solid")
-class Solid(Mixin3D, Shape[TopoDS_Solid]):
+class Solid(Mixin3D[TopoDS_Solid]):
"""A Solid in build123d represents a three-dimensional solid geometry
in a topological structure. A solid is a closed and bounded volume, enclosing
a region in 3D space. It comprises faces, edges, and vertices connected in a
diff --git a/src/build123d/topology/two_d.py b/src/build123d/topology/two_d.py
index c6102a7..65cee95 100644
--- a/src/build123d/topology/two_d.py
+++ b/src/build123d/topology/two_d.py
@@ -139,6 +139,7 @@ from build123d.geometry import (
from .one_d import Edge, Mixin1D, Wire
from .shape_core import (
+ TOPODS,
Shape,
ShapeList,
SkipClean,
@@ -165,7 +166,7 @@ if TYPE_CHECKING: # pragma: no cover
T = TypeVar("T", Edge, Wire, "Face")
-class Mixin2D(ABC, Shape):
+class Mixin2D(ABC, Shape[TOPODS]):
"""Additional methods to add to Face and Shell class"""
project_to_viewport = Mixin1D.project_to_viewport
@@ -434,7 +435,7 @@ class Mixin2D(ABC, Shape):
return projected_edge
-class Face(Mixin2D, Shape[TopoDS_Face]):
+class Face(Mixin2D[TopoDS_Face]):
"""A Face in build123d represents a 3D bounded surface within the topological data
structure. It encapsulates geometric information, defining a face of a 3D shape.
These faces are integral components of complex structures, such as solids and
@@ -2327,7 +2328,7 @@ class Face(Mixin2D, Shape[TopoDS_Face]):
return wrapped_wire
-class Shell(Mixin2D, Shape[TopoDS_Shell]):
+class Shell(Mixin2D[TopoDS_Shell]):
"""A Shell is a fundamental component in build123d's topological data structure
representing a connected set of faces forming a closed surface in 3D space. As
part of a geometric model, it defines a watertight enclosure, commonly encountered
From a6d8f9bdc16a18d9ca82491db194f2b1bbc71386 Mon Sep 17 00:00:00 2001
From: snoyer
Date: Tue, 21 Oct 2025 10:15:47 +0400
Subject: [PATCH 010/105] refactor `.wrapped is None` usages
---
src/build123d/exporters.py | 6 +-
src/build123d/mesher.py | 2 +-
src/build123d/operations_generic.py | 4 +-
src/build123d/topology/composite.py | 2 +-
src/build123d/topology/constrained_lines.py | 2 +-
src/build123d/topology/one_d.py | 16 ++---
src/build123d/topology/shape_core.py | 65 ++++++++++-----------
src/build123d/topology/two_d.py | 14 ++---
src/build123d/vtk_tools.py | 2 +-
9 files changed, 56 insertions(+), 57 deletions(-)
diff --git a/src/build123d/exporters.py b/src/build123d/exporters.py
index 49339ee..a229fa2 100644
--- a/src/build123d/exporters.py
+++ b/src/build123d/exporters.py
@@ -758,7 +758,7 @@ class ExportDXF(Export2D):
)
# need to apply the transform on the geometry level
- if edge.wrapped is None or edge.location is None:
+ if not edge or edge.location is None:
raise ValueError(f"Edge is empty {edge}.")
t = edge.location.wrapped.Transformation()
spline.Transform(t)
@@ -1345,7 +1345,7 @@ class ExportSVG(Export2D):
u2 = adaptor.LastParameter()
# Apply the shape location to the geometry.
- if edge.wrapped is None or edge.location is None:
+ if not edge or edge.location is None:
raise ValueError(f"Edge is empty {edge}.")
t = edge.location.wrapped.Transformation()
spline.Transform(t)
@@ -1411,7 +1411,7 @@ class ExportSVG(Export2D):
}
def _edge_segments(self, edge: Edge, reverse: bool) -> list[PathSegment]:
- if edge.wrapped is None:
+ if not edge:
raise ValueError(f"Edge is empty {edge}.")
edge_reversed = edge.wrapped.Orientation() == TopAbs_Orientation.TopAbs_REVERSED
geom_type = edge.geom_type
diff --git a/src/build123d/mesher.py b/src/build123d/mesher.py
index 5fb9a54..5433848 100644
--- a/src/build123d/mesher.py
+++ b/src/build123d/mesher.py
@@ -295,7 +295,7 @@ class Mesher:
ocp_mesh_vertices.append(pnt)
# Store the triangles from the triangulated faces
- if facet.wrapped is None:
+ if not facet:
continue
facet_reversed = facet.wrapped.Orientation() == ta.TopAbs_REVERSED
order = [1, 3, 2] if facet_reversed else [1, 2, 3]
diff --git a/src/build123d/operations_generic.py b/src/build123d/operations_generic.py
index 69d75cc..2a2f007 100644
--- a/src/build123d/operations_generic.py
+++ b/src/build123d/operations_generic.py
@@ -365,7 +365,7 @@ def chamfer(
if target._dim == 1:
if isinstance(target, BaseLineObject):
- if target.wrapped is None:
+ if not target:
target = Wire([]) # empty wire
else:
target = Wire(target.wrapped)
@@ -465,7 +465,7 @@ def fillet(
if target._dim == 1:
if isinstance(target, BaseLineObject):
- if target.wrapped is None:
+ if not target:
target = Wire([]) # empty wire
else:
target = Wire(target.wrapped)
diff --git a/src/build123d/topology/composite.py b/src/build123d/topology/composite.py
index 4faeb36..4e42b60 100644
--- a/src/build123d/topology/composite.py
+++ b/src/build123d/topology/composite.py
@@ -534,7 +534,7 @@ class Compound(Mixin3D[TopoDS_Compound]):
def __len__(self) -> int:
"""Return the number of subshapes"""
count = 0
- if self.wrapped is not None:
+ if self._wrapped is not None:
for _ in self:
count += 1
return count
diff --git a/src/build123d/topology/constrained_lines.py b/src/build123d/topology/constrained_lines.py
index 9c316b6..4e53ddb 100644
--- a/src/build123d/topology/constrained_lines.py
+++ b/src/build123d/topology/constrained_lines.py
@@ -174,7 +174,7 @@ def _as_gcc_arg(obj: Edge | Vector, constaint: Tangency) -> tuple[
- Edge -> (QualifiedCurve, h2d, first, last, True)
- Vector -> (CartesianPoint, None, None, None, False)
"""
- if obj.wrapped is None:
+ if not obj:
raise TypeError("Can't create a qualified curve from empty edge")
if isinstance(obj.wrapped, TopoDS_Edge):
diff --git a/src/build123d/topology/one_d.py b/src/build123d/topology/one_d.py
index 8e9939f..c12880f 100644
--- a/src/build123d/topology/one_d.py
+++ b/src/build123d/topology/one_d.py
@@ -809,7 +809,7 @@ class Mixin1D(Shape[TOPODS]):
case Edge() as obj, Plane() as plane:
# Find any edge / plane intersection points & edges
# Find point intersections
- if obj.wrapped is None:
+ if not obj:
continue
geom_line = BRep_Tool.Curve_s(
obj.wrapped, obj.param_at(0), obj.param_at(1)
@@ -1225,7 +1225,7 @@ class Mixin1D(Shape[TOPODS]):
Returns:
"""
- if self._wrapped is None or face.wrapped is None:
+ if self._wrapped is None or not face:
raise ValueError("Can't project an empty Edge or Wire onto empty Face")
bldr = BRepProj_Projection(
@@ -1400,7 +1400,7 @@ class Mixin1D(Shape[TOPODS]):
- **Keep.BOTH**: Returns a tuple `(inside, outside)` where each element is
either a `Self` or `list[Self]`, or `None` if no corresponding part is found.
"""
- if self._wrapped is None or tool.wrapped is None:
+ if self._wrapped is None or not tool:
raise ValueError("Can't split an empty edge/wire/tool")
shape_list = TopTools_ListOfShape()
@@ -1647,7 +1647,7 @@ class Edge(Mixin1D[TopoDS_Edge]):
Returns:
Edge: extruded shape
"""
- if obj.wrapped is None:
+ if not obj:
raise ValueError("Can't extrude empty vertex")
return Edge(TopoDS.Edge_s(_extrude_topods_shape(obj.wrapped, direction)))
@@ -3638,7 +3638,7 @@ class Wire(Mixin1D[TopoDS_Wire]):
)
for v in vertices:
- if v.wrapped is None:
+ if not v:
continue
edge_list = vertex_edge_map.FindFromKey(v.wrapped)
@@ -3932,7 +3932,7 @@ class Wire(Mixin1D[TopoDS_Wire]):
"""
# pylint: disable=too-many-branches
- if self._wrapped is None or target_object.wrapped is None:
+ if self._wrapped is None or not target_object:
raise ValueError("Can't project empty Wires or to empty Shapes")
if direction is not None and center is None:
@@ -4021,7 +4021,7 @@ class Wire(Mixin1D[TopoDS_Wire]):
Returns:
Wire: stitched wires
"""
- if self._wrapped is None or other.wrapped is None:
+ if self._wrapped is None or not other:
raise ValueError("Can't stitch empty wires")
wire_builder = BRepBuilderAPI_MakeWire()
@@ -4266,7 +4266,7 @@ def topo_explore_connected_faces(
raise ValueError("Can't explore from an empty edge")
parent = parent if parent is not None else edge.topo_parent
- if parent is None or parent.wrapped is None:
+ if not parent:
raise ValueError("edge has no valid parent")
# make a edge --> faces mapping
diff --git a/src/build123d/topology/shape_core.py b/src/build123d/topology/shape_core.py
index 6a270e8..1e18261 100644
--- a/src/build123d/topology/shape_core.py
+++ b/src/build123d/topology/shape_core.py
@@ -797,7 +797,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
Returns:
"""
- if obj.wrapped is None:
+ if not obj:
return 0.0
properties = GProp_GProps()
@@ -817,7 +817,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
],
) -> ShapeList:
"""Helper to extract entities of a specific type from a shape."""
- if shape.wrapped is None:
+ if not shape:
return ShapeList()
shape_list = ShapeList(
[shape.__class__.cast(i) for i in shape.entities(entity_type)]
@@ -1124,7 +1124,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
Returns:
"""
- if self._wrapped is None or other.wrapped is None:
+ if self._wrapped is None or not other:
raise ValueError("Cannot calculate distance to or from an empty shape")
return BRepExtrema_DistShapeShape(self.wrapped, other.wrapped).Value()
@@ -1137,9 +1137,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
self, other: Shape | VectorLike
) -> tuple[float, Vector, Vector]:
"""Minimal distance between two shapes and the points on each shape"""
- if self._wrapped is None or (
- isinstance(other, Shape) and other.wrapped is None
- ):
+ if self._wrapped is None or (isinstance(other, Shape) and not other):
raise ValueError("Cannot calculate distance to or from an empty shape")
if isinstance(other, Shape):
@@ -1176,7 +1174,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
dist_calc.LoadS1(self.wrapped)
for other_shape in others:
- if other_shape.wrapped is None:
+ if not other_shape:
raise ValueError("Cannot calculate distance to or from an empty shape")
dist_calc.LoadS2(other_shape.wrapped)
dist_calc.Perform()
@@ -1415,7 +1413,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
Returns:
"""
- if self._wrapped is None or other.wrapped is None:
+ if self._wrapped is None or not other:
return False
return self.wrapped.IsEqual(other.wrapped)
@@ -1430,7 +1428,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
Returns:
"""
- if self._wrapped is None or other.wrapped is None:
+ if self._wrapped is None or not other:
return False
return self.wrapped.IsSame(other.wrapped)
@@ -1877,7 +1875,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
raise ValueError("perimeter must be a closed Wire or Edge")
perimeter_edges = TopTools_SequenceOfShape()
for perimeter_edge in perimeter.edges():
- if perimeter_edge.wrapped is None:
+ if not perimeter_edge:
continue
perimeter_edges.Append(perimeter_edge.wrapped)
@@ -1885,7 +1883,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
lefts: list[Shell] = []
rights: list[Shell] = []
for target_shell in self.shells():
- if target_shell.wrapped is None:
+ if not target_shell:
continue
constructor = BRepFeat_SplitShape(target_shell.wrapped)
constructor.Add(perimeter_edges)
@@ -2214,7 +2212,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
Returns:
tuple[ShapeList[Vertex], ShapeList[Edge]]: section results
"""
- if self._wrapped is None or other.wrapped is None:
+ if self._wrapped is None or not other:
return (ShapeList(), ShapeList())
section = BRepAlgoAPI_Section(self.wrapped, other.wrapped)
@@ -2715,15 +2713,16 @@ class ShapeList(list[T]):
tol_digits,
)
- elif hasattr(group_by, "wrapped"):
- if group_by.wrapped is None:
- raise ValueError("Cannot group by an empty object")
+ elif not group_by:
+ raise ValueError("Cannot group by an empty object")
- if isinstance(group_by.wrapped, (TopoDS_Edge, TopoDS_Wire)):
+ elif hasattr(group_by, "wrapped") and isinstance(
+ group_by.wrapped, (TopoDS_Edge, TopoDS_Wire)
+ ):
- def key_f(obj):
- pnt1, _pnt2 = group_by.closest_points(obj.center())
- return round(group_by.param_at_point(pnt1), tol_digits)
+ def key_f(obj):
+ pnt1, _pnt2 = group_by.closest_points(obj.center())
+ return round(group_by.param_at_point(pnt1), tol_digits)
elif isinstance(group_by, SortBy):
if group_by == SortBy.LENGTH:
@@ -2829,22 +2828,22 @@ class ShapeList(list[T]):
).position.Z,
reverse=reverse,
)
- elif hasattr(sort_by, "wrapped"):
- if sort_by.wrapped is None:
- raise ValueError("Cannot sort by an empty object")
+ elif not sort_by:
+ raise ValueError("Cannot sort by an empty object")
+ elif hasattr(sort_by, "wrapped") and isinstance(
+ sort_by.wrapped, (TopoDS_Edge, TopoDS_Wire)
+ ):
- if isinstance(sort_by.wrapped, (TopoDS_Edge, TopoDS_Wire)):
+ def u_of_closest_center(obj) -> float:
+ """u-value of closest point between object center and sort_by"""
+ assert not isinstance(sort_by, SortBy)
+ pnt1, _pnt2 = sort_by.closest_points(obj.center())
+ return sort_by.param_at_point(pnt1)
- def u_of_closest_center(obj) -> float:
- """u-value of closest point between object center and sort_by"""
- assert not isinstance(sort_by, SortBy)
- pnt1, _pnt2 = sort_by.closest_points(obj.center())
- return sort_by.param_at_point(pnt1)
-
- # pylint: disable=unnecessary-lambda
- objects = sorted(
- self, key=lambda o: u_of_closest_center(o), reverse=reverse
- )
+ # pylint: disable=unnecessary-lambda
+ objects = sorted(
+ self, key=lambda o: u_of_closest_center(o), reverse=reverse
+ )
elif isinstance(sort_by, SortBy):
if sort_by == SortBy.LENGTH:
diff --git a/src/build123d/topology/two_d.py b/src/build123d/topology/two_d.py
index 65cee95..8c340a5 100644
--- a/src/build123d/topology/two_d.py
+++ b/src/build123d/topology/two_d.py
@@ -412,7 +412,7 @@ class Mixin2D(ABC, Shape[TOPODS]):
raise RuntimeError(
f"Length error of {length_error:.6f} exceeds tolerance {tolerance}"
)
- if wrapped_edge.wrapped is None or not wrapped_edge.is_valid:
+ if not wrapped_edge or not wrapped_edge.is_valid:
raise RuntimeError("Wrapped edge is invalid")
if not snap_to_face:
@@ -872,7 +872,7 @@ class Face(Mixin2D[TopoDS_Face]):
Returns:
Face: extruded shape
"""
- if obj.wrapped is None:
+ if not obj:
raise ValueError("Can't extrude empty object")
return Face(TopoDS.Face_s(_extrude_topods_shape(obj.wrapped, direction)))
@@ -982,7 +982,7 @@ class Face(Mixin2D[TopoDS_Face]):
)
return single_point_curve
- if shape.wrapped is None:
+ if not shape:
raise ValueError("input Edge cannot be empty")
adaptor = BRepAdaptor_Curve(shape.wrapped)
@@ -1105,7 +1105,7 @@ class Face(Mixin2D[TopoDS_Face]):
raise ValueError("exterior must be a Wire or list of Edges")
for edge in outside_edges:
- if edge.wrapped is None:
+ if not edge:
raise ValueError("exterior contains empty edges")
surface.Add(edge.wrapped, GeomAbs_C0)
@@ -1136,7 +1136,7 @@ class Face(Mixin2D[TopoDS_Face]):
if interior_wires:
makeface_object = BRepBuilderAPI_MakeFace(surface_face.wrapped)
for wire in interior_wires:
- if wire.wrapped is None:
+ if not wire:
raise ValueError("interior_wires contain an empty wire")
makeface_object.Add(wire.wrapped)
try:
@@ -1330,7 +1330,7 @@ class Face(Mixin2D[TopoDS_Face]):
) from err
result = result.fix()
- if not result.is_valid or result.wrapped is None:
+ if not result.is_valid or not result:
raise RuntimeError("Non planar face is invalid")
return result
@@ -2360,7 +2360,7 @@ class Shell(Mixin2D[TopoDS_Shell]):
obj = obj_list[0]
if isinstance(obj, Face):
- if obj.wrapped is None:
+ if not obj.wrapped:
raise ValueError(f"Can't create a Shell from empty Face")
builder = BRep_Builder()
shell = TopoDS_Shell()
diff --git a/src/build123d/vtk_tools.py b/src/build123d/vtk_tools.py
index 9d22185..a4af54a 100644
--- a/src/build123d/vtk_tools.py
+++ b/src/build123d/vtk_tools.py
@@ -80,7 +80,7 @@ def to_vtk_poly_data(
if not HAS_VTK:
warnings.warn("VTK not supported", stacklevel=2)
- if obj.wrapped is None:
+ if not obj:
raise ValueError("Cannot convert an empty shape")
vtk_shape = IVtkOCC_Shape(obj.wrapped)
From 6ce4a31355825233181cca478735133b138afa97 Mon Sep 17 00:00:00 2001
From: snoyer
Date: Tue, 21 Oct 2025 10:31:41 +0400
Subject: [PATCH 011/105] appease mypy
---
src/build123d/topology/three_d.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/build123d/topology/three_d.py b/src/build123d/topology/three_d.py
index ed3ba34..143e257 100644
--- a/src/build123d/topology/three_d.py
+++ b/src/build123d/topology/three_d.py
@@ -1269,7 +1269,7 @@ class Solid(Mixin3D[TopoDS_Solid]):
outer_wire = section
inner_wires = inner_wires if inner_wires else []
- shapes = []
+ shapes: list[Mixin3D[TopoDS_Shape]] = []
for wire in [outer_wire] + inner_wires:
builder = BRepOffsetAPI_MakePipeShell(Wire(path).wrapped)
From fb324adced38ca8aeb6808845aaecd16c034d7f0 Mon Sep 17 00:00:00 2001
From: Jonathan Wagenet
Date: Tue, 21 Oct 2025 12:57:03 -0400
Subject: [PATCH 012/105] Add 2d and 3d multi to_intersect cases, exception
cases
---
tests/test_direct_api/test_intersection.py | 10 ++++++++--
1 file changed, 8 insertions(+), 2 deletions(-)
diff --git a/tests/test_direct_api/test_intersection.py b/tests/test_direct_api/test_intersection.py
index 6262a23..696fa10 100644
--- a/tests/test_direct_api/test_intersection.py
+++ b/tests/test_direct_api/test_intersection.py
@@ -248,7 +248,6 @@ shape_2d_matrix = [
Case(fc2, ax1, None, "parallel/skew", None),
Case(fc3, ax1, [Vertex], "intersecting", None),
Case(fc1, ax1, [Edge], "collinear", None),
- # Case(fc7, ax1, [Vertex, Vertex], "multi intersect", None),
Case(fc1, pl3, None, "parallel/skew", None),
Case(fc1, pl1, [Edge], "intersecting", None),
@@ -283,7 +282,9 @@ shape_2d_matrix = [
Case(sh4, fc1, [Face, Face], "2 coplanar", None),
Case(sh5, fc1, [Edge, Edge], "2 intersecting", None),
- # Case(sh5, fc1, [Edge], "multi to_intersect, 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))
@@ -344,6 +345,9 @@ shape_3d_matrix = [
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))
@@ -410,6 +414,8 @@ def test_issues(obj, target, expected):
exception_matrix = [
Case(vt1, Color(), None, "Unsupported type", None),
Case(ed1, Color(), None, "Unsupported type", None),
+ Case(fc1, Color(), None, "Unsupported type", None),
+ Case(sl1, Color(), None, "Unsupported type", None),
]
@pytest.mark.skip
From 9a6c382ced3df76f2f12dea14f5e81fa78ee9f64 Mon Sep 17 00:00:00 2001
From: Jonathan Wagenet
Date: Tue, 21 Oct 2025 13:31:14 -0400
Subject: [PATCH 013/105] Replace Face.make_plane() with Face(Plane) to match
Edge(Axis)
---
src/build123d/topology/two_d.py | 20 ++++++++------------
tests/test_direct_api/test_face.py | 4 ++--
tests/test_direct_api/test_shape.py | 4 ++--
3 files changed, 12 insertions(+), 16 deletions(-)
diff --git a/src/build123d/topology/two_d.py b/src/build123d/topology/two_d.py
index 8b8f264..7c89746 100644
--- a/src/build123d/topology/two_d.py
+++ b/src/build123d/topology/two_d.py
@@ -449,7 +449,7 @@ class Face(Mixin2D, Shape[TopoDS_Face]):
@overload
def __init__(
self,
- obj: TopoDS_Face,
+ obj: TopoDS_Face | Plane,
label: str = "",
color: Color | None = None,
parent: Compound | None = None,
@@ -457,7 +457,7 @@ class Face(Mixin2D, Shape[TopoDS_Face]):
"""Build a Face from an OCCT TopoDS_Shape/TopoDS_Face
Args:
- obj (TopoDS_Shape, optional): OCCT Face.
+ obj (TopoDS_Shape | Plane, optional): OCCT Face or Plane.
label (str, optional): Defaults to ''.
color (Color, optional): Defaults to None.
parent (Compound, optional): assembly parent. Defaults to None.
@@ -487,7 +487,9 @@ class Face(Mixin2D, Shape[TopoDS_Face]):
if args:
l_a = len(args)
- if isinstance(args[0], TopoDS_Shape):
+ if isinstance(args[0], Plane):
+ obj = args[0]
+ elif isinstance(args[0], TopoDS_Shape):
obj, label, color, parent = args[:4] + (None,) * (4 - l_a)
elif isinstance(args[0], Wire):
outer_wire, inner_wires, label, color, parent = args[:5] + (None,) * (
@@ -516,6 +518,9 @@ class Face(Mixin2D, Shape[TopoDS_Face]):
color = kwargs.get("color", color)
parent = kwargs.get("parent", parent)
+ if isinstance(obj, Plane):
+ obj = BRepBuilderAPI_MakeFace(obj.wrapped).Face()
+
if outer_wire is not None:
inner_topods_wires = (
[w.wrapped for w in inner_wires] if inner_wires is not None else []
@@ -1009,15 +1014,6 @@ class Face(Mixin2D, Shape[TopoDS_Face]):
).Face()
)
- @classmethod
- def make_plane(
- cls,
- plane: Plane = Plane.XY,
- ) -> Face:
- """Create a unlimited size Face aligned with plane"""
- pln_shape = BRepBuilderAPI_MakeFace(plane.wrapped).Face()
- return cls(pln_shape)
-
@classmethod
def make_rect(cls, width: float, height: float, plane: Plane = Plane.XY) -> Face:
"""make_rect
diff --git a/tests/test_direct_api/test_face.py b/tests/test_direct_api/test_face.py
index f8619c5..2b71763 100644
--- a/tests/test_direct_api/test_face.py
+++ b/tests/test_direct_api/test_face.py
@@ -130,8 +130,8 @@ class TestFace(unittest.TestCase):
distance=1, distance2=2, vertices=[vertex], edge=other_edge
)
- def test_make_rect(self):
- test_face = Face.make_plane()
+ def test_plane_as_face(self):
+ test_face = Face(Plane.XY)
self.assertAlmostEqual(test_face.normal_at(), (0, 0, 1), 5)
def test_length_width(self):
diff --git a/tests/test_direct_api/test_shape.py b/tests/test_direct_api/test_shape.py
index 2c0bb3c..4f69dbf 100644
--- a/tests/test_direct_api/test_shape.py
+++ b/tests/test_direct_api/test_shape.py
@@ -475,7 +475,7 @@ class TestShape(unittest.TestCase):
self.assertAlmostEqual(Vector(verts[0]), (1, 2, 0), 5)
self.assertListEqual(edges, [])
- verts, edges = Vertex(1, 2, 0)._ocp_section(Face.make_plane(Plane.XY))
+ verts, edges = Vertex(1, 2, 0)._ocp_section(Face(Plane.XY))
self.assertAlmostEqual(Vector(verts[0]), (1, 2, 0), 5)
self.assertListEqual(edges, [])
@@ -493,7 +493,7 @@ class TestShape(unittest.TestCase):
self.assertEqual(len(edges1), 1)
self.assertAlmostEqual(edges1[0].length, 20, 5)
- vertices2, edges2 = cylinder._ocp_section(Face.make_plane(pln))
+ vertices2, edges2 = cylinder._ocp_section(Face(pln))
self.assertEqual(len(vertices2), 1)
self.assertEqual(len(edges2), 1)
self.assertAlmostEqual(Vector(vertices2[0]), (5, 0, 0), 5)
From 89dedd0888504a0d1344eb1f3785e60255c807c5 Mon Sep 17 00:00:00 2001
From: Jonathan Wagenet
Date: Tue, 21 Oct 2025 14:03:22 -0400
Subject: [PATCH 014/105] Add lexer to surface tuts
---
docs/tutorial_spitfire_wing_gordon.rst | 6 +++
docs/tutorial_surface_heart_token.rst | 53 +++++++++++++++-----------
2 files changed, 36 insertions(+), 23 deletions(-)
diff --git a/docs/tutorial_spitfire_wing_gordon.rst b/docs/tutorial_spitfire_wing_gordon.rst
index 716f862..18dd1f2 100644
--- a/docs/tutorial_spitfire_wing_gordon.rst
+++ b/docs/tutorial_spitfire_wing_gordon.rst
@@ -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.
.. literalinclude:: spitfire_wing_gordon.py
+ :language: build123d
:start-after: [Code]
: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.
.. literalinclude:: spitfire_wing_gordon.py
+ :language: build123d
:start-after: [AirfoilSizes]
:end-before: [Airfoils]
@@ -56,6 +58,7 @@ shifted so the leading edge fraction is aligned—then scale to the chord length
from Step 2.
.. literalinclude:: spitfire_wing_gordon.py
+ :language: build123d
:start-after: [Airfoils]
: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.
.. literalinclude:: spitfire_wing_gordon.py
+ :language: build123d
:start-after: [Profiles]
: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.
.. literalinclude:: spitfire_wing_gordon.py
+ :language: build123d
:start-after: [Solid]
:end-before: [End]
@@ -102,5 +107,6 @@ Complete listing
For convenience, here is the full script in one block:
.. literalinclude:: spitfire_wing_gordon.py
+ :language: build123d
:start-after: [Code]
:end-before: [End]
diff --git a/docs/tutorial_surface_heart_token.rst b/docs/tutorial_surface_heart_token.rst
index 2c45f62..ac819ad 100644
--- a/docs/tutorial_surface_heart_token.rst
+++ b/docs/tutorial_surface_heart_token.rst
@@ -7,9 +7,9 @@ a heart‑shaped token from a small set of non‑planar faces. We’ll create
non‑planar surfaces, mirror them, add side faces, and assemble a closed shell
into a solid.
-As described in the `topology_` section, a BREP model consists of vertices, edges, faces,
-and other elements that define the boundary of an object. When creating objects with
-non-planar faces, it is often more convenient to explicitly create the boundary faces of
+As described in the `topology_` section, a BREP model consists of vertices, edges, faces,
+and other elements that define the boundary of an object. When creating objects with
+non-planar faces, it is often more convenient to explicitly create the boundary faces of
the object. To illustrate this process, we will create the following game token:
.. raw:: html
@@ -22,13 +22,14 @@ Useful :class:`~topology.Face` creation methods include
and :meth:`~topology.Face.make_surface_from_array_of_points`. See the
:doc:`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.
-To create the perimeter, we'll define the perimeter edges. Since the heart is
+To create the perimeter, we'll define the perimeter edges. Since the heart is
symmetric, we'll only create half of its surface here:
.. literalinclude:: heart_token.py
+ :language: build123d
:start-after: [Code]
: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:
.. literalinclude:: heart_token.py
+ :language: build123d
:start-after: [SurfaceEdges]
:end-before: [SurfacePoint]
We will then use this point to create a non-planar ``Face``:
.. literalinclude:: heart_token.py
+ :language: build123d
:start-after: [SurfacePoint]
:end-before: [Surface]
@@ -55,21 +58,23 @@ We will then use this point to create a non-planar ``Face``:
:align: center
:alt: token perimeter
-Note that the surface was raised up by 0.5 using an Algebra expression with Pos. Also,
-note that the ``-`` in front of ``Face`` simply flips the face normal so that the colored
+Note that the surface was raised up by 0.5 using an Algebra expression with Pos. Also,
+note that the ``-`` in front of ``Face`` simply flips the face normal so that the colored
side is up, which isn't necessary but helps with viewing.
-Now that one half of the top of the heart has been created, the remainder of the top
+Now that one half of the top of the heart has been created, the remainder of the top
and bottom can be created by mirroring:
.. literalinclude:: heart_token.py
+ :language: build123d
:start-after: [Surface]
:end-before: [Surfaces]
-The sides of the heart are going to be created by extruding the outside of the perimeter
+The sides of the heart are going to be created by extruding the outside of the perimeter
as follows:
.. literalinclude:: heart_token.py
+ :language: build123d
:start-after: [Surfaces]
:end-before: [Sides]
@@ -77,11 +82,12 @@ as follows:
:align: center
:alt: token sides
-With the top, bottom, and sides, the complete boundary of the object is defined. We can
-now put them together, first into a :class:`~topology.Shell` and then into a
+With the top, bottom, and sides, the complete boundary of the object is defined. We can
+now put them together, first into a :class:`~topology.Shell` and then into a
:class:`~topology.Solid`:
.. literalinclude:: heart_token.py
+ :language: build123d
:start-after: [Sides]
:end-before: [Solid]
@@ -90,32 +96,33 @@ now put them together, first into a :class:`~topology.Shell` and then into a
:alt: token heart solid
.. note::
- When creating a Solid from a Shell, the Shell must be "water-tight," meaning it
- should have no holes. For objects with complex Edges, it's best practice to reuse
- Edges in adjoining Faces whenever possible to avoid slight mismatches that can
+ When creating a Solid from a Shell, the Shell must be "water-tight," meaning it
+ should have no holes. For objects with complex Edges, it's best practice to reuse
+ Edges in adjoining Faces whenever possible to avoid slight mismatches that can
create openings.
-Finally, we'll create the frame around the heart as a simple extrusion of a planar
+Finally, we'll create the frame around the heart as a simple extrusion of a planar
shape defined by the perimeter of the heart and merge all of the components together:
.. literalinclude:: heart_token.py
+ :language: build123d
:start-after: [Solid]
:end-before: [End]
-Note that an additional planar line is used to close ``l1`` and ``l3`` so a ``Face``
-can be created. The :func:`~operations_generic.offset` function defines the outside of
+Note that an additional planar line is used to close ``l1`` and ``l3`` so a ``Face``
+can be created. The :func:`~operations_generic.offset` function defines the outside of
the frame as a constant distance from the heart itself.
Summary
-------
-In this tutorial, we've explored surface modeling techniques to create a non-planar
+In this tutorial, we've explored surface modeling techniques to create a non-planar
heart-shaped object using build123d. By utilizing methods from the :class:`~topology.Face`
-class, such as :meth:`~topology.Face.make_surface`, we constructed the perimeter and
-central point of the surface. We then assembled the complete boundary of the object
-by creating the top, bottom, and sides, and combined them into a :class:`~topology.Shell`
-and eventually a :class:`~topology.Solid`. Finally, we added a frame around the heart
-using the :func:`~operations_generic.offset` function to maintain a constant distance
+class, such as :meth:`~topology.Face.make_surface`, we constructed the perimeter and
+central point of the surface. We then assembled the complete boundary of the object
+by creating the top, bottom, and sides, and combined them into a :class:`~topology.Shell`
+and eventually a :class:`~topology.Solid`. Finally, we added a frame around the heart
+using the :func:`~operations_generic.offset` function to maintain a constant distance
from the heart.
Next steps
From cfd45465854b0c750b65e57f96236ef010de321e Mon Sep 17 00:00:00 2001
From: Jonathan Wagenet
Date: Fri, 24 Oct 2025 22:36:56 -0400
Subject: [PATCH 015/105] Add Compound tests
---
tests/test_direct_api/test_intersection.py | 58 +++++++++++++++++++++-
1 file changed, 57 insertions(+), 1 deletion(-)
diff --git a/tests/test_direct_api/test_intersection.py b/tests/test_direct_api/test_intersection.py
index 696fa10..bdee46b 100644
--- a/tests/test_direct_api/test_intersection.py
+++ b/tests/test_direct_api/test_intersection.py
@@ -323,7 +323,7 @@ shape_3d_matrix = [
Case(sl1, ed3, None, "non-coincident", None),
Case(sl1, ed1, [Edge], "intersecting", None),
- Case(sl1, Pos(0, 1, 1) * ed1, [Edge], "edge collinear", "BRepAlgoAPI_Common and _Section both return edge"),
+ 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),
@@ -354,6 +354,61 @@ shape_3d_matrix = [
def test_shape_3d(obj, target, expected):
run_test(obj, target, expected)
+# Compound Shapes
+cp1 = Compound() + GridLocations(5, 0, 2, 1) * Vertex()
+cp2 = Compound() + GridLocations(5, 0, 2, 1) * Line((0, -1), (0, 1))
+cp3 = Compound() + GridLocations(5, 0, 2, 1) * Rectangle(2, 2)
+cp4 = Compound() + GridLocations(5, 0, 2, 1) * Box(2, 2, 2)
+
+cv1 = Curve() + [ed1, ed2, ed3]
+sk1 = Sketch() + [fc1, fc2, fc3]
+pt1 = Part() + [sl1, sl2, sl3]
+
+
+shape_compound_matrix = [
+ Case(cp1, vl1, None, "non-coincident", None),
+ Case(Pos(-.5) * cp1, vl1, [Vertex], "intersecting", None),
+
+ Case(cp2, lc1, None, "non-coincident", None),
+ Case(Pos(-.5) * cp2, lc1, [Vertex], "intersecting", None),
+
+ Case(Pos(Z=1) * cp3, ax1, None, "non-coincident", None),
+ Case(cp3, ax1, [Edge, Edge], "intersecting", None),
+
+ Case(Pos(Z=3) * cp4, pl2, None, "non-coincident", None),
+ Case(cp4, pl2, [Face, Face], "intersecting", None),
+
+ Case(cp1, vt1, None, "non-coincident", None),
+ Case(Pos(-.5) * cp1, vt1, [Vertex], "intersecting", None),
+
+ Case(Pos(Z=1) * cp2, ed1, None, "non-coincident", None),
+ Case(cp2, ed1, [Vertex], "intersecting", None),
+
+ Case(Pos(Z=1) * cp3, fc1, None, "non-coincident", None),
+ Case(cp3, fc1, [Face, Face], "intersecting", None),
+
+ Case(Pos(Z=5) * cp4, sl1, None, "non-coincident", None),
+ Case(Pos(2) * cp4, sl1, [Solid], "intersecting", None),
+
+ Case(cp1, Pos(Z=1) * cp1, None, "non-coincident", None),
+ Case(cp1, cp2, [Vertex, Vertex], "intersecting", None),
+ Case(cp2, cp3, [Edge, Edge], "intersecting", None),
+ Case(cp3, cp4, [Face, Face], "intersecting", None),
+
+ Case(cp1, Compound(children=cp1.get_type(Vertex)), [Vertex, Vertex], "mixed child type", None),
+ Case(cp4, Compound(children=cp3.get_type(Face)), [Face, Face], "mixed child type", None),
+
+ Case(cp2, [cp3, cp4], [Edge, Edge], "multi to_intersect, intersecting", None),
+
+ Case(cv1, cp3, [Edge, Edge], "intersecting", "duplicate edges, BRepAlgoAPI_Common and _Section both return edge"),
+ Case(sk1, cp3, [Face, Face], "intersecting", None),
+ Case(pt1, cp3, [Face, Face], "intersecting", None),
+
+]
+
+@pytest.mark.parametrize("obj, target, expected", make_params(shape_compound_matrix))
+def test_shape_compound(obj, target, expected):
+ run_test(obj, target, expected)
# FreeCAD issue example
c1 = CenterArc((0, 0), 10, 0, 360).edge()
@@ -416,6 +471,7 @@ exception_matrix = [
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
From a7b554001f9db3d124fe44cec8a8a163a7f3e622 Mon Sep 17 00:00:00 2001
From: Jonathan Wagenet
Date: Fri, 24 Oct 2025 22:37:28 -0400
Subject: [PATCH 016/105] Add intersect method to Compound, similar to 2d and
3d
---
src/build123d/topology/composite.py | 157 +++++++++++++++++++++++++++-
src/build123d/topology/three_d.py | 11 +-
src/build123d/topology/two_d.py | 5 +-
3 files changed, 163 insertions(+), 10 deletions(-)
diff --git a/src/build123d/topology/composite.py b/src/build123d/topology/composite.py
index 823eece..b5964a9 100644
--- a/src/build123d/topology/composite.py
+++ b/src/build123d/topology/composite.py
@@ -58,13 +58,12 @@ import copy
import os
import sys
import warnings
-from itertools import combinations
-from typing import Type, Union
-
from collections.abc import Iterable, Iterator, Sequence
+from itertools import combinations
+from typing_extensions import Self
import OCP.TopAbs as ta
-from OCP.BRepAlgoAPI import BRepAlgoAPI_Fuse
+from OCP.BRepAlgoAPI import BRepAlgoAPI_Common, BRepAlgoAPI_Fuse, BRepAlgoAPI_Section
from OCP.Font import (
Font_FA_Bold,
Font_FA_BoldItalic,
@@ -107,7 +106,6 @@ from build123d.geometry import (
VectorLike,
logger,
)
-from typing_extensions import Self
from .one_d import Edge, Wire, Mixin1D
from .shape_core import (
@@ -711,6 +709,155 @@ class Compound(Mixin3D, Shape[TopoDS_Compound]):
return results
+ def intersect(
+ self, *to_intersect: Shape | Vector | Location | Axis | Plane
+ ) -> None | ShapeList[Vertex | Edge | Face | Solid]:
+ """Intersect Compound with Shape or geometry object
+
+ Args:
+ to_intersect (Shape | Vector | Location | Axis | Plane): objects to intersect
+
+ Returns:
+ ShapeList[Vertex | Edge | Face | Solid] | None: ShapeList of vertices, edges,
+ faces, and/or solids.
+ """
+
+ def to_vector(objs: Iterable) -> ShapeList:
+ return ShapeList([Vector(v) if isinstance(v, Vertex) else v for v in objs])
+
+ def to_vertex(objs: Iterable) -> ShapeList:
+ return ShapeList([Vertex(v) if isinstance(v, Vector) else v for v in objs])
+
+ def bool_op(
+ args: Sequence,
+ tools: Sequence,
+ operation: BRepAlgoAPI_Common | BRepAlgoAPI_Section,
+ ) -> ShapeList | None:
+ # Wrap Shape._bool_op for corrected output
+ intersections = args[0]._bool_op(args, tools, operation)
+ if isinstance(intersections, ShapeList):
+ return intersections or None
+ if isinstance(intersections, Shape) and not intersections.is_null:
+ return ShapeList([intersections])
+ return None
+
+ def expand_compound(compound: Compound) -> ShapeList:
+ shapes = ShapeList(compound.children)
+ for shape_type in [Vertex, Edge, Wire, Face, Shell, Solid]:
+ new = compound.get_type(shape_type)
+ if shape_type == Wire:
+ new = [edge for new_shape in new for edge in new_shape.edges()]
+ elif shape_type == Shell:
+ new = [face for new_shape in new for face in new_shape.faces()]
+ shapes.extend(new)
+ 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[Vertex | Edge | Face | Solid] = expand_compound(self)
+ target: ShapeList | Shape
+ for other in to_intersect:
+ # Conform target type
+ # Vertices need to be Vector for set()
+ match other:
+ case Axis():
+ target = Edge(other)
+ case Plane():
+ target = Face.make_plane(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[Vector | Edge | Face] = []
+ result: ShapeList | Shape | None
+ for obj in common_set:
+ match (obj, target):
+ case (Vertex(), Vertex()):
+ result = obj.intersect(target)
+
+ case (Edge(), Edge() | Wire()):
+ result = obj.intersect(target)
+
+ case (_, ShapeList()):
+ result = ShapeList()
+ for t in target:
+ if (
+ not isinstance(obj, Edge) and not isinstance(t, (Edge))
+ ) or (isinstance(obj, Solid) or isinstance(t, 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,), (t,), operation) or [])
+ operation = BRepAlgoAPI_Section()
+ result.extend(bool_op((obj,), (t,), operation) or [])
+
+ case _ if issubclass(type(target), Shape):
+ if isinstance(target, Wire):
+ targets = target.edges()
+ elif isinstance(target, Shell):
+ targets = target.faces()
+ else:
+ targets = ShapeList([target])
+
+ result = ShapeList()
+ for t in targets:
+ if (
+ not isinstance(obj, Edge) and not isinstance(t, (Edge))
+ ) or (isinstance(obj, Solid) or isinstance(t, 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,), (t,), operation) or [])
+ operation = BRepAlgoAPI_Section()
+ result.extend(bool_op((obj,), (t,), operation) or [])
+
+ if result:
+ common.extend(to_vector(result))
+
+ if common:
+ common_set = to_vertex(set(common))
+ common_set = filter_shapes_by_order(
+ common_set, [Vertex, Edge, Face, Solid]
+ )
+ else:
+ return None
+
+ return ShapeList(common_set)
+
def unwrap(self, fully: bool = True) -> Self | Shape:
"""Strip unnecessary Compound wrappers
diff --git a/src/build123d/topology/three_d.py b/src/build123d/topology/three_d.py
index 0331317..9e22ce5 100644
--- a/src/build123d/topology/three_d.py
+++ b/src/build123d/topology/three_d.py
@@ -422,7 +422,7 @@ class Mixin3D(Shape):
def intersect(
self, *to_intersect: Shape | Vector | Location | Axis | Plane
- ) -> None | ShapeList[Vertex | Edge | Face | Shape]:
+ ) -> None | ShapeList[Vertex | Edge | Face | Solid]:
"""Intersect Solid with Shape or geometry object
Args:
@@ -448,7 +448,7 @@ class Mixin3D(Shape):
intersections = args[0]._bool_op(args, tools, operation)
if isinstance(intersections, ShapeList):
return intersections or None
- if (isinstance(intersections, Shape) and not intersections.is_null):
+ if isinstance(intersections, Shape) and not intersections.is_null:
return ShapeList([intersections])
return None
@@ -476,7 +476,7 @@ class Mixin3D(Shape):
return filtered_shapes
- common_set: ShapeList[Vertex | Edge | Face] = ShapeList(self.solids())
+ common_set: ShapeList[Vertex | Edge | Face | Solid] = ShapeList(self.solids())
target: ShapeList | Shape
for other in to_intersect:
# Conform target type
@@ -506,7 +506,7 @@ class Mixin3D(Shape):
case (Edge(), Edge() | Wire()):
result = obj.intersect(target)
- case _ if issubclass(type(target), Shape):
+ case (_, Vertex() | Edge() | Wire() | Face() | Shell() | Solid()):
if isinstance(target, Wire):
targets = target.edges()
elif isinstance(target, Shell):
@@ -528,6 +528,9 @@ class Mixin3D(Shape):
operation = BRepAlgoAPI_Section()
result.extend(bool_op((obj,), (t,), operation) or [])
+ case _ if issubclass(type(target), Shape):
+ result = target.intersect(obj)
+
if result:
common.extend(to_vector(result))
diff --git a/src/build123d/topology/two_d.py b/src/build123d/topology/two_d.py
index 0195c30..2a96f15 100644
--- a/src/build123d/topology/two_d.py
+++ b/src/build123d/topology/two_d.py
@@ -352,7 +352,7 @@ class Mixin2D(ABC, Shape):
case (Edge(), Edge() | Wire()):
result = obj.intersect(target)
- case _ if issubclass(type(target), Shape):
+ case (_, Vertex() | Edge() | Wire() | Face() | Shell()):
if isinstance(target, Wire):
targets = target.edges()
elif isinstance(target, Shell):
@@ -371,6 +371,9 @@ class Mixin2D(ABC, Shape):
operation = BRepAlgoAPI_Section()
result.extend(bool_op((obj,), (t,), operation) or [])
+ case _ if issubclass(type(target), Shape):
+ result = target.intersect(obj)
+
if result:
common.extend(to_vector(result))
From c13ef47cef35de0a593051658e7d1f00831dbbb8 Mon Sep 17 00:00:00 2001
From: Jonathan Wagenet
Date: Tue, 28 Oct 2025 23:33:29 -0400
Subject: [PATCH 017/105] Correct ex26 by revolving 180 and removing mirror
which creates invalid shape
---
examples/extrude.py | 3 +--
examples/extrude_algebra.py | 4 ++--
2 files changed, 3 insertions(+), 4 deletions(-)
diff --git a/examples/extrude.py b/examples/extrude.py
index fd30edb..e2f645a 100644
--- a/examples/extrude.py
+++ b/examples/extrude.py
@@ -68,8 +68,7 @@ with BuildPart() as ex26:
with BuildSketch() as ex26_sk:
with Locations((0, rev)):
Circle(rad)
- revolve(axis=Axis.X, revolution_arc=90)
- mirror(about=Plane.XZ)
+ revolve(axis=Axis.X, revolution_arc=180)
with BuildSketch() as ex26_sk2:
Rectangle(rad, rev)
ex26_target = ex26.part
diff --git a/examples/extrude_algebra.py b/examples/extrude_algebra.py
index e5340bb..6f3d6a6 100644
--- a/examples/extrude_algebra.py
+++ b/examples/extrude_algebra.py
@@ -26,8 +26,8 @@ rad, rev = 3, 25
# Extrude last
circle = Pos(0, rev) * Circle(rad)
-ex26_target = revolve(circle, Axis.X, revolution_arc=90)
-ex26_target = ex26_target + mirror(ex26_target, Plane.XZ)
+ex26_target = revolve(circle, Axis.X, revolution_arc=180)
+ex26_target = ex26_target
rect = Rectangle(rad, rev)
From 315605f485071340dce0bfa857d91d1639111e9c Mon Sep 17 00:00:00 2001
From: Jonathan Wagenet
Date: Tue, 28 Oct 2025 23:45:29 -0400
Subject: [PATCH 018/105] Correct area/volume calculations from intersect with
new return type of ShapeList
---
src/build123d/drafting.py | 4 ++--
src/build123d/topology/composite.py | 6 +-----
src/build123d/topology/two_d.py | 6 ++----
tests/test_direct_api/test_oriented_bound_box.py | 6 ++++--
tests/test_direct_api/test_shape.py | 5 +++--
tests/test_direct_api/test_solid.py | 8 ++++++--
tests/test_drafting.py | 3 ++-
7 files changed, 20 insertions(+), 18 deletions(-)
diff --git a/src/build123d/drafting.py b/src/build123d/drafting.py
index 07a5193..415d33e 100644
--- a/src/build123d/drafting.py
+++ b/src/build123d/drafting.py
@@ -453,7 +453,7 @@ class DimensionLine(BaseSketchObject):
if self_intersection is None:
self_intersection_area = 0.0
else:
- self_intersection_area = self_intersection.area
+ self_intersection_area = sum(f.area for f in self_intersection.faces())
d_line += placed_label
bbox_size = d_line.bounding_box().diagonal
@@ -467,7 +467,7 @@ class DimensionLine(BaseSketchObject):
if line_intersection is None:
common_area = 0.0
else:
- common_area = line_intersection.area
+ common_area = sum(f.area for f in line_intersection.faces())
common_area += self_intersection_area
score = (d_line.area - 10 * common_area) / bbox_size
d_lines[d_line] = score
diff --git a/src/build123d/topology/composite.py b/src/build123d/topology/composite.py
index b5964a9..0849e69 100644
--- a/src/build123d/topology/composite.py
+++ b/src/build123d/topology/composite.py
@@ -649,11 +649,7 @@ class Compound(Mixin3D, Shape[TopoDS_Compound]):
children[child_index_pair[1]]
)
if obj_intersection is not None:
- common_volume = (
- 0.0
- if isinstance(obj_intersection, list)
- else obj_intersection.volume
- )
+ common_volume = sum(s.volume for s in obj_intersection.solids())
if common_volume > tolerance:
return (
True,
diff --git a/src/build123d/topology/two_d.py b/src/build123d/topology/two_d.py
index 2a96f15..621062b 100644
--- a/src/build123d/topology/two_d.py
+++ b/src/build123d/topology/two_d.py
@@ -778,15 +778,13 @@ class Face(Mixin2D, Shape[TopoDS_Face]):
).sort_by(Axis(cog, cross_dir))
bottom_area = sum(f.area for f in bottom_list)
- intersect_area = 0.0
for flipped_face, bottom_face in zip(top_flipped_list, bottom_list):
intersection = flipped_face.intersect(bottom_face)
- if intersection is None or isinstance(intersection, list):
+ if intersection is None:
intersect_area = -1.0
break
else:
- assert isinstance(intersection, Face)
- intersect_area += intersection.area
+ intersect_area = sum(f.area for f in intersection.faces())
if intersect_area == -1.0:
continue
diff --git a/tests/test_direct_api/test_oriented_bound_box.py b/tests/test_direct_api/test_oriented_bound_box.py
index bcdb566..a083f7b 100644
--- a/tests/test_direct_api/test_oriented_bound_box.py
+++ b/tests/test_direct_api/test_oriented_bound_box.py
@@ -229,13 +229,15 @@ class TestOrientedBoundBox(unittest.TestCase):
obb = OrientedBoundBox(rect)
corners = obb.corners
poly = Polygon(*corners, align=None)
- self.assertAlmostEqual(rect.intersect(poly).area, rect.area, 5)
+ area = sum(f.area for f in rect.intersect(poly).faces())
+ self.assertAlmostEqual(area, rect.area, 5)
for face in Box(1, 2, 3).faces():
obb = OrientedBoundBox(face)
corners = obb.corners
poly = Polygon(*corners, align=None)
- self.assertAlmostEqual(face.intersect(poly).area, face.area, 5)
+ area = sum(f.area for f in face.intersect(poly).faces())
+ self.assertAlmostEqual(area, face.area, 5)
def test_line_corners(self):
"""
diff --git a/tests/test_direct_api/test_shape.py b/tests/test_direct_api/test_shape.py
index 2c0bb3c..a394d9e 100644
--- a/tests/test_direct_api/test_shape.py
+++ b/tests/test_direct_api/test_shape.py
@@ -299,7 +299,8 @@ class TestShape(unittest.TestCase):
predicted_location = Location(offset) * Rotation(*rotation)
located_shape = Solid.make_box(1, 1, 1).locate(predicted_location)
intersect = shape.intersect(located_shape)
- self.assertAlmostEqual(intersect.volume, 1, 5)
+ volume = sum(s.volume for s in intersect.solids())
+ self.assertAlmostEqual(volume, 1, 5)
def test_position_and_orientation(self):
box = Solid.make_box(1, 1, 1).locate(Location((1, 2, 3), (10, 20, 30)))
@@ -588,7 +589,7 @@ class TestShape(unittest.TestCase):
empty.distance_to_with_closest_points(Vector(1, 1, 1))
with self.assertRaises(ValueError):
empty.distance_to(Vector(1, 1, 1))
- with self.assertRaises(ValueError):
+ with self.assertRaises(AttributeError):
box.intersect(empty_loc)
self.assertEqual(empty._ocp_section(Vertex(1, 1, 1)), ([], []))
self.assertEqual(empty.faces_intersected_by_axis(Axis.Z), ShapeList())
diff --git a/tests/test_direct_api/test_solid.py b/tests/test_direct_api/test_solid.py
index a0fa0f3..75fad74 100644
--- a/tests/test_direct_api/test_solid.py
+++ b/tests/test_direct_api/test_solid.py
@@ -153,7 +153,9 @@ class TestSolid(unittest.TestCase):
self.assertAlmostEqual(twist.volume, 1, 5)
top = twist.faces().sort_by(Axis.Z)[-1].rotate(Axis.Z, 45)
bottom = twist.faces().sort_by(Axis.Z)[0]
- self.assertAlmostEqual(top.translate((0, 0, -1)).intersect(bottom).area, 1, 5)
+ intersect = top.translate((0, 0, -1)).intersect(bottom)
+ area = sum(f.area for f in intersect.faces())
+ self.assertAlmostEqual(area, 1, 5)
# Wire
base = Wire.make_rect(1, 1)
twist = Solid.extrude_linear_with_rotation(
@@ -162,7 +164,9 @@ class TestSolid(unittest.TestCase):
self.assertAlmostEqual(twist.volume, 1, 5)
top = twist.faces().sort_by(Axis.Z)[-1].rotate(Axis.Z, 45)
bottom = twist.faces().sort_by(Axis.Z)[0]
- self.assertAlmostEqual(top.translate((0, 0, -1)).intersect(bottom).area, 1, 5)
+ intersect = top.translate((0, 0, -1)).intersect(bottom)
+ area = sum(f.area for f in intersect.faces())
+ self.assertAlmostEqual(area, 1, 5)
def test_make_loft(self):
loft = Solid.make_loft(
diff --git a/tests/test_drafting.py b/tests/test_drafting.py
index 1bb97ab..2f1b301 100644
--- a/tests/test_drafting.py
+++ b/tests/test_drafting.py
@@ -231,7 +231,8 @@ class DimensionLineTestCase(unittest.TestCase):
],
draft=metric,
)
- self.assertGreater(hole.intersect(d_line).area, 0)
+ area = sum(f.area for f in hole.intersect(d_line).faces())
+ self.assertGreater(area, 0)
def test_outside_arrows(self):
d_line = DimensionLine([(0, 0, 0), (15, 0, 0)], draft=metric)
From 069b691964100117888b35fb940016f681af7743 Mon Sep 17 00:00:00 2001
From: Jonathan Wagenet
Date: Tue, 28 Oct 2025 23:56:29 -0400
Subject: [PATCH 019/105] Conform Shape.intersect to None | ShapeList
---
src/build123d/topology/shape_core.py | 21 +++++++++------------
1 file changed, 9 insertions(+), 12 deletions(-)
diff --git a/src/build123d/topology/shape_core.py b/src/build123d/topology/shape_core.py
index 6402c3e..d366ead 100644
--- a/src/build123d/topology/shape_core.py
+++ b/src/build123d/topology/shape_core.py
@@ -1327,7 +1327,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
def intersect(
self, *to_intersect: Shape | Vector | Location | Axis | Plane
- ) -> None | Self | ShapeList[Self]:
+ ) -> None | ShapeList[Self]:
"""Intersection of the arguments and this shape
Args:
@@ -1335,8 +1335,8 @@ class Shape(NodeMixin, Generic[TOPODS]):
intersect with
Returns:
- Self | ShapeList[Self]: Resulting object may be of a different class than self
- or a ShapeList if multiple non-Compound object created
+ None | ShapeList[Self]: Resulting ShapeList may contain different class
+ than self
"""
def _to_vertex(vec: Vector) -> Vertex:
@@ -1380,15 +1380,12 @@ class Shape(NodeMixin, Generic[TOPODS]):
# Find the shape intersections
intersect_op = BRepAlgoAPI_Common()
- shape_intersections = self._bool_op((self,), objs, intersect_op)
- if isinstance(shape_intersections, ShapeList) and not shape_intersections:
- return None
- if (
- not isinstance(shape_intersections, ShapeList)
- and shape_intersections.is_null
- ):
- return None
- return shape_intersections
+ intersections = self._bool_op((self,), objs, intersect_op)
+ if isinstance(intersections, ShapeList):
+ return intersections or None
+ if isinstance(intersections, Shape) and not intersections.is_null:
+ return ShapeList([intersections])
+ return None
def is_equal(self, other: Shape) -> bool:
"""Returns True if two shapes are equal, i.e. if they share the same
From 5d7b0983791ca9484661439034ed5b74d6642194 Mon Sep 17 00:00:00 2001
From: Jonathan Wagenet
Date: Wed, 29 Oct 2025 00:16:02 -0400
Subject: [PATCH 020/105] Correct mode == Mode.INTERSECT to iterate
intersections instead of pass all in to_intersect
Shape.intersect(A, B) through BRepAlgoAPI_Common appears to treat tool as a single object such that intersection is Shape ^ (A + B). The updated intersect methods treat this intersection as Shape ^ A ^ B. The intersections in this change need to be interated to accomadate.
---
src/build123d/build_common.py | 8 +++++++-
1 file changed, 7 insertions(+), 1 deletion(-)
diff --git a/src/build123d/build_common.py b/src/build123d/build_common.py
index e5edde3..abfa4a0 100644
--- a/src/build123d/build_common.py
+++ b/src/build123d/build_common.py
@@ -466,7 +466,13 @@ class Builder(ABC, Generic[ShapeT]):
elif mode == Mode.INTERSECT:
if self._obj is None:
raise RuntimeError("Nothing to intersect with")
- combined = self._obj.intersect(*typed[self._shape])
+ intersections: ShapeList[Shape] = ShapeList()
+ for target in typed[self._shape]:
+ result = self._obj.intersect(target)
+ if result is None:
+ continue
+ intersections.extend(result)
+ combined = self._sub_class(intersections)
elif mode == Mode.REPLACE:
combined = self._sub_class(list(typed[self._shape]))
From 37135745193e89502e07fbb871bc8f180af4d421 Mon Sep 17 00:00:00 2001
From: Jonathan Wagenet
Date: Wed, 29 Oct 2025 13:02:31 -0400
Subject: [PATCH 021/105] Remove xfail notes from issue tests
---
tests/test_direct_api/test_intersection.py | 10 +++++-----
1 file changed, 5 insertions(+), 5 deletions(-)
diff --git a/tests/test_direct_api/test_intersection.py b/tests/test_direct_api/test_intersection.py
index bdee46b..daad2bf 100644
--- a/tests/test_direct_api/test_intersection.py
+++ b/tests/test_direct_api/test_intersection.py
@@ -453,11 +453,11 @@ w1 = Wire.make_circle(0.5)
f1 = Face(Wire.make_circle(0.5))
issues_matrix = [
- Case(t, t, [Face, Face], "issue #1015", "Returns Compound"),
- Case(l, s, [Edge], "issue #945", "Edge.intersect only takes 1D"),
- Case(a, b, [Edge], "issue #918", "Returns empty Compound"),
- Case(e1, w1, [Vertex, Vertex], "issue #697"),
- Case(e1, f1, [Edge], "issue #697", "Edge.intersect only takes 1D"),
+ Case(t, t, [Face, Face], "issue #1015", None),
+ Case(l, s, [Edge], "issue #945", None),
+ Case(a, b, [Edge], "issue #918", None),
+ Case(e1, w1, [Vertex, Vertex], "issue #697", None),
+ Case(e1, f1, [Edge], "issue #697", None),
]
@pytest.mark.parametrize("obj, target, expected", make_params(issues_matrix))
From 27567a10efe5d233acdac749d0582409e76800b5 Mon Sep 17 00:00:00 2001
From: snoyer
Date: Fri, 7 Nov 2025 21:29:06 +0400
Subject: [PATCH 022/105] fix typo
---
src/build123d/topology/two_d.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/build123d/topology/two_d.py b/src/build123d/topology/two_d.py
index 8c340a5..a41e249 100644
--- a/src/build123d/topology/two_d.py
+++ b/src/build123d/topology/two_d.py
@@ -2360,7 +2360,7 @@ class Shell(Mixin2D[TopoDS_Shell]):
obj = obj_list[0]
if isinstance(obj, Face):
- if not obj.wrapped:
+ if not obj:
raise ValueError(f"Can't create a Shell from empty Face")
builder = BRep_Builder()
shell = TopoDS_Shell()
From 3bea4d32284b791cddff5ef4ee106b680eab22c2 Mon Sep 17 00:00:00 2001
From: Jonathan Wagenet
Date: Fri, 7 Nov 2025 16:11:33 -0500
Subject: [PATCH 023/105] Re-add make_plane with depreciation warning
---
src/build123d/topology/two_d.py | 15 +++++++++++++++
1 file changed, 15 insertions(+)
diff --git a/src/build123d/topology/two_d.py b/src/build123d/topology/two_d.py
index 7c89746..82c09de 100644
--- a/src/build123d/topology/two_d.py
+++ b/src/build123d/topology/two_d.py
@@ -1014,6 +1014,21 @@ class Face(Mixin2D, Shape[TopoDS_Face]):
).Face()
)
+ @classmethod
+ def make_plane(
+ cls,
+ plane: Plane = Plane.XY,
+ ) -> Face:
+ """Create a unlimited size Face aligned with plane"""
+ warnings.warn(
+ "The 'make_plane' method is deprecated and will be removed in a future version.",
+ DeprecationWarning,
+ stacklevel=2,
+ )
+
+ pln_shape = BRepBuilderAPI_MakeFace(plane.wrapped).Face()
+ return cls(pln_shape)
+
@classmethod
def make_rect(cls, width: float, height: float, plane: Plane = Plane.XY) -> Face:
"""make_rect
From 513c50530c9ca5ebcae5034f3c4ec305b4c36ea9 Mon Sep 17 00:00:00 2001
From: gumyr
Date: Sat, 8 Nov 2025 10:13:03 -0500
Subject: [PATCH 024/105] Added support for Face/cone properties: enhanced
axis_of_rotation added semi_angle
---
src/build123d/topology/two_d.py | 17 +++++++++++++++++
1 file changed, 17 insertions(+)
diff --git a/src/build123d/topology/two_d.py b/src/build123d/topology/two_d.py
index 8b8f264..d8517ef 100644
--- a/src/build123d/topology/two_d.py
+++ b/src/build123d/topology/two_d.py
@@ -60,6 +60,7 @@ import sys
import warnings
from abc import ABC, abstractmethod
from collections.abc import Iterable, Sequence
+from math import degrees
from typing import TYPE_CHECKING, Any, TypeVar, overload
import OCP.TopAbs as ta
@@ -556,6 +557,11 @@ class Face(Mixin2D, Shape[TopoDS_Face]):
if type(self.geom_adaptor()) == Geom_RectangularTrimmedSurface:
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:
return Axis(
self.geom_adaptor().Cylinder().Axis() # type:ignore[attr-defined]
@@ -837,6 +843,17 @@ class Face(Mixin2D, Shape[TopoDS_Face]):
else:
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
def volume(self) -> float:
"""volume - the volume of this Face, which is always zero"""
From 5d84002aa5211ace05a39b4891df052a5a180a83 Mon Sep 17 00:00:00 2001
From: Jonathan Wagenet
Date: Wed, 12 Nov 2025 10:37:45 -0500
Subject: [PATCH 025/105] Add Color support for RGBA hex string
---
src/build123d/geometry.py | 32 ++++++++++++++++++++++++-----
tests/test_direct_api/test_color.py | 30 +++++++++++++++++++++++++++
2 files changed, 57 insertions(+), 5 deletions(-)
diff --git a/src/build123d/geometry.py b/src/build123d/geometry.py
index e4c0eeb..5952389 100644
--- a/src/build123d/geometry.py
+++ b/src/build123d/geometry.py
@@ -1160,8 +1160,8 @@ class Color:
Args:
color_like (ColorLike):
- name, ex: "red",
- name + alpha, ex: ("red", 0.5),
+ name, ex: "red" or "#ff0000",
+ name + alpha, ex: ("red", 0.5) or "#ff000080",
rgb, ex: (1., 0., 0.),
rgb + alpha, ex: (1., 0., 0., 0.5),
hex, ex: 0xff0000,
@@ -1172,7 +1172,7 @@ class Color:
@overload
def __init__(self, name: str, alpha: float = 1.0):
- """Color from name
+ """Color from name or hexadecimal string
`CSS3 Color Names
`
@@ -1180,8 +1180,10 @@ class Color:
`OCCT Color Names
`_
+ Hexadecimal string may be RGB or RGBA format with leading "#"
+
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
"""
@@ -1237,6 +1239,27 @@ class Color:
return
case str():
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():
color_code, alpha = fill_defaults(args, (color_code, alpha))
case float():
@@ -1340,7 +1363,6 @@ class Color:
@staticmethod
def _rgb_from_str(name: str) -> tuple:
- name = name.strip()
if "#" not in name:
try:
# Use css3 color names by default
diff --git a/tests/test_direct_api/test_color.py b/tests/test_direct_api/test_color.py
index 62c26bf..aab9254 100644
--- a/tests/test_direct_api/test_color.py
+++ b/tests/test_direct_api/test_color.py
@@ -137,6 +137,18 @@ class TestColor(unittest.TestCase):
" #ff0000 ",
("#ff0000",),
("#ff0000", 1),
+ "#ff0000ff",
+ " #ff0000ff ",
+ ("#ff0000ff",),
+ ("#ff0000ff", .6),
+ "#f00",
+ " #f00 ",
+ ("#f00",),
+ ("#f00", 1),
+ "#f00f",
+ " #f00f ",
+ ("#f00f",),
+ ("#f00f", .6),
0xff0000,
(0xff0000),
(0xff0000, 0xff),
@@ -164,6 +176,9 @@ class TestColor(unittest.TestCase):
Quantity_ColorRGBA(1, 0, 0, 0.6),
("red", 0.6),
("#ff0000", 0.6),
+ ("#ff000099"),
+ ("#f00", 0.6),
+ ("#f009"),
(0xff0000, 153),
(1., 0., 0., 0.6)
]
@@ -177,6 +192,21 @@ class TestColor(unittest.TestCase):
with self.assertRaises(ValueError):
Color("build123d")
+ with self.assertRaises(ValueError):
+ Color("#ff")
+
+ with self.assertRaises(ValueError):
+ Color("#ffg")
+
+ with self.assertRaises(ValueError):
+ Color("#fffff")
+
+ with self.assertRaises(ValueError):
+ Color("#fffg")
+
+ with self.assertRaises(ValueError):
+ Color("#fff00gg")
+
def test_bad_color_type(self):
with self.assertRaises(TypeError):
Color(dict({"name": "red", "alpha": 1}))
From cc34b5a743f5983825988b9e28c46568b4aacd86 Mon Sep 17 00:00:00 2001
From: Jonathan Wagenet
Date: Wed, 12 Nov 2025 12:18:30 -0500
Subject: [PATCH 026/105] Convert to pytest with parameterization and test ids
---
tests/test_direct_api/test_color.py | 317 ++++++++++++----------------
1 file changed, 140 insertions(+), 177 deletions(-)
diff --git a/tests/test_direct_api/test_color.py b/tests/test_direct_api/test_color.py
index aab9254..4fa91fe 100644
--- a/tests/test_direct_api/test_color.py
+++ b/tests/test_direct_api/test_color.py
@@ -27,198 +27,161 @@ license:
"""
import copy
-import unittest
import numpy as np
-from build123d.geometry import Color
+import pytest
+
from OCP.Quantity import Quantity_ColorRGBA
+from build123d.geometry import Color
-class TestColor(unittest.TestCase):
- # name + alpha overload
- def test_name1(self):
- c = Color("blue")
- np.testing.assert_allclose(tuple(c), (0, 0, 1, 1), 1e-5)
+# Overloads
+@pytest.mark.parametrize("color, expected", [
+ 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)
+@pytest.mark.parametrize("color, expected", [
+ 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)
- def test_name3(self):
- c = Color("blue", 0.5)
- np.testing.assert_allclose(tuple(c), (0, 0, 1, 0.5), 1e-5)
+@pytest.mark.parametrize("color, expected", [
+ pytest.param(Color(0x996692), (0x99 / 0xFF, 0x66 / 0xFF, 0x92 / 0xFF, 1), id="color_code"),
+ 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)
- # 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)
+@pytest.mark.parametrize("color, expected", [
+ 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)
- def test_rgba1(self):
- c = Color(1.0, 1.0, 0.0, 0.5)
- self.assertEqual(c.wrapped.GetRGB().Red(), 1.0)
- self.assertEqual(c.wrapped.GetRGB().Green(), 1.0)
- self.assertEqual(c.wrapped.GetRGB().Blue(), 0.0)
- self.assertEqual(c.wrapped.Alpha(), 0.5)
+# ColorLikes
+@pytest.mark.parametrize("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", .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", .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.), id="tuple rgb float"),
+ pytest.param((1., 0., 0., 1.), 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)
- 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)
+@pytest.mark.parametrize("color_like, expected", [
+ pytest.param(Color(), (1, 1, 1, 1), id="empty Color()"),
+ pytest.param(1., (1, 1, 1, 1), id="r float"),
+ pytest.param((1.,), (1, 1, 1, 1), id="tuple r float"),
+ pytest.param((1., 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)
- def test_rgba3(self):
- c = Color(red=0.1, green=0.2, blue=0.3, alpha=0.5)
- np.testing.assert_allclose(tuple(c), (0.1, 0.2, 0.3, 0.5), 1e-5)
+@pytest.mark.parametrize("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.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)
- # 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
- )
+# Exceptions
+@pytest.mark.parametrize("name", [
+ 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)
- c = Color(0x006692, 0x80)
- np.testing.assert_allclose(
- tuple(c), (0, 0x66 / 0xFF, 0x92 / 0xFF, 0x80 / 0xFF), 5
- )
+@pytest.mark.parametrize("color_type", [
+ pytest.param((dict({"name": "red", "alpha": 1},)), id="dict arg"),
+ pytest.param(("red", "blue"), id="str + str"),
+ pytest.param((1., "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)
- c = Color(0x006692, alpha=0x80)
- np.testing.assert_allclose(tuple(c), (0, 102 / 255, 146 / 255, 128 / 255), 1e-5)
+# Methods
+def test_rgba_wrapped():
+ c = Color(1.0, 1.0, 0.0, 0.5)
+ assert c.wrapped.GetRGB().Red() == 1.0
+ assert c.wrapped.GetRGB().Green() == 1.0
+ assert c.wrapped.GetRGB().Blue() == 0.0
+ assert c.wrapped.Alpha() == 0.5
- c = Color(color_code=0x996692, alpha=0xCC)
- np.testing.assert_allclose(
- tuple(c), (153 / 255, 102 / 255, 146 / 255, 204 / 255), 5
- )
+def test_to_tuple():
+ c = Color("blue", alpha=0.5)
+ np.testing.assert_allclose(tuple(c), (0, 0, 1, 0.5), rtol=1e-5)
- c = Color(0.0, 0.0, 1.0, 1.0)
- np.testing.assert_allclose(tuple(c), (0, 0, 1, 1), 1e-5)
+def test_copy():
+ c = Color(0.1, 0.2, 0.3, alpha=0.4)
+ c_copy = copy.copy(c)
+ np.testing.assert_allclose(tuple(c_copy), (0.1, 0.2, 0.3, 0.4), rtol=1e-5)
- c = Color(0, 0, 1, 1)
- np.testing.assert_allclose(tuple(c), (0, 0, 1, 1), 1e-5)
+def test_str_repr_is():
+ c = Color(1, 0, 0)
+ 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)"
- # 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):
- c = Color(0.1, 0.2, 0.3, alpha=0.4)
- c_copy = copy.copy(c)
- np.testing.assert_allclose(tuple(c_copy), (0.1, 0.2, 0.3, 0.4), 1e-5)
-
- 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)
- self.assertEqual(str(c), "Color: (1.0, 0.5, 0.0, 1.0) near 'DARKGOLDENROD1'")
- self.assertEqual(repr(c), "Color(1.0, 0.5, 0.0, 1.0)")
-
- 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
- def test_color_like(self):
- red_color_likes = [
- Quantity_ColorRGBA(1, 0, 0, 1),
- "red",
- "red ",
- ("red",),
- ("red", 1),
- "#ff0000",
- " #ff0000 ",
- ("#ff0000",),
- ("#ff0000", 1),
- "#ff0000ff",
- " #ff0000ff ",
- ("#ff0000ff",),
- ("#ff0000ff", .6),
- "#f00",
- " #f00 ",
- ("#f00",),
- ("#f00", 1),
- "#f00f",
- " #f00f ",
- ("#f00f",),
- ("#f00f", .6),
- 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 = [
- Quantity_ColorRGBA(1, 0, 0, 0.6),
- ("red", 0.6),
- ("#ff0000", 0.6),
- ("#ff000099"),
- ("#f00", 0.6),
- ("#f009"),
- (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")
-
- with self.assertRaises(ValueError):
- Color("#ff")
-
- with self.assertRaises(ValueError):
- Color("#ffg")
-
- with self.assertRaises(ValueError):
- Color("#fffff")
-
- with self.assertRaises(ValueError):
- Color("#fffg")
-
- with self.assertRaises(ValueError):
- Color("#fff00gg")
-
- def test_bad_color_type(self):
- with self.assertRaises(TypeError):
- Color(dict({"name": "red", "alpha": 1}))
-
- with self.assertRaises(TypeError):
- Color("red", "blue")
-
- with self.assertRaises(TypeError):
- Color(1., "blue")
-
- with self.assertRaises(TypeError):
- Color(1, "blue")
-
-if __name__ == "__main__":
- unittest.main()
+def test_str_repr_near():
+ 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)"
From 083cb1611cd4db739bd1b2e8239cfca074fd8399 Mon Sep 17 00:00:00 2001
From: Jonathan Wagenet
Date: Wed, 12 Nov 2025 12:29:48 -0500
Subject: [PATCH 027/105] Remove depreciated Color.to_tuple
---
src/build123d/geometry.py | 10 ----------
tests/test_direct_api/test_color.py | 4 ----
2 files changed, 14 deletions(-)
diff --git a/src/build123d/geometry.py b/src/build123d/geometry.py
index 5952389..e17d049 100644
--- a/src/build123d/geometry.py
+++ b/src/build123d/geometry.py
@@ -1319,16 +1319,6 @@ class Color:
self.iter_index += 1
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:
"""Return copy of self"""
return Color(*tuple(self))
diff --git a/tests/test_direct_api/test_color.py b/tests/test_direct_api/test_color.py
index 4fa91fe..650f5c9 100644
--- a/tests/test_direct_api/test_color.py
+++ b/tests/test_direct_api/test_color.py
@@ -167,10 +167,6 @@ def test_rgba_wrapped():
assert c.wrapped.GetRGB().Blue() == 0.0
assert c.wrapped.Alpha() == 0.5
-def test_to_tuple():
- c = Color("blue", alpha=0.5)
- np.testing.assert_allclose(tuple(c), (0, 0, 1, 0.5), rtol=1e-5)
-
def test_copy():
c = Color(0.1, 0.2, 0.3, alpha=0.4)
c_copy = copy.copy(c)
From 20854b3d4d3a3f6e4ecd35c4415807d4a106cf8a Mon Sep 17 00:00:00 2001
From: jdegenstein
Date: Wed, 12 Nov 2025 15:40:23 -0600
Subject: [PATCH 028/105] pyproject.toml -> pin to pytest==8.4.2 per
pytest-dev/pytest-xdist/issues/1273
---
pyproject.toml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/pyproject.toml b/pyproject.toml
index fdb8bc0..22f6440 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -68,7 +68,7 @@ development = [
"black",
"mypy",
"pylint",
- "pytest",
+ "pytest==8.4.2", # TODO: unpin on resolution of pytest-dev/pytest-xdist/issues/1273
"pytest-benchmark",
"pytest-cov",
"pytest-xdist",
From a5d4229fdaa2f847fa6e7939a5d416a88268bd11 Mon Sep 17 00:00:00 2001
From: Jonathan Wagenet
Date: Thu, 13 Nov 2025 14:52:23 -0500
Subject: [PATCH 029/105] Update repr and str for Vector, Location, Axis, Plane
---
src/build123d/geometry.py | 98 ++++++++++---------
src/build123d/topology/shape_core.py | 2 +-
tests/test_direct_api/test_assembly.py | 6 +-
tests/test_direct_api/test_axis.py | 4 +-
tests/test_direct_api/test_location.py | 6 +-
.../test_oriented_bound_box.py | 6 +-
tests/test_direct_api/test_plane.py | 6 +-
tests/test_direct_api/test_vector.py | 4 +-
8 files changed, 70 insertions(+), 62 deletions(-)
diff --git a/src/build123d/geometry.py b/src/build123d/geometry.py
index e4c0eeb..b641b34 100644
--- a/src/build123d/geometry.py
+++ b/src/build123d/geometry.py
@@ -443,13 +443,19 @@ class Vector:
return self.intersect(other)
def __repr__(self) -> str:
- """Display vector"""
+ """Represent vector"""
x = round(self.X, 13) if abs(self.X) > TOLERANCE else 0.0
y = round(self.Y, 13) if abs(self.Y) > TOLERANCE else 0.0
z = round(self.Z, 13) if abs(self.Z) > TOLERANCE else 0.0
- return f"Vector({x:.14g}, {y:.14g}, {z:.14g})"
+ return f"{type(self).__name__}({x:.14g}, {y:.14g}, {z:.14g})"
- __str__ = __repr__
+ def __str__(self) -> str:
+ """Display vector"""
+ strf = f".{TOL_DIGITS}g"
+ x = round(self.X, 13) if abs(self.X) > TOLERANCE else 0.0
+ y = round(self.Y, 13) if abs(self.Y) > TOLERANCE else 0.0
+ z = round(self.Z, 13) if abs(self.Z) > TOLERANCE else 0.0
+ return f"{type(self).__name__}: (X={x:{strf}}, Y={y:{strf}}, Z={z:{strf}})"
def __eq__(self, other: object) -> bool:
"""Vectors equal operator =="""
@@ -738,14 +744,15 @@ class Axis(metaclass=AxisMeta):
)
def __repr__(self) -> str:
- """Display self"""
- return f"({tuple(self.position)},{tuple(self.direction)})"
+ """Represent axis"""
+ return f"{type(self).__name__}({tuple(self.position)}, {tuple(self.direction)})"
def __str__(self) -> str:
- """Display self"""
- return (
- f"{type(self).__name__}: ({tuple(self.position)},{tuple(self.direction)})"
- )
+ """Display axis"""
+ strf = f".{TOL_DIGITS}g"
+ position_str = ", ".join(f"{v:{strf}}" for v in tuple(self.position))
+ direction_str = ", ".join(f"{v:{strf}}" for v in tuple(self.direction))
+ return f"{type(self).__name__}: (position=({position_str}), direction=({direction_str}))"
def __eq__(self, other: object) -> bool:
if not isinstance(other, Axis):
@@ -1315,7 +1322,7 @@ class Color:
return Color(*tuple(self))
def __str__(self) -> str:
- """Generate string"""
+ """Display color"""
rgb = self.wrapped.GetRGB()
rgb = (rgb.Red(), rgb.Green(), rgb.Blue())
try:
@@ -1326,11 +1333,11 @@ class Color:
quantity_color_enum = self.wrapped.GetRGB().Name()
name = Quantity_Color.StringName_s(quantity_color_enum)
qualifier = "near"
- return f"Color: {str(tuple(self))} {qualifier} {name.upper()!r}"
+ return f"{type(self).__name__}: {str(tuple(self))} {qualifier} {name.upper()!r}"
def __repr__(self) -> str:
- """Color repr"""
- return f"Color{str(tuple(self))}"
+ """Represent colr"""
+ return f"{type(self).__name__}{str(tuple(self))}"
@staticmethod
def _rgb_from_int(triplet: int) -> tuple[float, float, float]:
@@ -1911,29 +1918,21 @@ class Location:
return rv_trans, rv_rot
- def __repr__(self):
- """To String
+ def __repr__(self) -> str:
+ """Represent location"""
+ return (
+ f"{type(self).__name__}({tuple(self.position)}, {tuple(self.orientation)})"
+ )
- Convert Location to String for display
-
- Returns:
- Location as String
- """
- position_str = ", ".join(f"{v:.2f}" for v in tuple(self)[0])
- orientation_str = ", ".join(f"{v:.2f}" for v in tuple(self)[1])
- return f"(p=({position_str}), o=({orientation_str}))"
-
- def __str__(self):
- """To String
-
- Convert Location to String for display
-
- Returns:
- Location as String
- """
- position_str = ", ".join(f"{v:.2f}" for v in tuple(self)[0])
- orientation_str = ", ".join(f"{v:.2f}" for v in tuple(self)[1])
- return f"Location: (position=({position_str}), orientation=({orientation_str}))"
+ def __str__(self) -> str:
+ """Display location"""
+ strf = f".{TOL_DIGITS}g"
+ position_str = ", ".join(f"{v:{strf}}" for v in tuple(self.position))
+ orientation_str = ", ".join(f"{v:{strf}}" for v in tuple(self.orientation))
+ return (
+ f"{type(self).__name__}: "
+ f"(position=({position_str}), orientation=({orientation_str}))"
+ )
@overload
def intersect(self, vector: VectorLike) -> Vector | None:
@@ -2231,7 +2230,7 @@ class OrientedBoundBox:
return self.wrapped.IsOut(point.to_pnt())
def __repr__(self) -> str:
- return f"OrientedBoundBox(center={self.center()}, size={self.size}, plane={self.plane})"
+ return f"OrientedBoundBox(center={self.center()!r}, size={self.size!r}, plane={self.plane!r})"
class Rotation(Location):
@@ -2854,18 +2853,23 @@ class Plane(metaclass=PlaneMeta):
"""intersect plane with other &"""
return self.intersect(other)
- def __repr__(self):
- """To String
+ def __repr__(self) -> str:
+ """Represent plane"""
+ return (
+ f"{type(self).__name__}"
+ f"({tuple(self._origin)}, {tuple(self.x_dir)}, {tuple(self.z_dir)})"
+ )
- Convert Plane to String for display
-
- Returns:
- Plane as String
- """
- origin_str = ", ".join(f"{v:.2f}" for v in tuple(self._origin))
- x_dir_str = ", ".join(f"{v:.2f}" for v in tuple(self.x_dir))
- z_dir_str = ", ".join(f"{v:.2f}" for v in tuple(self.z_dir))
- return f"Plane(o=({origin_str}), x=({x_dir_str}), z=({z_dir_str}))"
+ def __str__(self) -> str:
+ """Display plane"""
+ strf = f".{TOL_DIGITS}g"
+ origin_str = ", ".join(f"{v:{strf}}" for v in tuple(self.origin))
+ x_dir_str = ", ".join(f"{v:{strf}}" for v in tuple(self.x_dir))
+ z_dir_str = ", ".join(f"{v:{strf}}" for v in tuple(self.z_dir))
+ return (
+ f"{type(self).__name__}: "
+ f"(origin=({origin_str}), x_dir=({x_dir_str}), z_dir=({z_dir_str}))"
+ )
def reverse(self) -> Plane:
"""Reverse z direction of plane"""
diff --git a/src/build123d/topology/shape_core.py b/src/build123d/topology/shape_core.py
index 1e18261..24eb0fe 100644
--- a/src/build123d/topology/shape_core.py
+++ b/src/build123d/topology/shape_core.py
@@ -735,7 +735,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
loc = (
"Center" + str(tuple(node.center()))
if show_center
- else "Location" + repr(node.location)
+ else repr(node.location)
)
result += f"{treestr}{name}at {address:#x}, {loc}\n"
return result
diff --git a/tests/test_direct_api/test_assembly.py b/tests/test_direct_api/test_assembly.py
index 71e862b..d746478 100644
--- a/tests/test_direct_api/test_assembly.py
+++ b/tests/test_direct_api/test_assembly.py
@@ -70,9 +70,9 @@ class TestAssembly(unittest.TestCase):
def test_show_topology_compound(self):
assembly = TestAssembly.create_test_assembly()
expected = [
- "assembly Compound at 0x7fced0fd1b50, Location(p=(0.00, 0.00, 0.00), o=(-0.00, 0.00, -0.00))",
- "├── box Solid at 0x7fced102d3a0, Location(p=(0.00, 0.00, 0.00), o=(45.00, 45.00, -0.00))",
- "└── sphere Solid at 0x7fced0fd1f10, Location(p=(1.00, 2.00, 3.00), o=(-0.00, 0.00, -0.00))",
+ "assembly Compound at 0x7fced0fd1b50, Location((0.0, 0.0, 0.0), (-0.0, 0.0, -0.0))",
+ "├── box Solid at 0x7fced102d3a0, Location((0.0, 0.0, 0.0), (45.0, 45.0, -0.0))",
+ "└── sphere Solid at 0x7fced0fd1f10, Location((1.0, 2.0, 3.0), (-0.0, 0.0, -0.0))",
]
self.assertTopoEqual(assembly.show_topology("Solid"), expected)
diff --git a/tests/test_direct_api/test_axis.py b/tests/test_direct_api/test_axis.py
index c0bbd46..0296e0d 100644
--- a/tests/test_direct_api/test_axis.py
+++ b/tests/test_direct_api/test_axis.py
@@ -85,8 +85,8 @@ class TestAxis(unittest.TestCase):
self.assertAlmostEqual(test_axis.direction, (0, 1, 0), 5)
def test_axis_repr_and_str(self):
- self.assertEqual(repr(Axis.X), "((0.0, 0.0, 0.0),(1.0, 0.0, 0.0))")
- self.assertEqual(str(Axis.Y), "Axis: ((0.0, 0.0, 0.0),(0.0, 1.0, 0.0))")
+ self.assertEqual(repr(Axis.X), "Axis((0.0, 0.0, 0.0), (1.0, 0.0, 0.0))")
+ self.assertEqual(str(Axis.Y), "Axis: (position=(0, 0, 0), direction=(0, 1, 0))")
def test_axis_copy(self):
x_copy = copy.copy(Axis.X)
diff --git a/tests/test_direct_api/test_location.py b/tests/test_direct_api/test_location.py
index d22cb6c..6c3a134 100644
--- a/tests/test_direct_api/test_location.py
+++ b/tests/test_direct_api/test_location.py
@@ -228,16 +228,16 @@ class TestLocation(unittest.TestCase):
def test_location_repr_and_str(self):
self.assertEqual(
- repr(Location()), "(p=(0.00, 0.00, 0.00), o=(-0.00, 0.00, -0.00))"
+ repr(Location()), "Location((0.0, 0.0, 0.0), (-0.0, 0.0, -0.0))"
)
self.assertEqual(
str(Location()),
- "Location: (position=(0.00, 0.00, 0.00), orientation=(-0.00, 0.00, -0.00))",
+ "Location: (position=(0, 0, 0), orientation=(-0, 0, -0))",
)
loc = Location((1, 2, 3), (33, 45, 67))
self.assertEqual(
str(loc),
- "Location: (position=(1.00, 2.00, 3.00), orientation=(33.00, 45.00, 67.00))",
+ "Location: (position=(1, 2, 3), orientation=(33, 45, 67))",
)
def test_location_inverted(self):
diff --git a/tests/test_direct_api/test_oriented_bound_box.py b/tests/test_direct_api/test_oriented_bound_box.py
index bcdb566..7a2bff3 100644
--- a/tests/test_direct_api/test_oriented_bound_box.py
+++ b/tests/test_direct_api/test_oriented_bound_box.py
@@ -157,9 +157,9 @@ class TestOrientedBoundBox(unittest.TestCase):
pattern = (
r"OrientedBoundBox\(center=Vector\((?P[-\d\.]+), (?P[-\d\.]+), (?P[-\d\.]+)\), "
r"size=Vector\((?P[-\d\.]+), (?P[-\d\.]+), (?P[-\d\.]+)\), "
- r"plane=Plane\(o=\((?P[-\d\.]+), (?P[-\d\.]+), (?P[-\d\.]+)\), "
- r"x=\((?P[-\d\.]+), (?P[-\d\.]+), (?P[-\d\.]+)\), "
- r"z=\((?P[-\d\.]+), (?P[-\d\.]+), (?P[-\d\.]+)\)\)\)"
+ r"plane=Plane\(\((?P[-\d\.]+), (?P[-\d\.]+), (?P[-\d\.]+)\), "
+ r"\((?P[-\d\.]+), (?P[-\d\.]+), (?P[-\d\.]+)\), "
+ r"\((?P[-\d\.]+), (?P[-\d\.]+), (?P[-\d\.]+)\)\)\)"
)
m = re.match(pattern, rep)
self.assertIsNotNone(
diff --git a/tests/test_direct_api/test_plane.py b/tests/test_direct_api/test_plane.py
index e9e7faa..59b6d4e 100644
--- a/tests/test_direct_api/test_plane.py
+++ b/tests/test_direct_api/test_plane.py
@@ -335,7 +335,11 @@ class TestPlane(unittest.TestCase):
def test_repr(self):
self.assertEqual(
repr(Plane.XY),
- "Plane(o=(0.00, 0.00, 0.00), x=(1.00, 0.00, 0.00), z=(0.00, 0.00, 1.00))",
+ "Plane((0.0, 0.0, 0.0), (1.0, 0.0, 0.0), (0.0, 0.0, 1.0))",
+ )
+ self.assertEqual(
+ str(Plane.XY),
+ "Plane: (origin=(0, 0, 0), x_dir=(1, 0, 0), z_dir=(0, 0, 1))",
)
def test_shift_origin_axis(self):
diff --git a/tests/test_direct_api/test_vector.py b/tests/test_direct_api/test_vector.py
index 521c47c..b0bb0a9 100644
--- a/tests/test_direct_api/test_vector.py
+++ b/tests/test_direct_api/test_vector.py
@@ -221,10 +221,10 @@ class TestVector(unittest.TestCase):
def test_vector_special_methods(self):
self.assertEqual(repr(Vector(1, 2, 3)), "Vector(1, 2, 3)")
- self.assertEqual(str(Vector(1, 2, 3)), "Vector(1, 2, 3)")
+ self.assertEqual(str(Vector(1, 2, 3)), "Vector: (X=1, Y=2, Z=3)")
self.assertEqual(
str(Vector(9.99999999999999, -23.649999999999995, -7.37188088351e-15)),
- "Vector(10, -23.65, 0)",
+ "Vector: (X=10, Y=-23.65, Z=0)",
)
def test_vector_iter(self):
From 73a0a0ea21f2cd3b20aed35b6e5720c44e80ab5e Mon Sep 17 00:00:00 2001
From: Jonathan Wagenet
Date: Fri, 14 Nov 2025 10:50:32 -0500
Subject: [PATCH 030/105] Correct Location constructor kwargs and update
docstrings
---
src/build123d/geometry.py | 46 +++++++++++++++++++--------------------
1 file changed, 22 insertions(+), 24 deletions(-)
diff --git a/src/build123d/geometry.py b/src/build123d/geometry.py
index b641b34..299e416 100644
--- a/src/build123d/geometry.py
+++ b/src/build123d/geometry.py
@@ -1483,56 +1483,50 @@ class Location:
@overload
def __init__(self):
- """Empty location with not rotation or translation with respect to the original location."""
+ """Location with no position or orientation"""
@overload
def __init__(self, location: Location):
- """Location with another given location."""
+ """Location from Location"""
@overload
- def __init__(self, translation: VectorLike, angle: float = 0):
- """Location with translation with respect to the original location.
- If angle != 0 then the location includes a rotation around z-axis by angle"""
+ def __init__(self, position: VectorLike, angle: float = 0):
+ """Location from position and rotation around z-axis by optional angle"""
@overload
- def __init__(self, translation: VectorLike, rotation: RotationLike | None = None):
- """Location with translation with respect to the original location.
- If rotation is not None then the location includes the rotation (see also Rotation class)
- """
+ def __init__(self, position: VectorLike, orientation: RotationLike | None = None):
+ """Location from position and optional orientation (see Rotation class)"""
@overload
def __init__(
self,
- translation: VectorLike,
- rotation: RotationLike,
+ position: VectorLike,
+ orientation: RotationLike,
ordering: Extrinsic | Intrinsic,
):
- """Location with translation with respect to the original location.
- If rotation is not None then the location includes the rotation (see also Rotation class)
- ordering defaults to Intrinsic.XYZ, but can also be set to Extrinsic
+ """Location from position and optional orientation (see Rotation class).
+ Orientation determined by optional ordering, defaults to Intrinsic.XYZ
"""
@overload
def __init__(self, plane: Plane):
- """Location corresponding to the location of the Plane."""
+ """Location from location of Plane."""
@overload
def __init__(self, plane: Plane, plane_offset: VectorLike):
- """Location corresponding to the angular location of the Plane with
- translation plane_offset."""
+ """Location from location of Plane translated by plane_offset"""
@overload
def __init__(self, top_loc: TopLoc_Location):
- """Location wrapping the low-level TopLoc_Location object t"""
+ """Location from low-level TopLoc_Location object"""
@overload
def __init__(self, gp_trsf: gp_Trsf):
- """Location wrapping the low-level gp_Trsf object t"""
+ """Location from low-level gp_Trsf object"""
@overload
- def __init__(self, translation: VectorLike, direction: VectorLike, angle: float):
- """Location with translation t and rotation around direction by angle
- with respect to the original location."""
+ def __init__(self, position: VectorLike, direction: VectorLike, angle: float):
+ """Location from position and rotation around direction by angle"""
def __init__(
self, *args, **kwargs
@@ -1542,6 +1536,7 @@ class Location:
position = kwargs.pop("position", None)
orientation = kwargs.pop("orientation", None)
+ direction = kwargs.pop("direction", None)
ordering = kwargs.pop("ordering", None)
angle = kwargs.pop("angle", None)
plane = kwargs.pop("plane", None)
@@ -1571,7 +1566,10 @@ class Location:
elif isinstance(args[1], (int, float)):
angle = args[1]
if len(args) > 2:
- if isinstance(args[2], (int, float)) and orientation is not None:
+ if isinstance(args[1], (Vector, Iterable)) and isinstance(
+ args[2], (int, float)
+ ):
+ direction = Vector(args[1])
angle = args[2]
elif isinstance(args[2], (Intrinsic, Extrinsic)):
ordering = args[2]
@@ -1600,7 +1598,7 @@ class Location:
elif angle is not None:
axis = gp_Ax1(
gp_Pnt(0, 0, 0),
- Vector(orientation).to_dir() if orientation else gp_Dir(0, 0, 1),
+ Vector(direction).to_dir() if direction else gp_Dir(0, 0, 1),
)
trsf.SetRotation(axis, radians(angle))
From 3877fd58762dba9a835f38eb5287841e3c071d4a Mon Sep 17 00:00:00 2001
From: Jonathan Wagenet
Date: Fri, 14 Nov 2025 12:58:46 -0500
Subject: [PATCH 031/105] Ignore orderless Shapes in _bool_op
---
src/build123d/topology/shape_core.py | 6 +++++-
1 file changed, 5 insertions(+), 1 deletion(-)
diff --git a/src/build123d/topology/shape_core.py b/src/build123d/topology/shape_core.py
index d366ead..3940276 100644
--- a/src/build123d/topology/shape_core.py
+++ b/src/build123d/topology/shape_core.py
@@ -2123,7 +2123,11 @@ class Shape(NodeMixin, Generic[TOPODS]):
args = list(args)
tools = list(tools)
# Find the highest order class from all the inputs Solid > Vertex
- order_dict = {type(s): type(s).order for s in [self] + args + tools}
+ order_dict = {
+ type(s): type(s).order
+ for s in [self] + args + tools
+ if hasattr(type(s), "order")
+ }
highest_order = sorted(order_dict.items(), key=lambda item: item[1])[-1]
# The base of the operation
From 68f6ef2125e03aa34fe205b46c739afaeff1a363 Mon Sep 17 00:00:00 2001
From: Jonathan Wagenet
Date: Fri, 14 Nov 2025 13:26:17 -0500
Subject: [PATCH 032/105] Convert intersect to use _bool_op and split Wire
after intersect
---
src/build123d/topology/one_d.py | 172 +++++++++------------
tests/test_direct_api/test_intersection.py | 4 +-
2 files changed, 78 insertions(+), 98 deletions(-)
diff --git a/src/build123d/topology/one_d.py b/src/build123d/topology/one_d.py
index 25b817a..b0a59c3 100644
--- a/src/build123d/topology/one_d.py
+++ b/src/build123d/topology/one_d.py
@@ -52,12 +52,11 @@ license:
from __future__ import annotations
import copy
-import numpy as np
import warnings
-from collections.abc import Iterable
+from collections.abc import Iterable, Sequence
from itertools import combinations
from math import atan2, ceil, copysign, cos, floor, inf, isclose, pi, radians
-from typing import TYPE_CHECKING, Literal, TypeAlias, overload
+from typing import TYPE_CHECKING, Literal, overload
from typing import cast as tcast
import numpy as np
@@ -729,122 +728,103 @@ class Mixin1D(Shape):
def to_vertex(objs: Iterable) -> ShapeList:
return ShapeList([Vertex(v) if isinstance(v, Vector) else v for v in objs])
- common_set: ShapeList[Vertex | Edge] = ShapeList(self.edges())
- target: ShapeList | Shape | Plane
+ def bool_op(
+ args: Sequence,
+ tools: Sequence,
+ operation: BRepAlgoAPI_Section | BRepAlgoAPI_Common,
+ ) -> ShapeList:
+ # Wrap Shape._bool_op for corrected output
+ intersections: Shape | ShapeList = Shape()._bool_op(args, tools, operation)
+ if isinstance(intersections, ShapeList):
+ return intersections or ShapeList()
+ if isinstance(intersections, Shape) and not intersections.is_null:
+ return ShapeList([intersections])
+ return ShapeList()
+
+ def filter_shapes_by_order(shapes: ShapeList, orders: list) -> ShapeList:
+ # Remove lower order shapes from list which *appear* to be part of
+ # a higher order shape using a lazy distance check
+ # (sufficient for vertices, may be an issue for higher orders)
+ order_groups = []
+ for order in orders:
+ order_groups.append(
+ ShapeList([s for s in shapes if isinstance(s, order)])
+ )
+
+ filtered_shapes = order_groups[-1]
+ for i in range(len(order_groups) - 1):
+ los = order_groups[i]
+ his: list = sum(order_groups[i + 1 :], [])
+ filtered_shapes.extend(
+ ShapeList(
+ lo
+ for lo in los
+ if all(lo.distance_to(hi) > TOLERANCE for hi in his)
+ )
+ )
+
+ return filtered_shapes
+
+ common_set: ShapeList[Vertex | Edge | Wire] = ShapeList([self])
+ target: Shape | Plane
for other in to_intersect:
# Conform target type
- # Vertices need to be Vector for set()
match other:
case Axis():
- target = ShapeList([Edge(other)])
+ # BRepAlgoAPI_Section seems happier if Edge isnt infinite
+ bbox = self.bounding_box()
+ dist = self.distance_to(other.position)
+ dist = dist if dist >= 1 else 1
+ target = Edge.make_line(
+ other.position - other.direction * bbox.diagonal * dist,
+ other.position + other.direction * bbox.diagonal * dist,
+ )
case Plane():
target = other
case Vector():
target = Vertex(other)
case Location():
target = Vertex(other.position)
- case Edge():
- target = ShapeList([other])
- case Wire():
- target = ShapeList(other.edges())
case _ if issubclass(type(other), Shape):
target = other
case _:
raise ValueError(f"Unsupported type to_intersect: {type(other)}")
# Find common matches
- common: list[Vector | Edge] = []
- result: ShapeList | Shape | None
+ common: list[Vertex | Edge | Wire] = []
+ result: ShapeList | None
for obj in common_set:
match (obj, target):
- case obj, Shape() as target:
- # Find Shape with Edge/Wire
- if isinstance(target, Vertex):
- result = Shape.intersect(obj, target)
- else:
- result = target.intersect(obj)
+ case (_, Plane()):
+ target = Shape(BRepBuilderAPI_MakeFace(other.wrapped).Face())
+ operation = BRepAlgoAPI_Section()
+ result = bool_op((obj,), (target,), operation)
+ operation = BRepAlgoAPI_Common()
+ result.extend(bool_op((obj,), (target,), operation))
- if result:
- if not isinstance(result, list):
- result = ShapeList([result])
- common.extend(to_vector(result))
+ case (_, Vertex() | Edge() | Wire()):
+ operation = BRepAlgoAPI_Section()
+ section = bool_op((obj,), (target,), operation)
+ result = section
+ if not section:
+ operation = BRepAlgoAPI_Common()
+ result.extend(bool_op((obj,), (target,), operation))
- case Vertex() as obj, target:
- if not isinstance(target, ShapeList):
- target = ShapeList([target])
+ case _ if issubclass(type(target), Shape):
+ result = target.intersect(obj)
- for tar in target:
- if isinstance(tar, Edge):
- result = Shape.intersect(obj, tar)
- else:
- result = obj.intersect(tar)
-
- if result:
- if not isinstance(result, list):
- result = ShapeList([result])
- common.extend(to_vector(result))
-
- case Edge() as obj, ShapeList() as targets:
- # Find any edge / edge intersection points
- for tar in targets:
- # Find crossing points
- try:
- intersection_points = obj.find_intersection_points(tar)
- common.extend(intersection_points)
- except ValueError:
- pass
-
- # Find common end points
- obj_end_points = set(Vector(v) for v in obj.vertices())
- tar_end_points = set(Vector(v) for v in tar.vertices())
- points = set.intersection(obj_end_points, tar_end_points)
- common.extend(points)
-
- # Find Edge/Edge overlaps
- result = obj._bool_op(
- (obj,), targets, BRepAlgoAPI_Common()
- ).edges()
- common.extend(result if isinstance(result, list) else [result])
-
- case Edge() as obj, Plane() as plane:
- # Find any edge / plane intersection points & edges
- # Find point intersections
- if obj.wrapped is None:
- continue
- geom_line = BRep_Tool.Curve_s(
- obj.wrapped, obj.param_at(0), obj.param_at(1)
- )
- geom_plane = Geom_Plane(plane.local_coord_system)
- intersection_calculator = GeomAPI_IntCS(geom_line, geom_plane)
- plane_intersection_points: list[Vector] = []
- if intersection_calculator.IsDone():
- plane_intersection_points = [
- Vector(intersection_calculator.Point(i + 1))
- for i in range(intersection_calculator.NbPoints())
- ]
- common.extend(plane_intersection_points)
-
- # Find edge intersections
- if all(
- plane.contains(v)
- for v in obj.positions(i / 7 for i in range(8))
- ): # is a 2D edge
- common.append(obj)
+ if result:
+ common.extend(result)
if common:
- common_set = to_vertex(set(common))
- # Remove Vertex intersections coincident to Edge intersections
- vts = common_set.vertices()
- eds = common_set.edges()
- if vts and eds:
- filtered_vts = ShapeList(
- [
- v
- for v in vts
- if all(v.distance_to(e) > TOLERANCE for e in eds)
- ]
- )
- common_set = filtered_vts + eds
+ common_set = ShapeList()
+ for shape in common:
+ if isinstance(shape, Wire):
+ common_set.extend(shape.edges())
+ else:
+ common_set.append(shape)
+ common_set = to_vertex(set(to_vector(common_set)))
+ common_set = filter_shapes_by_order(common_set, [Vertex, Edge])
else:
return None
diff --git a/tests/test_direct_api/test_intersection.py b/tests/test_direct_api/test_intersection.py
index daad2bf..3a67415 100644
--- a/tests/test_direct_api/test_intersection.py
+++ b/tests/test_direct_api/test_intersection.py
@@ -296,7 +296,7 @@ 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].edge().trim(.3, .4),
+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)
])
@@ -430,7 +430,7 @@ freecad_matrix = [
Case(vert, e1, [Vertex], "vertical, ellipse, tangent", None),
Case(horz, e1, [Vertex], "horizontal, ellipse, tangent", None),
- Case(c1, c2, [Vertex, Vertex], "circle, skew, intersect", "Should return 2 Vertices"),
+ Case(c1, c2, [Vertex, Vertex], "circle, skew, intersect", None),
Case(c1, horz, [Vertex], "circle, horiz, tangent", None),
Case(c2, horz, [Vertex], "circle, horiz, tangent", None),
Case(c1, vert, [Vertex], "circle, vert, tangent", None),
From c384df21c7755d5bdaa58890704dce6c0d19c8f2 Mon Sep 17 00:00:00 2001
From: Jonathan Wagenet
Date: Fri, 14 Nov 2025 13:31:40 -0500
Subject: [PATCH 033/105] Intersect: dissolve Wire, Shell after intersection,
no need to process 0d, 1d separately
---
src/build123d/topology/composite.py | 113 +++++++++++++---------------
src/build123d/topology/three_d.py | 76 +++++++++----------
src/build123d/topology/two_d.py | 71 ++++++++---------
3 files changed, 127 insertions(+), 133 deletions(-)
diff --git a/src/build123d/topology/composite.py b/src/build123d/topology/composite.py
index 0849e69..3e0b4b3 100644
--- a/src/build123d/topology/composite.py
+++ b/src/build123d/topology/composite.py
@@ -728,24 +728,19 @@ class Compound(Mixin3D, Shape[TopoDS_Compound]):
args: Sequence,
tools: Sequence,
operation: BRepAlgoAPI_Common | BRepAlgoAPI_Section,
- ) -> ShapeList | None:
+ ) -> ShapeList:
# Wrap Shape._bool_op for corrected output
- intersections = args[0]._bool_op(args, tools, operation)
+ intersections: Shape | ShapeList = Shape()._bool_op(args, tools, operation)
if isinstance(intersections, ShapeList):
- return intersections or None
+ return intersections
if isinstance(intersections, Shape) and not intersections.is_null:
return ShapeList([intersections])
- return None
+ return ShapeList()
def expand_compound(compound: Compound) -> ShapeList:
shapes = ShapeList(compound.children)
for shape_type in [Vertex, Edge, Wire, Face, Shell, Solid]:
- new = compound.get_type(shape_type)
- if shape_type == Wire:
- new = [edge for new_shape in new for edge in new_shape.edges()]
- elif shape_type == Shell:
- new = [face for new_shape in new for face in new_shape.faces()]
- shapes.extend(new)
+ shapes.extend(compound.get_type(shape_type))
return shapes
def filter_shapes_by_order(shapes: ShapeList, orders: list) -> ShapeList:
@@ -772,14 +767,20 @@ class Compound(Mixin3D, Shape[TopoDS_Compound]):
return filtered_shapes
- common_set: ShapeList[Vertex | Edge | Face | Solid] = expand_compound(self)
+ common_set: ShapeList[Shape] = expand_compound(self)
target: ShapeList | Shape
for other in to_intersect:
# Conform target type
- # Vertices need to be Vector for set()
match other:
case Axis():
- target = Edge(other)
+ # BRepAlgoAPI_Section seems happier if Edge isnt infinite
+ bbox = self.bounding_box()
+ dist = self.distance_to(other.position)
+ dist = dist if dist >= 1 else 1
+ target = Edge.make_line(
+ other.position - other.direction * bbox.diagonal * dist,
+ other.position + other.direction * bbox.diagonal * dist,
+ )
case Plane():
target = Face.make_plane(other)
case Vector():
@@ -794,58 +795,50 @@ class Compound(Mixin3D, Shape[TopoDS_Compound]):
raise ValueError(f"Unsupported type to_intersect: {type(other)}")
# Find common matches
- common: list[Vector | Edge | Face] = []
- result: ShapeList | Shape | None
+ common: list[Vertex | Edge | Wire | Face | Shell | Solid] = []
+ result: ShapeList
for obj in common_set:
- match (obj, target):
- case (Vertex(), Vertex()):
- result = obj.intersect(target)
-
- case (Edge(), Edge() | Wire()):
- result = obj.intersect(target)
-
- case (_, ShapeList()):
- result = ShapeList()
- for t in target:
- if (
- not isinstance(obj, Edge) and not isinstance(t, (Edge))
- ) or (isinstance(obj, Solid) or isinstance(t, 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,), (t,), operation) or [])
- operation = BRepAlgoAPI_Section()
- result.extend(bool_op((obj,), (t,), operation) or [])
-
- case _ if issubclass(type(target), Shape):
- if isinstance(target, Wire):
- targets = target.edges()
- elif isinstance(target, Shell):
- targets = target.faces()
- else:
- targets = ShapeList([target])
-
- result = ShapeList()
- for t in targets:
- if (
- not isinstance(obj, Edge) and not isinstance(t, (Edge))
- ) or (isinstance(obj, Solid) or isinstance(t, 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,), (t,), operation) or [])
- operation = BRepAlgoAPI_Section()
- result.extend(bool_op((obj,), (t,), operation) or [])
+ if isinstance(target, Shape):
+ target = ShapeList([target])
+ result = ShapeList()
+ for t in target:
+ operation = BRepAlgoAPI_Section()
+ result.extend(bool_op((obj,), (t,), operation))
+ if (
+ not isinstance(obj, Edge | Wire)
+ and not isinstance(t, Edge | Wire)
+ ) or (
+ isinstance(obj, Solid | Compound)
+ or isinstance(t, Solid | Compound)
+ ):
+ # Face + Edge combinations may produce an intersection
+ # with Common but always with Section.
+ # No easy way to deduplicate
+ # Many Solid + Edge combinations need Common
+ operation = BRepAlgoAPI_Common()
+ result.extend(bool_op((obj,), (t,), operation))
if result:
- common.extend(to_vector(result))
+ common.extend(result)
+ expanded: ShapeList = ShapeList()
if common:
- common_set = to_vertex(set(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]
)
diff --git a/src/build123d/topology/three_d.py b/src/build123d/topology/three_d.py
index 9e22ce5..16379fb 100644
--- a/src/build123d/topology/three_d.py
+++ b/src/build123d/topology/three_d.py
@@ -443,14 +443,14 @@ class Mixin3D(Shape):
args: Sequence,
tools: Sequence,
operation: BRepAlgoAPI_Common | BRepAlgoAPI_Section,
- ) -> ShapeList | None:
+ ) -> ShapeList:
# Wrap Shape._bool_op for corrected output
- intersections = args[0]._bool_op(args, tools, operation)
+ intersections: Shape | ShapeList = Shape()._bool_op(args, tools, operation)
if isinstance(intersections, ShapeList):
- return intersections or None
+ return intersections or ShapeList()
if isinstance(intersections, Shape) and not intersections.is_null:
return ShapeList([intersections])
- return None
+ return ShapeList()
def filter_shapes_by_order(shapes: ShapeList, orders: list) -> ShapeList:
# Remove lower order shapes from list which *appear* to be part of
@@ -476,14 +476,20 @@ class Mixin3D(Shape):
return filtered_shapes
- common_set: ShapeList[Vertex | Edge | Face | Solid] = ShapeList(self.solids())
- target: ShapeList | Shape
+ common_set: ShapeList[Vertex | Edge | Face | Solid] = ShapeList([self])
+ target: Shape
for other in to_intersect:
# Conform target type
- # Vertices need to be Vector for set()
match other:
case Axis():
- target = Edge(other)
+ # BRepAlgoAPI_Section seems happier if Edge isnt infinite
+ bbox = self.bounding_box()
+ dist = self.distance_to(other.position)
+ dist = dist if dist >= 1 else 1
+ target = Edge.make_line(
+ other.position - other.direction * bbox.diagonal * dist,
+ other.position + other.direction * bbox.diagonal * dist,
+ )
case Plane():
target = Face.make_plane(other)
case Vector():
@@ -496,46 +502,40 @@ class Mixin3D(Shape):
raise ValueError(f"Unsupported type to_intersect: {type(other)}")
# Find common matches
- common: list[Vector | Edge | Face] = []
- result: ShapeList | Shape | None
+ common: list[Vertex | Edge | Wire | Face | Shell | Solid] = []
+ result: ShapeList | None
for obj in common_set:
match (obj, target):
- case (Vertex(), Vertex()):
- result = obj.intersect(target)
-
- case (Edge(), Edge() | Wire()):
- result = obj.intersect(target)
-
case (_, Vertex() | Edge() | Wire() | Face() | Shell() | Solid()):
- if isinstance(target, Wire):
- targets = target.edges()
- elif isinstance(target, Shell):
- targets = target.faces()
- else:
- targets = ShapeList([target])
-
- result = ShapeList()
- for t in targets:
- if (
- not isinstance(obj, Edge) and not isinstance(t, (Edge))
- ) or (isinstance(obj, Solid) or isinstance(t, 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,), (t,), operation) or [])
- operation = BRepAlgoAPI_Section()
- result.extend(bool_op((obj,), (t,), operation) or [])
+ operation = BRepAlgoAPI_Section()
+ result = bool_op((obj,), (target,), operation)
+ if (
+ not isinstance(obj, Edge | Wire)
+ and not isinstance(target, (Edge | Wire))
+ ) or (isinstance(obj, Solid) or isinstance(target, Solid)):
+ # Face + Edge combinations may produce an intersection
+ # with Common but always with Section.
+ # No easy way to deduplicate
+ # Many Solid + Edge combinations need Common
+ operation = BRepAlgoAPI_Common()
+ result.extend(bool_op((obj,), (target,), operation))
case _ if issubclass(type(target), Shape):
result = target.intersect(obj)
if result:
- common.extend(to_vector(result))
+ common.extend(result)
if common:
- common_set = to_vertex(set(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]
)
diff --git a/src/build123d/topology/two_d.py b/src/build123d/topology/two_d.py
index 862184b..2519c82 100644
--- a/src/build123d/topology/two_d.py
+++ b/src/build123d/topology/two_d.py
@@ -301,14 +301,14 @@ class Mixin2D(ABC, Shape):
args: Sequence,
tools: Sequence,
operation: BRepAlgoAPI_Section | BRepAlgoAPI_Common,
- ) -> ShapeList | None:
+ ) -> ShapeList:
# Wrap Shape._bool_op for corrected output
- intersections = args[0]._bool_op(args, tools, operation)
+ intersections: Shape | ShapeList = Shape()._bool_op(args, tools, operation)
if isinstance(intersections, ShapeList):
- return intersections or None
+ return intersections or ShapeList()
if isinstance(intersections, Shape) and not intersections.is_null:
return ShapeList([intersections])
- return None
+ return ShapeList()
def filter_shapes_by_order(shapes: ShapeList, orders: list) -> ShapeList:
# Remove lower order shapes from list which *appear* to be part of
@@ -334,14 +334,20 @@ class Mixin2D(ABC, Shape):
return filtered_shapes
- common_set: ShapeList[Vertex | Edge | Face] = ShapeList(self.faces())
- target: ShapeList | Shape
+ common_set: ShapeList[Vertex | Edge | Face | Shell] = ShapeList([self])
+ target: Shape
for other in to_intersect:
# Conform target type
- # Vertices need to be Vector for set()
match other:
case Axis():
- target = Edge(other)
+ # BRepAlgoAPI_Section seems happier if Edge isnt infinite
+ bbox = self.bounding_box()
+ dist = self.distance_to(other.position)
+ dist = dist if dist >= 1 else 1
+ target = Edge.make_line(
+ other.position - other.direction * bbox.diagonal * dist,
+ other.position + other.direction * bbox.diagonal * dist,
+ )
case Plane():
target = Face.make_plane(other)
case Vector():
@@ -354,43 +360,38 @@ class Mixin2D(ABC, Shape):
raise ValueError(f"Unsupported type to_intersect: {type(other)}")
# Find common matches
- common: list[Vector | Edge | Face] = []
- result: ShapeList | Shape | None
+ common: list[Vertex | Edge | Wire | Face | Shell] = []
+ result: ShapeList | None
for obj in common_set:
match (obj, target):
- case (Vertex(), Vertex()):
- result = obj.intersect(target)
-
- case (Edge(), Edge() | Wire()):
- result = obj.intersect(target)
-
case (_, Vertex() | Edge() | Wire() | Face() | Shell()):
- if isinstance(target, Wire):
- targets = target.edges()
- elif isinstance(target, Shell):
- targets = target.faces()
- else:
- targets = ShapeList([target])
-
- result = ShapeList()
- for t in targets:
- if not isinstance(obj, Edge) and not isinstance(t, (Edge)):
- # 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,), (t,), operation) or [])
- operation = BRepAlgoAPI_Section()
- result.extend(bool_op((obj,), (t,), operation) or [])
+ operation = BRepAlgoAPI_Section()
+ result = bool_op((obj,), (target,), operation)
+ if not isinstance(obj, Edge | Wire) and not isinstance(
+ target, (Edge | Wire)
+ ):
+ # Face + Edge combinations may produce an intersection
+ # with Common but always with Section.
+ # No easy way to deduplicate
+ operation = BRepAlgoAPI_Common()
+ result.extend(bool_op((obj,), (target,), operation))
case _ if issubclass(type(target), Shape):
result = target.intersect(obj)
if result:
- common.extend(to_vector(result))
+ common.extend(result)
if common:
- common_set = to_vertex(set(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
From 5523a2184c27a7936294d312d86f4535f08d05d5 Mon Sep 17 00:00:00 2001
From: Jonathan Wagenet
Date: Fri, 14 Nov 2025 14:40:58 -0500
Subject: [PATCH 034/105] Revert mode == Mode.INTERSECT iteration. pass
Compound instead
---
src/build123d/build_common.py | 8 +-------
1 file changed, 1 insertion(+), 7 deletions(-)
diff --git a/src/build123d/build_common.py b/src/build123d/build_common.py
index abfa4a0..d14b556 100644
--- a/src/build123d/build_common.py
+++ b/src/build123d/build_common.py
@@ -466,13 +466,7 @@ class Builder(ABC, Generic[ShapeT]):
elif mode == Mode.INTERSECT:
if self._obj is None:
raise RuntimeError("Nothing to intersect with")
- intersections: ShapeList[Shape] = ShapeList()
- for target in typed[self._shape]:
- result = self._obj.intersect(target)
- if result is None:
- continue
- intersections.extend(result)
- combined = self._sub_class(intersections)
+ combined = self._obj.intersect(Compound(typed[self._shape]))
elif mode == Mode.REPLACE:
combined = self._sub_class(list(typed[self._shape]))
From 5f67a1932afc03f425bd4e8231e80b4c439d2a42 Mon Sep 17 00:00:00 2001
From: Jonathan Wagenet
Date: Fri, 14 Nov 2025 17:30:55 -0500
Subject: [PATCH 035/105] Update for dev merge to Compound and Face(Plane)
---
src/build123d/topology/composite.py | 2 +-
src/build123d/topology/three_d.py | 2 +-
src/build123d/topology/two_d.py | 2 +-
tests/test_direct_api/test_intersection.py | 8 ++++----
4 files changed, 7 insertions(+), 7 deletions(-)
diff --git a/src/build123d/topology/composite.py b/src/build123d/topology/composite.py
index be136b2..14c67b4 100644
--- a/src/build123d/topology/composite.py
+++ b/src/build123d/topology/composite.py
@@ -782,7 +782,7 @@ class Compound(Mixin3D[TopoDS_Compound]):
other.position + other.direction * bbox.diagonal * dist,
)
case Plane():
- target = Face.make_plane(other)
+ target = Face(other)
case Vector():
target = Vertex(other)
case Location():
diff --git a/src/build123d/topology/three_d.py b/src/build123d/topology/three_d.py
index 80a8239..279f46f 100644
--- a/src/build123d/topology/three_d.py
+++ b/src/build123d/topology/three_d.py
@@ -491,7 +491,7 @@ class Mixin3D(Shape[TOPODS]):
other.position + other.direction * bbox.diagonal * dist,
)
case Plane():
- target = Face.make_plane(other)
+ target = Face(other)
case Vector():
target = Vertex(other)
case Location():
diff --git a/src/build123d/topology/two_d.py b/src/build123d/topology/two_d.py
index 37f51f0..450fa10 100644
--- a/src/build123d/topology/two_d.py
+++ b/src/build123d/topology/two_d.py
@@ -350,7 +350,7 @@ class Mixin2D(ABC, Shape[TOPODS]):
other.position + other.direction * bbox.diagonal * dist,
)
case Plane():
- target = Face.make_plane(other)
+ target = Face(other)
case Vector():
target = Vertex(other)
case Location():
diff --git a/tests/test_direct_api/test_intersection.py b/tests/test_direct_api/test_intersection.py
index 3a67415..758fd6f 100644
--- a/tests/test_direct_api/test_intersection.py
+++ b/tests/test_direct_api/test_intersection.py
@@ -355,10 +355,10 @@ 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)
+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]
From 173c7b08e22f961265377b595c49fa9ca7920c3a Mon Sep 17 00:00:00 2001
From: x0pherl
Date: Thu, 30 Oct 2025 21:08:26 -0400
Subject: [PATCH 036/105] added support for passing an iterable of radii to
FilletPolyline.
---
src/build123d/objects_curve.py | 22 +++++++++++++----
tests/test_build_line.py | 43 ++++++++++++++++++++++++++++++++++
2 files changed, 60 insertions(+), 5 deletions(-)
diff --git a/src/build123d/objects_curve.py b/src/build123d/objects_curve.py
index 262f9cf..6fbfbf6 100644
--- a/src/build123d/objects_curve.py
+++ b/src/build123d/objects_curve.py
@@ -793,7 +793,7 @@ class FilletPolyline(BaseLineObject):
Args:
pts (VectorLike | Iterable[VectorLike]): sequence of two or more points
- radius (float): fillet radius
+ radius (float | Iterable[float]): radius to fillet at each vertex or a single value for all vertices
close (bool, optional): close end points with extra Edge and corner fillets.
Defaults to False
mode (Mode, optional): combination mode. Defaults to Mode.ADD
@@ -808,7 +808,7 @@ class FilletPolyline(BaseLineObject):
def __init__(
self,
*pts: VectorLike | Iterable[VectorLike],
- radius: float,
+ radius: float | Iterable[float],
close: bool = False,
mode: Mode = Mode.ADD,
):
@@ -819,7 +819,16 @@ class FilletPolyline(BaseLineObject):
if len(points) < 2:
raise ValueError("FilletPolyline requires two or more pts")
- if radius <= 0:
+
+ 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):
+ raise ValueError(
+ f"radius list length ({len(radius_list)}) must match points ({len(points)})"
+ )
+ if any(r <= 0 for r in radius_list):
raise ValueError("radius must be positive")
lines_pts = WorkplaneList.localize(*points)
@@ -852,12 +861,14 @@ class FilletPolyline(BaseLineObject):
# For each corner vertex create a new fillet Edge
fillets = []
- for vertex, edges in vertex_to_edges.items():
+ for i, (vertex, edges) in enumerate(vertex_to_edges.items()):
if len(edges) != 2:
continue
other_vertices = {ve for e in edges for ve in e.vertices() if ve != vertex}
third_edge = Edge.make_line(*[v for v in other_vertices])
- fillet_face = Face(Wire(edges + [third_edge])).fillet_2d(radius, [vertex])
+ fillet_face = Face(Wire(edges + [third_edge])).fillet_2d(
+ radius_list[i], [vertex]
+ )
fillets.append(fillet_face.edges().filter_by(GeomType.CIRCLE)[0])
# Create the Edges that join the fillets
@@ -1597,6 +1608,7 @@ class ArcArcTangentLine(BaseEdgeObject):
Defaults to Keep.INSIDE
mode (Mode, optional): combination mode. Defaults to Mode.ADD
"""
+
warnings.warn(
"The 'ArcArcTangentLine' object is deprecated and will be removed in a future version.",
DeprecationWarning,
diff --git a/tests/test_build_line.py b/tests/test_build_line.py
index 01c2fbe..7b5858f 100644
--- a/tests/test_build_line.py
+++ b/tests/test_build_line.py
@@ -183,6 +183,49 @@ class BuildLineTests(unittest.TestCase):
self.assertEqual(len(p.edges().filter_by(GeomType.CIRCLE)), 2)
self.assertEqual(len(p.edges().filter_by(GeomType.LINE)), 3)
+ with self.assertRaises(ValueError):
+ p = FilletPolyline(
+ (0, 0),
+ (10, 0),
+ (10, 10),
+ (0, 10),
+ radius=(1, 2, 3, 0),
+ close=True,
+ )
+
+ with self.assertRaises(ValueError):
+ p = FilletPolyline(
+ (0, 0),
+ (10, 0),
+ (10, 10),
+ (0, 10),
+ radius=-1,
+ close=True,
+ )
+
+ with self.assertRaises(ValueError):
+ p = FilletPolyline(
+ (0, 0),
+ (10, 0),
+ (10, 10),
+ (0, 10),
+ radius=(1, 2),
+ close=True,
+ )
+
+ with BuildLine(Plane.YZ):
+ p = FilletPolyline(
+ (0, 0),
+ (10, 0),
+ (10, 10),
+ (0, 10),
+ radius=(1, 2, 3, 4),
+ close=True,
+ )
+ self.assertEqual(len(p.edges()), 8)
+ self.assertEqual(len(p.edges().filter_by(GeomType.CIRCLE)), 4)
+ self.assertEqual(len(p.edges().filter_by(GeomType.LINE)), 4)
+
with BuildLine(Plane.YZ):
p = FilletPolyline(
(0, 0, 0), (0, 0, 10), (10, 2, 10), (10, 0, 0), radius=2, close=True
From e92255cefcda727304e2022655773813aa728db3 Mon Sep 17 00:00:00 2001
From: x0pherl
Date: Fri, 31 Oct 2025 23:18:54 -0400
Subject: [PATCH 037/105] updated to handle polygons without closed lines
---
src/build123d/objects_curve.py | 11 ++++++-----
tests/test_build_line.py | 10 ++++++++++
2 files changed, 16 insertions(+), 5 deletions(-)
diff --git a/src/build123d/objects_curve.py b/src/build123d/objects_curve.py
index 6fbfbf6..71d788b 100644
--- a/src/build123d/objects_curve.py
+++ b/src/build123d/objects_curve.py
@@ -824,12 +824,13 @@ class FilletPolyline(BaseLineObject):
radius_list = [radius] * len(points) # Single radius for all points
else:
radius_list = list(radius)
- if len(radius_list) != len(points):
+ if len(radius_list) != len(points) - int(not close) * 2:
raise ValueError(
- f"radius list length ({len(radius_list)}) must match points ({len(points)})"
+ f"radius list length ({len(radius_list)}) must match angle count ({ len(points) - int(not close) * 2})"
)
- if any(r <= 0 for r in radius_list):
- raise ValueError("radius must be positive")
+ for r in radius_list:
+ if r <= 0:
+ raise ValueError(f"radius {r} must be positive")
lines_pts = WorkplaneList.localize(*points)
@@ -867,7 +868,7 @@ class FilletPolyline(BaseLineObject):
other_vertices = {ve for e in edges for ve in e.vertices() if ve != vertex}
third_edge = Edge.make_line(*[v for v in other_vertices])
fillet_face = Face(Wire(edges + [third_edge])).fillet_2d(
- radius_list[i], [vertex]
+ radius_list[i - int(not close)], [vertex]
)
fillets.append(fillet_face.edges().filter_by(GeomType.CIRCLE)[0])
diff --git a/tests/test_build_line.py b/tests/test_build_line.py
index 7b5858f..be4cd8d 100644
--- a/tests/test_build_line.py
+++ b/tests/test_build_line.py
@@ -193,6 +193,16 @@ class BuildLineTests(unittest.TestCase):
close=True,
)
+ with self.assertRaises(ValueError):
+ p = FilletPolyline(
+ (0, 0),
+ (10, 0),
+ (10, 10),
+ (0, 10),
+ radius=(1, 2, 3, 4),
+ close=False,
+ )
+
with self.assertRaises(ValueError):
p = FilletPolyline(
(0, 0),
From dc90a4b15a291b2584bd4917f9bd71eef7889769 Mon Sep 17 00:00:00 2001
From: Alex Verschoot
Date: Sun, 16 Nov 2025 15:48:30 +0100
Subject: [PATCH 038/105] Changed the FilletPolyLine to be compatible with
0-radius fillets, where it should behave like a normal Polyline
---
src/build123d/objects_curve.py | 109 ++++++++++++++++++++++++---------
1 file changed, 80 insertions(+), 29 deletions(-)
diff --git a/src/build123d/objects_curve.py b/src/build123d/objects_curve.py
index 71d788b..61731bd 100644
--- a/src/build123d/objects_curve.py
+++ b/src/build123d/objects_curve.py
@@ -787,20 +787,22 @@ class Helix(BaseEdgeObject):
class FilletPolyline(BaseLineObject):
"""Line Object: Fillet Polyline
-
Create a sequence of straight lines defined by successive points that are filleted
to a given radius.
Args:
pts (VectorLike | Iterable[VectorLike]): sequence of two or more points
- radius (float | Iterable[float]): radius to fillet at each vertex or a single value for all vertices
+ 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.
Defaults to False
+
mode (Mode, optional): combination mode. Defaults to Mode.ADD
Raises:
ValueError: Two or more points not provided
- ValueError: radius must be positive
+ ValueError: radius must be non-negative
"""
_applies_to = [BuildLine._tag]
@@ -812,9 +814,9 @@ class FilletPolyline(BaseLineObject):
close: bool = False,
mode: Mode = Mode.ADD,
):
+
context: BuildLine | None = BuildLine._get_context(self)
validate_inputs(context, self)
-
points = flatten_sequence(*pts)
if len(points) < 2:
@@ -822,30 +824,35 @@ class FilletPolyline(BaseLineObject):
if isinstance(radius, (int, float)):
radius_list = [radius] * len(points) # Single radius for all points
+
else:
radius_list = list(radius)
if len(radius_list) != len(points) - int(not close) * 2:
raise ValueError(
f"radius list length ({len(radius_list)}) must match angle count ({ len(points) - int(not close) * 2})"
)
+
for r in radius_list:
- if r <= 0:
- raise ValueError(f"radius {r} must be positive")
+ if r < 0:
+ raise ValueError(f"radius {r} must be non-negative")
lines_pts = WorkplaneList.localize(*points)
-
# Create the polyline
+
new_edges = [
Edge.make_line(lines_pts[i], lines_pts[i + 1])
for i in range(len(lines_pts) - 1)
]
+
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))
+
wire_of_lines = Wire(new_edges)
# Create a list of vertices from wire_of_lines in the same order as
# the original points so the resulting fillet edges are ordered
ordered_vertices = []
+
for pnts in lines_pts:
distance = {
v: (Vector(pnts) - Vector(*v)).length for v in wire_of_lines.vertices()
@@ -853,46 +860,90 @@ class FilletPolyline(BaseLineObject):
ordered_vertices.append(sorted(distance.items(), key=lambda x: x[1])[0][0])
# Fillet the corners
-
# Create a map of vertices to edges containing that vertex
vertex_to_edges = {
v: [e for e in wire_of_lines.edges() if v in e.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 = []
+
for i, (vertex, edges) in enumerate(vertex_to_edges.items()):
if len(edges) != 2:
continue
- other_vertices = {ve for e in edges for ve in e.vertices() if ve != vertex}
- third_edge = Edge.make_line(*[v for v in other_vertices])
- fillet_face = Face(Wire(edges + [third_edge])).fillet_2d(
- radius_list[i - int(not close)], [vertex]
- )
- fillets.append(fillet_face.edges().filter_by(GeomType.CIRCLE)[0])
+ current_radius = radius_list[i - int(not close)]
+
+ if current_radius == 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
if close:
- 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),
- ]
+ interior_edges = []
- new_wire = Wire(end_edges + interior_edges + fillets)
+ for i in range(len(fillets)):
+ prev_idx = i - 1
+ curr_idx = i
+ # Determine start and end points
+ if fillets[prev_idx] is None:
+ start_pt = ordered_vertices[prev_idx]
+ else:
+ start_pt = fillets[prev_idx] @ 1
+
+ if fillets[curr_idx] is None:
+ end_pt = ordered_vertices[curr_idx]
+ else:
+ end_pt = fillets[curr_idx] @ 0
+ interior_edges.append(Edge.make_line(start_pt, end_pt))
+
+ end_edges = []
+
+ else:
+ interior_edges = []
+ for i in range(len(fillets) - 1):
+ curr_idx = i
+ next_idx = i + 1
+ # Determine start and end points
+ if fillets[curr_idx] is None:
+ start_pt = ordered_vertices[curr_idx + 1] # +1 because first vertex has no fillet
+ else:
+ start_pt = fillets[curr_idx] @ 1
+
+ if fillets[next_idx] is None:
+ end_pt = ordered_vertices[next_idx + 1]
+ else:
+ end_pt = fillets[next_idx] @ 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)
+
class JernArc(BaseEdgeObject):
"""Line Object: Jern Arc
From c7034202f31b909703a2b310aa4ff2886df6cdee Mon Sep 17 00:00:00 2001
From: Alex Verschoot
Date: Sun, 16 Nov 2025 16:15:13 +0100
Subject: [PATCH 039/105] Changed the tests to not expect a valueorrer when
having a 0 radius, but add two assertEquals so the number of Circles and
Lines should be correct
---
tests/test_build_line.py | 5 ++++-
1 file changed, 4 insertions(+), 1 deletion(-)
diff --git a/tests/test_build_line.py b/tests/test_build_line.py
index be4cd8d..16f89ce 100644
--- a/tests/test_build_line.py
+++ b/tests/test_build_line.py
@@ -183,7 +183,7 @@ class BuildLineTests(unittest.TestCase):
self.assertEqual(len(p.edges().filter_by(GeomType.CIRCLE)), 2)
self.assertEqual(len(p.edges().filter_by(GeomType.LINE)), 3)
- with self.assertRaises(ValueError):
+ with BuildLine(Plane.YZ):
p = FilletPolyline(
(0, 0),
(10, 0),
@@ -192,6 +192,9 @@ class BuildLineTests(unittest.TestCase):
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(
From 1095f3ee4c02debba2f7473e9938dfcb50b63449 Mon Sep 17 00:00:00 2001
From: x0pherl
Date: Fri, 7 Nov 2025 21:40:11 -0500
Subject: [PATCH 040/105] changes to make development more friendly on MacOS
---
.gitignore | 3 +++
CONTRIBUTING.md | 4 ++--
2 files changed, 5 insertions(+), 2 deletions(-)
diff --git a/.gitignore b/.gitignore
index ed011f3..a79817d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -41,3 +41,6 @@ venv.bak/
# Profiling debris.
prof/
+
+# MacOS cruft
+.DS_Store
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index c1344c3..78f6540 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -3,8 +3,8 @@ tests, ensure they build and pass, and ensure that `pylint` and `mypy`
are happy with your code.
- Install `pip` following their [documentation](https://pip.pypa.io/en/stable/installation/).
-- Install development dependencies: `pip install -e .[development]`
-- Install docs dependencies: `pip install -e .[docs]`
+- Install development dependencies: `pip install -e ".[development]"`
+- Install docs dependencies: `pip install -e ".[docs]"`
- Install `build123d` in editable mode from current dir: `pip install -e .`
- Run tests with: `python -m pytest -n auto`
- Build docs with: `cd docs && make html`
From d329cf109484ccbb96872b311d4779d949c813bb Mon Sep 17 00:00:00 2001
From: jdegenstein
Date: Mon, 17 Nov 2025 10:09:54 -0600
Subject: [PATCH 041/105] initial changes to support BytesIO
---
src/build123d/exporters3d.py | 25 ++++++++++++++++---------
1 file changed, 16 insertions(+), 9 deletions(-)
diff --git a/src/build123d/exporters3d.py b/src/build123d/exporters3d.py
index 749e331..505d4aa 100644
--- a/src/build123d/exporters3d.py
+++ b/src/build123d/exporters3d.py
@@ -182,7 +182,7 @@ def export_brep(
def export_gltf(
to_export: Shape,
- file_path: PathLike | str | bytes,
+ file_path: PathLike | str | bytes | BytesIO,
unit: Unit = Unit.MM,
binary: bool = False,
linear_deflection: float = 0.001,
@@ -198,7 +198,7 @@ def export_gltf(
Args:
to_export (Shape): object or assembly
- file_path (Union[PathLike, str, bytes]): glTF file path
+ file_path (Union[PathLike, str, bytes, BytesIO]): glTF file path
unit (Unit, optional): shape units. Defaults to Unit.MM.
binary (bool, optional): output format. Defaults to False.
linear_deflection (float, optional): A linear deflection setting which limits
@@ -234,9 +234,12 @@ def export_gltf(
# Create the XCAF document
doc: TDocStd_Document = _create_xde(to_export, unit)
+ if not isinstance(file_path, BytesIO):
+ file_path = fsdecode(file_path)
+
# Write the glTF file
writer = RWGltf_CafWriter(
- theFile=TCollection_AsciiString(fsdecode(file_path)), theIsBinary=binary
+ theFile=TCollection_AsciiString(file_path, theIsBinary=binary
)
writer.SetParallel(True)
index_map = TColStd_IndexedDataMapOfStringString()
@@ -262,7 +265,7 @@ def export_gltf(
def export_step(
to_export: Shape,
- file_path: PathLike | str | bytes,
+ file_path: PathLike | str | bytes | BytesIO,
unit: Unit = Unit.MM,
write_pcurves: bool = True,
precision_mode: PrecisionMode = PrecisionMode.AVERAGE,
@@ -277,7 +280,7 @@ def export_step(
Args:
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.
write_pcurves (bool, optional): write parametric curves to the STEP file.
Defaults to True.
@@ -326,7 +329,10 @@ def export_step(
Interface_Static.SetIVal_s("write.precision.mode", precision_mode.value)
writer.Transfer(doc, STEPControl_StepModelType.STEPControl_AsIs)
- status = writer.Write(fspath(file_path)) == IFSelect_ReturnStatus.IFSelect_RetDone
+ if not isinstance(file_path, BytesIO):
+ file_path = fspath(file_path)
+
+ status = writer.Write(file_path) == IFSelect_ReturnStatus.IFSelect_RetDone
if not status:
raise RuntimeError("Failed to write STEP file")
@@ -335,7 +341,7 @@ def export_step(
def export_stl(
to_export: Shape,
- file_path: PathLike | str | bytes,
+ file_path: PathLike | str | bytes | BytesIO,
tolerance: float = 1e-3,
angular_tolerance: float = 0.1,
ascii_format: bool = False,
@@ -346,7 +352,7 @@ def export_stl(
Args:
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, BytesIO]): The path and file name to write the STL output to.
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
large meshes that can consume computing resources. Setting the value too high can
@@ -369,6 +375,7 @@ def export_stl(
writer.ASCIIMode = ascii_format
- file_path = str(file_path)
+ if not isinstance(file_path, BytesIO):
+ file_path = fsdecode(file_path)
return writer.Write(to_export.wrapped, file_path)
From 10ef3b0734b442abad02bd97f156bd2146a3b5d7 Mon Sep 17 00:00:00 2001
From: Jonathan Wagenet
Date: Mon, 17 Nov 2025 18:40:50 -0500
Subject: [PATCH 042/105] Shrink margin and center
---
docs/assets/build123d_logo/logo-banner.svg | 349 ++++++++++++++++++++-
1 file changed, 347 insertions(+), 2 deletions(-)
diff --git a/docs/assets/build123d_logo/logo-banner.svg b/docs/assets/build123d_logo/logo-banner.svg
index cb11160..cbfb8d7 100644
--- a/docs/assets/build123d_logo/logo-banner.svg
+++ b/docs/assets/build123d_logo/logo-banner.svg
@@ -1,2 +1,347 @@
-
-
+
+
From 7f4e92f0bf21f7d08725c376de99d25644ac58c2 Mon Sep 17 00:00:00 2001
From: jdegenstein
Date: Mon, 17 Nov 2025 22:05:45 -0600
Subject: [PATCH 043/105] enable BytesIO in STEP, STL and 3MF (via
lib3mf/Mesher). Add necessary tests
---
src/build123d/exporters.py | 20 +++++++++++++++-----
src/build123d/exporters3d.py | 26 +++++++++++---------------
src/build123d/mesher.py | 29 ++++++++++++++++++-----------
tests/test_exporters.py | 14 +++++++++++++-
tests/test_exporters3d.py | 5 +++--
tests/test_mesher.py | 9 +++++++++
6 files changed, 69 insertions(+), 34 deletions(-)
diff --git a/src/build123d/exporters.py b/src/build123d/exporters.py
index a229fa2..687e2f1 100644
--- a/src/build123d/exporters.py
+++ b/src/build123d/exporters.py
@@ -34,6 +34,7 @@ import math
import xml.etree.ElementTree as ET
from copy import copy
from enum import Enum, auto
+from io import BytesIO
from os import PathLike, fsdecode
from typing import Any, TypeAlias
from warnings import warn
@@ -636,13 +637,13 @@ class ExportDXF(Export2D):
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- def write(self, file_name: PathLike | str | bytes):
+ def write(self, file_name: PathLike | str | bytes | BytesIO):
"""write
Writes the DXF data to the specified file name.
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.
"""
# Reset the main CAD viewport of the model space to the
@@ -650,7 +651,12 @@ class ExportDXF(Export2D):
# https://github.com/gumyr/build123d/issues/382 tracks
# exposing viewport control to the user.
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")
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
@@ -1497,13 +1503,13 @@ class ExportSVG(Export2D):
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- def write(self, path: PathLike | str | bytes):
+ def write(self, path: PathLike | str | bytes | BytesIO):
"""write
Writes the SVG data to the specified file path.
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
bb = self._bounds
@@ -1549,5 +1555,9 @@ class ExportSVG(Export2D):
xml = ET.ElementTree(svg)
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=None)
diff --git a/src/build123d/exporters3d.py b/src/build123d/exporters3d.py
index 505d4aa..759464a 100644
--- a/src/build123d/exporters3d.py
+++ b/src/build123d/exporters3d.py
@@ -182,7 +182,7 @@ def export_brep(
def export_gltf(
to_export: Shape,
- file_path: PathLike | str | bytes | BytesIO,
+ file_path: PathLike | str | bytes,
unit: Unit = Unit.MM,
binary: bool = False,
linear_deflection: float = 0.001,
@@ -198,7 +198,7 @@ def export_gltf(
Args:
to_export (Shape): object or assembly
- file_path (Union[PathLike, str, bytes, BytesIO]): glTF file path
+ file_path (Union[PathLike, str, bytes]): glTF file path
unit (Unit, optional): shape units. Defaults to Unit.MM.
binary (bool, optional): output format. Defaults to False.
linear_deflection (float, optional): A linear deflection setting which limits
@@ -234,12 +234,9 @@ def export_gltf(
# Create the XCAF document
doc: TDocStd_Document = _create_xde(to_export, unit)
- if not isinstance(file_path, BytesIO):
- file_path = fsdecode(file_path)
-
# Write the glTF file
writer = RWGltf_CafWriter(
- theFile=TCollection_AsciiString(file_path, theIsBinary=binary
+ theFile=TCollection_AsciiString(fsdecode(file_path)), theIsBinary=binary
)
writer.SetParallel(True)
index_map = TColStd_IndexedDataMapOfStringString()
@@ -330,9 +327,12 @@ def export_step(
writer.Transfer(doc, STEPControl_StepModelType.STEPControl_AsIs)
if not isinstance(file_path, BytesIO):
- file_path = fspath(file_path)
+ status = (
+ writer.Write(fspath(file_path)) == IFSelect_ReturnStatus.IFSelect_RetDone
+ )
+ else:
+ status = writer.WriteStream(file_path) == IFSelect_ReturnStatus.IFSelect_RetDone
- status = writer.Write(file_path) == IFSelect_ReturnStatus.IFSelect_RetDone
if not status:
raise RuntimeError("Failed to write STEP file")
@@ -341,7 +341,7 @@ def export_step(
def export_stl(
to_export: Shape,
- file_path: PathLike | str | bytes | BytesIO,
+ file_path: PathLike | str | bytes,
tolerance: float = 1e-3,
angular_tolerance: float = 0.1,
ascii_format: bool = False,
@@ -352,7 +352,7 @@ def export_stl(
Args:
to_export (Shape): object or assembly
- file_path (Union[PathLike, str, bytes, BytesIO]): 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
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
@@ -374,8 +374,4 @@ def export_stl(
writer = StlAPI_Writer()
writer.ASCIIMode = ascii_format
-
- if not isinstance(file_path, BytesIO):
- file_path = fsdecode(file_path)
-
- return writer.Write(to_export.wrapped, file_path)
+ return writer.Write(to_export.wrapped, fsdecode(file_path))
diff --git a/src/build123d/mesher.py b/src/build123d/mesher.py
index 5433848..8c4ce42 100644
--- a/src/build123d/mesher.py
+++ b/src/build123d/mesher.py
@@ -83,6 +83,7 @@ license:
# pylint: disable=no-name-in-module, import-error
import copy as copy_module
import ctypes
+from io import BytesIO
import math
import os
import sys
@@ -312,12 +313,12 @@ class Mesher:
# Round off the vertices to avoid vertices within tolerance being
# considered as different vertices
digits = -int(round(math.log(TOLERANCE, 10), 1))
-
+
# Create vertex to index mapping directly
vertex_to_idx = {}
next_idx = 0
vert_table = {}
-
+
# First pass - create mapping
for i, (x, y, z) in enumerate(ocp_mesh_vertices):
key = (round(x, digits), round(y, digits), round(z, digits))
@@ -325,17 +326,16 @@ class Mesher:
vertex_to_idx[key] = next_idx
next_idx += 1
vert_table[i] = vertex_to_idx[key]
-
+
# Create vertices array in one shot
vertices_3mf = [
- Lib3MF.Position((ctypes.c_float * 3)(*v))
- for v in vertex_to_idx.keys()
+ Lib3MF.Position((ctypes.c_float * 3)(*v)) for v in vertex_to_idx.keys()
]
-
+
# Pre-allocate triangles array and process in bulk
c_uint3 = ctypes.c_uint * 3
triangles_3mf = []
-
+
# Process triangles in bulk
for tri in triangles:
# Map indices directly without list comprehension
@@ -343,11 +343,13 @@ class Mesher:
mapped_a = vert_table[a]
mapped_b = vert_table[b]
mapped_c = vert_table[c]
-
+
# Quick degenerate check without set creation
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)
def _add_color(self, b3d_shape: Shape, mesh_3mf: Lib3MF.MeshObject):
@@ -540,7 +542,7 @@ class Mesher:
"""write
Args:
- file_name Union[Pathlike, str, bytes]: file path
+ file_name Union[Pathlike, str, bytes, BytesIO]: file path
Raises:
ValueError: Unknown file format - must be 3mf or stl
@@ -551,3 +553,8 @@ class Mesher:
raise ValueError(f"Unknown file format {output_file_extension}")
writer = self.model.QueryWriter(output_file_extension[1:])
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)
diff --git a/tests/test_exporters.py b/tests/test_exporters.py
index f95a92e..11c63da 100644
--- a/tests/test_exporters.py
+++ b/tests/test_exporters.py
@@ -1,3 +1,4 @@
+from io import BytesIO
from os import fsdecode, fsencode
from typing import Union, Iterable
import math
@@ -194,7 +195,9 @@ class ExportersTestCase(unittest.TestCase):
@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))
def test_pathlike_exporters(tmp_path, format, Exporter):
@@ -205,5 +208,14 @@ def test_pathlike_exporters(tmp_path, format, Exporter):
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__":
unittest.main()
diff --git a/tests/test_exporters3d.py b/tests/test_exporters3d.py
index 644ac3e..bf1c1bd 100644
--- a/tests/test_exporters3d.py
+++ b/tests/test_exporters3d.py
@@ -206,10 +206,11 @@ def test_pathlike_exporters(tmp_path, format, exporter):
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()
box = Box(1, 1, 1).locate(Pos(-1, -2, -3))
- export_brep(box, buffer)
+ exporter(box, buffer)
if __name__ == "__main__":
diff --git a/tests/test_mesher.py b/tests/test_mesher.py
index 0be4ccb..59b7214 100644
--- a/tests/test_mesher.py
+++ b/tests/test_mesher.py
@@ -1,4 +1,5 @@
import unittest, uuid
+from io import BytesIO
from packaging.specifiers import SpecifierSet
from pathlib import Path
from os import fsdecode, fsencode
@@ -237,5 +238,13 @@ def test_pathlike_mesher(tmp_path, format):
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__":
unittest.main()
From f144ca5aa89d1df5be325befcd5d45d946d1ca8d Mon Sep 17 00:00:00 2001
From: Jonathan Wagenet
Date: Tue, 18 Nov 2025 10:34:21 -0500
Subject: [PATCH 044/105] Fix tutorial links
---
docs/tutorial_surface_heart_token.rst | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/docs/tutorial_surface_heart_token.rst b/docs/tutorial_surface_heart_token.rst
index ac819ad..9931108 100644
--- a/docs/tutorial_surface_heart_token.rst
+++ b/docs/tutorial_surface_heart_token.rst
@@ -20,7 +20,7 @@ the object. To illustrate this process, we will create the following game token:
Useful :class:`~topology.Face` creation methods include
:meth:`~topology.Face.make_surface`, :meth:`~topology.Face.make_bezier_surface`,
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
the perimeter of the surface and a central point on that surface.
@@ -128,5 +128,5 @@ from the heart.
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.
From 68e1c19c73aa2306b5dd05b66a7e8e8d31e44edf Mon Sep 17 00:00:00 2001
From: Jonathan Wagenet
Date: Tue, 18 Nov 2025 13:16:13 -0500
Subject: [PATCH 045/105] Add headings, demo images, section with basic usage
and feature overview, slim down installation
---
CONTRIBUTING.md | 37 ++++-
README.md | 234 ++++++++++++++++++++++++++----
docs/assets/readme/add_part.png | Bin 0 -> 22053 bytes
docs/assets/readme/create_1d.png | Bin 0 -> 9742 bytes
docs/assets/readme/extend.png | Bin 0 -> 22857 bytes
docs/assets/readme/readme.py | 95 ++++++++++++
docs/assets/readme/upgrade_2d.png | Bin 0 -> 39215 bytes
7 files changed, 329 insertions(+), 37 deletions(-)
create mode 100644 docs/assets/readme/add_part.png
create mode 100644 docs/assets/readme/create_1d.png
create mode 100644 docs/assets/readme/extend.png
create mode 100644 docs/assets/readme/readme.py
create mode 100644 docs/assets/readme/upgrade_2d.png
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 78f6540..24a462c 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -1,13 +1,34 @@
-When writing code for inclusion in build123d please add docs and
+# Contributing
+
+When writing code for inclusion in build123d please add docstrings and
tests, ensure they build and pass, and ensure that `pylint` and `mypy`
are happy with your code.
-- Install `pip` following their [documentation](https://pip.pypa.io/en/stable/installation/).
-- Install development dependencies: `pip install -e ".[development]"`
-- Install docs dependencies: `pip install -e ".[docs]"`
-- Install `build123d` in editable mode from current dir: `pip install -e .`
+## Setup
+
+Ensure `pip` is installed and [up-to-date](https://pip.pypa.io/en/stable/installation/#upgrading-pip).
+Clone the build123d repo and install in editable mode:
+
+```
+git clone https://github.com/gumyr/build123d.git
+cd build123d
+pip install -e .
+```
+
+Install development and docs dependencies:
+
+```
+pip install -e ".[development]"
+pip install -e ".[docs]"
+```
+
+## Before submitting a PR
+
- Run tests with: `python -m pytest -n auto`
-- Build docs with: `cd docs && make html`
-- Check added files' style with: `pylint `
+- Check added files' style with: `pylint `
- Check added files' type annotations with: `mypy `
-- Run black formatter against files' changed: `black --config pyproject.toml ` (where the pyproject.toml is from this project's repository)
+- Run black formatter against files' changed: `black `
+
+To verify documentation changes build docs with:
+- Linux/macOS: `./docs/make html`
+- Windows: `./docs/make.bat html`
diff --git a/README.md b/README.md
index cb7c309..afb9320 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,6 @@
-
+
-
+
[](https://build123d.readthedocs.io/en/latest/?badge=latest)
[](https://github.com/gumyr/build123d/actions/workflows/test.yml)
@@ -18,63 +18,239 @@
[](https://pypi.org/project/build123d/)
[](https://doi.org/10.5281/zenodo.14872322)
+[Documentation](https://build123d.readthedocs.io/en/latest/index.html) |
+[Cheat Sheet](https://build123d.readthedocs.io/en/latest/cheat_sheet.html) |
+[Discord](https://discord.com/invite/Bj9AQPsCfx) |
+[Discussions](https://github.com/gumyr/build123d/discussions) |
+[Issues](https://github.com/gumyr/build123d/issues ) |
+[Contributing]()
-Build123d is a Python-based, parametric [boundary representation (BREP)][BREP] modeling framework for 2D and 3D CAD. Built on the [Open Cascade] geometric kernel, it provides a clean, fully Pythonic interface for creating precise models suitable for 3D printing, CNC machining, laser cutting, and other manufacturing processes. Models can be exported to popular CAD tools such as [FreeCAD] and SolidWorks.
+build123d is a Python-based, parametric [boundary representation (BREP)][BREP] modeling framework for 2D and 3D CAD. Built on the [Open Cascade] geometric kernel, it provides a clean, fully Pythonic interface for creating precise models suitable for 3D printing, CNC machining, laser cutting, and other manufacturing processes.
+
+
+
+
+
+
+
+## Features
Designed for modern, maintainable CAD-as-code, build123d combines clear architecture with expressive, algebraic modeling. It offers:
- Minimal or no internal state depending on mode,
- Explicit 1D, 2D, and 3D geometry classes with well-defined operations,
- Extensibility through subclassing and functional composition—no monkey patching,
- Standards-compliant code (PEP 8, mypy, pylint) with rich pylance type hints,
-- Deep Python integration—selectors as lists, locations as iterables, and natural conversions (Solid(shell), tuple(Vector)),
-- Operator-driven modeling (obj += sub_obj, Plane.XZ * Pos(X=5) * Rectangle(1, 1)) for algebraic, readable, and composable design logic.
+- Deep Python integration—selectors as lists, locations as iterables, and natural conversions (`Solid(shell)`, `tuple(Vector)`),
+- Operator-driven modeling (`obj += sub_obj`, `Plane.XZ * Pos(X=5) * Rectangle(1, 1)`) for algebraic, readable, and composable design logic,
+- Export formats to popular CAD tools such as [FreeCAD] and SolidWorks.
-The result is a framework that feels native to Python while providing the full power of OpenCascade geometry underneath.
+## Usage
-The documentation for **build123d** can be found at [readthedocs](https://build123d.readthedocs.io/en/latest/index.html).
+Although wildcard imports are generally bad practice, build123d scripts are usually self contained and importing the large number of objects and methods into the namespace is common:
-There is a [***Discord***](https://discord.com/invite/Bj9AQPsCfx) server (shared with [CadQuery]) where you can ask for help in the build123d channel.
+```py
+from build123d import *
+```
-The recommended method for most users to install **build123d** is:
+### Constructing a 1D Shape
+
+Edges, Wires (multiple connected Edges), and Curves (a Compound of Edges and Wires) are the 1D Shapes available in build123d. A single Edge can be created from a Line object with two vector-like positions:
+
+```py
+line = Line((0, -3), (6, -3))
+```
+
+Additional Edges and Wires may be added to (or subtracted from) the initial line. These objects can reference coordinates along another line through the position (`@`) and tangent (`%`) operators to specify input Vectors:
+
+```py
+line += JernArc(line @ 1, line % 1, radius=3, arc_size=180)
+line += PolarLine(line @ 1, 6, direction=line % 1)
+```
+
+
+
+
+
+### Upgrading to 2D and 3D
+
+Faces, Shells (multiple connected Faces), and Sketches (a Compound of Faces and Shells) are the 2d Shapes available in build123d. The previous line is sufficiently defined to close the Wire and create a Face with `make_hull`:
+
+```py
+sketch = make_hull(line)
+```
+
+A Circle face is translated with `Pos`, a Location object like `Rot` for transforming Shapes, and subtracted from the sketch. This sketch face is then extruded into a Solid part:
+
+```py
+sketch -= Pos(6, 0, 0) * Circle(2)
+part = extrude(sketch, amount= 2)
+```
+
+
+
+
+
+### Adding to and modifying part
+
+Solids and Parts (a Compound of Solids) are the 1D Shapes available in build123d. A second part can be created from an additional Face. Planes can also be used for positioning and orienting Shape objects. Many objects offer an affordance for alignment relative to the object origin:
+
+```py
+plate_sketch = Plane.YZ * RectangleRounded(16, 6, 1.5, align=(Align.CENTER, Align.MIN))
+plate = extrude(plate_sketch, amount=-2)
+```
+
+Shape topology can be extracted from Shapes with selectors which return ShapeLists. ShapeLists offer methods for sorting, grouping, and filtering Shapes by Shape properties, such as finding a Face by area and selecting position along an Axis and specifying a target with a list slice. A Plane is created from the specified Face to locate an iterable of Locations to place multiple objects on the second part before it is added to the main part:
+
+```py
+plate_face = plate.faces().group_by(Face.area)[-1].sort_by(Axis.X)[-1]
+plate -= Plane(plate_face) * GridLocations(13, 3, 2, 2) * CounterSinkHole(.5, 1, 2)
+
+part += plate
+```
+
+ShapeList selectors and operators offer powerful methods for specifying Shape features through properties such as length/area/volume, orientation relative to an Axis or Plane, and geometry type:
+
+```py
+part = fillet(part.edges().filter_by(lambda e: e.length == 2).filter_by(Axis.Z), 1)
+bore = part.faces().filter_by(GeomType.CYLINDER).filter_by(lambda f: f.radius == 2)
+part = chamfer(bore.edges(), .2)
+```
+
+
+
+
+
+### Builder Mode
+
+The previous construction is through the **Algebra Mode** interface, which follows a stateless paradigm where each object is explicitly tracked and mutated by algebraic operators.
+
+**Builder Mode** is an alternative build123d interface where state is tracked and structured in a design history-like way where each dimension is distinct. Operations are aware pending faces and edges from Build contexts and location transformations are applied to all child objects in Build and Locations contexts. Builder mode also introduces the `mode` affordance to objects to specify how new Shapes are combined with the context:
+
+```py
+with BuildPart() as part_context:
+ with BuildSketch() as sketch:
+ with BuildLine() as line:
+ l1 = Line((0, -3), (6, -3))
+ l2 = JernArc(l1 @ 1, l1 % 1, radius=3, arc_size=180)
+ l3 = PolarLine(l2 @ 1, 6, direction=l2 % 1)
+ l4 = Line(l1 @ 0, l3 @ 1)
+ make_face()
+
+ with Locations((6, 0, 0)):
+ Circle(2, mode=Mode.SUBTRACT)
+
+ extrude(amount=2)
+
+ with BuildSketch(Plane.YZ) as plate_sketch:
+ RectangleRounded(16, 6, 1.5, align=(Align.CENTER, Align.MIN))
+
+ plate = extrude(amount=-2)
+
+ with Locations(plate.faces().group_by(Face.area)[-1].sort_by(Axis.X)[-1]):
+ with GridLocations(13, 3, 2, 2):
+ CounterSinkHole(.5, 1)
+
+ fillet(edges().filter_by(lambda e: e.length == 2).filter_by(Axis.Z), 1)
+ bore = faces().filter_by(GeomType.CYLINDER).filter_by(lambda f: f.radius == 2)
+ chamfer(bore.edges(), .2)
+```
+
+### Extending objects
+
+New objects may be created for parametric reusability from base object classes:
+
+```py
+class Punch(BaseSketchObject):
+ def __init__(
+ self,
+ radius: float,
+ size: float,
+ blobs: float,
+ mode: Mode = Mode.ADD,
+ ):
+ with BuildSketch() as punch:
+ if blobs == 1:
+ Circle(size)
+ else:
+ with PolarLocations(radius, blobs):
+ Circle(size)
+
+ if len(faces()) > 1:
+ raise RuntimeError("radius is too large for number and size of blobs")
+
+ add(Face(faces()[0].outer_wire()), mode=Mode.REPLACE)
+
+ super().__init__(obj=punch.sketch, mode=mode)
+
+tape = Rectangle(20, 5)
+for i, location in enumerate(GridLocations(5, 0, 4, 1)):
+ tape -= location * Punch(.8, 1, i + 1)
+```
+
+