feat(3d): 创建异常结束手势按业界通用做法重做(点单击/线双击/面点起点闭合)

双击作主手势别扭(与单击天然冲突)。按业界通用改:
- 点:左键单击即落点并完成(无需双击/回车)。
- 线:双击完成(保留)。
- 面:点回起点闭合(≥3点,屏幕邻近 12px 吸附);光标近起点时橡皮筋指向起点预览闭合,提示文案更新。
Esc 取消 / Backspace 撤点不变。
This commit is contained in:
gaozheng 2026-06-26 15:17:53 +08:00
parent 302d946bd9
commit 91a71064b2
2 changed files with 36 additions and 8 deletions

View File

@ -27,6 +27,7 @@ constexpr double kEps = 1e-9;
constexpr double kObserverPriority = 5.0; // 高于切片 widget 与右键菜单(1.0),绘制期独占 constexpr double kObserverPriority = 5.0; // 高于切片 widget 与右键菜单(1.0),绘制期独占
constexpr double kDoubleClickMs = 350.0; // 左键双击闭合阈值 constexpr double kDoubleClickMs = 350.0; // 左键双击闭合阈值
constexpr int kClickSlopPx = 6; // 双击位置相近阈值(px) constexpr int kClickSlopPx = 6; // 双击位置相近阈值(px)
constexpr int kCloseSnapPx = 12; // 面:光标/点击邻近起点的吸附阈值(px)
double nowMs() { double nowMs() {
return std::chrono::duration<double, std::milli>( return std::chrono::duration<double, std::milli>(
@ -60,10 +61,10 @@ void AnomalyDrawTool::start(DrawMode mode, const Vec3& planeOrigin, const Vec3&
if (renderer_) { if (renderer_) {
const char* tip = const char* tip =
mode_ == DrawMode::Point mode_ == DrawMode::Point
? "标注点:左键落点 · 再点可重定位 · 双击/回车确认 · Esc 取消" ? "标注点:左键单击落点即完成 · Esc 取消"
: (mode_ == DrawMode::Line : (mode_ == DrawMode::Line
? "标注线:左键逐点 · Backspace 撤点 · 双击/回车完成 · Esc 取消" ? "标注线:左键逐点 · 双击完成 · Backspace 撤点 · Esc 取消"
: "标注面:左键逐点 · Backspace 撤点 · 双击/回车闭合 · Esc 取消"); : "标注面:左键逐点 · 点回起点闭合 · Backspace 撤点 · Esc 取消");
hint_ = vtkSmartPointer<vtkTextActor>::New(); hint_ = vtkSmartPointer<vtkTextActor>::New();
hint_->SetInput(tip); hint_->SetInput(tip);
hint_->GetTextProperty()->SetFontSize(16); hint_->GetTextProperty()->SetFontSize(16);
@ -123,6 +124,15 @@ Vec3 AnomalyDrawTool::pickOnPlane() const {
return Vec3{{nearP[0] + t * dir[0], nearP[1] + t * dir[1], nearP[2] + t * dir[2]}}; return Vec3{{nearP[0] + t * dir[0], nearP[1] + t * dir[1], nearP[2] + t * dir[2]}};
} }
bool AnomalyDrawTool::nearFirstVertex(int sx, int sy) const {
if (pts_.empty() || !renderer_) return false;
renderer_->SetWorldPoint(pts_.front()[0], pts_.front()[1], pts_.front()[2], 1.0);
renderer_->WorldToDisplay();
double d[3];
renderer_->GetDisplayPoint(d);
return std::abs(d[0] - sx) <= kCloseSnapPx && std::abs(d[1] - sy) <= kCloseSnapPx;
}
void AnomalyDrawTool::addVertex() { void AnomalyDrawTool::addVertex() {
// 点模式:单点,再次左键 = 重定位(微调),不累积;线/面模式:累积顶点。 // 点模式:单点,再次左键 = 重定位(微调),不累积;线/面模式:累积顶点。
if (mode_ == DrawMode::Point && !pts_.empty()) if (mode_ == DrawMode::Point && !pts_.empty())
@ -185,12 +195,13 @@ void AnomalyDrawTool::updateRubber() {
if (interactor_) interactor_->Render(); if (interactor_) interactor_->Render();
return; return;
} }
// 末点 → 当前光标投影点 的虚线橡皮筋(跟手反馈)。 // 末点 → 光标 的虚线橡皮筋(跟手反馈);面模式光标邻近起点 → 指向起点,预览闭合
const Vec3& a = pts_.back(); const Vec3& a = pts_.back();
const Vec3 endP = cursorNearStart_ ? pts_.front() : cursorPt_;
vtkNew<vtkPoints> points; vtkNew<vtkPoints> points;
points->SetNumberOfPoints(2); points->SetNumberOfPoints(2);
points->SetPoint(0, a[0], a[1], a[2]); points->SetPoint(0, a[0], a[1], a[2]);
points->SetPoint(1, cursorPt_[0], cursorPt_[1], cursorPt_[2]); points->SetPoint(1, endP[0], endP[1], endP[2]);
vtkNew<vtkPolyData> poly; vtkNew<vtkPolyData> poly;
poly->SetPoints(points); poly->SetPoints(points);
vtkNew<vtkPolyLine> line; vtkNew<vtkPolyLine> line;
@ -237,6 +248,9 @@ void AnomalyDrawTool::installObservers() {
// 鼠标移动:更新末点→光标的虚线橡皮筋(跟手反馈)。不 abort不干扰其它悬停。 // 鼠标移动:更新末点→光标的虚线橡皮筋(跟手反馈)。不 abort不干扰其它悬停。
self->cursorPt_ = self->pickOnPlane(); self->cursorPt_ = self->pickOnPlane();
self->hasCursor_ = true; self->hasCursor_ = true;
const int* mp = self->interactor_->GetEventPosition();
self->cursorNearStart_ = self->mode_ == DrawMode::Face && self->pts_.size() >= 3 &&
self->nearFirstVertex(mp[0], mp[1]);
self->updateRubber(); self->updateRubber();
return; return;
} }
@ -245,9 +259,21 @@ void AnomalyDrawTool::installObservers() {
if (self->cmd_) self->cmd_->SetAbortFlag(1); if (self->cmd_) self->cmd_->SetAbortFlag(1);
switch (eid) { switch (eid) {
case vtkCommand::LeftButtonPressEvent: { case vtkCommand::LeftButtonPressEvent: {
// 左键双连击 = 闭合(标准多边形交互);否则加顶点。 // 点:单击即落点并完成(业界通用,无需双击/回车)。
if (self->mode_ == DrawMode::Point) {
self->addVertex();
self->finish();
break;
}
const double now = nowMs(); const double now = nowMs();
const int* p = self->interactor_->GetEventPosition(); const int* p = self->interactor_->GetEventPosition();
// 面:点回起点(屏幕邻近)闭合(≥3点),不加点。
if (self->mode_ == DrawMode::Face && self->pts_.size() >= 3 &&
self->nearFirstVertex(p[0], p[1])) {
self->finish();
break;
}
// 线:左键双连击 = 完成;否则加顶点。
const bool dbl = self->lastClickMs_ >= 0.0 && const bool dbl = self->lastClickMs_ >= 0.0 &&
(now - self->lastClickMs_) < kDoubleClickMs && (now - self->lastClickMs_) < kDoubleClickMs &&
std::abs(p[0] - self->lastClickX_) <= kClickSlopPx && std::abs(p[0] - self->lastClickX_) <= kClickSlopPx &&
@ -256,7 +282,7 @@ void AnomalyDrawTool::installObservers() {
self->lastClickX_ = p[0]; self->lastClickX_ = p[0];
self->lastClickY_ = p[1]; self->lastClickY_ = p[1];
if (dbl) { if (dbl) {
// 隔离单/双击:回滚"双击第一下"那次加点/移点(否则点会移、线多一段、面多一条边),再完成。 // 隔离单/双击:回滚"双击第一下"那次加点(否则线多一段、面多一条边),再完成。
self->pts_ = self->ptsBeforeClick_; self->pts_ = self->ptsBeforeClick_;
self->updatePreview(); self->updatePreview();
self->finish(); self->finish();

View File

@ -41,8 +41,9 @@ private:
void addVertex(); // 左键:加顶点 void addVertex(); // 左键:加顶点
void updatePreview(); // 重建已点几何(顶点圆点 + 实线折线;单点也可见) void updatePreview(); // 重建已点几何(顶点圆点 + 实线折线;单点也可见)
void updateRubber(); // 鼠标移动:末点→光标的虚线橡皮筋 void updateRubber(); // 鼠标移动:末点→光标的虚线橡皮筋
void finish(); // 右键/双击/回车:闭合 void finish(); // 双击/回车/(面)点起点:完成
Vec3 pickOnPlane() const; // 当前鼠标屏幕点 → 射线与平面交点 Vec3 pickOnPlane() const; // 当前鼠标屏幕点 → 射线与平面交点
bool nearFirstVertex(int sx, int sy) const; // 屏幕点是否邻近起点(面闭合判定/提示)
void installObservers(); void installObservers();
void removeObservers(); void removeObservers();
@ -64,6 +65,7 @@ private:
vtkSmartPointer<vtkTextActor> hint_; // 屏幕操作提示 vtkSmartPointer<vtkTextActor> hint_; // 屏幕操作提示
Vec3 cursorPt_{{0, 0, 0}}; // 当前鼠标在切面上的投影点 Vec3 cursorPt_{{0, 0, 0}}; // 当前鼠标在切面上的投影点
bool hasCursor_ = false; bool hasCursor_ = false;
bool cursorNearStart_ = false; // 面模式光标邻近起点 → 橡皮筋指向起点预览闭合
vtkSmartPointer<vtkCallbackCommand> cmd_; vtkSmartPointer<vtkCallbackCommand> cmd_;
unsigned long tagLeft_ = 0, tagMove_ = 0, tagRight_ = 0, tagKey_ = 0, tagDbl_ = 0; unsigned long tagLeft_ = 0, tagMove_ = 0, tagRight_ = 0, tagKey_ = 0, tagDbl_ = 0;
// 双击闭合检测(左键两连击):记上次左键时刻 + 屏幕位置。 // 双击闭合检测(左键两连击):记上次左键时刻 + 屏幕位置。