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": []} try:
for feat in part.Features:
outer = _ensure_closed([(float(x), float(y)) for x, y in profile["outer_boundary"]]) fname = ""
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) 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, # Rename the feature
) -> None: try:
import NXOpen # type: ignore 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() 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 work_part = session.Parts.Work
if work_part is None: part_name = work_part.Name if hasattr(work_part, "Name") else ""
raise RuntimeError("No active NX work part.") lister.WriteLine(f"[import] Work part: {part_name}")
profile = load_json(profile_path) # If not in idealized part, find it
geometry = load_json(geometry_path) if not part_name.endswith("_i"):
transform = geometry.get("transform") for part in session.Parts:
if not transform: pname = part.Name if hasattr(part, "Name") else ""
raise ValueError(f"Missing transform in {geometry_path}") 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) # Find data directory
created_curves = build_profile_curves(work_part, profile, transform) try:
replace_sandbox_face_geometry(work_part, sandbox_face, created_curves) 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: main()
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())

File diff suppressed because it is too large Load Diff