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"]}");
}
}