merge: take remote extract_sandbox.py v2
This commit is contained in:
@@ -1,20 +1,20 @@
|
||||
"""
|
||||
NXOpen script — Extract sandbox sheet geometry for Adaptive Isogrid.
|
||||
NXOpen script — Extract sandbox face geometry for Adaptive Isogrid.
|
||||
|
||||
Runs from the .sim file context. Navigates:
|
||||
SIM → FEM → Idealized Part → find sheet bodies with ISOGRID_SANDBOX attribute
|
||||
SIM → FEM → Idealized Part → find bodies with ISOGRID_SANDBOX attribute
|
||||
|
||||
For each sandbox sheet body, exports `geometry_<sandbox_id>.json` containing:
|
||||
For each sandbox body, exports `geometry_<sandbox_id>.json` containing:
|
||||
- outer_boundary: 2D polyline of the sandbox outline
|
||||
- inner_boundaries: 2D polylines of cutouts (reserved cylinder intersections, etc.)
|
||||
- transform: 3D ↔ 2D mapping for reimporting geometry
|
||||
- transform: 3D <-> 2D mapping for reimporting geometry
|
||||
- thickness: from NX midsurface (if available)
|
||||
|
||||
Inner loops are treated as boundary constraints (edges), NOT as holes to rib around,
|
||||
because hole reservations are handled by separate solid cylinders in the fixed geometry.
|
||||
|
||||
Usage (NX Journal):
|
||||
run_journal.exe extract_sandbox.py [--output-dir <path>] [--chord-tol <mm>]
|
||||
Usage (NX Journal — just run it, no args needed):
|
||||
File > Execute > NX Journal > extract_sandbox.py
|
||||
|
||||
Author: Atomizer / Adaptive Isogrid
|
||||
Created: 2026-02-16
|
||||
@@ -22,9 +22,10 @@ Created: 2026-02-16
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import math
|
||||
import os
|
||||
import sys
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, Sequence, Tuple
|
||||
@@ -137,10 +138,7 @@ def _close_polyline(points: List[Point3D]) -> List[Point3D]:
|
||||
|
||||
def _face_local_frame(face: Any) -> LocalFrame:
|
||||
"""
|
||||
Build a stable local frame on a planar face:
|
||||
- origin: first loop first point
|
||||
- normal: face normal at origin
|
||||
- x/y: orthonormal tangent basis
|
||||
Build a stable local frame on a planar face.
|
||||
"""
|
||||
loops = face.GetLoops()
|
||||
first_edge = loops[0].GetEdges()[0]
|
||||
@@ -149,7 +147,7 @@ def _face_local_frame(face: Any) -> LocalFrame:
|
||||
# Get face normal
|
||||
normal = (0.0, 0.0, 1.0)
|
||||
try:
|
||||
import NXOpen # noqa
|
||||
import NXOpen
|
||||
pt = NXOpen.Point3d(sample[0], sample[1], sample[2])
|
||||
n = face.GetFaceNormal(pt)
|
||||
normal = _normalize((float(n.X), float(n.Y), float(n.Z)))
|
||||
@@ -185,95 +183,63 @@ def _get_string_attribute(obj: Any, title: str) -> str | None:
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# SIM → FEM → Idealized Part navigation
|
||||
# SIM -> Idealized Part navigation
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _navigate_sim_to_idealized(session: Any) -> Any:
|
||||
"""
|
||||
From the active .sim work part, navigate to the idealized part.
|
||||
|
||||
Path: SIM → FEM component → idealized part (_i.prt)
|
||||
|
||||
Returns the idealized part (NXOpen.BasePart), and sets it as work part.
|
||||
From the active .sim work part, navigate to the idealized part (_i.prt).
|
||||
Sets idealized part as work part and returns it.
|
||||
"""
|
||||
import NXOpen.CAE # noqa
|
||||
|
||||
work_part = session.Parts.Work
|
||||
part_name = work_part.Name if hasattr(work_part, "Name") else ""
|
||||
part_path = work_part.FullPath if hasattr(work_part, "FullPath") else ""
|
||||
|
||||
print(f"[extract_sandbox] Starting from: {part_name}")
|
||||
print(f"[extract_sandbox] Full path: {part_path}")
|
||||
lister = session.ListingWindow
|
||||
lister.Open()
|
||||
lister.WriteLine(f"[extract_sandbox] Starting from: {part_name}")
|
||||
|
||||
# Check if we're already in an idealized part
|
||||
if "_i" in part_name.lower() or part_name.endswith("_i"):
|
||||
print("[extract_sandbox] Already in idealized part.")
|
||||
# Check if already in idealized part
|
||||
if part_name.endswith("_i"):
|
||||
lister.WriteLine("[extract_sandbox] Already in idealized part.")
|
||||
return work_part
|
||||
|
||||
# If in .sim, navigate to FEM then to idealized part
|
||||
# Strategy: iterate through loaded parts to find the _i.prt
|
||||
# Search loaded parts for the idealized part
|
||||
idealized_part = None
|
||||
fem_part = None
|
||||
|
||||
for part in session.Parts:
|
||||
pname = part.Name if hasattr(part, "Name") else ""
|
||||
ppath = ""
|
||||
try:
|
||||
ppath = part.FullPath
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if "_i" in pname and pname.endswith("_i"):
|
||||
# Found idealized part candidate
|
||||
if pname.endswith("_i"):
|
||||
idealized_part = part
|
||||
print(f"[extract_sandbox] Found idealized part: {pname}")
|
||||
elif "_fem" in pname.lower() and not pname.endswith("_i"):
|
||||
fem_part = part
|
||||
print(f"[extract_sandbox] Found FEM part: {pname}")
|
||||
|
||||
if idealized_part is None:
|
||||
# Try alternative: look for parts whose name contains "fem" + "_i"
|
||||
for part in session.Parts:
|
||||
pname = part.Name if hasattr(part, "Name") else ""
|
||||
if "fem" in pname.lower() and "_i" in pname:
|
||||
idealized_part = part
|
||||
print(f"[extract_sandbox] Found idealized part (alt): {pname}")
|
||||
break
|
||||
lister.WriteLine(f"[extract_sandbox] Found idealized part: {pname}")
|
||||
break
|
||||
|
||||
if idealized_part is None:
|
||||
raise RuntimeError(
|
||||
"Could not find idealized part (_i.prt). "
|
||||
"Ensure the SIM file is open and its FEM + idealized part are loaded."
|
||||
"Could not find idealized part (*_i.prt). "
|
||||
"Ensure the SIM is open with FEM + idealized part loaded."
|
||||
)
|
||||
|
||||
# Set idealized part as work part
|
||||
# Set as work part
|
||||
try:
|
||||
session.Parts.SetWork(idealized_part)
|
||||
print(f"[extract_sandbox] Set work part to: {idealized_part.Name}")
|
||||
lister.WriteLine(f"[extract_sandbox] Set work part to: {idealized_part.Name}")
|
||||
except Exception as exc:
|
||||
print(f"[extract_sandbox] Warning: Could not set work part: {exc}")
|
||||
print("[extract_sandbox] Proceeding with part reference anyway...")
|
||||
lister.WriteLine(f"[extract_sandbox] Warning: SetWork failed: {exc}")
|
||||
|
||||
return idealized_part
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Sandbox discovery — find sheet bodies with ISOGRID_SANDBOX attribute
|
||||
# Sandbox discovery
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def find_sandbox_bodies(
|
||||
part: Any,
|
||||
lister: Any,
|
||||
attr_name: str = "ISOGRID_SANDBOX",
|
||||
) -> List[Tuple[str, Any, Any]]:
|
||||
"""
|
||||
Find sheet bodies tagged with ISOGRID_SANDBOX attribute.
|
||||
|
||||
Searches:
|
||||
1. Body-level attributes
|
||||
2. Face-level attributes (fallback)
|
||||
|
||||
Find bodies tagged with ISOGRID_SANDBOX attribute.
|
||||
Returns list of (sandbox_id, body, face) tuples.
|
||||
The face is the primary face of the sheet body.
|
||||
"""
|
||||
tagged: List[Tuple[str, Any, Any]] = []
|
||||
|
||||
@@ -283,29 +249,34 @@ def find_sandbox_bodies(
|
||||
except Exception:
|
||||
bodies = list(part.Bodies)
|
||||
|
||||
lister.WriteLine(f"[extract_sandbox] Scanning {len(bodies)} bodies...")
|
||||
|
||||
for body in bodies:
|
||||
# Check body-level attribute first
|
||||
body_name = ""
|
||||
try:
|
||||
body_name = body.Name if hasattr(body, "Name") else str(body)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Check body-level attribute
|
||||
sandbox_id = _get_string_attribute(body, attr_name)
|
||||
if sandbox_id:
|
||||
# Sheet body — get its face(s)
|
||||
faces = body.GetFaces()
|
||||
if faces:
|
||||
face = faces[0] # Sheet body typically has one face
|
||||
tagged.append((sandbox_id, body, face))
|
||||
print(f"[extract_sandbox] Found sandbox body: {sandbox_id} "
|
||||
f"(body attr, {len(faces)} face(s))")
|
||||
tagged.append((sandbox_id, body, faces[0]))
|
||||
lister.WriteLine(f"[extract_sandbox] Found: {sandbox_id} (body attr on '{body_name}', {len(faces)} face(s))")
|
||||
continue
|
||||
|
||||
# Fallback: check face-level attributes
|
||||
# Check face-level attribute
|
||||
for face in body.GetFaces():
|
||||
sandbox_id = _get_string_attribute(face, attr_name)
|
||||
if sandbox_id:
|
||||
tagged.append((sandbox_id, body, face))
|
||||
print(f"[extract_sandbox] Found sandbox face: {sandbox_id} (face attr)")
|
||||
lister.WriteLine(f"[extract_sandbox] Found: {sandbox_id} (face attr on '{body_name}')")
|
||||
|
||||
# Fallback: body name matching
|
||||
if not tagged:
|
||||
# Last resort: try matching body names
|
||||
print(f"[extract_sandbox] No attributes found, trying body name matching...")
|
||||
lister.WriteLine("[extract_sandbox] No attributes found, trying name matching...")
|
||||
for body in bodies:
|
||||
bname = ""
|
||||
try:
|
||||
@@ -315,9 +286,9 @@ def find_sandbox_bodies(
|
||||
if "sandbox" in bname.lower():
|
||||
faces = body.GetFaces()
|
||||
if faces:
|
||||
sandbox_id = bname.lower().replace(" ", "_")
|
||||
tagged.append((sandbox_id, body, faces[0]))
|
||||
print(f"[extract_sandbox] Found sandbox by name: {sandbox_id}")
|
||||
sid = bname.lower().replace(" ", "_")
|
||||
tagged.append((sid, body, faces[0]))
|
||||
lister.WriteLine(f"[extract_sandbox] Found by name: {sid}")
|
||||
|
||||
return tagged
|
||||
|
||||
@@ -330,15 +301,12 @@ def extract_sandbox_geometry(
|
||||
face: Any,
|
||||
body: Any,
|
||||
sandbox_id: str,
|
||||
lister: Any,
|
||||
chord_tol_mm: float = 0.1,
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Extract a sandbox sheet face into a JSON-serializable dict.
|
||||
|
||||
- outer_boundary: the sandbox outline (2D polyline)
|
||||
- inner_boundaries: cutouts from reserved cylinders etc. (2D polylines)
|
||||
These are boundary constraints, NOT optimization holes.
|
||||
- transform: 3D ↔ 2D coordinate mapping
|
||||
Extract a sandbox face into a JSON-serializable dict.
|
||||
Inner loops are boundary constraints (reserved geometry edges), not holes.
|
||||
"""
|
||||
frame = _face_local_frame(face)
|
||||
|
||||
@@ -346,16 +314,15 @@ def extract_sandbox_geometry(
|
||||
inner_boundaries: List[Dict[str, Any]] = []
|
||||
|
||||
loops = face.GetLoops()
|
||||
print(f"[extract_sandbox] {sandbox_id}: {len(loops)} loop(s) on face")
|
||||
lister.WriteLine(f"[extract_sandbox] {sandbox_id}: {len(loops)} loop(s)")
|
||||
|
||||
for loop_index, loop in enumerate(loops):
|
||||
# Collect all edge points for this loop
|
||||
loop_pts3d: List[Point3D] = []
|
||||
edges = loop.GetEdges()
|
||||
for edge in edges:
|
||||
pts = _sample_edge_polyline(edge, chord_tol_mm)
|
||||
if loop_pts3d and pts:
|
||||
pts = pts[1:] # avoid duplicate at junction
|
||||
pts = pts[1:]
|
||||
loop_pts3d.extend(pts)
|
||||
|
||||
loop_pts3d = _close_polyline(loop_pts3d)
|
||||
@@ -370,7 +337,7 @@ def extract_sandbox_geometry(
|
||||
|
||||
if is_outer:
|
||||
outer_2d = [[round(x, 6), round(y, 6)] for x, y in loop_pts2d]
|
||||
print(f"[extract_sandbox] outer loop: {len(outer_2d)} points")
|
||||
lister.WriteLine(f"[extract_sandbox] outer loop: {len(outer_2d)} pts")
|
||||
else:
|
||||
boundary = [[round(x, 6), round(y, 6)] for x, y in loop_pts2d]
|
||||
inner_boundaries.append({
|
||||
@@ -378,18 +345,16 @@ def extract_sandbox_geometry(
|
||||
"boundary": boundary,
|
||||
"num_points": len(boundary),
|
||||
})
|
||||
print(f"[extract_sandbox] inner loop {len(inner_boundaries)}: "
|
||||
f"{len(boundary)} points")
|
||||
lister.WriteLine(f"[extract_sandbox] inner loop {len(inner_boundaries)}: {len(boundary)} pts")
|
||||
|
||||
# Try to get thickness from body or expression
|
||||
# Try thickness
|
||||
thickness = None
|
||||
try:
|
||||
# NX sheet bodies may expose thickness
|
||||
thickness = float(body.GetThickness())
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
geom: Dict[str, Any] = {
|
||||
return {
|
||||
"schema_version": "1.0",
|
||||
"units": "mm",
|
||||
"sandbox_id": sandbox_id,
|
||||
@@ -405,85 +370,69 @@ def extract_sandbox_geometry(
|
||||
},
|
||||
}
|
||||
|
||||
return geom
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Export
|
||||
# Main — NX Journal entry point
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def export_sandbox_geometries(
|
||||
output_dir: Path,
|
||||
geometries: Dict[str, Dict[str, Any]],
|
||||
) -> List[Path]:
|
||||
output_dir.mkdir(parents=True, exist_ok=True)
|
||||
written: List[Path] = []
|
||||
for sandbox_id, payload in geometries.items():
|
||||
out = output_dir / f"geometry_{sandbox_id}.json"
|
||||
out.write_text(json.dumps(payload, indent=2))
|
||||
written.append(out)
|
||||
print(f"[extract_sandbox] Wrote: {out}")
|
||||
return written
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Main entry point — runs inside NX
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def run_in_nx(output_dir: Path, chord_tol_mm: float = 0.1) -> List[Path]:
|
||||
import NXOpen # type: ignore
|
||||
def main():
|
||||
import NXOpen
|
||||
|
||||
session = NXOpen.Session.GetSession()
|
||||
lister = session.ListingWindow
|
||||
lister.Open()
|
||||
|
||||
# Navigate from .sim to idealized part
|
||||
lister.WriteLine("=" * 60)
|
||||
lister.WriteLine(" Adaptive Isogrid — Sandbox Geometry Extraction")
|
||||
lister.WriteLine("=" * 60)
|
||||
|
||||
# Navigate to idealized part
|
||||
idealized_part = _navigate_sim_to_idealized(session)
|
||||
|
||||
# Find sandbox bodies
|
||||
sandbox_entries = find_sandbox_bodies(idealized_part)
|
||||
# Find sandboxes
|
||||
sandbox_entries = find_sandbox_bodies(idealized_part, lister)
|
||||
if not sandbox_entries:
|
||||
raise RuntimeError(
|
||||
"No sandbox bodies found. Ensure sheet bodies have "
|
||||
"the ISOGRID_SANDBOX attribute set (body or face level)."
|
||||
)
|
||||
lister.WriteLine("[extract_sandbox] ERROR: No sandbox bodies found!")
|
||||
lister.WriteLine("Ensure bodies have ISOGRID_SANDBOX attribute set.")
|
||||
return
|
||||
|
||||
print(f"[extract_sandbox] Found {len(sandbox_entries)} sandbox(es)")
|
||||
lister.WriteLine(f"[extract_sandbox] Found {len(sandbox_entries)} sandbox(es)")
|
||||
|
||||
# Extract geometry for each sandbox
|
||||
payloads: Dict[str, Dict[str, Any]] = {}
|
||||
# Output directory: next to the .sim file (or idealized part)
|
||||
try:
|
||||
part_dir = os.path.dirname(idealized_part.FullPath)
|
||||
except Exception:
|
||||
part_dir = os.getcwd()
|
||||
|
||||
output_dir = os.path.join(part_dir, "adaptive_isogrid_data")
|
||||
os.makedirs(output_dir, exist_ok=True)
|
||||
lister.WriteLine(f"[extract_sandbox] Output dir: {output_dir}")
|
||||
|
||||
# Extract each sandbox
|
||||
for sandbox_id, body, face in sandbox_entries:
|
||||
payloads[sandbox_id] = extract_sandbox_geometry(
|
||||
lister.WriteLine(f"\n--- Extracting {sandbox_id} ---")
|
||||
geom = extract_sandbox_geometry(
|
||||
face=face,
|
||||
body=body,
|
||||
sandbox_id=sandbox_id,
|
||||
chord_tol_mm=chord_tol_mm,
|
||||
lister=lister,
|
||||
chord_tol_mm=0.1,
|
||||
)
|
||||
|
||||
# Export
|
||||
return export_sandbox_geometries(output_dir=output_dir, geometries=payloads)
|
||||
out_path = os.path.join(output_dir, f"geometry_{sandbox_id}.json")
|
||||
with open(out_path, "w") as f:
|
||||
json.dump(geom, f, indent=2)
|
||||
lister.WriteLine(f"[extract_sandbox] Wrote: {out_path}")
|
||||
|
||||
# Summary
|
||||
lister.WriteLine(f" outer_boundary: {len(geom['outer_boundary'])} points")
|
||||
lister.WriteLine(f" inner_boundaries: {geom['num_inner_boundaries']}")
|
||||
lister.WriteLine(f" thickness: {geom['thickness']}")
|
||||
|
||||
lister.WriteLine("\n" + "=" * 60)
|
||||
lister.WriteLine(f" Done — {len(sandbox_entries)} sandbox(es) exported")
|
||||
lister.WriteLine(f" Output: {output_dir}")
|
||||
lister.WriteLine("=" * 60)
|
||||
|
||||
|
||||
def main(argv: Sequence[str] | None = None) -> int:
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Extract NX sandbox sheet geometry to JSON (run from .sim)"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--output-dir", default=".",
|
||||
help="Directory for geometry_sandbox_*.json output files",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--chord-tol", type=float, default=0.1,
|
||||
help="Edge sampling chord tolerance in mm (default: 0.1)",
|
||||
)
|
||||
args = parser.parse_args(argv)
|
||||
|
||||
out_dir = Path(args.output_dir)
|
||||
written = run_in_nx(output_dir=out_dir, chord_tol_mm=args.chord_tol)
|
||||
|
||||
print(f"\n[extract_sandbox] Done — {len(written)} file(s) exported.")
|
||||
for p in written:
|
||||
print(f" → {p}")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
main()
|
||||
|
||||
Reference in New Issue
Block a user