Fix NX curved edge sampling with robust UF_EVAL parsing
This commit is contained in:
@@ -100,15 +100,36 @@ def _sample_edge_polyline(edge: Any, chord_tol_mm: float, lister: Any = None) ->
|
||||
"""
|
||||
Sample an NX edge as a polyline.
|
||||
|
||||
Uses Edge.GetVertices() which returns (start_point, end_point).
|
||||
For curved edges (arcs), we subdivide using the edge length and
|
||||
IBaseCurve evaluation if available.
|
||||
|
||||
NXOpen Python API on Edge:
|
||||
- GetVertices() -> Tuple[Point3d, Point3d] (start, end)
|
||||
- GetLength() -> float (inherited from IBaseCurve)
|
||||
- SolidEdgeType -> Edge.EdgeType
|
||||
Strategy (in order):
|
||||
1) Linear edges -> vertices only
|
||||
2) UF_EVAL sampling (robust parsing for NX Python return variants)
|
||||
3) IBaseCurve.Evaluate fallback (if available)
|
||||
4) UF arc analytic fallback for circular edges
|
||||
5) Last resort -> vertices only
|
||||
"""
|
||||
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
|
||||
try:
|
||||
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:
|
||||
raise RuntimeError(f"Edge.GetVertices() failed: {exc}")
|
||||
|
||||
# Check edge type
|
||||
is_linear = 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:
|
||||
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:
|
||||
pass
|
||||
|
||||
if lister and (is_closed or is_circular):
|
||||
lister.WriteLine(f"[edge] type={edge_type_str if 'edge_type_str' in dir() else '?'} closed={is_closed} circ={is_circular} p1={p1}")
|
||||
# Point density driven by chord_tol_mm (tighter tol => more points)
|
||||
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:
|
||||
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:
|
||||
import NXOpen
|
||||
session = NXOpen.Session.GetSession()
|
||||
uf = session.GetUFSession()
|
||||
edge_tag = edge.Tag
|
||||
import NXOpen.UF
|
||||
|
||||
# Get edge length for point density
|
||||
try:
|
||||
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)
|
||||
uf = NXOpen.UF.UFSession.GetUFSession()
|
||||
evaluator = uf.Eval.Initialize2(edge.Tag)
|
||||
limits = uf.Eval.AskLimits(evaluator)
|
||||
t0 = limits[0]
|
||||
t1 = limits[1]
|
||||
t0, t1 = float(limits[0]), float(limits[1])
|
||||
|
||||
pts: List[Point3D] = []
|
||||
for i in range(n_pts + 1):
|
||||
t = t0 + (t1 - t0) * (i / n_pts)
|
||||
# UF_EVAL_evaluate returns (point[3], derivatives[3], ...)
|
||||
# The output format depends on the derivative order requested
|
||||
result = uf.Eval.Evaluate(evaluator, 0, t)
|
||||
# Try different result formats
|
||||
if isinstance(result, (list, tuple)):
|
||||
if len(result) >= 3:
|
||||
pts.append((float(result[0]), float(result[1]), float(result[2])))
|
||||
elif len(result) == 1 and hasattr(result[0], '__len__'):
|
||||
r = result[0]
|
||||
pts.append((float(r[0]), float(r[1]), float(r[2])))
|
||||
elif hasattr(result, 'X'):
|
||||
pts.append((float(result.X), float(result.Y), float(result.Z)))
|
||||
|
||||
uf.Eval.Free(evaluator)
|
||||
parse_failures = 0
|
||||
try:
|
||||
for i in range(n_pts + 1):
|
||||
t = t0 + (t1 - t0) * (i / n_pts)
|
||||
result = uf.Eval.Evaluate(evaluator, 0, t)
|
||||
p = _parse_eval_point(result)
|
||||
if p is None:
|
||||
parse_failures += 1
|
||||
if parse_failures <= 3:
|
||||
_log(f"[edge] UF_EVAL parse miss at t={t:.6g}, raw={repr(result)}")
|
||||
continue
|
||||
pts.append(p)
|
||||
finally:
|
||||
try:
|
||||
uf.Eval.Free(evaluator)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if len(pts) >= 2:
|
||||
_log(f"[edge] sampled via UF_EVAL ({len(pts)} pts)")
|
||||
return pts
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Fallback for circular closed edges: try to get arc data from UF
|
||||
if is_circular and is_closed:
|
||||
_log(f"[edge] UF_EVAL insufficient points ({len(pts)}), falling back")
|
||||
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:
|
||||
import NXOpen
|
||||
session = NXOpen.Session.GetSession()
|
||||
uf = session.GetUFSession()
|
||||
import NXOpen.UF
|
||||
|
||||
# UF_CURVE_ask_arc_data returns (arc_center, radius, angles...)
|
||||
uf = NXOpen.UF.UFSession.GetUFSession()
|
||||
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:
|
||||
pass
|
||||
# Robust extraction from varying arc_data layouts
|
||||
center = None
|
||||
radius = None
|
||||
start_angle = None
|
||||
end_angle = None
|
||||
|
||||
# Fallback for closed edges: generate a small circle around the vertex
|
||||
# This is wrong geometrically but at least provides a visual marker
|
||||
if is_closed:
|
||||
try:
|
||||
length = edge.GetLength()
|
||||
radius = length / (2.0 * math.pi)
|
||||
# Generate circle in XY plane around vertex
|
||||
# (will be projected to 2D later, so orientation matters)
|
||||
pts = []
|
||||
n = 24
|
||||
for i in range(n + 1):
|
||||
angle = 2.0 * math.pi * i / n
|
||||
px = p1[0] + radius * math.cos(angle)
|
||||
py = p1[1] + radius * math.sin(angle)
|
||||
pz = p1[2]
|
||||
pts.append((px, py, pz))
|
||||
return pts
|
||||
except Exception:
|
||||
pass
|
||||
if hasattr(arc_data, "arc_center") and hasattr(arc_data, "radius"):
|
||||
c = arc_data.arc_center
|
||||
center = (float(c[0]), float(c[1]), float(c[2]))
|
||||
radius = float(arc_data.radius)
|
||||
start_angle = float(getattr(arc_data, "start_angle", 0.0))
|
||||
end_angle = float(getattr(arc_data, "end_angle", 2.0 * math.pi))
|
||||
elif isinstance(arc_data, (list, tuple)):
|
||||
# Look for center candidate [x,y,z] and a scalar radius
|
||||
for item in arc_data:
|
||||
if center is None and isinstance(item, (list, tuple)) and len(item) >= 3:
|
||||
if all(isinstance(v, (int, float)) for v in item[:3]):
|
||||
center = (float(item[0]), float(item[1]), float(item[2]))
|
||||
if radius is None and isinstance(item, (int, float)) and abs(float(item)) > 1e-9:
|
||||
# Keep first non-zero scalar as probable radius only if still missing
|
||||
radius = float(item) if radius is None else radius
|
||||
|
||||
# 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]
|
||||
|
||||
|
||||
@@ -237,6 +335,7 @@ def _chain_edges_into_loops(
|
||||
edges: List[Any],
|
||||
lister: Any = None,
|
||||
tol: float = 0.01,
|
||||
chord_tol_mm: float = 0.1,
|
||||
) -> List[Tuple[bool, List[Point3D]]]:
|
||||
"""
|
||||
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]
|
||||
|
||||
# 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_edges.append(edge)
|
||||
|
||||
@@ -311,7 +410,7 @@ def _chain_edges_into_loops(
|
||||
continue
|
||||
if pts_match(current_end, s1):
|
||||
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_edges.append(e)
|
||||
current_end = s2
|
||||
@@ -320,7 +419,7 @@ def _chain_edges_into_loops(
|
||||
elif pts_match(current_end, s2):
|
||||
# Edge is reversed — traverse backward
|
||||
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()
|
||||
chain_pts.extend(edge_pts[1:])
|
||||
chain_edges.append(e)
|
||||
@@ -621,7 +720,7 @@ def extract_sandbox_geometry(
|
||||
all_edges = list(face.GetEdges())
|
||||
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")
|
||||
|
||||
for loop_index, (is_outer, loop_pts3d) in enumerate(loops):
|
||||
|
||||
Reference in New Issue
Block a user