c# solidworks 标注攻牙

csharp 复制代码
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using SolidWorks.Interop.sldworks;
using SolidWorks.Interop.swconst;
using View = SolidWorks.Interop.sldworks.View;

namespace tools
{
    /// <summary>
    /// 工程图螺纹/孔:在零件中识别目标特征,将模型整圆边映射到视图可见边并创建直径尺寸。
    /// 支持选中「视图」或「零件/组件」(装配体工程图内点选零件);首次使用请看控制台 TypeName2 列表并调整 <see cref="TargetFeatureTypeNames"/>。
    /// 装饰螺纹(CosmeticThread)通常 <c>GetFaces()</c> 为 0,圆边优先来自 <see cref="CosmeticThreadFeatureData"/> 的 <c>Edge</c>;
    /// 若定义边读不到,可在工程图中用 <c>SelectByID2(..., \"CTHREAD\", ...)</c> 选中装饰螺纹再 <c>AddDimension2</c>(与录制宏一致)。
    /// 与 <see cref="benddim"/> 一致:创建尺寸前对关联零件调用全局明细注释文字格式;面数统计与圆边收集使用 <see cref="EnumerateFeatureFaces"/>,兼容 COM 仅返回单个 <see cref="Face"/> 的情形。
    /// 孔向导类特征(如 HoleWzd)名称中含 <c>M5</c>、<c>M8x1.25</c> 等时,可在直径尺寸创建后将显示文字改为该规格串(见 <see cref="ReplaceHoleWizardDiameterTextWithThreadLabel"/>)。
    /// </summary>
    public static class threadhol_dim
    {
        /// <summary>为 true 时仅打印特征 TypeName2 统计与明细,不创建尺寸。</summary>
        public static bool OnlyPrintFeatureTypeNames = false;

        /// <summary>为 true 时,TypeName2 包含子串 "thread"(忽略大小写)的特征也视为目标(便于覆盖本地化名称)。</summary>
        public static bool MatchTypeNameContainsThread = true;

        /// <summary>
        /// 为 true 时:对从孔向导类特征(HoleWzd / AdvHole / AdvancedHole)收集到的圆边创建直径尺寸后,
        /// 若特征名称中能解析出螺纹规格(如 <c>M5 螺纹孔1</c> → <c>M5</c>),则用 <see cref="DisplayDimension.SetText"/> 将尺寸显示改为该字符串(如由数值 Ø4.2 改为 <c>M5</c>)。
        /// </summary>
        public static bool ReplaceHoleWizardDiameterTextWithThreadLabel = true;

        /// <summary>
        /// 视为「螺纹孔相关」的特征 TypeName2(SolidWorks 英文环境常见值,请按控制台打印结果增删)。
        /// 常见:CosmeticThread、HoleWzd(孔向导)、AdvancedHole 等。
        /// </summary>
        public static readonly HashSet<string> TargetFeatureTypeNames = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
        {
            "CosmeticThread",
            "HoleWzd",
            "AdvHole",
            "AdvancedHole",
            "Thread",
        };

        const double DimensionTextPerpBaseOffsetM = 0.003;
        const double DimensionTextPerpStaggerStepM = 0.0025;
        const double CircleRadiusAbsTolM = 0.00015;
        const double CircleRadiusRelTol = 0.004;
        const double CircleCenterTolM = 0.00035;
        const int MaxSubFeatureWalkSteps = 50_000;

        /// <summary>圆弧视为「整圆孔边」的最小参数跨度(弧度),略小于 2π 以包容拓扑缝隙。</summary>
        const double MinCircleEdgeParamSpan = 5.5;

        /// <summary>从特征名称解析螺纹规格(如 M5、M8x1.25),用于孔向导类特征尺寸显示改写。</summary>
        static readonly Regex HoleFeatureThreadCalloutFromName = new(
            @"\b(M\s*[\d.]+(?:\s*[x×]\s*[\d.]+)?)\b",
            RegexOptions.IgnoreCase | RegexOptions.CultureInvariant | RegexOptions.Compiled);

        /// <summary>
        /// 兼容旧名:等价于 <see cref="DimensionThreadHolesFromDrawingSelection"/>。
        /// </summary>
        public static void DimensionSelectedViewThreadHoles(ISldWorks swApp) => DimensionThreadHolesFromDrawingSelection(swApp);

