From 98d510154dbdbe85f1d100e8d2d634f5b0af2226 Mon Sep 17 00:00:00 2001 From: Antoine Date: Mon, 16 Feb 2026 17:46:52 +0000 Subject: [PATCH] fix: rewrite edge sampling + loop building using verified NXOpen API (GetVertices, GetEdges, GetLength, UF.Eval) --- .../src/nx/extract_sandbox.py | 281 ++++++++++++------ 1 file changed, 186 insertions(+), 95 deletions(-) diff --git a/tools/adaptive-isogrid/src/nx/extract_sandbox.py b/tools/adaptive-isogrid/src/nx/extract_sandbox.py index 2891b8d0..83212313 100644 --- a/tools/adaptive-isogrid/src/nx/extract_sandbox.py +++ b/tools/adaptive-isogrid/src/nx/extract_sandbox.py @@ -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)