Files
mech-ai/tools/modeling-process/PartFeatureAudit/Program.cs
T
2026-07-17 17:45:56 +08:00

5499 lines
229 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using ClosedXML.Excel;
using SolidWorks.Interop.sldworks;
using SolidWorks.Interop.swconst;
using System.Runtime.InteropServices;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Text.RegularExpressions;
namespace PartFeatureAudit;
internal class Program
{
const int Part = 1, Silent = 1, ReadOnly = 2;
static readonly List<Row> Rows = new();
static readonly List<SkillFlowStep> Skills = new();
static readonly HashSet<IntPtr> SeenSketch = new();
static readonly HashSet<string> EmittedRefPlaneCreation = new(StringComparer.OrdinalIgnoreCase);
static readonly Dictionary<string, int> FeatureStepByName = new(StringComparer.OrdinalIgnoreCase);
static readonly Dictionary<IntPtr, string> SketchPlanNameById = new();
static readonly Dictionary<IntPtr, HashSet<IntPtr>> SketchSelectedContourSegmentIdsBySketchId = new();
static int SketchPlanCounter;
static SldWorks CurrentSwApp;
class Row
{
public string PartName = "", Feature = "", Type = "", Category = "", Sketch = "", SketchPlane = "", Dims = "", Rels = "", Note = "";
public int Step, Depth;
}
class SkillFlowStep
{
[JsonPropertyName("step")]
public int Step { get; set; }
[JsonPropertyName("skill")]
public string Skill { get; set; } = "";
[JsonPropertyName("args")]
public Dictionary<string, object> Args { get; set; } = new();
[JsonPropertyName("source_feature")]
public string SourceFeature { get; set; } = "";
[JsonPropertyName("note")]
public string Note { get; set; } = "";
}
class SkillFlowValidationResult
{
public bool Ok { get; set; } = true;
public int TotalSteps { get; set; }
public List<string> SkillsUsed { get; set; } = new();
public List<string> Errors { get; set; } = new();
public List<string> Warnings { get; set; } = new();
}
class SketchReferenceDebugInfo
{
public string DirectStatus = "";
public string EditSketchStatus = "";
public string RollbackStatus = "";
public string RefetchedSketchStatus = "";
public string FinalEntityType = "";
}
class SketchReferenceResult
{
public string Description = "";
public string EntityType = "";
public int EntityTypeCode;
public string Kind = "unresolved";
public string ReferenceName = "";
public string PersistReferenceHex = "";
public int FaceIndex;
public int HostFeatureStep;
public string HostFeatureName = "";
public string SurfaceType = "";
public double AreaMm2;
public double[] CenterMm = Array.Empty<double>();
public double[] BoxMm = Array.Empty<double>();
public double[] Normal = Array.Empty<double>();
public string PlaneName => !string.IsNullOrWhiteSpace(ReferenceName) ? ReferenceName : Description;
public Dictionary<string, object> ToSkillArgs()
{
var args = new Dictionary<string, object>
{
["reference_kind"] = Kind,
["reference"] = Description
};
if (FaceIndex > 0)
{
args["face_index"] = FaceIndex;
args["face_index_scope"] = "SWagent CommonExecutor.ListFaces order";
args["selection_strategy"] = "create_face_sketch_by_index";
}
if (!string.IsNullOrWhiteSpace(HostFeatureName)) args["host_feature"] = HostFeatureName;
if (HostFeatureStep > 0) args["host_step"] = HostFeatureStep;
if (!string.IsNullOrWhiteSpace(SurfaceType)) args["surface_type"] = SurfaceType;
if (AreaMm2 > 0) args["area_mm2"] = AreaMm2;
if (CenterMm.Length > 0) args["center_mm"] = CenterMm;
if (BoxMm.Length > 0) args["box_mm"] = BoxMm;
if (Normal.Length > 0) args["normal"] = Normal;
if (Normal.Length >= 3)
{
args["normal_x"] = Normal[0];
args["normal_y"] = Normal[1];
args["normal_z"] = Normal[2];
}
if (!string.IsNullOrWhiteSpace(ReferenceName)) args["reference_name"] = ReferenceName;
if (Kind == "datum_plane" && !string.IsNullOrWhiteSpace(PlaneName)) args["plane_name"] = PlaneName;
if (!string.IsNullOrWhiteSpace(PersistReferenceHex)) args["persist_ref_hex"] = PersistReferenceHex;
if (EntityTypeCode != 0) args["entity_type_code"] = EntityTypeCode;
return args;
}
}
class RefPlaneBuildInfo
{
public bool CanCreate;
public string BuildKind = "";
public string PlaneName = "";
public string BaseReferenceKind = "";
public string BaseReferenceName = "";
public int BaseFaceIndex;
public double OffsetMm;
public double OffsetAbsMm;
public bool ReverseDirection;
public bool ReversedReferenceDirection;
public double AngleDeg;
public string AxisName = "";
public List<Dictionary<string, object>> EdgeSignatures = new();
public Dictionary<string, object> FaceSignature = new();
public string Note = "";
}
[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);
[STAThread]
static void Main(string[] args)
{
try
{
string path = FirstPositionalArg(args, @"D:\Desktop\减速器\三维");
string outputDir = ReadOption(args, "--output-dir");
bool useActiveDocument = HasFlag(args, "--active");
Console.WriteLine("正在连接 SolidWorks...");
var sw = Connect() ?? throw new Exception("无法连接 SolidWorks");
if (useActiveDocument)
{
AuditActivePart(sw);
Save(string.IsNullOrWhiteSpace(outputDir) ? System.Environment.CurrentDirectory : outputDir);
}
else if (Directory.Exists(path))
{
foreach (var p in Directory.EnumerateFiles(path, "*.sldprt", SearchOption.AllDirectories)) AuditPartFile(sw, p);
Save(string.IsNullOrWhiteSpace(outputDir) ? path : outputDir);
}
else
{
if (Path.GetExtension(path).ToLowerInvariant() != ".sldprt")
throw new Exception("该程序只支持 .SLDPRT 零件文件或包含零件的目录");
AuditPartFile(sw, path);
Save(string.IsNullOrWhiteSpace(outputDir) ? Path.GetDirectoryName(path) ?? System.Environment.CurrentDirectory : outputDir);
}
}
catch (Exception ex)
{
Console.WriteLine("程序出错:" + ex.Message);
Console.WriteLine(ex);
}
Console.WriteLine("按回车退出...");
if (args.Length == 0)
Console.ReadLine();
}
static string FirstPositionalArg(string[] args, string fallback)
{
for (int i = 0; i < args.Length; i++)
{
if (string.Equals(args[i], "--output-dir", StringComparison.OrdinalIgnoreCase))
{
i++;
continue;
}
if (string.Equals(args[i], "--active", StringComparison.OrdinalIgnoreCase))
continue;
if (!args[i].StartsWith("--", StringComparison.OrdinalIgnoreCase))
return args[i];
}
return fallback;
}
static string ReadOption(string[] args, string name)
{
for (int i = 0; i < args.Length - 1; i++)
if (string.Equals(args[i], name, StringComparison.OrdinalIgnoreCase))
return args[i + 1];
return "";
}
static bool HasFlag(string[] args, string name)
{
return args.Any(arg => string.Equals(arg, name, StringComparison.OrdinalIgnoreCase));
}
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 void AuditPartFile(SldWorks sw, string path)
{
CurrentSwApp = sw;
int e = 0, w = 0;
var doc = sw.OpenDoc6(Path.GetFullPath(path), Part, Silent | ReadOnly, "", ref e, ref w) as ModelDoc2;
if (doc == null) throw new Exception($"OpenDoc6 失败 errors={e}, warnings={w}, path={path}");
string title = Safe(() => doc.GetTitle(), Path.GetFileName(path));
try
{
Console.WriteLine("正在读取零件:" + title);
AuditPart(doc);
}
finally
{
try
{
if (!string.IsNullOrWhiteSpace(title))
sw.CloseDoc(title);
}
catch
{
}
}
}
static void AuditActivePart(SldWorks sw)
{
CurrentSwApp = sw;
var doc = sw.ActiveDoc as ModelDoc2;
if (doc == null)
throw new Exception("No active SolidWorks document found.");
int docType = SafeInt(() => doc.GetType());
if (docType != (int)swDocumentTypes_e.swDocPART)
throw new Exception($"Active SolidWorks document is not a part. documentType={docType}");
string title = Safe(() => doc.GetTitle(), "active_part");
Console.WriteLine("Reading active SolidWorks part: " + title);
AuditPart(doc);
}
static void AuditPart(ModelDoc2 doc)
{
SeenSketch.Clear();
EmittedRefPlaneCreation.Clear();
FeatureStepByName.Clear();
SketchPlanNameById.Clear();
SketchSelectedContourSegmentIdsBySketchId.Clear();
SketchPlanCounter = 0;
string part = Safe(() => doc.GetTitle());
AddSkill("create_new_part", new(), part, "新建零件");
int step = 0;
var pendingTopLevelSketches = new List<Feature>();
Feature lastHolePlacementSketchFeature = null;
foreach (var it in Features(doc).Take(2000))
{
string name = Safe(() => it.f.Name), type = Safe(() => it.f.GetTypeName2());
if (ShouldSkipDefaultFeature(name, type)) continue;
if (IsSketch(type))
{
pendingTopLevelSketches.Add(it.f);
continue;
}
if (!string.IsNullOrWhiteSpace(name) && !FeatureStepByName.ContainsKey(name))
FeatureStepByName[name] = step + 1;
bool isRevolveBoss = IsRevolveBossFeature(name, type, it.f);
bool isRevolveCut = IsRevolveCutFeature(name, type, it.f);
bool isHelix = IsHelixFeature(name, type);
bool isSweep = IsSweepFeature(name, type);
bool isHoleWizard = IsHoleWizardFeature(name, type);
bool isGenericExtrude = !isHelix && !isSweep && !isRevolveBoss && !isRevolveCut &&
(IsExtrude(name, type, false) || IsExtrude(name, type, true));
Feature holePlacementSketchFeature = null;
if (isHoleWizard && pendingTopLevelSketches.Count > 0)
{
Feature placementSketchFeature = pendingTopLevelSketches[^1];
holePlacementSketchFeature = placementSketchFeature;
lastHolePlacementSketchFeature = placementSketchFeature;
pendingTopLevelSketches.Clear();
}
var row = new Row { PartName = part, Step = ++step, Depth = it.depth, Feature = name, Type = type, Category = Cat(name, type) };
bool keepsActiveSketch = isRevolveBoss || isRevolveCut;
if (IsCustomRefPlane(name, type)) AddRefPlaneCreationSkills(doc, it.f, name);
if (!isHoleWizard && isGenericExtrude)
RegisterSelectedContourSegmentFilter(doc, it.f);
if (!isHoleWizard) ReadSubSketches(doc, it.f, row, emitExitSketch: !keepsActiveSketch);
if (isGenericExtrude)
{
bool isCut = IsExtrude(name, type, true);
double depthMm = Depth(it.f);
bool reverseDir = ExtrudeReverseDir(it.f, out string directionSource);
int endCondition = ExtrudeEndCondition(it.f, true);
bool throughAll = IsThroughAllEndCondition(endCondition);
var selectedContours = ExtrudeSelectedContourSignatures(doc, it.f);
row.Dims = AppendText(row.Dims, depthMm > 0 ? $"拉伸深度={R(depthMm)}mm" : "拉伸深度=未读取到");
AddSkill(isCut ? "extrude_cut_mm" : "extrude_boss_mm", new() { ["depth_mm"] = depthMm, ["reverse_dir"] = reverseDir, ["direction_source"] = directionSource }, name, isCut ? "切除拉伸" : "凸台拉伸");
if (Skills.Count > 0)
AddExtrudeDirectionModelArgs(it.f, reverseDir, Skills[^1].Args);
if (Skills.Count > 0 && isCut)
AddExtrudeStartArgs(it.f, Skills[^1].Args);
if (Skills.Count > 0)
Skills[^1].Args.Remove("reverse_dir");
if (Skills.Count > 0)
{
if (endCondition >= 0)
{
Skills[^1].Args["end_condition_code"] = endCondition;
Skills[^1].Args["end_condition"] = EndConditionName(endCondition);
}
if (throughAll) Skills[^1].Args["through_all"] = true;
}
if (selectedContours.Count > 0 && Skills.Count > 0)
{
Skills[^1].Args["selected_contours"] = selectedContours;
Skills[^1].Args["selected_contour_count"] = selectedContours.Count;
}
}
else if (isRevolveBoss)
{
if (TryExtractRevolveBoss(doc, it.f, out var revolveArgs, out string revolveNote))
{
row.Dims = AppendText(row.Dims, revolveNote);
AddSkill("create_revolve_boss_mm", revolveArgs, name, "旋转凸台");
}
else
{
row.Note = AppendText(row.Note, revolveNote);
}
}
else if (isRevolveCut)
{
if (TryExtractRevolveCut(doc, it.f, out var revolveArgs, out string revolveNote))
{
row.Dims = AppendText(row.Dims, revolveNote);
AddSkill("create_revolve_cut_mm", revolveArgs, name, "旋转切除");
}
else
{
row.Note = AppendText(row.Note, revolveNote);
}
}
else if (isHelix)
{
if (TryExtractHelix(it.f, name, type, out var helixArgs, out string helixNote))
{
row.Note = AppendText(row.Note, helixNote);
AddSkill("create_helix_mm", helixArgs, name, "helix");
}
else
{
row.Note = AppendText(row.Note, helixNote);
}
}
else if (IsFilletFeature(name, type))
{
if (TryExtractSimpleFillet(doc, it.f, out var filletArgs, out string filletNote))
{
row.Note = AppendText(row.Note, filletNote);
AddSkill("select_edges_by_signature", new() { ["edge_signatures"] = filletArgs["edge_signatures"], ["source_feature"] = name }, name, "圆角边选择");
AddSkill("fillet_edges_radius_mm", new() { ["radius_mm"] = filletArgs["radius_mm"] }, name, "圆角");
}
else
{
row.Note = AppendText(row.Note, filletNote);
}
}
else if (IsChamferFeature(name, type))
{
if (TryExtractChamfer(doc, it.f, out var chamferArgs, out string chamferNote))
{
row.Note = AppendText(row.Note, chamferNote);
AddSkill("select_edges_by_signature", new() { ["edge_signatures"] = chamferArgs["edge_signatures"], ["source_feature"] = name }, name, "倒角边选择");
AddSkill("chamfer_edges_angle_distance_mm", new()
{
["distance_mm"] = chamferArgs["distance_mm"],
["angle_deg"] = chamferArgs["angle_deg"],
["other_distance_mm"] = chamferArgs["other_distance_mm"],
["chamfer_type"] = chamferArgs["chamfer_type"],
["equal_distance"] = chamferArgs["equal_distance"]
}, name, "倒角");
}
else
{
row.Note = AppendText(row.Note, chamferNote);
}
}
else if (TryExtractCircularPattern(doc, it.f, name, type, out var circularPatternArgs, out string circularPatternNote))
{
row.Note = AppendText(row.Note, circularPatternNote);
AddSkill("circular_pattern_feature", circularPatternArgs, name, "圆周阵列");
}
else if (TryExtractLinearPattern(doc, it.f, name, type, out var linearPatternArgs, out string linearPatternNote))
{
row.Note = AppendText(row.Note, linearPatternNote);
AddSkill("linear_pattern_feature_mm", linearPatternArgs, name, "线性阵列");
}
else if (TryExtractDraft(doc, it.f, name, type, out var draftArgs, out string draftNote))
{
row.Note = AppendText(row.Note, draftNote);
AddSkill("draft_faces_neutral_plane_mm", draftArgs, name, "拔模");
}
else if (TryExtractRib(it.f, name, type, out var ribArgs, out string ribNote))
{
row.Note = AppendText(row.Note, ribNote);
AddSkill("create_rib_mm", ribArgs, name, "筋");
}
else if (TryExtractReferenceAxis(doc, it.f, name, type, out var axisArgs, out string axisNote))
{
row.Note = AppendText(row.Note, axisNote);
AddSkill("create_reference_axis", axisArgs, name, "基准轴");
}
else if (TryExtractShell(doc, it.f, name, type, out var shellArgs, out string shellNote))
{
row.Note = AppendText(row.Note, shellNote);
AddSkill("shell_remove_face_at_point_mm", shellArgs, name, "抽壳");
}
else if (TryExtractHoleWizard(doc, it.f, name, type, out var holeArgs, out string holeNote))
{
if (!HasHoleWizardTargetFace(holeArgs))
{
Feature matchedPlacementSketch = null;
if (holePlacementSketchFeature != null &&
HoleWizardPointsMatchSketch(holePlacementSketchFeature, holeArgs))
matchedPlacementSketch = holePlacementSketchFeature;
else if (lastHolePlacementSketchFeature != null &&
HoleWizardPointsMatchSketch(lastHolePlacementSketchFeature, holeArgs))
matchedPlacementSketch = lastHolePlacementSketchFeature;
if (matchedPlacementSketch != null)
AddHoleWizardTargetFaceFromPlacementSketch(doc, matchedPlacementSketch, holeArgs);
}
row.Note = AppendText(row.Note, holeNote);
AddSkill("hole_wizard_threaded_mm", holeArgs, name, "异形孔向导");
}
else if (TryExtractMirror(doc, it.f, name, type, out var mirrorArgs, out string mirrorNote))
{
row.Note = AppendText(row.Note, mirrorNote);
AddSkill("mirror_feature_about_plane", mirrorArgs, name, "镜像");
}
else if (TryExtractWrap(doc, it.f, name, type, out var wrapArgs, out string wrapNote))
{
row.Note = AppendText(row.Note, wrapNote);
AddSkill("wrap_sketch_on_faces_mm", wrapArgs, name, "包覆");
}
else if (TryExtractSweep(doc, it.f, name, type, out string sweepSkill, out var sweepArgs, out string sweepNote))
{
row.Note = AppendText(row.Note, sweepNote);
AddSkill(sweepSkill, sweepArgs, name, sweepSkill.Contains("cut", StringComparison.OrdinalIgnoreCase) ? "扫描切除" : "扫描凸台");
}
else if (TryExtractLoftOrBoundary(it.f, name, type, out string blendSkill, out var blendArgs, out string blendNote))
{
row.Note = AppendText(row.Note, blendNote);
AddSkill(blendSkill, blendArgs, name, blendSkill.Contains("cut", StringComparison.OrdinalIgnoreCase) ? "放样/边界切除" : "放样/边界凸台");
}
Rows.Add(row);
}
Console.WriteLine($" -> 已读取 {step} 个特征");
}
static IEnumerable<(Feature f, int depth)> Features(ModelDoc2 doc)
{
var seen = new HashSet<IntPtr>();
Feature f = doc.FirstFeature() as Feature;
for (int guard = 0; f != null && guard < 2000; guard++)
{
var id = Id(f);
if (id != IntPtr.Zero && !seen.Add(id)) break;
yield return (f, 0);
f = f.GetNextFeature() as Feature;
}
}
static void ReadSubSketches(ModelDoc2 doc, Feature feat, Row row, bool emitExitSketch = true)
{
Feature sub = feat.GetFirstSubFeature() as Feature;
for (int i = 0; sub != null && i < 200; i++)
{
if (IsSketch(Safe(() => sub.GetTypeName2()))) ReadSketch(doc, sub, row, emitExitSketch);
sub = sub.GetNextSubFeature() as Feature;
}
}
static void ReadSketch(ModelDoc2 doc, Feature feat, Row row, bool emitExitSketch = true)
{
var id = Id(feat); if (id != IntPtr.Zero && !SeenSketch.Add(id)) return;
if (feat.GetSpecificFeature2() is not Sketch sk) { row.Note += "无法转 ISketch"; return; }
string sourceName = Safe(() => feat.Name, row.Feature);
string name = CanonicalSketchPlanName(feat, sourceName);
var debug = new SketchReferenceDebugInfo();
var sketchReference = ResolveSketchPlane(doc, feat, sk, debug);
NormalizeGenericDatumPlaneReference(sk, sketchReference);
row.SketchPlane = sketchReference.Description;
row.Note = AppendText(row.Note, BuildSketchReferenceDebugNote(debug));
EnsureRefPlaneCreationSkills(doc, sketchReference, name);
AddSketchStartSkill(sketchReference, name);
row.Sketch = Segments(doc, sk, name, row.Type);
row.Dims = Dimensions(sk, feat);
row.Rels = Relations(sk);
if (emitExitSketch)
AddSkill("exit_sketch", new(), name, "退出草图");
}
static string CanonicalSketchPlanName(Feature feat, string sourceName)
{
IntPtr id = Id(feat);
if (id != IntPtr.Zero && SketchPlanNameById.TryGetValue(id, out string existing))
return existing;
string canonical = "草图" + (++SketchPlanCounter).ToString(System.Globalization.CultureInfo.InvariantCulture);
if (id != IntPtr.Zero)
SketchPlanNameById[id] = canonical;
return canonical;
}
static SketchReferenceResult ResolveSketchPlane(ModelDoc2 doc, Feature feat, Sketch sk, SketchReferenceDebugInfo debug)
{
if (TryDescribeSketchReference(doc, sk, out var direct))
{
debug.DirectStatus = "直接获取成功";
debug.FinalEntityType = direct.EntityType;
return direct;
}
debug.DirectStatus = string.IsNullOrWhiteSpace(direct.EntityType) ? "直接获取失败:null" : $"直接获取失败:{direct.EntityType}";
if (TryDescribeSketchReferenceByEditSketch(doc, feat, out var editSketch, debug))
return editSketch;
return new SketchReferenceResult { Description = "未识别(引用实体可能已被消耗)" };
}
static bool TryDescribeSketchReference(ModelDoc2 doc, Sketch sk, out SketchReferenceResult result)
{
result = new SketchReferenceResult();
if (sk == null)
{
result.EntityType = "null";
return false;
}
int referenceType = 0;
object entity = SafeObj(() => sk.GetReferenceEntity(ref referenceType));
result.EntityTypeCode = referenceType;
result.EntityType = entity?.GetType().FullName ?? "null";
return TryDescribeReferenceEntity(doc, entity, result);
}
static bool TryDescribeSketchReferenceByEditSketch(ModelDoc2 doc, Feature feat, out SketchReferenceResult result, SketchReferenceDebugInfo debug)
{
result = new SketchReferenceResult();
if (doc == null || feat == null) return false;
string featureName = Safe(() => feat.Name);
bool enteredEditSketch = false;
try
{
SafeAction(() => doc.ClearSelection2(true));
bool selected = SafeBool(() => feat.Select2(false, -1));
if (!selected)
{
debug.EditSketchStatus = "编辑草图前选择草图失败";
return false;
}
enteredEditSketch = TryInvokeCom(doc, "EditSketch", Array.Empty<object>());
debug.EditSketchStatus = enteredEditSketch ? "EditSketch调用成功" : "EditSketch调用失败";
if (!enteredEditSketch) return false;
if (TryGetActiveSketch(doc, out var activeSketch) && TryDescribeSketchReference(doc, activeSketch, out result))
{
result.Description += "(编辑草图恢复)";
debug.FinalEntityType = result.EntityType;
return true;
}
if (TryGetFreshSketch(doc, featureName, out var refreshedSketch))
{
debug.RefetchedSketchStatus = "编辑草图后重新获取草图成功";
if (TryDescribeSketchReference(doc, refreshedSketch, out result))
{
result.Description += "(编辑草图恢复)";
debug.FinalEntityType = result.EntityType;
return true;
}
debug.FinalEntityType = result.EntityType;
return false;
}
debug.RefetchedSketchStatus = "编辑草图后重新获取草图失败";
return false;
}
finally
{
if (enteredEditSketch) TryExitSketch(doc);
SafeAction(() => doc.ClearSelection2(true));
}
}
static string BuildSketchReferenceDebugNote(SketchReferenceDebugInfo debug)
{
var parts = new List<string>();
if (!string.IsNullOrWhiteSpace(debug.DirectStatus)) parts.Add(debug.DirectStatus);
if (!string.IsNullOrWhiteSpace(debug.EditSketchStatus)) parts.Add(debug.EditSketchStatus);
if (!string.IsNullOrWhiteSpace(debug.RollbackStatus)) parts.Add(debug.RollbackStatus);
if (!string.IsNullOrWhiteSpace(debug.RefetchedSketchStatus)) parts.Add(debug.RefetchedSketchStatus);
if (!string.IsNullOrWhiteSpace(debug.FinalEntityType)) parts.Add("引用类型=" + debug.FinalEntityType);
return parts.Count == 0 ? "" : "草图基准调试=" + string.Join("", parts);
}
static bool TryGetActiveSketch(ModelDoc2 doc, out Sketch sketch)
{
sketch = null;
var sketchManager = SafeObj(() => doc.SketchManager);
if (sketchManager == null) return false;
sketch = SafeObj(() => GetComProperty(sketchManager, "ActiveSketch")) as Sketch;
return sketch != null;
}
static void TryExitSketch(ModelDoc2 doc)
{
var sketchManager = SafeObj(() => doc.SketchManager);
if (!TryInvokeCom(sketchManager, "InsertSketch", true))
TryInvokeCom(doc, "EditSketch", Array.Empty<object>());
}
static bool TryGetFreshSketch(ModelDoc2 doc, string featureName, out Sketch sketch)
{
sketch = null;
if (doc == null || string.IsNullOrWhiteSpace(featureName)) return false;
var feature = FindFeatureByName(doc, featureName);
if (feature == null) return false;
sketch = SafeObj(() => feature.GetSpecificFeature2()) as Sketch;
return sketch != null;
}
static Feature FindFeatureByName(ModelDoc2 doc, string featureName)
{
foreach (var it in Features(doc).Take(4000))
{
if (string.Equals(Safe(() => it.f.Name), featureName, StringComparison.OrdinalIgnoreCase))
return it.f;
}
return null;
}
static bool TryDescribeReferenceEntity(ModelDoc2 doc, object entity, SketchReferenceResult result)
{
result.Description = "";
if (entity == null) return false;
if (entity is Feature feature)
{
string featureType = Safe(() => feature.GetTypeName2(), "未知类型");
result.ReferenceName = Safe(() => feature.Name, "特征");
if (string.Equals(featureType, "RefPlane", StringComparison.OrdinalIgnoreCase))
{
result.Kind = "datum_plane";
result.Description = string.IsNullOrWhiteSpace(result.ReferenceName)
? "基准面 / RefPlane"
: result.ReferenceName + " / RefPlane";
return true;
}
if (string.Equals(featureType, "RefAxis", StringComparison.OrdinalIgnoreCase))
{
result.Kind = "datum_axis";
result.Description = string.IsNullOrWhiteSpace(result.ReferenceName)
? "基准轴 / RefAxis"
: result.ReferenceName + " / RefAxis";
return true;
}
result.Kind = "feature";
result.Description = result.ReferenceName + " / " + featureType;
return true;
}
if (entity is RefPlane plane)
{
string name = SafeObj(() => InvokeCom(plane, "Name"))?.ToString() ?? "";
if (IsGenericRefPlaneName(name))
name = FindRefPlaneFeatureName(doc, plane);
result.Kind = "datum_plane";
result.ReferenceName = string.IsNullOrWhiteSpace(name) ? "基准面" : name;
result.Description = result.ReferenceName + " / RefPlane";
return true;
}
if (entity is Face2 face)
{
result.Kind = "face";
FillFaceSelector(doc, face, result);
result.Description = DescribeFace(face, result);
result.PersistReferenceHex = TryGetPersistReferenceHex(doc, face);
return !string.IsNullOrWhiteSpace(result.Description);
}
result.Kind = "unknown_com_entity";
result.Description = entity.GetType().Name;
return true;
}
static bool IsGenericRefPlaneName(string name)
{
string n = (name ?? "").Trim();
return string.IsNullOrWhiteSpace(n)
|| string.Equals(n, "基准面", StringComparison.OrdinalIgnoreCase)
|| string.Equals(n, "基准面 / RefPlane", StringComparison.OrdinalIgnoreCase)
|| string.Equals(n, "Plane", StringComparison.OrdinalIgnoreCase)
|| string.Equals(n, "RefPlane", StringComparison.OrdinalIgnoreCase)
|| (n.Contains("RefPlane", StringComparison.OrdinalIgnoreCase)
&& !n.Any(char.IsDigit)
&& !n.Contains("front", StringComparison.OrdinalIgnoreCase)
&& !n.Contains("top", StringComparison.OrdinalIgnoreCase)
&& !n.Contains("right", StringComparison.OrdinalIgnoreCase)
&& !n.Contains("前")
&& !n.Contains("上")
&& !n.Contains("右"));
}
static void NormalizeGenericDatumPlaneReference(Sketch sketch, SketchReferenceResult reference)
{
if (sketch == null || reference == null) return;
if (!string.Equals(reference.Kind, "datum_plane", StringComparison.OrdinalIgnoreCase)) return;
if (!IsGenericRefPlaneName(reference.ReferenceName) && !IsGenericRefPlaneName(reference.Description)) return;
reference.Kind = "unresolved";
reference.ReferenceName = "";
reference.Description = "datum plane reference name unavailable from SolidWorks API";
}
static bool TryInferDefaultPlaneFromSketchTransform(Sketch sketch, out string planeKey)
{
planeKey = "";
if (!TrySketchLocalMmToModelMm(sketch, 0.0, 0.0, 0.0, out var origin) ||
!TrySketchLocalMmToModelMm(sketch, 0.0, 0.0, 1.0, out var zPoint))
return false;
double nx = zPoint[0] - origin[0];
double ny = zPoint[1] - origin[1];
double nz = zPoint[2] - origin[2];
var normal = Normalize(nx, ny, nz);
if (normal.Length < 3) return false;
double ax = Math.Abs(normal[0]);
double ay = Math.Abs(normal[1]);
double az = Math.Abs(normal[2]);
const double originToleranceMm = 0.05;
const double axisTolerance = 0.95;
if (ax >= ay && ax >= az && ax >= axisTolerance && Math.Abs(origin[0]) <= originToleranceMm)
{
planeKey = "right";
return true;
}
if (ay >= ax && ay >= az && ay >= axisTolerance && Math.Abs(origin[1]) <= originToleranceMm)
{
planeKey = "top";
return true;
}
if (az >= ax && az >= ay && az >= axisTolerance && Math.Abs(origin[2]) <= originToleranceMm)
{
planeKey = "front";
return true;
}
return false;
}
static string FindRefPlaneFeatureName(ModelDoc2 doc, RefPlane plane)
{
if (doc == null || plane == null) return "";
IntPtr targetId = Id(plane);
if (targetId == IntPtr.Zero) return "";
foreach (var it in Features(doc).Take(5000))
{
if (!string.Equals(Safe(() => it.f.GetTypeName2()), "RefPlane", StringComparison.OrdinalIgnoreCase))
continue;
object specific = SafeObj(() => it.f.GetSpecificFeature2());
if (specific == null) continue;
if (Id(specific) == targetId)
return Safe(() => it.f.Name, "");
}
return "";
}
static string DescribeFace(Face2 face, SketchReferenceResult result = null)
{
string host = result?.HostFeatureName ?? "";
try
{
if (string.IsNullOrWhiteSpace(host) && face.GetFeature() is Feature f) host = Safe(() => f.Name);
}
catch { }
string indexText = result != null && result.FaceIndex > 0 ? $"#{result.FaceIndex} " : "";
if (result != null && !string.IsNullOrWhiteSpace(result.SurfaceType))
{
string label = result.SurfaceType switch
{
"plane" => "平面面",
"cylinder" => "圆柱面",
"cone" => "圆锥面",
"sphere" => "球面",
_ => "面引用"
};
return string.IsNullOrWhiteSpace(host) ? $"{indexText}{label}" : $"{indexText}{label} / 宿主特征={host}";
}
try
{
if (face.GetSurface() is Surface s)
{
if (SafeBool(() => s.IsPlane())) return string.IsNullOrWhiteSpace(host) ? "平面面" : $"平面面 / 宿主特征={host}";
if (SafeBool(() => s.IsCylinder())) return string.IsNullOrWhiteSpace(host) ? "圆柱面" : $"圆柱面 / 宿主特征={host}";
if (SafeBool(() => s.IsCone())) return string.IsNullOrWhiteSpace(host) ? "圆锥面" : $"圆锥面 / 宿主特征={host}";
if (SafeBool(() => s.IsSphere())) return string.IsNullOrWhiteSpace(host) ? "球面" : $"球面 / 宿主特征={host}";
}
}
catch { }
return string.IsNullOrWhiteSpace(host) ? "面引用" : $"面引用 / 宿主特征={host}";
}
static void FillFaceSelector(ModelDoc2 doc, Face2 face, SketchReferenceResult result)
{
if (face == null || result == null) return;
try
{
if (face.GetFeature() is Feature host)
{
result.HostFeatureName = Safe(() => host.Name);
if (!string.IsNullOrWhiteSpace(result.HostFeatureName) &&
FeatureStepByName.TryGetValue(result.HostFeatureName, out int hostStep))
result.HostFeatureStep = hostStep;
}
}
catch { }
result.FaceIndex = FindFaceIndexLikeExecutor(doc, face);
result.AreaMm2 = R(SafeDouble(() => face.GetArea()) * 1_000_000.0);
result.BoxMm = TryGetFaceBoxMm(face);
if (result.BoxMm.Length >= 6)
{
result.CenterMm = new[]
{
R((result.BoxMm[0] + result.BoxMm[3]) / 2.0),
R((result.BoxMm[1] + result.BoxMm[4]) / 2.0),
R((result.BoxMm[2] + result.BoxMm[5]) / 2.0)
};
}
try
{
if (face.GetSurface() is Surface surface)
{
result.SurfaceType = SurfaceKind(surface);
if (SafeBool(() => surface.IsPlane()))
result.Normal = TryGetPlaneNormal(face, surface);
}
}
catch { }
}
static int FindFaceIndexLikeExecutor(ModelDoc2 doc, Face2 target)
{
if (doc == null || target == null) return 0;
var targetId = Id(target);
if (targetId == IntPtr.Zero) return 0;
try
{
if (doc is not PartDoc part) return 0;
object bodiesObj = part.GetBodies2((int)swBodyType_e.swAllBodies, true);
int faceIndex = 0;
foreach (object bodyObj in ToObjectArray(bodiesObj))
{
object facesObj = InvokeCom(bodyObj, "GetFaces");
foreach (object faceObj in ToObjectArray(facesObj))
{
if (faceObj is not Face2 candidate) continue;
faceIndex++;
if (Id(candidate) == targetId) return faceIndex;
}
}
}
catch { }
return 0;
}
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 double[] TryGetFaceBoxMm(Face2 face)
{
try
{
var values = ToDoubleArray(face.GetBox());
if (values.Length >= 6)
return values.Take(6).Select(v => R(v * 1000.0)).ToArray();
}
catch { }
return Array.Empty<double>();
}
static double[] TryGetPlaneNormal(Face2 face, Surface surface)
{
try
{
var values = ToDoubleArray(GetComProperty(surface, "PlaneParams"));
if (values.Length == 0)
values = ToDoubleArray(InvokeCom(surface, "PlaneParams"));
if (values.Length >= 6)
{
var first = Normalize(values[0], values[1], values[2]);
if (first.Length >= 3) return ApplyFaceSense(face, first);
var second = Normalize(values[3], values[4], values[5]);
if (second.Length >= 3) return ApplyFaceSense(face, second);
}
}
catch { }
return Array.Empty<double>();
}
static double[] FaceNormal(Face2 face)
{
if (face == null) return Array.Empty<double>();
try
{
if (face.GetSurface() is Surface surface && SafeBool(() => surface.IsPlane()))
return TryGetPlaneNormal(face, surface);
}
catch { }
return Array.Empty<double>();
}
static double[] ApplyFaceSense(Face2 face, double[] normal)
{
if (normal.Length < 3) return normal;
try
{
if (face.FaceInSurfaceSense())
return new[] { R(-normal[0]), R(-normal[1]), R(-normal[2]) };
}
catch { }
return normal;
}
static void EnrichExtrudeDirectionHints()
{
for (int i = 0; i < Skills.Count; i++)
{
var step = Skills[i];
if (!string.Equals(step.Skill, "extrude_boss_mm", StringComparison.OrdinalIgnoreCase))
continue;
if (!TryGetDouble(step.Args, "depth_mm", out double depthMm) || depthMm <= 0)
continue;
var start = FindNearestFaceSketchCenter(i, -1);
var end = FindNearestFaceSketchCenter(i, 1);
if (start == null || end == null)
continue;
double dx = end[0] - start[0];
double dy = end[1] - start[1];
double dz = end[2] - start[2];
double length = Math.Sqrt(dx * dx + dy * dy + dz * dz);
if (length <= 1e-6)
continue;
double tolerance = Math.Max(1.0, depthMm * 0.05);
if (Math.Abs(length - depthMm) > tolerance)
continue;
step.Args["direction_x"] = R(dx / length);
step.Args["direction_y"] = R(dy / length);
step.Args["direction_z"] = R(dz / length);
step.Args["expected_end_x_mm"] = R(end[0]);
step.Args["expected_end_y_mm"] = R(end[1]);
step.Args["expected_end_z_mm"] = R(end[2]);
step.Args["direction_hint_source"] = "next_face_sketch_center";
}
}
static double[] FindNearestFaceSketchCenter(int startIndex, int direction)
{
for (int i = startIndex + direction; i >= 0 && i < Skills.Count; i += direction)
{
var step = Skills[i];
if (string.Equals(step.Skill, "extrude_boss_mm", StringComparison.OrdinalIgnoreCase) ||
string.Equals(step.Skill, "extrude_cut_mm", StringComparison.OrdinalIgnoreCase))
return null;
if (!string.Equals(step.Skill, "create_face_sketch_by_signature", StringComparison.OrdinalIgnoreCase))
continue;
if (TryGetDouble(step.Args, "center_x_mm", out double x) &&
TryGetDouble(step.Args, "center_y_mm", out double y) &&
TryGetDouble(step.Args, "center_z_mm", out double z))
return new[] { x, y, z };
if (step.Args.TryGetValue("center_mm", out object value))
{
var values = FlattenDoubles(value).Take(3).ToArray();
if (values.Length >= 3)
return values;
}
}
return null;
}
static bool TryGetDouble(Dictionary<string, object> args, string key, out double value)
{
value = 0;
if (args == null || !args.TryGetValue(key, out object raw) || raw == null)
return false;
try
{
if (raw is double d) { value = d; return true; }
if (raw is float f) { value = f; return true; }
if (raw is int i) { value = i; return true; }
if (raw is long l) { value = l; return true; }
if (raw is decimal m) { value = (double)m; return true; }
return double.TryParse(raw.ToString(), out value);
}
catch
{
return false;
}
}
static void NormalizeAutoNumberedFeatureNames()
{
var counters = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
var renameMap = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
foreach (var step in Skills)
{
if (!IsFeatureCreatingSkill(step.Skill))
continue;
if (!TrySplitAutoNumberedName(step.SourceFeature, out string prefix, out int sourceNumber))
continue;
int expectedNumber = counters.TryGetValue(prefix, out int current) ? current + 1 : 1;
counters[prefix] = expectedNumber;
if (sourceNumber != expectedNumber)
renameMap[step.SourceFeature] = prefix + expectedNumber.ToString(System.Globalization.CultureInfo.InvariantCulture);
}
if (renameMap.Count == 0)
return;
foreach (var step in Skills)
{
step.SourceFeature = RewriteFeatureNameText(step.SourceFeature, renameMap);
step.Note = RewriteFeatureNameText(step.Note, renameMap);
var keys = step.Args.Keys.ToList();
foreach (string key in keys)
step.Args[key] = RewriteFeatureNameValue(step.Args[key], renameMap);
}
}
static bool IsFeatureCreatingSkill(string skill)
{
return string.Equals(skill, "extrude_boss_mm", StringComparison.OrdinalIgnoreCase)
|| string.Equals(skill, "extrude_cut_mm", StringComparison.OrdinalIgnoreCase)
|| string.Equals(skill, "create_revolve_boss_mm", StringComparison.OrdinalIgnoreCase)
|| string.Equals(skill, "create_revolve_cut_mm", StringComparison.OrdinalIgnoreCase)
|| string.Equals(skill, "fillet_edges_radius_mm", StringComparison.OrdinalIgnoreCase)
|| string.Equals(skill, "chamfer_edges_angle_distance_mm", StringComparison.OrdinalIgnoreCase)
|| string.Equals(skill, "linear_pattern_feature_mm", StringComparison.OrdinalIgnoreCase)
|| string.Equals(skill, "mirror_feature_about_plane", StringComparison.OrdinalIgnoreCase)
|| string.Equals(skill, "draft_faces_neutral_plane_mm", StringComparison.OrdinalIgnoreCase)
|| string.Equals(skill, "create_rib_mm", StringComparison.OrdinalIgnoreCase)
|| string.Equals(skill, "shell_remove_face_at_point_mm", StringComparison.OrdinalIgnoreCase)
|| string.Equals(skill, "hole_wizard_threaded_mm", StringComparison.OrdinalIgnoreCase)
|| string.Equals(skill, "wrap_sketch_on_faces_mm", StringComparison.OrdinalIgnoreCase)
|| string.Equals(skill, "swept_cut_circular_profile_mm", StringComparison.OrdinalIgnoreCase)
|| string.Equals(skill, "swept_boss_circular_profile_mm", StringComparison.OrdinalIgnoreCase);
}
static bool TrySplitAutoNumberedName(string name, out string prefix, out int number)
{
prefix = "";
number = 0;
if (string.IsNullOrWhiteSpace(name)) return false;
var match = Regex.Match(name.Trim(), @"^(?<prefix>.*?)(?<number>\d+)$");
if (!match.Success) return false;
prefix = match.Groups["prefix"].Value;
return !string.IsNullOrWhiteSpace(prefix)
&& int.TryParse(match.Groups["number"].Value, out number)
&& number > 0;
}
static object RewriteFeatureNameValue(object value, Dictionary<string, string> renameMap)
{
if (value == null) return value;
if (value is string text) return RewriteFeatureNameText(text, renameMap);
if (value is string[] strings) return strings.Select(item => RewriteFeatureNameText(item, renameMap)).ToArray();
if (value is List<string> stringList) return stringList.Select(item => RewriteFeatureNameText(item, renameMap)).ToList();
if (value is Dictionary<string, object> dict)
{
var keys = dict.Keys.ToList();
foreach (string key in keys)
dict[key] = RewriteFeatureNameValue(dict[key], renameMap);
return dict;
}
if (value is object[] objects)
return objects.Select(item => RewriteFeatureNameValue(item, renameMap)).ToArray();
return value;
}
static string RewriteFeatureNameText(string text, Dictionary<string, string> renameMap)
{
if (string.IsNullOrWhiteSpace(text) || renameMap.Count == 0)
return text;
string pattern = string.Join("|", renameMap.Keys
.OrderByDescending(key => key.Length)
.Select(Regex.Escape));
return Regex.Replace(text, $"({pattern})(?!\\d)", match =>
renameMap.TryGetValue(match.Value, out string mapped) ? mapped : match.Value);
}
static double[] Normalize(double x, double y, double z)
{
double len = Math.Sqrt(x * x + y * y + z * z);
if (len < 0.5 || len > 1.5) return Array.Empty<double>();
return new[] { R(x / len), R(y / len), R(z / len) };
}
static string TryGetPersistReferenceHex(ModelDoc2 doc, object entity)
{
try
{
object raw = doc?.Extension?.GetPersistReference3(entity);
if (raw is byte[] bytes) return Convert.ToHexString(bytes);
if (raw is Array array)
{
var bytesFromObjects = new List<byte>();
foreach (object value in array)
{
if (value == null) continue;
bytesFromObjects.Add(Convert.ToByte(value));
}
if (bytesFromObjects.Count > 0) return Convert.ToHexString(bytesFromObjects.ToArray());
}
}
catch { }
return "";
}
static string Segments(ModelDoc2 doc, Sketch sk, string name, string type)
{
int line = 0, arc = 0, spline = 0, point = 0, other = 0;
var details = new List<string>();
var canonicalPoints = BuildSketchPointCanonicalMap(sk);
HashSet<IntPtr> selectedContourSegmentIds = null;
var sketchId = Id(sk);
if (sketchId != IntPtr.Zero)
SketchSelectedContourSegmentIdsBySketchId.TryGetValue(sketchId, out selectedContourSegmentIds);
var segmentPointIds = new HashSet<IntPtr>();
var segmentPointKeys = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
foreach (object o in ToObjectArray(SafeObj(() => sk.GetSketchSegments())))
{
if (o is not SketchSegment s) continue;
var segmentId = Id(s);
if (selectedContourSegmentIds != null && (segmentId == IntPtr.Zero || !selectedContourSegmentIds.Contains(segmentId)))
continue;
int t = SafeInt(() => s.GetType());
RegisterSegmentPoints(s, canonicalPoints, segmentPointIds, segmentPointKeys);
if (t == 0 && Line(doc, sk, s, name, type, canonicalPoints, out string lineText)) { line++; details.Add(lineText); }
else if (t == 1 && Arc(doc, sk, s, name, type, canonicalPoints, out string arcText)) { arc++; details.Add(arcText); }
else if (s is SketchSpline && Spline(doc, sk, s, name, type, out string splineText)) { spline++; details.Add(splineText); }
else other++;
}
var freePointKeys = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
foreach (double[] pointLocal in UserSketchPointsLocalMm(sk))
{
if (pointLocal.Length < 3) continue;
double px = pointLocal[0], py = pointLocal[1], pz = pointLocal[2];
if (segmentPointKeys.Contains(SketchPointCoordKey(px, py, pz))) continue;
if (!freePointKeys.Add(SketchPointCoordKey(px, py, pz))) continue;
if (SketchPointLocal(doc, sk, px, py, pz, name, type, out string pointText))
{
point++;
details.Add(pointText);
}
}
string summary = $"lines={line}, arcs={arc}, splines={spline}, points={point}, other={other}";
return details.Count == 0 ? summary : summary + "" + string.Join("", details.Take(80));
}
static void RegisterSegmentPoints(SketchSegment segment, Dictionary<IntPtr, double[]> canonicalPoints, HashSet<IntPtr> ids, HashSet<string> coordKeys = null)
{
if (segment == null || ids == null) return;
void Add(SketchPoint p)
{
if (p == null) return;
IntPtr id = Id(p);
if (id != IntPtr.Zero) ids.Add(id);
if (coordKeys != null && Pt3(p, canonicalPoints, out var x, out var y, out var z))
coordKeys.Add(SketchPointCoordKey(x, y, z));
}
try
{
if (segment is SketchLine line)
{
Add(line.GetStartPoint2() as SketchPoint);
Add(line.GetEndPoint2() as SketchPoint);
}
else if (segment is SketchArc arc)
{
Add(arc.GetCenterPoint2() as SketchPoint);
Add(arc.GetStartPoint2() as SketchPoint);
Add(arc.GetEndPoint2() as SketchPoint);
}
else if (segment is SketchSpline spline)
{
foreach (object pointObj in ToObjectArray(SafeObj(() => spline.GetPoints2())))
Add(pointObj as SketchPoint);
}
}
catch { }
}
static bool IsSegmentOwnedSketchPoint(SketchPoint point, double x, double y, double z, HashSet<IntPtr> ids, HashSet<string> coordKeys)
{
IntPtr pointId = Id(point);
if (pointId != IntPtr.Zero && ids != null && ids.Contains(pointId)) return true;
return coordKeys != null && coordKeys.Contains(SketchPointCoordKey(x, y, z));
}
static List<double[]> UserSketchPointsLocalMm(Sketch sketch)
{
var result = new List<double[]>();
if (sketch == null) return result;
// SolidWorks ISketch.GetUserPoints2 returns only user-created sketch points.
// Its record layout is 8 doubles per point; the last three values are point location.
double[] raw = ToDoubleArray(SafeObj(() => sketch.GetUserPoints2()));
if (raw.Length == 0)
raw = ToDoubleArray(InvokeCom(sketch, "GetUserPoints2"));
if (raw.Length == 0)
return result;
int count = SafeInt(() => Convert.ToInt32(InvokeCom(sketch, "GetUserPointsCount")), 0);
int stride = 8;
if (count > 0 && raw.Length >= count * stride)
{
for (int i = 0; i < count; i++)
{
int offset = i * stride;
result.Add(new[] { raw[offset + 5] * 1000.0, raw[offset + 6] * 1000.0, raw[offset + 7] * 1000.0 });
}
return result;
}
if (raw.Length % stride == 0)
{
for (int offset = 0; offset + 7 < raw.Length; offset += stride)
result.Add(new[] { raw[offset + 5] * 1000.0, raw[offset + 6] * 1000.0, raw[offset + 7] * 1000.0 });
}
else if (raw.Length % 3 == 0)
{
for (int offset = 0; offset + 2 < raw.Length; offset += 3)
result.Add(new[] { raw[offset] * 1000.0, raw[offset + 1] * 1000.0, raw[offset + 2] * 1000.0 });
}
return result;
}
static string SketchPointCoordKey(double x, double y, double z)
{
const double tolMm = 0.001;
return $"{QuantizeMm(x, tolMm)}|{QuantizeMm(y, tolMm)}|{QuantizeMm(z, tolMm)}";
}
static long QuantizeMm(double value, double tolerance)
{
return (long)Math.Round(value / tolerance, MidpointRounding.AwayFromZero);
}
static bool SketchPoint(ModelDoc2 doc, Sketch sk, SketchPoint p, string name, string type, Dictionary<IntPtr, double[]> canonicalPoints, out string info)
{
info = "";
if (!Pt3(p, canonicalPoints, out var x, out var y, out var z)) return false;
return SketchPointLocal(doc, sk, x, y, z, name, type, out info);
}
static bool SketchPointLocal(ModelDoc2 doc, Sketch sk, double x, double y, double z, string name, string type, out string info)
{
info = "";
if (!TrySketchLocalMmToModelMm(sk, x, y, z, out var pointModel)) return false;
AddSkill("draw_point_mm", new()
{
["coordinate_system"] = "model",
["point_model_mm"] = pointModel
}, name, type);
info = $"point: model=({pointModel[0]},{pointModel[1]},{pointModel[2]})mm";
return true;
}
static bool Line(ModelDoc2 doc, Sketch sk, SketchSegment s, string name, string type, Dictionary<IntPtr, double[]> canonicalPoints, out string info)
{
info = "";
if (s is not SketchLine l) return false;
var rawStartPoint = l.GetStartPoint2() as SketchPoint;
var rawEndPoint = l.GetEndPoint2() as SketchPoint;
if (!Pt3(rawStartPoint, canonicalPoints, out var x1, out var y1, out var z1) || !Pt3(rawEndPoint, canonicalPoints, out var x2, out var y2, out var z2)) return false;
if (!TrySketchLocalMmToModelMm(sk, x1, y1, z1, out var startModel) ||
!TrySketchLocalMmToModelMm(sk, x2, y2, z2, out var endModel))
return false;
bool isConstruction = IsConstructionGeometry(s);
AddSkill(isConstruction ? "draw_center_line_mm" : "draw_line_mm", new()
{
["coordinate_system"] = "model",
["start_model_mm"] = startModel,
["end_model_mm"] = endModel
}, name, type);
string lineKind = isConstruction ? "centerline" : "line";
info = $"{lineKind}: model_start=({startModel[0]},{startModel[1]},{startModel[2]})mm, model_end=({endModel[0]},{endModel[1]},{endModel[2]})mm";
return true;
}
static bool Arc(ModelDoc2 doc, Sketch sk, SketchSegment s, string name, string type, Dictionary<IntPtr, double[]> canonicalPoints, out string info)
{
info = "";
if (IsConstructionGeometry(s)) return false;
if (s is not SketchArc a) return false;
if (!Pt3(a.GetCenterPoint2() as SketchPoint, canonicalPoints, out var cx, out var cy, out var cz)) return false;
double r = a.GetRadius() * 1000;
double sx = 0, sy = 0, sz = 0, ex = 0, ey = 0, ez = 0;
bool ok = Pt3(a.GetStartPoint2() as SketchPoint, canonicalPoints, out sx, out sy, out sz) && Pt3(a.GetEndPoint2() as SketchPoint, canonicalPoints, out ex, out ey, out ez);
if (!TrySketchLocalMmToModelMm(sk, cx, cy, cz, out var centerModel)) return false;
if (!ok || Math.Abs(sx - ex) + Math.Abs(sy - ey) < .001)
{
AddSkill("draw_circle_diameter_mm", new()
{
["coordinate_system"] = "model",
["center_model_mm"] = centerModel,
["diameter_mm"] = R(2 * r)
}, name, type);
info = $"圆: center=({R(cx)},{R(cy)})mm, diameter={R(2 * r)}mm";
}
else
{
int direction = TryGetArcDirection(a);
if (!TrySketchLocalMmToModelMm(sk, sx, sy, sz, out var startModel) ||
!TrySketchLocalMmToModelMm(sk, ex, ey, ez, out var endModel) ||
!TryArcMidLocalMm(cx, cy, cz, sx, sy, ex, ey, direction, out var midLocal) ||
!TrySketchLocalMmToModelMm(sk, midLocal[0], midLocal[1], midLocal[2], out var midModel))
return false;
AddSkill("draw_arc_center_start_end_mm", new()
{
["coordinate_system"] = "model",
["center_model_mm"] = centerModel,
["start_model_mm"] = startModel,
["end_model_mm"] = endModel,
["mid_model_mm"] = midModel,
["direction_source"] = "model_midpoint"
}, name, type);
info = $"圆弧: center=({R(cx)},{R(cy)})mm, start=({R(sx)},{R(sy)})mm, end=({R(ex)},{R(ey)})mm, radius={R(r)}mm";
}
return true;
}
static bool Spline(ModelDoc2 doc, Sketch sk, SketchSegment s, string name, string type, out string info)
{
info = "";
if (IsConstructionGeometry(s)) return false;
if (s is not SketchSpline spline) return false;
var pointsModel = new List<List<double>>();
var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
foreach (object pointObj in ToObjectArray(SafeObj(() => spline.GetPoints2())))
{
if (pointObj is not SketchPoint point) continue;
if (!Pt3(point, out double x, out double y, out double z)) continue;
string key = SketchPointCoordKey(x, y, z);
if (!seen.Add(key)) continue;
if (TrySketchLocalMmToModelMm(sk, x, y, z, out var modelPoint))
pointsModel.Add(modelPoint);
}
if (pointsModel.Count < 2)
return false;
AddSkill("draw_spline_points_mm", new()
{
["coordinate_system"] = "model",
["points_model_mm"] = pointsModel,
["simulate_natural_ends"] = true
}, name, type);
info = $"spline: points={pointsModel.Count}";
return true;
}
static int TryGetArcDirection(SketchArc arc)
{
try
{
int direction = arc.GetRotationDir();
if (direction < 0) return -1;
if (direction > 0) return 1;
}
catch { }
return 1;
}
static bool TryArcMidLocalMm(double cx, double cy, double cz, double sx, double sy, double ex, double ey, int direction, out double[] mid)
{
mid = Array.Empty<double>();
double r = Math.Sqrt((sx - cx) * (sx - cx) + (sy - cy) * (sy - cy));
if (r <= 1e-9) return false;
double start = Math.Atan2(sy - cy, sx - cx);
double end = Math.Atan2(ey - cy, ex - cx);
if (direction < 0)
{
while (end >= start) end -= Math.PI * 2.0;
}
else
{
while (end <= start) end += Math.PI * 2.0;
}
double angle = (start + end) / 2.0;
mid = new[] { R(cx + Math.Cos(angle) * r), R(cy + Math.Sin(angle) * r), R(cz) };
return true;
}
static bool TrySketchLocalMmToModelMm(Sketch sk, double xMm, double yMm, double zMm, out List<double> modelMm)
{
modelMm = new List<double>();
try
{
if (sk == null || CurrentSwApp == null) return false;
MathTransform modelToSketch = sk.ModelToSketchTransform;
MathTransform sketchToModel = modelToSketch?.Inverse() as MathTransform;
MathUtility mathUtility = CurrentSwApp.GetMathUtility() as MathUtility;
MathPoint point = mathUtility?.CreatePoint(new double[] { xMm / 1000.0, yMm / 1000.0, zMm / 1000.0 }) as MathPoint;
MathPoint transformed = point?.MultiplyTransform(sketchToModel) as MathPoint;
var data = ToDoubleArray(transformed?.ArrayData);
if (data.Length < 3) return false;
modelMm = new List<double> { R(data[0] * 1000.0), R(data[1] * 1000.0), R(data[2] * 1000.0) };
return true;
}
catch
{
modelMm = new List<double>();
return false;
}
}
static void AddExtrudeDirectionModelArgs(Feature feature, bool reverseDir, Dictionary<string, object> args)
{
try
{
Sketch sketch = FirstSketchFromFeature(feature);
if (sketch == null) return;
if (!TrySketchLocalMmToModelMm(sketch, 0.0, 0.0, 0.0, out var origin) ||
!TrySketchLocalMmToModelMm(sketch, 0.0, 0.0, 1.0, out var zPoint))
return;
var normal = Normalize(zPoint[0] - origin[0], zPoint[1] - origin[1], zPoint[2] - origin[2]);
if (normal.Length < 3) return;
var direction = reverseDir
? new[] { R(-normal[0]), R(-normal[1]), R(-normal[2]) }
: new[] { normal[0], normal[1], normal[2] };
args["sketch_normal_model"] = normal.ToList();
args["extrude_direction_model"] = direction.ToList();
args["direction_coordinate_system"] = "model";
}
catch { }
}
static Sketch FirstSketchFromFeature(Feature feature)
{
if (feature == null) return null;
Feature sub = feature.GetFirstSubFeature() as Feature;
for (int i = 0; sub != null && i < 200; i++)
{
if (IsSketch(Safe(() => sub.GetTypeName2())) && sub.GetSpecificFeature2() is Sketch sk)
return sk;
sub = sub.GetNextSubFeature() as Feature;
}
return null;
}
static bool IsConstructionGeometry(SketchSegment segment)
{
if (segment == null) return false;
try { return segment.ConstructionGeometry; }
catch
{
object value = GetComProperty(segment, "ConstructionGeometry");
return value is bool b && b;
}
}
static bool TryExtractSimpleFillet(ModelDoc2 doc, Feature feature, out Dictionary<string, object> args, out string note)
{
args = new Dictionary<string, object>();
note = "";
try
{
if (feature.GetDefinition() is not ISimpleFilletFeatureData2 data)
return false;
bool accessed = SafeBool(() => data.AccessSelections(doc, null));
try
{
double radiusMm = R(data.DefaultRadius * 1000.0);
var edgeSignatures = EdgeSignatures(data.Edges);
if (radiusMm <= 0 || edgeSignatures.Count == 0)
return false;
args["radius_mm"] = radiusMm;
args["edge_signatures"] = edgeSignatures;
args["fillet_type"] = data.Type;
args["edge_count"] = edgeSignatures.Count;
note = $"圆角半径={radiusMm}mm, edges={edgeSignatures.Count}, source=ISimpleFilletFeatureData2";
return true;
}
finally
{
if (accessed)
{
try { data.ReleaseSelectionAccess(); } catch { }
}
}
}
catch (Exception ex)
{
note = $"圆角提取失败: {ex.Message}";
return false;
}
}
static bool TryExtractChamfer(ModelDoc2 doc, Feature feature, out Dictionary<string, object> args, out string note)
{
args = new Dictionary<string, object>();
note = "";
try
{
object data = SafeObj(() => feature.GetDefinition());
if (data == null)
{
note = "倒角提取失败: GetDefinition 返回空";
return false;
}
IChamferFeatureData2 typedData = data as IChamferFeatureData2;
bool accessed = typedData != null
? SafeBool(() => typedData.AccessSelections(doc, null))
: SafeBool(() => Convert.ToBoolean(InvokeCom(data, "AccessSelections", doc, null)));
try
{
bool typedCast = typedData != null;
double distanceMm = typedCast ? R(typedData.GetEdgeChamferDistance(1) * 1000.0) : R(ReadChamferDistance(data, 1) * 1000.0);
if (distanceMm <= 0) distanceMm = R(ReadChamferDistance(data, 0) * 1000.0);
double otherDistanceMm = typedCast ? R(typedData.GetEdgeChamferDistance(2) * 1000.0) : R(ReadChamferDistance(data, 2) * 1000.0);
if (otherDistanceMm <= 0) otherDistanceMm = R(ReadChamferDistance(data, 1) * 1000.0);
double angleRad = typedCast ? typedData.EdgeChamferAngle : ReadDoubleMember(data, "EdgeChamferAngle");
double angleDeg = R(angleRad * 180.0 / Math.PI);
if (angleDeg <= 0) angleDeg = 45.0;
int chamferType = typedCast ? typedData.Type : RInt(ReadDoubleMember(data, "Type"));
bool equalDistance = typedCast ? typedData.EqualDistance : ReadBoolMember(data, "EqualDistance");
bool tangentPropagation = typedCast ? typedData.TangentPropagation : ReadBoolMember(data, "TangentPropagation");
int apiEdgeCount = typedCast ? SafeInt(() => typedData.GetEdgeCount()) : 0;
object edgesObj = typedCast ? typedData.Edges : GetComProperty(data, "Edges");
string edgesObjType = edgesObj?.GetType().FullName ?? "null";
if (edgesObj == null) edgesObj = InvokeCom(data, "IGetEdges");
var edgeSignatures = EdgeSignatures(edgesObj);
if (edgeSignatures.Count == 0 && typedCast && apiEdgeCount > 0)
{
object iEdgesObj = SafeObj(() => typedData.IGetEdges(apiEdgeCount));
edgesObjType += $"; IGetEdges={iEdgesObj?.GetType().FullName ?? "null"}";
edgeSignatures = EdgeSignatures(iEdgesObj);
}
if (edgeSignatures.Count == 0)
edgeSignatures = EdgeSignaturesFromSelection(doc);
int faceCount = ToObjectArray(GetComProperty(data, "Faces")).OfType<Face2>().Count();
int loopCount = ToObjectArray(GetComProperty(data, "Loops")).Length;
if (distanceMm <= 0)
distanceMm = FirstFeatureLengthDimensionMm(feature);
if (otherDistanceMm <= 0)
otherDistanceMm = distanceMm;
if (distanceMm <= 0 || edgeSignatures.Count == 0)
{
note = $"倒角提取失败: distance={distanceMm}mm, edges={edgeSignatures.Count}, apiEdgeCount={apiEdgeCount}, edgesObj={edgesObjType}, faces={faceCount}, loops={loopCount}, typed={typedCast}, data={data.GetType().FullName}";
return false;
}
args["distance_mm"] = distanceMm;
args["angle_deg"] = angleDeg;
args["other_distance_mm"] = otherDistanceMm;
args["chamfer_type"] = chamferType;
args["equal_distance"] = equalDistance;
args["tangent_propagation"] = tangentPropagation;
args["edge_signatures"] = edgeSignatures;
args["edge_count"] = edgeSignatures.Count;
note = $"倒角距离={distanceMm}mm, other={otherDistanceMm}mm, angle={angleDeg}deg, type={chamferType}, edges={edgeSignatures.Count}, source=IChamferFeatureData2";
return true;
}
finally
{
if (accessed)
{
try
{
if (typedData != null) typedData.ReleaseSelectionAccess();
else InvokeCom(data, "ReleaseSelectionAccess");
}
catch { }
}
}
}
catch (Exception ex)
{
note = $"倒角提取失败: {ex.Message}";
return false;
}
}
static double ReadChamferDistance(object data, int side)
{
try
{
object value = InvokeCom(data, "GetEdgeChamferDistance", side);
if (value != null && double.TryParse(value.ToString(), out double parsed)) return parsed;
}
catch { }
return 0.0;
}
static double ReadDoubleMember(object target, string name)
{
try
{
object raw = GetComProperty(target, name);
if (raw != null && double.TryParse(raw.ToString(), out double value)) return value;
}
catch { }
try
{
object raw = InvokeCom(target, name);
if (raw != null && double.TryParse(raw.ToString(), out double value)) return value;
}
catch { }
return 0.0;
}
static bool ReadBoolMember(object target, string name)
{
try
{
object raw = GetComProperty(target, name);
if (TryConvertBool(raw, out bool value)) return value;
}
catch { }
try
{
object raw = InvokeCom(target, name);
if (TryConvertBool(raw, out bool value)) return value;
}
catch { }
return false;
}
static int RInt(double value) => (int)Math.Round(value);
static double FirstFeatureLengthDimensionMm(Feature feature)
{
try
{
for (var dd = feature.GetFirstDisplayDimension() as DisplayDimension; dd != null; dd = feature.GetNextDisplayDimension(dd) as DisplayDimension)
{
if (dd.GetDimension() is Dimension dim)
{
double valueMm = R(dim.SystemValue * 1000.0);
if (valueMm > 0 && valueMm < 100000) return valueMm;
}
}
}
catch { }
return 0.0;
}
static List<Dictionary<string, object>> EdgeSignatures(object edgesObj)
{
var result = new List<Dictionary<string, object>>();
foreach (object item in FlattenComObjects(edgesObj))
{
if (TryAsEdge(item, out Edge edge))
{
var signature = EdgeSignature(edge);
if (signature.Count > 0)
result.Add(signature);
}
}
return ExpandSymmetricArcEdgeSignatures(result);
}
static List<Dictionary<string, object>> ExpandSymmetricArcEdgeSignatures(List<Dictionary<string, object>> signatures)
{
var expanded = signatures.Select(CloneSignature).ToList();
var circles = signatures
.Where(item => string.Equals(GetString(item, "curve_type"), "circle", StringComparison.OrdinalIgnoreCase))
.ToList();
var lines = signatures
.Where(item => string.Equals(GetString(item, "curve_type"), "line", StringComparison.OrdinalIgnoreCase))
.ToList();
foreach (var circle in circles)
{
var center = GetDoubleArray(circle, "center_mm");
if (center.Length < 3)
continue;
double radius = GetDouble(circle, "radius_mm");
var axis = GetDoubleArray(circle, "axis");
foreach (var line in lines)
{
var start = GetDoubleArray(line, "start_mm");
var end = GetDoubleArray(line, "end_mm");
if (start.Length < 3 || end.Length < 3)
continue;
int spanAxis = DominantAxis(start, end);
if (spanAxis < 0)
continue;
double a = start[spanAxis];
double b = end[spanAxis];
if (!NearlyEqual(center[spanAxis], a, 0.05) && !NearlyEqual(center[spanAxis], b, 0.05))
continue;
if (!CircleCompatibleWithLineLayer(center, radius, start, end, spanAxis))
continue;
var inferredCenter = center.ToArray();
inferredCenter[spanAxis] = NearlyEqual(center[spanAxis], a, 0.05) ? b : a;
if (HasCircleSignatureNear(expanded, inferredCenter, radius, axis))
continue;
var inferred = CloneSignature(circle);
inferred["center_mm"] = inferredCenter.Select(R).ToArray();
inferred.Remove("start_mm");
inferred.Remove("end_mm");
inferred["inferred_symmetric_counterpart"] = true;
inferred["inferred_from_center_mm"] = center;
inferred["inferred_span_axis"] = spanAxis;
expanded.Add(inferred);
}
}
return expanded;
}
static Dictionary<string, object> CloneSignature(Dictionary<string, object> source)
{
var clone = new Dictionary<string, object>();
foreach (var kv in source)
{
if (kv.Value is double[] doubles)
clone[kv.Key] = doubles.ToArray();
else if (kv.Value is Array array)
clone[kv.Key] = array.Cast<object>().ToArray();
else
clone[kv.Key] = kv.Value;
}
return clone;
}
static string GetString(Dictionary<string, object> dict, string key)
{
return dict.TryGetValue(key, out var value) ? value?.ToString() ?? "" : "";
}
static double GetDouble(Dictionary<string, object> dict, string key)
{
if (!dict.TryGetValue(key, out var value) || value == null)
return 0.0;
try { return Convert.ToDouble(value); }
catch { return 0.0; }
}
static double[] GetDoubleArray(Dictionary<string, object> dict, string key)
{
if (!dict.TryGetValue(key, out var value) || value == null)
return Array.Empty<double>();
return ToDoubleArray(value);
}
static int DominantAxis(double[] start, double[] end)
{
double best = 0.0;
int axis = -1;
for (int i = 0; i < 3; i++)
{
double delta = Math.Abs(end[i] - start[i]);
if (delta > best)
{
best = delta;
axis = i;
}
}
return best > 0.05 ? axis : -1;
}
static bool CircleCompatibleWithLineLayer(double[] center, double radiusMm, double[] start, double[] end, int spanAxis)
{
double tolerance = Math.Max(0.1, Math.Abs(radiusMm) + 0.1);
for (int i = 0; i < 3; i++)
{
if (i == spanAxis)
continue;
double min = Math.Min(start[i], end[i]) - tolerance;
double max = Math.Max(start[i], end[i]) + tolerance;
if (center[i] < min || center[i] > max)
return false;
}
return true;
}
static bool HasCircleSignatureNear(List<Dictionary<string, object>> signatures, double[] center, double radiusMm, double[] axis)
{
foreach (var item in signatures)
{
if (!string.Equals(GetString(item, "curve_type"), "circle", StringComparison.OrdinalIgnoreCase))
continue;
var existingCenter = GetDoubleArray(item, "center_mm");
if (existingCenter.Length < 3 || Distance(existingCenter, center) > 0.1)
continue;
double existingRadius = GetDouble(item, "radius_mm");
if (Math.Abs(existingRadius - radiusMm) > 0.1)
continue;
var existingAxis = GetDoubleArray(item, "axis");
if (axis.Length >= 3 && existingAxis.Length >= 3 && AxisMismatch(axis, existingAxis))
continue;
return true;
}
return false;
}
static bool AxisMismatch(double[] a, double[] b)
{
double dot = Math.Abs(a[0] * b[0] + a[1] * b[1] + a[2] * b[2]);
double la = Math.Sqrt(a[0] * a[0] + a[1] * a[1] + a[2] * a[2]);
double lb = Math.Sqrt(b[0] * b[0] + b[1] * b[1] + b[2] * b[2]);
if (la <= 1e-12 || lb <= 1e-12)
return false;
return dot / (la * lb) < 0.99;
}
static bool NearlyEqual(double a, double b, double tolerance)
{
return Math.Abs(a - b) <= tolerance;
}
static IEnumerable<object> FlattenComObjects(object value)
{
if (value == null) yield break;
if (value is DispatchWrapper wrapper)
{
foreach (object nested in FlattenComObjects(wrapper.WrappedObject))
yield return nested;
yield break;
}
if (value is Array array)
{
foreach (object item in array)
{
foreach (object nested in FlattenComObjects(item))
yield return nested;
}
yield break;
}
yield return value;
}
static bool TryAsEdge(object value, out Edge edge)
{
edge = null;
if (value == null) return false;
if (value is DispatchWrapper wrapper)
value = wrapper.WrappedObject;
if (value is Edge typed)
{
edge = typed;
return true;
}
try
{
edge = value as Edge;
return edge != null;
}
catch
{
return false;
}
}
static bool TryAsFace(object value, out Face2 face)
{
face = null;
if (value == null) return false;
if (value is DispatchWrapper wrapper)
value = wrapper.WrappedObject;
if (value is Face2 typed)
{
face = typed;
return true;
}
try
{
face = value as Face2;
return face != null;
}
catch
{
return false;
}
}
static List<Dictionary<string, object>> EdgeSignaturesFromSelection(ModelDoc2 doc)
{
var result = new List<Dictionary<string, object>>();
try
{
if (doc?.SelectionManager is not SelectionMgr selMgr)
return result;
int count = selMgr.GetSelectedObjectCount2(-1);
for (int i = 1; i <= count; i++)
{
object selected = selMgr.GetSelectedObject6(i, -1);
if (selected is Edge edge)
{
var signature = EdgeSignature(edge);
if (signature.Count > 0)
result.Add(signature);
}
}
}
catch { }
return result;
}
static Dictionary<string, object> EdgeSignature(Edge edge)
{
var result = new Dictionary<string, object>();
try
{
Vertex startVertex = SafeObj(() => edge.GetStartVertex()) as Vertex;
Vertex endVertex = SafeObj(() => edge.GetEndVertex()) as Vertex;
double[] start = VertexPointMm(startVertex);
double[] end = VertexPointMm(endVertex);
if (start.Length >= 3) result["start_mm"] = start;
if (end.Length >= 3) result["end_mm"] = end;
Curve curve = SafeObj(() => edge.GetCurve()) as Curve;
string curveType = "other";
if (curve != null)
{
if (SafeBool(() => curve.IsCircle()))
{
curveType = "circle";
var circle = ToDoubleArray(GetComProperty(curve, "CircleParams"));
if (circle.Length == 0) circle = ToDoubleArray(InvokeCom(curve, "CircleParams"));
if (circle.Length >= 7)
{
result["center_mm"] = new[] { R(circle[0] * 1000.0), R(circle[1] * 1000.0), R(circle[2] * 1000.0) };
result["axis"] = new[] { R(circle[3]), R(circle[4]), R(circle[5]) };
result["radius_mm"] = R(circle[6] * 1000.0);
}
}
else if (SafeBool(() => curve.IsLine()))
{
curveType = "line";
var line = ToDoubleArray(GetComProperty(curve, "LineParams"));
if (line.Length == 0) line = ToDoubleArray(InvokeCom(curve, "LineParams"));
if (line.Length >= 6)
{
result["line_point_mm"] = new[] { R(line[0] * 1000.0), R(line[1] * 1000.0), R(line[2] * 1000.0) };
result["line_direction"] = new[] { R(line[3]), R(line[4]), R(line[5]) };
}
}
}
if (!result.ContainsKey("center_mm") && 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));
}
result["curve_type"] = curveType;
}
catch { }
return result;
}
static double[] VertexPointMm(Vertex vertex)
{
if (vertex == null) return Array.Empty<double>();
try
{
var values = ToDoubleArray(vertex.GetPoint());
if (values.Length >= 3)
return new[] { R(values[0] * 1000.0), R(values[1] * 1000.0), R(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], dy = a[1] - b[1], dz = a[2] - b[2];
return Math.Sqrt(dx * dx + dy * dy + dz * dz);
}
static string Dimensions(Sketch sk, Feature feat)
{
var list = new List<string>();
try { foreach (object o in InvokeArray(sk, "GetDimensions")) AddDim(o, list); } catch { }
try { for (var dd = feat.GetFirstDisplayDimension() as DisplayDimension; dd != null; dd = feat.GetNextDisplayDimension(dd) as DisplayDimension) AddDim(dd, list); } catch { }
return string.Join("", list.Distinct().Take(80));
}
static void AddDim(object o, List<string> list)
{
Dimension d = o as Dimension;
if (d == null && o is DisplayDimension dd) d = dd.GetDimension() as Dimension;
if (d != null) list.Add($"{Safe(() => d.GetNameForSelection())}={R(d.SystemValue * 1000)}mm");
}
static string Relations(Sketch sk)
{
var d = new Dictionary<int, int>();
try { foreach (object o in SketchRelations(sk)) if (o is SketchRelation r) Inc(d, SafeInt(() => r.GetRelationType())); } catch { }
return string.Join("", d.Select(kv => $"约束{kv.Key}:{kv.Value}"));
}
static double Depth(Feature f)
{
try
{
var data = f.GetDefinition() as ExtrudeFeatureData2;
if (data == null) return 0;
return R(data.GetDepth(true) * 1000.0);
}
catch { return 0; }
}
static int ExtrudeEndCondition(Feature f, bool firstDirection)
{
try
{
var data = f.GetDefinition() as ExtrudeFeatureData2;
if (data == null) return -1;
object raw = InvokeCom(data, "GetEndCondition", firstDirection);
if (raw != null) return Convert.ToInt32(raw);
}
catch { }
return -1;
}
static bool IsThroughAllEndCondition(int endCondition)
=> endCondition == (int)swEndConditions_e.swEndCondThroughAll;
static string EndConditionName(int endCondition)
{
if (endCondition == (int)swEndConditions_e.swEndCondBlind) return "blind";
if (endCondition == (int)swEndConditions_e.swEndCondThroughAll) return "through_all";
if (endCondition == (int)swEndConditions_e.swEndCondThroughNext) return "through_next";
if (endCondition == (int)swEndConditions_e.swEndCondUpToVertex) return "up_to_vertex";
if (endCondition == (int)swEndConditions_e.swEndCondUpToSurface) return "up_to_surface";
if (endCondition == (int)swEndConditions_e.swEndCondOffsetFromSurface) return "offset_from_surface";
if (endCondition == (int)swEndConditions_e.swEndCondMidPlane) return "mid_plane";
return endCondition.ToString(System.Globalization.CultureInfo.InvariantCulture);
}
static void AddExtrudeStartArgs(Feature feature, Dictionary<string, object> args)
{
try
{
if (feature.GetDefinition() is not ExtrudeFeatureData2 data)
return;
int fromType = SafeValue(() => data.FromType, (int)swStartConditions_e.swStartSketchPlane);
if (fromType == (int)swStartConditions_e.swStartSketchPlane)
return;
args["start_condition_code"] = fromType;
args["start_condition"] = StartConditionName(fromType);
args["start_offset_mm"] = R(SafeValue(() => data.FromOffsetDistance, 0.0) * 1000.0);
args["flip_start_offset"] = SafeValue(() => data.FromOffsetReverse, false);
object fromEntity = null;
int fromEntityType = 0;
try { data.GetFromEntity(out fromEntity, out fromEntityType); } catch { }
string fromName = ReferenceName(fromEntity);
if (!string.IsNullOrWhiteSpace(fromName))
args["start_reference_name"] = fromName;
if (fromEntityType != 0)
args["start_entity_type_code"] = fromEntityType;
}
catch { }
}
static string StartConditionName(int startCondition)
{
if (startCondition == (int)swStartConditions_e.swStartSketchPlane) return "sketch_plane";
if (startCondition == (int)swStartConditions_e.swStartSurface) return "surface_face_or_plane";
if (startCondition == (int)swStartConditions_e.swStartVertex) return "vertex";
if (startCondition == (int)swStartConditions_e.swStartOffset) return "offset";
return startCondition.ToString(System.Globalization.CultureInfo.InvariantCulture);
}
static List<string> ExtrudeSelectedContourSignatures(ModelDoc2 doc, Feature feature)
{
var result = new List<string>();
try
{
if (feature.GetDefinition() is not ExtrudeFeatureData2 data)
return result;
bool accessed = SafeBool(() => data.AccessSelections(doc, null));
try
{
int count = SafeInt(() => data.GetContoursCount(), 0);
if (count <= 0) return result;
var contours = ToObjectArray(SafeObj(() => data.Contours));
if (contours.Length == 0)
contours = ToObjectArray(SafeObj(() => data.IGetContours(count)));
foreach (object contourObj in contours)
{
if (contourObj is not SketchContour contour)
continue;
string signature = SketchContourSignature(contour);
if (!string.IsNullOrWhiteSpace(signature) && !result.Contains(signature, StringComparer.OrdinalIgnoreCase))
result.Add(signature);
}
}
finally
{
if (accessed) SafeAction(() => data.ReleaseSelectionAccess());
}
}
catch { }
return result;
}
static void RegisterSelectedContourSegmentFilter(ModelDoc2 doc, Feature feature)
{
try
{
if (doc == null || feature == null)
return;
if (feature.GetDefinition() is not ExtrudeFeatureData2 data)
return;
bool accessed = SafeBool(() => data.AccessSelections(doc, null));
try
{
int count = SafeInt(() => data.GetContoursCount(), 0);
if (count <= 0)
return;
var contours = ToObjectArray(SafeObj(() => data.Contours));
if (contours.Length == 0)
contours = ToObjectArray(SafeObj(() => data.IGetContours(count)));
if (contours.Length == 0)
return;
foreach (object contourObj in contours)
{
if (contourObj is not SketchContour contour)
continue;
foreach (object segmentObj in ToObjectArray(SafeObj(() => contour.GetSketchSegments())))
{
if (segmentObj is not SketchSegment segment)
continue;
var segmentId = Id(segment);
if (segmentId == IntPtr.Zero)
continue;
Sketch sketch = SafeObj(() => segment.GetSketch()) as Sketch;
var sketchId = Id(sketch);
if (sketchId == IntPtr.Zero)
continue;
if (!SketchSelectedContourSegmentIdsBySketchId.TryGetValue(sketchId, out var ids))
{
ids = new HashSet<IntPtr>();
SketchSelectedContourSegmentIdsBySketchId[sketchId] = ids;
}
ids.Add(segmentId);
}
}
}
finally
{
if (accessed) SafeAction(() => data.ReleaseSelectionAccess());
}
}
catch { }
}
static string SketchContourSignature(SketchContour contour)
{
var tokens = new List<string>();
foreach (object segObj in ToObjectArray(SafeObj(() => contour.GetSketchSegments())))
{
if (segObj is SketchLine line)
{
if (!Pt(line.GetStartPoint2() as SketchPoint, out double x1, out double y1)
|| !Pt(line.GetEndPoint2() as SketchPoint, out double x2, out double y2))
continue;
string p1 = SigPoint(x1, y1);
string p2 = SigPoint(x2, y2);
if (string.CompareOrdinal(p1, p2) > 0) (p1, p2) = (p2, p1);
tokens.Add($"L:{p1}:{p2}");
}
else if (segObj is SketchArc arc)
{
if (!Pt(arc.GetCenterPoint2() as SketchPoint, out double cx, out double cy)
|| !Pt(arc.GetStartPoint2() as SketchPoint, out double sx, out double sy)
|| !Pt(arc.GetEndPoint2() as SketchPoint, out double ex, out double ey))
continue;
string p1 = SigPoint(sx, sy);
string p2 = SigPoint(ex, ey);
if (string.CompareOrdinal(p1, p2) > 0) (p1, p2) = (p2, p1);
tokens.Add($"A:{SigPoint(cx, cy)}:{p1}:{p2}");
}
}
tokens.Sort(StringComparer.Ordinal);
return string.Join("|", tokens);
}
static string SigPoint(double xMm, double yMm)
=> $"{SigNumber(xMm)},{SigNumber(yMm)}";
static string SigNumber(double value)
=> Math.Round(value, 3).ToString("0.###", System.Globalization.CultureInfo.InvariantCulture);
static bool IsRevolveBossFeature(string n, string t, Feature feature)
{
string s = (n + " " + t).ToLowerInvariant();
if (s.Contains("cut") || s.Contains("切除") || s.Contains("revcut")) return false;
if (s.Contains("revol") || s.Contains("revolution") || s.Contains("旋转") || s.Contains("旋轉"))
{
try
{
if (feature.GetDefinition() is IRevolveFeatureData2 data)
return data.IsBossFeature();
}
catch { }
return true;
}
try
{
if (feature.GetDefinition() is IRevolveFeatureData2 data)
return data.IsBossFeature();
}
catch { }
return false;
}
static bool TryExtractRevolveBoss(ModelDoc2 doc, Feature feature, out Dictionary<string, object> args, out string note)
{
args = new Dictionary<string, object>();
note = "";
try
{
if (feature.GetDefinition() is not IRevolveFeatureData2 data)
{
note = "RevolveFeatureData2 not available";
return false;
}
bool accessed = SafeBool(() => data.AccessSelections(doc, null));
try
{
double angleRad = data.GetRevolutionAngle(true);
if (Math.Abs(angleRad) < 1e-9)
angleRad = data.GetRevolutionAngle(false);
double angleDeg = R(angleRad * 180.0 / Math.PI);
if (angleDeg <= 0.0) angleDeg = 360.0;
bool reverseDir = false;
bool merge = true;
try { reverseDir = data.ReverseDirection; } catch { }
try { merge = data.Merge; } catch { }
args["angle_deg"] = angleDeg;
args["merge"] = merge;
args["reverse_dir"] = reverseDir;
AddRevolveAxisArgs(data, args);
var selectedContours = RevolveSelectedContourSignatures(data);
if (selectedContours.Count > 0)
{
args["selected_contours"] = selectedContours;
args["selected_contour_count"] = selectedContours.Count;
}
note = $"revolve angle={angleDeg}deg, reverse_dir={reverseDir}, merge={merge}";
return true;
}
finally
{
if (accessed) SafeAction(() => data.ReleaseSelectionAccess());
}
}
catch (Exception ex)
{
note = $"revolve extract failed: {ex.Message}";
return false;
}
}
static bool IsRevolveCutFeature(string n, string t, Feature feature)
{
string s = (n + " " + t).ToLowerInvariant();
if (s.Contains("revcut") || s.Contains("revolvecut") || s.Contains("cut-revolve") || s.Contains("cut revolve") || s.Contains("旋转切除"))
return true;
if ((s.Contains("revol") || s.Contains("revolution") || s.Contains("旋转") || s.Contains("鏃嬭浆") || s.Contains("鏃嬭綁")) &&
(s.Contains("cut") || s.Contains("切除") || s.Contains("鍒囬櫎")))
return true;
try
{
if (feature.GetDefinition() is IRevolveFeatureData2 data)
return !data.IsBossFeature();
}
catch { }
return false;
}
static bool TryExtractRevolveCut(ModelDoc2 doc, Feature feature, out Dictionary<string, object> args, out string note)
{
args = new Dictionary<string, object>();
note = "";
try
{
if (feature.GetDefinition() is not IRevolveFeatureData2 data)
{
note = "RevolveFeatureData2 not available";
return false;
}
bool accessed = SafeBool(() => data.AccessSelections(doc, null));
try
{
double angleRad = data.GetRevolutionAngle(true);
if (Math.Abs(angleRad) < 1e-9)
angleRad = data.GetRevolutionAngle(false);
double angleDeg = R(angleRad * 180.0 / Math.PI);
if (angleDeg <= 0.0) angleDeg = 360.0;
bool reverseDir = false;
try { reverseDir = data.ReverseDirection; } catch { }
args["angle_deg"] = angleDeg;
args["reverse_dir"] = reverseDir;
AddRevolveAxisArgs(data, args);
var selectedContours = RevolveSelectedContourSignatures(data);
if (selectedContours.Count > 0)
{
args["selected_contours"] = selectedContours;
args["selected_contour_count"] = selectedContours.Count;
}
note = $"revolve cut angle={angleDeg}deg, reverse_dir={reverseDir}";
return true;
}
finally
{
if (accessed) SafeAction(() => data.ReleaseSelectionAccess());
}
}
catch (Exception ex)
{
note = $"revolve cut extract failed: {ex.Message}";
return false;
}
}
static bool TryExtractLinearPattern(ModelDoc2 doc, Feature feature, string name, string type, out Dictionary<string, object> args, out string note)
{
args = new Dictionary<string, object>();
note = "";
if (!IsLinearPatternFeature(name, type)) return false;
object data = SafeObj(() => feature.GetDefinition());
if (data == null)
{
note = "linear pattern definition not available";
return false;
}
try { TryInvokeCom(data, "AccessSelections", doc, null); } catch { }
try
{
int count1 = FirstPositiveInt(data, new[] { "D1TotalInstances", "D1InstanceCount", "D1Count", "TotalInstances", "Instances" }, 2);
double spacing1Mm = FirstPositiveLengthMm(data, new[] { "D1Spacing", "Spacing", "Direction1Spacing" }, 0.0);
int count2 = FirstPositiveInt(data, new[] { "D2TotalInstances", "D2InstanceCount", "D2Count" }, 1);
double spacing2Mm = FirstPositiveLengthMm(data, new[] { "D2Spacing", "Direction2Spacing" }, 0.0);
bool reverseDir1 = FirstBool(data, new[] { "D1ReverseDirection", "ReverseDirection1", "Dir1ReverseDirection" }, false);
bool reverseDir2 = FirstBool(data, new[] { "D2ReverseDirection", "ReverseDirection2", "Dir2ReverseDirection" }, false);
bool geometryPattern = FirstBool(data, new[] { "GeometryPattern", "GeometryPatternOption" }, true);
if (spacing1Mm <= 0.0)
{
note = "linear pattern spacing1 not available";
return false;
}
if (!TryReadEdgePointMm(data, new[] { "D1Reference", "D1Direction", "Direction1", "Dir1", "Direction1Reference" }, out var dir1PointMm))
{
note = "linear pattern direction1 reference edge not available";
return false;
}
args["count1"] = count1;
args["spacing1_mm"] = spacing1Mm;
args["dir1_edge_x_mm"] = dir1PointMm[0];
args["dir1_edge_y_mm"] = dir1PointMm[1];
args["dir1_edge_z_mm"] = dir1PointMm[2];
args["reverse_dir1"] = reverseDir1;
args["geometry_pattern"] = geometryPattern;
args["count2"] = count2;
args["spacing2_mm"] = spacing2Mm;
args["reverse_dir2"] = reverseDir2;
string seedFeatureName = FirstFeatureName(data, new[] { "PatternFeatureArray", "FeatureArray", "SeedFeatures", "FeaturesToPattern" });
if (!string.IsNullOrWhiteSpace(seedFeatureName))
args["feature_name"] = seedFeatureName;
if (count2 > 1 && TryReadEdgePointMm(data, new[] { "D2Reference", "D2Direction", "Direction2", "Dir2", "Direction2Reference" }, out var dir2PointMm))
{
args["dir2_edge_x_mm"] = dir2PointMm[0];
args["dir2_edge_y_mm"] = dir2PointMm[1];
args["dir2_edge_z_mm"] = dir2PointMm[2];
}
note = $"linear pattern count1={count1}, spacing1={spacing1Mm}mm, count2={count2}";
return true;
}
finally
{
TryInvokeCom(data, "ReleaseSelectionAccess");
}
}
static bool TryExtractCircularPattern(ModelDoc2 doc, Feature feature, string name, string type, out Dictionary<string, object> args, out string note)
{
args = new Dictionary<string, object>();
note = "";
if (!IsCircularPatternFeature(name, type)) return false;
object data = SafeObj(() => feature.GetDefinition());
if (data is not ICircularPatternFeatureData circular)
{
note = "circular pattern definition not available";
return false;
}
bool accessed = SafeBool(() => circular.AccessSelections(doc, null));
try
{
int count = SafeValue(() => circular.TotalInstances, 0);
double spacingRad = SafeValue(() => circular.Spacing, 0.0);
if (count < 2)
{
note = $"circular pattern count invalid: {count}";
return false;
}
if (Math.Abs(spacingRad) <= 1e-9)
{
note = "circular pattern spacing angle not available";
return false;
}
object axisObject = SafeObj(() => circular.Axis);
string axisName = ReferenceName(axisObject);
if (string.IsNullOrWhiteSpace(axisName))
axisName = FirstReferenceName(data, new[] { "Axis", "PatternAxis" });
var axisReference = BuildCircularPatternAxisReference(doc, axisObject, axisName);
string seedFeatureName = FirstFeatureNameFromObject(SafeObj(() => circular.PatternFeatureArray));
if (string.IsNullOrWhiteSpace(seedFeatureName))
seedFeatureName = FirstFeatureName(data, new[] { "PatternFeatureArray", "FeatureArray", "SeedFeatures", "FeaturesToPattern" });
args["count"] = count;
args["angle_deg"] = R(spacingRad * 180.0 / Math.PI);
if (!string.IsNullOrWhiteSpace(axisName))
args["axis_name"] = axisName;
if (axisReference.Count > 0)
args["axis_reference"] = axisReference;
args["reverse_direction"] = SafeValue(() => circular.ReverseDirection, false);
args["equal_spacing"] = SafeValue(() => circular.EqualSpacing, false);
args["geometry_pattern"] = SafeValue(() => circular.GeometryPattern, true);
args["vary_sketch"] = SafeValue(() => circular.VarySketch, false);
if (!string.IsNullOrWhiteSpace(seedFeatureName))
args["source_feature_name"] = seedFeatureName;
note = $"circular pattern count={count}, angle={args["angle_deg"]}deg, axis={axisName}, axis_ref={GetString(axisReference, "reference_type")}, seed={seedFeatureName}";
return true;
}
finally
{
if (accessed) SafeAction(() => circular.ReleaseSelectionAccess());
}
}
static Dictionary<string, object> BuildCircularPatternAxisReference(ModelDoc2 doc, object axisObject, string axisName)
{
var result = new Dictionary<string, object>();
try
{
if (!string.IsNullOrWhiteSpace(axisName))
result["name"] = axisName;
if (axisObject is Edge edge)
{
var signature = EdgeSignature(edge);
string curveType = GetString(signature, "curve_type");
result["reference_type"] = string.Equals(curveType, "circle", StringComparison.OrdinalIgnoreCase)
? "circular_edge"
: string.Equals(curveType, "line", StringComparison.OrdinalIgnoreCase)
? "linear_edge"
: "edge";
result["edge_signature"] = signature;
return result;
}
if (axisObject is Face2 face)
{
var reference = new SketchReferenceResult();
if (TryDescribeReferenceEntity(doc, face, reference) &&
string.Equals(reference.Kind, "face", StringComparison.OrdinalIgnoreCase))
{
var signature = BuildFaceSignatureArgs(reference);
string surfaceType = GetString(signature, "surface_type");
result["reference_type"] = string.Equals(surfaceType, "cylinder", StringComparison.OrdinalIgnoreCase)
? "cylindrical_face"
: "face";
result["face_signature"] = signature;
return result;
}
}
if (axisObject is Feature feature)
{
string typeName = SafeValue(() => feature.GetTypeName2(), "");
string name = SafeValue(() => feature.Name, axisName);
if (!string.IsNullOrWhiteSpace(name))
result["name"] = name;
result["reference_type"] = string.Equals(typeName, "RefAxis", StringComparison.OrdinalIgnoreCase)
? "reference_axis"
: "feature";
result["feature_type"] = typeName;
return result;
}
if (!string.IsNullOrWhiteSpace(axisName))
result["reference_type"] = "reference_axis";
}
catch { }
return result;
}
static void AddRevolveAxisArgs(IRevolveFeatureData2 data, Dictionary<string, object> args)
{
try
{
object axis = SafeObj(() => data.Axis);
if (axis is not SketchLine line)
return;
var sketch = SafeObj(() => InvokeCom(line, "GetSketch")) as Sketch;
if (sketch == null)
return;
var sp = line.GetStartPoint2() as SketchPoint;
var ep = line.GetEndPoint2() as SketchPoint;
if (!Pt3(sp, out double sx, out double sy, out double sz) ||
!Pt3(ep, out double ex, out double ey, out double ez))
return;
if (!TrySketchLocalMmToModelMm(sketch, sx, sy, sz, out var startModel) ||
!TrySketchLocalMmToModelMm(sketch, ex, ey, ez, out var endModel))
return;
args["axis_start_model_mm"] = startModel;
args["axis_end_model_mm"] = endModel;
}
catch { }
}
static List<string> RevolveSelectedContourSignatures(IRevolveFeatureData2 data)
{
var result = new List<string>();
try
{
int count = SafeInt(() => data.GetContoursCount(), 0);
if (count <= 0)
return result;
var contours = ToObjectArray(SafeObj(() => data.Contours));
if (contours.Length == 0)
contours = ToObjectArray(SafeObj(() => data.IGetContours(count)));
foreach (object contourObj in contours)
{
if (contourObj is not SketchContour contour)
continue;
string signature = SketchContourSignature(contour);
if (!string.IsNullOrWhiteSpace(signature) && !result.Contains(signature, StringComparer.OrdinalIgnoreCase))
result.Add(signature);
}
}
catch { }
return result;
}
static bool TryExtractDraft(ModelDoc2 doc, Feature feature, string name, string type, out Dictionary<string, object> args, out string note)
{
args = new Dictionary<string, object>();
note = "";
if (!IsDraftFeature(name, type)) return false;
object data = SafeObj(() => feature.GetDefinition());
if (data == null)
{
note = "draft definition not available";
return false;
}
try { TryInvokeCom(data, "AccessSelections", doc, null); } catch { }
try
{
double angleDeg = FirstPositiveAngleDeg(data, new[] { "DraftAngle", "Angle", "Angle1" }, 0.0);
int neutralFaceIndex = FirstFaceIndex(doc, data, new[] { "NeutralPlane", "NeutralFace", "NeutralFaces", "PullDirectionFace" });
var draftFaceIndices = FaceIndices(doc, data, new[] { "FacesToDraft", "DraftFaces", "Faces", "FaceArray" })
.Where(i => i != neutralFaceIndex)
.Distinct()
.ToList();
if (angleDeg <= 0.0 || neutralFaceIndex <= 0 || draftFaceIndices.Count == 0)
{
note = $"draft incomplete: angle={angleDeg}, neutral={neutralFaceIndex}, faces={draftFaceIndices.Count}";
return false;
}
args["angle_deg"] = angleDeg;
args["neutral_face_index"] = neutralFaceIndex;
args["draft_face_indices"] = draftFaceIndices;
args["flip_dir"] = FirstBool(data, new[] { "FlipDir", "ReverseDirection", "Direction" }, false);
args["edge_draft"] = FirstBool(data, new[] { "EdgeDraft", "IsEdgeDraft" }, false);
args["propagation_type"] = FirstPositiveInt(data, new[] { "PropagationType" }, 0);
args["is_step_draft"] = FirstBool(data, new[] { "StepDraft", "IsStepDraft" }, false);
args["is_body_draft"] = FirstBool(data, new[] { "BodyDraft", "IsBodyDraft" }, false);
note = $"draft angle={angleDeg}deg, neutral_face={neutralFaceIndex}, draft_faces={draftFaceIndices.Count}";
return true;
}
finally
{
TryInvokeCom(data, "ReleaseSelectionAccess");
}
}
static bool TryExtractRib(Feature feature, string name, string type, out Dictionary<string, object> args, out string note)
{
args = new Dictionary<string, object>();
note = "";
if (!IsRibFeature(name, type)) return false;
object data = SafeObj(() => feature.GetDefinition());
double thicknessMm = data != null
? FirstPositiveLengthMm(data, new[] { "Thickness", "RibThickness", "WallThickness" }, 0.0)
: 0.0;
if (thicknessMm <= 0.0)
thicknessMm = FirstFeatureLengthDimensionMm(feature);
if (thicknessMm <= 0.0)
{
note = "rib thickness not available";
return false;
}
args["thickness_mm"] = thicknessMm;
if (data is IRibFeatureData2 rib2)
{
int extrusionDirection = SafeValue(() => rib2.ExtrusionDirection, (int)swRibExtrusionDirection_e.swRibNormalToSketch);
int ribType = SafeValue(() => rib2.Type, (int)swRibType_e.swRibLinear);
args["is_two_sided"] = SafeBool(() => rib2.IsTwoSided);
args["reverse_thickness_dir"] = SafeBool(() => rib2.ReverseThicknessDir);
args["reverse_material_dir"] = SafeBool(() => rib2.FlipSide);
args["is_norm_to_sketch"] = extrusionDirection == (int)swRibExtrusionDirection_e.swRibNormalToSketch;
args["is_drafted"] = SafeBool(() => rib2.EnableDraft);
args["draft_outward"] = SafeBool(() => rib2.DraftOutward);
args["draft_angle_deg"] = R(ToAngleDeg(SafeValue(() => rib2.DraftAngle, 0.0)));
args["is_drafted_from_wall"] = SafeBool(() => rib2.DraftFromWall);
args["reference_edge_index"] = SafeValue(() => rib2.RefSketchIndex, 0);
args["rib_type_code"] = ribType;
args["rib_type"] = ribType == (int)swRibType_e.swRibNatural ? "natural" : "linear";
args["extrusion_direction_code"] = extrusionDirection;
args["extrusion_direction"] = extrusionDirection == (int)swRibExtrusionDirection_e.swRibNormalToSketch ? "normal_to_sketch" : "parallel_to_sketch";
}
else if (data is IRibFeatureData rib1)
{
args["is_two_sided"] = SafeBool(() => rib1.IsTwoSided);
args["reverse_thickness_dir"] = SafeBool(() => rib1.ReverseThicknessDir);
args["reverse_material_dir"] = SafeBool(() => rib1.FlipSide);
args["is_norm_to_sketch"] = true;
args["is_drafted"] = SafeBool(() => rib1.EnableDraft);
args["draft_outward"] = SafeBool(() => rib1.DraftOutward);
args["draft_angle_deg"] = R(ToAngleDeg(SafeValue(() => rib1.DraftAngle, 0.0)));
args["is_drafted_from_wall"] = false;
args["reference_edge_index"] = SafeValue(() => rib1.RefSketchIndex, 0);
}
else if (data != null)
{
args["is_two_sided"] = FirstBool(data, new[] { "IsTwoSided", "BothSides", "TwoSided" }, false);
args["reverse_thickness_dir"] = FirstBool(data, new[] { "ReverseThicknessDir", "ReverseThicknessDirection" }, false);
args["reverse_material_dir"] = FirstBool(data, new[] { "FlipSide", "ReverseMaterialDir", "ReverseMaterialDirection", "FlipMaterialSide" }, false);
int extrusionDirection = FirstPositiveInt(data, new[] { "ExtrusionDirection" }, (int)swRibExtrusionDirection_e.swRibNormalToSketch);
args["is_norm_to_sketch"] = extrusionDirection == (int)swRibExtrusionDirection_e.swRibNormalToSketch;
args["is_drafted"] = FirstBool(data, new[] { "EnableDraft", "IsDrafted", "Drafted" }, false);
args["draft_outward"] = FirstBool(data, new[] { "DraftOutward", "OutwardDraft" }, false);
args["draft_angle_deg"] = FirstPositiveAngleDeg(data, new[] { "DraftAngle", "DraftAngle1" }, 0.0);
args["is_drafted_from_wall"] = FirstBool(data, new[] { "DraftFromWall", "IsDraftedFromWall", "DraftedFromWall" }, false);
args["reference_edge_index"] = FirstPositiveInt(data, new[] { "RefSketchIndex", "ReferenceEdgeIndex" }, 0);
}
note = $"rib thickness={thicknessMm}mm";
return true;
}
static bool TryExtractReferenceAxis(ModelDoc2 doc, Feature feature, string name, string type, out Dictionary<string, object> args, out string note)
{
args = new Dictionary<string, object>();
note = "";
if (!IsReferenceAxisFeature(name, type)) return false;
var edgeSignatures = new List<Dictionary<string, object>>();
var datumPlaneNames = new List<string>();
Dictionary<string, object> faceSignature = new();
object data = SafeObj(() => feature.GetDefinition());
bool accessed = false;
string dataType = data?.GetType().FullName ?? "";
try
{
if (data is IRefAxisFeatureData axisData)
{
accessed = SafeBool(() => axisData.AccessSelections(doc, null));
object selectionTypes = null;
foreach (object selected in ToObjectArray(SafeObj(() => axisData.GetSelections(out selectionTypes))))
AddReferenceAxisSelectionEntity(doc, selected, edgeSignatures, datumPlaneNames, ref faceSignature);
}
else if (data != null)
{
accessed = SafeBool(() => ((dynamic)data).AccessSelections(doc, null));
}
if (doc?.SelectionManager is SelectionMgr selMgr)
{
int count = selMgr.GetSelectedObjectCount2(-1);
for (int i = 1; i <= count; i++)
AddReferenceAxisSelectionEntity(doc, selMgr.GetSelectedObject6(i, -1), edgeSignatures, datumPlaneNames, ref faceSignature);
}
if (edgeSignatures.Count == 0 && datumPlaneNames.Count == 0 && faceSignature.Count == 0 && data != null)
{
foreach (object selected in ToObjectArray(SafeObj(() => InvokeCom(data, "GetSelections"))))
AddReferenceAxisSelectionEntity(doc, selected, edgeSignatures, datumPlaneNames, ref faceSignature);
}
}
finally
{
if (data != null && accessed)
TryInvokeCom(data, "ReleaseSelectionAccess");
}
args["axis_name"] = name;
args["selection_accessed"] = accessed;
if (!string.IsNullOrWhiteSpace(dataType))
args["axis_feature_data_type"] = dataType;
if (edgeSignatures.Count > 0)
args["edge_signatures"] = edgeSignatures;
if (faceSignature.Count > 0)
args["face_signature"] = faceSignature;
if (datumPlaneNames.Count > 0)
args["datum_plane_names"] = datumPlaneNames.Distinct(StringComparer.OrdinalIgnoreCase).ToList();
note = $"reference axis: edges={edgeSignatures.Count}, face={(faceSignature.Count > 0 ? 1 : 0)}, planes={datumPlaneNames.Count}, accessed={accessed}, data={dataType}";
return true;
}
static void AddReferenceAxisSelectionEntity(
ModelDoc2 doc,
object selected,
List<Dictionary<string, object>> edgeSignatures,
List<string> datumPlaneNames,
ref Dictionary<string, object> faceSignature)
{
if (selected == null) return;
if (selected is Edge edge)
{
var signature = EdgeSignature(edge);
if (signature.Count > 0)
edgeSignatures.Add(signature);
return;
}
var reference = new SketchReferenceResult();
if (!TryDescribeReferenceEntity(doc, selected, reference))
return;
if (string.Equals(reference.Kind, "datum_plane", StringComparison.OrdinalIgnoreCase))
{
string planeName = reference.PlaneName;
if (!string.IsNullOrWhiteSpace(planeName))
datumPlaneNames.Add(planeName);
return;
}
if (string.Equals(reference.Kind, "face", StringComparison.OrdinalIgnoreCase) && faceSignature.Count == 0)
faceSignature = ReferenceFaceSignature(reference);
}
static Dictionary<string, object> ReferenceFaceSignature(SketchReferenceResult reference)
{
var result = new Dictionary<string, object>();
if (reference == null) return result;
if (!string.IsNullOrWhiteSpace(reference.SurfaceType)) result["surface_type"] = reference.SurfaceType;
if (reference.AreaMm2 > 0) result["area_mm2"] = reference.AreaMm2;
if (reference.CenterMm != null && reference.CenterMm.Length >= 3)
{
result["center_mm"] = reference.CenterMm;
result["center_x_mm"] = reference.CenterMm[0];
result["center_y_mm"] = reference.CenterMm[1];
result["center_z_mm"] = reference.CenterMm[2];
}
if (reference.BoxMm != null && reference.BoxMm.Length >= 6) result["box_mm"] = reference.BoxMm;
if (reference.Normal != null && reference.Normal.Length >= 3)
{
result["normal"] = reference.Normal;
result["normal_x"] = reference.Normal[0];
result["normal_y"] = reference.Normal[1];
result["normal_z"] = reference.Normal[2];
}
if (reference.FaceIndex > 0) result["fallback_face_index"] = reference.FaceIndex;
if (!string.IsNullOrWhiteSpace(reference.HostFeatureName)) result["host_feature"] = reference.HostFeatureName;
return result;
}
static bool TryExtractShell(ModelDoc2 doc, Feature feature, string name, string type, out Dictionary<string, object> args, out string note)
{
args = new Dictionary<string, object>();
note = "";
if (!IsShellFeature(name, type)) return false;
if (feature.GetDefinition() is not IShellFeatureData data)
{
note = "shell definition not available";
return false;
}
try { data.AccessSelections(doc, null); } catch { }
try
{
double thicknessMm = R(ToLengthMm(data.Thickness));
if (thicknessMm <= 0.0)
{
note = "shell thickness not available";
return false;
}
var removedFaces = new List<Face2>();
try
{
object removedObj = data.FacesRemoved ?? data.IGetFacesRemoved(data.FacesRemovedCount);
foreach (object item in FlattenComObjects(removedObj))
{
if (item is Face2 face)
removedFaces.Add(face);
}
}
catch { }
if (removedFaces.Count == 0)
{
note = "shell remove face not available";
return false;
}
double[] point = FaceCenterMm(removedFaces[0]);
if (point.Length < 3)
{
note = "shell remove face point not available";
return false;
}
args["thickness_mm"] = thicknessMm;
args["x_mm"] = point[0];
args["y_mm"] = point[1];
args["z_mm"] = point[2];
args["outward"] = FirstBool(data, new[] { "Direction", "Outward", "ShellOutward", "ReverseDirection" }, false);
var removeFaceReference = new SketchReferenceResult();
if (TryDescribeReferenceEntity(doc, removedFaces[0], removeFaceReference) &&
string.Equals(removeFaceReference.Kind, "face", StringComparison.OrdinalIgnoreCase))
{
args["remove_face_signature"] = BuildFaceSignatureArgs(removeFaceReference);
if (removeFaceReference.FaceIndex > 0)
args["remove_face_index"] = removeFaceReference.FaceIndex;
if (removeFaceReference.AreaMm2 > 0)
args["remove_face_area_mm2"] = removeFaceReference.AreaMm2;
if (removeFaceReference.CenterMm.Length >= 3)
args["remove_face_center_mm"] = removeFaceReference.CenterMm;
if (removeFaceReference.BoxMm.Length >= 6)
args["remove_face_box_mm"] = removeFaceReference.BoxMm;
if (removeFaceReference.Normal.Length >= 3)
args["remove_face_normal"] = removeFaceReference.Normal;
if (!string.IsNullOrWhiteSpace(removeFaceReference.SurfaceType))
args["remove_face_surface_type"] = removeFaceReference.SurfaceType;
if (!string.IsNullOrWhiteSpace(removeFaceReference.HostFeatureName))
args["remove_face_host_feature"] = removeFaceReference.HostFeatureName;
}
note = $"shell thickness={thicknessMm}mm, remove_face_point=({point[0]},{point[1]},{point[2]})";
return true;
}
finally
{
try { data.ReleaseSelectionAccess(); } catch { }
}
}
static bool TryExtractHoleWizard(ModelDoc2 doc, Feature feature, string name, string type, out Dictionary<string, object> args, out string note)
{
args = new Dictionary<string, object>();
note = "";
if (!IsHoleWizardFeature(name, type)) return false;
object data = SafeObj(() => feature.GetDefinition());
bool accessed = false;
if (data != null)
accessed = SafeBool(() => Convert.ToBoolean(InvokeCom(data, "AccessSelections", doc, null)));
try
{
double diameterMm = data != null
? FirstPositiveLengthMm(data, new[] { "HoleDiameter", "Diameter", "FastenerDiameter" }, 0.0)
: 0.0;
double depthMm = data != null
? FirstPositiveLengthMm(data, new[] { "HoleDepth", "Depth", "EndConditionDepth" }, 0.0)
: 0.0;
bool throughAll = data != null && FirstBool(data, new[] { "ThroughAll", "Through", "IsThrough" }, false);
var dims = FeatureDimensionValuesMm(feature).Where(v => v > 0.0).OrderBy(v => v).ToList();
if (diameterMm <= 0.0 && dims.Count > 0) diameterMm = dims[0];
if (depthMm <= 0.0 && dims.Count > 1) depthMm = dims[^1];
if (diameterMm <= 0.0)
{
note = "hole diameter not available";
return false;
}
if (!throughAll && depthMm <= 0.0)
depthMm = diameterMm * 2.0;
args["diameter_mm"] = diameterMm;
args["depth_mm"] = depthMm;
var positionPoints = HoleWizardPositionPointsModelMm(data, feature);
if (positionPoints.Count > 0)
{
args["position_points_model_mm"] = positionPoints;
args["position_point_source"] = "wizard_hole_feature_data_get_sketch_points";
}
else
{
args["position_points_model_mm"] = new List<List<double>>();
args["position_point_missing"] = "IWizardHoleFeatureData2.GetSketchPoints returned no usable sketch points";
}
args["through_all"] = throughAll;
args["reverse_dir"] = data != null && FirstBool(data, new[] { "ReverseDirection", "FlipDirection" }, false);
AddHoleWizardFeatureDataArgs(doc, data, feature, args);
AddHoleWizardTargetFaceFromSelectionArgs(doc, args);
note = $"hole wizard diameter={diameterMm}mm, depth={depthMm}mm, through_all={throughAll}, points={positionPoints.Count}";
return true;
}
finally
{
if (accessed)
TryInvokeCom(data, "ReleaseSelectionAccess");
}
}
static void AddHoleWizardTargetFaceFromSelectionArgs(ModelDoc2 doc, Dictionary<string, object> args)
{
try
{
if (doc?.SelectionManager is not SelectionMgr selMgr)
return;
int count = selMgr.GetSelectedObjectCount2(-1);
Face2 selectedFace = null;
for (int i = 1; i <= count; i++)
{
object selected = selMgr.GetSelectedObject6(i, -1);
if (selected is Face2 face)
{
selectedFace = face;
break;
}
}
if (selectedFace == null)
return;
var reference = new SketchReferenceResult();
if (!TryDescribeReferenceEntity(doc, selectedFace, reference) ||
!string.Equals(reference.Kind, "face", StringComparison.OrdinalIgnoreCase))
return;
ApplyHoleWizardTargetFaceReferenceArgs(args, reference, "hole_wizard_selection", "");
}
catch { }
}
static void ApplyHoleWizardTargetFaceReferenceArgs(Dictionary<string, object> args, SketchReferenceResult reference, string source, string note)
{
if (args == null || reference == null)
return;
args["target_face_source"] = source;
args["target_face_role"] = "hole_start_face";
args["placement_reference_kind"] = reference.Kind;
args["placement_reference"] = reference.Description;
if (!string.IsNullOrWhiteSpace(reference.HostFeatureName))
args["target_host_feature"] = reference.HostFeatureName;
if (reference.HostFeatureStep > 0)
args["target_host_step"] = reference.HostFeatureStep;
if (reference.FaceIndex > 0)
{
args["target_face_indices"] = new List<int> { reference.FaceIndex };
args["target_face_index"] = reference.FaceIndex;
}
if (!string.IsNullOrWhiteSpace(reference.SurfaceType))
args["target_face_surface_type"] = reference.SurfaceType;
if (reference.AreaMm2 > 0)
args["target_face_area_mm2"] = reference.AreaMm2;
if (reference.CenterMm.Length >= 3)
{
args["target_face_center_mm"] = reference.CenterMm;
args["target_face_x_mm"] = reference.CenterMm[0];
args["target_face_y_mm"] = reference.CenterMm[1];
args["target_face_z_mm"] = reference.CenterMm[2];
}
if (reference.BoxMm.Length >= 6)
args["target_face_box_mm"] = reference.BoxMm;
if (reference.Normal.Length >= 3)
args["target_face_normal"] = reference.Normal;
if (!string.IsNullOrWhiteSpace(reference.PersistReferenceHex))
args["target_face_persist_ref_hex"] = reference.PersistReferenceHex;
var signature = BuildFaceSignatureArgs(reference);
if (signature.Count > 0)
args["target_face_signature"] = signature;
if (!string.IsNullOrWhiteSpace(note))
args["target_face_selection_note"] = note;
}
static bool TryReadArgDouble(Dictionary<string, object> args, string key, out double value)
{
value = 0.0;
if (args == null || !args.TryGetValue(key, out var obj) || obj == null) return false;
try
{
value = Convert.ToDouble(obj, System.Globalization.CultureInfo.InvariantCulture);
return true;
}
catch
{
return false;
}
}
static bool TryReadArgDoubleList(Dictionary<string, object> args, string key, out List<double> values)
{
values = new List<double>();
if (args == null || !args.TryGetValue(key, out var obj) || obj == null) return false;
if (obj is IEnumerable<double> doubles)
{
values = doubles.ToList();
return values.Count > 0;
}
if (obj is IEnumerable<object> objects)
{
foreach (var item in objects)
{
try { values.Add(Convert.ToDouble(item, System.Globalization.CultureInfo.InvariantCulture)); }
catch { return false; }
}
return values.Count > 0;
}
return false;
}
static bool TryReadPointListArg(Dictionary<string, object> args, string key, out List<List<double>> points)
{
points = new List<List<double>>();
if (args == null || !args.TryGetValue(key, out var obj) || obj == null) return false;
if (obj is IEnumerable<List<double>> listPoints)
{
points = listPoints.Select(p => p.ToList()).ToList();
return points.Count > 0;
}
if (obj is IEnumerable<object> objectPoints)
{
foreach (var pointObj in objectPoints)
{
if (pointObj is IEnumerable<double> doubles)
points.Add(doubles.ToList());
else if (pointObj is IEnumerable<object> objects)
points.Add(objects.Select(v => Convert.ToDouble(v, System.Globalization.CultureInfo.InvariantCulture)).ToList());
}
return points.Count > 0;
}
return false;
}
static bool HasHoleWizardTargetFace(Dictionary<string, object> args)
{
return args != null &&
args.TryGetValue("target_face_role", out var roleObj) &&
string.Equals(roleObj?.ToString(), "hole_start_face", StringComparison.OrdinalIgnoreCase);
}
static bool HoleWizardPointsMatchSketch(Feature sketchFeature, Dictionary<string, object> args)
{
if (sketchFeature == null || args == null)
return false;
if (!TryReadPointListArg(args, "position_points_model_mm", out var holePoints) || holePoints.Count == 0)
return false;
var sketchPoints = SketchFreePointModelMmFromFeature(sketchFeature);
if (sketchPoints.Count == 0)
return false;
foreach (var holePoint in holePoints)
{
if (holePoint == null || holePoint.Count < 3)
return false;
bool matched = sketchPoints.Any(sketchPoint =>
sketchPoint.Count >= 3 &&
Distance(holePoint.Take(3).ToArray(), sketchPoint.Take(3).ToArray()) <= 0.25);
if (!matched)
return false;
}
return true;
}
static void AddHoleWizardTargetFaceFromPlacementSketch(ModelDoc2 doc, Feature sketchFeature, Dictionary<string, object> args)
{
try
{
if (sketchFeature == null || sketchFeature.GetSpecificFeature2() is not Sketch sketch)
return;
var debug = new SketchReferenceDebugInfo();
var sketchReference = ResolveSketchPlane(doc, sketchFeature, sketch, debug);
NormalizeGenericDatumPlaneReference(sketch, sketchReference);
if (!string.Equals(sketchReference.Kind, "face", StringComparison.OrdinalIgnoreCase))
{
args["target_face_missing"] = "matched placement sketch did not resolve to a model face";
args["target_face_missing_reference_kind"] = sketchReference.Kind;
args["target_face_missing_reference"] = sketchReference.Description;
return;
}
string sketchName = Safe(() => sketchFeature.Name, "");
ApplyHoleWizardTargetFaceReferenceArgs(
args,
sketchReference,
"placement_sketch_matched_by_wizard_points",
string.IsNullOrWhiteSpace(sketchName) ? "" : $"placement_sketch={sketchName}");
}
catch { }
}
static List<List<double>> SketchFreePointModelMmFromFeature(Feature sketchFeature)
{
var points = new List<List<double>>();
if (sketchFeature == null || sketchFeature.GetSpecificFeature2() is not Sketch sketch)
return points;
var canonicalPoints = BuildSketchPointCanonicalMap(sketch);
var segmentPointIds = new HashSet<IntPtr>();
var segmentPointKeys = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
foreach (object o in ToObjectArray(SafeObj(() => sketch.GetSketchSegments())))
{
if (o is SketchSegment segment)
RegisterSegmentPoints(segment, canonicalPoints, segmentPointIds, segmentPointKeys);
}
var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
foreach (double[] pointLocal in UserSketchPointsLocalMm(sketch))
{
if (pointLocal.Length < 3) continue;
double x = pointLocal[0], y = pointLocal[1], z = pointLocal[2];
if (segmentPointKeys.Contains(SketchPointCoordKey(x, y, z))) continue;
if (!TrySketchLocalMmToModelMm(sketch, x, y, z, out var modelPoint)) continue;
string key = string.Join(",", modelPoint.Select(v => R(v).ToString(System.Globalization.CultureInfo.InvariantCulture)));
if (seen.Add(key))
points.Add(modelPoint);
}
return points;
}
static Feature FindFeatureOwningSketch(Feature feature, Sketch sketch)
{
if (feature == null || sketch == null) return null;
var targetId = Id(sketch);
Feature sub = feature.GetFirstSubFeature() as Feature;
for (int i = 0; sub != null && i < 300; i++)
{
try
{
if (sub.GetSpecificFeature2() is Sketch candidate && Id(candidate) == targetId)
return sub;
}
catch { }
sub = sub.GetNextSubFeature() as Feature;
}
return null;
}
static void AddHoleWizardFeatureDataArgs(ModelDoc2 doc, object data, Feature feature, Dictionary<string, object> args)
{
if (data == null) return;
args["hole_wizard_api"] = "HoleWizard5";
if (TryReadIntMember(data, "Type", out int holeType))
{
args["hole_type"] = holeType;
args["hole_type_name"] = EnumName(typeof(swWzdHoleTypes_e), holeType);
}
int genericHoleType = InferWizardGenericHoleType(holeType);
if (genericHoleType != int.MinValue)
{
args["generic_hole_type"] = genericHoleType;
args["generic_hole_type_name"] = EnumName(typeof(swWzdGeneralHoleTypes_e), genericHoleType);
args["generic_hole_type_source"] = "derived_from_iwizardholefeaturedata2_type";
}
AddIntArgAllowZero(args, "standard_index", data, new[] { "Standard2", "Standard", "StandardIndex", "HoleStandard" }, typeof(swWzdHoleStandards_e), "standard_name");
AddIntArgAllowZero(args, "fastener_type_index", data, new[] { "FastenerType2", "FastenerType", "TypeIndex" }, typeof(swWzdHoleStandardFastenerTypes_e), "fastener_type_name");
AddStringArg(args, "size", data, new[] { "FastenerSize", "Size", "SSize", "HoleSize" });
AddStringArg(args, "thread_class", data, new[] { "ThreadClass", "Class" });
AddStringArg(args, "thread_callout", data, new[] { "ThreadCallout", "Callout" });
AddIntArgAllowZero(args, "end_condition_code", data, new[] { "EndCondition" }, typeof(swEndConditions_e), "end_condition_name");
AddIntArgAllowZero(args, "thread_end_condition_code", data, new[] { "ThreadEndCondition", "ThreadEndType" }, null, "");
AddIntArgAllowZero(args, "cosmetic_thread_type", data, new[] { "CosmeticThreadType", "ThreadType" }, null, "");
AddIntArgAllowZero(args, "tap_type", data, new[] { "TapType" }, null, "");
AddIntArgAllowZero(args, "hole_fit", data, new[] { "HoleFit" }, null, "");
AddIntArgAllowZero(args, "head_clearance_type", data, new[] { "HeadClearanceType" }, null, "");
AddLengthArg(args, "hole_diameter_mm", data, new[] { "HoleDiameter", "Diameter" });
AddLengthArg(args, "hole_depth_mm", data, new[] { "HoleDepth", "Depth" });
AddLengthArg(args, "length_mm", data, new[] { "Length" });
AddLengthArg(args, "tap_drill_diameter_mm", data, new[] { "TapDrillDiameter" });
AddLengthArg(args, "tap_drill_depth_mm", data, new[] { "TapDrillDepth" });
AddLengthArg(args, "thread_diameter_mm", data, new[] { "ThreadDiameter", "TapDrillDiameter" });
AddLengthArg(args, "thread_depth_mm", data, new[] { "ThreadDepth", "TapDepth" });
AddLengthArg(args, "major_diameter_mm", data, new[] { "MajorDiameter" });
AddLengthArg(args, "minor_diameter_mm", data, new[] { "MinorDiameter" });
AddLengthArg(args, "thru_hole_diameter_mm", data, new[] { "ThruHoleDiameter" });
AddLengthArg(args, "thru_hole_depth_mm", data, new[] { "ThruHoleDepth" });
AddLengthArg(args, "thru_tap_drill_diameter_mm", data, new[] { "ThruTapDrillDiameter" });
AddLengthArg(args, "thru_tap_drill_depth_mm", data, new[] { "ThruTapDrillDepth" });
AddLengthArg(args, "counter_bore_diameter_mm", data, new[] { "CounterBoreDiameter" });
AddLengthArg(args, "counter_bore_depth_mm", data, new[] { "CounterBoreDepth" });
AddLengthArg(args, "counter_sink_diameter_mm", data, new[] { "CounterSinkDiameter" });
AddLengthArg(args, "near_counter_sink_diameter_mm", data, new[] { "NearCounterSinkDiameter" });
AddLengthArg(args, "mid_counter_sink_diameter_mm", data, new[] { "MidCounterSinkDiameter" });
AddLengthArg(args, "far_counter_sink_diameter_mm", data, new[] { "FarCounterSinkDiameter" });
AddLengthArg(args, "counter_drill_diameter_mm", data, new[] { "CounterDrillDiameter" });
AddLengthArg(args, "counter_drill_depth_mm", data, new[] { "CounterDrillDepth" });
AddLengthArg(args, "head_clearance_mm", data, new[] { "HeadClearance" });
AddLengthArg(args, "offset_distance_mm", data, new[] { "OffsetDistance" });
AddLengthArg(args, "drill_angle_deg", data, new[] { "DrillAngle", "TipAngle" }, isAngle: true);
AddLengthArg(args, "thread_angle_deg", data, new[] { "ThreadAngle" }, isAngle: true);
AddLengthArg(args, "counter_sink_angle_deg", data, new[] { "CounterSinkAngle" }, isAngle: true);
AddLengthArg(args, "near_counter_sink_angle_deg", data, new[] { "NearCounterSinkAngle" }, isAngle: true);
AddLengthArg(args, "mid_counter_sink_angle_deg", data, new[] { "MidCounterSinkAngle" }, isAngle: true);
AddLengthArg(args, "far_counter_sink_angle_deg", data, new[] { "FarCounterSinkAngle" }, isAngle: true);
AddLengthArg(args, "counter_drill_angle_deg", data, new[] { "CounterDrillAngle" }, isAngle: true);
args["feature_scope"] = FirstBool(data, new[] { "FeatureScope" }, true);
args["auto_select"] = FirstBool(data, new[] { "AutoSelect" }, true);
args["assembly_feature_scope"] = FirstBool(data, new[] { "AssemblyFeatureScope" }, false);
args["auto_select_components"] = FirstBool(data, new[] { "AutoSelectComponents" }, false);
args["propagate_feature_to_parts"] = FirstBool(data, new[] { "PropagateFeatureToParts" }, false);
var targetFaceIndices = FaceIndices(doc, data, new[] { "Face", "Faces", "FaceArray", "TargetFace", "TargetFaces", "PlacementFace", "PlacementFaces" }).Distinct().ToList();
if (targetFaceIndices.Count > 0)
args["target_face_indices"] = targetFaceIndices;
Face2 face = FirstFace(data, new[] { "Face", "Faces", "FaceArray", "TargetFace", "TargetFaces", "PlacementFace", "PlacementFaces" });
if (face != null)
{
var reference = new SketchReferenceResult();
if (TryDescribeReferenceEntity(doc, face, reference) &&
string.Equals(reference.Kind, "face", StringComparison.OrdinalIgnoreCase))
ApplyHoleWizardTargetFaceReferenceArgs(args, reference, "wizard_hole_feature_data_face", "");
args["target_face_area_mm2"] = R(SafeDouble(() => face.GetArea()) * 1_000_000.0);
var center = FaceCenterMm(face);
if (center.Length >= 3)
args["target_face_center_mm"] = center;
var box = TryGetFaceBoxMm(face);
if (box.Length >= 6)
args["target_face_box_mm"] = box;
var normal = FaceNormal(face);
if (normal.Length >= 3)
args["target_face_normal"] = normal;
}
}
static void AddIntArg(Dictionary<string, object> args, string key, object data, IEnumerable<string> names)
{
int value = FirstPositiveInt(data, names, int.MinValue);
if (value != int.MinValue) args[key] = value;
}
static void AddIntArgAllowZero(Dictionary<string, object> args, string key, object data, IEnumerable<string> names, Type enumType = null, string enumKey = "")
{
int value = FirstReadableInt(data, names, int.MinValue);
if (value == int.MinValue) return;
args[key] = value;
if (enumType != null && !string.IsNullOrWhiteSpace(enumKey))
args[enumKey] = EnumName(enumType, value);
}
static int FirstReadableInt(object data, IEnumerable<string> names, int fallback)
{
foreach (string name in names)
{
if (TryReadIntMember(data, name, out int value))
return value;
}
return fallback;
}
static bool TryReadIntMember(object data, string name, out int value)
{
value = 0;
object raw = SafeObj(() => GetComProperty(data, name)) ?? InvokeCom(data, name);
if (raw == null) return false;
try
{
value = Convert.ToInt32(raw);
return true;
}
catch
{
return int.TryParse(raw.ToString(), out value);
}
}
static int InferWizardGenericHoleType(int holeType)
{
if (holeType >= (int)swWzdHoleTypes_e.swPipeTapBlind && holeType <= (int)swWzdHoleTypes_e.swPipeTapThruCounterSinkTopBottom)
return (int)swWzdGeneralHoleTypes_e.swWzdPipeTap;
if ((holeType >= (int)swWzdHoleTypes_e.swTapBlind && holeType <= (int)swWzdHoleTypes_e.swTapThruCounterSinkTopBottom)
|| (holeType >= (int)swWzdHoleTypes_e.swTapBlindCosmeticThread && holeType <= (int)swWzdHoleTypes_e.swTapBlindRemoveThread))
return (int)swWzdGeneralHoleTypes_e.swWzdTap;
if (holeType >= (int)swWzdHoleTypes_e.swCounterBoreSlotBlind)
return (int)swWzdGeneralHoleTypes_e.swWzdHoleSlot;
if (holeType >= (int)swWzdHoleTypes_e.swCounterBoreBlind && holeType <= (int)swWzdHoleTypes_e.swCounterBoreThruCounterSinkTopMiddleBottom)
return (int)swWzdGeneralHoleTypes_e.swWzdCounterBore;
if (holeType == (int)swWzdHoleTypes_e.swCounterSunk
|| holeType == (int)swWzdHoleTypes_e.swCounterSinkBlind
|| holeType == (int)swWzdHoleTypes_e.swCounterSinkThru
|| holeType == (int)swWzdHoleTypes_e.swCounterSinkBlindWithoutHeadClearance
|| holeType == (int)swWzdHoleTypes_e.swCounterSinkThruWithoutHeadClearance
|| holeType == (int)swWzdHoleTypes_e.swCounterSinkThruCounterSinkBottomWithoutHeadClearance)
return (int)swWzdGeneralHoleTypes_e.swWzdCounterSink;
if (holeType != 0)
return (int)swWzdGeneralHoleTypes_e.swWzdHole;
return int.MinValue;
}
static string EnumName(Type enumType, int value)
{
try
{
string name = Enum.GetName(enumType, value);
return string.IsNullOrWhiteSpace(name) ? value.ToString(System.Globalization.CultureInfo.InvariantCulture) : name;
}
catch
{
return value.ToString(System.Globalization.CultureInfo.InvariantCulture);
}
}
static void AddStringArg(Dictionary<string, object> args, string key, object data, IEnumerable<string> names)
{
foreach (string name in names)
{
object value = SafeObj(() => GetComProperty(data, name)) ?? InvokeCom(data, name);
string text = value?.ToString() ?? "";
if (!string.IsNullOrWhiteSpace(text))
{
args[key] = text;
return;
}
}
}
static void AddLengthArg(Dictionary<string, object> args, string key, object data, IEnumerable<string> names, bool isAngle = false)
{
foreach (string name in names)
{
double value = ReadDoubleMember(data, name);
if (Math.Abs(value) <= 1e-9) continue;
args[key] = isAngle ? R(ToAngleDeg(value)) : R(ToLengthMm(value));
return;
}
}
static List<List<double>> HoleWizardPositionPointsModelMm(object data, Feature feature)
{
var points = new List<List<double>>();
if (data is not IWizardHoleFeatureData2 wizardData)
return points;
Sketch sketch = FirstSketchFromFeature(feature);
var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
void AddPoint(SketchPoint point)
{
if (point == null) return;
double x = point.X * 1000.0;
double y = point.Y * 1000.0;
double z = point.Z * 1000.0;
List<double> modelPoint;
if (sketch != null && TrySketchLocalMmToModelMm(sketch, x, y, z, out var transformedPoint))
modelPoint = transformedPoint;
else
modelPoint = new List<double> { R(x), R(y), R(z) };
string key = string.Join(",", modelPoint.Select(v => R(v).ToString(System.Globalization.CultureInfo.InvariantCulture)));
if (seen.Add(key))
points.Add(modelPoint);
}
foreach (object pointObj in ToObjectArray(SafeObj(() => wizardData.GetSketchPoints())))
{
if (pointObj is SketchPoint point)
AddPoint(point);
}
if (points.Count > 0)
return points;
int count = SafeInt(() => wizardData.GetSketchPointCount(), 0);
if (count <= 0)
return points;
object rawPoints = SafeObj(() => InvokeCom(data, "IGetSketchPoints", count)) ??
SafeObj(() => wizardData.IGetSketchPoints(count));
foreach (object pointObj in ToObjectArray(rawPoints))
{
if (pointObj is SketchPoint point)
AddPoint(point);
}
return points;
}
static List<List<double>> DominantCoplanarPoints(List<List<double>> candidates)
{
if (candidates == null || candidates.Count == 0) return new List<List<double>>();
var best = new List<List<double>>();
const double bucketMm = 0.01;
for (int axis = 0; axis < 3; axis++)
{
var groups = new Dictionary<long, List<List<double>>>();
foreach (var point in candidates)
{
if (point == null || point.Count < 3) continue;
long bucket = (long)Math.Round(point[axis] / bucketMm);
if (!groups.TryGetValue(bucket, out var group))
{
group = new List<List<double>>();
groups[bucket] = group;
}
group.Add(point);
}
foreach (var group in groups.Values)
{
if (group.Count > best.Count)
best = group;
}
}
var result = new List<List<double>>();
var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
foreach (var point in best)
{
string key = string.Join(",", point.Select(v => R(v).ToString(System.Globalization.CultureInfo.InvariantCulture)));
if (seen.Add(key))
result.Add(point);
}
return result;
}
static bool TryExtractMirror(ModelDoc2 doc, Feature feature, string name, string type, out Dictionary<string, object> args, out string note)
{
args = new Dictionary<string, object>();
note = "";
if (!IsMirrorFeature(name, type)) return false;
object data = SafeObj(() => feature.GetDefinition());
bool accessed = false;
if (data != null)
{
IMirrorPatternFeatureData typedData = data as IMirrorPatternFeatureData;
if (typedData != null)
accessed = SafeBool(() => typedData.AccessSelections(doc, null));
else
accessed = SafeBool(() => Convert.ToBoolean(InvokeCom(data, "AccessSelections", doc, null)));
var seedFeatures = typedData != null
? FeatureNamesFromObject(typedData.PatternFeatureArray)
: new List<string>();
if (seedFeatures.Count == 0)
seedFeatures = FeatureNames(data, new[] { "PatternFeatureArray", "FeatureArray", "SeedFeatures", "FeaturesToMirror" });
if (seedFeatures.Count > 0)
{
args["feature_name"] = seedFeatures[0];
args["feature_names"] = seedFeatures;
args["feature_count"] = seedFeatures.Count;
}
string planeName = typedData != null
? ReferenceName(typedData.Plane)
: "";
if (string.IsNullOrWhiteSpace(planeName))
planeName = FirstReferenceName(data, new[] { "Plane", "MirrorFaceOrPlane", "MirrorPlane", "MirrorReference" });
if (!string.IsNullOrWhiteSpace(planeName))
args["mirror_plane_name"] = planeName;
args["geometry_pattern"] = FirstBool(data, new[] { "GeometryPattern", "GeometryPatternOption" }, true);
args["pattern_seed_only"] = FirstBool(data, new[] { "PatternSeedOnly" }, false);
args["propagate_visual_property"] = FirstBool(data, new[] { "PropagateVisualProperty" }, true);
if (accessed)
{
try
{
if (typedData != null) typedData.ReleaseSelectionAccess();
else InvokeCom(data, "ReleaseSelectionAccess");
}
catch { }
}
}
if (!args.ContainsKey("mirror_plane_name"))
{
args["mirror_plane_missing"] = true;
note = $"mirror features={args.GetValueOrDefault("feature_count", 0)}, first={args.GetValueOrDefault("feature_name", "")}, plane=unresolved";
}
else
{
note = $"mirror features={args.GetValueOrDefault("feature_count", 0)}, first={args.GetValueOrDefault("feature_name", "")}, plane={args["mirror_plane_name"]}";
}
return true;
}
static bool TryExtractWrap(ModelDoc2 doc, Feature feature, string name, string type, out Dictionary<string, object> args, out string note)
{
args = new Dictionary<string, object>();
note = "";
if (!IsWrapFeature(name, type)) return false;
object data = SafeObj(() => feature.GetDefinition());
if (data == null)
{
note = "wrap definition not available";
return false;
}
try { TryInvokeCom(data, "AccessSelections", doc, null); } catch { }
try
{
double thicknessMm = FirstPositiveLengthMm(data, new[] { "Thickness", "Depth", "WrapThickness" }, 0.0);
var faceIndices = FaceIndices(doc, data, new[] { "Faces", "TargetFaces", "WrapFaces", "FaceArray" }).Distinct().ToList();
string sketchName = FirstReferenceName(data, new[] { "Sketch", "ProfileSketch", "SourceSketch" });
args["thickness_mm"] = thicknessMm;
if (faceIndices.Count > 0) args["target_face_indices"] = faceIndices;
if (!string.IsNullOrWhiteSpace(sketchName)) args["sketch_name"] = sketchName;
args["wrap_type"] = FirstPositiveInt(data, new[] { "WrapType", "Type" }, 0);
args["reverse_direction"] = FirstBool(data, new[] { "ReverseDirection", "FlipDirection" }, false);
args["method"] = FirstPositiveInt(data, new[] { "Method" }, 0);
args["spline_surface_quality"] = FirstPositiveInt(data, new[] { "SplineSurfaceQuality", "SurfaceQuality" }, 1);
note = $"wrap thickness={thicknessMm}mm, target_faces={faceIndices.Count}, sketch={sketchName}";
return true;
}
finally
{
TryInvokeCom(data, "ReleaseSelectionAccess");
}
}
static bool TryExtractHelix(Feature feature, string name, string type, out Dictionary<string, object> args, out string note)
{
args = new Dictionary<string, object>();
note = "";
if (!IsHelixFeature(name, type)) return false;
object data = SafeObj(() => feature.GetDefinition());
if (data is not IHelixFeatureData helix)
{
note = "helix definition not available";
return false;
}
args["feature_name"] = name;
args["defined_by"] = helix.DefinedBy;
args["height_mm"] = R(helix.Height * 1000.0);
args["pitch_mm"] = R(helix.Pitch * 1000.0);
args["revolution"] = R(helix.Revolution);
args["starting_angle_deg"] = R(helix.StartingAngle * 180.0 / Math.PI);
args["reverse_direction"] = helix.ReverseDirection;
args["clockwise"] = helix.Clockwise;
args["taper"] = helix.Taper;
args["taper_angle_deg"] = R(helix.TaperAngle * 180.0 / Math.PI);
args["taper_outward"] = helix.TaperOutward;
args["variable_pitch"] = helix.VariablePitch;
note = $"helix sketch=last, defined_by={helix.DefinedBy}, height={args["height_mm"]}mm, pitch={args["pitch_mm"]}mm, revolution={args["revolution"]}";
return true;
}
static bool TryExtractSweep(ModelDoc2 doc, Feature feature, string name, string type, out string skill, out Dictionary<string, object> args, out string note)
{
skill = "";
args = new Dictionary<string, object>();
note = "";
if (!IsSweepFeature(name, type)) return false;
bool isCut = IsCutLike(name, type);
object data = SafeObj(() => feature.GetDefinition());
if (data is ISweepFeatureData sweepData)
{
bool accessed = SafeBool(() => sweepData.AccessSelections(doc, null));
try
{
object profileObj = SafeObj(() => sweepData.Profile);
string profileName = CanonicalReplayReferenceName(profileObj);
string profileNameSource = ReferenceName(profileObj);
string subSketchProfileName = FirstCanonicalSubSketchName(feature);
if (!string.IsNullOrWhiteSpace(subSketchProfileName) &&
!string.Equals(profileName, subSketchProfileName, StringComparison.OrdinalIgnoreCase))
profileName = subSketchProfileName;
string pathName = ReferenceName(SafeObj(() => sweepData.Path));
bool circularProfile = SafeBool(() => sweepData.CircularProfile);
int direction = SafeInt(() => sweepData.Direction, 0);
if (!circularProfile && isCut && !string.IsNullOrWhiteSpace(profileName) && !string.IsNullOrWhiteSpace(pathName))
{
skill = "swept_cut_profile_path";
args["profile_name"] = profileName;
args["path_name"] = pathName;
args["direction"] = direction;
args["circular_profile"] = false;
if (!string.IsNullOrWhiteSpace(profileNameSource) &&
!string.Equals(profileName, profileNameSource, StringComparison.OrdinalIgnoreCase))
args["source_profile_name"] = profileNameSource;
note = $"sweep cut profile={profileName}, path={pathName}, direction={direction}";
return true;
}
double circularDiameterMm = R(sweepData.CircularProfileDiameter * 1000.0);
if (circularProfile && circularDiameterMm > 0.0)
{
skill = isCut ? "swept_cut_circular_profile_mm" : "swept_boss_circular_profile_mm";
args["diameter_mm"] = circularDiameterMm;
if (!string.IsNullOrWhiteSpace(pathName)) args["path_sketch_name"] = pathName;
args["direction"] = direction;
if (!isCut) args["merge"] = true;
note = $"sweep circular diameter={circularDiameterMm}mm, path={pathName}, direction={direction}";
return true;
}
note = $"sweep profile/path unresolved: circular={circularProfile}, profile={profileName}, path={pathName}";
return false;
}
finally
{
if (accessed) SafeAction(() => sweepData.ReleaseSelectionAccess());
}
}
double diameterMm = FirstFeatureLengthDimensionMm(feature);
if (diameterMm <= 0.0)
{
note = "sweep definition/profile diameter not available";
return false;
}
var sketchNames = SketchFeatureNames(feature);
string pathSketchName = sketchNames.Count > 0 ? sketchNames[^1] : "";
skill = isCut ? "swept_cut_circular_profile_mm" : "swept_boss_circular_profile_mm";
args["diameter_mm"] = diameterMm;
if (!string.IsNullOrWhiteSpace(pathSketchName)) args["path_sketch_name"] = pathSketchName;
args["direction"] = 0;
if (!isCut) args["merge"] = true;
note = $"sweep circular diameter={diameterMm}mm, path_sketch={pathSketchName}";
return true;
}
static bool TryExtractLoftOrBoundary(Feature feature, string name, string type, out string skill, out Dictionary<string, object> args, out string note)
{
skill = "";
args = new Dictionary<string, object>();
note = "";
bool isBoundary = IsBoundaryFeature(name, type);
bool isLoft = IsLoftFeature(name, type);
if (!isBoundary && !isLoft) return false;
bool isCut = IsCutLike(name, type);
var sketchNames = SketchFeatureNames(feature).Distinct(StringComparer.OrdinalIgnoreCase).ToList();
if (sketchNames.Count >= 2)
args["profile_sketch_names"] = sketchNames;
args["closed"] = false;
if (!isCut) args["merge"] = true;
if (isBoundary)
skill = isCut ? "boundary_cut_from_profiles" : "boundary_boss_from_profiles";
else
skill = isCut ? "loft_cut_from_profiles" : "loft_boss_from_profiles";
note = $"{(isBoundary ? "boundary" : "loft")} profiles={sketchNames.Count}";
return true;
}
static int FirstPositiveInt(object data, IEnumerable<string> names, int fallback)
{
foreach (string name in names)
{
double value = ReadDoubleMember(data, name);
if (Math.Abs(value) > 1e-9)
return RInt(value);
}
return fallback;
}
static double FirstPositiveLengthMm(object data, IEnumerable<string> names, double fallback)
{
foreach (string name in names)
{
double value = ReadDoubleMember(data, name);
if (Math.Abs(value) > 1e-9)
return R(ToLengthMm(value));
}
return fallback;
}
static double FirstPositiveAngleDeg(object data, IEnumerable<string> names, double fallback)
{
foreach (string name in names)
{
double value = ReadDoubleMember(data, name);
if (Math.Abs(value) > 1e-9)
return R(ToAngleDeg(value));
}
return fallback;
}
static bool FirstBool(object data, IEnumerable<string> names, bool fallback)
{
foreach (string name in names)
{
if (TryReadBoolMember(data, name, out bool value))
return value;
}
return fallback;
}
static double ToLengthMm(double value)
{
// SolidWorks feature data lengths are normally meters. Some dimension fallbacks
// already arrive in mm, so avoid multiplying obvious mm-scale values.
return Math.Abs(value) < 10.0 ? value * 1000.0 : value;
}
static double ToAngleDeg(double value)
{
return Math.Abs(value) <= Math.PI * 2.0 + 1e-6 ? value * 180.0 / Math.PI : value;
}
static bool TryReadEdgePointMm(object data, IEnumerable<string> names, out double[] point)
{
point = Array.Empty<double>();
foreach (string name in names)
{
object value = SafeObj(() => GetComProperty(data, name));
foreach (object item in FlattenComObjects(value))
{
if (TryAsEdge(item, out Edge edge))
{
point = EdgePointMm(edge);
if (point.Length >= 3)
return true;
}
}
}
return false;
}
static double[] EdgePointMm(Edge edge)
{
var signature = EdgeSignature(edge);
if (signature.TryGetValue("center_mm", out object centerObj))
{
double[] center = ToDoubleArray(centerObj);
if (center.Length >= 3) return new[] { R(center[0]), R(center[1]), R(center[2]) };
}
if (signature.TryGetValue("start_mm", out object startObj))
{
double[] start = ToDoubleArray(startObj);
if (start.Length >= 3) return new[] { R(start[0]), R(start[1]), R(start[2]) };
}
return Array.Empty<double>();
}
static string FirstFeatureName(object data, IEnumerable<string> names)
{
foreach (string name in names)
{
object value = SafeObj(() => GetComProperty(data, name));
string featureName = FirstFeatureNameFromObject(value);
if (!string.IsNullOrWhiteSpace(featureName))
return featureName;
}
return "";
}
static List<string> FeatureNames(object data, IEnumerable<string> names)
{
var result = new List<string>();
var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
foreach (string name in names)
{
object value = SafeObj(() => GetComProperty(data, name));
foreach (string featureName in FeatureNamesFromObject(value))
{
if (seen.Add(featureName))
result.Add(featureName);
}
if (result.Count > 0)
break;
}
return result;
}
static string FirstFeatureNameFromObject(object value)
{
foreach (string featureName in FeatureNamesFromObject(value))
return featureName;
return "";
}
static List<string> FeatureNamesFromObject(object value)
{
var result = new List<string>();
var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
foreach (object item in FlattenComObjects(value))
{
if (item is Feature feature)
{
string featureName = Safe(() => feature.Name);
if (!string.IsNullOrWhiteSpace(featureName) && seen.Add(featureName))
result.Add(featureName);
}
}
return result;
}
static string FirstReferenceName(object data, IEnumerable<string> names)
{
foreach (string name in names)
{
object value = SafeObj(() => GetComProperty(data, name));
foreach (object item in FlattenComObjects(value))
{
string referenceName = ReferenceName(item);
if (!string.IsNullOrWhiteSpace(referenceName))
return referenceName;
}
}
return "";
}
static string ReferenceName(object item)
{
if (item == null) return "";
if (item is Feature feature) return Safe(() => feature.Name);
if (item is RefPlane)
{
object refPlaneFeature = InvokeCom(item, "GetFeature");
if (refPlaneFeature is Feature planeFeature) return Safe(() => planeFeature.Name);
}
try
{
object ownerFeature = InvokeCom(item, "GetFeature");
if (ownerFeature is Feature featureOwner) return Safe(() => featureOwner.Name);
}
catch { }
return "";
}
static string CanonicalReplayReferenceName(object item)
{
if (item is Feature feature)
{
string type = Safe(() => feature.GetTypeName2());
string name = Safe(() => feature.Name);
if (IsSketch(type))
return CanonicalSketchPlanName(feature, name);
return name;
}
try
{
object ownerFeature = InvokeCom(item, "GetFeature");
if (ownerFeature is Feature featureOwner)
{
string type = Safe(() => featureOwner.GetTypeName2());
string name = Safe(() => featureOwner.Name);
if (IsSketch(type))
return CanonicalSketchPlanName(featureOwner, name);
return name;
}
}
catch { }
return ReferenceName(item);
}
static string FirstCanonicalSubSketchName(Feature feature)
{
Feature sub = SafeObj(() => feature.GetFirstSubFeature()) as Feature;
for (int i = 0; sub != null && i < 200; i++)
{
string type = Safe(() => sub.GetTypeName2());
if (IsSketch(type))
return CanonicalSketchPlanName(sub, Safe(() => sub.Name));
sub = SafeObj(() => sub.GetNextSubFeature()) as Feature;
}
return "";
}
static int FirstFaceIndex(ModelDoc2 doc, object data, IEnumerable<string> names)
{
foreach (int index in FaceIndices(doc, data, names))
return index;
return 0;
}
static List<int> FaceIndices(ModelDoc2 doc, object data, IEnumerable<string> names)
{
var result = new List<int>();
foreach (string name in names)
{
object value = SafeObj(() => GetComProperty(data, name));
foreach (object item in FlattenComObjects(value))
{
if (item is Face2 face)
{
int index = FindFaceIndexLikeExecutor(doc, face);
if (index > 0) result.Add(index);
}
}
}
return result;
}
static Face2 FirstFace(object data, IEnumerable<string> names)
{
foreach (string name in names)
{
object value = SafeObj(() => GetComProperty(data, name));
foreach (object item in FlattenComObjects(value))
{
if (item is Face2 face) return face;
}
}
return null;
}
static double[] FaceCenterMm(Face2 face)
{
double[] box = TryGetFaceBoxMm(face);
if (box.Length >= 6)
{
return new[]
{
R((box[0] + box[3]) / 2.0),
R((box[1] + box[4]) / 2.0),
R((box[2] + box[5]) / 2.0)
};
}
return Array.Empty<double>();
}
static List<double> FeatureDimensionValuesMm(Feature feature)
{
var values = new List<double>();
try
{
object dimObj = feature.GetFirstDisplayDimension();
while (dimObj != null)
{
object dimensionObj = InvokeCom(dimObj, "GetDimension");
if (dimensionObj is Dimension dimension)
{
double value = SafeDouble(() => dimension.SystemValue);
if (Math.Abs(value) > 1e-9)
values.Add(R(value * 1000.0));
}
dimObj = InvokeCom(dimObj, "GetNext");
}
}
catch { }
return values;
}
static double[] FirstSketchPointMm(Feature feature)
{
foreach (Sketch sketch in SketchesFromFeature(feature))
{
foreach (object pointObj in InvokeArray(sketch, "GetSketchPoints2").Concat(InvokeArray(sketch, "GetSketchPoints")))
{
if (pointObj is SketchPoint point && Pt(point, out double x, out double y))
return new[] { R(x), R(y) };
}
}
return Array.Empty<double>();
}
static List<string> SketchFeatureNames(Feature feature)
{
var names = new List<string>();
Feature sub = SafeObj(() => feature.GetFirstSubFeature()) as Feature;
int guard = 0;
while (sub != null && guard++ < 200)
{
string type = Safe(() => sub.GetTypeName2());
if (IsSketch(type))
{
string sketchName = Safe(() => sub.Name);
if (!string.IsNullOrWhiteSpace(sketchName))
names.Add(sketchName);
}
sub = SafeObj(() => sub.GetNextSubFeature()) as Feature;
}
return names;
}
static IEnumerable<Sketch> SketchesFromFeature(Feature feature)
{
Feature sub = SafeObj(() => feature.GetFirstSubFeature()) as Feature;
int guard = 0;
while (sub != null && guard++ < 200)
{
string type = Safe(() => sub.GetTypeName2());
if (IsSketch(type))
{
object sketchObj = SafeObj(() => sub.GetSpecificFeature2());
if (sketchObj is Sketch sketch)
yield return sketch;
}
sub = SafeObj(() => sub.GetNextSubFeature()) as Feature;
}
}
static bool ExtrudeReverseDir(Feature f, out string source)
{
source = "default_false";
try
{
object data = SafeObj(() => f.GetDefinition());
if (data == null) return false;
foreach (string name in new[]
{
"ReverseDirection",
"ReverseDirection1",
"Direction",
"Direction1",
"Dir",
"Ddir1"
})
{
if (TryReadBoolMember(data, name, out bool value))
{
source = name;
return value;
}
}
foreach (string name in new[]
{
"GetReverseDirection",
"GetDirection"
})
{
object value = InvokeCom(data, name);
if (TryConvertBool(value, out bool parsed))
{
source = name;
return parsed;
}
}
}
catch { }
return false;
}
static bool TryReadBoolMember(object target, string name, out bool value)
{
value = false;
object raw = GetComProperty(target, name);
if (TryConvertBool(raw, out value)) return true;
raw = InvokeCom(target, name);
return TryConvertBool(raw, out value);
}
static bool TryConvertBool(object raw, out bool value)
{
value = false;
if (raw == null) return false;
if (raw is bool b) { value = b; return true; }
if (raw is int i) { value = i != 0; return true; }
if (raw is short s) { value = s != 0; return true; }
if (raw is double d) { value = Math.Abs(d) > 1e-9; return true; }
if (bool.TryParse(raw.ToString(), out bool parsedBool)) { value = parsedBool; return true; }
if (double.TryParse(raw.ToString(), out double parsedDouble)) { value = Math.Abs(parsedDouble) > 1e-9; return true; }
return false;
}
static void Save(string dir)
{
Directory.CreateDirectory(dir);
string fileBase = BuildOutputFileBaseName();
string xlsx = Path.Combine(dir, $"{fileBase}_建模过程.xlsx");
string json = Path.Combine(dir, $"{fileBase}_skill_flow.json");
using var wb = new XLWorkbook(); var ws = wb.Worksheets.Add("建模过程");
string[] h = { "零件", "步骤", "层级", "特征", "类型", "分类", "草图图元", "草图基准", "尺寸", "约束", "备注" };
for (int i = 0; i < h.Length; i++) ws.Cell(1, i + 1).Value = h[i];
int r = 2; foreach (var a in Rows) { ws.Cell(r, 1).Value = a.PartName; ws.Cell(r, 2).Value = a.Step; ws.Cell(r, 3).Value = a.Depth; ws.Cell(r, 4).Value = a.Feature; ws.Cell(r, 5).Value = a.Type; ws.Cell(r, 6).Value = a.Category; ws.Cell(r, 7).Value = a.Sketch; ws.Cell(r, 8).Value = a.SketchPlane; ws.Cell(r, 9).Value = a.Dims; ws.Cell(r, 10).Value = a.Rels; ws.Cell(r, 11).Value = a.Note; r++; }
ws.Columns().AdjustToContents(8, 80); wb.SaveAs(xlsx);
EnrichExtrudeDirectionHints();
NormalizeAutoNumberedFeatureNames();
var jsonOptions = new JsonSerializerOptions { WriteIndented = true };
var validation = ValidateSkillFlow();
string swagentJson = Path.Combine(dir, $"{fileBase}_modeling_plan.json");
string validationJson = Path.Combine(dir, $"{fileBase}_skill_flow_validation.json");
File.WriteAllText(json, JsonSerializer.Serialize(Skills, jsonOptions));
File.WriteAllText(swagentJson, JsonSerializer.Serialize(BuildSwagentModelingPlan(fileBase), jsonOptions));
File.WriteAllText(validationJson, JsonSerializer.Serialize(validation, jsonOptions));
Console.WriteLine("SWagent ModelingPlan JSON: " + swagentJson);
Console.WriteLine("Skill Flow validation JSON: " + validationJson);
Console.WriteLine(validation.Ok
? "Skill Flow validation passed."
: $"Skill Flow validation failed with {validation.Errors.Count} error(s).");
Console.WriteLine("完成!Excel 已导出:" + xlsx); Console.WriteLine("完成!Skill Flow JSON 已导出:" + json);
}
static string BuildOutputFileBaseName()
{
var partNames = Rows.Select(r => r.PartName)
.Where(p => !string.IsNullOrWhiteSpace(p))
.Distinct(StringComparer.OrdinalIgnoreCase)
.ToList();
string name = partNames.Count == 1 ? partNames[0] : "multi_part";
name = Path.GetFileNameWithoutExtension(name);
if (string.IsNullOrWhiteSpace(name))
name = "part";
foreach (char c in Path.GetInvalidFileNameChars())
name = name.Replace(c, '_');
return name.Trim();
}
static void AddSketchStartSkill(SketchReferenceResult reference, string source)
{
string description = reference?.Description ?? "";
if (TryGetDefaultPlaneSketchSkill(description, out string defaultPlaneSkill))
{
AddSkill(defaultPlaneSkill, new(), source, $"草图基准={description}");
return;
}
if (reference == null || reference.Kind == "unresolved")
{
AddSkill("list_faces", new() { ["unresolved_sketch_reference"] = description }, source, "草图基准未识别,列出面供人工校验");
return;
}
if (reference.Kind == "face" && reference.FaceIndex > 0)
{
if (CanUseFaceSignature(reference))
{
var args = reference.ToSkillArgs();
args["center_x_mm"] = R(reference.CenterMm[0]);
args["center_y_mm"] = R(reference.CenterMm[1]);
args["center_z_mm"] = R(reference.CenterMm[2]);
args["fallback_face_index"] = reference.FaceIndex;
AddSkill("create_face_sketch_by_signature", args, source, $"草图基准={description}");
return;
}
AddSkill("create_face_sketch_by_index", reference.ToSkillArgs(), source, $"草图基准={description}");
return;
}
if (reference.Kind == "datum_plane")
{
AddSkill("create_ref_plane_sketch_by_name", reference.ToSkillArgs(), source, $"草图基准={description}");
return;
}
if (reference.Kind != "face")
{
AddSkill("list_faces", reference.ToSkillArgs(), source, $"草图基准尚无可执行选择 skill={description}");
return;
}
if (reference.Kind == "face")
{
AddSkill("list_faces", reference.ToSkillArgs(), source, $"实体面未获得 SWagent face_index={description}");
return;
}
if (reference == null || reference.Kind == "unresolved")
{
AddSkill("create_sketch_requires_manual_reference", new() { ["reason"] = description }, source, "草图基准未识别,未生成自动选面目标");
return;
}
AddSkill("create_sketch_on_reference", reference.ToSkillArgs(), source, $"草图基准={description}");
}
static bool CanUseFaceSignature(SketchReferenceResult reference)
{
return reference != null
&& string.Equals(reference.Kind, "face", StringComparison.OrdinalIgnoreCase)
&& string.Equals(reference.SurfaceType, "plane", StringComparison.OrdinalIgnoreCase)
&& reference.CenterMm != null
&& reference.CenterMm.Length >= 3
&& reference.AreaMm2 > 0;
}
static bool TryInferPlanarFacePosition(SketchReferenceResult reference, out string position)
{
position = "";
if (reference == null) return false;
if (!string.Equals(reference.Kind, "face", StringComparison.OrdinalIgnoreCase)) return false;
if (!string.Equals(reference.SurfaceType, "plane", StringComparison.OrdinalIgnoreCase)) return false;
double[] center = reference.CenterMm ?? Array.Empty<double>();
double[] box = reference.BoxMm ?? Array.Empty<double>();
if (center.Length < 3) return false;
int axis = -1;
if (box.Length >= 6)
{
double dx = Math.Abs(box[3] - box[0]);
double dy = Math.Abs(box[4] - box[1]);
double dz = Math.Abs(box[5] - box[2]);
double min = Math.Min(dx, Math.Min(dy, dz));
if (min <= 1e-6)
{
if (Math.Abs(dx - min) <= 1e-6) axis = 0;
else if (Math.Abs(dy - min) <= 1e-6) axis = 1;
else axis = 2;
}
}
if (axis < 0 && reference.Normal.Length >= 3)
{
double ax = Math.Abs(reference.Normal[0]);
double ay = Math.Abs(reference.Normal[1]);
double az = Math.Abs(reference.Normal[2]);
if (ax >= ay && ax >= az) axis = 0;
else if (ay >= ax && ay >= az) axis = 1;
else axis = 2;
}
if (axis < 0) return false;
double value = center[axis];
position = axis switch
{
0 => value >= 0 ? "right" : "left",
1 => value >= 0 ? "front" : "back",
_ => value >= 0 ? "top" : "bottom"
};
return true;
}
static void EnsureRefPlaneCreationSkills(ModelDoc2 doc, SketchReferenceResult reference, string source)
{
if (doc == null || reference == null) return;
if (!string.Equals(reference.Kind, "datum_plane", StringComparison.OrdinalIgnoreCase)) return;
string planeName = (reference.ReferenceName ?? "").Trim();
if (string.IsNullOrWhiteSpace(planeName) || IsDefaultPlaneName(planeName)) return;
if (EmittedRefPlaneCreation.Contains(planeName)) return;
var planeFeature = FindFeatureByName(doc, planeName);
if (planeFeature == null || !string.Equals(Safe(() => planeFeature.GetTypeName2()), "RefPlane", StringComparison.OrdinalIgnoreCase))
{
EmittedRefPlaneCreation.Add(planeName);
AddSkill("list_ref_planes", new() { ["reference_name"] = planeName, ["extract_note"] = "按名称未找到对应 RefPlane 特征,无法自动提取创建参数" }, source, "草图引用了自建基准面,但未定位到对应基准面特征");
return;
}
AddRefPlaneCreationSkills(doc, planeFeature, planeName);
}
static bool TryGetDefaultPlaneSketchSkill(string description, out string skill)
{
skill = "";
string text = (description ?? "").ToLowerInvariant();
if (text.Contains("前视")) { skill = "create_front_plane_sketch"; return true; }
if (text.Contains("上视")) { skill = "create_top_plane_sketch"; return true; }
if (text.Contains("右视")) { skill = "create_right_plane_sketch"; return true; }
if (text.Contains("前视") || text.Contains("front")) { skill = "create_front_plane_sketch"; return true; }
if (text.Contains("上视") || text.Contains("top")) { skill = "create_top_plane_sketch"; return true; }
if (text.Contains("右视") || text.Contains("right")) { skill = "create_right_plane_sketch"; return true; }
return false;
}
static bool IsCustomRefPlane(string name, string type)
{
if (!string.Equals(type, "RefPlane", StringComparison.OrdinalIgnoreCase)) return false;
return !IsDefaultPlaneName(name);
}
static bool IsDefaultPlaneName(string name)
{
string n = (name ?? "").Trim();
return string.Equals(n, "前视基准面", StringComparison.OrdinalIgnoreCase)
|| string.Equals(n, "上视基准面", StringComparison.OrdinalIgnoreCase)
|| string.Equals(n, "右视基准面", StringComparison.OrdinalIgnoreCase)
|| string.Equals(n, "Front Plane", StringComparison.OrdinalIgnoreCase)
|| string.Equals(n, "Top Plane", StringComparison.OrdinalIgnoreCase)
|| string.Equals(n, "Right Plane", StringComparison.OrdinalIgnoreCase);
}
static void AddRefPlaneCreationSkills(ModelDoc2 doc, Feature feature, string planeName)
{
if (string.IsNullOrWhiteSpace(planeName)) return;
if (!EmittedRefPlaneCreation.Add(planeName)) return;
var info = TryReadRefPlane(doc, feature, planeName);
if (!info.CanCreate)
{
AddSkill("list_ref_planes", new() { ["reference_name"] = planeName, ["extract_note"] = info.Note }, planeName, "自建基准面创建参数未能自动提取,列出基准面供校验");
return;
}
if (string.Equals(info.BuildKind, "angle_edge_face", StringComparison.OrdinalIgnoreCase))
{
AddSkill("create_angle_plane_by_edge_and_face", new()
{
["edge_signatures"] = info.EdgeSignatures,
["face_index"] = info.BaseFaceIndex,
["face_signature"] = info.FaceSignature,
["angle_deg"] = info.AngleDeg,
["plane_name"] = planeName
}, planeName, "创建角度自建基准面");
return;
}
if (string.Equals(info.BuildKind, "angle_axis_face", StringComparison.OrdinalIgnoreCase))
{
AddSkill("create_angle_plane_by_axis_and_face", new()
{
["axis_name"] = info.AxisName,
["face_index"] = info.BaseFaceIndex,
["face_signature"] = info.FaceSignature,
["angle_deg"] = info.AngleDeg,
["plane_name"] = planeName
}, planeName, "创建基准轴+面角度基准面");
return;
}
if (info.BaseReferenceKind == "default_plane" && TryGetDefaultPlaneSelector(info.BaseReferenceName, out string plane))
{
AddSkill("select_default_plane", new() { ["plane"] = plane }, planeName, $"创建自建基准面前选择基准={info.BaseReferenceName}");
}
else if (info.BaseReferenceKind == "face" && info.BaseFaceIndex > 0)
{
AddSkill("select_face_by_index", new() { ["face_index"] = info.BaseFaceIndex }, planeName, $"创建自建基准面前选择实体面={info.BaseReferenceName}");
}
else if (info.BaseReferenceKind == "datum_plane" && !string.IsNullOrWhiteSpace(info.BaseReferenceName))
{
AddSkill("select_reference_plane_by_name", new() { ["plane_name"] = info.BaseReferenceName }, planeName, $"创建自建基准面前选择自建基准面={info.BaseReferenceName}");
}
else
{
AddSkill("list_faces", new() { ["reference_name"] = planeName, ["base_reference"] = info.BaseReferenceName, ["extract_note"] = info.Note }, planeName, "自建基准面基准引用未能映射为 SWagent 选择 skill");
return;
}
AddSkill("create_offset_plane_mm", new()
{
["offset_mm"] = info.OffsetMm,
["offset_abs_mm"] = info.OffsetAbsMm,
["reverse_direction"] = info.ReverseDirection,
["reversed_reference_direction"] = info.ReversedReferenceDirection,
["plane_name"] = planeName
}, planeName, "创建偏移自建基准面");
}
static bool TryGetDefaultPlaneSelector(string description, out string plane)
{
plane = "";
string text = (description ?? "").ToLowerInvariant();
if (text.Contains("前视") || text.Contains("front")) { plane = "front"; return true; }
if (text.Contains("上视") || text.Contains("top")) { plane = "top"; return true; }
if (text.Contains("右视") || text.Contains("right")) { plane = "right"; return true; }
return false;
}
static RefPlaneBuildInfo TryReadRefPlane(ModelDoc2 doc, Feature feature, string planeName)
{
var info = new RefPlaneBuildInfo { PlaneName = planeName };
try { doc?.EditRebuild3(); } catch { }
object data = SafeObj(() => feature.GetDefinition());
if (data == null)
{
info.Note = "RefPlane.GetDefinition 返回空";
return info;
}
try { doc?.ClearSelection2(true); } catch { }
try { feature?.Select2(false, 0); } catch { }
AccessRefPlaneSelections(data, doc);
try
{
var angleInfo = TryReadAngleRefPlane(doc, data, planeName);
if (angleInfo.CanCreate)
return angleInfo;
string angleNote = angleInfo.Note;
int refPlaneType = ReadRefPlaneType(data);
double offsetMeters = 0.0;
object baseRef = null;
bool hasDistanceConstraint = refPlaneType == (int)swRefPlaneType_e.swRefPlaneDistance;
int distanceReferenceIndex = -1;
bool reverseDirection = false;
bool reversedReferenceDirection = false;
if (data is IRefPlaneFeatureData typedRefPlane)
{
reverseDirection = SafeBool(() => typedRefPlane.ReverseDirection);
}
else
{
reverseDirection = FirstBool(data, new[] { "ReverseDirection" }, false);
reversedReferenceDirection = FirstBool(data, new[] { "ReversedReferenceDirection" }, false);
}
for (int i = 0; i < 3; i++)
{
object referenceCandidate = FirstComReference(ReadRefPlaneReference(data, i));
int constraint = ReadRefPlaneConstraint(data, i);
double value = ReadRefPlaneAngleOrDistance(data, i);
bool isDistanceReference =
(constraint & (int)swRefPlaneReferenceConstraints_e.swRefPlaneReferenceConstraint_Distance) != 0 ||
refPlaneType == (int)swRefPlaneType_e.swRefPlaneDistance;
if (referenceCandidate != null && isDistanceReference)
{
baseRef = referenceCandidate;
distanceReferenceIndex = i;
hasDistanceConstraint = true;
if (Math.Abs(value) > 1e-9)
offsetMeters = value;
break;
}
}
if (data is IRefPlaneFeatureData typedRefPlaneWithIndex && distanceReferenceIndex >= 0)
reversedReferenceDirection = SafeBool(() => typedRefPlaneWithIndex.ReversedReferenceDirection[distanceReferenceIndex]);
if (hasDistanceConstraint && Math.Abs(offsetMeters) <= 1e-9)
{
offsetMeters = FirstMeaningfulDouble(
GetComProperty(data, "Distance"),
InvokeCom(data, "get_Distance"));
}
var reference = new SketchReferenceResult();
if (baseRef != null && TryDescribeReferenceEntity(doc, baseRef, reference))
{
string referencePlaneName = string.IsNullOrWhiteSpace(reference.ReferenceName) ? reference.Description : reference.ReferenceName;
if (reference.Kind == "datum_plane" && TryGetDefaultPlaneSelector(referencePlaneName, out _))
{
info.BaseReferenceKind = "default_plane";
info.BaseReferenceName = referencePlaneName;
}
else if (reference.Kind == "datum_plane")
{
info.BaseReferenceKind = "datum_plane";
info.BaseReferenceName = referencePlaneName;
}
else if (reference.Kind == "face" && reference.FaceIndex > 0)
{
info.BaseReferenceKind = "face";
info.BaseReferenceName = reference.Description;
info.BaseFaceIndex = reference.FaceIndex;
}
}
double offsetAbsMm = R(Math.Abs(offsetMeters) * 1000.0);
bool negativeOffset = reverseDirection || reversedReferenceDirection || offsetMeters < -1e-9;
info.OffsetAbsMm = offsetAbsMm;
info.ReverseDirection = reverseDirection;
info.ReversedReferenceDirection = reversedReferenceDirection;
info.OffsetMm = negativeOffset ? -offsetAbsMm : offsetAbsMm;
info.CanCreate = hasDistanceConstraint && !string.IsNullOrWhiteSpace(info.BaseReferenceKind);
if (info.CanCreate) info.BuildKind = "offset";
if (!info.CanCreate)
info.Note = $"angle plane not recognized: {angleNote}; offset plane not recognized: base={reference.Kind}/{reference.Description}, offset_mm={info.OffsetMm}";
info.Note = info.CanCreate
? "已识别单引用偏移基准面"
: $"angle plane not recognized: {angleNote}; offset plane not recognized: base={reference.Kind}/{reference.Description}, offset_mm={info.OffsetMm}";
return info;
}
finally
{
ReleaseRefPlaneSelections(data);
}
}
static bool AccessRefPlaneSelections(object data, ModelDoc2 doc)
{
if (data is IRefPlaneFeatureData typed)
return SafeBool(() => typed.AccessSelections(doc, null));
return TryInvokeCom(data, "AccessSelections", doc, null);
}
static void ReleaseRefPlaneSelections(object data)
{
if (data is IRefPlaneFeatureData typed)
{
try { typed.ReleaseSelectionAccess(); } catch { }
return;
}
TryInvokeCom(data, "ReleaseSelectionAccess");
}
static object FirstComReference(params object[] candidates)
{
foreach (object candidate in candidates)
{
foreach (object item in FlattenObjects(candidate))
{
if (item == null) continue;
if (TryAsEdge(item, out Edge edge)) return edge;
if (TryAsFace(item, out Face2 face)) return face;
if (item is Feature || item is RefPlane) return item;
}
}
return null;
}
static int ReadRefPlaneType(object data)
{
if (data is IRefPlaneFeatureData typed)
return SafeInt(() => typed.Type, 0);
return FirstMeaningfulInt(GetComProperty(data, "Type"), InvokeCom(data, "get_Type"));
}
static object ReadRefPlaneReference(object data, int index)
{
if (data is IRefPlaneFeatureData typed)
return SafeObj(() => typed.Reference[index]);
return FirstComReference(GetComProperty(data, "Reference", index), InvokeCom(data, "get_Reference", index));
}
static int ReadRefPlaneConstraint(object data, int index)
{
if (data is IRefPlaneFeatureData typed)
return SafeInt(() => typed.Constraint[index], 0);
return FirstMeaningfulInt(GetComProperty(data, "Constraint", index), InvokeCom(data, "get_Constraint", index));
}
static double ReadRefPlaneAngleOrDistance(object data, int index)
{
if (data is IRefPlaneFeatureData typed)
return SafeDouble(() => typed.AngleOrDistance[index], 0.0);
return FirstMeaningfulDouble(GetComProperty(data, "AngleOrDistance", index), InvokeCom(data, "get_AngleOrDistance", index));
}
static IEnumerable<object> ReadRefPlaneSelectionObjects(object data)
{
if (data is IRefPlaneFeatureData typed)
{
foreach (object item in FlattenComObjects(SafeObj(() => typed.Selections)))
yield return item;
yield break;
}
foreach (object item in FlattenComObjects(GetComProperty(data, "Selections")))
yield return item;
}
static RefPlaneBuildInfo TryReadAngleRefPlane(ModelDoc2 doc, object data, string planeName)
{
var info = new RefPlaneBuildInfo { PlaneName = planeName };
var edgeSignatures = new List<Dictionary<string, object>>();
var faceReference = new SketchReferenceResult();
string axisName = "";
double angleRad = 0.0;
var debug = new List<string>();
for (int i = 0; i < 3; i++)
{
object rawReference = ReadRefPlaneReference(data, i);
object reference = FirstComReference(rawReference);
int constraint = ReadRefPlaneConstraint(data, i);
double value = ReadRefPlaneAngleOrDistance(data, i);
if (edgeSignatures.Count == 0)
{
var candidateEdgeSignatures = EdgeSignatures(new object[] { rawReference });
debug.Add($"raw[{i}]={rawReference?.GetType().Name ?? "null"}, asEdge={TryAsEdge(rawReference, out _)}, edgeSigCount={candidateEdgeSignatures.Count}");
if (candidateEdgeSignatures.Count > 0)
edgeSignatures = candidateEdgeSignatures;
}
if (string.IsNullOrWhiteSpace(axisName))
{
foreach (object axisCandidate in FlattenComObjects(new object[] { rawReference, reference }))
{
var candidateAxisReference = new SketchReferenceResult();
if (axisCandidate != null &&
TryDescribeReferenceEntity(doc, axisCandidate, candidateAxisReference) &&
string.Equals(candidateAxisReference.Kind, "datum_axis", StringComparison.OrdinalIgnoreCase))
{
axisName = candidateAxisReference.ReferenceName;
break;
}
}
if (!string.IsNullOrWhiteSpace(axisName))
debug.Add($"axisProbe[{i}]={axisName}");
}
if (faceReference.FaceIndex <= 0)
{
var candidateFaceReference = new SketchReferenceResult();
foreach (object faceCandidate in FlattenComObjects(new object[] { rawReference, reference }))
{
if (faceCandidate != null &&
TryDescribeReferenceEntity(doc, faceCandidate, candidateFaceReference) &&
candidateFaceReference.FaceIndex > 0)
{
faceReference = candidateFaceReference;
break;
}
}
debug.Add($"faceProbe[{i}]={candidateFaceReference.Kind}/{candidateFaceReference.FaceIndex}");
}
if (Math.Abs(value) > 1e-9 &&
((constraint & (int)swRefPlaneReferenceConstraints_e.swRefPlaneReferenceConstraint_Angle) != 0 || angleRad == 0.0))
angleRad = value;
debug.Add($"i={i}, constraint={constraint}, value={value}, ref={reference?.GetType().Name ?? "null"}");
}
if (edgeSignatures.Count == 0 || faceReference.FaceIndex <= 0)
{
foreach (object selectedReference in ReadRefPlaneSelectionObjects(data))
{
if (edgeSignatures.Count == 0)
{
var candidateEdgeSignatures = EdgeSignatures(new object[] { selectedReference });
debug.Add($"selection={selectedReference?.GetType().Name ?? "null"}, asEdge={TryAsEdge(selectedReference, out _)}, edgeSigCount={candidateEdgeSignatures.Count}");
if (candidateEdgeSignatures.Count > 0)
edgeSignatures = candidateEdgeSignatures;
}
if (string.IsNullOrWhiteSpace(axisName))
{
foreach (object axisCandidate in FlattenComObjects(selectedReference))
{
var candidateAxisReference = new SketchReferenceResult();
if (axisCandidate != null &&
TryDescribeReferenceEntity(doc, axisCandidate, candidateAxisReference) &&
string.Equals(candidateAxisReference.Kind, "datum_axis", StringComparison.OrdinalIgnoreCase))
{
axisName = candidateAxisReference.ReferenceName;
break;
}
}
if (!string.IsNullOrWhiteSpace(axisName))
debug.Add($"selectionAxis={axisName}");
}
if (faceReference.FaceIndex <= 0)
{
var candidateFaceReference = new SketchReferenceResult();
foreach (object faceCandidate in FlattenComObjects(selectedReference))
{
if (faceCandidate != null &&
TryDescribeReferenceEntity(doc, faceCandidate, candidateFaceReference) &&
candidateFaceReference.FaceIndex > 0)
{
faceReference = candidateFaceReference;
break;
}
}
debug.Add($"selectionFace={candidateFaceReference.Kind}/{candidateFaceReference.FaceIndex}");
}
}
}
if (Math.Abs(angleRad) <= 1e-9)
{
angleRad = FirstMeaningfulDouble(
GetComProperty(data, "Angle"),
InvokeCom(data, "GetAngle"),
GetComProperty(data, "AngleOrDistance"));
}
bool hasAxisReference = edgeSignatures.Count > 0 || !string.IsNullOrWhiteSpace(axisName);
if (!hasAxisReference || faceReference.FaceIndex <= 0)
{
info.Note = "未识别为角度基准面: " + string.Join("; ", debug);
return info;
}
double angleDeg = R(angleRad * 180.0 / Math.PI);
while (angleDeg < 0) angleDeg += 360.0;
while (angleDeg >= 360.0) angleDeg -= 360.0;
info.CanCreate = true;
info.BuildKind = !string.IsNullOrWhiteSpace(axisName) ? "angle_axis_face" : "angle_edge_face";
info.BaseReferenceKind = "face";
info.BaseReferenceName = faceReference.Description;
info.BaseFaceIndex = faceReference.FaceIndex;
info.AngleDeg = R(angleDeg);
info.EdgeSignatures = edgeSignatures;
info.AxisName = axisName;
info.FaceSignature = BuildFaceSignatureArgs(faceReference);
info.Note = $"已识别边线+面角度基准面: face_index={info.BaseFaceIndex}, angle_deg={info.AngleDeg}";
return info;
}
static Dictionary<string, object> BuildFaceSignatureArgs(SketchReferenceResult reference)
{
var result = new Dictionary<string, object>();
if (reference == null) return result;
if (!string.IsNullOrWhiteSpace(reference.SurfaceType)) result["surface_type"] = reference.SurfaceType;
if (reference.AreaMm2 > 0) result["area_mm2"] = reference.AreaMm2;
if (reference.CenterMm != null && reference.CenterMm.Length >= 3)
{
result["center_mm"] = reference.CenterMm;
result["center_x_mm"] = R(reference.CenterMm[0]);
result["center_y_mm"] = R(reference.CenterMm[1]);
result["center_z_mm"] = R(reference.CenterMm[2]);
}
if (reference.BoxMm != null && reference.BoxMm.Length >= 6)
result["box_mm"] = reference.BoxMm;
if (reference.Normal != null && reference.Normal.Length >= 3)
{
result["normal"] = reference.Normal;
result["normal_x"] = reference.Normal[0];
result["normal_y"] = reference.Normal[1];
result["normal_z"] = reference.Normal[2];
}
return result;
}
static IEnumerable<object> FlattenObjects(object value)
{
if (value == null) yield break;
if (value is string) yield break;
if (value is Array array)
{
foreach (object item in array)
{
foreach (object nested in FlattenObjects(item)) yield return nested;
}
yield break;
}
yield return value;
}
static double FirstMeaningfulDouble(params object[] candidates)
{
foreach (object candidate in candidates)
{
foreach (double value in FlattenDoubles(candidate))
{
if (Math.Abs(value) > 1e-9) return value;
}
}
return 0;
}
static int FirstMeaningfulInt(params object[] candidates)
{
foreach (object candidate in candidates)
{
foreach (double value in FlattenDoubles(candidate))
{
int result = Convert.ToInt32(value);
if (result != 0) return result;
}
}
return 0;
}
static IEnumerable<double> FlattenDoubles(object value)
{
if (value == null) yield break;
if (value is double d) { yield return d; yield break; }
if (value is float f) { yield return f; yield break; }
if (value is int i) { yield return i; yield break; }
if (value is Array array)
{
foreach (object item in array)
{
foreach (double nested in FlattenDoubles(item)) yield return nested;
}
}
}
static object BuildSwagentModelingPlan(string fileBase)
{
string workflowId = "part-feature-audit-" + fileBase;
var steps = Skills.Select(item => new
{
id = $"step-{item.Step:000}",
targetAgent = "solidworks",
skillName = item.Skill,
name = string.IsNullOrWhiteSpace(item.SourceFeature) ? item.Skill : item.SourceFeature,
description = item.Note ?? "",
arguments = item.Args ?? new Dictionary<string, object>(),
dependencies = item.Step > 1 ? new[] { $"step-{item.Step - 1:000}" } : Array.Empty<string>(),
acceptance = new Dictionary<string, object>()
}).ToList();
return new
{
workflowId,
sourceAgent = "part_feature_audit",
approvedPlanText = "Generated from extracted SolidWorks feature skill flow.",
modelingPlan = new
{
planId = workflowId + "-plan",
targetAgent = "solidworks",
units = "mm",
summary = $"Rebuild part from extracted SolidWorks skill flow. steps={steps.Count}",
steps
},
simulationPlan = new Dictionary<string, object>(),
exportOptions = new
{
outputDirectory = "",
fileBaseName = "",
formats = Array.Empty<string>()
},
handoffOptions = new
{
autoSubmitToSimulation = false,
simulationAgentBaseUrl = "http://127.0.0.1:6066"
}
};
}
static SkillFlowValidationResult ValidateSkillFlow()
{
var result = new SkillFlowValidationResult
{
TotalSteps = Skills.Count,
SkillsUsed = Skills.Select(s => s.Skill)
.Where(s => !string.IsNullOrWhiteSpace(s))
.Distinct(StringComparer.OrdinalIgnoreCase)
.OrderBy(s => s)
.ToList()
};
var supportedSkills = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
{
"create_new_part",
"create_front_plane_sketch",
"create_top_plane_sketch",
"create_right_plane_sketch",
"enter_sketch",
"exit_sketch",
"draw_line_mm",
"draw_center_line_mm",
"draw_point_mm",
"draw_spline_points_mm",
"draw_circle_diameter_mm",
"draw_arc_center_start_end_mm",
"draw_3point_arc_mm",
"extrude_boss_mm",
"extrude_cut_mm",
"create_revolve_boss_mm",
"create_revolve_cut_mm",
"create_helix_mm",
"list_faces",
"create_face_sketch_by_index",
"create_face_sketch_by_signature",
"create_planar_face_sketch_by_position",
"select_face_by_index",
"list_ref_planes",
"select_default_plane",
"select_reference_plane_by_name",
"create_ref_plane_sketch_by_name",
"create_reference_axis",
"create_offset_plane_mm",
"create_angle_plane_by_edge_and_face",
"create_angle_plane_by_axis_and_face",
"select_edges_by_signature",
"fillet_edges_radius_mm",
"chamfer_edges_angle_distance_mm",
"linear_pattern_feature_mm",
"circular_pattern_feature",
"draft_faces_neutral_plane_mm",
"create_rib_mm",
"shell_remove_face_at_point_mm",
"hole_wizard_threaded_mm",
"mirror_feature_about_plane",
"wrap_sketch_on_faces_mm",
"swept_cut_circular_profile_mm",
"swept_cut_profile_path",
"swept_boss_circular_profile_mm",
"loft_boss_from_profiles",
"loft_cut_from_profiles",
"boundary_boss_from_profiles",
"boundary_cut_from_profiles"
};
var requiredArgs = new Dictionary<string, string[]>(StringComparer.OrdinalIgnoreCase)
{
["draw_line_mm"] = new[] { "start_model_mm", "end_model_mm" },
["draw_center_line_mm"] = new[] { "start_model_mm", "end_model_mm" },
["draw_point_mm"] = new[] { "point_model_mm" },
["draw_spline_points_mm"] = new[] { "points_model_mm" },
["draw_circle_diameter_mm"] = new[] { "diameter_mm", "center_model_mm" },
["draw_arc_center_start_end_mm"] = new[] { "center_model_mm", "start_model_mm", "end_model_mm", "mid_model_mm" },
["draw_3point_arc_mm"] = new[] { "start_x_mm", "start_y_mm", "point_x_mm", "point_y_mm", "end_x_mm", "end_y_mm" },
["extrude_boss_mm"] = new[] { "depth_mm", "extrude_direction_model" },
["extrude_cut_mm"] = new[] { "depth_mm", "extrude_direction_model" },
["create_revolve_cut_mm"] = new[] { "angle_deg" },
["create_helix_mm"] = new[] { "defined_by" },
["create_face_sketch_by_index"] = new[] { "face_index" },
["create_face_sketch_by_signature"] = new[] { "center_x_mm", "center_y_mm", "center_z_mm", "area_mm2" },
["select_face_by_index"] = new[] { "face_index" },
["select_default_plane"] = new[] { "plane" },
["select_reference_plane_by_name"] = new[] { "plane_name" },
["create_ref_plane_sketch_by_name"] = new[] { "plane_name" },
["create_offset_plane_mm"] = new[] { "offset_mm" },
["create_angle_plane_by_edge_and_face"] = new[] { "edge_signatures", "face_index", "angle_deg" },
["select_edges_by_signature"] = new[] { "edge_signatures" },
["fillet_edges_radius_mm"] = new[] { "radius_mm" },
["chamfer_edges_angle_distance_mm"] = new[] { "distance_mm", "angle_deg" },
["linear_pattern_feature_mm"] = new[] { "count1", "spacing1_mm", "dir1_edge_x_mm", "dir1_edge_y_mm", "dir1_edge_z_mm" },
["circular_pattern_feature"] = new[] { "count", "angle_deg" },
["draft_faces_neutral_plane_mm"] = new[] { "angle_deg", "neutral_face_index", "draft_face_indices" },
["create_rib_mm"] = new[] { "thickness_mm" },
["shell_remove_face_at_point_mm"] = new[] { "thickness_mm", "x_mm", "y_mm", "z_mm" },
["hole_wizard_threaded_mm"] = new[] { "diameter_mm", "depth_mm", "position_points_model_mm" },
["swept_cut_circular_profile_mm"] = new[] { "diameter_mm" },
["swept_cut_profile_path"] = new[] { "profile_name", "path_name" },
["swept_boss_circular_profile_mm"] = new[] { "diameter_mm" }
};
var reviewOnlySkills = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
{
"list_faces",
"list_ref_planes"
};
for (int i = 0; i < Skills.Count; i++)
{
var step = Skills[i];
string label = $"step-{step.Step:000}";
if (step.Step != i + 1)
result.Errors.Add($"{label}: step number is not sequential; expected {i + 1}.");
if (string.IsNullOrWhiteSpace(step.Skill))
{
result.Errors.Add($"{label}: missing skill.");
continue;
}
if (!supportedSkills.Contains(step.Skill))
{
result.Errors.Add($"{label}: skill '{step.Skill}' is not registered in the current SWagent skill registry.");
continue;
}
var args = step.Args ?? new Dictionary<string, object>();
if (requiredArgs.TryGetValue(step.Skill, out var required))
{
foreach (string key in required)
{
if (!args.TryGetValue(key, out var value) || IsBlankValue(value))
result.Errors.Add($"{label}: skill '{step.Skill}' is missing required argument '{key}'.");
}
}
if (reviewOnlySkills.Contains(step.Skill))
result.Warnings.Add($"{label}: skill '{step.Skill}' is an inspection/manual-review step; the flow may need a resolved selection before fully automatic replay.");
if ((string.Equals(step.Skill, "extrude_boss_mm", StringComparison.OrdinalIgnoreCase) ||
string.Equals(step.Skill, "extrude_cut_mm", StringComparison.OrdinalIgnoreCase)) &&
args.TryGetValue("depth_mm", out var depthValue) &&
TryToDouble(depthValue, out double depthMm) &&
Math.Abs(depthMm) <= 1e-9 &&
!HasTrueArg(args, "through_all") &&
!HasNonBlindEndCondition(args))
{
result.Warnings.Add($"{label}: '{step.Skill}' has depth_mm=0; SWagent execution will probably fail or create no geometry.");
}
}
result.Ok = result.Errors.Count == 0;
return result;
}
static bool IsBlankValue(object value) => value == null || value is string s && string.IsNullOrWhiteSpace(s);
static bool HasTrueArg(Dictionary<string, object> args, string key)
{
if (!args.TryGetValue(key, out var value) || value == null) return false;
if (value is bool b) return b;
return bool.TryParse(value.ToString(), out bool parsed) && parsed;
}
static bool HasNonBlindEndCondition(Dictionary<string, object> args)
{
if (args == null || !args.TryGetValue("end_condition_code", out var value))
return false;
return TryToDouble(value, out double code) &&
Math.Abs(code - (int)swEndConditions_e.swEndCondBlind) > 1e-9;
}
static bool TryToDouble(object value, out double number)
{
number = 0;
if (value == null) return false;
if (value is double d) { number = d; return true; }
if (value is float f) { number = f; return true; }
if (value is int i) { number = i; return true; }
return double.TryParse(value.ToString(), out number);
}
static void AddSkill(string skill, Dictionary<string, object> args, string source, string note) => Skills.Add(new SkillFlowStep { Step = Skills.Count + 1, Skill = skill, Args = args ?? new Dictionary<string, object>(), SourceFeature = source, Note = note });
static bool ShouldSkipDefaultFeature(string name, string type)
{
string n = (name ?? "").Trim();
string t = (type ?? "").Trim().ToLowerInvariant();
if (string.IsNullOrWhiteSpace(n) && string.IsNullOrWhiteSpace(t)) return true;
return n switch
{
"Favorites" => true,
"History" => true,
"Selection Sets" => true,
"传感器" => true,
"设计活页夹" => true,
"注解" => true,
"标注" => true,
"光源、相机与布景" => true,
"实体" => true,
"曲面实体" => true,
"备注" => true,
"方程式" => true,
"材料 <未指定>" => true,
"前视基准面" => true,
"上视基准面" => true,
"右视基准面" => true,
"原点" => true,
_ => t.Contains("historyfolder")
|| t.Contains("selectionsetfolder")
|| t.Contains("sensorfolder")
|| t.Contains("annotation")
|| t.Contains("commentsfolder")
|| t.Contains("equation")
|| t.Contains("refplane") && (n.Contains("前视") || n.Contains("上视") || n.Contains("右视"))
|| t.Contains("originprofilefeature")
};
}
static string Cat(string n, string t) { string s = (n + " " + t).ToLowerInvariant(); if (IsSketch(t)) return "草图"; if (IsHelixFeature(n, t)) return "螺旋线/涡状线"; if (IsSweepFeature(n, t)) return "扫描"; if (IsCutLike(n, t) && (s.Contains("revol") || s.Contains("revolution") || s.Contains("旋转"))) return "旋转切除"; if (s.Contains("revol") || s.Contains("revolution") || s.Contains("旋转")) return "旋转凸台"; if (IsExtrude(n, t, true)) return "切除-拉伸"; if (IsExtrude(n, t, false)) return "凸台-拉伸"; if (s.Contains("fillet") || s.Contains("圆角")) return "圆角"; if (s.Contains("chamfer") || s.Contains("倒角")) return "倒角"; if (IsCircularPatternFeature(n, t)) return "圆周阵列"; if (IsLinearPatternFeature(n, t)) return "线性阵列"; if (IsDraftFeature(n, t)) return "拔模"; if (IsRibFeature(n, t)) return "筋"; if (IsShellFeature(n, t)) return "抽壳"; if (IsHoleWizardFeature(n, t)) return "异形孔向导"; if (IsMirrorFeature(n, t)) return "镜像"; if (IsWrapFeature(n, t)) return "包覆"; if (IsBoundaryFeature(n, t)) return "边界"; if (IsLoftFeature(n, t)) return "放样"; return "其他"; }
static bool IsSketch(string t) { string s = (t ?? "").ToLowerInvariant(); return s.Contains("profilefeature") || s.Contains("sketch"); }
static bool IsExtrude(string n, string t, bool cut) { string s = (n + " " + t).ToLowerInvariant(); return cut ? s.Contains("cut") || s.Contains("切除") : (s.Contains("extrude") || s.Contains("boss") || s.Contains("拉伸")) && !s.Contains("cut") && !s.Contains("切除"); }
static bool IsFilletFeature(string n, string t) { string s = (n + " " + t).ToLowerInvariant(); return s.Contains("fillet") || s.Contains("圆角") || s.Contains("鍦嗚"); }
static bool IsChamferFeature(string n, string t) { string s = (n + " " + t).ToLowerInvariant(); return s.Contains("chamfer") || s.Contains("倒角") || s.Contains("鍊掕"); }
static bool IsCutLike(string n, string t) { string s = (n + " " + t).ToLowerInvariant(); return s.Contains("cut") || s.Contains("切除") || s.Contains("鍒囬櫎"); }
static bool IsLinearPatternFeature(string n, string t) { string s = (n + " " + t).ToLowerInvariant(); return s.Contains("lpattern") || s.Contains("linearpattern") || s.Contains("linear pattern") || s.Contains("线性阵列") || s.Contains("線性陣列"); }
static bool IsCircularPatternFeature(string n, string t) { string s = (n + " " + t).ToLowerInvariant(); return s.Contains("cirpattern") || s.Contains("circularpattern") || s.Contains("circular pattern") || s.Contains("圆周阵列") || s.Contains("圓周陣列") || s.Contains("阵列(圆周)") || s.Contains("陣列(圓周)"); }
static bool IsDraftFeature(string n, string t) { string s = (n + " " + t).ToLowerInvariant(); return s.Contains("draft") || s.Contains("拔模"); }
static bool IsRibFeature(string n, string t) { string s = (n + " " + t).ToLowerInvariant(); return s.Contains("rib") || s.Contains("筋"); }
static bool IsReferenceAxisFeature(string n, string t)
{
string type = (t ?? "").Trim();
if (string.Equals(type, "RefAxis", StringComparison.OrdinalIgnoreCase))
return true;
string s = (n + " " + t).ToLowerInvariant();
return s.Contains("refaxis") || s.Contains("reference axis") || s.Contains("基准轴");
}
static bool IsShellFeature(string n, string t) { string s = (n + " " + t).ToLowerInvariant(); return s.Contains("shell") || s.Contains("抽壳") || s.Contains("抽殼"); }
static bool IsHoleWizardFeature(string n, string t) { string s = (n + " " + t).ToLowerInvariant(); return s.Contains("holewzd") || s.Contains("hole wizard") || s.Contains("wizardhole") || s.Contains("异形孔") || s.Contains("異型孔"); }
static bool IsMirrorFeature(string n, string t) { string s = (n + " " + t).ToLowerInvariant(); return s.Contains("mirror") || s.Contains("镜像") || s.Contains("鏡像"); }
static bool IsWrapFeature(string n, string t) { string s = (n + " " + t).ToLowerInvariant(); return s.Contains("wrap") || s.Contains("包覆"); }
static bool IsHelixFeature(string n, string t) { string s = (n + " " + t).ToLowerInvariant(); return s.Contains("helix") || s.Contains("spiral") || s.Contains("螺旋") || s.Contains("涡状") || s.Contains("渦狀"); }
static bool IsSweepFeature(string n, string t) { string s = (n + " " + t).ToLowerInvariant(); return s.Contains("sweep") || s.Contains("swept") || s.Contains("扫描") || s.Contains("掃描"); }
static bool IsLoftFeature(string n, string t) { string s = (n + " " + t).ToLowerInvariant(); return s.Contains("loft") || s.Contains("blend") || s.Contains("放样") || s.Contains("放樣"); }
static bool IsBoundaryFeature(string n, string t) { string s = (n + " " + t).ToLowerInvariant(); return s.Contains("boundary") || s.Contains("netblend") || s.Contains("边界") || s.Contains("邊界"); }
static object SafeObj(Func<object> f) { try { return f(); } catch { return null; } }
static bool SafeBool(Func<bool> f, bool d = false) { try { return f(); } catch { return d; } }
static T SafeValue<T>(Func<T> f, T d = default) { try { return f(); } catch { return d; } }
static void SafeAction(Action a) { try { a(); } catch { } }
class SketchPointSample
{
public double X, Y, Z;
public int Priority, Order;
}
static Dictionary<IntPtr, double[]> BuildSketchPointCanonicalMap(Sketch sk)
{
var samples = new Dictionary<IntPtr, List<SketchPointSample>>();
var parent = new Dictionary<IntPtr, IntPtr>();
int order = 0;
IntPtr Find(IntPtr id)
{
if (id == IntPtr.Zero) return IntPtr.Zero;
if (!parent.TryGetValue(id, out var p))
{
parent[id] = id;
return id;
}
if (p == id) return id;
var root = Find(p);
parent[id] = root;
return root;
}
void Union(IntPtr a, IntPtr b)
{
var ra = Find(a);
var rb = Find(b);
if (ra == IntPtr.Zero || rb == IntPtr.Zero || ra == rb) return;
parent[rb] = ra;
}
IntPtr Register(SketchPoint point, int priority)
{
if (point == null) return IntPtr.Zero;
var id = Id(point);
if (id == IntPtr.Zero) return IntPtr.Zero;
if (!parent.ContainsKey(id)) parent[id] = id;
try
{
if (!samples.TryGetValue(id, out var list))
{
list = new List<SketchPointSample>();
samples[id] = list;
}
list.Add(new SketchPointSample
{
X = point.X * 1000.0,
Y = point.Y * 1000.0,
Z = point.Z * 1000.0,
Priority = priority,
Order = order++
});
}
catch { }
return id;
}
foreach (object o in ToObjectArray(SafeObj(() => sk.GetSketchSegments())))
{
if (o is SketchLine line)
{
Register(line.GetStartPoint2() as SketchPoint, 2);
Register(line.GetEndPoint2() as SketchPoint, 2);
}
else if (o is SketchArc arc)
{
Register(arc.GetCenterPoint2() as SketchPoint, 1);
Register(arc.GetStartPoint2() as SketchPoint, 3);
Register(arc.GetEndPoint2() as SketchPoint, 3);
}
}
foreach (object pointObj in InvokeArray(sk, "GetSketchPoints2").Concat(InvokeArray(sk, "GetSketchPoints")))
{
if (pointObj is SketchPoint point) Register(point, 0);
}
foreach (object relObj in SketchRelations(sk))
{
if (relObj is not SketchRelation relation) continue;
int relationType = SafeInt(() => relation.GetRelationType());
if (relationType != (int)swConstraintType_e.swConstraintType_COINCIDENT
&& relationType != (int)swConstraintType_e.swConstraintType_MERGEPOINTS)
continue;
var ids = new List<IntPtr>();
object[] entities = ToObjectArray(SafeObj(() => relation.GetEntities()));
if (entities.Length == 0)
{
int count = SafeInt(() => relation.GetEntitiesCount(), 0);
if (count > 0) entities = ToObjectArray(SafeObj(() => relation.IGetEntities(count)));
}
foreach (object entity in entities)
{
if (entity is SketchPoint point)
{
var id = Register(point, 0);
if (id != IntPtr.Zero) ids.Add(id);
}
}
for (int i = 1; i < ids.Count; i++)
Union(ids[0], ids[i]);
}
var groups = new Dictionary<IntPtr, List<SketchPointSample>>();
foreach (var kv in samples)
{
var root = Find(kv.Key);
if (!groups.TryGetValue(root, out var list))
{
list = new List<SketchPointSample>();
groups[root] = list;
}
list.AddRange(kv.Value);
}
var result = new Dictionary<IntPtr, double[]>();
foreach (var group in groups)
{
var chosen = group.Value
.OrderByDescending(p => p.Priority)
.ThenBy(p => p.Order)
.FirstOrDefault();
if (chosen == null) continue;
var canonical = new[] { chosen.X, chosen.Y, chosen.Z };
foreach (var id in samples.Keys.Where(id => Find(id) == group.Key))
result[id] = canonical;
}
return result;
}
static object[] SketchRelations(Sketch sk)
{
var list = new List<object>();
var seen = new HashSet<IntPtr>();
void Add(object value)
{
foreach (object item in ToObjectArray(value))
{
if (item == null) continue;
var id = Id(item);
if (id == IntPtr.Zero || seen.Add(id))
list.Add(item);
}
}
Add(InvokeCom(sk, "GetRelations"));
object manager = GetComProperty(sk, "RelationManager");
if (manager != null)
{
Add(InvokeCom(manager, "GetRelations"));
int count = SafeInt(() => Convert.ToInt32(InvokeCom(manager, "GetRelationsCount")), 0);
if (count > 0) Add(InvokeCom(manager, "IGetRelations", count));
}
return list.ToArray();
}
static bool Pt(SketchPoint p, out double x, out double y) { x = y = 0; if (p == null) return false; x = p.X * 1000; y = p.Y * 1000; return true; }
static bool Pt3(SketchPoint p, out double x, out double y, out double z) { x = y = z = 0; if (p == null) return false; x = p.X * 1000; y = p.Y * 1000; z = p.Z * 1000; return true; }
static bool Pt(SketchPoint p, Dictionary<IntPtr, double[]> canonical, out double x, out double y)
{
x = y = 0;
if (p == null) return false;
var id = Id(p);
if (id != IntPtr.Zero && canonical != null && canonical.TryGetValue(id, out var values) && values.Length >= 2)
{
x = values[0];
y = values[1];
return true;
}
return Pt(p, out x, out y);
}
static bool Pt3(SketchPoint p, Dictionary<IntPtr, double[]> canonical, out double x, out double y, out double z)
{
x = y = z = 0;
if (p == null) return false;
var id = Id(p);
if (id != IntPtr.Zero && canonical != null && canonical.TryGetValue(id, out var values) && values.Length >= 3)
{
x = values[0];
y = values[1];
z = values[2];
return true;
}
return Pt3(p, out x, out y, out z);
}
static IntPtr Id(object o) { if (!Marshal.IsComObject(o)) return IntPtr.Zero; IntPtr p = IntPtr.Zero; try { p = Marshal.GetIUnknownForObject(o); return p; } catch { return IntPtr.Zero; } finally { if (p != IntPtr.Zero) Marshal.Release(p); } }
static object[] InvokeArray(object target, string name)
{
try { return target.GetType().InvokeMember(name, System.Reflection.BindingFlags.InvokeMethod, null, target, Array.Empty<object>()) as object[] ?? Array.Empty<object>(); }
catch { return Array.Empty<object>(); }
}
static object[] ToObjectArray(object value)
{
if (value == null) return Array.Empty<object>();
if (value is DispatchWrapper wrapper) return ToObjectArray(wrapper.WrappedObject);
if (value is object[] objects) return objects;
if (value is Array array)
{
var list = new List<object>();
foreach (object item in array)
{
if (item is DispatchWrapper nestedWrapper)
list.Add(nestedWrapper.WrappedObject);
else
list.Add(item);
}
return list.ToArray();
}
return new[] { value };
}
static double[] ToDoubleArray(object value)
{
if (value == null) return Array.Empty<double>();
if (value is double[] doubles) return doubles;
if (value is Array array)
{
var list = new List<double>();
foreach (object item in array)
{
if (item == null) continue;
list.Add(Convert.ToDouble(item));
}
return list.ToArray();
}
return Array.Empty<double>();
}
static object InvokeCom(object target, string name, params object[] args)
{
try { return target?.GetType().InvokeMember(name, System.Reflection.BindingFlags.InvokeMethod, null, target, args ?? Array.Empty<object>()); }
catch { return null; }
}
static object GetComProperty(object target, string name)
{
try { return target?.GetType().InvokeMember(name, System.Reflection.BindingFlags.GetProperty, null, target, Array.Empty<object>()); }
catch { return null; }
}
static object GetComProperty(object target, string name, params object[] args)
{
try { return target?.GetType().InvokeMember(name, System.Reflection.BindingFlags.GetProperty, null, target, args ?? Array.Empty<object>()); }
catch { return null; }
}
static bool TryInvokeCom(object target, string name, params object[] args)
{
try
{
target?.GetType().InvokeMember(name, System.Reflection.BindingFlags.InvokeMethod, null, target, args ?? Array.Empty<object>());
return target != null;
}
catch { return false; }
}
static string Safe(Func<string> f, string d = "") { try { return f() ?? d; } catch { return d; } }
static int SafeInt(Func<int> f, int d = -1) { try { return f(); } catch { return d; } }
static double SafeDouble(Func<double> f, double d = 0) { try { return f(); } catch { return d; } }
static double R(double v) => Math.Round(v, 6);
static string AppendText(string a, string b) => string.IsNullOrWhiteSpace(a) ? b : a + "" + b;
static void Inc(Dictionary<int, int> d, int k) => d[k] = d.TryGetValue(k, out var v) ? v + 1 : 1;
}