c# solidworks 补画工程图里没显示的螺纹线

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

namespace tools
{
    public static partial class threadhol_dim
    {
        /// <summary>为 true 时优先从视图可见圆边定位攻牙孔(平板型式等)。</summary>
        public static bool TapHoleSpecArcUseVisibleEdges = true;

        /// <summary>攻牙孔规格圆弧:跳过已有同规格同心的视图草图圆弧(容差米)。</summary>
        public static double TapHoleSpecArcSkipExistingCenterTolM = 0.0004;

        /// <summary>每个攻牙孔绘制的圆弧张角(度),默认 270°。</summary>
        public static double TapHoleSpecArcSpanDegrees = 270.0;

        /// <summary>CreateArc 扫掠方向:1 或 -1,与 <see cref="TapHoleSpecArcSpanDegrees"/> 配合选取大弧。</summary>
        public static short TapHoleSpecArcSweepDirection = 1;

        static readonly Regex TapHoleNominalDiameterFromCallout = new(
            @"M\s*([\d.]+)",
            RegexOptions.IgnoreCase | RegexOptions.CultureInvariant | RegexOptions.Compiled);

        /// <summary>
        /// 工程图选中视图或零件:在攻牙孔位置于视图草图绘制规格直径圆弧(M4 → Ø4 mm 半圆等)。
        /// </summary>
        public static void InsertCosmeticThreadLinesFromDrawingSelection(ISldWorks swApp)
            => DrawTapHoleSpecDiameterArcsFromDrawingSelection(swApp);

        /// <summary>在指定视图上为攻牙孔绘制规格直径草图圆弧。</summary>
        public static void DrawTapHoleSpecDiameterArcsFromDrawingSelection(ISldWorks swApp)
        {
            try
            {
                var swModel = (ModelDoc2)swApp.ActiveDoc;
                if (swModel == null)
                {
                    Console.WriteLine("[tap_hole_arc] 没有活动文档");
                    return;
                }

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

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

                DrawTapHoleSpecDiameterArcsCore(swApp, swModel, view, partDoc, restrictComp);
            }
            catch (Exception ex)
            {
                Console.WriteLine($"[tap_hole_arc] 失败: {ex.Message}");
            }
        }

        /// <summary>在指定工程图视图上为攻牙孔绘制规格直径草图圆(自动化出图等)。</summary>
        public static void DrawTapHoleSpecDiameterArcsForDrawingView(ISldWorks swApp, View view, Component2? restrictComp = null)
        {
            if (view == null)
            {
                Console.WriteLine("[tap_hole_arc] 视图为空");
                return;
            }

            var swModel = (ModelDoc2)swApp.ActiveDoc;
            if (swModel == null)
            {
                Console.WriteLine("[tap_hole_arc] 没有活动文档");
                return;
            }

            if (view.ReferencedDocument is not PartDoc partDoc)
            {
                Console.WriteLine("[tap_hole_arc] 视图关联文档不是零件。");
                return;
            }

            DrawTapHoleSpecDiameterArcsCore(swApp, swModel, view, partDoc, restrictComp);
        }

