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]:
|
def _sample_edge_polyline(edge: Any, chord_tol_mm: float) -> List[Point3D]:
|
||||||
"""
|
"""
|
||||||
Sample an NX edge as a polyline with adaptive point density.
|
Sample an NX edge as a polyline.
|
||||||
Falls back to vertex extraction if evaluator is unavailable.
|
|
||||||
|
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:
|
||||||
|
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:
|
try:
|
||||||
evaluator = edge.CreateEvaluator()
|
|
||||||
t0, t1 = evaluator.GetLimits()
|
|
||||||
length = edge.GetLength()
|
length = edge.GetLength()
|
||||||
n = max(2, int(length / max(chord_tol_mm, 1e-3)))
|
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] = []
|
pts: List[Point3D] = []
|
||||||
for i in range(n + 1):
|
for i in range(n_pts + 1):
|
||||||
t = t0 + (t1 - t0) * (i / n)
|
t = t0 + (t1 - t0) * (i / n_pts)
|
||||||
p, _ = evaluator.Evaluate(t)
|
result = uf.Eval.Evaluate(evaluator, 0, t)
|
||||||
pts.append((float(p.X), float(p.Y), float(p.Z)))
|
# 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
|
return pts
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# Fallback: edge vertices only
|
# Last fallback: just vertices (straight-line approximation of curve)
|
||||||
try:
|
return [p1, p2]
|
||||||
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}")
|
|
||||||
|
|
||||||
|
|
||||||
def _close_polyline(points: List[Point3D]) -> List[Point3D]:
|
def _close_polyline(points: List[Point3D]) -> List[Point3D]:
|
||||||
@@ -136,93 +182,141 @@ def _close_polyline(points: List[Point3D]) -> List[Point3D]:
|
|||||||
# Face local frame
|
# 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.
|
Chain edges into closed loops by matching vertex endpoints.
|
||||||
Returns list of (is_outer, [edges]) tuples.
|
|
||||||
|
|
||||||
Tries multiple NX API patterns:
|
Returns list of (is_outer, points_3d) tuples.
|
||||||
1. face.GetEdgeLoops() (NX 2306+)
|
The largest loop (by area/perimeter) is assumed to be the outer loop.
|
||||||
2. UF layer: UF.Modeling.ask_face_loops()
|
|
||||||
3. Fallback: all edges as single outer loop
|
|
||||||
"""
|
"""
|
||||||
def _log(msg):
|
def _log(msg):
|
||||||
if lister:
|
if lister:
|
||||||
lister.WriteLine(msg)
|
lister.WriteLine(msg)
|
||||||
|
|
||||||
# Method 1: GetEdgeLoops (modern NX)
|
if not edges:
|
||||||
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
|
|
||||||
|
|
||||||
# Method 2: UF layer
|
|
||||||
try:
|
|
||||||
import NXOpen
|
|
||||||
session = NXOpen.Session.GetSession()
|
|
||||||
uf = session.GetUFSession()
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
# 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
|
|
||||||
|
|
||||||
return []
|
return []
|
||||||
|
|
||||||
|
# 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
|
||||||
|
|
||||||
|
_log(f"[chain] {len(segments)} edge segments to chain")
|
||||||
|
|
||||||
|
# Chain into loops
|
||||||
|
used = [False] * len(segments)
|
||||||
|
loops_points: List[List[Point3D]] = []
|
||||||
|
loops_edges: List[List[Any]] = []
|
||||||
|
|
||||||
|
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:
|
def _face_local_frame(face: Any, lister: Any = None) -> LocalFrame:
|
||||||
"""
|
"""
|
||||||
Build a stable local frame on a planar face.
|
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()
|
edges = face.GetEdges()
|
||||||
first_edge = edges[0]
|
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
|
# Get face normal
|
||||||
normal = (0.0, 0.0, 1.0)
|
normal = (0.0, 0.0, 1.0)
|
||||||
@@ -472,17 +566,14 @@ def extract_sandbox_geometry(
|
|||||||
outer_2d: List[List[float]] = []
|
outer_2d: List[List[float]] = []
|
||||||
inner_boundaries: List[Dict[str, Any]] = []
|
inner_boundaries: List[Dict[str, Any]] = []
|
||||||
|
|
||||||
loops = _get_face_loops(face, lister)
|
# Get all edges on the face and chain them into loops
|
||||||
lister.WriteLine(f"[extract_sandbox] {sandbox_id}: {len(loops)} loop(s)")
|
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):
|
loops = _chain_edges_into_loops(all_edges, lister)
|
||||||
loop_pts3d: List[Point3D] = []
|
lister.WriteLine(f"[extract_sandbox] {sandbox_id}: {len(loops)} loop(s) built")
|
||||||
for edge in edges:
|
|
||||||
pts = _sample_edge_polyline(edge, chord_tol_mm)
|
|
||||||
if loop_pts3d and pts:
|
|
||||||
pts = pts[1:]
|
|
||||||
loop_pts3d.extend(pts)
|
|
||||||
|
|
||||||
|
for loop_index, (is_outer, loop_pts3d) in enumerate(loops):
|
||||||
loop_pts3d = _close_polyline(loop_pts3d)
|
loop_pts3d = _close_polyline(loop_pts3d)
|
||||||
loop_pts2d = project_to_2d(loop_pts3d, frame)
|
loop_pts2d = project_to_2d(loop_pts3d, frame)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user