|
|
#include "nmPebiResultPlaybackController.h"
|
|
|
|
|
|
#include "nmPebiResultSceneController.h"
|
|
|
|
|
|
#include <QCoreApplication>
|
|
|
#include <QTimer>
|
|
|
#include <QtGlobal>
|
|
|
|
|
|
namespace {
|
|
|
|
|
|
const char* const PRESSURE_PROPERTY_ID = "pressure";
|
|
|
|
|
|
/* Qt 4.8 的 lupdate 无法从变量参数中推导翻译原文,必须显式登记。 */
|
|
|
static const char* const s_pPlaybackTranslationMarkers[] =
|
|
|
{
|
|
|
QT_TRANSLATE_NOOP("nmPebiResultPlaybackController",
|
|
|
"Result playback is unavailable."),
|
|
|
QT_TRANSLATE_NOOP("nmPebiResultPlaybackController",
|
|
|
"The selected result property is unavailable."),
|
|
|
QT_TRANSLATE_NOOP("nmPebiResultPlaybackController",
|
|
|
"The selected result frame is unavailable."),
|
|
|
QT_TRANSLATE_NOOP("nmPebiResultPlaybackController",
|
|
|
"Playback speed must be a positive integer.")
|
|
|
};
|
|
|
|
|
|
QString playbackText(const char* pText)
|
|
|
{
|
|
|
Q_UNUSED(s_pPlaybackTranslationMarkers);
|
|
|
return QCoreApplication::translate(
|
|
|
"nmPebiResultPlaybackController", pText);
|
|
|
}
|
|
|
|
|
|
bool isPressureProperty(const QString& sPropertyId)
|
|
|
{
|
|
|
return sPropertyId == QString::fromLatin1(PRESSURE_PROPERTY_ID);
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
nmPebiResultPlaybackController::nmPebiResultPlaybackController(
|
|
|
nmPebiResultSceneController* pSceneController,
|
|
|
QObject* pParent)
|
|
|
: QObject(pParent),
|
|
|
m_pSceneController(pSceneController),
|
|
|
m_pTimer(new QTimer(this)),
|
|
|
m_nObservedBindingRevision(0),
|
|
|
m_nPlaybackBindingRevision(0),
|
|
|
m_nLastPressureFrameIndex(0),
|
|
|
m_nStepsPerSecond(DefaultPlaybackSpeed),
|
|
|
m_bLoopEnabled(false),
|
|
|
m_bPlaying(false)
|
|
|
{
|
|
|
Q_ASSERT(m_pSceneController != nullptr);
|
|
|
m_pTimer->setInterval(calculatePlaybackInterval());
|
|
|
connect(m_pTimer, SIGNAL(timeout()),
|
|
|
this, SLOT(onPlaybackTimeout()));
|
|
|
}
|
|
|
|
|
|
nmPebiResultPlaybackController::~nmPebiResultPlaybackController()
|
|
|
{
|
|
|
stopTimerWithoutSignal();
|
|
|
m_pSceneController = nullptr;
|
|
|
}
|
|
|
|
|
|
void nmPebiResultPlaybackController::synchronizeFromScene()
|
|
|
{
|
|
|
const quint64 nPreviousBindingRevision =
|
|
|
m_nObservedBindingRevision;
|
|
|
const bool bAvailable = synchronizeSceneIdentity();
|
|
|
const bool bSnapshotChanged = bAvailable &&
|
|
|
nPreviousBindingRevision != m_nObservedBindingRevision;
|
|
|
|
|
|
// 无快照或新快照必须终止旧回放;同快照同步只校正 Scene 已提交状态。
|
|
|
if(!bAvailable || bSnapshotChanged) {
|
|
|
stopTimerWithoutSignal();
|
|
|
} else {
|
|
|
const QString sPropertyId = getCurrentPropertyId();
|
|
|
const int nFrameCount = getPropertyFrameCount(sPropertyId);
|
|
|
if(!isPropertyTimeDependent(sPropertyId) || nFrameCount <= 1) {
|
|
|
stopTimerWithoutSignal();
|
|
|
}
|
|
|
|
|
|
// 同一快照的显式同步以 Scene 的最后成功压力帧校正记忆值;
|
|
|
// 静态属性同步则保留此前压力帧,便于切回压力时恢复。
|
|
|
if(isPressureProperty(sPropertyId) &&
|
|
|
getCurrentFrameIndex() >= 0) {
|
|
|
m_nLastPressureFrameIndex = getCurrentFrameIndex();
|
|
|
}
|
|
|
}
|
|
|
|
|
|
emit stateChanged();
|
|
|
}
|
|
|
|
|
|
bool nmPebiResultPlaybackController::selectProperty(
|
|
|
const QString& sPropertyId)
|
|
|
{
|
|
|
stopTimerWithoutSignal();
|
|
|
if(!synchronizeSceneIdentity() ||
|
|
|
!getAvailablePropertyIds().contains(sPropertyId)) {
|
|
|
return finishFailedOperation(playbackText(
|
|
|
"The selected result property is unavailable."));
|
|
|
}
|
|
|
|
|
|
const QString sPreviousPropertyId = getCurrentPropertyId();
|
|
|
int nCandidateLastPressureFrame = m_nLastPressureFrameIndex;
|
|
|
if(isPressureProperty(sPreviousPropertyId) &&
|
|
|
getCurrentFrameIndex() >= 0) {
|
|
|
// 切换前只保存局部候选;Scene 提交失败时不改变正式记忆值。
|
|
|
nCandidateLastPressureFrame = getCurrentFrameIndex();
|
|
|
}
|
|
|
|
|
|
int nTargetFrame = 0;
|
|
|
if(isPressureProperty(sPropertyId)) {
|
|
|
const int nFrameCount = getPropertyFrameCount(sPropertyId);
|
|
|
if(nFrameCount <= 0) {
|
|
|
return finishFailedOperation(playbackText(
|
|
|
"The selected result property is unavailable."));
|
|
|
}
|
|
|
nTargetFrame = qBound(
|
|
|
0, nCandidateLastPressureFrame, nFrameCount - 1);
|
|
|
}
|
|
|
|
|
|
QString sError;
|
|
|
if(!m_pSceneController->applyPropertyFrame(
|
|
|
sPropertyId, nTargetFrame, &sError)) {
|
|
|
return finishFailedOperation(sError);
|
|
|
}
|
|
|
|
|
|
// 属性和帧均已由 Scene 原子提交,此时才提交最后压力帧候选。
|
|
|
if(isPressureProperty(sPropertyId)) {
|
|
|
m_nLastPressureFrameIndex = nTargetFrame;
|
|
|
} else if(isPressureProperty(sPreviousPropertyId)) {
|
|
|
m_nLastPressureFrameIndex = nCandidateLastPressureFrame;
|
|
|
}
|
|
|
emit stateChanged();
|
|
|
return true;
|
|
|
}
|
|
|
|
|
|
bool nmPebiResultPlaybackController::selectFrame(int nFrameIndex)
|
|
|
{
|
|
|
stopTimerWithoutSignal();
|
|
|
if(!synchronizeSceneIdentity()) {
|
|
|
return finishFailedOperation(playbackText(
|
|
|
"Result playback is unavailable."));
|
|
|
}
|
|
|
|
|
|
const QString sPropertyId = getCurrentPropertyId();
|
|
|
const int nFrameCount = getPropertyFrameCount(sPropertyId);
|
|
|
if(!isPropertyTimeDependent(sPropertyId) ||
|
|
|
nFrameIndex < 0 || nFrameIndex >= nFrameCount) {
|
|
|
return finishFailedOperation(playbackText(
|
|
|
"The selected result frame is unavailable."));
|
|
|
}
|
|
|
|
|
|
QString sError;
|
|
|
if(!m_pSceneController->applyPropertyFrame(
|
|
|
sPropertyId, nFrameIndex, &sError)) {
|
|
|
return finishFailedOperation(sError);
|
|
|
}
|
|
|
|
|
|
if(isPressureProperty(sPropertyId)) {
|
|
|
m_nLastPressureFrameIndex = nFrameIndex;
|
|
|
}
|
|
|
emit stateChanged();
|
|
|
return true;
|
|
|
}
|
|
|
|
|
|
bool nmPebiResultPlaybackController::selectFirstFrame()
|
|
|
{
|
|
|
return selectFrame(0);
|
|
|
}
|
|
|
|
|
|
bool nmPebiResultPlaybackController::selectPreviousFrame()
|
|
|
{
|
|
|
return selectFrame(qMax(0, getCurrentFrameIndex() - 1));
|
|
|
}
|
|
|
|
|
|
bool nmPebiResultPlaybackController::selectNextFrame()
|
|
|
{
|
|
|
const int nLastFrame = getCurrentPropertyFrameCount() - 1;
|
|
|
return selectFrame(qMin(nLastFrame, getCurrentFrameIndex() + 1));
|
|
|
}
|
|
|
|
|
|
bool nmPebiResultPlaybackController::selectLastFrame()
|
|
|
{
|
|
|
return selectFrame(getCurrentPropertyFrameCount() - 1);
|
|
|
}
|
|
|
|
|
|
bool nmPebiResultPlaybackController::togglePlayback()
|
|
|
{
|
|
|
if(m_bPlaying) {
|
|
|
pause();
|
|
|
return true;
|
|
|
}
|
|
|
|
|
|
if(!synchronizeSceneIdentity()) {
|
|
|
return finishFailedOperation(playbackText(
|
|
|
"Result playback is unavailable."));
|
|
|
}
|
|
|
|
|
|
const QString sPropertyId = getCurrentPropertyId();
|
|
|
const int nFrameCount = getPropertyFrameCount(sPropertyId);
|
|
|
if(!isPropertyTimeDependent(sPropertyId) || nFrameCount <= 1) {
|
|
|
return finishFailedOperation(playbackText(
|
|
|
"Result playback is unavailable."));
|
|
|
}
|
|
|
|
|
|
if(getCurrentFrameIndex() == nFrameCount - 1) {
|
|
|
QString sError;
|
|
|
if(!m_pSceneController->applyPropertyFrame(
|
|
|
sPropertyId, 0, &sError)) {
|
|
|
return finishFailedOperation(sError);
|
|
|
}
|
|
|
if(isPressureProperty(sPropertyId)) {
|
|
|
m_nLastPressureFrameIndex = 0;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
const quint64 nBindingRevision =
|
|
|
m_pSceneController->getSnapshotBindingRevision();
|
|
|
if(nBindingRevision == 0 ||
|
|
|
nBindingRevision != m_nObservedBindingRevision) {
|
|
|
return finishFailedOperation(playbackText(
|
|
|
"Result playback is unavailable."));
|
|
|
}
|
|
|
|
|
|
// Timer 归属只在全部校验和末帧复位成功后发布,防止旧回调推进新绑定。
|
|
|
m_nPlaybackBindingRevision = nBindingRevision;
|
|
|
m_pTimer->setInterval(calculatePlaybackInterval());
|
|
|
m_pTimer->start();
|
|
|
m_bPlaying = true;
|
|
|
emit stateChanged();
|
|
|
return true;
|
|
|
}
|
|
|
|
|
|
void nmPebiResultPlaybackController::pause()
|
|
|
{
|
|
|
if(stopTimerWithoutSignal()) {
|
|
|
emit stateChanged();
|
|
|
}
|
|
|
}
|
|
|
|
|
|
bool nmPebiResultPlaybackController::setPlaybackSettings(
|
|
|
int nStepsPerSecond,
|
|
|
bool bLoopEnabled)
|
|
|
{
|
|
|
if(nStepsPerSecond < MinimumPlaybackSpeed) {
|
|
|
// 公开接口不能依赖设置对话框兜底;非法参数也执行统一失败收尾。
|
|
|
return finishFailedOperation(playbackText(
|
|
|
"Playback speed must be a positive integer."));
|
|
|
}
|
|
|
if(m_nStepsPerSecond == nStepsPerSecond &&
|
|
|
m_bLoopEnabled == bLoopEnabled) {
|
|
|
return true;
|
|
|
}
|
|
|
|
|
|
// 两个设置在完整校验后一次提交,避免速度失败但循环开关已改变。
|
|
|
m_nStepsPerSecond = nStepsPerSecond;
|
|
|
m_bLoopEnabled = bLoopEnabled;
|
|
|
if(m_pTimer->isActive()) {
|
|
|
m_pTimer->setInterval(calculatePlaybackInterval());
|
|
|
}
|
|
|
emit stateChanged();
|
|
|
return true;
|
|
|
}
|
|
|
|
|
|
bool nmPebiResultPlaybackController::isAvailable() const
|
|
|
{
|
|
|
return m_pSceneController != nullptr &&
|
|
|
m_pSceneController->hasSnapshot();
|
|
|
}
|
|
|
|
|
|
QString nmPebiResultPlaybackController::getObservedSnapshotId() const
|
|
|
{
|
|
|
return m_sObservedSnapshotId;
|
|
|
}
|
|
|
|
|
|
quint64 nmPebiResultPlaybackController::getObservedBindingRevision() const
|
|
|
{
|
|
|
return m_nObservedBindingRevision;
|
|
|
}
|
|
|
|
|
|
QStringList nmPebiResultPlaybackController::getAvailablePropertyIds() const
|
|
|
{
|
|
|
return isAvailable()
|
|
|
? m_pSceneController->getAvailablePropertyIds() : QStringList();
|
|
|
}
|
|
|
|
|
|
QString nmPebiResultPlaybackController::getCurrentPropertyId() const
|
|
|
{
|
|
|
return isAvailable()
|
|
|
? m_pSceneController->getCurrentPropertyId() : QString();
|
|
|
}
|
|
|
|
|
|
int nmPebiResultPlaybackController::getCurrentFrameIndex() const
|
|
|
{
|
|
|
return isAvailable()
|
|
|
? m_pSceneController->getCurrentFrameIndex() : -1;
|
|
|
}
|
|
|
|
|
|
bool nmPebiResultPlaybackController::isPropertyTimeDependent(
|
|
|
const QString& sPropertyId) const
|
|
|
{
|
|
|
return isAvailable() &&
|
|
|
m_pSceneController->isPropertyTimeDependent(sPropertyId);
|
|
|
}
|
|
|
|
|
|
int nmPebiResultPlaybackController::getPropertyFrameCount(
|
|
|
const QString& sPropertyId) const
|
|
|
{
|
|
|
return isAvailable()
|
|
|
? m_pSceneController->getPropertyFrameCount(sPropertyId) : 0;
|
|
|
}
|
|
|
|
|
|
bool nmPebiResultPlaybackController::getPropertyTimeAt(
|
|
|
const QString& sPropertyId,
|
|
|
int nFrameIndex,
|
|
|
double& dTime) const
|
|
|
{
|
|
|
return isAvailable() &&
|
|
|
m_pSceneController->getPropertyTimeAt(
|
|
|
sPropertyId, nFrameIndex, dTime);
|
|
|
}
|
|
|
|
|
|
bool nmPebiResultPlaybackController::isCurrentPropertyTimeDependent() const
|
|
|
{
|
|
|
return isPropertyTimeDependent(getCurrentPropertyId());
|
|
|
}
|
|
|
|
|
|
int nmPebiResultPlaybackController::getCurrentPropertyFrameCount() const
|
|
|
{
|
|
|
return getPropertyFrameCount(getCurrentPropertyId());
|
|
|
}
|
|
|
|
|
|
int nmPebiResultPlaybackController::getLastPressureFrameIndex() const
|
|
|
{
|
|
|
return m_nLastPressureFrameIndex;
|
|
|
}
|
|
|
|
|
|
int nmPebiResultPlaybackController::getPlaybackSpeed() const
|
|
|
{
|
|
|
return m_nStepsPerSecond;
|
|
|
}
|
|
|
|
|
|
int nmPebiResultPlaybackController::getPlaybackIntervalMilliseconds() const
|
|
|
{
|
|
|
return calculatePlaybackInterval();
|
|
|
}
|
|
|
|
|
|
bool nmPebiResultPlaybackController::isLoopEnabled() const
|
|
|
{
|
|
|
return m_bLoopEnabled;
|
|
|
}
|
|
|
|
|
|
bool nmPebiResultPlaybackController::isPlaying() const
|
|
|
{
|
|
|
return m_bPlaying && m_pTimer != nullptr && m_pTimer->isActive();
|
|
|
}
|
|
|
|
|
|
void nmPebiResultPlaybackController::onPlaybackTimeout()
|
|
|
{
|
|
|
// 先验证 Timer 启动时捕获的绑定,已经排队的旧事件不得推进后来绑定的快照。
|
|
|
const bool bTimerOwnsCurrentBinding =
|
|
|
m_bPlaying && m_pTimer != nullptr && m_pTimer->isActive() &&
|
|
|
m_pSceneController != nullptr &&
|
|
|
m_pSceneController->hasSnapshot() &&
|
|
|
m_nPlaybackBindingRevision != 0 &&
|
|
|
m_pSceneController->getSnapshotBindingRevision() ==
|
|
|
m_nPlaybackBindingRevision;
|
|
|
if(!bTimerOwnsCurrentBinding) {
|
|
|
stopTimerWithoutSignal();
|
|
|
emit stateChanged();
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
// Timer 归属通过后再做普通身份同步;任何不一致都只停止,不提交下一帧。
|
|
|
if(!synchronizeSceneIdentity() || !m_bPlaying ||
|
|
|
m_nPlaybackBindingRevision == 0) {
|
|
|
stopTimerWithoutSignal();
|
|
|
emit stateChanged();
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
const QString sPropertyId = getCurrentPropertyId();
|
|
|
const int nFrameCount = getPropertyFrameCount(sPropertyId);
|
|
|
const int nCurrentFrame = getCurrentFrameIndex();
|
|
|
if(!isPropertyTimeDependent(sPropertyId) || nFrameCount <= 1 ||
|
|
|
nCurrentFrame < 0 || nCurrentFrame >= nFrameCount) {
|
|
|
finishFailedOperation(playbackText(
|
|
|
"Result playback is unavailable."));
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
int nTargetFrame = nCurrentFrame + 1;
|
|
|
if(nTargetFrame >= nFrameCount) {
|
|
|
if(!m_bLoopEnabled) {
|
|
|
stopTimerWithoutSignal();
|
|
|
emit stateChanged();
|
|
|
return;
|
|
|
}
|
|
|
nTargetFrame = 0;
|
|
|
}
|
|
|
|
|
|
QString sError;
|
|
|
if(!m_pSceneController->applyPropertyFrame(
|
|
|
sPropertyId, nTargetFrame, &sError)) {
|
|
|
finishFailedOperation(sError);
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
if(isPressureProperty(sPropertyId)) {
|
|
|
m_nLastPressureFrameIndex = nTargetFrame;
|
|
|
}
|
|
|
if(!m_bLoopEnabled && nTargetFrame == nFrameCount - 1) {
|
|
|
// 末帧已经成功显示,立即切换为暂停图标并保留该帧。
|
|
|
stopTimerWithoutSignal();
|
|
|
}
|
|
|
emit stateChanged();
|
|
|
}
|
|
|
|
|
|
bool nmPebiResultPlaybackController::stopTimerWithoutSignal()
|
|
|
{
|
|
|
const bool bWasPlaying = m_bPlaying ||
|
|
|
(m_pTimer != nullptr && m_pTimer->isActive());
|
|
|
if(m_pTimer != nullptr) {
|
|
|
m_pTimer->stop();
|
|
|
}
|
|
|
m_bPlaying = false;
|
|
|
m_nPlaybackBindingRevision = 0;
|
|
|
return bWasPlaying;
|
|
|
}
|
|
|
|
|
|
bool nmPebiResultPlaybackController::synchronizeSceneIdentity()
|
|
|
{
|
|
|
if(m_pSceneController == nullptr ||
|
|
|
!m_pSceneController->hasSnapshot()) {
|
|
|
stopTimerWithoutSignal();
|
|
|
m_sObservedSnapshotId.clear();
|
|
|
m_nObservedBindingRevision = 0;
|
|
|
m_nLastPressureFrameIndex = 0;
|
|
|
return false;
|
|
|
}
|
|
|
|
|
|
const QString sSnapshotId =
|
|
|
m_pSceneController->getCurrentSnapshotId();
|
|
|
const quint64 nBindingRevision =
|
|
|
m_pSceneController->getSnapshotBindingRevision();
|
|
|
if(sSnapshotId.isEmpty() || nBindingRevision == 0) {
|
|
|
stopTimerWithoutSignal();
|
|
|
m_sObservedSnapshotId.clear();
|
|
|
m_nObservedBindingRevision = 0;
|
|
|
m_nLastPressureFrameIndex = 0;
|
|
|
return false;
|
|
|
}
|
|
|
|
|
|
if(m_nObservedBindingRevision != nBindingRevision ||
|
|
|
m_sObservedSnapshotId != sSnapshotId) {
|
|
|
stopTimerWithoutSignal();
|
|
|
m_sObservedSnapshotId = sSnapshotId;
|
|
|
m_nObservedBindingRevision = nBindingRevision;
|
|
|
// 新快照由 SceneController 固定提交压力第 0 帧;无压力分支仍从 0 开始。
|
|
|
m_nLastPressureFrameIndex =
|
|
|
isPressureProperty(getCurrentPropertyId()) &&
|
|
|
getCurrentFrameIndex() >= 0
|
|
|
? getCurrentFrameIndex() : 0;
|
|
|
}
|
|
|
return true;
|
|
|
}
|
|
|
|
|
|
bool nmPebiResultPlaybackController::finishFailedOperation(
|
|
|
const QString& sError)
|
|
|
{
|
|
|
stopTimerWithoutSignal();
|
|
|
// Scene 极端回滚失败时可能已经自清空,此处先同步为不可用状态。
|
|
|
synchronizeSceneIdentity();
|
|
|
emit stateChanged();
|
|
|
emit operationFailed(sError.isEmpty()
|
|
|
? playbackText("Result playback is unavailable.")
|
|
|
: sError);
|
|
|
return false;
|
|
|
}
|
|
|
|
|
|
int nmPebiResultPlaybackController::calculatePlaybackInterval() const
|
|
|
{
|
|
|
return qMax(1, qRound(1000.0 /
|
|
|
static_cast<double>(m_nStepsPerSecond)));
|
|
|
}
|