feat(adaptive-isogrid): import_profile.py - push rib profile as NX sketch, sandbox1 brain input test file
This commit is contained in:
@@ -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()
|
||||
|
||||
1925
tools/adaptive-isogrid/tests/sandbox1_brain_input.json
Normal file
1925
tools/adaptive-isogrid/tests/sandbox1_brain_input.json
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user