This commit is contained in:
2026-05-23 20:49:32 +08:00
parent 12ea7273ef
commit f78de517ec
5 changed files with 245 additions and 310 deletions
@@ -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();
}
+80 -23
View File
@@ -687,8 +687,11 @@ void EditorCanvas::setEntities(const QVector<core::Project::Entity>& 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<core::Project::Entity>& 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<QString> 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<core::Project::Entity>& 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()) {
+8
View File
@@ -95,6 +95,8 @@ public:
void setEntities(const QVector<core::Project::Entity>& entities,
const QVector<double>& opacities01,
const QString& projectDirAbs);
/// 播放/时间轴轻量刷帧时置 true,避免 setEntities 每帧 emit selectedEntityChanged 拖垮主线程
void setSuppressSelectionSignals(bool on) { m_suppressSelectionSignals = on; }
void setTools(const QVector<core::Project::Tool>& tools, const QVector<double>& opacities01);
void setCameraOverlays(const QVector<core::Project::Camera>& 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<QString, QImage> m_entityRasterImageCache;
QString m_entityRasterCacheProjectDir;
bool m_suppressSelectionSignals = false;
QHash<QString, QImage> m_blackholeOverlayImageCache;
QString m_bgAbsPath;
bool m_backgroundVisible = true;
+145 -276
View File
@@ -43,6 +43,7 @@
#include <QDialog>
#include <QDockWidget>
#include <QDrag>
#include <QElapsedTimer>
#include <QEvent>
#include <QMouseEvent>
#include <QFrame>
@@ -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<core::Project::Entity> ents;
QVector<double> 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<core::Project::Tool> tools;
QVector<double> 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<core::Project::Camera> 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<QString> MainWindow::previewAnimationSchemeIdsNonNone() const {
QVector<QString> 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<CanvasHost*>(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<QString> 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<int>(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<CanvasHost*>(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<core::Project::Entity> ents;
QVector<double> 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<core::Project::Tool> tools;
QVector<double> 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<core::Project::Camera> 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<CanvasHost*>(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<QMouseEvent*>(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<CanvasHost*>(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();
+7 -11
View File
@@ -3,6 +3,7 @@
#include "core/workspace/ProjectWorkspace.h"
#include "main_window/RecentProjectHistory.h"
#include <QElapsedTimer>
#include <QMainWindow>
#include <QPointF>
#include <QFrame>
@@ -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<QString> 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;