""" NXOpen script — Import rib profile into NX as a sketch. Reads `rib_profile_.json` (output from Python Brain) and creates an NX sketch on the sandbox plane containing: - Outer boundary polyline - All pocket cutout polylines The sketch is placed in the idealized part. Antoine extrudes manually the first time; subsequent iterations only update the sketch and the extrude regenerates. Usage (NX Journal): File > Execute > NX Journal > import_profile.py Expects rib_profile JSON files in the same `adaptive_isogrid_data/` folder created by extract_sandbox.py (next to the idealized part). Author: Atomizer / Adaptive Isogrid Created: 2026-02-16 """ from __future__ import annotations import json import math import os from typing import Any, Dict, List, Tuple # --------------------------------------------------------------------------- # Config # --------------------------------------------------------------------------- SKETCH_NAME_PREFIX = "ISOGRID_RIB_" # e.g., ISOGRID_RIB_sandbox_1 # --------------------------------------------------------------------------- # Geometry helpers # --------------------------------------------------------------------------- def unproject_to_3d( points2d: List[List[float]], transform: Dict[str, List[float]], ) -> List[Tuple[float, float, float]]: """Convert 2D profile points back to 3D using the extraction transform.""" ox, oy, oz = transform["origin"] xx, xy, xz = transform["x_axis"] yx, yy, yz = transform["y_axis"] pts3d = [] for x, y in points2d: px = ox + x * xx + y * yx py = oy + x * xy + y * yy pz = oz + x * xz + y * yz pts3d.append((px, py, pz)) return pts3d # --------------------------------------------------------------------------- # NX sketch creation # --------------------------------------------------------------------------- def _find_or_create_sketch( part: Any, sketch_name: str, transform: Dict[str, List[float]], lister: Any, ) -> Any: """ Find existing sketch by name, or create a new one on the sandbox plane. If found, delete all existing geometry in it (for update). Returns the Sketch object. """ import NXOpen # Try to find existing sketch by name existing_sketch = None try: for feat in part.Features: fname = "" try: fname = feat.Name except Exception: continue if fname == sketch_name: # Get the sketch from the feature try: existing_sketch = feat.GetEntities()[0] lister.WriteLine(f"[import] Found existing sketch: {sketch_name}") except Exception: pass break except Exception: pass if existing_sketch is not None: # Clear existing geometry for update try: existing_sketch.Activate(NXOpen.ViewReorient.FalseValue) all_geom = existing_sketch.GetAllGeometry() if all_geom: existing_sketch.DeleteObjects(list(all_geom)) lister.WriteLine(f"[import] Cleared {len(all_geom)} objects from existing sketch") existing_sketch.Deactivate( NXOpen.ViewReorient.FalseValue, NXOpen.UpdateLevel.Model, ) except Exception as exc: lister.WriteLine(f"[import] Warning clearing sketch: {exc}") return existing_sketch # Create new sketch on the sandbox plane lister.WriteLine(f"[import] Creating new sketch: {sketch_name}") origin = transform["origin"] normal = transform["normal"] x_axis = transform["x_axis"] origin_pt = NXOpen.Point3d(origin[0], origin[1], origin[2]) normal_vec = NXOpen.Vector3d(normal[0], normal[1], normal[2]) x_vec = NXOpen.Vector3d(x_axis[0], x_axis[1], x_axis[2]) # Create a Plane (SmartObject) — NOT a DatumPlane (DisplayableObject) # PlaneCollection.CreatePlane(method, alternate, origin, normal, expr, flip, percent, geometry) plane = part.Planes.CreatePlane( NXOpen.PlaneTypes.MethodType.Fixed, # Fixed plane NXOpen.PlaneTypes.AlternateType.One, origin_pt, normal_vec, "", # expression False, # flip False, # percent [], # geometry refs ) lister.WriteLine(f"[import] Created plane: {plane} (type={type(plane).__name__})") if plane is None: # Fallback: try Distance method plane = part.Planes.CreatePlane( NXOpen.PlaneTypes.MethodType.Distance, NXOpen.PlaneTypes.AlternateType.One, origin_pt, normal_vec, "", False, False, [], ) lister.WriteLine(f"[import] Fallback plane: {plane}") if plane is None: # Fallback 2: CreateFixedTypePlane y_axis_t = transform["y_axis"] mtx = NXOpen.Matrix3x3() mtx.Xx = x_axis[0]; mtx.Xy = x_axis[1]; mtx.Xz = x_axis[2] mtx.Yx = y_axis_t[0]; mtx.Yy = y_axis_t[1]; mtx.Yz = y_axis_t[2] mtx.Zx = normal[0]; mtx.Zy = normal[1]; mtx.Zz = normal[2] plane = part.Planes.CreateFixedTypePlane( origin_pt, mtx, NXOpen.SmartObject.UpdateOption.WithinModeling, ) lister.WriteLine(f"[import] Fallback2 plane: {plane}") # Create sketch-in-place builder sketch_builder = part.Sketches.CreateSketchInPlaceBuilder2(NXOpen.Sketch.Null) # NXOpen Python naming collision: property getter and setter method share # the same name. builder.PlaneReference returns None (getter), so calling # builder.PlaneReference(plane) = None(plane) = TypeError. # Solution: use setattr() to bypass the getter and hit the setter. origin_point = part.Points.CreatePoint(origin_pt) axis_dir = part.Directions.CreateDirection( origin_pt, x_vec, NXOpen.SmartObject.UpdateOption.WithinModeling, ) # Try multiple setter patterns for attr, val, label in [ ("PlaneReference", plane, "plane"), ("SketchOrigin", origin_point, "origin"), ("AxisReference", axis_dir, "axis"), ]: # Pattern 1: setattr (property assignment) try: setattr(sketch_builder, attr, val) lister.WriteLine(f"[import] Set {label} via setattr") continue except Exception as e1: pass # Pattern 2: find the setter method with Set prefix setter_name = "Set" + attr try: setter = getattr(sketch_builder, setter_name, None) if setter: setter(val) lister.WriteLine(f"[import] Set {label} via {setter_name}()") continue except Exception: pass # Pattern 3: direct call (in case NX version supports it) try: getattr(sketch_builder, attr)(val) lister.WriteLine(f"[import] Set {label} via method call") continue except Exception: pass lister.WriteLine(f"[import] WARNING: Could not set {label} ({attr})") # Commit — Builder.Commit() returns NXObject sketch_obj = sketch_builder.Commit() lister.WriteLine(f"[import] Commit returned: {sketch_obj} (type={type(sketch_obj).__name__})") # Also get committed objects for inspection committed = sketch_builder.GetCommittedObjects() lister.WriteLine(f"[import] Committed objects: {len(committed)}") for i, obj in enumerate(committed): lister.WriteLine(f"[import] [{i}] {type(obj).__name__}: {obj}") sketch_builder.Destroy() # Find the Sketch object from committed objects or return value sketch = None if isinstance(sketch_obj, NXOpen.Sketch): sketch = sketch_obj else: # Search committed objects for obj in committed: if isinstance(obj, NXOpen.Sketch): sketch = obj break # Try GetObject if sketch is None: try: sketch = sketch_builder.GetObject() except Exception: pass # Last resort: find by name in part sketches if sketch is None: for s in part.Sketches: sketch = s # take the last one (just created) lister.WriteLine(f"[import] Found sketch by iteration: {sketch}") if sketch is None: raise RuntimeError("Could not get Sketch object after commit") # Rename try: sketch.Name = sketch_name except Exception: pass lister.WriteLine(f"[import] Created sketch: {sketch_name}") return sketch def _draw_polyline_in_sketch( part: Any, sketch: Any, points_3d: List[Tuple[float, float, float]], lister: Any, close: bool = True, ) -> int: """ Draw a closed polyline in the sketch using individual line segments. Returns number of lines created. """ import NXOpen if len(points_3d) < 2: return 0 lines_created = 0 n = len(points_3d) # If last point == first point, don't double-close if close and len(points_3d) >= 3: d = math.sqrt(sum((a - b) ** 2 for a, b in zip(points_3d[0], points_3d[-1]))) if d < 0.001: n = len(points_3d) - 1 # skip duplicate closing point segments = n if close else (n - 1) for i in range(segments): p1 = points_3d[i] p2 = points_3d[(i + 1) % n] try: start_pt = NXOpen.Point3d(p1[0], p1[1], p1[2]) end_pt = NXOpen.Point3d(p2[0], p2[1], p2[2]) # SketchCollection.CreateLineBuilder() -> SketchLineBuilder # SketchLineBuilder.SetStartPoint(Point3d), .SetEndPoint(Point3d) # Builder.Commit() -> NXObject, Builder.Destroy() line_builder = part.Sketches.CreateLineBuilder() line_builder.SetStartPoint(start_pt) line_builder.SetEndPoint(end_pt) line_builder.Commit() line_builder.Destroy() lines_created += 1 except Exception as exc: if lines_created == 0: lister.WriteLine(f"[import] Line creation failed: {exc}") # Try fallback: create curve + add to sketch try: start_obj = part.Points.CreatePoint(NXOpen.Point3d(p1[0], p1[1], p1[2])) end_obj = part.Points.CreatePoint(NXOpen.Point3d(p2[0], p2[1], p2[2])) line = part.Curves.CreateLine(start_obj, end_obj) sketch.AddGeometry( NXOpen.Sketch.InferConstraintsOption.DoNotInferConstraints, NXOpen.Sketch.AddEllipseOption.None_, [line], ) lines_created += 1 except Exception as exc2: lister.WriteLine(f"[import] Fallback line also failed: {exc2}") return lines_created def _draw_circle_in_sketch( part: Any, sketch: Any, center_3d: Tuple[float, float, float], radius: float, normal: List[float], lister: Any, ) -> bool: """Draw a circle in the sketch.""" import NXOpen try: circle_builder = part.Sketches.CreateCircleBuilder() center_pt = NXOpen.Point3d(center_3d[0], center_3d[1], center_3d[2]) circle_builder.SetCenterPoint(center_pt) # Size point = center + radius along x size_pt = NXOpen.Point3d( center_3d[0] + radius, center_3d[1], center_3d[2], ) circle_builder.SetSizePoint(size_pt) circle_builder.Commit() circle_builder.Destroy() return True except Exception as exc: lister.WriteLine(f"[import] Circle creation failed: {exc}") return False # --------------------------------------------------------------------------- # Main # --------------------------------------------------------------------------- def main(): import NXOpen session = NXOpen.Session.GetSession() lister = session.ListingWindow lister.Open() lister.WriteLine("=" * 60) lister.WriteLine(" Adaptive Isogrid — Rib Profile Import") lister.WriteLine("=" * 60) # Navigate to idealized part work_part = session.Parts.Work part_name = work_part.Name if hasattr(work_part, "Name") else "" lister.WriteLine(f"[import] Work part: {part_name}") # If not in idealized part, find it if not part_name.endswith("_i"): for part in session.Parts: pname = part.Name if hasattr(part, "Name") else "" if pname.endswith("_i"): session.Parts.SetWork(part) work_part = part lister.WriteLine(f"[import] Switched to idealized part: {pname}") break # Find data directory try: part_dir = os.path.dirname(work_part.FullPath) except Exception: part_dir = os.getcwd() data_dir = os.path.join(part_dir, "adaptive_isogrid_data") if not os.path.isdir(data_dir): lister.WriteLine(f"[import] ERROR: Data directory not found: {data_dir}") return # Find all rib profile + geometry JSON pairs profile_files = sorted([ f for f in os.listdir(data_dir) if f.startswith("rib_profile_") and f.endswith(".json") ]) if not profile_files: # Also check for sandbox1_rib_profile.json pattern profile_files = sorted([ f for f in os.listdir(data_dir) if "rib_profile" in f and f.endswith(".json") ]) if not profile_files: lister.WriteLine(f"[import] ERROR: No rib_profile*.json found in {data_dir}") lister.WriteLine(f"[import] Files present: {os.listdir(data_dir)}") return lister.WriteLine(f"[import] Found {len(profile_files)} profile(s) to import") for profile_file in profile_files: profile_path = os.path.join(data_dir, profile_file) # Determine sandbox_id from filename # Expected: rib_profile_sandbox_1.json or sandbox1_rib_profile.json sandbox_id = profile_file.replace("rib_profile_", "").replace(".json", "") if not sandbox_id.startswith("sandbox"): sandbox_id = "sandbox_1" # fallback # Load corresponding geometry JSON for the transform geom_path = os.path.join(data_dir, f"geometry_{sandbox_id}.json") if not os.path.exists(geom_path): # Try alternate names candidates = [f for f in os.listdir(data_dir) if f.startswith("geometry_") and f.endswith(".json")] if candidates: geom_path = os.path.join(data_dir, candidates[0]) else: lister.WriteLine(f"[import] ERROR: No geometry JSON found for transform data") continue lister.WriteLine(f"\n--- Importing {profile_file} ---") lister.WriteLine(f"[import] Geometry (transform): {geom_path}") try: with open(profile_path, "r") as f: profile = json.load(f) with open(geom_path, "r") as f: geometry = json.load(f) except Exception as exc: lister.WriteLine(f"[import] ERROR reading JSON: {exc}") continue transform = geometry["transform"] sketch_name = SKETCH_NAME_PREFIX + sandbox_id # Find or create sketch try: sketch = _find_or_create_sketch(work_part, sketch_name, transform, lister) except Exception as exc: lister.WriteLine(f"[import] ERROR creating sketch: {exc}") import traceback lister.WriteLine(traceback.format_exc()) continue # Activate sketch for drawing try: # NXOpen.ViewReorient: FalseValue = don't reorient, TrueValue = reorient sketch.Activate(NXOpen.ViewReorient.FalseValue) except Exception as exc: lister.WriteLine(f"[import] ERROR activating sketch: {exc}") continue total_lines = 0 # Draw outer boundary outer_2d = profile.get("outer_boundary", []) if outer_2d: outer_3d = unproject_to_3d(outer_2d, transform) n = _draw_polyline_in_sketch(work_part, sketch, outer_3d, lister, close=True) total_lines += n lister.WriteLine(f"[import] Outer boundary: {n} lines") # Draw pocket cutouts pockets = profile.get("pockets", []) lister.WriteLine(f"[import] Drawing {len(pockets)} pockets...") for pi, pocket_pts in enumerate(pockets): if len(pocket_pts) < 3: continue pocket_3d = unproject_to_3d(pocket_pts, transform) n = _draw_polyline_in_sketch(work_part, sketch, pocket_3d, lister, close=True) total_lines += n # Progress every 50 pockets if (pi + 1) % 50 == 0: lister.WriteLine(f"[import] ... {pi + 1}/{len(pockets)} pockets drawn") # Deactivate sketch try: sketch.Deactivate( NXOpen.ViewReorient.FalseValue, NXOpen.UpdateLevel.Model, ) except Exception as exc: lister.WriteLine(f"[import] Warning deactivating: {exc}") lister.WriteLine(f"[import] Done: {total_lines} total line segments in sketch '{sketch_name}'") lister.WriteLine(f"[import] Outer boundary: {len(outer_2d)} pts") lister.WriteLine(f"[import] Pockets: {len(pockets)}") lister.WriteLine("\n" + "=" * 60) lister.WriteLine(" Import complete — extrude the sketch to rib thickness") lister.WriteLine("=" * 60) main()