c# solidworks createline 拉伸发现有微小两点间隙 导致拉伸变成薄壁特征 改bug画了6个小时 解决结果

我怀疑就是ai把AutoSolve关掉了导致的

这次能跑通,基本是几类问题一起被对上了,可以分开看:

  1. 草图里不该 EditRebuild3

    在还在编辑草图时调用 EditRebuild3(),很容易让 SolidWorks 结束草图或把 ActiveSketch 清掉。后果是:拿不到草图特征名、拉伸选不上草图,甚至下一遍 InsertSketch(true) 也建不出活动草图。

    去掉草图内的重建、只在退出草图后再重建,这些后续步骤才稳定。

  2. 启动下一张草图前不要乱 InsertSketch(false)

    在并没有活动草图时也调 InsertSketch(false),部分版本会把内部状态弄乱,接着 InsertSketch(true) 后 ActiveSketch 一直为 null。

    只有 ActiveSketch != null 时才先退出上一张草图,下一张才能正常开。

  3. 特征树里对不上「当前草图」

    用 ReferenceEquals 比两个 COM 包装器会误判。改成 IUnknown 级比较 后,才能稳定拿到刚建好的草图特征名,拉伸才能用名字/选择集对准草图。

  4. 和你「手动动一下线就全好了」同机制的那一步

    推理被 PushSketchInferOff 关掉了,SketchAddConstraints("sgCOINCIDENT") 在你环境里又常常加不上(所以你看到过 0/4),等于既没有吸附也没有可靠重合。

后面的 TrySketchSnapClosureLikeManualDrag 做了三件事:临时打开 AutoSolve、临时打开草图推理、再对一点 SetCoords 微扰又还原,等价于你在界面里拖动一下,迫使求解器跑一遍,微小间隙被吸附闭合,轮廓成了真正可用的封闭链,后面特征才顺。

  1. 其它
    不画参考线:少一条构造线,少一批端点/选择上的干扰。
    不用 dynamic:避免缺 Microsoft.CSharp 编不过,用反射调 SetCoords 功能不变。
    所以「跑通」不是单点魔法,而是:草图状态不被重建/误退出搞坏 + 能指到正确草图 + 用微扰触发和你手动改线一样的求解 叠在一起的结果。
csharp 复制代码
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Threading;
using SolidWorks.Interop.sldworks;
using SolidWorks.Interop.swconst;

namespace tools
{
    /// <summary>
    /// 选中面上最短两条直线边;在每条边上以边为「宽」边、
    /// 垂直于边朝另一边的方向画 10×3 mm 矩形(CreateLine)。较高边一侧矩形朝「远离另一边」外扩(向上),较低边朝相反方向(向下)。
    /// 矩形贴底边的对侧两角(外侧)做直线倒角(沿两邻边各截一段,非圆角),尺寸见 <see cref="ChamferAlongEdgeMm"/>。
    /// 上侧与下侧矩形分两个独立草图绘制(同一面各 InsertSketch 一次)。
    /// 每个草图绘制完成后在退出编辑前做凸台拉伸:成形到面(<see cref="swEndConditions_e.swEndCondUpToSurface"/>),
    /// 在零件实体上找与草图所在面平行、且与草图面垂直距离最小(大于容差)的平面作为终止面;顶边为单段闭合轮廓以便拉伸(非薄壁)。
    /// 草图坐标与 ModelToSketchTransform 一致时为米;<see cref="SketchManager.CreateLine"/> 前将端点换为毫米量化(见 <see cref="SketchCreateLineMmDecimalPlaces"/>),
    /// 闭合轮廓采用链式端点且末段回到首点坐标,避免首尾缝隙。
    /// </summary>
    public static class face_shortest_edges_rectangles
    {
        const string LogPrefix = "[face_shortest_edge_rects]";
        /// <summary>每条 CreateLine 成功后暂停(毫秒);0 表示不延时。</summary>
        const int DelayMsAfterEachCreateLine = 0;

        /// <summary>CreateLine 入参在毫米单位下保留的小数位数(过小易产生闭合缝隙)。</summary>
        const int SketchCreateLineMmDecimalPlaces = 4;

        /// <summary>绘制期间临时关闭,避免顶边等被错误推断连到 c0 等现有点(如 p2b→(-5,0) 而非 p2b→p3a)。</summary>
        static readonly int[] SketchInferOffToggleIds =
        {
            (int)swUserPreferenceToggle_e.swSketchInference,
            (int)swUserPreferenceToggle_e.swSketchInferFromModel,
            (int)swUserPreferenceToggle_e.swSketchAutomaticRelations,
        };

        static void PushSketchInferOff(ISldWorks swApp, out bool[] prevBools, out bool[] applied)
        {
            int n = SketchInferOffToggleIds.Length;
            prevBools = new bool[n];
            applied = new bool[n];
            if (swApp == null)
                return;
            for (int i = 0; i < n; i++)
            {
                try
                {
                    int id = SketchInferOffToggleIds[i];
                    prevBools[i] = swApp.GetUserPreferenceToggle(id);
                    swApp.SetUserPreferenceToggle(id, false);
                    applied[i] = true;
                }
                catch (Exception ex)
                {
                    Debug.WriteLine($"{LogPrefix} PushSketchInferOff id={SketchInferOffToggleIds[i]}: {ex.Message}");
                }
            }
        }

        static void PopSketchInferOff(ISldWorks swApp, bool[] prevBools, bool[] applied)
        {
            if (swApp == null || applied == null || prevBools == null)
                return;
            for (int i = 0; i < applied.Length; i++)
            {
                if (!applied[i])
                    continue;
                try
                {
                    swApp.SetUserPreferenceToggle(SketchInferOffToggleIds[i], prevBools[i]);
                }
                catch (Exception ex)
                {
                    Debug.WriteLine($"{LogPrefix} PopSketchInferOff id={SketchInferOffToggleIds[i]}: {ex.Message}");
                }
            }
        }
        const double SegmentLengthMm = 10.0;
        const double RectWidthMm = 10.0;
        const double RectLengthMm = 3.0;
        /// <summary>外侧两角直线倒角:沿每条邻边从角点向内的截距(mm)。过大会按边长自动缩小;≤0 则画直角矩形(4 段)。</summary>
        const double ChamferAlongEdgeMm = 1.0;

        static void Log(string message)
        {
            string line = $"{LogPrefix} {message}";
            Console.WriteLine(line);
            Debug.WriteLine(line);
        }

