From c592ce825491036fb7ad090e46e10ab3c2769e3d Mon Sep 17 00:00:00 2001 From: adam-urbanczyk Date: Wed, 25 Sep 2024 19:21:06 +0200 Subject: [PATCH 01/80] Initial commit of better paramAt, solid and a new function check --- cadquery/occ_impl/shapes.py | 51 +++++++++++++++++++++++++++++++------ 1 file changed, 43 insertions(+), 8 deletions(-) diff --git a/cadquery/occ_impl/shapes.py b/cadquery/occ_impl/shapes.py index 53b0ec623..891a6187b 100644 --- a/cadquery/occ_impl/shapes.py +++ b/cadquery/occ_impl/shapes.py @@ -132,6 +132,7 @@ from OCP.GeomAPI import ( GeomAPI_Interpolate, GeomAPI_ProjectPointOnSurf, + GeomAPI_ProjectPointOnCurve, GeomAPI_PointsToBSpline, GeomAPI_PointsToBSplineSurface, ) @@ -144,6 +145,7 @@ BRepAlgoAPI_Cut, BRepAlgoAPI_BooleanOperation, BRepAlgoAPI_Splitter, + BRepAlgoAPI_Check, ) from OCP.Geom import ( @@ -1699,18 +1701,28 @@ def endPoint(self: Mixin1DProtocol) -> Vector: return Vector(curve.Value(umax)) - def paramAt(self: Mixin1DProtocol, d: float) -> float: + def paramAt(self: Mixin1DProtocol, d: Union[Real, Vector]) -> float: """ - Compute parameter value at the specified normalized distance. + Compute parameter value at the specified normalized distance or a point. - :param d: normalized distance [0, 1] + :param d: normalized distance [0, 1] or a point :return: parameter value """ curve = self._geomAdaptor() - l = GCPnts_AbscissaPoint.Length_s(curve) - return GCPnts_AbscissaPoint(curve, l * d, curve.FirstParameter()).Parameter() + if isinstance(d, Real): + l = GCPnts_AbscissaPoint.Length_s(curve) + rv = GCPnts_AbscissaPoint(curve, l * d, curve.FirstParameter()).Parameter() + else: + rv = GeomAPI_ProjectPointOnCurve( + d.toPnt(), + curve.Curve().Curve(), + curve.FirstParameter(), + curve.LastParameter(), + ).LowerDistanceParameter() + + return rv def tangentAt( self: Mixin1DProtocol, @@ -2517,7 +2529,7 @@ def fillet( ) -> "Wire": """ Apply 2D or 3D fillet to a wire - + :param wire: The input wire to fillet. Currently only open wires are supported :param radius: the radius of the fillet, must be > zero :param vertices: Optional list of vertices to fillet. By default all vertices are fillet. :return: A wire with filleted corners @@ -4419,9 +4431,18 @@ def solid(*s: Shape) -> Shape: @solid.register -def solid(s: Sequence[Shape]) -> Shape: +def solid(s: Sequence[Shape], inner: Optional[Sequence[Shape]] = None) -> Shape: + + builder = BRepBuilderAPI_MakeSolid() + builder.Add(shell(*s).wrapped) - return solid(*s) + if inner: + for sh in _get(shell(*s), "Shell"): + builder.Add(sh.wrapped) + + rv = builder.Solid() + + return _compound_or_shape(rv) @multimethod @@ -5111,3 +5132,17 @@ def loft(s: Sequence[Shape], cap: bool = False, ruled: bool = False) -> Shape: def loft(*s: Shape, cap: bool = False, ruled: bool = False) -> Shape: return loft(s, cap, ruled) + + +#%% diagnotics + + +def check(s: Shape) -> bool: + """ + Check if a shape is valid. + """ + + analyzer = BRepAlgoAPI_Check(s.wrapped) + rv = analyzer.isValid() + + return rv From 1dd74819c5be84406a3cdccbcbd5a5e9ba74974c Mon Sep 17 00:00:00 2001 From: adam-urbanczyk Date: Wed, 25 Sep 2024 19:39:06 +0200 Subject: [PATCH 02/80] mypy fix --- cadquery/occ_impl/shapes.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/cadquery/occ_impl/shapes.py b/cadquery/occ_impl/shapes.py index 891a6187b..89f0505f9 100644 --- a/cadquery/occ_impl/shapes.py +++ b/cadquery/occ_impl/shapes.py @@ -1711,16 +1711,16 @@ def paramAt(self: Mixin1DProtocol, d: Union[Real, Vector]) -> float: curve = self._geomAdaptor() - if isinstance(d, Real): - l = GCPnts_AbscissaPoint.Length_s(curve) - rv = GCPnts_AbscissaPoint(curve, l * d, curve.FirstParameter()).Parameter() - else: + if isinstance(d, Vector): rv = GeomAPI_ProjectPointOnCurve( d.toPnt(), curve.Curve().Curve(), curve.FirstParameter(), curve.LastParameter(), ).LowerDistanceParameter() + else: + l = GCPnts_AbscissaPoint.Length_s(curve) + rv = GCPnts_AbscissaPoint(curve, l * d, curve.FirstParameter()).Parameter() return rv From 3e9d30fe29950310b7e0608c0fbc6db25d2e9713 Mon Sep 17 00:00:00 2001 From: adam-urbanczyk Date: Wed, 25 Sep 2024 19:40:54 +0200 Subject: [PATCH 03/80] isSolid fix --- cadquery/occ_impl/shapes.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cadquery/occ_impl/shapes.py b/cadquery/occ_impl/shapes.py index 89f0505f9..9ec9e50dc 100644 --- a/cadquery/occ_impl/shapes.py +++ b/cadquery/occ_impl/shapes.py @@ -3312,8 +3312,8 @@ def isSolid(obj: Shape) -> bool: Returns true if the object is a solid, false otherwise """ if hasattr(obj, "ShapeType"): - if obj.ShapeType == "Solid" or ( - obj.ShapeType == "Compound" and len(obj.Solids()) > 0 + if obj.ShapeType() == "Solid" or ( + obj.ShapeType() == "Compound" and len(obj.Solids()) > 0 ): return True return False From 6ba8b72651066208bfd32b11a8b3b0de2586fd52 Mon Sep 17 00:00:00 2001 From: adam-urbanczyk Date: Wed, 25 Sep 2024 19:52:06 +0200 Subject: [PATCH 04/80] Implement outerShell, innerShells --- cadquery/occ_impl/shapes.py | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/cadquery/occ_impl/shapes.py b/cadquery/occ_impl/shapes.py index 9ec9e50dc..c8d422b08 100644 --- a/cadquery/occ_impl/shapes.py +++ b/cadquery/occ_impl/shapes.py @@ -226,7 +226,7 @@ from OCP.BRepFeat import BRepFeat_MakeDPrism -from OCP.BRepClass3d import BRepClass3d_SolidClassifier +from OCP.BRepClass3d import BRepClass3d_SolidClassifier, BRepClass3d from OCP.TCollection import TCollection_AsciiString @@ -3849,6 +3849,22 @@ def sweep_multi( return cls(builder.Shape()) + def outerShell(self) -> Shell: + """ + Returns outer shell. + """ + + return Shell(BRepClass3d.OuterShell_s(self.wrapped)) + + def innerShells(self) -> List[Shell]: + """ + Returns inner shells. + """ + + outer = self.outerShell() + + return [s for s in self.Shells() if not s.isSame(outer)] + class CompSolid(Shape, Mixin3D): """ From 44cb068525a381136c98347fa897703ac97cf5e0 Mon Sep 17 00:00:00 2001 From: adam-urbanczyk Date: Wed, 25 Sep 2024 19:53:46 +0200 Subject: [PATCH 05/80] Imports cleanup --- cadquery/occ_impl/shapes.py | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/cadquery/occ_impl/shapes.py b/cadquery/occ_impl/shapes.py index c8d422b08..51b7865f5 100644 --- a/cadquery/occ_impl/shapes.py +++ b/cadquery/occ_impl/shapes.py @@ -213,15 +213,6 @@ Graphic3d_VTA_TOP, ) -from OCP.Graphic3d import ( - Graphic3d_HTA_LEFT, - Graphic3d_HTA_CENTER, - Graphic3d_HTA_RIGHT, - Graphic3d_VTA_BOTTOM, - Graphic3d_VTA_CENTER, - Graphic3d_VTA_TOP, -) - from OCP.NCollection import NCollection_Utf8String from OCP.BRepFeat import BRepFeat_MakeDPrism @@ -235,10 +226,7 @@ from OCP.GeomAbs import ( GeomAbs_Shape, GeomAbs_C0, - GeomAbs_C1, - GeomAbs_C2, GeomAbs_G2, - GeomAbs_G1, GeomAbs_Intersection, GeomAbs_JoinType, ) From bac0c7a34b22746ec852ec80b284232782677cf7 Mon Sep 17 00:00:00 2001 From: adam-urbanczyk Date: Wed, 25 Sep 2024 20:37:28 +0200 Subject: [PATCH 06/80] Typo fix --- 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 51b7865f5..884c4f792 100644 --- a/cadquery/occ_impl/shapes.py +++ b/cadquery/occ_impl/shapes.py @@ -5147,6 +5147,6 @@ def check(s: Shape) -> bool: """ analyzer = BRepAlgoAPI_Check(s.wrapped) - rv = analyzer.isValid() + rv = analyzer.IsValid() return rv From 12c5e0e5c1e9e4c2f13a9addd1079b9a398a0309 Mon Sep 17 00:00:00 2001 From: adam-urbanczyk Date: Thu, 26 Sep 2024 08:37:25 +0200 Subject: [PATCH 07/80] Make paramAt fully compatibile with Wires --- cadquery/occ_impl/shapes.py | 56 +++++++++++++++++++++++++++++++------ 1 file changed, 47 insertions(+), 9 deletions(-) diff --git a/cadquery/occ_impl/shapes.py b/cadquery/occ_impl/shapes.py index 884c4f792..73c9e63e0 100644 --- a/cadquery/occ_impl/shapes.py +++ b/cadquery/occ_impl/shapes.py @@ -154,6 +154,7 @@ Geom_CylindricalSurface, Geom_Surface, Geom_Plane, + Geom_BSplineCurve, ) from OCP.Geom2d import Geom2d_Line @@ -227,6 +228,7 @@ GeomAbs_Shape, GeomAbs_C0, GeomAbs_G2, + GeomAbs_C2, GeomAbs_Intersection, GeomAbs_JoinType, ) @@ -239,7 +241,7 @@ from OCP.TopAbs import TopAbs_ShapeEnum, TopAbs_Orientation -from OCP.ShapeAnalysis import ShapeAnalysis_FreeBounds +from OCP.ShapeAnalysis import ShapeAnalysis_FreeBounds, ShapeAnalysis_Wire from OCP.TopTools import TopTools_HSequenceOfShape from OCP.GCPnts import GCPnts_AbscissaPoint @@ -271,6 +273,8 @@ from OCP.ChFi2d import ChFi2d_FilletAPI # For Wire.Fillet() +from OCP.GeomConvert import GeomConvert_ApproxCurve + from math import pi, sqrt, inf, radians, cos import warnings @@ -1633,6 +1637,9 @@ def makeVertex(cls, x: float, y: float, z: float) -> "Vertex": class Mixin1DProtocol(ShapeProtocol, Protocol): + def _approxCurve(self) -> Geom_BSplineCurve: + ... + def _geomAdaptor(self) -> Union[BRepAdaptor_Curve, BRepAdaptor_CompCurve]: ... @@ -1689,6 +1696,18 @@ def endPoint(self: Mixin1DProtocol) -> Vector: return Vector(curve.Value(umax)) + def _approxCurve(self: Mixin1DProtocol) -> Geom_BSplineCurve: + """ + Approximate curve adaptor into a real b-spline. Meant for handling of + BRepAdaptor_CompCurve. + """ + + rv = GeomConvert_ApproxCurve( + self._geomAdaptor(), TOLERANCE, GeomAbs_C2, MaxSegments=100, MaxDegree=3 + ).Curve() + + return rv + def paramAt(self: Mixin1DProtocol, d: Union[Real, Vector]) -> float: """ Compute parameter value at the specified normalized distance or a point. @@ -1700,12 +1719,16 @@ def paramAt(self: Mixin1DProtocol, d: Union[Real, Vector]) -> float: curve = self._geomAdaptor() if isinstance(d, Vector): + # handle comp curves (i.e. wire adaptors) + if isinstance(curve, BRepAdaptor_Curve): + curve_ = curve.Curve().Curve() # get the underlying curve object + else: + curve_ = self._approxCurve() # approximate the adaptor as a real curve + rv = GeomAPI_ProjectPointOnCurve( - d.toPnt(), - curve.Curve().Curve(), - curve.FirstParameter(), - curve.LastParameter(), + d.toPnt(), curve_, curve.FirstParameter(), curve.LastParameter(), ).LowerDistanceParameter() + else: l = GCPnts_AbscissaPoint.Length_s(curve) rv = GCPnts_AbscissaPoint(curve, l * d, curve.FirstParameter()).Parameter() @@ -2253,12 +2276,27 @@ class Wire(Shape, Mixin1D): wrapped: TopoDS_Wire - def _geomAdaptor(self) -> BRepAdaptor_CompCurve: + def _nbEdges(self) -> int: """ - Return the underlying geometry + Number of edges. """ - return BRepAdaptor_CompCurve(self.wrapped) + sa = ShapeAnalysis_Wire() + sa.Load(self.wrapped) + + return sa.nbEdges() + + def _geomAdaptor(self) -> Union[BRepAdaptor_Curve, BRepAdaptor_CompCurve]: + """ + Return the underlying geometry. + """ + + if self._nbEdges() == 1: + rv = self.Edges()[-1]._geomAdaptor() + else: + rv = BRepAdaptor_CompCurve(self.wrapped) + + return rv def close(self) -> "Wire": """ @@ -2948,7 +2986,7 @@ def thicken(self, thickness: float) -> "Solid": False, GeomAbs_Intersection, True, - ) # The last True is important to make solid + ) # The last True is important to make a solid builder.MakeOffsetShape() From b820c3a021656e125a025607d11bfa273d0c4cfb Mon Sep 17 00:00:00 2001 From: adam-urbanczyk Date: Thu, 26 Sep 2024 08:46:12 +0200 Subject: [PATCH 08/80] Typo fix --- cadquery/occ_impl/shapes.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/cadquery/occ_impl/shapes.py b/cadquery/occ_impl/shapes.py index 73c9e63e0..a35251ab9 100644 --- a/cadquery/occ_impl/shapes.py +++ b/cadquery/occ_impl/shapes.py @@ -2284,13 +2284,15 @@ def _nbEdges(self) -> int: sa = ShapeAnalysis_Wire() sa.Load(self.wrapped) - return sa.nbEdges() + return sa.NbEdges() def _geomAdaptor(self) -> Union[BRepAdaptor_Curve, BRepAdaptor_CompCurve]: """ Return the underlying geometry. """ + rv: Union[BRepAdaptor_Curve, BRepAdaptor_CompCurve] + if self._nbEdges() == 1: rv = self.Edges()[-1]._geomAdaptor() else: From 908064b721f25bb5b560e2d193e31755d7515ac6 Mon Sep 17 00:00:00 2001 From: adam-urbanczyk Date: Thu, 26 Sep 2024 08:55:56 +0200 Subject: [PATCH 09/80] Add check test --- tests/test_free_functions.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/tests/test_free_functions.py b/tests/test_free_functions.py index 2092ce24b..4d70b4d28 100644 --- a/tests/test_free_functions.py +++ b/tests/test_free_functions.py @@ -39,6 +39,7 @@ _get, _get_one, _get_edges, + check, ) from pytest import approx, raises @@ -558,3 +559,15 @@ def test_export(): b2 = Shape.importBrep("box.brep") assert (b1 - b2).Volume() == approx(0) + + +# %% diagnostics +def test_check(): + + s1 = box(1, 1, 1) + + assert check(s1) + + s2 = sweep(rect(1, 1), segment((0, 0), (1, 1))) + + assert not check(s2) From eda0d3f1bdbd97c3c0e4f0a2ae70c3c7d664c007 Mon Sep 17 00:00:00 2001 From: adam-urbanczyk Date: Thu, 26 Sep 2024 18:31:52 +0200 Subject: [PATCH 10/80] Fix orientation handling --- cadquery/occ_impl/shapes.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/cadquery/occ_impl/shapes.py b/cadquery/occ_impl/shapes.py index a35251ab9..2241c7cf9 100644 --- a/cadquery/occ_impl/shapes.py +++ b/cadquery/occ_impl/shapes.py @@ -4481,8 +4481,11 @@ def solid(s: Sequence[Shape], inner: Optional[Sequence[Shape]] = None) -> Shape: builder.Add(shell(*s).wrapped) if inner: - for sh in _get(shell(*s), "Shell"): - builder.Add(sh.wrapped) + for sh in _get(shell(*inner), "Shell"): + sh_inverted = TopoDS.Shell_s( + sh.wrapped.Oriented(TopAbs_Orientation.TopAbs_REVERSED) + ) + builder.Add(sh_inverted) rv = builder.Solid() From 53307773e9afc8a38cc04537ffe3a5ea14ede411 Mon Sep 17 00:00:00 2001 From: adam-urbanczyk Date: Thu, 26 Sep 2024 18:36:25 +0200 Subject: [PATCH 11/80] Add test --- tests/test_free_functions.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tests/test_free_functions.py b/tests/test_free_functions.py index 4d70b4d28..a4964a021 100644 --- a/tests/test_free_functions.py +++ b/tests/test_free_functions.py @@ -140,6 +140,13 @@ def test_constructors(): assert s1.Volume() == approx(1) assert s2.Volume() == approx(1) + # solid with voids + b1 = box(0.1, 0.1, 0.1) + + s3 = solid(b.Faces(), b1.moved([(0.2, 0, 0.5), (-0.2, 0, 0.5)]).Faces()) + + assert s3.Volume() == approx(1 - 2 * 0.1 ** 3) + # compound c1 = compound(b.Faces()) c2 = compound(*b.Faces()) From 151a70b04681f8f19efd6f3b5ecd31ce20660e81 Mon Sep 17 00:00:00 2001 From: adam-urbanczyk Date: Fri, 27 Sep 2024 08:48:44 +0200 Subject: [PATCH 12/80] paramAt test --- tests/test_shapes.py | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 tests/test_shapes.py diff --git a/tests/test_shapes.py b/tests/test_shapes.py new file mode 100644 index 000000000..862e14869 --- /dev/null +++ b/tests/test_shapes.py @@ -0,0 +1,37 @@ +from cadquery.occ_impl.shapes import wire, segment, polyline, Vector + +from pytest import approx + + +def test_paramAt(): + + # paramAt for a segment + e = segment((0, 0), (0, 1)) + + p1 = e.paramAt(Vector(0, 0)) + p2 = e.paramAt(Vector(-1, 0)) + p3 = e.paramAt(Vector(0, 1)) + + assert p1 == approx(p2) + assert p1 == approx(0) + assert p3 == approx(e.paramAt(1)) + + # paramAt for a simple wire + w1 = wire(e) + + p4 = w1.paramAt(Vector(0, 0)) + p5 = w1.paramAt(Vector(0, 1)) + + assert p4 == approx(p1) + assert p5 == approx(p3) + + # paramAt for a complex wire + w2 = polyline((0, 0), (0, 1), (1, 1)) + + p6 = w2.paramAt(Vector(0, 0)) + p7 = w2.paramAt(Vector(0, 1)) + p8 = w2.paramAt(Vector(0.1, 0.1)) + + assert p6 == approx(w2.paramAt(0)) + assert p7 == approx(w2.paramAt(0.5)) + assert p8 == approx(w2.paramAt(0.1 / 2)) From a39a8cc2743c318f4dbe24d533146a6c0eda358d Mon Sep 17 00:00:00 2001 From: adam-urbanczyk Date: Fri, 27 Sep 2024 08:54:59 +0200 Subject: [PATCH 13/80] More tests --- tests/test_shapes.py | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/tests/test_shapes.py b/tests/test_shapes.py index 862e14869..37840714c 100644 --- a/tests/test_shapes.py +++ b/tests/test_shapes.py @@ -1,4 +1,12 @@ -from cadquery.occ_impl.shapes import wire, segment, polyline, Vector +from cadquery.occ_impl.shapes import ( + wire, + segment, + polyline, + Vector, + box, + Solid, + compound, +) from pytest import approx @@ -35,3 +43,21 @@ def test_paramAt(): assert p6 == approx(w2.paramAt(0)) assert p7 == approx(w2.paramAt(0.5)) assert p8 == approx(w2.paramAt(0.1 / 2)) + + +def test_isSolid(): + + s = box(1, 1, 1) + + assert Solid.isSolid(s) + assert Solid.isSolid(compound(s)) + assert not Solid.isSolid(s.faces()) + + +def test_shells(): + + s = box(2, 2, 2) - box(1, 1, 1).moved(z=0.5) + + assert s.outerShell().Area() == approx(6 * 4) + assert len(s.innerShells()) == 1 + assert s.innerShells()[0].Area() == approx(6 * 1) From c6d372e8cf0bb4681ebc3758b56286a914b99df5 Mon Sep 17 00:00:00 2001 From: adam-urbanczyk Date: Tue, 1 Oct 2024 08:52:55 +0200 Subject: [PATCH 14/80] Add orientation fix and optimize check --- cadquery/occ_impl/shapes.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/cadquery/occ_impl/shapes.py b/cadquery/occ_impl/shapes.py index 2241c7cf9..f4586bb7f 100644 --- a/cadquery/occ_impl/shapes.py +++ b/cadquery/occ_impl/shapes.py @@ -4482,14 +4482,13 @@ def solid(s: Sequence[Shape], inner: Optional[Sequence[Shape]] = None) -> Shape: if inner: for sh in _get(shell(*inner), "Shell"): - sh_inverted = TopoDS.Shell_s( - sh.wrapped.Oriented(TopAbs_Orientation.TopAbs_REVERSED) - ) - builder.Add(sh_inverted) + builder.Add(sh.wrapped) - rv = builder.Solid() + # fix orientations + sf = ShapeFix_Solid(builder.Solid()) + sf.Perform() - return _compound_or_shape(rv) + return _compound_or_shape(sf.Solid()) @multimethod @@ -5190,6 +5189,11 @@ def check(s: Shape) -> bool: """ analyzer = BRepAlgoAPI_Check(s.wrapped) + analyzer.SetRunParallel(True) + analyzer.SetUseOBB(True) + + analyzer.Perform() + rv = analyzer.IsValid() return rv From 06aa44fcda91a983c08c9f7a44d5ceb3a7b8ed82 Mon Sep 17 00:00:00 2001 From: adam-urbanczyk Date: Tue, 1 Oct 2024 17:52:03 +0200 Subject: [PATCH 15/80] Switch to miniforge --- appveyor.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/appveyor.yml b/appveyor.yml index c1f71ee3d..171ed7316 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -16,14 +16,14 @@ environment: secure: $(anaconda_token) init: - - cmd: curl -fsSLo Miniforge.exe https://github.com/conda-forge/miniforge/releases/latest/download/Mambaforge-Windows-x86_64.exe + - cmd: curl -fsSLo Miniforge.exe https://github.com/conda-forge/miniforge/releases/latest/download/Miniforge3-Windows-x86_64.exe - cmd: Miniforge.exe /InstallationType=JustMe /RegisterPython=0 /S /D=%MINICONDA_DIRNAME% - cmd: set "PATH=%MINICONDA_DIRNAME%;%MINICONDA_DIRNAME%\\Scripts;%PATH%" - cmd: activate - - sh: curl -sL https://github.com/conda-forge/miniforge/releases/latest/download/Mambaforge-$OS-x86_64.sh > miniconda.sh + - sh: curl -sL https://github.com/conda-forge/miniforge/releases/latest/download/Miniforge3-$OS-x86_64.sh > miniconda.sh - sh: bash miniconda.sh -b -p $HOME/miniconda; - sh: export PATH="$HOME/miniconda/bin:$HOME/miniconda/lib:$PATH"; - - sh: source $HOME/miniconda/bin/activate + - sh: source $HOME/miniconda/bin/activate install: - mamba env create -f environment.yml From de5cc766ee8d47082b7086b5f91e9f426e0702f1 Mon Sep 17 00:00:00 2001 From: AU Date: Tue, 8 Oct 2024 07:48:20 +0200 Subject: [PATCH 16/80] Add missing newline --- cadquery/occ_impl/shapes.py | 1 + 1 file changed, 1 insertion(+) diff --git a/cadquery/occ_impl/shapes.py b/cadquery/occ_impl/shapes.py index f4586bb7f..890cc1eb9 100644 --- a/cadquery/occ_impl/shapes.py +++ b/cadquery/occ_impl/shapes.py @@ -2557,6 +2557,7 @@ def fillet( ) -> "Wire": """ Apply 2D or 3D fillet to a wire + :param wire: The input wire to fillet. Currently only open wires are supported :param radius: the radius of the fillet, must be > zero :param vertices: Optional list of vertices to fillet. By default all vertices are fillet. From a894918c8ff8e3989af302fa7ab4ced066aacdac Mon Sep 17 00:00:00 2001 From: adam-urbanczyk Date: Sun, 20 Oct 2024 18:26:52 +0200 Subject: [PATCH 17/80] Use OBB (perf) and modified frenet --- cadquery/occ_impl/shapes.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/cadquery/occ_impl/shapes.py b/cadquery/occ_impl/shapes.py index f4586bb7f..6815ad9d5 100644 --- a/cadquery/occ_impl/shapes.py +++ b/cadquery/occ_impl/shapes.py @@ -4803,6 +4803,7 @@ def _bool_op( builder.SetTools(tool) builder.SetRunParallel(parallel) + builder.SetUseOBB(True) if tol: builder.SetFuzzyValue(tol) @@ -5038,6 +5039,7 @@ def sweep(s: Shape, path: Shape, cap: bool = False) -> Shape: else: for w in _get_wires(s): builder = BRepOffsetAPI_MakePipeShell(spine.wrapped) + builder.SetMode(False) builder.Add(w.wrapped, False, False) builder.Build() @@ -5064,6 +5066,7 @@ def sweep(s: Sequence[Shape], path: Shape, cap: bool = False) -> Shape: for el in _get_face_lists(s): # build outer part builder = BRepOffsetAPI_MakePipeShell(spine.wrapped) + builder.SetMode(False) for f in el: builder.Add(f.outerWire().wrapped, False, False) @@ -5077,6 +5080,7 @@ def sweep(s: Sequence[Shape], path: Shape, cap: bool = False) -> Shape: # initialize builders for w in el[0].innerWires(): builder_inner = BRepOffsetAPI_MakePipeShell(spine.wrapped) + builder_inner.SetMode(False) builder_inner.Add(w.wrapped, False, False) builders_inner.append(builder_inner) From 6f3e93e495e714f08202733eedcebf6001449925 Mon Sep 17 00:00:00 2001 From: adam-urbanczyk Date: Mon, 21 Oct 2024 21:51:54 +0200 Subject: [PATCH 18/80] overload normalAt for parameters --- cadquery/occ_impl/shapes.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/cadquery/occ_impl/shapes.py b/cadquery/occ_impl/shapes.py index 73347293f..dbaeda83b 100644 --- a/cadquery/occ_impl/shapes.py +++ b/cadquery/occ_impl/shapes.py @@ -2677,6 +2677,7 @@ def _uvBounds(self) -> Tuple[float, float, float, float]: return BRepTools.UVBounds_s(self.wrapped) + @multimethod def normalAt(self, locationVector: Optional[Vector] = None) -> Vector: """ Computes the normal vector at the desired location on the face. @@ -2704,6 +2705,24 @@ def normalAt(self, locationVector: Optional[Vector] = None) -> Vector: return Vector(vn) + @normalAt.register + def normalAt(self, u: Real, v: Real) -> Tuple[Vector, Vector]: + """ + Computes the normal vector at the desired location in the u,v parameter space. + + :returns: a vector representing the normal direction and the position + :param u: the u parametric location to compute the normal at. + :type u: real. + :param v: the v parametric location to compute the normal at. + :type u: real. + """ + + p = gp_Pnt() + vn = gp_Vec() + BRepGProp_Face(self.wrapped).Normal(u, v, p, vn) + + return Vector(vn).normalized(), Vector(p) + def Center(self) -> Vector: Properties = GProp_GProps() From aabe46de3b6479892d2dc0f359be6045218dad83 Mon Sep 17 00:00:00 2001 From: adam-urbanczyk Date: Mon, 21 Oct 2024 22:04:23 +0200 Subject: [PATCH 19/80] bulk normals calculation --- cadquery/occ_impl/shapes.py | 29 +++++++++++++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/cadquery/occ_impl/shapes.py b/cadquery/occ_impl/shapes.py index dbaeda83b..408a8812d 100644 --- a/cadquery/occ_impl/shapes.py +++ b/cadquery/occ_impl/shapes.py @@ -2712,9 +2712,7 @@ def normalAt(self, u: Real, v: Real) -> Tuple[Vector, Vector]: :returns: a vector representing the normal direction and the position :param u: the u parametric location to compute the normal at. - :type u: real. :param v: the v parametric location to compute the normal at. - :type u: real. """ p = gp_Pnt() @@ -2723,6 +2721,33 @@ def normalAt(self, u: Real, v: Real) -> Tuple[Vector, Vector]: return Vector(vn).normalized(), Vector(p) + def normals( + self, us: Iterable[Real], vs: Iterable[Real] + ) -> Tuple[List[Vector], List[Vector]]: + """ + Computes the normal vectors at the desired locations in the u,v parameter space. + + :returns: a tuple of list of vectors representing the normal directions and the positions + :param us: the u parametric locations to compute the normal at. + :param vs: the v parametric locations to compute the normal at. + """ + + rv_n = [] + rv_p = [] + + p = gp_Pnt() + vn = gp_Vec() + BGP = BRepGProp_Face() + + for u, v in zip(us, vs): + BGP.Load(self.wrapped) + BGP.Normal(u, v, p, vn) + + rv_n.append(Vector(vn).normalized()) + rv_p.append(Vector(p)) + + return rv_n, rv_p + def Center(self) -> Vector: Properties = GProp_GProps() From 0446dcfb3a445603bf6ca731b30ffbd1d80a67dd Mon Sep 17 00:00:00 2001 From: adam-urbanczyk Date: Mon, 21 Oct 2024 22:12:35 +0200 Subject: [PATCH 20/80] workaround for bad fonts --- 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 408a8812d..744617671 100644 --- a/cadquery/occ_impl/shapes.py +++ b/cadquery/occ_impl/shapes.py @@ -4825,7 +4825,7 @@ def text( font_i, NCollection_Utf8String(txt), theHAlign=theHAlign, theVAlign=theVAlign ) - return _compound_or_shape(rv) + return clean(fuse(_compound_or_shape(rv).faces())) #%% ops From 8179a00e1ae723afb6898c6f9b9236087256a953 Mon Sep 17 00:00:00 2001 From: adam-urbanczyk Date: Tue, 22 Oct 2024 08:47:43 +0200 Subject: [PATCH 21/80] first pass at loft to vertex --- cadquery/occ_impl/shapes.py | 65 ++++++++++++++++++++++++++++--------- 1 file changed, 49 insertions(+), 16 deletions(-) diff --git a/cadquery/occ_impl/shapes.py b/cadquery/occ_impl/shapes.py index 744617671..b77e01569 100644 --- a/cadquery/occ_impl/shapes.py +++ b/cadquery/occ_impl/shapes.py @@ -4356,16 +4356,40 @@ def _get_wire_lists(s: Sequence[Shape]) -> List[List[Wire]]: return wire_lists -def _get_face_lists(s: Sequence[Shape]) -> List[List[Face]]: +def _get_face_lists(s: Sequence[Shape]) -> List[List[Union[Face, Vertex]]]: """ - Get lists of faces for sweeping or lofting. + Get lists of faces for sweeping or lofting. First and last shape can be a vertex """ - face_lists: List[List[Face]] = [] + face_lists: List[List[Union[Face, Vertex]]] = [] + + ix_last = len(s) - 1 + + for i, el in enumerate(s): + if i == 0: - for el in s: - if not face_lists: face_lists = [[f] for f in el.Faces()] + + # if no faces were detected, try vertices + if not face_lists: + face_lists = [[v] for v in el.Vertices()] + + # if not faces and vertices were detected throw + if not face_lists: + raise ValueError("Either Faces of Verties are required in {*el}") + + elif i == ix_last: + + # try to add faces + faces = el.Faces() + + if len(faces) == len(face_lists): + for face_list, f in zip(face_lists, faces): + face_list.append(f) + else: + for face_list, f in zip(face_lists, el.Vertices()): + face_list.append(f) + else: for face_list, f in zip(face_lists, el.Faces()): face_list.append(f) @@ -5178,25 +5202,34 @@ def loft(s: Sequence[Shape], cap: bool = False, ruled: bool = False) -> Shape: # build outer part builder.Init(True, ruled) + # used to check is building inner parts makes sense + has_vertex = False + for f in el: - builder.AddWire(f.outerWire().wrapped) + if isinstance(f, Face): + builder.AddWire(f.outerWire().wrapped) + else: + builder.AddVertex(f.wrapped) + has_vertex = True builder.Build() builder.Check() builders_inner = [] - # initialize builders - for w in el[0].innerWires(): - builder_inner = BRepOffsetAPI_ThruSections() - builder_inner.Init(True, ruled) - builder_inner.AddWire(w.wrapped) - builders_inner.append(builder_inner) - - # add remaining sections - for f in el[1:]: - for builder_inner, w in zip(builders_inner, f.innerWires()): + # only initialize inner builders if no vertex was encountered + if not has_vertex: + # initialize builders + for w in el[0].innerWires(): + builder_inner = BRepOffsetAPI_ThruSections() + builder_inner.Init(True, ruled) builder_inner.AddWire(w.wrapped) + builders_inner.append(builder_inner) + + # add remaining sections + for f in el[1:]: + for builder_inner, w in zip(builders_inner, f.innerWires()): + builder_inner.AddWire(w.wrapped) # actually build inner_parts = [] From a3e87af7d072246d331a33a278c8d607d767292a Mon Sep 17 00:00:00 2001 From: adam-urbanczyk Date: Tue, 22 Oct 2024 20:58:36 +0200 Subject: [PATCH 22/80] Add trim and more loft to vertex tweaks --- cadquery/occ_impl/shapes.py | 69 +++++++++++++++++++++++++++++++------ 1 file changed, 58 insertions(+), 11 deletions(-) diff --git a/cadquery/occ_impl/shapes.py b/cadquery/occ_impl/shapes.py index b77e01569..957b4d340 100644 --- a/cadquery/occ_impl/shapes.py +++ b/cadquery/occ_impl/shapes.py @@ -2001,6 +2001,15 @@ def arcCenter(self) -> Vector: return rv + def trim(self, u0: Real, u1: Real) -> "Edge": + """ + Trim the edge in the parametric space to (u0, u1). + """ + + bldr = BRepBuilderAPI_MakeEdge(self._geomAdaptor().Curve().Curve(), u0, u1) + + return self.__class__(bldr.Shape()) + @classmethod def makeCircle( cls, @@ -2737,10 +2746,9 @@ def normals( p = gp_Pnt() vn = gp_Vec() - BGP = BRepGProp_Face() + BGP = BRepGProp_Face(self.wrapped) for u, v in zip(us, vs): - BGP.Load(self.wrapped) BGP.Normal(u, v, p, vn) rv_n.append(Vector(vn).normalized()) @@ -3066,6 +3074,15 @@ def toArcs(self, tolerance: float = 1e-3) -> "Face": return self.__class__(BRepAlgo.ConvertFace_s(self.wrapped, tolerance)) + def trim(self, u0: Real, u1: Real, v0: Real, v1: Real, tol: Real = 1e-6) -> "Face": + """ + Trim the face in the parametric space to (u0, u1). + """ + + bldr = BRepBuilderAPI_MakeFace(self._geomAdaptor(), u0, u1, v0, v1, tol) + + return self.__class__(bldr.Shape()) + class Shell(Shape): """ @@ -4344,11 +4361,32 @@ def _get_wire_lists(s: Sequence[Shape]) -> List[List[Wire]]: Get lists of wires for sweeping or lofting. """ - wire_lists: List[List[Wire]] = [] + wire_lists: List[List[Union[Wire, Vertex]]] = [] + + ix_last = len(s) - 1 + + for i, el in enumerate(s): + if i == 0: + + try: + wire_lists = [[w] for w in _get_wires(el)] + except ValueError: + # if no wires were detected, try vertices + wire_lists = [[v] for v in el.Vertices()] + + # if not faces and vertices were detected return an empty list + if not wire_lists: + break + + elif i == ix_last: + + try: + for wire_list, w in zip(wire_lists, _get_wires(el)): + wire_list.append(w) + except ValueError: + for wire_list, v in zip(wire_lists, el.Vertices()): + wire_list.append(v) - for el in s: - if not wire_lists: - wire_lists = [[w] for w in _get_wires(el)] else: for wire_list, w in zip(wire_lists, _get_wires(el)): wire_list.append(w) @@ -4358,7 +4396,7 @@ def _get_wire_lists(s: Sequence[Shape]) -> List[List[Wire]]: def _get_face_lists(s: Sequence[Shape]) -> List[List[Union[Face, Vertex]]]: """ - Get lists of faces for sweeping or lofting. First and last shape can be a vertex + Get lists of faces for sweeping or lofting. First and last shape can be a vertex. """ face_lists: List[List[Union[Face, Vertex]]] = [] @@ -4371,12 +4409,12 @@ def _get_face_lists(s: Sequence[Shape]) -> List[List[Union[Face, Vertex]]]: face_lists = [[f] for f in el.Faces()] # if no faces were detected, try vertices - if not face_lists: + if not face_lists and not el.edges(): face_lists = [[v] for v in el.Vertices()] - # if not faces and vertices were detected throw + # if not faces and vertices were detected return an empty list if not face_lists: - raise ValueError("Either Faces of Verties are required in {*el}") + break elif i == ix_last: @@ -4394,6 +4432,12 @@ def _get_face_lists(s: Sequence[Shape]) -> List[List[Union[Face, Vertex]]]: for face_list, f in zip(face_lists, el.Faces()): face_list.append(f) + # check if the result makes sense + if any( + isinstance(el[0], Vertex) and isinstance(el[1], Vertex) for el in face_lists + ): + return [] + return face_lists @@ -5247,7 +5291,10 @@ def loft(s: Sequence[Shape], cap: bool = False, ruled: bool = False) -> Shape: builder.Init(cap, ruled) for w in el: - builder.AddWire(w.wrapped) + if isinstance(w, Wire): + builder.AddWire(w.wrapped) + else: + builder.AddVertex(w.wrapped) builder.Build() builder.Check() From 9af29a2dc5b1b7335c6137aa536b8435b56227c6 Mon Sep 17 00:00:00 2001 From: adam-urbanczyk Date: Thu, 24 Oct 2024 00:53:58 +0200 Subject: [PATCH 23/80] Some mypy fixes --- cadquery/occ_impl/shapes.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/cadquery/occ_impl/shapes.py b/cadquery/occ_impl/shapes.py index 957b4d340..5ad3bd905 100644 --- a/cadquery/occ_impl/shapes.py +++ b/cadquery/occ_impl/shapes.py @@ -4356,7 +4356,7 @@ def _get_edges(s: Shape) -> Iterable[Shape]: raise ValueError(f"Required type(s): Edge, Wire; encountered {t}") -def _get_wire_lists(s: Sequence[Shape]) -> List[List[Wire]]: +def _get_wire_lists(s: Sequence[Shape]) -> List[List[Union[Wire, Vertex]]]: """ Get lists of wires for sweeping or lofting. """ @@ -4425,8 +4425,8 @@ def _get_face_lists(s: Sequence[Shape]) -> List[List[Union[Face, Vertex]]]: for face_list, f in zip(face_lists, faces): face_list.append(f) else: - for face_list, f in zip(face_lists, el.Vertices()): - face_list.append(f) + for face_list, v in zip(face_lists, el.Vertices()): + face_list.append(v) else: for face_list, f in zip(face_lists, el.Faces()): @@ -4893,7 +4893,7 @@ def text( font_i, NCollection_Utf8String(txt), theHAlign=theHAlign, theVAlign=theVAlign ) - return clean(fuse(_compound_or_shape(rv).faces())) + return clean(_compound_or_shape(rv).faces().fuse()) #%% ops From 4e4ef165ca5a7e7cf31fb7c9a98e26151b14bdbd Mon Sep 17 00:00:00 2001 From: adam-urbanczyk Date: Thu, 24 Oct 2024 22:37:03 +0200 Subject: [PATCH 24/80] Loft improvements and sewing fixes. --- cadquery/occ_impl/shapes.py | 52 ++++++++++++++++++++++++++++++------- 1 file changed, 43 insertions(+), 9 deletions(-) diff --git a/cadquery/occ_impl/shapes.py b/cadquery/occ_impl/shapes.py index 5ad3bd905..851d16e4a 100644 --- a/cadquery/occ_impl/shapes.py +++ b/cadquery/occ_impl/shapes.py @@ -4452,7 +4452,7 @@ def _normalize(s: Shape) -> Shape: if t == "Shell": faces = s.Faces() - if len(faces) == 1: + if len(faces) == 1 and not BRep_Tool.IsClosed_s(s.wrapped): rv = faces[0] elif t == "Compound": objs = list(s) @@ -4552,12 +4552,12 @@ def face(s: Sequence[Shape]) -> Shape: @multimethod -def shell(*s: Shape) -> Shape: +def shell(*s: Shape, tol: float = 1e-6) -> Shape: """ Build shell from faces. """ - builder = BRepBuilderAPI_Sewing() + builder = BRepBuilderAPI_Sewing(tol) for el in s: for f in _get(el, "Face"): @@ -4565,7 +4565,20 @@ def shell(*s: Shape) -> Shape: builder.Perform() - return _compound_or_shape(builder.SewedShape()) + sewed = builder.SewedShape() + + # for one fase sewing will not produce a shell + if sewed.ShapeType() == TopAbs_ShapeEnum.TopAbs_FACE: + rv = TopoDS_Shell() + + builder = TopoDS_Builder() + builder.MakeShell(rv) + builder.Add(rv, sewed) + + else: + rv = sewed + + return _compound_or_shape(rv) @shell.register @@ -5233,13 +5246,27 @@ def sweep(s: Sequence[Shape], path: Shape, cap: bool = False) -> Shape: @multimethod -def loft(s: Sequence[Shape], cap: bool = False, ruled: bool = False) -> Shape: +def loft( + s: Sequence[Shape], + cap: bool = False, + ruled: bool = False, + degree: int = 3, + compat: bool = True, +) -> Shape: """ Loft edges, wires or faces. For faces cap has no effect. Do not mix faces with other types. """ results = [] - builder = BRepOffsetAPI_ThruSections() + + def _make_builder(): + rv = BRepOffsetAPI_ThruSections() + rv.SetMaxDegree(degree) + rv.CheckCompatibility(compat) + + return rv + + builder = _make_builder() # try to construct lofts using faces for el in _get_face_lists(s): @@ -5265,7 +5292,8 @@ def loft(s: Sequence[Shape], cap: bool = False, ruled: bool = False) -> Shape: if not has_vertex: # initialize builders for w in el[0].innerWires(): - builder_inner = BRepOffsetAPI_ThruSections() + builder_inner = _make_builder() + builder_inner.Init(True, ruled) builder_inner.AddWire(w.wrapped) builders_inner.append(builder_inner) @@ -5305,9 +5333,15 @@ def loft(s: Sequence[Shape], cap: bool = False, ruled: bool = False) -> Shape: @loft.register -def loft(*s: Shape, cap: bool = False, ruled: bool = False) -> Shape: +def loft( + *s: Shape, + cap: bool = False, + ruled: bool = False, + degree: int = 3, + compat: bool = True, +) -> Shape: - return loft(s, cap, ruled) + return loft(s, cap, ruled, degree, compat) #%% diagnotics From 2707a7d4262157af459b3dcab585e2ceec340b1c Mon Sep 17 00:00:00 2001 From: adam-urbanczyk Date: Thu, 24 Oct 2024 22:37:23 +0200 Subject: [PATCH 25/80] Add loft to vertox to workplane --- cadquery/cq.py | 37 ++++++++++++++++++++++++++++++++----- 1 file changed, 32 insertions(+), 5 deletions(-) diff --git a/cadquery/cq.py b/cadquery/cq.py index 0334aade1..569d49edd 100644 --- a/cadquery/cq.py +++ b/cadquery/cq.py @@ -42,6 +42,7 @@ from .occ_impl.geom import Vector, Plane, Location from .occ_impl.shapes import ( Shape, + Vertex, Edge, Wire, Face, @@ -49,6 +50,7 @@ Compound, wiresToFaces, Shapes, + loft, ) from .occ_impl.exporters.svg import getSVG, exportSVG @@ -3697,15 +3699,20 @@ def loft( """ + toLoft: List[Union[Wire, Vertex]] = [] + if self.ctx.pendingWires: - wiresToLoft = self.ctx.popPendingWires() + toLoft.extend(self.ctx.popPendingWires()) else: - wiresToLoft = [f.outerWire() for f in self._getFaces()] + toLoft = [ + el if isinstance(el, Vertex) else el.outerWire() + for el in self._getFacesVertices() + ] - if not wiresToLoft: + if not toLoft: raise ValueError("Nothing to loft") - r: Shape = Solid.makeLoft(wiresToLoft, ruled) + r: Shape = loft(toLoft, ruled) newS = self._combineWithBase(r, combine, clean) @@ -3731,6 +3738,26 @@ def _getFaces(self) -> List[Face]: return rv + def _getFacesVertices(self) -> List[Union[Face, Vertex]]: + """ + Convert pending wires or sketches to faces/vertices for subsequent operation + """ + + rv: List[Union[Face, Vertex]] = [] + + for el in self.objects: + if isinstance(el, Sketch): + rv.extend(el) + elif isinstance(el, (Face, Vertex)): + rv.append(el) + elif isinstance(el, Compound): + rv.extend(subel for subel in el if isinstance(subel, (Face, Vertex))) + + if not rv: + rv.extend(wiresToFaces(self.ctx.popPendingWires())) + + return rv + def _extrude( self, distance: Optional[float] = None, @@ -4574,7 +4601,7 @@ def export( ) -> T: """ Export Workplane to file. - + :param path: Filename. :param tolerance: the deflection tolerance, in model units. Default 0.1. :param angularTolerance: the angular tolerance, in radians. Default 0.1. From 36009e18a022772f3f8f7f7d51cac556984255e6 Mon Sep 17 00:00:00 2001 From: adam-urbanczyk Date: Thu, 24 Oct 2024 22:38:07 +0200 Subject: [PATCH 26/80] Add dummy kwargs for better unified codebase support (raw / CQ-editor) --- cadquery/vis.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/cadquery/vis.py b/cadquery/vis.py index 352b36e24..4b9a1db01 100644 --- a/cadquery/vis.py +++ b/cadquery/vis.py @@ -1,7 +1,7 @@ from . import Shape, Workplane, Assembly, Sketch, Compound, Color from .occ_impl.exporters.assembly import _vtkRenderWindow -from typing import Union +from typing import Union, Any from OCP.TopoDS import TopoDS_Shape @@ -30,13 +30,15 @@ def _to_assy(*objs: Union[Shape, Workplane, Assembly, Sketch]) -> Assembly: return assy -def show(*objs: Union[Shape, Workplane, Assembly, Sketch]): +def show(*objs: Union[Shape, Workplane, Assembly, Sketch], **kwargs: Any): """ Show CQ objects using VTK """ # construct the assy - assy = _to_assy(*objs) + assy = _to_assy( + *(obj for obj in objs if isinstance(obj, (Shape, Workplane, Assembly, Sketch))) + ) # create a VTK window win = _vtkRenderWindow(assy) From c7e676d143c26da6d71b6aa7f90c4b622f07e01d Mon Sep 17 00:00:00 2001 From: adam-urbanczyk Date: Fri, 25 Oct 2024 00:07:25 +0200 Subject: [PATCH 27/80] Add continuity --- cadquery/occ_impl/shapes.py | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/cadquery/occ_impl/shapes.py b/cadquery/occ_impl/shapes.py index 851d16e4a..e5f785285 100644 --- a/cadquery/occ_impl/shapes.py +++ b/cadquery/occ_impl/shapes.py @@ -4503,6 +4503,25 @@ def _shapes_to_toptools_list(s: Iterable[Shape]) -> TopTools_ListOfShape: return rv +_geomabsshape_dict = dict( + C0=GeomAbs_Shape.GeomAbs_C0, + C1=GeomAbs_Shape.GeomAbs_C1, + C2=GeomAbs_Shape.GeomAbs_C2, + C3=GeomAbs_Shape.GeomAbs_C3, + CN=GeomAbs_Shape.GeomAbs_CN, + G1=GeomAbs_Shape.GeomAbs_G1, + G2=GeomAbs_Shape.GeomAbs_G2, +) + + +def _to_geomabshape(name: str) -> GeomAbs_Shape: + """ + Convert a literal to GeomAbs_Shape enum (OCCT specific). + """ + + return _geomabsshape_dict[name.upper()] + + #%% alternative constructors @@ -4567,7 +4586,7 @@ def shell(*s: Shape, tol: float = 1e-6) -> Shape: sewed = builder.SewedShape() - # for one fase sewing will not produce a shell + # for one face sewing will not produce a shell if sewed.ShapeType() == TopAbs_ShapeEnum.TopAbs_FACE: rv = TopoDS_Shell() @@ -5250,6 +5269,7 @@ def loft( s: Sequence[Shape], cap: bool = False, ruled: bool = False, + continuity: Literal["C1", "C2", "C3"] = "C2", degree: int = 3, compat: bool = True, ) -> Shape: @@ -5263,6 +5283,7 @@ def _make_builder(): rv = BRepOffsetAPI_ThruSections() rv.SetMaxDegree(degree) rv.CheckCompatibility(compat) + rv.SetContinuity(_to_geomabshape(continuity)) return rv @@ -5337,6 +5358,7 @@ def loft( *s: Shape, cap: bool = False, ruled: bool = False, + continuity: Literal["C1", "C2", "C3"] = "C2", degree: int = 3, compat: bool = True, ) -> Shape: From ec7757e943e0c82885e1db25d7801799a6b5bfc9 Mon Sep 17 00:00:00 2001 From: adam-urbanczyk Date: Sun, 27 Oct 2024 10:00:11 +0100 Subject: [PATCH 28/80] Use seperate builders for loft --- cadquery/occ_impl/shapes.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/cadquery/occ_impl/shapes.py b/cadquery/occ_impl/shapes.py index e5f785285..b58426eac 100644 --- a/cadquery/occ_impl/shapes.py +++ b/cadquery/occ_impl/shapes.py @@ -5280,19 +5280,17 @@ def loft( results = [] def _make_builder(): - rv = BRepOffsetAPI_ThruSections() + rv = BRepOffsetAPI_ThruSections(cap, ruled) rv.SetMaxDegree(degree) rv.CheckCompatibility(compat) rv.SetContinuity(_to_geomabshape(continuity)) return rv - builder = _make_builder() - # try to construct lofts using faces for el in _get_face_lists(s): # build outer part - builder.Init(True, ruled) + builder = _make_builder() # used to check is building inner parts makes sense has_vertex = False @@ -5315,7 +5313,6 @@ def _make_builder(): for w in el[0].innerWires(): builder_inner = _make_builder() - builder_inner.Init(True, ruled) builder_inner.AddWire(w.wrapped) builders_inner.append(builder_inner) @@ -5337,7 +5334,7 @@ def _make_builder(): # otherwise construct using wires if not results: for el in _get_wire_lists(s): - builder.Init(cap, ruled) + builder = _make_builder() for w in el: if isinstance(w, Wire): @@ -5363,7 +5360,7 @@ def loft( compat: bool = True, ) -> Shape: - return loft(s, cap, ruled, degree, compat) + return loft(s, cap, ruled, continuity, degree, compat) #%% diagnotics From 3f96f181d6ba2c3e7d035b66f49445bfcc3026a9 Mon Sep 17 00:00:00 2001 From: adam-urbanczyk Date: Sun, 27 Oct 2024 14:39:56 +0100 Subject: [PATCH 29/80] Add parametrization to loft and aux spine to sweep --- cadquery/occ_impl/shapes.py | 63 ++++++++++++++++++++++++++++++------- 1 file changed, 52 insertions(+), 11 deletions(-) diff --git a/cadquery/occ_impl/shapes.py b/cadquery/occ_impl/shapes.py index b58426eac..f00f33833 100644 --- a/cadquery/occ_impl/shapes.py +++ b/cadquery/occ_impl/shapes.py @@ -275,6 +275,8 @@ from OCP.GeomConvert import GeomConvert_ApproxCurve +from OCP.Approx import Approx_ParametrizationType + from math import pi, sqrt, inf, radians, cos import warnings @@ -4522,6 +4524,21 @@ def _to_geomabshape(name: str) -> GeomAbs_Shape: return _geomabsshape_dict[name.upper()] +_parametrization_dict = dict( + uniform=Approx_ParametrizationType.Approx_IsoParametric, + chordal=Approx_ParametrizationType.Approx_ChordLength, + centripetal=Approx_ParametrizationType.Approx_Centripetal, +) + + +def _to_parametrization(name: str) -> Approx_ParametrizationType: + """ + Convert a literal to Approx_ParametrizationType enum (OCCT specific). + """ + + return _parametrization_dict[name.lower()] + + #%% alternative constructors @@ -5156,7 +5173,9 @@ def offset(s: Shape, t: float, cap=True, tol: float = 1e-6) -> Shape: @multimethod -def sweep(s: Shape, path: Shape, cap: bool = False) -> Shape: +def sweep( + s: Shape, path: Shape, aux: Optional[Shape] = None, cap: bool = False +) -> Shape: """ Sweep edge, wire or face along a path. For faces cap has no effect. Do not mix faces with other types. @@ -5166,26 +5185,36 @@ def sweep(s: Shape, path: Shape, cap: bool = False) -> Shape: results = [] + def _make_builder(): + + rv = BRepOffsetAPI_MakePipeShell(spine.wrapped) + if aux: + rv.SetMode(_get_one_wire(aux).wrapped, True) + else: + rv.SetMode(False) + + return rv + # try to get faces faces = s.Faces() # if faces were supplied if faces: for f in faces: - tmp = sweep(f.outerWire(), path, True) + tmp = sweep(f.outerWire(), path, aux, True) # if needed subtract two sweeps inner_wires = f.innerWires() if inner_wires: - tmp -= sweep(compound(inner_wires), path, True) + tmp -= sweep(compound(inner_wires), path, aux, True) results.append(tmp.wrapped) # otherwise sweep wires else: for w in _get_wires(s): - builder = BRepOffsetAPI_MakePipeShell(spine.wrapped) - builder.SetMode(False) + builder = _make_builder() + builder.Add(w.wrapped, False, False) builder.Build() @@ -5198,7 +5227,9 @@ def sweep(s: Shape, path: Shape, cap: bool = False) -> Shape: @sweep.register -def sweep(s: Sequence[Shape], path: Shape, cap: bool = False) -> Shape: +def sweep( + s: Sequence[Shape], path: Shape, aux: Optional[Shape] = None, cap: bool = False +) -> Shape: """ Sweep edges, wires or faces along a path, multiple sections are supported. For faces cap has no effect. Do not mix faces with other types. @@ -5208,11 +5239,20 @@ def sweep(s: Sequence[Shape], path: Shape, cap: bool = False) -> Shape: results = [] + def _make_builder(): + + rv = BRepOffsetAPI_MakePipeShell(spine.wrapped) + if aux: + rv.SetMode(_get_one_wire(aux).wrapped, True) + else: + rv.SetMode(False) + + return rv + # try to construct sweeps using faces for el in _get_face_lists(s): # build outer part - builder = BRepOffsetAPI_MakePipeShell(spine.wrapped) - builder.SetMode(False) + builder = _make_builder() for f in el: builder.Add(f.outerWire().wrapped, False, False) @@ -5225,8 +5265,7 @@ def sweep(s: Sequence[Shape], path: Shape, cap: bool = False) -> Shape: # initialize builders for w in el[0].innerWires(): - builder_inner = BRepOffsetAPI_MakePipeShell(spine.wrapped) - builder_inner.SetMode(False) + builder_inner = _make_builder() builder_inner.Add(w.wrapped, False, False) builders_inner.append(builder_inner) @@ -5249,7 +5288,7 @@ def sweep(s: Sequence[Shape], path: Shape, cap: bool = False) -> Shape: if not results: # construct sweeps for el in _get_wire_lists(s): - builder = BRepOffsetAPI_MakePipeShell(spine.wrapped) + builder = _make_builder() for w in el: builder.Add(w.wrapped, False, False) @@ -5270,6 +5309,7 @@ def loft( cap: bool = False, ruled: bool = False, continuity: Literal["C1", "C2", "C3"] = "C2", + parametrization: Literal["uniform", "chordal", "centripetal"] = "uniform", degree: int = 3, compat: bool = True, ) -> Shape: @@ -5284,6 +5324,7 @@ def _make_builder(): rv.SetMaxDegree(degree) rv.CheckCompatibility(compat) rv.SetContinuity(_to_geomabshape(continuity)) + rv.SetParType(_to_parametrization(parametrization)) return rv From d79f171866dc8503b6e52caf74e0168fb5fcd88a Mon Sep 17 00:00:00 2001 From: adam-urbanczyk Date: Sun, 27 Oct 2024 14:56:30 +0100 Subject: [PATCH 30/80] Add params to spline interpolation --- cadquery/occ_impl/shapes.py | 27 +++++++++++++++++++++++---- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/cadquery/occ_impl/shapes.py b/cadquery/occ_impl/shapes.py index f00f33833..7561fe4de 100644 --- a/cadquery/occ_impl/shapes.py +++ b/cadquery/occ_impl/shapes.py @@ -4479,19 +4479,32 @@ def _compound_or_shape(s: Union[TopoDS_Shape, List[TopoDS_Shape]]) -> Shape: return rv -def _pts_to_harray(ps: Sequence[VectorLike]) -> TColgp_HArray1OfPnt: +def _pts_to_harray(pts: Sequence[VectorLike]) -> TColgp_HArray1OfPnt: """ Convert a sequence of Vecotor to a TColgp harray (OCCT specific). """ - rv = TColgp_HArray1OfPnt(1, len(ps)) + rv = TColgp_HArray1OfPnt(1, len(pts)) - for i, p in enumerate(ps): + for i, p in enumerate(pts): rv.SetValue(i + 1, Vector(p).toPnt()) return rv +def _floats_to_harray(vals: Sequence[float]) -> TColStd_HArray1OfReal: + """ + Convert a sequence of floats to a TColstd harray (OCCT specific). + """ + + rv = TColStd_HArray1OfReal(1, len(vals)) + + for i, val in enumerate(vals): + rv.SetValue(i + 1, val) + + return rv + + def _shapes_to_toptools_list(s: Iterable[Shape]) -> TopTools_ListOfShape: """ Convert an iterable of Shape to a TopTools list (OCCT specific). @@ -4761,6 +4774,7 @@ def spline(*pts: VectorLike, tol: float = 1e-6, periodic: bool = False) -> Shape def spline( pts: Sequence[VectorLike], tgts: Sequence[VectorLike] = (), + params: Sequence[float] = (), tol: float = 1e-6, periodic: bool = False, scale: bool = True, @@ -4768,7 +4782,12 @@ def spline( data = _pts_to_harray(pts) - builder = GeomAPI_Interpolate(data, periodic, tol) + if params: + args = (data, _floats_to_harray(params), periodic, tol) + else: + args = (data, periodic, tol) + + builder = GeomAPI_Interpolate(*args) if tgts: builder.Load(Vector(tgts[0]).wrapped, Vector(tgts[1]).wrapped, scale) From d00f113cb0952e15738ac3067adf9cb133e27b99 Mon Sep 17 00:00:00 2001 From: adam-urbanczyk Date: Thu, 31 Oct 2024 20:13:34 +0100 Subject: [PATCH 31/80] Add missing arg --- 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 7561fe4de..a00663fa6 100644 --- a/cadquery/occ_impl/shapes.py +++ b/cadquery/occ_impl/shapes.py @@ -5416,11 +5416,12 @@ def loft( cap: bool = False, ruled: bool = False, continuity: Literal["C1", "C2", "C3"] = "C2", + parametrization: Literal["uniform", "chordal", "centripetal"] = "uniform", degree: int = 3, compat: bool = True, ) -> Shape: - return loft(s, cap, ruled, continuity, degree, compat) + return loft(s, cap, ruled, continuity, parametrization, degree, compat) #%% diagnotics From 64d88af241981b754abcad3f6cdb92ce12ad9d81 Mon Sep 17 00:00:00 2001 From: adam-urbanczyk Date: Fri, 1 Nov 2024 17:17:33 +0100 Subject: [PATCH 32/80] Add tol and smoothing --- cadquery/occ_impl/shapes.py | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/cadquery/occ_impl/shapes.py b/cadquery/occ_impl/shapes.py index a00663fa6..5a95e96a8 100644 --- a/cadquery/occ_impl/shapes.py +++ b/cadquery/occ_impl/shapes.py @@ -4631,13 +4631,13 @@ def shell(*s: Shape, tol: float = 1e-6) -> Shape: @shell.register -def shell(s: Sequence[Shape]) -> Shape: +def shell(s: Sequence[Shape], tol: float = 1e-6) -> Shape: - return shell(*s) + return shell(*s, tol=tol) @multimethod -def solid(*s: Shape) -> Shape: +def solid(*s: Shape, tol: float = 1e-6) -> Shape: """ Build solid from faces. """ @@ -4645,19 +4645,21 @@ def solid(*s: Shape) -> Shape: builder = ShapeFix_Solid() faces = [f for el in s for f in _get(el, "Face")] - rv = builder.SolidFromShell(shell(*faces).wrapped) + rv = builder.SolidFromShell(shell(*faces, tol=tol).wrapped) return _compound_or_shape(rv) @solid.register -def solid(s: Sequence[Shape], inner: Optional[Sequence[Shape]] = None) -> Shape: +def solid( + s: Sequence[Shape], inner: Optional[Sequence[Shape]] = None, tol: float = 1e-6 +) -> Shape: builder = BRepBuilderAPI_MakeSolid() - builder.Add(shell(*s).wrapped) + builder.Add(shell(*s, tol=tol).wrapped) if inner: - for sh in _get(shell(*inner), "Shell"): + for sh in _get(shell(*inner, tol=tol), "Shell"): builder.Add(sh.wrapped) # fix orientations @@ -5331,6 +5333,8 @@ def loft( parametrization: Literal["uniform", "chordal", "centripetal"] = "uniform", degree: int = 3, compat: bool = True, + smoothing: bool = False, + weights: Tuple[float, float, float] = (1, 1, 1), ) -> Shape: """ Loft edges, wires or faces. For faces cap has no effect. Do not mix faces with other types. @@ -5344,6 +5348,8 @@ def _make_builder(): rv.CheckCompatibility(compat) rv.SetContinuity(_to_geomabshape(continuity)) rv.SetParType(_to_parametrization(parametrization)) + rv.SetSmoothing(smoothing) + rv.SetCriteriumWeight(*weights) return rv @@ -5419,6 +5425,8 @@ def loft( parametrization: Literal["uniform", "chordal", "centripetal"] = "uniform", degree: int = 3, compat: bool = True, + smoothing: bool = False, + weights: Tuple[float, float, float] = (1, 1, 1), ) -> Shape: return loft(s, cap, ruled, continuity, parametrization, degree, compat) From 0584b37620e149896ffec8e965da443742ad499a Mon Sep 17 00:00:00 2001 From: adam-urbanczyk Date: Thu, 7 Nov 2024 20:03:43 +0100 Subject: [PATCH 33/80] Fix cap handling --- cadquery/occ_impl/shapes.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/cadquery/occ_impl/shapes.py b/cadquery/occ_impl/shapes.py index 5a95e96a8..767d6cc45 100644 --- a/cadquery/occ_impl/shapes.py +++ b/cadquery/occ_impl/shapes.py @@ -5342,7 +5342,7 @@ def loft( results = [] - def _make_builder(): + def _make_builder(cap): rv = BRepOffsetAPI_ThruSections(cap, ruled) rv.SetMaxDegree(degree) rv.CheckCompatibility(compat) @@ -5356,7 +5356,7 @@ def _make_builder(): # try to construct lofts using faces for el in _get_face_lists(s): # build outer part - builder = _make_builder() + builder = _make_builder(True) # used to check is building inner parts makes sense has_vertex = False @@ -5377,7 +5377,7 @@ def _make_builder(): if not has_vertex: # initialize builders for w in el[0].innerWires(): - builder_inner = _make_builder() + builder_inner = _make_builder(True) builder_inner.AddWire(w.wrapped) builders_inner.append(builder_inner) @@ -5400,7 +5400,7 @@ def _make_builder(): # otherwise construct using wires if not results: for el in _get_wire_lists(s): - builder = _make_builder() + builder = _make_builder(cap) for w in el: if isinstance(w, Wire): From e5ff3bce7259acc1b98cd2faefb40ca1f5827e8a Mon Sep 17 00:00:00 2001 From: adam-urbanczyk Date: Thu, 7 Nov 2024 20:36:13 +0100 Subject: [PATCH 34/80] Fix crash --- cadquery/cq.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/cadquery/cq.py b/cadquery/cq.py index 569d49edd..2899304d4 100644 --- a/cadquery/cq.py +++ b/cadquery/cq.py @@ -3711,6 +3711,8 @@ def loft( if not toLoft: raise ValueError("Nothing to loft") + elif len(toLoft) == 1: + raise ValueError("More than one wire for face is required") r: Shape = loft(toLoft, ruled) From 8410af59e57c63487c2948d570e7c432bcdf6f8d Mon Sep 17 00:00:00 2001 From: adam-urbanczyk Date: Thu, 7 Nov 2024 21:08:04 +0100 Subject: [PATCH 35/80] Fix loft args --- cadquery/cq.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cadquery/cq.py b/cadquery/cq.py index 2899304d4..cbb0f6f31 100644 --- a/cadquery/cq.py +++ b/cadquery/cq.py @@ -3714,7 +3714,7 @@ def loft( elif len(toLoft) == 1: raise ValueError("More than one wire for face is required") - r: Shape = loft(toLoft, ruled) + r: Shape = loft(toLoft, cap=True, ruled=ruled) newS = self._combineWithBase(r, combine, clean) From 198013e16b120ba418f55a86eaa7ec43cff631a3 Mon Sep 17 00:00:00 2001 From: adam-urbanczyk Date: Thu, 7 Nov 2024 21:08:36 +0100 Subject: [PATCH 36/80] Typo --- cadquery/cq.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cadquery/cq.py b/cadquery/cq.py index cbb0f6f31..4882ae1b7 100644 --- a/cadquery/cq.py +++ b/cadquery/cq.py @@ -3712,7 +3712,7 @@ def loft( if not toLoft: raise ValueError("Nothing to loft") elif len(toLoft) == 1: - raise ValueError("More than one wire for face is required") + raise ValueError("More than one wire or face is required") r: Shape = loft(toLoft, cap=True, ruled=ruled) From e3021352f173c8ea748457eb9a2704897cdf68cd Mon Sep 17 00:00:00 2001 From: adam-urbanczyk Date: Thu, 7 Nov 2024 21:10:57 +0100 Subject: [PATCH 37/80] Fix test --- tests/test_cadquery.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_cadquery.py b/tests/test_cadquery.py index 57726aa3e..6e551fa60 100644 --- a/tests/test_cadquery.py +++ b/tests/test_cadquery.py @@ -509,7 +509,7 @@ def testLoftRaisesValueError(self): with self.assertRaises(ValueError) as cm: s1.loft() err = cm.exception - self.assertEqual(str(err), "More than one wire is required") + self.assertEqual(str(err), "More than one wire or face is required") def testLoftCombine(self): """ From 750ea3b5593988f66512b43f6fac4cc9f6013a4b Mon Sep 17 00:00:00 2001 From: adam-urbanczyk Date: Thu, 7 Nov 2024 21:29:09 +0100 Subject: [PATCH 38/80] Fix show test --- tests/test_vis.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_vis.py b/tests/test_vis.py index 43127085a..80c94bb3b 100644 --- a/tests/test_vis.py +++ b/tests/test_vis.py @@ -79,5 +79,5 @@ def test_show(wp, assy, sk, monkeypatch): show_object(wp, sk, assy, wp.val()) show_object() - with raises(ValueError): - show_object("a") + # for compatibility with CQ-editor + show_object(wp, "a") From bc5b987997a92592a026941de703c4d5320051dc Mon Sep 17 00:00:00 2001 From: adam-urbanczyk Date: Thu, 7 Nov 2024 21:41:28 +0100 Subject: [PATCH 39/80] 2nd show fix --- tests/test_vis.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/test_vis.py b/tests/test_vis.py index 80c94bb3b..9c70d7c42 100644 --- a/tests/test_vis.py +++ b/tests/test_vis.py @@ -69,9 +69,6 @@ def test_show(wp, assy, sk, monkeypatch): show(wp, sk, assy, wp.val()) show() - with raises(ValueError): - show(1) - show_object(wp) show_object(wp.val()) show_object(assy) @@ -81,3 +78,6 @@ def test_show(wp, assy, sk, monkeypatch): # for compatibility with CQ-editor show_object(wp, "a") + + # for now a workaround to be compatibile with more complaicated CQ-editor invocations + show(1) From 3a02fe083c0de14ff74593dac029e9732c35c5d0 Mon Sep 17 00:00:00 2001 From: adam-urbanczyk Date: Thu, 7 Nov 2024 23:03:59 +0100 Subject: [PATCH 40/80] Some loft tests --- tests/test_cadquery.py | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/tests/test_cadquery.py b/tests/test_cadquery.py index 6e551fa60..d5ea80dcf 100644 --- a/tests/test_cadquery.py +++ b/tests/test_cadquery.py @@ -5829,3 +5829,31 @@ def test_workplane_iter(self): assert len(list(w1)) == 0 assert len(list(w2)) == 2 # 2 beacuase __iter__ unpacks Compounds assert len(list(w3)) == 2 + + def test_loft_face(self): + + f1 = plane(1, 1) + f2 = face(circle(1)).moved(z=1) + + c = compound(f1, f2) + + w1 = Workplane().add(f1).add(f2).loft() + w2 = Workplane().add(c).loft() + + # in both cases we get a solid + assert w1.solids().size() == 1 + assert w2.solids().size() == 1 + + def test_loft_to_vertex(self): + + f1 = plane(1, 1) + v1 = vertex(0, 0, 1) + + c = compound(f1, v1) + + w1 = Workplane().add(f1).add(v1).loft() + w2 = Workplane().add(c).loft() + + # in both cases we get a solid + assert w1.solids().size() == 1 + assert w2.solids().size() == 1 From de6dd891c72617518fc1662ee31966e427447272 Mon Sep 17 00:00:00 2001 From: adam-urbanczyk Date: Fri, 8 Nov 2024 22:52:48 +0100 Subject: [PATCH 41/80] Logic fix --- cadquery/cq.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/cadquery/cq.py b/cadquery/cq.py index 4882ae1b7..0564136b2 100644 --- a/cadquery/cq.py +++ b/cadquery/cq.py @@ -3755,9 +3755,6 @@ def _getFacesVertices(self) -> List[Union[Face, Vertex]]: elif isinstance(el, Compound): rv.extend(subel for subel in el if isinstance(subel, (Face, Vertex))) - if not rv: - rv.extend(wiresToFaces(self.ctx.popPendingWires())) - return rv def _extrude( From 4b6e84d34a91f3bbf714986ad532fdbc3f1d1f60 Mon Sep 17 00:00:00 2001 From: adam-urbanczyk Date: Sat, 9 Nov 2024 13:47:43 +0100 Subject: [PATCH 42/80] Add curvature calculation --- cadquery/occ_impl/shapes.py | 120 ++++++++++++++++++++++++++---------- 1 file changed, 86 insertions(+), 34 deletions(-) diff --git a/cadquery/occ_impl/shapes.py b/cadquery/occ_impl/shapes.py index 767d6cc45..8d0e1b489 100644 --- a/cadquery/occ_impl/shapes.py +++ b/cadquery/occ_impl/shapes.py @@ -277,6 +277,8 @@ from OCP.Approx import Approx_ParametrizationType +from OCP.LProp3d import LProp3d_CLProps + from math import pi, sqrt, inf, radians, cos import warnings @@ -1638,6 +1640,10 @@ def makeVertex(cls, x: float, y: float, z: float) -> "Vertex": return cls(BRepBuilderAPI_MakeVertex(gp_Pnt(x, y, z)).Vertex()) +ParamMode = Literal["length", "parameter"] +FrameMode = Literal["frenet", "corrected"] + + class Mixin1DProtocol(ShapeProtocol, Protocol): def _approxCurve(self) -> Geom_BSplineCurve: ... @@ -1645,23 +1651,31 @@ def _approxCurve(self) -> Geom_BSplineCurve: def _geomAdaptor(self) -> Union[BRepAdaptor_Curve, BRepAdaptor_CompCurve]: ... + def _curve_and_param( + self, d: float, mode: ParamMode + ) -> Tuple[Union[BRepAdaptor_Curve, BRepAdaptor_CompCurve], float]: + ... + def paramAt(self, d: float) -> float: ... - def positionAt( - self, d: float, mode: Literal["length", "parameter"] = "length", - ) -> Vector: + def positionAt(self, d: float, mode: ParamMode = "length",) -> Vector: ... def locationAt( self, d: float, - mode: Literal["length", "parameter"] = "length", - frame: Literal["frenet", "corrected"] = "frenet", + mode: ParamMode = "length", + frame: FrameMode = "frenet", planar: bool = False, ) -> Location: ... + def curvatureAt( + self, d: float, mode: ParamMode = "length", resolution: float = 1e-6, + ) -> float: + ... + T1D = TypeVar("T1D", bound=Mixin1DProtocol) @@ -1738,9 +1752,7 @@ def paramAt(self: Mixin1DProtocol, d: Union[Real, Vector]) -> float: return rv def tangentAt( - self: Mixin1DProtocol, - locationParam: float = 0.5, - mode: Literal["length", "parameter"] = "length", + self: Mixin1DProtocol, locationParam: float = 0.5, mode: ParamMode = "length", ) -> Vector: """ Compute tangent vector at the specified location. @@ -1823,10 +1835,24 @@ def IsClosed(self: Mixin1DProtocol) -> bool: return BRep_Tool.IsClosed_s(self.wrapped) + def _curve_and_param( + self: Mixin1DProtocol, d: float, mode: ParamMode + ) -> Tuple[Union[BRepAdaptor_Curve, BRepAdaptor_CompCurve], float]: + """ + Helper that reurns the curve and u value + """ + + curve = self._geomAdaptor() + + if mode == "length": + param = self.paramAt(d) + else: + param = d + + return curve, param + def positionAt( - self: Mixin1DProtocol, - d: float, - mode: Literal["length", "parameter"] = "length", + self: Mixin1DProtocol, d: float, mode: ParamMode = "length", ) -> Vector: """Generate a position along the underlying curve. @@ -1835,19 +1861,12 @@ def positionAt( :return: A Vector on the underlying curve located at the specified d value. """ - curve = self._geomAdaptor() - - if mode == "length": - param = self.paramAt(d) - else: - param = d + curve, param = self._curve_and_param(d, mode) return Vector(curve.Value(param)) def positions( - self: Mixin1DProtocol, - ds: Iterable[float], - mode: Literal["length", "parameter"] = "length", + self: Mixin1DProtocol, ds: Iterable[float], mode: ParamMode = "length", ) -> List[Vector]: """Generate positions along the underlying curve @@ -1861,8 +1880,8 @@ def positions( def locationAt( self: Mixin1DProtocol, d: float, - mode: Literal["length", "parameter"] = "length", - frame: Literal["frenet", "corrected"] = "frenet", + mode: ParamMode = "length", + frame: FrameMode = "frenet", planar: bool = False, ) -> Location: """Generate a location along the underlying curve. @@ -1874,12 +1893,7 @@ def locationAt( :return: A Location object representing local coordinate system at the specified distance. """ - curve = self._geomAdaptor() - - if mode == "length": - param = self.paramAt(d) - else: - param = d + curve, param = self._curve_and_param(d, mode) law: GeomFill_TrihedronLaw if frame == "frenet": @@ -1909,8 +1923,8 @@ def locationAt( def locations( self: Mixin1DProtocol, ds: Iterable[float], - mode: Literal["length", "parameter"] = "length", - frame: Literal["frenet", "corrected"] = "frenet", + mode: ParamMode = "length", + frame: FrameMode = "frenet", planar: bool = False, ) -> List[Location]: """Generate location along the curve @@ -1958,6 +1972,44 @@ def project( return rv + def curvatureAt( + self: Mixin1DProtocol, + d: float, + mode: ParamMode = "length", + resolution: float = 1e-6, + ) -> float: + """ + Caulcate mean curvature along the underlying curve. + + :param d: distance or parameter value + :param mode: position calculation mode (default: length) + :param resolution: resolution of the calculation (default: 1e-6) + :return: mean curvature value at the specified d value. + """ + + curve, param = self._curve_and_param(d, mode) + + props = LProp3d_CLProps(curve, param, 2, resolution) + + return props.Curvature() + + def curvatures( + self: Mixin1DProtocol, + ds: Iterable[float], + mode: ParamMode = "length", + resolution: float = 1e-6, + ) -> List[float]: + """ + Caulcate mean curvatures along the underlying curve. + + :param d: distance or parameter values + :param mode: position calculation mode (default: length) + :param resolution: resolution of the calculation (default: 1e-6) + :return: mean curvature value at the specified d value. + """ + + return [self.curvatureAt(d, mode, resolution) for d in ds] + class Edge(Shape, Mixin1D): """ @@ -4775,8 +4827,8 @@ def spline(*pts: VectorLike, tol: float = 1e-6, periodic: bool = False) -> Shape @spline.register def spline( pts: Sequence[VectorLike], - tgts: Sequence[VectorLike] = (), - params: Sequence[float] = (), + tgts: Optional[Sequence[VectorLike]] = None, + params: Optional[Sequence[float]] = None, tol: float = 1e-6, periodic: bool = False, scale: bool = True, @@ -4784,14 +4836,14 @@ def spline( data = _pts_to_harray(pts) - if params: + if params is not None: args = (data, _floats_to_harray(params), periodic, tol) else: args = (data, periodic, tol) builder = GeomAPI_Interpolate(*args) - if tgts: + if tgts is not None: builder.Load(Vector(tgts[0]).wrapped, Vector(tgts[1]).wrapped, scale) builder.Perform() From 649f8f046d11ecd972b9f0226e75535d1e49cb44 Mon Sep 17 00:00:00 2001 From: adam-urbanczyk Date: Sat, 9 Nov 2024 14:54:56 +0100 Subject: [PATCH 43/80] Add multimethod docstrings --- cadquery/occ_impl/shapes.py | 58 +++++++++++++++++++++++++++++++++---- 1 file changed, 53 insertions(+), 5 deletions(-) diff --git a/cadquery/occ_impl/shapes.py b/cadquery/occ_impl/shapes.py index 8d0e1b489..b64af5d4e 100644 --- a/cadquery/occ_impl/shapes.py +++ b/cadquery/occ_impl/shapes.py @@ -1042,7 +1042,7 @@ def location(self) -> Location: def locate(self: T, loc: Location) -> T: """ - Apply a location in absolute sense to self + Apply a location in absolute sense to self. """ self.wrapped.Location(loc.wrapped) @@ -1051,7 +1051,7 @@ def locate(self: T, loc: Location) -> T: def located(self: T, loc: Location) -> T: """ - Apply a location in absolute sense to a copy of self + Apply a location in absolute sense to a copy of self. """ r = self.__class__(self.wrapped.Located(loc.wrapped)) @@ -1062,7 +1062,7 @@ def located(self: T, loc: Location) -> T: @multimethod def move(self: T, loc: Location) -> T: """ - Apply a location in relative sense (i.e. update current location) to self + Apply a location in relative sense (i.e. update current location) to self. """ self.wrapped.Move(loc.wrapped) @@ -1079,6 +1079,9 @@ def move( ry: Real = 0, rz: Real = 0, ) -> T: + """ + Apply translation and rotation in relative sense (i.e. update current location) to self. + """ self.wrapped.Move(Location(x, y, z, rx, ry, rz).wrapped) @@ -1086,6 +1089,9 @@ def move( @move.register def move(self: T, loc: VectorLike) -> T: + """ + Apply a VectorLike in relative sense (i.e. update current location) to self. + """ self.wrapped.Move(Location(loc).wrapped) @@ -1094,7 +1100,7 @@ def move(self: T, loc: VectorLike) -> T: @multimethod def moved(self: T, loc: Location) -> T: """ - Apply a location in relative sense (i.e. update current location) to a copy of self + Apply a location in relative sense (i.e. update current location) to a copy of self. """ r = self.__class__(self.wrapped.Moved(loc.wrapped)) @@ -1104,11 +1110,17 @@ def moved(self: T, loc: Location) -> T: @moved.register def moved(self: T, loc1: Location, loc2: Location, *locs: Location) -> T: + """ + Apply multiple locations. + """ return self.moved((loc1, loc2) + locs) @moved.register def moved(self: T, locs: Sequence[Location]) -> T: + """ + Apply multiple locations. + """ rv = [] @@ -1127,16 +1139,25 @@ def moved( ry: Real = 0, rz: Real = 0, ) -> T: + """ + Apply translation and rotation in relative sense to a copy of self. + """ return self.moved(Location(x, y, z, rx, ry, rz)) @moved.register def moved(self: T, loc: VectorLike) -> T: + """ + Apply a VectorLike in relative sense to a copy of self. + """ return self.moved(Location(loc)) @moved.register def moved(self: T, loc1: VectorLike, loc2: VectorLike, *locs: VectorLike) -> T: + """ + Apply multiple VectorLikes in relative sense to a copy of self. + """ return self.moved( (Location(loc1), Location(loc2)) + tuple(Location(loc) for loc in locs) @@ -1144,6 +1165,9 @@ def moved(self: T, loc1: VectorLike, loc2: VectorLike, *locs: VectorLike) -> T: @moved.register def moved(self: T, loc: Sequence[VectorLike]) -> T: + """ + Apply multiple VectorLikes in relative sense to a copy of self. + """ return self.moved(tuple(Location(l) for l in loc)) @@ -4648,6 +4672,9 @@ def face(*s: Shape) -> Shape: @face.register def face(s: Sequence[Shape]) -> Shape: + """ + Build face from a sequence of edges or wires. + """ return face(*s) @@ -4684,6 +4711,9 @@ def shell(*s: Shape, tol: float = 1e-6) -> Shape: @shell.register def shell(s: Sequence[Shape], tol: float = 1e-6) -> Shape: + """ + Build shell from a sequence of faces. + """ return shell(*s, tol=tol) @@ -4706,6 +4736,9 @@ def solid(*s: Shape, tol: float = 1e-6) -> Shape: def solid( s: Sequence[Shape], inner: Optional[Sequence[Shape]] = None, tol: float = 1e-6 ) -> Shape: + """ + Build solid from a sequence of faces. + """ builder = BRepBuilderAPI_MakeSolid() builder.Add(shell(*s, tol=tol).wrapped) @@ -4740,6 +4773,9 @@ def compound(*s: Shape) -> Shape: @compound.register def compound(s: Sequence[Shape]) -> Shape: + """ + Build compound from a sequence of shapes. + """ return compound(*s) @@ -4758,6 +4794,9 @@ def vertex(x: Real, y: Real, z: Real) -> Shape: @vertex.register def vertex(p: VectorLike): + """ + Construct a vertex from VectorLike. + """ return _compound_or_shape(BRepBuilderAPI_MakeVertex(Vector(p).toPnt()).Vertex()) @@ -4833,6 +4872,9 @@ def spline( periodic: bool = False, scale: bool = True, ) -> Shape: + """ + Construct a spline from a sequence points. + """ data = _pts_to_harray(pts) @@ -4942,7 +4984,7 @@ def torus(d1: float, d2: float) -> Shape: @multimethod def cone(d1: Real, d2: Real, h: Real) -> Shape: """ - Construct a solid cone. + Construct a partial solid cone. """ return _compound_or_shape( @@ -4958,6 +5000,9 @@ def cone(d1: Real, d2: Real, h: Real) -> Shape: @cone.register def cone(d: Real, h: Real) -> Shape: + """ + Construct a full solid cone. + """ return cone(d, 0, h) @@ -5480,6 +5525,9 @@ def loft( smoothing: bool = False, weights: Tuple[float, float, float] = (1, 1, 1), ) -> Shape: + """ + Variadic loft overload. + """ return loft(s, cap, ruled, continuity, parametrization, degree, compat) From f60ea782451df8a861f80dbf5618523ed5749773 Mon Sep 17 00:00:00 2001 From: adam-urbanczyk Date: Sat, 9 Nov 2024 14:55:20 +0100 Subject: [PATCH 44/80] Some tests --- tests/test_shapes.py | 47 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/tests/test_shapes.py b/tests/test_shapes.py index 37840714c..cde1de778 100644 --- a/tests/test_shapes.py +++ b/tests/test_shapes.py @@ -6,6 +6,9 @@ box, Solid, compound, + circle, + plane, + torus, ) from pytest import approx @@ -61,3 +64,47 @@ def test_shells(): assert s.outerShell().Area() == approx(6 * 4) assert len(s.innerShells()) == 1 assert s.innerShells()[0].Area() == approx(6 * 1) + + +def test_curvature(): + + r = 10 + + c = circle(r) + w = polyline((0, 0), (1, 0), (1, 1)) + + assert c.curvatureAt(0) == approx(1 / r) + + curvatures = c.curvatures([0, 0.5]) + + assert approx(curvatures[0]) == curvatures[1] + + assert w.curvatureAt(0) == approx(w.curvatureAt(0.5)) + + +def test_normals(): + + d1 = 10 + d2 = 1 + + (t,) = torus(d1, d2).faces() + + n1 = t.normalAt((d1, d2)) + n2 = t.normalAt((d1 + d2, 0)) + + assert approx(n1.toTuple()) == (0, 0, 1) + assert approx(n2.toTuple()) == (1, 0, 0) + + n2, n3 = t.normals([(d1, d2), (d1 + d2, 0)]) + + assert approx(n2.toTuple()) == (0, 0, 1) + assert approx(n3.toTuple()) == (1, 0, 0) + + +def test_trimming(): + + e = segment((0, 0), (0, 1)) + f = plane(1, 1) + + assert e.trim(0, 0.5).Length() == approx(e.Lengnth() / 2) + assert f.trim(0, 0.5, -0.5, 0.5).Area() == approx(f.Area() / 2) From 23a5c2ede1b0c531fa2dd0af1cd9c13e6e5450ff Mon Sep 17 00:00:00 2001 From: adam-urbanczyk Date: Sat, 9 Nov 2024 15:14:48 +0100 Subject: [PATCH 45/80] Add and fix tests --- cadquery/occ_impl/shapes.py | 8 +++++--- tests/test_shapes.py | 29 ++++++++++++++++++----------- 2 files changed, 23 insertions(+), 14 deletions(-) diff --git a/cadquery/occ_impl/shapes.py b/cadquery/occ_impl/shapes.py index b64af5d4e..74e3d387f 100644 --- a/cadquery/occ_impl/shapes.py +++ b/cadquery/occ_impl/shapes.py @@ -2765,7 +2765,7 @@ def _uvBounds(self) -> Tuple[float, float, float, float]: return BRepTools.UVBounds_s(self.wrapped) @multimethod - def normalAt(self, locationVector: Optional[Vector] = None) -> Vector: + def normalAt(self, locationVector: Optional[VectorLike] = None) -> Vector: """ Computes the normal vector at the desired location on the face. @@ -2782,7 +2782,9 @@ def normalAt(self, locationVector: Optional[Vector] = None) -> Vector: v = 0.5 * (v0 + v1) else: # project point on surface - projector = GeomAPI_ProjectPointOnSurf(locationVector.toPnt(), surface) + projector = GeomAPI_ProjectPointOnSurf( + Vector(locationVector).toPnt(), surface + ) u, v = projector.LowerDistanceParameters() @@ -2790,7 +2792,7 @@ def normalAt(self, locationVector: Optional[Vector] = None) -> Vector: vn = gp_Vec() BRepGProp_Face(self.wrapped).Normal(u, v, p, vn) - return Vector(vn) + return Vector(vn).normalized() @normalAt.register def normalAt(self, u: Real, v: Real) -> Tuple[Vector, Vector]: diff --git a/tests/test_shapes.py b/tests/test_shapes.py index cde1de778..576241071 100644 --- a/tests/test_shapes.py +++ b/tests/test_shapes.py @@ -13,6 +13,8 @@ from pytest import approx +from math import pi + def test_paramAt(): @@ -84,21 +86,26 @@ def test_curvature(): def test_normals(): - d1 = 10 - d2 = 1 + r1 = 10 + r2 = 1 + + t = torus(2 * r1, 2 * r2).faces() + + n1 = t.normalAt((r1, 0, r2)) + n2 = t.normalAt((r1 + r2, 0)) - (t,) = torus(d1, d2).faces() + assert n1.toTuple() == approx((0, 0, 1)) + assert n2.toTuple() == approx((1, 0, 0)) - n1 = t.normalAt((d1, d2)) - n2 = t.normalAt((d1 + d2, 0)) + n3, p3 = t.normalAt(0, 0) - assert approx(n1.toTuple()) == (0, 0, 1) - assert approx(n2.toTuple()) == (1, 0, 0) + assert n3.toTuple() == approx((1, 0, 0)) + assert p3.toTuple() == approx((r1 + r2, 0, 0)) - n2, n3 = t.normals([(d1, d2), (d1 + d2, 0)]) + (n4, n5), _ = t.normals((0, 0), (0, pi / 2)) - assert approx(n2.toTuple()) == (0, 0, 1) - assert approx(n3.toTuple()) == (1, 0, 0) + assert n4.toTuple() == approx((1, 0, 0)) + assert n5.toTuple() == approx((0, 0, 1)) def test_trimming(): @@ -106,5 +113,5 @@ def test_trimming(): e = segment((0, 0), (0, 1)) f = plane(1, 1) - assert e.trim(0, 0.5).Length() == approx(e.Lengnth() / 2) + assert e.trim(0, 0.5).Length() == approx(e.Length() / 2) assert f.trim(0, 0.5, -0.5, 0.5).Area() == approx(f.Area() / 2) From 5040668a4c5b3b3caa577cd3ccbf523b9bedb809 Mon Sep 17 00:00:00 2001 From: adam-urbanczyk Date: Sat, 9 Nov 2024 16:05:37 +0100 Subject: [PATCH 46/80] More tests --- tests/test_free_functions.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/tests/test_free_functions.py b/tests/test_free_functions.py index a4964a021..4eb47bdf7 100644 --- a/tests/test_free_functions.py +++ b/tests/test_free_functions.py @@ -292,6 +292,14 @@ def test_spline(): assert s3.tangentAt(1).toTuple() == approx((-1, 0, 0)) +def test_spline_params(): + + s1 = spline([(0, 0), (0, 1), (1, 1)], params=[0, 1, 2]) + p1 = s1.positionAt(1, mode="parameter") + + assert p1.toTuple() == approx((0, 1, 0)) + + def test_text(): r1 = text("CQ", 10) @@ -526,6 +534,18 @@ def test_sweep(): assert len(r8.Faces()) == 6 +def test_sweep_aux(): + + p = plane(1, 1) + spine = spline((0, 0, 0), (0, 0, 1)) + aux = spline([(1, 0, 0), (1, 0, 1)], tgts=((0, 1, 0), (0, -1, 0))) + + r1 = sweep(p, spine, aux) + + assert r1.isValid() + assert len(r1.faces("%PLANE").Faces()) == 2 # only two planar faces are expected + + def test_loft(): w1 = circle(1) From 334bc2b29ab803b8f55e50cc82da4a70773d0d93 Mon Sep 17 00:00:00 2001 From: adam-urbanczyk Date: Sat, 9 Nov 2024 16:21:16 +0100 Subject: [PATCH 47/80] Another overload --- tests/test_free_functions.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/test_free_functions.py b/tests/test_free_functions.py index 4eb47bdf7..7f3ebdac6 100644 --- a/tests/test_free_functions.py +++ b/tests/test_free_functions.py @@ -541,9 +541,12 @@ def test_sweep_aux(): aux = spline([(1, 0, 0), (1, 0, 1)], tgts=((0, 1, 0), (0, -1, 0))) r1 = sweep(p, spine, aux) + r2 = sweep([p], spine, aux) assert r1.isValid() assert len(r1.faces("%PLANE").Faces()) == 2 # only two planar faces are expected + assert r2.isValid() + assert len(r1.faces("%PLANE").Faces()) == 2 # only two planar faces are expected def test_loft(): From 452032b114f0c0ec5f48ca560f5f6f4cc8b98cc9 Mon Sep 17 00:00:00 2001 From: adam-urbanczyk Date: Sat, 9 Nov 2024 18:02:02 +0100 Subject: [PATCH 48/80] Loft to vertex tests --- tests/test_free_functions.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/tests/test_free_functions.py b/tests/test_free_functions.py index 7f3ebdac6..3206aa40c 100644 --- a/tests/test_free_functions.py +++ b/tests/test_free_functions.py @@ -569,6 +569,7 @@ def test_loft(): r4 = loft(w1, w2, w3, cap=True) # capped loft r5 = loft(w4, w5) # loft with open edges r6 = loft(f1, f2) # loft with faces + r7 = loft() # returns an empty compound assert_all_valid(r1, r2, r3, r4, r5, r6) @@ -578,6 +579,24 @@ def test_loft(): assert r4.Volume() > 0 assert r5.Area() == approx(1) assert len(r6.Faces()) == 16 + assert len(r6.Faces()) == 16 + assert not bool(r7) + + +def test_loft_vertex(): + + r1 = loft(rect(1, 1), vertex(0, 0, 1)) + r2 = loft(plane(1, 1), vertex(0, 0, 1)) + r3 = loft(vertex(0, 0, -1), plane(1, 1), vertex(0, 0, 1)) + r4 = loft(vertex(0, 0, -1), plane(1, 1) - plane(0.5, 0.5), vertex(0, 0, 1)) + + assert len(r1.Faces()) == 4 + assert len(r2.Faces()) == 5 + assert len(r3.Faces()) == 4 + assert len(r3.Solids()) == 1 + assert len(r4.Faces()) == 4 + assert len(r4.Solids()) == 1 + assert r4.Volume() == approx(r3.Volume()) # inner features are ignored # %% export From 5db25e65fefaefe19b4fe4064a0fd51220f8c2a9 Mon Sep 17 00:00:00 2001 From: adam-urbanczyk Date: Sat, 9 Nov 2024 18:08:33 +0100 Subject: [PATCH 49/80] Imporve coverage --- tests/test_free_functions.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/tests/test_free_functions.py b/tests/test_free_functions.py index 3206aa40c..833c9c57d 100644 --- a/tests/test_free_functions.py +++ b/tests/test_free_functions.py @@ -34,6 +34,7 @@ compound, Location, Shape, + Compound, _get_one_wire, _get_wires, _get, @@ -129,9 +130,11 @@ def test_constructors(): sh1 = shell(b.Faces()) sh2 = shell(*b.Faces()) + sh3 = shell(torus(1, 0.1).Faces()) # check for issues when sewing single face assert sh1.Area() == approx(6) assert sh2.Area() == approx(6) + assert sh3.isValid() # solid s1 = solid(b.Faces()) @@ -570,6 +573,7 @@ def test_loft(): r5 = loft(w4, w5) # loft with open edges r6 = loft(f1, f2) # loft with faces r7 = loft() # returns an empty compound + r8 = loft(compound(), compound()) # returns an empty compound assert_all_valid(r1, r2, r3, r4, r5, r6) @@ -580,7 +584,8 @@ def test_loft(): assert r5.Area() == approx(1) assert len(r6.Faces()) == 16 assert len(r6.Faces()) == 16 - assert not bool(r7) + assert not bool(r7) and isinstance(r7, Compound) + assert not bool(r8) and isinstance(r8, Compound) def test_loft_vertex(): From 859572ba3d733c7ec743139f9dd0e48abcc12ff8 Mon Sep 17 00:00:00 2001 From: adam-urbanczyk Date: Sat, 9 Nov 2024 18:27:19 +0100 Subject: [PATCH 50/80] Remove dead code --- cadquery/occ_impl/shapes.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/cadquery/occ_impl/shapes.py b/cadquery/occ_impl/shapes.py index 74e3d387f..ad2264d37 100644 --- a/cadquery/occ_impl/shapes.py +++ b/cadquery/occ_impl/shapes.py @@ -4512,12 +4512,6 @@ def _get_face_lists(s: Sequence[Shape]) -> List[List[Union[Face, Vertex]]]: for face_list, f in zip(face_lists, el.Faces()): face_list.append(f) - # check if the result makes sense - if any( - isinstance(el[0], Vertex) and isinstance(el[1], Vertex) for el in face_lists - ): - return [] - return face_lists From a0c2f0802502d548320a7b737b1cd00b07b5a7b3 Mon Sep 17 00:00:00 2001 From: adam-urbanczyk Date: Sat, 9 Nov 2024 18:30:19 +0100 Subject: [PATCH 51/80] Readd code with better explanation --- cadquery/occ_impl/shapes.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/cadquery/occ_impl/shapes.py b/cadquery/occ_impl/shapes.py index ad2264d37..3bf78fefe 100644 --- a/cadquery/occ_impl/shapes.py +++ b/cadquery/occ_impl/shapes.py @@ -4512,6 +4512,12 @@ def _get_face_lists(s: Sequence[Shape]) -> List[List[Union[Face, Vertex]]]: for face_list, f in zip(face_lists, el.Faces()): face_list.append(f) + # check if the result makes sense - needed in loft to switch to wire mode + if any( + isinstance(el[0], Vertex) and isinstance(el[1], Vertex) for el in face_lists + ): + return [] + return face_lists From 57bc5eddf7de2bc3032e28ffd16fefa5e0d7ec8e Mon Sep 17 00:00:00 2001 From: adam-urbanczyk Date: Sat, 9 Nov 2024 18:30:57 +0100 Subject: [PATCH 52/80] Better coverage --- tests/test_free_functions.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/test_free_functions.py b/tests/test_free_functions.py index 833c9c57d..6a44721df 100644 --- a/tests/test_free_functions.py +++ b/tests/test_free_functions.py @@ -590,10 +590,11 @@ def test_loft(): def test_loft_vertex(): - r1 = loft(rect(1, 1), vertex(0, 0, 1)) + r1 = loft(vertex(0, 0, 1), rect(1, 1)) r2 = loft(plane(1, 1), vertex(0, 0, 1)) r3 = loft(vertex(0, 0, -1), plane(1, 1), vertex(0, 0, 1)) r4 = loft(vertex(0, 0, -1), plane(1, 1) - plane(0.5, 0.5), vertex(0, 0, 1)) + r5 = loft(vertex(0, 0, -1), rect(1, 1), vertex(0, 0, 1)) assert len(r1.Faces()) == 4 assert len(r2.Faces()) == 5 @@ -602,6 +603,7 @@ def test_loft_vertex(): assert len(r4.Faces()) == 4 assert len(r4.Solids()) == 1 assert r4.Volume() == approx(r3.Volume()) # inner features are ignored + assert len(r5.Faces()) == 4 # %% export From 938fed60003d6fbd20005a05061e1848519ac83e Mon Sep 17 00:00:00 2001 From: adam-urbanczyk Date: Sat, 9 Nov 2024 20:13:12 +0100 Subject: [PATCH 53/80] Extend vis --- cadquery/occ_impl/assembly.py | 27 +++++--- cadquery/vis.py | 122 +++++++++++++++++++++++++++++++--- tests/test_vis.py | 7 +- 3 files changed, 139 insertions(+), 17 deletions(-) diff --git a/cadquery/occ_impl/assembly.py b/cadquery/occ_impl/assembly.py index 369d4513b..e8c6ea06b 100644 --- a/cadquery/occ_impl/assembly.py +++ b/cadquery/occ_impl/assembly.py @@ -253,6 +253,23 @@ def _toCAF(el, ancestor, color) -> TDF_Label: return top, doc +def _loc2vtk( + loc: Location, +) -> Tuple[Tuple[float, float, float], Tuple[float, float, float]]: + """ + Convert location to t,rot pair following vtk conventions + """ + + T = loc.wrapped.Transformation() + + trans = T.TranslationPart().Coord() + rot = tuple( + map(degrees, T.GetRotation().GetEulerAngles(gp_EulerSequence.gp_Intrinsic_ZXY),) + ) + + return trans, rot + + def toVTK( assy: AssemblyProtocol, color: Tuple[float, float, float, float] = (1.0, 1.0, 1.0, 1.0), @@ -265,14 +282,8 @@ def toVTK( for shape, _, loc, col_ in assy: col = col_.toTuple() if col_ else color - T = loc.wrapped.Transformation() - trans = T.TranslationPart().Coord() - rot = tuple( - map( - degrees, - T.GetRotation().GetEulerAngles(gp_EulerSequence.gp_Intrinsic_ZXY), - ) - ) + + trans, rot = _loc2vtk(loc) data = shape.toVtkPolyData(tolerance, angularTolerance) diff --git a/cadquery/vis.py b/cadquery/vis.py index 4b9a1db01..087fab9eb 100644 --- a/cadquery/vis.py +++ b/cadquery/vis.py @@ -1,16 +1,34 @@ -from . import Shape, Workplane, Assembly, Sketch, Compound, Color +from . import Shape, Workplane, Assembly, Sketch, Compound, Color, Vector, Location from .occ_impl.exporters.assembly import _vtkRenderWindow +from .occ_impl.assembly import _loc2vtk -from typing import Union, Any +from typing import Union, Any, List, Tuple + +from typish import instance_of from OCP.TopoDS import TopoDS_Shape from vtkmodules.vtkInteractionWidgets import vtkOrientationMarkerWidget from vtkmodules.vtkRenderingAnnotation import vtkAxesActor from vtkmodules.vtkInteractionStyle import vtkInteractorStyleTrackballCamera -from vtkmodules.vtkRenderingCore import vtkMapper, vtkRenderWindowInteractor +from vtkmodules.vtkRenderingCore import ( + vtkMapper, + vtkRenderWindowInteractor, + vtkActor, + vtkPolyDataMapper, + vtkAssembly, +) +from vtkmodules.vtkCommonCore import vtkPoints +from vtkmodules.vtkCommonDataModel import vtkCellArray, vtkPolyData +from vtkmodules.vtkCommonColor import vtkNamedColors + DEFAULT_COLOR = [1, 0.8, 0, 1] +DEFAULT_PT_SIZE = 7.5 +DEFAULT_PT_COLOR = "darkviolet" + +ShapeLike = Union[Shape, Workplane, Assembly, Sketch, TopoDS_Shape] +Showable = Union[ShapeLike, List[ShapeLike], Vector, List[Vector]] def _to_assy(*objs: Union[Shape, Workplane, Assembly, Sketch]) -> Assembly: @@ -30,15 +48,99 @@ def _to_assy(*objs: Union[Shape, Workplane, Assembly, Sketch]) -> Assembly: return assy -def show(*objs: Union[Shape, Workplane, Assembly, Sketch], **kwargs: Any): +def _split_showables(objs) -> Tuple[List[ShapeLike], List[Vector], List[Location]]: + """ + Split into showables and others. + """ + + rv_s: List[ShapeLike] = [] + rv_v: List[Vector] = [] + rv_l: List[Location] = [] + + for el in objs: + if instance_of(el, ShapeLike): + rv_s.append(el) + elif isinstance(el, Vector): + rv_v.append(el) + elif isinstance(el, Location): + rv_l.append(el) + elif isinstance(el, list): + tmp1, tmp2, tmp3 = _split_showables(el) # split recursively + + rv_s.extend(tmp1) + rv_v.extend(tmp2) + rv_l.extend(tmp3) + + return rv_s, rv_v, rv_l + + +def _to_vtk_pts( + vecs: List[Vector], size: float = DEFAULT_PT_SIZE, color: str = DEFAULT_PT_COLOR +) -> vtkActor: """ - Show CQ objects using VTK + Convert vectors to vtkActor. """ + rv = vtkActor() + + mapper = vtkPolyDataMapper() + points = vtkPoints() + verts = vtkCellArray() + data = vtkPolyData() + + data.SetPoints(points) + data.SetVerts(verts) + + for v in vecs: + ix = points.InsertNextPoint(*v.toTuple()) + verts.InsertNextCell(1) + verts.InsertCellPoint(ix) + + mapper.SetInputData(data) + + rv.SetMapper(mapper) + + rv.GetProperty().SetColor(vtkNamedColors().GetColor3d(color)) + rv.GetProperty().SetPointSize(size) + + return rv + + +def _to_vtk_axs(locs: List[Location], scale: float = 0.1) -> vtkActor: + """ + Convert vectors to vtkActor. + """ + + rv = vtkAssembly() + + for l in locs: + trans, rot = _loc2vtk(l) + ax = vtkAxesActor() + ax.SetAxisLabels(0) + + ax.SetPosition(*trans) + ax.SetOrientation(*rot) + ax.SetScale(scale) + + rv.AddPart(ax) + + return rv + + +def show(*objs: Showable, scale: float = 0.2, **kwargs: Any): + """ + Show CQ objects using VTK. + """ + + # split objects + shapes, vecs, locs = _split_showables(objs) + # construct the assy - assy = _to_assy( - *(obj for obj in objs if isinstance(obj, (Shape, Workplane, Assembly, Sketch))) - ) + assy = _to_assy(*shapes) + + # construct the points and locs + pts = _to_vtk_pts(vecs) + axs = _to_vtk_axs(locs, scale=scale) # create a VTK window win = _vtkRenderWindow(assy) @@ -85,6 +187,10 @@ def show(*objs: Union[Shape, Workplane, Assembly, Sketch], **kwargs: Any): camera.Elevation(-45) renderer.ResetCamera() + # add pts and locs + renderer.AddActor(pts) + renderer.AddActor(axs) + # initialize and set size inter.Initialize() win.SetSize(*win.GetScreenSize()) diff --git a/tests/test_vis.py b/tests/test_vis.py index 9c70d7c42..deee4afc9 100644 --- a/tests/test_vis.py +++ b/tests/test_vis.py @@ -1,4 +1,4 @@ -from cadquery import Workplane, Assembly, Sketch +from cadquery import Workplane, Assembly, Sketch, Location, Vector, Location from cadquery.vis import show, show_object import cadquery.occ_impl.exporters.assembly as assembly @@ -64,9 +64,14 @@ def test_show(wp, assy, sk, monkeypatch): # simple smoke test show(wp) show(wp.val()) + show(wp.val().wrapped) show(assy) show(sk) show(wp, sk, assy, wp.val()) + show(Vector()) + show(Location()) + show([Vector, Vector, Location]) + show([wp, assy]) show() show_object(wp) From 5d2d90135cfa8293d4ca59519eac3d93a3cfbf88 Mon Sep 17 00:00:00 2001 From: adam-urbanczyk Date: Sat, 9 Nov 2024 20:32:33 +0100 Subject: [PATCH 54/80] Mypy fixes --- cadquery/occ_impl/assembly.py | 4 +--- cadquery/vis.py | 4 ++-- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/cadquery/occ_impl/assembly.py b/cadquery/occ_impl/assembly.py index e8c6ea06b..12c442153 100644 --- a/cadquery/occ_impl/assembly.py +++ b/cadquery/occ_impl/assembly.py @@ -253,9 +253,7 @@ def _toCAF(el, ancestor, color) -> TDF_Label: return top, doc -def _loc2vtk( - loc: Location, -) -> Tuple[Tuple[float, float, float], Tuple[float, float, float]]: +def _loc2vtk(loc: Location,) -> Tuple[Tuple[float, float, float], Tuple[float, ...]]: """ Convert location to t,rot pair following vtk conventions """ diff --git a/cadquery/vis.py b/cadquery/vis.py index 087fab9eb..d4f25f3ab 100644 --- a/cadquery/vis.py +++ b/cadquery/vis.py @@ -31,7 +31,7 @@ Showable = Union[ShapeLike, List[ShapeLike], Vector, List[Vector]] -def _to_assy(*objs: Union[Shape, Workplane, Assembly, Sketch]) -> Assembly: +def _to_assy(*objs: ShapeLike) -> Assembly: assy = Assembly(color=Color(*DEFAULT_COLOR)) @@ -127,7 +127,7 @@ def _to_vtk_axs(locs: List[Location], scale: float = 0.1) -> vtkActor: return rv -def show(*objs: Showable, scale: float = 0.2, **kwargs: Any): +def show(*objs: Showable, scale: float = 0.2, **kwrags: Any): """ Show CQ objects using VTK. """ From 94b67e5b2e66a830b14bb330ab5c8f5b5e0052eb Mon Sep 17 00:00:00 2001 From: adam-urbanczyk Date: Sat, 9 Nov 2024 22:07:31 +0100 Subject: [PATCH 55/80] Add alpha --- cadquery/vis.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/cadquery/vis.py b/cadquery/vis.py index d4f25f3ab..c586179d6 100644 --- a/cadquery/vis.py +++ b/cadquery/vis.py @@ -31,9 +31,9 @@ Showable = Union[ShapeLike, List[ShapeLike], Vector, List[Vector]] -def _to_assy(*objs: ShapeLike) -> Assembly: +def _to_assy(*objs: ShapeLike, alpha: float = 1) -> Assembly: - assy = Assembly(color=Color(*DEFAULT_COLOR)) + assy = Assembly(color=Color(*DEFAULT_COLOR[:3], alpha)) for obj in objs: if isinstance(obj, (Shape, Workplane, Assembly)): @@ -127,7 +127,7 @@ def _to_vtk_axs(locs: List[Location], scale: float = 0.1) -> vtkActor: return rv -def show(*objs: Showable, scale: float = 0.2, **kwrags: Any): +def show(*objs: Showable, scale: float = 0.2, alpha: float = 1, **kwrags: Any): """ Show CQ objects using VTK. """ @@ -136,7 +136,7 @@ def show(*objs: Showable, scale: float = 0.2, **kwrags: Any): shapes, vecs, locs = _split_showables(objs) # construct the assy - assy = _to_assy(*shapes) + assy = _to_assy(*shapes, alpha=alpha) # construct the points and locs pts = _to_vtk_pts(vecs) @@ -181,6 +181,9 @@ def show(*objs: Showable, scale: float = 0.2, **kwrags: Any): renderer = win.GetRenderers().GetFirstRenderer() renderer.GradientBackgroundOn() + # use FXXAA + renderer.UseFXAAOn() + # set camera camera = renderer.GetActiveCamera() camera.Roll(-35) From 4b227a5d32132de15c932afa58bee079f0a2d331 Mon Sep 17 00:00:00 2001 From: adam-urbanczyk Date: Sat, 9 Nov 2024 22:20:35 +0100 Subject: [PATCH 56/80] Mypy fix --- cadquery/vis.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/cadquery/vis.py b/cadquery/vis.py index c586179d6..333ce37c6 100644 --- a/cadquery/vis.py +++ b/cadquery/vis.py @@ -33,7 +33,9 @@ def _to_assy(*objs: ShapeLike, alpha: float = 1) -> Assembly: - assy = Assembly(color=Color(*DEFAULT_COLOR[:3], alpha)) + assy = Assembly( + color=Color(DEFAULT_COLOR[0], DEFAULT_COLOR[1], DEFAULT_COLOR[2], alpha) + ) for obj in objs: if isinstance(obj, (Shape, Workplane, Assembly)): From ff3fea59d78b97b8e9a3c97bff9cf9e5e44a3ba1 Mon Sep 17 00:00:00 2001 From: adam-urbanczyk Date: Sat, 9 Nov 2024 22:59:50 +0100 Subject: [PATCH 57/80] Fix vtk rotation order --- cadquery/occ_impl/assembly.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/cadquery/occ_impl/assembly.py b/cadquery/occ_impl/assembly.py index 12c442153..de0817522 100644 --- a/cadquery/occ_impl/assembly.py +++ b/cadquery/occ_impl/assembly.py @@ -253,7 +253,9 @@ def _toCAF(el, ancestor, color) -> TDF_Label: return top, doc -def _loc2vtk(loc: Location,) -> Tuple[Tuple[float, float, float], Tuple[float, ...]]: +def _loc2vtk( + loc: Location, +) -> Tuple[Tuple[float, float, float], Tuple[float, float, float]]: """ Convert location to t,rot pair following vtk conventions """ @@ -265,7 +267,7 @@ def _loc2vtk(loc: Location,) -> Tuple[Tuple[float, float, float], Tuple[float, . map(degrees, T.GetRotation().GetEulerAngles(gp_EulerSequence.gp_Intrinsic_ZXY),) ) - return trans, rot + return trans, (rot[1], rot[2], rot[0]) def toVTK( @@ -312,7 +314,7 @@ def toVTK( actor = vtkActor() actor.SetMapper(mapper) actor.SetPosition(*trans) - actor.SetOrientation(rot[1], rot[2], rot[0]) + actor.SetOrientation(*rot) actor.GetProperty().SetColor(*col[:3]) actor.GetProperty().SetOpacity(col[3]) From 4af1dd18f755bec8017001865cb55051a5dd377f Mon Sep 17 00:00:00 2001 From: adam-urbanczyk Date: Sat, 9 Nov 2024 23:19:31 +0100 Subject: [PATCH 58/80] Second vtk rot fix --- cadquery/occ_impl/assembly.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cadquery/occ_impl/assembly.py b/cadquery/occ_impl/assembly.py index de0817522..a9197f939 100644 --- a/cadquery/occ_impl/assembly.py +++ b/cadquery/occ_impl/assembly.py @@ -326,7 +326,7 @@ def toVTK( actor = vtkActor() actor.SetMapper(mapper) actor.SetPosition(*trans) - actor.SetOrientation(rot[1], rot[2], rot[0]) + actor.SetOrientation(*rot) actor.GetProperty().SetColor(0, 0, 0) actor.GetProperty().SetLineWidth(2) From 1fcefa482acc622174c346fe5d7e46a87cd37997 Mon Sep 17 00:00:00 2001 From: adam-urbanczyk Date: Mon, 11 Nov 2024 18:39:46 +0100 Subject: [PATCH 59/80] Typo fixes --- cadquery/occ_impl/shapes.py | 11 +++++++---- tests/test_vis.py | 2 +- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/cadquery/occ_impl/shapes.py b/cadquery/occ_impl/shapes.py index 3bf78fefe..e3526bc86 100644 --- a/cadquery/occ_impl/shapes.py +++ b/cadquery/occ_impl/shapes.py @@ -2003,7 +2003,7 @@ def curvatureAt( resolution: float = 1e-6, ) -> float: """ - Caulcate mean curvature along the underlying curve. + Calculate mean curvature along the underlying curve. :param d: distance or parameter value :param mode: position calculation mode (default: length) @@ -2024,7 +2024,7 @@ def curvatures( resolution: float = 1e-6, ) -> List[float]: """ - Caulcate mean curvatures along the underlying curve. + Calculate mean curvatures along the underlying curve. :param d: distance or parameter values :param mode: position calculation mode (default: length) @@ -2082,6 +2082,8 @@ def arcCenter(self) -> Vector: def trim(self, u0: Real, u1: Real) -> "Edge": """ Trim the edge in the parametric space to (u0, u1). + + NB: this operation is done on the base geometry. """ bldr = BRepBuilderAPI_MakeEdge(self._geomAdaptor().Curve().Curve(), u0, u1) @@ -2645,7 +2647,6 @@ def fillet( """ Apply 2D or 3D fillet to a wire - :param wire: The input wire to fillet. Currently only open wires are supported :param radius: the radius of the fillet, must be > zero :param vertices: the vertices to delete (where the fillet will be applied). By default all vertices are deleted except ends of open wires. @@ -3157,6 +3158,8 @@ def toArcs(self, tolerance: float = 1e-3) -> "Face": def trim(self, u0: Real, u1: Real, v0: Real, v1: Real, tol: Real = 1e-6) -> "Face": """ Trim the face in the parametric space to (u0, u1). + + NB: this operation is done on the base geometry. """ bldr = BRepBuilderAPI_MakeFace(self._geomAdaptor(), u0, u1, v0, v1, tol) @@ -5457,7 +5460,7 @@ def _make_builder(cap): # build outer part builder = _make_builder(True) - # used to check is building inner parts makes sense + # used to check if building inner parts makes sense has_vertex = False for f in el: diff --git a/tests/test_vis.py b/tests/test_vis.py index deee4afc9..954eefd7e 100644 --- a/tests/test_vis.py +++ b/tests/test_vis.py @@ -84,5 +84,5 @@ def test_show(wp, assy, sk, monkeypatch): # for compatibility with CQ-editor show_object(wp, "a") - # for now a workaround to be compatibile with more complaicated CQ-editor invocations + # for now a workaround to be compatible with more complicated CQ-editor invocations show(1) From e4b1dc5ee99c0ba0afb917853f9a61d3be2195f9 Mon Sep 17 00:00:00 2001 From: adam-urbanczyk Date: Wed, 13 Nov 2024 20:38:02 +0100 Subject: [PATCH 60/80] Fix regression --- cadquery/occ_impl/shapes.py | 2 +- tests/test_free_functions.py | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/cadquery/occ_impl/shapes.py b/cadquery/occ_impl/shapes.py index e3526bc86..fe1cdb76f 100644 --- a/cadquery/occ_impl/shapes.py +++ b/cadquery/occ_impl/shapes.py @@ -5065,7 +5065,7 @@ def text( font_i, NCollection_Utf8String(txt), theHAlign=theHAlign, theVAlign=theVAlign ) - return clean(_compound_or_shape(rv).faces().fuse()) + return clean(compound(_compound_or_shape(rv).faces()).fuse()) #%% ops diff --git a/tests/test_free_functions.py b/tests/test_free_functions.py index 6a44721df..e2bf387db 100644 --- a/tests/test_free_functions.py +++ b/tests/test_free_functions.py @@ -322,6 +322,12 @@ def test_text(): assert r4.faces(" r1.faces(" r5.faces(" Date: Thu, 14 Nov 2024 19:56:35 +0100 Subject: [PATCH 61/80] Text, offset, solid and exportBin changes - text on path - text on path/surface - symmetric offset - import/export binary brep --- cadquery/occ_impl/shapes.py | 183 +++++++++++++++++++++++++++++++----- 1 file changed, 162 insertions(+), 21 deletions(-) diff --git a/cadquery/occ_impl/shapes.py b/cadquery/occ_impl/shapes.py index fe1cdb76f..c6ca56495 100644 --- a/cadquery/occ_impl/shapes.py +++ b/cadquery/occ_impl/shapes.py @@ -279,6 +279,8 @@ from OCP.LProp3d import LProp3d_CLProps +from OCP.BinTools import BinTools + from math import pi, sqrt, inf, radians, cos import warnings @@ -520,6 +522,29 @@ def importBrep(cls, f: Union[str, BytesIO]) -> "Shape": return cls.cast(s) + def exportBin(self, f: Union[str, BytesIO]) -> bool: + """ + Export this shape to a binary BREP file + """ + + rv = BinTools.Write_s(self.wrapped, f) + + return True if rv is None else rv + + @classmethod + def importBin(cls, f: Union[str, BytesIO]) -> "Shape": + """ + Import shape from a binary BREP file + """ + s = TopoDS_Shape() + + BinTools.Read_s(s, f) + + if s.IsNull(): + raise ValueError(f"Could not import {f}") + + return cls.cast(s) + def geomType(self) -> Geoms: """ Gets the underlying geometry type. @@ -4528,6 +4553,7 @@ def _normalize(s: Shape) -> Shape: """ Apply some normalizations: - Shell with only one Face -> Face. + - Compound with only one element -> element. """ t = s.ShapeType() @@ -4726,15 +4752,23 @@ def shell(s: Sequence[Shape], tol: float = 1e-6) -> Shape: @multimethod def solid(*s: Shape, tol: float = 1e-6) -> Shape: """ - Build solid from faces. + Build solid from faces or shells. """ builder = ShapeFix_Solid() - faces = [f for el in s for f in _get(el, "Face")] - rv = builder.SolidFromShell(shell(*faces, tol=tol).wrapped) + # get both Shells and Faces + shells_faces = [f for el in s for f in _get(el, ("Shell", "Face"))] - return _compound_or_shape(rv) + # if no shells are present, use faces to construct them + shells = [el for el in shells_faces if el.ShapeType() == "Shell"] + if not shells: + faces = [el for el in shells_faces] + shells = [shell(*faces, tol=tol).wrapped] + + rvs = [builder.SolidFromShell(sh) for sh in shells] + + return _compound_or_shape(rvs) @solid.register @@ -4759,6 +4793,29 @@ def solid( return _compound_or_shape(sf.Solid()) +@solid.register +def solid(s: Shape, inner: Optional[Union[Shape, Sequence[Shape]]] = None) -> Shape: + """ + Build solid from a shell and inner shells. + """ + + builder = BRepBuilderAPI_MakeSolid() + builder.Add(_get_one(s, "Shell").wrapped) + + if inner: + inner_shells = ( + inner if isinstance(inner, Shape) else compound(*inner) + ).shells() + for sh in inner_shells: + builder.Add(sh.wrapped) + + # fix orientations + sf = ShapeFix_Solid(builder.Solid()) + sf.Perform() + + return _compound_or_shape(sf.Solid()) + + @multimethod def compound(*s: Shape) -> Shape: """ @@ -5012,6 +5069,7 @@ def cone(d: Real, h: Real) -> Shape: return cone(d, 0, h) +@multimethod def text( txt: str, size: float, @@ -5068,6 +5126,66 @@ def text( return clean(compound(_compound_or_shape(rv).faces()).fuse()) +@text.register +def text( + txt: str, + size: float, + spine: Shape, + planar: bool = False, + font: str = "Arial", + path: Optional[str] = None, + kind: Literal["regular", "bold", "italic"] = "regular", + halign: Literal["center", "left", "right"] = "center", + valign: Literal["center", "top", "bottom"] = "center", +) -> Shape: + """ + Create a text on a spine. + """ + + spine = _get_one_wire(spine) + L = spine.Length() + + rv = [] + for el in text(txt, size, font, path, kind, halign, valign): + pos = el.BoundingBox().center.x + + # position + rv.append( + el.moved(-pos) + .moved(rx=-90 if planar else 0, ry=-90) + .moved(spine.locationAt(pos / L)) + ) + + return _normalize(compound(rv)) + + +@text.register +def text( + txt: str, + size: float, + spine: Shape, + base: Shape, + font: str = "Arial", + path: Optional[str] = None, + kind: Literal["regular", "bold", "italic"] = "regular", + halign: Literal["center", "left", "right"] = "center", + valign: Literal["center", "top", "bottom"] = "center", +) -> Shape: + """ + Create a text on a spine and a base surface. + """ + + base = _get_one(base, "Face") + + tmp = text(txt, size, spine, False, font, path, kind, halign, valign) + + rv = [] + for f in tmp.faces(): + rv.append(f.project(base, f.normalAt())) + + return _normalize(compound(rv)) + + #%% ops @@ -5266,33 +5384,56 @@ def revolve(s: Shape, p: VectorLike, d: VectorLike, a: float = 360): return _compound_or_shape(results) -def offset(s: Shape, t: float, cap=True, tol: float = 1e-6) -> Shape: +def offset( + s: Shape, t: float, cap=True, both: bool = False, tol: float = 1e-6 +) -> Shape: """ Offset or thicken faces or shells. """ - builder = BRepOffset_MakeOffset() + def _offset(t): - results = [] + results = [] - for el in _get(s, ("Face", "Shell")): + for el in _get(s, ("Face", "Shell")): - builder.Initialize( - el.wrapped, - t, - tol, - BRepOffset_Mode.BRepOffset_Skin, - False, - False, - GeomAbs_Intersection, - cap, - ) + builder = BRepOffset_MakeOffset() - builder.MakeOffsetShape() + builder.Initialize( + el.wrapped, + t, + tol, + BRepOffset_Mode.BRepOffset_Skin, + False, + False, + GeomAbs_Intersection, + cap, + ) - results.append(builder.Shape()) + builder.MakeOffsetShape() - return _compound_or_shape(results) + results.append(builder.Shape()) + + return results + + if both: + results_pos = _offset(t) + results_neg = _offset(-t) + + results_both = [ + Shape(el1) + Shape(el2) for el1, el2 in zip(results_pos, results_neg) + ] + + if len(results_both) == 1: + rv = results_both[0] + else: + rv = Compound.makeCompound(results_both) + + else: + results = _offset(t) + rv = _compound_or_shape(results) + + return rv @multimethod From 18c0acdf79758fb102f6fd5c49531edea7f2d1a3 Mon Sep 17 00:00:00 2001 From: adam-urbanczyk Date: Thu, 14 Nov 2024 19:57:08 +0100 Subject: [PATCH 62/80] Binary BREP in exporters/importers --- cadquery/occ_impl/exporters/__init__.py | 8 ++++++-- cadquery/occ_impl/importers/__init__.py | 17 ++++++++++++++++- 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/cadquery/occ_impl/exporters/__init__.py b/cadquery/occ_impl/exporters/__init__.py index 985bba629..f7a988886 100644 --- a/cadquery/occ_impl/exporters/__init__.py +++ b/cadquery/occ_impl/exporters/__init__.py @@ -14,7 +14,7 @@ from .json import JsonMesh from .amf import AmfWriter from .threemf import ThreeMFWriter -from .dxf import exportDXF, DxfDocument +from .dxf import exportDXF from .vtk import exportVTP @@ -29,10 +29,11 @@ class ExportTypes: VTP = "VTP" THREEMF = "3MF" BREP = "BREP" + BIN = "BIN" ExportLiterals = Literal[ - "STL", "STEP", "AMF", "SVG", "TJS", "DXF", "VRML", "VTP", "3MF", "BREP" + "STL", "STEP", "AMF", "SVG", "TJS", "DXF", "VRML", "VTP", "3MF", "BREP", "BIN" ] @@ -128,6 +129,9 @@ def export( elif exportType == ExportTypes.BREP: shape.exportBrep(fname) + elif exportType == ExportTypes.BIN: + shape.exportBin(fname) + else: raise ValueError("Unknown export type") diff --git a/cadquery/occ_impl/importers/__init__.py b/cadquery/occ_impl/importers/__init__.py index 1ac48bc87..544fe1eeb 100644 --- a/cadquery/occ_impl/importers/__init__.py +++ b/cadquery/occ_impl/importers/__init__.py @@ -15,6 +15,7 @@ class ImportTypes: STEP = "STEP" DXF = "DXF" BREP = "BREP" + BIN = "BIN" class UNITS: @@ -23,7 +24,7 @@ class UNITS: def importShape( - importType: Literal["STEP", "DXF", "BREP"], fileName: str, *args, **kwargs + importType: Literal["STEP", "DXF", "BREP", "BIN"], fileName: str, *args, **kwargs ) -> "cq.Workplane": """ Imports a file based on the type (STEP, STL, etc) @@ -39,6 +40,8 @@ def importShape( return importDXF(fileName, *args, **kwargs) elif importType == ImportTypes.BREP: return importBrep(fileName) + elif importType == ImportTypes.BIN: + return importBin(fileName) else: raise RuntimeError("Unsupported import type: {!r}".format(importType)) @@ -60,6 +63,18 @@ def importBrep(fileName: str) -> "cq.Workplane": return cq.Workplane("XY").newObject([shape]) +def importBin(fileName: str) -> "cq.Workplane": + """ + Loads the binary BREP file as a single shape into a cadquery Workplane. + + :param fileName: The path and name of the BREP file to be imported + + """ + shape = Shape.importBin(fileName) + + return cq.Workplane("XY").newObject([shape]) + + # Loads a STEP file into a CQ.Workplane object def importStep(fileName: str) -> "cq.Workplane": """ From 556c73ce011948f7a620934068abe3c388ce8c16 Mon Sep 17 00:00:00 2001 From: adam-urbanczyk Date: Thu, 14 Nov 2024 19:57:26 +0100 Subject: [PATCH 63/80] Extra tests --- tests/test_free_functions.py | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/tests/test_free_functions.py b/tests/test_free_functions.py index e2bf387db..31ad8af2b 100644 --- a/tests/test_free_functions.py +++ b/tests/test_free_functions.py @@ -41,6 +41,7 @@ _get_one, _get_edges, check, + Vector, ) from pytest import approx, raises @@ -150,6 +151,20 @@ def test_constructors(): assert s3.Volume() == approx(1 - 2 * 0.1 ** 3) + # solid from shells + s4 = solid(b.Shells()) + s5 = solid(b.shells()) + + assert s4.Volume() == approx(1) + assert s5.Volume() == approx(1) + + # solid form shells with voids + s6 = solid(b.shells(), inner=b1.moved([(0.2, 0, 0.5), (-0.2, 0, 0.5)]).Shells()) + s7 = solid(b.shells(), inner=b1.moved([(0.2, 0, 0.5), (-0.2, 0, 0.5)]).shells()) + + assert s6.Volume() == approx(1 - 2 * 0.1 ** 3) + assert s7.Volume() == approx(1 - 2 * 0.1 ** 3) + # compound c1 = compound(b.Faces()) c2 = compound(*b.Faces()) @@ -328,6 +343,27 @@ def test_text(): assert len(r6.Faces()) == 1 assert len(r6.Wires()) == 1 + # test text on path + c = cylinder(10, 10).moved(rz=180) + cf = c.faces("%CYLINDER") + spine = c / plane(20, 20).moved(z=5) + + r7 = text("CQ", 1, spine) # normal + r8 = text("CQ", 1, spine, planar=True) # planar + r9 = text("CQ", 1, spine, cf) # projected + + assert r7.Center().z > 0 + assert r7.faces("< 0 + assert (r8.faces("< 0 + assert r9.faces("< Date: Thu, 14 Nov 2024 20:37:08 +0100 Subject: [PATCH 64/80] Fix tests --- cadquery/occ_impl/exporters/__init__.py | 2 +- cadquery/occ_impl/shapes.py | 8 ++++---- tests/test_free_functions.py | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/cadquery/occ_impl/exporters/__init__.py b/cadquery/occ_impl/exporters/__init__.py index f7a988886..3f9686630 100644 --- a/cadquery/occ_impl/exporters/__init__.py +++ b/cadquery/occ_impl/exporters/__init__.py @@ -14,7 +14,7 @@ from .json import JsonMesh from .amf import AmfWriter from .threemf import ThreeMFWriter -from .dxf import exportDXF +from .dxf import exportDXF, DxfDocument # DxfDocument is needed from .vtk import exportVTP diff --git a/cadquery/occ_impl/shapes.py b/cadquery/occ_impl/shapes.py index c6ca56495..e15854ae4 100644 --- a/cadquery/occ_impl/shapes.py +++ b/cadquery/occ_impl/shapes.py @@ -4794,7 +4794,7 @@ def solid( @solid.register -def solid(s: Shape, inner: Optional[Union[Shape, Sequence[Shape]]] = None) -> Shape: +def solid(s: Shape, *, inner: Optional[Union[Shape, Sequence[Shape]]] = None) -> Shape: """ Build solid from a shell and inner shells. """ @@ -5072,7 +5072,7 @@ def cone(d: Real, h: Real) -> Shape: @multimethod def text( txt: str, - size: float, + size: Real, font: str = "Arial", path: Optional[str] = None, kind: Literal["regular", "bold", "italic"] = "regular", @@ -5129,7 +5129,7 @@ def text( @text.register def text( txt: str, - size: float, + size: Real, spine: Shape, planar: bool = False, font: str = "Arial", @@ -5162,7 +5162,7 @@ def text( @text.register def text( txt: str, - size: float, + size: Real, spine: Shape, base: Shape, font: str = "Arial", diff --git a/tests/test_free_functions.py b/tests/test_free_functions.py index 31ad8af2b..d9167729a 100644 --- a/tests/test_free_functions.py +++ b/tests/test_free_functions.py @@ -346,7 +346,7 @@ def test_text(): # test text on path c = cylinder(10, 10).moved(rz=180) cf = c.faces("%CYLINDER") - spine = c / plane(20, 20).moved(z=5) + spine = c.edges(' 0 + assert r8.Center().z == approx(0) assert (r8.faces("< Date: Thu, 14 Nov 2024 20:37:31 +0100 Subject: [PATCH 65/80] Add bin imp/exp test --- tests/test_shapes.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/tests/test_shapes.py b/tests/test_shapes.py index 576241071..f1889a71c 100644 --- a/tests/test_shapes.py +++ b/tests/test_shapes.py @@ -9,6 +9,7 @@ circle, plane, torus, + Shape, ) from pytest import approx @@ -115,3 +116,20 @@ def test_trimming(): assert e.trim(0, 0.5).Length() == approx(e.Length() / 2) assert f.trim(0, 0.5, -0.5, 0.5).Area() == approx(f.Area() / 2) + + +def test_bin_import_export(): + + b = box(1, 1, 1) + + from io import BytesIO + + bio = BytesIO() + + b.exportBin(bio) + bio.seek(0) + + r = Shape.importBin(bio) + + assert r.isValid() + assert r.Volume() == approx(1) From 728bbd327ae76b02baaa6510a1dd9dc7a110a19f Mon Sep 17 00:00:00 2001 From: adam-urbanczyk Date: Thu, 14 Nov 2024 20:55:04 +0100 Subject: [PATCH 66/80] Blacken --- tests/test_free_functions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_free_functions.py b/tests/test_free_functions.py index d9167729a..41a35f12c 100644 --- a/tests/test_free_functions.py +++ b/tests/test_free_functions.py @@ -346,7 +346,7 @@ def test_text(): # test text on path c = cylinder(10, 10).moved(rz=180) cf = c.faces("%CYLINDER") - spine = c.edges(' Date: Thu, 14 Nov 2024 21:22:17 +0100 Subject: [PATCH 67/80] Remove one overload --- cadquery/occ_impl/shapes.py | 23 ----------------------- tests/test_free_functions.py | 7 ------- 2 files changed, 30 deletions(-) diff --git a/cadquery/occ_impl/shapes.py b/cadquery/occ_impl/shapes.py index e15854ae4..c6f35d7fc 100644 --- a/cadquery/occ_impl/shapes.py +++ b/cadquery/occ_impl/shapes.py @@ -4793,29 +4793,6 @@ def solid( return _compound_or_shape(sf.Solid()) -@solid.register -def solid(s: Shape, *, inner: Optional[Union[Shape, Sequence[Shape]]] = None) -> Shape: - """ - Build solid from a shell and inner shells. - """ - - builder = BRepBuilderAPI_MakeSolid() - builder.Add(_get_one(s, "Shell").wrapped) - - if inner: - inner_shells = ( - inner if isinstance(inner, Shape) else compound(*inner) - ).shells() - for sh in inner_shells: - builder.Add(sh.wrapped) - - # fix orientations - sf = ShapeFix_Solid(builder.Solid()) - sf.Perform() - - return _compound_or_shape(sf.Solid()) - - @multimethod def compound(*s: Shape) -> Shape: """ diff --git a/tests/test_free_functions.py b/tests/test_free_functions.py index 41a35f12c..aea749dc0 100644 --- a/tests/test_free_functions.py +++ b/tests/test_free_functions.py @@ -158,13 +158,6 @@ def test_constructors(): assert s4.Volume() == approx(1) assert s5.Volume() == approx(1) - # solid form shells with voids - s6 = solid(b.shells(), inner=b1.moved([(0.2, 0, 0.5), (-0.2, 0, 0.5)]).Shells()) - s7 = solid(b.shells(), inner=b1.moved([(0.2, 0, 0.5), (-0.2, 0, 0.5)]).shells()) - - assert s6.Volume() == approx(1 - 2 * 0.1 ** 3) - assert s7.Volume() == approx(1 - 2 * 0.1 ** 3) - # compound c1 = compound(b.Faces()) c2 = compound(*b.Faces()) From efba7dd35908f5c9ef547bbec7b9c21bdd449ab5 Mon Sep 17 00:00:00 2001 From: adam-urbanczyk Date: Thu, 14 Nov 2024 21:45:14 +0100 Subject: [PATCH 68/80] Fix dispatch --- cadquery/occ_impl/shapes.py | 5 +++-- tests/test_free_functions.py | 4 +--- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/cadquery/occ_impl/shapes.py b/cadquery/occ_impl/shapes.py index c6f35d7fc..d960fe1aa 100644 --- a/cadquery/occ_impl/shapes.py +++ b/cadquery/occ_impl/shapes.py @@ -4750,7 +4750,7 @@ def shell(s: Sequence[Shape], tol: float = 1e-6) -> Shape: @multimethod -def solid(*s: Shape, tol: float = 1e-6) -> Shape: +def solid(s1: Shape, *sn: Shape, tol: float = 1e-6) -> Shape: """ Build solid from faces or shells. """ @@ -4758,10 +4758,11 @@ def solid(*s: Shape, tol: float = 1e-6) -> Shape: builder = ShapeFix_Solid() # get both Shells and Faces + s = [s1, *sn] shells_faces = [f for el in s for f in _get(el, ("Shell", "Face"))] # if no shells are present, use faces to construct them - shells = [el for el in shells_faces if el.ShapeType() == "Shell"] + shells = [el.wrapped for el in shells_faces if el.ShapeType() == "Shell"] if not shells: faces = [el for el in shells_faces] shells = [shell(*faces, tol=tol).wrapped] diff --git a/tests/test_free_functions.py b/tests/test_free_functions.py index aea749dc0..9592132f5 100644 --- a/tests/test_free_functions.py +++ b/tests/test_free_functions.py @@ -152,11 +152,9 @@ def test_constructors(): assert s3.Volume() == approx(1 - 2 * 0.1 ** 3) # solid from shells - s4 = solid(b.Shells()) - s5 = solid(b.shells()) + s4 = solid(b.shells()) assert s4.Volume() == approx(1) - assert s5.Volume() == approx(1) # compound c1 = compound(b.Faces()) From c65c257e08e37879717cbebd37bbdd619c70ede2 Mon Sep 17 00:00:00 2001 From: adam-urbanczyk Date: Thu, 14 Nov 2024 22:29:44 +0100 Subject: [PATCH 69/80] Fix test --- tests/test_free_functions.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/test_free_functions.py b/tests/test_free_functions.py index 9592132f5..a62a5459b 100644 --- a/tests/test_free_functions.py +++ b/tests/test_free_functions.py @@ -337,21 +337,21 @@ def test_text(): # test text on path c = cylinder(10, 10).moved(rz=180) cf = c.faces("%CYLINDER") - spine = c.edges(" 0 + assert r7.faces(">>Z").Center().z > 0 assert r7.faces("<>Z").Center().z == approx(0) assert (r8.faces("< 0 + assert r9.faces(">>Z").Center().z > 0 assert r9.faces("< Date: Thu, 14 Nov 2024 23:16:24 +0100 Subject: [PATCH 70/80] Extra vis options --- cadquery/vis.py | 17 +++++++++++++++-- tests/test_free_functions.py | 2 ++ 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/cadquery/vis.py b/cadquery/vis.py index 333ce37c6..08aeba7b3 100644 --- a/cadquery/vis.py +++ b/cadquery/vis.py @@ -129,7 +129,14 @@ def _to_vtk_axs(locs: List[Location], scale: float = 0.1) -> vtkActor: return rv -def show(*objs: Showable, scale: float = 0.2, alpha: float = 1, **kwrags: Any): +def show( + *objs: Showable, + scale: float = 0.2, + alpha: float = 1, + tolerance: float = 1e-3, + edges: bool = False, + **kwrags: Any, +): """ Show CQ objects using VTK. """ @@ -145,10 +152,16 @@ def show(*objs: Showable, scale: float = 0.2, alpha: float = 1, **kwrags: Any): axs = _to_vtk_axs(locs, scale=scale) # create a VTK window - win = _vtkRenderWindow(assy) + win = _vtkRenderWindow(assy, tolerance=tolerance) win.SetWindowName("CQ viewer") + # get renderer and actor` + if edges: + ren = win.GetRenderers().GetFirstRenderer() + for act in ren.GetActors(): + act.GetProperty().EdgeVisibilityOn() + # rendering related settings win.SetMultiSamples(16) vtkMapper.SetResolveCoincidentTopologyToPolygonOffset() diff --git a/tests/test_free_functions.py b/tests/test_free_functions.py index a62a5459b..83d205eb5 100644 --- a/tests/test_free_functions.py +++ b/tests/test_free_functions.py @@ -530,9 +530,11 @@ def test_offset(): r1 = offset(f, 1) r2 = offset(s, -0.25) + r3 = offset(f, 1, both=True) assert r1.Volume() == approx(1) assert r2.Volume() == approx(1 - 0.5 ** 3) + assert r3.Volume() == approx(2) def test_sweep(): From 0b31ab64d3cd86b8964c56a29e7b1173126dc631 Mon Sep 17 00:00:00 2001 From: adam-urbanczyk Date: Thu, 14 Nov 2024 23:20:24 +0100 Subject: [PATCH 71/80] Different error --- cadquery/occ_impl/shapes.py | 3 --- tests/test_shapes.py | 7 ++++++- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/cadquery/occ_impl/shapes.py b/cadquery/occ_impl/shapes.py index d960fe1aa..6b001a0bc 100644 --- a/cadquery/occ_impl/shapes.py +++ b/cadquery/occ_impl/shapes.py @@ -540,9 +540,6 @@ def importBin(cls, f: Union[str, BytesIO]) -> "Shape": BinTools.Read_s(s, f) - if s.IsNull(): - raise ValueError(f"Could not import {f}") - return cls.cast(s) def geomType(self) -> Geoms: diff --git a/tests/test_shapes.py b/tests/test_shapes.py index f1889a71c..008520779 100644 --- a/tests/test_shapes.py +++ b/tests/test_shapes.py @@ -12,7 +12,9 @@ Shape, ) -from pytest import approx +from OCP.Storage import Storage_StreamReadError + +from pytest import approx, raises from math import pi @@ -133,3 +135,6 @@ def test_bin_import_export(): assert r.isValid() assert r.Volume() == approx(1) + + with raises(Storage_StreamReadError): + Shape.importBin(BytesIO()) From 238d50464018b8b722ba391c2e8c8858fc28c49a Mon Sep 17 00:00:00 2001 From: adam-urbanczyk Date: Thu, 14 Nov 2024 23:50:51 +0100 Subject: [PATCH 72/80] Fix/more tests --- tests/test_shapes.py | 4 +--- tests/test_vis.py | 3 +++ 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/tests/test_shapes.py b/tests/test_shapes.py index 008520779..7bd95f602 100644 --- a/tests/test_shapes.py +++ b/tests/test_shapes.py @@ -12,8 +12,6 @@ Shape, ) -from OCP.Storage import Storage_StreamReadError - from pytest import approx, raises from math import pi @@ -136,5 +134,5 @@ def test_bin_import_export(): assert r.isValid() assert r.Volume() == approx(1) - with raises(Storage_StreamReadError): + with raises(Exception): Shape.importBin(BytesIO()) diff --git a/tests/test_vis.py b/tests/test_vis.py index 954eefd7e..e7cbbe391 100644 --- a/tests/test_vis.py +++ b/tests/test_vis.py @@ -74,6 +74,9 @@ def test_show(wp, assy, sk, monkeypatch): show([wp, assy]) show() + # show with edges + show(wp, edges=True) + show_object(wp) show_object(wp.val()) show_object(assy) From 9de4a1de4c022a401b9e4f7a5153d58b44c3aaef Mon Sep 17 00:00:00 2001 From: adam-urbanczyk Date: Fri, 15 Nov 2024 08:36:32 +0100 Subject: [PATCH 73/80] Better coverage --- tests/test_free_functions.py | 2 ++ tests/test_importers.py | 13 +++++++++++++ 2 files changed, 15 insertions(+) diff --git a/tests/test_free_functions.py b/tests/test_free_functions.py index 83d205eb5..091e2d405 100644 --- a/tests/test_free_functions.py +++ b/tests/test_free_functions.py @@ -531,10 +531,12 @@ def test_offset(): r1 = offset(f, 1) r2 = offset(s, -0.25) r3 = offset(f, 1, both=True) + r4 = offset(f.moved((0, 0), (5, 5)), 1, both=True) assert r1.Volume() == approx(1) assert r2.Volume() == approx(1 - 0.5 ** 3) assert r3.Volume() == approx(2) + assert r4.Volume() == approx(4) def test_sweep(): diff --git a/tests/test_importers.py b/tests/test_importers.py index b79f3527b..9ba3bce41 100644 --- a/tests/test_importers.py +++ b/tests/test_importers.py @@ -31,6 +31,8 @@ def importBox(self, importType, fileName): shape.exportStep(fileName) elif importType == importers.ImportTypes.BREP: shape.exportBrep(fileName) + elif importType == importers.ImportTypes.BIN: + shape.exportBin(fileName) # Reimport the shape from the new file importedShape = importers.importShape(importType, fileName) @@ -62,6 +64,8 @@ def importCompound(self, importType, fileName): shape.exportStep(fileName) elif importType == importers.ImportTypes.BREP: shape.exportBrep(fileName) + elif importType == importers.ImportTypes.BIN: + shape.exportBin(fileName) # Reimport the shape from the new file importedShape = importers.importShape(importType, fileName) @@ -131,6 +135,15 @@ def testBREP(self): importers.ImportTypes.BREP, os.path.join(OUTDIR, "tempBREP.brep") ) + def testBIN(self): + """ + Test binary BREP file import. + """ + self.importBox(importers.ImportTypes.BIN, os.path.join(OUTDIR, "tempBIN.brep")) + self.importCompound( + importers.ImportTypes.BIN, os.path.join(OUTDIR, "tempBIN.brep") + ) + def testSTEP(self): """ Tests STEP file import From 67cd3f449fd0a55c290245061a2dc1f524fb6e46 Mon Sep 17 00:00:00 2001 From: adam-urbanczyk Date: Sat, 16 Nov 2024 00:22:33 +0100 Subject: [PATCH 74/80] More tests --- tests/test_exporters.py | 8 ++++++++ tests/test_importers.py | 4 ++-- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/tests/test_exporters.py b/tests/test_exporters.py index 40fc54235..fbd5c3599 100644 --- a/tests/test_exporters.py +++ b/tests/test_exporters.py @@ -28,6 +28,7 @@ Location, Vector, Color, + Shape, ) from cadquery.occ_impl.shapes import rect, face, compound @@ -744,6 +745,13 @@ def testDXF(self): self.assertAlmostEqual(s5.val().Area(), s5_i.val().Area(), 4) + def testBIN(self): + + exporters.export(self._box(), "out.bin") + + s = Shape.importBin("out.bin") + assert s.isValid() + def testTypeHandling(self): with self.assertRaises(ValueError): diff --git a/tests/test_importers.py b/tests/test_importers.py index 9ba3bce41..5245d915a 100644 --- a/tests/test_importers.py +++ b/tests/test_importers.py @@ -139,9 +139,9 @@ def testBIN(self): """ Test binary BREP file import. """ - self.importBox(importers.ImportTypes.BIN, os.path.join(OUTDIR, "tempBIN.brep")) + self.importBox(importers.ImportTypes.BIN, os.path.join(OUTDIR, "tempBIN.bin")) self.importCompound( - importers.ImportTypes.BIN, os.path.join(OUTDIR, "tempBIN.brep") + importers.ImportTypes.BIN, os.path.join(OUTDIR, "tempBIN.bin") ) def testSTEP(self): From a4e6a63612232759eba5b2b61fb34eb6983889d8 Mon Sep 17 00:00:00 2001 From: AU Date: Mon, 18 Nov 2024 20:29:34 +0100 Subject: [PATCH 75/80] Docs text placeholder --- doc/free-func.rst | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/doc/free-func.rst b/doc/free-func.rst index acedeef91..264cc2c86 100644 --- a/doc/free-func.rst +++ b/doc/free-func.rst @@ -237,3 +237,8 @@ Placement and creation of arrays is possible using :meth:`~cadquery.Shape.move` c = cylinder(1,2).move(rx=15).moved(*locs) result = compound(s, c.moved(2)) + +Text +---- + +The free functon API has extensive text creation capabilities including text on planar curves and text on surfaces. From fe33f7d4977b57f0c4756290904459f25bc3c2d4 Mon Sep 17 00:00:00 2001 From: adam-urbanczyk Date: Wed, 20 Nov 2024 08:12:23 +0100 Subject: [PATCH 76/80] Pseudo-infinite plane --- cadquery/occ_impl/shapes.py | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/cadquery/occ_impl/shapes.py b/cadquery/occ_impl/shapes.py index 6b001a0bc..57f255b0b 100644 --- a/cadquery/occ_impl/shapes.py +++ b/cadquery/occ_impl/shapes.py @@ -4954,9 +4954,10 @@ def ellipse(r1: float, r2: float) -> Shape: ) -def plane(w: float, l: float) -> Shape: +@multimethod +def plane(w: Real, l: Real) -> Shape: """ - Construct a planar face. + Construct a finite planar face. """ pln_geom = gp_Pln(Vector(0, 0, 0).toPnt(), Vector(0, 0, 1).toDir()) @@ -4966,6 +4967,24 @@ def plane(w: float, l: float) -> Shape: ) +@plane.register +def plane() -> Shape: + """ + Construct an infinite planar face. + + This is a crude approximation. Truly infinite faces in OCCT do not work as + expected in all contexts. + """ + + INF = 1e+60 + + pln_geom = gp_Pln(Vector(0, 0, 0).toPnt(), Vector(0, 0, 1).toDir()) + + return _compound_or_shape( + BRepBuilderAPI_MakeFace(pln_geom, -INF, INF, -INF, INF).Face() + ) + + def box(w: float, l: float, h: float) -> Shape: """ Construct a solid box. From eddaaa456bad056054ff8b16c8fc2be2f3f2a88f Mon Sep 17 00:00:00 2001 From: adam-urbanczyk Date: Wed, 20 Nov 2024 08:14:26 +0100 Subject: [PATCH 77/80] Add an example for text --- doc/free-func.rst | 45 +++++++++++++++++++++++++++++++++++++++------ 1 file changed, 39 insertions(+), 6 deletions(-) diff --git a/doc/free-func.rst b/doc/free-func.rst index 264cc2c86..e6db8cff5 100644 --- a/doc/free-func.rst +++ b/doc/free-func.rst @@ -197,27 +197,27 @@ Free function API currently supports :meth:`~cadquery.occ_impl.shapes.extrude`, .. cadquery:: from cadquery.occ_impl.shapes import * - + r = rect(1,0.5) f = face(r, circle(0.2).moved(0.2), rect(0.2, 0.4).moved(-0.2)) c = circle(0.2) p = spline([(0,0,0), (0,-1,2)], [(0,0,1), (0,-1,1)]) - + # extrude s1 = extrude(r, (0,0,2)) s2 = extrude(fill(r), (0,0,1)) - + # sweep s3 = sweep(r, p) s4 = sweep(f, p) - + # loft s5 = loft(r, c.moved(z=2)) s6 = loft(r, c.moved(z=1), cap=True)\ - + # revolve s7 = revolve(fill(r), (0.5, 0, 0), (0, 1, 0), 90) - + results = (s1, s2, s3, s4, s5, s6, s7) result = compound([el.moved(2*i) for i,el in enumerate(results)]) @@ -242,3 +242,36 @@ Text ---- The free functon API has extensive text creation capabilities including text on planar curves and text on surfaces. + + +.. cadquery:: + + from cadquery.occ_impl.shapes import * + + from math import pi + + # parameters + D = 5 + H = 2*D + S = H/10 + TH = S/10 + TXT = "CadQuery" + + # base and spine + c = cylinder(D, H).moved(rz=-135) + cf = c.faces("%CYLINDER") + spine = (c*plane().moved(z=D)).edges().trim(pi/2, pi) + + # planar + r1 = text(TXT, 1, spine, planar=True).moved(z=-S) + + # normal + r2 = text(TXT, 1, spine) + + # projected + r3 = text(TXT, 1, spine, cf).moved(z=S) + + # projected and thickend + r4 = offset(r3, TH).moved(z=S) + + result = compound(r1, r2, r3, r4) From 2f24f914fd5fad2408245c926cb9308690820632 Mon Sep 17 00:00:00 2001 From: adam-urbanczyk Date: Wed, 20 Nov 2024 08:33:42 +0100 Subject: [PATCH 78/80] Black fix --- 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 57f255b0b..b9f82f387 100644 --- a/cadquery/occ_impl/shapes.py +++ b/cadquery/occ_impl/shapes.py @@ -4976,7 +4976,7 @@ def plane() -> Shape: expected in all contexts. """ - INF = 1e+60 + INF = 1e60 pln_geom = gp_Pln(Vector(0, 0, 0).toPnt(), Vector(0, 0, 1).toDir()) From f7d6d958f03d2c7f61cdc1e85792947e52c554fb Mon Sep 17 00:00:00 2001 From: AU Date: Sat, 23 Nov 2024 12:11:16 +0100 Subject: [PATCH 79/80] Apply suggestions from code review Co-authored-by: Jeremy Wright --- cadquery/occ_impl/exporters/__init__.py | 2 +- cadquery/occ_impl/shapes.py | 4 ++-- cadquery/vis.py | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/cadquery/occ_impl/exporters/__init__.py b/cadquery/occ_impl/exporters/__init__.py index 3f9686630..7f9eddadb 100644 --- a/cadquery/occ_impl/exporters/__init__.py +++ b/cadquery/occ_impl/exporters/__init__.py @@ -14,7 +14,7 @@ from .json import JsonMesh from .amf import AmfWriter from .threemf import ThreeMFWriter -from .dxf import exportDXF, DxfDocument # DxfDocument is needed +from .dxf import exportDXF, DxfDocument from .vtk import exportVTP diff --git a/cadquery/occ_impl/shapes.py b/cadquery/occ_impl/shapes.py index b9f82f387..dc1ae574f 100644 --- a/cadquery/occ_impl/shapes.py +++ b/cadquery/occ_impl/shapes.py @@ -524,7 +524,7 @@ def importBrep(cls, f: Union[str, BytesIO]) -> "Shape": def exportBin(self, f: Union[str, BytesIO]) -> bool: """ - Export this shape to a binary BREP file + Export this shape to a binary BREP file. """ rv = BinTools.Write_s(self.wrapped, f) @@ -534,7 +534,7 @@ def exportBin(self, f: Union[str, BytesIO]) -> bool: @classmethod def importBin(cls, f: Union[str, BytesIO]) -> "Shape": """ - Import shape from a binary BREP file + Import shape from a binary BREP file. """ s = TopoDS_Shape() diff --git a/cadquery/vis.py b/cadquery/vis.py index 08aeba7b3..7e9c8d14f 100644 --- a/cadquery/vis.py +++ b/cadquery/vis.py @@ -156,7 +156,7 @@ def show( win.SetWindowName("CQ viewer") - # get renderer and actor` + # get renderer and actor if edges: ren = win.GetRenderers().GetFirstRenderer() for act in ren.GetActors(): From feeefdd3bdd1ec7ae4f97ad6ce1a8610e375bff0 Mon Sep 17 00:00:00 2001 From: AU Date: Sun, 24 Nov 2024 12:27:15 +0100 Subject: [PATCH 80/80] Typo fixes --- doc/free-func.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/free-func.rst b/doc/free-func.rst index e6db8cff5..be8085834 100644 --- a/doc/free-func.rst +++ b/doc/free-func.rst @@ -241,7 +241,7 @@ Placement and creation of arrays is possible using :meth:`~cadquery.Shape.move` Text ---- -The free functon API has extensive text creation capabilities including text on planar curves and text on surfaces. +The free function API has extensive text creation capabilities including text on planar curves and text on surfaces. .. cadquery:: @@ -271,7 +271,7 @@ The free functon API has extensive text creation capabilities including text on # projected r3 = text(TXT, 1, spine, cf).moved(z=S) - # projected and thickend + # projected and thickened r4 = offset(r3, TH).moved(z=S) result = compound(r1, r2, r3, r4)