在这里插入图片描述
cursor真香

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>
    /// 选中面上最短两条直线边(须为几何直线且长度不小于 <see cref="MinStraightEdgeLengthMm"/> mm);在每条边上以边为「宽」边、
    /// 垂直于边朝另一边的方向画 10×3 mm 矩形(CreateLine)。较高边一侧矩形朝「远离另一边」外扩(向上),较低边朝相反方向(向下)。
    /// 矩形贴底边的对侧两角(外侧)做直线倒角(沿两邻边各截一段,非圆角),尺寸见 <see cref="ChamferAlongEdgeMm"/>。
    /// 上侧与下侧矩形分两个独立草图绘制(同一面各 InsertSketch 一次)。
    /// 每个草图绘制完成后在退出编辑前做凸台拉伸:成形到面(<see cref="swEndConditions_e.swEndCondUpToSurface"/>),
    /// 在零件实体上找与草图所在面平行、且与草图面垂直距离最小(大于容差)的平面作为终止面;顶边为单段闭合轮廓以便拉伸(非薄壁)。
    /// 草图坐标与 ModelToSketchTransform 一致时为米;<see cref="SketchManager.CreateLine"/> 前将端点换为毫米量化(见 <see cref="SketchCreateLineMmDecimalPlaces"/>),
    /// 闭合轮廓采用链式端点且末段回到首点坐标,避免首尾缝隙。
    /// 矩形底边约束到对应最短模型边(共线 + 端点落在边上 + 相对边端点距离 + 底边长度 10mm + 首条竖边垂直),
    /// 方管伸长/改尺寸后草图与凸台随边移动,避免「白拉伸」。
    /// </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}");
                }
            }
        }

        /// <summary>关闭「输入尺寸值」弹窗,批量 AddDimension2 后改 SystemValue 无需每次手动确认(与 benddim 一致)。</summary>
        static void PushInputDimValOff(ISldWorks swApp, out bool prev, out bool applied)
        {
            prev = false;
            applied = false;
            if (swApp == null)
                return;
            try
            {
                prev = swApp.GetUserPreferenceToggle((int)swUserPreferenceToggle_e.swInputDimValOnCreate);
                swApp.SetUserPreferenceToggle((int)swUserPreferenceToggle_e.swInputDimValOnCreate, false);
                applied = true;
            }
            catch (Exception ex)
            {
                Debug.WriteLine($"{LogPrefix} PushInputDimValOff: {ex.Message}");
            }
        }

        static void PopInputDimValOff(ISldWorks swApp, bool prev, bool applied)
        {
            if (!applied || swApp == null)
                return;
            try
            {
                swApp.SetUserPreferenceToggle((int)swUserPreferenceToggle_e.swInputDimValOnCreate, prev);
            }
            catch (Exception ex)
            {
                Debug.WriteLine($"{LogPrefix} PopInputDimValOff: {ex.Message}");
            }
        }
        const double SegmentLengthMm = 10.0;
        /// <summary>参与「最短两边」筛选:仅直线边,且几何长度不小于该值(mm)。</summary>
        const double MinStraightEdgeLengthMm = 20.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;
            }

            RunOnPartFace(swApp, swModel, targetFace);
        }

        /// <summary>
        /// 装配体中选中零件上的面时调用:解析 <see cref="Component2"/> 与 <see cref="Component2.GetCorresponding"/> 得到零件文档中的面,
        /// 激活零件窗口后与当前活动文档指针对齐,再执行与 <see cref="run"/> 相同流程;草图选面在 <see cref="TryDrawHalfSketch"/> 内可射线重选。
        /// </summary>
        public static void run_from_assembly(ISldWorks swApp, ModelDoc2 asmDoc)
        {
            Log("开始(装配体):最短两边矩形 — 将选中面映射到零件文档后执行");

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

            if (asmDoc.GetType() != (int)swDocumentTypes_e.swDocASSEMBLY)
            {
                Log("中止:当前不是装配体文档");
                swApp.SendMsgToUser("请在装配体文档中使用本命令。");
                return;
            }

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

            Face2? asmFace = null;
            Component2? comp = null;
            int n = selMgr.GetSelectedObjectCount2(-1);
            for (int i = 1; i <= n; i++)
            {
                int t = selMgr.GetSelectedObjectType3(i, -1);
                if (t == (int)swSelectType_e.swSelFACES)
                {
                    asmFace = selMgr.GetSelectedObject6(i, -1) as Face2;
                    comp = (Component2)selMgr.GetSelectedObjectsComponent3(i, -1);
                    break;
                }
            }

            if (asmFace == null)
            {
                Log($"中止:选中项中无面(当前共 {n} 个选中对象)");
                swApp.SendMsgToUser("请在装配体中选中零件上的一个面。");
                return;
            }

            if (comp == null)
            {
                Log("中止:无组件上下文");
                swApp.SendMsgToUser("无法取得该面所属的零件实例,请直接选中零件上的面。");
                return;
            }

            ModelDoc2? partDoc = TryEnsurePartDocFromComponent(swApp, comp);
            if (partDoc == null || partDoc.GetType() != (int)swDocumentTypes_e.swDocPART)
            {
                Log("中止:无法打开零件文档(组件可能未解析)");
                swApp.SendMsgToUser("无法打开该面所属的零件文档,请确认零件已加载。");
                return;
            }

            Face2? partFace = null;
            try
            {
                object? corrObj = comp.GetCorresponding((Entity)asmFace);
                partFace = corrObj as Face2;
            }
            catch (Exception ex)
            {
                Log($"GetCorresponding 异常: {ex.Message}");
            }

            if (partFace == null)
            {
                Log("中止:GetCorresponding 未返回 Face2");
                swApp.SendMsgToUser("无法将装配体中的面映射到零件文档(GetCorresponding)。");
                return;
            }

            int activateErr = 0;
            string partTitle = partDoc.GetTitle() ?? "";
            if (!string.IsNullOrEmpty(partTitle))
            {
                swApp.ActivateDoc3(
                    partTitle,
                    true,
                    (int)swRebuildOnActivation_e.swDontRebuildActiveDoc,
                    ref activateErr);
            }

            partDoc = ResolveSketchTargetDoc(swApp, partDoc);

            RunOnPartFace(swApp, partDoc, partFace);
        }

        static ModelDoc2? TryEnsurePartDocFromComponent(ISldWorks swApp, Component2 comp)
        {
            if (comp == null || swApp == null)
                return null;

            ModelDoc2? partDoc = comp.GetModelDoc2() as ModelDoc2;
            if (partDoc == null)
            {
                string path = comp.GetPathName() ?? "";
                if (!string.IsNullOrEmpty(path))
                {
                    try
                    {
                        int errors = 0;
                        int warnings = 0;
                        partDoc = swApp.OpenDoc6(
                            path,
                            (int)swDocumentTypes_e.swDocPART,
                            (int)swOpenDocOptions_e.swOpenDocOptions_Silent,
                            "",
                            ref errors,
                            ref warnings) as ModelDoc2;
                    }
                    catch (Exception ex)
                    {
                        Log($"OpenDoc6 零件失败: {ex.Message}");
                    }
                }
            }

            if (partDoc == null)
                return null;

            try
            {
                partDoc.Visible = true;
            }
            catch (Exception ex)
            {
                Log($"设置零件 Visible: {ex.Message}");
            }

            return partDoc;
        }

        /// <summary>在零件文档坐标系下对给定面执行最短边矩形草图与拉伸(与 <see cref="run"/> 核心逻辑相同)。</summary>
        public static void RunOnPartFace(ISldWorks swApp, ModelDoc2 partModel, Face2 targetFace)
        {
            if (swApp == null || partModel == null || targetFace == null)
            {
                Log("RunOnPartFace:参数为空");
                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;
            }

            double minLenM = MinStraightEdgeLengthMm / 1000.0;
            var edgesWithLen = new List<(Edge edge, double len)>();
            foreach (object o in edgeObjs)
            {
                if (o is not Edge e)
                    continue;
                Curve? crv = null;
                try
                {
                    crv = e.GetCurve() as Curve;
                }
                catch
                {
                    continue;
                }

                if (crv == null || !crv.IsLine())
                    continue;

                double len = GetEdgeLengthMeters(e);
                if (len + 1e-12 < minLenM)
                    continue;

                edgesWithLen.Add((e, len));
            }

            if (edgesWithLen.Count < 2)
            {
                Log($"中止:直线边且长度≥{MinStraightEdgeLengthMm}mm 的边不足 2 条");
                swApp.SendMsgToUser(
                    $"该面上符合条件的直线边少于 2 条(须为直线且长度不小于 {MinStraightEdgeLengthMm} mm)。");
                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, partModel, targetFace, edgeA, edgeB, segA0, segA1, segB0, segB1,
                upperHalf: true, out modeUpper);
            bool drewLower = TryDrawHalfSketch(
                swApp, partModel, targetFace, edgeA, edgeB, 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>
        /// 装配体中 ActivateDoc3 后,传入的 <see cref="ModelDoc2"/> 与当前活动文档可能不是同一 COM 包装;
        /// 草图 API 必须以 <see cref="ISldWorks.ActiveDoc"/> 为准,否则 <c>InsertSketch(true)</c> 后 <c>ActiveSketch</c> 仍为空。
        /// </summary>
        static ModelDoc2 ResolveSketchTargetDoc(ISldWorks swApp, ModelDoc2 requested)
        {
            if (requested == null || swApp == null)
                return requested;

            ModelDoc2? active = swApp.ActiveDoc as ModelDoc2;
            if (active == null)
                return requested;

            string rqPath = requested.GetPathName()?.Trim() ?? "";
            string acPath = active.GetPathName()?.Trim() ?? "";
            if (!string.IsNullOrEmpty(rqPath) && !string.IsNullOrEmpty(acPath) &&
                string.Equals(rqPath, acPath, StringComparison.OrdinalIgnoreCase))
                return active;

            string rqTitle = requested.GetTitle()?.Trim() ?? "";
            string acTitle = active.GetTitle()?.Trim() ?? "";
            if (!string.IsNullOrEmpty(rqTitle) && !string.IsNullOrEmpty(acTitle) &&
                string.Equals(rqTitle, acTitle, StringComparison.OrdinalIgnoreCase))
                return active;

            return requested;
        }

        /// <summary>为新建草图选中承载面:先试 <see cref="Entity.Select4"/>,失败则用射线(装配体映射面激活零件后更可靠)。</summary>
        static bool TrySelectSketchHostFace(ModelDoc2 doc, Face2 face, string tag)
        {
            doc.ClearSelection2(true);
            try
            {
                if (((Entity)face).Select4(false, null))
                    return true;
            }
            catch (Exception ex)
            {
                Log($"{tag}:Select4 异常: {ex.Message}");
            }

            return TrySelectSketchHostFaceByRay(doc, face, tag);
        }

        static bool TrySelectSketchHostFaceByRay(ModelDoc2 doc, Face2 face, string tag)
        {
            if (!TryGetFaceRepresentativePoint(face, out double[] p) || p.Length < 3)
            {
                Log($"{tag}:射线选面失败(无面上点)");
                return false;
            }

            if (!TryGetPlanarUnitNormal(face, out double[] n) || n.Length < 3)
            {
                Log($"{tag}:射线选面失败(非平面或无平面法向)");
                return false;
            }

            const double eps = 2e-4;
            double ox = p[0] + n[0] * eps;
            double oy = p[1] + n[1] * eps;
            double oz = p[2] + n[2] * eps;

            try
            {
                bool ok = doc.Extension.SelectByRay(ox, oy, oz, -n[0], -n[1], -n[2], 0.0001, 1, false, 0, 0);
                if (!ok)
                    Log($"{tag}:SelectByRay 返回 false(面上点 {Vec3Fmt(p)})");
                return ok;
            }
            catch (Exception ex)
            {
                Log($"{tag}:SelectByRay 异常: {ex.Message}");
                return false;
            }
        }

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

            ModelDoc2 doc = ResolveSketchTargetDoc(swApp, swModel);
            if (!ReferenceEquals(doc, swModel))
                Log($"{tag}:使用 ActiveDoc 作为草图文档(与传入 ModelDoc 对齐)");

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

            bool sketchEntered = false;
            for (int attempt = 0; attempt < 2 && !sketchEntered; attempt++)
            {
                bool selOk = attempt == 0
                    ? TrySelectSketchHostFace(doc, sketchHostFace, tag)
                    : TrySelectSketchHostFaceByRay(doc, sketchHostFace, tag);

                if (!selOk)
                {
                    Log($"{tag}:选面失败(尝试 {attempt + 1}/2)");
                    if (attempt == 1)
                        return false;
                    continue;
                }

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

                if (doc.SketchManager.ActiveSketch != null)
                {
                    sketchEntered = true;
                    break;
                }

                Log($"{tag}:InsertSketch 后 ActiveSketch 为空(尝试 {attempt + 1}/2),将改用射线重选面");
                try
                {
                    if (doc.SketchManager.ActiveSketch != null)
                        doc.SketchManager.InsertSketch(false);
                }
                catch { /* ignore */ }
            }

            if (!sketchEntered || doc.SketchManager.ActiveSketch == null)
            {
                Log($"{tag}:无法进入草图(含射线重选后仍失败)");
                return false;
            }

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

            if (!TryComputeSketchRects(
                    math, doc, 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 { doc.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;

            Edge anchorEdge = upperHalf
                ? (aIsHigher ? edgeA : edgeB)
                : (aIsHigher ? edgeB : edgeA);
            double[] anchorModelSeg0 = upperHalf
                ? (aIsHigher ? segA0 : segB0)
                : (aIsHigher ? segB0 : segA0);

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

                            if (TryFeatureExtrudeUpToNearestParallelFace(
                                    doc, sketchHostFace, skFeatName, pickOnHostFace, tag))
                                modeUsed += ";已拉伸(成形到最近平行面)";
                            else
                                Log($"{tag}:成形到面拉伸失败");
                        }
                    }
                }
                else
                    Log($"{tag}:CreateLine 全部策略失败");
            }
            catch (Exception ex)
            {
                Log($"{tag}:CreateLine 异常: {ex.Message}");
            }
            finally
            {
                try
                {
                    if (doc.SketchManager.ActiveSketch != null)
                        doc.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;
            return TrySketchAddConstraint(swModel, "sgCOINCIDENT", a, b);
        }

        /// <summary>COM 对象未必实现 <see cref="Entity"/>,用反射调用 Select4(与宏里直接选 SKETCHSEGMENT 等价)。</summary>
        static bool TryComSelect4(object? comObj, bool append)
        {
            if (comObj == null)
                return false;
            try
            {
                if (comObj is Entity ent)
                    return ent.Select4(append, null);
            }
            catch { /* fall through */ }

            try
            {
                object? result = comObj.GetType().InvokeMember(
                    "Select4",
                    BindingFlags.InvokeMethod | BindingFlags.Public | BindingFlags.Instance,
                    null,
                    comObj,
                    new object[] { append, null });
                return result is bool b && b;
            }
            catch (Exception ex)
            {
                Debug.WriteLine($"{LogPrefix} TryComSelect4: {ex.Message}");
                return false;
            }
        }

        static string? TryGetComString(object comObj, string memberName)
        {
            try
            {
                if (memberName == "GetName")
                {
                    object? v = comObj.GetType().InvokeMember(
                        "GetName",
                        BindingFlags.InvokeMethod | BindingFlags.Public | BindingFlags.Instance,
                        null,
                        comObj,
                        Array.Empty<object>());
                    return v as string;
                }

                object? p = comObj.GetType().InvokeMember(
                    memberName,
                    BindingFlags.GetProperty | BindingFlags.Public | BindingFlags.Instance,
                    null,
                    comObj,
                    null);
                return p as string;
            }
            catch
            {
                return null;
            }
        }

        static bool TrySelectSketchSegment(ModelDoc2 swModel, SketchSegment seg, bool append)
        {
            if (TryComSelect4(seg, append))
                return true;

            string? name = TryGetComString(seg, "GetName");
            if (string.IsNullOrEmpty(name))
                return false;

            double x = 0, y = 0, z = 0;
            SketchPoint? sp = GetSegmentStartPoint(seg);
            if (sp != null)
            {
                x = sp.X;
                y = sp.Y;
                z = sp.Z;
            }

            try
            {
                return swModel.Extension.SelectByID2(name, "SKETCHSEGMENT", x, y, z, append, 0, null, 0);
            }
            catch (Exception ex)
            {
                Debug.WriteLine($"{LogPrefix} TrySelectSketchSegment SelectByID2: {ex.Message}");
                return false;
            }
        }

        /// <summary>草图编辑中选模型边:与宏一致,草图线 SelectByID2 + 边 SelectByRay。</summary>
        static bool TrySelectModelEdgeByRay(
            ModelDoc2 swModel,
            Edge edge,
            Face2? hostFace,
            bool append)
        {
            if (!TryGetEdgeVerticesModel(edge, out double[]? v0, out double[]? v1) ||
                v0 == null || v1 == null)
                return false;

            double[] mid = Mid3(v0, v1);
            double[] axis = Normalize3(Sub3(v1, v0));
            double[] ray = { 1, 0, 0 };
            if (hostFace != null && TryGetPlanarUnitNormal(hostFace, out double[] n) && n.Length >= 3)
                ray = n;
            else if (Math.Abs(axis[0]) < 0.9)
                ray = Normalize3(Cross3(axis, new double[] { 1, 0, 0 }));
            else
                ray = Normalize3(Cross3(axis, new double[] { 0, 1, 0 }));

            const double eps = 2e-4;
            double ox = mid[0] + ray[0] * eps;
            double oy = mid[1] + ray[1] * eps;
            double oz = mid[2] + ray[2] * eps;
            try
            {
                return swModel.Extension.SelectByRay(
                    ox, oy, oz, -ray[0], -ray[1], -ray[2], 0.0001, 1, append, 0, 0);
            }
            catch (Exception ex)
            {
                Debug.WriteLine($"{LogPrefix} TrySelectModelEdgeByRay: {ex.Message}");
                return false;
            }
        }

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

        static bool TrySketchAddConstraint(ModelDoc2 swModel, string constraintId, params object?[] items)
        {
            if (items == null || items.Length == 0)
                return false;
            try
            {
                swModel.ClearSelection2(true);
                for (int i = 0; i < items.Length; i++)
                {
                    if (items[i] == null)
                        return false;
                    bool append = i > 0;
                    bool picked = items[i] switch
                    {
                        SketchSegment seg => TrySelectSketchSegment(swModel, seg, append),
                        Edge edge => TrySelectModelEdgeByRay(swModel, edge, null, append),
                        _ => TryComSelect4(items[i], append),
                    };
                    if (!picked)
                        return false;
                }

                swModel.SketchAddConstraints(constraintId);
                return true;
            }
            catch (Exception ex)
            {
                Debug.WriteLine($"{LogPrefix} TrySketchAddConstraint {constraintId}: {ex.Message}");
                return false;
            }
        }

        static bool TrySketchColinearSegmentToEdge(
            ModelDoc2 swModel,
            Face2? hostFace,
            SketchSegment sketchSeg,
            Edge modelEdge)
        {
            try
            {
                swModel.ClearSelection2(true);
                if (!TrySelectSketchSegment(swModel, sketchSeg, false))
                    return false;
                if (!TrySelectModelEdgeByRay(swModel, modelEdge, hostFace, append: true))
                    return false;
                swModel.SketchAddConstraints("sgCOLINEAR");
                return true;
            }
            catch (Exception ex)
            {
                Debug.WriteLine($"{LogPrefix} TrySketchColinearSegmentToEdge: {ex.Message}");
                return false;
            }
        }

        /// <summary>
        /// 将矩形底边钉在对应最短模型边上,并标 10×3 mm,使方管改长后槽位随边移动、凸台仍有效。
        /// </summary>
        static void TrySketchConstrainRectToHostEdge(
            ISldWorks swApp,
            ModelDoc2 swModel,
            Face2? sketchHostFace,
            Edge anchorEdge,
            double[] anchorSeg0Model,
            IReadOnlyList<(SketchSegment? seg, SketchPoint? s, SketchPoint? e)> contourChain,
            SketchSegment? bottomSeg,
            SketchPoint? ptC0,
            SketchPoint? ptC1,
            SketchSegment? firstVerticalSeg)
        {
            if (anchorEdge == null || bottomSeg == null || ptC0 == null || ptC1 == null)
                return;

            int contourSegmentCount = contourChain.Count;
            int topIdx = GetRectTopSegmentIndex(contourSegmentCount);
            SketchSegment? topSeg =
                topIdx >= 0 && topIdx < contourChain.Count ? contourChain[topIdx].seg : null;

            PushInputDimValOff(swApp, out bool prevInputDim, out bool inputDimOff);
            bool prevAutoSolve = false;
            bool gotAutoSolve = false;
            try
            {
                prevAutoSolve = swModel.SketchManager.AutoSolve;
                gotAutoSolve = true;
                swModel.SketchManager.AutoSolve = true;
            }
            catch { /* ignore */ }

            int ok = 0;
            int tried = 0;

            tried++;
            if (TrySketchColinearSegmentToEdge(swModel, sketchHostFace, bottomSeg, anchorEdge))
                ok++;

            tried++;
            if (TrySketchOnEdge(swModel, sketchHostFace, ptC0, anchorEdge))
                ok++;
            else
            {
                tried++;
                if (TrySketchOnEdge(swModel, sketchHostFace, ptC0, anchorEdge, usePierce: true))
                    ok++;
            }

            tried++;
            if (TrySketchOnEdge(swModel, sketchHostFace, ptC1, anchorEdge))
                ok++;
            else
            {
                tried++;
                if (TrySketchOnEdge(swModel, sketchHostFace, ptC1, anchorEdge, usePierce: true))
                    ok++;
            }

            if (firstVerticalSeg != null)
            {
                tried++;
                if (TrySketchAddConstraint(swModel, "sgPERPENDICULAR", bottomSeg, firstVerticalSeg))
                    ok++;
            }

            if (TryGetEdgeVerticesModel(anchorEdge, out double[]? ev0, out double[]? ev1) &&
                ev0 != null && ev1 != null &&
                anchorSeg0Model != null && anchorSeg0Model.Length >= 3)
            {
                Vertex? vStart = PickEdgeVertexCloserToModelPoint(anchorEdge, ev0, ev1, anchorSeg0Model);
                if (vStart != null)
                {
                    var edgeStartVtx = anchorEdge.GetStartVertex() as Vertex;
                    bool fromStart = ComObjectIdentityEquals(vStart, edgeStartVtx);
                    double distFromStartM = DistAlongEdgeFromVertexM(
                        fromStart ? ev0 : ev1,
                        fromStart ? ev1 : ev0,
                        anchorSeg0Model);
                    tried++;
                    if (TrySketchSetDistanceBetweenPointAndVertex(swModel, ptC0, vStart, distFromStartM))
                        ok++;
                }
            }

            tried++;
            if (TrySketchSetSegmentLength(swModel, bottomSeg, SegmentLengthMm / 1000.0))
                ok++;

            if (topSeg != null)
            {
                tried++;
                if (TrySketchAddConstraint(swModel, "sgPARALLEL", bottomSeg, topSeg))
                    ok++;

                tried++;
                if (TrySketchSetDistanceBetweenTwoSegments(
                        swModel, bottomSeg, topSeg, RectLengthMm / 1000.0))
                    ok++;
            }
            else
            {
                Log($"矩形尺寸:无顶边线段(轮廓 {contourSegmentCount} 段),跳过底—顶 3mm 线间距");
            }

            int okContour = TrySketchConstrainFullContour(swModel, contourChain);
            ok += okContour;

            Log(
                $"矩形钉边约束:成功 {ok}/{tried}+轮廓{okContour} " +
                $"(共线+边上点+竖直+距端点+底边10mm+底顶3mm+其余线段尺寸/相等)");

            if (gotAutoSolve)
            {
                try
                {
                    swModel.SketchManager.AutoSolve = prevAutoSolve;
                }
                catch { /* ignore */ }
            }

            PopInputDimValOff(swApp, prevInputDim, inputDimOff);
        }

        /// <summary>倒角/直角轮廓其余线段用相等+垂直+长度尺寸一次标全,避免欠约束蓝线。</summary>
        static int TrySketchConstrainFullContour(
            ModelDoc2 swModel,
            IReadOnlyList<(SketchSegment? seg, SketchPoint? s, SketchPoint? e)> chain)
        {
            if (chain.Count == 6)
                return TrySketchConstrainChamferContourSix(swModel, chain);
            if (chain.Count == 4)
                return TrySketchConstrainRectContourFour(swModel, chain);
            return 0;
        }

        static int TrySketchConstrainChamferContourSix(
            ModelDoc2 swModel,
            IReadOnlyList<(SketchSegment? seg, SketchPoint? s, SketchPoint? e)> chain)
        {
            SketchSegment? bottom = chain[0].seg;
            SketchSegment? rightVert = chain[1].seg;
            SketchSegment? chamferA = chain[2].seg;
            SketchSegment? top = chain[3].seg;
            SketchSegment? chamferB = chain[4].seg;
            SketchSegment? leftVert = chain[5].seg;
            if (bottom == null || rightVert == null || chamferA == null || top == null ||
                chamferB == null || leftVert == null)
                return 0;

            int ok = 0;
            if (TrySketchAddConstraint(swModel, "sgPERPENDICULAR", bottom, rightVert))
                ok++;
            if (TrySketchAddConstraint(swModel, "sgPERPENDICULAR", bottom, leftVert))
                ok++;
            if (TrySketchAddConstraint(swModel, "sgPARALLEL", rightVert, leftVert))
                ok++;
            if (TrySketchAddConstraint(swModel, "sgPARALLEL", top, bottom))
                ok++;
            // 两外侧倒角斜边为镜面对称(走向相反),不可 sgPARALLEL,否则会扭坏轮廓

            double vertLegM = (RectLengthMm - ChamferAlongEdgeMm) / 1000.0;
            double chamferLenM = Math.Sqrt(2.0) * ChamferAlongEdgeMm / 1000.0;
            double topLenM = (SegmentLengthMm - 2.0 * ChamferAlongEdgeMm) / 1000.0;
            double bottomLenM = SegmentLengthMm / 1000.0;

            // 六条线段全部标长度(底边若已标过会多一次,SolidWorks 通常可共存或忽略)
            if (TrySketchSetSegmentLength(swModel, bottom, bottomLenM))
                ok++;
            if (TrySketchSetSegmentLength(swModel, rightVert, vertLegM))
                ok++;
            if (TrySketchSetSegmentLength(swModel, chamferA, chamferLenM))
                ok++;
            if (TrySketchSetSegmentLength(swModel, top, topLenM))
                ok++;
            if (TrySketchSetSegmentLength(swModel, chamferB, chamferLenM))
                ok++;
            if (TrySketchSetSegmentLength(swModel, leftVert, vertLegM))
                ok++;

            // 45° 倒角:四角夹角(先试 135° 外角,失败再试 45°)
            if (TrySketchSetChamferCornerAngle(swModel, rightVert, chamferA))
                ok++;
            if (TrySketchSetChamferCornerAngle(swModel, chamferA, top))
                ok++;
            if (TrySketchSetChamferCornerAngle(swModel, top, chamferB))
                ok++;
            if (TrySketchSetChamferCornerAngle(swModel, chamferB, leftVert))
                ok++;

            return ok;
        }

        static bool TrySketchSetChamferCornerAngle(
            ModelDoc2 swModel,
            SketchSegment segA,
            SketchSegment segB)
        {
            const double rad135 = 135.0 * Math.PI / 180.0;
            const double rad45 = 45.0 * Math.PI / 180.0;
            if (TrySketchSetAngleBetweenSegments(swModel, segA, segB, rad135))
                return true;
            return TrySketchSetAngleBetweenSegments(swModel, segA, segB, rad45);
        }

        static int TrySketchConstrainRectContourFour(
            ModelDoc2 swModel,
            IReadOnlyList<(SketchSegment? seg, SketchPoint? s, SketchPoint? e)> chain)
        {
            SketchSegment? bottom = chain[0].seg;
            SketchSegment? right = chain[1].seg;
            SketchSegment? top = chain[2].seg;
            SketchSegment? left = chain[3].seg;
            if (bottom == null || right == null || top == null || left == null)
                return 0;

            int ok = 0;
            if (TrySketchAddConstraint(swModel, "sgPERPENDICULAR", bottom, right))
                ok++;
            if (TrySketchAddConstraint(swModel, "sgPERPENDICULAR", bottom, left))
                ok++;
            if (TrySketchAddConstraint(swModel, "sgPARALLEL", top, bottom))
                ok++;
            if (TrySketchAddConstraint(swModel, "sgPARALLEL", right, left))
                ok++;

            double heightM = RectLengthMm / 1000.0;
            double widthM = SegmentLengthMm / 1000.0;

            if (TrySketchSetSegmentLength(swModel, bottom, widthM))
                ok++;
            if (TrySketchSetSegmentLength(swModel, right, heightM))
                ok++;
            if (TrySketchSetSegmentLength(swModel, top, widthM))
                ok++;
            if (TrySketchSetSegmentLength(swModel, left, heightM))
                ok++;

            if (TrySketchSetDistanceBetweenTwoSegments(swModel, bottom, top, heightM))
                ok++;

            return ok;
        }

        static bool TrySketchOnEdge(
            ModelDoc2 swModel,
            Face2? hostFace,
            SketchPoint pt,
            Edge edge,
            bool usePierce = false)
        {
            string id = usePierce ? "sgPIERCE" : "sgONEDGE";
            try
            {
                swModel.ClearSelection2(true);
                if (!TryComSelect4(pt, false))
                    return false;
                if (!TrySelectModelEdgeByRay(swModel, edge, hostFace, append: true))
                    return false;
                swModel.SketchAddConstraints(id);
                return true;
            }
            catch (Exception ex)
            {
                Debug.WriteLine($"{LogPrefix} TrySketchOnEdge {id}: {ex.Message}");
                return false;
            }
        }

        static Vertex? PickEdgeVertexCloserToModelPoint(Edge edge, double[] v0, double[] v1, double[] modelPt)
        {
            try
            {
                var vs = edge.GetStartVertex() as Vertex;
                var ve = edge.GetEndVertex() as Vertex;
                if (vs == null || ve == null)
                    return null;
                double d0 = Len3(Sub3(modelPt, v0));
                double d1 = Len3(Sub3(modelPt, v1));
                return d0 <= d1 ? vs : ve;
            }
            catch
            {
                return null;
            }
        }

        static double DistAlongEdgeFromVertexM(double[] edgeStart, double[] edgeEnd, double[] pt)
        {
            double[] axis = Normalize3(Sub3(edgeEnd, edgeStart));
            return Math.Max(0, Dot3(Sub3(pt, edgeStart), axis));
        }

        static bool TrySketchSetDistanceBetweenPointAndVertex(
            ModelDoc2 swModel,
            SketchPoint pt,
            Vertex vertex,
            double distM)
        {
            try
            {
                swModel.ClearSelection2(true);
                if (!TryComSelect4(pt, false))
                    return false;
                if (!TryComSelect4(vertex, true))
                    return false;
                return TryApplyNewDimensionValue(swModel, distM);
            }
            catch (Exception ex)
            {
                Debug.WriteLine($"{LogPrefix} TrySketchSetDistancePointVertex: {ex.Message}");
                return false;
            }
        }

        static bool TrySketchSetDistanceBetweenPoints(ModelDoc2 swModel, SketchPoint a, SketchPoint b, double distM)
        {
            try
            {
                swModel.ClearSelection2(true);
                if (!TryComSelect4(a, false))
                    return false;
                if (!TryComSelect4(b, true))
                    return false;
                TryGetSketchPointsPlacement(a, b, out double px, out double py, out double pz);
                return TryApplyNewDimensionValue(swModel, distM, px, py, pz);
            }
            catch (Exception ex)
            {
                Debug.WriteLine($"{LogPrefix} TrySketchSetDistancePoints: {ex.Message}");
                return false;
            }
        }

        /// <summary>与宏一致:先选两条 SKETCHSEGMENT,再 AddDimension2 得线间距(如底边—顶边 3mm)。</summary>
        static bool TrySketchSetDistanceBetweenTwoSegments(
            ModelDoc2 swModel,
            SketchSegment segA,
            SketchSegment segB,
            double distM)
        {
            try
            {
                swModel.ClearSelection2(true);
                if (!TrySelectSketchSegment(swModel, segA, false))
                    return false;
                if (!TrySelectSketchSegment(swModel, segB, true))
                    return false;
                TryGetSketchSegmentsPlacement(segA, segB, out double px, out double py, out double pz);
                return TryApplyNewDimensionValue(swModel, distM, px, py, pz);
            }
            catch (Exception ex)
            {
                Debug.WriteLine($"{LogPrefix} TrySketchSetDistanceBetweenTwoSegments: {ex.Message}");
                return false;
            }
        }

        /// <summary>两草图线夹角(弧度),用于 45° 倒角处 135° 外角。</summary>
        static bool TrySketchSetAngleBetweenSegments(
            ModelDoc2 swModel,
            SketchSegment segA,
            SketchSegment segB,
            double angleRad)
        {
            try
            {
                swModel.ClearSelection2(true);
                if (!TrySelectSketchSegment(swModel, segA, false))
                    return false;
                if (!TrySelectSketchSegment(swModel, segB, true))
                    return false;
                TryGetSketchSegmentsPlacement(segA, segB, out double px, out double py, out double pz);
                return TryApplyNewDimensionValue(swModel, angleRad, px, py, pz);
            }
            catch (Exception ex)
            {
                Debug.WriteLine($"{LogPrefix} TrySketchSetAngleBetweenSegments: {ex.Message}");
                return false;
            }
        }

        /// <summary>单条草图线长度尺寸(底边 10mm)。</summary>
        static bool TrySketchSetSegmentLength(ModelDoc2 swModel, SketchSegment seg, double lengthM)
        {
            try
            {
                swModel.ClearSelection2(true);
                if (!TrySelectSketchSegment(swModel, seg, false))
                    return false;
                TryGetSketchSegmentMidpoint(seg, out double px, out double py, out double pz);
                return TryApplyNewDimensionValue(swModel, lengthM, px, py, pz);
            }
            catch (Exception ex)
            {
                Debug.WriteLine($"{LogPrefix} TrySketchSetSegmentLength: {ex.Message}");
                return false;
            }
        }

        static void TryGetSketchSegmentMidpoint(SketchSegment seg, out double x, out double y, out double z)
        {
            x = y = z = 0;
            SketchPoint? s = GetSegmentStartPoint(seg);
            SketchPoint? e = GetSegmentEndPoint(seg);
            if (s == null || e == null)
                return;
            x = (s.X + e.X) * 0.5;
            y = (s.Y + e.Y) * 0.5;
            z = (s.Z + e.Z) * 0.5;
        }

        static void TryGetSketchPointsPlacement(SketchPoint a, SketchPoint b, out double x, out double y, out double z)
        {
            x = (a.X + b.X) * 0.5;
            y = (a.Y + b.Y) * 0.5;
            z = (a.Z + b.Z) * 0.5;
        }

        static void TryGetSketchSegmentsPlacement(
            SketchSegment segA,
            SketchSegment segB,
            out double x,
            out double y,
            out double z)
        {
            TryGetSketchSegmentMidpoint(segA, out double ax, out double ay, out double az);
            TryGetSketchSegmentMidpoint(segB, out double bx, out double by, out double bz);
            x = (ax + bx) * 0.5;
            y = (ay + by) * 0.5;
            z = (az + bz) * 0.5;
        }

        /// <summary>倒角轮廓 6 段时顶边为 #3,直角矩形 4 段时顶边为 #2。</summary>
        static int GetRectTopSegmentIndex(int segmentCount) =>
            segmentCount switch
            {
                6 => 3,
                4 => 2,
                _ => -1,
            };

        static bool TryApplyNewDimensionValue(
            ModelDoc2 swModel,
            double valueM,
            double? placeX = null,
            double? placeY = null,
            double? placeZ = null)
        {
            try
            {
                double px = placeX ?? 0, py = placeY ?? 0, pz = placeZ ?? 0;
                if (!placeX.HasValue)
                {
                    var sp = (SketchPoint?)((SelectionMgr)swModel.SelectionManager).GetSelectedObject6(1, -1);
                    if (sp != null)
                    {
                        px = sp.X;
                        py = sp.Y;
                        pz = sp.Z;
                    }
                }

                object? dimOb = swModel.AddDimension2(px, py, pz);
                if (dimOb is not DisplayDimension disp)
                    return false;

                var dim = (Dimension)disp.GetDimension();
                dim.SystemValue = valueM;
                return true;
            }
            catch (Exception ex)
            {
                Debug.WriteLine($"{LogPrefix} TryApplyNewDimensionValue: {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,
            Face2? sketchHostFace,
            double[] ref0,
            double[] ref1,
            double[] c0,
            double[] c1,
            double[] c2,
            double[] c3,
            Edge? anchorEdge,
            double[] anchorModelSeg0,
            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, sketchHostFace, ref0, ref1, c0, c1, c2, c3,
                        anchorEdge, anchorModelSeg0,
                        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 SketchSegment? segment,
            out SketchPoint? startPt,
            out SketchPoint? endPt)
        {
            segment = null;
            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)
            {
                segment = 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,
            Face2? sketchHostFace,
            double[] ra0,
            double[] ra1,
            double[] c0,
            double[] c1,
            double[] c2,
            double[] c3,
            Edge? anchorEdge,
            double[] anchorModelSeg0,
            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<(SketchSegment? seg, 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 SketchSegment? lineSeg, out SketchPoint? sp, out SketchPoint? ep))
                        return false;
                    contourChain.Add((lineSeg, 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} 处成功(首尾闭合处含于最后一项)");
                }

                if (anchorEdge != null && contourChain.Count > 0)
                {
                    SketchSegment? bottomSeg = contourChain[0].seg;
                    SketchPoint? ptC0 = contourChain[0].s;
                    SketchPoint? ptC1 = contourChain[0].e;
                    SketchSegment? vertSeg = contourChain.Count > 1 ? contourChain[1].seg : null;
                    try
                    {
                        TrySketchConstrainRectToHostEdge(
                            swApp,
                            swModel,
                            sketchHostFace,
                            anchorEdge,
                            anchorModelSeg0 ?? Array.Empty<double>(),
                            contourChain,
                            bottomSeg,
                            ptC0,
                            ptC1,
                            vertSeg);
                    }
                    catch (Exception ex)
                    {
                        Log($"矩形钉边约束异常(草图线段已生成): {ex.Message}");
                    }
                }

                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;
        }
    }
}

Logo

欢迎加入DeepSeek 技术社区。在这里,你可以找到志同道合的朋友,共同探索AI技术的奥秘。

更多推荐