Files
hfut-bishe/client/gui/editor/EditorCanvas.h
T
2026-05-14 22:12:40 +08:00

381 lines
17 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#pragma once
#include "core/domain/Project.h"
#include <atomic>
#include <QByteArray>
#include <QJsonArray>
#include <QPixmap>
#include <QPoint>
#include <QPointF>
#include <QHash>
#include <QImage>
#include <QPainterPath>
#include <QRect>
#include <QVector>
#include <QWidget>
#include <QElapsedTimer>
#include <QTimer>
class QKeyEvent;
class QDragEnterEvent;
class QDragMoveEvent;
class QDropEvent;
class EditorCanvas final : public QWidget {
Q_OBJECT
public:
enum class Tool { Move, Zoom, CreateEntity, AddHotspot, MoveHotspot };
Q_ENUM(Tool)
explicit EditorCanvas(QWidget* parent = nullptr);
void setBackgroundImagePath(const QString& absolutePath);
QString backgroundImagePath() const { return m_bgAbsPath; }
void setBackgroundVisible(bool on);
bool backgroundVisible() const { return m_backgroundVisible; }
void setDepthMapPath(const QString& absolutePath);
void setDepthOverlayEnabled(bool on);
bool depthOverlayEnabled() const { return m_depthOverlayEnabled; }
void setTool(Tool tool);
Tool tool() const { return m_tool; }
/// 创建实体的分割方式(在「创」工具被选中后再次单击弹出面板选择)。
enum class EntityCreateSegmentMode { Manual, Snap, Sam };
Q_ENUM(EntityCreateSegmentMode)
void setEntityCreateSegmentMode(EntityCreateSegmentMode m);
EntityCreateSegmentMode entityCreateSegmentMode() const { return m_entityCreateSegmentMode; }
/// 进入/更新「待确认」实体多边形(允许用户在画布上微调)。
void setPendingEntityPolygonWorld(const QVector<QPointF>& polyWorld);
void clearPendingEntityPolygon();
bool hasPendingEntityPolygon() const { return m_pendingPolyWorld.size() >= 3; }
void resetView();
void zoomToFit();
void setWorldAxesVisible(bool on);
bool worldAxesVisible() const { return m_worldAxesVisible; }
void setAxisLabelsVisible(bool on);
bool axisLabelsVisible() const { return m_axisLabelsVisible; }
void setGizmoLabelsVisible(bool on);
bool gizmoLabelsVisible() const { return m_gizmoLabelsVisible; }
void setGridVisible(bool on);
bool gridVisible() const { return m_gridVisible; }
void setCheckerboardVisible(bool on);
bool checkerboardVisible() const { return m_checkerboardVisible; }
// 预览呈现:完整背景 + 全部实体(忽略显隐开关),隐藏编辑辅助元素,仅可平移/缩放查看
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();
void setPresentationHotspots(const QVector<core::Project::PresentationHotspot>& hotspots);
void setPresentationHotspotEditorActive(bool on);
void setPresentationHotspotPlaybackTargetEnabled(bool on);
void setSelectedPresentationHotspotId(const QString& id);
QString selectedPresentationHotspotId() const { return m_selectedPresentationHotspotId; }
void clearPresentationHotspotSelection();
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 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; }
bool isDraggingEntity() const { return m_draggingEntity; }
void selectEntityById(const QString& id);
void clearEntitySelection();
void selectBlackholeByEntityId(const QString& entityId);
void clearBlackholeSelection();
bool startBlackholeCopyResolve(const QString& entityId);
void cancelBlackholeCopyResolve();
/// 背景图片文件内容被外部写盘更新(路径未变)时,强制重新加载缓存
void notifyBackgroundContentChanged();
/// 黑洞修复覆盖层(独立 PNG)更新,不修改 background 原图
void notifyBlackholeOverlaysChanged();
/// 当前帧求值后的变换原点(枢轴),与关键帧/父子偏移一致
QPointF selectedAnimatedOriginWorld() const;
/// 同 selectedAnimatedOriginWorld(语义名)
QPointF selectedEntityPivotWorld() const;
double selectedDepthScale01() const;
QPointF selectedEntityCentroidWorld() const;
double selectedDistanceScaleMultiplier() const;
double selectedUserScale() const;
double selectedCombinedScale() const;
enum class DragMode { None, Free, AxisX, AxisY };
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,
const QByteArray& overlayPng,
const QPointF& cropTopLeftWorld,
const QJsonArray& pointCoords,
const QJsonArray& pointLabels,
const QJsonArray& boxXyxy);
/// 待确认实体:用户按回车/点击空白处确认后触发(由主窗口弹窗命名并落盘)。
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);
/// 预览模式下点击实体:anchorView 为实体质心在视图中的位置,用于摆放介绍浮层
void presentationEntityIntroRequested(const QString& entityId, QPointF anchorViewPoint);
/// 预览模式下应关闭介绍层(空白处轻点、Esc、开始还原缩放时由画布侧触发)
void presentationInteractionDismissed();
/// 首张背景已可绘制,或整图同步路径已就绪(用于打开工程时关闭加载对话框)
void initialBackgroundLoadFinished();
void requestAddPresentationHotspot(const QPointF& centerWorld);
void requestAddPresentationHotspotForAnimation(const QPointF& centerWorld, const QString& animationId);
void presentationHotspotMoved(const QString& id, const QPointF& centerWorld);
void selectedPresentationHotspotChanged(const QString& id);
void hotspotPlayRequested(const QString& hotspotId);
protected:
void paintEvent(QPaintEvent* e) override;
void resizeEvent(QResizeEvent* e) override;
void mousePressEvent(QMouseEvent* e) override;
void mouseMoveEvent(QMouseEvent* e) override;
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;
void invalidatePixmap();
/// libvips + 视口分块:滚轮放大只拉高可见区域的解码分辨率(非整图降采样)
void invalidateViewportLod();
void ensureBackgroundViewport();
void tryEmitInitialBackgroundLoadFinished();
QRectF visibleWorldRectF() const;
int backgroundLogicalWidth() const;
int backgroundLogicalHeight() const;
void updateCursor();
QPointF viewToWorld(const QPointF& v) const;
QPointF worldToView(const QPointF& w) const;
QRectF worldRectOfBackground() const;
bool isPointNearPendingVertex(const QPointF& worldPos, int* outIndex) const;
bool pendingPolygonContains(const QPointF& worldPos) const;
int hitTestPresentationHotspot(const QPointF& worldPos) const;
/// 预览 none:热点内「播」触点命中则返回热点下标,否则 -1
int hitTestHotspotPlayControl(const QPointF& worldPos) const;
void tickPresentationZoomAnimation();
void tickPresentationHoverAnimation();
void beginPresentationZoomTowardEntity(int entityIndex);
void beginPresentationZoomRestore();
void presentationComputeZoomTarget(int entityIndex, QPointF* outPan, qreal* outScale) const;
void syncFullBackgroundImageFromDiskIfDirty();
void rebuildFullBackgroundCutoutIfDirty();
QVector<QVector<QPointF>> visibleBlackholeCutoutPolysWorld() const;
void applyBlackHolesWorldToCrop(QImage& cropPremul, const QRect& cropWorld) const;
/// 与画布黑洞一致:优先整图 cutout 缓存,否则区域解码后再填黑。
bool loadRgbCropForEntitySegmentStroke(const QRectF& polyBrWorld, QRect& outCropWorld, QImage& outCropRgb);
struct Entity {
QString id;
QRectF rect; // world 坐标(用于拖拽与约束)
QVector<QPointF> polygonWorld; // 非空则使用 polygon
QPainterPath pathWorld; // polygonWorld 对应的 world 路径(缓存,避免每帧重建)
QVector<QPointF> cutoutPolygonWorld;
QColor color;
// 实体独立信息:
int depth = 0; // 0..255,来自划分区域平均深度
QImage image; // 抠图后的实体图像(带透明)
QPointF imageTopLeft; // image 对应的 world 左上角
double visualScale = 1.0; // 实体在 world 坐标下的缩放(用于贴图绘制)
double userScale = 1.0; // 与深度距离缩放相乘
double distanceScaleCalibMult = 0.0; // 与 Project::Entity 一致;0=未校准
bool ignoreDistanceScale = false;
int priority = 1;
QPointF animatedOriginWorld;
double animatedDepthScale01 = 0.5;
double opacity = 1.0; // 0..1(由可见性轨道求值)
bool blackholeVisible = true;
QString blackholeOverlayPath;
QRect blackholeOverlayRect;
};
int hitTestEntity(const QPointF& worldPos) const;
private:
struct ToolView {
core::Project::Tool tool;
double opacity = 1.0; // 0..1
int depthZ = 0;
};
private:
QString m_projectDirAbs;
QHash<QString, QImage> m_blackholeOverlayImageCache;
QString m_bgAbsPath;
bool m_backgroundVisible = true;
mutable QSize m_bgLogicalSize; // 原图逻辑像素(与项目/实体世界坐标一致)
mutable bool m_useViewportLod = false;
mutable QImage m_bgViewportImage; // 仅覆盖 m_bgViewportCacheRect
mutable QRect m_bgViewportCacheRect;
mutable bool m_bgViewportDirty = true;
/// 记录上次视口纹理对应的「世界→屏幕」像素密度,滚轮缩放后需换更高分辨率纹理
mutable qreal m_vpCachedDensity = 0.0;
/// 递增以使进行中的后台解码结果作废(路径变更 / 缩放 / 平移需刷新纹理)
std::atomic<uint32_t> m_vpToken{0};
std::atomic<bool> m_vpDecodeInFlight{false};
/// 全图缩略解码成功后置位,下一帧强制区域解码以尽快变清晰
mutable bool m_vpPreferRegionDecode = false;
bool m_initialBgLoadFinishedEmitted = false;
QImage m_bhCopyPatch; // 黑洞「复制背景」预览:原图子区域
QPoint m_bhCopyPatchOrigin;
bool m_bhCopyPatchValid = false;
mutable QPixmap m_bgPixmap;
mutable bool m_pixmapDirty = true;
mutable QImage m_bgImage; // 原背景(用于抠图/填充)
mutable QImage m_bgImageCutout; // 抠图后的背景(黑洞区域填黑)
mutable bool m_bgImageDirty = true;
mutable bool m_bgCutoutDirty = true;
QString m_depthAbsPath;
mutable QImage m_depthImage8;
mutable bool m_depthDirty = true;
bool m_depthOverlayEnabled = false;
int m_depthOverlayAlpha = 110;
bool m_worldAxesVisible = true;
bool m_axisLabelsVisible = true;
bool m_gizmoLabelsVisible = true;
bool m_gridVisible = true;
bool m_checkerboardVisible = true;
bool m_presentationPreviewMode = false;
bool m_presentationHotspotEditorActive = false;
bool m_presentationHotspotPlaybackTargetEnabled = false;
QVector<core::Project::PresentationHotspot> m_presentationHotspots;
QString m_selectedPresentationHotspotId;
int m_draggingHotspotIndex = -1;
QPointF m_hotspotDragOffsetWorld;
Tool m_tool = Tool::Move;
EntityCreateSegmentMode m_entityCreateSegmentMode = EntityCreateSegmentMode::Manual;
qreal m_scale = 1.0;
QPointF m_pan; // world 原点对应的 view 坐标偏移(view = world*scale + pan
bool m_dragging = false;
bool m_draggingEntity = false;
bool m_drawingEntity = false;
QPointF m_lastMouseView;
// 拖动锚点(局部坐标,相对形心):解决拖动过程中距离缩放变化导致的“抖动/闪烁”
QPointF m_entityDragAnchorLocal;
QPointF m_entityDragStartCentroidWorld;
// 拖动性能优化:拖动过程中不逐点修改 polygonWorld,而是保留基准形状+增量参数,在 paint 时做变换预览
bool m_dragPreviewActive = false;
QVector<QPointF> m_dragPolyBase;
QPainterPath m_dragPathBase;
QPointF m_dragImageTopLeftBase;
QRectF m_dragRectBase;
QPointF m_dragOriginBase;
QPointF m_dragDelta; // 纯平移
QPointF m_dragCentroidBase;
double m_dragScaleBase = 1.0; // 拖动开始时的 visualScale
double m_dragScaleRatio = 1.0; // 相对 m_dragScaleBase 的缩放比(由深度重算驱动)
QElapsedTimer m_previewEmitTimer;
qint64 m_lastPreviewEmitMs = 0;
qint64 m_lastDepthScaleRecalcMs = 0;
int m_selectedEntity = -1;
QString m_selectedBlackholeEntityId;
bool m_blackholeCopyResolveActive = false;
QString m_blackholeCopyEntityId;
QRectF m_blackholeCopyHoleRect;
QRectF m_blackholeCopySourceRect;
bool m_blackholeCopyDragging = false;
QPointF m_blackholeCopyDragOffset;
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;
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;
int m_currentFrame = 0;
// —— 创建实体:待确认多边形(可微调)——
QVector<QPointF> m_pendingPolyWorld;
bool m_pendingDragging = false;
bool m_pendingDragWhole = false;
int m_pendingDragVertex = -1;
QPointF m_pendingLastMouseWorld;
// —— 预览展示:实体悬停动效、点击聚焦缩放 ——
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;
};