diff --git a/cadquery/occ_impl/shapes.py b/cadquery/occ_impl/shapes.py index 5390d8e69..c26889dbc 100644 --- a/cadquery/occ_impl/shapes.py +++ b/cadquery/occ_impl/shapes.py @@ -36,7 +36,7 @@ # collection of pints (used for spline construction) from OCP.TColgp import TColgp_HArray1OfPnt -from OCP.BRepAdaptor import BRepAdaptor_Curve, BRepAdaptor_Surface +from OCP.BRepAdaptor import BRepAdaptor_Curve, BRepAdaptor_Surface, BRepAdaptor_HCurve from OCP.BRepBuilderAPI import ( BRepBuilderAPI_MakeVertex, BRepBuilderAPI_MakeEdge, @@ -168,6 +168,17 @@ from OCP.ShapeAnalysis import ShapeAnalysis_FreeBounds from OCP.TopTools import TopTools_HSequenceOfShape +from OCP.GCPnts import GCPnts_AbscissaPoint + +from OCP.GeomFill import ( + GeomFill_Frenet, + GeomFill_CorrectedFrenet, + GeomFill_CorrectedFrenet, + GeomFill_DiscreteTrihedron, + GeomFill_ConstantBiNormal, + GeomFill_DraftTrihedron, + GeomFill_TrihedronLaw, +) from math import pi, sqrt import warnings @@ -1070,13 +1081,69 @@ def makeTangentArc(cls: Type["Edge"], v1: Vector, v2: Vector, v3: Vector) -> "Ed @classmethod def makeLine(cls: Type["Edge"], v1: Vector, v2: Vector) -> "Edge": """ - Create a line between two points - :param v1: Vector that represents the first point - :param v2: Vector that represents the second point - :return: A linear edge between the two provided points + Create a line between two points + :param v1: Vector that represents the first point + :param v2: Vector that represents the second point + :return: A linear edge between the two provided points """ return cls(BRepBuilderAPI_MakeEdge(v1.toPnt(), v2.toPnt()).Edge()) + def locationAt( + self, + d: float, + mode: Literal["length", "parameter"] = "length", + frame: Literal["frenet", "corrected"] = "frenet", + ) -> Location: + """Generate location along the curve + :param d: distance or parameter value + :param mode: position calculation mode (default: length) + :param frame: moving frame calculation method (default: frenet) + :return: A Location object representing local coordinate system at the specified distance. + """ + + curve = BRepAdaptor_Curve(self.wrapped) + + if mode == "length": + l = GCPnts_AbscissaPoint.Length_s(curve) + param = GCPnts_AbscissaPoint(curve, l * d, 0).Parameter() + else: + param = d + + law: GeomFill_TrihedronLaw + if frame == "frenet": + law = GeomFill_Frenet() + else: + law = GeomFill_CorrectedFrenet() + + law.SetCurve(BRepAdaptor_HCurve(curve)) + + tangent, normal, binormal = gp_Vec(), gp_Vec(), gp_Vec() + + law.D0(param, tangent, normal, binormal) + pnt = curve.Value(param) + + T = gp_Trsf() + T.SetTransformation( + gp_Ax3(pnt, gp_Dir(tangent.XYZ()), gp_Dir(normal.XYZ())), gp_Ax3() + ) + + return Location(TopLoc_Location(T)) + + def locations( + self, + ds: Iterable[float], + mode: Literal["length", "parameter"] = "length", + frame: Literal["frenet", "corrected"] = "frenet", + ) -> List[Location]: + """Generate location along the curve + :param ds: distance or parameter values + :param mode: position calculation mode (default: length) + :param frame: moving frame calculation method (default: frenet) + :return: A list of Location objects representing local coordinate systems at the specified distances. + """ + + return [self.locationAt(d, mode, frame) for d in ds] + class Wire(Shape, Mixin1D): """ diff --git a/tests/test_cadquery.py b/tests/test_cadquery.py index 0649cb9c7..10b7d8a9f 100644 --- a/tests/test_cadquery.py +++ b/tests/test_cadquery.py @@ -3559,3 +3559,33 @@ def testConsolidateWires(self): w1 = Workplane().consolidateWires() self.assertEqual(w1.size(), 0) + + def testLocationAt(self): + + r = 1 + e = Wire.makeHelix(r, r, r).Edges()[0] + + locs_frenet = e.locations([0, 1], frame="frenet") + + T1 = locs_frenet[0].wrapped.Transformation() + T2 = locs_frenet[1].wrapped.Transformation() + + self.assertAlmostEqual(T1.TranslationPart().X(), r, 6) + self.assertAlmostEqual(T2.TranslationPart().X(), r, 6) + self.assertAlmostEqual( + T1.GetRotation().GetRotationAngle(), -T2.GetRotation().GetRotationAngle(), 6 + ) + + ga = e._geomAdaptor() + + locs_corrected = e.locations( + [ga.FirstParameter(), ga.LastParameter()], + mode="parameter", + frame="corrected", + ) + + T3 = locs_corrected[0].wrapped.Transformation() + T4 = locs_corrected[1].wrapped.Transformation() + + self.assertAlmostEqual(T3.TranslationPart().X(), r, 6) + self.assertAlmostEqual(T4.TranslationPart().X(), r, 6)