first commit

This commit is contained in:
2026-07-17 17:45:56 +08:00
commit 04c487823b
5873 changed files with 12227228 additions and 0 deletions
@@ -0,0 +1,826 @@
using SolidWorks.Interop.sldworks;
static class AssemblyModelProcessing
{
public static List<ComponentHighlightImageRequest> BuildComponentHighlightImageRequests(SectionBrepReport report)
{
var componentsById = report.AssemblyComponents
.ToDictionary(component => component.Id, StringComparer.OrdinalIgnoreCase);
var designTargets = report.PartImagePlan
.Where(item => !string.IsNullOrWhiteSpace(item.ComponentId))
.Where(item => IsTopLevelAssemblyComponent(item.InstanceName))
.GroupBy(item => item.ComponentId, StringComparer.OrdinalIgnoreCase)
.Select(group => group.First())
.Select(item => new ComponentHighlightImageRequest
{
PlanId = item.AssemblyContextImagePlanId,
ComponentId = item.ComponentId,
InstanceName = item.InstanceName,
DisplayName = item.DisplayName,
ComponentPath = item.ComponentPath,
Views = ["best_oblique"],
PreferredDirectionMm = componentsById.TryGetValue(item.ComponentId, out var component)
? ChooseComponentHighlightDirection(component, report.AssemblyComponents)
: [],
ShowComponents =
[
BuildComponentImageRef(
componentsById.GetValueOrDefault(item.ComponentId),
item.InstanceName,
item.DisplayName,
item.ComponentPath)
]
})
.ToList();
return designTargets;
}
static bool IsTopLevelAssemblyComponent(string instanceName)
{
return !string.IsNullOrWhiteSpace(instanceName) &&
!instanceName.Contains("/", StringComparison.Ordinal) &&
!instanceName.Contains("\\", StringComparison.Ordinal);
}
public static List<ComponentContextImageRequest> BuildComponentContextImageRequests(SectionBrepReport report)
{
var componentsById = report.AssemblyComponents
.ToDictionary(component => component.Id, StringComparer.OrdinalIgnoreCase);
return report.ComponentContextImagePlan
.Where(item => ShouldExportComponentContext(item, report.ExternalInterfaceRoots))
.Where(item => item.ShowComponents.Count > 0)
.Select(item =>
{
var showComponents = item.ShowComponents
.Select(component => BuildComponentImageRef(component, componentsById))
.ToList();
var preserveComponents = item.BoundaryComponents
.Select(component => BuildComponentImageRef(component, componentsById))
.ToList();
var protectedIds = showComponents
.Concat(preserveComponents)
.Select(component => component.ComponentId)
.ToHashSet(StringComparer.OrdinalIgnoreCase);
var hideComponents = item.MemberOcclusionAnalyses
.SelectMany(analysis => analysis.Directions)
.Where(direction => direction.Occluded)
.SelectMany(direction => direction.FirstHitComponents)
.Where(blocker => !protectedIds.Contains(blocker.ComponentId))
.DistinctBy(blocker => $"{blocker.InstanceName}::{blocker.ComponentPath}", StringComparer.OrdinalIgnoreCase)
.Select(blocker => new ComponentImageRef
{
ComponentId = blocker.ComponentId,
InstanceName = blocker.InstanceName,
DisplayName = blocker.DisplayName,
ComponentPath = blocker.ComponentPath,
BBoxMm = componentsById.TryGetValue(blocker.ComponentId, out var blockerSummary)
? blockerSummary.BBoxMm
: []
})
.ToList();
return new ComponentContextImageRequest
{
PlanId = item.Id,
GroupId = item.GroupId,
TargetComponentId = item.TargetComponentId,
TargetInstanceName = item.TargetInstanceName,
TargetDisplayName = item.TargetDisplayName,
TargetComponentPath = item.TargetComponentPath,
Views = ["best_oblique"],
PreferredDirectionMm = componentsById.TryGetValue(item.TargetComponentId, out var component)
? ChooseComponentHighlightDirection(component, report.AssemblyComponents)
: [],
ShowComponents = showComponents,
PreserveComponents = preserveComponents,
HideComponents = hideComponents
};
})
.ToList();
}
static bool ShouldExportComponentContext(ComponentContextImagePlanItem item, IReadOnlyList<string> externalInterfaceRoots)
{
if (externalInterfaceRoots.Count == 0)
return true;
// Purchased-component hints are semantic hints, not a switch to disable
// all component context images. But ordinary component-context images
// should not expand the inside of a user-specified purchased module.
// Those internal contacts are represented by physical-interface images
// when they cross the purchased-module boundary.
return !IsExternalInterfaceRootDescendant(item.TargetInstanceName, item.TargetComponentPath, externalInterfaceRoots);
}
static bool IsExternalInterfaceRootDescendant(string instanceName, string path, IReadOnlyList<string> roots)
{
var name = NormalizeRootMatchText(instanceName);
var strippedName = NormalizeRootMatchText(StripInstanceSuffix(instanceName));
var file = NormalizeRootMatchText(Path.GetFileNameWithoutExtension(path));
foreach (var root in roots.Select(NormalizeRootMatchText).Where(root => root.Length > 0))
{
var exactRoot =
name.Equals(root, StringComparison.OrdinalIgnoreCase) ||
strippedName.Equals(root, StringComparison.OrdinalIgnoreCase) ||
file.Equals(root, StringComparison.OrdinalIgnoreCase);
if (exactRoot)
return false;
if (name.StartsWith(root + "-", StringComparison.OrdinalIgnoreCase) ||
name.StartsWith(root + "/", StringComparison.OrdinalIgnoreCase) ||
strippedName.StartsWith(root + "-", StringComparison.OrdinalIgnoreCase) ||
strippedName.StartsWith(root + "/", StringComparison.OrdinalIgnoreCase))
{
return true;
}
}
return false;
}
public static List<InterfaceHighlightImageRequest> BuildInterfaceHighlightImageRequests(
SectionBrepReport report,
int maxRequests = 30,
bool compactViews = false)
{
if (!report.DocumentKind.Equals("assembly", StringComparison.OrdinalIgnoreCase) ||
report.AssemblyFaceContacts.Count == 0)
{
return [];
}
var components = report.AssemblyComponents;
var groups = BuildPhysicalInterfaceGroups(report.AssemblyFaceContacts, report.ExternalInterfaceRoots)
.OrderByDescending(group => group.AnalysisPriority)
.ThenByDescending(group => group.Contacts.Max(contact => contact.Confidence))
.Take(Math.Max(0, maxRequests))
.ToList();
var requests = new List<InterfaceHighlightImageRequest>();
foreach (var group in groups)
{
var contact = group.Contacts[0];
var componentA = FindComponentSummary(components, contact.ComponentA, contact.ComponentPathA);
var componentB = FindComponentSummary(components, contact.ComponentB, contact.ComponentPathB);
var refA = BuildComponentImageRef(componentA, contact.ComponentA, contact.ComponentDisplayNameA, contact.ComponentPathA);
var refB = BuildComponentImageRef(componentB, contact.ComponentB, contact.ComponentDisplayNameB, contact.ComponentPathB);
var axis = RepresentativeAxis(group.Contacts);
var normal = RepresentativeNormal(group.Contacts);
var interfaceBox = UnionBoxes(group.Contacts.SelectMany(item => new[] { item.BBoxA, item.BBoxB }));
var isCylindrical = contact.ContactKind.Contains("cylindrical", StringComparison.OrdinalIgnoreCase);
var verificationStatuses = group.Contacts
.Select(item => item.VerificationStatus)
.Where(status => !string.IsNullOrWhiteSpace(status))
.Distinct(StringComparer.OrdinalIgnoreCase)
.ToList();
requests.Add(new InterfaceHighlightImageRequest
{
PlanId = $"physical_interface_highlight_{SanitizeId(group.Id)}",
InterfaceId = group.Id,
ContactKind = contact.ContactKind,
MateRole = contact.MateRole,
ComponentA = refA,
ComponentB = refB,
FaceARef = group.FaceARefs[0],
FaceBRef = group.FaceBRefs[0],
FaceAIndex = group.FaceAIndices[0],
FaceBIndex = group.FaceBIndices[0],
FaceARefs = group.FaceARefs,
FaceBRefs = group.FaceBRefs,
FaceAIndices = group.FaceAIndices,
FaceBIndices = group.FaceBIndices,
RuntimeFaces = group.RuntimeFaces,
SourceContactIds = group.Contacts.Select(item => item.Id).Distinct(StringComparer.OrdinalIgnoreCase).ToList(),
Views = compactViews
? ["assembled_oblique"]
: isCylindrical
? ["assembled_oblique", "axis_end", "axis_side"]
: ["assembled_oblique", "normal_view"],
PreferredDirectionMm = ChooseInterfaceHighlightDirection(contact, components),
InterfaceAxisMm = axis,
InterfaceNormalMm = normal,
InterfaceCenterMm = AverageCenters(group.Contacts),
InterfaceBBoxMm = interfaceBox,
AnalysisPriority = group.AnalysisPriority,
ConfidenceTier = group.ConfidenceTier,
EvidencePurpose = "physical_interface_function_analysis",
TrimmedPatchVerified = group.Contacts.All(item => item.TrimmedPatchVerified),
VerificationStatus = verificationStatuses.Count == 1
? verificationStatuses[0]
: "mixed_verified_interface",
VerificationMethods = group.Contacts
.Select(item => item.VerificationMethod)
.Where(method => !string.IsNullOrWhiteSpace(method))
.Distinct(StringComparer.OrdinalIgnoreCase)
.ToList(),
VerificationSampleHits = group.Contacts.Sum(item => item.VerificationSampleHits),
VerificationMinDistanceMm = group.Contacts.Min(item => item.VerificationMinDistanceMm),
DisplayMode = "actual_assembly_position_with_native_selected_interface_faces",
ActualAssemblyPosition = true,
NotActualClearance = true,
ShowComponents = [refA, refB],
PreserveComponents = []
});
}
return requests;
}
static List<PhysicalInterfaceGroup> BuildPhysicalInterfaceGroups(
IReadOnlyList<AssemblyFaceContactEvidence> contacts,
IReadOnlyList<string> externalInterfaceRoots)
{
var planarProfilePairKeys = BuildPlanarProfilePairKeys(contacts);
var groups = new List<PhysicalInterfaceGroup>();
foreach (var contact in contacts
.Where(contact => contact.FaceA > 0 && contact.FaceB > 0)
.Where(IsConfirmedInterfaceContact)
.Where(contact => IsExternalPurchasedInterfaceContact(contact, externalInterfaceRoots))
.Where(contact => ShouldExportFitLikeInterface(contact, planarProfilePairKeys)))
{
var key = PhysicalInterfaceKey(contact);
var group = groups.FirstOrDefault(candidate =>
candidate.BaseKey.Equals(key, StringComparison.OrdinalIgnoreCase) &&
candidate.Contacts.Any(existing => SamePhysicalInterfaceSegment(existing, contact)));
if (group == null)
{
group = new PhysicalInterfaceGroup
{
BaseKey = key,
Id = $"physical_interface_{groups.Count + 1:000}_{ShortInterfaceToken(contact.ComponentFileNameA, contact.ComponentA)}__{ShortInterfaceToken(contact.ComponentFileNameB, contact.ComponentB)}"
};
groups.Add(group);
}
// Keep every group oriented like its first contact so face-side identity remains stable.
var first = group.Contacts.FirstOrDefault();
var sameOrientation = first == null || SameComponentIdentity(first.ComponentA, first.ComponentPathA, contact.ComponentA, contact.ComponentPathA);
group.Contacts.Add(contact);
if (sameOrientation)
{
AddFace(group.FaceARefs, group.FaceAIndices, contact.ComponentA, contact.FaceA);
AddFace(group.FaceBRefs, group.FaceBIndices, contact.ComponentB, contact.FaceB);
AddRuntimeFace(group.RuntimeFaces, contact.RuntimeFaceA);
AddRuntimeFace(group.RuntimeFaces, contact.RuntimeFaceB);
}
else
{
AddFace(group.FaceARefs, group.FaceAIndices, contact.ComponentB, contact.FaceB);
AddFace(group.FaceBRefs, group.FaceBIndices, contact.ComponentA, contact.FaceA);
AddRuntimeFace(group.RuntimeFaces, contact.RuntimeFaceB);
AddRuntimeFace(group.RuntimeFaces, contact.RuntimeFaceA);
}
}
foreach (var group in groups)
{
var text = string.Join(" ", group.Contacts.Select(contact =>
$"{contact.ContactKind} {contact.MateRole} {contact.ComponentCategoryA} {contact.ComponentCategoryB} {contact.ComponentDisplayNameA} {contact.ComponentDisplayNameB}"));
var containsKnownStandardPart = ContainsAny(text, "bearing", "轴承", "bolt", "screw", "nut", "pin", "key", "螺", "销", "键");
var geometryConfidence = group.Contacts.Max(contact => contact.Confidence);
group.AnalysisPriority = (containsKnownStandardPart ? 100 : 0)
+ (group.Contacts.Any(contact => contact.ContactKind.Contains("cylindrical", StringComparison.OrdinalIgnoreCase)) ? 30 : 10)
+ geometryConfidence * 20
+ Math.Min(10, group.Contacts.Count);
group.ConfidenceTier = containsKnownStandardPart && geometryConfidence >= 0.75
? "anchor"
: geometryConfidence >= 0.75 ? "high" : geometryConfidence >= 0.45 ? "medium" : "low";
}
return groups;
}
// Physical-interface images are boundary evidence only:
// exactly one side must be a purchased/standard component and the other
// side must be a top-level external non-purchased component. Internal
// purchased-to-purchased or purchased-to-internal contacts remain in the
// B-rep contact report but are not exported as physical-interface images.
static bool IsExternalPurchasedInterfaceContact(AssemblyFaceContactEvidence contact, IReadOnlyList<string> externalInterfaceRoots)
{
if (externalInterfaceRoots.Count > 0)
{
var aIsRoot = IsExternalInterfaceRootMember(contact.ComponentA, contact.ComponentPathA, externalInterfaceRoots);
var bIsRoot = IsExternalInterfaceRootMember(contact.ComponentB, contact.ComponentPathB, externalInterfaceRoots);
return aIsRoot ^ bIsRoot;
}
var purchasedA = IsPurchasedInterfaceComponent(contact.ComponentCategoryA, contact.ComponentA, contact.ComponentPathA);
var purchasedB = IsPurchasedInterfaceComponent(contact.ComponentCategoryB, contact.ComponentB, contact.ComponentPathB);
if (purchasedA == purchasedB)
return false;
var externalOther = purchasedA
? contact.ComponentB
: contact.ComponentA;
var otherCategory = purchasedA
? contact.ComponentCategoryB
: contact.ComponentCategoryA;
var otherPath = purchasedA
? contact.ComponentPathB
: contact.ComponentPathA;
return !IsPurchasedInterfaceComponent(otherCategory, externalOther, otherPath) &&
IsTopLevelAssemblyComponent(externalOther);
}
static bool IsExternalInterfaceRootMember(string instanceName, string path, IReadOnlyList<string> roots)
{
var name = NormalizeRootMatchText(instanceName);
var strippedName = NormalizeRootMatchText(StripInstanceSuffix(instanceName));
var file = NormalizeRootMatchText(Path.GetFileNameWithoutExtension(path));
foreach (var root in roots.Select(NormalizeRootMatchText).Where(root => root.Length > 0))
{
if (name.Equals(root, StringComparison.OrdinalIgnoreCase) ||
strippedName.Equals(root, StringComparison.OrdinalIgnoreCase) ||
file.Equals(root, StringComparison.OrdinalIgnoreCase) ||
name.StartsWith(root + "-", StringComparison.OrdinalIgnoreCase) ||
name.StartsWith(root + "/", StringComparison.OrdinalIgnoreCase) ||
strippedName.StartsWith(root + "-", StringComparison.OrdinalIgnoreCase) ||
strippedName.StartsWith(root + "/", StringComparison.OrdinalIgnoreCase))
{
return true;
}
}
return false;
}
static string StripInstanceSuffix(string value) =>
System.Text.RegularExpressions.Regex.Replace(value ?? "", @"-\d+(?=/|$)", "");
static string NormalizeRootMatchText(string value) =>
(value ?? "").Trim().Replace('\\', '/');
static bool IsPurchasedInterfaceComponent(string category, string instanceName, string path)
{
if (category.Equals("bearing", StringComparison.OrdinalIgnoreCase) ||
category.Equals("purchased_motor", StringComparison.OrdinalIgnoreCase) ||
category.Equals("purchased_reducer", StringComparison.OrdinalIgnoreCase) ||
category.Equals("purchased_motor_reducer_assembly", StringComparison.OrdinalIgnoreCase) ||
category.Equals("user_purchased_component", StringComparison.OrdinalIgnoreCase))
{
return true;
}
var text = $"{category} {instanceName} {path}".ToLowerInvariant();
if (LooksLikeDesignedConnectorOrHousing(text))
return false;
return ContainsAny(
text,
"gb", "t70", "t276", "bearing", "bolt", "screw",
"csg", "shg", "mhc", "ec60", "purchased_motor", "purchased_reducer", "gearbox",
"motor", "servo", "reducer", "gear motor",
"\u8f74\u627f", "\u87ba\u9489", "\u87ba\u6813", "\u7535\u673a", "\u9a6c\u8fbe", "\u4f3a\u670d", "\u51cf\u901f\u5668", "\u51cf\u901f\u673a");
}
static bool LooksLikeDesignedConnectorOrHousing(string text)
{
return ContainsAny(
text,
"\u8fde\u63a5\u4ef6", "\u6cd5\u5170", "\u5916\u58f3", "\u58f3\u4f53", "\u652f\u67b6", "\u5ea7",
"connector", "adapter", "bracket", "flange", "housing", "mount");
}
static bool IsConfirmedInterfaceContact(AssemblyFaceContactEvidence contact) =>
contact.TrimmedPatchVerified &&
(contact.VerificationStatus.Equals("trimmed_interface_confirmed", StringComparison.OrdinalIgnoreCase) ||
contact.VerificationStatus.Equals("trimmed_cylindrical_domain_confirmed", StringComparison.OrdinalIgnoreCase) ||
contact.VerificationStatus.Equals("analytic_cylindrical_domain_confirmed", StringComparison.OrdinalIgnoreCase));
static string ShortInterfaceToken(string fileName, string componentName)
{
var token = SanitizeId(FirstNonEmpty(Path.GetFileNameWithoutExtension(fileName), componentName, "component"));
return token.Length <= 24 ? token : token[..24];
}
static bool SamePhysicalInterfaceSegment(AssemblyFaceContactEvidence a, AssemblyFaceContactEvidence b)
{
if (!a.ContactKind.Contains("cylindrical", StringComparison.OrdinalIgnoreCase) ||
!b.ContactKind.Contains("cylindrical", StringComparison.OrdinalIgnoreCase))
{
return true;
}
var axis = a.AxisA.Length >= 3 ? Vec.Normalize(a.AxisA) : Vec.Normalize(a.AxisB);
if (axis.Length < 3)
return true;
var intervalA = ContactAxialInterval(a, axis);
var intervalB = ContactAxialInterval(b, axis);
if (!intervalA.HasValue || !intervalB.HasValue)
return true;
var radius = new[] { a.RadiusAmm, a.RadiusBmm, b.RadiusAmm, b.RadiusBmm }.Where(value => value > 0).DefaultIfEmpty(1).Average();
var allowedGap = Math.Max(0.5, radius * 0.03);
return intervalA.Value.Min <= intervalB.Value.Max + allowedGap &&
intervalB.Value.Min <= intervalA.Value.Max + allowedGap;
}
static (double Min, double Max)? ContactAxialInterval(AssemblyFaceContactEvidence contact, double[] axis)
{
var boxes = new[] { contact.BBoxA, contact.BBoxB }.Where(box => box.Length >= 6).ToList();
if (boxes.Count == 0)
return null;
var values = boxes.SelectMany(BoxCorners).Select(point => Vec.Dot(point, axis)).ToList();
return (values.Min(), values.Max());
}
static string PhysicalInterfaceKey(AssemblyFaceContactEvidence contact)
{
var pair = ComponentPairKey(
FirstNonEmpty(contact.ComponentPathA, contact.ComponentA),
FirstNonEmpty(contact.ComponentPathB, contact.ComponentB));
if (!contact.ContactKind.Contains("cylindrical", StringComparison.OrdinalIgnoreCase))
return $"{pair}|planar_profile";
var axis = contact.AxisA.Length >= 3 ? Vec.Normalize(contact.AxisA) : Vec.Normalize(contact.AxisB);
axis = CanonicalDirection(axis);
var radius = new[] { contact.RadiusAmm, contact.RadiusBmm }.Where(value => value > 0).DefaultIfEmpty(0).Average();
var center = AverageVectors(contact.CenterA, contact.CenterB);
var axisOffset = axis.Length >= 3 && center.Length >= 3
? Vec.Sub(center, Vec.Mul(axis, Vec.Dot(center, axis)))
: [];
return string.Join("|", new[]
{
pair,
"cylindrical",
VectorKey(axis, 0.02),
VectorKey(axisOffset, Math.Max(0.1, radius * 0.01)),
$"r{Math.Round(radius, 2):0.00}"
});
}
static void AddFace(List<string> refs, List<int> indices, string component, int faceIndex)
{
if (faceIndex <= 0)
return;
var faceRef = $"{component}:face#{faceIndex}";
if (refs.Contains(faceRef, StringComparer.OrdinalIgnoreCase))
return;
refs.Add(faceRef);
indices.Add(faceIndex);
}
static void AddRuntimeFace(List<Face2> faces, Face2? face)
{
if (face != null && !faces.Any(existing => ReferenceEquals(existing, face)))
faces.Add(face);
}
static bool SameComponentIdentity(string nameA, string pathA, string nameB, string pathB) =>
!string.IsNullOrWhiteSpace(pathA) && !string.IsNullOrWhiteSpace(pathB)
? pathA.Equals(pathB, StringComparison.OrdinalIgnoreCase)
: nameA.Equals(nameB, StringComparison.OrdinalIgnoreCase);
static double[] RepresentativeAxis(IReadOnlyList<AssemblyFaceContactEvidence> contacts) =>
contacts.Select(contact => contact.AxisA.Length >= 3 ? contact.AxisA : contact.AxisB)
.FirstOrDefault(axis => axis.Length >= 3) is { Length: >= 3 } axis ? CanonicalDirection(Vec.Normalize(axis)) : [];
static double[] RepresentativeNormal(IReadOnlyList<AssemblyFaceContactEvidence> contacts) =>
contacts.Select(contact => contact.NormalA.Length >= 3 ? contact.NormalA : contact.NormalB)
.FirstOrDefault(normal => normal.Length >= 3) is { Length: >= 3 } normal ? CanonicalDirection(Vec.Normalize(normal)) : [];
static double[] AverageCenters(IReadOnlyList<AssemblyFaceContactEvidence> contacts)
{
var centers = contacts.Select(contact => AverageVectors(contact.CenterA, contact.CenterB)).Where(center => center.Length >= 3).ToList();
return centers.Count == 0
? []
: [centers.Average(center => center[0]), centers.Average(center => center[1]), centers.Average(center => center[2])];
}
static double[] CanonicalDirection(double[] direction)
{
if (direction.Length < 3)
return [];
var first = direction.FirstOrDefault(value => Math.Abs(value) > 1e-6);
return first < 0 ? Vec.Mul(direction, -1) : direction;
}
static string VectorKey(double[] vector, double tolerance)
{
if (vector.Length < 3)
return "none";
tolerance = Math.Max(tolerance, 1e-6);
return string.Join(",", vector.Take(3).Select(value => Math.Round(value / tolerance).ToString(System.Globalization.CultureInfo.InvariantCulture)));
}
static HashSet<string> BuildPlanarProfilePairKeys(IReadOnlyList<AssemblyFaceContactEvidence> contacts)
{
var result = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
foreach (var group in contacts
.Where(contact => contact.ContactKind.Equals("planar_face_contact_candidate", StringComparison.OrdinalIgnoreCase))
.GroupBy(contact => ComponentPairKey(contact.ComponentA, contact.ComponentB), StringComparer.OrdinalIgnoreCase))
{
var items = group.ToList();
if (items.Count < 2)
continue;
var normalFamilies = new List<double[]>();
foreach (var contact in items)
{
var normal = contact.NormalA.Length >= 3 ? Vec.Normalize(contact.NormalA) : [];
if (normal.Length < 3)
continue;
var existing = normalFamilies.Any(family => Math.Abs(Vec.Dot(family, normal)) >= 0.92);
if (!existing)
normalFamilies.Add(normal);
}
// Multi-direction planar contacts indicate prismatic/profile fits such as square shafts,
// rectangular bosses, slots, or keyed interfaces. Same-direction multi-face groups are
// kept as weaker profile candidates; ordinary single flat mounting contacts are excluded.
if (normalFamilies.Count >= 2 || items.Count >= 3)
result.Add(group.Key);
}
return result;
}
static bool ShouldExportFitLikeInterface(AssemblyFaceContactEvidence contact, IReadOnlySet<string> planarProfilePairKeys)
{
if (contact.ContactKind.Equals("cylindrical_coaxial_fit_candidate", StringComparison.OrdinalIgnoreCase))
return true;
if (!contact.ContactKind.Equals("planar_face_contact_candidate", StringComparison.OrdinalIgnoreCase))
return false;
return planarProfilePairKeys.Contains(ComponentPairKey(contact.ComponentA, contact.ComponentB));
}
static ComponentImageRef BuildComponentImageRef(
ComponentContextMember component,
IReadOnlyDictionary<string, AssemblyComponentSummary> componentsById)
{
return new ComponentImageRef
{
ComponentId = component.ComponentId,
InstanceName = component.InstanceName,
DisplayName = component.DisplayName,
ComponentPath = component.ComponentPath,
BBoxMm = componentsById.TryGetValue(component.ComponentId, out var summary)
? summary.BBoxMm
: []
};
}
static ComponentImageRef BuildComponentImageRef(
AssemblyComponentSummary? component,
string instanceName,
string displayName,
string componentPath)
{
return new ComponentImageRef
{
ComponentId = component?.Id ?? SanitizeId(FirstNonEmpty(instanceName, displayName, Path.GetFileNameWithoutExtension(componentPath), "component")),
InstanceName = component?.InstanceName ?? instanceName,
DisplayName = component?.DisplayName ?? displayName,
ComponentPath = component?.Path ?? componentPath,
BBoxMm = component?.BBoxMm ?? []
};
}
static AssemblyComponentSummary? FindComponentSummary(
IReadOnlyList<AssemblyComponentSummary> components,
string instanceName,
string componentPath)
{
return components.FirstOrDefault(component =>
!string.IsNullOrWhiteSpace(instanceName) &&
component.InstanceName.Equals(instanceName, StringComparison.OrdinalIgnoreCase) &&
(string.IsNullOrWhiteSpace(componentPath) ||
string.IsNullOrWhiteSpace(component.Path) ||
component.Path.Equals(componentPath, StringComparison.OrdinalIgnoreCase)))
?? components.FirstOrDefault(component =>
!string.IsNullOrWhiteSpace(componentPath) &&
!string.IsNullOrWhiteSpace(component.Path) &&
component.Path.Equals(componentPath, StringComparison.OrdinalIgnoreCase));
}
static double ContactPriority(AssemblyFaceContactEvidence contact)
{
var text = $"{contact.ContactKind} {contact.MateRole} {contact.FaceRoleA} {contact.FaceRoleB}".ToLowerInvariant();
if (text.Contains("cylindrical") || text.Contains("bearing") || text.Contains("hinge") || text.Contains("fit"))
return 3.0;
if (text.Contains("planar") || text.Contains("profile") || text.Contains("slot") || text.Contains("key"))
return 1.0;
return 0.0;
}
static string ComponentPairKey(string a, string b) =>
string.Compare(a, b, StringComparison.OrdinalIgnoreCase) <= 0
? $"{a}||{b}"
: $"{b}||{a}";
static double[] ChooseInterfaceHighlightDirection(
AssemblyFaceContactEvidence contact,
IReadOnlyList<AssemblyComponentSummary> components)
{
var center = AverageVectors(contact.CenterA, contact.CenterB);
var assemblyBox = UnionBoxes(components.Select(component => component.BBoxMm));
var outward = Array.Empty<double>();
if (center.Length >= 3 && assemblyBox.Length >= 6)
{
var away = Vec.Sub(center, BoxCenter(assemblyBox));
if (Vec.Norm(away) > 1.0)
outward = Vec.Normalize(away);
}
var axis = contact.AxisA.Length >= 3 ? Vec.Normalize(contact.AxisA) :
contact.AxisB.Length >= 3 ? Vec.Normalize(contact.AxisB) : [];
var normal = contact.NormalA.Length >= 3 ? contact.NormalA : contact.NormalB;
normal = normal.Length >= 3 ? Vec.Normalize(normal) : [];
var isCylindrical = contact.ContactKind.Contains("cylindrical", StringComparison.OrdinalIgnoreCase);
var componentA = FindComponentSummary(components, contact.ComponentA, contact.ComponentPathA);
var componentB = FindComponentSummary(components, contact.ComponentB, contact.ComponentPathB);
var boxA = componentA?.BBoxMm is { Length: >= 6 } ? componentA.BBoxMm : contact.BBoxA;
var boxB = componentB?.BBoxMm is { Length: >= 6 } ? componentB.BBoxMm : contact.BBoxB;
return InterfaceObliqueCandidateDirections()
.OrderByDescending(direction => ScoreInterfaceViewDirection(direction, boxA, boxB, outward, axis, normal, isCylindrical))
.First();
}
static double ScoreInterfaceViewDirection(double[] direction, double[] boxA, double[] boxB, double[] outward, double[] axis, double[] normal, bool isCylindrical)
{
var visibilityScore = ScorePairProjectedVisibility(boxA, boxB, direction);
var geometryPreference = 0.0;
if (outward.Length >= 3)
geometryPreference += 0.8 * Math.Max(0.0, Vec.Dot(direction, outward));
if (isCylindrical && axis.Length >= 3)
geometryPreference += 0.7 * (1.0 - Math.Abs(Vec.Dot(direction, axis)));
if (!isCylindrical && normal.Length >= 3)
geometryPreference += 0.7 * Math.Abs(Vec.Dot(direction, normal));
return visibilityScore * (1.0 + geometryPreference);
}
static double ScorePairProjectedVisibility(double[] boxA, double[] boxB, double[] direction)
{
if (boxA.Length < 6 || boxB.Length < 6)
return 1.0;
var projectionA = ProjectBoxForInterface(boxA, direction);
var projectionB = ProjectBoxForInterface(boxB, direction);
var areaA = projectionA.Area;
var areaB = projectionB.Area;
var overlap = OverlapArea(projectionA, projectionB);
var unionArea = Math.Max(1.0, areaA + areaB - overlap);
var smallerArea = Math.Max(1.0, Math.Min(areaA, areaB));
var largerArea = Math.Max(smallerArea, Math.Max(areaA, areaB));
var overlapRatio = Math.Min(1.0, overlap / smallerArea);
var balance = smallerArea / largerArea;
// Contact faces are usually hidden; favor views where both components expose
// enough surrounding exterior area instead of one box covering the other.
return unionArea * (1.0 - overlapRatio * 0.85) * (0.75 + balance * 0.25);
}
static InterfaceProjectionRect ProjectBoxForInterface(double[] box, double[] direction)
{
var d = Vec.Normalize(direction);
var reference = Math.Abs(Vec.Dot(d, [0.0, 1.0, 0.0])) > 0.92
? new[] { 0.0, 0.0, 1.0 }
: [0.0, 1.0, 0.0];
var u = Vec.Normalize(Vec.Cross(reference, d));
var v = Vec.Normalize(Vec.Cross(d, u));
var corners = BoxCorners(box);
var uRange = ProjectionRange(corners, u);
var vRange = ProjectionRange(corners, v);
return new InterfaceProjectionRect(uRange.Min, uRange.Max, vRange.Min, vRange.Max);
}
static double OverlapArea(InterfaceProjectionRect a, InterfaceProjectionRect b)
{
var u = Math.Max(0.0, Math.Min(a.UMax, b.UMax) - Math.Max(a.UMin, b.UMin));
var v = Math.Max(0.0, Math.Min(a.VMax, b.VMax) - Math.Max(a.VMin, b.VMin));
return u * v;
}
static List<double[]> BoxCorners(double[] box) =>
[
[box[0], box[1], box[2]],
[box[0], box[1], box[5]],
[box[0], box[4], box[2]],
[box[0], box[4], box[5]],
[box[3], box[1], box[2]],
[box[3], box[1], box[5]],
[box[3], box[4], box[2]],
[box[3], box[4], box[5]]
];
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 List<double[]> InterfaceObliqueCandidateDirections() =>
[
Vec.Normalize([1.0, 1.0, 1.0]),
Vec.Normalize([1.0, 1.0, -1.0]),
Vec.Normalize([1.0, -1.0, 1.0]),
Vec.Normalize([1.0, -1.0, -1.0]),
Vec.Normalize([-1.0, 1.0, 1.0]),
Vec.Normalize([-1.0, 1.0, -1.0]),
Vec.Normalize([-1.0, -1.0, 1.0]),
Vec.Normalize([-1.0, -1.0, -1.0])
];
static double[] AverageVectors(double[] a, double[] b)
{
if (a.Length < 3 && b.Length < 3)
return [];
if (a.Length < 3)
return b.Take(3).ToArray();
if (b.Length < 3)
return a.Take(3).ToArray();
return [(a[0] + b[0]) / 2.0, (a[1] + b[1]) / 2.0, (a[2] + b[2]) / 2.0];
}
static bool ContainsAny(string value, params string[] tokens) =>
tokens.Any(token => value.Contains(token, StringComparison.OrdinalIgnoreCase));
static double[] ChooseComponentHighlightDirection(
AssemblyComponentSummary component,
IReadOnlyList<AssemblyComponentSummary> allComponents)
{
if (component.BBoxMm.Length < 6)
return [1.0, 1.0, 1.0];
var assemblyBox = UnionBoxes(allComponents.Select(item => item.BBoxMm));
if (assemblyBox.Length < 6)
return [1.0, 1.0, 1.0];
var componentCenter = BoxCenter(component.BBoxMm);
var assemblyCenter = BoxCenter(assemblyBox);
var delta = componentCenter.Zip(assemblyCenter, (a, b) => a - b).ToArray();
double Axis(double value, int index)
{
if (Math.Abs(value) > 1.0)
return Math.Sign(value);
var componentSpan = Math.Abs(component.BBoxMm[index + 3] - component.BBoxMm[index]);
var assemblySpan = Math.Abs(assemblyBox[index + 3] - assemblyBox[index]);
return componentSpan >= assemblySpan * 0.45 ? 1.0 : (index == 1 ? 1.0 : -1.0);
}
return Vec.Normalize([Axis(delta[0], 0), Axis(delta[1], 1), Axis(delta[2], 2)]);
}
static double[] UnionBoxes(IEnumerable<double[]> boxes)
{
var valid = boxes.Where(box => box.Length >= 6).ToList();
if (valid.Count == 0)
return [];
return
[
valid.Min(box => box[0]),
valid.Min(box => box[1]),
valid.Min(box => box[2]),
valid.Max(box => box[3]),
valid.Max(box => box[4]),
valid.Max(box => box[5])
];
}
static double[] BoxCenter(double[] box) =>
box.Length >= 6
? [(box[0] + box[3]) / 2.0, (box[1] + box[4]) / 2.0, (box[2] + box[5]) / 2.0]
: [];
static string SanitizeId(string value)
{
var chars = value.Select(ch => char.IsLetterOrDigit(ch) ? ch : '_').ToArray();
return new string(chars).Trim('_');
}
static string FirstNonEmpty(params string[] values) =>
values.FirstOrDefault(value => !string.IsNullOrWhiteSpace(value)) ?? "";
}
readonly record struct InterfaceProjectionRect(double UMin, double UMax, double VMin, double VMax)
{
public double Area => Math.Max(1.0, (UMax - UMin) * (VMax - VMin));
}
sealed class PhysicalInterfaceGroup
{
public string BaseKey { get; set; } = "";
public string Id { get; set; } = "";
public List<AssemblyFaceContactEvidence> Contacts { get; set; } = [];
public List<string> FaceARefs { get; set; } = [];
public List<string> FaceBRefs { get; set; } = [];
public List<int> FaceAIndices { get; set; } = [];
public List<int> FaceBIndices { get; set; } = [];
public List<Face2> RuntimeFaces { get; set; } = [];
public double AnalysisPriority { get; set; }
public string ConfidenceTier { get; set; } = "";
}