载入中...
搜索中...
未找到
MapDocument.cpp
浏览该文件的文档.
2
3#include <algorithm>
4#include <cmath>
5#include <set>
6#include <utility>
7
8namespace eve::map_editing {
9namespace {
10
11const EditorValue* field(const EditorValue& value, const char* key) {
12 const auto* object = value.getIf<EditorValue::Object>();
13 if (!object) return nullptr;
14 const auto found = object->find(key);
15 return found == object->end() ? nullptr : &found->second;
16}
17
18EditorValue layerValue(const MapLayerRecord& layer) {
19 return EditorValue::Object{{"id", layer.id.value()}, {"name", layer.name}, {"kind", layer.kind},
20 {"visible", layer.visible}, {"locked", layer.locked}, {"opacity", layer.opacity},
21 {"order", int64_t{layer.order}}};
22}
23
24Result<MapLayerRecord> parseLayer(const EditorValue& value) {
25 const auto* idValue = field(value, "id");
26 const auto* nameValue = field(value, "name");
27 const auto* kindValue = field(value, "kind");
28 const auto* visibleValue = field(value, "visible");
29 const auto* lockedValue = field(value, "locked");
30 const auto* opacityValue = field(value, "opacity");
31 const auto* orderValue = field(value, "order");
32
33 const auto* id = idValue ? idValue->getIf<std::string>() : nullptr;
34 const auto* name = nameValue ? nameValue->getIf<std::string>() : nullptr;
35 const auto* kind = kindValue ? kindValue->getIf<std::string>() : nullptr;
36
37 const auto* visible = visibleValue ? visibleValue->getIf<bool>() : nullptr;
38 const auto* locked = lockedValue ? lockedValue->getIf<bool>() : nullptr;
39 const auto* opacity = opacityValue ? opacityValue->getIf<double>() : nullptr;
40 const auto* order = orderValue ? orderValue->getIf<int64_t>() : nullptr;
41
42 static const std::set<std::string> kinds{"tile", "object", "road", "group"};
43 if (!id || id->empty() || !name || name->empty() || !kind || !kinds.contains(*kind) || !visible || !locked ||
44 !opacity || *opacity < 0.0 || *opacity > 1.0 || !order)
45 return eve::editing::failed<MapLayerRecord>(EditorStatus::Rejected, RuleId("editor.map.invalid-layer"),
46 "Map layer has invalid identity, kind, opacity or order");
47 return eve::editing::applied<MapLayerRecord>(
48 {StableId(*id), *name, *kind, *visible, *locked, *opacity, static_cast<int>(*order)});
49}
50
51EditorValue pointValue(const MapSplinePointRecord& point) {
53 {"id", point.id.value()}, {"x", point.x}, {"y", point.y}, {"z", point.z}, {"width", point.width}};
54}
55
57 const auto* idValue = field(value, "id");
58 const auto* xValue = field(value, "x");
59 const auto* yValue = field(value, "y");
60 const auto* zValue = field(value, "z");
61 const auto* widthValue = field(value, "width");
62 const auto* id = idValue ? idValue->getIf<std::string>() : nullptr;
63 const auto* x = xValue ? xValue->getIf<double>() : nullptr;
64 const auto* y = yValue ? yValue->getIf<double>() : nullptr;
65 const auto* z = zValue ? zValue->getIf<double>() : nullptr;
66 const auto* width = widthValue ? widthValue->getIf<double>() : nullptr;
67 if (!id || id->empty() || !x || !y || !z || !width || *width <= 0.0 || !std::isfinite(*x) || !std::isfinite(*y) ||
68 !std::isfinite(*z) || !std::isfinite(*width))
69 return eve::editing::failed<MapSplinePointRecord>(
70 EditorStatus::Rejected, RuleId("editor.map.invalid-spline-point"),
71 "Spline point requires finite coordinates and positive width");
72 return eve::editing::applied<MapSplinePointRecord>({StableId(*id), *x, *y, *z, *width});
73}
74
75EditorValue roadValue(const MapRoadRecord& road) {
77 for (const MapSplinePointRecord& point : road.points) points.push_back(pointValue(point));
78 return EditorValue::Object{{"id", road.id.value()}, {"layer", road.layer.value()},
79 {"name", road.name}, {"materialAsset", road.materialAsset},
80 {"closed", road.closed}, {"points", std::move(points)}};
81}
82
83Result<MapRoadRecord> parseRoad(const EditorValue& value) {
84 const auto* idValue = field(value, "id");
85 const auto* layerValueEntry = field(value, "layer");
86 const auto* nameValue = field(value, "name");
87 const auto* materialValue = field(value, "materialAsset");
88 const auto* closedValue = field(value, "closed");
89 const auto* pointsValue = field(value, "points");
90
91 const auto* id = idValue ? idValue->getIf<std::string>() : nullptr;
92 const auto* layer = layerValueEntry ? layerValueEntry->getIf<std::string>() : nullptr;
93 const auto* name = nameValue ? nameValue->getIf<std::string>() : nullptr;
94 const auto* material = materialValue ? materialValue->getIf<std::string>() : nullptr;
95 const auto* closed = closedValue ? closedValue->getIf<bool>() : nullptr;
96 const auto* points = pointsValue ? pointsValue->getIf<EditorValue::Array>() : nullptr;
97
98 if (!id || id->empty() || !layer || layer->empty() || !name || name->empty() || !material || !closed || !points ||
99 points->size() < 2)
100 return eve::editing::failed<MapRoadRecord>(EditorStatus::Rejected, RuleId("editor.map.invalid-road"),
101 "Road requires identity, layer, name and at least two points");
102 MapRoadRecord result{StableId(*id), StableId(*layer), *name, *material, *closed, {}};
103 std::set<StableId> pointIds;
104 for (const EditorValue& pointValueEntry : *points) {
105 auto point = parsePoint(pointValueEntry);
106 if (!point.ok() || !pointIds.insert(point.value().id).second)
107 return eve::editing::failed<MapRoadRecord>(EditorStatus::Rejected,
108 RuleId("editor.map.duplicate-spline-point"),
109 "Road spline point ids must be unique");
110 result.points.push_back(std::move(point).value());
111 }
112 if (result.closed && result.points.size() < 3)
113 return eve::editing::failed<MapRoadRecord>(EditorStatus::Rejected, RuleId("editor.map.closed-road-point-count"),
114 "Closed road requires at least three points");
115 return eve::editing::applied<MapRoadRecord>(std::move(result));
116}
117
118EditorValue placementValue(const MapPlacementRecord& placement) {
119 return EditorValue::Object{{"id", placement.id.value()},
120 {"layer", placement.layer.value()},
121 {"asset", placement.asset},
122 {"x", placement.x},
123 {"y", placement.y},
124 {"z", placement.z},
125 {"rotationX", placement.rotationX},
126 {"rotationY", placement.rotationY},
127 {"rotationZ", placement.rotationZ},
128 {"scaleX", placement.scaleX},
129 {"scaleY", placement.scaleY},
130 {"scaleZ", placement.scaleZ}};
131}
132
133Result<MapPlacementRecord> parsePlacement(const EditorValue& value) {
134 const auto string = [&](const char* key) -> const std::string* {
135 const EditorValue* entry = field(value, key);
136 return entry ? entry->getIf<std::string>() : nullptr;
137 };
138 const auto number = [&](const char* key) -> const double* {
139 const EditorValue* entry = field(value, key);
140 return entry ? entry->getIf<double>() : nullptr;
141 };
142 const auto* id = string("id");
143 const auto* layer = string("layer");
144 const auto* asset = string("asset");
145 const auto* x = number("x");
146 const auto* y = number("y");
147 const auto* z = number("z");
148 const auto* rx = number("rotationX");
149 const auto* ry = number("rotationY");
150 const auto* rz = number("rotationZ");
151 const auto* sx = number("scaleX");
152 const auto* sy = number("scaleY");
153 const auto* sz = number("scaleZ");
154 if (!id || id->empty() || !layer || layer->empty() || !asset || asset->empty() || !x || !y || !z || !rx || !ry ||
155 !rz || !sx || !sy || !sz || *sx <= 0.0 || *sy <= 0.0 || *sz <= 0.0)
156 return eve::editing::failed<MapPlacementRecord>(
157 EditorStatus::Rejected, RuleId("editor.map.invalid-placement"),
158 "Placement requires ids, asset, complete TRS and positive scale");
159 for (const double* component : {x, y, z, rx, ry, rz, sx, sy, sz})
160 if (!std::isfinite(*component))
161 return eve::editing::failed<MapPlacementRecord>(
162 EditorStatus::Rejected, RuleId("editor.map.nonfinite-placement"), "Placement transform must be finite");
163 return eve::editing::applied<MapPlacementRecord>(
164 {StableId(*id), StableId(*layer), *asset, *x, *y, *z, *rx, *ry, *rz, *sx, *sy, *sz});
165}
166
167DomainOperation operation(std::string type, std::string inverseType, const std::string& target, EditorValue payload,
168 EditorValue inverse, const StableId& affected) {
169 DomainOperation result;
170 result.type = std::move(type);
171 result.inverseType = std::move(inverseType);
172 result.target = TargetId(target);
173 result.payload = std::move(payload);
174 result.inverse = std::move(inverse);
175 result.hasInverse = true;
176 result.affectedObjects.push_back({TargetId(target), affected.value(), 0});
177 return result;
178}
179
180} // namespace
181
182MapDocumentTarget::MapDocumentTarget(std::string id) : id_(std::move(id)) {}
183
185 TargetDescriptor result;
186 result.id = TargetId(id_);
187 result.type = "map-document";
188 result.revision = revisionValue();
190 return result;
191}
192
193void* MapDocumentTarget::queryCapability(const CapabilityId& capability) {
195 : nullptr;
196}
197
199 if (domainOperation.target != TargetId(id_))
200 return eve::editing::failed<void>(EditorStatus::Rejected, RuleId("editor.map.target-mismatch"),
201 "Map operation targets another document");
202 if (domainOperation.type == "map.layer.create.v1") {
203 auto layer = parseLayer(domainOperation.payload);
204 if (!layer.ok())
205 return eve::editing::failed<void>(layer.code(), RuleId("editor.map.invalid-layer"),
206 "Layer payload is invalid");
207 if (layers_.contains(layer.value().id))
208 return eve::editing::failed<void>(EditorStatus::Conflict, RuleId("editor.map.layer-exists"),
209 "Map layer already exists");
210 for (const auto& [id, current] : layers_) {
211 static_cast<void>(id);
212 if (current.order == layer.value().order)
213 return eve::editing::failed<void>(EditorStatus::Conflict, RuleId("editor.map.layer-order-conflict"),
214 "Map layer visual order is already occupied");
215 }
216 auto layerValue = std::move(layer).value();
217 layers_.emplace(layerValue.id, std::move(layerValue));
218 } else if (domainOperation.type == "map.layer.delete.v1") {
219 auto layer = parseLayer(domainOperation.payload);
220 if (!layer.ok() || !layers_.contains(layer.value().id))
221 return eve::editing::failed<void>(EditorStatus::NotFound, RuleId("editor.map.layer-not-found"),
222 "Map layer was not found");
223 if (layers_.at(layer.value().id).locked)
224 return eve::editing::failed<void>(EditorStatus::Rejected, RuleId("editor.map.layer-locked"),
225 "Map layer is locked");
226 for (const auto& [id, road] : roads_) {
227 static_cast<void>(id);
228 if (road.layer == layer.value().id)
229 return eve::editing::failed<void>(EditorStatus::Rejected, RuleId("editor.map.layer-not-empty"),
230 "Map layer still contains roads");
231 }
232 for (const auto& [id, placement] : placements_) {
233 static_cast<void>(id);
234 if (placement.layer == layer.value().id)
235 return eve::editing::failed<void>(EditorStatus::Rejected, RuleId("editor.map.layer-not-empty"),
236 "Map layer still contains placements");
237 }
238 layers_.erase(layer.value().id);
239 } else if (domainOperation.type == "map.layer.set.v1") {
240 auto layer = parseLayer(domainOperation.payload);
241 if (!layer.ok() || !layers_.contains(layer.value().id))
242 return eve::editing::failed<void>(EditorStatus::NotFound, RuleId("editor.map.layer-not-found"),
243 "Map layer was not found");
244 for (const auto& [id, current] : layers_)
245 if (id != layer.value().id && current.order == layer.value().order)
246 return eve::editing::failed<void>(EditorStatus::Conflict, RuleId("editor.map.layer-order-conflict"),
247 "Map layer visual order is already occupied");
248 if (layers_.at(layer.value().id).kind != layer.value().kind) {
249 for (const auto& [id, road] : roads_) {
250 static_cast<void>(id);
251 if (road.layer == layer.value().id)
252 return eve::editing::failed<void>(EditorStatus::Rejected, RuleId("editor.map.layer-kind-in-use"),
253 "Cannot change kind of a layer containing roads");
254 }
255 for (const auto& [id, placement] : placements_) {
256 static_cast<void>(id);
257 if (placement.layer == layer.value().id)
258 return eve::editing::failed<void>(EditorStatus::Rejected, RuleId("editor.map.layer-kind-in-use"),
259 "Cannot change kind of a layer containing placements");
260 }
261 }
262 auto layerValue = std::move(layer).value();
263 layers_[layerValue.id] = std::move(layerValue);
264 } else if (domainOperation.type == "map.road.set.v1") {
265 auto road = parseRoad(domainOperation.payload);
266 if (!road.ok())
267 return eve::editing::failed<void>(road.code(), RuleId("editor.map.invalid-road"),
268 "Road payload is invalid");
269 const auto layer = layers_.find(road.value().layer);
270 if (layer == layers_.end() || layer->second.kind != "road")
271 return eve::editing::failed<void>(EditorStatus::Rejected, RuleId("editor.map.road-layer-required"),
272 "Road must reference a road layer");
273 if (layer->second.locked)
274 return eve::editing::failed<void>(EditorStatus::Rejected, RuleId("editor.map.layer-locked"),
275 "Road layer is locked");
276 const auto existing = roads_.find(road.value().id);
277 if (existing != roads_.end() && layers_.at(existing->second.layer).locked)
278 return eve::editing::failed<void>(EditorStatus::Rejected, RuleId("editor.map.layer-locked"),
279 "Road cannot be moved out of a locked layer");
280 auto roadValue = std::move(road).value();
281 roads_[roadValue.id] = std::move(roadValue);
282 } else if (domainOperation.type == "map.road.delete.v1") {
283 auto road = parseRoad(domainOperation.payload);
284 if (!road.ok() || !roads_.contains(road.value().id))
285 return eve::editing::failed<void>(EditorStatus::NotFound, RuleId("editor.map.road-not-found"),
286 "Road was not found");
287 if (layers_.at(roads_.at(road.value().id).layer).locked)
288 return eve::editing::failed<void>(EditorStatus::Rejected, RuleId("editor.map.layer-locked"),
289 "Road layer is locked");
290 roads_.erase(road.value().id);
291 } else if (domainOperation.type == "map.placement.set.v1") {
292 auto placement = parsePlacement(domainOperation.payload);
293 if (!placement.ok())
294 return eve::editing::failed<void>(placement.code(), RuleId("editor.map.invalid-placement"),
295 "Placement payload is invalid");
296 const auto layer = layers_.find(placement.value().layer);
297 if (layer == layers_.end() || layer->second.kind != "object")
298 return eve::editing::failed<void>(EditorStatus::Rejected, RuleId("editor.map.object-layer-required"),
299 "Placement must reference an object layer");
300 if (layer->second.locked)
301 return eve::editing::failed<void>(EditorStatus::Rejected, RuleId("editor.map.layer-locked"),
302 "Object layer is locked");
303 const auto existing = placements_.find(placement.value().id);
304 if (existing != placements_.end() && layers_.at(existing->second.layer).locked)
305 return eve::editing::failed<void>(EditorStatus::Rejected, RuleId("editor.map.layer-locked"),
306 "Placement cannot be moved out of a locked layer");
307 auto placementValue = std::move(placement).value();
308 placements_[placementValue.id] = std::move(placementValue);
309 } else if (domainOperation.type == "map.placement.delete.v1") {
310 auto placement = parsePlacement(domainOperation.payload);
311 if (!placement.ok() || !placements_.contains(placement.value().id))
312 return eve::editing::failed<void>(EditorStatus::NotFound, RuleId("editor.map.placement-not-found"),
313 "Placement was not found");
314 if (layers_.at(placements_.at(placement.value().id).layer).locked)
315 return eve::editing::failed<void>(EditorStatus::Rejected, RuleId("editor.map.layer-locked"),
316 "Object layer is locked");
317 placements_.erase(placement.value().id);
318 } else {
319 return eve::editing::failed<void>(EditorStatus::Unsupported, RuleId("editor.map.operation-unsupported"),
320 "Map operation is unsupported: " + domainOperation.type);
321 }
322 bumpRevision();
323 widenDirty(0, 0);
324 return eve::editing::applied<void>();
325}
326
327std::unique_ptr<IDomainOperationTarget> MapDocumentTarget::cloneDomainState() const {
328 return std::make_unique<MapDocumentTarget>(*this);
329}
330
331Result<void> MapDocumentTarget::commitDomainState(std::unique_ptr<IDomainOperationTarget> candidate) {
332 auto* staged = dynamic_cast<MapDocumentTarget*>(candidate.get());
333 if (!staged || staged->id_ != id_)
334 return eve::editing::failed<void>(EditorStatus::Conflict, RuleId("editor.map.candidate-mismatch"),
335 "Map candidate belongs to another target");
336 layers_.swap(staged->layers_);
337 roads_.swap(staged->roads_);
338 placements_.swap(staged->placements_);
339 setRevision(staged->revisionValue());
340 setDirtyRegion(staged->dirtyRegion());
341 return eve::editing::applied<void>();
342}
343
344std::vector<MapLayerRecord> MapDocumentTarget::mapLayers() const {
345 std::vector<MapLayerRecord> result;
346 for (const auto& [id, layer] : layers_) {
347 static_cast<void>(id);
348 result.push_back(layer);
349 }
350 std::sort(result.begin(), result.end(), [](const MapLayerRecord& left, const MapLayerRecord& right) {
351 return left.order != right.order ? left.order < right.order : left.id < right.id;
352 });
353 return result;
354}
355
356std::vector<MapRoadRecord> MapDocumentTarget::mapRoads() const {
357 std::vector<MapRoadRecord> result;
358 for (const auto& [id, road] : roads_) {
359 static_cast<void>(id);
360 result.push_back(road);
361 }
362 return result;
363}
364
365std::vector<MapPlacementRecord> MapDocumentTarget::mapPlacements() const {
366 std::vector<MapPlacementRecord> result;
367 for (const auto& [id, placement] : placements_) {
368 static_cast<void>(id);
369 result.push_back(placement);
370 }
371 return result;
372}
373
375 auto parsed = parseLayer(layerValue(layer));
376 if (!parsed.ok())
377 return eve::editing::failed<DomainOperation>(parsed.code(), RuleId("editor.map.invalid-layer"),
378 "Layer is invalid");
379 if (layers_.contains(layer.id))
380 return eve::editing::failed<DomainOperation>(EditorStatus::Conflict, RuleId("editor.map.layer-exists"),
381 "Map layer already exists");
382 for (const auto& [id, current] : layers_) {
383 static_cast<void>(id);
384 if (current.order == layer.order)
385 return eve::editing::failed<DomainOperation>(EditorStatus::Conflict,
386 RuleId("editor.map.layer-order-conflict"),
387 "Map layer visual order is already occupied");
388 }
389 return eve::editing::applied<DomainOperation>(
390 operation("map.layer.create.v1", "map.layer.delete.v1", id_, layerValue(layer), layerValue(layer), layer.id));
391}
392
394 const auto found = layers_.find(layer);
395 if (found == layers_.end())
396 return eve::editing::failed<DomainOperation>(EditorStatus::NotFound, RuleId("editor.map.layer-not-found"),
397 "Map layer was not found");
398 if (found->second.locked)
399 return eve::editing::failed<DomainOperation>(EditorStatus::Rejected, RuleId("editor.map.layer-locked"),
400 "Map layer is locked");
401 for (const auto& [id, road] : roads_) {
402 static_cast<void>(id);
403 if (road.layer == layer)
404 return eve::editing::failed<DomainOperation>(EditorStatus::Rejected, RuleId("editor.map.layer-not-empty"),
405 "Delete roads before deleting their layer");
406 }
407 for (const auto& [id, placement] : placements_) {
408 static_cast<void>(id);
409 if (placement.layer == layer)
410 return eve::editing::failed<DomainOperation>(EditorStatus::Rejected, RuleId("editor.map.layer-not-empty"),
411 "Delete placements before deleting their layer");
412 }
413 return eve::editing::applied<DomainOperation>(operation("map.layer.delete.v1", "map.layer.create.v1", id_,
414 layerValue(found->second), layerValue(found->second),
415 layer));
416}
417
419 const auto found = layers_.find(layer.id);
420 if (found == layers_.end())
421 return eve::editing::failed<DomainOperation>(EditorStatus::NotFound, RuleId("editor.map.layer-not-found"),
422 "Map layer was not found");
423 auto parsed = parseLayer(layerValue(layer));
424 if (!parsed.ok())
425 return eve::editing::failed<DomainOperation>(parsed.code(), RuleId("editor.map.invalid-layer"),
426 "Layer is invalid");
427 for (const auto& [id, current] : layers_)
428 if (id != layer.id && current.order == layer.order)
429 return eve::editing::failed<DomainOperation>(EditorStatus::Conflict,
430 RuleId("editor.map.layer-order-conflict"),
431 "Map layer visual order is already occupied");
432 if (found->second.kind != layer.kind) {
433 for (const auto& [id, road] : roads_) {
434 static_cast<void>(id);
435 if (road.layer == layer.id)
436 return eve::editing::failed<DomainOperation>(EditorStatus::Rejected,
437 RuleId("editor.map.layer-kind-in-use"),
438 "Cannot change kind of a layer containing roads");
439 }
440 for (const auto& [id, placement] : placements_) {
441 static_cast<void>(id);
442 if (placement.layer == layer.id)
443 return eve::editing::failed<DomainOperation>(EditorStatus::Rejected,
444 RuleId("editor.map.layer-kind-in-use"),
445 "Cannot change kind of a layer containing placements");
446 }
447 }
448 return eve::editing::applied<DomainOperation>(
449 operation("map.layer.set.v1", "map.layer.set.v1", id_, layerValue(layer), layerValue(found->second), layer.id));
450}
451
453 auto parsed = parseRoad(roadValue(road));
454 if (!parsed.ok())
455 return eve::editing::failed<DomainOperation>(parsed.code(), RuleId("editor.map.invalid-road"),
456 "Road is invalid");
457 const auto layer = layers_.find(road.layer);
458 if (layer == layers_.end() || layer->second.kind != "road")
459 return eve::editing::failed<DomainOperation>(EditorStatus::Rejected, RuleId("editor.map.road-layer-required"),
460 "Road must reference a road layer");
461 if (layer->second.locked)
462 return eve::editing::failed<DomainOperation>(EditorStatus::Rejected, RuleId("editor.map.layer-locked"),
463 "Road layer is locked");
464 const auto found = roads_.find(road.id);
465 if (found != roads_.end() && layers_.at(found->second.layer).locked)
466 return eve::editing::failed<DomainOperation>(EditorStatus::Rejected, RuleId("editor.map.layer-locked"),
467 "Road cannot be moved out of a locked layer");
468 const EditorValue inverse = found == roads_.end() ? roadValue(road) : roadValue(found->second);
469 const std::string inverseType = found == roads_.end() ? "map.road.delete.v1" : "map.road.set.v1";
470 return eve::editing::applied<DomainOperation>(
471 operation("map.road.set.v1", inverseType, id_, roadValue(road), inverse, road.id));
472}
473
475 const auto found = roads_.find(road);
476 if (found == roads_.end())
477 return eve::editing::failed<DomainOperation>(EditorStatus::NotFound, RuleId("editor.map.road-not-found"),
478 "Road was not found");
479 if (layers_.at(found->second.layer).locked)
480 return eve::editing::failed<DomainOperation>(EditorStatus::Rejected, RuleId("editor.map.layer-locked"),
481 "Road layer is locked");
482 return eve::editing::applied<DomainOperation>(operation("map.road.delete.v1", "map.road.set.v1", id_,
483 roadValue(found->second), roadValue(found->second), road));
484}
485
487 auto parsed = parsePlacement(placementValue(placement));
488 if (!parsed.ok())
489 return eve::editing::failed<DomainOperation>(parsed.code(), RuleId("editor.map.invalid-placement"),
490 "Placement is invalid");
491 const auto layer = layers_.find(placement.layer);
492 if (layer == layers_.end() || layer->second.kind != "object")
493 return eve::editing::failed<DomainOperation>(EditorStatus::Rejected, RuleId("editor.map.object-layer-required"),
494 "Placement must reference an object layer");
495 if (layer->second.locked)
496 return eve::editing::failed<DomainOperation>(EditorStatus::Rejected, RuleId("editor.map.layer-locked"),
497 "Object layer is locked");
498 const auto found = placements_.find(placement.id);
499 if (found != placements_.end() && layers_.at(found->second.layer).locked)
500 return eve::editing::failed<DomainOperation>(EditorStatus::Rejected, RuleId("editor.map.layer-locked"),
501 "Placement cannot be moved out of a locked layer");
502 const EditorValue inverse = found == placements_.end() ? placementValue(placement) : placementValue(found->second);
503 const std::string inverseType = found == placements_.end() ? "map.placement.delete.v1" : "map.placement.set.v1";
504 return eve::editing::applied<DomainOperation>(
505 operation("map.placement.set.v1", inverseType, id_, placementValue(placement), inverse, placement.id));
506}
507
509 const auto found = placements_.find(placement);
510 if (found == placements_.end())
511 return eve::editing::failed<DomainOperation>(EditorStatus::NotFound, RuleId("editor.map.placement-not-found"),
512 "Placement was not found");
513 if (layers_.at(found->second.layer).locked)
514 return eve::editing::failed<DomainOperation>(EditorStatus::Rejected, RuleId("editor.map.layer-locked"),
515 "Object layer is locked");
516 return eve::editing::applied<DomainOperation>(operation("map.placement.delete.v1", "map.placement.set.v1", id_,
517 placementValue(found->second),
518 placementValue(found->second), placement));
519}
520
521std::vector<EditorDiagnostic> MapDocumentTarget::validate() const {
522 std::vector<EditorDiagnostic> diagnostics;
523 std::set<int> orders;
524 for (const auto& [id, layer] : layers_) {
525 static_cast<void>(id);
526 if (!orders.insert(layer.order).second)
527 diagnostics.push_back(eve::editing::ruleDiagnostic(
528 eve::DiagnosticCode::InvariantViolation, RuleId("editor.map.duplicate-layer-order"),
529 DiagnosticSeverity::Error, "Multiple map layers share visual order " + std::to_string(layer.order)));
530 }
531 for (const auto& [id, road] : roads_) {
532 const auto layer = layers_.find(road.layer);
533 if (layer == layers_.end() || layer->second.kind != "road")
534 diagnostics.push_back(eve::editing::ruleDiagnostic(
535 eve::DiagnosticCode::InvariantViolation, RuleId("editor.map.invalid-road-layer"),
536 DiagnosticSeverity::Error, "Road references a missing or non-road layer: " + id.value()));
537 double lengthSquared = 0.0;
538 for (size_t index = 1; index < road.points.size(); ++index) {
539 const double dx = road.points[index].x - road.points[index - 1].x;
540 const double dy = road.points[index].y - road.points[index - 1].y;
541 const double dz = road.points[index].z - road.points[index - 1].z;
542 lengthSquared += dx * dx + dy * dy + dz * dz;
543 }
544 if (lengthSquared <= 1e-12)
545 diagnostics.push_back(eve::editing::ruleDiagnostic(
546 eve::DiagnosticCode::InvariantViolation, RuleId("editor.map.zero-length-road"),
547 DiagnosticSeverity::Error, "Road has no geometric length: " + id.value()));
548 if (road.materialAsset.empty())
549 diagnostics.push_back(eve::editing::ruleDiagnostic(
550 eve::DiagnosticCode::PreconditionViolation, RuleId("editor.map.road-material-missing"),
551 DiagnosticSeverity::Warning, "Road has no material asset: " + id.value()));
552 }
553 for (const auto& [id, placement] : placements_) {
554 const auto layer = layers_.find(placement.layer);
555 if (layer == layers_.end() || layer->second.kind != "object")
556 diagnostics.push_back(eve::editing::ruleDiagnostic(
557 eve::DiagnosticCode::InvariantViolation, RuleId("editor.map.invalid-placement-layer"),
558 DiagnosticSeverity::Error, "Placement references a missing or non-object layer: " + id.value()));
559 }
560 return diagnostics;
561}
562
563MapRoadPreviewResult MapDocumentTarget::previewRoad(const StableId& roadId, int triangleBudget) const {
566 result.road = roadId;
567 const auto found = roads_.find(roadId);
568 if (found == roads_.end()) {
569 result.status = EditorStatus::NotFound;
571 eve::DiagnosticCode::NotFound, RuleId("editor.map.road-not-found"), DiagnosticSeverity::Error,
572 "Road preview target was not found"));
573 return result;
574 }
575 if (triangleBudget <= 0) {
576 result.status = EditorStatus::Rejected;
578 eve::DiagnosticCode::InvalidArgument, RuleId("editor.map.invalid-preview-budget"),
579 DiagnosticSeverity::Error, "Road preview triangle budget must be positive"));
580 return result;
581 }
582 const MapRoadRecord& road = found->second;
583 const int segmentCount = static_cast<int>(road.points.size()) - (road.closed ? 0 : 1);
584 const int triangleCount = segmentCount * 2;
585 if (triangleCount > triangleBudget) {
586 result.status = EditorStatus::Rejected;
588 eve::DiagnosticCode::PreconditionViolation, RuleId("editor.map.road-preview-budget"),
589 DiagnosticSeverity::Error, "Road strip exceeds preview triangle budget"));
590 return result;
591 }
594 for (size_t index = 0; index < road.points.size(); ++index) {
595 const MapSplinePointRecord& point = road.points[index];
597 road.points[index == 0 ? (road.closed ? road.points.size() - 1 : 0) : index - 1];
598 const MapSplinePointRecord& next =
599 road.points[index + 1 == road.points.size() ? (road.closed ? 0 : road.points.size() - 1) : index + 1];
600 double tangentX = next.x - previous.x;
601 double tangentZ = next.z - previous.z;
602 const double length = std::sqrt(tangentX * tangentX + tangentZ * tangentZ);
603 if (length <= 1e-9) {
604 result.status = EditorStatus::Failed;
606 eve::DiagnosticCode::InvariantViolation, RuleId("editor.map.degenerate-road-tangent"),
607 DiagnosticSeverity::Error,
608 "Road preview contains a degenerate tangent at point " + point.id.value()));
609 return result;
610 }
611 tangentX /= length;
612 tangentZ /= length;
613 const double halfWidth = point.width * 0.5;
614 vertices.emplace_back(
615 EditorValue::Array{point.x - tangentZ * halfWidth, point.y, point.z + tangentX * halfWidth});
616 vertices.emplace_back(
617 EditorValue::Array{point.x + tangentZ * halfWidth, point.y, point.z - tangentX * halfWidth});
618 if (index > 0) {
619 const int64_t base = static_cast<int64_t>(index * 2);
620 for (const int64_t value : {base - 2, base - 1, base, base, base - 1, base + 1})
621 indices.emplace_back(value);
622 const double dx = point.x - previous.x;
623 const double dy = point.y - previous.y;
624 const double dz = point.z - previous.z;
625 result.centerlineLength += std::sqrt(dx * dx + dy * dy + dz * dz);
626 }
627 }
628 if (road.closed) {
629 const int64_t last = static_cast<int64_t>(road.points.size() * 2 - 2);
630 for (const int64_t value : {last, last + 1, int64_t{0}, int64_t{0}, last + 1, int64_t{1}})
631 indices.emplace_back(value);
632 const MapSplinePointRecord& first = road.points.front();
633 const MapSplinePointRecord& lastPoint = road.points.back();
634 const double dx = first.x - lastPoint.x;
635 const double dy = first.y - lastPoint.y;
636 const double dz = first.z - lastPoint.z;
637 result.centerlineLength += std::sqrt(dx * dx + dy * dy + dz * dz);
638 }
639 result.mesh = EditorValue::Object{{"primitive", "triangles"},
640 {"vertices", std::move(vertices)},
641 {"indices", std::move(indices)},
642 {"materialAsset", road.materialAsset}};
643 result.status = EditorStatus::Applied;
644 return result;
645}
646
648 EditorValue::Array layers;
649 for (const MapLayerRecord& layer : mapLayers()) layers.push_back(layerValue(layer));
650 EditorValue::Array roads;
651 for (const MapRoadRecord& road : mapRoads()) roads.push_back(roadValue(road));
652 EditorValue::Array placements;
653 for (const MapPlacementRecord& placement : mapPlacements()) placements.push_back(placementValue(placement));
654 return EditorValue::Object{{"schemaVersion", int64_t{1}},
655 {"layers", std::move(layers)},
656 {"roads", std::move(roads)},
657 {"placements", std::move(placements)}};
658}
659
661 const auto* versionValue = field(snapshot, "schemaVersion");
662 const auto* layersValue = field(snapshot, "layers");
663 const auto* roadsValue = field(snapshot, "roads");
664 const auto* placementsValue = field(snapshot, "placements");
665 const auto* version = versionValue ? versionValue->getIf<int64_t>() : nullptr;
666 const auto* layers = layersValue ? layersValue->getIf<EditorValue::Array>() : nullptr;
667 const auto* roads = roadsValue ? roadsValue->getIf<EditorValue::Array>() : nullptr;
668 const auto* placements = placementsValue ? placementsValue->getIf<EditorValue::Array>() : nullptr;
669 if (!version || *version != 1 || !layers || !roads || !placements)
670 return eve::editing::failed<void>(EditorStatus::Unsupported, RuleId("editor.map.invalid-snapshot"),
671 "Map snapshot schema is invalid or unsupported");
672 MapDocumentTarget candidate(id_);
673 for (const EditorValue& value : *layers) {
674 auto layer = parseLayer(value);
675 if (!layer.ok())
676 return eve::editing::failed<void>(EditorStatus::Rejected, RuleId("editor.map.invalid-layer"),
677 "Invalid layer");
678 auto create = candidate.makeCreateLayer(layer.value());
679 if (!create.ok() || !candidate.applyDomainOperation(create.value()).ok())
680 return eve::editing::failed<void>(EditorStatus::Rejected, RuleId("editor.map.invalid-layer-set"),
681 "Map snapshot contains conflicting layers");
682 }
683 for (const EditorValue& value : *roads) {
684 auto road = parseRoad(value);
685 if (!road.ok())
686 return eve::editing::failed<void>(EditorStatus::Rejected, RuleId("editor.map.invalid-road"),
687 "Invalid road");
688 auto set = candidate.makeSetRoad(road.value());
689 if (!set.ok() || !candidate.applyDomainOperation(set.value()).ok())
690 return eve::editing::failed<void>(EditorStatus::Rejected, RuleId("editor.map.invalid-road-set"),
691 "Map snapshot contains an invalid road layer reference");
692 }
693 for (const EditorValue& value : *placements) {
694 auto placement = parsePlacement(value);
695 if (!placement.ok())
696 return eve::editing::failed<void>(EditorStatus::Rejected, RuleId("editor.map.invalid-placement"),
697 "Invalid placement");
698 auto set = candidate.makeSetPlacement(placement.value());
699 if (!set.ok() || !candidate.applyDomainOperation(set.value()).ok())
700 return eve::editing::failed<void>(EditorStatus::Rejected, RuleId("editor.map.invalid-placement-set"),
701 "Map snapshot contains an invalid placement layer reference");
702 }
703 layers_.swap(candidate.layers_);
704 roads_.swap(candidate.roads_);
705 placements_.swap(candidate.placements_);
706 bumpRevision();
708 return eve::editing::applied<void>();
709}
710
711} // namespace eve::map_editing
LogicalId target
ActionParameterOperation operation
double value
Value::Object payload
bool locked
float y
Definition AnimClip.cpp:738
float x
Definition AnimClip.cpp:738
float z
Definition AnimClip.cpp:738
float length
Definition CaveMesh.cpp:94
std::uint32_t key
std::vector< std::uint32_t > indices
HexVec3 left
HexVec3 right
std::int32_t first
std::uint32_t width
TokenKind kind
std::string name
std::vector< std::int32_t > order
graphics::Canvas * previous
TileLayer * layer
std::vector< Point > vertices
float halfWidth
std::string id
Definition PlayHost.cpp:108
std::shared_ptr< const std::vector< glm::vec2 > > points
Material * material
double number
bool found
double current
std::string string
float dz
float dy
float dx
float opacity
bool visible
uint32_t index
glm::vec3 point
bool ok() const noexcept
Whether this result represents a non-failure outcome.
Definition Result.h:480
Move-only operation result carrying either a value or Status.
Definition Result.h:155
const std::string & value() const noexcept
Returns the exact legacy or canonical compatibility spelling.
Definition Identity.h:362
void setRevision(Revision revision)
Adopt a revision observed elsewhere, without advancing it.
void bumpRevision()
Advance the revision without touching the dirty region.
void setDirtyRegion(const EditRegion &region)
Replace the dirty region wholesale, without advancing the revision.
Revision revisionValue() const noexcept
Return the revision as the shared protocol revision type.
void widenDirty()
Mark an unknown area dirty without advancing the revision.
eve::OptionalRef< C > capability()
Query a capability through its interface-owned stable identity.
virtual void clearDirtyRegion()=0
Clears dirty region.
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.
Layer, spline and placement editing capability for map presenters.
static CapabilityId editorCapabilityId()
Stable capability id for map structure operations.
UI-neutral map structure document with reversible domain operations.
EditorValue snapshotValue() const
Capture deterministic map structure data.
Result< DomainOperation > makeSetPlacement(const MapPlacementRecord &placement) const override
Make set placement.
Result< DomainOperation > makeDeletePlacement(const StableId &placement) const override
Make delete placement.
std::unique_ptr< IDomainOperationTarget > cloneDomainState() const override
Clones domain state.
Result< DomainOperation > makeSetLayer(const MapLayerRecord &layer) const override
Make set layer.
Result< DomainOperation > makeDeleteRoad(const StableId &road) const override
Make delete road.
Result< DomainOperation > makeSetRoad(const MapRoadRecord &road) const override
Make set road.
Result< DomainOperation > makeCreateLayer(const MapLayerRecord &layer) const override
Make create layer.
TargetDescriptor describe() const override
Describe.
std::vector< MapPlacementRecord > mapPlacements() const override
Map placements.
Result< void > commitDomainState(std::unique_ptr< IDomainOperationTarget > candidate) override
Commits domain state.
Result< void > loadSnapshot(const EditorValue &snapshot)
Atomically load schema-version-one map structure data.
std::vector< MapLayerRecord > mapLayers() const override
Map layers.
std::vector< EditorDiagnostic > validate() const
Validate layers, spline geometry and placement references.
void * queryCapability(const CapabilityId &capability) override
Query an optional target capability.
MapRoadPreviewResult previewRoad(const StableId &road, int triangleBudget=10000) const
Build a deterministic XZ road strip for viewport preview and budget diagnostics.
std::vector< MapRoadRecord > mapRoads() const override
Map roads.
Result< void > applyDomainOperation(const DomainOperation &operation) override
Applies domain operation.
Result< DomainOperation > makeDeleteLayer(const StableId &layer) const override
Make delete layer.
MapDocumentTarget(std::string id)
Map document target.
Diagnostic ruleDiagnostic(eve::DiagnosticCode code, RuleId rule, DiagnosticSeverity severity, std::string message)
Build a common diagnostic carrying an open editing rule identity.
DomainOperation public API.
std::vector< ObjectRefValue > affectedObjects
Stable discovery metadata exposed by every editable target.
std::vector< CapabilityId > capabilities
Stable map layer descriptor used by outliner presenters.
Definition MapDocument.h:15
Stable object placement stored on an object layer.
Definition MapDocument.h:45
Deterministic renderer-neutral road strip preview.
Definition MapDocument.h:61
std::vector< EditorDiagnostic > diagnostics
Definition MapDocument.h:67
Road spline authoring record independent from rendering/mesh generation.
Definition MapDocument.h:35
std::vector< MapSplinePointRecord > points
Definition MapDocument.h:41
Stable editable control point in a road or path spline.
Definition MapDocument.h:26