cad.net 封装JIG

说明

重构了一下自己的几处JIG代码,
发现可以抽象出一些公共内容,不再每次写类继承(麻烦).

设置JIG不预览:
setvar DRAGMODE 0是不预览,2是预览
控制进行拖动的对象的显示方式
你可以自行加入到封装或者命令层实现.

JIG每次克隆图元显示了,再立即释放,这个是为什么呢?
官方Adndev博客介绍这个操作叫做防止精度松动,
并说,现在的内存分配是很智能的,能够释放了立马再拿过来.
不需要担心内存碎片问题.

Jig分为两种情况:

Jig命令

图元在数据库

打开可写状态/只读貌似也行

newJig..

移动鼠标时修改图元字段

通过事件刷新

退出

图元不在数据库

newJig.

新建图元加入队列

移动鼠标时

通过队列刷新

Dispose图元

保留图元

不在数据库的图元,开启事务提交

命令

namespace JoinBox;
public partial class JigCmds {
    [CommandMethod("TestCmd_jig")]
    public static void TestCmd_jig() {
        var per = Env.Editor.GetEntity("\n选择图元:");
        if (per.Status != PromptStatus.OK)
            return;

        var pts = new List<Point3d>();
        for (int i = 0; i < 2; i++) {
            var getPt = Env.Editor.GetPoint($"\n输入点{i + 1}");
            if (getPt.Status != PromptStatus.OK)
                return;
            pts.Add(getPt.Value);
        }

        // JIG操作图元是自动提权
        // 所以这里即使事务传出Entity也是可以的,
        // 不过穿越事务,非必要不使用,而且cad貌似没有其他场景许可此类操作.
        using DBTrans tr = new();
        var ent = (Entity)tr.GetObject(per.ObjectId, OpenMode.ForRead);

        var pt1 = pts[0];
        var pt2 = pts[1];
        var pt12Dis = pt1.GetDistanceTo(pt2);
        var pt12Angle = pt1.GetVectorTo(pt2).GetAngle2XAxis();
        var roMat = Matrix3d.Rotation(-pt12Angle, Vector3d.ZAxis, pt1.ToPoint3d());

        using JigEx jig = new((mousePointWCS, drawEntitys) => {
            // 这里开始就会频繁执行刷新图元
            var dotPt = pt1.DotProduct(mousePointWCS, pt2);
            var dotPtRo = dotPt.TransformBy(roMat);
            var pt2Ro = pt2.TransformBy(roMat);

            // 复制的次数
            var num = (int)(Math.Abs(dotPtRo.X - pt2Ro.X) / pt12Dis);
            if (num == 0) return;
            for (int i = 0; i < num; i++) {
                var length = pt12Dis * (i + 1);
                // 克隆图元防止精度松动
                var entClone = (Entity)ent.Clone();
                // 旋转到x轴
                entClone.TransformBy(roMat);
                entClone.EntityMove(pt1.ToPoint3d(), new Point3d(pt1.X + length, pt1.Y, pt1.Z));
                entClone.TransformBy(roMat.Inverse());
                // 加入同步刷新队列
                drawEntitys.Enqueue(entClone);
            }
        });

        jig.SetOptions(pt2.ToPoint3d(), msg: "\n指定方向点:");
        var pr = jig.Drag();
        if (pr.Status != PromptStatus.OK)
            return;
        jig.AddEntityToMsPs();
    }
}

更多测试

