From e116a9ec79aeccfed31c7a8c6e08c3db5d52161a Mon Sep 17 00:00:00 2001 From: DingVero Date: Wed, 8 Apr 2026 15:01:17 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E5=8A=A8=E7=94=BB=E9=97=AE?= =?UTF-8?q?=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/core/workspace/ProjectWorkspace.cpp | 20 ++++++++++++ client/core/workspace/ProjectWorkspace.h | 2 ++ client/gui/dialogs/FrameAnimationDialog.cpp | 36 +++++++++++++++++++++ 3 files changed, 58 insertions(+) diff --git a/client/core/workspace/ProjectWorkspace.cpp b/client/core/workspace/ProjectWorkspace.cpp index fd1a743..8941e79 100644 --- a/client/core/workspace/ProjectWorkspace.cpp +++ b/client/core/workspace/ProjectWorkspace.cpp @@ -1348,6 +1348,26 @@ bool ProjectWorkspace::setEntityImageFrame(const QString& id, int frame, const Q return true; } +bool ProjectWorkspace::setEntityImageFramePath(const QString& id, int frame, const QString& relativePath) { + if (m_projectDir.isEmpty() || id.isEmpty() || frame < 0) { + return false; + } + const QString rel = relativePath.trimmed(); + if (rel.isEmpty()) { + return false; + } + auto ents = m_project.entities(); + bool found = false; + for (auto& e : ents) { + if (e.id != id) continue; + found = true; + upsertFrame(e.imageFrames, frame, rel); + break; + } + if (!found) return false; + return applyEntities(ents, true, QStringLiteral("插入关键帧(图像)")); +} + namespace { bool removeLocationKeyAtFrame(QVector& keys, int frame) { diff --git a/client/core/workspace/ProjectWorkspace.h b/client/core/workspace/ProjectWorkspace.h index 6c507f6..c497a5e 100644 --- a/client/core/workspace/ProjectWorkspace.h +++ b/client/core/workspace/ProjectWorkspace.h @@ -91,6 +91,8 @@ public: bool setEntityDepthScaleKey(const QString& id, int frame, double value01); bool setEntityUserScaleKey(const QString& id, int frame, double userScale); bool setEntityImageFrame(const QString& id, int frame, const QImage& image, QString* outRelPath = nullptr); + // 仅更新 imageFrames 中某帧的图像路径(不读图、不写盘),用于高性能地“切断”Hold 区间 + bool setEntityImageFramePath(const QString& id, int frame, const QString& relativePath); bool removeEntityLocationKey(const QString& id, int frame); bool removeEntityDepthScaleKey(const QString& id, int frame); bool removeEntityUserScaleKey(const QString& id, int frame); diff --git a/client/gui/dialogs/FrameAnimationDialog.cpp b/client/gui/dialogs/FrameAnimationDialog.cpp index e9a42ff..65f2a88 100644 --- a/client/gui/dialogs/FrameAnimationDialog.cpp +++ b/client/gui/dialogs/FrameAnimationDialog.cpp @@ -185,6 +185,34 @@ void FrameAnimationDialog::onReplaceCurrentFrame() { auto* it = m_list->currentItem(); if (!it) return; const int f = it->data(Qt::UserRole).toInt(); + + // 在真正替换前先记录当前帧/下一帧的旧图像来源,用于“只影响当前帧”: + // 对于原本只在区间端点设置了关键帧、使用 Hold 采样的情况, + // 若直接改写关键帧会导致后续所有帧都跟着换图,这里通过在 f+1 上补一帧旧图像来“切断”区间。 + QString prevRelPathForF; + QString prevRelPathForFPlus1; + bool hasExplicitKeyAtFPlus1 = false; + if (m_workspace.isOpen()) { + const auto& ents = m_workspace.entities(); + const core::Project::Entity* hit = nullptr; + for (const auto& e : ents) { + if (e.id == m_entityId) { + hit = &e; + break; + } + } + if (hit) { + // 是否已有精确关键帧 + for (const auto& k : hit->imageFrames) { + if (k.frame == f + 1) { + hasExplicitKeyAtFPlus1 = true; + } + } + prevRelPathForF = core::sampleImagePath(hit->imageFrames, f, hit->imagePath); + prevRelPathForFPlus1 = core::sampleImagePath(hit->imageFrames, f + 1, hit->imagePath); + } + } + const QString path = QFileDialog::getOpenFileName( this, QStringLiteral("选择该帧图像"), @@ -195,6 +223,14 @@ void FrameAnimationDialog::onReplaceCurrentFrame() { QMessageBox::warning(this, QStringLiteral("动画帧"), QStringLiteral("写入该帧失败。")); return; } + + // “单帧替换”的强语义:无论 f 原本是否是关键帧,都不应影响 f+1 之后的帧。 + // 因此在 f+1 上补一个“替换前 f+1 使用的来源”,以切断 Hold 区间(不覆盖已有关键帧)。 + // 性能:这里直接写入相对路径,不读图不写盘。 + if (!hasExplicitKeyAtFPlus1 && !prevRelPathForFPlus1.isEmpty()) { + m_workspace.setEntityImageFramePath(m_entityId, f + 1, prevRelPathForFPlus1); + } + rebuildFrameList(); updatePreviewForFrame(f); }