cad.net 约束求解器(模拟动态块)+有向无环图DAG

约束求解器

我已经有一篇Acad的动态块专篇:
https://www.cnblogs.com/JJBox/p/12371467.html

本篇不是Acad二次开发的代码,只是展露出动态块的原理.
Acad并没有透露出动态块创建API,不过却暴露了DXF的底层储存.
储存方式+本篇,基本上你就可以知道动态块的全部信息了.

模拟动态块功能,
不是通过C#重定义或者ARX自定义,
而是DWG数据字典记录参数+构造n个匿名块来实现的!!
https://www.cnblogs.com/JJBox/p/12371467.html#_label8
也就是就连Lisp也可以构建...
那夹点显示呢?还不是要靠重定义/自定义?

不动件是动件的前导版本,
二维约束只是三维约束的一个前导版本.
动态块又是二维约束的一个前导版本.
动态块采用了手工约束,可以很方便去验证约束效果.

我做了两个demo,
颜色约束器是Acad不存在的,但是它很好的说明驱动图元的原理.
拉伸约束器就是入门动态块时候常用的拉伸动态块了.

还有一些map<旋转参数,旋转动作>
这种一对一约束要自己构建起来,我觉得作为demo挺麻烦.
Acad用的是有向无环图结构,不是字典结构.

为什么用图结构?
因为驱动有顺序,例如 拉伸动作 包裹一组 旋转参数+动作,
此时就应该先拉伸,再旋转,能够沿路径传播事件.
图结构有个拓扑排序,
很好的利用入度进行区分,先做什么,再做什么.

颜色约束器:
L1和L2加一个颜色参数,然后绑定同一个动作
组块之后,从动态块属性的 颜色动作 就可以联动修改颜色.

拉伸约束器:
L1 加一个线性 参数a,
L2 加一个线性 参数b,
拉伸动作k 驱动 线性参数a和线性参数b的端点EndX.
组块之后,触发拉伸动作向右拉100,
就会联动修改两条Line的长度加上增量.

不过这样还是太简单了,
CAD的拉伸参数是可以带属性的角度,也就是向右拉100+90°,就是向上拉100.
我还没有这样做...作为demo我觉得简简单单就好了.
这个角度是依照 拉伸参数的两个Point 确定的向量,
也就是拉伸参数需要两个点...这个我也没有做

// 使用示例
class Program {
    static void Main() {
        var dag = new DAG<string>();
        
        // 构建DAG
        dag.AddEdge("A煮饭", "B吃饭");
        dag.AddEdge("A煮饭", "C抠脚");
        dag.AddEdge("B吃饭", "D睡觉");
        dag.AddEdge("C抠脚", "D睡觉");

        // 另一个顺序
/*
        dag.AddEdge("A煮饭", "C抠脚");
        dag.AddEdge("A煮饭", "B吃饭");
        dag.AddEdge("B吃饭", "D睡觉");
        dag.AddEdge("C抠脚", "D睡觉");
*/
        
        // 获取拓扑排序
        var sorted = dag.TopologicalSort();
        Console.WriteLine("拓扑排序结果: " + string.Join(" -> ", sorted));

        // 输出: A -> B -> C -> D 或 A -> C -> B -> D
        // 输出结果为什么多个?取决于加入边的顺序.
        // 因为存在多个入度为0,表示可以一起播放,这是正确的.
        // 先吃饭还是先抠脚并没有顺序,但是先煮饭才可以吃饭就需要有序.
    }
}
using System;
using System.Collections.Generic;
using System.Linq;

// 有向无环图(DAG)类,泛型T必须是非null且可比较的类型
public class DAG<T> where T : notnull , IEquatable<T> {
    // 邻接表存储图结构
    private readonly Dictionary<T, List<T>> _adjacencyList = new();
    
    // 添加节点
    public void AddNode(T node)
    {
        // 如果节点不存在则添加
        if (!_adjacencyList.ContainsKey(node))
            _adjacencyList[node] = new List<T>();
    }
    
    // 添加边并检查是否形成环
    public bool AddEdge(T from, T to)
    {
        // 确保两个节点都存在
        AddNode(from);
        AddNode(to);
        
        // 检查是否会形成环
        if (CreatesCycle(from, to))
            return false;
            
        // 添加边
        _adjacencyList[from].Add(to);
        return true;
    }
    
