505 lines
18 KiB
Python
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()
|