Files
mech-ai/tools/model-diagnostics/SectionBrepExtractor/ModelImageExporter.cs
T
2026-07-17 17:45:56 +08:00

2567 lines
104 KiB
C#

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<string> DefaultViewNames { get; } = ["front", "back", "top", "bottom", "left", "right", "isometric"];
public static List<string> DefaultSectionViewNames { get; } = ["front", "top", "right"];
public static List<string> DefaultHighlightViewNames { get; } = ["isometric"];
public static List<string> DefaultContextViewNames { get; } = ["isometric"];
private static readonly Dictionary<string, ModelImageViewSpec> 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<string, string[]> RefPlaneNameCandidates = new(StringComparer.OrdinalIgnoreCase)
{
["front"] = ["前视基准面", "Front Plane"],
["top"] = ["上视基准面", "Top Plane"],
["right"] = ["右视基准面", "Right Plane"]
};
public static ModelImageExportReport Export(
SldWorks sw,
string modelPath,
string outputDir,
IReadOnlyCollection<string> requestedViews,
IReadOnlyCollection<string> requestedSectionViews,
double sectionOffsetMm,
IReadOnlyCollection<ComponentHighlightImageRequest>? componentHighlightRequests,
IReadOnlyCollection<InterfaceHighlightImageRequest>? interfaceHighlightRequests,
IReadOnlyCollection<FeatureHighlightImageRequest>? featureHighlightRequests,
IReadOnlyCollection<ComponentContextImageRequest>? componentContextRequests,
Action<string, string> 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<int, Face2>();
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<string, string> 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<string, string> 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<string, string> 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<string, string> 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<string, string> 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<string, string> 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<Component2>())
{
try
{
if (component.Visible != (int)swComponentVisibilityState_e.swComponentHidden)
visibleCount++;
}
catch
{
}
}
return visibleCount;
}
private static ComponentVisibilityIsolationStats ApplyComponentVisibilityIsolation(
ModelDoc2 doc,
AssemblyDoc assembly,
IReadOnlyCollection<ComponentImageRef> showComponents,
IReadOnlyCollection<ComponentImageRef> preserveComponents,
IDictionary<Component2, int> originalVisibility)
{
var allComponents = (assembly.GetComponents(false) as object[] ?? [])
.OfType<Component2>()
.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<ComponentImageRef> showComponents,
IReadOnlyCollection<ComponentImageRef> preserveComponents,
IReadOnlyCollection<ComponentImageRef> hideComponents,
IDictionary<Component2, int> originalVisibility)
{
var allComponents = (assembly.GetComponents(false) as object[] ?? [])
.OfType<Component2>()
.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<Component2, int> 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<int, Face2> partFaceRuntimeMap,
FeatureHighlightImageRequest request,
string requestedView,
string outputDir,
Action<string, string> 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<Face2>();
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<string, string> 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<Component2, int>();
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<Face2> ResolveAssemblyComponentFaces(
Component2 component,
IReadOnlyCollection<int> faceIndices,
Action<string, string> 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<string, string> 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<string, string> 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<int, Face2> BuildComponentFaceRuntimeMap(
Component2 component,
Action<string, string> log,
string scope)
{
var result = new Dictionary<int, Face2>();
foreach (var bodyObj in EnumerateComponentBodies(component))
{
if (bodyObj is not Body2 body)
continue;
var faceIndex = 0;
for (object? faceObj = Safe<object?>(() => body.GetFirstFace(), null); faceObj != null;)
{
faceIndex++;
if (faceObj is not Face2 face)
break;
result[faceIndex] = face;
faceObj = Safe<object?>(() => 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<int, Face2> BuildPartFaceRuntimeMap(
PartDoc part,
Action<string, string> log)
{
var result = new Dictionary<int, Face2>();
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<object?>(() => body.GetFirstFace(), null); faceObj != null;)
{
faceIndex++;
if (faceObj is not Face2 face)
break;
result[faceIndex] = face;
faceObj = Safe<object?>(() => face.GetNextFace(), null);
}
}
log("image_part_face_runtime_map_built", $"faces={result.Count}");
return result;
}
private static List<Face2> ResolvePartFacesByRefs(
IReadOnlyDictionary<int, Face2> partFaceRuntimeMap,
IReadOnlyCollection<string> faceRefs,
Action<string, string> log,
string featureId)
{
var required = faceRefs
.Select(ParseFaceIndex)
.Where(index => index > 0)
.Distinct()
.ToList();
if (required.Count == 0)
return [];
var result = new List<Face2>();
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<Face2> faces,
Action<string, string> 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<string, string> log,
string scope)
{
var parts = new List<string> { 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=<no_active_view>");
}
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<string>();
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=<null>");
}
}
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<Face2> 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<ModelImageViewExport> ExportComponentContextGroupViews(
ModelDoc2 doc,
dynamic mathUtility,
List<ComponentContextImageRequest> requests,
string outputDir,
Action<string, string> log)
{
var results = new List<ModelImageViewExport>();
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<Component2, int>();
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<string, string> 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<string, string> 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<ContextBboxDirectionScore> bboxScores,
Action<string, string> 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<double[]> 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<ComponentImageRef> 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<double[]> BuildContextObliqueCandidateDirections()
{
var signs = new[] { -1.0, 1.0 };
var result = new List<double[]>();
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<double[]> 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<string, string> 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<string> { $"{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<string, string> 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<string, string> 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<string, string> 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<string, string> 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<string, string> 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<double> preferredDirection,
Action<string, string> 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<string> requestedViews,
Action<string, string> 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<T>(Func<T> 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<double> 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<string> Views { get; set; } = [];
public double[] PreferredDirectionMm { get; set; } = [];
public List<ComponentImageRef> ShowComponents { get; set; } = [];
public List<ComponentImageRef> 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<string> FaceRefs { get; set; } = [];
public List<string> Views { get; set; } = [];
public double[] PreferredDirectionMm { get; set; } = [];
public List<string> 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<string> FaceARefs { get; set; } = [];
public List<string> FaceBRefs { get; set; } = [];
public List<int> FaceAIndices { get; set; } = [];
public List<int> FaceBIndices { get; set; } = [];
[System.Text.Json.Serialization.JsonIgnore]
public List<Face2> RuntimeFaces { get; set; } = [];
public List<string> SourceContactIds { get; set; } = [];
public List<string> 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<string> 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<ComponentImageRef> ShowComponents { get; set; } = [];
public List<ComponentImageRef> 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<string> Views { get; set; } = [];
public double[] PreferredDirectionMm { get; set; } = [];
public List<ComponentImageRef> ShowComponents { get; set; } = [];
public List<ComponentImageRef> PreserveComponents { get; set; } = [];
public List<ComponentImageRef> 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<string> RequestedViews { get; set; } = [];
public List<string> 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<ModelImageViewExport> 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<string> HighlightFaceRefs { get; set; } = [];
public List<string> HighlightInterfaceSourceContactIds { get; set; } = [];
public List<double> HighlightInterfaceAxisMm { get; set; } = [];
public List<double> HighlightInterfaceCenterMm { get; set; } = [];
public List<double> 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<string> 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<double> HighlightViewDirectionMm { get; set; } = [];
public List<string> 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; }
}