载入中...
搜索中...
未找到
BuildingTarget.cpp
浏览该文件的文档.
2
6
7#include <algorithm>
8#include <cmath>
9#include <set>
10#include <utility>
11
13namespace {
14
15EditorValue stringArray(const std::vector<std::string>& values) {
16 EditorValue::Array result;
17 for (const std::string& value : values) result.emplace_back(value);
18 return result;
19}
20
21EditorValue numberArray(const std::vector<double>& values) {
22 EditorValue::Array result;
23 for (double value : values) result.emplace_back(value);
24 return result;
25}
26
27std::vector<std::string> parseStringArray(const EditorValue& value, bool& valid) {
28 std::vector<std::string> result;
29 const auto* values = value.getIf<EditorValue::Array>();
30 if (!values) {
31 valid = false;
32 return result;
33 }
34 for (const EditorValue& item : *values) {
35 const auto* text = item.getIf<std::string>();
36 if (!text) {
37 valid = false;
38 return {};
39 }
40 result.push_back(*text);
41 }
42 return result;
43}
44
45EditorValue instanceValue(const BuildingInstanceSnapshot& instance) {
47 for (const auto& [key, value] : instance.properties) properties[key] = value;
48 EditorValue::Array garrison;
49 for (const BuildingGarrisonMemberRecord& member : instance.garrison)
50 garrison.emplace_back(EditorValue::Object{{"id", member.id}, {"type", member.type},
51 {"tags", stringArray(member.tags)}});
52 return EditorValue::Object{{"instanceId", int64_t{instance.instanceId}},
53 {"buildingId", instance.buildingId},
54 {"placementKind", instance.placementKind},
55 {"edgeX", int64_t{instance.edgeX}}, {"edgeY", int64_t{instance.edgeY}},
56 {"edgeAxis", instance.edgeAxis},
57 {"cornerX", int64_t{instance.cornerX}},
58 {"cornerY", int64_t{instance.cornerY}},
59 {"freeRadius", instance.freeRadius},
60 {"freeHalfWidth", instance.freeHalfWidth},
61 {"freeHalfHeight", instance.freeHalfHeight},
62 {"freeFootprintVertices", numberArray(instance.freeFootprintVertices)},
63 {"cellX", int64_t{instance.cellX}}, {"cellY", int64_t{instance.cellY}},
64 {"level", int64_t{instance.level}},
65 {"worldX", instance.worldX}, {"worldY", instance.worldY},
66 {"elevation", instance.elevation}, {"surfaceId", instance.surfaceId},
67 {"surfaceRevision", static_cast<int64_t>(instance.surfaceRevision)},
68 {"surfaceNormalX", instance.surfaceNormalX},
69 {"surfaceNormalY", instance.surfaceNormalY},
70 {"surfaceNormalZ", instance.surfaceNormalZ},
71 {"surfaceTangentX", instance.surfaceTangentX},
72 {"surfaceTangentY", instance.surfaceTangentY},
73 {"surfaceTangentZ", instance.surfaceTangentZ},
74 {"surfaceSampleCount", int64_t{instance.surfaceSampleCount}},
75 {"surfaceMaxSlopeDegrees", instance.surfaceMaxSlopeDegrees},
76 {"surfaceHeightDelta", instance.surfaceHeightDelta},
77 {"rotationDegrees", instance.rotationDegrees},
78 {"channel", instance.channel}, {"properties", std::move(properties)},
79 {"tags", stringArray(instance.tags)}, {"garrison", std::move(garrison)},
80 {"garrisonRevision", static_cast<int64_t>(instance.garrisonRevision)}};
81}
82
83const EditorValue* field(const EditorValue& value, const char* key) {
84 const auto* object = value.getIf<EditorValue::Object>();
85 if (!object) return nullptr;
86 const auto found = object->find(key);
87 return found == object->end() ? nullptr : &found->second;
88}
89
90Result<BuildingInstanceSnapshot> parseInstance(const EditorValue& value) {
91 const auto integer = [&](const char* key) -> const int64_t* {
92 const EditorValue* entry = field(value, key);
93 return entry ? entry->getIf<int64_t>() : nullptr;
94 };
95 const auto number = [&](const char* key) -> const double* {
96 const EditorValue* entry = field(value, key);
97 return entry ? entry->getIf<double>() : nullptr;
98 };
99 const auto string = [&](const char* key) -> const std::string* {
100 const EditorValue* entry = field(value, key);
101 return entry ? entry->getIf<std::string>() : nullptr;
102 };
103 const auto* instanceId = integer("instanceId");
104 const auto* buildingId = string("buildingId");
105 const auto* cellX = integer("cellX");
106 const auto* cellY = integer("cellY");
107 const auto* worldX = number("worldX");
108 const auto* worldY = number("worldY");
109 const auto* elevation = number("elevation");
110 const auto* rotation = number("rotationDegrees");
111 const auto* channel = string("channel");
112 const auto* propertiesValue = field(value, "properties");
113 const auto* tagsValue = field(value, "tags");
114 const auto* garrisonValue = field(value, "garrison");
115 const auto* garrisonRevision = integer("garrisonRevision");
116 const auto* properties = propertiesValue ? propertiesValue->getIf<EditorValue::Object>() : nullptr;
117 const auto* garrison = garrisonValue ? garrisonValue->getIf<EditorValue::Array>() : nullptr;
118 if (!instanceId || *instanceId <= 0 || !buildingId || buildingId->empty() || !cellX || !cellY || !worldX ||
119 !worldY || !elevation || !rotation || !channel || !properties || !tagsValue || !garrison ||
120 !garrisonRevision || *garrisonRevision < 0)
121 return eve::editing::failed<BuildingInstanceSnapshot>(EditorStatus::Rejected,
122 RuleId("editor.building.invalid-instance"),
123 "Building instance payload is incomplete");
125 result.instanceId = static_cast<int>(*instanceId);
126 result.buildingId = *buildingId;
127 if (const auto* placementKind = string("placementKind")) result.placementKind = *placementKind;
128 if (const auto* edgeX = integer("edgeX")) result.edgeX = static_cast<int>(*edgeX);
129 if (const auto* edgeY = integer("edgeY")) result.edgeY = static_cast<int>(*edgeY);
130 if (const auto* edgeAxis = string("edgeAxis")) result.edgeAxis = *edgeAxis;
131 if (const auto* cornerX = integer("cornerX")) result.cornerX = static_cast<int>(*cornerX);
132 if (const auto* cornerY = integer("cornerY")) result.cornerY = static_cast<int>(*cornerY);
133 if (const auto* freeRadius = number("freeRadius")) result.freeRadius = *freeRadius;
134 if (const auto* freeHalfWidth = number("freeHalfWidth"))
135 result.freeHalfWidth = *freeHalfWidth;
136 if (const auto* freeHalfHeight = number("freeHalfHeight"))
137 result.freeHalfHeight = *freeHalfHeight;
138 if (const EditorValue* polygon = field(value, "freeFootprintVertices")) {
139 const auto* vertices = polygon->getIf<EditorValue::Array>();
140 if (!vertices)
141 return eve::editing::failed<BuildingInstanceSnapshot>(EditorStatus::Rejected,
142 RuleId("editor.building.invalid-free-footprint"),
143 "Free footprint vertices must be a numeric array");
144 for (const EditorValue& vertex : *vertices) {
145 const auto* coordinate = vertex.getIf<double>();
146 if (!coordinate)
147 return eve::editing::failed<BuildingInstanceSnapshot>(
148 EditorStatus::Rejected, RuleId("editor.building.invalid-free-footprint"),
149 "Free footprint vertices must contain only numbers");
150 result.freeFootprintVertices.push_back(*coordinate);
151 }
152 }
153 result.cellX = static_cast<int>(*cellX);
154 result.cellY = static_cast<int>(*cellY);
155 if (const auto* level = integer("level")) result.level = static_cast<int>(*level);
156 result.worldX = *worldX;
157 result.worldY = *worldY;
158 result.elevation = *elevation;
159 if (const auto* surfaceId = string("surfaceId")) result.surfaceId = *surfaceId;
160 if (const auto* surfaceRevision = integer("surfaceRevision")) {
161 if (*surfaceRevision < 0)
162 return eve::editing::failed<BuildingInstanceSnapshot>(EditorStatus::Rejected,
163 RuleId("editor.building.invalid-surface-revision"),
164 "Surface revision must not be negative");
165 result.surfaceRevision = static_cast<std::uint64_t>(*surfaceRevision);
166 }
167 if (const auto* n = number("surfaceNormalX")) result.surfaceNormalX = *n;
168 if (const auto* n = number("surfaceNormalY")) result.surfaceNormalY = *n;
169 if (const auto* n = number("surfaceNormalZ")) result.surfaceNormalZ = *n;
170 if (const auto* n = number("surfaceTangentX")) result.surfaceTangentX = *n;
171 if (const auto* n = number("surfaceTangentY")) result.surfaceTangentY = *n;
172 if (const auto* n = number("surfaceTangentZ")) result.surfaceTangentZ = *n;
173 if (const auto* count = integer("surfaceSampleCount")) result.surfaceSampleCount = static_cast<int>(*count);
174 if (const auto* slope = number("surfaceMaxSlopeDegrees")) result.surfaceMaxSlopeDegrees = *slope;
175 if (const auto* delta = number("surfaceHeightDelta")) result.surfaceHeightDelta = *delta;
176 if (result.placementKind != "cell" && result.placementKind != "edge" &&
177 result.placementKind != "corner" && result.placementKind != "free")
178 return eve::editing::failed<BuildingInstanceSnapshot>(EditorStatus::Rejected,
179 RuleId("editor.building.invalid-placement-kind"),
180 "Placement kind must be cell, edge, corner, or free");
181 if (result.placementKind == "edge" && result.edgeAxis != "horizontal" &&
182 result.edgeAxis != "vertical")
183 return eve::editing::failed<BuildingInstanceSnapshot>(EditorStatus::Rejected,
184 RuleId("editor.building.invalid-edge-axis"),
185 "Edge axis must be horizontal or vertical");
186 result.rotationDegrees = *rotation;
187 result.channel = *channel;
188 result.garrisonRevision = static_cast<std::uint64_t>(*garrisonRevision);
189 for (const auto& [key, property] : *properties) {
190 const auto* text = property.getIf<std::string>();
191 if (!text)
192 return eve::editing::failed<BuildingInstanceSnapshot>(EditorStatus::Rejected,
193 RuleId("editor.building.invalid-property"),
194 "Building properties must be strings");
195 result.properties[key] = *text;
196 }
197 bool valid = true;
198 result.tags = parseStringArray(*tagsValue, valid);
199 if (!valid)
200 return eve::editing::failed<BuildingInstanceSnapshot>(
201 EditorStatus::Rejected, RuleId("editor.building.invalid-tags"), "Building tags must be strings");
202 std::set<std::string> memberIds;
203 for (const EditorValue& memberValue : *garrison) {
204 const auto* idValue = field(memberValue, "id");
205 const auto* typeValue = field(memberValue, "type");
206 const auto* memberTags = field(memberValue, "tags");
207 const auto* id = idValue ? idValue->getIf<std::string>() : nullptr;
208 const auto* type = typeValue ? typeValue->getIf<std::string>() : nullptr;
209 if (!id || id->empty() || !memberIds.insert(*id).second || !type || type->empty() || !memberTags)
210 return eve::editing::failed<BuildingInstanceSnapshot>(EditorStatus::Rejected,
211 RuleId("editor.building.invalid-garrison"),
212 "Garrison members require unique ids, type and tags");
213 auto tags = parseStringArray(*memberTags, valid);
214 if (!valid)
215 return eve::editing::failed<BuildingInstanceSnapshot>(EditorStatus::Rejected,
216 RuleId("editor.building.invalid-garrison-tags"),
217 "Garrison member tags must be strings");
218 result.garrison.push_back({*id, *type, std::move(tags)});
219 }
220 return eve::editing::applied<BuildingInstanceSnapshot>(std::move(result));
221}
222
223Result<std::vector<BuildingInstanceSnapshot>> parseInstances(const EditorValue& value) {
224 const auto* values = value.getIf<EditorValue::Array>();
225 if (!values || values->empty())
226 return eve::editing::failed<std::vector<BuildingInstanceSnapshot>>(
227 EditorStatus::Rejected, RuleId("editor.building.invalid-area-payload"),
228 "Building area payload must contain at least one instance");
229 std::vector<BuildingInstanceSnapshot> result;
230 std::set<int> ids;
231 result.reserve(values->size());
232 for (const EditorValue& item : *values) {
233 auto parsed = parseInstance(item);
234 if (!parsed.ok())
235 return eve::editing::failed<std::vector<BuildingInstanceSnapshot>>(
236 parsed.code(), RuleId("editor.building.invalid-area-instance"),
237 "Building area payload contains an invalid instance");
238 if (!ids.insert(parsed.value().instanceId).second)
239 return eve::editing::failed<std::vector<BuildingInstanceSnapshot>>(
240 EditorStatus::Rejected, RuleId("editor.building.duplicate-area-instance"),
241 "Building area payload contains duplicate instance ids");
242 result.push_back(std::move(parsed).takeValue());
243 }
244 return eve::editing::applied<std::vector<BuildingInstanceSnapshot>>(std::move(result));
245}
246
247struct EdgeCurvePayload {
248 building::EdgeCurveGroup group;
249 std::vector<BuildingInstanceSnapshot> instances;
250};
251
252EditorValue edgeCurveValue(const building::EdgeCurveGroup& group,
253 const std::vector<BuildingInstanceSnapshot>& instances) {
254 std::vector<double> controls;
255 for (const building::EdgeCurveControlPoint& point : group.controlPoints) {
256 controls.push_back(point.x);
257 controls.push_back(point.y);
258 }
259 EditorValue::Array values;
260 for (const BuildingInstanceSnapshot& instance : instances)
261 values.push_back(instanceValue(instance));
262 std::vector<double> surfaceSamples;
263 for (const building::EdgeCurveSurfaceSample& sample : group.surfaceSamples) {
264 surfaceSamples.insert(surfaceSamples.end(),
265 {sample.worldX, sample.worldY, sample.worldZ, sample.normalX,
266 sample.normalY, sample.normalZ});
267 }
268 return EditorValue::Object{{"groupId", static_cast<int64_t>(group.id.value)},
269 {"buildingId", group.buildingId},
270 {"level", int64_t{group.level}},
271 {"subdivisions", int64_t{group.subdivisions}},
272 {"controlPoints", numberArray(controls)},
273 {"surfaceProviderName", group.surfaceProviderName},
274 {"surfaceId", group.surfaceId},
275 {"surfaceRevision", static_cast<int64_t>(group.surfaceRevision)},
276 {"surfaceSamples", numberArray(surfaceSamples)},
277 {"instances", std::move(values)}};
278}
279
280Result<EdgeCurvePayload> parseEdgeCurve(const EditorValue& value) {
281 const EditorValue* groupIdValue = field(value, "groupId");
282 const EditorValue* buildingIdValue = field(value, "buildingId");
283 const EditorValue* levelValue = field(value, "level");
284 const EditorValue* subdivisionsValue = field(value, "subdivisions");
285 const EditorValue* controlsValue = field(value, "controlPoints");
286 const EditorValue* instancesValue = field(value, "instances");
287 const auto* groupId = groupIdValue ? groupIdValue->getIf<int64_t>() : nullptr;
288 const auto* buildingId =
289 buildingIdValue ? buildingIdValue->getIf<std::string>() : nullptr;
290 const auto* level = levelValue ? levelValue->getIf<int64_t>() : nullptr;
291 const auto* subdivisions =
292 subdivisionsValue ? subdivisionsValue->getIf<int64_t>() : nullptr;
293 const auto* controls = controlsValue ? controlsValue->getIf<EditorValue::Array>() : nullptr;
294 if (!groupId || *groupId <= 0 || !buildingId || buildingId->empty() || !level ||
295 !subdivisions || *subdivisions < 2 || *subdivisions > 4096 || !controls ||
296 controls->size() != 8 || !instancesValue)
297 return eve::editing::failed<EdgeCurvePayload>(
298 EditorStatus::Rejected, RuleId("editor.building.invalid-edge-curve"), "Edge curve payload is incomplete");
299 auto instances = parseInstances(*instancesValue);
300 if (!instances.ok())
301 return eve::editing::failed<EdgeCurvePayload>(instances.code(),
302 RuleId("editor.building.invalid-edge-curve-instances"),
303 "Edge curve instances are invalid");
304 EdgeCurvePayload result;
305 result.group.id = {static_cast<std::uint64_t>(*groupId)};
306 result.group.buildingId = *buildingId;
307 result.group.level = static_cast<int>(*level);
308 result.group.subdivisions = static_cast<int>(*subdivisions);
309 if (const EditorValue* provider = field(value, "surfaceProviderName"))
310 if (const auto* text = provider->getIf<std::string>())
311 result.group.surfaceProviderName = *text;
312 if (const EditorValue* surface = field(value, "surfaceId"))
313 if (const auto* text = surface->getIf<std::string>()) result.group.surfaceId = *text;
314 if (const EditorValue* revision = field(value, "surfaceRevision")) {
315 const auto* integer = revision->getIf<int64_t>();
316 if (!integer || *integer < 0)
317 return eve::editing::failed<EdgeCurvePayload>(EditorStatus::Rejected,
318 RuleId("editor.building.invalid-edge-curve-surface-revision"),
319 "Edge curve surface revision is invalid");
320 result.group.surfaceRevision = static_cast<std::uint64_t>(*integer);
321 }
322 if (const EditorValue* sampleValue = field(value, "surfaceSamples")) {
323 const auto* samples = sampleValue->getIf<EditorValue::Array>();
324 if (!samples || samples->size() % 6 != 0)
325 return eve::editing::failed<EdgeCurvePayload>(
326 EditorStatus::Rejected, RuleId("editor.building.invalid-edge-curve-surface-samples"),
327 "Edge curve surface samples must contain position and normal sextuples");
328 for (size_t index = 0; index < samples->size(); index += 6) {
329 std::array<float, 6> frame{};
330 for (size_t component = 0; component < 6; ++component) {
331 const auto* number = (*samples)[index + component].getIf<double>();
332 if (!number)
333 return eve::editing::failed<EdgeCurvePayload>(
334 EditorStatus::Rejected, RuleId("editor.building.invalid-edge-curve-surface-samples"),
335 "Edge curve surface samples must be numeric");
336 frame[component] = static_cast<float>(*number);
337 }
338 result.group.surfaceSamples.push_back(
339 {frame[0], frame[1], frame[2], frame[3], frame[4], frame[5]});
340 }
341 }
342 for (size_t index = 0; index < controls->size(); index += 2) {
343 const auto* x = (*controls)[index].getIf<double>();
344 const auto* y = (*controls)[index + 1].getIf<double>();
345 if (!x || !y)
346 return eve::editing::failed<EdgeCurvePayload>(EditorStatus::Rejected,
347 RuleId("editor.building.invalid-edge-curve-controls"),
348 "Edge curve controls must be numbers");
349 result.group.controlPoints.push_back(
350 {static_cast<float>(*x), static_cast<float>(*y)});
351 }
352 result.instances = std::move(instances).takeValue();
353 for (const BuildingInstanceSnapshot& instance : result.instances)
354 result.group.instanceIds.push_back(instance.instanceId);
355 return eve::editing::applied<EdgeCurvePayload>(std::move(result));
356}
357
358BuildingInstanceSnapshot snapshot(const building::PlacedBuilding& placed) {
360 result.instanceId = placed.instanceId;
361 result.buildingId = placed.buildingId;
362 result.placementKind = placed.placementKind;
363 result.edgeX = placed.edge.x;
364 result.edgeY = placed.edge.y;
365 result.edgeAxis = placed.edge.axis == building::EdgeAxis::Horizontal ? "horizontal" : "vertical";
366 result.cornerX = placed.corner.x;
367 result.cornerY = placed.corner.y;
368 result.freeRadius = placed.freeRadius;
369 result.freeHalfWidth = placed.freeHalfWidth;
370 result.freeHalfHeight = placed.freeHalfHeight;
371 for (float coordinate : placed.freeFootprintVertices)
372 result.freeFootprintVertices.push_back(coordinate);
373 result.cellX = placed.originCellX;
374 result.cellY = placed.originCellY;
375 result.level = placed.level;
376 result.worldX = placed.worldX;
377 result.worldY = placed.worldY;
378 result.elevation = placed.elevation;
379 result.surfaceId = placed.surfaceId;
380 result.surfaceRevision = placed.surfaceRevision;
381 result.surfaceNormalX = placed.surfaceNormalX;
382 result.surfaceNormalY = placed.surfaceNormalY;
383 result.surfaceNormalZ = placed.surfaceNormalZ;
384 result.surfaceTangentX = placed.surfaceTangentX;
385 result.surfaceTangentY = placed.surfaceTangentY;
386 result.surfaceTangentZ = placed.surfaceTangentZ;
387 result.surfaceSampleCount = placed.surfaceSampleCount;
388 result.surfaceMaxSlopeDegrees = placed.surfaceMaxSlopeDegrees;
389 result.surfaceHeightDelta = placed.surfaceHeightDelta;
390 result.rotationDegrees = placed.rotationDeg;
391 result.channel = placed.channel;
392 result.properties.insert(placed.props.begin(), placed.props.end());
393 result.tags = placed.tags;
394 for (const building::GarrisonMember& member : placed.garrison)
395 result.garrison.push_back({member.id, member.type, member.tags});
396 result.garrisonRevision = placed.garrisonRevision.value();
397 return result;
398}
399
400building::PlacedBuilding runtime(const BuildingInstanceSnapshot& value) {
401 building::PlacedBuilding result;
402 result.instanceId = value.instanceId;
403 result.buildingId = value.buildingId;
404 result.placementKind = value.placementKind;
405 result.edge = {value.edgeX, value.edgeY,
406 value.edgeAxis == "vertical" ? building::EdgeAxis::Vertical
407 : building::EdgeAxis::Horizontal};
408 result.corner = {value.cornerX, value.cornerY};
409 result.freeRadius = static_cast<float>(value.freeRadius);
410 result.freeHalfWidth = static_cast<float>(value.freeHalfWidth);
411 result.freeHalfHeight = static_cast<float>(value.freeHalfHeight);
412 for (double coordinate : value.freeFootprintVertices)
413 result.freeFootprintVertices.push_back(static_cast<float>(coordinate));
414 result.originCellX = value.cellX;
415 result.originCellY = value.cellY;
416 result.level = value.level;
417 result.worldX = static_cast<float>(value.worldX);
418 result.worldY = static_cast<float>(value.worldY);
419 result.elevation = static_cast<float>(value.elevation);
420 result.surfaceId = value.surfaceId;
421 result.surfaceRevision = value.surfaceRevision;
422 result.surfaceNormalX = static_cast<float>(value.surfaceNormalX);
423 result.surfaceNormalY = static_cast<float>(value.surfaceNormalY);
424 result.surfaceNormalZ = static_cast<float>(value.surfaceNormalZ);
425 result.surfaceTangentX = static_cast<float>(value.surfaceTangentX);
426 result.surfaceTangentY = static_cast<float>(value.surfaceTangentY);
427 result.surfaceTangentZ = static_cast<float>(value.surfaceTangentZ);
428 result.surfaceSampleCount = value.surfaceSampleCount;
429 result.surfaceMaxSlopeDegrees = static_cast<float>(value.surfaceMaxSlopeDegrees);
430 result.surfaceHeightDelta = static_cast<float>(value.surfaceHeightDelta);
431 result.rotationDeg = static_cast<float>(value.rotationDegrees);
432 result.channel = value.channel;
433 result.props.insert(value.properties.begin(), value.properties.end());
434 result.tags = value.tags;
435 for (const BuildingGarrisonMemberRecord& member : value.garrison)
436 result.garrison.push_back({member.id, member.type, member.tags});
437 result.garrisonRevision = eve::Revision(value.garrisonRevision);
438 return result;
439}
440
441DomainOperation instanceOperation(const std::string& target, std::string type, std::string inverseType,
442 const BuildingInstanceSnapshot& payload,
443 const BuildingInstanceSnapshot& inverse) {
444 DomainOperation result;
445 result.type = std::move(type);
446 result.inverseType = std::move(inverseType);
447 result.target = TargetId(target);
448 result.payload = instanceValue(payload);
449 result.inverse = instanceValue(inverse);
450 result.hasInverse = true;
451 result.affectedObjects.push_back({TargetId(target), std::to_string(payload.instanceId), 0});
452 return result;
453}
454
455Result<DomainOperation> areaOperation(
456 const std::string& target, building::PlacementWorld* world,
457 const building::PlacementSystem::AreaPreview& preview) {
458 if (!world || preview.cells.empty() || preview.rejectedCount != 0 ||
459 preview.acceptedCount != static_cast<int>(preview.cells.size()))
460 return eve::editing::failed<DomainOperation>(EditorStatus::Rejected, RuleId("editor.building.area-rejected"),
461 "Area placement requires a fully accepted preview");
462 const building::BuildingDefinition* definition =
463 building::BuildingRegistry::find(preview.buildingId);
464 if (!definition || definition->placementKind == "edge")
465 return eve::editing::failed<DomainOperation>(EditorStatus::Rejected,
466 RuleId("editor.building.area-definition-invalid"),
467 "Area placement requires a cell building definition");
469 operation.type = "building.area.set.v1";
470 operation.inverseType = "building.area.delete.v1";
471 operation.target = TargetId(target);
472 EditorValue::Array instances;
473 int nextId = 1;
474 for (const building::PlacementSystem::AreaCellPreview& cell : preview.cells) {
475 while (world->hasBuilding(nextId)) ++nextId;
477 placed.instanceId = nextId++;
478 placed.buildingId = preview.buildingId;
479 placed.cellX = cell.cellX;
480 placed.cellY = cell.cellY;
481 placed.rotationDegrees = preview.rotationDeg;
482 placed.channel = definition->channel;
483 placed.tags = definition->tags;
484 float worldX = 0.f;
485 float worldY = 0.f;
486 world->cellToWorldPlane(cell.cellX, cell.cellY, worldX, worldY);
487 placed.worldX = worldX;
488 placed.worldY = worldY;
489 instances.push_back(instanceValue(placed));
490 operation.affectedObjects.push_back(
491 {TargetId(target), std::to_string(placed.instanceId), 0});
492 }
493 operation.payload = instances;
494 operation.inverse = std::move(instances);
495 operation.hasInverse = true;
496 return eve::editing::applied<DomainOperation>(std::move(operation));
497}
498
499} // namespace
500
501BuildingPlacementTarget::BuildingPlacementTarget(std::string id, building::PlacementWorld* world)
502 : id_(std::move(id)), world_(world) {}
503
504BuildingPlacementTarget::BuildingPlacementTarget(std::string id, std::unique_ptr<building::PlacementWorld> world,
505 unsigned long long revision)
506 : EditableTargetState(revision), id_(std::move(id)), world_(world.get()), ownedWorld_(std::move(world)) {}
507
508// Defined here (PlacementWorld.h is included above) so every other translation unit
509// gets a declaration to call instead of instantiating the deleter itself; see the
510// special-member comment in BuildingTarget.h.
511BuildingPlacementTarget::BuildingPlacementTarget(BuildingPlacementTarget&&) noexcept = default;
512BuildingPlacementTarget& BuildingPlacementTarget::operator=(BuildingPlacementTarget&&) noexcept = default;
513BuildingPlacementTarget::~BuildingPlacementTarget() = default;
514
516 TargetDescriptor result;
517 result.id = TargetId(id_);
518 result.type = "building-placement-world";
519 result.revision = revisionValue();
520 result.readOnly = world_ == nullptr;
521 result.capabilities = {editorCapabilityId()};
522 return result;
523}
524
526 return capability == editorCapabilityId() ? this : nullptr;
527}
528
530 if (!world_)
531 return eve::editing::failed<BuildingInstanceSnapshot>(
532 EditorStatus::Rejected, RuleId("editor.building.world-required"), "Placement world is unavailable");
533 const auto found = world_->buildings().find(instanceId);
534 if (found == world_->buildings().end())
535 return eve::editing::failed<BuildingInstanceSnapshot>(EditorStatus::NotFound,
536 RuleId("editor.building.instance-not-found"),
537 "Placed building instance was not found");
538 return eve::editing::applied<BuildingInstanceSnapshot>(snapshot(found->second));
539}
540
542 if (!world_) return 0;
543 int result = 1;
544 while (world_->hasBuilding(result)) ++result;
545 return result;
546}
547
548BuildingPlacementPreview BuildingPlacementTarget::preview(const std::string& buildingId, double worldX,
549 double worldY, double elevation,
550 double rotationDegrees,
551 int excludeInstanceId) const {
553 result.worldRevision = revisionValue();
554 if (!world_) {
555 result.status = EditorStatus::Rejected;
557 RuleId("editor.building.world-required"), DiagnosticSeverity::Error,
558 "Placement world is unavailable"));
559 return result;
560 }
563 if (!definition) {
564 result.status = EditorStatus::NotFound;
566 RuleId("editor.building.unknown-building"), DiagnosticSeverity::Error,
567 "Building definition was not found: " + buildingId));
568 return result;
569 }
570 const building::SnapResult snapped = building::PlacementSystem::snap(*world_, buildingId,
571 static_cast<float>(worldX),
572 static_cast<float>(worldY));
573 result.snappedCellX = snapped.cellX;
574 result.snappedCellY = snapped.cellY;
575 result.snappedWorldX = snapped.worldX;
576 result.snappedWorldY = snapped.worldY;
577 result.elevation = elevation;
579 buildingId, static_cast<float>(rotationDegrees));
581 *definition, result.snappedCellX, result.snappedCellY,
582 static_cast<float>(result.normalizedRotation), [&](int x, int y) {
583 BuildingFootprintCell cell;
584 cell.x = x;
585 cell.y = y;
586 cell.inBounds = world_->inBounds(x, y);
587 if (cell.inBounds) {
588 cell.occupant = world_->getOccupantInChannel(definition->channel, x, y);
589 cell.terrain = world_->getTerrain(x, y);
590 }
591 result.cells.push_back(cell);
592 return true;
593 });
594 std::string reason;
596 world_, buildingId, result.snappedCellX, result.snappedCellY, static_cast<float>(elevation),
597 static_cast<float>(result.normalizedRotation), excludeInstanceId, &reason);
598 result.status = valid ? EditorStatus::Applied : EditorStatus::Rejected;
599 if (!valid)
601 RuleId("editor.building." + (reason.empty() ? "rejected" : reason)), DiagnosticSeverity::Error,
602 "Building footprint validation rejected placement: " + reason));
603 return result;
604}
605
607 const BuildingInstanceSnapshot& placed) const {
608 if (!world_ || placed.instanceId <= 0 || world_->hasBuilding(placed.instanceId))
609 return eve::editing::failed<DomainOperation>(EditorStatus::Conflict,
610 RuleId("editor.building.instance-id-conflict"),
611 "Exact placement instance id is unavailable");
612 std::string reason;
613 const bool valid = placed.placementKind == "edge"
615 world_, placed.buildingId, placed.edgeX, placed.edgeY,
616 placed.edgeAxis == "vertical" ? "west" : "north", 0, &reason)
617 : placed.placementKind == "corner"
619 world_, placed.buildingId, placed.cornerX, placed.cornerY, 0,
620 &reason)
621 : placed.placementKind == "free"
623 world_, placed.buildingId, static_cast<float>(placed.worldX),
624 static_cast<float>(placed.worldY), 0, &reason, placed.level)
626 world_, placed.buildingId, placed.cellX, placed.cellY,
627 static_cast<float>(placed.elevation),
628 static_cast<float>(placed.rotationDegrees), 0, &reason);
629 if (!valid)
630 return eve::editing::failed<DomainOperation>(
631 EditorStatus::Rejected, RuleId("editor.building." + (reason.empty() ? "rejected" : reason)),
632 "Building footprint validation rejected placement: " + reason);
633 return eve::editing::applied<DomainOperation>(instanceOperation(
634 id_, "building.instance.set.v4", "building.instance.delete.v4", placed, placed));
635}
636
638 double rotationDegrees) const {
639 auto current = instance(instanceId);
640 if (!current.ok())
641 return eve::editing::failed<DomainOperation>(current.code(), RuleId("editor.building.instance-not-found"),
642 "Placed building instance was not found");
643 BuildingInstanceSnapshot desired = current.value();
644 if (desired.placementKind == "free")
645 return eve::editing::failed<DomainOperation>(EditorStatus::Rejected,
646 RuleId("editor.building.free-move-requires-world-anchor"),
647 "Free objects must be moved with makeMoveFree");
648 desired.cellX = cellX;
649 desired.cellY = cellY;
650 if (desired.placementKind == "corner") {
651 desired.cornerX = cellX;
652 desired.cornerY = cellY;
653 }
655 desired.buildingId, static_cast<float>(rotationDegrees));
656 float worldX = 0.f;
657 float worldY = 0.f;
658 world_->cellToWorldPlane(cellX, cellY, worldX, worldY);
659 desired.worldX = worldX;
660 desired.worldY = worldY;
661 std::string reason;
662 const bool valid = desired.placementKind == "corner"
664 world_, desired.buildingId, desired.cornerX, desired.cornerY,
665 instanceId, &reason)
666 : desired.placementKind == "free"
668 world_, desired.buildingId, static_cast<float>(desired.worldX),
669 static_cast<float>(desired.worldY), instanceId, &reason,
670 desired.level)
672 world_, desired.buildingId, cellX, cellY,
673 static_cast<float>(desired.elevation),
674 static_cast<float>(desired.rotationDegrees), instanceId,
675 &reason);
676 if (!valid)
677 return eve::editing::failed<DomainOperation>(
678 EditorStatus::Rejected, RuleId("editor.building." + (reason.empty() ? "rejected" : reason)),
679 "Building footprint validation rejected move: " + reason);
680 return eve::editing::applied<DomainOperation>(instanceOperation(
681 id_, "building.instance.set.v4", "building.instance.set.v4", desired, current.value()));
682}
683
685 int instanceId, double worldX, double worldY, double elevation,
686 double rotationDegrees) const {
687 auto current = instance(instanceId);
688 if (!current.ok())
689 return eve::editing::failed<DomainOperation>(current.code(), RuleId("editor.building.instance-not-found"),
690 "Placed building instance was not found");
691 if (current.value().placementKind != "free")
692 return eve::editing::failed<DomainOperation>(EditorStatus::Rejected,
693 RuleId("editor.building.not-free-instance"),
694 "makeMoveFree requires a free-domain instance");
695 BuildingInstanceSnapshot desired = current.value();
696 desired.worldX = worldX;
697 desired.worldY = worldY;
698 desired.elevation = elevation;
700 desired.buildingId, static_cast<float>(rotationDegrees));
701 std::string reason;
703 world_, desired.buildingId, static_cast<float>(worldX), static_cast<float>(worldY),
704 instanceId, &reason, desired.level))
705 return eve::editing::failed<DomainOperation>(
706 EditorStatus::Rejected, RuleId("editor.building." + (reason.empty() ? "rejected" : reason)),
707 "Free building validation rejected move: " + reason);
708 return eve::editing::applied<DomainOperation>(instanceOperation(
709 id_, "building.instance.set.v4", "building.instance.set.v4", desired,
710 current.value()));
711}
712
714 int instanceId, const std::string& replacementBuildingId) const {
715 auto current = instance(instanceId);
716 if (!current.ok())
717 return eve::editing::failed<DomainOperation>(current.code(), RuleId("editor.building.instance-not-found"),
718 "Placed building instance was not found");
719 const building::BuildingDefinition* replacement =
720 building::BuildingRegistry::find(replacementBuildingId);
721 if (!replacement)
722 return eve::editing::failed<DomainOperation>(EditorStatus::NotFound,
723 RuleId("editor.building.replacement-not-found"),
724 "Replacement building definition was not found");
725 const auto source = world_->buildings().find(instanceId);
726 if (source == world_->buildings().end())
727 return eve::editing::failed<DomainOperation>(EditorStatus::NotFound,
728 RuleId("editor.building.instance-not-found"),
729 "Placed building instance was not found");
730 const std::string& sourceKind = source->second.placementKind;
731 if (sourceKind != replacement->placementKind)
732 return eve::editing::failed<DomainOperation>(EditorStatus::Rejected,
733 RuleId("editor.building.replace-domain-mismatch"),
734 "Replacement must use the same placement domain");
735 std::string reason;
736 const bool valid = sourceKind == "edge"
738 world_, replacementBuildingId, current.value().edgeX,
739 current.value().edgeY,
740 current.value().edgeAxis == "vertical" ? "west" : "north",
741 instanceId, &reason)
742 : sourceKind == "corner"
744 world_, replacementBuildingId, current.value().cornerX,
745 current.value().cornerY, instanceId, &reason)
746 : sourceKind == "free"
748 world_, replacementBuildingId,
749 static_cast<float>(current.value().worldX),
750 static_cast<float>(current.value().worldY), instanceId, &reason,
751 current.value().level)
753 world_, replacementBuildingId, current.value().cellX,
754 current.value().cellY,
755 static_cast<float>(current.value().elevation),
756 static_cast<float>(current.value().rotationDegrees), instanceId,
757 &reason);
758 if (!valid)
759 return eve::editing::failed<DomainOperation>(
760 EditorStatus::Rejected, RuleId("editor.building." + (reason.empty() ? "rejected" : reason)),
761 "Building replacement validation failed: " + reason);
762 BuildingInstanceSnapshot desired = current.value();
763 desired.buildingId = replacementBuildingId;
764 desired.placementKind = replacement->placementKind;
765 desired.channel = replacement->channel;
766 desired.tags = replacement->tags;
767 if (sourceKind == "cell")
768 desired.rotationDegrees = building::PlacementSystem::normalizeRotation(
769 replacementBuildingId, static_cast<float>(desired.rotationDegrees));
770 return eve::editing::applied<DomainOperation>(instanceOperation(
771 id_, "building.instance.set.v4", "building.instance.set.v4", desired, current.value()));
772}
773
775 const std::string& buildingId, int minCellX, int minCellY, int maxCellX, int maxCellY,
776 double rotationDegrees) const {
778 world_, buildingId, minCellX, minCellY, maxCellX, maxCellY,
779 static_cast<float>(rotationDegrees));
780 if (!preview.ok())
781 return eve::editing::failed<DomainOperation>(EditorStatus::Rejected,
782 RuleId("editor.building.area-preview-failed"),
783 "Rectangle placement preview failed");
784 return areaOperation(id_, world_, preview.value());
785}
786
788 const std::string& buildingId, int centerCellX, int centerCellY, int radius,
789 double rotationDegrees) const {
791 world_, buildingId, centerCellX, centerCellY, radius,
792 static_cast<float>(rotationDegrees));
793 if (!preview.ok())
794 return eve::editing::failed<DomainOperation>(
795 EditorStatus::Rejected, RuleId("editor.building.area-preview-failed"), "Brush placement preview failed");
796 return areaOperation(id_, world_, preview.value());
797}
798
800 const std::string& buildingId,
801 const std::vector<BuildingEdgePathVertex>& vertices) const {
802 std::vector<building::CornerAddress> runtimeVertices;
803 runtimeVertices.reserve(vertices.size());
804 for (const BuildingEdgePathVertex& vertex : vertices)
805 runtimeVertices.push_back({vertex.x, vertex.y});
806 auto preview = building::PlacementSystem::previewEdgePath(world_, buildingId,
807 runtimeVertices);
808 if (!preview.ok())
809 return eve::editing::failed<DomainOperation>(
810 EditorStatus::Rejected, RuleId("editor.building.edge-path-rejected"), "Edge path preview was rejected");
811 const building::BuildingDefinition* definition =
813 if (!definition)
814 return eve::editing::failed<DomainOperation>(EditorStatus::NotFound,
815 RuleId("editor.building.definition-not-found"),
816 "Edge path definition was not found");
818 operation.type = "building.area.set.v1";
819 operation.inverseType = "building.area.delete.v1";
820 operation.target = TargetId(id_);
822 int nextId = 1;
823 for (const building::EdgeAddress& edge : preview.value().edges) {
824 while (world_->hasBuilding(nextId)) ++nextId;
826 placed.instanceId = nextId++;
827 placed.buildingId = buildingId;
828 placed.placementKind = "edge";
829 placed.edgeX = edge.x;
830 placed.edgeY = edge.y;
831 placed.edgeAxis = edge.axis == building::EdgeAxis::Horizontal ? "horizontal" : "vertical";
832 placed.cellX = edge.x;
833 placed.cellY = edge.y;
834 placed.level = preview.value().level;
835 placed.rotationDegrees = edge.axis == building::EdgeAxis::Horizontal ? 0.0 : 90.0;
836 placed.channel = definition->channel;
837 placed.tags = definition->tags;
838 float ax = 0.f, ay = 0.f, bx = 0.f, by = 0.f;
839 world_->cellToWorldPlane(edge.x, edge.y, ax, ay);
840 world_->cellToWorldPlane(edge.x + (edge.axis == building::EdgeAxis::Horizontal ? 1 : 0),
841 edge.y + (edge.axis == building::EdgeAxis::Vertical ? 1 : 0),
842 bx, by);
843 placed.worldX = (ax + bx) * 0.5;
844 placed.worldY = (ay + by) * 0.5;
845 instances.push_back(instanceValue(placed));
846 operation.affectedObjects.push_back(
847 {TargetId(id_), std::to_string(placed.instanceId), 0});
848 }
849 operation.payload = instances;
850 operation.inverse = std::move(instances);
851 operation.hasInverse = true;
852 return eve::editing::applied<DomainOperation>(std::move(operation));
853}
854
856 const std::string& buildingId,
857 const std::vector<BuildingEdgeCurvePoint>& controlPoints,
858 int subdivisions) const {
859 std::vector<building::PlacementSystem::EdgeCurvePoint> runtimeControls;
860 runtimeControls.reserve(controlPoints.size());
861 for (const BuildingEdgeCurvePoint& point : controlPoints)
862 runtimeControls.push_back(
863 {static_cast<float>(point.x), static_cast<float>(point.y)});
864 auto vertices =
865 building::PlacementSystem::sampleEdgeCubicBezier(runtimeControls, subdivisions);
866 if (!vertices.ok())
867 return eve::editing::failed<DomainOperation>(EditorStatus::Rejected,
868 RuleId("editor.building.edge-curve-rejected"),
869 "Cubic edge curve sampling was rejected");
870 std::vector<BuildingEdgePathVertex> editorVertices;
871 editorVertices.reserve(vertices.value().size());
872 for (const building::CornerAddress& vertex : vertices.value())
873 editorVertices.push_back({vertex.x, vertex.y});
874 auto path = makeEdgePath(buildingId, editorVertices);
875 if (!path.ok()) return path;
876 auto instances = parseInstances(path.value().payload);
877 if (!instances.ok())
878 return eve::editing::failed<DomainOperation>(EditorStatus::Failed,
879 RuleId("editor.building.edge-curve-encoding-failed"),
880 "Edge curve instances could not be encoded");
882 group.id = world_->nextEdgeCurveGroupId();
883 group.buildingId = buildingId;
884 group.level = instances.value().front().level;
885 group.subdivisions = subdivisions;
886 for (const BuildingEdgeCurvePoint& point : controlPoints)
887 group.controlPoints.push_back(
888 {static_cast<float>(point.x), static_cast<float>(point.y)});
889 for (const BuildingInstanceSnapshot& instance : instances.value())
890 group.instanceIds.push_back(instance.instanceId);
891 DomainOperation operation = std::move(path).takeValue();
892 operation.type = "building.edge-curve.set.v1";
893 operation.inverseType = "building.edge-curve.delete.v1";
894 operation.payload = edgeCurveValue(group, instances.value());
895 operation.inverse = operation.payload;
896 return eve::editing::applied<DomainOperation>(std::move(operation));
897}
898
900 const std::string& buildingId,
901 const std::vector<BuildingEdgeCurvePoint>& controlPoints, int subdivisions,
902 const std::string& surfaceName) const {
903 if (!world_)
904 return eve::editing::failed<DomainOperation>(EditorStatus::Rejected, RuleId("editor.building.world-required"),
905 "Placement world is unavailable");
906 auto operation = makeEdgeCubicBezier(buildingId, controlPoints, subdivisions);
907 if (!operation.ok()) return operation;
908 std::vector<building::PlacementSystem::EdgeCurvePoint> controls;
909 controls.reserve(controlPoints.size());
910 for (const BuildingEdgeCurvePoint& point : controlPoints)
911 controls.push_back({static_cast<float>(point.x), static_cast<float>(point.y)});
913 *world_, surfaceName, controls, subdivisions);
914 if (!surface.ok())
915 return eve::editing::failed<DomainOperation>(
916 EditorStatus::Rejected, RuleId("editor.building.edge-curve-surface-rejected"),
917 surface.error() ? surface.error()->message() : "Curve could not be sampled on the requested surface");
918 auto parsed = parseEdgeCurve(operation.value().payload);
919 if (!parsed.ok())
920 return eve::editing::failed<DomainOperation>(EditorStatus::Failed,
921 RuleId("editor.building.edge-curve-encoding-failed"),
922 "Edge curve instances could not be encoded");
923 parsed.value().group.surfaceProviderName = surface.value().providerName;
924 parsed.value().group.surfaceId = surface.value().surfaceId;
925 parsed.value().group.surfaceRevision = surface.value().surfaceRevision;
926 parsed.value().group.surfaceSamples = surface.value().samples;
927 DomainOperation enriched = std::move(operation).takeValue();
928 enriched.payload = edgeCurveValue(parsed.value().group, parsed.value().instances);
929 enriched.inverse = enriched.payload;
930 return eve::editing::applied<DomainOperation>(std::move(enriched));
931}
932
934 const std::string& buildingId,
935 const std::vector<BuildingEdgeCurvePoint>& controlPoints, int subdivisions,
936 int replacingMemberInstanceId, const std::string& surfaceName) const {
938 result.worldRevision = revisionValue();
939 result.controlPoints = controlPoints;
940 if (!world_) {
941 result.status = EditorStatus::Rejected;
944 RuleId("editor.building.world-required"), DiagnosticSeverity::Error,
945 "Placement world is unavailable"));
946 return result;
947 }
948 std::vector<building::PlacementSystem::EdgeCurvePoint> controls;
949 for (const BuildingEdgeCurvePoint& point : controlPoints)
950 controls.push_back({static_cast<float>(point.x), static_cast<float>(point.y)});
951 auto sampled = building::PlacementSystem::sampleEdgeCubicBezier(controls, subdivisions);
952 if (!sampled.ok()) {
953 result.status = EditorStatus::Rejected;
954 result.diagnostics.push_back(eve::editing::ruleDiagnostic(
956 RuleId("editor.building.edge-curve-invalid"), DiagnosticSeverity::Error,
957 "Cubic edge curve controls or subdivisions are invalid"));
958 return result;
959 }
960 for (const building::CornerAddress& vertex : sampled.value())
961 result.sampledVertices.push_back({vertex.x, vertex.y});
962 std::unique_ptr<building::PlacementWorld> previewOwner;
963 building::PlacementWorld* previewWorld = world_;
964 if (replacingMemberInstanceId > 0) {
965 auto replacingGroup = world_->edgeCurveGroupForInstance(replacingMemberInstanceId);
966 if (!replacingGroup.ok()) {
967 result.status = EditorStatus::NotFound;
968 result.diagnostics.push_back(eve::editing::ruleDiagnostic(
970 RuleId("editor.building.edge-curve-group-not-found"),
971 DiagnosticSeverity::Error,
972 "Replacement preview instance does not belong to a curve group"));
973 return result;
974 }
975 previewOwner = world_->cloneState();
976 for (int instanceId : replacingGroup.value().instanceIds)
977 building::PlacementSystem::removeBuilding(previewOwner.get(), instanceId);
978 previewWorld = previewOwner.get();
979 }
981 previewWorld, buildingId, controls, subdivisions);
982 if (!preview.ok()) {
983 result.status = EditorStatus::Rejected;
984 result.diagnostics.push_back(eve::editing::ruleDiagnostic(
985 preview.error() ? preview.error()->code()
987 RuleId("editor.building.edge-curve-rejected"), DiagnosticSeverity::Error,
988 preview.error() ? preview.error()->message()
989 : "Cubic edge curve preview was rejected"));
990 return result;
991 }
992 for (const building::EdgeAddress& edge : preview.value().edges)
993 result.edges.push_back(
994 {edge.x, edge.y,
995 edge.axis == building::EdgeAxis::Horizontal ? "horizontal" : "vertical"});
996 if (!surfaceName.empty()) {
998 *world_, surfaceName, controls, subdivisions);
999 if (!surface.ok()) {
1000 result.status = EditorStatus::Rejected;
1001 result.diagnostics.push_back(eve::editing::ruleDiagnostic(
1002 surface.error() ? surface.error()->code()
1004 RuleId("editor.building.edge-curve-surface-rejected"),
1006 surface.error() ? surface.error()->message()
1007 : "Curve surface preview was rejected"));
1008 return result;
1009 }
1010 result.surfaceProviderName = surface.value().providerName;
1011 result.surfaceId = surface.value().surfaceId;
1012 result.surfaceRevision = surface.value().surfaceRevision;
1013 for (const building::EdgeCurveSurfaceSample& sample : surface.value().samples)
1014 result.surfaceSamples.push_back(
1015 {sample.worldX, sample.worldY, sample.worldZ, sample.normalX,
1016 sample.normalY, sample.normalZ});
1017 }
1018 result.status = EditorStatus::Applied;
1019 return result;
1020}
1021
1023 const std::string& buildingId,
1024 const std::vector<BuildingEdgeCurvePoint>& controlPoints, int subdivisions,
1025 int level, int replacingMemberInstanceId, const std::string& surfaceName) const {
1027 gizmo.target = id_;
1028 gizmo.targetRevision = revisionValue();
1030 previewEdgeCubicBezier(buildingId, controlPoints, subdivisions,
1031 replacingMemberInstanceId, surfaceName);
1032 gizmo.status = preview.status;
1034 if (!world_) return gizmo;
1035 const auto project = [&](double gx, double gy) {
1036 float ox = 0.f, oy = 0.f, xx = 0.f, xy = 0.f, yx = 0.f, yy = 0.f;
1037 world_->cellToWorldPlane(0, 0, ox, oy);
1038 world_->cellToWorldPlane(1, 0, xx, xy);
1039 world_->cellToWorldPlane(0, 1, yx, yy);
1040 const double px = ox + gx * (xx - ox) + gy * (yx - ox);
1041 const double py = oy + gx * (xy - oy) + gy * (yy - oy);
1042 const double elevation = static_cast<double>(level) * world_->getFloorHeight();
1043 if (!surfaceName.empty()) {
1045 *world_, surfaceName, static_cast<float>(px), static_cast<float>(py));
1046 if (hit.ok())
1047 return std::array<double, 3>{hit.value().worldX, hit.value().worldY,
1048 hit.value().worldZ};
1049 }
1050 return world_->getGridPlaneName() == "xz"
1051 ? std::array<double, 3>{px, elevation, py}
1052 : std::array<double, 3>{px, py, elevation};
1053 };
1054 const auto addLine = [&](const std::string& id, const std::array<double, 3>& a,
1055 const std::array<double, 3>& b,
1056 const std::array<double, 4>& color, bool dashed) {
1058 line.id = id;
1059 line.kind = "line";
1060 for (size_t axis = 0; axis < 3; ++axis) {
1061 line.position[axis] = (a[axis] + b[axis]) * 0.5;
1062 line.direction[axis] = b[axis] - a[axis];
1063 }
1064 line.length = std::sqrt(line.direction[0] * line.direction[0] +
1065 line.direction[1] * line.direction[1] +
1066 line.direction[2] * line.direction[2]);
1067 if (line.length > 0.0)
1068 for (double& component : line.direction) component /= line.length;
1069 line.color = color;
1070 line.dashed = dashed;
1071 gizmo.primitives.push_back(std::move(line));
1072 };
1073 const std::array<double, 4> accepted{0.2, 0.95, 0.35, 1.0};
1074 const std::array<double, 4> rejected{0.95, 0.2, 0.2, 1.0};
1075 const auto& pathColor = preview.status == EditorStatus::Applied ? accepted : rejected;
1076 for (size_t index = 0; index < controlPoints.size(); ++index) {
1078 handle.id = "curve.control." + std::to_string(index);
1079 handle.kind = "sphere";
1080 handle.position = project(controlPoints[index].x, controlPoints[index].y);
1081 handle.radius = world_->getCellSize() * 0.15;
1082 handle.color = index == 0 || index + 1 == controlPoints.size()
1083 ? std::array<double, 4>{0.2, 0.65, 1.0, 1.0}
1084 : std::array<double, 4>{1.0, 0.7, 0.15, 1.0};
1085 gizmo.primitives.push_back(std::move(handle));
1086 if (index > 0)
1087 addLine("curve.tangent." + std::to_string(index - 1),
1088 project(controlPoints[index - 1].x, controlPoints[index - 1].y),
1089 project(controlPoints[index].x, controlPoints[index].y),
1090 {0.65, 0.65, 0.7, 0.8}, true);
1091 }
1092 if (!preview.surfaceSamples.empty()) {
1093 for (size_t index = 1; index < preview.surfaceSamples.size(); ++index) {
1094 const auto& a = preview.surfaceSamples[index - 1];
1095 const auto& b = preview.surfaceSamples[index];
1096 addLine("curve.path." + std::to_string(index - 1),
1097 {a.worldX, a.worldY, a.worldZ}, {b.worldX, b.worldY, b.worldZ},
1098 pathColor, false);
1099 }
1100 } else {
1101 for (size_t index = 1; index < preview.sampledVertices.size(); ++index)
1102 addLine("curve.path." + std::to_string(index - 1),
1103 project(preview.sampledVertices[index - 1].x,
1104 preview.sampledVertices[index - 1].y),
1105 project(preview.sampledVertices[index].x,
1106 preview.sampledVertices[index].y),
1107 pathColor, false);
1108 }
1109 return gizmo;
1110}
1111
1113 int memberInstanceId, const std::vector<BuildingEdgeCurvePoint>& controlPoints,
1114 int subdivisions) const {
1115 if (!world_)
1116 return eve::editing::failed<DomainOperation>(EditorStatus::Rejected, RuleId("editor.building.world-required"),
1117 "Placement world is unavailable");
1118 auto currentGroup = world_->edgeCurveGroupForInstance(memberInstanceId);
1119 if (!currentGroup.ok())
1120 return eve::editing::failed<DomainOperation>(EditorStatus::NotFound,
1121 RuleId("editor.building.edge-curve-group-not-found"),
1122 "Instance does not belong to a curve group");
1123 std::unique_ptr<building::PlacementWorld> draftWorld = world_->cloneState();
1124 for (int instanceId : currentGroup.value().instanceIds)
1125 if (!building::PlacementSystem::removeBuilding(draftWorld.get(), instanceId))
1126 return eve::editing::failed<DomainOperation>(EditorStatus::Failed,
1127 RuleId("editor.building.edge-curve-draft-delete-failed"),
1128 "Curve update could not create an isolated draft world");
1129 BuildingPlacementTarget draftTarget(id_, std::move(draftWorld), revisionValue());
1130 auto desiredOperation = draftTarget.makeEdgeCubicBezier(
1131 currentGroup.value().buildingId, controlPoints, subdivisions);
1132 if (!desiredOperation.ok()) return desiredOperation;
1133 auto desired = parseEdgeCurve(desiredOperation.value().payload);
1134 if (!desired.ok())
1135 return eve::editing::failed<DomainOperation>(EditorStatus::Failed,
1136 RuleId("editor.building.edge-curve-encoding-failed"),
1137 "Updated edge curve could not be encoded");
1138 desired.value().group.id = currentGroup.value().id;
1139 if (!currentGroup.value().surfaceProviderName.empty()) {
1140 std::vector<building::PlacementSystem::EdgeCurvePoint> runtimeControls;
1141 for (const BuildingEdgeCurvePoint& point : controlPoints)
1142 runtimeControls.push_back(
1143 {static_cast<float>(point.x), static_cast<float>(point.y)});
1145 *world_, currentGroup.value().surfaceProviderName, runtimeControls, subdivisions);
1146 if (!surface.ok())
1147 return eve::editing::failed<DomainOperation>(
1148 EditorStatus::Rejected, RuleId("editor.building.edge-curve-surface-rejected"),
1149 "Updated curve could not be sampled on its committed surface provider");
1150 desired.value().group.surfaceProviderName = surface.value().providerName;
1151 desired.value().group.surfaceId = surface.value().surfaceId;
1152 desired.value().group.surfaceRevision = surface.value().surfaceRevision;
1153 desired.value().group.surfaceSamples = surface.value().samples;
1154 }
1155 std::vector<BuildingInstanceSnapshot> beforeInstances;
1156 for (int instanceId : currentGroup.value().instanceIds)
1157 beforeInstances.push_back(snapshot(world_->buildings().at(instanceId)));
1158 DomainOperation operation = std::move(desiredOperation).takeValue();
1159 operation.type = "building.edge-curve.replace.v1";
1160 operation.inverseType = operation.type;
1161 operation.payload = edgeCurveValue(desired.value().group, desired.value().instances);
1162 operation.inverse = edgeCurveValue(currentGroup.value(), beforeInstances);
1163 operation.hasInverse = true;
1164 return eve::editing::applied<DomainOperation>(std::move(operation));
1165}
1166
1168 double worldX, double worldY, double worldZ) const {
1169 if (!world_ || !std::isfinite(worldX) || !std::isfinite(worldY) ||
1170 !std::isfinite(worldZ))
1171 return eve::editing::failed<BuildingEdgeCurvePoint>(
1172 EditorStatus::Rejected, RuleId("editor.building.curve-world-point-invalid"),
1173 "Curve drag requires a finite world-space point and an available world");
1174 float ox = 0.f, oy = 0.f, xx = 0.f, xy = 0.f, yx = 0.f, yy = 0.f;
1175 world_->cellToWorldPlane(0, 0, ox, oy);
1176 world_->cellToWorldPlane(1, 0, xx, xy);
1177 world_->cellToWorldPlane(0, 1, yx, yy);
1178 const double bxX = xx - ox;
1179 const double bxY = xy - oy;
1180 const double byX = yx - ox;
1181 const double byY = yy - oy;
1182 const double determinant = bxX * byY - bxY * byX;
1183 if (std::abs(determinant) < 1e-9)
1184 return eve::editing::failed<BuildingEdgeCurvePoint>(EditorStatus::Unsupported,
1185 RuleId("editor.building.curve-grid-basis-singular"),
1186 "Curve drag requires an invertible affine grid basis");
1187 const double planeX = worldX;
1188 const double planeY = world_->getGridPlaneName() == "xz" ? worldZ : worldY;
1189 const double dx = planeX - ox;
1190 const double dy = planeY - oy;
1192 point.x = (dx * byY - dy * byX) / determinant;
1193 point.y = (bxX * dy - bxY * dx) / determinant;
1194 return eve::editing::applied<BuildingEdgeCurvePoint>(point);
1195}
1196
1198 auto current = instance(instanceId);
1199 if (!current.ok())
1200 return eve::editing::failed<DomainOperation>(current.code(), RuleId("editor.building.instance-not-found"),
1201 "Placed building instance was not found");
1202 return eve::editing::applied<DomainOperation>(instanceOperation(
1203 id_, "building.instance.delete.v4", "building.instance.set.v4", current.value(), current.value()));
1204}
1205
1207 if (!world_)
1208 return eve::editing::failed<void>(EditorStatus::Rejected, RuleId("editor.building.world-required"),
1209 "Placement world is unavailable");
1210 if (operation.target != TargetId(id_))
1211 return eve::editing::failed<void>(EditorStatus::Rejected, RuleId("editor.building.target-mismatch"),
1212 "Building operation targets another world");
1213 const bool areaSet = operation.type == "building.area.set.v1";
1214 const bool areaDelete = operation.type == "building.area.delete.v1";
1215 const bool edgeCurveSet = operation.type == "building.edge-curve.set.v1";
1216 const bool edgeCurveDelete = operation.type == "building.edge-curve.delete.v1";
1217 const bool edgeCurveReplace = operation.type == "building.edge-curve.replace.v1";
1218 if (edgeCurveSet || edgeCurveDelete || edgeCurveReplace) {
1219 auto parsedCurve = parseEdgeCurve(operation.payload);
1220 if (!parsedCurve.ok())
1221 return eve::editing::failed<void>(parsedCurve.code(), RuleId("editor.building.invalid-edge-curve-payload"),
1222 "Building edge curve operation payload is invalid");
1223 std::unique_ptr<IDomainOperationTarget> candidate = cloneDomainState();
1224 auto* buildingCandidate = dynamic_cast<BuildingPlacementTarget*>(candidate.get());
1225 if (!buildingCandidate)
1226 return eve::editing::failed<void>(EditorStatus::Failed,
1227 RuleId("editor.building.edge-curve-staging-unavailable"),
1228 "Building edge curve operation could not create a staging world");
1229 if (edgeCurveSet || edgeCurveReplace) {
1230 if (edgeCurveReplace) {
1231 auto current = buildingCandidate->world_->edgeCurveGroup(
1232 parsedCurve.value().group.id);
1233 if (!current.ok())
1234 return eve::editing::failed<void>(EditorStatus::NotFound,
1235 RuleId("editor.building.edge-curve-replace-source-not-found"),
1236 "Building edge curve replacement source was not found");
1237 for (int instanceId : current.value().instanceIds)
1238 if (!building::PlacementSystem::removeBuilding(buildingCandidate->world_,
1239 instanceId))
1240 return eve::editing::failed<void>(
1241 EditorStatus::Rejected, RuleId("editor.building.edge-curve-replace-delete-rejected"),
1242 "Building edge curve replacement could not remove its source");
1243 }
1244 std::vector<building::PlacedBuilding> members;
1245 for (const BuildingInstanceSnapshot& instance : parsedCurve.value().instances)
1246 members.push_back(runtime(instance));
1248 buildingCandidate->world_, parsedCurve.value().group, members);
1249 if (!restored.ok())
1250 return eve::editing::failed<void>(EditorStatus::Rejected,
1251 RuleId("editor.building.edge-curve-atomic-rejected"),
1252 "Building edge curve restore was rejected without mutation");
1253 } else {
1254 for (int instanceId : parsedCurve.value().group.instanceIds) {
1255 if (!buildingCandidate->world_->hasBuilding(instanceId) ||
1256 !building::PlacementSystem::removeBuilding(buildingCandidate->world_,
1257 instanceId))
1258 return eve::editing::failed<void>(EditorStatus::Rejected,
1259 RuleId("editor.building.edge-curve-delete-rejected"),
1260 "Building edge curve delete was rejected without mutation");
1261 }
1262 }
1263 return commitDomainState(std::move(candidate));
1264 }
1265 if (areaSet || areaDelete) {
1266 auto parsedArea = parseInstances(operation.payload);
1267 if (!parsedArea.ok())
1268 return eve::editing::failed<void>(parsedArea.code(), RuleId("editor.building.invalid-area-payload"),
1269 "Building area operation payload is invalid");
1270 std::unique_ptr<IDomainOperationTarget> candidate = cloneDomainState();
1271 auto* buildingCandidate = dynamic_cast<BuildingPlacementTarget*>(candidate.get());
1272 if (!buildingCandidate)
1273 return eve::editing::failed<void>(EditorStatus::Failed, RuleId("editor.building.area-staging-unavailable"),
1274 "Building area operation could not create a staging world");
1275 for (const BuildingInstanceSnapshot& item : parsedArea.value()) {
1276 DomainOperation single = instanceOperation(
1277 id_, areaSet ? "building.instance.set.v4" : "building.instance.delete.v4",
1278 areaSet ? "building.instance.delete.v4" : "building.instance.set.v4", item,
1279 item);
1280 auto applied = buildingCandidate->applyDomainOperation(single);
1281 if (!applied.ok())
1282 return eve::editing::failed<void>(applied.code(), RuleId("editor.building.area-atomic-rejected"),
1283 "Building area operation was rejected without mutation");
1284 }
1285 return commitDomainState(std::move(candidate));
1286 }
1287 auto parsed = parseInstance(operation.payload);
1288 if (!parsed.ok())
1289 return eve::editing::failed<void>(parsed.code(), RuleId("editor.building.invalid-instance"),
1290 "Building operation payload is invalid");
1291 if (operation.type == "building.instance.delete.v1" ||
1292 operation.type == "building.instance.delete.v2" ||
1293 operation.type == "building.instance.delete.v3" ||
1294 operation.type == "building.instance.delete.v4") {
1295 if (!world_->hasBuilding(parsed.value().instanceId))
1296 return eve::editing::failed<void>(EditorStatus::NotFound, RuleId("editor.building.instance-not-found"),
1297 "Placed building instance was not found");
1298 if (!building::PlacementSystem::removeBuilding(world_, parsed.value().instanceId))
1299 return eve::editing::failed<void>(EditorStatus::Failed, RuleId("editor.building.remove-failed"),
1300 "PlacementSystem rejected building removal");
1301 } else if (operation.type == "building.instance.set.v1" ||
1302 operation.type == "building.instance.set.v2" ||
1303 operation.type == "building.instance.set.v3" ||
1304 operation.type == "building.instance.set.v4") {
1305 const building::PlacedBuilding desired = runtime(parsed.value());
1306 if (!world_->hasBuilding(desired.instanceId)) {
1307 std::string reason;
1308 if (building::PlacementSystem::restoreExact(world_, desired, &reason) !=
1310 return eve::editing::failed<void>(
1311 EditorStatus::Rejected, RuleId("editor.building." + (reason.empty() ? "restore-failed" : reason)),
1312 "PlacementSystem rejected exact instance restore: " + reason);
1313 } else {
1314 const auto current = world_->buildings().find(desired.instanceId);
1315 const building::BuildingDefinition* desiredDefinition =
1317 if (!desiredDefinition)
1318 return eve::editing::failed<void>(EditorStatus::NotFound,
1319 RuleId("editor.building.definition-not-found"),
1320 "Desired building definition was not found");
1321 if (current->second.placementKind != desired.placementKind ||
1322 desired.placementKind != desiredDefinition->placementKind)
1323 return eve::editing::failed<void>(EditorStatus::Rejected,
1324 RuleId("editor.building.placement-domain-mismatch"),
1325 "Building set cannot change placement domain");
1326 std::string reason;
1327 const bool valid = desired.placementKind == "edge"
1329 world_, desired.buildingId, desired.edge.x, desired.edge.y,
1330 desired.edge.axis == building::EdgeAxis::Vertical ? "west"
1331 : "north",
1332 desired.instanceId, &reason)
1333 : desired.placementKind == "corner"
1335 world_, desired.buildingId, desired.corner.x,
1336 desired.corner.y, desired.instanceId, &reason)
1337 : desired.placementKind == "free"
1339 world_, desired.buildingId, desired.worldX,
1340 desired.worldY, desired.instanceId, &reason,
1341 desired.level)
1343 world_, desired.buildingId, desired.originCellX,
1344 desired.originCellY, desired.elevation,
1345 desired.rotationDeg, desired.instanceId, &reason);
1346 if (!valid)
1347 return eve::editing::failed<void>(
1348 EditorStatus::Rejected, RuleId("editor.building." + (reason.empty() ? "set-rejected" : reason)),
1349 "Building set preflight rejected without mutation: " + reason);
1350 if (current->second.buildingId != desired.buildingId) {
1352 world_, desired.instanceId, desired.buildingId);
1353 if (!replaced.ok())
1354 return eve::editing::failed<void>(EditorStatus::Rejected, RuleId("editor.building.replace-failed"),
1355 "PlacementSystem rejected building replacement");
1356 }
1357 auto moved = desired.placementKind == "edge"
1359 world_, desired.instanceId, desired.edge.x, desired.edge.y,
1360 desired.edge.axis == building::EdgeAxis::Vertical ? "west"
1361 : "north")
1362 : desired.placementKind == "corner"
1364 world_, desired.instanceId, desired.corner.x,
1365 desired.corner.y)
1366 : desired.placementKind == "free"
1368 world_, desired.instanceId, desired.worldX, desired.worldY,
1369 desired.elevation, desired.rotationDeg)
1371 world_, desired.instanceId, desired.originCellX,
1372 desired.originCellY, desired.rotationDeg);
1373 if (!moved.ok())
1374 return eve::editing::failed<void>(EditorStatus::Rejected, RuleId("editor.building.move-failed"),
1375 "PlacementSystem rejected building movement");
1376 building::PlacedBuilding& updated = world_->buildings().at(desired.instanceId);
1377 updated.worldX = desired.worldX;
1378 updated.worldY = desired.worldY;
1379 updated.elevation = desired.elevation;
1380 updated.placementKind = desired.placementKind;
1381 updated.edge = desired.edge;
1382 updated.corner = desired.corner;
1383 updated.surfaceId = desired.surfaceId;
1384 updated.surfaceRevision = desired.surfaceRevision;
1385 updated.surfaceNormalX = desired.surfaceNormalX;
1386 updated.surfaceNormalY = desired.surfaceNormalY;
1387 updated.surfaceNormalZ = desired.surfaceNormalZ;
1388 updated.surfaceTangentX = desired.surfaceTangentX;
1389 updated.surfaceTangentY = desired.surfaceTangentY;
1390 updated.surfaceTangentZ = desired.surfaceTangentZ;
1391 updated.surfaceSampleCount = desired.surfaceSampleCount;
1392 updated.surfaceMaxSlopeDegrees = desired.surfaceMaxSlopeDegrees;
1393 updated.surfaceHeightDelta = desired.surfaceHeightDelta;
1394 updated.props = desired.props;
1395 updated.tags = desired.tags;
1396 updated.garrison = desired.garrison;
1397 updated.garrisonRevision = desired.garrisonRevision;
1398 }
1399 } else {
1400 return eve::editing::failed<void>(EditorStatus::Unsupported, RuleId("editor.building.operation-unsupported"),
1401 "Building placement operation is unsupported: " + operation.type);
1402 }
1403 bumpRevision();
1404 widenDirty(0, 0);
1405 return eve::editing::applied<void>();
1406}
1407
1408std::unique_ptr<IDomainOperationTarget> BuildingPlacementTarget::cloneDomainState() const {
1409 if (!world_) return {};
1410 return std::unique_ptr<IDomainOperationTarget>(
1411 new BuildingPlacementTarget(id_, world_->cloneState(), revisionValue()));
1412}
1413
1415 std::unique_ptr<IDomainOperationTarget> candidate) {
1416 auto* buildingCandidate = dynamic_cast<BuildingPlacementTarget*>(candidate.get());
1417 if (!world_ || !buildingCandidate || !buildingCandidate->ownedWorld_ ||
1418 buildingCandidate->targetId() != targetId())
1419 return eve::editing::failed<void>(EditorStatus::Conflict, RuleId("editor.building.candidate-mismatch"),
1420 "Building candidate cannot be published to this target");
1421 world_->swapState(*buildingCandidate->ownedWorld_);
1422 bumpRevision();
1423 widenDirty(0, 0);
1424 return eve::editing::applied<void>();
1425}
1426
1429 EditorValue::Array curveGroups;
1430 if (world_)
1431 for (int index = 0; index < world_->getBuildingCount(); ++index) {
1432 const int instanceId = world_->getBuildingInstanceAt(index);
1433 const auto found = world_->buildings().find(instanceId);
1434 if (found != world_->buildings().end()) instances.push_back(instanceValue(snapshot(found->second)));
1435 }
1436 if (world_)
1437 for (building::EdgeCurveGroupId groupId : world_->edgeCurveGroupIds()) {
1438 auto group = world_->edgeCurveGroup(groupId);
1439 if (!group.ok()) continue;
1440 std::vector<BuildingInstanceSnapshot> members;
1441 for (int instanceId : group.value().instanceIds) {
1442 const auto found = world_->buildings().find(instanceId);
1443 if (found != world_->buildings().end()) members.push_back(snapshot(found->second));
1444 }
1445 curveGroups.push_back(edgeCurveValue(group.value(), members));
1446 }
1447 return EditorValue::Object{{"schemaVersion", int64_t{8}},
1448 {"worldId", world_ ? world_->getId() : std::string{}},
1449 {"instances", std::move(instances)},
1450 {"edgeCurveGroups", std::move(curveGroups)}};
1451}
1452
1454 BuildingPlacementTarget* target, std::string buildingId, int memberInstanceId,
1455 std::vector<BuildingEdgeCurvePoint> controlPoints, int subdivisions,
1456 std::string surfaceName)
1457 : target_(target),
1458 buildingId_(std::move(buildingId)),
1459 memberInstanceId_(memberInstanceId),
1460 controls_(std::move(controlPoints)),
1461 subdivisions_(subdivisions),
1462 surfaceName_(std::move(surfaceName)) {}
1463
1464Result<BuildingEdgeCurveDragPreview> BuildingEdgeCurveDragSession::previewCurrent() const {
1465 if (!target_)
1466 return eve::editing::failed<BuildingEdgeCurveDragPreview>(EditorStatus::Rejected,
1467 RuleId("editor.building.curve-drag-target-required"),
1468 "Curve drag target is unavailable");
1470 result.controlPointIndex = activeControlPointIndex_;
1471 result.curve = target_->previewEdgeCubicBezier(
1472 buildingId_, controls_, subdivisions_, memberInstanceId_, surfaceName_);
1473 result.gizmo = target_->edgeCubicBezierGizmo(
1474 buildingId_, controls_, subdivisions_, 0, memberInstanceId_, surfaceName_);
1475 result.status = result.curve.status;
1476 result.diagnostics = result.curve.diagnostics;
1477 return eve::editing::applied<BuildingEdgeCurveDragPreview>(std::move(result));
1478}
1479
1481 double rayOriginX, double rayOriginY, double rayOriginZ, double rayDirectionX,
1482 double rayDirectionY, double rayDirectionZ) {
1483 cancelDrag();
1484 if (!target_ || memberInstanceId_ <= 0 || controls_.size() != 4)
1485 return eve::editing::failed<BuildingEdgeCurveDragPreview>(
1486 EditorStatus::Rejected, RuleId("editor.building.curve-drag-state-invalid"),
1487 "Curve drag requires an existing group member and four controls");
1488 const double directionLength = std::sqrt(rayDirectionX * rayDirectionX +
1489 rayDirectionY * rayDirectionY +
1490 rayDirectionZ * rayDirectionZ);
1491 if (!std::isfinite(directionLength) || directionLength < 1e-9)
1492 return eve::editing::failed<BuildingEdgeCurveDragPreview>(EditorStatus::Rejected,
1493 RuleId("editor.building.curve-drag-ray-invalid"),
1494 "Curve drag requires a finite non-zero pointer ray");
1495 const std::array<double, 3> origin{rayOriginX, rayOriginY, rayOriginZ};
1496 const std::array<double, 3> direction{rayDirectionX / directionLength,
1497 rayDirectionY / directionLength,
1498 rayDirectionZ / directionLength};
1499 const editing::GizmoSnapshot gizmo = target_->edgeCubicBezierGizmo(
1500 buildingId_, controls_, subdivisions_, 0, memberInstanceId_, surfaceName_);
1501 double closest = std::numeric_limits<double>::infinity();
1502 for (const editing::GizmoPrimitive& primitive : gizmo.primitives) {
1503 if (primitive.kind != "sphere" || primitive.id.rfind("curve.control.", 0) != 0)
1504 continue;
1505 const std::array<double, 3> offset{origin[0] - primitive.position[0],
1506 origin[1] - primitive.position[1],
1507 origin[2] - primitive.position[2]};
1508 const double b = offset[0] * direction[0] + offset[1] * direction[1] +
1509 offset[2] * direction[2];
1510 const double c = offset[0] * offset[0] + offset[1] * offset[1] +
1511 offset[2] * offset[2] - primitive.radius * primitive.radius;
1512 const double discriminant = b * b - c;
1513 if (discriminant < 0.0) continue;
1514 const double nearHit = -b - std::sqrt(discriminant);
1515 const double farHit = -b + std::sqrt(discriminant);
1516 const double hit = nearHit >= 0.0 ? nearHit : farHit;
1517 if (hit < 0.0 || hit >= closest) continue;
1518 closest = hit;
1519 activeControlPointIndex_ = std::stoi(primitive.id.substr(14));
1520 dragPlanePoint_ = primitive.position;
1521 }
1522 if (activeControlPointIndex_ < 0)
1523 return eve::editing::failed<BuildingEdgeCurveDragPreview>(EditorStatus::NotFound,
1524 RuleId("editor.building.curve-control-not-hit"),
1525 "Pointer ray did not hit a curve control handle");
1526 dragPlaneNormal_ = direction;
1527 baseRevision_ = target_->revision();
1528 dragging_ = true;
1529 lastPreviewValid_ = gizmo.status == EditorStatus::Applied;
1530 return previewCurrent();
1531}
1532
1534 double rayOriginX, double rayOriginY, double rayOriginZ, double rayDirectionX,
1535 double rayDirectionY, double rayDirectionZ) {
1536 if (!dragging_ || !target_)
1537 return eve::editing::failed<BuildingEdgeCurveDragPreview>(
1538 EditorStatus::Rejected, RuleId("editor.building.curve-drag-not-active"), "Curve drag has not begun");
1539 if (target_->revision() != baseRevision_) {
1540 cancelDrag();
1541 return eve::editing::failed<BuildingEdgeCurveDragPreview>(
1542 EditorStatus::Conflict, RuleId("editor.building.curve-drag-stale"),
1543 "Curve world changed while the control handle was being dragged");
1544 }
1545 const double length = std::sqrt(rayDirectionX * rayDirectionX +
1546 rayDirectionY * rayDirectionY +
1547 rayDirectionZ * rayDirectionZ);
1548 if (!std::isfinite(length) || length < 1e-9)
1549 return eve::editing::failed<BuildingEdgeCurveDragPreview>(EditorStatus::Rejected,
1550 RuleId("editor.building.curve-drag-ray-invalid"),
1551 "Curve drag requires a finite non-zero pointer ray");
1552 const std::array<double, 3> origin{rayOriginX, rayOriginY, rayOriginZ};
1553 const std::array<double, 3> direction{rayDirectionX / length, rayDirectionY / length,
1554 rayDirectionZ / length};
1555 const double denominator = direction[0] * dragPlaneNormal_[0] +
1556 direction[1] * dragPlaneNormal_[1] +
1557 direction[2] * dragPlaneNormal_[2];
1558 if (std::abs(denominator) < 1e-9)
1559 return eve::editing::failed<BuildingEdgeCurveDragPreview>(EditorStatus::Rejected,
1560 RuleId("editor.building.curve-drag-ray-parallel"),
1561 "Pointer ray is parallel to the curve drag plane");
1562 const double distance = ((dragPlanePoint_[0] - origin[0]) * dragPlaneNormal_[0] +
1563 (dragPlanePoint_[1] - origin[1]) * dragPlaneNormal_[1] +
1564 (dragPlanePoint_[2] - origin[2]) * dragPlaneNormal_[2]) /
1565 denominator;
1566 if (distance < 0.0)
1567 return eve::editing::failed<BuildingEdgeCurveDragPreview>(EditorStatus::Rejected,
1568 RuleId("editor.building.curve-drag-behind-camera"),
1569 "Curve drag plane is behind the pointer ray origin");
1570 auto logical = target_->curveLogicalPointFromWorld(
1571 origin[0] + direction[0] * distance, origin[1] + direction[1] * distance,
1572 origin[2] + direction[2] * distance);
1573 if (!logical.ok())
1574 return eve::editing::failed<BuildingEdgeCurveDragPreview>(
1575 logical.code(), RuleId("editor.building.curve-drag-projection-failed"),
1576 "Pointer ray could not be projected to logical curve coordinates");
1577 controls_[static_cast<size_t>(activeControlPointIndex_)] = logical.value();
1578 auto preview = previewCurrent();
1579 if (preview.ok()) lastPreviewValid_ = preview.value().status == EditorStatus::Applied;
1580 return preview;
1581}
1582
1584 if (!dragging_ || !target_)
1585 return eve::editing::failed<DomainOperation>(
1586 EditorStatus::Rejected, RuleId("editor.building.curve-drag-not-active"), "Curve drag has not begun");
1587 if (target_->revision() != baseRevision_) {
1588 cancelDrag();
1589 return eve::editing::failed<DomainOperation>(EditorStatus::Conflict, RuleId("editor.building.curve-drag-stale"),
1590 "Curve world changed before drag commit");
1591 }
1592 if (!lastPreviewValid_)
1593 return eve::editing::failed<DomainOperation>(EditorStatus::Rejected,
1594 RuleId("editor.building.curve-drag-preview-invalid"),
1595 "The last curve drag preview is not valid for commit");
1596 auto operation = target_->makeUpdateEdgeCubicBezier(memberInstanceId_, controls_,
1597 subdivisions_);
1598 if (!operation.ok()) return operation;
1599 cancelDrag();
1600 return operation;
1601}
1602
1604 dragging_ = false;
1605 activeControlPointIndex_ = -1;
1606 lastPreviewValid_ = false;
1607}
1608
1609} // namespace eve::building_editing
LogicalId target
ActionParameterOperation operation
double value
Value::Object payload
float y
Definition AnimClip.cpp:738
float x
Definition AnimClip.cpp:738
float rotationDegrees
building::EdgeCurveGroup group
std::vector< BuildingInstanceSnapshot > instances
int ax
Definition CaveMesh.cpp:113
int ay
Definition CaveMesh.cpp:113
int bx
Definition CaveMesh.cpp:114
float length
Definition CaveMesh.cpp:94
int by
Definition CaveMesh.cpp:114
float py
std::map< std::string, Var > values
std::string message
std::uint32_t key
glm::uvec4 ids
glm::vec3 n
Definition Grass.cpp:63
std::int32_t c
float elevation
std::vector< Colorf > px
std::string text
size_t offset
std::array< float, 4 > rotation
std::map< std::string, PropertyRule > properties
bool valid
MeleePoint3 b
Definition MeleeHit.cpp:41
MeleePoint3 a
Definition MeleeHit.cpp:40
float distance
int level
std::string error
Definition Package.cpp:60
World3D * world
std::array< std::uint64_t, kPixelChunkSize *kPixelChunkSize > updated
std::uint64_t revision
std::array< PixelCell, kPixelChunkSize *kPixelChunkSize > cells
std::vector< Point > vertices
float radius
std::string path
Definition PlayHost.cpp:110
std::string id
Definition PlayHost.cpp:108
bool hit
PrimitiveHandle handle
V3 origin
Definition RoadBake.cpp:138
RoadLaneDirection direction
const RoadEdge * edge
double number
bool found
double current
std::string string
float dy
float dx
std::uint32_t count
Cell cell
bool placed
LogicalId definition
float(ui::Theme::* member)[4]
std::map< std::string, std::span< const std::uint8_t > > members
uint32_t index
const UnitySourceAsset & source
std::size_t at
double oy
double ox
std::vector< int > edges
glm::vec3 point
Move-only operation result carrying either a value or Status.
Definition Result.h:155
UUID-backed identifier adapter for legacy textual boundaries.
Definition Identity.h:314
static const BuildingDefinition * find(const std::string &id)
Finds .
static eve::Result< EdgeCurveSurface > sampleEdgeCurveSurface(const PlacementWorld &world, const std::string &surfaceName, const std::vector< EdgeCurvePoint > &controlPoints, int subdivisions)
Sample one same-identity custom surface frame at every analytic curve segment.
static SnapResult snap(const PlacementWorld &world, const std::string &buildingId, float worldX, float worldY)
static eve::Result< std::vector< CornerAddress > > sampleEdgeCubicBezier(const std::vector< EdgeCurvePoint > &controlPoints, int subdivisions)
Deterministically rasterize four cubic Bezier controls to adjacent grid vertices.
static eve::Result< AreaPreview > previewRectangle(PlacementWorld *world, const std::string &buildingId, int minCellX, int minCellY, int maxCellX, int maxCellY, float rotationDeg=0.f)
Preview every anchor in an inclusive rectangular area without mutation.
static eve::Result< EdgePathPreview > previewEdgeCubicBezier(PlacementWorld *world, const std::string &buildingId, const std::vector< EdgeCurvePoint > &controlPoints, int subdivisions)
Sample and validate a cubic Bezier edge curve without mutating the world.
static eve::Result< EdgePathPreview > previewEdgePath(PlacementWorld *world, const std::string &buildingId, const std::vector< CornerAddress > &vertices)
Validate and expand a multi-segment orthogonal path without mutation.
static PlacementRestoreStatus restoreExact(PlacementWorld *world, const PlacedBuilding &placed, std::string *reason=nullptr)
Restore an exact persistent instance for editor undo or snapshot loading.
static eve::Result< PlacementEditReceipt > moveCornerResult(PlacementWorld *world, int instanceId, int vertexX, int vertexY)
Atomically move a corner-domain instance to another grid vertex.
static void ensureBuiltins()
确保内置规则已注册(模块首次使用时自动调用)。
static bool canPlaceFree(PlacementWorld *world, const std::string &buildingId, float worldX, float worldY, int excludeInstanceId=0, std::string *reason=nullptr, int level=std::numeric_limits< int >::min(), float rotationDeg=0.f)
Validate an exact free-domain world-plane anchor and circular or oriented-box footprint.
static bool foreachFootprintCell(const BuildingDefinition &def, int originCellX, int originCellY, float rotationDeg, const std::function< bool(int cx, int cy)> &fn)
枚举占地格子(旋转后局部 → 世界格子)。返回 false 若定义未知。
static eve::Result< PlacementHit > sampleSurface(const PlacementWorld &world, const std::string &name, float x, float y)
Sample a named placement surface.
static float normalizeRotation(const std::string &buildingId, float rotationDeg)
规范化旋转角(cardinal → 0/90/180/270;none → 0)。
static eve::Result< PlacementEditReceipt > moveFreeResult(PlacementWorld *world, int instanceId, float worldX, float worldY, float elevation, float rotationDeg)
Atomically move a free-domain instance to an exact world-plane anchor.
static eve::Result< PlacementEditReceipt > replaceBuildingResult(PlacementWorld *world, int instanceId, const std::string &replacementBuildingId)
Atomically replace an instance definition at its current cell or edge address.
static bool removeBuilding(PlacementWorld *world, int instanceId)
static eve::Result< PlacementEditReceipt > moveEdgeResult(PlacementWorld *world, int instanceId, int cellX, int cellY, const std::string &direction)
Atomically move an edge-domain instance to a canonical grid edge.
static bool canPlaceCorner(PlacementWorld *world, const std::string &buildingId, int vertexX, int vertexY, int excludeInstanceId=0, std::string *reason=nullptr, int level=std::numeric_limits< int >::min())
Validate a corner-domain placement at one canonical grid vertex.
static bool canPlaceElev(PlacementWorld *world, const std::string &buildingId, int cellX, int cellY, float elevation, float rotationDeg=0.f, int excludeInstanceId=0, std::string *reason=nullptr, int level=std::numeric_limits< int >::min())
static eve::Result< AreaPreview > previewBrush(PlacementWorld *world, const std::string &buildingId, int centerCellX, int centerCellY, int radius, float rotationDeg=0.f)
Preview anchors inside an inclusive Euclidean cell-radius brush.
static eve::Result< EdgeCurveGroup > restoreEdgeCurveGroupExact(PlacementWorld *world, const EdgeCurveGroup &group, const std::vector< PlacedBuilding > &members)
Atomically restore an exact curve group and all of its exact-id edge members.
static eve::Result< PlacementEditReceipt > moveBuildingResult(PlacementWorld *world, int instanceId, int cellX, int cellY, float rotationDeg=-1.f)
Atomically move a cell-domain instance while preserving its identity and state.
static bool canPlaceEdge(PlacementWorld *world, const std::string &buildingId, int cellX, int cellY, const std::string &direction, int excludeInstanceId=0, std::string *reason=nullptr, int level=std::numeric_limits< int >::min())
Validate an edge-domain placement without mutating the world.
格子型建筑放置世界(脚本可直接操作)。
std::unique_ptr< PlacementWorld > cloneState() const
Clone complete logical state into an independently owned world candidate.
float getFloorHeight() const
Height between discrete floors along the grid plane normal.
bool hasBuilding(int instanceId) const
True when building.
const std::unordered_map< int, PlacedBuilding > & buildings() const
int getBuildingInstanceAt(int index) const
按插入顺序取实例 id。
std::string getId() const
世界 id(用于变更事件定位)。
std::vector< EdgeCurveGroupId > edgeCurveGroupIds() const
Return an owning ascending list of all curve-group identities.
int getBuildingCount() const
Returns the building count.
std::string getGridPlaneName() const
Returns the grid plane name.
void swapState(PlacementWorld &candidate) noexcept
Atomically exchange complete logical state with a prepared candidate.
void cellToWorldPlane(int cellX, int cellY, float &px, float &py) const
Cell to world plane.
eve::Result< EdgeCurveGroup > edgeCurveGroupForInstance(int instanceId) const
Return the owning curve-group snapshot linked by an edge instance.
eve::Result< EdgeCurveGroup > edgeCurveGroup(EdgeCurveGroupId id) const
Return an owning curve-group snapshot.
float getCellSize() const
Returns the cell size.
BuildingEdgeCurveDragSession(BuildingPlacementTarget *target, std::string buildingId, int memberInstanceId, std::vector< BuildingEdgeCurvePoint > controlPoints, int subdivisions, std::string surfaceName={})
Configure an idle drag session from authoritative editor selection state.
Result< DomainOperation > finishDrag()
Build the atomic replacement operation for the last valid draft and become idle.
Result< BuildingEdgeCurveDragPreview > beginDrag(double rayOriginX, double rayOriginY, double rayOriginZ, double rayDirectionX, double rayDirectionY, double rayDirectionZ)
Pick and begin dragging the closest control sphere intersected by a world ray.
Result< BuildingEdgeCurveDragPreview > updateDrag(double rayOriginX, double rayOriginY, double rayOriginZ, double rayDirectionX, double rayDirectionY, double rayDirectionZ)
Reproject a pointer ray onto the camera-facing drag plane and refresh the draft.
void cancelDrag()
Discard transient drag state without producing an operation.
Live PlacementWorld target with reversible place/move/remove operations.
static CapabilityId editorCapabilityId()
Stable capability id for placement preview and mutation.
void * queryCapability(const CapabilityId &capability) override
Query an optional target capability.
Result< DomainOperation > makeMoveFree(int instanceId, double worldX, double worldY, double elevation, double rotationDegrees) const
Build an exact free-domain move operation without grid snapping.
TargetId targetId() const override
Target id.
Result< BuildingInstanceSnapshot > instance(int instanceId) const
Return a complete immutable instance snapshot.
Result< DomainOperation > makeEdgePath(const std::string &buildingId, const std::vector< BuildingEdgePathVertex > &vertices) const
Plan one atomic reversible multi-segment edge path gesture.
BuildingPlacementTarget(std::string id, building::PlacementWorld *world)
Bind a borrowed world that must outlive the target.
Result< BuildingEdgeCurvePoint > curveLogicalPointFromWorld(double worldX, double worldY, double worldZ) const
Convert a world-space point to continuous logical grid coordinates.
Result< DomainOperation > makePlace(const BuildingInstanceSnapshot &placed) const
Plan reversible exact-id placement after full footprint validation.
Result< DomainOperation > makeRectangle(const std::string &buildingId, int minCellX, int minCellY, int maxCellX, int maxCellY, double rotationDegrees) const
Plan one atomic reversible rectangular placement gesture.
Result< DomainOperation > makeMove(int instanceId, int cellX, int cellY, double rotationDegrees) const
Plan reversible movement of an existing building.
Result< DomainOperation > makeUpdateEdgeCubicBezier(int memberInstanceId, const std::vector< BuildingEdgeCurvePoint > &controlPoints, int subdivisions) const
Plan atomic replacement of an existing curve group after a handle drag.
Result< DomainOperation > makeEdgeCubicBezier(const std::string &buildingId, const std::vector< BuildingEdgeCurvePoint > &controlPoints, int subdivisions) const
Plan one atomic reversible cubic Bezier edge-curve gesture.
EditorValue snapshotValue() const
Capture all instances in deterministic insertion order.
Result< DomainOperation > makeReplace(int instanceId, const std::string &replacementBuildingId) const
Plan a reversible cell-definition replacement preserving instance state.
Result< void > applyDomainOperation(const DomainOperation &operation) override
Applies domain operation.
Result< DomainOperation > makeEdgeCubicBezierOnSurface(const std::string &buildingId, const std::vector< BuildingEdgeCurvePoint > &controlPoints, int subdivisions, const std::string &surfaceName) const
Plan one atomic reversible cubic curve sampled on a named surface.
Result< void > commitDomainState(std::unique_ptr< IDomainOperationTarget > candidate) override
Commits domain state.
BuildingPlacementPreview preview(const std::string &buildingId, double worldX, double worldY, double elevation, double rotationDegrees, int excludeInstanceId=0) const
Preview snap, normalized rotation, footprint cells and validation reasons.
BuildingEdgeCurvePreview previewEdgeCubicBezier(const std::string &buildingId, const std::vector< BuildingEdgeCurvePoint > &controlPoints, int subdivisions, int replacingMemberInstanceId=0, const std::string &surfaceName={}) const
Recompute an immutable interactive curve draft without mutating the world.
std::unique_ptr< IDomainOperationTarget > cloneDomainState() const override
Clones domain state.
int nextAvailableInstanceId() const
Return the smallest currently unused positive instance id.
Result< DomainOperation > makeBrush(const std::string &buildingId, int centerCellX, int centerCellY, int radius, double rotationDegrees) const
Plan one atomic reversible circular-brush placement gesture.
Result< DomainOperation > makeRemove(int instanceId) const
Plan reversible removal preserving properties, tags and garrison state.
editing::GizmoSnapshot edgeCubicBezierGizmo(const std::string &buildingId, const std::vector< BuildingEdgeCurvePoint > &controlPoints, int subdivisions, int level=0, int replacingMemberInstanceId=0, const std::string &surfaceName={}) const
Build renderer-neutral handles, tangent lines and sampled path for a curve draft.
void bumpRevision()
Advance the revision without touching the dirty region.
Revision revisionValue() const noexcept
Return the revision as the shared protocol revision type.
std::uint64_t revision() const override
Return the current revision.
void widenDirty()
Mark an unknown area dirty without advancing the revision.
eve::OptionalRef< C > capability()
Query a capability through its interface-owned stable identity.
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.
editing::DomainOperation DomainOperation
editing::TargetId TargetId
editing::DiagnosticSeverity DiagnosticSeverity
editing::DomainOperation DomainOperation
editing::RuleId RuleId
editing::TargetId TargetId
Result< T > failed(Status status, eve::DiagnosticCode code, RuleId rule, std::string message)
Construct a failed editing result with explicit status and diagnostic categories.
Diagnostic ruleDiagnostic(eve::DiagnosticCode code, RuleId rule, DiagnosticSeverity severity, std::string message)
Build a common diagnostic carrying an open editing rule identity.
eve::building_editing::BuildingInstanceSnapshot BuildingInstanceSnapshot
double sample(const Heightmap &map, double u, double v)
Sample.
const EditorValue * field(const EditorValue &value, const char *name)
int64_t integer(const RuntimeTensor &v, size_t i=0)
Integer.
Build metadata (engine git commit, build time, third-party version).
Definition Build.cpp:16
DiagnosticCode
Stable machine-readable diagnostic codes.
Definition Diagnostic.h:47
Severity
Stable severity of a diagnostic, independent of its display text.
Definition Diagnostic.h:20
detail::StrongUint64< detail::RevisionTag > Revision
Monotonic content/state revision used for optimistic-concurrency checks.
Definition Revision.h:13
建筑模板(进程级注册表中的定义)。
std::string placementKind
Placement domain: cell (default), edge, corner, or free.
std::string channel
占地格子尺寸。
std::vector< std::string > tags
Canonical address of one logical grid vertex used by corner objects.
Canonical address of one grid edge.
Strong world-local identity of one authoritative edge-curve group.
World-owned authoritative description and membership of one committed edge curve.
已放置的建筑实例。
eve::Revision garrisonRevision
Revision used by the garrison container adapter for stale checks.
std::vector< std::string > tags
EdgeAddress edge
Canonical edge address when placementKind is edge.
std::vector< GarrisonMember > garrison
Authoritative logical garrison membership, not a render list.
uint64_t surfaceRevision
Provider revision observed when the surface attachment was sampled.
std::unordered_map< std::string, std::string > props
int level
Authoritative discrete floor coordinate used by occupancy and topology.
std::string placementKind
Placement domain snapshot: cell, edge, corner, or free.
CornerAddress corner
Canonical grid vertex when placementKind is corner.
std::string surfaceId
Stable provider-local surface name used to create this placement; empty for unsurfaced placement.
float surfaceTangentX
Unit tangent captured with the normal to define the local placement frame.
float surfaceNormalX
Unit surface normal captured at placement time.
Immutable result of one curve-handle pointer update.
Runtime-neutral cubic Bezier control point in logical grid-vertex space.
Immutable revision-tagged result for one interactive cubic curve draft.
std::vector< BuildingEdgeCurvePoint > controlPoints
std::vector< EditorDiagnostic > diagnostics
Runtime-neutral logical vertex used to author an orthogonal edge path.
Persistent garrison member copied through building editor undo.
Complete runtime-neutral placed-building snapshot.
std::string placementKind
Placement coordinate domain: cell, edge, corner, or free.
int level
Authoritative discrete floor coordinate.
std::map< std::string, std::string > properties
int cornerX
Grid vertex address when placementKind is corner.
std::vector< BuildingGarrisonMemberRecord > garrison
Structured footprint/snap preview tied to a world revision.
std::vector< BuildingFootprintCell > cells
std::vector< EditorDiagnostic > diagnostics
DomainOperation public API.
Renderer-neutral wire primitive for authoring viewport overlays.
Immutable revision-tagged overlay snapshot consumed by any viewport renderer.
std::vector< GizmoPrimitive > primitives
std::vector< Diagnostic > diagnostics
Stable discovery metadata exposed by every editable target.
std::vector< CapabilityId > capabilities
glm::vec4 color