Files
Atomizer/tools/adaptive-isogrid/src/nx/import_profile.py

505 lines
18 KiB
Python

"""
NXOpen script — Import rib profile into NX as a sketch.
Reads `rib_profile_<sandbox_id>.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()