update
This commit is contained in:
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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()) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user