        static void DrawTapHoleSpecDiameterArcsCore(
            ISldWorks swApp,
            ModelDoc2 drwModel,
            View view,
            PartDoc partDoc,
            Component2? restrictComp)
        {
            bool restoreCmd = false;
            try
            {
                swApp.CommandInProgress = true;
                restoreCmd = true;
                ExitDrawingSketchIfActive(drwModel);

                var partModel = (ModelDoc2)partDoc;
                var candidates = new List<Feature>();
                CollectTargetFeaturesFromPart(partDoc, candidates);
                if (candidates.Count == 0)
                {
                    Console.WriteLine("[tap_hole_arc] 未找到螺纹/孔向导/装饰螺纹特征。");
                    return;
                }

                List<(Edge Edge, string? Callout)> dimTargets;
                using (ActivateViewReferencedPartConfiguration(partModel, view, restrictComp))
                {
                    if (ViewNeedsFlatPatternConfiguration(view))
                    {
                        TryEnsureFlatPatternFeaturesUnsuppressed(partModel);
                        try
                        {
                            partModel.EditRebuild3();
                        }
                        catch
                        {
                            // ignored
                        }
                    }

                    dimTargets = TapHoleSpecArcUseVisibleEdges
                        ? BuildAllTapHoleArcDimTargets(swApp, view, partModel, candidates, restrictComp)
                        : BuildAllTapHoleCircleDimTargets(partModel, candidates);
                }

                var arcTargets = new List<(double[] CenterModel, double Nx, double Ny, double Nz, double DiameterMm, string Label)>();
                foreach (var (edge, callout) in dimTargets)
                {
                    if (!TryParseThreadNominalDiameterMm(callout, out double diameterMm))
                        continue;
                    if (!TryGetNearFullCircleCenterAndPlane(edge, out var center, out var nx, out var ny, out var nz))
                        continue;

                    string label = FormatTapHoleArcLabel(callout, diameterMm);
                    if (TryAddTapHoleArcTarget(arcTargets, center, nx, ny, nz, diameterMm, label))
                        Console.WriteLine($"[tap_hole_arc] 目标 {label} · 中心模型mm({center[0] * 1000:F2},{center[1] * 1000:F2},{center[2] * 1000:F2})");
                }

                if (arcTargets.Count == 0)
                {
                    Console.WriteLine("[tap_hole_arc] 无攻牙孔可绘制(需特征名/标注含 M 规格,如 M4、M5x0.8)。");
                    return;
                }

                var specRadiiM = new List<double>();
                foreach (var t in arcTargets)
                {
                    double r = t.DiameterMm * 0.0005;
                    if (!specRadiiM.Exists(x => Math.Abs(x - r) < 1e-9))
                        specRadiiM.Add(r);
                }

                int existingArcCount = CountTapHoleSpecArcsInViewSketch(view, specRadiiM);
                int expectedCount = arcTargets.Count;
                Console.WriteLine(
                    $"[tap_hole_arc] 视图已有螺纹圆弧 {existingArcCount} 个,实际攻牙孔 {expectedCount} 个");
                if (existingArcCount == expectedCount)
                {
                    Console.WriteLine("[tap_hole_arc] 螺纹圆弧数量已满足,跳过绘制。");
                    return;
                }

                int created = DrawTapHoleSpecArcsInViewSketch(swApp, (DrawingDoc)drwModel, drwModel, view, arcTargets);
                Console.WriteLine($"[tap_hole_arc] 视图「{view.Name}」完成:绘制 {created}/{arcTargets.Count} 个规格圆弧。");
            }
            finally
            {
                if (restoreCmd)
                {
                    try
                    {
                        swApp.CommandInProgress = false;
                    }
                    catch
                    {
                        // ignored
                    }
                }
            }
        }

        static string FormatTapHoleArcLabel(string? callout, double diameterMm)
        {
            if (!string.IsNullOrWhiteSpace(callout))
            {
                string s = callout.Trim();
                int dash = s.IndexOf('-');
                if (dash > 0 && dash < s.Length - 1 && char.IsDigit(s[0]))
                    s = s.Substring(dash + 1).Trim();
                if (TryParseThreadNominalDiameterMm(s, out _))
                    return s;
            }

            return $"M{diameterMm:0.##}";
        }

        /// <summary>每个攻牙孔一条目标:扫描全部可见圆边 + 平板展开实体补全(不按 M 规格去重)。</summary>
        static List<(Edge ModelEdge, string? ThreadCalloutDisplay)> BuildAllTapHoleArcDimTargets(
            ISldWorks swApp,
            View view,
            ModelDoc2 partModel,
            List<Feature> candidates,
            Component2? restrictComp)
        {
            var merged = BuildAllTapHoleArcDimTargetsFromVisibleEdges(view, partModel, candidates, restrictComp);

            if (ViewNeedsFlatPatternConfiguration(view))
            {
                int before = merged.Count;
                SupplementTapHoleArcTargetsFromFlatPatternBodies(partModel, candidates, merged);
                if (merged.Count > before)
                {
                    Console.WriteLine(
                        $"[tap_hole_arc] 展开实体按底孔半径补全 {merged.Count - before} 个孔(共 {merged.Count})");
                }
            }

            if (merged.Count == 0)
            {
                Console.WriteLine("[tap_hole_arc] 可见圆边/展开实体均未匹配,回退特征面圆边...");
                merged = BuildAllTapHoleCircleDimTargets(partModel, candidates);
            }

            return merged;
        }