/*
 *  例子1:
 *  var ptjig = new JigEx();
 *  ptjig.SetOptions(Point3d.Origin);
 *  var pr = ptjig.Drag();
 *  if (pr.Status != PromptStatus.OK)
 *      return null;
 *
 *  例子2:
 *  var ppo1 = new PromptPointOptions(Environment.NewLine + "输入矩形角点1:<空格退出>")
 *  {
 *      AllowArbitraryInput = true,//任意输入
 *      AllowNone = true //允许回车
 *  };
 *  var ppr1 = ed.GetPoint(ppo1);//用户点选
 *  if (ppr1.Status != PromptStatus.OK)
 *      return;
 *  var getPt = ppr1.Value;
 *
 *  var recEntityJig = new JigEx((mousePoint, drawEntitys) => {
 *      #region 画柜子图形
 *      double length = Math.Abs(getPt.X - mousePoint.X);
 *      double high = Math.Abs(getPt.Y - mousePoint.Y);
 *      var ent = AddRecToEntity(Point3d.Origin, new Point3d(length, high, 0));
 *      drawEntitys.Enqueue(ent);
 *      #endregion
 *  });
 *  recEntityJig.SetOptions("指定矩形角点:", new Dictionary<string, string>() { { "Z", "中间(Z)" } );
 *
 *  bool flag = true;
 *  while (flag)
 *  {
 *      var pr = recEntityJig.Drag();
 *      if (string.IsNullOrEmpty(pr.StringResult))//在无输入的时候会等于空
 *          flag = false;
 *      else
 *      {
 *          switch (pr.StringResult.ToUpper()) //注意cad保留 https://www.cnblogs.com/JJBox/p/10224631.html
 *          {
 *              case "Z":
 *                  ed.WriteMessage("\n您触发了z关键字");
 *                  break;
 *              case " ":
 *                  flag = false;//空格结束
 *                  break;
 *          }
 *      }
 *  }
 *  // 开启事务之后,图元加入数据库
 *  recEntityJig.AddEntityToMsPs();
 */

封装

namespace JoinBox;
public delegate void WorldDrawEvent(WorldDraw draw);
public class JigEx : DrawJig {
    /// <summary>
    /// 最后的鼠标点,用来确认长度
    /// </summary>
    public Point3d MousePointWcsLast;

    // 事件:默认是图元刷新,其余的:亮显/暗显等等工作自由补充
    event WorldDrawEvent? WorldDrawEvent;
    // 委托用来获取图元
    Action<Point3d, Queue<Entity>>? _action;
    // 容差
    Autodesk.AutoCAD.Geometry.Tolerance _tolerance;
    // 鼠标配置
    JigPromptPointOptions? _options;
    // 防止刷新过程中更改同步队列
    bool _redrawingCompleted = false;
    // 同步队列
    Queue<Entity> _drawEntitys;

    // 正交修改标记,1正交,0非正交
    // setvar: https://www.cnblogs.com/JJBox/p/10209541.html
    bool _systemVariables_Orthomode = false;

    /// <summary>
    /// 在界面绘制图元
    /// </summary>
    /// <param name="action">
    /// 用来频繁执行的回调: <see langword="Point3d"/>鼠标点,<see langword="List"/>加入显示图元的容器
    /// </param>
    /// <param name="tolerance">鼠标移动的容差</param>
    public JigEx(Action<Point3d, Queue<Entity>>? action = null, double tolerance = 1e-6) :this() {
        _action = action;
        _tolerance = new(tolerance, tolerance);
        _drawEntitys = new();
        DimensionEntitys = new();
#if 双缓冲
        MyQueueCache.Clear();
#endif
    }

    /// <summary>
    /// 执行
    /// </summary>
    public PromptResult Drag() {
        // 拖拽图元必然是当前前台文档,所以封装内部更好调用.
        var old = Env.OrthoMode;
        if (Env.OrthoMode != _systemVariables_Orthomode)
            Env.OrthoMode = _systemVariables_Orthomode;
        var dr = Env.Editor.Drag(this);
        Env.OrthoMode = old;
        return dr;
    }

    /// <summary>
    /// 最后一次的图元加入当前空间
    /// </summary>
    /// <param name="tr">事务</param>
    /// <param name="removeEntity">设置Dispose排除不生成的图元,例如刷新时候的提示文字</param>
    /// <returns></returns>
    public List<ObjectId> AddEntityToMsPs(Action<Entity>? removeAction = null) {
        // 用最后一个点来生成一次图元
        // AddEntityToMsPs https://www.cnblogs.com/JJBox/p/14300098.html
        var tr = DBTrans.Top;
        // 过程图元全部Dispose.
        while (_drawEntitys.Count > 0)
            _drawEntitys.Dequeue().Dispose();

        _action?.Invoke(MousePointWcsLast, _drawEntitys);
        if(removeAction is not null)
            _drawEntitys.ForEach(ent=> removeAction(_drawEntitys));
  
        return _drawEntitys.Where(ent => !ent.IsDispose)
            .Select(ent => tr.AddEntityToMsPs(tr.Database, ent))
            .ToList();
    }

