载入中...
搜索中...
未找到
AvatarDocumentEditor.cpp
浏览该文件的文档.
2
4
5#include <algorithm>
6#include <cmath>
7#include <cstdint>
8#include <utility>
9
10namespace eve::avatar_editor {
11namespace {
12
13template <class T = void>
14avatar_editing::Result<T> editorError(avatar_editing::EditorStatus status, std::string rule,
15 std::string message) {
16 return eve::editing::failed<T>(status, avatar_editing::RuleId(std::move(rule)), std::move(message));
17}
18
20avatar_editing::EditorValue color(float r, float g, float b, float a) {
22}
23
24avatar_editing::EditorValue layerValue(const char* id, const char* name, const char* texture, int z, bool visible,
25 float ox, float oy, float w, float h, float r, float g, float bl, float a) {
26 return avatar_editing::EditorValue::Object{{"id", std::string(id)},
27 {"name", std::string(name)},
28 {"texture", std::string(texture)},
29 {"z", std::int64_t{z}},
30 {"visible", visible},
31 {"offset", floats(ox, oy)},
32 {"size", floats(w, h)},
33 {"color", color(r, g, bl, a)}};
34}
35
36} // namespace
37
39 : target_(std::move(targetId)), authority_(&target_), transactions_(&authority_) {
40 seedPreviewDocument();
41 auto previewed = refreshPreview();
42 if (!previewed.ok())
43 previewed.ignore("avatar editor keeps an empty composite when the seeded preview is rejected");
44}
45
46void AvatarDocumentEditor::seedPreviewDocument() {
48 layers.push_back(layerValue("body", "body", "asset://preview/body.png", 0, true, 40.0f, 20.0f, 96.0f, 128.0f, 0.35f,
49 0.55f, 0.72f, 1.0f));
50 layers.push_back(layerValue("eyes", "eyes", "asset://preview/eyes.png", 1, true, 62.0f, 78.0f, 52.0f, 22.0f, 0.95f,
51 0.92f, 0.88f, 1.0f));
53 parameters.push_back(avatar_editing::EditorValue::Object{{"id", std::string("smile")},
54 {"name", std::string("smile")},
55 {"default", 0.35},
56 {"minimum", 0.0},
57 {"maximum", 1.0},
58 {"value", 0.35}});
60 channels["smile"] = 1.0;
62 expressions.push_back(avatar_editing::EditorValue::Object{
63 {"id", std::string("happy")}, {"name", std::string("happy")}, {"channels", std::move(channels)}});
65 content["kind"] = std::string("image");
66 content["source"] = std::string{};
67 content["layers"] = std::move(layers);
68 content["parameters"] = std::move(parameters);
69 content["expressions"] = std::move(expressions);
71 root["schemaVersion"] = std::int64_t{1};
72 root["content"] = avatar_editing::EditorValue(std::move(content));
73 auto loaded = target_.loadSnapshot(avatar_editing::EditorValue(std::move(root)));
74 if (!loaded.ok()) loaded.ignore("avatar editor keeps defaults when the preview snapshot is rejected");
75}
76
78 editor::EditorWorkspace& workspace) const {
79 editor::EditorWorkspace candidate = workspace;
80 struct Panel {
81 const char* id;
82 const char* title;
83 const char* region;
84 const char* context;
85 int order;
86 };
87 constexpr Panel panels[] = {
88 {"avatar.layers", "Layers", "left", "list", 100},
89 {"avatar.preview", "Avatar Preview", "center", "preview", 100},
90 {"avatar.inspector", "Avatar Inspector", "right", "inspector", 100},
91 {"avatar.parameters", "Parameters", "bottom", "parameters", 100},
92 {"avatar.expressions", "Expressions", "right", "expressions", 200},
93 };
94 for (const auto& panel : panels) {
95 if (!candidate.registerPanel(panel.id, panel.title, panel.region, panel.order) ||
96 !candidate.setPanelCapability(panel.id, "avatar.document") ||
97 !candidate.setPanelContext(panel.id, panel.context))
98 return editorError(avatar_editing::EditorStatus::Rejected, "editor.avatar.workspace-conflict",
99 "Could not install the avatar workspace composition");
100 }
101 if (!candidate.activatePanel("avatar.preview"))
102 return editorError(avatar_editing::EditorStatus::Rejected, "editor.avatar.workspace-activate",
103 "Could not activate the avatar preview panel");
104 workspace = std::move(candidate);
105 return eve::editing::applied<void>();
106}
107
108editor::SelectionSnapshot AvatarDocumentEditor::selection() const {
110 snapshot.channel = "avatar";
112 item.domain = editor::SelectionDomain::Asset;
113 item.target = editor::TargetId(target_.targetId());
114 item.item = editor::StableId(selectedId_);
115 item.type = selectedType_;
116 snapshot.items.push_back(item);
117 snapshot.primary = item;
118 return snapshot;
119}
120
121avatar_editing::Result<void> AvatarDocumentEditor::commit(
123 if (!operation.ok())
126 spec.id = editor::TransactionId("avatar.document.tx." + std::to_string(++txSequence_));
127 spec.label = std::move(label);
128 spec.target = editor::TargetId(target_.targetId());
129 spec.baseRevision = target_.revision();
130 auto begun = transactions_.begin(std::move(spec));
131 if (!begun.ok())
132 return editorError(begun.code(), "editor.avatar.begin", "Could not begin the avatar transaction");
133 auto appended = transactions_.append(std::move(operation).takeValue());
134 if (!appended.ok()) {
135 auto discarded = transactions_.discard();
136 if (!discarded.ok()) discarded.ignore("pending avatar transaction already inactive");
137 return avatar_editing::Result<void>::failure(appended.status());
138 }
139 auto committed = transactions_.commit();
140 if (!committed.ok())
141 return avatar_editing::Result<void>::failure(committed.status());
142 return refreshPreview();
143}
144
145avatar_editing::Result<void> AvatarDocumentEditor::refreshPreview() {
146 std::vector<PreviewRect> next;
147 auto layers = target_.layers();
148 std::sort(layers.begin(), layers.end(), [](const auto& a, const auto& b) {
149 return a.zIndex == b.zIndex ? a.id.value() < b.id.value() : a.zIndex < b.zIndex;
150 });
151 const float smile = smileAmount();
152 for (const auto& layer : layers) {
153 if (!layer.visible) continue;
154 PreviewRect rect;
155 rect.id = layer.id.value();
156 rect.name = layer.name;
157 rect.x = layer.offset[0];
158 rect.y = layer.offset[1] + (layer.name == "eyes" ? smile * 10.0f : 0.0f);
159 rect.w = layer.size[0];
160 rect.h = layer.size[1];
161 rect.r = layer.color[0];
162 rect.g = layer.color[1];
163 rect.b = layer.color[2];
164 rect.a = layer.color[3];
165 rect.selected = layer.id.value() == selectedId_;
166 next.push_back(std::move(rect));
167 }
168 preview_ = std::move(next);
169 previewRevision_ = target_.revision();
170 return eve::editing::applied<void>();
171}
172
173float AvatarDocumentEditor::smileAmount() const {
174 for (const auto& parameter : target_.parameters())
175 if (parameter.name == "smile") return parameter.value;
176 return 0.0f;
177}
178
180 if (id.empty())
181 return editorError(avatar_editing::EditorStatus::Rejected, "editor.avatar.select-layer",
182 "Layer id must not be empty");
183 bool found = false;
184 for (const auto& layer : target_.layers())
185 if (layer.id.value() == id) found = true;
186 if (!found)
187 return editorError(avatar_editing::EditorStatus::NotFound, "editor.avatar.layer", "Avatar layer was not found");
188 selectedId_ = std::move(id);
189 selectedType_ = "avatar.layer";
190 return refreshPreview();
191}
192
194 if (id.empty())
195 return editorError(avatar_editing::EditorStatus::Rejected, "editor.avatar.select-parameter",
196 "Parameter id must not be empty");
197 bool found = false;
198 for (const auto& parameter : target_.parameters())
199 if (parameter.id.value() == id) found = true;
200 if (!found)
201 return editorError(avatar_editing::EditorStatus::NotFound, "editor.avatar.parameter",
202 "Avatar parameter was not found");
203 selectedId_ = std::move(id);
204 selectedType_ = "avatar.parameter";
205 return refreshPreview();
206}
207
209 if (id.empty())
210 return editorError(avatar_editing::EditorStatus::Rejected, "editor.avatar.select-expression",
211 "Expression id must not be empty");
212 bool found = false;
213 for (const auto& expression : target_.expressions())
214 if (expression.id.value() == id) found = true;
215 if (!found)
216 return editorError(avatar_editing::EditorStatus::NotFound, "editor.avatar.expression",
217 "Avatar expression was not found");
218 selectedId_ = std::move(id);
219 selectedType_ = "avatar.expression";
220 return refreshPreview();
221}
222
224 if (!std::isfinite(x) || !std::isfinite(y))
225 return editorError(avatar_editing::EditorStatus::Rejected, "editor.avatar.pointer",
226 "Pointer coordinates must be finite");
227 for (int i = static_cast<int>(preview_.size()) - 1; i >= 0; --i) {
228 const auto& rect = preview_[static_cast<std::size_t>(i)];
229 if (x >= rect.x && y >= rect.y && x <= rect.x + rect.w && y <= rect.y + rect.h)
230 return selectLayer(rect.id);
231 }
232 return eve::editing::applied<void>();
233}
234
236 if (selectedType_ != "avatar.layer")
237 return editorError(avatar_editing::EditorStatus::Rejected, "editor.avatar.selection",
238 "A layer must be selected to change visibility");
239 return commit(target_.makeSet(selection(), avatar_editing::PropertyPath("layer.visible"),
240 avatar_editing::EditorValue(visible), avatar_editing::PropertySetMode::Absolute),
241 "Set layer visibility");
242}
243
245 if (selectedType_ != "avatar.layer")
246 return editorError(avatar_editing::EditorStatus::Rejected, "editor.avatar.selection",
247 "A layer must be selected to change z-order");
248 return commit(target_.makeSet(selection(), avatar_editing::PropertyPath("layer.z"),
249 avatar_editing::EditorValue(std::int64_t{zIndex}),
250 avatar_editing::PropertySetMode::Absolute),
251 "Set layer z");
252}
253
255 if (selectedType_ != "avatar.parameter")
256 return editorError(avatar_editing::EditorStatus::Rejected, "editor.avatar.selection",
257 "A parameter must be selected to change its value");
258 return commit(target_.makeSet(selection(), avatar_editing::PropertyPath("parameter.value"),
259 avatar_editing::EditorValue(value), avatar_editing::PropertySetMode::Absolute),
260 "Set parameter value");
261}
262
265 layer.id = avatar_editing::ObjectId(std::move(id));
266 layer.name = std::move(name);
267 layer.textureAsset = "asset://preview/" + layer.name + ".png";
268 layer.zIndex = static_cast<int>(target_.layers().size());
269 layer.offset = {16.0f, 16.0f};
270 layer.size = {48.0f, 48.0f};
271 auto created = commit(target_.makeCreateLayer(layer), "Create layer");
272 if (!created.ok()) return created;
273 return selectLayer(layer.id.value());
274}
275
277 if (selectedType_ != "avatar.layer")
278 return editorError(avatar_editing::EditorStatus::Rejected, "editor.avatar.selection",
279 "A layer must be selected to delete it");
280 const auto previous = preview_;
281 const auto revision = previewRevision_;
282 auto deleted = commit(target_.makeDeleteLayer(avatar_editing::ObjectId(selectedId_)), "Delete layer");
283 if (!deleted.ok()) {
284 preview_ = previous;
285 previewRevision_ = revision;
286 return deleted;
287 }
288 if (!target_.layers().empty()) return selectLayer(target_.layers().front().id.value());
289 selectedId_.clear();
290 return eve::editing::applied<void>();
291}
292
295 parameter.id = avatar_editing::ObjectId(std::move(id));
296 parameter.name = std::move(name);
297 auto created = commit(target_.makeCreateParameter(parameter), "Create parameter");
298 if (!created.ok()) return created;
299 return selectParameter(parameter.id.value());
300}
301
303 if (selectedType_ != "avatar.parameter")
304 return editorError(avatar_editing::EditorStatus::Rejected, "editor.avatar.selection",
305 "A parameter must be selected to delete it");
306 const auto previous = preview_;
307 const auto revision = previewRevision_;
308 auto deleted =
309 commit(target_.makeDeleteParameter(avatar_editing::ObjectId(selectedId_)), "Delete parameter");
310 if (!deleted.ok()) {
311 preview_ = previous;
312 previewRevision_ = revision;
313 return deleted;
314 }
315 if (!target_.parameters().empty()) return selectParameter(target_.parameters().front().id.value());
316 selectedId_.clear();
317 return eve::editing::applied<void>();
318}
319
322 expression.id = avatar_editing::ObjectId(std::move(id));
323 expression.name = std::move(name);
324 auto created = commit(target_.makeCreateExpression(expression), "Create expression");
325 if (!created.ok()) return created;
326 return selectExpression(expression.id.value());
327}
328
330 if (selectedType_ != "avatar.expression")
331 return editorError(avatar_editing::EditorStatus::Rejected, "editor.avatar.selection",
332 "An expression must be selected to delete it");
333 auto deleted = commit(target_.makeDeleteExpression(avatar_editing::ObjectId(selectedId_)), "Delete expression");
334 if (!deleted.ok()) return deleted;
335 if (!target_.expressions().empty()) return selectExpression(target_.expressions().front().id.value());
336 selectedId_.clear();
337 return eve::editing::applied<void>();
338}
339
341 auto result = transactions_.undo();
342 if (!result.ok()) return result;
343 auto previewed = refreshPreview();
344 if (!previewed.ok())
346 return result;
347}
348
350 auto result = transactions_.redo();
351 if (!result.ok()) return result;
352 auto previewed = refreshPreview();
353 if (!previewed.ok())
355 return result;
356}
357
358std::string AvatarDocumentEditor::layerId(int index) const {
359 const auto* layer = layerAt(index);
360 return layer ? layer->id.value() : std::string{};
361}
363 const auto* layer = layerAt(index);
364 return layer ? layer->name : std::string{};
365}
367 const auto* layer = layerAt(index);
368 return layer && layer->visible;
369}
371 const auto* layer = layerAt(index);
372 return layer ? layer->zIndex : 0;
373}
375 const auto* layer = layerAt(index);
376 return layer && selectedType_ == "avatar.layer" && layer->id.value() == selectedId_;
377}
378
380 const auto* rect = previewAt(index);
381 return rect ? rect->x : 0.0f;
382}
384 const auto* rect = previewAt(index);
385 return rect ? rect->y : 0.0f;
386}
388 const auto* rect = previewAt(index);
389 return rect ? rect->w : 0.0f;
390}
392 const auto* rect = previewAt(index);
393 return rect ? rect->h : 0.0f;
394}
396 const auto* rect = previewAt(index);
397 return rect ? rect->r : 0.0f;
398}
400 const auto* rect = previewAt(index);
401 return rect ? rect->g : 0.0f;
402}
404 const auto* rect = previewAt(index);
405 return rect ? rect->b : 0.0f;
406}
408 const auto* rect = previewAt(index);
409 return rect ? rect->a : 0.0f;
410}
412 const auto* rect = previewAt(index);
413 return rect && rect->selected;
414}
416 const auto* rect = previewAt(index);
417 return rect ? rect->name : std::string{};
418}
419
421 const auto* parameter = parameterAt(index);
422 return parameter ? parameter->id.value() : std::string{};
423}
425 const auto* parameter = parameterAt(index);
426 return parameter ? parameter->name : std::string{};
427}
429 const auto* parameter = parameterAt(index);
430 return parameter ? parameter->value : 0.0f;
431}
433 const auto* parameter = parameterAt(index);
434 return parameter ? parameter->minimum : 0.0f;
435}
437 const auto* parameter = parameterAt(index);
438 return parameter ? parameter->maximum : 1.0f;
439}
441 const auto* parameter = parameterAt(index);
442 return parameter && selectedType_ == "avatar.parameter" && parameter->id.value() == selectedId_;
443}
444
446 const auto* expression = expressionAt(index);
447 return expression ? expression->id.value() : std::string{};
448}
450 const auto* expression = expressionAt(index);
451 return expression ? expression->name : std::string{};
452}
454 const auto* expression = expressionAt(index);
455 return expression && selectedType_ == "avatar.expression" && expression->id.value() == selectedId_;
456}
458 const auto* expression = expressionAt(index);
459 return expression ? static_cast<int>(expression->channels.size()) : 0;
460}
461std::string AvatarDocumentEditor::expressionChannelName(int expression, int channel) const {
462 const auto* value = expressionAt(expression);
463 if (!value || channel < 0 || static_cast<std::size_t>(channel) >= value->channels.size()) return {};
464 auto it = value->channels.begin();
465 for (int i = 0; i < channel; ++i) ++it;
466 return it->first;
467}
468
469const AvatarDocumentEditor::PreviewRect* AvatarDocumentEditor::previewAt(int index) const {
470 if (index < 0 || static_cast<std::size_t>(index) >= preview_.size()) return nullptr;
471 return &preview_[static_cast<std::size_t>(index)];
472}
473const avatar_editing::AvatarLayerValue* AvatarDocumentEditor::layerAt(int index) const {
474 if (index < 0 || static_cast<std::size_t>(index) >= target_.layers().size()) return nullptr;
475 return &target_.layers()[static_cast<std::size_t>(index)];
476}
477const avatar_editing::AvatarParameterValue* AvatarDocumentEditor::parameterAt(int index) const {
478 if (index < 0 || static_cast<std::size_t>(index) >= target_.parameters().size()) return nullptr;
479 return &target_.parameters()[static_cast<std::size_t>(index)];
480}
481const avatar_editing::AvatarExpressionValue* AvatarDocumentEditor::expressionAt(int index) const {
482 if (index < 0 || static_cast<std::size_t>(index) >= target_.expressions().size()) return nullptr;
483 return &target_.expressions()[static_cast<std::size_t>(index)];
484}
485
486} // namespace eve::avatar_editor
ActionParameterOperation operation
double value
float w
Definition AnimClip.cpp:738
float y
Definition AnimClip.cpp:738
float x
Definition AnimClip.cpp:738
float z
Definition AnimClip.cpp:738
std::map< std::string, std::vector< Key >, std::less<> > channels
int root
Definition AnimSmr.cpp:119
UI-neutral avatar editor: workspace, layer composite preview, undo.
std::string title
std::string label
std::string message
tensor::Graph g
Definition GpuGraph.cpp:7
wgpu::PopErrorScopeStatus status
double r
int h
std::string name
MeleePoint3 b
Definition MeleeHit.cpp:41
MeleePoint3 a
Definition MeleeHit.cpp:40
std::vector< std::int32_t > order
graphics::Canvas * previous
TileLayer * layer
std::string id
Definition PlayHost.cpp:108
std::uint32_t targetId
bool found
int created
bool visible
uint32_t index
double oy
double ox
const VegetationPresetContext & context
void ignore(std::string_view reason={}) const noexcept
Explicitly discard this result after documenting the reason.
Definition Result.h:537
Move-only operation result carrying either a value or Status.
Definition Result.h:155
static Result failure(Status status)
Construct a failed result from a structured status.
Definition Result.h:175
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
Result< DomainOperation > makeSet(const SelectionSnapshot &, const PropertyPath &, const EditorValue &, PropertySetMode) const override
Make set.
const std::vector< AvatarExpressionValue > & expressions() const
Expressions.
Result< void > loadSnapshot(const EditorValue &)
Loads snapshot.
Result< DomainOperation > makeDeleteParameter(const ObjectId &) const
Make delete parameter.
Result< DomainOperation > makeCreateExpression(const AvatarExpressionValue &) const
Make create expression.
Result< DomainOperation > makeCreateLayer(const AvatarLayerValue &) const
Make create layer.
Result< DomainOperation > makeCreateParameter(const AvatarParameterValue &) const
Make create parameter.
Result< DomainOperation > makeDeleteExpression(const ObjectId &) const
Make delete expression.
Result< DomainOperation > makeDeleteLayer(const ObjectId &) const
Make delete layer.
const std::vector< AvatarLayerValue > & layers() const
Layers.
TargetId targetId() const override
Target id.
const std::vector< AvatarParameterValue > & parameters() const
Parameters.
std::string expressionChannelName(int expression, int channel) const
Expression channel name.
avatar_editing::Result< void > setParameterValue(double value)
avatar_editing::Result< void > deleteSelectedExpression()
float parameterValue(int index) const
Parameter value.
std::string layerId(int index) const
Layer id.
std::string previewName(int index) const
Preview name.
float parameterMinimum(int index) const
Parameter minimum.
std::string expressionId(int index) const
Expression id.
avatar_editing::Result< editor::TransactionReceipt > redo()
avatar_editing::Result< void > selectParameter(std::string id)
float previewX(int index) const
Preview x.
avatar_editing::Result< void > deleteSelectedParameter()
avatar_editing::Result< void > selectExpression(std::string id)
std::string expressionName(int index) const
Expression name.
float previewA(int index) const
Preview a.
float previewB(int index) const
Preview b.
avatar_editing::Result< void > pointerDown(float x, float y)
avatar_editing::Result< void > createParameter(std::string id, std::string name)
bool isParameterSelected(int index) const
True when parameter selected.
avatar_editing::Result< void > deleteSelectedLayer()
float parameterMaximum(int index) const
Parameter maximum.
int expressionChannelCount(int index) const
Expression channel count.
bool isExpressionSelected(int index) const
True when expression selected.
avatar_editing::Result< void > createLayer(std::string id, std::string name)
AvatarDocumentEditor(std::string targetId)
Construct a seeded two-layer face with a referenced parameter.
float previewW(int index) const
Preview w.
std::uint64_t revision() const noexcept
Revision.
float previewG(int index) const
Preview g.
std::string parameterName(int index) const
Parameter name.
avatar_editing::Result< void > createExpression(std::string id, std::string name)
avatar_editing::Result< void > selectLayer(std::string id)
avatar_editing::Result< void > setLayerZ(int zIndex)
std::string parameterId(int index) const
Parameter id.
avatar_editing::Result< editor::TransactionReceipt > undo()
std::string layerName(int index) const
Layer name.
avatar_editing::Result< void > setLayerVisible(bool visible)
float previewR(int index) const
Preview r.
bool isPreviewSelected(int index) const
True when preview selected.
avatar_editing::Result< void > configureWorkspace(editor::EditorWorkspace &workspace) const
Install Layers / Preview / Inspector / Parameters / Expressions panels.
float previewH(int index) const
Preview h.
bool layerVisible(int index) const
Layer visible.
bool isLayerSelected(int index) const
True when layer selected.
float previewY(int index) const
Preview y.
std::uint64_t revision() const override
Return the current revision.
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
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.
Result< TransactionReceipt > undo() override
Compensate the most recent committed transaction.
Result< void > discard() override
Discard the active, not-yet-committed operation list.
Result< TransactionReceipt > redo() override
Reapply the most recently compensated transaction.
Result< void > append(DomainOperation operation) override
Append an operation to the active transaction without applying it.
editing::Status EditorStatus
editing::ObjectId ObjectId
editing::Value EditorValue
editing::RuleId RuleId
eve::editing::StableId StableId
Definition EditorIds.h:36
eve::editing::TargetId TargetId
Definition EditorIds.h:29
eve::editing::TransactionId TransactionId
Definition EditorIds.h:34
constexpr HexDirection next(HexDirection d) noexcept
The next direction clockwise (NW wraps to NE).
Definition HexMetrics.h:76
std::vector< float > floats(const RuntimeTensor &v)
Floats.
Named expression containing layer/parameter channel targets.
Stable image layer in an Avatar asset.
Reflected Avatar parameter and its initial value.
SelectionItem public API.
SelectionSnapshot public API.
std::vector< SelectionItem > items
std::optional< SelectionItem > primary
TransactionSpec public API.
glm::vec4 color