3747 lines
151 KiB
C#
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 { } }
|
|
}
|
|
|