#include "nmVolumeSliceInteraction.h" #include "nmWxVTKRenderContainerWidget.h" #include #include #include #include #include #include #include #include #include #include namespace { /** @brief VTK 7.1 的 DrawPlaneOff 仍拾取不可见填充面,需将拾取限定到可见轮廓和手柄。 */ class nmVolumePlaneRepresentation : public vtkImplicitPlaneRepresentation { public: /** @brief 按 VTK 引用计数规则创建本地预览表示。 */ static nmVolumePlaneRepresentation* New(); vtkTypeMacro(nmVolumePlaneRepresentation, vtkImplicitPlaneRepresentation); /** @brief 中心或轮廓沿法向移动平面,法向箭头继续使用 VTK 内建旋转。 */ virtual int ComputeInteractionState(int nX, int nY, int nModify = 0) { const bool bMoving = this->InteractionState == Moving; const int nState = this->Superclass::ComputeInteractionState(nX, nY, nModify); if(bMoving && (nState == MovingOrigin || this->Picker->GetActor() == this->EdgesActor)) { this->SetInteractionState(Pushing); this->SetRepresentationState(Pushing); } return this->InteractionState; } protected: /** @brief 去除填充面和固定包围盒的拾取,避免在模型空白区域抢占相机操作。 */ nmVolumePlaneRepresentation() { this->Picker->DeletePickList(this->CutActor); this->Picker->DeletePickList(this->OutlineActor); this->Picker->AddPickList(this->EdgesActor); } }; vtkStandardNewMacro(nmVolumePlaneRepresentation); /** @brief 在隐藏、导入或退出模式时安全取消未松手的拖动,不保留 Interactor 焦点。 */ class nmVolumePlaneWidget : public vtkImplicitPlaneWidget2 { public: /** @brief 创建本窗口独占的交互 Widget。 */ static nmVolumePlaneWidget* New(); vtkTypeMacro(nmVolumePlaneWidget, vtkImplicitPlaneWidget2); /** @brief 禁用时先解除主动拖动及焦点,不能发出一次伪造的松手应用。 */ virtual void SetEnabled(int nEnabled) { if(!nEnabled && this->Enabled && this->WidgetState == Active) { this->ReleaseFocus(); this->WidgetState = Start; this->GetImplicitPlaneRepresentation()->SetRepresentationState(vtkImplicitPlaneRepresentation::Outside); this->EndInteraction(); } this->Superclass::SetEnabled(nEnabled); } }; vtkStandardNewMacro(nmVolumePlaneWidget); } nmVolumeSliceInteraction::nmVolumeSliceInteraction(nmWxVTKRenderContainerWidget* pContainer, QObject* pParent) : QObject(pParent), m_pContainer(pContainer), m_pRepresentation(vtkSmartPointer::New()), m_pWidget(vtkSmartPointer::New()), m_pCallback(vtkSmartPointer::New()), m_nGeneration(0), m_nPendingGeneration(0), m_bQueued(false), m_bPendingApply(false) { m_pRepresentation->SetPlaceFactor(1.0); m_pRepresentation->DrawPlaneOff(); m_pRepresentation->OutlineTranslationOff(); m_pRepresentation->ScaleEnabledOff(); m_pRepresentation->OutsideBoundsOn(); m_pRepresentation->ConstrainToWidgetBoundsOff(); m_pRepresentation->GetOutlineProperty()->SetColor(0.45, 0.55, 0.65); m_pWidget->SetRepresentation(m_pRepresentation); m_pWidget->KeyPressActivationOff(); // 中键、右键及键盘保留给现有相机和菜单;仅左键手柄用于移动或旋转平面。 vtkWidgetEventTranslator* pTranslator = m_pWidget->GetEventTranslator(); pTranslator->RemoveTranslation(vtkCommand::MiddleButtonPressEvent); pTranslator->RemoveTranslation(vtkCommand::MiddleButtonReleaseEvent); pTranslator->RemoveTranslation(vtkCommand::RightButtonPressEvent); pTranslator->RemoveTranslation(vtkCommand::RightButtonReleaseEvent); pTranslator->RemoveTranslation(vtkCommand::KeyPressEvent); pTranslator->RemoveTranslation(vtkCommand::KeyReleaseEvent); m_pCallback->SetClientData(this); m_pCallback->SetCallback(onPlaneEvent); m_pWidget->AddObserver(vtkCommand::InteractionEvent, m_pCallback); m_pWidget->AddObserver(vtkCommand::EndInteractionEvent, m_pCallback); } nmVolumeSliceInteraction::~nmVolumeSliceInteraction() { unbind(); m_pWidget->RemoveObservers(vtkCommand::InteractionEvent, m_pCallback); m_pWidget->RemoveObservers(vtkCommand::EndInteractionEvent, m_pCallback); m_pCallback->SetClientData(nullptr); } void nmVolumeSliceInteraction::bind(const QString& sDataId, const double dBounds[6], const nmVolumeSliceSettings& oSettings) { unbind(); if(m_pContainer == nullptr || sDataId.isEmpty() || dBounds == nullptr) return; m_sDataId = sDataId; m_pWidget->SetInteractor(m_pContainer->getInteractor()); m_pWidget->SetCurrentRenderer(m_pContainer->getRenderer()); double dPlacement[6]; for(int nIndex = 0; nIndex < 6; ++nIndex) dPlacement[nIndex] = dBounds[nIndex]; m_pRepresentation->PlaceWidget(dPlacement); setPreview(oSettings); } void nmVolumeSliceInteraction::unbind() { ++m_nGeneration; m_bPendingApply = false; m_pWidget->SetEnabled(0); m_pWidget->SetInteractor(nullptr); m_pWidget->SetCurrentRenderer(nullptr); m_sDataId.clear(); } void nmVolumeSliceInteraction::setPreview(const nmVolumeSliceSettings& oSettings) { nmVolumeSliceSettings oNormalized = oSettings; if(!oNormalized.normalize()) return; // 数值输入、重置或成功应用取代旧拖动草稿,不能再交付尚未处理的旧松手事件。 ++m_nGeneration; m_bPendingApply = false; m_oPreview = oNormalized; m_pRepresentation->SetOrigin(m_oPreview.m_dOrigin); m_pRepresentation->SetNormal(m_oPreview.m_dNormal); } void nmVolumeSliceInteraction::setEnabled(bool bEnabled) { bEnabled = bEnabled && !m_sDataId.isEmpty(); if(isEnabled() == bEnabled) return; if(!bEnabled) { ++m_nGeneration; m_bPendingApply = false; } // VTK 禁用 Widget 时会清空 CurrentRenderer;恢复时不能依赖鼠标仍停留在视口内。 if(bEnabled) m_pWidget->SetCurrentRenderer(m_pContainer->getRenderer()); m_pWidget->SetEnabled(bEnabled ? 1 : 0); } bool nmVolumeSliceInteraction::isEnabled() const { return m_pWidget->GetEnabled() != 0; } void nmVolumeSliceInteraction::onPlaneEvent(vtkObject* pCaller, unsigned long nEvent, void* pClientData, void*) { nmVolumeSliceInteraction* pSelf = static_cast(pClientData); if(pSelf == nullptr || !pSelf->isEnabled() || pCaller != pSelf->m_pWidget.GetPointer()) return; pSelf->m_oPending = pSelf->m_oPreview; pSelf->m_pRepresentation->GetOrigin(pSelf->m_oPending.m_dOrigin); pSelf->m_pRepresentation->GetNormal(pSelf->m_oPending.m_dNormal); pSelf->m_nPendingGeneration = pSelf->m_nGeneration; pSelf->m_bPendingApply = pSelf->m_bPendingApply || nEvent == vtkCommand::EndInteractionEvent; if(!pSelf->m_bQueued) { pSelf->m_bQueued = true; QMetaObject::invokeMethod(pSelf, "deliverPendingEvent", Qt::QueuedConnection); } } void nmVolumeSliceInteraction::deliverPendingEvent() { m_bQueued = false; const bool bApply = m_bPendingApply; m_bPendingApply = false; if(!isEnabled() || m_nPendingGeneration != m_nGeneration) return; const unsigned int nGeneration = m_nGeneration; const QString sDataId = m_sDataId; m_oPreview = m_oPending; emit previewChanged(sDataId, m_oPreview); // previewChanged 的接收者可能关闭窗口或切换模式,必须再次检查代次。 if(bApply && isEnabled() && nGeneration == m_nGeneration) emit applyRequested(sDataId, m_oPreview); }