using SolidWorks.Interop.sldworks; static class AssemblyModelProcessing { public static List 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 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 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 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 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(); 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 BuildPhysicalInterfaceGroups( IReadOnlyList contacts, IReadOnlyList externalInterfaceRoots) { var planarProfilePairKeys = BuildPlanarProfilePairKeys(contacts); var groups = new List(); 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 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 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 refs, List 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 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 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 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 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 BuildPlanarProfilePairKeys(IReadOnlyList contacts) { var result = new HashSet(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(); 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 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 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 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 components) { var center = AverageVectors(contact.CenterA, contact.CenterB); var assemblyBox = UnionBoxes(components.Select(component => component.BBoxMm)); var outward = Array.Empty(); 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 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 points, double[] axis) { var values = points.Select(point => Vec.Dot(point, axis)).ToList(); return (values.Min(), values.Max()); } static List 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 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 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 Contacts { get; set; } = []; public List FaceARefs { get; set; } = []; public List FaceBRefs { get; set; } = []; public List FaceAIndices { get; set; } = []; public List FaceBIndices { get; set; } = []; public List RuntimeFaces { get; set; } = []; public double AnalysisPriority { get; set; } public string ConfidenceTier { get; set; } = ""; }