scene/edit_history.cpp
| Line | Branch | Exec | Source |
|---|---|---|---|
| 1 | // SPDX-License-Identifier: BSD-3-Clause | ||
| 2 | /// @file edit_history.cpp | ||
| 3 | /// @brief Recording edits as steps of before and after states, and applying them either way. | ||
| 4 | #include "ernie/scene/edit_history.h" | ||
| 5 | |||
| 6 | #include "ernie/util/text.h" | ||
| 7 | |||
| 8 | #include <algorithm> | ||
| 9 | #include <stdexcept> | ||
| 10 | |||
| 11 | namespace ernie { | ||
| 12 | |||
| 13 | 9 | void EditHistory::Set(Scene& scene, const Path& path, | |
| 14 | std::vector<std::pair<std::string, Value>> edits, bool open) { | ||
| 15 | 9 | const PrimId prim = scene.Require(path); | |
| 16 | 9 | Step step{.path = path, .open = open}; | |
| 17 | 9 | try { | |
| 18 | 17 | for (auto& [name, value] : edits) { | |
| 19 | 9 | Cell before = Capture(scene, prim, name); | |
| 20 | 9 | scene.Set(prim, name, std::move(value)); | |
| 21 | 8 | step.attributes.push_back({name, std::move(before), Capture(scene, prim, name)}); | |
| 22 | 9 | } | |
| 23 | 1 | } catch (...) { | |
| 24 | // What applied before the rejected edit stays undoable. | ||
| 25 | 1 | if (!step.attributes.empty()) { | |
| 26 | ✗ | Push(std::move(step)); | |
| 27 | } | ||
| 28 | 1 | throw; | |
| 29 | 1 | } | |
| 30 | 10 | const auto same = [&](const Step& last) { | |
| 31 | 4 | return !last.structural && last.open && last.path == path && | |
| 32 | 2 | std::ranges::equal(last.attributes, step.attributes, {}, &AttributeEdit::name, | |
| 33 | 2 | &AttributeEdit::name); | |
| 34 | 8 | }; | |
| 35 | 8 | if (open && open_ && next_ == steps_.size() && !steps_.empty() && same(steps_.back())) { | |
| 36 | 4 | for (std::size_t i = 0; i < step.attributes.size(); ++i) { | |
| 37 | 2 | steps_.back().attributes[i].after = std::move(step.attributes[i].after); | |
| 38 | } | ||
| 39 | 2 | return; | |
| 40 | } | ||
| 41 | 6 | Push(std::move(step)); | |
| 42 | 17 | } | |
| 43 | |||
| 44 | 1 | void EditHistory::Clear(Scene& scene, const Path& path, std::string_view attribute) { | |
| 45 | 1 | const PrimId prim = scene.Require(path); | |
| 46 | 1 | const std::string name(attribute); | |
| 47 | 1 | Cell before = Capture(scene, prim, name); | |
| 48 | 1 | scene.Clear(prim, attribute); | |
| 49 | 2 | Push({.path = path, .attributes = {{name, std::move(before), Capture(scene, prim, name)}}}); | |
| 50 | 3 | } | |
| 51 | |||
| 52 | 1 | void EditHistory::Define(Scene& scene, const Path& path, std::string_view type) { | |
| 53 | 1 | if (scene.Has(path)) { | |
| 54 | ✗ | throw std::invalid_argument("'" + path.String() + "' has a prim already"); | |
| 55 | } | ||
| 56 | 1 | scene.Define(path, type); | |
| 57 | 1 | Push({.path = path, .after = Snapshot(scene, path), .structural = true}); | |
| 58 | 2 | } | |
| 59 | |||
| 60 | 2 | bool EditHistory::Remove(Scene& scene, const Path& path) { | |
| 61 | 2 | if (!scene.Has(path)) { | |
| 62 | return false; | ||
| 63 | } | ||
| 64 | 1 | Step step{.path = path, .before = Snapshot(scene, path), .structural = true}; | |
| 65 | 1 | scene.Remove(path); | |
| 66 | 1 | Push(std::move(step)); | |
| 67 | 1 | return true; | |
| 68 | 1 | } | |
| 69 | |||
| 70 | 2 | void EditHistory::Add(Scene& scene, const Scene& source, const Path& path) { | |
| 71 | 4 | if (!source.Has(path) || scene.Has(path)) { | |
| 72 | 2 | throw std::invalid_argument("Cannot add '" + path.String() + | |
| 73 | 3 | "': the source has no prim there or the scene has one"); | |
| 74 | } | ||
| 75 | 1 | Step step{.path = path, .after = Snapshot(source, path), .structural = true}; | |
| 76 | 1 | Restore(scene, step.after); | |
| 77 | 1 | Push(std::move(step)); | |
| 78 | 1 | } | |
| 79 | |||
| 80 | 1 | void EditHistory::BeginGroup() { | |
| 81 | 1 | if (groupDepth_++ == 0) { | |
| 82 | 1 | group_ = nextGroup_++; | |
| 83 | } | ||
| 84 | 1 | } | |
| 85 | |||
| 86 | 1 | void EditHistory::EndGroup() { | |
| 87 | 1 | if (groupDepth_ > 0 && --groupDepth_ == 0) { | |
| 88 | 1 | group_ = 0; | |
| 89 | } | ||
| 90 | 1 | } | |
| 91 | |||
| 92 | 10 | bool EditHistory::Undo(Scene& scene) { | |
| 93 | 10 | return Move(scene, false); | |
| 94 | } | ||
| 95 | |||
| 96 | 3 | bool EditHistory::Redo(Scene& scene) { | |
| 97 | 3 | return Move(scene, true); | |
| 98 | } | ||
| 99 | |||
| 100 | 2 | std::string EditHistory::UndoLabel() const { | |
| 101 | 2 | return CanUndo() ? Label(steps_[next_ - 1]) : std::string(); | |
| 102 | } | ||
| 103 | |||
| 104 | ✗ | std::string EditHistory::RedoLabel() const { | |
| 105 | ✗ | return CanRedo() ? Label(steps_[next_]) : std::string(); | |
| 106 | } | ||
| 107 | |||
| 108 | ✗ | void EditHistory::Reset() { | |
| 109 | ✗ | *this = EditHistory(); | |
| 110 | ✗ | } | |
| 111 | |||
| 112 | 25 | EditHistory::Cell EditHistory::Capture(const Scene& scene, PrimId prim, | |
| 113 | const std::string& attribute) { | ||
| 114 | 25 | Cell cell; | |
| 115 | 25 | const std::span<const TimeSample> samples = scene.Samples(prim, attribute); | |
| 116 | 25 | cell.samples.assign(samples.begin(), samples.end()); | |
| 117 | 25 | if (const Tensor* array = scene.Read(prim, attribute)) { | |
| 118 | 2 | cell.array = *array; | |
| 119 | } else { | ||
| 120 | 23 | cell.value = scene.Get(prim, attribute); | |
| 121 | } | ||
| 122 | 25 | return cell; | |
| 123 | ✗ | } | |
| 124 | |||
| 125 | 14 | void EditHistory::Apply(Scene& scene, PrimId prim, const std::string& attribute, const Cell& cell) { | |
| 126 | 14 | if (!cell.samples.empty()) { | |
| 127 | 1 | scene.ClearSamples(prim, attribute); | |
| 128 | 3 | for (const TimeSample& sample : cell.samples) { | |
| 129 | 2 | sample.tensor.Empty() ? scene.SetAt(prim, attribute, sample.time, sample.value) | |
| 130 | 2 | : scene.WriteAt(prim, attribute, sample.time, sample.tensor); | |
| 131 | } | ||
| 132 | 13 | } else if (!cell.array.Empty()) { | |
| 133 | 1 | scene.Write(prim, attribute, cell.array); | |
| 134 | 12 | } else if (cell.value) { | |
| 135 | 9 | scene.Set(prim, attribute, *cell.value); | |
| 136 | } else { | ||
| 137 | 3 | scene.Clear(prim, attribute); | |
| 138 | } | ||
| 139 | 14 | } | |
| 140 | |||
| 141 | 3 | std::vector<EditHistory::PrimSnapshot> EditHistory::Snapshot(const Scene& scene, const Path& path) { | |
| 142 | 3 | std::vector<PrimSnapshot> prims; | |
| 143 | 6 | std::vector<Path> pending{path}; | |
| 144 | 8 | while (!pending.empty()) { | |
| 145 | 5 | const Path next = std::move(pending.back()); | |
| 146 | 5 | pending.pop_back(); | |
| 147 | 5 | const PrimId prim = scene.Require(next); | |
| 148 | 5 | PrimSnapshot& snapshot = prims.emplace_back(); | |
| 149 | 5 | snapshot.path = next; | |
| 150 | 5 | snapshot.type = std::string(scene.TypeName(prim)); | |
| 151 | 5 | snapshot.apis = scene.AppliedApis(prim); | |
| 152 | 11 | for (const std::string& name : scene.Attributes(next)) { | |
| 153 | 6 | snapshot.attributes.emplace(name, Capture(scene, prim, name)); | |
| 154 | 5 | } | |
| 155 | 5 | const std::vector<Path> children = scene.Children(next); | |
| 156 | 5 | pending.insert(pending.end(), children.rbegin(), children.rend()); | |
| 157 | 5 | } | |
| 158 | 3 | return prims; | |
| 159 | 6 | } | |
| 160 | |||
| 161 | 6 | void EditHistory::Restore(Scene& scene, const std::vector<PrimSnapshot>& prims) { | |
| 162 | 11 | for (const PrimSnapshot& snapshot : prims) { | |
| 163 | 5 | const PrimId prim = scene.Define(snapshot.path, snapshot.type); | |
| 164 | 6 | for (const std::string& api : snapshot.apis) { | |
| 165 | 1 | scene.ApplyApi(prim, api); | |
| 166 | } | ||
| 167 | 11 | for (const auto& [name, cell] : snapshot.attributes) { | |
| 168 | 6 | Apply(scene, prim, name, cell); | |
| 169 | } | ||
| 170 | } | ||
| 171 | 6 | } | |
| 172 | |||
| 173 | 14 | void EditHistory::Apply(Scene& scene, const Step& step, bool forward) { | |
| 174 | 14 | if (step.structural) { | |
| 175 | 5 | scene.Remove(step.path); | |
| 176 | 5 | Restore(scene, forward ? step.after : step.before); | |
| 177 | 11 | return; | |
| 178 | } | ||
| 179 | // Edits of a prim removed since are skipped. | ||
| 180 | 9 | const std::optional<PrimId> prim = scene.Find(step.path); | |
| 181 | 9 | if (!prim) { | |
| 182 | return; | ||
| 183 | } | ||
| 184 | 16 | for (std::size_t i = 0; i < step.attributes.size(); ++i) { | |
| 185 | 8 | const AttributeEdit& edit = step.attributes[forward ? i : step.attributes.size() - 1 - i]; | |
| 186 | 8 | Apply(scene, *prim, edit.name, forward ? edit.after : edit.before); | |
| 187 | } | ||
| 188 | } | ||
| 189 | |||
| 190 | 10 | void EditHistory::Push(Step step) { | |
| 191 | 10 | steps_.resize(next_); | |
| 192 | 10 | step.group = groupDepth_ > 0 ? group_ : 0; | |
| 193 | 10 | open_ = step.open; | |
| 194 | 10 | steps_.push_back(std::move(step)); | |
| 195 | 10 | next_ = steps_.size(); | |
| 196 | 10 | } | |
| 197 | |||
| 198 | 13 | bool EditHistory::Move(Scene& scene, bool forward) { | |
| 199 | 13 | open_ = false; | |
| 200 | 13 | if (forward ? !CanRedo() : !CanUndo()) { | |
| 201 | return false; | ||
| 202 | } | ||
| 203 | 12 | const std::uint64_t group = steps_[forward ? next_ : next_ - 1].group; | |
| 204 | 14 | do { | |
| 205 | 14 | if (forward) { | |
| 206 | 4 | Apply(scene, steps_[next_++], true); | |
| 207 | } else { | ||
| 208 | 10 | Apply(scene, steps_[--next_], false); | |
| 209 | } | ||
| 210 | 17 | } while (group != 0 && (forward ? CanRedo() && steps_[next_].group == group | |
| 211 | 2 | : CanUndo() && steps_[next_ - 1].group == group)); | |
| 212 | return true; | ||
| 213 | } | ||
| 214 | |||
| 215 | 2 | std::string EditHistory::Label(const Step& step) { | |
| 216 | 2 | if (step.structural) { | |
| 217 | 1 | return (step.before.empty() ? "Add " | |
| 218 | 1 | : step.after.empty() ? "Remove " | |
| 219 | : "Change ") + | ||
| 220 | 1 | step.path.String(); | |
| 221 | } | ||
| 222 | 2 | return step.path.String() + " " + Join(step.attributes, " ", &AttributeEdit::name); | |
| 223 | } | ||
| 224 | |||
| 225 | } // namespace ernie | ||
| 226 |