载入中...
搜索中...
未找到
ActionTimelineEditor.cpp
浏览该文件的文档.
2
5
6#include <algorithm>
7#include <cmath>
8#include <optional>
9#include <utility>
10
11namespace eve::editor {
12namespace {
13
14constexpr std::string_view kReplaceOperation = "action.timeline.replace";
15
16
17std::string commonMessage(const Status& status, std::string fallback) {
18 if (!status.diagnostics().empty() && !status.diagnostics().front().message().empty())
19 return status.diagnostics().front().message();
20 return fallback;
21}
22
23action::ActionTrack* findTrack(action::ActionTimeline& timeline, const LogicalId& id) {
24 auto found = std::find_if(timeline.tracks.begin(), timeline.tracks.end(),
25 [&](const action::ActionTrack& track) { return track.id == id; });
26 return found == timeline.tracks.end() ? nullptr : &*found;
27}
28
29const action::ActionTrack* findTrack(const action::ActionTimeline& timeline, const LogicalId& id) {
30 auto found = std::find_if(timeline.tracks.begin(), timeline.tracks.end(),
31 [&](const action::ActionTrack& track) { return track.id == id; });
32 return found == timeline.tracks.end() ? nullptr : &*found;
33}
34
35void sortTrack(action::ActionTrack& track) {
36 std::stable_sort(track.notifies.begin(), track.notifies.end(), [](const auto& left, const auto& right) {
37 return left.time == right.time ? left.id.format() < right.id.format() : left.time < right.time;
38 });
39 std::stable_sort(track.states.begin(), track.states.end(), [](const auto& left, const auto& right) {
40 return left.start == right.start ? left.id.format() < right.id.format() : left.start < right.start;
41 });
42}
43
44void sortAnimationSections(action::ActionTimeline& timeline) {
45 std::stable_sort(
46 timeline.animationSections.begin(), timeline.animationSections.end(), [](const auto& left, const auto& right) {
47 return left.start == right.start ? left.id.format() < right.id.format() : left.start < right.start;
48 });
49}
50
51bool containsId(const action::ActionTimeline& timeline, const LogicalId& id) {
52 if (std::any_of(timeline.animationSections.begin(), timeline.animationSections.end(),
53 [&](const auto& section) { return section.id == id; }))
54 return true;
55 for (const auto& track : timeline.tracks) {
56 if (track.id == id) return true;
57 if (std::any_of(track.notifies.begin(), track.notifies.end(),
58 [&](const auto& notify) { return notify.id == id; }))
59 return true;
60 if (std::any_of(track.states.begin(), track.states.end(), [&](const auto& state) { return state.id == id; }))
61 return true;
62 }
63 return false;
64}
65
66EditorValue replacementPayload(const std::string& json) {
68 payload["json"] = json;
69 return EditorValue(std::move(payload));
70}
71
72const std::string* jsonPayload(const EditorValue& value) {
73 const auto* object = value.getIf<EditorValue::Object>();
74 if (!object) return nullptr;
75 const auto found = object->find("json");
76 return found == object->end() ? nullptr : found->second.getIf<std::string>();
77}
78
79action::ActionTimeline seedTimeline(const std::string& targetId) {
80 action::ActionTimeline timeline;
81 if (auto parsed = LogicalId::parse(targetId)) {
82 timeline.actionId = std::move(*parsed);
83 } else if (auto fromName = LogicalId::fromParts("action", targetId.empty() ? "untitled" : targetId)) {
84 timeline.actionId = std::move(*fromName);
85 } else {
86 timeline.actionId = *LogicalId::fromParts("action", "untitled");
87 }
88 timeline.duration = Duration::fromNanoseconds(1'000'000'000);
89 return timeline;
90}
91
92PropertyDescriptor property(const char* path, const char* label, PropertyType type, EditorValue defaultValue,
93 double minimum = 0.0, double maximum = 1e9) {
96 descriptor.displayNameKey = label;
97 descriptor.category = "timeline";
98 descriptor.type = type;
99 descriptor.flags = PropertyFlag::Runtime;
100 descriptor.defaultValue = std::move(defaultValue);
101 descriptor.numeric.minimum = minimum;
102 descriptor.numeric.maximum = maximum;
103 return descriptor;
104}
105
106} // namespace
107
109 timeline_ = seedTimeline(targetId);
110 targetId_ = std::move(targetId);
111}
112
114 : targetId_(std::move(targetId)), timeline_(std::move(timeline)) {}
115
117 return {TargetId(targetId_),
118 "action-timeline",
119 revision_,
120 false,
122}
123
125 if (capability == propertyCapabilityId()) return static_cast<IPropertyProvider*>(this);
127 return static_cast<eve::editing::IEditingSnapshotProvider*>(this);
128 return nullptr;
129}
130
132 if (operation.target.value() != targetId_)
133 return eve::editing::failed<void>(EditorStatus::Rejected, RuleId("editor.action.timeline.target-mismatch"), "Action timeline operation targets another document");
134 if (operation.type != kReplaceOperation)
135 return eve::editing::failed<void>(EditorStatus::Unsupported, RuleId("editor.action.timeline.operation-unsupported"), "Action timeline target only accepts canonical replace operations");
136 const std::string* json = jsonPayload(operation.payload);
137 if (!json)
138 return eve::editing::failed<void>(EditorStatus::Rejected, RuleId("editor.action.timeline.payload-invalid"), "Action timeline replacement requires a JSON string");
139 auto value = Value::fromJson(*json);
140 if (!value)
141 return eve::editing::failed<void>(EditorStatus::Rejected, RuleId("editor.action.timeline.json-invalid"), commonMessage(value.status(), "Action timeline JSON is invalid"));
142 auto decoded = action::ActionTimeline::fromValue(value.value());
143 if (!decoded)
144 return eve::editing::failed<void>(EditorStatus::Rejected, RuleId("editor.action.timeline.asset-invalid"), commonMessage(decoded.status(), "Action timeline asset is invalid"));
145 timeline_ = std::move(decoded).takeValue();
146 ++revision_;
147 return eve::editing::applied<void>();
148}
149
150std::unique_ptr<IDomainOperationTarget> ActionTimelineTarget::cloneDomainState() const {
151 auto clone = std::make_unique<ActionTimelineTarget>(targetId_, timeline_);
152 clone->revision_ = revision_;
153 return clone;
154}
155
156Result<void> ActionTimelineTarget::commitDomainState(std::unique_ptr<IDomainOperationTarget> candidate) {
157 auto* typed = dynamic_cast<ActionTimelineTarget*>(candidate.get());
158 if (!typed || typed->targetId_ != targetId_)
159 return eve::editing::failed<void>(EditorStatus::Conflict, RuleId("editor.action.timeline.candidate-mismatch"), "Action timeline candidate belongs to another target");
160 auto valid = typed->timeline_.validate();
161 if (!valid)
162 return eve::editing::failed<void>(EditorStatus::Rejected, RuleId("editor.action.timeline.candidate-invalid"), commonMessage(valid.status(), "Action timeline candidate is invalid"));
163 timeline_ = std::move(typed->timeline_);
164 ++revision_;
165 return eve::editing::applied<void>();
166}
167
168bool ActionTimelineTarget::matches(const SelectionSnapshot& selection) const {
169 if (selection.items.empty()) return false;
170 for (const auto& item : selection.items) {
171 if (item.target != TargetId(targetId_)) return false;
172 }
173 return true;
174}
175
176Result<void> ActionTimelineTarget::assign(action::ActionTimeline candidate) {
177 auto valid = candidate.validate();
178 if (!valid)
179 return eve::editing::failed<void>(EditorStatus::Rejected, RuleId("editor.action.timeline.asset-invalid"), commonMessage(valid.status(), "Action timeline asset is invalid"));
180 timeline_ = std::move(candidate);
181 ++revision_;
182 return eve::editing::applied<void>();
183}
184
185Result<DomainOperation> ActionTimelineTarget::replacement(const action::ActionTimeline& candidate,
186 std::string property) const {
187 auto beforeValue = timeline_.toValue();
188 auto afterValue = candidate.toValue();
189 if (!beforeValue || !afterValue)
190 return eve::editing::failed<DomainOperation>(EditorStatus::Rejected, RuleId("editor.action.timeline.encode-failed"), "Could not encode the action timeline edit");
191 auto beforeJson = beforeValue.value().toJson();
192 auto afterJson = afterValue.value().toJson();
193 if (!beforeJson || !afterJson)
194 return eve::editing::failed<DomainOperation>(EditorStatus::Rejected, RuleId("editor.action.timeline.encode-failed"), "Could not serialize the action timeline edit");
196 operation.type = std::string(kReplaceOperation);
197 operation.inverseType = std::string(kReplaceOperation);
198 operation.target = TargetId(targetId_);
199 operation.payload = replacementPayload(afterJson.value());
200 operation.inverse = replacementPayload(beforeJson.value());
201 operation.hasInverse = true;
202 operation.affectedObjects.push_back({operation.target, timeline_.actionId.format(), revision_});
203 if (!property.empty()) operation.affectedProperties.push_back(std::move(property));
204 return eve::editing::applied<DomainOperation>(std::move(operation));
205}
206
208 if (!matches(selection))
210 eve::DiagnosticCode::InvalidArgument, "Action timeline selection mismatch", "editor.action.selection"));
212}
213
216 schema.typeId = "action.timeline";
217 schema.version = 1;
218 auto animation = property("timeline.animationUri", "editor.action.animation-uri", PropertyType::AssetRef, "");
219 animation.assetTypeFilters = {"animation", "model3d"};
220 schema.properties = {
221 property("timeline.actionId", "editor.action.action-id", PropertyType::String,
222 seedTimeline(targetId_).actionId.format()),
223 property("timeline.durationSeconds", "editor.action.duration", PropertyType::Float, 1.0, 0.0, 3600.0),
224 std::move(animation)};
225 return schema;
226}
227
229 if (!matches(selection) || !schema(selection).find(path)) return {};
230 if (path == PropertyPath("timeline.actionId")) return {PropertyReadState::Value, timeline_.actionId.format(), {}};
231 if (path == PropertyPath("timeline.durationSeconds"))
232 return {PropertyReadState::Value, timeline_.duration.seconds(), {}};
233 const std::string& projectedUri =
234 timeline_.animationSections.empty() ? timeline_.animationUri : timeline_.animationSections.front().animationUri;
235 return {PropertyReadState::Value, projectedUri, {}};
236}
237
239 const PropertyPath& path, const EditorValue& value,
240 PropertySetMode mode) const {
241 if (mode == PropertySetMode::Reset) return makeReset(selection, path);
242 const auto descriptor = schema(selection).find(path);
243 if (!matches(selection) || !descriptor || mode != PropertySetMode::Absolute ||
244 !validatePropertyValue(*descriptor, value).ok())
245 return eve::editing::failed<DomainOperation>(EditorStatus::Rejected, RuleId("editor.action.timeline.property-invalid"), "Action timeline property assignment is invalid");
246 action::ActionTimeline candidate = timeline_;
247 if (path == PropertyPath("timeline.actionId")) {
248 const auto* text = value.getIf<std::string>();
249 auto parsed = text ? LogicalId::parse(*text) : std::optional<LogicalId>{};
250 if (!parsed)
251 return eve::editing::failed<DomainOperation>(EditorStatus::Rejected, RuleId("editor.action.timeline.action-id"), "Action id must be a namespace:name logical id");
252 candidate.actionId = std::move(*parsed);
253 } else if (path == PropertyPath("timeline.durationSeconds")) {
254 const auto* seconds = value.getIf<double>();
255 if (!seconds)
256 return eve::editing::failed<DomainOperation>(EditorStatus::Rejected, RuleId("editor.action.timeline.duration"), "Timeline duration must be a number of seconds");
257 auto duration = Duration::fromSeconds(*seconds);
258 if (!duration)
259 return eve::editing::failed<DomainOperation>(EditorStatus::Rejected, RuleId("editor.action.timeline.duration"), commonMessage(duration.status(), "Timeline duration is invalid"));
260 candidate.duration = std::move(duration).takeValue();
261 } else {
262 const auto* uri = value.getIf<std::string>();
263 if (!uri)
264 return eve::editing::failed<DomainOperation>(EditorStatus::Rejected, RuleId("editor.action.timeline.animation-uri"), "Animation URI must be a string");
265 candidate.animationUri = *uri;
266 if (!candidate.animationSections.empty()) candidate.animationSections.front().animationUri = *uri;
267 }
268 auto valid = candidate.validate();
269 if (!valid)
270 return eve::editing::failed<DomainOperation>(EditorStatus::Rejected, RuleId("editor.action.timeline.asset-invalid"), commonMessage(valid.status(), "Action timeline property edit is invalid"));
271 return replacement(candidate, path.value());
272}
273
275 const PropertyPath& path) const {
276 const auto descriptor = schema(selection).find(path);
277 if (!descriptor)
278 return eve::editing::failed<DomainOperation>(EditorStatus::Unsupported, RuleId("editor.action.timeline.property-unsupported"), "Unknown action timeline property");
279 return makeSet(selection, path, descriptor->defaultValue, PropertySetMode::Absolute);
280}
281
283 auto encoded = timeline_.toValue();
284 if (!encoded) return {};
285 return toEditorValue(encoded.value());
286}
287
289 if (const std::string* json = jsonPayload(snapshot)) {
290 auto value = Value::fromJson(*json);
291 if (!value)
292 return eve::editing::failed<void>(EditorStatus::Rejected, RuleId("editor.action.timeline.json-invalid"), commonMessage(value.status(), "Action timeline JSON is invalid"));
293 auto decoded = action::ActionTimeline::fromValue(value.value());
294 if (!decoded)
295 return eve::editing::failed<void>(EditorStatus::Rejected, RuleId("editor.action.timeline.asset-invalid"), commonMessage(decoded.status(), "Action timeline asset is invalid"));
296 return assign(std::move(decoded).takeValue());
297 }
298 auto decoded = action::ActionTimeline::fromValue(toPresentationValue(snapshot));
299 if (!decoded)
300 return eve::editing::failed<void>(EditorStatus::Rejected, RuleId("editor.action.timeline.asset-invalid"), commonMessage(decoded.status(), "Action timeline asset is invalid"));
301 return assign(std::move(decoded).takeValue());
302}
303
305 items_.clear();
306 track_.reset();
307}
308
309bool ActionTimelineClipboard::empty() const noexcept { return items_.empty() && !track_; }
310
313
315 std::shared_ptr<ActionTimelineClipboard> clipboard)
316 : target_(std::move(targetId), std::move(timeline)),
317 authority_(&target_),
318 transactions_(&authority_),
319 clipboard_(clipboard ? std::move(clipboard) : std::make_shared<ActionTimelineClipboard>()) {}
320
321Result<void> ActionTimelineEditor::rejected(std::string rule, std::string message) {
322 return eve::editing::failed<void>(EditorStatus::Rejected, RuleId(std::move(rule)), std::move(message));
323}
324
326 if (transactions_.active())
327 return rejected("editor.action.timeline.reload-active-transaction",
328 "Cannot reload a Montage document during an active transaction");
329 auto loaded = target_.loadSnapshot(snapshot);
330 if (!loaded.ok()) return loaded;
331 transactions_.clear();
332 selection_.clear();
333 previewTime_ = Duration::zero();
334 playing_ = false;
335 previewStarted_ = false;
336 previewEvents_.clear();
337 return eve::editing::applied<void>();
338}
339
341 EditorWorkspace candidate = workspace;
342 struct Panel {
343 const char* id;
344 const char* title;
345 const char* region;
346 const char* context;
347 int order;
348 };
349 constexpr Panel panels[] = {
350 {"action.assets", "Actions", "left", "list", 100},
351 {"action.preview", "Action Preview", "center", "preview", 100},
352 {"action.inspector", "Action Inspector", "right", "inspector", 100},
353 {"action.timeline", "Action Timeline", "bottom", "timeline", 100},
354 };
355 for (const auto& panel : panels) {
356 if (!candidate.registerPanel(panel.id, panel.title, panel.region, panel.order) ||
357 !candidate.setPanelCapability(panel.id, "action.timeline") ||
358 !candidate.setPanelContext(panel.id, panel.context))
359 return rejected("editor.action.timeline.workspace-conflict",
360 "Could not install the action editor workspace composition");
361 }
362 if (!candidate.activatePanel("action.timeline"))
363 return rejected("editor.action.timeline.workspace-activate-failed",
364 "Could not activate the action timeline panel");
365 workspace = std::move(candidate);
366 return eve::editing::applied<void>();
367}
368
369Result<void> ActionTimelineEditor::commit(action::ActionTimeline candidate, std::string label,
370 std::string mergeKey) {
371 auto valid = candidate.validate();
372 if (!valid)
373 return rejected("editor.action.timeline.candidate-invalid",
374 commonMessage(valid.status(), "Action timeline edit is invalid"));
375 auto beforeValue = target_.timeline().toValue();
376 auto afterValue = candidate.toValue();
377 if (!beforeValue || !afterValue)
378 return rejected("editor.action.timeline.encode-failed", "Could not encode the action timeline edit");
379 auto beforeJson = beforeValue.value().toJson();
380 auto afterJson = afterValue.value().toJson();
381 if (!beforeJson || !afterJson)
382 return rejected("editor.action.timeline.encode-failed", "Could not serialize the action timeline edit");
383 if (beforeJson.value() == afterJson.value()) {
384 return eve::editing::noOp();
385 }
386
387 const std::string operationMergeKey = mergeKey;
388 TransactionSpec transaction;
389 transaction.id = TransactionId("action.timeline.tx." + std::to_string(++transactionSequence_));
390 transaction.label = std::move(label);
391 transaction.target = TargetId(target_.targetId());
392 transaction.baseRevision = target_.revision();
393 transaction.mergeKey = std::move(mergeKey);
394 auto begun = transactions_.begin(std::move(transaction));
395 if (!begun.ok())
396 return eve::editing::failed<void>(begun.code(), RuleId("editor.action.timeline.begin-failed"), "Could not begin action edit");
397
399 operation.type = std::string(kReplaceOperation);
400 operation.inverseType = std::string(kReplaceOperation);
401 operation.target = TargetId(target_.targetId());
402 operation.payload = replacementPayload(afterJson.value());
403 operation.inverse = replacementPayload(beforeJson.value());
404 operation.hasInverse = true;
405 operation.affectedObjects.push_back({operation.target, target_.timeline().actionId.format(), target_.revision()});
406 operation.affectedProperties.push_back("timeline");
407 operation.mergeKey = operationMergeKey;
408 auto appended = transactions_.append(std::move(operation));
409 if (!appended.ok()) {
410 auto discarded = transactions_.discard();
411 (void)discarded;
412 return appended;
413 }
414 auto committed = transactions_.commit();
415 if (!committed.ok())
416 return eve::editing::failed<void>(committed.code(), RuleId("editor.action.timeline.commit-failed"), "Action edit was rejected");
417 return eve::editing::applied<void>();
418}
419
421 action::ActionTimeline candidate = target_.timeline();
422 candidate.tracks.push_back(std::move(track));
423 return commit(std::move(candidate), "Add action track", "action.timeline.track.add");
424}
425
427 action::ActionTimeline candidate = target_.timeline();
428 const auto found = std::find_if(candidate.tracks.begin(), candidate.tracks.end(),
429 [&](const auto& track) { return track.id == trackId; });
430 if (found == candidate.tracks.end())
431 return rejected("editor.action.timeline.track-not-found", "Action track was not found");
432 for (const auto& notify : found->notifies) selection_.erase(notify.id.format());
433 for (const auto& state : found->states) selection_.erase(state.id.format());
434 candidate.tracks.erase(found);
435 return commit(std::move(candidate), "Delete action track", "action.timeline.track.delete");
436}
437
439 if (label.empty()) return rejected("editor.action.timeline.track-label", "Action track label must not be empty");
440 action::ActionTimeline candidate = target_.timeline();
441 auto* track = findTrack(candidate, trackId);
442 if (!track) return rejected("editor.action.timeline.track-not-found", "Action track was not found");
443 track->label = std::move(label);
444 return commit(std::move(candidate), "Rename action track", "action.timeline.track.rename");
445}
446
448 if (time <= Duration::zero() || time >= target_.timeline().duration)
449 return rejected("editor.action.timeline.section-split-range",
450 "Physical section split must be strictly inside the timeline");
451 action::ActionTimeline candidate = target_.timeline();
452 if (std::find(candidate.splitTimestamps.begin(), candidate.splitTimestamps.end(), time) !=
453 candidate.splitTimestamps.end())
454 return rejected("editor.action.timeline.section-split-duplicate",
455 "Physical section split already exists at this time");
456 candidate.splitTimestamps.push_back(time);
457 std::sort(candidate.splitTimestamps.begin(), candidate.splitTimestamps.end());
458 return commit(std::move(candidate), "Add physical section split", "action.timeline.section-split.add");
459}
460
462 if (index >= target_.timeline().splitTimestamps.size())
463 return rejected("editor.action.timeline.section-split-not-found", "Physical section split was not found");
464 if (time <= Duration::zero() || time >= target_.timeline().duration)
465 return rejected("editor.action.timeline.section-split-range",
466 "Physical section split must be strictly inside the timeline");
467 action::ActionTimeline candidate = target_.timeline();
468 candidate.splitTimestamps[index] = time;
469 std::sort(candidate.splitTimestamps.begin(), candidate.splitTimestamps.end());
470 return commit(std::move(candidate), "Move physical section split", "action.timeline.section-split.move");
471}
472
474 if (index >= target_.timeline().splitTimestamps.size())
475 return rejected("editor.action.timeline.section-split-not-found", "Physical section split was not found");
476 action::ActionTimeline candidate = target_.timeline();
477 candidate.splitTimestamps.erase(candidate.splitTimestamps.begin() + static_cast<std::ptrdiff_t>(index));
478 return commit(std::move(candidate), "Remove physical section split", "action.timeline.section-split.remove");
479}
480
482 action::ActionTimeline candidate = target_.timeline();
483 candidate.animationSections.push_back(std::move(section));
484 sortAnimationSections(candidate);
485 return commit(std::move(candidate), "Add montage animation section", "action.timeline.animation-section.add");
486}
487
489 action::ActionTimeline candidate = target_.timeline();
490 auto found = std::find_if(candidate.animationSections.begin(), candidate.animationSections.end(),
491 [&](const auto& section) { return section.id == sectionId; });
492 if (found == candidate.animationSections.end())
493 return rejected("editor.action.timeline.animation-section-not-found", "Animation section was not found");
494 auto start = found->start.tryAdd(delta);
495 auto end = found->end.tryAdd(delta);
496 if (!start || !end) return rejected("editor.action.timeline.time-overflow", "Animation section time overflowed");
497 found->start = std::move(start).takeValue();
498 found->end = std::move(end).takeValue();
499 sortAnimationSections(candidate);
500 return commit(std::move(candidate), "Move montage animation section", "action.timeline.animation-section.move");
501}
502
504 Duration end, Duration blendIn) {
505 const auto found =
506 std::find_if(target_.timeline().animationSections.begin(), target_.timeline().animationSections.end(),
507 [&](const auto& section) { return section.id == sectionId; });
508 if (found == target_.timeline().animationSections.end())
509 return rejected("editor.action.timeline.animation-section-not-found", "Animation section was not found");
510 return editAnimationSection(sectionId, start, end, blendIn, found->animationUri);
511}
512
514 Duration blendIn, std::string animationUri) {
515 action::ActionTimeline candidate = target_.timeline();
516 auto found = std::find_if(candidate.animationSections.begin(), candidate.animationSections.end(),
517 [&](const auto& section) { return section.id == sectionId; });
518 if (found == candidate.animationSections.end())
519 return rejected("editor.action.timeline.animation-section-not-found", "Animation section was not found");
520 found->start = start;
521 found->end = end;
522 found->blendIn = blendIn;
523 found->animationUri = std::move(animationUri);
524 sortAnimationSections(candidate);
525 return commit(std::move(candidate), "Resize montage animation section", "action.timeline.animation-section.resize");
526}
527
529 Duration sourceEnd,
530 action::ActionBlendCurve blendCurve) {
531 action::ActionTimeline candidate = target_.timeline();
532 auto found = std::find_if(candidate.animationSections.begin(), candidate.animationSections.end(),
533 [&](const auto& section) { return section.id == sectionId; });
534 if (found == candidate.animationSections.end())
535 return rejected("editor.action.timeline.animation-section-not-found", "Animation section was not found");
536 found->sourceStart = sourceStart;
537 found->sourceEnd = sourceEnd;
538 found->blendCurve = blendCurve;
539 return commit(std::move(candidate), "Edit montage clip trim", "action.timeline.animation-section.trim");
540}
541
543 action::ActionTimeline candidate = target_.timeline();
544 auto found = std::find_if(candidate.animationSections.begin(), candidate.animationSections.end(),
545 [&](const auto& value) { return value.id == section.id; });
546 if (found == candidate.animationSections.end())
547 return rejected("editor.action.timeline.animation-section-not-found", "Animation section was not found");
548 *found = std::move(section);
549 sortAnimationSections(candidate);
550 return commit(std::move(candidate), "Edit montage animation section", "action.timeline.animation-section.edit");
551}
552
554 action::ActionTimeline candidate = target_.timeline();
555 const auto before = candidate.animationSections.size();
556 std::erase_if(candidate.animationSections, [&](const auto& section) { return section.id == sectionId; });
557 if (candidate.animationSections.size() == before)
558 return rejected("editor.action.timeline.animation-section-not-found", "Animation section was not found");
559 return commit(std::move(candidate), "Remove montage animation section", "action.timeline.animation-section.remove");
560}
561
563 action::ActionTimeline candidate = target_.timeline();
564 auto* track = findTrack(candidate, trackId);
565 if (!track) return rejected("editor.action.timeline.track-not-found", "Action track was not found");
566 if (track->locked) return rejected("editor.action.timeline.track-locked", "Action track is locked");
567 track->notifies.push_back(std::move(notify));
568 sortTrack(*track);
569 return commit(std::move(candidate), "Add action notify", "action.timeline.notify.add");
570}
571
573 action::ActionTimeline candidate = target_.timeline();
574 auto* track = findTrack(candidate, trackId);
575 if (!track) return rejected("editor.action.timeline.track-not-found", "Action track was not found");
576 if (track->locked) return rejected("editor.action.timeline.track-locked", "Action track is locked");
577 track->states.push_back(std::move(state));
578 sortTrack(*track);
579 return commit(std::move(candidate), "Add action notify state", "action.timeline.state.add");
580}
581
583 action::ActionTimeline candidate = target_.timeline();
584 for (auto& track : candidate.tracks) {
585 for (auto& notify : track.notifies) {
586 if (notify.id != itemId) continue;
587 if (track.locked) return rejected("editor.action.timeline.track-locked", "Action track is locked");
588 auto moved = notify.time.tryAdd(delta);
589 if (!moved) return rejected("editor.action.timeline.time-overflow", "Notify time overflowed");
590 notify.time = std::move(moved).takeValue();
591 sortTrack(track);
592 return commit(std::move(candidate), "Move action notify", "action.timeline.item.move");
593 }
594 for (auto& state : track.states) {
595 if (state.id != itemId) continue;
596 if (track.locked) return rejected("editor.action.timeline.track-locked", "Action track is locked");
597 auto start = state.start.tryAdd(delta);
598 auto end = state.end.tryAdd(delta);
599 if (!start || !end) return rejected("editor.action.timeline.time-overflow", "Notify-state time overflowed");
600 state.start = std::move(start).takeValue();
601 state.end = std::move(end).takeValue();
602 sortTrack(track);
603 return commit(std::move(candidate), "Move action notify state", "action.timeline.item.move");
604 }
605 }
606 return rejected("editor.action.timeline.item-not-found", "Timeline item was not found");
607}
608
610 if (start < Duration::zero() || end < start || end > target_.timeline().duration)
611 return rejected("editor.action.timeline.state-range", "Notify-state range is outside the timeline");
612 action::ActionTimeline candidate = target_.timeline();
613 for (auto& track : candidate.tracks) {
614 auto found = std::find_if(track.states.begin(), track.states.end(),
615 [&](const auto& value) { return value.id == itemId; });
616 if (found == track.states.end()) continue;
617 if (track.locked) return rejected("editor.action.timeline.track-locked", "Action track is locked");
618 found->start = start;
619 found->end = end;
620 sortTrack(track);
621 return commit(std::move(candidate), "Resize action notify state", "action.timeline.state.resize");
622 }
623 return rejected("editor.action.timeline.state-not-found", "Notify state was not found");
624}
625
627 if (!type.isValid()) return rejected("editor.action.timeline.type-invalid", "Timeline item type is invalid");
628 action::ActionTimeline candidate = target_.timeline();
629 for (auto& track : candidate.tracks) {
630 for (auto& notify : track.notifies) {
631 if (notify.id != itemId) continue;
632 if (track.locked) return rejected("editor.action.timeline.track-locked", "Action track is locked");
633 notify.type = std::move(type);
634 notify.payload = std::move(payload);
635 return commit(std::move(candidate), "Edit action notify", "action.timeline.item.properties");
636 }
637 for (auto& state : track.states) {
638 if (state.id != itemId) continue;
639 if (track.locked) return rejected("editor.action.timeline.track-locked", "Action track is locked");
640 state.type = std::move(type);
641 state.payload = std::move(payload);
642 return commit(std::move(candidate), "Edit action notify state", "action.timeline.item.properties");
643 }
644 }
645 return rejected("editor.action.timeline.item-not-found", "Timeline item was not found");
646}
647
650 for (const auto& track : target_.timeline().tracks) {
651 const auto found = std::find_if(track.states.begin(), track.states.end(),
652 [&](const auto& state) { return state.id == itemId; });
653 if (found == track.states.end()) continue;
654 if (found->type.format() != "presentation:parameter-curve")
655 return rejected("editor.action.timeline.parameter-type", "Timeline item is not a parameter curve");
657 if (!binding)
658 return rejected("editor.action.timeline.parameter-invalid", commonMessage(binding.status(),
659 "Parameter curve is invalid"));
660 binding.value().keys.push_back(key);
661 std::sort(binding.value().keys.begin(), binding.value().keys.end(),
662 [](const auto& left, const auto& right) { return left.time < right.time; });
663 auto payload = binding.value().toPayload(found->payload);
665 if (!validated)
666 return rejected("editor.action.timeline.parameter-key-invalid",
667 commonMessage(validated.status(), "Parameter key is invalid"));
668 return updateItem(itemId, found->type, std::move(payload));
669 }
670 return rejected("editor.action.timeline.state-not-found", "Parameter curve was not found");
671}
672
675 for (const auto& track : target_.timeline().tracks) {
676 const auto found = std::find_if(track.states.begin(), track.states.end(),
677 [&](const auto& state) { return state.id == itemId; });
678 if (found == track.states.end()) continue;
679 if (found->type.format() != "presentation:parameter-curve")
680 return rejected("editor.action.timeline.parameter-type", "Timeline item is not a parameter curve");
682 if (!binding)
683 return rejected("editor.action.timeline.parameter-invalid", commonMessage(binding.status(),
684 "Parameter curve is invalid"));
685 if (index >= binding.value().keys.size())
686 return rejected("editor.action.timeline.parameter-key-not-found", "Parameter key index is unavailable");
687 binding.value().keys[index] = key;
688 auto payload = binding.value().toPayload(found->payload);
690 if (!validated)
691 return rejected("editor.action.timeline.parameter-key-invalid",
692 commonMessage(validated.status(), "Parameter key is invalid"));
693 return updateItem(itemId, found->type, std::move(payload));
694 }
695 return rejected("editor.action.timeline.state-not-found", "Parameter curve was not found");
696}
697
699 for (const auto& track : target_.timeline().tracks) {
700 const auto found = std::find_if(track.states.begin(), track.states.end(),
701 [&](const auto& state) { return state.id == itemId; });
702 if (found == track.states.end()) continue;
703 if (found->type.format() != "presentation:parameter-curve")
704 return rejected("editor.action.timeline.parameter-type", "Timeline item is not a parameter curve");
706 if (!binding)
707 return rejected("editor.action.timeline.parameter-invalid", commonMessage(binding.status(),
708 "Parameter curve is invalid"));
709 if (index == 0 || index + 1 >= binding.value().keys.size())
710 return rejected("editor.action.timeline.parameter-endpoint", "Parameter curve endpoints cannot be removed");
711 binding.value().keys.erase(binding.value().keys.begin() + static_cast<std::ptrdiff_t>(index));
712 auto payload = binding.value().toPayload(found->payload);
714 if (!validated)
715 return rejected("editor.action.timeline.parameter-key-invalid",
716 commonMessage(validated.status(), "Parameter key removal is invalid"));
717 return updateItem(itemId, found->type, std::move(payload));
718 }
719 return rejected("editor.action.timeline.state-not-found", "Parameter curve was not found");
720}
721
724 if (!type.isValid()) return rejected("editor.action.timeline.type-invalid", "Timeline item type is invalid");
725 if (start < Duration::zero() || end < start || end > target_.timeline().duration)
726 return rejected("editor.action.timeline.item-range", "Timeline item range is outside the timeline");
727 action::ActionTimeline candidate = target_.timeline();
728 for (auto& track : candidate.tracks) {
729 for (auto& notify : track.notifies) {
730 if (notify.id != itemId) continue;
731 if (track.locked) return rejected("editor.action.timeline.track-locked", "Action track is locked");
732 if (start != end)
733 return rejected("editor.action.timeline.notify-range", "Instant notify start and end must match");
734 notify.time = start;
735 notify.type = std::move(type);
736 notify.payload = std::move(payload);
737 sortTrack(track);
738 return commit(std::move(candidate), "Edit action notify", "action.timeline.item.edit");
739 }
740 for (auto& state : track.states) {
741 if (state.id != itemId) continue;
742 if (track.locked) return rejected("editor.action.timeline.track-locked", "Action track is locked");
743 state.start = start;
744 state.end = end;
745 state.type = std::move(type);
746 state.payload = std::move(payload);
747 sortTrack(track);
748 return commit(std::move(candidate), "Edit action notify state", "action.timeline.item.edit");
749 }
750 }
751 return rejected("editor.action.timeline.item-not-found", "Timeline item was not found");
752}
753
755 action::ActionTimeline candidate = target_.timeline();
756 const auto section = std::find_if(candidate.animationSections.begin(), candidate.animationSections.end(),
757 [&](const auto& value) { return value.id == itemId; });
758 if (section != candidate.animationSections.end()) {
759 candidate.animationSections.erase(section);
760 selection_.erase(itemId.format());
761 return commit(std::move(candidate), "Delete montage animation section",
762 "action.timeline.animation-section.delete");
763 }
764 for (auto& track : candidate.tracks) {
765 const auto notify = std::find_if(track.notifies.begin(), track.notifies.end(),
766 [&](const auto& value) { return value.id == itemId; });
767 if (notify != track.notifies.end()) {
768 if (track.locked) return rejected("editor.action.timeline.track-locked", "Action track is locked");
769 track.notifies.erase(notify);
770 selection_.erase(itemId.format());
771 return commit(std::move(candidate), "Delete action notify", "action.timeline.item.delete");
772 }
773 const auto state = std::find_if(track.states.begin(), track.states.end(),
774 [&](const auto& value) { return value.id == itemId; });
775 if (state != track.states.end()) {
776 if (track.locked) return rejected("editor.action.timeline.track-locked", "Action track is locked");
777 track.states.erase(state);
778 selection_.erase(itemId.format());
779 return commit(std::move(candidate), "Delete action notify state", "action.timeline.item.delete");
780 }
781 }
782 return rejected("editor.action.timeline.item-not-found", "Timeline item was not found");
783}
784
786 action::ActionTimeline candidate = target_.timeline();
787 for (auto& track : candidate.tracks) {
788 for (auto& notify : track.notifies) {
789 if (notify.id != itemId) continue;
790 if (track.locked) return rejected("editor.action.timeline.track-locked", "Action track is locked");
791 if (notify.enabled == enabled) return eve::editing::noOp();
792 notify.enabled = enabled;
793 return commit(std::move(candidate), enabled ? "Enable action notify" : "Disable action notify",
794 "action.timeline.item.enabled");
795 }
796 for (auto& state : track.states) {
797 if (state.id != itemId) continue;
798 if (track.locked) return rejected("editor.action.timeline.track-locked", "Action track is locked");
799 if (state.enabled == enabled) return eve::editing::noOp();
800 state.enabled = enabled;
801 return commit(std::move(candidate), enabled ? "Enable action notify state" : "Disable action notify state",
802 "action.timeline.item.enabled");
803 }
804 }
805 return rejected("editor.action.timeline.item-not-found", "Timeline item was not found");
806}
807
809 action::ActionTimeline candidate = target_.timeline();
810 auto* track = findTrack(candidate, trackId);
811 if (!track) return rejected("editor.action.timeline.track-not-found", "Action track was not found");
812 track->muted = muted;
813 return commit(std::move(candidate), muted ? "Mute action track" : "Unmute action track",
814 "action.timeline.track.mute");
815}
816
818 action::ActionTimeline candidate = target_.timeline();
819 auto* track = findTrack(candidate, trackId);
820 if (!track) return rejected("editor.action.timeline.track-not-found", "Action track was not found");
821 track->locked = locked;
822 return commit(std::move(candidate), locked ? "Lock action track" : "Unlock action track",
823 "action.timeline.track.lock");
824}
825
827 if (start < Duration::zero() || end < start || end > target_.timeline().duration)
828 return eve::editing::failed<std::size_t>(EditorStatus::Rejected,
829 RuleId("editor.action.timeline.selection-range"),
830 "Selection range is outside the timeline");
831 selection_.clear();
832 for (const auto& section : target_.timeline().animationSections)
833 if (section.end >= start && section.start <= end) selection_.insert(section.id.format());
834 for (const auto& track : target_.timeline().tracks) {
835 for (const auto& notify : track.notifies)
836 if (notify.time >= start && notify.time <= end) selection_.insert(notify.id.format());
837 for (const auto& state : track.states)
838 if (state.end >= start && state.start <= end) selection_.insert(state.id.format());
839 }
840 return eve::editing::applied<std::size_t>(selection_.size());
841}
842
844 bool found = std::any_of(target_.timeline().animationSections.begin(), target_.timeline().animationSections.end(),
845 [&](const auto& section) { return section.id == itemId; });
846 for (const auto& track : target_.timeline().tracks) {
847 found = found || std::any_of(track.notifies.begin(), track.notifies.end(),
848 [&](const auto& item) { return item.id == itemId; });
849 found = found || std::any_of(track.states.begin(), track.states.end(),
850 [&](const auto& item) { return item.id == itemId; });
851 }
852 if (!found) return rejected("editor.action.timeline.item-not-found", "Timeline item was not found");
853 if (!additive) selection_.clear();
854 selection_.insert(itemId.format());
855 return eve::editing::applied<void>();
856}
857
858std::vector<LogicalId> ActionTimelineEditor::selectedItemIds() const {
859 std::vector<LogicalId> result;
860 result.reserve(selection_.size());
861 for (const auto& value : selection_) {
862 auto parsed = LogicalId::parse(value);
863 if (parsed) result.push_back(std::move(*parsed));
864 }
865 return result;
866}
867
869 clipboard_->clear();
870 for (const auto& section : target_.timeline().animationSections)
871 if (selection_.contains(section.id.format()))
872 clipboard_->items_.push_back({ActionTimelineClipboard::Item::Kind::AnimationSection, {}, section, {}, {}});
873 for (const auto& track : target_.timeline().tracks) {
874 for (const auto& notify : track.notifies)
875 if (selection_.contains(notify.id.format()))
876 clipboard_->items_.push_back({ActionTimelineClipboard::Item::Kind::Notify, track.id, {}, notify, {}});
877 for (const auto& state : track.states)
878 if (selection_.contains(state.id.format()))
879 clipboard_->items_.push_back(
880 {ActionTimelineClipboard::Item::Kind::NotifyState, track.id, {}, {}, state});
881 }
882 return eve::editing::applied<std::size_t>(clipboard_->items_.size());
883}
884
885LogicalId ActionTimelineEditor::copiedId(const LogicalId& source, const action::ActionTimeline& candidate) {
886 while (true) {
887 const std::string name = std::string(source.name()) + ".copy." + std::to_string(++copySequence_);
888 auto id = LogicalId::fromParts(source.namespaceName(), name);
889 if (id && !containsId(candidate, *id)) return std::move(*id);
890 }
891}
892
893Result<std::size_t> ActionTimelineEditor::paste(Duration offset) { return pasteItems(offset, nullptr); }
894
895Result<std::size_t> ActionTimelineEditor::pasteToTrack(const LogicalId& trackId, Duration offset) {
896 return pasteItems(offset, &trackId);
897}
898
899Result<std::size_t> ActionTimelineEditor::pasteItems(Duration offset, const LogicalId* destinationTrack) {
900 if (clipboard_->items_.empty())
901 return eve::editing::failed<std::size_t>(EditorStatus::Rejected,
902 RuleId("editor.action.timeline.clipboard-empty"),
903 "Action timeline item clipboard is empty");
904 action::ActionTimeline candidate = target_.timeline();
905 std::set<std::string> pastedSelection;
906 for (const auto& item : clipboard_->items_) {
907 if (item.kind == ActionTimelineClipboard::Item::Kind::AnimationSection) {
908 auto copy = item.animationSection;
909 copy.id = copiedId(copy.id, candidate);
910 auto start = copy.start.tryAdd(offset);
911 auto end = copy.end.tryAdd(offset);
912 if (!start || !end)
913 return eve::editing::failed<std::size_t>(EditorStatus::Rejected,
914 RuleId("editor.action.timeline.time-overflow"),
915 "Pasted animation-section time overflowed");
916 copy.start = std::move(start).takeValue();
917 copy.end = std::move(end).takeValue();
918 pastedSelection.insert(copy.id.format());
919 candidate.animationSections.push_back(std::move(copy));
920 std::sort(candidate.animationSections.begin(), candidate.animationSections.end(),
921 [](const auto& left, const auto& right) { return left.start < right.start; });
922 continue;
923 }
924 auto* track = findTrack(candidate, destinationTrack ? *destinationTrack : item.trackId);
925 if (!track)
926 return eve::editing::failed<std::size_t>(EditorStatus::Conflict,
927 RuleId("editor.action.timeline.track-not-found"),
928 "Paste target track does not exist");
929 if (track->locked)
930 return eve::editing::failed<std::size_t>(
931 EditorStatus::Rejected, RuleId("editor.action.timeline.track-locked"), "Clipboard track is locked");
932 if (item.kind == ActionTimelineClipboard::Item::Kind::NotifyState) {
933 auto copy = item.notifyState;
934 copy.id = copiedId(copy.id, candidate);
935 auto start = copy.start.tryAdd(offset);
936 auto end = copy.end.tryAdd(offset);
937 if (!start || !end)
938 return eve::editing::failed<std::size_t>(EditorStatus::Rejected,
939 RuleId("editor.action.timeline.time-overflow"),
940 "Pasted notify-state time overflowed");
941 copy.start = std::move(start).takeValue();
942 copy.end = std::move(end).takeValue();
943 pastedSelection.insert(copy.id.format());
944 track->states.push_back(std::move(copy));
945 } else {
946 auto copy = item.notify;
947 copy.id = copiedId(copy.id, candidate);
948 auto time = copy.time.tryAdd(offset);
949 if (!time)
950 return eve::editing::failed<std::size_t>(EditorStatus::Rejected,
951 RuleId("editor.action.timeline.time-overflow"),
952 "Pasted notify time overflowed");
953 copy.time = std::move(time).takeValue();
954 pastedSelection.insert(copy.id.format());
955 track->notifies.push_back(std::move(copy));
956 }
957 sortTrack(*track);
958 }
959 const std::size_t count = pastedSelection.size();
960 auto committed = commit(std::move(candidate), "Paste action timeline items", "action.timeline.item.paste");
961 if (!committed.ok())
962 return eve::editing::failed<std::size_t>(committed.code(), RuleId("editor.action.timeline.paste-failed"),
963 "Could not paste action timeline items");
964 selection_ = std::move(pastedSelection);
965 return eve::editing::applied<std::size_t>(count);
966}
967
968Result<void> ActionTimelineEditor::copyTrack(const LogicalId& trackId) {
969 const auto* track = findTrack(target_.timeline(), trackId);
970 if (!track) return rejected("editor.action.timeline.track-not-found", "Action track was not found");
971 clipboard_->clear();
972 clipboard_->track_ = *track;
973 return eve::editing::applied<void>();
974}
975
976Result<LogicalId> ActionTimelineEditor::pasteTrack() {
977 if (!clipboard_->track_)
978 return eve::editing::failed<LogicalId>(EditorStatus::Rejected,
979 RuleId("editor.action.timeline.track-clipboard-empty"),
980 "Action timeline track clipboard is empty");
981 action::ActionTimeline candidate = target_.timeline();
982 auto track = *clipboard_->track_;
983 track.id = copiedId(track.id, candidate);
984 track.label += " Copy";
985 const LogicalId pastedId = track.id;
986 candidate.tracks.push_back(std::move(track));
987 auto& pasted = candidate.tracks.back();
988 for (auto& notify : pasted.notifies) notify.id = copiedId(notify.id, candidate);
989 for (auto& state : pasted.states) state.id = copiedId(state.id, candidate);
990 sortTrack(pasted);
991 auto committed = commit(std::move(candidate), "Paste action timeline track", "action.timeline.track.paste");
992 if (!committed.ok()) return Result<LogicalId>::failure(committed.status());
993 selection_.clear();
994 return eve::editing::applied<LogicalId>(pastedId);
995}
996
997Result<void> ActionTimelineEditor::deleteSelection() {
998 if (selection_.empty()) return rejected("editor.action.timeline.selection-empty", "No timeline items are selected");
999 action::ActionTimeline candidate = target_.timeline();
1000 std::erase_if(candidate.animationSections,
1001 [&](const auto& section) { return selection_.contains(section.id.format()); });
1002 for (auto& track : candidate.tracks) {
1003 const bool touchesLocked =
1004 track.locked && (std::any_of(track.notifies.begin(), track.notifies.end(),
1005 [&](const auto& item) { return selection_.contains(item.id.format()); }) ||
1006 std::any_of(track.states.begin(), track.states.end(),
1007 [&](const auto& item) { return selection_.contains(item.id.format()); }));
1008 if (touchesLocked)
1009 return rejected("editor.action.timeline.track-locked", "Selection contains an item on a locked track");
1010 std::erase_if(track.notifies, [&](const auto& item) { return selection_.contains(item.id.format()); });
1011 std::erase_if(track.states, [&](const auto& item) { return selection_.contains(item.id.format()); });
1012 }
1013 auto result = commit(std::move(candidate), "Delete action timeline selection", "action.timeline.item.delete");
1014 if (result.ok()) selection_.clear();
1015 return result;
1016}
1017
1018Result<TransactionReceipt> ActionTimelineEditor::undo() { return transactions_.undo(); }
1019
1020Result<TransactionReceipt> ActionTimelineEditor::redo() { return transactions_.redo(); }
1021
1022Result<void> ActionTimelineEditor::stop() {
1023 auto stopped = seek(Duration::zero());
1024 if (stopped.ok()) playing_ = false;
1025 return stopped;
1026}
1027
1028Result<Duration> ActionTimelineEditor::frameStepTarget(std::int64_t frames, double frameRate) const {
1029 if (!std::isfinite(frameRate) || frameRate <= 0.0)
1030 return eve::editing::failed<Duration>(EditorStatus::Rejected,
1031 RuleId("editor.action.timeline.frame-rate"),
1032 "Preview frame rate must be finite and positive");
1033 const long double seconds = static_cast<long double>(previewTime_.nanoseconds()) / 1'000'000'000.0L +
1034 static_cast<long double>(frames) / static_cast<long double>(frameRate);
1035 const long double maximum = static_cast<long double>(target_.timeline().duration.nanoseconds()) /
1036 1'000'000'000.0L;
1037 auto target = Duration::fromSeconds(static_cast<double>(std::clamp(seconds, 0.0L, maximum)));
1038 if (!target)
1039 return eve::editing::failed<Duration>(EditorStatus::Rejected,
1040 RuleId("editor.action.timeline.frame-step-overflow"),
1041 "Preview frame step is outside the duration domain");
1042 return eve::editing::applied<Duration>(std::move(target).takeValue());
1043}
1044
1045Result<void> ActionTimelineEditor::stepFrames(std::int64_t frames, double frameRate) {
1046 auto target = frameStepTarget(frames, frameRate);
1047 if (!target.ok()) return Result<void>::failure(target.status());
1048 auto stepped = seek(target.value());
1049 if (stepped.ok()) playing_ = false;
1050 return stepped;
1051}
1052
1053Result<void> ActionTimelineEditor::jumpToEnd() {
1054 auto jumped = seek(target_.timeline().duration);
1055 if (jumped.ok()) playing_ = false;
1056 return jumped;
1057}
1058
1059Result<void> ActionTimelineEditor::seek(Duration time) {
1060 if (time < Duration::zero() || time > target_.timeline().duration)
1061 return rejected("editor.action.timeline.seek-range", "Preview seek is outside the timeline");
1062 previewTime_ = time;
1063 previewStarted_ = false;
1064 previewEvents_.clear();
1065 return eve::editing::applied<void>();
1066}
1067
1068Result<std::size_t> ActionTimelineEditor::update(Duration delta) {
1069 auto plan = planPreview(delta);
1070 if (!plan.ok())
1071 return eve::editing::failed<std::size_t>(plan.code(), RuleId("editor.action.timeline.preview-plan"),
1072 "Could not prepare preview advance");
1073 if (plan.code() == EditorStatus::NoOp) {
1074 previewEvents_.clear();
1075 return Result<std::size_t>::success(0, Status::success(EditorStatus::NoOp));
1076 }
1077 return applyPreviewPlan(std::move(plan).takeValue());
1078}
1079
1080Result<ActionTimelinePreviewPlan> ActionTimelineEditor::planPreview(Duration delta) const {
1081 if (!playing_) {
1083 plan.timelineRevision = target_.revision();
1084 plan.previous = previewTime_;
1085 plan.current = previewTime_;
1086 return Result<ActionTimelinePreviewPlan>::success(std::move(plan), Status::success(EditorStatus::NoOp));
1087 }
1088 if (delta < Duration::zero())
1089 return eve::editing::failed<ActionTimelinePreviewPlan>(EditorStatus::Rejected,
1090 RuleId("editor.action.timeline.preview-delta"),
1091 "Preview delta must be non-negative");
1092 auto next = previewTime_.tryAdd(delta);
1093 if (!next)
1094 return eve::editing::failed<ActionTimelinePreviewPlan>(
1095 EditorStatus::Rejected, RuleId("editor.action.timeline.preview-overflow"), "Preview time overflowed");
1096 Duration current = std::move(next).takeValue();
1097 if (current > target_.timeline().duration) current = target_.timeline().duration;
1098 auto sampled = target_.timeline().sample(previewTime_, current, !previewStarted_);
1099 if (!sampled)
1100 return eve::editing::failed<ActionTimelinePreviewPlan>(
1101 EditorStatus::Rejected, RuleId("editor.action.timeline.preview-invalid"),
1102 commonMessage(sampled.status(), "Could not sample action timeline"));
1104 plan.timelineRevision = target_.revision();
1105 plan.previous = previewTime_;
1106 plan.current = current;
1107 plan.includeStart = !previewStarted_;
1108 plan.reachesEnd = current == target_.timeline().duration;
1109 plan.events = std::move(sampled).takeValue();
1110 return eve::editing::applied<ActionTimelinePreviewPlan>(std::move(plan));
1111}
1112
1113Result<std::size_t> ActionTimelineEditor::applyPreviewPlan(ActionTimelinePreviewPlan plan) {
1114 if (!playing_ || plan.timelineRevision != target_.revision() || plan.previous != previewTime_ ||
1115 plan.includeStart == previewStarted_)
1116 return eve::editing::failed<std::size_t>(EditorStatus::Conflict,
1117 RuleId("editor.action.timeline.preview-plan-stale"),
1118 "Prepared preview advance no longer matches transport state");
1119 auto expected = target_.timeline().sample(plan.previous, plan.current, plan.includeStart);
1120 if (!expected || expected.value() != plan.events ||
1121 plan.reachesEnd != (plan.current == target_.timeline().duration))
1122 return eve::editing::failed<std::size_t>(EditorStatus::Rejected,
1123 RuleId("editor.action.timeline.preview-plan-invalid"),
1124 "Prepared preview advance does not match the authoritative timeline");
1125 previewEvents_ = std::move(plan.events);
1126 previewTime_ = plan.current;
1127 previewStarted_ = true;
1128 if (plan.reachesEnd) playing_ = false;
1129 return eve::editing::applied<std::size_t>(previewEvents_.size());
1130}
1131
1132ActionTimelineDocumentWorkspace::ActionTimelineDocumentWorkspace(DocumentService& documents) : documents_(&documents) {}
1133
1134Result<action::ActionTimeline> ActionTimelineDocumentWorkspace::decode(const EditorValue& content) {
1135 auto timeline = action::ActionTimeline::fromValue(toPresentationValue(content));
1136 if (!timeline)
1137 return eve::editing::failed<action::ActionTimeline>(EditorStatus::Rejected,
1138 RuleId("editor.action.document.invalid-content"),
1139 commonMessage(timeline.status(),
1140 "Montage document content is invalid"));
1141 return eve::editing::applied<action::ActionTimeline>(std::move(timeline).takeValue());
1142}
1143
1144ActionTimelineDocumentWorkspace::Tab* ActionTimelineDocumentWorkspace::find(const DocumentId& document) {
1145 const auto found = std::find_if(tabs_.begin(), tabs_.end(),
1146 [&](const Tab& tab) { return tab.document == document; });
1147 return found == tabs_.end() ? nullptr : &*found;
1148}
1149
1150const ActionTimelineDocumentWorkspace::Tab* ActionTimelineDocumentWorkspace::find(const DocumentId& document) const {
1151 const auto found = std::find_if(tabs_.begin(), tabs_.end(),
1152 [&](const Tab& tab) { return tab.document == document; });
1153 return found == tabs_.end() ? nullptr : &*found;
1154}
1155
1157 std::string resourceUri,
1158 action::ActionTimeline initialTimeline) {
1159 if (key.kind != DocumentKind::Timeline)
1160 return eve::editing::failed<DocumentId>(EditorStatus::Rejected,
1161 RuleId("editor.action.document.kind"),
1162 "Montage workspace only opens Timeline documents");
1163 auto valid = initialTimeline.validate();
1164 if (!valid)
1165 return eve::editing::failed<DocumentId>(EditorStatus::Rejected,
1166 RuleId("editor.action.document.initial-invalid"),
1167 commonMessage(valid.status(), "Initial Montage timeline is invalid"));
1168 auto initial = initialTimeline.toValue();
1169 if (!initial)
1170 return eve::editing::failed<DocumentId>(EditorStatus::Rejected,
1171 RuleId("editor.action.document.initial-encode"),
1172 "Initial Montage timeline could not be encoded");
1173 auto opened = documents_->open(std::move(key), std::move(title), std::move(resourceUri),
1174 toEditorValue(initial.value()));
1175 if (!opened.ok()) return Result<DocumentId>::failure(opened.status());
1176 if (find(opened.value().id)) {
1177 activeDocument_ = opened.value().id;
1178 return eve::editing::applied<DocumentId>(activeDocument_);
1179 }
1180 auto content = documents_->content(opened.value().id);
1181 if (!content.ok()) return Result<DocumentId>::failure(content.status());
1182 auto timeline = decode(content.value());
1183 if (!timeline.ok()) return Result<DocumentId>::failure(timeline.status());
1184
1185 Tab tab;
1186 tab.document = opened.value().id;
1187 tab.editor = std::make_unique<ActionTimelineEditor>(tab.document.value(), std::move(timeline).takeValue(), clipboard_);
1188 tab.synchronizedEditorRevision = tab.editor->target().revision();
1189 tab.savedEditorRevision = tab.editor->target().revision();
1190 activeDocument_ = tab.document;
1191 tabs_.push_back(std::move(tab));
1192 return eve::editing::applied<DocumentId>(activeDocument_);
1193}
1194
1196 if (!find(document))
1197 return eve::editing::failed<void>(EditorStatus::NotFound, RuleId("editor.action.document.not-open"),
1198 "Montage document tab is not open");
1199 activeDocument_ = document;
1200 return eve::editing::applied<void>();
1201}
1202
1203Result<DocumentSnapshot> ActionTimelineDocumentWorkspace::synchronize(Tab& tab) {
1204 auto snapshot = documents_->snapshot(tab.document);
1205 if (!snapshot.ok()) return snapshot;
1206 if (tab.editor->target().revision() == tab.synchronizedEditorRevision) return snapshot;
1207 auto edited = documents_->edit(tab.document, tab.editor->target().snapshotValue(), snapshot.value().revision.edit);
1208 if (edited.ok()) tab.synchronizedEditorRevision = tab.editor->target().revision();
1209 return edited;
1210}
1211
1213 auto* tab = find(document);
1214 if (!tab)
1215 return eve::editing::failed<DocumentSnapshot>(EditorStatus::NotFound,
1216 RuleId("editor.action.document.not-open"),
1217 "Montage document tab is not open");
1218 auto synchronized = synchronize(*tab);
1219 if (!synchronized.ok()) return synchronized;
1220 auto ticket = documents_->requestSave(document);
1221 if (!ticket.ok()) return Result<DocumentSnapshot>::failure(ticket.status());
1222 auto saved = documents_->executeSave(ticket.value());
1223 if (saved.ok()) tab->savedEditorRevision = tab->editor->target().revision();
1224 return saved;
1225}
1226
1228 auto* tab = find(document);
1229 if (!tab)
1230 return eve::editing::failed<DocumentSnapshot>(EditorStatus::NotFound,
1231 RuleId("editor.action.document.not-open"),
1232 "Montage document tab is not open");
1233 auto synchronized = synchronize(*tab);
1234 if (!synchronized.ok()) return synchronized;
1235 auto reconciled = documents_->reconcileExternal(document, [](const EditorValue& content) {
1236 auto timeline = decode(content);
1237 if (!timeline.ok()) return Result<void>::failure(timeline.status());
1238 return eve::editing::applied<void>();
1239 });
1240 if (!reconciled.ok()) return reconciled;
1241 auto content = documents_->content(document);
1242 if (!content.ok()) return Result<DocumentSnapshot>::failure(content.status());
1243 if (content.value() == tab->editor->target().snapshotValue()) return reconciled;
1244 auto loaded = tab->editor->reloadDocument(content.value());
1245 if (!loaded.ok()) return Result<DocumentSnapshot>::failure(loaded.status());
1246 tab->synchronizedEditorRevision = tab->editor->target().revision();
1247 tab->savedEditorRevision = tab->editor->target().revision();
1248 return reconciled;
1249}
1250
1253 const auto found = std::find_if(tabs_.begin(), tabs_.end(),
1254 [&](const Tab& tab) { return tab.document == document; });
1255 if (found == tabs_.end())
1256 return eve::editing::failed<void>(EditorStatus::NotFound, RuleId("editor.action.document.not-open"),
1257 "Montage document tab is not open");
1258 auto snapshot = documents_->snapshot(document);
1259 if (!snapshot.ok()) return Result<void>::failure(snapshot.status());
1260 const bool dirty = found->editor->target().revision() != found->savedEditorRevision || snapshot.value().dirty();
1262 return eve::editing::failed<void>(EditorStatus::Conflict, RuleId("editor.action.document.unsaved"),
1263 "Montage document has unsaved edits");
1264 auto closed = documents_->close(document);
1265 if (!closed.ok()) return closed;
1266 const std::size_t index = static_cast<std::size_t>(std::distance(tabs_.begin(), found));
1267 tabs_.erase(found);
1268 if (activeDocument_ == document) {
1269 activeDocument_ = tabs_.empty() ? DocumentId{} : tabs_[std::min(index, tabs_.size() - 1)].document;
1270 }
1271 return eve::editing::applied<void>();
1272}
1273
1275 std::vector<ActionTimelineTabSnapshot> result;
1276 result.reserve(tabs_.size());
1277 for (const auto& tab : tabs_) {
1278 auto snapshot = documents_->snapshot(tab.document);
1279 if (!snapshot.ok())
1280 return Result<std::vector<ActionTimelineTabSnapshot>>::failure(snapshot.status());
1281 result.push_back({snapshot.value(), tab.document == activeDocument_,
1282 snapshot.value().dirty() ||
1283 tab.editor->target().revision() != tab.savedEditorRevision});
1284 }
1285 return eve::editing::applied<std::vector<ActionTimelineTabSnapshot>>(std::move(result));
1286}
1287
1289 auto* tab = find(activeDocument_);
1290 return tab ? tab->editor.get() : nullptr;
1291}
1292
1293} // namespace eve::editor
LogicalId target
ActionParameterOperation operation
double value
Typed continuous parameter-curve action block.
UI-neutral transactional action timeline editor and preview model.
LogicalId trackId
Value::Object payload
bool locked
Duration start
LogicalId itemId
float duration
std::string descriptor
std::string title
std::string label
std::string message
float maximum[3]
float minimum[3]
std::uint32_t key
wgpu::PopErrorScopeStatus status
HexVec3 left
HexVec3 right
std::string text
size_t offset
std::vector< Track > tracks
std::string name
bool valid
std::vector< std::int32_t > order
eve::action::ActionVfxBinding binding
bool dirty
std::string path
Definition PlayHost.cpp:110
std::string id
Definition PlayHost.cpp:108
std::string uri
std::uint32_t targetId
bool found
double current
std::uint32_t count
uint32_t index
const UnitySourceAsset & source
const VegetationPresetContext & context
static Diagnostic error(DiagnosticCode code, std::string message, std::string path={}, DiagnosticDetails details={}, std::string source={})
Construct an error diagnostic with the standard error severity.
Definition Diagnostic.h:125
Signed, fixed-resolution simulation duration in nanoseconds.
Definition Time.h:43
Result< Duration > tryAdd(Duration other) const
Add two durations with overflow validation.
Definition Time.cpp:30
static Result< Duration > fromSeconds(double seconds)
Convert finite seconds to the nearest nanosecond.
Definition Time.cpp:16
static constexpr Duration fromNanoseconds(std::int64_t nanoseconds) noexcept
Construct an exact duration from nanoseconds.
Definition Time.h:55
static constexpr Duration zero() noexcept
Return zero duration.
Definition Time.h:60
double seconds() const noexcept
Return this duration as seconds for legacy/presentation APIs.
Definition Time.cpp:28
Human-readable, scoped identifier in the form namespace:name.
Definition Identity.h:411
static std::optional< LogicalId > fromParts(std::string_view namespaceName, std::string_view name)
Builds a logical ID from separate namespace and name components.
Definition Identity.cpp:48
const std::string & format() const noexcept
Returns the canonical namespace:name representation.
Definition Identity.h:436
static std::optional< LogicalId > parse(std::string_view text)
Parses a scoped logical name.
Definition Identity.cpp:36
Move-only operation result carrying either a value or Status.
Definition Result.h:155
static Result success(T value)
Construct a successful result owning value.
Definition Result.h:164
static Result failure(Status status)
Construct a failed result from a structured status.
Definition Result.h:175
static Status success(StatusCode code=StatusCode::Ok)
Construct a successful status with an explicit non-error outcome.
Definition Status.h:81
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
static Result< Value > fromJson(std::string_view json)
Parse one strict JSON value into an owning Value.
Definition Value.cpp:57
std::map< std::string, Value > Object
Definition Value.h:34
Id128 public API.
Definition Identity.h:112
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.
UI-independent property access for an immutable selection snapshot.
Dotted property path (layout.size) compared and persisted as text.
Definition EditingIds.h:78
Deterministic owning value tree shared by authoring hosts.
std::map< std::string, Value > Object
Owning clipboard shared explicitly by one or more action timeline editors.
void clear() noexcept
Remove every copied item and track.
bool empty() const noexcept
Whether neither item nor track content is available.
Result< DocumentSnapshot > reconcile(const DocumentId &document)
Reconcile a tab with external persistence, rejecting dirty conflicts without data loss.
Result< std::vector< ActionTimelineTabSnapshot > > tabs() const
Ordered immutable tab projections for rendering a document bar.
Result< void > activate(const DocumentId &document)
Activate an already-open tab.
ActionTimelineEditor * activeEditor() noexcept
Borrow the active editor.
Result< void > close(const DocumentId &document, ActionTimelineCloseMode mode)
Close a tab, rejecting unsaved work unless explicit discard is requested.
Result< DocumentSnapshot > save(const DocumentId &document)
Save one exact canonical editor revision through DocumentService CAS persistence.
Result< DocumentId > open(DocumentKey key, std::string title, std::string resourceUri, action::ActionTimeline initialTimeline)
Open or activate one Montage asset document.
Timeline editing controller shared by developer and in-game editors.
Result< void > editAnimationSection(const LogicalId &sectionId, Duration start, Duration end, Duration blendIn, std::string animationUri)
Atomically edit section timing, blend and animation resource URI.
Result< std::size_t > boxSelect(Duration start, Duration end)
Replace selection with items intersecting a time interval.
Result< void > selectItem(const LogicalId &itemId, bool additive=false)
Select one existing item, optionally preserving the current selection.
Result< void > setSectionSplit(std::size_t index, Duration time)
Move one indexed physical-section split while preserving strict ordering.
std::vector< LogicalId > selectedItemIds() const
Return selected item IDs in lexical order as owning values.
Result< void > addParameterKey(const LogicalId &itemId, action::ActionParameterKey key)
Insert one normalized key into a parameter curve as an undoable edit.
Result< void > resizeState(const LogicalId &itemId, Duration start, Duration end)
Resize one notify-state interval to exact validated boundaries.
Result< void > setTrackLocked(const LogicalId &trackId, bool locked)
Lock/unlock one track through an undoable operation.
Result< void > addAnimationSection(action::ActionAnimationSection section)
Add one objective animation section to the montage lane.
Result< void > removeParameterKey(const LogicalId &itemId, std::size_t index)
Remove one non-endpoint parameter key as an undoable edit.
Result< void > editAnimationSectionFull(action::ActionAnimationSection section)
Replace every editable animation-section field in one undo step.
Result< void > moveItem(const LogicalId &itemId, Duration delta)
Move one notify or state by an exact deterministic delta.
Result< void > removeAnimationSection(const LogicalId &sectionId)
Remove one animation section from the montage lane.
Result< void > removeSectionSplit(std::size_t index)
Remove one indexed physical-section split.
Result< void > removeItem(const LogicalId &itemId)
Remove one notify or state.
Result< void > moveAnimationSection(const LogicalId &sectionId, Duration delta)
Move an animation section while preserving its duration.
Result< void > setItemEnabled(const LogicalId &itemId, bool enabled)
Enable or disable one notify/state through an undoable transaction.
Result< void > configureWorkspace(EditorWorkspace &workspace) const
Atomically install the standard asset/preview/timeline/inspector panel composition.
Result< void > addState(const LogicalId &trackId, action::ActionNotifyState state)
Add a notify state interval to an unlocked track.
Result< void > addNotify(const LogicalId &trackId, action::ActionNotify notify)
Add an instantaneous notify to an unlocked track.
Result< void > removeTrack(const LogicalId &trackId)
Remove one semantic track and all of its items in one undoable transaction.
Result< void > editAnimationSectionSource(const LogicalId &sectionId, Duration sourceStart, Duration sourceEnd, action::ActionBlendCurve blendCurve)
Atomically edit clip-local trim and incoming blend curve.
Result< void > addTrack(action::ActionTrack track)
Add an empty semantic track.
Result< void > setTrackMuted(const LogicalId &trackId, bool muted)
Mute/unmute one track through an undoable operation.
Result< void > editParameterKey(const LogicalId &itemId, std::size_t index, action::ActionParameterKey key)
Replace one indexed parameter key as an undoable edit.
ActionTimelineEditor(std::string targetId, action::ActionTimeline timeline)
Construct a self-contained editor for one canonical timeline.
Result< void > editItem(const LogicalId &itemId, Duration start, Duration end, LogicalId type, Value::Object payload)
Atomically replace one item's timing, type and owning payload.
Result< void > resizeAnimationSection(const LogicalId &sectionId, Duration start, Duration end, Duration blendIn)
Resize an animation section and its incoming blend window.
Result< void > addSectionSplit(Duration time)
Insert one physical-section split strictly inside the montage duration.
Result< void > updateItem(const LogicalId &itemId, LogicalId type, Value::Object payload)
Replace one item's type and owning payload through one undoable transaction.
Result< std::size_t > copySelection()
Copy selected items into an owning editor clipboard.
Result< void > renameTrack(const LogicalId &trackId, std::string label)
Rename one track without changing its stable identity.
Result< void > reloadDocument(const EditorValue &snapshot)
Atomically replace the document snapshot and clear history, selection and preview transport.
Authoritative editor target for one action timeline asset.
static CapabilityId propertyCapabilityId()
Capability identity published by describe() for Inspector property editing.
void * queryCapability(const CapabilityId &capability) override
Query property or snapshot capabilities.
TargetDescriptor describe() const override
Describe the timeline document for tools and automation.
EditorValue snapshotValue() const override
Capture the canonical timeline as an authoring value tree.
PropertyReadResult read(const SelectionSnapshot &selection, const PropertyPath &path) const override
Reads .
std::unique_ptr< IDomainOperationTarget > cloneDomainState() const override
Clone complete target state for atomic compensation.
TargetId targetId() const override
Stable editor target identity.
Result< DomainOperation > makeReset(const SelectionSnapshot &selection, const PropertyPath &path) const override
Make reset.
Result< DomainOperation > makeSet(const SelectionSnapshot &selection, const PropertyPath &path, const EditorValue &value, PropertySetMode mode) const override
Make set.
ActionTimelineTarget(std::string targetId)
Construct an empty, valid seed timeline for automation and inspectors.
Result< void > loadSnapshot(const EditorValue &snapshot)
Replace content when opening a persisted or automation-supplied timeline.
const action::ActionTimeline & timeline() const noexcept
Borrow the authoritative timeline until the next mutation.
eve::Result< eve::Revision > currentRevision(const SelectionSnapshot &selection) const override
Current revision.
Result< void > commitDomainState(std::unique_ptr< IDomainOperationTarget > candidate) override
Atomically publish a validated candidate.
Result< void > applyDomainOperation(const DomainOperation &operation) override
Apply a validated canonical replace operation.
std::uint64_t revision() const override
Monotonic content revision.
PropertySchema schema(const SelectionSnapshot &selection) const override
Schema.
Document coordinator with revision-safe requestSave/executeSave sequencing.
Result< DocumentSnapshot > reconcileExternal(const DocumentId &document)
Reconcile one open document with the latest store value. Clean documents adopt external content; dirt...
Result< DocumentSnapshot > edit(const DocumentId &document, EditorValue content, std::optional< Revision > expectedRevision=std::nullopt)
Replace the working content using an optional expected edit revision.
Result< DocumentSnapshot > snapshot(const DocumentId &document) const
Return one metadata snapshot.
Result< DocumentSnapshot > executeSave(const SaveTicket &ticket)
Execute a captured save ticket without reading newer working content.
Result< EditorValue > content(const DocumentId &document) const
Return the current working content snapshot.
Result< SaveTicket > requestSave(const DocumentId &document)
Capture an exact content/revision pair for a later CAS save.
Result< void > close(const DocumentId &document)
Close and discard in-memory state; persisted content is unchanged.
Result< DocumentSnapshot > open(DocumentKey key, std::string title, std::string resourceUri, EditorValue initialContent={})
Open an asset-backed document, loading existing persisted content when present.
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.
void clear()
Clear pending work and local undo/redo history without mutating the authority target.
bool active() const noexcept override
True while begin/append has an uncommitted transaction.
Result< void > discard() override
Discard the active, not-yet-committed operation list.
Result< void > append(DomainOperation operation) override
Append an operation to the active transaction without applying it.
const char * defaultValue
ActionBlendCurve
Built-in deterministic cross-fade curve evaluated without asset callbacks.
PropertySetMode
PropertySetMode public API.
Result< void > noOp(std::vector< Diagnostic > diagnostics={})
Construct a successful void result for an operation that made no change.
eve::editing::Value EditorValue
Compatibility alias for the UI-independent authoring value tree.
Definition EditorValue.h:8
eve::editing::PropertyType PropertyType
EditorValue toEditorValue(const eve::Value &value)
Compatibility spelling for conversion into the authoring value tree.
eve::editing::PropertyPath PropertyPath
Definition EditorIds.h:37
eve::editing::PropertyDescriptor PropertyDescriptor
eve::editing::TargetId TargetId
Definition EditorIds.h:29
eve::editing::RuleId RuleId
Definition EditorIds.h:32
eve::editing::TransactionSpec TransactionSpec
eve::editing::DomainOperation DomainOperation
eve::editing::TransactionId TransactionId
Definition EditorIds.h:34
eve::editing::DocumentId DocumentId
Definition EditorIds.h:40
ActionTimelineCloseMode
Dirty-document policy applied when closing one Montage tab.
bool enabled
Objective animation segment placed on the montage presentation lane.
Typed interval with explicit enter/exit boundaries.
Instantaneous typed event placed on an action track.
static Result< ActionParameterCurveBinding > fromPayload(const Value::Object &payload)
Decode and validate target, operation and normalized ordered keys.
One normalized, owning parameter-curve key.
Canonical action timeline asset shared by runtime and editor.
Result< Value > toValue() const
Encode the canonical schema as an owning deterministic Value.
static Result< ActionTimeline > fromValue(const Value &value)
Decode and validate one canonical schema value transactionally.
Result< void > validate() const
Validate ids, ranges, uniqueness and deterministic ordering.
std::vector< ActionAnimationSection > animationSections
std::string animationUri
Compatibility-only single-clip projection; v2 runtime uses animationSections.
std::vector< ActionTrack > tracks
std::vector< Duration > splitTimestamps
Sorted objective split timestamps forming N+1 physical sections.
Ordered semantic lane in one action timeline.
DomainOperation public API.
PropertyReadResult public API.
EVENGINE_API_PLATFORM public API.
std::vector< PropertyDescriptor > properties
PropertyDescriptorLookup find(const PropertyPath &path) const
Finds find.
SelectionSnapshot public API.
std::vector< SelectionItem > items
Stable discovery metadata exposed by every editable target.
Prepared deterministic preview advance that can be validated before presentation side effects.
std::vector< action::ActionTimelineEvent > events
Stable document identity; paths are deliberately excluded.