6895 lines
286 KiB
C#
6895 lines
286 KiB
C#
using SolidWorks.Interop.sldworks;
|
|
using SolidWorks.Interop.swconst;
|
|
using System.Diagnostics;
|
|
using System.Globalization;
|
|
using System.Runtime.InteropServices;
|
|
using System.Text;
|
|
using System.Text.Encodings.Web;
|
|
using System.Text.Json;
|
|
using System.Text.Json.Serialization;
|
|
|
|
const int PartDoc = 1;
|
|
const int AssemblyDocType = 2;
|
|
const int Silent = 1;
|
|
const int ReadOnly = 2;
|
|
const double CylindricalParamDomainAxialStepMm = 2.0;
|
|
const int CylindricalBoundarySamplesPerEdge = 24;
|
|
const int CylindricalFallbackAngularSectors = 8;
|
|
const double CylindricalFaceMembershipToleranceMm = 0.03;
|
|
const int MkEUnavailable = unchecked((int)0x800401E3);
|
|
const int RpcECallRejected = unchecked((int)0x80010001);
|
|
const int RpcEServerCallRetryLater = unchecked((int)0x8001010A);
|
|
|
|
Console.OutputEncoding = Encoding.UTF8;
|
|
try
|
|
{
|
|
if (OperatingSystem.IsWindows() && Thread.CurrentThread.GetApartmentState() != ApartmentState.STA)
|
|
Thread.CurrentThread.SetApartmentState(ApartmentState.STA);
|
|
}
|
|
catch
|
|
{
|
|
// The run status records later COM failures; some hosts initialize the apartment before user code.
|
|
}
|
|
|
|
if (args.Length == 0)
|
|
{
|
|
Console.WriteLine("Usage: dotnet run --project tools\\SectionBrepExtractor\\SectionBrepExtractor.csproj -c Release -- <SLDPRT-or-SLDASM> [axisX axisY axisZ] [--up-axis x y z] [--output-dir <dir>] [--export-images] [--image-dir <dir>] [--image-views front,back,top,bottom,left,right,isometric]. SLDPRT and SLDASM use separate processing policies.");
|
|
System.Environment.Exit(2);
|
|
}
|
|
|
|
var command = ParseCommandLine(args);
|
|
var path = Path.GetFullPath(command.Path);
|
|
var axis = command.Axis;
|
|
var outDir = command.OutputDir
|
|
?? (!string.IsNullOrWhiteSpace(command.ImageOnlyReportPath)
|
|
? Path.GetDirectoryName(command.ImageOnlyReportPath) ?? Path.Combine(System.Environment.CurrentDirectory, "tools", "SectionBrepExtractor", "output")
|
|
: Path.Combine(System.Environment.CurrentDirectory, "tools", "SectionBrepExtractor", "output"));
|
|
Directory.CreateDirectory(outDir);
|
|
var jsonPath = Path.Combine(outDir, "section_brep_report.json");
|
|
var failedJsonPath = Path.Combine(outDir, "section_brep_report.failed.json");
|
|
var mdPath = Path.Combine(outDir, "section_brep_report.md");
|
|
var statusPath = Path.Combine(outDir, "run_status.json");
|
|
var options = new JsonSerializerOptions
|
|
{
|
|
WriteIndented = true,
|
|
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping
|
|
};
|
|
|
|
var status = new ExtractionRunStatus
|
|
{
|
|
InputPath = path,
|
|
OutputDir = outDir,
|
|
StartedAt = DateTimeOffset.Now,
|
|
CurrentStage = "starting",
|
|
Axis = axis
|
|
};
|
|
|
|
void Mark(string stage, string message = "")
|
|
{
|
|
status.CurrentStage = stage;
|
|
status.UpdatedAt = DateTimeOffset.Now;
|
|
status.Stages.Add(new ExtractionStageLog
|
|
{
|
|
Stage = stage,
|
|
Message = message,
|
|
Timestamp = status.UpdatedAt
|
|
});
|
|
WriteStatus();
|
|
}
|
|
|
|
void WriteStatus() =>
|
|
WriteAllTextWithRetry(statusPath, JsonSerializer.Serialize(status, options), new UTF8Encoding(false));
|
|
|
|
static void WriteAllTextWithRetry(string path, string contents, Encoding encoding, int maxAttempts = 12)
|
|
{
|
|
for (var attempt = 1; attempt <= maxAttempts; attempt++)
|
|
{
|
|
try
|
|
{
|
|
File.WriteAllText(path, contents, encoding);
|
|
return;
|
|
}
|
|
catch (IOException) when (attempt < maxAttempts)
|
|
{
|
|
Thread.Sleep(Math.Min(1000, attempt * 100));
|
|
}
|
|
}
|
|
|
|
File.WriteAllText(path, contents, encoding);
|
|
}
|
|
|
|
try
|
|
{
|
|
Mark("connect_solidworks");
|
|
var sw = Connect(Mark);
|
|
|
|
if (!string.IsNullOrWhiteSpace(command.FaceHighlightPlanPath))
|
|
{
|
|
var imageOnlyReport = RunFaceHighlightPlan(sw, command.FaceHighlightPlanPath, outDir, Mark);
|
|
status.CompletedAt = DateTimeOffset.Now;
|
|
status.Success = imageOnlyReport.Success;
|
|
status.ReportPath = imageOnlyReport.ReportPath;
|
|
status.CurrentStage = imageOnlyReport.Success ? "completed" : "completed_with_image_errors";
|
|
WriteStatus();
|
|
Console.WriteLine($"Face highlight images exported: {imageOnlyReport.ReportPath}");
|
|
System.Environment.Exit(imageOnlyReport.Success ? 0 : 4);
|
|
}
|
|
|
|
if (!string.IsNullOrWhiteSpace(command.ImageOnlyReportPath))
|
|
{
|
|
var imageOnlyExistingReport = RunImageOnlyFromExistingReport(sw, path, command.ImageOnlyReportPath, outDir, command.ImageDir, Mark);
|
|
status.CompletedAt = DateTimeOffset.Now;
|
|
status.Success = imageOnlyExistingReport.Ok;
|
|
status.ReportPath = command.ImageOnlyReportPath;
|
|
status.CurrentStage = imageOnlyExistingReport.Ok ? "completed" : "completed_with_image_errors";
|
|
WriteStatus();
|
|
Console.WriteLine($"Images re-exported from existing report: {command.ImageOnlyReportPath}");
|
|
System.Environment.Exit(imageOnlyExistingReport.Ok ? 0 : 4);
|
|
}
|
|
|
|
Mark("extract_model_brep", "connected to SolidWorks");
|
|
var report = ExtractSectionBrep(
|
|
sw,
|
|
path,
|
|
axis,
|
|
command.UpAxis,
|
|
command.SkipSectionBrep,
|
|
command.PurchasedComponentHints,
|
|
command.ExternalInterfaceRoots,
|
|
Mark);
|
|
report.Ok = !report.Message.Contains("failed", StringComparison.OrdinalIgnoreCase) &&
|
|
!report.Message.Contains("No cylindrical axis group", StringComparison.OrdinalIgnoreCase);
|
|
report.Path = path;
|
|
report.OutputDir = outDir;
|
|
report.StartedAt = status.StartedAt;
|
|
report.CompletedAt = DateTimeOffset.Now;
|
|
report.StageLogs = status.Stages;
|
|
|
|
if (command.ExportImages)
|
|
{
|
|
var hasExternalInterfaceRoots = command.ExternalInterfaceRoots.Count > 0;
|
|
var componentHighlightRequests = report.DocumentKind.Equals("part", StringComparison.OrdinalIgnoreCase)
|
|
? PartModelProcessing.BuildComponentHighlightImageRequests(report)
|
|
: AssemblyModelProcessing.BuildComponentHighlightImageRequests(report);
|
|
var interfaceHighlightRequests = report.DocumentKind.Equals("part", StringComparison.OrdinalIgnoreCase)
|
|
? []
|
|
: AssemblyModelProcessing.BuildInterfaceHighlightImageRequests(report, compactViews: hasExternalInterfaceRoots);
|
|
var featureHighlightRequests = report.DocumentKind.Equals("part", StringComparison.OrdinalIgnoreCase)
|
|
? PartModelProcessing.BuildFeatureHighlightImageRequests(report)
|
|
: [];
|
|
var componentContextRequests = report.DocumentKind.Equals("part", StringComparison.OrdinalIgnoreCase)
|
|
? PartModelProcessing.BuildComponentContextImageRequests(report)
|
|
: AssemblyModelProcessing.BuildComponentContextImageRequests(report);
|
|
var sectionViewRequests = report.DocumentKind.Equals("part", StringComparison.OrdinalIgnoreCase)
|
|
? PartModelProcessing.BuildSectionViewRequests(report)
|
|
: [];
|
|
var standardViewRequests = new List<string>();
|
|
var sectionPlanNote = report.DocumentKind.Equals("part", StringComparison.OrdinalIgnoreCase)
|
|
? $"part_axis_section_views={sectionViewRequests.Count}"
|
|
: "assembly_sections=disabled";
|
|
Mark("export_model_images", $"kind={report.DocumentKind}; views={string.Join(",", standardViewRequests)}; {sectionPlanNote}; component_highlights={componentHighlightRequests.Count}; interface_highlights={interfaceHighlightRequests.Count}; feature_highlights={featureHighlightRequests.Count}; component_contexts={componentContextRequests.Count}");
|
|
var imageDir = command.ImageDir ?? Path.Combine(outDir, "model_images");
|
|
report.ImageExport = ModelImageExporter.Export(sw, path, imageDir, standardViewRequests, sectionViewRequests, 0, componentHighlightRequests, interfaceHighlightRequests, featureHighlightRequests, componentContextRequests, Mark);
|
|
}
|
|
|
|
Mark("write_report", report.Message);
|
|
File.WriteAllText(jsonPath, JsonSerializer.Serialize(report, options), new UTF8Encoding(false));
|
|
File.WriteAllText(mdPath, BuildMarkdown(report), new UTF8Encoding(false));
|
|
|
|
status.CompletedAt = DateTimeOffset.Now;
|
|
status.Success = report.Ok;
|
|
status.ReportPath = jsonPath;
|
|
status.CurrentStage = "completed";
|
|
WriteStatus();
|
|
|
|
Console.WriteLine($"Section B-rep extracted: {jsonPath}");
|
|
Console.WriteLine(mdPath);
|
|
System.Environment.Exit(report.Ok ? 0 : 3);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
status.CompletedAt = DateTimeOffset.Now;
|
|
status.Success = false;
|
|
status.Error = ex.ToString();
|
|
status.CurrentStage = "failed";
|
|
|
|
var failed = new SectionBrepReport
|
|
{
|
|
Ok = false,
|
|
Path = path,
|
|
OutputDir = outDir,
|
|
StartedAt = status.StartedAt,
|
|
CompletedAt = status.CompletedAt,
|
|
Message = $"Extraction failed at stage `{status.CurrentStage}`: {ex.Message}",
|
|
Error = ex.ToString(),
|
|
StageLogs = status.Stages
|
|
};
|
|
File.WriteAllText(failedJsonPath, JsonSerializer.Serialize(failed, options), new UTF8Encoding(false));
|
|
File.WriteAllText(mdPath, BuildMarkdown(failed), new UTF8Encoding(false));
|
|
status.ReportPath = failedJsonPath;
|
|
WriteStatus();
|
|
Console.Error.WriteLine(ex);
|
|
System.Environment.Exit(1);
|
|
}
|
|
|
|
[DllImport("ole32.dll", CharSet = CharSet.Unicode)]
|
|
static extern int CLSIDFromProgID(string progId, out Guid clsid);
|
|
|
|
[DllImport("oleaut32.dll", PreserveSig = false)]
|
|
[return: MarshalAs(UnmanagedType.IUnknown)]
|
|
static extern object GetActiveObject(ref Guid clsid, IntPtr reserved);
|
|
|
|
static SldWorks Connect(Action<string, string> log)
|
|
{
|
|
Exception? activeObjectError = null;
|
|
int hr = CLSIDFromProgID("SldWorks.Application", out var clsid);
|
|
if (hr < 0)
|
|
Marshal.ThrowExceptionForHR(hr);
|
|
|
|
var attachTimeoutSeconds = GetSolidWorksAttachTimeoutSeconds(IsSolidWorksProcessAlive());
|
|
log("connect_attach_timeout", $"{attachTimeoutSeconds} seconds");
|
|
var start = DateTimeOffset.Now;
|
|
while ((DateTimeOffset.Now - start).TotalSeconds < attachTimeoutSeconds)
|
|
{
|
|
try
|
|
{
|
|
log("connect_get_active_object", "SldWorks.Application");
|
|
var sw = (SldWorks)GetActiveObject(ref clsid, IntPtr.Zero);
|
|
_ = sw.Visible;
|
|
log("connect_get_active_object_ok", "attached to running SolidWorks instance");
|
|
return sw;
|
|
}
|
|
catch (COMException ex) when (ex.ErrorCode is MkEUnavailable or RpcECallRejected or RpcEServerCallRetryLater)
|
|
{
|
|
activeObjectError = ex;
|
|
log("connect_get_active_object_wait", $"0x{ex.ErrorCode:X8} {ex.Message}");
|
|
Thread.Sleep(1000);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
activeObjectError = ex;
|
|
log("connect_get_active_object_failed", ex.Message);
|
|
Thread.Sleep(1000);
|
|
}
|
|
}
|
|
|
|
log("connect_get_active_object_unavailable", $"SldWorks.Application was not available from ROT within {attachTimeoutSeconds} seconds. Last error: {activeObjectError?.Message}");
|
|
log("connect_create_instance", "SldWorks.Application");
|
|
var type = Type.GetTypeFromProgID("SldWorks.Application")
|
|
?? throw new InvalidOperationException("SolidWorks ProgID not found.");
|
|
|
|
try
|
|
{
|
|
var sw = (SldWorks?)Activator.CreateInstance(type)
|
|
?? throw new InvalidOperationException("Cannot create SolidWorks.Application.");
|
|
sw.Visible = true;
|
|
log("connect_create_instance_ok", "created or attached to SolidWorks automation instance");
|
|
return sw;
|
|
}
|
|
catch (Exception createEx)
|
|
{
|
|
throw new InvalidOperationException(
|
|
"Cannot connect to SolidWorks. " +
|
|
$"GetActiveObject did not expose SldWorks.Application from ROT: {activeObjectError?.Message}. " +
|
|
$"CreateInstance failed: {createEx.Message}. " +
|
|
"Check that SolidWorks is fully started, no modal startup dialog is blocking it, and this extractor is running at the same elevation level as SolidWorks.",
|
|
createEx);
|
|
}
|
|
}
|
|
|
|
static FaceHighlightPlanRunReport RunFaceHighlightPlan(
|
|
SldWorks sw,
|
|
string planPath,
|
|
string outDir,
|
|
Action<string, string> log)
|
|
{
|
|
log("face_highlight_plan_read", planPath);
|
|
if (!File.Exists(planPath))
|
|
throw new FileNotFoundException($"Face highlight plan not found: {planPath}");
|
|
|
|
var plan = JsonSerializer.Deserialize<FaceHighlightExportPlan>(
|
|
File.ReadAllText(planPath),
|
|
new JsonSerializerOptions { PropertyNameCaseInsensitive = true })
|
|
?? throw new InvalidOperationException($"Cannot parse face highlight plan: {planPath}");
|
|
|
|
Directory.CreateDirectory(outDir);
|
|
var startedAt = DateTimeOffset.Now;
|
|
var reportPath = Path.Combine(outDir, string.IsNullOrWhiteSpace(plan.ReportFileName) ? "face_highlight_export_manifest.json" : plan.ReportFileName);
|
|
var groups = plan.Groups ?? [];
|
|
var allRequests = groups
|
|
.SelectMany(group => group.Components ?? [], (group, component) => new { group, component })
|
|
.Where(item => !string.IsNullOrWhiteSpace(item.component.ComponentPath))
|
|
.GroupBy(item => Path.GetFullPath(item.component.ComponentPath), StringComparer.OrdinalIgnoreCase)
|
|
.ToList();
|
|
|
|
var groupReports = groups.ToDictionary(
|
|
group => group.GroupId ?? "",
|
|
group => new FaceHighlightGroupRunReport
|
|
{
|
|
GroupId = group.GroupId ?? "",
|
|
Directory = group.Directory ?? "",
|
|
Components = []
|
|
},
|
|
StringComparer.OrdinalIgnoreCase);
|
|
var componentReports = new Dictionary<string, FaceHighlightComponentRunReport>(StringComparer.OrdinalIgnoreCase);
|
|
|
|
foreach (var modelGroup in allRequests)
|
|
{
|
|
var modelPath = modelGroup.Key;
|
|
var requests = new List<FeatureHighlightImageRequest>();
|
|
foreach (var item in modelGroup)
|
|
{
|
|
var groupId = item.group.GroupId ?? "";
|
|
var component = item.component;
|
|
var componentKey = $"{groupId}::{component.ComponentId}";
|
|
var componentReport = new FaceHighlightComponentRunReport
|
|
{
|
|
ComponentId = component.ComponentId ?? "",
|
|
InstanceName = component.InstanceName ?? "",
|
|
DisplayName = component.DisplayName ?? "",
|
|
ComponentPath = component.ComponentPath ?? "",
|
|
RequestedFaceCount = component.Faces?.Count ?? 0
|
|
};
|
|
componentReports[componentKey] = componentReport;
|
|
if (groupReports.TryGetValue(groupId, out var groupReport))
|
|
groupReport.Components.Add(componentReport);
|
|
|
|
foreach (var face in component.Faces ?? [])
|
|
{
|
|
var faceRef = FirstNonEmpty(face.FaceRef, BuildFaceRef(component.InstanceName, face.FaceIndex));
|
|
if (string.IsNullOrWhiteSpace(faceRef))
|
|
continue;
|
|
|
|
var faceIndex = face.FaceIndex > 0 ? face.FaceIndex : ParseFaceIndex(faceRef);
|
|
var faceKey = faceIndex > 0 ? $"face_{faceIndex:0000}" : SanitizeFileName(faceRef);
|
|
requests.Add(new FeatureHighlightImageRequest
|
|
{
|
|
PlanId = FirstNonEmpty(face.PlanId, $"{SanitizeFileName(groupId)}_{SanitizeFileName(component.ComponentId ?? component.InstanceName ?? "component")}_{faceKey}"),
|
|
ComponentId = component.ComponentId ?? "",
|
|
InstanceName = component.InstanceName ?? "",
|
|
DisplayName = component.DisplayName ?? "",
|
|
ComponentPath = component.ComponentPath ?? "",
|
|
FeatureId = FirstNonEmpty(face.FeatureId, faceKey),
|
|
FeatureType = FirstNonEmpty(face.FaceKind, "single_face"),
|
|
FaceRefs = [faceRef],
|
|
Views = face.Views is { Count: > 0 } ? face.Views : ["isometric"],
|
|
PreferredDirectionMm = face.PreferredDirectionMm is { Length: >= 3 } ? face.PreferredDirectionMm : [1.0, 1.0, 1.0],
|
|
SelectionBasis = "functional_group_all_face_highlight_plan_reusing_initial_brep_face_refs",
|
|
ImageKind = "functional_group_all_face_highlight",
|
|
EvidenceFamily = "part_all_face_surface",
|
|
EvidencePurpose = "post_functional_group_part_and_surface_diagnosis",
|
|
OutputRelativeDirectory = FirstNonEmpty(
|
|
face.OutputRelativeDirectory,
|
|
Path.Combine(item.group.Directory ?? SanitizeFileName(groupId), "all_face_highlights", SanitizeFileName(component.ComponentId ?? component.InstanceName ?? "component")).Replace('\\', '/')),
|
|
OutputFileStem = FirstNonEmpty(face.OutputFileStem, faceKey)
|
|
});
|
|
}
|
|
}
|
|
|
|
if (requests.Count == 0)
|
|
continue;
|
|
|
|
log("face_highlight_plan_export_model", $"{modelPath}; requests={requests.Count}");
|
|
var imageReport = ModelImageExporter.Export(
|
|
sw,
|
|
modelPath,
|
|
outDir,
|
|
[],
|
|
[],
|
|
0,
|
|
[],
|
|
[],
|
|
requests,
|
|
[],
|
|
log);
|
|
|
|
foreach (var view in imageReport.Views)
|
|
{
|
|
var groupId = ResolveGroupIdFromOutputDirectory(view.OutputPath, outDir, groups);
|
|
var componentKey = $"{groupId}::{view.HighlightComponentId}";
|
|
if (!componentReports.TryGetValue(componentKey, out var componentReport))
|
|
continue;
|
|
componentReport.Views.Add(view);
|
|
}
|
|
}
|
|
|
|
foreach (var groupReport in groupReports.Values)
|
|
{
|
|
foreach (var component in groupReport.Components)
|
|
{
|
|
component.ExportedFaceCount = component.Views
|
|
.Where(view => view.Ok)
|
|
.SelectMany(view => view.HighlightFaceRefs)
|
|
.Distinct(StringComparer.OrdinalIgnoreCase)
|
|
.Count();
|
|
}
|
|
groupReport.RequestedFaceCount = groupReport.Components.Sum(component => component.RequestedFaceCount);
|
|
groupReport.ExportedFaceCount = groupReport.Components.Sum(component => component.ExportedFaceCount);
|
|
}
|
|
|
|
var runReport = new FaceHighlightPlanRunReport
|
|
{
|
|
Schema = "functional_group_all_face_highlight_export_manifest_v1",
|
|
GenerationMode = "image_only_from_functional_group_plan_no_brep_extraction",
|
|
PlanPath = planPath,
|
|
OutputDir = outDir,
|
|
StartedAt = startedAt,
|
|
CompletedAt = DateTimeOffset.Now,
|
|
Groups = groupReports.Values.OrderBy(group => group.GroupId, StringComparer.OrdinalIgnoreCase).ToList()
|
|
};
|
|
runReport.ReportPath = reportPath;
|
|
runReport.Success = runReport.Groups.All(group => group.Components.All(component => component.Views.Count == 0 || component.Views.All(view => view.Ok)));
|
|
runReport.Message = $"Exported {runReport.Groups.Sum(group => group.ExportedFaceCount)}/{runReport.Groups.Sum(group => group.RequestedFaceCount)} requested face highlight images.";
|
|
|
|
File.WriteAllText(reportPath, JsonSerializer.Serialize(runReport, new JsonSerializerOptions
|
|
{
|
|
WriteIndented = true,
|
|
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
|
|
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
|
|
}), new UTF8Encoding(false));
|
|
return runReport;
|
|
}
|
|
|
|
static SectionBrepReport RunImageOnlyFromExistingReport(
|
|
SldWorks sw,
|
|
string requestedModelPath,
|
|
string reportPath,
|
|
string outDir,
|
|
string? imageDirOverride,
|
|
Action<string, string> log)
|
|
{
|
|
log("image_only_report_read", reportPath);
|
|
if (!File.Exists(reportPath))
|
|
throw new FileNotFoundException($"Existing section_brep_report.json not found: {reportPath}");
|
|
|
|
var jsonOptions = new JsonSerializerOptions
|
|
{
|
|
PropertyNameCaseInsensitive = true,
|
|
WriteIndented = true,
|
|
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping
|
|
};
|
|
var report = JsonSerializer.Deserialize<SectionBrepReport>(File.ReadAllText(reportPath), jsonOptions)
|
|
?? throw new InvalidOperationException($"Cannot parse existing report: {reportPath}");
|
|
|
|
var modelPath = ResolveImageOnlyModelPath(requestedModelPath, report.Path);
|
|
if (!File.Exists(modelPath))
|
|
throw new FileNotFoundException($"Model path for image-only export not found: {modelPath}");
|
|
|
|
var componentHighlightRequests = report.DocumentKind.Equals("part", StringComparison.OrdinalIgnoreCase)
|
|
? PartModelProcessing.BuildComponentHighlightImageRequests(report)
|
|
: AssemblyModelProcessing.BuildComponentHighlightImageRequests(report);
|
|
var interfaceHighlightRequests = report.DocumentKind.Equals("part", StringComparison.OrdinalIgnoreCase)
|
|
? []
|
|
: AssemblyModelProcessing.BuildInterfaceHighlightImageRequests(report);
|
|
var featureHighlightRequests = report.DocumentKind.Equals("part", StringComparison.OrdinalIgnoreCase)
|
|
? PartModelProcessing.BuildFeatureHighlightImageRequests(report)
|
|
: [];
|
|
var componentContextRequests = report.DocumentKind.Equals("part", StringComparison.OrdinalIgnoreCase)
|
|
? PartModelProcessing.BuildComponentContextImageRequests(report)
|
|
: AssemblyModelProcessing.BuildComponentContextImageRequests(report);
|
|
var sectionViewRequests = report.DocumentKind.Equals("part", StringComparison.OrdinalIgnoreCase)
|
|
? PartModelProcessing.BuildSectionViewRequests(report)
|
|
: [];
|
|
var standardViewRequests = new List<string>();
|
|
var imageDir = imageDirOverride ?? Path.Combine(outDir, "model_images");
|
|
|
|
log(
|
|
"export_model_images_from_existing_report",
|
|
$"kind={report.DocumentKind}; component_highlights={componentHighlightRequests.Count}; interface_highlights={interfaceHighlightRequests.Count}; feature_highlights={featureHighlightRequests.Count}; component_contexts={componentContextRequests.Count}; no_brep_reextract=true");
|
|
report.ImageExport = ModelImageExporter.Export(
|
|
sw,
|
|
modelPath,
|
|
imageDir,
|
|
standardViewRequests,
|
|
sectionViewRequests,
|
|
0,
|
|
componentHighlightRequests,
|
|
interfaceHighlightRequests,
|
|
featureHighlightRequests,
|
|
componentContextRequests,
|
|
log);
|
|
report.Path = modelPath;
|
|
report.OutputDir = outDir;
|
|
report.CompletedAt = DateTimeOffset.Now;
|
|
report.StageLogs.Add(new ExtractionStageLog
|
|
{
|
|
Stage = "image_only_from_existing_report",
|
|
Message = $"Re-exported images without B-rep extraction. component_highlights={componentHighlightRequests.Count}; success={report.ImageExport.SuccessCount}/{report.ImageExport.Views.Count}",
|
|
Timestamp = DateTimeOffset.Now
|
|
});
|
|
report.Ok = report.ImageExport.Views.All(view => view.Ok);
|
|
report.Message = $"Image-only export from existing B-rep report completed: {report.ImageExport.SuccessCount}/{report.ImageExport.Views.Count} images.";
|
|
|
|
File.WriteAllText(reportPath, JsonSerializer.Serialize(report, jsonOptions), new UTF8Encoding(false));
|
|
var mdPath = Path.Combine(Path.GetDirectoryName(reportPath) ?? outDir, "section_brep_report.md");
|
|
File.WriteAllText(mdPath, BuildMarkdown(report), new UTF8Encoding(false));
|
|
return report;
|
|
}
|
|
|
|
static string ResolveImageOnlyModelPath(string requestedModelPath, string reportModelPath)
|
|
{
|
|
if (IsSolidWorksModelPath(requestedModelPath))
|
|
return Path.GetFullPath(requestedModelPath);
|
|
if (IsSolidWorksModelPath(reportModelPath))
|
|
return Path.GetFullPath(reportModelPath);
|
|
return Path.GetFullPath(FirstNonEmpty(reportModelPath, requestedModelPath));
|
|
}
|
|
|
|
static bool IsSolidWorksModelPath(string path)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(path))
|
|
return false;
|
|
var extension = Path.GetExtension(path);
|
|
return extension.Equals(".SLDASM", StringComparison.OrdinalIgnoreCase) ||
|
|
extension.Equals(".SLDPRT", StringComparison.OrdinalIgnoreCase);
|
|
}
|
|
|
|
static string ResolveGroupIdFromOutputDirectory(string outputPath, string outputDir, IReadOnlyList<FaceHighlightExportGroup> groups)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(outputPath))
|
|
return "";
|
|
|
|
var relative = Path.GetRelativePath(outputDir, outputPath).Replace('\\', '/');
|
|
foreach (var group in groups)
|
|
{
|
|
var directory = (group.Directory ?? "").Replace('\\', '/').Trim('/');
|
|
if (!string.IsNullOrWhiteSpace(directory) &&
|
|
relative.StartsWith(directory + "/", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
return group.GroupId ?? "";
|
|
}
|
|
}
|
|
|
|
return "";
|
|
}
|
|
|
|
static string FirstNonEmpty(params string?[] values)
|
|
{
|
|
foreach (var value in values)
|
|
{
|
|
if (!string.IsNullOrWhiteSpace(value))
|
|
return value.Trim();
|
|
}
|
|
return "";
|
|
}
|
|
|
|
static string BuildFaceRef(string? instanceName, int faceIndex) =>
|
|
!string.IsNullOrWhiteSpace(instanceName) && faceIndex > 0
|
|
? $"{instanceName}:face#{faceIndex}"
|
|
: "";
|
|
|
|
static string SanitizeFileName(string value)
|
|
{
|
|
var invalid = Path.GetInvalidFileNameChars().ToHashSet();
|
|
var chars = (value ?? "").Select(ch => invalid.Contains(ch) || char.IsControl(ch) ? '_' : ch).ToArray();
|
|
var result = new string(chars).Trim('_', '.', ' ');
|
|
return string.IsNullOrWhiteSpace(result) ? "unnamed" : result;
|
|
}
|
|
|
|
static int ParseFaceIndex(string faceRef)
|
|
{
|
|
var marker = faceRef.LastIndexOf("face#", StringComparison.OrdinalIgnoreCase);
|
|
if (marker < 0)
|
|
return 0;
|
|
var start = marker + "face#".Length;
|
|
var digits = new string(faceRef.Skip(start).TakeWhile(char.IsDigit).ToArray());
|
|
return int.TryParse(digits, NumberStyles.Integer, CultureInfo.InvariantCulture, out var index) ? index : 0;
|
|
}
|
|
|
|
static bool IsSolidWorksProcessAlive()
|
|
{
|
|
try
|
|
{
|
|
return Process.GetProcessesByName("SLDWORKS").Length > 0;
|
|
}
|
|
catch
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
static int GetSolidWorksAttachTimeoutSeconds(bool processAlive)
|
|
{
|
|
var defaultSeconds = processAlive ? 30 : 5;
|
|
var raw = System.Environment.GetEnvironmentVariable("SECTION_BREP_SW_ATTACH_TIMEOUT_SECONDS");
|
|
return int.TryParse(raw, out var configuredSeconds)
|
|
? Math.Clamp(configuredSeconds, 0, 300)
|
|
: defaultSeconds;
|
|
}
|
|
|
|
static ExtractorCommand ParseCommandLine(string[] args)
|
|
{
|
|
string? path = null;
|
|
string? outputDir = null;
|
|
string? imageDir = null;
|
|
string? faceHighlightPlanPath = null;
|
|
string? imageOnlyReportPath = null;
|
|
bool exportImages = false;
|
|
var imageViews = new List<string>();
|
|
var purchasedComponentHints = new List<string>();
|
|
var externalInterfaceRoots = new List<string>();
|
|
double[]? upAxis = null;
|
|
var positional = new List<string>();
|
|
|
|
for (int i = 0; i < args.Length; i++)
|
|
{
|
|
string arg = args[i];
|
|
if (arg.Equals("--output-dir", StringComparison.OrdinalIgnoreCase) && i + 1 < args.Length)
|
|
{
|
|
outputDir = Path.GetFullPath(args[++i]);
|
|
}
|
|
else if (arg.StartsWith("--output-dir=", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
outputDir = Path.GetFullPath(arg["--output-dir=".Length..]);
|
|
}
|
|
else if (arg.Equals("--export-images", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
exportImages = true;
|
|
}
|
|
else if (arg.Equals("--image-dir", StringComparison.OrdinalIgnoreCase) && i + 1 < args.Length)
|
|
{
|
|
imageDir = Path.GetFullPath(args[++i]);
|
|
exportImages = true;
|
|
}
|
|
else if (arg.StartsWith("--image-dir=", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
imageDir = Path.GetFullPath(arg["--image-dir=".Length..]);
|
|
exportImages = true;
|
|
}
|
|
else if (arg.Equals("--image-views", StringComparison.OrdinalIgnoreCase) && i + 1 < args.Length)
|
|
{
|
|
imageViews.AddRange(SplitCsv(args[++i]));
|
|
exportImages = true;
|
|
}
|
|
else if (arg.StartsWith("--image-views=", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
imageViews.AddRange(SplitCsv(arg["--image-views=".Length..]));
|
|
exportImages = true;
|
|
}
|
|
else if (arg.Equals("--face-highlight-plan", StringComparison.OrdinalIgnoreCase) && i + 1 < args.Length)
|
|
{
|
|
faceHighlightPlanPath = Path.GetFullPath(args[++i]);
|
|
exportImages = true;
|
|
}
|
|
else if (arg.StartsWith("--face-highlight-plan=", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
faceHighlightPlanPath = Path.GetFullPath(arg["--face-highlight-plan=".Length..]);
|
|
exportImages = true;
|
|
}
|
|
else if (arg.Equals("--image-only-from-report", StringComparison.OrdinalIgnoreCase) && i + 1 < args.Length)
|
|
{
|
|
imageOnlyReportPath = Path.GetFullPath(args[++i]);
|
|
exportImages = true;
|
|
}
|
|
else if (arg.StartsWith("--image-only-from-report=", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
imageOnlyReportPath = Path.GetFullPath(arg["--image-only-from-report=".Length..]);
|
|
exportImages = true;
|
|
}
|
|
else if (arg.Equals("--purchased-component-hint", StringComparison.OrdinalIgnoreCase) && i + 1 < args.Length)
|
|
{
|
|
purchasedComponentHints.Add(args[++i]);
|
|
}
|
|
else if (arg.StartsWith("--purchased-component-hint=", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
purchasedComponentHints.Add(arg["--purchased-component-hint=".Length..]);
|
|
}
|
|
else if (arg.Equals("--purchased-component-hints", StringComparison.OrdinalIgnoreCase) && i + 1 < args.Length)
|
|
{
|
|
purchasedComponentHints.AddRange(SplitCsv(args[++i]));
|
|
}
|
|
else if (arg.StartsWith("--purchased-component-hints=", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
purchasedComponentHints.AddRange(SplitCsv(arg["--purchased-component-hints=".Length..]));
|
|
}
|
|
else if (arg.Equals("--external-interface-root", StringComparison.OrdinalIgnoreCase) && i + 1 < args.Length)
|
|
{
|
|
externalInterfaceRoots.Add(args[++i]);
|
|
}
|
|
else if (arg.StartsWith("--external-interface-root=", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
externalInterfaceRoots.Add(arg["--external-interface-root=".Length..]);
|
|
}
|
|
else if (arg.Equals("--up-axis", StringComparison.OrdinalIgnoreCase) && i + 3 < args.Length)
|
|
{
|
|
upAxis = Vec.Normalize([double.Parse(args[++i]), double.Parse(args[++i]), double.Parse(args[++i])]);
|
|
}
|
|
else if (arg.StartsWith("--up-axis=", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
var parts = arg["--up-axis=".Length..].Split([',', ';', ' '], StringSplitOptions.RemoveEmptyEntries);
|
|
if (parts.Length != 3)
|
|
throw new ArgumentException("--up-axis requires three numbers.");
|
|
upAxis = Vec.Normalize([double.Parse(parts[0]), double.Parse(parts[1]), double.Parse(parts[2])]);
|
|
}
|
|
else if (arg.StartsWith("--", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
throw new ArgumentException($"Unknown option: {arg}");
|
|
}
|
|
else
|
|
{
|
|
positional.Add(arg);
|
|
}
|
|
}
|
|
|
|
if (positional.Count == 0 && string.IsNullOrWhiteSpace(faceHighlightPlanPath) && string.IsNullOrWhiteSpace(imageOnlyReportPath))
|
|
throw new ArgumentException("Model path is required.");
|
|
|
|
path = positional.Count > 0 ? positional[0] : FirstNonEmpty(imageOnlyReportPath, faceHighlightPlanPath)!;
|
|
var axis = positional.Count >= 4
|
|
? Vec.Normalize([double.Parse(positional[1]), double.Parse(positional[2]), double.Parse(positional[3])])
|
|
: new[] { 0.0, 1.0, 0.0 };
|
|
|
|
return new ExtractorCommand
|
|
{
|
|
Path = path,
|
|
Axis = axis,
|
|
UpAxis = upAxis,
|
|
OutputDir = outputDir,
|
|
ExportImages = exportImages,
|
|
ImageDir = imageDir,
|
|
ImageViews = exportImages
|
|
? (imageViews.Count > 0 ? imageViews.Distinct(StringComparer.OrdinalIgnoreCase).ToList() : ModelImageExporter.DefaultViewNames)
|
|
: imageViews.Distinct(StringComparer.OrdinalIgnoreCase).ToList(),
|
|
FaceHighlightPlanPath = faceHighlightPlanPath,
|
|
ImageOnlyReportPath = imageOnlyReportPath,
|
|
ExportSectionImages = false,
|
|
SectionImageViews = [],
|
|
AutoSectionImages = false,
|
|
SectionOffsetMm = 0,
|
|
SkipSectionBrep = true,
|
|
PurchasedComponentHints = purchasedComponentHints
|
|
.Select(hint => hint.Trim())
|
|
.Where(hint => !string.IsNullOrWhiteSpace(hint))
|
|
.Distinct(StringComparer.OrdinalIgnoreCase)
|
|
.ToList(),
|
|
ExternalInterfaceRoots = externalInterfaceRoots
|
|
.Select(hint => hint.Trim())
|
|
.Where(hint => !string.IsNullOrWhiteSpace(hint))
|
|
.Distinct(StringComparer.OrdinalIgnoreCase)
|
|
.ToList()
|
|
};
|
|
|
|
static IEnumerable<string> SplitCsv(string raw) =>
|
|
raw.Split([',', ';', '\n', '\r'], StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
|
|
}
|
|
|
|
static SectionBrepReport ExtractSectionBrep(
|
|
SldWorks sw,
|
|
string path,
|
|
double[] axis,
|
|
double[]? upAxis,
|
|
bool skipSectionBrep,
|
|
IReadOnlyList<string> purchasedComponentHints,
|
|
IReadOnlyList<string> externalInterfaceRoots,
|
|
Action<string, string> log)
|
|
{
|
|
int errors = 0;
|
|
int warnings = 0;
|
|
int docType = Path.GetExtension(path).Equals(".SLDASM", StringComparison.OrdinalIgnoreCase) ? AssemblyDocType : PartDoc;
|
|
var documentKind = docType == AssemblyDocType ? "assembly" : "part";
|
|
log("open_doc", path);
|
|
var doc = sw.OpenDoc6(path, docType, Silent | ReadOnly, "", ref errors, ref warnings) as ModelDoc2;
|
|
if (doc == null)
|
|
{
|
|
log("open_doc_failed", $"errors={errors}, warnings={warnings}");
|
|
return new SectionBrepReport
|
|
{
|
|
Ok = false,
|
|
Path = path,
|
|
Message = $"OpenDoc6 failed. errors={errors}, warnings={warnings}"
|
|
};
|
|
}
|
|
|
|
log("get_solidworks_utilities", $"errors={errors}, warnings={warnings}");
|
|
dynamic mathUtility = sw.GetMathUtility();
|
|
dynamic modeler = sw.GetModeler();
|
|
log("enumerate_components", documentKind);
|
|
var components = docType == AssemblyDocType && doc is AssemblyDoc assy
|
|
? EnumerateAssemblyComponents(assy, doc, mathUtility, purchasedComponentHints, externalInterfaceRoots, log)
|
|
: EnumeratePartComponents(doc, mathUtility, purchasedComponentHints);
|
|
log("enumerate_components_completed", $"component_count={components.Count}");
|
|
|
|
log("build_axis_groups", $"component_count={components.Count}");
|
|
var axisGroups = BuildAxisGroups(components);
|
|
var bestAxisGroup = axisGroups.OrderByDescending(g => g.ComponentCount).ThenByDescending(g => g.CylinderCount).FirstOrDefault();
|
|
if (bestAxisGroup == null)
|
|
{
|
|
log("build_axis_groups_failed", "no cylindrical axis group found");
|
|
return new SectionBrepReport
|
|
{
|
|
Ok = false,
|
|
Path = path,
|
|
Message = "No cylindrical axis group found."
|
|
};
|
|
}
|
|
|
|
var assemblyFrame = InferAssemblyFrame(components, bestAxisGroup.Axis, upAxis);
|
|
log("build_assembly_face_contacts", $"component_count={components.Count}");
|
|
var assemblyFaceContacts = BuildAssemblyFaceContacts(components, assemblyFrame, log);
|
|
if (externalInterfaceRoots.Count > 0)
|
|
{
|
|
var unfilteredCount = assemblyFaceContacts.Count;
|
|
assemblyFaceContacts = assemblyFaceContacts
|
|
.Where(contact => IsExternalInterfaceTargetRelation(
|
|
contact.ComponentA,
|
|
contact.ComponentPathA,
|
|
contact.ComponentB,
|
|
contact.ComponentPathB,
|
|
externalInterfaceRoots))
|
|
.ToList();
|
|
log("filter_external_interface_contacts", $"before={unfilteredCount}; after={assemblyFaceContacts.Count}; internal_and_unrelated_pairs=excluded");
|
|
}
|
|
var processLookup = BuildContactProcessLookup(assemblyFaceContacts);
|
|
var assemblyFaces = BuildAssemblyFaces(components, assemblyFrame, processLookup);
|
|
var assemblyComponents = BuildAssemblyComponentSummaries(components);
|
|
log("build_assembly_mates", $"component_count={assemblyComponents.Count}");
|
|
var assemblyMateRelations = docType == AssemblyDocType
|
|
? ExtractAssemblyMateRelations(doc, assemblyComponents, log)
|
|
: new List<AssemblyMateRelation>();
|
|
if (externalInterfaceRoots.Count > 0)
|
|
{
|
|
assemblyMateRelations = assemblyMateRelations
|
|
.Where(mate => IsExternalInterfaceTargetRelation(
|
|
mate.ComponentA,
|
|
mate.ComponentPathA,
|
|
mate.ComponentB,
|
|
mate.ComponentPathB,
|
|
externalInterfaceRoots))
|
|
.ToList();
|
|
}
|
|
var featureGraph = BuildMechanicalFeatureGraph(assemblyFaces, assemblyFaceContacts, assemblyFrame);
|
|
var componentBrepSource = docType == AssemblyDocType ? "assembly_component_brep_subset" : "part_brep_direct";
|
|
var componentEvidencePackages = BuildComponentEvidencePackages(assemblyComponents, assemblyFaces, assemblyFaceContacts, assemblyMateRelations, featureGraph, componentBrepSource);
|
|
var partImagePlan = BuildPartImagePlan(assemblyComponents, componentBrepSource);
|
|
var componentContextImagePlan = docType == AssemblyDocType
|
|
? BuildComponentContextImagePlan(assemblyComponents, componentEvidencePackages, assemblyFaceContacts, assemblyMateRelations)
|
|
: [];
|
|
|
|
log("skip_section_workflow", "section B-rep and section images are disabled; only assembly/component B-rep is extracted");
|
|
var sectionBodies = components.Select(component => new SectionBodyEvidence
|
|
{
|
|
ComponentName = component.Name,
|
|
Category = component.Category,
|
|
SectionEntities = []
|
|
}).ToList();
|
|
var report = new SectionBrepReport
|
|
{
|
|
Ok = true,
|
|
Path = path,
|
|
DocumentKind = documentKind,
|
|
Message = documentKind == "part"
|
|
? $"Part B-rep extracted from axis group {bestAxisGroup.GroupId}; part axis-section plan prepared when a cylindrical axis is available."
|
|
: $"Assembly/component B-rep extracted from axis group {bestAxisGroup.GroupId}; assembly section workflow disabled.",
|
|
AxisGroupId = bestAxisGroup.GroupId,
|
|
AssemblyFrame = assemblyFrame,
|
|
SectionPlane = new SectionPlaneEvidence(),
|
|
CanonicalSection = new CanonicalSectionBrep(),
|
|
Canonical2D = new Canonical2DStructureGraph(),
|
|
SketchGraph = new Canonical2DSketchGraph(),
|
|
AssemblyFaces = assemblyFaces,
|
|
AssemblyFaceContacts = assemblyFaceContacts,
|
|
AssemblyMates = assemblyMateRelations,
|
|
FeatureGraph = featureGraph,
|
|
SemanticFusionContract = BuildSemanticFusionContract(),
|
|
AssemblyComponents = assemblyComponents,
|
|
ComponentEvidencePackages = componentEvidencePackages,
|
|
PartImagePlan = partImagePlan,
|
|
ComponentContextImagePlan = componentContextImagePlan,
|
|
ExternalInterfaceRoots = externalInterfaceRoots.ToList(),
|
|
Components = sectionBodies
|
|
};
|
|
if (documentKind == "part")
|
|
report.SectionImagePlan = PartModelProcessing.BuildAxisSectionPlan(report);
|
|
return report;
|
|
}
|
|
|
|
static List<AssemblyComponentSummary> BuildAssemblyComponentSummaries(List<ComponentEvidence> components)
|
|
{
|
|
return components
|
|
.Select((component, index) => new AssemblyComponentSummary
|
|
{
|
|
Id = $"component_{index + 1}",
|
|
InstanceName = component.Name,
|
|
DisplayName = ComponentDisplayName(component.Name, component.Path),
|
|
FileName = string.IsNullOrWhiteSpace(component.Path) ? "" : Path.GetFileName(component.Path),
|
|
Path = component.Path,
|
|
Category = component.Category,
|
|
ReviewScope = ComponentReviewScope(component.Category, component.Name, component.Path),
|
|
BodyCount = component.Bodies.Count,
|
|
FaceCount = component.Faces.Count,
|
|
CylinderFaceCount = component.Cylinders.Count,
|
|
BBoxMm = component.BoundingBox?.ToArrayRounded()
|
|
?? ComponentBox(component)?.ToArrayRounded()
|
|
?? []
|
|
})
|
|
.OrderBy(c => c.InstanceName, StringComparer.OrdinalIgnoreCase)
|
|
.ToList();
|
|
}
|
|
|
|
static List<AssemblyMateRelation> ExtractAssemblyMateRelations(
|
|
ModelDoc2 doc,
|
|
List<AssemblyComponentSummary> components,
|
|
Action<string, string> log)
|
|
{
|
|
var relations = new List<AssemblyMateRelation>();
|
|
var byName = components.ToDictionary(c => c.InstanceName, StringComparer.OrdinalIgnoreCase);
|
|
var seenMates = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
|
var seenRelations = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
|
var scanned = 0;
|
|
var mateFeatures = 0;
|
|
|
|
foreach (var feature in WalkFeatures(Safe(() => doc.FirstFeature() as Feature, null)))
|
|
{
|
|
scanned++;
|
|
object specific = Safe<object>(() => feature.GetSpecificFeature2(), null!);
|
|
if (specific is not Mate2 mate)
|
|
continue;
|
|
|
|
mateFeatures++;
|
|
var mateKey = ComObjectIdentityKey(feature, $"{Safe(() => feature.GetTypeName2(), "")}|{Safe(() => feature.Name, "")}");
|
|
if (!seenMates.Add(mateKey))
|
|
continue;
|
|
|
|
var mateName = Safe(() => feature.Name, "");
|
|
var mateType = Safe(() => mate.Type, 0);
|
|
var alignment = Safe(() => mate.Alignment, 0);
|
|
var mateComponents = new Dictionary<string, AssemblyComponentSummary>(StringComparer.OrdinalIgnoreCase);
|
|
var entityCount = Safe(() => mate.GetMateEntityCount(), 0);
|
|
for (var i = 0; i < entityCount; i++)
|
|
{
|
|
object entityObj = Safe<object>(() => mate.MateEntity(i), null!);
|
|
if (entityObj is not MateEntity2 mateEntity)
|
|
continue;
|
|
|
|
var component = Safe(() => mateEntity.ReferenceComponent as Component2, null);
|
|
if (component == null)
|
|
continue;
|
|
|
|
var componentName = Safe(() => component.Name2, "");
|
|
var componentPath = Safe(() => component.GetPathName(), "");
|
|
var summary = ResolveComponent(components, byName, componentName, componentPath);
|
|
if (summary != null)
|
|
mateComponents[summary.InstanceName] = summary;
|
|
}
|
|
|
|
var linked = mateComponents.Values
|
|
.OrderBy(c => c.InstanceName, StringComparer.OrdinalIgnoreCase)
|
|
.ToList();
|
|
for (var i = 0; i < linked.Count; i++)
|
|
{
|
|
for (var j = i + 1; j < linked.Count; j++)
|
|
{
|
|
var a = linked[i];
|
|
var b = linked[j];
|
|
var relationKey = $"{mateName}|{mateType}|{alignment}|{a.InstanceName}|{b.InstanceName}";
|
|
if (!seenRelations.Add(relationKey))
|
|
continue;
|
|
|
|
relations.Add(new AssemblyMateRelation
|
|
{
|
|
Id = $"assembly_mate_{relations.Count + 1}",
|
|
MateName = mateName,
|
|
MateType = mateType,
|
|
MateTypeName = MateTypeName(mateType),
|
|
Alignment = alignment,
|
|
AlignmentName = MateAlignmentName(alignment),
|
|
ComponentA = a.InstanceName,
|
|
ComponentDisplayNameA = a.DisplayName,
|
|
ComponentPathA = a.Path,
|
|
ComponentFileNameA = a.FileName,
|
|
ComponentB = b.InstanceName,
|
|
ComponentDisplayNameB = b.DisplayName,
|
|
ComponentPathB = b.Path,
|
|
ComponentFileNameB = b.FileName,
|
|
EntityCount = entityCount,
|
|
Evidence = "solidworks_mate"
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
log("build_assembly_mates_completed", $"features={mateFeatures}; relations={relations.Count}; scanned_features={scanned}");
|
|
return relations
|
|
.OrderBy(m => m.MateName, StringComparer.OrdinalIgnoreCase)
|
|
.ThenBy(m => m.ComponentA, StringComparer.OrdinalIgnoreCase)
|
|
.ThenBy(m => m.ComponentB, StringComparer.OrdinalIgnoreCase)
|
|
.ToList();
|
|
}
|
|
|
|
static string ComponentReviewScope(string category, string name, string path)
|
|
{
|
|
if (IsStandardOrPurchasedComponent(category, name, path))
|
|
return "assembly_context_only";
|
|
return "part_design_required";
|
|
}
|
|
|
|
static bool IsStandardOrPurchasedComponent(string category, string name, string path)
|
|
{
|
|
var fileName = Path.GetFileNameWithoutExtension(path);
|
|
var nameAndFile = $"{name} {fileName}".ToLowerInvariant();
|
|
if (ContainsAny(nameAndFile, "\u5c0f\u81c2\u5173\u8282\u88c5\u914d2", "小臂关节装配2"))
|
|
return false;
|
|
if (ContainsAny(nameAndFile, "\u5c0f\u81c2\u5173\u8282\u88c5\u914d2", "小臂关节装配2"))
|
|
return true;
|
|
if (LooksLikeCustomAdapterPart(nameAndFile))
|
|
return false;
|
|
|
|
var s = $"{category} {nameAndFile}".ToLowerInvariant();
|
|
if (category is "bearing" or "purchased_motor" or "purchased_reducer" or "purchased_motor_reducer_assembly" or "user_purchased_component")
|
|
return true;
|
|
return ContainsAny(
|
|
s,
|
|
"gb", "t70", "t93", "t97", "t119", "t894",
|
|
"bearing", "bolt", "screw", "nut", "washer", "pin",
|
|
"motor", "servo", "gearbox", "reducer", "gear motor",
|
|
"\u8f74\u627f", "\u87ba\u9489", "\u87ba\u6813", "\u87ba\u6bcd", "\u57ab\u5708", "\u9500",
|
|
"\u7535\u673a", "\u9a6c\u8fbe", "\u4f3a\u670d", "\u51cf\u901f\u5668", "\u51cf\u901f\u673a", "\u51cf\u901f\u7535\u673a");
|
|
}
|
|
|
|
static bool ShouldSkipDetailedAssemblyBrep(string category, string name, string path)
|
|
{
|
|
var fileName = Path.GetFileNameWithoutExtension(path);
|
|
var nameAndFile = $"{name} {fileName}".ToLowerInvariant();
|
|
if (ContainsAny(nameAndFile, "\u5c0f\u81c2\u5173\u8282\u88c5\u914d2", "小臂关节装配2"))
|
|
return false;
|
|
if (LooksLikeCustomAdapterPart(nameAndFile))
|
|
return false;
|
|
|
|
var s = $"{category} {nameAndFile}".ToLowerInvariant();
|
|
if (category is "bearing" or "purchased_motor" or "purchased_reducer" or "purchased_motor_reducer_assembly" or "user_purchased_component")
|
|
return true;
|
|
return ContainsAny(
|
|
s,
|
|
"bearing", "motor", "servo", "gearbox", "reducer", "gear motor",
|
|
"\u8f74\u627f", "\u7535\u673a", "\u9a6c\u8fbe", "\u4f3a\u670d", "\u51cf\u901f\u5668", "\u51cf\u901f\u673a", "\u51cf\u901f\u7535\u673a");
|
|
}
|
|
|
|
static string ComponentDisplayName(string instanceName, string path)
|
|
{
|
|
var fileStem = string.IsNullOrWhiteSpace(path) ? "" : Path.GetFileNameWithoutExtension(path);
|
|
if (!string.IsNullOrWhiteSpace(fileStem))
|
|
return $"{instanceName} ({fileStem})";
|
|
return instanceName;
|
|
}
|
|
|
|
static List<ComponentEvidencePackage> BuildComponentEvidencePackages(
|
|
List<AssemblyComponentSummary> components,
|
|
List<AssemblyFaceEvidence> faces,
|
|
List<AssemblyFaceContactEvidence> contacts,
|
|
List<AssemblyMateRelation> mates,
|
|
MechanicalFeatureGraph featureGraph,
|
|
string brepSource)
|
|
{
|
|
var packages = new List<ComponentEvidencePackage>();
|
|
|
|
foreach (var component in components)
|
|
{
|
|
var ownedFaces = faces
|
|
.Where(f => SameComponent(f.ComponentName, component.InstanceName, f.ComponentPath, component.Path))
|
|
.OrderBy(f => f.FaceIndex)
|
|
.Select(FaceRef)
|
|
.Distinct(StringComparer.OrdinalIgnoreCase)
|
|
.ToList();
|
|
|
|
var ownedFeatureIds = featureGraph.Features
|
|
.Where(f => SameComponent(f.ComponentName, component.InstanceName, f.ComponentPath, component.Path))
|
|
.Select(f => f.Id)
|
|
.Distinct(StringComparer.OrdinalIgnoreCase)
|
|
.ToList();
|
|
|
|
var ownedContactIds = contacts
|
|
.Where(c =>
|
|
SameComponent(c.ComponentA, component.InstanceName, c.ComponentPathA, component.Path) ||
|
|
SameComponent(c.ComponentB, component.InstanceName, c.ComponentPathB, component.Path))
|
|
.Select(c => c.Id)
|
|
.Distinct(StringComparer.OrdinalIgnoreCase)
|
|
.ToList();
|
|
|
|
var ownedMateIds = mates
|
|
.Where(m =>
|
|
SameComponent(m.ComponentA, component.InstanceName, m.ComponentPathA, component.Path) ||
|
|
SameComponent(m.ComponentB, component.InstanceName, m.ComponentPathB, component.Path))
|
|
.Select(m => m.Id)
|
|
.Distinct(StringComparer.OrdinalIgnoreCase)
|
|
.ToList();
|
|
|
|
packages.Add(new ComponentEvidencePackage
|
|
{
|
|
ComponentId = component.Id,
|
|
InstanceName = component.InstanceName,
|
|
DisplayName = component.DisplayName,
|
|
FileName = component.FileName,
|
|
Path = component.Path,
|
|
Category = component.Category,
|
|
ReviewScope = component.ReviewScope,
|
|
BrepSource = brepSource,
|
|
ImageSource = component.ReviewScope.Equals("part_design_required", StringComparison.OrdinalIgnoreCase)
|
|
? "component_part_file_images_planned_and_assembly_component_highlight_images_planned"
|
|
: "assembly_images_only",
|
|
PartImagePlanId = component.ReviewScope.Equals("part_design_required", StringComparison.OrdinalIgnoreCase)
|
|
? PartImagePlanId(component.Id)
|
|
: "",
|
|
AssemblyContextImagePlanId = component.ReviewScope.Equals("part_design_required", StringComparison.OrdinalIgnoreCase)
|
|
? AssemblyContextImagePlanId(component.Id)
|
|
: "",
|
|
FaceRefs = ownedFaces,
|
|
FeatureIds = ownedFeatureIds,
|
|
ContactIds = ownedContactIds,
|
|
MateIds = ownedMateIds,
|
|
BodyCount = component.BodyCount,
|
|
FaceCount = component.FaceCount,
|
|
CylinderFaceCount = component.CylinderFaceCount,
|
|
BBoxMm = component.BBoxMm,
|
|
Rules =
|
|
[
|
|
"This package is the only B-rep/feature/contact/mate evidence package for this component instance.",
|
|
"Do not merge faces, features, or images from another component unless a relation/contact/mate id explicitly links them.",
|
|
"For part-design diagnosis, use this assembly-derived B-rep subset plus the matching PartImagePlan images."
|
|
]
|
|
});
|
|
}
|
|
|
|
return packages;
|
|
}
|
|
|
|
static List<PartImagePlanItem> BuildPartImagePlan(List<AssemblyComponentSummary> components, string brepSource)
|
|
{
|
|
var isDirectPart = brepSource.Equals(PartModelProcessing.DirectPartBrepSource, StringComparison.OrdinalIgnoreCase);
|
|
return components
|
|
.Where(c => c.ReviewScope.Equals("part_design_required", StringComparison.OrdinalIgnoreCase))
|
|
.Select(c => new PartImagePlanItem
|
|
{
|
|
Id = PartImagePlanId(c.Id),
|
|
ComponentId = c.Id,
|
|
InstanceName = c.InstanceName,
|
|
DisplayName = c.DisplayName,
|
|
ComponentFileName = c.FileName,
|
|
ComponentPath = c.Path,
|
|
BrepSource = brepSource,
|
|
ImageSource = isDirectPart ? "part_file_direct_views" : "component_part_file",
|
|
AssemblyContextImagePlanId = isDirectPart ? "" : AssemblyContextImagePlanId(c.Id),
|
|
AssemblyContextImageSource = isDirectPart ? "" : "assembly_component_highlight_images",
|
|
DoNotReextractBrep = !isDirectPart,
|
|
SuggestedViews = isDirectPart ? [] : ModelImageExporter.DefaultViewNames.ToList(),
|
|
SuggestedAssemblyContextViews = isDirectPart ? [] : ModelImageExporter.DefaultHighlightViewNames.ToList(),
|
|
HighlightInstruction = isDirectPart
|
|
? "SLDPRT input: do not export ordinary whole-part appearance views and do not use assembly hiding/isolation/component highlight. Export feature-group face highlight images tied to FeatureGraph ids first; export one unhighlighted axis section view only as supplemental internal-structure evidence."
|
|
: "When exporting assembly context images, select/highlight this whole component instance, not a feature or face.",
|
|
Purpose = isDirectPart
|
|
?
|
|
[
|
|
"Provide direct visual evidence for single-part diagnosis.",
|
|
"Keep geometry facts tied to the opened SLDPRT B-rep.",
|
|
"Use feature-group highlight images after B-rep feature grouping; each highlighted image must carry the matching feature id and face refs.",
|
|
"Use one unhighlighted axis section view only as supplemental internal/coaxial structure evidence.",
|
|
"Do not export ordinary whole-part appearance views for direct part diagnosis.",
|
|
"Do not use assembly-context hiding, component selection, or component highlight operations."
|
|
]
|
|
:
|
|
[
|
|
"Provide isolated visual evidence for nonstandard part-design diagnosis.",
|
|
"Keep all geometry facts tied to the component B-rep subset extracted from the assembly.",
|
|
"Use highlighted assembly context images to identify this component instance position and function in the assembly.",
|
|
"Use assembly contacts first to infer functional faces, then use part images to inspect local design and manufacturability."
|
|
]
|
|
})
|
|
.OrderBy(i => i.InstanceName, StringComparer.OrdinalIgnoreCase)
|
|
.ToList();
|
|
}
|
|
|
|
static string PartImagePlanId(string componentId) => $"part_image_{SanitizeId(componentId)}";
|
|
static string AssemblyContextImagePlanId(string componentId) => $"assembly_context_highlight_{SanitizeId(componentId)}";
|
|
|
|
static List<ComponentContextImagePlanItem> BuildComponentContextImagePlan(
|
|
List<AssemblyComponentSummary> components,
|
|
List<ComponentEvidencePackage> packages,
|
|
List<AssemblyFaceContactEvidence> contacts,
|
|
List<AssemblyMateRelation> mates)
|
|
{
|
|
var plans = new List<ComponentContextImagePlanItem>();
|
|
var byName = components.ToDictionary(c => c.InstanceName, StringComparer.OrdinalIgnoreCase);
|
|
var contactGraph = BuildComponentContactGraph(components, contacts);
|
|
var mateGraph = BuildComponentMateGraph(components, mates);
|
|
var relationGraph = MergeComponentRelationGraphs(contactGraph, mateGraph);
|
|
var assemblyBox = ComponentSummaryBBox(components);
|
|
var occlusionCache = new Dictionary<string, InternalOcclusionAnalysis>(StringComparer.OrdinalIgnoreCase);
|
|
var groupedInternal = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
|
var targetSeeds = components
|
|
.Where(c => ShouldAnalyzeInternalCandidate(c, assemblyBox))
|
|
.OrderBy(c => c.InstanceName, StringComparer.OrdinalIgnoreCase)
|
|
.ToList();
|
|
|
|
foreach (var seed in targetSeeds)
|
|
{
|
|
if (groupedInternal.Contains(seed.InstanceName))
|
|
continue;
|
|
|
|
var seedAnalysis = GetInternalOcclusion(seed);
|
|
if (!seedAnalysis.IsInternal)
|
|
continue;
|
|
|
|
var internalMembers = new Dictionary<string, ComponentContextMember>(StringComparer.OrdinalIgnoreCase);
|
|
var boundaryMembers = new Dictionary<string, ComponentContextMember>(StringComparer.OrdinalIgnoreCase);
|
|
var memberAnalyses = new Dictionary<string, InternalOcclusionAnalysis>(StringComparer.OrdinalIgnoreCase);
|
|
var queue = new Queue<AssemblyComponentSummary>();
|
|
AddContextMember(internalMembers, seed, "internal_group_seed");
|
|
memberAnalyses[seed.InstanceName] = seedAnalysis;
|
|
queue.Enqueue(seed);
|
|
|
|
while (queue.Count > 0)
|
|
{
|
|
var current = queue.Dequeue();
|
|
if (!relationGraph.TryGetValue(current.InstanceName, out var neighborNames))
|
|
continue;
|
|
|
|
foreach (var neighborName in neighborNames)
|
|
{
|
|
if (!byName.TryGetValue(neighborName, out var other))
|
|
continue;
|
|
if (internalMembers.ContainsKey(other.InstanceName))
|
|
continue;
|
|
|
|
var otherAnalysis = GetInternalOcclusion(other);
|
|
if (otherAnalysis.IsInternal && !IsStandardOrPurchasedContext(other))
|
|
{
|
|
AddContextMember(internalMembers, other, "contact_internal_member");
|
|
memberAnalyses[other.InstanceName] = otherAnalysis;
|
|
queue.Enqueue(other);
|
|
}
|
|
else
|
|
{
|
|
AddContextMember(boundaryMembers, other, "contact_boundary_component");
|
|
}
|
|
}
|
|
}
|
|
|
|
var shown = internalMembers.Values
|
|
.OrderBy(m => m.Role == "internal_group_seed" ? 0 : 1)
|
|
.ThenBy(m => m.InstanceName, StringComparer.OrdinalIgnoreCase)
|
|
.ToList();
|
|
|
|
boundaryMembers.Clear();
|
|
AddDirectRelationBoundaryMembers(boundaryMembers, internalMembers, byName, relationGraph);
|
|
|
|
foreach (var target in shown)
|
|
groupedInternal.Add(target.InstanceName);
|
|
|
|
var groupId = $"internal_context_group_{plans.Count + 1}_{SanitizeId(seed.Id)}";
|
|
var boundaries = boundaryMembers.Values
|
|
.OrderBy(m => m.InstanceName, StringComparer.OrdinalIgnoreCase)
|
|
.ToList();
|
|
var analyses = memberAnalyses.Values
|
|
.OrderBy(a => a.InstanceName, StringComparer.OrdinalIgnoreCase)
|
|
.ToList();
|
|
|
|
foreach (var targetMember in shown)
|
|
{
|
|
if (!byName.TryGetValue(targetMember.InstanceName, out var targetComponent))
|
|
continue;
|
|
|
|
var targetAnalysis = GetInternalOcclusion(targetComponent);
|
|
plans.Add(new ComponentContextImagePlanItem
|
|
{
|
|
Id = $"{groupId}_target_{SanitizeId(targetComponent.Id)}",
|
|
GroupId = groupId,
|
|
TargetComponentId = targetComponent.Id,
|
|
TargetInstanceName = targetComponent.InstanceName,
|
|
TargetDisplayName = targetComponent.DisplayName,
|
|
TargetComponentPath = targetComponent.Path,
|
|
Strategy = "show_internal_component_group_hide_only_occlusion_blockers_and_highlight_target",
|
|
DisplayMode = "targeted_blocker_hiding_with_connected_context",
|
|
OcclusionMethod = "six_axis_bbox_projection_four_extreme_samples_first_hit_blockers",
|
|
RestoreRequired = true,
|
|
SaveModel = false,
|
|
SuggestedViews = ModelImageExporter.DefaultContextViewNames.ToList(),
|
|
ShowComponents = shown,
|
|
InternalMembers = shown,
|
|
BoundaryComponents = boundaries,
|
|
TargetOcclusion = targetAnalysis,
|
|
MemberOcclusionAnalyses = analyses,
|
|
HiddenPolicy = "hide_only_detected_occlusion_blockers_except_direct_contact_or_mate_neighbors",
|
|
Purpose =
|
|
[
|
|
"Expose this internal target component inside its geometrically internal component group.",
|
|
"Highlight the target component after hiding only detected geometric occlusion blockers.",
|
|
"Preserve every direct contact or mate neighbor of the internal component group as assembly-function context, even when it also appears in the blocker list.",
|
|
"Leave unrelated non-blocking components in their original visibility state.",
|
|
"Standard or purchased components are not diagnostic targets, but may appear as occlusion blockers or boundary context.",
|
|
"Use temporary component visibility changes only for image export.",
|
|
"Restore every component's original visibility immediately after each context image is saved.",
|
|
"Never save the SolidWorks model after temporary hiding."
|
|
]
|
|
});
|
|
}
|
|
|
|
InternalOcclusionAnalysis GetInternalOcclusion(AssemblyComponentSummary component)
|
|
{
|
|
if (!occlusionCache.TryGetValue(component.InstanceName, out var analysis))
|
|
{
|
|
analysis = AnalyzeInternalOcclusion(component, components);
|
|
occlusionCache[component.InstanceName] = analysis;
|
|
}
|
|
|
|
return analysis;
|
|
}
|
|
}
|
|
|
|
return plans;
|
|
}
|
|
|
|
static void AddDirectRelationBoundaryMembers(
|
|
Dictionary<string, ComponentContextMember> boundaryMembers,
|
|
Dictionary<string, ComponentContextMember> internalMembers,
|
|
Dictionary<string, AssemblyComponentSummary> byName,
|
|
Dictionary<string, List<string>> relationGraph)
|
|
{
|
|
var internalRoots = internalMembers.Keys
|
|
.OrderBy(name => name, StringComparer.OrdinalIgnoreCase)
|
|
.ToList();
|
|
var internalOrDescendantNames = byName.Keys
|
|
.Where(name => internalRoots.Any(root => IsSameOrChildComponentName(name, root)))
|
|
.OrderBy(name => name, StringComparer.OrdinalIgnoreCase)
|
|
.ToList();
|
|
|
|
foreach (var internalName in internalOrDescendantNames)
|
|
{
|
|
if (!relationGraph.TryGetValue(internalName, out var directNeighborNames))
|
|
continue;
|
|
|
|
foreach (var neighborName in directNeighborNames.OrderBy(name => name, StringComparer.OrdinalIgnoreCase))
|
|
{
|
|
if (!byName.TryGetValue(neighborName, out var neighbor))
|
|
continue;
|
|
if (internalRoots.Any(root => IsSameOrChildComponentName(neighbor.InstanceName, root)))
|
|
continue;
|
|
|
|
AddContextMember(boundaryMembers, neighbor, "direct_contact_or_mate_boundary_component");
|
|
}
|
|
}
|
|
}
|
|
|
|
static bool IsSameOrChildComponentName(string candidateName, string rootName)
|
|
{
|
|
if (candidateName.Equals(rootName, StringComparison.OrdinalIgnoreCase))
|
|
return true;
|
|
return candidateName.StartsWith(rootName + "/", StringComparison.OrdinalIgnoreCase);
|
|
}
|
|
|
|
static bool ShouldAnalyzeInternalCandidate(
|
|
AssemblyComponentSummary component,
|
|
Box? assemblyBox)
|
|
{
|
|
var wholeContextCandidate = IsWholeContextOcclusionCandidate(component);
|
|
if (!component.ReviewScope.Equals("part_design_required", StringComparison.OrdinalIgnoreCase) &&
|
|
!wholeContextCandidate)
|
|
return false;
|
|
if (IsLargeContextConnector(component) || !TryGetSummaryBox(component, out var box))
|
|
return false;
|
|
if (!wholeContextCandidate && assemblyBox.HasValue)
|
|
{
|
|
var assemblyVolume = BoxVolume(assemblyBox.Value);
|
|
var componentVolume = BoxVolume(box);
|
|
if (assemblyVolume > 1e-6 && componentVolume / assemblyVolume > 0.35)
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
static bool IsWholeContextOcclusionCandidate(AssemblyComponentSummary component)
|
|
{
|
|
if (component.Category.Equals("bearing", StringComparison.OrdinalIgnoreCase))
|
|
return true;
|
|
if (component.Category is "purchased_motor" or "purchased_reducer")
|
|
return true;
|
|
if (component.Category.Equals("purchased_motor_reducer_assembly", StringComparison.OrdinalIgnoreCase))
|
|
return !component.InstanceName.Contains('/');
|
|
|
|
return false;
|
|
}
|
|
|
|
static Dictionary<string, List<string>> BuildComponentContactGraph(
|
|
List<AssemblyComponentSummary> components,
|
|
List<AssemblyFaceContactEvidence> contacts)
|
|
{
|
|
var byName = components.ToDictionary(c => c.InstanceName, StringComparer.OrdinalIgnoreCase);
|
|
var graph = components.ToDictionary(
|
|
c => c.InstanceName,
|
|
_ => new HashSet<string>(StringComparer.OrdinalIgnoreCase),
|
|
StringComparer.OrdinalIgnoreCase);
|
|
|
|
foreach (var contact in contacts)
|
|
{
|
|
var a = ResolveComponent(components, byName, contact.ComponentA, contact.ComponentPathA);
|
|
var b = ResolveComponent(components, byName, contact.ComponentB, contact.ComponentPathB);
|
|
if (a == null || b == null || a.InstanceName.Equals(b.InstanceName, StringComparison.OrdinalIgnoreCase))
|
|
continue;
|
|
|
|
graph[a.InstanceName].Add(b.InstanceName);
|
|
graph[b.InstanceName].Add(a.InstanceName);
|
|
}
|
|
|
|
return graph.ToDictionary(
|
|
kvp => kvp.Key,
|
|
kvp => kvp.Value.OrderBy(v => v, StringComparer.OrdinalIgnoreCase).ToList(),
|
|
StringComparer.OrdinalIgnoreCase);
|
|
}
|
|
|
|
static Dictionary<string, List<string>> BuildComponentMateGraph(
|
|
List<AssemblyComponentSummary> components,
|
|
List<AssemblyMateRelation> mates)
|
|
{
|
|
var byName = components.ToDictionary(c => c.InstanceName, StringComparer.OrdinalIgnoreCase);
|
|
var graph = components.ToDictionary(
|
|
c => c.InstanceName,
|
|
_ => new HashSet<string>(StringComparer.OrdinalIgnoreCase),
|
|
StringComparer.OrdinalIgnoreCase);
|
|
|
|
foreach (var mate in mates)
|
|
{
|
|
var a = ResolveComponent(components, byName, mate.ComponentA, mate.ComponentPathA);
|
|
var b = ResolveComponent(components, byName, mate.ComponentB, mate.ComponentPathB);
|
|
if (a == null || b == null || a.InstanceName.Equals(b.InstanceName, StringComparison.OrdinalIgnoreCase))
|
|
continue;
|
|
|
|
graph[a.InstanceName].Add(b.InstanceName);
|
|
graph[b.InstanceName].Add(a.InstanceName);
|
|
}
|
|
|
|
return graph.ToDictionary(
|
|
kvp => kvp.Key,
|
|
kvp => kvp.Value.OrderBy(v => v, StringComparer.OrdinalIgnoreCase).ToList(),
|
|
StringComparer.OrdinalIgnoreCase);
|
|
}
|
|
|
|
static Dictionary<string, List<string>> MergeComponentRelationGraphs(
|
|
params Dictionary<string, List<string>>[] graphs)
|
|
{
|
|
var merged = new Dictionary<string, HashSet<string>>(StringComparer.OrdinalIgnoreCase);
|
|
foreach (var graph in graphs)
|
|
{
|
|
foreach (var (component, neighbors) in graph)
|
|
{
|
|
if (!merged.TryGetValue(component, out var set))
|
|
{
|
|
set = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
|
merged[component] = set;
|
|
}
|
|
|
|
foreach (var neighbor in neighbors)
|
|
set.Add(neighbor);
|
|
}
|
|
}
|
|
|
|
return merged.ToDictionary(
|
|
kvp => kvp.Key,
|
|
kvp => kvp.Value.OrderBy(v => v, StringComparer.OrdinalIgnoreCase).ToList(),
|
|
StringComparer.OrdinalIgnoreCase);
|
|
}
|
|
|
|
static AssemblyComponentSummary? ResolveComponent(
|
|
List<AssemblyComponentSummary> components,
|
|
Dictionary<string, AssemblyComponentSummary> byName,
|
|
string name,
|
|
string path)
|
|
{
|
|
if (byName.TryGetValue(name, out var direct))
|
|
return direct;
|
|
return components.FirstOrDefault(c => SameComponent(c.InstanceName, name, c.Path, path));
|
|
}
|
|
|
|
static InternalOcclusionAnalysis AnalyzeInternalOcclusion(
|
|
AssemblyComponentSummary target,
|
|
List<AssemblyComponentSummary> components)
|
|
{
|
|
var analysis = new InternalOcclusionAnalysis
|
|
{
|
|
ComponentId = target.Id,
|
|
InstanceName = target.InstanceName,
|
|
DisplayName = target.DisplayName,
|
|
Method = "six_axis_bbox_projection_four_extreme_samples_first_hit_blockers",
|
|
Decision = "insufficient_bbox"
|
|
};
|
|
|
|
if (!TryGetSummaryBox(target, out var targetBox))
|
|
return analysis;
|
|
|
|
var occludedCount = 0;
|
|
var openCount = 0;
|
|
foreach (var direction in OcclusionDirections())
|
|
{
|
|
var directionAnalysis = AnalyzeOcclusionDirection(target, targetBox, components, direction);
|
|
analysis.Directions.Add(directionAnalysis);
|
|
if (directionAnalysis.Occluded)
|
|
occludedCount++;
|
|
else
|
|
openCount++;
|
|
|
|
if (occludedCount >= 4)
|
|
{
|
|
analysis.Decision = "occluded_direction_count_reached_4";
|
|
analysis.EarlyStop = true;
|
|
break;
|
|
}
|
|
|
|
if (openCount >= 3)
|
|
{
|
|
analysis.Decision = "open_direction_count_reached_3";
|
|
analysis.EarlyStop = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
analysis.OccludedDirectionCount = occludedCount;
|
|
analysis.OpenDirectionCount = openCount;
|
|
analysis.IsInternal = occludedCount >= 4;
|
|
if (!analysis.EarlyStop)
|
|
analysis.Decision = analysis.IsInternal ? "occluded_direction_count_gt_3" : "occluded_direction_count_not_gt_3";
|
|
return analysis;
|
|
}
|
|
|
|
static InternalOcclusionDirectionAnalysis AnalyzeOcclusionDirection(
|
|
AssemblyComponentSummary target,
|
|
Box targetBox,
|
|
List<AssemblyComponentSummary> components,
|
|
OcclusionDirectionSpec direction)
|
|
{
|
|
var targetProjection = ProjectBoxOnAxes(targetBox, direction.UAxis, direction.VAxis);
|
|
var samples = ExtremeProjectionSamples(targetProjection);
|
|
var blockers = new Dictionary<string, AssemblyComponentSummary>(StringComparer.OrdinalIgnoreCase);
|
|
|
|
foreach (var sample in samples)
|
|
{
|
|
var blocker = FindFirstHitBlocker(target, targetBox, components, direction, sample.U, sample.V);
|
|
if (blocker != null)
|
|
blockers[blocker.InstanceName] = blocker;
|
|
}
|
|
|
|
var blockerRects = blockers.Values
|
|
.Select(c => TryGetSummaryBox(c, out var box)
|
|
? (ProjectionRect?)ProjectBoxOnAxes(box, direction.UAxis, direction.VAxis)
|
|
: null)
|
|
.Where(r => r.HasValue)
|
|
.Select(r => r!.Value)
|
|
.ToList();
|
|
var targetArea = targetProjection.AreaMm2;
|
|
var blockedArea = targetArea <= 1e-9
|
|
? 0.0
|
|
: UnionAreaClipped(blockerRects, targetProjection);
|
|
var ratio = targetArea <= 1e-9 ? 0.0 : blockedArea / targetArea;
|
|
|
|
return new InternalOcclusionDirectionAnalysis
|
|
{
|
|
Direction = direction.Name,
|
|
Occluded = ratio > 0.5,
|
|
OcclusionRatio = Math.Round(ratio, 3),
|
|
FirstHitComponents = blockers.Values
|
|
.OrderBy(c => c.InstanceName, StringComparer.OrdinalIgnoreCase)
|
|
.Select(c => new OcclusionBlocker
|
|
{
|
|
ComponentId = c.Id,
|
|
InstanceName = c.InstanceName,
|
|
DisplayName = c.DisplayName,
|
|
ComponentPath = c.Path
|
|
})
|
|
.ToList()
|
|
};
|
|
}
|
|
|
|
static AssemblyComponentSummary? FindFirstHitBlocker(
|
|
AssemblyComponentSummary target,
|
|
Box targetBox,
|
|
List<AssemblyComponentSummary> components,
|
|
OcclusionDirectionSpec direction,
|
|
double u,
|
|
double v)
|
|
{
|
|
const double toleranceMm = 0.5;
|
|
AssemblyComponentSummary? best = null;
|
|
double bestGap = double.PositiveInfinity;
|
|
|
|
foreach (var candidate in components)
|
|
{
|
|
if (candidate.InstanceName.Equals(target.InstanceName, StringComparison.OrdinalIgnoreCase))
|
|
continue;
|
|
if (!TryGetSummaryBox(candidate, out var candidateBox))
|
|
continue;
|
|
|
|
var candidateProjection = ProjectBoxOnAxes(candidateBox, direction.UAxis, direction.VAxis);
|
|
if (!ProjectionContains(candidateProjection, u, v, toleranceMm))
|
|
continue;
|
|
|
|
var gap = DirectionalOutsideGap(targetBox, candidateBox, direction.Axis, direction.Sign, toleranceMm);
|
|
if (!gap.HasValue)
|
|
continue;
|
|
|
|
var candidateVolume = BoxVolume(candidateBox);
|
|
var bestVolume = best == null || !TryGetSummaryBox(best, out var bestBox) ? double.PositiveInfinity : BoxVolume(bestBox);
|
|
if (gap.Value < bestGap - toleranceMm ||
|
|
(Math.Abs(gap.Value - bestGap) <= toleranceMm && candidateVolume < bestVolume))
|
|
{
|
|
best = candidate;
|
|
bestGap = gap.Value;
|
|
}
|
|
}
|
|
|
|
return best;
|
|
}
|
|
|
|
static IReadOnlyList<OcclusionDirectionSpec> OcclusionDirections() =>
|
|
[
|
|
new("+X", 0, 1.0, 1, 2),
|
|
new("-X", 0, -1.0, 1, 2),
|
|
new("+Y", 1, 1.0, 0, 2),
|
|
new("-Y", 1, -1.0, 0, 2),
|
|
new("+Z", 2, 1.0, 0, 1),
|
|
new("-Z", 2, -1.0, 0, 1)
|
|
];
|
|
|
|
static ProjectionRect ProjectBoxOnAxes(Box box, int uAxis, int vAxis)
|
|
{
|
|
var uMin = BoxAxisMin(box, uAxis);
|
|
var uMax = BoxAxisMax(box, uAxis);
|
|
var vMin = BoxAxisMin(box, vAxis);
|
|
var vMax = BoxAxisMax(box, vAxis);
|
|
return new ProjectionRect(uMin, uMax, vMin, vMax);
|
|
}
|
|
|
|
static List<(double U, double V)> ExtremeProjectionSamples(ProjectionRect rect)
|
|
{
|
|
var uMid = (rect.UMin + rect.UMax) / 2.0;
|
|
var vMid = (rect.VMin + rect.VMax) / 2.0;
|
|
return
|
|
[
|
|
(uMid, rect.VMax),
|
|
(uMid, rect.VMin),
|
|
(rect.UMin, vMid),
|
|
(rect.UMax, vMid)
|
|
];
|
|
}
|
|
|
|
static bool ProjectionContains(ProjectionRect rect, double u, double v, double toleranceMm) =>
|
|
u >= rect.UMin - toleranceMm && u <= rect.UMax + toleranceMm &&
|
|
v >= rect.VMin - toleranceMm && v <= rect.VMax + toleranceMm;
|
|
|
|
static double? DirectionalOutsideGap(Box target, Box candidate, int axis, double sign, double toleranceMm)
|
|
{
|
|
var targetMin = BoxAxisMin(target, axis);
|
|
var targetMax = BoxAxisMax(target, axis);
|
|
var candidateMin = BoxAxisMin(candidate, axis);
|
|
var candidateMax = BoxAxisMax(candidate, axis);
|
|
|
|
if (sign >= 0)
|
|
{
|
|
if (candidateMax <= targetMax + toleranceMm)
|
|
return null;
|
|
return Math.Max(0.0, candidateMin - targetMax);
|
|
}
|
|
|
|
if (candidateMin >= targetMin - toleranceMm)
|
|
return null;
|
|
return Math.Max(0.0, targetMin - candidateMax);
|
|
}
|
|
|
|
static double UnionAreaClipped(List<ProjectionRect> rects, ProjectionRect clip)
|
|
{
|
|
var clipped = rects
|
|
.Select(r => IntersectProjectionRect(r, clip))
|
|
.Where(r => r.HasValue && r.Value.AreaMm2 > 1e-9)
|
|
.Select(r => r!.Value)
|
|
.ToList();
|
|
if (clipped.Count == 0)
|
|
return 0.0;
|
|
|
|
var uBreaks = clipped
|
|
.SelectMany(r => new[] { r.UMin, r.UMax })
|
|
.Distinct()
|
|
.OrderBy(v => v)
|
|
.ToList();
|
|
double area = 0.0;
|
|
for (int i = 0; i < uBreaks.Count - 1; i++)
|
|
{
|
|
var u0 = uBreaks[i];
|
|
var u1 = uBreaks[i + 1];
|
|
if (u1 <= u0)
|
|
continue;
|
|
|
|
var active = clipped
|
|
.Where(r => r.UMin < u1 && r.UMax > u0)
|
|
.Select(r => (r.VMin, r.VMax))
|
|
.OrderBy(r => r.VMin)
|
|
.ToList();
|
|
if (active.Count == 0)
|
|
continue;
|
|
|
|
double coveredV = 0.0;
|
|
var currentMin = active[0].VMin;
|
|
var currentMax = active[0].VMax;
|
|
foreach (var interval in active.Skip(1))
|
|
{
|
|
if (interval.VMin <= currentMax)
|
|
{
|
|
currentMax = Math.Max(currentMax, interval.VMax);
|
|
}
|
|
else
|
|
{
|
|
coveredV += currentMax - currentMin;
|
|
currentMin = interval.VMin;
|
|
currentMax = interval.VMax;
|
|
}
|
|
}
|
|
|
|
coveredV += currentMax - currentMin;
|
|
area += (u1 - u0) * coveredV;
|
|
}
|
|
|
|
return area;
|
|
}
|
|
|
|
static ProjectionRect? IntersectProjectionRect(ProjectionRect a, ProjectionRect b)
|
|
{
|
|
var uMin = Math.Max(a.UMin, b.UMin);
|
|
var uMax = Math.Min(a.UMax, b.UMax);
|
|
var vMin = Math.Max(a.VMin, b.VMin);
|
|
var vMax = Math.Min(a.VMax, b.VMax);
|
|
return uMax <= uMin || vMax <= vMin ? null : new ProjectionRect(uMin, uMax, vMin, vMax);
|
|
}
|
|
|
|
static Box? ComponentSummaryBBox(List<AssemblyComponentSummary> components)
|
|
{
|
|
var boxes = components
|
|
.Where(c => TryGetSummaryBox(c, out _))
|
|
.Select(c =>
|
|
{
|
|
TryGetSummaryBox(c, out var box);
|
|
return box;
|
|
})
|
|
.ToList();
|
|
if (boxes.Count == 0)
|
|
return null;
|
|
return new Box(
|
|
boxes.Min(b => b.XMin),
|
|
boxes.Min(b => b.YMin),
|
|
boxes.Min(b => b.ZMin),
|
|
boxes.Max(b => b.XMax),
|
|
boxes.Max(b => b.YMax),
|
|
boxes.Max(b => b.ZMax));
|
|
}
|
|
|
|
static bool TryGetSummaryBox(AssemblyComponentSummary component, out Box box)
|
|
{
|
|
box = default;
|
|
if (component.BBoxMm.Count() < 6)
|
|
return false;
|
|
box = Box.FromArray(component.BBoxMm.Take(6).ToArray());
|
|
return BoxVolume(box) > 1e-9;
|
|
}
|
|
|
|
static double BoxVolume(Box box) =>
|
|
Math.Max(0.0, box.XMax - box.XMin) *
|
|
Math.Max(0.0, box.YMax - box.YMin) *
|
|
Math.Max(0.0, box.ZMax - box.ZMin);
|
|
|
|
static double BoxAxisMin(Box box, int axis) => axis switch
|
|
{
|
|
0 => box.XMin,
|
|
1 => box.YMin,
|
|
_ => box.ZMin
|
|
};
|
|
|
|
static double BoxAxisMax(Box box, int axis) => axis switch
|
|
{
|
|
0 => box.XMax,
|
|
1 => box.YMax,
|
|
_ => box.ZMax
|
|
};
|
|
|
|
static bool IsStandardOrPurchasedContext(AssemblyComponentSummary component) =>
|
|
component.ReviewScope.Equals("assembly_context_only", StringComparison.OrdinalIgnoreCase) ||
|
|
IsStandardOrPurchasedComponent(component.Category, component.InstanceName, component.Path);
|
|
|
|
static void AddContextMember(Dictionary<string, ComponentContextMember> members, AssemblyComponentSummary component, string role)
|
|
{
|
|
members[component.InstanceName] = new ComponentContextMember
|
|
{
|
|
ComponentId = component.Id,
|
|
InstanceName = component.InstanceName,
|
|
DisplayName = component.DisplayName,
|
|
ComponentPath = component.Path,
|
|
Role = role
|
|
};
|
|
}
|
|
|
|
static bool IsLargeContextConnector(AssemblyComponentSummary component)
|
|
{
|
|
return component.Category is "housing" or "inspection_cover";
|
|
}
|
|
|
|
static bool SameComponent(string nameA, string nameB, string pathA, string pathB)
|
|
{
|
|
if (!string.IsNullOrWhiteSpace(pathA) &&
|
|
!string.IsNullOrWhiteSpace(pathB) &&
|
|
pathA.Equals(pathB, StringComparison.OrdinalIgnoreCase) &&
|
|
nameA.Equals(nameB, StringComparison.OrdinalIgnoreCase))
|
|
return true;
|
|
|
|
return nameA.Equals(nameB, StringComparison.OrdinalIgnoreCase);
|
|
}
|
|
|
|
static MechanicalFeatureGraph BuildMechanicalFeatureGraph(
|
|
List<AssemblyFaceEvidence> faces,
|
|
List<AssemblyFaceContactEvidence> contacts,
|
|
AssemblyFrameEvidence frame)
|
|
{
|
|
var graph = new MechanicalFeatureGraph();
|
|
var cylinders = faces
|
|
.Where(f => f.FaceKind.Equals("cylinder", StringComparison.OrdinalIgnoreCase) && f.RadiusMm > 0 && f.Axis.Length >= 3)
|
|
.ToList();
|
|
var planes = faces
|
|
.Where(f => f.FaceKind.Equals("plane", StringComparison.OrdinalIgnoreCase))
|
|
.ToList();
|
|
|
|
var maxRadius = cylinders.Count == 0 ? 0 : cylinders.Max(f => f.RadiusMm);
|
|
var holeLimit = Math.Max(12.0, maxRadius * 0.35);
|
|
|
|
int index = 0;
|
|
foreach (var holeGroup in BuildHoleFeatureGroups(cylinders.Where(f => f.RadiusMm <= holeLimit).ToList(), frame))
|
|
{
|
|
index++;
|
|
var members = holeGroup.Members.OrderBy(f => f.FaceIndex).ToList();
|
|
var representative = members.OrderByDescending(f => f.AreaMm2).First();
|
|
var entryCandidates = members
|
|
.SelectMany(m => FindHoleEntryFaceCandidates(m, planes))
|
|
.GroupBy(FaceRef, StringComparer.OrdinalIgnoreCase)
|
|
.Select(g => g.First())
|
|
.Take(8)
|
|
.ToList();
|
|
var geometry = new Dictionary<string, object?>
|
|
{
|
|
["radius_mm"] = Math.Round(representative.RadiusMm, 4),
|
|
["diameter_mm"] = Math.Round(representative.RadiusMm * 2.0, 4),
|
|
["axis"] = Vec.Round(representative.Axis),
|
|
["center_points_mm"] = members.Select(m => Vec.Round(m.CenterMm)).ToList(),
|
|
["entry_face_candidate_refs"] = entryCandidates.Select(FaceRef).ToList(),
|
|
["member_count"] = members.Count,
|
|
["pattern_type"] = holeGroup.PatternType,
|
|
["grouping_basis"] = holeGroup.GroupingBasis
|
|
};
|
|
if (holeGroup.PatternNormal.Length >= 3)
|
|
geometry["pattern_plane_normal"] = Vec.Round(holeGroup.PatternNormal);
|
|
if (holeGroup.PatternPlaneTmm.HasValue)
|
|
geometry["pattern_plane_t_mm"] = Math.Round(holeGroup.PatternPlaneTmm.Value, 3);
|
|
if (holeGroup.PatternCenterMm.Length >= 3)
|
|
geometry["pattern_center_mm"] = Vec.Round(holeGroup.PatternCenterMm);
|
|
if (holeGroup.PatternRadiusMm > 0)
|
|
geometry["pattern_radius_mm"] = Math.Round(holeGroup.PatternRadiusMm, 3);
|
|
if (holeGroup.SpacingMm > 0)
|
|
geometry["spacing_mm"] = Math.Round(holeGroup.SpacingMm, 3);
|
|
|
|
graph.Features.Add(new MechanicalFeature
|
|
{
|
|
Id = $"hole_group_{index}",
|
|
Type = members.Count > 1 ? "hole_group" : "hole_feature",
|
|
ComponentName = representative.ComponentName,
|
|
ComponentDisplayName = representative.ComponentDisplayName,
|
|
ComponentPath = representative.ComponentPath,
|
|
ComponentFileName = representative.ComponentFileName,
|
|
FaceRefs = members.Select(FaceRef).ToList(),
|
|
Confidence = 0.74,
|
|
Source = "brep_cylinder_grouping",
|
|
PossibleFunctions = ["fastener_hole_candidate", "port_or_pin_hole_candidate"],
|
|
VisualObservationNeeds =
|
|
[
|
|
"confirm_hole_entry_location_in_model_images",
|
|
"confirm_whether_entry_has_flat_pad_or_spotface",
|
|
"confirm_whether_hole_is_internal_external_or_edge_near"
|
|
],
|
|
RequiredBrepChecks =
|
|
[
|
|
"entry_face_normal_vs_hole_axis_angle",
|
|
"hole_throughness_or_blind_depth",
|
|
"minimum_edge_clearance_to_neighbor_boundaries",
|
|
"spotface_or_counterbore_detection",
|
|
"hole_pattern_grouping_by_center_parameters"
|
|
],
|
|
Geometry = geometry
|
|
});
|
|
}
|
|
|
|
int cylIndex = 0;
|
|
foreach (var group in cylinders
|
|
.Where(f => f.RadiusMm > holeLimit)
|
|
.GroupBy(f => $"{f.ComponentName}:{AxisKey(f.Axis)}:{Math.Round(f.RadiusMm, 1)}", StringComparer.OrdinalIgnoreCase)
|
|
.OrderByDescending(g => g.Sum(f => f.AreaMm2)))
|
|
{
|
|
cylIndex++;
|
|
var members = group.OrderBy(f => f.FaceIndex).ToList();
|
|
var representative = members.OrderByDescending(f => f.AreaMm2).First();
|
|
bool shaftAxis = IsParallel(representative.Axis, frame.ShaftAxisMm, 0.85);
|
|
graph.Features.Add(new MechanicalFeature
|
|
{
|
|
Id = $"cylindrical_surface_group_{cylIndex}",
|
|
Type = shaftAxis ? "main_axis_cylindrical_surface_group" : "cylindrical_surface_group",
|
|
ComponentName = representative.ComponentName,
|
|
ComponentDisplayName = representative.ComponentDisplayName,
|
|
ComponentPath = representative.ComponentPath,
|
|
ComponentFileName = representative.ComponentFileName,
|
|
FaceRefs = members.Select(FaceRef).ToList(),
|
|
Confidence = 0.7,
|
|
Source = "brep_cylinder_grouping",
|
|
PossibleFunctions = shaftAxis
|
|
? ["bearing_or_hinge_seat_candidate", "internal_cylindrical_surface_candidate", "shell_arc_candidate"]
|
|
: ["outer_shell_candidate", "boss_or_side_cylindrical_surface_candidate"],
|
|
VisualObservationNeeds =
|
|
[
|
|
"confirm_internal_or_external_side_from_images",
|
|
"confirm_if_surface_looks_like_precision_seat_or_cast_shell"
|
|
],
|
|
RequiredBrepChecks =
|
|
[
|
|
"internal_external_classification",
|
|
"tool_access_direction_and_surrounding_wall_clearance",
|
|
"surface_continuity_and_interruption"
|
|
],
|
|
Geometry =
|
|
{
|
|
["radius_mm"] = Math.Round(representative.RadiusMm, 4),
|
|
["diameter_mm"] = Math.Round(representative.RadiusMm * 2.0, 4),
|
|
["axis"] = Vec.Round(representative.Axis),
|
|
["axis_point_mm"] = Vec.Round(representative.AxisPointMm),
|
|
["total_area_mm2"] = Math.Round(members.Sum(m => m.AreaMm2), 3),
|
|
["member_count"] = members.Count
|
|
}
|
|
});
|
|
}
|
|
|
|
int planeIndex = 0;
|
|
foreach (var plane in planes.OrderByDescending(f => f.AreaMm2).Take(10))
|
|
{
|
|
planeIndex++;
|
|
graph.Features.Add(new MechanicalFeature
|
|
{
|
|
Id = $"planar_surface_candidate_{planeIndex}",
|
|
Type = "planar_pad_step_or_face_candidate",
|
|
ComponentName = plane.ComponentName,
|
|
ComponentDisplayName = plane.ComponentDisplayName,
|
|
ComponentPath = plane.ComponentPath,
|
|
ComponentFileName = plane.ComponentFileName,
|
|
FaceRefs = [FaceRef(plane)],
|
|
Confidence = 0.62,
|
|
Source = "brep_plane_face",
|
|
PossibleFunctions = ["mounting_pad_candidate", "step_face_candidate", "datum_or_machined_face_candidate"],
|
|
VisualObservationNeeds =
|
|
[
|
|
"confirm_whether_face_is_visible_as_real_plane_or_projection",
|
|
"confirm_if_face_contacts_other_parts_or appears as mounting pad"
|
|
],
|
|
RequiredBrepChecks =
|
|
[
|
|
"adjacency_to_nonmachined_regions",
|
|
"contact_or_fit_evidence",
|
|
"local_height_difference_to_surrounding_faces"
|
|
],
|
|
Geometry =
|
|
{
|
|
["area_mm2"] = Math.Round(plane.AreaMm2, 3),
|
|
["normal"] = Vec.Round(plane.Normal),
|
|
["center_mm"] = Vec.Round(plane.CenterMm),
|
|
["bbox_mm"] = Vec.Round(plane.BBoxMm)
|
|
}
|
|
});
|
|
}
|
|
|
|
int complexIndex = 0;
|
|
foreach (var face in faces
|
|
.Where(f => !f.FaceKind.Equals("plane", StringComparison.OrdinalIgnoreCase) &&
|
|
!f.FaceKind.Equals("cylinder", StringComparison.OrdinalIgnoreCase))
|
|
.Where(f => f.AreaMm2 >= 5.0)
|
|
.OrderByDescending(f => f.AreaMm2)
|
|
.Take(24))
|
|
{
|
|
complexIndex++;
|
|
graph.Features.Add(new MechanicalFeature
|
|
{
|
|
Id = $"complex_unknown_surface_candidate_{complexIndex}",
|
|
Type = "complex_unknown_surface_candidate",
|
|
ComponentName = face.ComponentName,
|
|
ComponentDisplayName = face.ComponentDisplayName,
|
|
ComponentPath = face.ComponentPath,
|
|
ComponentFileName = face.ComponentFileName,
|
|
FaceRefs = [FaceRef(face)],
|
|
Confidence = 0.58,
|
|
Source = "brep_other_face",
|
|
PossibleFunctions =
|
|
[
|
|
"transition_surface_candidate",
|
|
"partial_bore_or_non_cylindrical_hole_boundary_candidate",
|
|
"machining_risk_surface_candidate"
|
|
],
|
|
VisualObservationNeeds =
|
|
[
|
|
"highlight_face_to_identify_real_surface_shape",
|
|
"confirm_if_surface_is_chamfer_fillet_cone_partial_bore_or_freeform_transition",
|
|
"confirm_tool_access_and_adjacent_boundary_from_best_view"
|
|
],
|
|
RequiredBrepChecks =
|
|
[
|
|
"unknown_surface_visual_grounding",
|
|
"edge_loop_or_arc_boundary_extraction_needed",
|
|
"adjacent_surface_transition_and_tool_access_check"
|
|
],
|
|
Geometry =
|
|
{
|
|
["face_kind"] = face.FaceKind,
|
|
["area_mm2"] = Math.Round(face.AreaMm2, 3),
|
|
["center_mm"] = Vec.Round(face.CenterMm),
|
|
["bbox_mm"] = Vec.Round(face.BBoxMm),
|
|
["orientation_role"] = face.OrientationRole,
|
|
["functional_role"] = face.FunctionalRole,
|
|
["surface_process_role"] = face.SurfaceProcessRole
|
|
}
|
|
});
|
|
}
|
|
|
|
int contactIndex = 0;
|
|
foreach (var contact in contacts)
|
|
{
|
|
contactIndex++;
|
|
graph.Features.Add(new MechanicalFeature
|
|
{
|
|
Id = $"assembly_contact_feature_{contactIndex}",
|
|
Type = contact.ContactKind,
|
|
ComponentName = $"{contact.ComponentA}|{contact.ComponentB}",
|
|
ComponentDisplayName = $"{contact.ComponentDisplayNameA}|{contact.ComponentDisplayNameB}",
|
|
ComponentPath = $"{contact.ComponentPathA}|{contact.ComponentPathB}",
|
|
ComponentFileName = $"{contact.ComponentFileNameA}|{contact.ComponentFileNameB}",
|
|
FaceRefs =
|
|
[
|
|
$"{contact.ComponentA}:face#{contact.FaceA}",
|
|
$"{contact.ComponentB}:face#{contact.FaceB}"
|
|
],
|
|
Confidence = contact.Confidence,
|
|
Source = "brep_assembly_face_contact",
|
|
PossibleFunctions = ["fit_contact_candidate", "locating_or_support_contact_candidate"],
|
|
VisualObservationNeeds =
|
|
[
|
|
"confirm_external_accessibility_around_contact",
|
|
"confirm_relation_context_in_standard_or_section_images"
|
|
],
|
|
RequiredBrepChecks =
|
|
[
|
|
"clearance_or_gap_near_contact",
|
|
"contact_overlap_area",
|
|
"coaxiality_or_face_parallelism"
|
|
],
|
|
Geometry =
|
|
{
|
|
["gap_mm"] = contact.GapMm,
|
|
["mate_role"] = contact.MateRole,
|
|
["component_a"] = contact.ComponentA,
|
|
["component_b"] = contact.ComponentB,
|
|
["component_a_file"] = contact.ComponentFileNameA,
|
|
["component_b_file"] = contact.ComponentFileNameB,
|
|
["center_a_mm"] = Vec.Round(contact.CenterA),
|
|
["center_b_mm"] = Vec.Round(contact.CenterB),
|
|
["axis_a"] = Vec.Round(contact.AxisA),
|
|
["axis_b"] = Vec.Round(contact.AxisB)
|
|
}
|
|
});
|
|
}
|
|
|
|
graph.FusionGuidance =
|
|
[
|
|
"Image observations should propose global layout, accessibility, internal/external side, and mechanical intent.",
|
|
"B-rep features should confirm face type, radius, axis, area, centers, candidate entry faces, and contact facts.",
|
|
"A diagnosis should cite both image observations and feature ids when both are available.",
|
|
"If image and B-rep evidence suggest different meanings, keep the issue as uncertain and request an additional standard/context image or computed B-rep check."
|
|
];
|
|
|
|
return graph;
|
|
}
|
|
|
|
static SemanticFusionContract BuildSemanticFusionContract()
|
|
{
|
|
return new SemanticFusionContract
|
|
{
|
|
Schema = "mechanical_semantic_fusion_contract_v2",
|
|
Pipeline =
|
|
[
|
|
"Stage 1: extract assembly B-rep once. For an assembly, every component's B-rep facts must come from its assembly component instance, including assembly transforms and contacts.",
|
|
"Stage 2: build ComponentEvidencePackages. Keep each component's faces, features, contacts, and planned images separated by SolidWorks instance name and file path.",
|
|
"Stage 3: export baseline assembly images, component highlight images, component part-file images, physical-interface/contact highlight images, and internal component group context images. Section images are disabled.",
|
|
"Stage 4: VLM analyzes physical-interface/contact highlight images together with B-rep contacts, SolidWorks mates, adjacent component context, and component function profiles. Export standalone per-interface contact function descriptions under diagnostic_context/interface_function_descriptions.json.",
|
|
"Stage 5: AI first builds assembly functional context: infer each component's role in the assembly from SolidWorks mates, B-rep face contacts, physical-interface image conclusions, coaxial/flush/parallel relations, component positions, and images. Each component-function conclusion must preserve the part-to-part mating/contact relations that support it.",
|
|
"Stage 6: AI then diagnoses assembly-level issues: contacts, fits, locating, disassembly, interference, accessibility, support, sealing, lubrication, and motion relations.",
|
|
"Stage 7: for every component with review_scope=part_design_required, export or use its part-file images according to PartImagePlan, but do not re-extract or mix part B-rep. Diagnose part-design issues using the assembly-derived component B-rep subset plus that component's images and the inherited assembly functional context for that component.",
|
|
"Stage 8: components with review_scope=assembly_context_only are standard or purchased context, including bearings, fasteners, motors, servos, reducers, and gearboxes. Do not run nonstandard part-structure taboo checks on them unless the task is standard-part selection/calculation; keep them as identified purchased components, occlusion blockers, contact boundaries, and assembly-function context.",
|
|
"Stage 9: for geometrically internal nonstandard components, use ComponentContextImagePlan internal groups. These groups are generated before AI function understanding by six-axis BBox occlusion analysis and contact/mate relation expansion; context images hide only detected blockers, preserve direct contact/mate neighbors, and leave unrelated non-blockers unchanged.",
|
|
"Stage 10: AI generates a knowledge-blind global mechanical semantic graph: parts, feature groups, contacts, fits, surface roles, manufacturing candidates, function candidates, component_function_profiles, interface_function_profiles, and uncertainty. Always preserve SolidWorks component names and file names.",
|
|
"Stage 11: AI generates LocalSemanticUnit objects from the global semantic graph. Split units by coverage anchors: part, feature, interface, and functional_group. Every part-level or feature-level unit for an assembly component must carry the inherited assembly_function_context for that component in fact_bundle.",
|
|
"Stage 12: AI writes semantic_coverage_audit to prove which part/design components, feature anchors, interface relations, and functional groups are covered or intentionally excluded.",
|
|
"Stage 13: retrieve knowledge rules by LocalSemanticUnit.primary_view/secondary_views and retrieval_sentence. Do not attach rule ids before retrieval.",
|
|
"Stage 14: load detailed verification_rule only for retrieved candidates and judge with LocalSemanticUnit.fact_bundle plus global semantic graph evidence.",
|
|
"Stage 15: if a candidate rule cannot be verified because extractable facts are missing, request additional standard/context images or computed B-rep checks instead of guessing."
|
|
],
|
|
FeatureTypes =
|
|
[
|
|
"hole_feature",
|
|
"hole_group",
|
|
"main_axis_cylindrical_surface_group",
|
|
"cylindrical_surface_group",
|
|
"planar_pad_step_or_face_candidate",
|
|
"assembly_contact_feature"
|
|
],
|
|
SemanticTypes =
|
|
[
|
|
new SemanticTypeSpec
|
|
{
|
|
Name = "part_identity",
|
|
AppliesTo = "part_or_component",
|
|
Purpose = "Identify likely part class and role, such as housing, shell, cover, bracket, sleeve, shaft, or support.",
|
|
TypicalTargets = ["component"]
|
|
},
|
|
new SemanticTypeSpec
|
|
{
|
|
Name = "structural_semantic",
|
|
AppliesTo = "feature",
|
|
Purpose = "Describe what the feature physically is: hole group, internal cylindrical seat, shell wall, boss, pad, step, rib, cavity, groove, datum face.",
|
|
TypicalTargets = ["hole_group", "cylindrical_surface_group", "planar_pad_step_or_face_candidate"]
|
|
},
|
|
new SemanticTypeSpec
|
|
{
|
|
Name = "functional_semantic",
|
|
AppliesTo = "feature_or_relation",
|
|
Purpose = "Describe likely mechanical function: locating, supporting, fastening, bearing/hinge seating, sealing, guiding, avoiding, accessing, or assembly positioning.",
|
|
TypicalTargets = ["hole_group", "main_axis_cylindrical_surface_group", "assembly_contact_feature"]
|
|
},
|
|
new SemanticTypeSpec
|
|
{
|
|
Name = "manufacturing_semantic",
|
|
AppliesTo = "feature",
|
|
Purpose = "Describe manufacturing interpretation: machined face candidate, unmachined candidate, drilling entry check, internal surface machining, multi-setup candidate, tool access candidate.",
|
|
TypicalTargets = ["hole_group", "main_axis_cylindrical_surface_group", "planar_pad_step_or_face_candidate"]
|
|
},
|
|
new SemanticTypeSpec
|
|
{
|
|
Name = "assembly_semantic",
|
|
AppliesTo = "feature_or_component_relation",
|
|
Purpose = "Describe contact, fit, coaxiality, locating, support, clearance, disassembly path, or external accessibility.",
|
|
TypicalTargets = ["assembly_contact_feature", "hole_group", "cylindrical_surface_group"]
|
|
},
|
|
new SemanticTypeSpec
|
|
{
|
|
Name = "risk_semantic",
|
|
AppliesTo = "feature_or_local_subgraph",
|
|
Purpose = "Describe possible design/manufacturing risk only as an uncertainty or candidate, without final judgement.",
|
|
TypicalTargets = ["internal_surface_machining_candidate", "hole_entry_risk_candidate", "tool_access_blocked_candidate", "multi_setup_candidate"]
|
|
}
|
|
],
|
|
RetrievalViews =
|
|
[
|
|
new RetrievalViewSpec { Id = "system_function_condition", Name = "整机功能与使用条件视角", Focus = "整体功能、工况、输入输出、总体布局和方案约束。" },
|
|
new RetrievalViewSpec { Id = "part_local_shape", Name = "零件局部形状视角", Focus = "局部几何、壁厚、圆角、倒角、开口、尖角和复杂区域。" },
|
|
new RetrievalViewSpec { Id = "surface_role_boundary", Name = "表面角色与边界视角", Focus = "加工面、非加工面、接触面、定位面、密封面和边界分离。" },
|
|
new RetrievalViewSpec { Id = "feature_pattern", Name = "孔槽凸台筋板等特征视角", Focus = "孔、槽、凸台、凹台、筋板、台阶、止口和沟槽等特征组合。" },
|
|
new RetrievalViewSpec { Id = "assembly_contact_fit", Name = "装配接触与配合视角", Focus = "零件之间的接触、配合、间隙、同轴、支承和相对位置。" },
|
|
new RetrievalViewSpec { Id = "locating_limit_datum", Name = "定位限位与基准视角", Focus = "定位、限位、基准、测量基面、对中面、装夹面和自由度约束。" },
|
|
new RetrievalViewSpec { Id = "disassembly_maintenance_access", Name = "拆装维修与可达性视角", Focus = "装配路径、拆卸方向、工具伸入空间、外部可达性和遮挡。" },
|
|
new RetrievalViewSpec { Id = "tool_path_access", Name = "刀具路径与加工可达性视角", Focus = "进刀、退刀、钻削入口、封闭槽、深腔、遮挡壁、刀具半径和加工振动。" },
|
|
new RetrievalViewSpec { Id = "operation_process_economy", Name = "加工工序与工艺经济性视角", Focus = "一次走刀、批量加工、换刀数量、多次装夹、组合加工和工序合并。" },
|
|
new RetrievalViewSpec { Id = "blank_forming_material", Name = "毛坯成形与材料工艺视角", Focus = "铸造、锻造、焊接、冲压、机加工毛坯、成形误差和余量。" },
|
|
new RetrievalViewSpec { Id = "load_strength_stiffness", Name = "载荷强度刚度与应力视角", Focus = "受力路径、强度、刚度、疲劳、应力集中、薄弱截面和变形。" },
|
|
new RetrievalViewSpec { Id = "motion_interference_dof", Name = "运动关系干涉与自由度视角", Focus = "运动副、自由度、相对运动、碰撞、干涉、行程和运动空间。" },
|
|
new RetrievalViewSpec { Id = "transmission_standard_parameters", Name = "传动关系与标准件参数视角", Focus = "轴承、齿轮、联轴器、键、螺栓、弹簧等标准件或传动件参数。" },
|
|
new RetrievalViewSpec { Id = "lubrication_sealing_flow", Name = "润滑密封排水排气与流动路径视角", Focus = "润滑路径、油孔、油槽、密封面、排水、排气、泄漏和流体通道。" },
|
|
new RetrievalViewSpec { Id = "operation_safety_environment", Name = "操作安全环境与维护视角", Focus = "操作空间、防护、人机、散热、腐蚀、噪声、清理和维护。" },
|
|
new RetrievalViewSpec { Id = "drawing_annotation_requirement", Name = "图纸标注尺寸公差与技术要求视角", Focus = "尺寸链、公差、形位公差、粗糙度、技术要求和图纸一致性。" }
|
|
],
|
|
GlobalMechanicalSemanticGraphSchema = new Dictionary<string, object?>
|
|
{
|
|
["parts"] = new[] { "part id, solidworks_instance_name, component_file_name, component_path, category, review_scope, inferred_role, material or blank candidate, confidence, evidence refs" },
|
|
["features"] = new[] { "feature id/type/owner part/face refs/geometry summary/possible functions/possible process roles" },
|
|
["relations"] = new[] { "contact/fit/coaxial/adjacent/flush/blocked/access/path/located_by/supports relations with evidence" },
|
|
["surface_roles"] = new[] { "machined candidate, unmachined candidate, contact surface, datum candidate, sealing surface, exterior/interior side" },
|
|
["component_function_profiles"] = new[] { "one profile per component: whole-component purpose only, solidworks instance, assembly role, function inputs/outputs, supported or supporting components, load/motion/sealing/locating responsibilities, related interface ids, confidence, evidence refs; do not place per-contact function conclusions here" },
|
|
["interface_function_profiles"] = new[] { "one profile per physical interface location: relation/contact ids, participating faces/components, B-rep contact type, locating/supporting/sealing/guiding/fastening/motion limiting/load transfer function, relative motion intent, axial/circumferential/radial constraint responsibilities, confidence, evidence refs; do not repeat the whole-component purpose" },
|
|
["connection_assessments"] = new[] { "one assessment per physical interface: intended fixed/sliding/rotating relation, torque/load transfer, disassembly intent, observed fastener/key/spline/weld/adhesive/clamp/shoulder/retaining-ring evidence, other positive-retention status, interference-fit necessity candidate, reasoning, confidence, alternatives and missing facts" },
|
|
["interface_drawing_requirements"] = new[] { "one requirement profile per physical interface: fit intent/category without inventing an exact ISO fit, tolerance-critical dimensions, geometric-tolerance intent, surface-finish intent for each participating face, drawing requirement confidence and evidence refs" },
|
|
["uncertainties"] = new[] { "facts that cannot be inferred reliably and suggested extraction/check" },
|
|
["component_evidence_packages"] = new[] { "one package per SolidWorks component instance; assembly-derived B-rep refs and matching image plan refs must not be mixed across components" },
|
|
["diagnosis_order"] = new[] { "assembly_level_first", "nonstandard_part_design_second" },
|
|
["evidence_refs"] = new[] { "B-rep feature ids, face refs, image names, component context image names" }
|
|
},
|
|
AssertionSchema = new Dictionary<string, object?>
|
|
{
|
|
["semantic_id"] = "stable id, such as manufacturing_semantic_1",
|
|
["semantic_type"] = "one of SemanticTypes.Name",
|
|
["target_feature_ids"] = new[] { "FeatureGraph feature ids" },
|
|
["statement"] = "short neutral mechanical semantic statement, not a final fault judgement",
|
|
["image_evidence"] = new[] { "view/image path plus what is seen" },
|
|
["image_evidence_names"] = new[] { "exact image names or plan ids used by this semantic assertion" },
|
|
["brep_evidence"] = new[] { "feature ids, face refs, measured radius/axis/area/contact facts" },
|
|
["confidence"] = "0.0-1.0",
|
|
["status"] = "confirmed | likely | candidate | uncertain | contradicted",
|
|
["missing_checks"] = new[] { "computed B-rep checks or visual checks still needed" }
|
|
},
|
|
LocalSemanticUnitSchema = new Dictionary<string, object?>
|
|
{
|
|
["local_id"] = "stable id, such as local_001",
|
|
["anchor_type"] = "part | feature | interface | functional_group",
|
|
["anchor_id"] = "component id, feature id, contact/mate relation id, or internal/component group id",
|
|
["primary_view"] = "one RetrievalViews.Id",
|
|
["secondary_views"] = new[] { "optional RetrievalViews.Id values" },
|
|
["retrieval_sentence"] = "one neutral fact sentence for retrieval; no rule id, no should/violate/fault wording",
|
|
["image_evidence_names"] = new[] { "exact component context, component highlight, or part image names that support this local unit" },
|
|
["image_observations"] = new[] { "image_name/observation/supports/confidence records" },
|
|
["fact_bundle"] = "BrepFactBundleSchema",
|
|
["source_semantic_ids"] = new[] { "global semantic assertion ids used to create this unit" },
|
|
["dedup_key"] = "primary_view + anchor id + core relation signature"
|
|
},
|
|
CoverageAuditSchema = new Dictionary<string, object?>
|
|
{
|
|
["covered_part_component_ids"] = new[] { "part_design_required component instance ids with at least one part-level local unit" },
|
|
["covered_feature_anchor_ids"] = new[] { "hole/hole_group/slot/boss/rib/step/shaft/sleeve/pad/thin_wall/machining/locating/contact face anchors covered by feature-level units" },
|
|
["covered_interface_relation_ids"] = new[] { "contact, fit, coaxial, mate, support, locating, motion, clearance, or accessibility relation ids covered by interface-level units" },
|
|
["covered_functional_group_ids"] = new[] { "internal component group or mechanism group ids covered by functional_group units" },
|
|
["uncovered_candidate_anchors"] = new[] { "anchor_type/anchor_id/reason_not_covered records for intentionally excluded candidates" },
|
|
["local_unit_count_by_anchor_type"] = new Dictionary<string, object?> { ["part"] = 0, ["feature"] = 0, ["interface"] = 0, ["functional_group"] = 0 }
|
|
},
|
|
BrepFactBundleSchema = new Dictionary<string, object?>
|
|
{
|
|
["anchor"] = new Dictionary<string, object?> { ["kind"] = "part | feature | face_group | relation | assembly_region", ["id"] = "feature/face/relation id", ["solidworks_name"] = "real component or feature owner name when available", ["component_file_name"] = "real component file name when available", ["inferred_role"] = "AI role label such as lower support seat or side bracket, never replacing the real name" },
|
|
["related_parts"] = new[] { "solidworks instance names, file names, paths, and inferred role candidates" },
|
|
["related_features"] = new[] { "feature ids and role candidates" },
|
|
["known_relations"] = new[] { "relation type, a, b, confidence, evidence refs" },
|
|
["assembly_function_context"] = new[] { "required for assembly part/feature units: component assembly role, inherited functional requirements, related interface_function_profile ids, supporting SolidWorks mate ids, B-rep contact ids, coaxial/flush/parallel relation facts, functional contact/mate face refs, related components, and confidence" },
|
|
["brep_facts"] = new[] { "type/source_ref/value/unit/confidence, e.g. radius_mm, area_mm2, axis, bbox_mm, adjacency, contact_candidate, distance_mm, angle_deg, thickness_mm, clearance_mm" },
|
|
["image_evidence_names"] = new[] { "exact image names or image plan ids supporting this fact bundle" },
|
|
["image_observations"] = new[] { "image_name/observation/supports/confidence records used for later image reload" },
|
|
["image_facts"] = new[] { "view/observation/confidence, e.g. internal side, blocked by wall, likely cover mounting region" },
|
|
["missing_facts"] = new[] { "fact/why_needed/suggested_extraction, used to trigger second-pass extraction" },
|
|
["confidence"] = "0.0-1.0",
|
|
["evidence_status"] = "confirmed | likely | candidate | uncertain | contradicted"
|
|
},
|
|
EvidenceStatusValues = ["confirmed", "likely", "candidate", "uncertain", "contradicted"],
|
|
ConfidencePolicy =
|
|
[
|
|
"Use confirmed only when image and B-rep evidence agree or the fact is directly measured.",
|
|
"Use likely when one evidence channel is strong and the other does not contradict it.",
|
|
"Use mechanical_common_sense_inference when B-rep, mates, contacts, and images do not directly state a function/process/property, but mechanical design knowledge can answer whether this kind of part or structure should normally have it.",
|
|
"Use candidate when the feature pattern is plausible but function, process intent, or local relation is missing.",
|
|
"Use uncertain when image and B-rep evidence cannot be aligned.",
|
|
"Use contradicted when a visual interpretation is not supported by corresponding B-rep feature facts."
|
|
],
|
|
DedupPolicy =
|
|
[
|
|
"Merge local units with the same primary_view, same anchor id, and same core relation signature.",
|
|
"If two units only differ by wording, keep the one with more concrete B-rep facts and merge evidence/missing_facts.",
|
|
"Do not create separate biased units such as risk/boundary/fault for the same anchor and view before retrieval; keep one neutral fact unit.",
|
|
"The same anchor may appear in multiple views only when the retrieval purpose differs, such as surface boundary vs tool access.",
|
|
"Never remove uncertainty or missing facts during deduplication."
|
|
],
|
|
RetrievalPolicy =
|
|
[
|
|
"Model-side semantic generation is knowledge-blind: no detailed rule store, no rule ids, no final fault words.",
|
|
"Use SolidWorks component names/file names as primary names. Put AI inferred functional names in parentheses or inferred_role fields only.",
|
|
"For assembly input, generate local units for assembly relations and for each nonstandard component with review_scope=part_design_required.",
|
|
"For assembly input, always infer component_function_profiles and interface_function_profiles before part-design local units. Part-level and feature-level local units must inherit the component's assembly role and functional face/interface roles in fact_bundle.assembly_function_context.",
|
|
"Keep whole-component purpose, physical-interface function, and connection-method assessment in separate records. They may reference each other by stable ids but must not copy conclusions across layers.",
|
|
"A B-rep contact proves geometric adjacency only. It does not prove clearance, transition, or interference fit and does not prove that the pair is positively retained.",
|
|
"For every cylindrical or load-transferring contact, inspect the whole assembly for alternative retention or torque-transfer mechanisms: screws/bolts, keys/splines, pins, shoulders, retaining rings, clamps, welds, adhesives and connected intermediate parts. Record observed, not_observed and unknown separately.",
|
|
"Consider interference fit only after deciding whether relative motion is prohibited, whether disassembly is intended, what loads must transfer, and whether another positive-retention mechanism exists. No observed mechanism is not proof of absence; keep missing evidence explicit.",
|
|
"Infer fit category and surface-finish intent from interface function and connection method, but do not invent an exact tolerance designation or Ra value without a retrieved standard/knowledge rule. Exact drawing values are judged downstream against the knowledge base.",
|
|
"When inferring whole-assembly function, explicitly use part-to-part mating relations: SolidWorks mates, B-rep contacts, coaxial cylindrical relations, planar contacts, support/locating faces, and relative positions. Do not infer a component function from its name or image alone when mating/contact evidence is available.",
|
|
"When diagnosing a component as a part inside an assembly, do not treat it as an isolated unknown part. Use its assembly_function_context to decide whether its faces/features are locating, supporting, sealing, guiding, fastening, bearing, load-carrying, motion-limiting, or access-related.",
|
|
"Do not run part-structure taboo checks on standard or purchased components with review_scope=assembly_context_only; use them only for mating context, occlusion/boundary context, purchased-component identification, or standard-part selection/calculation checks.",
|
|
"Motors, servos, reducers, gearboxes, and gear motors are purchased components by default. Identify them and preserve their assembly function, but do not create part-design local units for their internal structure.",
|
|
"Use ComponentContextImagePlan images as reusable internal component group evidence. These groups are geometry-derived, not AI functional group guesses.",
|
|
"Do not treat missing semantic labels as final missing information. If an attribute/function/process is not directly extractable from B-rep, contacts, mates, or images, perform mechanical_common_sense_inference before marking it unsupported.",
|
|
"Create local units by coverage anchors: every part_design_required component instance gets at least one part-level unit; supported local features get feature-level units; contacts/mates/fits/coaxial/support/locating/motion/accessibility relations get interface-level units; geometry-derived internal/component groups get functional_group units when group evidence matters.",
|
|
"Do not collapse separate physical locations into one generic local unit. Merge only repeated equivalent anchors with the same owner, role, dimensions, relation signature, primary_view, and evidence.",
|
|
"Write semantic_coverage_audit and list uncovered_candidate_anchors instead of silently omitting supported parts/features/interfaces/groups.",
|
|
"Use LocalSemanticUnit.retrieval_sentence as the main query and primary_view/secondary_views as filters or weights.",
|
|
"The retrieval index returns rule_id only; the detailed verification rule is loaded after retrieval.",
|
|
"Final judgement must cite fact_bundle items and verification_rule.required_model_facts.",
|
|
"If a likely candidate lacks required extractable facts, issue a second-pass extraction request rather than guessing."
|
|
]
|
|
};
|
|
}
|
|
|
|
static List<AssemblyFaceEvidence> FindHoleEntryFaceCandidates(AssemblyFaceEvidence hole, List<AssemblyFaceEvidence> planes)
|
|
{
|
|
if (hole.Axis.Length < 3 || hole.CenterMm.Length < 3)
|
|
return [];
|
|
|
|
var axis = Vec.Normalize(hole.Axis);
|
|
return planes
|
|
.Where(p => p.Normal.Length >= 3)
|
|
.Select(p => new
|
|
{
|
|
Face = p,
|
|
Parallel = Math.Abs(Vec.Dot(axis, Vec.Normalize(p.Normal))),
|
|
AxialDistance = Math.Abs(Vec.Dot(Vec.Sub(p.CenterMm, hole.CenterMm), axis)),
|
|
CenterDistance = Vec.Norm(Vec.Sub(p.CenterMm, hole.CenterMm))
|
|
})
|
|
.Where(x => x.Parallel >= 0.85)
|
|
.OrderBy(x => x.AxialDistance)
|
|
.ThenBy(x => x.CenterDistance)
|
|
.Take(4)
|
|
.Select(x => x.Face)
|
|
.ToList();
|
|
}
|
|
|
|
static string FaceRef(AssemblyFaceEvidence face) => $"{face.ComponentName}:face#{face.FaceIndex}";
|
|
|
|
static List<HoleFeatureGroup> BuildHoleFeatureGroups(List<AssemblyFaceEvidence> holeFaces, AssemblyFrameEvidence frame)
|
|
{
|
|
var result = new List<HoleFeatureGroup>();
|
|
var remaining = holeFaces
|
|
.Where(f => f.Axis.Length >= 3 && f.CenterMm.Length >= 3)
|
|
.OrderBy(f => f.FaceIndex)
|
|
.ToList();
|
|
|
|
foreach (var radiusGroup in remaining
|
|
.GroupBy(f => $"{f.ComponentName}:{Math.Round(f.RadiusMm, 1)}", StringComparer.OrdinalIgnoreCase)
|
|
.OrderBy(g => g.Min(f => f.RadiusMm)))
|
|
{
|
|
var candidates = radiusGroup.OrderBy(f => f.FaceIndex).ToList();
|
|
ExtractCircularHolePatterns(candidates, result, frame);
|
|
ExtractCoplanarParallelHolePatterns(candidates, result);
|
|
ExtractFallbackAxisRadiusGroups(candidates, result);
|
|
}
|
|
|
|
return result.OrderBy(g => g.Members.Min(f => f.FaceIndex)).ToList();
|
|
}
|
|
|
|
static void ExtractCircularHolePatterns(List<AssemblyFaceEvidence> candidates, List<HoleFeatureGroup> result, AssemblyFrameEvidence frame)
|
|
{
|
|
var patternAxes = CandidatePatternAxes(frame);
|
|
foreach (var patternAxis in patternAxes)
|
|
{
|
|
var free = candidates.Where(f => !IsAlreadyGrouped(f, result)).ToList();
|
|
if (free.Count < 3)
|
|
return;
|
|
|
|
var radius = free.Average(f => f.RadiusMm);
|
|
var planeTolerance = Math.Max(1.0, radius * 0.75);
|
|
foreach (var planeGroup in free
|
|
.GroupBy(f => $"{f.ComponentName}:{Quantize(Vec.Dot(f.CenterMm, patternAxis), planeTolerance)}", StringComparer.OrdinalIgnoreCase)
|
|
.Where(g => g.Count() >= 3)
|
|
.OrderByDescending(g => g.Count()))
|
|
{
|
|
var members = planeGroup.OrderBy(f => f.FaceIndex).ToList();
|
|
if (!TryDescribeCircularPattern(members, patternAxis, out var patternType, out var center, out var patternRadius, out var spacing))
|
|
continue;
|
|
|
|
result.Add(new HoleFeatureGroup
|
|
{
|
|
Members = members,
|
|
PatternType = patternType,
|
|
GroupingBasis = "same diameter, shared pattern plane, and center-parameter circular distribution",
|
|
PatternNormal = Vec.Round(patternAxis),
|
|
PatternPlaneTmm = members.Average(f => Vec.Dot(f.CenterMm, patternAxis)),
|
|
PatternCenterMm = center,
|
|
PatternRadiusMm = patternRadius,
|
|
SpacingMm = spacing
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
static void ExtractCoplanarParallelHolePatterns(List<AssemblyFaceEvidence> candidates, List<HoleFeatureGroup> result)
|
|
{
|
|
var free = candidates.Where(f => !IsAlreadyGrouped(f, result)).ToList();
|
|
if (free.Count < 2)
|
|
return;
|
|
|
|
var radius = free.Average(f => f.RadiusMm);
|
|
var planeTolerance = Math.Max(1.0, radius * 0.75);
|
|
foreach (var group in free
|
|
.GroupBy(f => $"{f.ComponentName}:{AxisKey(f.Axis)}:{Quantize(Vec.Dot(f.CenterMm, Vec.Normalize(f.Axis)), planeTolerance)}", StringComparer.OrdinalIgnoreCase)
|
|
.Where(g => g.Count() >= 2)
|
|
.OrderByDescending(g => g.Count()))
|
|
{
|
|
var members = group.OrderBy(f => f.FaceIndex).ToList();
|
|
var axis = Vec.Normalize(members[0].Axis);
|
|
var center = AveragePoint(members.Select(f => f.CenterMm));
|
|
result.Add(new HoleFeatureGroup
|
|
{
|
|
Members = members,
|
|
PatternType = ClassifyParallelCenterPattern(members, axis),
|
|
GroupingBasis = "same diameter, parallel axes, shared axial parameter plane, and center-parameter distribution",
|
|
PatternNormal = Vec.Round(axis),
|
|
PatternPlaneTmm = members.Average(f => Vec.Dot(f.CenterMm, axis)),
|
|
PatternCenterMm = center,
|
|
SpacingMm = EstimateAverageNearestSpacing(members)
|
|
});
|
|
}
|
|
}
|
|
|
|
static void ExtractFallbackAxisRadiusGroups(List<AssemblyFaceEvidence> candidates, List<HoleFeatureGroup> result)
|
|
{
|
|
foreach (var group in candidates
|
|
.Where(f => !IsAlreadyGrouped(f, result))
|
|
.GroupBy(f => $"{f.ComponentName}:{AxisKey(f.Axis)}:{Math.Round(f.RadiusMm, 1)}", StringComparer.OrdinalIgnoreCase)
|
|
.OrderBy(g => g.Min(f => f.RadiusMm)))
|
|
{
|
|
var members = group.OrderBy(f => f.FaceIndex).ToList();
|
|
result.Add(new HoleFeatureGroup
|
|
{
|
|
Members = members,
|
|
PatternType = members.Count > 1 ? "same_axis_same_diameter_hole_group" : "single_hole_candidate",
|
|
GroupingBasis = "fallback same component, axis direction, and diameter",
|
|
PatternNormal = members.Count > 0 ? Vec.Round(Vec.Normalize(members[0].Axis)) : [],
|
|
PatternCenterMm = AveragePoint(members.Select(f => f.CenterMm)),
|
|
SpacingMm = EstimateAverageNearestSpacing(members)
|
|
});
|
|
}
|
|
}
|
|
|
|
static List<double[]> CandidatePatternAxes(AssemblyFrameEvidence frame)
|
|
{
|
|
var axes = new List<double[]>();
|
|
AddAxis(frame.ShaftAxisMm);
|
|
AddAxis(frame.VerticalAxisMm);
|
|
AddAxis([1.0, 0.0, 0.0]);
|
|
AddAxis([0.0, 1.0, 0.0]);
|
|
AddAxis([0.0, 0.0, 1.0]);
|
|
return axes;
|
|
|
|
void AddAxis(double[]? axis)
|
|
{
|
|
if (axis == null || axis.Length < 3 || Vec.Norm(axis) < 1e-9)
|
|
return;
|
|
var normalized = CanonicalAxis(Vec.Normalize(axis));
|
|
if (!axes.Any(existing => Math.Abs(Vec.Dot(existing, normalized)) > 0.985))
|
|
axes.Add(normalized);
|
|
}
|
|
}
|
|
|
|
static bool TryDescribeCircularPattern(
|
|
List<AssemblyFaceEvidence> members,
|
|
double[] patternAxis,
|
|
out string patternType,
|
|
out double[] centerMm,
|
|
out double patternRadiusMm,
|
|
out double spacingMm)
|
|
{
|
|
patternType = "";
|
|
centerMm = [];
|
|
patternRadiusMm = 0;
|
|
spacingMm = 0;
|
|
if (members.Count < 3)
|
|
return false;
|
|
|
|
var axis = Vec.Normalize(patternAxis);
|
|
var (u, v) = PlaneBasis(axis);
|
|
var t = members.Average(f => Vec.Dot(f.CenterMm, axis));
|
|
var us = members.Select(f => Vec.Dot(f.CenterMm, u)).ToList();
|
|
var vs = members.Select(f => Vec.Dot(f.CenterMm, v)).ToList();
|
|
var centerU = us.Average();
|
|
var centerV = vs.Average();
|
|
centerMm = Vec.Add(Vec.Add(Vec.Mul(axis, t), Vec.Mul(u, centerU)), Vec.Mul(v, centerV));
|
|
var center = centerMm;
|
|
|
|
var distances = members
|
|
.Select(f => Vec.Norm(Vec.Sub(f.CenterMm, center)))
|
|
.ToList();
|
|
patternRadiusMm = distances.Average();
|
|
if (patternRadiusMm < Math.Max(2.0, members.Average(f => f.RadiusMm) * 1.5))
|
|
return false;
|
|
|
|
var patternRadius = patternRadiusMm;
|
|
var maxDistanceDeviation = distances.Max(d => Math.Abs(d - patternRadius));
|
|
if (maxDistanceDeviation > Math.Max(1.5, patternRadius * 0.12))
|
|
return false;
|
|
|
|
var parallelCount = members.Count(f => IsParallel(f.Axis, axis, 0.85));
|
|
var radialCount = members.Count(f =>
|
|
{
|
|
var radial = Vec.Sub(f.CenterMm, center);
|
|
radial = Vec.Sub(radial, Vec.Mul(axis, Vec.Dot(radial, axis)));
|
|
return Vec.Norm(radial) > 1e-6 && Math.Abs(Vec.Dot(Vec.Normalize(radial), Vec.Normalize(f.Axis))) >= 0.72;
|
|
});
|
|
|
|
if (parallelCount < members.Count - 1 && radialCount < members.Count - 1)
|
|
return false;
|
|
|
|
spacingMm = EstimateAverageNearestSpacing(members);
|
|
patternType = radialCount >= parallelCount
|
|
? "radial_circular_hole_pattern"
|
|
: "parallel_circular_hole_pattern";
|
|
return true;
|
|
}
|
|
|
|
static string ClassifyParallelCenterPattern(List<AssemblyFaceEvidence> members, double[] axis)
|
|
{
|
|
if (members.Count < 3)
|
|
return "coplanar_parallel_hole_pair";
|
|
|
|
var center = AveragePoint(members.Select(f => f.CenterMm));
|
|
var distances = members.Select(f => Vec.Norm(Vec.Sub(f.CenterMm, center))).ToList();
|
|
var average = distances.Average();
|
|
var maxDeviation = distances.Max(d => Math.Abs(d - average));
|
|
if (average > 1e-6 && maxDeviation <= Math.Max(1.5, average * 0.12))
|
|
return "parallel_circular_hole_pattern";
|
|
|
|
return "coplanar_parallel_hole_pattern";
|
|
}
|
|
|
|
static bool IsAlreadyGrouped(AssemblyFaceEvidence face, List<HoleFeatureGroup> groups) =>
|
|
groups.Any(g => g.Members.Any(m => ReferenceEquals(m, face)));
|
|
|
|
static int Quantize(double value, double tolerance) =>
|
|
(int)Math.Round(value / Math.Max(tolerance, 1e-6));
|
|
|
|
static double[] CanonicalAxis(double[] axis)
|
|
{
|
|
var n = Vec.Normalize(axis);
|
|
if (n[0] < -0.0001 || (Math.Abs(n[0]) < 0.0001 && n[1] < -0.0001) || (Math.Abs(n[0]) < 0.0001 && Math.Abs(n[1]) < 0.0001 && n[2] < -0.0001))
|
|
n = Vec.Mul(n, -1);
|
|
return n;
|
|
}
|
|
|
|
static double[] AveragePoint(IEnumerable<double[]> points)
|
|
{
|
|
var list = points.Where(p => p.Length >= 3).ToList();
|
|
if (list.Count == 0)
|
|
return [];
|
|
return
|
|
[
|
|
list.Average(p => p[0]),
|
|
list.Average(p => p[1]),
|
|
list.Average(p => p[2])
|
|
];
|
|
}
|
|
|
|
static double EstimateAverageNearestSpacing(List<AssemblyFaceEvidence> members)
|
|
{
|
|
if (members.Count < 2)
|
|
return 0;
|
|
var nearest = new List<double>();
|
|
foreach (var member in members)
|
|
{
|
|
var d = members
|
|
.Where(other => !ReferenceEquals(other, member))
|
|
.Select(other => Vec.Norm(Vec.Sub(other.CenterMm, member.CenterMm)))
|
|
.DefaultIfEmpty(0)
|
|
.Min();
|
|
if (d > 0)
|
|
nearest.Add(d);
|
|
}
|
|
return nearest.Count == 0 ? 0 : nearest.Average();
|
|
}
|
|
|
|
static string AxisKey(double[] axis)
|
|
{
|
|
if (axis.Length < 3 || Vec.Norm(axis) < 1e-9)
|
|
return "none";
|
|
|
|
var n = Vec.Normalize(axis);
|
|
if (n[0] < -0.0001 || (Math.Abs(n[0]) < 0.0001 && n[1] < -0.0001) || (Math.Abs(n[0]) < 0.0001 && Math.Abs(n[1]) < 0.0001 && n[2] < -0.0001))
|
|
n = Vec.Mul(n, -1);
|
|
return $"{Math.Round(n[0], 2)}_{Math.Round(n[1], 2)}_{Math.Round(n[2], 2)}";
|
|
}
|
|
|
|
#pragma warning disable CS8321 // Legacy section graph builders retained only for backward-compatible empty report fields.
|
|
static Canonical2DStructureGraph BuildCanonicalModel2D(SectionPlaneEvidence plane, List<SectionBodyEvidence> bodies)
|
|
{
|
|
var canonical = BuildCanonicalModelSection(plane, bodies);
|
|
var graph = new Canonical2DStructureGraph
|
|
{
|
|
Source = new Canonical2DSource
|
|
{
|
|
Kind = "model_section",
|
|
CoordinateSystem = "section_plane_uv_mm",
|
|
Metadata = canonical.Metadata
|
|
},
|
|
Edges = canonical.Edges.Select(e => new Canonical2DEdge
|
|
{
|
|
Id = e.Id,
|
|
Type = e.Kind,
|
|
Start = e.Start,
|
|
End = e.End,
|
|
Owner = Attr(e, "component"),
|
|
Role = "unknown",
|
|
SourceEntity = Attr(e, "provenance"),
|
|
Attributes = e.Attributes
|
|
}).ToList(),
|
|
Annotations = []
|
|
};
|
|
|
|
foreach (var group in graph.Edges.GroupBy(e => e.Owner).Where(g => !string.IsNullOrWhiteSpace(g.Key)))
|
|
{
|
|
graph.Regions.Add(new Canonical2DRegion
|
|
{
|
|
Id = $"region_{SanitizeId(group.Key)}",
|
|
Owner = group.Key,
|
|
Role = "material_region",
|
|
EdgeRefs = group.Select(e => e.Id).ToList()
|
|
});
|
|
}
|
|
|
|
graph.Regions.Add(new Canonical2DRegion
|
|
{
|
|
Id = "region_outside",
|
|
Owner = "environment",
|
|
Role = "outside_region",
|
|
EdgeRefs = []
|
|
});
|
|
|
|
foreach (var region in graph.Regions.Where(r => r.Role == "material_region"))
|
|
{
|
|
foreach (var edgeId in region.EdgeRefs)
|
|
{
|
|
graph.Relations.Add(new Canonical2DRelation
|
|
{
|
|
Type = "edge_bounds_region",
|
|
A = edgeId,
|
|
B = region.Id,
|
|
Attributes = new Dictionary<string, object?>
|
|
{
|
|
["region_role"] = region.Role,
|
|
["owner"] = region.Owner
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
graph.Relations.AddRange(BuildModel2DRelations(graph.Edges));
|
|
foreach (var edge in FindGlobalOutsideBoundaryEdges(graph.Edges))
|
|
{
|
|
edge.Role = "outer_boundary_candidate";
|
|
graph.Relations.Add(new Canonical2DRelation
|
|
{
|
|
Type = "adjacent_to_outside",
|
|
A = edge.Id,
|
|
B = "region_outside",
|
|
Attributes = new Dictionary<string, object?>
|
|
{
|
|
["reason"] = "edge_on_global_section_profile_extreme"
|
|
}
|
|
});
|
|
}
|
|
return graph;
|
|
}
|
|
|
|
static List<Canonical2DEdge> FindGlobalOutsideBoundaryEdges(List<Canonical2DEdge> edges)
|
|
{
|
|
var valid = edges.Where(e => e.Start.Length >= 2 && e.End.Length >= 2).ToList();
|
|
if (valid.Count == 0)
|
|
return [];
|
|
|
|
var xs = valid.SelectMany(e => new[] { e.Start[0], e.End[0] }).ToList();
|
|
var ys = valid.SelectMany(e => new[] { e.Start[1], e.End[1] }).ToList();
|
|
double xmin = xs.Min();
|
|
double xmax = xs.Max();
|
|
double ymin = ys.Min();
|
|
double ymax = ys.Max();
|
|
double tol = 0.35;
|
|
|
|
return valid
|
|
.Where(e => e.Type.Contains("line", StringComparison.OrdinalIgnoreCase))
|
|
.Where(e => Distance2D(e.Start, e.End) > 1.0)
|
|
.Where(e =>
|
|
(Math.Abs(e.Start[0] - xmin) <= tol && Math.Abs(e.End[0] - xmin) <= tol) ||
|
|
(Math.Abs(e.Start[0] - xmax) <= tol && Math.Abs(e.End[0] - xmax) <= tol) ||
|
|
(Math.Abs(e.Start[1] - ymin) <= tol && Math.Abs(e.End[1] - ymin) <= tol) ||
|
|
(Math.Abs(e.Start[1] - ymax) <= tol && Math.Abs(e.End[1] - ymax) <= tol))
|
|
.ToList();
|
|
}
|
|
|
|
static double Distance2D(double[] a, double[] b)
|
|
{
|
|
if (a.Length < 2 || b.Length < 2)
|
|
return 0;
|
|
double dx = a[0] - b[0];
|
|
double dy = a[1] - b[1];
|
|
return Math.Sqrt(dx * dx + dy * dy);
|
|
}
|
|
|
|
static List<Canonical2DRelation> BuildModel2DRelations(List<Canonical2DEdge> edges)
|
|
{
|
|
var result = new List<Canonical2DRelation>();
|
|
for (int i = 0; i < edges.Count; i++)
|
|
{
|
|
for (int j = i + 1; j < edges.Count; j++)
|
|
{
|
|
var a = edges[i];
|
|
var b = edges[j];
|
|
if (string.Equals(a.Owner, b.Owner, StringComparison.OrdinalIgnoreCase))
|
|
continue;
|
|
if (CoincidentLine(a, b, 0.25))
|
|
{
|
|
result.Add(new Canonical2DRelation
|
|
{
|
|
Type = "contact",
|
|
A = a.Id,
|
|
B = b.Id,
|
|
Attributes = new Dictionary<string, object?>
|
|
{
|
|
["relation"] = "coincident_section_edges",
|
|
["owner_a"] = a.Owner,
|
|
["owner_b"] = b.Owner
|
|
}
|
|
});
|
|
}
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
static bool CoincidentLine(Canonical2DEdge a, Canonical2DEdge b, double tolerance)
|
|
{
|
|
if (a.Start.Length < 2 || a.End.Length < 2 || b.Start.Length < 2 || b.End.Length < 2)
|
|
return false;
|
|
bool aHorizontal = Math.Abs(a.Start[1] - a.End[1]) <= tolerance;
|
|
bool bHorizontal = Math.Abs(b.Start[1] - b.End[1]) <= tolerance;
|
|
if (aHorizontal && bHorizontal && Math.Abs(a.Start[1] - b.Start[1]) <= tolerance)
|
|
return SegmentOverlap(a.Start[0], a.End[0], b.Start[0], b.End[0]) > tolerance;
|
|
|
|
bool aVertical = Math.Abs(a.Start[0] - a.End[0]) <= tolerance;
|
|
bool bVertical = Math.Abs(b.Start[0] - b.End[0]) <= tolerance;
|
|
if (aVertical && bVertical && Math.Abs(a.Start[0] - b.Start[0]) <= tolerance)
|
|
return SegmentOverlap(a.Start[1], a.End[1], b.Start[1], b.End[1]) > tolerance;
|
|
|
|
return false;
|
|
}
|
|
|
|
static double SegmentOverlap(double a0, double a1, double b0, double b1)
|
|
{
|
|
var amin = Math.Min(a0, a1);
|
|
var amax = Math.Max(a0, a1);
|
|
var bmin = Math.Min(b0, b1);
|
|
var bmax = Math.Max(b0, b1);
|
|
return Math.Max(0, Math.Min(amax, bmax) - Math.Max(amin, bmin));
|
|
}
|
|
|
|
static Canonical2DSketchGraph BuildSketchGraph(Canonical2DStructureGraph graph, string sourceKind, string units, double vertexTolerance)
|
|
{
|
|
var sketch = new Canonical2DSketchGraph
|
|
{
|
|
Source = new Canonical2DSketchSource
|
|
{
|
|
Kind = sourceKind,
|
|
CoordinateSystem = graph.Source.CoordinateSystem,
|
|
Units = units,
|
|
Metadata = graph.Source.Metadata
|
|
},
|
|
Relations = graph.Relations.Select(r => new SketchRelation
|
|
{
|
|
Type = r.Type,
|
|
A = r.A,
|
|
B = r.B,
|
|
Attributes = r.Attributes
|
|
}).ToList()
|
|
};
|
|
|
|
var unique = MergeDuplicateEdges(graph.Edges, vertexTolerance);
|
|
var vertexIndex = new Dictionary<string, SketchVertex>(StringComparer.OrdinalIgnoreCase);
|
|
|
|
foreach (var edge in unique)
|
|
{
|
|
var start = GetOrAddVertex(edge.Start);
|
|
var end = GetOrAddVertex(edge.End);
|
|
if (start.Id == end.Id)
|
|
continue;
|
|
|
|
double length = Math.Round(Distance2D(edge.Start, edge.End), 3);
|
|
var drawable = new SketchDrawableEdge
|
|
{
|
|
Id = $"sketch_edge_{sketch.Edges.Count + 1}",
|
|
SourceEdgeIds = edge.SourceIds,
|
|
Type = edge.Type,
|
|
StartVertex = start.Id,
|
|
EndVertex = end.Id,
|
|
Start = start.Point,
|
|
End = end.Point,
|
|
NormalizedStart = [],
|
|
NormalizedEnd = [],
|
|
Owner = edge.Owner,
|
|
Role = edge.Role,
|
|
Length = length,
|
|
Attributes = edge.Attributes
|
|
};
|
|
drawable.Attributes["orientation"] = EdgeOrientation(edge.Start, edge.End);
|
|
sketch.Edges.Add(drawable);
|
|
}
|
|
|
|
sketch.Vertices = vertexIndex.Values.OrderBy(v => v.Id, StringComparer.OrdinalIgnoreCase).ToList();
|
|
NormalizeSketchCoordinates(sketch);
|
|
sketch.Loops = BuildSketchLoops(sketch);
|
|
sketch.Paths = BuildSketchPaths(sketch);
|
|
|
|
foreach (var region in graph.Regions)
|
|
{
|
|
sketch.Regions.Add(new SketchRegion
|
|
{
|
|
Id = region.Id,
|
|
Owner = region.Owner,
|
|
Role = region.Role,
|
|
SourceEdgeRefs = region.EdgeRefs,
|
|
LoopRefs = sketch.Loops
|
|
.Where(loop => loop.SourceEdgeRefs.Any(id => region.EdgeRefs.Contains(id, StringComparer.OrdinalIgnoreCase)))
|
|
.Select(loop => loop.Id)
|
|
.Distinct(StringComparer.OrdinalIgnoreCase)
|
|
.ToList()
|
|
});
|
|
}
|
|
|
|
sketch.Dimensions = BuildSketchDimensions(sketch, graph.Relations);
|
|
sketch.Dimensions.AddRange(BuildSketchGlobalDimensions(sketch));
|
|
return sketch;
|
|
|
|
SketchVertex GetOrAddVertex(double[] point)
|
|
{
|
|
var snapped = SnapPoint(point, vertexTolerance);
|
|
string key = $"{snapped[0]:0.###},{snapped[1]:0.###}";
|
|
if (vertexIndex.TryGetValue(key, out var existing))
|
|
return existing;
|
|
|
|
var vertex = new SketchVertex
|
|
{
|
|
Id = $"v{vertexIndex.Count + 1}",
|
|
Point = snapped,
|
|
NormalizedPoint = []
|
|
};
|
|
vertexIndex[key] = vertex;
|
|
return vertex;
|
|
}
|
|
}
|
|
|
|
#pragma warning restore CS8321
|
|
|
|
static List<SketchDimension> BuildSketchGlobalDimensions(Canonical2DSketchGraph sketch)
|
|
{
|
|
if (sketch.Vertices.Count == 0)
|
|
return [];
|
|
double minX = sketch.Vertices.Min(v => v.Point[0]);
|
|
double maxX = sketch.Vertices.Max(v => v.Point[0]);
|
|
double minY = sketch.Vertices.Min(v => v.Point[1]);
|
|
double maxY = sketch.Vertices.Max(v => v.Point[1]);
|
|
return
|
|
[
|
|
new SketchDimension
|
|
{
|
|
Id = "dim_global_width",
|
|
Kind = "global_width",
|
|
Value = Math.Round(maxX - minX, 3),
|
|
Units = sketch.Source.Units,
|
|
Role = "scale"
|
|
},
|
|
new SketchDimension
|
|
{
|
|
Id = "dim_global_height",
|
|
Kind = "global_height",
|
|
Value = Math.Round(maxY - minY, 3),
|
|
Units = sketch.Source.Units,
|
|
Role = "scale"
|
|
}
|
|
];
|
|
}
|
|
|
|
static List<MergedSketchEdge> MergeDuplicateEdges(List<Canonical2DEdge> edges, double tolerance)
|
|
{
|
|
var merged = new List<MergedSketchEdge>();
|
|
foreach (var edge in edges.Where(e => e.Start.Length >= 2 && e.End.Length >= 2))
|
|
{
|
|
var s = SnapPoint(edge.Start, tolerance);
|
|
var e = SnapPoint(edge.End, tolerance);
|
|
if (Distance2D(s, e) <= tolerance)
|
|
continue;
|
|
var keyA = PointKey(s);
|
|
var keyB = PointKey(e);
|
|
string key = string.CompareOrdinal(keyA, keyB) <= 0 ? $"{keyA}|{keyB}" : $"{keyB}|{keyA}";
|
|
var existing = merged.FirstOrDefault(item => item.Key == key && string.Equals(item.Owner, edge.Owner, StringComparison.OrdinalIgnoreCase));
|
|
if (existing != null)
|
|
{
|
|
existing.SourceIds.Add(edge.Id);
|
|
continue;
|
|
}
|
|
|
|
merged.Add(new MergedSketchEdge
|
|
{
|
|
Key = key,
|
|
Type = edge.Type,
|
|
Start = s,
|
|
End = e,
|
|
Owner = edge.Owner,
|
|
Role = edge.Role,
|
|
SourceIds = [edge.Id],
|
|
Attributes = new Dictionary<string, object?>(edge.Attributes)
|
|
});
|
|
}
|
|
return merged;
|
|
}
|
|
|
|
static double[] SnapPoint(double[] point, double tolerance)
|
|
{
|
|
if (point.Length < 2)
|
|
return [];
|
|
return
|
|
[
|
|
Math.Round(Math.Round(point[0] / tolerance) * tolerance, 3),
|
|
Math.Round(Math.Round(point[1] / tolerance) * tolerance, 3)
|
|
];
|
|
}
|
|
|
|
static string PointKey(double[] point) => point.Length < 2 ? "" : $"{point[0]:0.###},{point[1]:0.###}";
|
|
|
|
static string EdgeOrientation(double[] a, double[] b)
|
|
{
|
|
double dx = Math.Abs(a[0] - b[0]);
|
|
double dy = Math.Abs(a[1] - b[1]);
|
|
if (dx <= 0.35 && dy > 0.35)
|
|
return "vertical";
|
|
if (dy <= 0.35 && dx > 0.35)
|
|
return "horizontal";
|
|
return "oblique";
|
|
}
|
|
|
|
static void NormalizeSketchCoordinates(Canonical2DSketchGraph sketch)
|
|
{
|
|
if (sketch.Vertices.Count == 0)
|
|
return;
|
|
double minX = sketch.Vertices.Min(v => v.Point[0]);
|
|
double maxX = sketch.Vertices.Max(v => v.Point[0]);
|
|
double minY = sketch.Vertices.Min(v => v.Point[1]);
|
|
double maxY = sketch.Vertices.Max(v => v.Point[1]);
|
|
double span = Math.Max(Math.Max(maxX - minX, maxY - minY), 1e-6);
|
|
|
|
foreach (var vertex in sketch.Vertices)
|
|
{
|
|
vertex.NormalizedPoint =
|
|
[
|
|
Math.Round((vertex.Point[0] - minX) / span, 6),
|
|
Math.Round((vertex.Point[1] - minY) / span, 6)
|
|
];
|
|
}
|
|
|
|
var vertexById = sketch.Vertices.ToDictionary(v => v.Id, StringComparer.OrdinalIgnoreCase);
|
|
foreach (var edge in sketch.Edges)
|
|
{
|
|
edge.NormalizedStart = vertexById.TryGetValue(edge.StartVertex, out var sv) ? sv.NormalizedPoint : [];
|
|
edge.NormalizedEnd = vertexById.TryGetValue(edge.EndVertex, out var ev) ? ev.NormalizedPoint : [];
|
|
}
|
|
}
|
|
|
|
static List<SketchPath> BuildSketchPaths(Canonical2DSketchGraph sketch)
|
|
{
|
|
var paths = new List<SketchPath>();
|
|
var edgeById = sketch.Edges.ToDictionary(e => e.Id, StringComparer.OrdinalIgnoreCase);
|
|
var vertexById = sketch.Vertices.ToDictionary(v => v.Id, StringComparer.OrdinalIgnoreCase);
|
|
var adjacency = new Dictionary<string, List<SketchDrawableEdge>>(StringComparer.OrdinalIgnoreCase);
|
|
foreach (var edge in sketch.Edges)
|
|
{
|
|
Add(edge.StartVertex, edge);
|
|
Add(edge.EndVertex, edge);
|
|
}
|
|
|
|
var remaining = new HashSet<string>(sketch.Edges.Select(e => e.Id), StringComparer.OrdinalIgnoreCase);
|
|
while (remaining.Count > 0)
|
|
{
|
|
var component = CollectPathComponent(edgeById[remaining.First()], adjacency, remaining);
|
|
foreach (var edge in component)
|
|
remaining.Remove(edge.Id);
|
|
|
|
var ordered = OrderPathComponent(component, adjacency);
|
|
var vertexRefs = OrderedPathVertices(ordered);
|
|
bool closed = vertexRefs.Count > 2 && string.Equals(vertexRefs[0], vertexRefs[^1], StringComparison.OrdinalIgnoreCase);
|
|
if (closed)
|
|
vertexRefs.RemoveAt(vertexRefs.Count - 1);
|
|
|
|
paths.Add(new SketchPath
|
|
{
|
|
Id = $"path_{paths.Count + 1}",
|
|
EdgeRefs = ordered.Select(e => e.Id).ToList(),
|
|
SourceEdgeRefs = ordered.SelectMany(e => e.SourceEdgeIds).Distinct(StringComparer.OrdinalIgnoreCase).ToList(),
|
|
VertexRefs = vertexRefs,
|
|
Polyline = vertexRefs
|
|
.Where(vertexById.ContainsKey)
|
|
.Select(v => vertexById[v].NormalizedPoint.Length >= 2 ? vertexById[v].NormalizedPoint : vertexById[v].Point)
|
|
.ToList(),
|
|
Closed = closed,
|
|
Role = closed ? "closed_drawable_contour" : "open_drawable_contour"
|
|
});
|
|
}
|
|
|
|
return paths
|
|
.OrderByDescending(p => p.EdgeRefs.Count)
|
|
.ThenBy(p => p.Id, StringComparer.OrdinalIgnoreCase)
|
|
.ToList();
|
|
|
|
void Add(string vertex, SketchDrawableEdge edge)
|
|
{
|
|
if (!adjacency.TryGetValue(vertex, out var list))
|
|
adjacency[vertex] = list = [];
|
|
list.Add(edge);
|
|
}
|
|
}
|
|
|
|
static List<SketchDrawableEdge> CollectPathComponent(SketchDrawableEdge seed, Dictionary<string, List<SketchDrawableEdge>> adjacency, HashSet<string> available)
|
|
{
|
|
var result = new List<SketchDrawableEdge>();
|
|
var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { seed.Id };
|
|
var queue = new Queue<SketchDrawableEdge>();
|
|
queue.Enqueue(seed);
|
|
|
|
while (queue.Count > 0)
|
|
{
|
|
var edge = queue.Dequeue();
|
|
result.Add(edge);
|
|
foreach (var vertex in new[] { edge.StartVertex, edge.EndVertex })
|
|
{
|
|
if (!adjacency.TryGetValue(vertex, out var incident))
|
|
continue;
|
|
foreach (var next in incident)
|
|
{
|
|
if (!available.Contains(next.Id) || !seen.Add(next.Id))
|
|
continue;
|
|
queue.Enqueue(next);
|
|
}
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
static List<SketchDrawableEdge> OrderPathComponent(List<SketchDrawableEdge> component, Dictionary<string, List<SketchDrawableEdge>> adjacency)
|
|
{
|
|
var edgeSet = component.Select(e => e.Id).ToHashSet(StringComparer.OrdinalIgnoreCase);
|
|
string start = component
|
|
.SelectMany(e => new[] { e.StartVertex, e.EndVertex })
|
|
.GroupBy(v => v, StringComparer.OrdinalIgnoreCase)
|
|
.Where(g => g.Count() == 1)
|
|
.Select(g => g.Key)
|
|
.OrderBy(v => v, StringComparer.OrdinalIgnoreCase)
|
|
.FirstOrDefault() ?? component[0].StartVertex;
|
|
|
|
var ordered = new List<SketchDrawableEdge>();
|
|
var used = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
|
string current = start;
|
|
while (used.Count < component.Count)
|
|
{
|
|
var next = adjacency.TryGetValue(current, out var incident)
|
|
? incident.FirstOrDefault(e => edgeSet.Contains(e.Id) && !used.Contains(e.Id))
|
|
: null;
|
|
next ??= component.FirstOrDefault(e => !used.Contains(e.Id));
|
|
if (next == null)
|
|
break;
|
|
|
|
ordered.Add(next);
|
|
used.Add(next.Id);
|
|
current = next.StartVertex == current ? next.EndVertex : next.StartVertex;
|
|
}
|
|
|
|
return ordered;
|
|
}
|
|
|
|
static List<string> OrderedPathVertices(List<SketchDrawableEdge> ordered)
|
|
{
|
|
if (ordered.Count == 0)
|
|
return [];
|
|
|
|
var vertices = new List<string> { ordered[0].StartVertex, ordered[0].EndVertex };
|
|
string current = ordered[0].EndVertex;
|
|
for (int i = 1; i < ordered.Count; i++)
|
|
{
|
|
var edge = ordered[i];
|
|
if (edge.StartVertex == current)
|
|
current = edge.EndVertex;
|
|
else if (edge.EndVertex == current)
|
|
current = edge.StartVertex;
|
|
else
|
|
{
|
|
vertices.Add(edge.StartVertex);
|
|
current = edge.EndVertex;
|
|
}
|
|
vertices.Add(current);
|
|
}
|
|
return vertices;
|
|
}
|
|
|
|
static List<SketchLoop> BuildSketchLoops(Canonical2DSketchGraph sketch)
|
|
{
|
|
var loops = new List<SketchLoop>();
|
|
var edgeById = sketch.Edges.ToDictionary(e => e.Id, StringComparer.OrdinalIgnoreCase);
|
|
var adjacency = new Dictionary<string, List<SketchDrawableEdge>>(StringComparer.OrdinalIgnoreCase);
|
|
foreach (var edge in sketch.Edges)
|
|
{
|
|
Add(edge.StartVertex, edge);
|
|
Add(edge.EndVertex, edge);
|
|
}
|
|
|
|
var visited = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
|
foreach (var edge in sketch.Edges)
|
|
{
|
|
if (visited.Contains(edge.Id))
|
|
continue;
|
|
|
|
var loopEdges = new List<string>();
|
|
string startVertex = edge.StartVertex;
|
|
string currentVertex = edge.EndVertex;
|
|
string currentEdgeId = edge.Id;
|
|
loopEdges.Add(edge.Id);
|
|
visited.Add(edge.Id);
|
|
|
|
for (int guard = 0; guard < sketch.Edges.Count + 2; guard++)
|
|
{
|
|
var next = adjacency.TryGetValue(currentVertex, out var candidates)
|
|
? candidates.FirstOrDefault(e => !visited.Contains(e.Id))
|
|
: null;
|
|
if (next == null)
|
|
break;
|
|
visited.Add(next.Id);
|
|
loopEdges.Add(next.Id);
|
|
currentVertex = next.StartVertex == currentVertex ? next.EndVertex : next.StartVertex;
|
|
currentEdgeId = next.Id;
|
|
if (currentVertex == startVertex)
|
|
break;
|
|
}
|
|
|
|
bool closed = currentVertex == startVertex && loopEdges.Count >= 3;
|
|
if (closed)
|
|
{
|
|
var sourceRefs = loopEdges
|
|
.SelectMany(id => edgeById.TryGetValue(id, out var e) ? e.SourceEdgeIds : [])
|
|
.Distinct(StringComparer.OrdinalIgnoreCase)
|
|
.ToList();
|
|
loops.Add(new SketchLoop
|
|
{
|
|
Id = $"loop_{loops.Count + 1}",
|
|
EdgeRefs = loopEdges,
|
|
SourceEdgeRefs = sourceRefs,
|
|
Closed = true,
|
|
Role = "profile_loop"
|
|
});
|
|
}
|
|
}
|
|
|
|
return loops;
|
|
|
|
void Add(string vertex, SketchDrawableEdge edge)
|
|
{
|
|
if (!adjacency.TryGetValue(vertex, out var list))
|
|
adjacency[vertex] = list = [];
|
|
list.Add(edge);
|
|
}
|
|
}
|
|
|
|
static List<SketchDimension> BuildSketchDimensions(Canonical2DSketchGraph sketch, List<Canonical2DRelation> relations)
|
|
{
|
|
var dims = new List<SketchDimension>();
|
|
foreach (var edge in sketch.Edges)
|
|
{
|
|
dims.Add(new SketchDimension
|
|
{
|
|
Id = $"dim_length_{edge.Id}",
|
|
Kind = "edge_length",
|
|
TargetRefs = [edge.Id],
|
|
Value = edge.Length,
|
|
Units = sketch.Source.Units,
|
|
Role = edge.Role
|
|
});
|
|
}
|
|
|
|
foreach (var contact in relations.Where(r => r.Type == "contact"))
|
|
{
|
|
dims.Add(new SketchDimension
|
|
{
|
|
Id = $"dim_contact_{dims.Count + 1}",
|
|
Kind = "contact_length_candidate",
|
|
TargetRefs = [contact.A, contact.B],
|
|
Value = EstimateContactLength(sketch, contact.A, contact.B),
|
|
Units = sketch.Source.Units,
|
|
Role = "close_fit_contact"
|
|
});
|
|
}
|
|
|
|
foreach (var edge in sketch.Edges.Where(e => string.Equals(e.Role, "outer_boundary_candidate", StringComparison.OrdinalIgnoreCase)))
|
|
{
|
|
dims.Add(new SketchDimension
|
|
{
|
|
Id = $"dim_outer_{edge.Id}",
|
|
Kind = "outer_span",
|
|
TargetRefs = [edge.Id],
|
|
Value = Math.Round(edge.Length, 3),
|
|
Units = sketch.Source.Units,
|
|
Role = edge.Role
|
|
});
|
|
}
|
|
|
|
foreach (var edge in sketch.Edges.Where(e => string.Equals(e.Role, "push_face", StringComparison.OrdinalIgnoreCase)))
|
|
{
|
|
dims.Add(new SketchDimension
|
|
{
|
|
Id = $"dim_push_{edge.Id}",
|
|
Kind = "push_face_height_candidate",
|
|
TargetRefs = [edge.Id],
|
|
Value = Math.Round(edge.Length, 3),
|
|
Units = sketch.Source.Units,
|
|
Role = edge.Role
|
|
});
|
|
}
|
|
|
|
foreach (var edge in sketch.Edges.Where(e => IsSleeveOwner(e.Owner) && IsOutsideAdjacent(sketch, e) && IsVerticalEdge(e)))
|
|
{
|
|
dims.Add(new SketchDimension
|
|
{
|
|
Id = $"dim_exposed_sleeve_end_{edge.Id}",
|
|
Kind = "exposed_sleeve_end_height_candidate",
|
|
TargetRefs = [edge.Id],
|
|
Value = Math.Round(edge.Length, 3),
|
|
Units = sketch.Source.Units,
|
|
Role = "exposed_push_face_candidate"
|
|
});
|
|
}
|
|
|
|
return dims;
|
|
}
|
|
|
|
static bool IsSleeveOwner(string owner)
|
|
{
|
|
return owner.Contains("\u5957\u7b52", StringComparison.OrdinalIgnoreCase) ||
|
|
owner.Contains("\u8f74\u5957", StringComparison.OrdinalIgnoreCase) ||
|
|
owner.Contains("\u886c\u5957", StringComparison.OrdinalIgnoreCase) ||
|
|
owner.Contains("sleeve", StringComparison.OrdinalIgnoreCase) ||
|
|
owner.Contains("bushing", StringComparison.OrdinalIgnoreCase);
|
|
}
|
|
|
|
static bool IsOutsideAdjacent(Canonical2DSketchGraph sketch, SketchDrawableEdge edge)
|
|
{
|
|
return sketch.Relations.Any(r =>
|
|
r.Type == "adjacent_to_outside" &&
|
|
(string.Equals(r.A, edge.Id, StringComparison.OrdinalIgnoreCase) ||
|
|
edge.SourceEdgeIds.Any(id => string.Equals(r.A, id, StringComparison.OrdinalIgnoreCase))));
|
|
}
|
|
|
|
static bool IsVerticalEdge(SketchDrawableEdge edge)
|
|
{
|
|
if (edge.Start.Length < 2 || edge.End.Length < 2)
|
|
return false;
|
|
return Math.Abs(edge.Start[0] - edge.End[0]) <= 0.35 && Math.Abs(edge.Start[1] - edge.End[1]) > 1.0;
|
|
}
|
|
|
|
static double EstimateContactLength(Canonical2DSketchGraph sketch, string aId, string bId)
|
|
{
|
|
if (!TryEdge(sketch, aId, out var a) || !TryEdge(sketch, bId, out var b))
|
|
return 0;
|
|
if (a.StartVertex == b.StartVertex || a.StartVertex == b.EndVertex || a.EndVertex == b.StartVertex || a.EndVertex == b.EndVertex)
|
|
return Math.Round(Math.Min(a.Length, b.Length), 3);
|
|
return Math.Round((a.Length + b.Length) * 0.5, 3);
|
|
}
|
|
|
|
static bool TryEdge(Canonical2DSketchGraph sketch, string id, out SketchDrawableEdge edge)
|
|
{
|
|
edge = sketch.Edges.FirstOrDefault(e =>
|
|
string.Equals(e.Id, id, StringComparison.OrdinalIgnoreCase) ||
|
|
e.SourceEdgeIds.Any(sourceId => string.Equals(sourceId, id, StringComparison.OrdinalIgnoreCase))) ?? new SketchDrawableEdge();
|
|
return !string.IsNullOrWhiteSpace(edge.Id);
|
|
}
|
|
|
|
static string Attr(CanonicalSectionEdge edge, string key)
|
|
{
|
|
if (!edge.Attributes.TryGetValue(key, out var value) || value == null)
|
|
return "";
|
|
return value.ToString() ?? "";
|
|
}
|
|
|
|
static string SanitizeId(string value)
|
|
{
|
|
return string.Concat(value.Select(ch => char.IsLetterOrDigit(ch) ? ch : '_'));
|
|
}
|
|
|
|
static CanonicalSectionBrep BuildCanonicalModelSection(SectionPlaneEvidence plane, List<SectionBodyEvidence> bodies)
|
|
{
|
|
var edges = new List<CanonicalSectionEdge>();
|
|
foreach (var body in bodies)
|
|
{
|
|
foreach (var entity in body.SectionEntities.Where(e => e.EntityKind == "edge"))
|
|
{
|
|
var start3 = GetDoubleArray(entity.Geometry, "curve_start");
|
|
var end3 = GetDoubleArray(entity.Geometry, "curve_end");
|
|
if (start3.Length < 3 || end3.Length < 3)
|
|
continue;
|
|
var start2 = ProjectToSection2D(start3, plane);
|
|
var end2 = ProjectToSection2D(end3, plane);
|
|
edges.Add(new CanonicalSectionEdge
|
|
{
|
|
Id = $"{body.ComponentName}:edge_{entity.EntityIndex}",
|
|
Kind = EdgeKind(entity.Geometry),
|
|
Start = start2,
|
|
End = end2,
|
|
Length = Math.Round(Vec.Norm(Vec.Sub(end2, start2)), 3),
|
|
Attributes = new Dictionary<string, object?>
|
|
{
|
|
["component"] = body.ComponentName,
|
|
["component_category"] = body.Category,
|
|
["provenance"] = entity.Provenance,
|
|
["source"] = "solidworks_section_intersection",
|
|
["is_line"] = entity.Geometry.GetValueOrDefault("is_line"),
|
|
["is_circle"] = entity.Geometry.GetValueOrDefault("is_circle"),
|
|
["is_ellipse"] = entity.Geometry.GetValueOrDefault("is_ellipse")
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
return new CanonicalSectionBrep
|
|
{
|
|
SourceKind = "solidworks_model_section",
|
|
CoordinateSystem = "section_plane_uv_mm",
|
|
Metadata = new Dictionary<string, object?>
|
|
{
|
|
["origin_mm"] = plane.OriginMm,
|
|
["axis_mm"] = plane.AxisMm,
|
|
["normal_mm"] = plane.PlaneNormalMm,
|
|
["u_axis_mm"] = plane.UAxisMm,
|
|
["v_axis_mm"] = plane.VAxisMm
|
|
},
|
|
Edges = edges,
|
|
Annotations = []
|
|
};
|
|
}
|
|
|
|
static double[] ProjectToSection2D(double[] pointMm, SectionPlaneEvidence plane)
|
|
{
|
|
var rel = Vec.Sub(pointMm, plane.OriginMm);
|
|
return [Math.Round(Vec.Dot(rel, plane.UAxisMm), 3), Math.Round(Vec.Dot(rel, plane.VAxisMm), 3)];
|
|
}
|
|
|
|
static string EdgeKind(Dictionary<string, object?> geometry)
|
|
{
|
|
if (geometry.TryGetValue("is_line", out var line) && line is bool isLine && isLine)
|
|
return "line";
|
|
if (geometry.TryGetValue("is_circle", out var circle) && circle is bool isCircle && isCircle)
|
|
return "arc_or_circle";
|
|
if (geometry.TryGetValue("is_ellipse", out var ellipse) && ellipse is bool isEllipse && isEllipse)
|
|
return "ellipse";
|
|
return "curve";
|
|
}
|
|
|
|
static double[] GetDoubleArray(Dictionary<string, object?> data, string key)
|
|
{
|
|
if (!data.TryGetValue(key, out var value) || value == null)
|
|
return [];
|
|
if (value is double[] d)
|
|
return d;
|
|
if (value is JsonElement json && json.ValueKind == JsonValueKind.Array)
|
|
return json.EnumerateArray().Select(e => e.GetDouble()).ToArray();
|
|
if (value is object[] o)
|
|
return o.Select(Convert.ToDouble).ToArray();
|
|
return [];
|
|
}
|
|
|
|
static List<ComponentEvidence> EnumerateAssemblyComponents(
|
|
AssemblyDoc assy,
|
|
ModelDoc2 doc,
|
|
dynamic mathUtility,
|
|
IReadOnlyList<string> purchasedComponentHints,
|
|
IReadOnlyList<string> externalInterfaceRoots,
|
|
Action<string, string> log)
|
|
{
|
|
var result = new List<ComponentEvidence>();
|
|
var rawComponents = assy.GetComponents(false) as object[] ?? [];
|
|
var componentObjects = rawComponents.OfType<Component2>().ToList();
|
|
var purchasedVisibilityRoots = BuildPurchasedVisibilityRootNames(componentObjects, purchasedComponentHints);
|
|
var visibleFaceKeyCache = new Dictionary<string, HashSet<IntPtr>>(StringComparer.OrdinalIgnoreCase);
|
|
var transformSnapshotCache = new Dictionary<string, double[]>(StringComparer.OrdinalIgnoreCase);
|
|
var total = componentObjects.Count;
|
|
var index = 0;
|
|
foreach (var comp in componentObjects)
|
|
{
|
|
index++;
|
|
var name = Safe(() => comp.Name2, "");
|
|
var path = Safe(() => comp.GetPathName(), "");
|
|
if (IsPurchasedVisibilityChild(name, purchasedVisibilityRoots))
|
|
{
|
|
log("enumerate_component_skipped_purchased_child", $"{index}/{total} {ComponentDisplayName(name, path)} parent={ResolvePurchasedVisibilityGroupName(name, purchasedVisibilityRoots)}");
|
|
continue;
|
|
}
|
|
|
|
var category = IsUserPurchasedComponent(name, path, purchasedComponentHints)
|
|
? "user_purchased_component"
|
|
: ClassifyStable(name, path);
|
|
var reviewScope = IsStandardOrPurchasedComponent(category, name, path)
|
|
? "assembly_context_only"
|
|
: "part_design_required";
|
|
var skipDetailedBrep = ShouldSkipDetailedAssemblyBrep(category, name, path);
|
|
var preserveExternalInterfaceFaces = IsUserPurchasedComponent(name, path, externalInterfaceRoots);
|
|
log("enumerate_component", $"{index}/{total} {ComponentDisplayName(name, path)} category={category} review_scope={reviewScope}");
|
|
|
|
var component = new ComponentEvidence { Name = name, Path = path, Category = category, Transform = Safe(() => comp.Transform2 as MathTransform, null) };
|
|
if (skipDetailedBrep)
|
|
{
|
|
var visibilityGroupName = ResolvePurchasedVisibilityGroupName(name, purchasedVisibilityRoots);
|
|
var visibilityGroupComponents = ComponentsInVisibilityGroup(componentObjects, visibilityGroupName);
|
|
if (visibilityGroupComponents.Count == 0)
|
|
visibilityGroupComponents = [comp];
|
|
|
|
foreach (var body in visibilityGroupComponents.SelectMany(EnumerateComponentBodies))
|
|
component.Bodies.Add(body);
|
|
|
|
var indexedFaces = EnumerateComponentGroupIndexedFaces(visibilityGroupComponents);
|
|
component.BoundingBox = ComponentGroupBox(visibilityGroupComponents) ?? ComponentBoxFromComponent(comp);
|
|
if (preserveExternalInterfaceFaces)
|
|
{
|
|
foreach (var face in indexedFaces)
|
|
{
|
|
var owner = face.OwnerComponent ?? comp;
|
|
var evidence = BuildFaceEvidence(face.Face, face.FaceIndex, owner, GetComponentTransformSnapshot(owner, transformSnapshotCache));
|
|
if (evidence != null)
|
|
component.Faces.Add(evidence);
|
|
}
|
|
log(
|
|
"enumerate_component_external_interface_black_box",
|
|
$"{index}/{total} {ComponentDisplayName(name, path)} internal_pairs=excluded full_leaf_faces={component.Faces.Count} leaf_components={visibilityGroupComponents.Count}");
|
|
}
|
|
else if (component.BoundingBox.HasValue)
|
|
{
|
|
var visibleFaces = SelectMultiViewVisibleFaces(
|
|
doc,
|
|
assy,
|
|
comp,
|
|
component.BoundingBox.Value,
|
|
indexedFaces,
|
|
visibilityGroupComponents,
|
|
visibilityGroupName,
|
|
visibleFaceKeyCache,
|
|
log);
|
|
foreach (var face in visibleFaces)
|
|
{
|
|
var owner = face.OwnerComponent ?? comp;
|
|
var evidence = BuildFaceEvidence(face.Face, face.FaceIndex, owner, GetComponentTransformSnapshot(owner, transformSnapshotCache));
|
|
if (evidence != null)
|
|
component.Faces.Add(evidence);
|
|
}
|
|
}
|
|
|
|
if (component.Faces.Count == 0)
|
|
{
|
|
foreach (var face in SelectOuterEnvelopeIndexedFaces(indexedFaces, component.BoundingBox, mathUtility))
|
|
{
|
|
var owner = face.OwnerComponent ?? comp;
|
|
var evidence = BuildFaceEvidence(face.Face, face.FaceIndex, owner, GetComponentTransformSnapshot(owner, transformSnapshotCache));
|
|
if (evidence != null)
|
|
component.Faces.Add(evidence);
|
|
}
|
|
}
|
|
|
|
component.Cylinders.AddRange(component.Faces.Where(f => f.Kind == "cylinder"));
|
|
|
|
log("enumerate_component_visible_faces_purchased", $"{index}/{total} {ComponentDisplayName(name, path)} scope=overall_envelope_only bbox={(component.BoundingBox.HasValue ? "yes" : "no")} bodies={component.Bodies.Count} envelope_faces={component.Faces.Count} candidate_face_refs={indexedFaces.Count}");
|
|
result.Add(component);
|
|
continue;
|
|
}
|
|
|
|
foreach (var body in EnumerateComponentBodies(comp))
|
|
component.Bodies.Add(body);
|
|
var componentTransformSnapshot = GetComponentTransformSnapshot(comp, transformSnapshotCache);
|
|
foreach (var body in component.Bodies)
|
|
component.Faces.AddRange(EnumerateFaces(body, comp, componentTransformSnapshot));
|
|
component.Cylinders.AddRange(component.Faces.Where(f => f.Kind == "cylinder"));
|
|
component.BoundingBox = ComponentBox(component) ?? ComponentBoxFromComponent(comp);
|
|
log("enumerate_component_completed", $"{index}/{total} {ComponentDisplayName(name, path)} bodies={component.Bodies.Count} faces={component.Faces.Count}");
|
|
result.Add(component);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
static List<ComponentEvidence> EnumeratePartComponents(ModelDoc2 doc, dynamic mathUtility, IReadOnlyList<string> purchasedComponentHints)
|
|
{
|
|
var result = new List<ComponentEvidence>();
|
|
if (doc is not PartDoc part)
|
|
return result;
|
|
var name = Safe(() => doc.GetTitle(), "");
|
|
var path = Safe(() => doc.GetPathName(), "");
|
|
var category = IsUserPurchasedComponent(name, path, purchasedComponentHints)
|
|
? "user_purchased_component"
|
|
: ClassifyStable(name, path);
|
|
var component = new ComponentEvidence { Name = name, Path = path, Category = category };
|
|
foreach (object bodyObj in ToObjectArray(part.GetBodies2((int)swBodyType_e.swSolidBody, true)))
|
|
{
|
|
if (bodyObj is Body2 body)
|
|
{
|
|
component.Bodies.Add(body);
|
|
component.Faces.AddRange(EnumerateFaces(body, null, []));
|
|
}
|
|
}
|
|
component.Cylinders.AddRange(component.Faces.Where(f => f.Kind == "cylinder"));
|
|
component.BoundingBox = ComponentBox(component);
|
|
result.Add(component);
|
|
return result;
|
|
}
|
|
|
|
static List<string> BuildPurchasedVisibilityRootNames(IReadOnlyList<Component2> components, IReadOnlyList<string> purchasedComponentHints)
|
|
{
|
|
return components
|
|
.Select(component =>
|
|
{
|
|
var name = Safe(() => component.Name2, "");
|
|
var path = Safe(() => component.GetPathName(), "");
|
|
var category = IsUserPurchasedComponent(name, path, purchasedComponentHints)
|
|
? "user_purchased_component"
|
|
: ClassifyStable(name, path);
|
|
return (Name: name, Path: path, Category: category);
|
|
})
|
|
.Where(item => IsPurchasedVisibilityRoot(item.Category, item.Name, item.Path))
|
|
.Select(item => item.Name)
|
|
.Where(name => !string.IsNullOrWhiteSpace(name))
|
|
.Distinct(StringComparer.OrdinalIgnoreCase)
|
|
.OrderByDescending(name => name.Length)
|
|
.ToList();
|
|
}
|
|
|
|
static bool IsPurchasedVisibilityRoot(string category, string name, string path)
|
|
{
|
|
if (category.Equals("user_purchased_component", StringComparison.OrdinalIgnoreCase))
|
|
return false;
|
|
|
|
if (!IsStandardOrPurchasedComponent(category, name, path))
|
|
return false;
|
|
|
|
var extension = Path.GetExtension(path);
|
|
if (extension.Equals(".SLDASM", StringComparison.OrdinalIgnoreCase))
|
|
return true;
|
|
|
|
var text = $"{name} {Path.GetFileNameWithoutExtension(path)}";
|
|
return ContainsAny(text, "小臂关节装配2", "灏忚噦鍏宠妭瑁呴厤2");
|
|
}
|
|
|
|
static string ResolvePurchasedVisibilityGroupName(string componentName, IReadOnlyList<string> purchasedVisibilityRoots)
|
|
{
|
|
return purchasedVisibilityRoots.FirstOrDefault(root => IsSameOrChildComponentName(componentName, root)) ?? componentName;
|
|
}
|
|
|
|
static bool IsPurchasedVisibilityChild(string componentName, IReadOnlyList<string> purchasedVisibilityRoots)
|
|
{
|
|
return purchasedVisibilityRoots.Any(root =>
|
|
!componentName.Equals(root, StringComparison.OrdinalIgnoreCase) &&
|
|
IsSameOrChildComponentName(componentName, root));
|
|
}
|
|
|
|
static List<Component2> ComponentsInVisibilityGroup(IReadOnlyList<Component2> components, string visibilityGroupName)
|
|
{
|
|
return components
|
|
.Where(component => IsSameOrChildComponentName(Safe(() => component.Name2, ""), visibilityGroupName))
|
|
.ToList();
|
|
}
|
|
|
|
static Box? ComponentGroupBox(IReadOnlyList<Component2> components)
|
|
{
|
|
var boxes = components
|
|
.Select(ComponentBoxFromComponent)
|
|
.Where(box => box.HasValue)
|
|
.Select(box => box!.Value)
|
|
.ToList();
|
|
if (boxes.Count == 0)
|
|
return null;
|
|
|
|
return new Box(
|
|
boxes.Min(box => box.XMin),
|
|
boxes.Min(box => box.YMin),
|
|
boxes.Min(box => box.ZMin),
|
|
boxes.Max(box => box.XMax),
|
|
boxes.Max(box => box.YMax),
|
|
boxes.Max(box => box.ZMax));
|
|
}
|
|
|
|
static List<Body2> EnumerateComponentBodies(Component2 component)
|
|
{
|
|
try
|
|
{
|
|
dynamic dyn = component;
|
|
object info;
|
|
return ToObjectArray(dyn.GetBodies3((int)swBodyType_e.swSolidBody, out info)).OfType<Body2>().ToList();
|
|
}
|
|
catch
|
|
{
|
|
return [];
|
|
}
|
|
}
|
|
|
|
static List<FaceEvidence> EnumerateFaces(Body2 body, Component2? component, double[] transformSnapshot)
|
|
{
|
|
var result = new List<FaceEvidence>();
|
|
foreach (var item in EnumerateIndexedFaces([body], component))
|
|
{
|
|
var evidence = BuildFaceEvidence(item.Face, item.FaceIndex, component, transformSnapshot);
|
|
if (evidence != null)
|
|
result.Add(evidence);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
static List<IndexedFace> EnumerateComponentGroupIndexedFaces(IReadOnlyList<Component2> components)
|
|
{
|
|
var result = new List<IndexedFace>();
|
|
var faceIndex = 0;
|
|
foreach (var component in components)
|
|
{
|
|
foreach (var body in EnumerateComponentBodies(component))
|
|
{
|
|
for (object? faceObj = Safe<object?>(() => body.GetFirstFace(), null); faceObj != null;)
|
|
{
|
|
faceIndex++;
|
|
if (faceObj is Face2 face)
|
|
{
|
|
result.Add(new IndexedFace(faceIndex, face, component));
|
|
faceObj = Safe<object?>(() => face.GetNextFace(), null);
|
|
}
|
|
else
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
static List<IndexedFace> EnumerateIndexedFaces(IEnumerable<Body2> bodies, Component2? ownerComponent)
|
|
{
|
|
var result = new List<IndexedFace>();
|
|
int faceIndex = 0;
|
|
foreach (var body in bodies)
|
|
{
|
|
faceIndex = 0;
|
|
for (object? faceObj = Safe<object?>(() => body.GetFirstFace(), null); faceObj != null;)
|
|
{
|
|
faceIndex++;
|
|
if (faceObj is Face2 face)
|
|
{
|
|
result.Add(new IndexedFace(faceIndex, face, ownerComponent));
|
|
faceObj = Safe<object?>(() => face.GetNextFace(), null);
|
|
}
|
|
else
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
static FaceEvidence? BuildFaceEvidence(Face2 face, int faceIndex, Component2? component, double[] transformSnapshot)
|
|
{
|
|
var surface = Safe(() => face.GetSurface() as Surface, null);
|
|
if (surface == null)
|
|
return null;
|
|
|
|
var box = ToDoubleArray(Safe<object?>(() => face.GetBox(), null)).Take(6).Select(v => v * 1000.0).ToArray();
|
|
if (box.Length < 6)
|
|
return null;
|
|
|
|
double[] centerLocalMm = [(box[0] + box[3]) / 2.0, (box[1] + box[4]) / 2.0, (box[2] + box[5]) / 2.0];
|
|
var centerMm = component == null ? centerLocalMm : TransformPointBySnapshot(transformSnapshot, centerLocalMm);
|
|
var evidence = new FaceEvidence
|
|
{
|
|
FaceIndex = faceIndex,
|
|
Box = Box.FromArray(component == null ? box : TransformBBoxBySnapshot(transformSnapshot, box)),
|
|
AreaMm2 = Safe(() => face.GetArea() * 1_000_000.0, 0),
|
|
CenterMm = centerMm,
|
|
RuntimeFace = face,
|
|
AssemblyTransformSnapshot = transformSnapshot
|
|
};
|
|
|
|
if (Safe(() => surface.IsCylinder(), false))
|
|
{
|
|
var c = ToDoubleArray(GetComProperty(surface, "CylinderParams"));
|
|
if (c.Length >= 7)
|
|
{
|
|
evidence.Kind = "cylinder";
|
|
double[] axisPoint = [c[0] * 1000.0, c[1] * 1000.0, c[2] * 1000.0];
|
|
double[] axis = [c[3], c[4], c[5]];
|
|
evidence.AxisPointMm = component == null ? axisPoint : TransformPointBySnapshot(transformSnapshot, axisPoint);
|
|
evidence.Axis = Vec.Normalize(component == null ? axis : TransformVectorBySnapshot(transformSnapshot, axis));
|
|
evidence.RadiusMm = c[6] * 1000.0;
|
|
}
|
|
}
|
|
else if (Safe(() => surface.IsPlane(), false))
|
|
{
|
|
evidence.Kind = "plane";
|
|
var p = ToDoubleArray(GetComProperty(surface, "PlaneParams"));
|
|
if (p.Length >= 6)
|
|
{
|
|
var normal = component == null ? [p[0], p[1], p[2]] : TransformVectorBySnapshot(transformSnapshot, [p[0], p[1], p[2]]);
|
|
evidence.Normal = Vec.Normalize(normal);
|
|
evidence.RootPointMm = component == null
|
|
? [p[3] * 1000.0, p[4] * 1000.0, p[5] * 1000.0]
|
|
: TransformPointBySnapshot(transformSnapshot, [p[3] * 1000.0, p[4] * 1000.0, p[5] * 1000.0]);
|
|
}
|
|
}
|
|
|
|
return evidence;
|
|
}
|
|
|
|
static FaceTessellation ExtractFaceTessellation(
|
|
Face2 face,
|
|
double[] transformSnapshot,
|
|
int maxTriangles = 512,
|
|
int maxSamples = 320,
|
|
Box? expectedAssemblyBox = null)
|
|
{
|
|
var raw = ToDoubleArray(Safe<object?>(() =>
|
|
{
|
|
dynamic f = face;
|
|
return f.GetTessTriangles(false);
|
|
}, null));
|
|
|
|
if (raw.Length < 9)
|
|
return new FaceTessellation([], []);
|
|
|
|
var rawTriangles = new List<FaceTriangleMm>();
|
|
for (int i = 0; i + 8 < raw.Length; i += 9)
|
|
{
|
|
var a = TessellationPointMm(raw, i);
|
|
var b = TessellationPointMm(raw, i + 3);
|
|
var c = TessellationPointMm(raw, i + 6);
|
|
if (TriangleAreaMm2(a, b, c) > 1e-8)
|
|
rawTriangles.Add(new FaceTriangleMm(a, b, c));
|
|
}
|
|
|
|
var allTriangles = SelectBestAssemblyTessellation(rawTriangles, transformSnapshot, expectedAssemblyBox);
|
|
var triangles = maxTriangles <= 0 || allTriangles.Count <= maxTriangles
|
|
? allTriangles
|
|
: Enumerable.Range(0, maxTriangles)
|
|
.Select(i => allTriangles[Math.Min(allTriangles.Count - 1, (int)Math.Floor(i * allTriangles.Count / (double)maxTriangles))])
|
|
.ToList();
|
|
|
|
if (maxSamples <= 0)
|
|
return new FaceTessellation([], triangles);
|
|
|
|
var samples = new List<double[]>();
|
|
var seen = new HashSet<string>(StringComparer.Ordinal);
|
|
foreach (var triangle in triangles)
|
|
{
|
|
AddSample(triangle.A);
|
|
AddSample(triangle.B);
|
|
AddSample(triangle.C);
|
|
AddSample(Vec.Mul(Vec.Add(Vec.Add(triangle.A, triangle.B), triangle.C), 1.0 / 3.0));
|
|
}
|
|
|
|
if (samples.Count > maxSamples)
|
|
{
|
|
var allSamples = samples;
|
|
samples = Enumerable.Range(0, maxSamples)
|
|
.Select(i => allSamples[Math.Min(allSamples.Count - 1, (int)Math.Floor(i * allSamples.Count / (double)maxSamples))])
|
|
.ToList();
|
|
}
|
|
|
|
return new FaceTessellation(samples, triangles);
|
|
|
|
void AddSample(double[] point)
|
|
{
|
|
var key = $"{Math.Round(point[0], 3)}:{Math.Round(point[1], 3)}:{Math.Round(point[2], 3)}";
|
|
if (seen.Add(key))
|
|
samples.Add(point);
|
|
}
|
|
}
|
|
|
|
static double[] TessellationPointMm(double[] raw, int index)
|
|
{
|
|
return [raw[index] * 1000.0, raw[index + 1] * 1000.0, raw[index + 2] * 1000.0];
|
|
}
|
|
|
|
static List<FaceTriangleMm> SelectBestAssemblyTessellation(
|
|
List<FaceTriangleMm> rawTriangles,
|
|
double[] transformSnapshot,
|
|
Box? expectedAssemblyBox)
|
|
{
|
|
if (rawTriangles.Count == 0 || transformSnapshot.Length < 13)
|
|
return rawTriangles;
|
|
|
|
var transformedTriangles = rawTriangles
|
|
.Select(triangle => new FaceTriangleMm(
|
|
TransformPointBySnapshot(transformSnapshot, triangle.A),
|
|
TransformPointBySnapshot(transformSnapshot, triangle.B),
|
|
TransformPointBySnapshot(transformSnapshot, triangle.C)))
|
|
.ToList();
|
|
|
|
if (!expectedAssemblyBox.HasValue)
|
|
return transformedTriangles;
|
|
|
|
var rawScore = TessellationBoxScore(rawTriangles, expectedAssemblyBox.Value);
|
|
var transformedScore = TessellationBoxScore(transformedTriangles, expectedAssemblyBox.Value);
|
|
return rawScore <= transformedScore ? rawTriangles : transformedTriangles;
|
|
}
|
|
|
|
static double TessellationBoxScore(IReadOnlyList<FaceTriangleMm> triangles, Box expected)
|
|
{
|
|
if (triangles.Count == 0)
|
|
return double.PositiveInfinity;
|
|
|
|
var actual = TriangleBox(triangles);
|
|
var expectedDiagonal = Math.Max(1.0, Math.Sqrt(
|
|
Math.Pow(expected.XMax - expected.XMin, 2.0) +
|
|
Math.Pow(expected.YMax - expected.YMin, 2.0) +
|
|
Math.Pow(expected.ZMax - expected.ZMin, 2.0)));
|
|
var cornerDelta =
|
|
Math.Abs(actual.XMin - expected.XMin) + Math.Abs(actual.YMin - expected.YMin) + Math.Abs(actual.ZMin - expected.ZMin) +
|
|
Math.Abs(actual.XMax - expected.XMax) + Math.Abs(actual.YMax - expected.YMax) + Math.Abs(actual.ZMax - expected.ZMax);
|
|
var separation =
|
|
Math.Max(0, Math.Max(expected.XMin - actual.XMax, actual.XMin - expected.XMax)) +
|
|
Math.Max(0, Math.Max(expected.YMin - actual.YMax, actual.YMin - expected.YMax)) +
|
|
Math.Max(0, Math.Max(expected.ZMin - actual.ZMax, actual.ZMin - expected.ZMax));
|
|
return cornerDelta / expectedDiagonal + separation * 10.0 / expectedDiagonal;
|
|
}
|
|
|
|
static Box TriangleBox(IReadOnlyList<FaceTriangleMm> triangles)
|
|
{
|
|
var points = triangles.SelectMany(triangle => new[] { triangle.A, triangle.B, triangle.C }).ToList();
|
|
return new Box(
|
|
points.Min(point => point[0]),
|
|
points.Min(point => point[1]),
|
|
points.Min(point => point[2]),
|
|
points.Max(point => point[0]),
|
|
points.Max(point => point[1]),
|
|
points.Max(point => point[2]));
|
|
}
|
|
|
|
static double TriangleAreaMm2(double[] a, double[] b, double[] c) =>
|
|
Vec.Norm(Vec.Cross(Vec.Sub(b, a), Vec.Sub(c, a))) * 0.5;
|
|
|
|
static void EnsureFaceTessellation(FaceEvidence face)
|
|
{
|
|
if (face.TessellationAttempted)
|
|
return;
|
|
|
|
face.TessellationAttempted = true;
|
|
if (face.RuntimeFace == null)
|
|
return;
|
|
|
|
try
|
|
{
|
|
var tessellation = ExtractFaceTessellation(face.RuntimeFace, face.AssemblyTransformSnapshot, expectedAssemblyBox: face.Box);
|
|
face.SamplePointsMm = tessellation.SamplePointsMm;
|
|
face.TrianglesMm = tessellation.TrianglesMm;
|
|
}
|
|
catch
|
|
{
|
|
face.SamplePointsMm = [];
|
|
face.TrianglesMm = [];
|
|
}
|
|
}
|
|
|
|
static AssemblyFrameEvidence InferAssemblyFrame(List<ComponentEvidence> components, double[] shaftAxis, double[]? upAxis)
|
|
{
|
|
if (upAxis is { Length: >= 3 })
|
|
{
|
|
return new AssemblyFrameEvidence
|
|
{
|
|
ShaftAxisMm = Vec.Round(Vec.Normalize(shaftAxis)),
|
|
VerticalAxisMm = Vec.Round(Vec.Normalize(upAxis)),
|
|
Inference = "vertical axis configured by --up-axis"
|
|
};
|
|
}
|
|
|
|
double[] vertical = [0.0, 1.0, 0.0];
|
|
var upper = components.FirstOrDefault(c => ContainsAny(c.Name, "\u4e0a\u7bb1\u4f53", "\u4e0a"));
|
|
var lower = components.FirstOrDefault(c => ContainsAny(c.Name, "\u4e0b\u7bb1\u4f53", "\u4e0b"));
|
|
if (upper?.BoundingBox != null && lower?.BoundingBox != null)
|
|
{
|
|
var delta = Vec.Sub(BoxCenter(upper.BoundingBox.Value), BoxCenter(lower.BoundingBox.Value));
|
|
int axisIndex = AbsMaxIndex(delta);
|
|
vertical = UnitAxis(axisIndex, Math.Sign(delta[axisIndex]) == 0 ? 1.0 : Math.Sign(delta[axisIndex]));
|
|
}
|
|
else
|
|
{
|
|
var spans = ComponentBBox(components);
|
|
var candidates = new[]
|
|
{
|
|
new { Axis = new[] { 1.0, 0.0, 0.0 }, Span = spans.XMax - spans.XMin },
|
|
new { Axis = new[] { 0.0, 1.0, 0.0 }, Span = spans.YMax - spans.YMin },
|
|
new { Axis = new[] { 0.0, 0.0, 1.0 }, Span = spans.ZMax - spans.ZMin }
|
|
};
|
|
vertical = candidates
|
|
.OrderBy(c => Math.Abs(Vec.Dot(c.Axis, shaftAxis)))
|
|
.ThenByDescending(c => c.Span)
|
|
.First()
|
|
.Axis;
|
|
}
|
|
|
|
return new AssemblyFrameEvidence
|
|
{
|
|
ShaftAxisMm = Vec.Round(Vec.Normalize(shaftAxis)),
|
|
VerticalAxisMm = Vec.Round(Vec.Normalize(vertical)),
|
|
Inference = upper != null && lower != null
|
|
? "vertical axis inferred from upper/lower housing component positions"
|
|
: "vertical axis inferred from global axis least aligned with main shaft axis"
|
|
};
|
|
}
|
|
|
|
static List<AssemblyFaceContactEvidence> BuildAssemblyFaceContacts(
|
|
List<ComponentEvidence> components,
|
|
AssemblyFrameEvidence frame,
|
|
Action<string, string> log)
|
|
{
|
|
var contacts = new List<AssemblyFaceContactEvidence>();
|
|
const double boxToleranceMm = 1.0;
|
|
var candidatePairs = new List<(ComponentEvidence A, ComponentEvidence B)>();
|
|
|
|
for (int i = 0; i < components.Count; i++)
|
|
{
|
|
var a = components[i];
|
|
for (int j = i + 1; j < components.Count; j++)
|
|
{
|
|
var b = components[j];
|
|
if (a.BoundingBox.HasValue && b.BoundingBox.HasValue && !BoxesOverlap(a.BoundingBox.Value, b.BoundingBox.Value, 5.0))
|
|
continue;
|
|
|
|
candidatePairs.Add((a, b));
|
|
}
|
|
}
|
|
|
|
for (var pairIndex = 0; pairIndex < candidatePairs.Count; pairIndex++)
|
|
{
|
|
var (a, b) = candidatePairs[pairIndex];
|
|
var pairCylinderCandidates = 0;
|
|
var pairCylinderCheapCandidates = 0;
|
|
var pairCylinderConfirmed = 0;
|
|
var pairCylinderRejectReasons = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
|
|
log("verify_trimmed_contact_pair",
|
|
$"pair={pairIndex + 1}/{candidatePairs.Count}; a={a.Name}; b={b.Name}; faces={a.Faces.Count}x{b.Faces.Count}; confirmed={contacts.Count}");
|
|
try
|
|
{
|
|
foreach (var af in a.Faces)
|
|
foreach (var bf in b.Faces)
|
|
{
|
|
var faceBoxesOverlap = BoxesOverlap(af.Box, bf.Box, boxToleranceMm);
|
|
|
|
if (faceBoxesOverlap)
|
|
{
|
|
var plane = TryBuildPlanarContact(a, af, b, bf, frame);
|
|
if (plane != null)
|
|
{
|
|
contacts.Add(plane);
|
|
continue;
|
|
}
|
|
}
|
|
|
|
var bothCylinders = af.Kind == "cylinder" && bf.Kind == "cylinder";
|
|
if (bothCylinders)
|
|
pairCylinderCandidates++;
|
|
|
|
var cylindricalCandidate = IsPotentialCylindricalContactCandidate(af, bf);
|
|
if (cylindricalCandidate)
|
|
pairCylinderCheapCandidates++;
|
|
|
|
if (bothCylinders && (faceBoxesOverlap || cylindricalCandidate))
|
|
{
|
|
var cylinder = TryBuildCylindricalContactWithReason(a, af, b, bf, frame, out var cylinderRejectReason);
|
|
if (cylinder != null)
|
|
{
|
|
contacts.Add(cylinder);
|
|
pairCylinderConfirmed++;
|
|
if (cylindricalCandidate)
|
|
{
|
|
log("verify_cylindrical_contact_candidate_confirmed",
|
|
$"a={a.Name}:face_{af.FaceIndex}; b={b.Name}:face_{bf.FaceIndex}; radius={af.RadiusMm:F4}/{bf.RadiusMm:F4}mm; axis_distance={AxisDistanceMm(af.AxisPointMm!, af.Axis!, bf.AxisPointMm!):F4}mm; axial_overlap={cylinder.AxialOverlapMm:F3}mm; method={cylinder.VerificationMethod}");
|
|
}
|
|
}
|
|
else if (!string.IsNullOrWhiteSpace(cylinderRejectReason))
|
|
{
|
|
pairCylinderRejectReasons[cylinderRejectReason] = pairCylinderRejectReasons.GetValueOrDefault(cylinderRejectReason) + 1;
|
|
if (cylindricalCandidate)
|
|
{
|
|
log("verify_cylindrical_contact_candidate_rejected",
|
|
$"a={a.Name}:face_{af.FaceIndex}; b={b.Name}:face_{bf.FaceIndex}; radius={af.RadiusMm:F4}/{bf.RadiusMm:F4}mm; axis_distance={AxisDistanceMm(af.AxisPointMm!, af.Axis!, bf.AxisPointMm!):F4}mm; reason={cylinderRejectReason}");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
log("verify_trimmed_contact_pair_failed", $"a={a.Name}; b={b.Name}; {ex.Message}");
|
|
}
|
|
if (pairCylinderCandidates > 0 || pairCylinderCheapCandidates > 0 || pairCylinderConfirmed > 0)
|
|
{
|
|
log("verify_cylindrical_contact_pair_summary",
|
|
$"pair={pairIndex + 1}/{candidatePairs.Count}; a={a.Name}; b={b.Name}; cylinder_pairs={pairCylinderCandidates}; cheap_candidates={pairCylinderCheapCandidates}; confirmed={pairCylinderConfirmed}; rejects={string.Join(",", pairCylinderRejectReasons.Select(item => $"{item.Key}:{item.Value}"))}; total_confirmed={contacts.Count}");
|
|
}
|
|
}
|
|
|
|
log("verify_trimmed_contacts_completed", $"component_pairs={candidatePairs.Count}; confirmed_contacts={contacts.Count}");
|
|
|
|
return contacts
|
|
.OrderBy(c => c.ComponentA, StringComparer.OrdinalIgnoreCase)
|
|
.ThenBy(c => c.ComponentB, StringComparer.OrdinalIgnoreCase)
|
|
.ThenByDescending(c => c.Confidence)
|
|
.Take(500)
|
|
.ToList();
|
|
}
|
|
|
|
static Dictionary<string, FaceProcessAnnotation> BuildContactProcessLookup(List<AssemblyFaceContactEvidence> contacts)
|
|
{
|
|
var result = new Dictionary<string, FaceProcessAnnotation>(StringComparer.OrdinalIgnoreCase);
|
|
foreach (var contact in contacts)
|
|
{
|
|
Add(contact.ComponentA, contact.FaceA, new FaceProcessAnnotation
|
|
{
|
|
SurfaceProcessRole = contact.SurfaceProcessRoleA,
|
|
ProcessConfidence = contact.ProcessConfidenceA,
|
|
ProcessBasis = contact.ProcessBasisA,
|
|
MachiningScope = contact.MachiningScopeA
|
|
});
|
|
Add(contact.ComponentB, contact.FaceB, new FaceProcessAnnotation
|
|
{
|
|
SurfaceProcessRole = contact.SurfaceProcessRoleB,
|
|
ProcessConfidence = contact.ProcessConfidenceB,
|
|
ProcessBasis = contact.ProcessBasisB,
|
|
MachiningScope = contact.MachiningScopeB
|
|
});
|
|
}
|
|
|
|
return result;
|
|
|
|
void Add(string component, int faceIndex, FaceProcessAnnotation annotation)
|
|
{
|
|
var key = FaceKey(component, faceIndex);
|
|
if (!result.TryGetValue(key, out var existing) || ProcessRank(annotation.ProcessConfidence) > ProcessRank(existing.ProcessConfidence))
|
|
result[key] = annotation;
|
|
}
|
|
}
|
|
|
|
static List<AssemblyFaceEvidence> BuildAssemblyFaces(List<ComponentEvidence> components, AssemblyFrameEvidence frame, Dictionary<string, FaceProcessAnnotation> contactProcessLookup)
|
|
{
|
|
return components
|
|
.SelectMany(component => component.Faces.Select(face =>
|
|
{
|
|
var standaloneProcess = ClassifyStandaloneProcessRole(component, face, frame);
|
|
var process = contactProcessLookup.TryGetValue(FaceKey(component.Name, face.FaceIndex), out var contactProcess)
|
|
? contactProcess
|
|
: standaloneProcess;
|
|
return new AssemblyFaceEvidence
|
|
{
|
|
ComponentName = component.Name,
|
|
ComponentDisplayName = ComponentDisplayName(component.Name, component.Path),
|
|
ComponentPath = component.Path,
|
|
ComponentFileName = string.IsNullOrWhiteSpace(component.Path) ? "" : Path.GetFileName(component.Path),
|
|
ComponentCategory = component.Category,
|
|
FaceIndex = face.FaceIndex,
|
|
FaceKind = face.Kind,
|
|
AreaMm2 = Math.Round(face.AreaMm2, 3),
|
|
CenterMm = Vec.Round(face.CenterMm),
|
|
BBoxMm = face.Box.ToArrayRounded(),
|
|
Normal = Vec.Round(face.Normal),
|
|
RootPointMm = Vec.Round(face.RootPointMm),
|
|
AxisPointMm = Vec.Round(face.AxisPointMm),
|
|
Axis = Vec.Round(face.Axis),
|
|
RadiusMm = Math.Round(face.RadiusMm, 4),
|
|
DiameterMm = Math.Round(face.RadiusMm * 2.0, 4),
|
|
OrientationRole = ClassifyFaceOrientation(face, frame),
|
|
FunctionalRole = ClassifyFaceFunctionalRole(component, face, frame),
|
|
SurfaceProcessRole = process.SurfaceProcessRole,
|
|
ProcessConfidence = process.ProcessConfidence,
|
|
ProcessBasis = process.ProcessBasis,
|
|
MachiningScope = process.MachiningScope
|
|
};
|
|
}))
|
|
.OrderBy(f => f.ComponentName, StringComparer.OrdinalIgnoreCase)
|
|
.ThenBy(f => f.FaceIndex)
|
|
.ToList();
|
|
}
|
|
|
|
static AssemblyFaceContactEvidence? TryBuildPlanarContact(ComponentEvidence a, FaceEvidence af, ComponentEvidence b, FaceEvidence bf, AssemblyFrameEvidence frame)
|
|
{
|
|
if (af.Kind != "plane" || bf.Kind != "plane" || af.Normal == null || bf.Normal == null || af.RootPointMm == null || bf.RootPointMm == null)
|
|
return null;
|
|
|
|
var normalDot = Vec.Dot(af.Normal, bf.Normal);
|
|
if (Math.Abs(normalDot) < 0.985)
|
|
return null;
|
|
|
|
var gap = Math.Abs(Vec.Dot(Vec.Sub(bf.RootPointMm, af.RootPointMm), af.Normal));
|
|
if (gap > 0.15)
|
|
return null;
|
|
|
|
var overlap = EstimateProjectedBoxOverlap(af.Box, bf.Box, af.Normal);
|
|
if (overlap.AreaMm2 < 1.0)
|
|
return null;
|
|
var trimmedVerification = VerifyPlanarTrimmedPatchOverlap(af, bf);
|
|
if (!trimmedVerification.Verified)
|
|
return null;
|
|
|
|
var opposingBonus = normalDot < -0.985 ? 0.1 : 0.0;
|
|
var confidence = Math.Clamp(0.78 + opposingBonus - gap * 0.2 + Math.Min(trimmedVerification.OverlapMeasure, 2500.0) / 25000.0, 0.0, 0.97);
|
|
return new AssemblyFaceContactEvidence
|
|
{
|
|
Id = $"contact_{SanitizeId(a.Name)}_face_{af.FaceIndex}__{SanitizeId(b.Name)}_face_{bf.FaceIndex}",
|
|
ContactKind = "planar_face_contact_candidate",
|
|
ComponentA = a.Name,
|
|
ComponentDisplayNameA = ComponentDisplayName(a.Name, a.Path),
|
|
ComponentPathA = a.Path,
|
|
ComponentFileNameA = string.IsNullOrWhiteSpace(a.Path) ? "" : Path.GetFileName(a.Path),
|
|
ComponentCategoryA = a.Category,
|
|
FaceA = af.FaceIndex,
|
|
FaceKindA = af.Kind,
|
|
ComponentB = b.Name,
|
|
ComponentDisplayNameB = ComponentDisplayName(b.Name, b.Path),
|
|
ComponentPathB = b.Path,
|
|
ComponentFileNameB = string.IsNullOrWhiteSpace(b.Path) ? "" : Path.GetFileName(b.Path),
|
|
ComponentCategoryB = b.Category,
|
|
FaceB = bf.FaceIndex,
|
|
FaceKindB = bf.Kind,
|
|
MateRole = ClassifyMateRole(a, af, b, bf, "planar", frame),
|
|
FaceRoleA = ClassifyContactFaceRole(a, af, "planar", frame),
|
|
FaceRoleB = ClassifyContactFaceRole(b, bf, "planar", frame),
|
|
OrientationRoleA = ClassifyFaceOrientation(af, frame),
|
|
OrientationRoleB = ClassifyFaceOrientation(bf, frame),
|
|
SurfaceProcessRoleA = ClassifyContactProcessRole(a, af, "planar", ClassifyMateRole(a, af, b, bf, "planar", frame)).SurfaceProcessRole,
|
|
SurfaceProcessRoleB = ClassifyContactProcessRole(b, bf, "planar", ClassifyMateRole(a, af, b, bf, "planar", frame)).SurfaceProcessRole,
|
|
ProcessConfidenceA = ClassifyContactProcessRole(a, af, "planar", ClassifyMateRole(a, af, b, bf, "planar", frame)).ProcessConfidence,
|
|
ProcessConfidenceB = ClassifyContactProcessRole(b, bf, "planar", ClassifyMateRole(a, af, b, bf, "planar", frame)).ProcessConfidence,
|
|
ProcessBasisA = ClassifyContactProcessRole(a, af, "planar", ClassifyMateRole(a, af, b, bf, "planar", frame)).ProcessBasis,
|
|
ProcessBasisB = ClassifyContactProcessRole(b, bf, "planar", ClassifyMateRole(a, af, b, bf, "planar", frame)).ProcessBasis,
|
|
MachiningScopeA = ClassifyContactProcessRole(a, af, "planar", ClassifyMateRole(a, af, b, bf, "planar", frame)).MachiningScope,
|
|
MachiningScopeB = ClassifyContactProcessRole(b, bf, "planar", ClassifyMateRole(a, af, b, bf, "planar", frame)).MachiningScope,
|
|
GapMm = Math.Round(gap, 4),
|
|
NormalDot = Math.Round(normalDot, 4),
|
|
OverlapMeasureMm2 = Math.Round(trimmedVerification.OverlapMeasure, 3),
|
|
TrimmedPatchVerified = true,
|
|
VerificationStatus = "trimmed_interface_confirmed",
|
|
VerificationMethod = trimmedVerification.Method,
|
|
VerificationSampleHits = trimmedVerification.SampleHits,
|
|
VerificationMinDistanceMm = Math.Round(trimmedVerification.MinDistanceMm, 4),
|
|
Confidence = Math.Round(confidence, 3),
|
|
Evidence = "coplanar/opposite planar faces with verified overlap between tessellated trimmed face domains",
|
|
CenterA = Vec.Round(af.CenterMm),
|
|
CenterB = Vec.Round(bf.CenterMm),
|
|
NormalA = Vec.Round(af.Normal),
|
|
NormalB = Vec.Round(bf.Normal),
|
|
BBoxA = af.Box.ToArrayRounded(),
|
|
BBoxB = bf.Box.ToArrayRounded(),
|
|
RuntimeFaceA = af.RuntimeFace,
|
|
RuntimeFaceB = bf.RuntimeFace
|
|
};
|
|
}
|
|
|
|
static AssemblyFaceContactEvidence? TryBuildCylindricalContact(ComponentEvidence a, FaceEvidence af, ComponentEvidence b, FaceEvidence bf, AssemblyFrameEvidence frame) =>
|
|
TryBuildCylindricalContactWithReason(a, af, b, bf, frame, out _);
|
|
|
|
static AssemblyFaceContactEvidence? TryBuildCylindricalContactWithReason(ComponentEvidence a, FaceEvidence af, ComponentEvidence b, FaceEvidence bf, AssemblyFrameEvidence frame, out string rejectReason)
|
|
{
|
|
rejectReason = "";
|
|
if (af.Kind != "cylinder" || bf.Kind != "cylinder" ||
|
|
af.Axis == null || bf.Axis == null || af.AxisPointMm == null || bf.AxisPointMm == null)
|
|
{
|
|
rejectReason = "not_cylinder_or_axis_missing";
|
|
return null;
|
|
}
|
|
|
|
var axisDotAbs = Math.Abs(Vec.Dot(af.Axis, bf.Axis));
|
|
if (axisDotAbs < 0.9999)
|
|
{
|
|
rejectReason = "axis_not_parallel";
|
|
return null;
|
|
}
|
|
|
|
var axisDistance = AxisDistanceMm(af.AxisPointMm, af.Axis, bf.AxisPointMm);
|
|
var radiusGap = Math.Abs(af.RadiusMm - bf.RadiusMm);
|
|
if (axisDistance > 0.15 || radiusGap > 0.15)
|
|
{
|
|
rejectReason = "axis_or_radius_gap";
|
|
return null;
|
|
}
|
|
|
|
var ar = ProjectBoxRange(af.Box, af.Axis);
|
|
var br = ProjectBoxRange(bf.Box, af.Axis);
|
|
var axialOverlap = Math.Min(ar.Max, br.Max) - Math.Max(ar.Min, br.Min);
|
|
if (axialOverlap <= 0.05)
|
|
{
|
|
rejectReason = "axial_range_no_overlap";
|
|
return null;
|
|
}
|
|
|
|
var trimmedVerification = VerifyCylindricalTrimmedParamDomainOverlap(af, bf);
|
|
if (!trimmedVerification.Verified)
|
|
{
|
|
rejectReason = trimmedVerification.Method;
|
|
return null;
|
|
}
|
|
|
|
var confidence = Math.Clamp(0.80 - axisDistance * 0.15 - radiusGap * 0.15 + Math.Min(axialOverlap, 50.0) / 500.0, 0.0, 0.98);
|
|
return new AssemblyFaceContactEvidence
|
|
{
|
|
Id = $"contact_{SanitizeId(a.Name)}_face_{af.FaceIndex}__{SanitizeId(b.Name)}_face_{bf.FaceIndex}",
|
|
ContactKind = "cylindrical_coaxial_fit_candidate",
|
|
ComponentA = a.Name,
|
|
ComponentDisplayNameA = ComponentDisplayName(a.Name, a.Path),
|
|
ComponentPathA = a.Path,
|
|
ComponentFileNameA = string.IsNullOrWhiteSpace(a.Path) ? "" : Path.GetFileName(a.Path),
|
|
ComponentCategoryA = a.Category,
|
|
FaceA = af.FaceIndex,
|
|
FaceKindA = af.Kind,
|
|
ComponentB = b.Name,
|
|
ComponentDisplayNameB = ComponentDisplayName(b.Name, b.Path),
|
|
ComponentPathB = b.Path,
|
|
ComponentFileNameB = string.IsNullOrWhiteSpace(b.Path) ? "" : Path.GetFileName(b.Path),
|
|
ComponentCategoryB = b.Category,
|
|
FaceB = bf.FaceIndex,
|
|
FaceKindB = bf.Kind,
|
|
MateRole = ClassifyMateRole(a, af, b, bf, "cylindrical", frame),
|
|
FaceRoleA = ClassifyContactFaceRole(a, af, "cylindrical", frame),
|
|
FaceRoleB = ClassifyContactFaceRole(b, bf, "cylindrical", frame),
|
|
OrientationRoleA = ClassifyFaceOrientation(af, frame),
|
|
OrientationRoleB = ClassifyFaceOrientation(bf, frame),
|
|
SurfaceProcessRoleA = ClassifyContactProcessRole(a, af, "cylindrical", ClassifyMateRole(a, af, b, bf, "cylindrical", frame)).SurfaceProcessRole,
|
|
SurfaceProcessRoleB = ClassifyContactProcessRole(b, bf, "cylindrical", ClassifyMateRole(a, af, b, bf, "cylindrical", frame)).SurfaceProcessRole,
|
|
ProcessConfidenceA = ClassifyContactProcessRole(a, af, "cylindrical", ClassifyMateRole(a, af, b, bf, "cylindrical", frame)).ProcessConfidence,
|
|
ProcessConfidenceB = ClassifyContactProcessRole(b, bf, "cylindrical", ClassifyMateRole(a, af, b, bf, "cylindrical", frame)).ProcessConfidence,
|
|
ProcessBasisA = ClassifyContactProcessRole(a, af, "cylindrical", ClassifyMateRole(a, af, b, bf, "cylindrical", frame)).ProcessBasis,
|
|
ProcessBasisB = ClassifyContactProcessRole(b, bf, "cylindrical", ClassifyMateRole(a, af, b, bf, "cylindrical", frame)).ProcessBasis,
|
|
MachiningScopeA = ClassifyContactProcessRole(a, af, "cylindrical", ClassifyMateRole(a, af, b, bf, "cylindrical", frame)).MachiningScope,
|
|
MachiningScopeB = ClassifyContactProcessRole(b, bf, "cylindrical", ClassifyMateRole(a, af, b, bf, "cylindrical", frame)).MachiningScope,
|
|
GapMm = Math.Round(Math.Max(axisDistance, radiusGap), 4),
|
|
NormalDot = Math.Round(double.IsFinite(trimmedVerification.RepresentativeNormalDot) ? trimmedVerification.RepresentativeNormalDot : axisDotAbs, 4),
|
|
RadiusAmm = Math.Round(af.RadiusMm, 4),
|
|
RadiusBmm = Math.Round(bf.RadiusMm, 4),
|
|
AxisDistanceMm = Math.Round(axisDistance, 4),
|
|
AxialOverlapMm = Math.Round(axialOverlap, 3),
|
|
OverlapMeasureMm2 = Math.Round(trimmedVerification.OverlapMeasure, 3),
|
|
TrimmedPatchVerified = true,
|
|
VerificationStatus = trimmedVerification.Status,
|
|
VerificationMethod = trimmedVerification.Method,
|
|
VerificationSampleHits = trimmedVerification.SampleHits,
|
|
VerificationMinDistanceMm = Math.Round(trimmedVerification.MinDistanceMm, 4),
|
|
Confidence = Math.Round(confidence, 3),
|
|
Evidence = "coaxial cylindrical faces with compatible radius and at least one 2 mm axial slice whose SolidWorks-trimmed angular intervals intersect",
|
|
CenterA = Vec.Round(af.CenterMm),
|
|
CenterB = Vec.Round(bf.CenterMm),
|
|
AxisA = Vec.Round(af.Axis),
|
|
AxisB = Vec.Round(bf.Axis),
|
|
BBoxA = af.Box.ToArrayRounded(),
|
|
BBoxB = bf.Box.ToArrayRounded(),
|
|
RuntimeFaceA = af.RuntimeFace,
|
|
RuntimeFaceB = bf.RuntimeFace
|
|
};
|
|
}
|
|
|
|
static bool IsPotentialCylindricalContactCandidate(FaceEvidence af, FaceEvidence bf)
|
|
{
|
|
if (af.Kind != "cylinder" || bf.Kind != "cylinder" ||
|
|
af.Axis == null || bf.Axis == null || af.AxisPointMm == null || bf.AxisPointMm == null)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var axisDotAbs = Math.Abs(Vec.Dot(af.Axis, bf.Axis));
|
|
if (axisDotAbs < 0.9999)
|
|
return false;
|
|
|
|
var axisDistance = AxisDistanceMm(af.AxisPointMm, af.Axis, bf.AxisPointMm);
|
|
var radiusGap = Math.Abs(af.RadiusMm - bf.RadiusMm);
|
|
if (axisDistance > 0.15 || radiusGap > 0.15)
|
|
return false;
|
|
|
|
var ar = ProjectBoxRange(af.Box, af.Axis);
|
|
var br = ProjectBoxRange(bf.Box, af.Axis);
|
|
var axialOverlap = Math.Min(ar.Max, br.Max) - Math.Max(ar.Min, br.Min);
|
|
return axialOverlap > 0.05;
|
|
}
|
|
|
|
static string ClassifyMateRole(ComponentEvidence a, FaceEvidence af, ComponentEvidence b, FaceEvidence bf, string contactKind, AssemblyFrameEvidence frame)
|
|
{
|
|
bool aHousing = a.Category == "housing";
|
|
bool bHousing = b.Category == "housing";
|
|
bool aEndCap = a.Category == "end_cap";
|
|
bool bEndCap = b.Category == "end_cap";
|
|
bool aInspection = a.Category == "inspection_cover";
|
|
bool bInspection = b.Category == "inspection_cover";
|
|
|
|
if (contactKind == "cylindrical")
|
|
{
|
|
if ((aEndCap && bHousing) || (bEndCap && aHousing))
|
|
return "end_cap_register_fit";
|
|
if (aHousing && bHousing)
|
|
return "split_housing_bearing_seat_alignment";
|
|
return "cylindrical_fit";
|
|
}
|
|
|
|
if (aHousing && bHousing)
|
|
return "housing_split_joint";
|
|
if ((aEndCap && bHousing) || (bEndCap && aHousing))
|
|
return "end_cap_mounting_face";
|
|
if ((aInspection && bHousing) || (bInspection && aHousing))
|
|
return "inspection_cover_mounting_face";
|
|
return "planar_mating_face";
|
|
}
|
|
|
|
static FaceProcessAnnotation ClassifyContactProcessRole(ComponentEvidence component, FaceEvidence face, string contactKind, string mateRole)
|
|
{
|
|
return mateRole switch
|
|
{
|
|
"end_cap_register_fit" => new FaceProcessAnnotation
|
|
{
|
|
SurfaceProcessRole = "precision_machined_mating_surface",
|
|
ProcessConfidence = "high",
|
|
ProcessBasis = "end cap register fit: functional locating cylindrical interface",
|
|
MachiningScope = "full_contact_face"
|
|
},
|
|
"split_housing_bearing_seat_alignment" => new FaceProcessAnnotation
|
|
{
|
|
SurfaceProcessRole = "precision_machined_bearing_seat_surface",
|
|
ProcessConfidence = "high",
|
|
ProcessBasis = "split housing bearing seat alignment: bearing-seat bore is a precision machined surface",
|
|
MachiningScope = "full_contact_face"
|
|
},
|
|
"housing_split_joint" => new FaceProcessAnnotation
|
|
{
|
|
SurfaceProcessRole = "machined_split_joint_surface",
|
|
ProcessConfidence = "high",
|
|
ProcessBasis = "upper/lower housing split joint requires flatness and repeatable assembly accuracy",
|
|
MachiningScope = "full_contact_face_or_local_joint_pad"
|
|
},
|
|
"end_cap_mounting_face" => new FaceProcessAnnotation
|
|
{
|
|
SurfaceProcessRole = "machined_mounting_surface",
|
|
ProcessConfidence = "high",
|
|
ProcessBasis = "end cap flange mounting face: functional support/sealing/positioning interface",
|
|
MachiningScope = "contact_patch_or_local_boss"
|
|
},
|
|
"inspection_cover_mounting_face" => new FaceProcessAnnotation
|
|
{
|
|
SurfaceProcessRole = "machined_cover_mounting_surface",
|
|
ProcessConfidence = "medium_high",
|
|
ProcessBasis = "inspection cover mounting face: cover support/sealing interface; local pad preferred over full large surface",
|
|
MachiningScope = "contact_patch_or_local_boss"
|
|
},
|
|
"cylindrical_fit" => new FaceProcessAnnotation
|
|
{
|
|
SurfaceProcessRole = "machined_cylindrical_fit_surface",
|
|
ProcessConfidence = "medium_high",
|
|
ProcessBasis = "coaxial cylindrical fit candidate",
|
|
MachiningScope = "full_contact_face"
|
|
},
|
|
_ => new FaceProcessAnnotation
|
|
{
|
|
SurfaceProcessRole = "machined_surface_candidate",
|
|
ProcessConfidence = "medium",
|
|
ProcessBasis = $"{mateRole}: contact is evidence, but process role needs tolerance/PMI or functional confirmation",
|
|
MachiningScope = "contact_patch_unknown"
|
|
}
|
|
};
|
|
}
|
|
|
|
static FaceProcessAnnotation ClassifyStandaloneProcessRole(ComponentEvidence component, FaceEvidence face, AssemblyFrameEvidence frame)
|
|
{
|
|
var functionalRole = ClassifyFaceFunctionalRole(component, face, frame);
|
|
var orientation = ClassifyFaceOrientation(face, frame);
|
|
|
|
if (functionalRole is "housing_bearing_seat_or_bore" or "end_cap_register_or_bore")
|
|
{
|
|
return new FaceProcessAnnotation
|
|
{
|
|
SurfaceProcessRole = "machined_surface_candidate",
|
|
ProcessConfidence = "medium",
|
|
ProcessBasis = $"{functionalRole}: precision role inferred from geometry/category, but no explicit contact or PMI evidence",
|
|
MachiningScope = "full_functional_face"
|
|
};
|
|
}
|
|
|
|
if (component.Category == "housing" && face.Kind == "plane" && orientation == "side_face")
|
|
{
|
|
return new FaceProcessAnnotation
|
|
{
|
|
SurfaceProcessRole = "unmachined_cast_surface_candidate",
|
|
ProcessConfidence = "medium",
|
|
ProcessBasis = "large housing side face without contact/fit evidence is likely cast surface rather than machined surface",
|
|
MachiningScope = "not_applicable"
|
|
};
|
|
}
|
|
|
|
if (component.Category == "housing" && face.Kind == "plane" && orientation is "upward_facing_face" or "downward_facing_face")
|
|
{
|
|
return new FaceProcessAnnotation
|
|
{
|
|
SurfaceProcessRole = "unknown_process_surface",
|
|
ProcessConfidence = "low",
|
|
ProcessBasis = "horizontal housing face may contain local machined pads, but full-face machining cannot be inferred without contact/pad evidence",
|
|
MachiningScope = "unknown_local_pad_or_full_face"
|
|
};
|
|
}
|
|
|
|
if (face.Kind == "cylinder" && face.RadiusMm > 0)
|
|
{
|
|
return new FaceProcessAnnotation
|
|
{
|
|
SurfaceProcessRole = "machined_feature_candidate",
|
|
ProcessConfidence = "low",
|
|
ProcessBasis = "cylindrical hole/boss feature is often produced by machining, but functional precision role is not confirmed",
|
|
MachiningScope = "feature_surface"
|
|
};
|
|
}
|
|
|
|
return new FaceProcessAnnotation
|
|
{
|
|
SurfaceProcessRole = "unknown_process_surface",
|
|
ProcessConfidence = "low",
|
|
ProcessBasis = "no contact, fit, support, sealing, locating, tolerance or PMI evidence",
|
|
MachiningScope = "unknown"
|
|
};
|
|
}
|
|
|
|
static string FaceKey(string componentName, int faceIndex) => $"{componentName}|{faceIndex}";
|
|
|
|
static int ProcessRank(string confidence) => confidence switch
|
|
{
|
|
"high" => 4,
|
|
"medium_high" => 3,
|
|
"medium" => 2,
|
|
"low" => 1,
|
|
_ => 0
|
|
};
|
|
|
|
static string ClassifyContactFaceRole(ComponentEvidence component, FaceEvidence face, string contactKind, AssemblyFrameEvidence frame)
|
|
{
|
|
if (contactKind == "cylindrical")
|
|
{
|
|
if (component.Category == "end_cap")
|
|
return "end_cap_register_cylindrical_face";
|
|
if (component.Category == "housing")
|
|
return "housing_bore_or_register_cylindrical_face";
|
|
return "cylindrical_mating_face";
|
|
}
|
|
|
|
var orientation = ClassifyFaceOrientation(face, frame);
|
|
if (component.Category == "end_cap")
|
|
return orientation == "downward_facing_face" ? "end_cap_bottom_mating_face" : "end_cap_planar_mating_face";
|
|
if (component.Category == "inspection_cover")
|
|
return orientation == "downward_facing_face" ? "cover_bottom_mating_face" : "cover_planar_mating_face";
|
|
if (component.Category == "housing")
|
|
return orientation switch
|
|
{
|
|
"upward_facing_face" => "housing_top_or_support_face",
|
|
"downward_facing_face" => "housing_bottom_or_split_face",
|
|
"side_face" => "housing_side_face",
|
|
"axial_end_face" => "housing_axial_end_face",
|
|
_ => "housing_planar_mating_face"
|
|
};
|
|
return "planar_mating_face";
|
|
}
|
|
|
|
static string ClassifyFaceFunctionalRole(ComponentEvidence component, FaceEvidence face, AssemblyFrameEvidence frame)
|
|
{
|
|
if (face.Kind == "cylinder")
|
|
{
|
|
if (component.Category == "end_cap" && IsParallel(face.Axis, frame.ShaftAxisMm, 0.85))
|
|
return "end_cap_register_or_bore";
|
|
if (component.Category == "housing" && IsParallel(face.Axis, frame.ShaftAxisMm, 0.85))
|
|
return "housing_bearing_seat_or_bore";
|
|
return "cylindrical_feature";
|
|
}
|
|
|
|
if (face.Kind == "plane")
|
|
{
|
|
var orientation = ClassifyFaceOrientation(face, frame);
|
|
if (component.Category == "inspection_cover" && orientation == "downward_facing_face")
|
|
return "cover_bottom_mounting_face";
|
|
if (component.Category == "end_cap" && orientation == "axial_end_face")
|
|
return "end_cap_axial_mounting_face";
|
|
if (component.Category == "housing" && orientation == "side_face")
|
|
return "housing_side_wall_face";
|
|
if (component.Category == "housing" && orientation is "upward_facing_face" or "downward_facing_face")
|
|
return "housing_horizontal_face";
|
|
return "planar_feature";
|
|
}
|
|
|
|
return "unknown";
|
|
}
|
|
|
|
static string ClassifyFaceOrientation(FaceEvidence face, AssemblyFrameEvidence frame)
|
|
{
|
|
if (face.Kind == "plane" && face.Normal != null)
|
|
{
|
|
var vertical = Vec.Normalize(frame.VerticalAxisMm);
|
|
var shaft = Vec.Normalize(frame.ShaftAxisMm);
|
|
var vDot = Vec.Dot(face.Normal, vertical);
|
|
if (vDot > 0.85)
|
|
return "upward_facing_face";
|
|
if (vDot < -0.85)
|
|
return "downward_facing_face";
|
|
if (Math.Abs(Vec.Dot(face.Normal, shaft)) > 0.85)
|
|
return "axial_end_face";
|
|
return "side_face";
|
|
}
|
|
|
|
if (face.Kind == "cylinder" && face.Axis != null)
|
|
{
|
|
if (IsParallel(face.Axis, frame.ShaftAxisMm, 0.85))
|
|
return "shaft_axis_cylindrical_face";
|
|
if (IsParallel(face.Axis, frame.VerticalAxisMm, 0.85))
|
|
return "vertical_axis_cylindrical_face";
|
|
return "side_axis_cylindrical_face";
|
|
}
|
|
|
|
return "unknown";
|
|
}
|
|
|
|
static bool IsParallel(double[]? a, double[]? b, double tolerance)
|
|
{
|
|
if (a == null || b == null || a.Length < 3 || b.Length < 3)
|
|
return false;
|
|
return Math.Abs(Vec.Dot(Vec.Normalize(a), Vec.Normalize(b))) >= tolerance;
|
|
}
|
|
|
|
static List<AxisGroupEvidence> BuildAxisGroups(List<ComponentEvidence> components)
|
|
{
|
|
var cylinders = components.SelectMany(c => c.Cylinders.Select(f => (Component: c, Face: f))).ToList();
|
|
var groups = new List<AxisGroupEvidence>();
|
|
var used = new HashSet<string>();
|
|
int index = 0;
|
|
|
|
for (int i = 0; i < cylinders.Count; i++)
|
|
{
|
|
var seed = cylinders[i];
|
|
var key = $"{seed.Component.Name}:{seed.Face.FaceIndex}";
|
|
if (used.Contains(key))
|
|
continue;
|
|
|
|
var members = new List<(ComponentEvidence Component, FaceEvidence Face)> { seed };
|
|
used.Add(key);
|
|
for (int j = i + 1; j < cylinders.Count; j++)
|
|
{
|
|
var candidate = cylinders[j];
|
|
var candidateKey = $"{candidate.Component.Name}:{candidate.Face.FaceIndex}";
|
|
if (used.Contains(candidateKey))
|
|
continue;
|
|
if (AreSameAxis(seed.Face, candidate.Face))
|
|
{
|
|
members.Add(candidate);
|
|
used.Add(candidateKey);
|
|
}
|
|
}
|
|
|
|
index++;
|
|
groups.Add(new AxisGroupEvidence
|
|
{
|
|
GroupId = $"axis_{index}",
|
|
AxisPointMm = Vec.Round(seed.Face.AxisPointMm),
|
|
Axis = Vec.Round(seed.Face.Axis),
|
|
CylinderCount = members.Count,
|
|
ComponentCount = members.Select(m => m.Component.Name).Distinct(StringComparer.OrdinalIgnoreCase).Count(),
|
|
RadiusMinMm = Math.Round(members.Min(m => m.Face.RadiusMm), 3),
|
|
RadiusMaxMm = Math.Round(members.Max(m => m.Face.RadiusMm), 3),
|
|
Members = members.Select(m => new AxisMemberEvidence
|
|
{
|
|
ComponentName = m.Component.Name,
|
|
ComponentCategory = m.Component.Category,
|
|
FaceIndex = m.Face.FaceIndex,
|
|
RadiusMm = Math.Round(m.Face.RadiusMm, 3),
|
|
AreaMm2 = Math.Round(m.Face.AreaMm2, 3)
|
|
}).ToList()
|
|
});
|
|
}
|
|
|
|
return groups.OrderByDescending(g => g.ComponentCount).ThenByDescending(g => g.CylinderCount).ToList();
|
|
}
|
|
|
|
static bool AreSameAxis(FaceEvidence a, FaceEvidence b)
|
|
{
|
|
if (a.Axis == null || b.Axis == null || a.AxisPointMm == null || b.AxisPointMm == null)
|
|
return false;
|
|
if (Math.Abs(Vec.Dot(a.Axis, b.Axis)) < 0.985)
|
|
return false;
|
|
var delta = Vec.Sub(a.AxisPointMm, b.AxisPointMm);
|
|
var along = Vec.Mul(a.Axis, Vec.Dot(delta, a.Axis));
|
|
var radial = Vec.Sub(delta, along);
|
|
var tolerance = Math.Max(1.0, Math.Min(a.RadiusMm, b.RadiusMm) * 0.08);
|
|
return Vec.Norm(radial) <= tolerance;
|
|
}
|
|
|
|
static bool BoxesOverlap(Box a, Box b, double toleranceMm)
|
|
{
|
|
return a.XMin <= b.XMax + toleranceMm && a.XMax + toleranceMm >= b.XMin &&
|
|
a.YMin <= b.YMax + toleranceMm && a.YMax + toleranceMm >= b.YMin &&
|
|
a.ZMin <= b.ZMax + toleranceMm && a.ZMax + toleranceMm >= b.ZMin;
|
|
}
|
|
|
|
static Box ComponentBBox(List<ComponentEvidence> components)
|
|
{
|
|
var boxes = components.Where(c => c.BoundingBox.HasValue).Select(c => c.BoundingBox!.Value).ToList();
|
|
if (boxes.Count == 0)
|
|
return new Box(0, 0, 0, 0, 0, 0);
|
|
return new Box(
|
|
boxes.Min(b => b.XMin),
|
|
boxes.Min(b => b.YMin),
|
|
boxes.Min(b => b.ZMin),
|
|
boxes.Max(b => b.XMax),
|
|
boxes.Max(b => b.YMax),
|
|
boxes.Max(b => b.ZMax));
|
|
}
|
|
|
|
static Box? ComponentBox(ComponentEvidence component)
|
|
{
|
|
if (component.Faces.Count == 0)
|
|
return null;
|
|
return FaceBox(component.Faces);
|
|
}
|
|
|
|
static Box? FaceBox(IReadOnlyList<FaceEvidence> faces)
|
|
{
|
|
if (faces.Count == 0)
|
|
return null;
|
|
return new Box(
|
|
faces.Min(f => f.Box.XMin),
|
|
faces.Min(f => f.Box.YMin),
|
|
faces.Min(f => f.Box.ZMin),
|
|
faces.Max(f => f.Box.XMax),
|
|
faces.Max(f => f.Box.YMax),
|
|
faces.Max(f => f.Box.ZMax));
|
|
}
|
|
|
|
static List<IndexedFace> SelectOuterEnvelopeIndexedFaces(
|
|
IReadOnlyList<IndexedFace> faces,
|
|
Box? componentBox,
|
|
dynamic mathUtility)
|
|
{
|
|
if (!componentBox.HasValue)
|
|
return [];
|
|
|
|
const double toleranceMm = 0.25;
|
|
return faces
|
|
.Where(face =>
|
|
{
|
|
var localBox = ToDoubleArray(Safe<object?>(() => face.Face.GetBox(), null))
|
|
.Take(6)
|
|
.Select(value => value * 1000.0)
|
|
.ToArray();
|
|
if (localBox.Length < 6)
|
|
return false;
|
|
|
|
var assemblyBox = face.OwnerComponent == null
|
|
? localBox
|
|
: TransformBBox(mathUtility, face.OwnerComponent, localBox);
|
|
return assemblyBox.Length >= 6 &&
|
|
TouchesOuterEnvelope(Box.FromArray(assemblyBox), componentBox.Value, toleranceMm);
|
|
})
|
|
.OrderBy(face => face.FaceIndex)
|
|
.ToList();
|
|
}
|
|
|
|
static bool TouchesOuterEnvelope(Box faceBox, Box componentBox, double toleranceMm)
|
|
{
|
|
return faceBox.XMin <= componentBox.XMin + toleranceMm ||
|
|
faceBox.YMin <= componentBox.YMin + toleranceMm ||
|
|
faceBox.ZMin <= componentBox.ZMin + toleranceMm ||
|
|
faceBox.XMax >= componentBox.XMax - toleranceMm ||
|
|
faceBox.YMax >= componentBox.YMax - toleranceMm ||
|
|
faceBox.ZMax >= componentBox.ZMax - toleranceMm;
|
|
}
|
|
|
|
static List<IndexedFace> SelectMultiViewVisibleFaces(
|
|
ModelDoc2 doc,
|
|
AssemblyDoc assembly,
|
|
Component2 target,
|
|
Box visibilityBox,
|
|
IReadOnlyList<IndexedFace> indexedFaces,
|
|
IReadOnlyList<Component2> visibilityGroupComponents,
|
|
string visibilityGroupName,
|
|
IDictionary<string, HashSet<IntPtr>> visibleFaceKeyCache,
|
|
Action<string, string> log)
|
|
{
|
|
if (indexedFaces.Count == 0)
|
|
return [];
|
|
|
|
var selectionManager = Safe(() => doc.SelectionManager as SelectionMgr, null);
|
|
if (selectionManager == null)
|
|
return [];
|
|
|
|
if (!visibleFaceKeyCache.TryGetValue(visibilityGroupName, out var visibleFaceKeys))
|
|
{
|
|
var originalVisibility = new Dictionary<Component2, int>();
|
|
visibleFaceKeys = [];
|
|
var rayCount = 0;
|
|
try
|
|
{
|
|
IsolateComponentsForVisibilitySampling(assembly, visibilityGroupComponents, originalVisibility);
|
|
doc.ClearSelection2(true);
|
|
doc.GraphicsRedraw2();
|
|
|
|
foreach (var direction in MultiViewDirections())
|
|
{
|
|
foreach (var ray in BuildVisibilitySampleRays(visibilityBox, direction))
|
|
{
|
|
rayCount++;
|
|
var faceKey = TrySelectVisibleFaceKeyByRay(doc, selectionManager, visibilityGroupComponents, ray.OriginMm, ray.Direction);
|
|
if (faceKey != IntPtr.Zero)
|
|
visibleFaceKeys.Add(faceKey);
|
|
}
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
log("visible_face_sampling_failed", $"{visibilityGroupName}; {ex.Message}");
|
|
}
|
|
finally
|
|
{
|
|
try { doc.ClearSelection2(true); } catch { }
|
|
RestoreComponentVisibility(originalVisibility);
|
|
}
|
|
|
|
visibleFaceKeyCache[visibilityGroupName] = visibleFaceKeys;
|
|
log("visible_face_sampling_completed", $"{visibilityGroupName}; directions=6; rays={rayCount}; visible_face_keys={visibleFaceKeys.Count}");
|
|
}
|
|
|
|
return indexedFaces
|
|
.Where(face =>
|
|
{
|
|
var key = ComIdentity(face.Face);
|
|
return key != IntPtr.Zero && visibleFaceKeys.Contains(key);
|
|
})
|
|
.OrderBy(face => face.FaceIndex)
|
|
.ToList();
|
|
}
|
|
|
|
static IntPtr TrySelectVisibleFaceKeyByRay(
|
|
ModelDoc2 doc,
|
|
SelectionMgr selectionManager,
|
|
IReadOnlyList<Component2> visibilityGroupComponents,
|
|
double[] originMm,
|
|
double[] direction)
|
|
{
|
|
try
|
|
{
|
|
doc.ClearSelection2(true);
|
|
var selected = doc.Extension.SelectByRay(
|
|
originMm[0] / 1000.0,
|
|
originMm[1] / 1000.0,
|
|
originMm[2] / 1000.0,
|
|
direction[0],
|
|
direction[1],
|
|
direction[2],
|
|
0.00075,
|
|
(int)swSelectType_e.swSelFACES,
|
|
false,
|
|
0,
|
|
0);
|
|
|
|
if (!selected || selectionManager.GetSelectedObjectCount2(-1) <= 0)
|
|
return IntPtr.Zero;
|
|
|
|
var selectedComponentObj = selectionManager.GetSelectedObjectsComponent4(1, -1);
|
|
if (selectedComponentObj is Component2 selectedComponent &&
|
|
!visibilityGroupComponents.Any(component => SameRuntimeComponent(selectedComponent, component)))
|
|
{
|
|
return IntPtr.Zero;
|
|
}
|
|
|
|
var selectedObject = selectionManager.GetSelectedObject6(1, -1);
|
|
if (selectedObject == null)
|
|
return IntPtr.Zero;
|
|
|
|
return ComIdentity(selectedObject);
|
|
}
|
|
catch
|
|
{
|
|
return IntPtr.Zero;
|
|
}
|
|
}
|
|
|
|
static IEnumerable<(double[] OriginMm, double[] Direction)> BuildVisibilitySampleRays(Box box, double[] viewDirection)
|
|
{
|
|
const int samplesPerAxis = 9;
|
|
const double marginMm = 30.0;
|
|
var d = Vec.Normalize(viewDirection);
|
|
var basis = RayPlaneBasis(d);
|
|
var corners = BoxCorners(box);
|
|
var uRange = ProjectionRange(corners, basis.U);
|
|
var vRange = ProjectionRange(corners, basis.V);
|
|
var dRange = ProjectionRange(corners, d);
|
|
var originDepth = dRange.Max + marginMm;
|
|
|
|
for (int i = 0; i < samplesPerAxis; i++)
|
|
{
|
|
var u = Lerp(uRange.Min, uRange.Max, samplesPerAxis == 1 ? 0.5 : i / (double)(samplesPerAxis - 1));
|
|
for (int j = 0; j < samplesPerAxis; j++)
|
|
{
|
|
var v = Lerp(vRange.Min, vRange.Max, samplesPerAxis == 1 ? 0.5 : j / (double)(samplesPerAxis - 1));
|
|
var origin = Vec.Add(Vec.Add(Vec.Mul(basis.U, u), Vec.Mul(basis.V, v)), Vec.Mul(d, originDepth));
|
|
yield return (origin, Vec.Mul(d, -1.0));
|
|
}
|
|
}
|
|
}
|
|
|
|
static List<double[]> MultiViewDirections() =>
|
|
[
|
|
[1.0, 0.0, 0.0],
|
|
[-1.0, 0.0, 0.0],
|
|
[0.0, 1.0, 0.0],
|
|
[0.0, -1.0, 0.0],
|
|
[0.0, 0.0, 1.0],
|
|
[0.0, 0.0, -1.0]
|
|
];
|
|
|
|
static (double[] U, double[] V) RayPlaneBasis(double[] direction)
|
|
{
|
|
var reference = Math.Abs(direction[2]) < 0.9 ? new[] { 0.0, 0.0, 1.0 } : [0.0, 1.0, 0.0];
|
|
var u = Vec.Normalize(Vec.Cross(reference, direction));
|
|
var v = Vec.Normalize(Vec.Cross(direction, u));
|
|
return (u, v);
|
|
}
|
|
|
|
static List<double[]> BoxCorners(Box box) =>
|
|
[
|
|
[box.XMin, box.YMin, box.ZMin],
|
|
[box.XMin, box.YMin, box.ZMax],
|
|
[box.XMin, box.YMax, box.ZMin],
|
|
[box.XMin, box.YMax, box.ZMax],
|
|
[box.XMax, box.YMin, box.ZMin],
|
|
[box.XMax, box.YMin, box.ZMax],
|
|
[box.XMax, box.YMax, box.ZMin],
|
|
[box.XMax, box.YMax, box.ZMax]
|
|
];
|
|
|
|
static (double Min, double Max) ProjectionRange(IReadOnlyList<double[]> points, double[] axis)
|
|
{
|
|
var values = points.Select(point => Vec.Dot(point, axis)).ToList();
|
|
return (values.Min(), values.Max());
|
|
}
|
|
|
|
static double Lerp(double a, double b, double t) => a + (b - a) * t;
|
|
|
|
static void IsolateComponentsForVisibilitySampling(
|
|
AssemblyDoc assembly,
|
|
IReadOnlyList<Component2> visibleComponents,
|
|
IDictionary<Component2, int> originalVisibility)
|
|
{
|
|
var allComponents = (assembly.GetComponents(false) as object[] ?? [])
|
|
.OfType<Component2>()
|
|
.ToList();
|
|
|
|
foreach (var component in allComponents)
|
|
{
|
|
int original = swComponentVisibilityState_e.swComponentUnknown.GetHashCode();
|
|
try { original = component.Visible; } catch { }
|
|
originalVisibility[component] = original;
|
|
|
|
component.Visible = visibleComponents.Any(visible => SameRuntimeComponent(component, visible))
|
|
? (int)swComponentVisibilityState_e.swComponentVisible
|
|
: (int)swComponentVisibilityState_e.swComponentHidden;
|
|
}
|
|
}
|
|
|
|
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
|
|
{
|
|
}
|
|
}
|
|
}
|
|
|
|
static bool SameRuntimeComponent(Component2 a, Component2 b)
|
|
{
|
|
if (ReferenceEquals(a, b))
|
|
return true;
|
|
|
|
var aKey = ComIdentity(a);
|
|
var bKey = ComIdentity(b);
|
|
if (aKey != IntPtr.Zero && bKey != IntPtr.Zero && aKey == bKey)
|
|
return true;
|
|
|
|
var aName = Safe(() => a.Name2, "");
|
|
var bName = Safe(() => b.Name2, "");
|
|
var aPath = Safe(() => a.GetPathName(), "");
|
|
var bPath = Safe(() => b.GetPathName(), "");
|
|
return SameComponent(aName, bName, aPath, bPath);
|
|
}
|
|
|
|
static IntPtr ComIdentity(object value)
|
|
{
|
|
IntPtr unknown = IntPtr.Zero;
|
|
try
|
|
{
|
|
unknown = Marshal.GetIUnknownForObject(value);
|
|
return unknown;
|
|
}
|
|
catch
|
|
{
|
|
return IntPtr.Zero;
|
|
}
|
|
finally
|
|
{
|
|
if (unknown != IntPtr.Zero)
|
|
Marshal.Release(unknown);
|
|
}
|
|
}
|
|
|
|
static Box? ComponentBoxFromComponent(Component2 component)
|
|
{
|
|
try
|
|
{
|
|
dynamic dyn = component;
|
|
var values = ToDoubleArray(dyn.GetBox(false, false)).Take(6).Select(v => v * 1000.0).ToArray();
|
|
if (values.Length < 6 || values.Any(v => double.IsNaN(v) || double.IsInfinity(v)))
|
|
return null;
|
|
return Box.FromArray(values);
|
|
}
|
|
catch
|
|
{
|
|
return null;
|
|
}
|
|
}
|
|
|
|
static double[] BoxCenter(Box box) =>
|
|
[
|
|
(box.XMin + box.XMax) / 2.0,
|
|
(box.YMin + box.YMax) / 2.0,
|
|
(box.ZMin + box.ZMax) / 2.0
|
|
];
|
|
|
|
static int AbsMaxIndex(double[] values)
|
|
{
|
|
int index = 0;
|
|
double best = Math.Abs(values[0]);
|
|
for (int i = 1; i < values.Length; i++)
|
|
{
|
|
var current = Math.Abs(values[i]);
|
|
if (current > best)
|
|
{
|
|
best = current;
|
|
index = i;
|
|
}
|
|
}
|
|
return index;
|
|
}
|
|
|
|
static double[] UnitAxis(int axisIndex, double sign)
|
|
{
|
|
var axis = new[] { 0.0, 0.0, 0.0 };
|
|
axis[Math.Clamp(axisIndex, 0, 2)] = sign >= 0 ? 1.0 : -1.0;
|
|
return axis;
|
|
}
|
|
|
|
static (double AreaMm2, double UOverlapMm, double VOverlapMm) EstimateProjectedBoxOverlap(Box a, Box b, double[] normal)
|
|
{
|
|
var (u, v) = PlaneBasis(normal);
|
|
var ar = ProjectBox2D(a, u, v);
|
|
var br = ProjectBox2D(b, u, v);
|
|
var uOverlap = Math.Min(ar.UMax, br.UMax) - Math.Max(ar.UMin, br.UMin);
|
|
var vOverlap = Math.Min(ar.VMax, br.VMax) - Math.Max(ar.VMin, br.VMin);
|
|
if (uOverlap <= 0 || vOverlap <= 0)
|
|
return (0, 0, 0);
|
|
return (uOverlap * vOverlap, uOverlap, vOverlap);
|
|
}
|
|
|
|
static (double[] U, double[] V) PlaneBasis(double[] normal)
|
|
{
|
|
var n = Vec.Normalize(normal);
|
|
var seed = Math.Abs(n[2]) < 0.9 ? new[] { 0.0, 0.0, 1.0 } : new[] { 0.0, 1.0, 0.0 };
|
|
var u = Vec.Normalize(Vec.Cross(seed, n));
|
|
var v = Vec.Normalize(Vec.Cross(n, u));
|
|
return (u, v);
|
|
}
|
|
|
|
static (double UMin, double UMax, double VMin, double VMax) ProjectBox2D(Box box, double[] u, double[] v)
|
|
{
|
|
var points = Corners(box).ToList();
|
|
var us = points.Select(p => Vec.Dot(p, u)).ToList();
|
|
var vs = points.Select(p => Vec.Dot(p, v)).ToList();
|
|
return (us.Min(), us.Max(), vs.Min(), vs.Max());
|
|
}
|
|
|
|
static TrimmedPatchVerification VerifyPlanarTrimmedPatchOverlap(FaceEvidence a, FaceEvidence b)
|
|
{
|
|
EnsureFaceTessellation(a);
|
|
EnsureFaceTessellation(b);
|
|
if (a.TrianglesMm.Count == 0 || b.TrianglesMm.Count == 0 || a.Normal == null)
|
|
return TrimmedPatchVerification.Unverified("planar_trimmed_mesh_unavailable");
|
|
|
|
var (u, v) = PlaneBasis(a.Normal);
|
|
var minimumOverlapArea = Math.Max(0.05, Math.Min(1.0, Math.Min(a.AreaMm2, b.AreaMm2) * 0.0005));
|
|
var overlapArea = 0.0;
|
|
var overlapPairs = 0;
|
|
var projectedA = a.TrianglesMm
|
|
.Select(triangle => ProjectTriangle2D(triangle, u, v))
|
|
.Select(polygon => (Polygon: polygon, Box: PolygonBox2D(polygon)))
|
|
.ToList();
|
|
var projectedB = b.TrianglesMm
|
|
.Select(triangle => ProjectTriangle2D(triangle, u, v))
|
|
.Select(polygon => (Polygon: polygon, Box: PolygonBox2D(polygon)))
|
|
.ToList();
|
|
foreach (var triangleA in projectedA)
|
|
{
|
|
foreach (var triangleB in projectedB)
|
|
{
|
|
if (!BoxesOverlap2D(triangleA.Box, triangleB.Box, 1e-6))
|
|
continue;
|
|
|
|
var area = ConvexPolygonIntersectionArea(triangleA.Polygon, triangleB.Polygon);
|
|
if (area <= 1e-8)
|
|
continue;
|
|
overlapArea += area;
|
|
overlapPairs++;
|
|
if (overlapArea >= minimumOverlapArea)
|
|
{
|
|
var minDistance = MinimumTessellationDistanceMm(a, b);
|
|
return new TrimmedPatchVerification(
|
|
true,
|
|
"trimmed_interface_confirmed",
|
|
"planar_tessellated_trimmed_domain_intersection",
|
|
overlapArea,
|
|
overlapPairs,
|
|
minDistance);
|
|
}
|
|
}
|
|
}
|
|
|
|
return new TrimmedPatchVerification(
|
|
false,
|
|
"extension_only_rejected",
|
|
"planar_tessellated_trimmed_domain_intersection",
|
|
overlapArea,
|
|
overlapPairs,
|
|
double.PositiveInfinity);
|
|
}
|
|
|
|
static TrimmedPatchVerification VerifyCylindricalTrimmedParamDomainOverlap(FaceEvidence a, FaceEvidence b)
|
|
{
|
|
if (a.Axis == null || a.AxisPointMm == null || b.Axis == null || b.AxisPointMm == null)
|
|
return TrimmedPatchVerification.Unverified("cylindrical_analytic_axis_unavailable");
|
|
|
|
var commonAxis = Vec.Normalize(a.Axis);
|
|
var commonOrigin = a.AxisPointMm;
|
|
if (!TryBuildCylindricalTrimmedParamDomain(a, commonOrigin, commonAxis, out var domainA) ||
|
|
!TryBuildCylindricalTrimmedParamDomain(b, commonOrigin, commonAxis, out var domainB))
|
|
{
|
|
return TrimmedPatchVerification.Unverified("cylindrical_trimmed_param_domain_unavailable");
|
|
}
|
|
|
|
var axialOverlap = Math.Min(domainA.AxialMaxMm, domainB.AxialMaxMm) -
|
|
Math.Max(domainA.AxialMinMm, domainB.AxialMinMm);
|
|
if (axialOverlap <= 0.05)
|
|
{
|
|
return new TrimmedPatchVerification(
|
|
false,
|
|
"trimmed_domain_rejected",
|
|
"cylindrical_trimmed_param_axial_ranges_do_not_overlap",
|
|
0,
|
|
0,
|
|
Math.Abs(a.RadiusMm - b.RadiusMm));
|
|
}
|
|
|
|
var normalDot = domainA.NormalSense * domainB.NormalSense;
|
|
var axialMin = Math.Max(domainA.AxialMinMm, domainB.AxialMinMm);
|
|
var axialMax = Math.Min(domainA.AxialMaxMm, domainB.AxialMaxMm);
|
|
var testedSlices = 0;
|
|
for (var bandStart = axialMin; bandStart < axialMax - 1e-9; bandStart += CylindricalParamDomainAxialStepMm)
|
|
{
|
|
var bandEnd = Math.Min(axialMax, bandStart + CylindricalParamDomainAxialStepMm);
|
|
if (bandEnd - bandStart <= 1e-9)
|
|
continue;
|
|
|
|
var sampleT = (bandStart + bandEnd) * 0.5;
|
|
testedSlices++;
|
|
var intervalsA = CylindricalAngularIntervalsAtT(domainA, sampleT);
|
|
if (intervalsA.Count == 0)
|
|
continue;
|
|
var intervalsB = CylindricalAngularIntervalsAtT(domainB, sampleT);
|
|
if (intervalsB.Count == 0 || !AngularIntervalsOverlap(intervalsA, intervalsB))
|
|
continue;
|
|
|
|
if (!double.IsFinite(normalDot) || normalDot > -0.5)
|
|
{
|
|
return new TrimmedPatchVerification(
|
|
false,
|
|
"trimmed_domain_rejected",
|
|
"cylindrical_trimmed_face_angular_intersection_but_normals_not_opposed",
|
|
0,
|
|
1,
|
|
Math.Abs(a.RadiusMm - b.RadiusMm),
|
|
normalDot);
|
|
}
|
|
|
|
return new TrimmedPatchVerification(
|
|
true,
|
|
"trimmed_cylindrical_domain_confirmed",
|
|
"cylindrical_trimmed_face_2mm_slice_angular_intersection",
|
|
0,
|
|
1,
|
|
Math.Abs(a.RadiusMm - b.RadiusMm),
|
|
normalDot);
|
|
}
|
|
|
|
return new TrimmedPatchVerification(
|
|
false,
|
|
"trimmed_domain_rejected",
|
|
testedSlices == 0
|
|
? "cylindrical_trimmed_face_no_testable_axial_slice"
|
|
: "cylindrical_trimmed_face_no_angular_intersection_in_2mm_slices",
|
|
0,
|
|
0,
|
|
Math.Abs(a.RadiusMm - b.RadiusMm),
|
|
normalDot);
|
|
}
|
|
|
|
static bool TryBuildCylindricalTrimmedParamDomain(
|
|
FaceEvidence face,
|
|
double[] commonOriginMm,
|
|
double[] commonAxis,
|
|
out CylindricalTrimmedParamDomain domain)
|
|
{
|
|
domain = new CylindricalTrimmedParamDomain(0, 0, face, [], [], [], [], 1.0, []);
|
|
if (face.RuntimeFace == null || face.AxisPointMm == null || face.Axis == null || face.RadiusMm <= 1e-8)
|
|
return false;
|
|
|
|
var axisRange = ProjectBoxRange(face.Box, commonAxis);
|
|
var originProjection = Vec.Dot(commonOriginMm, commonAxis);
|
|
var axialMin = axisRange.Min - originProjection;
|
|
var axialMax = axisRange.Max - originProjection;
|
|
if (!double.IsFinite(axialMin) || !double.IsFinite(axialMax) || axialMax - axialMin <= 1e-8)
|
|
return false;
|
|
|
|
var (u, v) = PlaneBasis(commonAxis);
|
|
var faceAxisOriginAtCommonT0 = Vec.Add(
|
|
face.AxisPointMm,
|
|
Vec.Mul(commonAxis, Vec.Dot(Vec.Sub(commonOriginMm, face.AxisPointMm), commonAxis)));
|
|
domain = new CylindricalTrimmedParamDomain(
|
|
axialMin,
|
|
axialMax,
|
|
face,
|
|
commonAxis,
|
|
faceAxisOriginAtCommonT0,
|
|
u,
|
|
v,
|
|
CylindricalFaceSense(face),
|
|
ExtractCylindricalBoundarySegments(face, commonOriginMm, commonAxis, u, v));
|
|
return true;
|
|
}
|
|
|
|
static double CylindricalFaceSense(FaceEvidence face)
|
|
{
|
|
if (face.RuntimeFace == null)
|
|
return 1.0;
|
|
|
|
try
|
|
{
|
|
return face.RuntimeFace.FaceInSurfaceSense() ? -1.0 : 1.0;
|
|
}
|
|
catch
|
|
{
|
|
return 1.0;
|
|
}
|
|
}
|
|
|
|
static List<CylindricalBoundarySegment> ExtractCylindricalBoundarySegments(
|
|
FaceEvidence face,
|
|
double[] commonOriginMm,
|
|
double[] commonAxis,
|
|
double[] u,
|
|
double[] v)
|
|
{
|
|
var segments = new List<CylindricalBoundarySegment>();
|
|
if (face.RuntimeFace == null)
|
|
return segments;
|
|
|
|
try
|
|
{
|
|
foreach (var loopObject in ToObjectArray(face.RuntimeFace.GetLoops()))
|
|
{
|
|
if (loopObject is not Loop2 loop)
|
|
continue;
|
|
foreach (var edgeObject in ToObjectArray(loop.GetEdges()))
|
|
{
|
|
if (edgeObject is not Edge edge)
|
|
continue;
|
|
var curveParams = Safe(() => edge.GetCurveParams3(), null);
|
|
if (curveParams == null)
|
|
continue;
|
|
var parameterMin = curveParams.UMinValue;
|
|
var parameterMax = curveParams.UMaxValue;
|
|
if (!double.IsFinite(parameterMin) || !double.IsFinite(parameterMax))
|
|
continue;
|
|
|
|
CylindricalBoundaryPoint? previous = null;
|
|
for (var sampleIndex = 0; sampleIndex <= CylindricalBoundarySamplesPerEdge; sampleIndex++)
|
|
{
|
|
var fraction = sampleIndex / (double)CylindricalBoundarySamplesPerEdge;
|
|
var parameter = parameterMin + (parameterMax - parameterMin) * fraction;
|
|
var evaluated = ToDoubleArray(Safe<object?>(() => edge.Evaluate2(parameter, 0), null));
|
|
if (evaluated.Length < 3)
|
|
continue;
|
|
var localPointMm = new[] { evaluated[0] * 1000.0, evaluated[1] * 1000.0, evaluated[2] * 1000.0 };
|
|
var assemblyPointMm = TransformPointBySnapshot(face.AssemblyTransformSnapshot, localPointMm);
|
|
if (!TryMapPointToCylindricalCoordinates(assemblyPointMm, commonOriginMm, commonAxis, u, v, out var current))
|
|
continue;
|
|
current = current with { PointMm = assemblyPointMm };
|
|
|
|
if (previous.HasValue && Vec.Norm(Vec.Sub(current.PointMm, previous.Value.PointMm)) > 1e-7)
|
|
{
|
|
var thetaEnd = previous.Value.ThetaRadians +
|
|
NormalizeSignedAngleRadians(current.ThetaRadians - previous.Value.ThetaRadians);
|
|
segments.Add(new CylindricalBoundarySegment(
|
|
previous.Value.ThetaRadians,
|
|
thetaEnd,
|
|
previous.Value.AxialMm,
|
|
current.AxialMm));
|
|
}
|
|
previous = current;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
catch
|
|
{
|
|
}
|
|
return segments;
|
|
}
|
|
|
|
static bool TryMapPointToCylindricalCoordinates(
|
|
double[] pointMm,
|
|
double[] originMm,
|
|
double[] axis,
|
|
double[] u,
|
|
double[] v,
|
|
out CylindricalBoundaryPoint point)
|
|
{
|
|
point = default;
|
|
var relative = Vec.Sub(pointMm, originMm);
|
|
var axial = Vec.Dot(relative, axis);
|
|
var radial = Vec.Sub(relative, Vec.Mul(axis, axial));
|
|
if (Vec.Norm(radial) <= 1e-8)
|
|
return false;
|
|
point = new CylindricalBoundaryPoint(
|
|
NormalizeAngleRadians(Math.Atan2(Vec.Dot(radial, v), Vec.Dot(radial, u))),
|
|
axial,
|
|
pointMm);
|
|
return true;
|
|
}
|
|
|
|
static List<AngularInterval> CylindricalAngularIntervalsAtT(CylindricalTrimmedParamDomain domain, double axialMm)
|
|
{
|
|
var fullTurn = Math.PI * 2.0;
|
|
var cuts = new List<double>(domain.BoundarySegments.Count + CylindricalFallbackAngularSectors + 2)
|
|
{
|
|
0.0,
|
|
fullTurn
|
|
};
|
|
for (var sector = 1; sector < CylindricalFallbackAngularSectors; sector++)
|
|
cuts.Add(fullTurn * sector / CylindricalFallbackAngularSectors);
|
|
|
|
foreach (var segment in domain.BoundarySegments)
|
|
{
|
|
var axialDelta = segment.AxialEndMm - segment.AxialStartMm;
|
|
if (Math.Abs(axialDelta) <= 1e-8)
|
|
continue;
|
|
var minAxial = Math.Min(segment.AxialStartMm, segment.AxialEndMm);
|
|
var maxAxial = Math.Max(segment.AxialStartMm, segment.AxialEndMm);
|
|
if (axialMm < minAxial - 1e-7 || axialMm > maxAxial + 1e-7)
|
|
continue;
|
|
var fraction = Math.Clamp((axialMm - segment.AxialStartMm) / axialDelta, 0.0, 1.0);
|
|
var theta = segment.ThetaStartRadians + (segment.ThetaEndRadians - segment.ThetaStartRadians) * fraction;
|
|
cuts.Add(NormalizeAngleRadians(theta));
|
|
}
|
|
|
|
var orderedCuts = new List<double>();
|
|
foreach (var cut in cuts.Where(double.IsFinite).OrderBy(value => value))
|
|
{
|
|
if (orderedCuts.Count == 0 || cut - orderedCuts[^1] > 1e-6)
|
|
orderedCuts.Add(cut);
|
|
}
|
|
|
|
var intervals = new List<AngularInterval>();
|
|
for (var i = 0; i + 1 < orderedCuts.Count; i++)
|
|
{
|
|
var min = orderedCuts[i];
|
|
var max = orderedCuts[i + 1];
|
|
if (max - min <= 1e-6)
|
|
continue;
|
|
if (IsPointOnCylindricalTrimmedFace(domain, axialMm, (min + max) * 0.5))
|
|
intervals.Add(new AngularInterval(min, max));
|
|
}
|
|
return MergeAngularIntervals(intervals);
|
|
}
|
|
|
|
static bool IsPointOnCylindricalTrimmedFace(CylindricalTrimmedParamDomain domain, double axialMm, double thetaRadians)
|
|
{
|
|
var radial = Vec.Add(
|
|
Vec.Mul(domain.U, Math.Cos(thetaRadians)),
|
|
Vec.Mul(domain.V, Math.Sin(thetaRadians)));
|
|
var pointMm = Vec.Add(
|
|
Vec.Add(domain.FaceAxisOriginAtCommonT0Mm, Vec.Mul(domain.CommonAxis, axialMm)),
|
|
Vec.Mul(radial, domain.Face.RadiusMm));
|
|
return DistanceToTrimmedFaceMm(domain.Face, pointMm) <= CylindricalFaceMembershipToleranceMm;
|
|
}
|
|
|
|
static double DistanceToTrimmedFaceMm(FaceEvidence face, double[] assemblyPointMm)
|
|
{
|
|
if (face.RuntimeFace == null)
|
|
return double.PositiveInfinity;
|
|
|
|
var localPointMm = InverseTransformPointBySnapshot(face.AssemblyTransformSnapshot, assemblyPointMm);
|
|
var closest = ToDoubleArray(Safe<object?>(() => face.RuntimeFace.GetClosestPointOn(
|
|
localPointMm[0] / 1000.0,
|
|
localPointMm[1] / 1000.0,
|
|
localPointMm[2] / 1000.0), null));
|
|
var best = ClosestPointDistanceMm(face, assemblyPointMm, closest);
|
|
if (best <= CylindricalFaceMembershipToleranceMm || face.AssemblyTransformSnapshot.Length < 13)
|
|
return best;
|
|
|
|
var assemblyClosest = ToDoubleArray(Safe<object?>(() => face.RuntimeFace.GetClosestPointOn(
|
|
assemblyPointMm[0] / 1000.0,
|
|
assemblyPointMm[1] / 1000.0,
|
|
assemblyPointMm[2] / 1000.0), null));
|
|
return Math.Min(best, ClosestPointDistanceMm(face, assemblyPointMm, assemblyClosest));
|
|
}
|
|
|
|
static double ClosestPointDistanceMm(FaceEvidence face, double[] assemblyPointMm, double[] closest)
|
|
{
|
|
if (closest.Length < 3)
|
|
return double.PositiveInfinity;
|
|
var rawMm = new[] { closest[0] * 1000.0, closest[1] * 1000.0, closest[2] * 1000.0 };
|
|
var transformedMm = TransformPointBySnapshot(face.AssemblyTransformSnapshot, rawMm);
|
|
return Math.Min(
|
|
Vec.Norm(Vec.Sub(assemblyPointMm, transformedMm)),
|
|
Vec.Norm(Vec.Sub(assemblyPointMm, rawMm)));
|
|
}
|
|
|
|
static List<AngularInterval> MergeAngularIntervals(List<AngularInterval> intervals)
|
|
{
|
|
if (intervals.Count == 0)
|
|
return [];
|
|
var ordered = intervals.OrderBy(interval => interval.MinRadians).ToList();
|
|
var merged = new List<AngularInterval>();
|
|
var current = ordered[0];
|
|
for (var i = 1; i < ordered.Count; i++)
|
|
{
|
|
var next = ordered[i];
|
|
if (next.MinRadians <= current.MaxRadians + 1e-6)
|
|
{
|
|
current = new AngularInterval(current.MinRadians, Math.Max(current.MaxRadians, next.MaxRadians));
|
|
continue;
|
|
}
|
|
merged.Add(current);
|
|
current = next;
|
|
}
|
|
merged.Add(current);
|
|
return merged;
|
|
}
|
|
|
|
static bool AngularIntervalsOverlap(IReadOnlyList<AngularInterval> a, IReadOnlyList<AngularInterval> b)
|
|
{
|
|
foreach (var intervalA in a)
|
|
foreach (var intervalB in b)
|
|
{
|
|
if (Math.Min(intervalA.MaxRadians, intervalB.MaxRadians) -
|
|
Math.Max(intervalA.MinRadians, intervalB.MinRadians) > 1e-6)
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
static double NormalizeAngleRadians(double angle)
|
|
{
|
|
var full = Math.PI * 2.0;
|
|
angle %= full;
|
|
return angle < 0 ? angle + full : angle;
|
|
}
|
|
|
|
static double NormalizeSignedAngleRadians(double angle)
|
|
{
|
|
var full = Math.PI * 2.0;
|
|
angle %= full;
|
|
if (angle > Math.PI)
|
|
angle -= full;
|
|
else if (angle < -Math.PI)
|
|
angle += full;
|
|
return angle;
|
|
}
|
|
|
|
static List<double[]> ProjectTriangle2D(FaceTriangleMm triangle, double[] u, double[] v) =>
|
|
[
|
|
[Vec.Dot(triangle.A, u), Vec.Dot(triangle.A, v)],
|
|
[Vec.Dot(triangle.B, u), Vec.Dot(triangle.B, v)],
|
|
[Vec.Dot(triangle.C, u), Vec.Dot(triangle.C, v)]
|
|
];
|
|
|
|
static (double XMin, double XMax, double YMin, double YMax) PolygonBox2D(IReadOnlyList<double[]> polygon) =>
|
|
(
|
|
polygon.Min(point => point[0]),
|
|
polygon.Max(point => point[0]),
|
|
polygon.Min(point => point[1]),
|
|
polygon.Max(point => point[1])
|
|
);
|
|
|
|
static bool BoxesOverlap2D(
|
|
(double XMin, double XMax, double YMin, double YMax) a,
|
|
(double XMin, double XMax, double YMin, double YMax) b,
|
|
double tolerance) =>
|
|
a.XMax + tolerance >= b.XMin && b.XMax + tolerance >= a.XMin &&
|
|
a.YMax + tolerance >= b.YMin && b.YMax + tolerance >= a.YMin;
|
|
|
|
static double ConvexPolygonIntersectionArea(IReadOnlyList<double[]> subject, IReadOnlyList<double[]> clip)
|
|
{
|
|
var output = subject.Select(point => point.ToArray()).ToList();
|
|
var clipSign = SignedArea2D(clip) >= 0 ? 1.0 : -1.0;
|
|
for (var i = 0; i < clip.Count && output.Count > 0; i++)
|
|
{
|
|
var edgeA = clip[i];
|
|
var edgeB = clip[(i + 1) % clip.Count];
|
|
var input = output;
|
|
output = [];
|
|
var previous = input[^1];
|
|
var previousInside = IsInsideClipEdge(previous, edgeA, edgeB, clipSign);
|
|
foreach (var current in input)
|
|
{
|
|
var currentInside = IsInsideClipEdge(current, edgeA, edgeB, clipSign);
|
|
if (currentInside != previousInside && TryLineIntersection2D(previous, current, edgeA, edgeB, out var intersection))
|
|
output.Add(intersection);
|
|
if (currentInside)
|
|
output.Add(current);
|
|
previous = current;
|
|
previousInside = currentInside;
|
|
}
|
|
}
|
|
|
|
return Math.Abs(SignedArea2D(output));
|
|
}
|
|
|
|
static double SignedArea2D(IReadOnlyList<double[]> polygon)
|
|
{
|
|
if (polygon.Count < 3)
|
|
return 0;
|
|
var twiceArea = 0.0;
|
|
for (var i = 0; i < polygon.Count; i++)
|
|
{
|
|
var a = polygon[i];
|
|
var b = polygon[(i + 1) % polygon.Count];
|
|
twiceArea += a[0] * b[1] - a[1] * b[0];
|
|
}
|
|
return twiceArea * 0.5;
|
|
}
|
|
|
|
static bool IsInsideClipEdge(double[] point, double[] edgeA, double[] edgeB, double clipSign) =>
|
|
clipSign * ((edgeB[0] - edgeA[0]) * (point[1] - edgeA[1]) -
|
|
(edgeB[1] - edgeA[1]) * (point[0] - edgeA[0])) >= -1e-9;
|
|
|
|
static bool TryLineIntersection2D(double[] a, double[] b, double[] c, double[] d, out double[] intersection)
|
|
{
|
|
var rx = b[0] - a[0];
|
|
var ry = b[1] - a[1];
|
|
var sx = d[0] - c[0];
|
|
var sy = d[1] - c[1];
|
|
var denominator = rx * sy - ry * sx;
|
|
if (Math.Abs(denominator) < 1e-12)
|
|
{
|
|
intersection = [];
|
|
return false;
|
|
}
|
|
|
|
var t = ((c[0] - a[0]) * sy - (c[1] - a[1]) * sx) / denominator;
|
|
intersection = [a[0] + t * rx, a[1] + t * ry];
|
|
return true;
|
|
}
|
|
|
|
static double MinimumTessellationDistanceMm(FaceEvidence a, FaceEvidence b)
|
|
{
|
|
var minA = MinimumSamplesToTrianglesDistanceMm(a.SamplePointsMm, b.TrianglesMm);
|
|
var minB = MinimumSamplesToTrianglesDistanceMm(b.SamplePointsMm, a.TrianglesMm);
|
|
return Math.Min(minA, minB);
|
|
}
|
|
|
|
static double MinimumSamplesToTrianglesDistanceMm(
|
|
IReadOnlyList<double[]> samples,
|
|
IReadOnlyList<FaceTriangleMm> triangles)
|
|
{
|
|
var minimumSquared = double.PositiveInfinity;
|
|
foreach (var sample in samples)
|
|
foreach (var triangle in triangles)
|
|
{
|
|
var distanceSquared = PointTriangleDistanceSquared(sample, triangle.A, triangle.B, triangle.C);
|
|
if (distanceSquared < minimumSquared)
|
|
minimumSquared = distanceSquared;
|
|
}
|
|
return double.IsFinite(minimumSquared) ? Math.Sqrt(minimumSquared) : double.PositiveInfinity;
|
|
}
|
|
|
|
static double PointTriangleDistanceSquared(double[] point, double[] a, double[] b, double[] c)
|
|
{
|
|
var ab = Vec.Sub(b, a);
|
|
var ac = Vec.Sub(c, a);
|
|
var ap = Vec.Sub(point, a);
|
|
var d1 = Vec.Dot(ab, ap);
|
|
var d2 = Vec.Dot(ac, ap);
|
|
if (d1 <= 0 && d2 <= 0)
|
|
return Vec.Dot(ap, ap);
|
|
|
|
var bp = Vec.Sub(point, b);
|
|
var d3 = Vec.Dot(ab, bp);
|
|
var d4 = Vec.Dot(ac, bp);
|
|
if (d3 >= 0 && d4 <= d3)
|
|
return Vec.Dot(bp, bp);
|
|
|
|
var vc = d1 * d4 - d3 * d2;
|
|
if (vc <= 0 && d1 >= 0 && d3 <= 0)
|
|
{
|
|
var edge = Vec.Add(a, Vec.Mul(ab, d1 / (d1 - d3)));
|
|
var delta = Vec.Sub(point, edge);
|
|
return Vec.Dot(delta, delta);
|
|
}
|
|
|
|
var cp = Vec.Sub(point, c);
|
|
var d5 = Vec.Dot(ab, cp);
|
|
var d6 = Vec.Dot(ac, cp);
|
|
if (d6 >= 0 && d5 <= d6)
|
|
return Vec.Dot(cp, cp);
|
|
|
|
var vb = d5 * d2 - d1 * d6;
|
|
if (vb <= 0 && d2 >= 0 && d6 <= 0)
|
|
{
|
|
var edge = Vec.Add(a, Vec.Mul(ac, d2 / (d2 - d6)));
|
|
var delta = Vec.Sub(point, edge);
|
|
return Vec.Dot(delta, delta);
|
|
}
|
|
|
|
var va = d3 * d6 - d5 * d4;
|
|
if (va <= 0 && d4 - d3 >= 0 && d5 - d6 >= 0)
|
|
{
|
|
var bc = Vec.Sub(c, b);
|
|
var edge = Vec.Add(b, Vec.Mul(bc, (d4 - d3) / ((d4 - d3) + (d5 - d6))));
|
|
var delta = Vec.Sub(point, edge);
|
|
return Vec.Dot(delta, delta);
|
|
}
|
|
|
|
var denominator = 1.0 / (va + vb + vc);
|
|
var closest = Vec.Add(a, Vec.Add(Vec.Mul(ab, vb * denominator), Vec.Mul(ac, vc * denominator)));
|
|
var closestDelta = Vec.Sub(point, closest);
|
|
return Vec.Dot(closestDelta, closestDelta);
|
|
}
|
|
|
|
static (double Min, double Max) ProjectBoxRange(Box box, double[] axis)
|
|
{
|
|
var values = Corners(box).Select(p => Vec.Dot(p, axis)).ToList();
|
|
return (values.Min(), values.Max());
|
|
}
|
|
|
|
static double AxisDistanceMm(double[] pointA, double[] axisA, double[] pointB)
|
|
{
|
|
var delta = Vec.Sub(pointB, pointA);
|
|
var along = Vec.Mul(axisA, Vec.Dot(delta, axisA));
|
|
return Vec.Norm(Vec.Sub(delta, along));
|
|
}
|
|
|
|
static IEnumerable<double[]> Corners(Box b)
|
|
{
|
|
foreach (double x in new[] { b.XMin, b.XMax })
|
|
foreach (double y in new[] { b.YMin, b.YMax })
|
|
foreach (double z in new[] { b.ZMin, b.ZMax })
|
|
yield return [x, y, z];
|
|
}
|
|
|
|
static string ClassifyStable(string name, string path)
|
|
{
|
|
var fileName = Path.GetFileNameWithoutExtension(path);
|
|
var s = $"{name} {fileName}".ToLowerInvariant();
|
|
if (ContainsAny(s, "\u5c0f\u81c2\u5173\u8282\u88c5\u914d2", "小臂关节装配2"))
|
|
return "unknown";
|
|
if (ContainsAny(s, "\u5c0f\u81c2\u5173\u8282\u88c5\u914d2", "小臂关节装配2"))
|
|
return "purchased_motor_reducer_assembly";
|
|
var purchasedExcluded = LooksLikeCustomAdapterPart(s);
|
|
if (!purchasedExcluded && ContainsAny(s, "\u8f74\u627f", "bearing"))
|
|
return "bearing";
|
|
if (!purchasedExcluded && ContainsAny(s, "\u7535\u673a", "\u9a6c\u8fbe", "\u4f3a\u670d", "motor", "servo"))
|
|
return "purchased_motor";
|
|
if (!purchasedExcluded && ContainsAny(s, "\u51cf\u901f\u5668", "\u51cf\u901f\u673a", "\u51cf\u901f\u7535\u673a", "reducer", "gearbox", "gear motor"))
|
|
return "purchased_reducer";
|
|
if (ContainsAny(s, "\u5957\u7b52", "\u8f74\u5957", "\u886c\u5957", "sleeve", "bushing"))
|
|
return "sleeve";
|
|
if (ContainsAny(s, "\u7aa5\u89c6\u5b54\u76d6", "inspection cover", "peephole cover"))
|
|
return "inspection_cover";
|
|
if (ContainsAny(s, "\u7aef\u76d6", "end cap", "end cover", "cover"))
|
|
return "end_cap";
|
|
if (ContainsAny(s, "\u7bb1\u4f53", "\u673a\u5ea7", "\u5b54\u5ea7", "housing", "case"))
|
|
return "housing";
|
|
if (ContainsAny(s, "\u8f74", "shaft"))
|
|
return "shaft";
|
|
return "unknown";
|
|
}
|
|
|
|
static bool IsUserPurchasedComponent(string name, string path, IReadOnlyList<string> hints)
|
|
{
|
|
if (hints.Count == 0)
|
|
return false;
|
|
|
|
var fileName = Path.GetFileNameWithoutExtension(path);
|
|
var candidates = new[]
|
|
{
|
|
NormalizeMatchText(name),
|
|
NormalizeMatchText(fileName),
|
|
NormalizeMatchText(StripSolidWorksInstanceSuffix(name))
|
|
};
|
|
return hints
|
|
.Select(NormalizeMatchText)
|
|
.Where(hint => hint.Length > 0)
|
|
.Any(hint => candidates.Any(candidate => candidate.Equals(hint, StringComparison.OrdinalIgnoreCase)));
|
|
}
|
|
|
|
static bool IsExternalInterfaceTargetRelation(
|
|
string componentA,
|
|
string componentPathA,
|
|
string componentB,
|
|
string componentPathB,
|
|
IReadOnlyList<string> roots)
|
|
{
|
|
var aIsTarget = IsUserPurchasedComponentOrChild(componentA, componentPathA, roots);
|
|
var bIsTarget = IsUserPurchasedComponentOrChild(componentB, componentPathB, roots);
|
|
return aIsTarget ^ bIsTarget;
|
|
}
|
|
|
|
static bool IsUserPurchasedComponentOrChild(string name, string path, IReadOnlyList<string> roots)
|
|
{
|
|
if (IsUserPurchasedComponent(name, path, roots))
|
|
return true;
|
|
|
|
var candidates = new[]
|
|
{
|
|
NormalizeHierarchyMatchText(name),
|
|
NormalizeHierarchyMatchText(StripSolidWorksInstanceSuffix(name)),
|
|
NormalizeHierarchyMatchText(Path.GetFileNameWithoutExtension(path))
|
|
};
|
|
|
|
foreach (var root in roots.Select(NormalizeHierarchyMatchText).Where(root => root.Length > 0))
|
|
{
|
|
if (candidates.Any(candidate =>
|
|
candidate.Equals(root, StringComparison.OrdinalIgnoreCase) ||
|
|
candidate.StartsWith(root + "/", StringComparison.OrdinalIgnoreCase) ||
|
|
candidate.StartsWith(root + "-", StringComparison.OrdinalIgnoreCase)))
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
static string StripSolidWorksInstanceSuffix(string name)
|
|
{
|
|
var value = name.Trim();
|
|
var slash = value.LastIndexOfAny(['/', '\\']);
|
|
var prefix = slash >= 0 ? value[..(slash + 1)] : "";
|
|
var leaf = slash >= 0 ? value[(slash + 1)..] : value;
|
|
|
|
var angleIndex = leaf.LastIndexOf('<');
|
|
if (angleIndex > 0 && leaf.EndsWith(">", StringComparison.Ordinal))
|
|
leaf = leaf[..angleIndex];
|
|
else
|
|
{
|
|
var dash = leaf.LastIndexOf('-');
|
|
if (dash > 0 && dash + 1 < leaf.Length && leaf[(dash + 1)..].All(char.IsDigit))
|
|
leaf = leaf[..dash];
|
|
}
|
|
|
|
return prefix + leaf;
|
|
}
|
|
|
|
static string NormalizeMatchText(string value) =>
|
|
new(value
|
|
.Trim()
|
|
.ToLowerInvariant()
|
|
.Where(ch => !char.IsWhiteSpace(ch) && ch is not '-' and not '_' and not '/' and not '\\')
|
|
.ToArray());
|
|
|
|
static string NormalizeHierarchyMatchText(string value) =>
|
|
new((value ?? "")
|
|
.Trim()
|
|
.Replace('\\', '/')
|
|
.ToLowerInvariant()
|
|
.Where(ch => !char.IsWhiteSpace(ch))
|
|
.ToArray());
|
|
|
|
static bool ContainsAny(string value, params string[] tokens)
|
|
{
|
|
return tokens.Any(t => value.Contains(t, StringComparison.OrdinalIgnoreCase));
|
|
}
|
|
|
|
static bool LooksLikeCustomAdapterPart(string value) =>
|
|
ContainsAny(
|
|
value,
|
|
"\u8fde\u63a5\u4ef6", "\u8fde\u63a5\u81c2", "\u8fde\u63a5\u6cd5\u5170", "\u6cd5\u5170",
|
|
"\u652f\u67b6", "\u5b89\u88c5\u5ea7", "\u5b89\u88c5\u677f", "\u56fa\u5b9a\u677f",
|
|
"\u5916\u58f3", "\u58f3\u4f53", "\u7aef\u76d6", "\u673a\u5ea7", "\u5ea7", "\u81c2", "\u677f",
|
|
"connector", "adapter", "flange", "bracket", "mount", "mounting", "housing", "shell", "cover", "arm", "plate");
|
|
|
|
#pragma warning disable CS8321
|
|
static string Classify(string name, string path)
|
|
{
|
|
var s = $"{name} {path}".ToLowerInvariant();
|
|
if (s.Contains("套筒") || s.Contains("轴套") || s.Contains("sleeve") || s.Contains("bushing"))
|
|
return "sleeve";
|
|
if (s.Contains("箱") || s.Contains("座") || s.Contains("housing") || s.Contains("case"))
|
|
return "housing";
|
|
if (s.Contains("轴") || s.Contains("shaft"))
|
|
return "shaft";
|
|
if (s.Contains("轴承") || s.Contains("bearing"))
|
|
return "bearing";
|
|
return "unknown";
|
|
}
|
|
|
|
#pragma warning restore CS8321
|
|
static string BuildMarkdown(SectionBrepReport report)
|
|
{
|
|
var sb = new StringBuilder();
|
|
sb.AppendLine("# Section B-rep Report");
|
|
sb.AppendLine($"- Ok: {report.Ok}");
|
|
sb.AppendLine($"- Path: `{report.Path}`");
|
|
sb.AppendLine($"- Document kind: `{report.DocumentKind}`");
|
|
sb.AppendLine($"- Message: {report.Message}");
|
|
if (!string.IsNullOrWhiteSpace(report.Error))
|
|
sb.AppendLine($"- Error: `{report.Error.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries).FirstOrDefault()}`");
|
|
if (!string.IsNullOrWhiteSpace(report.OutputDir))
|
|
sb.AppendLine($"- Output dir: `{report.OutputDir}`");
|
|
sb.AppendLine($"- Axis group: `{report.AxisGroupId}`");
|
|
sb.AppendLine($"- Canonical edges: {report.CanonicalSection.Edges.Count}");
|
|
sb.AppendLine($"- Assembly components: {report.AssemblyComponents.Count}");
|
|
sb.AppendLine($"- Component evidence packages: {report.ComponentEvidencePackages.Count}");
|
|
sb.AppendLine($"- Part image plan items: {report.PartImagePlan.Count}");
|
|
sb.AppendLine($"- Assembly faces: {report.AssemblyFaces.Count}");
|
|
sb.AppendLine($"- Assembly face contacts: {report.AssemblyFaceContacts.Count}");
|
|
sb.AppendLine($"- Assembly mates: {report.AssemblyMates.Count}");
|
|
sb.AppendLine($"- Mechanical features: {report.FeatureGraph.Features.Count}");
|
|
if (report.ImageExport != null)
|
|
{
|
|
sb.AppendLine($"- Image export: {report.ImageExport.SuccessCount}/{report.ImageExport.Views.Count} views -> `{report.ImageExport.OutputDir}`");
|
|
}
|
|
if (report.SectionImagePlan.Requests.Count > 0)
|
|
{
|
|
sb.AppendLine($"- Auto section image plan: {report.SectionImagePlan.Requests.Count}/{report.SectionImagePlan.MaxRequests} requests");
|
|
}
|
|
sb.AppendLine();
|
|
sb.AppendLine("## Plane");
|
|
sb.AppendLine($"- Origin: [{string.Join(", ", report.SectionPlane.OriginMm)}]");
|
|
sb.AppendLine($"- Axis: [{string.Join(", ", report.SectionPlane.AxisMm)}]");
|
|
sb.AppendLine($"- Plane normal: [{string.Join(", ", report.SectionPlane.PlaneNormalMm)}]");
|
|
sb.AppendLine();
|
|
sb.AppendLine("## Assembly Frame");
|
|
sb.AppendLine($"- Shaft axis: [{string.Join(", ", report.AssemblyFrame.ShaftAxisMm)}]");
|
|
sb.AppendLine($"- Vertical axis: [{string.Join(", ", report.AssemblyFrame.VerticalAxisMm)}]");
|
|
sb.AppendLine($"- Inference: {report.AssemblyFrame.Inference}");
|
|
sb.AppendLine();
|
|
if (report.AssemblyComponents.Count > 0)
|
|
{
|
|
sb.AppendLine("## Assembly Components");
|
|
foreach (var component in report.AssemblyComponents.Take(120))
|
|
{
|
|
sb.AppendLine($"- {component.Id}: {component.DisplayName}, category={component.Category}, review_scope={component.ReviewScope}, bodies={component.BodyCount}, faces={component.FaceCount}, file=`{component.FileName}`");
|
|
}
|
|
sb.AppendLine();
|
|
}
|
|
if (report.ComponentEvidencePackages.Count > 0)
|
|
{
|
|
sb.AppendLine("## Component Evidence Packages");
|
|
foreach (var package in report.ComponentEvidencePackages.Take(120))
|
|
{
|
|
sb.AppendLine($"- {package.ComponentId}: {package.DisplayName}, review_scope={package.ReviewScope}, brep_source={package.BrepSource}, image_source={package.ImageSource}, faces={package.FaceRefs.Count}, features={package.FeatureIds.Count}, contacts={package.ContactIds.Count}, mates={package.MateIds.Count}, part_image_plan={package.PartImagePlanId}");
|
|
}
|
|
sb.AppendLine();
|
|
}
|
|
if (report.PartImagePlan.Count > 0)
|
|
{
|
|
sb.AppendLine("## Part Image Plan");
|
|
foreach (var item in report.PartImagePlan.Take(120))
|
|
{
|
|
sb.AppendLine($"- {item.Id}: {item.DisplayName}, file=`{item.ComponentFileName}`, brep_source={item.BrepSource}, image_source={item.ImageSource}, do_not_reextract_brep={item.DoNotReextractBrep}, views={string.Join(",", item.SuggestedViews)}");
|
|
}
|
|
sb.AppendLine();
|
|
}
|
|
if (report.ComponentContextImagePlan.Count > 0)
|
|
{
|
|
sb.AppendLine("## Component Context Image Plan");
|
|
foreach (var item in report.ComponentContextImagePlan.Take(120))
|
|
{
|
|
var shown = string.Join(",", item.ShowComponents.Take(8).Select(c => c.ComponentId));
|
|
var internalMembers = string.Join(",", item.InternalMembers.Take(8).Select(c => c.ComponentId));
|
|
sb.AppendLine($"- {item.Id}: target={item.TargetComponentId} `{item.TargetDisplayName}`, group={item.GroupId}, strategy={item.Strategy}, display={item.DisplayMode}, views={string.Join(",", item.SuggestedViews)}, shown=[{shown}], internal_members=[{internalMembers}]");
|
|
}
|
|
sb.AppendLine();
|
|
}
|
|
foreach (var c in report.Components)
|
|
{
|
|
sb.AppendLine($"## {c.ComponentName}");
|
|
sb.AppendLine($"- Category: {c.Category}");
|
|
sb.AppendLine($"- Entities: {c.SectionEntities.Count}");
|
|
foreach (var e in c.SectionEntities.Take(12))
|
|
sb.AppendLine($" - {e.EntityKind}#{e.EntityIndex} via {e.Provenance}");
|
|
sb.AppendLine();
|
|
}
|
|
if (report.AssemblyFaceContacts.Count > 0)
|
|
{
|
|
sb.AppendLine("## Assembly Face Contacts");
|
|
foreach (var contact in report.AssemblyFaceContacts.Take(40))
|
|
{
|
|
sb.AppendLine($"- {contact.MateRole}: {contact.ComponentDisplayNameA} face#{contact.FaceA} ({contact.FaceRoleA}/{contact.OrientationRoleA}/{contact.SurfaceProcessRoleA}) <-> {contact.ComponentDisplayNameB} face#{contact.FaceB} ({contact.FaceRoleB}/{contact.OrientationRoleB}/{contact.SurfaceProcessRoleB}), confidence={contact.Confidence}, gap={contact.GapMm}");
|
|
}
|
|
sb.AppendLine();
|
|
}
|
|
if (report.AssemblyMates.Count > 0)
|
|
{
|
|
sb.AppendLine("## Assembly Mates");
|
|
foreach (var mate in report.AssemblyMates.Take(60))
|
|
{
|
|
sb.AppendLine($"- {mate.Id}: {mate.MateTypeName}/{mate.AlignmentName}, {mate.ComponentDisplayNameA} <-> {mate.ComponentDisplayNameB}, name=`{mate.MateName}`");
|
|
}
|
|
sb.AppendLine();
|
|
}
|
|
if (report.FeatureGraph.Features.Count > 0)
|
|
{
|
|
sb.AppendLine("## Mechanical Feature Graph");
|
|
foreach (var feature in report.FeatureGraph.Features.Take(40))
|
|
{
|
|
var functions = string.Join(",", feature.PossibleFunctions.Take(3));
|
|
var faces = string.Join(",", feature.FaceRefs.Take(4));
|
|
var owner = string.IsNullOrWhiteSpace(feature.ComponentDisplayName) ? feature.ComponentName : feature.ComponentDisplayName;
|
|
sb.AppendLine($"- {feature.Id}: type={feature.Type}, component={owner}, faces=[{faces}], possible={functions}, confidence={feature.Confidence}");
|
|
}
|
|
sb.AppendLine();
|
|
}
|
|
if (!string.IsNullOrWhiteSpace(report.SemanticFusionContract.Schema))
|
|
{
|
|
sb.AppendLine("## Semantic Fusion Contract");
|
|
sb.AppendLine($"- Schema: `{report.SemanticFusionContract.Schema}`");
|
|
sb.AppendLine($"- Feature types: {string.Join(", ", report.SemanticFusionContract.FeatureTypes)}");
|
|
sb.AppendLine("- Pipeline:");
|
|
foreach (var step in report.SemanticFusionContract.Pipeline)
|
|
sb.AppendLine($" - {step}");
|
|
sb.AppendLine("- Semantic types:");
|
|
foreach (var semanticType in report.SemanticFusionContract.SemanticTypes)
|
|
sb.AppendLine($" - {semanticType.Name}: {semanticType.Purpose}");
|
|
sb.AppendLine();
|
|
}
|
|
if (report.ImageExport != null)
|
|
{
|
|
sb.AppendLine("## Model Images");
|
|
foreach (var view in report.ImageExport.Views)
|
|
{
|
|
sb.AppendLine($"- {view.ViewName}: ok={view.Ok}, file=`{view.OutputPath}`, errors={view.SaveErrors}, warnings={view.SaveWarnings}, message={view.Message}");
|
|
}
|
|
sb.AppendLine();
|
|
}
|
|
if (report.SectionImagePlan.Requests.Count > 0)
|
|
{
|
|
sb.AppendLine("## Auto Section Image Plan");
|
|
foreach (var request in report.SectionImagePlan.Requests)
|
|
{
|
|
sb.AppendLine($"- {request.Rule}: plane={request.PlaneKey}, offset_mm={request.OffsetMm}, target={request.Target}, reason={request.Reason}");
|
|
}
|
|
sb.AppendLine();
|
|
}
|
|
return sb.ToString();
|
|
}
|
|
|
|
static object? GetComProperty(object obj, string name)
|
|
{
|
|
return obj.GetType().InvokeMember(name, System.Reflection.BindingFlags.GetProperty, null, obj, null);
|
|
}
|
|
|
|
static object[] ToObjectArray(object? value)
|
|
{
|
|
if (value is object[] objects)
|
|
return objects;
|
|
if (value is Array array)
|
|
return array.Cast<object>().ToArray();
|
|
return [];
|
|
}
|
|
|
|
static double[] ToDoubleArray(object? value)
|
|
{
|
|
if (value is double[] d)
|
|
return d;
|
|
if (value is float[] f)
|
|
return f.Select(item => (double)item).ToArray();
|
|
if (value is object[] o)
|
|
return o.Select(Convert.ToDouble).ToArray();
|
|
if (value is Array array)
|
|
return array.Cast<object>().Select(Convert.ToDouble).ToArray();
|
|
return [];
|
|
}
|
|
|
|
static double[] GetComponentTransformSnapshot(
|
|
Component2 component,
|
|
IDictionary<string, double[]> cache)
|
|
{
|
|
var name = Safe(() => component.Name2, "");
|
|
if (!string.IsNullOrWhiteSpace(name) && cache.TryGetValue(name, out var cached))
|
|
return cached;
|
|
|
|
var snapshot = Safe(() =>
|
|
{
|
|
var transform = component.Transform2 as MathTransform;
|
|
return transform == null ? [] : ToDoubleArray(transform.ArrayData);
|
|
}, []);
|
|
|
|
if (!string.IsNullOrWhiteSpace(name))
|
|
cache[name] = snapshot;
|
|
return snapshot;
|
|
}
|
|
|
|
static double[] TransformPointBySnapshot(double[] transform, double[] pointMm)
|
|
{
|
|
if (transform.Length < 13 || pointMm.Length < 3)
|
|
return pointMm;
|
|
|
|
var scale = transform.Length > 12 && Math.Abs(transform[12]) > 1e-12 ? transform[12] : 1.0;
|
|
return
|
|
[
|
|
scale * (pointMm[0] * transform[0] + pointMm[1] * transform[3] + pointMm[2] * transform[6]) + transform[9] * 1000.0,
|
|
scale * (pointMm[0] * transform[1] + pointMm[1] * transform[4] + pointMm[2] * transform[7]) + transform[10] * 1000.0,
|
|
scale * (pointMm[0] * transform[2] + pointMm[1] * transform[5] + pointMm[2] * transform[8]) + transform[11] * 1000.0
|
|
];
|
|
}
|
|
|
|
static double[] InverseTransformPointBySnapshot(double[] transform, double[] pointMm)
|
|
{
|
|
if (transform.Length < 13 || pointMm.Length < 3)
|
|
return pointMm;
|
|
|
|
var scale = Math.Abs(transform[12]) > 1e-12 ? transform[12] : 1.0;
|
|
var x = (pointMm[0] - transform[9] * 1000.0) / scale;
|
|
var y = (pointMm[1] - transform[10] * 1000.0) / scale;
|
|
var z = (pointMm[2] - transform[11] * 1000.0) / scale;
|
|
return
|
|
[
|
|
x * transform[0] + y * transform[1] + z * transform[2],
|
|
x * transform[3] + y * transform[4] + z * transform[5],
|
|
x * transform[6] + y * transform[7] + z * transform[8]
|
|
];
|
|
}
|
|
|
|
static double[] TransformVectorBySnapshot(double[] transform, double[] vector)
|
|
{
|
|
if (transform.Length < 9 || vector.Length < 3)
|
|
return vector;
|
|
|
|
var scale = transform.Length > 12 && Math.Abs(transform[12]) > 1e-12 ? transform[12] : 1.0;
|
|
return
|
|
[
|
|
scale * (vector[0] * transform[0] + vector[1] * transform[3] + vector[2] * transform[6]),
|
|
scale * (vector[0] * transform[1] + vector[1] * transform[4] + vector[2] * transform[7]),
|
|
scale * (vector[0] * transform[2] + vector[1] * transform[5] + vector[2] * transform[8])
|
|
];
|
|
}
|
|
|
|
static double[] TransformBBoxBySnapshot(double[] transform, double[] bboxMm)
|
|
{
|
|
if (transform.Length < 13)
|
|
return bboxMm;
|
|
var corners = Corners(Box.FromArray(bboxMm)).Select(point => TransformPointBySnapshot(transform, point)).ToList();
|
|
return
|
|
[
|
|
corners.Min(point => point[0]), corners.Min(point => point[1]), corners.Min(point => point[2]),
|
|
corners.Max(point => point[0]), corners.Max(point => point[1]), corners.Max(point => point[2])
|
|
];
|
|
}
|
|
|
|
static double[] TransformPoint(dynamic mathUtility, Component2 component, double[] pointMm)
|
|
{
|
|
dynamic p = mathUtility.CreatePoint(new[] { pointMm[0] / 1000.0, pointMm[1] / 1000.0, pointMm[2] / 1000.0 });
|
|
object transform = component.Transform2;
|
|
dynamic t = p.MultiplyTransform(transform);
|
|
var data = ToDoubleArray(t.ArrayData);
|
|
return data.Length >= 3 ? [data[0] * 1000.0, data[1] * 1000.0, data[2] * 1000.0] : pointMm;
|
|
}
|
|
|
|
static double[] TransformBBox(dynamic mathUtility, Component2 component, double[] bboxMm)
|
|
{
|
|
var corners = Corners(Box.FromArray(bboxMm)).Select(p => TransformPoint(mathUtility, component, p)).ToList();
|
|
return
|
|
[
|
|
corners.Min(p => p[0]), corners.Min(p => p[1]), corners.Min(p => p[2]),
|
|
corners.Max(p => p[0]), corners.Max(p => p[1]), corners.Max(p => p[2])
|
|
];
|
|
}
|
|
|
|
static T Safe<T>(Func<T> func, T fallback)
|
|
{
|
|
try { return func(); }
|
|
catch { return fallback; }
|
|
}
|
|
|
|
static IEnumerable<Feature> WalkFeatures(Feature? feature)
|
|
{
|
|
var visited = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
|
foreach (var item in WalkFeaturesRecursive(feature, visited))
|
|
yield return item;
|
|
}
|
|
|
|
static IEnumerable<Feature> WalkFeaturesRecursive(Feature? feature, HashSet<string> visited)
|
|
{
|
|
for (var current = feature; current != null; current = Safe(() => current.GetNextFeature() as Feature, null))
|
|
{
|
|
var key = ComObjectIdentityKey(current, $"{Safe(() => current.GetTypeName2(), "")}|{Safe(() => current.Name, "")}");
|
|
if (!visited.Add(key))
|
|
continue;
|
|
|
|
yield return current;
|
|
for (var sub = Safe(() => current.GetFirstSubFeature() as Feature, null); sub != null; sub = Safe(() => sub.GetNextSubFeature() as Feature, null))
|
|
{
|
|
foreach (var nested in WalkFeaturesRecursive(sub, visited))
|
|
yield return nested;
|
|
}
|
|
}
|
|
}
|
|
|
|
static string ComObjectIdentityKey(object value, string fallback)
|
|
{
|
|
try
|
|
{
|
|
var ptr = Marshal.GetIUnknownForObject(value);
|
|
try
|
|
{
|
|
if (ptr != IntPtr.Zero)
|
|
return ptr.ToString();
|
|
}
|
|
finally
|
|
{
|
|
if (ptr != IntPtr.Zero)
|
|
Marshal.Release(ptr);
|
|
}
|
|
}
|
|
catch { }
|
|
|
|
return fallback;
|
|
}
|
|
|
|
static string MateTypeName(int mateType)
|
|
{
|
|
try { return ((swMateType_e)mateType).ToString(); }
|
|
catch { return mateType.ToString(CultureInfo.InvariantCulture); }
|
|
}
|
|
|
|
static string MateAlignmentName(int alignment)
|
|
{
|
|
try { return ((swMateAlign_e)alignment).ToString(); }
|
|
catch { return alignment.ToString(CultureInfo.InvariantCulture); }
|
|
}
|
|
|
|
sealed class ExtractorCommand
|
|
{
|
|
public string Path { get; set; } = "";
|
|
public double[] Axis { get; set; } = [0.0, 1.0, 0.0];
|
|
public double[]? UpAxis { get; set; }
|
|
public string? OutputDir { get; set; }
|
|
public bool ExportImages { get; set; }
|
|
public string? ImageDir { get; set; }
|
|
public List<string> ImageViews { get; set; } = [];
|
|
public string? FaceHighlightPlanPath { get; set; }
|
|
public string? ImageOnlyReportPath { get; set; }
|
|
public bool ExportSectionImages { get; set; }
|
|
public List<string> SectionImageViews { get; set; } = [];
|
|
public bool AutoSectionImages { get; set; }
|
|
public double SectionOffsetMm { get; set; }
|
|
public bool SkipSectionBrep { get; set; }
|
|
public List<string> PurchasedComponentHints { get; set; } = [];
|
|
public List<string> ExternalInterfaceRoots { get; set; } = [];
|
|
}
|
|
|
|
sealed class FaceHighlightExportPlan
|
|
{
|
|
[JsonPropertyName("schema")]
|
|
public string Schema { get; set; } = "";
|
|
[JsonPropertyName("report_file_name")]
|
|
public string ReportFileName { get; set; } = "";
|
|
[JsonPropertyName("groups")]
|
|
public List<FaceHighlightExportGroup> Groups { get; set; } = [];
|
|
}
|
|
|
|
sealed class FaceHighlightExportGroup
|
|
{
|
|
[JsonPropertyName("group_id")]
|
|
public string GroupId { get; set; } = "";
|
|
[JsonPropertyName("directory")]
|
|
public string Directory { get; set; } = "";
|
|
[JsonPropertyName("components")]
|
|
public List<FaceHighlightExportComponent> Components { get; set; } = [];
|
|
}
|
|
|
|
sealed class FaceHighlightExportComponent
|
|
{
|
|
[JsonPropertyName("component_id")]
|
|
public string ComponentId { get; set; } = "";
|
|
[JsonPropertyName("instance_name")]
|
|
public string InstanceName { get; set; } = "";
|
|
[JsonPropertyName("display_name")]
|
|
public string DisplayName { get; set; } = "";
|
|
[JsonPropertyName("component_path")]
|
|
public string ComponentPath { get; set; } = "";
|
|
[JsonPropertyName("faces")]
|
|
public List<FaceHighlightExportFace> Faces { get; set; } = [];
|
|
}
|
|
|
|
sealed class FaceHighlightExportFace
|
|
{
|
|
[JsonPropertyName("plan_id")]
|
|
public string PlanId { get; set; } = "";
|
|
[JsonPropertyName("face_ref")]
|
|
public string FaceRef { get; set; } = "";
|
|
[JsonPropertyName("face_index")]
|
|
public int FaceIndex { get; set; }
|
|
[JsonPropertyName("face_kind")]
|
|
public string FaceKind { get; set; } = "";
|
|
[JsonPropertyName("feature_id")]
|
|
public string FeatureId { get; set; } = "";
|
|
[JsonPropertyName("views")]
|
|
public List<string> Views { get; set; } = [];
|
|
[JsonPropertyName("preferred_direction_mm")]
|
|
public double[] PreferredDirectionMm { get; set; } = [];
|
|
[JsonPropertyName("output_relative_directory")]
|
|
public string OutputRelativeDirectory { get; set; } = "";
|
|
[JsonPropertyName("output_file_stem")]
|
|
public string OutputFileStem { get; set; } = "";
|
|
}
|
|
|
|
sealed class FaceHighlightPlanRunReport
|
|
{
|
|
public string Schema { get; set; } = "";
|
|
public string GenerationMode { get; set; } = "";
|
|
public string PlanPath { get; set; } = "";
|
|
public string OutputDir { get; set; } = "";
|
|
public string ReportPath { get; set; } = "";
|
|
public bool Success { get; set; }
|
|
public string Message { get; set; } = "";
|
|
public DateTimeOffset StartedAt { get; set; }
|
|
public DateTimeOffset? CompletedAt { get; set; }
|
|
public List<FaceHighlightGroupRunReport> Groups { get; set; } = [];
|
|
}
|
|
|
|
sealed class FaceHighlightGroupRunReport
|
|
{
|
|
public string GroupId { get; set; } = "";
|
|
public string Directory { get; set; } = "";
|
|
public int RequestedFaceCount { get; set; }
|
|
public int ExportedFaceCount { get; set; }
|
|
public List<FaceHighlightComponentRunReport> Components { get; set; } = [];
|
|
}
|
|
|
|
sealed class FaceHighlightComponentRunReport
|
|
{
|
|
public string ComponentId { get; set; } = "";
|
|
public string InstanceName { get; set; } = "";
|
|
public string DisplayName { get; set; } = "";
|
|
public string ComponentPath { get; set; } = "";
|
|
public int RequestedFaceCount { get; set; }
|
|
public int ExportedFaceCount { get; set; }
|
|
public List<ModelImageViewExport> Views { get; set; } = [];
|
|
}
|
|
|
|
sealed class ExtractionRunStatus
|
|
{
|
|
public string InputPath { get; set; } = "";
|
|
public string OutputDir { get; set; } = "";
|
|
public double[] Axis { get; set; } = [];
|
|
public DateTimeOffset StartedAt { get; set; }
|
|
public DateTimeOffset UpdatedAt { get; set; }
|
|
public DateTimeOffset? CompletedAt { get; set; }
|
|
public bool Success { get; set; }
|
|
public string CurrentStage { get; set; } = "";
|
|
public string ReportPath { get; set; } = "";
|
|
public string Error { get; set; } = "";
|
|
public List<ExtractionStageLog> Stages { get; set; } = [];
|
|
}
|
|
|
|
sealed class ExtractionStageLog
|
|
{
|
|
public string Stage { get; set; } = "";
|
|
public string Message { get; set; } = "";
|
|
public DateTimeOffset Timestamp { get; set; }
|
|
}
|
|
|
|
sealed class SectionBrepReport
|
|
{
|
|
public bool Ok { get; set; }
|
|
public string Path { get; set; } = "";
|
|
public string DocumentKind { get; set; } = "";
|
|
public string OutputDir { get; set; } = "";
|
|
public DateTimeOffset StartedAt { get; set; }
|
|
public DateTimeOffset? CompletedAt { get; set; }
|
|
public string Message { get; set; } = "";
|
|
public string Error { get; set; } = "";
|
|
public List<ExtractionStageLog> StageLogs { get; set; } = [];
|
|
public string AxisGroupId { get; set; } = "";
|
|
public AssemblyFrameEvidence AssemblyFrame { get; set; } = new();
|
|
public SectionPlaneEvidence SectionPlane { get; set; } = new();
|
|
public CanonicalSectionBrep CanonicalSection { get; set; } = new();
|
|
public Canonical2DStructureGraph Canonical2D { get; set; } = new();
|
|
public Canonical2DSketchGraph SketchGraph { get; set; } = new();
|
|
public ModelImageExportReport? ImageExport { get; set; }
|
|
public AutoSectionImagePlan SectionImagePlan { get; set; } = new();
|
|
public MechanicalFeatureGraph FeatureGraph { get; set; } = new();
|
|
public SemanticFusionContract SemanticFusionContract { get; set; } = new();
|
|
public List<AssemblyComponentSummary> AssemblyComponents { get; set; } = [];
|
|
public List<ComponentEvidencePackage> ComponentEvidencePackages { get; set; } = [];
|
|
public List<string> ExternalInterfaceRoots { get; set; } = [];
|
|
public List<PartImagePlanItem> PartImagePlan { get; set; } = [];
|
|
public List<ComponentContextImagePlanItem> ComponentContextImagePlan { get; set; } = [];
|
|
public List<AssemblyFaceEvidence> AssemblyFaces { get; set; } = [];
|
|
public List<AssemblyFaceContactEvidence> AssemblyFaceContacts { get; set; } = [];
|
|
public List<AssemblyMateRelation> AssemblyMates { get; set; } = [];
|
|
public List<SectionBodyEvidence> Components { get; set; } = [];
|
|
}
|
|
|
|
sealed class AutoSectionImagePlan
|
|
{
|
|
public string Schema { get; set; } = "auto_section_image_plan_v1";
|
|
public int MaxRequests { get; set; } = 6;
|
|
public List<AutoSectionImageRequest> Requests { get; set; } = [];
|
|
}
|
|
|
|
sealed class AutoSectionImageRequest
|
|
{
|
|
public string Request { get; set; } = "";
|
|
public string Rule { get; set; } = "";
|
|
public string Target { get; set; } = "";
|
|
public string Reason { get; set; } = "";
|
|
public string PlaneKey { get; set; } = "";
|
|
public double OffsetMm { get; set; }
|
|
public double[] OriginMm { get; set; } = [];
|
|
public double[] PreferredNormalMm { get; set; } = [];
|
|
public double[] PreferredContainedAxisMm { get; set; } = [];
|
|
public List<string> SourceRefs { get; set; } = [];
|
|
}
|
|
|
|
sealed class MechanicalFeatureGraph
|
|
{
|
|
public string Schema { get; set; } = "mechanical_feature_graph_v1";
|
|
public List<MechanicalFeature> Features { get; set; } = [];
|
|
public List<MechanicalFeatureRelation> Relations { get; set; } = [];
|
|
public List<string> FusionGuidance { get; set; } = [];
|
|
}
|
|
|
|
sealed class MechanicalFeature
|
|
{
|
|
public string Id { get; set; } = "";
|
|
public string Type { get; set; } = "";
|
|
public string ComponentName { get; set; } = "";
|
|
public string ComponentDisplayName { get; set; } = "";
|
|
public string ComponentPath { get; set; } = "";
|
|
public string ComponentFileName { get; set; } = "";
|
|
public List<string> FaceRefs { get; set; } = [];
|
|
public double Confidence { get; set; }
|
|
public string Source { get; set; } = "";
|
|
public List<string> PossibleFunctions { get; set; } = [];
|
|
public List<string> VisualObservationNeeds { get; set; } = [];
|
|
public List<string> RequiredBrepChecks { get; set; } = [];
|
|
public Dictionary<string, object?> Geometry { get; set; } = [];
|
|
}
|
|
|
|
sealed class MechanicalFeatureRelation
|
|
{
|
|
public string Type { get; set; } = "";
|
|
public string A { get; set; } = "";
|
|
public string B { get; set; } = "";
|
|
public double Confidence { get; set; }
|
|
public string Source { get; set; } = "";
|
|
public Dictionary<string, object?> Attributes { get; set; } = [];
|
|
}
|
|
|
|
sealed class HoleFeatureGroup
|
|
{
|
|
public List<AssemblyFaceEvidence> Members { get; set; } = [];
|
|
public string PatternType { get; set; } = "";
|
|
public string GroupingBasis { get; set; } = "";
|
|
public double[] PatternNormal { get; set; } = [];
|
|
public double? PatternPlaneTmm { get; set; }
|
|
public double[] PatternCenterMm { get; set; } = [];
|
|
public double PatternRadiusMm { get; set; }
|
|
public double SpacingMm { get; set; }
|
|
}
|
|
|
|
sealed class SemanticFusionContract
|
|
{
|
|
public string Schema { get; set; } = "mechanical_semantic_fusion_contract_v1";
|
|
public List<string> Pipeline { get; set; } = [];
|
|
public List<string> FeatureTypes { get; set; } = [];
|
|
public List<SemanticTypeSpec> SemanticTypes { get; set; } = [];
|
|
public List<RetrievalViewSpec> RetrievalViews { get; set; } = [];
|
|
public Dictionary<string, object?> GlobalMechanicalSemanticGraphSchema { get; set; } = [];
|
|
public Dictionary<string, object?> AssertionSchema { get; set; } = [];
|
|
public Dictionary<string, object?> LocalSemanticUnitSchema { get; set; } = [];
|
|
public Dictionary<string, object?> CoverageAuditSchema { get; set; } = [];
|
|
public Dictionary<string, object?> BrepFactBundleSchema { get; set; } = [];
|
|
public Dictionary<string, object?> LocalSubgraphSchema { get; set; } = [];
|
|
public List<string> EvidenceStatusValues { get; set; } = [];
|
|
public List<string> ConfidencePolicy { get; set; } = [];
|
|
public List<string> DedupPolicy { get; set; } = [];
|
|
public List<string> RetrievalPolicy { get; set; } = [];
|
|
}
|
|
|
|
sealed class SemanticTypeSpec
|
|
{
|
|
public string Name { get; set; } = "";
|
|
public string AppliesTo { get; set; } = "";
|
|
public string Purpose { get; set; } = "";
|
|
public List<string> TypicalTargets { get; set; } = [];
|
|
}
|
|
|
|
sealed class RetrievalViewSpec
|
|
{
|
|
public string Id { get; set; } = "";
|
|
public string Name { get; set; } = "";
|
|
public string Focus { get; set; } = "";
|
|
}
|
|
|
|
sealed class Canonical2DStructureGraph
|
|
{
|
|
public Canonical2DSource Source { get; set; } = new();
|
|
public List<Canonical2DEdge> Edges { get; set; } = [];
|
|
public List<Canonical2DRegion> Regions { get; set; } = [];
|
|
public List<Canonical2DRelation> Relations { get; set; } = [];
|
|
public List<Canonical2DAnnotation> Annotations { get; set; } = [];
|
|
}
|
|
|
|
sealed class Canonical2DSource
|
|
{
|
|
public string Kind { get; set; } = "";
|
|
public string CoordinateSystem { get; set; } = "";
|
|
public Dictionary<string, object?> Metadata { get; set; } = [];
|
|
}
|
|
|
|
sealed class Canonical2DEdge
|
|
{
|
|
public string Id { get; set; } = "";
|
|
public string Type { get; set; } = "";
|
|
public double[] Start { get; set; } = [];
|
|
public double[] End { get; set; } = [];
|
|
public string Owner { get; set; } = "";
|
|
public string Role { get; set; } = "";
|
|
public string SourceEntity { get; set; } = "";
|
|
public Dictionary<string, object?> Attributes { get; set; } = [];
|
|
}
|
|
|
|
sealed class Canonical2DRegion
|
|
{
|
|
public string Id { get; set; } = "";
|
|
public string Owner { get; set; } = "";
|
|
public string Role { get; set; } = "";
|
|
public List<string> EdgeRefs { get; set; } = [];
|
|
}
|
|
|
|
sealed class Canonical2DRelation
|
|
{
|
|
public string Type { get; set; } = "";
|
|
public string A { get; set; } = "";
|
|
public string B { get; set; } = "";
|
|
public Dictionary<string, object?> Attributes { get; set; } = [];
|
|
}
|
|
|
|
sealed class Canonical2DAnnotation
|
|
{
|
|
public string Id { get; set; } = "";
|
|
public string Type { get; set; } = "";
|
|
public double[] Start { get; set; } = [];
|
|
public double[] End { get; set; } = [];
|
|
public Dictionary<string, object?> Attributes { get; set; } = [];
|
|
}
|
|
|
|
sealed class Canonical2DSketchGraph
|
|
{
|
|
public string Schema { get; set; } = "canonical_2d_sketch_graph_v1";
|
|
public Canonical2DSketchSource Source { get; set; } = new();
|
|
public List<SketchVertex> Vertices { get; set; } = [];
|
|
public List<SketchDrawableEdge> Edges { get; set; } = [];
|
|
public List<SketchLoop> Loops { get; set; } = [];
|
|
public List<SketchPath> Paths { get; set; } = [];
|
|
public List<SketchRegion> Regions { get; set; } = [];
|
|
public List<SketchRelation> Relations { get; set; } = [];
|
|
public List<SketchDimension> Dimensions { get; set; } = [];
|
|
}
|
|
|
|
sealed class Canonical2DSketchSource
|
|
{
|
|
public string Kind { get; set; } = "";
|
|
public string CoordinateSystem { get; set; } = "";
|
|
public string Units { get; set; } = "";
|
|
public Dictionary<string, object?> Metadata { get; set; } = [];
|
|
}
|
|
|
|
sealed class SketchVertex
|
|
{
|
|
public string Id { get; set; } = "";
|
|
public double[] Point { get; set; } = [];
|
|
public double[] NormalizedPoint { get; set; } = [];
|
|
}
|
|
|
|
sealed class SketchDrawableEdge
|
|
{
|
|
public string Id { get; set; } = "";
|
|
public List<string> SourceEdgeIds { get; set; } = [];
|
|
public string Type { get; set; } = "";
|
|
public string StartVertex { get; set; } = "";
|
|
public string EndVertex { get; set; } = "";
|
|
public double[] Start { get; set; } = [];
|
|
public double[] End { get; set; } = [];
|
|
public double[] NormalizedStart { get; set; } = [];
|
|
public double[] NormalizedEnd { get; set; } = [];
|
|
public string Owner { get; set; } = "";
|
|
public string Role { get; set; } = "";
|
|
public double Length { get; set; }
|
|
public Dictionary<string, object?> Attributes { get; set; } = [];
|
|
}
|
|
|
|
sealed class SketchLoop
|
|
{
|
|
public string Id { get; set; } = "";
|
|
public List<string> EdgeRefs { get; set; } = [];
|
|
public List<string> SourceEdgeRefs { get; set; } = [];
|
|
public bool Closed { get; set; }
|
|
public string Role { get; set; } = "";
|
|
}
|
|
|
|
sealed class SketchPath
|
|
{
|
|
public string Id { get; set; } = "";
|
|
public List<string> EdgeRefs { get; set; } = [];
|
|
public List<string> SourceEdgeRefs { get; set; } = [];
|
|
public List<string> VertexRefs { get; set; } = [];
|
|
public List<double[]> Polyline { get; set; } = [];
|
|
public bool Closed { get; set; }
|
|
public string Role { get; set; } = "";
|
|
}
|
|
|
|
sealed class SketchRegion
|
|
{
|
|
public string Id { get; set; } = "";
|
|
public string Owner { get; set; } = "";
|
|
public string Role { get; set; } = "";
|
|
public List<string> SourceEdgeRefs { get; set; } = [];
|
|
public List<string> LoopRefs { get; set; } = [];
|
|
}
|
|
|
|
sealed class SketchRelation
|
|
{
|
|
public string Type { get; set; } = "";
|
|
public string A { get; set; } = "";
|
|
public string B { get; set; } = "";
|
|
public Dictionary<string, object?> Attributes { get; set; } = [];
|
|
}
|
|
|
|
sealed class SketchDimension
|
|
{
|
|
public string Id { get; set; } = "";
|
|
public string Kind { get; set; } = "";
|
|
public List<string> TargetRefs { get; set; } = [];
|
|
public double Value { get; set; }
|
|
public string Units { get; set; } = "";
|
|
public string Role { get; set; } = "";
|
|
}
|
|
|
|
sealed class MergedSketchEdge
|
|
{
|
|
public string Key { get; set; } = "";
|
|
public string Type { get; set; } = "";
|
|
public double[] Start { get; set; } = [];
|
|
public double[] End { get; set; } = [];
|
|
public string Owner { get; set; } = "";
|
|
public string Role { get; set; } = "";
|
|
public List<string> SourceIds { get; set; } = [];
|
|
public Dictionary<string, object?> Attributes { get; set; } = [];
|
|
}
|
|
|
|
sealed class CanonicalSectionBrep
|
|
{
|
|
public string Schema { get; set; } = "canonical_section_brep_v0";
|
|
public string SourceKind { get; set; } = "";
|
|
public string CoordinateSystem { get; set; } = "";
|
|
public List<CanonicalSectionEdge> Edges { get; set; } = [];
|
|
public List<CanonicalSectionAnnotation> Annotations { get; set; } = [];
|
|
public Dictionary<string, object?> Metadata { get; set; } = [];
|
|
}
|
|
|
|
sealed class CanonicalSectionEdge
|
|
{
|
|
public string Id { get; set; } = "";
|
|
public string Kind { get; set; } = "";
|
|
public double[] Start { get; set; } = [];
|
|
public double[] End { get; set; } = [];
|
|
public double Length { get; set; }
|
|
public Dictionary<string, object?> Attributes { get; set; } = [];
|
|
}
|
|
|
|
sealed class CanonicalSectionAnnotation
|
|
{
|
|
public string Id { get; set; } = "";
|
|
public string Kind { get; set; } = "";
|
|
public double[] Start { get; set; } = [];
|
|
public double[] End { get; set; } = [];
|
|
public Dictionary<string, object?> Attributes { get; set; } = [];
|
|
}
|
|
|
|
sealed class SectionPlaneEvidence
|
|
{
|
|
public double[] OriginMm { get; set; } = [];
|
|
public double[] AxisMm { get; set; } = [];
|
|
public double[] PlaneNormalMm { get; set; } = [];
|
|
public double[] UAxisMm { get; set; } = [];
|
|
public double[] VAxisMm { get; set; } = [];
|
|
}
|
|
|
|
sealed class SectionBodyEvidence
|
|
{
|
|
public string ComponentName { get; set; } = "";
|
|
public string Category { get; set; } = "";
|
|
public List<SectionEntityEvidence> SectionEntities { get; set; } = [];
|
|
}
|
|
|
|
sealed class SectionEntityEvidence
|
|
{
|
|
public string ComponentName { get; set; } = "";
|
|
public string EntityKind { get; set; } = "";
|
|
public int EntityIndex { get; set; }
|
|
public string Provenance { get; set; } = "";
|
|
public Dictionary<string, object?> Geometry { get; set; } = [];
|
|
}
|
|
|
|
sealed class AssemblyComponentSummary
|
|
{
|
|
public string Id { get; set; } = "";
|
|
public string InstanceName { get; set; } = "";
|
|
public string DisplayName { get; set; } = "";
|
|
public string FileName { get; set; } = "";
|
|
public string Path { get; set; } = "";
|
|
public string Category { get; set; } = "";
|
|
public string ReviewScope { get; set; } = "";
|
|
public int BodyCount { get; set; }
|
|
public int FaceCount { get; set; }
|
|
public int CylinderFaceCount { get; set; }
|
|
public double[] BBoxMm { get; set; } = [];
|
|
}
|
|
|
|
sealed class ComponentEvidencePackage
|
|
{
|
|
public string ComponentId { get; set; } = "";
|
|
public string InstanceName { get; set; } = "";
|
|
public string DisplayName { get; set; } = "";
|
|
public string FileName { get; set; } = "";
|
|
public string Path { get; set; } = "";
|
|
public string Category { get; set; } = "";
|
|
public string ReviewScope { get; set; } = "";
|
|
public string BrepSource { get; set; } = "";
|
|
public string ImageSource { get; set; } = "";
|
|
public string PartImagePlanId { get; set; } = "";
|
|
public string AssemblyContextImagePlanId { get; set; } = "";
|
|
public List<string> FaceRefs { get; set; } = [];
|
|
public List<string> FeatureIds { get; set; } = [];
|
|
public List<string> ContactIds { get; set; } = [];
|
|
public List<string> MateIds { get; set; } = [];
|
|
public int BodyCount { get; set; }
|
|
public int FaceCount { get; set; }
|
|
public int CylinderFaceCount { get; set; }
|
|
public double[] BBoxMm { get; set; } = [];
|
|
public List<string> Rules { get; set; } = [];
|
|
}
|
|
|
|
sealed class PartImagePlanItem
|
|
{
|
|
public string Id { get; set; } = "";
|
|
public string ComponentId { get; set; } = "";
|
|
public string InstanceName { get; set; } = "";
|
|
public string DisplayName { get; set; } = "";
|
|
public string ComponentFileName { get; set; } = "";
|
|
public string ComponentPath { get; set; } = "";
|
|
public string BrepSource { get; set; } = "";
|
|
public string ImageSource { get; set; } = "";
|
|
public string AssemblyContextImagePlanId { get; set; } = "";
|
|
public string AssemblyContextImageSource { get; set; } = "";
|
|
public bool DoNotReextractBrep { get; set; }
|
|
public List<string> SuggestedViews { get; set; } = [];
|
|
public List<string> SuggestedAssemblyContextViews { get; set; } = [];
|
|
public string HighlightInstruction { get; set; } = "";
|
|
public List<string> Purpose { get; set; } = [];
|
|
}
|
|
|
|
sealed class ComponentContextImagePlanItem
|
|
{
|
|
public string Id { 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 string Strategy { get; set; } = "";
|
|
public string DisplayMode { get; set; } = "";
|
|
public string OcclusionMethod { get; set; } = "";
|
|
public bool RestoreRequired { get; set; }
|
|
public bool SaveModel { get; set; }
|
|
public List<string> SuggestedViews { get; set; } = [];
|
|
public List<ComponentContextMember> ShowComponents { get; set; } = [];
|
|
public List<ComponentContextMember> InternalMembers { get; set; } = [];
|
|
public List<ComponentContextMember> BoundaryComponents { get; set; } = [];
|
|
public InternalOcclusionAnalysis TargetOcclusion { get; set; } = new();
|
|
public List<InternalOcclusionAnalysis> MemberOcclusionAnalyses { get; set; } = [];
|
|
public string HiddenPolicy { get; set; } = "";
|
|
public List<string> Purpose { get; set; } = [];
|
|
}
|
|
|
|
sealed class ComponentContextMember
|
|
{
|
|
public string ComponentId { get; set; } = "";
|
|
public string InstanceName { get; set; } = "";
|
|
public string DisplayName { get; set; } = "";
|
|
public string ComponentPath { get; set; } = "";
|
|
public string Role { get; set; } = "";
|
|
}
|
|
|
|
sealed class InternalOcclusionAnalysis
|
|
{
|
|
public string ComponentId { get; set; } = "";
|
|
public string InstanceName { get; set; } = "";
|
|
public string DisplayName { get; set; } = "";
|
|
public string Method { get; set; } = "";
|
|
public bool IsInternal { get; set; }
|
|
public int OccludedDirectionCount { get; set; }
|
|
public int OpenDirectionCount { get; set; }
|
|
public bool EarlyStop { get; set; }
|
|
public string Decision { get; set; } = "";
|
|
public List<InternalOcclusionDirectionAnalysis> Directions { get; set; } = [];
|
|
}
|
|
|
|
sealed class InternalOcclusionDirectionAnalysis
|
|
{
|
|
public string Direction { get; set; } = "";
|
|
public bool Occluded { get; set; }
|
|
public double OcclusionRatio { get; set; }
|
|
public List<OcclusionBlocker> FirstHitComponents { get; set; } = [];
|
|
}
|
|
|
|
sealed class OcclusionBlocker
|
|
{
|
|
public string ComponentId { get; set; } = "";
|
|
public string InstanceName { get; set; } = "";
|
|
public string DisplayName { get; set; } = "";
|
|
public string ComponentPath { get; set; } = "";
|
|
}
|
|
|
|
sealed class ComponentEvidence
|
|
{
|
|
public string Name { get; set; } = "";
|
|
public string Path { get; set; } = "";
|
|
public string Category { get; set; } = "";
|
|
public MathTransform? Transform { get; set; }
|
|
public List<Body2> Bodies { get; set; } = [];
|
|
public List<FaceEvidence> Faces { get; set; } = [];
|
|
public List<FaceEvidence> Cylinders { get; set; } = [];
|
|
public Box? BoundingBox { get; set; }
|
|
}
|
|
|
|
sealed class FaceEvidence
|
|
{
|
|
public int FaceIndex { get; set; }
|
|
public string Kind { get; set; } = "other";
|
|
public double AreaMm2 { get; set; }
|
|
public Box Box { get; set; }
|
|
public double[] CenterMm { get; set; } = [];
|
|
public double[]? Normal { get; set; }
|
|
public double[]? RootPointMm { get; set; }
|
|
public double[]? AxisPointMm { get; set; }
|
|
public double[]? Axis { get; set; }
|
|
public double RadiusMm { get; set; }
|
|
public List<double[]> SamplePointsMm { get; set; } = [];
|
|
public List<FaceTriangleMm> TrianglesMm { get; set; } = [];
|
|
public bool TessellationAttempted { get; set; }
|
|
[JsonIgnore]
|
|
public Face2? RuntimeFace { get; set; }
|
|
[JsonIgnore]
|
|
public double[] AssemblyTransformSnapshot { get; set; } = [];
|
|
}
|
|
|
|
sealed class AxisGroupEvidence
|
|
{
|
|
public string GroupId { get; set; } = "";
|
|
public double[] AxisPointMm { get; set; } = [];
|
|
public double[] Axis { get; set; } = [];
|
|
public int CylinderCount { get; set; }
|
|
public int ComponentCount { get; set; }
|
|
public double RadiusMinMm { get; set; }
|
|
public double RadiusMaxMm { get; set; }
|
|
public List<AxisMemberEvidence> Members { get; set; } = [];
|
|
}
|
|
|
|
sealed class AxisMemberEvidence
|
|
{
|
|
public string ComponentName { get; set; } = "";
|
|
public string ComponentCategory { get; set; } = "";
|
|
public int FaceIndex { get; set; }
|
|
public double RadiusMm { get; set; }
|
|
public double AreaMm2 { get; set; }
|
|
}
|
|
|
|
sealed class AssemblyMateRelation
|
|
{
|
|
public string Id { get; set; } = "";
|
|
public string MateName { get; set; } = "";
|
|
public int MateType { get; set; }
|
|
public string MateTypeName { get; set; } = "";
|
|
public int Alignment { get; set; }
|
|
public string AlignmentName { get; set; } = "";
|
|
public string ComponentA { get; set; } = "";
|
|
public string ComponentDisplayNameA { get; set; } = "";
|
|
public string ComponentPathA { get; set; } = "";
|
|
public string ComponentFileNameA { get; set; } = "";
|
|
public string ComponentB { get; set; } = "";
|
|
public string ComponentDisplayNameB { get; set; } = "";
|
|
public string ComponentPathB { get; set; } = "";
|
|
public string ComponentFileNameB { get; set; } = "";
|
|
public int EntityCount { get; set; }
|
|
public string Evidence { get; set; } = "";
|
|
}
|
|
|
|
sealed class AssemblyFaceContactEvidence
|
|
{
|
|
public string Id { get; set; } = "";
|
|
public string ContactKind { get; set; } = "";
|
|
public string ComponentA { get; set; } = "";
|
|
public string ComponentDisplayNameA { get; set; } = "";
|
|
public string ComponentPathA { get; set; } = "";
|
|
public string ComponentFileNameA { get; set; } = "";
|
|
public string ComponentCategoryA { get; set; } = "";
|
|
public int FaceA { get; set; }
|
|
public string FaceKindA { get; set; } = "";
|
|
public string ComponentB { get; set; } = "";
|
|
public string ComponentDisplayNameB { get; set; } = "";
|
|
public string ComponentPathB { get; set; } = "";
|
|
public string ComponentFileNameB { get; set; } = "";
|
|
public string ComponentCategoryB { get; set; } = "";
|
|
public int FaceB { get; set; }
|
|
public string FaceKindB { get; set; } = "";
|
|
public string MateRole { get; set; } = "";
|
|
public string FaceRoleA { get; set; } = "";
|
|
public string FaceRoleB { get; set; } = "";
|
|
public string OrientationRoleA { get; set; } = "";
|
|
public string OrientationRoleB { get; set; } = "";
|
|
public string SurfaceProcessRoleA { get; set; } = "";
|
|
public string SurfaceProcessRoleB { get; set; } = "";
|
|
public string ProcessConfidenceA { get; set; } = "";
|
|
public string ProcessConfidenceB { get; set; } = "";
|
|
public string ProcessBasisA { get; set; } = "";
|
|
public string ProcessBasisB { get; set; } = "";
|
|
public string MachiningScopeA { get; set; } = "";
|
|
public string MachiningScopeB { get; set; } = "";
|
|
public double GapMm { get; set; }
|
|
public double NormalDot { get; set; }
|
|
public double RadiusAmm { get; set; }
|
|
public double RadiusBmm { get; set; }
|
|
public double AxisDistanceMm { get; set; }
|
|
public double AxialOverlapMm { get; set; }
|
|
public double OverlapMeasureMm2 { get; set; }
|
|
public bool TrimmedPatchVerified { get; set; }
|
|
public string VerificationStatus { get; set; } = "";
|
|
public string VerificationMethod { get; set; } = "";
|
|
public int VerificationSampleHits { get; set; }
|
|
public double VerificationMinDistanceMm { get; set; }
|
|
public double Confidence { get; set; }
|
|
public string Evidence { get; set; } = "";
|
|
public double[] CenterA { get; set; } = [];
|
|
public double[] CenterB { get; set; } = [];
|
|
public double[] NormalA { get; set; } = [];
|
|
public double[] NormalB { get; set; } = [];
|
|
public double[] AxisA { get; set; } = [];
|
|
public double[] AxisB { get; set; } = [];
|
|
public double[] BBoxA { get; set; } = [];
|
|
public double[] BBoxB { get; set; } = [];
|
|
[JsonIgnore]
|
|
public Face2? RuntimeFaceA { get; set; }
|
|
[JsonIgnore]
|
|
public Face2? RuntimeFaceB { get; set; }
|
|
}
|
|
|
|
sealed class AssemblyFaceEvidence
|
|
{
|
|
public string ComponentName { get; set; } = "";
|
|
public string ComponentDisplayName { get; set; } = "";
|
|
public string ComponentPath { get; set; } = "";
|
|
public string ComponentFileName { get; set; } = "";
|
|
public string ComponentCategory { get; set; } = "";
|
|
public int FaceIndex { get; set; }
|
|
public string FaceKind { get; set; } = "";
|
|
public double AreaMm2 { get; set; }
|
|
public double[] CenterMm { get; set; } = [];
|
|
public double[] BBoxMm { get; set; } = [];
|
|
public double[] Normal { get; set; } = [];
|
|
public double[] RootPointMm { get; set; } = [];
|
|
public double[] AxisPointMm { get; set; } = [];
|
|
public double[] Axis { get; set; } = [];
|
|
public double RadiusMm { get; set; }
|
|
public double DiameterMm { get; set; }
|
|
public string OrientationRole { get; set; } = "";
|
|
public string FunctionalRole { get; set; } = "";
|
|
public string SurfaceProcessRole { get; set; } = "";
|
|
public string ProcessConfidence { get; set; } = "";
|
|
public string ProcessBasis { get; set; } = "";
|
|
public string MachiningScope { get; set; } = "";
|
|
}
|
|
|
|
sealed class AssemblyFrameEvidence
|
|
{
|
|
public double[] ShaftAxisMm { get; set; } = [];
|
|
public double[] VerticalAxisMm { get; set; } = [];
|
|
public string Inference { get; set; } = "";
|
|
}
|
|
|
|
sealed class FaceProcessAnnotation
|
|
{
|
|
public string SurfaceProcessRole { get; set; } = "";
|
|
public string ProcessConfidence { get; set; } = "";
|
|
public string ProcessBasis { get; set; } = "";
|
|
public string MachiningScope { get; set; } = "";
|
|
}
|
|
|
|
readonly record struct Box(double XMin, double YMin, double ZMin, double XMax, double YMax, double ZMax)
|
|
{
|
|
public static Box FromArray(double[] b) => new(b[0], b[1], b[2], b[3], b[4], b[5]);
|
|
public double[] ToArrayRounded() => [Math.Round(XMin, 3), Math.Round(YMin, 3), Math.Round(ZMin, 3), Math.Round(XMax, 3), Math.Round(YMax, 3), Math.Round(ZMax, 3)];
|
|
}
|
|
|
|
readonly record struct OcclusionDirectionSpec(string Name, int Axis, double Sign, int UAxis, int VAxis);
|
|
|
|
readonly record struct ProjectionRect(double UMin, double UMax, double VMin, double VMax)
|
|
{
|
|
public double AreaMm2 => Math.Max(0.0, UMax - UMin) * Math.Max(0.0, VMax - VMin);
|
|
}
|
|
|
|
sealed record CylindricalTrimmedParamDomain(
|
|
double AxialMinMm,
|
|
double AxialMaxMm,
|
|
FaceEvidence Face,
|
|
double[] CommonAxis,
|
|
double[] FaceAxisOriginAtCommonT0Mm,
|
|
double[] U,
|
|
double[] V,
|
|
double NormalSense,
|
|
List<CylindricalBoundarySegment> BoundarySegments);
|
|
readonly record struct CylindricalBoundaryPoint(double ThetaRadians, double AxialMm, double[] PointMm);
|
|
readonly record struct CylindricalBoundarySegment(
|
|
double ThetaStartRadians,
|
|
double ThetaEndRadians,
|
|
double AxialStartMm,
|
|
double AxialEndMm);
|
|
readonly record struct AngularInterval(double MinRadians, double MaxRadians);
|
|
|
|
readonly record struct IndexedFace(int FaceIndex, Face2 Face, Component2? OwnerComponent);
|
|
sealed record FaceTriangleMm(double[] A, double[] B, double[] C);
|
|
sealed record FaceTessellation(List<double[]> SamplePointsMm, List<FaceTriangleMm> TrianglesMm);
|
|
sealed record TrimmedPatchVerification(
|
|
bool Verified,
|
|
string Status,
|
|
string Method,
|
|
double OverlapMeasure,
|
|
int SampleHits,
|
|
double MinDistanceMm,
|
|
double RepresentativeNormalDot = double.NaN)
|
|
{
|
|
public static TrimmedPatchVerification Unverified(string method) =>
|
|
new(false, "unverified_trimmed_domain", method, 0, 0, double.PositiveInfinity);
|
|
|
|
public static TrimmedPatchVerification Rejected(string method) =>
|
|
new(false, "extension_only_rejected", method, 0, 0, double.PositiveInfinity);
|
|
}
|
|
|
|
static class Vec
|
|
{
|
|
public static double[] Normalize(double[] v)
|
|
{
|
|
var n = Norm(v);
|
|
return n < 1e-9 ? [0, 0, 0] : v.Select(x => x / n).ToArray();
|
|
}
|
|
public static double Norm(double[] v) => Math.Sqrt(v.Sum(x => x * x));
|
|
public static double Dot(double[] a, double[] b) => a.Zip(b, (x, y) => x * y).Sum();
|
|
public static double[] Add(double[] a, double[] b) => a.Zip(b, (x, y) => x + y).ToArray();
|
|
public static double[] Sub(double[] a, double[] b) => a.Zip(b, (x, y) => x - y).ToArray();
|
|
public static double[] Mul(double[] a, double s) => a.Select(x => x * s).ToArray();
|
|
public 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]
|
|
];
|
|
public static double[] Round(double[]? v) => v == null ? [] : v.Select(x => Math.Round(x, 3)).ToArray();
|
|
}
|