Add new docs section "Selectors and Operators"

- Expand ideas and concepts from key concepts section
- Provide minimal usage examples in front matter
- Add structure for examples of low to medium complexity for different criteria types
This commit is contained in:
Jonathan Wagenet 2025-04-06 20:10:00 -04:00
parent bde03f40e7
commit ee11c3517d
61 changed files with 1411 additions and 2 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 111 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

View file

@ -113,6 +113,7 @@ Table Of Contents
introductory_examples.rst
tutorials.rst
objects.rst
selectors_operators.rst
operations.rst
builders.rst
joints.rst

View file

@ -0,0 +1,393 @@
#######################
Selectors and Operators
#######################
Selectors and operators are powerful methods to select and organize CAD objects for
subsequent operations.
.. _selectors:
*********
Selectors
*********
Selectors provide methods to extract all or a subset of a feature type in the referenced
object. These methods select Edges, Faces, Solids, Vertices, or Wires in Builder objects
or from Shape objects themselves. All of these methods return a :class:`~topology.ShapeList`,
which is a subclass of ``list`` and may be sorted, grouped, or filtered by
:ref:`operators`.
Overview
========
+--------------+----------------+-----------------------------------------------+-----------------------+
| Selector | Criteria | Applicability | Description |
+==============+================+===============================================+=======================+
| |vertices| | ALL, LAST | ``BuildLine``, ``BuildSketch``, ``BuildPart`` | ``Vertex`` extraction |
+--------------+----------------+-----------------------------------------------+-----------------------+
| |edges| | ALL, LAST, NEW | ``BuildLine``, ``BuildSketch``, ``BuildPart`` | ``Edge`` extraction |
+--------------+----------------+-----------------------------------------------+-----------------------+
| |wires| | ALL, LAST | ``BuildLine``, ``BuildSketch``, ``BuildPart`` | ``Wire`` extraction |
+--------------+----------------+-----------------------------------------------+-----------------------+
| |faces| | ALL, LAST | ``BuildSketch``, ``BuildPart`` | ``Face`` extraction |
+--------------+----------------+-----------------------------------------------+-----------------------+
| |solids| | ALL, LAST | ``BuildPart`` | ``Solid`` extraction |
+--------------+----------------+-----------------------------------------------+-----------------------+
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
# In context
with BuildSketch() as context:
Rectangle(1, 1)
context.edges()
# Build context implicitly has access to the selector
edges()
# Taking the sketch out of context
context.sketch.edges()
# Create sketch out of context
Rectangle(1, 1).edges()
Select In Build Objects
========================
Build objects track the last operation and their selector methods can take
:class:`~build_enums.Select` as criteria to specify a subset of
features to extract. By default, a selector will select ``ALL`` of a feature, while
``LAST`` selects features created or altered by the most recent operation. |edges| can
uniquely specify ``NEW`` to only select edges created in the last operation which neither
existed in the referenced object before the last operation, nor the modifying object.
.. important::
:class:`~build_enums.Select` as selector criteria is only valid for builder objects!
.. code-block:: python
# In context
with BuildPart() as context:
Box(2, 2, 1)
Cylinder(1, 2)
context.edges(Select.LAST)
# Does not work out of context!
context.part.edges(Select.LAST)
(Box(2, 2, 1) + Cylinder(1, 2)).edges(Select.LAST)
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
with BuildPart() as part:
Box(5, 5, 1)
Cylinder(1, 5)
part.vertices()
part.edges()
part.faces()
# Is the same as
part.vertices(Select.ALL)
part.edges(Select.ALL)
part.faces(Select.ALL)
.. figure:: assets/selectors_operators/selectors_select_all.png
:align: center
The default ``Select.ALL`` features
Select features changed in the last operation with criteria ``Select.LAST``.
.. code-block:: python
with BuildPart() as part:
Box(5, 5, 1)
Cylinder(1, 5)
part.vertices(Select.LAST)
part.edges(Select.LAST)
part.faces(Select.LAST)
.. figure:: assets/selectors_operators/selectors_select_last.png
:align: center
``Select.LAST`` features
Select only new edges from the last operation with ``Select.NEW``. This option is only
available for a ``ShapeList`` of edges!
.. code-block:: python
with BuildPart() as part:
Box(5, 5, 1)
Cylinder(1, 5)
part.edges(Select.NEW)
.. figure:: assets/selectors_operators/selectors_select_new.png
:align: center
``Select.NEW`` edges where box and cylinder intersect
This only returns new edges which are not reused from Box or Cylinder, in this case where
the objects `intersect`. But what happens if the objects don't intersect and all the
edges are reused?
.. code-block:: python
with BuildPart() as part:
Box(5, 5, 1, align=(Align.CENTER, Align.CENTER, Align.MAX))
Cylinder(2, 2, align=(Align.CENTER, Align.CENTER, Align.MIN))
part.edges(Select.NEW)
.. figure:: assets/selectors_operators/selectors_select_new_none.png
:align: center
``Select.NEW`` edges when box and cylinder don't intersect
No edges are selected! Unlike the previous example, the Edge between the Box and Cylinder
objects is an edge reused from the Cylinder. Think of ``Select.NEW`` as a way to select
only completely new edges created by the operation.
.. note::
Chamfer and fillet modify the current object, but do not have new edges.
.. code-block:: python
with BuildPart() as part:
Box(5, 5, 1)
Cylinder(1, 5)
edges = part.edges().filter_by(lambda a: a.length == 1)
fillet(edges, 1)
part.edges(Select.NEW)
.. figure:: assets/selectors_operators/selectors_select_new_fillet.png
:align: center
Left, ``Select.NEW`` returns no edges after fillet. Right, ``Select.LAST``
.. _operators:
*********
Operators
*********
Operators provide methods refine a ``ShapeList`` of features isolated by a *selector* to
further specify feature(s). These methods can sort, group, or filter ``ShapeList``
objects and return a modified ``ShapeList``, or in the case of |group_by|, ``GroupBy``,
a list of ``ShapeList`` objects accessible by index or key.
Overview
========
+----------------------+------------------------------------------------------------------+-------------------------------------------------------+
| Method | Criteria | Description |
+======================+==================================================================+=======================================================+
| |sort_by| | ``Axis``, ``Edge``, ``Wire``, ``SortBy``, callable, property | Sort ``ShapeList`` by criteria |
+----------------------+------------------------------------------------------------------+-------------------------------------------------------+
| |sort_by_distance| | ``Shape``, ``VectorLike`` | Sort ``ShapeList`` by distance from criteria |
+----------------------+------------------------------------------------------------------+-------------------------------------------------------+
| |group_by| | ``Axis``, ``Edge``, ``Wire``, ``SortBy``, callable, property | Group ``ShapeList`` by criteria |
+----------------------+------------------------------------------------------------------+-------------------------------------------------------+
| |filter_by| | ``Axis``, ``Plane``, ``GeomType``, ``ShapePredicate``, property | Filter ``ShapeList`` by criteria |
+----------------------+------------------------------------------------------------------+-------------------------------------------------------+
| |filter_by_position| | ``Axis`` | Filter ``ShapeList`` by ``Axis`` & mix / max values |
+----------------------+------------------------------------------------------------------+-------------------------------------------------------+
Operator methods take criteria to refine ``ShapeList``. Broadly speaking, the criteria
fall into the following categories, though not all operators take all criteria:
- Geometric objects: ``Axis``, ``Plane``
- Topological objects: ``Edge``, ``Wire``
- Enums: :class:`~build_enums.SortBy`, :class:`~build_enums.GeomType`
- Properties, eg: ``Face.area``, ``Edge.length``
- ``ShapePredicate``, eg: ``lambda e: e.is_interior == 1``, ``lambda f: lf.edges() >= 3``
- Callable eg: ``Vertex().distance``
Sort
=======
A ``ShapeList`` can be sorted with the |sort_by| and |sort_by_distance|
methods based on a sorting criteria. Sorting is a critical step when isolating individual
features as a ``ShapeList`` from a selector is typically unordered.
Here we want to capture some vertices from the object furthest along ``X``: All the
vertices are first captured with the |vertices| selector, then sort by ``Axis.X``.
Finally, the vertices can be captured with a list slice for the last 4 list items, as the
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
part.vertices().sort_by(Axis.X)[-4:]
.. figure:: assets/selectors_operators/operators_sort_x.png
:align: center
|
Examples
--------
.. toctree::
:maxdepth: 2
:hidden:
selectors_operators/sort_examples
.. grid:: 3
:gutter: 3
.. grid-item-card:: SortBy
:img-top: assets/selectors_operators/thumb_sort_sortby.png
:link: sort_sortby
:link-type: ref
.. grid-item-card:: Along Wire
:img-top: assets/selectors_operators/thumb_sort_along_wire.png
:link: sort_along_wire
:link-type: ref
.. grid-item-card:: Axis
:img-top: assets/selectors_operators/thumb_sort_axis.png
:link: sort_axis
:link-type: ref
.. grid-item-card:: Distance From
:img-top: assets/selectors_operators/thumb_sort_distance.png
:link: sort_distance_from
:link-type: ref
Group
========
A ShapeList can be grouped and sorted with the |group_by| method based on a grouping
criteria. Grouping can be a great way to organize features without knowing the values of
specific feature properties. Rather than returning a ``Shapelist``, |group_by| returns
a ``GroupBy`` which can be indexed to retrieve a ``Shapelist`` for further operations.
``GroupBy`` groups can also be accessed using a key with the ``group`` method. If the
keys are unknown they can be discovered with ``key_to_group_index``.
If we want only the edges from the smallest faces by area we can get the faces, then
group by ``SortBy.AREA``. The ``ShapeList`` of smallest faces is available from the first
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
part.faces().group_by(SortBy.AREA)[0].edges())
.. figure:: assets/selectors_operators/operators_group_area.png
:align: center
|
Examples
--------
.. toctree::
:maxdepth: 2
:hidden:
selectors_operators/group_examples
.. grid:: 3
:gutter: 3
.. grid-item-card:: Axis and Length
:img-top: assets/selectors_operators/thumb_group_axis.png
:link: group_axis
:link-type: ref
.. grid-item-card:: Hole Area
:img-top: assets/selectors_operators/thumb_group_hole_area.png
:link: group_hole_area
:link-type: ref
.. grid-item-card:: Properties with Keys
:img-top: assets/selectors_operators/thumb_group_properties_with_keys.png
:link: group_properties_with_keys
:link-type: ref
Filter
=========
A ``ShapeList`` can be filtered with the |filter_by| and |filter_by_position| methods based
on a filtering criteria. Filters are flexible way to isolate (or exclude) features based
on known criteria.
Lets say we need all the faces with a normal in the ``+Z`` direction. One way to do this
might be with a list comprehension, however |filter_by| has the capability to take a
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
part.faces().filter_by(lambda f: f.normal_at() == Vector(0, 0, 1))
.. figure:: assets/selectors_operators/operators_filter_z_normal.png
:align: center
|
Examples
--------
.. toctree::
:maxdepth: 2
:hidden:
selectors_operators/filter_examples
.. grid:: 3
:gutter: 3
.. grid-item-card:: GeomType
:img-top: assets/selectors_operators/thumb_filter_geomtype.png
:link: filter_geomtype
:link-type: ref
.. grid-item-card:: All Edges Circle
:img-top: assets/selectors_operators/thumb_filter_all_edges_circle.png
:link: filter_all_edges_circle
:link-type: ref
.. grid-item-card:: Axis and Plane
:img-top: assets/selectors_operators/thumb_filter_axisplane.png
:link: filter_axis_plane
:link-type: ref
.. grid-item-card:: Inner Wire Count
:img-top: assets/selectors_operators/thumb_filter_inner_wire_count.png
:link: filter_inner_wire_count
:link-type: ref
.. grid-item-card:: Nested Filters
:img-top: assets/selectors_operators/thumb_filter_nested.png
:link: filter_nested
:link-type: ref
.. grid-item-card:: Shape Properties
:img-top: assets/selectors_operators/thumb_filter_shape_properties.png
:link: filter_shape_properties
:link-type: ref
.. |vertices| replace:: :meth:`~topology.Shape.vertices`
.. |edges| replace:: :meth:`~topology.Shape.edges`
.. |wires| replace:: :meth:`~topology.Shape.wires`
.. |faces| replace:: :meth:`~topology.Shape.faces`
.. |solids| replace:: :meth:`~topology.Shape.solids`
.. |sort_by| replace:: :meth:`~topology.ShapeList.sort_by`
.. |sort_by_distance| replace:: :meth:`~topology.ShapeList.sort_by_distance`
.. |group_by| replace:: :meth:`~topology.ShapeList.group_by`
.. |filter_by| replace:: :meth:`~topology.ShapeList.filter_by`
.. |filter_by_position| replace:: :meth:`~topology.ShapeList.filter_by_position`

