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(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 --rules [--out ] 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 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 RunModelEvidenceExtractors(string root, string modelPath) { var evidencePaths = new List(); 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 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 BuildSketchEdgeIndex(JsonElement sketch) { var index = new Dictionary(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 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 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 SourceEdgeIds { get; set; } = []; } static class GapFeatureSynthesizer { public static void Synthesize(JsonElement sketch, List 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(); 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 { ["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 BuildMaterialPolygons(JsonElement sketch, List edges) { var edgeMap = edges.ToDictionary(e => e.Id, StringComparer.OrdinalIgnoreCase); var result = new List(); 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(); 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 BuildContactPairs(JsonElement sketch) { var result = new HashSet(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 ChainLoopPoints(List loopEdges) { var points = new List(); 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> BuildClusters(List 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>(); 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(); var q = new Queue(); 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? 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(attributes, StringComparer.OrdinalIgnoreCase) }); } static SectionBounds Bounds(List 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 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 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 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 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 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 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 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 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 factTypes) { return factTypes.All(type => evidence.Has(type, 0.7)); } static void RemoveFacts(ModelEvidence evidence, IEnumerable 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 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 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(); var missing = new List(); 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 MatchPassEvidence(RuleSemanticTemplate rule, List templates, ModelEvidence evidence) { var matches = new List(); 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(); var missing = new List(); 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 _values; Args(Dictionary values) { _values = values; } public static Args Parse(string[] args) { var values = new Dictionary(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 EvidencePaths { get; set; } = []; public string RulePackagePath { get; set; } = ""; public PipelineSummary PipelineSummary { get; set; } = new(); public ModelEvidence ObservedEvidence { get; set; } = new(); public List 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 SceneTypes { get; set; } = []; public List 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 PassEvidenceMatches { get; set; } = []; } sealed class ApplicabilityResult { public string Status { get; set; } = ""; public double Confidence { get; set; } public List FoundEvidence { get; set; } = []; public List 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 MatchedModelEvidence { get; set; } = []; public List MissingModelEvidence { get; set; } = []; public List MissingModelEvidenceDiagnostics { get; set; } = []; public StructureMatch StructureMatch { get; set; } = new(); } static class BrepFactCapabilityCatalog { static readonly List 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 FoundRelations { get; set; } = []; public List MissingRelations { get; set; } = []; } sealed class ModelEvidence { public string Schema { get; set; } = "model_evidence_v1"; public List SourcePaths { get; set; } = []; public string ModelPath { get; set; } = ""; public List Components { get; set; } = []; public List Items { get; set; } = []; public List 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 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 Evidence { get; set; } = []; } sealed class StructureInstance2D { public string Schema { get; set; } = "structure_instance_2d_v1"; public List Entities { get; set; } = []; public List 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 SemanticRules { get; set; } = []; public List 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 PassEvidenceTypes { get; set; } = []; public RuleDecisionLogic DecisionLogic { get; set; } = new(); } sealed class RuleTriggerCondition { public string SceneType { get; set; } = ""; public List 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 RequiredModelEvidence { get; set; } = []; public List 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 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 }; }