cad.net 动态块

动态块名

测试版本: Acad2008

问题一:
BlockReference.IsDynamicBlock
判断是否为动态块,当频繁使用的时候,会出现报错:
eInvalidObjectId错误. System.AccessViolationException的异常
img

我是在频繁重复空格执行上次命令时候报这个错误,
修复方法是,在所有GetObject语句,最前面加using.
或者最后加.Dispose() 一样效果的.

问题二:
动态块的块参照,Z比例为0,或许不等比缩放,
就会令动态块变成普通块,
导致BlockReference.IsDynamicBlock判断动态块失效.

问题三:
动态块不拉伸时候就是原本名称,一旦拉伸块表记录就是u名,
所以既要普通块名,又要动态块名,

针对问题二和问题三,
我发现打开动态块记录直接读名称就好了,不需要判断它是不是动态块的.

namespace JoinBox;
public static partial class BlockHelper {
    /// <summary>
    /// 动态块真实块名获取
    /// </summary>
    /// <param name="brRec">块参照</param>
    /// <returns>成功返回:块的真实名称,失败返回:string.Empty</returns>
    public static string GetBlockName(this BlockReference brRec)
    {
        var tr = DBTrans.Top; // IFox事务栈顶获取事务
        string blockName = string.Empty;
        if (brRec.DynamicBlockTableRecord.IsOk()) {
            using var btRec = (BlockTableRecord)tr.GetObject(brRec.DynamicBlockTableRecord, OpenMode.ForRead);
            blockName = btRec.Name;
        }
        return blockName;
    }
}

public static partial class EntityEdit {
    /// <summary>
    /// id有效,未被删除
    /// </summary>
    /// <param name="id"></param>
    /// <returns></returns>
    public static bool IsOk(this ObjectId id) {
        return !id.IsNull && id.IsValid && !id.IsErased && !id.IsEffectivelyErased && id.IsResident;
    }
}

动态块过滤器

注意,此过滤器会获取当前空间全部动态块(不止仅限同名)同名普通块,
要根据块真名循环时候过滤一次.

若不想这样做,需要自己构建索引组织表.
但是似乎过于麻烦
参考 https://www.cnblogs.com/JJBox/p/18580188

#if !HC2020
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.EditorInput;
#else
using GrxCAD.DatabaseServices;
using GrxCAD.EditorInput;
#endif
using System.Collections.Generic;

namespace JoinBox;

public static class SsgetFilterEx {
    // https://blog.csdn.net/yxp_xa/article/details/72229202?tdsourcetag=s_pcqq_aiomsg
    /// <summary>
    /// 转义通配符,加入过滤器必须转义
    /// </summary>
    /// <param name="blockName">块名</param>
    /// <returns></returns>
    static string BlockNameEscape(string blockName)
    {
        return blockName.Replace("#", "`#").Replace("@", "`@");
    }

    // https://www.cnblogs.com/edata/p/6797362.html
    /// <summary>
    /// 通过块信息,设置动态块过滤器或普通块过滤器
    /// </summary>
    /// <param name="baseId">块的图元id</param>
    /// <returns>过滤器</returns>
    public static SelectionFilter GetBlockSelectionFilter(ObjectId baseId) {
        var tr = DBTrans.Top;
        // 选择集过滤器
        var list = new List<TypedValue>();
        list.Add(new TypedValue((int)DxfCode.Start, "INSERT"));

        // 过滤得到同一个空间下图元,布局名称(布局视口内则为模型)
        string layoutName = CadSystem.GetMouseSpace();
        list.Add(new TypedValue((int)DxfCode.LayoutName, layoutName));

        // 动态块过滤器{动态块名,真实名称(匿名块不提供)}
        using var brRec = (BlockReference)tr.GetObject(baseId, OpenMode.ForRead);
        string blockName = BlockNameEscape(brRec.GetBlockName());
        list.Add(new TypedValue((int)DxfCode.BlockName, $"`*U*,{blockName}"));
        return new SelectionFilter(list.ToArray());
    }

