feat(adaptive-isogrid): import_profile.py - push rib profile as NX sketch, sandbox1 brain input test file

This commit is contained in:
2026-02-16 18:45:24 +00:00
parent 23b6fe855b
commit f4cfc9b1b7
2 changed files with 2289 additions and 131 deletions

View File

@@ -1,163 +1,396 @@
"""
NXOpen script — import rib profile JSON and replace sandbox geometry.
NXOpen script — Import rib profile into NX as a sketch.
Input:
rib_profile_<sandbox_id>.json (or rib_profile.json)
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
Responsibilities:
- Recreate closed polylines from profile coordinate arrays
- Build sheet region for sandbox
- Replace sandbox face geometry only
- Sew/unite with neighboring reserved faces
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 argparse
import json
from pathlib import Path
from typing import Any, Dict, Iterable, List, Sequence, Tuple
import math
import os
from typing import Any, Dict, List, Tuple
# ---------------------------------------------------------------------------
# Config
# ---------------------------------------------------------------------------
SKETCH_NAME_PREFIX = "ISOGRID_RIB_" # e.g., ISOGRID_RIB_sandbox_1
Point2D = Tuple[float, float]
Point3D = Tuple[float, float, float]
# ---------------------------------------------------------------------------
# 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
def _add(a: Sequence[float], b: Sequence[float]) -> Point3D:
return (a[0] + b[0], a[1] + b[1], a[2] + b[2])
# ---------------------------------------------------------------------------
# NX sketch creation
# ---------------------------------------------------------------------------
def _mul(v: Sequence[float], s: float) -> Point3D:
return (v[0] * s, v[1] * s, v[2] * s)
def load_json(path: Path) -> Dict[str, Any]:
return json.loads(path.read_text())
def map_2d_to_3d(p: Point2D, transform: Dict[str, List[float]]) -> Point3D:
origin = transform["origin"]
x_axis = transform["x_axis"]
y_axis = transform["y_axis"]
return _add(_add(origin, _mul(x_axis, p[0])), _mul(y_axis, p[1]))
def _ensure_closed(coords: List[Point2D]) -> List[Point2D]:
if not coords:
return coords
if coords[0] != coords[-1]:
coords.append(coords[0])
return coords
def _create_polyline_curve(work_part: Any, pts3d: List[Point3D]) -> Any:
def _find_or_create_sketch(
part: Any,
sketch_name: str,
transform: Dict[str, List[float]],
lister: Any,
) -> Any:
"""
Create a closed polyline curve in NX.
API notes: this can be implemented with StudioSplineBuilderEx, PolygonBuilder,
or line segments + composite curve depending on NX version/license.
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.
"""
# Line-segment fallback (works in all NX versions)
curves = []
for i in range(len(pts3d) - 1):
p1 = work_part.Points.CreatePoint(pts3d[i])
p2 = work_part.Points.CreatePoint(pts3d[i + 1])
curves.append(work_part.Curves.CreateLine(p1, p2))
return curves
import NXOpen
def build_profile_curves(work_part: Any, profile: Dict[str, Any], transform: Dict[str, List[float]]) -> Dict[str, List[Any]]:
created: Dict[str, List[Any]] = {"outer": [], "pockets": [], "holes": []}
outer = _ensure_closed([(float(x), float(y)) for x, y in profile["outer_boundary"]])
outer_3d = [map_2d_to_3d(p, transform) for p in outer]
created["outer"] = _create_polyline_curve(work_part, outer_3d)
for pocket in profile.get("pockets", []):
coords = _ensure_closed([(float(x), float(y)) for x, y in pocket])
pts3d = [map_2d_to_3d(p, transform) for p in coords]
created["pockets"].extend(_create_polyline_curve(work_part, pts3d))
for hole in profile.get("hole_boundaries", []):
coords = _ensure_closed([(float(x), float(y)) for x, y in hole])
pts3d = [map_2d_to_3d(p, transform) for p in coords]
created["holes"].extend(_create_polyline_curve(work_part, pts3d))
return created
def _find_sandbox_face(work_part: Any, sandbox_id: str) -> Any:
for body in getattr(work_part.Bodies, "ToArray", lambda: work_part.Bodies)():
for face in body.GetFaces():
# Try to find existing sketch by name
existing_sketch = None
try:
for feat in part.Features:
fname = ""
try:
tag = face.GetStringUserAttribute("ISOGRID_SANDBOX", -1)
fname = feat.Name
except Exception:
tag = None
if tag == sandbox_id:
return face
raise RuntimeError(f"Sandbox face not found for id={sandbox_id}")
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.Sketch.ViewReorient.DoNotOrientView)
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.Sketch.ViewReorient.DoNotOrientView,
NXOpen.Sketch.UpdateLevel.Model,
)
except Exception as exc:
lister.WriteLine(f"[import] Warning clearing sketch: {exc}")
return existing_sketch
def replace_sandbox_face_geometry(work_part: Any, sandbox_face: Any, created_curves: Dict[str, List[Any]]) -> None:
"""
Replace sandbox surface region from generated profile curves.
# Create new sketch on the sandbox plane
lister.WriteLine(f"[import] Creating new sketch: {sketch_name}")
This operation depends on the model topology and NX license package.
Typical implementation:
1) Build bounded plane/sheet from outer and inner loops
2) Trim/split host face by new boundaries
3) Delete old sandbox patch
4) Sew new patch with reserved neighboring faces
5) Unite if multiple sheet bodies are produced
"""
# Recommended implementation hook points.
# - Through Curve Mesh / Bounded Plane builders in NXOpen.Features
# - SewBuilder in NXOpen.Features
# - DeleteFace + ReplaceFace in synchronous modeling toolkit
raise NotImplementedError(
"Sandbox face replacement is model-specific. Implement with NXOpen feature builders "
"(bounded sheet + replace face + sew/unite) in target NX environment."
origin = transform["origin"]
normal = transform["normal"]
x_axis = transform["x_axis"]
# Create datum plane at the sandbox location
sketch_builder = part.Sketches.CreateNewSketchInPlaceBuilder(NXOpen.Sketch.Null)
# Set the plane
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 datum plane for the sketch
plane = part.Datums.CreateFixedDatumPlane(origin_pt, normal_vec)
sketch_builder.PlaneReference(plane)
# Set sketch origin
sketch_builder.SketchOrigin(origin_pt)
sketch_builder.AxisReference(
part.Datums.CreateFixedDatumAxis(origin_pt, x_vec)
)
# Commit
sketch_feature = sketch_builder.CommitFeature()
sketch_builder.Destroy()
def run_in_nx(
profile_path: Path,
geometry_path: Path,
sandbox_id: str,
) -> None:
import NXOpen # type: ignore
# Get the sketch object
sketch = sketch_feature.GetEntities()[0]
# Rename the feature
try:
sketch_feature.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])
line_builder = part.Sketches.CreateLineBuilder()
line_builder.SetStartPoint(start_pt)
line_builder.SetEndPoint(end_pt)
line = line_builder.Commit()
line_builder.Destroy()
lines_created += 1
except Exception:
# Fallback: try creating a curve and adding 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(line, NXOpen.Sketch.InferConstraintsOption.DoNotInferConstraints)
lines_created += 1
except Exception as exc2:
if lines_created == 0:
lister.WriteLine(f"[import] Line creation 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
if work_part is None:
raise RuntimeError("No active NX work part.")
part_name = work_part.Name if hasattr(work_part, "Name") else ""
lister.WriteLine(f"[import] Work part: {part_name}")
profile = load_json(profile_path)
geometry = load_json(geometry_path)
transform = geometry.get("transform")
if not transform:
raise ValueError(f"Missing transform in {geometry_path}")
# 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
sandbox_face = _find_sandbox_face(work_part, sandbox_id)
created_curves = build_profile_curves(work_part, profile, transform)
replace_sandbox_face_geometry(work_part, sandbox_face, created_curves)
# Find data directory
try:
part_dir = os.path.dirname(work_part.FullPath)
except Exception:
part_dir = os.getcwd()
print(f"[import_profile] Imported profile for {sandbox_id}: {profile_path}")
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:
sketch.Activate(NXOpen.Sketch.ViewReorient.DoNotOrientView)
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.Sketch.ViewReorient.DoNotOrientView,
NXOpen.Sketch.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)
def main(argv: Sequence[str] | None = None) -> int:
parser = argparse.ArgumentParser(description="Import rib profile JSON into NX sandbox face")
parser.add_argument("--profile", required=True, help="Path to rib_profile json")
parser.add_argument("--geometry", required=True, help="Path to geometry_sandbox json")
parser.add_argument("--sandbox-id", required=True, help="Sandbox id (e.g. sandbox_1)")
args = parser.parse_args(argv)
run_in_nx(
profile_path=Path(args.profile),
geometry_path=Path(args.geometry),
sandbox_id=args.sandbox_id,
)
return 0
if __name__ == "__main__":
raise SystemExit(main())
main()

File diff suppressed because it is too large Load Diff