增加摄像机
This commit is contained in:
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user