    // 过滤得到同名的块参照
    public static List<ObjectId> SelectBlockReference(ObjectId baseId, List<ObjectId> blockIds){
        List<ObjectId> ids = new();
        var tr = DBTrans.Top;
        using var brRec = (BlockReference)tr.GetObject(baseId, OpenMode.ForRead);
        string blockName = brRec.GetBlockName();
        foreach(var id in blockIds){
            using var brRec = (BlockReference)tr.GetObject(id, OpenMode.ForRead);
            if (brRec is null) continue;
            if (brRec.GetBlockName() == blockName)
                ids.Add(id);
        }
        return ids;
    }
}

public static partial class CadSystem {
    /// <summary>
    /// 当前布局名称(视口内为模型)
    /// </summary>
    /// <returns>模型或布局名称</returns>
    public static string GetMouseSpace() {
        string layoutName = LayoutManager.Current.CurrentLayout;
        // SpatialPoint 在 https://www.cnblogs.com/JJBox/p/11464977.html
        if (CadSystem.SpatialPoint() == SpatialPosition.Viewport) {
            layoutName = "Model";
        }
        return layoutName;
    }
}

动态块参数

这个方法封装不好,会导致外面已经提权,内部重复提权.

public static void ChangeDynamicValue(BlockReference brf, string key, string value) {
    if (!brf.IsDynamicBlock) return;
    brf.UpgradeOpen();
    // 动态块属性集
    var props = brf.DynamicBlockReferencePropertyCollection;
    foreach (DynamicBlockReferenceProperty item in props) {
        // 参数名:可见性/距离之类的
        if (item.PropertyName.Contains(key))
            item.Value = value; // 参数值
    }
    brf.DowngradeOpen();
}

好的写法:
因为任何提权行为都不应该封装,
而是传入id,再打开对象,再提权,再处理,再降权,再释放对象.

/// <summary>
/// 更改动态块拉伸参数
/// </summary>
/// <param name="brfId">动态块参照的图元id</param>
/// <param name="name">动作参数名</param>
/// <param name="value">动作参数值</param>
public static void ChangeDynamicValue(ObjectId brfId, string key, string value) {
    var tr = DBTrans.Top;
    if (!brfId.IsOk) return;
    using var brf = (BlockReference)tr.GetObject(brfId, OpenMode.ForRead);
    if (brf is null || !brf.IsDynamicBlock) return;
    br.UpgradeOpen();
    // 动态块属性集
    var props = br.DynamicBlockReferencePropertyCollection;
    foreach (DynamicBlockReferenceProperty item in props) {
        // 参数名:可见性/距离之类的
        if (item.PropertyName.Contains(key))
            item.Value = value; // 参数值
    }
    br.DowngradeOpen();
}

可见性名称

例如:我需要找到可见性被改名之后的参数名才能设置参数值,
不然缺失参数名则无法操作(因为不能枚举字符串),
发现是301组码下面:

那么C#如何得到这个组码呢?
cad.net 句柄遍历数据库

那么Lisp如何得到这个组码呢?

