fix: apply character bubble anchors

This commit is contained in:
2026-05-30 13:44:37 +08:00
parent 37b43624a7
commit 636b0ff872
5 changed files with 107 additions and 9 deletions
+1 -1
View File
@@ -15,7 +15,7 @@
},
"bubble": {
"offsetX": 0,
"offsetY": -180
"offsetY": -475
},
"states": {
"idle": {
+17 -1
View File
@@ -14,6 +14,21 @@ struct CharacterState
QString nextState;
};
struct CharacterBase
{
int width = 320;
int height = 320;
double scale = 1.0;
double anchorX = 0.5;
double anchorY = 0.0;
};
struct CharacterBubble
{
double offsetX = 0.0;
double offsetY = -8.0;
};
class CharacterPackage
{
public:
@@ -25,9 +40,10 @@ public:
QString packagePath;
QString previewPath;
QString defaultState;
CharacterBase base;
CharacterBubble bubble;
QMap<QString, CharacterState> states;
bool hasState(const QString &stateName) const;
const CharacterState *state(const QString &stateName) const;
};
+59
View File
@@ -6,6 +6,7 @@
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonValue>
#include <QtGlobal>
namespace
{
@@ -21,6 +22,62 @@ QString requiredString(const QJsonObject &object, const QString &key)
return value.toString().trimmed();
}
int optionalPositiveInt(const QJsonObject &object, const QString &key, int fallback)
{
const int value = object.value(key).toInt(fallback);
return value > 0 ? value : fallback;
}
double optionalPositiveDouble(const QJsonObject &object, const QString &key, double fallback)
{
const double value = object.value(key).toDouble(fallback);
return value > 0.0 ? value : fallback;
}
double optionalNormalizedDouble(const QJsonObject &object, const QString &key, double fallback)
{
const double value = object.value(key).toDouble(fallback);
return qBound(0.0, value, 1.0);
}
double optionalDouble(const QJsonObject &object, const QString &key, double fallback)
{
return object.value(key).toDouble(fallback);
}
CharacterBase parseBase(const QJsonObject &root)
{
CharacterBase base;
const QJsonValue baseValue = root.value(QStringLiteral("base"));
if (!baseValue.isObject())
{
return base;
}
const QJsonObject baseObject = baseValue.toObject();
base.width = optionalPositiveInt(baseObject, QStringLiteral("width"), base.width);
base.height = optionalPositiveInt(baseObject, QStringLiteral("height"), base.height);
base.scale = optionalPositiveDouble(baseObject, QStringLiteral("scale"), base.scale);
base.anchorX = optionalNormalizedDouble(baseObject, QStringLiteral("anchorX"), base.anchorX);
base.anchorY = optionalNormalizedDouble(baseObject, QStringLiteral("anchorY"), base.anchorY);
return base;
}
CharacterBubble parseBubble(const QJsonObject &root)
{
CharacterBubble bubble;
const QJsonValue bubbleValue = root.value(QStringLiteral("bubble"));
if (!bubbleValue.isObject())
{
return bubble;
}
const QJsonObject bubbleObject = bubbleValue.toObject();
bubble.offsetX = optionalDouble(bubbleObject, QStringLiteral("offsetX"), bubble.offsetX);
bubble.offsetY = optionalDouble(bubbleObject, QStringLiteral("offsetY"), bubble.offsetY);
return bubble;
}
}
CharacterPackage CharacterPackageLoader::load(const QString &packagePath, QString *errorMessage)
@@ -69,6 +126,8 @@ CharacterPackage CharacterPackageLoader::load(const QString &packagePath, QStrin
package.author = requiredString(root, QStringLiteral("author"));
package.version = requiredString(root, QStringLiteral("version"));
package.defaultState = requiredString(root, QStringLiteral("defaultState"));
package.base = parseBase(root);
package.bubble = parseBubble(root);
if (package.id.isEmpty())
{
+2 -2
View File
@@ -15,7 +15,6 @@ namespace
constexpr int MinBubbleWidth = 120;
constexpr int MaxBubbleWidth = 420;
constexpr int MaxBubbleHeight = 220;
constexpr int BubbleOffsetY = 8;
constexpr int BubblePaddingWidth = 28;
constexpr int BubblePaddingHeight = 24;
constexpr int BubbleWidthSafetyMargin = 12;
@@ -98,6 +97,7 @@ void ChatBubble::showMessage(const QString &message, const QPoint &anchorPositio
setFixedSize(bubbleSize);
updatePosition();
show();
raise();
m_textEdit->verticalScrollBar()->setValue(
scrollToBottom ? m_textEdit->verticalScrollBar()->maximum() : 0);
@@ -223,5 +223,5 @@ QSize ChatBubble::preferredBubbleSize(const QString &message) const
void ChatBubble::updatePosition()
{
move(m_anchorPosition.x() - width() / 2, m_anchorPosition.y() - height() - BubbleOffsetY);
move(m_anchorPosition.x() - width() / 2, m_anchorPosition.y() - height());
}
+28 -5
View File
@@ -21,6 +21,7 @@
#include <QMenu>
#include <QMouseEvent>
#include <QPixmap>
#include <QPointF>
#include <QPointer>
#include <QRandomGenerator>
#include <QScreen>
@@ -46,10 +47,20 @@ QString previewImagePath()
constexpr int MaxUserMessageLength = 4000;
constexpr int ChatInputLowerOffsetY = 48;
constexpr int StreamBubbleUpdateIntervalMs = 80;
constexpr int BaseAnimationTargetSize = 320;
constexpr int MinAnimationTargetSide = 32;
constexpr int MaxAnimationTargetSide = 2048;
constexpr int LowPowerFpsCap = 6;
constexpr int ChatFinishedReturnDelayMs = 1500;
int boundedAnimationTargetSide(double sideLength)
{
const double boundedSideLength = qBound(
static_cast<double>(MinAnimationTargetSide),
sideLength,
static_cast<double>(MaxAnimationTargetSide));
return qRound(boundedSideLength);
}
int evenBoundedHistoryLimit(int value, int minimum, int maximum)
{
const int boundedValue = qBound(minimum, value, maximum);
@@ -801,7 +812,17 @@ void PetWindow::updateBubblePosition()
QPoint PetWindow::bubbleAnchorPosition() const
{
return frameGeometry().topLeft() + QPoint(width() / 2, 0);
const CharacterBase &base = m_characterPackage.base;
const CharacterBubble &bubble = m_characterPackage.bubble;
const double baseWidth = base.width > 0 ? static_cast<double>(base.width) : static_cast<double>(width());
const double baseHeight = base.height > 0 ? static_cast<double>(base.height) : static_cast<double>(height());
const double scaleX = baseWidth > 0.0 ? static_cast<double>(width()) / baseWidth : 1.0;
const double scaleY = baseHeight > 0.0 ? static_cast<double>(height()) / baseHeight : 1.0;
const QPointF localAnchor(
static_cast<double>(width()) * base.anchorX + bubble.offsetX * scaleX,
static_cast<double>(height()) * base.anchorY + bubble.offsetY * scaleY);
return frameGeometry().topLeft() + QPoint(qRound(localAnchor.x()), qRound(localAnchor.y()));
}
void PetWindow::playState(const QString &stateName, bool centerWindow)
@@ -869,9 +890,11 @@ void PetWindow::playResolvedState(const QString &stateName, bool centerWindow)
QSize PetWindow::animationTargetSize() const
{
const int sideLength = qRound(BaseAnimationTargetSize * m_appConfig.scale);
const int boundedSideLength = qBound(BaseAnimationTargetSize / 2, sideLength, BaseAnimationTargetSize * 2);
return QSize(boundedSideLength, boundedSideLength);
const CharacterBase &base = m_characterPackage.base;
const double totalScale = base.scale * m_appConfig.scale;
return QSize(
boundedAnimationTargetSide(static_cast<double>(base.width) * totalScale),
boundedAnimationTargetSide(static_cast<double>(base.height) * totalScale));
}
int PetWindow::effectiveAnimationFps(int fps) const