Most functionality working

This commit is contained in:
gumyr 2025-08-16 17:49:33 -04:00
parent 94d0d2a868
commit 2efa2a3a09
3 changed files with 172 additions and 56 deletions

View file

@ -1272,7 +1272,7 @@ class PointArcTangentArc(BaseEdgeObject):
# Confirm new tangent point is colinear with point tangent on arc # Confirm new tangent point is colinear with point tangent on arc
arc_dir = arc.tangent_at(tangent_point) 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.") raise RuntimeError("No tangent arc found, found tangent out of tolerance.")
arc = TangentArc(arc_point, tangent_point, tangent=arc_tangent) arc = TangentArc(arc_point, tangent_point, tangent=arc_tangent)

View file

@ -484,6 +484,52 @@ class Mixin1D(Shape):
return result 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: def edge(self) -> Edge | None:
"""Return the Edge""" """Return the Edge"""
return Shape.get_single_shape(self, "Edge") return Shape.get_single_shape(self, "Edge")
@ -822,11 +868,11 @@ class Mixin1D(Shape):
return line return line
def position_at( def position_at(
self, distance: float, position_mode: PositionMode = PositionMode.PARAMETER self, position: float, position_mode: PositionMode = PositionMode.PARAMETER
) -> Vector: ) -> Vector:
"""Position At """Position At
Generate a position along the underlying curve. Generate a position along the underlying Wire.
Args: Args:
distance (float): distance or parameter value distance (float): distance or parameter value
@ -836,18 +882,12 @@ class Mixin1D(Shape):
Returns: Returns:
Vector: position on the underlying curve 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: return Vector(edge_curve_adaptor.Value(occt_edge_param))
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))
def positions( def positions(
self, self,
@ -1191,51 +1231,10 @@ class Mixin1D(Shape):
position_mode (PositionMode, optional): position calculation mode. position_mode (PositionMode, optional): position calculation mode.
Defaults to PositionMode.PARAMETER. Defaults to PositionMode.PARAMETER.
Raises:
ValueError: invalid position
Returns: Returns:
Vector: tangent value Vector: tangent value
""" """
return self.derivative_at(position, 1, position_mode).normalized()
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
def vertex(self) -> Vertex | None: def vertex(self) -> Vertex | None:
"""Return the Vertex""" """Return the Vertex"""
@ -1789,6 +1788,36 @@ class Edge(Mixin1D, Shape[TopoDS_Edge]):
return return_value 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( def distribute_locations(
self: Wire | Edge, self: Wire | Edge,
count: int, count: int,
@ -2092,6 +2121,26 @@ class Edge(Mixin1D, Shape[TopoDS_Edge]):
return ShapeList(common_vertices + common_edges) return ShapeList(common_vertices + common_edges)
return None 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: def param_at_point(self, point: VectorLike) -> float:
"""param_at_point """param_at_point
@ -2163,6 +2212,24 @@ class Edge(Mixin1D, Shape[TopoDS_Edge]):
raise RuntimeError("Unable to find parameter, Edge is too complex") 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( def project_to_shape(
self, self,
target_object: Shape, target_object: Shape,
@ -3000,6 +3067,50 @@ class Wire(Mixin1D, Shape[TopoDS_Wire]):
return distance / wire_length 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( def project_to_shape(
self, self,
target_object: Shape, target_object: Shape,

View file

@ -30,6 +30,8 @@ import unittest
from math import sqrt, pi from math import sqrt, pi
from build123d import * from build123d import *
from ocp_vscode import show
def _assertTupleAlmostEquals(self, expected, actual, places, msg=None): def _assertTupleAlmostEquals(self, expected, actual, places, msg=None):
"""Check Tuples""" """Check Tuples"""
@ -673,6 +675,9 @@ class BuildLineTests(unittest.TestCase):
# Check coincidence, tangency with each arc # Check coincidence, tangency with each arc
_, p1, p2 = start_arc.distance_to_with_closest_points(l1) _, 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.assertTupleAlmostEquals(tuple(p1), tuple(p2), 5)
self.assertAlmostEqual( self.assertAlmostEqual(
start_arc.tangent_at(p1).cross(l1.tangent_at(p2)).length, 0, 5 start_arc.tangent_at(p1).cross(l1.tangent_at(p2)).length, 0, 5