        /// <summary>
        /// 在工程图中根据当前选择标注螺纹/孔直径:可选中「视图」或「零件/组件」(装配体图内点选零件);亦支持通过面/边选择解析到零件实例。
        /// 螺纹特征由 <see cref="TargetFeatureTypeNames"/> / <see cref="MatchTypeNameContainsThread"/> 筛选;几何优先来自特征面(孔向导等),装饰螺纹则补充 <c>CosmeticThreadFeatureData.Edge</c>。
        /// </summary>
        public static void DimensionThreadHolesFromDrawingSelection(ISldWorks swApp)
        {
            bool restoreInputDimValOnCreate = false;
            bool prevInputDimValOnCreate = false;
            try
            {
                var swModel = (ModelDoc2)swApp.ActiveDoc;
                if (swModel == null)
                {
                    Console.WriteLine("[threadhol_dim] 没有活动文档");
                    return;
                }

                var swSelMgr = (SelectionMgr)swModel.SelectionManager;
                if (swSelMgr.GetSelectedObjectCount() < 1)
                {
                    Console.WriteLine("[threadhol_dim] 请先选择:工程图视图,或装配体视图中的零件/组件(亦可选中零件上的面或圆边)。");
                    return;
                }

                if (!TryResolveThreadHoleDrawingSelection(swModel, swSelMgr, out var view, out var partDoc, out var restrictComp, out var resolveMsg))
                {
                    Console.WriteLine("[threadhol_dim] " + resolveMsg);
                    return;
                }

                ApplyGlobalAnnotationTextHeight((ModelDoc2)partDoc);

                prevInputDimValOnCreate = swApp.GetUserPreferenceToggle((int)swUserPreferenceToggle_e.swInputDimValOnCreate);
                swApp.SetUserPreferenceToggle((int)swUserPreferenceToggle_e.swInputDimValOnCreate, false);
                restoreInputDimValOnCreate = true;
                swApp.CommandInProgress = true;

                ExitDrawingSketchIfActive(swModel);

                var typeCounts = new Dictionary<string, int>(StringComparer.Ordinal);
                var lines = new List<string>();
                WalkPartFeatureTree((Feature?)partDoc.FirstFeature(), typeCounts, lines, 0);

                Console.WriteLine("========== [threadhol_dim] 关联零件中特征 TypeName2(去重统计)==========");
                var sortedTypes = new List<string>(typeCounts.Keys);
                sortedTypes.Sort(StringComparer.Ordinal);
                foreach (var t in sortedTypes)
                    Console.WriteLine($"  {t}  ×{typeCounts[t]}");
                Console.WriteLine("========== [threadhol_dim] 明细(深度 前缀 TypeName2 · 名称)==========");
                foreach (var line in lines)
                    Console.WriteLine(line);

                if (OnlyPrintFeatureTypeNames)
                {
                    LogTargetFeaturesThreadFaceSummary((ModelDoc2)partDoc, partDoc, view, restrictComp);
                    Console.WriteLine("[threadhol_dim] OnlyPrintFeatureTypeNames=true,已跳过标注。需要出尺寸时请设为 false 并调整 TargetFeatureTypeNames。");
                    Console.WriteLine("[threadhol_dim] 提示:孔向导等从特征面扫整圆模型边,再映射视图可见边出直径(非 CTHREAD);装饰螺纹定义边读不到时才用下方「CTHREAD 候选名」SelectByID2,可与录制宏第一参数对照。");
                    return;
                }

                var candidates = new List<Feature>();
                CollectTargetFeaturesFromPart(partDoc, candidates);

                if (candidates.Count == 0)
                {
                    Console.WriteLine("[threadhol_dim] 未找到匹配 TargetFeatureTypeNames / Thread 子串的特征,请根据上方 TypeName 列表调整筛选条件。");
                    return;
                }

                Console.WriteLine($"[threadhol_dim] 匹配到 {candidates.Count} 个候选特征,开始收集圆边...");

                var partModel = (ModelDoc2)partDoc;
                var circleDimTargets = BuildThreadHoleCircleDimTargets(partModel, candidates);

                Console.WriteLine($"[threadhol_dim] 去重后模型圆边 {circleDimTargets.Count} 条");

                double textStagger = 0;
                int ok = 0;
                int fail = 0;

                var drawingDoc = (DrawingDoc)swModel;
                foreach (var feat in candidates)
                {
                    if (!IsCosmeticThreadType(feat))
                        continue;
                    if (TryHasUsableCosmeticThreadDefinitionCircleEdge(partModel, feat))
                        continue;

                    if (TryAddCosmeticThreadDiameterViaCthreadSelect(
                            swModel, drawingDoc, view, partDoc, feat, restrictComp, ref textStagger))
                    {
                        ok++;
                    }
                    else
                    {
                        fail++;
                        Console.WriteLine($"[threadhol_dim] 装饰螺纹 CTHREAD 捷径未成功: {feat.Name}(可对照宏中 SelectByID2 全名字符串)");
                    }
                }

                foreach (var t in circleDimTargets)
                {
                    var vis = FindVisibleCircleEdge(view, t.ModelEdge, restrictComp);
                    if (vis == null)
                    {
                        fail++;
                        LogCircleSkip(t.ModelEdge, restrictComp != null ? "可见边中未匹配到同圆(已限定零件实例)" : "可见边中未匹配到同圆");
                        continue;
                    }

                    if (!TryAddDiameterDimension(swApp, swModel, view, t.ModelEdge, vis, t.ThreadCalloutDisplay, ref textStagger))
                    {
                        fail++;
                        continue;
                    }

                    ok++;
                }

                Console.WriteLine($"[threadhol_dim] 完成:成功 {ok},失败 {fail}。");
            }
            finally
            {
                if (restoreInputDimValOnCreate)
                {
                    try
                    {
                        swApp.SetUserPreferenceToggle((int)swUserPreferenceToggle_e.swInputDimValOnCreate, prevInputDimValOnCreate);
                    }
                    catch
                    {
                        // ignored
                    }
                }

                try
                {
                    swApp.CommandInProgress = false;
                }
                catch
                {
                    // ignored
                }
            }
        }

        /// <summary>
        /// 解析工程图选择:返回目标 <see cref="PartDoc"/>、用于几何投影的 <see cref="View"/>,以及在装配体视图中限定可见边搜索范围的组件(零件图时为 null)。
        /// </summary>
        static bool TryResolveThreadHoleDrawingSelection(
            ModelDoc2 swModel,
            SelectionMgr swSelMgr,
            out View view,
            out PartDoc partDoc,
            out Component2? restrictComp,
            out string resolveMsg)
        {
            view = null!;
            partDoc = null!;
            restrictComp = null;
            resolveMsg = "";

            int selType = swSelMgr.GetSelectedObjectType3(1, -1);

            if (selType == (int)swSelectType_e.swSelDRAWINGVIEWS)
            {
                var v = (View)swSelMgr.GetSelectedObject(1);
                var refDoc = v.ReferencedDocument;
                if (refDoc == null)
                {
                    resolveMsg = "无法获取视图关联文档。";
                    return false;
                }

                if (refDoc is not PartDoc pd)
                {
                    resolveMsg = "当前选中视图的引用模型不是零件。装配体工程图请在视图中选中「零件/组件」,或使用显示单个零件的视图。";
                    return false;
                }

                view = v;
                partDoc = pd;
                restrictComp = null;
                return true;
            }

            if (selType == (int)swSelectType_e.swSelCOMPONENTS)
            {
                var comp = (Component2)swSelMgr.GetSelectedObject(1);
                return TryResolveFromDrawingComponent(swModel, swSelMgr, comp, 1, out view, out partDoc, out restrictComp, out resolveMsg);
            }

            if (selType == (int)swSelectType_e.swSelFACES
                || selType == (int)swSelectType_e.swSelEDGES)
            {
                Component2? comp = null;
                try
                {
                    comp = swSelMgr.GetSelectedObjectsComponent3(1, -1);
                }
                catch
                {
                    comp = null;
                }

                if (comp == null)
                {
                    resolveMsg = "无法从当前面/边选择解析到零件组件,请改为在视图中选中零件实例。";
                    return false;
                }

                return TryResolveFromDrawingComponent(swModel, swSelMgr, comp, 1, out view, out partDoc, out restrictComp, out resolveMsg);
            }

            resolveMsg = $"不支持的选中类型 ({(swSelectType_e)selType})。请选择视图、零件/组件,或零件上的面/圆边。";
            return false;
        }

        static bool TryResolveFromDrawingComponent(
            ModelDoc2 swModel,
            SelectionMgr swSelMgr,
            Component2 comp,
            int selectionIndex,
            out View view,
            out PartDoc partDoc,
            out Component2? restrictComp,
            out string resolveMsg)
        {
            view = null!;
            partDoc = null!;
            restrictComp = comp;
            resolveMsg = "";

            ModelDoc2? modelDoc = null;
            try
            {
                modelDoc = comp.GetModelDoc2() as ModelDoc2;
            }
            catch
            {
                modelDoc = null;
            }

            if (modelDoc is not PartDoc pd)
            {
                resolveMsg = "选中项不是零件级组件(例如子装配体)。请展开后选择具体零件。";
                return false;
            }

            partDoc = pd;

            View? v = null;
            try
            {
                v = swSelMgr.GetSelectedObjectsDrawingView2(selectionIndex, -1) as View;
            }
            catch
            {
                v = null;
            }

            if (v == null && swModel is DrawingDoc)
                v = TryFindViewContainingComponent(swModel, comp);

            if (v == null)
            {
                resolveMsg = "无法确定该零件所在的工程图视图(可尝试在图形区域从该视图中重新选择零件)。";
                return false;
            }

            view = v;
            return true;
        }