(setq Pros (vlax-invoke (vlax-ename->vla-object (car (entsel))) 'GetDynamicBlockProperties))
(mapcar '(lambda (X)(cons (vlax-get X 'PropertyName) (vlax-get X 'Value)))Pros)

分解块参照(炸开)

插入动态块再根据鼠标修改参数是很正常的功能,
青蛙在动态块外面再加了一层普通块A,然后炸开,会发现动态块的参数丢失了.
解决方案是深度克隆之后,块表记录有动态块记录的,直接插入就好了,不需要普通块A再炸开.

E1旧版炸开: 标注关联× 动态块参数√

var objects = new DBObjectCollection();
blkRef.Explode(objects);
foreach(DB0bject entity in objects) {
  space.AppendEntity(entity as Entity);
}
trans.AddNewlyCreatedDBObject(entity,true);

E2新版炸开: 标注关联√ 动态块参数√

blkRef.ExplodeToOwnerSpace();

未解决的案例

我有一个动态块,无论新版炸开函数还是旧版都存在问题.
新版:导致阵列跑位.
旧版:丢失标注关联.
这个动态块是利用高版本阵列,制作过程如下:
1,画一个圆,然后用AR阵列,再组块,然后进入块编辑状态.

2,点击参数管理器,新增两个key(参数)名称是可以改的.

3,选中阵列,在特性面板中,直接输入第二步的key名.

4,保存块编辑器后,选中块参照,在特性面板中可以看到刚才设置的列数和水平距离,并能够修改值.
实现了一种外部驱动块内阵列的效果.

经过研究之后,E2新版炸开是可以的.
炸开之前需要逆变换到原点,否则直接炸开的话,高版本阵列会跑位.
而旧版炸开会丢失标注关联.

组块

0,选择或创建图元/属性定义.

1,基变换:插入点到原点

2,获取绘图次序之后深度克隆,然后创建块表记录并加入其中
https://www.cnblogs.com/d1742647821/p/18325096

using DBTrans tr = new();
var objectIdList = tr.CurrentSpace.OfType<ObjectId>().ToList();
if (objectIdList.Count == 0) return;

// 绘图次序排序
using var drt = (DrawOrderTable)tr.GetObject(tr.CurrentSpace.DrawOrderTableId, OpenMode.ForRead);
var objectIdCollection = objectIdList
    .OrderBy(id => drt.GetSortHandle(id).Value)
    .ToCollection();

// 创建块表记录
var btrId = tr.BlockTable.Add("*U", 
    btr => { btr.DeepCloneEx(objectIdCollection); });

// 插入当前空间
tr.CurrentSpace.InsertBlock(Point3d.Origin, btrId);

// 删除旧图元
var ents = objectIdList.GetObject<Entity>(OpenMode.ForWrite);
foreach (var ent in ents) {
    ent.Erase();
}

3,注释性
https://gitee.com/inspirefunction/ifoxcad/issues/I820Z0

4,根据属性定义附加属性参照

5,附加动态参数和动作(这里甚至没有完成)

6,基变换:原点到插入点

炸开(分解)

理论上炸开底层实现是克隆,可以改用克隆到当前块表记录,然后标注关联要依靠什么什么重建(或者保留key关系)才对.
没找到C#怎么重建标注关联...

注意:
新旧版本炸开不同,不能单纯认为可以相互替代,尤其是新版组块旧版打开炸开.
E1旧版炸开: BlockReference.Explode();
E2新版炸开: BlockReference.ExplodeToOwnerSpace()

旧版表现:
标注关联被取消,新版阵列被炸开会怎么样等等.
我们应该用插件进行反向支持,在低版本补充炸开高版本炸开功能才对.
官方保存低版本dwg有没有自动转换动态块内容呢?

新版表现:
能够保留块内的标注关联.
如果遇到新版阵列,那么需要把块参照进行逆变换到原点,否则阵列内容会跑位.
这个函数没有提供返回值,炸开内容只能通过db.ObjectAppended事件获取,
而事件会将全部层次结构和过程id给平铺了.
可以通过遍历事件收集的id输出DBObject.Gettype().Name得知:
它们并不都是Entity,会有IsErased==true的,会有重复的,会有动态块参数和动作的.
而直接把全部内容进行矩阵变换,则会把图元变换了两次.

阵列原理:
https://www.cnblogs.com/ztcad/p/14707974.html
https://www.cnblogs.com/ztcad/p/14707880.html

一个罕见函数,

转为静态块: BlockReference.ConvertToStaticBlock();

删除对象 br.Erase();

实际上是修改IsErased=true,并通知数据库把它放入UndoLog用于撤回
它仍然在内存暂留,所以仍然需要降权.
真正删除数据行破坏内存:
https://adndevblog.typepad.com/autocad/2012/04/reclaiming-memory-of-erased-objects.html

测试代码:

public class TestBlock {
// 容器记录炸开内容
List<ObjectId> _ids = new();

// 通过事件获取炸开内容,保存入容器中
void ObjectAppended(object sender, ObjectEventArgs e) {
    _ids.Add(e.DBObject.ObjectId);
}

[CommandMethod(nameof(cs21))]
public void cs21() {
    PromptEntityOptions peo = new("选择块参照: ");
    var per = Env.Editor.GetEntity(peo);
    if (per.Status != PromptStatus.OK) return;

    db.ObjectAppended += ObjectAppended;
    Matrix3d mat = Matrix3d.Identity;

    using(DBTrans trans = new()){
        using var ent = (Entity)trans.GetObject(per.ObjectId, OpenMode.ForRead);
        if (ent is not BlockReference br) return;
        br.UpgradeOpen();
        // 记录块矩阵,给炸开后的内部图元使用
        mat = br.BlockTransform;
        // 把块参照逆变换到原点
        br.TransformBy(mat.Inverse());
        // 新版炸开
        br.ExplodeToOwnerSpace();
        br.Erase();
        br.DowngradeOpen();
    }

    db.ObjectAppended -= ObjectAppended;

    // 目的:炸开后的图元全部变换到块矩阵
    // 从事件获取内容
    // 1,存在过程图元,有被删除的图元.(过程图元会恢复吗?)
    // 2,动态块参数也在其中.(它是隐藏的吗?)
    // 3,仅排除ent.IsErased不行的,有些东西无法矩阵变换.
    // 4,如果将可以变换的都进行矩阵变换,会把阵列内容移动了两次,
    // 而仅仅移动前面十个就不会(正确变换),如何排除呢?
    // 前面十个中有AcDbAssocArrayActionBody是不是就是阵列参数,
    // 而十个之后是阵列图元?

    using (DocumentLock docLock = doc.LockDocument()) {
    using(DBTrans t2 = new()){

        // 统计重复(开窗函数),发现都是1,就不用输出了
        var map = new Dictionary<ObjectId,int>();
        for (int j = 0; j < _ids.Count; j++){
            var id = _ids[j];
            if (map.ContainsKey(id)) {
                ++map[id];
            }else{ map.Add(id,1); }
        }

        // 打印统计信息
        var sb = new StringBuilder();
        for (int j = 0; j < _ids.Count; j++){
            var id = _ids[j];
            string name = "";

            /* 
            已经删除打开会报错eWasErased
            无论是id.Open,还是OpenMode.ForNotify模式
            那么Acad撤回时候怎么用UndoLog复活它呢?
            先设置id.IsErased=true吗?还是说深度克隆?
            只允许内部实现?通过id的话那么图元内容无法获取.
            if (id.IsErased){
                using var objx = t2.GetObject(id, OpenMode.ForRead);
               name=objx.GetType().Name;
            }
            */

            if (id.IsErased) name+="-"
            if (id.IsNull) name+="*"
            // 全有 if(id.IsValid) name+="+"
            if (id.IsEffectivelyErased) name+="/"
            // 全有 if(id.IsResident) name+=">"
            name += id.ObjectClass.Name;

            sb.AppendLine($"序号:{j},句柄:{new Handle(id.Handle.Value)},重复:{map[id]},图元名称:{name}");
        }

        Debug.Print(sb.ToString());

        // 测试前面十个
        for (int j = 0; j < 10; j++){
            var id = _ids[j];
            if (id.IsErased) continue;
            using var objx = t2.GetObject(id, OpenMode.ForRead);
            if (objx.GetType().Name is "BlockBegin" or "BlockEnd") continue; //跳过不能矩阵变换的对象,它将导致eNotApplicable.
            if (objx is not Entity ent) continue;
            ent.TransformBy(mat);
            ent.DowngradeOpen();
        }

    }//tr
    }//lock

    // 清理,这里仅仅是重置容器计数,而不释放内存容量,下次使用将从0进行覆盖内容.
    // 容器持有对象不释放,并不导致对象引用计数(菱形引用计数从不为0,无法释放对象),不需要改用WeakMap.
    // 因为容器持有的id是二级指针,它指向的Entity才是真正对象,所以你甚至会发现桌子设计数据库时,Entity字段从来不持有另一个Entity而是持有id,标识符模式.
    _ids.Clear();
}
}

未解决块内关联标注

插入动态块后更新关联标注不成功
建议不要把标注做到动态块内...这样就规避问题了...

[CommandMethod(nameof(CS116), CommandFlags.UsePickSet | CommandFlags.Redraw)]
public static void CS116() {
    using var tr = new DBTrans();
    ObjectId blkRefId = ObjectId.Null;
    // 插入...试试不要用insert,用深度克隆看看?
    tr.Task(()=>{
        blkRefId = tr.CurrentSpace.InsertBlock(
                new Point3d(0, 0, 0), "1", new Scale3d(1, 1, 1), 0, null);
    });

    // 分开插入和修改刷新为两个命令,居然是成功的.
    var map = new Dictionary<string, double>
    {
        { "test1", 1000 },
        { "test2", 2000 }
    };
    ChangeDynamicValue(blkRefId, map);
    // 发送刷新后关联标注能够更新
    Env.Document.SendStringToExecute("regen\n", true, false, true);
}

// 修改动态块数值
public static void ChangeDynamicValue(ObjectId id, Dictionary<string, double> map) {
    var tr = DBTans.Top;
    using var bref = tr.GetObject<BlockReference>(id, OpenMode.ForWrite);
    // 动态块属性集
    var props = bref?.DynamicBlockReferencePropertyCollection;
    if (props is null) return;

    foreach (DynamicBlockReferenceProperty item in props) {
        if (map.TryGetValue(item.PropertyName, out var v)) // 参数名:可见性/距离      
            item.Value = v;   
    }

    // 是不是这里缺少了遍历块参照内,而不是块参照块表记录,然后就能刷新标注了?
    // 关联数据AssocBody打开句柄获取?
    // https://www.cnblogs.com/ztcad/p/14107353.html
    // 动画,更新标注
    // https://www.cnblogs.com/JJBox/p/11354224.html
    bref.RecordGraphicsModified(true);
    CadSystem.UpdateScreenEx(bref);
 }

未解决动态块纯代码创建

怎么用纯代码的方式新建一个动态块?
edata思路:
1:弄一个空的DXF;
2:构造一个有动态块的块定义;
3:删除后的;
然后比较这三者,但是怎么用DXF组码来创建的呢?对应的API是什么?

解析动态块

本文来自 https://blog.csdn.net/jiangyb999/article/details/128725215

块有2种,普通块和动态块;

普通块
大家应该很熟悉,定义好块,插入到某个布局中,就成了块参照.
对块参照能做的无非就是缩放旋转等常规操作,块内的图形是不能再被编辑的;

动态块
也是像普通快一样插入,缩放旋转的常规操作自然也是具备的,
但是,它的超能力来自于,我们可以修改参照内的图形;

听起来还是蛮简单的,但是动态块的内容十分庞大.
其实,动态块不应该当做图块来看,
定义一个动态块就是定义了一段程序,
在程序的辅助下,我们才能实现复杂的修改;

按照Autodesk的说法,动态块是基于DAG的一套求解器;
DAG就是有向无环图(Directed Acycle Graph),
这个图是由Node和Edge构成的一个网络;
Node代表动作,
Edge是对象属性之间的依赖关系,并且Edge是有方向的;

现在该是去探究一下动态块的存储了;

在解析DWG文件时,并没有所谓的动态块图元类型.
我们知道,对象扩展字典是一个很强大的工具.
二次开发者可以借助该字典对元对象实现无限扩展.

动态块可以认为是Autodesk假装自己是第三方开发者,
然后对普通块进行扩展的极好示范,
借助扩展字典就无需对DWG格式做任何修改,
动态块是2006版引入的功能,而2006版使用的DWG格式和2004是一样的.

实验

创建一个动态块BB1,
由4条直线组成,
其中加入了1个点参数/1个线性参数/2个拉伸动作;

去除了不相关的部分,该块描述如下:

Object(BLOCK_HEADER/31)
handle: 0.1.F1
owner: 4.1.1
xdicobjhandle: 3.2.5BF
name: *U
entries:4
inserts:2
1), 3.2.5BB
2), 3.2.5BC
3), 3.2.5BD
4), 3.2.5BE
block:3.1.F2
endblk:3.1.F3
Object(BLOCK/4)
handle: 0.1.F2
owner: 4.0.F1
xdicobjhandle: 0.0.0
name: BB1
Object(LINE/13)
handle: 0.2.5BB
owner: 4.1.F1
start: 0.000000, 0.000000, 0.000000
end  : 36.067069, 49.732573, 0.000000

