2567 lines
104 KiB
C#
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; }
|
|
}
|