        static View? TryFindViewContainingComponent(ModelDoc2 swModel, Component2 target)
        {
            try
            {
                var dd = (DrawingDoc)swModel;
                var sheet = (Sheet)dd.GetCurrentSheet();
                if (sheet == null)
                    return null;
                var views = (object[])sheet.GetViews();
                if (views == null)
                    return null;
                foreach (View v in views)
                {
                    if (v == null)
                        continue;
                    if (ViewShowsComponent(v, target))
                        return v;
                }
            }
            catch
            {
                // ignored
            }

            return null;
        }

        static bool ViewShowsComponent(View view, Component2 target)
        {
            object? visObj = null;
            try
            {
                visObj = view.GetVisibleComponents();
            }
            catch
            {
                return false;
            }

            if (visObj is not object[] roots)
                return false;
            foreach (object? o in roots)
            {
                if (o is Component2 root && ComponentSubtreeContains(root, target))
                    return true;
            }

            return false;
        }

        static bool ComponentSubtreeContains(Component2 root, Component2 target)
        {
            if (root == null || target == null)
                return false;
            if (NamesEqualInstance(root, target))
                return true;
            object? chObj = null;
            try
            {
                chObj = root.GetChildren();
            }
            catch
            {
                return false;
            }

            if (chObj is not object[] children)
                return false;
            foreach (object? c in children)
            {
                if (c is Component2 child && ComponentSubtreeContains(child, target))
                    return true;
            }

            return false;
        }

        static bool NamesEqualInstance(Component2 a, Component2 b)
        {
            string na = "";
            string nb = "";
            try
            {
                na = a.Name2 ?? "";
            }
            catch
            {
                na = "";
            }

            try
            {
                nb = b.Name2 ?? "";
            }
            catch
            {
                nb = "";
            }

            return string.Equals(na, nb, StringComparison.Ordinal);
        }

        static void LogTargetFeaturesThreadFaceSummary(
            ModelDoc2 partModel,
            PartDoc partDoc,
            View view,
            Component2? restrictComp)
        {
            var candidates = new List<Feature>();
            CollectTargetFeaturesFromPart(partDoc, candidates);
            Console.WriteLine("========== [threadhol_dim] 螺纹/孔候选特征:面数与几何来源 ==========");
            if (candidates.Count == 0)
            {
                Console.WriteLine("  (无候选特征)");
                return;
            }

            foreach (var feat in candidates)
            {
                int faceCount;
                try
                {
                    faceCount = EnumerateFeatureFaces(feat).Count();
                }
                catch
                {
                    faceCount = -1;
                }

                string tn = "";
                try
                {
                    tn = feat.GetTypeName2() ?? "";
                }
                catch
                {
                    tn = "?";
                }

                string nm = "";
                try
                {
                    nm = feat.Name ?? "";
                }
                catch
                {
                    nm = "";
                }

                string extra = "";
                if (string.Equals(tn, "CosmeticThread", StringComparison.OrdinalIgnoreCase))
                    extra = "  " + DescribeCosmeticThreadDefinitionEdge(partModel, feat);
                else if (IsHoleLikeThreadSourceFeature(feat))
                    extra = "  " + DescribeHoleLikeFeatureCircleEdgesFromFaces(feat);

                Console.WriteLine($"  {tn} · {nm}  →  GetFaces 面数: {faceCount}{extra}");

                if (string.Equals(tn, "CosmeticThread", StringComparison.OrdinalIgnoreCase)
                    && !TryHasUsableCosmeticThreadDefinitionCircleEdge(partModel, feat))
                {
                    var ids = BuildCthreadSelectByIdNameCandidates(feat, view, partDoc, restrictComp);
                    if (ids.Count > 0)
                    {
                        Console.WriteLine("    → CTHREAD SelectByID2 候选(第二参 Type 填 CTHREAD):");
                        foreach (var id in ids)
                            Console.WriteLine("       \"" + id + "\"");
                    }
                    else
                    {
                        Console.WriteLine("    → CTHREAD:当前规则未生成候选全名(请检查视图/引用零件)。");
                    }
                }
            }
        }

        /// <summary>装饰螺纹不返回 BREP 面时,说明其依附圆边是否可读。</summary>
        static string DescribeCosmeticThreadDefinitionEdge(ModelDoc2 partModel, Feature feat)
        {
            object? defObj = null;
            try
            {
                defObj = feat.GetDefinition();
            }
            catch
            {
                return "· CosmeticThread 定义: 无法 GetDefinition";
            }

            if (defObj is not CosmeticThreadFeatureData ctData)
                return "· CosmeticThread 定义: 类型非 CosmeticThreadFeatureData";

            Edge? edge = TryGetCosmeticThreadAttachmentEdge(partModel, ctData);
            if (edge == null)
                return "· CosmeticThread 定义 Edge: 未能读取(标注阶段将尝试 CTHREAD/SelectByID2 捷径)";

            if (!IsNearFullCircleEdge(edge, out var curve) || curve == null)
                return "· CosmeticThread 定义 Edge: 存在但非整圆(或参数跨度不足)";

            try
            {
                var cp = (double[])curve.CircleParams;
                if (cp != null && cp.Length >= 7)
                    return $"· 定义整圆边 R={cp[6] * 1000:F3} mm";
            }
            catch
            {
                // ignored
            }

            return "· CosmeticThread 定义 Edge: 整圆(半径未读)";
        }

        /// <summary>孔向导类特征有 BREP 面时,与标注阶段一致地从面收集整圆边,便于与「仅 CTHREAD」区分。</summary>
        static string DescribeHoleLikeFeatureCircleEdgesFromFaces(Feature feat)
        {
            var edges = new List<Edge>();
            CollectNearFullCircleEdgesFromFeature(feat, edges);
            if (edges.Count == 0)
                return "· 从特征面收集整圆边: 0 条(标注阶段无可用圆边)";

            var radiiMm = new List<double>(edges.Count);
            foreach (var edge in edges)
            {
                if (!IsNearFullCircleEdge(edge, out var curve) || curve == null)
                    continue;
                try
                {
                    var cp = (double[])curve.CircleParams;
                    if (cp != null && cp.Length >= 7)
                        radiiMm.Add(cp[6] * 1000.0);
                }
                catch
                {
                    // ignored
                }
            }

            string tail = "";
            if (radiiMm.Count > 0)
            {
                radiiMm.Sort();
                const int maxShow = 16;
                var parts = new List<string>(Math.Min(radiiMm.Count, maxShow));
                for (int i = 0; i < radiiMm.Count && i < maxShow; i++)
                    parts.Add(radiiMm[i].ToString("F3"));
                tail = " · R(mm)=" + string.Join(", ", parts);
                if (radiiMm.Count > maxShow)
                    tail += "...";
            }

            return $"· 从特征面收集整圆边: {edges.Count} 条(模型边→视图可见边,不用 CTHREAD){tail}";
        }

