28 std::vector<std::string> result;
35 const auto*
text = item.getIf<std::string>();
40 result.push_back(*
text);
51 {
"tags", stringArray(
member.tags)}});
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)}};
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;
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;
95 const auto number = [&](
const char*
key) ->
const double* {
97 return entry ? entry->getIf<
double>() : nullptr;
99 const auto string = [&](
const char*
key) ->
const std::string* {
101 return entry ? entry->getIf<std::string>() : nullptr;
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");
111 const auto* channel =
string(
"channel");
112 const auto* propertiesValue =
field(
value,
"properties");
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 ||
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>();
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>();
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);
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;
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);
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");
187 result.channel = *channel;
188 result.garrisonRevision =
static_cast<std::uint64_t
>(*garrisonRevision);
190 const auto*
text =
property.getIf<std::string>();
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;
198 result.tags = parseStringArray(*tagsValue,
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);
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)});
220 return eve::editing::applied<BuildingInstanceSnapshot>(std::move(result));
223Result<std::vector<BuildingInstanceSnapshot>> parseInstances(
const EditorValue&
value) {
224 const auto*
values =
value.getIf<EditorValue::Array>();
227 EditorStatus::Rejected,
RuleId(
"editor.building.invalid-area-payload"),
228 "Building area payload must contain at least one instance");
229 std::vector<BuildingInstanceSnapshot> result;
231 result.reserve(
values->size());
232 for (
const EditorValue& item : *
values) {
233 auto parsed = parseInstance(item);
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)
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());
244 return eve::editing::applied<std::vector<BuildingInstanceSnapshot>>(std::move(result));
247struct EdgeCurvePayload {
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);
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});
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)}};
280Result<EdgeCurvePayload> parseEdgeCurve(
const EditorValue&
value) {
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);
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;
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);
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");
329 std::array<float, 6> frame{};
330 for (
size_t component = 0; component < 6; ++component) {
331 const auto*
number = (*samples)[
index + component].getIf<
double>();
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);
338 result.group.surfaceSamples.push_back(
339 {frame[0], frame[1], frame[2], frame[3], frame[4], frame[5]});
343 const auto*
x = (*controls)[
index].getIf<
double>();
344 const auto*
y = (*controls)[
index + 1].getIf<
double>();
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)});
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));
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)
396 result.garrisonRevision =
placed.garrisonRevision.value();
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;
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)
442 const BuildingInstanceSnapshot&
payload,
443 const BuildingInstanceSnapshot& inverse) {
446 result.inverseType = std::move(inverseType);
448 result.payload = instanceValue(
payload);
449 result.inverse = instanceValue(inverse);
450 result.hasInverse =
true;
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);
465 return eve::editing::failed<DomainOperation>(EditorStatus::Rejected,
466 RuleId(
"editor.building.area-definition-invalid"),
467 "Area placement requires a cell building definition");
470 operation.inverseType =
"building.area.delete.v1";
474 for (
const building::PlacementSystem::AreaCellPreview&
cell : preview.
cells) {
475 while (
world->hasBuilding(nextId)) ++nextId;
478 placed.buildingId = preview.buildingId;
481 placed.rotationDegrees = preview.rotationDeg;
486 world->cellToWorldPlane(
cell.cellX,
cell.cellY, worldX, worldY);
496 return eve::editing::applied<DomainOperation>(std::move(
operation));
512BuildingPlacementTarget& BuildingPlacementTarget::operator=(BuildingPlacementTarget&&) noexcept = default;
513BuildingPlacementTarget::~BuildingPlacementTarget() = default;
518 result.
type =
"building-placement-world";
520 result.
readOnly = world_ ==
nullptr;
531 return eve::editing::failed<BuildingInstanceSnapshot>(
532 EditorStatus::Rejected,
RuleId(
"editor.building.world-required"),
"Placement world is unavailable");
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));
542 if (!world_)
return 0;
551 int excludeInstanceId)
const {
555 result.
status = EditorStatus::Rejected;
557 RuleId(
"editor.building.world-required"), DiagnosticSeverity::Error,
558 "Placement world is unavailable"));
564 result.
status = EditorStatus::NotFound;
566 RuleId(
"editor.building.unknown-building"), DiagnosticSeverity::Error,
567 "Building definition was not found: " + buildingId));
571 static_cast<float>(worldX),
572 static_cast<float>(worldY));
583 BuildingFootprintCell cell;
586 cell.inBounds = world_->inBounds(x, y);
588 cell.occupant = world_->getOccupantInChannel(definition->channel, x, y);
589 cell.terrain = world_->getTerrain(x, y);
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;
602 "Building footprint validation rejected placement: " + reason));
609 return eve::editing::failed<DomainOperation>(EditorStatus::Conflict,
610 RuleId(
"editor.building.instance-id-conflict"),
611 "Exact placement instance id is unavailable");
616 placed.edgeAxis ==
"vertical" ?
"west" :
"north", 0, &reason)
617 :
placed.placementKind ==
"corner"
621 :
placed.placementKind ==
"free"
623 world_,
placed.buildingId,
static_cast<float>(
placed.worldX),
624 static_cast<float>(
placed.worldY), 0, &reason,
placed.level)
627 static_cast<float>(
placed.elevation),
628 static_cast<float>(
placed.rotationDegrees), 0, &reason);
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));
641 return eve::editing::failed<DomainOperation>(
current.code(),
RuleId(
"editor.building.instance-not-found"),
642 "Placed building instance was not found");
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;
669 static_cast<float>(desired.
worldY), instanceId, &reason,
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()));
685 int instanceId,
double worldX,
double worldY,
double elevation,
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");
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,
714 int instanceId,
const std::string& replacementBuildingId)
const {
717 return eve::editing::failed<DomainOperation>(
current.code(),
RuleId(
"editor.building.instance-not-found"),
718 "Placed building instance was not found");
722 return eve::editing::failed<DomainOperation>(EditorStatus::NotFound,
723 RuleId(
"editor.building.replacement-not-found"),
724 "Replacement building definition was not found");
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;
732 return eve::editing::failed<DomainOperation>(EditorStatus::Rejected,
733 RuleId(
"editor.building.replace-domain-mismatch"),
734 "Replacement must use the same placement domain");
736 const bool valid = sourceKind ==
"edge"
738 world_, replacementBuildingId,
current.value().edgeX,
740 current.value().edgeAxis ==
"vertical" ?
"west" :
"north",
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,
753 world_, replacementBuildingId,
current.value().cellX,
755 static_cast<float>(
current.value().elevation),
756 static_cast<float>(
current.value().rotationDegrees), instanceId,
759 return eve::editing::failed<DomainOperation>(
760 EditorStatus::Rejected,
RuleId(
"editor.building." + (reason.empty() ?
"rejected" : reason)),
761 "Building replacement validation failed: " + reason);
765 desired.channel = replacement->
channel;
766 desired.tags = replacement->
tags;
767 if (sourceKind ==
"cell")
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()));
775 const std::string& buildingId,
int minCellX,
int minCellY,
int maxCellX,
int maxCellY,
778 world_, buildingId, minCellX, minCellY, maxCellX, maxCellY,
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());
788 const std::string& buildingId,
int centerCellX,
int centerCellY,
int radius,
791 world_, buildingId, centerCellX, centerCellY,
radius,
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());
800 const std::string& buildingId,
801 const std::vector<BuildingEdgePathVertex>&
vertices)
const {
802 std::vector<building::CornerAddress> runtimeVertices;
803 runtimeVertices.reserve(
vertices.size());
805 runtimeVertices.push_back({vertex.x, vertex.y});
809 return eve::editing::failed<DomainOperation>(
810 EditorStatus::Rejected,
RuleId(
"editor.building.edge-path-rejected"),
"Edge path preview was rejected");
814 return eve::editing::failed<DomainOperation>(EditorStatus::NotFound,
815 RuleId(
"editor.building.definition-not-found"),
816 "Edge path definition was not found");
819 operation.inverseType =
"building.area.delete.v1";
824 while (world_->hasBuilding(nextId)) ++nextId;
827 placed.buildingId = buildingId;
828 placed.placementKind =
"edge";
834 placed.level = preview.value().level;
836 placed.channel = definition->channel;
837 placed.tags = definition->tags;
838 float ax = 0.f,
ay = 0.f,
bx = 0.f,
by = 0.f;
841 edge.
y + (
edge.axis == building::EdgeAxis::Vertical ? 1 : 0),
852 return eve::editing::applied<DomainOperation>(std::move(
operation));
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());
862 runtimeControls.push_back(
863 {
static_cast<float>(
point.x),
static_cast<float>(
point.y)});
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());
873 editorVertices.push_back({vertex.x, vertex.y});
874 auto path = makeEdgePath(buildingId, editorVertices);
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;
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);
893 operation.inverseType =
"building.edge-curve.delete.v1";
896 return eve::editing::applied<DomainOperation>(std::move(
operation));
900 const std::string& buildingId,
901 const std::vector<BuildingEdgeCurvePoint>& controlPoints,
int subdivisions,
902 const std::string& surfaceName)
const {
904 return eve::editing::failed<DomainOperation>(EditorStatus::Rejected,
RuleId(
"editor.building.world-required"),
905 "Placement world is unavailable");
908 std::vector<building::PlacementSystem::EdgeCurvePoint> controls;
909 controls.reserve(controlPoints.size());
911 controls.push_back({
static_cast<float>(
point.x),
static_cast<float>(
point.y)});
913 *world_, surfaceName, controls, subdivisions);
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);
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;
928 enriched.
payload = edgeCurveValue(parsed.value().group, parsed.value().instances);
930 return eve::editing::applied<DomainOperation>(std::move(enriched));
934 const std::string& buildingId,
935 const std::vector<BuildingEdgeCurvePoint>& controlPoints,
int subdivisions,
936 int replacingMemberInstanceId,
const std::string& surfaceName)
const {
941 result.
status = EditorStatus::Rejected;
944 RuleId(
"editor.building.world-required"), DiagnosticSeverity::Error,
945 "Placement world is unavailable"));
948 std::vector<building::PlacementSystem::EdgeCurvePoint> controls;
950 controls.push_back({
static_cast<float>(
point.x),
static_cast<float>(
point.y)});
953 result.status = EditorStatus::Rejected;
956 RuleId(
"editor.building.edge-curve-invalid"), DiagnosticSeverity::Error,
957 "Cubic edge curve controls or subdivisions are invalid"));
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;
970 RuleId(
"editor.building.edge-curve-group-not-found"),
971 DiagnosticSeverity::Error,
972 "Replacement preview instance does not belong to a curve group"));
975 previewOwner = world_->cloneState();
976 for (
int instanceId : replacingGroup.
value().instanceIds)
977 building::PlacementSystem::removeBuilding(previewOwner.get(), instanceId);
978 previewWorld = previewOwner.get();
981 previewWorld, buildingId, controls, subdivisions);
983 result.status = EditorStatus::Rejected;
985 preview.error() ? preview.error()->code()
989 :
"Cubic edge curve preview was rejected"));
993 result.
edges.push_back(
996 if (!surfaceName.empty()) {
998 *world_, surfaceName, controls, subdivisions);
1000 result.status = EditorStatus::Rejected;
1002 surface.error() ? surface.error()->code()
1004 RuleId(
"editor.building.edge-curve-surface-rejected"),
1007 :
"Curve surface preview was rejected"));
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(
1018 result.status = EditorStatus::Applied;
1023 const std::string& buildingId,
1024 const std::vector<BuildingEdgeCurvePoint>& controlPoints,
int subdivisions,
1025 int level,
int replacingMemberInstanceId,
const std::string& surfaceName)
const {
1031 replacingMemberInstanceId, surfaceName);
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;
1040 const double px =
ox + gx * (xx -
ox) + gy * (yx -
ox);
1041 const double py =
oy + gx * (xy -
oy) + gy * (yy -
oy);
1043 if (!surfaceName.empty()) {
1045 *world_, surfaceName,
static_cast<float>(
px),
static_cast<float>(
py));
1047 return std::array<double, 3>{
hit.value().worldX,
hit.value().worldY,
1048 hit.value().worldZ};
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) {
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];
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;
1070 line.dashed = dashed;
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;
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};
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);
1092 if (!
preview.surfaceSamples.empty()) {
1096 addLine(
"curve.path." + std::to_string(
index - 1),
1097 {
a.worldX,
a.worldY,
a.worldZ}, {
b.worldX,
b.worldY,
b.worldZ},
1102 addLine(
"curve.path." + std::to_string(
index - 1),
1113 int memberInstanceId,
const std::vector<BuildingEdgeCurvePoint>& controlPoints,
1114 int subdivisions)
const {
1116 return eve::editing::failed<DomainOperation>(EditorStatus::Rejected,
RuleId(
"editor.building.world-required"),
1117 "Placement world is unavailable");
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)
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");
1131 currentGroup.value().buildingId, controlPoints, subdivisions);
1132 if (!desiredOperation.ok())
return desiredOperation;
1133 auto desired = parseEdgeCurve(desiredOperation.value().payload);
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;
1142 runtimeControls.push_back(
1143 {
static_cast<float>(
point.x),
static_cast<float>(
point.y)});
1145 *world_, currentGroup.value().surfaceProviderName, runtimeControls, subdivisions);
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;
1155 std::vector<BuildingInstanceSnapshot> beforeInstances;
1156 for (
int instanceId : currentGroup.
value().instanceIds)
1157 beforeInstances.push_back(snapshot(world_->buildings().
at(instanceId)));
1161 operation.payload = edgeCurveValue(desired.value().group, desired.value().instances);
1162 operation.inverse = edgeCurveValue(currentGroup.value(), beforeInstances);
1164 return eve::editing::applied<DomainOperation>(std::move(
operation));
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;
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);
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()));
1208 return eve::editing::failed<void>(EditorStatus::Rejected,
RuleId(
"editor.building.world-required"),
1209 "Placement world is unavailable");
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");
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);
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)
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");
1244 std::vector<building::PlacedBuilding>
members;
1248 buildingCandidate->world_, parsedCurve.value().group,
members);
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");
1254 for (
int instanceId : parsedCurve.value().group.instanceIds) {
1255 if (!buildingCandidate->world_->hasBuilding(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");
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");
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");
1277 id_, areaSet ?
"building.instance.set.v4" :
"building.instance.delete.v4",
1278 areaSet ?
"building.instance.delete.v4" :
"building.instance.set.v4", item,
1280 auto applied = buildingCandidate->applyDomainOperation(single);
1282 return eve::editing::failed<void>(applied.code(),
RuleId(
"editor.building.area-atomic-rejected"),
1283 "Building area operation was rejected without mutation");
1287 auto parsed = parseInstance(
operation.payload);
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");
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") {
1310 return eve::editing::failed<void>(
1311 EditorStatus::Rejected,
RuleId(
"editor.building." + (reason.empty() ?
"restore-failed" : reason)),
1312 "PlacementSystem rejected exact instance restore: " + reason);
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");
1323 return eve::editing::failed<void>(EditorStatus::Rejected,
1324 RuleId(
"editor.building.placement-domain-mismatch"),
1325 "Building set cannot change placement domain");
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);
1354 return eve::editing::failed<void>(EditorStatus::Rejected,
RuleId(
"editor.building.replace-failed"),
1355 "PlacementSystem rejected building replacement");
1374 return eve::editing::failed<void>(EditorStatus::Rejected,
RuleId(
"editor.building.move-failed"),
1375 "PlacementSystem rejected building movement");
1400 return eve::editing::failed<void>(EditorStatus::Unsupported,
RuleId(
"editor.building.operation-unsupported"),
1401 "Building placement operation is unsupported: " +
operation.type);
1405 return eve::editing::applied<void>();
1409 if (!world_)
return {};
1410 return std::unique_ptr<IDomainOperationTarget>(
1415 std::unique_ptr<IDomainOperationTarget> candidate) {
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_);
1424 return eve::editing::applied<void>();
1439 if (!
group.ok())
continue;
1440 std::vector<BuildingInstanceSnapshot>
members;
1441 for (
int instanceId :
group.value().instanceIds) {
1445 curveGroups.push_back(edgeCurveValue(
group.value(),
members));
1448 {
"worldId", world_ ? world_->
getId() : std::string{}},
1450 {
"edgeCurveGroups", std::move(curveGroups)}};
1455 std::vector<BuildingEdgeCurvePoint> controlPoints,
int subdivisions,
1456 std::string surfaceName)
1458 buildingId_(
std::move(buildingId)),
1459 memberInstanceId_(memberInstanceId),
1460 controls_(
std::move(controlPoints)),
1461 subdivisions_(subdivisions),
1462 surfaceName_(
std::move(surfaceName)) {}
1466 return eve::editing::failed<BuildingEdgeCurveDragPreview>(EditorStatus::Rejected,
1467 RuleId(
"editor.building.curve-drag-target-required"),
1468 "Curve drag target is unavailable");
1472 buildingId_, controls_, subdivisions_, memberInstanceId_, surfaceName_);
1474 buildingId_, controls_, subdivisions_, 0, memberInstanceId_, surfaceName_);
1477 return eve::editing::applied<BuildingEdgeCurveDragPreview>(std::move(result));
1481 double rayOriginX,
double rayOriginY,
double rayOriginZ,
double rayDirectionX,
1482 double rayDirectionY,
double rayDirectionZ) {
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};
1500 buildingId_, controls_, subdivisions_, 0, memberInstanceId_, surfaceName_);
1501 double closest = std::numeric_limits<double>::infinity();
1503 if (primitive.kind !=
"sphere" || primitive.id.rfind(
"curve.control.", 0) != 0)
1505 const std::array<double, 3>
offset{
origin[0] - primitive.position[0],
1506 origin[1] - primitive.position[1],
1507 origin[2] - primitive.position[2]};
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;
1519 activeControlPointIndex_ = std::stoi(primitive.id.substr(14));
1520 dragPlanePoint_ = primitive.position;
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");
1527 baseRevision_ = target_->
revision();
1529 lastPreviewValid_ = gizmo.
status == EditorStatus::Applied;
1530 return previewCurrent();
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_) {
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");
1545 const double length = std::sqrt(rayDirectionX * rayDirectionX +
1546 rayDirectionY * rayDirectionY +
1547 rayDirectionZ * rayDirectionZ);
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};
1555 const double denominator =
direction[0] * dragPlaneNormal_[0] +
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]) /
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");
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;
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_) {
1589 return eve::editing::failed<DomainOperation>(EditorStatus::Conflict,
RuleId(
"editor.building.curve-drag-stale"),
1590 "Curve world changed before drag commit");
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");
1605 activeControlPointIndex_ = -1;
1606 lastPreviewValid_ =
false;
ActionParameterOperation operation
building::EdgeCurveGroup group
std::vector< BuildingInstanceSnapshot > instances
std::map< std::string, Var > values
std::array< float, 4 > rotation
std::map< std::string, PropertyRule > properties
std::array< std::uint64_t, kPixelChunkSize *kPixelChunkSize > updated
std::array< PixelCell, kPixelChunkSize *kPixelChunkSize > cells
std::vector< Point > vertices
RoadLaneDirection direction
float(ui::Theme::* member)[4]
std::map< std::string, std::span< const std::uint8_t > > members
const UnitySourceAsset & source
Move-only operation result carrying either a value or Status.
UUID-backed identifier adapter for legacy textual boundaries.
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::Value EditorValue
editing::DomainOperation DomainOperation
editing::TargetId TargetId
editing::DiagnosticSeverity DiagnosticSeverity
editing::DomainOperation DomainOperation
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).
DiagnosticCode
Stable machine-readable diagnostic codes.
Severity
Stable severity of a diagnostic, independent of its display text.
detail::StrongUint64< detail::RevisionTag > Revision
Monotonic content/state revision used for optimistic-concurrency checks.
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.
float surfaceMaxSlopeDegrees
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.
std::vector< EditorDiagnostic > diagnostics
editing::GizmoSnapshot gizmo
BuildingEdgeCurvePreview curve
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
double normalizedRotation
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