        public static void run(ISldWorks swApp, ModelDoc2 swModel)
        {
            Log("开始:最短两边→分两个草图:各 1 个 10×3mm 矩形,不画最短边参考线(Console→plugin_runtime_log.txt)");

            if (swApp == null || swModel == null)
            {
                Log("中止:swApp 或 swModel 为空");
                return;
            }

            if (swModel.GetType() != (int)swDocumentTypes_e.swDocPART)
            {
                Log("中止:当前不是零件文档");
                swApp.SendMsgToUser("请在零件文档中使用本命令。");
                return;
            }

            var selMgr = (SelectionMgr)swModel.SelectionManager;
            if (selMgr == null)
            {
                Log("中止:SelectionMgr 为空");
                swApp.SendMsgToUser("无法获取选择管理器。");
                return;
            }

            Face2? targetFace = null;
            int n = selMgr.GetSelectedObjectCount();
            for (int i = 1; i <= n; i++)
            {
                int t = selMgr.GetSelectedObjectType3(i, -1);
                if (t == (int)swSelectType_e.swSelFACES)
                {
                    targetFace = (Face2)selMgr.GetSelectedObject6(i, -1);
                    break;
                }
            }

            if (targetFace == null)
            {
                Log($"中止:选中项中无面(当前共 {n} 个选中对象)");
                swApp.SendMsgToUser("请先选中一个面。");
                return;
            }

            object[]? edgeObjs;
            try
            {
                edgeObjs = (object[])targetFace.GetEdges();
            }
            catch (Exception ex)
            {
                Log($"GetEdges 异常: {ex.Message}");
                swApp.SendMsgToUser($"无法读取面的边: {ex.Message}");
                return;
            }

            if (edgeObjs == null || edgeObjs.Length < 2)
            {
                Log($"中止:面上边数不足");
                swApp.SendMsgToUser("该面上的边少于 2 条,无法继续。");
                return;
            }

            var edgesWithLen = new List<(Edge edge, double len)>();
            foreach (object o in edgeObjs)
            {
                if (o is not Edge e)
                    continue;
                double len = GetEdgeLengthMeters(e);
                edgesWithLen.Add((e, len));
            }

            if (edgesWithLen.Count < 2)
            {
                Log("中止:未能得到至少 2 条有效边");
                swApp.SendMsgToUser("未能解析该面上的边。");
                return;
            }

            edgesWithLen.Sort((a, b) => a.len.CompareTo(b.len));
            Edge edgeA = edgesWithLen[0].edge;
            Edge edgeB = edgesWithLen[1].edge;
            if (ReferenceEquals(edgeA, edgeB))
            {
                Log("中止:最短两条边引用相同");
                swApp.SendMsgToUser("无法找到两条不同的最短边。");
                return;
            }

            if (!TryGetEdgeVerticesModel(edgeA, out double[]? v0a, out double[]? v1a) ||
                !TryGetEdgeVerticesModel(edgeB, out double[]? v0b, out double[]? v1b))
            {
                Log("中止:无法取得边的两端点(顶点)");
                swApp.SendMsgToUser("无法取得最短两边的端点。");
                return;
            }

            double segM = SegmentLengthMm / 1000.0;
            if (!TryGetCenteredChordSegmentModel(v0a, v1a, segM, out double[]? segA0, out double[]? segA1) ||
                !TryGetCenteredChordSegmentModel(v0b, v1b, segM, out double[]? segB0, out double[]? segB1))
            {
                Log("中止:线段端点计算失败(边可能退化)");
                swApp.SendMsgToUser("无法计算边上的线段端点。");
                return;
            }

            Log(
                $"最短两边长度(m) L1={edgesWithLen[0].len:G9} L2={edgesWithLen[1].len:G9} | " +
                $"参考线段≤{SegmentLengthMm}mm | " +
                $"边A模型 {Vec3Fmt(segA0)} --- {Vec3Fmt(segA1)} | 边B {Vec3Fmt(segB0)} --- {Vec3Fmt(segB1)}");

            Log("拉伸:成形到面,终止面=与草图面平行且法向距离最近之平面(非 LinkToThickness)。");

            string modeUpper = "";
            string modeLower = "";
            bool drewUpper = TryDrawHalfSketch(
                swApp, swModel, targetFace, segA0, segA1, segB0, segB1,
                upperHalf: true, out modeUpper);
            bool drewLower = TryDrawHalfSketch(
                swApp, swModel, targetFace, segA0, segA1, segB0, segB1,
                upperHalf: false, out modeLower);

            if (drewUpper || drewLower)
            {
                Log(
                    $"完成:{(drewUpper ? $"上侧({modeUpper})" : "上侧失败")} | " +
                    $"{(drewLower ? $"下侧({modeLower})" : "下侧失败")}");
                //swApp.SendMsgToUser(
                  //  $"已在同一面上分两个草图绘制矩形(各含一条参考线段)。上侧:{(drewUpper ? modeUpper : "未完成")};下侧:{(drewLower ? modeLower : "未完成")}。[face_shortest_edge_rects]");
            }
            else
            {
                Log("两个草图均未成功绘制");
                swApp.SendMsgToUser("绘制失败:两个草图均未完成,详见插件日志。[face_shortest_edge_rects]");
            }
        }

        /// <summary>在同一面上打开新草图,只画上侧或下侧一个矩形(默认不画最短边构造参考线);绘制成功后尝试成形到「最近平行面」。</summary>
        static bool TryDrawHalfSketch(
            ISldWorks swApp,
            ModelDoc2 swModel,
            Face2 sketchHostFace,
            double[] segA0,
            double[] segA1,
            double[] segB0,
            double[] segB1,
            bool upperHalf,
            out string modeUsed)
        {
            modeUsed = "";
            string tag = upperHalf ? "上侧矩形草图" : "下侧矩形草图";

            // 仅在确有活动草图时退出;无草图时调用 InsertSketch(false) 部分 SolidWorks 版本会破坏后续 InsertSketch(true),导致 ActiveSketch 一直为空
            try
            {
                if (swModel.SketchManager.ActiveSketch != null)
                    swModel.SketchManager.InsertSketch(false);
            }
            catch { /* ignore */ }

            swModel.ClearSelection2(true);
            if (!((Entity)sketchHostFace).Select4(false, null))
            {
                Log($"{tag}:Select4 面失败");
                return false;
            }

            try
            {
                swModel.SketchManager.InsertSketch(true);
            }
            catch (Exception ex)
            {
                Log($"{tag}:InsertSketch 异常: {ex.Message}");
                return false;
            }

            if (swModel.SketchManager.ActiveSketch == null)
            {
                Log($"{tag}:InsertSketch 后 ActiveSketch 为空");
                return false;
            }

            var math = swApp.IGetMathUtility();
            if (math == null)
            {
                Log($"{tag}:MathUtility 为空");
                try { swModel.SketchManager.InsertSketch(false); } catch { /* ignore */ }
                return false;
            }

            if (!TryComputeSketchRects(
                    math, swModel, segA0, segA1, segB0, segB1,
                    out double[] skA0, out double[] skA1, out double[] skB0, out double[] skB1,
                    out double[] upper0, out double[] upper1, out double[] lower0, out double[] lower1,
                    out double[] u0, out double[] u1, out double[] u2, out double[] u3,
                    out double[] l0, out double[] l1, out double[] l2, out double[] l3,
                    out bool aIsHigher))
            {
                Log($"{tag}:ModelToSketchTransform 或矩形几何失败");
                try { swModel.SketchManager.InsertSketch(false); } catch { /* ignore */ }
                return false;
            }

            Log(
                $"{tag}:最短边A 草图 {Vec3Fmt(skA0)} → {Vec3Fmt(skA1)} | 边B {Vec3Fmt(skB0)} → {Vec3Fmt(skB1)} | " +
                $"分层 上边={(aIsHigher ? "边A" : "边B")}");

            double[] r0 = upperHalf ? upper0 : lower0;
            double[] r1 = upperHalf ? upper1 : lower1;
            double[] q0 = upperHalf ? u0 : l0;
            double[] q1 = upperHalf ? u1 : l1;
            double[] q2 = upperHalf ? u2 : l2;
            double[] q3 = upperHalf ? u3 : l3;

            bool ok = false;
            string skFeatName = "";
            try
            {
                if (TryDrawSingleSketchGeometry(swApp, swModel, r0, r1, q0, q1, q2, q3, drawRefSegment: false, out string mode))
                {
                    modeUsed = mode;
                    ok = true;
                    
                    // 在退出草图前先获取草图特征名
                    if (!TryGetActiveSketchFeatureName(swModel, out skFeatName))
                    {
                        Log($"{tag}:无法取得草图特征名,跳过拉伸");
                        // 即使获取失败也要退出草图
                        try
                        {
                            swModel.SketchManager.InsertSketch(false);
                        }
                        catch { }
                    }
                    else
                    {
                        Log($"{tag}:获取到草图特征名: {skFeatName}");
                        
                        // 获取代表点(在退出草图前)
                        if (!TryGetFaceRepresentativePoint(sketchHostFace, out double[] pickOnHostFace))
                        {
                            Log($"{tag}:无法取草图所在面代表点,跳过拉伸");
                            try
                            {
                                swModel.SketchManager.InsertSketch(false);
                            }
                            catch { }
                        }
                        else
                        {
                            // 退出草图
                            try
                            {
                                swModel.SketchManager.InsertSketch(false);
                            }
                            catch (Exception ex)
                            {
                                Log($"{tag}:退出草图 InsertSketch(false): {ex.Message}");
                            }
                            
                            TryEditRebuildAfterSketch(swModel, tag);

                            if (TryFeatureExtrudeUpToNearestParallelFace(
                                    swModel, sketchHostFace, skFeatName, pickOnHostFace, tag))
                                modeUsed += ";已拉伸(成形到最近平行面)";
                            else
                                Log($"{tag}:成形到面拉伸失败");
                        }
                    }
                }
                else
                    Log($"{tag}:CreateLine 全部策略失败");
            }
            catch (Exception ex)
            {
                Log($"{tag}:CreateLine 异常: {ex.Message}");
            }
            finally
            {
                try
                {
                    if (swModel.SketchManager.ActiveSketch != null)
                        swModel.SketchManager.InsertSketch(false);
                }
                catch (Exception ex)
                {
                    Log($"{tag}:InsertSketch(false) 异常: {ex.Message}");
                }
            }

            return ok;
        }