        /// <summary>从孔/螺纹类特征收集用于标直径的模型整圆边:实体面边 + 装饰螺纹定义边。</summary>
        static void CollectThreadHoleCircleEdgesFromFeature(ModelDoc2 partModel, Feature feat, List<Edge> sink)
        {
            CollectNearFullCircleEdgesFromFeature(feat, sink);
            TryCollectCosmeticThreadCircleEdgeFromDefinition(partModel, feat, sink);
        }

        /// <summary>特征名称是否视为装饰螺纹线计数用(含「装饰螺纹线」及常见误写「孔螺蚊线」)。</summary>
        static bool NameLooksLikeDecorativeCosmeticThreadLine(Feature f)
        {
            string n = "";
            try
            {
                n = f.Name ?? "";
            }
            catch
            {
                return false;
            }

            return n.IndexOf("装饰螺纹线", StringComparison.OrdinalIgnoreCase) >= 0
                || n.IndexOf("孔螺蚊线", StringComparison.OrdinalIgnoreCase) >= 0;
        }

        /// <summary>在孔向导子树中数 CosmeticThread 且名称匹配 <see cref="NameLooksLikeDecorativeCosmeticThreadLine"/>;子树为 0 时回退为候选中名称含「孔螺蚊线」的个数,仍为 0 再数「装饰螺纹线」。</summary>
        static int CountHoleQuantityFromDecorativeCosmeticThreadLine(Feature holeLike, List<Feature> candidates)
        {
            int sub = CountDecorativeCosmeticThreadLineUnderFeature(holeLike);
            if (sub > 0)
                return sub;
            int nk = CountCosmeticCandidatesNameContains(candidates, "孔螺蚊线");
            if (nk > 0)
                return nk;
            return CountCosmeticCandidatesNameContains(candidates, "装饰螺纹线");
        }

        static int CountCosmeticCandidatesNameContains(List<Feature> cand, string fragment)
        {
            int n = 0;
            foreach (var c in cand)
            {
                if (!IsCosmeticThreadType(c))
                    continue;
                string name = "";
                try
                {
                    name = c.Name ?? "";
                }
                catch
                {
                    continue;
                }

                if (name.IndexOf(fragment, StringComparison.OrdinalIgnoreCase) >= 0)
                    n++;
            }

            return n;
        }

        static int CountDecorativeCosmeticThreadInSubtreeRecursive(Feature feat, ref int steps)
        {
            if (feat == null || steps >= MaxSubFeatureWalkSteps)
                return 0;
            steps++;
            int n = 0;
            try
            {
                if (string.Equals(feat.GetTypeName2() ?? "", "CosmeticThread", StringComparison.OrdinalIgnoreCase)
                    && NameLooksLikeDecorativeCosmeticThreadLine(feat))
                    n++;
            }
            catch
            {
                // ignored
            }

            Feature? ch = (Feature?)feat.GetFirstSubFeature();
            while (ch != null && steps < MaxSubFeatureWalkSteps)
            {
                n += CountDecorativeCosmeticThreadInSubtreeRecursive(ch, ref steps);
                ch = (Feature?)ch.GetNextSubFeature();
            }

            return n;
        }

        /// <summary>在 <paramref name="root"/> 的直接子特征及其子树中,统计名称含「装饰螺纹线」或「孔螺蚊线」的 CosmeticThread 个数。</summary>
        static int CountDecorativeCosmeticThreadLineUnderFeature(Feature root)
        {
            int steps = 0;
            int n = 0;
            Feature? sub = (Feature?)root.GetFirstSubFeature();
            while (sub != null && steps < MaxSubFeatureWalkSteps)
            {
                n += CountDecorativeCosmeticThreadInSubtreeRecursive(sub, ref steps);
                sub = (Feature?)sub.GetNextSubFeature();
            }

            return n;
        }

        /// <summary>
        /// 每个候选特征最多一条圆边尺寸(首条整圆边);存在孔向导类候选时不标装饰螺纹定义圆边,以免与孔向导重复。
        /// 多个孔向导特征名称解析出<strong>同一规格</strong>(如均为 <c>M5</c>)时只标第一次遇到的。
        /// 孔向导尺寸显示:子树或候选中名称含「装饰螺纹线」(及常见误写「孔螺蚊线」)的 CosmeticThread 个数,大于 1 时显示 <c>4-M5</c>。
        /// </summary>
        static List<(Edge ModelEdge, string? ThreadCalloutDisplay)> BuildThreadHoleCircleDimTargets(
            ModelDoc2 partModel,
            List<Feature> candidates)
        {
            bool anyHoleLike = false;
            foreach (var f in candidates)
            {
                if (IsHoleLikeThreadSourceFeature(f))
                {
                    anyHoleLike = true;
                    break;
                }
            }

            var seenHoleCallout = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
            var pairs = new List<(Edge e, string? c)>();

            foreach (var feat in candidates)
            {
                var edges = new List<Edge>();
                if (anyHoleLike && IsCosmeticThreadType(feat))
                    CollectNearFullCircleEdgesFromFeature(feat, edges);
                else
                    CollectThreadHoleCircleEdgesFromFeature(partModel, feat, edges);

                string? callout = ReplaceHoleWizardDiameterTextWithThreadLabel
                    ? TryParseThreadCalloutFromHoleLikeFeature(feat)
                    : null;

                if (IsHoleLikeThreadSourceFeature(feat) && !string.IsNullOrWhiteSpace(callout))
                {
                    if (seenHoleCallout.Contains(callout))
                        continue;
                    seenHoleCallout.Add(callout);
                }

                Edge? pick = null;
                foreach (var e in edges)
                {
                    if (IsNearFullCircleEdge(e, out _))
                    {
                        pick = e;
                        break;
                    }
                }

                if (pick == null)
                    continue;

                string? threadDisplay = callout;
                if (IsHoleLikeThreadSourceFeature(feat) && !string.IsNullOrWhiteSpace(callout))
                {
                    int qty = CountHoleQuantityFromDecorativeCosmeticThreadLine(feat, candidates);
                    if (qty > 1)
                        threadDisplay = $"{qty}-{callout}";
                }

                pairs.Add((pick, threadDisplay));
            }

            var merged = new List<(Edge ModelEdge, string? ThreadCalloutDisplay)>();
            foreach (var (e, c) in pairs)
            {
                int idx = -1;
                for (int i = 0; i < merged.Count; i++)
                {
                    if (ReferenceEquals(merged[i].ModelEdge, e))
                    {
                        idx = i;
                        break;
                    }
                }

                if (idx < 0)
                {
                    merged.Add((e, c));
                    continue;
                }

                if (c != null && merged[idx].ThreadCalloutDisplay == null)
                    merged[idx] = (e, c);
            }

            return merged;
        }

        static bool IsHoleLikeThreadSourceFeature(Feature feat)
        {
            if (feat == null)
                return false;
            string tn;
            try
            {
                tn = feat.GetTypeName2() ?? "";
            }
            catch
            {
                return false;
            }

            return string.Equals(tn, "HoleWzd", StringComparison.OrdinalIgnoreCase)
                || string.Equals(tn, "AdvHole", StringComparison.OrdinalIgnoreCase)
                || string.Equals(tn, "AdvancedHole", StringComparison.OrdinalIgnoreCase);
        }