------------
Object(LINE/13)
handle: 0.2.5BC
owner: 4.1.F1
start: 36.067069, 49.732573, 0.000000
end  : 69.634529, 29.107227, 0.000000

------------
Object(LINE/13)
handle: 0.2.5BD
owner: 4.1.F1
start: 44.258738, 7.308900, 0.000000
end  : 69.634529, 29.107227, 0.000000

------------
Object(LINE/13)
handle: 0.2.5BE
owner: 4.1.F1
start: 44.258738, 7.308900, 0.000000
end  : 0.000000, 0.000000, 0.000000

然后在模型空间插入此块,只调整了比例;
可以看出,参照的块名是"BB1";

这时,我们再到解析的数据里找句柄为669的块参照,
看看它的数据,注意看它引用的块的句柄,
正是我们刚定义的动态块BB1的句柄.

Object(INSERT/7)
handle: 0.2.669
owner: 0.0.0
xdicobjhandle: 0.0.0
entmode: 2 (MSPACE)
insert point: (989.208137, -530.611402)
block header: 5.1.F1

就是说,未对块参照执行动态调整时,
它是直接引用原始的块定义的,并且没有扩展字典;

我们使用动态参数来调整一下,形状明显与上面的不同.

保存DWG,再解析一遍,查看669的数据;