View file

@ -0,0 +1,50 @@
import os
from build123d import *
from ocp_vscode import *
working_path = os.path.dirname(os.path.abspath(__file__))
filedir = os.path.join(working_path, "..", "..", "assets", "selectors_operators")
with BuildPart() as part:
with BuildSketch() as s:
Rectangle(115, 50)
with Locations((5 / 2, 0)):
SlotOverall(90, 12, mode=Mode.SUBTRACT)
extrude(amount=15)
with BuildSketch(Plane.XZ.offset(50 / 2)) as s3:
with Locations((-115 / 2 + 26, 15)):
SlotOverall(42 + 2 * 26 + 12, 2 * 26, rotation=90)
zz = extrude(amount=-12)
split(bisect_by=Plane.XY)
edgs = part.part.edges().filter_by(Axis.Y).group_by(Axis.X)[-2]
fillet(edgs, 9)
with Locations(zz.faces().sort_by(Axis.Y)[0]):
with Locations((42 / 2 + 6, 0)):
CounterBoreHole(24 / 2, 34 / 2, 4)
mirror(about=Plane.XZ)
with BuildSketch() as s4:
RectangleRounded(115, 50, 6)
extrude(amount=80, mode=Mode.INTERSECT)
# fillet does not work right, mode intersect is safer
with BuildSketch(Plane.YZ) as s4:
with BuildLine() as bl:
l1 = Line((0, 0), (18 / 2, 0))
l2 = PolarLine(l1 @ 1, 8, 60, length_mode=LengthMode.VERTICAL)
l3 = Line(l2 @ 1, (0, 8))
mirror(about=Plane.YZ)
make_face()
extrude(amount=115 / 2, both=True, mode=Mode.SUBTRACT)
faces = part.faces().filter_by(
lambda f: all(e.geom_type == GeomType.CIRCLE for e in f.edges())
)
for i, f in enumerate(faces):
RigidJoint(f"bearing_bore_{i}", joint_location=f.center_location)
show(part, [f.translate(f.normal_at() * 0.01) for f in faces], render_joints=True)
save_screenshot(os.path.join(filedir, "filter_all_edges_circle.png"))

