载入中...
搜索中...
未找到
StylizeTarget.cpp
浏览该文件的文档.
2
4
5#include <algorithm>
6#include <cmath>
7#include <set>
8#include <utility>
9
10namespace eve::stylize_editing {
11namespace {
12const EditorValue* field(const EditorValue& v, const char* k) {
13 const auto* o = v.getIf<EditorValue::Object>();
14 if (!o) return nullptr;
15 auto i = o->find(k);
16 return i == o->end() ? nullptr : &i->second;
17}
18EditorValue passValue(const StylizePassValue& p) {
19 EditorValue::Object overrides;
20 for (const auto& [name, value] : p.overrides) overrides[name] = value;
21 return EditorValue::Object{{"id", p.id.value()},
22 {"style", p.style},
23 {"enabled", p.enabled},
24 {"priority", int64_t{p.priority}},
25 {"overrides", std::move(overrides)}};
26}
27Result<StylizePassValue> parsePass(const EditorValue& v) {
28 const auto *idv = field(v, "id"), *stylev = field(v, "style"), *enabledv = field(v, "enabled"),
29 *priorityv = field(v, "priority"), *overridesv = field(v, "overrides");
30 const auto* id = idv ? idv->getIf<std::string>() : nullptr;
31 const auto* style = stylev ? stylev->getIf<std::string>() : nullptr;
32 const auto* enabled = enabledv ? enabledv->getIf<bool>() : nullptr;
33 const auto* priority = priorityv ? priorityv->getIf<int64_t>() : nullptr;
34 const auto* overrides = overridesv ? overridesv->getIf<EditorValue::Object>() : nullptr;
35 if (!id || id->empty() || !style || !stylize::isKnownStyle(*style) || !enabled || !priority ||
36 *priority < -100000 || *priority > 100000 || !overrides)
37 return eve::editing::failed<StylizePassValue>(EditorStatus::Rejected, RuleId("editor.stylize.pass"),
38 "Invalid stylize pass fields");
39 StylizePassValue p{ObjectId(*id), *style, *enabled, static_cast<int>(*priority), {}};
40 for (const auto& [name, value] : *overrides) {
41 const auto* n = value.getIf<double>();
42 const auto* d = stylize::findStyleParameter(*style, name);
43 if (!n || !std::isfinite(*n) || !d || *n < d->minValue || *n > d->maxValue)
44 return eve::editing::failed<StylizePassValue>(EditorStatus::Rejected, RuleId("editor.stylize.parameter"),
45 "Invalid stylize parameter override");
46 p.overrides[name] = *n;
47 }
48 return eve::editing::applied<StylizePassValue>(std::move(p));
49}
50bool hasError(const std::vector<EditorDiagnostic>& d) {
51 return std::any_of(d.begin(), d.end(), [](const auto& v) { return v.severity() == DiagnosticSeverity::Error; });
52}
53PropertyDescriptor property(const std::string& path, PropertyType type, EditorValue value, const char* category) {
56 d.displayNameKey = "editor.stylize." + path;
57 d.category = category;
58 d.type = type;
59 d.flags = PropertyFlag::Runtime;
60 d.defaultValue = std::move(value);
61 if (path == "pass.priority") {
62 d.numeric.minimum = -100000;
63 d.numeric.maximum = 100000;
64 }
65 return d;
66}
67} // namespace
68
69StylizeRecipeTarget::StylizeRecipeTarget(std::string id) : id_(std::move(id)) {}
71 return {TargetId(id_),
72 "stylize-recipe",
74 false,
75 {CapabilityId("eve.editor.target.stylize-properties")}};
76}
77void* StylizeRecipeTarget::queryCapability(const CapabilityId& c) {
78 return c == CapabilityId("eve.editor.target.stylize-properties") ? static_cast<IPropertyProvider*>(this) : nullptr;
79}
80bool StylizeRecipeTarget::matches(const SelectionSnapshot& s) const {
81 if (s.items.empty()) return false;
82 for (const auto& i : s.items)
83 if (i.target != TargetId(id_) || !passes_.contains(ObjectId(i.item.value()))) return false;
84 return true;
85}
94 schema.typeId = "stylize.pass";
95 schema.properties = {property("pass.enabled", PropertyType::Bool, true, "pass"),
96 property("pass.priority", PropertyType::Int, int64_t{0}, "pass")};
97 if (!matches(s)) return schema;
98 const std::string style = passes_.at(ObjectId(s.items.front().item.value())).style;
99 for (const auto& i : s.items)
100 if (passes_.at(ObjectId(i.item.value())).style != style) return schema;
101 for (int index = 0; index < stylize::styleParamCount(style); ++index) {
102 const auto* d = stylize::styleParameterAt(style, index);
103 auto p = property("parameter." + std::string(d->id), PropertyType::Float, static_cast<double>(d->defaultValue),
104 "parameters");
105 p.numeric.minimum = d->minValue;
106 p.numeric.maximum = d->maxValue;
107 schema.properties.push_back(std::move(p));
108 }
109 return schema;
110}
112 if (!matches(s) || !schema(s).find(path)) return {};
113 std::optional<EditorValue> common;
114 for (const auto& i : s.items) {
115 const auto& p = passes_.at(ObjectId(i.item.value()));
117 if (path == PropertyPath("pass.enabled"))
118 value = p.enabled;
119 else if (path == PropertyPath("pass.priority"))
120 value = int64_t{p.priority};
121 else {
122 const std::string name = path.value().substr(std::string("parameter.").size());
123 auto found = p.overrides.find(name);
124 const auto* d = stylize::findStyleParameter(p.style, name);
125 value = found == p.overrides.end() ? EditorValue(static_cast<double>(d->defaultValue))
126 : EditorValue(found->second);
127 }
128 if (!common)
129 common = value;
130 else if (*common != value)
131 return {PropertyReadState::Mixed, {}, {}};
132 }
133 return {PropertyReadState::Value, *common, {}};
134}
135EditorValue StylizeRecipeTarget::contentValue() const {
136 EditorValue::Array array;
137 for (const auto& id : order_) array.push_back(passValue(passes_.at(id)));
138 return EditorValue::Object{{"passes", std::move(array)}};
139}
140Result<DomainOperation> StylizeRecipeTarget::replacement(EditorValue payload,
141 std::string propertyName) const {
143 op.type = "stylize.recipe.replace.v1";
144 op.inverseType = op.type;
145 op.target = TargetId(id_);
146 op.payload = std::move(payload);
147 op.inverse = contentValue();
148 op.hasInverse = true;
149 if (!propertyName.empty()) op.affectedProperties.push_back(propertyName);
150 op.mergeKey = "stylize:" + id_ + ":" + (propertyName.empty() ? "structure" : propertyName);
151 return eve::editing::applied<DomainOperation>(std::move(op));
152}
154 const EditorValue& value, PropertySetMode mode) const {
155 if (mode == PropertySetMode::Reset) return makeReset(s, path);
156 auto d = schema(s).find(path);
157 if (!matches(s) || !d || mode != PropertySetMode::Absolute)
158 return eve::editing::failed<DomainOperation>(EditorStatus::Rejected, RuleId("editor.stylize.set"),
159 "Stylize property requires matching absolute selection");
160 auto checked = validatePropertyValue(*d, value);
161 if (!checked.ok())
162 return eve::editing::failed<DomainOperation>(EditorStatus::Rejected, RuleId("editor.stylize.value"),
163 "Stylize property is outside its metadata range");
164 auto candidate = *this;
165 for (const auto& i : s.items) {
166 auto& p = candidate.passes_[ObjectId(i.item.value())];
167 if (path == PropertyPath("pass.enabled"))
168 p.enabled = *value.getIf<bool>();
169 else if (path == PropertyPath("pass.priority"))
170 p.priority = static_cast<int>(*value.getIf<int64_t>());
171 else
172 p.overrides[path.value().substr(10)] = *value.getIf<double>();
173 }
174 if (hasError(candidate.validate()))
175 return eve::editing::failed<DomainOperation>(EditorStatus::Rejected, RuleId("editor.stylize.invalid"),
176 "Stylize edit produces an invalid recipe");
177 return replacement(candidate.contentValue(), path.value());
178}
180 const PropertyPath& path) const {
181 auto d = schema(s).find(path);
182 if (!d)
183 return eve::editing::failed<DomainOperation>(EditorStatus::Unsupported, RuleId("editor.stylize.property"),
184 "Unknown stylize property");
185 if (path.value().starts_with("parameter.")) {
186 auto candidate = *this;
187 for (const auto& i : s.items)
188 candidate.passes_[ObjectId(i.item.value())].overrides.erase(path.value().substr(10));
189 return replacement(candidate.contentValue(), path.value());
190 }
191 return makeSet(s, path, d->defaultValue, PropertySetMode::Absolute);
192}
193Result<DomainOperation> StylizeRecipeTarget::makeCreate(const ObjectId& id, const std::string& style) const {
194 if (id.empty() || passes_.contains(id) || !stylize::isKnownStyle(style))
195 return eve::editing::failed<DomainOperation>(EditorStatus::Rejected, RuleId("editor.stylize.create"),
196 "Stylize pass ID or style is invalid");
197 auto candidate = *this;
198 candidate.passes_[id] = {id, style, true, static_cast<int>(order_.size()), {}};
199 candidate.order_.push_back(id);
200 if (hasError(candidate.validate()))
201 return eve::editing::failed<DomainOperation>(EditorStatus::Rejected, RuleId("editor.stylize.stage"),
202 "Stylize recipe cannot mix post and mesh stages");
203 return replacement(candidate.contentValue());
204}
206 if (!passes_.contains(id))
207 return eve::editing::failed<DomainOperation>(EditorStatus::NotFound, RuleId("editor.stylize.pass"),
208 "Stylize pass does not exist");
209 auto candidate = *this;
210 candidate.passes_.erase(id);
211 std::erase(candidate.order_, id);
212 return replacement(candidate.contentValue());
213}
214Result<DomainOperation> StylizeRecipeTarget::makeMove(const ObjectId& id, std::size_t index) const {
215 if (!passes_.contains(id) || index >= order_.size())
216 return eve::editing::failed<DomainOperation>(EditorStatus::Rejected, RuleId("editor.stylize.move"),
217 "Stylize move target or index is invalid");
218 auto candidate = *this;
219 std::erase(candidate.order_, id);
220 candidate.order_.insert(candidate.order_.begin() + static_cast<std::ptrdiff_t>(index), id);
221 for (std::size_t i = 0; i < candidate.order_.size(); ++i)
222 candidate.passes_[candidate.order_[i]].priority = static_cast<int>(i);
223 return replacement(candidate.contentValue());
224}
226 if (op.target != TargetId(id_) || op.type != "stylize.recipe.replace.v1")
227 return eve::editing::failed<void>(EditorStatus::Rejected, RuleId("editor.stylize.operation"),
228 "Stylize operation mismatch");
229 const auto* p = field(op.payload, "passes");
230 const auto* array = p ? p->getIf<EditorValue::Array>() : nullptr;
231 if (!array || array->size() > 64)
232 return eve::editing::failed<void>(EditorStatus::Rejected, RuleId("editor.stylize.payload"),
233 "Stylize recipe payload is invalid or exceeds 64 passes");
234 StylizeRecipeTarget candidate(id_);
235 for (const auto& entry : *array) {
236 auto parsed = parsePass(entry);
237 if (!parsed.ok() || candidate.passes_.contains(parsed.value().id))
238 return eve::editing::failed<void>(EditorStatus::Rejected, RuleId("editor.stylize.pass"),
239 "Stylize recipe contains invalid or duplicate passes");
240 candidate.order_.push_back(parsed.value().id);
241 candidate.passes_.emplace(parsed.value().id, parsed.value());
242 }
243 if (hasError(candidate.validate()))
244 return eve::editing::failed<void>(EditorStatus::Rejected, RuleId("editor.stylize.invalid"),
245 "Stylize recipe violates stage or parameter rules");
246 passes_ = std::move(candidate.passes_);
247 order_ = std::move(candidate.order_);
248 bumpRevision();
249 widenDirty(0, 0);
250 return eve::editing::applied<void>();
251}
252std::unique_ptr<IDomainOperationTarget> StylizeRecipeTarget::cloneDomainState() const {
253 return std::make_unique<StylizeRecipeTarget>(*this);
254}
255Result<void> StylizeRecipeTarget::commitDomainState(std::unique_ptr<IDomainOperationTarget> candidate) {
256 auto* t = dynamic_cast<StylizeRecipeTarget*>(candidate.get());
257 if (!t || t->id_ != id_)
258 return eve::editing::failed<void>(EditorStatus::Conflict, RuleId("editor.stylize.candidate"),
259 "Stylize candidate mismatch");
260 *this = *t;
261 return eve::editing::applied<void>();
262}
263std::vector<StylizePassValue> StylizeRecipeTarget::passes() const {
264 std::vector<StylizePassValue> result;
265 for (const auto& id : order_) result.push_back(passes_.at(id));
266 return result;
267}
268std::vector<EditorDiagnostic> StylizeRecipeTarget::validate() const {
269 std::vector<EditorDiagnostic> d;
270 std::string stage;
271 for (const auto& id : order_) {
272 const auto& p = passes_.at(id);
273 const auto* definition = stylize::findStyleDefinition(p.style);
274 if (!definition) {
276 RuleId("editor.stylize.unknown-style"), DiagnosticSeverity::Error, "Unknown style: " + p.style));
277 continue;
278 }
279 if (!p.enabled) continue;
280 const std::string current = graphics::postEffectStageName(definition->stage);
281 if (stage.empty())
282 stage = current;
283 else if (stage != current)
285 RuleId("editor.stylize.mixed-stage"), DiagnosticSeverity::Error,
286 "Enabled recipe passes must share one pipeline stage"));
287 if (!definition->post)
289 RuleId("editor.stylize.mesh-in-recipe"), DiagnosticSeverity::Error,
290 "Post recipe cannot contain a mesh-only style: " + p.style));
291 }
292 if (order_.empty())
294 RuleId("editor.stylize.empty"), DiagnosticSeverity::Warning, "Stylize recipe has no passes"));
295 return d;
296}
298 return EditorValue::Object{{"schemaVersion", int64_t{1}}, {"content", contentValue()}};
299}
301 const auto *v = field(s, "schemaVersion"), *content = field(s, "content");
302 const auto* version = v ? v->getIf<int64_t>() : nullptr;
303 if (!version || *version != 1 || !content)
304 return eve::editing::failed<void>(EditorStatus::Unsupported, RuleId("editor.stylize.snapshot"),
305 "Unsupported stylize snapshot");
307 op.target = TargetId(id_);
308 op.type = "stylize.recipe.replace.v1";
309 op.payload = *content;
310 auto result = applyDomainOperation(op);
311 if (result.ok()) clearDirtyRegion();
312 return result;
313}
314
315} // namespace eve::stylize_editing
LogicalId target
double value
Value::Object payload
const std::string & s
int priority
glm::vec4 p[6]
glm::vec3 n
Definition Grass.cpp:63
float v
std::int32_t c
std::string name
std::string path
Definition PlayHost.cpp:110
std::string id
Definition PlayHost.cpp:108
float d
float t
bool found
double current
float size
Definition TreeMesh.cpp:156
const char * category
uint32_t index
std::size_t at
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
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
void bumpRevision()
Advance the revision without touching the dirty region.
Revision revisionValue() const noexcept
Return the revision as the shared protocol revision type.
void widenDirty()
Mark an unknown area dirty without advancing the revision.
virtual void clearDirtyRegion()=0
Clears dirty region.
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::vector< Value > Array
std::map< std::string, Value > Object
const T * getIf() const
Return an immediately borrowed typed value.
Serializable ordered style recipe with dynamic parameter Inspector.
std::unique_ptr< IDomainOperationTarget > cloneDomainState() const override
Clones domain state.
Result< DomainOperation > makeMove(const ObjectId &id, std::size_t index) const
Plan stable reordering by destination index.
TargetDescriptor describe() const override
Describe.
Result< DomainOperation > makeDelete(const ObjectId &id) const
Plan removal of one pass.
PropertyReadResult read(const SelectionSnapshot &selection, const PropertyPath &path) const override
Reads .
std::vector< EditorDiagnostic > validate() const
Validate style existence, stage compatibility, parameters and budgets.
PropertySchema schema(const SelectionSnapshot &selection) const override
Schema.
Result< DomainOperation > makeReset(const SelectionSnapshot &selection, const PropertyPath &path) const override
Make reset.
StylizeRecipeTarget(std::string id)
Stylize recipe target.
void * queryCapability(const CapabilityId &capability) override
Query an optional target capability.
eve::Result< eve::Revision > currentRevision(const SelectionSnapshot &selection) const override
Current revision.
Result< DomainOperation > makeCreate(const ObjectId &id, const std::string &style) const
Plan insertion of a known style under a stable pass ID.
std::vector< StylizePassValue > passes() const
Return ordered immutable pass values.
Result< DomainOperation > makeSet(const SelectionSnapshot &selection, const PropertyPath &path, const EditorValue &value, PropertySetMode mode) const override
Make set.
EditorValue snapshotValue() const
Capture schema-version-one ordered recipe.
Result< void > loadSnapshot(const EditorValue &snapshot)
Atomically load and validate a persisted recipe.
Result< void > commitDomainState(std::unique_ptr< IDomainOperationTarget > candidate) override
Commits domain state.
Result< void > applyDomainOperation(const DomainOperation &operation) override
Applies domain operation.
editing::PropertyDescriptor PropertyDescriptor
editing::DomainOperation DomainOperation
editing::PropertyPath PropertyPath
editing::ObjectId ObjectId
editing::TargetId TargetId
Diagnostic ruleDiagnostic(eve::DiagnosticCode code, RuleId rule, DiagnosticSeverity severity, std::string message)
Build a common diagnostic carrying an open editing rule identity.
stylize_editing::StylizePassValue StylizePassValue
const char * postEffectStageName(PostEffectStage stage)
Post effect stage name.
Definition PostEffect.h:28
const EditorValue * field(const EditorValue &value, const char *name)
editing::Value EditorValue
int styleParamCount(const std::string &style)
Style param count.
bool isKnownStyle(const std::string &style)
True when known style.
const StyleParameterDesc * findStyleParameter(const std::string &style, const std::string &name)
Finds style parameter.
const StyleParameterDesc * styleParameterAt(const std::string &style, int index)
Style parameter at.
const StyleDefinition * findStyleDefinition(const std::string &style)
Return a built-in definition, or nullptr when the id is unknown.
bool enabled
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.
Stable discovery metadata exposed by every editable target.