2524 lines
106 KiB
C#
2524 lines
106 KiB
C#
using System.Diagnostics;
|
|
using System.Text;
|
|
using System.Text.Encodings.Web;
|
|
using System.Text.Json;
|
|
using System.Text.Json.Serialization;
|
|
|
|
Console.OutputEncoding = Encoding.UTF8;
|
|
|
|
if (args.Length == 0 || IsHelp(args[0]))
|
|
{
|
|
PrintUsage();
|
|
return 1;
|
|
}
|
|
|
|
try
|
|
{
|
|
return args[0].Trim().ToLowerInvariant() switch
|
|
{
|
|
"verify" => Verify(args.Skip(1).ToArray()),
|
|
_ => Fail($"Unknown command: {args[0]}")
|
|
};
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Console.Error.WriteLine(ex.Message);
|
|
return 1;
|
|
}
|
|
|
|
static int Verify(string[] args)
|
|
{
|
|
var parsed = Args.Parse(args);
|
|
var modelInput = parsed.Required("--model");
|
|
var rulePath = parsed.Required("--rules");
|
|
var outBase = parsed.Optional("--out") ?? Path.Combine("runtime", "diagnostics", "model_diagnostic_result");
|
|
|
|
var root = LocateRepoRoot();
|
|
var evidencePaths = EvidenceInputResolver.Resolve(root, modelInput);
|
|
var package = JsonSerializer.Deserialize<RuleTemplatePackage>(File.ReadAllText(rulePath, Encoding.UTF8), JsonOptions.Default)
|
|
?? throw new InvalidOperationException("Cannot parse rule template package.");
|
|
|
|
var modelEvidence = ModelEvidenceNormalizer.Normalize(evidencePaths);
|
|
var result = DiagnosticPipeline.Run(package, modelEvidence, modelInput, evidencePaths, rulePath);
|
|
|
|
var fullOutBase = Path.GetFullPath(outBase);
|
|
Directory.CreateDirectory(Path.GetDirectoryName(fullOutBase) ?? ".");
|
|
File.WriteAllText(fullOutBase + ".json", JsonSerializer.Serialize(result, JsonOptions.Default), new UTF8Encoding(false));
|
|
File.WriteAllText(fullOutBase + ".md", MarkdownReport.Write(result), new UTF8Encoding(false));
|
|
|
|
Console.WriteLine(JsonSerializer.Serialize(result, JsonOptions.Default));
|
|
return result.RuleResults.Any(r => r.Decision == "problem") ? 4 : 0;
|
|
}
|
|
|
|
static bool IsHelp(string arg) => arg is "-h" or "--help" or "/?" or "help";
|
|
|
|
static int Fail(string message)
|
|
{
|
|
Console.Error.WriteLine(message);
|
|
PrintUsage();
|
|
return 1;
|
|
}
|
|
|
|
static void PrintUsage()
|
|
{
|
|
Console.WriteLine("""
|
|
ModelDiagnosticVerifier
|
|
|
|
Purpose:
|
|
Verify SolidWorks model evidence against rule packages generated by RuleTemplateGenerator.
|
|
|
|
Usage:
|
|
ModelDiagnosticVerifier verify --model <assembly.SLDASM|evidence.json> --rules <rule_template.json> [--out <outputBase>]
|
|
|
|
Flow:
|
|
model -> raw B-rep/structural evidence -> scene candidates -> rule applicability
|
|
-> model 2D section structure instances -> image structure template matching
|
|
-> real geometry/contact evidence validation -> pass/problem/needs_review
|
|
""");
|
|
}
|
|
|
|
static string LocateRepoRoot()
|
|
{
|
|
var dir = new DirectoryInfo(Environment.CurrentDirectory);
|
|
while (dir != null)
|
|
{
|
|
if (File.Exists(Path.Combine(dir.FullName, "project_paths.json")) &&
|
|
Directory.Exists(Path.Combine(dir.FullName, "tools")))
|
|
return dir.FullName;
|
|
dir = dir.Parent;
|
|
}
|
|
|
|
return Environment.CurrentDirectory;
|
|
}
|
|
|
|
static class EvidenceInputResolver
|
|
{
|
|
public static List<string> Resolve(string root, string modelInput)
|
|
{
|
|
var full = Path.GetFullPath(modelInput);
|
|
if (!File.Exists(full))
|
|
throw new FileNotFoundException($"Model/evidence input not found: {full}");
|
|
|
|
var ext = Path.GetExtension(full).ToLowerInvariant();
|
|
if (ext == ".json")
|
|
return [full];
|
|
|
|
if (ext is ".sldasm" or ".sldprt")
|
|
return RunModelEvidenceExtractors(root, full);
|
|
|
|
throw new InvalidOperationException($"Unsupported model input extension: {ext}");
|
|
}
|
|
|
|
static List<string> RunModelEvidenceExtractors(string root, string modelPath)
|
|
{
|
|
var evidencePaths = new List<string>();
|
|
var section = TryRunProject(
|
|
root,
|
|
Path.Combine(root, "tools", "model-diagnostics", "SectionBrepExtractor", "SectionBrepExtractor.csproj"),
|
|
modelPath,
|
|
Path.Combine(root, "tools", "model-diagnostics", "SectionBrepExtractor", "output", "section_brep_report.json"),
|
|
"SectionBrepExtractor");
|
|
if (section != null)
|
|
evidencePaths.Add(section);
|
|
|
|
var structural = TryRunProject(
|
|
root,
|
|
Path.Combine(root, "tools", "model-diagnostics", "StructuralFaultProbe", "StructuralFaultProbe.csproj"),
|
|
modelPath,
|
|
Path.Combine(root, "runtime", "structural_fault_probe", "structural_fault_evidence.json"),
|
|
"StructuralFaultProbe");
|
|
if (structural != null)
|
|
evidencePaths.Add(structural);
|
|
|
|
if (evidencePaths.Count == 0)
|
|
throw new InvalidOperationException("No model evidence extractor produced a usable JSON file.");
|
|
|
|
return evidencePaths;
|
|
}
|
|
|
|
static string? TryRunProject(string root, string projectPath, string modelPath, string expectedOutput, string name)
|
|
{
|
|
if (!File.Exists(projectPath))
|
|
return null;
|
|
|
|
var psi = new ProcessStartInfo("dotnet")
|
|
{
|
|
WorkingDirectory = root,
|
|
RedirectStandardOutput = true,
|
|
RedirectStandardError = true,
|
|
UseShellExecute = false,
|
|
CreateNoWindow = true
|
|
};
|
|
psi.ArgumentList.Add("run");
|
|
psi.ArgumentList.Add("--project");
|
|
psi.ArgumentList.Add(projectPath);
|
|
psi.ArgumentList.Add("-c");
|
|
psi.ArgumentList.Add("Release");
|
|
psi.ArgumentList.Add("--");
|
|
psi.ArgumentList.Add(modelPath);
|
|
|
|
using var process = Process.Start(psi) ?? throw new InvalidOperationException($"Failed to start {name}.");
|
|
var stdout = process.StandardOutput.ReadToEnd();
|
|
var stderr = process.StandardError.ReadToEnd();
|
|
process.WaitForExit();
|
|
if (process.ExitCode != 0)
|
|
{
|
|
Console.Error.WriteLine($"{name} failed with exit={process.ExitCode}");
|
|
Console.Error.WriteLine(stdout);
|
|
Console.Error.WriteLine(stderr);
|
|
return null;
|
|
}
|
|
|
|
return File.Exists(expectedOutput) ? expectedOutput : null;
|
|
}
|
|
}
|
|
|
|
static class ModelEvidenceNormalizer
|
|
{
|
|
public static ModelEvidence Normalize(List<string> sourcePaths)
|
|
{
|
|
var evidence = new ModelEvidence
|
|
{
|
|
Schema = "model_evidence_v1",
|
|
SourcePaths = sourcePaths.Select(Path.GetFullPath).ToList()
|
|
};
|
|
|
|
foreach (var sourcePath in sourcePaths)
|
|
{
|
|
using var json = JsonDocument.Parse(File.ReadAllText(sourcePath, Encoding.UTF8));
|
|
var root = json.RootElement;
|
|
if (root.ValueKind == JsonValueKind.Array)
|
|
{
|
|
foreach (var assembly in root.EnumerateArray())
|
|
ReadStructuralAssemblyReport(assembly, evidence);
|
|
}
|
|
else
|
|
{
|
|
ReadStructuralAssemblyReport(root, evidence);
|
|
ReadSectionBrepReport(root, evidence);
|
|
}
|
|
}
|
|
|
|
SectionStructureSynthesizer.Synthesize(evidence);
|
|
SceneRecognizer.Recognize(evidence);
|
|
PortFeatureSynthesizer.Synthesize(evidence);
|
|
DisassemblyAccessDefinitionFilter.Apply(evidence);
|
|
return evidence;
|
|
}
|
|
|
|
static void ReadStructuralAssemblyReport(JsonElement assembly, ModelEvidence evidence)
|
|
{
|
|
if (assembly.ValueKind != JsonValueKind.Object)
|
|
return;
|
|
|
|
if (assembly.TryGetProperty("AssemblyPath", out var assemblyPath) && assemblyPath.ValueKind == JsonValueKind.String)
|
|
evidence.ModelPath = assemblyPath.GetString() ?? evidence.ModelPath;
|
|
|
|
if (TryArray(assembly, "Components", out var components))
|
|
{
|
|
foreach (var component in components.EnumerateArray())
|
|
{
|
|
var c = new ComponentEvidence
|
|
{
|
|
Id = GetString(component, "ComponentName"),
|
|
Name = GetString(component, "ComponentName"),
|
|
PartName = GetString(component, "PartName"),
|
|
Category = NormalizeCategory(GetString(component, "Category")),
|
|
Path = GetString(component, "Path")
|
|
};
|
|
AddOrMergeComponent(evidence, c);
|
|
if (TryArray(component, "CylinderFaces", out var cylinderFaces))
|
|
ReadComponentCylinderFaces(c, cylinderFaces, evidence);
|
|
}
|
|
}
|
|
|
|
if (TryArray(assembly, "ComponentFixations", out var fixations))
|
|
{
|
|
foreach (var fixation in fixations.EnumerateArray())
|
|
{
|
|
var componentName = GetString(fixation, "ComponentName");
|
|
var category = NormalizeCategory(GetString(fixation, "Category"));
|
|
MergeComponentFrame(evidence, componentName, fixation);
|
|
AddIfTrue(evidence, GetBool(fixation, "LeftFixed"), "left_axial_limit", componentName, 0.8, "StructuralFaultProbe.ComponentFixations");
|
|
AddIfTrue(evidence, GetBool(fixation, "RightFixed"), "right_axial_limit", componentName, 0.8, "StructuralFaultProbe.ComponentFixations");
|
|
AddIfTrue(evidence, GetBool(fixation, "FixedBothSides"), "both_sides_axial_limit", componentName, 0.85, "StructuralFaultProbe.ComponentFixations");
|
|
|
|
if (TryArray(fixation, "Contacts", out var contacts))
|
|
{
|
|
foreach (var contact in contacts.EnumerateArray())
|
|
{
|
|
var accepted = GetBool(contact, "Accepted");
|
|
var other = GetString(contact, "ComponentName");
|
|
var side = NormalizeToken(GetString(contact, "Side"));
|
|
var ring = NormalizeToken(GetString(contact, "BearingRing"));
|
|
var id = $"{componentName}:{other}:{side}:{ring}:{evidence.Items.Count + 1}";
|
|
evidence.Items.Add(new EvidenceItem
|
|
{
|
|
Id = id,
|
|
Type = "axial_contact",
|
|
Owner = componentName,
|
|
Related = other,
|
|
Confidence = accepted ? 0.82 : 0.35,
|
|
Source = "StructuralFaultProbe.ComponentFixations.Contacts",
|
|
Attributes =
|
|
{
|
|
["side"] = side,
|
|
["ring"] = ring,
|
|
["accepted"] = accepted.ToString().ToLowerInvariant(),
|
|
["target_category"] = category,
|
|
["center_mm"] = FormatArray(GetDoubleArray(contact, "CenterMm")),
|
|
["normal"] = FormatArray(GetDoubleArray(contact, "Normal")),
|
|
["axial_position_mm"] = FormatDouble(GetDouble(contact, "AxialPositionMm")),
|
|
["face_radial_min_mm"] = FormatDouble(GetDouble(contact, "FaceRadialMinMm")),
|
|
["face_radial_max_mm"] = FormatDouble(GetDouble(contact, "FaceRadialMaxMm"))
|
|
}
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (TryArray(assembly, "Bearings", out var bearings))
|
|
{
|
|
foreach (var bearing in bearings.EnumerateArray())
|
|
ReadBearingReport(bearing, evidence);
|
|
}
|
|
}
|
|
|
|
static void ReadBearingReport(JsonElement bearing, ModelEvidence evidence)
|
|
{
|
|
var name = GetString(bearing, "ComponentName");
|
|
AddIfNotEmpty(evidence, name, "bearing_component", name, 0.85, "StructuralFaultProbe.Bearings");
|
|
|
|
if (bearing.TryGetProperty("RuleInterpretation", out var rule) && rule.ValueKind == JsonValueKind.Object)
|
|
{
|
|
AddIfTrue(evidence, GetBool(rule, "InnerLeft"), "bearing_inner_left_limit", name, 0.84, "StructuralFaultProbe.Bearings");
|
|
AddIfTrue(evidence, GetBool(rule, "InnerRight"), "bearing_inner_right_limit", name, 0.84, "StructuralFaultProbe.Bearings");
|
|
AddIfTrue(evidence, GetBool(rule, "OuterLeft"), "bearing_outer_left_limit", name, 0.84, "StructuralFaultProbe.Bearings");
|
|
AddIfTrue(evidence, GetBool(rule, "OuterRight"), "bearing_outer_right_limit", name, 0.84, "StructuralFaultProbe.Bearings");
|
|
}
|
|
}
|
|
|
|
static void ReadSectionBrepReport(JsonElement report, ModelEvidence evidence)
|
|
{
|
|
ReadMechanicalFeatureGraph(report, evidence);
|
|
|
|
if (!TryObject(report, "SketchGraph", out var sketch))
|
|
return;
|
|
|
|
var edgeIndex = BuildSketchEdgeIndex(sketch);
|
|
|
|
if (TryArray(sketch, "Relations", out var relations))
|
|
{
|
|
foreach (var relation in relations.EnumerateArray())
|
|
{
|
|
var type = NormalizeToken(GetString(relation, "Type"));
|
|
var a = GetString(relation, "A");
|
|
var b = GetString(relation, "B");
|
|
if (string.IsNullOrWhiteSpace(type))
|
|
continue;
|
|
|
|
AddComponentFromReference(evidence, a);
|
|
AddComponentFromReference(evidence, b);
|
|
evidence.SectionInstances.AddRelation(type, a, b, 0.86, "SketchGraph.Relations");
|
|
if (type == "contact")
|
|
evidence.Items.Add(Item("close_fit_contact", "section_graph", $"{a}|{b}", 0.9, "SketchGraph.Relations"));
|
|
}
|
|
}
|
|
|
|
if (TryArray(sketch, "Dimensions", out var dimensions))
|
|
{
|
|
foreach (var dimension in dimensions.EnumerateArray())
|
|
ReadSketchDimension(dimension, edgeIndex, evidence);
|
|
}
|
|
|
|
if (TryArray(sketch, "Regions", out var regions))
|
|
{
|
|
foreach (var region in regions.EnumerateArray())
|
|
{
|
|
var id = GetString(region, "Id");
|
|
var role = NormalizeToken(GetString(region, "Role"));
|
|
if (role is "tool_gap" or "gap_region" or "void_region")
|
|
{
|
|
evidence.SectionInstances.Entities.Add(new StructureEntity { Id = id, Kind = "region", SemanticRole = "void_gap" });
|
|
PromoteGapRegionEvidence(id, role, evidence);
|
|
}
|
|
if (role is "ejector_port" or "port_region")
|
|
{
|
|
evidence.SectionInstances.Entities.Add(new StructureEntity { Id = id, Kind = "region", SemanticRole = "ejector_or_threaded_hole" });
|
|
PromotePortRegionEvidence(id, role, evidence);
|
|
}
|
|
}
|
|
}
|
|
|
|
GapFeatureSynthesizer.Synthesize(sketch, edgeIndex.Values
|
|
.GroupBy(e => e.Id, StringComparer.OrdinalIgnoreCase)
|
|
.Select(g => g.First())
|
|
.ToList(), evidence);
|
|
}
|
|
|
|
static void ReadMechanicalFeatureGraph(JsonElement report, ModelEvidence evidence)
|
|
{
|
|
if (!TryObject(report, "FeatureGraph", out var graph) || !TryArray(graph, "Features", out var features))
|
|
return;
|
|
|
|
foreach (var feature in features.EnumerateArray())
|
|
{
|
|
var id = GetString(feature, "Id");
|
|
var type = NormalizeToken(GetString(feature, "Type"));
|
|
var component = GetString(feature, "ComponentName");
|
|
var source = GetString(feature, "Source");
|
|
var confidence = GetDouble(feature, "Confidence");
|
|
if (string.IsNullOrWhiteSpace(id) || string.IsNullOrWhiteSpace(type))
|
|
continue;
|
|
|
|
if (confidence <= 0)
|
|
confidence = 0.65;
|
|
|
|
evidence.SectionInstances.Entities.Add(new StructureEntity
|
|
{
|
|
Id = id,
|
|
Kind = "brep_feature",
|
|
SemanticRole = type
|
|
});
|
|
|
|
var faceRefs = ReadStringArray(feature, "FaceRefs").ToList();
|
|
evidence.Items.Add(new EvidenceItem
|
|
{
|
|
Id = $"feature:{id}",
|
|
Type = type,
|
|
Owner = component,
|
|
Related = id,
|
|
Confidence = confidence,
|
|
Source = string.IsNullOrWhiteSpace(source) ? "FeatureGraph" : $"FeatureGraph.{source}",
|
|
Attributes =
|
|
{
|
|
["feature_id"] = id,
|
|
["face_refs"] = string.Join(",", faceRefs)
|
|
}
|
|
});
|
|
|
|
foreach (var faceRef in faceRefs)
|
|
evidence.SectionInstances.AddRelation("feature_has_face", id, faceRef, confidence, "FeatureGraph.FaceRefs");
|
|
|
|
foreach (var possible in ReadStringArray(feature, "PossibleFunctions"))
|
|
{
|
|
var possibleType = NormalizeToken(possible);
|
|
if (string.IsNullOrWhiteSpace(possibleType))
|
|
continue;
|
|
|
|
evidence.Items.Add(new EvidenceItem
|
|
{
|
|
Id = $"feature_possible:{id}:{possibleType}",
|
|
Type = possibleType,
|
|
Owner = component,
|
|
Related = id,
|
|
Confidence = Math.Min(confidence, 0.68),
|
|
Source = "FeatureGraph.PossibleFunctions",
|
|
Attributes =
|
|
{
|
|
["feature_id"] = id,
|
|
["feature_type"] = type
|
|
}
|
|
});
|
|
evidence.SectionInstances.AddRelation(possibleType, id, component, Math.Min(confidence, 0.68), "FeatureGraph.PossibleFunctions");
|
|
}
|
|
}
|
|
}
|
|
|
|
static Dictionary<string, SketchEdgeInfo> BuildSketchEdgeIndex(JsonElement sketch)
|
|
{
|
|
var index = new Dictionary<string, SketchEdgeInfo>(StringComparer.OrdinalIgnoreCase);
|
|
if (!TryArray(sketch, "Edges", out var edges))
|
|
return index;
|
|
|
|
foreach (var edge in edges.EnumerateArray())
|
|
{
|
|
var id = GetString(edge, "Id");
|
|
if (string.IsNullOrWhiteSpace(id))
|
|
continue;
|
|
var info = new SketchEdgeInfo
|
|
{
|
|
Id = id,
|
|
Owner = GetString(edge, "Owner"),
|
|
Role = NormalizeToken(GetString(edge, "Role")),
|
|
Orientation = NormalizeToken(AttributeString(edge, "orientation")),
|
|
Length = GetDouble(edge, "Length"),
|
|
Start = GetDoubleArray(edge, "Start"),
|
|
End = GetDoubleArray(edge, "End")
|
|
};
|
|
index[id] = info;
|
|
|
|
if (TryArray(edge, "SourceEdgeIds", out var sourceIds))
|
|
{
|
|
foreach (var source in sourceIds.EnumerateArray())
|
|
{
|
|
if (source.ValueKind == JsonValueKind.String)
|
|
{
|
|
var sourceId = source.GetString();
|
|
if (!string.IsNullOrWhiteSpace(sourceId))
|
|
{
|
|
info.SourceEdgeIds.Add(sourceId);
|
|
index[sourceId] = info;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return index;
|
|
}
|
|
|
|
static void ReadSketchDimension(JsonElement dimension, Dictionary<string, SketchEdgeInfo> edgeIndex, ModelEvidence evidence)
|
|
{
|
|
var kind = NormalizeToken(GetString(dimension, "Kind"));
|
|
var role = NormalizeToken(GetString(dimension, "Role"));
|
|
var value = GetDouble(dimension, "Value");
|
|
|
|
if (kind is "gap_width_h" or "gap_width_candidate" or "minimum_gap_width_candidate" ||
|
|
role is "tool_gap_width_candidate" or "tool_insertion_clearance_candidate")
|
|
{
|
|
if (value >= 2.0)
|
|
{
|
|
AddEvidenceOnce(evidence, "gap_width_sufficient", "gap_region", "h", 0.76, "SketchGraph.Dimensions");
|
|
evidence.SectionInstances.AddRelation("gap_width_sufficient", "gap_region", "h", 0.76, "SketchGraph.Dimensions");
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (kind is "port_diameter_t" or "port_diameter_candidate" or "ejector_port_diameter_candidate" ||
|
|
role is "ejector_port_size_candidate" or "threaded_hole_size_candidate")
|
|
{
|
|
if (value >= 1.0)
|
|
{
|
|
AddEvidenceOnce(evidence, "port_diameter_sufficient", "port_region", "t", 0.76, "SketchGraph.Dimensions");
|
|
evidence.SectionInstances.AddRelation("port_diameter_sufficient", "port_region", "t", 0.76, "SketchGraph.Dimensions");
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (kind != "exposed_sleeve_end_height_candidate" && role != "exposed_push_face_candidate")
|
|
return;
|
|
|
|
if (value < 1.0)
|
|
return;
|
|
|
|
if (!TryArray(dimension, "TargetRefs", out var refs))
|
|
return;
|
|
|
|
foreach (var reference in refs.EnumerateArray())
|
|
{
|
|
if (reference.ValueKind != JsonValueKind.String)
|
|
continue;
|
|
var id = reference.GetString() ?? "";
|
|
if (!edgeIndex.TryGetValue(id, out var edge))
|
|
continue;
|
|
if (!IsSleeveOwner(edge.Owner))
|
|
continue;
|
|
|
|
evidence.Items.Add(new EvidenceItem
|
|
{
|
|
Id = $"exposed_axial_push_face:{edge.Id}",
|
|
Type = "exposed_axial_push_face",
|
|
Owner = edge.Owner,
|
|
Related = edge.Id,
|
|
Confidence = 0.86,
|
|
Source = "SketchGraph.Dimensions",
|
|
Attributes =
|
|
{
|
|
["dimension_kind"] = kind,
|
|
["height_mm"] = value.ToString(System.Globalization.CultureInfo.InvariantCulture),
|
|
["orientation"] = edge.Orientation
|
|
}
|
|
});
|
|
evidence.Items.Add(new EvidenceItem
|
|
{
|
|
Id = $"outside_accessible_force_path:{edge.Id}",
|
|
Type = "outside_accessible_force_path",
|
|
Owner = edge.Owner,
|
|
Related = edge.Id,
|
|
Confidence = 0.8,
|
|
Source = "SketchGraph.Dimensions",
|
|
Attributes =
|
|
{
|
|
["via"] = "exposed_sleeve_end_height_candidate"
|
|
}
|
|
});
|
|
AddEvidenceOnce(evidence, "outside_accessible_push_path", edge.Owner, edge.Id, 0.82, "SketchGraph.Dimensions");
|
|
AddEvidenceOnce(evidence, "push_path_reaches_sleeve_push_area", edge.Owner, edge.Id, 0.82, "SketchGraph.Dimensions");
|
|
AddEvidenceOnce(evidence, "push_direction_has_removal_component", edge.Owner, edge.Id, 0.78, "SketchGraph.Dimensions");
|
|
|
|
evidence.SectionInstances.AddRelation("owned_by", "push_face", "sleeve_material", 0.86, "SketchGraph.Dimensions");
|
|
evidence.SectionInstances.AddRelation("force_direction_parallel_to_axis", "push_face", "sleeve_axis", 0.78, "SketchGraph.Dimensions");
|
|
evidence.SectionInstances.AddRelation("can_push_or_strike", "outside", "push_face", 0.8, "SketchGraph.Dimensions");
|
|
evidence.SectionInstances.AddRelation("adjacent_to_outside", "push_face", "outside", 0.86, "SketchGraph.Dimensions");
|
|
evidence.SectionInstances.AddRelation("connected_to_outside", "push_access_path", "outside", 0.82, "SketchGraph.Dimensions");
|
|
evidence.SectionInstances.AddRelation("reaches", "push_access_path", "sleeve_push_area", 0.82, "SketchGraph.Dimensions");
|
|
evidence.SectionInstances.AddRelation("direction_has_component", "push_access_path", "removal_axis", 0.78, "SketchGraph.Dimensions");
|
|
}
|
|
}
|
|
|
|
static void PromoteGapRegionEvidence(string regionId, string role, ModelEvidence evidence)
|
|
{
|
|
bool outside = RegionConnectedToOutside(evidence, regionId);
|
|
bool adjacentSleeve = RegionAdjacentToSleeve(evidence, regionId);
|
|
bool adjacentEnd = RegionHasRelation(evidence, regionId, ["adjacent_to_sleeve_end", "reaches_sleeve_end", "force_path_reaches"]);
|
|
bool toolCanEnter = RegionHasRelation(evidence, regionId, ["tool_can_enter", "tool_access", "tool_insertable"]);
|
|
bool widthOk = evidence.Has("gap_width_sufficient", 0.7) || RegionHasRelation(evidence, regionId, ["gap_width_sufficient", "minimum_width_sufficient"]);
|
|
|
|
if (outside)
|
|
{
|
|
AddEvidenceOnce(evidence, "outside_connected_gap", regionId, "outside", 0.82, "SketchGraph.Regions");
|
|
AddEvidenceOnce(evidence, "outside_connected_void_gap", regionId, "outside", 0.82, "SketchGraph.Regions");
|
|
evidence.SectionInstances.AddRelation("connected_to_outside", "gap_region", "outside", 0.82, "SketchGraph.Regions");
|
|
}
|
|
|
|
if (adjacentSleeve)
|
|
{
|
|
AddEvidenceOnce(evidence, "gap_adjacent_to_sleeve_end", regionId, "sleeve_end", adjacentEnd ? 0.82 : 0.72, "SketchGraph.Regions");
|
|
evidence.SectionInstances.AddRelation("adjacent_to", "gap_region", "sleeve_material", 0.78, "SketchGraph.Regions");
|
|
evidence.SectionInstances.AddRelation("adjacent_to_sleeve_end", "gap_region", "sleeve_end", adjacentEnd ? 0.82 : 0.72, "SketchGraph.Regions");
|
|
}
|
|
|
|
if (outside && (toolCanEnter || role is "tool_gap" or "gap_region"))
|
|
{
|
|
AddEvidenceOnce(evidence, "tool_can_enter_gap", regionId, "outside", toolCanEnter ? 0.82 : 0.72, "SketchGraph.Regions");
|
|
evidence.SectionInstances.AddRelation("tool_can_enter", "outside", "gap_region", toolCanEnter ? 0.82 : 0.72, "SketchGraph.Regions");
|
|
}
|
|
|
|
if (outside && adjacentSleeve)
|
|
{
|
|
AddEvidenceOnce(evidence, "gap_force_path_reaches_sleeve", regionId, "sleeve_end", 0.76, "SketchGraph.Regions");
|
|
AddEvidenceOnce(evidence, "gap_path_reaches_sleeve_push_area", regionId, "sleeve_push_area", 0.76, "SketchGraph.Regions");
|
|
AddEvidenceOnce(evidence, "tool_force_direction_valid", regionId, "removal_axis", 0.72, "SketchGraph.Regions");
|
|
evidence.SectionInstances.AddRelation("force_path_reaches", "gap_region", "sleeve_end", 0.76, "SketchGraph.Regions");
|
|
evidence.SectionInstances.AddRelation("reaches", "gap_region", "sleeve_push_area", 0.76, "SketchGraph.Regions");
|
|
evidence.SectionInstances.AddRelation("direction_has_component", "gap_region", "removal_axis", 0.72, "SketchGraph.Regions");
|
|
}
|
|
|
|
if (widthOk)
|
|
{
|
|
AddEvidenceOnce(evidence, "gap_width_sufficient", regionId, "h", 0.76, "SketchGraph.Regions");
|
|
evidence.SectionInstances.AddRelation("gap_width_sufficient", "gap_region", "h", 0.76, "SketchGraph.Regions");
|
|
}
|
|
}
|
|
|
|
static void PromotePortRegionEvidence(string regionId, string role, ModelEvidence evidence)
|
|
{
|
|
bool outside = RegionConnectedToOutside(evidence, regionId);
|
|
bool reachesPushArea = RegionHasRelation(evidence, regionId, ["reaches_sleeve_push_area", "reaches", "push_area_on"]);
|
|
bool axisOk = RegionHasRelation(evidence, regionId, ["axis_points_to", "axis_can_push", "port_axis_can_push_sleeve"]);
|
|
bool threaded = role == "ejector_port" || RegionHasRelation(evidence, regionId, ["threaded_or_ejector_port", "threaded_hole", "ejector_screw_hole"]);
|
|
bool diameterOk = evidence.Has("port_diameter_sufficient", 0.7) || RegionHasRelation(evidence, regionId, ["port_diameter_sufficient", "hole_size_sufficient"]);
|
|
|
|
if (outside)
|
|
{
|
|
AddEvidenceOnce(evidence, "outside_connected_port", regionId, "outside", 0.82, "SketchGraph.Regions");
|
|
AddEvidenceOnce(evidence, "outside_connected_cylindrical_port", regionId, "outside", 0.82, "SketchGraph.Regions");
|
|
evidence.SectionInstances.AddRelation("connected_to_outside", "port_region", "outside", 0.82, "SketchGraph.Regions");
|
|
}
|
|
|
|
if (reachesPushArea)
|
|
{
|
|
AddEvidenceOnce(evidence, "port_reaches_sleeve_push_area", regionId, "push_area", 0.84, "SketchGraph.Regions");
|
|
AddEvidenceOnce(evidence, "port_axis_intersects_sleeve_push_area", regionId, "sleeve_push_area", 0.78, "SketchGraph.Regions");
|
|
evidence.SectionInstances.AddRelation("reaches", "port_region", "push_area", 0.84, "SketchGraph.Regions");
|
|
evidence.SectionInstances.AddRelation("push_area_on", "push_area", "sleeve_material", 0.84, "SketchGraph.Regions");
|
|
evidence.SectionInstances.AddRelation("axis_intersects", "port_axis", "sleeve_push_area", 0.78, "SketchGraph.Regions");
|
|
}
|
|
|
|
if (axisOk || reachesPushArea)
|
|
{
|
|
AddEvidenceOnce(evidence, "port_axis_can_push_sleeve", regionId, "push_area", axisOk ? 0.82 : 0.72, "SketchGraph.Regions");
|
|
AddEvidenceOnce(evidence, "port_push_direction_valid", regionId, "removal_axis", axisOk ? 0.82 : 0.72, "SketchGraph.Regions");
|
|
evidence.SectionInstances.AddRelation("axis_points_to", "port_axis", "push_area", axisOk ? 0.82 : 0.72, "SketchGraph.Regions");
|
|
evidence.SectionInstances.AddRelation("screw_can_push", "port_region", "push_area", axisOk ? 0.82 : 0.72, "SketchGraph.Regions");
|
|
evidence.SectionInstances.AddRelation("direction_has_component", "port_axis", "removal_axis", axisOk ? 0.82 : 0.72, "SketchGraph.Regions");
|
|
}
|
|
|
|
if (threaded)
|
|
{
|
|
AddEvidenceOnce(evidence, "threaded_or_ejector_port", regionId, "t", role == "ejector_port" ? 0.84 : 0.74, "SketchGraph.Regions");
|
|
evidence.SectionInstances.AddRelation("threaded_or_ejector_port", "port_region", "t", role == "ejector_port" ? 0.84 : 0.74, "SketchGraph.Regions");
|
|
evidence.SectionInstances.AddRelation("owned_by", "port_region", "host_material", 0.72, "SketchGraph.Regions");
|
|
}
|
|
|
|
if (diameterOk)
|
|
{
|
|
AddEvidenceOnce(evidence, "port_diameter_sufficient", regionId, "t", 0.76, "SketchGraph.Regions");
|
|
evidence.SectionInstances.AddRelation("port_diameter_sufficient", "port_region", "t", 0.76, "SketchGraph.Regions");
|
|
}
|
|
}
|
|
|
|
static void AddIfTrue(ModelEvidence evidence, bool condition, string type, string owner, double confidence, string source)
|
|
{
|
|
if (condition)
|
|
evidence.Items.Add(Item(type, owner, "", confidence, source));
|
|
}
|
|
|
|
static void AddIfNotEmpty(ModelEvidence evidence, string value, string type, string owner, double confidence, string source)
|
|
{
|
|
if (!string.IsNullOrWhiteSpace(value))
|
|
evidence.Items.Add(Item(type, owner, "", confidence, source));
|
|
}
|
|
|
|
static EvidenceItem Item(string type, string owner, string related, double confidence, string source)
|
|
{
|
|
var rawId = $"{type}:{owner}:{related}:{Guid.NewGuid():N}";
|
|
return new EvidenceItem
|
|
{
|
|
Id = rawId[..Math.Min(64, rawId.Length)],
|
|
Type = NormalizeToken(type),
|
|
Owner = owner,
|
|
Related = related,
|
|
Confidence = confidence,
|
|
Source = source
|
|
};
|
|
}
|
|
|
|
static void AddEvidenceOnce(ModelEvidence evidence, string type, string owner, string related, double confidence, string source)
|
|
{
|
|
var normalized = NormalizeToken(type);
|
|
if (evidence.Items.Any(i =>
|
|
i.Type.Equals(normalized, StringComparison.OrdinalIgnoreCase) &&
|
|
i.Owner.Equals(owner, StringComparison.OrdinalIgnoreCase) &&
|
|
i.Related.Equals(related, StringComparison.OrdinalIgnoreCase)))
|
|
return;
|
|
|
|
evidence.Items.Add(new EvidenceItem
|
|
{
|
|
Id = $"{normalized}:{owner}:{related}:promoted"[..Math.Min(64, $"{normalized}:{owner}:{related}:promoted".Length)],
|
|
Type = normalized,
|
|
Owner = owner,
|
|
Related = related,
|
|
Confidence = confidence,
|
|
Source = source
|
|
});
|
|
}
|
|
|
|
static bool RegionConnectedToOutside(ModelEvidence evidence, string regionId)
|
|
{
|
|
return evidence.SectionInstances.Relations.Any(r =>
|
|
(r.Type is "connected_to_outside" or "region_connected_to_region" or "adjacent_to_outside") &&
|
|
RelationTouches(r, regionId) &&
|
|
(IsOutsideReference(r.A) || IsOutsideReference(r.B) || r.Type == "connected_to_outside"));
|
|
}
|
|
|
|
static bool RegionAdjacentToSleeve(ModelEvidence evidence, string regionId)
|
|
{
|
|
return evidence.SectionInstances.Relations.Any(r =>
|
|
(r.Type is "adjacent_to_sleeve_region" or "adjacent_to" or "adjacent_to_sleeve_end") &&
|
|
RelationTouches(r, regionId) &&
|
|
(IsSleeveReference(r.A) || IsSleeveReference(r.B) || r.Type is "adjacent_to_sleeve_region" or "adjacent_to_sleeve_end"));
|
|
}
|
|
|
|
static bool RegionHasRelation(ModelEvidence evidence, string regionId, string[] relationTypes)
|
|
{
|
|
var normalized = relationTypes.Select(NormalizeToken).ToHashSet(StringComparer.OrdinalIgnoreCase);
|
|
return evidence.SectionInstances.Relations.Any(r => normalized.Contains(r.Type) && RelationTouches(r, regionId));
|
|
}
|
|
|
|
static bool RelationTouches(StructureRelation relation, string id)
|
|
{
|
|
return relation.A.Equals(id, StringComparison.OrdinalIgnoreCase) ||
|
|
relation.B.Equals(id, StringComparison.OrdinalIgnoreCase);
|
|
}
|
|
|
|
static bool IsOutsideReference(string value)
|
|
{
|
|
return NormalizeToken(value) is "outside" or "region_outside" or "outside_air" or "environment";
|
|
}
|
|
|
|
static bool IsSleeveReference(string value)
|
|
{
|
|
return NormalizeToken(value) is "sleeve" or "sleeve_material" or "sleeve_end" or "sleeve_push_area" ||
|
|
IsSleeveOwner(value);
|
|
}
|
|
|
|
static void AddComponentFromReference(ModelEvidence evidence, string reference)
|
|
{
|
|
var component = ExtractComponentName(reference);
|
|
if (string.IsNullOrWhiteSpace(component) || evidence.Components.Any(c => c.Id == component))
|
|
return;
|
|
|
|
AddOrMergeComponent(evidence, new ComponentEvidence
|
|
{
|
|
Id = component,
|
|
Name = component,
|
|
Category = GuessCategory(component)
|
|
});
|
|
}
|
|
|
|
static void ReadComponentCylinderFaces(ComponentEvidence component, JsonElement cylinderFaces, ModelEvidence evidence)
|
|
{
|
|
foreach (var face in cylinderFaces.EnumerateArray())
|
|
{
|
|
var radius = GetDouble(face, "RadiusMm");
|
|
var axis = GetDoubleArray(face, "Axis");
|
|
var axisPoint = GetDoubleArray(face, "AxisPointMm");
|
|
if (radius <= 0 || axis.Length != 3 || axisPoint.Length != 3)
|
|
continue;
|
|
|
|
var faceIndex = GetString(face, "FaceIndex");
|
|
var id = $"cylindrical_face:{component.Id}:{faceIndex}:{radius:0.###}";
|
|
evidence.Items.Add(new EvidenceItem
|
|
{
|
|
Id = id,
|
|
Type = "cylindrical_face",
|
|
Owner = component.Id,
|
|
Related = faceIndex,
|
|
Confidence = 0.78,
|
|
Source = "StructuralFaultProbe.Components.CylinderFaces",
|
|
Attributes =
|
|
{
|
|
["component_category"] = component.Category,
|
|
["radius_mm"] = FormatDouble(radius),
|
|
["diameter_mm"] = FormatDouble(radius * 2.0),
|
|
["axis"] = FormatArray(axis),
|
|
["axis_point_mm"] = FormatArray(axisPoint),
|
|
["center_mm"] = FormatArray(GetDoubleArray(face, "CenterMm")),
|
|
["bbox_mm"] = FormatArray(GetDoubleArray(face, "BBoxMm")),
|
|
["axial_min_mm"] = FormatDouble(GetDouble(face, "AxialMinMm")),
|
|
["axial_max_mm"] = FormatDouble(GetDouble(face, "AxialMaxMm")),
|
|
["loop_count"] = GetString(face, "LoopCount")
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
static void MergeComponentFrame(ModelEvidence evidence, string componentName, JsonElement fixation)
|
|
{
|
|
var component = evidence.Components.FirstOrDefault(c => c.Id.Equals(componentName, StringComparison.OrdinalIgnoreCase));
|
|
if (component == null)
|
|
{
|
|
component = new ComponentEvidence
|
|
{
|
|
Id = componentName,
|
|
Name = componentName,
|
|
Category = NormalizeCategory(GetString(fixation, "Category"))
|
|
};
|
|
evidence.Components.Add(component);
|
|
}
|
|
|
|
var axis = GetDoubleArray(fixation, "Axis");
|
|
var axisPoint = GetDoubleArray(fixation, "AxisPointMm");
|
|
if (axis.Length == 3)
|
|
component.Axis = axis;
|
|
if (axisPoint.Length == 3)
|
|
component.AxisPointMm = axisPoint;
|
|
component.AxialMinMm = GetDouble(fixation, "AxialMinMm");
|
|
component.AxialMaxMm = GetDouble(fixation, "AxialMaxMm");
|
|
component.InnerRadiusMm = GetNullableDouble(fixation, "InnerRadiusMm");
|
|
component.OuterRadiusMm = GetNullableDouble(fixation, "OuterRadiusMm");
|
|
}
|
|
|
|
static void AddOrMergeComponent(ModelEvidence evidence, ComponentEvidence component)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(component.Id))
|
|
return;
|
|
|
|
var existing = evidence.Components.FirstOrDefault(c => c.Id.Equals(component.Id, StringComparison.OrdinalIgnoreCase));
|
|
if (existing == null)
|
|
{
|
|
evidence.Components.Add(component);
|
|
return;
|
|
}
|
|
|
|
if (string.IsNullOrWhiteSpace(existing.Name))
|
|
existing.Name = component.Name;
|
|
if (string.IsNullOrWhiteSpace(existing.PartName))
|
|
existing.PartName = component.PartName;
|
|
if (string.IsNullOrWhiteSpace(existing.Path))
|
|
existing.Path = component.Path;
|
|
bool shouldUpgradeCategory =
|
|
string.IsNullOrWhiteSpace(existing.Category) ||
|
|
existing.Category == "unknown" ||
|
|
(existing.Category == "sleeve" && component.Category == "sleeve_or_ring");
|
|
if (shouldUpgradeCategory)
|
|
existing.Category = component.Category;
|
|
if (existing.Axis.Length == 0 && component.Axis.Length == 3)
|
|
existing.Axis = component.Axis;
|
|
if (existing.AxisPointMm.Length == 0 && component.AxisPointMm.Length == 3)
|
|
existing.AxisPointMm = component.AxisPointMm;
|
|
existing.AxialMinMm = existing.AxialMinMm == 0 ? component.AxialMinMm : existing.AxialMinMm;
|
|
existing.AxialMaxMm = existing.AxialMaxMm == 0 ? component.AxialMaxMm : existing.AxialMaxMm;
|
|
existing.InnerRadiusMm ??= component.InnerRadiusMm;
|
|
existing.OuterRadiusMm ??= component.OuterRadiusMm;
|
|
}
|
|
|
|
static string ExtractComponentName(string reference)
|
|
{
|
|
var idx = reference.IndexOf(':');
|
|
return idx > 0 ? reference[..idx] : "";
|
|
}
|
|
|
|
static string GuessCategory(string name)
|
|
{
|
|
if (ContainsAny(name, "套筒", "轴套", "衬套", "sleeve", "bushing"))
|
|
return "sleeve";
|
|
if (ContainsAny(name, "端盖", "箱体", "孔", "housing", "cover", "hole"))
|
|
return "host_hole_component";
|
|
if (ContainsAny(name, "轴承", "bearing"))
|
|
return "bearing";
|
|
return "unknown";
|
|
}
|
|
|
|
static bool ContainsAny(string text, params string[] tokens)
|
|
{
|
|
return tokens.Any(t => text.Contains(t, StringComparison.OrdinalIgnoreCase));
|
|
}
|
|
|
|
static bool IsSleeveOwner(string owner)
|
|
{
|
|
return ContainsAny(owner, "套筒", "轴套", "衬套", "sleeve", "bushing");
|
|
}
|
|
|
|
static bool TryArray(JsonElement obj, string name, out JsonElement value)
|
|
{
|
|
if (obj.ValueKind == JsonValueKind.Object && obj.TryGetProperty(name, out value) && value.ValueKind == JsonValueKind.Array)
|
|
return true;
|
|
value = default;
|
|
return false;
|
|
}
|
|
|
|
static bool TryObject(JsonElement obj, string name, out JsonElement value)
|
|
{
|
|
if (obj.ValueKind == JsonValueKind.Object && obj.TryGetProperty(name, out value) && value.ValueKind == JsonValueKind.Object)
|
|
return true;
|
|
value = default;
|
|
return false;
|
|
}
|
|
|
|
static string GetString(JsonElement obj, string name)
|
|
{
|
|
if (obj.ValueKind != JsonValueKind.Object || !obj.TryGetProperty(name, out var value))
|
|
return "";
|
|
return value.ValueKind == JsonValueKind.String ? value.GetString() ?? "" : value.ToString();
|
|
}
|
|
|
|
static double GetDouble(JsonElement obj, string name)
|
|
{
|
|
if (obj.ValueKind != JsonValueKind.Object || !obj.TryGetProperty(name, out var value))
|
|
return 0;
|
|
return value.ValueKind == JsonValueKind.Number && value.TryGetDouble(out var result) ? result : 0;
|
|
}
|
|
|
|
static double? GetNullableDouble(JsonElement obj, string name)
|
|
{
|
|
if (obj.ValueKind != JsonValueKind.Object || !obj.TryGetProperty(name, out var value))
|
|
return null;
|
|
return value.ValueKind == JsonValueKind.Number && value.TryGetDouble(out var result) ? result : null;
|
|
}
|
|
|
|
static double[] GetDoubleArray(JsonElement obj, string name)
|
|
{
|
|
if (obj.ValueKind != JsonValueKind.Object || !obj.TryGetProperty(name, out var value) || value.ValueKind != JsonValueKind.Array)
|
|
return [];
|
|
return value.EnumerateArray()
|
|
.Where(v => v.ValueKind == JsonValueKind.Number && v.TryGetDouble(out _))
|
|
.Select(v => v.GetDouble())
|
|
.ToArray();
|
|
}
|
|
|
|
static IEnumerable<string> ReadStringArray(JsonElement obj, string name)
|
|
{
|
|
if (obj.ValueKind != JsonValueKind.Object || !obj.TryGetProperty(name, out var value) || value.ValueKind != JsonValueKind.Array)
|
|
yield break;
|
|
|
|
foreach (var item in value.EnumerateArray())
|
|
{
|
|
if (item.ValueKind == JsonValueKind.String)
|
|
yield return item.GetString() ?? "";
|
|
else if (item.ValueKind != JsonValueKind.Null && item.ValueKind != JsonValueKind.Undefined)
|
|
yield return item.ToString();
|
|
}
|
|
}
|
|
|
|
static string FormatDouble(double value) => value.ToString("0.####", System.Globalization.CultureInfo.InvariantCulture);
|
|
|
|
static string FormatArray(double[] values) => string.Join(",", values.Select(FormatDouble));
|
|
|
|
static string AttributeString(JsonElement obj, string key)
|
|
{
|
|
if (!TryObject(obj, "Attributes", out var attrs) && !TryObject(obj, "attributes", out attrs))
|
|
return "";
|
|
return GetString(attrs, key);
|
|
}
|
|
|
|
static bool GetBool(JsonElement obj, string name)
|
|
{
|
|
if (obj.ValueKind != JsonValueKind.Object || !obj.TryGetProperty(name, out var value))
|
|
return false;
|
|
return value.ValueKind switch
|
|
{
|
|
JsonValueKind.True => true,
|
|
JsonValueKind.False => false,
|
|
JsonValueKind.String => bool.TryParse(value.GetString(), out var b) && b,
|
|
_ => false
|
|
};
|
|
}
|
|
|
|
static string NormalizeCategory(string value)
|
|
{
|
|
var v = NormalizeToken(value);
|
|
return v switch
|
|
{
|
|
"spacer_or_ring" => "sleeve_or_ring",
|
|
"end_cap" or "housing" => "host_hole_component",
|
|
_ => v
|
|
};
|
|
}
|
|
|
|
static string NormalizeToken(string value)
|
|
{
|
|
return value.Trim().Replace(' ', '_').ToLowerInvariant();
|
|
}
|
|
}
|
|
|
|
sealed class SketchEdgeInfo
|
|
{
|
|
public string Id { get; set; } = "";
|
|
public string Owner { get; set; } = "";
|
|
public string Role { get; set; } = "";
|
|
public string Orientation { get; set; } = "";
|
|
public double Length { get; set; }
|
|
public double[] Start { get; set; } = [];
|
|
public double[] End { get; set; } = [];
|
|
public List<string> SourceEdgeIds { get; set; } = [];
|
|
}
|
|
|
|
static class GapFeatureSynthesizer
|
|
{
|
|
public static void Synthesize(JsonElement sketch, List<SketchEdgeInfo> edges, ModelEvidence evidence)
|
|
{
|
|
var usableEdges = edges
|
|
.Where(e => e.Start.Length >= 2 && e.End.Length >= 2 && EdgeLength(e) > 0.2)
|
|
.ToList();
|
|
var sleeveEdges = usableEdges.Where(e => IsSleeveOwner(e.Owner)).ToList();
|
|
var hostEdges = usableEdges.Where(e => IsHostOwner(e.Owner)).ToList();
|
|
if (sleeveEdges.Count == 0 || hostEdges.Count == 0)
|
|
return;
|
|
|
|
var polygons = BuildMaterialPolygons(sketch, usableEdges);
|
|
if (polygons.Count == 0)
|
|
return;
|
|
var contactPairs = BuildContactPairs(sketch);
|
|
|
|
var bounds = Bounds(usableEdges);
|
|
var width = bounds.MaxX - bounds.MinX;
|
|
var height = bounds.MaxY - bounds.MinY;
|
|
var span = Math.Max(width, height);
|
|
if (span <= 0)
|
|
return;
|
|
|
|
var margin = Math.Max(5.0, span * 0.05);
|
|
bounds = bounds.Expand(margin);
|
|
var resolution = Math.Clamp(span / 180.0, 0.75, 2.0);
|
|
var nx = Math.Clamp((int)Math.Ceiling((bounds.MaxX - bounds.MinX) / resolution), 20, 280);
|
|
var ny = Math.Clamp((int)Math.Ceiling((bounds.MaxY - bounds.MinY) / resolution), 20, 280);
|
|
resolution = Math.Max((bounds.MaxX - bounds.MinX) / nx, (bounds.MaxY - bounds.MinY) / ny);
|
|
|
|
var solid = new bool[nx, ny];
|
|
var outside = new bool[nx, ny];
|
|
for (int ix = 0; ix < nx; ix++)
|
|
for (int iy = 0; iy < ny; iy++)
|
|
{
|
|
var p = CellCenter(bounds, resolution, ix, iy);
|
|
solid[ix, iy] = polygons.Any(poly => PointInPolygon(p, poly.Points));
|
|
}
|
|
|
|
FloodOutside(solid, outside, nx, ny);
|
|
|
|
var candidateCells = new List<GapCell>();
|
|
const double MinimumToolGapWidthMm = 2.0;
|
|
var maxGapWidth = Math.Min(25.0, Math.Max(6.0, span * 0.16));
|
|
for (int ix = 0; ix < nx; ix++)
|
|
for (int iy = 0; iy < ny; iy++)
|
|
{
|
|
if (!outside[ix, iy] || solid[ix, iy])
|
|
continue;
|
|
var p = CellCenter(bounds, resolution, ix, iy);
|
|
var sleeveNearest = NearestEdge(p, sleeveEdges);
|
|
var hostNearest = NearestEdge(p, hostEdges);
|
|
if (IsKnownContactPair(contactPairs, sleeveNearest.Edge, hostNearest.Edge))
|
|
continue;
|
|
if (!IsBetweenMaterialBoundaries(p, sleeveNearest.Point, hostNearest.Point))
|
|
continue;
|
|
var sleeveDistance = sleeveNearest.Distance;
|
|
var hostDistance = hostNearest.Distance;
|
|
var estimatedWidth = sleeveDistance + hostDistance;
|
|
if (sleeveDistance <= maxGapWidth &&
|
|
hostDistance <= maxGapWidth &&
|
|
estimatedWidth >= MinimumToolGapWidthMm &&
|
|
estimatedWidth <= maxGapWidth)
|
|
{
|
|
candidateCells.Add(new GapCell
|
|
{
|
|
X = ix,
|
|
Y = iy,
|
|
Point = p,
|
|
SleeveDistance = sleeveDistance,
|
|
HostDistance = hostDistance,
|
|
EstimatedWidth = estimatedWidth
|
|
});
|
|
}
|
|
}
|
|
|
|
if (candidateCells.Count == 0)
|
|
return;
|
|
|
|
var clusters = BuildClusters(candidateCells, nx, ny);
|
|
var best = clusters
|
|
.Select(c => new GapCluster(c))
|
|
.Where(c => c.MinSleeveDistance <= resolution * 2.5)
|
|
.Where(c => c.MinEstimatedWidth >= MinimumToolGapWidthMm)
|
|
.Where(c => c.AxialExtent >= Math.Max(2.0, resolution * 3.0))
|
|
.Where(c => HasExteriorMouth(c, solid, nx, ny))
|
|
.OrderBy(c => c.MinEstimatedWidth)
|
|
.ThenByDescending(c => c.Cells.Count)
|
|
.FirstOrDefault();
|
|
if (best == null)
|
|
return;
|
|
|
|
var confidence = best.MinEstimatedWidth <= maxGapWidth * 0.65 ? 0.74 : 0.70;
|
|
var attributes = new Dictionary<string, string>
|
|
{
|
|
["estimated_min_width_mm"] = FormatDouble(best.MinEstimatedWidth),
|
|
["axial_extent_mm"] = FormatDouble(best.AxialExtent),
|
|
["radial_min_mm"] = FormatDouble(best.RadialMin),
|
|
["radial_max_mm"] = FormatDouble(best.RadialMax),
|
|
["centroid_axial_mm"] = FormatDouble(best.CentroidX),
|
|
["centroid_radial_mm"] = FormatDouble(best.CentroidY),
|
|
["exterior_mouth"] = "radial_clear_ray",
|
|
["cell_count"] = best.Cells.Count.ToString(System.Globalization.CultureInfo.InvariantCulture),
|
|
["grid_resolution_mm"] = FormatDouble(resolution),
|
|
["max_gap_width_mm"] = FormatDouble(maxGapWidth)
|
|
};
|
|
AddEntityOnce(evidence, "gap_region:auto_tool_gap", "region", "void_gap");
|
|
AddEvidenceOnce(evidence, "outside_connected_void_gap", "gap_region", "outside", confidence, "GapFeatureSynthesizer.SectionFloodFill", attributes);
|
|
AddEvidenceOnce(evidence, "gap_path_reaches_sleeve_push_area", "gap_region", "sleeve_push_area", confidence, "GapFeatureSynthesizer.SectionFloodFill", attributes);
|
|
AddEvidenceOnce(evidence, "tool_force_direction_valid", "gap_region", "removal_axis", 0.70, "GapFeatureSynthesizer.SectionFloodFill", attributes);
|
|
AddEvidenceOnce(evidence, "gap_width_sufficient", "gap_region", "h", confidence, "GapFeatureSynthesizer.SectionFloodFill", attributes);
|
|
|
|
evidence.SectionInstances.AddRelation("connected_to_outside", "gap_region", "outside", confidence, "GapFeatureSynthesizer.SectionFloodFill");
|
|
evidence.SectionInstances.AddRelation("reaches", "gap_region", "sleeve_push_area", confidence, "GapFeatureSynthesizer.SectionFloodFill");
|
|
evidence.SectionInstances.AddRelation("direction_has_component", "gap_region", "removal_axis", 0.70, "GapFeatureSynthesizer.SectionFloodFill");
|
|
evidence.SectionInstances.AddRelation("gap_width_sufficient", "gap_region", "h", confidence, "GapFeatureSynthesizer.SectionFloodFill");
|
|
}
|
|
|
|
static List<MaterialPolygon> BuildMaterialPolygons(JsonElement sketch, List<SketchEdgeInfo> edges)
|
|
{
|
|
var edgeMap = edges.ToDictionary(e => e.Id, StringComparer.OrdinalIgnoreCase);
|
|
var result = new List<MaterialPolygon>();
|
|
if (!TryArray(sketch, "Loops", out var loops))
|
|
return result;
|
|
|
|
foreach (var loop in loops.EnumerateArray())
|
|
{
|
|
if (!GetBool(loop, "Closed") || !TryArray(loop, "EdgeRefs", out var refs))
|
|
continue;
|
|
|
|
var loopEdges = new List<SketchEdgeInfo>();
|
|
foreach (var reference in refs.EnumerateArray())
|
|
{
|
|
if (reference.ValueKind == JsonValueKind.String &&
|
|
edgeMap.TryGetValue(reference.GetString() ?? "", out var edge))
|
|
loopEdges.Add(edge);
|
|
}
|
|
|
|
if (loopEdges.Count < 3)
|
|
continue;
|
|
var points = ChainLoopPoints(loopEdges);
|
|
if (points.Count < 3 || Math.Abs(PolygonArea(points)) < 1.0)
|
|
continue;
|
|
|
|
result.Add(new MaterialPolygon { Points = points });
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
static HashSet<string> BuildContactPairs(JsonElement sketch)
|
|
{
|
|
var result = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
|
if (!TryArray(sketch, "Relations", out var relations))
|
|
return result;
|
|
|
|
foreach (var relation in relations.EnumerateArray())
|
|
{
|
|
var type = NormalizeToken(GetString(relation, "Type"));
|
|
if (type != "contact")
|
|
continue;
|
|
var a = GetString(relation, "A");
|
|
var b = GetString(relation, "B");
|
|
if (!string.IsNullOrWhiteSpace(a) && !string.IsNullOrWhiteSpace(b))
|
|
result.Add(PairKey(a, b));
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
static List<Point2> ChainLoopPoints(List<SketchEdgeInfo> loopEdges)
|
|
{
|
|
var points = new List<Point2>();
|
|
var first = ToPoint(loopEdges[0].Start);
|
|
var current = ToPoint(loopEdges[0].End);
|
|
points.Add(first);
|
|
points.Add(current);
|
|
|
|
foreach (var edge in loopEdges.Skip(1))
|
|
{
|
|
var start = ToPoint(edge.Start);
|
|
var end = ToPoint(edge.End);
|
|
if (Distance(current, start) <= Distance(current, end))
|
|
{
|
|
current = end;
|
|
points.Add(end);
|
|
}
|
|
else
|
|
{
|
|
current = start;
|
|
points.Add(start);
|
|
}
|
|
}
|
|
|
|
if (points.Count > 1 && Distance(points[0], points[^1]) < 0.5)
|
|
points.RemoveAt(points.Count - 1);
|
|
return points;
|
|
}
|
|
|
|
static void FloodOutside(bool[,] solid, bool[,] outside, int nx, int ny)
|
|
{
|
|
var q = new Queue<(int X, int Y)>();
|
|
void Enqueue(int x, int y)
|
|
{
|
|
if (x < 0 || y < 0 || x >= nx || y >= ny || solid[x, y] || outside[x, y])
|
|
return;
|
|
outside[x, y] = true;
|
|
q.Enqueue((x, y));
|
|
}
|
|
|
|
for (int x = 0; x < nx; x++)
|
|
{
|
|
Enqueue(x, 0);
|
|
Enqueue(x, ny - 1);
|
|
}
|
|
for (int y = 0; y < ny; y++)
|
|
{
|
|
Enqueue(0, y);
|
|
Enqueue(nx - 1, y);
|
|
}
|
|
|
|
int[] dx = [1, -1, 0, 0];
|
|
int[] dy = [0, 0, 1, -1];
|
|
while (q.Count > 0)
|
|
{
|
|
var c = q.Dequeue();
|
|
for (int i = 0; i < 4; i++)
|
|
Enqueue(c.X + dx[i], c.Y + dy[i]);
|
|
}
|
|
}
|
|
|
|
static List<List<GapCell>> BuildClusters(List<GapCell> cells, int nx, int ny)
|
|
{
|
|
var byCoord = cells.ToDictionary(c => (c.X, c.Y));
|
|
var visited = new HashSet<(int X, int Y)>();
|
|
var clusters = new List<List<GapCell>>();
|
|
int[] dx = [1, -1, 0, 0];
|
|
int[] dy = [0, 0, 1, -1];
|
|
|
|
foreach (var seed in cells)
|
|
{
|
|
if (!visited.Add((seed.X, seed.Y)))
|
|
continue;
|
|
var cluster = new List<GapCell>();
|
|
var q = new Queue<GapCell>();
|
|
q.Enqueue(seed);
|
|
while (q.Count > 0)
|
|
{
|
|
var cell = q.Dequeue();
|
|
cluster.Add(cell);
|
|
for (int i = 0; i < 4; i++)
|
|
{
|
|
var key = (cell.X + dx[i], cell.Y + dy[i]);
|
|
if (key.Item1 < 0 || key.Item2 < 0 || key.Item1 >= nx || key.Item2 >= ny)
|
|
continue;
|
|
if (visited.Contains(key) || !byCoord.TryGetValue(key, out var next))
|
|
continue;
|
|
visited.Add(key);
|
|
q.Enqueue(next);
|
|
}
|
|
}
|
|
clusters.Add(cluster);
|
|
}
|
|
|
|
return clusters;
|
|
}
|
|
|
|
static bool HasExteriorMouth(GapCluster cluster, bool[,] solid, int nx, int ny)
|
|
{
|
|
return cluster.Cells.Any(c =>
|
|
HasClearRadialRay(c.X, c.Y, 1, solid, nx, ny) ||
|
|
HasClearRadialRay(c.X, c.Y, -1, solid, nx, ny));
|
|
}
|
|
|
|
static bool HasClearRadialRay(int x, int y, int dy, bool[,] solid, int nx, int ny)
|
|
{
|
|
for (var iy = y; iy >= 0 && iy < ny; iy += dy)
|
|
{
|
|
if (x < 0 || x >= nx || solid[x, iy])
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
static void AddEntityOnce(ModelEvidence evidence, string id, string kind, string role)
|
|
{
|
|
if (evidence.SectionInstances.Entities.Any(e => e.Id.Equals(id, StringComparison.OrdinalIgnoreCase)))
|
|
return;
|
|
evidence.SectionInstances.Entities.Add(new StructureEntity { Id = id, Kind = kind, SemanticRole = role });
|
|
}
|
|
|
|
static void AddEvidenceOnce(ModelEvidence evidence, string type, string owner, string related, double confidence, string source, Dictionary<string, string>? attributes = null)
|
|
{
|
|
if (evidence.Items.Any(i =>
|
|
i.Type.Equals(type, StringComparison.OrdinalIgnoreCase) &&
|
|
i.Owner.Equals(owner, StringComparison.OrdinalIgnoreCase) &&
|
|
i.Related.Equals(related, StringComparison.OrdinalIgnoreCase)))
|
|
return;
|
|
|
|
evidence.Items.Add(new EvidenceItem
|
|
{
|
|
Id = $"{type}:{owner}:{related}:gap",
|
|
Type = type,
|
|
Owner = owner,
|
|
Related = related,
|
|
Confidence = confidence,
|
|
Source = source,
|
|
Attributes = attributes == null
|
|
? []
|
|
: new Dictionary<string, string>(attributes, StringComparer.OrdinalIgnoreCase)
|
|
});
|
|
}
|
|
|
|
static SectionBounds Bounds(List<SketchEdgeInfo> edges)
|
|
{
|
|
var xs = edges.SelectMany(e => new[] { e.Start[0], e.End[0] }).ToList();
|
|
var ys = edges.SelectMany(e => new[] { e.Start[1], e.End[1] }).ToList();
|
|
return new SectionBounds(xs.Min(), xs.Max(), ys.Min(), ys.Max());
|
|
}
|
|
|
|
static Point2 CellCenter(SectionBounds bounds, double resolution, int ix, int iy)
|
|
{
|
|
return new Point2(bounds.MinX + (ix + 0.5) * resolution, bounds.MinY + (iy + 0.5) * resolution);
|
|
}
|
|
|
|
static bool PointInPolygon(Point2 p, List<Point2> poly)
|
|
{
|
|
var inside = false;
|
|
for (int i = 0, j = poly.Count - 1; i < poly.Count; j = i++)
|
|
{
|
|
var a = poly[i];
|
|
var b = poly[j];
|
|
if (((a.Y > p.Y) != (b.Y > p.Y)) &&
|
|
p.X < (b.X - a.X) * (p.Y - a.Y) / ((b.Y - a.Y) == 0 ? 1e-9 : (b.Y - a.Y)) + a.X)
|
|
inside = !inside;
|
|
}
|
|
return inside;
|
|
}
|
|
|
|
static NearestEdgeResult NearestEdge(Point2 p, List<SketchEdgeInfo> edges)
|
|
{
|
|
var bestEdge = edges[0];
|
|
var bestPoint = ToPoint(bestEdge.Start);
|
|
var bestDistance = double.PositiveInfinity;
|
|
foreach (var edge in edges)
|
|
{
|
|
var point = ClosestPointOnSegment(p, ToPoint(edge.Start), ToPoint(edge.End));
|
|
var distance = Distance(p, point);
|
|
if (distance < bestDistance)
|
|
{
|
|
bestEdge = edge;
|
|
bestPoint = point;
|
|
bestDistance = distance;
|
|
}
|
|
}
|
|
return new NearestEdgeResult(bestEdge, bestPoint, bestDistance);
|
|
}
|
|
|
|
static double DistancePointToSegment(Point2 p, Point2 a, Point2 b)
|
|
{
|
|
return Distance(p, ClosestPointOnSegment(p, a, b));
|
|
}
|
|
|
|
static Point2 ClosestPointOnSegment(Point2 p, Point2 a, Point2 b)
|
|
{
|
|
var dx = b.X - a.X;
|
|
var dy = b.Y - a.Y;
|
|
var len2 = dx * dx + dy * dy;
|
|
if (len2 < 1e-9)
|
|
return a;
|
|
var t = Math.Clamp(((p.X - a.X) * dx + (p.Y - a.Y) * dy) / len2, 0, 1);
|
|
return new Point2(a.X + t * dx, a.Y + t * dy);
|
|
}
|
|
|
|
static bool IsBetweenMaterialBoundaries(Point2 p, Point2 sleevePoint, Point2 hostPoint)
|
|
{
|
|
var a = Normalize(Sub(sleevePoint, p));
|
|
var b = Normalize(Sub(hostPoint, p));
|
|
return Dot(a, b) <= -0.25;
|
|
}
|
|
|
|
static bool IsKnownContactPair(HashSet<string> contactPairs, SketchEdgeInfo sleeveEdge, SketchEdgeInfo hostEdge)
|
|
{
|
|
foreach (var a in EdgeAliases(sleeveEdge))
|
|
foreach (var b in EdgeAliases(hostEdge))
|
|
{
|
|
if (contactPairs.Contains(PairKey(a, b)))
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
static IEnumerable<string> EdgeAliases(SketchEdgeInfo edge)
|
|
{
|
|
yield return edge.Id;
|
|
foreach (var source in edge.SourceEdgeIds)
|
|
yield return source;
|
|
}
|
|
|
|
static string PairKey(string a, string b)
|
|
{
|
|
return string.Compare(a, b, StringComparison.OrdinalIgnoreCase) <= 0
|
|
? $"{NormalizeToken(a)}|{NormalizeToken(b)}"
|
|
: $"{NormalizeToken(b)}|{NormalizeToken(a)}";
|
|
}
|
|
|
|
static double EdgeLength(SketchEdgeInfo edge) => Distance(ToPoint(edge.Start), ToPoint(edge.End));
|
|
static Point2 ToPoint(double[] value) => new(value[0], value[1]);
|
|
static double Distance(Point2 a, Point2 b)
|
|
{
|
|
var dx = a.X - b.X;
|
|
var dy = a.Y - b.Y;
|
|
return Math.Sqrt(dx * dx + dy * dy);
|
|
}
|
|
static Point2 Sub(Point2 a, Point2 b) => new(a.X - b.X, a.Y - b.Y);
|
|
static double Dot(Point2 a, Point2 b) => a.X * b.X + a.Y * b.Y;
|
|
static Point2 Normalize(Point2 p)
|
|
{
|
|
var n = Math.Sqrt(p.X * p.X + p.Y * p.Y);
|
|
return n < 1e-9 ? new Point2(0, 0) : new Point2(p.X / n, p.Y / n);
|
|
}
|
|
static double PolygonArea(List<Point2> points)
|
|
{
|
|
double area = 0;
|
|
for (int i = 0; i < points.Count; i++)
|
|
{
|
|
var a = points[i];
|
|
var b = points[(i + 1) % points.Count];
|
|
area += a.X * b.Y - b.X * a.Y;
|
|
}
|
|
return area / 2.0;
|
|
}
|
|
|
|
static string FormatDouble(double value) => value.ToString("0.####", System.Globalization.CultureInfo.InvariantCulture);
|
|
|
|
static bool IsSleeveOwner(string owner)
|
|
{
|
|
return ContainsAny(owner, "套筒", "濂楃瓛", "轴套", "衬套", "sleeve", "bushing");
|
|
}
|
|
|
|
static bool IsHostOwner(string owner)
|
|
{
|
|
return ContainsAny(owner, "端盖", "绔洊", "箱体", "孔", "housing", "cover", "hole", "host");
|
|
}
|
|
|
|
static bool ContainsAny(string text, params string[] tokens)
|
|
{
|
|
return tokens.Any(t => text.Contains(t, StringComparison.OrdinalIgnoreCase));
|
|
}
|
|
|
|
static bool TryArray(JsonElement obj, string name, out JsonElement value)
|
|
{
|
|
if (obj.ValueKind == JsonValueKind.Object && obj.TryGetProperty(name, out value) && value.ValueKind == JsonValueKind.Array)
|
|
return true;
|
|
value = default;
|
|
return false;
|
|
}
|
|
|
|
static string GetString(JsonElement obj, string name)
|
|
{
|
|
if (obj.ValueKind != JsonValueKind.Object || !obj.TryGetProperty(name, out var value))
|
|
return "";
|
|
return value.ValueKind == JsonValueKind.String ? value.GetString() ?? "" : value.ToString();
|
|
}
|
|
|
|
static string NormalizeToken(string value) => value.Trim().Replace(' ', '_').ToLowerInvariant();
|
|
|
|
static bool GetBool(JsonElement obj, string name)
|
|
{
|
|
return obj.ValueKind == JsonValueKind.Object &&
|
|
obj.TryGetProperty(name, out var value) &&
|
|
value.ValueKind is JsonValueKind.True;
|
|
}
|
|
|
|
readonly record struct Point2(double X, double Y);
|
|
readonly record struct NearestEdgeResult(SketchEdgeInfo Edge, Point2 Point, double Distance);
|
|
sealed class MaterialPolygon
|
|
{
|
|
public List<Point2> Points { get; set; } = [];
|
|
}
|
|
sealed class GapCell
|
|
{
|
|
public int X { get; set; }
|
|
public int Y { get; set; }
|
|
public Point2 Point { get; set; }
|
|
public double SleeveDistance { get; set; }
|
|
public double HostDistance { get; set; }
|
|
public double EstimatedWidth { get; set; }
|
|
}
|
|
sealed class GapCluster
|
|
{
|
|
public GapCluster(List<GapCell> cells)
|
|
{
|
|
Cells = cells;
|
|
MinSleeveDistance = cells.Min(c => c.SleeveDistance);
|
|
MinEstimatedWidth = cells.Min(c => c.EstimatedWidth);
|
|
AxialExtent = cells.Max(c => c.Point.X) - cells.Min(c => c.Point.X);
|
|
RadialMin = cells.Min(c => c.Point.Y);
|
|
RadialMax = cells.Max(c => c.Point.Y);
|
|
CentroidX = cells.Average(c => c.Point.X);
|
|
CentroidY = cells.Average(c => c.Point.Y);
|
|
}
|
|
public List<GapCell> Cells { get; }
|
|
public double MinSleeveDistance { get; }
|
|
public double MinEstimatedWidth { get; }
|
|
public double AxialExtent { get; }
|
|
public double RadialMin { get; }
|
|
public double RadialMax { get; }
|
|
public double CentroidX { get; }
|
|
public double CentroidY { get; }
|
|
}
|
|
readonly record struct SectionBounds(double MinX, double MaxX, double MinY, double MaxY)
|
|
{
|
|
public SectionBounds Expand(double amount) => new(MinX - amount, MaxX + amount, MinY - amount, MaxY + amount);
|
|
}
|
|
}
|
|
|
|
static class PortFeatureSynthesizer
|
|
{
|
|
public static void Synthesize(ModelEvidence evidence)
|
|
{
|
|
foreach (var scene in evidence.Scenes.Where(s => s.SceneType.Equals("sleeve_in_hole", StringComparison.OrdinalIgnoreCase)))
|
|
{
|
|
var sleeve = evidence.Components.FirstOrDefault(c => c.Id.Equals(scene.Target, StringComparison.OrdinalIgnoreCase));
|
|
var host = evidence.Components.FirstOrDefault(c => c.Id.Equals(scene.Host, StringComparison.OrdinalIgnoreCase));
|
|
if (sleeve == null || host == null || sleeve.Axis.Length != 3 || sleeve.AxisPointMm.Length != 3)
|
|
continue;
|
|
|
|
var sleeveAxis = Normalize(sleeve.Axis);
|
|
var sleeveAxisPoint = sleeve.AxisPointMm;
|
|
var sleeveOuterRadius = sleeve.OuterRadiusMm ?? Math.Max(host.InnerRadiusMm ?? 0, 1.0);
|
|
var hostOuterRadius = host.OuterRadiusMm ?? Math.Max(sleeveOuterRadius, 1.0);
|
|
var maxPortRadius = Math.Max(2.0, Math.Min(20.0, hostOuterRadius * 0.30));
|
|
|
|
var candidates = evidence.Items
|
|
.Where(i => i.Type.Equals("cylindrical_face", StringComparison.OrdinalIgnoreCase))
|
|
.Where(i => i.Owner.Equals(host.Id, StringComparison.OrdinalIgnoreCase))
|
|
.Select(ParseCylinder)
|
|
.Where(c => c != null)
|
|
.Select(c => c!)
|
|
.Where(c => c.RadiusMm >= 0.5 && c.RadiusMm <= maxPortRadius)
|
|
.Where(c => !LooksLikeMainCoaxialCylinder(c, sleeve, host))
|
|
.Select(c => new
|
|
{
|
|
Cylinder = c,
|
|
AxisDistance = DistanceLineToLine(c.AxisPointMm, c.Axis, sleeveAxisPoint, sleeveAxis),
|
|
DirectionComponent = Math.Abs(Dot(Normalize(c.Axis), sleeveAxis))
|
|
})
|
|
.Where(c => c.AxisDistance <= sleeveOuterRadius + c.Cylinder.RadiusMm + 2.0)
|
|
.Where(c => c.DirectionComponent >= 0.35)
|
|
.OrderBy(c => c.AxisDistance)
|
|
.ThenBy(c => c.Cylinder.RadiusMm)
|
|
.ToList();
|
|
|
|
foreach (var candidate in candidates.Take(2))
|
|
{
|
|
var id = $"port_region:{host.Id}:face_{candidate.Cylinder.FaceIndex}";
|
|
AddEntityOnce(evidence, id, "region", "ejector_or_threaded_hole");
|
|
AddEvidenceOnce(evidence, "outside_connected_cylindrical_port", host.Id, id, 0.72, "PortFeatureSynthesizer.CylinderFaces");
|
|
AddEvidenceOnce(evidence, "port_axis_intersects_sleeve_push_area", host.Id, id, 0.72, "PortFeatureSynthesizer.CylinderFaces");
|
|
AddEvidenceOnce(evidence, "port_push_direction_valid", host.Id, id, 0.72, "PortFeatureSynthesizer.CylinderFaces");
|
|
if (candidate.Cylinder.RadiusMm * 2.0 >= 1.0)
|
|
AddEvidenceOnce(evidence, "port_diameter_sufficient", host.Id, id, 0.74, "PortFeatureSynthesizer.CylinderFaces");
|
|
|
|
evidence.SectionInstances.AddRelation("connected_to_outside", "port_region", "outside", 0.72, "PortFeatureSynthesizer.CylinderFaces");
|
|
evidence.SectionInstances.AddRelation("axis_intersects", "port_axis", "sleeve_push_area", 0.72, "PortFeatureSynthesizer.CylinderFaces");
|
|
evidence.SectionInstances.AddRelation("direction_has_component", "port_axis", "removal_axis", 0.72, "PortFeatureSynthesizer.CylinderFaces");
|
|
evidence.SectionInstances.AddRelation("port_diameter_sufficient", "port_region", "t", 0.74, "PortFeatureSynthesizer.CylinderFaces");
|
|
}
|
|
}
|
|
}
|
|
|
|
static CylinderCandidate? ParseCylinder(EvidenceItem item)
|
|
{
|
|
var radius = ParseDouble(Attribute(item, "radius_mm"));
|
|
var axis = ParseArray(Attribute(item, "axis"));
|
|
var axisPoint = ParseArray(Attribute(item, "axis_point_mm"));
|
|
if (radius <= 0 || axis.Length != 3 || axisPoint.Length != 3)
|
|
return null;
|
|
|
|
return new CylinderCandidate
|
|
{
|
|
FaceIndex = item.Related,
|
|
RadiusMm = radius,
|
|
Axis = Normalize(axis),
|
|
AxisPointMm = axisPoint
|
|
};
|
|
}
|
|
|
|
static bool LooksLikeMainCoaxialCylinder(CylinderCandidate cylinder, ComponentEvidence sleeve, ComponentEvidence host)
|
|
{
|
|
if (sleeve.Axis.Length != 3 || sleeve.AxisPointMm.Length != 3)
|
|
return false;
|
|
|
|
var sleeveAxis = Normalize(sleeve.Axis);
|
|
var parallel = Math.Abs(Dot(cylinder.Axis, sleeveAxis)) >= 0.97;
|
|
if (!parallel)
|
|
return false;
|
|
|
|
var axisDistance = DistancePointToLine(cylinder.AxisPointMm, sleeve.AxisPointMm, sleeveAxis);
|
|
if (axisDistance > 2.0)
|
|
return false;
|
|
|
|
var mainRadii = new[] { sleeve.InnerRadiusMm, sleeve.OuterRadiusMm, host.InnerRadiusMm, host.OuterRadiusMm }
|
|
.Where(r => r.HasValue && r.Value > 0)
|
|
.Select(r => r!.Value);
|
|
return mainRadii.Any(r => Math.Abs(r - cylinder.RadiusMm) <= Math.Max(1.5, r * 0.05));
|
|
}
|
|
|
|
static void AddEntityOnce(ModelEvidence evidence, string id, string kind, string role)
|
|
{
|
|
if (evidence.SectionInstances.Entities.Any(e => e.Id.Equals(id, StringComparison.OrdinalIgnoreCase)))
|
|
return;
|
|
evidence.SectionInstances.Entities.Add(new StructureEntity { Id = id, Kind = kind, SemanticRole = role });
|
|
}
|
|
|
|
static void AddEvidenceOnce(ModelEvidence evidence, string type, string owner, string related, double confidence, string source)
|
|
{
|
|
if (evidence.Items.Any(i =>
|
|
i.Type.Equals(type, StringComparison.OrdinalIgnoreCase) &&
|
|
i.Owner.Equals(owner, StringComparison.OrdinalIgnoreCase) &&
|
|
i.Related.Equals(related, StringComparison.OrdinalIgnoreCase)))
|
|
return;
|
|
|
|
evidence.Items.Add(new EvidenceItem
|
|
{
|
|
Id = $"{type}:{owner}:{related}:port",
|
|
Type = type,
|
|
Owner = owner,
|
|
Related = related,
|
|
Confidence = confidence,
|
|
Source = source
|
|
});
|
|
}
|
|
|
|
static string Attribute(EvidenceItem item, string key)
|
|
{
|
|
return item.Attributes.TryGetValue(key, out var value) ? value : "";
|
|
}
|
|
|
|
static double ParseDouble(string value)
|
|
{
|
|
return double.TryParse(value, System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out var result)
|
|
? result
|
|
: 0;
|
|
}
|
|
|
|
static double[] ParseArray(string value)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(value))
|
|
return [];
|
|
return value.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
|
|
.Select(ParseDouble)
|
|
.ToArray();
|
|
}
|
|
|
|
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)
|
|
{
|
|
var n = Norm(a);
|
|
return n < 1e-9 ? [0, 0, 0] : [a[0] / n, a[1] / n, a[2] / n];
|
|
}
|
|
|
|
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 double DistanceLineToLine(double[] p1, double[] d1Raw, double[] p2, double[] d2Raw)
|
|
{
|
|
var d1 = Normalize(d1Raw);
|
|
var d2 = Normalize(d2Raw);
|
|
var cross = Cross(d1, d2);
|
|
var crossNorm = Norm(cross);
|
|
if (crossNorm < 1e-8)
|
|
return DistancePointToLine(p1, p2, d2);
|
|
return Math.Abs(Dot(Sub(p2, p1), cross)) / crossNorm;
|
|
}
|
|
|
|
static double[] Cross(double[] a, double[] b)
|
|
{
|
|
return [
|
|
a[1] * b[2] - a[2] * b[1],
|
|
a[2] * b[0] - a[0] * b[2],
|
|
a[0] * b[1] - a[1] * b[0]
|
|
];
|
|
}
|
|
|
|
sealed class CylinderCandidate
|
|
{
|
|
public string FaceIndex { get; set; } = "";
|
|
public double RadiusMm { get; set; }
|
|
public double[] Axis { get; set; } = [];
|
|
public double[] AxisPointMm { get; set; } = [];
|
|
}
|
|
}
|
|
|
|
static class SceneRecognizer
|
|
{
|
|
public static void Recognize(ModelEvidence evidence)
|
|
{
|
|
RecognizeSleeveInHole(evidence);
|
|
RecognizeBearingMounting(evidence);
|
|
RecognizeGenericAxialLimit(evidence);
|
|
}
|
|
|
|
static void RecognizeSleeveInHole(ModelEvidence evidence)
|
|
{
|
|
var sleeves = evidence.Components
|
|
.Where(c => c.Category is "sleeve_or_ring" || ContainsAny($"{c.Name} {c.PartName}", "套筒", "轴套", "衬套", "sleeve", "bushing"))
|
|
.ToList();
|
|
var hosts = evidence.Components
|
|
.Where(c => c.Category is "host_hole_component" || ContainsAny($"{c.Name} {c.PartName}", "孔", "端盖", "箱体", "housing", "cover", "hole"))
|
|
.ToList();
|
|
|
|
foreach (var sleeve in sleeves)
|
|
foreach (var host in hosts)
|
|
{
|
|
var scene = new SceneCandidate
|
|
{
|
|
Id = $"sleeve_in_hole:{sleeve.Id}:{host.Id}",
|
|
SceneType = "sleeve_in_hole",
|
|
Target = sleeve.Id,
|
|
Host = host.Id,
|
|
Confidence = 0.45
|
|
};
|
|
scene.Evidence.Add("sleeve_component");
|
|
scene.Evidence.Add("host_hole_component");
|
|
|
|
if (evidence.Has("close_fit_contact", 0.75) || evidence.SectionInstances.HasRelation("contact"))
|
|
{
|
|
scene.Evidence.Add("close_fit_contact");
|
|
scene.Confidence = 0.78;
|
|
}
|
|
|
|
if (evidence.Has("coaxial_cylinder_pair", 0.6) || evidence.SectionInstances.HasRelation("coaxial"))
|
|
{
|
|
scene.Evidence.Add("coaxial_cylinder_pair");
|
|
scene.Confidence = Math.Max(scene.Confidence, 0.78);
|
|
}
|
|
|
|
if (evidence.Items.Any(i => i.Type == "axial_contact" && (i.Owner == sleeve.Id || i.Related == sleeve.Id)) ||
|
|
evidence.Has("axial_overlap", 0.6) ||
|
|
evidence.SectionInstances.HasRelation("axial_overlap"))
|
|
{
|
|
scene.Evidence.Add("axial_overlap");
|
|
scene.Confidence = Math.Max(scene.Confidence, 0.62);
|
|
}
|
|
|
|
AddOrMergeScene(evidence, scene);
|
|
}
|
|
}
|
|
|
|
static void RecognizeBearingMounting(ModelEvidence evidence)
|
|
{
|
|
foreach (var bearing in evidence.Components.Where(c => c.Category == "bearing" || ContainsAny(c.Name, "轴承", "bearing")))
|
|
{
|
|
var scene = new SceneCandidate
|
|
{
|
|
Id = $"bearing_mounting:{bearing.Id}",
|
|
SceneType = "bearing_mounting",
|
|
Target = bearing.Id,
|
|
Confidence = 0.75,
|
|
Evidence = ["bearing_component"]
|
|
};
|
|
foreach (var item in evidence.Items.Where(i => i.Owner == bearing.Id && i.Type.StartsWith("bearing_", StringComparison.OrdinalIgnoreCase)))
|
|
scene.Evidence.Add(item.Type);
|
|
AddOrMergeScene(evidence, scene);
|
|
}
|
|
}
|
|
|
|
static void RecognizeGenericAxialLimit(ModelEvidence evidence)
|
|
{
|
|
foreach (var component in evidence.Components)
|
|
{
|
|
bool left = evidence.Items.Any(i => i.Owner == component.Id && i.Type == "left_axial_limit");
|
|
bool right = evidence.Items.Any(i => i.Owner == component.Id && i.Type == "right_axial_limit");
|
|
if (!left && !right)
|
|
continue;
|
|
|
|
AddOrMergeScene(evidence, new SceneCandidate
|
|
{
|
|
Id = $"axial_limited_component:{component.Id}",
|
|
SceneType = "axial_limited_component",
|
|
Target = component.Id,
|
|
Confidence = left && right ? 0.82 : 0.55,
|
|
Evidence = left && right ? ["left_axial_limit", "right_axial_limit"] : left ? ["left_axial_limit"] : ["right_axial_limit"]
|
|
});
|
|
}
|
|
}
|
|
|
|
static void AddOrMergeScene(ModelEvidence evidence, SceneCandidate scene)
|
|
{
|
|
var existing = evidence.Scenes.FirstOrDefault(s =>
|
|
s.SceneType.Equals(scene.SceneType, StringComparison.OrdinalIgnoreCase) &&
|
|
s.Target.Equals(scene.Target, StringComparison.OrdinalIgnoreCase) &&
|
|
s.Host.Equals(scene.Host, StringComparison.OrdinalIgnoreCase));
|
|
if (existing == null)
|
|
{
|
|
evidence.Scenes.Add(scene);
|
|
return;
|
|
}
|
|
|
|
existing.Confidence = Math.Max(existing.Confidence, scene.Confidence);
|
|
foreach (var item in scene.Evidence)
|
|
{
|
|
if (!existing.Evidence.Contains(item, StringComparer.OrdinalIgnoreCase))
|
|
existing.Evidence.Add(item);
|
|
}
|
|
}
|
|
|
|
static bool ContainsAny(string text, params string[] tokens)
|
|
{
|
|
return tokens.Any(t => text.Contains(t, StringComparison.OrdinalIgnoreCase));
|
|
}
|
|
}
|
|
|
|
static class SectionStructureSynthesizer
|
|
{
|
|
public static void Synthesize(ModelEvidence evidence)
|
|
{
|
|
foreach (var item in evidence.Items.ToList())
|
|
{
|
|
switch (item.Type)
|
|
{
|
|
case "close_fit_contact":
|
|
evidence.SectionInstances.AddRelation("contact", "sleeve_material", "host_material", item.Confidence, item.Source);
|
|
evidence.SectionInstances.AddRelation("axial_overlap", "sleeve_material", "host_material", 0.65, item.Source);
|
|
evidence.SectionInstances.AddRelation("coaxial", "sleeve_material", "host_material", 0.55, item.Source);
|
|
AddDerivedEvidence(evidence, "axial_overlap", "section_graph", "sleeve_material|host_material", 0.65, item.Source);
|
|
AddDerivedEvidence(evidence, "coaxial_cylinder_pair", "section_graph", "sleeve_material|host_material", 0.62, item.Source);
|
|
break;
|
|
case "axial_contact":
|
|
evidence.SectionInstances.AddRelation("adjacent_to", item.Owner, item.Related, item.Confidence, item.Source);
|
|
break;
|
|
case "left_axial_limit":
|
|
case "right_axial_limit":
|
|
evidence.SectionInstances.AddRelation("limits_axial_motion", item.Related, item.Owner, item.Confidence, item.Source);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
static void AddDerivedEvidence(ModelEvidence evidence, string type, string owner, string related, double confidence, string source)
|
|
{
|
|
if (evidence.Items.Any(i => i.Type == type && i.Owner == owner && i.Related == related))
|
|
return;
|
|
var rawId = $"{type}:{owner}:{related}:derived";
|
|
evidence.Items.Add(new EvidenceItem
|
|
{
|
|
Id = rawId[..Math.Min(64, rawId.Length)],
|
|
Type = type,
|
|
Owner = owner,
|
|
Related = related,
|
|
Confidence = confidence,
|
|
Source = source
|
|
});
|
|
}
|
|
}
|
|
|
|
static class DisassemblyAccessDefinitionFilter
|
|
{
|
|
static readonly string[] DirectPushFacts =
|
|
[
|
|
"outside_accessible_push_path",
|
|
"push_path_reaches_sleeve_push_area",
|
|
"push_direction_has_removal_component"
|
|
];
|
|
|
|
static readonly string[] GapFacts =
|
|
[
|
|
"outside_connected_void_gap",
|
|
"gap_path_reaches_sleeve_push_area",
|
|
"gap_width_sufficient",
|
|
"tool_force_direction_valid"
|
|
];
|
|
|
|
public static void Apply(ModelEvidence evidence)
|
|
{
|
|
// These are not priority rules. They remove evidence only when an extractor
|
|
// produced a fact outside that fact's own definition:
|
|
// - c is an open tool gap, not a cylindrical ejector/threaded port.
|
|
// - b is a directly accessible push face, not access mediated by a gap/port.
|
|
var hasCylindricalPortAccess = HasAll(evidence, [
|
|
"outside_connected_cylindrical_port",
|
|
"port_axis_intersects_sleeve_push_area",
|
|
"port_diameter_sufficient",
|
|
"port_push_direction_valid"
|
|
]);
|
|
var hasOpenToolGapAccess = HasAll(evidence, GapFacts);
|
|
|
|
if (hasCylindricalPortAccess)
|
|
{
|
|
RemoveFacts(evidence, GapFacts, sourcePrefix: "GapFeatureSynthesizer");
|
|
RemoveGapRelations(evidence);
|
|
hasOpenToolGapAccess = false;
|
|
}
|
|
|
|
if (hasCylindricalPortAccess || hasOpenToolGapAccess)
|
|
{
|
|
RemoveFacts(evidence, DirectPushFacts, sourcePrefix: "SketchGraph.Dimensions");
|
|
RemoveDirectPushRelations(evidence);
|
|
}
|
|
}
|
|
|
|
static bool HasAll(ModelEvidence evidence, IEnumerable<string> factTypes)
|
|
{
|
|
return factTypes.All(type => evidence.Has(type, 0.7));
|
|
}
|
|
|
|
static void RemoveFacts(ModelEvidence evidence, IEnumerable<string> factTypes, string sourcePrefix)
|
|
{
|
|
var set = factTypes.ToHashSet(StringComparer.OrdinalIgnoreCase);
|
|
evidence.Items.RemoveAll(i =>
|
|
set.Contains(i.Type) &&
|
|
i.Source.StartsWith(sourcePrefix, StringComparison.OrdinalIgnoreCase));
|
|
}
|
|
|
|
static void RemoveGapRelations(ModelEvidence evidence)
|
|
{
|
|
evidence.SectionInstances.Entities.RemoveAll(e =>
|
|
e.Id.Equals("gap_region:auto_tool_gap", StringComparison.OrdinalIgnoreCase) ||
|
|
e.SemanticRole.Equals("void_gap", StringComparison.OrdinalIgnoreCase));
|
|
evidence.SectionInstances.Relations.RemoveAll(r =>
|
|
r.Source.StartsWith("GapFeatureSynthesizer", StringComparison.OrdinalIgnoreCase) ||
|
|
r.A.Equals("gap_region", StringComparison.OrdinalIgnoreCase) ||
|
|
r.B.Equals("gap_region", StringComparison.OrdinalIgnoreCase));
|
|
}
|
|
|
|
static void RemoveDirectPushRelations(ModelEvidence evidence)
|
|
{
|
|
evidence.SectionInstances.Relations.RemoveAll(r =>
|
|
r.Source.Equals("SketchGraph.Dimensions", StringComparison.OrdinalIgnoreCase) &&
|
|
(r.A.Equals("push_access_path", StringComparison.OrdinalIgnoreCase) ||
|
|
r.B.Equals("push_access_path", StringComparison.OrdinalIgnoreCase)));
|
|
}
|
|
}
|
|
|
|
static class DiagnosticPipeline
|
|
{
|
|
public static DiagnosticRunResult Run(RuleTemplatePackage package, ModelEvidence evidence, string modelInput, List<string> evidencePaths, string rulePath)
|
|
{
|
|
var result = new DiagnosticRunResult
|
|
{
|
|
Schema = "model_diagnostic_run_v1",
|
|
ModelInput = modelInput,
|
|
EvidencePaths = evidencePaths.Select(Path.GetFullPath).ToList(),
|
|
RulePackagePath = Path.GetFullPath(rulePath),
|
|
ObservedEvidence = evidence,
|
|
PipelineSummary = BuildPipelineSummary(package, evidence)
|
|
};
|
|
|
|
foreach (var rule in package.SemanticRules)
|
|
result.RuleResults.Add(RunRule(rule, package.ImageStructureTemplates, evidence));
|
|
|
|
return result;
|
|
}
|
|
|
|
static PipelineSummary BuildPipelineSummary(RuleTemplatePackage package, ModelEvidence evidence)
|
|
{
|
|
return new PipelineSummary
|
|
{
|
|
RuleCount = package.SemanticRules.Count,
|
|
ComponentCount = evidence.Components.Count,
|
|
EvidenceItemCount = evidence.Items.Count,
|
|
SceneCandidateCount = evidence.Scenes.Count,
|
|
SectionRelationCount = evidence.SectionInstances.Relations.Count,
|
|
SceneTypes = evidence.Scenes.Select(s => s.SceneType).Distinct(StringComparer.OrdinalIgnoreCase).OrderBy(s => s).ToList(),
|
|
EvidenceTypes = evidence.Items.Select(i => i.Type).Distinct(StringComparer.OrdinalIgnoreCase).OrderBy(s => s).ToList()
|
|
};
|
|
}
|
|
|
|
static RuleDiagnosticResult RunRule(RuleSemanticTemplate rule, List<ImageStructureTemplate2D> templates, ModelEvidence evidence)
|
|
{
|
|
var scenes = evidence.Scenes
|
|
.Where(s => s.SceneType.Equals(rule.TriggerCondition.SceneType, StringComparison.OrdinalIgnoreCase))
|
|
.OrderByDescending(s => s.Confidence)
|
|
.ToList();
|
|
|
|
if (scenes.Count == 0)
|
|
{
|
|
return new RuleDiagnosticResult
|
|
{
|
|
RuleId = rule.RuleId,
|
|
Decision = rule.DecisionLogic.IfTriggerMissing,
|
|
ProblemType = null,
|
|
Reason = "No scene candidate matched the rule trigger scene type.",
|
|
Applicability = new ApplicabilityResult { Status = "not_applicable", MissingEvidence = rule.TriggerCondition.RequiredEvidence }
|
|
};
|
|
}
|
|
|
|
var bestScene = scenes[0];
|
|
var applicability = EvaluateApplicability(rule, bestScene, evidence);
|
|
if (applicability.Status != "applicable")
|
|
{
|
|
return new RuleDiagnosticResult
|
|
{
|
|
RuleId = rule.RuleId,
|
|
Decision = rule.DecisionLogic.IfTriggerUnknown,
|
|
ProblemType = null,
|
|
Reason = "Scene candidate exists, but trigger evidence is incomplete or weak.",
|
|
Scene = bestScene,
|
|
Applicability = applicability
|
|
};
|
|
}
|
|
|
|
var matches = MatchPassEvidence(rule, templates, evidence);
|
|
var foundPass = matches.Any(m => m.Status == "found");
|
|
var decision = foundPass ? rule.DecisionLogic.IfTriggeredAndAnyPassEvidence : rule.DecisionLogic.IfTriggeredAndNoPassEvidence;
|
|
|
|
return new RuleDiagnosticResult
|
|
{
|
|
RuleId = rule.RuleId,
|
|
Decision = decision,
|
|
ProblemType = decision == "problem" ? rule.Problem.ProblemType : null,
|
|
Message = decision == "problem" ? rule.Problem.Message : "未发现该项设计问题。",
|
|
Reason = foundPass
|
|
? "Rule applies and at least one pass evidence template matched the model structure/evidence."
|
|
: "Rule applies, but no pass evidence template matched the model structure/evidence.",
|
|
Scene = bestScene,
|
|
Applicability = applicability,
|
|
PassEvidenceMatches = matches
|
|
};
|
|
}
|
|
|
|
static ApplicabilityResult EvaluateApplicability(RuleSemanticTemplate rule, SceneCandidate scene, ModelEvidence evidence)
|
|
{
|
|
var found = new List<string>();
|
|
var missing = new List<string>();
|
|
|
|
foreach (var required in rule.TriggerCondition.RequiredEvidence)
|
|
{
|
|
if (scene.Evidence.Contains(required, StringComparer.OrdinalIgnoreCase) || evidence.Has(required, 0.65) || evidence.SectionInstances.HasRelation(required))
|
|
found.Add(required);
|
|
else
|
|
missing.Add(required);
|
|
}
|
|
|
|
var enough = missing.Count == 0 ||
|
|
(found.Contains("sleeve_component") && found.Contains("host_hole_component") && found.Contains("close_fit_contact"));
|
|
|
|
return new ApplicabilityResult
|
|
{
|
|
Status = enough ? "applicable" : found.Count == 0 ? "not_applicable" : "candidate",
|
|
Confidence = enough ? Math.Max(scene.Confidence, 0.75) : scene.Confidence,
|
|
FoundEvidence = found,
|
|
MissingEvidence = missing
|
|
};
|
|
}
|
|
|
|
static List<PassEvidenceMatch> MatchPassEvidence(RuleSemanticTemplate rule, List<ImageStructureTemplate2D> templates, ModelEvidence evidence)
|
|
{
|
|
var matches = new List<PassEvidenceMatch>();
|
|
foreach (var pass in rule.PassEvidenceTypes)
|
|
{
|
|
var template = templates.FirstOrDefault(t => t.TemplateId.Equals(pass.Required2DTemplateId, StringComparison.OrdinalIgnoreCase));
|
|
var modelEvidenceFound = pass.RequiredModelEvidence.Where(req => evidence.Has(req, 0.7) || evidence.SectionInstances.HasRelation(req)).ToList();
|
|
var modelEvidenceMissing = pass.RequiredModelEvidence
|
|
.Where(req => !modelEvidenceFound.Contains(req, StringComparer.OrdinalIgnoreCase))
|
|
.ToList();
|
|
var structure = template != null
|
|
? StructureTemplateMatcher.Match(template, evidence.SectionInstances)
|
|
: new StructureMatch { Status = "missing_template" };
|
|
|
|
bool found = modelEvidenceFound.Count == pass.RequiredModelEvidence.Count && structure.Status == "found";
|
|
bool candidate = modelEvidenceFound.Count > 0 || structure.Status == "candidate";
|
|
matches.Add(new PassEvidenceMatch
|
|
{
|
|
EvidenceType = pass.EvidenceType,
|
|
SourceExample = pass.SourceExample,
|
|
Anchor = pass.Anchor,
|
|
TemplateId = pass.Required2DTemplateId,
|
|
Status = found ? "found" : candidate ? "candidate" : "missing",
|
|
MatchedModelEvidence = modelEvidenceFound,
|
|
MissingModelEvidence = modelEvidenceMissing,
|
|
MissingModelEvidenceDiagnostics = modelEvidenceMissing
|
|
.Select(BrepFactCapabilityCatalog.ExplainMissing)
|
|
.ToList(),
|
|
StructureMatch = structure
|
|
});
|
|
}
|
|
return matches;
|
|
}
|
|
}
|
|
|
|
static class StructureTemplateMatcher
|
|
{
|
|
public static StructureMatch Match(ImageStructureTemplate2D template, StructureInstance2D instance)
|
|
{
|
|
var found = new List<string>();
|
|
var missing = new List<string>();
|
|
|
|
foreach (var relation in template.RequiredRelations)
|
|
{
|
|
if (relation.Type == "missing")
|
|
continue;
|
|
|
|
var relationFound = string.IsNullOrWhiteSpace(relation.A) && string.IsNullOrWhiteSpace(relation.B)
|
|
? instance.HasRelation(relation.Type)
|
|
: instance.HasRelation(relation.Type, relation.A, relation.B);
|
|
|
|
if (relationFound)
|
|
found.Add($"{relation.Type}({relation.A},{relation.B})");
|
|
else
|
|
missing.Add($"{relation.Type}({relation.A},{relation.B})");
|
|
}
|
|
|
|
return new StructureMatch
|
|
{
|
|
TemplateId = template.TemplateId,
|
|
Status = missing.Count == 0 ? "found" : found.Count > 0 ? "candidate" : "missing",
|
|
FoundRelations = found,
|
|
MissingRelations = missing
|
|
};
|
|
}
|
|
}
|
|
|
|
static class MarkdownReport
|
|
{
|
|
public static string Write(DiagnosticRunResult run)
|
|
{
|
|
var sb = new StringBuilder();
|
|
sb.AppendLine("# Model Diagnostic Verifier");
|
|
sb.AppendLine();
|
|
sb.AppendLine($"- Model input: `{run.ModelInput}`");
|
|
sb.AppendLine($"- Evidence: `{string.Join("`, `", run.EvidencePaths)}`");
|
|
sb.AppendLine($"- Rule package: `{run.RulePackagePath}`");
|
|
sb.AppendLine();
|
|
sb.AppendLine("## Decisions");
|
|
foreach (var rule in run.RuleResults)
|
|
{
|
|
sb.AppendLine($"- `{rule.RuleId}` decision=`{rule.Decision}`, problemType=`{rule.ProblemType ?? ""}`");
|
|
sb.AppendLine($" - Reason: {rule.Reason}");
|
|
sb.AppendLine($" - Applicability: `{rule.Applicability.Status}`, found=[{string.Join(", ", rule.Applicability.FoundEvidence)}], missing=[{string.Join(", ", rule.Applicability.MissingEvidence)}]");
|
|
foreach (var match in rule.PassEvidenceMatches)
|
|
{
|
|
sb.AppendLine($" - Pass evidence `{match.EvidenceType}` example=`{match.SourceExample}` template=`{match.TemplateId}` status=`{match.Status}`");
|
|
sb.AppendLine($" - model found=[{string.Join(", ", match.MatchedModelEvidence)}], model missing=[{string.Join(", ", match.MissingModelEvidence)}]");
|
|
foreach (var diagnostic in match.MissingModelEvidenceDiagnostics)
|
|
sb.AppendLine($" - missing `{diagnostic.Fact}`: status=`{diagnostic.ExtractorStatus}`, source=`{diagnostic.SourceLayer}`, reason={diagnostic.MissingWhenAbsent}");
|
|
sb.AppendLine($" - structure found=[{string.Join(", ", match.StructureMatch.FoundRelations)}], structure missing=[{string.Join(", ", match.StructureMatch.MissingRelations)}]");
|
|
}
|
|
}
|
|
sb.AppendLine();
|
|
sb.AppendLine("## Scenes");
|
|
foreach (var scene in run.ObservedEvidence.Scenes)
|
|
sb.AppendLine($"- `{scene.SceneType}` target=`{scene.Target}`, host=`{scene.Host}`, confidence={scene.Confidence:0.00}, evidence={string.Join(", ", scene.Evidence)}");
|
|
sb.AppendLine();
|
|
sb.AppendLine("## Evidence Summary");
|
|
foreach (var item in run.ObservedEvidence.Items.Take(80))
|
|
sb.AppendLine($"- `{item.Type}` owner=`{item.Owner}`, related=`{item.Related}`, confidence={item.Confidence:0.00}, source={item.Source}");
|
|
return sb.ToString();
|
|
}
|
|
}
|
|
|
|
sealed class Args
|
|
{
|
|
readonly Dictionary<string, string> _values;
|
|
|
|
Args(Dictionary<string, string> values)
|
|
{
|
|
_values = values;
|
|
}
|
|
|
|
public static Args Parse(string[] args)
|
|
{
|
|
var values = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
|
for (int i = 0; i < args.Length; i++)
|
|
{
|
|
var key = args[i];
|
|
if (!key.StartsWith("--", StringComparison.Ordinal))
|
|
throw new ArgumentException($"Unexpected argument: {key}");
|
|
if (i + 1 >= args.Length)
|
|
throw new ArgumentException($"Missing value for {key}");
|
|
values[key] = args[++i];
|
|
}
|
|
return new Args(values);
|
|
}
|
|
|
|
public string Required(string key)
|
|
{
|
|
if (!_values.TryGetValue(key, out var value) || string.IsNullOrWhiteSpace(value))
|
|
throw new ArgumentException($"{key} is required.");
|
|
return value;
|
|
}
|
|
|
|
public string? Optional(string key)
|
|
{
|
|
return _values.TryGetValue(key, out var value) ? value : null;
|
|
}
|
|
}
|
|
|
|
sealed class DiagnosticRunResult
|
|
{
|
|
public string Schema { get; set; } = "model_diagnostic_run_v1";
|
|
public string ModelInput { get; set; } = "";
|
|
public List<string> EvidencePaths { get; set; } = [];
|
|
public string RulePackagePath { get; set; } = "";
|
|
public PipelineSummary PipelineSummary { get; set; } = new();
|
|
public ModelEvidence ObservedEvidence { get; set; } = new();
|
|
public List<RuleDiagnosticResult> RuleResults { get; set; } = [];
|
|
}
|
|
|
|
sealed class PipelineSummary
|
|
{
|
|
public int RuleCount { get; set; }
|
|
public int ComponentCount { get; set; }
|
|
public int EvidenceItemCount { get; set; }
|
|
public int SceneCandidateCount { get; set; }
|
|
public int SectionRelationCount { get; set; }
|
|
public List<string> SceneTypes { get; set; } = [];
|
|
public List<string> EvidenceTypes { get; set; } = [];
|
|
}
|
|
|
|
sealed class RuleDiagnosticResult
|
|
{
|
|
public string RuleId { get; set; } = "";
|
|
public string Decision { get; set; } = "";
|
|
public string? ProblemType { get; set; }
|
|
public string Message { get; set; } = "";
|
|
public string Reason { get; set; } = "";
|
|
public SceneCandidate? Scene { get; set; }
|
|
public ApplicabilityResult Applicability { get; set; } = new();
|
|
public List<PassEvidenceMatch> PassEvidenceMatches { get; set; } = [];
|
|
}
|
|
|
|
sealed class ApplicabilityResult
|
|
{
|
|
public string Status { get; set; } = "";
|
|
public double Confidence { get; set; }
|
|
public List<string> FoundEvidence { get; set; } = [];
|
|
public List<string> MissingEvidence { get; set; } = [];
|
|
}
|
|
|
|
sealed class PassEvidenceMatch
|
|
{
|
|
public string EvidenceType { get; set; } = "";
|
|
public string SourceExample { get; set; } = "";
|
|
public string Anchor { get; set; } = "";
|
|
public string TemplateId { get; set; } = "";
|
|
public string Status { get; set; } = "";
|
|
public List<string> MatchedModelEvidence { get; set; } = [];
|
|
public List<string> MissingModelEvidence { get; set; } = [];
|
|
public List<BrepFactCapability> MissingModelEvidenceDiagnostics { get; set; } = [];
|
|
public StructureMatch StructureMatch { get; set; } = new();
|
|
}
|
|
|
|
static class BrepFactCapabilityCatalog
|
|
{
|
|
static readonly List<BrepFactCapability> Items =
|
|
[
|
|
Fact("sleeve_component", "3d_brep+component", "implemented", "SceneRecognizer.RecognizeSleeveInHole", "component category/name plus cylindrical section/contact evidence", "No sleeve-like component candidate was recognized."),
|
|
Fact("sleeve_like_component", "3d_brep+component", "implemented", "SceneRecognizer.RecognizeSleeveInHole", "component category/name plus cylindrical section/contact evidence", "No sleeve-like component candidate was recognized."),
|
|
Fact("host_hole_component", "3d_brep+component", "implemented", "SceneRecognizer.RecognizeSleeveInHole", "host/housing component and coaxial/contact evidence", "No host or hole component candidate was recognized."),
|
|
Fact("coaxial_cylinder_pair", "2d_section_brep", "implemented", "SectionStructureSynthesizer/SketchGraph.Relations", "coaxial sleeve/host section geometry", "No coaxial sleeve-host pair was detected."),
|
|
Fact("axial_overlap", "2d_section_brep", "implemented", "SectionStructureSynthesizer/SketchGraph.Relations", "overlapping axial ranges in section", "No axial overlap between sleeve and host was detected."),
|
|
Fact("close_fit_contact", "2d_section_brep", "implemented", "SketchGraph.Relations contact", "contact or near-contact section edges", "No close-fit contact relation was detected."),
|
|
Fact("close_fit_contact_or_small_clearance", "2d_section_brep", "implemented", "SketchGraph.Relations contact/clearance", "contact or small clearance section edges", "No close-fit contact or small clearance relation was detected."),
|
|
Fact("outside_accessible_push_path", "2d_section_brep", "implemented", "ReadSketchDimension exposed_push_face_candidate", "exposed sleeve end edge and outside clearance", "No exposed sleeve push face was detected."),
|
|
Fact("push_path_reaches_sleeve_push_area", "2d_section_brep", "implemented", "ReadSketchDimension exposed_push_face_candidate", "exposed sleeve end edge", "No push path reaches a sleeve push area."),
|
|
Fact("push_direction_has_removal_component", "2d_section_brep", "implemented", "ReadSketchDimension exposed_push_face_candidate", "push face orientation against sleeve axis", "No push direction with removal-axis component was detected."),
|
|
Fact("outside_connected_void_gap", "2d_section_brep", "partial", "GapFeatureSynthesizer.SectionFloodFill", "outside-connected free-space cells between sleeve and host", "No outside-connected sleeve/host gap candidate was detected."),
|
|
Fact("gap_path_reaches_sleeve_push_area", "2d_section_brep", "partial", "GapFeatureSynthesizer.SectionFloodFill", "gap cluster adjacent to sleeve push area", "No detected gap cluster reached the sleeve push area."),
|
|
Fact("gap_width_sufficient", "2d_section_brep", "partial", "GapFeatureSynthesizer.SectionFloodFill", "estimated minimum sleeve-host free-space width", "No detected gap satisfied the minimum tool-width threshold."),
|
|
Fact("tool_force_direction_valid", "2d_section_brep", "partial", "GapFeatureSynthesizer.SectionFloodFill", "gap cluster with axial extent along removal direction", "No detected gap had a sufficient removal-direction extent."),
|
|
Fact("outside_connected_cylindrical_port", "3d_brep", "partial", "PortFeatureSynthesizer.CylinderFaces", "small host/end-cap cylindrical face treated as port candidate", "No small host/end-cap cylindrical port candidate was detected."),
|
|
Fact("port_axis_intersects_sleeve_push_area", "3d_brep", "partial", "PortFeatureSynthesizer.CylinderFaces", "port axis close enough to sleeve push area", "No port axis was found to reach the sleeve push area."),
|
|
Fact("port_diameter_sufficient", "3d_brep", "partial", "PortFeatureSynthesizer.CylinderFaces", "cylindrical port radius/diameter", "No detected port diameter satisfied the minimum threshold."),
|
|
Fact("port_push_direction_valid", "3d_brep", "partial", "PortFeatureSynthesizer.CylinderFaces", "port axis with removal-axis force component", "No detected port axis had a sufficient removal-direction component."),
|
|
Fact("thread_or_screw_feature", "feature_metadata_or_modeled_thread", "optional_only", "optional SolidWorks feature/thread metadata or modeled helical B-rep", "thread feature metadata or real thread geometry", "Pure B-rep often does not contain reliable thread semantics; keep this optional.")
|
|
];
|
|
|
|
public static BrepFactCapability ExplainMissing(string fact)
|
|
{
|
|
var normalized = NormalizeToken(fact);
|
|
var capability = Items.FirstOrDefault(i => i.Fact.Equals(normalized, StringComparison.OrdinalIgnoreCase));
|
|
return capability?.Clone() ?? new BrepFactCapability
|
|
{
|
|
Fact = normalized,
|
|
SourceLayer = "unknown",
|
|
ExtractorStatus = "unregistered",
|
|
Extractor = "",
|
|
RequiredPrimitive = "",
|
|
MissingWhenAbsent = "No B-rep fact capability is registered for this evidence name.",
|
|
AuthoringAllowed = false
|
|
};
|
|
}
|
|
|
|
static BrepFactCapability Fact(string fact, string source, string status, string extractor, string primitive, string missingWhenAbsent)
|
|
{
|
|
return new BrepFactCapability
|
|
{
|
|
Fact = fact,
|
|
SourceLayer = source,
|
|
ExtractorStatus = status,
|
|
Extractor = extractor,
|
|
RequiredPrimitive = primitive,
|
|
MissingWhenAbsent = missingWhenAbsent,
|
|
AuthoringAllowed = status is "implemented" or "partial" or "planned"
|
|
};
|
|
}
|
|
|
|
static string NormalizeToken(string value) => value.Trim().Replace(' ', '_').ToLowerInvariant();
|
|
}
|
|
|
|
sealed class BrepFactCapability
|
|
{
|
|
public string Fact { get; set; } = "";
|
|
public string SourceLayer { get; set; } = "";
|
|
public string ExtractorStatus { get; set; } = "";
|
|
public string Extractor { get; set; } = "";
|
|
public string RequiredPrimitive { get; set; } = "";
|
|
public string MissingWhenAbsent { get; set; } = "";
|
|
public bool AuthoringAllowed { get; set; }
|
|
|
|
public BrepFactCapability Clone()
|
|
{
|
|
return new BrepFactCapability
|
|
{
|
|
Fact = Fact,
|
|
SourceLayer = SourceLayer,
|
|
ExtractorStatus = ExtractorStatus,
|
|
Extractor = Extractor,
|
|
RequiredPrimitive = RequiredPrimitive,
|
|
MissingWhenAbsent = MissingWhenAbsent,
|
|
AuthoringAllowed = AuthoringAllowed
|
|
};
|
|
}
|
|
}
|
|
|
|
sealed class StructureMatch
|
|
{
|
|
public string TemplateId { get; set; } = "";
|
|
public string Status { get; set; } = "";
|
|
public List<string> FoundRelations { get; set; } = [];
|
|
public List<string> MissingRelations { get; set; } = [];
|
|
}
|
|
|
|
sealed class ModelEvidence
|
|
{
|
|
public string Schema { get; set; } = "model_evidence_v1";
|
|
public List<string> SourcePaths { get; set; } = [];
|
|
public string ModelPath { get; set; } = "";
|
|
public List<ComponentEvidence> Components { get; set; } = [];
|
|
public List<EvidenceItem> Items { get; set; } = [];
|
|
public List<SceneCandidate> Scenes { get; set; } = [];
|
|
public StructureInstance2D SectionInstances { get; set; } = new();
|
|
|
|
public bool Has(string type, double minConfidence)
|
|
{
|
|
var normalized = NormalizeToken(type);
|
|
return Items.Any(i => i.Type.Equals(normalized, StringComparison.OrdinalIgnoreCase) && i.Confidence >= minConfidence);
|
|
}
|
|
|
|
static string NormalizeToken(string value) => value.Trim().Replace(' ', '_').ToLowerInvariant();
|
|
}
|
|
|
|
sealed class ComponentEvidence
|
|
{
|
|
public string Id { get; set; } = "";
|
|
public string Name { get; set; } = "";
|
|
public string PartName { get; set; } = "";
|
|
public string Category { get; set; } = "";
|
|
public string Path { 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; }
|
|
}
|
|
|
|
sealed class EvidenceItem
|
|
{
|
|
public string Id { get; set; } = "";
|
|
public string Type { get; set; } = "";
|
|
public string Owner { get; set; } = "";
|
|
public string Related { get; set; } = "";
|
|
public double Confidence { get; set; }
|
|
public string Source { get; set; } = "";
|
|
public Dictionary<string, string> Attributes { get; set; } = [];
|
|
}
|
|
|
|
sealed class SceneCandidate
|
|
{
|
|
public string Id { get; set; } = "";
|
|
public string SceneType { get; set; } = "";
|
|
public string Target { get; set; } = "";
|
|
public string Host { get; set; } = "";
|
|
public double Confidence { get; set; }
|
|
public List<string> Evidence { get; set; } = [];
|
|
}
|
|
|
|
sealed class StructureInstance2D
|
|
{
|
|
public string Schema { get; set; } = "structure_instance_2d_v1";
|
|
public List<StructureEntity> Entities { get; set; } = [];
|
|
public List<StructureRelation> Relations { get; set; } = [];
|
|
|
|
public void AddRelation(string type, string a, string b, double confidence, string source)
|
|
{
|
|
var normalized = NormalizeToken(type);
|
|
var existing = Relations.FirstOrDefault(r =>
|
|
r.Type.Equals(normalized, StringComparison.OrdinalIgnoreCase) &&
|
|
r.A.Equals(a, StringComparison.OrdinalIgnoreCase) &&
|
|
r.B.Equals(b, StringComparison.OrdinalIgnoreCase));
|
|
if (existing != null)
|
|
{
|
|
existing.Confidence = Math.Max(existing.Confidence, confidence);
|
|
return;
|
|
}
|
|
|
|
Relations.Add(new StructureRelation
|
|
{
|
|
Type = normalized,
|
|
A = a,
|
|
B = b,
|
|
Confidence = confidence,
|
|
Source = source
|
|
});
|
|
}
|
|
|
|
public bool HasRelation(string type)
|
|
{
|
|
var normalized = NormalizeToken(type);
|
|
return Relations.Any(r => r.Type.Equals(normalized, StringComparison.OrdinalIgnoreCase));
|
|
}
|
|
|
|
public bool HasRelation(string type, string a, string b)
|
|
{
|
|
var normalized = NormalizeToken(type);
|
|
return Relations.Any(r =>
|
|
r.Type.Equals(normalized, StringComparison.OrdinalIgnoreCase) &&
|
|
(string.IsNullOrWhiteSpace(a) || r.A.Equals(a, StringComparison.OrdinalIgnoreCase) || r.B.Equals(a, StringComparison.OrdinalIgnoreCase)) &&
|
|
(string.IsNullOrWhiteSpace(b) || r.A.Equals(b, StringComparison.OrdinalIgnoreCase) || r.B.Equals(b, StringComparison.OrdinalIgnoreCase)));
|
|
}
|
|
|
|
static string NormalizeToken(string value) => value.Trim().Replace(' ', '_').ToLowerInvariant();
|
|
}
|
|
|
|
sealed class StructureEntity
|
|
{
|
|
public string Id { get; set; } = "";
|
|
public string Kind { get; set; } = "";
|
|
public string SemanticRole { get; set; } = "";
|
|
}
|
|
|
|
sealed class StructureRelation
|
|
{
|
|
public string Type { get; set; } = "";
|
|
public string A { get; set; } = "";
|
|
public string B { get; set; } = "";
|
|
public double Confidence { get; set; }
|
|
public string Source { get; set; } = "";
|
|
}
|
|
|
|
sealed class RuleTemplatePackage
|
|
{
|
|
public string Schema { get; set; } = "";
|
|
public string PackageId { get; set; } = "";
|
|
public List<RuleSemanticTemplate> SemanticRules { get; set; } = [];
|
|
public List<ImageStructureTemplate2D> ImageStructureTemplates { get; set; } = [];
|
|
}
|
|
|
|
sealed class RuleSemanticTemplate
|
|
{
|
|
public string RuleId { get; set; } = "";
|
|
public RuleTriggerCondition TriggerCondition { get; set; } = new();
|
|
public ProblemTemplate Problem { get; set; } = new();
|
|
public List<PassEvidenceTemplate> PassEvidenceTypes { get; set; } = [];
|
|
public RuleDecisionLogic DecisionLogic { get; set; } = new();
|
|
}
|
|
|
|
sealed class RuleTriggerCondition
|
|
{
|
|
public string SceneType { get; set; } = "";
|
|
public List<string> RequiredEvidence { get; set; } = [];
|
|
}
|
|
|
|
sealed class ProblemTemplate
|
|
{
|
|
public string ProblemType { get; set; } = "";
|
|
public string Message { get; set; } = "";
|
|
}
|
|
|
|
sealed class PassEvidenceTemplate
|
|
{
|
|
public string EvidenceType { get; set; } = "";
|
|
public string SourceExample { get; set; } = "";
|
|
public string Anchor { get; set; } = "";
|
|
public string Required2DTemplateId { get; set; } = "";
|
|
public List<string> RequiredModelEvidence { get; set; } = [];
|
|
public List<string> OptionalModelEvidence { get; set; } = [];
|
|
}
|
|
|
|
sealed class RuleDecisionLogic
|
|
{
|
|
public string IfTriggerMissing { get; set; } = "not_applicable";
|
|
public string IfTriggerUnknown { get; set; } = "needs_review";
|
|
public string IfTriggeredAndAnyPassEvidence { get; set; } = "pass";
|
|
public string IfTriggeredAndNoPassEvidence { get; set; } = "problem";
|
|
}
|
|
|
|
sealed class ImageStructureTemplate2D
|
|
{
|
|
public string TemplateId { get; set; } = "";
|
|
public string Role { get; set; } = "";
|
|
public List<TemplateRelation> RequiredRelations { get; set; } = [];
|
|
}
|
|
|
|
sealed class TemplateRelation
|
|
{
|
|
public string Type { get; set; } = "";
|
|
public string A { get; set; } = "";
|
|
public string B { get; set; } = "";
|
|
}
|
|
|
|
static class JsonOptions
|
|
{
|
|
public static readonly JsonSerializerOptions Default = new()
|
|
{
|
|
PropertyNameCaseInsensitive = true,
|
|
WriteIndented = true,
|
|
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
|
|
NumberHandling = JsonNumberHandling.AllowNamedFloatingPointLiterals
|
|
};
|
|
}
|