        static List<(Edge ModelEdge, string? ThreadCalloutDisplay)> BuildAllTapHoleArcDimTargetsFromVisibleEdges(
            View view,
            ModelDoc2 partModel,
            List<Feature> candidates,
            Component2? restrictComp)
        {
            var merged = new List<(Edge ModelEdge, string? ThreadCalloutDisplay)>();
            var seenEdges = new List<Edge>();
            int scanned = 0;
            int matched = 0;

            foreach (Edge visEdge in EnumerateVisibleCircleEdges(view, restrictComp))
            {
                scanned++;
                if (ContainsEdgeByReference(seenEdges, visEdge))
                    continue;

                if (!TryResolveThreadHoleOwnerFromVisibleEdge(partModel, visEdge, candidates, out var owner))
                    continue;

                string? callout = TryParseThreadCalloutFromHoleLikeFeature(owner);
                if (string.IsNullOrWhiteSpace(callout))
                {
                    try
                    {
                        callout = owner.Name;
                    }
                    catch
                    {
                        callout = null;
                    }
                }

                if (!TryParseThreadNominalDiameterMm(callout, out _))
                    continue;

                matched++;
                seenEdges.Add(visEdge);
                merged.Add((visEdge, callout));
            }

            Console.WriteLine(
                $"[tap_hole_arc] 可见圆边 {scanned} 条,攻牙匹配 {matched} 条(未按规格去重)");
            return merged;
        }

        static void SupplementTapHoleArcTargetsFromFlatPatternBodies(
            ModelDoc2 partModel,
            List<Feature> candidates,
            List<(Edge ModelEdge, string? ThreadCalloutDisplay)> sink)
        {
            var specs = CollectKnownThreadCallouts(candidates);
            if (specs.Count == 0)
                return;

            var flatBodies = GetSheetMetalFlatPatternBodies(partModel);
            if (flatBodies.Count == 0)
                return;

            foreach (Body2 body in flatBodies)
            {
                foreach (Edge edge in EnumerateBodyNearFullCircleEdges(body))
                {
                    if (TapHoleArcTargetsContainEdge(sink, edge))
                        continue;
                    if (!TryGetCircleCenterRadiusM(edge, out _, out _, out _, out var edgeR))
                        continue;

                    foreach (string spec in specs)
                    {
                        if (!TryGetTapDrillRadiusMFromCallout(spec, out var expectedR))
                            continue;

                        double radTol = Math.Max(CircleRadiusAbsTolM, Math.Max(edgeR, expectedR) * CircleRadiusRelTol);
                        if (Math.Abs(edgeR - expectedR) > radTol)
                            continue;

                        sink.Add((edge, spec));
                        break;
                    }
                }
            }
        }

        static HashSet<string> CollectKnownThreadCallouts(List<Feature> candidates)
        {
            var set = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
            foreach (var feat in candidates)
            {
                string? callout = TryParseThreadCalloutFromHoleLikeFeature(feat);
                if (!string.IsNullOrWhiteSpace(callout))
                    set.Add(callout);
            }

            return set;
        }

        static bool TapHoleArcTargetsContainEdge(
            List<(Edge ModelEdge, string? ThreadCalloutDisplay)> sink,
            Edge edge)
        {
            if (ContainsEdgeByReference(sink.ConvertAll(t => t.ModelEdge), edge))
                return true;

            if (!TryGetCircleCenterRadiusM(edge, out var cx, out var cy, out var cz, out var r))
                return false;

            const double centerTol = 0.0002;
            foreach (var (existing, _) in sink)
            {
                if (!TryGetCircleCenterRadiusM(existing, out var ex, out var ey, out var ez, out var er))
                    continue;
                double dx = cx - ex;
                double dy = cy - ey;
                double dz = cz - ez;
                if (dx * dx + dy * dy + dz * dz <= centerTol * centerTol
                    && Math.Abs(r - er) <= Math.Max(CircleRadiusAbsTolM, r * CircleRadiusRelTol))
                    return true;
            }

            return false;
        }

