From 369d67aa7755d73333d9ef1618f8be6691e472b7 Mon Sep 17 00:00:00 2001 From: lh <2334563547@qq.com> Date: Mon, 14 Sep 2026 14:13:19 +0800 Subject: [PATCH] =?UTF-8?q?feat(render):=20=E6=96=B0=E5=A2=9E=E5=B9=B3?= =?UTF-8?q?=E9=9D=A2=E6=AF=94=E4=BE=8B=E7=BD=91=E6=A0=BC=E5=B9=B6=E6=8E=A5?= =?UTF-8?q?=E5=85=A5=20PEBI=20=E9=A2=84=E8=A7=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 使用独立平面比例网格替换二维 vtkCubeAxesActor - 支持动态轴边、刻度标签、底板及前后遮挡 - 调整网格、比例线和方向轴文字配色 - 初始化 FreeType 并同步工程配置、翻译和开发清单 --- Bin/Config/Lang/cn/nmNum_cn.qm | Bin 141205 -> 141196 bytes Bin/Config/Lang/cn/nmNum_cn.ts | 2 +- .../nmRender/nmVTKPlanarScaleGridLayer.h | 41 + .../nmSubWxs/nmPebiGridSceneController.h | 2 +- .../nmVTKOrientationMarkerController.cpp | 27 +- .../nmRender/nmVTKPlanarScaleGridLayer.cpp | 987 ++++++++++++++++++ .../nmRender/nmWxVTKRenderContainerWidget.cpp | 1 + .../nmSubWxs/nmPebiGridSceneController.cpp | 42 +- Src/nmNum/nmSubWxs/nmWxPebiGridViewWidget.cpp | 4 +- Src4/nmNum/nmRender/nmRender.pro | 2 + 10 files changed, 1074 insertions(+), 34 deletions(-) create mode 100644 Include/nmNum/nmRender/nmVTKPlanarScaleGridLayer.h create mode 100644 Src/nmNum/nmRender/nmVTKPlanarScaleGridLayer.cpp diff --git a/Bin/Config/Lang/cn/nmNum_cn.qm b/Bin/Config/Lang/cn/nmNum_cn.qm index 41456ca454c09dc25d5f8f284827e78bb67421be..f040dfd33c4f036a9094912abe2170b622af159d 100644 GIT binary patch delta 3084 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 Property:属性: Show Grid Lines显示网格线 Show Scalar Bar显示色标 - Coordinate Grid坐标比例网格 + Scale Grid比例网格 The grid preview data is invalid.网格预览数据无效。 No property无属性 Material材质 diff --git a/Include/nmNum/nmRender/nmVTKPlanarScaleGridLayer.h b/Include/nmNum/nmRender/nmVTKPlanarScaleGridLayer.h new file mode 100644 index 00000000..386c4cad --- /dev/null +++ b/Include/nmNum/nmRender/nmVTKPlanarScaleGridLayer.h @@ -0,0 +1,41 @@ +#ifndef NMVTKPLANARSCALEGRIDLAYER_H +#define NMVTKPLANARSCALEGRIDLAYER_H + +#include "nmVTKRenderLayer.h" + +class nmVTKPlanarScaleGridLayerPrivate; + +/** + * @brief 在世界坐标 XY 平面中显示比例网格、坐标轴和数值标签。 + * + * 图层只复制 X/Y 边界和所在平面的 Z 值,不引用业务网格。浅色底板、 + * 背景比例线、坐标轴及刻度均使用世界坐标几何;数值标签由世界坐标 + * 刻度点逐帧投影为屏幕 Overlay。绘制前根据相机所在平面侧别调整深度 + * 顺序,但不管理相机,也不注册 Interactor 或 Camera Observer。 + */ +class NM_RENDER_EXPORT nmVTKPlanarScaleGridLayer : public nmVTKRenderLayer +{ +public: + explicit nmVTKPlanarScaleGridLayer(const QString& sLayerId); + virtual ~nmVTKPlanarScaleGridLayer(); + + /** + * @brief 设置真实 X/Y 边界和网格所在平面的 Z 值,不主动渲染。 + * @return 参数均有限且 X/Y 范围严格递增时返回 true。 + */ + bool setBounds(double dXMinimum, + double dXMaximum, + double dYMinimum, + double dYMaximum, + double dPlaneZ); + + /** @brief 返回未经显示偏移处理的真实 X/Y 边界。 */ + bool getBounds(double dBounds[4]) const; + +private: + Q_DISABLE_COPY(nmVTKPlanarScaleGridLayer) + + nmVTKPlanarScaleGridLayerPrivate* m_pPrivate; +}; + +#endif // NMVTKPLANARSCALEGRIDLAYER_H diff --git a/Include/nmNum/nmSubWxs/nmPebiGridSceneController.h b/Include/nmNum/nmSubWxs/nmPebiGridSceneController.h index 3f255bcd..d769e7d8 100644 --- a/Include/nmNum/nmSubWxs/nmPebiGridSceneController.h +++ b/Include/nmNum/nmSubWxs/nmPebiGridSceneController.h @@ -12,7 +12,7 @@ class nmWxVTKRenderContainerWidget; /** * @brief 把二维 PEBI 网格组合为 nmRender 通用图层。 * - * Scene 拥有 Mesh、按需创建的 ScalarBar 和 BoundsGrid 图层;本控制器只 + * Scene 拥有 Mesh、按需创建的 ScalarBar 和平面比例网格图层;本控制器只 * 保存非所有权指针。所有批量修改完成后由控制器统一请求一次渲染。 */ class NM_SUB_WXS_EXPORT nmPebiGridSceneController diff --git a/Src/nmNum/nmRender/nmVTKOrientationMarkerController.cpp b/Src/nmNum/nmRender/nmVTKOrientationMarkerController.cpp index 690e8995..165153cc 100644 --- a/Src/nmNum/nmRender/nmVTKOrientationMarkerController.cpp +++ b/Src/nmNum/nmRender/nmVTKOrientationMarkerController.cpp @@ -23,13 +23,12 @@ bool isOrientationMarkerGuiThread() pApplication->thread() == QThread::currentThread(); } -// 同时设置轴杆、箭头和标签颜色,避免依赖不同 VTK 版本的默认外观。 +// 方向轴杆和箭头使用各自的方向色,标签颜色由独立函数统一设置。 void setAxisColor(vtkProperty* pShaftProperty, - vtkProperty* pTipProperty, - vtkCaptionActor2D* pCaptionActor, - double dRed, - double dGreen, - double dBlue) + vtkProperty* pTipProperty, + double dRed, + double dGreen, + double dBlue) { if(pShaftProperty != nullptr) { pShaftProperty->SetColor(dRed, dGreen, dBlue); @@ -37,6 +36,13 @@ void setAxisColor(vtkProperty* pShaftProperty, if(pTipProperty != nullptr) { pTipProperty->SetColor(dRed, dGreen, dBlue); } +} + +void setCaptionColor(vtkCaptionActor2D* pCaptionActor, + double dRed, + double dGreen, + double dBlue) +{ if(pCaptionActor != nullptr && pCaptionActor->GetCaptionTextProperty() != nullptr) { pCaptionActor->GetCaptionTextProperty()->SetColor( @@ -68,16 +74,19 @@ nmVTKOrientationMarkerController::nmVTKOrientationMarkerController() m_pAxesActor->SetZAxisLabelText("Z"); setAxisColor(m_pAxesActor->GetXAxisShaftProperty(), m_pAxesActor->GetXAxisTipProperty(), - m_pAxesActor->GetXAxisCaptionActor2D(), 1.0, 0.0, 0.0); setAxisColor(m_pAxesActor->GetYAxisShaftProperty(), m_pAxesActor->GetYAxisTipProperty(), - m_pAxesActor->GetYAxisCaptionActor2D(), 0.0, 1.0, 0.0); setAxisColor(m_pAxesActor->GetZAxisShaftProperty(), m_pAxesActor->GetZAxisTipProperty(), - m_pAxesActor->GetZAxisCaptionActor2D(), 0.0, 0.0, 1.0); + setCaptionColor(m_pAxesActor->GetXAxisCaptionActor2D(), + 1.0, 1.0, 1.0); + setCaptionColor(m_pAxesActor->GetYAxisCaptionActor2D(), + 1.0, 1.0, 1.0); + setCaptionColor(m_pAxesActor->GetZAxisCaptionActor2D(), + 1.0, 1.0, 1.0); } if(m_pMarkerWidget != nullptr) { diff --git a/Src/nmNum/nmRender/nmVTKPlanarScaleGridLayer.cpp b/Src/nmNum/nmRender/nmVTKPlanarScaleGridLayer.cpp new file mode 100644 index 00000000..c16152d3 --- /dev/null +++ b/Src/nmNum/nmRender/nmVTKPlanarScaleGridLayer.cpp @@ -0,0 +1,987 @@ +#include "nmVTKPlanarScaleGridLayer.h" + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +namespace { + +const double PLANAR_AXIS_DEPTH_FACTOR = 0.00020; +const double PLANAR_GRID_DEPTH_FACTOR = 0.00050; +const double PLANAR_BACKGROUND_DEPTH_FACTOR = 0.00100; +const double PLANAR_CAMERA_SIDE_TOLERANCE_FACTOR = 0.000001; +const double PLANAR_EDGE_SWITCH_PIXEL_TOLERANCE = 2.0; +const double PLANAR_TICK_LENGTH_FACTOR = 0.012; +const int PLANAR_LABEL_PIXEL_GAP = 8; +const int PLANAR_CORNER_LABEL_PIXEL_SHIFT = 12; +const int PLANAR_TARGET_INTERVAL_COUNT = 6; + +bool isPlanarScaleGridGuiThread() +{ + QCoreApplication* pApplication = QCoreApplication::instance(); + return pApplication == nullptr || + pApplication->thread() == QThread::currentThread(); +} + +bool isValidPlanarBounds(double dXMinimum, + double dXMaximum, + double dYMinimum, + double dYMaximum, + double dPlaneZ) +{ + return vtkMath::IsFinite(dXMinimum) && + vtkMath::IsFinite(dXMaximum) && + vtkMath::IsFinite(dYMinimum) && + vtkMath::IsFinite(dYMaximum) && + vtkMath::IsFinite(dPlaneZ) && + dXMinimum < dXMaximum && dYMinimum < dYMaximum; +} + +void appendLine(vtkPoints* pPoints, + vtkCellArray* pLines, + double dX1, + double dY1, + double dZ1, + double dX2, + double dY2, + double dZ2) +{ + if(pPoints == nullptr || pLines == nullptr) { + return; + } + vtkIdType nPointIds[2]; + nPointIds[0] = pPoints->InsertNextPoint(dX1, dY1, dZ1); + nPointIds[1] = pPoints->InsertNextPoint(dX2, dY2, dZ2); + pLines->InsertNextCell(2, nPointIds); +} + +void applyLineData(vtkPolyData* pPolyData, + vtkPoints* pPoints, + vtkCellArray* pLines) +{ + if(pPolyData == nullptr || pPoints == nullptr || pLines == nullptr) { + return; + } + pPolyData->SetPoints(pPoints); + pPolyData->SetLines(pLines); + pPolyData->Modified(); +} + +void applySurfaceData(vtkPolyData* pPolyData, + vtkPoints* pPoints, + vtkCellArray* pPolygons) +{ + if(pPolyData == nullptr || pPoints == nullptr || pPolygons == nullptr) { + return; + } + pPolyData->SetPoints(pPoints); + pPolyData->SetPolys(pPolygons); + pPolyData->Modified(); +} + +void updatePolyDataZ(vtkPolyData* pPolyData, double dZ) +{ + vtkPoints* pPoints = pPolyData != nullptr + ? pPolyData->GetPoints() : nullptr; + if(pPoints == nullptr) { + return; + } + + double dPoint[3]; + const vtkIdType nPointCount = pPoints->GetNumberOfPoints(); + for(vtkIdType nPoint = 0; nPoint < nPointCount; ++nPoint) { + pPoints->GetPoint(nPoint, dPoint); + dPoint[2] = dZ; + pPoints->SetPoint(nPoint, dPoint); + } + pPoints->Modified(); + pPolyData->Modified(); +} + +void configureLineActor(vtkPolyData* pPolyData, + vtkPolyDataMapper* pMapper, + vtkActor* pActor, + double dRed, + double dGreen, + double dBlue, + float fLineWidth) +{ + if(pPolyData == nullptr || pMapper == nullptr || pActor == nullptr) { + return; + } + pMapper->SetInputData(pPolyData); + pMapper->ScalarVisibilityOff(); + pActor->SetMapper(pMapper); + pActor->GetProperty()->SetColor(dRed, dGreen, dBlue); + pActor->GetProperty()->SetLineWidth(fLineWidth); + pActor->GetProperty()->SetOpacity(1.0); + pActor->GetProperty()->LightingOff(); + pActor->PickableOff(); +} + +void configureBackgroundActor(vtkPolyData* pPolyData, + vtkPolyDataMapper* pMapper, + vtkActor* pActor) +{ + if(pPolyData == nullptr || pMapper == nullptr || pActor == nullptr) { + return; + } + pMapper->SetInputData(pPolyData); + pMapper->ScalarVisibilityOff(); + pActor->SetMapper(pMapper); + pActor->GetProperty()->SetColor( + 171.0 / 255.0, 232.0 / 255.0, 241.0 / 255.0); + pActor->GetProperty()->SetOpacity(1.0); + pActor->GetProperty()->SetRepresentationToSurface(); + pActor->GetProperty()->EdgeVisibilityOff(); + pActor->GetProperty()->LightingOff(); + // 底板两面都需要可见,相机翻到 XY 平面另一侧时仍由深度关系遮挡。 + pActor->GetProperty()->FrontfaceCullingOff(); + pActor->GetProperty()->BackfaceCullingOff(); + pActor->PickableOff(); +} + +bool calculateNiceTicks(double dMinimum, + double dMaximum, + std::vector& vTicks, + double& dStep) +{ + vTicks.clear(); + dStep = 0.0; + const double dRange = dMaximum - dMinimum; + if(!vtkMath::IsFinite(dRange) || dRange <= 0.0) { + return false; + } + + const double dRawStep = + dRange / static_cast(PLANAR_TARGET_INTERVAL_COUNT); + const double dMagnitude = std::pow( + 10.0, std::floor(std::log10(dRawStep))); + if(!vtkMath::IsFinite(dMagnitude) || dMagnitude <= 0.0) { + return false; + } + + const double dNormalizedStep = dRawStep / dMagnitude; + double dNiceFactor = 10.0; + if(dNormalizedStep <= 1.0) { + dNiceFactor = 1.0; + } else if(dNormalizedStep <= 2.0) { + dNiceFactor = 2.0; + } else if(dNormalizedStep <= 2.5) { + dNiceFactor = 2.5; + } else if(dNormalizedStep <= 5.0) { + dNiceFactor = 5.0; + } + dStep = dNiceFactor * dMagnitude; + if(!vtkMath::IsFinite(dStep) || dStep <= 0.0) { + return false; + } + + const double dTolerance = dStep * 0.00000001; + const double dFirstTick = + std::ceil((dMinimum - dTolerance) / dStep) * dStep; + for(int nIndex = 0; nIndex < 128; ++nIndex) { + double dValue = dFirstTick + static_cast(nIndex) * dStep; + if(dValue > dMaximum + dTolerance) { + break; + } + if(dValue >= dMinimum - dTolerance) { + if(std::fabs(dValue) < dTolerance) { + dValue = 0.0; + } + vTicks.push_back(dValue); + } + } + return !vTicks.empty(); +} + +QString formatTickValue(double dValue, double dStep) +{ + const double dAbsoluteStep = std::fabs(dStep); + if(std::fabs(dValue) < dAbsoluteStep * 0.00000001) { + dValue = 0.0; + } + + if(std::fabs(dValue) >= 1000000000.0) { + return QString::number(dValue, 'g', 8); + } + + int nDecimalCount = 0; + double dScaledStep = dAbsoluteStep; + while(nDecimalCount < 8 && + std::fabs(dScaledStep - std::floor(dScaledStep + 0.5)) > + 0.00000001) { + dScaledStep *= 10.0; + ++nDecimalCount; + } + return QString::number(dValue, 'f', nDecimalCount); +} + +int roundedPixelOffset(double dValue) +{ + return static_cast(dValue >= 0.0 + ? std::floor(dValue + 0.5) + : std::ceil(dValue - 0.5)); +} + +} + +class nmVTKPlanarScaleGridLayerPrivate; + +/** + * @brief 在 VTK 每次绘制组合 Prop 前更新相机相关轴边,不注册额外 Observer。 + */ +class vtkNmPlanarScaleGridProp : public vtkPropAssembly +{ +public: + static vtkNmPlanarScaleGridProp* New(); + vtkTypeMacro(vtkNmPlanarScaleGridProp, vtkPropAssembly); + + void setOwner(nmVTKPlanarScaleGridLayerPrivate* pOwner) + { + m_pOwner = pOwner; + } + + virtual int RenderOpaqueGeometry(vtkViewport* pViewport); + virtual int RenderOverlay(vtkViewport* pViewport); + +protected: + vtkNmPlanarScaleGridProp() + : m_pOwner(nullptr) + { + } + + virtual ~vtkNmPlanarScaleGridProp() + { + m_pOwner = nullptr; + } + +private: + vtkNmPlanarScaleGridProp(const vtkNmPlanarScaleGridProp&); + void operator=(const vtkNmPlanarScaleGridProp&); + + nmVTKPlanarScaleGridLayerPrivate* m_pOwner; ///< 非所有权指针。 +}; + +vtkStandardNewMacro(vtkNmPlanarScaleGridProp); + +class nmVTKPlanarScaleGridLayerPrivate +{ +public: + nmVTKPlanarScaleGridLayerPrivate() + : m_pRootProp(vtkSmartPointer::New()), + m_pBackgroundData(vtkSmartPointer::New()), + m_pGridData(vtkSmartPointer::New()), + m_pFrameData(vtkSmartPointer::New()), + m_pXAxisData(vtkSmartPointer::New()), + m_pYAxisData(vtkSmartPointer::New()), + m_pBackgroundMapper(vtkSmartPointer::New()), + m_pGridMapper(vtkSmartPointer::New()), + m_pFrameMapper(vtkSmartPointer::New()), + m_pXAxisMapper(vtkSmartPointer::New()), + m_pYAxisMapper(vtkSmartPointer::New()), + m_pBackgroundActor(vtkSmartPointer::New()), + m_pGridActor(vtkSmartPointer::New()), + m_pFrameActor(vtkSmartPointer::New()), + m_pXAxisActor(vtkSmartPointer::New()), + m_pYAxisActor(vtkSmartPointer::New()), + m_pLabelTextProperty(vtkSmartPointer::New()), + m_dPlaneZ(0.0), + m_dScale(0.0), + m_dBackgroundZ(0.0), + m_dGridZ(0.0), + m_dAxisZ(0.0), + m_dTickLength(0.0), + m_dXTickStep(0.0), + m_dYTickStep(0.0), + m_bHasBounds(false), + m_bAxesInitialized(false), + m_bXAxisAtMaximum(false), + m_bYAxisAtMaximum(false), + m_nCameraSide(1) + { + for(int nIndex = 0; nIndex < 4; ++nIndex) { + m_dBounds[nIndex] = 0.0; + } + + configureBackgroundActor(m_pBackgroundData, + m_pBackgroundMapper, + m_pBackgroundActor); + configureLineActor(m_pGridData, m_pGridMapper, m_pGridActor, + 25.0 / 255.0, 25.0 / 255.0, + 112.0 / 255.0, 1.0f); + // 先使用 VTK 7.1 原生线型;不支持线型的后端会自然回退为实线。 + m_pGridActor->GetProperty()->SetLineStipplePattern(0xF0F0); + m_pGridActor->GetProperty()->SetLineStippleRepeatFactor(1); + configureLineActor(m_pFrameData, m_pFrameMapper, m_pFrameActor, + 25.0 / 255.0, 25.0 / 255.0, + 112.0 / 255.0, 1.5f); + m_pFrameActor->GetProperty()->SetLineStipplePattern(0xFFFF); + m_pFrameActor->GetProperty()->SetLineStippleRepeatFactor(1); + configureLineActor(m_pXAxisData, m_pXAxisMapper, m_pXAxisActor, + 1.0, 0.0, 0.0, 1.5f); + configureLineActor(m_pYAxisData, m_pYAxisMapper, m_pYAxisActor, + 0.0, 160.0 / 255.0, 0.0, 1.5f); + + m_pLabelTextProperty->SetFontFamilyToArial(); + m_pLabelTextProperty->SetFontSize(14); + m_pLabelTextProperty->SetColor( + 235.0 / 255.0, 235.0 / 255.0, 190.0 / 255.0); + m_pLabelTextProperty->SetBold(0); + m_pLabelTextProperty->SetItalic(0); + // VTK 7.1 根据浅色文字自动计算黑色对比阴影,没有 SetShadowColor 接口。 + m_pLabelTextProperty->SetShadow(1); + m_pLabelTextProperty->SetShadowOffset(1, -1); + m_pLabelTextProperty->SetBackgroundOpacity(0.0); + m_pLabelTextProperty->SetJustificationToCentered(); + m_pLabelTextProperty->SetVerticalJustificationToCentered(); + + // RootProp 是 Scene 唯一接管的主 Prop,其余 Actor 生命周期均归本图层。 + m_pRootProp->setOwner(this); + m_pRootProp->AddPart(m_pBackgroundActor); + m_pRootProp->AddPart(m_pGridActor); + m_pRootProp->AddPart(m_pFrameActor); + m_pRootProp->AddPart(m_pXAxisActor); + m_pRootProp->AddPart(m_pYAxisActor); + m_pRootProp->PickableOff(); + } + + ~nmVTKPlanarScaleGridLayerPrivate() + { + // 先断开逐帧回调目标,再从组合 Prop 移除其持有的所有子 Prop 引用。 + m_pRootProp->setOwner(nullptr); + clearLabels(); + m_pRootProp->RemovePart(m_pBackgroundActor); + m_pRootProp->RemovePart(m_pGridActor); + m_pRootProp->RemovePart(m_pFrameActor); + m_pRootProp->RemovePart(m_pXAxisActor); + m_pRootProp->RemovePart(m_pYAxisActor); + } + + vtkProp* getRootProp() const + { + return m_pRootProp; + } + + bool setBounds(double dXMinimum, + double dXMaximum, + double dYMinimum, + double dYMaximum, + double dPlaneZ) + { + std::vector vXTicks; + std::vector vYTicks; + double dXTickStep = 0.0; + double dYTickStep = 0.0; + if(!isValidPlanarBounds(dXMinimum, dXMaximum, + dYMinimum, dYMaximum, dPlaneZ) || + !calculateNiceTicks(dXMinimum, dXMaximum, + vXTicks, dXTickStep) || + !calculateNiceTicks(dYMinimum, dYMaximum, + vYTicks, dYTickStep)) { + return false; + } + + m_dBounds[0] = dXMinimum; + m_dBounds[1] = dXMaximum; + m_dBounds[2] = dYMinimum; + m_dBounds[3] = dYMaximum; + m_vXTicks = vXTicks; + m_vYTicks = vYTicks; + m_dXTickStep = dXTickStep; + m_dYTickStep = dYTickStep; + m_dPlaneZ = dPlaneZ; + + m_dScale = dXMaximum - dXMinimum; + if(dYMaximum - dYMinimum > m_dScale) { + m_dScale = dYMaximum - dYMinimum; + } + m_dTickLength = m_dScale * PLANAR_TICK_LENGTH_FACTOR; + updateDepthCoordinates(); + + rebuildBackgroundGeometry(); + rebuildGridGeometry(); + rebuildLabels(); + rebuildAxisGeometry(false, false); + m_bHasBounds = true; + return true; + } + + bool getBounds(double dBounds[4]) const + { + if(dBounds == nullptr || !m_bHasBounds) { + return false; + } + for(int nIndex = 0; nIndex < 4; ++nIndex) { + dBounds[nIndex] = m_dBounds[nIndex]; + } + return true; + } + + void updateForRenderer(vtkRenderer* pRenderer) + { + if(pRenderer == nullptr || !m_bHasBounds) { + return; + } + + // 仅当相机明确越过带容差的平面分界时翻转深度顺序,侧视附近沿用上次侧别。 + const bool bDepthChanged = updateCameraSide(pRenderer); + if(bDepthChanged) { + updateGeometryDepth(); + } + + double dCornerDisplay[4][3]; + const double dCorners[4][3] = { + { m_dBounds[0], m_dBounds[2], m_dPlaneZ }, + { m_dBounds[1], m_dBounds[2], m_dPlaneZ }, + { m_dBounds[1], m_dBounds[3], m_dPlaneZ }, + { m_dBounds[0], m_dBounds[3], m_dPlaneZ } + }; + for(int nCorner = 0; nCorner < 4; ++nCorner) { + if(!projectPoint(pRenderer, dCorners[nCorner], + dCornerDisplay[nCorner])) { + return; + } + } + + const double dMinimumYAverage = + (dCornerDisplay[0][1] + dCornerDisplay[1][1]) * 0.5; + const double dMaximumYAverage = + (dCornerDisplay[2][1] + dCornerDisplay[3][1]) * 0.5; + const double dMinimumXAverage = + (dCornerDisplay[0][0] + dCornerDisplay[3][0]) * 0.5; + const double dMaximumXAverage = + (dCornerDisplay[1][0] + dCornerDisplay[2][0]) * 0.5; + + // VTK 显示坐标原点位于左下角,平均值较小的边即当前底边或左边。 + const bool bXAxisAtMaximum = selectMaximumEdge( + dMinimumYAverage, dMaximumYAverage, + m_bXAxisAtMaximum); + const bool bYAxisAtMaximum = selectMaximumEdge( + dMinimumXAverage, dMaximumXAverage, + m_bYAxisAtMaximum); + if(!m_bAxesInitialized || + bXAxisAtMaximum != m_bXAxisAtMaximum || + bYAxisAtMaximum != m_bYAxisAtMaximum) { + rebuildAxisGeometry(bXAxisAtMaximum, bYAxisAtMaximum); + } + updateLabelPositions(pRenderer, + bXAxisAtMaximum, + bYAxisAtMaximum); + } + +private: + void updateDepthCoordinates() + { + const double dCameraSide = static_cast(m_nCameraSide); + m_dAxisZ = m_dPlaneZ + dCameraSide * + m_dScale * PLANAR_AXIS_DEPTH_FACTOR; + m_dGridZ = m_dPlaneZ - dCameraSide * + m_dScale * PLANAR_GRID_DEPTH_FACTOR; + m_dBackgroundZ = m_dPlaneZ - dCameraSide * + m_dScale * PLANAR_BACKGROUND_DEPTH_FACTOR; + } + + bool updateCameraSide(vtkRenderer* pRenderer) + { + vtkCamera* pCamera = pRenderer != nullptr + ? pRenderer->GetActiveCamera() : nullptr; + const double* pCameraPosition = pCamera != nullptr + ? pCamera->GetPosition() : nullptr; + if(pCameraPosition == nullptr || + !vtkMath::IsFinite(pCameraPosition[2])) { + return false; + } + + const double dDistanceToPlane = pCameraPosition[2] - m_dPlaneZ; + const double dTolerance = + m_dScale * PLANAR_CAMERA_SIDE_TOLERANCE_FACTOR; + int nCameraSide = m_nCameraSide; + if(dDistanceToPlane > dTolerance) { + nCameraSide = 1; + } else if(dDistanceToPlane < -dTolerance) { + nCameraSide = -1; + } + if(nCameraSide == m_nCameraSide) { + return false; + } + + m_nCameraSide = nCameraSide; + updateDepthCoordinates(); + return true; + } + + bool selectMaximumEdge(double dMinimumEdgeAverage, + double dMaximumEdgeAverage, + bool bCurrentMaximum) const + { + const double dDifference = + dMaximumEdgeAverage - dMinimumEdgeAverage; + if(dDifference < -PLANAR_EDGE_SWITCH_PIXEL_TOLERANCE) { + return true; + } + if(dDifference > PLANAR_EDGE_SWITCH_PIXEL_TOLERANCE) { + return false; + } + return m_bAxesInitialized ? bCurrentMaximum : false; + } + + void updateGeometryDepth() + { + // 相机翻面只改已有点的 Z,不重建底板、比例线拓扑或任何 VTK Prop。 + updatePolyDataZ(m_pBackgroundData, m_dBackgroundZ); + updatePolyDataZ(m_pGridData, m_dGridZ); + updatePolyDataZ(m_pFrameData, m_dAxisZ); + updatePolyDataZ(m_pXAxisData, m_dAxisZ); + updatePolyDataZ(m_pYAxisData, m_dAxisZ); + } + + void rebuildBackgroundGeometry() + { + vtkSmartPointer pPoints = + vtkSmartPointer::New(); + vtkSmartPointer pPolygons = + vtkSmartPointer::New(); + pPoints->SetDataTypeToDouble(); + + vtkIdType nPointIds[4]; + nPointIds[0] = pPoints->InsertNextPoint( + m_dBounds[0], m_dBounds[2], m_dBackgroundZ); + nPointIds[1] = pPoints->InsertNextPoint( + m_dBounds[1], m_dBounds[2], m_dBackgroundZ); + nPointIds[2] = pPoints->InsertNextPoint( + m_dBounds[1], m_dBounds[3], m_dBackgroundZ); + nPointIds[3] = pPoints->InsertNextPoint( + m_dBounds[0], m_dBounds[3], m_dBackgroundZ); + pPolygons->InsertNextCell(4, nPointIds); + applySurfaceData(m_pBackgroundData, pPoints, pPolygons); + } + + void rebuildGridGeometry() + { + vtkSmartPointer pPoints = + vtkSmartPointer::New(); + vtkSmartPointer pLines = + vtkSmartPointer::New(); + pPoints->SetDataTypeToDouble(); + + // 内部主刻度使用虚线;矩形外框由独立实线 Actor 绘制。 + const double dXTolerance = m_dXTickStep * 0.00000001; + for(std::size_t nIndex = 0; nIndex < m_vXTicks.size(); ++nIndex) { + const double dX = m_vXTicks[nIndex]; + if(dX > m_dBounds[0] + dXTolerance && + dX < m_dBounds[1] - dXTolerance) { + appendLine(pPoints, pLines, dX, m_dBounds[2], m_dGridZ, + dX, m_dBounds[3], m_dGridZ); + } + } + const double dYTolerance = m_dYTickStep * 0.00000001; + for(std::size_t nIndex = 0; nIndex < m_vYTicks.size(); ++nIndex) { + const double dY = m_vYTicks[nIndex]; + if(dY > m_dBounds[2] + dYTolerance && + dY < m_dBounds[3] - dYTolerance) { + appendLine(pPoints, pLines, m_dBounds[0], dY, m_dGridZ, + m_dBounds[1], dY, m_dGridZ); + } + } + applyLineData(m_pGridData, pPoints, pLines); + } + + void rebuildAxisGeometry(bool bXAxisAtMaximum, + bool bYAxisAtMaximum) + { + const double dXAxisY = bXAxisAtMaximum + ? m_dBounds[3] : m_dBounds[2]; + const double dXAxisDirection = bXAxisAtMaximum ? 1.0 : -1.0; + vtkSmartPointer pXPoints = + vtkSmartPointer::New(); + vtkSmartPointer pXLines = + vtkSmartPointer::New(); + pXPoints->SetDataTypeToDouble(); + appendLine(pXPoints, pXLines, + m_dBounds[0], dXAxisY, m_dAxisZ, + m_dBounds[1], dXAxisY, m_dAxisZ); + for(std::size_t nIndex = 0; nIndex < m_vXTicks.size(); ++nIndex) { + appendLine(pXPoints, pXLines, + m_vXTicks[nIndex], dXAxisY, m_dAxisZ, + m_vXTicks[nIndex], + dXAxisY + dXAxisDirection * m_dTickLength, + m_dAxisZ); + } + applyLineData(m_pXAxisData, pXPoints, pXLines); + + const double dYAxisX = bYAxisAtMaximum + ? m_dBounds[1] : m_dBounds[0]; + const double dYAxisDirection = bYAxisAtMaximum ? 1.0 : -1.0; + vtkSmartPointer pYPoints = + vtkSmartPointer::New(); + vtkSmartPointer pYLines = + vtkSmartPointer::New(); + pYPoints->SetDataTypeToDouble(); + appendLine(pYPoints, pYLines, + dYAxisX, m_dBounds[2], m_dAxisZ, + dYAxisX, m_dBounds[3], m_dAxisZ); + for(std::size_t nIndex = 0; nIndex < m_vYTicks.size(); ++nIndex) { + appendLine(pYPoints, pYLines, + dYAxisX, m_vYTicks[nIndex], m_dAxisZ, + dYAxisX + dYAxisDirection * m_dTickLength, + m_vYTicks[nIndex], m_dAxisZ); + } + applyLineData(m_pYAxisData, pYPoints, pYLines); + + // 外框只绘制当前坐标轴的两条对边,底边和左边分别由红、绿轴覆盖。 + vtkSmartPointer pFramePoints = + vtkSmartPointer::New(); + vtkSmartPointer pFrameLines = + vtkSmartPointer::New(); + pFramePoints->SetDataTypeToDouble(); + const double dOppositeXAxisY = bXAxisAtMaximum + ? m_dBounds[2] : m_dBounds[3]; + const double dOppositeYAxisX = bYAxisAtMaximum + ? m_dBounds[0] : m_dBounds[1]; + appendLine(pFramePoints, pFrameLines, + m_dBounds[0], dOppositeXAxisY, m_dAxisZ, + m_dBounds[1], dOppositeXAxisY, m_dAxisZ); + appendLine(pFramePoints, pFrameLines, + dOppositeYAxisX, m_dBounds[2], m_dAxisZ, + dOppositeYAxisX, m_dBounds[3], m_dAxisZ); + applyLineData(m_pFrameData, pFramePoints, pFrameLines); + + m_bXAxisAtMaximum = bXAxisAtMaximum; + m_bYAxisAtMaximum = bYAxisAtMaximum; + m_bAxesInitialized = true; + } + + vtkSmartPointer createLabel( + const QString& sText) + { + vtkSmartPointer pLabel = + vtkSmartPointer::New(); + const QByteArray aText = sText.toUtf8(); + pLabel->SetInput(aText.constData()); + pLabel->SetTextProperty(m_pLabelTextProperty); + pLabel->SetTextScaleModeToNone(); + pLabel->GetProperty()->SetDisplayLocationToForeground(); + pLabel->PickableOff(); + m_pRootProp->AddPart(pLabel); + return pLabel; + } + + void rebuildLabels() + { + clearLabels(); + for(std::size_t nIndex = 0; nIndex < m_vXTicks.size(); ++nIndex) { + m_vXLabels.push_back(createLabel( + formatTickValue(m_vXTicks[nIndex], + m_dXTickStep))); + } + for(std::size_t nIndex = 0; nIndex < m_vYTicks.size(); ++nIndex) { + m_vYLabels.push_back(createLabel( + formatTickValue(m_vYTicks[nIndex], + m_dYTickStep))); + } + } + + void clearLabels() + { + for(std::size_t nIndex = 0; nIndex < m_vXLabels.size(); ++nIndex) { + m_pRootProp->RemovePart(m_vXLabels[nIndex]); + } + for(std::size_t nIndex = 0; nIndex < m_vYLabels.size(); ++nIndex) { + m_pRootProp->RemovePart(m_vYLabels[nIndex]); + } + m_vXLabels.clear(); + m_vYLabels.clear(); + } + + bool projectPoint(vtkRenderer* pRenderer, + const double dWorld[3], + double dDisplay[3]) const + { + if(pRenderer == nullptr || dWorld == nullptr || dDisplay == nullptr) { + return false; + } + pRenderer->SetWorldPoint( + dWorld[0], dWorld[1], dWorld[2], 1.0); + pRenderer->WorldToDisplay(); + const double* pDisplay = pRenderer->GetDisplayPoint(); + if(pDisplay == nullptr || + !vtkMath::IsFinite(pDisplay[0]) || + !vtkMath::IsFinite(pDisplay[1]) || + !vtkMath::IsFinite(pDisplay[2])) { + return false; + } + dDisplay[0] = pDisplay[0]; + dDisplay[1] = pDisplay[1]; + dDisplay[2] = pDisplay[2]; + return true; + } + + bool calculateDisplayDirection(vtkRenderer* pRenderer, + const double dAnchor[3], + const double dOuterPoint[3], + double dDirection[2]) const + { + double dAnchorDisplay[3]; + double dOuterDisplay[3]; + if(!projectPoint(pRenderer, dAnchor, dAnchorDisplay) || + !projectPoint(pRenderer, dOuterPoint, dOuterDisplay)) { + return false; + } + const double dX = dOuterDisplay[0] - dAnchorDisplay[0]; + const double dY = dOuterDisplay[1] - dAnchorDisplay[1]; + const double dLength = std::sqrt(dX * dX + dY * dY); + if(!vtkMath::IsFinite(dLength) || dLength < 0.000001) { + return false; + } + dDirection[0] = dX / dLength; + dDirection[1] = dY / dLength; + return true; + } + + void updateLabelPositions(vtkRenderer* pRenderer, + bool bXAxisAtMaximum, + bool bYAxisAtMaximum) + { + const double dXAxisY = bXAxisAtMaximum + ? m_dBounds[3] : m_dBounds[2]; + const double dXAxisDirection = bXAxisAtMaximum ? 1.0 : -1.0; + const double dXAxisAnchor[3] = { + (m_dBounds[0] + m_dBounds[1]) * 0.5, + dXAxisY, + m_dAxisZ + }; + const double dXAxisOuterPoint[3] = { + dXAxisAnchor[0], + dXAxisY + dXAxisDirection * m_dTickLength, + m_dAxisZ + }; + double dXAxisDisplayDirection[2] = { 0.0, -1.0 }; + calculateDisplayDirection(pRenderer, + dXAxisAnchor, + dXAxisOuterPoint, + dXAxisDisplayDirection); + const int nXAxisOffsetX = roundedPixelOffset( + dXAxisDisplayDirection[0] * PLANAR_LABEL_PIXEL_GAP); + const int nXAxisOffsetY = roundedPixelOffset( + dXAxisDisplayDirection[1] * PLANAR_LABEL_PIXEL_GAP); + + const double dXAxisMinimumPoint[3] = { + m_dBounds[0], dXAxisY, m_dAxisZ + }; + const double dXAxisMaximumPoint[3] = { + m_dBounds[1], dXAxisY, m_dAxisZ + }; + double dXAxisPositiveDirection[2] = { 1.0, 0.0 }; + calculateDisplayDirection(pRenderer, + dXAxisMinimumPoint, + dXAxisMaximumPoint, + dXAxisPositiveDirection); + const double dXAxisCornerValue = bYAxisAtMaximum + ? m_dBounds[1] : m_dBounds[0]; + const double dXAxisCornerDirection = bYAxisAtMaximum ? -1.0 : 1.0; + const double dXAxisTolerance = m_dXTickStep * 0.00000001; + for(std::size_t nIndex = 0; nIndex < m_vXLabels.size(); ++nIndex) { + const double dLabelWorld[3] = { + m_vXTicks[nIndex], dXAxisOuterPoint[1], m_dAxisZ + }; + double dLabelDisplay[3]; + if(!projectPoint(pRenderer, dLabelWorld, dLabelDisplay)) { + m_vXLabels[nIndex]->SetVisibility(0); + continue; + } + int nOffsetX = nXAxisOffsetX; + int nOffsetY = nXAxisOffsetY; + if(std::fabs(m_vXTicks[nIndex] - dXAxisCornerValue) <= + dXAxisTolerance) { + nOffsetX += roundedPixelOffset( + dXAxisPositiveDirection[0] * + dXAxisCornerDirection * + PLANAR_CORNER_LABEL_PIXEL_SHIFT); + nOffsetY += roundedPixelOffset( + dXAxisPositiveDirection[1] * + dXAxisCornerDirection * + PLANAR_CORNER_LABEL_PIXEL_SHIFT); + } + m_vXLabels[nIndex]->SetDisplayPosition( + roundedPixelOffset(dLabelDisplay[0]) + nOffsetX, + roundedPixelOffset(dLabelDisplay[1]) + nOffsetY); + m_vXLabels[nIndex]->SetVisibility(1); + } + + const double dYAxisX = bYAxisAtMaximum + ? m_dBounds[1] : m_dBounds[0]; + const double dYAxisDirection = bYAxisAtMaximum ? 1.0 : -1.0; + const double dYAxisAnchor[3] = { + dYAxisX, + (m_dBounds[2] + m_dBounds[3]) * 0.5, + m_dAxisZ + }; + const double dYAxisOuterPoint[3] = { + dYAxisX + dYAxisDirection * m_dTickLength, + dYAxisAnchor[1], + m_dAxisZ + }; + double dYAxisDisplayDirection[2] = { -1.0, 0.0 }; + calculateDisplayDirection(pRenderer, + dYAxisAnchor, + dYAxisOuterPoint, + dYAxisDisplayDirection); + const int nYAxisOffsetX = roundedPixelOffset( + dYAxisDisplayDirection[0] * PLANAR_LABEL_PIXEL_GAP); + const int nYAxisOffsetY = roundedPixelOffset( + dYAxisDisplayDirection[1] * PLANAR_LABEL_PIXEL_GAP); + + const double dYAxisMinimumPoint[3] = { + dYAxisX, m_dBounds[2], m_dAxisZ + }; + const double dYAxisMaximumPoint[3] = { + dYAxisX, m_dBounds[3], m_dAxisZ + }; + double dYAxisPositiveDirection[2] = { 0.0, 1.0 }; + calculateDisplayDirection(pRenderer, + dYAxisMinimumPoint, + dYAxisMaximumPoint, + dYAxisPositiveDirection); + const double dYAxisCornerValue = bXAxisAtMaximum + ? m_dBounds[3] : m_dBounds[2]; + const double dYAxisCornerDirection = bXAxisAtMaximum ? -1.0 : 1.0; + const double dYAxisTolerance = m_dYTickStep * 0.00000001; + for(std::size_t nIndex = 0; nIndex < m_vYLabels.size(); ++nIndex) { + const double dLabelWorld[3] = { + dYAxisOuterPoint[0], m_vYTicks[nIndex], m_dAxisZ + }; + double dLabelDisplay[3]; + if(!projectPoint(pRenderer, dLabelWorld, dLabelDisplay)) { + m_vYLabels[nIndex]->SetVisibility(0); + continue; + } + int nOffsetX = nYAxisOffsetX; + int nOffsetY = nYAxisOffsetY; + if(std::fabs(m_vYTicks[nIndex] - dYAxisCornerValue) <= + dYAxisTolerance) { + nOffsetX += roundedPixelOffset( + dYAxisPositiveDirection[0] * + dYAxisCornerDirection * + PLANAR_CORNER_LABEL_PIXEL_SHIFT); + nOffsetY += roundedPixelOffset( + dYAxisPositiveDirection[1] * + dYAxisCornerDirection * + PLANAR_CORNER_LABEL_PIXEL_SHIFT); + } + m_vYLabels[nIndex]->SetDisplayPosition( + roundedPixelOffset(dLabelDisplay[0]) + nOffsetX, + roundedPixelOffset(dLabelDisplay[1]) + nOffsetY); + m_vYLabels[nIndex]->SetVisibility(1); + } + } + + vtkSmartPointer m_pRootProp; + vtkSmartPointer m_pBackgroundData; + vtkSmartPointer m_pGridData; + vtkSmartPointer m_pFrameData; + vtkSmartPointer m_pXAxisData; + vtkSmartPointer m_pYAxisData; + vtkSmartPointer m_pBackgroundMapper; + vtkSmartPointer m_pGridMapper; + vtkSmartPointer m_pFrameMapper; + vtkSmartPointer m_pXAxisMapper; + vtkSmartPointer m_pYAxisMapper; + vtkSmartPointer m_pBackgroundActor; + vtkSmartPointer m_pGridActor; + vtkSmartPointer m_pFrameActor; + vtkSmartPointer m_pXAxisActor; + vtkSmartPointer m_pYAxisActor; + vtkSmartPointer m_pLabelTextProperty; + std::vector > m_vXLabels; + std::vector > m_vYLabels; + std::vector m_vXTicks; + std::vector m_vYTicks; + double m_dBounds[4]; + double m_dPlaneZ; + double m_dScale; + double m_dBackgroundZ; + double m_dGridZ; + double m_dAxisZ; + double m_dTickLength; + double m_dXTickStep; + double m_dYTickStep; + bool m_bHasBounds; + bool m_bAxesInitialized; + bool m_bXAxisAtMaximum; + bool m_bYAxisAtMaximum; + int m_nCameraSide; +}; + +int vtkNmPlanarScaleGridProp::RenderOpaqueGeometry(vtkViewport* pViewport) +{ + if(m_pOwner != nullptr) { + m_pOwner->updateForRenderer(vtkRenderer::SafeDownCast(pViewport)); + } + return this->Superclass::RenderOpaqueGeometry(pViewport); +} + +int vtkNmPlanarScaleGridProp::RenderOverlay(vtkViewport* pViewport) +{ + if(m_pOwner != nullptr) { + // Overlay 阶段再次同步屏幕坐标,确保文字使用本帧最终相机矩阵。 + m_pOwner->updateForRenderer(vtkRenderer::SafeDownCast(pViewport)); + } + return this->Superclass::RenderOverlay(pViewport); +} + +nmVTKPlanarScaleGridLayer::nmVTKPlanarScaleGridLayer( + const QString& sLayerId) + : nmVTKRenderLayer(sLayerId), + m_pPrivate(new nmVTKPlanarScaleGridLayerPrivate) +{ + Q_ASSERT(isPlanarScaleGridGuiThread()); + setRenderProp(m_pPrivate->getRootProp()); +} + +nmVTKPlanarScaleGridLayer::~nmVTKPlanarScaleGridLayer() +{ + Q_ASSERT(isPlanarScaleGridGuiThread()); + // 先从 Renderer 移除组合 Prop,再清除其逐帧更新目标和子 Prop 引用。 + detach(); + delete m_pPrivate; + m_pPrivate = nullptr; +} + +bool nmVTKPlanarScaleGridLayer::setBounds( + double dXMinimum, + double dXMaximum, + double dYMinimum, + double dYMaximum, + double dPlaneZ) +{ + Q_ASSERT(isPlanarScaleGridGuiThread()); + return isPlanarScaleGridGuiThread() && m_pPrivate != nullptr && + m_pPrivate->setBounds(dXMinimum, dXMaximum, + dYMinimum, dYMaximum, dPlaneZ); +} + +bool nmVTKPlanarScaleGridLayer::getBounds(double dBounds[4]) const +{ + return m_pPrivate != nullptr && m_pPrivate->getBounds(dBounds); +} diff --git a/Src/nmNum/nmRender/nmWxVTKRenderContainerWidget.cpp b/Src/nmNum/nmRender/nmWxVTKRenderContainerWidget.cpp index fc583fb4..b7b97502 100644 --- a/Src/nmNum/nmRender/nmWxVTKRenderContainerWidget.cpp +++ b/Src/nmNum/nmRender/nmWxVTKRenderContainerWidget.cpp @@ -21,6 +21,7 @@ #include VTK_MODULE_INIT(vtkRenderingOpenGL) +VTK_MODULE_INIT(vtkRenderingFreeType) VTK_MODULE_INIT(vtkInteractionStyle) #include diff --git a/Src/nmNum/nmSubWxs/nmPebiGridSceneController.cpp b/Src/nmNum/nmSubWxs/nmPebiGridSceneController.cpp index e83ed20b..daf4cf60 100644 --- a/Src/nmNum/nmSubWxs/nmPebiGridSceneController.cpp +++ b/Src/nmNum/nmSubWxs/nmPebiGridSceneController.cpp @@ -2,8 +2,8 @@ #include "nmPebiGridViewData.h" -#include "nmVTKBoundsGridLayer.h" #include "nmVTKMeshLayer.h" +#include "nmVTKPlanarScaleGridLayer.h" #include "nmVTKScalarBarLayer.h" #include "nmVTKScene.h" #include "nmWxVTKRenderContainerWidget.h" @@ -11,7 +11,6 @@ #include #include -#include #include #include #include @@ -54,7 +53,7 @@ public: : m_pContainer(pRenderContainer), m_pMeshLayer(nullptr), m_pScalarBarLayer(nullptr), - m_pBoundsGridLayer(nullptr), + m_pPlanarScaleGridLayer(nullptr), m_bGridLinesVisible(true), m_bScalarBarVisible(true), m_bBoundsGridVisible(false), @@ -82,24 +81,25 @@ public: return false; } m_pMeshLayer = pLayer; - // 固定面色、边线颜色和完全不透明外观沿用旧 PEBI 预览。 + // 无属性时使用稳定灰色;Material 有效时仍由 MeshLayer 的 LUT 着色。 m_pMeshLayer->getActor()->GetProperty()->SetColor( - 0.0, 120.0 / 255.0, 215.0 / 255.0); + 211.0 / 255.0, 211.0 / 255.0, + 211.0 / 255.0); m_pMeshLayer->getActor()->GetProperty()->SetEdgeColor( 0.0, 0.0, 0.0); + // 仅关闭当前 PEBI 预览 Actor 的光照,避免固定面色随视角变化。 + m_pMeshLayer->getActor()->GetProperty()->LightingOff(); } - if(m_pBoundsGridLayer == nullptr) { - nmVTKBoundsGridLayer* pLayer = - new nmVTKBoundsGridLayer("pebi.grid.bounds_grid"); - pLayer->setMode(nmVTKBoundsGridLayer::Mode_PlanarXY); - pLayer->setCamera(pRenderer->GetActiveCamera()); + if(m_pPlanarScaleGridLayer == nullptr) { + nmVTKPlanarScaleGridLayer* pLayer = + new nmVTKPlanarScaleGridLayer("pebi.grid.scale_grid"); pLayer->setVisible(false); if(!pScene->addLayer(pLayer)) { delete pLayer; return false; } - m_pBoundsGridLayer = pLayer; + m_pPlanarScaleGridLayer = pLayer; } return true; } @@ -175,7 +175,7 @@ public: const QString& sRequestedProperty, const double dBounds[6]) { - if(m_pMeshLayer == nullptr || m_pBoundsGridLayer == nullptr || + if(m_pMeshLayer == nullptr || m_pPlanarScaleGridLayer == nullptr || dBounds == nullptr) { return false; } @@ -189,15 +189,15 @@ public: m_pMeshLayer->setEdgeVisibility(m_bGridLinesVisible); m_pMeshLayer->setVisible(true); - if(!m_pBoundsGridLayer->setBounds( + if(!m_pPlanarScaleGridLayer->setBounds( dBounds[0], dBounds[1], dBounds[2], - dBounds[3], dBounds[4], dBounds[5])) { + dBounds[3], dBounds[4])) { m_pMeshLayer->setDataSet(nullptr); m_pMeshLayer->setVisible(false); m_oViewData.clear(); return false; } - m_pBoundsGridLayer->setVisible(m_bBoundsGridVisible); + m_pPlanarScaleGridLayer->setVisible(m_bBoundsGridVisible); m_bHasData = true; // 属性不可用只回退固定面色,不影响已经验证成功的网格几何提交。 @@ -216,8 +216,8 @@ public: if(m_pScalarBarLayer != nullptr) { m_pScalarBarLayer->setVisible(false); } - if(m_pBoundsGridLayer != nullptr) { - m_pBoundsGridLayer->setVisible(false); + if(m_pPlanarScaleGridLayer != nullptr) { + m_pPlanarScaleGridLayer->setVisible(false); } m_oViewData.clear(); m_sPropertyName.clear(); @@ -227,7 +227,7 @@ public: nmWxVTKRenderContainerWidget* m_pContainer; ///< 非所有权指针。 nmVTKMeshLayer* m_pMeshLayer; ///< Scene 所有,控制器只观察。 nmVTKScalarBarLayer* m_pScalarBarLayer; ///< Scene 所有,按需创建。 - nmVTKBoundsGridLayer* m_pBoundsGridLayer; ///< Scene 所有。 + nmVTKPlanarScaleGridLayer* m_pPlanarScaleGridLayer; ///< Scene 所有。 nmPebiGridViewData m_oViewData; bool m_bGridLinesVisible; bool m_bScalarBarVisible; ///< 用户偏好,无属性时仍保留。 @@ -252,7 +252,7 @@ nmPebiGridSceneController::~nmPebiGridSceneController() m_pPrivate->m_pContainer = nullptr; m_pPrivate->m_pMeshLayer = nullptr; m_pPrivate->m_pScalarBarLayer = nullptr; - m_pPrivate->m_pBoundsGridLayer = nullptr; + m_pPrivate->m_pPlanarScaleGridLayer = nullptr; delete m_pPrivate; m_pPrivate = nullptr; } @@ -394,8 +394,8 @@ void nmPebiGridSceneController::setBoundsGridVisible(bool bVisible) return; } m_pPrivate->m_bBoundsGridVisible = bVisible; - if(m_pPrivate->m_pBoundsGridLayer != nullptr) { - m_pPrivate->m_pBoundsGridLayer->setVisible( + if(m_pPrivate->m_pPlanarScaleGridLayer != nullptr) { + m_pPrivate->m_pPlanarScaleGridLayer->setVisible( bVisible && m_pPrivate->m_bHasData); } if(m_pPrivate->m_bHasData && m_pPrivate->m_pContainer != nullptr) { diff --git a/Src/nmNum/nmSubWxs/nmWxPebiGridViewWidget.cpp b/Src/nmNum/nmSubWxs/nmWxPebiGridViewWidget.cpp index d70516c7..b1815e59 100644 --- a/Src/nmNum/nmSubWxs/nmWxPebiGridViewWidget.cpp +++ b/Src/nmNum/nmSubWxs/nmWxPebiGridViewWidget.cpp @@ -107,8 +107,8 @@ void nmWxPebiGridViewWidget::createUi() m_pScalarBarAction->setCheckable(true); m_pBoundsGridAction = new QAction( QIcon(gridViewIconPath("CalGrid.png")), - tr("Coordinate Grid"), this); - m_pBoundsGridAction->setToolTip(tr("Coordinate Grid")); + tr("Scale Grid"), this); + m_pBoundsGridAction->setToolTip(tr("Scale Grid")); m_pBoundsGridAction->setCheckable(true); if(pCommandController != nullptr) { diff --git a/Src4/nmNum/nmRender/nmRender.pro b/Src4/nmNum/nmRender/nmRender.pro index ca70498e..f2f7ebfd 100644 --- a/Src4/nmNum/nmRender/nmRender.pro +++ b/Src4/nmNum/nmRender/nmRender.pro @@ -21,6 +21,7 @@ SOURCES += $${wtSrc}/nmNum/nmRender/nmVTKCameraController.cpp \ $${wtSrc}/nmNum/nmRender/nmVTKInteractionManager.cpp \ $${wtSrc}/nmNum/nmRender/nmVTKOrientationMarkerController.cpp \ $${wtSrc}/nmNum/nmRender/nmVTKMeshLayer.cpp \ + $${wtSrc}/nmNum/nmRender/nmVTKPlanarScaleGridLayer.cpp \ $${wtSrc}/nmNum/nmRender/nmVTKRenderLayer.cpp \ $${wtSrc}/nmNum/nmRender/nmVTKScalarBarLayer.cpp \ $${wtSrc}/nmNum/nmRender/nmVTKScene.cpp \ @@ -34,6 +35,7 @@ HEADERS += $${wtInclude}/nmNum/nmRender/nmRender_global.h \ $${wtInclude}/nmNum/nmRender/nmVTKInteractionManager.h \ $${wtInclude}/nmNum/nmRender/nmVTKOrientationMarkerController.h \ $${wtInclude}/nmNum/nmRender/nmVTKMeshLayer.h \ + $${wtInclude}/nmNum/nmRender/nmVTKPlanarScaleGridLayer.h \ $${wtInclude}/nmNum/nmRender/nmVTKRenderLayer.h \ $${wtInclude}/nmNum/nmRender/nmVTKScalarBarLayer.h \ $${wtInclude}/nmNum/nmRender/nmVTKScene.h \