View file

@ -0,0 +1,47 @@
import os
from build123d import *
from ocp_vscode import *
working_path = os.path.dirname(os.path.abspath(__file__))
filedir = os.path.join(working_path, "..", "..", "assets", "selectors_operators")
axis = Axis.Z
plane = Plane.XY
with BuildPart() as part:
with BuildSketch(Plane.XY.shift_origin((1, 1))) as plane_rep:
Rectangle(2, 2)
with Locations((-.9, -.9)):
Text("Plane.XY", .2, align=(Align.MIN, Align.MIN), mode=Mode.SUBTRACT)
plane_rep = plane_rep.sketch
plane_rep.color = Color(0, .55, .55, .1)
with Locations((-1, -1, 0)):
b = Box(1, 1, 1)
f = b.faces()
res = f.filter_by(axis)
axis_rep = [Axis(f.center(), f.normal_at()) for f in res]
show_object([b, res, axis_rep])
with Locations((1, 1, 0)):
b = Box(1, 1, 1)
f = b.faces()
res = f.filter_by(plane)
show_object([b, res, plane_rep])
save_screenshot(os.path.join(filedir, "filter_axisplane.png"))
reset_show()
with Locations((-1, -1, 0)):
b = Box(1, 1, 1)
f = b.faces()
res = f.filter_by(lambda f: abs(f.normal_at().dot(axis.direction)) < 1e-6)
show_object([b, res, axis_rep])
with Locations((1, 1, 0)):
b = Box(1, 1, 1)
f = b.faces()
res = f.filter_by(lambda f: abs(f.normal_at().dot(plane.z_dir)) < 1e-6)
show_object([b, res, plane_rep])
save_screenshot(os.path.join(filedir, "filter_dot_axisplane.png"))

View file

@ -0,0 +1,23 @@
import os
from build123d import *
from ocp_vscode import *
working_path = os.path.dirname(os.path.abspath(__file__))
filedir = os.path.join(working_path, "..", "..", "assets", "selectors_operators")
with BuildPart() as part:
Box(5, 5, 1)
Cylinder(2, 5)
edges = part.edges().filter_by(lambda a: a.length == 1)
fillet(edges, 1)
part.edges().filter_by(GeomType.LINE)
part.faces().filter_by(GeomType.CYLINDER)
show(part, part.edges().filter_by(GeomType.LINE))
save_screenshot(os.path.join(filedir, "filter_geomtype_line.png"))
show(part, part.faces().filter_by(GeomType.CYLINDER))
save_screenshot(os.path.join(filedir, "filter_geomtype_cylinder.png"))

