新增根据帧数控制可见性
This commit is contained in:
@@ -4,6 +4,7 @@ set(GUI_ROOT ${CMAKE_CURRENT_SOURCE_DIR})
|
||||
set(GUI_SOURCES
|
||||
${GUI_ROOT}/app/main.cpp
|
||||
${GUI_ROOT}/main_window/MainWindow.cpp
|
||||
${GUI_ROOT}/library/ResourceLibraryDock.cpp
|
||||
${GUI_ROOT}/widgets/LongPressSwitchToolButton.cpp
|
||||
${GUI_ROOT}/widgets/ToolOptionPopup.cpp
|
||||
${GUI_ROOT}/main_window/RecentProjectHistory.cpp
|
||||
@@ -18,11 +19,13 @@ set(GUI_SOURCES
|
||||
${GUI_ROOT}/params/ParamControls.cpp
|
||||
${GUI_ROOT}/props/BackgroundPropertySection.cpp
|
||||
${GUI_ROOT}/props/EntityPropertySection.cpp
|
||||
${GUI_ROOT}/props/ToolPropertySection.cpp
|
||||
${GUI_ROOT}/timeline/TimelineWidget.cpp
|
||||
)
|
||||
|
||||
set(GUI_HEADERS
|
||||
${GUI_ROOT}/main_window/MainWindow.h
|
||||
${GUI_ROOT}/library/ResourceLibraryDock.h
|
||||
${GUI_ROOT}/widgets/LongPressSwitchToolButton.h
|
||||
${GUI_ROOT}/widgets/ToolOptionPopup.h
|
||||
${GUI_ROOT}/main_window/RecentProjectHistory.h
|
||||
@@ -37,6 +40,7 @@ set(GUI_HEADERS
|
||||
${GUI_ROOT}/params/ParamControls.h
|
||||
${GUI_ROOT}/props/BackgroundPropertySection.h
|
||||
${GUI_ROOT}/props/EntityPropertySection.h
|
||||
${GUI_ROOT}/props/ToolPropertySection.h
|
||||
${GUI_ROOT}/props/PropertySectionWidget.h
|
||||
${GUI_ROOT}/timeline/TimelineWidget.h
|
||||
)
|
||||
|
||||
@@ -17,10 +17,19 @@
|
||||
#include <QWheelEvent>
|
||||
#include <QKeyEvent>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QMimeData>
|
||||
#include <QDragEnterEvent>
|
||||
#include <QDropEvent>
|
||||
#include <QTextOption>
|
||||
#include <QIODevice>
|
||||
#include <QPen>
|
||||
#include <QPolygonF>
|
||||
|
||||
#include "core/library/EntityJson.h"
|
||||
#include "core/library/ToolJson.h"
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr int kSamCropMargin = 32;
|
||||
@@ -275,6 +284,37 @@ GizmoHit hitTestGizmo(const QPointF& mouseView, const QPointF& originView) {
|
||||
return {};
|
||||
}
|
||||
|
||||
struct BubbleLayoutWorld {
|
||||
QPainterPath path;
|
||||
QRectF bodyRect;
|
||||
};
|
||||
|
||||
// originWorld = 朝下三角形尖端;滑块改变主体水平位置,使「平直底边」上 t01 对应点始终在尖端正上方(三角竖直、与主体一体平移)
|
||||
static BubbleLayoutWorld bubbleLayoutWorld(const core::Project::Tool& tool) {
|
||||
const QPointF tip = tool.originWorld;
|
||||
const qreal w = 220.0;
|
||||
const qreal h = 110.0;
|
||||
const qreal rx = 16.0;
|
||||
const qreal arrowH = 22.0;
|
||||
const double t01 = std::clamp(tool.bubblePointerT01, 0.0, 1.0);
|
||||
const qreal spanFlat = std::max(w - 2.0 * rx, 1.0);
|
||||
const qreal bodyLeft = tip.x() - rx - static_cast<qreal>(t01) * spanFlat;
|
||||
const QRectF body(bodyLeft, tip.y() - (h + arrowH), w, h);
|
||||
const qreal halfTri = 14.0;
|
||||
const qreal baseCx = tip.x();
|
||||
|
||||
QPainterPath path;
|
||||
path.addRoundedRect(body, rx, rx);
|
||||
QPolygonF tri;
|
||||
tri << QPointF(baseCx - halfTri, body.bottom()) << QPointF(baseCx + halfTri, body.bottom()) << QPointF(tip.x(), tip.y());
|
||||
path.addPolygon(tri);
|
||||
return BubbleLayoutWorld{path, body};
|
||||
}
|
||||
|
||||
static QPainterPath bubblePathWorld(const core::Project::Tool& tool) {
|
||||
return bubbleLayoutWorld(tool).path;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
EditorCanvas::EditorCanvas(QWidget* parent)
|
||||
@@ -283,6 +323,7 @@ EditorCanvas::EditorCanvas(QWidget* parent)
|
||||
setMinimumSize(480, 320);
|
||||
setFocusPolicy(Qt::StrongFocus);
|
||||
setMouseTracking(true);
|
||||
setAcceptDrops(true);
|
||||
|
||||
m_previewEmitTimer.start();
|
||||
|
||||
@@ -297,6 +338,129 @@ EditorCanvas::EditorCanvas(QWidget* parent)
|
||||
updateCursor();
|
||||
}
|
||||
|
||||
void EditorCanvas::dragEnterEvent(QDragEnterEvent* e) {
|
||||
if (!e || !e->mimeData()) {
|
||||
return;
|
||||
}
|
||||
if (e->mimeData()->hasFormat(QStringLiteral("application/x-hfut-resource+json"))) {
|
||||
e->acceptProposedAction();
|
||||
return;
|
||||
}
|
||||
QWidget::dragEnterEvent(e);
|
||||
}
|
||||
|
||||
void EditorCanvas::dragMoveEvent(QDragMoveEvent* e) {
|
||||
if (!e || !e->mimeData()) {
|
||||
return;
|
||||
}
|
||||
if (e->mimeData()->hasFormat(QStringLiteral("application/x-hfut-resource+json"))) {
|
||||
e->acceptProposedAction();
|
||||
return;
|
||||
}
|
||||
QWidget::dragMoveEvent(e);
|
||||
}
|
||||
|
||||
void EditorCanvas::dropEvent(QDropEvent* e) {
|
||||
if (!e || !e->mimeData()) {
|
||||
QWidget::dropEvent(e);
|
||||
return;
|
||||
}
|
||||
if (!e->mimeData()->hasFormat(QStringLiteral("application/x-hfut-resource+json"))) {
|
||||
QWidget::dropEvent(e);
|
||||
return;
|
||||
}
|
||||
const QByteArray bytes = e->mimeData()->data(QStringLiteral("application/x-hfut-resource+json"));
|
||||
const auto doc = QJsonDocument::fromJson(bytes);
|
||||
if (!doc.isObject()) {
|
||||
e->ignore();
|
||||
return;
|
||||
}
|
||||
const QJsonObject root = doc.object();
|
||||
const QString kind = root.value(QStringLiteral("kind")).toString(QStringLiteral("entity"));
|
||||
|
||||
const QPointF dropWorld = viewToWorld(e->position());
|
||||
if (kind == QStringLiteral("tool")) {
|
||||
if (!root.value(QStringLiteral("tool")).isObject()) {
|
||||
e->ignore();
|
||||
return;
|
||||
}
|
||||
core::Project::Tool t;
|
||||
if (!core::library::toolFromJson(root.value(QStringLiteral("tool")).toObject(), t)) {
|
||||
e->ignore();
|
||||
return;
|
||||
}
|
||||
// 让主窗口分配 id,避免冲突
|
||||
t.id.clear();
|
||||
t.parentId.clear();
|
||||
t.parentOffsetWorld = QPointF();
|
||||
t.originWorld = dropWorld;
|
||||
emit requestAddTool(t);
|
||||
e->acceptProposedAction();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!root.value(QStringLiteral("entity")).isObject()) {
|
||||
e->ignore();
|
||||
return;
|
||||
}
|
||||
core::Project::Entity ent;
|
||||
if (!core::library::entityFromJson(root.value(QStringLiteral("entity")).toObject(), ent)) {
|
||||
e->ignore();
|
||||
return;
|
||||
}
|
||||
|
||||
// 让主窗口分配 id,避免资源 id 与工程内冲突
|
||||
ent.id.clear();
|
||||
ent.imagePath.clear();
|
||||
ent.entityPayloadPath.clear();
|
||||
ent.legacyAnimSidecarPath.clear();
|
||||
|
||||
ent.originWorld = dropWorld;
|
||||
|
||||
// 默认把贴图左上角放到 originWorld + offset
|
||||
QPointF imageOffset(-128, -128);
|
||||
if (root.value(QStringLiteral("imageOffsetFromOrigin")).isArray()) {
|
||||
const QJsonArray a = root.value(QStringLiteral("imageOffsetFromOrigin")).toArray();
|
||||
if (a.size() >= 2) {
|
||||
imageOffset = QPointF(a.at(0).toDouble(), a.at(1).toDouble());
|
||||
}
|
||||
}
|
||||
ent.imageTopLeftWorld = ent.originWorld + imageOffset;
|
||||
|
||||
// 生成占位贴图(未来可替换为真实资源图片)
|
||||
QSize imgSize(256, 256);
|
||||
if (root.value(QStringLiteral("imageSize")).isArray()) {
|
||||
const QJsonArray a = root.value(QStringLiteral("imageSize")).toArray();
|
||||
if (a.size() >= 2) {
|
||||
imgSize = QSize(a.at(0).toInt(256), a.at(1).toInt(256));
|
||||
}
|
||||
}
|
||||
QColor accent(80, 160, 255);
|
||||
if (root.value(QStringLiteral("accent")).isArray()) {
|
||||
const QJsonArray a = root.value(QStringLiteral("accent")).toArray();
|
||||
if (a.size() >= 4) {
|
||||
accent = QColor(a.at(0).toInt(80), a.at(1).toInt(160), a.at(2).toInt(255), a.at(3).toInt(255));
|
||||
}
|
||||
}
|
||||
if (!imgSize.isValid()) {
|
||||
imgSize = QSize(256, 256);
|
||||
}
|
||||
QImage img(imgSize, QImage::Format_ARGB32_Premultiplied);
|
||||
img.fill(Qt::transparent);
|
||||
{
|
||||
QPainter p(&img);
|
||||
p.setRenderHint(QPainter::Antialiasing, true);
|
||||
QRectF rr(QPointF(0, 0), QSizeF(imgSize));
|
||||
rr = rr.adjusted(6, 6, -6, -6);
|
||||
p.setPen(QPen(QColor(0, 0, 0, 60), 2));
|
||||
p.setBrush(QBrush(accent));
|
||||
p.drawRoundedRect(rr, 18, 18);
|
||||
}
|
||||
|
||||
emit requestAddEntity(ent, img);
|
||||
e->acceptProposedAction();
|
||||
}
|
||||
|
||||
void EditorCanvas::setPresentationPreviewMode(bool on) {
|
||||
if (m_presentationPreviewMode == on) {
|
||||
return;
|
||||
@@ -328,7 +492,9 @@ void EditorCanvas::setPresentationPreviewMode(bool on) {
|
||||
update();
|
||||
}
|
||||
|
||||
void EditorCanvas::setEntities(const QVector<core::Project::Entity>& entities, const QString& projectDirAbs) {
|
||||
void EditorCanvas::setEntities(const QVector<core::Project::Entity>& entities,
|
||||
const QVector<double>& opacities01,
|
||||
const QString& projectDirAbs) {
|
||||
const QString prevSelectedId =
|
||||
(m_selectedEntity >= 0 && m_selectedEntity < m_entities.size()) ? m_entities[m_selectedEntity].id : QString();
|
||||
|
||||
@@ -344,16 +510,19 @@ void EditorCanvas::setEntities(const QVector<core::Project::Entity>& entities, c
|
||||
}
|
||||
}
|
||||
|
||||
for (const auto& e : entities) {
|
||||
const qsizetype nEnt = entities.size();
|
||||
for (qsizetype iEnt = 0; iEnt < nEnt; ++iEnt) {
|
||||
const auto& e = entities[iEnt];
|
||||
Entity v;
|
||||
v.id = e.id;
|
||||
v.hiddenInEditMode = !m_presentationPreviewMode && !e.visible;
|
||||
using core::KeyInterpolation;
|
||||
const QPointF originWorld =
|
||||
core::sampleLocation(e.locationKeys, m_currentFrame, e.originWorld, KeyInterpolation::Linear);
|
||||
v.opacity = (iEnt < opacities01.size()) ? std::clamp(opacities01[iEnt], 0.0, 1.0) : 1.0;
|
||||
// 注意:MainWindow 传入的是“按当前帧求值后的实体”(包含父子跟随与曲线采样)。
|
||||
// 这里必须直接使用 e.originWorld,不能再对 locationKeys 做二次采样,否则父子实体会在刷新时复位/跳变。
|
||||
const QPointF originWorld = e.originWorld;
|
||||
v.animatedOriginWorld = originWorld;
|
||||
v.cutoutPolygonWorld = e.cutoutPolygonWorld;
|
||||
v.distanceScaleCalibMult = e.distanceScaleCalibMult;
|
||||
v.ignoreDistanceScale = e.ignoreDistanceScale;
|
||||
|
||||
// 逐帧自动算 z:使用实体多边形质心作为锚点采样深度(O(1)),避免卡顿
|
||||
QVector<QPointF> polyTmp;
|
||||
@@ -370,7 +539,7 @@ void EditorCanvas::setEntities(const QVector<core::Project::Entity>& entities, c
|
||||
const double userScaleAnimated =
|
||||
core::sampleUserScale(e.userScaleKeys, m_currentFrame, e.userScale, core::KeyInterpolation::Linear);
|
||||
v.userScale = std::max(1e-6, userScaleAnimated);
|
||||
const double distScale = distanceScaleFromDepth01(ds01, e.distanceScaleCalibMult);
|
||||
const double distScale = e.ignoreDistanceScale ? 1.0 : distanceScaleFromDepth01(ds01, e.distanceScaleCalibMult);
|
||||
const double scale = distScale * v.userScale;
|
||||
v.visualScale = scale;
|
||||
|
||||
@@ -432,6 +601,41 @@ void EditorCanvas::setEntities(const QVector<core::Project::Entity>& entities, c
|
||||
update();
|
||||
}
|
||||
|
||||
void EditorCanvas::setTools(const QVector<core::Project::Tool>& tools, const QVector<double>& opacities01) {
|
||||
m_tools.clear();
|
||||
const qsizetype n = tools.size();
|
||||
m_tools.reserve(n);
|
||||
for (qsizetype i = 0; i < n; ++i) {
|
||||
ToolView tv;
|
||||
tv.tool = tools[i];
|
||||
tv.opacity = (i < opacities01.size()) ? std::clamp(opacities01[i], 0.0, 1.0) : 1.0;
|
||||
m_tools.push_back(tv);
|
||||
}
|
||||
// 轨道变更:若当前选中的工具已不存在,则清除
|
||||
if (m_selectedTool >= 0) {
|
||||
const QString selId = (m_selectedTool >= 0 && m_selectedTool < m_tools.size()) ? m_tools[m_selectedTool].tool.id : QString();
|
||||
if (!selId.isEmpty()) {
|
||||
int hit = -1;
|
||||
for (int i = 0; i < m_tools.size(); ++i) {
|
||||
if (m_tools[i].tool.id == selId) {
|
||||
hit = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
m_selectedTool = hit;
|
||||
} else {
|
||||
m_selectedTool = -1;
|
||||
}
|
||||
}
|
||||
update();
|
||||
}
|
||||
|
||||
void EditorCanvas::setTempHiddenIds(const QSet<QString>& entityIds, const QSet<QString>& toolIds) {
|
||||
m_tempHiddenEntityIds = entityIds;
|
||||
m_tempHiddenToolIds = toolIds;
|
||||
update();
|
||||
}
|
||||
|
||||
void EditorCanvas::setCurrentFrame(int frame) {
|
||||
if (m_currentFrame == frame) {
|
||||
return;
|
||||
@@ -475,7 +679,7 @@ double EditorCanvas::selectedDistanceScaleMultiplier() const {
|
||||
return 1.0;
|
||||
}
|
||||
const auto& ent = m_entities[m_selectedEntity];
|
||||
return distanceScaleFromDepth01(ent.animatedDepthScale01, ent.distanceScaleCalibMult);
|
||||
return ent.ignoreDistanceScale ? 1.0 : distanceScaleFromDepth01(ent.animatedDepthScale01, ent.distanceScaleCalibMult);
|
||||
}
|
||||
|
||||
double EditorCanvas::selectedUserScale() const {
|
||||
@@ -844,7 +1048,10 @@ int EditorCanvas::hitTestEntity(const QPointF& worldPos) const {
|
||||
for (qsizetype i = m_entities.size(); i > 0; --i) {
|
||||
const qsizetype idx = i - 1;
|
||||
const auto& ent = m_entities[idx];
|
||||
if (ent.hiddenInEditMode) {
|
||||
if (ent.opacity <= 0.001) {
|
||||
continue;
|
||||
}
|
||||
if (!ent.id.isEmpty() && m_tempHiddenEntityIds.contains(ent.id)) {
|
||||
continue;
|
||||
}
|
||||
if (!ent.polygonWorld.isEmpty()) {
|
||||
@@ -908,7 +1115,7 @@ void EditorCanvas::paintEvent(QPaintEvent* e) {
|
||||
m_bgCutoutDirty = false;
|
||||
m_bgImageCutout = m_bgImage;
|
||||
for (const auto& ent : m_entities) {
|
||||
if (!ent.cutoutPolygonWorld.isEmpty() && !m_bgImageCutout.isNull()) {
|
||||
if (ent.opacity > 0.001 && !ent.cutoutPolygonWorld.isEmpty() && !m_bgImageCutout.isNull()) {
|
||||
entity_cutout::applyBlackFillToBackground(m_bgImageCutout, ent.cutoutPolygonWorld);
|
||||
}
|
||||
}
|
||||
@@ -969,7 +1176,10 @@ void EditorCanvas::paintEvent(QPaintEvent* e) {
|
||||
// 实体元素(占位):后续可替换为真实数据
|
||||
for (int i = 0; i < m_entities.size(); ++i) {
|
||||
const auto& ent = m_entities[i];
|
||||
if (ent.hiddenInEditMode && i != m_selectedEntity) {
|
||||
if (ent.opacity <= 0.001) {
|
||||
continue;
|
||||
}
|
||||
if (!ent.id.isEmpty() && m_tempHiddenEntityIds.contains(ent.id)) {
|
||||
continue;
|
||||
}
|
||||
const bool isDragPreview = (!m_presentationPreviewMode && m_draggingEntity && m_dragPreviewActive && i == m_selectedEntity);
|
||||
@@ -1070,7 +1280,7 @@ void EditorCanvas::paintEvent(QPaintEvent* e) {
|
||||
p.drawRect(ent.rect.adjusted(-2, -2, 2, 2));
|
||||
}
|
||||
}
|
||||
if (m_presentationPreviewMode && !ent.hiddenInEditMode) {
|
||||
if (m_presentationPreviewMode && ent.opacity > 0.001) {
|
||||
const bool showHover = (i == m_presHoverEntityIndex);
|
||||
const bool showFocus = (i == m_presFocusedEntityIndex);
|
||||
if (showHover || showFocus) {
|
||||
@@ -1099,6 +1309,54 @@ void EditorCanvas::paintEvent(QPaintEvent* e) {
|
||||
}
|
||||
}
|
||||
|
||||
// 工具:对话气泡(world 坐标),按 opacity 淡入淡出
|
||||
for (int i = 0; i < m_tools.size(); ++i) {
|
||||
const auto& tv = m_tools[i];
|
||||
const auto& tool = tv.tool;
|
||||
const double opacity = std::clamp(tv.opacity, 0.0, 1.0);
|
||||
// tool.visible 仅表示“基础可见性”,动画可见性由 opacity(关键帧+淡入淡出)驱动
|
||||
if (opacity <= 0.001) {
|
||||
continue;
|
||||
}
|
||||
if (!tool.id.isEmpty() && m_tempHiddenToolIds.contains(tool.id)) {
|
||||
continue;
|
||||
}
|
||||
if (tool.type != core::Project::Tool::Type::Bubble) {
|
||||
continue;
|
||||
}
|
||||
const BubbleLayoutWorld lay = bubbleLayoutWorld(tool);
|
||||
const QPainterPath& path = lay.path;
|
||||
const QRectF& body = lay.bodyRect;
|
||||
|
||||
QColor fill(255, 255, 255, int(220 * opacity));
|
||||
QColor border(0, 0, 0, int(120 * opacity));
|
||||
p.setBrush(fill);
|
||||
p.setPen(QPen(border, 1.2 / std::max<qreal>(m_scale, 0.001)));
|
||||
p.drawPath(path);
|
||||
|
||||
// 文本
|
||||
if (!tool.text.trimmed().isEmpty()) {
|
||||
p.setPen(QColor(10, 10, 10, int(230 * opacity)));
|
||||
QFont f = p.font();
|
||||
f.setPixelSize(std::clamp(tool.fontPx, 8, 120));
|
||||
p.setFont(f);
|
||||
QTextOption opt;
|
||||
opt.setWrapMode(QTextOption::WrapAtWordBoundaryOrAnywhere);
|
||||
if (tool.align == core::Project::Tool::TextAlign::Left) opt.setAlignment(Qt::AlignLeft | Qt::AlignVCenter);
|
||||
else if (tool.align == core::Project::Tool::TextAlign::Right) opt.setAlignment(Qt::AlignRight | Qt::AlignVCenter);
|
||||
else opt.setAlignment(Qt::AlignHCenter | Qt::AlignVCenter);
|
||||
const QRectF textRect = body.adjusted(10, 8, -10, -8);
|
||||
p.drawText(textRect, tool.text, opt);
|
||||
}
|
||||
|
||||
// 选中描边
|
||||
if (!m_presentationPreviewMode && i == m_selectedTool) {
|
||||
p.setBrush(Qt::NoBrush);
|
||||
p.setPen(QPen(QColor(80, 160, 255, 220), 2.0 / std::max<qreal>(m_scale, 0.001)));
|
||||
p.drawPath(path);
|
||||
}
|
||||
}
|
||||
|
||||
// 创建实体:手绘轨迹预览(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));
|
||||
@@ -1321,6 +1579,28 @@ void EditorCanvas::mousePressEvent(QMouseEvent* e) {
|
||||
}
|
||||
|
||||
if (m_tool == Tool::Move && e->button() == Qt::LeftButton) {
|
||||
// 工具(气泡)优先命中:绘制在实体之后,交互也应优先
|
||||
for (qsizetype i = m_tools.size(); i > 0; --i) {
|
||||
const qsizetype idx = i - 1;
|
||||
const auto& tv = m_tools[idx];
|
||||
if (tv.opacity <= 0.001) continue;
|
||||
if (!tv.tool.id.isEmpty() && m_tempHiddenToolIds.contains(tv.tool.id)) continue;
|
||||
if (tv.tool.type != core::Project::Tool::Type::Bubble) continue;
|
||||
const QPainterPath path = bubblePathWorld(tv.tool);
|
||||
if (path.contains(worldPos)) {
|
||||
m_selectedTool = static_cast<int>(idx);
|
||||
m_selectedEntity = -1;
|
||||
m_draggingTool = true;
|
||||
m_dragMode = DragMode::Free;
|
||||
m_toolDragOffsetOriginWorld = worldPos - m_tools[m_selectedTool].tool.originWorld;
|
||||
m_toolDragStartOriginWorld = m_tools[m_selectedTool].tool.originWorld;
|
||||
emit selectedEntityChanged(false, QString(), 0, QPointF());
|
||||
emit selectedToolChanged(true, m_tools[m_selectedTool].tool.id, m_tools[m_selectedTool].tool.originWorld);
|
||||
update();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 优先:若已选中实体,且点在 gizmo 手柄上,则开启轴约束拖动
|
||||
if (m_selectedEntity >= 0 && m_selectedEntity < m_entities.size()) {
|
||||
const auto& ent = m_entities[m_selectedEntity];
|
||||
@@ -1364,9 +1644,50 @@ void EditorCanvas::mousePressEvent(QMouseEvent* e) {
|
||||
}
|
||||
}
|
||||
|
||||
// 若已选中实体:点击命中该实体本体时,优先拖动“已选中实体”。
|
||||
// 这对父子层级很重要:子实体可能被父实体遮挡,但用户在项目树中选中子实体后仍应可拖动它。
|
||||
if (m_selectedEntity >= 0 && m_selectedEntity < m_entities.size()) {
|
||||
const auto& ent = m_entities[m_selectedEntity];
|
||||
bool hitSelected = false;
|
||||
if (!ent.pathWorld.isEmpty()) {
|
||||
hitSelected = ent.pathWorld.contains(worldPos);
|
||||
} else if (!ent.polygonWorld.isEmpty()) {
|
||||
hitSelected = entity_cutout::pathFromWorldPolygon(ent.polygonWorld).contains(worldPos);
|
||||
} else {
|
||||
hitSelected = ent.rect.contains(worldPos);
|
||||
}
|
||||
if (hitSelected) {
|
||||
m_draggingEntity = true;
|
||||
m_dragMode = DragMode::Free;
|
||||
emit entityDragActiveChanged(true);
|
||||
const QRectF r = ent.rect.isNull() && !ent.polygonWorld.isEmpty()
|
||||
? entity_cutout::pathFromWorldPolygon(ent.polygonWorld).boundingRect()
|
||||
: ent.rect;
|
||||
m_entities[m_selectedEntity].rect = r;
|
||||
m_entityDragOffsetOriginWorld = worldPos - m_entities[m_selectedEntity].animatedOriginWorld;
|
||||
m_entityDragStartAnimatedOrigin = m_entities[m_selectedEntity].animatedOriginWorld;
|
||||
// drag preview baseline
|
||||
m_dragPreviewActive = true;
|
||||
m_dragDelta = QPointF(0, 0);
|
||||
m_dragOriginBase = m_entities[m_selectedEntity].animatedOriginWorld;
|
||||
m_dragRectBase = m_entities[m_selectedEntity].rect;
|
||||
m_dragImageTopLeftBase = m_entities[m_selectedEntity].imageTopLeft;
|
||||
m_dragScaleBase = std::max(1e-6, m_entities[m_selectedEntity].visualScale);
|
||||
m_dragScaleRatio = 1.0;
|
||||
m_dragPolyBase = m_entities[m_selectedEntity].polygonWorld;
|
||||
m_dragPathBase = m_entities[m_selectedEntity].pathWorld;
|
||||
m_dragCentroidBase =
|
||||
m_dragPolyBase.isEmpty() ? m_dragRectBase.center() : entity_cutout::polygonCentroid(m_dragPolyBase);
|
||||
update();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const int hit = hitTestEntity(worldPos);
|
||||
if (hit >= 0) {
|
||||
m_selectedEntity = hit;
|
||||
m_selectedTool = -1;
|
||||
m_draggingTool = false;
|
||||
m_draggingEntity = true;
|
||||
m_dragMode = DragMode::Free;
|
||||
emit entityDragActiveChanged(true);
|
||||
@@ -1391,13 +1712,17 @@ void EditorCanvas::mousePressEvent(QMouseEvent* e) {
|
||||
const QPointF origin = !m_entities[hit].polygonWorld.isEmpty() ? entity_cutout::polygonCentroid(m_entities[hit].polygonWorld)
|
||||
: m_entities[hit].rect.center();
|
||||
emit selectedEntityChanged(true, m_entities[hit].id, m_entities[hit].depth, origin);
|
||||
emit selectedToolChanged(false, QString(), QPointF());
|
||||
update();
|
||||
return;
|
||||
}
|
||||
m_selectedEntity = -1;
|
||||
m_draggingEntity = false;
|
||||
m_selectedTool = -1;
|
||||
m_draggingTool = false;
|
||||
m_dragMode = DragMode::None;
|
||||
emit selectedEntityChanged(false, QString(), 0, QPointF());
|
||||
emit selectedToolChanged(false, QString(), QPointF());
|
||||
update();
|
||||
}
|
||||
}
|
||||
@@ -1536,7 +1861,7 @@ void EditorCanvas::mouseMoveEvent(QMouseEvent* e) {
|
||||
const double ds01 = depthToScale01(depthZ);
|
||||
ent.animatedDepthScale01 = ds01;
|
||||
const double newScale =
|
||||
distanceScaleFromDepth01(ds01, ent.distanceScaleCalibMult) * ent.userScale;
|
||||
(ent.ignoreDistanceScale ? 1.0 : distanceScaleFromDepth01(ds01, ent.distanceScaleCalibMult)) * ent.userScale;
|
||||
ent.visualScale = newScale;
|
||||
if (m_dragPreviewActive) {
|
||||
m_dragScaleRatio = std::clamp(newScale / std::max(1e-6, m_dragScaleBase), 0.02, 50.0);
|
||||
@@ -1553,6 +1878,16 @@ void EditorCanvas::mouseMoveEvent(QMouseEvent* e) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_draggingTool && m_selectedTool >= 0 && m_selectedTool < m_tools.size()) {
|
||||
const QPointF worldPos = viewToWorld(cur);
|
||||
const QPointF newOrigin = worldPos - m_toolDragOffsetOriginWorld;
|
||||
QPointF delta = newOrigin - m_tools[m_selectedTool].tool.originWorld;
|
||||
m_tools[m_selectedTool].tool.originWorld += delta;
|
||||
emit selectedToolChanged(true, m_tools[m_selectedTool].tool.id, m_tools[m_selectedTool].tool.originWorld);
|
||||
update();
|
||||
return;
|
||||
}
|
||||
|
||||
// 平移画布
|
||||
if (m_tool == Tool::Move || (e->buttons() & Qt::MiddleButton) ||
|
||||
(m_presentationPreviewMode && (e->buttons() & Qt::LeftButton))) {
|
||||
@@ -1626,6 +1961,16 @@ void EditorCanvas::mouseReleaseEvent(QMouseEvent* e) {
|
||||
}
|
||||
}
|
||||
|
||||
if (m_draggingTool && m_selectedTool >= 0 && m_selectedTool < m_tools.size() && e->button() == Qt::LeftButton) {
|
||||
const auto& tv = m_tools[m_selectedTool];
|
||||
const QPointF delta = tv.tool.originWorld - m_toolDragStartOriginWorld;
|
||||
if (!tv.tool.id.isEmpty() && (!qFuzzyIsNull(delta.x()) || !qFuzzyIsNull(delta.y()))) {
|
||||
emit requestMoveTool(tv.tool.id, delta);
|
||||
} else if (!tv.tool.id.isEmpty()) {
|
||||
emit selectedToolChanged(true, tv.tool.id, tv.tool.originWorld);
|
||||
}
|
||||
}
|
||||
|
||||
m_dragging = false;
|
||||
if (m_pendingDragging && e->button() == Qt::LeftButton) {
|
||||
m_pendingDragging = false;
|
||||
@@ -1636,6 +1981,7 @@ void EditorCanvas::mouseReleaseEvent(QMouseEvent* e) {
|
||||
emit entityDragActiveChanged(false);
|
||||
}
|
||||
m_draggingEntity = false;
|
||||
m_draggingTool = false;
|
||||
m_dragPreviewActive = false;
|
||||
m_dragMode = DragMode::None;
|
||||
updateCursor();
|
||||
|
||||
@@ -14,6 +14,9 @@
|
||||
#include <QTimer>
|
||||
|
||||
class QKeyEvent;
|
||||
class QDragEnterEvent;
|
||||
class QDragMoveEvent;
|
||||
class QDropEvent;
|
||||
|
||||
class EditorCanvas final : public QWidget {
|
||||
Q_OBJECT
|
||||
@@ -70,7 +73,11 @@ public:
|
||||
/// 退出「点击实体放大」状态并平滑回到进入前的视图(预览模式)
|
||||
void clearPresentationEntityFocus();
|
||||
|
||||
void setEntities(const QVector<core::Project::Entity>& entities, const QString& projectDirAbs);
|
||||
void setEntities(const QVector<core::Project::Entity>& entities,
|
||||
const QVector<double>& opacities01,
|
||||
const QString& projectDirAbs);
|
||||
void setTools(const QVector<core::Project::Tool>& tools, const QVector<double>& opacities01);
|
||||
void setTempHiddenIds(const QSet<QString>& entityIds, const QSet<QString>& toolIds);
|
||||
void setCurrentFrame(int frame);
|
||||
int currentFrame() const { return m_currentFrame; }
|
||||
|
||||
@@ -93,7 +100,9 @@ signals:
|
||||
void hoveredWorldPosChanged(const QPointF& worldPos);
|
||||
void hoveredWorldPosDepthChanged(const QPointF& worldPos, int depthZ);
|
||||
void selectedEntityChanged(bool hasSelection, const QString& id, int depth, const QPointF& originWorld);
|
||||
void selectedToolChanged(bool hasSelection, const QString& id, const QPointF& originWorld);
|
||||
void requestAddEntity(const core::Project::Entity& entity, const QImage& image);
|
||||
void requestAddTool(const core::Project::Tool& tool);
|
||||
/// 创建实体:将裁剪 RGB、标记叠加层与 SAM 提示发往模型服务,由主窗口收 JSON 轮廓后再 addEntity。
|
||||
void requestSamSegment(
|
||||
const QByteArray& cropRgbPng,
|
||||
@@ -105,6 +114,7 @@ signals:
|
||||
/// 待确认实体:用户按回车/点击空白处确认后触发(由主窗口弹窗命名并落盘)。
|
||||
void requestFinalizePendingEntity(const QVector<QPointF>& polyWorld);
|
||||
void requestMoveEntity(const QString& id, const QPointF& delta);
|
||||
void requestMoveTool(const QString& id, const QPointF& delta);
|
||||
void entityDragActiveChanged(bool on);
|
||||
void selectedEntityPreviewChanged(const QString& id, int depth, const QPointF& originWorld);
|
||||
/// 预览模式下点击实体:anchorView 为实体质心在视图中的位置,用于摆放介绍浮层
|
||||
@@ -120,6 +130,9 @@ protected:
|
||||
void mouseReleaseEvent(QMouseEvent* e) override;
|
||||
void wheelEvent(QWheelEvent* e) override;
|
||||
void keyPressEvent(QKeyEvent* e) override;
|
||||
void dragEnterEvent(QDragEnterEvent* e) override;
|
||||
void dragMoveEvent(QDragMoveEvent* e) override;
|
||||
void dropEvent(QDropEvent* e) override;
|
||||
|
||||
private:
|
||||
void ensurePixmapLoaded() const;
|
||||
@@ -154,13 +167,19 @@ private:
|
||||
double visualScale = 1.0; // 实体在 world 坐标下的缩放(用于贴图绘制)
|
||||
double userScale = 1.0; // 与深度距离缩放相乘
|
||||
double distanceScaleCalibMult = 0.0; // 与 Project::Entity 一致;0=未校准
|
||||
bool ignoreDistanceScale = false;
|
||||
QPointF animatedOriginWorld;
|
||||
double animatedDepthScale01 = 0.5;
|
||||
// 编辑模式下实体被设为隐藏时:不响应点选且不绘制,除非当前选中(便于树选隐藏实体)
|
||||
bool hiddenInEditMode = false;
|
||||
double opacity = 1.0; // 0..1(由可见性轨道求值)
|
||||
};
|
||||
int hitTestEntity(const QPointF& worldPos) const;
|
||||
|
||||
private:
|
||||
struct ToolView {
|
||||
core::Project::Tool tool;
|
||||
double opacity = 1.0; // 0..1
|
||||
};
|
||||
|
||||
private:
|
||||
QString m_bgAbsPath;
|
||||
bool m_backgroundVisible = true;
|
||||
@@ -210,11 +229,18 @@ private:
|
||||
qint64 m_lastPreviewEmitMs = 0;
|
||||
qint64 m_lastDepthScaleRecalcMs = 0;
|
||||
int m_selectedEntity = -1;
|
||||
int m_selectedTool = -1;
|
||||
bool m_draggingTool = false;
|
||||
QPointF m_toolDragOffsetOriginWorld;
|
||||
QPointF m_toolDragStartOriginWorld;
|
||||
|
||||
DragMode m_dragMode = DragMode::None;
|
||||
QPointF m_dragStartMouseWorld;
|
||||
|
||||
QVector<Entity> m_entities;
|
||||
QVector<ToolView> m_tools;
|
||||
QSet<QString> m_tempHiddenEntityIds;
|
||||
QSet<QString> m_tempHiddenToolIds;
|
||||
QVector<QPointF> m_strokeWorld;
|
||||
|
||||
int m_currentFrame = 0;
|
||||
|
||||
264
client/gui/library/ResourceLibraryDock.cpp
Normal file
264
client/gui/library/ResourceLibraryDock.cpp
Normal file
@@ -0,0 +1,264 @@
|
||||
#include "library/ResourceLibraryDock.h"
|
||||
|
||||
#include "core/library/EntityJson.h"
|
||||
#include "core/library/ToolJson.h"
|
||||
|
||||
#include <QApplication>
|
||||
#include <QDrag>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonDocument>
|
||||
#include <QListWidget>
|
||||
#include <QMimeData>
|
||||
#include <QPainter>
|
||||
#include <QVBoxLayout>
|
||||
#include <QWidget>
|
||||
|
||||
#include <QScrollBar>
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
namespace gui {
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr const char* kMimeType = "application/x-hfut-resource+json";
|
||||
|
||||
QPixmap makePreviewPixmap(const core::library::LibraryResource& r) {
|
||||
const QSize sz = r.imageSize.isValid() ? r.imageSize : QSize(200, 200);
|
||||
QImage img(sz, QImage::Format_ARGB32_Premultiplied);
|
||||
img.fill(Qt::transparent);
|
||||
|
||||
QPainter p(&img);
|
||||
p.setRenderHint(QPainter::Antialiasing, true);
|
||||
|
||||
QRectF rr(QPointF(0, 0), QSizeF(sz));
|
||||
rr = rr.adjusted(10, 10, -10, -10);
|
||||
p.setPen(QPen(QColor(0, 0, 0, 50), 2));
|
||||
p.setBrush(QBrush(r.accent));
|
||||
p.drawRoundedRect(rr, 18, 18);
|
||||
|
||||
p.setPen(QColor(255, 255, 255, 240));
|
||||
QFont f = p.font();
|
||||
f.setPointSize(std::max(9, f.pointSize()));
|
||||
f.setBold(true);
|
||||
p.setFont(f);
|
||||
p.drawText(rr.adjusted(12, 12, -12, -12), Qt::AlignLeft | Qt::AlignTop, r.displayName);
|
||||
|
||||
return QPixmap::fromImage(img);
|
||||
}
|
||||
|
||||
class ResourceListWidget final : public QListWidget {
|
||||
public:
|
||||
explicit ResourceListWidget(QWidget* parent = nullptr) : QListWidget(parent) {
|
||||
setSelectionMode(QAbstractItemView::SingleSelection);
|
||||
setDragEnabled(true);
|
||||
setDragDropMode(QAbstractItemView::DragOnly);
|
||||
setDefaultDropAction(Qt::CopyAction);
|
||||
setUniformItemSizes(false);
|
||||
setSpacing(6);
|
||||
setViewMode(QListView::IconMode);
|
||||
setResizeMode(QListView::Adjust);
|
||||
// 注意:Static 会让拖拽“完全没反应”(不会启动 drag)。
|
||||
// 这里用 Free,仍然不会在列表内部重排(DragOnly),但允许拖出到画布。
|
||||
setMovement(QListView::Free);
|
||||
setIconSize(QSize(96, 96));
|
||||
setWordWrap(true);
|
||||
// “一行两个资源”:用网格尺寸 + 固定列宽近似实现
|
||||
setGridSize(QSize(180, 140));
|
||||
}
|
||||
|
||||
void setResources(const QVector<core::library::LibraryResource>& r) { m_resources = r; }
|
||||
|
||||
protected:
|
||||
void startDrag(Qt::DropActions supportedActions) override {
|
||||
Q_UNUSED(supportedActions);
|
||||
auto* item = currentItem();
|
||||
if (!item) {
|
||||
return;
|
||||
}
|
||||
bool ok = false;
|
||||
const int idx = item->data(Qt::UserRole).toInt(&ok);
|
||||
if (!ok || idx < 0 || idx >= m_resources.size()) {
|
||||
return;
|
||||
}
|
||||
const auto& res = m_resources[idx];
|
||||
|
||||
QJsonObject payload;
|
||||
payload.insert(QStringLiteral("resourceId"), res.resourceId);
|
||||
payload.insert(QStringLiteral("displayName"), res.displayName);
|
||||
payload.insert(QStringLiteral("kind"), res.kind == core::library::LibraryResource::Kind::Tool ? QStringLiteral("tool")
|
||||
: QStringLiteral("entity"));
|
||||
if (res.kind == core::library::LibraryResource::Kind::Tool) {
|
||||
payload.insert(QStringLiteral("tool"), core::library::toolToJson(res.toolTemplate));
|
||||
} else {
|
||||
payload.insert(QStringLiteral("entity"), core::library::entityToJson(res.entityTemplate));
|
||||
}
|
||||
{
|
||||
QJsonArray a;
|
||||
a.append(res.imageSize.width());
|
||||
a.append(res.imageSize.height());
|
||||
payload.insert(QStringLiteral("imageSize"), a);
|
||||
}
|
||||
{
|
||||
QJsonArray a;
|
||||
a.append(res.accent.red());
|
||||
a.append(res.accent.green());
|
||||
a.append(res.accent.blue());
|
||||
a.append(res.accent.alpha());
|
||||
payload.insert(QStringLiteral("accent"), a);
|
||||
}
|
||||
{
|
||||
QJsonArray a;
|
||||
a.append(res.imageOffsetFromOrigin.x());
|
||||
a.append(res.imageOffsetFromOrigin.y());
|
||||
payload.insert(QStringLiteral("imageOffsetFromOrigin"), a);
|
||||
}
|
||||
|
||||
const QByteArray bytes = QJsonDocument(payload).toJson(QJsonDocument::Compact);
|
||||
auto* mime = new QMimeData();
|
||||
mime->setData(QString::fromUtf8(kMimeType), bytes);
|
||||
|
||||
auto* drag = new QDrag(this);
|
||||
drag->setMimeData(mime);
|
||||
drag->setPixmap(item->icon().pixmap(iconSize()));
|
||||
drag->setHotSpot(QPoint(iconSize().width() / 2, iconSize().height() / 2));
|
||||
drag->exec(Qt::CopyAction);
|
||||
}
|
||||
|
||||
private:
|
||||
QVector<core::library::LibraryResource> m_resources;
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
ResourceLibraryDock::ResourceLibraryDock(QWidget* parent)
|
||||
: QDockWidget(QStringLiteral("资源库"), parent) {
|
||||
setAllowedAreas(Qt::LeftDockWidgetArea | Qt::RightDockWidgetArea);
|
||||
setFeatures(QDockWidget::DockWidgetMovable | QDockWidget::DockWidgetFloatable | QDockWidget::DockWidgetClosable);
|
||||
|
||||
auto* shell = new QWidget(this);
|
||||
auto* layout = new QVBoxLayout(shell);
|
||||
layout->setContentsMargins(8, 8, 8, 8);
|
||||
layout->setSpacing(6);
|
||||
|
||||
m_list = new ResourceListWidget(shell);
|
||||
layout->addWidget(m_list, 1);
|
||||
if (m_list && m_list->verticalScrollBar()) {
|
||||
connect(m_list->verticalScrollBar(), &QScrollBar::valueChanged, this, [this](int value) {
|
||||
if (!m_list || !m_list->verticalScrollBar()) return;
|
||||
if (!m_onlineProvider || !m_onlineBound || m_onlineLoading || m_onlineExhausted) return;
|
||||
const int maxV = m_list->verticalScrollBar()->maximum();
|
||||
// 接近底部(阈值 80px):自动加载下一页
|
||||
if (value >= maxV - 80) {
|
||||
fetchOnlineNextPage();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
setWidget(shell);
|
||||
}
|
||||
|
||||
void ResourceLibraryDock::setProviders(core::library::ResourceLibraryProvider* localProvider,
|
||||
core::library::ResourceLibraryProvider* onlineProvider) {
|
||||
if (m_localProvider == localProvider && m_onlineProvider == onlineProvider) {
|
||||
return;
|
||||
}
|
||||
if (m_localProvider) disconnect(m_localProvider, nullptr, this, nullptr);
|
||||
if (m_onlineProvider) disconnect(m_onlineProvider, nullptr, this, nullptr);
|
||||
|
||||
m_localProvider = localProvider;
|
||||
m_onlineProvider = onlineProvider;
|
||||
m_onlineExhausted = false;
|
||||
|
||||
auto bind = [&](core::library::ResourceLibraryProvider* p,
|
||||
QVector<core::library::LibraryResource>& store,
|
||||
bool isOnline) {
|
||||
if (!p) {
|
||||
store.clear();
|
||||
return;
|
||||
}
|
||||
connect(p, &core::library::ResourceLibraryProvider::resourcesReady,
|
||||
this, [this, &store, isOnline](const QVector<core::library::LibraryResource>& res) {
|
||||
if (!isOnline) {
|
||||
store = res;
|
||||
} else {
|
||||
m_onlineLoading = false;
|
||||
if (res.isEmpty()) {
|
||||
m_onlineExhausted = true;
|
||||
return;
|
||||
}
|
||||
store += res;
|
||||
}
|
||||
rebuildCombinedList();
|
||||
});
|
||||
connect(p, &core::library::ResourceLibraryProvider::resourcesFailed,
|
||||
this, [this, isOnline](const QString& err) {
|
||||
Q_UNUSED(err);
|
||||
if (isOnline) {
|
||||
m_onlineLoading = false;
|
||||
// 在线错误默认不打扰 UI;下次滚动到底仍可重试(除非 provider 选择返回空 ready)。
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
bind(m_localProvider, m_localResources, false);
|
||||
bind(m_onlineProvider, m_onlineResources, true);
|
||||
|
||||
if (m_localProvider) {
|
||||
m_localProvider->fetchResourcesAsync();
|
||||
} else {
|
||||
m_localResources.clear();
|
||||
}
|
||||
rebuildCombinedList();
|
||||
ensureOnlineLoaded();
|
||||
}
|
||||
|
||||
void ResourceLibraryDock::ensureOnlineLoaded() {
|
||||
if (m_onlineBound) {
|
||||
return;
|
||||
}
|
||||
m_onlineBound = true;
|
||||
if (!m_onlineProvider) {
|
||||
return;
|
||||
}
|
||||
fetchOnlineNextPage();
|
||||
}
|
||||
|
||||
void ResourceLibraryDock::fetchOnlineNextPage() {
|
||||
if (!m_onlineProvider) {
|
||||
return;
|
||||
}
|
||||
if (m_onlineLoading) {
|
||||
return;
|
||||
}
|
||||
if (m_onlineExhausted) {
|
||||
return;
|
||||
}
|
||||
m_onlineLoading = true;
|
||||
// provider 当前仍是预留实现;这里保持“分页”调用语义(多次调用 fetchResourcesAsync)
|
||||
m_onlineProvider->fetchResourcesAsync();
|
||||
}
|
||||
|
||||
void ResourceLibraryDock::rebuildCombinedList() {
|
||||
if (!m_list) return;
|
||||
auto* list = static_cast<ResourceListWidget*>(m_list);
|
||||
list->clear();
|
||||
QVector<core::library::LibraryResource> combined;
|
||||
combined.reserve(m_localResources.size() + m_onlineResources.size());
|
||||
combined += m_localResources;
|
||||
combined += m_onlineResources;
|
||||
list->setResources(combined);
|
||||
for (int i = 0; i < combined.size(); ++i) {
|
||||
const auto& r = combined[i];
|
||||
auto* it = new QListWidgetItem();
|
||||
it->setFlags(it->flags() | Qt::ItemIsDragEnabled);
|
||||
it->setText(r.displayName);
|
||||
it->setIcon(QIcon(makePreviewPixmap(r)));
|
||||
it->setData(Qt::UserRole, i);
|
||||
it->setToolTip(r.resourceId);
|
||||
list->addItem(it);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace gui
|
||||
|
||||
38
client/gui/library/ResourceLibraryDock.h
Normal file
38
client/gui/library/ResourceLibraryDock.h
Normal file
@@ -0,0 +1,38 @@
|
||||
#pragma once
|
||||
|
||||
#include "core/library/ResourceLibraryProvider.h"
|
||||
|
||||
#include <QDockWidget>
|
||||
|
||||
class QListWidget;
|
||||
|
||||
namespace gui {
|
||||
|
||||
class ResourceLibraryDock final : public QDockWidget {
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit ResourceLibraryDock(QWidget* parent = nullptr);
|
||||
~ResourceLibraryDock() override = default;
|
||||
|
||||
void setProviders(core::library::ResourceLibraryProvider* localProvider,
|
||||
core::library::ResourceLibraryProvider* onlineProvider);
|
||||
|
||||
private:
|
||||
void rebuildCombinedList();
|
||||
void ensureOnlineLoaded();
|
||||
void fetchOnlineNextPage();
|
||||
|
||||
private:
|
||||
core::library::ResourceLibraryProvider* m_localProvider = nullptr;
|
||||
core::library::ResourceLibraryProvider* m_onlineProvider = nullptr;
|
||||
QVector<core::library::LibraryResource> m_localResources;
|
||||
QVector<core::library::LibraryResource> m_onlineResources;
|
||||
|
||||
QListWidget* m_list = nullptr;
|
||||
bool m_onlineBound = false;
|
||||
bool m_onlineLoading = false;
|
||||
bool m_onlineExhausted = false;
|
||||
};
|
||||
|
||||
} // namespace gui
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -10,6 +10,7 @@
|
||||
#include <QTimer>
|
||||
|
||||
#include <QToolButton>
|
||||
#include <QSet>
|
||||
|
||||
class ToolOptionPopup;
|
||||
|
||||
@@ -34,7 +35,14 @@ class TimelineWidget;
|
||||
namespace gui {
|
||||
class BackgroundPropertySection;
|
||||
class EntityPropertySection;
|
||||
class ToolPropertySection;
|
||||
class EntityIntroPopup;
|
||||
class ResourceLibraryDock;
|
||||
}
|
||||
namespace core::library {
|
||||
class ResourceLibraryProvider;
|
||||
class FakeResourceLibraryProvider;
|
||||
class OnlineResourceLibraryProvider;
|
||||
}
|
||||
|
||||
class MainWindow : public QMainWindow {
|
||||
@@ -80,8 +88,10 @@ private:
|
||||
void createEditMenu(); // 编辑菜单
|
||||
void createHelpMenu(); // 帮助菜单
|
||||
void createViewMenu(); // 视图菜单
|
||||
void createWindowMenu(); // 窗口菜单(资源库等)
|
||||
void createProjectTreeDock();
|
||||
void createTimelineDock();
|
||||
void createResourceLibraryDock();
|
||||
void refreshProjectTree();
|
||||
void updateUiEnabledState(); // 更新“可用性/勾选/默认显隐”,不要做业务逻辑
|
||||
void applyUiMode(UiMode mode); // 统一控制 welcome/editor 两态的显隐策略
|
||||
@@ -99,6 +109,7 @@ private:
|
||||
void refreshPreviewPage();
|
||||
void refreshEditorPage();
|
||||
void applyTimelineFromProject();
|
||||
void updateTimelineTracks();
|
||||
void refreshDopeSheet();
|
||||
void setPreviewRequested(bool preview);
|
||||
void syncPreviewPlaybackBar();
|
||||
@@ -115,6 +126,7 @@ private:
|
||||
QStackedWidget* m_propertyStack = nullptr;
|
||||
gui::BackgroundPropertySection* m_bgPropertySection = nullptr;
|
||||
gui::EntityPropertySection* m_entityPropertySection = nullptr;
|
||||
gui::ToolPropertySection* m_toolPropertySection = nullptr;
|
||||
QToolButton* m_btnCreateEntity = nullptr;
|
||||
ToolOptionPopup* m_createEntityPopup = nullptr;
|
||||
QToolButton* m_btnToggleDepthOverlay = nullptr;
|
||||
@@ -125,6 +137,7 @@ private:
|
||||
QDockWidget* m_dockProjectTree = nullptr;
|
||||
QDockWidget* m_dockProperties = nullptr;
|
||||
QDockWidget* m_dockTimeline = nullptr;
|
||||
QDockWidget* m_dockResourceLibrary = nullptr;
|
||||
QTreeWidgetItem* m_itemBackground = nullptr;
|
||||
|
||||
QAction* m_actionUndo = nullptr;
|
||||
@@ -134,6 +147,7 @@ private:
|
||||
QAction* m_actionToggleProjectTree = nullptr;
|
||||
QAction* m_actionToggleProperties = nullptr;
|
||||
QAction* m_actionToggleTimeline = nullptr;
|
||||
QAction* m_actionToggleResourceLibrary = nullptr;
|
||||
QAction* m_actionEnterPreview = nullptr;
|
||||
QAction* m_actionBackToEditor = nullptr;
|
||||
QAction* m_actionCanvasWorldAxes = nullptr;
|
||||
@@ -152,13 +166,18 @@ private:
|
||||
QPointF m_lastWorldPos;
|
||||
int m_lastWorldZ = -1;
|
||||
bool m_hasSelectedEntity = false;
|
||||
bool m_hasSelectedTool = false;
|
||||
bool m_syncingTreeSelection = false;
|
||||
int m_selectedEntityDepth = 0;
|
||||
QPointF m_selectedEntityOrigin;
|
||||
QString m_selectedEntityId;
|
||||
QString m_selectedToolId;
|
||||
QString m_selectedEntityDisplayNameCache;
|
||||
QString m_bgAbsCache;
|
||||
QString m_bgSizeTextCache;
|
||||
// 项目树“眼睛”:仅用于画布临时隐藏(不持久化、不进时间轴)
|
||||
QSet<QString> m_tempHiddenEntityIds;
|
||||
QSet<QString> m_tempHiddenToolIds;
|
||||
void updateStatusBarText();
|
||||
void syncCreateEntityToolButtonTooltip();
|
||||
void refreshPropertyPanel();
|
||||
@@ -174,7 +193,7 @@ private:
|
||||
QTimer* m_playTimer = nullptr;
|
||||
TimelineWidget* m_timeline = nullptr;
|
||||
QToolButton* m_btnPlay = nullptr;
|
||||
QLabel* m_frameLabel = nullptr;
|
||||
QComboBox* m_schemeSelector = nullptr;
|
||||
// 时间轴区间选择(用于逐帧贴图动画)
|
||||
int m_timelineRangeStart = -1;
|
||||
int m_timelineRangeEnd = -1;
|
||||
@@ -187,4 +206,7 @@ private:
|
||||
QFrame* m_previewPlaybackBar = nullptr;
|
||||
QToolButton* m_previewBtnPlay = nullptr;
|
||||
QToolButton* m_previewBtnPause = nullptr;
|
||||
|
||||
gui::ResourceLibraryDock* m_resourceLibraryDockWidget = nullptr;
|
||||
core::library::ResourceLibraryProvider* m_resourceLibraryProvider = nullptr;
|
||||
};
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#include <QDoubleSpinBox>
|
||||
#include <QFormLayout>
|
||||
#include <QHBoxLayout>
|
||||
#include <QCheckBox>
|
||||
#include <QLabel>
|
||||
#include <QLineEdit>
|
||||
#include <QListWidget>
|
||||
@@ -55,6 +56,15 @@ EntityPropertySection::EntityPropertySection(QWidget* parent)
|
||||
m_userScale->setToolTip(QStringLiteral("人为整体缩放,与深度距离缩放相乘"));
|
||||
form->addRow(QStringLiteral("整体缩放"), m_userScale);
|
||||
|
||||
m_ignoreDistanceScale = new QCheckBox(QStringLiteral("不受距离缩放影响"), this);
|
||||
m_ignoreDistanceScale->setToolTip(QStringLiteral("开启后实体不受深度驱动的距离缩放影响,仅受整体缩放影响(对话气泡默认开启)"));
|
||||
form->addRow(QStringLiteral("距离缩放"), m_ignoreDistanceScale);
|
||||
|
||||
m_visible = new QCheckBox(QString(), this);
|
||||
m_visible->setChecked(true);
|
||||
m_visible->setToolTip(QStringLiteral("随帧变化:在当前帧切换会写入可见性关键帧(10帧淡入淡出)"));
|
||||
form->addRow(QStringLiteral("可见性"), m_visible);
|
||||
|
||||
lay->addLayout(form);
|
||||
|
||||
m_introHeader = new QWidget(this);
|
||||
@@ -133,6 +143,8 @@ EntityPropertySection::EntityPropertySection(QWidget* parent)
|
||||
connect(m_pivot, &Vec2ParamControl::valueChanged, this, &EntityPropertySection::pivotEdited);
|
||||
connect(m_centroid, &Vec2ParamControl::valueChanged, this, &EntityPropertySection::centroidEdited);
|
||||
connect(m_userScale, qOverload<double>(&QDoubleSpinBox::valueChanged), this, &EntityPropertySection::userScaleEdited);
|
||||
connect(m_ignoreDistanceScale, &QCheckBox::toggled, this, &EntityPropertySection::ignoreDistanceScaleToggled);
|
||||
connect(m_visible, &QCheckBox::toggled, this, &EntityPropertySection::visibleToggled);
|
||||
|
||||
connect(m_introTitle, &QLineEdit::textChanged, this, [this](const QString&) { scheduleIntroPersist(); });
|
||||
connect(m_introBody, &QTextEdit::textChanged, this, [this]() { scheduleIntroPersist(); });
|
||||
@@ -183,6 +195,16 @@ void EntityPropertySection::clearDisconnected() {
|
||||
m_userScale->setValue(1.0);
|
||||
m_userScale->blockSignals(false);
|
||||
}
|
||||
if (m_ignoreDistanceScale) {
|
||||
m_ignoreDistanceScale->blockSignals(true);
|
||||
m_ignoreDistanceScale->setChecked(false);
|
||||
m_ignoreDistanceScale->blockSignals(false);
|
||||
}
|
||||
if (m_visible) {
|
||||
m_visible->blockSignals(true);
|
||||
m_visible->setChecked(true);
|
||||
m_visible->blockSignals(false);
|
||||
}
|
||||
if (m_introTitle) {
|
||||
m_introTitle->blockSignals(true);
|
||||
m_introTitle->clear();
|
||||
@@ -224,6 +246,16 @@ void EntityPropertySection::applyState(const EntityPropertyUiState& s) {
|
||||
m_userScale->setValue(s.userScale);
|
||||
m_userScale->blockSignals(false);
|
||||
}
|
||||
if (m_ignoreDistanceScale) {
|
||||
m_ignoreDistanceScale->blockSignals(true);
|
||||
m_ignoreDistanceScale->setChecked(s.ignoreDistanceScale);
|
||||
m_ignoreDistanceScale->blockSignals(false);
|
||||
}
|
||||
if (m_visible) {
|
||||
m_visible->blockSignals(true);
|
||||
m_visible->setChecked(s.visible);
|
||||
m_visible->blockSignals(false);
|
||||
}
|
||||
if (m_introTitle) {
|
||||
m_introTitle->blockSignals(true);
|
||||
m_introTitle->setText(s.intro.title);
|
||||
@@ -288,6 +320,8 @@ void EntityPropertySection::setEditingEnabled(bool on) {
|
||||
if (m_pivot) m_pivot->setEnabled(on);
|
||||
if (m_centroid) m_centroid->setEnabled(on);
|
||||
if (m_userScale) m_userScale->setEnabled(on);
|
||||
if (m_ignoreDistanceScale) m_ignoreDistanceScale->setEnabled(on);
|
||||
if (m_visible) m_visible->setEnabled(on);
|
||||
if (m_introHeader) m_introHeader->setEnabled(on);
|
||||
if (m_introToggle) m_introToggle->setEnabled(on);
|
||||
if (m_introTitle) m_introTitle->setEnabled(on);
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
class QLabel;
|
||||
class QLineEdit;
|
||||
class QDoubleSpinBox;
|
||||
class QCheckBox;
|
||||
class QTextEdit;
|
||||
class QListWidget;
|
||||
class QPushButton;
|
||||
@@ -29,6 +30,8 @@ struct EntityPropertyUiState {
|
||||
QPointF pivot;
|
||||
QPointF centroid;
|
||||
double userScale = 1.0;
|
||||
bool ignoreDistanceScale = false;
|
||||
bool visible = true;
|
||||
core::EntityIntroContent intro;
|
||||
};
|
||||
|
||||
@@ -49,6 +52,9 @@ signals:
|
||||
void pivotEdited(double x, double y);
|
||||
void centroidEdited(double x, double y);
|
||||
void userScaleEdited(double value);
|
||||
void ignoreDistanceScaleToggled(bool on);
|
||||
// 可见性(动画通道):在当前帧写关键帧
|
||||
void visibleToggled(bool on);
|
||||
/// 介绍字段变更后防抖触发,由主窗口写入工程
|
||||
void introContentEdited();
|
||||
void introAddImageRequested();
|
||||
@@ -63,6 +69,8 @@ private:
|
||||
Vec2ParamControl* m_pivot = nullptr;
|
||||
Vec2ParamControl* m_centroid = nullptr;
|
||||
QDoubleSpinBox* m_userScale = nullptr;
|
||||
QCheckBox* m_ignoreDistanceScale = nullptr;
|
||||
QCheckBox* m_visible = nullptr;
|
||||
|
||||
QLineEdit* m_introTitle = nullptr;
|
||||
QTextEdit* m_introBody = nullptr;
|
||||
|
||||
130
client/gui/props/ToolPropertySection.cpp
Normal file
130
client/gui/props/ToolPropertySection.cpp
Normal file
@@ -0,0 +1,130 @@
|
||||
#include "props/ToolPropertySection.h"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
#include <QComboBox>
|
||||
#include <QFormLayout>
|
||||
#include <QCheckBox>
|
||||
#include <QLineEdit>
|
||||
#include <QSlider>
|
||||
#include <QSpinBox>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
namespace gui {
|
||||
|
||||
ToolPropertySection::ToolPropertySection(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_text = new QLineEdit(this);
|
||||
m_text->setPlaceholderText(QStringLiteral("对话内容…"));
|
||||
form->addRow(QStringLiteral("文字"), m_text);
|
||||
|
||||
m_pointerT = new QSlider(Qt::Horizontal, this);
|
||||
m_pointerT->setRange(0, 1000);
|
||||
m_pointerT->setSingleStep(10);
|
||||
m_pointerT->setPageStep(50);
|
||||
m_pointerT->setValue(500);
|
||||
m_pointerT->setToolTip(QStringLiteral("发言实体位置"));
|
||||
form->addRow(QStringLiteral("指向"), m_pointerT);
|
||||
|
||||
m_fontPx = new QSpinBox(this);
|
||||
m_fontPx->setRange(8, 120);
|
||||
m_fontPx->setSingleStep(1);
|
||||
m_fontPx->setValue(18);
|
||||
form->addRow(QStringLiteral("字号"), m_fontPx);
|
||||
|
||||
m_align = new QComboBox(this);
|
||||
m_align->addItems({QStringLiteral("左对齐"), QStringLiteral("居中"), QStringLiteral("右对齐")});
|
||||
form->addRow(QStringLiteral("对齐"), m_align);
|
||||
|
||||
m_visible = new QCheckBox(QString(), this);
|
||||
m_visible->setChecked(true);
|
||||
m_visible->setToolTip(QStringLiteral("随帧变化:在当前帧切换会写入可见性关键帧(10帧淡入淡出)"));
|
||||
form->addRow(QStringLiteral("可见性"), m_visible);
|
||||
|
||||
lay->addLayout(form);
|
||||
lay->addStretch(1);
|
||||
|
||||
connect(m_text, &QLineEdit::editingFinished, this, [this]() {
|
||||
if (m_text) emit textCommitted(m_text->text());
|
||||
});
|
||||
connect(m_pointerT, &QSlider::valueChanged, this, &ToolPropertySection::pointerTChanged);
|
||||
connect(m_fontPx, qOverload<int>(&QSpinBox::valueChanged), this, &ToolPropertySection::fontPxChanged);
|
||||
connect(m_align, qOverload<int>(&QComboBox::currentIndexChanged), this, &ToolPropertySection::alignChanged);
|
||||
connect(m_visible, &QCheckBox::toggled, this, &ToolPropertySection::visibleToggled);
|
||||
}
|
||||
|
||||
void ToolPropertySection::setEditingEnabled(bool on) {
|
||||
for (auto* w : {static_cast<QWidget*>(m_text), static_cast<QWidget*>(m_pointerT),
|
||||
static_cast<QWidget*>(m_fontPx), static_cast<QWidget*>(m_align),
|
||||
static_cast<QWidget*>(m_visible)}) {
|
||||
if (w) w->setEnabled(on);
|
||||
}
|
||||
}
|
||||
|
||||
void ToolPropertySection::clearDisconnected() {
|
||||
setEditingEnabled(false);
|
||||
if (m_text) {
|
||||
m_text->blockSignals(true);
|
||||
m_text->clear();
|
||||
m_text->blockSignals(false);
|
||||
}
|
||||
if (m_pointerT) {
|
||||
m_pointerT->blockSignals(true);
|
||||
m_pointerT->setValue(500);
|
||||
m_pointerT->blockSignals(false);
|
||||
}
|
||||
if (m_fontPx) {
|
||||
m_fontPx->blockSignals(true);
|
||||
m_fontPx->setValue(18);
|
||||
m_fontPx->blockSignals(false);
|
||||
}
|
||||
if (m_align) {
|
||||
m_align->blockSignals(true);
|
||||
m_align->setCurrentIndex(1);
|
||||
m_align->blockSignals(false);
|
||||
}
|
||||
if (m_visible) {
|
||||
m_visible->blockSignals(true);
|
||||
m_visible->setChecked(true);
|
||||
m_visible->blockSignals(false);
|
||||
}
|
||||
}
|
||||
|
||||
void ToolPropertySection::applyState(const ToolPropertyUiState& s) {
|
||||
setEditingEnabled(true);
|
||||
if (m_text) {
|
||||
m_text->blockSignals(true);
|
||||
m_text->setText(s.text);
|
||||
m_text->blockSignals(false);
|
||||
}
|
||||
if (m_pointerT) {
|
||||
m_pointerT->blockSignals(true);
|
||||
m_pointerT->setValue(std::clamp(s.pointerTThousandths, 0, 1000));
|
||||
m_pointerT->blockSignals(false);
|
||||
}
|
||||
if (m_fontPx) {
|
||||
m_fontPx->blockSignals(true);
|
||||
m_fontPx->setValue(std::clamp(s.fontPx, 8, 120));
|
||||
m_fontPx->blockSignals(false);
|
||||
}
|
||||
if (m_align) {
|
||||
m_align->blockSignals(true);
|
||||
m_align->setCurrentIndex(std::clamp(s.alignIndex, 0, 2));
|
||||
m_align->blockSignals(false);
|
||||
}
|
||||
if (m_visible) {
|
||||
m_visible->blockSignals(true);
|
||||
m_visible->setChecked(s.visible);
|
||||
m_visible->blockSignals(false);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace gui
|
||||
50
client/gui/props/ToolPropertySection.h
Normal file
50
client/gui/props/ToolPropertySection.h
Normal file
@@ -0,0 +1,50 @@
|
||||
#pragma once
|
||||
|
||||
#include "props/PropertySectionWidget.h"
|
||||
|
||||
#include <QString>
|
||||
|
||||
class QComboBox;
|
||||
class QLineEdit;
|
||||
class QSlider;
|
||||
class QSpinBox;
|
||||
class QCheckBox;
|
||||
|
||||
namespace gui {
|
||||
|
||||
struct ToolPropertyUiState {
|
||||
QString displayName;
|
||||
QString text;
|
||||
int pointerTThousandths = 500; // bubblePointerT01 * 1000,0=左 1000=右
|
||||
int fontPx = 18;
|
||||
int alignIndex = 1; // 0=left,1=center,2=right
|
||||
bool visible = true;
|
||||
};
|
||||
|
||||
class ToolPropertySection final : public PropertySectionWidget {
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit ToolPropertySection(QWidget* parent = nullptr);
|
||||
|
||||
void clearDisconnected();
|
||||
void applyState(const ToolPropertyUiState& s);
|
||||
void setEditingEnabled(bool on);
|
||||
|
||||
signals:
|
||||
void textCommitted(const QString& text);
|
||||
void pointerTChanged(int thousandths);
|
||||
void fontPxChanged(int px);
|
||||
void alignChanged(int alignIndex);
|
||||
// 可见性(动画通道):在当前帧写关键帧
|
||||
void visibleToggled(bool on);
|
||||
|
||||
private:
|
||||
QLineEdit* m_text = nullptr;
|
||||
QSlider* m_pointerT = nullptr;
|
||||
QSpinBox* m_fontPx = nullptr;
|
||||
QComboBox* m_align = nullptr;
|
||||
QCheckBox* m_visible = nullptr;
|
||||
};
|
||||
|
||||
} // namespace gui
|
||||
|
||||
@@ -1,16 +1,33 @@
|
||||
#include "timeline/TimelineWidget.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
|
||||
#include <QFontMetrics>
|
||||
#include <QMouseEvent>
|
||||
#include <QPainter>
|
||||
#include <QResizeEvent>
|
||||
#include <QWheelEvent>
|
||||
|
||||
namespace {
|
||||
|
||||
int clampFrame(int f, int a, int b) {
|
||||
if (a > b) std::swap(a, b);
|
||||
return std::clamp(f, a, b);
|
||||
static int pickMajorStep(double pixelsPerFrame) {
|
||||
// 主刻度间距(帧):保证屏幕上大约 ≥ 48px
|
||||
const double targetPx = 48.0;
|
||||
const double raw = targetPx / std::max(pixelsPerFrame, 1e-6);
|
||||
static const int cand[] = {1, 2, 5, 10, 15, 20, 25, 50, 75, 100, 150, 200, 250, 500, 1000, 2000, 5000};
|
||||
for (int c : cand) {
|
||||
if (c >= raw) return c;
|
||||
}
|
||||
return cand[sizeof(cand) / sizeof(cand[0]) - 1];
|
||||
}
|
||||
|
||||
static int pickMinorStep(int major) {
|
||||
if (major >= 100) return major / 5;
|
||||
if (major >= 20) return major / 4;
|
||||
if (major >= 10) return major / 5;
|
||||
if (major >= 5) return 1;
|
||||
return 1;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
@@ -18,17 +35,20 @@ int clampFrame(int f, int a, int b) {
|
||||
TimelineWidget::TimelineWidget(QWidget* parent)
|
||||
: QWidget(parent) {
|
||||
setMouseTracking(true);
|
||||
setMinimumHeight(28);
|
||||
// 单行紧凑:标尺 + 轨道(帧号画在播放头处,随坐标轴滚动)
|
||||
setMinimumHeight(kRulerHeight + 18 + 6);
|
||||
setFocusPolicy(Qt::StrongFocus);
|
||||
setToolTip(QStringLiteral("片段时间轴(固定 0-600):左键拖动播放头;滚轮:逐帧"));
|
||||
}
|
||||
|
||||
void TimelineWidget::resizeEvent(QResizeEvent* e) {
|
||||
QWidget::resizeEvent(e);
|
||||
update();
|
||||
}
|
||||
|
||||
void TimelineWidget::setFrameRange(int start, int end) {
|
||||
if (m_start == start && m_end == end) {
|
||||
return;
|
||||
}
|
||||
m_start = start;
|
||||
m_end = end;
|
||||
m_currentFrame = clampFrame(m_currentFrame, m_start, m_end);
|
||||
(void)start;
|
||||
(void)end;
|
||||
update();
|
||||
}
|
||||
|
||||
@@ -36,6 +56,15 @@ void TimelineWidget::setCurrentFrame(int frame) {
|
||||
setFrameInternal(frame, false);
|
||||
}
|
||||
|
||||
void TimelineWidget::setCurrentFrameProgrammatic(int frame) {
|
||||
const int f = std::clamp(frame, kStart, kEnd - 1);
|
||||
if (m_currentFrame == f) {
|
||||
return;
|
||||
}
|
||||
m_currentFrame = f;
|
||||
update();
|
||||
}
|
||||
|
||||
void TimelineWidget::setSelectionRange(int start, int end) {
|
||||
if (start < 0 || end < 0) {
|
||||
m_selStart = -1;
|
||||
@@ -43,41 +72,40 @@ void TimelineWidget::setSelectionRange(int start, int end) {
|
||||
update();
|
||||
return;
|
||||
}
|
||||
m_selStart = clampFrame(std::min(start, end), m_start, m_end);
|
||||
m_selEnd = clampFrame(std::max(start, end), m_start, m_end);
|
||||
const int lo = std::min(start, end);
|
||||
const int hi = std::max(start, end);
|
||||
m_selStart = std::clamp(lo, kStart, kEnd - 1);
|
||||
m_selEnd = std::clamp(hi, m_selStart, kEnd - 1);
|
||||
update();
|
||||
}
|
||||
|
||||
void TimelineWidget::setKeyframeTracks(const core::Project::Entity* e) {
|
||||
m_locFrames.clear();
|
||||
m_scaleFrames.clear();
|
||||
m_imgFrames.clear();
|
||||
if (!e) {
|
||||
update();
|
||||
return;
|
||||
}
|
||||
m_locFrames.reserve(e->locationKeys.size());
|
||||
for (const auto& k : e->locationKeys) m_locFrames.push_back(k.frame);
|
||||
m_scaleFrames.reserve(e->userScaleKeys.size());
|
||||
for (const auto& k : e->userScaleKeys) m_scaleFrames.push_back(k.frame);
|
||||
m_imgFrames.reserve(e->imageFrames.size());
|
||||
for (const auto& k : e->imageFrames) m_imgFrames.push_back(k.frame);
|
||||
static void uniqSort(QVector<int>& v) {
|
||||
std::sort(v.begin(), v.end());
|
||||
v.erase(std::unique(v.begin(), v.end()), v.end());
|
||||
}
|
||||
|
||||
auto uniqSort = [](QVector<int>& v) {
|
||||
std::sort(v.begin(), v.end());
|
||||
v.erase(std::unique(v.begin(), v.end()), v.end());
|
||||
};
|
||||
static bool containsFrame(const QVector<int>& v, int f) {
|
||||
return std::binary_search(v.begin(), v.end(), f);
|
||||
}
|
||||
|
||||
void TimelineWidget::setKeyframeTracks(const QVector<int>& locFrames,
|
||||
const QVector<int>& scaleFrames,
|
||||
const QVector<int>& imgFrames,
|
||||
const QVector<int>& visFrames) {
|
||||
m_locFrames = locFrames;
|
||||
m_scaleFrames = scaleFrames;
|
||||
m_imgFrames = imgFrames;
|
||||
m_visFrames = visFrames;
|
||||
uniqSort(m_locFrames);
|
||||
uniqSort(m_scaleFrames);
|
||||
uniqSort(m_imgFrames);
|
||||
// 轨道变了:若当前选中的关键帧不再存在,则清除
|
||||
auto contains = [](const QVector<int>& v, int f) {
|
||||
return std::binary_search(v.begin(), v.end(), f);
|
||||
};
|
||||
uniqSort(m_visFrames);
|
||||
|
||||
bool ok = true;
|
||||
if (m_selKeyKind == KeyKind::Location) ok = contains(m_locFrames, m_selKeyFrame);
|
||||
if (m_selKeyKind == KeyKind::UserScale) ok = contains(m_scaleFrames, m_selKeyFrame);
|
||||
if (m_selKeyKind == KeyKind::Image) ok = contains(m_imgFrames, m_selKeyFrame);
|
||||
if (m_selKeyKind == KeyKind::Location) ok = containsFrame(m_locFrames, m_selKeyFrame);
|
||||
if (m_selKeyKind == KeyKind::UserScale) ok = containsFrame(m_scaleFrames, m_selKeyFrame);
|
||||
if (m_selKeyKind == KeyKind::Image) ok = containsFrame(m_imgFrames, m_selKeyFrame);
|
||||
if (m_selKeyKind == KeyKind::Visibility) ok = containsFrame(m_visFrames, m_selKeyFrame);
|
||||
if (!ok) {
|
||||
m_selKeyKind = KeyKind::None;
|
||||
m_selKeyFrame = -1;
|
||||
@@ -86,33 +114,67 @@ void TimelineWidget::setKeyframeTracks(const core::Project::Entity* e) {
|
||||
update();
|
||||
}
|
||||
|
||||
QRect TimelineWidget::trackRect() const {
|
||||
const int pad = 8;
|
||||
const int h = height();
|
||||
return QRect(pad, 0, std::max(1, width() - pad * 2), h);
|
||||
void TimelineWidget::setToolKeyframeTracks(const QVector<int>& locFrames,
|
||||
const QVector<int>& visFrames) {
|
||||
m_locFrames = locFrames;
|
||||
m_scaleFrames.clear();
|
||||
m_imgFrames.clear();
|
||||
m_visFrames = visFrames;
|
||||
uniqSort(m_locFrames);
|
||||
uniqSort(m_visFrames);
|
||||
|
||||
bool ok = true;
|
||||
if (m_selKeyKind == KeyKind::Location) ok = containsFrame(m_locFrames, m_selKeyFrame);
|
||||
if (m_selKeyKind == KeyKind::Visibility) ok = containsFrame(m_visFrames, m_selKeyFrame);
|
||||
if (!ok) {
|
||||
m_selKeyKind = KeyKind::None;
|
||||
m_selKeyFrame = -1;
|
||||
emit keyframeSelectionChanged(m_selKeyKind, m_selKeyFrame);
|
||||
}
|
||||
update();
|
||||
}
|
||||
|
||||
int TimelineWidget::xToFrame(int x) const {
|
||||
const QRect r = trackRect();
|
||||
if (r.width() <= 1) return m_start;
|
||||
const double t = std::clamp((x - r.left()) / double(r.width() - 1), 0.0, 1.0);
|
||||
const int span = std::max(1, m_end - m_start);
|
||||
const int f = m_start + int(std::round(t * span));
|
||||
return clampFrame(f, m_start, m_end);
|
||||
QRect TimelineWidget::contentRect() const {
|
||||
return QRect(contentLeft(), 3, contentWidth(), std::max(24, height() - 6));
|
||||
}
|
||||
|
||||
QRect TimelineWidget::rulerRect() const {
|
||||
const QRect c = contentRect();
|
||||
return QRect(c.left(), c.top(), c.width(), kRulerHeight);
|
||||
}
|
||||
|
||||
QRect TimelineWidget::keyAreaRect() const {
|
||||
const QRect c = contentRect();
|
||||
const QRect r = rulerRect();
|
||||
const int top = r.bottom();
|
||||
return QRect(c.left(), top, c.width(), std::max(1, c.bottom() - top));
|
||||
}
|
||||
|
||||
double TimelineWidget::frameToXf(double frame) const {
|
||||
const double pxf = double(contentWidth()) / double(std::max(1, kEnd - kStart));
|
||||
return double(contentLeft()) + (frame - double(kStart)) * pxf;
|
||||
}
|
||||
|
||||
int TimelineWidget::frameToX(int frame) const {
|
||||
const QRect r = trackRect();
|
||||
if (r.width() <= 1) return r.left();
|
||||
const int f = clampFrame(frame, m_start, m_end);
|
||||
const int span = std::max(1, m_end - m_start);
|
||||
const double t = double(f - m_start) / double(span);
|
||||
return r.left() + int(std::round(t * (r.width() - 1)));
|
||||
return int(std::lround(frameToXf(double(frame))));
|
||||
}
|
||||
|
||||
double TimelineWidget::xToFramef(int x) const {
|
||||
const double pxf = double(contentWidth()) / double(std::max(1, kEnd - kStart));
|
||||
return double(kStart) + double(x - contentLeft()) / std::max(pxf, 1e-9);
|
||||
}
|
||||
|
||||
int TimelineWidget::xToFrame(int x) const {
|
||||
return int(std::lround(xToFramef(x)));
|
||||
}
|
||||
|
||||
void TimelineWidget::setFrameInternal(int frame, bool commit) {
|
||||
const int f = clampFrame(frame, m_start, m_end);
|
||||
if (m_currentFrame == f && !commit) {
|
||||
const int f = std::clamp(frame, kStart, kEnd - 1);
|
||||
// 松手时若帧未变:只发 committed,禁止再发 scrubbed,否则主窗口会双次求值/刷新导致帧号与红线闪烁
|
||||
if (m_currentFrame == f) {
|
||||
if (commit) {
|
||||
emit frameCommitted(f);
|
||||
}
|
||||
return;
|
||||
}
|
||||
m_currentFrame = f;
|
||||
@@ -127,64 +189,164 @@ void TimelineWidget::paintEvent(QPaintEvent*) {
|
||||
QPainter p(this);
|
||||
p.setRenderHint(QPainter::Antialiasing, true);
|
||||
|
||||
const QRect r = rect();
|
||||
p.fillRect(r, palette().base());
|
||||
p.fillRect(rect(), palette().base());
|
||||
|
||||
const QRect tr = trackRect().adjusted(0, 8, 0, -8);
|
||||
const QColor rail = palette().mid().color();
|
||||
p.setPen(Qt::NoPen);
|
||||
p.setBrush(rail);
|
||||
p.drawRoundedRect(tr, 6, 6);
|
||||
const QRect cr = contentRect();
|
||||
const QRect kr = keyAreaRect();
|
||||
const QRect rr = rulerRect();
|
||||
const double fLeft = double(kStart);
|
||||
const int visMin = kStart;
|
||||
const int visMax = kEnd;
|
||||
|
||||
// selection range
|
||||
auto frameVisible = [&](int fr) { return fr >= visMin && fr <= visMax; };
|
||||
|
||||
// —— 区间:贯穿标尺+轨道,贴在时间坐标上 ——
|
||||
if (m_selStart >= 0 && m_selEnd >= 0) {
|
||||
const int x0 = frameToX(m_selStart);
|
||||
const int x1 = frameToX(m_selEnd);
|
||||
QRect sel(QPoint(std::min(x0, x1), tr.top()), QPoint(std::max(x0, x1), tr.bottom()));
|
||||
sel = sel.adjusted(0, 2, 0, -2);
|
||||
QColor c = palette().highlight().color();
|
||||
c.setAlpha(50);
|
||||
p.setBrush(c);
|
||||
p.drawRoundedRect(sel, 4, 4);
|
||||
const int xa = std::min(x0, x1);
|
||||
const int xb = std::max(x0, x1);
|
||||
QRect sel(xa, cr.top(), xb - xa, cr.height());
|
||||
sel = sel.intersected(cr);
|
||||
if (sel.width() > 0) {
|
||||
QColor c = palette().highlight().color();
|
||||
c.setAlpha(72);
|
||||
p.setPen(Qt::NoPen);
|
||||
p.setBrush(c);
|
||||
p.drawRoundedRect(sel, 3, 3);
|
||||
p.setPen(QPen(palette().highlight().color(), 1));
|
||||
p.setBrush(Qt::NoBrush);
|
||||
p.drawRoundedRect(sel, 3, 3);
|
||||
}
|
||||
}
|
||||
|
||||
auto drawDots = [&](const QVector<int>& frames, const QColor& c, int y) {
|
||||
p.setBrush(c);
|
||||
p.setPen(Qt::NoPen);
|
||||
for (int f : frames) {
|
||||
if (f < m_start || f > m_end) continue;
|
||||
const int x = frameToX(f);
|
||||
const bool sel =
|
||||
(m_selKeyFrame == f)
|
||||
&& ((m_selKeyKind == KeyKind::Image && &frames == &m_imgFrames)
|
||||
|| (m_selKeyKind == KeyKind::Location && &frames == &m_locFrames)
|
||||
|| (m_selKeyKind == KeyKind::UserScale && &frames == &m_scaleFrames));
|
||||
if (sel) {
|
||||
p.setPen(QPen(palette().highlight().color(), 2.0));
|
||||
p.setBrush(c);
|
||||
p.drawEllipse(QPointF(x, y), 4.4, 4.4);
|
||||
p.setPen(Qt::NoPen);
|
||||
} else {
|
||||
p.drawEllipse(QPointF(x, y), 2.6, 2.6);
|
||||
}
|
||||
// —— 关键帧切分背景(仅轨道):按可见范围切分,不因 m_end 隐藏尾部关键帧 ——
|
||||
QVector<int> allK;
|
||||
allK.reserve(m_locFrames.size() + m_scaleFrames.size() + m_imgFrames.size() + m_visFrames.size());
|
||||
allK += m_locFrames;
|
||||
allK += m_scaleFrames;
|
||||
allK += m_imgFrames;
|
||||
allK += m_visFrames;
|
||||
std::sort(allK.begin(), allK.end());
|
||||
allK.erase(std::unique(allK.begin(), allK.end()), allK.end());
|
||||
|
||||
const int v0 = visMin;
|
||||
const int v1 = visMax;
|
||||
QVector<int> cuts;
|
||||
cuts.reserve(allK.size() + 4);
|
||||
cuts.push_back(v0);
|
||||
for (int k : allK) {
|
||||
if (k > v0 && k < v1) {
|
||||
cuts.push_back(k);
|
||||
}
|
||||
}
|
||||
cuts.push_back(v1);
|
||||
std::sort(cuts.begin(), cuts.end());
|
||||
cuts.erase(std::unique(cuts.begin(), cuts.end()), cuts.end());
|
||||
|
||||
for (int i = 0; i + 1 < cuts.size(); ++i) {
|
||||
const int a = cuts[i];
|
||||
const int b = cuts[i + 1];
|
||||
if (a >= b) continue;
|
||||
const int x0 = frameToX(a);
|
||||
const int x1 = frameToX(b);
|
||||
if (x1 < kr.left() || x0 > kr.right()) continue;
|
||||
QRect seg(std::max(x0, kr.left()), kr.top(), std::min(x1, kr.right()) - std::max(x0, kr.left()), kr.height());
|
||||
if (seg.width() <= 0) continue;
|
||||
QColor c = (i % 2) ? QColor(255, 255, 255, 28) : QColor(0, 0, 0, 12);
|
||||
p.setPen(Qt::NoPen);
|
||||
p.setBrush(c);
|
||||
p.drawRect(seg);
|
||||
}
|
||||
|
||||
const QColor rail = palette().mid().color();
|
||||
p.setPen(Qt::NoPen);
|
||||
p.setBrush(rail);
|
||||
p.drawRoundedRect(kr, 4, 4);
|
||||
|
||||
// —— 关键帧:竖线贯穿标尺+轨道(位置钉在帧坐标上,随平移/缩放移动)——
|
||||
auto drawKeyLine = [&](int fr, const QColor& col, KeyKind kind, int xOffPx) {
|
||||
if (!frameVisible(fr)) return;
|
||||
const int x = frameToX(fr) + xOffPx;
|
||||
if (x < cr.left() - 4 || x > cr.right() + 4) return;
|
||||
const bool sel = (m_selKeyFrame == fr && m_selKeyKind == kind);
|
||||
QPen pen(col, sel ? 3.2 : 2.0);
|
||||
pen.setCapStyle(Qt::FlatCap);
|
||||
p.setPen(pen);
|
||||
p.drawLine(x, cr.top() + 1, x, cr.bottom() - 1);
|
||||
const int yb = kr.bottom() - 4;
|
||||
QPolygonF dia;
|
||||
dia << QPointF(x, yb - 5) << QPointF(x + 4, yb) << QPointF(x, yb + 4) << QPointF(x - 4, yb);
|
||||
p.setPen(Qt::NoPen);
|
||||
p.setBrush(col);
|
||||
p.drawPolygon(dia);
|
||||
};
|
||||
|
||||
const int yMid = tr.center().y();
|
||||
drawDots(m_imgFrames, QColor(80, 160, 255, 230), yMid - 6);
|
||||
drawDots(m_locFrames, QColor(255, 120, 0, 230), yMid);
|
||||
drawDots(m_scaleFrames, QColor(140, 220, 140, 230), yMid + 6);
|
||||
for (int fr : m_imgFrames) drawKeyLine(fr, QColor(70, 130, 240), KeyKind::Image, -3);
|
||||
for (int fr : m_locFrames) drawKeyLine(fr, QColor(240, 110, 40), KeyKind::Location, -1);
|
||||
for (int fr : m_scaleFrames) drawKeyLine(fr, QColor(80, 190, 90), KeyKind::UserScale, 1);
|
||||
for (int fr : m_visFrames) drawKeyLine(fr, QColor(160, 100, 230), KeyKind::Visibility, 3);
|
||||
|
||||
// current frame caret
|
||||
// —— 标尺底与刻度文字(与帧一一对应,随 view 滚动)——
|
||||
p.setPen(Qt::NoPen);
|
||||
p.setBrush(palette().alternateBase());
|
||||
p.drawRoundedRect(rr, 3, 3);
|
||||
|
||||
const double pxf = double(contentWidth()) / double(std::max(1, kEnd - kStart));
|
||||
const int major = pickMajorStep(pxf);
|
||||
const int minor = pickMinorStep(major);
|
||||
QPen minorPen(QColor(60, 60, 60, 100));
|
||||
minorPen.setWidth(1);
|
||||
QPen majorPen(QColor(35, 35, 35, 170));
|
||||
majorPen.setWidth(1);
|
||||
QFont rulerFont = font();
|
||||
rulerFont.setPointSize(std::max(7, rulerFont.pointSize() - 1));
|
||||
p.setFont(rulerFont);
|
||||
|
||||
for (int f = static_cast<int>(std::floor(fLeft / minor) * minor); f <= visMax + minor; f += minor) {
|
||||
if (f < visMin - minor) continue;
|
||||
const double xf = frameToXf(f);
|
||||
if (xf < rr.left() - 2 || xf > rr.right() + 2) continue;
|
||||
const int xi = int(std::lround(xf));
|
||||
const bool isMajor = (major > 0) && (f % major == 0);
|
||||
p.setPen(isMajor ? majorPen : minorPen);
|
||||
const int tickH = isMajor ? 8 : 4;
|
||||
p.drawLine(xi, rr.bottom() - tickH, xi, rr.bottom());
|
||||
if (isMajor && pxf > 0.28) {
|
||||
p.setPen(QColor(25, 25, 25, 235));
|
||||
const QString txt = QString::number(f);
|
||||
const int tw = QFontMetrics(rulerFont).horizontalAdvance(txt);
|
||||
int tx = xi - tw / 2;
|
||||
tx = std::clamp(tx, rr.left() + 1, rr.right() - tw - 1);
|
||||
p.drawText(tx, rr.top() + QFontMetrics(rulerFont).ascent() + 1, txt);
|
||||
}
|
||||
}
|
||||
|
||||
// 播放头 + 帧号(钉在轴上,与红线同一 x)
|
||||
const int cx = frameToX(m_currentFrame);
|
||||
p.setPen(QPen(palette().highlight().color(), 2.0));
|
||||
p.drawLine(QPoint(cx, tr.top() - 6), QPoint(cx, tr.bottom() + 6));
|
||||
}
|
||||
p.setPen(QPen(QColor(220, 55, 55, 250), 2.0));
|
||||
p.drawLine(QPoint(cx, cr.top()), QPoint(cx, cr.bottom()));
|
||||
|
||||
static bool hitDot(const QPoint& pos, int dotX, int dotY, int radiusPx) {
|
||||
const int dx = pos.x() - dotX;
|
||||
const int dy = pos.y() - dotY;
|
||||
return (dx * dx + dy * dy) <= (radiusPx * radiusPx);
|
||||
QFont bf = font();
|
||||
bf.setBold(true);
|
||||
p.setFont(bf);
|
||||
QFontMetrics fm(bf);
|
||||
const QString ft = QString::number(m_currentFrame);
|
||||
const int tw = fm.horizontalAdvance(ft);
|
||||
const int ph = fm.height() + 3;
|
||||
const int pw = tw + 10;
|
||||
int px = cx - pw / 2;
|
||||
if (pw <= cr.width() - 4) {
|
||||
px = std::clamp(px, cr.left() + 2, cr.right() - pw - 2);
|
||||
} else {
|
||||
px = cr.left() + 2;
|
||||
}
|
||||
const int py = cr.top() + 1;
|
||||
p.setPen(QPen(QColor(180, 40, 40), 1));
|
||||
p.setBrush(QColor(255, 245, 245, 245));
|
||||
p.drawRoundedRect(px, py, pw, ph, 3, 3);
|
||||
p.setPen(QColor(20, 20, 20));
|
||||
p.drawText(px + 5, py + fm.ascent() + 1, ft);
|
||||
}
|
||||
|
||||
static int findNearestFrameInTrack(const QVector<int>& frames, int frame) {
|
||||
@@ -241,44 +403,36 @@ void TimelineWidget::mouseReleaseEvent(QMouseEvent* e) {
|
||||
const int f = xToFrame(e->pos().x());
|
||||
setFrameInternal(f, true);
|
||||
|
||||
// 点击(非拖拽)时做选中:关键帧或区间
|
||||
if (!m_moved) {
|
||||
const QRect tr = trackRect().adjusted(0, 8, 0, -8);
|
||||
const int yMid = tr.center().y();
|
||||
const int yImg = yMid - 6;
|
||||
const int yLoc = yMid;
|
||||
const int ySc = yMid + 6;
|
||||
const int rad = 7;
|
||||
const QRect cr = contentRect();
|
||||
const int mx = e->pos().x();
|
||||
const int my = e->pos().y();
|
||||
|
||||
auto trySelectKey = [&](KeyKind kind, const QVector<int>& frames, int laneY) -> bool {
|
||||
auto trySelectKey = [&](KeyKind kind, const QVector<int>& frames, int xOff) -> bool {
|
||||
const int nearest = findNearestFrameInTrack(frames, f);
|
||||
if (nearest < 0) return false;
|
||||
const int x = frameToX(nearest);
|
||||
if (hitDot(e->pos(), x, laneY, rad)) {
|
||||
m_selKeyKind = kind;
|
||||
m_selKeyFrame = nearest;
|
||||
emit keyframeSelectionChanged(m_selKeyKind, m_selKeyFrame);
|
||||
update();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
const int xk = frameToX(nearest) + xOff;
|
||||
if (std::abs(mx - xk) > 9) return false;
|
||||
if (my < cr.top() || my > cr.bottom()) return false;
|
||||
m_selKeyKind = kind;
|
||||
m_selKeyFrame = nearest;
|
||||
emit keyframeSelectionChanged(m_selKeyKind, m_selKeyFrame);
|
||||
update();
|
||||
return true;
|
||||
};
|
||||
|
||||
// 先尝试命中关键帧(按 lane 优先)
|
||||
if (trySelectKey(KeyKind::Image, m_imgFrames, yImg)
|
||||
|| trySelectKey(KeyKind::Location, m_locFrames, yLoc)
|
||||
|| trySelectKey(KeyKind::UserScale, m_scaleFrames, ySc)) {
|
||||
// 选中关键帧时清掉区间
|
||||
if (trySelectKey(KeyKind::Visibility, m_visFrames, 3) || trySelectKey(KeyKind::Image, m_imgFrames, -3)
|
||||
|| trySelectKey(KeyKind::Location, m_locFrames, -1) || trySelectKey(KeyKind::UserScale, m_scaleFrames, 1)) {
|
||||
if (m_selStart >= 0 && m_selEnd >= 0) {
|
||||
m_selStart = -1;
|
||||
m_selEnd = -1;
|
||||
emit intervalSelectionChanged(m_selStart, m_selEnd);
|
||||
}
|
||||
} else {
|
||||
// 未命中关键帧:尝试选中由关键帧切分出的区间(使用三轨道的并集)
|
||||
QVector<int> all = m_locFrames;
|
||||
all += m_scaleFrames;
|
||||
all += m_imgFrames;
|
||||
all += m_visFrames;
|
||||
std::sort(all.begin(), all.end());
|
||||
all.erase(std::unique(all.begin(), all.end()), all.end());
|
||||
int a = -1, b = -1;
|
||||
@@ -286,7 +440,6 @@ void TimelineWidget::mouseReleaseEvent(QMouseEvent* e) {
|
||||
if (a >= 0 && b >= 0) {
|
||||
setSelectionRange(a, b);
|
||||
emit intervalSelectionChanged(m_selStart, m_selEnd);
|
||||
// 选中区间时清掉关键帧选中
|
||||
if (m_selKeyKind != KeyKind::None) {
|
||||
m_selKeyKind = KeyKind::None;
|
||||
m_selKeyFrame = -1;
|
||||
@@ -303,8 +456,9 @@ void TimelineWidget::mouseReleaseEvent(QMouseEvent* e) {
|
||||
}
|
||||
|
||||
void TimelineWidget::wheelEvent(QWheelEvent* e) {
|
||||
const int delta = (e->angleDelta().y() > 0) ? 1 : -1;
|
||||
setFrameInternal(m_currentFrame + delta, true);
|
||||
const double steps = e->angleDelta().y() / 120.0;
|
||||
const int delta = (steps > 0) ? 1 : -1;
|
||||
// 滚轮只走 scrubbed:避免每格同时 scrub+committed 造成双次 refresh 与帧号闪烁
|
||||
setFrameInternal(m_currentFrame + delta, false);
|
||||
e->accept();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,33 +1,41 @@
|
||||
#pragma once
|
||||
|
||||
#include "core/domain/Project.h"
|
||||
|
||||
#include <QWidget>
|
||||
|
||||
class QResizeEvent;
|
||||
|
||||
class TimelineWidget final : public QWidget {
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit TimelineWidget(QWidget* parent = nullptr);
|
||||
|
||||
// 兼容旧接口:NLA/片段系统下时间轴始终固定为 0..600(local frame)。
|
||||
void setFrameRange(int start, int end);
|
||||
void setCurrentFrame(int frame);
|
||||
/// 由主窗口同步工程帧时调用:不发射 frameScrubbed,避免与拖动/刷新打架造成数字闪烁
|
||||
void setCurrentFrameProgrammatic(int frame);
|
||||
int currentFrame() const { return m_currentFrame; }
|
||||
|
||||
void setSelectionRange(int start, int end); // -1,-1 清除
|
||||
int selectionStart() const { return m_selStart; }
|
||||
int selectionEnd() const { return m_selEnd; }
|
||||
|
||||
// 只显示“当前选中实体”的关键帧标记
|
||||
void setKeyframeTracks(const core::Project::Entity* entityOrNull);
|
||||
// 轨道数据直接由上层提供(通常来自当前条带引用的 clip)。
|
||||
void setKeyframeTracks(const QVector<int>& locFrames,
|
||||
const QVector<int>& scaleFrames,
|
||||
const QVector<int>& imgFrames,
|
||||
const QVector<int>& visFrames);
|
||||
void setToolKeyframeTracks(const QVector<int>& locFrames,
|
||||
const QVector<int>& visFrames);
|
||||
|
||||
enum class KeyKind { None, Location, UserScale, Image };
|
||||
enum class KeyKind { None, Location, UserScale, Image, Visibility };
|
||||
KeyKind selectedKeyKind() const { return m_selKeyKind; }
|
||||
int selectedKeyFrame() const { return m_selKeyFrame; }
|
||||
bool hasSelectedKeyframe() const { return m_selKeyKind != KeyKind::None && m_selKeyFrame >= 0; }
|
||||
|
||||
signals:
|
||||
void frameScrubbed(int frame); // 拖动中实时触发(用于实时预览)
|
||||
void frameCommitted(int frame); // 松手/点击确认(用于较重的刷新)
|
||||
void frameScrubbed(int frame);
|
||||
void frameCommitted(int frame);
|
||||
void contextMenuRequested(const QPoint& globalPos, int frame);
|
||||
void keyframeSelectionChanged(KeyKind kind, int frame);
|
||||
void intervalSelectionChanged(int start, int end);
|
||||
@@ -38,18 +46,26 @@ protected:
|
||||
void mouseMoveEvent(QMouseEvent*) override;
|
||||
void mouseReleaseEvent(QMouseEvent*) override;
|
||||
void wheelEvent(QWheelEvent*) override;
|
||||
void resizeEvent(QResizeEvent* e) override;
|
||||
|
||||
private:
|
||||
int xToFrame(int x) const;
|
||||
int contentLeft() const { return 6; }
|
||||
int contentWidth() const { return std::max(1, width() - 12); }
|
||||
QRect contentRect() const;
|
||||
|
||||
double frameToXf(double frame) const;
|
||||
int frameToX(int frame) const;
|
||||
QRect trackRect() const;
|
||||
double xToFramef(int x) const;
|
||||
int xToFrame(int x) const;
|
||||
|
||||
QRect rulerRect() const;
|
||||
QRect keyAreaRect() const;
|
||||
|
||||
void setFrameInternal(int frame, bool commit);
|
||||
|
||||
private:
|
||||
int m_start = 0;
|
||||
int m_end = 600;
|
||||
int m_currentFrame = 0;
|
||||
static constexpr int kStart = 0;
|
||||
static constexpr int kEnd = 600; // exclusive for mapping, inclusive for UI labels
|
||||
int m_currentFrame = 0; // local frame: 0..599
|
||||
|
||||
int m_selStart = -1;
|
||||
int m_selEnd = -1;
|
||||
@@ -58,12 +74,13 @@ private:
|
||||
QPoint m_pressPos;
|
||||
bool m_moved = false;
|
||||
|
||||
// snapshot(避免频繁遍历 workspace)
|
||||
static constexpr int kRulerHeight = 14;
|
||||
|
||||
QVector<int> m_locFrames;
|
||||
QVector<int> m_scaleFrames;
|
||||
QVector<int> m_imgFrames;
|
||||
QVector<int> m_visFrames;
|
||||
|
||||
KeyKind m_selKeyKind = KeyKind::None;
|
||||
int m_selKeyFrame = -1;
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user