        const double ParallelFaceMinAbsDot = 0.985;
        const double MinDistinctPlaneGapM = 1e-4;

        const double MacroDraftRad = 0.01745329251994;

        /// <summary>
        /// 草图须已退出编辑。先成形到最近平行面(多组选择 × Dir × 选点 等尝试);失败则盲孔拉伸。
        /// Interop 中 FeatureExtrusion2 第 3 个 bool 为 <c>Dir</c>(与宏里成功示例一致时常为 true),不是「先选草图/先选面」。
        /// EnableContourSelection 在 FeatureExtrusion2 之后置 false,与宏一致。
        /// </summary>
        static bool TryFeatureExtrudeUpToNearestParallelFace(
            ModelDoc2 swModel,
            Face2 sketchHostFace,
            string sketchFeatureName,
            double[] pickOnHostFaceModel,
            string logTag)
        {
            if (swModel == null || sketchHostFace == null || string.IsNullOrEmpty(sketchFeatureName) ||
                pickOnHostFaceModel == null || pickOnHostFaceModel.Length < 3)
                return false;

            if (!TryGetFaceRepresentativePoint(sketchHostFace, out double[] p0) ||
                !TryGetPlanarUnitNormal(sketchHostFace, out double[] n0))
            {
                Log($"{logTag}:草图所在面非法向或无法取点,无法成形到面");
                return false;
            }

            if (!TryFindNearestParallelPlanarFace(
                    swModel, sketchHostFace, p0, n0,
                    out Face2? endFace,
                    out double gapMm))
            {
                Log($"{logTag}:未找到与草图面平行且间距>{MinDistinctPlaneGapM * 1000:G3}mm 的终止面");
                return false;
            }

            double gapM = gapMm / 1000.0;
            Log($"{logTag}:成形到面候选 间距≈{gapMm:G4}mm");

            var selMgr = (SelectionMgr)swModel.SelectionManager;
            var fm = (FeatureManager)swModel.FeatureManager;

            // ------ 成形到面:草图→面 / 面→草图 × Dir × Sd × 选点(面上点/原点)
            foreach (bool sketchFirst in new[] { true, false })
            {
                foreach (bool dir in new[] { true, false })
                {
                    foreach (bool sd in new[] { true, false })
                    {
                        foreach (bool useZeroPick in new[] { false, true })
                        {
                            double px, py, pz;
                            if (useZeroPick)
                            {
                                px = py = pz = 0;
                            }
                            else
                            {
                                px = pickOnHostFaceModel[0];
                                py = pickOnHostFaceModel[1];
                                pz = pickOnHostFaceModel[2];
                            }

                            swModel.ClearSelection2(true);
                            bool picked;
                            if (sketchFirst)
                            {
                                picked = swModel.Extension.SelectByID2(
                                    sketchFeatureName, "SKETCH", px, py, pz, false, 0, null, 0);
                                if (picked)
                                    picked = ((Entity)endFace).Select4(true, null);
                            }
                            else
                            {
                                picked = ((Entity)endFace).Select4(false, null);
                                if (picked)
                                    picked = swModel.Extension.SelectByID2(
                                        sketchFeatureName, "SKETCH", px, py, pz, true, 0, null, 0);
                            }

                            if (!picked)
                                continue;

                            Feature? feat = fm.FeatureExtrusion2(
                                sd,
                                false,
                                dir,
                                (int)swEndConditions_e.swEndCondUpToSurface,
                                (int)swEndConditions_e.swEndCondBlind,
                                0.01,
                                0.01,
                                false,
                                false,
                                false,
                                false,
                                MacroDraftRad,
                                MacroDraftRad,
                                false,
                                false,
                                false,
                                false,
                                true,
                                true,
                                true,
                                0,
                                0.0,
                                false);
                            if (feat != null)
                            {
                                SetEnableContourSelectionSafe(selMgr, false);
                                Log(
                                    $"{logTag}:成形到面成功(sketchFirst={sketchFirst} Dir={dir} Sd={sd} " +
                                    $"选点={(useZeroPick ? "0,0,0" : "面上点")})");
                                return true;
                            }
                        }
                    }
                }
            }

            Log($"{logTag}:成形到面全部组合失败,尝试盲孔拉伸(宏:T1=T2=0, D1≈min(0.05,间距), D2=0.01)");

            SetEnableContourSelectionSafe(selMgr, false);

            // ------ 盲孔回退:与宏一致 True,False,False,0,0, D1,D2, ..., 末三 true(VB 里 1,1,1)
            swModel.ClearSelection2(true);
            bool skOk = TrySelectSketchOnly(swModel, sketchFeatureName, pickOnHostFaceModel, logTag);
            if (!skOk)
                return false;

            double blindD1 = Math.Min(0.05, Math.Max(gapM * 1.02, MinDistinctPlaneGapM * 2.0));
            double blindD2 = 0.01;
            Feature? blindFeat = null;
            bool blindOkSd = false, blindOkDir = false;
            foreach (bool dir in new[] { true, false })
            {
                foreach (bool sd in new[] { true, false })
                {
                    blindFeat = fm.FeatureExtrusion2(
                        sd,
                        false,
                        dir,
                        (int)swEndConditions_e.swEndCondBlind,
                        (int)swEndConditions_e.swEndCondBlind,
                        blindD1,
                        blindD2,
                        false,
                        false,
                        false,
                        false,
                        MacroDraftRad,
                        MacroDraftRad,
                        false,
                        false,
                        false,
                        false,
                        true,
                        true,
                        true,
                        0,
                        0.0,
                        false);
                    if (blindFeat != null)
                    {
                        blindOkSd = sd;
                        blindOkDir = dir;
                        break;
                    }
                }

                if (blindFeat != null)
                    break;
            }

            SetEnableContourSelectionSafe(selMgr, false);
            if (blindFeat == null)
            {
                Log($"{logTag}:盲孔 FeatureExtrusion2 仍返回 null(已尝试 Dir/Sd 组合)");
                return false;
            }

            Log($"{logTag}:盲孔拉伸成功 D1={blindD1:G6}m D2={blindD2:G6}m Sd={blindOkSd} Dir={blindOkDir}");
            return true;
        }

