拾取图元,指的是判断鼠标光标(具体地讲是鼠标光标的热点)是否位于要拾取的图元上方。就好像在商店里找一个什么东西,我们的目光就是鼠标光标的热点,这东西就是要拾取的目标图元。如果目光定位在这东西上,就说这个东西被找到了。而对于图元来讲,就说图元被拾取了。拾取和选择容易混淆,实际上是一件事情的两个阶段。就好比这东西我找到了,但不一定把它买回去。如果把它买回去,就可以说我选择了它,我要了。
拾取图元是交互式CAD系统中非常关键的一个部分,因为大部分的计算工作都在这部分完成。如果鼠标光标离图元足够近,就说图元被拾取了。所以拾取的过程也就是计算点与图元之间距离的过程。
进行一次拾取操作时,需要对所有图元进行遍历,即对每个图元都进行计算。所以,虽然单个图元的拾取运算比较简单,但当图形比较复杂,由成千上万个基本图元组成时,计算量就相当可观了。人们想了很多办法来减少拾取工作的计算量。目前比较通用的办法是把拾取过程分成两个阶段,首先判断拾取点是否位于图元的包围矩形中,如果不在矩形中,则图元不被拾取;如果在,则计算拾取点与图元之间的距离:如果距离足够小,则图元被拾取,否则不被拾取。由于判断点是否位于矩形中的运算量比计算点与图元之间距离的运算量要小得多,所以采用这样的两步判别法拾取图元比直接通过计算点与图元的距离来拾取要快得多。包围矩形指的是包围图元的最小矩形。下面一节将介绍包围矩形的计算。
包围矩形的计算
计算包围矩形之前,首先创建一个CBox类,定义包围矩形的数据格式。然后在CGElement类中添加一个获取图元包围矩形的函数GetBox,并在各派生类中实现它。
1. 创建CBox类
用X方向上的最大值、最小值和Y方向上的最大值、最小值就可以确定一个矩形。在第5章的示例程序NetCAD的基础上,添加一个Cbox类,其代码如下:
public class CBox
{
public CBox()
{
}
private float m_minX;
private float m_minY;
private float m_maxX;
private float m_maxY;
public float minX
{
get{return m_minX;}
set{m_minX=value;}
}
public float maxX
{
get{return m_maxX;}
set{m_maxX=value;}
}
public float minY
{
get{return m_minY;}
set{m_minY=value;}
}
public float maxY
{
get{return m_maxY;}
set{m_maxY=value;}
}
}
2. 在CGElement类中添加GetBox方法
定义CBox 类以后,在CGElement类中添加一个GetBox方法。该方法返回一个CBox对象,它是图元的包围矩形。
//获取图元的包围矩形
abstract public CBox GetBox();
3. 直线段的包围矩形
直线段的包围矩形是以直线段为对角线的矩形。所以,通过比较直线段起点和终点的横坐标和纵坐标的大小就可以确定包围矩形的参数。但是,还要考虑两个特殊情况,即直线段水平或竖直的情况。在这两种情况下,按照上面定义得到的矩形就是直线段本身。所以,需要在竖向上或横向上扩大拾取范围。在CLine类中实现GetBox方法。
//计算包围矩形
override public CBox GetBox()
{
CBox aBox=new CBox();
if(m_Begin.X==m_End.X)
{
aBox.minX=m_Begin.X-Const.PickRadius;
aBox.minY=Math.Min(m_Begin.Y,m_End.Y);
aBox.maxX=m_Begin.X+Const.PickRadius;
aBox.maxY=Math.Max(m_Begin.Y,m_End.Y);
}
else if(m_Begin.Y==m_End.Y)
{
aBox.minX=Math.Min(m_Begin.X,m_End.X);
aBox.minY=m_Begin.Y-Const.PickRadius;
aBox.maxX=Math.Max(m_Begin.X,m_End.X);
aBox.maxY=m_Begin.Y+Const.PickRadius;
}
else
{
aBox.minX=Math.Min(m_Begin.X,m_End.X);
aBox.minY=Math.Min(m_Begin.Y,m_End.Y);
aBox.maxX=Math.Max(m_Begin.X,m_End.X);
aBox.maxY=Math.Max(m_Begin.Y,m_End.Y);
}
return aBox;
}
4. 矩形的包围矩形
知形的包围知形就是它本长身。在CRectangle类中添加一个GetBox方法,它重写基觉的GelBox方法并返回一个CBox兴型的对象.包围矩形的属性值用CRectangle对象左上角顶点和右下角顶点的坐标进行设置。
//计算包围矩形
override public CBox GetBox()
{
CBox aBox=new CBox();
aBox.minX = this.LT.X;
aBox.minY = this.RB.Y;
aBox.maxX = this.RB.X;
aBox.maxY = this.LT.Y;
return aBox;
}
5. 圆的包围矩形
圆的包围矩形是中心为圆心,边长为圆的直径的正方形。在CCircle类中重写基类的GetBox 方法:
//计算圆的包围矩形
override public CBox GetBox()
{
CBox aBox=new CBox();
aBox .minX=m_Center.X-Radius;
aBox .minY=m_Center.Y-Radius;
aBox .maxX=m_Center.X+Radius;
aBox .maxY=m_Center.Y+Radius;
return aBox;
}
6. 圆弧的包围矩形
圆弧的包围矩形如图5-1所示。计算圆弧的包围矩形时,首先假设有一个局部坐标系,它的原点在圆弧圆心的位置上,然后计算圆弧与坐标轴4个方向上的相交关系。
到这些相交关系呢?因为圆弧与坐标轴正向或负向的交点坐标在数值上代表了这个方向上的最大值。如果相交,则包围矩形在这个方向上的参数值便是该值。如果不相交,最大值就是圆弧起点和终点X、Y坐标的值中在这个方向上的最大值。
图5-1 圆弧的包围矩形
圆弧与4个轴的相交关系可以通过比较可能的交点与圆弧之间的关系得到。如果圆弧所在的圆与坐标轴某个方向上的交点在圆弧内,则该交点便是圆弧与坐标轴的交点,否则不是。
计算圆弧的包围矩形时,需要比较拾取点相对于圆心的方位角与圆弧起点和终点的方位角之间的关系。下面的程序中用了一个私有的InArc函数来实现。如果拾取点相对于圆心的方位角位于圆弧起点和终点的方位角之间,则返回True,否则返回False。这里需要考虑一个问题,那就是缺省时系统以逆时针方向画弧。
在CArc类中重写基类的GetBox方法:
//计算圆弧的包围矩形
override public CBox GetBox(){
float x1=Math.Min(m_Begin.X,m_End.X);
float y1=Math.Min(m_Begin.Y,m_End.Y);
float x2=Math.Max(m_Begin.X,m_End.X);
float y2=Math.Max(m_Begin.Y,m_End.Y);
for(int i=0;i<4;i++)
{
if(InArc(AngleBegin,AngleEnd,Const.PI/2*i))
{
//如果圆弧与X轴正向相交
if(i==0) { x2=m_Center.X+Radius;}
//如果圆弧与Y轴正向相交
if(i==1) { y2=m_Center.Y+Radius;}
//如果圆弧与X轴负向相交
if(i==2) {x1=m_Center.X-Radius;}
//如果圆弧与Y轴负向相交
if(i==3) { y1=m_Center.Y-Radius;}
}
}
CBox aBox=new CBox();
aBox.minX=x1;
aBox.minY=y1;
aBox.maxX=x2;
aBox.maxY=y2;
return aBox;
}
//判断角度Angle对应的点是否在Angle1至Angle2的圆弧上
private bool InArc(float Angle1,float Angle2,float Angle)
{
//如果起始角小于终止角
if(Angle1<Angle2)
{
//如果方向角在起始角与终止角之间,则返回true;
//否则返回false
if(Angle>=Angle1 && Angle<=Angle2)
{
return true;
}
else
{
return false;
}
}
//如果起始角小于终止角
else
{
//如果方向角大于起始角或小于终止角,则返回true
//否则返回false
if(Angle>=Angle1 || Angle<=Angle2)
{
return true;
}
else
{
return false;
}
}
}
7. 文本的包围矩形
根据文本的插入点、文本的宽度和高度,可以获得文本的包围矩形。下面我们没有采用这种方法,而是采用了GDI+的一些方法。首先将文本添加到一个GraphicsPath对象中,然后利用该对象的GetBounds方法获得路径的包围矩形,该包围矩形也就是文本的包围矩形。
在CText类中重写基类的GetBox 方法:
//计算文本的包围矩形
override public CBox GetBox()
{
CBox aBox=new CBox();
GraphicsPath gp=new GraphicsPath();
FontFamily fm=new FontFamily("宋体");
StringFormat sf=new StringFormat(StringFormatFlags.NoWrap);
int i=2;
switch(m_Style)
{
case FontStyle.Bold:
i=0;
break;
case FontStyle.Italic:
i=1;
break;
case FontStyle.Regular :
i=2;
break;
case FontStyle.Strikeout :
i=3;
break;
case FontStyle.Underline :
i=4;
break;
}
gp.AddString(m_Content, fm, i, m_Size, m_Pos, sf);
RectangleF rect= new RectangleF(0,0,0,0);
rect=gp.GetBounds();
aBox.minX = rect.Left;
aBox.minY = rect.Top - rect.Height;
aBox.maxX = rect.Right;
aBox.maxY = rect.Bottom - rect.Height;
gp.Dispose();
return aBox;
}
拾取图元
前面说了,如果拾取点位于包围矩形中,则继续计算拾取点与图元之间的距离。如果距离足够小,则图元被拾取,否则不被拾取。在图元拾取的计算中,有3种计算是最常用的,即点与点之间的距离计算、点与直线段之间距离的计算和点相对于另一点的方位角的计算。为了提高拾取效率,常常采用距离的近似算法。比如点与点之间的距离计算和点与直线段之间的距离计算都涉及到开方运算,比较费时。所以,常常采用近似计算,把它转化为四则运算。
实现图元拾取,需要在CGElement 类中添加一个Pick方法,然后在各派生类中实现它。
1. 在CGElement类中添加 Pick方法
在CGElement 类中添加一个必须重写的Pick 方法。该方法有两个参数,一个是PointF对象,表示拾取点;另一个是单精度值,表示拾取半径。返回一个布尔值。
//拾取图元
abstract public bool Pick(PointF aPos);
2. 直线段的拾取
拾取直线段,需要知道拾取点与直线段之间的距离。CAD应用中常采用近似计算。一种方案是用拾取点到直线段的水平距离和竖直距离之间的小者来代替真实距离?
在CLine类中添加一个计算点到直线段距离的私有函数distPtoL和一个计算直线段截距式方程参数的私有函数LineKX,然后重写基类的Pick方法:
//计算点到直线段的距离
private float distPtoL(PointF aPos,CLine aLine)
{
float px,py,dist;
float distX,distY;
float[] kc;
px=aPos.X;
py=aPos.Y;
//获取直线段的截距式方程,返回斜率和截距
kc=aLine.LineKX();
//如果为水平直线段
if(kc[0]==0)
{
distX=10000;
distY=Math.Abs(py-aLine.LBegin.Y);
}
//如果为竖直直线段
else if(kc[0]==10000)
{
distX=Math.Abs(px-aLine.LBegin.X);
distY=10000;
}
//如果为斜线
else
{
distX=Math.Abs(px-(py-kc[1])/kc[0]);
distY=Math.Abs(py-(kc[0]*px+kc[1]));
}
//返回水平距离和竖直距离之间的小值
dist=Math.Min(distX,distY);
return dist;
}
//计算直线段的截距式方程
private float[] LineKX()
{
float kc0,kc1;
//若直线段不为竖直线段
if(m_Begin.X!=m_End.X)
{
kc0=(m_End.Y-m_Begin.Y)/(m_End.X-m_Begin.X);
}
//如果是竖直线段
else
{
kc0=10000f;
}
//计算截距
kc1=m_Begin.Y-kc0*m_Begin.X;
float[] kc={kc0,kc1};
return kc;
}
//拾取直线段
override public bool Pick(PointF aPos)
{
CBox geBox=new CBox();
//判断拾取点是否在测试包围矩形中,若不是,
//则直线段不被拾取
if(!(m.InBox(GetBox(),aPos)))
{
return false;
}
else
{
if(distPtoL(aPos,this)<Const.PickRadius)
{
return true;
}
else
{
return false;
}
}
}
3. 矩形的拾取
拾取矩形的问题可以转化为拾取直线段的问题,当代表矩形任意一条边的直线段被拾取时,矩形被拾取。在CRectangle类中添加一个Pick 方法,它重写了基类的Pick 方法。代码中,首先创建了4个CLine类实例,并分别设置为代表矩形4条边的直线段,然后给出一个If语句,当至少有一条直线段被拾取时,整个矩形被拾取。
//拾取矩形
override public bool Pick(PointF aPos)
{
//首先把要拾取的矩形分解为四条直线段
CLine line0=new CLine();
CLine line1=new CLine();
CLine line2=new CLine();
CLine line3=new CLine();
line0 = new CLine(this.LT, new PointF(this.LT.X, this.RB.Y));
line1 = new CLine(new PointF(this.LT.X, this.RB.Y), this.RB);
line2 = new CLine(this.RB, new PointF(this.RB.X, this.LT.Y));
line3 = new CLine(new PointF(this.RB.X, this.LT.Y), this.LT);
//如果有一条直线段被拾取,则整个矩形被拾取
if(line0.Pick(aPos) ||
line1.Pick(aPos) ||
line2.Pick(aPos) ||
line3.Pick(aPos))
{
return true;
}
else
{
return false;
}
}
4. 圆的拾取
假设拾取半径为PickRadius,要拾取的圆的半径为R,则以该圆的圆心为圆心,以R-PickRadius和R+PickRadius为半径得到一个环带,当拾取点落在该环带内时,圆被拾取。在CCircle类中重写基类的Pick方法:
//拾取圆
override public bool Pick(PointF aPos)
{
float dist;
if(!(m.InBox(GetBox(),aPos)))
{
return false;
}
else
{
dist=m.DistPtoP(aPos,m_Center);
if((dist>Radius-Const.PickRadius) && (dist<Radius+Const.PickRadius))
{
return true;
}
else
{
return false;
}
}
}
5. 圆弧的拾取
判断圆孤是否被拾取,需要首先判断拾取点是否落在园孤所在的圆上;如果在圆上,则继续判断拾取点相对于圆心的方位角是否位于圆弧起点和终点的方位角之间;如果在,则圆弧被拾取,否则不被拾取。编程时需要注意,缺省时GDI+按逆时针方向绘圆弧。
在CArc类中重写基类的Pick方法:
//判断角度Angle对应的点是否在Angle1至Angle2的圆弧上
private bool InArc(float Angle1,float Angle2,float Angle)
{
//如果起始角小于终止角
if(Angle1<Angle2)
{
//如果方向角在起始角与终止角之间,则返回true;
//否则返回false
if(Angle>=Angle1 && Angle<=Angle2)
{
return true;
}
else
{
return false;
}
}
//如果起始角小于终止角
else
{
//如果方向角大于起始角或小于终止角,则返回true
//否则返回false
if(Angle>=Angle1 || Angle<=Angle2)
{
return true;
}
else
{
return false;
}
}
}
//拾取圆弧
override public bool Pick(PointF aPos)
{
//如果拾取点不在包围矩形中,则该圆弧不被拾取
if(!(m.InBox(GetBox(),aPos)))
{
return false;
}
else
{
//计算拾取点与圆心之间的距离
float dist=m.DistPtoP(aPos,m_Center);
//计算拾取点的方位角
float angle=m.GetAngle(m_Center,aPos);
//如果起始角小于终止角
if(AngleBegin<AngleEnd)
{
//如果拾取点的方位角界于起始角和终止角之间,
//则拾取点到圆心的距离与圆弧的半径接近,
//则圆弧被拾取,否则不被拾取
if((angle>=AngleBegin && angle<=AngleEnd) &&
(Math.Abs(Radius-dist)<=Const.PickRadius))
{
return true;
}
else
{
return false;
}
}
//如果起始角大于终止角
else
{
//如果拾取点的方位角大于等于起始角或小于等于终止角
//且拾取点到圆心的距离与圆弧的半径接近,
//则该圆弧被拾取;否则不被拾取
if((angle>=AngleBegin || angle<=AngleEnd) &&
(Math.Abs(Radius-dist)<=Const.PickRadius))
{
return true;
}
else
{
return false;
}
}
}
}
6. 文本的拾取
拾取文本比较简单,当拾取点位于文本的包围矩形中时,文本被拾取。在CText 类中重写基类的Pick 方法。
//拾取文本
override public bool Pick(PointF aPos)
{
if(m.InBox(GetBox(), aPos))
{
return true;
}
else
{
return false;
}
}