        /// <summary>从孔向导类特征名称提取 <c>M5</c>、<c>M8x1.25</c> 等(忽略大小写与空格)。</summary>
        static string? TryParseThreadCalloutFromHoleLikeFeature(Feature feat)
        {
            if (!IsHoleLikeThreadSourceFeature(feat))
                return null;
            string name;
            try
            {
                name = feat.Name ?? "";
            }
            catch
            {
                return null;
            }

            if (string.IsNullOrWhiteSpace(name))
                return null;
            var m = HoleFeatureThreadCalloutFromName.Match(name);
            if (!m.Success)
                return null;
            string raw = m.Groups[1].Value;
            if (string.IsNullOrWhiteSpace(raw))
                return null;
            string compact = raw.Replace(" ", "").Replace("×", "x");
            return string.IsNullOrWhiteSpace(compact) ? null : compact;
        }

        /// <summary>
        /// SolidWorks 中 <c>CosmeticThread</c> 常无 <c>GetFaces()</c>,依附孔口圆边保存在 <see cref="CosmeticThreadFeatureData.Edge"/>。
        /// </summary>
        static void TryCollectCosmeticThreadCircleEdgeFromDefinition(ModelDoc2 partModel, Feature feat, List<Edge> sink)
        {
            if (partModel == null)
                return;
            string tn = "";
            try
            {
                tn = feat.GetTypeName2() ?? "";
            }
            catch
            {
                return;
            }

            if (!string.Equals(tn, "CosmeticThread", StringComparison.OrdinalIgnoreCase))
                return;

            object? defObj = null;
            try
            {
                defObj = feat.GetDefinition();
            }
            catch
            {
                return;
            }

            if (defObj is not CosmeticThreadFeatureData ctData)
                return;

            Edge? edge = TryGetCosmeticThreadAttachmentEdge(partModel, ctData);
            if (edge == null)
                return;
            if (!IsNearFullCircleEdge(edge, out _))
                return;
            if (!ContainsEdgeByReference(sink, edge))
                sink.Add(edge);
        }

        static Edge? TryGetCosmeticThreadAttachmentEdge(ModelDoc2 partModel, CosmeticThreadFeatureData ctData)
        {
            try
            {
                var e = ctData.Edge;
                if (e != null)
                    return e;
            }
            catch
            {
                // 部分版本需 AccessSelections 后才可读 Edge
            }

            try
            {
                try
                {
                    partModel.ClearSelection2(true);
                }
                catch
                {
                    // ignored
                }

                if (!ctData.AccessSelections(partModel, null))
                    return null;
                try
                {
                    return ctData.Edge;
                }
                finally
                {
                    try
                    {
                        ctData.ReleaseSelectionAccess();
                    }
                    catch
                    {
                        // ignored
                    }
                }
            }
            catch
            {
                return null;
            }
        }

        const string DrawingCosmeticThreadSelectType = "CTHREAD";

        static bool IsCosmeticThreadType(Feature feat)
        {
            try
            {
                return string.Equals(feat.GetTypeName2() ?? "", "CosmeticThread", StringComparison.OrdinalIgnoreCase);
            }
            catch
            {
                return false;
            }
        }

        /// <summary>装饰螺纹是否已能通过零件定义边走「圆边→视图边」标注路径。</summary>
        static bool TryHasUsableCosmeticThreadDefinitionCircleEdge(ModelDoc2 partModel, Feature feat)
        {
            if (!IsCosmeticThreadType(feat))
                return false;
            object? defObj = null;
            try
            {
                defObj = feat.GetDefinition();
            }
            catch
            {
                return false;
            }

            if (defObj is not CosmeticThreadFeatureData ctData)
                return false;
            Edge? edge = TryGetCosmeticThreadAttachmentEdge(partModel, ctData);
            return edge != null && IsNearFullCircleEdge(edge, out _);
        }

        /// <summary>
        /// 工程图宏常用路径:<c>ActivateView</c> 后用 <c>SelectByID2(全名, CTHREAD, x,y,z)</c> 选中装饰螺纹,再 <c>AddDimension2</c>。
        /// 全名常见形式:<c>特征名@零件名-配置名@视图名</c>(零件图)或 <c>特征名@Component.Name2@视图名</c>(装配图)。
        /// </summary>
        static bool TryAddCosmeticThreadDiameterViaCthreadSelect(
            ModelDoc2 drwModel,
            DrawingDoc drawingDoc,
            View view,
            PartDoc partDoc,
            Feature cosmeticFeat,
            Component2? restrictComp,
            ref double textStaggerM)
        {
            ExitDrawingSketchIfActive(drwModel);
            try
            {
                drwModel.ClearSelection2(true);
            }
            catch
            {
                // ignored
            }

            TryActivateDrawingSheetAndView(drawingDoc, view);

            var names = BuildCthreadSelectByIdNameCandidates(cosmeticFeat, view, partDoc, restrictComp);
            if (names.Count == 0)
                return false;

            double[] pickZs = { 0, 500, -500 };
            double[] outline = Array.Empty<double>();
            try
            {
                var o = (double[])view.GetOutline();
                if (o != null && o.Length >= 4)
                    outline = o;
            }
            catch
            {
                outline = Array.Empty<double>();
            }

            double cx = 0, cy = 0;
            bool haveCenter = false;
            if (outline.Length >= 4)
            {
                cx = (outline[0] + outline[2]) * 0.5;
                cy = (outline[1] + outline[3]) * 0.5;
                haveCenter = true;
            }

            foreach (string id in names)
            {
                foreach (double pz in pickZs)
                {
                    foreach (var pick in EnumerateCthreadPickPoints(haveCenter, cx, cy, pz))
                    {
                        try
                        {
                            drwModel.ClearSelection2(true);
                        }
                        catch
                        {
                            // ignored
                        }

                        bool sel;
                        try
                        {
                            sel = drwModel.Extension.SelectByID2(
                                id,
                                DrawingCosmeticThreadSelectType,
                                pick.x,
                                pick.y,
                                pick.z,
                                false,
                                0,
                                null,
                                0);
                        }
                        catch
                        {
                            sel = false;
                        }

                        if (!sel)
                            continue;

                        if (!TryGetCthreadDimensionLeaderSheetXY(view, ref textStaggerM, out var dx, out var dy))
                        {
                            try
                            {
                                drwModel.ClearSelection2(true);
                            }
                            catch
                            {
                                // ignored
                            }

                            continue;
                        }

                        object? dim = null;
                        try
                        {
                            dim = drwModel.AddDimension2(dx, dy, 0);
                        }
                        catch
                        {
                            dim = null;
                        }

                        try
                        {
                            drwModel.ClearSelection2(true);
                        }
                        catch
                        {
                            // ignored
                        }

                        if (dim != null)
                        {
                            Console.WriteLine($"[threadhol_dim] CTHREAD 捷径成功: {id}");
                            return true;
                        }
                    }
                }
            }

            return false;
        }