View file

@ -0,0 +1,38 @@
from copy import copy
import os
from build123d import *
from ocp_vscode import *
working_path = os.path.dirname(os.path.abspath(__file__))
filedir = os.path.join(working_path, "..", "..", "assets", "selectors_operators")
bracket = import_step(os.path.join(working_path, "nema-17-bracket.step"))
faces = bracket.faces()
motor_mounts = faces.filter_by(GeomType.CYLINDER).filter_by(lambda f: f.radius == 3.3/2)
for i, f in enumerate(motor_mounts):
location = f.axis_of_rotation.location
RigidJoint(f"motor_m3_{i}", bracket, joint_location=location)
motor_face = faces.filter_by(lambda f: len(f.inner_wires()) == 5).sort_by(Axis.X)[-1]
motor_bore = motor_face.inner_wires().edges().filter_by(lambda e: e.radius == 16).edge()
location = Location(motor_bore.arc_center, motor_bore.normal() * 90, Intrinsic.YXZ)
RigidJoint(f"motor", bracket, joint_location=location)
before_linear = copy(bracket)
mount_face = faces.filter_by(lambda f: len(f.inner_wires()) == 6).sort_by(Axis.Z)[-1]
mount_slots = mount_face.inner_wires().edges().filter_by(GeomType.CIRCLE)
joint_edges = [
Line(mount_slots[i].arc_center, mount_slots[i + 1].arc_center)
for i in range(0, len(mount_slots), 2)
]
for i, e in enumerate(joint_edges):
LinearJoint(f"mount_m4_{i}", bracket, axis=Axis(e), linear_range=(0, e.length / 2))
show(before_linear, render_joints=True)
save_screenshot(os.path.join(filedir, "filter_inner_wire_count.png"))
show(bracket, render_joints=True)
save_screenshot(os.path.join(filedir, "filter_inner_wire_count_linear.png"))

View file

@ -0,0 +1,39 @@
from copy import copy
import os
from build123d import *
from ocp_vscode import *
working_path = os.path.dirname(os.path.abspath(__file__))
filedir = os.path.join(working_path, "..", "..", "assets", "selectors_operators")
with BuildPart() as part:
Cylinder(15, 2, align=(Align.CENTER, Align.CENTER, Align.MIN))
with BuildSketch():
RectangleRounded(10, 10, 2.5)
extrude(amount=15)
with BuildSketch():
Circle(2.5)
Rectangle(4, 5, mode=Mode.INTERSECT)
extrude(amount=15, mode=Mode.SUBTRACT)
with GridLocations(20, 0, 2, 1):
Hole(3.5 / 2)
before = copy(part)
faces = part.faces().filter_by(
lambda f: len(f.inner_wires().edges().filter_by(GeomType.LINE)) == 2
)
wires = faces.wires().filter_by(
lambda w: any(e.geom_type == GeomType.LINE for e in w.edges())
)
chamfer(wires.edges(), 0.5)
location = Location((-25, -25))
b = before.part.moved(location)
f = [f.moved(location) for f in faces]
show(b, f, part)
save_screenshot(os.path.join(filedir, "filter_nested.png"))

View file

@ -0,0 +1,25 @@
import os
from build123d import *
from ocp_vscode import *
working_path = os.path.dirname(os.path.abspath(__file__))
filedir = os.path.join(working_path, "..", "..", "assets", "selectors_operators")
with BuildPart() as open_box_builder:
Box(20, 20, 5)
offset(amount=-2, openings=open_box_builder.faces().sort_by(Axis.Z)[-1])
inside_edges = open_box_builder.edges().filter_by(Edge.is_interior)
fillet(inside_edges, 1.5)
outside_edges = open_box_builder.edges().filter_by(Edge.is_interior, reverse=True)
fillet(outside_edges, 0.5)
open_box = open_box_builder.part
open_box.color = Color(0xEDAE49)
outside_fillets = Compound(open_box.faces().filter_by(Face.is_circular_convex))
outside_fillets.color = Color(0xD1495B)
inside_fillets = Compound(open_box.faces().filter_by(Face.is_circular_concave))
inside_fillets.color = Color(0x00798C)
show(open_box, inside_fillets, outside_fillets)
save_screenshot(os.path.join(filedir, "filter_shape_properties.png"))

View file

@ -0,0 +1,28 @@
import os
from copy import copy
from build123d import *
from ocp_vscode import *
working_path = os.path.dirname(os.path.abspath(__file__))
filedir = os.path.join(working_path, "..", "..", "assets", "selectors_operators")
with BuildPart() as fins:
with GridLocations(4, 6, 4, 4):
Box(2, 3, 10, align=(Align.CENTER, Align.CENTER, Align.MIN))
with BuildPart() as part:
Box(34, 48, 5, align=(Align.CENTER, Align.CENTER, Align.MAX))
with GridLocations(20, 27, 2, 2):
add(fins)
without = copy(part)
target = part.edges().group_by(Axis.Z)[-1].group_by(Edge.length)[-1]
fillet(target, .75)
show(without)
save_screenshot(os.path.join(filedir, "group_axis_without.png"))
show(part)
save_screenshot(os.path.join(filedir, "group_axis_with.png"))

View file

