using SolidWorks.Interop.sldworks; using SolidWorks.Interop.swconst; using System.Diagnostics; using System.Globalization; using System.Runtime.InteropServices; using System.Text; using System.Text.Json; namespace AssemblyKnowledgeAudit; internal class Program { const int Asm = 2, Silent = 1, ReadOnly = 2; const string StandardPartSourceRoot = "D:\\Desktop\\\u51cf\u901f\u5668\\\u4e09\u7ef4"; [DllImport("ole32.dll", CharSet = CharSet.Unicode)] static extern int CLSIDFromProgID(string progId, out Guid clsid); [DllImport("oleaut32.dll", PreserveSig = false)] [return: MarshalAs(UnmanagedType.IUnknown)] static extern object GetActiveObject(ref Guid clsid, IntPtr reserved); static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; static string DiagnosticLogPath = ""; static readonly Dictionary>> ComponentCylinderSignatureCache = new(StringComparer.OrdinalIgnoreCase); [STAThread] static void Main(string[] args) { try { string asmPath = args.Length > 0 ? args[0] : @"D:\Desktop\鍑忛€熷櫒\涓夌淮\瑁呴厤浣?.SLDASM"; if (!File.Exists(asmPath) || !string.Equals(Path.GetExtension(asmPath), ".SLDASM", StringComparison.OrdinalIgnoreCase)) throw new Exception("Please input a valid .SLDASM assembly path."); Console.WriteLine("姝e湪杩炴帴 SolidWorks..."); var sw = Connect() ?? throw new Exception("鏃犳硶杩炴帴 SolidWorks"); string fileBase = MakeSafeFileName(Path.GetFileNameWithoutExtension(asmPath)); string outPath = Path.Combine(Path.GetDirectoryName(asmPath) ?? System.Environment.CurrentDirectory, $"{fileBase}_knowledge_skillflow.json"); var report = BuildAssemblyKnowledge(sw, Path.GetFullPath(asmPath)); File.WriteAllText(outPath, JsonSerializer.Serialize(report, JsonOptions)); Console.WriteLine("瀹屾垚锛佺粺涓€鐭ヨ瘑 JSON 宸插鍑猴細" + outPath); } catch (Exception ex) { Console.WriteLine("Program error: " + ex.Message); Console.WriteLine(ex); } Console.WriteLine("Press Enter to exit..."); if (System.Environment.UserInteractive && args.Length == 0) Console.ReadLine(); } static SldWorks Connect() { try { int hr = CLSIDFromProgID("SldWorks.Application", out var clsid); if (hr < 0) Marshal.ThrowExceptionForHR(hr); return (SldWorks)GetActiveObject(ref clsid, IntPtr.Zero); } catch { try { var t = Type.GetTypeFromProgID("SldWorks.Application"); if (t == null) return null; var sw = (SldWorks)Activator.CreateInstance(t); if (sw != null) sw.Visible = true; return sw; } catch { return null; } } } static object BuildAssemblyKnowledge(SldWorks sw, string asmPath) { InitDiagnosticLog(asmPath); Log("[assembly] BuildAssemblyKnowledge started"); string extractionArtifactDir = Path.GetDirectoryName(asmPath) ?? System.Environment.CurrentDirectory; int errors = 0, warnings = 0; var doc = sw.OpenDoc6(asmPath, Asm, Silent | ReadOnly, "", ref errors, ref warnings) as ModelDoc2; if (doc is not AssemblyDoc assy) throw new Exception($"OpenDoc6 澶辫触 errors={errors}, warnings={warnings}, path={asmPath}"); string assemblyName = Safe(() => doc.GetTitle(), Path.GetFileName(asmPath)); Console.WriteLine("Extracting assembly knowledge: " + assemblyName); Log("[assembly] reading components..."); var components = ReadComponents(assy, assemblyName); var componentIdMap = NormalizeComponentInstanceIds(components); Log("[assembly] extracting assembly component patterns..."); var componentPatterns = ReadAssemblyComponentPatterns(doc, componentIdMap); AnnotatePatternGeneratedComponents(components, componentPatterns); Log($"[assembly] components={components.Count}"); var uniqueParts = components .Where(c => string.Equals(c.DocumentType, "part", StringComparison.OrdinalIgnoreCase)) .GroupBy(c => c.FilePath, StringComparer.OrdinalIgnoreCase) .Select(g => new { FilePath = g.Key, PartName = Path.GetFileNameWithoutExtension(g.Key), InstanceCount = g.Count(), Instances = g.Select(x => x.ComponentName).ToList(), OriginalInstances = g.Select(x => x.OriginalComponentName).Where(x => !string.IsNullOrWhiteSpace(x)).ToList(), IsStandardPart = g.Any(x => x.IsStandardPart) || IsGbStandardPart(Path.GetFileNameWithoutExtension(g.Key), g.Key), StandardRule = "name_contains_GB" }) .OrderBy(x => x.PartName) .ToList(); Log($"[assembly] unique_parts={uniqueParts.Count}, gb_standard_parts={uniqueParts.Count(x => x.IsStandardPart)}"); var parts = new List(); foreach (var part in uniqueParts) { if (part.IsStandardPart) { Log($"[assembly] GB standard part: skip modeling and insert directly: {part.PartName}"); string standardPartPath = ResolveStandardPartSourcePath(part.PartName, part.FilePath, part.Instances); parts.Add(CreateStandardPartKnowledge(part.FilePath, standardPartPath, part.PartName, part.InstanceCount, part.Instances, part.OriginalInstances, part.StandardRule)); continue; } Console.WriteLine($" -> extracting/reusing part skill flow: {part.PartName}"); Log($"[assembly] extracting/reusing part skill flow: {part.PartName}"); parts.Add(ExtractPartKnowledge(part.FilePath, part.PartName, part.InstanceCount, part.Instances, part.OriginalInstances, extractionArtifactDir)); } Log("[assembly] extracting mates..."); var mates = ExtractMates(doc); NormalizeMateComponentIds(mates, componentIdMap); Log($"[assembly] mates={mates.Count}"); Log("[assembly] building functional face repository..."); var functionalFaces = BuildFunctionalFaceRepository(mates); Log($"[assembly] functional_faces={functionalFaces.Count}"); Log("[assembly] normalizing standard part functional face paths..."); NormalizeStandardPartFunctionalFacePaths(parts, functionalFaces); Log("[assembly] backfilling part functional knowledge..."); BackfillPartFunctionalKnowledge(parts, functionalFaces); Log("[assembly] persisting enriched part modeling plans..."); PersistEnrichedPartModelingPlans(parts, assemblyName, asmPath); string workflowId = "assembly-knowledge-skillflow-" + MakeAssemblyStepFileBase(assemblyName); Log("[assembly] building executable assembly steps..."); var executableSteps = BuildExecutableSteps(workflowId, assemblyName, components, parts, componentPatterns, mates, functionalFaces); Log($"[assembly] executable_steps={executableSteps.Count}"); return new { schemaVersion = "assembly_knowledge_skillflow.v1", workflowId, sourceAgent = "assembly_knowledge_audit", approvedPlanText = "Generated from extracted part modeling plans, assembly mates, and functional face repository.", generatedAt = DateTime.Now, modelingPlan = new { planId = workflowId + "-plan", targetAgent = "solidworks", units = "mm", summary = $"Rebuild assembly from extracted part skill flows and functional-face mates. steps={executableSteps.Count}", steps = executableSteps }, knowledge = new { sourceAssembly = new { name = assemblyName, path = asmPath }, parts, functionalFaceRepository = functionalFaces, assembly = new { components, componentPatterns, mates, matePorts = BuildMatePorts(mates) } } }; } sealed class ComponentInfo { public string AssemblyName { get; set; } = ""; public string ParentComponent { get; set; } = ""; public string OriginalParentComponent { get; set; } = ""; public string ComponentName { get; set; } = ""; public string OriginalComponentName { get; set; } = ""; public string PartName { get; set; } = ""; public string FilePath { get; set; } = ""; public string DocumentType { get; set; } = ""; public string SourceKind { get; set; } = "direct_seed"; public bool IsPatternInstance { get; set; } public bool IsMirrored { get; set; } public string SourcePattern { get; set; } = ""; public string SourcePatternType { get; set; } = ""; public string SourceSeedComponent { get; set; } = ""; public bool IsStandardPart { get; set; } public string StandardRule { get; set; } = ""; public double X { get; set; } public double Y { get; set; } public double Z { get; set; } public double[] Transform { get; set; } = Array.Empty(); } sealed class MateFeatureSnapshot { public Feature Feature { get; set; } public Mate2 Mate { get; set; } public string MateName { get; set; } = ""; public int ScannedFeatureIndex { get; set; } } sealed class PartKnowledge { public string PartName { get; set; } = ""; public string FilePath { get; set; } = ""; public int InstanceCount { get; set; } public List Instances { get; set; } = new(); public List OriginalInstances { get; set; } = new(); public object Extraction { get; set; } = new(); public string SkillFlowPath { get; set; } = ""; public string ModelingPlanPath { get; set; } = ""; public string ValidationPath { get; set; } = ""; public string RebuiltOutputPath { get; set; } = ""; public bool IsStandardPart { get; set; } public string StandardRule { get; set; } = ""; public string DirectInsertPath { get; set; } = ""; public object SkillFlow { get; set; } public object ModelingPlan { get; set; } public object Validation { get; set; } public List> FunctionalFaces { get; set; } = new(); public List> MatePorts { get; set; } = new(); } sealed class AssemblyComponentPatternInfo { public string PatternName { get; set; } = ""; public string PatternType { get; set; } = ""; public string FeatureTypeName { get; set; } = ""; public List SeedComponents { get; set; } = new(); public List GeneratedComponents { get; set; } = new(); public List SkippedItems { get; set; } = new(); public Dictionary Parameters { get; set; } = new(); } static List ReadComponents(AssemblyDoc assy, string assemblyName) { var result = new List(); foreach (object item in assy.GetComponents(false) as object[] ?? Array.Empty()) { if (item is not Component2 comp) continue; string path = Safe(() => comp.GetPathName()); if (string.IsNullOrWhiteSpace(path)) continue; string ext = Path.GetExtension(path).ToLowerInvariant(); string componentName = Safe(() => comp.Name2); string partName = Path.GetFileNameWithoutExtension(path); bool isStandardPart = IsGbStandardPart(componentName, partName, path); var transform = ComponentTransform(comp); result.Add(new ComponentInfo { AssemblyName = assemblyName, ParentComponent = ParentName(comp), OriginalParentComponent = ParentName(comp), ComponentName = componentName, OriginalComponentName = componentName, PartName = partName, FilePath = Path.GetFullPath(path), DocumentType = ext == ".sldprt" ? "part" : ext == ".sldasm" ? "assembly" : ext.TrimStart('.'), IsPatternInstance = SafeBool(() => comp.IsPatternInstance()), IsMirrored = SafeBool(() => comp.IsMirrored()), IsStandardPart = isStandardPart, StandardRule = isStandardPart ? "name_contains_GB" : "", X = transform.Length >= 12 ? transform[9] : 0.0, Y = transform.Length >= 12 ? transform[10] : 0.0, Z = transform.Length >= 12 ? transform[11] : 0.0, Transform = transform }); } return result; } static Dictionary NormalizeComponentInstanceIds(List components) { var map = new Dictionary(StringComparer.OrdinalIgnoreCase); var counters = new Dictionary(StringComparer.OrdinalIgnoreCase); foreach (var component in components .OrderBy(c => ComponentIdBaseName(c), StringComparer.OrdinalIgnoreCase) .ThenBy(c => IsGeneratedAssemblyComponent(c) ? 1 : 0) .ThenBy(c => OriginalInstanceNumber(c)) .ThenBy(c => c.OriginalComponentName, StringComparer.OrdinalIgnoreCase)) { string originalName = string.IsNullOrWhiteSpace(component.OriginalComponentName) ? component.ComponentName : component.OriginalComponentName; component.OriginalComponentName = originalName; string baseName = ComponentIdBaseName(component); if (string.IsNullOrWhiteSpace(baseName)) continue; counters.TryGetValue(baseName, out int count); count++; counters[baseName] = count; string normalizedName = $"{baseName}-{count}"; component.ComponentName = normalizedName; if (!string.IsNullOrWhiteSpace(originalName) && !map.ContainsKey(originalName)) map[originalName] = normalizedName; } foreach (var component in components) { if (string.IsNullOrWhiteSpace(component.OriginalParentComponent)) component.OriginalParentComponent = component.ParentComponent; if (!string.IsNullOrWhiteSpace(component.ParentComponent) && map.TryGetValue(component.ParentComponent, out string normalizedParent)) { component.ParentComponent = normalizedParent; } } Log($"[assembly] normalized_component_ids={map.Count}"); return map; } static bool IsGeneratedAssemblyComponent(ComponentInfo component) { if (component.IsPatternInstance || component.IsMirrored) return true; return string.Equals(component.SourceKind, "generated_by_pattern", StringComparison.OrdinalIgnoreCase) || string.Equals(component.SourceKind, "generated_by_mirror", StringComparison.OrdinalIgnoreCase); } static int OriginalInstanceNumber(ComponentInfo component) { string value = string.IsNullOrWhiteSpace(component.OriginalComponentName) ? component.ComponentName : component.OriginalComponentName; if (string.IsNullOrWhiteSpace(value)) return int.MaxValue; int dash = value.LastIndexOf('-'); if (dash < 0 || dash >= value.Length - 1) return int.MaxValue; return int.TryParse(value[(dash + 1)..], NumberStyles.Integer, CultureInfo.InvariantCulture, out int number) ? number : int.MaxValue; } static string ComponentIdBaseName(ComponentInfo component) { string baseName = StripInstanceSuffix(component.PartName); if (string.IsNullOrWhiteSpace(baseName)) baseName = StripInstanceSuffix(component.OriginalComponentName); if (string.IsNullOrWhiteSpace(baseName)) baseName = StripInstanceSuffix(component.ComponentName); return baseName.Trim(); } static List ReadAssemblyComponentPatterns(ModelDoc2 doc, IReadOnlyDictionary componentIdMap) { var result = new List(); foreach (var feat in WalkFeatures(doc.FirstFeature() as Feature)) { string featureName = Safe(() => feat.Name); string typeName = Safe(() => feat.GetTypeName2()); string patternType = AssemblyPatternType(typeName, featureName); if (string.IsNullOrWhiteSpace(patternType)) continue; var pattern = new AssemblyComponentPatternInfo { PatternName = featureName, PatternType = patternType, FeatureTypeName = typeName }; object specific = SafeObj(() => feat.GetDefinition()) ?? SafeObj(() => feat.GetSpecificFeature2()); if (specific != null) { AccessPatternSelections(specific, doc, pattern.Parameters); ReadPatternComponents(specific, "SeedComponentArray", pattern.SeedComponents, componentIdMap); ReadPatternComponents(specific, "ComponentsToInstanceAlignToComponentOrigin", pattern.SeedComponents, componentIdMap); ReadPatternComponents(specific, "ComponentsToInstanceAlignToSelection", pattern.SeedComponents, componentIdMap); ReadPatternComponents(specific, "OppositeHandComponents", pattern.SeedComponents, componentIdMap); ReadPatternComponentGroup(specific, "SeedComponentArray", pattern.Parameters, componentIdMap); ReadPatternComponentGroup(specific, "ComponentsToInstanceAlignToComponentOrigin", pattern.Parameters, componentIdMap); ReadPatternComponentGroup(specific, "ComponentsToInstanceAlignToSelection", pattern.Parameters, componentIdMap); ReadPatternComponentGroup(specific, "OppositeHandComponents", pattern.Parameters, componentIdMap); ReadPatternValueArray(specific, pattern.Parameters, "ComponentOrientationsAlignToComponentOrigin"); ReadPatternValueArray(specific, pattern.Parameters, "ComponentOrientationsAlignToSelection"); ReadPatternValueArray(specific, pattern.Parameters, "FlipDirections"); ReadPatternValueArray(specific, pattern.Parameters, "MirroredComponentFilenames"); ReadPatternComponents(specific, "SkippedItemArray", pattern.SkippedItems, componentIdMap); ReadPatternScalar(specific, pattern.Parameters, "D1TotalInstances"); ReadPatternScalar(specific, pattern.Parameters, "D1Spacing"); ReadPatternScalar(specific, pattern.Parameters, "D1ReverseDirection"); ReadPatternScalar(specific, pattern.Parameters, "D2TotalInstances"); ReadPatternScalar(specific, pattern.Parameters, "D2Spacing"); ReadPatternScalar(specific, pattern.Parameters, "D2ReverseDirection"); ReadPatternScalar(specific, pattern.Parameters, "TotalInstances"); ReadPatternScalar(specific, pattern.Parameters, "TotalInstances2"); ReadPatternScalar(specific, pattern.Parameters, "Spacing"); ReadPatternScalar(specific, pattern.Parameters, "Spacing2"); ReadPatternScalar(specific, pattern.Parameters, "EqualSpacing"); ReadPatternScalar(specific, pattern.Parameters, "EqualSpacing2"); ReadPatternScalar(specific, pattern.Parameters, "ReverseDirection"); ReadPatternScalar(specific, pattern.Parameters, "Direction2"); ReadPatternScalar(specific, pattern.Parameters, "Symmetric"); ReadPatternScalar(specific, pattern.Parameters, "SynchronizeFlexibleComponents"); ReadPatternScalar(specific, pattern.Parameters, "ForceUseSeedConfiguration"); ReadPatternScalar(specific, pattern.Parameters, "MirrorType"); ReadPatternScalar(specific, pattern.Parameters, "NameModifierType"); ReadPatternScalar(specific, pattern.Parameters, "NameModifier"); ReadPatternScalar(specific, pattern.Parameters, "MirrorTransferOptions"); ReadPatternScalar(specific, pattern.Parameters, "BreakLinksToOriginalPart"); ReadPatternScalar(specific, pattern.Parameters, "CreateDerivedConfigurations"); ReadPatternScalar(specific, pattern.Parameters, "PreserveZAxis"); ReadPatternScalar(specific, pattern.Parameters, "SyncFlexibleSubAssemblies"); ReadPatternScalar(specific, pattern.Parameters, "MirrorComponentsFolderLocation"); ReadPatternReference(specific, pattern.Parameters, "D1Axis"); ReadPatternReference(specific, pattern.Parameters, "D2Axis"); ReadPatternReference(specific, pattern.Parameters, "RotationAxis"); ReadPatternReference(specific, pattern.Parameters, "Axis"); ReadPatternReference(specific, pattern.Parameters, "MirrorPlane"); ReadPatternMethodScalar(specific, pattern.Parameters, "GetAxisType", "AxisType"); ReadPatternMethodScalar(specific, pattern.Parameters, "GetD1AxisType", "D1AxisType"); ReadPatternMethodScalar(specific, pattern.Parameters, "GetD2AxisType", "D2AxisType"); ReadTypedPatternData(specific, pattern.Parameters); ReadPatternSelectionReferences(doc, pattern.Parameters); PromotePatternReferenceFromSelections(pattern.PatternType, pattern.Parameters); NormalizePatternReferenceComponentIds(pattern.Parameters, componentIdMap); if (string.Equals(pattern.PatternType, "mirror_component", StringComparison.OrdinalIgnoreCase)) NormalizeMirrorComponentParameters(pattern.Parameters); ReleasePatternSelections(specific); } ReadPatternGeneratedComponents(feat, pattern.GeneratedComponents, componentIdMap); result.Add(pattern); } Log($"[assembly] component_patterns={result.Count}"); return result; } static string AssemblyPatternType(string typeName, string featureName) { string text = $"{typeName} {featureName}"; if (ContainsAny(text, "LocalLPattern", "局部线性阵列")) return "local_linear_pattern"; if (ContainsAny(text, "LocalCirPattern", "局部圆周阵列")) return "local_circular_pattern"; if (ContainsAny(text, "MirrorComponent", "镜向零部件")) return "mirror_component"; return ""; } static void ReadPatternComponents(object featureData, string propertyName, List target, IReadOnlyDictionary componentIdMap) { object value = GetComProperty(featureData, propertyName); foreach (object item in ToObjectArray(value)) { string name = ""; if (item is Component2 comp) name = Safe(() => comp.Name2); else if (item is Feature feature) name = Safe(() => feature.Name); AddPatternComponentName(target, name, componentIdMap); } } static void ReadPatternGeneratedComponents(Feature patternFeature, List target, IReadOnlyDictionary componentIdMap) { Feature sub = SafeObj(() => patternFeature.GetFirstSubFeature()) as Feature; while (sub != null) { string type = Safe(() => sub.GetTypeName2()); if (ContainsAny(type, "ReferencePattern")) AddPatternComponentName(target, Safe(() => sub.Name), componentIdMap); sub = SafeObj(() => sub.GetNextSubFeature()) as Feature; } } static void AddPatternComponentName(List target, string name, IReadOnlyDictionary componentIdMap) { if (string.IsNullOrWhiteSpace(name)) return; if (componentIdMap.TryGetValue(name, out string normalized)) name = normalized; if (!string.IsNullOrWhiteSpace(name) && !target.Contains(name, StringComparer.OrdinalIgnoreCase)) target.Add(name); } static void NormalizePatternReferenceComponentIds(Dictionary parameters, IReadOnlyDictionary componentIdMap) { if (parameters.Count == 0 || componentIdMap.Count == 0) return; foreach (var value in parameters.Values) NormalizeReferenceComponentIds(value, componentIdMap); } static void NormalizeReferenceComponentIds(object value, IReadOnlyDictionary componentIdMap) { if (value is Dictionary dict) { NormalizeReferenceComponentId(dict, "owning_component", componentIdMap); foreach (var nested in dict.Values) NormalizeReferenceComponentIds(nested, componentIdMap); return; } if (value is List> dictList) { foreach (var item in dictList) NormalizeReferenceComponentIds(item, componentIdMap); return; } if (value is IEnumerable items && value is not string) { foreach (var item in items) NormalizeReferenceComponentIds(item, componentIdMap); } } static void NormalizeReferenceComponentId(Dictionary dict, string key, IReadOnlyDictionary componentIdMap) { if (!dict.TryGetValue(key, out var value) || value == null) return; string original = Convert.ToString(value, CultureInfo.InvariantCulture) ?? ""; if (string.IsNullOrWhiteSpace(original)) return; if (componentIdMap.TryGetValue(original, out string normalized) && !string.IsNullOrWhiteSpace(normalized)) { dict[key] = normalized; if (!dict.ContainsKey("original_" + key)) dict["original_" + key] = original; } } static void ReadPatternComponentGroup(object featureData, string propertyName, Dictionary parameters, IReadOnlyDictionary componentIdMap) { var names = new List(); ReadPatternComponents(featureData, propertyName, names, componentIdMap); if (names.Count > 0) parameters[propertyName + "Components"] = names; } static void ReadPatternValueArray(object featureData, Dictionary parameters, string propertyName) { object value = GetComProperty(featureData, propertyName); if (value == null) return; var values = new List(); foreach (object item in ToObjectArray(value)) { if (item == null) continue; if (item is bool or int or double or float or decimal or string) { values.Add(item); continue; } try { values.Add(Convert.ToInt32(item)); } catch { string text = Safe(() => item.ToString() ?? ""); if (!string.IsNullOrWhiteSpace(text)) values.Add(text); } } if (values.Count > 0) parameters[propertyName] = values; } static void ReadPatternScalar(object featureData, Dictionary parameters, string propertyName) { object value = GetComProperty(featureData, propertyName); if (value == null) return; if (value is string s) { if (!string.IsNullOrWhiteSpace(s)) parameters[propertyName] = s; return; } if (value is bool or int or double or float or decimal) parameters[propertyName] = value; } static void ReadPatternReference(object featureData, Dictionary parameters, string propertyName) { object value = GetComProperty(featureData, propertyName); if (value == null) return; var reference = DescribeReference(value); if (reference.Count > 0) parameters[propertyName + "Reference"] = reference; } static void ReadPatternMethodScalar(object featureData, Dictionary parameters, string methodName, string parameterName) { object value = InvokeIfExists(featureData, methodName); if (value == null) return; if (value is bool or int or double or float or decimal) { parameters[parameterName] = value; return; } try { parameters[parameterName] = Convert.ToInt32(value, CultureInfo.InvariantCulture); } catch { } } static void AccessPatternSelections(object featureData, ModelDoc2 doc, Dictionary parameters) { bool accessed = false; if (featureData is ILocalCircularPatternFeatureData circular) accessed = SafeBool(() => circular.IAccessSelections2(doc, null)) || SafeBool(() => circular.AccessSelections(doc, null)); else if (featureData is ILocalLinearPatternFeatureData linear) accessed = SafeBool(() => linear.IAccessSelections2(doc, null)) || SafeBool(() => linear.AccessSelections(doc, null)); else if (featureData is IMirrorComponentFeatureData mirror) accessed = SafeBool(() => mirror.AccessSelections(doc, null)); else accessed = SafeBool(() => Convert.ToBoolean(InvokeIfExists(featureData, "AccessSelections", doc, null), CultureInfo.InvariantCulture)); parameters["AccessSelectionsOk"] = accessed; } static void ReleasePatternSelections(object featureData) { if (featureData is ILocalCircularPatternFeatureData circular) SafeAction(() => circular.ReleaseSelectionAccess()); else if (featureData is ILocalLinearPatternFeatureData linear) SafeAction(() => linear.ReleaseSelectionAccess()); else if (featureData is IMirrorComponentFeatureData mirror) SafeAction(() => mirror.ReleaseSelectionAccess()); else SafeAction(() => InvokeIfExists(featureData, "ReleaseSelectionAccess")); } static void ReadTypedPatternData(object featureData, Dictionary parameters) { if (featureData is ILocalCircularPatternFeatureData circular) { int axisType = SafeInt(() => circular.GetAxisType(), -1); parameters["AxisType"] = axisType; parameters["AxisTypeName"] = LocalCircularAxisTypeName(axisType); parameters["SeedComponentCount"] = SafeInt(() => circular.GetSeedComponentCount(), 0); parameters["SkippedItemCount"] = SafeInt(() => circular.GetSkippedItemCount(), 0); var axisRef = DescribeReference(SafeObj(() => circular.Axis)); if (axisRef.Count > 0) { axisRef["axis_type"] = axisType; axisRef["axis_type_name"] = LocalCircularAxisTypeName(axisType); axisRef["reference_source"] = "ILocalCircularPatternFeatureData.Axis"; parameters["AxisReference"] = axisRef; } ReadPatternTransforms( parameters, "InstanceTransforms", SafeInt(() => circular.TotalInstances, ReadIntParameter(parameters, "TotalInstances", 0)), index => SafeObj(() => circular.GetTransform(index))); } else if (featureData is ILocalLinearPatternFeatureData linear) { parameters["D1AxisType"] = SafeInt(() => linear.GetD1AxisType(), -1); parameters["D2AxisType"] = SafeInt(() => linear.GetD2AxisType(), -1); parameters["SeedComponentCount"] = SafeInt(() => linear.GetSeedComponentCount(), 0); parameters["SkippedItemCount"] = SafeInt(() => linear.GetSkippedItemCount(), 0); var d1Ref = DescribeReference(SafeObj(() => linear.D1Axis)); if (d1Ref.Count > 0) parameters["D1AxisReference"] = d1Ref; var d2Ref = DescribeReference(SafeObj(() => linear.D2Axis)); if (d2Ref.Count > 0) parameters["D2AxisReference"] = d2Ref; int d1 = SafeInt(() => linear.D1TotalInstances, ReadIntParameter(parameters, "D1TotalInstances", 0)); int d2 = SafeInt(() => linear.D2TotalInstances, ReadIntParameter(parameters, "D2TotalInstances", 0)); ReadPatternTransforms(parameters, "InstanceTransforms", Math.Max(d1, d1 * Math.Max(1, d2)), index => SafeObj(() => linear.GetTransform(index))); } } static string LocalCircularAxisTypeName(int axisType) { return axisType switch { 0 => "reference_axis", 1 => "edge", 2 => "dimension", _ => "unknown" }; } static void ReadPatternTransforms(Dictionary parameters, string key, int totalInstances, Func getTransform) { if (totalInstances <= 0) return; var transforms = new List>(); for (int i = 0; i <= totalInstances; i++) { object transform = getTransform(i); var values = MathTransformArray(transform); if (values.Length == 0) continue; transforms.Add(new Dictionary { ["instance_index"] = i, ["array_data"] = values, ["coordinate_system"] = "solidworks_math_transform" }); } if (transforms.Count > 0) parameters[key] = transforms; } static double[] MathTransformArray(object transform) { if (transform == null) return Array.Empty(); object raw = GetComProperty(transform, "ArrayData"); var values = ToDoubleArray(raw); return values.Length > 0 ? values.Select(R).ToArray() : Array.Empty(); } static void ReadPatternSelectionReferences(ModelDoc2 doc, Dictionary parameters) { var selections = new List>(); try { if (doc?.SelectionManager is not SelectionMgr selMgr) return; int count = SafeInt(() => selMgr.GetSelectedObjectCount2(-1), 0); for (int i = 1; i <= count; i++) { object selected = SafeObj(() => selMgr.GetSelectedObject6(i, -1)); if (selected == null) continue; var reference = DescribeReference(selected); if (reference.Count == 0) continue; int selectionType = SafeInt(() => selMgr.GetSelectedObjectType3(i, -1), 0); int selectionMark = SafeInt(() => Convert.ToInt32(InvokeIfExists(selMgr, "GetSelectedObjectMark", i), CultureInfo.InvariantCulture), 0); reference["selection_index"] = i; reference["selection_object_type"] = selectionType; reference["selection_object_type_name"] = SelectionTypeName(selectionType); reference["selection_mark"] = selectionMark; selections.Add(reference); } } catch { } if (selections.Count > 0) parameters["SelectionReferences"] = selections; } static void PromotePatternReferenceFromSelections(string patternType, Dictionary parameters) { if (!string.Equals(patternType, "local_circular_pattern", StringComparison.OrdinalIgnoreCase)) return; if (parameters.ContainsKey("AxisReference")) return; if (!parameters.TryGetValue("SelectionReferences", out object value) || value is not List> selections) return; var explicitAxisCandidates = selections .Where(IsAxisLikeSelectionReference) .ToList(); if (explicitAxisCandidates.Count == 1) { explicitAxisCandidates[0]["reference_source"] = "AccessSelections axis-like selection"; parameters["AxisReference"] = explicitAxisCandidates[0]; parameters["AxisReferenceSource"] = "feature_data_access_selections"; } } static bool IsAxisLikeSelectionReference(Dictionary reference) { string kind = ReadDictString(reference, "reference_kind"); string selectionType = ReadDictString(reference, "selection_type"); string selectionObjectType = ReadDictString(reference, "selection_object_type_name"); string featureType = ReadDictString(reference, "feature_type_name"); string curveType = ""; if (reference.TryGetValue("edge_signature", out object edgeObj) && edgeObj is Dictionary edgeSig) curveType = ReadDictString(edgeSig, "curve_type"); if (string.Equals(kind, "feature", StringComparison.OrdinalIgnoreCase) && featureType.Contains("RefAxis", StringComparison.OrdinalIgnoreCase)) return true; if (selectionType.Contains("AXIS", StringComparison.OrdinalIgnoreCase) || selectionObjectType.Contains("AXIS", StringComparison.OrdinalIgnoreCase) || selectionObjectType.Contains("DATUMAXES", StringComparison.OrdinalIgnoreCase)) return true; if (string.Equals(kind, "edge", StringComparison.OrdinalIgnoreCase) && (string.Equals(curveType, "circle", StringComparison.OrdinalIgnoreCase) || string.Equals(curveType, "line", StringComparison.OrdinalIgnoreCase))) return true; if (string.Equals(kind, "face", StringComparison.OrdinalIgnoreCase) && reference.TryGetValue("face_signature", out object faceObj) && faceObj is Dictionary faceSig && string.Equals(ReadDictString(faceSig, "surface_type"), "cylinder", StringComparison.OrdinalIgnoreCase)) return true; return false; } static string SelectionTypeName(int selectionType) { try { string name = Enum.GetName(typeof(swSelectType_e), selectionType); return name ?? selectionType.ToString(CultureInfo.InvariantCulture); } catch { return selectionType.ToString(CultureInfo.InvariantCulture); } } static void NormalizeMirrorComponentParameters(Dictionary parameters) { var originComponents = ReadObjectList(parameters, "ComponentsToInstanceAlignToComponentOriginComponents"); var selectionComponents = ReadObjectList(parameters, "ComponentsToInstanceAlignToSelectionComponents"); var oppositeComponents = ReadObjectList(parameters, "OppositeHandComponentsComponents"); var originOrientations = ReadObjectList(parameters, "ComponentOrientationsAlignToComponentOrigin"); var selectionOrientations = ReadObjectList(parameters, "ComponentOrientationsAlignToSelection"); var instanceComponents = new List(); instanceComponents.AddRange(originComponents); instanceComponents.AddRange(selectionComponents); var componentOrientations = new List(); componentOrientations.AddRange(originOrientations); componentOrientations.AddRange(selectionOrientations); parameters["MirrorComponentsApi"] = "IAssemblyDoc.MirrorComponents3"; parameters["ComponentsToInstanceComponents"] = instanceComponents; parameters["ComponentOrientations"] = componentOrientations; parameters["OrientAboutCenterOfMass"] = selectionComponents.Count > 0; parameters["ComponentsToMirrorComponents"] = oppositeComponents; EnsureParameter(parameters, "CreateDerivedConfigurations", false); EnsureParameter(parameters, "MirroredComponentFilenames", new List()); EnsureParameter(parameters, "NameModifierType", 0); EnsureParameter(parameters, "NameModifier", ""); EnsureParameter(parameters, "MirrorTransferOptions", 0); EnsureParameter(parameters, "BreakLinksToOriginalPart", false); EnsureParameter(parameters, "PreserveZAxis", false); EnsureParameter(parameters, "SyncFlexibleSubAssemblies", false); EnsureParameter(parameters, "MirrorComponentsFolderLocation", ""); parameters["MirroredComponentFileLocation"] = ReadStringParameter(parameters, "MirrorComponentsFolderLocation"); parameters["ImportOptions"] = ReadIntParameter(parameters, "MirrorTransferOptions", 0); parameters["BreakLinks"] = ReadBoolParameter(parameters, "BreakLinksToOriginalPart", false); } static List ReadObjectList(Dictionary values, string key) { if (!values.TryGetValue(key, out object value) || value == null) return new List(); if (value is IEnumerable objectItems) return objectItems.Where(x => x != null).ToList(); if (value is System.Collections.IEnumerable enumerable && value is not string) { var result = new List(); foreach (object item in enumerable) { if (item != null) result.Add(item); } return result; } return new List { value }; } static void EnsureParameter(Dictionary values, string key, object defaultValue) { if (!values.ContainsKey(key) || values[key] == null) values[key] = defaultValue; } static string ReadStringParameter(Dictionary values, string key) { return values.TryGetValue(key, out object value) ? value?.ToString() ?? "" : ""; } static int ReadIntParameter(Dictionary values, string key, int defaultValue) { if (!values.TryGetValue(key, out object value) || value == null) return defaultValue; try { return Convert.ToInt32(value, CultureInfo.InvariantCulture); } catch { return defaultValue; } } static bool ReadBoolParameter(Dictionary values, string key, bool defaultValue) { if (!values.TryGetValue(key, out object value) || value == null) return defaultValue; if (value is bool b) return b; try { return Convert.ToBoolean(value, CultureInfo.InvariantCulture); } catch { return defaultValue; } } static Dictionary DescribeReference(object reference) { var result = new Dictionary(StringComparer.OrdinalIgnoreCase); try { result["runtime_type"] = reference.GetType().Name; } catch { } if (reference is Component2 component) { result["reference_kind"] = "component"; string name = Safe(() => component.Name2); string path = Safe(() => component.GetPathName()); if (!string.IsNullOrWhiteSpace(name)) result["component_name"] = name; if (!string.IsNullOrWhiteSpace(path)) result["component_path"] = path; } else if (reference is Feature feature) { result["reference_kind"] = "feature"; string type = ""; string selectionName = Safe(() => feature.GetNameForSelection(out type)); string name = Safe(() => feature.Name); string featureType = Safe(() => feature.GetTypeName2()); if (!string.IsNullOrWhiteSpace(name)) result["name"] = name; if (!string.IsNullOrWhiteSpace(featureType)) result["feature_type_name"] = featureType; if (!string.IsNullOrWhiteSpace(selectionName)) result["selection_name"] = selectionName; if (!string.IsNullOrWhiteSpace(type)) result["selection_type"] = type; } else if (reference is Edge edge) { result["reference_kind"] = "edge"; result["edge_signature"] = EdgeSignature(edge); if (reference is Entity edgeEntity) DescribeEntity(edgeEntity, result); } else if (reference is Face2 face) { result["reference_kind"] = "face"; result["face_signature"] = FaceSignature(face); if (reference is Entity faceEntity) DescribeEntity(faceEntity, result); } else if (reference is Entity entity) { result["reference_kind"] = "entity"; DescribeEntity(entity, result); } return result; } static void DescribeEntity(Entity entity, Dictionary result) { string modelName = Safe(() => entity.ModelName); int type = SafeInt(() => entity.GetType(), 0); if (!string.IsNullOrWhiteSpace(modelName)) result["model_name"] = modelName; if (type != 0) result["entity_type"] = type; if (SafeObj(() => entity.GetComponent()) is Component2 component) { string componentName = Safe(() => component.Name2); string componentPath = Safe(() => component.GetPathName()); if (!string.IsNullOrWhiteSpace(componentName)) result["owning_component"] = componentName; if (!string.IsNullOrWhiteSpace(componentPath)) result["owning_component_path"] = componentPath; result["reference_coordinate_system"] = "component_model"; } } static void AnnotatePatternGeneratedComponents(List components, List patterns) { foreach (var component in components) { if (!component.IsPatternInstance && !component.IsMirrored) continue; component.SourceKind = component.IsMirrored ? "generated_by_mirror" : "generated_by_pattern"; var pattern = FindLikelySourcePattern(component, patterns); if (pattern != null) { component.SourcePattern = pattern.PatternName; component.SourcePatternType = pattern.PatternType; component.SourceSeedComponent = pattern.SeedComponents.FirstOrDefault() ?? ""; if (!pattern.GeneratedComponents.Contains(component.ComponentName, StringComparer.OrdinalIgnoreCase)) pattern.GeneratedComponents.Add(component.ComponentName); } } foreach (var pattern in patterns) { pattern.SeedComponents = pattern.SeedComponents .Distinct(StringComparer.OrdinalIgnoreCase) .ToList(); pattern.GeneratedComponents = pattern.GeneratedComponents .Where(item => !pattern.SeedComponents.Contains(item, StringComparer.OrdinalIgnoreCase)) .Distinct(StringComparer.OrdinalIgnoreCase) .ToList(); } int generatedCount = components.Count(c => !IsDirectInsertComponent(c)); Log($"[assembly] pattern_generated_components={generatedCount}"); } static AssemblyComponentPatternInfo FindLikelySourcePattern(ComponentInfo component, List patterns) { var candidates = patterns .Where(pattern => component.IsMirrored ? string.Equals(pattern.PatternType, "mirror_component", StringComparison.OrdinalIgnoreCase) : !string.Equals(pattern.PatternType, "mirror_component", StringComparison.OrdinalIgnoreCase)) .Where(pattern => pattern.SeedComponents.Any(seed => seed.StartsWith(component.PartName + "-", StringComparison.OrdinalIgnoreCase) || string.Equals(ComponentBaseName(seed), component.PartName, StringComparison.OrdinalIgnoreCase))) .ToList(); return candidates.Count == 1 ? candidates[0] : null; } static bool IsDirectInsertComponent(ComponentInfo component) { return string.Equals(component.DocumentType, "part", StringComparison.OrdinalIgnoreCase) && !string.Equals(component.SourceKind, "generated_by_pattern", StringComparison.OrdinalIgnoreCase) && !string.Equals(component.SourceKind, "generated_by_mirror", StringComparison.OrdinalIgnoreCase); } static string StripInstanceSuffix(string value) { if (string.IsNullOrWhiteSpace(value)) return ""; string name = value.Trim(); if (LooksLikePathOrSolidWorksFile(name)) name = Path.GetFileNameWithoutExtension(name); int dash = name.LastIndexOf('-'); if (dash > 0 && dash < name.Length - 1 && name[(dash + 1)..].All(char.IsDigit)) name = name[..dash]; return name.Trim(); } static bool LooksLikePathOrSolidWorksFile(string value) { if (string.IsNullOrWhiteSpace(value)) return false; if (value.Contains('\\') || value.Contains('/')) return true; string ext = Path.GetExtension(value); return string.Equals(ext, ".SLDPRT", StringComparison.OrdinalIgnoreCase) || string.Equals(ext, ".SLDASM", StringComparison.OrdinalIgnoreCase); } static double[] ComponentTransform(Component2 comp) { try { if (comp.Transform2 is MathTransform transform) { var data = ToDoubleArray(transform.ArrayData); if (data.Length >= 16) return data.Take(16).ToArray(); } } catch { } return new double[] { 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0 }; } static PartKnowledge CreateStandardPartKnowledge(string partPath, string directInsertPath, string partName, int instanceCount, List instances, List originalInstances, string standardRule) { return new PartKnowledge { PartName = partName, FilePath = partPath, InstanceCount = instanceCount, Instances = instances, OriginalInstances = originalInstances, IsStandardPart = true, StandardRule = standardRule, DirectInsertPath = directInsertPath, Extraction = new { ok = true, skippedModeling = true, reason = "standard_part_name_contains_GB", standardPartSourceRoot = StandardPartSourceRoot, originalComponentPath = partPath, directCallPath = directInsertPath } }; } static PartKnowledge ExtractPartKnowledge(string partPath, string partName, int instanceCount, List instances, List originalInstances, string extractionArtifactDir) { string dir = string.IsNullOrWhiteSpace(extractionArtifactDir) ? Path.GetDirectoryName(partPath) ?? System.Environment.CurrentDirectory : extractionArtifactDir; string fileBase = MakeSafeFileName(partName); string skillFlowPath = PreferredOrLatestFile(dir, $"{fileBase}_skill_flow.json", $"{fileBase}_skill_flow_*.json"); string modelingPlanPath = PreferredOrLatestFile(dir, $"{fileBase}_modeling_plan.json", $"{fileBase}_modeling_plan_*.json"); string validationPath = PreferredOrLatestFile(dir, $"{fileBase}_skill_flow_validation.json", $"{fileBase}_skill_flow_validation_*.json"); string rebuiltOutputPath = LatestRebuiltPartOutput(partName); bool forcePartExtraction = IsForcePartExtractionEnabled(); bool hasExistingExtraction = File.Exists(skillFlowPath) && File.Exists(modelingPlanPath); object result = hasExistingExtraction && !forcePartExtraction ? new { ok = true, reusedExistingExtraction = true, skillFlowPath, modelingPlanPath, validationPath, rebuiltOutputPath } : RunPartFeatureAudit(partPath, dir); if (!hasExistingExtraction || forcePartExtraction) { skillFlowPath = PreferredOrLatestFile(dir, $"{fileBase}_skill_flow.json", $"{fileBase}_skill_flow_*.json"); modelingPlanPath = PreferredOrLatestFile(dir, $"{fileBase}_modeling_plan.json", $"{fileBase}_modeling_plan_*.json"); validationPath = PreferredOrLatestFile(dir, $"{fileBase}_skill_flow_validation.json", $"{fileBase}_skill_flow_validation_*.json"); rebuiltOutputPath = LatestRebuiltPartOutput(partName); } return new PartKnowledge { PartName = partName, FilePath = partPath, InstanceCount = instanceCount, Instances = instances, OriginalInstances = originalInstances, IsStandardPart = false, StandardRule = "", DirectInsertPath = "", Extraction = result, SkillFlowPath = skillFlowPath, ModelingPlanPath = modelingPlanPath, ValidationPath = validationPath, RebuiltOutputPath = rebuiltOutputPath, SkillFlow = ReadJsonOrNull(skillFlowPath), ModelingPlan = ReadJsonOrNull(modelingPlanPath), Validation = ReadJsonOrNull(validationPath) }; } static object RunPartFeatureAudit(string partPath, string outputDir) { string exe = FindPartFeatureAuditExe(); if (string.IsNullOrWhiteSpace(exe) || !File.Exists(exe)) { return new { ok = false, message = "鏈壘鍒?PartFeatureAudit.exe锛岃烦杩囬浂浠跺缓妯?skill flow 鎻愬彇" }; } try { var psi = new ProcessStartInfo { FileName = exe, Arguments = Quote(partPath) + " --output-dir " + Quote(outputDir), UseShellExecute = false, RedirectStandardInput = true, RedirectStandardOutput = true, RedirectStandardError = true, CreateNoWindow = true }; using var p = Process.Start(psi); if (p == null) return new { ok = false, message = "鍚姩 PartFeatureAudit 澶辫触" }; p.StandardInput.WriteLine(); p.StandardInput.Close(); var stdoutTask = p.StandardOutput.ReadToEndAsync(); var stderrTask = p.StandardError.ReadToEndAsync(); bool exited = p.WaitForExit(180_000); if (!exited) { try { p.Kill(true); } catch { } try { p.WaitForExit(10_000); } catch { } } string stdout = SafeTaskResult(stdoutTask); string stderr = SafeTaskResult(stderrTask); return new { ok = exited && p.ExitCode == 0, timedOut = !exited, exitCode = exited ? p.ExitCode : -1, stdoutTail = Tail(stdout, 4000), stderrTail = Tail(stderr, 2000) }; } catch (Exception ex) { return new { ok = false, message = ex.Message }; } } static string FindPartFeatureAuditExe() { var candidates = new[] { Path.Combine(System.Environment.CurrentDirectory, "1", "1", "PartFeatureAudit", "bin", "Release", "net10.0", "PartFeatureAudit.exe"), Path.Combine(System.Environment.CurrentDirectory, "1", "1", "PartFeatureAudit", "bin", "Debug", "net10.0", "PartFeatureAudit.exe"), Path.GetFullPath(Path.Combine(AppContext.BaseDirectory, "..", "..", "..", "..", "PartFeatureAudit", "bin", "Release", "net10.0", "PartFeatureAudit.exe")), Path.GetFullPath(Path.Combine(AppContext.BaseDirectory, "..", "..", "..", "..", "PartFeatureAudit", "bin", "Debug", "net10.0", "PartFeatureAudit.exe")), Path.GetFullPath(Path.Combine(AppContext.BaseDirectory, "..", "..", "..", "..", "..", "PartFeatureAudit", "bin", "Release", "net10.0", "PartFeatureAudit.exe")), Path.GetFullPath(Path.Combine(AppContext.BaseDirectory, "..", "..", "..", "..", "..", "PartFeatureAudit", "bin", "Debug", "net10.0", "PartFeatureAudit.exe")) }; return candidates.FirstOrDefault(File.Exists) ?? ""; } static List BuildExecutableSteps( string workflowId, string assemblyName, List components, List parts, List componentPatterns, List mates, List> functionalFaces) { var steps = new List(); int generatedIndex = 1; AddGeneratedStep( steps, ref generatedIndex, "clear_functional_faces", "clear_functional_faces", "寮€濮嬭閰嶅墠娓呯┖涓存椂鍔熻兘闈㈠瓨鍌ㄥ簱", new Dictionary()); foreach (var part in parts) { if (part.IsStandardPart) continue; foreach (var step in ExtractModelingSteps(part.ModelingPlan)) steps.Add(step); AddGeneratedStep( steps, ref generatedIndex, "save_part_to_ai_folder", part.PartName, "Save rebuilt part for assembly insertion", new Dictionary { ["file_base_name"] = SyntheticPartFileBase(part.PartName, workflowId) }); } AddGeneratedStep( steps, ref generatedIndex, "create_new_assembly", assemblyName, "Create new assembly", new Dictionary()); foreach (var component in components .Where(IsDirectInsertComponent) .OrderBy(c => ComponentIdBaseName(c), StringComparer.OrdinalIgnoreCase) .ThenBy(c => OriginalInstanceNumber(c)) .ThenBy(c => c.ComponentName, StringComparer.OrdinalIgnoreCase)) { var part = parts.FirstOrDefault(p => string.Equals(p.FilePath, component.FilePath, StringComparison.OrdinalIgnoreCase)); if (part == null) continue; string partPath = ResolveInsertPartPath(part, workflowId); AddGeneratedStep( steps, ref generatedIndex, "insert_part_into_active_assembly", component.ComponentName, "鎻掑叆閲嶅缓闆朵欢鍒板綋鍓嶈閰嶄綋", new Dictionary { ["part_path"] = partPath, ["desired_component_id"] = component.ComponentName, ["x"] = component.X, ["y"] = component.Y, ["z"] = component.Z }); AddGeneratedStep( steps, ref generatedIndex, "set_component_transform", component.ComponentName, "鎸夊師瑁呴厤浣?Transform2 鎭㈠缁勪欢瀹屾暣浣嶅Э", new Dictionary { ["component_id"] = component.ComponentName, ["transform"] = component.Transform }); } foreach (var pattern in componentPatterns) { string skill = PatternSkillName(pattern.PatternType); if (string.IsNullOrWhiteSpace(skill)) continue; AddGeneratedStep( steps, ref generatedIndex, skill, pattern.PatternName, $"Create assembly component pattern/mirror from extracted {pattern.PatternType}", BuildPatternStepArgs(pattern)); } foreach (var face in functionalFaces) { AddGeneratedStep( steps, ref generatedIndex, "register_functional_face", face.TryGetValue("role", out var role) ? role?.ToString() ?? "functional_face" : "functional_face", "Register functional face inferred from assembly mate", new Dictionary(face)); } var faceBySource = functionalFaces .Where(face => face.TryGetValue("source_feature", out var src) && src != null) .ToDictionary(face => face["source_feature"].ToString() ?? "", face => face, StringComparer.OrdinalIgnoreCase); foreach (dynamic mate in mates) { string mateName = Convert.ToString(mate.mateName) ?? ""; string mateType = MateTypeForSkill(Convert.ToString(mate.mateTypeName) ?? ""); string alignment = AlignmentForSkill(Convert.ToString(mate.alignmentName) ?? ""); if (string.IsNullOrWhiteSpace(mateType)) continue; var entities = new List>(); foreach (Dictionary entity in mate.entities) entities.Add(entity); if (entities.Count != 2) continue; string sourceA = $"mate:{mateName}:0"; string sourceB = $"mate:{mateName}:1"; if (!faceBySource.TryGetValue(sourceA, out var faceA) || !faceBySource.TryGetValue(sourceB, out var faceB)) continue; if (string.Equals(mateType, "Gear", StringComparison.OrdinalIgnoreCase)) { if (!TryReadGearMateInfo(mate, out double ratioNumerator, out double ratioDenominator, out bool reverse)) { Log($"[mates] skip gear mate without extracted IGearMateFeatureData ratio: {mateName}"); continue; } var gearArgs = new Dictionary { ["mate_type"] = mateType, ["mate_name"] = mateName, ["component_a"] = ReadDictString(faceA, "component_id"), ["axis_point_a_mm"] = ReadDictDoubleArray(faceA, "mate_point_mm"), ["axis_a"] = ReadDictDoubleArray(faceA, "axis"), ["radius_a_mm"] = ReadDictDouble(faceA, "radius_mm"), ["source_feature_a"] = sourceA, ["mate_entity_index_a"] = ReadDictInt(faceA, "mate_entity_index", 0), ["component_b"] = ReadDictString(faceB, "component_id"), ["axis_point_b_mm"] = ReadDictDoubleArray(faceB, "mate_point_mm"), ["axis_b"] = ReadDictDoubleArray(faceB, "axis"), ["radius_b_mm"] = ReadDictDouble(faceB, "radius_mm"), ["source_feature_b"] = sourceB, ["mate_entity_index_b"] = ReadDictInt(faceB, "mate_entity_index", 1), ["ratio_numerator"] = ratioNumerator, ["ratio_denominator"] = ratioDenominator, ["reverse"] = reverse }; AddGeneratedStep( steps, ref generatedIndex, "create_gear_mate_by_axis_signature", mateName, "Create gear mate from extracted IGearMateFeatureData and mate-entity axis signatures", gearArgs); continue; } var mateArgs = new Dictionary { ["mate_type"] = mateType, ["component_a"] = faceA.TryGetValue("component_id", out var compA) ? compA?.ToString() ?? "" : "", ["face_role_a"] = faceA.TryGetValue("role", out var roleA) ? roleA?.ToString() ?? "" : "", ["source_feature_a"] = sourceA, ["mate_entity_index_a"] = ReadDictInt(faceA, "mate_entity_index", 0), ["side_tag_a"] = ReadDictString(faceA, "side_tag"), ["port_role_a"] = ReadDictString(faceA, "port_role"), ["counterpart_face_role_a"] = ReadDictString(faceA, "counterpart_role"), ["counterpart_side_tag_a"] = ReadDictString(faceA, "counterpart_side_tag"), ["component_b"] = faceB.TryGetValue("component_id", out var compB) ? compB?.ToString() ?? "" : "", ["face_role_b"] = faceB.TryGetValue("role", out var roleB) ? roleB?.ToString() ?? "" : "", ["source_feature_b"] = sourceB, ["mate_entity_index_b"] = ReadDictInt(faceB, "mate_entity_index", 1), ["side_tag_b"] = ReadDictString(faceB, "side_tag"), ["port_role_b"] = ReadDictString(faceB, "port_role"), ["counterpart_face_role_b"] = ReadDictString(faceB, "counterpart_role"), ["counterpart_side_tag_b"] = ReadDictString(faceB, "counterpart_side_tag"), ["alignment"] = alignment }; if (string.Equals(mateType, "Distance", StringComparison.OrdinalIgnoreCase)) { if (!TryReadMateDistanceMm(mate, out double distanceMm)) { Log($"[mates] skip distance mate without extracted distance: {mateName}"); continue; } mateArgs["distance_mm"] = distanceMm; if (TryReadMateDistanceFlipDimension(mate, out bool flipDimension)) mateArgs["flip_dimension"] = flipDimension; } AddGeneratedStep( steps, ref generatedIndex, "create_mate_by_functional_face", mateName, "鎸夊姛鑳介潰鍒涘缓瑁呴厤閰嶅悎", mateArgs); } AddGeneratedStep( steps, ref generatedIndex, "save_step_to_ai_folder", assemblyName, "瀵煎嚭閲嶅缓瑁呴厤浣?STEP 渚?ANSYS 浠跨湡浣跨敤", new Dictionary { ["file_base_name"] = MakeAssemblyStepFileBase(assemblyName) }); return steps; } static string PatternSkillName(string patternType) { if (string.Equals(patternType, "local_linear_pattern", StringComparison.OrdinalIgnoreCase)) return "create_local_linear_component_pattern"; if (string.Equals(patternType, "local_circular_pattern", StringComparison.OrdinalIgnoreCase)) return "create_local_circular_component_pattern"; if (string.Equals(patternType, "mirror_component", StringComparison.OrdinalIgnoreCase)) return "create_mirror_components"; return ""; } static Dictionary BuildPatternStepArgs(AssemblyComponentPatternInfo pattern) { var args = new Dictionary(StringComparer.OrdinalIgnoreCase) { ["pattern_name"] = pattern.PatternName, ["pattern_type"] = pattern.PatternType, ["feature_type_name"] = pattern.FeatureTypeName, ["seed_components"] = pattern.SeedComponents, ["generated_components"] = pattern.GeneratedComponents, ["skipped_items"] = pattern.SkippedItems, ["parameters"] = pattern.Parameters }; foreach (var kv in pattern.Parameters) args[ToSnakeCase(kv.Key)] = kv.Value; return args; } static string ToSnakeCase(string value) { if (string.IsNullOrWhiteSpace(value)) return ""; var sb = new StringBuilder(); for (int i = 0; i < value.Length; i++) { char c = value[i]; if (char.IsUpper(c) && i > 0 && value[i - 1] != '_') sb.Append('_'); sb.Append(char.ToLowerInvariant(c)); } return sb.ToString(); } static List> BuildFunctionalFaceRepository(List mates) { var faces = new List>(); foreach (dynamic mate in mates) { try { var entities = new List>(); foreach (var entity in mate.entities) if (entity is Dictionary dict) entities.Add(dict); if (entities.Count == 0) continue; var mateFaces = new List>(); for (int i = 0; i < entities.Count; i++) { var entity = entities[i]; var counterpart = entities.Count == 2 ? entities[1 - i] : null; string entityKind = ReadDictString(entity, "entity_kind"); bool hasFace = entity.TryGetValue("face_signature", out var sigObj) && sigObj is Dictionary; bool hasEdge = entity.TryGetValue("edge_signature", out var edgeObj) && edgeObj is Dictionary; if (!hasFace && !hasEdge) continue; var roles = ToStringList(entity.TryGetValue("role_candidates", out var rolesObj) ? rolesObj : null); string role = ChooseVerifiedFaceRole(entity, counterpart, SafeInt(() => Convert.ToInt32(mate.mateType)), roles); if (string.IsNullOrWhiteSpace(role)) continue; string sideTag = InferSideTag(entity); var face = new Dictionary { ["source_mate"] = Convert.ToString(mate.mateName) ?? "", ["source_feature"] = $"mate:{Convert.ToString(mate.mateName) ?? ""}:{i}", ["mate_type"] = MateTypeForSkill(Convert.ToString(mate.mateTypeName) ?? ""), ["alignment"] = AlignmentForSkill(Convert.ToString(mate.alignmentName) ?? ""), ["mate_entity_index"] = ReadDictInt(entity, "mate_entity_index", i), ["component_id"] = ReadDictString(entity, "component"), ["part_name"] = ReadDictString(entity, "part_name"), ["part_path"] = ReadDictString(entity, "component_path"), ["entity_kind"] = entityKind, ["role"] = role, ["side_tag"] = sideTag, ["port_role"] = string.IsNullOrWhiteSpace(sideTag) ? role : $"{role}:{sideTag}", ["source"] = "verified_from_assembly_mate", ["verified"] = true, ["confidence"] = 1.0, ["geometry_coordinate_system"] = "part_model", ["evidence"] = entity.TryGetValue("role_evidence", out var evidence) ? evidence : Array.Empty() }; CopyIfExists(entity, face, "original_component", "original_component_id"); CopySignature(entity, face, hasFace ? "face_signature" : "edge_signature"); if (counterpart != null) { var counterpartRoles = ToStringList(counterpart.TryGetValue("role_candidates", out var counterpartRolesObj) ? counterpartRolesObj : null); string counterpartRole = ChooseVerifiedFaceRole(counterpart, entity, SafeInt(() => Convert.ToInt32(mate.mateType)), counterpartRoles); string counterpartSideTag = InferSideTag(counterpart); face["counterpart_component_id"] = ReadDictString(counterpart, "component"); CopyIfExists(counterpart, face, "original_component", "original_counterpart_component_id"); face["counterpart_part_name"] = ReadDictString(counterpart, "part_name"); face["counterpart_part_path"] = ReadDictString(counterpart, "component_path"); face["counterpart_entity_kind"] = ReadDictString(counterpart, "entity_kind"); face["counterpart_role"] = counterpartRole; face["counterpart_side_tag"] = counterpartSideTag; face["counterpart_port_role"] = string.IsNullOrWhiteSpace(counterpartSideTag) ? counterpartRole : $"{counterpartRole}:{counterpartSideTag}"; } if (TryReadMateDistanceMm(mate, out double distanceMm)) face["distance_mm"] = distanceMm; if (TryReadMateDistanceFlipDimension(mate, out bool flipDimension)) face["flip_dimension"] = flipDimension; mateFaces.Add(face); } HarmonizeMateFaceSides(mateFaces); faces.AddRange(mateFaces); } catch { } } return DeduplicateDictionaries(faces, FunctionalFaceKey); } static void CopySignature(Dictionary entity, Dictionary face, string signatureKey) { if (!entity.TryGetValue(signatureKey, out var sigObj) || sigObj is not Dictionary sig) return; CopyIfExists(sig, face, "surface_type", "surface_type"); CopyIfExists(sig, face, "curve_type", "curve_type"); CopyIfExists(sig, face, "area_mm2", "area_mm2"); CopyIfExists(sig, face, "center_mm", "center_mm"); CopyIfExists(sig, face, "start_mm", "start_mm"); CopyIfExists(sig, face, "end_mm", "end_mm"); CopyIfExists(sig, face, "line_point_mm", "line_point_mm"); CopyIfExists(sig, face, "line_direction", "line_direction"); CopyIfExists(sig, face, "length_mm", "length_mm"); CopyIfExists(sig, face, "box_mm", "bbox_mm"); CopyIfExists(sig, face, "axis", "axis"); CopyIfExists(sig, face, "normal", "normal"); CopyIfExists(sig, face, "origin_mm", "origin_mm"); CopyIfExists(sig, face, "axis_point_mm", "axis_point_mm"); CopyIfExists(sig, face, "axis_line_point_mm", "axis_line_point_mm"); CopyIfExists(sig, face, "axial_center_mm", "axial_center_mm"); CopyIfExists(sig, face, "axial_min_mm", "axial_min_mm"); CopyIfExists(sig, face, "axial_max_mm", "axial_max_mm"); CopyIfExists(sig, face, "radius_mm", "radius_mm"); CopyIfExists(sig, face, "source_reference", "source_reference"); CopyIfExists(sig, face, "source_feature", "solidworks_source_feature"); if (sig.TryGetValue("radius_mm", out var radiusObj)) { try { face["diameter_mm"] = R(Convert.ToDouble(radiusObj) * 2.0); } catch { } } } static string FunctionalFaceKey(Dictionary face) { return string.Join("|", ReadDictString(face, "source_mate"), ReadDictString(face, "mate_entity_index"), ReadDictString(face, "component_id"), ReadDictString(face, "part_path"), ReadDictString(face, "role"), ReadDictString(face, "side_tag"), ReadDictString(face, "surface_type"), ReadDictString(face, "curve_type"), ReadDictString(face, "area_mm2"), ReadDictString(face, "center_mm"), ReadDictString(face, "axis"), ReadDictString(face, "normal")); } static void BackfillPartFunctionalKnowledge(List parts, List> functionalFaces) { foreach (var part in parts) { var related = functionalFaces .Where(face => SamePath(ReadDictString(face, "part_path"), part.FilePath) || string.Equals(ReadDictString(face, "part_name"), part.PartName, StringComparison.OrdinalIgnoreCase)) .ToList(); part.FunctionalFaces = DeduplicateDictionaries(related.Select(face => { var result = new Dictionary { ["face_role"] = ReadDictString(face, "role"), ["surface_type"] = ReadDictString(face, "surface_type"), ["side_tag"] = ReadDictString(face, "side_tag"), ["source"] = "verified_from_assembly_mate", ["verified"] = true, ["confidence"] = 1.0, ["source_assembly_component"] = ReadDictString(face, "component_id"), ["source_mate"] = ReadDictString(face, "source_mate"), ["mate_entity_index"] = ReadDictInt(face, "mate_entity_index", -1), ["source_feature"] = ReadDictString(face, "source_feature"), ["solidworks_source_feature"] = ReadDictString(face, "solidworks_source_feature") }; CopyIfExists(face, result, "original_component_id", "original_source_assembly_component"); CopyIfExists(face, result, "radius_mm", "radius_mm"); CopyIfExists(face, result, "diameter_mm", "diameter_mm"); CopyIfExists(face, result, "area_mm2", "area_mm2"); CopyIfExists(face, result, "center_mm", "center_mm"); CopyIfExists(face, result, "mate_point_mm", "mate_point_mm"); CopyIfExists(face, result, "distance_mm", "distance_mm"); CopyIfExists(face, result, "flip_dimension", "flip_dimension"); CopyIfExists(face, result, "axis", "axis"); CopyIfExists(face, result, "normal", "normal"); CopyIfExists(face, result, "origin_mm", "origin_mm"); CopyIfExists(face, result, "axis_point_mm", "axis_point_mm"); CopyIfExists(face, result, "axis_line_point_mm", "axis_line_point_mm"); CopyIfExists(face, result, "axial_center_mm", "axial_center_mm"); CopyIfExists(face, result, "axial_min_mm", "axial_min_mm"); CopyIfExists(face, result, "axial_max_mm", "axial_max_mm"); CopyIfExists(face, result, "bbox_mm", "bbox_mm"); CopyIfExists(face, result, "geometry_coordinate_system", "geometry_coordinate_system"); CopyIfExists(face, result, "source_reference", "source_reference"); CopyIfExists(face, result, "evidence", "evidence"); return result; }), PartFaceKey); part.MatePorts = DeduplicateDictionaries(related.Select(face => { var port = new Dictionary { ["port_role"] = ReadDictString(face, "port_role"), ["local_face_role"] = ReadDictString(face, "role"), ["local_side_tag"] = ReadDictString(face, "side_tag"), ["mate_type"] = ReadDictString(face, "mate_type"), ["alignment"] = ReadDictString(face, "alignment"), ["counterpart_part_name"] = ReadDictString(face, "counterpart_part_name"), ["counterpart_part_path"] = ReadDictString(face, "counterpart_part_path"), ["counterpart_face_role"] = ReadDictString(face, "counterpart_role"), ["counterpart_side_tag"] = ReadDictString(face, "counterpart_side_tag"), ["counterpart_port_role"] = ReadDictString(face, "counterpart_port_role"), ["source"] = "verified_from_assembly_mate", ["verified"] = true, ["confidence"] = 1.0, ["source_assembly_component"] = ReadDictString(face, "component_id"), ["source_mate"] = ReadDictString(face, "source_mate"), ["mate_entity_index"] = ReadDictInt(face, "mate_entity_index", -1) }; CopyIfExists(face, port, "original_component_id", "original_source_assembly_component"); CopyIfExists(face, port, "distance_mm", "distance_mm"); CopyIfExists(face, port, "flip_dimension", "flip_dimension"); return port; }), PartPortKey); } } static void NormalizeStandardPartFunctionalFacePaths(List parts, List> functionalFaces) { foreach (var face in functionalFaces) { var part = FindPartForFace(parts, face, "part_path", "part_name"); if (part?.IsStandardPart == true) { string originalPath = ReadDictString(face, "part_path"); string insertPath = StandardPartInsertPath(part); if (!string.IsNullOrWhiteSpace(originalPath) && !SamePath(originalPath, insertPath)) face["original_part_path"] = originalPath; face["part_path"] = insertPath; face["standard_part"] = true; face["standard_rule"] = part.StandardRule; face["direct_insert_path"] = insertPath; } var counterpart = FindPartForFace(parts, face, "counterpart_part_path", "counterpart_part_name"); if (counterpart?.IsStandardPart == true) { string originalPath = ReadDictString(face, "counterpart_part_path"); string insertPath = StandardPartInsertPath(counterpart); if (!string.IsNullOrWhiteSpace(originalPath) && !SamePath(originalPath, insertPath)) face["original_counterpart_part_path"] = originalPath; face["counterpart_part_path"] = insertPath; face["counterpart_standard_part"] = true; face["counterpart_direct_insert_path"] = insertPath; } } } static PartKnowledge FindPartForFace(List parts, Dictionary face, string pathKey, string nameKey) { string path = ReadDictString(face, pathKey); string name = ReadDictString(face, nameKey); return parts.FirstOrDefault(part => SamePath(path, part.FilePath) || SamePath(path, part.DirectInsertPath) || string.Equals(name, part.PartName, StringComparison.OrdinalIgnoreCase)); } static void PersistEnrichedPartModelingPlans(List parts, string assemblyName, string asmPath) { foreach (var part in parts) { if (string.IsNullOrWhiteSpace(part.ModelingPlanPath) || !File.Exists(part.ModelingPlanPath)) continue; var enriched = new { schemaVersion = "part_modeling_plan_with_functional_faces.v1", partName = part.PartName, partPath = part.FilePath, sourceAssembly = new { name = assemblyName, path = asmPath }, modelingPlan = ExtractModelingPlanPayload(part.ModelingPlan), functionalFaces = part.FunctionalFaces, matePorts = part.MatePorts, note = "functionalFaces and matePorts are verified from assembly mates and written back to the executable part modeling plan for knowledge-base reuse." }; File.WriteAllText(part.ModelingPlanPath, JsonSerializer.Serialize(enriched, JsonOptions)); part.ModelingPlan = ReadJsonOrNull(part.ModelingPlanPath); } } static object ExtractModelingPlanPayload(object modelingPlan) { if (modelingPlan is JsonElement root && root.ValueKind == JsonValueKind.Object && root.TryGetProperty("modelingPlan", out var plan) && plan.ValueKind == JsonValueKind.Object) { return plan.Clone(); } return modelingPlan; } static void AddGeneratedStep(List steps, ref int index, string skillName, string name, string description, Dictionary args) { string id = $"assembly-generated-{index:D4}"; index++; steps.Add(new { id, targetAgent = "solidworks", skillName, name, description, arguments = args, dependencies = Array.Empty(), acceptance = new Dictionary() }); } static List ExtractModelingSteps(object modelingPlan) { var steps = new List(); if (modelingPlan is not JsonElement root) return steps; if (!root.TryGetProperty("modelingPlan", out var plan) || plan.ValueKind != JsonValueKind.Object) return steps; if (!plan.TryGetProperty("steps", out var stepArray) || stepArray.ValueKind != JsonValueKind.Array) return steps; var createdRefPlanes = CollectCreatedRefPlaneNames(stepArray); foreach (var step in stepArray.EnumerateArray()) steps.Add(NormalizeExecutablePartStep(step, createdRefPlanes)); return steps; } static HashSet CollectCreatedRefPlaneNames(JsonElement stepArray) { var names = new HashSet(StringComparer.OrdinalIgnoreCase); if (stepArray.ValueKind != JsonValueKind.Array) return names; foreach (var step in stepArray.EnumerateArray()) { string skillName = step.TryGetProperty("skillName", out var skill) ? skill.GetString() ?? "" : ""; if (!string.Equals(skillName, "create_offset_plane_mm", StringComparison.OrdinalIgnoreCase) && !string.Equals(skillName, "create_angle_plane_by_edge_and_face", StringComparison.OrdinalIgnoreCase)) continue; if (step.TryGetProperty("arguments", out var args) && args.ValueKind == JsonValueKind.Object && args.TryGetProperty("plane_name", out var plane)) { string planeName = plane.GetString() ?? ""; if (!string.IsNullOrWhiteSpace(planeName)) names.Add(planeName); } } return names; } static object NormalizeExecutablePartStep(JsonElement step, HashSet createdRefPlanes) { string skillName = step.TryGetProperty("skillName", out var skill) ? skill.GetString() ?? "" : ""; if (!string.Equals(skillName, "create_ref_plane_sketch_by_name", StringComparison.OrdinalIgnoreCase)) return step.Clone(); string planeName = ""; if (step.TryGetProperty("arguments", out var args) && args.ValueKind == JsonValueKind.Object && args.TryGetProperty("plane_name", out var plane)) { planeName = plane.GetString() ?? ""; } return step.Clone(); } static void CopyFaceSignature(Dictionary entity, Dictionary target) { if (!entity.TryGetValue("face_signature", out var sigObj) || sigObj is not Dictionary sig) return; CopyIfExists(sig, target, "surface_type", "surface_type"); CopyIfExists(sig, target, "area_mm2", "area_mm2"); CopyIfExists(sig, target, "center_mm", "center_mm"); CopyIfExists(sig, target, "axis", "axis"); CopyIfExists(sig, target, "normal", "normal"); CopyIfExists(sig, target, "origin_mm", "origin_mm"); CopyIfExists(sig, target, "axis_point_mm", "axis_point_mm"); CopyIfExists(sig, target, "axis_line_point_mm", "axis_line_point_mm"); CopyIfExists(sig, target, "axial_center_mm", "axial_center_mm"); CopyIfExists(sig, target, "axial_min_mm", "axial_min_mm"); CopyIfExists(sig, target, "axial_max_mm", "axial_max_mm"); CopyIfExists(sig, target, "radius_mm", "radius_mm"); CopyIfExists(sig, target, "box_mm", "bbox_mm"); CopyIfExists(sig, target, "geometry_coordinate_system", "geometry_coordinate_system"); CopyIfExists(sig, target, "source_reference", "source_reference"); CopyIfExists(sig, target, "source_feature", "solidworks_source_feature"); } static void CopyIfExists(Dictionary source, Dictionary target, string sourceKey, string targetKey) { if (source.TryGetValue(sourceKey, out var value) && value != null) target[targetKey] = value; } static string ChoosePrimaryRole(List roles) { string[] preferred = { "oil_ring_mount_face", "oil_ring_axial_stop_face", "bearing_mount_face", "key_contact_or_keyway_face", "key_mate_entity", "distance_mate_face", "gear_mate_axis", "cylindrical_mate_face", "planar_contact_face", "coincident_mate_entity" }; foreach (string role in preferred) if (roles.Any(item => string.Equals(item, role, StringComparison.OrdinalIgnoreCase))) return role; return roles.FirstOrDefault() ?? ""; } static string ChooseVerifiedFaceRole(Dictionary entity, Dictionary counterpart, int mateType, List roles) { bool thisIsKey = IsKeyLike(entity); bool otherIsKey = counterpart != null && IsKeyLike(counterpart); bool thisIsOilRing = IsOilRingLike(entity); bool otherIsOilRing = counterpart != null && IsOilRingLike(counterpart); if (mateType == (int)swMateType_e.swMateCONCENTRIC && (thisIsKey || otherIsKey)) return thisIsKey ? "key_end_arc_face" : "keyway_end_arc_face"; if (mateType == (int)swMateType_e.swMateCOINCIDENT && (thisIsKey || otherIsKey)) return thisIsKey ? KeyPlaneRole(entity) : KeywayPlaneRole(entity); if (mateType == (int)swMateType_e.swMateCONCENTRIC && (thisIsOilRing || otherIsOilRing)) return thisIsOilRing ? "oil_ring_inner_cylindrical_face" : "oil_ring_mount_face"; if (mateType == (int)swMateType_e.swMateCOINCIDENT && (thisIsOilRing || otherIsOilRing)) return thisIsOilRing ? "oil_ring_planar_contact_face" : "oil_ring_axial_stop_face"; if (mateType == (int)swMateType_e.swMateDISTANCE) return "distance_mate_face"; if (mateType == (int)swMateType_e.swMateGEAR) return "gear_mate_axis"; return ChoosePrimaryRole(roles); } static string InferSideTag(Dictionary entity) { var center = SignatureDoubleArray(entity, "center_mm"); if (center.Length < 3) return ""; if (string.Equals(SignatureString(entity, "surface_type"), "plane", StringComparison.OrdinalIgnoreCase)) { int planeAxis = PlanarConstantAxis(entity); if (planeAxis >= 0) return SideName(planeAxis, center[planeAxis]); int normalAxis = DominantNormalAxis(entity); if (normalAxis >= 0) return SideName(normalAxis, center[normalAxis]); } int axis = 0; double best = Math.Abs(center[0]); for (int i = 1; i < 3; i++) { double value = Math.Abs(center[i]); if (value > best) { best = value; axis = i; } } if (best < 1e-6) return ""; double signed = center[axis]; return SideName(axis, signed); } static int DominantNormalAxis(Dictionary entity) { var normal = SignatureDoubleArray(entity, "normal"); if (normal.Length < 3) return -1; int axis = 0; double best = Math.Abs(normal[0]); for (int i = 1; i < 3; i++) { double value = Math.Abs(normal[i]); if (value > best) { best = value; axis = i; } } return best >= 0.7 ? axis : -1; } static string KeyPlaneRole(Dictionary entity) { int axis = PlanarConstantAxis(entity); return axis switch { 1 => "key_side_contact_face", 2 => "key_seating_face", _ => "key_planar_contact_face" }; } static string KeywayPlaneRole(Dictionary entity) { int axis = PlanarConstantAxis(entity); return axis switch { 0 => "keyway_side_contact_face", 1 => "keyway_bottom_contact_face", 2 => "keyway_depth_contact_face", _ => "keyway_planar_contact_face" }; } static string SideName(int axis, double signed) { return axis switch { 0 => signed >= 0 ? "right" : "left", 1 => signed >= 0 ? "front" : "back", 2 => signed >= 0 ? "top" : "bottom", _ => "" }; } static int PlanarConstantAxis(Dictionary entity) { var box = SignatureDoubleArray(entity, "box_mm"); if (box.Length < 6) return -1; var extents = new[] { Math.Abs(box[3] - box[0]), Math.Abs(box[4] - box[1]), Math.Abs(box[5] - box[2]) }; int axis = 0; double best = extents[0]; for (int i = 1; i < 3; i++) { if (extents[i] < best) { best = extents[i]; axis = i; } } return best <= 0.05 ? axis : -1; } static void HarmonizeMateFaceSides(List> mateFaces) { if (mateFaces.Count != 2) return; string sideA = ReadDictString(mateFaces[0], "side_tag"); string sideB = ReadDictString(mateFaces[1], "side_tag"); string roleA = ReadDictString(mateFaces[0], "role"); string roleB = ReadDictString(mateFaces[1], "role"); HarmonizeSideTags(roleA, roleB, ref sideA, ref sideB); SetSideTag(mateFaces[0], roleA, sideA); SetSideTag(mateFaces[1], roleB, sideB); } static void HarmonizeSideTags(string roleA, string roleB, ref string sideA, ref string sideB) { bool keyArcA = string.Equals(roleA, "key_end_arc_face", StringComparison.OrdinalIgnoreCase); bool keyArcB = string.Equals(roleB, "key_end_arc_face", StringComparison.OrdinalIgnoreCase); bool keywayArcA = string.Equals(roleA, "keyway_end_arc_face", StringComparison.OrdinalIgnoreCase); bool keywayArcB = string.Equals(roleB, "keyway_end_arc_face", StringComparison.OrdinalIgnoreCase); if (keyArcA && keywayArcB && IsLeftRight(sideA)) sideB = sideA; else if (keyArcB && keywayArcA && IsLeftRight(sideB)) sideA = sideB; } static bool IsLeftRight(string value) { return string.Equals(value, "left", StringComparison.OrdinalIgnoreCase) || string.Equals(value, "right", StringComparison.OrdinalIgnoreCase); } static void SetSideTag(Dictionary face, string role, string sideTag) { if (string.IsNullOrWhiteSpace(sideTag)) { face.Remove("side_tag"); face["port_role"] = role; return; } face["side_tag"] = sideTag; face["port_role"] = $"{role}:{sideTag}"; } static double[] SignatureDoubleArray(Dictionary entity, string key) { if (!entity.TryGetValue("face_signature", out var sigObj) || sigObj is not Dictionary sig) { if (!entity.TryGetValue("edge_signature", out sigObj) || sigObj is not Dictionary edgeSig) return Array.Empty(); else { sig = edgeSig; } } return sig.TryGetValue(key, out var value) ? ToDoubleArray(value) : Array.Empty(); } static string SignatureString(Dictionary entity, string key) { if (!entity.TryGetValue("face_signature", out var sigObj) || sigObj is not Dictionary sig) { if (!entity.TryGetValue("edge_signature", out sigObj) || sigObj is not Dictionary edgeSig) return ""; else { sig = edgeSig; } } return sig.TryGetValue(key, out var value) ? value?.ToString() ?? "" : ""; } static bool IsKeyLike(Dictionary entity) { string text = EntityText(entity); return ContainsAny(text, "\u952e", "key", "1096"); } static bool IsOilRingLike(Dictionary entity) { string text = EntityText(entity); return ContainsAny(text, "\u6321\u6cb9\u73af", "oil", "ring"); } static string EntityText(Dictionary entity) { return $"{ReadDictString(entity, "component")} {ReadDictString(entity, "part_name")} {ReadDictString(entity, "component_path")}"; } static string ReadDictString(Dictionary dict, string key) { if (dict == null || !dict.TryGetValue(key, out var value) || value == null) return ""; return value.ToString() ?? ""; } static int ReadDictInt(Dictionary dict, string key, int fallback = -1) { if (dict == null || !dict.TryGetValue(key, out var value) || value == null) return fallback; try { return Convert.ToInt32(value); } catch { return int.TryParse(value.ToString(), out int parsed) ? parsed : fallback; } } static double ReadDictDouble(Dictionary dict, string key, double fallback = 0.0) { if (dict == null || !dict.TryGetValue(key, out var value) || value == null) return fallback; try { if (value is double d) return d; if (value is float f) return f; if (value is int i) return i; if (value is long l) return l; if (value is decimal m) return (double)m; return double.TryParse(value.ToString(), out double parsed) ? parsed : fallback; } catch { return fallback; } } static double[] ReadDictDoubleArray(Dictionary dict, string key) { if (dict == null || !dict.TryGetValue(key, out var value) || value == null) return Array.Empty(); try { if (value is double[] doubles) return doubles; if (value is IEnumerable doubleEnumerable) return doubleEnumerable.ToArray(); if (value is Array array) return array.Cast() .Select(item => item == null ? (double?)null : double.TryParse(item.ToString(), out double parsed) ? parsed : null) .Where(item => item.HasValue) .Select(item => item!.Value) .ToArray(); if (value is IEnumerable objectEnumerable) return objectEnumerable .Select(item => item == null ? (double?)null : double.TryParse(item.ToString(), out double parsed) ? parsed : null) .Where(item => item.HasValue) .Select(item => item!.Value) .ToArray(); } catch { } return Array.Empty(); } static bool TryReadMateDistanceMm(object mate, out double distanceMm) { distanceMm = 0.0; if (mate == null) return false; try { object value = mate.GetType().GetProperty("distanceMm")?.GetValue(mate); if (value == null) return false; if (value is double direct) { distanceMm = direct; return !double.IsNaN(distanceMm) && !double.IsInfinity(distanceMm); } distanceMm = Convert.ToDouble(value); return !double.IsNaN(distanceMm) && !double.IsInfinity(distanceMm); } catch { return false; } } static bool TryReadMateDistanceFlipDimension(object mate, out bool flipDimension) { flipDimension = false; if (mate == null) return false; try { object value = mate.GetType().GetProperty("distanceFlipDimension")?.GetValue(mate) ?? mate.GetType().GetProperty("flipDimension")?.GetValue(mate); if (value == null) return false; if (value is bool direct) { flipDimension = direct; return true; } if (value is int i) { flipDimension = i != 0; return true; } if (value is long l) { flipDimension = l != 0; return true; } if (value is double d) { flipDimension = Math.Abs(d) > 1e-12; return !double.IsNaN(d) && !double.IsInfinity(d); } if (bool.TryParse(value.ToString(), out bool parsedBool)) { flipDimension = parsedBool; return true; } if (double.TryParse(value.ToString(), out double parsedNumber)) { flipDimension = Math.Abs(parsedNumber) > 1e-12; return !double.IsNaN(parsedNumber) && !double.IsInfinity(parsedNumber); } } catch { } return false; } static bool TryReadGearMateInfo(object mate, out double ratioNumerator, out double ratioDenominator, out bool reverse) { ratioNumerator = 0.0; ratioDenominator = 0.0; reverse = false; if (mate == null) return false; try { object gearObj = mate.GetType().GetProperty("gearMate")?.GetValue(mate); if (gearObj is not Dictionary gear || gear.Count == 0) return false; ratioNumerator = ReadDictDouble(gear, "ratio_numerator"); ratioDenominator = ReadDictDouble(gear, "ratio_denominator"); reverse = ReadDictBool(gear, "reverse"); return !double.IsNaN(ratioNumerator) && !double.IsNaN(ratioDenominator) && !double.IsInfinity(ratioNumerator) && !double.IsInfinity(ratioDenominator) && Math.Abs(ratioNumerator) > 1e-9 && Math.Abs(ratioDenominator) > 1e-9; } catch { return false; } } static bool ReadDictBool(Dictionary dict, string key, bool fallback = false) { if (dict == null || !dict.TryGetValue(key, out var value) || value == null) return fallback; try { if (value is bool b) return b; if (value is int i) return i != 0; if (value is long l) return l != 0; if (value is double d) return Math.Abs(d) > 1e-12; return bool.TryParse(value.ToString(), out bool parsed) ? parsed : fallback; } catch { return fallback; } } static bool SamePath(string a, string b) { if (string.IsNullOrWhiteSpace(a) || string.IsNullOrWhiteSpace(b)) return false; try { return string.Equals(Path.GetFullPath(a), Path.GetFullPath(b), StringComparison.OrdinalIgnoreCase); } catch { return string.Equals(a, b, StringComparison.OrdinalIgnoreCase); } } static List> DeduplicateDictionaries(IEnumerable> items, Func, string> keySelector) { var result = new List>(); var seen = new HashSet(StringComparer.OrdinalIgnoreCase); foreach (var item in items) { string key = keySelector(item); if (seen.Add(key)) result.Add(item); } return result; } static string PartFaceKey(Dictionary face) { return string.Join("|", ReadDictString(face, "face_role"), ReadDictString(face, "side_tag"), ReadDictString(face, "surface_type"), ReadDictString(face, "radius_mm"), ReadDictString(face, "center_mm"), ReadDictString(face, "source_mate"), ReadDictString(face, "mate_entity_index")); } static string PartPortKey(Dictionary port) { return string.Join("|", ReadDictString(port, "local_face_role"), ReadDictString(port, "local_side_tag"), ReadDictString(port, "mate_type"), ReadDictString(port, "counterpart_part_name"), ReadDictString(port, "counterpart_face_role"), ReadDictString(port, "counterpart_side_tag"), ReadDictString(port, "source_mate"), ReadDictString(port, "mate_entity_index")); } static List ToStringList(object value) { if (value == null) return new List(); if (value is List typed) return typed; if (value is IEnumerable strings) return strings.ToList(); if (value is Array array) return array.Cast().Select(item => item?.ToString() ?? "").Where(item => item.Length > 0).ToList(); return new List { value.ToString() ?? "" }.Where(item => item.Length > 0).ToList(); } static string MateTypeForSkill(string mateTypeName) { if (mateTypeName.Contains("CONCENTRIC", StringComparison.OrdinalIgnoreCase)) return "Concentric"; if (mateTypeName.Contains("COINCIDENT", StringComparison.OrdinalIgnoreCase)) return "Coincident"; if (mateTypeName.Contains("DISTANCE", StringComparison.OrdinalIgnoreCase)) return "Distance"; if (mateTypeName.Contains("GEAR", StringComparison.OrdinalIgnoreCase)) return "Gear"; if (mateTypeName.Contains("PARALLEL", StringComparison.OrdinalIgnoreCase)) return "Parallel"; if (mateTypeName.Contains("PERPENDICULAR", StringComparison.OrdinalIgnoreCase)) return "Perpendicular"; if (mateTypeName.Contains("TANGENT", StringComparison.OrdinalIgnoreCase)) return "Tangent"; return ""; } static string AlignmentForSkill(string alignmentName) { if (alignmentName.Contains("ANTI", StringComparison.OrdinalIgnoreCase)) return "AntiAligned"; if (alignmentName.Contains("CLOSEST", StringComparison.OrdinalIgnoreCase)) return "Closest"; return "Aligned"; } static string RebuiltPartPath(string partName) { return Path.Combine(GetSwagentOutputRoot(), MakeSafeFileName(partName) + ".SLDPRT"); } static string ResolveInsertPartPath(PartKnowledge part, string workflowId) { if (part.IsStandardPart) return StandardPartInsertPath(part); if (!string.IsNullOrWhiteSpace(part.RebuiltOutputPath) && File.Exists(part.RebuiltOutputPath)) return part.RebuiltOutputPath; string existing = LatestRebuiltPartOutput(part.PartName); if (!string.IsNullOrWhiteSpace(existing)) return existing; return SyntheticRebuiltPartPath(part.PartName, workflowId); } static string LatestRebuiltPartOutput(string partName) { try { string outputRoot = GetSwagentOutputRoot(); if (!Directory.Exists(outputRoot)) return ""; string fileBase = MakeSafeFileName(partName); return Directory.GetFiles(outputRoot, $"{fileBase}_*.SLDPRT") .Concat(Directory.GetFiles(outputRoot, $"{fileBase}.SLDPRT")) .OrderByDescending(path => File.GetLastWriteTime(path)) .FirstOrDefault(path => File.Exists(path)) ?? ""; } catch { return ""; } } static string SyntheticPartFileBase(string partName, string workflowId) { return MakeSafeFileName(partName); } static string SyntheticRebuiltPartPath(string partName, string workflowId) { return Path.Combine(GetSwagentOutputRoot(), SyntheticPartFileBase(partName, workflowId) + ".SLDPRT"); } static string GetSwagentOutputRoot() { string dir = System.Environment.CurrentDirectory; for (int i = 0; i < 8 && !string.IsNullOrWhiteSpace(dir); i++) { if (File.Exists(Path.Combine(dir, "project_paths.json"))) return Path.Combine(dir, "SWagent", "runtime", "outputs"); dir = Directory.GetParent(dir)?.FullName ?? ""; } return Path.Combine(System.Environment.CurrentDirectory, "SWagent", "runtime", "outputs"); } static List ExtractMates(ModelDoc2 doc) { var mates = new List(); var mateSnapshots = CollectMateFeatureSnapshots(doc); int estimatedTotal = mateSnapshots.Count; var estimatedMateNames = mateSnapshots.Select(item => item.MateName).ToList(); string totalText = estimatedTotal > 0 ? estimatedTotal.ToString() : "?"; Log($"[mates] estimated_total={totalText}"); int extractedMates = 0; int skippedDuplicateMates = 0; var seenMateKeys = new HashSet(StringComparer.OrdinalIgnoreCase); var stopwatch = Stopwatch.StartNew(); foreach (var snapshot in mateSnapshots) { var feat = snapshot.Feature; var mate = snapshot.Mate; string mateName = snapshot.MateName; int mateType = SafeInt(() => mate.Type); int alignment = SafeInt(() => mate.Alignment); double? distanceMm = ExtractMateDistanceMm(feat, mateType); bool? distanceFlipDimension = ExtractMateDistanceFlipDimension(feat, mateType); var gearMate = ExtractGearMateInfo(feat, mateType); var entities = new List>(); string mateKey = FeatureIdentityKey(feat); if (!seenMateKeys.Add(mateKey)) { skippedDuplicateMates++; if (skippedDuplicateMates % 10 == 1) Log($"[mates] skip_duplicate count={skippedDuplicateMates}, current={mateName}, scanned_features={snapshot.ScannedFeatureIndex}"); continue; } int count = SafeInt(() => mate.GetMateEntityCount()); for (int i = 0; i < count; i++) { object entityObj = SafeObj(() => mate.MateEntity(i)); if (entityObj is not MateEntity2 mateEntity) continue; var entity = ReadMateEntity(mateEntity); entity["mate_entity_index"] = i; entities.Add(entity); } AnnotateMateEntityRoles(entities, mateType); extractedMates++; Log($"[mates] {ProgressBar(extractedMates, estimatedTotal)} extracted={extractedMates}/{totalText}, current={mateName}, type={MateTypeName(mateType)}, entities={entities.Count}, scanned_features={snapshot.ScannedFeatureIndex}, elapsed={stopwatch.Elapsed:mm\\:ss}"); mates.Add(new { mateName, mateType, mateTypeName = MateTypeName(mateType), distanceMm, distanceFlipDimension, gearMate, alignment, alignmentName = AlignmentName(alignment), entities }); } var extractedNames = new HashSet( mates.Select(item => Safe(() => Convert.ToString(item.GetType().GetProperty("mateName")?.GetValue(item)) ?? "")), StringComparer.OrdinalIgnoreCase); var missingNames = estimatedMateNames.Where(name => !extractedNames.Contains(name)).ToList(); if (missingNames.Count > 0) Log("[mates] missing_estimated_names=" + string.Join(", ", missingNames)); int scannedFeatures = mateSnapshots.Count == 0 ? 0 : mateSnapshots.Max(item => item.ScannedFeatureIndex); Log($"[mates] completed extracted={mates.Count}/{totalText}, skipped_duplicates={skippedDuplicateMates}, scanned_features={scannedFeatures}, elapsed={stopwatch.Elapsed:mm\\:ss}"); return mates; } static List CollectMateFeatureSnapshots(ModelDoc2 doc) { var snapshots = new List(); int scannedFeatures = 0; foreach (var feat in WalkFeatures(doc.FirstFeature() as Feature)) { scannedFeatures++; if (scannedFeatures % 100 == 0) Log($"[mates] scanning_features={scannedFeatures}, collected={snapshots.Count}"); object specific = SafeObj(() => feat.GetSpecificFeature2()); if (specific is not Mate2 mate) continue; snapshots.Add(new MateFeatureSnapshot { Feature = feat, Mate = mate, MateName = Safe(() => feat.Name), ScannedFeatureIndex = scannedFeatures }); } Log($"[mates] collected_snapshot_count={snapshots.Count}, scanned_features={scannedFeatures}"); return snapshots; } static double? ExtractMateDistanceMm(Feature feat, int mateType) { if (mateType != (int)swMateType_e.swMateDISTANCE) return null; var values = new List(); try { for (var displayDimension = SafeObj(() => feat.GetFirstDisplayDimension()) as DisplayDimension; displayDimension != null; displayDimension = SafeObj(() => feat.GetNextDisplayDimension(displayDimension)) as DisplayDimension) { var dimension = SafeObj(() => displayDimension.GetDimension()) as Dimension; if (dimension == null) continue; if (!TryReadComDouble(dimension, "SystemValue", out double valueM)) continue; if (double.IsNaN(valueM) || double.IsInfinity(valueM)) continue; values.Add(Math.Abs(valueM) * 1000.0); } } catch { } if (values.Count == 0) return null; return R(values[0]); } static bool? ExtractMateDistanceFlipDimension(Feature feat, int mateType) { if (mateType != (int)swMateType_e.swMateDISTANCE) return null; try { object definitionObj = SafeObj(() => feat.GetDefinition()); if (definitionObj == null) return null; foreach (string name in new[] { "FlipDimension", "Flip", "ReverseDimension", "ReverseDirection" }) { if (TryReadComBool(definitionObj, name, out bool value)) return value; } } catch { } return null; } static Dictionary ExtractGearMateInfo(Feature feat, int mateType) { var result = new Dictionary(); if (mateType != (int)swMateType_e.swMateGEAR) return result; try { object definitionObj = SafeObj(() => feat.GetDefinition()); if (definitionObj is IGearMateFeatureData gearData) { double numerator = SafeDouble(() => gearData.GearRatioNumerator, double.NaN); double denominator = SafeDouble(() => gearData.GearRatioDenominator, double.NaN); if (!double.IsNaN(numerator) && !double.IsNaN(denominator) && !double.IsInfinity(numerator) && !double.IsInfinity(denominator) && Math.Abs(numerator) > 1e-9 && Math.Abs(denominator) > 1e-9) { result["ratio_numerator"] = R(numerator); result["ratio_denominator"] = R(denominator); result["reverse"] = SafeBool(() => gearData.Reverse); result["source"] = "IGearMateFeatureData"; } } } catch { } return result; } static void NormalizeMateComponentIds(List mates, IReadOnlyDictionary componentIdMap) { if (mates.Count == 0 || componentIdMap.Count == 0) return; int updated = 0; int missing = 0; foreach (dynamic mate in mates) { try { foreach (var entityObj in mate.entities) { if (entityObj is not Dictionary entity) continue; string original = ReadDictString(entity, "component"); if (string.IsNullOrWhiteSpace(original)) continue; if (!componentIdMap.TryGetValue(original, out string normalized) || string.IsNullOrWhiteSpace(normalized)) { missing++; continue; } if (!entity.ContainsKey("original_component")) entity["original_component"] = original; entity["component"] = normalized; updated++; } } catch { } } Log($"[assembly] normalized_mate_entities={updated}, unmatched_mate_entities={missing}"); } static int EstimateMateCount(ModelDoc2 doc) { try { var keys = new HashSet(StringComparer.OrdinalIgnoreCase); foreach (var feat in WalkFeatures(doc.FirstFeature() as Feature)) { object specific = SafeObj(() => feat.GetSpecificFeature2()); if (specific is not Mate2 mate) continue; string mateName = Safe(() => feat.Name); keys.Add(mateName + "|" + MateIdentityKey(mate)); } return keys.Count; } catch (Exception ex) { Log("[mates] estimate_total_failed=" + ex.Message); return 0; } } static List EstimateMateNames(ModelDoc2 doc) { var names = new List(); try { var keys = new HashSet(StringComparer.OrdinalIgnoreCase); foreach (var feat in WalkFeatures(doc.FirstFeature() as Feature)) { object specific = SafeObj(() => feat.GetSpecificFeature2()); if (specific is not Mate2 mate) continue; string mateName = Safe(() => feat.Name); string key = mateName + "|" + MateIdentityKey(mate); if (keys.Add(key)) names.Add(mateName); } } catch (Exception ex) { Log("[mates] estimate_names_failed=" + ex.Message); } return names; } static string MateIdentityKey(Mate2 mate) { var parts = new List { SafeInt(() => mate.Type).ToString(), SafeInt(() => mate.Alignment).ToString() }; int count = SafeInt(() => mate.GetMateEntityCount()); for (int i = 0; i < count; i++) { object entityObj = SafeObj(() => mate.MateEntity(i)); if (entityObj is not MateEntity2 entity) continue; var comp = SafeObj(() => entity.ReferenceComponent) as Component2; parts.Add($"{Safe(() => comp?.Name2 ?? "")}:{Safe(() => comp?.GetPathName() ?? "")}:{SafeInt(() => entity.ReferenceType)}:{SafeInt(() => entity.ReferenceType2)}"); } return string.Join("|", parts.OrderBy(x => x, StringComparer.OrdinalIgnoreCase)); } static object[] ToObjectArray(object value) { if (value == null) return Array.Empty(); if (value is object[] objects) return objects; if (value is Array array) return array.Cast().ToArray(); return new[] { value }; } static string ProgressBar(int current, int total) { if (total <= 0) return "[????????????????????]"; const int width = 20; double percent = Math.Min(100.0, Math.Max(0.0, current * 100.0 / total)); int filled = Math.Max(0, Math.Min(width, (int)Math.Round(percent * width / 100.0))); string suffix = current > total ? "+" : ""; return "[" + new string('#', filled) + new string('-', width - filled) + $"] {percent:0.0}%{suffix}"; } static IEnumerable WalkFeatures(Feature feature) { var visited = new HashSet(StringComparer.OrdinalIgnoreCase); foreach (var item in WalkFeatures(feature, visited)) yield return item; } static IEnumerable WalkFeatures(Feature feature, HashSet visited) { for (Feature f = feature; f != null; f = SafeObj(() => f.GetNextFeature()) as Feature) { string key = FeatureIdentityKey(f); if (!visited.Add(key)) continue; yield return f; for (Feature sub = SafeObj(() => f.GetFirstSubFeature()) as Feature; sub != null; sub = SafeObj(() => sub.GetNextSubFeature()) as Feature) foreach (var nested in WalkFeatures(sub, visited)) yield return nested; } } static string FeatureIdentityKey(Feature feature) { try { IntPtr ptr = Marshal.GetIUnknownForObject(feature); try { if (ptr != IntPtr.Zero) return ptr.ToString(); } finally { if (ptr != IntPtr.Zero) Marshal.Release(ptr); } } catch { } return $"{Safe(() => feature.GetTypeName2())}|{Safe(() => feature.Name)}"; } static Dictionary ReadMateEntity(MateEntity2 mateEntity) { var comp = SafeObj(() => mateEntity.ReferenceComponent) as Component2; object reference = SafeObj(() => mateEntity.Reference); var result = new Dictionary { ["component"] = comp == null ? "" : Safe(() => comp.Name2), ["component_path"] = comp == null ? "" : Safe(() => comp.GetPathName()), ["part_name"] = comp == null ? "" : Path.GetFileNameWithoutExtension(Safe(() => comp.GetPathName())), ["reference_type"] = SafeInt(() => mateEntity.ReferenceType), ["reference_type2"] = SafeInt(() => mateEntity.ReferenceType2), ["entity_kind"] = EntityKind(reference) }; if (reference is Face2 face) result["face_signature"] = FaceSignature(face); else if (reference is Edge edge) result["edge_signature"] = EdgeSignature(edge); else { result["reference_runtime_type"] = reference?.GetType().FullName ?? ""; result["reference_resolution"] = "unresolved_without_face_or_edge"; } return result; } static List> GetComponentCylinderSignatures(Component2 comp) { string key = $"{Safe(() => comp.Name2)}|{Safe(() => comp.GetPathName())}"; if (ComponentCylinderSignatureCache.TryGetValue(key, out var cached)) return cached; var result = new List>(); try { if (comp.GetModelDoc2() is PartDoc partDoc) { foreach (var face in EnumeratePartFaces(partDoc)) { var sig = FaceSignature(face); if (string.Equals(ReadDictString(sig, "surface_type"), "cylinder", StringComparison.OrdinalIgnoreCase)) result.Add(sig); } } } catch { } ComponentCylinderSignatureCache[key] = result; return result; } static IEnumerable EnumeratePartFaces(PartDoc partDoc) { object bodiesObj = partDoc.GetBodies2((int)swBodyType_e.swSolidBody, true); foreach (object bodyObj in ToObjectArray(bodiesObj)) { if (bodyObj is not Body2 body) continue; for (object faceObj = SafeObj(() => body.GetFirstFace()); faceObj != null;) { if (faceObj is not Face2 face) yield break; yield return face; faceObj = SafeObj(() => face.GetNextFace()); } } } static double[] TransformAssemblyPointToPartModel(double[] point, double[] transform) { if (point.Length < 3 || transform.Length < 12) return point; double scale = transform.Length > 12 && Math.Abs(transform[12]) > 1e-12 ? transform[12] : 1.0; double x = (point[0] - transform[9]) / scale; double y = (point[1] - transform[10]) / scale; double z = (point[2] - transform[11]) / scale; return new[] { transform[0] * x + transform[3] * y + transform[6] * z, transform[1] * x + transform[4] * y + transform[7] * z, transform[2] * x + transform[5] * y + transform[8] * z }; } static double[] TransformAssemblyVectorToPartModel(double[] vector, double[] transform) { if (vector.Length < 3 || transform.Length < 9) return Normalize(vector.ElementAtOrDefault(0), vector.ElementAtOrDefault(1), vector.ElementAtOrDefault(2)); double x = vector[0], y = vector[1], z = vector[2]; return Normalize( transform[0] * x + transform[3] * y + transform[6] * z, transform[1] * x + transform[4] * y + transform[7] * z, transform[2] * x + transform[5] * y + transform[8] * z); } static Dictionary FaceSignature(Face2 face) { var result = new Dictionary { ["geometry_coordinate_system"] = "part_model" }; try { result["area_mm2"] = R(SafeDouble(() => face.GetArea()) * 1_000_000.0); var box = ToDoubleArray(face.GetBox()); if (box.Length >= 6) { var boxMm = box.Take(6).Select(v => R(v * 1000.0)).ToArray(); result["box_mm"] = boxMm; result["center_mm"] = new[] { R((boxMm[0] + boxMm[3]) / 2.0), R((boxMm[1] + boxMm[4]) / 2.0), R((boxMm[2] + boxMm[5]) / 2.0) }; } if (face.GetFeature() is Feature feat) result["source_feature"] = Safe(() => feat.Name); if (face.GetSurface() is Surface surface) { string surfaceType = SurfaceKind(surface); result["surface_type"] = surfaceType; if (surfaceType == "cylinder") { var cp = ToDoubleArray(GetComProperty(surface, "CylinderParams")); if (cp.Length >= 7) { result["origin_mm"] = new[] { R(cp[0] * 1000.0), R(cp[1] * 1000.0), R(cp[2] * 1000.0) }; result["axis_point_mm"] = new[] { R(cp[0] * 1000.0), R(cp[1] * 1000.0), R(cp[2] * 1000.0) }; result["axis"] = new[] { R(cp[3]), R(cp[4]), R(cp[5]) }; result["radius_mm"] = R(cp[6] * 1000.0); AddCylinderAxialSignature(result); } } else if (surfaceType == "plane") { var pp = ToDoubleArray(GetComProperty(surface, "PlaneParams")); if (pp.Length >= 6) result["normal"] = Normalize(pp[0], pp[1], pp[2]); } } } catch { } return result; } static Dictionary EdgeSignature(Edge edge) { var result = new Dictionary { ["geometry_coordinate_system"] = "part_model" }; try { var start = VertexPointMm(SafeObj(() => edge.GetStartVertex()) as Vertex); var end = VertexPointMm(SafeObj(() => edge.GetEndVertex()) as Vertex); if (start.Length >= 3) result["start_mm"] = start.Select(R).ToArray(); if (end.Length >= 3) result["end_mm"] = end.Select(R).ToArray(); if (start.Length >= 3 && end.Length >= 3) { result["center_mm"] = new[] { R((start[0] + end[0]) / 2.0), R((start[1] + end[1]) / 2.0), R((start[2] + end[2]) / 2.0) }; result["length_mm"] = R(Distance(start, end)); } Curve curve = SafeObj(() => edge.GetCurve()) as Curve; if (curve == null) return result; if (SafeBool(() => curve.IsCircle())) { result["curve_type"] = "circle"; var cp = ToDoubleArray(GetComProperty(curve, "CircleParams")); if (cp.Length >= 7) { result["center_mm"] = new[] { R(cp[0] * 1000.0), R(cp[1] * 1000.0), R(cp[2] * 1000.0) }; result["axis"] = new[] { R(cp[3]), R(cp[4]), R(cp[5]) }; result["radius_mm"] = R(cp[6] * 1000.0); } } else if (SafeBool(() => curve.IsLine())) { result["curve_type"] = "line"; var lp = ToDoubleArray(GetComProperty(curve, "LineParams")); if (lp.Length >= 6) { result["line_point_mm"] = new[] { R(lp[0] * 1000.0), R(lp[1] * 1000.0), R(lp[2] * 1000.0) }; result["line_direction"] = Normalize(lp[3], lp[4], lp[5]); } } else { result["curve_type"] = "other"; } } catch { } return result; } static void AddCylinderAxialSignature(Dictionary signature) { var axis = NormalizeRaw(ReadVector(signature, "axis")); var axisPoint = ReadVector(signature, "axis_point_mm"); if (axisPoint.Length < 3) axisPoint = ReadVector(signature, "origin_mm"); if (axis.Length < 3 || axisPoint.Length < 3) return; signature["axis"] = axis.Select(R).ToArray(); signature["axis_line_point_mm"] = axisPoint.Select(R).ToArray(); var center = ReadVector(signature, "center_mm"); if (center.Length >= 3) signature["axial_center_mm"] = R(Dot(center, axis)); var box = ReadVector(signature, "box_mm"); if (box.Length < 6) box = ReadVector(signature, "bbox_mm"); if (box.Length < 6) return; double min = double.PositiveInfinity; double max = double.NegativeInfinity; for (int xi = 0; xi < 2; xi++) for (int yi = 0; yi < 2; yi++) for (int zi = 0; zi < 2; zi++) { var p = new[] { box[xi == 0 ? 0 : 3], box[yi == 0 ? 1 : 4], box[zi == 0 ? 2 : 5] }; double t = Dot(p, axis); min = Math.Min(min, t); max = Math.Max(max, t); } if (!double.IsInfinity(min) && !double.IsInfinity(max)) { signature["axial_min_mm"] = R(min); signature["axial_max_mm"] = R(max); if (!signature.ContainsKey("axial_center_mm")) signature["axial_center_mm"] = R((min + max) / 2.0); } } static double[] VertexPointMm(Vertex vertex) { if (vertex == null) return Array.Empty(); try { var values = ToDoubleArray(vertex.GetPoint()); if (values.Length >= 3) return new[] { values[0] * 1000.0, values[1] * 1000.0, values[2] * 1000.0 }; } catch { } return Array.Empty(); } static double Distance(double[] a, double[] b) { if (a.Length < 3 || b.Length < 3) return 0.0; double dx = a[0] - b[0]; double dy = a[1] - b[1]; double dz = a[2] - b[2]; return Math.Sqrt(dx * dx + dy * dy + dz * dz); } static double DistancePointToLine(double[] point, double[] linePoint, double[] lineDirection) { var dir = NormalizeRaw(lineDirection); if (point.Length < 3 || linePoint.Length < 3 || dir.Length < 3) return double.PositiveInfinity; var delta = new[] { point[0] - linePoint[0], point[1] - linePoint[1], point[2] - linePoint[2] }; double projection = Dot(delta, dir); var closest = new[] { linePoint[0] + dir[0] * projection, linePoint[1] + dir[1] * projection, linePoint[2] + dir[2] * projection }; return Distance(point, closest); } static double Dot(double[] a, double[] b) { if (a.Length < 3 || b.Length < 3) return 0.0; return a[0] * b[0] + a[1] * b[1] + a[2] * b[2]; } static double[] NormalizeRaw(double[] values) { if (values.Length < 3) return Array.Empty(); double len = Math.Sqrt(values[0] * values[0] + values[1] * values[1] + values[2] * values[2]); return len <= 1e-12 ? Array.Empty() : new[] { values[0] / len, values[1] / len, values[2] / len }; } static double[] ReadVector(Dictionary source, string key) { return source.TryGetValue(key, out var value) ? ToDoubleArray(value) : Array.Empty(); } static double? ReadNullableDouble(Dictionary source, string key) { try { if (!source.TryGetValue(key, out var value) || value == null) return null; double result = Convert.ToDouble(value); return double.IsNaN(result) || double.IsInfinity(result) ? null : result; } catch { return null; } } static void AnnotateMateEntityRoles(List> entities, int mateType) { foreach (var entity in entities) { string thisText = ((entity.TryGetValue("component", out var c) ? c : "") + " " + (entity.TryGetValue("part_name", out var p) ? p : "")).ToString() ?? ""; string otherText = string.Join(" ", entities.Where(e => !ReferenceEquals(e, entity)).Select(e => $"{(e.TryGetValue("component", out var oc) ? oc : "")} {(e.TryGetValue("part_name", out var op) ? op : "")}")); var roles = new List(); string surfaceType = ""; if (entity.TryGetValue("face_signature", out var sigObj) && sigObj is Dictionary sig) surfaceType = sig.TryGetValue("surface_type", out var st) ? st?.ToString() ?? "" : ""; if (mateType == (int)swMateType_e.swMateCONCENTRIC) { roles.Add("cylindrical_mate_face"); if (ContainsAny(otherText, "\u6321\u6cb9\u73af", "oil", "ring")) roles.Add("oil_ring_mount_face"); if (ContainsAny(otherText, "\u8f74\u627f", "bearing")) roles.Add("bearing_mount_face"); } if (mateType == (int)swMateType_e.swMateCOINCIDENT) { roles.Add(surfaceType == "plane" ? "planar_contact_face" : "coincident_mate_entity"); if (ContainsAny(otherText, "\u952e", "key")) roles.Add("key_contact_or_keyway_face"); if (ContainsAny(otherText, "\u6321\u6cb9\u73af", "oil", "ring")) roles.Add("oil_ring_axial_stop_face"); } if (mateType == (int)swMateType_e.swMateDISTANCE) roles.Add("distance_mate_face"); if (mateType == (int)swMateType_e.swMateGEAR) roles.Add("gear_mate_axis"); if (ContainsAny(otherText, "\u952e", "key") && !roles.Contains("key_mate_entity")) roles.Add("key_mate_entity"); entity["role_candidates"] = roles.Distinct(StringComparer.OrdinalIgnoreCase).ToList(); entity["role_evidence"] = new[] { $"mate_type={MateTypeName(mateType)}", $"entity_kind={entity["entity_kind"]}", $"surface_type={surfaceType}", $"other_component_text={otherText}" }; } } static List BuildMatePorts(List mates) { var ports = new List(); foreach (dynamic mate in mates) { try { var entities = new List>(); foreach (var entity in mate.entities) if (entity is Dictionary dict) entities.Add(dict); if (entities.Count != 2) continue; var rolesA = ToStringList(entities[0].TryGetValue("role_candidates", out var rolesObjA) ? rolesObjA : null); var rolesB = ToStringList(entities[1].TryGetValue("role_candidates", out var rolesObjB) ? rolesObjB : null); string roleA = ChooseVerifiedFaceRole(entities[0], entities[1], SafeInt(() => Convert.ToInt32(mate.mateType)), rolesA); string roleB = ChooseVerifiedFaceRole(entities[1], entities[0], SafeInt(() => Convert.ToInt32(mate.mateType)), rolesB); if (string.IsNullOrWhiteSpace(roleA) || string.IsNullOrWhiteSpace(roleB)) continue; string sideA = InferSideTag(entities[0]); string sideB = InferSideTag(entities[1]); HarmonizeSideTags(roleA, roleB, ref sideA, ref sideB); var port = new Dictionary { ["sourceMate"] = mate.mateName, ["mateType"] = MateTypeForSkill(Convert.ToString(mate.mateTypeName) ?? ""), ["alignment"] = AlignmentForSkill(Convert.ToString(mate.alignmentName) ?? ""), ["a"] = new { component = ReadDictString(entities[0], "component"), originalComponent = ReadDictString(entities[0], "original_component"), part = ReadDictString(entities[0], "part_name"), faceRole = roleA, sideTag = sideA, portRole = string.IsNullOrWhiteSpace(sideA) ? roleA : $"{roleA}:{sideA}", entityKind = ReadDictString(entities[0], "entity_kind") }, ["b"] = new { component = ReadDictString(entities[1], "component"), originalComponent = ReadDictString(entities[1], "original_component"), part = ReadDictString(entities[1], "part_name"), faceRole = roleB, sideTag = sideB, portRole = string.IsNullOrWhiteSpace(sideB) ? roleB : $"{roleB}:{sideB}", entityKind = ReadDictString(entities[1], "entity_kind") } }; if (TryReadMateDistanceMm(mate, out double distanceMm)) port["distanceMm"] = distanceMm; ports.Add(port); } catch { } } return ports; } static object ReadJsonOrNull(string path) { try { if (string.IsNullOrWhiteSpace(path) || !File.Exists(path)) return null; using var doc = JsonDocument.Parse(File.ReadAllText(path)); return doc.RootElement.Clone(); } catch { return null; } } static string LatestFile(string dir, string pattern) { try { return Directory.EnumerateFiles(dir, pattern) .OrderByDescending(File.GetLastWriteTime) .FirstOrDefault() ?? ""; } catch { return ""; } } static string PreferredOrLatestFile(string dir, string preferredFileName, string fallbackPattern) { try { string preferred = Path.Combine(dir, preferredFileName); if (File.Exists(preferred)) return preferred; } catch { } return LatestFile(dir, fallbackPattern); } static bool IsForcePartExtractionEnabled() { string value = System.Environment.GetEnvironmentVariable("SWAGENT_FORCE_PART_EXTRACTION") ?? ""; return string.Equals(value, "1", StringComparison.OrdinalIgnoreCase) || string.Equals(value, "true", StringComparison.OrdinalIgnoreCase) || string.Equals(value, "yes", StringComparison.OrdinalIgnoreCase); } static string SurfaceKind(Surface surface) { if (surface == null) return ""; try { if (surface.IsPlane()) return "plane"; if (surface.IsCylinder()) return "cylinder"; if (surface.IsCone()) return "cone"; if (surface.IsSphere()) return "sphere"; } catch { } return "unknown"; } static string EntityKind(object reference) { if (reference is Face2) return "face"; if (reference is Edge) return "edge"; if (reference is Vertex) return "vertex"; if (reference is RefPlane) return "ref_plane"; if (reference is Feature) return "feature"; return reference?.GetType().Name ?? ""; } static string MateTypeName(int mateType) { try { return ((swMateType_e)mateType).ToString(); } catch { return mateType.ToString(); } } static string AlignmentName(int alignment) { try { return ((swMateAlign_e)alignment).ToString(); } catch { return alignment.ToString(); } } static string ParentName(Component2 c) { try { if (c.GetParent() is Component2 parent) return Safe(() => parent.Name2); } catch { } return ""; } static bool ContainsAny(string text, params string[] keys) { if (string.IsNullOrWhiteSpace(text)) return false; return keys.Any(k => text.Contains(k, StringComparison.OrdinalIgnoreCase)); } static bool IsGbStandardPart(params string[] values) { return values.Any(value => !string.IsNullOrWhiteSpace(value) && value.Contains("GB", StringComparison.OrdinalIgnoreCase)); } static string StandardPartInsertPath(PartKnowledge part) { if (!string.IsNullOrWhiteSpace(part.DirectInsertPath) && File.Exists(part.DirectInsertPath)) return part.DirectInsertPath; return part.FilePath; } static string ResolveStandardPartSourcePath(string partName, string originalPath, List instances) { try { if (!Directory.Exists(StandardPartSourceRoot)) return originalPath; var names = new List(); AddStandardPartSearchName(names, partName); AddStandardPartSearchName(names, Path.GetFileNameWithoutExtension(originalPath)); foreach (string instance in instances ?? new List()) AddStandardPartSearchName(names, ComponentBaseName(instance)); foreach (string name in names.Distinct(StringComparer.OrdinalIgnoreCase)) { foreach (string ext in new[] { ".SLDPRT", ".sldprt", ".SLDASM", ".sldasm" }) { string candidate = Path.Combine(StandardPartSourceRoot, name + ext); if (File.Exists(candidate)) return Path.GetFullPath(candidate); } } var files = Directory.EnumerateFiles(StandardPartSourceRoot, "*.*", SearchOption.TopDirectoryOnly) .Where(path => string.Equals(Path.GetExtension(path), ".SLDPRT", StringComparison.OrdinalIgnoreCase) || string.Equals(Path.GetExtension(path), ".SLDASM", StringComparison.OrdinalIgnoreCase)) .ToList(); foreach (string name in names.Distinct(StringComparer.OrdinalIgnoreCase)) { string matched = files.FirstOrDefault(path => string.Equals(Path.GetFileNameWithoutExtension(path), name, StringComparison.OrdinalIgnoreCase)) ?? ""; if (!string.IsNullOrWhiteSpace(matched)) return Path.GetFullPath(matched); } foreach (string name in names.Distinct(StringComparer.OrdinalIgnoreCase)) { string matched = files.FirstOrDefault(path => Path.GetFileNameWithoutExtension(path).Contains(name, StringComparison.OrdinalIgnoreCase) || name.Contains(Path.GetFileNameWithoutExtension(path), StringComparison.OrdinalIgnoreCase)) ?? ""; if (!string.IsNullOrWhiteSpace(matched)) return Path.GetFullPath(matched); } } catch { } return originalPath; } static void AddStandardPartSearchName(List names, string value) { string name = ComponentBaseName(value); if (!string.IsNullOrWhiteSpace(name)) names.Add(name); } static string ComponentBaseName(string value) { if (string.IsNullOrWhiteSpace(value)) return ""; string name = Path.GetFileNameWithoutExtension(value.Trim()); int slash = name.IndexOf('/'); if (slash >= 0) name = name[..slash]; int dash = name.LastIndexOf('-'); if (dash > 0 && dash < name.Length - 1 && name[(dash + 1)..].All(char.IsDigit)) name = name[..dash]; return name.Trim(); } static string MakeSafeFileName(string value) { string name = string.IsNullOrWhiteSpace(value) ? "assembly" : value.Trim(); foreach (char c in Path.GetInvalidFileNameChars()) name = name.Replace(c, '_'); return name; } static string MakeAssemblyStepFileBase(string assemblyName) { string name = string.IsNullOrWhiteSpace(assemblyName) ? "assembly" : assemblyName.Trim(); if (string.Equals(Path.GetExtension(name), ".SLDASM", StringComparison.OrdinalIgnoreCase)) name = Path.GetFileNameWithoutExtension(name); return MakeSafeFileName(name); } static string Quote(string value) => "\"" + value.Replace("\"", "\\\"") + "\""; static string Tail(string value, int max) { if (string.IsNullOrEmpty(value) || value.Length <= max) return value ?? ""; return value[^max..]; } static string SafeTaskResult(Task task) { try { return task.Wait(10_000) ? task.Result : ""; } catch { return ""; } } static void InitDiagnosticLog(string asmPath) { try { string dir = Path.GetDirectoryName(asmPath) ?? System.Environment.CurrentDirectory; string fileBase = MakeSafeFileName(Path.GetFileNameWithoutExtension(asmPath)); DiagnosticLogPath = Path.Combine(dir, $"{fileBase}_assembly_audit_trace_{DateTime.Now:yyyyMMdd_HHmmss}.log"); File.WriteAllText(DiagnosticLogPath, "", Encoding.UTF8); Log("[main] diagnostic_log=" + DiagnosticLogPath); Log("[main] assembly_path=" + asmPath); } catch { } } static void Log(string message) { string line = $"{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff} {message}"; try { Console.WriteLine(message); } catch { } try { if (!string.IsNullOrWhiteSpace(DiagnosticLogPath)) File.AppendAllText(DiagnosticLogPath, line + System.Environment.NewLine, Encoding.UTF8); } catch { } } static double R(double v) => Math.Round(v, 6); static double[] Normalize(double x, double y, double z) { double len = Math.Sqrt(x * x + y * y + z * z); return len <= 1e-12 ? Array.Empty() : new[] { R(x / len), R(y / len), R(z / len) }; } static object GetComProperty(object obj, string name) { try { return obj?.GetType().InvokeMember(name, System.Reflection.BindingFlags.GetProperty, null, obj, null); } catch { return null; } } static bool TryReadComDouble(object obj, string name, out double value) { value = 0.0; try { object raw = GetComProperty(obj, name); if (raw == null) return false; value = Convert.ToDouble(raw); return !double.IsNaN(value) && !double.IsInfinity(value); } catch { return false; } } static bool TryReadComBool(object obj, string name, out bool value) { value = false; try { object raw = GetComProperty(obj, name); if (raw == null) return false; if (raw is bool direct) { value = direct; return true; } if (raw is int i) { value = i != 0; return true; } if (raw is long l) { value = l != 0; return true; } if (raw is double d) { value = Math.Abs(d) > 1e-12; return !double.IsNaN(d) && !double.IsInfinity(d); } if (bool.TryParse(raw.ToString(), out bool parsedBool)) { value = parsedBool; return true; } if (double.TryParse(raw.ToString(), out double parsedNumber)) { value = Math.Abs(parsedNumber) > 1e-12; return !double.IsNaN(parsedNumber) && !double.IsInfinity(parsedNumber); } } catch { } return false; } static object InvokeIfExists(object obj, string name, params object[] args) { try { return obj?.GetType().InvokeMember( name, System.Reflection.BindingFlags.InvokeMethod, null, obj, args); } catch { return null; } } static double[] ToDoubleArray(object value) { try { if (value is double[] typed) return typed; if (value is Array array) { var list = new List(); foreach (object item in array) if (item != null) list.Add(Convert.ToDouble(item)); return list.ToArray(); } } catch { } return Array.Empty(); } static string Safe(Func f, string d = "") { try { return f() ?? d; } catch { return d; } } static object SafeObj(Func f) { try { return f(); } catch { return null; } } static int SafeInt(Func f, int d = 0) { try { return f(); } catch { return d; } } static double SafeDouble(Func f, double d = 0) { try { return f(); } catch { return d; } } static bool SafeBool(Func f, bool d = false) { try { return f(); } catch { return d; } } static void SafeAction(Action action) { try { action(); } catch { } } }