    // 使用DFS检查添加边是否会形成环
    private bool CreatesCycle(T start, T end)
    {
        var visited = new HashSet<T>();
        var stack = new Stack<T>();
        stack.Push(end);
        
        while (stack.Count > 0)
        {
            var current = stack.Pop();
            
            // 如果找到起点,说明会形成环
            if (current.Equals(start))
                return true;
                
            // 跳过已访问节点
            if (!visited.Add(current))
                continue;
                
            // 将邻居压入栈
            foreach (var neighbor in _adjacencyList[current])
                stack.Push(neighbor);
        }
        return false;
    }
    
    // 使用Kahn算法进行拓扑排序
    public List<T> TopologicalSort()
    {
        var inDegree = new Dictionary<T, int>(); // 存储每个节点的入度
        var result = new List<T>();             // 存储排序结果
        var queue = new Queue<T>();              // 存储入度为0的节点
        
        // 初始化所有节点的入度为0
        foreach (var node in _adjacencyList.Keys)
            inDegree[node] = 0;
            
        // 计算每个节点的入度
        foreach (var edges in _adjacencyList.Values)
            foreach (var node in edges)
                inDegree[node]++;
        
        // 将所有入度为0的节点加入队列
        foreach (var node in inDegree.Keys)
            if (inDegree[node] == 0)
                queue.Enqueue(node);
                
        // 处理队列
        while (queue.Count > 0)
        {
            var current = queue.Dequeue();
            result.Add(current);
            
            // 减少邻居节点的入度
            foreach (var neighbor in _adjacencyList[current])
            {
                inDegree[neighbor]--;
                // 如果邻居节点入度变为0,加入队列
                if (inDegree[neighbor] == 0)
                    queue.Enqueue(neighbor);
            }
        }
        // 如果结果数量不等于节点数量,说明有环,返回空列表
        if (result.Count != _adjacencyList.Count) result.Clear();
        return result;
    }
}

// CAD图元类
public class CadEntity
{
    public int ObjectId { get; set; }           // 图元ID
    public string Type { get; set; }            // 图元类型

    // 图元属性<"EndX", 50.0>
    public Dictionary<string, object> Properties { get; } = new();
    
    // 参数名到属性名的映射<"距离1","EndX">
    private readonly Dictionary<string, string> _paramToPropertyMap = new();
    // 存储属性原始值
    private readonly Dictionary<string, object> _originalValues = new();

    // 添加驱动关系
    public void AddDrivenProperty(string paramName, string propertyName)
    {
        // ....距离1,EndX
        // Console.WriteLine($"....{paramName},{propertyName}");
        _paramToPropertyMap[paramName] = propertyName;
        // 保存属性原始值
        if (!_originalValues.ContainsKey(propertyName) && Properties.ContainsKey(propertyName))
        {
            _originalValues[propertyName] = Properties[propertyName];
        }
    }

    // 获取驱动属性名
    public bool TryGetDrivenProperty(string paramName, out string propertyName)
    {
        return _paramToPropertyMap.TryGetValue(paramName, out propertyName);
    }

    // 获取属性原始值
    public object GetOriginalValue(string propertyName)
    {
        return _originalValues.TryGetValue(propertyName, out var value) ? value : null;
    }
}

// 动态参数类
public class DynParameter
{
    public string Type { get; set; }     // 参数类型,例如:线性参数
    public string Name { get; set; }     // 参数名,例如:距离1
    public object Value { get; set; }    // 参数值
    public bool IsIncremental { get; set; } // 是否为增量更新
}

// 动态动作类,实现IEquatable接口
public class DynAction : IEquatable<DynAction>
{
    public string Type { get; set; }     // 参数类型,例如:拉伸动作
    public string Name { get; set; }                     // 动作名称,例如:拉伸1
    public bool IsIncremental { get; set; }             // 是否为增量动作
    public HashSet<DynParameter> SourceParameters { get; } = new(); // 依赖的参数集合
    public HashSet<CadEntity> TargetEntities { get; } = new();      // 目标图元集合

    // 添加依赖参数
    public void AddSourceParameter(DynParameter param) => SourceParameters.Add(param);
    // 添加目标图元
    public void AddTargetEntity(CadEntity entity) => TargetEntities.Add(entity);
    // 检查是否包含参数
    public bool ContainsParameter(DynParameter param) => SourceParameters.Contains(param);