    /// <summary>
    /// 鼠标频繁采点
    /// </summary>
    /// <param name="prompts"></param>
    /// <returns>返回状态:令频繁刷新结束</returns>
    protected override SamplerStatus Sampler(JigPrompts prompts) {
        if (_redrawingCompleted) {
            Debug.WriteLine($"SamplerRC线程ID:{Thread.CurrentThread.ManagedThreadId}");
            return SamplerStatus.NoChange;
        }
        Debug.WriteLine($"Sampler线程ID:{Thread.CurrentThread.ManagedThreadId}");

        var pro = prompts.AcquirePoint(_options);
        if (pro.Status == PromptStatus.Keyword)
            return SamplerStatus.OK;
        else if (pro.Status != PromptStatus.OK)
            return SamplerStatus.Cancel;

        // 上次鼠标点不同(一定要这句,不然图元刷新太快会看到奇怪的边线)
        // == 和 IsEqualTo 方形判断(仅加法器)
        // Distance 圆形判断(会求平方根,除法器牛顿迭代)
        var mpt = pro.Value;
        if (mpt.IsEqualTo(MousePointWcsLast, _tolerance))
            return SamplerStatus.NoChange;

        // 上轮循环的缓冲区图元清理,否则将会在vs输出遗忘释放
        while (_drawEntitys.Count > 0)
            _drawEntitys.Dequeue().Dispose();
        // 委托接收新创建的图元,然后给重绘刷新.
        // 由于此处动态修改了容器,所以不能异步刷新,
        // 异步刷新必须固定容器长度,然后不断更改图元数据.
        _action?.Invoke(mpt, _drawEntitys);
#if 双缓冲
        MyQueueCache.Add(_drawEntitys);
#endif
        MousePointWcsLast = mpt;
        return SamplerStatus.OK;
    }

    /* WorldDraw 操作说明:
     * 0x01
     * 那么可以先提交事务,再开事务,把Entity传给JIG,最后选择删除部分.
     * 虽然这是可行方案,但是Entity穿越事务本身来说是非必要不使用的.
     * 0x02
     * 我有个功能是一次生成四个方向的箭头,
     * 四个箭头最近鼠标的亮显,其余淡显,确认之后最近保留其余删除,
     * acad08缺少瞬时图元,
     * 在JIG使用淡显ent.Unhighlight()/亮显ent.Highlight()需要绕过队列,
     * 否则采样和释放图元将导致图元频闪,令这两个操作失效,
     * 因此不使用JIG的队列容器,而是自定义队列容器,
     * 再传给DatabaseEntityDraw事件实现这两个操作.
     * 0x03 
     * acad08没有异步刷新
     * acad22有异步刷新
     * 方案一 draw.Geometry.Draw(entity);
     * 重绘时候不会跳到Sampler()
     * 方案二 draw.RawGeometry.Draw(entity);
     * 重绘时候会跳到Sampler()重入导致单队列更改出错,
     * 所以通过_redrawingCompleted防止刷新过程更新单队列,
     * 之后再进入WorldDraw,
     * 重绘结束才允许鼠标采集,保证单容器不更改的方案.
     * 0x04
     * WorldDraw不要用flag屏蔽,不然鼠标停着时就看不见图元.
     * 0x05
     * 异步刷新
     * 方案一: 不允许用队列,否则导致容器错误.
     * 改用DatabaseEntityDraw刷新.
     * 并且外部容器也需要遵守不能更改容量,只能一直改图元数据.
     * 方案二: 双缓冲方案 @零点 崩溃
     */

    /// <summary>
    /// 重绘图形
    /// </summary>
    protected override bool WorldDraw(WorldDraw draw) {
#if 双缓冲
        Debug.WriteLine($"WorldDraw线程ID:{Thread.CurrentThread.ManagedThreadId}");
        MyQueueCache.ReadDraw(ent => draw.RawGeometry.Draw(ent));
        WorldDrawEvent?.Invoke(draw);
#else
        if (_drawEntitys.Count > 0)
            _redrawingCompleted = true;
        WorldDrawEvent?.Invoke(draw);
        _drawEntitys.ForEach(ent => draw.RawGeometry.Draw(ent));
        _redrawingCompleted = false;
#endif
        return true;
    }

    public void DatabaseEntityDraw(WorldDrawEvent draw) {
        WorldDrawEvent = draw;
    }