@ -0,0 +1,31 @@
from copy import copy
import os
from build123d import *
from ocp_vscode import *
working_path = os.path.dirname(os.path.abspath(__file__))
filedir = os.path.join(working_path, "..", "..", "assets", "selectors_operators")
with BuildPart() as part:
Cylinder(10, 30, rotation=(90, 0, 0))
Cylinder(8, 40, rotation=(90, 0, 0), align=(Align.CENTER, Align.CENTER, Align.MAX))
Cylinder(8, 23, rotation=(90, 0, 0), align=(Align.CENTER, Align.CENTER, Align.MIN))
Cylinder(5, 40, rotation=(90, 0, 0), align=(Align.CENTER, Align.CENTER, Align.MIN))
with BuildSketch(Plane.XY.offset(8)) as s:
SlotCenterPoint((0, 38), (0, 48), 5)
extrude(amount=2.5, both=True, mode=Mode.SUBTRACT)
before = copy(part)
faces = part.faces().group_by(
lambda f: Face(f.inner_wires()[0]).area if f.inner_wires() else 0
)
chamfer([f.outer_wire().edges() for f in faces[-1]], 0.5)
show(
before,
[f.translate(f.normal_at() * 0.01) for f in faces],
part.part.translate((40, 40)),
)
save_screenshot(os.path.join(filedir, "group_hole_area.png"))

View file

@ -0,0 +1,61 @@
import os
from copy import copy
from build123d import *
from ocp_vscode import *
working_path = os.path.dirname(os.path.abspath(__file__))
filedir = os.path.join(working_path, "..", "..", "assets", "selectors_operators")
with BuildPart() as part:
with BuildSketch(Plane.XZ) as sketch:
with BuildLine():
CenterArc((-6, 12), 10, 0, 360)
Line((-16, 0), (16, 0))
make_hull()
Rectangle(50, 5, align=(Align.CENTER, Align.MAX))
extrude(amount=12)
Box(38, 6, 22, align=(Align.CENTER, Align.MAX, Align.MIN), mode=Mode.SUBTRACT)
circle = part.edges().filter_by(GeomType.CIRCLE).sort_by(Axis.Y)[0]
with Locations(Plane(circle.arc_center, z_dir=circle.normal())):
CounterBoreHole(13 / 2, 16 / 2, 4)
mirror(about=Plane.XZ)
before_fillet = copy(part)
length_groups = part.edges().group_by(Edge.length)
fillet(length_groups.group(6) + length_groups.group(5), 4)
after_fillet = copy(part)
with BuildSketch() as pins:
with Locations((-21, 0)):
Circle(3 / 2)
with Locations((21, 0)):
SlotCenterToCenter(1, 3)
extrude(amount=-12, mode=Mode.SUBTRACT)
with GridLocations(42, 16, 2, 2):
CounterBoreHole(3.5 / 2, 3.5, 0)
after_holes = copy(part)
radius_groups = part.edges().filter_by(GeomType.CIRCLE).group_by(Edge.radius)
bearing_edges = radius_groups.group(8).group_by(SortBy.DISTANCE)[-1]
pin_edges = radius_groups.group(1.5).filter_by_position(Axis.Z, -5, -5)
chamfer([pin_edges, bearing_edges], .5)
location = Location((-20, -20))
items = [before_fillet.part] + length_groups.group(6) + length_groups.group(5)
before = Compound(items).move(location)
show(before, after_fillet.part.move(Location((20, 20))))
save_screenshot(os.path.join(filedir, "group_length_key.png"))
location = Location((-20, -20), (180, 0, 0))
after = Compound([after_holes.part] + pin_edges + bearing_edges).move(location)
show(after, part.part.move(Location((20, 20), (180, 0, 0))))
save_screenshot(os.path.join(filedir, "group_radius_key.png"))

View file

@ -0,0 +1,93 @@
from copy import copy
import os
from build123d import *
from ocp_vscode import *
working_path = os.path.dirname(os.path.abspath(__file__))
filedir = os.path.join(working_path, "..", "..", "assets", "selectors_operators")
selectors = [solids, vertices, edges, faces]
line = Line((-9, -9), (9, 9))
for i, selector in enumerate(selectors):
u = i / (len(selectors) - 1)
with BuildPart() as part:
with Locations(line @ u):
Box(5, 5, 1)
Cylinder(2, 5)
show_object([part, selector()])
save_screenshot(os.path.join(filedir, "selectors_select_all.png"))
reset_show()
for i, selector in enumerate(selectors[1:4]):
u = i / (len(selectors) - 1)
with BuildPart() as part:
with Locations(line @ u):
Box(5, 5, 1)
Cylinder(2, 5)
show_object([part, selector(Select.LAST)])
save_screenshot(os.path.join(filedir, "selectors_select_last.png"))
reset_show()
with BuildPart() as part:
with Locations(line @ 1/3):
Box(5, 5, 1)
Cylinder(2, 5)
edges = part.edges(Select.NEW)
part_copy = copy(part)
with Locations(line @ 2/3):
b = Box(5, 5, 1)
c = Cylinder(2, 5)
c.color = Color("DarkTurquoise")
show(part_copy, edges, b, c, alphas=[.5, 1, .5, 1])
save_screenshot(os.path.join(filedir, "selectors_select_new.png"))
reset_show()
with BuildPart() as part:
with Locations(line @ 1/3):
Box(5, 5, 1, align=(Align.CENTER, Align.CENTER, Align.MAX))
Cylinder(2, 2, align=(Align.CENTER, Align.CENTER, Align.MIN))
edges = part.edges(Select.NEW)
part_copy = copy(part)
with Locations(line @ 2/3):
b = Box(5, 5, 1, align=(Align.CENTER, Align.CENTER, Align.MAX), mode=Mode.PRIVATE)
c = Cylinder(2, 2, align=(Align.CENTER, Align.CENTER, Align.MIN), mode=Mode.PRIVATE)
c.color = Color("DarkTurquoise")
show(part_copy, edges, b, c, alphas=[.5, 1, .5, 1])
save_screenshot(os.path.join(filedir, "selectors_select_new_none.png"))
reset_show()
with BuildPart() as part:
with Locations(line @ 1/3):
Box(5, 5, 1)
Cylinder(2, 5)
edges = part.edges().filter_by(lambda a: a.length == 1)
fillet(edges, 1)
show_object([part, part.edges(Select.NEW)])
with BuildPart() as part:
with Locations(line @ 2/3):
Box(5, 5, 1)
Cylinder(2, 5)
edges = part.edges().filter_by(lambda a: a.length == 1)
fillet(edges, 1)
show_object([part, part.edges(Select.LAST)])
save_screenshot(os.path.join(filedir, "selectors_select_new_fillet.png"))
show(part, part.vertices().sort_by(Axis.X)[-4:])
save_screenshot(os.path.join(filedir, "operators_sort_x.png"))
show(part, part.faces().group_by(SortBy.AREA)[0].edges())
save_screenshot(os.path.join(filedir, "operators_group_area.png"))
faces = part.faces().filter_by(lambda f: f.normal_at() == Vector(0, 0, 1))
show(part, [f.translate(f.normal_at() * 0.01) for f in faces])
save_screenshot(os.path.join(filedir, "operators_filter_z_normal.png"))