    // 执行动作
    public void Execute(DynParameter changedParam)
    {
        // 如果动作不依赖此参数,直接返回
        if (!SourceParameters.Contains(changedParam)) return;

        // 更新所有目标图元
        foreach (var entity in TargetEntities)
        {
            if (entity.TryGetDrivenProperty(changedParam.Name, out var propertyName))
            {
                // 增量更新处理
                if (IsIncremental || changedParam.IsIncremental)
                {
                    var originalValue = Convert.ToDouble(entity.GetOriginalValue(propertyName));
                    var delta = Convert.ToDouble(changedParam.Value);
                    entity.Properties[propertyName] = originalValue + delta;
                }
                else
                {
                    // 直接更新
                    entity.Properties[propertyName] = changedParam.Value;
                }
            }
        }
    }

    // 实现IEquatable接口
    public bool Equals(DynAction other) {
        if (other is null) return false;
        return Name == other.Name;
    }
    public override int GetHashCode() => Name.GetHashCode();
}

// 动态块类
public class DynamicBlock {
    // 参数名(自增/用户改),动态参数
    private readonly Dictionary<string, DynParameter> _parameterMap = new();
    // id,包裹信息的自定义图元
    private readonly Dictionary<int, CadEntity> _entitieMap = new();
    // 有向无环图,动作的播放顺序,例如先拉伸,后旋转,不能无序播放
    private readonly DAG<DynAction> _actionGraph = new();
     // 动作映射表,key是new一个对象用来查询,只要名称相同就提取value里面已经储存的.
    private readonly Dictionary<DynAction, DynAction> _actionMap = new();


    // 添加约束关系
    public void AddConstraint(DynParameter param, DynAction action, CadEntity entity, string drivenProperty)
    {
        // 直接抛错?不行啊,顶多是提示一下,因为存在包裹关系.
        // 注册参数,多个图元绑定同一个动作.
        // throw new($"1,重复键: {param.Name}");
        if (!_parameterMap.ContainsKey(param.Name))
            _parameterMap.Add(param.Name, param);

        // 注册图元
        if (!_entitieMap.ContainsKey(entity.ObjectId))
            _entitieMap.Add(entity.ObjectId, entity);

        // 建立驱动关系
        entity.AddDrivenProperty(param.Name, drivenProperty);
        
        // 获取或创建动作实例
        if (!_actionMap.TryGetValue(action, out var existingAction))
        {
            existingAction = action;
            _actionMap[action] = existingAction;
            _actionGraph.AddNode(existingAction);
        }
        // 添加动作依赖和目标
        existingAction.AddSourceParameter(param);
        existingAction.AddTargetEntity(entity);
    }

    // 添加动作间的依赖关系
    public bool AddActionDependency(DynAction fromAction, DynAction toAction)
    {
        // 确保两个动作都存在
        if (!_actionMap.ContainsKey(fromAction) || !_actionMap.ContainsKey(toAction))
            return false;
            
        return _actionGraph.AddEdge(_actionMap[fromAction], _actionMap[toAction]);
    }

    // 更新参数值
    public void UpdateParameter(string paramName, object value, bool isIncremental = false)
    {
        // 如果参数不存在则返回
        if (!_parameterMap.TryGetValue(paramName, out var param)) return;
        
        // 更新参数值
        param.Value = value;
        param.IsIncremental = isIncremental;
        
        // 获取所有依赖该参数的动作
        var dependentActions = _actionMap.Values
            .Where(a => a.ContainsParameter(param))
            .ToList();
            
        if (dependentActions.Count == 0) return;
        
        // 获取拓扑排序后的动作执行顺序
        var sortedActions = _actionGraph.TopologicalSort();
        Console.WriteLine("执行顺序: " + string.Join(" -> ", sortedActions.Select(a => a.Name)));

        // 按顺序执行相关动作
        foreach (var action in sortedActions)
        {
            if (dependentActions.Contains(action))
            {
                action.Execute(param);
            }
        }
    }
}

