增加摄像机

This commit is contained in:
2026-04-23 13:11:36 +08:00
parent a78b290920
commit 974946cee4
12 changed files with 1134 additions and 23 deletions

View File

@@ -360,9 +360,55 @@ bool ProjectWorkspace::createNew(const QString& projectDir, const QString& name,
m_project.setSelectedStripId(strip.id);
}
if (!ensureDefaultCameraIfMissing(nullptr)) {
return false;
}
return writeIndexJson();
}
bool ProjectWorkspace::ensureDefaultCameraIfMissing(bool* outAdded) {
if (m_projectDir.isEmpty()) {
return false;
}
if (!m_project.cameras().isEmpty()) {
if (outAdded) {
*outAdded = false;
}
return true;
}
Project::Camera c;
c.id = QStringLiteral("camera-default");
c.displayName = QStringLiteral("主摄像机");
c.visible = true;
const QString bgAbs = backgroundAbsolutePath();
QSize sz;
if (!bgAbs.isEmpty() && QFileInfo::exists(bgAbs)) {
QImageReader reader(bgAbs);
reader.setAutoTransform(true);
sz = reader.size();
}
if (!sz.isValid() || sz.width() < 1 || sz.height() < 1) {
c.centerWorld = QPointF(512.0, 384.0);
c.viewScale = 1.0;
} else {
c.centerWorld = QPointF(sz.width() * 0.5, sz.height() * 0.5);
// 参考视口下「适配整张背景」的像素/世界单位比,与画布 zoomToFit 语义一致
constexpr double kRefViewportW = 1600.0;
constexpr double kRefViewportH = 900.0;
const double sw = kRefViewportW / static_cast<double>(sz.width());
const double sh = kRefViewportH / static_cast<double>(sz.height());
c.viewScale = std::clamp(std::min(sw, sh), 1e-6, 1e3);
}
m_project.setCameras({c});
if (m_project.activeCameraId().isEmpty()) {
m_project.setActiveCameraId(c.id);
}
if (outAdded) {
*outAdded = true;
}
return true;
}
bool ProjectWorkspace::openExisting(const QString& projectDir) {
const auto dir = normalizedProjectDir(projectDir);
const auto indexPath = QDir(dir).filePath(QString::fromUtf8(kProjectIndexFileName));
@@ -440,6 +486,13 @@ bool ProjectWorkspace::openExisting(const QString& projectDir) {
// 迁移后立即落盘,避免后续求值出现双来源
writeIndexJson();
}
bool cameraAdded = false;
if (!ensureDefaultCameraIfMissing(&cameraAdded)) {
return false;
}
if (cameraAdded && !writeIndexJson()) {
return false;
}
return true;
}
@@ -489,6 +542,11 @@ bool ProjectWorkspace::undo() {
m_undoStack.push_back(op);
return false;
}
} else if (op.type == Operation::Type::SetCameras) {
if (!applyCameras(op.beforeCameras, false, QString())) {
m_undoStack.push_back(op);
return false;
}
} else if (op.type == Operation::Type::SetProjectTitle) {
m_project.setName(op.beforeProjectTitle);
if (!writeIndexJson()) {
@@ -532,6 +590,11 @@ bool ProjectWorkspace::redo() {
m_redoStack.push_back(op);
return false;
}
} else if (op.type == Operation::Type::SetCameras) {
if (!applyCameras(op.afterCameras, false, QString())) {
m_redoStack.push_back(op);
return false;
}
} else if (op.type == Operation::Type::SetProjectTitle) {
m_project.setName(op.afterProjectTitle);
if (!writeIndexJson()) {
@@ -726,6 +789,13 @@ QJsonObject ProjectWorkspace::projectToJson(const Project& project) {
}
root.insert("tools", tools);
QJsonArray cams;
for (const auto& c : project.cameras()) {
cams.append(cameraToJson(c));
}
root.insert("cameras", cams);
root.insert("activeCameraId", project.activeCameraId());
// —— 动画v3——
root.insert("activeSchemeId", project.activeSchemeId());
root.insert("selectedStripId", project.selectedStripId());
@@ -792,6 +862,8 @@ QJsonObject ProjectWorkspace::projectToJson(const Project& project) {
co.insert("entityVisibilityKeys", encodeHash(c.entityVisibilityKeys, encodeBool));
co.insert("toolLocationKeys", encodeHash(c.toolLocationKeys, encodeVec2));
co.insert("toolVisibilityKeys", encodeHash(c.toolVisibilityKeys, encodeBool));
co.insert("cameraLocationKeys", encodeHash(c.cameraLocationKeys, encodeVec2));
co.insert("cameraScaleKeys", encodeHash(c.cameraScaleKeys, encodeDouble));
clips.append(co);
}
@@ -836,7 +908,7 @@ bool ProjectWorkspace::projectFromJson(const QJsonObject& root, Project& outProj
return false;
}
const int version = root.value("version").toInt();
if (version != 1 && version != 2 && version != 3) {
if (version != 1 && version != 2 && version != 3 && version != 4) {
return false;
}
if (outFileVersion) {
@@ -895,6 +967,24 @@ bool ProjectWorkspace::projectFromJson(const QJsonObject& root, Project& outProj
}
outProject.setTools(tools);
QVector<Project::Camera> cameras;
if (version >= 2) {
const auto camsVal = root.value("cameras");
if (camsVal.isArray()) {
const QJsonArray arr = camsVal.toArray();
cameras.reserve(arr.size());
for (const auto& v : arr) {
if (!v.isObject()) continue;
Project::Camera c;
if (cameraFromJsonV4(v.toObject(), c)) {
cameras.push_back(c);
}
}
}
}
outProject.setCameras(cameras);
outProject.setActiveCameraId(root.value("activeCameraId").toString());
// —— 动画v3可选——
if (version >= 3) {
outProject.setActiveSchemeId(root.value("activeSchemeId").toString());
@@ -988,6 +1078,8 @@ bool ProjectWorkspace::projectFromJson(const QJsonObject& root, Project& outProj
decodeHash(co.value("entityVisibilityKeys"), decodeBool, c.entityVisibilityKeys);
decodeHash(co.value("toolLocationKeys"), decodeVec2, c.toolLocationKeys);
decodeHash(co.value("toolVisibilityKeys"), decodeBool, c.toolVisibilityKeys);
decodeHash(co.value("cameraLocationKeys"), decodeVec2, c.cameraLocationKeys);
decodeHash(co.value("cameraScaleKeys"), decodeDouble, c.cameraScaleKeys);
clips.push_back(std::move(c));
}
@@ -1176,6 +1268,76 @@ bool ProjectWorkspace::toolFromJsonV2(const QJsonObject& o, Project::Tool& out)
return true;
}
QJsonObject ProjectWorkspace::cameraToJson(const Project::Camera& c) {
QJsonObject o;
o.insert("id", c.id);
o.insert("displayName", c.displayName);
o.insert("visible", c.visible);
o.insert("centerX", c.centerWorld.x());
o.insert("centerY", c.centerWorld.y());
o.insert("viewScale", c.viewScale);
QJsonArray loc;
for (const auto& k : c.locationKeys) {
QJsonObject ko;
ko.insert("frame", k.frame);
ko.insert("x", k.value.x());
ko.insert("y", k.value.y());
loc.append(ko);
}
o.insert("locationKeys", loc);
QJsonArray sc;
for (const auto& k : c.scaleKeys) {
QJsonObject ko;
ko.insert("frame", k.frame);
ko.insert("value", k.value);
sc.append(ko);
}
o.insert("scaleKeys", sc);
return o;
}
bool ProjectWorkspace::cameraFromJsonV4(const QJsonObject& o, Project::Camera& out) {
Project::Camera c;
c.id = o.value("id").toString();
if (c.id.isEmpty()) {
return false;
}
c.displayName = o.value("displayName").toString();
c.visible = o.value("visible").toBool(true);
c.centerWorld = QPointF(o.value("centerX").toDouble(0.0), o.value("centerY").toDouble(0.0));
c.viewScale = std::clamp(o.value("viewScale").toDouble(1.0), 1e-6, 1e3);
c.locationKeys.clear();
const auto locVal = o.value("locationKeys");
if (locVal.isArray()) {
const QJsonArray arr = locVal.toArray();
c.locationKeys.reserve(arr.size());
for (const auto& v : arr) {
if (!v.isObject()) continue;
const auto ko = v.toObject();
Project::Entity::KeyframeVec2 k;
k.frame = ko.value("frame").toInt(0);
k.value = QPointF(ko.value("x").toDouble(0.0), ko.value("y").toDouble(0.0));
c.locationKeys.push_back(k);
}
}
c.scaleKeys.clear();
const auto scVal = o.value("scaleKeys");
if (scVal.isArray()) {
const QJsonArray arr = scVal.toArray();
c.scaleKeys.reserve(arr.size());
for (const auto& v : arr) {
if (!v.isObject()) continue;
const auto ko = v.toObject();
Project::Entity::KeyframeDouble k;
k.frame = ko.value("frame").toInt(0);
k.value = std::clamp(ko.value("value").toDouble(1.0), 1e-6, 1e3);
c.scaleKeys.push_back(k);
}
}
out = std::move(c);
return true;
}
bool ProjectWorkspace::entityStubFromJsonV2(const QJsonObject& o, Project::Entity& out) {
out = Project::Entity{};
out.id = o.value("id").toString();
@@ -1412,6 +1574,27 @@ bool ProjectWorkspace::applyTools(const QVector<Project::Tool>& tools,
return true;
}
bool ProjectWorkspace::applyCameras(const QVector<Project::Camera>& cameras,
bool recordHistory,
const QString& label) {
const auto before = m_project.cameras();
m_project.setCameras(cameras);
if (!writeIndexJson()) {
m_project.setCameras(before);
return false;
}
if (recordHistory) {
Operation op;
op.type = Operation::Type::SetCameras;
op.label = label;
op.beforeCameras = before;
op.afterCameras = cameras;
pushOperation(op);
m_redoStack.clear();
}
return true;
}
QString ProjectWorkspace::ensureEntitiesDir() const {
const auto assets = assetsDirPath();
if (assets.isEmpty()) {
@@ -2400,6 +2583,214 @@ bool ProjectWorkspace::addTool(const Project::Tool& tool) {
return applyTools(tools, true, QStringLiteral("添加工具"));
}
bool ProjectWorkspace::setCameraVisible(const QString& id, bool on) {
if (m_projectDir.isEmpty() || id.isEmpty()) {
return false;
}
auto cams = m_project.cameras();
bool found = false;
bool changed = false;
for (auto& c : cams) {
if (c.id != id) continue;
found = true;
if (c.visible != on) {
c.visible = on;
changed = true;
}
break;
}
if (!found) return false;
if (!changed) return true;
return applyCameras(cams, true, on ? QStringLiteral("显示摄像机") : QStringLiteral("隐藏摄像机"));
}
bool ProjectWorkspace::setActiveCameraId(const QString& id) {
if (m_projectDir.isEmpty()) {
return false;
}
if (!id.isEmpty()) {
bool ok = false;
for (const auto& c : m_project.cameras()) {
if (c.id == id) {
ok = true;
break;
}
}
if (!ok) {
return false;
}
}
if (m_project.activeCameraId() == id) {
return true;
}
m_project.setActiveCameraId(id);
return writeIndexJson();
}
bool ProjectWorkspace::setCameraDisplayName(const QString& id, const QString& displayName) {
if (m_projectDir.isEmpty() || id.isEmpty()) {
return false;
}
auto cams = m_project.cameras();
bool found = false;
for (auto& c : cams) {
if (c.id != id) continue;
found = true;
c.displayName = displayName;
break;
}
if (!found) return false;
return applyCameras(cams, true, QStringLiteral("摄像机名称"));
}
bool ProjectWorkspace::setCameraCenterWorld(const QString& id, const QPointF& centerWorld) {
if (m_projectDir.isEmpty() || id.isEmpty()) {
return false;
}
auto cams = m_project.cameras();
bool found = false;
for (auto& c : cams) {
if (c.id != id) continue;
found = true;
c.centerWorld = centerWorld;
break;
}
if (!found) return false;
return applyCameras(cams, true, QStringLiteral("摄像机位置"));
}
bool ProjectWorkspace::setCameraViewScaleValue(const QString& id, double viewScale, int keyframeAtFrame) {
if (m_projectDir.isEmpty() || id.isEmpty()) {
return false;
}
const double v = std::clamp(viewScale, 1e-6, 1e3);
auto cams = m_project.cameras();
bool found = false;
for (auto& c : cams) {
if (c.id != id) continue;
found = true;
const bool baseSame = qFuzzyCompare(c.viewScale + 1.0, v + 1.0);
c.viewScale = v;
if (keyframeAtFrame >= 0) {
Project::AnimationClip* clip = activeClipOrNull(m_project);
if (!clip) return false;
auto& keys = clip->cameraScaleKeys[id];
upsertKey(keys, keyframeAtFrame, v);
} else if (baseSame) {
return true;
}
break;
}
if (!found) return false;
return applyCameras(cams, true, QStringLiteral("摄像机缩放"));
}
bool ProjectWorkspace::moveCameraBy(const QString& id, const QPointF& delta, int currentFrame, bool autoKeyLocation) {
if (m_projectDir.isEmpty() || id.isEmpty()) {
return false;
}
if (qFuzzyIsNull(delta.x()) && qFuzzyIsNull(delta.y())) {
return true;
}
auto cams = m_project.cameras();
bool found = false;
for (auto& c : cams) {
if (c.id != id) continue;
found = true;
Project::AnimationClip* clip = activeClipOrNull(m_project);
QVector<Project::Entity::KeyframeVec2>* keys = nullptr;
if (clip) {
keys = &clip->cameraLocationKeys[c.id];
}
const bool hasCurve = (keys && !keys->isEmpty()) || (!c.locationKeys.isEmpty());
if (autoKeyLocation && currentFrame >= 0) {
const QPointF sampled =
(keys) ? sampleLocation(*keys, currentFrame, c.centerWorld, KeyInterpolation::Linear)
: sampleLocation(c.locationKeys, currentFrame, c.centerWorld, KeyInterpolation::Linear);
if (keys) upsertKey(*keys, currentFrame, sampled + delta);
else upsertKey(c.locationKeys, currentFrame, sampled + delta);
} else if (!hasCurve) {
c.centerWorld += delta;
} else if (currentFrame >= 0) {
const QPointF sampled =
(keys) ? sampleLocation(*keys, currentFrame, c.centerWorld, KeyInterpolation::Linear)
: sampleLocation(c.locationKeys, currentFrame, c.centerWorld, KeyInterpolation::Linear);
if (keys) upsertKey(*keys, currentFrame, sampled + delta);
else upsertKey(c.locationKeys, currentFrame, sampled + delta);
} else {
c.centerWorld += delta;
}
break;
}
if (!found) return false;
return applyCameras(cams, true, QStringLiteral("移动摄像机"));
}
bool ProjectWorkspace::setCameraLocationKey(const QString& id, int frame, const QPointF& centerWorld) {
if (m_projectDir.isEmpty() || id.isEmpty() || frame < 0) {
return false;
}
Project::AnimationClip* clip = activeClipOrNull(m_project);
if (!clip) return false;
auto& keys = clip->cameraLocationKeys[id];
upsertKey(keys, frame, centerWorld);
return writeIndexJson();
}
bool ProjectWorkspace::setCameraScaleKey(const QString& id, int frame, double viewScale) {
if (m_projectDir.isEmpty() || id.isEmpty() || frame < 0) {
return false;
}
const double v = std::clamp(viewScale, 1e-6, 1e3);
Project::AnimationClip* clip = activeClipOrNull(m_project);
if (!clip) return false;
auto& keys = clip->cameraScaleKeys[id];
upsertKey(keys, frame, v);
return writeIndexJson();
}
bool ProjectWorkspace::removeCameraLocationKey(const QString& id, int frame) {
if (m_projectDir.isEmpty() || id.isEmpty()) {
return false;
}
Project::AnimationClip* clip = activeClipOrNull(m_project);
if (!clip) return false;
if (!clip->cameraLocationKeys.contains(id)) return false;
auto keys = clip->cameraLocationKeys.value(id);
bool removed = false;
for (int i = 0; i < keys.size(); ++i) {
if (keys[i].frame == frame) {
keys.removeAt(i);
removed = true;
break;
}
}
if (!removed) return false;
clip->cameraLocationKeys.insert(id, keys);
return writeIndexJson();
}
bool ProjectWorkspace::removeCameraScaleKey(const QString& id, int frame) {
if (m_projectDir.isEmpty() || id.isEmpty()) {
return false;
}
Project::AnimationClip* clip = activeClipOrNull(m_project);
if (!clip) return false;
if (!clip->cameraScaleKeys.contains(id)) return false;
auto keys = clip->cameraScaleKeys.value(id);
bool removed = false;
for (int i = 0; i < keys.size(); ++i) {
if (keys[i].frame == frame) {
keys.removeAt(i);
removed = true;
break;
}
}
if (!removed) return false;
clip->cameraScaleKeys.insert(id, keys);
return writeIndexJson();
}
bool ProjectWorkspace::setToolVisible(const QString& id, bool on) {
if (m_projectDir.isEmpty() || id.isEmpty()) {
return false;