引子
在上一篇博客【工业软件开发学习——AnyCAD篇】事务(Transaction)与撤销(Undo)回退(Redo)机制我们简单介绍了AnyCAD的事务机制,今天接着讨论一些与文档、对象相关联的话题。
现代工业设计软件离不开参数化建模这一核心功能,这是Rhino、NX、Revit等一众软件与AutoCAD这类传统绘图软件的一大不同,能够自定义对象并关联参数是开发与设计复杂工业软件的重要前提,我们来看看AnyCAD提供了何种机制来帮助开发者实现这一目标。
概述
遵循 组合优于继承(Composition over inheritance) 这一原则,AnyCAD不允许通过直接继承EntityElement或其子类的方式来扩展实体对象,而是提供了一套模式(Schema)与组件(Component)相互作用的机制来自定义实体的行为与参数特性,本篇文章通过一个简单例子(定义一个参数化的圆柱体,使其属性可以被查询与修改)展示这一框架的应用。
数据模型与对象模式
AnyCAD中,在不采用继承的情况下自定义一个具有特定数据与行为的对象需要分别从ElementSchema与ElementModel进行扩展。
MyCylinderModel类定义了MyCylinder所需要的三个几何参数(Location、Radius、Height)和一个纯业务参数(UserName):
/// <summary>
/// 参数化圆柱的数据模型,用于定义参数
/// </summary>
public class MyCylinderModel : ElementModel
{
/// <summary>
/// 圆柱位置
/// </summary>
public GPnt Location = new();
/// <summary>
/// 圆柱半径
/// </summary>
public double Radius = 10;
/// <summary>
/// 圆柱高
/// </summary>
public double Height = 20;
/// <summary>
/// 自定义名称
/// </summary>
public string UserName = "我是圆柱体";
}
MyCylinderSchema类定义了MyCylinder如何基于属性参数构造几何:
/// <summary>
/// 参数化圆柱的数据模式,用于定义行为
/// </summary>
public class MyCylinderSchema : ElementSchema
{
/// <summary>
/// 构造函数,指定SchemaName
/// </summary>
public MyCylinderSchema()
: base(nameof(MyCylinderModel)) // Schema名称
{
}
/// <summary>
/// 重写CreateModel方法,关联所定义的MyCylinderModel
/// </summary>
/// <returns></returns>
public override ElementModel CreateModel()
{
return new MyCylinderModel();
}
/// <summary>
/// 指定参数化圆柱用何种Element表示,这里我们用最基础的ShapeElement
/// </summary>
/// <returns></returns>
public override string OnGetInstanceClassName()
{
return nameof(ShapeElement);
}
/// <summary>
/// 定义参数变化时如何进行更新
/// </summary>
/// <param name="document"></param>
/// <param name="properties"></param>
/// <returns></returns>
public override bool OnParameterChanged(Document document, SchemaComponent properties)
{
var element = ShapeElement.Cast(properties.GetEntity());
if (element == null)
{
return false;
}
// 通过model加载参数
MyCylinderModel model = new();
model.Load(properties);
// 基于参数更新对象几何
var shape = ShapeBuilder.MakeCylinder(new GAx2(model.Location, new GDir(0, 0, 1)), model.Radius, model.Height, 0);
element.SetShape(shape);
return true;
}
}
其中SchemaName是用于唯一标识该schema的字符串,可以自行指定,这里我们直接用了MyCylinderModel的类名;CreateModel返回一个实例化的MyCylinderModel对象,用于参数的传递;OnGetInstanceClassName用于指定使用何种Element,我们继续沿用通过TopoShape表达形状的ShapeElement(这里也从侧面表明,并没有新的Element子类被定义,只是通过组合的方式扩展了ShapeElement的表达能力);OnParameterChanged会在参数发生变化时被调用(复习:注意参数更新需要在事务中),这里我们通过model.Load
将保存在SchemaComponent中的属性集加载到model实例(原理后面将讨论),然后基于这些参数通过我们熟悉的方式构造TopoShape,并赋予ShapeElement实例。
最后,上面定义的模式(Schema)需要进行注册才能使用,我们直接在AnyCAD组件初始化完成时增加注册的代码:
// MainViewModelImpl.cs
// ..
protected override void DoInitialize()
{
base.DoInitialize();
Viewer.SetStandardView(EnumStandardView.DefaultView, false);
// 注册定义好的schema
{
ElementSchemaStore.Instance.RegisterType(typeof(MyCylinderSchema));
}
}
基于模式创建MyCylinder
进行完上述准备工作,我们回到如何创建对象上来。之前更改先前实现的OnNewCylinder方法:
// MainViewModelImpl.cs
// ..
[RelayCommand]
void OnNewCylinder()
{
UndoTransaction undo = new(Document);
undo.Start("create.cylinder.by.schema"); // 开启事务
// 从单例ElementSchemaManager中获取schema的实例
var schema = ElementSchemaManager.Instance().FindSchema(nameof(MyCylinderModel));
if (schema != null)
{
// 通过schema创建实体对象
var element = schema.CreateInstance(schema.GetName(), Document);
// 为实体对象关联组件,以记录属性
schema.AddComponent(element.GetId(), Document);
undo.Commit(); // 创建成功,提交事务
}
else
{
undo.Abort(); // schema为空,回退事务
}
}
此时编译运行程序,依然通过菜单栏创建圆柱体,可以看到场景中对象正确显示:
属性查询
单看上面结果与此前直接创建ShapeElement并无区别,但此时文档中的圆柱体对象已经关联了我们所定义的属性参数,这里新增一个菜单项查询选中对象的属性:
// MainViewModelImpl.cs
// ..
[RelayCommand]
void OnQueryProperties()
{
// 获取选择集
var selection = mRenderView.SelectionManager.GetSelection();
if (selection != null && !selection.IsEmpty())
{
// 根据ObjectId从文档中查找对象
var id = selection.GetItem(0).GetObjectId();
var entity = EntityElement.Cast(Document.FindElement(id));
if (entity == null)
return;
// 查询实体对象所关联的属性参数
ParameterDict pd = new();
entity.ListParameters(pd);
MessageBox.Show(pd.ToJSON(), "Properties");
}
}
简单用一个MessageBox打印结果:
可以看到属性集序列化后是一个JSON对象,其中包括一些系统内置(BuiltIn)的属性字段如ClassName、SchemaName等,也有我们自定义的4个新字段(Location、Radius、Height、UserName):
{
"ClassName": {
"type": "STRING",
"value": "ShapeElement",
"unit": "",
"group": ""
},
"Height": {
"type": "DOUBLE",
"value": 20.0,
"unit": "",
"group": ""
},
"Location": {
"type": "GPNT",
"value": [
0.0,
0.0,
0.0
],
"unit": "",
"group": ""
},
"Name": {
"type": "STRING",
"value": "MyCylinderModel",
"unit": "",
"group": ""
},
"Radius": {
"type": "DOUBLE",
"value": 10.0,
"unit": "",
"group": ""
},
"SchemaName": {
"type": "STRING",
"value": "MyCylinderModel",
"unit": "",
"group": ""
},
"UserId": {
"type": "INT32",
"value": 0,
"unit": "",
"group": ""
},
"UserName": {
"type": "STRING",
"value": "我是圆柱体",
"unit": "",
"group": ""
}
}
总结
AnyCAD提供模式(Schema)与组件(Component)的机制让我们避免使用继承就扩展了实体对象(EntityElement)的数据与行为,由于SDK对这部分内容封装得比较完善,我们只是简单定义了两个子类MyCylinderModel和MyCylinderSchema就实现了参数化对象定义,而并未实际看到组件(Component)是如何在其中起作用,下一篇文章我们将通过尝试修改属性->触发关联更新来探索更多细节,感兴趣的小伙伴可以关注订阅我的博客,谢谢!
P.S. 惊喜地发现已经有朋友因为看到我的文章开始尝试使用AnyCAD(试用AnyCAD的造型功能(一)),希望自己也能再接再厉,学习和分享更多有关工业软件开发的知识。