        static IEnumerable<(double x, double y, double z)> EnumerateCthreadPickPoints(bool haveCenter, double cx, double cy, double pz)
        {
            if (haveCenter)
            {
                yield return (cx, cy, pz);
                yield return (cx + 0.0005, cy + 0.0005, pz);
                yield return (cx - 0.0005, cy - 0.0005, pz);
            }

            yield return (0, 0, pz);
        }

        static void TryActivateDrawingSheetAndView(DrawingDoc drawingDoc, View view)
        {
            try
            {
                var sheet = view.Sheet as Sheet;
                if (sheet != null)
                {
                    string sn = sheet.GetName();
                    if (!string.IsNullOrWhiteSpace(sn))
                        drawingDoc.ActivateSheet(sn);
                }
            }
            catch
            {
                // ignored
            }

            try
            {
                drawingDoc.ActivateView(view.GetName2());
            }
            catch
            {
                try
                {
                    drawingDoc.ActivateView(view.Name);
                }
                catch
                {
                    // ignored
                }
            }
        }

        static List<string> BuildCthreadSelectByIdNameCandidates(
            Feature feat,
            View view,
            PartDoc partDoc,
            Component2? restrictComp)
        {
            var set = new HashSet<string>(StringComparer.Ordinal);
            string featName = "";
            try
            {
                featName = feat.Name ?? "";
            }
            catch
            {
                featName = "";
            }

            if (string.IsNullOrWhiteSpace(featName))
                return new List<string>();

            var viewNames = new List<string>();
            void addViewName(string? s)
            {
                if (string.IsNullOrWhiteSpace(s))
                    return;
                s = s.Trim();
                if (!viewNames.Contains(s, StringComparer.Ordinal))
                    viewNames.Add(s);
            }

            try
            {
                addViewName(view.GetName2());
            }
            catch
            {
                // ignored
            }

            try
            {
                addViewName(view.Name);
            }
            catch
            {
                // ignored
            }

            if (viewNames.Count == 0)
                return new List<string>();

            string cfg = "";
            try
            {
                cfg = view.ReferencedConfiguration ?? "";
            }
            catch
            {
                cfg = "";
            }

            cfg = cfg.Trim();

            var refDoc = view.ReferencedDocument as ModelDoc2;
            if (refDoc == null)
                refDoc = (ModelDoc2)partDoc;
            string path = "";
            try
            {
                path = refDoc?.GetPathName() ?? "";
            }
            catch
            {
                path = "";
            }

            string baseName = "";
            if (!string.IsNullOrWhiteSpace(path))
                baseName = Path.GetFileNameWithoutExtension(path.Trim());
            if (string.IsNullOrWhiteSpace(baseName) && refDoc != null)
            {
                try
                {
                    baseName = TrimSolidWorksModelTitle(refDoc.GetTitle());
                }
                catch
                {
                    baseName = "";
                }
            }

            foreach (string vn in viewNames)
            {
                if (restrictComp != null)
                {
                    string compName = "";
                    try
                    {
                        compName = restrictComp.Name2 ?? "";
                    }
                    catch
                    {
                        compName = "";
                    }

                    if (!string.IsNullOrWhiteSpace(compName))
                        set.Add($"{featName}@{compName}@{vn}");
                }
                else
                {
                    if (!string.IsNullOrWhiteSpace(baseName))
                    {
                        if (!string.IsNullOrWhiteSpace(cfg) && !string.Equals(cfg, "默认", StringComparison.OrdinalIgnoreCase)
                            && !string.Equals(cfg, "Default", StringComparison.OrdinalIgnoreCase))
                        {
                            set.Add($"{featName}@{baseName}-{cfg}@{vn}");
                        }

                        set.Add($"{featName}@{baseName}@{vn}");
                    }
                }
            }

            return set.ToList();
        }

        static string TrimSolidWorksModelTitle(string title)
        {
            if (string.IsNullOrWhiteSpace(title))
                return "";
            string t = title.Trim();
            foreach (var ext in new[] { ".sldprt", ".SLDPRT", ".sldasm", ".SLDASM" })
            {
                if (t.EndsWith(ext, StringComparison.OrdinalIgnoreCase))
                    return t.Substring(0, t.Length - ext.Length);
            }

            return t;
        }

        static bool TryGetCthreadDimensionLeaderSheetXY(
            View view,
            ref double staggerAlongM,
            out double sheetX,
            out double sheetY)
        {
            sheetX = sheetY = 0;
            try
            {
                var o = (double[])view.GetOutline();
                if (o == null || o.Length < 4)
                    return false;
                double mx = (o[0] + o[2]) * 0.5;
                double my = (o[1] + o[3]) * 0.5;
                double offBase = DimensionTextPerpBaseOffsetM;
                sheetX = mx + staggerAlongM;
                sheetY = my + offBase;
                staggerAlongM += DimensionTextPerpStaggerStepM;
                return true;
            }
            catch
            {
                return false;
            }
        }

        static bool IsTargetFeature(Feature feat)
        {
            string tn = feat.GetTypeName2() ?? "";
            if (TargetFeatureTypeNames.Contains(tn))
                return true;
            if (MatchTypeNameContainsThread && tn.IndexOf("thread", StringComparison.OrdinalIgnoreCase) >= 0)
                return true;
            return false;
        }

        static void CollectTargetFeaturesFromPart(PartDoc partDoc, List<Feature> sink)
        {
            int steps = 0;
            Feature? f = (Feature?)partDoc.FirstFeature();
            while (f != null && steps < MaxSubFeatureWalkSteps)
            {
                CollectTargetFeaturesFromNode(f, sink, ref steps);
                f = (Feature?)f.GetNextFeature();
            }
        }

        static void CollectTargetFeaturesFromNode(Feature feat, List<Feature> sink, ref int steps)
        {
            if (steps >= MaxSubFeatureWalkSteps)
                return;
            steps++;
            if (IsTargetFeature(feat))
                sink.Add(feat);
            Feature? sub = (Feature?)feat.GetFirstSubFeature();
            while (sub != null && steps < MaxSubFeatureWalkSteps)
            {
                CollectTargetFeaturesFromNode(sub, sink, ref steps);
                sub = (Feature?)sub.GetNextSubFeature();
            }
        }

        static void WalkPartFeatureTree(Feature? top, Dictionary<string, int> typeCounts, List<string> lines, int depth)
        {
            int steps = 0;
            Feature? f = top;
            while (f != null && steps < MaxSubFeatureWalkSteps)
            {
                WalkFeatureNode(f, typeCounts, lines, depth, ref steps);
                f = (Feature?)f.GetNextFeature();
            }
        }

        static void WalkFeatureNode(Feature feat, Dictionary<string, int> typeCounts, List<string> lines, int depth, ref int steps)
        {
            if (steps >= MaxSubFeatureWalkSteps)
                return;
            steps++;
            string tn = feat.GetTypeName2() ?? "";
            if (!typeCounts.ContainsKey(tn))
                typeCounts[tn] = 0;
            typeCounts[tn]++;
            string pad = new string(' ', depth * 2);
            lines.Add($"{pad}[{depth}] {tn} · {feat.Name}");

            Feature? sub = (Feature?)feat.GetFirstSubFeature();
            while (sub != null && steps < MaxSubFeatureWalkSteps)
            {
                WalkFeatureNode(sub, typeCounts, lines, depth + 1, ref steps);
                sub = (Feature?)sub.GetNextSubFeature();
            }
        }

