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.
|
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] = []
|
||||||
|
parse_failures = 0
|
||||||
|
try:
|
||||||
for i in range(n_pts + 1):
|
for i in range(n_pts + 1):
|
||||||
t = t0 + (t1 - t0) * (i / n_pts)
|
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)
|
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
|
||||||
|
|
||||||
|
_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:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# Fallback for circular closed edges: try to get arc data from UF
|
for i in range(n_pts + 1):
|
||||||
if is_circular and is_closed:
|
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
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
# 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 = []
|
pts = []
|
||||||
n = 24
|
for i in range(n_pts + 1):
|
||||||
for i in range(n + 1):
|
a = a0 + (a1 - a0) * (i / n_pts)
|
||||||
angle = 2.0 * math.pi * i / n
|
ca, sa = math.cos(a), math.sin(a)
|
||||||
px = p1[0] + radius * math.cos(angle)
|
px = center[0] + radius * (ca * x_axis[0] + sa * y_axis[0])
|
||||||
py = p1[1] + radius * math.sin(angle)
|
py = center[1] + radius * (ca * x_axis[1] + sa * y_axis[1])
|
||||||
pz = p1[2]
|
pz = center[2] + radius * (ca * x_axis[2] + sa * y_axis[2])
|
||||||
pts.append((px, py, pz))
|
pts.append((px, py, pz))
|
||||||
return pts
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
# Last resort: vertices only
|
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):
|
||||||
|
|||||||
Reference in New Issue
Block a user