    #region 配置
    /// <summary>
    /// 鼠标配置:基点
    /// </summary>
    /// <param name="basePoint">基点</param>
    /// <param name="msg">提示信息</param>
    /// <param name="cursorType">光标绑定</param>
    /// <param name="orthomode">正交开关</param>
    public JigPromptPointOptions SetOptions(Point3d basePoint,
        CursorType cursorType = CursorType.RubberBand,
        string msg = "点选第二点", bool orthomode = false) {
        _systemVariables_Orthomode = orthomode;
        _options = JigPointOptions();
        _options.Message = Environment.NewLine + msg;
        _options.Cursor = cursorType;   // 光标绑定
        _options.UseBasePoint = true;   // 基点打开
        _options.BasePoint = basePoint; // 基点设定
        return _options;
    }

    /// <summary>
    /// 鼠标配置:提示信息,关键字
    /// </summary>
    /// <param name="msg">信息</param>
    /// <param name="keywords">关键字</param>
    /// <param name="orthomode">正交开关</param>
    /// <returns></returns>
    public JigPromptPointOptions SetOptions(string msg,
        Dictionary<string, string>? keywords = null, bool orthomode = false) {
        _systemVariables_Orthomode = orthomode;
        _options = JigPointOptions(); 
        _options.Message = Environment.NewLine + msg;

        // 加入关键字
        const string spaceKey = " ";
        if (keywords != null) {
            foreach (var ge in keywords) {
                if (ge.Key != spaceKey)
                    _options.Keywords.Add(ge.Key, ge.Key, ge.Value);
            }
        }
        // 空格要放最后,才能优先触发其他关键字
        var spaceValue = keywords?.Where(ge => ge.Key == spaceKey)
            .Select(ge => ge.Value)
            .FirstOrDefualt();
        if (spaceValue is null) spaceValue = "<空格退出>";
        _options.Keywords.Add(spaceKey, spaceKey, spaceValue);
        return _options;
    }

    /// <summary>
    /// 鼠标配置:自定义
    /// </summary>
    /// <param name="action"></param>
    /// <param name="orthomode">正交开关</param>
    public void SetOptions(Action<JigPromptPointOptions> action, bool orthomode = false) {
        _systemVariables_Orthomode = orthomode;
        _options = new JigPromptPointOptions();
        action.Invoke(_options);
    }

    /// <summary>
    /// 用户输入控制默认配置
    /// <para>令jig.Drag().Status == <see cref="PromptStatus.None"/></para>
    /// </summary>
    /// <returns></returns>
    static JigPromptPointOptions JigPointOptions() {
        return new JigPromptPointOptions() {
            UserInputControls =
                  UserInputControls.GovernedByUCSDetect     // 由UCS探测用
                | UserInputControls.Accept3dCoordinates     // 接受三维坐标
                | UserInputControls.NullResponseAccepted    // 输入了鼠标右键,结束jig
                | UserInputControls.AnyBlankTerminatesInput // 空格或回车,结束jig;
        };
    }

    /// <summary>
    /// 空格默认是<see cref="PromptStatus.None"/>,
    /// <para>将它设置为<see cref="PromptStatus.Keyword"/></para>
    /// </summary>
    public void SetSpaceIsKeyword() {
        if (_options is null)
            throw new ArgumentNullException(nameof(_options));
        if ((_options.UserInputControls & UserInputControls.NullResponseAccepted) == UserInputControls.NullResponseAccepted)
            _options.UserInputControls ^= UserInputControls.NullResponseAccepted; // 输入了鼠标右键,结束jig
        if ((_options.UserInputControls & UserInputControls.AnyBlankTerminatesInput) == UserInputControls.AnyBlankTerminatesInput)
            _options.UserInputControls ^= UserInputControls.AnyBlankTerminatesInput; // 空格或回车,结束jig
    }
    #endregion

    #region 注释数据
    /// <summary>
    /// 注释数据,在缩放的时候不受影响
    /// </summary>
    public DynamicDimensionDataCollection DimensionEntitys;

    /// <summary>
    /// 重写注释数据
    /// </summary>
    /// <param name="dimScale"></param>
    /// <returns></returns>
    protected override DynamicDimensionDataCollection GetDynamicDimensionData(double dimScale) {
        base.GetDynamicDimensionData(dimScale);
        return DimensionEntitys;
    }
    #endregion

    #region IDisposable接口相关函数
    ~JigEx() { Dispose(false); }