Object(INSERT/7)
handle: 0.2.669
owner: 0.0.0
xdicobjhandle: 3.2.6BC
entmode: 2 (MSPACE)
insert point: (989.208137, -530.611402)
block header: 5.2.6B4

看到没,这时引用的块定义变成 6B4 了,而且有扩展字典,
我们再看看块 6B4 是咋个样子;

Object(BLOCK_HEADER/31)
handle: 0.2.6B4
owner: 4.1.1
xdicobjhandle: 3.2.6BB
name: *U
entries:4
inserts:1
1), 3.2.6B7
2), 3.2.6B8
3), 3.2.6B9
4), 3.2.6BA
block:3.2.6B5
endblk:3.2.6B6
Object(BLOCK/4)
handle: 0.2.6B5
owner: 4.0.6B4
xdicobjhandle: 0.0.0
name: *U25
Object(LINE/13)
handle: 0.2.6B7
owner: 4.0.6B4
start: 0.000000, 0.000000, 0.000000
end  : 36.067069, 49.732573, 0.000000
------------
Object(LINE/13)
handle: 0.2.6B8
owner: 4.0.6B4
start: 36.067069, 49.732573, 0.000000
end  : 53.107381, 39.262245, 0.000000
------------
Object(LINE/13)
handle: 0.2.6B9
owner: 4.0.6B4
start: 44.258738, -7.308900, 0.000000
end  : 53.107381, 39.262245, 0.000000
------------
Object(LINE/13)
handle: 0.2.6BA
owner: 4.0.6B4
start: 44.258738, -7.308900, 0.000000
end  : 0.000000, 0.000000, 0.000000

