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 Rows = new(); static readonly List Skills = new(); static readonly HashSet SeenSketch = new(); static readonly HashSet EmittedRefPlaneCreation = new(StringComparer.OrdinalIgnoreCase); static readonly Dictionary FeatureStepByName = new(StringComparer.OrdinalIgnoreCase); static readonly Dictionary SketchPlanNameById = new(); static readonly Dictionary> 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 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 SkillsUsed { get; set; } = new(); public List Errors { get; set; } = new(); public List 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(); public double[] BoxMm = Array.Empty(); public double[] Normal = Array.Empty(); public string PlaneName => !string.IsNullOrWhiteSpace(ReferenceName) ? ReferenceName : Description; public Dictionary ToSkillArgs() { var args = new Dictionary { ["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> EdgeSignatures = new(); public Dictionary 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 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(); 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()); 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(); 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()); } 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(); } 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(); } static double[] FaceNormal(Face2 face) { if (face == null) return Array.Empty(); try { if (face.GetSurface() is Surface surface && SafeBool(() => surface.IsPlane())) return TryGetPlaneNormal(face, surface); } catch { } return Array.Empty(); } 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 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(StringComparer.OrdinalIgnoreCase); var renameMap = new Dictionary(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(), @"^(?.*?)(?\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 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 stringList) return stringList.Select(item => RewriteFeatureNameText(item, renameMap)).ToList(); if (value is Dictionary 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 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(); 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(); 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(); var canonicalPoints = BuildSketchPointCanonicalMap(sk); HashSet selectedContourSegmentIds = null; var sketchId = Id(sk); if (sketchId != IntPtr.Zero) SketchSelectedContourSegmentIdsBySketchId.TryGetValue(sketchId, out selectedContourSegmentIds); var segmentPointIds = new HashSet(); var segmentPointKeys = new HashSet(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(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 canonicalPoints, HashSet ids, HashSet 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 ids, HashSet 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 UserSketchPointsLocalMm(Sketch sketch) { var result = new List(); 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 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 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 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>(); var seen = new HashSet(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 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 modelMm) { modelMm = new List(); 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 { R(data[0] * 1000.0), R(data[1] * 1000.0), R(data[2] * 1000.0) }; return true; } catch { modelMm = new List(); return false; } } static void AddExtrudeDirectionModelArgs(Feature feature, bool reverseDir, Dictionary 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 args, out string note) { args = new Dictionary(); 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 args, out string note) { args = new Dictionary(); 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().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> EdgeSignatures(object edgesObj) { var result = new List>(); 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> ExpandSymmetricArcEdgeSignatures(List> 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 CloneSignature(Dictionary source) { var clone = new Dictionary(); 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().ToArray(); else clone[kv.Key] = kv.Value; } return clone; } static string GetString(Dictionary dict, string key) { return dict.TryGetValue(key, out var value) ? value?.ToString() ?? "" : ""; } static double GetDouble(Dictionary 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 dict, string key) { if (!dict.TryGetValue(key, out var value) || value == null) return Array.Empty(); 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> 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 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> EdgeSignaturesFromSelection(ModelDoc2 doc) { var result = new List>(); 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 EdgeSignature(Edge edge) { var result = new Dictionary(); 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(); 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(); } 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(); 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 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(); 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 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 ExtrudeSelectedContourSignatures(ModelDoc2 doc, Feature feature) { var result = new List(); 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(); SketchSelectedContourSegmentIdsBySketchId[sketchId] = ids; } ids.Add(segmentId); } } } finally { if (accessed) SafeAction(() => data.ReleaseSelectionAccess()); } } catch { } } static string SketchContourSignature(SketchContour contour) { var tokens = new List(); 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 args, out string note) { args = new Dictionary(); 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 args, out string note) { args = new Dictionary(); 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 args, out string note) { args = new Dictionary(); 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 args, out string note) { args = new Dictionary(); 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 BuildCircularPatternAxisReference(ModelDoc2 doc, object axisObject, string axisName) { var result = new Dictionary(); 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 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 RevolveSelectedContourSignatures(IRevolveFeatureData2 data) { var result = new List(); 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 args, out string note) { args = new Dictionary(); 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 args, out string note) { args = new Dictionary(); 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 args, out string note) { args = new Dictionary(); note = ""; if (!IsReferenceAxisFeature(name, type)) return false; var edgeSignatures = new List>(); var datumPlaneNames = new List(); Dictionary 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> edgeSignatures, List datumPlaneNames, ref Dictionary 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 ReferenceFaceSignature(SketchReferenceResult reference) { var result = new Dictionary(); 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 args, out string note) { args = new Dictionary(); 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(); 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 args, out string note) { args = new Dictionary(); 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>(); 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 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 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 { 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 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 args, string key, out List values) { values = new List(); if (args == null || !args.TryGetValue(key, out var obj) || obj == null) return false; if (obj is IEnumerable doubles) { values = doubles.ToList(); return values.Count > 0; } if (obj is IEnumerable 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 args, string key, out List> points) { points = new List>(); if (args == null || !args.TryGetValue(key, out var obj) || obj == null) return false; if (obj is IEnumerable> listPoints) { points = listPoints.Select(p => p.ToList()).ToList(); return points.Count > 0; } if (obj is IEnumerable objectPoints) { foreach (var pointObj in objectPoints) { if (pointObj is IEnumerable doubles) points.Add(doubles.ToList()); else if (pointObj is IEnumerable objects) points.Add(objects.Select(v => Convert.ToDouble(v, System.Globalization.CultureInfo.InvariantCulture)).ToList()); } return points.Count > 0; } return false; } static bool HasHoleWizardTargetFace(Dictionary 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 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 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> SketchFreePointModelMmFromFeature(Feature sketchFeature) { var points = new List>(); if (sketchFeature == null || sketchFeature.GetSpecificFeature2() is not Sketch sketch) return points; var canonicalPoints = BuildSketchPointCanonicalMap(sketch); var segmentPointIds = new HashSet(); var segmentPointKeys = new HashSet(StringComparer.OrdinalIgnoreCase); foreach (object o in ToObjectArray(SafeObj(() => sketch.GetSketchSegments()))) { if (o is SketchSegment segment) RegisterSegmentPoints(segment, canonicalPoints, segmentPointIds, segmentPointKeys); } var seen = new HashSet(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 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 args, string key, object data, IEnumerable names) { int value = FirstPositiveInt(data, names, int.MinValue); if (value != int.MinValue) args[key] = value; } static void AddIntArgAllowZero(Dictionary args, string key, object data, IEnumerable 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 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 args, string key, object data, IEnumerable 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 args, string key, object data, IEnumerable 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> HoleWizardPositionPointsModelMm(object data, Feature feature) { var points = new List>(); if (data is not IWizardHoleFeatureData2 wizardData) return points; Sketch sketch = FirstSketchFromFeature(feature); var seen = new HashSet(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 modelPoint; if (sketch != null && TrySketchLocalMmToModelMm(sketch, x, y, z, out var transformedPoint)) modelPoint = transformedPoint; else modelPoint = new List { 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> DominantCoplanarPoints(List> candidates) { if (candidates == null || candidates.Count == 0) return new List>(); var best = new List>(); const double bucketMm = 0.01; for (int axis = 0; axis < 3; axis++) { var groups = new Dictionary>>(); 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>(); groups[bucket] = group; } group.Add(point); } foreach (var group in groups.Values) { if (group.Count > best.Count) best = group; } } var result = new List>(); var seen = new HashSet(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 args, out string note) { args = new Dictionary(); 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(); 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 args, out string note) { args = new Dictionary(); 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 args, out string note) { args = new Dictionary(); 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 args, out string note) { skill = ""; args = new Dictionary(); 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 args, out string note) { skill = ""; args = new Dictionary(); 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 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 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 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 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 names, out double[] point) { point = Array.Empty(); 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(); } static string FirstFeatureName(object data, IEnumerable 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 FeatureNames(object data, IEnumerable names) { var result = new List(); var seen = new HashSet(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 FeatureNamesFromObject(object value) { var result = new List(); var seen = new HashSet(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 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 names) { foreach (int index in FaceIndices(doc, data, names)) return index; return 0; } static List FaceIndices(ModelDoc2 doc, object data, IEnumerable names) { var result = new List(); 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 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(); } static List FeatureDimensionValuesMm(Feature feature) { var values = new List(); 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(); } static List SketchFeatureNames(Feature feature) { var names = new List(); 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 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[] box = reference.BoxMm ?? Array.Empty(); 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 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>(); var faceReference = new SketchReferenceResult(); string axisName = ""; double angleRad = 0.0; var debug = new List(); 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 BuildFaceSignatureArgs(SketchReferenceResult reference) { var result = new Dictionary(); 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 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 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(), dependencies = item.Step > 1 ? new[] { $"step-{item.Step - 1:000}" } : Array.Empty(), acceptance = new Dictionary() }).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(), exportOptions = new { outputDirectory = "", fileBaseName = "", formats = Array.Empty() }, 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(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(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(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(); 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 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 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 args, string source, string note) => Skills.Add(new SkillFlowStep { Step = Skills.Count + 1, Skill = skill, Args = args ?? new Dictionary(), 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 f) { try { return f(); } catch { return null; } } static bool SafeBool(Func f, bool d = false) { try { return f(); } catch { return d; } } static T SafeValue(Func 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 BuildSketchPointCanonicalMap(Sketch sk) { var samples = new Dictionary>(); var parent = new Dictionary(); 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(); 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(); 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>(); foreach (var kv in samples) { var root = Find(kv.Key); if (!groups.TryGetValue(root, out var list)) { list = new List(); groups[root] = list; } list.AddRange(kv.Value); } var result = new Dictionary(); 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(); var seen = new HashSet(); 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 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 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()) as object[] ?? Array.Empty(); } catch { return Array.Empty(); } } static object[] ToObjectArray(object value) { if (value == null) return Array.Empty(); 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(); 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(); if (value is double[] doubles) return doubles; if (value is Array array) { var list = new List(); foreach (object item in array) { if (item == null) continue; list.Add(Convert.ToDouble(item)); } return list.ToArray(); } return Array.Empty(); } 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()); } catch { return null; } } static object GetComProperty(object target, string name) { try { return target?.GetType().InvokeMember(name, System.Reflection.BindingFlags.GetProperty, null, target, Array.Empty()); } 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()); } 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()); return target != null; } catch { return false; } } static string Safe(Func f, string d = "") { try { return f() ?? d; } catch { return d; } } static int SafeInt(Func f, int d = -1) { try { return f(); } catch { return d; } } static double SafeDouble(Func 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 d, int k) => d[k] = d.TryGetValue(k, out var v) ? v + 1 : 1; }