进阶教程 | AnyCAD .NET桥梁建模(3):交互式创建
点击此处查看最新的网赚项目教程
上一章节,我们引入 dxf 格式对桥梁模型数据进行描述、保存和解析。
本章节中,我们将采用交互式创建桥梁。在此章节完成后,我们的桥梁设计工具将更贴近桥梁工程师的使用习惯。
几何数据的导出
在介绍交互式操作之前,先介绍几何数据的持久化,在本章节后续内容中,将会用到保存好的几何文件。
数据格式
常见的几何数据表达格式有:stp/step, igs/iges, stl, stb 等, 在 AnyCAD 中对上述格式都进行了支持。此外,也可以采用兼容性更好的 brep 格式进行持久化。
数据导出
我们将之前章节中提到的桥墩几何进行保存:
pierShapes = createPierTypeB(1.6, 2.6, 10.5)
compound = AnyCAD.ShapeBuilder.MakeCompound(pierShapes)
AnyCAD.ShapeIO.Save(compound, 'k1.brep')
经过上述简单操作,我们就已经将参数为 W=1.6 W1=2.6 HD=10.5 的 B 型桥墩保存在 k1 文件中。
在后文中,我们将读取这一数据并使用。
交互式操作含义
交互式操作,简单来说是输入与输出的交互,输入可以是鼠标、键盘等设备信号。输出则是内容的展示。
交互式操作中,对于使用者更弱化了数据的实际数值。
下文中,我们使用 C# .NET 进行交互式操作的实现。
环境准备
从头搭建一个 AnyCAD 环境,虽说并不复杂,但老助手选择拿已有的工程来用一下。
从官方找到高级示例 RapidCAX:
git clone git@gitee.com:anycad/RapidCAX.git
编译运行,即可看到软件的运行界面。
软件启动界面AnyCAD 中的 Editor
AnyCAD 中的 Editor 定义为编辑器。可以理解为,当 Editor 激活时,会对键盘、鼠标等设备输入进行响应,而响应的具体内容可以由用户自行实现。
下面将介绍如何通过自定义响应的实现,制作一个桥梁创建工具。
在界面上添加按钮
在 SketchRibbonTab.xaml 中添加行,创建我们需要的按钮:
<Fluent:RibbonGroupBox Header="测试" IsLauncherVisible="False" Margin="7,0,0,0">
...
<Fluent:Button Header="放置桥墩" Icon="/Rapid.Common.Res;component/Image/SectionBar.png" Size="Large" Command="{Binding AddPierCommand}" Margin="0,0,7,0"/>
<Fluent:Button Header="创建直线梁" Icon="/Rapid.Common.Res;component/Image/SectionBar.png" Size="Large" Command="{Binding AddLineBeamCommand}" Margin="0,0,7,0"/>
</Fluent:RibbonGroupBox>
添加按钮导入 DXF
注意到,这个示例中已经为我们实现了导入 DXF 的功能,我们可以用该功能直接导入桥梁中轴线的草图。
导入dxf实现“放置桥墩”功能
下文将介绍放置桥墩功能的实现,放置桥墩的关键在于确定桥墩的位置和方向,我们不妨将操作定义为:用户点击的第一个点为桥墩的位置,第一个点到第二个点的向量为桥墩的方向(顺桥向)。
我们先从 Editor 继承一个子类,用于表示放置桥墩功能编辑器:
class AddPierEditor: Editor
{
public AddPierEditor()
{
SetName("AddPier");
}
}
我们重写该类的 Start 函数,用于表示当 Editor 被激活时,需要进行的准备工作。
先展示代码:
public override EnumEditorCode Start(ViewContext ctx)
{
var tmp = ctx.GetTempContext();
tmp.Start();
tmp.SetPickFilter(EnumShapeFilter.Edge);
var fileName = DialogUtil.OpenFileDialog("Shape", new StringList { "Shape Files(.brep)", "*.brep" });
if (fileName.IsEmpty())
return EnumEditorCode.Failed;
var shape = ShapeIO.Open(fileName);
if (shape == null)
return EnumEditorCode.Failed;
mShapeMaterial = BasicMaterial.Create("temp");
mShapeMaterial.SetColor(ColorTable.Green);
mNode = BrepSceneNode.Create(shape, null, mShapeMaterial);
if (mNode == null)
return EnumEditorCode.Failed;
mNode.SetVisible(false);
ctx.GetScene().AddNode(mNode);
return base.Start(ctx);
}
可以看到,当 Editor 被激活时,我们做了下面几件事:
为了将节点存储下来,我们将其定义为成员变量:
BrepSceneNode mNode;
BasicMaterial mShapeMaterial;
public override EnumEditorCode Start(ViewContext ctx)
{
//implement start here
}
接下来,我们定义“鼠标移动”时的行为。这里可以发现,鼠标移动时的行为可以分为两个阶段:
1. 桥墩的位置还没确定;
2. 桥墩的位置已经确定。
我们定义一个成员变量表示桥墩的位置,当它为 null 的时候,表示上述阶段 1;当该变量有数据时,表示上述阶段 2:
Vector3? mLocation = null;
我们定义“鼠标移动”时的行为如下:
if (mNode == null)
return EnumEditorCode.Ignored;
var pick = ctx.SnapPoint(evt.GetX(), evt.GetY());
var pt = pick.GetPosition();
if (mLocation == null)
{
mNode.SetTransform(Matrix4.makeTranslate(pt.x, pt.y, pt.z));
ctx.RequestUpdate(EnumUpdateFlags.Scene);
}
else
{
var offset = pt - mLocation;
offset.normalize();
var rot = Matrix4.makeRotation(Vector3.UNIT_X, offset);
var translate = Matrix4.makeTranslate(mLocation.x, mLocation.y, mLocation.z);
mNode.SetTransform(translate * rot);
ctx.RequestUpdate(EnumUpdateFlags.Scene);
}
mNode.SetVisible(true);
return base.OnMouseMove(ctx, evt);
代码逻辑解释为:当桥墩位置还不确定时,通过鼠标位置更新桥墩位置(桥墩跟着鼠标移动)。当桥墩位置确定时,鼠标与桥墩相对位置的向量将用于调整桥墩的朝向。
注意到,当场景 node 的数据被更新时,我们需要主动提示更新场景。并且当鼠标出现首次移动的动作时,就可以将 node 设置为可见了。
接下来我们定义鼠标点击(以弹起动作为准)时的行为:
public override EnumEditorCode OnMouseUp(ViewContext ctx, InputEvent evt)
{
if (mLocation == null)
{
var pick = ctx.SnapPoint(evt.GetX(), evt.GetY());
mLocation = pick.GetPosition();
}
else
{
// 创建好了
mNode.SetEdgeMaterial(null);
//重新创建个临时对象
mNode = BrepSceneNode.Create(mNode.GetTopoShape(), null, mShapeMaterial);
mNode.SetVisible(false);
ctx.GetScene().AddNode(mNode);
ctx.RequestUpdate(EnumUpdateFlags.Scene);
return EnumEditorCode.Exit;
}
return base.OnMouseDown(ctx, evt);
}
这段代码也容易理解:当桥墩位置还没确定时,点击确定桥墩位置;当桥墩位置已经确定时,点击确定桥墩朝向,并退出。
这里:
return EnumEditorCode.Exit;
将提示系统关闭当前 Editor。关闭前,Editor 的 Finish 成员方法会被执行:
public override void Finish(ViewContext ctx)
{
ctx.GetScene().RemoveNode(mNode.GetUuid());
ctx.RequestUpdate(EnumUpdateFlags.Scene);
base.Finish(ctx);
}
我们在 Finish 方法中定义当前 Editor 被关闭时需要执行的动作,往往是一些清理、提交、回滚等收尾工作。
由此,我们就完成了放置桥墩工具的实现。
绑定命令
命令管理器可以将命令和 Editor 关联起来,我们在 SketchRibbonViewModel.cs 中添加如下代码:
[RelayCommand]
void AddPier()
{
MainViewModel.ActiveViewModel.RenderView.SetEditor(new AddPierEditor());
}
这样就将命令与 Editor 进行了关联。还记得我们添加按钮时在 xaml 中写的 Binding 吗,绑定到的就是这个命令。
效果放置桥墩Editor实现“创建直线梁”功能
我们注意到,该例子中,桥梁的梁体有直线和圆弧两种,本段落介绍直线梁的创建工具实现。
设计思路
直线梁的创建和桥墩的放置比较相似,都是“先点一个点——再点一个点”,我们这回用第一个点表示线段起点,另一个点表示线段终点。
同样也使用一个变量对操作阶段进行标记。
相信读者心中对代码已经有个大致的思路了,附代码:
using AnyCAD.Foundation;
namespace Rapid.Sketch.Plugin
{
class AddLineBeamEditor : Editor
{
public AddLineBeamEditor()
{
SetName("AddLineBeam");
}
TopoShape mContour;
BrepSceneNode mNode;
BasicMaterial mShapeMaterial;
public override EnumEditorCode Start(ViewContext ctx)
{
var tmp = ctx.GetTempContext();
tmp.Start();
tmp.SetPickFilter(EnumShapeFilter.Edge);
var fileName = DialogUtil.OpenFileDialog("DXF", new StringList { "DXF Files(.brep)", "*.dxf" });
if (fileName.IsEmpty())
return EnumEditorCode.Failed;
var dxfShapes = DxfIO.Load(fileName.GetString());
var wires = CurveBuilder.ConnectEdgesToWires(dxfShapes, 1.0e-7, false);
var outter = CurveBuilder.MakePlanarFace(wires[1]);
var inner = CurveBuilder.MakePlanarFace(wires[0]);
mContour = BooleanTool.Cut(outter, inner);
mShapeMaterial = BasicMaterial.Create("temp");
mShapeMaterial.SetColor(ColorTable.Green);
mNode = BrepSceneNode.Create(mContour, null, mShapeMaterial);
if (mNode == null)
return EnumEditorCode.Failed;
mNode.SetVisible(false);
ctx.GetScene().AddNode(mNode);
return base.Start(ctx);
}
public override void Finish(ViewContext ctx)
{
ctx.GetScene().RemoveNode(mNode.GetUuid());
ctx.RequestUpdate(EnumUpdateFlags.Scene);
base.Finish(ctx);
}
Vector3? mStartPt = null;
public override EnumEditorCode OnMouseMove(ViewContext ctx, InputEvent evt)
{
if (mNode == null)
return EnumEditorCode.Ignored;
var pick = ctx.SnapPoint(evt.GetX(), evt.GetY());
var pt = pick.GetPosition();
if (mStartPt == null)
{
//do nothing
}
else
{
mNode.SetVisible(true);
var offset = pt - mStartPt;
if (offset.length() < 0.01)
return EnumEditorCode.Ignored;
var dir = offset.ToDir();
var zdir = new GDir(0.0, 0.0, 1.0);
var axis = new GAx3(mStartPt.ToPnt(), dir, zdir.Crossed(dir));
var worldShape = TransformTool.Transform(mContour, axis);
var path = CurveBuilder.MakeLine(pt.ToPnt(), mStartPt.ToPnt());
var beam = FeatureTool.SweepByFrenet(worldShape, path, EnumSweepTransitionMode.RightCorner, true, false, true);
mNode.SetTopoShape(beam);
ctx.RequestUpdate(EnumUpdateFlags.Scene);
}
return base.OnMouseMove(ctx, evt);
}
public override EnumEditorCode OnMouseUp(ViewContext ctx, InputEvent evt)
{
if (mStartPt == null)
{
var pick = ctx.SnapPoint(evt.GetX(), evt.GetY());
mStartPt = pick.GetPosition();
}
else
{
// 创建好了
mNode.SetEdgeMaterial(null);
//重新创建个临时对象
mNode = BrepSceneNode.Create(mNode.GetTopoShape(), null, mShapeMaterial);
mNode.SetVisible(false);
ctx.GetScene().AddNode(mNode);
ctx.RequestUpdate(EnumUpdateFlags.Scene);
return EnumEditorCode.Exit;
}
return base.OnMouseDown(ctx, evt);
}
}
}
这里对代码就不过多赘述。
注意到,在 Start 方法中,老助手直接做了这样的操作:
var outter = CurveBuilder.MakePlanarFace(wires[1]);
var inner = CurveBuilder.MakePlanarFace(wires[0]);
mContour = BooleanTool.Cut(outter, inner);
这个操作并不严谨,首先并不能保证 wires 的 size,并且也不能保证成员出现的顺序。这里只是用于示范 Editor 的实现,读者可以用 WireTreeBuilder 进行更严谨的实现。
绑定命令
同样我们对命令进行绑定
[RelayCommand]
void AddLineBeam()
{
MainViewModel.ActiveViewModel.RenderView.SetEditor(new AddLineBeamEditor());
}
效果创建直线梁Editor其他创建工具
创建圆弧梁的 Editor 留给读者作为练习。
此外,加上隔板创建工具、桥台创建工具,完整的桥梁交互式创建就基本完成了。
结语
本文介绍了 AnyCAD 中 Editor 的定义与实现方法,以放置桥墩、创建直线梁为例,示范了 Editor 的具体实现方式。桥梁建模教程也从“数值时代”跨越到了“点击时代”。
———END———
限 时 特 惠: 本站每日持续更新海量各大内部创业教程,一年会员只需98元,全站资源免费下载 点击查看详情
站 长 微 信: qs62318888
主题授权提示:请在后台主题设置-主题授权-激活主题的正版授权,授权购买:RiTheme官网