载入中...
搜索中...
未找到
PixelWorldCatalogPanel.cpp
浏览该文件的文档.
2
4#include "ui/UIHost.h"
5
6#include <algorithm>
7#include <array>
8#include <charconv>
9#include <iomanip>
10#include <limits>
11#include <sstream>
12#include <utility>
13
15namespace {
16
17using namespace eve::pixelworld;
18
19constexpr const char* kCatalogPanelHostName = "eve_pixelworld_catalog";
20
21std::vector<MaterialDefinition> definitions(const MaterialCatalog& catalog) {
22 return {catalog.definitions().begin(), catalog.definitions().end()};
23}
24
25std::vector<MaterialReactionRule> reactions(const MaterialCatalog& catalog) {
26 return {catalog.reactions().begin(), catalog.reactions().end()};
27}
28
29std::vector<MaterialPhaseRule> phases(const MaterialCatalog& catalog) {
30 return {catalog.phaseRules().begin(), catalog.phaseRules().end()};
31}
32
33std::string stateName(MaterialState state) {
34 constexpr std::array names{"Empty", "Solid", "Powder", "Liquid", "Gas", "Energy"};
35 const auto index = static_cast<std::size_t>(state);
36 return index < names.size() ? names[index] : "Unknown";
37}
38
39template <class Integer>
40std::optional<Integer> parseInteger(std::string_view text) {
41 Integer value{};
42 const auto parsed = std::from_chars(text.data(), text.data() + text.size(), value);
43 if (parsed.ec != std::errc{} || parsed.ptr != text.data() + text.size()) return std::nullopt;
44 return value;
45}
46
47std::optional<std::uint32_t> parseRgba(std::string_view text) {
48 if (!text.empty() && text.front() == '#') text.remove_prefix(1);
49 if (text.size() != 8) return std::nullopt;
50 std::uint32_t value = 0;
51 const auto parsed = std::from_chars(text.data(), text.data() + text.size(), value, 16);
52 if (parsed.ec != std::errc{} || parsed.ptr != text.data() + text.size()) return std::nullopt;
53 return value;
54}
55
56std::string rgbaText(std::uint32_t rgba) {
57 std::ostringstream output;
58 output << '#' << std::uppercase << std::hex << std::setw(8) << std::setfill('0') << rgba;
59 return output.str();
60}
61
62std::vector<std::string> splitTags(std::string_view value) {
63 std::vector<std::string> result;
64 while (!value.empty()) {
65 const auto comma = value.find(',');
66 std::string tag(value.substr(0, comma));
67 const auto first = tag.find_first_not_of(" \t");
68 const auto last = tag.find_last_not_of(" \t");
69 if (first != std::string::npos) result.push_back(tag.substr(first, last - first + 1));
70 if (comma == std::string_view::npos) break;
71 value.remove_prefix(comma + 1);
72 }
73 std::sort(result.begin(), result.end());
74 result.erase(std::unique(result.begin(), result.end()), result.end());
75 return result;
76}
77
78std::string joinTags(const std::vector<std::string>& tags) {
79 std::string result;
80 for (const auto& tag : tags) {
81 if (!result.empty()) result += ", ";
82 result += tag;
83 }
84 return result;
85}
86
87template <class Integer, class Setter>
88eve::ui::WidgetDesc integerField(std::string label, std::string id, Integer value,
89 Setter setter) {
90 return eve::ui::inputText(std::move(label), std::to_string(value), std::move(id),
91 [setter = std::move(setter)](const std::string& text) mutable {
92 if (const auto parsed = parseInteger<Integer>(text)) setter(*parsed);
93 });
94}
95
96} // namespace
97
99 : draft_(std::move(catalog)) {
100 if (!draft_.reactions().empty()) selectedReaction_ = 0;
101 if (!draft_.phaseRules().empty()) selectedPhase_ = 0;
102}
103
105 if (auto host = eve::ui::UIHost::resolve(host_)) {
106 host->get().setTree(eve::ui::window("", {}));
107 host->get().setVisible(false);
108 }
109}
110
112 : draft_(std::move(other.draft_)),
113 revision_(other.revision_),
114 selectedMaterial_(other.selectedMaterial_),
115 selectedReaction_(other.selectedReaction_),
116 selectedPhase_(other.selectedPhase_),
117 statusText_(std::move(other.statusText_)),
118 host_(other.host_) {
119 other.host_ = {};
120 if (host_.table) (void)refresh();
121}
122
124 if (this == &other) return *this;
125 if (auto host = eve::ui::UIHost::resolve(host_)) {
126 host->get().setTree(eve::ui::window("", {}));
127 host->get().setVisible(false);
128 }
129 draft_ = std::move(other.draft_);
130 revision_ = other.revision_;
131 selectedMaterial_ = other.selectedMaterial_;
132 selectedReaction_ = other.selectedReaction_;
133 selectedPhase_ = other.selectedPhase_;
134 statusText_ = std::move(other.statusText_);
135 host_ = other.host_;
136 other.host_ = {};
137 if (host_.table) (void)refresh();
138 return *this;
139}
140
142 auto decoded = decodeMaterialCatalogJson(catalogJson);
143 if (!decoded.ok())
144 return eve::Result<PixelWorldCatalogPanel>::failure(decoded.status());
146 PixelWorldCatalogPanel(std::move(decoded).takeValue()));
147}
148
152
153std::vector<MaterialCard> PixelWorldCatalogPanel::materialCards() const {
154 std::vector<MaterialCard> result;
155 result.reserve(draft_.definitions().size());
156 for (std::size_t index = 0; index < draft_.definitions().size(); ++index) {
157 const auto& definition = draft_.definitions()[index];
158 result.push_back({definition.id, definition.name, definition.state,
159 definition.displayRgba, index == selectedMaterial_,
160 draft_.canReact(definition.id),
161 draft_.canPhaseTransition(definition.id)});
162 }
163 return result;
164}
165
167 if (index >= draft_.definitions().size())
169 "material selection is outside the Catalog",
170 "materialIndex", {}, "pixelworld.catalog-panel"));
171 selectedMaterial_ = index;
173}
174
176 if (index >= draft_.reactions().size())
178 "reaction selection is outside the Catalog",
179 "reactionIndex", {}, "pixelworld.catalog-panel"));
180 selectedReaction_ = index;
182}
183
185 if (index >= draft_.phaseRules().size())
187 "phase selection is outside the Catalog", "phaseIndex",
188 {}, "pixelworld.catalog-panel"));
189 selectedPhase_ = index;
191}
192
193eve::Result<CatalogDraftReceipt> PixelWorldCatalogPanel::commitCandidate(
194 std::vector<MaterialDefinition> candidateDefinitions,
195 std::vector<MaterialReactionRule> candidateReactions,
196 std::vector<MaterialPhaseRule> candidatePhases) {
197 auto candidate = MaterialCatalog::create(std::move(candidateDefinitions),
198 std::move(candidateReactions),
199 std::move(candidatePhases));
200 if (!candidate.ok())
201 return eve::Result<CatalogDraftReceipt>::failure(candidate.status());
202 CatalogDraftReceipt receipt{revision_, revision_ + 1, candidate.value().fingerprint()};
203 draft_ = std::move(candidate).takeValue();
204 revision_ = receipt.revisionAfter;
205 if (selectedMaterial_ >= draft_.definitions().size()) selectedMaterial_ = 0;
206 if (draft_.reactions().empty()) selectedReaction_.reset();
207 else if (!selectedReaction_ || *selectedReaction_ >= draft_.reactions().size())
208 selectedReaction_ = draft_.reactions().size() - 1;
209 if (draft_.phaseRules().empty()) selectedPhase_.reset();
210 else if (!selectedPhase_ || *selectedPhase_ >= draft_.phaseRules().size())
211 selectedPhase_ = draft_.phaseRules().size() - 1;
213}
214
216 MaterialDefinition definition) {
217 const auto index = static_cast<std::size_t>(definition.id);
218 if (index >= draft_.definitions().size())
220 eve::Diagnostic::error(eve::DiagnosticCode::InvalidArgument, "material id is outside the Catalog",
221 "material.id", {}, "pixelworld.catalog-panel"));
222 auto candidateDefinitions = definitions(draft_);
223 candidateDefinitions[index] = std::move(definition);
224 return commitCandidate(std::move(candidateDefinitions), reactions(draft_), phases(draft_));
225}
226
228 std::size_t index, MaterialReactionRule rule) {
229 if (index >= draft_.reactions().size())
231 eve::Diagnostic::error(eve::DiagnosticCode::InvalidArgument, "reaction index is outside the Catalog",
232 "reactionIndex", {}, "pixelworld.catalog-panel"));
233 auto candidate = reactions(draft_);
234 candidate[index] = std::move(rule);
235 return commitCandidate(definitions(draft_), std::move(candidate), phases(draft_));
236}
237
240 const std::string selectedId = rule.id;
241 auto candidate = reactions(draft_);
242 candidate.push_back(std::move(rule));
243 auto committed = commitCandidate(definitions(draft_), std::move(candidate), phases(draft_));
244 if (committed.ok()) {
245 for (std::size_t index = 0; index < draft_.reactions().size(); ++index)
246 if (draft_.reactions()[index].id == selectedId) selectedReaction_ = index;
247 }
248 return committed;
249}
250
252 if (index >= draft_.reactions().size())
254 eve::Diagnostic::error(eve::DiagnosticCode::InvalidArgument, "reaction index is outside the Catalog",
255 "reactionIndex", {}, "pixelworld.catalog-panel"));
256 auto candidate = reactions(draft_);
257 candidate.erase(candidate.begin() + static_cast<std::ptrdiff_t>(index));
258 return commitCandidate(definitions(draft_), std::move(candidate), phases(draft_));
259}
260
262 std::size_t index, MaterialPhaseRule rule) {
263 if (index >= draft_.phaseRules().size())
265 eve::Diagnostic::error(eve::DiagnosticCode::InvalidArgument, "phase index is outside the Catalog",
266 "phaseIndex", {}, "pixelworld.catalog-panel"));
267 auto candidate = phases(draft_);
268 candidate[index] = std::move(rule);
269 return commitCandidate(definitions(draft_), reactions(draft_), std::move(candidate));
270}
271
273 const std::string selectedId = rule.id;
274 auto candidate = phases(draft_);
275 candidate.push_back(std::move(rule));
276 auto committed = commitCandidate(definitions(draft_), reactions(draft_), std::move(candidate));
277 if (committed.ok()) {
278 for (std::size_t index = 0; index < draft_.phaseRules().size(); ++index)
279 if (draft_.phaseRules()[index].id == selectedId) selectedPhase_ = index;
280 }
281 return committed;
282}
283
285 if (index >= draft_.phaseRules().size())
287 eve::Diagnostic::error(eve::DiagnosticCode::InvalidArgument, "phase index is outside the Catalog",
288 "phaseIndex", {}, "pixelworld.catalog-panel"));
289 auto candidate = phases(draft_);
290 candidate.erase(candidate.begin() + static_cast<std::ptrdiff_t>(index));
291 return commitCandidate(definitions(draft_), reactions(draft_), std::move(candidate));
292}
293
297
299 std::string_view catalogJson) {
300 auto decoded = decodeMaterialCatalogJson(catalogJson);
301 if (!decoded.ok())
302 return eve::Result<CatalogDraftReceipt>::failure(decoded.status());
303 return commitCandidate(definitions(decoded.value()), reactions(decoded.value()),
304 phases(decoded.value()));
305}
306
308 PixelWorldControlService& control, std::uint64_t worldId,
309 std::uint64_t expectedFingerprint) const {
310 auto encoded = document();
311 if (!encoded.ok())
313 return control.reloadMaterialCatalog(worldId, encoded.value(), expectedFingerprint);
314}
315
317 auto host = eve::ui::UIHost::resolve(host_);
318 if (!host) {
319 host_ = eve::ui::UIHost::createHost(kCatalogPanelHostName);
321 }
322 if (!host) {
324 eve::DiagnosticCode::PreconditionViolation, "pixelworld catalog panel requires an active UI ECS world",
325 "uiHost", {}, "pixelworld.catalog-panel"));
326 }
327 host->get().setVisible(true);
328 host->get().setLayer(90);
329 auto meta = host->get().meta();
330 meta->hasPos = true;
331 meta->posX = 12.0F;
332 meta->posY = 12.0F;
333 meta->hasSize = true;
334 meta->sizeX = 820.0F;
335 meta->sizeY = 540.0F;
336 auto refreshed = refresh();
337 if (!refreshed) {
338 return eve::Result<eve::ui::UIHostHandle>::failure(refreshed.status());
339 }
341}
342
344 if (auto host = eve::ui::UIHost::resolve(host_)) host->get().setVisible(false);
345}
346
348 auto host = eve::ui::UIHost::resolve(host_);
349 return host && host->get().meta()->visible;
350}
351
353 auto host = eve::ui::UIHost::resolve(host_);
354 if (!host)
356 "pixelworld catalog panel is not mounted", "uiHost",
357 {}, "pixelworld.catalog-panel"));
358 host->get().setTree(buildWidgetTree());
360}
361
362void PixelWorldCatalogPanel::observe(eve::Result<CatalogDraftReceipt> result) {
363 if (result.ok()) statusText_ = "Catalog draft validated at revision " +
364 std::to_string(result.value().revisionAfter);
365 else statusText_ = result.status().describe();
366 if (isOpen()) (void)refresh();
367}
368
369void PixelWorldCatalogPanel::observe(eve::Result<void> result) {
370 if (result.ok()) statusText_ = "Selection updated";
371 else statusText_ = result.status().describe();
372 if (isOpen()) (void)refresh();
373}
374
376 using namespace eve::ui;
377 std::vector<WidgetDesc> materialRows;
378 for (std::size_t index = 0; index < draft_.definitions().size(); ++index) {
379 const auto& material = draft_.definitions()[index];
380 const float red = float((material.displayRgba >> 24U) & 0xFFU) / 255.0f;
381 const float green = float((material.displayRgba >> 16U) & 0xFFU) / 255.0f;
382 const float blue = float((material.displayRgba >> 8U) & 0xFFU) / 255.0f;
383 WidgetDesc swatch = image("material-swatch/" + std::to_string(index), 18.0f, 18.0f)
384 .withTint(red, green, blue, 1.0f)
385 .withCornerRadius(3.0f);
386 WidgetDesc choose = button(material.name + " · " + stateName(material.state),
387 "material/" + std::to_string(index), [this, index] {
388 observe(selectMaterial(index));
389 });
390 choose.checked = index == selectedMaterial_;
391 materialRows.push_back(row({std::move(swatch), std::move(choose)},
392 "material-row/" + std::to_string(index)));
393 }
394
395 const MaterialDefinition selected = draft_.definitions()[selectedMaterial_];
396 auto mutateMaterial = [this, id = selected.id](auto mutation) {
398 mutation(value);
399 observe(replaceMaterial(std::move(value)));
400 };
401 std::vector<std::string> stateOptions{"Empty", "Solid", "Powder", "Liquid", "Gas", "Energy"};
402 const float red = float((selected.displayRgba >> 24U) & 0xFFU) / 255.0f;
403 const float green = float((selected.displayRgba >> 16U) & 0xFFU) / 255.0f;
404 const float blue = float((selected.displayRgba >> 8U) & 0xFFU) / 255.0f;
405 const float alpha = float(selected.displayRgba & 0xFFU) / 255.0f;
406
407 std::vector<WidgetDesc> materialInspector{
408 sectionHeader("Material · " + selected.name, "material-heading"),
409 image("material-preview", 96.0f, 56.0f).withTint(red, green, blue, alpha).withCornerRadius(6.0f),
410 inputText("Name", selected.name, "material-name", [mutateMaterial](const std::string& value) mutable {
411 mutateMaterial([&](auto& material) { material.name = value; });
412 }),
413 combo("State", stateOptions, static_cast<int>(selected.state), "material-state",
414 [mutateMaterial](float value) mutable {
415 mutateMaterial([&](auto& material) {
416 material.state = static_cast<MaterialState>(std::clamp(int(value), 0, 5));
417 });
418 }),
419 inputText("RGBA", rgbaText(selected.displayRgba), "material-rgba",
420 [mutateMaterial](const std::string& value) mutable {
421 if (const auto parsed = parseRgba(value))
422 mutateMaterial([&](auto& material) { material.displayRgba = *parsed; });
423 }),
424 checkbox("Flammable", selected.flammable, "material-flammable",
425 [mutateMaterial](bool value) mutable {
426 mutateMaterial([&](auto& material) { material.flammable = value; });
427 }),
428 inputText("Tags", joinTags(selected.tags), "material-tags",
429 [mutateMaterial](const std::string& value) mutable {
430 mutateMaterial([&](auto& material) { material.tags = splitTags(value); });
431 }),
432 integerField("Density", "material-density", selected.density,
433 [mutateMaterial](std::uint16_t value) mutable {
434 mutateMaterial([&](auto& material) { material.density = value; });
435 }),
436 integerField("Viscosity", "material-viscosity", selected.viscosity,
437 [mutateMaterial](std::uint8_t value) mutable {
438 mutateMaterial([&](auto& material) { material.viscosity = value; });
439 }),
440 integerField("Thermal conductivity", "material-conductivity", selected.thermalConductivity,
441 [mutateMaterial](std::uint8_t value) mutable {
442 mutateMaterial([&](auto& material) { material.thermalConductivity = value; });
443 }),
444 integerField("Heat capacity", "material-capacity", selected.heatCapacity,
445 [mutateMaterial](std::uint16_t value) mutable {
446 mutateMaterial([&](auto& material) { material.heatCapacity = value; });
447 }),
448 integerField("Ignition temperature", "material-ignition", selected.ignitionTemperature,
449 [mutateMaterial](std::int16_t value) mutable {
450 mutateMaterial([&](auto& material) { material.ignitionTemperature = value; });
451 }),
452 integerField("Melting temperature", "material-melting", selected.meltingTemperature,
453 [mutateMaterial](std::int16_t value) mutable {
454 mutateMaterial([&](auto& material) { material.meltingTemperature = value; });
455 }),
456 integerField("Boiling temperature", "material-boiling", selected.boilingTemperature,
457 [mutateMaterial](std::int16_t value) mutable {
458 mutateMaterial([&](auto& material) { material.boilingTemperature = value; });
459 }),
460 integerField("Default temperature", "material-default-temperature", selected.defaultTemperature,
461 [mutateMaterial](std::int16_t value) mutable {
462 mutateMaterial([&](auto& material) { material.defaultTemperature = value; });
463 }),
464 integerField("Default lifetime", "material-lifetime", selected.defaultLifetime,
465 [mutateMaterial](std::uint16_t value) mutable {
466 mutateMaterial([&](auto& material) { material.defaultLifetime = value; });
467 }),
468 integerField("Blast resistance", "material-blast", selected.blastResistance,
469 [mutateMaterial](std::uint16_t value) mutable {
470 mutateMaterial([&](auto& material) { material.blastResistance = value; });
471 })};
472
473 std::vector<std::string> materialNames;
474 for (const auto& material : draft_.definitions()) materialNames.push_back(material.name);
475 std::vector<std::string> reactionNames;
476 for (const auto& reaction : draft_.reactions()) reactionNames.push_back(reaction.id);
477 std::vector<WidgetDesc> reactionEditor{sectionHeader("Binary reactions", "reaction-heading")};
478 if (!reactionNames.empty()) {
479 const auto reactionIndex = selectedReaction_.value_or(0);
480 const MaterialReactionRule selectedRule = draft_.reactions()[reactionIndex];
481 auto mutateReaction = [this, reactionIndex](auto mutation) {
482 MaterialReactionRule value = draft_.reactions()[reactionIndex];
483 mutation(value);
484 observe(replaceReaction(reactionIndex, std::move(value)));
485 };
486 reactionEditor.push_back(combo("Rule", reactionNames, int(reactionIndex), "reaction-select",
487 [this](float value) {
488 observe(selectReaction(std::size_t(std::max(0, int(value)))));
489 }));
490 reactionEditor.push_back(inputText("ID", selectedRule.id, "reaction-id",
491 [mutateReaction](const std::string& value) mutable {
492 mutateReaction([&](auto& rule) { rule.id = value; });
493 }));
494 auto materialCombo = [&](std::string label, std::string id, MaterialId value, auto assign) {
495 return combo(std::move(label), materialNames, int(value), std::move(id),
496 [mutateReaction, assign](float index) mutable {
497 mutateReaction([&](auto& rule) {
498 assign(rule, static_cast<MaterialId>(std::max(0, int(index))));
499 });
500 });
501 };
502 reactionEditor.push_back(materialCombo("First", "reaction-first", selectedRule.first,
503 [](auto& rule, auto value) { rule.first = value; }));
504 reactionEditor.push_back(materialCombo("Second", "reaction-second", selectedRule.second,
505 [](auto& rule, auto value) { rule.second = value; }));
506 reactionEditor.push_back(materialCombo("First result", "reaction-first-result",
507 selectedRule.firstResult,
508 [](auto& rule, auto value) { rule.firstResult = value; }));
509 reactionEditor.push_back(materialCombo("Second result", "reaction-second-result",
510 selectedRule.secondResult,
511 [](auto& rule, auto value) { rule.secondResult = value; }));
512 reactionEditor.push_back(integerField("Minimum temperature", "reaction-min-temperature",
513 selectedRule.minimumTemperature,
514 [mutateReaction](std::int16_t value) mutable {
515 mutateReaction([&](auto& rule) {
516 rule.minimumTemperature = value;
517 });
518 }));
519 reactionEditor.push_back(integerField("Heat delta", "reaction-heat", selectedRule.heatDelta,
520 [mutateReaction](std::int16_t value) mutable {
521 mutateReaction([&](auto& rule) { rule.heatDelta = value; });
522 }));
523 reactionEditor.push_back(integerField("Priority", "reaction-priority", selectedRule.priority,
524 [mutateReaction](std::int32_t value) mutable {
525 mutateReaction([&](auto& rule) { rule.priority = value; });
526 }));
527 reactionEditor.push_back(button("Delete reaction", "reaction-delete", [this, reactionIndex] {
528 observe(removeReaction(reactionIndex));
529 }));
530 }
531 reactionEditor.push_back(button("Add reaction", "reaction-add", [this] {
533 rule.id = "reaction-" + std::to_string(revision_ + 1);
534 observe(addReaction(std::move(rule)));
535 }));
536
537 std::vector<std::string> phaseNames;
538 for (const auto& phase : draft_.phaseRules()) phaseNames.push_back(phase.id);
539 std::vector<WidgetDesc> phaseEditor{sectionHeader("Phase transitions", "phase-heading")};
540 if (!phaseNames.empty()) {
541 const auto phaseIndex = selectedPhase_.value_or(0);
542 const MaterialPhaseRule selectedRule = draft_.phaseRules()[phaseIndex];
543 auto mutatePhase = [this, phaseIndex](auto mutation) {
544 MaterialPhaseRule value = draft_.phaseRules()[phaseIndex];
545 mutation(value);
546 observe(replacePhase(phaseIndex, std::move(value)));
547 };
548 phaseEditor.push_back(combo("Rule", phaseNames, int(phaseIndex), "phase-select",
549 [this](float value) {
550 observe(selectPhase(std::size_t(std::max(0, int(value)))));
551 }));
552 phaseEditor.push_back(inputText("ID", selectedRule.id, "phase-id",
553 [mutatePhase](const std::string& value) mutable {
554 mutatePhase([&](auto& rule) { rule.id = value; });
555 }));
556 phaseEditor.push_back(combo("Source", materialNames, int(selectedRule.source), "phase-source",
557 [mutatePhase](float value) mutable {
558 mutatePhase([&](auto& rule) {
559 rule.source = static_cast<MaterialId>(std::max(0, int(value)));
560 });
561 }));
562 phaseEditor.push_back(combo("Result", materialNames, int(selectedRule.result), "phase-result",
563 [mutatePhase](float value) mutable {
564 mutatePhase([&](auto& rule) {
565 rule.result = static_cast<MaterialId>(std::max(0, int(value)));
566 });
567 }));
568 phaseEditor.push_back(combo("Direction", {"At or below", "At or above"},
569 int(selectedRule.direction), "phase-direction",
570 [mutatePhase](float value) mutable {
571 mutatePhase([&](auto& rule) {
572 rule.direction = value < 0.5f ? TemperatureDirection::AtOrBelow
573 : TemperatureDirection::AtOrAbove;
574 });
575 }));
576 phaseEditor.push_back(integerField("Threshold", "phase-threshold", selectedRule.threshold,
577 [mutatePhase](std::int16_t value) mutable {
578 mutatePhase([&](auto& rule) { rule.threshold = value; });
579 }));
580 phaseEditor.push_back(integerField("Temperature delta", "phase-temperature-delta",
581 selectedRule.temperatureDelta,
582 [mutatePhase](std::int16_t value) mutable {
583 mutatePhase([&](auto& rule) {
584 rule.temperatureDelta = value;
585 });
586 }));
587 phaseEditor.push_back(integerField("Priority", "phase-priority", selectedRule.priority,
588 [mutatePhase](std::int32_t value) mutable {
589 mutatePhase([&](auto& rule) { rule.priority = value; });
590 }));
591 phaseEditor.push_back(button("Delete transition", "phase-delete", [this, phaseIndex] {
592 observe(removePhase(phaseIndex));
593 }));
594 }
595 phaseEditor.push_back(button("Add transition", "phase-add", [this] {
597 rule.id = "phase-" + std::to_string(revision_ + 1);
598 observe(addPhase(std::move(rule)));
599 }));
600
601 WidgetDesc browser = sidebar({searchField("Filter materials", {}, "material-filter"),
602 scrollList("materials", std::move(materialRows), 0.0f, 28.0f)},
603 "catalog-browser", 260.0f);
604 WidgetDesc inspector = child(
605 "catalog-inspector",
606 {collapsingHeader("Material properties", std::move(materialInspector), "material-card", true),
607 collapsingHeader("Reactions", std::move(reactionEditor), "reaction-section", false),
608 collapsingHeader("Phase transitions", std::move(phaseEditor), "phase-section", false)});
609 WidgetDesc content = splitPane(FlexDirection::Row, std::move(browser), std::move(inspector),
610 0.28f, "catalog-split");
611 return window("PixelWorld Material & Reaction Catalog",
612 {toolbar({badge("Validated", "catalog-valid"), spacer(),
613 text("Fingerprint " + std::to_string(draft_.fingerprint()),
614 "catalog-fingerprint")},
615 "catalog-toolbar"),
616 std::move(content), statusBar({text(statusText_, "catalog-status")},
617 "catalog-statusbar")},
618 "pixelworld-catalog-editor");
619}
620
621} // namespace eve::pixelworld_editor
double value
std::string output
float phase
Definition CaveMesh.cpp:58
std::string label
vk::UniqueImage image
std::int32_t first
std::string text
std::string name
const std::string * tag
std::string id
Definition PlayHost.cpp:108
Material * material
Battle::Reactions reactions
float size
Definition TreeMesh.cpp:156
uint32_t index
const std::map< std::string, Value > & definitions
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
const T & value() const &
Borrow the value from a const lvalue after checking success.
Definition Result.h:308
bool ok() const noexcept
Whether this result represents a non-failure outcome.
Definition Result.h:255
const Status & status() const noexcept
Inspect the structured operation status.
Definition Result.h:269
static Result failure(Status status)
Construct a failed result from a structured status.
Definition Result.h:175
std::string describe() const
Render a compact human-readable summary.
Definition Status.h:143
Owning immutable material and reaction table used by one PixelWorld.
const MaterialDefinition & definition(MaterialId id) const noexcept
Resolve a validated id; invalid ids safely project to air.
bool canReact(MaterialId id) const noexcept
Whether this material participates in at least one binary reaction.
static MaterialCatalog builtIn()
Construct the engine's canonical built-in catalog.
std::span< const MaterialReactionRule > reactions() const noexcept
Borrow immutable canonical-priority reaction rules.
std::span< const MaterialDefinition > definitions() const noexcept
Borrow immutable definitions until this catalog is destroyed.
std::span< const MaterialPhaseRule > phaseRules() const noexcept
Borrow immutable canonical-priority phase rules.
static eve::Result< MaterialCatalog > create(std::vector< MaterialDefinition > definitions, std::vector< MaterialReactionRule > reactions={}, std::vector< MaterialPhaseRule > phaseRules={})
Validate and own a complete catalog candidate transactionally.
bool canPhaseTransition(MaterialId id) const noexcept
Whether this material owns at least one phase transition rule.
Main-thread-affine control surface shared by editor, CLI and MCP adapters.
eve::Result< PixelCatalogReloadReceipt > reloadMaterialCatalog(std::uint64_t worldId, std::string_view catalogJson, std::uint64_t expectedFingerprint)
Validate JSON and transactionally hot-reload a compatible paused world Catalog.
Dedicated visual material/reaction Catalog editor with transactional draft admission.
eve::ui::WidgetDesc buildWidgetTree()
Build a dedicated material browser, property inspector and rule editor visual tree.
eve::Result< CatalogDraftReceipt > addReaction(eve::pixelworld::MaterialReactionRule rule)
Add a binary rule and select its canonical post-validation position.
eve::Result< CatalogDraftReceipt > removePhase(std::size_t index)
Remove one phase rule transactionally.
eve::Result< CatalogDraftReceipt > removeReaction(std::size_t index)
Remove one binary rule transactionally.
static eve::Result< PixelWorldCatalogPanel > create(std::string_view catalogJson)
Decode and validate a versioned Catalog document into a fresh panel draft.
eve::Result< CatalogDraftReceipt > replaceReaction(std::size_t index, eve::pixelworld::MaterialReactionRule rule)
Replace one binary rule after validating the entire reconstructed Catalog.
PixelWorldCatalogPanel(const PixelWorldCatalogPanel &)=delete
std::vector< MaterialCard > materialCards() const
Project deterministic cards for the material browser.
eve::Result< void > selectPhase(std::size_t index)
Select a phase-transition rule by canonical index.
eve::Result< void > selectMaterial(std::size_t index)
Select a material by canonical index without mutating the Catalog.
eve::Result< CatalogDraftReceipt > loadDocument(std::string_view catalogJson)
Replace the draft from a validated document; failure leaves all panel state intact.
eve::Result< void > selectReaction(std::size_t index)
Select a binary reaction rule by canonical index.
void close()
Hide the mounted panel without discarding its validated draft.
eve::Result< CatalogDraftReceipt > replaceMaterial(eve::pixelworld::MaterialDefinition definition)
Replace one material definition after validating the entire reconstructed Catalog.
eve::Result< eve::pixelworld::PixelCatalogReloadReceipt > apply(eve::pixelworld::PixelWorldControlService &control, std::uint64_t worldId, std::uint64_t expectedFingerprint) const
Apply the draft through the canonical paused-world authority path.
eve::Result< CatalogDraftReceipt > replacePhase(std::size_t index, eve::pixelworld::MaterialPhaseRule rule)
Replace one phase rule after validating the entire reconstructed Catalog.
eve::Result< CatalogDraftReceipt > addPhase(eve::pixelworld::MaterialPhaseRule rule)
Add a phase rule and select its canonical post-validation position.
PixelWorldCatalogPanel & operator=(const PixelWorldCatalogPanel &)=delete
bool isOpen() const
True while the panel has a live, visible UI host.
eve::ui::UIHostHandle host() const noexcept
Mounted UI host handle, or an empty handle before open().
eve::Result< eve::ui::UIHostHandle > open()
Mount or reveal the panel in the UI ECS world and publish its current tree.
static PixelWorldCatalogPanel builtIn()
Create a panel over the canonical built-in Catalog.
eve::Result< void > refresh()
Reconcile the mounted host with the latest draft and selection state.
eve::Result< std::string > document() const
Encode the current validated draft using the canonical versioned JSON codec.
static UIHostHandle createHost(const std::string &name="")
Creates an ECS-owned UI host.
Definition UIHost.cpp:48
static eve::OptionalRef< UIHost > resolve(UIHostHandle handle) noexcept
Resolves a UI host handle with ECS generation checking.
Definition UIHost.cpp:61
eve::Result< MaterialCatalog > decodeMaterialCatalogJson(std::string_view json)
Decode and validate schema eve.pixelworld.material-catalog version 1.
MaterialId
Compact stable material identifier stored in each authoritative cell.
MaterialState
Broad movement class for one pixel material.
eve::Result< std::string > encodeMaterialCatalogJson(const MaterialCatalog &catalog)
Encode schema eve.pixelworld.material-catalog version 1 canonically.
ControlEdit button(const char *label, float width=0.f, float height=0.f)
Shared immediate renderer for retained and EditorHost buttons.
ControlEdit combo(const char *label, int &selected, const std::vector< const char * > &items)
Shared immediate renderer for indexed combo boxes.
ControlEdit inputText(const char *label, std::string &value, bool multiline=false, ImVec2 multilineSize=ImVec2(0.f, 96.f))
UTF-8 owning text editor with automatic buffer growth.
WidgetDesc collapsingHeader(std::string label, std::vector< WidgetDesc > children, std::string id, bool defaultOpen)
Collapsible header containing child widgets.
Definition Widget.cpp:623
WidgetDesc badge(std::string label, std::string id)
Compact status/category pill. Tint controls its background color.
Definition Widget.cpp:489
WidgetDesc statusBar(std::vector< WidgetDesc > children, std::string id)
Compact horizontal status strip; Spacer children absorb free width.
Definition Widget.cpp:596
WidgetDesc inputText(std::string label, std::string value, std::string id, std::function< void(const std::string &)> onChange)
Editable text field; fires onTextChange.
Definition Widget.cpp:453
WidgetDesc window(std::string title, std::vector< WidgetDesc > children, std::string id)
Top-level window widget with a title bar.
Definition Widget.cpp:292
WidgetDesc spacer(std::string id, float grow)
Flexible empty space; default flexGrow=1 so it absorbs free space in a Flex parent.
Definition Widget.cpp:697
WidgetDesc child(std::string id, std::vector< WidgetDesc > children, float width, float height)
Scrollable child region with an explicit size.
Definition Widget.cpp:635
WidgetDesc toolbar(std::vector< WidgetDesc > children, std::string id)
Horizontal editor command strip; Spacer children absorb free width.
Definition Widget.cpp:563
WidgetDesc searchField(std::string hint, std::string value, std::string id, std::function< void(const std::string &)> onChange)
Compact search field with placeholder text and a bundled search icon.
Definition Widget.cpp:465
WidgetDesc sidebar(std::vector< WidgetDesc > children, std::string id, float width)
Vertical editor side panel. Width defaults to 240 logical pixels.
Definition Widget.cpp:586
WidgetDesc scrollList(std::string id, std::vector< WidgetDesc > children, float height, float itemHeight)
Scroll list.
Definition Widget.cpp:646
WidgetDesc splitPane(FlexDirection direction, WidgetDesc first, WidgetDesc second, float ratio, std::string id, std::function< void(float)> onResize)
Two resizable panes separated by a drag handle.
Definition Widget.cpp:607
WidgetDesc sectionHeader(std::string label, std::string id)
Non-collapsible editor section heading with an accent marker.
Definition Widget.cpp:522
enum EVENGINE_API_FOUNDATION Integer
OT_INTEGER.
Definition Runtime.h:117
Immutable validated properties for one catalog material.
std::uint32_t displayRgba
Canonical preview/render color encoded as 0xRRGGBBAA.
std::vector< std::string > tags
Canonical sorted semantic tags used by authoring and rule queries.
Data-driven phase transition evaluated after heat diffusion.
Deterministically ordered binary material reaction.
Draft mutation receipt used for UI reconciliation and optimistic publication.
Declarative widget description (build once / on dirty → flatten into UIHost::Tree).
Definition Widget.h:15