        /// <summary>退出草图后重建,便于轮廓参与后续 FeatureExtrusion2 选择。</summary>
        static void TryEditRebuildAfterSketch(ModelDoc2 swModel, string logTag)
        {
            try
            {
                swModel.EditRebuild3();
            }
            catch (Exception ex)
            {
                Log($"{logTag}:EditRebuild3: {ex.Message}");
            }
        }

        static void SetEnableContourSelectionSafe(SelectionMgr selMgr, bool value)
        {
            try
            {
                selMgr.EnableContourSelection = value;
            }
            catch (Exception ex)
            {
                Debug.WriteLine($"{LogPrefix} EnableContourSelection={value}: {ex.Message}");
            }
        }

        /// <summary>仅选中草图特征;先试面上坐标,再试 (0,0,0)(与示例宏一致)。</summary>
        static bool TrySelectSketchOnly(
            ModelDoc2 swModel,
            string sketchFeatureName,
            double[] pickOnHostFaceModel,
            string logTag)
        {
            swModel.ClearSelection2(true);
            double px = pickOnHostFaceModel[0], py = pickOnHostFaceModel[1], pz = pickOnHostFaceModel[2];
            if (swModel.Extension.SelectByID2(sketchFeatureName, "SKETCH", px, py, pz, false, 0, null, 0))
                return true;
            if (swModel.Extension.SelectByID2(sketchFeatureName, "SKETCH", 0, 0, 0, false, 0, null, 0))
            {
                Log($"{logTag}:SelectByID2 草图改用(0,0,0)成功");
                return true;
            }

            Log($"{logTag}:SelectByID2 草图「{sketchFeatureName}」在面上点与(0,0,0)均失败");
            return false;
        }

        /// <summary>同一 COM 对象可能被包装为不同 RCW,ReferenceEquals 不可靠。</summary>
        static bool ComObjectIdentityEquals(object? a, object? b)
        {
            if (a == null || b == null)
                return a == null && b == null;
            try
            {
                IntPtr pa = Marshal.GetIUnknownForObject(a);
                try
                {
                    IntPtr pb = Marshal.GetIUnknownForObject(b);
                    try
                    {
                        return pa == pb;
                    }
                    finally
                    {
                        Marshal.Release(pb);
                    }
                }
                finally
                {
                    Marshal.Release(pa);
                }
            }
            catch
            {
                return ReferenceEquals(a, b);
            }
        }

        /// <summary>对两草图点添加「重合」关系(特征树/显示约束图标);已为同一 COM 点时跳过。</summary>
        static bool TrySketchCoincident(ModelDoc2 swModel, SketchPoint? a, SketchPoint? b)
        {
            if (a == null || b == null)
                return false;
            if (ComObjectIdentityEquals(a, b))
                return true;
            try
            {
                swModel.ClearSelection2(true);
                ((Entity)a).Select4(false, null);
                ((Entity)b).Select4(true, null);
                swModel.SketchAddConstraints("sgCOINCIDENT");
                return true;
            }
            catch (Exception ex)
            {
                Debug.WriteLine($"{LogPrefix} TrySketchCoincident: {ex.Message}");
                return false;
            }
        }

        /// <summary>
        /// 手动拖动一条线会触发草图求解与推理,邻近顶点常被吸附闭合。临时开启 AutoSolve、草图推理,
        /// 并对轮廓上一点做 ISketchPoint.SetCoords 微扰再还原,用 API 模拟该效果。
        /// </summary>
        static void TrySketchSnapClosureLikeManualDrag(ISldWorks swApp, ModelDoc2 swModel, SketchPoint? anyContourPoint)
        {
            bool prevAutoSolve = false;
            bool gotAutoSolve = false;
            try
            {
                prevAutoSolve = swModel.SketchManager.AutoSolve;
                gotAutoSolve = true;
            }
            catch { /* ignore */ }

            try
            {
                try
                {
                    swModel.SketchManager.AutoSolve = true;
                }
                catch { /* ignore */ }

                foreach (int id in SketchInferOffToggleIds)
                {
                    try
                    {
                        swApp.SetUserPreferenceToggle(id, true);
                    }
                    catch { /* ignore */ }
                }

                TryNudgeSketchPointCoords(anyContourPoint);
                Log("已触发草图微扰+自动求解(等价于手动改线后间隙被吸附闭合)");
            }
            catch (Exception ex)
            {
                Debug.WriteLine($"{LogPrefix} TrySketchSnapClosureLikeManualDrag: {ex.Message}");
            }
            finally
            {
                if (gotAutoSolve)
                {
                    try
                    {
                        swModel.SketchManager.AutoSolve = prevAutoSolve;
                    }
                    catch { /* ignore */ }
                }
            }
        }

        static void TryNudgeSketchPointCoords(SketchPoint? pt)
        {
            if (pt == null)
                return;
            try
            {
                Type t = pt.GetType();
                PropertyInfo? px = t.GetProperty("X");
                PropertyInfo? py = t.GetProperty("Y");
                PropertyInfo? pz = t.GetProperty("Z");
                MethodInfo? setCoords = t.GetMethod("SetCoords");
                if (px == null || py == null || pz == null || setCoords == null)
                    return;

                double x = Convert.ToDouble(px.GetValue(pt));
                double y = Convert.ToDouble(py.GetValue(pt));
                double z = Convert.ToDouble(pz.GetValue(pt));
                const double eps = 1e-9;
                setCoords.Invoke(pt, new object[] { x + eps, y, z });
                setCoords.Invoke(pt, new object[] { x, y, z });
            }
            catch (Exception ex)
            {
                Debug.WriteLine($"{LogPrefix} TryNudgeSketchPointCoords: {ex.Message}");
            }
        }

        /// <summary>Interop 未暴露时,用反射调用 COM Sketch.GetFeature() 取树中草图特征名。</summary>
        static bool TryGetActiveSketchFeatureName(ModelDoc2 swModel, out string name)
        {
            name = "";
            try
            {
                object? sk = swModel.SketchManager.ActiveSketch;
                if (sk == null)
                    return false;
                
                // 方法1: 直接尝试获取 Name 属性(SolidWorks 2018+可能支持)
                Type skType = sk.GetType();
                PropertyInfo? nameProp = skType.GetProperty("Name");
                if (nameProp != null)
                {
                    object? nameVal = nameProp.GetValue(sk);
                    if (nameVal is string n && !string.IsNullOrEmpty(n))
                    {
                        name = n;
                        Log($"[调试] 通过Sketch.Name获取: {name}");
                        return true;
                    }
                }
                
                // 方法2: 反射调用 GetFeature()
                MethodInfo? mi = skType.GetMethod("GetFeature");
                if (mi != null)
                {
                    object? fob = mi.Invoke(sk, null);
                    if (fob is Feature feat)
                    {
                        name = feat.Name ?? "";
                        if (!string.IsNullOrEmpty(name))
                        {
                            Log($"[调试] 通过GetFeature()获取: {name}");
                            return true;
                        }
                    }
                }
                
                // 方法3: 遍历特征树找当前激活草图
                Feature? currentFeat = swModel.FirstFeature() as Feature;
                while (currentFeat != null)
                {
                    if (currentFeat.GetTypeName2() == "ProfileFeature")
                    {
                        object? sketchObj = currentFeat.GetSpecificFeature2();
                        if (sketchObj != null && ComObjectIdentityEquals(sketchObj, sk))
                        {
                            name = currentFeat.Name ?? "";
                            Log($"[调试] 通过遍历特征树获取: {name}");
                            return !string.IsNullOrEmpty(name);
                        }
                    }
                    currentFeat = currentFeat.GetNextFeature() as Feature;
                }
                
                Log("[调试] 所有方法均未获取到草图特征名");
                return false;
            }
            catch (Exception ex)
            {
                Debug.WriteLine($"{LogPrefix} TryGetActiveSketchFeatureName: {ex.Message}");
                return false;
            }
        }

