using ClosedXML.Excel;
using SolidWorks.Interop.sldworks;
using SolidWorks.Interop.swconst;
using System.Runtime.InteropServices;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Text.RegularExpressions;

namespace PartFeatureAudit;

internal class Program
{
    const int Part = 1, Silent = 1, ReadOnly = 2;
    static readonly List<Row> Rows = new();
    static readonly List<SkillFlowStep> Skills = new();
    static readonly HashSet<IntPtr> SeenSketch = new();
    static readonly HashSet<string> EmittedRefPlaneCreation = new(StringComparer.OrdinalIgnoreCase);
    static readonly Dictionary<string, int> FeatureStepByName = new(StringComparer.OrdinalIgnoreCase);
    static readonly Dictionary<IntPtr, string> SketchPlanNameById = new();
    static readonly Dictionary<IntPtr, HashSet<IntPtr>> SketchSelectedContourSegmentIdsBySketchId = new();
    static int SketchPlanCounter;
    static SldWorks CurrentSwApp;

    class Row
    {
        public string PartName = "", Feature = "", Type = "", Category = "", Sketch = "", SketchPlane = "", Dims = "", Rels = "", Note = "";
        public int Step, Depth;
    }

    class SkillFlowStep
    {
        [JsonPropertyName("step")]
        public int Step { get; set; }
        [JsonPropertyName("skill")]
        public string Skill { get; set; } = "";
        [JsonPropertyName("args")]
        public Dictionary<string, object> Args { get; set; } = new();
        [JsonPropertyName("source_feature")]
        public string SourceFeature { get; set; } = "";
        [JsonPropertyName("note")]
        public string Note { get; set; } = "";
    }

    class SkillFlowValidationResult
    {
        public bool Ok { get; set; } = true;
        public int TotalSteps { get; set; }
        public List<string> SkillsUsed { get; set; } = new();
        public List<string> Errors { get; set; } = new();
        public List<string> Warnings { get; set; } = new();
    }

    class SketchReferenceDebugInfo
    {
        public string DirectStatus = "";
        public string EditSketchStatus = "";
        public string RollbackStatus = "";
        public string RefetchedSketchStatus = "";
        public string FinalEntityType = "";
    }

    class SketchReferenceResult
    {
        public string Description = "";
        public string EntityType = "";
        public int EntityTypeCode;
        public string Kind = "unresolved";
        public string ReferenceName = "";
        public string PersistReferenceHex = "";
        public int FaceIndex;
        public int HostFeatureStep;
        public string HostFeatureName = "";
        public string SurfaceType = "";
        public double AreaMm2;
        public double[] CenterMm = Array.Empty<double>();
        public double[] BoxMm = Array.Empty<double>();
        public double[] Normal = Array.Empty<double>();

        public string PlaneName => !string.IsNullOrWhiteSpace(ReferenceName) ? ReferenceName : Description;

        public Dictionary<string, object> ToSkillArgs()
        {
            var args = new Dictionary<string, object>
            {
                ["reference_kind"] = Kind,
                ["reference"] = Description
            };
            if (FaceIndex > 0)
            {
                args["face_index"] = FaceIndex;
                args["face_index_scope"] = "SWagent CommonExecutor.ListFaces order";
                args["selection_strategy"] = "create_face_sketch_by_index";
            }
            if (!string.IsNullOrWhiteSpace(HostFeatureName)) args["host_feature"] = HostFeatureName;
            if (HostFeatureStep > 0) args["host_step"] = HostFeatureStep;
            if (!string.IsNullOrWhiteSpace(SurfaceType)) args["surface_type"] = SurfaceType;
            if (AreaMm2 > 0) args["area_mm2"] = AreaMm2;
            if (CenterMm.Length > 0) args["center_mm"] = CenterMm;
            if (BoxMm.Length > 0) args["box_mm"] = BoxMm;
            if (Normal.Length > 0) args["normal"] = Normal;
            if (Normal.Length >= 3)
            {
                args["normal_x"] = Normal[0];
                args["normal_y"] = Normal[1];
                args["normal_z"] = Normal[2];
            }
            if (!string.IsNullOrWhiteSpace(ReferenceName)) args["reference_name"] = ReferenceName;
            if (Kind == "datum_plane" && !string.IsNullOrWhiteSpace(PlaneName)) args["plane_name"] = PlaneName;
            if (!string.IsNullOrWhiteSpace(PersistReferenceHex)) args["persist_ref_hex"] = PersistReferenceHex;
            if (EntityTypeCode != 0) args["entity_type_code"] = EntityTypeCode;
            return args;
        }
    }

    class RefPlaneBuildInfo
    {
        public bool CanCreate;
        public string BuildKind = "";
        public string PlaneName = "";
        public string BaseReferenceKind = "";
        public string BaseReferenceName = "";
        public int BaseFaceIndex;
        public double OffsetMm;
        public double OffsetAbsMm;
        public bool ReverseDirection;
        public bool ReversedReferenceDirection;
        public double AngleDeg;
        public string AxisName = "";
        public List<Dictionary<string, object>> EdgeSignatures = new();
        public Dictionary<string, object> FaceSignature = new();
        public string Note = "";
    }

    [DllImport("ole32.dll", CharSet = CharSet.Unicode)] static extern int CLSIDFromProgID(string progId, out Guid clsid);
    [DllImport("oleaut32.dll", PreserveSig = false)] [return: MarshalAs(UnmanagedType.IUnknown)] static extern object GetActiveObject(ref Guid clsid, IntPtr reserved);

    [STAThread]
    static void Main(string[] args)
    {
        try
        {
            string path = FirstPositionalArg(
                args,
                Environment.GetEnvironmentVariable("AGENT4_REDUCER_MODEL_DIR") ?? Environment.CurrentDirectory);
            string outputDir = ReadOption(args, "--output-dir");
            bool useActiveDocument = HasFlag(args, "--active");
            Console.WriteLine("正在连接 SolidWorks...");
            var sw = Connect() ?? throw new Exception("无法连接 SolidWorks");

            if (useActiveDocument)
            {
                AuditActivePart(sw);
                Save(string.IsNullOrWhiteSpace(outputDir) ? System.Environment.CurrentDirectory : outputDir);
            }
            else if (Directory.Exists(path))
            {
                foreach (var p in Directory.EnumerateFiles(path, "*.sldprt", SearchOption.AllDirectories)) AuditPartFile(sw, p);
                Save(string.IsNullOrWhiteSpace(outputDir) ? path : outputDir);
            }
            else
            {
                if (Path.GetExtension(path).ToLowerInvariant() != ".sldprt")
                    throw new Exception("该程序只支持 .SLDPRT 零件文件或包含零件的目录");
                AuditPartFile(sw, path);
                Save(string.IsNullOrWhiteSpace(outputDir) ? Path.GetDirectoryName(path) ?? System.Environment.CurrentDirectory : outputDir);
            }
        }
        catch (Exception ex)
        {
            Console.WriteLine("程序出错：" + ex.Message);
            Console.WriteLine(ex);
        }

        Console.WriteLine("按回车退出...");
        if (args.Length == 0)
            Console.ReadLine();
    }

    static string FirstPositionalArg(string[] args, string fallback)
    {
        for (int i = 0; i < args.Length; i++)
        {
            if (string.Equals(args[i], "--output-dir", StringComparison.OrdinalIgnoreCase))
            {
                i++;
                continue;
            }

            if (string.Equals(args[i], "--active", StringComparison.OrdinalIgnoreCase))
                continue;

            if (!args[i].StartsWith("--", StringComparison.OrdinalIgnoreCase))
                return args[i];
        }

        return fallback;
    }

    static string ReadOption(string[] args, string name)
    {
        for (int i = 0; i < args.Length - 1; i++)
            if (string.Equals(args[i], name, StringComparison.OrdinalIgnoreCase))
                return args[i + 1];

        return "";
    }

    static bool HasFlag(string[] args, string name)
    {
        return args.Any(arg => string.Equals(arg, name, StringComparison.OrdinalIgnoreCase));
    }

    static SldWorks Connect()
    {
        try
        {
            int hr = CLSIDFromProgID("SldWorks.Application", out var clsid);
            if (hr < 0) Marshal.ThrowExceptionForHR(hr);
            return (SldWorks)GetActiveObject(ref clsid, IntPtr.Zero);
        }
        catch
        {
            try
            {
                var t = Type.GetTypeFromProgID("SldWorks.Application");
                if (t == null) return null;
                var sw = (SldWorks)Activator.CreateInstance(t);
                if (sw != null) sw.Visible = true;
                return sw;
            }
            catch { return null; }
        }
    }

    static void AuditPartFile(SldWorks sw, string path)
    {
        CurrentSwApp = sw;
        int e = 0, w = 0;
        var doc = sw.OpenDoc6(Path.GetFullPath(path), Part, Silent | ReadOnly, "", ref e, ref w) as ModelDoc2;
        if (doc == null) throw new Exception($"OpenDoc6 失败 errors={e}, warnings={w}, path={path}");
        string title = Safe(() => doc.GetTitle(), Path.GetFileName(path));
        try
        {
            Console.WriteLine("正在读取零件：" + title);
            AuditPart(doc);
        }
        finally
        {
            try
            {
                if (!string.IsNullOrWhiteSpace(title))
                    sw.CloseDoc(title);
            }
            catch
            {
            }
        }
    }

    static void AuditActivePart(SldWorks sw)
    {
        CurrentSwApp = sw;
        var doc = sw.ActiveDoc as ModelDoc2;
        if (doc == null)
            throw new Exception("No active SolidWorks document found.");

        int docType = SafeInt(() => doc.GetType());
        if (docType != (int)swDocumentTypes_e.swDocPART)
            throw new Exception($"Active SolidWorks document is not a part. documentType={docType}");

        string title = Safe(() => doc.GetTitle(), "active_part");
        Console.WriteLine("Reading active SolidWorks part: " + title);
        AuditPart(doc);
    }

    static void AuditPart(ModelDoc2 doc)
    {
        SeenSketch.Clear();
        EmittedRefPlaneCreation.Clear();
        FeatureStepByName.Clear();
        SketchPlanNameById.Clear();
        SketchSelectedContourSegmentIdsBySketchId.Clear();
        SketchPlanCounter = 0;
        string part = Safe(() => doc.GetTitle());
        AddSkill("create_new_part", new(), part, "新建零件");
        int step = 0;
        var pendingTopLevelSketches = new List<Feature>();
        Feature lastHolePlacementSketchFeature = null;
        foreach (var it in Features(doc).Take(2000))
        {
            string name = Safe(() => it.f.Name), type = Safe(() => it.f.GetTypeName2());
            if (ShouldSkipDefaultFeature(name, type)) continue;
            if (IsSketch(type))
            {
                pendingTopLevelSketches.Add(it.f);
                continue;
            }
            if (!string.IsNullOrWhiteSpace(name) && !FeatureStepByName.ContainsKey(name))
                FeatureStepByName[name] = step + 1;
            bool isRevolveBoss = IsRevolveBossFeature(name, type, it.f);
            bool isRevolveCut = IsRevolveCutFeature(name, type, it.f);
            bool isHelix = IsHelixFeature(name, type);
            bool isSweep = IsSweepFeature(name, type);
            bool isHoleWizard = IsHoleWizardFeature(name, type);
            bool isGenericExtrude = !isHelix && !isSweep && !isRevolveBoss && !isRevolveCut &&
                (IsExtrude(name, type, false) || IsExtrude(name, type, true));
            Feature holePlacementSketchFeature = null;
            if (isHoleWizard && pendingTopLevelSketches.Count > 0)
            {
                Feature placementSketchFeature = pendingTopLevelSketches[^1];
                holePlacementSketchFeature = placementSketchFeature;
                lastHolePlacementSketchFeature = placementSketchFeature;
                pendingTopLevelSketches.Clear();
            }
            var row = new Row { PartName = part, Step = ++step, Depth = it.depth, Feature = name, Type = type, Category = Cat(name, type) };
            bool keepsActiveSketch = isRevolveBoss || isRevolveCut;
            if (IsCustomRefPlane(name, type)) AddRefPlaneCreationSkills(doc, it.f, name);
            if (!isHoleWizard && isGenericExtrude)
                RegisterSelectedContourSegmentFilter(doc, it.f);
            if (!isHoleWizard) ReadSubSketches(doc, it.f, row, emitExitSketch: !keepsActiveSketch);
            if (isGenericExtrude)
            {
                bool isCut = IsExtrude(name, type, true);
                double depthMm = Depth(it.f);
                bool reverseDir = ExtrudeReverseDir(it.f, out string directionSource);
                int endCondition = ExtrudeEndCondition(it.f, true);
                bool throughAll = IsThroughAllEndCondition(endCondition);
                var selectedContours = ExtrudeSelectedContourSignatures(doc, it.f);
                row.Dims = AppendText(row.Dims, depthMm > 0 ? $"拉伸深度={R(depthMm)}mm" : "拉伸深度=未读取到");
                AddSkill(isCut ? "extrude_cut_mm" : "extrude_boss_mm", new() { ["depth_mm"] = depthMm, ["reverse_dir"] = reverseDir, ["direction_source"] = directionSource }, name, isCut ? "切除拉伸" : "凸台拉伸");
                if (Skills.Count > 0)
                    AddExtrudeDirectionModelArgs(it.f, reverseDir, Skills[^1].Args);
                if (Skills.Count > 0 && isCut)
                    AddExtrudeStartArgs(it.f, Skills[^1].Args);
                if (Skills.Count > 0)
                    Skills[^1].Args.Remove("reverse_dir");
                if (Skills.Count > 0)
                {
                    if (endCondition >= 0)
                    {
                        Skills[^1].Args["end_condition_code"] = endCondition;
                        Skills[^1].Args["end_condition"] = EndConditionName(endCondition);
                    }
                    if (throughAll) Skills[^1].Args["through_all"] = true;
                }
                if (selectedContours.Count > 0 && Skills.Count > 0)
                {
                    Skills[^1].Args["selected_contours"] = selectedContours;
                    Skills[^1].Args["selected_contour_count"] = selectedContours.Count;
                }
            }
            else if (isRevolveBoss)
            {
                if (TryExtractRevolveBoss(doc, it.f, out var revolveArgs, out string revolveNote))
                {
                    row.Dims = AppendText(row.Dims, revolveNote);
                    AddSkill("create_revolve_boss_mm", revolveArgs, name, "旋转凸台");
                }
                else
                {
                    row.Note = AppendText(row.Note, revolveNote);
                }
            }
            else if (isRevolveCut)
            {
                if (TryExtractRevolveCut(doc, it.f, out var revolveArgs, out string revolveNote))
                {
                    row.Dims = AppendText(row.Dims, revolveNote);
                    AddSkill("create_revolve_cut_mm", revolveArgs, name, "旋转切除");
                }
                else
                {
                    row.Note = AppendText(row.Note, revolveNote);
                }
            }
            else if (isHelix)
            {
                if (TryExtractHelix(it.f, name, type, out var helixArgs, out string helixNote))
                {
                    row.Note = AppendText(row.Note, helixNote);
                    AddSkill("create_helix_mm", helixArgs, name, "helix");
                }
                else
                {
                    row.Note = AppendText(row.Note, helixNote);
                }
            }
            else if (IsFilletFeature(name, type))
            {
                if (TryExtractSimpleFillet(doc, it.f, out var filletArgs, out string filletNote))
                {
                    row.Note = AppendText(row.Note, filletNote);
                    AddSkill("select_edges_by_signature", new() { ["edge_signatures"] = filletArgs["edge_signatures"], ["source_feature"] = name }, name, "圆角边选择");
                    AddSkill("fillet_edges_radius_mm", new() { ["radius_mm"] = filletArgs["radius_mm"] }, name, "圆角");
                }
                else
                {
                    row.Note = AppendText(row.Note, filletNote);
                }
            }
            else if (IsChamferFeature(name, type))
            {
                if (TryExtractChamfer(doc, it.f, out var chamferArgs, out string chamferNote))
                {
                    row.Note = AppendText(row.Note, chamferNote);
                    AddSkill("select_edges_by_signature", new() { ["edge_signatures"] = chamferArgs["edge_signatures"], ["source_feature"] = name }, name, "倒角边选择");
                    AddSkill("chamfer_edges_angle_distance_mm", new()
                    {
                        ["distance_mm"] = chamferArgs["distance_mm"],
                        ["angle_deg"] = chamferArgs["angle_deg"],
                        ["other_distance_mm"] = chamferArgs["other_distance_mm"],
                        ["chamfer_type"] = chamferArgs["chamfer_type"],
                        ["equal_distance"] = chamferArgs["equal_distance"]
                    }, name, "倒角");
                }
                else
                {
                    row.Note = AppendText(row.Note, chamferNote);
                }
            }
            else if (TryExtractCircularPattern(doc, it.f, name, type, out var circularPatternArgs, out string circularPatternNote))
            {
                row.Note = AppendText(row.Note, circularPatternNote);
                AddSkill("circular_pattern_feature", circularPatternArgs, name, "圆周阵列");
            }
            else if (TryExtractLinearPattern(doc, it.f, name, type, out var linearPatternArgs, out string linearPatternNote))
            {
                row.Note = AppendText(row.Note, linearPatternNote);
                AddSkill("linear_pattern_feature_mm", linearPatternArgs, name, "线性阵列");
            }
            else if (TryExtractDraft(doc, it.f, name, type, out var draftArgs, out string draftNote))
            {
                row.Note = AppendText(row.Note, draftNote);
                AddSkill("draft_faces_neutral_plane_mm", draftArgs, name, "拔模");
            }
            else if (TryExtractRib(it.f, name, type, out var ribArgs, out string ribNote))
            {
                row.Note = AppendText(row.Note, ribNote);
                AddSkill("create_rib_mm", ribArgs, name, "筋");
            }
            else if (TryExtractReferenceAxis(doc, it.f, name, type, out var axisArgs, out string axisNote))
            {
                row.Note = AppendText(row.Note, axisNote);
                AddSkill("create_reference_axis", axisArgs, name, "基准轴");
            }
            else if (TryExtractShell(doc, it.f, name, type, out var shellArgs, out string shellNote))
            {
                row.Note = AppendText(row.Note, shellNote);
                AddSkill("shell_remove_face_at_point_mm", shellArgs, name, "抽壳");
            }
            else if (TryExtractHoleWizard(doc, it.f, name, type, out var holeArgs, out string holeNote))
            {
                if (!HasHoleWizardTargetFace(holeArgs))
                {
                    Feature matchedPlacementSketch = null;
                    if (holePlacementSketchFeature != null &&
                        HoleWizardPointsMatchSketch(holePlacementSketchFeature, holeArgs))
                        matchedPlacementSketch = holePlacementSketchFeature;
                    else if (lastHolePlacementSketchFeature != null &&
                        HoleWizardPointsMatchSketch(lastHolePlacementSketchFeature, holeArgs))
                        matchedPlacementSketch = lastHolePlacementSketchFeature;

                    if (matchedPlacementSketch != null)
                        AddHoleWizardTargetFaceFromPlacementSketch(doc, matchedPlacementSketch, holeArgs);
                }
                row.Note = AppendText(row.Note, holeNote);
                AddSkill("hole_wizard_threaded_mm", holeArgs, name, "异形孔向导");
            }
            else if (TryExtractMirror(doc, it.f, name, type, out var mirrorArgs, out string mirrorNote))
            {
                row.Note = AppendText(row.Note, mirrorNote);
                AddSkill("mirror_feature_about_plane", mirrorArgs, name, "镜像");
            }
            else if (TryExtractWrap(doc, it.f, name, type, out var wrapArgs, out string wrapNote))
            {
                row.Note = AppendText(row.Note, wrapNote);
                AddSkill("wrap_sketch_on_faces_mm", wrapArgs, name, "包覆");
            }
            else if (TryExtractSweep(doc, it.f, name, type, out string sweepSkill, out var sweepArgs, out string sweepNote))
            {
                row.Note = AppendText(row.Note, sweepNote);
                AddSkill(sweepSkill, sweepArgs, name, sweepSkill.Contains("cut", StringComparison.OrdinalIgnoreCase) ? "扫描切除" : "扫描凸台");
            }
            else if (TryExtractLoftOrBoundary(it.f, name, type, out string blendSkill, out var blendArgs, out string blendNote))
            {
                row.Note = AppendText(row.Note, blendNote);
                AddSkill(blendSkill, blendArgs, name, blendSkill.Contains("cut", StringComparison.OrdinalIgnoreCase) ? "放样/边界切除" : "放样/边界凸台");
            }
            Rows.Add(row);
        }
        Console.WriteLine($"  -> 已读取 {step} 个特征");
    }

    static IEnumerable<(Feature f, int depth)> Features(ModelDoc2 doc)
    {
        var seen = new HashSet<IntPtr>();
        Feature f = doc.FirstFeature() as Feature;
        for (int guard = 0; f != null && guard < 2000; guard++)
        {
            var id = Id(f);
            if (id != IntPtr.Zero && !seen.Add(id)) break;
            yield return (f, 0);
            f = f.GetNextFeature() as Feature;
        }
    }

    static void ReadSubSketches(ModelDoc2 doc, Feature feat, Row row, bool emitExitSketch = true)
    {
        Feature sub = feat.GetFirstSubFeature() as Feature;
        for (int i = 0; sub != null && i < 200; i++)
        {
            if (IsSketch(Safe(() => sub.GetTypeName2()))) ReadSketch(doc, sub, row, emitExitSketch);
            sub = sub.GetNextSubFeature() as Feature;
        }
    }

    static void ReadSketch(ModelDoc2 doc, Feature feat, Row row, bool emitExitSketch = true)
    {
        var id = Id(feat); if (id != IntPtr.Zero && !SeenSketch.Add(id)) return;
        if (feat.GetSpecificFeature2() is not Sketch sk) { row.Note += "无法转 ISketch；"; return; }
        string sourceName = Safe(() => feat.Name, row.Feature);
        string name = CanonicalSketchPlanName(feat, sourceName);
        var debug = new SketchReferenceDebugInfo();
        var sketchReference = ResolveSketchPlane(doc, feat, sk, debug);
        NormalizeGenericDatumPlaneReference(sk, sketchReference);
        row.SketchPlane = sketchReference.Description;
        row.Note = AppendText(row.Note, BuildSketchReferenceDebugNote(debug));
        EnsureRefPlaneCreationSkills(doc, sketchReference, name);
        AddSketchStartSkill(sketchReference, name);
        row.Sketch = Segments(doc, sk, name, row.Type);
        row.Dims = Dimensions(sk, feat);
        row.Rels = Relations(sk);
        if (emitExitSketch)
            AddSkill("exit_sketch", new(), name, "退出草图");
    }

    static string CanonicalSketchPlanName(Feature feat, string sourceName)
    {
        IntPtr id = Id(feat);
        if (id != IntPtr.Zero && SketchPlanNameById.TryGetValue(id, out string existing))
            return existing;

        string canonical = "草图" + (++SketchPlanCounter).ToString(System.Globalization.CultureInfo.InvariantCulture);
        if (id != IntPtr.Zero)
            SketchPlanNameById[id] = canonical;
        return canonical;
    }

    static SketchReferenceResult ResolveSketchPlane(ModelDoc2 doc, Feature feat, Sketch sk, SketchReferenceDebugInfo debug)
    {
        if (TryDescribeSketchReference(doc, sk, out var direct))
        {
            debug.DirectStatus = "直接获取成功";
            debug.FinalEntityType = direct.EntityType;
            return direct;
        }

        debug.DirectStatus = string.IsNullOrWhiteSpace(direct.EntityType) ? "直接获取失败:null" : $"直接获取失败:{direct.EntityType}";

        if (TryDescribeSketchReferenceByEditSketch(doc, feat, out var editSketch, debug))
            return editSketch;

        return new SketchReferenceResult { Description = "未识别（引用实体可能已被消耗）" };
    }

    static bool TryDescribeSketchReference(ModelDoc2 doc, Sketch sk, out SketchReferenceResult result)
    {
        result = new SketchReferenceResult();
        if (sk == null)
        {
            result.EntityType = "null";
            return false;
        }

        int referenceType = 0;
        object entity = SafeObj(() => sk.GetReferenceEntity(ref referenceType));
        result.EntityTypeCode = referenceType;
        result.EntityType = entity?.GetType().FullName ?? "null";
        return TryDescribeReferenceEntity(doc, entity, result);
    }

    static bool TryDescribeSketchReferenceByEditSketch(ModelDoc2 doc, Feature feat, out SketchReferenceResult result, SketchReferenceDebugInfo debug)
    {
        result = new SketchReferenceResult();
        if (doc == null || feat == null) return false;

        string featureName = Safe(() => feat.Name);
        bool enteredEditSketch = false;
        try
        {
            SafeAction(() => doc.ClearSelection2(true));
            bool selected = SafeBool(() => feat.Select2(false, -1));
            if (!selected)
            {
                debug.EditSketchStatus = "编辑草图前选择草图失败";
                return false;
            }

            enteredEditSketch = TryInvokeCom(doc, "EditSketch", Array.Empty<object>());
            debug.EditSketchStatus = enteredEditSketch ? "EditSketch调用成功" : "EditSketch调用失败";
            if (!enteredEditSketch) return false;

            if (TryGetActiveSketch(doc, out var activeSketch) && TryDescribeSketchReference(doc, activeSketch, out result))
            {
                result.Description += "（编辑草图恢复）";
                debug.FinalEntityType = result.EntityType;
                return true;
            }

            if (TryGetFreshSketch(doc, featureName, out var refreshedSketch))
            {
                debug.RefetchedSketchStatus = "编辑草图后重新获取草图成功";
                if (TryDescribeSketchReference(doc, refreshedSketch, out result))
                {
                    result.Description += "（编辑草图恢复）";
                    debug.FinalEntityType = result.EntityType;
                    return true;
                }

                debug.FinalEntityType = result.EntityType;
                return false;
            }

            debug.RefetchedSketchStatus = "编辑草图后重新获取草图失败";
            return false;
        }
        finally
        {
            if (enteredEditSketch) TryExitSketch(doc);
            SafeAction(() => doc.ClearSelection2(true));
        }
    }

    static string BuildSketchReferenceDebugNote(SketchReferenceDebugInfo debug)
    {
        var parts = new List<string>();
        if (!string.IsNullOrWhiteSpace(debug.DirectStatus)) parts.Add(debug.DirectStatus);
        if (!string.IsNullOrWhiteSpace(debug.EditSketchStatus)) parts.Add(debug.EditSketchStatus);
        if (!string.IsNullOrWhiteSpace(debug.RollbackStatus)) parts.Add(debug.RollbackStatus);
        if (!string.IsNullOrWhiteSpace(debug.RefetchedSketchStatus)) parts.Add(debug.RefetchedSketchStatus);
        if (!string.IsNullOrWhiteSpace(debug.FinalEntityType)) parts.Add("引用类型=" + debug.FinalEntityType);
        return parts.Count == 0 ? "" : "草图基准调试=" + string.Join("；", parts);
    }

    static bool TryGetActiveSketch(ModelDoc2 doc, out Sketch sketch)
    {
        sketch = null;
        var sketchManager = SafeObj(() => doc.SketchManager);
        if (sketchManager == null) return false;

        sketch = SafeObj(() => GetComProperty(sketchManager, "ActiveSketch")) as Sketch;
        return sketch != null;
    }

    static void TryExitSketch(ModelDoc2 doc)
    {
        var sketchManager = SafeObj(() => doc.SketchManager);
        if (!TryInvokeCom(sketchManager, "InsertSketch", true))
            TryInvokeCom(doc, "EditSketch", Array.Empty<object>());
    }

