""" NX Feature Manager Hook ======================= Provides Python functions to manage NX features (suppress, unsuppress, etc.). API Reference (verified via Siemens MCP docs): - Part.Features() -> FeatureCollection - Feature.Suppress() -> Suppresses the feature - Feature.Unsuppress() -> Unsuppresses the feature - Feature.Name, Feature.IsSuppressed - Session.UpdateManager.DoUpdate() -> Update the model Usage: from optimization_engine.hooks.nx_cad import feature_manager # Get all features result = feature_manager.get_features("C:/path/to/part.prt") # Suppress a feature result = feature_manager.suppress_feature("C:/path/to/part.prt", "HOLE(1)") # Unsuppress a feature result = feature_manager.unsuppress_feature("C:/path/to/part.prt", "HOLE(1)") # Get feature status result = feature_manager.get_feature_status("C:/path/to/part.prt", "HOLE(1)") """ import os import json import subprocess import tempfile from pathlib import Path from typing import Optional, Dict, Any, List, 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 feature operations FEATURE_OPERATIONS_JOURNAL = ''' # NX Open Python Journal - Feature Operations # Auto-generated by Atomizer hooks # # Based on Siemens NX Open Python API: # - Part.Features() # - Feature.Suppress() / Feature.Unsuppress() # - Feature.Name, Feature.IsSuppressed import NXOpen import NXOpen.Features import json import sys import os def main(): """Execute feature operation based on command arguments.""" # Get the NX session session = NXOpen.Session.GetSession() # Parse arguments: operation, part_path, output_json, [extra_args...] args = sys.argv[1:] if len(sys.argv) > 1 else [] if len(args) < 3: raise ValueError("Usage: script.py [args...]") operation = args[0] part_path = args[1] output_json = args[2] extra_args = args[3:] if len(args) > 3 else [] result = {"success": False, "error": None, "data": {}} try: # Ensure part is open part = ensure_part_open(session, part_path) if part is None: result["error"] = f"Failed to open part: {part_path}" elif operation == "get_all": result = get_all_features(part) elif operation == "get_status": feature_name = extra_args[0] if extra_args else None result = get_feature_status(part, feature_name) elif operation == "suppress": feature_name = extra_args[0] if extra_args else None result = suppress_feature(session, part, feature_name) elif operation == "unsuppress": feature_name = extra_args[0] if extra_args else None result = unsuppress_feature(session, part, feature_name) elif operation == "suppress_multiple": feature_names = json.loads(extra_args[0]) if extra_args else [] result = suppress_multiple_features(session, part, feature_names) elif operation == "unsuppress_multiple": feature_names = json.loads(extra_args[0]) if extra_args else [] result = unsuppress_multiple_features(session, part, feature_names) else: result["error"] = f"Unknown operation: {operation}" except Exception as e: import traceback result["error"] = str(e) result["traceback"] = traceback.format_exc() # Write result to output JSON with open(output_json, 'w') as f: json.dump(result, f, indent=2) return result def ensure_part_open(session, part_path): """Ensure the part is open and return it.""" # Check if already open 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 # Need to open it if not os.path.exists(part_path): return None 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]) # Use OpenActiveDisplay instead of OpenBase for better compatibility part, load_status = session.Parts.OpenActiveDisplay( part_path, NXOpen.DisplayPartOption.AllowAdditional ) load_status.Dispose() return part except: return None def find_feature_by_name(part, feature_name): """Find a feature by name.""" for feature in part.Features: if feature.Name == feature_name: return feature return None def get_all_features(part): """Get all features from a part. NX Open API: Part.Features() """ result = {"success": False, "error": None, "data": {}} try: features = [] for feature in part.Features: try: feature_data = { "name": feature.Name, "type": feature.FeatureType, "is_suppressed": feature.IsSuppressed, "is_internal": feature.IsInternal, } features.append(feature_data) except: # Skip features that can't be read pass result["success"] = True result["data"] = { "count": len(features), "suppressed_count": sum(1 for f in features if f["is_suppressed"]), "features": features } except Exception as e: result["error"] = str(e) return result def get_feature_status(part, feature_name): """Get status of a specific feature. NX Open API: Feature properties """ result = {"success": False, "error": None, "data": {}} if not feature_name: result["error"] = "Feature name is required" return result try: feature = find_feature_by_name(part, feature_name) if feature is None: result["error"] = f"Feature not found: {feature_name}" return result result["success"] = True result["data"] = { "name": feature.Name, "type": feature.FeatureType, "is_suppressed": feature.IsSuppressed, "is_internal": feature.IsInternal, } except Exception as e: result["error"] = str(e) return result def suppress_feature(session, part, feature_name): """Suppress a feature. NX Open API: Feature.Suppress() """ result = {"success": False, "error": None, "data": {}} if not feature_name: result["error"] = "Feature name is required" return result try: feature = find_feature_by_name(part, feature_name) if feature is None: result["error"] = f"Feature not found: {feature_name}" return result if feature.IsSuppressed: result["success"] = True result["data"] = { "name": feature_name, "action": "already_suppressed", "is_suppressed": True } return result # Set undo mark mark_id = session.SetUndoMark(NXOpen.Session.MarkVisibility.Visible, "Suppress Feature") # Suppress the feature feature.Suppress() # Update the model session.UpdateManager.DoUpdate(mark_id) result["success"] = True result["data"] = { "name": feature_name, "action": "suppressed", "is_suppressed": True } except Exception as e: result["error"] = str(e) return result def unsuppress_feature(session, part, feature_name): """Unsuppress a feature. NX Open API: Feature.Unsuppress() """ result = {"success": False, "error": None, "data": {}} if not feature_name: result["error"] = "Feature name is required" return result try: feature = find_feature_by_name(part, feature_name) if feature is None: result["error"] = f"Feature not found: {feature_name}" return result if not feature.IsSuppressed: result["success"] = True result["data"] = { "name": feature_name, "action": "already_unsuppressed", "is_suppressed": False } return result # Set undo mark mark_id = session.SetUndoMark(NXOpen.Session.MarkVisibility.Visible, "Unsuppress Feature") # Unsuppress the feature feature.Unsuppress() # Update the model session.UpdateManager.DoUpdate(mark_id) result["success"] = True result["data"] = { "name": feature_name, "action": "unsuppressed", "is_suppressed": False } except Exception as e: result["error"] = str(e) return result def suppress_multiple_features(session, part, feature_names): """Suppress multiple features. Args: session: NX session part: NX part feature_names: List of feature names to suppress """ result = {"success": False, "error": None, "data": {}} if not feature_names: result["error"] = "No feature names provided" return result try: # Set undo mark for all changes mark_id = session.SetUndoMark(NXOpen.Session.MarkVisibility.Visible, "Suppress Multiple Features") suppressed = [] errors = [] for feature_name in feature_names: feature = find_feature_by_name(part, feature_name) if feature is None: errors.append(f"Feature not found: {feature_name}") continue try: if not feature.IsSuppressed: feature.Suppress() suppressed.append(feature_name) except Exception as e: errors.append(f"Failed to suppress {feature_name}: {str(e)}") # Update the model session.UpdateManager.DoUpdate(mark_id) result["success"] = len(errors) == 0 result["data"] = { "suppressed": suppressed, "errors": errors, "suppressed_count": len(suppressed), "error_count": len(errors), } except Exception as e: result["error"] = str(e) return result def unsuppress_multiple_features(session, part, feature_names): """Unsuppress multiple features. Args: session: NX session part: NX part feature_names: List of feature names to unsuppress """ result = {"success": False, "error": None, "data": {}} if not feature_names: result["error"] = "No feature names provided" return result try: # Set undo mark for all changes mark_id = session.SetUndoMark(NXOpen.Session.MarkVisibility.Visible, "Unsuppress Multiple Features") unsuppressed = [] errors = [] for feature_name in feature_names: feature = find_feature_by_name(part, feature_name) if feature is None: errors.append(f"Feature not found: {feature_name}") continue try: if feature.IsSuppressed: feature.Unsuppress() unsuppressed.append(feature_name) except Exception as e: errors.append(f"Failed to unsuppress {feature_name}: {str(e)}") # Update the model session.UpdateManager.DoUpdate(mark_id) result["success"] = len(errors) == 0 result["data"] = { "unsuppressed": unsuppressed, "errors": errors, "unsuppressed_count": len(unsuppressed), "error_count": len(errors), } except Exception as e: result["error"] = str(e) return result 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_feature_operation( operation: str, part_path: str, extra_args: list = None ) -> Dict[str, Any]: """Execute a feature operation via NX journal. Args: operation: The operation to perform 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(FEATURE_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 get_features(part_path: str) -> Dict[str, Any]: """Get all features from 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 count, suppressed_count, features list Example: >>> result = get_features("C:/models/bracket.prt") >>> if result["success"]: ... for f in result["data"]["features"]: ... status = "suppressed" if f["is_suppressed"] else "active" ... print(f"{f['name']} ({f['type']}): {status}") """ 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_feature_operation("get_all", part_path) def get_feature_status(part_path: str, feature_name: str) -> Dict[str, Any]: """Get status of a specific feature. Args: part_path: Full path to the .prt file feature_name: Name of the feature Returns: Dict with keys: - success: bool - error: Optional error message - data: Dict with name, type, is_suppressed, is_internal Example: >>> result = get_feature_status("C:/models/bracket.prt", "HOLE(1)") >>> if result["success"]: ... print(f"Suppressed: {result['data']['is_suppressed']}") """ 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_feature_operation("get_status", part_path, [feature_name]) def suppress_feature(part_path: str, feature_name: str) -> Dict[str, Any]: """Suppress a feature in an NX part. Args: part_path: Full path to the .prt file feature_name: Name of the feature to suppress Returns: Dict with keys: - success: bool - error: Optional error message - data: Dict with name, action, is_suppressed Example: >>> result = suppress_feature("C:/models/bracket.prt", "HOLE(1)") >>> if result["success"]: ... print(f"Feature {result['data']['action']}") """ 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_feature_operation("suppress", part_path, [feature_name]) def unsuppress_feature(part_path: str, feature_name: str) -> Dict[str, Any]: """Unsuppress a feature in an NX part. Args: part_path: Full path to the .prt file feature_name: Name of the feature to unsuppress Returns: Dict with keys: - success: bool - error: Optional error message - data: Dict with name, action, is_suppressed Example: >>> result = unsuppress_feature("C:/models/bracket.prt", "HOLE(1)") >>> if result["success"]: ... print(f"Feature {result['data']['action']}") """ 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_feature_operation("unsuppress", part_path, [feature_name]) def suppress_features(part_path: str, feature_names: List[str]) -> Dict[str, Any]: """Suppress multiple features in an NX part. Args: part_path: Full path to the .prt file feature_names: List of feature names to suppress Returns: Dict with keys: - success: bool - error: Optional error message - data: Dict with suppressed list, errors list, counts Example: >>> result = suppress_features("C:/models/bracket.prt", ["HOLE(1)", "HOLE(2)"]) >>> if result["success"]: ... print(f"Suppressed {result['data']['suppressed_count']} features") """ 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": {} } # Convert list to JSON string names_json = json.dumps(feature_names) return _execute_feature_operation("suppress_multiple", part_path, [names_json]) def unsuppress_features(part_path: str, feature_names: List[str]) -> Dict[str, Any]: """Unsuppress multiple features in an NX part. Args: part_path: Full path to the .prt file feature_names: List of feature names to unsuppress Returns: Dict with keys: - success: bool - error: Optional error message - data: Dict with unsuppressed list, errors list, counts Example: >>> result = unsuppress_features("C:/models/bracket.prt", ["HOLE(1)", "HOLE(2)"]) >>> if result["success"]: ... print(f"Unsuppressed {result['data']['unsuppressed_count']} features") """ 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": {} } # Convert list to JSON string names_json = json.dumps(feature_names) return _execute_feature_operation("unsuppress_multiple", part_path, [names_json])