        static bool TryGetFaceRepresentativePoint(Face2 face, out double[] p)
        {
            p = Array.Empty<double>();
            try
            {
                var edges = (object[])face.GetEdges();
                foreach (object o in edges)
                {
                    if (o is not Edge ed)
                        continue;
                    var v = (Vertex)ed.GetStartVertex();
                    if (v == null)
                        continue;
                    var arr = (double[])v.GetPoint();
                    if (arr != null && arr.Length >= 3)
                    {
                        p = new[] { arr[0], arr[1], arr[2] };
                        return true;
                    }
                }
            }
            catch (Exception ex)
            {
                Debug.WriteLine($"{LogPrefix} TryGetFaceRepresentativePoint: {ex.Message}");
            }

            return false;
        }

        static bool TryGetPlanarUnitNormal(Face2 face, out double[] n)
        {
            n = new[] { 0.0, 0.0, 1.0 };
            try
            {
                var surf = face.IGetSurface();
                if (surf == null || !surf.IsPlane())
                    return false;

                var pp = (double[])surf.PlaneParams;
                if (pp != null && pp.Length >= 3)
                {
                    double L = Math.Sqrt(pp[0] * pp[0] + pp[1] * pp[1] + pp[2] * pp[2]);
                    if (L < 1e-15)
                        return false;
                    n = new[] { pp[0] / L, pp[1] / L, pp[2] / L };
                    return true;
                }
            }
            catch (Exception ex)
            {
                Debug.WriteLine($"{LogPrefix} TryGetPlanarUnitNormal: {ex.Message}");
            }

            return false;
        }

        static bool TryFindNearestParallelPlanarFace(
            ModelDoc2 swModel,
            Face2 sketchFace,
            double[] p0,
            double[] n0,
            out Face2? bestFace,
            out double gapMm)
        {
            bestFace = null;
            gapMm = double.PositiveInfinity;
            if (swModel is not PartDoc partDoc)
                return false;

            object? bodiesOb;
            try
            {
                bodiesOb = partDoc.GetBodies2((int)swBodyType_e.swSolidBody, false);
            }
            catch (Exception ex)
            {
                Log($"{LogPrefix} GetBodies2: {ex.Message}");
                return false;
            }

            if (bodiesOb is not object[] bodies)
                return false;

            double bestGapM = double.PositiveInfinity;
            foreach (object bo in bodies)
            {
                if (bo is not Body2 body)
                    continue;
                object? facesOb;
                try
                {
                    facesOb = body.GetFaces();
                }
                catch
                {
                    continue;
                }

                if (facesOb is not object[] faces)
                    continue;

                foreach (object fo in faces)
                {
                    if (fo is not Face2 f)
                        continue;
                    if (ReferenceEquals(f, sketchFace))
                        continue;

                    var surf = f.IGetSurface();
                    if (surf == null || !surf.IsPlane())
                        continue;
                    if (!TryGetPlanarUnitNormal(f, out double[] nf))
                        continue;

                    double align = Math.Abs(Dot3(nf, n0));
                    if (align < ParallelFaceMinAbsDot)
                        continue;
                    if (!TryGetFaceRepresentativePoint(f, out double[] pf))
                        continue;

                    double gapM = Math.Abs(Dot3(Sub3(pf, p0), n0));
                    if (gapM < MinDistinctPlaneGapM)
                        continue;
                    if (gapM < bestGapM - 1e-9)
                    {
                        bestGapM = gapM;
                        bestFace = f;
                    }
                }
            }

            if (bestFace == null || double.IsPositiveInfinity(bestGapM))
                return false;

            gapMm = bestGapM * 1000.0;
            return true;
        }

        /// <summary>当前激活草图下,将模型线段变为草图坐标并计算上下矩形四角。</summary>
        static bool TryComputeSketchRects(
            MathUtility math,
            ModelDoc2 swModel,
            double[] segA0,
            double[] segA1,
            double[] segB0,
            double[] segB1,
            out double[] skA0,
            out double[] skA1,
            out double[] skB0,
            out double[] skB1,
            out double[] upper0,
            out double[] upper1,
            out double[] lower0,
            out double[] lower1,
            out double[] u0,
            out double[] u1,
            out double[] u2,
            out double[] u3,
            out double[] l0,
            out double[] l1,
            out double[] l2,
            out double[] l3,
            out bool aIsHigher)
        {
            skA0 = skA1 = skB0 = skB1 = Array.Empty<double>();
            upper0 = upper1 = lower0 = lower1 = Array.Empty<double>();
            u0 = u1 = u2 = u3 = l0 = l1 = l2 = l3 = Array.Empty<double>();
            aIsHigher = false;

            TryModelPointToActiveSketch(math, swModel, segA0, out skA0);
            TryModelPointToActiveSketch(math, swModel, segA1, out skA1);
            TryModelPointToActiveSketch(math, swModel, segB0, out skB0);
            TryModelPointToActiveSketch(math, swModel, segB1, out skB1);

            if (skA0.Length < 3 || skA1.Length < 3 || skB0.Length < 3 || skB1.Length < 3)
                return false;

            double[] midA = Mid3(skA0, skA1);
            double[] midB = Mid3(skB0, skB1);
            aIsHigher = midA[1] > midB[1]
                || (Math.Abs(midA[1] - midB[1]) < 1e-12 && midA[0] >= midB[0]);
            upper0 = aIsHigher ? skA0 : skB0;
            upper1 = aIsHigher ? skA1 : skB1;
            lower0 = aIsHigher ? skB0 : skA0;
            lower1 = aIsHigher ? skB1 : skA1;
            double[] midU = Mid3(upper0, upper1);
            double[] midL = Mid3(lower0, lower1);
            double[] nSep = Sub3(midU, midL);
            double lenSep = Len3(nSep);
            double[] nUp;
            if (lenSep < 1e-12)
                nUp = new double[] { 0, 1, 0 };
            else
                nUp = Scale3(nSep, 1.0 / lenSep);

            double h = RectLengthMm / 1000.0;
            double[] tU = Normalize3(Sub3(upper1, upper0));
            if (Len3(tU) < 1e-12)
                return false;

            BuildInPlanePerpToward(nUp, tU, towardPositiveNUp: true, out double[] perpUp);
            BuildInPlanePerpToward(nUp, tU, towardPositiveNUp: false, out double[] perpDn);

            CornersRect(upper0, upper1, perpUp, h, out u0, out u1, out u2, out u3);
            CornersRect(lower0, lower1, perpDn, h, out l0, out l1, out l2, out l3);
            return true;
        }

        /// <summary>可选一条最短边构造参考线 + 矩形(底边 + 外侧两角直线倒角...);CreateLine 先试米,再试×1000。</summary>
        static bool TryDrawSingleSketchGeometry(
            ISldWorks swApp,
            ModelDoc2 swModel,
            double[] ref0,
            double[] ref1,
            double[] c0,
            double[] c1,
            double[] c2,
            double[] c3,
            bool drawRefSegment,
            out string modeUsed)
        {
            modeUsed = "";
            (bool meters, bool addDb)[] attempts =
            {
                (true, false),
                (true, true),
                (false, false),
                (false, true),
            };
            foreach (var a in attempts)
            {
                if (TryDrawRectOnce(swApp, swModel, ref0, ref1, c0, c1, c2, c3, drawRefSegment, a.meters, a.addDb))
                {
                    modeUsed =
                        $"CreateLine {(a.meters ? "米" : "×1000mm")} AddToDB={a.addDb}" +
                        (drawRefSegment ? " +参考线段" : "");
                    return true;
                }
            }

            return false;
        }

