增加摄像机
This commit is contained in:
@@ -148,6 +148,24 @@ public:
|
||||
void setTools(const QVector<Tool>& tools) { m_tools = tools; }
|
||||
const QVector<Tool>& tools() const { return m_tools; }
|
||||
|
||||
// —— 摄像机(正交俯视;centerWorld 为视口中心;viewScale 为相对参考分辨率 1600×900 的像素/世界单位比,
|
||||
// 预览与视口框按该参考换算,不随实际窗口宽高改变「镜头」所覆盖的世界范围)——
|
||||
struct Camera {
|
||||
QString id;
|
||||
QString displayName;
|
||||
bool visible = true;
|
||||
QPointF centerWorld;
|
||||
double viewScale = 1.0;
|
||||
QVector<Entity::KeyframeVec2> locationKeys;
|
||||
QVector<Entity::KeyframeDouble> scaleKeys;
|
||||
};
|
||||
|
||||
void setCameras(const QVector<Camera>& cameras) { m_cameras = cameras; }
|
||||
const QVector<Camera>& cameras() const { return m_cameras; }
|
||||
|
||||
void setActiveCameraId(const QString& id) { m_activeCameraId = id; }
|
||||
const QString& activeCameraId() const { return m_activeCameraId; }
|
||||
|
||||
// —— 动画系统(Blender/NLA 风格简化版,工程级)——
|
||||
struct AnimationClip {
|
||||
QString id;
|
||||
@@ -162,6 +180,9 @@ public:
|
||||
// Tool channels (keyed by tool id)
|
||||
QHash<QString, QVector<Entity::KeyframeVec2>> toolLocationKeys;
|
||||
QHash<QString, QVector<ToolKeyframeBool>> toolVisibilityKeys;
|
||||
|
||||
QHash<QString, QVector<Entity::KeyframeVec2>> cameraLocationKeys;
|
||||
QHash<QString, QVector<Entity::KeyframeDouble>> cameraScaleKeys;
|
||||
};
|
||||
|
||||
struct NlaStrip {
|
||||
@@ -247,6 +268,8 @@ private:
|
||||
int m_fps = 60;
|
||||
QVector<Entity> m_entities;
|
||||
QVector<Tool> m_tools;
|
||||
QVector<Camera> m_cameras;
|
||||
QString m_activeCameraId;
|
||||
|
||||
QVector<AnimationClip> m_clips;
|
||||
QVector<AnimationScheme> m_schemes;
|
||||
|
||||
@@ -45,6 +45,28 @@ QPointF sampledOriginForTool(const core::Project::Tool& t,
|
||||
return core::sampleLocation(t.locationKeys, localFrame, t.originWorld, core::KeyInterpolation::Linear);
|
||||
}
|
||||
|
||||
QPointF sampledCenterForCamera(const core::Project::Camera& c,
|
||||
const core::Project::AnimationClip* clipOrNull,
|
||||
int localFrame) {
|
||||
if (clipOrNull && clipOrNull->cameraLocationKeys.contains(c.id)) {
|
||||
const auto& keys = clipOrNull->cameraLocationKeys.value(c.id);
|
||||
return core::sampleLocation(keys, localFrame, c.centerWorld, core::KeyInterpolation::Linear);
|
||||
}
|
||||
return core::sampleLocation(c.locationKeys, localFrame, c.centerWorld, core::KeyInterpolation::Linear);
|
||||
}
|
||||
|
||||
double sampledViewScaleForCamera(const core::Project::Camera& c,
|
||||
const core::Project::AnimationClip* clipOrNull,
|
||||
int localFrame) {
|
||||
if (clipOrNull && clipOrNull->cameraScaleKeys.contains(c.id)) {
|
||||
const auto& keys = clipOrNull->cameraScaleKeys.value(c.id);
|
||||
if (!keys.isEmpty()) {
|
||||
return core::sampleUserScale(keys, localFrame, c.viewScale, core::KeyInterpolation::Linear);
|
||||
}
|
||||
}
|
||||
return core::sampleUserScale(c.scaleKeys, localFrame, c.viewScale, core::KeyInterpolation::Linear);
|
||||
}
|
||||
|
||||
QPointF sampledRelativeForTool(const core::Project::Tool& t,
|
||||
const core::Project::AnimationClip* clipOrNull,
|
||||
int localFrame) {
|
||||
@@ -201,8 +223,10 @@ ResolvedProjectFrame evaluateAtFrame(const core::Project& project, int frame, in
|
||||
ResolvedProjectFrame out;
|
||||
const auto& ents = project.entities();
|
||||
const auto& tools = project.tools();
|
||||
const auto& cams = project.cameras();
|
||||
out.entities.reserve(ents.size());
|
||||
out.tools.reserve(tools.size());
|
||||
out.cameras.reserve(cams.size());
|
||||
|
||||
const StripEvalCtx ctx = resolveStripCtx(project, frame);
|
||||
const int localFrame = ctx.localFrame;
|
||||
@@ -324,6 +348,13 @@ ResolvedProjectFrame evaluateAtFrame(const core::Project& project, int frame, in
|
||||
out.tools.push_back(ResolvedTool{t, op});
|
||||
}
|
||||
|
||||
for (const auto& c : cams) {
|
||||
core::Project::Camera cam = c;
|
||||
cam.centerWorld = sampledCenterForCamera(c, clip, localFrame);
|
||||
cam.viewScale = sampledViewScaleForCamera(c, clip, localFrame);
|
||||
out.cameras.push_back(ResolvedCamera{std::move(cam)});
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
|
||||
@@ -19,9 +19,14 @@ struct ResolvedTool {
|
||||
double opacity = 1.0; // 0..1
|
||||
};
|
||||
|
||||
struct ResolvedCamera {
|
||||
core::Project::Camera camera; // 拷贝:centerWorld / viewScale 已替换为求值后的值
|
||||
};
|
||||
|
||||
struct ResolvedProjectFrame {
|
||||
QVector<ResolvedEntity> entities;
|
||||
QVector<ResolvedTool> tools;
|
||||
QVector<ResolvedCamera> cameras;
|
||||
};
|
||||
|
||||
/// 逐帧求值:处理父子跟随与工具可见性淡入淡出。
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -17,7 +17,7 @@ public:
|
||||
static constexpr const char* kProjectIndexFileName = "project.json";
|
||||
static constexpr const char* kAssetsDirName = "assets";
|
||||
// 写入 project.json 的 version 字段;仍可读 version 1(内嵌实体 + 可选 .anim)。
|
||||
static constexpr int kProjectIndexFormatVersion = 3;
|
||||
static constexpr int kProjectIndexFormatVersion = 4;
|
||||
|
||||
ProjectWorkspace() = default;
|
||||
|
||||
@@ -83,6 +83,7 @@ public:
|
||||
|
||||
const QVector<Project::Entity>& entities() const { return m_project.entities(); }
|
||||
const QVector<Project::Tool>& tools() const { return m_project.tools(); }
|
||||
const QVector<Project::Camera>& cameras() const { return m_project.cameras(); }
|
||||
|
||||
bool addTool(const Project::Tool& tool);
|
||||
bool setToolVisible(const QString& id, bool on);
|
||||
@@ -94,6 +95,17 @@ public:
|
||||
bool removeToolVisibilityKey(const QString& id, int frame);
|
||||
bool setToolParent(const QString& id, const QString& parentId, const QPointF& parentOffsetWorld);
|
||||
bool moveToolBy(const QString& id, const QPointF& delta, int currentFrame, bool autoKeyLocation);
|
||||
bool setCameraVisible(const QString& id, bool on);
|
||||
bool setActiveCameraId(const QString& id);
|
||||
bool setCameraDisplayName(const QString& id, const QString& displayName);
|
||||
bool setCameraCenterWorld(const QString& id, const QPointF& centerWorld);
|
||||
/// keyframeAtFrame >= 0 时同时写入当前活动 clip 的缩放关键帧(与实体 userScale 行为一致)
|
||||
bool setCameraViewScaleValue(const QString& id, double viewScale, int keyframeAtFrame = -1);
|
||||
bool moveCameraBy(const QString& id, const QPointF& delta, int currentFrame, bool autoKeyLocation);
|
||||
bool setCameraLocationKey(const QString& id, int frame, const QPointF& centerWorld);
|
||||
bool setCameraScaleKey(const QString& id, int frame, double viewScale);
|
||||
bool removeCameraLocationKey(const QString& id, int frame);
|
||||
bool removeCameraScaleKey(const QString& id, int frame);
|
||||
bool addEntity(const Project::Entity& entity, const QImage& image);
|
||||
bool setEntityVisible(const QString& id, bool on);
|
||||
bool setEntityBlackholeVisible(const QString& id, bool on);
|
||||
@@ -155,9 +167,14 @@ private:
|
||||
static bool entityStubFromJsonV2(const QJsonObject& o, Project::Entity& out);
|
||||
static QJsonObject toolToJson(const Project::Tool& t);
|
||||
static bool toolFromJsonV2(const QJsonObject& o, Project::Tool& out);
|
||||
static QJsonObject cameraToJson(const Project::Camera& c);
|
||||
static bool cameraFromJsonV4(const QJsonObject& o, Project::Camera& out);
|
||||
|
||||
/// 若工程尚无摄像机,则创建默认主摄像机(居中背景、缩放适配整张背景图)。不写盘;若创建了摄像机且 outAdded 非空则 *outAdded=true。
|
||||
bool ensureDefaultCameraIfMissing(bool* outAdded = nullptr);
|
||||
|
||||
struct Operation {
|
||||
enum class Type { ImportBackground, SetEntities, SetTools, SetProjectTitle, SetProjectFrameRange };
|
||||
enum class Type { ImportBackground, SetEntities, SetTools, SetCameras, SetProjectTitle, SetProjectFrameRange };
|
||||
Type type {Type::ImportBackground};
|
||||
QString label;
|
||||
QString beforeBackgroundPath;
|
||||
@@ -166,6 +183,8 @@ private:
|
||||
QVector<Project::Entity> afterEntities;
|
||||
QVector<Project::Tool> beforeTools;
|
||||
QVector<Project::Tool> afterTools;
|
||||
QVector<Project::Camera> beforeCameras;
|
||||
QVector<Project::Camera> afterCameras;
|
||||
QString beforeProjectTitle;
|
||||
QString afterProjectTitle;
|
||||
int beforeFrameStart = 0;
|
||||
@@ -180,6 +199,7 @@ private:
|
||||
bool applyBackgroundPath(const QString& relativePath, bool recordHistory, const QString& label);
|
||||
bool applyEntities(const QVector<Project::Entity>& entities, bool recordHistory, const QString& label);
|
||||
bool applyTools(const QVector<Project::Tool>& tools, bool recordHistory, const QString& label);
|
||||
bool applyCameras(const QVector<Project::Camera>& cameras, bool recordHistory, const QString& label);
|
||||
QString copyIntoAssetsAsBackground(const QString& sourceFilePath, const QRect& cropRectInSourceImage);
|
||||
bool writeDepthMap(const QImage& depth8);
|
||||
bool writeDepthMapBytes(const QByteArray& pngBytes);
|
||||
|
||||
@@ -23,6 +23,7 @@ set(GUI_SOURCES
|
||||
${GUI_ROOT}/props/BlackholePropertySection.cpp
|
||||
${GUI_ROOT}/props/EntityPropertySection.cpp
|
||||
${GUI_ROOT}/props/ToolPropertySection.cpp
|
||||
${GUI_ROOT}/props/CameraPropertySection.cpp
|
||||
${GUI_ROOT}/timeline/TimelineWidget.cpp
|
||||
)
|
||||
|
||||
@@ -47,6 +48,7 @@ set(GUI_HEADERS
|
||||
${GUI_ROOT}/props/BlackholePropertySection.h
|
||||
${GUI_ROOT}/props/EntityPropertySection.h
|
||||
${GUI_ROOT}/props/ToolPropertySection.h
|
||||
${GUI_ROOT}/props/CameraPropertySection.h
|
||||
${GUI_ROOT}/props/PropertySectionWidget.h
|
||||
${GUI_ROOT}/timeline/TimelineWidget.h
|
||||
)
|
||||
|
||||
@@ -33,6 +33,10 @@
|
||||
|
||||
namespace {
|
||||
|
||||
// 摄像机「输出」参考分辨率:视口框与预览缩放均按此换算,避免随窗口大小改变镜头覆盖的世界范围
|
||||
constexpr double kCameraRefViewportW = 1600.0;
|
||||
constexpr double kCameraRefViewportH = 900.0;
|
||||
|
||||
constexpr int kSamCropMargin = 32;
|
||||
constexpr int kMinStrokePointsSam = 4;
|
||||
constexpr int kMinStrokePointsManual = 8;
|
||||
@@ -63,6 +67,13 @@ static QImage readImageTolerant(const QString& absPath) {
|
||||
return reader.read();
|
||||
}
|
||||
|
||||
QRectF cameraWorldViewportRect(const core::Project::Camera& cam) {
|
||||
const double s = std::max(1e-9, cam.viewScale);
|
||||
const double halfW = (kCameraRefViewportW * 0.5) / s;
|
||||
const double halfH = (kCameraRefViewportH * 0.5) / s;
|
||||
return QRectF(cam.centerWorld.x() - halfW, cam.centerWorld.y() - halfH, 2.0 * halfW, 2.0 * halfH);
|
||||
}
|
||||
|
||||
QRectF clampRectTopLeftToBounds(const QRectF& rect, const QRectF& bounds) {
|
||||
if (rect.isNull() || bounds.isNull()) {
|
||||
return rect;
|
||||
@@ -518,6 +529,26 @@ void EditorCanvas::dropEvent(QDropEvent* e) {
|
||||
e->acceptProposedAction();
|
||||
}
|
||||
|
||||
void EditorCanvas::setPreviewCameraViewLocked(bool on) {
|
||||
m_previewCameraViewLocked = on;
|
||||
}
|
||||
|
||||
void EditorCanvas::applyCameraViewport(const QPointF& centerWorld, double viewScale) {
|
||||
// 与 cameraWorldViewportRect 一致:viewScale 表示在 1600×900 参考视口下的像素/世界比;
|
||||
// 实际画布用 min(宽/1600, 高/900) 将参考视口适配进当前控件,使可见世界宽高恒为 1600/s × 900/s。
|
||||
const double pixelRatio =
|
||||
std::min(static_cast<double>(std::max(1, width())) / kCameraRefViewportW,
|
||||
static_cast<double>(std::max(1, height())) / kCameraRefViewportH);
|
||||
const double eff = std::max(1e-9, static_cast<double>(viewScale)) * pixelRatio;
|
||||
m_scale = std::clamp(static_cast<qreal>(eff), 0.05, 50.0);
|
||||
m_pan = QPointF(width() / 2.0, height() / 2.0) - QPointF(centerWorld.x() * m_scale, centerWorld.y() * m_scale);
|
||||
update();
|
||||
}
|
||||
|
||||
QPointF EditorCanvas::viewCenterWorld() const {
|
||||
return viewToWorld(QPointF(width() / 2.0, height() / 2.0));
|
||||
}
|
||||
|
||||
void EditorCanvas::setPresentationPreviewMode(bool on) {
|
||||
if (m_presentationPreviewMode == on) {
|
||||
return;
|
||||
@@ -715,12 +746,73 @@ void EditorCanvas::setTools(const QVector<core::Project::Tool>& tools, const QVe
|
||||
update();
|
||||
}
|
||||
|
||||
void EditorCanvas::setCameraOverlays(const QVector<core::Project::Camera>& cameras,
|
||||
const QString& selectedId,
|
||||
const QSet<QString>& tempHiddenCameraIds) {
|
||||
m_cameraOverlays = cameras;
|
||||
m_tempHiddenCameraIds = tempHiddenCameraIds;
|
||||
m_selectedCameraId = selectedId;
|
||||
m_selectedCameraIndex = -1;
|
||||
if (!selectedId.isEmpty()) {
|
||||
for (int i = 0; i < m_cameraOverlays.size(); ++i) {
|
||||
if (m_cameraOverlays[i].id == selectedId) {
|
||||
m_selectedCameraIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (m_selectedCameraIndex < 0) {
|
||||
m_selectedCameraId.clear();
|
||||
m_draggingCamera = false;
|
||||
}
|
||||
update();
|
||||
}
|
||||
|
||||
void EditorCanvas::selectCameraById(const QString& id) {
|
||||
if (id.isEmpty()) {
|
||||
clearCameraSelection();
|
||||
return;
|
||||
}
|
||||
clearEntitySelection();
|
||||
m_selectedTool = -1;
|
||||
m_draggingTool = false;
|
||||
emit selectedToolChanged(false, QString(), QPointF());
|
||||
clearBlackholeSelection();
|
||||
for (int i = 0; i < m_cameraOverlays.size(); ++i) {
|
||||
if (m_cameraOverlays[i].id == id) {
|
||||
m_selectedCameraIndex = i;
|
||||
m_selectedCameraId = id;
|
||||
const auto& c = m_cameraOverlays[i];
|
||||
emit selectedCameraChanged(true, id, c.centerWorld, c.viewScale);
|
||||
update();
|
||||
return;
|
||||
}
|
||||
}
|
||||
clearCameraSelection();
|
||||
}
|
||||
|
||||
void EditorCanvas::clearCameraSelection() {
|
||||
if (m_selectedCameraId.isEmpty() && m_selectedCameraIndex < 0 && !m_draggingCamera) {
|
||||
return;
|
||||
}
|
||||
m_selectedCameraId.clear();
|
||||
m_selectedCameraIndex = -1;
|
||||
m_draggingCamera = false;
|
||||
emit selectedCameraChanged(false, QString(), QPointF(), 1.0);
|
||||
update();
|
||||
}
|
||||
|
||||
void EditorCanvas::setTempHiddenIds(const QSet<QString>& entityIds, const QSet<QString>& toolIds) {
|
||||
m_tempHiddenEntityIds = entityIds;
|
||||
m_tempHiddenToolIds = toolIds;
|
||||
update();
|
||||
}
|
||||
|
||||
void EditorCanvas::setTempHiddenCameraIds(const QSet<QString>& cameraIds) {
|
||||
m_tempHiddenCameraIds = cameraIds;
|
||||
update();
|
||||
}
|
||||
|
||||
void EditorCanvas::setCurrentFrame(int frame) {
|
||||
if (m_currentFrame == frame) {
|
||||
return;
|
||||
@@ -879,8 +971,10 @@ void EditorCanvas::clearEntitySelection() {
|
||||
void EditorCanvas::selectEntityById(const QString& id) {
|
||||
if (id.isEmpty()) {
|
||||
clearEntitySelection();
|
||||
clearCameraSelection();
|
||||
return;
|
||||
}
|
||||
clearCameraSelection();
|
||||
for (int i = 0; i < m_entities.size(); ++i) {
|
||||
if (m_entities[i].id != id) {
|
||||
continue;
|
||||
@@ -1608,6 +1702,30 @@ void EditorCanvas::paintEvent(QPaintEvent* e) {
|
||||
}
|
||||
}
|
||||
|
||||
// 摄像机视口框(编辑模式)
|
||||
if (!m_presentationPreviewMode) {
|
||||
const qreal handleRWorld = 10.0 / std::max(m_scale, 0.001);
|
||||
for (int i = 0; i < m_cameraOverlays.size(); ++i) {
|
||||
const auto& cam = m_cameraOverlays[i];
|
||||
if (!cam.visible || cam.id.isEmpty() || m_tempHiddenCameraIds.contains(cam.id)) {
|
||||
continue;
|
||||
}
|
||||
const QRectF camRect = cameraWorldViewportRect(cam);
|
||||
QColor fill(80, 140, 255, 38);
|
||||
QColor border(80, 140, 255, 170);
|
||||
if (i == m_selectedCameraIndex) {
|
||||
border = QColor(255, 170, 60, 230);
|
||||
fill = QColor(255, 170, 60, 48);
|
||||
}
|
||||
p.setBrush(fill);
|
||||
p.setPen(QPen(border, 1.5 / std::max(m_scale, 0.001)));
|
||||
p.drawRect(camRect);
|
||||
p.setBrush(QColor(255, 210, 120, 230));
|
||||
p.setPen(QPen(QColor(30, 30, 30, 160), 1.0 / std::max(m_scale, 0.001)));
|
||||
p.drawEllipse(cam.centerWorld, handleRWorld, handleRWorld);
|
||||
}
|
||||
}
|
||||
|
||||
// 创建实体:手绘轨迹预览(world 坐标)
|
||||
if (!m_presentationPreviewMode && m_tool == Tool::CreateEntity && m_drawingEntity && m_strokeWorld.size() >= 2) {
|
||||
QPen pen(QColor(255, 120, 0, 220), 2.0 / std::max<qreal>(m_scale, 0.001));
|
||||
@@ -1840,11 +1958,13 @@ void EditorCanvas::mousePressEvent(QMouseEvent* e) {
|
||||
emit presentationEntityIntroRequested(ent.id, anchorView);
|
||||
return;
|
||||
}
|
||||
if (!m_previewCameraViewLocked) {
|
||||
m_dragging = true;
|
||||
m_presBgPanSession = true;
|
||||
m_presBgDragDist = 0.0;
|
||||
m_lastMouseView = e->position();
|
||||
setCursor(Qt::ClosedHandCursor);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (e->button() == Qt::MiddleButton) {
|
||||
@@ -1905,6 +2025,42 @@ void EditorCanvas::mousePressEvent(QMouseEvent* e) {
|
||||
}
|
||||
|
||||
if (m_tool == Tool::Move && e->button() == Qt::LeftButton) {
|
||||
// 摄像机:绘制在工具之上,命中优先于工具
|
||||
if (!m_presentationPreviewMode) {
|
||||
const qreal handleRWorld = 12.0 / std::max(m_scale, 0.001);
|
||||
for (int idx = static_cast<int>(m_cameraOverlays.size()) - 1; idx >= 0; --idx) {
|
||||
const auto& cam = m_cameraOverlays[idx];
|
||||
if (!cam.visible || cam.id.isEmpty() || m_tempHiddenCameraIds.contains(cam.id)) {
|
||||
continue;
|
||||
}
|
||||
const QRectF camRect = cameraWorldViewportRect(cam);
|
||||
const double dist = QLineF(worldPos, cam.centerWorld).length();
|
||||
const bool inHandle = dist <= handleRWorld;
|
||||
const bool inRect = camRect.contains(worldPos);
|
||||
if (!inHandle && !inRect) {
|
||||
continue;
|
||||
}
|
||||
m_selectedCameraIndex = idx;
|
||||
m_selectedCameraId = cam.id;
|
||||
m_selectedEntity = -1;
|
||||
m_selectedTool = -1;
|
||||
m_draggingTool = false;
|
||||
m_draggingEntity = false;
|
||||
emit selectedEntityChanged(false, QString(), 0, QPointF());
|
||||
emit selectedToolChanged(false, QString(), QPointF());
|
||||
if (inHandle) {
|
||||
m_draggingCamera = true;
|
||||
m_cameraDragOffsetWorld = worldPos - cam.centerWorld;
|
||||
m_cameraDragStartCenterWorld = cam.centerWorld;
|
||||
} else {
|
||||
m_draggingCamera = false;
|
||||
}
|
||||
emit selectedCameraChanged(true, cam.id, cam.centerWorld, cam.viewScale);
|
||||
update();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 工具(气泡)优先命中:绘制在实体之后,交互也应优先
|
||||
for (qsizetype i = m_tools.size(); i > 0; --i) {
|
||||
const qsizetype idx = i - 1;
|
||||
@@ -1914,6 +2070,7 @@ void EditorCanvas::mousePressEvent(QMouseEvent* e) {
|
||||
if (tv.tool.type != core::Project::Tool::Type::Bubble) continue;
|
||||
const QPainterPath path = bubblePathWorld(tv.tool);
|
||||
if (path.contains(worldPos)) {
|
||||
clearCameraSelection();
|
||||
m_selectedTool = static_cast<int>(idx);
|
||||
m_selectedEntity = -1;
|
||||
m_draggingTool = true;
|
||||
@@ -1940,6 +2097,7 @@ void EditorCanvas::mousePressEvent(QMouseEvent* e) {
|
||||
const QPointF originView = worldToView(originWorld);
|
||||
const GizmoHit gh = hitTestGizmo(e->position(), originView);
|
||||
if (gh.mode == DragMode::AxisX || gh.mode == DragMode::AxisY) {
|
||||
clearCameraSelection();
|
||||
m_dragging = true;
|
||||
m_draggingEntity = true;
|
||||
m_dragMode = gh.mode;
|
||||
@@ -1983,6 +2141,7 @@ void EditorCanvas::mousePressEvent(QMouseEvent* e) {
|
||||
hitSelected = ent.rect.contains(worldPos);
|
||||
}
|
||||
if (hitSelected) {
|
||||
clearCameraSelection();
|
||||
m_draggingEntity = true;
|
||||
m_dragMode = DragMode::Free;
|
||||
emit entityDragActiveChanged(true);
|
||||
@@ -2011,6 +2170,7 @@ void EditorCanvas::mousePressEvent(QMouseEvent* e) {
|
||||
|
||||
const int hit = hitTestEntity(worldPos);
|
||||
if (hit >= 0) {
|
||||
clearCameraSelection();
|
||||
m_selectedEntity = hit;
|
||||
m_selectedTool = -1;
|
||||
m_draggingTool = false;
|
||||
@@ -2047,6 +2207,7 @@ void EditorCanvas::mousePressEvent(QMouseEvent* e) {
|
||||
m_selectedTool = -1;
|
||||
m_draggingTool = false;
|
||||
m_dragMode = DragMode::None;
|
||||
clearCameraSelection();
|
||||
emit selectedEntityChanged(false, QString(), 0, QPointF());
|
||||
emit selectedToolChanged(false, QString(), QPointF());
|
||||
update();
|
||||
@@ -2144,6 +2305,17 @@ void EditorCanvas::mouseMoveEvent(QMouseEvent* e) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_draggingCamera && m_selectedCameraIndex >= 0 && m_selectedCameraIndex < m_cameraOverlays.size()) {
|
||||
const QPointF worldPos = viewToWorld(cur);
|
||||
const QPointF newCenter = worldPos - m_cameraDragOffsetWorld;
|
||||
QPointF delta = newCenter - m_cameraOverlays[m_selectedCameraIndex].centerWorld;
|
||||
m_cameraOverlays[m_selectedCameraIndex].centerWorld += delta;
|
||||
const auto& c = m_cameraOverlays[m_selectedCameraIndex];
|
||||
emit selectedCameraChanged(true, c.id, c.centerWorld, c.viewScale);
|
||||
update();
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_draggingEntity && m_selectedEntity >= 0 && m_selectedEntity < m_entities.size()) {
|
||||
const QPointF worldPos = viewToWorld(cur);
|
||||
auto& ent = m_entities[m_selectedEntity];
|
||||
@@ -2229,10 +2401,12 @@ void EditorCanvas::mouseMoveEvent(QMouseEvent* e) {
|
||||
// 平移画布
|
||||
if (m_tool == Tool::Move || (e->buttons() & Qt::MiddleButton) ||
|
||||
(m_presentationPreviewMode && (e->buttons() & Qt::LeftButton))) {
|
||||
if (!(m_presentationPreviewMode && m_previewCameraViewLocked)) {
|
||||
if (m_presentationPreviewMode && m_presBgPanSession) {
|
||||
m_presBgDragDist += std::abs(deltaView.x()) + std::abs(deltaView.y());
|
||||
}
|
||||
m_pan += deltaView;
|
||||
}
|
||||
update();
|
||||
return;
|
||||
}
|
||||
@@ -2321,6 +2495,17 @@ void EditorCanvas::mouseReleaseEvent(QMouseEvent* e) {
|
||||
}
|
||||
}
|
||||
|
||||
if (m_draggingCamera && m_selectedCameraIndex >= 0 && m_selectedCameraIndex < m_cameraOverlays.size() &&
|
||||
e->button() == Qt::LeftButton) {
|
||||
const auto& cam = m_cameraOverlays[m_selectedCameraIndex];
|
||||
const QPointF delta = cam.centerWorld - m_cameraDragStartCenterWorld;
|
||||
if (!cam.id.isEmpty() && (!qFuzzyIsNull(delta.x()) || !qFuzzyIsNull(delta.y()))) {
|
||||
emit requestMoveCamera(cam.id, delta);
|
||||
} else if (!cam.id.isEmpty()) {
|
||||
emit selectedCameraChanged(true, cam.id, cam.centerWorld, cam.viewScale);
|
||||
}
|
||||
}
|
||||
|
||||
m_dragging = false;
|
||||
if (m_pendingDragging && e->button() == Qt::LeftButton) {
|
||||
m_pendingDragging = false;
|
||||
@@ -2332,6 +2517,7 @@ void EditorCanvas::mouseReleaseEvent(QMouseEvent* e) {
|
||||
}
|
||||
m_draggingEntity = false;
|
||||
m_draggingTool = false;
|
||||
m_draggingCamera = false;
|
||||
m_dragPreviewActive = false;
|
||||
m_dragMode = DragMode::None;
|
||||
updateCursor();
|
||||
@@ -2340,6 +2526,19 @@ void EditorCanvas::mouseReleaseEvent(QMouseEvent* e) {
|
||||
}
|
||||
|
||||
void EditorCanvas::wheelEvent(QWheelEvent* e) {
|
||||
if (m_presentationPreviewMode && m_previewCameraViewLocked) {
|
||||
e->accept();
|
||||
return;
|
||||
}
|
||||
if (!m_presentationPreviewMode && !m_selectedCameraId.isEmpty() && m_tool == Tool::Move) {
|
||||
const qreal steps = e->angleDelta().y() / 120.0;
|
||||
const qreal factor = std::pow(1.15, steps);
|
||||
if (!qFuzzyCompare(factor, 1.0)) {
|
||||
emit requestCameraViewScaleAdjust(m_selectedCameraId, factor);
|
||||
}
|
||||
e->accept();
|
||||
return;
|
||||
}
|
||||
if (m_tool != Tool::Zoom && !(e->modifiers() & Qt::ControlModifier)) {
|
||||
// 默认仍允许滚轮缩放:不强制用户切换工具
|
||||
//(若你希望仅在 Zoom 工具下才缩放,可在此 return)
|
||||
|
||||
@@ -71,6 +71,13 @@ public:
|
||||
// 预览呈现:完整背景 + 全部实体(忽略显隐开关),隐藏编辑辅助元素,仅可平移/缩放查看
|
||||
void setPresentationPreviewMode(bool on);
|
||||
bool presentationPreviewMode() const { return m_presentationPreviewMode; }
|
||||
/// 预览展示由活动摄像机驱动时禁止画布平移/滚轮缩放,避免与镜头关键帧冲突
|
||||
void setPreviewCameraViewLocked(bool on);
|
||||
bool previewCameraViewLocked() const { return m_previewCameraViewLocked; }
|
||||
/// 将视口对齐到摄像机;viewScale 为相对参考分辨率 1600×900 的像素/世界比(见 Project::Camera)
|
||||
void applyCameraViewport(const QPointF& centerWorld, double viewScale);
|
||||
QPointF viewCenterWorld() const;
|
||||
double viewWorldScale() const { return m_scale; }
|
||||
/// 退出「点击实体放大」状态并平滑回到进入前的视图(预览模式)
|
||||
void clearPresentationEntityFocus();
|
||||
|
||||
@@ -78,7 +85,13 @@ public:
|
||||
const QVector<double>& opacities01,
|
||||
const QString& projectDirAbs);
|
||||
void setTools(const QVector<core::Project::Tool>& tools, const QVector<double>& opacities01);
|
||||
void setCameraOverlays(const QVector<core::Project::Camera>& cameras,
|
||||
const QString& selectedId,
|
||||
const QSet<QString>& tempHiddenCameraIds);
|
||||
void selectCameraById(const QString& id);
|
||||
void clearCameraSelection();
|
||||
void setTempHiddenIds(const QSet<QString>& entityIds, const QSet<QString>& toolIds);
|
||||
void setTempHiddenCameraIds(const QSet<QString>& cameraIds);
|
||||
void setCurrentFrame(int frame);
|
||||
int currentFrame() const { return m_currentFrame; }
|
||||
|
||||
@@ -122,6 +135,9 @@ signals:
|
||||
void requestFinalizePendingEntity(const QVector<QPointF>& polyWorld);
|
||||
void requestMoveEntity(const QString& id, const QPointF& delta);
|
||||
void requestMoveTool(const QString& id, const QPointF& delta);
|
||||
void requestMoveCamera(const QString& id, const QPointF& delta);
|
||||
void requestCameraViewScaleAdjust(const QString& id, double factor);
|
||||
void selectedCameraChanged(bool hasSelection, const QString& id, const QPointF& centerWorld, double viewScale);
|
||||
void requestResolveBlackholeCopy(const QString& entityId, const QPoint& sourceOffsetPx);
|
||||
void entityDragActiveChanged(bool on);
|
||||
void selectedEntityPreviewChanged(const QString& id, int depth, const QPointF& originWorld);
|
||||
@@ -255,6 +271,14 @@ private:
|
||||
|
||||
QVector<Entity> m_entities;
|
||||
QVector<ToolView> m_tools;
|
||||
QVector<core::Project::Camera> m_cameraOverlays;
|
||||
QSet<QString> m_tempHiddenCameraIds;
|
||||
QString m_selectedCameraId;
|
||||
int m_selectedCameraIndex = -1;
|
||||
bool m_draggingCamera = false;
|
||||
QPointF m_cameraDragOffsetWorld;
|
||||
QPointF m_cameraDragStartCenterWorld;
|
||||
bool m_previewCameraViewLocked = false;
|
||||
QSet<QString> m_tempHiddenEntityIds;
|
||||
QSet<QString> m_tempHiddenToolIds;
|
||||
QVector<QPointF> m_strokeWorld;
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
#include "props/BlackholePropertySection.h"
|
||||
#include "props/EntityPropertySection.h"
|
||||
#include "props/ToolPropertySection.h"
|
||||
#include "props/CameraPropertySection.h"
|
||||
#include "timeline/TimelineWidget.h"
|
||||
#include "dialogs/FrameAnimationDialog.h"
|
||||
#include "dialogs/EntityIntroPopup.h"
|
||||
@@ -559,7 +560,7 @@ void MainWindow::createTimelineDock() {
|
||||
// 合并后的关键帧按钮:一次写入位置 + userScale
|
||||
auto* btnKeyCombined = new QToolButton(bar);
|
||||
setToolButtonIconOrText(btnKeyCombined, QStringLiteral("media-record"), QStringLiteral("关键帧"));
|
||||
btnKeyCombined->setToolTip(QStringLiteral("在当前帧记录实体的位置与缩放关键帧"));
|
||||
btnKeyCombined->setToolTip(QStringLiteral("在当前帧记录实体或摄像机的位置与缩放关键帧"));
|
||||
polishCompactToolButton(btnKeyCombined, 34);
|
||||
layout->addWidget(btnKeyCombined);
|
||||
|
||||
@@ -611,7 +612,27 @@ void MainWindow::createTimelineDock() {
|
||||
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);
|
||||
@@ -710,7 +731,9 @@ void MainWindow::createTimelineDock() {
|
||||
|
||||
const bool entityKeyUi = (m_workspace.isOpen() && !m_selectedEntityId.isEmpty());
|
||||
const bool toolKeyUi = (m_workspace.isOpen() && m_hasSelectedTool && !m_selectedToolId.isEmpty());
|
||||
actDeleteKey->setEnabled(m_workspace.isOpen() && m_timeline->hasSelectedKeyframe() && (entityKeyUi || toolKeyUi));
|
||||
const bool cameraKeyUi = (m_workspace.isOpen() && m_hasSelectedCamera && !m_selectedCameraId.isEmpty());
|
||||
actDeleteKey->setEnabled(m_workspace.isOpen() && m_timeline->hasSelectedKeyframe() &&
|
||||
(entityKeyUi || toolKeyUi || cameraKeyUi));
|
||||
const int selA = m_timeline->selectionStart();
|
||||
const int selB = m_timeline->selectionEnd();
|
||||
const bool hasRange = (selA >= 0 && selB >= 0);
|
||||
@@ -734,10 +757,18 @@ void MainWindow::createTimelineDock() {
|
||||
bool ok = false;
|
||||
switch (m_timeline->selectedKeyKind()) {
|
||||
case TimelineWidget::KeyKind::Location:
|
||||
if (!m_selectedEntityId.isEmpty()) ok = m_workspace.removeEntityLocationKey(m_selectedEntityId, f);
|
||||
if (!m_selectedEntityId.isEmpty()) {
|
||||
ok = m_workspace.removeEntityLocationKey(m_selectedEntityId, f);
|
||||
} else if (m_hasSelectedCamera && !m_selectedCameraId.isEmpty()) {
|
||||
ok = m_workspace.removeCameraLocationKey(m_selectedCameraId, f);
|
||||
}
|
||||
break;
|
||||
case TimelineWidget::KeyKind::UserScale:
|
||||
if (!m_selectedEntityId.isEmpty()) ok = m_workspace.removeEntityUserScaleKey(m_selectedEntityId, f);
|
||||
if (!m_selectedEntityId.isEmpty()) {
|
||||
ok = m_workspace.removeEntityUserScaleKey(m_selectedEntityId, f);
|
||||
} else if (m_hasSelectedCamera && !m_selectedCameraId.isEmpty()) {
|
||||
ok = m_workspace.removeCameraScaleKey(m_selectedCameraId, f);
|
||||
}
|
||||
break;
|
||||
case TimelineWidget::KeyKind::Image:
|
||||
if (!m_selectedEntityId.isEmpty()) ok = m_workspace.removeEntityImageFrame(m_selectedEntityId, f);
|
||||
@@ -1007,16 +1038,33 @@ void MainWindow::syncPreviewPlaybackBar() {
|
||||
}
|
||||
|
||||
void MainWindow::onInsertCombinedKey() {
|
||||
if (m_selectedEntityId.isEmpty() || !m_editorCanvas) {
|
||||
if (!m_editorCanvas || !m_workspace.isOpen()) {
|
||||
return;
|
||||
}
|
||||
const int lf = m_currentFrame % core::Project::kClipFixedFrames;
|
||||
if (m_hasSelectedCamera && !m_selectedCameraId.isEmpty()) {
|
||||
const core::eval::ResolvedProjectFrame rf =
|
||||
core::eval::evaluateAtFrame(m_workspace.project(), m_currentFrame, 10);
|
||||
for (const auto& rc : rf.cameras) {
|
||||
if (rc.camera.id == m_selectedCameraId) {
|
||||
m_workspace.setCameraLocationKey(m_selectedCameraId, lf, rc.camera.centerWorld);
|
||||
m_workspace.setCameraScaleKey(m_selectedCameraId, lf, rc.camera.viewScale);
|
||||
refreshEditorPage();
|
||||
return;
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (m_selectedEntityId.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
// 位置关键帧:使用当前帧下的动画原点
|
||||
const QPointF o = m_editorCanvas->selectedAnimatedOriginWorld();
|
||||
m_workspace.setEntityLocationKey(m_selectedEntityId, m_currentFrame % core::Project::kClipFixedFrames, o);
|
||||
m_workspace.setEntityLocationKey(m_selectedEntityId, lf, o);
|
||||
|
||||
// 缩放关键帧:使用当前帧下的 userScale(而非 depthScale01)
|
||||
const double s = m_editorCanvas->selectedUserScale();
|
||||
m_workspace.setEntityUserScaleKey(m_selectedEntityId, m_currentFrame % core::Project::kClipFixedFrames, s);
|
||||
m_workspace.setEntityUserScaleKey(m_selectedEntityId, lf, s);
|
||||
|
||||
refreshEditorPage();
|
||||
}
|
||||
@@ -1380,6 +1428,7 @@ void MainWindow::createProjectTreeDock() {
|
||||
m_bgPropertySection = new gui::BackgroundPropertySection();
|
||||
m_blackholePropertySection = new gui::BlackholePropertySection();
|
||||
m_entityPropertySection = new gui::EntityPropertySection();
|
||||
m_cameraPropertySection = new gui::CameraPropertySection();
|
||||
m_toolPropertySection = new gui::ToolPropertySection();
|
||||
m_propertyStack = new QStackedWidget();
|
||||
m_propertyStack->setContentsMargins(4, 4, 4, 4);
|
||||
@@ -1387,6 +1436,7 @@ void MainWindow::createProjectTreeDock() {
|
||||
m_propertyStack->addWidget(m_bgPropertySection);
|
||||
m_propertyStack->addWidget(m_blackholePropertySection);
|
||||
m_propertyStack->addWidget(m_entityPropertySection);
|
||||
m_propertyStack->addWidget(m_cameraPropertySection);
|
||||
m_propertyStack->addWidget(m_toolPropertySection);
|
||||
|
||||
connect(m_bgPropertySection, &gui::BackgroundPropertySection::backgroundVisibleToggled, this, [this](bool on) {
|
||||
@@ -1600,6 +1650,37 @@ void MainWindow::createProjectTreeDock() {
|
||||
refreshEditorPage();
|
||||
});
|
||||
|
||||
connect(m_cameraPropertySection, &gui::CameraPropertySection::displayNameCommitted, this, [this](const QString& text) {
|
||||
if (m_selectedCameraId.isEmpty() || !m_workspace.isOpen()) return;
|
||||
if (!m_workspace.setCameraDisplayName(m_selectedCameraId, text)) return;
|
||||
refreshProjectTree();
|
||||
refreshPropertyPanel();
|
||||
});
|
||||
connect(m_cameraPropertySection, &gui::CameraPropertySection::centerEdited, this, [this](double x, double y) {
|
||||
if (m_selectedCameraId.isEmpty() || !m_workspace.isOpen()) return;
|
||||
if (!m_workspace.setCameraCenterWorld(m_selectedCameraId, QPointF(x, y))) return;
|
||||
refreshEditorPage();
|
||||
});
|
||||
connect(m_cameraPropertySection, &gui::CameraPropertySection::viewScaleEdited, this, [this](double vs) {
|
||||
if (m_selectedCameraId.isEmpty() || !m_workspace.isOpen()) return;
|
||||
const int f = std::clamp(m_currentFrame, 0, core::Project::kClipFixedFrames - 1);
|
||||
if (!m_workspace.setCameraViewScaleValue(m_selectedCameraId, vs, f)) return;
|
||||
refreshEditorPage();
|
||||
refreshDopeSheet();
|
||||
});
|
||||
connect(m_cameraPropertySection, &gui::CameraPropertySection::activePreviewToggled, this, [this](bool on) {
|
||||
if (m_selectedCameraId.isEmpty() || !m_workspace.isOpen()) return;
|
||||
if (on) {
|
||||
if (!m_workspace.setActiveCameraId(m_selectedCameraId)) return;
|
||||
} else {
|
||||
if (m_workspace.project().activeCameraId() == m_selectedCameraId) {
|
||||
if (!m_workspace.setActiveCameraId(QString())) return;
|
||||
}
|
||||
}
|
||||
refreshEditorPage();
|
||||
refreshPropertyPanel();
|
||||
});
|
||||
|
||||
auto* propScroll = new SpinFriendlyScrollArea(m_dockProperties);
|
||||
propScroll->setWidgetResizable(true);
|
||||
propScroll->setFrameShape(QFrame::NoFrame);
|
||||
@@ -1652,7 +1733,7 @@ void MainWindow::createProjectTreeDock() {
|
||||
|
||||
void MainWindow::refreshPropertyPanel() {
|
||||
if (!m_bgPropertySection || !m_blackholePropertySection || !m_entityPropertySection ||
|
||||
!m_toolPropertySection || !m_propertyStack) {
|
||||
!m_cameraPropertySection || !m_toolPropertySection || !m_propertyStack) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1735,8 +1816,31 @@ void MainWindow::refreshPropertyPanel() {
|
||||
return cur;
|
||||
};
|
||||
|
||||
const bool cameraUi = m_hasSelectedCamera && m_workspace.isOpen() && !m_selectedCameraId.isEmpty();
|
||||
if (cameraUi) {
|
||||
m_entityPropertySection->clearDisconnected();
|
||||
m_toolPropertySection->clearDisconnected();
|
||||
gui::CameraPropertyUiState st;
|
||||
const auto rf = core::eval::evaluateAtFrame(m_workspace.project(), m_currentFrame, 10);
|
||||
for (const auto& rc : rf.cameras) {
|
||||
if (rc.camera.id != m_selectedCameraId) {
|
||||
continue;
|
||||
}
|
||||
st.displayName = rc.camera.displayName.isEmpty() ? rc.camera.id : rc.camera.displayName;
|
||||
st.centerWorld = rc.camera.centerWorld;
|
||||
st.viewScale = rc.camera.viewScale;
|
||||
st.isActivePreviewCamera = (m_workspace.project().activeCameraId() == m_selectedCameraId);
|
||||
break;
|
||||
}
|
||||
m_cameraPropertySection->applyState(st);
|
||||
m_propertyStack->setCurrentWidget(m_cameraPropertySection);
|
||||
m_dockProperties->setWindowTitle(QStringLiteral("属性 — 摄像机"));
|
||||
return;
|
||||
}
|
||||
|
||||
const bool toolUi = m_hasSelectedTool && m_workspace.isOpen() && !m_selectedToolId.isEmpty();
|
||||
if (toolUi) {
|
||||
m_cameraPropertySection->clearDisconnected();
|
||||
gui::ToolPropertyUiState st;
|
||||
const int f = std::clamp(m_currentFrame, 0, core::Project::kClipFixedFrames - 1);
|
||||
const auto* clip = activeClipForUi();
|
||||
@@ -1796,6 +1900,7 @@ void MainWindow::refreshPropertyPanel() {
|
||||
|
||||
const bool holeUi = m_workspace.isOpen() && !m_selectedBlackholeEntityId.isEmpty();
|
||||
if (holeUi) {
|
||||
m_cameraPropertySection->clearDisconnected();
|
||||
gui::BlackholePropertyUiState st;
|
||||
for (const auto& e : m_workspace.entities()) {
|
||||
if (e.id != m_selectedBlackholeEntityId) {
|
||||
@@ -1826,11 +1931,13 @@ void MainWindow::refreshPropertyPanel() {
|
||||
if (!entUi) {
|
||||
m_entityPropertySection->clearDisconnected();
|
||||
m_toolPropertySection->clearDisconnected();
|
||||
m_cameraPropertySection->clearDisconnected();
|
||||
m_propertyStack->setCurrentWidget(m_bgPropertySection);
|
||||
m_dockProperties->setWindowTitle(QStringLiteral("属性 — 背景"));
|
||||
return;
|
||||
}
|
||||
|
||||
m_cameraPropertySection->clearDisconnected();
|
||||
QString displayName;
|
||||
double userScale = 1.0;
|
||||
bool ignoreDist = false;
|
||||
@@ -2147,6 +2254,28 @@ void MainWindow::refreshProjectTree() {
|
||||
};
|
||||
|
||||
addSubtree(QString(), nullptr);
|
||||
|
||||
if (m_workspace.isOpen()) {
|
||||
for (const auto& c : m_workspace.cameras()) {
|
||||
auto* it = new QTreeWidgetItem(m_projectTree);
|
||||
it->setText(1, c.displayName.isEmpty() ? QStringLiteral("摄像机") : c.displayName);
|
||||
it->setTextAlignment(1, Qt::AlignRight | Qt::AlignVCenter);
|
||||
it->setData(0, Qt::UserRole, QStringLiteral("camera"));
|
||||
it->setData(0, Qt::UserRole + 1, c.id);
|
||||
it->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled);
|
||||
auto* eye = makeEye(!m_tempHiddenCameraIds.contains(c.id), true);
|
||||
m_projectTree->setItemWidget(it, 0, eye);
|
||||
connect(eye, &QToolButton::toggled, this, [this, id = c.id](bool on) {
|
||||
if (!m_workspace.isOpen()) return;
|
||||
if (on) m_tempHiddenCameraIds.remove(id);
|
||||
else m_tempHiddenCameraIds.insert(id);
|
||||
if (m_editorCanvas) {
|
||||
m_editorCanvas->setTempHiddenCameraIds(m_tempHiddenCameraIds);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
m_projectTree->expandAll();
|
||||
|
||||
if (m_projectTree->header()) {
|
||||
@@ -2165,6 +2294,7 @@ void MainWindow::syncProjectTreeFromCanvasSelection() {
|
||||
m_projectTree->blockSignals(true);
|
||||
if ((!m_hasSelectedEntity || m_selectedEntityId.isEmpty()) &&
|
||||
(!m_hasSelectedTool || m_selectedToolId.isEmpty()) &&
|
||||
(!m_hasSelectedCamera || m_selectedCameraId.isEmpty()) &&
|
||||
m_selectedBlackholeEntityId.isEmpty()) {
|
||||
m_projectTree->clearSelection();
|
||||
} else {
|
||||
@@ -2181,6 +2311,11 @@ void MainWindow::syncProjectTreeFromCanvasSelection() {
|
||||
found = node;
|
||||
break;
|
||||
}
|
||||
if (m_hasSelectedCamera && !m_selectedCameraId.isEmpty() && kind == QStringLiteral("camera") &&
|
||||
id == m_selectedCameraId) {
|
||||
found = node;
|
||||
break;
|
||||
}
|
||||
if (!m_selectedBlackholeEntityId.isEmpty() && kind == QStringLiteral("blackhole") &&
|
||||
id == m_selectedBlackholeEntityId) {
|
||||
found = node;
|
||||
@@ -2213,9 +2348,14 @@ void MainWindow::onProjectTreeItemClicked(QTreeWidgetItem* item, int column) {
|
||||
}
|
||||
m_hasSelectedTool = false;
|
||||
m_selectedToolId.clear();
|
||||
m_hasSelectedCamera = false;
|
||||
m_selectedCameraId.clear();
|
||||
if (m_timeline) {
|
||||
m_timeline->setToolKeyframeTracks({}, {});
|
||||
}
|
||||
if (m_editorCanvas) {
|
||||
m_editorCanvas->clearCameraSelection();
|
||||
}
|
||||
m_editorCanvas->selectEntityById(id);
|
||||
}
|
||||
} else if (kind == QStringLiteral("tool")) {
|
||||
@@ -2229,8 +2369,33 @@ void MainWindow::onProjectTreeItemClicked(QTreeWidgetItem* item, int column) {
|
||||
m_selectedToolId = id;
|
||||
m_hasSelectedEntity = false;
|
||||
m_selectedEntityId.clear();
|
||||
m_hasSelectedCamera = false;
|
||||
m_selectedCameraId.clear();
|
||||
if (m_editorCanvas) {
|
||||
m_editorCanvas->clearEntitySelection();
|
||||
m_editorCanvas->clearCameraSelection();
|
||||
}
|
||||
if (m_timeline) {
|
||||
updateTimelineTracks();
|
||||
}
|
||||
refreshPropertyPanel();
|
||||
}
|
||||
} else if (kind == QStringLiteral("camera")) {
|
||||
const QString id = item->data(0, Qt::UserRole + 1).toString();
|
||||
if (!id.isEmpty()) {
|
||||
m_selectedBlackholeEntityId.clear();
|
||||
if (m_editorCanvas) {
|
||||
m_editorCanvas->clearBlackholeSelection();
|
||||
}
|
||||
m_hasSelectedTool = false;
|
||||
m_selectedToolId.clear();
|
||||
m_hasSelectedEntity = false;
|
||||
m_selectedEntityId.clear();
|
||||
m_hasSelectedCamera = true;
|
||||
m_selectedCameraId = id;
|
||||
if (m_editorCanvas) {
|
||||
m_editorCanvas->clearEntitySelection();
|
||||
m_editorCanvas->selectCameraById(id);
|
||||
}
|
||||
if (m_timeline) {
|
||||
updateTimelineTracks();
|
||||
@@ -2246,8 +2411,11 @@ void MainWindow::onProjectTreeItemClicked(QTreeWidgetItem* item, int column) {
|
||||
m_hasSelectedEntity = false;
|
||||
m_selectedEntityId.clear();
|
||||
m_selectedEntityDisplayNameCache.clear();
|
||||
m_hasSelectedCamera = false;
|
||||
m_selectedCameraId.clear();
|
||||
if (m_editorCanvas) {
|
||||
m_editorCanvas->clearEntitySelection();
|
||||
m_editorCanvas->clearCameraSelection();
|
||||
m_editorCanvas->selectBlackholeByEntityId(entityId);
|
||||
}
|
||||
updateTimelineTracks();
|
||||
@@ -2257,8 +2425,11 @@ void MainWindow::onProjectTreeItemClicked(QTreeWidgetItem* item, int column) {
|
||||
m_selectedBlackholeEntityId.clear();
|
||||
m_hasSelectedTool = false;
|
||||
m_selectedToolId.clear();
|
||||
m_hasSelectedCamera = false;
|
||||
m_selectedCameraId.clear();
|
||||
m_editorCanvas->clearEntitySelection();
|
||||
m_editorCanvas->clearBlackholeSelection();
|
||||
m_editorCanvas->clearCameraSelection();
|
||||
updateTimelineTracks();
|
||||
}
|
||||
}
|
||||
@@ -2751,6 +2922,8 @@ void MainWindow::rebuildCentralPages() {
|
||||
m_selectedEntityOrigin = origin;
|
||||
m_hasSelectedTool = false;
|
||||
m_selectedToolId.clear();
|
||||
m_hasSelectedCamera = false;
|
||||
m_selectedCameraId.clear();
|
||||
if (hasSel && !id.isEmpty()) {
|
||||
for (const auto& e : m_workspace.entities()) {
|
||||
if (e.id == id) {
|
||||
@@ -2776,6 +2949,8 @@ void MainWindow::rebuildCentralPages() {
|
||||
m_hasSelectedEntity = false;
|
||||
m_selectedEntityId.clear();
|
||||
m_selectedEntityDisplayNameCache.clear();
|
||||
m_hasSelectedCamera = false;
|
||||
m_selectedCameraId.clear();
|
||||
}
|
||||
updateTimelineTracks();
|
||||
if (!m_timelineScrubbing) {
|
||||
@@ -3176,6 +3351,50 @@ void MainWindow::rebuildCentralPages() {
|
||||
refreshProjectTree();
|
||||
updateUiEnabledState();
|
||||
});
|
||||
connect(m_editorCanvas, &EditorCanvas::requestMoveCamera, this, [this](const QString& id, const QPointF& delta) {
|
||||
const bool autoKey = true;
|
||||
if (!m_workspace.moveCameraBy(id, delta, m_currentFrame % core::Project::kClipFixedFrames, autoKey)) {
|
||||
return;
|
||||
}
|
||||
refreshEditorPage();
|
||||
refreshProjectTree();
|
||||
updateUiEnabledState();
|
||||
});
|
||||
connect(m_editorCanvas, &EditorCanvas::requestCameraViewScaleAdjust, this, [this](const QString& id, double factor) {
|
||||
if (id.isEmpty() || !m_workspace.isOpen()) return;
|
||||
const core::eval::ResolvedProjectFrame resFrame =
|
||||
core::eval::evaluateAtFrame(m_workspace.project(), m_currentFrame, 10);
|
||||
for (const auto& rc : resFrame.cameras) {
|
||||
if (rc.camera.id != id) continue;
|
||||
const double ns = std::clamp(rc.camera.viewScale * factor, 1e-6, 1e3);
|
||||
const int kf = std::clamp(m_currentFrame, 0, core::Project::kClipFixedFrames - 1);
|
||||
if (!m_workspace.setCameraViewScaleValue(id, ns, kf)) return;
|
||||
refreshEditorPage();
|
||||
refreshDopeSheet();
|
||||
return;
|
||||
}
|
||||
});
|
||||
connect(m_editorCanvas, &EditorCanvas::selectedCameraChanged, this,
|
||||
[this](bool hasSel, const QString& id, const QPointF& centerWorld, double viewScale) {
|
||||
Q_UNUSED(centerWorld);
|
||||
Q_UNUSED(viewScale);
|
||||
m_hasSelectedCamera = hasSel;
|
||||
m_selectedCameraId = id;
|
||||
if (hasSel) {
|
||||
m_hasSelectedEntity = false;
|
||||
m_selectedEntityId.clear();
|
||||
m_selectedEntityDisplayNameCache.clear();
|
||||
m_hasSelectedTool = false;
|
||||
m_selectedToolId.clear();
|
||||
m_selectedBlackholeEntityId.clear();
|
||||
}
|
||||
updateTimelineTracks();
|
||||
if (!m_timelineScrubbing) {
|
||||
updateStatusBarText();
|
||||
refreshPropertyPanel();
|
||||
syncProjectTreeFromCanvasSelection();
|
||||
}
|
||||
});
|
||||
connect(m_editorCanvas, &EditorCanvas::requestResolveBlackholeCopy, this,
|
||||
[this](const QString& entityId, const QPoint& sourceOffsetPx) {
|
||||
if (!m_workspace.resolveBlackholeByCopyBackground(entityId, sourceOffsetPx, true)) {
|
||||
@@ -3406,12 +3625,32 @@ void MainWindow::refreshEditorPage() {
|
||||
opacities.push_back(rt.opacity);
|
||||
}
|
||||
m_editorCanvas->setTools(tools, opacities);
|
||||
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->setCurrentFrame(m_currentFrame);
|
||||
m_editorCanvas->setTempHiddenIds(m_tempHiddenEntityIds, m_tempHiddenToolIds);
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
updateTimelineTracks();
|
||||
|
||||
} else {
|
||||
m_editorCanvas->setEntities({}, {}, QString());
|
||||
m_editorCanvas->setCameraOverlays({}, QString(), {});
|
||||
if (m_timeline) {
|
||||
m_timeline->setKeyframeTracks({}, {}, {}, {});
|
||||
m_timeline->setToolKeyframeTracks({}, {});
|
||||
@@ -3438,11 +3677,13 @@ void MainWindow::updateTimelineTracks() {
|
||||
|
||||
const bool wantEntity = !m_selectedEntityId.isEmpty();
|
||||
const bool wantTool = (m_hasSelectedTool && !m_selectedToolId.isEmpty());
|
||||
const bool wantCamera = (m_hasSelectedCamera && !m_selectedCameraId.isEmpty());
|
||||
|
||||
// 未选中时不显示关键帧(按需求)
|
||||
if (!wantEntity) m_timeline->setKeyframeTracks({}, {}, {}, {});
|
||||
if (!wantTool) m_timeline->setToolKeyframeTracks({}, {});
|
||||
if (!wantEntity && !wantTool) return;
|
||||
if (!wantEntity && !wantTool && !wantCamera) {
|
||||
m_timeline->setKeyframeTracks({}, {}, {}, {});
|
||||
m_timeline->setToolKeyframeTracks({}, {});
|
||||
return;
|
||||
}
|
||||
|
||||
// 选择当前 clip(与 workspace 写入规则一致)
|
||||
const core::Project::AnimationClip* clip = nullptr;
|
||||
@@ -3513,7 +3754,16 @@ void MainWindow::updateTimelineTracks() {
|
||||
const auto im = clip->entityImageFrames.value(m_selectedEntityId);
|
||||
const auto vis = clip->entityVisibilityKeys.value(m_selectedEntityId);
|
||||
m_timeline->setKeyframeTracks(framesOfVec2(loc), framesOfDouble(sc), framesOfImage(im), framesOfBool(vis));
|
||||
} else if (wantCamera) {
|
||||
const auto loc = clip->cameraLocationKeys.value(m_selectedCameraId);
|
||||
const auto sc = clip->cameraScaleKeys.value(m_selectedCameraId);
|
||||
m_timeline->setKeyframeTracks(framesOfVec2(loc), framesOfDouble(sc), {}, {});
|
||||
} else {
|
||||
m_timeline->setKeyframeTracks({}, {}, {}, {});
|
||||
}
|
||||
|
||||
// 注意:未选中工具时不能调用 setToolKeyframeTracks({}, {}),其实现会清空 m_locFrames/m_scaleFrames,
|
||||
// 从而冲掉上面已为实体/摄像机写入的轨道数据。
|
||||
if (wantTool) {
|
||||
const auto loc = clip->toolLocationKeys.value(m_selectedToolId);
|
||||
const auto vis = clip->toolVisibilityKeys.value(m_selectedToolId);
|
||||
@@ -4082,6 +4332,9 @@ void MainWindow::onCloseProject() {
|
||||
m_selectedEntityDepth = 0;
|
||||
m_selectedEntityOrigin = QPointF();
|
||||
m_selectedEntityId.clear();
|
||||
m_hasSelectedCamera = false;
|
||||
m_selectedCameraId.clear();
|
||||
m_tempHiddenCameraIds.clear();
|
||||
m_currentFrame = 0;
|
||||
|
||||
statusBar()->showMessage(QStringLiteral("工程已关闭"));
|
||||
|
||||
@@ -37,6 +37,7 @@ class BackgroundPropertySection;
|
||||
class BlackholePropertySection;
|
||||
class EntityPropertySection;
|
||||
class ToolPropertySection;
|
||||
class CameraPropertySection;
|
||||
class EntityIntroPopup;
|
||||
class ResourceLibraryDock;
|
||||
}
|
||||
@@ -130,6 +131,7 @@ private:
|
||||
gui::BlackholePropertySection* m_blackholePropertySection = nullptr;
|
||||
gui::EntityPropertySection* m_entityPropertySection = nullptr;
|
||||
gui::ToolPropertySection* m_toolPropertySection = nullptr;
|
||||
gui::CameraPropertySection* m_cameraPropertySection = nullptr;
|
||||
QToolButton* m_btnCreateEntity = nullptr;
|
||||
ToolOptionPopup* m_createEntityPopup = nullptr;
|
||||
QToolButton* m_btnToggleDepthOverlay = nullptr;
|
||||
@@ -170,11 +172,13 @@ private:
|
||||
int m_lastWorldZ = -1;
|
||||
bool m_hasSelectedEntity = false;
|
||||
bool m_hasSelectedTool = false;
|
||||
bool m_hasSelectedCamera = false;
|
||||
bool m_syncingTreeSelection = false;
|
||||
int m_selectedEntityDepth = 0;
|
||||
QPointF m_selectedEntityOrigin;
|
||||
QString m_selectedEntityId;
|
||||
QString m_selectedToolId;
|
||||
QString m_selectedCameraId;
|
||||
QString m_selectedBlackholeEntityId;
|
||||
QString m_selectedEntityDisplayNameCache;
|
||||
QString m_bgAbsCache;
|
||||
@@ -182,6 +186,7 @@ private:
|
||||
// 项目树“眼睛”:仅用于画布临时隐藏(不持久化、不进时间轴)
|
||||
QSet<QString> m_tempHiddenEntityIds;
|
||||
QSet<QString> m_tempHiddenToolIds;
|
||||
QSet<QString> m_tempHiddenCameraIds;
|
||||
void updateStatusBarText();
|
||||
void syncCreateEntityToolButtonTooltip();
|
||||
void refreshPropertyPanel();
|
||||
|
||||
111
client/gui/props/CameraPropertySection.cpp
Normal file
111
client/gui/props/CameraPropertySection.cpp
Normal file
@@ -0,0 +1,111 @@
|
||||
#include "props/CameraPropertySection.h"
|
||||
|
||||
#include "params/ParamControls.h"
|
||||
|
||||
#include <QCheckBox>
|
||||
#include <QDoubleSpinBox>
|
||||
#include <QFormLayout>
|
||||
#include <QLabel>
|
||||
#include <QLineEdit>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
namespace gui {
|
||||
|
||||
CameraPropertySection::CameraPropertySection(QWidget* parent) : PropertySectionWidget(parent) {
|
||||
auto* lay = new QVBoxLayout(this);
|
||||
lay->setContentsMargins(0, 0, 0, 0);
|
||||
lay->setSpacing(6);
|
||||
|
||||
auto* form = new QFormLayout();
|
||||
form->setContentsMargins(0, 0, 0, 0);
|
||||
form->setSpacing(6);
|
||||
|
||||
m_name = new QLineEdit(this);
|
||||
m_name->setPlaceholderText(QStringLiteral("显示名称…"));
|
||||
form->addRow(QStringLiteral("名称"), m_name);
|
||||
|
||||
m_center = new Vec2ParamControl(this);
|
||||
m_center->setToolTip(QStringLiteral("摄像机中心(世界坐标),与画布上黄色圆点一致"));
|
||||
form->addRow(QStringLiteral("中心"), m_center);
|
||||
|
||||
m_viewScale = new QDoubleSpinBox(this);
|
||||
m_viewScale->setRange(1e-4, 1000.0);
|
||||
m_viewScale->setDecimals(5);
|
||||
m_viewScale->setSingleStep(0.01);
|
||||
m_viewScale->setToolTip(QStringLiteral(
|
||||
"视口缩放:在参考分辨率 1600×900 下的像素/世界单位比(与预览、画布上镜头框一致);"
|
||||
"不随当前窗口大小改变镜头覆盖的世界范围。数值越小,可见的世界范围越大。"));
|
||||
form->addRow(QStringLiteral("缩放"), m_viewScale);
|
||||
|
||||
m_activePreview = new QCheckBox(QStringLiteral("用作预览展示镜头"), this);
|
||||
m_activePreview->setToolTip(QStringLiteral("进入预览展示时,按该摄像机在当前帧的位置与缩放呈现画面"));
|
||||
form->addRow(QStringLiteral("预览"), m_activePreview);
|
||||
|
||||
lay->addLayout(form);
|
||||
lay->addStretch(1);
|
||||
|
||||
connect(m_name, &QLineEdit::editingFinished, this, [this]() {
|
||||
if (m_name) emit displayNameCommitted(m_name->text());
|
||||
});
|
||||
connect(m_center, &Vec2ParamControl::valueChanged, this, [this](double x, double y) { emit centerEdited(x, y); });
|
||||
connect(m_viewScale, qOverload<double>(&QDoubleSpinBox::valueChanged), this,
|
||||
&CameraPropertySection::viewScaleEdited);
|
||||
connect(m_activePreview, &QCheckBox::toggled, this, &CameraPropertySection::activePreviewToggled);
|
||||
}
|
||||
|
||||
void CameraPropertySection::setEditingEnabled(bool on) {
|
||||
for (auto* w : {static_cast<QWidget*>(m_name), static_cast<QWidget*>(m_center), static_cast<QWidget*>(m_viewScale),
|
||||
static_cast<QWidget*>(m_activePreview)}) {
|
||||
if (w) w->setEnabled(on);
|
||||
}
|
||||
}
|
||||
|
||||
void CameraPropertySection::clearDisconnected() {
|
||||
setEditingEnabled(false);
|
||||
if (m_name) {
|
||||
m_name->blockSignals(true);
|
||||
m_name->clear();
|
||||
m_name->blockSignals(false);
|
||||
}
|
||||
if (m_center) {
|
||||
m_center->blockSignals(true);
|
||||
m_center->setValue(0.0, 0.0);
|
||||
m_center->blockSignals(false);
|
||||
}
|
||||
if (m_viewScale) {
|
||||
m_viewScale->blockSignals(true);
|
||||
m_viewScale->setValue(1.0);
|
||||
m_viewScale->blockSignals(false);
|
||||
}
|
||||
if (m_activePreview) {
|
||||
m_activePreview->blockSignals(true);
|
||||
m_activePreview->setChecked(false);
|
||||
m_activePreview->blockSignals(false);
|
||||
}
|
||||
}
|
||||
|
||||
void CameraPropertySection::applyState(const CameraPropertyUiState& s) {
|
||||
setEditingEnabled(true);
|
||||
if (m_name) {
|
||||
m_name->blockSignals(true);
|
||||
m_name->setText(s.displayName);
|
||||
m_name->blockSignals(false);
|
||||
}
|
||||
if (m_center) {
|
||||
m_center->blockSignals(true);
|
||||
m_center->setValue(s.centerWorld.x(), s.centerWorld.y());
|
||||
m_center->blockSignals(false);
|
||||
}
|
||||
if (m_viewScale) {
|
||||
m_viewScale->blockSignals(true);
|
||||
m_viewScale->setValue(s.viewScale);
|
||||
m_viewScale->blockSignals(false);
|
||||
}
|
||||
if (m_activePreview) {
|
||||
m_activePreview->blockSignals(true);
|
||||
m_activePreview->setChecked(s.isActivePreviewCamera);
|
||||
m_activePreview->blockSignals(false);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace gui
|
||||
47
client/gui/props/CameraPropertySection.h
Normal file
47
client/gui/props/CameraPropertySection.h
Normal file
@@ -0,0 +1,47 @@
|
||||
#pragma once
|
||||
|
||||
#include "props/PropertySectionWidget.h"
|
||||
|
||||
#include <QPointF>
|
||||
#include <QString>
|
||||
|
||||
class QCheckBox;
|
||||
class QDoubleSpinBox;
|
||||
class QLineEdit;
|
||||
|
||||
namespace gui {
|
||||
class Vec2ParamControl;
|
||||
}
|
||||
|
||||
namespace gui {
|
||||
|
||||
struct CameraPropertyUiState {
|
||||
QString displayName;
|
||||
QPointF centerWorld;
|
||||
double viewScale = 1.0;
|
||||
bool isActivePreviewCamera = false;
|
||||
};
|
||||
|
||||
class CameraPropertySection final : public PropertySectionWidget {
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit CameraPropertySection(QWidget* parent = nullptr);
|
||||
|
||||
void clearDisconnected();
|
||||
void applyState(const CameraPropertyUiState& s);
|
||||
void setEditingEnabled(bool on);
|
||||
|
||||
signals:
|
||||
void displayNameCommitted(const QString& text);
|
||||
void centerEdited(double x, double y);
|
||||
void viewScaleEdited(double viewScale);
|
||||
void activePreviewToggled(bool on);
|
||||
|
||||
private:
|
||||
QLineEdit* m_name = nullptr;
|
||||
Vec2ParamControl* m_center = nullptr;
|
||||
QDoubleSpinBox* m_viewScale = nullptr;
|
||||
QCheckBox* m_activePreview = nullptr;
|
||||
};
|
||||
|
||||
} // namespace gui
|
||||
Reference in New Issue
Block a user