    static bool TryGetFreshSketch(ModelDoc2 doc, string featureName, out Sketch sketch)
    {
        sketch = null;
        if (doc == null || string.IsNullOrWhiteSpace(featureName)) return false;

        var feature = FindFeatureByName(doc, featureName);
        if (feature == null) return false;
        sketch = SafeObj(() => feature.GetSpecificFeature2()) as Sketch;
        return sketch != null;
    }

    static Feature FindFeatureByName(ModelDoc2 doc, string featureName)
    {
        foreach (var it in Features(doc).Take(4000))
        {
            if (string.Equals(Safe(() => it.f.Name), featureName, StringComparison.OrdinalIgnoreCase))
                return it.f;
        }
        return null;
    }

    static bool TryDescribeReferenceEntity(ModelDoc2 doc, object entity, SketchReferenceResult result)
    {
        result.Description = "";
        if (entity == null) return false;

        if (entity is Feature feature)
        {
            string featureType = Safe(() => feature.GetTypeName2(), "未知类型");
            result.ReferenceName = Safe(() => feature.Name, "特征");
            if (string.Equals(featureType, "RefPlane", StringComparison.OrdinalIgnoreCase))
            {
                result.Kind = "datum_plane";
                result.Description = string.IsNullOrWhiteSpace(result.ReferenceName)
                    ? "基准面 / RefPlane"
                    : result.ReferenceName + " / RefPlane";
                return true;
            }

            if (string.Equals(featureType, "RefAxis", StringComparison.OrdinalIgnoreCase))
            {
                result.Kind = "datum_axis";
                result.Description = string.IsNullOrWhiteSpace(result.ReferenceName)
                    ? "基准轴 / RefAxis"
                    : result.ReferenceName + " / RefAxis";
                return true;
            }

            result.Kind = "feature";
            result.Description = result.ReferenceName + " / " + featureType;
            return true;
        }

        if (entity is RefPlane plane)
        {
            string name = SafeObj(() => InvokeCom(plane, "Name"))?.ToString() ?? "";
            if (IsGenericRefPlaneName(name))
                name = FindRefPlaneFeatureName(doc, plane);
            result.Kind = "datum_plane";
            result.ReferenceName = string.IsNullOrWhiteSpace(name) ? "基准面" : name;
            result.Description = result.ReferenceName + " / RefPlane";
            return true;
        }

        if (entity is Face2 face)
        {
            result.Kind = "face";
            FillFaceSelector(doc, face, result);
            result.Description = DescribeFace(face, result);
            result.PersistReferenceHex = TryGetPersistReferenceHex(doc, face);
            return !string.IsNullOrWhiteSpace(result.Description);
        }

        result.Kind = "unknown_com_entity";
        result.Description = entity.GetType().Name;
        return true;
    }

    static bool IsGenericRefPlaneName(string name)
    {
        string n = (name ?? "").Trim();
        return string.IsNullOrWhiteSpace(n)
            || string.Equals(n, "基准面", StringComparison.OrdinalIgnoreCase)
            || string.Equals(n, "基准面 / RefPlane", StringComparison.OrdinalIgnoreCase)
            || string.Equals(n, "Plane", StringComparison.OrdinalIgnoreCase)
            || string.Equals(n, "RefPlane", StringComparison.OrdinalIgnoreCase)
            || (n.Contains("RefPlane", StringComparison.OrdinalIgnoreCase)
                && !n.Any(char.IsDigit)
                && !n.Contains("front", StringComparison.OrdinalIgnoreCase)
                && !n.Contains("top", StringComparison.OrdinalIgnoreCase)
                && !n.Contains("right", StringComparison.OrdinalIgnoreCase)
                && !n.Contains("前")
                && !n.Contains("上")
                && !n.Contains("右"));
    }

    static void NormalizeGenericDatumPlaneReference(Sketch sketch, SketchReferenceResult reference)
    {
        if (sketch == null || reference == null) return;
        if (!string.Equals(reference.Kind, "datum_plane", StringComparison.OrdinalIgnoreCase)) return;
        if (!IsGenericRefPlaneName(reference.ReferenceName) && !IsGenericRefPlaneName(reference.Description)) return;

        reference.Kind = "unresolved";
        reference.ReferenceName = "";
        reference.Description = "datum plane reference name unavailable from SolidWorks API";
    }

    static bool TryInferDefaultPlaneFromSketchTransform(Sketch sketch, out string planeKey)
    {
        planeKey = "";
        if (!TrySketchLocalMmToModelMm(sketch, 0.0, 0.0, 0.0, out var origin) ||
            !TrySketchLocalMmToModelMm(sketch, 0.0, 0.0, 1.0, out var zPoint))
            return false;

        double nx = zPoint[0] - origin[0];
        double ny = zPoint[1] - origin[1];
        double nz = zPoint[2] - origin[2];
        var normal = Normalize(nx, ny, nz);
        if (normal.Length < 3) return false;

        double ax = Math.Abs(normal[0]);
        double ay = Math.Abs(normal[1]);
        double az = Math.Abs(normal[2]);
        const double originToleranceMm = 0.05;
        const double axisTolerance = 0.95;

        if (ax >= ay && ax >= az && ax >= axisTolerance && Math.Abs(origin[0]) <= originToleranceMm)
        {
            planeKey = "right";
            return true;
        }
        if (ay >= ax && ay >= az && ay >= axisTolerance && Math.Abs(origin[1]) <= originToleranceMm)
        {
            planeKey = "top";
            return true;
        }
        if (az >= ax && az >= ay && az >= axisTolerance && Math.Abs(origin[2]) <= originToleranceMm)
        {
            planeKey = "front";
            return true;
        }

        return false;
    }

    static string FindRefPlaneFeatureName(ModelDoc2 doc, RefPlane plane)
    {
        if (doc == null || plane == null) return "";
        IntPtr targetId = Id(plane);
        if (targetId == IntPtr.Zero) return "";

        foreach (var it in Features(doc).Take(5000))
        {
            if (!string.Equals(Safe(() => it.f.GetTypeName2()), "RefPlane", StringComparison.OrdinalIgnoreCase))
                continue;

            object specific = SafeObj(() => it.f.GetSpecificFeature2());
            if (specific == null) continue;
            if (Id(specific) == targetId)
                return Safe(() => it.f.Name, "");
        }

        return "";
    }

    static string DescribeFace(Face2 face, SketchReferenceResult result = null)
    {
        string host = result?.HostFeatureName ?? "";
        try
        {
            if (string.IsNullOrWhiteSpace(host) && face.GetFeature() is Feature f) host = Safe(() => f.Name);
        }
        catch { }

        string indexText = result != null && result.FaceIndex > 0 ? $"#{result.FaceIndex} " : "";
        if (result != null && !string.IsNullOrWhiteSpace(result.SurfaceType))
        {
            string label = result.SurfaceType switch
            {
                "plane" => "平面面",
                "cylinder" => "圆柱面",
                "cone" => "圆锥面",
                "sphere" => "球面",
                _ => "面引用"
            };
            return string.IsNullOrWhiteSpace(host) ? $"{indexText}{label}" : $"{indexText}{label} / 宿主特征={host}";
        }

        try
        {
            if (face.GetSurface() is Surface s)
            {
                if (SafeBool(() => s.IsPlane())) return string.IsNullOrWhiteSpace(host) ? "平面面" : $"平面面 / 宿主特征={host}";
                if (SafeBool(() => s.IsCylinder())) return string.IsNullOrWhiteSpace(host) ? "圆柱面" : $"圆柱面 / 宿主特征={host}";
                if (SafeBool(() => s.IsCone())) return string.IsNullOrWhiteSpace(host) ? "圆锥面" : $"圆锥面 / 宿主特征={host}";
                if (SafeBool(() => s.IsSphere())) return string.IsNullOrWhiteSpace(host) ? "球面" : $"球面 / 宿主特征={host}";
            }
        }
        catch { }

        return string.IsNullOrWhiteSpace(host) ? "面引用" : $"面引用 / 宿主特征={host}";
    }

    static void FillFaceSelector(ModelDoc2 doc, Face2 face, SketchReferenceResult result)
    {
        if (face == null || result == null) return;

        try
        {
            if (face.GetFeature() is Feature host)
            {
                result.HostFeatureName = Safe(() => host.Name);
                if (!string.IsNullOrWhiteSpace(result.HostFeatureName) &&
                    FeatureStepByName.TryGetValue(result.HostFeatureName, out int hostStep))
                    result.HostFeatureStep = hostStep;
            }
        }
        catch { }

        result.FaceIndex = FindFaceIndexLikeExecutor(doc, face);
        result.AreaMm2 = R(SafeDouble(() => face.GetArea()) * 1_000_000.0);
        result.BoxMm = TryGetFaceBoxMm(face);
        if (result.BoxMm.Length >= 6)
        {
            result.CenterMm = new[]
            {
                R((result.BoxMm[0] + result.BoxMm[3]) / 2.0),
                R((result.BoxMm[1] + result.BoxMm[4]) / 2.0),
                R((result.BoxMm[2] + result.BoxMm[5]) / 2.0)
            };
        }

        try
        {
            if (face.GetSurface() is Surface surface)
            {
                result.SurfaceType = SurfaceKind(surface);
                if (SafeBool(() => surface.IsPlane()))
                    result.Normal = TryGetPlaneNormal(face, surface);
            }
        }
        catch { }
    }

    static int FindFaceIndexLikeExecutor(ModelDoc2 doc, Face2 target)
    {
        if (doc == null || target == null) return 0;
        var targetId = Id(target);
        if (targetId == IntPtr.Zero) return 0;

        try
        {
            if (doc is not PartDoc part) return 0;
            object bodiesObj = part.GetBodies2((int)swBodyType_e.swAllBodies, true);
            int faceIndex = 0;
            foreach (object bodyObj in ToObjectArray(bodiesObj))
            {
                object facesObj = InvokeCom(bodyObj, "GetFaces");
                foreach (object faceObj in ToObjectArray(facesObj))
                {
                    if (faceObj is not Face2 candidate) continue;
                    faceIndex++;
                    if (Id(candidate) == targetId) return faceIndex;
                }
            }
        }
        catch { }

        return 0;
    }

    static string SurfaceKind(Surface surface)
    {
        if (surface == null) return "";
        try
        {
            if (surface.IsPlane()) return "plane";
            if (surface.IsCylinder()) return "cylinder";
            if (surface.IsCone()) return "cone";
            if (surface.IsSphere()) return "sphere";
        }
        catch { }
        return "unknown";
    }

    static double[] TryGetFaceBoxMm(Face2 face)
    {
        try
        {
            var values = ToDoubleArray(face.GetBox());
            if (values.Length >= 6)
                return values.Take(6).Select(v => R(v * 1000.0)).ToArray();
        }
        catch { }
        return Array.Empty<double>();
    }

    static double[] TryGetPlaneNormal(Face2 face, Surface surface)
    {
        try
        {
            var values = ToDoubleArray(GetComProperty(surface, "PlaneParams"));
            if (values.Length == 0)
                values = ToDoubleArray(InvokeCom(surface, "PlaneParams"));
            if (values.Length >= 6)
            {
                var first = Normalize(values[0], values[1], values[2]);
                if (first.Length >= 3) return ApplyFaceSense(face, first);

                var second = Normalize(values[3], values[4], values[5]);
                if (second.Length >= 3) return ApplyFaceSense(face, second);
            }
        }
        catch { }
        return Array.Empty<double>();
    }

    static double[] FaceNormal(Face2 face)
    {
        if (face == null) return Array.Empty<double>();
        try
        {
            if (face.GetSurface() is Surface surface && SafeBool(() => surface.IsPlane()))
                return TryGetPlaneNormal(face, surface);
        }
        catch { }
        return Array.Empty<double>();
    }

    static double[] ApplyFaceSense(Face2 face, double[] normal)
    {
        if (normal.Length < 3) return normal;
        try
        {
            if (face.FaceInSurfaceSense())
                return new[] { R(-normal[0]), R(-normal[1]), R(-normal[2]) };
        }
        catch { }
        return normal;
    }

    static void EnrichExtrudeDirectionHints()
    {
        for (int i = 0; i < Skills.Count; i++)
        {
            var step = Skills[i];
            if (!string.Equals(step.Skill, "extrude_boss_mm", StringComparison.OrdinalIgnoreCase))
                continue;
            if (!TryGetDouble(step.Args, "depth_mm", out double depthMm) || depthMm <= 0)
                continue;

            var start = FindNearestFaceSketchCenter(i, -1);
            var end = FindNearestFaceSketchCenter(i, 1);
            if (start == null || end == null)
                continue;

            double dx = end[0] - start[0];
            double dy = end[1] - start[1];
            double dz = end[2] - start[2];
            double length = Math.Sqrt(dx * dx + dy * dy + dz * dz);
            if (length <= 1e-6)
                continue;

            double tolerance = Math.Max(1.0, depthMm * 0.05);
            if (Math.Abs(length - depthMm) > tolerance)
                continue;

            step.Args["direction_x"] = R(dx / length);
            step.Args["direction_y"] = R(dy / length);
            step.Args["direction_z"] = R(dz / length);
            step.Args["expected_end_x_mm"] = R(end[0]);
            step.Args["expected_end_y_mm"] = R(end[1]);
            step.Args["expected_end_z_mm"] = R(end[2]);
            step.Args["direction_hint_source"] = "next_face_sketch_center";
        }
    }

    static double[] FindNearestFaceSketchCenter(int startIndex, int direction)
    {
        for (int i = startIndex + direction; i >= 0 && i < Skills.Count; i += direction)
        {
            var step = Skills[i];
            if (string.Equals(step.Skill, "extrude_boss_mm", StringComparison.OrdinalIgnoreCase) ||
                string.Equals(step.Skill, "extrude_cut_mm", StringComparison.OrdinalIgnoreCase))
                return null;

            if (!string.Equals(step.Skill, "create_face_sketch_by_signature", StringComparison.OrdinalIgnoreCase))
                continue;

            if (TryGetDouble(step.Args, "center_x_mm", out double x) &&
                TryGetDouble(step.Args, "center_y_mm", out double y) &&
                TryGetDouble(step.Args, "center_z_mm", out double z))
                return new[] { x, y, z };

            if (step.Args.TryGetValue("center_mm", out object value))
            {
                var values = FlattenDoubles(value).Take(3).ToArray();
                if (values.Length >= 3)
                    return values;
            }
        }

        return null;
    }

    static bool TryGetDouble(Dictionary<string, object> args, string key, out double value)
    {
        value = 0;
        if (args == null || !args.TryGetValue(key, out object raw) || raw == null)
            return false;
        try
        {
            if (raw is double d) { value = d; return true; }
            if (raw is float f) { value = f; return true; }
            if (raw is int i) { value = i; return true; }
            if (raw is long l) { value = l; return true; }
            if (raw is decimal m) { value = (double)m; return true; }
            return double.TryParse(raw.ToString(), out value);
        }
        catch
        {
            return false;
        }
    }

    static void NormalizeAutoNumberedFeatureNames()
    {
        var counters = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
        var renameMap = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);

        foreach (var step in Skills)
        {
            if (!IsFeatureCreatingSkill(step.Skill))
                continue;
            if (!TrySplitAutoNumberedName(step.SourceFeature, out string prefix, out int sourceNumber))
                continue;

            int expectedNumber = counters.TryGetValue(prefix, out int current) ? current + 1 : 1;
            counters[prefix] = expectedNumber;
            if (sourceNumber != expectedNumber)
                renameMap[step.SourceFeature] = prefix + expectedNumber.ToString(System.Globalization.CultureInfo.InvariantCulture);
        }

        if (renameMap.Count == 0)
            return;