块 "*U25" 中的4条直线 6B7,6B8,6B9,6BA
分别对应 块"BB1"中的 5BB,5BC,5BD和5BE,
由于做了参数调整,他们的坐标值发生了变化;

这一切说明了什么呢?
当我们使用动态块时,99.99999%的概率是要对它修改的,
所以,插入动态块后,AutoCAD会复刻一份原始动态块的定义,
生成一个匿名动态块,作为块参照的引用块;

所有对块参照的修改,都反映在这个匿名块身上,
然后像使用普通块一样使用这个匿名块,
这样做,可以保证原始的动态块定义永远保持不变;

当然,这是在你不人为修改它的定义的前提下,
如果你参照它生成多个块参照对象后,再修改它的定义,
AutoCAD总会询问你:是否更新这些块参照.
还记得这个画面吗?

总结

到这里,可以更新开头我对动态块的解读认知了;

解析INSERT图元,如果要生成图形,
直接使用其参考的块包含的对象就可以了,
不用分辨它是动态块还是普通块,
动态调整的过程AutoCAD已经帮我们完成了;

起初我还以为,
生成动态块的参照,需要从参数/动作等自己去完成,
那对大多数人来说都是不可能完成的任务,
好在AutoCAD保存了块参照参考的匿名块的最后状态,
而不是仅仅保存该状态的自定义参数值,
不过想想也是,
它不保存不是难为自己吗?是我想的复杂了;

没有时间去比对原始动态块和新的匿名块,
他们的扩展字典的情况了,不影响大局,
那是APP(这里是AutoCAD)要关心的事;

(完)

posted @ 2020-02-27 12:17  惊惊  阅读(2795)  评论(2编辑  收藏  举报