一、VTK的框选支持类vtkInteractorStyleRubberBandPick
FastCAE的鼠标事件交互类是PropPickerInteractionStyle,它扩展自vtkInteractorStyleRubberBandPick。vtkInteractorStyleRubberBandPick类可以实现鼠标框选物体,默认情况下按下键盘r键开启框选模式,这时拖动鼠标可拾取物体。VTK官网有其例子:HighlightSelection。
二、FastCAE框选产品设计
我们看FastCAE的鼠标拾取产品设计。其只支持框选网格点与网格单元,几何点、线、面都不支持。框选网格单元效果如下:
在VTK的给的案例中,按r键是为了打开vtkInteractorStyleRubberBandPick类的框选开关,设置vtkInteractorStyleRubberBandPick字段CurrentMode=1(VTKISRBP_SELECT),表示开启框选模式。VTK中vtkInteractorStyleRubberBandPick.cxx源码如下:
void vtkInteractorStyleRubberBandPick::OnChar()
{
switch (this->Interactor->GetKeyCode())
{
case 'r':
case 'R':
// r toggles the rubber band selection mode for mouse button 1
if (this->CurrentMode == VTKISRBP_ORIENT)
{
this->CurrentMode = VTKISRBP_SELECT;
}
else
{
this->CurrentMode = VTKISRBP_ORIENT;
}
break;
case 'p':
case 'P':
{
vtkRenderWindowInteractor* rwi = this->Interactor;
int* eventPos = rwi->GetEventPosition();
this->FindPokedRenderer(eventPos[0], eventPos[1]);
this->StartPosition[0] = eventPos[0];
this->StartPosition[1] = eventPos[1];
this->EndPosition[0] = eventPos[0];
this->EndPosition[1] = eventPos[1];
this->Pick();
break;
}
default:
this->Superclass::OnChar();
}
}
而在FastCAE框选时,不需要按下r键,其原因在用切换拾取模式时,直接改掉了CurrentMode的值,源码如下(注意看这个函数最后一行):
void PropPickerInteractionStyle::setSelectModel(int m)
{
_selectModel = (SelectModel)m;
this->CurrentMode = 0;
if (_actor != nullptr)
_actor->GetProperty()->DeepCopy(_property);
_actor = nullptr;
_preGeoSeltctActor = nullptr;
_selectItems.clear();
emit grabKeyBoard(false);
switch (_selectModel)
{
case ModuleBase::MeshNode:
case ModuleBase::MeshCell:
case ModuleBase::GeometryWinPoint:
case ModuleBase::GeometryWinCurve:
case ModuleBase::GeometryWinSurface:
case ModuleBase::GeometryWinBody:
emit grabKeyBoard(true);
break;
case ModuleBase::GeometryPoint:
case ModuleBase::GeometryCurve:
case ModuleBase::GeometrySurface:
case ModuleBase::GeometryBody:
break;
case ModuleBase::BoxMeshNode: // 当选择方式是框选时,直接设置开始框选
case ModuleBase::BoxMeshCell:
case ModuleBase::DrawSketch:
this->CurrentMode = 1;
break;
}
}
这种设计的好处是不用按键盘进行交互,但造成激活框选按钮之后,视图的角度无法更改。本来按住左键拖拽可以旋转视图的,打开框选之后就失效了。
其框选还有一个比较严重的问题:框选会同时拾取物体的表面与背面单元,效果如下:
这种效果惊不惊喜,意不意外!?很多场景下,这种拾取是不满足要求的。进一步分析其框选逻辑就很好理解这种现象了。
三、框选计算逻辑
PropPickerInteractionStyle::OnLeftButtonUp()处理鼠标抬起事件,框选计算哪些物体要被选中的逻辑也在这里被触发。
void PropPickerInteractionStyle::OnLeftButtonUp()
{
vtkInteractorStyleRubberBandPick::OnLeftButtonUp();
if (_selectModel == None && !_mouseMoved)
emit this->clearAllHighLight();
if ((_selectModel != BoxMeshCell) && (_selectModel != BoxMeshNode) && (_selectModel != DrawSketch))
return;
if (this->CurrentMode == 0)
return;
// _selectItemIDs->SetNumberOfValues(0);
_selectItems.clear();
int *endPos = this->GetInteractor()->GetEventPosition();
_endPos[0] = endPos[0];
_endPos[1] = endPos[1];
// qDebug() << "end " << _endPos[0] << " " << _endPos[1];
if (_selectModel != DrawSketch)
{
vtkActor *ac = nullptr;
vtkAreaPicker *areaPicker = dynamic_cast<vtkAreaPicker *>(this->GetInteractor()->GetPicker());
ac = areaPicker->GetActor();
if (ac == nullptr)
return;
}
switch (_selectModel)
{
case ModuleBase::BoxMeshNode: // 计算哪些节点被选中
boxSelectMeshNode();
break;
case ModuleBase::BoxMeshCell: // 计算哪些单元要被选中
boxSelectMeshCell();
break;
case ModuleBase::DrawSketch:
_coordinate->SetCoordinateSystemToDisplay();
_coordinate->SetValue(endPos[0], endPos[1], 0);
double *d = _coordinate->GetComputedWorldValue(_renderer);
emit mouseReleasePoint(d);
break;
}
_mouseMoved = false;
_leftButtonDown = false;
}
boxSelectMeshNode()、boxSelectMeshCell()函数分别哪些节点、单元要被拾取。
void PropPickerInteractionStyle::boxSelectMeshNode()
{
emit clearAllHighLight(); // 清除掉当前高亮
_selectItems.clear(); // 清理当前选择项
// Forward events
int range[4];
this->getBoxRange(range); // 获取框选矩形的坐标
vtkActorCollection *actors = _renderer->GetActors(); // 获取当前的场景中所有actor
actors->InitTraversal();
const int nac = actors->GetNumberOfItems();
for (int i = 0; i < nac; ++i) // 对Actor进行遍历
{
vtkActor *actor = actors->GetNextActor();
if (actor == nullptr)
if (!actor->GetVisibility())
continue;
if (!actor->GetPickable())
continue;
vtkMapper *mapper = actor->GetMapper();
if (mapper == nullptr)
continue;
vtkDataSet *dataset = mapper->GetInputAsDataSet();
if (dataset == nullptr)
continue;
vtkDataArray *IDS = dataset->GetPointData()->GetArray("IDS"); // 提取Actor的点数据
if (IDS == nullptr)
continue;
this->selectMesh(dataset, range);
}
emit highLight(&_selectItems);
}
void PropPickerInteractionStyle::selectMesh(vtkDataSet *dataSet, int *range)
{
vtkRenderer *render = this->GetInteractor()->GetRenderWindow()->GetRenderers()->GetFirstRenderer();
vtkSmartPointer<vtkCoordinate> coordinate = vtkSmartPointer<vtkCoordinate>::New();
coordinate->SetCoordinateSystemToWorld();
coordinate->GetComputedDisplayValue(render);
if (_selectModel == BoxMeshNode)
{
vtkDataArray *ids = dataSet->GetPointData()->GetArray("IDS"); // 获取点集
const int npoint = dataSet->GetNumberOfPoints();
for (int i = 0; i < npoint; ++i)
{
double coor[3];
dataSet->GetPoint(i, coor); // 获取点的坐标
coordinate->SetValue(coor); // 将点的坐标设置给coordinate
int *va = coordinate->GetComputedDisplayValue(render); // 计算屏幕坐标
if (isPointInRange(va, range)) // 是否在鼠标框内部
{
double *k_id = ids->GetTuple2(i); // 看不懂?
_selectItems.insert(k_id[0], k_id[1]);
}
}
}
else if (_selectModel == BoxMeshCell)
{
vtkDataArray *ids = dataSet->GetCellData()->GetArray("IDS"); // 获取cell数据
const int ncell = dataSet->GetNumberOfCells();
for (int i = 0; i < ncell; ++i) // 遍历cell
{
vtkCell *cell = dataSet->GetCell(i); // 当前的cell
double pcenter[3] = {0};
cell->GetParametricCenter(pcenter); // 获取当前cell的中心点参数坐标
int subid;
double coor[3];
double w[100];
cell->EvaluateLocation(subid, pcenter, coor, w); // 根据参数坐标获取中心点世界空间坐标
coordinate->SetValue(coor);
int *va = coordinate->GetComputedDisplayValue(render); // 计算屏幕坐标
if (isPointInRange(va, range)) // 屏幕坐标是否在选择框内
{
double *k_id = ids->GetTuple2(i);
_selectItems.insert(k_id[0], k_id[1]);
}
}
}
}
根据以上代码,框选点时,直接根据点坐标计算其投影到屏幕上的坐标,判断是否在选择框内。单元是判断中心点是否在选择框内部。因为投影之后,丢弃了深度方向的信息,没有考虑物体的遮挡信息,所以框选时表面、背面均可选择。而且其计算框选时遍历所有网格,没有借助一些加速结构,如BVH树等,造成框选效率较低,当网格数量较多时,这种方式很慢。
总结:
FastCAE的框选逻辑过于简单,只是demo阶段,实际的CAE软件的拾取逻辑要远比这复杂。