        static string SketchPointMmFmt(double[] skM) =>
            skM == null || skM.Length < 3
                ? "?"
                : $"{skM[0] * 1000:G9},{skM[1] * 1000:G9},{skM[2] * 1000:G9}";

        /// <summary>
        /// 草图空间点(米)先换为毫米并按 <see cref="SketchCreateLineMmDecimalPlaces"/> 量化,再转为 <see cref="SketchManager.CreateLine"/> 所用单位:
        /// <paramref name="apiCoordsInMeters"/> 为 true 时输出米,为 false 时输出毫米(与 sketchMeters=false 分支一致)。
        /// </summary>
        static double[] SketchPointForCreateLine(double[] sketchMeters, bool apiCoordsInMeters)
        {
            if (sketchMeters == null || sketchMeters.Length < 3)
                return new double[] { 0, 0, 0 };

            double MmQuantize(double coordMeters) =>
                Math.Round(
                    coordMeters * 1000.0,
                    SketchCreateLineMmDecimalPlaces,
                    MidpointRounding.AwayFromZero);

            double xMm = MmQuantize(sketchMeters[0]);
            double yMm = MmQuantize(sketchMeters[1]);
            double zMm = MmQuantize(sketchMeters[2]);
            if (apiCoordsInMeters)
                return new[] { xMm / 1000.0, yMm / 1000.0, zMm / 1000.0 };
            return new[] { xMm, yMm, zMm };
        }

        static bool SketchCreateLineEndpoints(
            ModelDoc2 swModel,
            double[] ra,
            double[] rb,
            out SketchPoint? startPt,
            out SketchPoint? endPt)
        {
            startPt = endPt = null;
            try
            {
                swModel.ClearSelection2(true);
            }
            catch (Exception ex)
            {
                Debug.WriteLine($"{LogPrefix} ClearSelection2: {ex.Message}");
            }

            object? o = swModel.SketchManager.CreateLine(ra[0], ra[1], ra[2], rb[0], rb[1], rb[2]);
            if (o == null)
                return false;
            if (o is SketchSegment seg)
            {
                startPt = GetSegmentStartPoint(seg);
                endPt = GetSegmentEndPoint(seg);
            }

            if (DelayMsAfterEachCreateLine > 0)
                Thread.Sleep(DelayMsAfterEachCreateLine);
            return true;
        }

        /// <summary>草图空间两点欧氏长度(mm);输入为草图米。</summary>
        static double SketchSegLengthMm(double[] a, double[] b) =>
            a == null || b == null || a.Length < 3 || b.Length < 3
                ? double.NaN
                : Len3(Sub3(b, a)) * 1000.0;

        /// <summary>与 <see cref="TryBuildRectSketchSegments"/> 输出顺序一致,便于对照日志。</summary>
        static string RectContourSegLabel(int index, int segmentCount)
        {
            if (segmentCount == 4)
            {
                return index switch
                {
                    0 => "矩形底边(c0---c1)",
                    1 => "矩形右侧(c1---c2)",
                    2 => "矩形顶边(c2---c3)",
                    3 => "矩形左侧(c3---c0)",
                    _ => $"矩形边#{index}",
                };
            }

            if (segmentCount == 6)
            {
                return index switch
                {
                    0 => "矩形底边(c0---c1)",
                    1 => "矩形右竖边至倒角(c1---p2a)",
                    2 => "外侧倒角斜线(c2 角 p2a---p2b)",
                    3 => "矩形顶边整段(p2b---p3a)",
                    4 => "外侧倒角斜线(c3 角 p3a---p3b)",
                    5 => "矩形左竖边至底(p3b---c0)",
                    _ => $"矩形边#{index}",
                };
            }

            return $"矩形边#{index}";
        }

        static bool TryDrawRectOnce(
            ISldWorks swApp,
            ModelDoc2 swModel,
            double[] ra0,
            double[] ra1,
            double[] c0,
            double[] c1,
            double[] c2,
            double[] c3,
            bool drawRefSegment,
            bool sketchMeters,
            bool addToDb)
        {
            PushSketchInferOff(swApp, out bool[] inferPrev, out bool[] inferApplied);
            try
            {
                swModel.SketchManager.AddToDB = addToDb;

                Log(
                    $"草图线段调试 CreateLine入参 sketchMeters={sketchMeters} AddToDB={addToDb} " +
                    $"(端点:毫米保留 {SketchCreateLineMmDecimalPlaces} 位;闭合轮廓链式端点+首尾重合;下列日志仍为理论值 mm)");

                int segIdx = 0;
                if (drawRefSegment)
                {
                    double refLenMm = SketchSegLengthMm(ra0, ra1);
                    Log(
                        $"  线段#{segIdx} 参考线(最短边段) {SketchPointMmFmt(ra0)} --- {SketchPointMmFmt(ra1)} | " +
                        $"L={(double.IsNaN(refLenMm) ? "?" : $"{refLenMm:G9}")} mm");
                    segIdx++;
                    try
                    {
                        swModel.ClearSelection2(true);
                    }
                    catch (Exception ex)
                    {
                        Debug.WriteLine($"{LogPrefix} ClearSelection2(ref): {ex.Message}");
                    }

                    double[] pa = SketchPointForCreateLine(ra0, sketchMeters);
                    double[] pb = SketchPointForCreateLine(ra1, sketchMeters);
                    object? refObj = swModel.SketchManager.CreateLine(pa[0], pa[1], pa[2], pb[0], pb[1], pb[2]);
                    if (refObj == null)
                        return false;
                    if (refObj is SketchSegment skRef)
                    {
                        try
                        {
                            skRef.ConstructionGeometry = true;
                        }
                        catch (Exception ex)
                        {
                            Debug.WriteLine($"{LogPrefix} 参考线设构造几何: {ex.Message}");
                        }
                    }

                    if (DelayMsAfterEachCreateLine > 0)
                        Thread.Sleep(DelayMsAfterEachCreateLine);
                }

                double chamferM = ChamferAlongEdgeMm / 1000.0;
                if (!TryBuildRectSketchSegments(c0, c1, c2, c3, chamferM, out var segs))
                    return false;

                // 预计算所有量化后的端点,确保首尾完全重合
                double[][] quantizedPoints = new double[segs.Count + 1][];
                for (int i = 0; i < segs.Count; i++)
                {
                    quantizedPoints[i] = SketchPointForCreateLine(segs[i].p0, sketchMeters);
                }
                // 最后一个点的终点强制等于第一个点的起点,确保闭合
                quantizedPoints[segs.Count] = quantizedPoints[0];

                var contourChain = new List<(SketchPoint? s, SketchPoint? e)>();

                // 关键:使用原始量化点,不依赖前一段的返回值,避免SolidWorks内部微调导致的间隙
                for (int i = 0; i < segs.Count; i++)
                {
                    var (pa, pb) = segs[i];
                    double lenMm = SketchSegLengthMm(pa, pb);
                    Log(
                        $"  线段#{segIdx} {RectContourSegLabel(i, segs.Count)} " +
                        $"{SketchPointMmFmt(pa)} --- {SketchPointMmFmt(pb)} | " +
                        $"L={(double.IsNaN(lenMm) ? "?" : $"{lenMm:G9}")} mm");
                    segIdx++;

                    // 直接使用预计算的量化点,最后一段强制闭合到起点
                    double[] ra = quantizedPoints[i];
                    double[] rb = (i == segs.Count - 1) ? quantizedPoints[0] : quantizedPoints[i + 1];

                    if (!SketchCreateLineEndpoints(swModel, ra, rb, out SketchPoint? sp, out SketchPoint? ep))
                        return false;
                    contourChain.Add((sp, ep));
                }

                // 相邻线段端点逐对添加「重合」约束(树上可见);sgMERGEPOINTS 多为合并实体,不一定显示为约束
                int nChain = contourChain.Count;
                if (nChain >= 2)
                {
                    int okCoin = 0;
                    for (int i = 0; i < nChain; i++)
                    {
                        SketchPoint? atCorner = contourChain[i].e;
                        SketchPoint? nextStart = contourChain[(i + 1) % nChain].s;
                        if (TrySketchCoincident(swModel, atCorner, nextStart))
                            okCoin++;
                    }

                    Log($"闭合轮廓重合(Coincident):{okCoin}/{nChain} 处成功(首尾闭合处含于最后一项)");
                }

                SketchPoint? snapHook =
                    contourChain.Count > 0 ? contourChain[0].s ?? contourChain[0].e : null;
                TrySketchSnapClosureLikeManualDrag(swApp, swModel, snapHook);

                // 勿在此处 EditRebuild3:草图编辑中重建常会退出草图或清空 ActiveSketch,
                // 导致无法取得特征名、下一草图 InsertSketch 后 ActiveSketch 仍为空。重建见 TryEditRebuildAfterSketch(退出草图后)。

                return true;
            }
            finally
            {
                PopSketchInferOff(swApp, inferPrev, inferApplied);
                try { swModel.SketchManager.AddToDB = false; } catch { /* ignore */ }
            }
        }

