新增根据帧数控制可见性
This commit is contained in:
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user