From 58544ffb3c7c5848fe92a191a6995778bd472c2b Mon Sep 17 00:00:00 2001 From: gaozheng Date: Fri, 26 Jun 2026 10:36:38 +0800 Subject: [PATCH] =?UTF-8?q?feat(3d):=20=E5=88=9B=E5=BB=BA=E5=BC=82?= =?UTF-8?q?=E5=B8=B8=E6=94=AF=E6=8C=81=E7=82=B9/=E7=BA=BF/=E9=9D=A2?= =?UTF-8?q?=E4=B8=89=E6=80=81(=E5=AD=90=E8=8F=9C=E5=8D=95+=E7=BB=9F?= =?UTF-8?q?=E4=B8=80=E5=8F=8C=E5=87=BB=C2=B7=E5=9B=9E=E8=BD=A6=E6=8F=90?= =?UTF-8?q?=E4=BA=A4)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 按原型「创建异常→点/线/面」改造: - 右键菜单单项「创建异常」→ 点/线/面 子菜单;形态(1/2/3)同时驱动绘制 mode、a.markType、 对话框查平台类型的 remarkSourceType(core::AnomalyMarkType 与 remarkSourceType 同值,一个 shape 贯通) - AnomalyDrawTool 泛化 DrawMode{Point,Line,Face}:点≥1(再点重定位微调)/线≥2(开放)/面≥3(闭合); 最少点数按模式;分形态屏幕提示 - 交互按锁定规范:**双击/回车 提交**(去掉右键提交,右键绘制中消费不响应保留菜单语义); **Backspace/Delete 撤上一点**;Esc 取消 - AnomalyActor 已支持点(verts)/折线/闭合多边形三态渲染,无需改 测试:439/439 通过 --- src/app/main.cpp | 27 ++++++++++------- src/render/interact/AnomalyDrawTool.cpp | 39 ++++++++++++++++++++----- src/render/interact/AnomalyDrawTool.hpp | 16 ++++++---- 3 files changed, 58 insertions(+), 24 deletions(-) diff --git a/src/app/main.cpp b/src/app/main.cpp index 3f3e198..2f4cd82 100644 --- a/src/app/main.cpp +++ b/src/app/main.cpp @@ -506,7 +506,10 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re [&window, &cmdRepo, &nav, interactionMgr, sceneView, scene3dRepo, refreshAnalysis, refreshAnomalies, drawer, anomalyDrawTool, renderWindowPtr]() { QMenu menu(&window); - QAction* aAnomaly = menu.addAction(QStringLiteral("创建异常")); + QMenu* anomMenu = menu.addMenu(QStringLiteral("创建异常")); // → 点/线/面 子菜单 + QAction* aAnoPoint = anomMenu->addAction(QStringLiteral("点")); + QAction* aAnoLine = anomMenu->addAction(QStringLiteral("线")); + QAction* aAnoFace = anomMenu->addAction(QStringLiteral("面")); QAction* aSave = menu.addAction(QStringLiteral("保存")); QMenu* expMenu = menu.addMenu(QStringLiteral("导出")); QAction* aImg = expMenu->addAction(QStringLiteral("图片")); @@ -521,9 +524,14 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re if (chosen == aFace) { interactionMgr->faceSelected(); return; } if (chosen == aFlip) { interactionMgr->flipView(); return; } if (chosen == aClose) { interactionMgr->closeSelected(); return; } // →onSliceClosed→取消列表勾选 - if (chosen == aAnomaly) { - // 在选中切片平面上启动圈定(左键逐点、右键/回车闭合、Esc 取消)。 + if (chosen == aAnoPoint || chosen == aAnoLine || chosen == aAnoFace) { + // 形态(1点/2线/3面):同时决定绘制工具 mode、a.markType、对话框查平台类型的 remarkSourceType。 + // core::AnomalyMarkType 与 remarkSourceType 同值(Point=1/Polyline=2/Polygon=3),用一个 shape 贯通。 namespace ri = geopro::render::interact; + using DM = ri::AnomalyDrawTool::DrawMode; + const int shape = (chosen == aAnoPoint) ? 1 : (chosen == aAnoLine) ? 2 : 3; + const DM mode = + (chosen == aAnoPoint) ? DM::Point : (chosen == aAnoLine) ? DM::Line : DM::Face; int axis = 3; ri::Vec3 o{}, p1{}, p2{}; if (!interactionMgr->selectedSlicePlane(axis, o, p1, p2)) return; @@ -536,13 +544,13 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re // 异常归属(spec §8):当前选中切片已保存(selectedSliceDsId 非空)→挂该切片;临时切片→挂体。 const std::string savedSliceId = interactionMgr->selectedSliceDsId(); anomalyDrawTool->start( - o, normal, + mode, o, normal, [&window, &cmdRepo, &nav, sceneView, scene3dRepo, renderWindowPtr, refreshAnomalies, - refreshAnalysis, volId, savedSliceId, normal, o, p1, - p2](const std::vector& worldPts) { + refreshAnalysis, volId, savedSliceId, normal, o, p1, p2, + shape](const std::vector& worldPts) { // 草稿异常:先临时渲染(让用户在对话框前看到所画,且截图含异常)。 geopro::core::Anomaly a; - a.markType = geopro::core::AnomalyMarkType::Polygon; + a.markType = static_cast(shape); a.remarkSourceId = geopro::core::resolveAnomalyMount(!savedSliceId.empty(), savedSliceId, volId); a.lineColor = "#ff3030"; @@ -576,10 +584,9 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re const double minExt = 0.25 * std::min(e1, e2); // 点/线退化框景半径 geopro::app::captureFramedRegionPng(renderWindowPtr, rb, 1.4, minExt, shot.toStdString(), sw, sh); - // 异常类型按标注形态拉平台类型(与平台一致)。当前仅面(Polygon)→remarkSourceType=3; - // P2 接入点/线后由菜单形态决定 1/2/3。 + // 异常类型按标注形态(shape=1点/2线/3面)拉对应平台类型,与平台一致。 geopro::app::AnomalySaveDialog dlg(shot, sw, sh, &cmdRepo, - nav.currentProjectId(), 3, &window); + nav.currentProjectId(), shape, &window); if (dlg.exec() != QDialog::Accepted) { sceneView->removeAnomaly(draftId); renderWindowPtr->Render(); diff --git a/src/render/interact/AnomalyDrawTool.cpp b/src/render/interact/AnomalyDrawTool.cpp index 2cff543..ef10f7c 100644 --- a/src/render/interact/AnomalyDrawTool.cpp +++ b/src/render/interact/AnomalyDrawTool.cpp @@ -40,10 +40,11 @@ AnomalyDrawTool::AnomalyDrawTool(vtkRenderWindowInteractor* interactor, vtkRende AnomalyDrawTool::~AnomalyDrawTool() { removeObservers(); } -void AnomalyDrawTool::start(const Vec3& planeOrigin, const Vec3& planeNormal, +void AnomalyDrawTool::start(DrawMode mode, const Vec3& planeOrigin, const Vec3& planeNormal, std::function&)> onFinish, std::function onCancel) { if (active_) cancel(); + mode_ = mode; origin_ = planeOrigin; normal_ = normalize(planeNormal); onFinish_ = std::move(onFinish); @@ -54,10 +55,16 @@ void AnomalyDrawTool::start(const Vec3& planeOrigin, const Vec3& planeNormal, active_ = true; installObservers(); - // 屏幕操作提示(左上角),解决"不知如何闭合"。 + // 屏幕操作提示(左上角),按形态给不同指引。 if (renderer_) { + const char* tip = + mode_ == DrawMode::Point + ? "标注点:左键落点 · 再点可重定位 · 双击/回车确认 · Esc 取消" + : (mode_ == DrawMode::Line + ? "标注线:左键逐点 · Backspace 撤点 · 双击/回车完成 · Esc 取消" + : "标注面:左键逐点 · Backspace 撤点 · 双击/回车闭合 · Esc 取消"); hint_ = vtkSmartPointer::New(); - hint_->SetInput("圈定异常:左键逐点 · 双击或右键完成 · Esc 取消"); + hint_->SetInput(tip); hint_->GetTextProperty()->SetFontSize(16); hint_->GetTextProperty()->SetColor(1.0, 0.9, 0.0); hint_->GetPositionCoordinate()->SetCoordinateSystemToNormalizedViewport(); @@ -116,7 +123,11 @@ Vec3 AnomalyDrawTool::pickOnPlane() const { } void AnomalyDrawTool::addVertex() { - pts_.push_back(pickOnPlane()); + // 点模式:单点,再次左键 = 重定位(微调),不累积;线/面模式:累积顶点。 + if (mode_ == DrawMode::Point && !pts_.empty()) + pts_[0] = pickOnPlane(); + else + pts_.push_back(pickOnPlane()); updatePreview(); } @@ -200,7 +211,9 @@ void AnomalyDrawTool::updateRubber() { } void AnomalyDrawTool::finish() { - if (pts_.size() < 3) { // 不足以成面 → 取消 + const std::size_t minPts = + mode_ == DrawMode::Point ? 1 : (mode_ == DrawMode::Line ? 2 : 3); // 点1/线2/面3 + if (pts_.size() < minPts) { // 不足以成形 → 取消 cancel(); return; } @@ -245,11 +258,21 @@ void AnomalyDrawTool::installObservers() { self->addVertex(); break; } - case vtkCommand::RightButtonPressEvent: self->finish(); break; + case vtkCommand::RightButtonPressEvent: + // 绘制中右键不提交(保留给「创建异常」菜单语义);已 abort 消费,不打开菜单。 + break; case vtkCommand::KeyPressEvent: { const char* key = self->interactor_->GetKeySym(); - if (key && (std::string(key) == "Escape")) self->cancel(); - else if (key && (std::string(key) == "Return")) self->finish(); + const std::string k = key ? std::string(key) : std::string(); + if (k == "Escape") + self->cancel(); + else if (k == "Return" || k == "KP_Enter") + self->finish(); + else if ((k == "BackSpace" || k == "Delete") && !self->pts_.empty()) { + self->pts_.pop_back(); // 撤上一点 + self->updatePreview(); + self->updateRubber(); + } break; } default: break; diff --git a/src/render/interact/AnomalyDrawTool.hpp b/src/render/interact/AnomalyDrawTool.hpp index ef5d692..36c4e86 100644 --- a/src/render/interact/AnomalyDrawTool.hpp +++ b/src/render/interact/AnomalyDrawTool.hpp @@ -14,21 +14,24 @@ class vtkCallbackCommand; namespace geopro::render::interact { -// 异常圈定工具(#4b):在给定切片平面上交互式画多边形。 -// 左键逐点加顶点(屏幕射线与平面求交,落在平面上);右键 / 双击 / 回车 闭合 → onFinish(worldPts); -// Esc / 不足 3 点闭合 → onCancel。绘制中实时预览折线。 -// 高优先级(2.0)交互器观察者抢输入:先于切片 widget 与 InteractionManager 右键菜单,绘制期独占左右键。 +// 异常圈定工具(#4b):在给定切片平面上交互式画 点 / 线 / 面。 +// 左键逐点加顶点(屏幕射线与平面求交,落在平面上);**双击 / 回车 提交** → onFinish(worldPts); +// Esc 取消;Backspace 撤上一点;点模式再次左键=重定位单点(微调)。右键绘制中不响应(保留给菜单语义)。 +// 点≥1 / 线≥2(开放) / 面≥3(闭合);闭合与否由上层据 markType 渲染,本工具只产顶点。 +// 高优先级(5.0)交互器观察者抢输入:先于切片 widget 与 InteractionManager 右键菜单,绘制期独占左右键。 // render 层:只碰 VTK,不认业务;产物(平面上的世界点)经回调交上层组装 core::Anomaly。 class AnomalyDrawTool { public: + enum class DrawMode { Point, Line, Face }; // 点(1)/线(2,开放)/面(3,闭合) + AnomalyDrawTool(vtkRenderWindowInteractor* interactor, vtkRenderer* renderer); ~AnomalyDrawTool(); AnomalyDrawTool(const AnomalyDrawTool&) = delete; AnomalyDrawTool& operator=(const AnomalyDrawTool&) = delete; - // 开始在平面(origin/normal)上圈定。onFinish 收闭合多边形顶点(世界系);onCancel 取消。 - void start(const Vec3& planeOrigin, const Vec3& planeNormal, + // 开始在平面(origin/normal)上按 mode 圈定。onFinish 收顶点(世界系);onCancel 取消。 + void start(DrawMode mode, const Vec3& planeOrigin, const Vec3& planeNormal, std::function&)> onFinish, std::function onCancel); bool active() const { return active_; } @@ -49,6 +52,7 @@ private: vtkRenderer* renderer_; bool active_ = false; + DrawMode mode_ = DrawMode::Face; Vec3 origin_{{0, 0, 0}}, normal_{{0, 0, 1}}; std::vector pts_; std::function&)> onFinish_;