// 使用示例
class Program
{
    static void Main()
    {
        // 创建两条直线图元
        var line1 = new CadEntity { ObjectId = 12, Type = "Line" };
        line1.Properties["StartX"] = 0.0;
        line1.Properties["EndX"] = 50.0;
        line1.Properties["Color"] = 61;
        line1.Properties["Rotation"] = 0.0; // 添加初始旋转角度

        var line2 = new CadEntity { ObjectId = 13, Type = "Line" };
        line2.Properties["StartX"] = 0.0;
        line2.Properties["EndX"] = 60.0;
        line2.Properties["Color"] = 61;
        line2.Properties["Rotation"] = 0.0; // 添加初始旋转角度

        // 创建动态块
        var block = new DynamicBlock();

        // 创建参数
        var stretchParam = new DynParameter { Type = "线性参数", Name = "距离1", Value = line1.Properties["EndX"]  };
        var stretchParam2 = new DynParameter { Type = "线性参数", Name = "距离2", Value = line2.Properties["EndX"] };
        var colorParam = new DynParameter { Type = "颜色参数", Name = "颜色1", Value = line1.Properties["Color"] };
        var rotateParam = new DynParameter { Type = "旋转参数", Name = "旋转1", Value = 0.0 };

        // 创建动作
        var stretchAction = new DynAction { Type = "拉伸动作", Name = "拉伸动作1", IsIncremental = true };
        var colorAction = new DynAction { Type = "颜色动作",  Name = "颜色动作1" };
        var rotateAction = new DynAction { Type = "旋转动作", Name = "旋转动作1", IsIncremental = true };

        // 建立约束关系
        // 两个图元各自参数,绑定一个动作
        block.AddConstraint(stretchParam, stretchAction, line1, "EndX");
        block.AddConstraint(stretchParam2, stretchAction, line2, "EndX");

        // 同一个参数和同一个动作,绑定多个图元
        block.AddConstraint(colorParam, colorAction, line1, "Color");
        block.AddConstraint(colorParam, colorAction, line2, "Color");
        block.AddConstraint(rotateParam, rotateAction, line1, "Rotation");
        block.AddConstraint(rotateParam, rotateAction, line2, "Rotation");

        // 建立动作间的依赖关系
        // 假设旋转动作依赖于拉伸动作
        block.AddActionDependency(stretchAction, rotateAction);

        // 执行参数更新.
        Console.WriteLine("第一次更新 - 只修改拉伸量:");

        // 1,点选图块,四叉树过滤.
        // 2,夹点Point3d拉伸动作,二分法获取夹点是哪个动作的.
        // 50.0 => "EndX" => "距离1"
        // 然后检索得到"距离1",再设置数值
        // var getPt = Console.ReadLine("交互输入拉伸点"); 
        // var act = block.GetDynAction(getPt);
        
        block.UpdateParameter("距离1", 50.0, true); // 拉伸动作

        Console.WriteLine($"L1长度: {line1.Properties["EndX"]}, 颜色: {line1.Properties["Color"]}, 旋转: {line1.Properties["Rotation"]}");
        Console.WriteLine($"L2长度: {line2.Properties["EndX"]}, 颜色: {line2.Properties["Color"]}, 旋转: {line2.Properties["Rotation"]}");

// 这是在原始的基础上面拉的,
// 如果你需要在上面已经拉伸过的基础再拉,就需要换算一下
// 那么为什么不距离1就是0呢?这样换算就方便多了.
        Console.WriteLine("\n第二次更新 - 同时修改拉伸量和旋转角度:");
        block.UpdateParameter("距离1", 30.0, true);
        block.UpdateParameter("旋转1", 45.0, true);
        Console.WriteLine($"L1长度: {line1.Properties["EndX"]}, 颜色: {line1.Properties["Color"]}, 旋转: {line1.Properties["Rotation"]}");
        Console.WriteLine($"L2长度: {line2.Properties["EndX"]}, 颜色: {line2.Properties["Color"]}, 旋转: {line2.Properties["Rotation"]}");

        Console.WriteLine("\n第三次更新 - 修改颜色:");
        block.UpdateParameter("颜色1", 97);
        Console.WriteLine($"L1长度: {line1.Properties["EndX"]}, 颜色: {line1.Properties["Color"]}, 旋转: {line1.Properties["Rotation"]}");
        Console.WriteLine($"L2长度: {line2.Properties["EndX"]}, 颜色: {line2.Properties["Color"]}, 旋转: {line2.Properties["Rotation"]}");
    }
}
posted @ 2025-05-05 04:19  惊惊  阅读(160)  评论(0)    收藏  举报