View file

@ -0,0 +1,31 @@
import os
from build123d import *
from ocp_vscode import *
working_path = os.path.dirname(os.path.abspath(__file__))
filedir = os.path.join(working_path, "..", "..", "assets", "selectors_operators")
with BuildSketch() as along_wire:
Rectangle(48, 16, align=Align.MIN)
Rectangle(16, 48, align=Align.MIN)
Rectangle(32, 32, align=Align.MIN)
for i, v in enumerate(along_wire.vertices()):
fillet(v, i + 1)
show(along_wire)
save_screenshot(os.path.join(filedir, "sort_not_along_wire.png"))
with BuildSketch() as along_wire:
Rectangle(48, 16, align=Align.MIN)
Rectangle(16, 48, align=Align.MIN)
Rectangle(32, 32, align=Align.MIN)
sorted_verts = along_wire.vertices().sort_by(along_wire.wire())
for i, v in enumerate(sorted_verts):
fillet(v, i + 1)
show(along_wire)
save_screenshot(os.path.join(filedir, "sort_along_wire.png"))

View file

@ -0,0 +1,28 @@
from copy import copy
import os
from build123d import *
from ocp_vscode import *
working_path = os.path.dirname(os.path.abspath(__file__))
filedir = os.path.join(working_path, "..", "..", "assets", "selectors_operators")
with BuildPart() as part:
with BuildSketch(Plane.YZ) as profile:
with BuildLine():
l1 = FilletPolyline((16, 0), (32, 0), (32, 25), radius=12)
l2 = FilletPolyline((16, 4), (28, 4), (28, 15), radius=8)
Line(l1 @ 0, l2 @ 0)
Polyline(l1 @ 1, l1 @ 1 - Vector(2, 0), l2 @ 1 + Vector(2, 0), l2 @ 1)
make_face()
extrude(amount=34)
before = copy(part).part
face = part.faces().sort_by(Axis.X)[-1]
edge = face.edges().sort_by(Axis.Y)[0]
revolve(face, -Axis(edge), 90)
f = face.translate(face.normal_at() * 0.01)
show(before, f, edge, part.part.translate((25, 33)))
save_screenshot(os.path.join(filedir, "sort_axis.png"))

View file

@ -0,0 +1,21 @@
import os
from itertools import product
from build123d import *
from ocp_vscode import *
working_path = os.path.dirname(os.path.abspath(__file__))
filedir = os.path.join(working_path, "..", "..", "assets", "selectors_operators")
boxes = ShapeList(
Box(1, 1, 1).scale(0.75 if (i, j) == (1, 2) else 0.25).translate((i, j, 0))
for i, j in product(range(-3, 4), repeat=2)
)
boxes = boxes.sort_by_distance(Vertex())
show(*boxes, colors=ColorMap.listed(len(boxes)))
save_screenshot(os.path.join(filedir, "sort_distance_from_origin.png"))
boxes = boxes.sort_by_distance(boxes.sort_by(Solid.volume).last)
show(*boxes, colors=ColorMap.listed(len(boxes)))
save_screenshot(os.path.join(filedir, "sort_distance_from_largest.png"))

View file

@ -0,0 +1,45 @@
import os
from build123d import *
from ocp_vscode import *
working_path = os.path.dirname(os.path.abspath(__file__))
filedir = os.path.join(working_path, "..", "..", "assets", "selectors_operators")
with BuildPart() as part:
Box(5, 5, 1)
Cylinder(2, 5)
edges = part.edges().filter_by(lambda a: a.length == 1)
fillet(edges, 1)
box = Box(5, 5, 5).move(Location((-6, -6)))
sphere = Sphere(5 / 2).move(Location((6, 6)))
solids = ShapeList([part.part, box, sphere])
part.wires().sort_by(SortBy.LENGTH)[:4]
part.wires().sort_by(Wire.length)[:4]
part.wires().group_by(SortBy.LENGTH)[0]
part.vertices().sort_by(SortBy.DISTANCE)[-2:]
part.vertices().sort_by_distance(Vertex())[-2:]
part.vertices().group_by(Vertex().distance)[-1]
show(part, part.wires().sort_by(SortBy.LENGTH)[:4])
save_screenshot(os.path.join(filedir, "sort_sortby_length.png"))
# show(part, part.faces().sort_by(SortBy.AREA)[-2:])
# save_screenshot(os.path.join(filedir, "sort_sortby_area.png"))
# solid = solids.sort_by(SortBy.VOLUME)[-1]
# solid.color = "violet"
# show([part, box, sphere], solid)
# save_screenshot(os.path.join(filedir, "sort_sortby_volume.png"))
# show(part, part.edges().filter_by(GeomType.CIRCLE).sort_by(SortBy.RADIUS)[-4:])
# save_screenshot(os.path.join(filedir, "sort_sortby_radius.png"))
show(part, part.vertices().sort_by(SortBy.DISTANCE)[-2:])
save_screenshot(os.path.join(filedir, "sort_sortby_distance.png"))

View file

