vtk拖拽小球互動實現
阿新 • • 發佈:2020-10-12
- 功能描述:
1)在vmtk生成的中心線上通過滑鼠懸浮選中一條中心線;
2)在選中的中心線上,通過LMB單擊生成近端遠端兩個點的球;
3)兩個生成的球可以通過滑鼠中鍵滾輪的滾動調節它們在中心線上的位置。
1.標頭檔案
#ifndef MOUSEINTERACTORSTYLE_H #define MOUSEINTERACTORSTYLE_H #include <vector> #include <QObject> #include <vtkActor.h> #include <vtkCellPicker.h> #include <vtkInteractorStyleTrackballCamera.h> #include <vtkPolyData.h> #include <vtkPolyDataMapper.h> #include <vtkProperty.h> #include <vtkRenderWindow.h> #include <vtkRenderWindowInteractor.h> #include <vtkRenderer.h> #include <vtkSmartPointer.h> #include <vtkSphereSource.h> #include <vtkPoints.h> #include <vtkPolyLine.h> #include <vtkUnstructuredGrid.h> #include <vtkDataSetSurfaceFilter.h> #include <vtkKdTree.h> #include <vtkPointPicker.h> #include <vtkRendererCollection.h> class MouseInteractorStyle : public QObject, public vtkInteractorStyleTrackballCamera { Q_OBJECT public: static MouseInteractorStyle *New(); vtkTypeMacro(MouseInteractorStyle, vtkInteractorStyleTrackballCamera); MouseInteractorStyle(); virtual ~MouseInteractorStyle() override; void SetSphereAttributes(std::vector<double> sp1color, std::vector<double> sp2color, double sp1size, double sp2size); std::vector<int> GetPointIds(); vtkIdType GetLineId(); void SetIdType(bool bReverse); void RemoveAllActors(); virtual void OnMouseMove() override; virtual void OnLeftButtonDown() override; virtual void OnMouseWheelForward() override; virtual void OnMouseWheelBackward() override; public slots: void SlotSetPointVecIn(const std::vector<vtkIdType> &pointVec); signals: void SignalPointsIndexOut(const QString &d_id, const QString &p_id); private: void DrawPath(); int BinaryFindLastLessNum(std::vector<vtkIdType> &nums, int n); std::vector<int> pointIds_; std::vector<vtkIdType> pathList_; double *worldPosition_; vtkSmartPointer<vtkPolyData> polydata_; vtkSmartPointer<vtkPolyData> finalpolydata_; vtkSmartPointer<vtkActor> actor_; vtkSmartPointer<vtkPolyDataMapper> mapper_d_; vtkSmartPointer<vtkPolyDataMapper> mapper_p_; vtkSmartPointer<vtkActor> actor_d_; vtkSmartPointer<vtkActor> actor_p_; vtkSmartPointer<vtkPolyData> sphere_d_; vtkSmartPointer<vtkPolyData> sphere_p_; vtkSmartPointer<vtkActor> lastMoveActor_d_; vtkSmartPointer<vtkActor> lastMoveActor_p_; vtkIdType lineId_; bool bEditLines_; bool bEidtPoints_; bool bAddOnce_; vtkIdType point_d_index_; vtkIdType point_p_index_; vtkIdType upper_limit_index_; vtkIdType lower_limit_index_; vtkIdType cur_line_point_nums_; bool bRemoveFirst_; std::vector<vtkIdType> pointsIdxVec_; std::vector<double> sp1color_; std::vector<double> sp2color_; double sp1size_; double sp2size_; bool bReverse_; }; #endif // MOUSEINTERACTORSTYLE_H
2.原始檔
#include "mouseinteractorstyle.h" vtkStandardNewMacro(MouseInteractorStyle); const int step = 1; MouseInteractorStyle::MouseInteractorStyle() { bEditLines_ = true; bEidtPoints_ = true; bAddOnce_ = false; bRemoveFirst_ = true; actor_ = vtkSmartPointer<vtkActor>::New(); polydata_ = vtkSmartPointer<vtkPolyData>::New(); finalpolydata_ = vtkSmartPointer<vtkPolyData>::New(); actor_d_ = vtkSmartPointer<vtkActor>::New(); actor_p_ = vtkSmartPointer<vtkActor>::New(); mapper_d_ = vtkSmartPointer<vtkPolyDataMapper>::New(); mapper_p_ = vtkSmartPointer<vtkPolyDataMapper>::New(); sphere_d_ = vtkSmartPointer<vtkPolyData>::New(); sphere_p_ = vtkSmartPointer<vtkPolyData>::New(); lastMoveActor_d_ = vtkSmartPointer<vtkActor>::New(); lastMoveActor_p_ = vtkSmartPointer<vtkActor>::New(); lineId_ = 0; point_d_index_ = 0; point_p_index_ = 0; cur_line_point_nums_ = 0; upper_limit_index_ = VTK_INT_MIN; lower_limit_index_ = VTK_INT_MAX; sp1color_ = {1.0, 0.0, 0.0}; sp2color_ = {0.0, 1.0, 0.0}; sp1size_ = 0.3; sp2size_ = 0.3; bReverse_ = true; } MouseInteractorStyle::~MouseInteractorStyle() { } void MouseInteractorStyle::SetSphereAttributes( std::vector<double> sp1color, std::vector<double> sp2color, double sp1size, double sp2size) { sp1color_ = sp1color; sp2color_ = sp2color; sp1size_ = sp1size; sp2size_ = sp2size; } std::vector<int> MouseInteractorStyle::GetPointIds() { return pointIds_; } vtkIdType MouseInteractorStyle::GetLineId() { return lineId_; } void MouseInteractorStyle::SetIdType(bool bReverse) { bReverse_ = bReverse; } void MouseInteractorStyle::RemoveAllActors() { // remove line this->GetDefaultRenderer()->RemoveActor(actor_); // remove spheres this->Interactor->GetRenderWindow() ->GetRenderers()->GetFirstRenderer()->RemoveActor(actor_d_); this->Interactor->GetRenderWindow() ->GetRenderers()->GetFirstRenderer()->RemoveActor(actor_p_); this->GetDefaultRenderer()->GetRenderWindow()->Render(); } void MouseInteractorStyle::OnMouseMove() { // Get the location of the click (in window coordinates) int *pos = this->GetInteractor()->GetEventPosition(); //std::cout << "Picking pixel: " << pos[0] << " " << pos[1] << std::endl; vtkSmartPointer<vtkCellPicker> picker = vtkSmartPointer<vtkCellPicker>::New(); picker->SetTolerance(0.0005); // Pick from this location. picker->Pick(pos[0], pos[1], 0, this->GetDefaultRenderer()); //std::cout << "Cell id is: " << picker->GetCellId() << std::endl; if (picker->GetCellId() != -1 && bEditLines_) { lineId_ = picker->GetCellId(); //std::cout << "lineId: " << lineId_ << std::endl; //picker->GetActor()->GetProperty()->SetColor(1, 0, 0); vtkSmartPointer<vtkPoints> points = vtkSmartPointer<vtkPoints>::New(); points->DeepCopy(picker->GetActor()->GetMapper()->GetInput()-> GetCell(lineId_)->GetPoints()); polydata_->DeepCopy(picker->GetActor()->GetMapper()->GetInput()); vtkSmartPointer<vtkPolyLine> polyLine = vtkSmartPointer<vtkPolyLine>::New(); polyLine->GetPointIds()->SetNumberOfIds(points->GetNumberOfPoints()); for(int i = 0; i < points->GetNumberOfPoints(); i++) { polyLine->GetPointIds()->SetId(i, i); } vtkSmartPointer<vtkUnstructuredGrid> unstructuredGrid = vtkSmartPointer<vtkUnstructuredGrid>::New(); unstructuredGrid->Allocate(1, 1); unstructuredGrid->InsertNextCell(polyLine->GetCellType(), polyLine->GetPointIds()); unstructuredGrid->SetPoints(points); vtkSmartPointer<vtkDataSetSurfaceFilter> surfaceFilter = vtkSmartPointer<vtkDataSetSurfaceFilter>::New(); surfaceFilter->SetInputData(unstructuredGrid); surfaceFilter->Update(); vtkSmartPointer<vtkPolyData> polydata = surfaceFilter->GetOutput(); finalpolydata_ = polydata; vtkSmartPointer<vtkPolyDataMapper> mapper = vtkSmartPointer<vtkPolyDataMapper>::New(); mapper->SetInputData(polydata); mapper->Update(); actor_->SetMapper(mapper); actor_->GetProperty()->SetLineWidth(8.0); this->GetDefaultRenderer()->AddActor(actor_); this->GetDefaultRenderer()->GetRenderWindow()->Render(); } else { //this->GetDefaultRenderer()->RemoveActor(actor_); //this->GetDefaultRenderer()->GetRenderWindow()->Render(); } // Forward events vtkInteractorStyleTrackballCamera::OnMouseMove(); } void MouseInteractorStyle::OnLeftButtonDown() { // Forward events vtkInteractorStyleTrackballCamera::OnLeftButtonDown(); if(this->Interactor->GetControlKey()) { std::cout << "Control held." << std::endl; // Get the location of the click (in window coordinates) int *pos = this->GetInteractor()->GetEventPosition(); //std::cout << "Picking pixel: " << pos[0] << " " << pos[1] << std::endl; auto picker = vtkSmartPointer<vtkPointPicker>::New(); // Pick from this location. picker->Pick(pos[0], pos[1], 0, this->Interactor->GetRenderWindow() ->GetRenderers()->GetFirstRenderer()); worldPosition_ = picker->GetPickPosition(); //std::cout << "Cell id is: " << picker->GetCellId() << std::endl; if (finalpolydata_->GetNumberOfPoints() == 0) { return; } // Create the tree auto pointTree2 = vtkSmartPointer<vtkKdTree>::New(); pointTree2->BuildLocatorFromPoints(finalpolydata_->GetPoints()); //Find the closest point to TargetPoint double radius = 1.5; double closestPointDist2; vtkIdType id = pointTree2->FindClosestPointWithinRadius( radius, worldPosition_, closestPointDist2); id = finalpolydata_->GetNumberOfPoints() - id; std::cout << id << std::endl; std::cout << "The dist2 is: " << closestPointDist2 << std::endl; int frontPointsSum = 0; for (int i = 0; i < lineId_; i++) { frontPointsSum += polydata_->GetCell(i)->GetNumberOfPoints(); } cur_line_point_nums_ = polydata_->GetCell(lineId_)->GetNumberOfPoints(); upper_limit_index_ = frontPointsSum + cur_line_point_nums_; lower_limit_index_ = frontPointsSum; if (id <= polydata_->GetCell(lineId_)->GetNumberOfPoints() && bEidtPoints_) { std::cout << "Pick position is: " << worldPosition_[0] << " " << worldPosition_[1] << " " << worldPosition_[2] << std::endl; pointIds_.push_back(static_cast<int>( finalpolydata_->GetNumberOfPoints() - id)); pathList_.push_back(id + frontPointsSum); auto mapper_d = vtkSmartPointer<vtkPolyDataMapper>::New(); auto mapper_p = vtkSmartPointer<vtkPolyDataMapper>::New(); auto actor_d = vtkSmartPointer<vtkActor>::New(); auto actor_p = vtkSmartPointer<vtkActor>::New(); if (!bAddOnce_) { auto sphereSource_d = vtkSmartPointer<vtkSphereSource>::New(); sphereSource_d->SetRadius(sp1size_); sphereSource_d->Update(); sphere_d_ = sphereSource_d->GetOutput(); mapper_d_ = mapper_d; mapper_d_->SetInputData(sphere_d_); actor_d_ = actor_d; actor_d_->SetMapper(mapper_d_); actor_d_->GetProperty()->SetColor(sp1color_[0], sp1color_[1], sp1color_[2]); actor_d_->PickableOff(); if (!bReverse_) { id = finalpolydata_->GetNumberOfPoints() - id; } actor_d_->SetPosition(polydata_->GetPoint(id + frontPointsSum)); this->Interactor->GetRenderWindow() ->GetRenderers()->GetFirstRenderer()->AddActor(actor_d_); if (!bReverse_) { id = finalpolydata_->GetNumberOfPoints() - id; } point_d_index_ = finalpolydata_->GetNumberOfPoints() - id + frontPointsSum; bAddOnce_ = true; } else { auto sphereSource_p = vtkSmartPointer<vtkSphereSource>::New(); sphereSource_p->SetRadius(sp2size_); sphereSource_p->Update(); sphere_p_ = sphereSource_p->GetOutput(); mapper_p_ = mapper_p; mapper_p_->SetInputData(sphere_p_); actor_p_ = actor_p; actor_p_->SetMapper(mapper_p_); actor_p_->GetProperty()->SetColor(sp2color_[0], sp2color_[1], sp2color_[2]); actor_p_->PickableOff(); if (!bReverse_) { id = finalpolydata_->GetNumberOfPoints() - id; } actor_p_->SetPosition(polydata_->GetPoint(id + frontPointsSum)); this->Interactor->GetRenderWindow() ->GetRenderers()->GetFirstRenderer()->AddActor(actor_p_); if (!bReverse_) { id = finalpolydata_->GetNumberOfPoints() - id; } point_p_index_ = finalpolydata_->GetNumberOfPoints() - id + frontPointsSum; } bEditLines_ = false; // Find the shortest path if (pathList_.size() == 2) { //this->DrawPath(); actor_d_->PickableOn(); actor_p_->PickableOn(); bEidtPoints_ = false; } } } } void MouseInteractorStyle::OnMouseWheelForward() { // std::cout << "OnMouseWheelForward..." << std::endl; //std::cout << "Vec.size: " << pointsIdxVec_.size() << std::endl; if (pointsIdxVec_.empty()) { return; } if(point_d_index_ >= upper_limit_index_ - 1 || point_d_index_ - lower_limit_index_ + step > cur_line_point_nums_) { return; } auto moveSphereSource_d = vtkSmartPointer<vtkSphereSource>::New(); moveSphereSource_d->SetRadius(sp1size_); moveSphereSource_d->Update(); auto moveSphereSource_p = vtkSmartPointer<vtkSphereSource>::New(); moveSphereSource_p->SetRadius(sp2size_); moveSphereSource_p->Update(); auto moveMapper_d = vtkSmartPointer<vtkPolyDataMapper>::New(); moveMapper_d->SetInputConnection(moveSphereSource_d->GetOutputPort()); auto moveMapper_p = vtkSmartPointer<vtkPolyDataMapper>::New(); moveMapper_p->SetInputConnection(moveSphereSource_p->GetOutputPort()); auto moveActor_d = vtkSmartPointer<vtkActor>::New(); moveActor_d->GetProperty()->SetColor(sp1color_[0], sp1color_[1], sp1color_[2]); moveActor_d->SetMapper(moveMapper_d); auto moveActor_p = vtkSmartPointer<vtkActor>::New(); moveActor_p->GetProperty()->SetColor(sp2color_[0], sp2color_[1], sp2color_[2]); moveActor_p->SetMapper(moveMapper_p); auto real_d_index = point_d_index_ + step; auto value_d = std::lower_bound(pointsIdxVec_.begin(), pointsIdxVec_.end(), real_d_index - lower_limit_index_); if (value_d != pointsIdxVec_.end()) { //std::cout << *value_d << std::endl; real_d_index = *value_d + lower_limit_index_; } else { std::cout << "Remote point not found." << std::endl; return; } auto real_p_index = point_p_index_ + step; auto value_p = std::lower_bound(pointsIdxVec_.begin(), pointsIdxVec_.end(), real_p_index - lower_limit_index_); if (value_p != pointsIdxVec_.end()) { //std::cout << *value_p << std::endl; real_p_index = *value_p + lower_limit_index_; } else { std::cout << "Near end not found." << std::endl; return; } // Remove the previous ball if (bRemoveFirst_) { this->Interactor->GetRenderWindow() ->GetRenderers()->GetFirstRenderer()->RemoveActor(actor_d_); this->Interactor->GetRenderWindow() ->GetRenderers()->GetFirstRenderer()->RemoveActor(actor_p_); bRemoveFirst_ = false; } else { this->Interactor->GetRenderWindow() ->GetRenderers()->GetFirstRenderer()->RemoveActor(lastMoveActor_d_); this->Interactor->GetRenderWindow() ->GetRenderers()->GetFirstRenderer()->RemoveActor(lastMoveActor_p_); } // Add the new ball if (!bReverse_) { moveActor_d->SetPosition(polydata_->GetPoint(real_d_index)); } else { moveActor_d->SetPosition(polydata_->GetPoint(cur_line_point_nums_ - (*value_d) + lower_limit_index_)); } this->GetDefaultRenderer()->AddActor(moveActor_d); if (!bReverse_) { moveActor_p->SetPosition(polydata_->GetPoint(real_p_index)); } else { moveActor_p->SetPosition(polydata_->GetPoint(cur_line_point_nums_ - (*value_p) + lower_limit_index_)); } this->GetDefaultRenderer()->AddActor(moveActor_p); point_d_index_ = real_d_index; point_p_index_ = real_p_index; lastMoveActor_d_ = moveActor_d; lastMoveActor_p_ = moveActor_p; QString d_id = QString::number(point_d_index_ - lower_limit_index_); QString p_id = QString::number(point_p_index_ - lower_limit_index_); emit SignalPointsIndexOut(d_id, p_id); this->Interactor->GetRenderWindow()->Render(); // Forward events //vtkInteractorStyleTrackballCamera::OnMouseWheelForward(); } void MouseInteractorStyle::OnMouseWheelBackward() { //std::cout << "OnMouseWheelBackward..." << std::endl; //std::cout << "Vec.size: " << pointsIdxVec_.size() << std::endl; if (pointsIdxVec_.empty()) { return; } if(point_p_index_ <= lower_limit_index_ || point_p_index_ - lower_limit_index_ - step < 0) { return; } auto moveSphereSource_d = vtkSmartPointer<vtkSphereSource>::New(); moveSphereSource_d->SetRadius(sp1size_); moveSphereSource_d->Update(); auto moveSphereSource_p = vtkSmartPointer<vtkSphereSource>::New(); moveSphereSource_p->SetRadius(sp2size_); moveSphereSource_p->Update(); auto moveMapper_d = vtkSmartPointer<vtkPolyDataMapper>::New(); moveMapper_d->SetInputConnection(moveSphereSource_d->GetOutputPort()); auto moveMapper_p = vtkSmartPointer<vtkPolyDataMapper>::New(); moveMapper_p->SetInputConnection(moveSphereSource_p->GetOutputPort()); auto moveActor_d = vtkSmartPointer<vtkActor>::New(); moveActor_d->GetProperty()->SetColor(sp1color_[0], sp1color_[1], sp1color_[2]); moveActor_d->SetMapper(moveMapper_d); auto moveActor_p = vtkSmartPointer<vtkActor>::New(); moveActor_p->GetProperty()->SetColor(sp2color_[0], sp2color_[1], sp2color_[2]); moveActor_p->SetMapper(moveMapper_p); auto real_d_index = point_d_index_ - step; auto value_d = BinaryFindLastLessNum(pointsIdxVec_, static_cast<int>(real_d_index - lower_limit_index_)); if (value_d) { //std::cout << pointsIdxVec_[static_cast<unsigned int>(value_d)] << std::endl; real_d_index = pointsIdxVec_[static_cast<unsigned int>(value_d)] + lower_limit_index_; } else { std::cout << "Remote point not found." << std::endl; return; } auto real_p_index = point_p_index_ - step; auto value_p = BinaryFindLastLessNum(pointsIdxVec_, static_cast<int>(real_p_index - lower_limit_index_)); if (value_p) { //std::cout << pointsIdxVec_[static_cast<unsigned int>(value_p)] << std::endl; real_p_index = pointsIdxVec_[static_cast<unsigned int>(value_p)] + lower_limit_index_; } else { std::cout << "Near end not found." << std::endl; return; } // Remove the previous ball if (bRemoveFirst_) { this->Interactor->GetRenderWindow() ->GetRenderers()->GetFirstRenderer()->RemoveActor(actor_d_); this->Interactor->GetRenderWindow() ->GetRenderers()->GetFirstRenderer()->RemoveActor(actor_p_); bRemoveFirst_ = false; } else { this->Interactor->GetRenderWindow() ->GetRenderers()->GetFirstRenderer()->RemoveActor(lastMoveActor_d_); this->Interactor->GetRenderWindow() ->GetRenderers()->GetFirstRenderer()->RemoveActor(lastMoveActor_p_); } // Add the new ball if (!bReverse_) { moveActor_d->SetPosition(polydata_->GetPoint(real_d_index)); } else { moveActor_d->SetPosition(polydata_->GetPoint(cur_line_point_nums_ - pointsIdxVec_[static_cast<unsigned int>(value_d)] + lower_limit_index_)); } this->GetDefaultRenderer()->AddActor(moveActor_d); if (!bReverse_) { moveActor_p->SetPosition(polydata_->GetPoint(real_p_index)); } else { moveActor_p->SetPosition(polydata_->GetPoint(cur_line_point_nums_ - pointsIdxVec_[static_cast<unsigned int>(value_p)] + lower_limit_index_)); } this->GetDefaultRenderer()->AddActor(moveActor_p); point_d_index_ = real_d_index; point_p_index_ = real_p_index; lastMoveActor_d_ = moveActor_d; lastMoveActor_p_ = moveActor_p; QString d_id = QString::number(point_d_index_ - lower_limit_index_); QString p_id = QString::number(point_p_index_ - lower_limit_index_); emit SignalPointsIndexOut(d_id, p_id); this->Interactor->GetRenderWindow()->Render(); // Forward events //vtkInteractorStyleTrackballCamera::OnMouseWheelBackward(); } void MouseInteractorStyle::SlotSetPointVecIn( const std::vector<vtkIdType> &pointVec) { auto iter = pointVec.begin(); for (; iter != pointVec.end(); ++iter) { pointsIdxVec_.push_back(*iter); } } void MouseInteractorStyle::DrawPath() { vtkIdType minId, maxId; if (pathList_[1] - pathList_[0] < 0) { minId = pathList_[1]; maxId = pathList_[0]; } else { minId = pathList_[0]; maxId = pathList_[1]; } vtkIdType size = maxId - minId + 1; //std::cout << "size: " << size << std::endl; // Draw path vtkIdType *line_id; vtkSmartPointer<vtkCellArray> lines = vtkSmartPointer<vtkCellArray>::New(); line_id = new vtkIdType[static_cast<unsigned long>(size)]; vtkNew<vtkPolyData> curPolydata; vtkNew<vtkPoints> curPoints; for (vtkIdType i = 0; i < size; i++) { curPoints->InsertNextPoint(polydata_->GetPoint(minId + i)); line_id[i] = i; } line_id[size] = size - 1; lines->InsertNextCell(size + 1, line_id); curPolydata->SetPoints(curPoints); curPolydata->SetLines(lines); vtkNew<vtkPolyDataMapper> curMapper; curMapper->SetInputData(curPolydata); std::cout << "drawing path..." << endl; // Create an actor to represent the polyline vtkSmartPointer<vtkActor> pathActor = vtkSmartPointer<vtkActor>::New(); pathActor->SetMapper(curMapper); pathActor->GetProperty()->SetColor(0, 0, 1); pathActor->GetProperty()->SetLineWidth(10); this->GetDefaultRenderer()->AddActor(pathActor); } int MouseInteractorStyle::BinaryFindLastLessNum(std::vector<vtkIdType> &nums, int n) { int left = 0; int right = static_cast<int>(nums.size() - 1); while (left < right) { int middle = (left + right) / 2; if (nums[static_cast<unsigned int>(middle)] >= n) { right = middle - 1; } else if (nums[static_cast<unsigned int>(middle)] < n) { if (left == middle) { if (nums[static_cast<unsigned int>(right)] <= n) { left = right; } break; } else { left = middle; } } } if(nums[static_cast<unsigned int>(left)] <= n) { return left; } else { return -1; } }
3.呼叫示例
vtkSmartPointer<MouseInteractorStyle> style = vtkSmartPointer<MouseInteractorStyle>::New(); style->SetDefaultRenderer(this->_renderer); if (bExtract_) { style->SetIdType(false); } this->style_ = style; this->_renderer->GetRenderWindow() ->GetInteractor()->SetInteractorStyle(style); connect(this->style_, SIGNAL(SignalPointsIndexOut(const QString &, const QString &)), this, SLOT(SlotGetPointsIndexIn(const QString &, const QString &))); connect(this, SIGNAL(SignalGetPointVecOut(const std::vector<vtkIdType> &)), this->style_, SLOT(SlotSetPointVecIn(const std::vector<vtkIdType> &)));
4.補充
1)該類額外繼承了一個QObject,這裡是方便與外面的呼叫類通過訊號和槽函式進行資料傳輸;
2)該類繼承自vtkInteractorStyleTrackballCamera,所以無法實現滑鼠拖動生成的球拖動,這裡改為了通過滑鼠滾輪控制;
3)因為具體實現過程中還涉及到了滾動過程中的球的範圍限制,滾動後球的位置是否處於正確的位置等因素,這裡僅說明了基本功能的注意事項。
5.實現效果