From 2efa2a3a093b096cb74246f6175cfee95e10977d Mon Sep 17 00:00:00 2001 From: gumyr Date: Sat, 16 Aug 2025 17:49:33 -0400 Subject: [PATCH] Most functionality working --- src/build123d/objects_curve.py | 2 +- src/build123d/topology/one_d.py | 221 ++++++++++++++++++++++++-------- tests/test_build_line.py | 5 + 3 files changed, 172 insertions(+), 56 deletions(-) diff --git a/src/build123d/objects_curve.py b/src/build123d/objects_curve.py index 3ea3adf..8a6cc46 100644 --- a/src/build123d/objects_curve.py +++ b/src/build123d/objects_curve.py @@ -1272,7 +1272,7 @@ class PointArcTangentArc(BaseEdgeObject): # Confirm new tangent point is colinear with point tangent on arc arc_dir = arc.tangent_at(tangent_point) - if tangent_dir.cross(arc_dir).length > TOLERANCE: + if tangent_dir.cross(arc_dir).length > TOLERANCE * 10: raise RuntimeError("No tangent arc found, found tangent out of tolerance.") arc = TangentArc(arc_point, tangent_point, tangent=arc_tangent) diff --git a/src/build123d/topology/one_d.py b/src/build123d/topology/one_d.py index ff75d64..2520d53 100644 --- a/src/build123d/topology/one_d.py +++ b/src/build123d/topology/one_d.py @@ -484,6 +484,52 @@ class Mixin1D(Shape): return result + def derivative_at( + self, + position: float | VectorLike, + order: int = 2, + position_mode: PositionMode = PositionMode.PARAMETER, + ) -> Vector: + """Derivative At + + Generate a derivative along the underlying curve. + + Args: + position (float | VectorLike): distance, parameter value or point + order (int): derivative order. Defaults to 2 + position_mode (PositionMode, optional): position calculation mode. Defaults to + PositionMode.PARAMETER. + + Raises: + ValueError: position must be a float or a point + + Returns: + Vector: position on the underlying curve + """ + if isinstance(position, (float, int)): + comp_curve, occt_param = self._occt_param_at(position, position_mode) + else: + try: + point_on_curve = Vector(position) + except Exception as exc: + raise ValueError("position must be a float or a point") from exc + if isinstance(self, Wire): + closest_edge = min( + self.edges(), key=lambda e: e.distance_to(point_on_curve) + ) + else: + closest_edge = self + u_value = closest_edge.param_at_point(point_on_curve) + comp_curve, occt_param = closest_edge._occt_param_at(u_value) + + derivative_gp_vec = comp_curve.DN(occt_param, order) + if derivative_gp_vec.Magnitude() == 0: + return Vector(0, 0, 0) + + if self.is_forward: + return Vector(derivative_gp_vec) + return Vector(derivative_gp_vec) * -1 + def edge(self) -> Edge | None: """Return the Edge""" return Shape.get_single_shape(self, "Edge") @@ -822,11 +868,11 @@ class Mixin1D(Shape): return line def position_at( - self, distance: float, position_mode: PositionMode = PositionMode.PARAMETER + self, position: float, position_mode: PositionMode = PositionMode.PARAMETER ) -> Vector: """Position At - Generate a position along the underlying curve. + Generate a position along the underlying Wire. Args: distance (float): distance or parameter value @@ -836,18 +882,12 @@ class Mixin1D(Shape): Returns: Vector: position on the underlying curve """ - curve = self.geom_adaptor() + # Find the TopoDS_Edge and parameter on that edge at given position + edge_curve_adaptor, occt_edge_param = self._occt_param_at( + position, position_mode + ) - if position_mode == PositionMode.PARAMETER: - if not self.is_forward: - distance = 1 - distance - param = self.param_at(distance) - else: - if not self.is_forward: - distance = self.length - distance - param = self.param_at(distance / self.length) - - return Vector(curve.Value(param)) + return Vector(edge_curve_adaptor.Value(occt_edge_param)) def positions( self, @@ -1191,51 +1231,10 @@ class Mixin1D(Shape): position_mode (PositionMode, optional): position calculation mode. Defaults to PositionMode.PARAMETER. - Raises: - ValueError: invalid position - Returns: Vector: tangent value """ - - if isinstance(position, (float, int)): - if not self.is_forward: - if position_mode == PositionMode.PARAMETER: - position = 1 - position - else: - position = self.length - position - - curve = self.geom_adaptor() - if position_mode == PositionMode.PARAMETER: - parameter = self.param_at(position) - else: - parameter = self.param_at(position / self.length) - else: - try: - pnt = Vector(position) - except Exception as exc: - raise ValueError("position must be a float or a point") from exc - # GeomAPI_ProjectPointOnCurve only works with Edges so find - # the closest Edge if the shape has multiple Edges. - my_edges: list[Edge] = self.edges() - distances = [(e.distance_to(pnt), i) for i, e in enumerate(my_edges)] - sorted_distances = sorted(distances, key=lambda x: x[0]) - closest_edge = my_edges[sorted_distances[0][1]] - # Get the extreme of the parameter values for this Edge - first: float = closest_edge.param_at(0) - last: float = closest_edge.param_at(1) - # Extract the Geom_Curve from the Shape - curve = BRep_Tool.Curve_s(closest_edge.wrapped, first, last) - projector = GeomAPI_ProjectPointOnCurve(pnt.to_pnt(), curve) - parameter = projector.LowerDistanceParameter() - - tmp = gp_Pnt() - res = gp_Vec() - curve.D1(parameter, tmp, res) - - if self.is_forward: - return Vector(gp_Dir(res)) - return Vector(gp_Dir(res)) * -1 + return self.derivative_at(position, 1, position_mode).normalized() def vertex(self) -> Vertex | None: """Return the Vertex""" @@ -1789,6 +1788,36 @@ class Edge(Mixin1D, Shape[TopoDS_Edge]): return return_value + # def derivative_at( + # self, + # position: float, + # order: int = 2, + # position_mode: PositionMode = PositionMode.PARAMETER, + # ) -> Vector: + # """Derivative At + + # Generate a derivative along the underlying curve. + + # Args: + # position (float): distance or parameter value + # order (int): derivative order. Defaults to 2 + # position_mode (PositionMode, optional): position calculation mode. Defaults to + # PositionMode.PARAMETER. + + # Returns: + # Vector: position on the underlying curve + # """ + # comp_curve, occt_param = self._occt_param_at(position, position_mode) + # derivative_gp_vec = comp_curve.DN(occt_param, order) + # if derivative_gp_vec.Magnitude() == 0: + # return Vector(0, 0, 0) + # else: + # gp_dir = gp_Dir(derivative_gp_vec) + + # if self.is_forward: + # return Vector(gp_dir) + # return Vector(gp_dir) * -1 + def distribute_locations( self: Wire | Edge, count: int, @@ -2092,6 +2121,26 @@ class Edge(Mixin1D, Shape[TopoDS_Edge]): return ShapeList(common_vertices + common_edges) return None + def _occt_param_at( + self, position: float, position_mode: PositionMode = PositionMode.PARAMETER + ) -> tuple[BRepAdaptor_CompCurve, float]: + comp_curve = self.geom_adaptor() + length = GCPnts_AbscissaPoint.Length_s(comp_curve) + + if position_mode == PositionMode.PARAMETER: + if not self.is_forward: + position = 1 - position + value = position + else: + if not self.is_forward: + position = self.length - position + value = position / self.length + + occt_param = GCPnts_AbscissaPoint( + comp_curve, length * value, comp_curve.FirstParameter() + ).Parameter() + return comp_curve, occt_param + def param_at_point(self, point: VectorLike) -> float: """param_at_point @@ -2163,6 +2212,24 @@ class Edge(Mixin1D, Shape[TopoDS_Edge]): raise RuntimeError("Unable to find parameter, Edge is too complex") + # def position_at( + # self, position: float, position_mode: PositionMode = PositionMode.PARAMETER + # ) -> Vector: + # """Position At + + # Generate a position along the underlying curve. + + # Args: + # position (float): distance or parameter value + # position_mode (PositionMode, optional): position calculation mode. Defaults to + # PositionMode.PARAMETER. + + # Returns: + # Vector: position on the underlying curve + # """ + # comp_curve, occt_param = self._occt_param_at(position, position_mode) + # return Vector(comp_curve.Value(occt_param)) + def project_to_shape( self, target_object: Shape, @@ -3000,6 +3067,50 @@ class Wire(Mixin1D, Shape[TopoDS_Wire]): return distance / wire_length + def _occt_param_at( + self, position: float, position_mode: PositionMode = PositionMode.PARAMETER + ) -> tuple[BRepAdaptor_CompCurve, float]: + wire_curve_adaptor = self.geom_adaptor() + + if position_mode == PositionMode.PARAMETER: + if not self.is_forward: + position = 1 - position + occt_wire_param = self.param_at(position) + else: + if not self.is_forward: + position = self.length - position + occt_wire_param = self.param_at(position / self.length) + + topods_edge_at_position = TopoDS_Edge() + occt_edge_params = wire_curve_adaptor.Edge( + occt_wire_param, topods_edge_at_position + ) + edge_curve_adaptor = BRepAdaptor_Curve(topods_edge_at_position) + + return edge_curve_adaptor, occt_edge_params[0] + + # def position_at( + # self, position: float, position_mode: PositionMode = PositionMode.PARAMETER + # ) -> Vector: + # """Position At + + # Generate a position along the underlying Wire. + + # Args: + # distance (float): distance or parameter value + # position_mode (PositionMode, optional): position calculation mode. Defaults to + # PositionMode.PARAMETER. + + # Returns: + # Vector: position on the underlying curve + # """ + # # Find the TopoDS_Edge and parameter on that edge at given position + # edge_curve_adaptor, occt_edge_param = self._occt_param_at( + # position, position_mode + # ) + + # return Vector(edge_curve_adaptor.Value(occt_edge_param)) + def project_to_shape( self, target_object: Shape, diff --git a/tests/test_build_line.py b/tests/test_build_line.py index 01c2fbe..145d43f 100644 --- a/tests/test_build_line.py +++ b/tests/test_build_line.py @@ -30,6 +30,8 @@ import unittest from math import sqrt, pi from build123d import * +from ocp_vscode import show + def _assertTupleAlmostEquals(self, expected, actual, places, msg=None): """Check Tuples""" @@ -673,6 +675,9 @@ class BuildLineTests(unittest.TestCase): # Check coincidence, tangency with each arc _, p1, p2 = start_arc.distance_to_with_closest_points(l1) + a1 = Axis(p1, start_arc.tangent_at(p1)) + a2 = Axis(p2, l1.tangent_at(p2)) + show(start_arc, l1, p1, p2, a1, a2) self.assertTupleAlmostEquals(tuple(p1), tuple(p2), 5) self.assertAlmostEqual( start_arc.tangent_at(p1).cross(l1.tangent_at(p2)).length, 0, 5