@ -0,0 +1,195 @@
##################
Filter Examples
##################
.. _filter_geomtype:
GeomType
=============
:class:`~build_enums.GeomType` enums are shape type shorthands for ``Edge`` and ``Face``
objects. They are most helpful for filtering objects of that specific type for further
operations, and are sometimes necessary e.g. before sorting or filtering by radius.
``Edge`` and ``Face`` each support a subset of ``GeomType``:
* ``Edge`` can be type ``LINE``, ``CIRCLE``, ``ELLIPSE``, ``HYPERBOLA``, ``PARABOLA``, ``BEZIER``, ``BSPLINE``, ``OFFSET``, ``OTHER``
* ``Face`` can be type ``PLANE``, ``CYLINDER``, ``CONE``, ``SPHERE``, ``TORUS``, ``BEZIER``, ``BSPLINE``, ``REVOLUTION``, ``EXTRUSION``, ``OFFSET``, ``OTHER``
.. dropdown:: Setup
.. literalinclude:: examples/filter_geomtype.py
:language: python
:lines: 3, 8-13
.. literalinclude:: examples/filter_geomtype.py
:language: python
:lines: 15
.. figure:: ../assets/selectors_operators/filter_geomtype_line.png
:align: center
|
.. literalinclude:: examples/filter_geomtype.py
:language: python
:lines: 17
.. figure:: ../assets/selectors_operators/filter_geomtype_cylinder.png
:align: center
|
.. _filter_all_edges_circle:
All Edges Circle
========================
In this complete bearing block, we want to add joints for the bearings. These should be
located in the counterbore recess. One way to locate the joints is by finding faces with
centers located where the joints need to be located. Filtering for faces with only
circular edges selects the counterbore faces that meet the joint criteria.
.. dropdown:: Setup
.. literalinclude:: examples/filter_all_edges_circle.py
:language: python
:lines: 3, 8-41
.. literalinclude:: examples/filter_all_edges_circle.py
:language: python
:lines: 43-47
.. figure:: ../assets/selectors_operators/filter_all_edges_circle.png
:align: center
|
.. _filter_axis_plane:
Axis and Plane
=================
Filtering by an Axis will select faces perpendicular to the axis. Likewise filtering by
Plane will select faces parallel to the plane.
.. dropdown:: Setup
.. code-block:: python
from build123d import *
with BuildPart() as part:
Box(1, 1, 1)
.. code-block:: python
part.faces().filter_by(Axis.Z)
part.faces().filter_by(Plane.XY)
.. figure:: ../assets/selectors_operators/filter_axisplane.png
:align: center
|
It might be useful to filter by an Axis or Plane in other ways. A lambda can be used to
accomplish this with feature properties or methods. Here, we are looking for faces where
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
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)
.. figure:: ../assets/selectors_operators/filter_dot_axisplane.png
:align: center
|
.. _filter_inner_wire_count:
Inner Wire Count
========================
This motor bracket imported from a step file needs joints for adding to an assembly.
Joints for the M3 clearance holes were already found by using the cylindrical face's
axis of rotation, but the motor bore and slots need specific placement. The motor bore
can be found by filtering for faces with 5 inner wires, sorting for the desired face,
and then filtering for the specific inner wire by radius.
- bracket STEP model: :download:`nema-17-bracket.step <examples/nema-17-bracket.step>`
.. dropdown:: Setup
.. literalinclude:: examples/filter_inner_wire_count.py
:language: python
:lines: 4, 9-16
.. literalinclude:: examples/filter_inner_wire_count.py
:language: python
:lines: 18-21
.. figure:: ../assets/selectors_operators/filter_inner_wire_count.png
:align: center
|
Linear joints for the slots are appropriate for mating flexibility, but require more
than a single location. The slot arc centers can be used for creating a linear joint
axis and range. To do that we can filter for faces with 6 inner wires, sort for and
select the top face, and then filter for the circular edges of the inner wires.
.. literalinclude:: examples/filter_inner_wire_count.py
:language: python
:lines: 25-32
.. figure:: ../assets/selectors_operators/filter_inner_wire_count_linear.png
:align: center
|
.. _filter_nested:
Nested Filters
========================
Filters can be nested to specify features by characteristics other than their own, like
child properties. Here we want to chamfer the mating edges of the D bore and square
shaft. A way to do this is first looking for faces with only 2 line edges among the
inner wires. The nested filter captures the straight edges, while the parent filter
selects faces based on the count. Then, from those faces, we filter for the wires with
any line edges.
.. dropdown:: Setup
.. literalinclude:: examples/filter_nested.py
:language: python
:lines: 4, 9-22
.. literalinclude:: examples/filter_nested.py
:language: python
:lines: 26-32
.. figure:: ../assets/selectors_operators/filter_nested.png
:align: center
|
.. _filter_shape_properties:
Shape Properties
========================
Selected features can be quickly filtered by feature properties. First, we filter by
interior and exterior edges using the ``Edge`` ``is interior`` property to apply
different fillets accordingly. Then the ``Face`` ``is_circular_*`` properties are used
to highlight the resulting fillets.
.. literalinclude:: examples/filter_shape_properties.py
:language: python
:lines: 3-4, 8-22
.. figure:: ../assets/selectors_operators/filter_shape_properties.png
:align: center
|

View file

