diff --git a/src/app/AnomalySaveDialog.cpp b/src/app/AnomalySaveDialog.cpp index fa85572..fd10f34 100644 --- a/src/app/AnomalySaveDialog.cpp +++ b/src/app/AnomalySaveDialog.cpp @@ -37,6 +37,7 @@ AnomalySaveDialog::AnomalySaveDialog(const QString& screenshotPath, int shotW, i form->addRow(formkit::editLabel(QStringLiteral("名称")), name_); type_ = new EmptyAwareComboBox(); + type_->setPlaceholderText(QStringLiteral("请选择异常类型")); // 空(如该形态平台无类型)时显灰占位+「暂无数据」 formkit::capField(type_); form->addRow(formkit::editLabel(QStringLiteral("异常类型")), type_); // 选中类型变化 → 拉其平台样式(legend),使保存的异常按平台类型样式渲染。 @@ -89,9 +90,9 @@ void AnomalySaveDialog::loadStyleForCurrent() { const QString typeId = type_->currentData().toString(); if (typeId.isEmpty()) return; QPointer self(this); - cmdRepo_->getExceptionTypeDetail(typeId, [self](bool ok, QJsonObject data, const QString&) { + cmdRepo_->getExceptionTypeDetail(typeId, [self](bool ok, QJsonObject detail, const QString&) { if (!self || !ok) return; - const QJsonObject lg = data.value(QStringLiteral("legend")).toObject(); + const QJsonObject lg = detail.value(QStringLiteral("legend")).toObject(); // 按形态(1点/2线/3面)从 legend 派生样式:点用 pointColor;线/面用 polyline*。 if (self->remarkSourceType_ == 1) { self->styleColor_ = lg.value(QStringLiteral("pointColor")).toString(); diff --git a/src/app/SliceExport.cpp b/src/app/SliceExport.cpp index 3e29ff5..1c10a56 100644 --- a/src/app/SliceExport.cpp +++ b/src/app/SliceExport.cpp @@ -1,7 +1,18 @@ #include "SliceExport.hpp" +#include +#include #include +#include +#include +#include +#include +#include +#include +#include +#include + #include #include #include @@ -106,6 +117,107 @@ bool captureFramedRegionPng(vtkRenderWindow* win, const double regionBounds[6], return ok; } +bool captureAnomalyShotFromSlice(vtkImageData* colorImg, const double o[3], const double p1[3], + const double p2[3], + const std::vector>& worldPts, int markType, + const std::string& outlineHex, const std::string& path, int& outW, + int& outH) { + outW = outH = 0; + if (colorImg == nullptr || worldPts.empty() || path.empty()) return false; + int dims[3]; + colorImg->GetDimensions(dims); + const int nx = dims[0], ny = dims[1]; + if (nx < 2 || ny < 2) return false; + + // 平面两轴(image i↔e1=p1-o, j↔e2=p2-o);世界点 → 归一(u,v) → 像素(QImage 顶左原点,需翻 j)。 + const double e1[3] = {p1[0] - o[0], p1[1] - o[1], p1[2] - o[2]}; + const double e2[3] = {p2[0] - o[0], p2[1] - o[1], p2[2] - o[2]}; + const double L1 = e1[0] * e1[0] + e1[1] * e1[1] + e1[2] * e1[2]; + const double L2 = e2[0] * e2[0] + e2[1] * e2[1] + e2[2] * e2[2]; + if (L1 < 1e-12 || L2 < 1e-12) return false; + QPolygonF poly; + poly.reserve(static_cast(worldPts.size())); + for (const auto& P : worldPts) { + const double d[3] = {P[0] - o[0], P[1] - o[1], P[2] - o[2]}; + const double u = (d[0] * e1[0] + d[1] * e1[1] + d[2] * e1[2]) / L1; + const double v = (d[0] * e2[0] + d[1] * e2[1] + d[2] * e2[2]) / L2; + poly << QPointF(u * (nx - 1), (ny - 1) - v * (ny - 1)); // 翻 j 到 QImage 坐标 + } + + // 缓冲半径:异常包围盒对角的 15%,最小取图长边 4%(点/小异常也有可见外扩)。 + const QRectF pb = poly.boundingRect(); + const double diag = std::hypot(pb.width(), pb.height()); + const double buffer = std::max(0.04 * std::max(nx, ny), 0.15 * diag); + + // 按形态构 buffer 后的裁剪形状:点→圆、线→胶囊带、面→外扩多边形(填充 ∪ 描边)。 + QPainterPath shape; + if (markType == 1 || poly.size() == 1) { + shape.addEllipse(poly.first(), buffer, buffer); + } else if (markType == 2) { + QPainterPath line; + line.moveTo(poly.first()); + for (int i = 1; i < poly.size(); ++i) line.lineTo(poly[i]); + QPainterPathStroker st; + st.setWidth(2.0 * buffer); + st.setCapStyle(Qt::RoundCap); + st.setJoinStyle(Qt::RoundJoin); + shape = st.createStroke(line); + } else { + QPainterPath fill; + fill.addPolygon(poly); + fill.closeSubpath(); + QPainterPath outline = fill; + QPainterPathStroker st; + st.setWidth(2.0 * buffer); + st.setJoinStyle(Qt::RoundJoin); + shape = fill.united(st.createStroke(outline)); // 向外扩 buffer + } + + // 裁剪区 = 形状包围盒(夹到图内)。 + const QRect crop = shape.boundingRect().toAlignedRect().intersected(QRect(0, 0, nx, ny)); + if (crop.width() < 1 || crop.height() < 1) return false; + + // 切片 RGB(vtk, j=0 在底) → QImage(顶左原点,翻行)。 + QImage src(nx, ny, QImage::Format_RGB888); + for (int j = 0; j < ny; ++j) { + uchar* row = src.scanLine(ny - 1 - j); + for (int i = 0; i < nx; ++i) { + const auto* px = static_cast(colorImg->GetScalarPointer(i, j, 0)); + row[i * 3] = px[0]; + row[i * 3 + 1] = px[1]; + row[i * 3 + 2] = px[2]; + } + } + + // 输出:buffer 形状内贴剖面像素(外透明),再描异常轮廓。 + QImage out(crop.size(), QImage::Format_ARGB32); + out.fill(Qt::transparent); + QPainter pr(&out); + pr.setRenderHint(QPainter::Antialiasing, true); + pr.translate(-crop.topLeft()); + pr.setClipPath(shape); + pr.drawImage(0, 0, src); + pr.setClipping(false); + QColor oc(QString::fromStdString(outlineHex)); + if (!oc.isValid()) oc = QColor(255, 48, 48); + QPen pen(oc); + pen.setWidthF(2.0); + pr.setPen(pen); + pr.setBrush(Qt::NoBrush); + if (markType == 1 || poly.size() == 1) + pr.drawEllipse(poly.first(), 4.0, 4.0); // 点:小标记 + else if (markType == 2) + pr.drawPolyline(poly); + else + pr.drawPolygon(poly); + pr.end(); + + if (!out.save(QString::fromStdString(path), "PNG")) return false; + outW = out.width(); + outH = out.height(); + return true; +} + bool exportSliceDat(vtkImageData* slice, const std::string& path) { if (slice == nullptr || path.empty()) return false; vtkDataArray* arr = slice->GetPointData() ? slice->GetPointData()->GetScalars() : nullptr; diff --git a/src/app/SliceExport.hpp b/src/app/SliceExport.hpp index 15ae84f..d2d9504 100644 --- a/src/app/SliceExport.hpp +++ b/src/app/SliceExport.hpp @@ -1,5 +1,7 @@ #pragma once +#include #include +#include class vtkImageData; class vtkRenderWindow; @@ -21,6 +23,17 @@ bool captureRenderWindowPng(vtkRenderWindow* win, const std::string& path, int& bool captureFramedRegionPng(vtkRenderWindow* win, const double regionBounds[6], double padFactor, double minExtent, const std::string& path, int& outW, int& outH); +// 异常截图(正确做法):**只从切片那张 2D 剖面彩图**上,按异常几何**向外缓冲(buffer)一圈后裁剪**输出。 +// 业界范式 = GIS「几何缓冲 + 按掩膜裁剪栅格」:点→圆、线→胶囊带、面→外扩多边形;缓冲外透明。 +// colorImg:selectedSliceColorImage() 的剖面 RGB 图;o/p1/p2:该切片平面三点(image i↔p1-o, j↔p2-o); +// worldPts:异常顶点(世界系,落在该平面);markType:1点/2线/3面;outlineHex:在裁图上描异常轮廓的颜色。 +// 成功返回 true,填回输出像素宽高。失败(无图/几何退化)返回 false,调用方可回退。 +bool captureAnomalyShotFromSlice(vtkImageData* colorImg, const double o[3], const double p1[3], + const double p2[3], + const std::vector>& worldPts, int markType, + const std::string& outlineHex, const std::string& path, int& outW, + int& outH); + // 把切片重采样 2D 标量影像写为 .dat 文本网格(行=j、列=i,空格分隔,每格取标量首分量);成功返回 true。 bool exportSliceDat(vtkImageData* slice, const std::string& path); diff --git a/src/app/main.cpp b/src/app/main.cpp index 7dad451..459d7e1 100644 --- a/src/app/main.cpp +++ b/src/app/main.cpp @@ -545,8 +545,8 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re const std::string savedSliceId = interactionMgr->selectedSliceDsId(); anomalyDrawTool->start( mode, o, normal, - [&window, &cmdRepo, &nav, sceneView, scene3dRepo, renderWindowPtr, refreshAnomalies, - refreshAnalysis, volId, savedSliceId, normal, o, p1, p2, + [&window, &cmdRepo, &nav, interactionMgr, sceneView, scene3dRepo, renderWindowPtr, + refreshAnomalies, refreshAnalysis, volId, savedSliceId, normal, o, p1, p2, shape](const std::vector& worldPts) { // 草稿异常:先临时渲染(让用户在对话框前看到所画,且截图含异常)。 geopro::core::Anomaly a; @@ -563,27 +563,37 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re a.id = draftId; sceneView->addAnomaly(a); renderWindowPtr->Render(); - // 截图(含异常)→ 临时文件。 const QString shot = QDir(QDir::tempPath()).filePath(QStringLiteral("geopro_anomaly_shot.png")); int sw = 0, sh = 0; - // 相机重构图(方案A):框住圈定 worldPts 外扩区域(异常居中带语境); - // 点/共面退化用切片尺寸兜底框景半径。 - double rb[6] = {worldPts[0][0], worldPts[0][0], worldPts[0][1], - worldPts[0][1], worldPts[0][2], worldPts[0][2]}; - for (const auto& p : worldPts) { - rb[0] = std::min(rb[0], p[0]); rb[1] = std::max(rb[1], p[0]); - rb[2] = std::min(rb[2], p[1]); rb[3] = std::max(rb[3], p[1]); - rb[4] = std::min(rb[4], p[2]); rb[5] = std::max(rb[5], p[2]); + // 截图(正确做法):只从切片那张 2D 剖面彩图、按异常几何向外缓冲一圈裁剪(GIS buffer+掩膜)。 + std::vector> wpts; + wpts.reserve(worldPts.size()); + for (const auto& p : worldPts) wpts.push_back({p[0], p[1], p[2]}); + vtkSmartPointer sliceColor = interactionMgr->selectedSliceColorImage(); + const double oo[3] = {o[0], o[1], o[2]}; + const double pp1[3] = {p1[0], p1[1], p1[2]}; + const double pp2[3] = {p2[0], p2[1], p2[2]}; + bool shotOk = sliceColor && geopro::app::captureAnomalyShotFromSlice( + sliceColor, oo, pp1, pp2, wpts, shape, + a.lineColor, shot.toStdString(), sw, sh); + if (!shotOk) { // 回退:无切片图时退回相机框景(整窗外扩),至少有图。 + double rb[6] = {worldPts[0][0], worldPts[0][0], worldPts[0][1], + worldPts[0][1], worldPts[0][2], worldPts[0][2]}; + for (const auto& p : worldPts) { + rb[0] = std::min(rb[0], p[0]); rb[1] = std::max(rb[1], p[0]); + rb[2] = std::min(rb[2], p[1]); rb[3] = std::max(rb[3], p[1]); + rb[4] = std::min(rb[4], p[2]); rb[5] = std::max(rb[5], p[2]); + } + auto vlen = [](double x, double y, double z) { + return std::sqrt(x * x + y * y + z * z); + }; + const double e1 = vlen(p1[0] - o[0], p1[1] - o[1], p1[2] - o[2]); + const double e2 = vlen(p2[0] - o[0], p2[1] - o[1], p2[2] - o[2]); + const double minExt = 0.25 * std::min(e1, e2); + geopro::app::captureFramedRegionPng(renderWindowPtr, rb, 1.4, minExt, + shot.toStdString(), sw, sh); } - auto vlen = [](double x, double y, double z) { - return std::sqrt(x * x + y * y + z * z); - }; - const double e1 = vlen(p1[0] - o[0], p1[1] - o[1], p1[2] - o[2]); - const double e2 = vlen(p2[0] - o[0], p2[1] - o[1], p2[2] - o[2]); - const double minExt = 0.25 * std::min(e1, e2); // 点/线退化框景半径 - geopro::app::captureFramedRegionPng(renderWindowPtr, rb, 1.4, minExt, - shot.toStdString(), sw, sh); // 异常类型按标注形态(shape=1点/2线/3面)拉对应平台类型,与平台一致。 geopro::app::AnomalySaveDialog dlg(shot, sw, sh, &cmdRepo, nav.currentProjectId(), shape, &window); diff --git a/src/render/actors/AnomalyActor.cpp b/src/render/actors/AnomalyActor.cpp index fcaf848..32d41e3 100644 --- a/src/render/actors/AnomalyActor.cpp +++ b/src/render/actors/AnomalyActor.cpp @@ -19,8 +19,8 @@ namespace { // 虚线点画图案(16 位)与重复因子;dashed 异常用。 constexpr int kDashPattern = 0xF0F0; constexpr int kDashRepeat = 1; -// Point 型异常的方块点像素边长。 -constexpr float kPointSize = 8.0F; +// Point 型异常的小球像素直径(RenderPointsAsSpheres 下为球径)。 +constexpr float kPointSize = 13.0F; // 把一个异常的 localPts 灌入 points(x, -y, 0:深度取负,与 #18 同坐标系)。 void fillPoints2D(vtkPoints* points, const geopro::core::Anomaly& a) @@ -82,6 +82,7 @@ vtkSmartPointer buildActor(vtkPoints* points, std::size_t n, actor->GetProperty()->SetColor(c.r / 255.0, c.g / 255.0, c.b / 255.0); if (asPoints) { actor->GetProperty()->SetPointSize(kPointSize); + actor->GetProperty()->SetRenderPointsAsSpheres(true); // 点异常渲染为小球(非扁平方点) } else { actor->GetProperty()->SetLineWidth(a.lineWidth > 0.0 ? a.lineWidth : 1.0); if (a.dashed) { diff --git a/src/render/interact/AnomalyDrawTool.cpp b/src/render/interact/AnomalyDrawTool.cpp index ef10f7c..49fb759 100644 --- a/src/render/interact/AnomalyDrawTool.cpp +++ b/src/render/interact/AnomalyDrawTool.cpp @@ -169,7 +169,8 @@ void AnomalyDrawTool::updatePreview() { preview_->SetMapper(mapper); preview_->GetProperty()->SetColor(1.0, 0.9, 0.0); // 亮黄 preview_->GetProperty()->SetLineWidth(2.0); - preview_->GetProperty()->SetPointSize(9.0); // 醒目圆点 + preview_->GetProperty()->SetPointSize(mode_ == DrawMode::Point ? 16.0 : 9.0); // 点模式更醒目 + preview_->GetProperty()->SetRenderPointsAsSpheres(true); // 顶点渲染为小球(图钉感) renderer_->AddActor(preview_); interactor_->Render(); } @@ -178,7 +179,8 @@ void AnomalyDrawTool::updateRubber() { if (!renderer_) return; if (rubber_) renderer_->RemoveViewProp(rubber_); rubber_ = nullptr; - if (pts_.empty() || !hasCursor_) { + // 点模式:单点标注,不拉末点→光标的橡皮筋线(否则点完还甩出一条线,用户反馈)。 + if (mode_ == DrawMode::Point || pts_.empty() || !hasCursor_) { if (interactor_) interactor_->Render(); return; }