fix: rewrite edge sampling + loop building using verified NXOpen API (GetVertices, GetEdges, GetLength, UF.Eval)

This commit is contained in:
2026-02-16 17:46:52 +00:00
parent 851a8d3df0
commit 98d510154d

View File

@@ -98,30 +98,76 @@ def unproject_to_3d(points2d: Sequence[Point2D], frame: LocalFrame) -> List[Poin
def _sample_edge_polyline(edge: Any, chord_tol_mm: float) -> List[Point3D]:
"""
Sample an NX edge as a polyline with adaptive point density.
Falls back to vertex extraction if evaluator is unavailable.
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
"""
# Preferred: parametric evaluator
# Get start and end vertices
try:
evaluator = edge.CreateEvaluator()
t0, t1 = evaluator.GetLimits()
length = edge.GetLength()
n = max(2, int(length / max(chord_tol_mm, 1e-3)))
v1, v2 = edge.GetVertices()
p1 = (float(v1.X), float(v1.Y), float(v1.Z))
p2 = (float(v2.X), float(v2.Y), float(v2.Z))
except Exception as exc:
raise RuntimeError(f"Edge.GetVertices() failed: {exc}")
# Check if edge is straight (linear) — vertices are sufficient
try:
edge_type = edge.SolidEdgeType
# EdgeType values: Linear=1, Circular=2, Elliptical=3, etc.
is_linear = (str(edge_type) == "EdgeType.Linear" or int(edge_type) == 1)
except Exception:
is_linear = False
if is_linear:
return [p1, p2]
# For curved edges: try to evaluate intermediate points
# Method: use NXOpen.Session.GetSession().GetUFSession() for UF curve evaluation
try:
import NXOpen
session = NXOpen.Session.GetSession()
uf = session.GetUFSession()
edge_tag = edge.Tag
# Get edge length
try:
length = edge.GetLength()
except Exception:
length = _norm(_sub(p2, p1))
n_pts = max(2, int(length / max(chord_tol_mm, 0.1)))
# UF_EVAL: initialize evaluator, get points along curve
evaluator = uf.Eval.Initialize2(edge_tag)
limits = uf.Eval.AskLimits(evaluator)
t0, t1 = limits[0], limits[1]
pts: List[Point3D] = []
for i in range(n + 1):
t = t0 + (t1 - t0) * (i / n)
p, _ = evaluator.Evaluate(t)
pts.append((float(p.X), float(p.Y), float(p.Z)))
return pts
for i in range(n_pts + 1):
t = t0 + (t1 - t0) * (i / n_pts)
result = uf.Eval.Evaluate(evaluator, 0, t)
# result is typically a tuple/array with (x, y, z, ...)
if hasattr(result, '__len__') and len(result) >= 3:
pts.append((float(result[0]), float(result[1]), float(result[2])))
else:
break
uf.Eval.Free(evaluator)
if len(pts) >= 2:
return pts
except Exception:
pass
# Fallback: edge vertices only
try:
verts = edge.GetVertices()
return [(float(v.Coordinates.X), float(v.Coordinates.Y), float(v.Coordinates.Z)) for v in verts]
except Exception as exc:
raise RuntimeError(f"Could not sample edge polyline: {exc}")
# Last fallback: just vertices (straight-line approximation of curve)
return [p1, p2]
def _close_polyline(points: List[Point3D]) -> List[Point3D]:
@@ -136,93 +182,141 @@ def _close_polyline(points: List[Point3D]) -> List[Point3D]:
# Face local frame
# ---------------------------------------------------------------------------
def _get_face_loops(face: Any, lister: Any = None) -> List[Tuple[bool, List[Any]]]:
def _chain_edges_into_loops(
edges: List[Any],
lister: Any = None,
tol: float = 0.01,
) -> List[Tuple[bool, List[Point3D]]]:
"""
Get edge loops from an NX face.
Returns list of (is_outer, [edges]) tuples.
Chain edges into closed loops by matching vertex endpoints.
Tries multiple NX API patterns:
1. face.GetEdgeLoops() (NX 2306+)
2. UF layer: UF.Modeling.ask_face_loops()
3. Fallback: all edges as single outer loop
Returns list of (is_outer, points_3d) tuples.
The largest loop (by area/perimeter) is assumed to be the outer loop.
"""
def _log(msg):
if lister:
lister.WriteLine(msg)
# Method 1: GetEdgeLoops (modern NX)
try:
edge_loops = face.GetEdgeLoops()
if edge_loops:
result = []
for i, el in enumerate(edge_loops):
edges = el.GetEdges() if hasattr(el, "GetEdges") else list(el)
is_outer = (i == 0) # first loop is typically outer
try:
is_outer = el.IsOuter() if hasattr(el, "IsOuter") else (i == 0)
except Exception:
pass
result.append((is_outer, list(edges)))
_log(f"[loops] GetEdgeLoops: {len(result)} loop(s)")
return result
except Exception:
pass
if not edges:
return []
# Method 2: UF layer
try:
import NXOpen
session = NXOpen.Session.GetSession()
uf = session.GetUFSession()
# Build edge segments as (start_pt, end_pt, edge_ref)
segments = []
for edge in edges:
try:
v1, v2 = edge.GetVertices()
p1 = (float(v1.X), float(v1.Y), float(v1.Z))
p2 = (float(v2.X), float(v2.Y), float(v2.Z))
segments.append((p1, p2, edge))
except Exception as exc:
_log(f"[chain] Edge.GetVertices failed: {exc}")
continue
face_tag = face.Tag
loop_count = [0]
loop_list = uf.Modeling.AskFaceLoops(face_tag)
# loop_list is (num_loops, loop_tags[])
if loop_list and len(loop_list) >= 2:
num_loops = loop_list[0]
loop_tags = loop_list[1]
result = []
for li in range(num_loops):
loop_tag = loop_tags[li]
# Get edges from loop
edge_info = uf.Modeling.AskLoopListOfEdges(loop_tag)
edges_tags = edge_info[1] if len(edge_info) >= 2 else []
edges = []
for et in edges_tags:
try:
edge_obj = NXOpen.TaggedObjectManager.GetTaggedObject(et)
edges.append(edge_obj)
except Exception:
pass
is_outer_val = uf.Modeling.AskLoopType(loop_tag)
is_outer = (is_outer_val == 1) # 1 = outer, 2 = inner typically
result.append((is_outer, edges))
_log(f"[loops] UF layer: {len(result)} loop(s)")
return result
except Exception:
pass
_log(f"[chain] {len(segments)} edge segments to chain")
# Method 3: Fallback — all edges as single outer loop
try:
all_edges = face.GetEdges()
if all_edges:
edges = list(all_edges)
_log(f"[loops] Fallback: {len(edges)} edges as single outer loop")
return [(True, edges)]
except Exception:
pass
# Chain into loops
used = [False] * len(segments)
loops_points: List[List[Point3D]] = []
loops_edges: List[List[Any]] = []
return []
def pts_match(a: Point3D, b: Point3D) -> bool:
return _norm(_sub(a, b)) < tol
while True:
# Find first unused segment
start_idx = None
for i, u in enumerate(used):
if not u:
start_idx = i
break
if start_idx is None:
break
# Start a new loop
chain_pts: List[Point3D] = []
chain_edges: List[Any] = []
used[start_idx] = True
p_start, p_end, edge = segments[start_idx]
# Sample this edge
edge_pts = _sample_edge_polyline(edge, chord_tol_mm=0.5)
chain_pts.extend(edge_pts)
chain_edges.append(edge)
current_end = p_end
loop_start = p_start
# Follow the chain
max_iters = len(segments) + 1
for _ in range(max_iters):
if pts_match(current_end, loop_start) and len(chain_edges) > 1:
# Loop closed
break
# Find next segment connecting to current_end
found = False
for i, (s1, s2, e) in enumerate(segments):
if used[i]:
continue
if pts_match(current_end, s1):
used[i] = True
edge_pts = _sample_edge_polyline(e, chord_tol_mm=0.5)
chain_pts.extend(edge_pts[1:]) # skip duplicate junction point
chain_edges.append(e)
current_end = s2
found = True
break
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)
edge_pts.reverse()
chain_pts.extend(edge_pts[1:])
chain_edges.append(e)
current_end = s1
found = True
break
if not found:
_log(f"[chain] Warning: could not continue chain at {current_end}")
break
loops_points.append(chain_pts)
loops_edges.append(chain_edges)
_log(f"[chain] Built {len(loops_points)} loop(s)")
if not loops_points:
return []
# Determine which loop is outer (largest perimeter)
def _perimeter(pts: List[Point3D]) -> float:
total = 0.0
for i in range(len(pts) - 1):
total += _norm(_sub(pts[i + 1], pts[i]))
return total
perimeters = [_perimeter(pts) for pts in loops_points]
outer_idx = perimeters.index(max(perimeters))
result: List[Tuple[bool, List[Point3D]]] = []
for i, pts in enumerate(loops_points):
is_outer = (i == outer_idx)
result.append((is_outer, pts))
_log(f"[chain] loop {i}: {len(pts)} pts, perimeter={perimeters[i]:.1f} mm {'(OUTER)' if is_outer else '(inner)'}")
return result
def _face_local_frame(face: Any, lister: Any = None) -> LocalFrame:
"""
Build a stable local frame on a planar face.
"""
# Get a sample point from the first edge
# Get a sample point from the first edge vertex
edges = face.GetEdges()
first_edge = edges[0]
sample = _sample_edge_polyline(first_edge, chord_tol_mm=1.0)[0]
v1, v2 = first_edge.GetVertices()
sample = (float(v1.X), float(v1.Y), float(v1.Z))
# Get face normal
normal = (0.0, 0.0, 1.0)
@@ -472,17 +566,14 @@ def extract_sandbox_geometry(
outer_2d: List[List[float]] = []
inner_boundaries: List[Dict[str, Any]] = []
loops = _get_face_loops(face, lister)
lister.WriteLine(f"[extract_sandbox] {sandbox_id}: {len(loops)} loop(s)")
# Get all edges on the face and chain them into loops
all_edges = list(face.GetEdges())
lister.WriteLine(f"[extract_sandbox] {sandbox_id}: {len(all_edges)} edges on face")
for loop_index, (is_outer, edges) in enumerate(loops):
loop_pts3d: List[Point3D] = []
for edge in edges:
pts = _sample_edge_polyline(edge, chord_tol_mm)
if loop_pts3d and pts:
pts = pts[1:]
loop_pts3d.extend(pts)
loops = _chain_edges_into_loops(all_edges, lister)
lister.WriteLine(f"[extract_sandbox] {sandbox_id}: {len(loops)} loop(s) built")
for loop_index, (is_outer, loop_pts3d) in enumerate(loops):
loop_pts3d = _close_polyline(loop_pts3d)
loop_pts2d = project_to_2d(loop_pts3d, frame)