""" NX Part Manager Hook ==================== Provides Python functions to open, close, and save NX parts. API Reference (verified via Siemens MCP docs): - Session.Parts() -> PartCollection - PartCollection.OpenBase() -> Opens a part file - Part.Close() -> Closes the part - Part.Save() -> Saves the part - Part.SaveAs() -> Saves the part with a new name Usage: from optimization_engine.hooks.nx_cad import part_manager # Open a part part = part_manager.open_part("C:/path/to/part.prt") # Save the part part_manager.save_part(part) # Close the part part_manager.close_part(part) """ import os import json import subprocess import tempfile from pathlib import Path from typing import Optional, Dict, Any, Tuple # Import NX path from centralized config try: from config import NX_BIN_DIR NX_BIN_PATH = str(NX_BIN_DIR) except ImportError: NX_BIN_PATH = os.environ.get( "NX_BIN_PATH", r"C:\Program Files\Siemens\DesigncenterNX2512\NXBIN" ) # Journal template for part operations PART_OPERATIONS_JOURNAL = ''' # NX Open Python Journal - Part Operations # Auto-generated by Atomizer hooks import NXOpen import NXOpen.UF import json import sys import os def main(): """Execute part operation based on command arguments.""" # Get the NX session session = NXOpen.Session.GetSession() # Parse arguments: operation, part_path, [output_json] args = sys.argv[1:] if len(sys.argv) > 1 else [] if len(args) < 2: raise ValueError("Usage: script.py [output_json]") operation = args[0] part_path = args[1] output_json = args[2] if len(args) > 2 else None result = {"success": False, "error": None, "data": {}} try: if operation == "open": result = open_part(session, part_path) elif operation == "close": result = close_part(session, part_path) elif operation == "save": result = save_part(session, part_path) elif operation == "save_as": new_path = args[3] if len(args) > 3 else None result = save_part_as(session, part_path, new_path) elif operation == "info": result = get_part_info(session, part_path) else: result["error"] = f"Unknown operation: {operation}" except Exception as e: result["error"] = str(e) # Write result to output JSON if specified if output_json: with open(output_json, 'w') as f: json.dump(result, f, indent=2) return result def open_part(session, part_path): """Open a part file. NX Open API: Session.Parts().OpenActiveDisplay() """ result = {"success": False, "error": None, "data": {}} if not os.path.exists(part_path): result["error"] = f"Part file not found: {part_path}" return result try: # Set load options for the working directory working_dir = os.path.dirname(part_path) session.Parts.LoadOptions.ComponentLoadMethod = NXOpen.LoadOptions.LoadMethod.FromDirectory session.Parts.LoadOptions.SetSearchDirectories([working_dir], [True]) # Open the part using OpenActiveDisplay (more compatible with batch mode) part, load_status = session.Parts.OpenActiveDisplay( part_path, NXOpen.DisplayPartOption.AllowAdditional ) load_status.Dispose() if part is None: result["error"] = "Failed to open part - returned None" return result result["success"] = True result["data"] = { "part_name": part.Name, "full_path": part.FullPath, "leaf": part.Leaf, "is_modified": part.IsModified, "is_fully_loaded": part.IsFullyLoaded, } except Exception as e: result["error"] = str(e) return result def close_part(session, part_path): """Close a part. NX Open API: Part.Close() """ result = {"success": False, "error": None, "data": {}} try: # Find the part in the session part = find_part_by_path(session, part_path) if part is None: result["error"] = f"Part not found in session: {part_path}" return result # Close the part # Parameters: close_whole_tree, close_modified, responses part.Close( NXOpen.BasePart.CloseWholeTree.TrueValue, NXOpen.BasePart.CloseModified.CloseModified, None ) result["success"] = True result["data"] = {"closed": part_path} except Exception as e: result["error"] = str(e) return result def save_part(session, part_path): """Save a part. NX Open API: Part.Save() """ result = {"success": False, "error": None, "data": {}} try: # Find the part in the session part = find_part_by_path(session, part_path) if part is None: result["error"] = f"Part not found in session: {part_path}" return result # Save the part # Parameters: save_component_parts, close_after_save save_status = part.Save( NXOpen.BasePart.SaveComponents.TrueValue, NXOpen.BasePart.CloseAfterSave.FalseValue ) result["success"] = True result["data"] = { "saved": part_path, "is_modified": part.IsModified } except Exception as e: result["error"] = str(e) return result def save_part_as(session, part_path, new_path): """Save a part with a new name. NX Open API: Part.SaveAs() """ result = {"success": False, "error": None, "data": {}} if not new_path: result["error"] = "New path is required for SaveAs operation" return result try: # Find the part in the session part = find_part_by_path(session, part_path) if part is None: result["error"] = f"Part not found in session: {part_path}" return result # Save as new file part.SaveAs(new_path) result["success"] = True result["data"] = { "original": part_path, "saved_as": new_path } except Exception as e: result["error"] = str(e) return result def get_part_info(session, part_path): """Get information about a part. NX Open API: Part properties """ result = {"success": False, "error": None, "data": {}} try: # Find the part in the session part = find_part_by_path(session, part_path) if part is None: result["error"] = f"Part not found in session: {part_path}" return result # Get part info result["success"] = True result["data"] = { "name": part.Name, "full_path": part.FullPath, "leaf": part.Leaf, "is_modified": part.IsModified, "is_fully_loaded": part.IsFullyLoaded, "is_read_only": part.IsReadOnly, "has_write_access": part.HasWriteAccess, "part_units": str(part.PartUnits), } except Exception as e: result["error"] = str(e) return result def find_part_by_path(session, part_path): """Find a part in the session by its file path.""" part_path_normalized = os.path.normpath(part_path).lower() for part in session.Parts: if os.path.normpath(part.FullPath).lower() == part_path_normalized: return part return None if __name__ == "__main__": main() ''' def _get_run_journal_exe() -> str: """Get the path to run_journal.exe.""" return os.path.join(NX_BIN_PATH, "run_journal.exe") def _run_journal(journal_path: str, args: list) -> Tuple[bool, str]: """Run an NX journal with arguments. Returns: Tuple of (success, output_or_error) """ run_journal = _get_run_journal_exe() if not os.path.exists(run_journal): return False, f"run_journal.exe not found at {run_journal}" cmd = [run_journal, journal_path, "-args"] + args try: result = subprocess.run( cmd, capture_output=True, text=True, timeout=120 # 2 minute timeout ) if result.returncode != 0: return False, f"Journal execution failed: {result.stderr}" return True, result.stdout except subprocess.TimeoutExpired: return False, "Journal execution timed out" except Exception as e: return False, str(e) def _execute_part_operation( operation: str, part_path: str, extra_args: list = None ) -> Dict[str, Any]: """Execute a part operation via NX journal. Args: operation: The operation to perform (open, close, save, save_as, info) part_path: Path to the part file extra_args: Additional arguments for the operation Returns: Dict with operation result """ # Create temporary journal file with tempfile.NamedTemporaryFile( mode='w', suffix='.py', delete=False ) as journal_file: journal_file.write(PART_OPERATIONS_JOURNAL) journal_path = journal_file.name # Create temporary output file output_file = tempfile.NamedTemporaryFile( mode='w', suffix='.json', delete=False ).name try: # Build arguments args = [operation, part_path, output_file] if extra_args: args.extend(extra_args) # Run the journal success, output = _run_journal(journal_path, args) if not success: return {"success": False, "error": output, "data": {}} # Read the result if os.path.exists(output_file): with open(output_file, 'r') as f: return json.load(f) else: return {"success": False, "error": "Output file not created", "data": {}} finally: # Cleanup temporary files if os.path.exists(journal_path): os.unlink(journal_path) if os.path.exists(output_file): os.unlink(output_file) # ============================================================================= # Public API # ============================================================================= def open_part(part_path: str) -> Dict[str, Any]: """Open an NX part file. Args: part_path: Full path to the .prt file Returns: Dict with keys: - success: bool - error: Optional error message - data: Dict with part_name, full_path, leaf, is_modified, is_fully_loaded Example: >>> result = open_part("C:/models/bracket.prt") >>> if result["success"]: ... print(f"Opened: {result['data']['part_name']}") """ part_path = os.path.abspath(part_path) if not os.path.exists(part_path): return { "success": False, "error": f"Part file not found: {part_path}", "data": {} } return _execute_part_operation("open", part_path) def close_part(part_path: str) -> Dict[str, Any]: """Close an NX part. Args: part_path: Full path to the .prt file Returns: Dict with keys: - success: bool - error: Optional error message - data: Dict with closed path """ part_path = os.path.abspath(part_path) return _execute_part_operation("close", part_path) def save_part(part_path: str) -> Dict[str, Any]: """Save an NX part. Args: part_path: Full path to the .prt file Returns: Dict with keys: - success: bool - error: Optional error message - data: Dict with saved path and is_modified flag """ part_path = os.path.abspath(part_path) return _execute_part_operation("save", part_path) def save_part_as(part_path: str, new_path: str) -> Dict[str, Any]: """Save an NX part with a new name. Args: part_path: Full path to the original .prt file new_path: Full path for the new file Returns: Dict with keys: - success: bool - error: Optional error message - data: Dict with original and saved_as paths """ part_path = os.path.abspath(part_path) new_path = os.path.abspath(new_path) return _execute_part_operation("save_as", part_path, [new_path]) def get_part_info(part_path: str) -> Dict[str, Any]: """Get information about an NX part. Args: part_path: Full path to the .prt file Returns: Dict with keys: - success: bool - error: Optional error message - data: Dict with name, full_path, leaf, is_modified, is_fully_loaded, is_read_only, has_write_access, part_units """ part_path = os.path.abspath(part_path) return _execute_part_operation("info", part_path)