        static void CollectNearFullCircleEdgesFromFeature(Feature feat, List<Edge> sink)
        {
            foreach (Face face in EnumerateFeatureFaces(feat))
            {
                object? edgesObj = null;
                try
                {
                    edgesObj = face.GetEdges();
                }
                catch
                {
                    continue;
                }

                if (edgesObj is not object[] edgeArr)
                    continue;

                foreach (object? eo in edgeArr)
                {
                    if (eo is not Edge edge)
                        continue;
                    if (!IsNearFullCircleEdge(edge, out _))
                        continue;
                    if (!ContainsEdgeByReference(sink, edge))
                        sink.Add(edge);
                }
            }
        }

        static bool ContainsEdgeByReference(List<Edge> list, Edge e)
        {
            foreach (var x in list)
            {
                if (ReferenceEquals(x, e))
                    return true;
            }

            return false;
        }

        static bool IsNearFullCircleEdge(Edge edge, out Curve? curveOut)
        {
            curveOut = null;
            try
            {
                var curve = (Curve)edge.GetCurve();
                if (curve == null || !curve.IsCircle())
                    return false;
                curve.GetEndParams(out var t0, out var t1, out _, out _);
                double span = Math.Abs(t1 - t0);
                if (span < MinCircleEdgeParamSpan)
                    return false;
                curveOut = curve;
                return true;
            }
            catch
            {
                return false;
            }
        }

        static Edge? FindVisibleCircleEdge(View view, Edge modelEdge, Component2? restrictToComponent)
        {
            var mCurve = (Curve)modelEdge.GetCurve();
            if (mCurve == null || !mCurve.IsCircle())
                return null;
            var mCp = (double[])mCurve.CircleParams;
            if (mCp == null || mCp.Length < 7)
                return null;
            double rM = mCp[6];
            double cxM = mCp[0], cyM = mCp[1], czM = mCp[2];
            double radTol = Math.Max(CircleRadiusAbsTolM, rM * CircleRadiusRelTol);

            var visibleComps = (object[])view.GetVisibleComponents();
            if (visibleComps == null)
                return null;

            Edge? best = null;
            double bestScore = double.MaxValue;

            foreach (Component2 comp in visibleComps)
            {
                if (comp == null)
                    continue;
                if (restrictToComponent != null && !ComponentSubtreeContains(comp, restrictToComponent))
                    continue;
                var visibleEdges = (object[])view.GetVisibleEntities(
                    comp, (int)swViewEntityType_e.swViewEntityType_Edge);
                if (visibleEdges == null)
                    continue;

                foreach (object obj in visibleEdges)
                {
                    if (obj is not Edge visEdge)
                        continue;
                    if (ReferenceEquals(visEdge, modelEdge))
                        return visEdge;

                    try
                    {
                        var vCurve = (Curve)visEdge.GetCurve();
                        if (vCurve == null || !vCurve.IsCircle())
                            continue;
                        var vCp = (double[])vCurve.CircleParams;
                        if (vCp == null || vCp.Length < 7)
                            continue;
                        double rV = vCp[6];
                        if (Math.Abs(rV - rM) > radTol)
                            continue;
                        double dx = vCp[0] - cxM;
                        double dy = vCp[1] - cyM;
                        double dz = vCp[2] - czM;
                        double dCtr = Math.Sqrt(dx * dx + dy * dy + dz * dz);
                        if (dCtr > CircleCenterTolM)
                            continue;
                        double score = dCtr + Math.Abs(rV - rM);
                        if (score < bestScore)
                        {
                            bestScore = score;
                            best = visEdge;
                        }
                    }
                    catch
                    {
                        // continue
                    }
                }
            }

            return best;
        }

        static void LogCircleSkip(Edge modelEdge, string reason)
        {
            try
            {
                var c = (Curve)modelEdge.GetCurve();
                var cp = c != null ? (double[])c.CircleParams : null;
                if (cp != null && cp.Length >= 7)
                {
                    Console.WriteLine(
                        $"[threadhol_dim] 跳过:{reason} · R={cp[6] * 1000:F3} mm 中心(mm)({cp[0] * 1000:F2},{cp[1] * 1000:F2},{cp[2] * 1000:F2})");
                }
                else
                {
                    Console.WriteLine($"[threadhol_dim] 跳过:{reason}");
                }
            }
            catch
            {
                Console.WriteLine($"[threadhol_dim] 跳过:{reason}");
            }
        }

        static bool TryApplyThreadCalloutToDisplayDimension(DisplayDimension disp, string text)
        {
            if (disp == null || string.IsNullOrWhiteSpace(text))
                return false;
            try
            {
                disp.SetText((int)swDimensionTextParts_e.swDimensionTextAll, text);
                return true;
            }
            catch
            {
                // ignored
            }

            try
            {
                disp.SetText((int)swDimensionTextParts_e.swDimensionTextCalloutAbove, text);
                return true;
            }
            catch
            {
                // ignored
            }

            return false;
        }

        static bool TryAddDiameterDimension(
            ISldWorks swApp,
            ModelDoc2 swModel,
            View view,
            Edge modelCircleEdge,
            Edge visibleCircleEdge,
            string? threadCalloutDisplay,
            ref double textStaggerM)
        {
            ExitDrawingSketchIfActive(swModel);

            if (!TryCircleDiameterOppositePoints(modelCircleEdge, out var pA, out var pB))
            {
                Console.WriteLine("[threadhol_dim] 无法取圆直径端点,跳过。");
                return false;
            }

            if (!TryGetDimensionLeaderSheetXYFromTwoModelPoints(swApp, view, pA, pB, ref textStaggerM, out var x, out var y))
            {
                swModel.ClearSelection2(true);
                Console.WriteLine("[threadhol_dim] 尺寸文字位置计算失败。");
                return false;
            }

            var selMgr = (SelectionMgr)swModel.SelectionManager;
            var selData = selMgr.CreateSelectData();
            selData.View = view;
            ((Entity)visibleCircleEdge).Select4(true, selData);

            var dimObj = swModel.AddDimension2(x, y, 0);
            swModel.ClearSelection2(true);

            if (dimObj is not DisplayDimension disp)
            {
                Console.WriteLine("[threadhol_dim] AddDimension2 返回空(可检查视图是否显示该圆边)。");
                return false;
            }

            if (!string.IsNullOrWhiteSpace(threadCalloutDisplay))
            {
                if (TryApplyThreadCalloutToDisplayDimension(disp, threadCalloutDisplay.Trim()))
                    Console.WriteLine($"[threadhol_dim] 孔向导直径显示已改为: {threadCalloutDisplay.Trim()}");
                else
                    Console.WriteLine($"[threadhol_dim] 孔向导直径显示改写未成功,保留数值显示: {threadCalloutDisplay.Trim()}");
            }

            return true;
        }