        /// <summary>
        /// c0--c1 为贴参考边的底边;仅在远离底边的两角 c2、c3 做直线倒角。
        /// 返回闭合轮廓的线段列表(顺序连接)。
        /// </summary>
        static bool TryBuildRectSketchSegments(
            double[] c0,
            double[] c1,
            double[] c2,
            double[] c3,
            double chamferAlongEdgeM,
            out List<(double[] p0, double[] p1)> segments)
        {
            segments = new List<(double[], double[])>();
            if (chamferAlongEdgeM <= 1e-15)
            {
                segments.Add((c0, c1));
                segments.Add((c1, c2));
                segments.Add((c2, c3));
                segments.Add((c3, c0));
                return true;
            }

            double len12 = Len3(Sub3(c2, c1));
            double len23 = Len3(Sub3(c3, c2));
            double len30 = Len3(Sub3(c0, c3));
            if (len12 < 1e-15 || len23 < 1e-15 || len30 < 1e-15)
                return false;

            double d2 = Math.Min(chamferAlongEdgeM, Math.Min(len12, len23) * 0.499);
            double d3 = Math.Min(chamferAlongEdgeM, Math.Min(len23, len30) * 0.499);
            const double eps = 1e-11;
            if (d2 + d3 > len23 - eps)
            {
                double s = (len23 - eps) / (d2 + d3);
                d2 *= s;
                d3 *= s;
            }

            if (d2 < 1e-12 || d3 < 1e-12)
            {
                segments.Add((c0, c1));
                segments.Add((c1, c2));
                segments.Add((c2, c3));
                segments.Add((c3, c0));
                return true;
            }

            // c2:沿 c1→c2、c2→c3 各截 d2
            double[] u21 = Normalize3(Sub3(c1, c2));
            double[] u23 = Normalize3(Sub3(c3, c2));
            double[] p2a = Add3(c2, Scale3(u21, d2));
            double[] p2b = Add3(c2, Scale3(u23, d2));
            // c3:沿 c2→c3、c3→c0 各截 d3
            double[] u32 = Normalize3(Sub3(c2, c3));
            double[] u30 = Normalize3(Sub3(c0, c3));
            double[] p3a = Add3(c3, Scale3(u32, d3));
            double[] p3b = Add3(c3, Scale3(u30, d3));

            segments.Add((c0, c1));
            segments.Add((c1, p2a));
            segments.Add((p2a, p2b));
            // 顶边必须单段闭合,否则凸台拉伸易被识别为薄壁;推断问题由 SketchInferOff 与构造参考线承担。
            segments.Add((p2b, p3a));
            segments.Add((p3a, p3b));
            segments.Add((p3b, c0));

            Log(
                $"外侧直线倒角(草图米):沿边截距 C2={d2 * 1000:G4}mm C3={d3 * 1000:G4}mm | " +
                $"顶边可用长={len23 * 1000:G4}mm");
            return true;
        }

        static void CornersRect(double[] e0, double[] e1, double[] perp, double h, out double[] c0, out double[] c1, out double[] c2, out double[] c3)
        {
            double[] o = Scale3(perp, h);
            c0 = e0;
            c1 = e1;
            c2 = Add3(e1, o);
            c3 = Add3(e0, o);
        }

        /// <summary>在草图平面内、垂直于边切向 t,取指向 ±n_up 一侧的单位法向。</summary>
        static void BuildInPlanePerpToward(double[] nUp, double[] t, bool towardPositiveNUp, out double[] perp)
        {
            double dt = Dot3(nUp, t);
            double[] proj =
            {
                nUp[0] - dt * t[0],
                nUp[1] - dt * t[1],
                nUp[2] - dt * t[2],
            };
            double len = Len3(proj);
            if (len < 1e-12)
                proj = new double[] { -t[1], t[0], 0 };
            len = Len3(proj);
            if (len < 1e-12)
            {
                perp = new double[] { 0, 0, 1 };
                len = 1;
            }
            perp = Scale3(proj, 1.0 / len);
            double dn = Dot3(perp, nUp);
            if (towardPositiveNUp && dn < 0 || !towardPositiveNUp && dn > 0)
                perp = Scale3(perp, -1.0);
        }

        static double[] Mid3(double[] a, double[] b) =>
            new[]
            {
                (a[0] + b[0]) * 0.5,
                (a[1] + b[1]) * 0.5,
                (a[2] + b[2]) * 0.5,
            };

        static double[] Sub3(double[] a, double[] b) =>
            new[] { a[0] - b[0], a[1] - b[1], a[2] - b[2] };

        static double[] Add3(double[] a, double[] b) =>
            new[] { a[0] + b[0], a[1] + b[1], a[2] + b[2] };

        static double[] Scale3(double[] a, double s) =>
            new[] { a[0] * s, a[1] * s, a[2] * s };

        static double Dot3(double[] a, double[] b) =>
            a[0] * b[0] + a[1] * b[1] + a[2] * b[2];

        static double Len3(double[] v) =>
            Math.Sqrt(v[0] * v[0] + v[1] * v[1] + v[2] * v[2]);

        static double[] Normalize3(double[] v)
        {
            double L = Len3(v);
            if (L < 1e-15)
                return new double[] { 1, 0, 0 };
            return Scale3(v, 1.0 / L);
        }

        static string Vec3Fmt(double[] v) =>
            v == null || v.Length < 3 ? "?" : $"({v[0]:G9},{v[1]:G9},{v[2]:G9})";