    public bool IsDisposed { get; private set; } = false;
    public void Dispose() {
        Dispose(true);
        GC.SuppressFinalize(this);
    }
    protected virtual void Dispose(bool disposing) {
        if (IsDisposed) return;
        IsDisposed = true;
        // 最后一次的图元如果没有加入数据库就在此销毁,
        // 所以JigEx调用的时候加using
        while (_drawEntitys.Count > 0) {
            var ent = _drawEntitys.Dequeue();
            if (ent.Database is null && !ent.IsDisposed)
                ent.Dispose();
        }
#if 双缓冲
        MyQueueCache.Clear();
#endif
    }
    #endregion

#if false
    | UserInputControls.NullResponseAccepted           // 接受空响应
    | UserInputControls.DoNotEchoCancelForCtrlC        // 不要取消CtrlC的回音
    | UserInputControls.DoNotUpdateLastPoint           // 不要更新最后一点
    | UserInputControls.NoDwgLimitsChecking            // 没有Dwg限制检查,这应该是自定义图元的拖拽
    | UserInputControls.NoZeroResponseAccepted         // 接受非零响应
    | UserInputControls.NoNegativeResponseAccepted     // 不否定回复已被接受
    | UserInputControls.Accept3dCoordinates            // 返回三维坐标点,默认是二维点
    | UserInputControls.AcceptMouseUpAsPoint           // 接受释放按键时的点而不是按下时
    | UserInputControls.AnyBlankTerminatesInput        // 任何空白终止输入
    | UserInputControls.InitialBlankTerminatesInput    // 初始空白终止输入
    | UserInputControls.AcceptOtherInputString         // 接受其他输入字符串
    | UserInputControls.NoZDirectionOrtho              // 无方向正射,直接输入数字时以基点到当前点作为方向
    | UserInputControls.UseBasePointElevation          // 使用基点高程,基点的Z高度探测
#endif

双缓冲

// 首先要有两个队列,一个负责存,一个负责画.
// 每画一个都会切到鼠标采样,采用里面就会用另一个存,存了之后二者对调..
// 所以每次画完要判断当前容器是否切换.
// 那存的岂不是原子性保护?

using System;
using System.Collections.Generic;
using System.Threading;

public class MyQueueCache {
    static int IsDrawNum = 0;
    static Queue<Entity>[] _q = {new Queue<Entity>(), new Queue<Entity>()};
    static Queue<Entity> GetDepositQueue() => _q[IsDrawNum];
    static Queue<Entity> GetDrawQueue() => _q[IsDrawNum == 0 ? 1 : 0];

    public static void Add(IEnumerable<Entity> ents) {
        var q = GetDepositQueue();
        // 上次没有刷新完的需要释放
        while (q.Count > 0) q.Dequeue().Dispose();
        foreach (var ent in ents) q.Enqueue(ent);
        Interlocked.Exchange(ref IsDrawNum, IsDrawNum == 0 ? 1 : 0);
    }

    public static void ReadDraw(Action<Entity> action) {
        var q = GetDrawQueue();
        while (q.Count > 0) {
            var ent = q.Dequeue();
            // 每次绘制都可能切换,之后需要重新获取队列
            action(ent);
            ent.Dispose();
            q = GetDrawQueue();
        }
    }

    public static void Clear() {
        var q0 = _q[0];
        while (q0.Count > 0) q0.Dequeue().Dispose();
        var q1 = _q[1];
        while (q1.Count > 0) q1.Dequeue().Dispose();
    }
}

备注

virtual AcGiGeometry * rawGeometry() const = 0;
将当前几何类作为AcGiGeometry返回,保证指针不是NULL.
此函数允许例程在worldDraw上下文和viewportDraw上下文中运行.
而无raw就是无上下文的异步操作

Lisp

(完)

posted @   惊惊  阅读(1773)  评论(2编辑  收藏  举报
相关博文:
阅读排行:
· winform 绘制太阳,地球,月球 运作规律
· AI与.NET技术实操系列(五):向量存储与相似性搜索在 .NET 中的实现
· 超详细:普通电脑也行Windows部署deepseek R1训练数据并当服务器共享给他人
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 上周热点回顾(3.3-3.9)
历史上的今天:
2020-12-06 cad.net WPF嵌入技术1_嵌入WPF到cad(MFC,win32窗体),Win32API嵌入WPF位置跳走的解决方案
点击右上角即可分享
微信分享提示