载入中...
搜索中...
未找到
AnimationClip.cpp
浏览该文件的文档.
2
3#include <algorithm>
4#include <cctype>
5#include <cmath>
6#include <cstdint>
7#include <set>
8#include <utility>
9
11namespace {
12
13const EditorValue* field(const EditorValue& value, const char* key) {
14 const auto* object = value.getIf<EditorValue::Object>();
15 if (!object) return nullptr;
16 const auto found = object->find(key);
17 return found == object->end() ? nullptr : &found->second;
18}
19
20const std::string* stringField(const EditorValue& value, const char* key) {
21 const EditorValue* entry = field(value, key);
22 return entry ? entry->getIf<std::string>() : nullptr;
23}
24
25bool readNumber(const EditorValue& value, const char* key, double& out) {
26 const EditorValue* entry = field(value, key);
27 if (!entry) return false;
28 if (const auto* real = entry->getIf<double>()) {
29 out = *real;
30 return true;
31 }
32 if (const auto* integer = entry->getIf<std::int64_t>()) {
33 out = static_cast<double>(*integer);
34 return true;
35 }
36 return false;
37}
38
39EditorValue keyValue(const AnimationTransformKey& key) {
40 return EditorValue::Object{{"id", key.id.value()}, {"time", key.time},
41 {"px", key.positionX}, {"py", key.positionY}, {"pz", key.positionZ},
42 {"rx", key.rotationX}, {"ry", key.rotationY}, {"rz", key.rotationZ},
43 {"rw", key.rotationW}, {"sx", key.scaleX}, {"sy", key.scaleY},
44 {"sz", key.scaleZ}};
45}
46
48 const auto* id = stringField(value, "id");
49 double time = 0, px = 0, py = 0, pz = 0, rx = 0, ry = 0, rz = 0, rw = 0, sx = 0, sy = 0, sz = 0;
50 if (!id || id->empty() || !readNumber(value, "time", time) || !readNumber(value, "px", px) ||
51 !readNumber(value, "py", py) || !readNumber(value, "pz", pz) || !readNumber(value, "rx", rx) ||
52 !readNumber(value, "ry", ry) || !readNumber(value, "rz", rz) || !readNumber(value, "rw", rw) ||
53 !readNumber(value, "sx", sx) || !readNumber(value, "sy", sy) || !readNumber(value, "sz", sz))
54 return eve::editing::failed<AnimationTransformKey>(EditorStatus::Rejected,
55 RuleId("editor.animation.invalid-key"),
56 "Transform key requires a stable id, time and complete TRS");
57 const double values[]{time, px, py, pz, rx, ry, rz, rw, sx, sy, sz};
58 for (double component : values)
59 if (!std::isfinite(component))
60 return eve::editing::failed<AnimationTransformKey>(EditorStatus::Rejected,
61 RuleId("editor.animation.nonfinite-key"),
62 "Transform key components must be finite");
63 const double length = std::sqrt(rx * rx + ry * ry + rz * rz + rw * rw);
64 if (time < 0.0 || sx <= 0.0 || sy <= 0.0 || sz <= 0.0 || length < 1e-8)
65 return eve::editing::failed<AnimationTransformKey>(
66 EditorStatus::Rejected, RuleId("editor.animation.invalid-key-range"),
67 "Key time must be non-negative, scale positive and rotation non-zero");
68 return eve::editing::applied<AnimationTransformKey>({StableId(*id), time, px, py, pz,
69 rx / length, ry / length, rz / length, rw / length,
70 sx, sy, sz});
71}
72
73EditorValue trackValue(const AnimationBoneTrack& track) {
75 for (const auto& key : track.keys) keys.push_back(keyValue(key));
76 return EditorValue::Object{{"id", track.id.value()}, {"bone", track.bone}, {"keys", std::move(keys)}};
77}
78
80 const auto* id = stringField(value, "id");
81 const auto* bone = stringField(value, "bone");
82 const EditorValue* keyEntry = field(value, "keys");
83 const auto* keys = keyEntry ? keyEntry->getIf<EditorValue::Array>() : nullptr;
84 if (!id || id->empty() || !bone || bone->empty() || !keys)
85 return eve::editing::failed<AnimationBoneTrack>(EditorStatus::Rejected,
86 RuleId("editor.animation.invalid-track"),
87 "Bone track requires stable id, bone name and keys");
88 AnimationBoneTrack result{StableId(*id), *bone, {}};
89 std::set<StableId> ids;
90 for (const auto& entry : *keys) {
91 auto key = parseKey(entry);
92 if (!key.ok() || !ids.insert(key.value().id).second)
93 return eve::editing::failed<AnimationBoneTrack>(EditorStatus::Rejected,
94 RuleId("editor.animation.duplicate-key"),
95 "Transform key ids must be unique inside a track");
96 result.keys.push_back(std::move(key).value());
97 }
98 std::sort(result.keys.begin(), result.keys.end(), [](const auto& a, const auto& b) {
99 return a.time == b.time ? a.id < b.id : a.time < b.time;
100 });
101 return eve::editing::applied<AnimationBoneTrack>(std::move(result));
102}
103
104EditorValue eventValue(const AnimationEventRecord& event) {
105 return EditorValue::Object{{"id", event.id.value()}, {"time", event.time},
106 {"name", event.name}, {"payload", event.payload}};
107}
108
110 const auto* id = stringField(value, "id");
111 double time = 0;
112 const auto* name = stringField(value, "name");
113 const auto* payload = stringField(value, "payload");
114 if (!id || id->empty() || !readNumber(value, "time", time) || !std::isfinite(time) || time < 0.0 ||
115 !name || name->empty() || !payload)
116 return eve::editing::failed<AnimationEventRecord>(
117 EditorStatus::Rejected, RuleId("editor.animation.invalid-event"),
118 "Event requires stable id, non-negative time, name and payload");
119 return eve::editing::applied<AnimationEventRecord>({StableId(*id), time, *name, *payload});
120}
121
122EditorValue settingsValue(double duration, double sampleRate, bool loop) {
123 return EditorValue::Object{{"duration", duration}, {"sampleRate", sampleRate}, {"loop", loop}};
124}
125
126DomainOperation operation(const char* type, const char* inverseType, const std::string& target,
127 EditorValue payload, EditorValue inverse, std::string object) {
128 DomainOperation result;
129 result.type = type; result.inverseType = inverseType; result.target = TargetId(target);
130 result.payload = std::move(payload); result.inverse = std::move(inverse); result.hasInverse = true;
131 result.affectedObjects.push_back({TargetId(target), std::move(object), 0});
132 return result;
133}
134
135std::string normalized(std::string name) {
136 std::string result;
137 for (unsigned char c : name)
138 if (std::isalnum(c)) result.push_back(static_cast<char>(std::tolower(c)));
139 return result;
140}
141
142double mix(double a, double b, double amount) { return a + (b - a) * amount; }
143
144EditorDiagnostic clipDiagnostic(const char* rule, DiagnosticSeverity severity, std::string message) {
146 std::move(message));
147}
148
149} // namespace
150
152
154 TargetDescriptor result;
155 result.id = TargetId(id_);
156 result.type = "animation-clip-document";
157 result.revision = revisionValue();
160 return result;
161}
162
163void* AnimationClipDocumentTarget::queryCapability(const CapabilityId& capability) {
165 return static_cast<IAnimationClipEditTarget*>(this);
167 return static_cast<eve::editing::IEditingSnapshotProvider*>(this);
168 return nullptr;
169}
170
172 if (op.target != TargetId(id_))
173 return eve::editing::failed<void>(EditorStatus::Rejected, RuleId("editor.animation.target-mismatch"),
174 "Operation targets another clip");
175 if (op.type == "animation.clip.settings.v1") {
176 double duration = 0, rate = 0;
177 const EditorValue* loopEntry = field(op.payload, "loop"); const auto* loop = loopEntry ? loopEntry->getIf<bool>() : nullptr;
178 if (!readNumber(op.payload, "duration", duration) || !readNumber(op.payload, "sampleRate", rate) ||
179 !loop || !std::isfinite(duration) || !std::isfinite(rate) || duration <= 0.0 || rate <= 0.0)
180 return eve::editing::failed<void>(EditorStatus::Rejected, RuleId("editor.animation.invalid-settings"),
181 "Duration and sample rate must be positive");
182 duration_ = duration; sampleRate_ = rate; loop_ = *loop;
183 } else if (op.type == "animation.clip.track.set.v1") {
184 auto track = parseTrack(op.payload);
185 if (!track.ok())
186 return eve::editing::failed<void>(track.code(), RuleId("editor.animation.invalid-track"),
187 "Track payload is invalid");
188 for (const auto& key : track.value().keys)
189 if (key.time > duration_)
190 return eve::editing::failed<void>(EditorStatus::Rejected, RuleId("editor.animation.key-after-duration"),
191 "Track key exceeds clip duration");
192 for (const auto& [id, current] : tracks_)
193 if (id != track.value().id && current.bone == track.value().bone)
194 return eve::editing::failed<void>(EditorStatus::Conflict,
195 RuleId("editor.animation.duplicate-bone-track"),
196 "A bone may have only one track");
197 auto trackRecord = std::move(track).value(); tracks_[trackRecord.id] = std::move(trackRecord);
198 } else if (op.type == "animation.clip.track.delete.v1") {
199 auto track = parseTrack(op.payload); if (!track.ok() || !tracks_.erase(track.value().id))
200 return eve::editing::failed<void>(EditorStatus::NotFound, RuleId("editor.animation.track-not-found"),
201 "Bone track was not found");
202 } else if (op.type == "animation.clip.event.set.v1") {
203 auto event = parseEvent(op.payload);
204 if (!event.ok())
205 return eve::editing::failed<void>(event.code(), RuleId("editor.animation.invalid-event"),
206 "Event payload is invalid");
207 if (event.value().time > duration_)
208 return eve::editing::failed<void>(EditorStatus::Rejected, RuleId("editor.animation.event-after-duration"),
209 "Event exceeds clip duration");
210 auto eventRecord = std::move(event).value(); events_[eventRecord.id] = std::move(eventRecord);
211 } else if (op.type == "animation.clip.event.delete.v1") {
212 auto event = parseEvent(op.payload); if (!event.ok() || !events_.erase(event.value().id))
213 return eve::editing::failed<void>(EditorStatus::NotFound, RuleId("editor.animation.event-not-found"),
214 "Event was not found");
215 } else if (op.type == "animation.clip.mask.set.v1") {
216 const auto* bone = stringField(op.payload, "bone"); double weight = 0;
217 if (!bone || bone->empty() || !readNumber(op.payload, "weight", weight) || !std::isfinite(weight) ||
218 weight < 0.0 || weight > 1.0)
219 return eve::editing::failed<void>(EditorStatus::Rejected, RuleId("editor.animation.invalid-mask"),
220 "Mask weight must be in [0, 1]");
221 mask_[*bone] = weight;
222 } else {
223 return eve::editing::failed<void>(EditorStatus::Unsupported, RuleId("editor.animation.unsupported-operation"),
224 "Unsupported clip operation");
225 }
226 bumpRevision();
227 widenDirty(0, 0);
228 return eve::editing::applied<void>();
229}
230
231std::unique_ptr<IDomainOperationTarget> AnimationClipDocumentTarget::cloneDomainState() const {
232 return std::make_unique<AnimationClipDocumentTarget>(*this);
233}
234
235Result<void> AnimationClipDocumentTarget::commitDomainState(std::unique_ptr<IDomainOperationTarget> candidate) {
236 auto* clip = dynamic_cast<AnimationClipDocumentTarget*>(candidate.get());
237 if (!clip || clip->id_ != id_)
238 return eve::editing::failed<void>(EditorStatus::Rejected, RuleId("editor.animation.invalid-candidate"),
239 "Candidate is not this clip document");
240 *this = std::move(*clip); return eve::editing::applied<void>();
241}
242
244 if (!std::isfinite(duration) || !std::isfinite(sampleRate) || duration <= 0.0 || sampleRate <= 0.0)
245 return eve::editing::failed<DomainOperation>(EditorStatus::Rejected,
246 RuleId("editor.animation.invalid-settings"),
247 "Duration and sample rate must be positive");
248 for (const auto& [id, track] : tracks_) { static_cast<void>(id); for (const auto& key : track.keys)
249 if (key.time > duration)
250 return eve::editing::failed<DomainOperation>(EditorStatus::Rejected,
251 RuleId("editor.animation.duration-truncates-key"),
252 "New duration would truncate a transform key");
253 }
254 for (const auto& [id, event] : events_) { static_cast<void>(id); if (event.time > duration)
255 return eve::editing::failed<DomainOperation>(EditorStatus::Rejected,
256 RuleId("editor.animation.duration-truncates-event"),
257 "New duration would truncate an event");
258 }
259 return eve::editing::applied<DomainOperation>(operation("animation.clip.settings.v1", "animation.clip.settings.v1", id_,
260 settingsValue(duration, sampleRate, loop), settingsValue(duration_, sampleRate_, loop_), "settings"));
261}
262
264 auto parsed = parseTrack(trackValue(track));
265 if (!parsed.ok())
266 return eve::editing::failed<DomainOperation>(parsed.code(), RuleId("editor.animation.invalid-track"),
267 "Track is invalid");
268 const auto old = tracks_.find(track.id); EditorValue inverse = old == tracks_.end() ? trackValue(track) : trackValue(old->second);
269 return eve::editing::applied<DomainOperation>(operation("animation.clip.track.set.v1",
270 old == tracks_.end() ? "animation.clip.track.delete.v1" : "animation.clip.track.set.v1", id_, trackValue(parsed.value()), std::move(inverse), track.id.value()));
271}
272
274 const auto found = tracks_.find(id);
275 if (found == tracks_.end())
276 return eve::editing::failed<DomainOperation>(EditorStatus::NotFound, RuleId("editor.animation.track-not-found"),
277 "Bone track was not found");
278 return eve::editing::applied<DomainOperation>(operation("animation.clip.track.delete.v1", "animation.clip.track.set.v1", id_, trackValue(found->second), trackValue(found->second), id.value()));
279}
280
282 auto parsed = parseEvent(eventValue(event));
283 if (!parsed.ok())
284 return eve::editing::failed<DomainOperation>(parsed.code(), RuleId("editor.animation.invalid-event"),
285 "Event is invalid");
286 const auto old = events_.find(event.id); EditorValue inverse = old == events_.end() ? eventValue(event) : eventValue(old->second);
287 return eve::editing::applied<DomainOperation>(operation("animation.clip.event.set.v1",
288 old == events_.end() ? "animation.clip.event.delete.v1" : "animation.clip.event.set.v1", id_, eventValue(parsed.value()), std::move(inverse), event.id.value()));
289}
290
292 const auto found = events_.find(id);
293 if (found == events_.end())
294 return eve::editing::failed<DomainOperation>(EditorStatus::NotFound, RuleId("editor.animation.event-not-found"),
295 "Event was not found");
296 return eve::editing::applied<DomainOperation>(operation("animation.clip.event.delete.v1", "animation.clip.event.set.v1", id_, eventValue(found->second), eventValue(found->second), id.value()));
297}
298
300 if (mask.bone.empty() || !std::isfinite(mask.weight) || mask.weight < 0.0 || mask.weight > 1.0)
301 return eve::editing::failed<DomainOperation>(EditorStatus::Rejected, RuleId("editor.animation.invalid-mask"),
302 "Mask weight must be in [0, 1]");
303 const auto old = mask_.find(mask.bone); const double oldWeight = old == mask_.end() ? 1.0 : old->second;
304 return eve::editing::applied<DomainOperation>(operation("animation.clip.mask.set.v1", "animation.clip.mask.set.v1", id_,
305 EditorValue::Object{{"bone", mask.bone}, {"weight", mask.weight}}, EditorValue::Object{{"bone", mask.bone}, {"weight", oldWeight}}, mask.bone));
306}
307
308std::vector<AnimationBoneTrack> AnimationClipDocumentTarget::tracks() const {
309 std::vector<AnimationBoneTrack> result; for (const auto& [id, track] : tracks_) { static_cast<void>(id); result.push_back(track); } return result;
310}
311
312std::vector<AnimationEventRecord> AnimationClipDocumentTarget::events() const {
313 std::vector<AnimationEventRecord> result; for (const auto& [id, event] : events_) { static_cast<void>(id); result.push_back(event); }
314 std::sort(result.begin(), result.end(), [](const auto& a, const auto& b) { return a.time == b.time ? a.id < b.id : a.time < b.time; }); return result;
315}
316
317std::vector<EditorDiagnostic> AnimationClipDocumentTarget::validate(const std::vector<std::string>& bones) const {
318 std::vector<EditorDiagnostic> result; const std::set<std::string> known(bones.begin(), bones.end());
319 for (const auto& [id, track] : tracks_) { static_cast<void>(id);
320 if (!known.empty() && !known.contains(track.bone)) result.push_back(clipDiagnostic("editor.animation.missing-track-bone", DiagnosticSeverity::Error, "Track references missing bone: " + track.bone));
321 if (track.keys.empty()) result.push_back(clipDiagnostic("editor.animation.empty-track", DiagnosticSeverity::Warning, "Bone track has no keys: " + track.bone));
322 }
323 for (const auto& [bone, weight] : mask_) { static_cast<void>(weight); if (!known.empty() && !known.contains(bone))
324 result.push_back(clipDiagnostic("editor.animation.missing-mask-bone", DiagnosticSeverity::Error, "Mask references missing bone: " + bone)); }
325 return result;
326}
327
328AnimationClipPreview AnimationClipDocumentTarget::preview(double time, const std::vector<std::string>& bones) const {
331 if (!std::isfinite(time)) { result.diagnostics.push_back(clipDiagnostic("editor.animation.invalid-preview-time", DiagnosticSeverity::Error, "Preview time must be finite")); return result; }
332 result.time = loop_ ? std::fmod(std::max(0.0, time), duration_) : std::clamp(time, 0.0, duration_);
333 result.diagnostics = validate(bones);
334 for (const auto& [id, track] : tracks_) { static_cast<void>(id); if (track.keys.empty()) continue;
335 auto upper = std::upper_bound(track.keys.begin(), track.keys.end(), result.time, [](double t, const auto& key) { return t < key.time; });
336 const auto& b = upper == track.keys.end() ? track.keys.back() : *upper;
337 const auto& a = upper == track.keys.begin() ? track.keys.front() : *(upper - 1);
338 const double amount = b.time > a.time ? (result.time - a.time) / (b.time - a.time) : 0.0;
339 AnimationSampledBone sample; sample.bone = track.bone;
340 sample.positionX = mix(a.positionX, b.positionX, amount); sample.positionY = mix(a.positionY, b.positionY, amount); sample.positionZ = mix(a.positionZ, b.positionZ, amount);
341 sample.rotationX = mix(a.rotationX, b.rotationX, amount); sample.rotationY = mix(a.rotationY, b.rotationY, amount); sample.rotationZ = mix(a.rotationZ, b.rotationZ, amount); sample.rotationW = mix(a.rotationW, b.rotationW, amount);
342 const double length = std::sqrt(sample.rotationX * sample.rotationX + sample.rotationY * sample.rotationY + sample.rotationZ * sample.rotationZ + sample.rotationW * sample.rotationW);
343 if (length > 1e-8) { sample.rotationX /= length; sample.rotationY /= length; sample.rotationZ /= length; sample.rotationW /= length; }
344 sample.scaleX = mix(a.scaleX, b.scaleX, amount); sample.scaleY = mix(a.scaleY, b.scaleY, amount); sample.scaleZ = mix(a.scaleZ, b.scaleZ, amount);
345 const auto mask = mask_.find(track.bone); sample.maskWeight = mask == mask_.end() ? 1.0 : mask->second; result.bones.push_back(std::move(sample));
346 }
347 result.status = std::any_of(result.diagnostics.begin(), result.diagnostics.end(), [](const auto& d) { return d.severity() == DiagnosticSeverity::Error; }) ? EditorStatus::Failed : EditorStatus::Applied;
348 return result;
349}
350
351AnimationRetargetPreview AnimationClipDocumentTarget::previewRetarget(const std::vector<std::string>& targetBones) const {
354 std::set<std::string> used;
355 for (const auto& [id, track] : tracks_) { static_cast<void>(id); auto exact = std::find(targetBones.begin(), targetBones.end(), track.bone);
356 auto found = exact; if (found == targetBones.end()) found = std::find_if(targetBones.begin(), targetBones.end(), [&](const auto& target) { return normalized(target) == normalized(track.bone); });
357 if (found == targetBones.end() || used.contains(*found)) result.unmatchedSourceBones.push_back(track.bone); else { result.mapping[track.bone] = *found; used.insert(*found); }
358 }
359 for (const auto& target : targetBones) if (!used.contains(target)) result.unmatchedTargetBones.push_back(target);
360 if (!result.unmatchedSourceBones.empty()) result.diagnostics.push_back(clipDiagnostic("editor.animation.unmatched-source-bones", DiagnosticSeverity::Warning, "Some source tracks cannot be mapped"));
361 if (!result.unmatchedTargetBones.empty()) result.diagnostics.push_back(clipDiagnostic("editor.animation.unmatched-target-bones", DiagnosticSeverity::Info, "Some target bones remain at bind pose"));
362 result.status = EditorStatus::Applied; return result;
363}
364
366 EditorValue::Array tracks; for (const auto& [id, track] : tracks_) { static_cast<void>(id); tracks.push_back(trackValue(track)); }
367 EditorValue::Array events; for (const auto& event : this->events()) events.push_back(eventValue(event));
368 EditorValue::Array masks; for (const auto& [bone, weight] : mask_) masks.push_back(EditorValue::Object{{"bone", bone}, {"weight", weight}});
369 return EditorValue::Object{{"schemaVersion", int64_t{1}}, {"settings", settingsValue(duration_, sampleRate_, loop_)}, {"tracks", std::move(tracks)}, {"events", std::move(events)}, {"masks", std::move(masks)}};
370}
371
372Result<void> AnimationClipDocumentTarget::loadSnapshot(const EditorValue& snapshot) {
373 const EditorValue* versionEntry = field(snapshot, "schemaVersion"); const auto* version = versionEntry ? versionEntry->getIf<int64_t>() : nullptr;
374 const EditorValue* settings = field(snapshot, "settings"); const EditorValue* tracksEntry = field(snapshot, "tracks"); const EditorValue* eventsEntry = field(snapshot, "events"); const EditorValue* masksEntry = field(snapshot, "masks");
375 const auto* tracks = tracksEntry ? tracksEntry->getIf<EditorValue::Array>() : nullptr; const auto* events = eventsEntry ? eventsEntry->getIf<EditorValue::Array>() : nullptr; const auto* masks = masksEntry ? masksEntry->getIf<EditorValue::Array>() : nullptr;
376 if (!version || *version != 1 || !settings || !tracks || !events || !masks)
377 return eve::editing::failed<void>(EditorStatus::Unsupported, RuleId("editor.animation.invalid-snapshot"),
378 "Clip snapshot schema is unsupported");
379 double duration = 0, rate = 0;
380 const EditorValue* loopEntry = field(*settings, "loop"); const auto* loop = loopEntry ? loopEntry->getIf<bool>() : nullptr;
381 if (!readNumber(*settings, "duration", duration) || !readNumber(*settings, "sampleRate", rate) || !loop)
382 return eve::editing::failed<void>(EditorStatus::Rejected, RuleId("editor.animation.invalid-settings"),
383 "Clip snapshot settings are incomplete");
384 AnimationClipDocumentTarget candidate(id_); auto settingsOp = candidate.makeSetSettings(duration, rate, *loop);
385 if (!settingsOp.ok() || !candidate.applyDomainOperation(settingsOp.value()).ok())
386 return eve::editing::failed<void>(EditorStatus::Rejected, RuleId("editor.animation.invalid-settings"),
387 "Clip snapshot settings are invalid");
388 for (const auto& entry : *tracks) {
389 auto parsed = parseTrack(entry);
390 if (!parsed.ok())
391 return eve::editing::failed<void>(EditorStatus::Rejected, RuleId("editor.animation.invalid-track"),
392 "Clip snapshot contains invalid track");
393 auto op = candidate.makeSetTrack(parsed.value());
394 if (!op.ok() || !candidate.applyDomainOperation(op.value()).ok())
395 return eve::editing::failed<void>(EditorStatus::Rejected, RuleId("editor.animation.invalid-track"),
396 "Clip snapshot track cannot be applied");
397 }
398 for (const auto& entry : *events) {
399 auto parsed = parseEvent(entry);
400 if (!parsed.ok())
401 return eve::editing::failed<void>(EditorStatus::Rejected, RuleId("editor.animation.invalid-event"),
402 "Clip snapshot contains invalid event");
403 auto op = candidate.makeSetEvent(parsed.value());
404 if (!op.ok() || !candidate.applyDomainOperation(op.value()).ok())
405 return eve::editing::failed<void>(EditorStatus::Rejected, RuleId("editor.animation.invalid-event"),
406 "Clip snapshot event cannot be applied");
407 }
408 for (const auto& entry : *masks) {
409 const auto* bone = stringField(entry, "bone");
410 double weight = 0;
411 if (!bone || !readNumber(entry, "weight", weight))
412 return eve::editing::failed<void>(EditorStatus::Rejected, RuleId("editor.animation.invalid-mask"),
413 "Clip snapshot contains invalid mask");
414 auto op = candidate.makeSetMask({*bone, weight});
415 if (!op.ok() || !candidate.applyDomainOperation(op.value()).ok())
416 return eve::editing::failed<void>(EditorStatus::Rejected, RuleId("editor.animation.invalid-mask"),
417 "Clip snapshot mask cannot be applied");
418 }
419 candidate.setRevision(revisionValue() + 1);
420 candidate.widenDirty(0, 0);
421 *this = std::move(candidate);
422 return eve::editing::applied<void>();
423}
424
427
428} // namespace eve::animation_editing
LogicalId target
ActionParameterOperation operation
double value
Value::Object payload
float duration
int mask
float length
Definition CaveMesh.cpp:94
float py
float pz
std::map< std::string, Var > values
std::string message
std::uint32_t key
glm::vec4 clip
glm::uvec4 ids
std::int32_t c
std::vector< Colorf > px
std::vector< Track > tracks
std::vector< Bone > bones
std::uint32_t bone
std::string name
MeleePoint3 b
Definition MeleeHit.cpp:41
MeleePoint3 a
Definition MeleeHit.cpp:40
std::string id
Definition PlayHost.cpp:108
float d
float t
bool found
double current
Battle::Events events
TerrainThermalSettings settings
A structured explanation of a failed, degraded, or noteworthy result.
Definition Diagnostic.h:94
bool ok() const noexcept
Whether this result represents a non-failure outcome.
Definition Result.h:480
Move-only operation result carrying either a value or Status.
Definition Result.h:155
UUID-backed identifier adapter for legacy textual boundaries.
Definition Identity.h:314
const std::string & value() const noexcept
Returns the exact legacy or canonical compatibility spelling.
Definition Identity.h:362
UI-neutral animation clip document with reversible stable-id timeline edits.
std::vector< EditorDiagnostic > validate(const std::vector< std::string > &skeletonBones={}) const
Validate timeline, references and mask against optional target bone names.
double sampleRate() const noexcept
Authored sample rate.
Result< DomainOperation > makeSetSettings(double duration, double sampleRate, bool loop) const override
Make set settings.
Result< DomainOperation > makeDeleteEvent(const StableId &event) const override
Make delete event.
Result< DomainOperation > makeSetMask(const AnimationMaskEntry &mask) const override
Make set mask.
std::unique_ptr< IDomainOperationTarget > cloneDomainState() const override
Clones domain state.
AnimationClipPreview preview(double time, const std::vector< std::string > &skeletonBones={}) const
Sample the authored clip without mutating a runtime asset.
Result< DomainOperation > makeDeleteTrack(const StableId &track) const override
Make delete track.
Result< void > commitDomainState(std::unique_ptr< IDomainOperationTarget > candidate) override
Commits domain state.
TargetDescriptor describe() const override
Describe.
Result< DomainOperation > makeSetEvent(const AnimationEventRecord &event) const override
Make set event.
EditorValue snapshotValue() const override
Capture deterministic schema-version-one clip data.
Result< void > applyDomainOperation(const DomainOperation &operation) override
Applies domain operation.
double duration() const noexcept
Clip duration in seconds.
AnimationClipDocumentTarget(std::string id)
Animation clip document target.
std::vector< AnimationEventRecord > events() const
Return event markers ordered by time then stable id.
std::vector< AnimationBoneTrack > tracks() const
Return tracks ordered by stable id.
void * queryCapability(const CapabilityId &capability) override
Query an optional target capability.
Result< DomainOperation > makeSetTrack(const AnimationBoneTrack &track) const override
Make set track.
AnimationRetargetPreview previewRetarget(const std::vector< std::string > &targetBones) const
Preview exact then normalized-name retarget mapping without baking a clip.
Timeline editing capability for skeletal clips.
static CapabilityId editingCapabilityId()
Stable capability id for clip timeline operations.
void setRevision(Revision revision)
Adopt a revision observed elsewhere, without advancing it.
void bumpRevision()
Advance the revision without touching the dirty region.
Revision revisionValue() const noexcept
Return the revision as the shared protocol revision type.
void widenDirty(int x, int y)
Widen the dirty region without advancing the revision.
void widenDirty()
Mark an unknown area dirty without advancing the revision.
eve::OptionalRef< C > capability()
Query a capability through its interface-owned stable identity.
Optional capability exposing a deterministic, persistence-safe editing snapshot.
static CapabilityId editingCapabilityId()
Stable capability identity shared by every editable domain target.
Deterministic owning value tree shared by authoring hosts.
std::vector< Value > Array
std::map< std::string, Value > Object
const T * getIf() const
Return an immediately borrowed typed value.
editing::StableId StableId
Result< AnimationEventRecord > parseAnimationEventRecord(const EditorValue &value)
Parse one authored event marker from an owning Value payload.
Result< AnimationBoneTrack > parseAnimationBoneTrack(const EditorValue &value)
Parse one authored bone track from an owning Value payload.
Diagnostic ruleDiagnostic(eve::DiagnosticCode code, RuleId rule, DiagnosticSeverity severity, std::string message)
Build a common diagnostic carrying an open editing rule identity.
Severity
Stable severity of a diagnostic, independent of its display text.
Definition Diagnostic.h:20
Stable bone track addressed by skeleton bone name.
std::vector< AnimationTransformKey > keys
Immutable timeline scrub result tied to one document revision.
std::vector< AnimationSampledBone > bones
std::vector< EditorDiagnostic > diagnostics
Stable timeline event marker with optional payload.
Per-bone blend mask entry in normalized [0, 1] weight space.
Non-destructive source-to-target skeleton mapping preview.
std::map< std::string, std::string > mapping
std::vector< EditorDiagnostic > diagnostics
Sampled local transform returned by non-destructive timeline scrubbing.
One stable transform key used by the animation timeline.
DomainOperation public API.
std::vector< ObjectRefValue > affectedObjects
Stable discovery metadata exposed by every editable target.
std::vector< CapabilityId > capabilities