1300 lines
46 KiB
C#
1300 lines
46 KiB
C#
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<AssemblyReport>();
|
||
|
||
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<ComponentInfo> EnumerateComponents(AssemblyDoc assy)
|
||
{
|
||
var result = new List<ComponentInfo>();
|
||
foreach (object item in assy.GetComponents(false) as object[] ?? Array.Empty<object>())
|
||
{
|
||
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<FaceInfo> EnumerateFaces(ComponentInfo component, dynamic mathUtility)
|
||
{
|
||
var result = new List<FaceInfo>();
|
||
var bodies = EnumerateComponentBodies(component.Component);
|
||
int faceIndex = 0;
|
||
|
||
foreach (var body in bodies)
|
||
{
|
||
for (object? faceObj = Safe<object?>(() => 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<object?>(() => face.GetNextFace(), null);
|
||
}
|
||
}
|
||
|
||
return result;
|
||
}
|
||
|
||
static List<Body2> EnumerateComponentBodies(Component2 component)
|
||
{
|
||
var result = new List<Body2>();
|
||
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<ComponentInfo> 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<ComponentInfo> 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<ComponentInfo> 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<SideContactEvidence> CollectSideContacts(ComponentInfo target, List<ComponentInfo> components, AxialFrame frame)
|
||
{
|
||
var result = new List<SideContactEvidence>();
|
||
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<string>();
|
||
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<FaceInfo> SelectBearingAxisCluster(List<FaceInfo> 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<StopEvidence> CollectAxialStopCandidates(
|
||
ComponentInfo bearing,
|
||
List<ComponentInfo> 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<StopEvidence>();
|
||
|
||
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<string>();
|
||
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<AssemblyReport> 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<T>(Func<T> 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<double[]> 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<FaceInfo> 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<double> 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<ComponentSummary> Components { get; set; } = [];
|
||
public List<ComponentFixationReport> ComponentFixations { get; set; } = [];
|
||
public List<BearingReport> 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<FaceSummary> 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<FaceSummary> CylinderFaces { get; set; } = [];
|
||
public List<FaceSummary> EndFaces { get; set; } = [];
|
||
public List<SideContactEvidence> SideContacts { get; set; } = [];
|
||
public List<StopEvidence> 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<SideContactEvidence> 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<string> 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<string> 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)
|
||
};
|
||
}
|
||
}
|