-
Notifications
You must be signed in to change notification settings - Fork 0
feat(pose-lib): sub-slice D-Project — .poselib sidecar persistence #602
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -14,6 +14,11 @@ | |
| #include "SentryReporter.h" | ||
|
|
||
| #include <QCoreApplication> | ||
| #include <QFile> | ||
| #include <QJsonArray> | ||
| #include <QJsonDocument> | ||
| #include <QJsonObject> | ||
| #include <QSaveFile> | ||
| #include <QThread> | ||
|
|
||
| #include <OgreBone.h> | ||
|
|
@@ -304,6 +309,147 @@ | |
| } | ||
| } | ||
|
|
||
| namespace { | ||
|
|
||
| // Schema string written into / verified out of the sidecar JSON. | ||
| // Bump when the format changes incompatibly so loadPoseLibrary can | ||
| // reject older files cleanly. | ||
| constexpr const char* kPoseLibSchemaV1 = "qtmesheditor.poselib.v1"; | ||
|
|
||
| } // namespace | ||
|
|
||
| bool PoseLibrary::savePoseLibrary(Ogre::Entity* entity, const QString& filePath) const | ||
| { | ||
| assertMainThread(); | ||
| if (!entity || filePath.isEmpty()) return false; | ||
| auto entIt = m_byEntity.constFind(entity); | ||
| if (entIt == m_byEntity.constEnd()) return false; | ||
| if (entIt->order.isEmpty()) return false; | ||
|
|
||
| QJsonArray poses; | ||
| for (const QString& name : entIt->order) { | ||
| auto poseIt = entIt->byName.constFind(name); | ||
| if (poseIt == entIt->byName.constEnd()) continue; | ||
| QJsonObject bones; | ||
| for (auto bIt = poseIt->cbegin(); bIt != poseIt->cend(); ++bIt) { | ||
| const auto& trs = bIt.value(); | ||
| QJsonObject bone; | ||
| bone["t"] = QJsonArray{ trs.translate.x, trs.translate.y, trs.translate.z }; | ||
| bone["r"] = QJsonArray{ trs.rotation.w, trs.rotation.x, | ||
| trs.rotation.y, trs.rotation.z }; | ||
| bone["s"] = QJsonArray{ trs.scale.x, trs.scale.y, trs.scale.z }; | ||
| bones[bIt.key()] = bone; | ||
| } | ||
| QJsonObject poseObj; | ||
| poseObj["name"] = name; | ||
| poseObj["bones"] = bones; | ||
| poses.append(poseObj); | ||
| } | ||
|
|
||
| QJsonObject root; | ||
| root["schema"] = kPoseLibSchemaV1; | ||
| root["poses"] = poses; | ||
|
|
||
| // QSaveFile gives atomic write: temp file + rename on commit, so | ||
| // a power loss / kill -9 mid-write doesn't leave a half-written | ||
| // sidecar that loadPoseLibrary later rejects. | ||
| QSaveFile file(filePath); | ||
| if (!file.open(QIODevice::WriteOnly | QIODevice::Truncate)) return false; | ||
| if (file.write(QJsonDocument(root).toJson(QJsonDocument::Indented)) < 0) { | ||
| file.cancelWriting(); | ||
| return false; | ||
| } | ||
| if (!file.commit()) return false; | ||
|
|
||
| SentryReporter::addBreadcrumb("scene.anim.pose", | ||
| QStringLiteral("save library to '%1' (%2 poses)") | ||
| .arg(filePath).arg(entIt->order.size())); | ||
| return true; | ||
| } | ||
|
|
||
| bool PoseLibrary::loadPoseLibrary(Ogre::Entity* entity, const QString& filePath) | ||
| { | ||
| assertMainThread(); | ||
| if (!entity || filePath.isEmpty()) return false; | ||
| QFile file(filePath); | ||
| if (!file.open(QIODevice::ReadOnly)) return false; | ||
| const QByteArray bytes = file.readAll(); | ||
| file.close(); | ||
| QJsonParseError parseError{}; | ||
| const QJsonDocument doc = QJsonDocument::fromJson(bytes, &parseError); | ||
| if (parseError.error != QJsonParseError::NoError) return false; | ||
| if (!doc.isObject()) return false; | ||
| const QJsonObject root = doc.object(); | ||
| if (root.value("schema").toString() != QString::fromLatin1(kPoseLibSchemaV1)) | ||
| return false; | ||
| const QJsonArray poses = root.value("poses").toArray(); | ||
|
|
||
| // Wipe the existing per-entity entry — the file is canonical | ||
| // for this load. Partial overlay would be confusing UX (which | ||
| // pose wins on name collision?), so we go with replacement. | ||
| m_byEntity.remove(entity); | ||
|
Comment on lines
+385
to
+390
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When the file has a valid Useful? React with 👍 / 👎. |
||
| auto& store = m_byEntity[entity]; | ||
|
|
||
|
Comment on lines
+385
to
+392
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Validate full payload before wiping existing poses.
Proposed fix- const QJsonArray poses = root.value("poses").toArray();
+ if (!root.contains("poses") || !root.value("poses").isArray()) return false;
+ const QJsonArray poses = root.value("poses").toArray();
- m_byEntity.remove(entity);
- auto& store = m_byEntity[entity];
+ EntityPoses parsed;
+ QSet<QString> seenNames;
@@
- for (const QJsonValue& p : poses) {
+ for (const QJsonValue& p : poses) {
+ if (!p.isObject()) return false;
const QJsonObject pObj = p.toObject();
const QString name = pObj.value("name").toString();
- if (name.isEmpty()) continue;
+ if (name.isEmpty() || seenNames.contains(name)) return false;
+ if (!pObj.value("bones").isObject()) return false;
+ seenNames.insert(name);
PoseSnapshot snapshot;
const QJsonObject bones = pObj.value("bones").toObject();
@@
- store.byName.insert(name, snapshot);
- store.order.append(name);
+ parsed.byName.insert(name, snapshot);
+ parsed.order.append(name);
}
+ m_byEntity.insert(entity, parsed);Also applies to: 407-426 🤖 Prompt for AI Agents |
||
| auto readVec3 = [](const QJsonArray& a, const Ogre::Vector3& def) -> Ogre::Vector3 { | ||
|
Check warning on line 393 in src/PoseLibrary.cpp
|
||
| if (a.size() != 3) return def; | ||
| return Ogre::Vector3(static_cast<Ogre::Real>(a[0].toDouble()), | ||
| static_cast<Ogre::Real>(a[1].toDouble()), | ||
| static_cast<Ogre::Real>(a[2].toDouble())); | ||
| }; | ||
| auto readQuat = [](const QJsonArray& a, const Ogre::Quaternion& def) -> Ogre::Quaternion { | ||
|
Check warning on line 399 in src/PoseLibrary.cpp
|
||
| if (a.size() != 4) return def; | ||
| return Ogre::Quaternion(static_cast<Ogre::Real>(a[0].toDouble()), | ||
| static_cast<Ogre::Real>(a[1].toDouble()), | ||
| static_cast<Ogre::Real>(a[2].toDouble()), | ||
| static_cast<Ogre::Real>(a[3].toDouble())); | ||
| }; | ||
|
|
||
| for (const QJsonValue& p : poses) { | ||
| const QJsonObject pObj = p.toObject(); | ||
| const QString name = pObj.value("name").toString(); | ||
| if (name.isEmpty()) continue; | ||
| PoseSnapshot snapshot; | ||
| const QJsonObject bones = pObj.value("bones").toObject(); | ||
| for (auto it = bones.constBegin(); it != bones.constEnd(); ++it) { | ||
| const QJsonObject boneObj = it.value().toObject(); | ||
| BonePoseSnapshot trs; | ||
| trs.translate = readVec3(boneObj.value("t").toArray(), | ||
| Ogre::Vector3::ZERO); | ||
| trs.rotation = readQuat(boneObj.value("r").toArray(), | ||
| Ogre::Quaternion::IDENTITY); | ||
| trs.scale = readVec3(boneObj.value("s").toArray(), | ||
| Ogre::Vector3(1, 1, 1)); | ||
| snapshot.insert(it.key(), trs); | ||
| } | ||
| store.byName.insert(name, snapshot); | ||
| store.order.append(name); | ||
|
Comment on lines
+424
to
+425
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Loading appends every parsed pose name into Useful? React with 👍 / 👎. |
||
| } | ||
|
|
||
| SentryReporter::addBreadcrumb("scene.anim.pose", | ||
| QStringLiteral("load library from '%1' (%2 poses)") | ||
| .arg(filePath).arg(store.order.size())); | ||
| emit posesChanged(entity); | ||
| return true; | ||
| } | ||
|
|
||
| bool PoseLibrary::savePoseLibraryForSelection(const QString& filePath) const | ||
| { | ||
| auto* sel = SelectionSet::getSingleton(); | ||
|
Check warning on line 437 in src/PoseLibrary.cpp
|
||
| if (!sel) return false; | ||
| auto ents = sel->getResolvedEntities(); | ||
| if (ents.isEmpty()) return false; | ||
| return savePoseLibrary(ents.first(), filePath); | ||
| } | ||
|
|
||
| bool PoseLibrary::loadPoseLibraryForSelection(const QString& filePath) | ||
| { | ||
| auto* sel = SelectionSet::getSingleton(); | ||
|
Check warning on line 446 in src/PoseLibrary.cpp
|
||
| if (!sel) return false; | ||
| auto ents = sel->getResolvedEntities(); | ||
| if (ents.isEmpty()) return false; | ||
| return loadPoseLibrary(ents.first(), filePath); | ||
| } | ||
|
|
||
| bool PoseLibrary::savePoseForSelection(const QString& name) | ||
| { | ||
| auto* sel = SelectionSet::getSingleton(); | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Use file I/O breadcrumb categories required by policy.
For save/load sidecar operations, breadcrumb categories should be
file.exportandfile.importinstead ofscene.anim.pose.Proposed fix
As per coding guidelines: "All user-facing actions and significant operations must be tracked with SentryReporter::addBreadcrumb() using appropriate categories ... 'file.import'/'file.export' for I/O operations".
Also applies to: 428-430
🤖 Prompt for AI Agents