载入中...
搜索中...
未找到
VoxelCatalog.cpp
浏览该文件的文档.
2
3#include <algorithm>
4#include <cmath>
5#include <limits>
6#include <optional>
7#include <set>
8#include <utility>
9
11namespace {
12
13const EditorValue* field(const EditorValue& v, const char* k) {
14 const auto* o = v.getIf<EditorValue::Object>();
15 if (!o) return nullptr;
16 const auto i = o->find(k);
17 return i == o->end() ? nullptr : &i->second;
18}
19
20bool errors(const std::vector<EditorDiagnostic>& d) {
21 return std::any_of(d.begin(), d.end(), [](const auto& v) { return v.severity() == DiagnosticSeverity::Error; });
22}
23
24int pack(int x, int y, int z) { return x | (y << 8) | (z << 16); }
25
26bool inBounds(const VoxelModelValue& model, int x, int y, int z) {
27 return x >= 0 && y >= 0 && z >= 0 && x < model.sizeX && y < model.sizeY && z < model.sizeZ;
28}
29
30bool validSize(int n) { return n >= 1 && n <= kVoxelModelMaxSize; }
31
32EditorValue socketValue(const VoxelSocket& s) {
33 return EditorValue::Object{{"tag", s.tag}, {"kind", std::string(voxelSocketKindName(s.kind))}};
34}
35
36Result<VoxelSocket> parseSocket(const EditorValue& v) {
37 const auto* tag = field(v, "tag");
38 const auto* kind = field(v, "kind");
39 const auto* tags = tag ? tag->getIf<std::string>() : nullptr;
40 const auto* kinds = kind ? kind->getIf<std::string>() : nullptr;
41 if (!tags || !kinds)
42 return eve::editing::failed<VoxelSocket>(EditorStatus::Rejected, RuleId("editor.voxel-model.socket"),
43 "Voxel socket requires tag and kind");
44 VoxelSocket out;
45 out.tag = *tags;
46 out.kind = voxelSocketKindFromName(*kinds);
47 if (*kinds != voxelSocketKindName(out.kind) && !kinds->empty())
48 return eve::editing::failed<VoxelSocket>(EditorStatus::Rejected, RuleId("editor.voxel-model.socket-kind"),
49 "Unknown voxel socket kind");
50 return eve::editing::applied<VoxelSocket>(std::move(out));
51}
52
54 const auto* a = value ? value->getIf<EditorValue::Array>() : nullptr;
55 if (!a || a->size() != 6)
56 return eve::editing::failed<std::array<VoxelSocket, 6>>(
57 EditorStatus::Rejected, RuleId("editor.voxel-model.sockets"), "Voxel sockets require six faces");
58 std::array<VoxelSocket, 6> out{};
59 for (int i = 0; i < 6; ++i) {
60 auto parsed = parseSocket((*a)[static_cast<std::size_t>(i)]);
61 if (!parsed.ok())
62 return eve::editing::failed<std::array<VoxelSocket, 6>>(parsed.code(), RuleId("editor.voxel-model.socket"),
63 "Voxel socket cannot be parsed");
64 out[static_cast<std::size_t>(i)] = parsed.value();
65 }
66 return eve::editing::applied<std::array<VoxelSocket, 6>>(out);
67}
68
69EditorValue voxelsValue(const std::vector<VoxelCoord>& voxels) {
71 a.reserve(voxels.size());
72 for (const auto& v : voxels) a.push_back(int64_t{pack(v.x, v.y, v.z)});
73 return a;
74}
75
77 const auto* a = value ? value->getIf<EditorValue::Array>() : nullptr;
78 if (!a)
79 return eve::editing::failed<std::vector<VoxelCoord>>(
80 EditorStatus::Rejected, RuleId("editor.voxel-model.voxels"), "Voxel occupancy must be an array");
81 if (a->size() > static_cast<std::size_t>(kVoxelModelMaxOccupied))
82 return eve::editing::failed<std::vector<VoxelCoord>>(
83 EditorStatus::Rejected, RuleId("editor.voxel-model.voxels-capacity"), "Model occupancy exceeds 4096 cells");
84 std::set<int> unique;
85 std::vector<VoxelCoord> out;
86 out.reserve(a->size());
87 for (const auto& entry : *a) {
88 const auto* n = entry.getIf<int64_t>();
89 if (!n || *n < 0)
90 return eve::editing::failed<std::vector<VoxelCoord>>(EditorStatus::Rejected,
91 RuleId("editor.voxel-model.voxel"),
92 "Voxel cell must be a packed non-negative integer");
93 VoxelCoord coord;
94 coord.x = static_cast<int>(*n) & 255;
95 coord.y = (static_cast<int>(*n) >> 8) & 255;
96 coord.z = (static_cast<int>(*n) >> 16) & 255;
97 if (!unique.insert(static_cast<int>(*n)).second)
98 return eve::editing::failed<std::vector<VoxelCoord>>(
99 EditorStatus::Rejected, RuleId("editor.voxel-model.voxel-dup"), "Voxel cells must be unique");
100 out.push_back(coord);
101 }
102 return eve::editing::applied<std::vector<VoxelCoord>>(std::move(out));
103}
104
105const int64_t* intField(const EditorValue& v, const char* key) {
106 const auto* f = field(v, key);
107 return f ? f->getIf<int64_t>() : nullptr;
108}
109
110EditorValue modelValue(const VoxelModelValue& v) {
111 EditorValue::Array sockets;
112 for (const auto& s : v.sockets) sockets.push_back(socketValue(s));
113 return EditorValue::Object{{"id", v.id.value()},
114 {"name", v.name},
115 {"sizeX", int64_t{v.sizeX}},
116 {"sizeY", int64_t{v.sizeY}},
117 {"sizeZ", int64_t{v.sizeZ}},
118 {"sockets", std::move(sockets)},
119 {"voxels", voxelsValue(v.voxels)}};
120}
121
122Result<VoxelModelValue> parseModel(const EditorValue& v) {
123 const auto *id = field(v, "id"), *name = field(v, "name");
124 const auto* ids = id ? id->getIf<std::string>() : nullptr;
125 const auto* names = name ? name->getIf<std::string>() : nullptr;
126 const auto* sx = intField(v, "sizeX");
127 const auto* sy = intField(v, "sizeY");
128 const auto* sz = intField(v, "sizeZ");
129 auto sockets = parseSockets(field(v, "sockets"));
130 auto voxels = parseVoxels(field(v, "voxels"));
131 if (!ids || ids->empty() || !names || names->empty() || !sx || !sy || !sz || !sockets.ok() || !voxels.ok())
132 return eve::editing::failed<VoxelModelValue>(EditorStatus::Rejected, RuleId("editor.voxel-model.entry"),
133 "Voxel model entry is invalid");
134 VoxelModelValue out;
135 out.id = ObjectId(*ids);
136 out.name = *names;
137 out.sizeX = static_cast<int>(*sx);
138 out.sizeY = static_cast<int>(*sy);
139 out.sizeZ = static_cast<int>(*sz);
140 out.sockets = sockets.value();
141 out.voxels = std::move(voxels).takeValue();
142 return eve::editing::applied<VoxelModelValue>(std::move(out));
143}
144
148 d.displayNameKey = "editor.voxel-model." + path;
149 d.category = "voxel-model";
150 d.type = type;
151 d.defaultValue = std::move(value);
152 d.flags = PropertyFlag::Runtime;
153 return d;
154}
155
156bool parseFacePath(const PropertyPath& path, int& face, std::string& fieldName) {
157 const std::string& text = path.value();
158 const std::string head = "model.socket.";
159 if (text.rfind(head, 0) != 0) return false;
160 const auto rest = text.substr(head.size());
161 const auto dot = rest.find('.');
162 if (dot == std::string::npos) return false;
163 try {
164 face = std::stoi(rest.substr(0, dot));
165 } catch (...) {
166 return false;
167 }
168 if (face < 0 || face > 5) return false;
169 fieldName = rest.substr(dot + 1);
170 return fieldName == "tag" || fieldName == "kind";
171}
172
173} // namespace
174
176 if (name == "symmetric") return VoxelSocketKind::Symmetric;
177 if (name == "male") return VoxelSocketKind::Male;
178 if (name == "female") return VoxelSocketKind::Female;
179 return VoxelSocketKind::None;
180}
181
183 switch (kind) {
184 case VoxelSocketKind::Symmetric:
185 return "symmetric";
186 case VoxelSocketKind::Male:
187 return "male";
188 case VoxelSocketKind::Female:
189 return "female";
190 case VoxelSocketKind::None:
191 default:
192 return "none";
193 }
194}
195
197 if (a.kind == VoxelSocketKind::None || b.kind == VoxelSocketKind::None) return false;
198 if (a.tag.empty() || a.tag != b.tag) return false;
199 if (a.kind == VoxelSocketKind::Symmetric && b.kind == VoxelSocketKind::Symmetric) return true;
200 return (a.kind == VoxelSocketKind::Male && b.kind == VoxelSocketKind::Female) ||
201 (a.kind == VoxelSocketKind::Female && b.kind == VoxelSocketKind::Male);
202}
203
204int voxelOppositeFace(int face) {
205 static constexpr int opp[] = {1, 0, 3, 2, 5, 4};
206 if (face < 0 || face > 5) return face;
207 return opp[face];
208}
209
210bool isVoxelModelOccupied(const VoxelModelValue& model, int x, int y, int z) {
211 return std::any_of(model.voxels.begin(), model.voxels.end(),
212 [&](const auto& v) { return v.x == x && v.y == y && v.z == z; });
213}
214
216 if (model.voxels.empty()) return VoxelCellFill::Empty;
217 const int volume = model.sizeX * model.sizeY * model.sizeZ;
218 if (volume > 0 && static_cast<int>(model.voxels.size()) == volume) return VoxelCellFill::Filled;
219 return VoxelCellFill::Partial;
220}
221
222VoxelPick pickVoxelModel(const VoxelModelValue& model, float ox, float oy, float oz, float dx, float dy, float dz,
223 float maxDistance) {
224 VoxelPick pick;
225 const float lenSq = dx * dx + dy * dy + dz * dz;
226 if (lenSq <= 1e-12f || maxDistance <= 0.f) return pick;
227 const float invLen = 1.f / std::sqrt(lenSq);
228 const float rx = dx * invLen;
229 const float ry = dy * invLen;
230 const float rz = dz * invLen;
231 int ix = static_cast<int>(std::floor(ox));
232 int iy = static_cast<int>(std::floor(oy));
233 int iz = static_cast<int>(std::floor(oz));
234 int lastX = ix;
235 int lastY = iy;
236 int lastZ = iz;
237 bool haveEmpty = false;
238 int emptyX = 0;
239 int emptyY = 0;
240 int emptyZ = 0;
241 auto recordEmpty = [&](int x, int y, int z) {
242 if (haveEmpty || !inBounds(model, x, y, z) || isVoxelModelOccupied(model, x, y, z)) return;
243 haveEmpty = true;
244 emptyX = x;
245 emptyY = y;
246 emptyZ = z;
247 };
248 if (isVoxelModelOccupied(model, ix, iy, iz)) {
249 pick.hit = true;
250 pick.canAttach = inBounds(model, ix, iy, iz);
251 pick.hitX = pick.prevX = ix;
252 pick.hitY = pick.prevY = iy;
253 pick.hitZ = pick.prevZ = iz;
254 return pick;
255 }
256 recordEmpty(ix, iy, iz);
257 const float inf = std::numeric_limits<float>::infinity();
258 const int stepX = rx > 0.f ? 1 : -1;
259 const int stepY = ry > 0.f ? 1 : -1;
260 const int stepZ = rz > 0.f ? 1 : -1;
261 const float absInvX = rx != 0.f ? std::fabs(1.f / rx) : inf;
262 const float absInvY = ry != 0.f ? std::fabs(1.f / ry) : inf;
263 const float absInvZ = rz != 0.f ? std::fabs(1.f / rz) : inf;
264 float tMaxX = rx != 0.f ? (rx > 0.f ? float(ix + 1) - ox : ox - float(ix)) * absInvX : inf;
265 float tMaxY = ry != 0.f ? (ry > 0.f ? float(iy + 1) - oy : oy - float(iy)) * absInvY : inf;
266 float tMaxZ = rz != 0.f ? (rz > 0.f ? float(iz + 1) - oz : oz - float(iz)) * absInvZ : inf;
267 for (int iter = 0; iter < 4096; ++iter) {
268 float t = 0.f;
269 if (tMaxX <= tMaxY && tMaxX <= tMaxZ) {
270 ix += stepX;
271 t = tMaxX;
272 tMaxX += absInvX;
273 } else if (tMaxY <= tMaxZ) {
274 iy += stepY;
275 t = tMaxY;
276 tMaxY += absInvY;
277 } else {
278 iz += stepZ;
279 t = tMaxZ;
280 tMaxZ += absInvZ;
281 }
282 if (t > maxDistance) break;
283 if (isVoxelModelOccupied(model, ix, iy, iz)) {
284 pick.hit = true;
285 pick.hitX = ix;
286 pick.hitY = iy;
287 pick.hitZ = iz;
288 pick.prevX = lastX;
289 pick.prevY = lastY;
290 pick.prevZ = lastZ;
291 pick.canAttach =
292 inBounds(model, lastX, lastY, lastZ) && !isVoxelModelOccupied(model, lastX, lastY, lastZ);
293 return pick;
294 }
295 recordEmpty(ix, iy, iz);
296 lastX = ix;
297 lastY = iy;
298 lastZ = iz;
299 }
300 if (haveEmpty) {
301 pick.canAttach = true;
302 pick.prevX = emptyX;
303 pick.prevY = emptyY;
304 pick.prevZ = emptyZ;
305 }
306 return pick;
307}
308
309VoxelCatalogTarget::VoxelCatalogTarget(std::string id) : id_(std::move(id)) {}
310
312 return {TargetId(id_),
313 "voxel-catalog",
315 false,
317}
318
320 if (c == propertyCapabilityId()) return static_cast<IPropertyProvider*>(this);
322 return nullptr;
323}
324
326 const auto it = std::find_if(models_.begin(), models_.end(), [&](const auto& v) { return v.id == id; });
327 return it == models_.end() ? nullptr : &*it;
328}
329
330VoxelModelValue* VoxelCatalogTarget::findModelMut(const ObjectId& id) {
331 return const_cast<VoxelModelValue*>(findModel(id));
332}
333
334bool VoxelCatalogTarget::matches(const SelectionSnapshot& s) const {
335 if (s.items.empty()) return false;
336 for (const auto& i : s.items) {
337 if (i.target != TargetId(id_) || !findModel(ObjectId(i.item.value()))) return false;
338 }
339 return true;
340}
341
349
352 schema.typeId = "voxel.model";
353 schema.properties.push_back(prop("model.name", PropertyType::String, ""));
354 for (int face = 0; face < 6; ++face) {
355 schema.properties.push_back(prop("model.socket." + std::to_string(face) + ".tag", PropertyType::String, ""));
356 schema.properties.push_back(
357 prop("model.socket." + std::to_string(face) + ".kind", PropertyType::String, "none"));
358 }
359 return schema;
360}
361
363 if (!matches(s) || !schema(s).find(p)) return {};
364 std::optional<EditorValue> common;
365 for (const auto& i : s.items) {
366 const auto* model = findModel(ObjectId(i.item.value()));
367 if (!model) return {};
369 if (p == PropertyPath("model.name"))
370 value = model->name;
371 else {
372 int face = 0;
373 std::string fieldName;
374 if (!parseFacePath(p, face, fieldName)) return {};
375 const auto& socket = model->sockets[static_cast<std::size_t>(face)];
376 value = fieldName == "tag" ? EditorValue(socket.tag)
377 : EditorValue(std::string(voxelSocketKindName(socket.kind)));
378 }
379 if (!common)
380 common = value;
381 else if (*common != value)
382 return {PropertyReadState::Mixed, {}, {}};
383 }
384 return {PropertyReadState::Value, *common, {}};
385}
386
387EditorValue VoxelCatalogTarget::contentValue() const {
389 for (const auto& v : models_) models.push_back(modelValue(v));
390 return EditorValue::Object{{"models", std::move(models)}};
391}
392
393Result<DomainOperation> VoxelCatalogTarget::replacement(EditorValue content, std::string property) const {
395 op.type = "voxel.catalog.replace.v1";
396 op.inverseType = op.type;
397 op.target = TargetId(id_);
398 op.payload = std::move(content);
399 op.inverse = contentValue();
400 op.hasInverse = true;
401 if (!property.empty()) op.affectedProperties.push_back(property);
402 op.mergeKey = "voxel-catalog:" + id_ + ":" + (property.empty() ? "structure" : property);
403 return eve::editing::applied<DomainOperation>(std::move(op));
404}
405
407 const EditorValue& value, PropertySetMode mode) const {
408 if (mode == PropertySetMode::Reset) return makeReset(s, p);
409 if (!matches(s) || !schema(s).find(p) || mode != PropertySetMode::Absolute)
410 return eve::editing::failed<DomainOperation>(EditorStatus::Rejected, RuleId("editor.voxel-model.set"),
411 "Voxel model property edit is invalid");
412 auto c = *this;
413 for (const auto& i : s.items) {
414 auto* model = c.findModelMut(ObjectId(i.item.value()));
415 if (!model) continue;
416 if (p == PropertyPath("model.name")) {
417 const auto* name = value.getIf<std::string>();
418 if (!name || name->empty())
419 return eve::editing::failed<DomainOperation>(EditorStatus::Rejected, RuleId("editor.voxel-model.name"),
420 "Model name must be non-empty");
421 model->name = *name;
422 } else {
423 int face = 0;
424 std::string fieldName;
425 if (!parseFacePath(p, face, fieldName))
426 return eve::editing::failed<DomainOperation>(
427 EditorStatus::Unsupported, RuleId("editor.voxel-model.property"), "Unknown voxel model property");
428 auto& socket = model->sockets[static_cast<std::size_t>(face)];
429 if (fieldName == "tag") {
430 const auto* tag = value.getIf<std::string>();
431 if (!tag)
432 return eve::editing::failed<DomainOperation>(
433 EditorStatus::Rejected, RuleId("editor.voxel-model.tag"), "Socket tag must be a string");
434 socket.tag = *tag;
435 } else {
436 const auto* kind = value.getIf<std::string>();
437 if (!kind)
438 return eve::editing::failed<DomainOperation>(
439 EditorStatus::Rejected, RuleId("editor.voxel-model.kind"), "Socket kind must be a string");
440 socket.kind = voxelSocketKindFromName(*kind);
441 if (*kind != voxelSocketKindName(socket.kind))
442 return eve::editing::failed<DomainOperation>(
443 EditorStatus::Rejected, RuleId("editor.voxel-model.kind"), "Unknown voxel socket kind");
444 }
445 }
446 }
447 if (errors(c.validate()))
448 return eve::editing::failed<DomainOperation>(EditorStatus::Rejected, RuleId("editor.voxel-model.invalid"),
449 "Voxel model property edit failed validation");
450 return replacement(c.contentValue(), p.value());
451}
452
454 auto d = schema(s).find(p);
455 if (!d)
456 return eve::editing::failed<DomainOperation>(EditorStatus::Unsupported, RuleId("editor.voxel-model.property"),
457 "Unknown voxel model property");
458 return makeSet(s, p, d->defaultValue, PropertySetMode::Absolute);
459}
460
462 if (v.id.empty() || v.name.empty() || findModel(v.id) ||
463 std::any_of(models_.begin(), models_.end(), [&](const auto& x) { return x.name == v.name; }))
464 return eve::editing::failed<DomainOperation>(EditorStatus::Rejected, RuleId("editor.voxel-model.identity"),
465 "Voxel model ID and name must be unique");
466 auto c = *this;
467 c.models_.push_back(v);
468 if (errors(c.validate()))
469 return eve::editing::failed<DomainOperation>(EditorStatus::Rejected, RuleId("editor.voxel-model.invalid"),
470 "Voxel model create failed validation");
471 return replacement(c.contentValue());
472}
473
475 if (!findModel(id))
476 return eve::editing::failed<DomainOperation>(EditorStatus::NotFound, RuleId("editor.voxel-model.entry"),
477 "Voxel model does not exist");
478 auto c = *this;
479 std::erase_if(c.models_, [&](const auto& v) { return v.id == id; });
480 return replacement(c.contentValue());
481}
482
484 bool occupied) const {
485 const auto* current = findModel(model);
486 if (!current)
487 return eve::editing::failed<DomainOperation>(EditorStatus::NotFound, RuleId("editor.voxel-model.entry"),
488 "Voxel model does not exist");
489 if (!inBounds(*current, x, y, z))
490 return eve::editing::failed<DomainOperation>(EditorStatus::Rejected, RuleId("editor.voxel-model.range"),
491 "Voxel is outside the model bounds");
492 auto c = *this;
493 auto* dest = c.findModelMut(model);
494 const auto it =
495 std::find_if(dest->voxels.begin(), dest->voxels.end(), [&](const auto& v) { return v.x == x && v.y == y && v.z == z; });
496 if (occupied && it == dest->voxels.end())
497 dest->voxels.push_back({x, y, z});
498 else if (!occupied && it != dest->voxels.end())
499 dest->voxels.erase(it);
500 if (errors(c.validate()))
501 return eve::editing::failed<DomainOperation>(EditorStatus::Rejected, RuleId("editor.voxel-model.invalid"),
502 "Voxel occupancy edit failed validation");
503 return replacement(c.contentValue(), "model.voxels");
504}
505
506std::vector<EditorDiagnostic> VoxelCatalogTarget::validate() const {
507 std::vector<EditorDiagnostic> d;
508 std::set<std::string> ids, names;
509 for (const auto& model : models_) {
510 if (model.id.empty() || model.name.empty() || !ids.insert(model.id.value()).second ||
511 !names.insert(model.name).second)
512 d.push_back(eve::editing::ruleDiagnostic(eve::DiagnosticCode::Conflict, RuleId("editor.voxel-model.identity"),
513 DiagnosticSeverity::Error,
514 "Voxel model IDs and names must be non-empty and unique"));
515 if (!validSize(model.sizeX) || !validSize(model.sizeY) || !validSize(model.sizeZ))
517 RuleId("editor.voxel-model.size"), DiagnosticSeverity::Error,
518 "Model size must be 1..32 on each axis"));
519 std::set<int> unique;
520 for (const auto& v : model.voxels) {
521 if (!inBounds(model, v.x, v.y, v.z) || !unique.insert(pack(v.x, v.y, v.z)).second)
523 RuleId("editor.voxel-model.voxel"), DiagnosticSeverity::Error,
524 "Occupied cells must be unique and inside the model"));
525 }
526 if (static_cast<int>(model.voxels.size()) > kVoxelModelMaxOccupied)
528 RuleId("editor.voxel-model.capacity"), DiagnosticSeverity::Error,
529 "Model occupancy exceeds 4096 cells"));
530 if (model.voxels.empty())
532 RuleId("editor.voxel-model.empty"), DiagnosticSeverity::Warning,
533 "Voxel model has no occupied cells"));
534 }
535 if (models_.empty())
537 RuleId("editor.voxel-model.empty-project"), DiagnosticSeverity::Warning,
538 "Voxel project has no models"));
539 return d;
540}
541
543 if (op.target != TargetId(id_) || op.type != "voxel.catalog.replace.v1")
544 return eve::editing::failed<void>(EditorStatus::Rejected, RuleId("editor.voxel-model.operation"),
545 "Voxel catalog operation mismatch");
546 const auto* models = field(op.payload, "models");
547 const auto* modelArray = models ? models->getIf<EditorValue::Array>() : nullptr;
548 if (!modelArray || !op.payload.isWithinLimits(8, 20000, 1024 * 1024))
549 return eve::editing::failed<void>(EditorStatus::Rejected, RuleId("editor.voxel-model.payload"),
550 "Voxel catalog payload exceeds limits");
552 for (const auto& v : *modelArray) {
553 auto parsed = parseModel(v);
554 if (!parsed.ok())
555 return eve::editing::failed<void>(EditorStatus::Rejected, RuleId("editor.voxel-model.entry"),
556 "Voxel model cannot be parsed");
557 c.models_.push_back(parsed.value());
558 }
559 if (errors(c.validate()))
560 return eve::editing::failed<void>(EditorStatus::Rejected, RuleId("editor.voxel-model.invalid"),
561 "Voxel catalog validation failed");
562 models_ = std::move(c.models_);
563 bumpRevision();
564 widenDirty(0, 0);
565 return eve::editing::applied<void>();
566}
567
568std::unique_ptr<IDomainOperationTarget> VoxelCatalogTarget::cloneDomainState() const {
569 return std::make_unique<VoxelCatalogTarget>(*this);
570}
571
572Result<void> VoxelCatalogTarget::commitDomainState(std::unique_ptr<IDomainOperationTarget> c) {
573 auto* t = dynamic_cast<VoxelCatalogTarget*>(c.get());
574 if (!t || t->id_ != id_)
575 return eve::editing::failed<void>(EditorStatus::Conflict, RuleId("editor.voxel-model.candidate"),
576 "Voxel catalog candidate mismatch");
577 *this = *t;
578 return eve::editing::applied<void>();
579}
580
582 return EditorValue::Object{{"schemaVersion", int64_t{1}}, {"content", contentValue()}};
583}
584
586 const auto *v = field(s, "schemaVersion"), *content = field(s, "content");
587 const auto* version = v ? v->getIf<int64_t>() : nullptr;
588 if (!version || *version != 1 || !content)
589 return eve::editing::failed<void>(EditorStatus::Unsupported, RuleId("editor.voxel-model.snapshot"),
590 "Unsupported Voxel catalog snapshot");
592 op.target = TargetId(id_);
593 op.type = "voxel.catalog.replace.v1";
594 op.payload = *content;
595 auto result = applyDomainOperation(op);
596 if (result.ok()) clearDirtyRegion();
597 return result;
598}
599
600std::vector<ObjectId> VoxelCatalogTarget::hullJoinPartners(const ObjectId& model, int face) const {
601 std::vector<ObjectId> partners;
602 const auto* self = findModel(model);
603 if (!self || face < 0 || face > 5) return partners;
604 const auto opposite = voxelOppositeFace(face);
605 const auto& socket = self->sockets[static_cast<std::size_t>(face)];
606 for (const auto& other : models_) {
607 if (other.id == model) continue;
608 if (canJoinVoxelSockets(socket, other.sockets[static_cast<std::size_t>(opposite)]))
609 partners.push_back(other.id);
610 }
611 return partners;
612}
613
614} // namespace eve::voxel_editing
double value
float y
Definition AnimClip.cpp:738
float x
Definition AnimClip.cpp:738
float z
Definition AnimClip.cpp:738
double volume
const std::string & s
glm::vec4 p[6]
std::uint32_t key
glm::uvec4 ids
glm::vec3 n
Definition Grass.cpp:63
float v
std::int32_t second
std::int32_t c
std::string text
TokenKind kind
std::string name
MeleePoint3 b
Definition MeleeHit.cpp:41
MeleePoint3 a
Definition MeleeHit.cpp:40
const std::string * tag
float f
std::string path
Definition PlayHost.cpp:110
std::string id
Definition PlayHost.cpp:108
float d
float t
glm::mat4 model
double current
float dz
float dy
float dx
bool occupied
double oy
double ox
MagicaVoxel-style sculpted models: bounded occupancy grids and hull sockets.
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
UUID-backed identifier adapter for legacy textual boundaries.
Definition Identity.h:314
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.
Optional capability exposing a deterministic, persistence-safe editing snapshot.
static CapabilityId editingCapabilityId()
Stable capability identity shared by every editable domain target.
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
bool isWithinLimits(size_t maxDepth, size_t maxElements, size_t maxStringBytes) const
Validate nesting, element count and approximate string byte limits.
std::map< std::string, Value > Object
const T * getIf() const
Return an immediately borrowed typed value.
Revisioned project of MagicaVoxel-style sculpted models.
Result< DomainOperation > makeSet(const SelectionSnapshot &, const PropertyPath &, const EditorValue &, PropertySetMode) const override
Make set.
std::vector< ObjectId > hullJoinPartners(const ObjectId &model, int face) const
Hull join partners.
Result< void > applyDomainOperation(const DomainOperation &) override
Applies domain operation.
Result< DomainOperation > makeCreateModel(const VoxelModelValue &) const
Make create model.
Result< DomainOperation > makeSetVoxel(const ObjectId &model, int x, int y, int z, bool occupied) const
Make set voxel.
const VoxelModelValue * findModel(const ObjectId &) const
Look up a sculpted model by stable id.
Result< void > loadSnapshot(const EditorValue &)
Loads snapshot.
EditorValue snapshotValue() const override
Snapshot value.
PropertyReadResult read(const SelectionSnapshot &, const PropertyPath &) const override
Reads .
Result< DomainOperation > makeReset(const SelectionSnapshot &, const PropertyPath &) const override
Make reset.
eve::Result< eve::Revision > currentRevision(const SelectionSnapshot &) const override
Current revision.
Result< void > commitDomainState(std::unique_ptr< IDomainOperationTarget >) override
Commits domain state.
PropertySchema schema(const SelectionSnapshot &) const override
Schema.
std::unique_ptr< IDomainOperationTarget > cloneDomainState() const override
Clones domain state.
std::vector< EditorDiagnostic > validate() const
Validate.
void * queryCapability(const CapabilityId &) override
Query Inspector property and snapshot capabilities.
Result< DomainOperation > makeDeleteModel(const ObjectId &) const
Make delete model.
TargetDescriptor describe() const override
Describe.
const std::vector< VoxelModelValue > & models() const
Models.
static CapabilityId propertyCapabilityId()
Property capability id.
PropertyType
PropertyType public API.
PropertySetMode
PropertySetMode public API.
Diagnostic ruleDiagnostic(eve::DiagnosticCode code, RuleId rule, DiagnosticSeverity severity, std::string message)
Build a common diagnostic carrying an open editing rule identity.
VoxelSocketKind
Polarity of one authored join socket.
VoxelCellFill voxelClassifyModelFill(const VoxelModelValue &model)
Classify a sculpted model as empty, partial, or a solid cube.
constexpr int kVoxelModelMaxOccupied
Occupied-cell budget for one sculpted object.
editing::ObjectId ObjectId
editing::DomainOperation DomainOperation
VoxelSocketKind voxelSocketKindFromName(std::string_view name)
Parse a socket kind name.
editing::TargetId TargetId
bool canJoinVoxelSockets(const VoxelSocket &a, const VoxelSocket &b)
True when two facing sockets may join.
bool isVoxelModelOccupied(const VoxelModelValue &model, int x, int y, int z)
True when model contains an occupied cell at (x,y,z).
editing::Value EditorValue
int voxelOppositeFace(int face)
Opposite FaceDir index in PosX/NegX/PosY/NegY/PosZ/NegZ order.
constexpr int kVoxelModelMaxSize
Inclusive maximum MagicaVoxel-like object edge (cells).
const char * voxelSocketKindName(VoxelSocketKind kind)
Stable serialized name for a socket kind. @ownership Observed static string; callers must not free it...
VoxelCellFill
Derived occupancy of a sculpted object.
editing::RuleId RuleId
VoxelPick pickVoxelModel(const VoxelModelValue &model, float ox, float oy, float oz, float dx, float dy, float dz, float maxDistance)
Raycast occupied cells with MagicaVoxel-style previous-cell attach.
editing::SelectionSnapshot SelectionSnapshot
editing::PropertyPath PropertyPath
DomainOperation public API.
PropertyDescriptor 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.
One occupied cell inside a sculpted model.
One MagicaVoxel-style object: a bounded occupancy grid.
std::array< VoxelSocket, 6 > sockets
std::vector< VoxelCoord > voxels
DDA pick against a sculpted model.
One face socket: matching tag plus compatible polarity.