        /// <summary>每个螺纹/孔特征各取一条整圆边(不按 M 规格去重,供规格圆逐个绘制)。</summary>
        static List<(Edge ModelEdge, string? ThreadCalloutDisplay)> BuildAllTapHoleCircleDimTargets(
            ModelDoc2 partModel,
            List<Feature> candidates)
        {
            bool anyHoleLike = false;
            foreach (var f in candidates)
            {
                if (IsHoleLikeThreadSourceFeature(f))
                {
                    anyHoleLike = true;
                    break;
                }
            }

            var pairs = new List<(Edge e, string? c)>();
            foreach (var feat in candidates)
            {
                if (anyHoleLike && IsCosmeticThreadType(feat))
                    continue;

                var edges = new List<Edge>();
                CollectThreadHoleCircleEdgesFromFeature(partModel, feat, edges);

                string? callout = TryParseThreadCalloutFromHoleLikeFeature(feat);
                if (string.IsNullOrWhiteSpace(callout))
                {
                    try
                    {
                        callout = feat.Name;
                    }
                    catch
                    {
                        callout = null;
                    }
                }

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

                if (pick == null)
                    continue;

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

            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));
                else if (c != null && merged[idx].ThreadCalloutDisplay == null)
                    merged[idx] = (e, c);
            }

