载入中...
搜索中...
未找到
AnimationClipEditor.cpp
浏览该文件的文档.
2
3#if defined(EVE_ANIMATION_EDITOR_RUNTIME)
7#endif
9
10#include <algorithm>
11#include <cmath>
12#include <cstdint>
13#include <numbers>
14#include <set>
15#include <utility>
16
18namespace {
19
20template <class T = void>
22 std::string message) {
23 return eve::editing::failed<T>(status, animation_editing::RuleId(std::move(rule)), std::move(message));
24}
25
26animation_editing::EditorValue keyValue(const char* id, double time, double px, double py, double pz) {
28 {"id", std::string(id)}, {"time", time}, {"px", px}, {"py", py}, {"pz", pz},
29 {"rx", 0.0}, {"ry", 0.0}, {"rz", 0.0}, {"rw", 1.0}, {"sx", 1.0},
30 {"sy", 1.0}, {"sz", 1.0}};
31}
32
33animation_editing::EditorValue trackValue(const char* id, const char* bone,
36 {"id", std::string(id)}, {"bone", std::string(bone)}, {"keys", std::move(keys)}};
37}
38
39} // namespace
40
42 : target_(std::move(targetId)), authority_(&target_), transactions_(&authority_) {
43 skeleton_ = {{"Hips", {}}, {"Spine", "Hips"}, {"Chest", "Spine"}, {"Head", "Chest"},
44 {"LeftArm", "Chest"}, {"RightArm", "Chest"}};
45 seedPreviewClip();
46 auto previewed = refreshPreview();
47 if (!previewed.ok())
48 previewed.ignore("animation clip editor keeps an empty overlay when the seeded preview is rejected");
49}
50
51#if defined(EVE_ANIMATION_EDITOR_RUNTIME)
54 if (skeleton.getBoneCount() <= 0 || clip.getDuration() <= 0.0f)
55 return editorError(animation_editing::EditorStatus::Rejected, "editor.animation.runtime-source",
56 "Runtime skeleton and clip must contain bones and a positive duration");
58 std::vector<SkeletonBone> candidateSkeleton;
60 for (int bone = 0; bone < skeleton.getBoneCount(); ++bone) {
61 const std::string name = skeleton.getBoneName(bone);
62 const int parent = skeleton.getParent(bone);
63 candidateSkeleton.push_back({name, parent >= 0 ? skeleton.getBoneName(parent) : std::string{}});
64 std::set<double> times;
65 for (int key = 0; key < clip.getPositionKeyCount(bone); ++key) times.insert(clip.getPositionKeyTime(bone, key));
66 for (int key = 0; key < clip.getRotationKeyCount(bone); ++key) times.insert(clip.getRotationKeyTime(bone, key));
67 for (int key = 0; key < clip.getScaleKeyCount(bone); ++key) times.insert(clip.getScaleKeyTime(bone, key));
68 if (times.empty()) times.insert(0.0);
70 int keyIndex = 0;
71 for (const double time : times) {
72 clip.sample(static_cast<float>(time), &pose, &skeleton);
74 {"id", "runtime-key:" + std::to_string(bone) + ":" + std::to_string(keyIndex++)}, {"time", time},
75 {"px", static_cast<double>(pose.getLocalPositionX(bone))}, {"py", static_cast<double>(pose.getLocalPositionY(bone))},
76 {"pz", static_cast<double>(pose.getLocalPositionZ(bone))}, {"rx", static_cast<double>(pose.getLocalRotationX(bone))},
77 {"ry", static_cast<double>(pose.getLocalRotationY(bone))}, {"rz", static_cast<double>(pose.getLocalRotationZ(bone))},
78 {"rw", static_cast<double>(pose.getLocalRotationW(bone))}, {"sx", static_cast<double>(pose.getLocalScaleX(bone))},
79 {"sy", static_cast<double>(pose.getLocalScaleY(bone))}, {"sz", static_cast<double>(pose.getLocalScaleZ(bone))}});
80 }
81 tracks.push_back(trackValue(("runtime-track:" + std::to_string(bone)).c_str(), name.c_str(), std::move(keys)));
82 }
84 root["schemaVersion"] = std::int64_t{1};
85 root["settings"] = animation_editing::EditorValue::Object{{"duration", static_cast<double>(clip.getDuration())},
86 {"sampleRate", static_cast<double>(clip.getSampleRate())},
87 {"loop", clip.getLoop()}};
88 root["tracks"] = std::move(tracks); root["events"] = animation_editing::EditorValue::Array{};
90 auto loaded = target_.loadSnapshot(animation_editing::EditorValue(std::move(root)));
91 if (!loaded.ok()) return loaded;
92 skeleton_ = std::move(candidateSkeleton);
93 selectedBone_ = skeleton_.front().name; selectedKeyId_ = animation_editing::StableId();
94 playhead_ = 0.0; playing_ = false;
95 return refreshPreview();
96}
97
98animation_editing::Result<void> AnimationClipEditor::writeRuntimeClip(
99 animation::AnimClip& clip, const animation::AnimSkeleton& skeleton) const {
100 animation_editing::AnimationClipRuntimeBuilder builder;
101 auto built = builder.build(target_, &skeleton);
102 if (!built.ok()) return animation_editing::Result<void>::failure(built.status());
103 std::unique_ptr<animation::AnimClip> candidate(std::move(built).takeValue());
104 clip.adopt(*candidate);
105 return eve::editing::applied<void>();
106}
107#endif
108
109void AnimationClipEditor::seedPreviewClip() {
111 tracks.push_back(trackValue("hips-track", "Hips",
112 {keyValue("hips-start", 0.0, 0.0, 0.0, 0.0),
113 keyValue("hips-end", 2.0, 2.0, 0.0, 0.0)}));
114 tracks.push_back(trackValue("spine-track", "Spine",
115 {keyValue("spine-start", 0.0, 0.0, 1.0, 0.0),
116 keyValue("spine-end", 2.0, 2.0, 1.2, 0.0)}));
117 tracks.push_back(trackValue("chest-track", "Chest", {keyValue("chest-start", 0.0, 0.0, 1.7, 0.0), keyValue("chest-end", 2.0, 2.0, 1.8, 0.0)}));
118 tracks.push_back(trackValue("head-track", "Head", {keyValue("head-start", 0.0, 0.0, 2.25, 0.0), keyValue("head-end", 2.0, 2.0, 2.3, 0.0)}));
119 tracks.push_back(trackValue("left-arm-track", "LeftArm", {keyValue("left-arm-start", 0.0, -0.55, 1.75, 0.0), keyValue("left-arm-end", 2.0, 1.45, 1.65, 0.0)}));
120 tracks.push_back(trackValue("right-arm-track", "RightArm", {keyValue("right-arm-start", 0.0, 0.55, 1.75, 0.0), keyValue("right-arm-end", 2.0, 2.55, 1.85, 0.0)}));
122 events.push_back(animation_editing::EditorValue::Object{{"id", std::string("footstep")},
123 {"time", 0.5},
124 {"name", std::string("footstep")},
125 {"payload", std::string("left")}});
127 masks.push_back(animation_editing::EditorValue::Object{{"bone", std::string("Hips")}, {"weight", 1.0}});
128 masks.push_back(animation_editing::EditorValue::Object{{"bone", std::string("Spine")}, {"weight", 1.0}});
130 root["schemaVersion"] = std::int64_t{1};
131 root["settings"] =
132 animation_editing::EditorValue::Object{{"duration", 2.0}, {"sampleRate", 30.0}, {"loop", true}};
133 root["tracks"] = std::move(tracks);
134 root["events"] = std::move(events);
135 root["masks"] = std::move(masks);
136 auto loaded = target_.loadSnapshot(animation_editing::EditorValue(std::move(root)));
137 if (!loaded.ok())
138 loaded.ignore("animation clip editor keeps defaults when the preview snapshot is rejected");
139}
140
141std::vector<std::string> AnimationClipEditor::skeletonBones() const {
142 std::vector<std::string> result; result.reserve(skeleton_.size());
143 for (const auto& bone : skeleton_) result.push_back(bone.name);
144 return result;
145}
146
147std::string AnimationClipEditor::skeletonParent(const std::string& bone) const {
148 for (const auto& entry : skeleton_) if (entry.name == bone) return entry.parent;
149 return {};
150}
151
153 editor::EditorWorkspace& workspace) const {
154 editor::EditorWorkspace candidate = workspace;
155 struct Panel {
156 const char* id;
157 const char* title;
158 const char* region;
159 const char* context;
160 int order;
161 };
162 constexpr Panel panels[] = {
163 {"animation.skeleton", "Skeleton", "left", "list", 100},
164 {"animation.preview", "Pose Preview", "center", "preview", 100},
165 {"animation.inspector", "Clip Inspector", "right", "inspector", 100},
166 {"animation.timeline", "Dope / Curve", "bottom", "timeline", 100},
167 };
168 for (const auto& panel : panels) {
169 if (!candidate.registerPanel(panel.id, panel.title, panel.region, panel.order) ||
170 !candidate.setPanelCapability(panel.id, "animation.clip") ||
171 !candidate.setPanelContext(panel.id, panel.context))
172 return editorError(animation_editing::EditorStatus::Rejected, "editor.animation.workspace-conflict",
173 "Could not install the animation clip workspace composition");
174 }
175 if (!candidate.activatePanel("animation.timeline"))
176 return editorError(animation_editing::EditorStatus::Rejected, "editor.animation.workspace-activate",
177 "Could not activate the animation timeline panel");
178 workspace = std::move(candidate);
179 return eve::editing::applied<void>();
180}
181
183 float labelWidth) {
184 if (!std::isfinite(width) || !std::isfinite(rowHeight) || !std::isfinite(labelWidth) || width < 8.0f ||
185 rowHeight < 8.0f || labelWidth < 0.0f || labelWidth >= width)
186 return editorError(animation_editing::EditorStatus::Rejected, "editor.animation.viewport",
187 "Dope-sheet viewport width, row height and label width are invalid");
188 viewportWidth_ = width;
189 rowHeight_ = rowHeight;
190 labelWidth_ = labelWidth;
191 return eve::editing::applied<void>();
192}
193
195 if (!std::isfinite(seconds))
196 return editorError(animation_editing::EditorStatus::Rejected, "editor.animation.seek",
197 "Seek time must be finite");
198 const double duration = target_.duration();
199 playhead_ = duration <= 0.0 ? 0.0 : std::clamp(seconds, 0.0, duration);
200 return refreshPreview();
201}
202
204 if (!std::isfinite(x))
205 return editorError(animation_editing::EditorStatus::Rejected, "editor.animation.seek-x",
206 "Dope-sheet seek requires a finite x");
207 return seekSeconds(xToTime(x));
208}
209
211 if (!std::isfinite(x) || !std::isfinite(y))
212 return editorError(animation_editing::EditorStatus::Rejected, "editor.animation.pointer",
213 "Pointer coordinates must be finite");
214 const auto tracks = target_.tracks();
215 if (tracks.empty()) return seekX(x);
216 const int row = static_cast<int>(std::floor(y / rowHeight_));
217 if (x < labelWidth_ && row >= 0 && static_cast<std::size_t>(row) < tracks.size())
218 return selectBone(tracks[static_cast<std::size_t>(row)].bone);
219
220 const auto keys = flattenKeys();
221 int best = -1;
222 float bestDistance = 8.0f;
223 for (int i = 0; i < static_cast<int>(keys.size()); ++i) {
224 const float dx = x - timeToX(keys[static_cast<std::size_t>(i)].time);
225 const float dy = y - (static_cast<float>(keys[static_cast<std::size_t>(i)].row) * rowHeight_ + rowHeight_ * 0.5f);
226 const float distance = std::hypot(dx, dy);
227 if (distance < bestDistance) {
228 bestDistance = distance;
229 best = i;
230 }
231 }
232 if (best >= 0) {
233 selectedKeyTrack_ = keys[static_cast<std::size_t>(best)].trackId;
234 selectedKeyId_ = target_.tracks()[static_cast<std::size_t>(keys[static_cast<std::size_t>(best)].row)]
235 .keys[static_cast<std::size_t>(keys[static_cast<std::size_t>(best)].keyIndex)]
236 .id;
237 return selectBone(keys[static_cast<std::size_t>(best)].bone);
238 }
239 selectedKeyId_ = animation_editing::StableId();
240 return seekX(x);
241}
242
244 if (bone.empty())
245 return editorError(animation_editing::EditorStatus::Rejected, "editor.animation.select-bone",
246 "Bone name must not be empty");
247 selectedBone_ = std::move(bone);
248 return refreshPreview();
249}
250
252 return commit(target_.makeSetMask({selectedBone_, weight}), "Set mask " + selectedBone_);
253}
254
256 return commit(target_.makeSetSettings(duration, target_.sampleRate(), target_.isLooping()), "Set clip duration");
257}
258
260 return commit(target_.makeSetSettings(target_.duration(), sampleRate, target_.isLooping()), "Set clip sample rate");
261}
262
264 return commit(target_.makeSetSettings(target_.duration(), target_.sampleRate(), loop), "Set clip loop");
265}
266
268 if (selectedKeyId_.empty())
269 return editorError(animation_editing::EditorStatus::NotFound, "editor.animation.no-selected-key",
270 "No key is selected");
271 if (!std::isfinite(time) || time < 0.0 || time > target_.duration())
272 return editorError(animation_editing::EditorStatus::Rejected, "editor.animation.key-time",
273 "Key time must lie inside the clip duration");
274 auto tracks = target_.tracks();
275 for (auto& track : tracks) {
276 if (track.id != selectedKeyTrack_) continue;
277 const auto key = std::find_if(track.keys.begin(), track.keys.end(),
278 [&](const auto& candidate) { return candidate.id == selectedKeyId_; });
279 if (key == track.keys.end()) continue;
280 key->time = time;
281 return commit(target_.makeSetTrack(track), "Move key");
282 }
283 return editorError(animation_editing::EditorStatus::NotFound, "editor.animation.track-missing",
284 "Selected key track was not found");
285}
286
287animation_editing::AnimationTransformKey AnimationClipEditor::sampledSelectedTransform() const {
289 result.time = playhead_;
290 for (const auto& bone : preview_.bones) {
291 if (bone.bone != selectedBone_) continue;
292 result.positionX = bone.positionX; result.positionY = bone.positionY; result.positionZ = bone.positionZ;
293 result.rotationX = bone.rotationX; result.rotationY = bone.rotationY; result.rotationZ = bone.rotationZ; result.rotationW = bone.rotationW;
294 result.scaleX = bone.scaleX; result.scaleY = bone.scaleY; result.scaleZ = bone.scaleZ;
295 break;
296 }
297 return result;
298}
299
301 auto tracks = target_.tracks();
302 for (auto& track : tracks) {
303 if (track.bone != selectedBone_) continue;
304 auto key = std::find_if(track.keys.begin(), track.keys.end(), [&](const auto& candidate) {
305 return std::abs(candidate.time - playhead_) < 1e-6;
306 });
307 if (key == track.keys.end()) {
308 auto inserted = sampledSelectedTransform();
309 inserted.id = animation_editing::StableId("key." + selectedBone_ + "." + std::to_string(++txSequence_));
310 track.keys.push_back(std::move(inserted));
311 selectedKeyId_ = track.keys.back().id;
312 } else {
313 selectedKeyId_ = key->id;
314 }
315 selectedKeyTrack_ = track.id;
316 return commit(target_.makeSetTrack(track), "Key " + selectedBone_);
317 }
318 return editorError(animation_editing::EditorStatus::NotFound, "editor.animation.track-missing",
319 "Selected bone has no editable transform track");
320}
321
323 if (selectedKeyId_.empty())
324 return editorError(animation_editing::EditorStatus::NotFound, "editor.animation.no-selected-key", "No key is selected");
325 auto tracks = target_.tracks();
326 for (auto& track : tracks) {
327 if (track.id != selectedKeyTrack_) continue;
328 const auto before = track.keys.size();
329 std::erase_if(track.keys, [&](const auto& key) { return key.id == selectedKeyId_; });
330 if (track.keys.size() == before) break;
331 selectedKeyId_ = animation_editing::StableId();
332 return commit(target_.makeSetTrack(track), "Delete key");
333 }
334 return editorError(animation_editing::EditorStatus::NotFound, "editor.animation.key-missing", "Selected key no longer exists");
335}
336
337animation_editing::Result<void> AnimationClipEditor::updateSelectedTransform(
339 auto tracks = target_.tracks();
340 for (auto& track : tracks) {
341 if (track.bone != selectedBone_) continue;
342 auto key = std::find_if(track.keys.begin(), track.keys.end(), [&](const auto& candidate) {
343 return std::abs(candidate.time - playhead_) < 1e-6;
344 });
345 if (key == track.keys.end()) {
346 auto inserted = value;
347 inserted.id = animation_editing::StableId("key." + selectedBone_ + "." + std::to_string(++txSequence_));
348 inserted.time = playhead_;
349 track.keys.push_back(std::move(inserted));
350 selectedKeyId_ = track.keys.back().id;
351 } else {
352 auto replacement = value; replacement.id = key->id; replacement.time = key->time; *key = replacement;
353 selectedKeyId_ = key->id;
354 }
355 selectedKeyTrack_ = track.id;
356 return commit(target_.makeSetTrack(track), std::move(label));
357 }
358 return editorError(animation_editing::EditorStatus::NotFound, "editor.animation.track-missing", "Selected bone has no editable transform track");
359}
360
362 if (!std::isfinite(x) || !std::isfinite(y) || !std::isfinite(z))
363 return editorError(animation_editing::EditorStatus::Rejected, "editor.animation.position", "Position must be finite");
364 auto value = sampledSelectedTransform(); value.positionX = x; value.positionY = y; value.positionZ = z;
365 return updateSelectedTransform(value, "Set position " + selectedBone_);
366}
367
369 if (!std::isfinite(xd) || !std::isfinite(yd) || !std::isfinite(zd))
370 return editorError(animation_editing::EditorStatus::Rejected, "editor.animation.rotation", "Rotation must be finite");
371 const double k = std::numbers::pi / 360.0;
372 const double cx = std::cos(xd*k), sx = std::sin(xd*k), cy = std::cos(yd*k), sy = std::sin(yd*k), cz = std::cos(zd*k), sz = std::sin(zd*k);
373 auto value = sampledSelectedTransform();
374 value.rotationW = cx*cy*cz + sx*sy*sz; value.rotationX = sx*cy*cz - cx*sy*sz;
375 value.rotationY = cx*sy*cz + sx*cy*sz; value.rotationZ = cx*cy*sz - sx*sy*cz;
376 return updateSelectedTransform(value, "Set rotation " + selectedBone_);
377}
378
380 if (!std::isfinite(x) || !std::isfinite(y) || !std::isfinite(z) || x <= 0.0 || y <= 0.0 || z <= 0.0)
381 return editorError(animation_editing::EditorStatus::Rejected, "editor.animation.scale", "Scale must be positive and finite");
382 auto value = sampledSelectedTransform(); value.scaleX = x; value.scaleY = y; value.scaleZ = z;
383 return updateSelectedTransform(value, "Set scale " + selectedBone_);
384}
385
386animation_editing::Result<void> AnimationClipEditor::commit(
388 if (!operation.ok())
391 spec.id = editor::TransactionId("animation.clip.tx." + std::to_string(++txSequence_));
392 spec.label = std::move(label);
393 spec.target = editor::TargetId(target_.targetId());
394 spec.baseRevision = target_.revision();
395 auto begun = transactions_.begin(std::move(spec));
396 if (!begun.ok())
397 return editorError(begun.code(), "editor.animation.begin", "Could not begin the animation clip transaction");
398 auto appended = transactions_.append(std::move(operation).takeValue());
399 if (!appended.ok()) {
400 auto discarded = transactions_.discard();
401 if (!discarded.ok()) discarded.ignore("pending animation clip transaction already inactive");
402 return animation_editing::Result<void>::failure(appended.status());
403 }
404 auto committed = transactions_.commit();
405 if (!committed.ok())
406 return animation_editing::Result<void>::failure(committed.status());
407 return refreshPreview();
408}
409
411 auto result = transactions_.undo();
412 if (!result.ok()) return result;
413 auto previewed = refreshPreview();
414 if (!previewed.ok())
416 return result;
417}
418
420 auto result = transactions_.redo();
421 if (!result.ok()) return result;
422 auto previewed = refreshPreview();
423 if (!previewed.ok())
425 return result;
426}
427
428void AnimationClipEditor::play() noexcept { playing_ = true; }
429void AnimationClipEditor::pause() noexcept { playing_ = false; }
431 playing_ = false;
432 playhead_ = 0.0;
433}
434
436 if (playing_) {
437 if (!std::isfinite(deltaSeconds) || deltaSeconds < 0.0)
438 return editorError(animation_editing::EditorStatus::Rejected, "editor.animation.dt",
439 "Playback delta must be a non-negative finite time");
440 playhead_ += deltaSeconds;
441 const double duration = target_.duration();
442 if (duration <= 0.0) {
443 playhead_ = 0.0;
444 } else if (target_.isLooping()) {
445 playhead_ = std::fmod(playhead_, duration);
446 if (playhead_ < 0.0) playhead_ += duration;
447 } else if (playhead_ >= duration) {
448 playhead_ = duration;
449 playing_ = false;
450 }
451 }
452 return refreshPreview();
453}
454
455animation_editing::Result<void> AnimationClipEditor::refreshPreview() {
456 const auto sampled = target_.preview(playhead_, skeletonBones());
457 if (sampled.documentRevision != target_.revision()) {
458 return editorError(animation_editing::EditorStatus::Conflict, "editor.animation.stale-preview",
459 "Clip preview revision does not match the document");
460 }
461 if (sampled.status != animation_editing::EditorStatus::Applied)
462 return editorError(sampled.status, "editor.animation.preview", "Could not sample the animation clip pose");
463
464 std::vector<animation_editing::SkeletonOverlayBone> bones;
465 bones.reserve(sampled.bones.size());
466 for (const auto& sample : sampled.bones) {
468 bone.id = animation_editing::StableId(sample.bone);
469 bone.parent = animation_editing::StableId(skeletonParent(sample.bone));
470 bone.name = sample.bone;
471 bone.position = {sample.positionX, sample.positionY, sample.positionZ};
472 bone.rotation = {sample.rotationX, sample.rotationY, sample.rotationZ, sample.rotationW};
473 bone.selected = sample.bone == selectedBone_;
474 bone.maskWeight = sample.maskWeight;
475 bones.push_back(std::move(bone));
476 }
477 animation_editing::SkeletonOverlayOptions options;
478 options.showAxes = true;
479 options.axesForSelectedOnly = true;
480 options.showConstraints = false;
481 auto overlay = overlayBuilder_.build(target_.targetId().value(), animation_editing::Revision(target_.revision()),
482 bones, options);
483 if (overlay.status != eve::editing::Status::Applied)
484 return editorError(overlay.status, "editor.animation.overlay",
485 "Could not rebuild the skeleton overlay from the sampled pose");
486 preview_ = sampled;
487 overlay_ = std::move(overlay);
488 return eve::editing::applied<void>();
489}
490
491float AnimationClipEditor::layoutHeight() const noexcept {
492 return static_cast<float>(target_.tracks().size()) * rowHeight_ + 28.0f;
493}
494
495float AnimationClipEditor::playheadX() const noexcept { return timeToX(playhead_); }
496
498 for (const auto& sample : preview_.bones)
499 if (sample.bone == selectedBone_) return sample.maskWeight;
500 return 1.0;
501}
502
504 for (const auto& track : target_.tracks()) for (const auto& key : track.keys) if (key.id == selectedKeyId_) return key.time;
505 return playhead_;
506}
507
508double AnimationClipEditor::selectedPositionX() const { return sampledSelectedTransform().positionX; }
509double AnimationClipEditor::selectedPositionY() const { return sampledSelectedTransform().positionY; }
510double AnimationClipEditor::selectedPositionZ() const { return sampledSelectedTransform().positionZ; }
511double AnimationClipEditor::selectedScaleX() const { return sampledSelectedTransform().scaleX; }
512double AnimationClipEditor::selectedScaleY() const { return sampledSelectedTransform().scaleY; }
513double AnimationClipEditor::selectedScaleZ() const { return sampledSelectedTransform().scaleZ; }
514double AnimationClipEditor::selectedRotationX() const { const auto q=sampledSelectedTransform(); return std::atan2(2*(q.rotationW*q.rotationX+q.rotationY*q.rotationZ),1-2*(q.rotationX*q.rotationX+q.rotationY*q.rotationY))*180/std::numbers::pi; }
515double AnimationClipEditor::selectedRotationY() const { const auto q=sampledSelectedTransform(); return std::asin(std::clamp(2*(q.rotationW*q.rotationY-q.rotationZ*q.rotationX),-1.0,1.0))*180/std::numbers::pi; }
516double AnimationClipEditor::selectedRotationZ() const { const auto q=sampledSelectedTransform(); return std::atan2(2*(q.rotationW*q.rotationZ+q.rotationX*q.rotationY),1-2*(q.rotationY*q.rotationY+q.rotationZ*q.rotationZ))*180/std::numbers::pi; }
517int AnimationClipEditor::boneCount() const noexcept { return static_cast<int>(skeletonBones().size()); }
518std::string AnimationClipEditor::boneName(int index) const { const auto bones=skeletonBones(); return index >= 0 && static_cast<std::size_t>(index) < bones.size() ? bones[static_cast<std::size_t>(index)] : std::string{}; }
519std::string AnimationClipEditor::boneParent(int index) const { return skeletonParent(boneName(index)); }
520
521int AnimationClipEditor::trackCount() const { return static_cast<int>(target_.tracks().size()); }
522
523std::string AnimationClipEditor::trackBone(int index) const {
524 const auto tracks = target_.tracks();
525 if (index < 0 || static_cast<std::size_t>(index) >= tracks.size()) return {};
526 return tracks[static_cast<std::size_t>(index)].bone;
527}
528
529std::string AnimationClipEditor::trackId(int index) const {
530 const auto tracks = target_.tracks();
531 if (index < 0 || static_cast<std::size_t>(index) >= tracks.size()) return {};
532 return tracks[static_cast<std::size_t>(index)].id.value();
533}
534
535bool AnimationClipEditor::isTrackSelected(int index) const { return trackBone(index) == selectedBone_; }
536
537int AnimationClipEditor::keyCount() const { return static_cast<int>(flattenKeys().size()); }
538
540 const auto keys = flattenKeys();
541 if (index < 0 || static_cast<std::size_t>(index) >= keys.size()) return 0.0f;
542 return timeToX(keys[static_cast<std::size_t>(index)].time);
543}
544
546 const auto keys = flattenKeys();
547 if (index < 0 || static_cast<std::size_t>(index) >= keys.size()) return 0.0f;
548 return static_cast<float>(keys[static_cast<std::size_t>(index)].row) * rowHeight_ + rowHeight_ * 0.5f;
549}
550
552 const auto keys = flattenKeys();
553 if (index < 0 || static_cast<std::size_t>(index) >= keys.size()) return false;
554 const auto& key = keys[static_cast<std::size_t>(index)];
555 const auto tracks = target_.tracks();
556 return selectedKeyTrack_ == key.trackId && tracks[static_cast<std::size_t>(key.row)].keys[static_cast<std::size_t>(key.keyIndex)].id == selectedKeyId_;
557}
558
559int AnimationClipEditor::eventCount() const { return static_cast<int>(target_.events().size()); }
560
562 const auto events = target_.events();
563 if (index < 0 || static_cast<std::size_t>(index) >= events.size()) return 0.0f;
564 return timeToX(events[static_cast<std::size_t>(index)].time);
565}
566
567std::string AnimationClipEditor::eventName(int index) const {
568 const auto events = target_.events();
569 if (index < 0 || static_cast<std::size_t>(index) >= events.size()) return {};
570 return events[static_cast<std::size_t>(index)].name;
571}
572
573int AnimationClipEditor::primitiveCount() const { return static_cast<int>(overlay_.primitives.size()); }
574
576 const auto* primitive = primitiveAt(index);
577 return primitive ? primitive->kind : std::string{};
578}
579
581 const auto* primitive = primitiveAt(index);
582 return primitive ? static_cast<float>(primitive->position[0]) : 0.0f;
583}
584
586 const auto* primitive = primitiveAt(index);
587 return primitive ? static_cast<float>(primitive->position[1]) : 0.0f;
588}
589
591 const auto* primitive = primitiveAt(index);
592 return primitive ? static_cast<float>(primitive->direction[0]) : 0.0f;
593}
594
596 const auto* primitive = primitiveAt(index);
597 return primitive ? static_cast<float>(primitive->direction[1]) : 0.0f;
598}
599
601 const auto* primitive = primitiveAt(index);
602 return primitive ? static_cast<float>(primitive->length) : 0.0f;
603}
604
606 const auto* primitive = primitiveAt(index);
607 return primitive ? static_cast<float>(primitive->radius) : 0.0f;
608}
609
611 const auto* primitive = primitiveAt(index);
612 return primitive ? static_cast<float>(primitive->color[0]) : 0.0f;
613}
614
616 const auto* primitive = primitiveAt(index);
617 return primitive ? static_cast<float>(primitive->color[1]) : 0.0f;
618}
619
621 const auto* primitive = primitiveAt(index);
622 return primitive ? static_cast<float>(primitive->color[2]) : 0.0f;
623}
624
625float AnimationClipEditor::timeToX(double time) const {
626 const double duration = target_.duration();
627 const float usable = std::max(1.0f, viewportWidth_ - labelWidth_);
628 if (duration <= 0.0) return labelWidth_;
629 return labelWidth_ + static_cast<float>(std::clamp(time / duration, 0.0, 1.0)) * usable;
630}
631
632double AnimationClipEditor::xToTime(float x) const {
633 const float usable = std::max(1.0f, viewportWidth_ - labelWidth_);
634 const float t = std::clamp((x - labelWidth_) / usable, 0.0f, 1.0f);
635 return static_cast<double>(t) * target_.duration();
636}
637
638const eve::editing::GizmoPrimitive* AnimationClipEditor::primitiveAt(int index) const {
639 if (index < 0 || static_cast<std::size_t>(index) >= overlay_.primitives.size()) return nullptr;
640 return &overlay_.primitives[static_cast<std::size_t>(index)];
641}
642
643std::vector<AnimationClipEditor::TimelineKey> AnimationClipEditor::flattenKeys() const {
644 std::vector<TimelineKey> keys;
645 const auto tracks = target_.tracks();
646 for (int row = 0; row < static_cast<int>(tracks.size()); ++row) {
647 const auto& track = tracks[static_cast<std::size_t>(row)];
648 for (int i = 0; i < static_cast<int>(track.keys.size()); ++i) {
649 TimelineKey key;
650 key.trackId = track.id;
651 key.bone = track.bone;
652 key.keyIndex = i;
653 key.time = track.keys[static_cast<std::size_t>(i)].time;
654 key.row = row;
655 keys.push_back(std::move(key));
656 }
657 }
658 return keys;
659}
660
661} // namespace eve::animation_editor
ActionParameterOperation operation
double value
LogicalId trackId
float y
Definition AnimClip.cpp:738
float x
Definition AnimClip.cpp:738
float z
Definition AnimClip.cpp:738
int root
Definition AnimSmr.cpp:119
UI-neutral animation clip editor: workspace, pose scrub, dope sheet, undo.
eve::EntitySpatialPose pose
float cx
Definition CardTypes.cpp:33
float cy
Definition CardTypes.cpp:34
float py
float pz
std::string title
std::string label
std::string message
std::uint32_t key
wgpu::PopErrorScopeStatus status
glm::vec4 clip
std::array< double, 10 > q
std::vector< Colorf > px
std::uint32_t width
std::vector< Track > tracks
std::vector< Bone > bones
std::uint32_t bone
std::int32_t parent
std::string name
float distance
std::vector< std::int32_t > order
std::string id
Definition PlayHost.cpp:108
std::uint32_t targetId
float t
std::string string
float dy
float dx
const SquirrelValueOptions & options
std::map< Cell, int > best
Battle::Events events
uint32_t index
const VegetationPresetContext & context
void ignore(std::string_view reason={}) const noexcept
Explicitly discard this result after documenting the reason.
Definition Result.h:537
Move-only operation result carrying either a value or Status.
Definition Result.h:155
static Result failure(Status status)
Construct a failed result from a structured status.
Definition Result.h:175
UUID-backed identifier adapter for legacy textual boundaries.
Definition Identity.h:314
bool empty() const noexcept
True when no compatibility spelling has been assigned.
Definition Identity.h:374
Keyframed skeletal animation clip (local TRS tracks per bone). Script type: AnimClip.
Definition AnimClip.h:145
Evaluated local (and optional world) pose for an AnimSkeleton. Script type: AnimPose.
Definition AnimPose.h:17
3D bone hierarchy + bind-pose local TRS for skeletal animation. Independent of ik::Skeleton3D (FABRIK...
int getBoneCount() const
Returns the bone count.
int getParent(int boneIndex) const
Returns the parent.
std::string getBoneName(int boneIndex) const
Returns the bone name.
double sampleRate() const noexcept
Authored sample rate.
Result< DomainOperation > makeSetSettings(double duration, double sampleRate, bool loop) const override
Make set settings.
TargetId targetId() const override
Target id.
Result< DomainOperation > makeSetMask(const AnimationMaskEntry &mask) const override
Make set mask.
AnimationClipPreview preview(double time, const std::vector< std::string > &skeletonBones={}) const
Sample the authored clip without mutating a runtime asset.
double duration() const noexcept
Clip duration in seconds.
std::vector< AnimationEventRecord > events() const
Return event markers ordered by time then stable id.
Result< void > loadSnapshot(const EditorValue &snapshot)
Atomically load deterministic schema-version-one clip data.
bool isLooping() const noexcept
Whether preview time wraps.
std::vector< AnimationBoneTrack > tracks() const
Return tracks ordered by stable id.
Result< DomainOperation > makeSetTrack(const AnimationBoneTrack &track) const override
Make set track.
editing::GizmoSnapshot build(std::string target, Revision revision, const std::vector< SkeletonOverlayBone > &bones, const SkeletonOverlayOptions &options={}) const
Build a deterministic overlay or structured hierarchy/value diagnostics.
animation_editing::Result< void > setDuration(double duration)
Commit clip duration through the document transaction path.
animation_editing::Result< void > update(double deltaSeconds)
Advance playhead by injected dt and refresh pose/overlay.
animation_editing::Result< void > setMaskWeight(double weight)
std::string trackId(int index) const
Track id.
double selectedScaleX() const
Selected scale x.
float primitiveX(int index) const
Primitive x.
float primitiveG(int index) const
Primitive g.
double duration() const noexcept
Duration.
double selectedRotationX() const
Selected rotation x.
bool isTrackSelected(int index) const
True when track selected.
animation_editing::Result< void > loadRuntimeClip(const animation::AnimSkeleton &skeleton, const animation::AnimClip &clip)
Atomically replace the editor document from a real runtime skeleton and clip.
float primitiveY(int index) const
Primitive y.
bool isKeySelected(int index) const
True when key selected.
std::string eventName(int index) const
Event name.
std::string primitiveKind(int index) const
Primitive kind.
double selectedScaleY() const
Selected scale y.
std::string boneParent(int index) const
Bone parent.
animation_editing::Result< void > setSelectedScale(double x, double y, double z)
Replace the selected bone scale at the playhead and create a key when needed.
animation_editing::Result< void > setSelectedRotation(double xDegrees, double yDegrees, double zDegrees)
Replace the selected bone Euler rotation in degrees and create a key when needed.
std::string boneName(int index) const
Bone name.
AnimationClipEditor(std::string targetId)
Construct a seeded two-bone preview clip.
animation_editing::Result< void > seekSeconds(double seconds)
animation_editing::Result< void > writeRuntimeClip(animation::AnimClip &clip, const animation::AnimSkeleton &skeleton) const
Replace a runtime clip with the current authoritative editor document.
double sampleRate() const noexcept
Sample rate.
float primitiveDirX(int index) const
Primitive dir x.
double selectedPositionY() const
Selected position y.
float primitiveLength(int index) const
Primitive length.
double selectedMaskWeight() const
Selected mask weight.
animation_editing::Result< void > deleteSelectedKey()
Delete the selected transform key.
float layoutHeight() const noexcept
Layout height.
animation_editing::Result< void > moveSelectedKey(double time)
float primitiveRadius(int index) const
Primitive radius.
animation_editing::Result< void > seekX(float x)
float playheadX() const noexcept
Playhead x.
animation_editing::Result< void > selectBone(std::string bone)
animation_editing::Result< editor::TransactionReceipt > undo()
animation_editing::Result< void > setLoop(bool loop)
Commit looping through the document transaction path.
double selectedRotationY() const
Selected rotation y.
std::string trackBone(int index) const
Track bone.
double selectedPositionX() const
Selected position x.
double selectedRotationZ() const
Selected rotation z.
animation_editing::Result< void > setSampleRate(double sampleRate)
Commit authored sample rate through the document transaction path.
double selectedKeyTime() const
Selected key time.
double selectedScaleZ() const
Selected scale z.
float primitiveB(int index) const
Primitive b.
animation_editing::Result< void > keySelectedBone()
Insert or replace a transform key for the selected bone at the playhead.
animation_editing::Result< void > configureWorkspace(editor::EditorWorkspace &workspace) const
Install Skeleton / Pose Preview / Inspector / Dope panels.
animation_editing::Result< void > setViewport(float width, float rowHeight, float labelWidth)
Configure dope-sheet pixel layout.
float primitiveDirY(int index) const
Primitive dir y.
animation_editing::Result< void > setSelectedPosition(double x, double y, double z)
Replace the selected bone position at the playhead and create a key when needed.
animation_editing::Result< void > pointerDown(float x, float y)
double selectedPositionZ() const
Selected position z.
float primitiveR(int index) const
Primitive r.
animation_editing::Result< editor::TransactionReceipt > redo()
std::uint64_t revision() const override
Return the current revision.
Deterministic owning value tree shared by authoring hosts.
std::vector< Value > Array
std::map< std::string, Value > Object
Composition model shared by project-specific developer and runtime editors.
bool setPanelContext(const std::string &id, const std::string &context)
Attach a semantic input context such as scene, graph, or timeline.
Result< WorkspacePanelDescriptor > registerPanel(WorkspacePanelDescriptor descriptor)
Register a checked owning panel descriptor.
Result< WorkspacePanelDescriptor > activatePanel(const StableId &id)
Activate a visible panel with structured not-found/rejected results.
bool setPanelCapability(const std::string &id, const std::string &capability)
Attach a capability requirement used by the host when filtering panels.
Result< TransactionReceipt > commit() override
Preflight and commit all active operations through the authority.
Result< TransactionId > begin(TransactionSpec specification) override
Begin a transaction with a stable id and base revision.
Result< TransactionReceipt > undo() override
Compensate the most recent committed transaction.
Result< void > discard() override
Discard the active, not-yet-committed operation list.
Result< TransactionReceipt > redo() override
Reapply the most recently compensated transaction.
Result< void > append(DomainOperation operation) override
Append an operation to the active transaction without applying it.
editing::StableId StableId
editing::Revision Revision
eve::editing::TargetId TargetId
Definition EditorIds.h:29
eve::editing::TransactionId TransactionId
Definition EditorIds.h:34
double sample(const Heightmap &map, double u, double v)
Sample.
WidgetDesc row(std::vector< WidgetDesc > children, std::string id)
Horizontal elastic layout row.
Definition Widget.cpp:679
StatusCode
Stable outcome category for an operation.
Definition Status.h:27
std::vector< AnimationSampledBone > bones
One stable transform key used by the animation timeline.
Immutable renderer-neutral world-space bone snapshot.
Renderer-neutral wire primitive for authoring viewport overlays.
std::vector< GizmoPrimitive > primitives
TransactionSpec public API.