using SolidWorks.Interop.sldworks; using SolidWorks.Interop.swconst; using System.Globalization; using System.Runtime.InteropServices; using System.Text; using System.Text.Encodings.Web; using System.Text.Json; const int Asm = 2; const int Silent = 1; const int ReadOnly = 2; if (args.Length == 0) { Console.Error.WriteLine("StructuralFaultProbe requires one or more assembly paths."); return 2; } string outputDir = Path.Combine(System.Environment.CurrentDirectory, "runtime", "structural_fault_probe"); Directory.CreateDirectory(outputDir); var assemblyPaths = args; var sw = Connect(); var reports = new List(); foreach (string rawPath in assemblyPaths) { string asmPath = Path.GetFullPath(rawPath); if (!File.Exists(asmPath)) { reports.Add(new AssemblyReport { AssemblyPath = asmPath, Ok = false, Message = "Assembly file does not exist." }); continue; } reports.Add(AnalyzeAssembly(sw, asmPath)); } var jsonOptions = new JsonSerializerOptions { WriteIndented = true, Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping }; string jsonPath = Path.Combine(outputDir, "structural_fault_evidence.json"); File.WriteAllText(jsonPath, JsonSerializer.Serialize(reports, jsonOptions), new UTF8Encoding(false)); string mdPath = Path.Combine(outputDir, "structural_fault_evidence.md"); File.WriteAllText(mdPath, BuildMarkdown(reports), new UTF8Encoding(false)); Console.OutputEncoding = Encoding.UTF8; Console.WriteLine("Structural fault probe completed."); Console.WriteLine(jsonPath); Console.WriteLine(mdPath); return 0; [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() { try { int hr = CLSIDFromProgID("SldWorks.Application", out var clsid); if (hr < 0) Marshal.ThrowExceptionForHR(hr); return (SldWorks)GetActiveObject(ref clsid, IntPtr.Zero); } catch { var type = Type.GetTypeFromProgID("SldWorks.Application") ?? throw new InvalidOperationException("SolidWorks ProgID not found."); var sw = (SldWorks?)Activator.CreateInstance(type) ?? throw new InvalidOperationException("Cannot create SolidWorks.Application."); sw.Visible = true; return sw; } } static AssemblyReport AnalyzeAssembly(SldWorks sw, string asmPath) { var report = new AssemblyReport { AssemblyPath = asmPath, AssemblyName = Path.GetFileNameWithoutExtension(asmPath), Ok = true }; int errors = 0; int warnings = 0; var doc = sw.OpenDoc6(asmPath, Asm, Silent | ReadOnly, "", ref errors, ref warnings) as ModelDoc2; report.OpenErrors = errors; report.OpenWarnings = warnings; if (doc is not AssemblyDoc assy) { report.Ok = false; report.Message = $"OpenDoc6 failed or document is not an assembly. errors={errors}, warnings={warnings}"; return report; } report.AssemblyTitle = Safe(() => doc.GetTitle(), ""); dynamic mathUtility = sw.GetMathUtility(); var components = EnumerateComponents(assy); report.ComponentCount = components.Count; foreach (var component in components) { component.Faces = EnumerateFaces(component, mathUtility); component.FaceCount = component.Faces.Count; } report.Components = components .Select(c => new ComponentSummary { ComponentName = c.Name, PartName = c.PartName, Category = c.Category, Path = c.Path, FaceCount = c.FaceCount, CylinderFaces = c.Faces .Where(f => f.SurfaceType == "cylinder" && f.RadiusMm.HasValue && f.Axis != null && f.AxisPointMm != null) .OrderByDescending(f => f.AreaMm2) .Take(32) .Select(FaceSummary.From) .ToList() }) .OrderBy(c => c.Category) .ThenBy(c => c.ComponentName, StringComparer.OrdinalIgnoreCase) .ToList(); var frames = components .Select(c => new { Component = c, Frame = TryBuildAxialFrame(c) }) .Where(x => x.Frame != null) .ToDictionary(x => x.Component.Name, x => x.Frame!, StringComparer.OrdinalIgnoreCase); foreach (var component in components) { if (frames.TryGetValue(component.Name, out var frame)) report.ComponentFixations.Add(AnalyzeComponentFixation(component, components, frame)); } var bearings = components.Where(c => c.Category == "bearing").ToList(); report.BearingCount = bearings.Count; foreach (var bearing in bearings) { frames.TryGetValue(bearing.Name, out var frame); report.Bearings.Add(AnalyzeBearingByFaceContact(bearing, components, frame)); } report.Message = $"Analyzed {components.Count} components, {bearings.Count} bearing candidates."; return report; } static List EnumerateComponents(AssemblyDoc assy) { var result = new List(); foreach (object item in assy.GetComponents(false) as object[] ?? Array.Empty()) { if (item is not Component2 comp) continue; string name = Safe(() => comp.Name2, ""); string path = Safe(() => comp.GetPathName(), ""); string partName = string.IsNullOrWhiteSpace(path) ? StripInstanceSuffix(name) : Path.GetFileNameWithoutExtension(path); result.Add(new ComponentInfo { Component = comp, Name = name, Path = path, PartName = partName, Category = ClassifyComponent(name, partName, path) }); } return result; } static string ClassifyComponent(string componentName, string partName, string path) { string text = $"{componentName} {partName} {Path.GetFileName(path)}".ToLowerInvariant(); if (ContainsAny(text, "轴承", "bearing", "gbt276", "gb276", "gb╱t", "7211", "620", "621", "630", "631", "600", "601")) return "bearing"; if (ContainsAny(text, "端盖", "盖")) return "end_cap"; if (ContainsAny(text, "箱体", "箱座", "箱盖", "housing")) return "housing"; if (ContainsAny(text, "套筒", "隔套", "挡油环", "挡圈", "垫片", "螺母", "sleeve", "spacer", "ring")) return "spacer_or_ring"; if (ContainsAny(text, "齿轮", "gear")) return "gear"; if (ContainsAny(text, "轴", "shaft")) return "shaft"; return "other"; } static List EnumerateFaces(ComponentInfo component, dynamic mathUtility) { var result = new List(); var bodies = EnumerateComponentBodies(component.Component); int faceIndex = 0; foreach (var body in bodies) { for (object? faceObj = Safe(() => body.GetFirstFace(), null); faceObj != null;) { if (faceObj is not Face2 face) break; faceIndex++; var info = BuildFaceInfo(component, mathUtility, face, faceIndex); if (info != null) result.Add(info); faceObj = Safe(() => face.GetNextFace(), null); } } return result; } static List EnumerateComponentBodies(Component2 component) { var result = new List(); try { if (component.GetModelDoc2() is PartDoc partDoc) { object bodiesObj = partDoc.GetBodies2((int)swBodyType_e.swSolidBody, true); foreach (object bodyObj in ToObjectArray(bodiesObj)) if (bodyObj is Body2 body) result.Add(body); } } catch { } if (result.Count > 0) return result; try { object bodyInfo = null!; dynamic dynComponent = component; object bodiesObj = dynComponent.GetBodies3((int)swBodyType_e.swSolidBody, out bodyInfo); foreach (object bodyObj in ToObjectArray(bodiesObj)) if (bodyObj is Body2 body) result.Add(body); } catch { } return result; } static FaceInfo? BuildFaceInfo(ComponentInfo component, dynamic mathUtility, Face2 face, int faceIndex) { try { var surface = face.GetSurface() as Surface; if (surface == null) return null; var localBox = ToDoubleArray(face.GetBox()).Take(6).Select(v => v * 1000.0).ToArray(); if (localBox.Length < 6) return null; var box = TransformBBox(mathUtility, component.Component, localBox); var info = new FaceInfo { ComponentName = component.Name, ComponentCategory = component.Category, PartName = component.PartName, FaceIndex = faceIndex, BBoxMm = box, CenterMm = TransformPoint(mathUtility, component.Component, [(localBox[0] + localBox[3]) / 2.0, (localBox[1] + localBox[4]) / 2.0, (localBox[2] + localBox[5]) / 2.0]), AreaMm2 = Safe(() => face.GetArea() * 1_000_000.0, 0.0) }; AddLoopInfo(face, info); if (surface.IsPlane()) { var p = ToDoubleArray(GetComProperty(surface, "PlaneParams")); if (p.Length < 6) return null; info.SurfaceType = "plane"; info.Normal = Normalize(TransformVector(mathUtility, component.Component, [p[0], p[1], p[2]])); if (Safe(() => face.FaceInSurfaceSense(), true) == false) info.Normal = Scale(info.Normal, -1); info.RootPointMm = TransformPoint(mathUtility, component.Component, [p[3] * 1000.0, p[4] * 1000.0, p[5] * 1000.0]); info.PlaneOffsetMm = Dot(info.Normal, info.RootPointMm); AddCircularEdgeRadii(face, info); } else if (surface.IsCylinder()) { var c = ToDoubleArray(GetComProperty(surface, "CylinderParams")); if (c.Length < 7) return null; info.SurfaceType = "cylinder"; info.AxisPointMm = TransformPoint(mathUtility, component.Component, [c[0] * 1000.0, c[1] * 1000.0, c[2] * 1000.0]); info.Axis = Normalize(TransformVector(mathUtility, component.Component, [c[3], c[4], c[5]])); info.RadiusMm = c[6] * 1000.0; AddCylinderAxialRange(info); } else { info.SurfaceType = "other"; } return info; } catch { return null; } } static BearingReport AnalyzeBearing(ComponentInfo bearing, List components) { var report = new BearingReport { ComponentName = bearing.Name, PartName = bearing.PartName, Path = bearing.Path, FaceCount = bearing.FaceCount }; var cylinders = bearing.Faces .Where(f => f.SurfaceType == "cylinder" && f.RadiusMm.HasValue) .OrderByDescending(f => f.AreaMm2) .ToList(); report.CylinderFaces = cylinders .Take(12) .Select(f => FaceSummary.From(f)) .ToList(); var axisCluster = SelectBearingAxisCluster(cylinders); if (axisCluster.Count == 0) { report.Status = "unknown"; report.Message = "No usable bearing cylindrical faces found."; return report; } var outer = axisCluster.OrderByDescending(f => f.RadiusMm!.Value).First(); var inner = axisCluster.OrderBy(f => f.RadiusMm!.Value).First(); report.Axis = outer.Axis; report.AxisPointMm = outer.AxisPointMm; report.InnerRadiusMm = inner.RadiusMm; report.OuterRadiusMm = outer.RadiusMm; report.AxialMinMm = axisCluster.Min(f => f.AxialMinMm ?? Project(outer.AxisPointMm, outer)); report.AxialMaxMm = axisCluster.Max(f => f.AxialMaxMm ?? Project(outer.AxisPointMm, outer)); var planes = bearing.Faces .Where(f => f.SurfaceType == "plane") .Select(f => new { Face = f, Axial = Project(f.CenterMm, outer) }) .Where(x => x.Axial >= report.AxialMinMm - 1.0 && x.Axial <= report.AxialMaxMm + 1.0) .ToList(); report.EndFaces = planes .OrderBy(x => Math.Abs(x.Axial - report.AxialMinMm) < Math.Abs(x.Axial - report.AxialMaxMm) ? 0 : 1) .ThenByDescending(x => x.Face.AreaMm2) .Take(12) .Select(x => FaceSummary.From(x.Face)) .ToList(); var innerLeftCandidates = CollectAxialStopCandidates(bearing, components, outer, inner, report.AxialMinMm, -1, "inner"); var innerRightCandidates = CollectAxialStopCandidates(bearing, components, outer, inner, report.AxialMaxMm, 1, "inner"); var outerLeftCandidates = CollectAxialStopCandidates(bearing, components, outer, inner, report.AxialMinMm, -1, "outer"); var outerRightCandidates = CollectAxialStopCandidates(bearing, components, outer, inner, report.AxialMaxMm, 1, "outer"); report.StopCandidates.AddRange(innerLeftCandidates.Take(20)); report.StopCandidates.AddRange(innerRightCandidates.Take(20)); report.StopCandidates.AddRange(outerLeftCandidates.Take(20)); report.StopCandidates.AddRange(outerRightCandidates.Take(20)); report.InnerLeftStop = innerLeftCandidates.FirstOrDefault(c => c.Accepted); report.InnerRightStop = innerRightCandidates.FirstOrDefault(c => c.Accepted); report.OuterLeftStop = outerLeftCandidates.FirstOrDefault(c => c.Accepted); report.OuterRightStop = outerRightCandidates.FirstOrDefault(c => c.Accepted); report.RuleInterpretation = new RuleInterpretation { Principle = "One side inner ring fixed and the opposite side outer ring fixed is treated as correct for this experiment.", InnerLeft = report.InnerLeftStop != null, InnerRight = report.InnerRightStop != null, OuterLeft = report.OuterLeftStop != null, OuterRight = report.OuterRightStop != null }; bool patternA = report.InnerLeftStop != null && report.OuterRightStop != null; bool patternB = report.InnerRightStop != null && report.OuterLeftStop != null; report.RuleInterpretation.PatternInnerLeftOuterRight = patternA; report.RuleInterpretation.PatternInnerRightOuterLeft = patternB; report.RuleInterpretation.ExperimentalPass = patternA || patternB; report.Status = report.RuleInterpretation.ExperimentalPass ? "pass" : "fail"; report.Message = report.RuleInterpretation.ExperimentalPass ? "Detected opposite-side inner/outer axial stop evidence." : "Did not detect the required opposite-side inner/outer axial stop evidence."; return report; } static BearingReport AnalyzeBearingByFaceContact(ComponentInfo bearing, List components, AxialFrame? frame) { var report = new BearingReport { ComponentName = bearing.Name, PartName = bearing.PartName, Path = bearing.Path, FaceCount = bearing.FaceCount }; if (frame == null) { report.Status = "unknown"; report.Message = "No usable axial frame found for bearing."; return report; } report.Axis = frame.Axis; report.AxisPointMm = frame.AxisPointMm; report.AxialMinMm = frame.MinAxialMm; report.AxialMaxMm = frame.MaxAxialMm; report.InnerRadiusMm = frame.InnerRadiusMm; report.OuterRadiusMm = frame.OuterRadiusMm; report.CylinderFaces = bearing.Faces .Where(f => f.SurfaceType == "cylinder" && f.RadiusMm.HasValue) .OrderByDescending(f => f.AreaMm2) .Take(12) .Select(FaceSummary.From) .ToList(); var contacts = CollectSideContacts(bearing, components, frame); report.SideContacts = contacts; var leftInner = contacts.Where(c => c.Accepted && c.Side == "left" && c.BearingRing == "inner").OrderBy(c => c.Score).FirstOrDefault(); var leftOuter = contacts.Where(c => c.Accepted && c.Side == "left" && c.BearingRing == "outer").OrderBy(c => c.Score).FirstOrDefault(); var rightInner = contacts.Where(c => c.Accepted && c.Side == "right" && c.BearingRing == "inner").OrderBy(c => c.Score).FirstOrDefault(); var rightOuter = contacts.Where(c => c.Accepted && c.Side == "right" && c.BearingRing == "outer").OrderBy(c => c.Score).FirstOrDefault(); report.InnerLeftStop = leftInner?.ToStopEvidence(); report.InnerRightStop = rightInner?.ToStopEvidence(); report.OuterLeftStop = leftOuter?.ToStopEvidence(); report.OuterRightStop = rightOuter?.ToStopEvidence(); bool bothSidesContact = contacts.Any(c => c.Side == "left" && c.Accepted) && contacts.Any(c => c.Side == "right" && c.Accepted); bool patternA = leftInner != null && rightOuter != null; bool patternB = rightInner != null && leftOuter != null; report.RuleInterpretation = new RuleInterpretation { Principle = "Bearing must have contacts on both axial sides; valid special bearing fixation requires one side on inner ring and the opposite side on outer ring.", InnerLeft = leftInner != null, InnerRight = rightInner != null, OuterLeft = leftOuter != null, OuterRight = rightOuter != null, PatternInnerLeftOuterRight = patternA, PatternInnerRightOuterLeft = patternB, ExperimentalPass = bothSidesContact && (patternA || patternB) }; report.Status = report.RuleInterpretation.ExperimentalPass ? "pass" : "fail"; report.Message = report.RuleInterpretation.ExperimentalPass ? "Both sides are contacted and opposite sides contact inner/outer rings." : "Bearing does not satisfy both-side contact plus opposite inner/outer ring contact."; return report; } static ComponentFixationReport AnalyzeComponentFixation(ComponentInfo target, List components, AxialFrame frame) { var contacts = CollectSideContacts(target, components, frame); bool left = contacts.Any(c => c.Side == "left" && c.Accepted); bool right = contacts.Any(c => c.Side == "right" && c.Accepted); return new ComponentFixationReport { ComponentName = target.Name, PartName = target.PartName, Category = target.Category, Axis = Util.Round(frame.Axis), AxisPointMm = Util.Round(frame.AxisPointMm), AxialMinMm = Util.Round(frame.MinAxialMm), AxialMaxMm = Util.Round(frame.MaxAxialMm), InnerRadiusMm = Util.Round(frame.InnerRadiusMm), OuterRadiusMm = Util.Round(frame.OuterRadiusMm), LeftFixed = left, RightFixed = right, FixedBothSides = left && right, Status = left && right ? "fixed_both_sides" : "not_fixed_both_sides", Contacts = contacts.Take(40).ToList() }; } static AxialFrame? TryBuildAxialFrame(ComponentInfo component) { var cylinders = component.Faces .Where(f => f.SurfaceType == "cylinder" && f.Axis != null && f.AxisPointMm != null && f.RadiusMm.HasValue) .OrderByDescending(f => f.AreaMm2) .ToList(); if (cylinders.Count > 0) { var seed = cylinders.First(); var cluster = cylinders .Where(c => AreParallel(c.Axis!, seed.Axis!, 0.98)) .Where(c => DistancePointToLine(c.AxisPointMm!, seed.AxisPointMm!, seed.Axis!) < 1.0) .ToList(); if (cluster.Count > 0) { double clusterMin = cluster.Min(f => f.AxialMinMm ?? Project(f.CenterMm, seed)); double clusterMax = cluster.Max(f => f.AxialMaxMm ?? Project(f.CenterMm, seed)); var radii = cluster.Select(c => c.RadiusMm!.Value).OrderBy(r => r).ToList(); return new AxialFrame { Axis = seed.Axis!, AxisPointMm = seed.AxisPointMm!, MinAxialMm = clusterMin, MaxAxialMm = clusterMax, InnerRadiusMm = radii.First(), OuterRadiusMm = radii.Last() }; } } var box = ComponentBBox(component); if (box == null) return null; double dx = box[3] - box[0]; double dy = box[4] - box[1]; double dz = box[5] - box[2]; double[] axis; double min; double max; if (dx <= dy && dx <= dz) { axis = [1, 0, 0]; min = box[0]; max = box[3]; } else if (dy <= dx && dy <= dz) { axis = [0, 1, 0]; min = box[1]; max = box[4]; } else { axis = [0, 0, 1]; min = box[2]; max = box[5]; } return new AxialFrame { Axis = axis, AxisPointMm = [(box[0] + box[3]) / 2.0, (box[1] + box[4]) / 2.0, (box[2] + box[5]) / 2.0], MinAxialMm = min, MaxAxialMm = max, InnerRadiusMm = 0, OuterRadiusMm = Math.Max(dx, Math.Max(dy, dz)) / 2.0 }; } static List CollectSideContacts(ComponentInfo target, List components, AxialFrame frame) { var result = new List(); double sideToleranceMm = 1.5; double normalTolerance = 0.80; foreach (var other in components) { if (other.Name == target.Name) continue; foreach (var face in other.Faces.Where(f => f.SurfaceType == "plane" && f.Normal != null)) { double axial = ProjectOnFrame(face.CenterMm, frame); string? side = null; double gap = 0; if (Math.Abs(axial - frame.MinAxialMm) <= sideToleranceMm) { side = "left"; gap = Math.Abs(axial - frame.MinAxialMm); } else if (Math.Abs(axial - frame.MaxAxialMm) <= sideToleranceMm) { side = "right"; gap = Math.Abs(axial - frame.MaxAxialMm); } else { continue; } double axisNormalAbs = Math.Abs(Dot(face.Normal!, frame.Axis)); string ring = ClassifyBearingRingByCoverage(face, frame); var radialRange = FaceRadialRange(face, frame); bool overlap = RingCoverage(face, frame, ring); var reasons = new List(); if (axisNormalAbs < normalTolerance) reasons.Add($"plane is not perpendicular to axis enough: abs(dot)={Math.Round(axisNormalAbs, 4)}"); if (!overlap) reasons.Add("face bbox does not overlap target radial envelope"); result.Add(new SideContactEvidence { Side = side, ComponentName = other.Name, PartName = other.PartName, ComponentCategory = other.Category, FaceIndex = face.FaceIndex, CenterMm = Util.Round(face.CenterMm), Normal = Util.Round(face.Normal!), AxialPositionMm = Util.Round(axial), AxialGapMm = Util.Round(gap), AxisNormalAbs = Util.Round(axisNormalAbs), BearingRing = ring, FaceRadialMinMm = Util.Round(radialRange.Min), FaceRadialMaxMm = Util.Round(radialRange.Max), Accepted = reasons.Count == 0, RejectReasons = reasons, Score = Util.Round(gap * 10.0 + (1.0 - axisNormalAbs)), Reason = "plane at target axial side with radial overlap" }); } } return result .OrderBy(c => c.Side) .ThenBy(c => c.Score) .ToList(); } static string ClassifyBearingRingByCoverage(FaceInfo face, AxialFrame frame) { var (minR, maxR) = FaceRadialRange(face, frame); double inner = frame.InnerRadiusMm; double outer = frame.OuterRadiusMm; bool coversInner = inner <= 0 || (minR <= inner + 3.0 && maxR >= Math.Max(0.0, inner - 3.0)); bool coversOuter = maxR >= outer - 3.0 && minR <= outer + 3.0; if (coversInner && !coversOuter) return "inner"; if (coversOuter && !coversInner) return "outer"; if (coversInner && coversOuter) return "full"; return "unknown"; } static bool RingCoverage(FaceInfo face, AxialFrame frame, string ring) { var (minR, maxR) = FaceRadialRange(face, frame); if (frame.OuterRadiusMm <= 0) return true; return maxR >= Math.Max(1.0, frame.InnerRadiusMm - 3.0) && minR <= frame.OuterRadiusMm + 3.0; } static (double Min, double Max) FaceRadialRange(FaceInfo face, AxialFrame frame) { if (face.CircularEdgeRadiiMm.Count > 0) { var radii = face.CircularEdgeRadiiMm.OrderBy(r => r).ToList(); double min = radii.Count == 1 ? 0.0 : radii.First(); double max = radii.Last(); return (min, max); } var radials = BBoxCorners(face.BBoxMm).Select(p => DistancePointToLine(p, frame.AxisPointMm, frame.Axis)).ToList(); return (radials.Min(), radials.Max()); } static double[]? ComponentBBox(ComponentInfo component) { var boxes = component.Faces.Where(f => f.BBoxMm.Length >= 6).Select(f => f.BBoxMm).ToList(); if (boxes.Count == 0) return null; return [ boxes.Min(b => b[0]), boxes.Min(b => b[1]), boxes.Min(b => b[2]), boxes.Max(b => b[3]), boxes.Max(b => b[4]), boxes.Max(b => b[5]) ]; } static List SelectBearingAxisCluster(List cylinders) { if (cylinders.Count == 0) return []; FaceInfo seed = cylinders.OrderByDescending(c => c.AreaMm2).First(); return cylinders .Where(c => c.Axis != null && c.AxisPointMm != null && c.RadiusMm.HasValue) .Where(c => AreParallel(c.Axis!, seed.Axis!, 0.98)) .Where(c => DistancePointToLine(c.AxisPointMm!, seed.AxisPointMm!, seed.Axis!) < 0.5) .ToList(); } static List CollectAxialStopCandidates( ComponentInfo bearing, List components, FaceInfo outer, FaceInfo inner, double axialEnd, int side, string ring) { double innerRadius = inner.RadiusMm ?? 0.0; double outerRadius = outer.RadiusMm ?? 0.0; double targetRadius = ring == "inner" ? innerRadius : outerRadius; double radialTolerance = Math.Max(1.5, outerRadius * 0.08); double axialAcceptanceTolerance = 2.0; double axialDebugTolerance = 30.0; var candidates = new List(); foreach (var component in components) { if (component.Name == bearing.Name) continue; if (component.Category is "bearing" or "other") continue; foreach (var face in component.Faces) { if (face.SurfaceType != "plane" || face.Normal == null) continue; double axial = Project(face.CenterMm, outer); double axialGap = Math.Abs(axial - axialEnd); if (axialGap > axialDebugTolerance) continue; double radial = DistancePointToLine(face.CenterMm, outer.AxisPointMm!, outer.Axis!); if (radial > outerRadius + 80.0) continue; var reasons = new List(); bool categoryOk = ring == "inner" ? component.Category is "shaft" or "spacer_or_ring" or "gear" : component.Category is "end_cap" or "housing" or "spacer_or_ring"; if (!categoryOk) reasons.Add($"category {component.Category} is not valid for {ring} stop"); if (axialGap > axialAcceptanceTolerance) reasons.Add($"axial gap {Math.Round(axialGap, 4)} mm exceeds {axialAcceptanceTolerance} mm"); double axisNormalAbs = Math.Abs(Dot(face.Normal, outer.Axis!)); if (axisNormalAbs < 0.85) reasons.Add($"plane normal is not axial enough, abs(dot)={Math.Round(axisNormalAbs, 4)}"); if (!OverlapsProjectedBBox(face, outer, axialEnd, innerRadius, outerRadius, ring)) reasons.Add("projected bbox does not cover the required ring region"); double normalAlongAxis = Dot(face.Normal, outer.Axis!); double expectedSign = -side; double normalScore = Math.Abs(normalAlongAxis - expectedSign); var evidence = new StopEvidence { ComponentName = component.Name, PartName = component.PartName, ComponentCategory = component.Category, FaceIndex = face.FaceIndex, SurfaceType = face.SurfaceType, AreaMm2 = Util.Round(face.AreaMm2), CenterMm = Util.Round(face.CenterMm), Normal = Util.Round(face.Normal), AxialPositionMm = Util.Round(axial), AxialGapMm = Util.Round(Math.Abs(axial - axialEnd)), RadialDistanceMm = Util.Round(radial), Ring = ring, Side = side < 0 ? "left" : "right", Score = Util.Round(Math.Abs(axial - axialEnd) * 10.0 + normalScore + Math.Abs(radial - targetRadius) * (ring == "inner" ? 0.2 : 0.05)), Accepted = reasons.Count == 0, RejectReasons = reasons, AxisNormalAbs = Util.Round(axisNormalAbs), Reason = $"plane near bearing {ring} {(side < 0 ? "left" : "right")} end; component category={component.Category}" }; candidates.Add(evidence); } } return candidates .OrderBy(c => c.Score) .ThenBy(c => c.AxialGapMm) .ToList(); } static bool OverlapsProjectedBBox(FaceInfo face, FaceInfo bearingOuter, double axialEnd, double innerRadius, double outerRadius, string ring) { var axis = bearingOuter.Axis!; var axisPoint = bearingOuter.AxisPointMm!; var corners = BBoxCorners(face.BBoxMm); var radials = corners.Select(c => DistancePointToLine(c, axisPoint, axis)).ToList(); double minR = radials.Min(); double maxR = radials.Max(); if (ring == "inner") return minR <= innerRadius + 3.0 && maxR >= Math.Max(0.0, innerRadius - 3.0); return maxR >= innerRadius + 1.0 && minR <= outerRadius + 3.0; } static void AddLoopInfo(Face2 face, FaceInfo info) { try { object loopsObj = face.GetLoops(); var loops = ToObjectArray(loopsObj); info.LoopCount = loops.Length; } catch { } } static void AddCircularEdgeRadii(Face2 face, FaceInfo info) { try { foreach (object edgeObj in ToObjectArray(face.GetEdges())) { if (edgeObj is not Edge edge) continue; var curve = edge.GetCurve() as Curve; if (curve == null || !Safe(() => curve.IsCircle(), false)) continue; var cp = ToDoubleArray(GetComProperty(curve, "CircleParams")); if (cp.Length >= 7) { double radiusMm = Math.Abs(cp[6]) * 1000.0; if (radiusMm > 0.001 && !info.CircularEdgeRadiiMm.Any(r => Math.Abs(r - radiusMm) < 0.01)) info.CircularEdgeRadiiMm.Add(radiusMm); } } info.CircularEdgeRadiiMm.Sort(); } catch { } } static void AddCylinderAxialRange(FaceInfo info) { if (info.Axis == null || info.AxisPointMm == null || info.BBoxMm.Length < 6) return; var values = BBoxCorners(info.BBoxMm).Select(p => Project(p, info)).ToList(); info.AxialMinMm = values.Min(); info.AxialMaxMm = values.Max(); info.AxialLengthMm = info.AxialMaxMm - info.AxialMinMm; } static double Project(double[] point, FaceInfo axisFace) { return Dot(Sub(point, axisFace.AxisPointMm!), axisFace.Axis!); } static double ProjectOnFrame(double[] point, AxialFrame frame) { return Dot(Sub(point, frame.AxisPointMm), frame.Axis); } static string BuildMarkdown(List reports) { var sb = new StringBuilder(); sb.AppendLine("# Structural Fault Evidence"); sb.AppendLine(); sb.AppendLine("Experimental rule: one side inner ring fixed and the opposite side outer ring fixed is treated as correct."); sb.AppendLine(); foreach (var report in reports) { sb.AppendLine($"## {report.AssemblyName}"); sb.AppendLine(); sb.AppendLine($"- Path: `{report.AssemblyPath}`"); sb.AppendLine($"- Status: {(report.Ok ? "ok" : "failed")}"); sb.AppendLine($"- Message: {report.Message}"); sb.AppendLine($"- Components: {report.ComponentCount}, bearing candidates: {report.BearingCount}"); sb.AppendLine(); sb.AppendLine("### Component Side Fixation"); sb.AppendLine(); foreach (var fixation in report.ComponentFixations.OrderBy(f => f.Category).ThenBy(f => f.ComponentName, StringComparer.OrdinalIgnoreCase)) { sb.AppendLine($"- `{fixation.Status}` {fixation.ComponentName}: left={fixation.LeftFixed}, right={fixation.RightFixed}"); foreach (var contact in fixation.Contacts.Where(c => c.Accepted).Take(4)) sb.AppendLine($" - {contact.Side}: {contact.ComponentName} face#{contact.FaceIndex}, ring={contact.BearingRing}, gap={contact.AxialGapMm} mm"); } sb.AppendLine(); foreach (var bearing in report.Bearings) { sb.AppendLine($"### {bearing.ComponentName}"); sb.AppendLine(); sb.AppendLine($"- Diagnosis: `{bearing.Status}` - {bearing.Message}"); sb.AppendLine($"- Inner radius: {Util.Round(bearing.InnerRadiusMm)} mm, outer radius: {Util.Round(bearing.OuterRadiusMm)} mm"); sb.AppendLine($"- Axial range: {Util.Round(bearing.AxialMinMm)} to {Util.Round(bearing.AxialMaxMm)} mm"); sb.AppendLine($"- Inner left stop: {FormatStop(bearing.InnerLeftStop)}"); sb.AppendLine($"- Inner right stop: {FormatStop(bearing.InnerRightStop)}"); sb.AppendLine($"- Outer left stop: {FormatStop(bearing.OuterLeftStop)}"); sb.AppendLine($"- Outer right stop: {FormatStop(bearing.OuterRightStop)}"); sb.AppendLine("- Accepted side contacts:"); foreach (var contact in bearing.SideContacts.Where(c => c.Accepted).Take(10)) sb.AppendLine($" - {contact.Side}: {contact.ComponentName} face#{contact.FaceIndex}, ring={contact.BearingRing}, gap={contact.AxialGapMm} mm"); sb.AppendLine(); } } return sb.ToString(); } static string FormatStop(StopEvidence? stop) { if (stop == null) return "not detected"; return $"{stop.ComponentName} face#{stop.FaceIndex}, category={stop.ComponentCategory}, gap={stop.AxialGapMm} mm, radial={stop.RadialDistanceMm} mm"; } static bool ContainsAny(string text, params string[] needles) { return needles.Any(n => text.Contains(n, StringComparison.OrdinalIgnoreCase)); } static double[] TransformPoint(dynamic mathUtility, Component2 component, double[] pointMm) { try { object pointObj = mathUtility.CreatePoint(new[] { pointMm[0] / 1000.0, pointMm[1] / 1000.0, pointMm[2] / 1000.0 }); dynamic point = pointObj; object transformObj = component.Transform2; dynamic transformed = point.MultiplyTransform(transformObj); return ToDoubleArray(transformed.ArrayData).Take(3).Select(v => v * 1000.0).ToArray(); } catch { return pointMm; } } static double[] TransformVector(dynamic mathUtility, Component2 component, double[] vector) { try { object vectorObj = mathUtility.CreateVector(vector); dynamic mathVector = vectorObj; object transformObj = component.Transform2; dynamic transformed = mathVector.MultiplyTransform(transformObj); return Normalize(ToDoubleArray(transformed.ArrayData).Take(3).ToArray()); } catch { return Normalize(vector); } } static double[] TransformBBox(dynamic mathUtility, Component2 component, double[] bboxMm) { var corners = BBoxCorners(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 string StripInstanceSuffix(string value) { int dash = value.LastIndexOf('-'); return dash > 0 && int.TryParse(value[(dash + 1)..], out _) ? value[..dash] : value; } static object? GetComProperty(object obj, string propertyName) { return obj.GetType().InvokeMember(propertyName, System.Reflection.BindingFlags.GetProperty, null, obj, null, CultureInfo.InvariantCulture); } static object[] ToObjectArray(object? value) { if (value == null) return []; if (value is object[] objects) return objects; if (value is Array array) { var result = new object[array.Length]; for (int i = 0; i < array.Length; i++) result[i] = array.GetValue(i)!; return result; } return []; } static double[] ToDoubleArray(object? value) { if (value == null) return []; if (value is double[] doubles) return doubles; if (value is object[] objects) return objects.Select(o => Convert.ToDouble(o, CultureInfo.InvariantCulture)).ToArray(); if (value is Array array) { var result = new double[array.Length]; for (int i = 0; i < array.Length; i++) result[i] = Convert.ToDouble(array.GetValue(i), CultureInfo.InvariantCulture); return result; } return []; } static T Safe(Func fn, T fallback) { try { return fn(); } catch { return fallback; } } static double Dot(double[] a, double[] b) => a[0] * b[0] + a[1] * b[1] + a[2] * b[2]; static double[] Sub(double[] a, double[] b) => [a[0] - b[0], a[1] - b[1], a[2] - b[2]]; static double[] Scale(double[] a, double s) => [a[0] * s, a[1] * s, a[2] * s]; static double Norm(double[] a) => Math.Sqrt(Dot(a, a)); static double[] Normalize(double[] a) { double n = Norm(a); return n < 1e-12 ? [0, 0, 0] : [a[0] / n, a[1] / n, a[2] / n]; } static bool AreParallel(double[] a, double[] b, double threshold) => Math.Abs(Dot(Normalize(a), Normalize(b))) >= threshold; static double DistancePointToLine(double[] p, double[] linePoint, double[] lineDir) { var d = Normalize(lineDir); var v = Sub(p, linePoint); var projected = Scale(d, Dot(v, d)); return Norm(Sub(v, projected)); } static List BBoxCorners(double[] b) { return [ [b[0], b[1], b[2]], [b[0], b[1], b[5]], [b[0], b[4], b[2]], [b[0], b[4], b[5]], [b[3], b[1], b[2]], [b[3], b[1], b[5]], [b[3], b[4], b[2]], [b[3], b[4], b[5]] ]; } sealed class ComponentInfo { public required Component2 Component { get; init; } public string Name { get; init; } = ""; public string PartName { get; init; } = ""; public string Path { get; init; } = ""; public string Category { get; init; } = ""; public int FaceCount { get; set; } public List Faces { get; set; } = []; } sealed class FaceInfo { public string ComponentName { get; set; } = ""; public string ComponentCategory { get; set; } = ""; public string PartName { get; set; } = ""; public int FaceIndex { get; set; } public string SurfaceType { get; set; } = ""; public double AreaMm2 { get; set; } public double[] BBoxMm { get; set; } = []; public double[] CenterMm { get; set; } = []; public int LoopCount { get; set; } public double[]? Normal { get; set; } public double[]? RootPointMm { get; set; } public double? PlaneOffsetMm { get; set; } public List CircularEdgeRadiiMm { get; set; } = []; public double[]? AxisPointMm { get; set; } public double[]? Axis { get; set; } public double? RadiusMm { get; set; } public double? AxialMinMm { get; set; } public double? AxialMaxMm { get; set; } public double? AxialLengthMm { get; set; } } sealed class AssemblyReport { public bool Ok { get; set; } public string Message { get; set; } = ""; public string AssemblyName { get; set; } = ""; public string AssemblyTitle { get; set; } = ""; public string AssemblyPath { get; set; } = ""; public int OpenErrors { get; set; } public int OpenWarnings { get; set; } public int ComponentCount { get; set; } public int BearingCount { get; set; } public List Components { get; set; } = []; public List ComponentFixations { get; set; } = []; public List Bearings { get; set; } = []; } sealed class ComponentSummary { public string ComponentName { get; set; } = ""; public string PartName { get; set; } = ""; public string Category { get; set; } = ""; public string Path { get; set; } = ""; public int FaceCount { get; set; } public List CylinderFaces { get; set; } = []; } sealed class BearingReport { public string ComponentName { get; set; } = ""; public string PartName { get; set; } = ""; public string Path { get; set; } = ""; public int FaceCount { get; set; } public string Status { get; set; } = ""; public string Message { get; set; } = ""; public double[]? Axis { get; set; } public double[]? AxisPointMm { get; set; } public double? InnerRadiusMm { get; set; } public double? OuterRadiusMm { get; set; } public double AxialMinMm { get; set; } public double AxialMaxMm { get; set; } public List CylinderFaces { get; set; } = []; public List EndFaces { get; set; } = []; public List SideContacts { get; set; } = []; public List StopCandidates { get; set; } = []; public StopEvidence? InnerLeftStop { get; set; } public StopEvidence? InnerRightStop { get; set; } public StopEvidence? OuterLeftStop { get; set; } public StopEvidence? OuterRightStop { get; set; } public RuleInterpretation? RuleInterpretation { get; set; } } sealed class AxialFrame { public double[] Axis { get; set; } = []; public double[] AxisPointMm { get; set; } = []; public double MinAxialMm { get; set; } public double MaxAxialMm { get; set; } public double InnerRadiusMm { get; set; } public double OuterRadiusMm { get; set; } } sealed class ComponentFixationReport { public string ComponentName { get; set; } = ""; public string PartName { get; set; } = ""; public string Category { get; set; } = ""; public string Status { get; set; } = ""; public double[] Axis { get; set; } = []; public double[] AxisPointMm { get; set; } = []; public double AxialMinMm { get; set; } public double AxialMaxMm { get; set; } public double? InnerRadiusMm { get; set; } public double? OuterRadiusMm { get; set; } public bool LeftFixed { get; set; } public bool RightFixed { get; set; } public bool FixedBothSides { get; set; } public List Contacts { get; set; } = []; } sealed class SideContactEvidence { public string Side { get; set; } = ""; public string ComponentName { get; set; } = ""; public string PartName { get; set; } = ""; public string ComponentCategory { get; set; } = ""; public int FaceIndex { get; set; } public string BearingRing { get; set; } = ""; public double FaceRadialMinMm { get; set; } public double FaceRadialMaxMm { get; set; } public double[] CenterMm { get; set; } = []; public double[] Normal { get; set; } = []; public double AxialPositionMm { get; set; } public double AxialGapMm { get; set; } public double AxisNormalAbs { get; set; } public double Score { get; set; } public bool Accepted { get; set; } public List RejectReasons { get; set; } = []; public string Reason { get; set; } = ""; public StopEvidence ToStopEvidence() { return new StopEvidence { ComponentName = ComponentName, PartName = PartName, ComponentCategory = ComponentCategory, FaceIndex = FaceIndex, SurfaceType = "plane", CenterMm = CenterMm, Normal = Normal, AxialPositionMm = AxialPositionMm, AxialGapMm = AxialGapMm, AxisNormalAbs = AxisNormalAbs, Ring = BearingRing, Side = Side, Score = Score, Accepted = Accepted, RejectReasons = RejectReasons, Reason = Reason }; } } sealed class RuleInterpretation { public string Principle { get; set; } = ""; public bool InnerLeft { get; set; } public bool InnerRight { get; set; } public bool OuterLeft { get; set; } public bool OuterRight { get; set; } public bool PatternInnerLeftOuterRight { get; set; } public bool PatternInnerRightOuterLeft { get; set; } public bool ExperimentalPass { get; set; } } sealed class StopEvidence { public string ComponentName { get; set; } = ""; public string PartName { get; set; } = ""; public string ComponentCategory { get; set; } = ""; public int FaceIndex { get; set; } public string SurfaceType { get; set; } = ""; public double AreaMm2 { get; set; } public double[] CenterMm { get; set; } = []; public double[] Normal { get; set; } = []; public double AxialPositionMm { get; set; } public double AxialGapMm { get; set; } public double RadialDistanceMm { get; set; } public double AxisNormalAbs { get; set; } public string Ring { get; set; } = ""; public string Side { get; set; } = ""; public double Score { get; set; } public bool Accepted { get; set; } public List RejectReasons { get; set; } = []; public string Reason { get; set; } = ""; } static class Util { public static double? Round(double? value) => value.HasValue ? Math.Round(value.Value, 4) : null; public static double Round(double value) => Math.Round(value, 4); public static double[] Round(double[] value) => value.Select(v => Math.Round(v, 4)).ToArray(); } sealed class FaceSummary { public int FaceIndex { get; set; } public string SurfaceType { get; set; } = ""; public double AreaMm2 { get; set; } public double[] CenterMm { get; set; } = []; public double[] BBoxMm { get; set; } = []; public int LoopCount { get; set; } public double? RadiusMm { get; set; } public double[]? AxisPointMm { get; set; } public double[]? Axis { get; set; } public double? AxialMinMm { get; set; } public double? AxialMaxMm { get; set; } public static FaceSummary From(FaceInfo face) { return new FaceSummary { FaceIndex = face.FaceIndex, SurfaceType = face.SurfaceType, AreaMm2 = Util.Round(face.AreaMm2), CenterMm = Util.Round(face.CenterMm), BBoxMm = Util.Round(face.BBoxMm), LoopCount = face.LoopCount, RadiusMm = Util.Round(face.RadiusMm), AxisPointMm = face.AxisPointMm == null ? null : Util.Round(face.AxisPointMm), Axis = face.Axis == null ? null : Util.Round(face.Axis), AxialMinMm = Util.Round(face.AxialMinMm), AxialMaxMm = Util.Round(face.AxialMaxMm) }; } }