        static bool TryCircleDiameterOppositePoints(Edge edge, out double[] a, out double[] b)
        {
            a = b = Array.Empty<double>();
            try
            {
                var curve = (Curve)edge.GetCurve();
                if (curve == null || !curve.IsCircle())
                    return false;
                var cp = (double[])curve.CircleParams;
                if (cp == null || cp.Length < 7)
                    return false;
                double cx = cp[0], cy = cp[1], cz = cp[2];
                double ax = cp[3], ay = cp[4], az = cp[5];
                double r = cp[6];
                double alen = Math.Sqrt(ax * ax + ay * ay + az * az);
                if (alen < 1e-12)
                    return false;
                ax /= alen;
                ay /= alen;
                az /= alen;

                double ux = 1, uy = 0, uz = 0;
                double cxu = Math.Abs(ax * ux + ay * uy + az * uz);
                if (cxu > 0.95)
                {
                    ux = 0;
                    uy = 1;
                    uz = 0;
                }

                double px = ay * uz - az * uy;
                double py = az * ux - ax * uz;
                double pz = ax * uy - ay * ux;
                double plen = Math.Sqrt(px * px + py * py + pz * pz);
                if (plen < 1e-12)
                    return false;
                px /= plen;
                py /= plen;
                pz /= plen;

                a = new[] { cx + px * r, cy + py * r, cz + pz * r };
                b = new[] { cx - px * r, cy - py * r, cz - pz * r };
                return true;
            }
            catch
            {
                return false;
            }
        }

        static bool TryModelPointToSheetXY(ISldWorks swApp, View view, double[] model3, out double sheetX, out double sheetY)
        {
            sheetX = sheetY = 0;
            try
            {
                var math = swApp.IGetMathUtility();
                if (math == null)
                    return false;
                var mp = (MathPoint)math.CreatePoint(model3);
                if (mp == null)
                    return false;
                var modelToSheet = (MathTransform)view.ModelToViewTransform;
                if (modelToSheet == null)
                    return false;
                mp = (MathPoint)mp.MultiplyTransform(modelToSheet);
                var arr = (double[])mp.ArrayData;
                if (arr == null || arr.Length < 2)
                    return false;
                sheetX = arr[0];
                sheetY = arr[1];
                return true;
            }
            catch
            {
                return false;
            }
        }

        static bool TryGetDimensionLeaderSheetXYFromTwoModelPoints(
            ISldWorks swApp,
            View view,
            double[] modelA,
            double[] modelB,
            ref double perpStaggerM,
            out double sheetX,
            out double sheetY)
        {
            sheetX = sheetY = 0;
            if (!TryModelPointToSheetXY(swApp, view, modelA, out var ax, out var ay))
                return false;
            if (!TryModelPointToSheetXY(swApp, view, modelB, out var bx, out var by))
                return false;
            double mx = (ax + bx) * 0.5;
            double my = (ay + by) * 0.5;
            double vx = bx - ax;
            double vy = by - ay;
            double len = Math.Sqrt(vx * vx + vy * vy);
            double nx, ny;
            if (len > 1e-12)
            {
                nx = -vy / len;
                ny = vx / len;
            }
            else
            {
                nx = 0;
                ny = 1;
            }

            double off = DimensionTextPerpBaseOffsetM + perpStaggerM;
            perpStaggerM += DimensionTextPerpStaggerStepM;
            sheetX = mx + nx * off;
            sheetY = my + ny * off;
            return true;
        }

        /// <summary>
        /// <c>Feature.GetFaces()</c> 可能为 null;COM 在仅一面时有时返回单个 <see cref="Face"/> 而非 object[](与 <see cref="benddim"/> 一致)。
        /// </summary>
        static IEnumerable<Face> EnumerateFeatureFaces(Feature feat)
        {
            if (feat == null) yield break;
            object raw;
            try
            {
                raw = feat.GetFaces();
            }
            catch
            {
                yield break;
            }

            if (raw == null) yield break;
            if (raw is object[] arr)
            {
                foreach (var o in arr)
                {
                    if (o is Face f) yield return f;
                }

                yield break;
            }

            if (raw is Face one) yield return one;
        }

        /// <summary>
        /// 使用零件文档全局明细注释文字格式,与折弯标注 <see cref="benddim"/> 一致,避免新建尺寸文字被局部样式托管覆盖。
        /// </summary>
        static void ApplyGlobalAnnotationTextHeight(ModelDoc2 partModel)
        {
            try
            {
                var myTextFormat = partModel.Extension.GetUserPreferenceTextFormat(
                    (int)swUserPreferenceTextFormat_e.swDetailingAnnotationTextFormat, 0) as TextFormat;
                if (myTextFormat == null) return;

                myTextFormat.CharHeight = 0.0035;
                bool boolstatus = partModel.Extension.SetUserPreferenceTextFormat(
                    (int)swUserPreferenceTextFormat_e.swDetailingAnnotationTextFormat, 0, myTextFormat);
                Console.WriteLine($"[threadhol_dim] 全局注释文字高度 0.0035 m,SetUserPreferenceTextFormat: {boolstatus}");
            }
            catch (Exception ex)
            {
                Console.WriteLine($"[threadhol_dim] 设置全局注释文字高度失败:{ex.Message}");
            }
        }

        static void ExitDrawingSketchIfActive(ModelDoc2 swModel)
        {
            try
            {
                try
                {
                    swModel.SketchManager.AddToDB = false;
                }
                catch
                {
                    // ignored
                }

                for (int i = 0; i < 6; i++)
                {
                    try
                    {
                        if (swModel.SketchManager.ActiveSketch == null)
                            break;
                        swModel.SketchManager.InsertSketch(false);
                    }
                    catch
                    {
                        break;
                    }
                }

                try
                {
                    swModel.ClearSelection2(true);
                }
                catch
                {
                    // ignored
                }
            }
            catch
            {
                // ignored
            }
        }
    }
}
相关推荐
吴声子夜歌1 小时前
Java——显示条件
java·开发语言
有味道的男人1 小时前
1688 商品价格 API:阶梯价、代发价、批发价实时查询
开发语言·windows·python
范范@1 小时前
python基础-for循环和列表
开发语言·python
小白学大数据1 小时前
Python 爬虫动态 JS 渲染与无头浏览器实战选型指南
开发语言·javascript·爬虫·python
朔北之忘 Clancy2 小时前
2026 年 3 月青少年软编等考 C 语言一级真题解析
c语言·开发语言·c++·学习·青少年编程·题解·一级
佳xuan2 小时前
模型训练之爬取数据
开发语言·python
之歆2 小时前
DAY_10 JavaScript 深度解析:原型链 · 引用类型 · 内置对象 · 数组方法全攻略(上)
开发语言·javascript·ecmascript
zmzb01032 小时前
Python课后习题训练记录Day122
开发语言·python
陳土2 小时前
R语言jiebaR包使用摘要
开发语言·r语言