最近,在进行一个LEGO项目时,需要构建一个由LEGO砖块组成的穹顶。尝试了几个LEGO CAD应用程序后,仍然难以构建穹顶。然后,偶然发现了一个名为Arthur Gugick的人开发的LEGO穹顶创建器应用程序。这个应用程序是用VB6开发的,存在一些bug,比如一旦最小化,表单就会被清除。这个应用程序提供了穹顶的高度和每个螺柱的位置,这也是WPF 3D Dome Creator的工作原理。然而,它是完全2D的,无法直观地看到穹顶的外观。因此,联系了Arthur,询问是否可以提供源代码。他非常乐意提供,将其作为WPF 3D Dome Creator的基础。
LDrawPartLib是为类似于MLCad的LEGO CAD应用程序开发的库,目前仍在开发中,但库已经完成。LDrawPartLib使用LDraw零件库进行零件几何建模。LDraw是LEGO CAD应用程序的开源标准。他们有一个非常活跃的社区,并定期更新库中的新零件。在这里讨论LDrawPartLib可能超出了本文的范围。然而,如果对LDrawPartLib如何解析和创建3D LEGO零件感兴趣,将不得不查看代码,代码中有相当多的注释和LDraw的文档。当然,如果有非常具体的问题,可以随时联系。
关于穹顶创建器,使用这个应用程序作为学习WPF、3D和著名的MVVM的方式。这个应用程序是对MVVM和WPF实现的理解。对评论和改进这个应用程序的方式持开放态度。由于这个应用程序只使用一个零件,所有所需的文件都放在了“Parts”文件夹中。如果想使用LDraw零件库,需要从这里下载,并将路径放在App.Config中。另外,ldconfig.ldr,作为LDraw的一部分,需要放在应用程序路径中,因为它包含了零件颜色的所有详细信息。
应用程序本身非常简单易用。选择想要构建的穹顶类型,设置各种参数,如穹顶的直径和高度,高度只在穹顶类型不是球形时才考虑。可以通过点击颜色调色板来改变穹顶的颜色。
首先,它使用了WPF 3D。使用WPF 3D是因为想知道它的能力。这是一个很棒的3D框架,但严重怀疑这将用于开发游戏。有许多缺点,首先,框架没有内置支持创建基本图形,如立方体、球体等,其次,正如痛苦地学到的,在一个3D画布上画线可能是一个真正的痛苦。然而,后来发现如何使用3DTools和Charles Petzold的库来画线。他也写了一本关于WPF 3D的优秀书籍,名为《Windows 3D编程》。前者在实现中有bug,更多信息请参见这里,所以没有使用3DTools的线条实现。然而,3DTools的TrackBall实现是极好的。第二个有一些优秀的特性,但随着3D场景中线条零件数量的增加,性能显著下降。如果想看到线条的零件,取消注释以下行:
public class Dome3DViewModel : DomeViewModel {
...
public void CalculateDomeValues() {
...
int y = (currentHeight - noOfPlates) * (int)ScaleHeight;
for (int i = 0; i < noOfPlates; i++, y += (int)ScaleHeight) {
Point3D pt = new Point3D(x * ScaleWidth, y, z * ScaleWidth);
PartColors color = (PartColors)Enum.Parse(typeof(PartColors), MainWindowVM.ColorChooserVM.CurrentColor.Name);
Part3D part = new Part3D(PLATE_PART_CODE, PLATE_PART_CODE, color) { Position = pt };
// part.ShowLines = true;
Viewport.Children.Add(part);
}
...
}
}
关于WPF 3D有一个极好的教程在这里。
其次,正如之前提到的,Arthur的程序给出了在特定位置的LEGO板的高度。可以从“地面”插入确切数量的板,但这将创建一个实心穹顶。因此,决定只添加“可见”的板。这是通过找到其在网格中的八个邻居中哪一个有最低的高度,然后只添加最低和当前高度之间的差值来实现的。为了实现这一点,使用了以下代码:
List neighbours = new List();
...
// Fill all the values from the surrounding neighbours.
FillNeighbours(diameter, z, drCurrent, neighbours);
if (drPrev != null) FillNeighbours(dt.Rows.Count, z, drPrev, neighbours);
if (drNext != null) FillNeighbours(dt.Rows.Count, z, drNext, neighbours);
// Gets the lowest values on the top.
neighbours.Sort();
// no of viewable plates.
if (drNext == null || drPrev == null || z == 0 || z == (diameter - 1))
noOfPlates = currentHeight;
// if dome edges then show all the plates.
else if (currentHeight == neighbours[0] && currentHeight > 0)
noOfPlates = 1;
// If lowest neighbours is of the current height
// then add only one plate.
else
noOfPlates = currentHeight - neighbours[0];
第三,想从其他视图模型中访问一个视图模型。为了实现这一点,创建了一个名为ApplicationViewModel的基类。
public abstract class ApplicationViewModel : ViewModelBase {
#region ctor
protected ApplicationViewModel(MainWindowViewModel mainWindowModel) {
MainWindowVM = mainWindowModel;
}
#endregion
#region Public Properties
public MainWindowViewModel MainWindowVM {
get;
set;
}
#endregion
}
这个类的构造函数接受MainWindowViewModel作为参数。所有需要在应用程序中相互通信的视图模型都从ApplicationViewModel派生。
public class ParametersViewModel : ApplicationViewModel {
#region ctor
public ParametersViewModel(MainWindowViewModel mainWindowModel)
: base(mainWindowModel) {
DomeDiameter = 12;
DomeHeight = 16;
}
#endregion
...
}
MainWindowViewModel为这些模型中的每一个都有属性。
public class MainWindowViewModel : ViewModelBase {
#region ctor
public MainWindowViewModel() {
ParametersVM = new ParametersViewModel(this);
Dome2DVM = new Dome2DViewModel(this);
Dome3DVM = new Dome3DViewModel(this);
ColorChooserVM = new ColorChooserViewModel(this);
}
#endregion
#region Public Properties
public ParametersViewModel ParametersVM {
get;
set;
}
public Dome2DViewModel Dome2DVM {
get;
set;
}
public Dome3DViewModel Dome3DVM {
get;
set;
}
public ColorChooserViewModel ColorChooserVM {
get;
set;
}
#endregion
}
作为一个学习者,很想知道对这种应用程序开发方式的看法。如果批评是建设性的,那就太好了。