增加预览页介绍信息显示

This commit is contained in:
2026-04-08 09:56:25 +08:00
parent f53fee8e5a
commit 028ed1b18d
17 changed files with 1059 additions and 23 deletions

View File

@@ -13,6 +13,7 @@
#include <QPaintEvent>
#include <QPainterPath>
#include <QWheelEvent>
#include <QKeyEvent>
namespace {
@@ -163,6 +164,16 @@ double depthToScale01(int depthZ) {
return static_cast<double>(d) / 255.0;
}
// depth01 0..1 -> 原始距离乘子 0.5..1.5calibMult>0 时除以创建时记录的基准,使「原位置」为 1.0
double distanceScaleFromDepth01(double depth01, double calibMult) {
const double d = std::clamp(depth01, 0.0, 1.0);
const double raw = 0.5 + d * 1.0;
if (calibMult > 0.0) {
return raw / std::max(calibMult, 1e-6);
}
return raw;
}
QImage extractEntityImage(const QImage& bg, const QVector<QPointF>& polyWorld, QPointF& outTopLeftWorld) {
if (bg.isNull() || polyWorld.size() < 3) {
outTopLeftWorld = {};
@@ -246,6 +257,15 @@ EditorCanvas::EditorCanvas(QWidget* parent)
setMouseTracking(true);
m_previewEmitTimer.start();
m_presZoomTimer = new QTimer(this);
m_presZoomTimer->setInterval(16);
connect(m_presZoomTimer, &QTimer::timeout, this, &EditorCanvas::tickPresentationZoomAnimation);
m_presHoverTimer = new QTimer(this);
m_presHoverTimer->setInterval(40);
connect(m_presHoverTimer, &QTimer::timeout, this, &EditorCanvas::tickPresentationHoverAnimation);
updateCursor();
}
@@ -254,6 +274,19 @@ void EditorCanvas::setPresentationPreviewMode(bool on) {
return;
}
m_presentationPreviewMode = on;
if (m_presZoomTimer) {
m_presZoomTimer->stop();
}
if (m_presHoverTimer) {
m_presHoverTimer->stop();
}
m_presHoverEntityIndex = -1;
m_presFocusedEntityIndex = -1;
m_presHoverPhase = 0.0;
m_presZoomAnimT = 0.0;
m_presZoomFinishingRestore = false;
m_presBgPanSession = false;
m_presBgDragDist = 0.0;
if (on) {
m_tool = Tool::Move;
m_selectedEntity = -1;
@@ -292,6 +325,7 @@ void EditorCanvas::setEntities(const QVector<core::Project::Entity>& entities, c
core::sampleLocation(e.locationKeys, m_currentFrame, e.originWorld, KeyInterpolation::Linear);
v.animatedOriginWorld = originWorld;
v.cutoutPolygonWorld = e.cutoutPolygonWorld;
v.distanceScaleCalibMult = e.distanceScaleCalibMult;
// 逐帧自动算 z使用实体多边形质心作为锚点采样深度O(1)),避免卡顿
QVector<QPointF> polyTmp;
@@ -308,7 +342,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 = 0.5 + ds01 * 1.0; // 0..1 -> 0.5..1.5(深度距离)
const double distScale = distanceScaleFromDepth01(ds01, e.distanceScaleCalibMult);
const double scale = distScale * v.userScale;
v.visualScale = scale;
@@ -412,8 +446,8 @@ double EditorCanvas::selectedDistanceScaleMultiplier() const {
if (m_selectedEntity < 0 || m_selectedEntity >= m_entities.size()) {
return 1.0;
}
const double ds01 = std::clamp(m_entities[m_selectedEntity].animatedDepthScale01, 0.0, 1.0);
return 0.5 + ds01 * 1.0;
const auto& ent = m_entities[m_selectedEntity];
return distanceScaleFromDepth01(ent.animatedDepthScale01, ent.distanceScaleCalibMult);
}
double EditorCanvas::selectedUserScale() const {
@@ -427,6 +461,95 @@ double EditorCanvas::selectedCombinedScale() const {
return selectedDistanceScaleMultiplier() * selectedUserScale();
}
void EditorCanvas::tickPresentationHoverAnimation() {
if (!m_presentationPreviewMode) {
return;
}
m_presHoverPhase += 0.35;
if (m_presHoverPhase > 6.28318530718) {
m_presHoverPhase -= 6.28318530718;
}
update();
}
void EditorCanvas::tickPresentationZoomAnimation() {
m_presZoomAnimT += 0.16;
qreal u = std::min(1.0, static_cast<qreal>(m_presZoomAnimT));
u = 1.0 - std::pow(1.0 - u, 3.0);
m_pan = m_presZoomFromPan + (m_presZoomToPan - m_presZoomFromPan) * u;
m_scale = m_presZoomFromScale + (m_presZoomToScale - m_presZoomFromScale) * u;
if (m_presZoomAnimT >= 1.0) {
m_presZoomTimer->stop();
m_pan = m_presZoomToPan;
m_scale = m_presZoomToScale;
if (m_presZoomFinishingRestore) {
m_presFocusedEntityIndex = -1;
m_presZoomFinishingRestore = false;
}
}
update();
}
void EditorCanvas::presentationComputeZoomTarget(int entityIndex, QPointF* outPan, qreal* outScale) const {
if (!outPan || !outScale || entityIndex < 0 || entityIndex >= m_entities.size()) {
return;
}
const Entity& ent = m_entities[entityIndex];
QRectF bb;
if (!ent.image.isNull()) {
const QSizeF sz(ent.image.width() * ent.visualScale, ent.image.height() * ent.visualScale);
bb = QRectF(ent.imageTopLeft, sz);
} else if (!ent.polygonWorld.isEmpty()) {
bb = pathFromWorldPolygon(ent.polygonWorld).boundingRect();
} else {
bb = ent.rect;
}
const QPointF c = bb.center();
const qreal rw = std::max(1.0, bb.width());
const qreal rh = std::max(1.0, bb.height());
qreal s = std::min(static_cast<qreal>(width()) / (rw * 1.28), static_cast<qreal>(height()) / (rh * 1.28));
s = std::clamp(s, 0.12, 14.0);
*outScale = s;
*outPan = QPointF(width() / 2.0, height() / 2.0) - c * s;
}
void EditorCanvas::beginPresentationZoomTowardEntity(int entityIndex) {
if (entityIndex < 0 || entityIndex >= m_entities.size()) {
return;
}
if (m_presFocusedEntityIndex < 0) {
m_presRestorePan = m_pan;
m_presRestoreScale = m_scale;
}
m_presFocusedEntityIndex = entityIndex;
m_presZoomFromPan = m_pan;
m_presZoomFromScale = m_scale;
presentationComputeZoomTarget(entityIndex, &m_presZoomToPan, &m_presZoomToScale);
m_presZoomAnimT = 0.0;
m_presZoomFinishingRestore = false;
m_presZoomTimer->start();
}
void EditorCanvas::beginPresentationZoomRestore() {
m_presZoomFromPan = m_pan;
m_presZoomFromScale = m_scale;
m_presZoomToPan = m_presRestorePan;
m_presZoomToScale = m_presRestoreScale;
m_presZoomAnimT = 0.0;
m_presZoomFinishingRestore = true;
m_presZoomTimer->start();
}
void EditorCanvas::clearPresentationEntityFocus() {
emit presentationInteractionDismissed();
if (m_presZoomFinishingRestore) {
return;
}
if (m_presFocusedEntityIndex >= 0) {
beginPresentationZoomRestore();
}
}
void EditorCanvas::clearEntitySelection() {
if (m_selectedEntity < 0) {
return;
@@ -598,7 +721,11 @@ void EditorCanvas::ensurePixmapLoaded() const {
void EditorCanvas::updateCursor() {
if (m_presentationPreviewMode) {
setCursor(Qt::OpenHandCursor);
if (m_presHoverEntityIndex >= 0) {
setCursor(Qt::PointingHandCursor);
} else {
setCursor(Qt::OpenHandCursor);
}
return;
}
switch (m_tool) {
@@ -784,8 +911,18 @@ void EditorCanvas::paintEvent(QPaintEvent* e) {
p.drawImage(target, ent.image);
p.restore();
} else {
const QSizeF sz(ent.image.width() * ent.visualScale, ent.image.height() * ent.visualScale);
const QRectF target(ent.imageTopLeft, sz);
const qreal pop =
(m_presentationPreviewMode && i == m_presFocusedEntityIndex) ? 1.1 : 1.0;
const QSizeF sz0(ent.image.width() * ent.visualScale, ent.image.height() * ent.visualScale);
QRectF target;
if (pop > 1.001) {
const QRectF orig(ent.imageTopLeft, sz0);
const QPointF cen = orig.center();
const QSizeF sz = orig.size() * pop;
target = QRectF(QPointF(cen.x() - sz.width() * 0.5, cen.y() - sz.height() * 0.5), sz);
} else {
target = QRectF(ent.imageTopLeft, sz0);
}
p.drawImage(target, ent.image);
}
} else {
@@ -853,6 +990,33 @@ void EditorCanvas::paintEvent(QPaintEvent* e) {
p.drawRect(ent.rect.adjusted(-2, -2, 2, 2));
}
}
if (m_presentationPreviewMode && !ent.hiddenInEditMode) {
const bool showHover = (i == m_presHoverEntityIndex);
const bool showFocus = (i == m_presFocusedEntityIndex);
if (showHover || showFocus) {
p.setBrush(Qt::NoBrush);
if (showHover) {
const qreal pulse = 0.45 + 0.55 * std::sin(static_cast<double>(m_presHoverPhase));
const qreal lw =
(2.0 + 2.8 * pulse) / std::max(static_cast<qreal>(m_scale), static_cast<qreal>(0.001));
p.setPen(QPen(QColor(255, 210, 80, static_cast<int>(65 + 110 * pulse)), lw));
if (!ent.pathWorld.isEmpty()) {
p.drawPath(ent.pathWorld);
} else {
p.drawRect(ent.rect);
}
}
if (showFocus) {
const qreal lw = 2.8 / std::max(static_cast<qreal>(m_scale), static_cast<qreal>(0.001));
p.setPen(QPen(QColor(255, 120, 40, 230), lw));
if (!ent.pathWorld.isEmpty()) {
p.drawPath(ent.pathWorld);
} else {
p.drawRect(ent.rect);
}
}
}
}
}
// 创建实体手绘轨迹预览world 坐标)
@@ -986,8 +1150,27 @@ void EditorCanvas::mousePressEvent(QMouseEvent* e) {
emit hoveredWorldPosDepthChanged(wp0, z0);
if (m_presentationPreviewMode) {
if (e->button() == Qt::LeftButton || e->button() == Qt::MiddleButton) {
if (e->button() == Qt::LeftButton) {
const int hit = hitTestEntity(wp0);
if (hit >= 0) {
const auto& ent = m_entities[hit];
const QPointF cWorld =
ent.polygonWorld.isEmpty() ? ent.rect.center() : polygonCentroid(ent.polygonWorld);
const QPointF anchorView = worldToView(cWorld);
beginPresentationZoomTowardEntity(hit);
emit presentationEntityIntroRequested(ent.id, anchorView);
return;
}
m_dragging = true;
m_presBgPanSession = true;
m_presBgDragDist = 0.0;
m_lastMouseView = e->position();
setCursor(Qt::ClosedHandCursor);
return;
}
if (e->button() == Qt::MiddleButton) {
m_dragging = true;
m_presBgPanSession = false;
m_lastMouseView = e->position();
setCursor(Qt::ClosedHandCursor);
}
@@ -1117,6 +1300,23 @@ void EditorCanvas::mouseMoveEvent(QMouseEvent* e) {
}
emit hoveredWorldPosDepthChanged(wp, z);
if (m_presentationPreviewMode) {
const int h = hitTestEntity(wp);
if (h != m_presHoverEntityIndex) {
m_presHoverEntityIndex = h;
updateCursor();
update();
}
if (h >= 0) {
if (m_presHoverTimer && !m_presHoverTimer->isActive()) {
m_presHoverTimer->start();
}
} else if (m_presHoverTimer) {
m_presHoverTimer->stop();
m_presHoverPhase = 0.0;
}
}
if (!m_dragging) {
QWidget::mouseMoveEvent(e);
return;
@@ -1198,7 +1398,8 @@ void EditorCanvas::mouseMoveEvent(QMouseEvent* e) {
const double ds01 = depthToScale01(depthZ);
ent.animatedDepthScale01 = ds01;
const double newScale = (0.5 + ds01 * 1.0) * ent.userScale;
const double newScale =
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);
@@ -1218,6 +1419,9 @@ void EditorCanvas::mouseMoveEvent(QMouseEvent* e) {
// 平移画布
if (m_tool == Tool::Move || (e->buttons() & Qt::MiddleButton) ||
(m_presentationPreviewMode && (e->buttons() & Qt::LeftButton))) {
if (m_presentationPreviewMode && m_presBgPanSession) {
m_presBgDragDist += std::abs(deltaView.x()) + std::abs(deltaView.y());
}
m_pan += deltaView;
update();
return;
@@ -1226,6 +1430,13 @@ void EditorCanvas::mouseMoveEvent(QMouseEvent* e) {
void EditorCanvas::mouseReleaseEvent(QMouseEvent* e) {
if (e->button() == Qt::LeftButton || e->button() == Qt::MiddleButton) {
if (m_presentationPreviewMode && e->button() == Qt::LeftButton) {
if (m_presBgPanSession && m_presBgDragDist < 8.0) {
clearPresentationEntityFocus();
}
m_presBgPanSession = false;
m_presBgDragDist = 0.0;
}
if (m_tool == Tool::CreateEntity && e->button() == Qt::LeftButton && m_drawingEntity) {
m_dragging = false;
m_drawingEntity = false;
@@ -1252,6 +1463,10 @@ void EditorCanvas::mouseReleaseEvent(QMouseEvent* e) {
}
const QPointF c = polygonCentroid(ent.cutoutPolygonWorld);
ent.depth = m_depthImage8.isNull() ? 0 : sampleDepthAtPoint(m_depthImage8, c);
{
const double ds01 = depthToScale01(ent.depth);
ent.distanceScaleCalibMult = 0.5 + ds01 * 1.0;
}
// 抠实体:从原背景抠出,背景对应区域填黑
QImage cutout;
@@ -1325,3 +1540,12 @@ void EditorCanvas::wheelEvent(QWheelEvent* e) {
e->accept();
}
void EditorCanvas::keyPressEvent(QKeyEvent* e) {
if (m_presentationPreviewMode && e->key() == Qt::Key_Escape) {
clearPresentationEntityFocus();
e->accept();
return;
}
QWidget::keyPressEvent(e);
}

View File

@@ -9,6 +9,9 @@
#include <QVector>
#include <QWidget>
#include <QElapsedTimer>
#include <QTimer>
class QKeyEvent;
class EditorCanvas final : public QWidget {
Q_OBJECT
@@ -51,6 +54,8 @@ public:
// 预览呈现:完整背景 + 全部实体(忽略显隐开关),隐藏编辑辅助元素,仅可平移/缩放查看
void setPresentationPreviewMode(bool on);
bool presentationPreviewMode() const { return m_presentationPreviewMode; }
/// 退出「点击实体放大」状态并平滑回到进入前的视图(预览模式)
void clearPresentationEntityFocus();
void setEntities(const QVector<core::Project::Entity>& entities, const QString& projectDirAbs);
void setCurrentFrame(int frame);
@@ -79,6 +84,10 @@ signals:
void requestMoveEntity(const QString& id, const QPointF& delta);
void entityDragActiveChanged(bool on);
void selectedEntityPreviewChanged(const QString& id, int depth, const QPointF& originWorld);
/// 预览模式下点击实体anchorView 为实体质心在视图中的位置,用于摆放介绍浮层
void presentationEntityIntroRequested(const QString& entityId, QPointF anchorViewPoint);
/// 预览模式下应关闭介绍层空白处轻点、Esc、开始还原缩放时由画布侧触发
void presentationInteractionDismissed();
protected:
void paintEvent(QPaintEvent* e) override;
@@ -87,6 +96,7 @@ protected:
void mouseMoveEvent(QMouseEvent* e) override;
void mouseReleaseEvent(QMouseEvent* e) override;
void wheelEvent(QWheelEvent* e) override;
void keyPressEvent(QKeyEvent* e) override;
private:
void ensurePixmapLoaded() const;
@@ -97,6 +107,12 @@ private:
QPointF worldToView(const QPointF& w) const;
QRectF worldRectOfBackground() const;
void tickPresentationZoomAnimation();
void tickPresentationHoverAnimation();
void beginPresentationZoomTowardEntity(int entityIndex);
void beginPresentationZoomRestore();
void presentationComputeZoomTarget(int entityIndex, QPointF* outPan, qreal* outScale) const;
private:
struct Entity {
QString id;
@@ -112,6 +128,7 @@ private:
QPointF imageTopLeft; // image 对应的 world 左上角
double visualScale = 1.0; // 实体在 world 坐标下的缩放(用于贴图绘制)
double userScale = 1.0; // 与深度距离缩放相乘
double distanceScaleCalibMult = 0.0; // 与 Project::Entity 一致0=未校准
QPointF animatedOriginWorld;
double animatedDepthScale01 = 0.5;
// 编辑模式下实体被设为隐藏时:不响应点选且不绘制,除非当前选中(便于树选隐藏实体)
@@ -175,5 +192,22 @@ private:
QVector<QPointF> m_strokeWorld;
int m_currentFrame = 0;
// —— 预览展示:实体悬停动效、点击聚焦缩放 ——
QTimer* m_presZoomTimer = nullptr;
QTimer* m_presHoverTimer = nullptr;
int m_presHoverEntityIndex = -1;
int m_presFocusedEntityIndex = -1;
qreal m_presHoverPhase = 0.0;
QPointF m_presRestorePan;
qreal m_presRestoreScale = 1.0;
QPointF m_presZoomFromPan;
QPointF m_presZoomToPan;
qreal m_presZoomFromScale = 1.0;
qreal m_presZoomToScale = 1.0;
qreal m_presZoomAnimT = 0.0;
bool m_presZoomFinishingRestore = false;
bool m_presBgPanSession = false;
qreal m_presBgDragDist = 0.0;
};