            return merged;
        }

        /// <summary>从 <c>M4</c>、<c>4-M5</c>、<c>M8x1.25</c> 等解析公称直径(mm)。</summary>
        static bool TryParseThreadNominalDiameterMm(string? callout, out double diameterMm)
        {
            diameterMm = 0;
            if (string.IsNullOrWhiteSpace(callout))
                return false;

            string s = callout.Trim();
            int dash = s.IndexOf('-');
            if (dash > 0 && dash < s.Length - 1 && char.IsDigit(s[0]))
                s = s.Substring(dash + 1).Trim();

            var m = TapHoleNominalDiameterFromCallout.Match(s);
            if (!m.Success)
                return false;

            return double.TryParse(
                m.Groups[1].Value,
                System.Globalization.NumberStyles.Float,
                System.Globalization.CultureInfo.InvariantCulture,
                out diameterMm) && diameterMm > 0;
        }

        static bool TryAddTapHoleArcTarget(
            List<(double[] CenterModel, double Nx, double Ny, double Nz, double DiameterMm, string Label)> sink,
            double[] center,
            double nx,
            double ny,
            double nz,
            double diameterMm,
            string label)
        {
            const double centerTol = 0.00015;
            foreach (var t in sink)
            {
                if (Math.Abs(t.DiameterMm - diameterMm) > 1e-6)
                    continue;
                double dx = t.CenterModel[0] - center[0];
                double dy = t.CenterModel[1] - center[1];
                double dz = t.CenterModel[2] - center[2];
                if (dx * dx + dy * dy + dz * dz <= centerTol * centerTol)
                    return false;
            }

            sink.Add((center, nx, ny, nz, diameterMm, label));
            return true;
        }

        static bool TryGetNearFullCircleCenterAndPlane(
            Edge edge,
            out double[] center,
            out double nx,
            out double ny,
            out double nz)
        {
            center = Array.Empty<double>();
            nx = ny = nz = 0;
            if (!IsNearFullCircleEdge(edge, out var curve) || curve == null)
                return false;

            double[]? cp = null;
            try
            {
                cp = (double[])curve.CircleParams;
            }
            catch
            {
                return false;
            }

            if (cp == null || cp.Length < 7)
                return false;

            center = new[] { cp[0], cp[1], cp[2] };
            nx = cp[3];
            ny = cp[4];
            nz = cp[5];
            double len = Math.Sqrt(nx * nx + ny * ny + nz * nz);
            if (len < 1e-12)
                return false;
            nx /= len;
            ny /= len;
            nz /= len;
            return true;
        }

        static int DrawTapHoleSpecArcsInViewSketch(
            ISldWorks swApp,
            DrawingDoc drawingDoc,
            ModelDoc2 drwModel,
            View view,
            List<(double[] CenterModel, double Nx, double Ny, double Nz, double DiameterMm, string Label)> targets)
        {
            ExitDrawingSketchIfActive(drwModel);
            drwModel.ClearSelection2(true);
            int created = 0;
            try
            {
                TryActivateDrawingSheetAndView(drawingDoc, view);
                if (view.GetSketch() == null)
                {
                    Console.WriteLine("[tap_hole_arc] view.GetSketch() 为空,无法绘制。");
                    return 0;
                }

                drwModel.SketchManager.AddToDB = true;
                drwModel.SketchManager.DisplayWhenAdded = true;

                foreach (var (center, nx, ny, nz, diameterMm, label) in targets)
                {
                    double radiusM = diameterMm * 0.0005;
                    if (!TryBuildTapHoleSpecArcSketchPoints(
                            swApp, view, drwModel, center, nx, ny, nz, radiusM,
                            out var skCenter, out var skStart, out var skEnd))
                    {
                        Console.WriteLine($"[tap_hole_arc] 投影失败,跳过 {label}");
                        continue;
                    }

                    if (ViewSketchHasSimilarSpecArc(view, skCenter, radiusM))
                    {
                        Console.WriteLine($"[tap_hole_arc] 已有相近圆弧,跳过 {label}");
                        continue;
                    }

                    try
                    {
                        // SW CreateArc:圆心、起点、终点(非三点定弧)
                        object? seg = drwModel.SketchManager.CreateArc(
                            skCenter[0], skCenter[1], skCenter[2],
                            skStart[0], skStart[1], skStart[2],
                            skEnd[0], skEnd[1], skEnd[2],
                            (short)TapHoleSpecArcSweepDirection);
                        if (seg != null)
                        {
                            created++;
                            Console.WriteLine(
                                $"[tap_hole_arc] 已绘制 {label} Ø{diameterMm:0.##} mm 圆弧({TapHoleSpecArcSpanDegrees:0.##}°)");
                        }
                    }
                    catch (Exception ex)
                    {
                        Console.WriteLine($"[tap_hole_arc] CreateArc 失败 {label}: {ex.Message}");
                    }
                }

                drwModel.SketchManager.AddToDB = false;
                try
                {
                    drwModel.SketchManager.InsertSketch(false);
                }
                catch
                {
                    // ignored
                }
            }
            finally
            {
                ExitDrawingSketchIfActive(drwModel);
            }

            return created;
        }

        /// <summary>在视图草图空间构造圆心/起/终点,保证半径等于规格半径。</summary>
        static bool TryBuildTapHoleSpecArcSketchPoints(
            ISldWorks swApp,
            View view,
            ModelDoc2 drwModel,
            double[] centerModel,
            double nx,
            double ny,
            double nz,
            double radiusM,
            out double[] skCenter,
            out double[] skStart,
            out double[] skEnd)
        {
            skCenter = skStart = skEnd = Array.Empty<double>();

            if (!TryBuildPerpendicularUnit3(nx, ny, nz, out var ux, out var uy, out var uz))
                return false;

            double vx = ny * uz - nz * uy;
            double vy = nz * ux - nx * uz;
            double vz = nx * uy - ny * ux;
            double vlen = Math.Sqrt(vx * vx + vy * vy + vz * vz);
            if (vlen < 1e-12)
                return false;
            vx /= vlen;
            vy /= vlen;
            vz /= vlen;

            if (!TryModelPointToViewSketchLocal(swApp, view, drwModel, centerModel, out skCenter))
                return false;

            // 在草图空间求平面基向量,避免分点投影后半径失真
            if (!TryModelDirectionToViewSketchLocal(
                    swApp, view, drwModel, centerModel, ux, uy, uz, out var uSk))
                return false;
            if (!TryModelDirectionToViewSketchLocal(
                    swApp, view, drwModel, centerModel, vx, vy, vz, out var vSk))
                return false;

            if (!TryNormalize3(uSk, out uSk) || !TryNormalize3(vSk, out vSk))
                return false;

            double halfSpanRad = TapHoleSpecArcSpanDegrees * Math.PI / 360.0;
            double cosNeg = Math.Cos(-halfSpanRad);
            double sinNeg = Math.Sin(-halfSpanRad);
            double cosPos = Math.Cos(halfSpanRad);
            double sinPos = Math.Sin(halfSpanRad);

            skStart = new[]
            {
                skCenter[0] + (uSk[0] * cosNeg + vSk[0] * sinNeg) * radiusM,
                skCenter[1] + (uSk[1] * cosNeg + vSk[1] * sinNeg) * radiusM,
                skCenter[2] + (uSk[2] * cosNeg + vSk[2] * sinNeg) * radiusM,
            };
            skEnd = new[]
            {
                skCenter[0] + (uSk[0] * cosPos + vSk[0] * sinPos) * radiusM,
                skCenter[1] + (uSk[1] * cosPos + vSk[1] * sinPos) * radiusM,
                skCenter[2] + (uSk[2] * cosPos + vSk[2] * sinPos) * radiusM,
            };

            return true;
        }

        static bool TryModelDirectionToViewSketchLocal(
            ISldWorks swApp,
            View view,
            ModelDoc2 drwModel,
            double[] originModel,
            double dx,
            double dy,
            double dz,
            out double[] dirSketch)
        {
            dirSketch = Array.Empty<double>();
            const double scaleM = 0.005;
            var offset = new[]
            {
                originModel[0] + dx * scaleM,
                originModel[1] + dy * scaleM,
                originModel[2] + dz * scaleM,
            };
            if (!TryModelPointToViewSketchLocal(swApp, view, drwModel, originModel, out var sk0))
                return false;
            if (!TryModelPointToViewSketchLocal(swApp, view, drwModel, offset, out var sk1))
                return false;

            dirSketch = new[]
            {
                sk1[0] - sk0[0],
                sk1[1] - sk0[1],
                sk1[2] - sk0[2],
            };
            return TryNormalize3(dirSketch, out dirSketch);
        }

        static bool TryNormalize3(double[] v, out double[] unit)
        {
            unit = v;
            if (v == null || v.Length < 3)
                return false;
            double len = Math.Sqrt(v[0] * v[0] + v[1] * v[1] + v[2] * v[2]);
            if (len < 1e-15)
                return false;
            unit = new[] { v[0] / len, v[1] / len, v[2] / len };
            return true;
        }

        static int CountTapHoleSpecArcsInViewSketch(View view, List<double> specRadiiM)
        {
            Sketch? sk = null;
            try
            {
                sk = view.GetSketch() as Sketch;
            }
            catch
            {
                return 0;
            }

            if (sk == null || specRadiiM.Count == 0)
                return 0;

            object? segsObj = null;
            try
            {
                segsObj = sk.GetSketchSegments();
            }
            catch
            {
                return 0;
            }

            if (segsObj is not object[] segs)
                return 0;

            int count = 0;
            foreach (object? o in segs)
            {
                if (o is not SketchArc arc)
                    continue;
                if (!TryGetSketchArcRadiusM(arc, out var r))
                    continue;
                if (!TapHoleSpecRadiusMatchesAny(r, specRadiiM))
                    continue;
                if (!IsPartialThreadSpecSketchArc(arc))
                    continue;
                count++;
            }

            return count;
        }

        static bool TapHoleSpecRadiusMatchesAny(double radiusM, List<double> specRadiiM)
        {
            foreach (double spec in specRadiiM)
            {
                double rTol = Math.Max(TapHoleSpecArcSkipExistingCenterTolM * 0.5, spec * 0.05);
                if (Math.Abs(radiusM - spec) <= rTol)
                    return true;
            }

            return false;
        }

        /// <summary>排除整圆(CreateCircle 或近 360° 圆弧),只计螺纹规格圆弧。</summary>
        static bool IsPartialThreadSpecSketchArc(SketchArc arc)
        {
            if (!TryGetSketchArcSpanRadians(arc, out var span))
                return true;

            const double fullCircleRad = Math.PI * 2.0;
            const double minSpan = 0.15;
            const double fullTol = 0.25;
            return span >= minSpan && span <= fullCircleRad - fullTol;
        }

        static bool TryGetSketchArcRadiusM(SketchArc arc, out double radiusM)
        {
            radiusM = 0;
            try
            {
                radiusM = arc.GetRadius();
                return radiusM > 1e-9;
            }
            catch
            {
                return false;
            }
        }

        static bool TryGetSketchArcSpanRadians(SketchArc arc, out double span)
        {
            span = 0;
            try
            {
                var c = (double[])arc.GetCenterPoint2();
                var s = (double[])arc.GetStartPoint2();
                var e = (double[])arc.GetEndPoint2();
                if (c == null || s == null || e == null || c.Length < 3 || s.Length < 3 || e.Length < 3)
                    return false;

                double r = arc.GetRadius();
                if (r < 1e-9)
                    return false;

                double chord = Math.Sqrt(
                    Math.Pow(e[0] - s[0], 2) + Math.Pow(e[1] - s[1], 2) + Math.Pow(e[2] - s[2], 2));
                double ratio = Math.Min(1.0, Math.Max(-1.0, chord / (2.0 * r)));
                span = 2.0 * Math.Asin(ratio);
                return true;
            }
            catch
            {
                return false;
            }
        }

        static bool ViewSketchHasSimilarSpecArc(View view, double[] skCenter, double radiusM)
        {
            Sketch? sk = null;
            try
            {
                sk = view.GetSketch() as Sketch;
            }
            catch
            {
                return false;
            }

            if (sk == null)
                return false;

            object? segsObj = null;
            try
            {
                segsObj = sk.GetSketchSegments();
            }
            catch
            {
                return false;
            }

            if (segsObj is not object[] segs)
                return false;

            double tol = TapHoleSpecArcSkipExistingCenterTolM;
            double rTol = Math.Max(tol, radiusM * 0.05);

            foreach (object? o in segs)
            {
                if (o is not SketchArc arc)
                    continue;
                if (!TryGetSketchArcRadiusM(arc, out var r))
                    continue;
                if (Math.Abs(r - radiusM) > rTol)
                    continue;
                if (!IsPartialThreadSpecSketchArc(arc))
                    continue;

                if (!TryGetSketchArcCenter(arc, out var cx, out var cy, out var cz))
                    continue;

                double dx = cx - skCenter[0];
                double dy = cy - skCenter[1];
                double dz = cz - skCenter[2];
                if (dx * dx + dy * dy + dz * dz <= tol * tol)
                    return true;
            }

            return false;
        }

        static bool TryGetSketchArcCenter(SketchArc arc, out double cx, out double cy, out double cz)
        {
            cx = cy = cz = 0;
            try
            {
                var c = (double[])arc.GetCenterPoint2();
                if (c == null || c.Length < 3)
                    return false;
                cx = c[0];
                cy = c[1];
                cz = c[2];
                return true;
            }
            catch
            {
                return false;
            }
        }

        static bool TryBuildPerpendicularUnit3(double ax, double ay, double az, out double px, out double py, out double pz)
        {
            px = py = pz = 0;
            double ux = 1, uy = 0, uz = 0;
            if (Math.Abs(ax * ux + ay * uy + az * uz) > 0.9)
            {
                ux = 0;
                uy = 1;
                uz = 0;
            }

            px = ay * uz - az * uy;
            py = az * ux - ax * uz;
            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;
            return true;
        }

        static bool TryModelPointToViewSketchLocal(
            ISldWorks swApp,
            View view,
            ModelDoc2 drwModel,
            double[] model3,
            out double[] sketch3)
        {
            sketch3 = Array.Empty<double>();
            if (!TryModelPointToSheetXY(swApp, view, model3, out var sheetX, out var sheetY))
                return false;

            return TrySheetPointToViewSketchLocal(
                swApp, view, drwModel, new[] { sheetX, sheetY, 0 }, out sketch3);
        }

        static bool TrySheetPointToViewSketchLocal(
            ISldWorks swApp,
            View view,
            ModelDoc2 drwModel,
            double[] sheet3,
            out double[] sketch3)
        {
            sketch3 = Array.Empty<double>();
            try
            {
                Sketch? sk = null;
                try
                {
                    if (drwModel.SketchManager.ActiveSketch != null)
                        sk = (Sketch)drwModel.SketchManager.ActiveSketch;
                }
                catch
                {
                    // ignored
                }

                sk ??= view.GetSketch() as Sketch;
                if (sk == null)
                    return false;

                var math = swApp.IGetMathUtility();
                if (math == null)
                    return false;

                var mp = (MathPoint)math.CreatePoint(sheet3);
                if (mp == null)
                    return false;

                var sheetToSketch = (MathTransform)sk.ModelToSketchTransform;
                if (sheetToSketch == null)
                    return false;

                mp = (MathPoint)mp.MultiplyTransform(sheetToSketch);
                var arr = (double[])mp.ArrayData;
                if (arr == null || arr.Length < 3)
                    return false;

                sketch3 = new[] { arr[0], arr[1], arr[2] };
                return true;
            }
            catch
            {
                return false;
            }
        }
    }
}