cad.net 封装JIG

说明

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

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

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

JIG这个东西有个难点,异步刷新和同步刷新,
同步刷新封装之后写起来简单,图元可以动态加入容器.
异步则需要暴露事件使用,并且是固定数组长度,毕竟它任何时候都会高频获取数组成员.

Jig分为两种情况:

graph TB Jig命令 --> 图元在数据库 --> 打开可写状态/只读貌似也行 --> newJig.. --> 移动鼠标时修改图元字段 --> 通过事件刷新 --> 退出 Jig命令 --> 图元不在数据库 --> 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();
    }

    /// <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);
        MousePointWcsLast = mpt;
        return SamplerStatus.OK;
    }

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

    /// <summary>
    /// 重绘图形
    /// </summary>
    protected override bool WorldDraw(WorldDraw draw) {
        if (_drawEntitys.Count > 0)
            _redrawingCompleted = true;
        WorldDrawEvent?.Invoke(draw);
        _drawEntitys.ForEach(ent => draw.RawGeometry.Draw(ent));
        _redrawingCompleted = false;
        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();
        }
    }
    #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

备注

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

Lisp

(完)

posted @ 2021-12-06 17:11  惊惊  阅读(2054)  评论(2)    收藏  举报