From f78de517eca52ab041c5fa34591940584d4fa5fa Mon Sep 17 00:00:00 2001 From: DingVero Date: Sat, 23 May 2026 20:49:32 +0800 Subject: [PATCH] update --- client/core/workspace/ProjectWorkspace.cpp | 5 + client/gui/editor/EditorCanvas.cpp | 103 +++-- client/gui/editor/EditorCanvas.h | 8 + client/gui/main_window/MainWindow.cpp | 421 +++++++-------------- client/gui/main_window/MainWindow.h | 18 +- 5 files changed, 245 insertions(+), 310 deletions(-) diff --git a/client/core/workspace/ProjectWorkspace.cpp b/client/core/workspace/ProjectWorkspace.cpp index c3d0a8e..e22a9fd 100644 --- a/client/core/workspace/ProjectWorkspace.cpp +++ b/client/core/workspace/ProjectWorkspace.cpp @@ -218,6 +218,11 @@ QPointF resolvedOriginAtFrame(const Project& project, const QString& id, int fra return rt.tool.originWorld; } } + for (const auto& rc : rf.cameras) { + if (rc.camera.id == id) { + return rc.camera.centerWorld; + } + } return QPointF(); } diff --git a/client/gui/editor/EditorCanvas.cpp b/client/gui/editor/EditorCanvas.cpp index 4c95988..842a0e8 100644 --- a/client/gui/editor/EditorCanvas.cpp +++ b/client/gui/editor/EditorCanvas.cpp @@ -687,8 +687,11 @@ void EditorCanvas::setEntities(const QVector& entities, const QString prevSelectedId = (m_selectedEntity >= 0 && m_selectedEntity < m_entities.size()) ? m_entities[m_selectedEntity].id : QString(); - m_blackholeOverlayImageCache.clear(); m_projectDirAbs = projectDirAbs; + if (m_entityRasterCacheProjectDir != projectDirAbs) { + m_entityRasterImageCache.clear(); + m_entityRasterCacheProjectDir = projectDirAbs; + } m_entities.clear(); m_entities.reserve(entities.size()); @@ -761,37 +764,87 @@ void EditorCanvas::setEntities(const QVector& entities, v.color = QColor(255, 120, 0, 70); if (!e.runtimeImagePng.isEmpty()) { - QImage img; - img.loadFromData(e.runtimeImagePng, "PNG"); - if (!img.isNull() && img.format() != QImage::Format_ARGB32_Premultiplied) { - img = img.convertToFormat(QImage::Format_ARGB32_Premultiplied); + const QString cacheKey = QStringLiteral("rt:%1:%2:%3") + .arg(e.id) + .arg(e.runtimeImagePng.size()) + .arg(qHash(e.runtimeImagePng)); + auto cit = m_entityRasterImageCache.constFind(cacheKey); + if (cit != m_entityRasterImageCache.cend() && !cit->isNull()) { + v.image = cit.value(); + } else { + QImage img; + img.loadFromData(e.runtimeImagePng, "PNG"); + if (!img.isNull() && img.format() != QImage::Format_ARGB32_Premultiplied) { + img = img.convertToFormat(QImage::Format_ARGB32_Premultiplied); + } + if (!img.isNull()) { + m_entityRasterImageCache.insert(cacheKey, img); + } + v.image = img; } - v.image = img; } else { const QString imgRel = e.imagePath; if (imgRel.startsWith(QStringLiteral("pngb64:"))) { const QByteArray b64 = imgRel.mid(QStringLiteral("pngb64:").size()).toLatin1(); const QByteArray raw = QByteArray::fromBase64(b64); - QImage img; - img.loadFromData(raw, "PNG"); - if (!img.isNull() && img.format() != QImage::Format_ARGB32_Premultiplied) { - img = img.convertToFormat(QImage::Format_ARGB32_Premultiplied); - } - v.image = img; - } else if (!imgRel.isEmpty() && !projectDirAbs.isEmpty()) { - const QString abs = QDir(projectDirAbs).filePath(imgRel); - if (QFileInfo::exists(abs)) { - QImage img(abs); + const QString cacheKey = + QStringLiteral("b64:%1:%2").arg(raw.size()).arg(qHash(raw)); + auto cit = m_entityRasterImageCache.constFind(cacheKey); + if (cit != m_entityRasterImageCache.cend() && !cit->isNull()) { + v.image = cit.value(); + } else { + QImage img; + img.loadFromData(raw, "PNG"); if (!img.isNull() && img.format() != QImage::Format_ARGB32_Premultiplied) { img = img.convertToFormat(QImage::Format_ARGB32_Premultiplied); } + if (!img.isNull()) { + m_entityRasterImageCache.insert(cacheKey, img); + } v.image = img; } + } else if (!imgRel.isEmpty() && !projectDirAbs.isEmpty()) { + const QString abs = QDir::cleanPath(QDir(projectDirAbs).filePath(imgRel)); + if (QFileInfo::exists(abs)) { + auto cit = m_entityRasterImageCache.constFind(abs); + if (cit != m_entityRasterImageCache.cend() && !cit->isNull()) { + v.image = cit.value(); + } else { + QImage img(abs); + if (!img.isNull() && img.format() != QImage::Format_ARGB32_Premultiplied) { + img = img.convertToFormat(QImage::Format_ARGB32_Premultiplied); + } + if (!img.isNull()) { + m_entityRasterImageCache.insert(abs, img); + } + v.image = img; + } + } } } m_entities.push_back(v); } + constexpr int kMaxEntityRasterCacheEntries = 768; + if (m_entityRasterImageCache.size() > kMaxEntityRasterCacheEntries) { + m_entityRasterImageCache.clear(); + } + + if (!m_blackholeOverlayImageCache.isEmpty()) { + QSet aliveIds; + aliveIds.reserve(m_entities.size()); + for (const auto& ent : m_entities) { + aliveIds.insert(ent.id); + } + for (auto it = m_blackholeOverlayImageCache.begin(); it != m_blackholeOverlayImageCache.end();) { + if (!aliveIds.contains(it.key())) { + it = m_blackholeOverlayImageCache.erase(it); + } else { + ++it; + } + } + } + // 绘制/命中顺序: // - priority 小(低)先画,大(高)后画,高优先级盖住低优先级 // - priority 相同:深度小(远)先画,大(近)后画,近处盖住远处(等价于按距离缩放决定上下层) @@ -816,13 +869,15 @@ void EditorCanvas::setEntities(const QVector& entities, } } - if (m_selectedEntity >= 0) { - const auto& ent = m_entities[m_selectedEntity]; - const QPointF origin = - ent.polygonWorld.isEmpty() ? ent.rect.center() : entity_cutout::polygonCentroid(ent.polygonWorld); - emit selectedEntityChanged(true, ent.id, ent.depth, origin); - } else if (!prevSelectedId.isEmpty()) { - emit selectedEntityChanged(false, QString(), 0, QPointF()); + if (!m_suppressSelectionSignals) { + if (m_selectedEntity >= 0) { + const auto& ent = m_entities[m_selectedEntity]; + const QPointF origin = + ent.polygonWorld.isEmpty() ? ent.rect.center() : entity_cutout::polygonCentroid(ent.polygonWorld); + emit selectedEntityChanged(true, ent.id, ent.depth, origin); + } else if (!prevSelectedId.isEmpty()) { + emit selectedEntityChanged(false, QString(), 0, QPointF()); + } } if (!m_selectedBlackholeEntityId.isEmpty()) { @@ -3375,6 +3430,8 @@ void EditorCanvas::mouseReleaseEvent(QMouseEvent* e) { e->button() == Qt::LeftButton) { const auto& cam = m_cameraOverlays[m_selectedCameraIndex]; const QPointF delta = cam.centerWorld - m_cameraDragStartCenterWorld; + // 先于 requestMoveCamera 清除:MainWindow::refreshEditorPage 需刷新 overlays,否则会误判“仍在拖动”而跳过 setCameraOverlays + m_draggingCamera = false; if (!cam.id.isEmpty() && (!qFuzzyIsNull(delta.x()) || !qFuzzyIsNull(delta.y()))) { emit requestMoveCamera(cam.id, delta); } else if (!cam.id.isEmpty()) { diff --git a/client/gui/editor/EditorCanvas.h b/client/gui/editor/EditorCanvas.h index 2b1892a..3d636d0 100644 --- a/client/gui/editor/EditorCanvas.h +++ b/client/gui/editor/EditorCanvas.h @@ -95,6 +95,8 @@ public: void setEntities(const QVector& entities, const QVector& opacities01, const QString& projectDirAbs); + /// 播放/时间轴轻量刷帧时置 true,避免 setEntities 每帧 emit selectedEntityChanged 拖垮主线程 + void setSuppressSelectionSignals(bool on) { m_suppressSelectionSignals = on; } void setTools(const QVector& tools, const QVector& opacities01); void setCameraOverlays(const QVector& cameras, const QString& selectedId, @@ -107,6 +109,8 @@ public: int currentFrame() const { return m_currentFrame; } bool isDraggingEntity() const { return m_draggingEntity; } + /// 正在拖动摄像机中心手柄(Move 工具);此时 overlays 由画布更新,勿用工作区求值覆盖 + bool isDraggingCamera() const { return m_draggingCamera; } void selectEntityById(const QString& id); void clearEntitySelection(); @@ -252,6 +256,10 @@ private: private: QString m_projectDirAbs; + /// 实体抠图贴图缓存(键:磁盘绝对路径或 runtime/png 内容摘要),避免播放时每帧 QImage(path) 读盘 + QHash m_entityRasterImageCache; + QString m_entityRasterCacheProjectDir; + bool m_suppressSelectionSignals = false; QHash m_blackholeOverlayImageCache; QString m_bgAbsPath; bool m_backgroundVisible = true; diff --git a/client/gui/main_window/MainWindow.cpp b/client/gui/main_window/MainWindow.cpp index f942cef..ede7490 100644 --- a/client/gui/main_window/MainWindow.cpp +++ b/client/gui/main_window/MainWindow.cpp @@ -43,6 +43,7 @@ #include #include #include +#include #include #include #include @@ -307,9 +308,7 @@ public: EditorCanvas* canvas = nullptr; QWidget* modeDock = nullptr; QWidget* toolDock = nullptr; - QWidget* previewPlaybackBar = nullptr; - QWidget* previewArrowLeft = nullptr; - QWidget* previewArrowRight = nullptr; + QWidget* previewBackButton = nullptr; void relayoutFloaters() { if (canvas) { @@ -333,6 +332,12 @@ public: modeDock->adjustSize(); } + if (previewBackButton && previewBackButton->isVisible()) { + previewBackButton->updateGeometry(); + previewBackButton->adjustSize(); + previewBackButton->move(kMargin, kMargin); + } + if (toolDock && toolDock->isVisible()) { if (QLayout* lay = toolDock->layout()) { lay->activate(); @@ -343,6 +348,9 @@ public: if (modeDock && modeDock->isVisible()) { y = modeDock->y() + modeDock->height() + kGap; } + if (previewBackButton && previewBackButton->isVisible()) { + y = std::max(y, previewBackButton->y() + previewBackButton->height() + kGap); + } toolDock->move(kMargin, y); } @@ -353,36 +361,8 @@ public: if (toolDock && toolDock->isVisible()) { toolDock->raise(); } - - constexpr int arrowW = 44; - constexpr int arrowH = 72; - - auto placeArrowCenterY = [&](QWidget* arrow, bool leftSide) { - if (!arrow || !arrow->isVisible()) { - return; - } - arrow->setFixedSize(arrowW, arrowH); - const int y = std::max(kMargin, (height() - arrowH) / 2); - if (leftSide) { - arrow->move(kMargin, y); - } else { - arrow->move(std::max(kMargin, width() - arrowW - kMargin), y); - } - arrow->raise(); - }; - placeArrowCenterY(previewArrowLeft, true); - placeArrowCenterY(previewArrowRight, false); - - if (previewPlaybackBar && previewPlaybackBar->isVisible()) { - if (QLayout* lay = previewPlaybackBar->layout()) { - lay->activate(); - } - previewPlaybackBar->updateGeometry(); - previewPlaybackBar->adjustSize(); - const int x = std::max(kMargin, (width() - previewPlaybackBar->width()) / 2); - const int y = std::max(kMargin, height() - previewPlaybackBar->height() - kMargin); - previewPlaybackBar->move(x, y); - previewPlaybackBar->raise(); + if (previewBackButton && previewBackButton->isVisible()) { + previewBackButton->raise(); } } @@ -752,71 +732,35 @@ void MainWindow::createTimelineDock() { }); m_playTimer = new QTimer(this); + m_playTimer->setTimerType(Qt::PreciseTimer); connect(m_playTimer, &QTimer::timeout, this, [this]() { if (!m_timeline || !m_workspace.isOpen() || m_previewRequested) { return; } const int len = m_workspace.activeAnimationLengthFrames(); m_currentFrame = (m_currentFrame + 1) % std::max(1, len); - m_timeline->setCurrentFrame(m_currentFrame); + m_timeline->setCurrentFrameProgrammatic(m_currentFrame); + applyTimelineFrameToEditor(m_currentFrame); }); connect(m_btnPlay, &QToolButton::toggled, this, &MainWindow::onTogglePlay); connect(m_timeline, &TimelineWidget::frameScrubbed, this, [this](int v) { - // 轻量实时预览:只更新画布帧,不做 refreshEditorPage 的全量重建 - m_currentFrame = std::clamp(v, 0, m_workspace.activeAnimationLengthFrames() - 1); - if (m_editorCanvas && m_workspace.isOpen()) { - // 需要重新求值实体几何/贴图轨道,否则拖动实体与属性变更在非 0 帧会失效 - m_timelineScrubbing = true; - m_editorCanvas->setCurrentFrame(m_currentFrame); - const core::eval::ResolvedProjectFrame rf = core::eval::evaluateAtFrame(m_workspace.project(), m_currentFrame, 10); - QVector ents; - QVector entOps; - ents.reserve(rf.entities.size()); - entOps.reserve(rf.entities.size()); - for (const auto& re : rf.entities) { - ents.push_back(re.entity); - entOps.push_back(re.opacity); - } - m_editorCanvas->setEntities(ents, entOps, m_workspace.projectDir()); - QVector tools; - QVector toolOps; - tools.reserve(rf.tools.size()); - toolOps.reserve(rf.tools.size()); - for (const auto& rt : rf.tools) { - tools.push_back(rt.tool); - toolOps.push_back(rt.opacity); - } - m_editorCanvas->setTools(tools, toolOps); - QVector cams; - cams.reserve(rf.cameras.size()); - for (const auto& rc : rf.cameras) { - cams.push_back(rc.camera); - } - m_editorCanvas->setCameraOverlays(cams, m_selectedCameraId, m_tempHiddenCameraIds); - m_editorCanvas->setTempHiddenIds(m_tempHiddenEntityIds, m_tempHiddenToolIds); - const bool presentation = m_previewRequested && m_workspace.hasBackground(); - m_editorCanvas->setPreviewCameraViewLocked(false); - if (presentation) { - const QString acid = m_workspace.project().activeCameraId(); - if (!acid.isEmpty()) { - for (const auto& rc : rf.cameras) { - if (rc.camera.id == acid) { - m_editorCanvas->setPreviewCameraViewLocked(true); - m_editorCanvas->applyCameraViewport(rc.camera.centerWorld, rc.camera.viewScale); - break; - } - } - } - } - m_timelineScrubbing = false; - } else if (m_editorCanvas) { - m_editorCanvas->setCurrentFrame(m_currentFrame); + const int len = m_workspace.activeAnimationLengthFrames(); + const int maxF = std::max(0, len - 1); + m_currentFrame = std::clamp(v, 0, maxF); + if (!m_timelineEditorApplyThrottle.isValid()) { + m_timelineEditorApplyThrottle.start(); + applyTimelineFrameToEditor(m_currentFrame); + return; + } + if (m_timelineEditorApplyThrottle.hasExpired(22)) { + applyTimelineFrameToEditor(m_currentFrame); + m_timelineEditorApplyThrottle.restart(); } }); connect(m_timeline, &TimelineWidget::frameCommitted, this, [this](int v) { - // 松手再做一次较重刷新(如果后续还有需要同步的 UI) - m_currentFrame = std::clamp(v, 0, m_workspace.activeAnimationLengthFrames() - 1); + m_timelineEditorApplyThrottle.invalidate(); + m_currentFrame = std::clamp(v, 0, std::max(0, m_workspace.activeAnimationLengthFrames() - 1)); refreshEditorPage(); }); connect(btnKeyCombined, &QToolButton::clicked, this, &MainWindow::onInsertCombinedKey); @@ -1163,7 +1107,7 @@ void MainWindow::onTogglePlay(bool on) { if (m_playTimer) { if (on) { const int fps = m_workspace.isOpen() ? m_workspace.activeAnimationFps() : 30; - m_playTimer->start(1000 / std::max(1, fps)); + m_playTimer->start(std::max(1, qRound(1000.0 / double(std::max(1, fps))))); } else { m_playTimer->stop(); } @@ -1203,19 +1147,11 @@ void MainWindow::startPreviewPlayback() { const QString id = effectivePreviewAnimationId(); const int fps = m_workspace.animationFpsForId(id); if (m_previewPlayTimer) { - m_previewPlayTimer->start(1000 / std::max(1, fps)); + m_previewPlayTimer->start(std::max(1, qRound(1000.0 / double(std::max(1, fps))))); } syncPreviewPlaybackBar(); } -void MainWindow::togglePreviewPlayback() { - if (m_previewPlaying) { - stopPreviewPlayback(); - } else { - startPreviewPlayback(); - } -} - QVector MainWindow::previewAnimationSchemeIdsNonNone() const { QVector out; if (!m_workspace.isOpen()) { @@ -1252,44 +1188,32 @@ QString MainWindow::effectivePreviewAnimationId() const { return fb.isEmpty() ? QStringLiteral("none") : fb; } -void MainWindow::refreshPreviewSchemeCombo() { - if (!m_previewSchemeCombo || !m_workspace.isOpen()) { +void MainWindow::syncPreviewFloatingChrome() { + if (!m_canvasHost || !m_floatingModeDock || !m_previewBackToStaticBtn) { return; } - const QString keep = effectivePreviewAnimationId(); - m_previewSchemeCombo->blockSignals(true); - m_previewSchemeCombo->clear(); - QString noneLabel = QStringLiteral("画布"); - for (const auto& a : m_workspace.project().animations()) { - if (a.id == QStringLiteral("none")) { - if (!a.name.isEmpty()) - noneLabel = a.name; - break; - } + auto* ch = static_cast(m_canvasHost); + ch->previewBackButton = m_previewBackToStaticBtn; + + const bool projectOpen = m_workspace.isOpen(); + if (!projectOpen) { + m_floatingModeDock->hide(); + m_previewBackToStaticBtn->hide(); + ch->relayoutFloaters(); + return; } - if (m_workspace.project().findAnimationById(QStringLiteral("none"))) { - m_previewSchemeCombo->addItem(noneLabel, QStringLiteral("none")); + + const bool preview = m_previewRequested; + const bool showBack = preview && (effectivePreviewAnimationId() != QStringLiteral("none")); + + if (showBack) { + m_floatingModeDock->hide(); + m_previewBackToStaticBtn->show(); + } else { + m_floatingModeDock->show(); + m_previewBackToStaticBtn->hide(); } - for (const auto& a : m_workspace.project().animations()) { - if (a.id == QStringLiteral("none")) { - continue; - } - const QString label = a.name.isEmpty() ? a.id : a.name; - m_previewSchemeCombo->addItem(label, a.id); - } - int idx = -1; - for (int i = 0; i < m_previewSchemeCombo->count(); ++i) { - if (m_previewSchemeCombo->itemData(i).toString() == keep) { - idx = i; - break; - } - } - if (idx >= 0) { - m_previewSchemeCombo->setCurrentIndex(idx); - } else if (m_previewSchemeCombo->count() > 0) { - m_previewSchemeCombo->setCurrentIndex(0); - } - m_previewSchemeCombo->blockSignals(false); + ch->relayoutFloaters(); } void MainWindow::requestPreviewSchemeSwitchTo(const QString& id, bool showProgress) { @@ -1318,7 +1242,6 @@ void MainWindow::requestPreviewSchemeSwitchTo(const QString& id, bool showProgre m_previewFrame = 0; auto finishUi = [this]() { - refreshPreviewSchemeCombo(); refreshEditorPage(); if (effectivePreviewAnimationId() != QStringLiteral("none")) startPreviewPlayback(); @@ -1338,35 +1261,6 @@ void MainWindow::requestPreviewSchemeSwitchTo(const QString& id, bool showProgre finishUi(); } -void MainWindow::switchPreviewSchemeAdjacent(int delta) { - QVector ids = previewAnimationSchemeIdsNonNone(); - if (ids.size() <= 1) { - return; - } - const QString cur = effectivePreviewAnimationId(); - int ix = 0; - for (int i = 0; i < ids.size(); ++i) { - if (ids[i] == cur) { - ix = i; - break; - } - } - const int n = static_cast(ids.size()); - ix = ((ix + delta) % n + n) % n; - requestPreviewSchemeSwitchTo(ids[ix], true); -} - -void MainWindow::onPreviewSchemeComboChanged(int idx) { - if (!m_previewSchemeCombo || !m_workspace.isOpen() || !m_previewRequested || idx < 0) { - return; - } - const QString animId = m_previewSchemeCombo->itemData(idx).toString(); - if (animId.isEmpty() || animId == m_previewAnimationId) { - return; - } - requestPreviewSchemeSwitchTo(animId, true); -} - void MainWindow::syncHotspotEnterPreviewCombo() { const bool any = previewAnimationSchemeIdsNonNone().size() > 0; if (m_btnHotspotEnterPreview) { @@ -1401,10 +1295,7 @@ void MainWindow::syncEditorRailForMode() { } void MainWindow::syncPreviewPlaybackBar() { - if (!m_previewBtnPlay) { - return; - } - m_previewBtnPlay->setText(m_previewPlaying ? QStringLiteral("暂停") : QStringLiteral("播放")); + syncPreviewFloatingChrome(); } void MainWindow::onInsertCombinedKey() { @@ -1474,7 +1365,7 @@ void MainWindow::createAnimationMenu() { AnimationSchemeSettingsDialog dlg(AnimationSchemeSettingsDialog::Mode::Edit, m_workspace, this); dlg.setEditTarget(targetId); if (dlg.exec() == QDialog::Accepted) { - refreshPreviewSchemeCombo(); + syncPreviewFloatingChrome(); refreshEditorPage(); } }); @@ -3031,10 +2922,6 @@ void MainWindow::updateUiEnabledState() { m_modeSelector->setItemData(3, (projectOpen && hasBg) ? QVariant() : QVariant(0), Qt::UserRole - 1); m_modeSelector->blockSignals(false); } - if (m_previewBackToCanvasBtn) { - m_previewBackToCanvasBtn->setVisible(m_previewRequested && projectOpen && - effectivePreviewAnimationId() != QStringLiteral("none")); - } if (projectOpen && hasBg) { syncHotspotEnterPreviewCombo(); } @@ -3144,24 +3031,11 @@ void MainWindow::applyUiMode(UiMode mode) { } } - if (m_floatingModeDock) { - m_floatingModeDock->setVisible(projectOpen); - } if (m_floatingToolDock) { m_floatingToolDock->setVisible(projectOpen && !preview); } + syncPreviewFloatingChrome(); syncEditorRailForMode(); - if (m_previewPlaybackBar) { - m_previewPlaybackBar->setVisible(projectOpen && preview); - } - if (!preview || !projectOpen) { - if (m_previewArrowLeft) { - m_previewArrowLeft->hide(); - } - if (m_previewArrowRight) { - m_previewArrowRight->hide(); - } - } if (m_canvasHost) { m_canvasHost->updateGeometry(); static_cast(m_canvasHost)->relayoutFloaters(); @@ -3372,47 +3246,25 @@ void MainWindow::rebuildCentralPages() { m_entityIntroPopup = new gui::EntityIntroPopup(this); - m_previewArrowLeft = new QToolButton(canvasHost); - m_previewArrowLeft->setText(QStringLiteral("◀")); - m_previewArrowLeft->setToolTip({}); - polishCompactToolButton(m_previewArrowLeft, 44); - m_previewArrowLeft->setStyleSheet( - QStringLiteral("QToolButton { background-color: rgba(40,40,40,168); border-radius: 8px; color: palette(text); " - "padding: 4px 8px; } QToolButton:hover { background-color: rgba(60,60,60,208); }")); - m_previewArrowLeft->hide(); - connect(m_previewArrowLeft, &QToolButton::clicked, this, - [this]() { switchPreviewSchemeAdjacent(-1); }); - - m_previewArrowRight = new QToolButton(canvasHost); - m_previewArrowRight->setText(QStringLiteral("▶")); - m_previewArrowRight->setToolTip({}); - polishCompactToolButton(m_previewArrowRight, 44); - m_previewArrowRight->setStyleSheet( - QStringLiteral("QToolButton { background-color: rgba(40,40,40,168); border-radius: 8px; color: palette(text); " - "padding: 4px 8px; } QToolButton:hover { background-color: rgba(60,60,60,208); }")); - m_previewArrowRight->hide(); - connect(m_previewArrowRight, &QToolButton::clicked, this, - [this]() { switchPreviewSchemeAdjacent(1); }); - - canvasHost->previewArrowLeft = m_previewArrowLeft; - canvasHost->previewArrowRight = m_previewArrowRight; - - m_previewPlaybackBar = new QFrame(canvasHost); - m_previewPlaybackBar->setObjectName(QStringLiteral("PreviewPlaybackBar")); - m_previewPlaybackBar->setStyleSheet(QString::fromUtf8(kTimelineBarQss)); - auto* pbl = new QHBoxLayout(m_previewPlaybackBar); - pbl->setContentsMargins(10, 6, 10, 6); - pbl->setSpacing(8); - m_previewSchemeCombo = new QComboBox(m_previewPlaybackBar); - m_previewSchemeCombo->setMinimumWidth(160); - pbl->addWidget(m_previewSchemeCombo); - m_previewBtnPlay = new QToolButton(m_previewPlaybackBar); - m_previewBtnPlay->setCheckable(false); - m_previewBtnPlay->setText(QStringLiteral("播放")); - m_previewBtnPlay->setToolTip({}); - polishCompactToolButton(m_previewBtnPlay, 36); - pbl->addWidget(m_previewBtnPlay); - m_previewPlaybackBar->setParent(canvasHost); + m_previewBackToStaticBtn = new QToolButton(canvasHost); + m_previewBackToStaticBtn->setText(QStringLiteral("返回")); + m_previewBackToStaticBtn->setToolTip(QStringLiteral("回到无动画的静态预览(停止播放),并恢复左上角模式切换")); + m_previewBackToStaticBtn->setStyleSheet( + QStringLiteral("QToolButton { background-color: palette(base); border: 1px solid palette(midlight); " + "border-radius: 6px; padding: 4px 10px; }" + "QToolButton:hover { background-color: palette(light); }")); + polishCompactToolButton(m_previewBackToStaticBtn, 36); + m_previewBackToStaticBtn->hide(); + canvasHost->previewBackButton = m_previewBackToStaticBtn; + connect(m_previewBackToStaticBtn, &QToolButton::clicked, this, [this]() { + if (!m_previewRequested) { + return; + } + stopPreviewPlayback(); + m_previewAnimationId = QStringLiteral("none"); + m_previewFrame = 0; + refreshEditorPage(); + }); m_floatingModeDock = new QFrame(canvasHost); m_floatingModeDock->setObjectName(QStringLiteral("FloatingModeDock")); @@ -3589,7 +3441,7 @@ void MainWindow::rebuildCentralPages() { m_btnHotspotEnterPreview = new QToolButton(m_floatingToolDock); m_btnHotspotEnterPreview->setText(QStringLiteral("预览")); m_btnHotspotEnterPreview->setVisible(false); - m_btnHotspotEnterPreview->setToolTip({}); + m_btnHotspotEnterPreview->setToolTip(QStringLiteral("进入无动画的静态预览;点击画布上的热点再播放方案动画")); polishCompactToolButton(m_btnHotspotEnterPreview, 30); toolLayout->addWidget(m_btnHotspotEnterPreview, 0, Qt::AlignHCenter); @@ -4231,13 +4083,12 @@ void MainWindow::rebuildCentralPages() { } }); connect(m_btnHotspotEnterPreview, &QToolButton::clicked, this, [this]() { - const QString aid = firstAnimationSchemeIdNonNoneWs(m_workspace.project()); - if (aid.isEmpty()) { + if (previewAnimationSchemeIdsNonNone().isEmpty()) { return; } m_animationRequested = false; m_hotspotRequested = false; - m_previewAnimationId = aid; + m_previewAnimationId = QStringLiteral("none"); m_previewFrame = 0; setPreviewRequested(true); if (m_modeSelector) { @@ -4245,7 +4096,11 @@ void MainWindow::rebuildCentralPages() { m_modeSelector->setCurrentIndex(3); m_modeSelector->blockSignals(false); } - requestPreviewSchemeSwitchTo(aid, true); + stopPreviewPlayback(); + syncPreviewFloatingChrome(); + refreshEditorPage(); + updateUiEnabledState(); + refreshProjectTree(); }); m_centerStack->addWidget(m_pageWelcome); @@ -4437,9 +4292,6 @@ void MainWindow::refreshEditorPage() { applyTimelineFromProject(); core::eval::ResolvedProjectFrame rf; const bool presentationPreview = presentation; - if (presentationPreview && !m_previewPlaying) { - refreshPreviewSchemeCombo(); - } if (presentationPreview) { const QString pvId = effectivePreviewAnimationId(); const int pvLen = m_workspace.animationLengthFramesForId(pvId); @@ -4484,7 +4336,9 @@ void MainWindow::refreshEditorPage() { m_editorCanvas->setPreviewCameraViewLocked(false); m_editorCanvas->clearCameraSelection(); } else { - m_editorCanvas->setCameraOverlays(cams, m_selectedCameraId, m_tempHiddenCameraIds); + if (!m_editorCanvas->isDraggingCamera()) { + m_editorCanvas->setCameraOverlays(cams, m_selectedCameraId, m_tempHiddenCameraIds); + } m_editorCanvas->setPreviewCameraViewLocked(false); if (presentation) { const QString acid = m_workspace.project().activeCameraId(); @@ -4520,6 +4374,7 @@ void MainWindow::refreshEditorPage() { } } } + syncPreviewFloatingChrome(); refreshPropertyPanel(); if (m_canvasHost) { m_canvasHost->updateGeometry(); @@ -4621,6 +4476,61 @@ void MainWindow::updateTimelineTracks() { } } +void MainWindow::applyTimelineFrameToEditor(int frame) { + if (!m_editorCanvas || !m_workspace.isOpen()) { + return; + } + m_editorCanvas->setSuppressSelectionSignals(true); + const int len = m_workspace.activeAnimationLengthFrames(); + const int f = std::clamp(frame, 0, std::max(0, len - 1)); + m_timelineScrubbing = true; + m_editorCanvas->setCurrentFrame(f); + const core::eval::ResolvedProjectFrame rf = core::eval::evaluateAtFrame(m_workspace.project(), f, 10); + QVector ents; + QVector entOps; + ents.reserve(rf.entities.size()); + entOps.reserve(rf.entities.size()); + for (const auto& re : rf.entities) { + ents.push_back(re.entity); + entOps.push_back(re.opacity); + } + m_editorCanvas->setEntities(ents, entOps, m_workspace.projectDir()); + QVector tools; + QVector toolOps; + tools.reserve(rf.tools.size()); + toolOps.reserve(rf.tools.size()); + for (const auto& rt : rf.tools) { + tools.push_back(rt.tool); + toolOps.push_back(rt.opacity); + } + m_editorCanvas->setTools(tools, toolOps); + QVector cams; + cams.reserve(rf.cameras.size()); + for (const auto& rc : rf.cameras) { + cams.push_back(rc.camera); + } + if (!m_editorCanvas->isDraggingCamera()) { + m_editorCanvas->setCameraOverlays(cams, m_selectedCameraId, m_tempHiddenCameraIds); + } + m_editorCanvas->setTempHiddenIds(m_tempHiddenEntityIds, m_tempHiddenToolIds); + const bool presentation = m_previewRequested && m_workspace.hasBackground(); + m_editorCanvas->setPreviewCameraViewLocked(false); + if (presentation) { + const QString acid = m_workspace.project().activeCameraId(); + if (!acid.isEmpty()) { + for (const auto& rc : rf.cameras) { + if (rc.camera.id == acid) { + m_editorCanvas->setPreviewCameraViewLocked(true); + m_editorCanvas->applyCameraViewport(rc.camera.centerWorld, rc.camera.viewScale); + break; + } + } + } + } + m_timelineScrubbing = false; + m_editorCanvas->setSuppressSelectionSignals(false); +} + void MainWindow::applyTimelineFromProject() { if (!m_timeline || !m_workspace.isOpen()) { return; @@ -4629,7 +4539,9 @@ void MainWindow::applyTimelineFromProject() { const int local = std::clamp(g, 0, m_workspace.activeAnimationLengthFrames() - 1); m_timeline->setFrameRange(0, m_workspace.activeAnimationLengthFrames()); m_timeline->setCurrentFrameProgrammatic(local); - if (m_editorCanvas) m_editorCanvas->setCurrentFrame(g); + if (m_editorCanvas) { + m_editorCanvas->setCurrentFrame(local); + } } void MainWindow::refreshDopeSheet() { @@ -5413,50 +5325,7 @@ void MainWindow::onAbout() { aboutDialog->exec(); } -void MainWindow::updatePreviewEdgeArrowsHover(const QPointF& canvasHostPos) { - if (!m_previewArrowLeft || !m_previewArrowRight || !m_canvasHost || !m_previewRequested) { - return; - } - if (previewAnimationSchemeIdsNonNone().size() <= 1) { - m_previewArrowLeft->hide(); - m_previewArrowRight->hide(); - return; - } - constexpr qreal kZone = 96.0; - const qreal w = m_canvasHost->width(); - const bool nearLeft = canvasHostPos.x() < kZone; - const bool nearRight = canvasHostPos.x() > w - kZone; - m_previewArrowLeft->setVisible(m_previewRequested && nearLeft); - m_previewArrowRight->setVisible(m_previewRequested && nearRight); - if (auto* ch = dynamic_cast(m_canvasHost)) { - ch->relayoutFloaters(); - } -} - bool MainWindow::eventFilter(QObject* watched, QEvent* event) { - if (watched == m_editorCanvas && m_previewRequested && m_editorCanvas && m_canvasHost) { - if (event->type() == QEvent::MouseMove) { - const auto me = static_cast(event); -#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) - const QPoint pe = me->position().toPoint(); -#else - const QPoint pe = me->pos(); -#endif - const QPoint inHost = m_editorCanvas->mapTo(m_canvasHost, pe); - updatePreviewEdgeArrowsHover(QPointF(inHost)); - } else if (event->type() == QEvent::Leave) { - if (m_previewArrowLeft) { - m_previewArrowLeft->hide(); - } - if (m_previewArrowRight) { - m_previewArrowRight->hide(); - } - if (auto* ch = dynamic_cast(m_canvasHost)) { - ch->relayoutFloaters(); - } - } - } - if (event->type() == QEvent::Resize && watched == m_dockProjectTree) { if (m_dockProjectTree && m_workspace.isOpen() && !m_previewRequested && !m_dockProjectTree->isFloating()) { const int w = m_dockProjectTree->width(); diff --git a/client/gui/main_window/MainWindow.h b/client/gui/main_window/MainWindow.h index 88a8f41..a1abbad 100644 --- a/client/gui/main_window/MainWindow.h +++ b/client/gui/main_window/MainWindow.h @@ -3,6 +3,7 @@ #include "core/workspace/ProjectWorkspace.h" #include "main_window/RecentProjectHistory.h" +#include #include #include #include @@ -120,20 +121,18 @@ private: void refreshPreviewPage(); void refreshEditorPage(); void applyTimelineFromProject(); + /// 时间轴当前帧 → 画布:求值并回灌实体/工具/摄像机(不经 TimelineWidget::setCurrentFrame,避免触发 frameScrubbed) + void applyTimelineFrameToEditor(int frame); void updateTimelineTracks(); void refreshDopeSheet(); void setPreviewRequested(bool preview); void syncPreviewPlaybackBar(); + void syncPreviewFloatingChrome(); void stopEditorPlayback(); void stopPreviewPlayback(); - void togglePreviewPlayback(); void startPreviewPlayback(); QString effectivePreviewAnimationId() const; - void refreshPreviewSchemeCombo(); - void onPreviewSchemeComboChanged(int idx); - void switchPreviewSchemeAdjacent(int delta); void requestPreviewSchemeSwitchTo(const QString& id, bool warmupDialog); - void updatePreviewEdgeArrowsHover(const QPointF& posInCanvasHost); QVector previewAnimationSchemeIdsNonNone() const; void syncHotspotEnterPreviewCombo(); @@ -222,6 +221,8 @@ private: void updateRightDockColumnMinimumWidthFromContent(); bool m_timelineScrubbing = false; + /// 限制 scrub 时全量回灌频率,避免每像素一次 evaluate + 磁盘读图导致时间轴极卡 + QElapsedTimer m_timelineEditorApplyThrottle; bool m_entityDragging = false; QTimer* m_propertySyncTimer = nullptr; @@ -240,7 +241,6 @@ private: QToolButton* m_btnRailAddHotspot = nullptr; QToolButton* m_btnRailMoveHotspot = nullptr; QToolButton* m_btnHotspotEnterPreview = nullptr; - QToolButton* m_previewBackToCanvasBtn = nullptr; int m_previewFrame = 0; bool m_previewPlaying = false; QTimer* m_previewPlayTimer = nullptr; @@ -257,11 +257,7 @@ private: QPushButton* m_btnDopeDeleteKey = nullptr; gui::EntityIntroPopup* m_entityIntroPopup = nullptr; - QFrame* m_previewPlaybackBar = nullptr; - QToolButton* m_previewBtnPlay = nullptr; - QComboBox* m_previewSchemeCombo = nullptr; - QToolButton* m_previewArrowLeft = nullptr; - QToolButton* m_previewArrowRight = nullptr; + QToolButton* m_previewBackToStaticBtn = nullptr; gui::ResourceLibraryDock* m_resourceLibraryDockWidget = nullptr; core::library::ResourceLibraryProvider* m_resourceLibraryProvider = nullptr;