using System.Runtime.InteropServices; using SolidWorks.Interop.sldworks; using SolidWorks.Interop.swconst; static class ModelImageExporter { private const int PartDoc = 1; private const int AssemblyDocType = 2; private const int Silent = 1; private const int ReadOnly = 2; private const int HighlightSettleDelayMs = 650; public static List DefaultViewNames { get; } = ["front", "back", "top", "bottom", "left", "right", "isometric"]; public static List DefaultSectionViewNames { get; } = ["front", "top", "right"]; public static List DefaultHighlightViewNames { get; } = ["isometric"]; public static List DefaultContextViewNames { get; } = ["isometric"]; private static readonly Dictionary ViewSpecs = new(StringComparer.OrdinalIgnoreCase) { ["front"] = new("front", "*Front", (int)swStandardViews_e.swFrontView), ["back"] = new("back", "*Back", (int)swStandardViews_e.swBackView), ["left"] = new("left", "*Left", (int)swStandardViews_e.swLeftView), ["right"] = new("right", "*Right", (int)swStandardViews_e.swRightView), ["top"] = new("top", "*Top", (int)swStandardViews_e.swTopView), ["bottom"] = new("bottom", "*Bottom", (int)swStandardViews_e.swBottomView), ["isometric"] = new("isometric", "*Isometric", (int)swStandardViews_e.swIsometricView), ["trimetric"] = new("trimetric", "*Trimetric", (int)swStandardViews_e.swTrimetricView), ["dimetric"] = new("dimetric", "*Dimetric", (int)swStandardViews_e.swDimetricView) }; private static readonly Dictionary RefPlaneNameCandidates = new(StringComparer.OrdinalIgnoreCase) { ["front"] = ["前视基准面", "Front Plane"], ["top"] = ["上视基准面", "Top Plane"], ["right"] = ["右视基准面", "Right Plane"] }; public static ModelImageExportReport Export( SldWorks sw, string modelPath, string outputDir, IReadOnlyCollection requestedViews, IReadOnlyCollection requestedSectionViews, double sectionOffsetMm, IReadOnlyCollection? componentHighlightRequests, IReadOnlyCollection? interfaceHighlightRequests, IReadOnlyCollection? featureHighlightRequests, IReadOnlyCollection? componentContextRequests, Action log) { Directory.CreateDirectory(outputDir); ClearGeneratedImageFamilyDirectory(outputDir, "component_highlights", log); ClearGeneratedImageFamilyDirectory(outputDir, "physical_interfaces", log); ClearGeneratedImageFamilyDirectory(outputDir, "feature_highlights", log); ClearGeneratedImageFamilyDirectory(outputDir, "component_contexts", log); var report = new ModelImageExportReport { Schema = "solidworks_model_image_export_v1", ModelPath = modelPath, OutputDir = outputDir, StartedAt = DateTimeOffset.Now, RequestedViews = requestedViews.ToList(), RequestedSectionViews = requestedSectionViews.ToList(), SectionOffsetMm = sectionOffsetMm }; ModelDoc2? doc = null; var shadedFaceHighlightPreference = default(TemporaryUserPreferenceToggle); try { shadedFaceHighlightPreference = EnableTemporaryUserPreferenceToggle( sw, (int)swUserPreferenceToggle_e.swUseShadedFaceHighlight, "swUseShadedFaceHighlight", log); doc = OpenOrActivateModel(sw, modelPath, log, out var openErrors, out var openWarnings); report.OpenErrors = openErrors; report.OpenWarnings = openWarnings; if (doc == null) { report.Message = $"OpenDoc6 failed. errors={openErrors}, warnings={openWarnings}"; report.CompletedAt = DateTimeOffset.Now; return report; } dynamic mathUtility = sw.GetMathUtility(); var partFaceRuntimeMap = doc is SolidWorks.Interop.sldworks.PartDoc partDoc ? BuildPartFaceRuntimeMap(partDoc, log) : new Dictionary(); ClearSectionViewBeforeStandardViews(doc, requestedViews, log); foreach (var viewName in requestedViews) { report.Views.Add(ExportView(doc, viewName, outputDir, log)); } foreach (var request in componentHighlightRequests ?? []) { foreach (var viewName in request.Views.Count > 0 ? request.Views : DefaultHighlightViewNames) { report.Views.Add(ExportHighlightedComponentView(doc, mathUtility, request, viewName, outputDir, log)); } } foreach (var request in interfaceHighlightRequests ?? []) { var views = request.Views.Count > 0 ? request.Views : ["best_oblique"]; foreach (var viewName in views.Take(3)) { ClearNativeSelectionHighlight(doc, log, $"before_interface:{request.InterfaceId}"); report.Views.Add(ExportHighlightedInterfaceView(sw, doc, mathUtility, request, viewName, outputDir, log)); } } foreach (var request in featureHighlightRequests ?? []) { var views = request.Views.Count > 0 ? request.Views : ["isometric"]; foreach (var viewName in views.Take(2)) { ClearNativeSelectionHighlight(doc, log, $"before_feature:{request.FeatureId}"); report.Views.Add(ExportHighlightedFeatureView(sw, doc, mathUtility, partFaceRuntimeMap, request, viewName, outputDir, log)); } } foreach (var group in (componentContextRequests ?? []).GroupBy(ContextVisibilityGroupKey, StringComparer.OrdinalIgnoreCase)) { report.Views.AddRange(ExportComponentContextGroupViews(doc, mathUtility, group.ToList(), outputDir, log)); } foreach (var sectionViewName in requestedSectionViews) { ClearNativeSelectionHighlight(doc, log, $"before_section:{sectionViewName}"); report.Views.Add(ExportSectionView(doc, sectionViewName, sectionOffsetMm, outputDir, log)); } report.Message = $"Exported {report.SuccessCount}/{report.Views.Count} model images."; } catch (Exception ex) { report.Message = $"Image export failed: {ex.Message}"; report.Error = ex.ToString(); } finally { RestoreTemporaryUserPreferenceToggle(sw, shadedFaceHighlightPreference, log); report.CompletedAt = DateTimeOffset.Now; } return report; } private static void ClearGeneratedImageFamilyDirectory(string outputDir, string familyDirectoryName, Action log) { var path = Path.Combine(outputDir, familyDirectoryName); if (!Directory.Exists(path)) return; try { Directory.Delete(path, recursive: true); log("image_clear_generated_family_dir", path); } catch (Exception ex) { log("image_clear_generated_family_dir_failed", $"{path}; {ex.Message}"); } } private static ModelDoc2? OpenOrActivateModel( SldWorks sw, string modelPath, Action log, out int errors, out int warnings) { errors = 0; warnings = 0; string title = Path.GetFileName(modelPath); int docType = Path.GetExtension(modelPath).Equals(".SLDASM", StringComparison.OrdinalIgnoreCase) ? AssemblyDocType : PartDoc; if (docType == PartDoc) { TryCloseOpenPartDocument(sw, modelPath, title, log); log("image_open_doc", $"{modelPath}; force_reopen_part=true"); return sw.OpenDoc6(modelPath, docType, Silent | ReadOnly, "", ref errors, ref warnings) as ModelDoc2; } try { log("image_activate_doc", title); int activateErrors = 0; var active = sw.ActivateDoc3(title, true, 0, ref activateErrors) as ModelDoc2; if (active != null) return active; } catch (Exception ex) { log("image_activate_doc_failed", ex.Message); } log("image_open_doc", modelPath); return sw.OpenDoc6(modelPath, docType, Silent | ReadOnly, "", ref errors, ref warnings) as ModelDoc2; } private static void TryCloseOpenPartDocument( SldWorks sw, string modelPath, string title, Action log) { try { log("image_force_reopen_part_activate_existing", title); int activateErrors = 0; var active = sw.ActivateDoc3(title, true, 0, ref activateErrors) as ModelDoc2; if (active == null) { log("image_force_reopen_part_no_existing_doc", $"title={title}; activate_errors={activateErrors}"); return; } var activePath = Safe(() => active.GetPathName(), ""); if (!string.Equals(Path.GetFullPath(activePath), Path.GetFullPath(modelPath), StringComparison.OrdinalIgnoreCase)) { log("image_force_reopen_part_skip_close_path_mismatch", $"title={title}; active_path={activePath}"); return; } var closeTitle = Safe(() => active.GetTitle(), title); sw.CloseDoc(closeTitle); log("image_force_reopen_part_closed_existing_doc", $"title={closeTitle}; path={activePath}"); } catch (Exception ex) { log("image_force_reopen_part_close_existing_failed", $"{title}; {ex.Message}"); } } private static ModelImageViewExport ExportView( ModelDoc2 doc, string requestedView, string outputDir, Action log) { var view = NormalizeViewSpec(requestedView); var result = new ModelImageViewExport { ImageKind = "standard_model_view", EvidenceFamily = "model_overview", EvidencePurpose = "global_shape_overview", RequestedView = requestedView, ViewName = view.Name, SolidWorksNamedView = view.NamedView, SolidWorksViewId = view.ViewId, StartedAt = DateTimeOffset.Now }; try { log("image_show_named_view", view.Name); doc.ShowNamedView2(view.NamedView, view.ViewId); doc.ViewZoomtofit2(); doc.GraphicsRedraw2(); string fileName = $"{SanitizeFileName(view.Name)}.jpg"; string outputPath = Path.Combine(outputDir, fileName); int errors = 0; int warnings = 0; log("image_save_as_jpg", outputPath); bool saved = doc.SaveAs4( outputPath, (int)swSaveAsVersion_e.swSaveAsCurrentVersion, (int)swSaveAsOptions_e.swSaveAsOptions_Silent, ref errors, ref warnings); result.Ok = saved && File.Exists(outputPath); result.OutputPath = outputPath; result.SaveErrors = errors; result.SaveWarnings = warnings; result.Message = result.Ok ? "saved" : $"SaveAs4 returned {saved}, file_exists={File.Exists(outputPath)}"; } catch (Exception ex) { result.Ok = false; result.Message = ex.Message; result.Error = ex.ToString(); } finally { result.CompletedAt = DateTimeOffset.Now; } return result; } private static ModelImageViewExport ExportSectionView( ModelDoc2 doc, string requestedSectionView, double offsetMm, string outputDir, Action log) { var spec = ParseSectionViewRequest(requestedSectionView, globalOffsetMm: offsetMm); var planeKey = spec.PlaneKey; var result = new ModelImageViewExport { ImageKind = "section_model_view", EvidenceFamily = "model_section", EvidencePurpose = "internal_structure_overview", RequestedView = requestedSectionView, ViewName = string.IsNullOrWhiteSpace(spec.Label) ? $"section_{planeKey}" : $"section_{spec.Label}", SectionPlaneKey = planeKey, SectionOffsetMm = spec.OffsetMm, SectionTarget = spec.Target, SectionReason = spec.Reason, StartedAt = DateTimeOffset.Now }; ModelViewManager? manager = null; try { ClearNativeSelectionHighlight(doc, log, $"section_start:{planeKey}"); manager = doc.ModelViewManager; if (manager == null) throw new InvalidOperationException("ModelViewManager is null."); log("image_remove_existing_section_view", planeKey); SafeRemoveSectionView(manager); ClearNativeSelectionHighlight(doc, log, $"section_after_remove_existing:{planeKey}"); var plane = FindReferencePlane(doc, planeKey, out var planeName, out var planeSource); result.SectionPlaneName = planeName; result.Message = planeSource; if (plane == null) throw new InvalidOperationException($"Reference plane not found for section view `{planeKey}`."); log("image_create_section_view_data", $"{planeKey}; plane={planeName}; offset_mm={spec.OffsetMm}; target={spec.Target}"); var sectionData = manager.CreateSectionViewData(); if (sectionData == null) throw new InvalidOperationException("CreateSectionViewData returned null."); sectionData.FirstPlane = plane; sectionData.FirstOffset = spec.OffsetMm / 1000.0; sectionData.ShowSectionCap = false; sectionData.GraphicsOnlySection = true; sectionData.Redraw = true; bool created = manager.CreateSectionView(sectionData); if (!created) throw new InvalidOperationException("CreateSectionView returned false."); var display = ViewSpecs["isometric"]; doc.ShowNamedView2(display.NamedView, display.ViewId); doc.ViewZoomtofit2(); ClearNativeSelectionHighlight(doc, log, $"section_before_save:{planeKey}"); string outputPath = Path.Combine(outputDir, $"{SanitizeFileName(result.ViewName)}.jpg"); int errors = 0; int warnings = 0; log("image_save_section_jpg", outputPath); bool saved = doc.SaveAs4( outputPath, (int)swSaveAsVersion_e.swSaveAsCurrentVersion, (int)swSaveAsOptions_e.swSaveAsOptions_Silent, ref errors, ref warnings); result.SolidWorksNamedView = display.NamedView; result.SolidWorksViewId = display.ViewId; result.OutputPath = outputPath; result.SaveErrors = errors; result.SaveWarnings = warnings; result.Ok = saved && File.Exists(outputPath); result.Message = result.Ok ? $"saved with section plane {planeName}" : $"SaveAs4 returned {saved}, file_exists={File.Exists(outputPath)}"; } catch (Exception ex) { result.Ok = false; result.Message = ex.Message; result.Error = ex.ToString(); } finally { if (manager != null) { log("image_remove_section_view", planeKey); SafeRemoveSectionView(manager); } try { doc.GraphicsRedraw2(); } catch { } result.CompletedAt = DateTimeOffset.Now; } return result; } private static ModelImageViewExport ExportHighlightedComponentView( ModelDoc2 doc, dynamic mathUtility, ComponentHighlightImageRequest request, string requestedView, string outputDir, Action log) { var view = TryNormalizeViewSpec(requestedView); var viewName = view?.Name ?? SanitizeFileName(requestedView); var result = new ModelImageViewExport { ImageKind = "assembly_component_highlight_view", EvidenceFamily = "assembly_component_position", EvidencePurpose = "component_position_and_whole_component_function", PositionVisibility = "unobstructed", RequestedView = requestedView, ViewName = $"highlight_{request.ComponentId}_{viewName}", SolidWorksNamedView = view?.NamedView ?? "custom_oblique", SolidWorksViewId = view?.ViewId ?? 0, HighlightPlanId = request.PlanId, HighlightComponentId = request.ComponentId, HighlightComponentInstanceName = request.InstanceName, HighlightComponentDisplayName = request.DisplayName, HighlightViewDirectionMm = request.PreferredDirectionMm.ToList(), ContextShownComponentCount = 0, StartedAt = DateTimeOffset.Now }; try { var component = FindComponent(doc, request); if (component == null) throw new InvalidOperationException($"Component instance not found: {request.InstanceName}"); if (doc is AssemblyDoc assembly) { result.ContextShownComponentCount = CountVisibleAssemblyComponents(assembly); log( "image_component_highlight_visibility_preserved", $"{request.ComponentId}; mode=whole_assembly_preserved; visible_components={result.ContextShownComponentCount}; hidden_by_exporter=0"); } var customViewApplied = ApplyPreferredObliqueView(doc, mathUtility, request.PreferredDirectionMm, log, request.ComponentId); if (!customViewApplied) { var fallback = view ?? NormalizeViewSpec("isometric"); doc.ShowNamedView2(fallback.NamedView, fallback.ViewId); } doc.ViewZoomtofit2(); doc.GraphicsRedraw2(); doc.ClearSelection2(true); var selectionManager = doc.SelectionManager as SelectionMgr ?? throw new InvalidOperationException("SelectionManager is null."); var selectData = selectionManager.CreateSelectData() as SelectData ?? throw new InvalidOperationException("CreateSelectData returned null."); log("image_select_component_for_highlight", $"{request.InstanceName}; view={viewName}; custom_oblique_view_applied={customViewApplied}"); var selected = component.Select4(false, selectData, false); if (!selected) throw new InvalidOperationException($"Select4 returned false for component `{request.InstanceName}`."); LogComponentSelectionDiagnostics(doc, component, request.InstanceName, request.ComponentPath, log, $"highlight:{request.ComponentId}:after_select4"); FlushGraphics(doc, log, $"highlight:{request.ComponentId}:before_final_highlight", HighlightSettleDelayMs); var nativeHighlightDrawn = DrawNativeSelectionHighlight(doc, log, $"highlight:{request.ComponentId}"); LogComponentSelectionDiagnostics(doc, component, request.InstanceName, request.ComponentPath, log, $"highlight:{request.ComponentId}:before_save"); var highlightDir = Path.Combine(outputDir, "component_highlights", SanitizeFileName(request.ComponentId)); Directory.CreateDirectory(highlightDir); string outputPath = Path.Combine(highlightDir, $"{SanitizeFileName(viewName)}.jpg"); int errors = 0; int warnings = 0; log("image_save_component_highlight_jpg", outputPath); bool saved = doc.SaveAs4( outputPath, (int)swSaveAsVersion_e.swSaveAsCurrentVersion, (int)swSaveAsOptions_e.swSaveAsOptions_Silent, ref errors, ref warnings); FlushGraphics(doc, log, $"highlight:{request.ComponentId}:after_save_settle", HighlightSettleDelayMs); result.Ok = saved && File.Exists(outputPath); result.OutputPath = outputPath; result.SaveErrors = errors; result.SaveWarnings = warnings; result.Message = result.Ok ? $"saved in the unchanged assembly visibility state with whole component selected using SolidWorks native selection highlight immediately before JPG export; custom_oblique_view_applied={customViewApplied}; native_highlight_drawn={nativeHighlightDrawn}; hidden_by_exporter=0; model not saved" : $"SaveAs4 returned {saved}, file_exists={File.Exists(outputPath)}"; } catch (Exception ex) { result.Ok = false; result.Message = ex.Message; result.Error = ex.ToString(); } finally { ClearNativeSelectionHighlight(doc, log, $"after_component:{request.ComponentId}"); try { doc.GraphicsRedraw2(); } catch { } result.CompletedAt = DateTimeOffset.Now; } return result; } private static Component2? FindComponent(ModelDoc2 doc, ComponentHighlightImageRequest request) { return FindComponent(doc, request.InstanceName, request.ComponentPath); } private static int CountVisibleAssemblyComponents(AssemblyDoc assembly) { var visibleCount = 0; foreach (var component in (assembly.GetComponents(false) as object[] ?? []).OfType()) { try { if (component.Visible != (int)swComponentVisibilityState_e.swComponentHidden) visibleCount++; } catch { } } return visibleCount; } private static ComponentVisibilityIsolationStats ApplyComponentVisibilityIsolation( ModelDoc2 doc, AssemblyDoc assembly, IReadOnlyCollection showComponents, IReadOnlyCollection preserveComponents, IDictionary originalVisibility) { var allComponents = (assembly.GetComponents(false) as object[] ?? []) .OfType() .ToList(); var showKeys = showComponents .Select(component => (component.InstanceName, component.ComponentPath)) .ToList(); var preserveKeys = preserveComponents .Select(component => (component.InstanceName, component.ComponentPath)) .DistinctBy(key => $"{key.InstanceName}::{key.ComponentPath}", StringComparer.OrdinalIgnoreCase) .ToList(); var shownCount = 0; var preservedCount = 0; var hiddenCount = 0; foreach (var component in allComponents) { int original = swComponentVisibilityState_e.swComponentUnknown.GetHashCode(); try { original = component.Visible; } catch { } originalVisibility[component] = original; var show = showKeys.Any(key => SameComponentHierarchyRef(component, key.InstanceName, key.ComponentPath)); if (show) { component.Visible = (int)swComponentVisibilityState_e.swComponentVisible; shownCount++; continue; } var preserve = preserveKeys.Any(key => SameComponentHierarchyRef(component, key.InstanceName, key.ComponentPath)); if (preserve) { preservedCount++; continue; } component.Visible = (int)swComponentVisibilityState_e.swComponentHidden; hiddenCount++; } return new ComponentVisibilityIsolationStats(shownCount, preservedCount, hiddenCount); } private static ComponentVisibilityIsolationStats ApplySelectiveComponentVisibility( AssemblyDoc assembly, IReadOnlyCollection showComponents, IReadOnlyCollection preserveComponents, IReadOnlyCollection hideComponents, IDictionary originalVisibility) { var allComponents = (assembly.GetComponents(false) as object[] ?? []) .OfType() .ToList(); var showKeys = showComponents .Select(component => (component.InstanceName, component.ComponentPath)) .ToList(); var preserveKeys = preserveComponents .Select(component => (component.InstanceName, component.ComponentPath)) .DistinctBy(key => $"{key.InstanceName}::{key.ComponentPath}", StringComparer.OrdinalIgnoreCase) .ToList(); var hideKeys = hideComponents .Select(component => (component.InstanceName, component.ComponentPath)) .DistinctBy(key => $"{key.InstanceName}::{key.ComponentPath}", StringComparer.OrdinalIgnoreCase) .ToList(); var shownCount = 0; var preservedCount = 0; var hiddenCount = 0; foreach (var component in allComponents) { int original = swComponentVisibilityState_e.swComponentUnknown.GetHashCode(); try { original = component.Visible; } catch { } originalVisibility[component] = original; var show = showKeys.Any(key => SameComponentHierarchyRef(component, key.InstanceName, key.ComponentPath)); if (show) { component.Visible = (int)swComponentVisibilityState_e.swComponentVisible; shownCount++; continue; } var preserve = preserveKeys.Any(key => SameComponentHierarchyRef(component, key.InstanceName, key.ComponentPath)); if (preserve) { preservedCount++; continue; } var hide = hideKeys.Any(key => SameComponentHierarchyRef(component, key.InstanceName, key.ComponentPath)); if (!hide) continue; component.Visible = (int)swComponentVisibilityState_e.swComponentHidden; hiddenCount++; } return new ComponentVisibilityIsolationStats(shownCount, preservedCount, hiddenCount); } private static void RestoreComponentVisibility(IDictionary originalVisibility) { foreach (var pair in originalVisibility) { try { if (pair.Value != (int)swComponentVisibilityState_e.swComponentUnknown) pair.Key.Visible = pair.Value; } catch { } } } private static ModelImageViewExport ExportHighlightedFeatureView( SldWorks sw, ModelDoc2 doc, dynamic mathUtility, IReadOnlyDictionary partFaceRuntimeMap, FeatureHighlightImageRequest request, string requestedView, string outputDir, Action log) { var view = TryNormalizeViewSpec(requestedView); var viewName = view?.Name ?? SanitizeFileName(requestedView); var result = new ModelImageViewExport { ImageKind = string.IsNullOrWhiteSpace(request.ImageKind) ? "part_feature_highlight_view" : request.ImageKind, EvidenceFamily = string.IsNullOrWhiteSpace(request.EvidenceFamily) ? "part_feature_surface" : request.EvidenceFamily, EvidencePurpose = string.IsNullOrWhiteSpace(request.EvidencePurpose) ? "part_feature_and_surface_analysis" : request.EvidencePurpose, RequestedView = requestedView, ViewName = $"feature_highlight_{request.FeatureId}_{viewName}", SolidWorksNamedView = view?.NamedView ?? "custom_oblique", SolidWorksViewId = view?.ViewId ?? 0, HighlightPlanId = request.PlanId, HighlightComponentId = request.ComponentId, HighlightComponentInstanceName = request.InstanceName, HighlightComponentDisplayName = request.DisplayName, HighlightFeatureId = request.FeatureId, HighlightFeatureType = request.FeatureType, HighlightFaceRefs = request.FaceRefs.ToList(), HighlightViewDirectionMm = request.PreferredDirectionMm.ToList(), HighlightBlockingFaceRefs = request.BlockingFaceRefs.ToList(), StartedAt = DateTimeOffset.Now }; var targetFaces = new List(); try { if (doc is not SolidWorks.Interop.sldworks.PartDoc) throw new InvalidOperationException("Feature highlight images currently require a direct part document."); var customViewApplied = ApplyPreferredObliqueView(doc, mathUtility, request.PreferredDirectionMm, log, request.FeatureId); if (!customViewApplied) { var fallback = view ?? NormalizeViewSpec("isometric"); doc.ShowNamedView2(fallback.NamedView, fallback.ViewId); } doc.ViewZoomtofit2(); doc.GraphicsRedraw2(); doc.ClearSelection2(true); targetFaces = ResolvePartFacesByRefs(partFaceRuntimeMap, request.FaceRefs, log, request.FeatureId); if (targetFaces.Count == 0) throw new InvalidOperationException($"No faces found for feature `{request.FeatureId}`."); var selected = SelectFeatureFaces(doc, targetFaces, log, request.FeatureId); if (selected == 0) throw new InvalidOperationException($"No faces selected for feature `{request.FeatureId}`."); LogFeatureHighlightDiagnostics(sw, doc, selected, log, $"{request.FeatureId}; after_select"); var nativeHighlightDrawn = DrawNativeSelectionHighlight(doc, log, $"feature:{request.FeatureId}"); LogFeatureHighlightDiagnostics(sw, doc, selected, log, $"{request.FeatureId}; after_draw_highlight"); var highlightDir = string.IsNullOrWhiteSpace(request.OutputRelativeDirectory) ? Path.Combine(outputDir, "feature_highlights", SanitizeFileName(request.FeatureId)) : Path.Combine(outputDir, request.OutputRelativeDirectory.Replace('/', Path.DirectorySeparatorChar)); Directory.CreateDirectory(highlightDir); var fileStem = string.IsNullOrWhiteSpace(request.OutputFileStem) ? SanitizeFileName(viewName) : SanitizeFileName(request.OutputFileStem); string outputPath = Path.Combine(highlightDir, $"{fileStem}.jpg"); int errors = 0; int warnings = 0; log("image_save_feature_highlight_jpg", $"{request.FeatureId}; faces={selected}; path={outputPath}"); bool saved = doc.SaveAs4( outputPath, (int)swSaveAsVersion_e.swSaveAsCurrentVersion, (int)swSaveAsOptions_e.swSaveAsOptions_Silent, ref errors, ref warnings); LogFeatureHighlightDiagnostics(sw, doc, selected, log, $"{request.FeatureId}; after_save"); result.Ok = saved && File.Exists(outputPath); result.OutputPath = outputPath; result.SaveErrors = errors; result.SaveWarnings = warnings; result.Message = result.Ok ? $"saved with runtime B-rep Face2 objects selected immediately before SaveAs4; selected_faces={selected}; custom_oblique_view_applied={customViewApplied}; native_highlight_drawn={nativeHighlightDrawn}" : $"SaveAs4 returned {saved}, file_exists={File.Exists(outputPath)}"; } catch (Exception ex) { result.Ok = false; result.Message = ex.Message; result.Error = ex.ToString(); } finally { ClearNativeSelectionHighlight(doc, log, $"after_feature:{request.FeatureId}"); result.CompletedAt = DateTimeOffset.Now; } return result; } private static ModelImageViewExport ExportHighlightedInterfaceView( SldWorks sw, ModelDoc2 doc, dynamic mathUtility, InterfaceHighlightImageRequest request, string requestedView, string outputDir, Action log) { var view = TryNormalizeViewSpec(requestedView); var viewName = view?.Name ?? SanitizeFileName(requestedView); var result = new ModelImageViewExport { ImageKind = "assembly_physical_interface_view", EvidenceFamily = "assembly_physical_interface", EvidencePurpose = "contact_and_fit_function_analysis", PositionVisibility = "interface_isolated", RequestedView = requestedView, ViewName = $"interface_highlight_{request.InterfaceId}_{viewName}", SolidWorksNamedView = view?.NamedView ?? "custom_oblique", SolidWorksViewId = view?.ViewId ?? 0, HighlightPlanId = request.PlanId, HighlightInterfaceId = request.InterfaceId, HighlightInterfaceContactKind = request.ContactKind, HighlightInterfaceMateRole = request.MateRole, HighlightComponentId = request.ComponentA.ComponentId, HighlightComponentInstanceName = request.ComponentA.InstanceName, HighlightComponentDisplayName = request.ComponentA.DisplayName, HighlightInterfaceComponentAId = request.ComponentA.ComponentId, HighlightInterfaceComponentAInstanceName = request.ComponentA.InstanceName, HighlightInterfaceComponentADisplayName = request.ComponentA.DisplayName, HighlightInterfaceComponentBId = request.ComponentB.ComponentId, HighlightInterfaceComponentBInstanceName = request.ComponentB.InstanceName, HighlightInterfaceComponentBDisplayName = request.ComponentB.DisplayName, HighlightFaceRefs = request.FaceARefs.Concat(request.FaceBRefs).Distinct(StringComparer.OrdinalIgnoreCase).ToList(), HighlightViewDirectionMm = request.PreferredDirectionMm.ToList(), HighlightInterfaceSourceContactIds = request.SourceContactIds.ToList(), HighlightInterfaceAxisMm = request.InterfaceAxisMm.ToList(), HighlightInterfaceCenterMm = request.InterfaceCenterMm.ToList(), HighlightInterfaceBBoxMm = request.InterfaceBBoxMm.ToList(), InterfaceAnalysisPriority = request.AnalysisPriority, InterfaceConfidenceTier = request.ConfidenceTier, InterfaceEvidencePurpose = request.EvidencePurpose, InterfaceTrimmedPatchVerified = request.TrimmedPatchVerified, InterfaceVerificationStatus = request.VerificationStatus, InterfaceVerificationMethods = request.VerificationMethods.ToList(), InterfaceVerificationSampleHits = request.VerificationSampleHits, InterfaceVerificationMinDistanceMm = request.VerificationMinDistanceMm, InterfaceDisplayMode = request.DisplayMode, InterfaceActualAssemblyPosition = request.ActualAssemblyPosition, InterfaceNotActualClearance = request.NotActualClearance, ContextShownComponentCount = request.ShowComponents.Count, StartedAt = DateTimeOffset.Now }; var originalVisibility = new Dictionary(); var visibilityContextApplied = false; var visibilityHiddenCount = 0; var visibilityPreservedCount = 0; try { if (doc is not AssemblyDoc assembly) throw new InvalidOperationException("Interface highlight images require an assembly document."); var componentA = FindComponent(doc, request.ComponentA.InstanceName, request.ComponentA.ComponentPath); var componentB = FindComponent(doc, request.ComponentB.InstanceName, request.ComponentB.ComponentPath); if (componentA == null) throw new InvalidOperationException($"Component A not found: {request.ComponentA.InstanceName}"); if (componentB == null) throw new InvalidOperationException($"Component B not found: {request.ComponentB.InstanceName}"); if (request.ShowComponents.Count > 0) { var stats = ApplyComponentVisibilityIsolation( doc, assembly, request.ShowComponents, request.PreserveComponents, originalVisibility); visibilityContextApplied = true; visibilityHiddenCount = stats.Hidden; visibilityPreservedCount = stats.Preserved; log("image_interface_highlight_visibility_applied", $"{request.InterfaceId}; shown={stats.Shown}; preserved_context={stats.Preserved}; hidden={stats.Hidden}"); doc.ClearSelection2(true); doc.GraphicsRedraw2(); } var effectiveDirection = ResolveInterfaceViewDirection(request, requestedView); result.HighlightViewDirectionMm = effectiveDirection.ToList(); var customViewApplied = ApplyPreferredObliqueView(doc, mathUtility, effectiveDirection, log, request.InterfaceId); if (!customViewApplied) { var fallback = view ?? NormalizeViewSpec("isometric"); doc.ShowNamedView2(fallback.NamedView, fallback.ViewId); } doc.GraphicsRedraw2(); var faces = request.RuntimeFaces.Count > 0 ? request.RuntimeFaces.Where(face => face != null).Distinct().ToList() : []; if (faces.Count == 0) { faces.AddRange(ResolveAssemblyComponentFaces(componentA, request.FaceAIndices, log, $"{request.InterfaceId}:A")); faces.AddRange(ResolveAssemblyComponentFaces(componentB, request.FaceBIndices, log, $"{request.InterfaceId}:B")); } else { log("image_resolve_interface_runtime_faces", $"{request.InterfaceId}; faces={faces.Count}"); } if (faces.Count == 0) throw new InvalidOperationException($"No runtime faces resolved for interface `{request.InterfaceId}`."); var selected = SelectFeatureFaces(doc, faces, log, $"interface:{request.InterfaceId}"); if (selected == 0) throw new InvalidOperationException($"No faces selected for interface `{request.InterfaceId}`."); var localZoomApplied = TryZoomToSelectedInterface(doc, log, request.InterfaceId); if (!localZoomApplied) doc.ViewZoomtofit2(); doc.GraphicsRedraw2(); LogFeatureHighlightDiagnostics(sw, doc, selected, log, $"interface:{request.InterfaceId}; after_select"); FlushGraphics(doc, log, $"interface:{request.InterfaceId}:before_final_highlight", HighlightSettleDelayMs); var nativeHighlightDrawn = DrawNativeSelectionHighlight(doc, log, $"interface:{request.InterfaceId}"); LogFeatureHighlightDiagnostics(sw, doc, selected, log, $"interface:{request.InterfaceId}; after_draw_highlight"); var highlightDir = Path.Combine(outputDir, "physical_interfaces", SanitizeFileName(request.InterfaceId)); Directory.CreateDirectory(highlightDir); var fileStem = SanitizeFileName(viewName); string outputPath = Path.Combine(highlightDir, $"{fileStem}.jpg"); int errors = 0; int warnings = 0; log("image_save_interface_highlight_jpg", $"{request.InterfaceId}; faces={selected}; path={outputPath}"); bool saved = doc.SaveAs4( outputPath, (int)swSaveAsVersion_e.swSaveAsCurrentVersion, (int)swSaveAsOptions_e.swSaveAsOptions_Silent, ref errors, ref warnings); result.Ok = saved && File.Exists(outputPath); result.OutputPath = outputPath; result.SaveErrors = errors; result.SaveWarnings = warnings; result.Message = result.Ok ? $"saved physical interface evidence; selected_faces={selected}; source_contacts={request.SourceContactIds.Count}; view_purpose={requestedView}; custom_view={customViewApplied}; local_zoom={localZoomApplied}; native_highlight={nativeHighlightDrawn}; actual_assembly_position={request.ActualAssemblyPosition}; not_actual_clearance={request.NotActualClearance}; visibility_context_applied={visibilityContextApplied}; hidden={visibilityHiddenCount}; preserved_context={visibilityPreservedCount}; model not saved" : $"SaveAs4 returned {saved}, file_exists={File.Exists(outputPath)}"; } catch (Exception ex) { result.Ok = false; result.Message = ex.Message; result.Error = ex.ToString(); } finally { ClearNativeSelectionHighlight(doc, log, $"after_interface:{request.InterfaceId}"); RestoreComponentVisibility(originalVisibility); try { doc.GraphicsRedraw2(); } catch { } result.CompletedAt = DateTimeOffset.Now; } return result; } private static List ResolveAssemblyComponentFaces( Component2 component, IReadOnlyCollection faceIndices, Action log, string scope) { var required = faceIndices.Where(index => index > 0).Distinct().ToList(); if (required.Count == 0) return []; var map = BuildComponentFaceRuntimeMap(component, log, scope); var result = required.Where(map.ContainsKey).Select(index => map[index]).ToList(); if (result.Count != required.Count) log("image_resolve_interface_faces_partial", $"{scope}; requested={required.Count}; found={result.Count}"); return result; } private static double[] ResolveInterfaceViewDirection(InterfaceHighlightImageRequest request, string requestedView) { var axis = request.InterfaceAxisMm.Length >= 3 ? NormalizeVector(request.InterfaceAxisMm) : []; var normal = request.InterfaceNormalMm.Length >= 3 ? NormalizeVector(request.InterfaceNormalMm) : []; if (requestedView.Equals("axis_end", StringComparison.OrdinalIgnoreCase) && axis.Length >= 3) return axis; if (requestedView.Equals("axis_side", StringComparison.OrdinalIgnoreCase) && axis.Length >= 3) { var reference = Math.Abs(Dot(axis, [0.0, 1.0, 0.0])) > 0.88 ? new[] { 0.0, 0.0, 1.0 } : new[] { 0.0, 1.0, 0.0 }; return NormalizeVector(Cross(axis, reference)); } if (requestedView.Equals("normal_view", StringComparison.OrdinalIgnoreCase) && normal.Length >= 3) return normal; return request.PreferredDirectionMm.Length >= 3 ? NormalizeVector(request.PreferredDirectionMm) : [1.0, 1.0, 1.0]; } private static bool TryZoomToSelectedInterface(ModelDoc2 doc, Action log, string interfaceId) { try { dynamic extension = doc.Extension; extension.ViewZoomToSelection(); log("image_interface_zoom_to_selection", interfaceId); return true; } catch (Exception ex) { log("image_interface_zoom_to_selection_failed", $"{interfaceId}; {ex.Message}"); return false; } } private static Face2? ResolveAssemblyComponentFace( Component2 component, int faceIndex, Action log, string scope) { if (faceIndex <= 0) return null; var map = BuildComponentFaceRuntimeMap(component, log, scope); if (map.TryGetValue(faceIndex, out var face)) return face; log("image_resolve_interface_face_missing", $"{scope}; face_index={faceIndex}; available={map.Count}"); return null; } private static Dictionary BuildComponentFaceRuntimeMap( Component2 component, Action log, string scope) { var result = new Dictionary(); foreach (var bodyObj in EnumerateComponentBodies(component)) { if (bodyObj is not Body2 body) continue; var faceIndex = 0; for (object? faceObj = Safe(() => body.GetFirstFace(), null); faceObj != null;) { faceIndex++; if (faceObj is not Face2 face) break; result[faceIndex] = face; faceObj = Safe(() => face.GetNextFace(), null); } } log("image_component_face_runtime_map_built", $"{scope}; faces={result.Count}"); return result; } private static object[] EnumerateComponentBodies(Component2 component) { try { dynamic dyn = component; object info; return ToObjectArray(dyn.GetBodies3((int)swBodyType_e.swSolidBody, out info)); } catch { return []; } } private static Dictionary BuildPartFaceRuntimeMap( PartDoc part, Action log) { var result = new Dictionary(); var faceIndex = 0; foreach (var bodyObj in ToObjectArray(part.GetBodies2((int)swBodyType_e.swSolidBody, true))) { if (bodyObj is not Body2 body) continue; for (object? faceObj = Safe(() => body.GetFirstFace(), null); faceObj != null;) { faceIndex++; if (faceObj is not Face2 face) break; result[faceIndex] = face; faceObj = Safe(() => face.GetNextFace(), null); } } log("image_part_face_runtime_map_built", $"faces={result.Count}"); return result; } private static List ResolvePartFacesByRefs( IReadOnlyDictionary partFaceRuntimeMap, IReadOnlyCollection faceRefs, Action log, string featureId) { var required = faceRefs .Select(ParseFaceIndex) .Where(index => index > 0) .Distinct() .ToList(); if (required.Count == 0) return []; var result = new List(); foreach (var index in required) { if (partFaceRuntimeMap.TryGetValue(index, out var face)) result.Add(face); } var missing = required.Count - result.Count; if (missing > 0) log("image_resolve_runtime_feature_faces_partial", $"{featureId}; found={result.Count}; missing={missing}"); return result; } private static int SelectFeatureFaces( ModelDoc2 doc, IReadOnlyList faces, Action log, string featureId) { if (faces.Count == 0) return 0; doc.ClearSelection2(true); var selectData = (doc.SelectionManager as SelectionMgr)?.CreateSelectData() as SelectData; var faceObjects = BuildFaceDispatchWrappers(faces); log("image_feature_face_dispatch_wrappers", $"{featureId}; faces={faces.Count}; wrappers={faceObjects.Length}"); var selected = 0; try { selected = doc.Extension.MultiSelect2(faceObjects, false, selectData); log("image_multiselect_feature_faces", $"{featureId}; requested={faces.Count}; selected={selected}"); } catch (Exception ex) { log("image_multiselect_feature_faces_failed", $"{featureId}; {ex.Message}"); } if (selected <= 0 && doc.SelectionManager is SelectionMgr selectionManager) { try { selectionManager.SuspendSelectionList(); selected = selectionManager.AddSelectionListObjects(faceObjects, selectData); selectionManager.ResumeSelectionList2(true); log("image_selection_list_feature_faces", $"{featureId}; requested={faces.Count}; selected={selected}"); } catch (Exception ex) { try { selectionManager.ResumeSelectionList2(false); } catch { } log("image_selection_list_feature_faces_failed", $"{featureId}; {ex.Message}"); } } if (selected <= 0) { foreach (var face in faces) { var append = selected > 0; var ok = false; try { if (face is Entity entity) ok = entity.Select4(append, selectData); } catch { } if (ok) selected++; } log("image_select4_feature_faces_fallback", $"{featureId}; requested={faces.Count}; selected={selected}"); } doc.WindowRedraw(); doc.GraphicsRedraw2(); return selected; } private static void LogFeatureHighlightDiagnostics( SldWorks sw, ModelDoc2 doc, int selected, Action log, string scope) { var parts = new List { scope, $"selected={selected}" }; try { var activeView = doc.IActiveView as ModelView ?? doc.ActiveView as ModelView; if (activeView != null) parts.Add($"display_mode={activeView.DisplayMode}"); else parts.Add("display_mode="); } catch (Exception ex) { parts.Add($"display_mode_error={ex.Message}"); } try { if (doc.SelectionManager is SelectionMgr selectionManager) { var count = Safe(() => selectionManager.GetSelectedObjectCount2(-1), selected); parts.Add($"selection_count={count}"); var types = new List(); for (var i = 1; i <= Math.Min(count, 12); i++) { var type = Safe(() => selectionManager.GetSelectedObjectType3(i, -1), 0); var name = Enum.GetName(typeof(swSelectType_e), type) ?? type.ToString(); types.Add($"{i}:{name}({type})"); } parts.Add($"selection_types=[{string.Join(",", types)}]"); } else { parts.Add("selection_manager="); } } catch (Exception ex) { parts.Add($"selection_diagnostics_error={ex.Message}"); } try { parts.Add($"swUseShadedFaceHighlight={sw.GetUserPreferenceToggle((int)swUserPreferenceToggle_e.swUseShadedFaceHighlight)}"); parts.Add($"selected_face_shaded_color={sw.GetUserPreferenceIntegerValue((int)swUserPreferenceIntegerValue_e.swSystemColorsSelectedFaceShaded)}"); parts.Add($"highlight_color={sw.GetUserPreferenceIntegerValue((int)swUserPreferenceIntegerValue_e.swSystemColorsHighlight)}"); } catch (Exception ex) { parts.Add($"preference_diagnostics_error={ex.Message}"); } log("image_feature_highlight_diagnostics", string.Join("; ", parts)); } private static DispatchWrapper[] BuildFaceDispatchWrappers(IReadOnlyList faces) { return faces .Select(face => face is Entity entity ? new DispatchWrapper(entity) : new DispatchWrapper(face)) .ToArray(); } private static int ParseFaceIndex(string faceRef) { var marker = faceRef.LastIndexOf("face#", StringComparison.OrdinalIgnoreCase); if (marker < 0) return 0; var text = faceRef[(marker + "face#".Length)..].Trim(); return int.TryParse(text, out var index) ? index : 0; } private static string ContextVisibilityGroupKey(ComponentContextImageRequest request) { if (!string.IsNullOrWhiteSpace(request.GroupId)) return request.GroupId; var showKey = string.Join("|", request.ShowComponents .Select(c => $"{c.InstanceName}::{c.ComponentPath}") .OrderBy(v => v, StringComparer.OrdinalIgnoreCase)); return string.IsNullOrWhiteSpace(showKey) ? request.PlanId : showKey; } private static List ExportComponentContextGroupViews( ModelDoc2 doc, dynamic mathUtility, List requests, string outputDir, Action log) { var results = new List(); var first = requests.FirstOrDefault(); var startedAt = DateTimeOffset.Now; if (first == null) return results; if (doc is not AssemblyDoc assembly) { foreach (var request in requests) results.Add(BuildFailedContextResult(request, "isometric", startedAt, "Context isolate images require an assembly document.")); return results; } var originalVisibility = new Dictionary(); try { var preserveComponents = requests .SelectMany(request => request.PreserveComponents) .DistinctBy(component => $"{component.InstanceName}::{component.ComponentPath}", StringComparer.OrdinalIgnoreCase) .ToList(); var hideComponents = requests .SelectMany(request => request.HideComponents) .DistinctBy(component => $"{component.InstanceName}::{component.ComponentPath}", StringComparer.OrdinalIgnoreCase) .ToList(); var stats = ApplySelectiveComponentVisibility( assembly, first.ShowComponents, preserveComponents, hideComponents, originalVisibility); log("image_component_context_visibility_applied", $"{first.GroupId}; mode=hide_blockers_only; targets={requests.Count}; shown={stats.Shown}; preserved_connected={stats.Preserved}; requested_blockers={hideComponents.Count}; hidden={stats.Hidden}"); doc.ClearSelection2(true); doc.GraphicsRedraw2(); foreach (var request in requests) { var views = request.Views.Count > 0 ? request.Views : DefaultContextViewNames; var target = FindComponent(doc, request.TargetInstanceName, request.TargetComponentPath); if (target == null) { results.Add(BuildFailedContextResult(request, views.FirstOrDefault() ?? "isometric", startedAt, $"Target component instance not found: {request.TargetInstanceName}")); continue; } foreach (var viewName in views) { results.Add(SaveComponentContextView(doc, mathUtility, target, request, viewName, outputDir, log)); } } } catch (Exception ex) { foreach (var request in requests) { var views = request.Views.Count > 0 ? request.Views : DefaultContextViewNames; results.Add(BuildFailedContextResult(request, views.FirstOrDefault() ?? "isometric", startedAt, ex.Message, ex.ToString())); } } finally { RestoreComponentVisibility(originalVisibility); try { doc.ClearSelection2(true); doc.GraphicsRedraw2(); } catch { } } return results; } private static ModelImageViewExport SaveComponentContextView( ModelDoc2 doc, dynamic mathUtility, Component2 target, ComponentContextImageRequest request, string requestedView, string outputDir, Action log) { var view = TryNormalizeViewSpec(requestedView); var viewName = view?.Name ?? SanitizeFileName(requestedView); var result = new ModelImageViewExport { ImageKind = "assembly_component_context_isolate_view", EvidenceFamily = "assembly_component_position", EvidencePurpose = "component_position_and_whole_component_function", PositionVisibility = "occluded_context", RequestedView = requestedView, ViewName = $"context_{request.TargetComponentId}_{viewName}", SolidWorksNamedView = view?.NamedView ?? "custom_oblique", SolidWorksViewId = view?.ViewId ?? 0, HighlightPlanId = request.PlanId, HighlightComponentId = request.TargetComponentId, HighlightComponentInstanceName = request.TargetInstanceName, HighlightComponentDisplayName = request.TargetDisplayName, HighlightViewDirectionMm = request.PreferredDirectionMm.ToList(), ContextShownComponentCount = doc is AssemblyDoc assembly ? CountVisibleAssemblyComponents(assembly) : request.ShowComponents.Count, StartedAt = DateTimeOffset.Now }; try { var effectiveDirection = ResolveComponentContextViewDirection(doc, target, request, log); result.HighlightViewDirectionMm = effectiveDirection.ToList(); var customViewApplied = ApplyPreferredObliqueView(doc, mathUtility, effectiveDirection, log, request.TargetComponentId); if (!customViewApplied) { var fallback = view ?? NormalizeViewSpec("isometric"); doc.ShowNamedView2(fallback.NamedView, fallback.ViewId); } doc.ViewZoomtofit2(); doc.GraphicsRedraw2(); doc.ClearSelection2(true); var selectionManager = doc.SelectionManager as SelectionMgr ?? throw new InvalidOperationException("SelectionManager is null."); var selectData = selectionManager.CreateSelectData() as SelectData ?? throw new InvalidOperationException("CreateSelectData returned null."); var selected = target.Select4(false, selectData, false); if (!selected) throw new InvalidOperationException($"Select4 returned false for target component `{request.TargetInstanceName}`."); LogComponentSelectionDiagnostics(doc, target, request.TargetInstanceName, request.TargetComponentPath, log, $"context:{request.TargetComponentId}:after_select4"); FlushGraphics(doc, log, $"context:{request.TargetComponentId}:before_final_highlight", HighlightSettleDelayMs); var nativeHighlightDrawn = DrawNativeSelectionHighlight(doc, log, $"context:{request.TargetComponentId}"); LogComponentSelectionDiagnostics(doc, target, request.TargetInstanceName, request.TargetComponentPath, log, $"context:{request.TargetComponentId}:before_save"); var contextDir = Path.Combine(outputDir, "component_contexts", SanitizeFileName(request.TargetComponentId)); Directory.CreateDirectory(contextDir); string outputPath = Path.Combine(contextDir, $"{SanitizeFileName(viewName)}.jpg"); int errors = 0; int warnings = 0; log("image_save_component_context_jpg", outputPath); bool saved = doc.SaveAs4( outputPath, (int)swSaveAsVersion_e.swSaveAsCurrentVersion, (int)swSaveAsOptions_e.swSaveAsOptions_Silent, ref errors, ref warnings); FlushGraphics(doc, log, $"context:{request.TargetComponentId}:after_save_settle", HighlightSettleDelayMs); result.Ok = saved && File.Exists(outputPath); result.OutputPath = outputPath; result.SaveErrors = errors; result.SaveWarnings = warnings; result.Message = result.Ok ? $"saved after selectively hiding detected blockers while preserving connected context using SolidWorks native selection highlight; custom_oblique_view_applied={customViewApplied}; native_highlight_drawn={nativeHighlightDrawn}; effective_direction=[{FormatVector(effectiveDirection)}]; model not saved" : $"SaveAs4 returned {saved}, file_exists={File.Exists(outputPath)}"; } catch (Exception ex) { result.Ok = false; result.Message = ex.Message; result.Error = ex.ToString(); } finally { ClearNativeSelectionHighlight(doc, log, $"after_context:{request.TargetComponentId}"); result.CompletedAt = DateTimeOffset.Now; } return result; } private static double[] ResolveComponentContextViewDirection( ModelDoc2 doc, Component2 target, ComponentContextImageRequest request, Action log) { var fallback = request.PreferredDirectionMm.Length >= 3 ? NormalizeVector([request.PreferredDirectionMm[0], request.PreferredDirectionMm[1], request.PreferredDirectionMm[2]]) : NormalizeVector([1.0, 1.0, 1.0]); var targetRef = request.ShowComponents.FirstOrDefault(component => component.ComponentId.Equals(request.TargetComponentId, StringComparison.OrdinalIgnoreCase)) ?? request.ShowComponents.FirstOrDefault(component => component.InstanceName.Equals(request.TargetInstanceName, StringComparison.OrdinalIgnoreCase)); if (targetRef == null || targetRef.BBoxMm.Length < 6) return fallback; var targetBox = NormalizeBox(targetRef.BBoxMm); if (targetBox.Length < 6) return fallback; var visibleBlockers = request.ShowComponents .Concat(request.PreserveComponents) .Where(component => !component.ComponentId.Equals(targetRef.ComponentId, StringComparison.OrdinalIgnoreCase)) .Where(component => component.BBoxMm.Length >= 6) .ToList(); var candidates = BuildContextObliqueCandidateDirections(); var bboxScores = candidates .Select(direction => ScoreContextDirectionByBBox(targetBox, visibleBlockers, direction)) .OrderByDescending(score => score.Score) .ToList(); if (bboxScores.Count == 0) return fallback; var best = bboxScores[0]; var worst = bboxScores[^1]; var spread = best.Score - worst.Score; var scale = Math.Max(1.0, Math.Abs(best.Score)); var ambiguous = spread <= scale * 0.08 || bboxScores.All(score => score.OcclusionRatio >= 0.65); if (!ambiguous) { log("image_component_context_bbox_view_selected", $"{request.TargetComponentId}; direction=[{FormatVector(best.Direction)}]; score={Math.Round(best.Score, 3)}; occlusion={Math.Round(best.OcclusionRatio, 3)}"); return best.Direction; } var rayDirection = TryChooseComponentContextDirectionByRay( doc, target, request, targetBox, bboxScores, log); if (rayDirection.Length >= 3) return rayDirection; log("image_component_context_bbox_ambiguous_ray_fallback", $"{request.TargetComponentId}; using_bbox_best=[{FormatVector(best.Direction)}]; spread={Math.Round(spread, 3)}"); return best.Direction; } private static double[] TryChooseComponentContextDirectionByRay( ModelDoc2 doc, Component2 target, ComponentContextImageRequest request, double[] targetBoxMm, IReadOnlyList bboxScores, Action log) { if (doc.Extension == null || doc.SelectionManager is not SelectionMgr selectionManager) return []; var sceneBox = NormalizeBox(request.ShowComponents .Concat(request.PreserveComponents) .Where(component => component.BBoxMm.Length >= 6) .Select(component => component.BBoxMm) .Aggregate(targetBoxMm, UnionTwoBoxes)); var sceneSpanMm = BoxDiagonalMm(sceneBox.Length >= 6 ? sceneBox : targetBoxMm); var originOffsetMm = Math.Max(1000.0, sceneSpanMm * 4.0); var rayRadiusM = Math.Max(0.0002, Math.Min(0.003, sceneSpanMm / 1000.0 * 0.002)); var scored = new List<(double[] Direction, int Hits, int Samples, double BboxScore)>(); foreach (var candidate in bboxScores.Select(score => score.Direction)) { var samples = BuildRaySamplePointsOnTargetBox(targetBoxMm, candidate); var hits = 0; foreach (var sampleMm in samples) { if (RayFirstHitMatchesTarget(doc, selectionManager, target, request, sampleMm, candidate, originOffsetMm, rayRadiusM)) hits++; } var bboxScore = bboxScores.First(score => SameDirection(score.Direction, candidate)).Score; scored.Add((candidate, hits, samples.Count, bboxScore)); } try { doc.ClearSelection2(true); } catch { } if (scored.Count == 0) return []; var best = scored .OrderByDescending(item => item.Hits) .ThenByDescending(item => item.BboxScore) .First(); if (best.Hits <= 0) { log("image_component_context_ray_view_no_target_hits", $"{request.TargetComponentId}; candidates={scored.Count}; samples_per_candidate={best.Samples}"); return []; } log("image_component_context_ray_view_selected", $"{request.TargetComponentId}; direction=[{FormatVector(best.Direction)}]; target_hits={best.Hits}/{best.Samples}; bbox_score={Math.Round(best.BboxScore, 3)}"); return best.Direction; } private static bool RayFirstHitMatchesTarget( ModelDoc2 doc, SelectionMgr selectionManager, Component2 target, ComponentContextImageRequest request, double[] sampleMm, double[] cameraDirection, double originOffsetMm, double rayRadiusM) { try { var d = NormalizeVector(cameraDirection); var originMm = Add(sampleMm, Mul(d, originOffsetMm)); doc.ClearSelection2(true); var selected = doc.Extension.SelectByRay( originMm[0] / 1000.0, originMm[1] / 1000.0, originMm[2] / 1000.0, -d[0], -d[1], -d[2], rayRadiusM, (int)swSelectType_e.swSelFACES, false, 0, 0); if (!selected || selectionManager.GetSelectedObjectCount2(-1) <= 0) return false; var selectedComponentObj = selectionManager.GetSelectedObjectsComponent4(1, -1); if (selectedComponentObj is Component2 selectedComponent) return SameOrChildComponentRef(selectedComponent, target, request.TargetInstanceName, request.TargetComponentPath); return false; } catch { return false; } finally { try { doc.ClearSelection2(true); } catch { } } } private static List BuildRaySamplePointsOnTargetBox(double[] box, double[] viewDirection) { var d = NormalizeVector(viewDirection); var up = Math.Abs(Dot(d, [0.0, 1.0, 0.0])) > 0.92 ? new[] { 0.0, 0.0, 1.0 } : new[] { 0.0, 1.0, 0.0 }; var u = NormalizeVector(Cross(up, d)); var v = NormalizeVector(Cross(d, u)); var center = BoxCenter(box); var projected = ProjectBoxForContext(box, d); var centerDepth = Dot(center, d); var uMid = (projected.MinU + projected.MaxU) / 2.0; var vMid = (projected.MinV + projected.MaxV) / 2.0; var halfU = (projected.MaxU - projected.MinU) * 0.3; var halfV = (projected.MaxV - projected.MinV) * 0.3; double[] Point(double pu, double pv) => Add(Add(Mul(u, pu), Mul(v, pv)), Mul(d, centerDepth)); return [ Point(uMid, vMid), Point(uMid - halfU, vMid - halfV), Point(uMid - halfU, vMid + halfV), Point(uMid + halfU, vMid - halfV), Point(uMid + halfU, vMid + halfV) ]; } private static ContextBboxDirectionScore ScoreContextDirectionByBBox( double[] targetBox, IReadOnlyList blockers, double[] direction) { var targetProjection = ProjectBoxForContext(targetBox, direction); var targetArea = Math.Max(1e-9, targetProjection.Area); var occlusionRatio = 0.0; foreach (var blocker in blockers) { var blockerBox = NormalizeBox(blocker.BBoxMm); if (blockerBox.Length < 6) continue; var blockerProjection = ProjectBoxForContext(blockerBox, direction); var blockerCenterDepth = (blockerProjection.MinDepth + blockerProjection.MaxDepth) / 2.0; var targetCenterDepth = (targetProjection.MinDepth + targetProjection.MaxDepth) / 2.0; if (blockerCenterDepth >= targetCenterDepth) continue; var overlapRatio = OverlapAreaForContext(targetProjection, blockerProjection) / targetArea; occlusionRatio += Math.Min(1.0, overlapRatio); } occlusionRatio = Math.Min(1.0, occlusionRatio); var score = targetArea * (1.0 - occlusionRatio * 1.8); return new ContextBboxDirectionScore(direction, score, occlusionRatio); } private static List BuildContextObliqueCandidateDirections() { var signs = new[] { -1.0, 1.0 }; var result = new List(); foreach (var sx in signs) foreach (var sy in signs) foreach (var sz in signs) result.Add(NormalizeVector([sx, sy, sz])); return result; } private static ContextProjectedBox ProjectBoxForContext(double[] box, double[] viewDirection) { var d = NormalizeVector(viewDirection); var up = Math.Abs(Dot(d, [0.0, 1.0, 0.0])) > 0.92 ? new[] { 0.0, 0.0, 1.0 } : new[] { 0.0, 1.0, 0.0 }; var u = NormalizeVector(Cross(up, d)); var v = NormalizeVector(Cross(d, u)); var corners = BoxCornersForContext(box); var uValues = corners.Select(p => Dot(p, u)).ToList(); var vValues = corners.Select(p => Dot(p, v)).ToList(); var dValues = corners.Select(p => Dot(p, d)).ToList(); return new ContextProjectedBox( uValues.Min(), uValues.Max(), vValues.Min(), vValues.Max(), dValues.Min(), dValues.Max()); } private static List BoxCornersForContext(double[] box) => [ [box[0], box[1], box[2]], [box[0], box[1], box[5]], [box[0], box[4], box[2]], [box[0], box[4], box[5]], [box[3], box[1], box[2]], [box[3], box[1], box[5]], [box[3], box[4], box[2]], [box[3], box[4], box[5]] ]; private static double OverlapAreaForContext(ContextProjectedBox a, ContextProjectedBox b) { var width = Math.Max(0.0, Math.Min(a.MaxU, b.MaxU) - Math.Max(a.MinU, b.MinU)); var height = Math.Max(0.0, Math.Min(a.MaxV, b.MaxV) - Math.Max(a.MinV, b.MinV)); return width * height; } private static double[] NormalizeBox(double[] box) { if (box.Length < 6) return []; return [ Math.Min(box[0], box[3]), Math.Min(box[1], box[4]), Math.Min(box[2], box[5]), Math.Max(box[0], box[3]), Math.Max(box[1], box[4]), Math.Max(box[2], box[5]) ]; } private static double[] UnionTwoBoxes(double[] a, double[] b) { var boxA = NormalizeBox(a); var boxB = NormalizeBox(b); if (boxA.Length < 6) return boxB; if (boxB.Length < 6) return boxA; return [ Math.Min(boxA[0], boxB[0]), Math.Min(boxA[1], boxB[1]), Math.Min(boxA[2], boxB[2]), Math.Max(boxA[3], boxB[3]), Math.Max(boxA[4], boxB[4]), Math.Max(boxA[5], boxB[5]) ]; } private static double[] BoxCenter(double[] box) => box.Length >= 6 ? [(box[0] + box[3]) / 2.0, (box[1] + box[4]) / 2.0, (box[2] + box[5]) / 2.0] : []; private static double BoxDiagonalMm(double[] box) { if (box.Length < 6) return 1000.0; var dx = box[3] - box[0]; var dy = box[4] - box[1]; var dz = box[5] - box[2]; return Math.Sqrt(dx * dx + dy * dy + dz * dz); } private static bool SameDirection(double[] a, double[] b) => a.Length >= 3 && b.Length >= 3 && Math.Abs(a[0] - b[0]) < 1e-6 && Math.Abs(a[1] - b[1]) < 1e-6 && Math.Abs(a[2] - b[2]) < 1e-6; private static bool SameOrChildComponentRef( Component2 selectedComponent, Component2 target, string targetInstanceName, string targetComponentPath) { if (ReferenceEquals(selectedComponent, target)) return true; string selectedName = ""; string selectedPath = ""; string targetName = ""; string targetPath = ""; try { selectedName = selectedComponent.Name2 ?? ""; } catch { } try { selectedPath = selectedComponent.GetPathName() ?? ""; } catch { } try { targetName = target.Name2 ?? ""; } catch { } try { targetPath = target.GetPathName() ?? ""; } catch { } if (SameComponentRef(selectedComponent, targetInstanceName, targetComponentPath)) return true; if (!string.IsNullOrWhiteSpace(targetName) && (selectedName.Equals(targetName, StringComparison.OrdinalIgnoreCase) || selectedName.StartsWith(targetName + "/", StringComparison.OrdinalIgnoreCase))) { return true; } return !string.IsNullOrWhiteSpace(targetPath) && !string.IsNullOrWhiteSpace(selectedPath) && selectedPath.Equals(targetPath, StringComparison.OrdinalIgnoreCase); } private static ModelImageViewExport BuildFailedContextResult( ComponentContextImageRequest request, string requestedView, DateTimeOffset startedAt, string message, string error = "") { return new ModelImageViewExport { ImageKind = "assembly_component_context_isolate_view", EvidenceFamily = "assembly_component_position", EvidencePurpose = "component_position_and_whole_component_function", PositionVisibility = "occluded_context", RequestedView = requestedView, ViewName = $"context_{request.TargetComponentId}_{requestedView}", HighlightPlanId = request.PlanId, HighlightComponentId = request.TargetComponentId, HighlightComponentInstanceName = request.TargetInstanceName, HighlightComponentDisplayName = request.TargetDisplayName, ContextShownComponentCount = request.ShowComponents.Count, StartedAt = startedAt, CompletedAt = DateTimeOffset.Now, Ok = false, Message = message, Error = error }; } private static Component2? FindComponent(ModelDoc2 doc, string instanceName, string componentPath) { if (doc is not AssemblyDoc assembly) return null; var components = assembly.GetComponents(false) as object[]; if (components == null) return null; Component2? fallback = null; foreach (var item in components) { if (item is not Component2 component) continue; string name = ""; string path = ""; try { name = component.Name2 ?? ""; } catch { } try { path = component.GetPathName() ?? ""; } catch { } if (!string.IsNullOrWhiteSpace(instanceName) && name.Equals(instanceName, StringComparison.OrdinalIgnoreCase) && (string.IsNullOrWhiteSpace(componentPath) || string.IsNullOrWhiteSpace(path) || path.Equals(componentPath, StringComparison.OrdinalIgnoreCase))) { return component; } if (fallback == null && !string.IsNullOrWhiteSpace(componentPath) && !string.IsNullOrWhiteSpace(path) && path.Equals(componentPath, StringComparison.OrdinalIgnoreCase)) { fallback = component; } } return fallback; } private static void LogComponentSelectionDiagnostics( ModelDoc2 doc, Component2 expectedComponent, string expectedInstanceName, string expectedComponentPath, Action log, string scope) { try { if (doc.SelectionManager is not SelectionMgr selectionManager) { log("image_component_selection_diagnostics", $"{scope}; selection_manager=null"); return; } var count = selectionManager.GetSelectedObjectCount2(-1); var parts = new List { $"{scope}; selected_count={count}" }; for (var index = 1; index <= Math.Min(count, 5); index++) { var type = 0; try { type = selectionManager.GetSelectedObjectType3(index, -1); } catch { } Component2? selectedComponent = null; try { selectedComponent = selectionManager.GetSelectedObjectsComponent4(index, -1) as Component2; } catch { } if (selectedComponent == null) { try { var selectedObject = selectionManager.GetSelectedObject6(index, -1); if (selectedObject is Component2 component) selectedComponent = component; else if (selectedObject is Entity entity) selectedComponent = entity.GetComponent() as Component2; } catch { } } var selectedName = ""; var selectedPath = ""; try { selectedName = selectedComponent?.Name2 ?? ""; } catch { } try { selectedPath = selectedComponent?.GetPathName() ?? ""; } catch { } var same = selectedComponent != null && SameComponentRef(selectedComponent, expectedInstanceName, expectedComponentPath); var sameObject = false; try { sameObject = selectedComponent != null && ReferenceEquals(selectedComponent, expectedComponent); } catch { } parts.Add($"item{index}=type:{SelectionTypeName(type)}, component:{selectedName}, same_ref:{same}, same_object:{sameObject}, path:{selectedPath}"); } log("image_component_selection_diagnostics", string.Join("; ", parts)); } catch (Exception ex) { log("image_component_selection_diagnostics_failed", $"{scope}; {ex.Message}"); } } private static string SelectionTypeName(int selectionType) { try { return $"{Enum.GetName(typeof(swSelectType_e), selectionType) ?? selectionType.ToString()}({selectionType})"; } catch { return selectionType.ToString(); } } private static bool SameComponentRef(Component2 component, string instanceName, string componentPath) { string name = ""; string path = ""; try { name = component.Name2 ?? ""; } catch { } try { path = component.GetPathName() ?? ""; } catch { } if (!string.IsNullOrWhiteSpace(instanceName)) { return name.Equals(instanceName, StringComparison.OrdinalIgnoreCase) && (string.IsNullOrWhiteSpace(componentPath) || string.IsNullOrWhiteSpace(path) || path.Equals(componentPath, StringComparison.OrdinalIgnoreCase)); } return !string.IsNullOrWhiteSpace(componentPath) && !string.IsNullOrWhiteSpace(path) && path.Equals(componentPath, StringComparison.OrdinalIgnoreCase); } private static bool SameOrChildComponentNameRef(Component2 component, string instanceName, string componentPath) { if (SameComponentRef(component, instanceName, componentPath)) return true; if (string.IsNullOrWhiteSpace(instanceName)) return false; string name = ""; try { name = component.Name2 ?? ""; } catch { } return name.StartsWith(instanceName + "/", StringComparison.OrdinalIgnoreCase); } private static bool SameComponentHierarchyRef(Component2 component, string instanceName, string componentPath) { if (SameOrChildComponentNameRef(component, instanceName, componentPath)) return true; if (string.IsNullOrWhiteSpace(instanceName)) return false; string name = ""; try { name = component.Name2 ?? ""; } catch { } return !string.IsNullOrWhiteSpace(name) && instanceName.StartsWith(name + "/", StringComparison.OrdinalIgnoreCase); } private static object? FindReferencePlane(ModelDoc2 doc, string planeKey, out string planeName, out string source) { planeName = ""; source = ""; if (!RefPlaneNameCandidates.TryGetValue(planeKey, out var candidates)) throw new ArgumentException($"Unsupported section view `{planeKey}`. Supported: {string.Join(", ", RefPlaneNameCandidates.Keys)}"); var allRefPlanes = new List<(Feature Feature, string Name)>(); Feature? feature = null; try { feature = doc.FirstFeature() as Feature; } catch { feature = null; } while (feature != null) { string typeName = ""; string name = ""; try { typeName = feature.GetTypeName2(); } catch { } try { name = feature.Name; } catch { } if (typeName.Equals("RefPlane", StringComparison.OrdinalIgnoreCase) && !string.IsNullOrWhiteSpace(name)) allRefPlanes.Add((feature, name)); try { feature = feature.GetNextFeature() as Feature; } catch { break; } } foreach (var candidate in candidates) { var match = allRefPlanes.FirstOrDefault(p => p.Name.Equals(candidate, StringComparison.OrdinalIgnoreCase)); if (match.Feature != null) return BuildPlaneReturn(match.Feature, match.Name, "matched_by_name", out planeName, out source); } int fallbackIndex = planeKey.ToLowerInvariant() switch { "front" => 0, "top" => 1, "right" => 2, _ => -1 }; if (fallbackIndex >= 0 && allRefPlanes.Count > fallbackIndex) { var fallback = allRefPlanes[fallbackIndex]; return BuildPlaneReturn(fallback.Feature, fallback.Name, "matched_by_refplane_order", out planeName, out source); } source = $"available_ref_planes={string.Join(", ", allRefPlanes.Select(p => p.Name))}"; return null; } private static object BuildPlaneReturn(Feature feature, string name, string source, out string planeName, out string planeSource) { planeName = name; planeSource = source; try { var specific = feature.GetSpecificFeature2(); if (specific != null) return specific; } catch { } return feature; } private static bool DrawNativeSelectionHighlight(ModelDoc2 doc, Action log, string scope) { try { var activeView = doc.IActiveView as ModelView ?? doc.ActiveView as ModelView; if (activeView == null) { log("image_native_selection_highlight_unavailable", $"{scope}; active view is null"); doc.GraphicsRedraw2(); return false; } doc.GraphicsRedraw2(); activeView.GraphicsRedraw(null); activeView.DrawHighlightedItems(); log("image_native_selection_highlight_drawn", scope); return true; } catch (Exception ex) { log("image_native_selection_highlight_failed", $"{scope}; {ex.Message}"); try { doc.GraphicsRedraw2(); } catch { } return false; } } private static void FlushGraphics(ModelDoc2 doc, Action log, string scope, int delayMs) { try { doc.GraphicsRedraw2(); var activeView = doc.IActiveView as ModelView ?? doc.ActiveView as ModelView; activeView?.GraphicsRedraw(null); if (delayMs > 0) System.Threading.Thread.Sleep(delayMs); doc.GraphicsRedraw2(); log("image_graphics_settled", $"{scope}; delay_ms={delayMs}"); } catch (Exception ex) { log("image_graphics_settle_failed", $"{scope}; {ex.Message}"); try { if (delayMs > 0) System.Threading.Thread.Sleep(delayMs); } catch { } } } private static void ClearNativeSelectionHighlight(ModelDoc2 doc, Action log, string scope) { try { doc.ClearSelection2(true); doc.GraphicsRedraw2(); var activeView = doc.IActiveView as ModelView ?? doc.ActiveView as ModelView; activeView?.DrawHighlightedItems(); doc.GraphicsRedraw2(); log("image_native_selection_highlight_cleared", scope); } catch (Exception ex) { log("image_native_selection_highlight_clear_failed", $"{scope}; {ex.Message}"); try { doc.ClearSelection2(true); } catch { } try { doc.GraphicsRedraw2(); } catch { } } } private static TemporaryUserPreferenceToggle EnableTemporaryUserPreferenceToggle( SldWorks sw, int preference, string name, Action log) { try { var original = sw.GetUserPreferenceToggle(preference); if (!original) sw.SetUserPreferenceToggle(preference, true); log("image_user_preference_toggle_enabled", $"{name}; original={original}; current=true"); return new TemporaryUserPreferenceToggle(name, preference, original, true); } catch (Exception ex) { log("image_user_preference_toggle_enable_failed", $"{name}; {ex.Message}"); return new TemporaryUserPreferenceToggle(name, preference, false, false); } } private static void RestoreTemporaryUserPreferenceToggle( SldWorks sw, TemporaryUserPreferenceToggle preference, Action log) { if (!preference.Valid) return; try { sw.SetUserPreferenceToggle(preference.Preference, preference.OriginalValue); log("image_user_preference_toggle_restored", $"{preference.Name}; restored={preference.OriginalValue}"); } catch (Exception ex) { log("image_user_preference_toggle_restore_failed", $"{preference.Name}; {ex.Message}"); } } private static bool ApplyPreferredObliqueView( ModelDoc2 doc, dynamic mathUtility, IReadOnlyList preferredDirection, Action log, string featureId) { if (preferredDirection.Count < 3) return false; try { var activeView = doc.IActiveView as ModelView ?? doc.ActiveView as ModelView; if (activeView == null) { log("image_custom_oblique_view_unavailable", $"{featureId}; active view is null"); return false; } var cameraDirection = NormalizeVector([preferredDirection[0], preferredDirection[1], preferredDirection[2]]); if (VectorNorm(cameraDirection) < 1e-9) return false; var d = cameraDirection; var up = Math.Abs(Dot(d, [0.0, 1.0, 0.0])) > 0.92 ? new[] { 0.0, 0.0, 1.0 } : new[] { 0.0, 1.0, 0.0 }; var u = NormalizeVector(Cross(up, d)); var v = NormalizeVector(Cross(d, u)); // SolidWorks MathTransform uses a 3x3 rotation, translation, scale, and unused slots. // The third basis vector is the model-space view direction used by Orientation3. double[] transformData = [ u[0], u[1], u[2], v[0], v[1], v[2], d[0], d[1], d[2], 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0 ]; var transform = mathUtility.CreateTransform(transformData); activeView.Orientation3 = transform; doc.GraphicsRedraw2(); log("image_custom_oblique_view_applied", $"{featureId}; camera_direction=[{FormatVector(cameraDirection)}]; solidworks_view_direction=[{FormatVector(d)}]"); return true; } catch (Exception ex) { log("image_custom_oblique_view_failed", $"{featureId}; {ex.Message}"); return false; } } private static void ClearSectionViewBeforeStandardViews( ModelDoc2 doc, IReadOnlyCollection requestedViews, Action log) { if (requestedViews.Count == 0) return; try { var manager = doc.ModelViewManager; if (manager == null) { log("image_clear_section_before_standard_views_skipped", "ModelViewManager is null"); return; } log("image_clear_section_before_standard_views", $"standard_views={requestedViews.Count}"); SafeRemoveSectionView(manager); doc.GraphicsRedraw2(); } catch (Exception ex) { log("image_clear_section_before_standard_views_failed", ex.Message); } } private static void SafeRemoveSectionView(ModelViewManager manager) { try { manager.RemoveSectionView(); } catch { } } private static T Safe(Func action, T fallback) { try { return action(); } catch { return fallback; } } private static object? InvokeCom(object obj, string name, params object?[] args) { try { return obj.GetType().InvokeMember( name, System.Reflection.BindingFlags.InvokeMethod, null, obj, args); } catch { return null; } } private static object[] ToObjectArray(object? value) => value as object[] ?? []; private static ModelImageViewSpec NormalizeViewSpec(string requestedView) { var key = (requestedView ?? "").Trim(); if (ViewSpecs.TryGetValue(key, out var view)) return view; throw new ArgumentException($"Unsupported image view `{requestedView}`. Supported: {string.Join(", ", ViewSpecs.Keys)}"); } private static ModelImageViewSpec? TryNormalizeViewSpec(string requestedView) { var key = (requestedView ?? "").Trim(); return ViewSpecs.TryGetValue(key, out var view) ? view : null; } private static double[] NormalizeVector(double[] v) { var norm = VectorNorm(v); return norm < 1e-9 ? [0.0, 0.0, 0.0] : v.Select(x => x / norm).ToArray(); } private static string FormatVector(IReadOnlyList values) => string.Join(",", values.Take(3).Select(v => Math.Round(v, 3).ToString("0.###"))); private static double VectorNorm(double[] v) => Math.Sqrt(v.Sum(x => x * x)); private static double Dot(double[] a, double[] b) => a.Zip(b, (x, y) => x * y).Sum(); private static double[] Add(double[] a, double[] b) => a.Zip(b, (x, y) => x + y).ToArray(); private static double[] Mul(double[] a, double s) => a.Select(x => x * s).ToArray(); private static double[] Cross(double[] a, double[] b) => [ a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0] ]; private static string SanitizeFileName(string value) { var invalid = Path.GetInvalidFileNameChars(); return string.Concat(value.Select(ch => invalid.Contains(ch) ? '_' : ch)); } private static SectionViewRequestSpec ParseSectionViewRequest(string requestedSectionView, double globalOffsetMm) { var raw = (requestedSectionView ?? "").Trim(); if (string.IsNullOrWhiteSpace(raw)) throw new ArgumentException("Section view request is empty."); string metadata = ""; int metadataIndex = raw.IndexOf('|'); if (metadataIndex >= 0) { metadata = raw[(metadataIndex + 1)..]; raw = raw[..metadataIndex]; } string label = ""; int labelIndex = raw.IndexOf(':'); if (labelIndex >= 0) { label = raw[(labelIndex + 1)..].Trim(); raw = raw[..labelIndex]; } double offsetMm = globalOffsetMm; string planeKey = raw; int offsetIndex = raw.IndexOf('@'); if (offsetIndex >= 0) { planeKey = raw[..offsetIndex]; var offsetText = raw[(offsetIndex + 1)..]; if (!double.TryParse(offsetText, out offsetMm)) throw new ArgumentException($"Invalid section offset in `{requestedSectionView}`."); } var target = ""; var reason = ""; foreach (var item in metadata.Split(';', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)) { var parts = item.Split('=', 2, StringSplitOptions.TrimEntries); if (parts.Length != 2) continue; if (parts[0].Equals("target", StringComparison.OrdinalIgnoreCase)) target = parts[1]; else if (parts[0].Equals("reason", StringComparison.OrdinalIgnoreCase)) reason = parts[1]; } if (string.IsNullOrWhiteSpace(label)) label = $"{planeKey}_{Math.Round(offsetMm, 3).ToString(System.Globalization.CultureInfo.InvariantCulture).Replace('-', 'm').Replace('.', 'p')}"; return new SectionViewRequestSpec(planeKey.Trim(), offsetMm, label, target, reason); } } sealed record ModelImageViewSpec(string Name, string NamedView, int ViewId); sealed record SectionViewRequestSpec(string PlaneKey, double OffsetMm, string Label, string Target, string Reason); readonly record struct TemporaryUserPreferenceToggle(string Name, int Preference, bool OriginalValue, bool Valid); readonly record struct ComponentVisibilityIsolationStats(int Shown, int Preserved, int Hidden); sealed record ContextBboxDirectionScore(double[] Direction, double Score, double OcclusionRatio); sealed record ContextProjectedBox(double MinU, double MaxU, double MinV, double MaxV, double MinDepth, double MaxDepth) { public double Area => Math.Max(0.0, MaxU - MinU) * Math.Max(0.0, MaxV - MinV); } sealed class ComponentHighlightImageRequest { public string PlanId { get; set; } = ""; public string ComponentId { get; set; } = ""; public string InstanceName { get; set; } = ""; public string DisplayName { get; set; } = ""; public string ComponentPath { get; set; } = ""; public List Views { get; set; } = []; public double[] PreferredDirectionMm { get; set; } = []; public List ShowComponents { get; set; } = []; public List PreserveComponents { get; set; } = []; } sealed class FeatureHighlightImageRequest { public string PlanId { get; set; } = ""; public string ComponentId { get; set; } = ""; public string InstanceName { get; set; } = ""; public string DisplayName { get; set; } = ""; public string ComponentPath { get; set; } = ""; public string FeatureId { get; set; } = ""; public string FeatureType { get; set; } = ""; public List FaceRefs { get; set; } = []; public List Views { get; set; } = []; public double[] PreferredDirectionMm { get; set; } = []; public List BlockingFaceRefs { get; set; } = []; public string SelectionBasis { get; set; } = ""; public string OutputRelativeDirectory { get; set; } = ""; public string OutputFileStem { get; set; } = ""; public string ImageKind { get; set; } = ""; public string EvidenceFamily { get; set; } = ""; public string EvidencePurpose { get; set; } = ""; } sealed class InterfaceHighlightImageRequest { public string PlanId { get; set; } = ""; public string InterfaceId { get; set; } = ""; public string ContactKind { get; set; } = ""; public string MateRole { get; set; } = ""; public ComponentImageRef ComponentA { get; set; } = new(); public ComponentImageRef ComponentB { get; set; } = new(); public string FaceARef { get; set; } = ""; public string FaceBRef { get; set; } = ""; public int FaceAIndex { get; set; } public int FaceBIndex { get; set; } public List FaceARefs { get; set; } = []; public List FaceBRefs { get; set; } = []; public List FaceAIndices { get; set; } = []; public List FaceBIndices { get; set; } = []; [System.Text.Json.Serialization.JsonIgnore] public List RuntimeFaces { get; set; } = []; public List SourceContactIds { get; set; } = []; public List Views { get; set; } = []; public double[] PreferredDirectionMm { get; set; } = []; public double[] InterfaceAxisMm { get; set; } = []; public double[] InterfaceNormalMm { get; set; } = []; public double[] InterfaceCenterMm { get; set; } = []; public double[] InterfaceBBoxMm { get; set; } = []; public double AnalysisPriority { get; set; } public string ConfidenceTier { get; set; } = ""; public string EvidencePurpose { get; set; } = ""; public bool TrimmedPatchVerified { get; set; } public string VerificationStatus { get; set; } = ""; public List VerificationMethods { get; set; } = []; public int VerificationSampleHits { get; set; } public double VerificationMinDistanceMm { get; set; } public string DisplayMode { get; set; } = ""; public bool ActualAssemblyPosition { get; set; } = true; public bool NotActualClearance { get; set; } = true; public List ShowComponents { get; set; } = []; public List PreserveComponents { get; set; } = []; } sealed class ComponentContextImageRequest { public string PlanId { get; set; } = ""; public string GroupId { get; set; } = ""; public string TargetComponentId { get; set; } = ""; public string TargetInstanceName { get; set; } = ""; public string TargetDisplayName { get; set; } = ""; public string TargetComponentPath { get; set; } = ""; public List Views { get; set; } = []; public double[] PreferredDirectionMm { get; set; } = []; public List ShowComponents { get; set; } = []; public List PreserveComponents { get; set; } = []; public List HideComponents { get; set; } = []; } sealed class ComponentImageRef { public string ComponentId { get; set; } = ""; public string InstanceName { get; set; } = ""; public string DisplayName { get; set; } = ""; public string ComponentPath { get; set; } = ""; public double[] BBoxMm { get; set; } = []; } sealed class ModelImageExportReport { public string Schema { get; set; } = ""; public string ModelPath { get; set; } = ""; public string OutputDir { get; set; } = ""; public List RequestedViews { get; set; } = []; public List RequestedSectionViews { get; set; } = []; public double SectionOffsetMm { get; set; } public int OpenErrors { get; set; } public int OpenWarnings { get; set; } public DateTimeOffset StartedAt { get; set; } public DateTimeOffset? CompletedAt { get; set; } public string Message { get; set; } = ""; public string Error { get; set; } = ""; public List Views { get; set; } = []; public int SuccessCount => Views.Count(v => v.Ok); } sealed class ModelImageViewExport { public string ImageKind { get; set; } = ""; public string EvidenceFamily { get; set; } = ""; public string EvidencePurpose { get; set; } = ""; public string PositionVisibility { get; set; } = ""; public string RequestedView { get; set; } = ""; public string ViewName { get; set; } = ""; public string SolidWorksNamedView { get; set; } = ""; public int SolidWorksViewId { get; set; } public string SectionPlaneKey { get; set; } = ""; public string SectionPlaneName { get; set; } = ""; public double SectionOffsetMm { get; set; } public string SectionTarget { get; set; } = ""; public string SectionReason { get; set; } = ""; public string HighlightPlanId { get; set; } = ""; public string HighlightComponentId { get; set; } = ""; public string HighlightComponentInstanceName { get; set; } = ""; public string HighlightComponentDisplayName { get; set; } = ""; public string HighlightFeatureId { get; set; } = ""; public string HighlightFeatureType { get; set; } = ""; public string HighlightInterfaceId { get; set; } = ""; public string HighlightInterfaceContactKind { get; set; } = ""; public string HighlightInterfaceMateRole { get; set; } = ""; public string HighlightInterfaceComponentAId { get; set; } = ""; public string HighlightInterfaceComponentAInstanceName { get; set; } = ""; public string HighlightInterfaceComponentADisplayName { get; set; } = ""; public string HighlightInterfaceComponentBId { get; set; } = ""; public string HighlightInterfaceComponentBInstanceName { get; set; } = ""; public string HighlightInterfaceComponentBDisplayName { get; set; } = ""; public List HighlightFaceRefs { get; set; } = []; public List HighlightInterfaceSourceContactIds { get; set; } = []; public List HighlightInterfaceAxisMm { get; set; } = []; public List HighlightInterfaceCenterMm { get; set; } = []; public List HighlightInterfaceBBoxMm { get; set; } = []; public double InterfaceAnalysisPriority { get; set; } public string InterfaceConfidenceTier { get; set; } = ""; public string InterfaceEvidencePurpose { get; set; } = ""; public bool InterfaceTrimmedPatchVerified { get; set; } public string InterfaceVerificationStatus { get; set; } = ""; public List InterfaceVerificationMethods { get; set; } = []; public int InterfaceVerificationSampleHits { get; set; } public double InterfaceVerificationMinDistanceMm { get; set; } public string InterfaceDisplayMode { get; set; } = ""; public bool InterfaceActualAssemblyPosition { get; set; } public bool InterfaceNotActualClearance { get; set; } public List HighlightViewDirectionMm { get; set; } = []; public List HighlightBlockingFaceRefs { get; set; } = []; public int ContextShownComponentCount { get; set; } public bool Ok { get; set; } public string OutputPath { get; set; } = ""; public int SaveErrors { get; set; } public int SaveWarnings { get; set; } public string Message { get; set; } = ""; public string Error { get; set; } = ""; public DateTimeOffset StartedAt { get; set; } public DateTimeOffset? CompletedAt { get; set; } }