Fix NX curved edge sampling with robust UF_EVAL parsing

This commit is contained in:
2026-02-17 01:24:55 +00:00
parent 20d035205a
commit 89e0ffbbf2

View File

@@ -100,15 +100,36 @@ def _sample_edge_polyline(edge: Any, chord_tol_mm: float, lister: Any = None) ->
""" """
Sample an NX edge as a polyline. Sample an NX edge as a polyline.
Uses Edge.GetVertices() which returns (start_point, end_point). Strategy (in order):
For curved edges (arcs), we subdivide using the edge length and 1) Linear edges -> vertices only
IBaseCurve evaluation if available. 2) UF_EVAL sampling (robust parsing for NX Python return variants)
3) IBaseCurve.Evaluate fallback (if available)
NXOpen Python API on Edge: 4) UF arc analytic fallback for circular edges
- GetVertices() -> Tuple[Point3d, Point3d] (start, end) 5) Last resort -> vertices only
- GetLength() -> float (inherited from IBaseCurve)
- SolidEdgeType -> Edge.EdgeType
""" """
def _log(msg: str) -> None:
if lister:
lister.WriteLine(msg)
def _parse_eval_point(result: Any) -> Point3D | None:
"""Parse NX UF_EVAL/IBaseCurve return variants into a 3D point."""
# Direct NXOpen.Point3d-like object
if hasattr(result, "X") and hasattr(result, "Y") and hasattr(result, "Z"):
return (float(result.X), float(result.Y), float(result.Z))
# Flat numeric array [x,y,z,...]
if isinstance(result, (list, tuple)):
if len(result) >= 3 and all(isinstance(v, (int, float)) for v in result[:3]):
return (float(result[0]), float(result[1]), float(result[2]))
# Nested tuple patterns, e.g. (point,), (point, deriv), etc.
for item in result:
p = _parse_eval_point(item)
if p is not None:
return p
return None
# Get start and end vertices # Get start and end vertices
try: try:
v1, v2 = edge.GetVertices() v1, v2 = edge.GetVertices()
@@ -117,10 +138,10 @@ def _sample_edge_polyline(edge: Any, chord_tol_mm: float, lister: Any = None) ->
except Exception as exc: except Exception as exc:
raise RuntimeError(f"Edge.GetVertices() failed: {exc}") raise RuntimeError(f"Edge.GetVertices() failed: {exc}")
# Check edge type
is_linear = False is_linear = False
is_circular = False is_circular = False
is_closed = (_norm(_sub(p1, p2)) < 0.001) # closed edge = start == end is_closed = (_norm(_sub(p1, p2)) < 0.001)
edge_type_str = "?"
try: try:
edge_type_str = str(edge.SolidEdgeType) edge_type_str = str(edge.SolidEdgeType)
@@ -129,95 +150,172 @@ def _sample_edge_polyline(edge: Any, chord_tol_mm: float, lister: Any = None) ->
except Exception: except Exception:
pass pass
if lister and (is_closed or is_circular): # Point density driven by chord_tol_mm (tighter tol => more points)
lister.WriteLine(f"[edge] type={edge_type_str if 'edge_type_str' in dir() else '?'} closed={is_closed} circ={is_circular} p1={p1}") try:
length = float(edge.GetLength())
except Exception:
length = _norm(_sub(p2, p1)) if not is_closed else 50.0
tol = max(float(chord_tol_mm), 0.01)
n_pts = max(8, int(math.ceil(length / tol)))
if is_circular or is_closed:
n_pts = max(24, n_pts)
if is_linear and not is_closed: if is_linear and not is_closed:
return [p1, p2] return [p1, p2]
# For curved/closed edges: try UF_EVAL for parametric evaluation _log(
f"[edge] type={edge_type_str} closed={is_closed} circular={is_circular} "
f"len={length:.3f} tol={tol:.3f} n_pts={n_pts}"
)
# 1) Primary: UF_EVAL sampling
try: try:
import NXOpen import NXOpen
session = NXOpen.Session.GetSession() import NXOpen.UF
uf = session.GetUFSession()
edge_tag = edge.Tag
# Get edge length for point density uf = NXOpen.UF.UFSession.GetUFSession()
try: evaluator = uf.Eval.Initialize2(edge.Tag)
length = edge.GetLength()
except Exception:
length = _norm(_sub(p2, p1)) if not is_closed else 50.0 # estimate
n_pts = max(8, int(length / max(chord_tol_mm, 0.1)))
if is_closed:
n_pts = max(24, n_pts) # circles need enough points
# UF_EVAL approach
evaluator = uf.Eval.Initialize2(edge_tag)
limits = uf.Eval.AskLimits(evaluator) limits = uf.Eval.AskLimits(evaluator)
t0 = limits[0] t0, t1 = float(limits[0]), float(limits[1])
t1 = limits[1]
pts: List[Point3D] = [] pts: List[Point3D] = []
for i in range(n_pts + 1): parse_failures = 0
t = t0 + (t1 - t0) * (i / n_pts) try:
# UF_EVAL_evaluate returns (point[3], derivatives[3], ...) for i in range(n_pts + 1):
# The output format depends on the derivative order requested t = t0 + (t1 - t0) * (i / n_pts)
result = uf.Eval.Evaluate(evaluator, 0, t) result = uf.Eval.Evaluate(evaluator, 0, t)
# Try different result formats p = _parse_eval_point(result)
if isinstance(result, (list, tuple)): if p is None:
if len(result) >= 3: parse_failures += 1
pts.append((float(result[0]), float(result[1]), float(result[2]))) if parse_failures <= 3:
elif len(result) == 1 and hasattr(result[0], '__len__'): _log(f"[edge] UF_EVAL parse miss at t={t:.6g}, raw={repr(result)}")
r = result[0] continue
pts.append((float(r[0]), float(r[1]), float(r[2]))) pts.append(p)
elif hasattr(result, 'X'): finally:
pts.append((float(result.X), float(result.Y), float(result.Z))) try:
uf.Eval.Free(evaluator)
uf.Eval.Free(evaluator) except Exception:
pass
if len(pts) >= 2: if len(pts) >= 2:
_log(f"[edge] sampled via UF_EVAL ({len(pts)} pts)")
return pts return pts
except Exception:
pass
# Fallback for circular closed edges: try to get arc data from UF _log(f"[edge] UF_EVAL insufficient points ({len(pts)}), falling back")
if is_circular and is_closed: except Exception as exc:
_log(f"[edge] UF_EVAL failed: {exc}")
# 2) Fallback: IBaseCurve.Evaluate (signature differs by NX versions)
try:
pts: List[Point3D] = []
# Some NX APIs expose parameter limits directly on curve objects.
t0 = 0.0
t1 = 1.0
for lim_name in ("GetLimits", "AskLimits"):
lim_fn = getattr(edge, lim_name, None)
if callable(lim_fn):
try:
lim = lim_fn()
if isinstance(lim, (list, tuple)) and len(lim) >= 2:
t0, t1 = float(lim[0]), float(lim[1])
break
except Exception:
pass
for i in range(n_pts + 1):
t = t0 + (t1 - t0) * (i / n_pts)
result = edge.Evaluate(t)
p = _parse_eval_point(result)
if p is not None:
pts.append(p)
if len(pts) >= 2:
_log(f"[edge] sampled via IBaseCurve.Evaluate ({len(pts)} pts)")
return pts
_log(f"[edge] IBaseCurve.Evaluate insufficient points ({len(pts)})")
except Exception as exc:
_log(f"[edge] IBaseCurve.Evaluate failed: {exc}")
# 3) Circular analytic fallback using UF arc data
if is_circular:
try: try:
import NXOpen import NXOpen
session = NXOpen.Session.GetSession() import NXOpen.UF
uf = session.GetUFSession()
# UF_CURVE_ask_arc_data returns (arc_center, radius, angles...) uf = NXOpen.UF.UFSession.GetUFSession()
arc_data = uf.Curve.AskArcData(edge.Tag) arc_data = uf.Curve.AskArcData(edge.Tag)
# arc_data typically: (matrix_tag, start_angle, end_angle, center[3], radius)
# or it could be structured differently
# Generate circle points manually if we can extract center + radius
except Exception: # Robust extraction from varying arc_data layouts
pass center = None
radius = None
start_angle = None
end_angle = None
# Fallback for closed edges: generate a small circle around the vertex if hasattr(arc_data, "arc_center") and hasattr(arc_data, "radius"):
# This is wrong geometrically but at least provides a visual marker c = arc_data.arc_center
if is_closed: center = (float(c[0]), float(c[1]), float(c[2]))
try: radius = float(arc_data.radius)
length = edge.GetLength() start_angle = float(getattr(arc_data, "start_angle", 0.0))
radius = length / (2.0 * math.pi) end_angle = float(getattr(arc_data, "end_angle", 2.0 * math.pi))
# Generate circle in XY plane around vertex elif isinstance(arc_data, (list, tuple)):
# (will be projected to 2D later, so orientation matters) # Look for center candidate [x,y,z] and a scalar radius
pts = [] for item in arc_data:
n = 24 if center is None and isinstance(item, (list, tuple)) and len(item) >= 3:
for i in range(n + 1): if all(isinstance(v, (int, float)) for v in item[:3]):
angle = 2.0 * math.pi * i / n center = (float(item[0]), float(item[1]), float(item[2]))
px = p1[0] + radius * math.cos(angle) if radius is None and isinstance(item, (int, float)) and abs(float(item)) > 1e-9:
py = p1[1] + radius * math.sin(angle) # Keep first non-zero scalar as probable radius only if still missing
pz = p1[2] radius = float(item) if radius is None else radius
pts.append((px, py, pz))
return pts
except Exception:
pass
# Last resort: vertices only nums = [float(x) for x in arc_data if isinstance(x, (int, float))]
if len(nums) >= 2:
start_angle = nums[-2]
end_angle = nums[-1]
if center is not None and radius is not None and radius > 0.0:
# Build local basis in edge plane from endpoints + center
r1 = _sub(p1, center)
r2 = _sub(p2, center)
if _norm(r1) < 1e-9:
r1 = (radius, 0.0, 0.0)
x_axis = _normalize(r1)
normal = _normalize(_cross(r1, r2)) if _norm(_cross(r1, r2)) > 1e-9 else (0.0, 0.0, 1.0)
y_axis = _normalize(_cross(normal, x_axis))
a0 = 0.0
a1 = math.atan2(_dot(r2, y_axis), _dot(r2, x_axis))
if is_closed and abs(a1) < 1e-9:
a1 = 2.0 * math.pi
elif a1 <= 0.0:
a1 += 2.0 * math.pi
# If UF supplied angles, prefer them when they look valid
if start_angle is not None and end_angle is not None:
da = end_angle - start_angle
if abs(da) > 1e-9:
a0, a1 = start_angle, end_angle
pts = []
for i in range(n_pts + 1):
a = a0 + (a1 - a0) * (i / n_pts)
ca, sa = math.cos(a), math.sin(a)
px = center[0] + radius * (ca * x_axis[0] + sa * y_axis[0])
py = center[1] + radius * (ca * x_axis[1] + sa * y_axis[1])
pz = center[2] + radius * (ca * x_axis[2] + sa * y_axis[2])
pts.append((px, py, pz))
if len(pts) >= 2:
_log(f"[edge] sampled via UF arc analytic ({len(pts)} pts)")
return pts
_log(f"[edge] UF arc fallback could not decode arc_data: {repr(arc_data)}")
except Exception as exc:
_log(f"[edge] UF arc fallback failed: {exc}")
_log("[edge] fallback to vertices only")
return [p1, p2] return [p1, p2]
@@ -237,6 +335,7 @@ def _chain_edges_into_loops(
edges: List[Any], edges: List[Any],
lister: Any = None, lister: Any = None,
tol: float = 0.01, tol: float = 0.01,
chord_tol_mm: float = 0.1,
) -> List[Tuple[bool, List[Point3D]]]: ) -> List[Tuple[bool, List[Point3D]]]:
""" """
Chain edges into closed loops by matching vertex endpoints. Chain edges into closed loops by matching vertex endpoints.
@@ -290,7 +389,7 @@ def _chain_edges_into_loops(
p_start, p_end, edge = segments[start_idx] p_start, p_end, edge = segments[start_idx]
# Sample this edge # Sample this edge
edge_pts = _sample_edge_polyline(edge, chord_tol_mm=0.5, lister=lister) edge_pts = _sample_edge_polyline(edge, chord_tol_mm=chord_tol_mm, lister=lister)
chain_pts.extend(edge_pts) chain_pts.extend(edge_pts)
chain_edges.append(edge) chain_edges.append(edge)
@@ -311,7 +410,7 @@ def _chain_edges_into_loops(
continue continue
if pts_match(current_end, s1): if pts_match(current_end, s1):
used[i] = True used[i] = True
edge_pts = _sample_edge_polyline(e, chord_tol_mm=0.5, lister=lister) edge_pts = _sample_edge_polyline(e, chord_tol_mm=chord_tol_mm, lister=lister)
chain_pts.extend(edge_pts[1:]) # skip duplicate junction point chain_pts.extend(edge_pts[1:]) # skip duplicate junction point
chain_edges.append(e) chain_edges.append(e)
current_end = s2 current_end = s2
@@ -320,7 +419,7 @@ def _chain_edges_into_loops(
elif pts_match(current_end, s2): elif pts_match(current_end, s2):
# Edge is reversed — traverse backward # Edge is reversed — traverse backward
used[i] = True used[i] = True
edge_pts = _sample_edge_polyline(e, chord_tol_mm=0.5, lister=lister) edge_pts = _sample_edge_polyline(e, chord_tol_mm=chord_tol_mm, lister=lister)
edge_pts.reverse() edge_pts.reverse()
chain_pts.extend(edge_pts[1:]) chain_pts.extend(edge_pts[1:])
chain_edges.append(e) chain_edges.append(e)
@@ -621,7 +720,7 @@ def extract_sandbox_geometry(
all_edges = list(face.GetEdges()) all_edges = list(face.GetEdges())
lister.WriteLine(f"[extract_sandbox] {sandbox_id}: {len(all_edges)} edges on face") lister.WriteLine(f"[extract_sandbox] {sandbox_id}: {len(all_edges)} edges on face")
loops = _chain_edges_into_loops(all_edges, lister) loops = _chain_edges_into_loops(all_edges, lister, chord_tol_mm=chord_tol_mm)
lister.WriteLine(f"[extract_sandbox] {sandbox_id}: {len(loops)} loop(s) built") lister.WriteLine(f"[extract_sandbox] {sandbox_id}: {len(loops)} loop(s) built")
for loop_index, (is_outer, loop_pts3d) in enumerate(loops): for loop_index, (is_outer, loop_pts3d) in enumerate(loops):