Files
2026-07-17 17:45:56 +08:00

3747 lines
151 KiB
C#

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<string, List<Dictionary<string, object>>> 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<PartKnowledge>();
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<double>();
}
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<string> Instances { get; set; } = new();
public List<string> 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<Dictionary<string, object>> FunctionalFaces { get; set; } = new();
public List<Dictionary<string, object>> MatePorts { get; set; } = new();
}
sealed class AssemblyComponentPatternInfo
{
public string PatternName { get; set; } = "";
public string PatternType { get; set; } = "";
public string FeatureTypeName { get; set; } = "";
public List<string> SeedComponents { get; set; } = new();
public List<string> GeneratedComponents { get; set; } = new();
public List<string> SkippedItems { get; set; } = new();
public Dictionary<string, object> Parameters { get; set; } = new();
}
static List<ComponentInfo> ReadComponents(AssemblyDoc assy, string assemblyName)
{
var result = new List<ComponentInfo>();
foreach (object item in assy.GetComponents(false) as object[] ?? Array.Empty<object>())
{
if (item is not Component2 comp) continue;
string 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<string, string> NormalizeComponentInstanceIds(List<ComponentInfo> components)
{
var map = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
var counters = new Dictionary<string, int>(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<AssemblyComponentPatternInfo> ReadAssemblyComponentPatterns(ModelDoc2 doc, IReadOnlyDictionary<string, string> componentIdMap)
{
var result = new List<AssemblyComponentPatternInfo>();
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<string> target, IReadOnlyDictionary<string, string> 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<string> target, IReadOnlyDictionary<string, string> 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<string> target, string name, IReadOnlyDictionary<string, string> 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<string, object> parameters, IReadOnlyDictionary<string, string> componentIdMap)
{
if (parameters.Count == 0 || componentIdMap.Count == 0)
return;
foreach (var value in parameters.Values)
NormalizeReferenceComponentIds(value, componentIdMap);
}
static void NormalizeReferenceComponentIds(object value, IReadOnlyDictionary<string, string> componentIdMap)
{
if (value is Dictionary<string, object> dict)
{
NormalizeReferenceComponentId(dict, "owning_component", componentIdMap);
foreach (var nested in dict.Values)
NormalizeReferenceComponentIds(nested, componentIdMap);
return;
}
if (value is List<Dictionary<string, object>> dictList)
{
foreach (var item in dictList)
NormalizeReferenceComponentIds(item, componentIdMap);
return;
}
if (value is IEnumerable<object> items && value is not string)
{
foreach (var item in items)
NormalizeReferenceComponentIds(item, componentIdMap);
}
}
static void NormalizeReferenceComponentId(Dictionary<string, object> dict, string key, IReadOnlyDictionary<string, string> 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<string, object> parameters, IReadOnlyDictionary<string, string> componentIdMap)
{
var names = new List<string>();
ReadPatternComponents(featureData, propertyName, names, componentIdMap);
if (names.Count > 0)
parameters[propertyName + "Components"] = names;
}
static void ReadPatternValueArray(object featureData, Dictionary<string, object> parameters, string propertyName)
{
object value = GetComProperty(featureData, propertyName);
if (value == null)
return;
var values = new List<object>();
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<string, object> 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<string, object> 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<string, object> 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<string, object> 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<string, object> 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<string, object> parameters, string key, int totalInstances, Func<int, object> getTransform)
{
if (totalInstances <= 0)
return;
var transforms = new List<Dictionary<string, object>>();
for (int i = 0; i <= totalInstances; i++)
{
object transform = getTransform(i);
var values = MathTransformArray(transform);
if (values.Length == 0)
continue;
transforms.Add(new Dictionary<string, object>
{
["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<double>();
object raw = GetComProperty(transform, "ArrayData");
var values = ToDoubleArray(raw);
return values.Length > 0 ? values.Select(R).ToArray() : Array.Empty<double>();
}
static void ReadPatternSelectionReferences(ModelDoc2 doc, Dictionary<string, object> parameters)
{
var selections = new List<Dictionary<string, object>>();
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<string, object> 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<Dictionary<string, object>> 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<string, object> 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<string, object> 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<string, object> 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<string, object> 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<object>();
instanceComponents.AddRange(originComponents);
instanceComponents.AddRange(selectionComponents);
var componentOrientations = new List<object>();
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<object>());
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<object> ReadObjectList(Dictionary<string, object> values, string key)
{
if (!values.TryGetValue(key, out object value) || value == null)
return new List<object>();
if (value is IEnumerable<object> objectItems)
return objectItems.Where(x => x != null).ToList();
if (value is System.Collections.IEnumerable enumerable && value is not string)
{
var result = new List<object>();
foreach (object item in enumerable)
{
if (item != null)
result.Add(item);
}
return result;
}
return new List<object> { value };
}
static void EnsureParameter(Dictionary<string, object> values, string key, object defaultValue)
{
if (!values.ContainsKey(key) || values[key] == null)
values[key] = defaultValue;
}
static string ReadStringParameter(Dictionary<string, object> values, string key)
{
return values.TryGetValue(key, out object value) ? value?.ToString() ?? "" : "";
}
static int ReadIntParameter(Dictionary<string, object> 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<string, object> 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<string, object> DescribeReference(object reference)
{
var result = new Dictionary<string, object>(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<string, object> 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<ComponentInfo> components, List<AssemblyComponentPatternInfo> 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<AssemblyComponentPatternInfo> 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<string> instances, List<string> 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<string> instances, List<string> 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<object> BuildExecutableSteps(
string workflowId,
string assemblyName,
List<ComponentInfo> components,
List<PartKnowledge> parts,
List<AssemblyComponentPatternInfo> componentPatterns,
List<object> mates,
List<Dictionary<string, object>> functionalFaces)
{
var steps = new List<object>();
int generatedIndex = 1;
AddGeneratedStep(
steps,
ref generatedIndex,
"clear_functional_faces",
"clear_functional_faces",
"寮€濮嬭閰嶅墠娓呯┖涓存椂鍔熻兘闈㈠瓨鍌ㄥ簱",
new Dictionary<string, object>());
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<string, object>
{
["file_base_name"] = SyntheticPartFileBase(part.PartName, workflowId)
});
}
AddGeneratedStep(
steps,
ref generatedIndex,
"create_new_assembly",
assemblyName,
"Create new assembly",
new Dictionary<string, object>());
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<string, object>
{
["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<string, object>
{
["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<string, object>(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<Dictionary<string, object>>();
foreach (Dictionary<string, object> 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<string, object>
{
["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<string, object>
{
["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<string, object>
{
["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<string, object> BuildPatternStepArgs(AssemblyComponentPatternInfo pattern)
{
var args = new Dictionary<string, object>(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<Dictionary<string, object>> BuildFunctionalFaceRepository(List<object> mates)
{
var faces = new List<Dictionary<string, object>>();
foreach (dynamic mate in mates)
{
try
{
var entities = new List<Dictionary<string, object>>();
foreach (var entity in mate.entities)
if (entity is Dictionary<string, object> dict)
entities.Add(dict);
if (entities.Count == 0)
continue;
var mateFaces = new List<Dictionary<string, object>>();
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<string, object>;
bool hasEdge = entity.TryGetValue("edge_signature", out var edgeObj) && edgeObj is Dictionary<string, object>;
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<string, object>
{
["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<string>()
};
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<string, object> entity, Dictionary<string, object> face, string signatureKey)
{
if (!entity.TryGetValue(signatureKey, out var sigObj) || sigObj is not Dictionary<string, object> 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<string, object> 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<PartKnowledge> parts, List<Dictionary<string, object>> 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<string, object>
{
["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<string, object>
{
["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<PartKnowledge> parts, List<Dictionary<string, object>> 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<PartKnowledge> parts, Dictionary<string, object> 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<PartKnowledge> 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<object> steps, ref int index, string skillName, string name, string description, Dictionary<string, object> args)
{
string id = $"assembly-generated-{index:D4}";
index++;
steps.Add(new
{
id,
targetAgent = "solidworks",
skillName,
name,
description,
arguments = args,
dependencies = Array.Empty<string>(),
acceptance = new Dictionary<string, object>()
});
}
static List<object> ExtractModelingSteps(object modelingPlan)
{
var steps = new List<object>();
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<string> CollectCreatedRefPlaneNames(JsonElement stepArray)
{
var names = new HashSet<string>(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<string> 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<string, object> entity, Dictionary<string, object> target)
{
if (!entity.TryGetValue("face_signature", out var sigObj) || sigObj is not Dictionary<string, object> 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<string, object> source, Dictionary<string, object> target, string sourceKey, string targetKey)
{
if (source.TryGetValue(sourceKey, out var value) && value != null)
target[targetKey] = value;
}
static string ChoosePrimaryRole(List<string> 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<string, object> entity, Dictionary<string, object> counterpart, int mateType, List<string> 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<string, object> 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<string, object> 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<string, object> 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<string, object> 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<string, object> 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<Dictionary<string, object>> 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<string, object> 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<string, object> entity, string key)
{
if (!entity.TryGetValue("face_signature", out var sigObj) || sigObj is not Dictionary<string, object> sig)
{
if (!entity.TryGetValue("edge_signature", out sigObj) || sigObj is not Dictionary<string, object> edgeSig)
return Array.Empty<double>();
else
{
sig = edgeSig;
}
}
return sig.TryGetValue(key, out var value) ? ToDoubleArray(value) : Array.Empty<double>();
}
static string SignatureString(Dictionary<string, object> entity, string key)
{
if (!entity.TryGetValue("face_signature", out var sigObj) || sigObj is not Dictionary<string, object> sig)
{
if (!entity.TryGetValue("edge_signature", out sigObj) || sigObj is not Dictionary<string, object> edgeSig)
return "";
else
{
sig = edgeSig;
}
}
return sig.TryGetValue(key, out var value) ? value?.ToString() ?? "" : "";
}
static bool IsKeyLike(Dictionary<string, object> entity)
{
string text = EntityText(entity);
return ContainsAny(text, "\u952e", "key", "1096");
}
static bool IsOilRingLike(Dictionary<string, object> entity)
{
string text = EntityText(entity);
return ContainsAny(text, "\u6321\u6cb9\u73af", "oil", "ring");
}
static string EntityText(Dictionary<string, object> entity)
{
return $"{ReadDictString(entity, "component")} {ReadDictString(entity, "part_name")} {ReadDictString(entity, "component_path")}";
}
static string ReadDictString(Dictionary<string, object> dict, string key)
{
if (dict == null || !dict.TryGetValue(key, out var value) || value == null)
return "";
return value.ToString() ?? "";
}
static int ReadDictInt(Dictionary<string, object> 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<string, object> 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<string, object> dict, string key)
{
if (dict == null || !dict.TryGetValue(key, out var value) || value == null)
return Array.Empty<double>();
try
{
if (value is double[] doubles)
return doubles;
if (value is IEnumerable<double> doubleEnumerable)
return doubleEnumerable.ToArray();
if (value is Array array)
return array.Cast<object>()
.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<object> 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<double>();
}
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<string, object> 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<string, object> 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<Dictionary<string, object>> DeduplicateDictionaries(IEnumerable<Dictionary<string, object>> items, Func<Dictionary<string, object>, string> keySelector)
{
var result = new List<Dictionary<string, object>>();
var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
foreach (var item in items)
{
string key = keySelector(item);
if (seen.Add(key))
result.Add(item);
}
return result;
}
static string PartFaceKey(Dictionary<string, object> 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<string, object> 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<string> ToStringList(object value)
{
if (value == null) return new List<string>();
if (value is List<string> typed) return typed;
if (value is IEnumerable<string> strings) return strings.ToList();
if (value is Array array) return array.Cast<object>().Select(item => item?.ToString() ?? "").Where(item => item.Length > 0).ToList();
return new List<string> { 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<object> ExtractMates(ModelDoc2 doc)
{
var mates = new List<object>();
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<string>(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<Dictionary<string, object>>();
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<string>(
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<MateFeatureSnapshot> CollectMateFeatureSnapshots(ModelDoc2 doc)
{
var snapshots = new List<MateFeatureSnapshot>();
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<double>();
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<string, object> ExtractGearMateInfo(Feature feat, int mateType)
{
var result = new Dictionary<string, object>();
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<object> mates, IReadOnlyDictionary<string, string> 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<string, object> 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<string>(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<string> EstimateMateNames(ModelDoc2 doc)
{
var names = new List<string>();
try
{
var keys = new HashSet<string>(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<string>
{
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<object>();
if (value is object[] objects)
return objects;
if (value is Array array)
return array.Cast<object>().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<Feature> WalkFeatures(Feature feature)
{
var visited = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
foreach (var item in WalkFeatures(feature, visited))
yield return item;
}
static IEnumerable<Feature> WalkFeatures(Feature feature, HashSet<string> 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<string, object> ReadMateEntity(MateEntity2 mateEntity)
{
var comp = SafeObj(() => mateEntity.ReferenceComponent) as Component2;
object reference = SafeObj(() => mateEntity.Reference);
var result = new Dictionary<string, object>
{
["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<Dictionary<string, object>> GetComponentCylinderSignatures(Component2 comp)
{
string key = $"{Safe(() => comp.Name2)}|{Safe(() => comp.GetPathName())}";
if (ComponentCylinderSignatureCache.TryGetValue(key, out var cached))
return cached;
var result = new List<Dictionary<string, object>>();
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<Face2> 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<string, object> FaceSignature(Face2 face)
{
var result = new Dictionary<string, object>
{
["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<string, object> EdgeSignature(Edge edge)
{
var result = new Dictionary<string, object>
{
["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<string, object> 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<double>();
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<double>();
}
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>();
double len = Math.Sqrt(values[0] * values[0] + values[1] * values[1] + values[2] * values[2]);
return len <= 1e-12 ? Array.Empty<double>() : new[] { values[0] / len, values[1] / len, values[2] / len };
}
static double[] ReadVector(Dictionary<string, object> source, string key)
{
return source.TryGetValue(key, out var value) ? ToDoubleArray(value) : Array.Empty<double>();
}
static double? ReadNullableDouble(Dictionary<string, object> 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<Dictionary<string, object>> 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>();
string surfaceType = "";
if (entity.TryGetValue("face_signature", out var sigObj) && sigObj is Dictionary<string, object> 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<object> BuildMatePorts(List<object> mates)
{
var ports = new List<object>();
foreach (dynamic mate in mates)
{
try
{
var entities = new List<Dictionary<string, object>>();
foreach (var entity in mate.entities)
if (entity is Dictionary<string, object> 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<string, object>
{
["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<string> instances)
{
try
{
if (!Directory.Exists(StandardPartSourceRoot))
return originalPath;
var names = new List<string>();
AddStandardPartSearchName(names, partName);
AddStandardPartSearchName(names, Path.GetFileNameWithoutExtension(originalPath));
foreach (string instance in instances ?? new List<string>())
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<string> 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<string> 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<double>() : 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<double>();
foreach (object item in array)
if (item != null) list.Add(Convert.ToDouble(item));
return list.ToArray();
}
}
catch { }
return Array.Empty<double>();
}
static string Safe(Func<string> f, string d = "") { try { return f() ?? d; } catch { return d; } }
static object SafeObj(Func<object> f) { try { return f(); } catch { return null; } }
static int SafeInt(Func<int> f, int d = 0) { try { return f(); } catch { return d; } }
static double SafeDouble(Func<double> f, double d = 0) { try { return f(); } catch { return d; } }
static bool SafeBool(Func<bool> f, bool d = false) { try { return f(); } catch { return d; } }
static void SafeAction(Action action) { try { action(); } catch { } }
}