From 0a4ec69f7f1089010f53f0bf44266ae7c62ca2f9 Mon Sep 17 00:00:00 2001 From: adam-urbanczyk Date: Thu, 13 Jul 2023 18:14:18 +0200 Subject: [PATCH 01/22] Ordered wire iteration --- cadquery/occ_impl/exporters/dxf.py | 26 ++++++++++++++++++++++---- cadquery/occ_impl/shapes.py | 26 +++++++++++++++++++------- 2 files changed, 41 insertions(+), 11 deletions(-) diff --git a/cadquery/occ_impl/exporters/dxf.py b/cadquery/occ_impl/exporters/dxf.py index 418737433..1a6b27548 100644 --- a/cadquery/occ_impl/exporters/dxf.py +++ b/cadquery/occ_impl/exporters/dxf.py @@ -11,7 +11,7 @@ from ...cq import Face, Plane, Workplane from ...units import RAD2DEG -from ..shapes import Edge +from ..shapes import Edge, Shape, Compound from .utils import toCompound ApproxOptions = Literal["spline", "arc"] @@ -139,7 +139,8 @@ def add_shape(self, workplane: Workplane, layer: str = "") -> Self: if self.approx == "spline": edges = [ - e.toSplines() if e.geomType() == "BSPLINE" else e for e in shape.Edges() + e.toSplines() if e.geomType() == "BSPLINE" else e + for e in self._ordered_edges(shape) ] elif self.approx == "arc": @@ -147,10 +148,12 @@ def add_shape(self, workplane: Workplane, layer: str = "") -> Self: # this is needed to handle free wires for el in shape.Wires(): - edges.extend(Face.makeFromWires(el).toArcs(self.tolerance).Edges()) + edges.extend( + self._ordered_edges(Face.makeFromWires(el).toArcs(self.tolerance)) + ) else: - edges = shape.Edges() + edges = self._ordered_edges(shape) for edge in edges: converter = self._DISPATCH_MAP.get(edge.geomType(), None) @@ -172,6 +175,21 @@ def add_shape(self, workplane: Workplane, layer: str = "") -> Self: return self + @staticmethod + def _ordered_edges(s: Shape) -> List[Edge]: + + rv: List[Edge] = [] + + # iterato over wires and then edges + for w in s.Wires(): + rv.extend(w) + + # add free edges + if isinstance(s, Compound): + rv.extend(e for e in s if isinstance(e, Edge)) + + return rv + @staticmethod def _dxf_line(edge: Edge) -> DxfEntityAttributes: """Convert a Line to DXF entity attributes. diff --git a/cadquery/occ_impl/shapes.py b/cadquery/occ_impl/shapes.py index a3d4a8e9d..e1ebaa997 100644 --- a/cadquery/occ_impl/shapes.py +++ b/cadquery/occ_impl/shapes.py @@ -173,7 +173,7 @@ from OCP.ShapeUpgrade import ShapeUpgrade_UnifySameDomain -from OCP.BRepTools import BRepTools +from OCP.BRepTools import BRepTools, BRepTools_WireExplorer from OCP.LocOpe import LocOpe_DPrism @@ -545,9 +545,9 @@ def geomType(self) -> Geoms: The return values depend on the type of the shape: | Vertex: always 'Vertex' - | Edge: LINE, CIRCLE, ELLIPSE, HYPERBOLA, PARABOLA, BEZIER, + | Edge: LINE, CIRCLE, ELLIPSE, HYPERBOLA, PARABOLA, BEZIER, | BSPLINE, OFFSET, OTHER - | Face: PLANE, CYLINDER, CONE, SPHERE, TORUS, BEZIER, BSPLINE, + | Face: PLANE, CYLINDER, CONE, SPHERE, TORUS, BEZIER, BSPLINE, | REVOLUTION, EXTRUSION, OFFSET, OTHER | Solid: 'Solid' | Shell: 'Shell' @@ -1056,7 +1056,7 @@ def _bool_op( def cut(self, *toCut: "Shape", tol: Optional[float] = None) -> "Shape": """ Remove the positional arguments from this Shape. - + :param tol: Fuzzy mode tolerance """ @@ -1091,7 +1091,7 @@ def fuse( def intersect(self, *toIntersect: "Shape", tol: Optional[float] = None) -> "Shape": """ Intersection of the positional arguments and this Shape. - + :param tol: Fuzzy mode tolerance """ @@ -2252,6 +2252,18 @@ def chamfer2D(self, d: float, vertices: Iterable[Vertex]) -> "Wire": return f.chamfer2D(d, vertices).outerWire() + def __iter__(self) -> Iterator[Edge]: + """ + Iterate over subshapes. + + """ + + exp = BRepTools_WireExplorer(self.wrapped) + + while exp.Current(): + yield Edge(exp.Current()) + exp.Next() + class Face(Shape): """ @@ -3595,7 +3607,7 @@ def __bool__(self) -> bool: def cut(self, *toCut: "Shape", tol: Optional[float] = None) -> "Compound": """ Remove the positional arguments from this Shape. - + :param tol: Fuzzy mode tolerance """ @@ -3636,7 +3648,7 @@ def intersect( ) -> "Compound": """ Intersection of the positional arguments and this Shape. - + :param tol: Fuzzy mode tolerance """ From 880bfde455909dd8fac3016fb2cd6735ce9df6b5 Mon Sep 17 00:00:00 2001 From: adam-urbanczyk Date: Fri, 14 Jul 2023 18:40:26 +0200 Subject: [PATCH 02/22] Update docstring --- cadquery/occ_impl/shapes.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cadquery/occ_impl/shapes.py b/cadquery/occ_impl/shapes.py index e1ebaa997..ed99ad243 100644 --- a/cadquery/occ_impl/shapes.py +++ b/cadquery/occ_impl/shapes.py @@ -2254,7 +2254,7 @@ def chamfer2D(self, d: float, vertices: Iterable[Vertex]) -> "Wire": def __iter__(self) -> Iterator[Edge]: """ - Iterate over subshapes. + Iterate over edges in an ordered way. """ From dcb677a021b0ddf392ab7577f37789d33ed09e12 Mon Sep 17 00:00:00 2001 From: adam-urbanczyk Date: Fri, 14 Jul 2023 19:20:13 +0200 Subject: [PATCH 03/22] Move __iter__ to Shape --- cadquery/occ_impl/shapes.py | 24 ++++++++++++------------ cadquery/sketch.py | 2 +- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/cadquery/occ_impl/shapes.py b/cadquery/occ_impl/shapes.py index ed99ad243..20cd1b644 100644 --- a/cadquery/occ_impl/shapes.py +++ b/cadquery/occ_impl/shapes.py @@ -1353,6 +1353,18 @@ def _repr_javascript_(self): return display(self)._repr_javascript_() + def __iter__(self) -> Iterator["Shape"]: + """ + Iterate over subshapes. + + """ + + it = TopoDS_Iterator(self.wrapped) + + while it.More(): + yield Shape.cast(it.Value()) + it.Next() + class ShapeProtocol(Protocol): @property @@ -3585,18 +3597,6 @@ def makeText( return rv - def __iter__(self) -> Iterator[Shape]: - """ - Iterate over subshapes. - - """ - - it = TopoDS_Iterator(self.wrapped) - - while it.More(): - yield Shape.cast(it.Value()) - it.Next() - def __bool__(self) -> bool: """ Check if empty. diff --git a/cadquery/sketch.py b/cadquery/sketch.py index adc874a06..1b0bd2888 100644 --- a/cadquery/sketch.py +++ b/cadquery/sketch.py @@ -154,7 +154,7 @@ def face( res = Face.makeFromWires(b) elif isinstance(b, (Sketch, Compound)): res = b - elif isinstance(b, Iterable): + elif isinstance(b, Iterable) and not isinstance(b, Shape): wires = edgesToWires(tcast(Iterable[Edge], b)) res = Face.makeFromWires(*(wires[0], wires[1:])) else: From b25c50d4d4183ad6f58fb378f8cb499b345bfae6 Mon Sep 17 00:00:00 2001 From: adam-urbanczyk Date: Tue, 18 Jul 2023 08:13:19 +0200 Subject: [PATCH 04/22] Additional iterators --- cadquery/occ_impl/shapes.py | 42 ++++++++++++++++++++++++++++++++++++- 1 file changed, 41 insertions(+), 1 deletion(-) diff --git a/cadquery/occ_impl/shapes.py b/cadquery/occ_impl/shapes.py index 20cd1b644..c33b81c88 100644 --- a/cadquery/occ_impl/shapes.py +++ b/cadquery/occ_impl/shapes.py @@ -93,7 +93,7 @@ ) from OCP.BRepIntCurveSurface import BRepIntCurveSurface_Inter -from OCP.TopExp import TopExp_Explorer # Topology explorer +from OCP.TopExp import TopExp_Explorer, TopExp # Topology explorer # used for getting underlying geometry -- is this equivalent to brep adaptor? from OCP.BRep import BRep_Tool, BRep_Builder @@ -301,6 +301,14 @@ ta.TopAbs_COMPOUND: "Compound", } +ancestors_LUT = { + "Vertex": ta.TopAbs_EDGE, + "Edge": ta.TopAbs_WIRE, + "Wire": ta.TopAbs_FACE, + "Face": ta.TopAbs_SHELL, + "Shell": ta.TopAbs_SOLID, +} + geom_LUT_FACE = { ga.GeomAbs_Plane: "PLANE", ga.GeomAbs_Cylinder: "CYLINDER", @@ -330,6 +338,7 @@ Shapes = Literal[ "Vertex", "Edge", "Wire", "Face", "Shell", "Solid", "CompSolid", "Compound" ] + Geoms = Literal[ "Vertex", "Wire", @@ -1365,6 +1374,37 @@ def __iter__(self) -> Iterator["Shape"]: yield Shape.cast(it.Value()) it.Next() + def ancestors(self, shape: "Shape", kind: Shapes) -> Iterator["Shape"]: + """ + Iterate over ancestors, i.e. shapes of kind within self that contain shape. + + """ + + shape_map = TopTools_IndexedDataMapOfShapeListOfShape() + + TopExp.MapShapesAndAncestors_s( + self.wrapped, shapetype(shape.wrapped), inverse_shape_LUT[kind], shape_map + ) + + for s in shape_map.FindFromKey(shape.wrapped): + yield Shape.cast(s) + + def siblings(self, shape: "Shape", kind: Shapes) -> Iterator["Shape"]: + """ + Iterate over siblings, i.e. shapes within self that share subshapes of kind with shape. + + """ + + shape_map = TopTools_IndexedDataMapOfShapeListOfShape() + + TopExp.MapShapesAndAncestors_s( + self.wrapped, inverse_shape_LUT[kind], shapetype(shape.wrapped), shape_map + ) + + for child in shape._entities(kind): + for s in shape_map.FindFromKey(child): + yield Shape.cast(s) + class ShapeProtocol(Protocol): @property From fce60a7291c118d70436b54b496bcff63d421e12 Mon Sep 17 00:00:00 2001 From: adam-urbanczyk Date: Fri, 21 Jul 2023 08:21:31 +0200 Subject: [PATCH 05/22] Exclude the argument --- cadquery/occ_impl/shapes.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cadquery/occ_impl/shapes.py b/cadquery/occ_impl/shapes.py index c33b81c88..b012afee3 100644 --- a/cadquery/occ_impl/shapes.py +++ b/cadquery/occ_impl/shapes.py @@ -1403,7 +1403,8 @@ def siblings(self, shape: "Shape", kind: Shapes) -> Iterator["Shape"]: for child in shape._entities(kind): for s in shape_map.FindFromKey(child): - yield Shape.cast(s) + if not shape.wrapped.IsSame(s): + yield Shape.cast(s) class ShapeProtocol(Protocol): From bfd7d81b601ecc8345fd8dc5947bdb0d8e88dfa4 Mon Sep 17 00:00:00 2001 From: adam-urbanczyk Date: Fri, 21 Jul 2023 08:24:31 +0200 Subject: [PATCH 06/22] Test for iterators --- tests/test_cadquery.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/tests/test_cadquery.py b/tests/test_cadquery.py index 55424f9ec..672d6feef 100644 --- a/tests/test_cadquery.py +++ b/tests/test_cadquery.py @@ -5546,3 +5546,27 @@ def test_toVtk(self): assert isinstance(vtk, vtkPolyData) assert vtk.GetNumberOfPolys() == 2 + + def test_iterators(self): + + s = Workplane().box(1, 1, 1).val() + + # check ancestors + res1 = list(s.ancestors(s.Edges()[0], "Face")) + assert len(res1) == 2 + + # check siblings + res2 = list(s.siblings(s.Faces()[0], "Edge")) + assert len(res2) == 4 + + # check regular iterator + res3 = list(s) + assert len(res3) == 1 + assert isinstance(res3[0], Shell) + + # check ordered iteration for wires + w = Workplane().polygon(5, 1).val() + edges = list(w) + + for e1, e2 in zip(edges, edges[1:]): + assert (e2.startPoint() - e1.endPoint()).Length == approx(0.0) From 9915956124925b1b52ec5a94fd0578a19cfd8e63 Mon Sep 17 00:00:00 2001 From: adam-urbanczyk Date: Fri, 21 Jul 2023 18:22:51 +0200 Subject: [PATCH 07/22] Change semantics - ctx shape from the arg --- cadquery/occ_impl/shapes.py | 14 +++++++------- tests/test_cadquery.py | 4 ++-- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/cadquery/occ_impl/shapes.py b/cadquery/occ_impl/shapes.py index b012afee3..ddbc6e47e 100644 --- a/cadquery/occ_impl/shapes.py +++ b/cadquery/occ_impl/shapes.py @@ -1376,34 +1376,34 @@ def __iter__(self) -> Iterator["Shape"]: def ancestors(self, shape: "Shape", kind: Shapes) -> Iterator["Shape"]: """ - Iterate over ancestors, i.e. shapes of kind within self that contain shape. + Iterate over ancestors, i.e. shapes of same kind within shape that contain self. """ shape_map = TopTools_IndexedDataMapOfShapeListOfShape() TopExp.MapShapesAndAncestors_s( - self.wrapped, shapetype(shape.wrapped), inverse_shape_LUT[kind], shape_map + shape.wrapped, shapetype(self.wrapped), inverse_shape_LUT[kind], shape_map ) - for s in shape_map.FindFromKey(shape.wrapped): + for s in shape_map.FindFromKey(self.wrapped): yield Shape.cast(s) def siblings(self, shape: "Shape", kind: Shapes) -> Iterator["Shape"]: """ - Iterate over siblings, i.e. shapes within self that share subshapes of kind with shape. + Iterate over siblings, i.e. shapes within shape that share subshapes of kind with self. """ shape_map = TopTools_IndexedDataMapOfShapeListOfShape() TopExp.MapShapesAndAncestors_s( - self.wrapped, inverse_shape_LUT[kind], shapetype(shape.wrapped), shape_map + shape.wrapped, inverse_shape_LUT[kind], shapetype(self.wrapped), shape_map ) - for child in shape._entities(kind): + for child in self._entities(kind): for s in shape_map.FindFromKey(child): - if not shape.wrapped.IsSame(s): + if not self.wrapped.IsSame(s): yield Shape.cast(s) diff --git a/tests/test_cadquery.py b/tests/test_cadquery.py index 672d6feef..3eb9f77ca 100644 --- a/tests/test_cadquery.py +++ b/tests/test_cadquery.py @@ -5552,11 +5552,11 @@ def test_iterators(self): s = Workplane().box(1, 1, 1).val() # check ancestors - res1 = list(s.ancestors(s.Edges()[0], "Face")) + res1 = list(s.Edges()[0].ancestors(s, "Face")) assert len(res1) == 2 # check siblings - res2 = list(s.siblings(s.Faces()[0], "Edge")) + res2 = list(s.Faces()[0].siblings(s, "Edge")) assert len(res2) == 4 # check regular iterator From 813b4803da58a9169b7bf1f5b89614e3cb6da876 Mon Sep 17 00:00:00 2001 From: adam-urbanczyk Date: Sun, 23 Jul 2023 10:10:06 +0200 Subject: [PATCH 08/22] Return Compound and use special implementation --- cadquery/occ_impl/shapes.py | 58 ++++++++++++++++++++++++++++++++----- 1 file changed, 50 insertions(+), 8 deletions(-) diff --git a/cadquery/occ_impl/shapes.py b/cadquery/occ_impl/shapes.py index ddbc6e47e..488c1c8eb 100644 --- a/cadquery/occ_impl/shapes.py +++ b/cadquery/occ_impl/shapes.py @@ -1374,7 +1374,7 @@ def __iter__(self) -> Iterator["Shape"]: yield Shape.cast(it.Value()) it.Next() - def ancestors(self, shape: "Shape", kind: Shapes) -> Iterator["Shape"]: + def ancestors(self, shape: "Shape", kind: Shapes) -> "Compound": """ Iterate over ancestors, i.e. shapes of same kind within shape that contain self. @@ -1386,10 +1386,11 @@ def ancestors(self, shape: "Shape", kind: Shapes) -> Iterator["Shape"]: shape.wrapped, shapetype(self.wrapped), inverse_shape_LUT[kind], shape_map ) - for s in shape_map.FindFromKey(self.wrapped): - yield Shape.cast(s) + return Compound.makeCompound( + Shape.cast(s) for s in shape_map.FindFromKey(self.wrapped) + ) - def siblings(self, shape: "Shape", kind: Shapes) -> Iterator["Shape"]: + def siblings(self, shape: "Shape", kind: Shapes) -> "Compound": """ Iterate over siblings, i.e. shapes within shape that share subshapes of kind with self. @@ -1401,10 +1402,12 @@ def siblings(self, shape: "Shape", kind: Shapes) -> Iterator["Shape"]: shape.wrapped, inverse_shape_LUT[kind], shapetype(self.wrapped), shape_map ) - for child in self._entities(kind): - for s in shape_map.FindFromKey(child): - if not self.wrapped.IsSame(s): - yield Shape.cast(s) + return Compound.makeCompound( + Shape.cast(s) + for child in self._entities(kind) + for s in shape_map.FindFromKey(child) + if not self.wrapped.IsSame(s) + ) class ShapeProtocol(Protocol): @@ -3700,6 +3703,45 @@ def intersect( return tcast(Compound, self._bool_op(self, toIntersect, intersect_op)) + def ancestors(self, shape: "Shape", kind: Shapes) -> "Compound": + """ + Iterate over ancestors, i.e. shapes of same kind within shape that contain elements of self. + + """ + + shape_map = TopTools_IndexedDataMapOfShapeListOfShape() + + for ch in self: + TopExp.MapShapesAndAncestors_s( + shape.wrapped, shapetype(ch.wrapped), inverse_shape_LUT[kind], shape_map + ) + + return Compound.makeCompound( + Shape.cast(s) for s in shape_map.FindFromKey(self.wrapped) + ) + + def siblings(self, shape: "Shape", kind: Shapes) -> "Compound": + """ + Iterate over siblings, i.e. shapes within shape that share subshapes of kind with the elements of self. + + """ + + shape_map = TopTools_IndexedDataMapOfShapeListOfShape() + + items = set() + for ch in self: + TopExp.MapShapesAndAncestors_s( + shape.wrapped, inverse_shape_LUT[kind], shapetype(ch.wrapped), shape_map + ) + items.add(ch.wrapped) + + return Compound.makeCompound( + Shape.cast(s) + for child in self._entities(kind) + for s in shape_map.FindFromKey(child) + if not s.wrapped in items + ) + def sortWiresByBuildOrder(wireList: List[Wire]) -> List[List[Wire]]: """Tries to determine how wires should be combined into faces. From 22910b1bbd5fdbaf86ccc59ca9430c7fa90c4859 Mon Sep 17 00:00:00 2001 From: adam-urbanczyk Date: Mon, 24 Jul 2023 22:07:37 +0200 Subject: [PATCH 09/22] Add level to siblings --- cadquery/occ_impl/shapes.py | 67 +++++++++++++++++++++++++------------ 1 file changed, 45 insertions(+), 22 deletions(-) diff --git a/cadquery/occ_impl/shapes.py b/cadquery/occ_impl/shapes.py index 488c1c8eb..285e3f3c8 100644 --- a/cadquery/occ_impl/shapes.py +++ b/cadquery/occ_impl/shapes.py @@ -1390,24 +1390,37 @@ def ancestors(self, shape: "Shape", kind: Shapes) -> "Compound": Shape.cast(s) for s in shape_map.FindFromKey(self.wrapped) ) - def siblings(self, shape: "Shape", kind: Shapes) -> "Compound": + def siblings(self, shape: "Shape", kind: Shapes, level: int = 1) -> "Compound": """ Iterate over siblings, i.e. shapes within shape that share subshapes of kind with self. """ shape_map = TopTools_IndexedDataMapOfShapeListOfShape() - TopExp.MapShapesAndAncestors_s( - shape.wrapped, inverse_shape_LUT[kind], shapetype(self.wrapped), shape_map + shape.wrapped, inverse_shape_LUT[kind], shapetype(self.wrapped), shape_map, ) + exclude = TopTools_MapOfShape() - return Compound.makeCompound( - Shape.cast(s) - for child in self._entities(kind) - for s in shape_map.FindFromKey(child) - if not self.wrapped.IsSame(s) - ) + def _siblings(shapes, level): + + rv = set() + + for s in shapes: + exclude.Add(s.wrapped) + + for s in shapes: + + rv.update( + Shape.cast(el) + for child in s._entities(kind) + for el in shape_map.FindFromKey(child) + if not exclude.Contains(el) + ) + + return rv if level == 1 else _siblings(rv, level - 1) + + return Compound.makeCompound(_siblings([self], level)) class ShapeProtocol(Protocol): @@ -3720,27 +3733,37 @@ def ancestors(self, shape: "Shape", kind: Shapes) -> "Compound": Shape.cast(s) for s in shape_map.FindFromKey(self.wrapped) ) - def siblings(self, shape: "Shape", kind: Shapes) -> "Compound": + def siblings(self, shape: "Shape", kind: Shapes, level: int = 1) -> "Compound": """ Iterate over siblings, i.e. shapes within shape that share subshapes of kind with the elements of self. """ shape_map = TopTools_IndexedDataMapOfShapeListOfShape() + TopExp.MapShapesAndAncestors_s( + shape.wrapped, inverse_shape_LUT[kind], shapetype(self.wrapped), shape_map, + ) + exclude = TopTools_MapOfShape() - items = set() - for ch in self: - TopExp.MapShapesAndAncestors_s( - shape.wrapped, inverse_shape_LUT[kind], shapetype(ch.wrapped), shape_map - ) - items.add(ch.wrapped) + def _siblings(shapes, level): - return Compound.makeCompound( - Shape.cast(s) - for child in self._entities(kind) - for s in shape_map.FindFromKey(child) - if not s.wrapped in items - ) + rv = set() + + for s in shapes: + exclude.Add(s.wrapped) + + for s in shapes: + + rv.update( + Shape.cast(el) + for child in s._entities(kind) + for el in shape_map.FindFromKey(child) + if not exclude.Contains(el) + ) + + return rv if level == 1 else _siblings(rv, level - 1) + + return Compound.makeCompound(_siblings(self, level)) def sortWiresByBuildOrder(wireList: List[Wire]) -> List[List[Wire]]: From 5c2197ab1a8f08f797a4335294131512ab3eb698 Mon Sep 17 00:00:00 2001 From: adam-urbanczyk Date: Tue, 25 Jul 2023 08:46:30 +0200 Subject: [PATCH 10/22] Tests and Compound fix --- cadquery/occ_impl/shapes.py | 6 +++++- tests/test_cadquery.py | 18 +++++++++++++----- 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/cadquery/occ_impl/shapes.py b/cadquery/occ_impl/shapes.py index 285e3f3c8..0d916460a 100644 --- a/cadquery/occ_impl/shapes.py +++ b/cadquery/occ_impl/shapes.py @@ -3740,8 +3740,12 @@ def siblings(self, shape: "Shape", kind: Shapes, level: int = 1) -> "Compound": """ shape_map = TopTools_IndexedDataMapOfShapeListOfShape() + TopExp.MapShapesAndAncestors_s( - shape.wrapped, inverse_shape_LUT[kind], shapetype(self.wrapped), shape_map, + shape.wrapped, + inverse_shape_LUT[kind], + shapetype(next(iter(self)).wrapped), + shape_map, ) exclude = TopTools_MapOfShape() diff --git a/tests/test_cadquery.py b/tests/test_cadquery.py index 3eb9f77ca..b65beaad8 100644 --- a/tests/test_cadquery.py +++ b/tests/test_cadquery.py @@ -5549,20 +5549,28 @@ def test_toVtk(self): def test_iterators(self): - s = Workplane().box(1, 1, 1).val() + w = Workplane().box(1, 1, 1) + s = w.val() + f = w.faces(">Z").val() # check ancestors res1 = list(s.Edges()[0].ancestors(s, "Face")) assert len(res1) == 2 # check siblings - res2 = list(s.Faces()[0].siblings(s, "Edge")) + res2 = list(f.siblings(s, "Edge")) assert len(res2) == 4 - # check regular iterator - res3 = list(s) + res3 = list(f.siblings(s, "Edge", 2)) assert len(res3) == 1 - assert isinstance(res3[0], Shell) + + res4 = list(f.siblings(s, "Edge").siblings(s, "Edge")) + assert len(res4) == 2 + + # check regular iterator + res5 = list(s) + assert len(res5) == 1 + assert isinstance(res5[0], Shell) # check ordered iteration for wires w = Workplane().polygon(5, 1).val() From bdbf60dd44c5b2f3b3bc1cf5d0633add0e8efc51 Mon Sep 17 00:00:00 2001 From: adam-urbanczyk Date: Thu, 27 Jul 2023 08:25:13 +0200 Subject: [PATCH 11/22] Tests+fixes for Compound --- cadquery/occ_impl/shapes.py | 20 ++++++++++---------- tests/test_cadquery.py | 11 +++++++++++ 2 files changed, 21 insertions(+), 10 deletions(-) diff --git a/cadquery/occ_impl/shapes.py b/cadquery/occ_impl/shapes.py index 0d916460a..1894a1d43 100644 --- a/cadquery/occ_impl/shapes.py +++ b/cadquery/occ_impl/shapes.py @@ -3723,14 +3723,15 @@ def ancestors(self, shape: "Shape", kind: Shapes) -> "Compound": """ shape_map = TopTools_IndexedDataMapOfShapeListOfShape() + shapetypes = set(shapetype(ch.wrapped) for ch in self) - for ch in self: + for t in shapetypes: TopExp.MapShapesAndAncestors_s( - shape.wrapped, shapetype(ch.wrapped), inverse_shape_LUT[kind], shape_map + shape.wrapped, t, inverse_shape_LUT[kind], shape_map ) return Compound.makeCompound( - Shape.cast(s) for s in shape_map.FindFromKey(self.wrapped) + Shape.cast(a) for s in self for a in shape_map.FindFromKey(s.wrapped) ) def siblings(self, shape: "Shape", kind: Shapes, level: int = 1) -> "Compound": @@ -3740,13 +3741,13 @@ def siblings(self, shape: "Shape", kind: Shapes, level: int = 1) -> "Compound": """ shape_map = TopTools_IndexedDataMapOfShapeListOfShape() + shapetypes = set(shapetype(ch.wrapped) for ch in self) + + for t in shapetypes: + TopExp.MapShapesAndAncestors_s( + shape.wrapped, inverse_shape_LUT[kind], t, shape_map, + ) - TopExp.MapShapesAndAncestors_s( - shape.wrapped, - inverse_shape_LUT[kind], - shapetype(next(iter(self)).wrapped), - shape_map, - ) exclude = TopTools_MapOfShape() def _siblings(shapes, level): @@ -3757,7 +3758,6 @@ def _siblings(shapes, level): exclude.Add(s.wrapped) for s in shapes: - rv.update( Shape.cast(el) for child in s._entities(kind) diff --git a/tests/test_cadquery.py b/tests/test_cadquery.py index b65beaad8..215988fa6 100644 --- a/tests/test_cadquery.py +++ b/tests/test_cadquery.py @@ -5578,3 +5578,14 @@ def test_iterators(self): for e1, e2 in zip(edges, edges[1:]): assert (e2.startPoint() - e1.endPoint()).Length == approx(0.0) + + # check ancestors on a compound + w = Workplane().pushPoints([(0, 0), (2, 0)]).box(1, 1, 1) + c = w.val() + fs = w.faces(">Z").combine().val() + + res6 = list(fs.ancestors(c, "Solid")) + assert len(res6) == 2 + + res7 = list(fs.siblings(c, "Edge", 2)) + assert len(res7) == 2 From e4e5e9f20d286b046c71d08424f7d2f39a0038ff Mon Sep 17 00:00:00 2001 From: adam-urbanczyk Date: Tue, 1 Aug 2023 08:27:37 +0200 Subject: [PATCH 12/22] Fix _collectProperty and add _filter --- cadquery/cq.py | 26 +++++++------- tests/test_cadquery.py | 77 +++++++++++++++++++++++------------------- 2 files changed, 56 insertions(+), 47 deletions(-) diff --git a/cadquery/cq.py b/cadquery/cq.py index 7a4c4cc29..2d06ee342 100644 --- a/cadquery/cq.py +++ b/cadquery/cq.py @@ -238,15 +238,12 @@ def _collectProperty(self, propName: str) -> List[CQObject]: """ Collects all of the values for propName, for all items on the stack. - OCCT objects do not implement id correctly, - so hashCode is used to ensure we don't add the same - object multiple times. One weird use case is that the stack could have a solid reference object on it. This is meant to be a reference to the most recently modified version of the context solid, whatever it is. """ - all = {} + rv = set() for o in self.objects: # tricky-- if an object is a compound of solids, @@ -257,14 +254,12 @@ def _collectProperty(self, propName: str) -> List[CQObject]: and isinstance(o, Solid) and o.ShapeType() == "Compound" ): - for i in getattr(o, "Compounds")(): - all[i.hashCode()] = i + rv.update(getattr(o, "Compounds")()) else: if hasattr(o, propName): - for i in getattr(o, propName)(): - all[i.hashCode()] = i + rv.update(getattr(o, propName)()) - return list(all.values()) + return list(rv) @overload def split(self: T, keepTop: bool = False, keepBottom: bool = False) -> T: @@ -471,7 +466,7 @@ def val(self) -> CQObject: """ return self.objects[0] if self.objects else self.plane.origin - def _getTagged(self, name: str) -> "Workplane": + def _getTagged(self: T, name: str) -> "Workplane": """ Search the parent chain for an object with tag == name. @@ -831,18 +826,25 @@ def _selectObjects( """ self_as_workplane: Workplane = self cq_obj = self._getTagged(tag) if tag else self_as_workplane + # A single list of all faces from all objects on the stack toReturn = cq_obj._collectProperty(objType) + return self.newObject(self._filter(toReturn, selector)) + + def _filter(self, objs, selector): + selectorObj: Selector if selector: if isinstance(selector, str): selectorObj = StringSyntaxSelector(selector) else: selectorObj = selector - toReturn = selectorObj.filter(toReturn) + toReturn = selectorObj.filter(objs) + else: + toReturn = objs - return self.newObject(toReturn) + return toReturn def vertices( self: T, diff --git a/tests/test_cadquery.py b/tests/test_cadquery.py index 215988fa6..f3538cf03 100644 --- a/tests/test_cadquery.py +++ b/tests/test_cadquery.py @@ -877,14 +877,14 @@ def ellipsePoints(r1, r2, a): r1, r2, startAtCurrent=False, angle1=a1, angle2=a2, rotation_angle=ra ) ) - start = ellipseArc1.vertices().objects[0] - end = ellipseArc1.vertices().objects[1] + start = ellipseArc1.val().startPoint() + end = ellipseArc1.val().endPoint() self.assertTupleAlmostEquals( - (start.X, start.Y), (p0[0] + sx_rot, p0[1] + sy_rot), 3 + (start.x, start.y), (p0[0] + sx_rot, p0[1] + sy_rot), 3 ) self.assertTupleAlmostEquals( - (end.X, end.Y), (p0[0] + ex_rot, p0[1] + ey_rot), 3 + (end.x, end.y), (p0[0] + ex_rot, p0[1] + ey_rot), 3 ) # startAtCurrent=True, sense = 1 @@ -895,14 +895,14 @@ def ellipsePoints(r1, r2, a): r1, r2, startAtCurrent=True, angle1=a1, angle2=a2, rotation_angle=ra ) ) - start = ellipseArc2.vertices().objects[0] - end = ellipseArc2.vertices().objects[1] + start = ellipseArc2.val().startPoint() + end = ellipseArc2.val().endPoint() self.assertTupleAlmostEquals( - (start.X, start.Y), (p0[0] + sx_rot - sx_rot, p0[1] + sy_rot - sy_rot), 3 + (start.x, start.y), (p0[0] + sx_rot - sx_rot, p0[1] + sy_rot - sy_rot), 3 ) self.assertTupleAlmostEquals( - (end.X, end.Y), (p0[0] + ex_rot - sx_rot, p0[1] + ey_rot - sy_rot), 3 + (end.x, end.y), (p0[0] + ex_rot - sx_rot, p0[1] + ey_rot - sy_rot), 3 ) # startAtCurrent=False, sense = -1 @@ -919,15 +919,15 @@ def ellipsePoints(r1, r2, a): sense=-1, ) ) - start = ellipseArc3.vertices().objects[0] - end = ellipseArc3.vertices().objects[1] + start = ellipseArc3.val().startPoint() + end = ellipseArc3.val().endPoint() # swap start and end points for comparison due to different sense self.assertTupleAlmostEquals( - (start.X, start.Y), (p0[0] + ex_rot, p0[1] + ey_rot), 3 + (start.x, start.y), (p0[0] + ex_rot, p0[1] + ey_rot), 3 ) self.assertTupleAlmostEquals( - (end.X, end.Y), (p0[0] + sx_rot, p0[1] + sy_rot), 3 + (end.x, end.y), (p0[0] + sx_rot, p0[1] + sy_rot), 3 ) # startAtCurrent=True, sense = -1 @@ -948,15 +948,15 @@ def ellipsePoints(r1, r2, a): self.assertEqual(len(ellipseArc4.ctx.pendingWires), 1) - start = ellipseArc4.vertices().objects[0] - end = ellipseArc4.vertices().objects[1] + start = ellipseArc4.val().startPoint() + end = ellipseArc4.val().endPoint() # swap start and end points for comparison due to different sense self.assertTupleAlmostEquals( - (start.X, start.Y), (p0[0] + ex_rot - ex_rot, p0[1] + ey_rot - ey_rot), 3 + (start.x, start.y), (p0[0] + ex_rot - ex_rot, p0[1] + ey_rot - ey_rot), 3 ) self.assertTupleAlmostEquals( - (end.X, end.Y), (p0[0] + sx_rot - ex_rot, p0[1] + sy_rot - ey_rot), 3 + (end.x, end.y), (p0[0] + sx_rot - ex_rot, p0[1] + sy_rot - ey_rot), 3 ) def testEllipseArcsClockwise(self): @@ -5333,33 +5333,40 @@ def circumradius(n, a): a = 1 # Test triangle - vs = Workplane("XY").polygon(3, 2 * a, circumscribed=True).vertices().vals() + w = Workplane("XY").polygon(3, 2 * a, circumscribed=True) + vs = w.vertices().vals() + self.assertEqual(3, len(vs)) + R = circumradius(3, a) - self.assertEqual( - vs[0].toTuple(), approx((a, a * math.tan(math.radians(60)), 0)) - ) - self.assertEqual(vs[1].toTuple(), approx((-R, 0, 0))) - self.assertEqual( - vs[2].toTuple(), approx((a, -a * math.tan(math.radians(60)), 0)) - ) + + vs0 = w.vertices(">X").vertices(">Y").val() + vs1 = w.vertices("X").vertices("X").vertices(">Y").val() + vs1 = w.vertices("Y").val() + vs2 = w.vertices("X").vertices(" Date: Tue, 1 Aug 2023 17:42:46 +0200 Subject: [PATCH 13/22] Try to preserve order --- cadquery/cq.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/cadquery/cq.py b/cadquery/cq.py index 2d06ee342..8af01577a 100644 --- a/cadquery/cq.py +++ b/cadquery/cq.py @@ -243,7 +243,8 @@ def _collectProperty(self, propName: str) -> List[CQObject]: on it. This is meant to be a reference to the most recently modified version of the context solid, whatever it is. """ - rv = set() + rv: Dict[CQObject, Any] = {} # used as an ordered set + for o in self.objects: # tricky-- if an object is a compound of solids, @@ -254,12 +255,14 @@ def _collectProperty(self, propName: str) -> List[CQObject]: and isinstance(o, Solid) and o.ShapeType() == "Compound" ): - rv.update(getattr(o, "Compounds")()) + for k in getattr(o, "Compounds")(): + rv[k] = None else: if hasattr(o, propName): - rv.update(getattr(o, propName)()) + for k in getattr(o, propName)(): + rv[k] = None - return list(rv) + return list(rv.keys()) @overload def split(self: T, keepTop: bool = False, keepBottom: bool = False) -> T: From f6f726db75b6e3ed1367c996b1b322262d29a9f4 Mon Sep 17 00:00:00 2001 From: adam-urbanczyk Date: Thu, 3 Aug 2023 08:15:24 +0200 Subject: [PATCH 14/22] Ancestors and siblings for Workplane --- cadquery/cq.py | 39 ++++++++++++++++++++++++++++++++++----- 1 file changed, 34 insertions(+), 5 deletions(-) diff --git a/cadquery/cq.py b/cadquery/cq.py index 8af01577a..05d683a52 100644 --- a/cadquery/cq.py +++ b/cadquery/cq.py @@ -469,7 +469,7 @@ def val(self) -> CQObject: """ return self.objects[0] if self.objects else self.plane.origin - def _getTagged(self: T, name: str) -> "Workplane": + def _getTagged(self: T, name: str) -> T: """ Search the parent chain for an object with tag == name. @@ -482,7 +482,7 @@ def _getTagged(self: T, name: str) -> "Workplane": if rv is None: raise ValueError(f"No Workplane object named {name} in chain") - return rv + return cast(T, rv) def _mergeTags(self: T, obj: "Workplane") -> T: """ @@ -827,15 +827,14 @@ def _selectObjects( solids,shells, and other similar selector methods. It is a useful extension point for plugin developers to make other selector methods. """ - self_as_workplane: Workplane = self - cq_obj = self._getTagged(tag) if tag else self_as_workplane + cq_obj = self._getTagged(tag) if tag else self # A single list of all faces from all objects on the stack toReturn = cq_obj._collectProperty(objType) return self.newObject(self._filter(toReturn, selector)) - def _filter(self, objs, selector): + def _filter(self, objs, selector: Optional[Union[Selector, str]]): selectorObj: Selector if selector: @@ -1051,6 +1050,36 @@ def compounds( """ return self._selectObjects("Compounds", selector, tag) + def ancestors(self: T, kind, tag: Optional[str] = None) -> T: + """ + Iterate over ancestors. + + """ + ctx_solid = self.findSolid() + objects = self._getTagged(tag).objects if tag else self.objects + + results = [ + el.ancestors(ctx_solid, kind) for el in objects if isinstance(el, Shape) + ] + + return self.newObject(el for res in results for el in res) + + def siblings(self: T, kind, level: int = 1, tag: Optional[str] = None) -> T: + """ + Iterate over siblings. + + """ + ctx_solid = self.findSolid() + objects = self._getTagged(tag).objects if tag else self.objects + + results = [ + el.siblings(ctx_solid, kind, level) + for el in objects + if isinstance(el, Shape) + ] + + return self.newObject(el for res in results for el in res) + def toSvg(self, opts: Any = None) -> str: """ Returns svg text that represents the first item on the stack. From ddc2423069dd4c0e19718a4b3fd009690782c8fe Mon Sep 17 00:00:00 2001 From: adam-urbanczyk Date: Thu, 3 Aug 2023 08:16:16 +0200 Subject: [PATCH 15/22] Redundant import --- cadquery/occ_impl/shapes.py | 1 - 1 file changed, 1 deletion(-) diff --git a/cadquery/occ_impl/shapes.py b/cadquery/occ_impl/shapes.py index 1894a1d43..c1f617940 100644 --- a/cadquery/occ_impl/shapes.py +++ b/cadquery/occ_impl/shapes.py @@ -162,7 +162,6 @@ TopTools_MapOfShape, ) -from OCP.TopExp import TopExp from OCP.ShapeFix import ShapeFix_Shape, ShapeFix_Solid, ShapeFix_Face From f69231a6db31f2aa4d4773f47bf10d6cc1c8334a Mon Sep 17 00:00:00 2001 From: adam-urbanczyk Date: Fri, 18 Aug 2023 08:55:29 +0200 Subject: [PATCH 16/22] Add tests and fix duplicate results --- cadquery/cq.py | 11 ++++------- tests/test_cadquery.py | 5 +++++ 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/cadquery/cq.py b/cadquery/cq.py index 05d683a52..1d9d2d773 100644 --- a/cadquery/cq.py +++ b/cadquery/cq.py @@ -1062,7 +1062,7 @@ def ancestors(self: T, kind, tag: Optional[str] = None) -> T: el.ancestors(ctx_solid, kind) for el in objects if isinstance(el, Shape) ] - return self.newObject(el for res in results for el in res) + return self.newObject(set(el for res in results for el in res)) def siblings(self: T, kind, level: int = 1, tag: Optional[str] = None) -> T: """ @@ -1071,14 +1071,11 @@ def siblings(self: T, kind, level: int = 1, tag: Optional[str] = None) -> T: """ ctx_solid = self.findSolid() objects = self._getTagged(tag).objects if tag else self.objects + shapes = [el for el in objects if isinstance(el, Shape)] - results = [ - el.siblings(ctx_solid, kind, level) - for el in objects - if isinstance(el, Shape) - ] + results = [el.siblings(ctx_solid, kind, level) for el in shapes] - return self.newObject(el for res in results for el in res) + return self.newObject(set(el for res in results for el in res) - set(shapes)) def toSvg(self, opts: Any = None) -> str: """ diff --git a/tests/test_cadquery.py b/tests/test_cadquery.py index f3538cf03..a862d200b 100644 --- a/tests/test_cadquery.py +++ b/tests/test_cadquery.py @@ -5563,16 +5563,21 @@ def test_iterators(self): # check ancestors res1 = list(s.Edges()[0].ancestors(s, "Face")) assert len(res1) == 2 + assert w.faces(">Z").edges(">X").ancestors("Face").size() == 2 + assert w.faces(">Z").edges(">X or Z").siblings("Edge").size() == 4 res3 = list(f.siblings(s, "Edge", 2)) assert len(res3) == 1 + assert w.faces(">Z").siblings("Edge", 2).size() == 1 res4 = list(f.siblings(s, "Edge").siblings(s, "Edge")) assert len(res4) == 2 + assert w.faces(">Z").siblings("Edge").siblings("Edge").size() == 2 # check regular iterator res5 = list(s) From 28ab97f7539d97767fbd1fc39ed2b596e8567f33 Mon Sep 17 00:00:00 2001 From: adam-urbanczyk Date: Tue, 22 Aug 2023 08:41:14 +0200 Subject: [PATCH 17/22] Docstrings and additional annotations --- cadquery/cq.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/cadquery/cq.py b/cadquery/cq.py index 1d9d2d773..b20f3bec6 100644 --- a/cadquery/cq.py +++ b/cadquery/cq.py @@ -47,6 +47,7 @@ Solid, Compound, wiresToFaces, + Shapes, ) from .occ_impl.exporters.svg import getSVG, exportSVG @@ -1050,10 +1051,15 @@ def compounds( """ return self._selectObjects("Compounds", selector, tag) - def ancestors(self: T, kind, tag: Optional[str] = None) -> T: + def ancestors(self: T, kind: Shapes, tag: Optional[str] = None) -> T: """ Iterate over ancestors. + :param kind: kind of ancestor, e.g. "Face" or "Edge" + :param tag: if set, search the tagged object instead of self + :return: a Workplane object who's stack contains all ancestors. + + """ ctx_solid = self.findSolid() objects = self._getTagged(tag).objects if tag else self.objects @@ -1064,10 +1070,15 @@ def ancestors(self: T, kind, tag: Optional[str] = None) -> T: return self.newObject(set(el for res in results for el in res)) - def siblings(self: T, kind, level: int = 1, tag: Optional[str] = None) -> T: + def siblings(self: T, kind: Shapes, level: int = 1, tag: Optional[str] = None) -> T: """ Iterate over siblings. + :param kind: kind of linking element, e.g. "Vertex" or "Edge" + :param level: level of relation - how many elements of kind are in the link + :param tag: if set, search the tagged object instead of self + :return: a Workplane object who's stack contains all ancestors. + """ ctx_solid = self.findSolid() objects = self._getTagged(tag).objects if tag else self.objects From 510ed270c933c7c91bcefbcc3209dc96ae6528ec Mon Sep 17 00:00:00 2001 From: adam-urbanczyk Date: Tue, 22 Aug 2023 08:57:48 +0200 Subject: [PATCH 18/22] Docs update --- doc/selectors.rst | 31 +++++++++++++++++++++++++++++-- 1 file changed, 29 insertions(+), 2 deletions(-) diff --git a/doc/selectors.rst b/doc/selectors.rst index 9cc5d65a0..6a5858841 100644 --- a/doc/selectors.rst +++ b/doc/selectors.rst @@ -1,7 +1,7 @@ .. _selector_reference: -String Selectors Reference -============================= +Selectors Reference +=================== CadQuery selector strings allow filtering various types of object lists. Most commonly, Edges, Faces, and Vertices are @@ -148,3 +148,30 @@ It is possible to use user defined vectors as a basis for the selectors. For exa # chamfer only one edge result = result.edges('>(-1, 1, 0)').chamfer(1) + + +Topological Selectors +--------------------- + +Is is also possible to use topological relations to select objects. Currently +the following methods are supported: + + * :py:meth:`cadquery.Workplane.ancestors` + * :py:meth:`cadquery.Workplane.siblings` + +Ancestors allows to select all objects containing currently selected object. + +.. cadquery:: + + result = cq.Workplane("XY").box(10, 10, 10).faces(">Z").edges("Z") + + result = result.siblings("Edge") From 827d2088983d64413c6d0b8e81797585e418a27c Mon Sep 17 00:00:00 2001 From: AU Date: Fri, 25 Aug 2023 08:17:33 +0200 Subject: [PATCH 19/22] Fix formatting --- doc/selectors.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/selectors.rst b/doc/selectors.rst index e8f6bcb5a..ae51cee1a 100644 --- a/doc/selectors.rst +++ b/doc/selectors.rst @@ -142,7 +142,7 @@ It is possible to use user defined vectors as a basis for the selectors. For exa result = cq.Workplane("XY").box(10, 10, 10) # chamfer only one edge - result = result.edges('>(-1, 1, 0)').chamfer(1) + result = result.edges(">(-1, 1, 0)").chamfer(1) Topological Selectors From 0137b97f699daf40efe991c068571802cc6448ce Mon Sep 17 00:00:00 2001 From: AU Date: Thu, 7 Sep 2023 18:35:24 +0200 Subject: [PATCH 20/22] Typo --- cadquery/occ_impl/exporters/dxf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cadquery/occ_impl/exporters/dxf.py b/cadquery/occ_impl/exporters/dxf.py index 20803e20e..8300d3fc2 100644 --- a/cadquery/occ_impl/exporters/dxf.py +++ b/cadquery/occ_impl/exporters/dxf.py @@ -181,7 +181,7 @@ def _ordered_edges(s: Shape) -> List[Edge]: rv: List[Edge] = [] - # iterato over wires and then edges + # iterate over wires and then edges for w in s.Wires(): rv.extend(w) From 9f6aa12fc4a67b44abf14cc197094ecb94d57193 Mon Sep 17 00:00:00 2001 From: AU Date: Thu, 7 Sep 2023 19:39:55 +0200 Subject: [PATCH 21/22] Docstring fix --- cadquery/cq.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/cadquery/cq.py b/cadquery/cq.py index e1b8fde5c..3c3403d6e 100644 --- a/cadquery/cq.py +++ b/cadquery/cq.py @@ -1053,11 +1053,11 @@ def compounds( def ancestors(self: T, kind: Shapes, tag: Optional[str] = None) -> T: """ - Iterate over ancestors. + Select topological ancestors. :param kind: kind of ancestor, e.g. "Face" or "Edge" :param tag: if set, search the tagged object instead of self - :return: a Workplane object who's stack contains all ancestors. + :return: a Workplane object whose stack contains selected ancestors. """ @@ -1072,12 +1072,12 @@ def ancestors(self: T, kind: Shapes, tag: Optional[str] = None) -> T: def siblings(self: T, kind: Shapes, level: int = 1, tag: Optional[str] = None) -> T: """ - Iterate over siblings. + Select topological siblings. :param kind: kind of linking element, e.g. "Vertex" or "Edge" :param level: level of relation - how many elements of kind are in the link :param tag: if set, search the tagged object instead of self - :return: a Workplane object who's stack contains all ancestors. + :return: a Workplane object whose stack contains selected siblings. """ ctx_solid = self.findSolid() From 952722fa4824701ca92ad4167943844f3769640c Mon Sep 17 00:00:00 2001 From: AU Date: Thu, 7 Sep 2023 19:42:37 +0200 Subject: [PATCH 22/22] Docstring fixes --- cadquery/cq.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/cadquery/cq.py b/cadquery/cq.py index 3c3403d6e..f6ec428a5 100644 --- a/cadquery/cq.py +++ b/cadquery/cq.py @@ -862,7 +862,7 @@ def vertices( :param selector: optional Selector object, or string selector expression (see :class:`StringSyntaxSelector`) :param tag: if set, search the tagged object instead of self - :return: a CQ object who's stack contains the *distinct* vertices of *all* objects on the + :return: a CQ object whose stack contains the *distinct* vertices of *all* objects on the current stack, after being filtered by the selector, if provided If there are no vertices for any objects on the current stack, an empty CQ object @@ -896,7 +896,7 @@ def faces( :param selector: optional Selector object, or string selector expression (see :class:`StringSyntaxSelector`) :param tag: if set, search the tagged object instead of self - :return: a CQ object who's stack contains all of the *distinct* faces of *all* objects on + :return: a CQ object whose stack contains all of the *distinct* faces of *all* objects on the current stack, filtered by the provided selector. If there are no faces for any objects on the current stack, an empty CQ object @@ -931,7 +931,7 @@ def edges( :param selector: optional Selector object, or string selector expression (see :class:`StringSyntaxSelector`) :param tag: if set, search the tagged object instead of self - :return: a CQ object who's stack contains all of the *distinct* edges of *all* objects on + :return: a CQ object whose stack contains all of the *distinct* edges of *all* objects on the current stack, filtered by the provided selector. If there are no edges for any objects on the current stack, an empty CQ object is returned @@ -965,7 +965,7 @@ def wires( :param selector: optional Selector object, or string selector expression (see :class:`StringSyntaxSelector`) :param tag: if set, search the tagged object instead of self - :return: a CQ object who's stack contains all of the *distinct* wires of *all* objects on + :return: a CQ object whose stack contains all of the *distinct* wires of *all* objects on the current stack, filtered by the provided selector. If there are no wires for any objects on the current stack, an empty CQ object is returned @@ -991,7 +991,7 @@ def solids( :param selector: optional Selector object, or string selector expression (see :class:`StringSyntaxSelector`) :param tag: if set, search the tagged object instead of self - :return: a CQ object who's stack contains all of the *distinct* solids of *all* objects on + :return: a CQ object whose stack contains all of the *distinct* solids of *all* objects on the current stack, filtered by the provided selector. If there are no solids for any objects on the current stack, an empty CQ object is returned @@ -1020,7 +1020,7 @@ def shells( :param selector: optional Selector object, or string selector expression (see :class:`StringSyntaxSelector`) :param tag: if set, search the tagged object instead of self - :return: a CQ object who's stack contains all of the *distinct* shells of *all* objects on + :return: a CQ object whose stack contains all of the *distinct* shells of *all* objects on the current stack, filtered by the provided selector. If there are no shells for any objects on the current stack, an empty CQ object is returned @@ -1043,7 +1043,7 @@ def compounds( :param selector: optional Selector object, or string selector expression (see :class:`StringSyntaxSelector`) :param tag: if set, search the tagged object instead of self - :return: a CQ object who's stack contains all of the *distinct* compounds of *all* objects on + :return: a CQ object whose stack contains all of the *distinct* compounds of *all* objects on the current stack, filtered by the provided selector. A compound contains multiple CAD primitives that resulted from a single operation, such as