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: Reads `rib_profile_<sandbox_id>.json` (output from Python Brain) and creates
rib_profile_<sandbox_id>.json (or rib_profile.json) an NX sketch on the sandbox plane containing:
- Outer boundary polyline
- All pocket cutout polylines
Responsibilities: The sketch is placed in the idealized part. Antoine extrudes manually the first
- Recreate closed polylines from profile coordinate arrays time; subsequent iterations only update the sketch and the extrude regenerates.
- Build sheet region for sandbox
- Replace sandbox face geometry only Usage (NX Journal):
- Sew/unite with neighboring reserved faces 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 from __future__ import annotations
import argparse
import json import json
from pathlib import Path import math
from typing import Any, Dict, Iterable, List, Sequence, Tuple 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 _find_or_create_sketch(
def _mul(v: Sequence[float], s: float) -> Point3D: part: Any,
return (v[0] * s, v[1] * s, v[2] * s) sketch_name: str,
transform: Dict[str, List[float]],
lister: Any,
def load_json(path: Path) -> Dict[str, Any]: ) -> 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:
""" """
Create a closed polyline curve in NX. Find existing sketch by name, or create a new one on the sandbox plane.
API notes: this can be implemented with StudioSplineBuilderEx, PolygonBuilder, If found, delete all existing geometry in it (for update).
or line segments + composite curve depending on NX version/license. Returns the Sketch object.
""" """
# Line-segment fallback (works in all NX versions) import NXOpen
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
# Try to find existing sketch by name
def build_profile_curves(work_part: Any, profile: Dict[str, Any], transform: Dict[str, List[float]]) -> Dict[str, List[Any]]: existing_sketch = None
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: try:
tag = face.GetStringUserAttribute("ISOGRID_SANDBOX", -1) for feat in part.Features:
fname = ""
try:
fname = feat.Name
except Exception: except Exception:
tag = None continue
if tag == sandbox_id: if fname == sketch_name:
return face # Get the sketch from the feature
raise RuntimeError(f"Sandbox face not found for id={sandbox_id}") 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: # Create new sketch on the sandbox plane
""" lister.WriteLine(f"[import] Creating new sketch: {sketch_name}")
Replace sandbox surface region from generated profile curves.
This operation depends on the model topology and NX license package. origin = transform["origin"]
Typical implementation: normal = transform["normal"]
1) Build bounded plane/sheet from outer and inner loops x_axis = transform["x_axis"]
2) Trim/split host face by new boundaries
3) Delete old sandbox patch # Create datum plane at the sandbox location
4) Sew new patch with reserved neighboring faces sketch_builder = part.Sketches.CreateNewSketchInPlaceBuilder(NXOpen.Sketch.Null)
5) Unite if multiple sheet bodies are produced
""" # Set the plane
# Recommended implementation hook points. origin_pt = NXOpen.Point3d(origin[0], origin[1], origin[2])
# - Through Curve Mesh / Bounded Plane builders in NXOpen.Features normal_vec = NXOpen.Vector3d(normal[0], normal[1], normal[2])
# - SewBuilder in NXOpen.Features x_vec = NXOpen.Vector3d(x_axis[0], x_axis[1], x_axis[2])
# - DeleteFace + ReplaceFace in synchronous modeling toolkit
raise NotImplementedError( # Create a datum plane for the sketch
"Sandbox face replacement is model-specific. Implement with NXOpen feature builders " plane = part.Datums.CreateFixedDatumPlane(origin_pt, normal_vec)
"(bounded sheet + replace face + sew/unite) in target NX environment." 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( # Get the sketch object
profile_path: Path, sketch = sketch_feature.GetEntities()[0]
geometry_path: Path,
sandbox_id: str,
) -> None:
import NXOpen # type: ignore
session = NXOpen.Session.GetSession() # Rename the feature
work_part = session.Parts.Work try:
if work_part is None: sketch_feature.Name = sketch_name
raise RuntimeError("No active NX work part.") except Exception:
pass
profile = load_json(profile_path) lister.WriteLine(f"[import] Created sketch: {sketch_name}")
geometry = load_json(geometry_path) return sketch
transform = geometry.get("transform")
if not transform:
raise ValueError(f"Missing transform in {geometry_path}")
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)
print(f"[import_profile] Imported profile for {sandbox_id}: {profile_path}")
def main(argv: Sequence[str] | None = None) -> int: def _draw_polyline_in_sketch(
parser = argparse.ArgumentParser(description="Import rib profile JSON into NX sandbox face") part: Any,
parser.add_argument("--profile", required=True, help="Path to rib_profile json") sketch: Any,
parser.add_argument("--geometry", required=True, help="Path to geometry_sandbox json") points_3d: List[Tuple[float, float, float]],
parser.add_argument("--sandbox-id", required=True, help="Sandbox id (e.g. sandbox_1)") lister: Any,
args = parser.parse_args(argv) close: bool = True,
) -> int:
"""
Draw a closed polyline in the sketch using individual line segments.
Returns number of lines created.
"""
import NXOpen
run_in_nx( if len(points_3d) < 2:
profile_path=Path(args.profile),
geometry_path=Path(args.geometry),
sandbox_id=args.sandbox_id,
)
return 0 return 0
lines_created = 0
n = len(points_3d)
if __name__ == "__main__": # If last point == first point, don't double-close
raise SystemExit(main()) 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
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:
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)
main()

File diff suppressed because it is too large Load Diff