fix: rewrite edge sampling + loop building using verified NXOpen API (GetVertices, GetEdges, GetLength, UF.Eval)
This commit is contained in:
@@ -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)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user