        foreach (var step in Skills)
        {
            step.SourceFeature = RewriteFeatureNameText(step.SourceFeature, renameMap);
            step.Note = RewriteFeatureNameText(step.Note, renameMap);

            var keys = step.Args.Keys.ToList();
            foreach (string key in keys)
                step.Args[key] = RewriteFeatureNameValue(step.Args[key], renameMap);
        }
    }

    static bool IsFeatureCreatingSkill(string skill)
    {
        return string.Equals(skill, "extrude_boss_mm", StringComparison.OrdinalIgnoreCase)
            || string.Equals(skill, "extrude_cut_mm", StringComparison.OrdinalIgnoreCase)
            || string.Equals(skill, "create_revolve_boss_mm", StringComparison.OrdinalIgnoreCase)
            || string.Equals(skill, "create_revolve_cut_mm", StringComparison.OrdinalIgnoreCase)
            || string.Equals(skill, "fillet_edges_radius_mm", StringComparison.OrdinalIgnoreCase)
            || string.Equals(skill, "chamfer_edges_angle_distance_mm", StringComparison.OrdinalIgnoreCase)
            || string.Equals(skill, "linear_pattern_feature_mm", StringComparison.OrdinalIgnoreCase)
            || string.Equals(skill, "mirror_feature_about_plane", StringComparison.OrdinalIgnoreCase)
            || string.Equals(skill, "draft_faces_neutral_plane_mm", StringComparison.OrdinalIgnoreCase)
            || string.Equals(skill, "create_rib_mm", StringComparison.OrdinalIgnoreCase)
            || string.Equals(skill, "shell_remove_face_at_point_mm", StringComparison.OrdinalIgnoreCase)
            || string.Equals(skill, "hole_wizard_threaded_mm", StringComparison.OrdinalIgnoreCase)
            || string.Equals(skill, "wrap_sketch_on_faces_mm", StringComparison.OrdinalIgnoreCase)
            || string.Equals(skill, "swept_cut_circular_profile_mm", StringComparison.OrdinalIgnoreCase)
            || string.Equals(skill, "swept_boss_circular_profile_mm", StringComparison.OrdinalIgnoreCase);
    }

    static bool TrySplitAutoNumberedName(string name, out string prefix, out int number)
    {
        prefix = "";
        number = 0;
        if (string.IsNullOrWhiteSpace(name)) return false;

        var match = Regex.Match(name.Trim(), @"^(?<prefix>.*?)(?<number>\d+)$");
        if (!match.Success) return false;

        prefix = match.Groups["prefix"].Value;
        return !string.IsNullOrWhiteSpace(prefix)
            && int.TryParse(match.Groups["number"].Value, out number)
            && number > 0;
    }

    static object RewriteFeatureNameValue(object value, Dictionary<string, string> renameMap)
    {
        if (value == null) return value;
        if (value is string text) return RewriteFeatureNameText(text, renameMap);
        if (value is string[] strings) return strings.Select(item => RewriteFeatureNameText(item, renameMap)).ToArray();
        if (value is List<string> stringList) return stringList.Select(item => RewriteFeatureNameText(item, renameMap)).ToList();
        if (value is Dictionary<string, object> dict)
        {
            var keys = dict.Keys.ToList();
            foreach (string key in keys)
                dict[key] = RewriteFeatureNameValue(dict[key], renameMap);
            return dict;
        }
        if (value is object[] objects)
            return objects.Select(item => RewriteFeatureNameValue(item, renameMap)).ToArray();
        return value;
    }

    static string RewriteFeatureNameText(string text, Dictionary<string, string> renameMap)
    {
        if (string.IsNullOrWhiteSpace(text) || renameMap.Count == 0)
            return text;

        string pattern = string.Join("|", renameMap.Keys
            .OrderByDescending(key => key.Length)
            .Select(Regex.Escape));
        return Regex.Replace(text, $"({pattern})(?!\\d)", match =>
            renameMap.TryGetValue(match.Value, out string mapped) ? mapped : match.Value);
    }

    static double[] Normalize(double x, double y, double z)
    {
        double len = Math.Sqrt(x * x + y * y + z * z);
        if (len < 0.5 || len > 1.5) return Array.Empty<double>();
        return new[] { R(x / len), R(y / len), R(z / len) };
    }

    static string TryGetPersistReferenceHex(ModelDoc2 doc, object entity)
    {
        try
        {
            object raw = doc?.Extension?.GetPersistReference3(entity);
            if (raw is byte[] bytes) return Convert.ToHexString(bytes);
            if (raw is Array array)
            {
                var bytesFromObjects = new List<byte>();
                foreach (object value in array)
                {
                    if (value == null) continue;
                    bytesFromObjects.Add(Convert.ToByte(value));
                }
                if (bytesFromObjects.Count > 0) return Convert.ToHexString(bytesFromObjects.ToArray());
            }
        }
        catch { }

        return "";
    }

    static string Segments(ModelDoc2 doc, Sketch sk, string name, string type)
    {
        int line = 0, arc = 0, spline = 0, point = 0, other = 0;
        var details = new List<string>();
        var canonicalPoints = BuildSketchPointCanonicalMap(sk);
        HashSet<IntPtr> selectedContourSegmentIds = null;
        var sketchId = Id(sk);
        if (sketchId != IntPtr.Zero)
            SketchSelectedContourSegmentIdsBySketchId.TryGetValue(sketchId, out selectedContourSegmentIds);
        var segmentPointIds = new HashSet<IntPtr>();
        var segmentPointKeys = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
        foreach (object o in ToObjectArray(SafeObj(() => sk.GetSketchSegments())))
        {
            if (o is not SketchSegment s) continue;
            var segmentId = Id(s);
            if (selectedContourSegmentIds != null && (segmentId == IntPtr.Zero || !selectedContourSegmentIds.Contains(segmentId)))
                continue;
            int t = SafeInt(() => s.GetType());
            RegisterSegmentPoints(s, canonicalPoints, segmentPointIds, segmentPointKeys);
            if (t == 0 && Line(doc, sk, s, name, type, canonicalPoints, out string lineText)) { line++; details.Add(lineText); }
            else if (t == 1 && Arc(doc, sk, s, name, type, canonicalPoints, out string arcText)) { arc++; details.Add(arcText); }
            else if (s is SketchSpline && Spline(doc, sk, s, name, type, out string splineText)) { spline++; details.Add(splineText); }
            else other++;
        }
        var freePointKeys = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
        foreach (double[] pointLocal in UserSketchPointsLocalMm(sk))
        {
            if (pointLocal.Length < 3) continue;
            double px = pointLocal[0], py = pointLocal[1], pz = pointLocal[2];
            if (segmentPointKeys.Contains(SketchPointCoordKey(px, py, pz))) continue;
            if (!freePointKeys.Add(SketchPointCoordKey(px, py, pz))) continue;
            if (SketchPointLocal(doc, sk, px, py, pz, name, type, out string pointText))
            {
                point++;
                details.Add(pointText);
            }
        }
        string summary = $"lines={line}, arcs={arc}, splines={spline}, points={point}, other={other}";
        return details.Count == 0 ? summary : summary + "；" + string.Join("；", details.Take(80));
    }

    static void RegisterSegmentPoints(SketchSegment segment, Dictionary<IntPtr, double[]> canonicalPoints, HashSet<IntPtr> ids, HashSet<string> coordKeys = null)
    {
        if (segment == null || ids == null) return;
        void Add(SketchPoint p)
        {
            if (p == null) return;
            IntPtr id = Id(p);
            if (id != IntPtr.Zero) ids.Add(id);
            if (coordKeys != null && Pt3(p, canonicalPoints, out var x, out var y, out var z))
                coordKeys.Add(SketchPointCoordKey(x, y, z));
        }

        try
        {
            if (segment is SketchLine line)
            {
                Add(line.GetStartPoint2() as SketchPoint);
                Add(line.GetEndPoint2() as SketchPoint);
            }
            else if (segment is SketchArc arc)
            {
                Add(arc.GetCenterPoint2() as SketchPoint);
                Add(arc.GetStartPoint2() as SketchPoint);
                Add(arc.GetEndPoint2() as SketchPoint);
            }
            else if (segment is SketchSpline spline)
            {
                foreach (object pointObj in ToObjectArray(SafeObj(() => spline.GetPoints2())))
                    Add(pointObj as SketchPoint);
            }
        }
        catch { }
    }

    static bool IsSegmentOwnedSketchPoint(SketchPoint point, double x, double y, double z, HashSet<IntPtr> ids, HashSet<string> coordKeys)
    {
        IntPtr pointId = Id(point);
        if (pointId != IntPtr.Zero && ids != null && ids.Contains(pointId)) return true;
        return coordKeys != null && coordKeys.Contains(SketchPointCoordKey(x, y, z));
    }

    static List<double[]> UserSketchPointsLocalMm(Sketch sketch)
    {
        var result = new List<double[]>();
        if (sketch == null) return result;

        // SolidWorks ISketch.GetUserPoints2 returns only user-created sketch points.
        // Its record layout is 8 doubles per point; the last three values are point location.
        double[] raw = ToDoubleArray(SafeObj(() => sketch.GetUserPoints2()));
        if (raw.Length == 0)
            raw = ToDoubleArray(InvokeCom(sketch, "GetUserPoints2"));
        if (raw.Length == 0)
            return result;

        int count = SafeInt(() => Convert.ToInt32(InvokeCom(sketch, "GetUserPointsCount")), 0);
        int stride = 8;
        if (count > 0 && raw.Length >= count * stride)
        {
            for (int i = 0; i < count; i++)
            {
                int offset = i * stride;
                result.Add(new[] { raw[offset + 5] * 1000.0, raw[offset + 6] * 1000.0, raw[offset + 7] * 1000.0 });
            }
            return result;
        }

        if (raw.Length % stride == 0)
        {
            for (int offset = 0; offset + 7 < raw.Length; offset += stride)
                result.Add(new[] { raw[offset + 5] * 1000.0, raw[offset + 6] * 1000.0, raw[offset + 7] * 1000.0 });
        }
        else if (raw.Length % 3 == 0)
        {
            for (int offset = 0; offset + 2 < raw.Length; offset += 3)
                result.Add(new[] { raw[offset] * 1000.0, raw[offset + 1] * 1000.0, raw[offset + 2] * 1000.0 });
        }

        return result;
    }

    static string SketchPointCoordKey(double x, double y, double z)
    {
        const double tolMm = 0.001;
        return $"{QuantizeMm(x, tolMm)}|{QuantizeMm(y, tolMm)}|{QuantizeMm(z, tolMm)}";
    }

    static long QuantizeMm(double value, double tolerance)
    {
        return (long)Math.Round(value / tolerance, MidpointRounding.AwayFromZero);
    }

    static bool SketchPoint(ModelDoc2 doc, Sketch sk, SketchPoint p, string name, string type, Dictionary<IntPtr, double[]> canonicalPoints, out string info)
    {
        info = "";
        if (!Pt3(p, canonicalPoints, out var x, out var y, out var z)) return false;
        return SketchPointLocal(doc, sk, x, y, z, name, type, out info);
    }

    static bool SketchPointLocal(ModelDoc2 doc, Sketch sk, double x, double y, double z, string name, string type, out string info)
    {
        info = "";
        if (!TrySketchLocalMmToModelMm(sk, x, y, z, out var pointModel)) return false;
        AddSkill("draw_point_mm", new()
        {
            ["coordinate_system"] = "model",
            ["point_model_mm"] = pointModel
        }, name, type);
        info = $"point: model=({pointModel[0]},{pointModel[1]},{pointModel[2]})mm";
        return true;
    }

    static bool Line(ModelDoc2 doc, Sketch sk, SketchSegment s, string name, string type, Dictionary<IntPtr, double[]> canonicalPoints, out string info)
    {
        info = "";
        if (s is not SketchLine l) return false;
        var rawStartPoint = l.GetStartPoint2() as SketchPoint;
        var rawEndPoint = l.GetEndPoint2() as SketchPoint;
        if (!Pt3(rawStartPoint, canonicalPoints, out var x1, out var y1, out var z1) || !Pt3(rawEndPoint, canonicalPoints, out var x2, out var y2, out var z2)) return false;
        if (!TrySketchLocalMmToModelMm(sk, x1, y1, z1, out var startModel) ||
            !TrySketchLocalMmToModelMm(sk, x2, y2, z2, out var endModel))
            return false;
        bool isConstruction = IsConstructionGeometry(s);
        AddSkill(isConstruction ? "draw_center_line_mm" : "draw_line_mm", new()
        {
            ["coordinate_system"] = "model",
            ["start_model_mm"] = startModel,
            ["end_model_mm"] = endModel
        }, name, type);
        string lineKind = isConstruction ? "centerline" : "line";
        info = $"{lineKind}: model_start=({startModel[0]},{startModel[1]},{startModel[2]})mm, model_end=({endModel[0]},{endModel[1]},{endModel[2]})mm";
        return true;
    }

    static bool Arc(ModelDoc2 doc, Sketch sk, SketchSegment s, string name, string type, Dictionary<IntPtr, double[]> canonicalPoints, out string info)
    {
        info = "";
        if (IsConstructionGeometry(s)) return false;
        if (s is not SketchArc a) return false;
        if (!Pt3(a.GetCenterPoint2() as SketchPoint, canonicalPoints, out var cx, out var cy, out var cz)) return false;
        double r = a.GetRadius() * 1000;
        double sx = 0, sy = 0, sz = 0, ex = 0, ey = 0, ez = 0;
        bool ok = Pt3(a.GetStartPoint2() as SketchPoint, canonicalPoints, out sx, out sy, out sz) && Pt3(a.GetEndPoint2() as SketchPoint, canonicalPoints, out ex, out ey, out ez);
        if (!TrySketchLocalMmToModelMm(sk, cx, cy, cz, out var centerModel)) return false;
        if (!ok || Math.Abs(sx - ex) + Math.Abs(sy - ey) < .001)
        {
            AddSkill("draw_circle_diameter_mm", new()
            {
                ["coordinate_system"] = "model",
                ["center_model_mm"] = centerModel,
                ["diameter_mm"] = R(2 * r)
            }, name, type);
            info = $"圆: center=({R(cx)},{R(cy)})mm, diameter={R(2 * r)}mm";
        }
        else
        {
            int direction = TryGetArcDirection(a);
            if (!TrySketchLocalMmToModelMm(sk, sx, sy, sz, out var startModel) ||
                !TrySketchLocalMmToModelMm(sk, ex, ey, ez, out var endModel) ||
                !TryArcMidLocalMm(cx, cy, cz, sx, sy, ex, ey, direction, out var midLocal) ||
                !TrySketchLocalMmToModelMm(sk, midLocal[0], midLocal[1], midLocal[2], out var midModel))
                return false;
            AddSkill("draw_arc_center_start_end_mm", new()
            {
                ["coordinate_system"] = "model",
                ["center_model_mm"] = centerModel,
                ["start_model_mm"] = startModel,
                ["end_model_mm"] = endModel,
                ["mid_model_mm"] = midModel,
                ["direction_source"] = "model_midpoint"
            }, name, type);
            info = $"圆弧: center=({R(cx)},{R(cy)})mm, start=({R(sx)},{R(sy)})mm, end=({R(ex)},{R(ey)})mm, radius={R(r)}mm";
        }
        return true;
    }

    static bool Spline(ModelDoc2 doc, Sketch sk, SketchSegment s, string name, string type, out string info)
    {
        info = "";
        if (IsConstructionGeometry(s)) return false;
        if (s is not SketchSpline spline) return false;

        var pointsModel = new List<List<double>>();
        var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
        foreach (object pointObj in ToObjectArray(SafeObj(() => spline.GetPoints2())))
        {
            if (pointObj is not SketchPoint point) continue;
            if (!Pt3(point, out double x, out double y, out double z)) continue;
            string key = SketchPointCoordKey(x, y, z);
            if (!seen.Add(key)) continue;
            if (TrySketchLocalMmToModelMm(sk, x, y, z, out var modelPoint))
                pointsModel.Add(modelPoint);
        }

        if (pointsModel.Count < 2)
            return false;

        AddSkill("draw_spline_points_mm", new()
        {
            ["coordinate_system"] = "model",
            ["points_model_mm"] = pointsModel,
            ["simulate_natural_ends"] = true
        }, name, type);

        info = $"spline: points={pointsModel.Count}";
        return true;
    }

    static int TryGetArcDirection(SketchArc arc)
    {
        try
        {
            int direction = arc.GetRotationDir();
            if (direction < 0) return -1;
            if (direction > 0) return 1;
        }
        catch { }
        return 1;
    }

    static bool TryArcMidLocalMm(double cx, double cy, double cz, double sx, double sy, double ex, double ey, int direction, out double[] mid)
    {
        mid = Array.Empty<double>();
        double r = Math.Sqrt((sx - cx) * (sx - cx) + (sy - cy) * (sy - cy));
        if (r <= 1e-9) return false;
        double start = Math.Atan2(sy - cy, sx - cx);
        double end = Math.Atan2(ey - cy, ex - cx);
        if (direction < 0)
        {
            while (end >= start) end -= Math.PI * 2.0;
        }
        else
        {
            while (end <= start) end += Math.PI * 2.0;
        }
        double angle = (start + end) / 2.0;
        mid = new[] { R(cx + Math.Cos(angle) * r), R(cy + Math.Sin(angle) * r), R(cz) };
        return true;
    }

    static bool TrySketchLocalMmToModelMm(Sketch sk, double xMm, double yMm, double zMm, out List<double> modelMm)
    {
        modelMm = new List<double>();
        try
        {
            if (sk == null || CurrentSwApp == null) return false;
            MathTransform modelToSketch = sk.ModelToSketchTransform;
            MathTransform sketchToModel = modelToSketch?.Inverse() as MathTransform;
            MathUtility mathUtility = CurrentSwApp.GetMathUtility() as MathUtility;
            MathPoint point = mathUtility?.CreatePoint(new double[] { xMm / 1000.0, yMm / 1000.0, zMm / 1000.0 }) as MathPoint;
            MathPoint transformed = point?.MultiplyTransform(sketchToModel) as MathPoint;
            var data = ToDoubleArray(transformed?.ArrayData);
            if (data.Length < 3) return false;
            modelMm = new List<double> { R(data[0] * 1000.0), R(data[1] * 1000.0), R(data[2] * 1000.0) };
            return true;
        }
        catch
        {
            modelMm = new List<double>();
            return false;
        }
    }

    static void AddExtrudeDirectionModelArgs(Feature feature, bool reverseDir, Dictionary<string, object> args)
    {
        try
        {
            Sketch sketch = FirstSketchFromFeature(feature);
            if (sketch == null) return;
            if (!TrySketchLocalMmToModelMm(sketch, 0.0, 0.0, 0.0, out var origin) ||
                !TrySketchLocalMmToModelMm(sketch, 0.0, 0.0, 1.0, out var zPoint))
                return;

            var normal = Normalize(zPoint[0] - origin[0], zPoint[1] - origin[1], zPoint[2] - origin[2]);
            if (normal.Length < 3) return;
            var direction = reverseDir
                ? new[] { R(-normal[0]), R(-normal[1]), R(-normal[2]) }
                : new[] { normal[0], normal[1], normal[2] };

            args["sketch_normal_model"] = normal.ToList();
            args["extrude_direction_model"] = direction.ToList();
            args["direction_coordinate_system"] = "model";
        }
        catch { }
    }

    static Sketch FirstSketchFromFeature(Feature feature)
    {
        if (feature == null) return null;
        Feature sub = feature.GetFirstSubFeature() as Feature;
        for (int i = 0; sub != null && i < 200; i++)
        {
            if (IsSketch(Safe(() => sub.GetTypeName2())) && sub.GetSpecificFeature2() is Sketch sk)
                return sk;
            sub = sub.GetNextSubFeature() as Feature;
        }
        return null;
    }

    static bool IsConstructionGeometry(SketchSegment segment)
    {
        if (segment == null) return false;
        try { return segment.ConstructionGeometry; }
        catch
        {
            object value = GetComProperty(segment, "ConstructionGeometry");
            return value is bool b && b;
        }
    }

    static bool TryExtractSimpleFillet(ModelDoc2 doc, Feature feature, out Dictionary<string, object> args, out string note)
    {
        args = new Dictionary<string, object>();
        note = "";
        try
        {
            if (feature.GetDefinition() is not ISimpleFilletFeatureData2 data)
                return false;

            bool accessed = SafeBool(() => data.AccessSelections(doc, null));
            try
            {
                double radiusMm = R(data.DefaultRadius * 1000.0);
                var edgeSignatures = EdgeSignatures(data.Edges);
                if (radiusMm <= 0 || edgeSignatures.Count == 0)
                    return false;

                args["radius_mm"] = radiusMm;
                args["edge_signatures"] = edgeSignatures;
                args["fillet_type"] = data.Type;
                args["edge_count"] = edgeSignatures.Count;
                note = $"圆角半径={radiusMm}mm, edges={edgeSignatures.Count}, source=ISimpleFilletFeatureData2";
                return true;
            }
            finally
            {
                if (accessed)
                {
                    try { data.ReleaseSelectionAccess(); } catch { }
                }
            }
        }
        catch (Exception ex)
        {
            note = $"圆角提取失败: {ex.Message}";
            return false;
        }
    }

    static bool TryExtractChamfer(ModelDoc2 doc, Feature feature, out Dictionary<string, object> args, out string note)
    {
        args = new Dictionary<string, object>();
        note = "";
        try
        {
            object data = SafeObj(() => feature.GetDefinition());
            if (data == null)
            {
                note = "倒角提取失败: GetDefinition 返回空";
                return false;
            }

            IChamferFeatureData2 typedData = data as IChamferFeatureData2;
            bool accessed = typedData != null
                ? SafeBool(() => typedData.AccessSelections(doc, null))
                : SafeBool(() => Convert.ToBoolean(InvokeCom(data, "AccessSelections", doc, null)));
            try
            {
                bool typedCast = typedData != null;
                double distanceMm = typedCast ? R(typedData.GetEdgeChamferDistance(1) * 1000.0) : R(ReadChamferDistance(data, 1) * 1000.0);
                if (distanceMm <= 0) distanceMm = R(ReadChamferDistance(data, 0) * 1000.0);
                double otherDistanceMm = typedCast ? R(typedData.GetEdgeChamferDistance(2) * 1000.0) : R(ReadChamferDistance(data, 2) * 1000.0);
                if (otherDistanceMm <= 0) otherDistanceMm = R(ReadChamferDistance(data, 1) * 1000.0);

                double angleRad = typedCast ? typedData.EdgeChamferAngle : ReadDoubleMember(data, "EdgeChamferAngle");
                double angleDeg = R(angleRad * 180.0 / Math.PI);
                if (angleDeg <= 0) angleDeg = 45.0;

                int chamferType = typedCast ? typedData.Type : RInt(ReadDoubleMember(data, "Type"));
                bool equalDistance = typedCast ? typedData.EqualDistance : ReadBoolMember(data, "EqualDistance");
                bool tangentPropagation = typedCast ? typedData.TangentPropagation : ReadBoolMember(data, "TangentPropagation");
                int apiEdgeCount = typedCast ? SafeInt(() => typedData.GetEdgeCount()) : 0;
                object edgesObj = typedCast ? typedData.Edges : GetComProperty(data, "Edges");
                string edgesObjType = edgesObj?.GetType().FullName ?? "null";
                if (edgesObj == null) edgesObj = InvokeCom(data, "IGetEdges");
                var edgeSignatures = EdgeSignatures(edgesObj);
                if (edgeSignatures.Count == 0 && typedCast && apiEdgeCount > 0)
                {
                    object iEdgesObj = SafeObj(() => typedData.IGetEdges(apiEdgeCount));
                    edgesObjType += $"; IGetEdges={iEdgesObj?.GetType().FullName ?? "null"}";
                    edgeSignatures = EdgeSignatures(iEdgesObj);
                }
                if (edgeSignatures.Count == 0)
                    edgeSignatures = EdgeSignaturesFromSelection(doc);
                int faceCount = ToObjectArray(GetComProperty(data, "Faces")).OfType<Face2>().Count();
                int loopCount = ToObjectArray(GetComProperty(data, "Loops")).Length;

                if (distanceMm <= 0)
                    distanceMm = FirstFeatureLengthDimensionMm(feature);
                if (otherDistanceMm <= 0)
                    otherDistanceMm = distanceMm;

                if (distanceMm <= 0 || edgeSignatures.Count == 0)
                {
                    note = $"倒角提取失败: distance={distanceMm}mm, edges={edgeSignatures.Count}, apiEdgeCount={apiEdgeCount}, edgesObj={edgesObjType}, faces={faceCount}, loops={loopCount}, typed={typedCast}, data={data.GetType().FullName}";
                    return false;
                }

                args["distance_mm"] = distanceMm;
                args["angle_deg"] = angleDeg;
                args["other_distance_mm"] = otherDistanceMm;
                args["chamfer_type"] = chamferType;
                args["equal_distance"] = equalDistance;
                args["tangent_propagation"] = tangentPropagation;
                args["edge_signatures"] = edgeSignatures;
                args["edge_count"] = edgeSignatures.Count;
                note = $"倒角距离={distanceMm}mm, other={otherDistanceMm}mm, angle={angleDeg}deg, type={chamferType}, edges={edgeSignatures.Count}, source=IChamferFeatureData2";
                return true;
            }
            finally
            {
                if (accessed)
                {
                    try
                    {
                        if (typedData != null) typedData.ReleaseSelectionAccess();
                        else InvokeCom(data, "ReleaseSelectionAccess");
                    }
                    catch { }
                }
            }
        }
        catch (Exception ex)
        {
            note = $"倒角提取失败: {ex.Message}";
            return false;
        }
    }

    static double ReadChamferDistance(object data, int side)
    {
        try
        {
            object value = InvokeCom(data, "GetEdgeChamferDistance", side);
            if (value != null && double.TryParse(value.ToString(), out double parsed)) return parsed;
        }
        catch { }
        return 0.0;
    }

    static double ReadDoubleMember(object target, string name)
    {
        try
        {
            object raw = GetComProperty(target, name);
            if (raw != null && double.TryParse(raw.ToString(), out double value)) return value;
        }
        catch { }
        try
        {
            object raw = InvokeCom(target, name);
            if (raw != null && double.TryParse(raw.ToString(), out double value)) return value;
        }
        catch { }
        return 0.0;
    }

    static bool ReadBoolMember(object target, string name)
    {
        try
        {
            object raw = GetComProperty(target, name);
            if (TryConvertBool(raw, out bool value)) return value;
        }
        catch { }
        try
        {
            object raw = InvokeCom(target, name);
            if (TryConvertBool(raw, out bool value)) return value;
        }
        catch { }
        return false;
    }

    static int RInt(double value) => (int)Math.Round(value);

    static double FirstFeatureLengthDimensionMm(Feature feature)
    {
        try
        {
            for (var dd = feature.GetFirstDisplayDimension() as DisplayDimension; dd != null; dd = feature.GetNextDisplayDimension(dd) as DisplayDimension)
            {
                if (dd.GetDimension() is Dimension dim)
                {
                    double valueMm = R(dim.SystemValue * 1000.0);
                    if (valueMm > 0 && valueMm < 100000) return valueMm;
                }
            }
        }
        catch { }
        return 0.0;
    }

    static List<Dictionary<string, object>> EdgeSignatures(object edgesObj)
    {
        var result = new List<Dictionary<string, object>>();
        foreach (object item in FlattenComObjects(edgesObj))
        {
            if (TryAsEdge(item, out Edge edge))
            {
                var signature = EdgeSignature(edge);
                if (signature.Count > 0)
                    result.Add(signature);
            }
        }
        return ExpandSymmetricArcEdgeSignatures(result);
    }

    static List<Dictionary<string, object>> ExpandSymmetricArcEdgeSignatures(List<Dictionary<string, object>> signatures)
    {
        var expanded = signatures.Select(CloneSignature).ToList();
        var circles = signatures
            .Where(item => string.Equals(GetString(item, "curve_type"), "circle", StringComparison.OrdinalIgnoreCase))
            .ToList();
        var lines = signatures
            .Where(item => string.Equals(GetString(item, "curve_type"), "line", StringComparison.OrdinalIgnoreCase))
            .ToList();

        foreach (var circle in circles)
        {
            var center = GetDoubleArray(circle, "center_mm");
            if (center.Length < 3)
                continue;

            double radius = GetDouble(circle, "radius_mm");
            var axis = GetDoubleArray(circle, "axis");
            foreach (var line in lines)
            {
                var start = GetDoubleArray(line, "start_mm");
                var end = GetDoubleArray(line, "end_mm");
                if (start.Length < 3 || end.Length < 3)
                    continue;

                int spanAxis = DominantAxis(start, end);
                if (spanAxis < 0)
                    continue;

                double a = start[spanAxis];
                double b = end[spanAxis];
                if (!NearlyEqual(center[spanAxis], a, 0.05) && !NearlyEqual(center[spanAxis], b, 0.05))
                    continue;

                if (!CircleCompatibleWithLineLayer(center, radius, start, end, spanAxis))
                    continue;

                var inferredCenter = center.ToArray();
                inferredCenter[spanAxis] = NearlyEqual(center[spanAxis], a, 0.05) ? b : a;
                if (HasCircleSignatureNear(expanded, inferredCenter, radius, axis))
                    continue;

                var inferred = CloneSignature(circle);
                inferred["center_mm"] = inferredCenter.Select(R).ToArray();
                inferred.Remove("start_mm");
                inferred.Remove("end_mm");
                inferred["inferred_symmetric_counterpart"] = true;
                inferred["inferred_from_center_mm"] = center;
                inferred["inferred_span_axis"] = spanAxis;
                expanded.Add(inferred);
            }
        }

        return expanded;
    }

    static Dictionary<string, object> CloneSignature(Dictionary<string, object> source)
    {
        var clone = new Dictionary<string, object>();
        foreach (var kv in source)
        {
            if (kv.Value is double[] doubles)
                clone[kv.Key] = doubles.ToArray();
            else if (kv.Value is Array array)
                clone[kv.Key] = array.Cast<object>().ToArray();
            else
                clone[kv.Key] = kv.Value;
        }
        return clone;
    }

    static string GetString(Dictionary<string, object> dict, string key)
    {
        return dict.TryGetValue(key, out var value) ? value?.ToString() ?? "" : "";
    }

    static double GetDouble(Dictionary<string, object> dict, string key)
    {
        if (!dict.TryGetValue(key, out var value) || value == null)
            return 0.0;
        try { return Convert.ToDouble(value); }
        catch { return 0.0; }
    }

    static double[] GetDoubleArray(Dictionary<string, object> dict, string key)
    {
        if (!dict.TryGetValue(key, out var value) || value == null)
            return Array.Empty<double>();
        return ToDoubleArray(value);
    }

    static int DominantAxis(double[] start, double[] end)
    {
        double best = 0.0;
        int axis = -1;
        for (int i = 0; i < 3; i++)
        {
            double delta = Math.Abs(end[i] - start[i]);
            if (delta > best)
            {
                best = delta;
                axis = i;
            }
        }
        return best > 0.05 ? axis : -1;
    }

    static bool CircleCompatibleWithLineLayer(double[] center, double radiusMm, double[] start, double[] end, int spanAxis)
    {
        double tolerance = Math.Max(0.1, Math.Abs(radiusMm) + 0.1);
        for (int i = 0; i < 3; i++)
        {
            if (i == spanAxis)
                continue;
            double min = Math.Min(start[i], end[i]) - tolerance;
            double max = Math.Max(start[i], end[i]) + tolerance;
            if (center[i] < min || center[i] > max)
                return false;
        }
        return true;
    }

    static bool HasCircleSignatureNear(List<Dictionary<string, object>> signatures, double[] center, double radiusMm, double[] axis)
    {
        foreach (var item in signatures)
        {
            if (!string.Equals(GetString(item, "curve_type"), "circle", StringComparison.OrdinalIgnoreCase))
                continue;
            var existingCenter = GetDoubleArray(item, "center_mm");
            if (existingCenter.Length < 3 || Distance(existingCenter, center) > 0.1)
                continue;
            double existingRadius = GetDouble(item, "radius_mm");
            if (Math.Abs(existingRadius - radiusMm) > 0.1)
                continue;
            var existingAxis = GetDoubleArray(item, "axis");
            if (axis.Length >= 3 && existingAxis.Length >= 3 && AxisMismatch(axis, existingAxis))
                continue;
            return true;
        }
        return false;
    }

    static bool AxisMismatch(double[] a, double[] b)
    {
        double dot = Math.Abs(a[0] * b[0] + a[1] * b[1] + a[2] * b[2]);
        double la = Math.Sqrt(a[0] * a[0] + a[1] * a[1] + a[2] * a[2]);
        double lb = Math.Sqrt(b[0] * b[0] + b[1] * b[1] + b[2] * b[2]);
        if (la <= 1e-12 || lb <= 1e-12)
            return false;
        return dot / (la * lb) < 0.99;
    }

    static bool NearlyEqual(double a, double b, double tolerance)
    {
        return Math.Abs(a - b) <= tolerance;
    }

    static IEnumerable<object> FlattenComObjects(object value)
    {
        if (value == null) yield break;

        if (value is DispatchWrapper wrapper)
        {
            foreach (object nested in FlattenComObjects(wrapper.WrappedObject))
                yield return nested;
            yield break;
        }

        if (value is Array array)
        {
            foreach (object item in array)
            {
                foreach (object nested in FlattenComObjects(item))
                    yield return nested;
            }
            yield break;
        }

        yield return value;
    }

    static bool TryAsEdge(object value, out Edge edge)
    {
        edge = null;
        if (value == null) return false;
        if (value is DispatchWrapper wrapper)
            value = wrapper.WrappedObject;
        if (value is Edge typed)
        {
            edge = typed;
            return true;
        }
        try
        {
            edge = value as Edge;
            return edge != null;
        }
        catch
        {
            return false;
        }
    }

    static bool TryAsFace(object value, out Face2 face)
    {
        face = null;
        if (value == null) return false;
        if (value is DispatchWrapper wrapper)
            value = wrapper.WrappedObject;
        if (value is Face2 typed)
        {
            face = typed;
            return true;
        }
        try
        {
            face = value as Face2;
            return face != null;
        }
        catch
        {
            return false;
        }
    }

    static List<Dictionary<string, object>> EdgeSignaturesFromSelection(ModelDoc2 doc)
    {
        var result = new List<Dictionary<string, object>>();
        try
        {
            if (doc?.SelectionManager is not SelectionMgr selMgr)
                return result;

            int count = selMgr.GetSelectedObjectCount2(-1);
            for (int i = 1; i <= count; i++)
            {
                object selected = selMgr.GetSelectedObject6(i, -1);
                if (selected is Edge edge)
                {
                    var signature = EdgeSignature(edge);
                    if (signature.Count > 0)
                        result.Add(signature);
                }
            }
        }
        catch { }
        return result;
    }

    static Dictionary<string, object> EdgeSignature(Edge edge)
    {
        var result = new Dictionary<string, object>();
        try
        {
            Vertex startVertex = SafeObj(() => edge.GetStartVertex()) as Vertex;
            Vertex endVertex = SafeObj(() => edge.GetEndVertex()) as Vertex;
            double[] start = VertexPointMm(startVertex);
            double[] end = VertexPointMm(endVertex);
            if (start.Length >= 3) result["start_mm"] = start;
            if (end.Length >= 3) result["end_mm"] = end;

            Curve curve = SafeObj(() => edge.GetCurve()) as Curve;
            string curveType = "other";
            if (curve != null)
            {
                if (SafeBool(() => curve.IsCircle()))
                {
                    curveType = "circle";
                    var circle = ToDoubleArray(GetComProperty(curve, "CircleParams"));
                    if (circle.Length == 0) circle = ToDoubleArray(InvokeCom(curve, "CircleParams"));
                    if (circle.Length >= 7)
                    {
                        result["center_mm"] = new[] { R(circle[0] * 1000.0), R(circle[1] * 1000.0), R(circle[2] * 1000.0) };
                        result["axis"] = new[] { R(circle[3]), R(circle[4]), R(circle[5]) };
                        result["radius_mm"] = R(circle[6] * 1000.0);
                    }
                }
                else if (SafeBool(() => curve.IsLine()))
                {
                    curveType = "line";
                    var line = ToDoubleArray(GetComProperty(curve, "LineParams"));
                    if (line.Length == 0) line = ToDoubleArray(InvokeCom(curve, "LineParams"));
                    if (line.Length >= 6)
                    {
                        result["line_point_mm"] = new[] { R(line[0] * 1000.0), R(line[1] * 1000.0), R(line[2] * 1000.0) };
                        result["line_direction"] = new[] { R(line[3]), R(line[4]), R(line[5]) };
                    }
                }
            }

            if (!result.ContainsKey("center_mm") && start.Length >= 3 && end.Length >= 3)
            {
                result["center_mm"] = new[]
                {
                    R((start[0] + end[0]) / 2.0),
                    R((start[1] + end[1]) / 2.0),
                    R((start[2] + end[2]) / 2.0)
                };
                result["length_mm"] = R(Distance(start, end));
            }

            result["curve_type"] = curveType;
        }
        catch { }
        return result;
    }

    static double[] VertexPointMm(Vertex vertex)
    {
        if (vertex == null) return Array.Empty<double>();
        try
        {
            var values = ToDoubleArray(vertex.GetPoint());
            if (values.Length >= 3)
                return new[] { R(values[0] * 1000.0), R(values[1] * 1000.0), R(values[2] * 1000.0) };
        }
        catch { }
        return Array.Empty<double>();
    }

    static double Distance(double[] a, double[] b)
    {
        if (a.Length < 3 || b.Length < 3) return 0.0;
        double dx = a[0] - b[0], dy = a[1] - b[1], dz = a[2] - b[2];
        return Math.Sqrt(dx * dx + dy * dy + dz * dz);
    }

    static string Dimensions(Sketch sk, Feature feat)
    {
        var list = new List<string>();
        try { foreach (object o in InvokeArray(sk, "GetDimensions")) AddDim(o, list); } catch { }
        try { for (var dd = feat.GetFirstDisplayDimension() as DisplayDimension; dd != null; dd = feat.GetNextDisplayDimension(dd) as DisplayDimension) AddDim(dd, list); } catch { }
        return string.Join("；", list.Distinct().Take(80));
    }

    static void AddDim(object o, List<string> list)
    {
        Dimension d = o as Dimension;
        if (d == null && o is DisplayDimension dd) d = dd.GetDimension() as Dimension;
        if (d != null) list.Add($"{Safe(() => d.GetNameForSelection())}={R(d.SystemValue * 1000)}mm");
    }

    static string Relations(Sketch sk)
    {
        var d = new Dictionary<int, int>();
        try { foreach (object o in SketchRelations(sk)) if (o is SketchRelation r) Inc(d, SafeInt(() => r.GetRelationType())); } catch { }
        return string.Join("，", d.Select(kv => $"约束{kv.Key}:{kv.Value}"));
    }

    static double Depth(Feature f)
    {
        try
        {
            var data = f.GetDefinition() as ExtrudeFeatureData2;
            if (data == null) return 0;
            return R(data.GetDepth(true) * 1000.0);
        }
        catch { return 0; }
    }

    static int ExtrudeEndCondition(Feature f, bool firstDirection)
    {
        try
        {
            var data = f.GetDefinition() as ExtrudeFeatureData2;
            if (data == null) return -1;
            object raw = InvokeCom(data, "GetEndCondition", firstDirection);
            if (raw != null) return Convert.ToInt32(raw);
        }
        catch { }
        return -1;
    }

    static bool IsThroughAllEndCondition(int endCondition)
        => endCondition == (int)swEndConditions_e.swEndCondThroughAll;

    static string EndConditionName(int endCondition)
    {
        if (endCondition == (int)swEndConditions_e.swEndCondBlind) return "blind";
        if (endCondition == (int)swEndConditions_e.swEndCondThroughAll) return "through_all";
        if (endCondition == (int)swEndConditions_e.swEndCondThroughNext) return "through_next";
        if (endCondition == (int)swEndConditions_e.swEndCondUpToVertex) return "up_to_vertex";
        if (endCondition == (int)swEndConditions_e.swEndCondUpToSurface) return "up_to_surface";
        if (endCondition == (int)swEndConditions_e.swEndCondOffsetFromSurface) return "offset_from_surface";
        if (endCondition == (int)swEndConditions_e.swEndCondMidPlane) return "mid_plane";
        return endCondition.ToString(System.Globalization.CultureInfo.InvariantCulture);
    }

    static void AddExtrudeStartArgs(Feature feature, Dictionary<string, object> args)
    {
        try
        {
            if (feature.GetDefinition() is not ExtrudeFeatureData2 data)
                return;

            int fromType = SafeValue(() => data.FromType, (int)swStartConditions_e.swStartSketchPlane);
            if (fromType == (int)swStartConditions_e.swStartSketchPlane)
                return;

            args["start_condition_code"] = fromType;
            args["start_condition"] = StartConditionName(fromType);
            args["start_offset_mm"] = R(SafeValue(() => data.FromOffsetDistance, 0.0) * 1000.0);
            args["flip_start_offset"] = SafeValue(() => data.FromOffsetReverse, false);

            object fromEntity = null;
            int fromEntityType = 0;
            try { data.GetFromEntity(out fromEntity, out fromEntityType); } catch { }
            string fromName = ReferenceName(fromEntity);
            if (!string.IsNullOrWhiteSpace(fromName))
                args["start_reference_name"] = fromName;
            if (fromEntityType != 0)
                args["start_entity_type_code"] = fromEntityType;
        }
        catch { }
    }

    static string StartConditionName(int startCondition)
    {
        if (startCondition == (int)swStartConditions_e.swStartSketchPlane) return "sketch_plane";
        if (startCondition == (int)swStartConditions_e.swStartSurface) return "surface_face_or_plane";
        if (startCondition == (int)swStartConditions_e.swStartVertex) return "vertex";
        if (startCondition == (int)swStartConditions_e.swStartOffset) return "offset";
        return startCondition.ToString(System.Globalization.CultureInfo.InvariantCulture);
    }

    static List<string> ExtrudeSelectedContourSignatures(ModelDoc2 doc, Feature feature)
    {
        var result = new List<string>();
        try
        {
            if (feature.GetDefinition() is not ExtrudeFeatureData2 data)
                return result;

            bool accessed = SafeBool(() => data.AccessSelections(doc, null));
            try
            {
                int count = SafeInt(() => data.GetContoursCount(), 0);
                if (count <= 0) return result;

                var contours = ToObjectArray(SafeObj(() => data.Contours));
                if (contours.Length == 0)
                    contours = ToObjectArray(SafeObj(() => data.IGetContours(count)));

                foreach (object contourObj in contours)
                {
                    if (contourObj is not SketchContour contour)
                        continue;
                    string signature = SketchContourSignature(contour);
                    if (!string.IsNullOrWhiteSpace(signature) && !result.Contains(signature, StringComparer.OrdinalIgnoreCase))
                        result.Add(signature);
                }
            }
            finally
            {
                if (accessed) SafeAction(() => data.ReleaseSelectionAccess());
            }
        }
        catch { }
        return result;
    }

    static void RegisterSelectedContourSegmentFilter(ModelDoc2 doc, Feature feature)
    {
        try
        {
            if (doc == null || feature == null)
                return;
            if (feature.GetDefinition() is not ExtrudeFeatureData2 data)
                return;

            bool accessed = SafeBool(() => data.AccessSelections(doc, null));
            try
            {
                int count = SafeInt(() => data.GetContoursCount(), 0);
                if (count <= 0)
                    return;

                var contours = ToObjectArray(SafeObj(() => data.Contours));
                if (contours.Length == 0)
                    contours = ToObjectArray(SafeObj(() => data.IGetContours(count)));
                if (contours.Length == 0)
                    return;

                foreach (object contourObj in contours)
                {
                    if (contourObj is not SketchContour contour)
                        continue;
                    foreach (object segmentObj in ToObjectArray(SafeObj(() => contour.GetSketchSegments())))
                    {
                        if (segmentObj is not SketchSegment segment)
                            continue;
                        var segmentId = Id(segment);
                        if (segmentId == IntPtr.Zero)
                            continue;
                        Sketch sketch = SafeObj(() => segment.GetSketch()) as Sketch;
                        var sketchId = Id(sketch);
                        if (sketchId == IntPtr.Zero)
                            continue;
                        if (!SketchSelectedContourSegmentIdsBySketchId.TryGetValue(sketchId, out var ids))
                        {
                            ids = new HashSet<IntPtr>();
                            SketchSelectedContourSegmentIdsBySketchId[sketchId] = ids;
                        }
                        ids.Add(segmentId);
                    }
                }
            }
            finally
            {
                if (accessed) SafeAction(() => data.ReleaseSelectionAccess());
            }
        }
        catch { }
    }

    static string SketchContourSignature(SketchContour contour)
    {
        var tokens = new List<string>();
        foreach (object segObj in ToObjectArray(SafeObj(() => contour.GetSketchSegments())))
        {
            if (segObj is SketchLine line)
            {
                if (!Pt(line.GetStartPoint2() as SketchPoint, out double x1, out double y1)
                    || !Pt(line.GetEndPoint2() as SketchPoint, out double x2, out double y2))
                    continue;
                string p1 = SigPoint(x1, y1);
                string p2 = SigPoint(x2, y2);
                if (string.CompareOrdinal(p1, p2) > 0) (p1, p2) = (p2, p1);
                tokens.Add($"L:{p1}:{p2}");
            }
            else if (segObj is SketchArc arc)
            {
                if (!Pt(arc.GetCenterPoint2() as SketchPoint, out double cx, out double cy)
                    || !Pt(arc.GetStartPoint2() as SketchPoint, out double sx, out double sy)
                    || !Pt(arc.GetEndPoint2() as SketchPoint, out double ex, out double ey))
                    continue;
                string p1 = SigPoint(sx, sy);
                string p2 = SigPoint(ex, ey);
                if (string.CompareOrdinal(p1, p2) > 0) (p1, p2) = (p2, p1);
                tokens.Add($"A:{SigPoint(cx, cy)}:{p1}:{p2}");
            }
        }

        tokens.Sort(StringComparer.Ordinal);
        return string.Join("|", tokens);
    }

    static string SigPoint(double xMm, double yMm)
        => $"{SigNumber(xMm)},{SigNumber(yMm)}";

    static string SigNumber(double value)
        => Math.Round(value, 3).ToString("0.###", System.Globalization.CultureInfo.InvariantCulture);

    static bool IsRevolveBossFeature(string n, string t, Feature feature)
    {
        string s = (n + " " + t).ToLowerInvariant();
        if (s.Contains("cut") || s.Contains("切除") || s.Contains("revcut")) return false;
        if (s.Contains("revol") || s.Contains("revolution") || s.Contains("旋转") || s.Contains("旋轉"))
        {
            try
            {
                if (feature.GetDefinition() is IRevolveFeatureData2 data)
                    return data.IsBossFeature();
            }
            catch { }
            return true;
        }

        try
        {
            if (feature.GetDefinition() is IRevolveFeatureData2 data)
                return data.IsBossFeature();
        }
        catch { }
        return false;
    }

    static bool TryExtractRevolveBoss(ModelDoc2 doc, Feature feature, out Dictionary<string, object> args, out string note)
    {
        args = new Dictionary<string, object>();
        note = "";
        try
        {
            if (feature.GetDefinition() is not IRevolveFeatureData2 data)
            {
                note = "RevolveFeatureData2 not available";
                return false;
            }

            bool accessed = SafeBool(() => data.AccessSelections(doc, null));
            try
            {
                double angleRad = data.GetRevolutionAngle(true);
                if (Math.Abs(angleRad) < 1e-9)
                    angleRad = data.GetRevolutionAngle(false);
                double angleDeg = R(angleRad * 180.0 / Math.PI);
                if (angleDeg <= 0.0) angleDeg = 360.0;

                bool reverseDir = false;
                bool merge = true;
                try { reverseDir = data.ReverseDirection; } catch { }
                try { merge = data.Merge; } catch { }

                args["angle_deg"] = angleDeg;
                args["merge"] = merge;
                args["reverse_dir"] = reverseDir;
                AddRevolveAxisArgs(data, args);
                var selectedContours = RevolveSelectedContourSignatures(data);
                if (selectedContours.Count > 0)
                {
                    args["selected_contours"] = selectedContours;
                    args["selected_contour_count"] = selectedContours.Count;
                }
                note = $"revolve angle={angleDeg}deg, reverse_dir={reverseDir}, merge={merge}";
                return true;
            }
            finally
            {
                if (accessed) SafeAction(() => data.ReleaseSelectionAccess());
            }
        }
        catch (Exception ex)
        {
            note = $"revolve extract failed: {ex.Message}";
            return false;
        }
    }

    static bool IsRevolveCutFeature(string n, string t, Feature feature)
    {
        string s = (n + " " + t).ToLowerInvariant();
        if (s.Contains("revcut") || s.Contains("revolvecut") || s.Contains("cut-revolve") || s.Contains("cut revolve") || s.Contains("旋转切除"))
            return true;
        if ((s.Contains("revol") || s.Contains("revolution") || s.Contains("旋转") || s.Contains("鏃嬭浆") || s.Contains("鏃嬭綁")) &&
            (s.Contains("cut") || s.Contains("切除") || s.Contains("鍒囬櫎")))
            return true;

        try
        {
            if (feature.GetDefinition() is IRevolveFeatureData2 data)
                return !data.IsBossFeature();
        }
        catch { }
        return false;
    }

    static bool TryExtractRevolveCut(ModelDoc2 doc, Feature feature, out Dictionary<string, object> args, out string note)
    {
        args = new Dictionary<string, object>();
        note = "";
        try
        {
            if (feature.GetDefinition() is not IRevolveFeatureData2 data)
            {
                note = "RevolveFeatureData2 not available";
                return false;
            }

            bool accessed = SafeBool(() => data.AccessSelections(doc, null));
            try
            {
                double angleRad = data.GetRevolutionAngle(true);
                if (Math.Abs(angleRad) < 1e-9)
                    angleRad = data.GetRevolutionAngle(false);
                double angleDeg = R(angleRad * 180.0 / Math.PI);
                if (angleDeg <= 0.0) angleDeg = 360.0;

                bool reverseDir = false;
                try { reverseDir = data.ReverseDirection; } catch { }

                args["angle_deg"] = angleDeg;
                args["reverse_dir"] = reverseDir;
                AddRevolveAxisArgs(data, args);
                var selectedContours = RevolveSelectedContourSignatures(data);
                if (selectedContours.Count > 0)
                {
                    args["selected_contours"] = selectedContours;
                    args["selected_contour_count"] = selectedContours.Count;
                }
                note = $"revolve cut angle={angleDeg}deg, reverse_dir={reverseDir}";
                return true;
            }
            finally
            {
                if (accessed) SafeAction(() => data.ReleaseSelectionAccess());
            }
        }
        catch (Exception ex)
        {
            note = $"revolve cut extract failed: {ex.Message}";
            return false;
        }
    }

    static bool TryExtractLinearPattern(ModelDoc2 doc, Feature feature, string name, string type, out Dictionary<string, object> args, out string note)
    {
        args = new Dictionary<string, object>();
        note = "";
        if (!IsLinearPatternFeature(name, type)) return false;

        object data = SafeObj(() => feature.GetDefinition());
        if (data == null)
        {
            note = "linear pattern definition not available";
            return false;
        }

        try { TryInvokeCom(data, "AccessSelections", doc, null); } catch { }
        try
        {
            int count1 = FirstPositiveInt(data, new[] { "D1TotalInstances", "D1InstanceCount", "D1Count", "TotalInstances", "Instances" }, 2);
            double spacing1Mm = FirstPositiveLengthMm(data, new[] { "D1Spacing", "Spacing", "Direction1Spacing" }, 0.0);
            int count2 = FirstPositiveInt(data, new[] { "D2TotalInstances", "D2InstanceCount", "D2Count" }, 1);
            double spacing2Mm = FirstPositiveLengthMm(data, new[] { "D2Spacing", "Direction2Spacing" }, 0.0);
            bool reverseDir1 = FirstBool(data, new[] { "D1ReverseDirection", "ReverseDirection1", "Dir1ReverseDirection" }, false);
            bool reverseDir2 = FirstBool(data, new[] { "D2ReverseDirection", "ReverseDirection2", "Dir2ReverseDirection" }, false);
            bool geometryPattern = FirstBool(data, new[] { "GeometryPattern", "GeometryPatternOption" }, true);

            if (spacing1Mm <= 0.0)
            {
                note = "linear pattern spacing1 not available";
                return false;
            }

            if (!TryReadEdgePointMm(data, new[] { "D1Reference", "D1Direction", "Direction1", "Dir1", "Direction1Reference" }, out var dir1PointMm))
            {
                note = "linear pattern direction1 reference edge not available";
                return false;
            }

            args["count1"] = count1;
            args["spacing1_mm"] = spacing1Mm;
            args["dir1_edge_x_mm"] = dir1PointMm[0];
            args["dir1_edge_y_mm"] = dir1PointMm[1];
            args["dir1_edge_z_mm"] = dir1PointMm[2];
            args["reverse_dir1"] = reverseDir1;
            args["geometry_pattern"] = geometryPattern;
            args["count2"] = count2;
            args["spacing2_mm"] = spacing2Mm;
            args["reverse_dir2"] = reverseDir2;

            string seedFeatureName = FirstFeatureName(data, new[] { "PatternFeatureArray", "FeatureArray", "SeedFeatures", "FeaturesToPattern" });
            if (!string.IsNullOrWhiteSpace(seedFeatureName))
                args["feature_name"] = seedFeatureName;

            if (count2 > 1 && TryReadEdgePointMm(data, new[] { "D2Reference", "D2Direction", "Direction2", "Dir2", "Direction2Reference" }, out var dir2PointMm))
            {
                args["dir2_edge_x_mm"] = dir2PointMm[0];
                args["dir2_edge_y_mm"] = dir2PointMm[1];
                args["dir2_edge_z_mm"] = dir2PointMm[2];
            }

            note = $"linear pattern count1={count1}, spacing1={spacing1Mm}mm, count2={count2}";
            return true;
        }
        finally
        {
            TryInvokeCom(data, "ReleaseSelectionAccess");
        }
    }

    static bool TryExtractCircularPattern(ModelDoc2 doc, Feature feature, string name, string type, out Dictionary<string, object> args, out string note)
    {
        args = new Dictionary<string, object>();
        note = "";
        if (!IsCircularPatternFeature(name, type)) return false;

        object data = SafeObj(() => feature.GetDefinition());
        if (data is not ICircularPatternFeatureData circular)
        {
            note = "circular pattern definition not available";
            return false;
        }

        bool accessed = SafeBool(() => circular.AccessSelections(doc, null));
        try
        {
            int count = SafeValue(() => circular.TotalInstances, 0);
            double spacingRad = SafeValue(() => circular.Spacing, 0.0);
            if (count < 2)
            {
                note = $"circular pattern count invalid: {count}";
                return false;
            }

            if (Math.Abs(spacingRad) <= 1e-9)
            {
                note = "circular pattern spacing angle not available";
                return false;
            }

            object axisObject = SafeObj(() => circular.Axis);
            string axisName = ReferenceName(axisObject);
            if (string.IsNullOrWhiteSpace(axisName))
                axisName = FirstReferenceName(data, new[] { "Axis", "PatternAxis" });
            var axisReference = BuildCircularPatternAxisReference(doc, axisObject, axisName);

            string seedFeatureName = FirstFeatureNameFromObject(SafeObj(() => circular.PatternFeatureArray));
            if (string.IsNullOrWhiteSpace(seedFeatureName))
                seedFeatureName = FirstFeatureName(data, new[] { "PatternFeatureArray", "FeatureArray", "SeedFeatures", "FeaturesToPattern" });

            args["count"] = count;
            args["angle_deg"] = R(spacingRad * 180.0 / Math.PI);
            if (!string.IsNullOrWhiteSpace(axisName))
                args["axis_name"] = axisName;
            if (axisReference.Count > 0)
                args["axis_reference"] = axisReference;
            args["reverse_direction"] = SafeValue(() => circular.ReverseDirection, false);
            args["equal_spacing"] = SafeValue(() => circular.EqualSpacing, false);
            args["geometry_pattern"] = SafeValue(() => circular.GeometryPattern, true);
            args["vary_sketch"] = SafeValue(() => circular.VarySketch, false);
            if (!string.IsNullOrWhiteSpace(seedFeatureName))
                args["source_feature_name"] = seedFeatureName;

            note = $"circular pattern count={count}, angle={args["angle_deg"]}deg, axis={axisName}, axis_ref={GetString(axisReference, "reference_type")}, seed={seedFeatureName}";
            return true;
        }
        finally
        {
            if (accessed) SafeAction(() => circular.ReleaseSelectionAccess());
        }
    }

    static Dictionary<string, object> BuildCircularPatternAxisReference(ModelDoc2 doc, object axisObject, string axisName)
    {
        var result = new Dictionary<string, object>();
        try
        {
            if (!string.IsNullOrWhiteSpace(axisName))
                result["name"] = axisName;

            if (axisObject is Edge edge)
            {
                var signature = EdgeSignature(edge);
                string curveType = GetString(signature, "curve_type");
                result["reference_type"] = string.Equals(curveType, "circle", StringComparison.OrdinalIgnoreCase)
                    ? "circular_edge"
                    : string.Equals(curveType, "line", StringComparison.OrdinalIgnoreCase)
                        ? "linear_edge"
                        : "edge";
                result["edge_signature"] = signature;
                return result;
            }

            if (axisObject is Face2 face)
            {
                var reference = new SketchReferenceResult();
                if (TryDescribeReferenceEntity(doc, face, reference) &&
                    string.Equals(reference.Kind, "face", StringComparison.OrdinalIgnoreCase))
                {
                    var signature = BuildFaceSignatureArgs(reference);
                    string surfaceType = GetString(signature, "surface_type");
                    result["reference_type"] = string.Equals(surfaceType, "cylinder", StringComparison.OrdinalIgnoreCase)
                        ? "cylindrical_face"
                        : "face";
                    result["face_signature"] = signature;
                    return result;
                }
            }

            if (axisObject is Feature feature)
            {
                string typeName = SafeValue(() => feature.GetTypeName2(), "");
                string name = SafeValue(() => feature.Name, axisName);
                if (!string.IsNullOrWhiteSpace(name))
                    result["name"] = name;
                result["reference_type"] = string.Equals(typeName, "RefAxis", StringComparison.OrdinalIgnoreCase)
                    ? "reference_axis"
                    : "feature";
                result["feature_type"] = typeName;
                return result;
            }

            if (!string.IsNullOrWhiteSpace(axisName))
                result["reference_type"] = "reference_axis";
        }
        catch { }

        return result;
    }

    static void AddRevolveAxisArgs(IRevolveFeatureData2 data, Dictionary<string, object> args)
    {
        try
        {
            object axis = SafeObj(() => data.Axis);
            if (axis is not SketchLine line)
                return;

            var sketch = SafeObj(() => InvokeCom(line, "GetSketch")) as Sketch;
            if (sketch == null)
                return;

            var sp = line.GetStartPoint2() as SketchPoint;
            var ep = line.GetEndPoint2() as SketchPoint;
            if (!Pt3(sp, out double sx, out double sy, out double sz) ||
                !Pt3(ep, out double ex, out double ey, out double ez))
                return;

            if (!TrySketchLocalMmToModelMm(sketch, sx, sy, sz, out var startModel) ||
                !TrySketchLocalMmToModelMm(sketch, ex, ey, ez, out var endModel))
                return;

            args["axis_start_model_mm"] = startModel;
            args["axis_end_model_mm"] = endModel;
        }
        catch { }
    }

    static List<string> RevolveSelectedContourSignatures(IRevolveFeatureData2 data)
    {
        var result = new List<string>();
        try
        {
            int count = SafeInt(() => data.GetContoursCount(), 0);
            if (count <= 0)
                return result;

            var contours = ToObjectArray(SafeObj(() => data.Contours));
            if (contours.Length == 0)
                contours = ToObjectArray(SafeObj(() => data.IGetContours(count)));

            foreach (object contourObj in contours)
            {
                if (contourObj is not SketchContour contour)
                    continue;
                string signature = SketchContourSignature(contour);
                if (!string.IsNullOrWhiteSpace(signature) && !result.Contains(signature, StringComparer.OrdinalIgnoreCase))
                    result.Add(signature);
            }
        }
        catch { }

        return result;
    }

    static bool TryExtractDraft(ModelDoc2 doc, Feature feature, string name, string type, out Dictionary<string, object> args, out string note)
    {
        args = new Dictionary<string, object>();
        note = "";
        if (!IsDraftFeature(name, type)) return false;

        object data = SafeObj(() => feature.GetDefinition());
        if (data == null)
        {
            note = "draft definition not available";
            return false;
        }

        try { TryInvokeCom(data, "AccessSelections", doc, null); } catch { }
        try
        {
            double angleDeg = FirstPositiveAngleDeg(data, new[] { "DraftAngle", "Angle", "Angle1" }, 0.0);
            int neutralFaceIndex = FirstFaceIndex(doc, data, new[] { "NeutralPlane", "NeutralFace", "NeutralFaces", "PullDirectionFace" });
            var draftFaceIndices = FaceIndices(doc, data, new[] { "FacesToDraft", "DraftFaces", "Faces", "FaceArray" })
                .Where(i => i != neutralFaceIndex)
                .Distinct()
                .ToList();

            if (angleDeg <= 0.0 || neutralFaceIndex <= 0 || draftFaceIndices.Count == 0)
            {
                note = $"draft incomplete: angle={angleDeg}, neutral={neutralFaceIndex}, faces={draftFaceIndices.Count}";
                return false;
            }

            args["angle_deg"] = angleDeg;
            args["neutral_face_index"] = neutralFaceIndex;
            args["draft_face_indices"] = draftFaceIndices;
            args["flip_dir"] = FirstBool(data, new[] { "FlipDir", "ReverseDirection", "Direction" }, false);
            args["edge_draft"] = FirstBool(data, new[] { "EdgeDraft", "IsEdgeDraft" }, false);
            args["propagation_type"] = FirstPositiveInt(data, new[] { "PropagationType" }, 0);
            args["is_step_draft"] = FirstBool(data, new[] { "StepDraft", "IsStepDraft" }, false);
            args["is_body_draft"] = FirstBool(data, new[] { "BodyDraft", "IsBodyDraft" }, false);
            note = $"draft angle={angleDeg}deg, neutral_face={neutralFaceIndex}, draft_faces={draftFaceIndices.Count}";
            return true;
        }
        finally
        {
            TryInvokeCom(data, "ReleaseSelectionAccess");
        }
    }

    static bool TryExtractRib(Feature feature, string name, string type, out Dictionary<string, object> args, out string note)
    {
        args = new Dictionary<string, object>();
        note = "";
        if (!IsRibFeature(name, type)) return false;

        object data = SafeObj(() => feature.GetDefinition());
        double thicknessMm = data != null
            ? FirstPositiveLengthMm(data, new[] { "Thickness", "RibThickness", "WallThickness" }, 0.0)
            : 0.0;
        if (thicknessMm <= 0.0)
            thicknessMm = FirstFeatureLengthDimensionMm(feature);
        if (thicknessMm <= 0.0)
        {
            note = "rib thickness not available";
            return false;
        }

        args["thickness_mm"] = thicknessMm;
        if (data is IRibFeatureData2 rib2)
        {
            int extrusionDirection = SafeValue(() => rib2.ExtrusionDirection, (int)swRibExtrusionDirection_e.swRibNormalToSketch);
            int ribType = SafeValue(() => rib2.Type, (int)swRibType_e.swRibLinear);
            args["is_two_sided"] = SafeBool(() => rib2.IsTwoSided);
            args["reverse_thickness_dir"] = SafeBool(() => rib2.ReverseThicknessDir);
            args["reverse_material_dir"] = SafeBool(() => rib2.FlipSide);
            args["is_norm_to_sketch"] = extrusionDirection == (int)swRibExtrusionDirection_e.swRibNormalToSketch;
            args["is_drafted"] = SafeBool(() => rib2.EnableDraft);
            args["draft_outward"] = SafeBool(() => rib2.DraftOutward);
            args["draft_angle_deg"] = R(ToAngleDeg(SafeValue(() => rib2.DraftAngle, 0.0)));
            args["is_drafted_from_wall"] = SafeBool(() => rib2.DraftFromWall);
            args["reference_edge_index"] = SafeValue(() => rib2.RefSketchIndex, 0);
            args["rib_type_code"] = ribType;
            args["rib_type"] = ribType == (int)swRibType_e.swRibNatural ? "natural" : "linear";
            args["extrusion_direction_code"] = extrusionDirection;
            args["extrusion_direction"] = extrusionDirection == (int)swRibExtrusionDirection_e.swRibNormalToSketch ? "normal_to_sketch" : "parallel_to_sketch";
        }
        else if (data is IRibFeatureData rib1)
        {
            args["is_two_sided"] = SafeBool(() => rib1.IsTwoSided);
            args["reverse_thickness_dir"] = SafeBool(() => rib1.ReverseThicknessDir);
            args["reverse_material_dir"] = SafeBool(() => rib1.FlipSide);
            args["is_norm_to_sketch"] = true;
            args["is_drafted"] = SafeBool(() => rib1.EnableDraft);
            args["draft_outward"] = SafeBool(() => rib1.DraftOutward);
            args["draft_angle_deg"] = R(ToAngleDeg(SafeValue(() => rib1.DraftAngle, 0.0)));
            args["is_drafted_from_wall"] = false;
            args["reference_edge_index"] = SafeValue(() => rib1.RefSketchIndex, 0);
        }
        else if (data != null)
        {
            args["is_two_sided"] = FirstBool(data, new[] { "IsTwoSided", "BothSides", "TwoSided" }, false);
            args["reverse_thickness_dir"] = FirstBool(data, new[] { "ReverseThicknessDir", "ReverseThicknessDirection" }, false);
            args["reverse_material_dir"] = FirstBool(data, new[] { "FlipSide", "ReverseMaterialDir", "ReverseMaterialDirection", "FlipMaterialSide" }, false);
            int extrusionDirection = FirstPositiveInt(data, new[] { "ExtrusionDirection" }, (int)swRibExtrusionDirection_e.swRibNormalToSketch);
            args["is_norm_to_sketch"] = extrusionDirection == (int)swRibExtrusionDirection_e.swRibNormalToSketch;
            args["is_drafted"] = FirstBool(data, new[] { "EnableDraft", "IsDrafted", "Drafted" }, false);
            args["draft_outward"] = FirstBool(data, new[] { "DraftOutward", "OutwardDraft" }, false);
            args["draft_angle_deg"] = FirstPositiveAngleDeg(data, new[] { "DraftAngle", "DraftAngle1" }, 0.0);
            args["is_drafted_from_wall"] = FirstBool(data, new[] { "DraftFromWall", "IsDraftedFromWall", "DraftedFromWall" }, false);
            args["reference_edge_index"] = FirstPositiveInt(data, new[] { "RefSketchIndex", "ReferenceEdgeIndex" }, 0);
        }
        note = $"rib thickness={thicknessMm}mm";
        return true;
    }

    static bool TryExtractReferenceAxis(ModelDoc2 doc, Feature feature, string name, string type, out Dictionary<string, object> args, out string note)
    {
        args = new Dictionary<string, object>();
        note = "";
        if (!IsReferenceAxisFeature(name, type)) return false;

        var edgeSignatures = new List<Dictionary<string, object>>();
        var datumPlaneNames = new List<string>();
        Dictionary<string, object> faceSignature = new();

        object data = SafeObj(() => feature.GetDefinition());
        bool accessed = false;
        string dataType = data?.GetType().FullName ?? "";
        try
        {
            if (data is IRefAxisFeatureData axisData)
            {
                accessed = SafeBool(() => axisData.AccessSelections(doc, null));
                object selectionTypes = null;
                foreach (object selected in ToObjectArray(SafeObj(() => axisData.GetSelections(out selectionTypes))))
                    AddReferenceAxisSelectionEntity(doc, selected, edgeSignatures, datumPlaneNames, ref faceSignature);
            }
            else if (data != null)
            {
                accessed = SafeBool(() => ((dynamic)data).AccessSelections(doc, null));
            }

            if (doc?.SelectionManager is SelectionMgr selMgr)
            {
                int count = selMgr.GetSelectedObjectCount2(-1);
                for (int i = 1; i <= count; i++)
                    AddReferenceAxisSelectionEntity(doc, selMgr.GetSelectedObject6(i, -1), edgeSignatures, datumPlaneNames, ref faceSignature);
            }

            if (edgeSignatures.Count == 0 && datumPlaneNames.Count == 0 && faceSignature.Count == 0 && data != null)
            {
                foreach (object selected in ToObjectArray(SafeObj(() => InvokeCom(data, "GetSelections"))))
                    AddReferenceAxisSelectionEntity(doc, selected, edgeSignatures, datumPlaneNames, ref faceSignature);
            }
        }
        finally
        {
            if (data != null && accessed)
                TryInvokeCom(data, "ReleaseSelectionAccess");
        }

        args["axis_name"] = name;
        args["selection_accessed"] = accessed;
        if (!string.IsNullOrWhiteSpace(dataType))
            args["axis_feature_data_type"] = dataType;
        if (edgeSignatures.Count > 0)
            args["edge_signatures"] = edgeSignatures;
        if (faceSignature.Count > 0)
            args["face_signature"] = faceSignature;
        if (datumPlaneNames.Count > 0)
            args["datum_plane_names"] = datumPlaneNames.Distinct(StringComparer.OrdinalIgnoreCase).ToList();

        note = $"reference axis: edges={edgeSignatures.Count}, face={(faceSignature.Count > 0 ? 1 : 0)}, planes={datumPlaneNames.Count}, accessed={accessed}, data={dataType}";
        return true;
    }

    static void AddReferenceAxisSelectionEntity(
        ModelDoc2 doc,
        object selected,
        List<Dictionary<string, object>> edgeSignatures,
        List<string> datumPlaneNames,
        ref Dictionary<string, object> faceSignature)
    {
        if (selected == null) return;

        if (selected is Edge edge)
        {
            var signature = EdgeSignature(edge);
            if (signature.Count > 0)
                edgeSignatures.Add(signature);
            return;
        }

        var reference = new SketchReferenceResult();
        if (!TryDescribeReferenceEntity(doc, selected, reference))
            return;

        if (string.Equals(reference.Kind, "datum_plane", StringComparison.OrdinalIgnoreCase))
        {
            string planeName = reference.PlaneName;
            if (!string.IsNullOrWhiteSpace(planeName))
                datumPlaneNames.Add(planeName);
            return;
        }

        if (string.Equals(reference.Kind, "face", StringComparison.OrdinalIgnoreCase) && faceSignature.Count == 0)
            faceSignature = ReferenceFaceSignature(reference);
    }

    static Dictionary<string, object> ReferenceFaceSignature(SketchReferenceResult reference)
    {
        var result = new Dictionary<string, object>();
        if (reference == null) return result;
        if (!string.IsNullOrWhiteSpace(reference.SurfaceType)) result["surface_type"] = reference.SurfaceType;
        if (reference.AreaMm2 > 0) result["area_mm2"] = reference.AreaMm2;
        if (reference.CenterMm != null && reference.CenterMm.Length >= 3)
        {
            result["center_mm"] = reference.CenterMm;
            result["center_x_mm"] = reference.CenterMm[0];
            result["center_y_mm"] = reference.CenterMm[1];
            result["center_z_mm"] = reference.CenterMm[2];
        }
        if (reference.BoxMm != null && reference.BoxMm.Length >= 6) result["box_mm"] = reference.BoxMm;
        if (reference.Normal != null && reference.Normal.Length >= 3)
        {
            result["normal"] = reference.Normal;
            result["normal_x"] = reference.Normal[0];
            result["normal_y"] = reference.Normal[1];
            result["normal_z"] = reference.Normal[2];
        }
        if (reference.FaceIndex > 0) result["fallback_face_index"] = reference.FaceIndex;
        if (!string.IsNullOrWhiteSpace(reference.HostFeatureName)) result["host_feature"] = reference.HostFeatureName;
        return result;
    }

    static bool TryExtractShell(ModelDoc2 doc, Feature feature, string name, string type, out Dictionary<string, object> args, out string note)
    {
        args = new Dictionary<string, object>();
        note = "";
        if (!IsShellFeature(name, type)) return false;

        if (feature.GetDefinition() is not IShellFeatureData data)
        {
            note = "shell definition not available";
            return false;
        }

        try { data.AccessSelections(doc, null); } catch { }
        try
        {
            double thicknessMm = R(ToLengthMm(data.Thickness));
            if (thicknessMm <= 0.0)
            {
                note = "shell thickness not available";
                return false;
            }

            var removedFaces = new List<Face2>();
            try
            {
                object removedObj = data.FacesRemoved ?? data.IGetFacesRemoved(data.FacesRemovedCount);
                foreach (object item in FlattenComObjects(removedObj))
                {
                    if (item is Face2 face)
                        removedFaces.Add(face);
                }
            }
            catch { }

            if (removedFaces.Count == 0)
            {
                note = "shell remove face not available";
                return false;
            }

            double[] point = FaceCenterMm(removedFaces[0]);
            if (point.Length < 3)
            {
                note = "shell remove face point not available";
                return false;
            }

            args["thickness_mm"] = thicknessMm;
            args["x_mm"] = point[0];
            args["y_mm"] = point[1];
            args["z_mm"] = point[2];
            args["outward"] = FirstBool(data, new[] { "Direction", "Outward", "ShellOutward", "ReverseDirection" }, false);
            var removeFaceReference = new SketchReferenceResult();
            if (TryDescribeReferenceEntity(doc, removedFaces[0], removeFaceReference) &&
                string.Equals(removeFaceReference.Kind, "face", StringComparison.OrdinalIgnoreCase))
            {
                args["remove_face_signature"] = BuildFaceSignatureArgs(removeFaceReference);
                if (removeFaceReference.FaceIndex > 0)
                    args["remove_face_index"] = removeFaceReference.FaceIndex;
                if (removeFaceReference.AreaMm2 > 0)
                    args["remove_face_area_mm2"] = removeFaceReference.AreaMm2;
                if (removeFaceReference.CenterMm.Length >= 3)
                    args["remove_face_center_mm"] = removeFaceReference.CenterMm;
                if (removeFaceReference.BoxMm.Length >= 6)
                    args["remove_face_box_mm"] = removeFaceReference.BoxMm;
                if (removeFaceReference.Normal.Length >= 3)
                    args["remove_face_normal"] = removeFaceReference.Normal;
                if (!string.IsNullOrWhiteSpace(removeFaceReference.SurfaceType))
                    args["remove_face_surface_type"] = removeFaceReference.SurfaceType;
                if (!string.IsNullOrWhiteSpace(removeFaceReference.HostFeatureName))
                    args["remove_face_host_feature"] = removeFaceReference.HostFeatureName;
            }
            note = $"shell thickness={thicknessMm}mm, remove_face_point=({point[0]},{point[1]},{point[2]})";
            return true;
        }
        finally
        {
            try { data.ReleaseSelectionAccess(); } catch { }
        }
    }

    static bool TryExtractHoleWizard(ModelDoc2 doc, Feature feature, string name, string type, out Dictionary<string, object> args, out string note)
    {
        args = new Dictionary<string, object>();
        note = "";
        if (!IsHoleWizardFeature(name, type)) return false;

        object data = SafeObj(() => feature.GetDefinition());
        bool accessed = false;
        if (data != null)
            accessed = SafeBool(() => Convert.ToBoolean(InvokeCom(data, "AccessSelections", doc, null)));
        try
        {
        double diameterMm = data != null
            ? FirstPositiveLengthMm(data, new[] { "HoleDiameter", "Diameter", "FastenerDiameter" }, 0.0)
            : 0.0;
        double depthMm = data != null
            ? FirstPositiveLengthMm(data, new[] { "HoleDepth", "Depth", "EndConditionDepth" }, 0.0)
            : 0.0;
        bool throughAll = data != null && FirstBool(data, new[] { "ThroughAll", "Through", "IsThrough" }, false);

        var dims = FeatureDimensionValuesMm(feature).Where(v => v > 0.0).OrderBy(v => v).ToList();
        if (diameterMm <= 0.0 && dims.Count > 0) diameterMm = dims[0];
        if (depthMm <= 0.0 && dims.Count > 1) depthMm = dims[^1];
        if (diameterMm <= 0.0)
        {
            note = "hole diameter not available";
            return false;
        }
        if (!throughAll && depthMm <= 0.0)
            depthMm = diameterMm * 2.0;

        args["diameter_mm"] = diameterMm;
        args["depth_mm"] = depthMm;
        var positionPoints = HoleWizardPositionPointsModelMm(data, feature);
        if (positionPoints.Count > 0)
        {
            args["position_points_model_mm"] = positionPoints;
            args["position_point_source"] = "wizard_hole_feature_data_get_sketch_points";
        }
        else
        {
            args["position_points_model_mm"] = new List<List<double>>();
            args["position_point_missing"] = "IWizardHoleFeatureData2.GetSketchPoints returned no usable sketch points";
        }
        args["through_all"] = throughAll;
        args["reverse_dir"] = data != null && FirstBool(data, new[] { "ReverseDirection", "FlipDirection" }, false);

        AddHoleWizardFeatureDataArgs(doc, data, feature, args);
        AddHoleWizardTargetFaceFromSelectionArgs(doc, args);
        note = $"hole wizard diameter={diameterMm}mm, depth={depthMm}mm, through_all={throughAll}, points={positionPoints.Count}";
        return true;
        }
        finally
        {
            if (accessed)
                TryInvokeCom(data, "ReleaseSelectionAccess");
        }
    }

    static void AddHoleWizardTargetFaceFromSelectionArgs(ModelDoc2 doc, Dictionary<string, object> args)
    {
        try
        {
            if (doc?.SelectionManager is not SelectionMgr selMgr)
                return;

            int count = selMgr.GetSelectedObjectCount2(-1);
            Face2 selectedFace = null;
            for (int i = 1; i <= count; i++)
            {
                object selected = selMgr.GetSelectedObject6(i, -1);
                if (selected is Face2 face)
                {
                    selectedFace = face;
                    break;
                }
            }

            if (selectedFace == null)
                return;

            var reference = new SketchReferenceResult();
            if (!TryDescribeReferenceEntity(doc, selectedFace, reference) ||
                !string.Equals(reference.Kind, "face", StringComparison.OrdinalIgnoreCase))
                return;

            ApplyHoleWizardTargetFaceReferenceArgs(args, reference, "hole_wizard_selection", "");
        }
        catch { }
    }

    static void ApplyHoleWizardTargetFaceReferenceArgs(Dictionary<string, object> args, SketchReferenceResult reference, string source, string note)
    {
        if (args == null || reference == null)
            return;

        args["target_face_source"] = source;
        args["target_face_role"] = "hole_start_face";
        args["placement_reference_kind"] = reference.Kind;
        args["placement_reference"] = reference.Description;
        if (!string.IsNullOrWhiteSpace(reference.HostFeatureName))
            args["target_host_feature"] = reference.HostFeatureName;
        if (reference.HostFeatureStep > 0)
            args["target_host_step"] = reference.HostFeatureStep;
        if (reference.FaceIndex > 0)
        {
            args["target_face_indices"] = new List<int> { reference.FaceIndex };
            args["target_face_index"] = reference.FaceIndex;
        }
        if (!string.IsNullOrWhiteSpace(reference.SurfaceType))
            args["target_face_surface_type"] = reference.SurfaceType;
        if (reference.AreaMm2 > 0)
            args["target_face_area_mm2"] = reference.AreaMm2;
        if (reference.CenterMm.Length >= 3)
        {
            args["target_face_center_mm"] = reference.CenterMm;
            args["target_face_x_mm"] = reference.CenterMm[0];
            args["target_face_y_mm"] = reference.CenterMm[1];
            args["target_face_z_mm"] = reference.CenterMm[2];
        }
        if (reference.BoxMm.Length >= 6)
            args["target_face_box_mm"] = reference.BoxMm;
        if (reference.Normal.Length >= 3)
            args["target_face_normal"] = reference.Normal;
        if (!string.IsNullOrWhiteSpace(reference.PersistReferenceHex))
            args["target_face_persist_ref_hex"] = reference.PersistReferenceHex;

        var signature = BuildFaceSignatureArgs(reference);
        if (signature.Count > 0)
            args["target_face_signature"] = signature;
        if (!string.IsNullOrWhiteSpace(note))
            args["target_face_selection_note"] = note;
    }

    static bool TryReadArgDouble(Dictionary<string, object> args, string key, out double value)
    {
        value = 0.0;
        if (args == null || !args.TryGetValue(key, out var obj) || obj == null) return false;
        try
        {
            value = Convert.ToDouble(obj, System.Globalization.CultureInfo.InvariantCulture);
            return true;
        }
        catch
        {
            return false;
        }
    }

    static bool TryReadArgDoubleList(Dictionary<string, object> args, string key, out List<double> values)
    {
        values = new List<double>();
        if (args == null || !args.TryGetValue(key, out var obj) || obj == null) return false;
        if (obj is IEnumerable<double> doubles)
        {
            values = doubles.ToList();
            return values.Count > 0;
        }
        if (obj is IEnumerable<object> objects)
        {
            foreach (var item in objects)
            {
                try { values.Add(Convert.ToDouble(item, System.Globalization.CultureInfo.InvariantCulture)); }
                catch { return false; }
            }
            return values.Count > 0;
        }
        return false;
    }

    static bool TryReadPointListArg(Dictionary<string, object> args, string key, out List<List<double>> points)
    {
        points = new List<List<double>>();
        if (args == null || !args.TryGetValue(key, out var obj) || obj == null) return false;
        if (obj is IEnumerable<List<double>> listPoints)
        {
            points = listPoints.Select(p => p.ToList()).ToList();
            return points.Count > 0;
        }
        if (obj is IEnumerable<object> objectPoints)
        {
            foreach (var pointObj in objectPoints)
            {
                if (pointObj is IEnumerable<double> doubles)
                    points.Add(doubles.ToList());
                else if (pointObj is IEnumerable<object> objects)
                    points.Add(objects.Select(v => Convert.ToDouble(v, System.Globalization.CultureInfo.InvariantCulture)).ToList());
            }
            return points.Count > 0;
        }
        return false;
    }

    static bool HasHoleWizardTargetFace(Dictionary<string, object> args)
    {
        return args != null &&
            args.TryGetValue("target_face_role", out var roleObj) &&
            string.Equals(roleObj?.ToString(), "hole_start_face", StringComparison.OrdinalIgnoreCase);
    }

    static bool HoleWizardPointsMatchSketch(Feature sketchFeature, Dictionary<string, object> args)
    {
        if (sketchFeature == null || args == null)
            return false;
        if (!TryReadPointListArg(args, "position_points_model_mm", out var holePoints) || holePoints.Count == 0)
            return false;

        var sketchPoints = SketchFreePointModelMmFromFeature(sketchFeature);
        if (sketchPoints.Count == 0)
            return false;

        foreach (var holePoint in holePoints)
        {
            if (holePoint == null || holePoint.Count < 3)
                return false;
            bool matched = sketchPoints.Any(sketchPoint =>
                sketchPoint.Count >= 3 &&
                Distance(holePoint.Take(3).ToArray(), sketchPoint.Take(3).ToArray()) <= 0.25);
            if (!matched)
                return false;
        }

        return true;
    }

    static void AddHoleWizardTargetFaceFromPlacementSketch(ModelDoc2 doc, Feature sketchFeature, Dictionary<string, object> args)
    {
        try
        {
            if (sketchFeature == null || sketchFeature.GetSpecificFeature2() is not Sketch sketch)
                return;

            var debug = new SketchReferenceDebugInfo();
            var sketchReference = ResolveSketchPlane(doc, sketchFeature, sketch, debug);
            NormalizeGenericDatumPlaneReference(sketch, sketchReference);
            if (!string.Equals(sketchReference.Kind, "face", StringComparison.OrdinalIgnoreCase))
            {
                args["target_face_missing"] = "matched placement sketch did not resolve to a model face";
                args["target_face_missing_reference_kind"] = sketchReference.Kind;
                args["target_face_missing_reference"] = sketchReference.Description;
                return;
            }

            string sketchName = Safe(() => sketchFeature.Name, "");
            ApplyHoleWizardTargetFaceReferenceArgs(
                args,
                sketchReference,
                "placement_sketch_matched_by_wizard_points",
                string.IsNullOrWhiteSpace(sketchName) ? "" : $"placement_sketch={sketchName}");
        }
        catch { }
    }

    static List<List<double>> SketchFreePointModelMmFromFeature(Feature sketchFeature)
    {
        var points = new List<List<double>>();
        if (sketchFeature == null || sketchFeature.GetSpecificFeature2() is not Sketch sketch)
            return points;

        var canonicalPoints = BuildSketchPointCanonicalMap(sketch);
        var segmentPointIds = new HashSet<IntPtr>();
        var segmentPointKeys = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
        foreach (object o in ToObjectArray(SafeObj(() => sketch.GetSketchSegments())))
        {
            if (o is SketchSegment segment)
                RegisterSegmentPoints(segment, canonicalPoints, segmentPointIds, segmentPointKeys);
        }

        var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
        foreach (double[] pointLocal in UserSketchPointsLocalMm(sketch))
        {
            if (pointLocal.Length < 3) continue;
            double x = pointLocal[0], y = pointLocal[1], z = pointLocal[2];
            if (segmentPointKeys.Contains(SketchPointCoordKey(x, y, z))) continue;
            if (!TrySketchLocalMmToModelMm(sketch, x, y, z, out var modelPoint)) continue;
            string key = string.Join(",", modelPoint.Select(v => R(v).ToString(System.Globalization.CultureInfo.InvariantCulture)));
            if (seen.Add(key))
                points.Add(modelPoint);
        }

        return points;
    }

    static Feature FindFeatureOwningSketch(Feature feature, Sketch sketch)
    {
        if (feature == null || sketch == null) return null;
        var targetId = Id(sketch);
        Feature sub = feature.GetFirstSubFeature() as Feature;
        for (int i = 0; sub != null && i < 300; i++)
        {
            try
            {
                if (sub.GetSpecificFeature2() is Sketch candidate && Id(candidate) == targetId)
                    return sub;
            }
            catch { }
            sub = sub.GetNextSubFeature() as Feature;
        }
        return null;
    }

    static void AddHoleWizardFeatureDataArgs(ModelDoc2 doc, object data, Feature feature, Dictionary<string, object> args)
    {
        if (data == null) return;

        args["hole_wizard_api"] = "HoleWizard5";
        if (TryReadIntMember(data, "Type", out int holeType))
        {
            args["hole_type"] = holeType;
            args["hole_type_name"] = EnumName(typeof(swWzdHoleTypes_e), holeType);
        }

        int genericHoleType = InferWizardGenericHoleType(holeType);
        if (genericHoleType != int.MinValue)
        {
            args["generic_hole_type"] = genericHoleType;
            args["generic_hole_type_name"] = EnumName(typeof(swWzdGeneralHoleTypes_e), genericHoleType);
            args["generic_hole_type_source"] = "derived_from_iwizardholefeaturedata2_type";
        }

        AddIntArgAllowZero(args, "standard_index", data, new[] { "Standard2", "Standard", "StandardIndex", "HoleStandard" }, typeof(swWzdHoleStandards_e), "standard_name");
        AddIntArgAllowZero(args, "fastener_type_index", data, new[] { "FastenerType2", "FastenerType", "TypeIndex" }, typeof(swWzdHoleStandardFastenerTypes_e), "fastener_type_name");
        AddStringArg(args, "size", data, new[] { "FastenerSize", "Size", "SSize", "HoleSize" });
        AddStringArg(args, "thread_class", data, new[] { "ThreadClass", "Class" });
        AddStringArg(args, "thread_callout", data, new[] { "ThreadCallout", "Callout" });
        AddIntArgAllowZero(args, "end_condition_code", data, new[] { "EndCondition" }, typeof(swEndConditions_e), "end_condition_name");
        AddIntArgAllowZero(args, "thread_end_condition_code", data, new[] { "ThreadEndCondition", "ThreadEndType" }, null, "");
        AddIntArgAllowZero(args, "cosmetic_thread_type", data, new[] { "CosmeticThreadType", "ThreadType" }, null, "");
        AddIntArgAllowZero(args, "tap_type", data, new[] { "TapType" }, null, "");
        AddIntArgAllowZero(args, "hole_fit", data, new[] { "HoleFit" }, null, "");
        AddIntArgAllowZero(args, "head_clearance_type", data, new[] { "HeadClearanceType" }, null, "");
        AddLengthArg(args, "hole_diameter_mm", data, new[] { "HoleDiameter", "Diameter" });
        AddLengthArg(args, "hole_depth_mm", data, new[] { "HoleDepth", "Depth" });
        AddLengthArg(args, "length_mm", data, new[] { "Length" });
        AddLengthArg(args, "tap_drill_diameter_mm", data, new[] { "TapDrillDiameter" });
        AddLengthArg(args, "tap_drill_depth_mm", data, new[] { "TapDrillDepth" });
        AddLengthArg(args, "thread_diameter_mm", data, new[] { "ThreadDiameter", "TapDrillDiameter" });
        AddLengthArg(args, "thread_depth_mm", data, new[] { "ThreadDepth", "TapDepth" });
        AddLengthArg(args, "major_diameter_mm", data, new[] { "MajorDiameter" });
        AddLengthArg(args, "minor_diameter_mm", data, new[] { "MinorDiameter" });
        AddLengthArg(args, "thru_hole_diameter_mm", data, new[] { "ThruHoleDiameter" });
        AddLengthArg(args, "thru_hole_depth_mm", data, new[] { "ThruHoleDepth" });
        AddLengthArg(args, "thru_tap_drill_diameter_mm", data, new[] { "ThruTapDrillDiameter" });
        AddLengthArg(args, "thru_tap_drill_depth_mm", data, new[] { "ThruTapDrillDepth" });
        AddLengthArg(args, "counter_bore_diameter_mm", data, new[] { "CounterBoreDiameter" });
        AddLengthArg(args, "counter_bore_depth_mm", data, new[] { "CounterBoreDepth" });
        AddLengthArg(args, "counter_sink_diameter_mm", data, new[] { "CounterSinkDiameter" });
        AddLengthArg(args, "near_counter_sink_diameter_mm", data, new[] { "NearCounterSinkDiameter" });
        AddLengthArg(args, "mid_counter_sink_diameter_mm", data, new[] { "MidCounterSinkDiameter" });
        AddLengthArg(args, "far_counter_sink_diameter_mm", data, new[] { "FarCounterSinkDiameter" });
        AddLengthArg(args, "counter_drill_diameter_mm", data, new[] { "CounterDrillDiameter" });
        AddLengthArg(args, "counter_drill_depth_mm", data, new[] { "CounterDrillDepth" });
        AddLengthArg(args, "head_clearance_mm", data, new[] { "HeadClearance" });
        AddLengthArg(args, "offset_distance_mm", data, new[] { "OffsetDistance" });
        AddLengthArg(args, "drill_angle_deg", data, new[] { "DrillAngle", "TipAngle" }, isAngle: true);
        AddLengthArg(args, "thread_angle_deg", data, new[] { "ThreadAngle" }, isAngle: true);
        AddLengthArg(args, "counter_sink_angle_deg", data, new[] { "CounterSinkAngle" }, isAngle: true);
        AddLengthArg(args, "near_counter_sink_angle_deg", data, new[] { "NearCounterSinkAngle" }, isAngle: true);
        AddLengthArg(args, "mid_counter_sink_angle_deg", data, new[] { "MidCounterSinkAngle" }, isAngle: true);
        AddLengthArg(args, "far_counter_sink_angle_deg", data, new[] { "FarCounterSinkAngle" }, isAngle: true);
        AddLengthArg(args, "counter_drill_angle_deg", data, new[] { "CounterDrillAngle" }, isAngle: true);
        args["feature_scope"] = FirstBool(data, new[] { "FeatureScope" }, true);
        args["auto_select"] = FirstBool(data, new[] { "AutoSelect" }, true);
        args["assembly_feature_scope"] = FirstBool(data, new[] { "AssemblyFeatureScope" }, false);
        args["auto_select_components"] = FirstBool(data, new[] { "AutoSelectComponents" }, false);
        args["propagate_feature_to_parts"] = FirstBool(data, new[] { "PropagateFeatureToParts" }, false);

        var targetFaceIndices = FaceIndices(doc, data, new[] { "Face", "Faces", "FaceArray", "TargetFace", "TargetFaces", "PlacementFace", "PlacementFaces" }).Distinct().ToList();
        if (targetFaceIndices.Count > 0)
            args["target_face_indices"] = targetFaceIndices;

        Face2 face = FirstFace(data, new[] { "Face", "Faces", "FaceArray", "TargetFace", "TargetFaces", "PlacementFace", "PlacementFaces" });
        if (face != null)
        {
            var reference = new SketchReferenceResult();
            if (TryDescribeReferenceEntity(doc, face, reference) &&
                string.Equals(reference.Kind, "face", StringComparison.OrdinalIgnoreCase))
                ApplyHoleWizardTargetFaceReferenceArgs(args, reference, "wizard_hole_feature_data_face", "");

            args["target_face_area_mm2"] = R(SafeDouble(() => face.GetArea()) * 1_000_000.0);
            var center = FaceCenterMm(face);
            if (center.Length >= 3)
                args["target_face_center_mm"] = center;
            var box = TryGetFaceBoxMm(face);
            if (box.Length >= 6)
                args["target_face_box_mm"] = box;
            var normal = FaceNormal(face);
            if (normal.Length >= 3)
                args["target_face_normal"] = normal;
        }
    }

    static void AddIntArg(Dictionary<string, object> args, string key, object data, IEnumerable<string> names)
    {
        int value = FirstPositiveInt(data, names, int.MinValue);
        if (value != int.MinValue) args[key] = value;
    }

    static void AddIntArgAllowZero(Dictionary<string, object> args, string key, object data, IEnumerable<string> names, Type enumType = null, string enumKey = "")
    {
        int value = FirstReadableInt(data, names, int.MinValue);
        if (value == int.MinValue) return;
        args[key] = value;
        if (enumType != null && !string.IsNullOrWhiteSpace(enumKey))
            args[enumKey] = EnumName(enumType, value);
    }

    static int FirstReadableInt(object data, IEnumerable<string> names, int fallback)
    {
        foreach (string name in names)
        {
            if (TryReadIntMember(data, name, out int value))
                return value;
        }
        return fallback;
    }

    static bool TryReadIntMember(object data, string name, out int value)
    {
        value = 0;
        object raw = SafeObj(() => GetComProperty(data, name)) ?? InvokeCom(data, name);
        if (raw == null) return false;
        try
        {
            value = Convert.ToInt32(raw);
            return true;
        }
        catch
        {
            return int.TryParse(raw.ToString(), out value);
        }
    }

    static int InferWizardGenericHoleType(int holeType)
    {
        if (holeType >= (int)swWzdHoleTypes_e.swPipeTapBlind && holeType <= (int)swWzdHoleTypes_e.swPipeTapThruCounterSinkTopBottom)
            return (int)swWzdGeneralHoleTypes_e.swWzdPipeTap;
        if ((holeType >= (int)swWzdHoleTypes_e.swTapBlind && holeType <= (int)swWzdHoleTypes_e.swTapThruCounterSinkTopBottom)
            || (holeType >= (int)swWzdHoleTypes_e.swTapBlindCosmeticThread && holeType <= (int)swWzdHoleTypes_e.swTapBlindRemoveThread))
            return (int)swWzdGeneralHoleTypes_e.swWzdTap;
        if (holeType >= (int)swWzdHoleTypes_e.swCounterBoreSlotBlind)
            return (int)swWzdGeneralHoleTypes_e.swWzdHoleSlot;
        if (holeType >= (int)swWzdHoleTypes_e.swCounterBoreBlind && holeType <= (int)swWzdHoleTypes_e.swCounterBoreThruCounterSinkTopMiddleBottom)
            return (int)swWzdGeneralHoleTypes_e.swWzdCounterBore;
        if (holeType == (int)swWzdHoleTypes_e.swCounterSunk
            || holeType == (int)swWzdHoleTypes_e.swCounterSinkBlind
            || holeType == (int)swWzdHoleTypes_e.swCounterSinkThru
            || holeType == (int)swWzdHoleTypes_e.swCounterSinkBlindWithoutHeadClearance
            || holeType == (int)swWzdHoleTypes_e.swCounterSinkThruWithoutHeadClearance
            || holeType == (int)swWzdHoleTypes_e.swCounterSinkThruCounterSinkBottomWithoutHeadClearance)
            return (int)swWzdGeneralHoleTypes_e.swWzdCounterSink;
        if (holeType != 0)
            return (int)swWzdGeneralHoleTypes_e.swWzdHole;
        return int.MinValue;
    }

    static string EnumName(Type enumType, int value)
    {
        try
        {
            string name = Enum.GetName(enumType, value);
            return string.IsNullOrWhiteSpace(name) ? value.ToString(System.Globalization.CultureInfo.InvariantCulture) : name;
        }
        catch
        {
            return value.ToString(System.Globalization.CultureInfo.InvariantCulture);
        }
    }

    static void AddStringArg(Dictionary<string, object> args, string key, object data, IEnumerable<string> names)
    {
        foreach (string name in names)
        {
            object value = SafeObj(() => GetComProperty(data, name)) ?? InvokeCom(data, name);
            string text = value?.ToString() ?? "";
            if (!string.IsNullOrWhiteSpace(text))
            {
                args[key] = text;
                return;
            }
        }
    }

    static void AddLengthArg(Dictionary<string, object> args, string key, object data, IEnumerable<string> names, bool isAngle = false)
    {
        foreach (string name in names)
        {
            double value = ReadDoubleMember(data, name);
            if (Math.Abs(value) <= 1e-9) continue;
            args[key] = isAngle ? R(ToAngleDeg(value)) : R(ToLengthMm(value));
            return;
        }
    }

    static List<List<double>> HoleWizardPositionPointsModelMm(object data, Feature feature)
    {
        var points = new List<List<double>>();
        if (data is not IWizardHoleFeatureData2 wizardData)
            return points;

        Sketch sketch = FirstSketchFromFeature(feature);
        var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);

        void AddPoint(SketchPoint point)
        {
            if (point == null) return;
            double x = point.X * 1000.0;
            double y = point.Y * 1000.0;
            double z = point.Z * 1000.0;

            List<double> modelPoint;
            if (sketch != null && TrySketchLocalMmToModelMm(sketch, x, y, z, out var transformedPoint))
                modelPoint = transformedPoint;
            else
                modelPoint = new List<double> { R(x), R(y), R(z) };

            string key = string.Join(",", modelPoint.Select(v => R(v).ToString(System.Globalization.CultureInfo.InvariantCulture)));
            if (seen.Add(key))
                points.Add(modelPoint);
        }

        foreach (object pointObj in ToObjectArray(SafeObj(() => wizardData.GetSketchPoints())))
        {
            if (pointObj is SketchPoint point)
                AddPoint(point);
        }

        if (points.Count > 0)
            return points;

        int count = SafeInt(() => wizardData.GetSketchPointCount(), 0);
        if (count <= 0)
            return points;

        object rawPoints = SafeObj(() => InvokeCom(data, "IGetSketchPoints", count)) ??
            SafeObj(() => wizardData.IGetSketchPoints(count));
        foreach (object pointObj in ToObjectArray(rawPoints))
        {
            if (pointObj is SketchPoint point)
                AddPoint(point);
        }

        return points;
    }

    static List<List<double>> DominantCoplanarPoints(List<List<double>> candidates)
    {
        if (candidates == null || candidates.Count == 0) return new List<List<double>>();
        var best = new List<List<double>>();
        const double bucketMm = 0.01;
        for (int axis = 0; axis < 3; axis++)
        {
            var groups = new Dictionary<long, List<List<double>>>();
            foreach (var point in candidates)
            {
                if (point == null || point.Count < 3) continue;
                long bucket = (long)Math.Round(point[axis] / bucketMm);
                if (!groups.TryGetValue(bucket, out var group))
                {
                    group = new List<List<double>>();
                    groups[bucket] = group;
                }
                group.Add(point);
            }

            foreach (var group in groups.Values)
            {
                if (group.Count > best.Count)
                    best = group;
            }
        }

        var result = new List<List<double>>();
        var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
        foreach (var point in best)
        {
            string key = string.Join(",", point.Select(v => R(v).ToString(System.Globalization.CultureInfo.InvariantCulture)));
            if (seen.Add(key))
                result.Add(point);
        }
        return result;
    }

    static bool TryExtractMirror(ModelDoc2 doc, Feature feature, string name, string type, out Dictionary<string, object> args, out string note)
    {
        args = new Dictionary<string, object>();
        note = "";
        if (!IsMirrorFeature(name, type)) return false;

        object data = SafeObj(() => feature.GetDefinition());
        bool accessed = false;
        if (data != null)
        {
            IMirrorPatternFeatureData typedData = data as IMirrorPatternFeatureData;
            if (typedData != null)
                accessed = SafeBool(() => typedData.AccessSelections(doc, null));
            else
                accessed = SafeBool(() => Convert.ToBoolean(InvokeCom(data, "AccessSelections", doc, null)));

            var seedFeatures = typedData != null
                ? FeatureNamesFromObject(typedData.PatternFeatureArray)
                : new List<string>();
            if (seedFeatures.Count == 0)
                seedFeatures = FeatureNames(data, new[] { "PatternFeatureArray", "FeatureArray", "SeedFeatures", "FeaturesToMirror" });
            if (seedFeatures.Count > 0)
            {
                args["feature_name"] = seedFeatures[0];
                args["feature_names"] = seedFeatures;
                args["feature_count"] = seedFeatures.Count;
            }

            string planeName = typedData != null
                ? ReferenceName(typedData.Plane)
                : "";
            if (string.IsNullOrWhiteSpace(planeName))
                planeName = FirstReferenceName(data, new[] { "Plane", "MirrorFaceOrPlane", "MirrorPlane", "MirrorReference" });
            if (!string.IsNullOrWhiteSpace(planeName))
                args["mirror_plane_name"] = planeName;

            args["geometry_pattern"] = FirstBool(data, new[] { "GeometryPattern", "GeometryPatternOption" }, true);
            args["pattern_seed_only"] = FirstBool(data, new[] { "PatternSeedOnly" }, false);
            args["propagate_visual_property"] = FirstBool(data, new[] { "PropagateVisualProperty" }, true);

            if (accessed)
            {
                try
                {
                    if (typedData != null) typedData.ReleaseSelectionAccess();
                    else InvokeCom(data, "ReleaseSelectionAccess");
                }
                catch { }
            }
        }

        if (!args.ContainsKey("mirror_plane_name"))
        {
            args["mirror_plane_missing"] = true;
            note = $"mirror features={args.GetValueOrDefault("feature_count", 0)}, first={args.GetValueOrDefault("feature_name", "")}, plane=unresolved";
        }
        else
        {
            note = $"mirror features={args.GetValueOrDefault("feature_count", 0)}, first={args.GetValueOrDefault("feature_name", "")}, plane={args["mirror_plane_name"]}";
        }
        return true;
    }

    static bool TryExtractWrap(ModelDoc2 doc, Feature feature, string name, string type, out Dictionary<string, object> args, out string note)
    {
        args = new Dictionary<string, object>();
        note = "";
        if (!IsWrapFeature(name, type)) return false;

        object data = SafeObj(() => feature.GetDefinition());
        if (data == null)
        {
            note = "wrap definition not available";
            return false;
        }

        try { TryInvokeCom(data, "AccessSelections", doc, null); } catch { }
        try
        {
            double thicknessMm = FirstPositiveLengthMm(data, new[] { "Thickness", "Depth", "WrapThickness" }, 0.0);
            var faceIndices = FaceIndices(doc, data, new[] { "Faces", "TargetFaces", "WrapFaces", "FaceArray" }).Distinct().ToList();
            string sketchName = FirstReferenceName(data, new[] { "Sketch", "ProfileSketch", "SourceSketch" });

            args["thickness_mm"] = thicknessMm;
            if (faceIndices.Count > 0) args["target_face_indices"] = faceIndices;
            if (!string.IsNullOrWhiteSpace(sketchName)) args["sketch_name"] = sketchName;
            args["wrap_type"] = FirstPositiveInt(data, new[] { "WrapType", "Type" }, 0);
            args["reverse_direction"] = FirstBool(data, new[] { "ReverseDirection", "FlipDirection" }, false);
            args["method"] = FirstPositiveInt(data, new[] { "Method" }, 0);
            args["spline_surface_quality"] = FirstPositiveInt(data, new[] { "SplineSurfaceQuality", "SurfaceQuality" }, 1);

            note = $"wrap thickness={thicknessMm}mm, target_faces={faceIndices.Count}, sketch={sketchName}";
            return true;
        }
        finally
        {
            TryInvokeCom(data, "ReleaseSelectionAccess");
        }
    }

    static bool TryExtractHelix(Feature feature, string name, string type, out Dictionary<string, object> args, out string note)
    {
        args = new Dictionary<string, object>();
        note = "";
        if (!IsHelixFeature(name, type)) return false;

        object data = SafeObj(() => feature.GetDefinition());
        if (data is not IHelixFeatureData helix)
        {
            note = "helix definition not available";
            return false;
        }

        args["feature_name"] = name;
        args["defined_by"] = helix.DefinedBy;
        args["height_mm"] = R(helix.Height * 1000.0);
        args["pitch_mm"] = R(helix.Pitch * 1000.0);
        args["revolution"] = R(helix.Revolution);
        args["starting_angle_deg"] = R(helix.StartingAngle * 180.0 / Math.PI);
        args["reverse_direction"] = helix.ReverseDirection;
        args["clockwise"] = helix.Clockwise;
        args["taper"] = helix.Taper;
        args["taper_angle_deg"] = R(helix.TaperAngle * 180.0 / Math.PI);
        args["taper_outward"] = helix.TaperOutward;
        args["variable_pitch"] = helix.VariablePitch;

        note = $"helix sketch=last, defined_by={helix.DefinedBy}, height={args["height_mm"]}mm, pitch={args["pitch_mm"]}mm, revolution={args["revolution"]}";
        return true;
    }

    static bool TryExtractSweep(ModelDoc2 doc, Feature feature, string name, string type, out string skill, out Dictionary<string, object> args, out string note)
    {
        skill = "";
        args = new Dictionary<string, object>();
        note = "";
        if (!IsSweepFeature(name, type)) return false;

        bool isCut = IsCutLike(name, type);
        object data = SafeObj(() => feature.GetDefinition());
        if (data is ISweepFeatureData sweepData)
        {
            bool accessed = SafeBool(() => sweepData.AccessSelections(doc, null));
            try
            {
                object profileObj = SafeObj(() => sweepData.Profile);
                string profileName = CanonicalReplayReferenceName(profileObj);
                string profileNameSource = ReferenceName(profileObj);
                string subSketchProfileName = FirstCanonicalSubSketchName(feature);
                if (!string.IsNullOrWhiteSpace(subSketchProfileName) &&
                    !string.Equals(profileName, subSketchProfileName, StringComparison.OrdinalIgnoreCase))
                    profileName = subSketchProfileName;
                string pathName = ReferenceName(SafeObj(() => sweepData.Path));
                bool circularProfile = SafeBool(() => sweepData.CircularProfile);
                int direction = SafeInt(() => sweepData.Direction, 0);

                if (!circularProfile && isCut && !string.IsNullOrWhiteSpace(profileName) && !string.IsNullOrWhiteSpace(pathName))
                {
                    skill = "swept_cut_profile_path";
                    args["profile_name"] = profileName;
                    args["path_name"] = pathName;
                    args["direction"] = direction;
                    args["circular_profile"] = false;
                    if (!string.IsNullOrWhiteSpace(profileNameSource) &&
                        !string.Equals(profileName, profileNameSource, StringComparison.OrdinalIgnoreCase))
                        args["source_profile_name"] = profileNameSource;
                    note = $"sweep cut profile={profileName}, path={pathName}, direction={direction}";
                    return true;
                }

                double circularDiameterMm = R(sweepData.CircularProfileDiameter * 1000.0);
                if (circularProfile && circularDiameterMm > 0.0)
                {
                    skill = isCut ? "swept_cut_circular_profile_mm" : "swept_boss_circular_profile_mm";
                    args["diameter_mm"] = circularDiameterMm;
                    if (!string.IsNullOrWhiteSpace(pathName)) args["path_sketch_name"] = pathName;
                    args["direction"] = direction;
                    if (!isCut) args["merge"] = true;
                    note = $"sweep circular diameter={circularDiameterMm}mm, path={pathName}, direction={direction}";
                    return true;
                }

                note = $"sweep profile/path unresolved: circular={circularProfile}, profile={profileName}, path={pathName}";
                return false;
            }
            finally
            {
                if (accessed) SafeAction(() => sweepData.ReleaseSelectionAccess());
            }
        }

        double diameterMm = FirstFeatureLengthDimensionMm(feature);
        if (diameterMm <= 0.0)
        {
            note = "sweep definition/profile diameter not available";
            return false;
        }
        var sketchNames = SketchFeatureNames(feature);
        string pathSketchName = sketchNames.Count > 0 ? sketchNames[^1] : "";
        skill = isCut ? "swept_cut_circular_profile_mm" : "swept_boss_circular_profile_mm";
        args["diameter_mm"] = diameterMm;
        if (!string.IsNullOrWhiteSpace(pathSketchName)) args["path_sketch_name"] = pathSketchName;
        args["direction"] = 0;
        if (!isCut) args["merge"] = true;
        note = $"sweep circular diameter={diameterMm}mm, path_sketch={pathSketchName}";
        return true;
    }

    static bool TryExtractLoftOrBoundary(Feature feature, string name, string type, out string skill, out Dictionary<string, object> args, out string note)
    {
        skill = "";
        args = new Dictionary<string, object>();
        note = "";
        bool isBoundary = IsBoundaryFeature(name, type);
        bool isLoft = IsLoftFeature(name, type);
        if (!isBoundary && !isLoft) return false;

        bool isCut = IsCutLike(name, type);
        var sketchNames = SketchFeatureNames(feature).Distinct(StringComparer.OrdinalIgnoreCase).ToList();
        if (sketchNames.Count >= 2)
            args["profile_sketch_names"] = sketchNames;
        args["closed"] = false;
        if (!isCut) args["merge"] = true;

        if (isBoundary)
            skill = isCut ? "boundary_cut_from_profiles" : "boundary_boss_from_profiles";
        else
            skill = isCut ? "loft_cut_from_profiles" : "loft_boss_from_profiles";

        note = $"{(isBoundary ? "boundary" : "loft")} profiles={sketchNames.Count}";
        return true;
    }

    static int FirstPositiveInt(object data, IEnumerable<string> names, int fallback)
    {
        foreach (string name in names)
        {
            double value = ReadDoubleMember(data, name);
            if (Math.Abs(value) > 1e-9)
                return RInt(value);
        }
        return fallback;
    }

    static double FirstPositiveLengthMm(object data, IEnumerable<string> names, double fallback)
    {
        foreach (string name in names)
        {
            double value = ReadDoubleMember(data, name);
            if (Math.Abs(value) > 1e-9)
                return R(ToLengthMm(value));
        }
        return fallback;
    }

    static double FirstPositiveAngleDeg(object data, IEnumerable<string> names, double fallback)
    {
        foreach (string name in names)
        {
            double value = ReadDoubleMember(data, name);
            if (Math.Abs(value) > 1e-9)
                return R(ToAngleDeg(value));
        }
        return fallback;
    }

    static bool FirstBool(object data, IEnumerable<string> names, bool fallback)
    {
        foreach (string name in names)
        {
            if (TryReadBoolMember(data, name, out bool value))
                return value;
        }
        return fallback;
    }

    static double ToLengthMm(double value)
    {
        // SolidWorks feature data lengths are normally meters. Some dimension fallbacks
        // already arrive in mm, so avoid multiplying obvious mm-scale values.
        return Math.Abs(value) < 10.0 ? value * 1000.0 : value;
    }

    static double ToAngleDeg(double value)
    {
        return Math.Abs(value) <= Math.PI * 2.0 + 1e-6 ? value * 180.0 / Math.PI : value;
    }

    static bool TryReadEdgePointMm(object data, IEnumerable<string> names, out double[] point)
    {
        point = Array.Empty<double>();
        foreach (string name in names)
        {
            object value = SafeObj(() => GetComProperty(data, name));
            foreach (object item in FlattenComObjects(value))
            {
                if (TryAsEdge(item, out Edge edge))
                {
                    point = EdgePointMm(edge);
                    if (point.Length >= 3)
                        return true;
                }
            }
        }
        return false;
    }

    static double[] EdgePointMm(Edge edge)
    {
        var signature = EdgeSignature(edge);
        if (signature.TryGetValue("center_mm", out object centerObj))
        {
            double[] center = ToDoubleArray(centerObj);
            if (center.Length >= 3) return new[] { R(center[0]), R(center[1]), R(center[2]) };
        }
        if (signature.TryGetValue("start_mm", out object startObj))
        {
            double[] start = ToDoubleArray(startObj);
            if (start.Length >= 3) return new[] { R(start[0]), R(start[1]), R(start[2]) };
        }
        return Array.Empty<double>();
    }

    static string FirstFeatureName(object data, IEnumerable<string> names)
    {
        foreach (string name in names)
        {
            object value = SafeObj(() => GetComProperty(data, name));
            string featureName = FirstFeatureNameFromObject(value);
            if (!string.IsNullOrWhiteSpace(featureName))
                return featureName;
        }
        return "";
    }

    static List<string> FeatureNames(object data, IEnumerable<string> names)
    {
        var result = new List<string>();
        var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
        foreach (string name in names)
        {
            object value = SafeObj(() => GetComProperty(data, name));
            foreach (string featureName in FeatureNamesFromObject(value))
            {
                if (seen.Add(featureName))
                    result.Add(featureName);
            }
            if (result.Count > 0)
                break;
        }
        return result;
    }

    static string FirstFeatureNameFromObject(object value)
    {
        foreach (string featureName in FeatureNamesFromObject(value))
            return featureName;
        return "";
    }

    static List<string> FeatureNamesFromObject(object value)
    {
        var result = new List<string>();
        var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
        foreach (object item in FlattenComObjects(value))
        {
            if (item is Feature feature)
            {
                string featureName = Safe(() => feature.Name);
                if (!string.IsNullOrWhiteSpace(featureName) && seen.Add(featureName))
                    result.Add(featureName);
            }
        }
        return result;
    }

    static string FirstReferenceName(object data, IEnumerable<string> names)
    {
        foreach (string name in names)
        {
            object value = SafeObj(() => GetComProperty(data, name));
            foreach (object item in FlattenComObjects(value))
            {
                string referenceName = ReferenceName(item);
                if (!string.IsNullOrWhiteSpace(referenceName))
                    return referenceName;
            }
        }
        return "";
    }

    static string ReferenceName(object item)
    {
        if (item == null) return "";
        if (item is Feature feature) return Safe(() => feature.Name);
        if (item is RefPlane)
        {
            object refPlaneFeature = InvokeCom(item, "GetFeature");
            if (refPlaneFeature is Feature planeFeature) return Safe(() => planeFeature.Name);
        }
        try
        {
            object ownerFeature = InvokeCom(item, "GetFeature");
            if (ownerFeature is Feature featureOwner) return Safe(() => featureOwner.Name);
        }
        catch { }
        return "";
    }

    static string CanonicalReplayReferenceName(object item)
    {
        if (item is Feature feature)
        {
            string type = Safe(() => feature.GetTypeName2());
            string name = Safe(() => feature.Name);
            if (IsSketch(type))
                return CanonicalSketchPlanName(feature, name);
            return name;
        }

        try
        {
            object ownerFeature = InvokeCom(item, "GetFeature");
            if (ownerFeature is Feature featureOwner)
            {
                string type = Safe(() => featureOwner.GetTypeName2());
                string name = Safe(() => featureOwner.Name);
                if (IsSketch(type))
                    return CanonicalSketchPlanName(featureOwner, name);
                return name;
            }
        }
        catch { }

        return ReferenceName(item);
    }

    static string FirstCanonicalSubSketchName(Feature feature)
    {
        Feature sub = SafeObj(() => feature.GetFirstSubFeature()) as Feature;
        for (int i = 0; sub != null && i < 200; i++)
        {
            string type = Safe(() => sub.GetTypeName2());
            if (IsSketch(type))
                return CanonicalSketchPlanName(sub, Safe(() => sub.Name));
            sub = SafeObj(() => sub.GetNextSubFeature()) as Feature;
        }
        return "";
    }

    static int FirstFaceIndex(ModelDoc2 doc, object data, IEnumerable<string> names)
    {
        foreach (int index in FaceIndices(doc, data, names))
            return index;
        return 0;
    }

    static List<int> FaceIndices(ModelDoc2 doc, object data, IEnumerable<string> names)
    {
        var result = new List<int>();
        foreach (string name in names)
        {
            object value = SafeObj(() => GetComProperty(data, name));
            foreach (object item in FlattenComObjects(value))
            {
                if (item is Face2 face)
                {
                    int index = FindFaceIndexLikeExecutor(doc, face);
                    if (index > 0) result.Add(index);
                }
            }
        }
        return result;
    }

    static Face2 FirstFace(object data, IEnumerable<string> names)
    {
        foreach (string name in names)
        {
            object value = SafeObj(() => GetComProperty(data, name));
            foreach (object item in FlattenComObjects(value))
            {
                if (item is Face2 face) return face;
            }
        }
        return null;
    }

    static double[] FaceCenterMm(Face2 face)
    {
        double[] box = TryGetFaceBoxMm(face);
        if (box.Length >= 6)
        {
            return new[]
            {
                R((box[0] + box[3]) / 2.0),
                R((box[1] + box[4]) / 2.0),
                R((box[2] + box[5]) / 2.0)
            };
        }
        return Array.Empty<double>();
    }

    static List<double> FeatureDimensionValuesMm(Feature feature)
    {
        var values = new List<double>();
        try
        {
            object dimObj = feature.GetFirstDisplayDimension();
            while (dimObj != null)
            {
                object dimensionObj = InvokeCom(dimObj, "GetDimension");
                if (dimensionObj is Dimension dimension)
                {
                    double value = SafeDouble(() => dimension.SystemValue);
                    if (Math.Abs(value) > 1e-9)
                        values.Add(R(value * 1000.0));
                }
                dimObj = InvokeCom(dimObj, "GetNext");
            }
        }
        catch { }
        return values;
    }

    static double[] FirstSketchPointMm(Feature feature)
    {
        foreach (Sketch sketch in SketchesFromFeature(feature))
        {
            foreach (object pointObj in InvokeArray(sketch, "GetSketchPoints2").Concat(InvokeArray(sketch, "GetSketchPoints")))
            {
                if (pointObj is SketchPoint point && Pt(point, out double x, out double y))
                    return new[] { R(x), R(y) };
            }
        }
        return Array.Empty<double>();
    }

    static List<string> SketchFeatureNames(Feature feature)
    {
        var names = new List<string>();
        Feature sub = SafeObj(() => feature.GetFirstSubFeature()) as Feature;
        int guard = 0;
        while (sub != null && guard++ < 200)
        {
            string type = Safe(() => sub.GetTypeName2());
            if (IsSketch(type))
            {
                string sketchName = Safe(() => sub.Name);
                if (!string.IsNullOrWhiteSpace(sketchName))
                    names.Add(sketchName);
            }
            sub = SafeObj(() => sub.GetNextSubFeature()) as Feature;
        }
        return names;
    }

    static IEnumerable<Sketch> SketchesFromFeature(Feature feature)
    {
        Feature sub = SafeObj(() => feature.GetFirstSubFeature()) as Feature;
        int guard = 0;
        while (sub != null && guard++ < 200)
        {
            string type = Safe(() => sub.GetTypeName2());
            if (IsSketch(type))
            {
                object sketchObj = SafeObj(() => sub.GetSpecificFeature2());
                if (sketchObj is Sketch sketch)
                    yield return sketch;
            }
            sub = SafeObj(() => sub.GetNextSubFeature()) as Feature;
        }
    }

    static bool ExtrudeReverseDir(Feature f, out string source)
    {
        source = "default_false";
        try
        {
            object data = SafeObj(() => f.GetDefinition());
            if (data == null) return false;

            foreach (string name in new[]
            {
                "ReverseDirection",
                "ReverseDirection1",
                "Direction",
                "Direction1",
                "Dir",
                "Ddir1"
            })
            {
                if (TryReadBoolMember(data, name, out bool value))
                {
                    source = name;
                    return value;
                }
            }

            foreach (string name in new[]
            {
                "GetReverseDirection",
                "GetDirection"
            })
            {
                object value = InvokeCom(data, name);
                if (TryConvertBool(value, out bool parsed))
                {
                    source = name;
                    return parsed;
                }
            }
        }
        catch { }

        return false;
    }

    static bool TryReadBoolMember(object target, string name, out bool value)
    {
        value = false;
        object raw = GetComProperty(target, name);
        if (TryConvertBool(raw, out value)) return true;

        raw = InvokeCom(target, name);
        return TryConvertBool(raw, out value);
    }

    static bool TryConvertBool(object raw, out bool value)
    {
        value = false;
        if (raw == null) return false;
        if (raw is bool b) { value = b; return true; }
        if (raw is int i) { value = i != 0; return true; }
        if (raw is short s) { value = s != 0; return true; }
        if (raw is double d) { value = Math.Abs(d) > 1e-9; return true; }
        if (bool.TryParse(raw.ToString(), out bool parsedBool)) { value = parsedBool; return true; }
        if (double.TryParse(raw.ToString(), out double parsedDouble)) { value = Math.Abs(parsedDouble) > 1e-9; return true; }
        return false;
    }

    static void Save(string dir)
    {
        Directory.CreateDirectory(dir);
        string fileBase = BuildOutputFileBaseName();
        string xlsx = Path.Combine(dir, $"{fileBase}_建模过程.xlsx");
        string json = Path.Combine(dir, $"{fileBase}_skill_flow.json");
        using var wb = new XLWorkbook(); var ws = wb.Worksheets.Add("建模过程");
        string[] h = { "零件", "步骤", "层级", "特征", "类型", "分类", "草图图元", "草图基准", "尺寸", "约束", "备注" };
        for (int i = 0; i < h.Length; i++) ws.Cell(1, i + 1).Value = h[i];
        int r = 2; foreach (var a in Rows) { ws.Cell(r, 1).Value = a.PartName; ws.Cell(r, 2).Value = a.Step; ws.Cell(r, 3).Value = a.Depth; ws.Cell(r, 4).Value = a.Feature; ws.Cell(r, 5).Value = a.Type; ws.Cell(r, 6).Value = a.Category; ws.Cell(r, 7).Value = a.Sketch; ws.Cell(r, 8).Value = a.SketchPlane; ws.Cell(r, 9).Value = a.Dims; ws.Cell(r, 10).Value = a.Rels; ws.Cell(r, 11).Value = a.Note; r++; }
        ws.Columns().AdjustToContents(8, 80); wb.SaveAs(xlsx);
        EnrichExtrudeDirectionHints();
        NormalizeAutoNumberedFeatureNames();
        var jsonOptions = new JsonSerializerOptions { WriteIndented = true };
        var validation = ValidateSkillFlow();
        string swagentJson = Path.Combine(dir, $"{fileBase}_modeling_plan.json");
        string validationJson = Path.Combine(dir, $"{fileBase}_skill_flow_validation.json");
        File.WriteAllText(json, JsonSerializer.Serialize(Skills, jsonOptions));
        File.WriteAllText(swagentJson, JsonSerializer.Serialize(BuildSwagentModelingPlan(fileBase), jsonOptions));
        File.WriteAllText(validationJson, JsonSerializer.Serialize(validation, jsonOptions));
        Console.WriteLine("SWagent ModelingPlan JSON: " + swagentJson);
        Console.WriteLine("Skill Flow validation JSON: " + validationJson);
        Console.WriteLine(validation.Ok
            ? "Skill Flow validation passed."
            : $"Skill Flow validation failed with {validation.Errors.Count} error(s).");
        Console.WriteLine("完成！Excel 已导出：" + xlsx); Console.WriteLine("完成！Skill Flow JSON 已导出：" + json);
    }

    static string BuildOutputFileBaseName()
    {
        var partNames = Rows.Select(r => r.PartName)
            .Where(p => !string.IsNullOrWhiteSpace(p))
            .Distinct(StringComparer.OrdinalIgnoreCase)
            .ToList();

        string name = partNames.Count == 1 ? partNames[0] : "multi_part";
        name = Path.GetFileNameWithoutExtension(name);
        if (string.IsNullOrWhiteSpace(name))
            name = "part";

        foreach (char c in Path.GetInvalidFileNameChars())
            name = name.Replace(c, '_');

        return name.Trim();
    }

    static void AddSketchStartSkill(SketchReferenceResult reference, string source)
    {
        string description = reference?.Description ?? "";
        if (TryGetDefaultPlaneSketchSkill(description, out string defaultPlaneSkill))
        {
            AddSkill(defaultPlaneSkill, new(), source, $"草图基准={description}");
            return;
        }

        if (reference == null || reference.Kind == "unresolved")
        {
            AddSkill("list_faces", new() { ["unresolved_sketch_reference"] = description }, source, "草图基准未识别，列出面供人工校验");
            return;
        }

        if (reference.Kind == "face" && reference.FaceIndex > 0)
        {
            if (CanUseFaceSignature(reference))
            {
                var args = reference.ToSkillArgs();
                args["center_x_mm"] = R(reference.CenterMm[0]);
                args["center_y_mm"] = R(reference.CenterMm[1]);
                args["center_z_mm"] = R(reference.CenterMm[2]);
                args["fallback_face_index"] = reference.FaceIndex;
                AddSkill("create_face_sketch_by_signature", args, source, $"草图基准={description}");
                return;
            }

            AddSkill("create_face_sketch_by_index", reference.ToSkillArgs(), source, $"草图基准={description}");
            return;
        }

        if (reference.Kind == "datum_plane")
        {
            AddSkill("create_ref_plane_sketch_by_name", reference.ToSkillArgs(), source, $"草图基准={description}");
            return;
        }

        if (reference.Kind != "face")
        {
            AddSkill("list_faces", reference.ToSkillArgs(), source, $"草图基准尚无可执行选择 skill={description}");
            return;
        }

        if (reference.Kind == "face")
        {
            AddSkill("list_faces", reference.ToSkillArgs(), source, $"实体面未获得 SWagent face_index={description}");
            return;
        }

        if (reference == null || reference.Kind == "unresolved")
        {
            AddSkill("create_sketch_requires_manual_reference", new() { ["reason"] = description }, source, "草图基准未识别，未生成自动选面目标");
            return;
        }

        AddSkill("create_sketch_on_reference", reference.ToSkillArgs(), source, $"草图基准={description}");
    }

    static bool CanUseFaceSignature(SketchReferenceResult reference)
    {
        return reference != null
            && string.Equals(reference.Kind, "face", StringComparison.OrdinalIgnoreCase)
            && string.Equals(reference.SurfaceType, "plane", StringComparison.OrdinalIgnoreCase)
            && reference.CenterMm != null
            && reference.CenterMm.Length >= 3
            && reference.AreaMm2 > 0;
    }

    static bool TryInferPlanarFacePosition(SketchReferenceResult reference, out string position)
    {
        position = "";
        if (reference == null) return false;
        if (!string.Equals(reference.Kind, "face", StringComparison.OrdinalIgnoreCase)) return false;
        if (!string.Equals(reference.SurfaceType, "plane", StringComparison.OrdinalIgnoreCase)) return false;

        double[] center = reference.CenterMm ?? Array.Empty<double>();
        double[] box = reference.BoxMm ?? Array.Empty<double>();
        if (center.Length < 3) return false;

        int axis = -1;
        if (box.Length >= 6)
        {
            double dx = Math.Abs(box[3] - box[0]);
            double dy = Math.Abs(box[4] - box[1]);
            double dz = Math.Abs(box[5] - box[2]);
            double min = Math.Min(dx, Math.Min(dy, dz));
            if (min <= 1e-6)
            {
                if (Math.Abs(dx - min) <= 1e-6) axis = 0;
                else if (Math.Abs(dy - min) <= 1e-6) axis = 1;
                else axis = 2;
            }
        }

        if (axis < 0 && reference.Normal.Length >= 3)
        {
            double ax = Math.Abs(reference.Normal[0]);
            double ay = Math.Abs(reference.Normal[1]);
            double az = Math.Abs(reference.Normal[2]);
            if (ax >= ay && ax >= az) axis = 0;
            else if (ay >= ax && ay >= az) axis = 1;
            else axis = 2;
        }

        if (axis < 0) return false;

        double value = center[axis];
        position = axis switch
        {
            0 => value >= 0 ? "right" : "left",
            1 => value >= 0 ? "front" : "back",
            _ => value >= 0 ? "top" : "bottom"
        };
        return true;
    }

    static void EnsureRefPlaneCreationSkills(ModelDoc2 doc, SketchReferenceResult reference, string source)
    {
        if (doc == null || reference == null) return;
        if (!string.Equals(reference.Kind, "datum_plane", StringComparison.OrdinalIgnoreCase)) return;

        string planeName = (reference.ReferenceName ?? "").Trim();
        if (string.IsNullOrWhiteSpace(planeName) || IsDefaultPlaneName(planeName)) return;
        if (EmittedRefPlaneCreation.Contains(planeName)) return;

        var planeFeature = FindFeatureByName(doc, planeName);
        if (planeFeature == null || !string.Equals(Safe(() => planeFeature.GetTypeName2()), "RefPlane", StringComparison.OrdinalIgnoreCase))
        {
            EmittedRefPlaneCreation.Add(planeName);
            AddSkill("list_ref_planes", new() { ["reference_name"] = planeName, ["extract_note"] = "按名称未找到对应 RefPlane 特征，无法自动提取创建参数" }, source, "草图引用了自建基准面，但未定位到对应基准面特征");
            return;
        }

        AddRefPlaneCreationSkills(doc, planeFeature, planeName);
    }

    static bool TryGetDefaultPlaneSketchSkill(string description, out string skill)
    {
        skill = "";
        string text = (description ?? "").ToLowerInvariant();
        if (text.Contains("前视")) { skill = "create_front_plane_sketch"; return true; }
        if (text.Contains("上视")) { skill = "create_top_plane_sketch"; return true; }
        if (text.Contains("右视")) { skill = "create_right_plane_sketch"; return true; }
        if (text.Contains("前视") || text.Contains("front")) { skill = "create_front_plane_sketch"; return true; }
        if (text.Contains("上视") || text.Contains("top")) { skill = "create_top_plane_sketch"; return true; }
        if (text.Contains("右视") || text.Contains("right")) { skill = "create_right_plane_sketch"; return true; }
        return false;
    }

    static bool IsCustomRefPlane(string name, string type)
    {
        if (!string.Equals(type, "RefPlane", StringComparison.OrdinalIgnoreCase)) return false;
        return !IsDefaultPlaneName(name);
    }

    static bool IsDefaultPlaneName(string name)
    {
        string n = (name ?? "").Trim();
        return string.Equals(n, "前视基准面", StringComparison.OrdinalIgnoreCase)
            || string.Equals(n, "上视基准面", StringComparison.OrdinalIgnoreCase)
            || string.Equals(n, "右视基准面", StringComparison.OrdinalIgnoreCase)
            || string.Equals(n, "Front Plane", StringComparison.OrdinalIgnoreCase)
            || string.Equals(n, "Top Plane", StringComparison.OrdinalIgnoreCase)
            || string.Equals(n, "Right Plane", StringComparison.OrdinalIgnoreCase);
    }

    static void AddRefPlaneCreationSkills(ModelDoc2 doc, Feature feature, string planeName)
    {
        if (string.IsNullOrWhiteSpace(planeName)) return;
        if (!EmittedRefPlaneCreation.Add(planeName)) return;

        var info = TryReadRefPlane(doc, feature, planeName);
        if (!info.CanCreate)
        {
            AddSkill("list_ref_planes", new() { ["reference_name"] = planeName, ["extract_note"] = info.Note }, planeName, "自建基准面创建参数未能自动提取，列出基准面供校验");
            return;
        }

        if (string.Equals(info.BuildKind, "angle_edge_face", StringComparison.OrdinalIgnoreCase))
        {
            AddSkill("create_angle_plane_by_edge_and_face", new()
            {
                ["edge_signatures"] = info.EdgeSignatures,
                ["face_index"] = info.BaseFaceIndex,
                ["face_signature"] = info.FaceSignature,
                ["angle_deg"] = info.AngleDeg,
                ["plane_name"] = planeName
            }, planeName, "创建角度自建基准面");
            return;
        }

        if (string.Equals(info.BuildKind, "angle_axis_face", StringComparison.OrdinalIgnoreCase))
        {
            AddSkill("create_angle_plane_by_axis_and_face", new()
            {
                ["axis_name"] = info.AxisName,
                ["face_index"] = info.BaseFaceIndex,
                ["face_signature"] = info.FaceSignature,
                ["angle_deg"] = info.AngleDeg,
                ["plane_name"] = planeName
            }, planeName, "创建基准轴+面角度基准面");
            return;
        }

        if (info.BaseReferenceKind == "default_plane" && TryGetDefaultPlaneSelector(info.BaseReferenceName, out string plane))
        {
            AddSkill("select_default_plane", new() { ["plane"] = plane }, planeName, $"创建自建基准面前选择基准={info.BaseReferenceName}");
        }
        else if (info.BaseReferenceKind == "face" && info.BaseFaceIndex > 0)
        {
            AddSkill("select_face_by_index", new() { ["face_index"] = info.BaseFaceIndex }, planeName, $"创建自建基准面前选择实体面={info.BaseReferenceName}");
        }
        else if (info.BaseReferenceKind == "datum_plane" && !string.IsNullOrWhiteSpace(info.BaseReferenceName))
        {
            AddSkill("select_reference_plane_by_name", new() { ["plane_name"] = info.BaseReferenceName }, planeName, $"创建自建基准面前选择自建基准面={info.BaseReferenceName}");
        }
        else
        {
            AddSkill("list_faces", new() { ["reference_name"] = planeName, ["base_reference"] = info.BaseReferenceName, ["extract_note"] = info.Note }, planeName, "自建基准面基准引用未能映射为 SWagent 选择 skill");
            return;
        }

        AddSkill("create_offset_plane_mm", new()
        {
            ["offset_mm"] = info.OffsetMm,
            ["offset_abs_mm"] = info.OffsetAbsMm,
            ["reverse_direction"] = info.ReverseDirection,
            ["reversed_reference_direction"] = info.ReversedReferenceDirection,
            ["plane_name"] = planeName
        }, planeName, "创建偏移自建基准面");
    }

    static bool TryGetDefaultPlaneSelector(string description, out string plane)
    {
        plane = "";
        string text = (description ?? "").ToLowerInvariant();
        if (text.Contains("前视") || text.Contains("front")) { plane = "front"; return true; }
        if (text.Contains("上视") || text.Contains("top")) { plane = "top"; return true; }
        if (text.Contains("右视") || text.Contains("right")) { plane = "right"; return true; }
        return false;
    }

    static RefPlaneBuildInfo TryReadRefPlane(ModelDoc2 doc, Feature feature, string planeName)
    {
        var info = new RefPlaneBuildInfo { PlaneName = planeName };
        try { doc?.EditRebuild3(); } catch { }
        object data = SafeObj(() => feature.GetDefinition());
        if (data == null)
        {
            info.Note = "RefPlane.GetDefinition 返回空";
            return info;
        }

        try { doc?.ClearSelection2(true); } catch { }
        try { feature?.Select2(false, 0); } catch { }
        AccessRefPlaneSelections(data, doc);
        try
        {
            var angleInfo = TryReadAngleRefPlane(doc, data, planeName);
            if (angleInfo.CanCreate)
                return angleInfo;
            string angleNote = angleInfo.Note;

            int refPlaneType = ReadRefPlaneType(data);
            double offsetMeters = 0.0;
            object baseRef = null;
            bool hasDistanceConstraint = refPlaneType == (int)swRefPlaneType_e.swRefPlaneDistance;
            int distanceReferenceIndex = -1;
            bool reverseDirection = false;
            bool reversedReferenceDirection = false;
            if (data is IRefPlaneFeatureData typedRefPlane)
            {
                reverseDirection = SafeBool(() => typedRefPlane.ReverseDirection);
            }
            else
            {
                reverseDirection = FirstBool(data, new[] { "ReverseDirection" }, false);
                reversedReferenceDirection = FirstBool(data, new[] { "ReversedReferenceDirection" }, false);
            }

            for (int i = 0; i < 3; i++)
            {
                object referenceCandidate = FirstComReference(ReadRefPlaneReference(data, i));
                int constraint = ReadRefPlaneConstraint(data, i);
                double value = ReadRefPlaneAngleOrDistance(data, i);

                bool isDistanceReference =
                    (constraint & (int)swRefPlaneReferenceConstraints_e.swRefPlaneReferenceConstraint_Distance) != 0 ||
                    refPlaneType == (int)swRefPlaneType_e.swRefPlaneDistance;

                if (referenceCandidate != null && isDistanceReference)
                {
                    baseRef = referenceCandidate;
                    distanceReferenceIndex = i;
                    hasDistanceConstraint = true;
                    if (Math.Abs(value) > 1e-9)
                        offsetMeters = value;
                    break;
                }
            }

            if (data is IRefPlaneFeatureData typedRefPlaneWithIndex && distanceReferenceIndex >= 0)
                reversedReferenceDirection = SafeBool(() => typedRefPlaneWithIndex.ReversedReferenceDirection[distanceReferenceIndex]);

            if (hasDistanceConstraint && Math.Abs(offsetMeters) <= 1e-9)
            {
                offsetMeters = FirstMeaningfulDouble(
                    GetComProperty(data, "Distance"),
                    InvokeCom(data, "get_Distance"));
            }

            var reference = new SketchReferenceResult();
            if (baseRef != null && TryDescribeReferenceEntity(doc, baseRef, reference))
            {
                string referencePlaneName = string.IsNullOrWhiteSpace(reference.ReferenceName) ? reference.Description : reference.ReferenceName;
                if (reference.Kind == "datum_plane" && TryGetDefaultPlaneSelector(referencePlaneName, out _))
                {
                    info.BaseReferenceKind = "default_plane";
                    info.BaseReferenceName = referencePlaneName;
                }
                else if (reference.Kind == "datum_plane")
                {
                    info.BaseReferenceKind = "datum_plane";
                    info.BaseReferenceName = referencePlaneName;
                }
                else if (reference.Kind == "face" && reference.FaceIndex > 0)
                {
                    info.BaseReferenceKind = "face";
                    info.BaseReferenceName = reference.Description;
                    info.BaseFaceIndex = reference.FaceIndex;
                }
            }

            double offsetAbsMm = R(Math.Abs(offsetMeters) * 1000.0);
            bool negativeOffset = reverseDirection || reversedReferenceDirection || offsetMeters < -1e-9;
            info.OffsetAbsMm = offsetAbsMm;
            info.ReverseDirection = reverseDirection;
            info.ReversedReferenceDirection = reversedReferenceDirection;
            info.OffsetMm = negativeOffset ? -offsetAbsMm : offsetAbsMm;
            info.CanCreate = hasDistanceConstraint && !string.IsNullOrWhiteSpace(info.BaseReferenceKind);
            if (info.CanCreate) info.BuildKind = "offset";
            if (!info.CanCreate)
                info.Note = $"angle plane not recognized: {angleNote}; offset plane not recognized: base={reference.Kind}/{reference.Description}, offset_mm={info.OffsetMm}";
            info.Note = info.CanCreate
                ? "已识别单引用偏移基准面"
                : $"angle plane not recognized: {angleNote}; offset plane not recognized: base={reference.Kind}/{reference.Description}, offset_mm={info.OffsetMm}";
            return info;
        }
        finally
        {
            ReleaseRefPlaneSelections(data);
        }
    }

    static bool AccessRefPlaneSelections(object data, ModelDoc2 doc)
    {
        if (data is IRefPlaneFeatureData typed)
            return SafeBool(() => typed.AccessSelections(doc, null));
        return TryInvokeCom(data, "AccessSelections", doc, null);
    }

    static void ReleaseRefPlaneSelections(object data)
    {
        if (data is IRefPlaneFeatureData typed)
        {
            try { typed.ReleaseSelectionAccess(); } catch { }
            return;
        }
        TryInvokeCom(data, "ReleaseSelectionAccess");
    }

    static object FirstComReference(params object[] candidates)
    {
        foreach (object candidate in candidates)
        {
            foreach (object item in FlattenObjects(candidate))
            {
                if (item == null) continue;
                if (TryAsEdge(item, out Edge edge)) return edge;
                if (TryAsFace(item, out Face2 face)) return face;
                if (item is Feature || item is RefPlane) return item;
            }
        }
        return null;
    }

    static int ReadRefPlaneType(object data)
    {
        if (data is IRefPlaneFeatureData typed)
            return SafeInt(() => typed.Type, 0);
        return FirstMeaningfulInt(GetComProperty(data, "Type"), InvokeCom(data, "get_Type"));
    }

    static object ReadRefPlaneReference(object data, int index)
    {
        if (data is IRefPlaneFeatureData typed)
            return SafeObj(() => typed.Reference[index]);
        return FirstComReference(GetComProperty(data, "Reference", index), InvokeCom(data, "get_Reference", index));
    }

    static int ReadRefPlaneConstraint(object data, int index)
    {
        if (data is IRefPlaneFeatureData typed)
            return SafeInt(() => typed.Constraint[index], 0);
        return FirstMeaningfulInt(GetComProperty(data, "Constraint", index), InvokeCom(data, "get_Constraint", index));
    }

    static double ReadRefPlaneAngleOrDistance(object data, int index)
    {
        if (data is IRefPlaneFeatureData typed)
            return SafeDouble(() => typed.AngleOrDistance[index], 0.0);
        return FirstMeaningfulDouble(GetComProperty(data, "AngleOrDistance", index), InvokeCom(data, "get_AngleOrDistance", index));
    }

    static IEnumerable<object> ReadRefPlaneSelectionObjects(object data)
    {
        if (data is IRefPlaneFeatureData typed)
        {
            foreach (object item in FlattenComObjects(SafeObj(() => typed.Selections)))
                yield return item;

            yield break;
        }

        foreach (object item in FlattenComObjects(GetComProperty(data, "Selections")))
            yield return item;
    }

    static RefPlaneBuildInfo TryReadAngleRefPlane(ModelDoc2 doc, object data, string planeName)
    {
        var info = new RefPlaneBuildInfo { PlaneName = planeName };
        var edgeSignatures = new List<Dictionary<string, object>>();
        var faceReference = new SketchReferenceResult();
        string axisName = "";
        double angleRad = 0.0;
        var debug = new List<string>();

        for (int i = 0; i < 3; i++)
        {
            object rawReference = ReadRefPlaneReference(data, i);
            object reference = FirstComReference(rawReference);
            int constraint = ReadRefPlaneConstraint(data, i);
            double value = ReadRefPlaneAngleOrDistance(data, i);

            if (edgeSignatures.Count == 0)
            {
                var candidateEdgeSignatures = EdgeSignatures(new object[] { rawReference });
                debug.Add($"raw[{i}]={rawReference?.GetType().Name ?? "null"}, asEdge={TryAsEdge(rawReference, out _)}, edgeSigCount={candidateEdgeSignatures.Count}");
                if (candidateEdgeSignatures.Count > 0)
                    edgeSignatures = candidateEdgeSignatures;
            }

            if (string.IsNullOrWhiteSpace(axisName))
            {
                foreach (object axisCandidate in FlattenComObjects(new object[] { rawReference, reference }))
                {
                    var candidateAxisReference = new SketchReferenceResult();
                    if (axisCandidate != null &&
                        TryDescribeReferenceEntity(doc, axisCandidate, candidateAxisReference) &&
                        string.Equals(candidateAxisReference.Kind, "datum_axis", StringComparison.OrdinalIgnoreCase))
                    {
                        axisName = candidateAxisReference.ReferenceName;
                        break;
                    }
                }
                if (!string.IsNullOrWhiteSpace(axisName))
                    debug.Add($"axisProbe[{i}]={axisName}");
            }

            if (faceReference.FaceIndex <= 0)
            {
                var candidateFaceReference = new SketchReferenceResult();
                foreach (object faceCandidate in FlattenComObjects(new object[] { rawReference, reference }))
                {
                    if (faceCandidate != null &&
                        TryDescribeReferenceEntity(doc, faceCandidate, candidateFaceReference) &&
                        candidateFaceReference.FaceIndex > 0)
                    {
                        faceReference = candidateFaceReference;
                        break;
                    }
                }
                debug.Add($"faceProbe[{i}]={candidateFaceReference.Kind}/{candidateFaceReference.FaceIndex}");
            }

            if (Math.Abs(value) > 1e-9 &&
                ((constraint & (int)swRefPlaneReferenceConstraints_e.swRefPlaneReferenceConstraint_Angle) != 0 || angleRad == 0.0))
                angleRad = value;

            debug.Add($"i={i}, constraint={constraint}, value={value}, ref={reference?.GetType().Name ?? "null"}");
        }

        if (edgeSignatures.Count == 0 || faceReference.FaceIndex <= 0)
        {
            foreach (object selectedReference in ReadRefPlaneSelectionObjects(data))
            {
                if (edgeSignatures.Count == 0)
                {
                    var candidateEdgeSignatures = EdgeSignatures(new object[] { selectedReference });
                    debug.Add($"selection={selectedReference?.GetType().Name ?? "null"}, asEdge={TryAsEdge(selectedReference, out _)}, edgeSigCount={candidateEdgeSignatures.Count}");
                    if (candidateEdgeSignatures.Count > 0)
                        edgeSignatures = candidateEdgeSignatures;
                }

                if (string.IsNullOrWhiteSpace(axisName))
                {
                    foreach (object axisCandidate in FlattenComObjects(selectedReference))
                    {
                        var candidateAxisReference = new SketchReferenceResult();
                        if (axisCandidate != null &&
                            TryDescribeReferenceEntity(doc, axisCandidate, candidateAxisReference) &&
                            string.Equals(candidateAxisReference.Kind, "datum_axis", StringComparison.OrdinalIgnoreCase))
                        {
                            axisName = candidateAxisReference.ReferenceName;
                            break;
                        }
                    }
                    if (!string.IsNullOrWhiteSpace(axisName))
                        debug.Add($"selectionAxis={axisName}");
                }

                if (faceReference.FaceIndex <= 0)
                {
                    var candidateFaceReference = new SketchReferenceResult();
                    foreach (object faceCandidate in FlattenComObjects(selectedReference))
                    {
                        if (faceCandidate != null &&
                            TryDescribeReferenceEntity(doc, faceCandidate, candidateFaceReference) &&
                            candidateFaceReference.FaceIndex > 0)
                        {
                            faceReference = candidateFaceReference;
                            break;
                        }
                    }
                    debug.Add($"selectionFace={candidateFaceReference.Kind}/{candidateFaceReference.FaceIndex}");
                }
            }
        }

        if (Math.Abs(angleRad) <= 1e-9)
        {
            angleRad = FirstMeaningfulDouble(
                GetComProperty(data, "Angle"),
                InvokeCom(data, "GetAngle"),
                GetComProperty(data, "AngleOrDistance"));
        }

        bool hasAxisReference = edgeSignatures.Count > 0 || !string.IsNullOrWhiteSpace(axisName);
        if (!hasAxisReference || faceReference.FaceIndex <= 0)
        {
            info.Note = "未识别为角度基准面: " + string.Join("; ", debug);
            return info;
        }

        double angleDeg = R(angleRad * 180.0 / Math.PI);
        while (angleDeg < 0) angleDeg += 360.0;
        while (angleDeg >= 360.0) angleDeg -= 360.0;

        info.CanCreate = true;
        info.BuildKind = !string.IsNullOrWhiteSpace(axisName) ? "angle_axis_face" : "angle_edge_face";
        info.BaseReferenceKind = "face";
        info.BaseReferenceName = faceReference.Description;
        info.BaseFaceIndex = faceReference.FaceIndex;
        info.AngleDeg = R(angleDeg);
        info.EdgeSignatures = edgeSignatures;
        info.AxisName = axisName;
        info.FaceSignature = BuildFaceSignatureArgs(faceReference);
        info.Note = $"已识别边线+面角度基准面: face_index={info.BaseFaceIndex}, angle_deg={info.AngleDeg}";
        return info;
    }

    static Dictionary<string, object> BuildFaceSignatureArgs(SketchReferenceResult reference)
    {
        var result = new Dictionary<string, object>();
        if (reference == null) return result;
        if (!string.IsNullOrWhiteSpace(reference.SurfaceType)) result["surface_type"] = reference.SurfaceType;
        if (reference.AreaMm2 > 0) result["area_mm2"] = reference.AreaMm2;
        if (reference.CenterMm != null && reference.CenterMm.Length >= 3)
        {
            result["center_mm"] = reference.CenterMm;
            result["center_x_mm"] = R(reference.CenterMm[0]);
            result["center_y_mm"] = R(reference.CenterMm[1]);
            result["center_z_mm"] = R(reference.CenterMm[2]);
        }
        if (reference.BoxMm != null && reference.BoxMm.Length >= 6)
            result["box_mm"] = reference.BoxMm;
        if (reference.Normal != null && reference.Normal.Length >= 3)
        {
            result["normal"] = reference.Normal;
            result["normal_x"] = reference.Normal[0];
            result["normal_y"] = reference.Normal[1];
            result["normal_z"] = reference.Normal[2];
        }
        return result;
    }

    static IEnumerable<object> FlattenObjects(object value)
    {
        if (value == null) yield break;
        if (value is string) yield break;
        if (value is Array array)
        {
            foreach (object item in array)
            {
                foreach (object nested in FlattenObjects(item)) yield return nested;
            }
            yield break;
        }
        yield return value;
    }

    static double FirstMeaningfulDouble(params object[] candidates)
    {
        foreach (object candidate in candidates)
        {
            foreach (double value in FlattenDoubles(candidate))
            {
                if (Math.Abs(value) > 1e-9) return value;
            }
        }
        return 0;
    }

    static int FirstMeaningfulInt(params object[] candidates)
    {
        foreach (object candidate in candidates)
        {
            foreach (double value in FlattenDoubles(candidate))
            {
                int result = Convert.ToInt32(value);
                if (result != 0) return result;
            }
        }
        return 0;
    }

    static IEnumerable<double> FlattenDoubles(object value)
    {
        if (value == null) yield break;
        if (value is double d) { yield return d; yield break; }
        if (value is float f) { yield return f; yield break; }
        if (value is int i) { yield return i; yield break; }
        if (value is Array array)
        {
            foreach (object item in array)
            {
                foreach (double nested in FlattenDoubles(item)) yield return nested;
            }
        }
    }

    static object BuildSwagentModelingPlan(string fileBase)
    {
        string workflowId = "part-feature-audit-" + fileBase;
        var steps = Skills.Select(item => new
        {
            id = $"step-{item.Step:000}",
            targetAgent = "solidworks",
            skillName = item.Skill,
            name = string.IsNullOrWhiteSpace(item.SourceFeature) ? item.Skill : item.SourceFeature,
            description = item.Note ?? "",
            arguments = item.Args ?? new Dictionary<string, object>(),
            dependencies = item.Step > 1 ? new[] { $"step-{item.Step - 1:000}" } : Array.Empty<string>(),
            acceptance = new Dictionary<string, object>()
        }).ToList();

        return new
        {
            workflowId,
            sourceAgent = "part_feature_audit",
            approvedPlanText = "Generated from extracted SolidWorks feature skill flow.",
            modelingPlan = new
            {
                planId = workflowId + "-plan",
                targetAgent = "solidworks",
                units = "mm",
                summary = $"Rebuild part from extracted SolidWorks skill flow. steps={steps.Count}",
                steps
            },
            simulationPlan = new Dictionary<string, object>(),
            exportOptions = new
            {
                outputDirectory = "",
                fileBaseName = "",
                formats = Array.Empty<string>()
            },
            handoffOptions = new
            {
                autoSubmitToSimulation = false,
                simulationAgentBaseUrl = Environment.GetEnvironmentVariable("AGENT4_SIMULATION_API_BASE") ?? ""
            }
        };
    }

    static SkillFlowValidationResult ValidateSkillFlow()
    {
        var result = new SkillFlowValidationResult
        {
            TotalSteps = Skills.Count,
            SkillsUsed = Skills.Select(s => s.Skill)
                .Where(s => !string.IsNullOrWhiteSpace(s))
                .Distinct(StringComparer.OrdinalIgnoreCase)
                .OrderBy(s => s)
                .ToList()
        };

        var supportedSkills = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
        {
            "create_new_part",
            "create_front_plane_sketch",
            "create_top_plane_sketch",
            "create_right_plane_sketch",
            "enter_sketch",
            "exit_sketch",
            "draw_line_mm",
            "draw_center_line_mm",
            "draw_point_mm",
            "draw_spline_points_mm",
            "draw_circle_diameter_mm",
            "draw_arc_center_start_end_mm",
            "draw_3point_arc_mm",
            "extrude_boss_mm",
            "extrude_cut_mm",
            "create_revolve_boss_mm",
            "create_revolve_cut_mm",
            "create_helix_mm",
            "list_faces",
            "create_face_sketch_by_index",
            "create_face_sketch_by_signature",
            "create_planar_face_sketch_by_position",
            "select_face_by_index",
            "list_ref_planes",
            "select_default_plane",
            "select_reference_plane_by_name",
            "create_ref_plane_sketch_by_name",
            "create_reference_axis",
            "create_offset_plane_mm",
            "create_angle_plane_by_edge_and_face",
            "create_angle_plane_by_axis_and_face",
            "select_edges_by_signature",
            "fillet_edges_radius_mm",
            "chamfer_edges_angle_distance_mm",
            "linear_pattern_feature_mm",
            "circular_pattern_feature",
            "draft_faces_neutral_plane_mm",
            "create_rib_mm",
            "shell_remove_face_at_point_mm",
            "hole_wizard_threaded_mm",
            "mirror_feature_about_plane",
            "wrap_sketch_on_faces_mm",
            "swept_cut_circular_profile_mm",
            "swept_cut_profile_path",
            "swept_boss_circular_profile_mm",
            "loft_boss_from_profiles",
            "loft_cut_from_profiles",
            "boundary_boss_from_profiles",
            "boundary_cut_from_profiles"
        };

        var requiredArgs = new Dictionary<string, string[]>(StringComparer.OrdinalIgnoreCase)
        {
            ["draw_line_mm"] = new[] { "start_model_mm", "end_model_mm" },
            ["draw_center_line_mm"] = new[] { "start_model_mm", "end_model_mm" },
            ["draw_point_mm"] = new[] { "point_model_mm" },
            ["draw_spline_points_mm"] = new[] { "points_model_mm" },
            ["draw_circle_diameter_mm"] = new[] { "diameter_mm", "center_model_mm" },
            ["draw_arc_center_start_end_mm"] = new[] { "center_model_mm", "start_model_mm", "end_model_mm", "mid_model_mm" },
            ["draw_3point_arc_mm"] = new[] { "start_x_mm", "start_y_mm", "point_x_mm", "point_y_mm", "end_x_mm", "end_y_mm" },
            ["extrude_boss_mm"] = new[] { "depth_mm", "extrude_direction_model" },
            ["extrude_cut_mm"] = new[] { "depth_mm", "extrude_direction_model" },
            ["create_revolve_cut_mm"] = new[] { "angle_deg" },
            ["create_helix_mm"] = new[] { "defined_by" },
            ["create_face_sketch_by_index"] = new[] { "face_index" },
            ["create_face_sketch_by_signature"] = new[] { "center_x_mm", "center_y_mm", "center_z_mm", "area_mm2" },
            ["select_face_by_index"] = new[] { "face_index" },
            ["select_default_plane"] = new[] { "plane" },
            ["select_reference_plane_by_name"] = new[] { "plane_name" },
            ["create_ref_plane_sketch_by_name"] = new[] { "plane_name" },
            ["create_offset_plane_mm"] = new[] { "offset_mm" },
            ["create_angle_plane_by_edge_and_face"] = new[] { "edge_signatures", "face_index", "angle_deg" },
            ["select_edges_by_signature"] = new[] { "edge_signatures" },
            ["fillet_edges_radius_mm"] = new[] { "radius_mm" },
            ["chamfer_edges_angle_distance_mm"] = new[] { "distance_mm", "angle_deg" },
            ["linear_pattern_feature_mm"] = new[] { "count1", "spacing1_mm", "dir1_edge_x_mm", "dir1_edge_y_mm", "dir1_edge_z_mm" },
            ["circular_pattern_feature"] = new[] { "count", "angle_deg" },
            ["draft_faces_neutral_plane_mm"] = new[] { "angle_deg", "neutral_face_index", "draft_face_indices" },
            ["create_rib_mm"] = new[] { "thickness_mm" },
            ["shell_remove_face_at_point_mm"] = new[] { "thickness_mm", "x_mm", "y_mm", "z_mm" },
            ["hole_wizard_threaded_mm"] = new[] { "diameter_mm", "depth_mm", "position_points_model_mm" },
            ["swept_cut_circular_profile_mm"] = new[] { "diameter_mm" },
            ["swept_cut_profile_path"] = new[] { "profile_name", "path_name" },
            ["swept_boss_circular_profile_mm"] = new[] { "diameter_mm" }
        };

        var reviewOnlySkills = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
        {
            "list_faces",
            "list_ref_planes"
        };

        for (int i = 0; i < Skills.Count; i++)
        {
            var step = Skills[i];
            string label = $"step-{step.Step:000}";

            if (step.Step != i + 1)
                result.Errors.Add($"{label}: step number is not sequential; expected {i + 1}.");

            if (string.IsNullOrWhiteSpace(step.Skill))
            {
                result.Errors.Add($"{label}: missing skill.");
                continue;
            }

            if (!supportedSkills.Contains(step.Skill))
            {
                result.Errors.Add($"{label}: skill '{step.Skill}' is not registered in the current SWagent skill registry.");
                continue;
            }

            var args = step.Args ?? new Dictionary<string, object>();
            if (requiredArgs.TryGetValue(step.Skill, out var required))
            {
                foreach (string key in required)
                {
                    if (!args.TryGetValue(key, out var value) || IsBlankValue(value))
                        result.Errors.Add($"{label}: skill '{step.Skill}' is missing required argument '{key}'.");
                }
            }

            if (reviewOnlySkills.Contains(step.Skill))
                result.Warnings.Add($"{label}: skill '{step.Skill}' is an inspection/manual-review step; the flow may need a resolved selection before fully automatic replay.");

            if ((string.Equals(step.Skill, "extrude_boss_mm", StringComparison.OrdinalIgnoreCase) ||
                string.Equals(step.Skill, "extrude_cut_mm", StringComparison.OrdinalIgnoreCase)) &&
                args.TryGetValue("depth_mm", out var depthValue) &&
                TryToDouble(depthValue, out double depthMm) &&
                Math.Abs(depthMm) <= 1e-9 &&
                !HasTrueArg(args, "through_all") &&
                !HasNonBlindEndCondition(args))
            {
                result.Warnings.Add($"{label}: '{step.Skill}' has depth_mm=0; SWagent execution will probably fail or create no geometry.");
            }
        }

        result.Ok = result.Errors.Count == 0;
        return result;
    }

    static bool IsBlankValue(object value) => value == null || value is string s && string.IsNullOrWhiteSpace(s);

    static bool HasTrueArg(Dictionary<string, object> args, string key)
    {
        if (!args.TryGetValue(key, out var value) || value == null) return false;
        if (value is bool b) return b;
        return bool.TryParse(value.ToString(), out bool parsed) && parsed;
    }

    static bool HasNonBlindEndCondition(Dictionary<string, object> args)
    {
        if (args == null || !args.TryGetValue("end_condition_code", out var value))
            return false;
        return TryToDouble(value, out double code) &&
            Math.Abs(code - (int)swEndConditions_e.swEndCondBlind) > 1e-9;
    }

    static bool TryToDouble(object value, out double number)
    {
        number = 0;
        if (value == null) return false;
        if (value is double d) { number = d; return true; }
        if (value is float f) { number = f; return true; }
        if (value is int i) { number = i; return true; }
        return double.TryParse(value.ToString(), out number);
    }

    static void AddSkill(string skill, Dictionary<string, object> args, string source, string note) => Skills.Add(new SkillFlowStep { Step = Skills.Count + 1, Skill = skill, Args = args ?? new Dictionary<string, object>(), SourceFeature = source, Note = note });
    static bool ShouldSkipDefaultFeature(string name, string type)
    {
        string n = (name ?? "").Trim();
        string t = (type ?? "").Trim().ToLowerInvariant();
        if (string.IsNullOrWhiteSpace(n) && string.IsNullOrWhiteSpace(t)) return true;

        return n switch
        {
            "Favorites" => true,
            "History" => true,
            "Selection Sets" => true,
            "传感器" => true,
            "设计活页夹" => true,
            "注解" => true,
            "标注" => true,
            "光源、相机与布景" => true,
            "实体" => true,
            "曲面实体" => true,
            "备注" => true,
            "方程式" => true,
            "材料 <未指定>" => true,
            "前视基准面" => true,
            "上视基准面" => true,
            "右视基准面" => true,
            "原点" => true,
            _ => t.Contains("historyfolder")
                || t.Contains("selectionsetfolder")
                || t.Contains("sensorfolder")
                || t.Contains("annotation")
                || t.Contains("commentsfolder")
                || t.Contains("equation")
                || t.Contains("refplane") && (n.Contains("前视") || n.Contains("上视") || n.Contains("右视"))
                || t.Contains("originprofilefeature")
        };
    }

    static string Cat(string n, string t) { string s = (n + " " + t).ToLowerInvariant(); if (IsSketch(t)) return "草图"; if (IsHelixFeature(n, t)) return "螺旋线/涡状线"; if (IsSweepFeature(n, t)) return "扫描"; if (IsCutLike(n, t) && (s.Contains("revol") || s.Contains("revolution") || s.Contains("旋转"))) return "旋转切除"; if (s.Contains("revol") || s.Contains("revolution") || s.Contains("旋转")) return "旋转凸台"; if (IsExtrude(n, t, true)) return "切除-拉伸"; if (IsExtrude(n, t, false)) return "凸台-拉伸"; if (s.Contains("fillet") || s.Contains("圆角")) return "圆角"; if (s.Contains("chamfer") || s.Contains("倒角")) return "倒角"; if (IsCircularPatternFeature(n, t)) return "圆周阵列"; if (IsLinearPatternFeature(n, t)) return "线性阵列"; if (IsDraftFeature(n, t)) return "拔模"; if (IsRibFeature(n, t)) return "筋"; if (IsShellFeature(n, t)) return "抽壳"; if (IsHoleWizardFeature(n, t)) return "异形孔向导"; if (IsMirrorFeature(n, t)) return "镜像"; if (IsWrapFeature(n, t)) return "包覆"; if (IsBoundaryFeature(n, t)) return "边界"; if (IsLoftFeature(n, t)) return "放样"; return "其他"; }
    static bool IsSketch(string t) { string s = (t ?? "").ToLowerInvariant(); return s.Contains("profilefeature") || s.Contains("sketch"); }
    static bool IsExtrude(string n, string t, bool cut) { string s = (n + " " + t).ToLowerInvariant(); return cut ? s.Contains("cut") || s.Contains("切除") : (s.Contains("extrude") || s.Contains("boss") || s.Contains("拉伸")) && !s.Contains("cut") && !s.Contains("切除"); }
    static bool IsFilletFeature(string n, string t) { string s = (n + " " + t).ToLowerInvariant(); return s.Contains("fillet") || s.Contains("圆角") || s.Contains("鍦嗚"); }
    static bool IsChamferFeature(string n, string t) { string s = (n + " " + t).ToLowerInvariant(); return s.Contains("chamfer") || s.Contains("倒角") || s.Contains("鍊掕"); }
    static bool IsCutLike(string n, string t) { string s = (n + " " + t).ToLowerInvariant(); return s.Contains("cut") || s.Contains("切除") || s.Contains("鍒囬櫎"); }
    static bool IsLinearPatternFeature(string n, string t) { string s = (n + " " + t).ToLowerInvariant(); return s.Contains("lpattern") || s.Contains("linearpattern") || s.Contains("linear pattern") || s.Contains("线性阵列") || s.Contains("線性陣列"); }
    static bool IsCircularPatternFeature(string n, string t) { string s = (n + " " + t).ToLowerInvariant(); return s.Contains("cirpattern") || s.Contains("circularpattern") || s.Contains("circular pattern") || s.Contains("圆周阵列") || s.Contains("圓周陣列") || s.Contains("阵列(圆周)") || s.Contains("陣列(圓周)"); }
    static bool IsDraftFeature(string n, string t) { string s = (n + " " + t).ToLowerInvariant(); return s.Contains("draft") || s.Contains("拔模"); }
    static bool IsRibFeature(string n, string t) { string s = (n + " " + t).ToLowerInvariant(); return s.Contains("rib") || s.Contains("筋"); }
    static bool IsReferenceAxisFeature(string n, string t)
    {
        string type = (t ?? "").Trim();
        if (string.Equals(type, "RefAxis", StringComparison.OrdinalIgnoreCase))
            return true;
        string s = (n + " " + t).ToLowerInvariant();
        return s.Contains("refaxis") || s.Contains("reference axis") || s.Contains("基准轴");
    }
    static bool IsShellFeature(string n, string t) { string s = (n + " " + t).ToLowerInvariant(); return s.Contains("shell") || s.Contains("抽壳") || s.Contains("抽殼"); }
    static bool IsHoleWizardFeature(string n, string t) { string s = (n + " " + t).ToLowerInvariant(); return s.Contains("holewzd") || s.Contains("hole wizard") || s.Contains("wizardhole") || s.Contains("异形孔") || s.Contains("異型孔"); }
    static bool IsMirrorFeature(string n, string t) { string s = (n + " " + t).ToLowerInvariant(); return s.Contains("mirror") || s.Contains("镜像") || s.Contains("鏡像"); }
    static bool IsWrapFeature(string n, string t) { string s = (n + " " + t).ToLowerInvariant(); return s.Contains("wrap") || s.Contains("包覆"); }
    static bool IsHelixFeature(string n, string t) { string s = (n + " " + t).ToLowerInvariant(); return s.Contains("helix") || s.Contains("spiral") || s.Contains("螺旋") || s.Contains("涡状") || s.Contains("渦狀"); }
    static bool IsSweepFeature(string n, string t) { string s = (n + " " + t).ToLowerInvariant(); return s.Contains("sweep") || s.Contains("swept") || s.Contains("扫描") || s.Contains("掃描"); }
    static bool IsLoftFeature(string n, string t) { string s = (n + " " + t).ToLowerInvariant(); return s.Contains("loft") || s.Contains("blend") || s.Contains("放样") || s.Contains("放樣"); }
    static bool IsBoundaryFeature(string n, string t) { string s = (n + " " + t).ToLowerInvariant(); return s.Contains("boundary") || s.Contains("netblend") || s.Contains("边界") || s.Contains("邊界"); }
    static object SafeObj(Func<object> f) { try { return f(); } catch { return null; } }
    static bool SafeBool(Func<bool> f, bool d = false) { try { return f(); } catch { return d; } }
    static T SafeValue<T>(Func<T> f, T d = default) { try { return f(); } catch { return d; } }
    static void SafeAction(Action a) { try { a(); } catch { } }
    class SketchPointSample
    {
        public double X, Y, Z;
        public int Priority, Order;
    }

    static Dictionary<IntPtr, double[]> BuildSketchPointCanonicalMap(Sketch sk)
    {
        var samples = new Dictionary<IntPtr, List<SketchPointSample>>();
        var parent = new Dictionary<IntPtr, IntPtr>();
        int order = 0;

        IntPtr Find(IntPtr id)
        {
            if (id == IntPtr.Zero) return IntPtr.Zero;
            if (!parent.TryGetValue(id, out var p))
            {
                parent[id] = id;
                return id;
            }
            if (p == id) return id;
            var root = Find(p);
            parent[id] = root;
            return root;
        }

        void Union(IntPtr a, IntPtr b)
        {
            var ra = Find(a);
            var rb = Find(b);
            if (ra == IntPtr.Zero || rb == IntPtr.Zero || ra == rb) return;
            parent[rb] = ra;
        }

        IntPtr Register(SketchPoint point, int priority)
        {
            if (point == null) return IntPtr.Zero;
            var id = Id(point);
            if (id == IntPtr.Zero) return IntPtr.Zero;
            if (!parent.ContainsKey(id)) parent[id] = id;
            try
            {
                if (!samples.TryGetValue(id, out var list))
                {
                    list = new List<SketchPointSample>();
                    samples[id] = list;
                }
                list.Add(new SketchPointSample
                {
                    X = point.X * 1000.0,
                    Y = point.Y * 1000.0,
                    Z = point.Z * 1000.0,
                    Priority = priority,
                    Order = order++
                });
            }
            catch { }
            return id;
        }

        foreach (object o in ToObjectArray(SafeObj(() => sk.GetSketchSegments())))
        {
            if (o is SketchLine line)
            {
                Register(line.GetStartPoint2() as SketchPoint, 2);
                Register(line.GetEndPoint2() as SketchPoint, 2);
            }
            else if (o is SketchArc arc)
            {
                Register(arc.GetCenterPoint2() as SketchPoint, 1);
                Register(arc.GetStartPoint2() as SketchPoint, 3);
                Register(arc.GetEndPoint2() as SketchPoint, 3);
            }
        }

        foreach (object pointObj in InvokeArray(sk, "GetSketchPoints2").Concat(InvokeArray(sk, "GetSketchPoints")))
        {
            if (pointObj is SketchPoint point) Register(point, 0);
        }

        foreach (object relObj in SketchRelations(sk))
        {
            if (relObj is not SketchRelation relation) continue;
            int relationType = SafeInt(() => relation.GetRelationType());
            if (relationType != (int)swConstraintType_e.swConstraintType_COINCIDENT
                && relationType != (int)swConstraintType_e.swConstraintType_MERGEPOINTS)
                continue;

            var ids = new List<IntPtr>();
            object[] entities = ToObjectArray(SafeObj(() => relation.GetEntities()));
            if (entities.Length == 0)
            {
                int count = SafeInt(() => relation.GetEntitiesCount(), 0);
                if (count > 0) entities = ToObjectArray(SafeObj(() => relation.IGetEntities(count)));
            }

            foreach (object entity in entities)
            {
                if (entity is SketchPoint point)
                {
                    var id = Register(point, 0);
                    if (id != IntPtr.Zero) ids.Add(id);
                }
            }

            for (int i = 1; i < ids.Count; i++)
                Union(ids[0], ids[i]);
        }

        var groups = new Dictionary<IntPtr, List<SketchPointSample>>();
        foreach (var kv in samples)
        {
            var root = Find(kv.Key);
            if (!groups.TryGetValue(root, out var list))
            {
                list = new List<SketchPointSample>();
                groups[root] = list;
            }
            list.AddRange(kv.Value);
        }

        var result = new Dictionary<IntPtr, double[]>();
        foreach (var group in groups)
        {
            var chosen = group.Value
                .OrderByDescending(p => p.Priority)
                .ThenBy(p => p.Order)
                .FirstOrDefault();
            if (chosen == null) continue;
            var canonical = new[] { chosen.X, chosen.Y, chosen.Z };
            foreach (var id in samples.Keys.Where(id => Find(id) == group.Key))
                result[id] = canonical;
        }

        return result;
    }

    static object[] SketchRelations(Sketch sk)
    {
        var list = new List<object>();
        var seen = new HashSet<IntPtr>();

        void Add(object value)
        {
            foreach (object item in ToObjectArray(value))
            {
                if (item == null) continue;
                var id = Id(item);
                if (id == IntPtr.Zero || seen.Add(id))
                    list.Add(item);
            }
        }

        Add(InvokeCom(sk, "GetRelations"));

        object manager = GetComProperty(sk, "RelationManager");
        if (manager != null)
        {
            Add(InvokeCom(manager, "GetRelations"));
            int count = SafeInt(() => Convert.ToInt32(InvokeCom(manager, "GetRelationsCount")), 0);
            if (count > 0) Add(InvokeCom(manager, "IGetRelations", count));
        }

        return list.ToArray();
    }

    static bool Pt(SketchPoint p, out double x, out double y) { x = y = 0; if (p == null) return false; x = p.X * 1000; y = p.Y * 1000; return true; }
    static bool Pt3(SketchPoint p, out double x, out double y, out double z) { x = y = z = 0; if (p == null) return false; x = p.X * 1000; y = p.Y * 1000; z = p.Z * 1000; return true; }
    static bool Pt(SketchPoint p, Dictionary<IntPtr, double[]> canonical, out double x, out double y)
    {
        x = y = 0;
        if (p == null) return false;
        var id = Id(p);
        if (id != IntPtr.Zero && canonical != null && canonical.TryGetValue(id, out var values) && values.Length >= 2)
        {
            x = values[0];
            y = values[1];
            return true;
        }
        return Pt(p, out x, out y);
    }

    static bool Pt3(SketchPoint p, Dictionary<IntPtr, double[]> canonical, out double x, out double y, out double z)
    {
        x = y = z = 0;
        if (p == null) return false;
        var id = Id(p);
        if (id != IntPtr.Zero && canonical != null && canonical.TryGetValue(id, out var values) && values.Length >= 3)
        {
            x = values[0];
            y = values[1];
            z = values[2];
            return true;
        }
        return Pt3(p, out x, out y, out z);
    }

    static IntPtr Id(object o) { if (!Marshal.IsComObject(o)) return IntPtr.Zero; IntPtr p = IntPtr.Zero; try { p = Marshal.GetIUnknownForObject(o); return p; } catch { return IntPtr.Zero; } finally { if (p != IntPtr.Zero) Marshal.Release(p); } }
    static object[] InvokeArray(object target, string name)
    {
        try { return target.GetType().InvokeMember(name, System.Reflection.BindingFlags.InvokeMethod, null, target, Array.Empty<object>()) as object[] ?? Array.Empty<object>(); }
        catch { return Array.Empty<object>(); }
    }
    static object[] ToObjectArray(object value)
    {
        if (value == null) return Array.Empty<object>();
        if (value is DispatchWrapper wrapper) return ToObjectArray(wrapper.WrappedObject);
        if (value is object[] objects) return objects;
        if (value is Array array)
        {
            var list = new List<object>();
            foreach (object item in array)
            {
                if (item is DispatchWrapper nestedWrapper)
                    list.Add(nestedWrapper.WrappedObject);
                else
                    list.Add(item);
            }
            return list.ToArray();
        }
        return new[] { value };
    }
    static double[] ToDoubleArray(object value)
    {
        if (value == null) return Array.Empty<double>();
        if (value is double[] doubles) return doubles;
        if (value is Array array)
        {
            var list = new List<double>();
            foreach (object item in array)
            {
                if (item == null) continue;
                list.Add(Convert.ToDouble(item));
            }
            return list.ToArray();
        }
        return Array.Empty<double>();
    }
    static object InvokeCom(object target, string name, params object[] args)
    {
        try { return target?.GetType().InvokeMember(name, System.Reflection.BindingFlags.InvokeMethod, null, target, args ?? Array.Empty<object>()); }
        catch { return null; }
    }
    static object GetComProperty(object target, string name)
    {
        try { return target?.GetType().InvokeMember(name, System.Reflection.BindingFlags.GetProperty, null, target, Array.Empty<object>()); }
        catch { return null; }
    }
    static object GetComProperty(object target, string name, params object[] args)
    {
        try { return target?.GetType().InvokeMember(name, System.Reflection.BindingFlags.GetProperty, null, target, args ?? Array.Empty<object>()); }
        catch { return null; }
    }
    static bool TryInvokeCom(object target, string name, params object[] args)
    {
        try
        {
            target?.GetType().InvokeMember(name, System.Reflection.BindingFlags.InvokeMethod, null, target, args ?? Array.Empty<object>());
            return target != null;
        }
        catch { return false; }
    }
    static string Safe(Func<string> f, string d = "") { try { return f() ?? d; } catch { return d; } }
    static int SafeInt(Func<int> f, int d = -1) { try { return f(); } catch { return d; } }
    static double SafeDouble(Func<double> f, double d = 0) { try { return f(); } catch { return d; } }
    static double R(double v) => Math.Round(v, 6);
    static string AppendText(string a, string b) => string.IsNullOrWhiteSpace(a) ? b : a + "；" + b;
    static void Inc(Dictionary<int, int> d, int k) => d[k] = d.TryGetValue(k, out var v) ? v + 1 : 1;
}