@ -0,0 +1,116 @@
#################
Group Examples
#################
.. _group_axis:
Axis and Length
==================
This heatsink component could use fillets on the ends of the fins on the long ends. One
way to accomplish this is to filter by length, sort by axis, and slice the
result knowing how many edges to expect.
.. dropdown:: Setup
.. literalinclude:: examples/group_axis.py
:language: python
:lines: 4, 9-17
.. figure:: ../assets/selectors_operators/group_axis_without.png
:align: center
|
However, ``group_by`` can be used to first group all the edges by z-axis position and then
group again by length. In both cases, you can select the desired edges from the last group.
.. literalinclude:: examples/group_axis.py
:language: python
:lines: 21-22
.. figure:: ../assets/selectors_operators/group_axis_with.png
:align: center
|
.. _group_hole_area:
Hole Area
==================
Callables are available to ``group_by``, like ``sort_by``. Here, the first inner wire
is converted to a face and then that area is the grouping criteria to find the faces
with the largest hole.
.. dropdown:: Setup
.. literalinclude:: examples/group_hole_area.py
:language: python
:lines: 4, 9-17
.. literalinclude:: examples/group_hole_area.py
:language: python
:lines: 21-24
.. figure:: ../assets/selectors_operators/group_hole_area.png
:align: center
|
.. _group_properties_with_keys:
Properties with Keys
====================
Groups are usually selected by list slice, often smallest ``[0]`` or largest ``[-1]``,
but they can also be selected by key with the ``group`` method if the keys are known.
Starting with an incomplete bearing block we are looking to add fillets to the ribs
and corners. We know the edge lengths so the edges can be grouped by ``Edge.Length`` and
then the desired groups are selected with the ``group`` method using the lengths as keys.
.. dropdown:: Setup
.. literalinclude:: examples/group_properties_with_keys.py
:language: python
:lines: 4, 9-26
.. literalinclude:: examples/group_properties_with_keys.py
:language: python
:lines: 30, 31
.. figure:: ../assets/selectors_operators/group_length_key.png
:align: center
|
Next, we add alignment pin and counterbore holes after the fillets to make sure
screw heads sit flush where they overlap the fillet. Once that is done, it's time to
finalize the tight-tolerance bearing and pin holes with chamfers to make installation
easier. We can filter by ``GeomType.CIRCLE`` and group by ``Edge.radius`` to group the
circular edges. Again, the radii are known, so we can retrieve those groups directly
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
:lines: 35-43
.. literalinclude:: examples/group_properties_with_keys.py
:language: python
:lines: 47-50
.. figure:: ../assets/selectors_operators/group_radius_key.png
:align: center
|
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
radius_groups = part.edges().filter_by(GeomType.CIRCLE)
bearing_edges = radius_groups.filter_by(lambda e: e.radius == 8)
pin_edges = radius_groups.filter_by(lambda e: e.radius == 1.5)

View file

@ -0,0 +1,144 @@
################
Sort Examples
################
.. _sort_sortby:
SortBy
=============
:class:`~build_enums.SortBy` enums are shape property shorthands which work across
``Shape`` multiple object types. ``SortBy`` is a criteria for both ``sort_by`` and
``group_by``.
* ``SortBy.LENGTH`` works with ``Edge``, ``Wire``
* ``SortBy.AREA`` works with ``Face``, ``Solid``
* ``SortBy.VOLUME`` works with ``Solid``
* ``SortBy.RADIUS`` works with ``Edge``, ``Face`` with :class:`~build_enums.GeomType` ``CIRCLE``, ``CYLINDER``, ``SPHERE``
* ``SortBy.DISTANCE`` works ``Vertex``, ``Edge``, ``Wire``, ``Face``, ``Solid``
``SortBy`` is often interchangeable with specific shape properties and can alternatively
be used with``group_by``.
.. dropdown:: Setup
.. literalinclude:: examples/sort_sortby.py
:language: python
:lines: 3, 8-13
.. literalinclude:: examples/sort_sortby.py
:language: python
:lines: 19-22
.. figure:: ../assets/selectors_operators/sort_sortby_length.png
:align: center
|
.. literalinclude:: examples/sort_sortby.py
:language: python
:lines: 24-27
.. figure:: ../assets/selectors_operators/sort_sortby_distance.png
:align: center
|
.. _sort_along_wire:
Along Wire
=============
Vertices selected from an edge or wire might have a useful ordering when created from
a single object, but when created from multiple objects, the ordering not useful. For
example, when applying incrementing fillet radii to a list of vertices from the face,
the order is random.
.. dropdown:: Setup
.. literalinclude:: examples/sort_along_wire.py
:language: python
:lines: 3, 8-12
.. literalinclude:: examples/sort_along_wire.py
:language: python
:lines: 14-15
.. figure:: ../assets/selectors_operators/sort_not_along_wire.png
:align: center
|
Vertices may be sorted along the wire they fall on to create order. Notice the fillet
radii now increase in order.
.. literalinclude:: examples/sort_along_wire.py
:language: python
:lines: 26-28
.. figure:: ../assets/selectors_operators/sort_along_wire.png
:align: center
|
.. _sort_axis:
Axis
=========================
Sorting by axis is often the most straightforward way to optimize selections. In this
part we want to revolve the face at the end around an inside edge of the completed
extrusion. First, the face to extrude can be found by sorting along x-axis and the revolution
edge can be found sorting along y-axis.
.. dropdown:: Setup
.. literalinclude:: examples/sort_axis.py
:language: python
:lines: 4, 9-18
.. literalinclude:: examples/sort_axis.py
:language: python
:lines: 22-24
.. figure:: ../assets/selectors_operators/sort_axis.png
:align: center
|
.. _sort_distance_from:
Distance From
=========================
A ``sort_by_distance`` can be used to sort objects by their distance from another object.
Here we are sorting the boxes by distance from the origin, using an empty ``Vertex``
(at the origin) as the reference shape to find distance to.
.. dropdown:: Setup
.. literalinclude:: examples/sort_distance_from.py
:language: python
:lines: 2-5, 9-13
.. literalinclude:: examples/sort_distance_from.py
:language: python
:lines: 15-16
.. figure:: ../assets/selectors_operators/sort_distance_from_origin.png
:align: center
|
The example can be extended by first sorting the boxes by volume using the ``Solid``
property ``volume``, and getting the last (largest) box. Then, the boxes sorted by
their distance from the largest box.
.. literalinclude:: examples/sort_distance_from.py
:language: python
:lines: 19-20
.. figure:: ../assets/selectors_operators/sort_distance_from_largest.png
:align: center
|