载入中...
搜索中...
未找到
EditorCurveDocument.cpp
浏览该文件的文档.
2
3#include <algorithm>
4#include <cmath>
5#include <set>
6#include <utility>
7
8namespace eve::editor {
9namespace {
10
11const EditorValue* field(const EditorValue& value, const char* key) {
12 const auto* object = value.getIf<EditorValue::Object>();
13 if (!object) return nullptr;
14 const auto found = object->find(key);
15 return found == object->end() ? nullptr : &found->second;
16}
17
18EditorValue keyValue(const EditorCurveKey& key) {
19 return EditorValue::Object{{"id", key.id.value()}, {"time", key.time}, {"value", key.value},
20 {"inTangent", key.inTangent}, {"outTangent", key.outTangent},
21 {"interpolation", key.interpolation}};
22}
23
24Result<EditorCurveKey> parseKey(const EditorValue& value) {
25 const auto string = [&](const char* name) { const auto* entry = field(value, name); return entry ? entry->getIf<std::string>() : nullptr; };
26 const auto number = [&](const char* name) { const auto* entry = field(value, name); return entry ? entry->getIf<double>() : nullptr; };
27 const auto* id = string("id"); const auto* time = number("time"); const auto* assigned = number("value");
28 const auto* in = number("inTangent"); const auto* out = number("outTangent"); const auto* interpolation = string("interpolation");
29 const std::set<std::string> modes{"constant", "linear", "cubic"};
30 if (!id || id->empty() || !time || !assigned || !in || !out || !interpolation ||
31 !modes.contains(*interpolation) || !std::isfinite(*time) || !std::isfinite(*assigned) ||
32 !std::isfinite(*in) || !std::isfinite(*out) || *time < 0.0 || *time > 1.0)
33 return eve::editing::failed<EditorCurveKey>(EditorStatus::Rejected, RuleId("editor.curve.invalid-key"),
34 "Curve key requires stable id, normalized time and finite values");
35 return eve::editing::applied<EditorCurveKey>({StableId(*id), *time, *assigned, *in, *out, *interpolation});
36}
37
38EditorValue stopValue(const EditorGradientStop& stop) {
39 EditorValue::Array color; for (double component : stop.color) color.emplace_back(component);
40 return EditorValue::Object{{"id", stop.id.value()}, {"time", stop.time}, {"color", std::move(color)}};
41}
42
43Result<EditorGradientStop> parseStop(const EditorValue& value) {
44 const auto* idValue = field(value, "id"); const auto* timeValue = field(value, "time"); const auto* colorValue = field(value, "color");
45 const auto* id = idValue ? idValue->getIf<std::string>() : nullptr; const auto* time = timeValue ? timeValue->getIf<double>() : nullptr;
46 const auto* color = colorValue ? colorValue->getIf<EditorValue::Array>() : nullptr;
47 if (!id || id->empty() || !time || !std::isfinite(*time) || *time < 0.0 || *time > 1.0 || !color || color->size() != 4)
48 return eve::editing::failed<EditorGradientStop>(EditorStatus::Rejected, RuleId("editor.curve.invalid-stop"),
49 "Gradient stop requires stable id, normalized time and RGBA");
50 EditorGradientStop result; result.id = StableId(*id); result.time = *time;
51 for (std::size_t i = 0; i < 4; ++i) {
52 const auto* number = (*color)[i].getIf<double>();
53 if (!number || !std::isfinite(*number) || *number < 0.0 || *number > 1.0)
54 return eve::editing::failed<EditorGradientStop>(
55 EditorStatus::Rejected, RuleId("editor.curve.invalid-color"), "Gradient color must be normalized");
56 result.color[i] = *number;
57 }
58 return eve::editing::applied<EditorGradientStop>(std::move(result));
59}
60
61DomainOperation operation(const char* type, const char* inverseType, const std::string& target,
62 EditorValue payload, EditorValue inverse, const StableId& affected) {
63 DomainOperation result; result.type = type; result.inverseType = inverseType; result.target = TargetId(target);
64 result.payload = std::move(payload); result.inverse = std::move(inverse); result.hasInverse = true;
65 result.affectedObjects.push_back({TargetId(target), affected.value(), 0}); return result;
66}
67
68template <class T>
69std::vector<T> timeline(const std::map<StableId, T>& values) {
70 std::vector<T> result; for (const auto& [id, value] : values) { static_cast<void>(id); result.push_back(value); }
71 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; });
72 return result;
73}
74
75double mix(double a, double b, double t) { return a + (b - a) * t; }
76
77} // namespace
78
79EditorCurveDocument::EditorCurveDocument(std::string id) : id_(std::move(id)) {}
80
84
87 ? static_cast<ICurveDocumentEditTarget*>(this) : nullptr;
88}
89
91 if (operationValue.target != TargetId(id_))
92 return eve::editing::failed<void>(EditorStatus::Rejected, RuleId("editor.curve.target-mismatch"),
93 "Curve operation targets another document");
94 if (operationValue.type == "curve.key.set.v1") {
95 auto parsed = parseKey(operationValue.payload);
96 if (!parsed.ok())
97 return eve::editing::failed<void>(parsed.code(), RuleId("editor.curve.invalid-key"),
98 "Curve key is invalid");
99 keys_[parsed.value().id] = std::move(parsed.value());
100 } else if (operationValue.type == "curve.key.delete.v1") {
101 auto parsed = parseKey(operationValue.payload);
102 if (!parsed.ok() || !keys_.erase(parsed.value().id))
103 return eve::editing::failed<void>(EditorStatus::NotFound, RuleId("editor.curve.key-not-found"),
104 "Curve key was not found");
105 } else if (operationValue.type == "curve.stop.set.v1") {
106 auto parsed = parseStop(operationValue.payload);
107 if (!parsed.ok())
108 return eve::editing::failed<void>(parsed.code(), RuleId("editor.curve.invalid-stop"),
109 "Gradient stop is invalid");
110 stops_[parsed.value().id] = std::move(parsed.value());
111 } else if (operationValue.type == "curve.stop.delete.v1") {
112 auto parsed = parseStop(operationValue.payload);
113 if (!parsed.ok() || !stops_.erase(parsed.value().id))
114 return eve::editing::failed<void>(EditorStatus::NotFound, RuleId("editor.curve.stop-not-found"),
115 "Gradient stop was not found");
116 } else
117 return eve::editing::failed<void>(EditorStatus::Unsupported, RuleId("editor.curve.operation-unsupported"),
118 "Curve operation is unsupported");
119 bumpRevision();
120 widenDirty(0, 0);
121 return eve::editing::applied<void>();
122}
123
124std::unique_ptr<IDomainOperationTarget> EditorCurveDocument::cloneDomainState() const {
125 return std::make_unique<EditorCurveDocument>(*this);
126}
127
128Result<void> EditorCurveDocument::commitDomainState(std::unique_ptr<IDomainOperationTarget> candidate) {
129 auto* curve = dynamic_cast<EditorCurveDocument*>(candidate.get());
130 if (!curve || curve->id_ != id_)
131 return eve::editing::failed<void>(EditorStatus::Rejected, RuleId("editor.curve.invalid-candidate"),
132 "Curve candidate does not match this document");
133 *this = std::move(*curve); return eve::editing::applied<void>();
134}
135
137 auto parsed = parseKey(keyValue(key));
138 if (!parsed.ok())
139 return eve::editing::failed<DomainOperation>(parsed.code(), RuleId("editor.curve.invalid-key"),
140 "Curve key is invalid");
141 const auto found = keys_.find(key.id); const bool exists = found != keys_.end();
142 return eve::editing::applied<DomainOperation>(operation("curve.key.set.v1", exists ? "curve.key.set.v1" : "curve.key.delete.v1", id_, keyValue(parsed.value()), exists ? keyValue(found->second) : keyValue(parsed.value()), key.id));
143}
144
146 const auto found = keys_.find(key);
147 if (found == keys_.end())
148 return eve::editing::failed<DomainOperation>(EditorStatus::NotFound, RuleId("editor.curve.key-not-found"),
149 "Curve key was not found");
150 return eve::editing::applied<DomainOperation>(operation("curve.key.delete.v1", "curve.key.set.v1", id_, keyValue(found->second), keyValue(found->second), key));
151}
152
154 auto parsed = parseStop(stopValue(stop));
155 if (!parsed.ok())
156 return eve::editing::failed<DomainOperation>(parsed.code(), RuleId("editor.curve.invalid-stop"),
157 "Gradient stop is invalid");
158 const auto found = stops_.find(stop.id); const bool exists = found != stops_.end();
159 return eve::editing::applied<DomainOperation>(operation("curve.stop.set.v1", exists ? "curve.stop.set.v1" : "curve.stop.delete.v1", id_, stopValue(parsed.value()), exists ? stopValue(found->second) : stopValue(parsed.value()), stop.id));
160}
161
163 const auto found = stops_.find(stop);
164 if (found == stops_.end())
165 return eve::editing::failed<DomainOperation>(EditorStatus::NotFound, RuleId("editor.curve.stop-not-found"),
166 "Gradient stop was not found");
167 return eve::editing::applied<DomainOperation>(operation("curve.stop.delete.v1", "curve.stop.set.v1", id_, stopValue(found->second), stopValue(found->second), stop));
168}
169
170std::vector<EditorCurveKey> EditorCurveDocument::keys() const { return timeline(keys_); }
171std::vector<EditorGradientStop> EditorCurveDocument::stops() const { return timeline(stops_); }
172
174 const auto values = keys(); if (values.empty()) return 0.0;
175 time = std::clamp(time, 0.0, 1.0);
176 const auto upper = std::upper_bound(values.begin(), values.end(), time, [](double t, const auto& key) { return t < key.time; });
177 if (upper == values.begin()) return upper->value;
178 if (upper == values.end()) return values.back().value;
179 const auto& a = *(upper - 1); const auto& b = *upper; const double span = b.time - a.time;
180 const double t = span > 0.0 ? (time - a.time) / span : 0.0;
181 if (a.interpolation == "constant") return a.value;
182 if (a.interpolation == "linear") return mix(a.value, b.value, t);
183 const double t2 = t * t, t3 = t2 * t;
184 return (2 * t3 - 3 * t2 + 1) * a.value + (t3 - 2 * t2 + t) * a.outTangent * span +
185 (-2 * t3 + 3 * t2) * b.value + (t3 - t2) * b.inTangent * span;
186}
187
188std::array<double, 4> EditorCurveDocument::sampleGradient(double time) const {
189 const auto values = stops(); if (values.empty()) return {1.0, 1.0, 1.0, 1.0};
190 time = std::clamp(time, 0.0, 1.0);
191 const auto upper = std::upper_bound(values.begin(), values.end(), time, [](double t, const auto& stop) { return t < stop.time; });
192 if (upper == values.begin()) return upper->color;
193 if (upper == values.end()) return values.back().color;
194 const auto& a = *(upper - 1); const auto& b = *upper; const double t = b.time > a.time ? (time - a.time) / (b.time - a.time) : 0.0;
195 std::array<double, 4> result; for (std::size_t i = 0; i < 4; ++i) result[i] = mix(a.color[i], b.color[i], t); return result;
196}
197
198EditorCurvePreview EditorCurveDocument::preview(int sampleCount, int maximumSamples) const {
199 EditorCurvePreview result;
201 if (sampleCount < 2 || maximumSamples < 2 || sampleCount > maximumSamples) {
202 result.status = EditorStatus::Rejected;
203 result.diagnostics.push_back(editing::ruleDiagnostic(
204 eve::DiagnosticCode::PreconditionViolation, RuleId("editor.curve.preview-budget"),
205 DiagnosticSeverity::Error, "Curve preview sample count exceeds its bounded budget"));
206 return result;
207 }
208 result.curveSamples.reserve(static_cast<std::size_t>(sampleCount)); result.gradientSamples.reserve(static_cast<std::size_t>(sampleCount));
209 for (int i = 0; i < sampleCount; ++i) { const double t = static_cast<double>(i) / (sampleCount - 1); result.curveSamples.push_back(sampleCurve(t)); result.gradientSamples.push_back(sampleGradient(t)); }
210 result.status = EditorStatus::Applied; return result;
211}
212
214 EditorValue::Array keys; for (const auto& key : this->keys()) keys.push_back(keyValue(key));
215 EditorValue::Array stops; for (const auto& stop : this->stops()) stops.push_back(stopValue(stop));
216 return EditorValue::Object{{"schemaVersion", int64_t{1}}, {"keys", std::move(keys)}, {"stops", std::move(stops)}};
217}
218
220 const auto* versionValue = field(snapshot, "schemaVersion"); const auto* keysValue = field(snapshot, "keys"); const auto* stopsValue = field(snapshot, "stops");
221 const auto* version = versionValue ? versionValue->getIf<int64_t>() : nullptr; const auto* keys = keysValue ? keysValue->getIf<EditorValue::Array>() : nullptr; const auto* stops = stopsValue ? stopsValue->getIf<EditorValue::Array>() : nullptr;
222 if (!version || *version != 1 || !keys || !stops)
223 return eve::editing::failed<void>(EditorStatus::Unsupported, RuleId("editor.curve.invalid-snapshot"),
224 "Curve snapshot schema is unsupported");
225 EditorCurveDocument candidate(id_);
226 for (const auto& value : *keys) {
227 auto parsed = parseKey(value);
228 if (!parsed.ok() || candidate.keys_.contains(parsed.value().id))
229 return eve::editing::failed<void>(EditorStatus::Rejected, RuleId("editor.curve.invalid-snapshot-key"),
230 "Curve snapshot contains invalid or duplicate keys");
231 candidate.keys_[parsed.value().id] = std::move(parsed.value());
232 }
233 for (const auto& value : *stops) {
234 auto parsed = parseStop(value);
235 if (!parsed.ok() || candidate.stops_.contains(parsed.value().id))
236 return eve::editing::failed<void>(EditorStatus::Rejected, RuleId("editor.curve.invalid-snapshot-stop"),
237 "Curve snapshot contains invalid or duplicate stops");
238 candidate.stops_[parsed.value().id] = std::move(parsed.value());
239 }
240 candidate.setRevision(revisionValue() + 1);
241 candidate.clearDirtyRegion();
242 *this = std::move(candidate);
243 return eve::editing::applied<void>();
244}
245
246} // namespace eve::editor
LogicalId target
ActionParameterOperation operation
double value
Value::Object payload
std::map< std::string, Var > values
std::uint32_t key
bool exists
std::string name
MeleePoint3 b
Definition MeleeHit.cpp:41
MeleePoint3 a
Definition MeleeHit.cpp:40
std::string id
Definition PlayHost.cpp:108
float t
double number
bool found
std::string string
Heightmap curve
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
void setRevision(Revision revision)
Adopt a revision observed elsewhere, without advancing it.
void bumpRevision()
Advance the revision without touching the dirty region.
void clearDirtyRegion() override
Discard the dirty region; the revision does not change.
Revision revisionValue() const noexcept
Return the revision as the shared protocol revision type.
void widenDirty()
Mark an unknown area dirty without advancing the revision.
eve::OptionalRef< C > capability()
Query a capability through its interface-owned stable identity.
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.
UI-neutral reversible curve and gradient timeline document.
Result< void > commitDomainState(std::unique_ptr< IDomainOperationTarget > candidate) override
Commits domain state.
std::unique_ptr< IDomainOperationTarget > cloneDomainState() const override
Clones domain state.
EditorCurvePreview preview(int sampleCount, int maximumSamples=4096) const
Build a bounded lookup preview for viewport/material upload.
std::vector< EditorCurveKey > keys() const
Return scalar keys in stable timeline order.
TargetDescriptor describe() const override
Describe.
Result< void > applyDomainOperation(const DomainOperation &operation) override
Applies domain operation.
double sampleCurve(double time) const
Sample scalar curve at normalized time.
EditorValue snapshotValue() const
Capture deterministic schema-version-one timeline data.
Result< void > loadSnapshot(const EditorValue &snapshot)
Atomically load a validated timeline snapshot.
Result< DomainOperation > makeSetKey(const EditorCurveKey &key) const override
Make set key.
Result< DomainOperation > makeDeleteStop(const StableId &stop) const override
Make delete stop.
EditorCurveDocument(std::string id)
Editor curve document.
Result< DomainOperation > makeDeleteKey(const StableId &key) const override
Make delete key.
std::vector< EditorGradientStop > stops() const
Return gradient stops in stable timeline order.
Result< DomainOperation > makeSetStop(const EditorGradientStop &stop) const override
Make set stop.
std::array< double, 4 > sampleGradient(double time) const
Sample gradient at normalized time.
void * queryCapability(const CapabilityId &capability) override
Query an optional target capability.
Stable key/gradient editing capability shared by effects and animation presenters.
static CapabilityId editorCapabilityId()
Editor capability id.
Diagnostic ruleDiagnostic(eve::DiagnosticCode code, RuleId rule, DiagnosticSeverity severity, std::string message)
Build a common diagnostic carrying an open editing rule identity.
eve::editing::StableId StableId
Definition EditorIds.h:36
eve::editing::Value EditorValue
Compatibility alias for the UI-independent authoring value tree.
Definition EditorValue.h:8
eve::editing::TargetId TargetId
Definition EditorIds.h:29
eve::editing::RuleId RuleId
Definition EditorIds.h:32
eve::editing::DomainOperation DomainOperation
const EditorValue * field(const EditorValue &value, const char *name)
DomainOperation public API.
Stable discovery metadata exposed by every editable target.
Stable scalar curve key with editable Hermite tangents.
Bounded revision-tagged curve/gradient lookup preview.
std::vector< EditorDiagnostic > diagnostics
std::vector< std::array< double, 4 > > gradientSamples
Stable normalized RGBA gradient stop.
glm::vec4 color