目录
- 1 基本思路
- 2 相关知识点
- 2.1 ECS坐标系概述
- 2.2 其他点坐标转换接口
- 2.3 如何获取多段线的顶点ECS坐标
- 3 实现例程
- 3.1 接口实现
- 3.2 测试代码
- 4 实现效果
在CAD的二次开发中,点和多段线的关系是一个非常重要且常见的问题,本文实现例程以张帆所著《ObjectARX 开发基础与实例教程》为基础,并完善和修复了部分问题。
1 基本思路
点和多段线的关系判断算法有两个思路:叉乘判断法(适用于凸多边形)和射线法,本文以射线法进行代码实现。其基本思路为:
- 如果点在多段线上,返回该结果。
- 从给定点出发,沿某个方向做一条射线,计算射线与多边形交点的数量。如果交点数量为奇数,那么点在图形内部;如果交点数量为偶数,那么点在图形外部。
- 在第2条的基础上,排除交点在多段线的顶点上的情况;若出现该情况,需要旋转射线重新判断。
2 相关知识点
2.1 ECS坐标系概述
在该例程中,有一个关键点是理解ECS(或者OCS)坐标系统。
ECS是实体(对象)坐标系,其原点是WCS的原点,X和Y轴所在平面的法向量是实体的法向量。ECS的 X 轴和Y轴的方向由任意轴算法确定,也就是X、Y轴的方向是由法向量与 WCS 的关系来确定的。因此,ECS的X 轴和Y轴是唯一且仅由法向量决定的。另外,ECS的Z坐标是指xy平面距离WCS原点的距离(有正负)。
在ObjectARX有一个暴露的接口可以完成WCS→ECS的转换
bool acdbWcs2Ecs(ads_point p,ads_point q,ads_point norm,bool vec);
上述接口的实现应该是下列办法
//将WCS转为一个平面实体的ECS坐标
int Wcs2Ecs(const AcGeVector3d vtNorm, const AcGePoint3d ptWcs, BOOL bDisp, AcGePoint3d& ptEcs)
{
struct resbuf fromrb, torb;
fromrb.restype = RTSHORT;
fromrb.resval.rint = 0; // WCS
torb.restype = RT3DPOINT;
ads_point_set(asDblArray(vtNorm), torb.resval.rpoint);
return acedTrans(asDblArray(ptWcs), &fromrb, &torb, bDisp, asDblArray(ptEcs));
}
理解了上述知识点,我们之后在判断射线和多段线关系的时候,就可以把射线转为多段线的ECS坐标,然后在多段线所在的ECS平面上,比较射线和多段线的每一条线段的相交关系。从而可以优化算法。
2.2 其他点坐标转换接口
- 点或向量坐标变换
函数名 | 作用 |
---|---|
acdbUcs2Ecs | UCS→ECS |
acdbEcs2Ucs | ECS→UCS |
acdbUcs2Wcs | UCS→WCS |
acdbWcs2Ucs | WCS→UCS |
acdbEcs2Wcs | ECS→WCS |
acdbWcs2Ecs | WCS→ECS |
- AcGePoint3d和ads_point互转
函数名 | 作用 |
---|---|
AcGePoint3d (AcGePoint2d ) → ads_point | asDblArray |
ads_point → AcGePoint3d (AcGePoint2d ) | aspnt3d 或 asPnt2d |
2.3 如何获取多段线的顶点ECS坐标
多段线获取顶点坐标有两种重载形式:
Acad::ErrorStatus getPointAt(
unsigned int,
AcGePoint3d& pt
) const;
Acad::ErrorStatus getPointAt(
unsigned int index,
AcGePoint2d& pt
) const;
其中第一种获取的是顶点的WCS坐标,第二种获取的是顶点的ECS 2D坐标。这个区别一定要看仔细。
3 实现例程
3.1 接口实现
该例程在张帆所著方法的基础上,增加了对UCS坐标系的支持。在此,再次向原书作者表达敬意。
#include "dbxutil.h"
#define POINT_POLY_INSIDE 1
#define POINT_POLY_OUTSIDE 0
#define POINT_POLY_ONEDGE 2
// 在数组中查找某个点,返回点在数组中的索引,未找到则返回-1
int FindPoint(const AcGePoint2dArray &points, const AcGePoint2d &point, double tol /*= 1.0E-7*/)
{
TADSGePoint3d pt1;
TADSGePoint3d pt2(point);
for (int i = 0; i < points.length(); i++)
{
pt1 = points[i];
if (IsEqual(pt1, pt2, tol))
{
return i;
}
}
return -1;
}
//-----------------------------------------------------------------------------+
//=Description: 几何类射线和多段线的交点
//=Parameter: pPoly[in] 多段线
//=Parameter: geRay[in] 射线(位于多段线的OCS平面上)
//=Parameter: arptIntersect[in] 返回的交点(坐标系为多段线的OCS坐标系)
//=Parameter: tol[in] 容差
//-----------------------------------------------------------------------------+
static void IntersectWithGeRay(const AcDbPolyline *pPoly, const AcGeRay2d &geRay, AcGePoint2dArray& arptIntersect, double tol = 1.0E-7)
{
arptIntersect.removeAll();
//设置容差,该容差为两点相同时的容差
AcGeTol geTol;
geTol.setEqualPoint(tol);
// 多段线的每一段分别与射线计算交点
AcGePoint2d pt2d;
for (int i = 0; i < pPoly->numVerts(); i++)
{
if (i < pPoly->numVerts() - 1 || pPoly->isClosed() == Adesk::kTrue)
{
double dBulge = 0;
pPoly->getBulgeAt(i, dBulge);
if (fabs(dBulge) < 1.0E-7)
{
// 构建几何类的线段来计算交点
AcGeLineSeg2d geLine;
Acad::ErrorStatus es = pPoly->getLineSegAt(i, geLine);
AcGePoint2d ptIntersect;
if (geLine.intersectWith(geRay, ptIntersect, geTol) == Adesk::kTrue)
{
if (FindPoint(arptIntersect, ptIntersect, tol) < 0)
arptIntersect.append(ptIntersect);
}
}
else
{
// 构建几何类的圆弧来计算交点
AcGeCircArc2d geArc;
pPoly->getArcSegAt(i, geArc);
AcGePoint2d pt1, pt2;
int iCount = 0;
if (Adesk::kTrue == geArc.intersectWith(geRay, iCount, pt1, pt2, geTol))
{
if (FindPoint(arptIntersect, pt1, tol) < 0)
arptIntersect.append(pt1);
if (iCount > 1 && FindPoint(arptIntersect, pt2, tol) < 0)
arptIntersect.append(pt2);
}
}
}
}
}
// 点是否是多段线的顶点
static bool PointIsPolyVert(AcDbPolyline *pPoly, const AcGePoint2d &pt, double tol)
{
AcGeTol geTol;
geTol.setEqualPoint(tol);
AcGePoint2d ptVert;
for (int i = 0; i < (int)pPoly->numVerts(); i++)
{
pPoly->getPointAt(i, ptVert);
if (ptVert.isEqualTo(pt, geTol))
{
return true;
}
}
return false;
}
// 从数组中过滤掉重复点
static void FilterEqualPoints(AcGePoint2dArray &points, double tol = 1.0E-7)
{
AcGeTol geTol;
geTol.setEqualPoint(tol);
for (int i = points.length() - 1; i > 0; i--)
{
for (int j = 0; j < i; j++)
{
if (points[i].isEqualTo(points[j],geTol))
{
points.removeAt(i);
break;
}
}
}
}
// 从数组中过滤掉某个点
static void FilterEqualPoints(AcGePoint2dArray &points, const AcGePoint2d &pt, double tol = 1.0E-7)
{
AcGeTol geTol;
geTol.setEqualPoint(tol);
for (int i = points.length() - 1; i >= 0; i--)
if (points[i].isEqualTo(pt, geTol))
points.removeAt(i);
}
//-----------------------------------------------------------------------------+
//=Description: 判断点是否在多段线内部
//=Return: 0多段线外,1多段线内,2多段线上
//=Parameter: pPoly[in]
//=Parameter: ptPickWcs[in]
//=Parameter: tol[in]
//-----------------------------------------------------------------------------+
int IsPointInPoly(AcDbPolyline *pPoly, const AcGePoint3d &ptPickWcs, double tol = 1.0E-7)
{
if(!pPoly || !pPoly->isClosed())
return POINT_POLY_OUTSIDE;
AcGeTol geTol;
geTol.setEqualPoint(tol);
//转换坐标,将点转为ECS坐标系
AcGePoint3d ptPick;
acdbWcs2Ecs(asDblArray(ptPickWcs), asDblArray(ptPick), asDblArray(pPoly->normal()), false);
//判断点和多段线平面是否共面
double dElevation = pPoly->elevation();
if (fabs(dElevation - ptPick.z) > tol)
return POINT_POLY_OUTSIDE;
//如果点到多段线的最近点和给定的点重合,表示点在多段线上
AcGePoint3d ptClosestWcs;
pPoly->getClosestPointTo(ptPickWcs, ptClosestWcs);
if (ptPickWcs.isEqualTo(ptClosestWcs, geTol))
return POINT_POLY_ONEDGE;
//转换最近点为ECS坐标系下
AcGePoint3d ptClosest;
acdbWcs2Ecs(asDblArray(ptClosestWcs), asDblArray(ptClosest), asDblArray(pPoly->normal()), false);
// 第一个射线的方向是从最近点到当前点,起点是当前点
// 射线的起点是pt,方向为从最近点到pt,如果反向做判断,则最近点距离pt太近的时候,
// 最近点也会被作为一个交点(这个交点不太容易被排除掉)
AcGeVector2d vtRay((ptPick - ptClosest).x, (ptPick - ptClosest).y);
AcGeRay2d geRay(AcGePoint2d(ptPick.x, ptPick.y), vtRay);
// 判断点和多段线的位置关系
while (true)
{
bool bContinue = false;
AcGePoint2dArray arptIntersect;
IntersectWithGeRay(pPoly, geRay, arptIntersect, 1.0E-4);
FilterEqualPoints(arptIntersect, 1.0E-4);// IntersectWith函数经常会得到很近的交点,这些点必须进行过滤
if (arptIntersect.length() == 0)
return POINT_POLY_OUTSIDE;// 没有交点,表示点在多段线的外部
else
{
//特殊情况1:过滤掉由于射线被反向延长带来的影响,当pt距离最近点比较近的时候,
//最近点竟然被当作一个交点,所以,首先删除最近点(如果有的话)
FilterEqualPoints(arptIntersect, AcGePoint2d(ptClosest.x, ptClosest.y));
//特殊情况2:如果某个交点与最近点在给定点的同一方向,要去掉这个点
//,这个点明显不是交点,还是由于intersectwith函数的Bug
for (int i = arptIntersect.length() - 1; i >= 0; i--)
{
if ((arptIntersect[i].x - ptPick.x) * (ptClosest.x - ptPick.x) >= 0 &&
(arptIntersect[i].y - ptPick.y) * (ptClosest.y - ptPick.y) >= 0)
arptIntersect.removeAt(i);
}
for (i = 0; i < arptIntersect.length(); i++)
{
if (PointIsPolyVert(pPoly, arptIntersect[i], 1.0E-4)) // 只要有交点是多段线的顶点就重新进行判断
{
// 处理给定点很靠近多段线顶点的情况(如果与顶点距离很近,就认为这个点在多段线上,因为这种情况没有什么好的判断方法)
if (PointIsPolyVert(pPoly, AcGePoint2d(ptPick.x, ptPick.y), 1.0E-4))
return POINT_POLY_ONEDGE;
// 将射线旋转一个极小的角度(2度)再次判断(假定这样不会再通过上次判断到的顶点)
vtRay = vtRay.rotateBy(0.035);
geRay.set(AcGePoint2d(ptPick.x, ptPick.y), vtRay);
bContinue = true;
break;
}
}
if (!bContinue)
{
if (0 == arptIntersect.length() % 2)
return POINT_POLY_OUTSIDE;
else
return POINT_POLY_INSIDE;
}
}
}
}
3.2 测试代码
void CmdPtInPoly()
{
struct resbuf* rb = NULL;
rb = acutBuildList(RTDXF0, _T("LWPOLYLINE"), RTNONE);
TCHAR* prompts[2] = { _T("\n请选择了一个实体:"),_T("\n取消了一个实体") };
ads_name ssPick;
if (RTNORM == acedSSGet(_T(":S:$-M"), prompts, NULL, rb, ssPick))
{
ads_name ent;
if (RTNORM == acedSSName(ssPick, 0, ent))
{
AcDbObjectId id;
acdbGetObjectId(id, ent);
AcDbPolyline* pPoly;
if (Acad::eOk == acdbOpenObject(pPoly, id, AcDb::kForRead))
{
ads_point ptRet;
while (RTNORM == acedGetPoint(NULL, _T("\n请任意点选一点:"), ptRet))
{
//判断点是否在多段线以内
int iRelation = IsPointInPoly(pPoly, Ucs2Wcs(ptRet));
if (POINT_POLY_INSIDE == iRelation)
acutPrintf(_T("\n\t点在多段线内"));
else if (POINT_POLY_ONEDGE == iRelation)
acutPrintf(_T("\n\t点在多段线上"));
else if (POINT_POLY_OUTSIDE == iRelation)
acutPrintf(_T("\n\t点在多段线外"));
}
pPoly->close();
}
}
acedSSFree(ssPick);
}
acutRelRb(rb);
}