        static bool TryGetCenteredChordSegmentModel(
            double[] p0,
            double[] p1,
            double segmentLengthM,
            out double[]? a,
            out double[]? b)
        {
            a = b = null;
            double dx = p1[0] - p0[0], dy = p1[1] - p0[1], dz = p1[2] - p0[2];
            double L = Math.Sqrt(dx * dx + dy * dy + dz * dz);
            if (L < 1e-15)
                return false;
            dx /= L;
            dy /= L;
            dz /= L;
            double mx = (p0[0] + p1[0]) * 0.5;
            double my = (p0[1] + p1[1]) * 0.5;
            double mz = (p0[2] + p1[2]) * 0.5;

            if (L <= segmentLengthM + 1e-12)
            {
                a = new[] { p0[0], p0[1], p0[2] };
                b = new[] { p1[0], p1[1], p1[2] };
                return true;
            }

            double h = segmentLengthM * 0.5;
            a = new[] { mx - dx * h, my - dy * h, mz - dz * h };
            b = new[] { mx + dx * h, my + dy * h, mz + dz * h };
            return true;
        }

        static bool TryGetEdgeVerticesModel(Edge edge, out double[]? p0, out double[]? p1)
        {
            p0 = p1 = null;
            try
            {
                var v1 = edge.GetStartVertex() as Vertex;
                var v2 = edge.GetEndVertex() as Vertex;
                if (v1 == null || v2 == null)
                    return false;
                p0 = (double[])v1.GetPoint();
                p1 = (double[])v2.GetPoint();
                return p0 != null && p1 != null && p0.Length >= 3 && p1.Length >= 3;
            }
            catch (Exception ex)
            {
                Debug.WriteLine($"TryGetEdgeVerticesModel: {ex.Message}");
                return false;
            }
        }

        static double GetEdgeLengthMeters(Edge edge)
        {
            try
            {
                var v1 = edge.GetStartVertex() as Vertex;
                var v2 = edge.GetEndVertex() as Vertex;
                if (v1 != null && v2 != null)
                {
                    var p1 = (double[])v1.GetPoint();
                    var p2 = (double[])v2.GetPoint();
                    double dx = p2[0] - p1[0], dy = p2[1] - p1[1], dz = p2[2] - p1[2];
                    return Math.Sqrt(dx * dx + dy * dy + dz * dz);
                }

                var c = (Curve)edge.GetCurve();
                if (c == null)
                    return double.MaxValue;

                if (c.IsCircle())
                {
                    c.GetEndParams(out double t0, out double t1, out _, out _);
                    var cp = (double[])c.CircleParams;
                    if (cp != null && cp.Length > 6)
                        return Math.Abs(t1 - t0) * cp[6];
                }

                c.GetEndParams(out double a0, out double a1, out _, out _);
                if (TryCurveEvaluatePoint3D(c, a0, out var pt0) &&
                    TryCurveEvaluatePoint3D(c, a1, out var pt1))
                {
                    double dx = pt1[0] - pt0[0], dy = pt1[1] - pt0[1], dz = pt1[2] - pt0[2];
                    return Math.Sqrt(dx * dx + dy * dy + dz * dz);
                }
            }
            catch (Exception ex)
            {
                Debug.WriteLine($"GetEdgeLengthMeters: {ex.Message}");
            }

            return double.MaxValue;
        }

        static bool TryModelPointToActiveSketch(
            MathUtility math,
            ModelDoc2 swModel,
            double[] model3,
            out double[] sketch3)
        {
            sketch3 = Array.Empty<double>();
            try
            {
                var sk = (Sketch)swModel.SketchManager.ActiveSketch;
                var mt = (MathTransform)sk.ModelToSketchTransform;
                if (mt == null)
                    return false;

                var mp = (MathPoint)math.CreatePoint(model3);
                mp = (MathPoint)mp.MultiplyTransform(mt);
                var arr = (double[])mp.ArrayData;
                if (arr == null || arr.Length < 3)
                    return false;
                sketch3 = new[] { arr[0], arr[1], arr[2] };
                return true;
            }
            catch (Exception ex)
            {
                Debug.WriteLine($"TryModelPointToActiveSketch: {ex.Message}");
                return false;
            }
        }

        static bool TryCurveEvaluatePoint3D(Curve curve, double t, out double[]? pt)
        {
            pt = null;
            try
            {
                object ev = curve.Evaluate(t);
                if (ev is double[] da && da.Length >= 3)
                {
                    pt = new[] { da[0], da[1], da[2] };
                    return true;
                }

                if (ev is object[] oa && oa.Length >= 3 &&
                    TryCoerceToDouble(oa[0], out var x) &&
                    TryCoerceToDouble(oa[1], out var y) &&
                    TryCoerceToDouble(oa[2], out var z))
                {
                    pt = new[] { x, y, z };
                    return true;
                }
            }
            catch (Exception ex)
            {
                Debug.WriteLine($"TryCurveEvaluatePoint3D: {ex.Message}");
            }

            return false;
        }

        static bool TryCoerceToDouble(object? o, out double v)
        {
            v = 0;
            if (o == null) return false;
            switch (o)
            {
                case double d:
                    v = d;
                    return true;
                case float f:
                    v = f;
                    return true;
                case int i:
                    v = i;
                    return true;
                default:
                    return double.TryParse(o.ToString(), out v);
            }
        }

        /// <summary>
        /// 执行草图修复,自动闭合微小间隙、合并重叠线段等。
        /// 策略:降低量化精度到4位小数(mm),减少浮点误差;退出草图前强制重建。
        /// </summary>
        static void TryRepairSketch(ISldWorks swApp, ModelDoc2 swModel, bool sketchMeters)
        {
            try
            {
                // 在退出草图前执行一次重建,让SolidWorks内部优化几何
                swModel.EditRebuild3();
                Log("草图绘制完成,已执行EditRebuild3优化几何");
            }
            catch (Exception ex)
            {
                Debug.WriteLine($"{LogPrefix} TryRepairSketch: {ex.Message}");
            }
        }

        /// <summary>获取线段的起点</summary>
        static SketchPoint? GetSegmentStartPoint(SketchSegment segment)
        {
            try
            {
                if (segment is SketchLine line)
                    return (SketchPoint?)line.GetStartPoint2();
                else if (segment is SketchArc arc)
                    return (SketchPoint?)arc.GetStartPoint2();
            }
            catch { }
            return null;
        }

        /// <summary>获取线段的终点</summary>
        static SketchPoint? GetSegmentEndPoint(SketchSegment segment)
        {
            try
            {
                if (segment is SketchLine line)
                    return (SketchPoint?)line.GetEndPoint2();
                else if (segment is SketchArc arc)
                    return (SketchPoint?)arc.GetEndPoint2();
            }
            catch { }
            return null;
        }
    }
}
相关推荐
时光追逐者1 小时前
一款基于 C# 开发的 Windows 10/11 系统增强工具,精简、优化、定制一站完成!
开发语言·windows·c#·.net
绿豆人1 小时前
进入内核-中断开启
开发语言·c#
前端达人1 小时前
第18课:实战案例二,线上紧急 Bug 修复全过程
bug
步步为营DotNet1 小时前
.NET 11 中 C# 14 新特性在云原生微服务安全与性能优化的深度探索
云原生·c#·.net
代钦塔拉2 小时前
Qt 按钮 Lambda 信号槽重复绑定、多次触发 BUG 深度剖析与终极解决方案
c++·qt·bug
工程师0072 小时前
C# foreach 为什么不能增删、迭代器底层原理、版本号机制、以及所有能遍历中增删的方案
c#·foreach·迭代器底层
顾温10 小时前
default——C#/C++
java·c++·c#
InCerry11 小时前
.NET性能优化:提升Apache Arrow读写性能
c#·.net周刊
黑咩狗夜.cm15 小时前
(aspose.words .net)内容分别固定在一行左右俩端
c#·word·.net