载入中...
搜索中...
未找到
PlacementSystem.cpp
浏览该文件的文档.
2
6#include "building/Ghost.h"
8#include "common/Diagnostic.h"
10
11#include <algorithm>
12#include <cmath>
13#include <limits>
14#include <unordered_set>
15
16namespace eve::building {
17
18namespace {
19
20float wrapDeg(float deg) {
21 deg = std::fmod(deg, 360.f);
22 if (deg < 0.f) deg += 360.f;
23 return deg;
24}
25
26int cardinalQuarter(float rotationDeg) {
27 const float d = wrapDeg(rotationDeg);
28 int q = int(std::lround(d / 90.f)) % 4;
29 if (q < 0) q += 4;
30 return q;
31}
32
33float length3(float x, float y, float z) { return std::sqrt(x * x + y * y + z * z); }
34
35struct FreeFootprint {
36 struct Point {
37 float x = 0.f;
38 float y = 0.f;
39 };
40 float x = 0.f;
41 float y = 0.f;
42 float radius = 0.f;
43 float halfWidth = 0.f;
44 float halfHeight = 0.f;
45 float rotationRadians = 0.f;
46 std::vector<Point> vertices;
47
48 bool isPolygon() const { return vertices.size() >= 3; }
49};
50
51FreeFootprint definitionFreeFootprint(const BuildingDefinition &definition,
52 const PlacementWorld &world, float x, float y,
53 float rotationDeg) {
54 FreeFootprint result;
55 result.x = x;
56 result.y = y;
57 result.radius = definition.freeRadiusCells *
58 std::min(world.getGrid().cellW, world.getGrid().cellH);
59 if (!definition.freeFootprintVertices.empty()) {
60 for (size_t i = 0; i < definition.freeFootprintVertices.size(); i += 2) {
61 result.vertices.push_back({definition.freeFootprintVertices[i] * world.getGrid().cellW,
62 definition.freeFootprintVertices[i + 1] *
63 world.getGrid().cellH});
64 result.halfWidth = std::max(result.halfWidth, std::fabs(result.vertices.back().x));
65 result.halfHeight = std::max(result.halfHeight, std::fabs(result.vertices.back().y));
66 }
67 } else if (definition.freeFootprintWidthCells > 0.f &&
68 definition.freeFootprintHeightCells > 0.f) {
69 result.halfWidth = definition.freeFootprintWidthCells * world.getGrid().cellW * 0.5f;
70 result.halfHeight = definition.freeFootprintHeightCells * world.getGrid().cellH * 0.5f;
71 result.vertices = {{-result.halfWidth, -result.halfHeight},
72 {result.halfWidth, -result.halfHeight},
73 {result.halfWidth, result.halfHeight},
74 {-result.halfWidth, result.halfHeight}};
75 }
76 result.rotationRadians = wrapDeg(rotationDeg) * 3.14159265358979323846f / 180.f;
77 return result;
78}
79
80void applyDefinitionFreeFootprint(PlacedBuilding &placed, const BuildingDefinition &definition,
81 const PlacementWorld &world) {
82 const FreeFootprint footprint =
83 definitionFreeFootprint(definition, world, placed.worldX, placed.worldY, 0.f);
84 placed.freeRadius = footprint.radius;
85 placed.freeHalfWidth = footprint.halfWidth;
86 placed.freeHalfHeight = footprint.halfHeight;
87 placed.freeFootprintVertices.clear();
88 if (!definition.freeFootprintVertices.empty()) {
89 placed.freeFootprintVertices.reserve(footprint.vertices.size() * 2);
90 for (const auto &point : footprint.vertices) {
91 placed.freeFootprintVertices.push_back(point.x);
92 placed.freeFootprintVertices.push_back(point.y);
93 }
94 }
95}
96
97FreeFootprint placedFreeFootprint(const PlacedBuilding &placed) {
98 FreeFootprint result;
99 result.x = placed.worldX;
100 result.y = placed.worldY;
101 result.radius = placed.freeRadius;
102 result.halfWidth = placed.freeHalfWidth;
103 result.halfHeight = placed.freeHalfHeight;
104 result.rotationRadians = wrapDeg(placed.rotationDeg) * 3.14159265358979323846f / 180.f;
105 for (size_t i = 0; i + 1 < placed.freeFootprintVertices.size(); i += 2)
106 result.vertices.push_back(
107 {placed.freeFootprintVertices[i], placed.freeFootprintVertices[i + 1]});
108 if (result.vertices.empty() && result.halfWidth > 0.f && result.halfHeight > 0.f)
109 result.vertices = {{-result.halfWidth, -result.halfHeight},
110 {result.halfWidth, -result.halfHeight},
111 {result.halfWidth, result.halfHeight},
112 {-result.halfWidth, result.halfHeight}};
113 return result;
114}
115
116std::vector<FreeFootprint::Point> worldVertices(const FreeFootprint &footprint) {
117 std::vector<FreeFootprint::Point> result;
118 result.reserve(footprint.vertices.size());
119 const float c = std::cos(footprint.rotationRadians);
120 const float s = std::sin(footprint.rotationRadians);
121 for (const auto &point : footprint.vertices)
122 result.push_back({footprint.x + point.x * c - point.y * s,
123 footprint.y + point.x * s + point.y * c});
124 return result;
125}
126
127bool pointInPolygon(const FreeFootprint &polygon, float x, float y) {
128 const auto vertices = worldVertices(polygon);
129 float winding = 0.f;
130 for (size_t i = 0; i < vertices.size(); ++i) {
131 const auto &a = vertices[i];
132 const auto &b = vertices[(i + 1) % vertices.size()];
133 const float cross = (b.x - a.x) * (y - a.y) - (b.y - a.y) * (x - a.x);
134 if (std::fabs(cross) <= 1e-5f) continue;
135 if (winding == 0.f)
136 winding = cross;
137 else if (cross * winding < 0.f)
138 return false;
139 }
140 return true;
141}
142
143bool freeFootprintsOverlap(const FreeFootprint &a, const FreeFootprint &b) {
144 const float dx = b.x - a.x;
145 const float dy = b.y - a.y;
146 if (!a.isPolygon() && !b.isPolygon()) return std::hypot(dx, dy) < a.radius + b.radius;
147
148 auto circlePolygonOverlap = [](const FreeFootprint &circle, const FreeFootprint &polygon) {
149 if (pointInPolygon(polygon, circle.x, circle.y)) return true;
150 const auto vertices = worldVertices(polygon);
151 const float radiusSquared = circle.radius * circle.radius;
152 for (size_t i = 0; i < vertices.size(); ++i) {
153 const auto &a = vertices[i];
154 const auto &b = vertices[(i + 1) % vertices.size()];
155 const float edgeX = b.x - a.x;
156 const float edgeY = b.y - a.y;
157 const float lengthSquared = edgeX * edgeX + edgeY * edgeY;
158 const float t = lengthSquared > 0.f
159 ? std::clamp(((circle.x - a.x) * edgeX +
160 (circle.y - a.y) * edgeY) /
161 lengthSquared,
162 0.f, 1.f)
163 : 0.f;
164 const float closestX = a.x + edgeX * t;
165 const float closestY = a.y + edgeY * t;
166 const float distanceX = circle.x - closestX;
167 const float distanceY = circle.y - closestY;
168 if (distanceX * distanceX + distanceY * distanceY < radiusSquared) return true;
169 }
170 return false;
171 };
172 if (!a.isPolygon()) return circlePolygonOverlap(a, b);
173 if (!b.isPolygon()) return circlePolygonOverlap(b, a);
174
175 const auto aVertices = worldVertices(a);
176 const auto bVertices = worldVertices(b);
177 for (const auto *vertices : {&aVertices, &bVertices}) {
178 for (size_t i = 0; i < vertices->size(); ++i) {
179 const auto &p = vertices->at(i);
180 const auto &q = vertices->at((i + 1) % vertices->size());
181 const float axisX = -(q.y - p.y);
182 const float axisY = q.x - p.x;
183 float aMin = std::numeric_limits<float>::max();
184 float aMax = std::numeric_limits<float>::lowest();
185 float bMin = std::numeric_limits<float>::max();
186 float bMax = std::numeric_limits<float>::lowest();
187 for (const auto &vertex : aVertices) {
188 const float projection = vertex.x * axisX + vertex.y * axisY;
189 aMin = std::min(aMin, projection);
190 aMax = std::max(aMax, projection);
191 }
192 for (const auto &vertex : bVertices) {
193 const float projection = vertex.x * axisX + vertex.y * axisY;
194 bMin = std::min(bMin, projection);
195 bMax = std::max(bMax, projection);
196 }
197 if (aMax <= bMin || bMax <= aMin) return false;
198 }
199 }
200 return true;
201}
202
203bool freeFootprintContainsInternal(const FreeFootprint &footprint, float x, float y) {
204 const float dx = x - footprint.x;
205 const float dy = y - footprint.y;
206 if (!footprint.isPolygon()) return std::hypot(dx, dy) <= footprint.radius;
207 return pointInPolygon(footprint, x, y);
208}
209
210void cross3(float ax, float ay, float az, float bx, float by, float bz, float &x, float &y,
211 float &z) {
212 x = ay * bz - az * by;
213 y = az * bx - ax * bz;
214 z = ax * by - ay * bx;
215}
216
217bool normalizeSurfaceFrame(PlacementSystem::PlacementHit &hit) {
218 if (!std::isfinite(hit.worldX) || !std::isfinite(hit.worldY) ||
219 !std::isfinite(hit.worldZ) || !std::isfinite(hit.normalX) ||
220 !std::isfinite(hit.normalY) || !std::isfinite(hit.normalZ)) {
221 return false;
222 }
223 const float normalLength = length3(hit.normalX, hit.normalY, hit.normalZ);
224 if (normalLength <= 1e-5f) return false;
225 hit.normalX /= normalLength;
226 hit.normalY /= normalLength;
227 hit.normalZ /= normalLength;
228
229 const float tangentDot = hit.tangentX * hit.normalX + hit.tangentY * hit.normalY +
230 hit.tangentZ * hit.normalZ;
231 hit.tangentX -= tangentDot * hit.normalX;
232 hit.tangentY -= tangentDot * hit.normalY;
233 hit.tangentZ -= tangentDot * hit.normalZ;
234 float tangentLength = length3(hit.tangentX, hit.tangentY, hit.tangentZ);
235 if (tangentLength <= 1e-5f) {
236 if (std::fabs(hit.normalY) < 0.9f) {
237 hit.tangentX = 0.f;
238 hit.tangentY = 1.f;
239 hit.tangentZ = 0.f;
240 } else {
241 hit.tangentX = 1.f;
242 hit.tangentY = 0.f;
243 hit.tangentZ = 0.f;
244 }
245 const float fallbackDot = hit.tangentX * hit.normalX + hit.tangentY * hit.normalY +
246 hit.tangentZ * hit.normalZ;
247 hit.tangentX -= fallbackDot * hit.normalX;
248 hit.tangentY -= fallbackDot * hit.normalY;
249 hit.tangentZ -= fallbackDot * hit.normalZ;
250 tangentLength = length3(hit.tangentX, hit.tangentY, hit.tangentZ);
251 }
252 hit.tangentX /= tangentLength;
253 hit.tangentY /= tangentLength;
254 hit.tangentZ /= tangentLength;
255 cross3(hit.tangentX, hit.tangentY, hit.tangentZ, hit.normalX, hit.normalY,
256 hit.normalZ, hit.bitangentX, hit.bitangentY, hit.bitangentZ);
257 return true;
258}
259
260} // namespace
261
263 static PlacementSystem instance;
264 return instance;
265}
266
267std::unordered_map<std::string, PlacementSystem::ValidateFn> &PlacementSystem::validateRules() {
268 return inst().validateRules_;
269}
270
271std::unordered_map<std::string, PlacementSystem::SnapFn> &PlacementSystem::snapRules() {
272 return inst().snapRules_;
273}
274
275std::unordered_map<std::string, PlacementSystem::ChangeHook> &PlacementSystem::changeHooks() {
276 return inst().changeHooks_;
277}
278
279std::unordered_map<std::string, PlacementSystem::SurfaceFn> &PlacementSystem::surfaces() {
280 return inst().surfaces_;
281}
282
283std::unordered_map<std::string, PlacementSystem::SurfaceProviderFn> &
284PlacementSystem::surfaceProviders() {
285 return inst().surfaceProviders_;
286}
287
288std::vector<BuildingChangeEvent> &PlacementSystem::eventQueue() {
289 return inst().eventQueue_;
290}
291
292int &PlacementSystem::instanceCounter() {
293 return inst().instanceCounter_;
294}
295
296bool &PlacementSystem::builtinsReady() {
297 return inst().builtinsReady_;
298}
299
301 if (name.empty() || !fn) return;
302 validateRules()[name] = std::move(fn);
303}
304
306 validateRules().erase(name);
307}
308
309bool PlacementSystem::hasValidateRule(const std::string &name) {
311 return validateRules().count(name) > 0;
312}
313
315 if (name.empty() || !fn) return;
316 snapRules()[name] = std::move(fn);
317}
318
319void PlacementSystem::unregisterSnapRule(const std::string &name) { snapRules().erase(name); }
320
321bool PlacementSystem::hasSnapRule(const std::string &name) {
323 return snapRules().count(name) > 0;
324}
325
327 if (name.empty() || !fn) return;
328 changeHooks()[name] = std::move(fn);
329}
330
331void PlacementSystem::unregisterChangeHook(const std::string &name) { changeHooks().erase(name); }
332
333bool PlacementSystem::hasChangeHook(const std::string &name) {
334 return changeHooks().count(name) > 0;
335}
336
338 if (name.empty() || !fn) return;
339 surfaces()[name] = fn;
341 name, [name, fn = std::move(fn)](const PlacementWorld &world, float x, float y) {
343 if (!fn(world, x, y, &hit)) {
345 eve::DiagnosticCode::NotFound, "placement surface did not produce a hit",
346 name, {}, "building.surface"));
347 }
348 return eve::Result<PlacementHit>::success(std::move(hit));
349 });
350}
351
353 if (name.empty() || !fn) return;
354 surfaceProviders()[name] = std::move(fn);
355}
356
358 const std::string &name, std::shared_ptr<const HeightfieldSurface> surface) {
359 if (name.empty() || !surface) {
362 "heightfield registration requires a name and an owning surface", name, {},
363 "building.heightfield-surface"));
364 }
366 name, [surface = std::move(surface)](const PlacementWorld &world, float x, float y) {
367 return surface->sample(world, x, y);
368 });
370}
371
373 const std::string &name, std::shared_ptr<const StaticMeshSurface> surface) {
374 if (name.empty() || !surface) {
377 "static mesh registration requires a name and an owning surface", name, {},
378 "building.static-mesh-surface"));
379 }
381 name, [surface = std::move(surface)](const PlacementWorld &world, float x, float y) {
382 return surface->sample(world, x, y);
383 });
385}
386
387void PlacementSystem::unregisterSurface(const std::string &name) {
388 surfaces().erase(name);
389 surfaceProviders().erase(name);
390}
391
392bool PlacementSystem::hasSurface(const std::string &name) {
394 return surfaceProviders().count(name) > 0;
395}
396
397bool PlacementSystem::surfaceHit(const PlacementWorld &world, const std::string &name, float x,
398 float y, PlacementHit *hit) {
399 if (!hit) return false;
400 auto result = sampleSurface(world, name, x, y);
401 if (!result.ok()) return false;
402 *hit = std::move(result).takeValue();
403 return true;
404}
405
407PlacementSystem::sampleSurface(const PlacementWorld &world, const std::string &name, float x,
408 float y) {
410 const std::string resolvedName = name.empty() ? "plane" : name;
411 auto it = surfaceProviders().find(resolvedName);
412 if (it == surfaceProviders().end() || !it->second) {
414 eve::DiagnosticCode::NotFound, "placement surface provider is not registered",
415 resolvedName, {}, "building.surface"));
416 }
417 auto result = it->second(world, x, y);
418 if (!result.ok()) return eve::Result<PlacementHit>::failure(result.status());
419 PlacementHit hit = std::move(result).takeValue();
420 if (!normalizeSurfaceFrame(hit)) {
423 "placement surface returned a non-finite position or zero normal", resolvedName,
424 {}, "building.surface"));
425 }
426 if (hit.surfaceId.empty()) hit.surfaceId = resolvedName;
427 return eve::Result<PlacementHit>::success(std::move(hit));
428}
429
431PlacementSystem::sampleSurfacePatch(const PlacementWorld &world, const std::string &buildingId,
432 const std::string &surfaceName, float x, float y,
433 float rotationDegrees) {
434 const BuildingDefinition *def = BuildingRegistry::find(buildingId);
435 if (!def) {
437 eve::DiagnosticCode::NotFound, "building definition is not registered", buildingId,
438 {}, "building.surface-patch"));
439 }
440
441 const SnapResult snapped = snap(world, buildingId, x, y);
442 auto anchorResult = sampleSurface(world, surfaceName, snapped.worldX, snapped.worldY);
443 if (!anchorResult.ok()) return eve::Result<SurfacePatch>::failure(anchorResult.status());
444
445 SurfacePatch patch;
446 patch.anchor = std::move(anchorResult).takeValue();
447 float originX = 0.f;
448 float originY = 0.f;
449 world.cellToWorldPlane(snapped.cellX, snapped.cellY, originX, originY);
450
451 float minHeight = std::numeric_limits<float>::max();
452 float maxHeight = std::numeric_limits<float>::lowest();
453 const auto slopeDegrees = [&](const PlacementHit &hit) {
454 const float upDot = world.getGrid().plane == grid::GridPlane::XZ ? hit.normalY
455 : hit.normalZ;
456 return std::acos(std::clamp(upDot, -1.f, 1.f)) * 180.f / 3.14159265358979323846f;
457 };
458 patch.maxSlopeDegrees = 0.f;
459 bool failed = false;
460 eve::Status failureStatus;
461 const auto appendSample = [&](float sampleX, float sampleY, int cellX, int cellY,
462 bool anchor) {
463 if (failed) return false;
465 if (anchor) {
466 hit = patch.anchor;
467 } else {
468 auto sample = sampleSurface(world, surfaceName, sampleX, sampleY);
469 if (!sample.ok()) {
470 failed = true;
471 failureStatus = sample.status();
472 return false;
473 }
474 hit = std::move(sample).takeValue();
475 }
476 if (hit.surfaceId != patch.anchor.surfaceId ||
477 hit.surfaceRevision != patch.anchor.surfaceRevision) {
478 failed = true;
481 "footprint crosses a surface identity or revision boundary", hit.surfaceId,
482 {}, "building.surface-patch"));
483 return false;
484 }
485 const float height =
486 world.getGrid().plane == grid::GridPlane::XZ ? hit.worldY : hit.worldZ;
487 minHeight = std::min(minHeight, height);
488 maxHeight = std::max(maxHeight, height);
489 patch.maxSlopeDegrees = std::max(patch.maxSlopeDegrees, slopeDegrees(hit));
490 patch.samples.push_back({cellX, cellY, std::move(hit)});
491 return true;
492 };
493 if (def->placementKind == "free" &&
494 (!def->freeFootprintVertices.empty() ||
495 (def->freeFootprintWidthCells > 0.f && def->freeFootprintHeightCells > 0.f))) {
496 appendSample(snapped.worldX, snapped.worldY, snapped.cellX, snapped.cellY, true);
497 const FreeFootprint footprint = definitionFreeFootprint(
498 *def, world, snapped.worldX, snapped.worldY,
500 for (const auto &point : worldVertices(footprint)) {
501 int cellX = 0;
502 int cellY = 0;
503 grid::worldToCell(world.getGrid(), point.x, point.y, cellX, cellY,
504 world.getWidth(), world.getHeight());
505 if (!appendSample(point.x, point.y, cellX, cellY, false)) break;
506 }
507 } else {
509 *def, snapped.cellX, snapped.cellY,
510 normalizeRotation(buildingId, rotationDegrees), [&](int cellX, int cellY) {
511 float cellWorldX = 0.f;
512 float cellWorldY = 0.f;
513 world.cellToWorldPlane(cellX, cellY, cellWorldX, cellWorldY);
514 return appendSample(snapped.worldX + cellWorldX - originX,
515 snapped.worldY + cellWorldY - originY, cellX, cellY,
516 cellX == snapped.cellX && cellY == snapped.cellY);
517 });
518 }
519 if (failed) return eve::Result<SurfacePatch>::failure(std::move(failureStatus));
520 if (patch.samples.empty()) {
522 eve::DiagnosticCode::InvalidArgument, "building footprint has no occupied cells",
523 buildingId, {}, "building.surface-patch"));
524 }
525 patch.heightDelta = maxHeight - minHeight;
526 return eve::Result<SurfacePatch>::success(std::move(patch));
527}
528
529std::vector<std::string> PlacementSystem::surfaceNames() {
531 std::vector<std::string> names;
532 names.reserve(surfaceProviders().size());
533 for (const auto &kv : surfaceProviders()) names.push_back(kv.first);
534 return names;
535}
536
537void PlacementSystem::setPlaneSurfaceHeight(float h) { planeSurfaceHeight() = h; }
538
539float PlacementSystem::getPlaneSurfaceHeight() { return planeSurfaceHeight(); }
540
541float &PlacementSystem::planeSurfaceHeight() {
542 return inst().planeSurfaceHeight_;
543}
544
546 if (builtinsReady()) return;
547 builtinsReady() = true;
548
549 registerSnapRule("grid", [](const PlacementWorld &world, float worldX, float worldY) {
551 int cx = 0, cy = 0;
552 grid::worldToCell(world.getGrid(), worldX, worldY, cx, cy, world.getWidth(),
553 world.getHeight());
554 r.cellX = cx;
555 r.cellY = cy;
556 world.cellToWorldPlane(cx, cy, r.worldX, r.worldY);
557 return r;
558 });
559
560 registerSnapRule("cell", [](const PlacementWorld &world, float worldX, float worldY) {
562 r.cellX = int(std::floor(worldX + 0.5f));
563 r.cellY = int(std::floor(worldY + 0.5f));
564 r.worldX = float(r.cellX);
565 r.worldY = float(r.cellY);
566 (void)world;
567 return r;
568 });
569
570 registerSnapRule("free", [](const PlacementWorld &world, float worldX, float worldY) {
572 r.worldX = worldX;
573 r.worldY = worldY;
574 r.cellX = world.worldToCellX(worldX);
575 r.cellY = world.worldToCellY(worldY);
576 return r;
577 });
578
579 // default: bounds + occupancy + terrain + adjacency
580 registerValidateRule("default", [](const PlacementWorld &world, const PlacementQuery &q,
581 std::string *reason) {
582 const BuildingDefinition *def = BuildingRegistry::find(q.buildingId);
583 if (!def) {
584 if (reason) *reason = "unknown_building";
585 return false;
586 }
587 if (!checkBoundsAndOccupancy(world, *def, q, true, reason)) return false;
588 if (!checkTerrain(world, *def, q, reason)) return false;
589 if (!checkAdjacency(world, *def, q, reason)) return false;
590 if (!checkSurfacePatch(*def, q, reason)) return false;
591 if (!checkStructuralSupport(world, *def, q, reason)) return false;
592 return true;
593 });
594
595 registerValidateRule("boundsOnly", [](const PlacementWorld &world, const PlacementQuery &q,
596 std::string *reason) {
597 const BuildingDefinition *def = BuildingRegistry::find(q.buildingId);
598 if (!def) {
599 if (reason) *reason = "unknown_building";
600 return false;
601 }
602 return checkBoundsAndOccupancy(world, *def, q, false, reason);
603 });
604
605 registerValidateRule("overlapOk", [](const PlacementWorld &world, const PlacementQuery &q,
606 std::string *reason) {
607 const BuildingDefinition *def = BuildingRegistry::find(q.buildingId);
608 if (!def) {
609 if (reason) *reason = "unknown_building";
610 return false;
611 }
612 if (!checkBoundsAndOccupancy(world, *def, q, false, reason)) return false;
613 if (!checkTerrain(world, *def, q, reason)) return false;
614 if (!checkAdjacency(world, *def, q, reason)) return false;
615 if (!checkSurfacePatch(*def, q, reason)) return false;
616 if (!checkStructuralSupport(world, *def, q, reason)) return false;
617 return true;
618 });
619
620 registerSurfaceProvider("plane", [](const PlacementWorld &world, float x, float y) {
622 const float h = planeSurfaceHeight();
623 if (world.getGrid().plane == grid::GridPlane::XZ) {
624 hit.worldX = x;
625 hit.worldY = h;
626 hit.worldZ = y;
627 } else {
628 hit.worldX = x;
629 hit.worldY = y;
630 hit.worldZ = h;
631 hit.normalX = 0.f;
632 hit.normalY = 0.f;
633 hit.normalZ = 1.f;
634 }
635 hit.surfaceId = "plane";
636 return eve::Result<PlacementHit>::success(std::move(hit));
637 });
638}
639
640int PlacementSystem::nextInstanceId() { return ++instanceCounter(); }
641
642void PlacementSystem::pushEvent(BuildingChangeEvent ev) { emit(std::move(ev)); }
643
644void PlacementSystem::pollEvents(std::vector<BuildingChangeEvent> &out) {
645 out = eventQueue();
646 eventQueue().clear();
647}
648
649void PlacementSystem::clearEvents() { eventQueue().clear(); }
650
651const std::vector<BuildingChangeEvent> &PlacementSystem::events() { return eventQueue(); }
652
653void PlacementSystem::emit(BuildingChangeEvent ev) {
654 eventQueue().push_back(ev);
655 for (auto &kv : changeHooks()) {
656 if (kv.second) kv.second(ev);
657 }
658}
659
661 float worldX, float worldY) {
663 auto &rules = snapRules();
664 auto it = rules.find(mode.empty() ? "grid" : mode);
665 if (it == rules.end() || !it->second) {
666 it = rules.find("grid");
667 }
668 if (it != rules.end() && it->second) return it->second(world, worldX, worldY);
670 r.cellX = world.worldToCellX(worldX);
671 r.cellY = world.worldToCellY(worldY);
672 r.worldX = world.cellToWorldX(r.cellX);
673 r.worldY = world.cellToWorldY(r.cellY);
674 return r;
675}
676
677SnapResult PlacementSystem::snap(const PlacementWorld &world, const std::string &buildingId,
678 float worldX, float worldY) {
680 std::string mode = world.getSnapMode();
681 if (const BuildingDefinition *def = BuildingRegistry::find(buildingId)) {
682 if (!def->snapMode.empty()) mode = def->snapMode;
683 }
684 return snapWithMode(world, mode, worldX, worldY);
685}
686
687SnapResult PlacementSystem::snap3D(const PlacementWorld &world, const std::string &buildingId,
688 float worldX, float worldY, float worldZ) {
689 float px = worldX;
690 float py = worldY;
691 float elev = worldZ;
692 if (world.getGrid().plane == grid::GridPlane::XZ) {
693 // 世界坐标 (x, y=高度, z):网格平面第二轴取 z,垂直高度取 y。
694 py = worldZ;
695 elev = worldY;
696 }
697 SnapResult s = snap(world, buildingId, px, py);
698 s.elevation = elev;
699 return s;
700}
701
702float PlacementSystem::normalizeRotation(const std::string &buildingId, float rotationDeg) {
703 const BuildingDefinition *def = BuildingRegistry::find(buildingId);
704 const std::string mode = def ? def->rotationMode : "cardinal";
705 if (mode == "none") return 0.f;
706 if (mode == "free") return wrapDeg(rotationDeg);
707 if (mode == "hex") {
708 const float d = wrapDeg(rotationDeg);
709 const int steps = int(std::lround(d / 60.f)) % 6;
710 return float((steps < 0 ? steps + 6 : steps) * 60);
711 }
712 // cardinal(默认)
713 return float(cardinalQuarter(rotationDeg) * 90);
714}
715
716void PlacementSystem::effectiveFootprint(const BuildingDefinition &def, float rotationDeg, int *outW,
717 int *outH) {
718 int w = def.footprintW;
719 int h = def.footprintH;
720 if (def.rotationMode == "hex") {
721 const int steps = int(std::lround(wrapDeg(rotationDeg) / 60.f)) % 6;
723 w, h);
724 } else if (def.rotationMode == "cardinal" || def.rotationMode.empty()) {
725 const int q = cardinalQuarter(rotationDeg);
726 if (q == 1 || q == 3) std::swap(w, h);
727 }
728 if (outW) *outW = w;
729 if (outH) *outH = h;
730}
731
733 int originCellY, float rotationDeg,
734 const std::function<bool(int cx, int cy)> &fn) {
735 if (!fn) return false;
736 const bool hex = def.rotationMode == "hex";
737 const int steps =
738 hex ? int(std::lround(wrapDeg(rotationDeg) / 60.f)) % 6 : cardinalQuarter(rotationDeg);
739 bool ok = true;
741 [&](int lx, int ly) {
742 if (!fn(originCellX + lx, originCellY + ly)) ok = false;
743 });
744 return ok;
745}
746
747bool PlacementSystem::checkBoundsAndOccupancy(const PlacementWorld &world,
748 const BuildingDefinition &def,
749 const PlacementQuery &q, bool checkOccupancy,
750 std::string *reason) {
751 bool ok = true;
752 foreachFootprintCell(def, q.cellX, q.cellY, q.rotationDeg, [&](int cx, int cy) {
753 if (!world.inBounds(cx, cy)) {
754 if (reason) *reason = "out_of_bounds";
755 ok = false;
756 return false;
757 }
758 if (checkOccupancy) {
759 const int occ = world.getOccupantAtLevel(def.channel, cx, cy, q.level);
760 if (occ != 0 && occ != q.excludeInstanceId) {
761 if (reason) *reason = "occupied";
762 ok = false;
763 return false;
764 }
765 }
766 return true;
767 });
768 return ok;
769}
770
771bool PlacementSystem::checkTerrain(const PlacementWorld &world, const BuildingDefinition &def,
772 const PlacementQuery &q, std::string *reason) {
773 if (def.requireTerrain.empty() && def.forbidTerrain.empty()) return true;
774 bool ok = true;
775 foreachFootprintCell(def, q.cellX, q.cellY, q.rotationDeg, [&](int cx, int cy) {
776 const int sem = world.getTerrain(cx, cy);
777 if (!def.forbidTerrain.empty()) {
778 if (std::find(def.forbidTerrain.begin(), def.forbidTerrain.end(), sem) !=
779 def.forbidTerrain.end()) {
780 if (reason) *reason = "terrain_forbidden";
781 ok = false;
782 return false;
783 }
784 }
785 if (!def.requireTerrain.empty()) {
786 if (std::find(def.requireTerrain.begin(), def.requireTerrain.end(), sem) ==
787 def.requireTerrain.end()) {
788 if (reason) *reason = "terrain_mismatch";
789 ok = false;
790 return false;
791 }
792 }
793 return true;
794 });
795 return ok;
796}
797
798bool PlacementSystem::checkAdjacency(const PlacementWorld &world, const BuildingDefinition &def,
799 const PlacementQuery &q, std::string *reason) {
800 const bool needTag = !def.requireAdjacentTag.empty();
801 const bool needTerrain = def.requireAdjacentTerrain >= 0;
802 if (!needTag && !needTerrain) return true;
803
804 // Collect footprint cells + expand by 1 (4-neigh).
805 std::vector<std::pair<int, int>> cells;
806 foreachFootprintCell(def, q.cellX, q.cellY, q.rotationDeg, [&](int cx, int cy) {
807 cells.emplace_back(cx, cy);
808 return true;
809 });
810
811 auto isFootprint = [&](int x, int y) {
812 return std::find(cells.begin(), cells.end(), std::make_pair(x, y)) != cells.end();
813 };
814
815 bool tagOk = !needTag;
816 bool terrainOk = !needTerrain;
817 static const int dx[4] = {1, -1, 0, 0};
818 static const int dy[4] = {0, 0, 1, -1};
819
820 for (const auto &c : cells) {
821 for (int i = 0; i < 4; ++i) {
822 const int nx = c.first + dx[i];
823 const int ny = c.second + dy[i];
824 if (isFootprint(nx, ny)) continue;
825 if (!world.inBounds(nx, ny)) continue;
826
827 if (needTag && !tagOk) {
828 const int occ = world.getAnyOccupantAtLevel(nx, ny, q.level);
829 if (occ != 0 && occ != q.excludeInstanceId) {
830 auto it = world.buildings().find(occ);
831 if (it != world.buildings().end()) {
832 const PlacedBuilding &pb = it->second;
833 bool hit = pb.hasTag(def.requireAdjacentTag);
834 if (!hit) {
835 if (const BuildingDefinition *odef =
836 BuildingRegistry::find(pb.buildingId)) {
837 hit = odef->hasTag(def.requireAdjacentTag);
838 }
839 }
840 if (hit) tagOk = true;
841 }
842 }
843 }
844 if (needTerrain && !terrainOk) {
845 if (world.getTerrain(nx, ny) == def.requireAdjacentTerrain) terrainOk = true;
846 }
847 if (tagOk && terrainOk) return true;
848 }
849 }
850
851 if (needTag && !tagOk) {
852 if (reason) *reason = "adjacency_tag";
853 return false;
854 }
855 if (needTerrain && !terrainOk) {
856 if (reason) *reason = "adjacency_terrain";
857 return false;
858 }
859 return true;
860}
861
862bool PlacementSystem::runValidate(const PlacementWorld &world, const BuildingDefinition &def,
863 const PlacementQuery &q, std::string *reason) {
864 ensureBuiltins();
865 std::string rule = def.validateRule.empty() ? world.getValidateRule() : def.validateRule;
866 if (rule.empty()) rule = "default";
867 auto &rules = validateRules();
868 auto it = rules.find(rule);
869 if (it == rules.end() || !it->second) {
870 it = rules.find("default");
871 }
872 if (it == rules.end() || !it->second) {
873 if (reason) *reason = "validate_rejected";
874 return false;
875 }
876 return it->second(world, q, reason);
877}
878
879bool PlacementSystem::canPlaceElev(PlacementWorld *world, const std::string &buildingId, int cellX,
880 int cellY, float elevation, float rotationDeg,
881 int excludeInstanceId, std::string *reason, int level) {
882 const BuildingDefinition *definition = BuildingRegistry::find(buildingId);
883 if (definition && definition->placementKind != "cell") {
884 if (reason) {
885 if (definition->placementKind == "edge")
886 *reason = "edge_requires_edge_address";
887 else if (definition->placementKind == "corner")
888 *reason = "corner_requires_corner_address";
889 else
890 *reason = "free_requires_free_address";
891 }
892 return false;
893 }
895 q.buildingId = buildingId;
896 q.cellX = cellX;
897 q.cellY = cellY;
898 q.level = level == std::numeric_limits<int>::min() && world ? world->getActiveLevel() : level;
899 q.rotationDeg = normalizeRotation(buildingId, rotationDeg);
900 if (world) world->cellToWorldPlane(cellX, cellY, q.worldX, q.worldY);
901 q.elevation = elevation;
902 q.excludeInstanceId = excludeInstanceId;
903 return canPlaceQuery(world, q, reason);
904}
905
906bool PlacementSystem::checkSurfacePatch(const BuildingDefinition &def, const PlacementQuery &q,
907 std::string *reason) {
908 if (q.surfaceSampleCount <= 0) return true;
909 if (q.surfaceMaxSlopeDegrees > def.maxSurfaceSlopeDegrees + 1e-4f) {
910 if (reason) *reason = "surface_slope";
911 return false;
912 }
913 if (def.maxSurfaceHeightDelta >= 0.f &&
914 q.surfaceHeightDelta > def.maxSurfaceHeightDelta + 1e-4f) {
915 if (reason) *reason = "surface_height_delta";
916 return false;
917 }
918 return true;
919}
920
921bool PlacementSystem::checkStructuralSupport(const PlacementWorld &world,
922 const BuildingDefinition &def,
923 const PlacementQuery &q, std::string *reason) {
924 if (def.supportMode == "corner_below") {
925 if (def.placementKind != "corner") {
926 if (reason) *reason = "support_mode_mismatch";
927 return false;
928 }
929 const int instanceId = world.getAnyCornerOccupantAtLevel(q.cellX, q.cellY, q.level - 1);
930 const auto found = world.buildings().find(instanceId);
931 if (instanceId == 0 || found == world.buildings().end()) {
932 if (reason) *reason = "support_missing";
933 return false;
934 }
935 if (def.supportTag.empty()) return true;
936 const BuildingDefinition *support = BuildingRegistry::find(found->second.buildingId);
937 const bool tagged = found->second.hasTag(def.supportTag) ||
938 (support && support->hasTag(def.supportTag));
939 if (!tagged && reason) *reason = "support_missing";
940 return tagged;
941 }
942 if (def.supportMode != "cell_below") return true;
943 bool supported = true;
944 foreachFootprintCell(def, q.cellX, q.cellY, q.rotationDeg, [&](int cellX, int cellY) {
945 const int instanceId = world.getAnyOccupantAtLevel(cellX, cellY, q.level - 1);
946 const auto found = world.buildings().find(instanceId);
947 if (instanceId == 0 || found == world.buildings().end()) {
948 supported = false;
949 } else if (!def.supportTag.empty()) {
950 const PlacedBuilding &placed = found->second;
951 const BuildingDefinition *support = BuildingRegistry::find(placed.buildingId);
952 supported = placed.hasTag(def.supportTag) ||
953 (support && support->hasTag(def.supportTag));
954 }
955 if (!supported && reason) *reason = "support_missing";
956 return supported;
957 });
958 return supported;
959}
960
961std::vector<int> PlacementSystem::collectStructuralSupports(const PlacementWorld &world,
962 const BuildingDefinition &def,
963 const PlacementQuery &q) {
964 std::vector<int> result;
965 if (def.supportMode == "corner_below") {
966 const int support = world.getAnyCornerOccupantAtLevel(q.cellX, q.cellY, q.level - 1);
967 if (support != 0) result.push_back(support);
968 return result;
969 }
970 if (def.supportMode != "cell_below") return result;
971 foreachFootprintCell(def, q.cellX, q.cellY, q.rotationDeg, [&](int cellX, int cellY) {
972 const int support = world.getAnyOccupantAtLevel(cellX, cellY, q.level - 1);
973 if (support != 0 && std::find(result.begin(), result.end(), support) == result.end())
974 result.push_back(support);
975 return true;
976 });
977 return result;
978}
979
980bool PlacementSystem::hasStructuralDependents(const PlacementWorld &world, int instanceId) {
981 for (const auto &[id, placed] : world.buildings_) {
982 (void)id;
983 if (std::find(placed.supportInstanceIds.begin(), placed.supportInstanceIds.end(),
984 instanceId) != placed.supportInstanceIds.end())
985 return true;
986 }
987 return false;
988}
989
990bool PlacementSystem::replacementPreservesStructuralDependents(
991 const PlacementWorld &world, int instanceId, const BuildingDefinition &replacement) {
992 const auto sourceIt = world.buildings_.find(instanceId);
993 if (sourceIt == world.buildings_.end()) return false;
994 const PlacedBuilding &source = sourceIt->second;
995
996 std::unordered_set<uint64_t> replacementCells;
997 if (replacement.placementKind != "edge") {
998 foreachFootprintCell(replacement, source.originCellX, source.originCellY,
999 source.rotationDeg, [&](int x, int y) {
1000 replacementCells.insert((uint64_t(uint32_t(x)) << 32U) |
1001 uint32_t(y));
1002 return true;
1003 });
1004 }
1005
1006 for (const auto &[candidateId, dependent] : world.buildings_) {
1007 (void)candidateId;
1008 if (std::find(dependent.supportInstanceIds.begin(),
1009 dependent.supportInstanceIds.end(), instanceId) ==
1010 dependent.supportInstanceIds.end())
1011 continue;
1012 const BuildingDefinition *dependentDef = BuildingRegistry::find(dependent.buildingId);
1013 if (!dependentDef) return false;
1014 const bool tagMatches = dependentDef->supportTag.empty() ||
1015 replacement.hasTag(dependentDef->supportTag);
1016 if (!tagMatches) return false;
1017 if (dependentDef->supportMode == "corner_below") {
1018 if (replacement.placementKind != "corner" || source.corner != dependent.corner)
1019 return false;
1020 continue;
1021 }
1022 if (dependentDef->supportMode != "cell_below" || replacement.placementKind != "cell")
1023 return false;
1024 bool covered = true;
1025 foreachFootprintCell(*dependentDef, dependent.originCellX, dependent.originCellY,
1026 dependent.rotationDeg, [&](int x, int y) {
1027 if (world.getAnyOccupantAtLevel(x, y, dependent.level - 1) !=
1028 instanceId)
1029 return true;
1030 const uint64_t key = (uint64_t(uint32_t(x)) << 32U) | uint32_t(y);
1031 covered = replacementCells.contains(key);
1032 return covered;
1033 });
1034 if (!covered) return false;
1035 }
1036 return true;
1037}
1038
1039bool PlacementSystem::canPlaceQuery(PlacementWorld *world, const PlacementQuery &q,
1040 std::string *reason) {
1041 ensureBuiltins();
1042 if (!world) {
1043 if (reason) *reason = "no_world";
1044 return false;
1045 }
1046 const BuildingDefinition *def = BuildingRegistry::find(q.buildingId);
1047 if (!def) {
1048 if (reason) *reason = "unknown_building";
1049 return false;
1050 }
1051 return runValidate(*world, *def, q, reason);
1052}
1053
1054bool PlacementSystem::canPlace(PlacementWorld *world, const std::string &buildingId, int cellX,
1055 int cellY, float rotationDeg, int excludeInstanceId,
1056 std::string *reason) {
1057 return canPlaceElev(world, buildingId, cellX, cellY, 0.f, rotationDeg, excludeInstanceId,
1058 reason);
1059}
1060
1061eve::Result<EdgeAddress> PlacementSystem::canonicalEdge(int cellX, int cellY,
1062 const std::string &direction) {
1064 edge.x = cellX;
1065 edge.y = cellY;
1066 if (direction == "north" || direction == "n") {
1067 edge.axis = EdgeAxis::Horizontal;
1068 } else if (direction == "south" || direction == "s") {
1069 edge.axis = EdgeAxis::Horizontal;
1070 ++edge.y;
1071 } else if (direction == "west" || direction == "w") {
1072 edge.axis = EdgeAxis::Vertical;
1073 } else if (direction == "east" || direction == "e") {
1074 edge.axis = EdgeAxis::Vertical;
1075 ++edge.x;
1076 } else {
1078 eve::DiagnosticCode::InvalidArgument, "edge direction must be north/east/south/west",
1079 direction, {}, "building.edge"));
1080 }
1082}
1083
1084bool PlacementSystem::canPlaceEdge(PlacementWorld *world, const std::string &buildingId, int cellX,
1085 int cellY, const std::string &direction, int excludeInstanceId,
1086 std::string *reason, int level) {
1087 if (!world) {
1088 if (reason) *reason = "no_world";
1089 return false;
1090 }
1091 const BuildingDefinition *def = BuildingRegistry::find(buildingId);
1092 if (!def) {
1093 if (reason) *reason = "unknown_building";
1094 return false;
1095 }
1096 if (def->placementKind != "edge") {
1097 if (reason) *reason = "not_edge_building";
1098 return false;
1099 }
1100 auto result = canonicalEdge(cellX, cellY, direction);
1101 if (!result.ok()) {
1102 if (reason) *reason = "invalid_edge_direction";
1103 return false;
1104 }
1105 const EdgeAddress edge = std::move(result).takeValue();
1106 const bool inBounds = edge.axis == EdgeAxis::Horizontal
1107 ? edge.x >= 0 && edge.x < world->width_ && edge.y >= 0 &&
1108 edge.y <= world->height_
1109 : edge.x >= 0 && edge.x <= world->width_ && edge.y >= 0 &&
1110 edge.y < world->height_;
1111 if (!inBounds) {
1112 if (reason) *reason = "edge_out_of_bounds";
1113 return false;
1114 }
1115 const int resolvedLevel =
1116 level == std::numeric_limits<int>::min() ? world->getActiveLevel() : level;
1117 const auto *channels = world->findEdgeChannels(edge.axis, resolvedLevel);
1118 const size_t index = edge.axis == EdgeAxis::Horizontal
1119 ? size_t(edge.y) * size_t(world->width_) + size_t(edge.x)
1120 : size_t(edge.y) * size_t(world->width_ + 1) + size_t(edge.x);
1121 if (!channels) return true;
1122 const auto channelIt = channels->find(def->channel);
1123 const int occupant = channelIt != channels->end() && index < channelIt->second.size()
1124 ? channelIt->second[index]
1125 : 0;
1126 if (occupant != 0 && occupant != excludeInstanceId) {
1127 if (reason) *reason = "edge_occupied";
1128 return false;
1129 }
1130 return true;
1131}
1132
1133int PlacementSystem::placeEdge(PlacementWorld *world, const std::string &buildingId, int cellX,
1134 int cellY, const std::string &direction) {
1135 if (!canPlaceEdge(world, buildingId, cellX, cellY, direction, 0, nullptr)) return 0;
1136 const BuildingDefinition *def = BuildingRegistry::find(buildingId);
1137 auto address = canonicalEdge(cellX, cellY, direction);
1138 if (!def || !address.ok()) return 0;
1139 const EdgeAddress edge = std::move(address).takeValue();
1140
1142 placed.instanceId = nextInstanceId();
1143 placed.buildingId = buildingId;
1144 placed.placementKind = "edge";
1145 placed.edge = edge;
1146 placed.originCellX = edge.x;
1147 placed.originCellY = edge.y;
1148 placed.level = world->getActiveLevel();
1149 placed.channel = def->channel;
1150 placed.tags = def->tags;
1151 float ax = 0.f, ay = 0.f, bx = 0.f, by = 0.f;
1152 world->cellToWorldPlane(edge.x, edge.y, ax, ay);
1153 world->cellToWorldPlane(edge.x + (edge.axis == EdgeAxis::Horizontal ? 1 : 0),
1154 edge.y + (edge.axis == EdgeAxis::Vertical ? 1 : 0), bx, by);
1155 placed.worldX = (ax + bx) * 0.5f;
1156 placed.worldY = (ay + by) * 0.5f;
1157 placed.rotationDeg = edge.axis == EdgeAxis::Horizontal ? 0.f : 90.f;
1158
1159 auto &channels = world->edgeChannels(edge.axis, placed.level);
1160 auto &occupancy = channels[def->channel];
1161 const size_t required = edge.axis == EdgeAxis::Horizontal
1162 ? size_t(world->width_) * size_t(world->height_ + 1)
1163 : size_t(world->width_ + 1) * size_t(world->height_);
1164 if (occupancy.size() != required) occupancy.assign(required, 0);
1165 const size_t index = edge.axis == EdgeAxis::Horizontal
1166 ? size_t(edge.y) * size_t(world->width_) + size_t(edge.x)
1167 : size_t(edge.y) * size_t(world->width_ + 1) + size_t(edge.x);
1168 occupancy[index] = placed.instanceId;
1169 world->buildings()[placed.instanceId] = placed;
1170 world->instanceOrder_.push_back(placed.instanceId);
1171
1172 BuildingChangeEvent event;
1173 event.action = "place";
1174 event.worldId = world->getId();
1175 event.buildingId = buildingId;
1176 event.instanceId = placed.instanceId;
1177 event.cellX = edge.x;
1178 event.cellY = edge.y;
1179 event.rotationDeg = placed.rotationDeg;
1180 event.worldX = placed.worldX;
1181 event.worldY = placed.worldY;
1182 event.channel = placed.channel;
1183 if (world->publishEvents_) emit(std::move(event));
1184 return placed.instanceId;
1185}
1186
1187bool PlacementSystem::canPlaceCorner(PlacementWorld *world, const std::string &buildingId,
1188 int vertexX, int vertexY, int excludeInstanceId,
1189 std::string *reason, int level) {
1190 if (!world) {
1191 if (reason) *reason = "no_world";
1192 return false;
1193 }
1194 const BuildingDefinition *def = BuildingRegistry::find(buildingId);
1195 if (!def) {
1196 if (reason) *reason = "unknown_building";
1197 return false;
1198 }
1199 if (def->placementKind != "corner") {
1200 if (reason) *reason = "not_corner_building";
1201 return false;
1202 }
1203 if (vertexX < 0 || vertexX > world->width_ || vertexY < 0 || vertexY > world->height_) {
1204 if (reason) *reason = "corner_out_of_bounds";
1205 return false;
1206 }
1207 const int resolvedLevel =
1208 level == std::numeric_limits<int>::min() ? world->getActiveLevel() : level;
1209 const int occupant =
1210 world->getCornerOccupantAtLevel(def->channel, vertexX, vertexY, resolvedLevel);
1211 if (occupant != 0 && occupant != excludeInstanceId) {
1212 if (reason) *reason = "corner_occupied";
1213 return false;
1214 }
1215 PlacementQuery query;
1216 query.buildingId = buildingId;
1217 query.cellX = vertexX;
1218 query.cellY = vertexY;
1219 query.level = resolvedLevel;
1220 query.excludeInstanceId = excludeInstanceId;
1221 return checkStructuralSupport(*world, *def, query, reason);
1222}
1223
1225PlacementSystem::placeCornerResult(PlacementWorld *world, const std::string &buildingId,
1226 int vertexX, int vertexY) {
1227 std::string reason;
1228 if (!canPlaceCorner(world, buildingId, vertexX, vertexY, 0, &reason)) {
1230 eve::DiagnosticCode::Conflict, "corner placement rejected without mutation", reason,
1231 {}, "building.corner"));
1232 }
1233 const BuildingDefinition *def = BuildingRegistry::find(buildingId);
1234 if (!def) {
1236 eve::DiagnosticCode::NotFound, "corner definition was not found", buildingId, {},
1237 "building.corner"));
1238 }
1239
1241 placed.instanceId = nextInstanceId();
1242 placed.buildingId = buildingId;
1243 placed.placementKind = "corner";
1244 placed.corner = CornerAddress{vertexX, vertexY};
1245 placed.originCellX = vertexX;
1246 placed.originCellY = vertexY;
1247 placed.level = world->getActiveLevel();
1248 placed.channel = def->channel;
1249 placed.tags = def->tags;
1250 world->cellToWorldPlane(vertexX, vertexY, placed.worldX, placed.worldY);
1251 PlacementQuery supportQuery;
1252 supportQuery.cellX = vertexX;
1253 supportQuery.cellY = vertexY;
1254 supportQuery.level = placed.level;
1255 placed.supportInstanceIds = collectStructuralSupports(*world, *def, supportQuery);
1256
1257 auto &occupancy = world->cornerChannels(placed.level)[def->channel];
1258 const size_t required = size_t(world->width_ + 1) * size_t(world->height_ + 1);
1259 if (occupancy.size() != required) occupancy.assign(required, 0);
1260 occupancy[size_t(vertexY) * size_t(world->width_ + 1) + size_t(vertexX)] =
1261 placed.instanceId;
1262 world->buildings_[placed.instanceId] = placed;
1263 world->instanceOrder_.push_back(placed.instanceId);
1264
1265 BuildingChangeEvent event;
1266 event.action = "place";
1267 event.worldId = world->getId();
1268 event.buildingId = buildingId;
1269 event.instanceId = placed.instanceId;
1270 event.cellX = vertexX;
1271 event.cellY = vertexY;
1272 event.level = placed.level;
1273 event.worldX = placed.worldX;
1274 event.worldY = placed.worldY;
1275 event.channel = placed.channel;
1276 if (world->publishEvents_) emit(std::move(event));
1278}
1279
1280int PlacementSystem::placeCorner(PlacementWorld *world, const std::string &buildingId,
1281 int vertexX, int vertexY) {
1282 auto result = placeCornerResult(world, buildingId, vertexX, vertexY);
1283 return result.ok() ? result.value().instanceId : 0;
1284}
1285
1286bool PlacementSystem::canPlaceFree(PlacementWorld *world, const std::string &buildingId,
1287 float worldX, float worldY, int excludeInstanceId,
1288 std::string *reason, int level, float rotationDeg) {
1289 if (!world) {
1290 if (reason) *reason = "no_world";
1291 return false;
1292 }
1293 const BuildingDefinition *def = BuildingRegistry::find(buildingId);
1294 if (!def) {
1295 if (reason) *reason = "unknown_building";
1296 return false;
1297 }
1298 if (def->placementKind != "free") {
1299 if (reason) *reason = "not_free_building";
1300 return false;
1301 }
1302 if (!std::isfinite(worldX) || !std::isfinite(worldY)) {
1303 if (reason) *reason = "free_invalid_anchor";
1304 return false;
1305 }
1306 int cellX = 0;
1307 int cellY = 0;
1308 grid::worldToCell(*world->grid_, worldX, worldY, cellX, cellY, world->width_,
1309 world->height_);
1310 if (cellX < 0 || cellX >= world->width_ || cellY < 0 || cellY >= world->height_) {
1311 if (reason) *reason = "free_out_of_bounds";
1312 return false;
1313 }
1314 const int resolvedLevel =
1315 level == std::numeric_limits<int>::min() ? world->getActiveLevel() : level;
1316 const FreeFootprint candidate =
1317 definitionFreeFootprint(*def, *world, worldX, worldY,
1318 normalizeRotation(buildingId, rotationDeg));
1319 for (const auto &[id, placed] : world->buildings_) {
1320 if (id == excludeInstanceId || placed.placementKind != "free" ||
1321 placed.level != resolvedLevel || placed.channel != def->channel)
1322 continue;
1323 if (freeFootprintsOverlap(placedFreeFootprint(placed), candidate)) {
1324 if (reason) *reason = "free_overlap";
1325 return false;
1326 }
1327 }
1328 PlacementQuery query;
1329 query.buildingId = buildingId;
1330 query.cellX = cellX;
1331 query.cellY = cellY;
1332 query.worldX = worldX;
1333 query.worldY = worldY;
1334 query.level = resolvedLevel;
1335 query.excludeInstanceId = excludeInstanceId;
1336 return checkStructuralSupport(*world, *def, query, reason);
1337}
1338
1339bool PlacementSystem::containsFreePoint(const PlacedBuilding &placed, float worldX,
1340 float worldY) {
1341 return placed.placementKind == "free" &&
1342 freeFootprintContainsInternal(placedFreeFootprint(placed), worldX, worldY);
1343}
1344
1346PlacementSystem::placeFreeResult(PlacementWorld *world, const std::string &buildingId,
1347 float worldX, float worldY, float elevation,
1348 float rotationDeg) {
1349 std::string reason;
1350 if (!canPlaceFree(world, buildingId, worldX, worldY, 0, &reason,
1351 std::numeric_limits<int>::min(), rotationDeg)) {
1353 eve::DiagnosticCode::Conflict, "free placement rejected without mutation", reason,
1354 {}, "building.free"));
1355 }
1356 const BuildingDefinition *def = BuildingRegistry::find(buildingId);
1358 placed.instanceId = nextInstanceId();
1359 placed.buildingId = buildingId;
1360 placed.placementKind = "free";
1361 placed.level = world->getActiveLevel();
1362 placed.worldX = worldX;
1363 placed.worldY = worldY;
1364 placed.elevation = elevation;
1365 placed.rotationDeg = normalizeRotation(buildingId, rotationDeg);
1366 applyDefinitionFreeFootprint(placed, *def, *world);
1367 grid::worldToCell(*world->grid_, worldX, worldY, placed.originCellX,
1368 placed.originCellY, world->width_, world->height_);
1369 placed.channel = def->channel;
1370 placed.tags = def->tags;
1371 world->buildings_[placed.instanceId] = placed;
1372 world->instanceOrder_.push_back(placed.instanceId);
1373
1374 BuildingChangeEvent event;
1375 event.action = "place";
1376 event.worldId = world->getId();
1377 event.buildingId = buildingId;
1378 event.instanceId = placed.instanceId;
1379 event.cellX = placed.originCellX;
1380 event.cellY = placed.originCellY;
1381 event.level = placed.level;
1382 event.rotationDeg = placed.rotationDeg;
1383 event.worldX = placed.worldX;
1384 event.worldY = placed.worldY;
1385 event.elevation = placed.elevation;
1386 event.channel = placed.channel;
1387 if (world->publishEvents_) emit(std::move(event));
1389}
1390
1392PlacementSystem::placeFreeSurfaceResult(PlacementWorld *world,
1393 const std::string &buildingId,
1394 const SurfacePatch &patch, float rotationDeg) {
1395 if (!world) {
1397 eve::DiagnosticCode::InvalidArgument, "free surface placement requires a world",
1398 {}, {}, "building.free"));
1399 }
1400 const BuildingDefinition *def = BuildingRegistry::find(buildingId);
1401 if (!def || def->placementKind != "free") {
1404 "free surface placement requires a free definition", buildingId, {},
1405 "building.free"));
1406 }
1407 const float planeX = patch.anchor.worldX;
1408 const float planeY = world->getGrid().plane == grid::GridPlane::XZ
1409 ? patch.anchor.worldZ
1410 : patch.anchor.worldY;
1411 const float elevation = world->getGrid().plane == grid::GridPlane::XZ
1412 ? patch.anchor.worldY
1413 : patch.anchor.worldZ;
1414 std::string reason;
1415 if (!canPlaceFree(world, buildingId, planeX, planeY, 0, &reason,
1416 std::numeric_limits<int>::min(), rotationDeg) ||
1418 (def->maxSurfaceHeightDelta >= 0.f &&
1419 patch.heightDelta > def->maxSurfaceHeightDelta)) {
1420 if (reason.empty())
1421 reason = patch.maxSlopeDegrees > def->maxSurfaceSlopeDegrees
1422 ? "surface_slope"
1423 : "surface_height_delta";
1426 "free surface placement rejected without mutation", reason, {},
1427 "building.free"));
1428 }
1429
1431 placed.instanceId = nextInstanceId();
1432 placed.buildingId = buildingId;
1433 placed.placementKind = "free";
1434 placed.level = world->getActiveLevel();
1435 placed.worldX = planeX;
1436 placed.worldY = planeY;
1437 placed.elevation = elevation;
1438 placed.rotationDeg = normalizeRotation(buildingId, rotationDeg);
1439 applyDefinitionFreeFootprint(placed, *def, *world);
1440 grid::worldToCell(*world->grid_, planeX, planeY, placed.originCellX,
1441 placed.originCellY, world->width_, world->height_);
1442 placed.channel = def->channel;
1443 placed.tags = def->tags;
1444 placed.surfaceId = patch.anchor.surfaceId;
1445 placed.surfaceRevision = patch.anchor.surfaceRevision;
1446 placed.surfaceNormalX = patch.anchor.normalX;
1447 placed.surfaceNormalY = patch.anchor.normalY;
1448 placed.surfaceNormalZ = patch.anchor.normalZ;
1449 placed.surfaceTangentX = patch.anchor.tangentX;
1450 placed.surfaceTangentY = patch.anchor.tangentY;
1451 placed.surfaceTangentZ = patch.anchor.tangentZ;
1452 placed.surfaceSampleCount = static_cast<int>(patch.samples.size());
1453 placed.surfaceMaxSlopeDegrees = patch.maxSlopeDegrees;
1454 placed.surfaceHeightDelta = patch.heightDelta;
1455 world->buildings_[placed.instanceId] = placed;
1456 world->instanceOrder_.push_back(placed.instanceId);
1457
1458 BuildingChangeEvent event;
1459 event.action = "place";
1460 event.worldId = world->getId();
1461 event.buildingId = buildingId;
1462 event.instanceId = placed.instanceId;
1463 event.cellX = placed.originCellX;
1464 event.cellY = placed.originCellY;
1465 event.level = placed.level;
1466 event.rotationDeg = placed.rotationDeg;
1467 event.worldX = placed.worldX;
1468 event.worldY = placed.worldY;
1469 event.elevation = placed.elevation;
1470 event.channel = placed.channel;
1471 if (world->publishEvents_) emit(std::move(event));
1473}
1474
1475int PlacementSystem::placeFree(PlacementWorld *world, const std::string &buildingId,
1476 float worldX, float worldY, float elevation,
1477 float rotationDeg) {
1478 auto result = placeFreeResult(world, buildingId, worldX, worldY, elevation, rotationDeg);
1479 return result.ok() ? std::move(result).takeValue().instanceId : 0;
1480}
1481
1482uint8_t PlacementSystem::edgeConnectionMask(const PlacementWorld &world, int instanceId) {
1483 const auto placedIt = world.buildings_.find(instanceId);
1484 if (placedIt == world.buildings_.end() || placedIt->second.placementKind != "edge") return 0;
1485 const PlacedBuilding &placed = placedIt->second;
1486 const BuildingDefinition *def = BuildingRegistry::find(placed.buildingId);
1487 if (!def) return 0;
1488 const std::string group = def->connectionGroup.empty() ? def->id : def->connectionGroup;
1489
1490 auto occupantAt = [&](const EdgeAddress &edge) {
1491 const auto *channels = world.findEdgeChannels(edge.axis, placed.level);
1492 const bool inBounds = edge.axis == EdgeAxis::Horizontal
1493 ? edge.x >= 0 && edge.x < world.width_ && edge.y >= 0 &&
1494 edge.y <= world.height_
1495 : edge.x >= 0 && edge.x <= world.width_ && edge.y >= 0 &&
1496 edge.y < world.height_;
1497 if (!channels || !inBounds) return 0;
1498 const auto channelIt = channels->find(placed.channel);
1499 if (channelIt == channels->end()) return 0;
1500 const size_t index = edge.axis == EdgeAxis::Horizontal
1501 ? size_t(edge.y) * size_t(world.width_) + size_t(edge.x)
1502 : size_t(edge.y) * size_t(world.width_ + 1) + size_t(edge.x);
1503 return index < channelIt->second.size() ? channelIt->second[index] : 0;
1504 };
1505 auto connects = [&](const EdgeAddress &edge) {
1506 const int otherId = occupantAt(edge);
1507 if (otherId == 0 || otherId == instanceId) return false;
1508 const auto otherIt = world.buildings_.find(otherId);
1509 if (otherIt == world.buildings_.end()) return false;
1510 const BuildingDefinition *otherDef = BuildingRegistry::find(otherIt->second.buildingId);
1511 if (!otherDef) return false;
1512 const std::string otherGroup = otherDef->connectionGroup.empty() ? otherDef->id
1513 : otherDef->connectionGroup;
1514 return group == otherGroup;
1515 };
1516
1517 const EdgeAddress e = placed.edge;
1518 EdgeAddress candidates[6];
1519 if (e.axis == EdgeAxis::Horizontal) {
1520 candidates[0] = {e.x - 1, e.y, EdgeAxis::Horizontal};
1521 candidates[1] = {e.x + 1, e.y, EdgeAxis::Horizontal};
1522 candidates[2] = {e.x, e.y - 1, EdgeAxis::Vertical};
1523 candidates[3] = {e.x, e.y, EdgeAxis::Vertical};
1524 candidates[4] = {e.x + 1, e.y - 1, EdgeAxis::Vertical};
1525 candidates[5] = {e.x + 1, e.y, EdgeAxis::Vertical};
1526 } else {
1527 candidates[0] = {e.x, e.y - 1, EdgeAxis::Vertical};
1528 candidates[1] = {e.x, e.y + 1, EdgeAxis::Vertical};
1529 candidates[2] = {e.x - 1, e.y, EdgeAxis::Horizontal};
1530 candidates[3] = {e.x, e.y, EdgeAxis::Horizontal};
1531 candidates[4] = {e.x - 1, e.y + 1, EdgeAxis::Horizontal};
1532 candidates[5] = {e.x, e.y + 1, EdgeAxis::Horizontal};
1533 }
1534 uint8_t mask = 0;
1535 for (int i = 0; i < 6; ++i)
1536 if (connects(candidates[i])) mask |= uint8_t(1u << i);
1537 return mask;
1538}
1539
1540std::string PlacementSystem::edgeVariant(const PlacementWorld &world, int instanceId) {
1541 const uint8_t mask = edgeConnectionMask(world, instanceId);
1542 int count = 0;
1543 for (int bit = 0; bit < 6; ++bit)
1544 if ((mask & uint8_t(1u << bit)) != 0) ++count;
1545 if (count == 0) return "isolated";
1546 if (count == 1) return "end";
1547 if (count == 2) return (mask & 0x03u) == 0x03u ? "straight" : "corner";
1548 if (count == 3) return "tee";
1549 return "cross";
1550}
1551
1553PlacementSystem::previewEdgePath(PlacementWorld *world, const std::string &buildingId,
1554 const std::vector<CornerAddress> &vertices) {
1555 if (!world || vertices.size() < 2) {
1558 "edge path preview requires a world and at least two vertices", buildingId, {},
1559 "building.edge-path"));
1560 }
1561 const BuildingDefinition *definition = BuildingRegistry::find(buildingId);
1562 if (!definition || definition->placementKind != "edge") {
1565 "edge path preview requires an edge building definition", buildingId, {},
1566 "building.edge-path"));
1567 }
1568 EdgePathPreview preview;
1569 preview.buildingId = buildingId;
1570 preview.level = world->getActiveLevel();
1571 std::unordered_set<std::string> uniqueEdges;
1572 for (size_t i = 1; i < vertices.size(); ++i) {
1573 const CornerAddress start = vertices[i - 1];
1574 const CornerAddress end = vertices[i];
1575 const bool horizontal = start.y == end.y;
1576 const bool vertical = start.x == end.x;
1577 if (horizontal == vertical) {
1580 "each edge path segment must be non-empty and axis-aligned",
1581 std::to_string(i - 1), {}, "building.edge-path"));
1582 }
1583 const int length = horizontal ? std::abs(end.x - start.x) : std::abs(end.y - start.y);
1584 for (int step = 0; step < length; ++step) {
1585 EdgeAddress edge{horizontal ? std::min(start.x, end.x) + step : start.x,
1586 vertical ? std::min(start.y, end.y) + step : start.y,
1587 horizontal ? EdgeAxis::Horizontal : EdgeAxis::Vertical};
1588 const std::string key = std::to_string(edge.x) + ":" + std::to_string(edge.y) +
1589 ":" + (horizontal ? "h" : "v");
1590 if (!uniqueEdges.insert(key).second) {
1593 "edge path contains the same canonical edge more than once",
1594 "edge_path_duplicate", {}, "building.edge-path"));
1595 }
1596 const char *direction = horizontal ? "north" : "west";
1597 std::string reason;
1598 if (!canPlaceEdge(world, buildingId, edge.x, edge.y, direction, 0, &reason)) {
1601 "edge path preflight rejected without mutation", reason, {},
1602 "building.edge-path"));
1603 }
1604 preview.edges.push_back(edge);
1605 }
1606 }
1607 return eve::Result<EdgePathPreview>::success(std::move(preview));
1608}
1609
1610eve::Result<std::vector<CornerAddress>> PlacementSystem::sampleEdgeCubicBezier(
1611 const std::vector<EdgeCurvePoint> &controlPoints, int subdivisions) {
1612 constexpr int kMaxSubdivisions = 4096;
1613 constexpr size_t kMaxRasterEdges = 65536;
1614 if (controlPoints.size() != 4 || subdivisions < 2 || subdivisions > kMaxSubdivisions) {
1617 "cubic edge curve requires four controls and 2..4096 subdivisions", {}, {},
1618 "building.edge-curve"));
1619 }
1620 for (const EdgeCurvePoint &point : controlPoints) {
1621 if (!std::isfinite(point.x) || !std::isfinite(point.y)) {
1623 eve::DiagnosticCode::InvalidArgument, "edge curve controls must be finite", {},
1624 {}, "building.edge-curve"));
1625 }
1626 }
1627
1628 auto roundedPoint = [&](int sample) -> eve::Result<CornerAddress> {
1629 const double t = static_cast<double>(sample) / static_cast<double>(subdivisions);
1630 const double u = 1.0 - t;
1631 const double x = u * u * u * controlPoints[0].x +
1632 3.0 * u * u * t * controlPoints[1].x +
1633 3.0 * u * t * t * controlPoints[2].x +
1634 t * t * t * controlPoints[3].x;
1635 const double y = u * u * u * controlPoints[0].y +
1636 3.0 * u * u * t * controlPoints[1].y +
1637 3.0 * u * t * t * controlPoints[2].y +
1638 t * t * t * controlPoints[3].y;
1639 constexpr double kIntMin = static_cast<double>(std::numeric_limits<int>::min()) + 1.0;
1640 constexpr double kIntMax = static_cast<double>(std::numeric_limits<int>::max()) - 1.0;
1641 if (x < kIntMin || x > kIntMax || y < kIntMin || y > kIntMax) {
1643 eve::DiagnosticCode::InvalidArgument, "edge curve exceeds grid coordinate range",
1644 {}, {}, "building.edge-curve"));
1645 }
1647 {static_cast<int>(std::lround(x)), static_cast<int>(std::lround(y))});
1648 };
1649
1650 auto first = roundedPoint(0);
1651 if (!first.ok())
1652 return eve::Result<std::vector<CornerAddress>>::failure(first.status());
1653 std::vector<CornerAddress> vertices;
1654 vertices.push_back(std::move(first).takeValue());
1655 for (int sample = 1; sample <= subdivisions; ++sample) {
1656 auto targetResult = roundedPoint(sample);
1657 if (!targetResult.ok())
1658 return eve::Result<std::vector<CornerAddress>>::failure(targetResult.status());
1659 const CornerAddress target = std::move(targetResult).takeValue();
1660 CornerAddress cursor = vertices.back();
1661 while (cursor != target) {
1662 const int remainingX = target.x - cursor.x;
1663 const int remainingY = target.y - cursor.y;
1664 if (remainingX != 0 &&
1665 (remainingY == 0 || std::abs(remainingX) >= std::abs(remainingY)))
1666 cursor.x += remainingX > 0 ? 1 : -1;
1667 else
1668 cursor.y += remainingY > 0 ? 1 : -1;
1669 vertices.push_back(cursor);
1670 if (vertices.size() - 1 > kMaxRasterEdges) {
1673 "edge curve rasterization exceeds 65536 unit edges", {}, {},
1674 "building.edge-curve"));
1675 }
1676 }
1677 }
1678 if (vertices.size() < 2) {
1681 "edge curve rasterization must cover at least one unit edge", {}, {},
1682 "building.edge-curve"));
1683 }
1684 return eve::Result<std::vector<CornerAddress>>::success(std::move(vertices));
1685}
1686
1687eve::Result<PlacementSystem::EdgePathPreview> PlacementSystem::previewEdgeCubicBezier(
1688 PlacementWorld *world, const std::string &buildingId,
1689 const std::vector<EdgeCurvePoint> &controlPoints, int subdivisions) {
1690 auto vertices = sampleEdgeCubicBezier(controlPoints, subdivisions);
1691 if (!vertices.ok()) return eve::Result<EdgePathPreview>::failure(vertices.status());
1692 return previewEdgePath(world, buildingId, std::move(vertices).takeValue());
1693}
1694
1695eve::Result<PlacementSystem::EdgePathPlacement> PlacementSystem::placeEdgeCubicBezier(
1696 PlacementWorld *world, const std::string &buildingId,
1697 const std::vector<EdgeCurvePoint> &controlPoints, int subdivisions) {
1698 auto preview = previewEdgeCubicBezier(world, buildingId, controlPoints, subdivisions);
1699 if (!preview.ok()) return eve::Result<EdgePathPlacement>::failure(preview.status());
1700 return commitEdgePath(world, std::move(preview).takeValue(), &controlPoints, subdivisions);
1701}
1702
1703eve::Result<PlacementSystem::EdgeCurveSurface> PlacementSystem::sampleEdgeCurveSurface(
1704 const PlacementWorld &world, const std::string &surfaceName,
1705 const std::vector<EdgeCurvePoint> &controlPoints, int subdivisions) {
1706 auto validated = sampleEdgeCubicBezier(controlPoints, subdivisions);
1707 if (!validated.ok()) return eve::Result<EdgeCurveSurface>::failure(validated.status());
1708 if (surfaceName.empty())
1710 eve::DiagnosticCode::InvalidArgument, "curve surface provider name is required", {},
1711 {}, "building.edge-curve-surface"));
1712 float ox = 0.f, oy = 0.f, xx = 0.f, xy = 0.f, yx = 0.f, yy = 0.f;
1713 world.cellToWorldPlane(0, 0, ox, oy);
1714 world.cellToWorldPlane(1, 0, xx, xy);
1715 world.cellToWorldPlane(0, 1, yx, yy);
1716 xx -= ox;
1717 xy -= oy;
1718 yx -= ox;
1719 yy -= oy;
1720 EdgeCurveSurface result;
1721 result.providerName = surfaceName;
1722 result.samples.reserve(static_cast<size_t>(subdivisions + 1));
1723 for (int index = 0; index <= subdivisions; ++index) {
1724 const double t = static_cast<double>(index) / subdivisions;
1725 const double u = 1.0 - t;
1726 const double gx = u * u * u * controlPoints[0].x +
1727 3.0 * u * u * t * controlPoints[1].x +
1728 3.0 * u * t * t * controlPoints[2].x +
1729 t * t * t * controlPoints[3].x;
1730 const double gy = u * u * u * controlPoints[0].y +
1731 3.0 * u * u * t * controlPoints[1].y +
1732 3.0 * u * t * t * controlPoints[2].y +
1733 t * t * t * controlPoints[3].y;
1734 auto sampled = sampleSurface(world, surfaceName,
1735 ox + static_cast<float>(gx) * xx +
1736 static_cast<float>(gy) * yx,
1737 oy + static_cast<float>(gx) * xy +
1738 static_cast<float>(gy) * yy);
1739 if (!sampled.ok()) return eve::Result<EdgeCurveSurface>::failure(sampled.status());
1740 const PlacementHit &hit = sampled.value();
1741 if (index == 0) {
1742 result.surfaceId = hit.surfaceId;
1743 result.surfaceRevision = hit.surfaceRevision;
1744 } else if (hit.surfaceId != result.surfaceId ||
1745 hit.surfaceRevision != result.surfaceRevision) {
1748 "curve crosses a surface identity or revision boundary", hit.surfaceId, {},
1749 "building.edge-curve-surface"));
1750 }
1751 result.samples.push_back({hit.worldX, hit.worldY, hit.worldZ, hit.normalX, hit.normalY,
1752 hit.normalZ});
1753 }
1754 return eve::Result<EdgeCurveSurface>::success(std::move(result));
1755}
1756
1758PlacementSystem::placeEdgeCubicBezierOnSurface(
1759 PlacementWorld *world, const std::string &buildingId,
1760 const std::vector<EdgeCurvePoint> &controlPoints, int subdivisions,
1761 const std::string &surfaceName) {
1762 if (!world)
1764 eve::DiagnosticCode::InvalidArgument, "curve surface placement requires a world",
1765 {}, {}, "building.edge-curve-surface"));
1766 auto preview = previewEdgeCubicBezier(world, buildingId, controlPoints, subdivisions);
1767 if (!preview.ok()) return eve::Result<EdgePathPlacement>::failure(preview.status());
1768 auto surface = sampleEdgeCurveSurface(*world, surfaceName, controlPoints, subdivisions);
1769 if (!surface.ok()) return eve::Result<EdgePathPlacement>::failure(surface.status());
1770 return commitEdgePath(world, std::move(preview).takeValue(), &controlPoints, subdivisions,
1771 &surface.value());
1772}
1773
1775PlacementSystem::placeEdgeLine(PlacementWorld *world, const std::string &buildingId,
1776 int startVertexX, int startVertexY, int endVertexX,
1777 int endVertexY) {
1778 auto path = placeEdgePath(world, buildingId,
1779 {{startVertexX, startVertexY}, {endVertexX, endVertexY}});
1780 if (!path.ok()) return eve::Result<EdgeLinePlacement>::failure(path.status());
1781 EdgeLinePlacement result;
1782 result.instanceIds = std::move(path).takeValue().instanceIds;
1783 return eve::Result<EdgeLinePlacement>::success(std::move(result));
1784}
1785
1787PlacementSystem::placeEdgePath(PlacementWorld *world, const std::string &buildingId,
1788 const std::vector<CornerAddress> &vertices) {
1789 auto previewResult = previewEdgePath(world, buildingId, vertices);
1790 if (!previewResult.ok())
1791 return eve::Result<EdgePathPlacement>::failure(previewResult.status());
1792 return commitEdgePath(world, std::move(previewResult).takeValue());
1793}
1794
1795eve::Result<PlacementSystem::EdgePathPlacement> PlacementSystem::commitEdgePath(
1796 PlacementWorld *world, EdgePathPreview preview,
1797 const std::vector<EdgeCurvePoint> *curveControls, int curveSubdivisions,
1798 const EdgeCurveSurface *curveSurface) {
1799 const BuildingDefinition *def = BuildingRegistry::find(preview.buildingId);
1800 const std::vector<EdgeAddress> &edges = preview.edges;
1801 const EdgeCurveGroupId curveGroupId =
1802 curveControls ? EdgeCurveGroupId{world->nextEdgeCurveGroupId_++} : EdgeCurveGroupId{};
1803
1804 EdgePathPlacement result;
1805 result.instanceIds.reserve(edges.size());
1806 std::vector<BuildingChangeEvent> events;
1807 events.reserve(edges.size());
1808 for (const EdgeAddress &edge : edges) {
1809 PlacedBuilding placed;
1810 placed.instanceId = nextInstanceId();
1811 placed.buildingId = preview.buildingId;
1812 placed.placementKind = "edge";
1813 placed.edge = edge;
1814 placed.edgeCurveGroupId = curveGroupId;
1815 placed.originCellX = edge.x;
1816 placed.originCellY = edge.y;
1817 placed.level = preview.level;
1818 placed.channel = def->channel;
1819 placed.tags = def->tags;
1820 float ax = 0.f, ay = 0.f, bx = 0.f, by = 0.f;
1821 world->cellToWorldPlane(edge.x, edge.y, ax, ay);
1822 world->cellToWorldPlane(edge.x + (edge.axis == EdgeAxis::Horizontal ? 1 : 0),
1823 edge.y + (edge.axis == EdgeAxis::Vertical ? 1 : 0), bx, by);
1824 placed.worldX = (ax + bx) * 0.5f;
1825 placed.worldY = (ay + by) * 0.5f;
1826 placed.rotationDeg = edge.axis == EdgeAxis::Horizontal ? 0.f : 90.f;
1827
1828 auto &channels = world->edgeChannels(edge.axis, placed.level);
1829 auto &occupancy = channels[def->channel];
1830 const size_t required = edge.axis == EdgeAxis::Horizontal
1831 ? size_t(world->width_) * size_t(world->height_ + 1)
1832 : size_t(world->width_ + 1) * size_t(world->height_);
1833 if (occupancy.size() != required) occupancy.assign(required, 0);
1834 const size_t index = edge.axis == EdgeAxis::Horizontal
1835 ? size_t(edge.y) * size_t(world->width_) + size_t(edge.x)
1836 : size_t(edge.y) * size_t(world->width_ + 1) + size_t(edge.x);
1837 occupancy[index] = placed.instanceId;
1838 world->buildings()[placed.instanceId] = placed;
1839 world->instanceOrder_.push_back(placed.instanceId);
1840 result.instanceIds.push_back(placed.instanceId);
1841
1842 BuildingChangeEvent event;
1843 event.action = "place";
1844 event.worldId = world->getId();
1845 event.buildingId = preview.buildingId;
1846 event.instanceId = placed.instanceId;
1847 event.cellX = edge.x;
1848 event.cellY = edge.y;
1849 event.level = placed.level;
1850 event.rotationDeg = placed.rotationDeg;
1851 event.worldX = placed.worldX;
1852 event.worldY = placed.worldY;
1853 event.channel = placed.channel;
1854 events.push_back(std::move(event));
1855 }
1856 if (curveControls) {
1857 EdgeCurveGroup group;
1858 group.id = curveGroupId;
1859 group.buildingId = preview.buildingId;
1860 group.level = preview.level;
1861 group.controlPoints = *curveControls;
1862 group.subdivisions = curveSubdivisions;
1863 group.instanceIds = result.instanceIds;
1864 if (curveSurface) {
1865 group.surfaceProviderName = curveSurface->providerName;
1866 group.surfaceId = curveSurface->surfaceId;
1867 group.surfaceRevision = curveSurface->surfaceRevision;
1868 group.surfaceSamples = curveSurface->samples;
1869 }
1870 world->edgeCurveGroups_.emplace(group.id.value, std::move(group));
1871 }
1872 if (world->publishEvents_)
1873 for (BuildingChangeEvent &event : events) emit(std::move(event));
1874 return eve::Result<EdgePathPlacement>::success(std::move(result));
1875}
1876
1878PlacementSystem::previewArea(PlacementWorld *world, const std::string &buildingId,
1879 const std::vector<std::pair<int, int>> &anchors,
1880 float rotationDeg) {
1881 if (!world || anchors.empty()) {
1883 eve::DiagnosticCode::InvalidArgument, "area preview requires a world and anchors",
1884 buildingId, {}, "building.area"));
1885 }
1886 const BuildingDefinition *def = BuildingRegistry::find(buildingId);
1887 if (!def || def->placementKind == "edge") {
1889 eve::DiagnosticCode::InvalidArgument, "area tools require a cell building definition",
1890 buildingId, {}, "building.area"));
1891 }
1892 AreaPreview preview;
1893 preview.buildingId = buildingId;
1894 preview.level = world->getActiveLevel();
1895 preview.rotationDeg = normalizeRotation(buildingId, rotationDeg);
1896 std::unordered_set<uint64_t> reserved;
1897 for (const auto &[x, y] : anchors) {
1898 AreaCellPreview cell{x, y, false, {}};
1899 if (!canPlace(world, buildingId, x, y, preview.rotationDeg, 0, &cell.reason)) {
1900 ++preview.rejectedCount;
1901 preview.cells.push_back(std::move(cell));
1902 continue;
1903 }
1904 bool overlaps = false;
1905 std::vector<uint64_t> footprint;
1906 foreachFootprintCell(*def, x, y, preview.rotationDeg, [&](int cx, int cy) {
1907 const uint64_t key = (uint64_t(uint32_t(cx)) << 32u) | uint32_t(cy);
1908 footprint.push_back(key);
1909 if (reserved.count(key) != 0) overlaps = true;
1910 return true;
1911 });
1912 if (overlaps) {
1913 cell.reason = "area_candidate_overlap";
1914 ++preview.rejectedCount;
1915 } else {
1916 cell.accepted = true;
1917 ++preview.acceptedCount;
1918 reserved.insert(footprint.begin(), footprint.end());
1919 }
1920 preview.cells.push_back(std::move(cell));
1921 }
1922 return eve::Result<AreaPreview>::success(std::move(preview));
1923}
1924
1926PlacementSystem::previewRectangle(PlacementWorld *world, const std::string &buildingId,
1927 int minCellX, int minCellY, int maxCellX, int maxCellY,
1928 float rotationDeg) {
1929 if (minCellX > maxCellX) std::swap(minCellX, maxCellX);
1930 if (minCellY > maxCellY) std::swap(minCellY, maxCellY);
1931 std::vector<std::pair<int, int>> anchors;
1932 for (int y = minCellY; y <= maxCellY; ++y)
1933 for (int x = minCellX; x <= maxCellX; ++x) anchors.emplace_back(x, y);
1934 return previewArea(world, buildingId, anchors, rotationDeg);
1935}
1936
1938PlacementSystem::previewBrush(PlacementWorld *world, const std::string &buildingId,
1939 int centerCellX, int centerCellY, int radius, float rotationDeg) {
1940 if (radius < 0) {
1942 eve::DiagnosticCode::InvalidArgument, "brush radius must be non-negative",
1943 std::to_string(radius), {}, "building.area"));
1944 }
1945 std::vector<std::pair<int, int>> anchors;
1946 for (int y = centerCellY - radius; y <= centerCellY + radius; ++y)
1947 for (int x = centerCellX - radius; x <= centerCellX + radius; ++x)
1948 if ((x - centerCellX) * (x - centerCellX) + (y - centerCellY) * (y - centerCellY) <=
1949 radius * radius)
1950 anchors.emplace_back(x, y);
1951 return previewArea(world, buildingId, anchors, rotationDeg);
1952}
1953
1955PlacementSystem::previewRectangleOutline(PlacementWorld *world,
1956 const std::string &buildingId, int minCellX,
1957 int minCellY, int maxCellX, int maxCellY,
1958 float rotationDeg) {
1959 if (minCellX > maxCellX) std::swap(minCellX, maxCellX);
1960 if (minCellY > maxCellY) std::swap(minCellY, maxCellY);
1961 std::vector<std::pair<int, int>> anchors;
1962 for (int x = minCellX; x <= maxCellX; ++x) anchors.emplace_back(x, minCellY);
1963 if (maxCellY != minCellY)
1964 for (int x = minCellX; x <= maxCellX; ++x) anchors.emplace_back(x, maxCellY);
1965 for (int y = minCellY + 1; y < maxCellY; ++y) {
1966 anchors.emplace_back(minCellX, y);
1967 if (maxCellX != minCellX) anchors.emplace_back(maxCellX, y);
1968 }
1969 std::sort(anchors.begin(), anchors.end(), [](const auto &lhs, const auto &rhs) {
1970 return lhs.second == rhs.second ? lhs.first < rhs.first : lhs.second < rhs.second;
1971 });
1972 return previewArea(world, buildingId, anchors, rotationDeg);
1973}
1974
1976PlacementSystem::previewPattern(PlacementWorld *world, const std::string &buildingId,
1977 const PatternRequest &request) {
1978 const auto failure = [&](std::string message) {
1981 "building.pattern"));
1982 };
1983 const auto corners = [&]() -> eve::Result<std::vector<CornerAddress>> {
1984 std::vector<CornerAddress> result;
1985 result.reserve(request.points.size());
1986 for (const EdgeCurvePoint &point : request.points) {
1987 if (!std::isfinite(point.x) || !std::isfinite(point.y) ||
1988 std::abs(point.x - std::round(point.x)) > 0.0001f ||
1989 std::abs(point.y - std::round(point.y)) > 0.0001f)
1990 return eve::Result<std::vector<CornerAddress>>::failure(
1993 "discrete pattern points must be finite integer grid vertices",
1994 buildingId, {}, "building.pattern"));
1995 result.push_back(
1996 {static_cast<int>(std::lround(point.x)), static_cast<int>(std::lround(point.y))});
1997 }
1998 return eve::Result<std::vector<CornerAddress>>::success(std::move(result));
1999 };
2000
2001 PatternPreview preview;
2002 preview.request = request;
2003 switch (request.kind) {
2004 case PatternKind::EdgeLine:
2005 case PatternKind::EdgePath: {
2006 if ((request.kind == PatternKind::EdgeLine && request.points.size() != 2) ||
2007 (request.kind == PatternKind::EdgePath && request.points.size() < 2))
2008 return failure(request.kind == PatternKind::EdgeLine
2009 ? "edge-line pattern requires exactly two points"
2010 : "edge-path pattern requires at least two points");
2011 if (!request.surfaceName.empty())
2012 return failure("custom surfaces are currently supported only by cubic curves");
2013 auto vertices = corners();
2014 if (!vertices.ok())
2016 auto edge = previewEdgePath(world, buildingId, vertices.value());
2017 if (!edge.ok()) return eve::Result<PatternPreview>::failure(edge.status());
2018 preview.edge = std::move(edge).takeValue();
2019 break;
2020 }
2021 case PatternKind::EdgeCubicBezier: {
2022 if (request.points.size() != 4)
2023 return failure("cubic edge pattern requires exactly four control points");
2024 auto edge = previewEdgeCubicBezier(world, buildingId, request.points,
2025 request.subdivisions);
2026 if (!edge.ok()) return eve::Result<PatternPreview>::failure(edge.status());
2027 if (!request.surfaceName.empty()) {
2028 auto surface = sampleEdgeCurveSurface(*world, request.surfaceName,
2029 request.points, request.subdivisions);
2030 if (!surface.ok())
2031 return eve::Result<PatternPreview>::failure(surface.status());
2032 }
2033 preview.edge = std::move(edge).takeValue();
2034 break;
2035 }
2036 case PatternKind::RectangleFill:
2037 case PatternKind::RectangleOutline: {
2038 if (request.points.size() != 2)
2039 return failure("rectangle pattern requires exactly two corner points");
2040 if (!request.surfaceName.empty())
2041 return failure("area patterns do not accept a custom surface name");
2042 auto vertices = corners();
2043 if (!vertices.ok())
2045 const CornerAddress &a = vertices.value()[0];
2046 const CornerAddress &b = vertices.value()[1];
2047 auto area = request.kind == PatternKind::RectangleFill
2048 ? previewRectangle(world, buildingId, a.x, a.y, b.x, b.y,
2049 request.rotationDeg)
2050 : previewRectangleOutline(world, buildingId, a.x, a.y, b.x, b.y,
2051 request.rotationDeg);
2052 if (!area.ok()) return eve::Result<PatternPreview>::failure(area.status());
2053 preview.area = std::move(area).takeValue();
2054 break;
2055 }
2056 case PatternKind::CircleBrush: {
2057 if (request.points.size() != 1)
2058 return failure("circle-brush pattern requires exactly one center point");
2059 if (!request.surfaceName.empty())
2060 return failure("area patterns do not accept a custom surface name");
2061 auto vertices = corners();
2062 if (!vertices.ok())
2064 auto area = previewBrush(world, buildingId, vertices.value()[0].x,
2065 vertices.value()[0].y, request.radius,
2066 request.rotationDeg);
2067 if (!area.ok()) return eve::Result<PatternPreview>::failure(area.status());
2068 preview.area = std::move(area).takeValue();
2069 break;
2070 }
2071 }
2072 return eve::Result<PatternPreview>::success(std::move(preview));
2073}
2074
2076PlacementSystem::placePattern(PlacementWorld *world, const std::string &buildingId,
2077 const PatternRequest &request) {
2078 auto expanded = previewPattern(world, buildingId, request);
2079 if (!expanded.ok()) return eve::Result<PatternPlacement>::failure(expanded.status());
2080 PatternPlacement result;
2081 result.preview = std::move(expanded).takeValue();
2082 if (request.kind == PatternKind::EdgeLine || request.kind == PatternKind::EdgePath) {
2083 auto placed = commitEdgePath(world, result.preview.edge);
2084 if (!placed.ok()) return eve::Result<PatternPlacement>::failure(placed.status());
2085 result.instanceIds = std::move(placed).takeValue().instanceIds;
2086 } else if (request.kind == PatternKind::EdgeCubicBezier) {
2087 auto placed = [&]() -> eve::Result<EdgePathPlacement> {
2088 if (request.surfaceName.empty())
2089 return commitEdgePath(world, result.preview.edge,
2090 &result.preview.request.points,
2091 request.subdivisions);
2092 auto surface = sampleEdgeCurveSurface(*world, request.surfaceName,
2093 result.preview.request.points,
2094 request.subdivisions);
2095 if (!surface.ok())
2096 return eve::Result<EdgePathPlacement>::failure(surface.status());
2097 const EdgeCurveSurface frames = std::move(surface).takeValue();
2098 return commitEdgePath(world, result.preview.edge,
2099 &result.preview.request.points,
2100 request.subdivisions, &frames);
2101 }();
2102 if (!placed.ok()) return eve::Result<PatternPlacement>::failure(placed.status());
2103 result.instanceIds = std::move(placed).takeValue().instanceIds;
2104 } else {
2105 auto placed = commitArea(world, result.preview.area);
2106 if (!placed.ok()) return eve::Result<PatternPlacement>::failure(placed.status());
2107 result.instanceIds = std::move(placed).takeValue().instanceIds;
2108 }
2109 return eve::Result<PatternPlacement>::success(std::move(result));
2110}
2111
2113PlacementSystem::commitArea(PlacementWorld *world, AreaPreview preview) {
2114 if (!world || preview.rejectedCount != 0 || preview.cells.empty()) {
2116 eve::DiagnosticCode::Conflict, "area commit requires a fully accepted preview",
2117 std::to_string(preview.rejectedCount), {}, "building.area"));
2118 }
2119 const BuildingDefinition *def = BuildingRegistry::find(preview.buildingId);
2120 if (!def) {
2122 eve::DiagnosticCode::NotFound, "area definition was not found", preview.buildingId, {},
2123 "building.area"));
2124 }
2125 AreaPlacement placement;
2126 placement.preview = std::move(preview);
2127 std::vector<BuildingChangeEvent> events;
2128 for (const AreaCellPreview &cell : placement.preview.cells) {
2129 PlacedBuilding pb;
2130 pb.instanceId = nextInstanceId();
2131 pb.buildingId = placement.preview.buildingId;
2132 pb.originCellX = cell.cellX;
2133 pb.originCellY = cell.cellY;
2134 pb.level = placement.preview.level;
2135 pb.rotationDeg = placement.preview.rotationDeg;
2136 pb.channel = def->channel;
2137 pb.tags = def->tags;
2138 world->cellToWorldPlane(cell.cellX, cell.cellY, pb.worldX, pb.worldY);
2139 PlacementQuery supportQuery;
2140 supportQuery.cellX = pb.originCellX;
2141 supportQuery.cellY = pb.originCellY;
2142 supportQuery.level = pb.level;
2143 supportQuery.rotationDeg = pb.rotationDeg;
2144 pb.supportInstanceIds = collectStructuralSupports(*world, *def, supportQuery);
2145 writeOccupancy(*world, *def, pb, pb.instanceId);
2146 world->buildings()[pb.instanceId] = pb;
2147 world->instanceOrder_.push_back(pb.instanceId);
2148 placement.instanceIds.push_back(pb.instanceId);
2149 BuildingChangeEvent event;
2150 event.action = "place";
2151 event.worldId = world->getId();
2152 event.buildingId = pb.buildingId;
2153 event.instanceId = pb.instanceId;
2154 event.cellX = pb.originCellX;
2155 event.cellY = pb.originCellY;
2156 event.rotationDeg = pb.rotationDeg;
2157 event.worldX = pb.worldX;
2158 event.worldY = pb.worldY;
2159 event.channel = pb.channel;
2160 events.push_back(std::move(event));
2161 }
2162 if (world->publishEvents_)
2163 for (BuildingChangeEvent &event : events) emit(std::move(event));
2164 return eve::Result<AreaPlacement>::success(std::move(placement));
2165}
2166
2168PlacementSystem::placeRectangle(PlacementWorld *world, const std::string &buildingId,
2169 int minCellX, int minCellY, int maxCellX, int maxCellY,
2170 float rotationDeg) {
2171 auto preview = previewRectangle(world, buildingId, minCellX, minCellY, maxCellX, maxCellY,
2172 rotationDeg);
2173 if (!preview.ok()) return eve::Result<AreaPlacement>::failure(preview.status());
2174 return commitArea(world, std::move(preview).takeValue());
2175}
2176
2178PlacementSystem::placeBrush(PlacementWorld *world, const std::string &buildingId, int centerCellX,
2179 int centerCellY, int radius, float rotationDeg) {
2180 auto preview = previewBrush(world, buildingId, centerCellX, centerCellY, radius, rotationDeg);
2181 if (!preview.ok()) return eve::Result<AreaPlacement>::failure(preview.status());
2182 return commitArea(world, std::move(preview).takeValue());
2183}
2184
2186PlacementSystem::placeRectangleOutline(PlacementWorld *world,
2187 const std::string &buildingId, int minCellX,
2188 int minCellY, int maxCellX, int maxCellY,
2189 float rotationDeg) {
2190 auto preview = previewRectangleOutline(world, buildingId, minCellX, minCellY,
2191 maxCellX, maxCellY, rotationDeg);
2192 if (!preview.ok()) return eve::Result<AreaPlacement>::failure(preview.status());
2193 return commitArea(world, std::move(preview).takeValue());
2194}
2195
2196void PlacementSystem::writeOccupancy(PlacementWorld &world, const BuildingDefinition &def,
2197 const PlacedBuilding &placed, int instanceId) {
2198 std::vector<int> &occ = world.channelOccupancy(def.channel, placed.level);
2199 foreachFootprintCell(def, placed.originCellX, placed.originCellY, placed.rotationDeg,
2200 [&](int cx, int cy) {
2201 if (world.inBounds(cx, cy)) {
2202 const size_t idx =
2203 size_t(cy) * size_t(world.getWidth()) + size_t(cx);
2204 if (idx < occ.size()) occ[idx] = instanceId;
2205 }
2206 return true;
2207 });
2208}
2209
2210void PlacementSystem::clearOccupancy(PlacementWorld &world, int instanceId) {
2211 // 默认通道("")占用存于 occupancy_,附加通道存于 allChannels_,两者都要清。
2212 for (int &v : world.occupancy()) {
2213 if (v == instanceId) v = 0;
2214 }
2215 for (auto &kv : world.allChannels()) {
2216 for (int &v : kv.second) {
2217 if (v == instanceId) v = 0;
2218 }
2219 }
2220 for (auto &kv : world.horizontalEdgeChannels_) {
2221 for (int &v : kv.second)
2222 if (v == instanceId) v = 0;
2223 }
2224 for (auto &kv : world.verticalEdgeChannels_) {
2225 for (int &v : kv.second)
2226 if (v == instanceId) v = 0;
2227 }
2228 for (auto &kv : world.cornerChannels_) {
2229 for (int &v : kv.second)
2230 if (v == instanceId) v = 0;
2231 }
2232 for (auto &level : world.cellChannelsByLevel_)
2233 for (auto &channel : level.second)
2234 for (int &value : channel.second)
2235 if (value == instanceId) value = 0;
2236 for (auto &levels : {&world.horizontalEdgesByLevel_, &world.verticalEdgesByLevel_})
2237 for (auto &level : *levels)
2238 for (auto &channel : level.second)
2239 for (int &value : channel.second)
2240 if (value == instanceId) value = 0;
2241 for (auto &level : world.cornersByLevel_)
2242 for (auto &channel : level.second)
2243 for (int &value : channel.second)
2244 if (value == instanceId) value = 0;
2245}
2246
2247int PlacementSystem::placeAt(PlacementWorld *world, const std::string &buildingId, int cellX,
2248 int cellY, float rotationDeg) {
2249 ensureBuiltins();
2250 if (!world) return 0;
2251 const BuildingDefinition *def = BuildingRegistry::find(buildingId);
2252 if (!def) return 0;
2253 if (def->placementKind != "cell") return 0;
2254 const float rot = normalizeRotation(buildingId, rotationDeg);
2255 if (!canPlace(world, buildingId, cellX, cellY, rot, 0, nullptr)) return 0;
2256
2257 PlacedBuilding pb;
2258 pb.instanceId = nextInstanceId();
2259 pb.buildingId = buildingId;
2260 pb.originCellX = cellX;
2261 pb.originCellY = cellY;
2262 pb.level = world->getActiveLevel();
2263 world->cellToWorldPlane(cellX, cellY, pb.worldX, pb.worldY);
2264 pb.elevation = 0.f;
2265 pb.rotationDeg = rot;
2266 pb.channel = def->channel;
2267 pb.tags = def->tags;
2268 PlacementQuery supportQuery;
2269 supportQuery.cellX = pb.originCellX;
2270 supportQuery.cellY = pb.originCellY;
2271 supportQuery.level = pb.level;
2272 supportQuery.rotationDeg = pb.rotationDeg;
2273 pb.supportInstanceIds = collectStructuralSupports(*world, *def, supportQuery);
2274
2275 writeOccupancy(*world, *def, pb, pb.instanceId);
2276 world->buildings()[pb.instanceId] = pb;
2277 world->instanceOrder_.push_back(pb.instanceId);
2278
2280 ev.action = "place";
2281 ev.worldId = world->getId();
2282 ev.buildingId = buildingId;
2283 ev.instanceId = pb.instanceId;
2284 ev.cellX = cellX;
2285 ev.cellY = cellY;
2286 ev.rotationDeg = rot;
2287 ev.worldX = pb.worldX;
2288 ev.worldY = pb.worldY;
2289 ev.elevation = pb.elevation;
2290 ev.channel = pb.channel;
2291 if (world->publishEvents_) emit(std::move(ev));
2292 return pb.instanceId;
2293}
2294
2295int PlacementSystem::placeAtWorld(PlacementWorld *world, const std::string &buildingId,
2296 float worldX, float worldY, float rotationDeg) {
2297 ensureBuiltins();
2298 if (!world) return 0;
2299 const SnapResult s = snap(*world, buildingId, worldX, worldY);
2300 const int id = placeAt(world, buildingId, s.cellX, s.cellY, rotationDeg);
2301 if (id <= 0) return 0;
2302 // Preserve free / custom snap world pose (cell placement alone snaps to cell origin).
2303 auto it = world->buildings().find(id);
2304 if (it != world->buildings().end()) {
2305 it->second.worldX = s.worldX;
2306 it->second.worldY = s.worldY;
2307 }
2308 return id;
2309}
2310
2311int PlacementSystem::placeAtWorld3D(PlacementWorld *world, const std::string &buildingId,
2312 float worldX, float worldY, float worldZ,
2313 float rotationDeg) {
2314 ensureBuiltins();
2315 if (!world) return 0;
2316 const SnapResult s = snap3D(*world, buildingId, worldX, worldY, worldZ);
2317 const int id = placeAt(world, buildingId, s.cellX, s.cellY, rotationDeg);
2318 if (id <= 0) return 0;
2319 auto it = world->buildings().find(id);
2320 if (it != world->buildings().end()) {
2321 it->second.worldX = s.worldX;
2322 it->second.worldY = s.worldY;
2323 it->second.elevation = s.elevation;
2324 }
2325 return id;
2326}
2327
2328int PlacementSystem::placeGhost(PlacementWorld *world, Ghost *ghost) {
2329 if (!world || !ghost) return 0;
2330 if (!ghost->validate(world)) return 0;
2331 const BuildingDefinition *def = BuildingRegistry::find(ghost->getBuildingId());
2332 if (!def) return 0;
2333 if (def->placementKind == "edge") {
2334 const char *direction = ghost->edge_.axis == EdgeAxis::Horizontal ? "north" : "west";
2335 return placeEdge(world, ghost->getBuildingId(), ghost->edge_.x, ghost->edge_.y, direction);
2336 }
2337 if (def->placementKind == "corner") {
2338 if (ghost->placementKind_ != "corner") return 0;
2339 return placeCorner(world, ghost->getBuildingId(), ghost->corner_.x, ghost->corner_.y);
2340 }
2341 if (def->placementKind == "free") {
2342 if (ghost->placementKind_ != "free") return 0;
2343 if (!ghost->surfaceId_.empty()) {
2344 SurfacePatch patch;
2345 if (world->getGrid().plane == grid::GridPlane::XZ) {
2346 patch.anchor.worldX = ghost->worldX_;
2347 patch.anchor.worldY = ghost->elevation_;
2348 patch.anchor.worldZ = ghost->worldY_;
2349 } else {
2350 patch.anchor.worldX = ghost->worldX_;
2351 patch.anchor.worldY = ghost->worldY_;
2352 patch.anchor.worldZ = ghost->elevation_;
2353 }
2354 patch.anchor.surfaceId = ghost->surfaceId_;
2355 patch.anchor.surfaceRevision = ghost->surfaceRevision_;
2356 patch.anchor.normalX = ghost->surfaceNormalX_;
2357 patch.anchor.normalY = ghost->surfaceNormalY_;
2358 patch.anchor.normalZ = ghost->surfaceNormalZ_;
2359 patch.anchor.tangentX = ghost->surfaceTangentX_;
2360 patch.anchor.tangentY = ghost->surfaceTangentY_;
2361 patch.anchor.tangentZ = ghost->surfaceTangentZ_;
2362 patch.samples.resize(static_cast<size_t>(std::max(ghost->surfaceSampleCount_, 0)));
2363 patch.maxSlopeDegrees = ghost->surfaceMaxSlopeDegrees_;
2364 patch.heightDelta = ghost->surfaceHeightDelta_;
2365 auto result = placeFreeSurfaceResult(world, ghost->getBuildingId(), patch,
2366 ghost->rotationDeg_);
2367 return result.ok() ? std::move(result).takeValue().instanceId : 0;
2368 }
2369 return placeFree(world, ghost->getBuildingId(), ghost->worldX_, ghost->worldY_,
2370 ghost->elevation_, ghost->rotationDeg_);
2371 }
2372
2374 placed.instanceId = nextInstanceId();
2375 placed.buildingId = ghost->getBuildingId();
2376 placed.originCellX = ghost->getCellX();
2377 placed.originCellY = ghost->getCellY();
2378 placed.level = world->getActiveLevel();
2379 placed.worldX = ghost->getWorldX();
2380 placed.worldY = ghost->getWorldY();
2381 placed.elevation = ghost->getElevation();
2382 placed.rotationDeg = ghost->getRotationDeg();
2383 placed.channel = def->channel;
2384 placed.tags = def->tags;
2385 placed.surfaceId = ghost->surfaceId_;
2386 placed.surfaceRevision = ghost->surfaceRevision_;
2387 placed.surfaceNormalX = ghost->surfaceNormalX_;
2388 placed.surfaceNormalY = ghost->surfaceNormalY_;
2389 placed.surfaceNormalZ = ghost->surfaceNormalZ_;
2390 placed.surfaceTangentX = ghost->surfaceTangentX_;
2391 placed.surfaceTangentY = ghost->surfaceTangentY_;
2392 placed.surfaceTangentZ = ghost->surfaceTangentZ_;
2393 placed.surfaceSampleCount = ghost->surfaceSampleCount_;
2394 placed.surfaceMaxSlopeDegrees = ghost->surfaceMaxSlopeDegrees_;
2395 placed.surfaceHeightDelta = ghost->surfaceHeightDelta_;
2396 PlacementQuery supportQuery;
2397 supportQuery.cellX = placed.originCellX;
2398 supportQuery.cellY = placed.originCellY;
2399 supportQuery.level = placed.level;
2400 supportQuery.rotationDeg = placed.rotationDeg;
2401 placed.supportInstanceIds = collectStructuralSupports(*world, *def, supportQuery);
2402
2403 writeOccupancy(*world, *def, placed, placed.instanceId);
2404 world->buildings()[placed.instanceId] = placed;
2405 world->instanceOrder_.push_back(placed.instanceId);
2406
2407 BuildingChangeEvent event;
2408 event.action = "place";
2409 event.worldId = world->getId();
2410 event.buildingId = placed.buildingId;
2411 event.instanceId = placed.instanceId;
2412 event.cellX = placed.originCellX;
2413 event.cellY = placed.originCellY;
2414 event.rotationDeg = placed.rotationDeg;
2415 event.worldX = placed.worldX;
2416 event.worldY = placed.worldY;
2417 event.elevation = placed.elevation;
2418 event.channel = placed.channel;
2419 if (world->publishEvents_) emit(std::move(event));
2420 return placed.instanceId;
2421}
2422
2423PlacementRestoreStatus PlacementSystem::restoreExact(PlacementWorld *world,
2424 const PlacedBuilding &placed,
2425 std::string *reason) {
2426 ensureBuiltins();
2427 if (!world || placed.instanceId <= 0 || world->hasBuilding(placed.instanceId)) {
2428 if (reason) *reason = "instance_id_conflict";
2429 return PlacementRestoreStatus::Rejected;
2430 }
2431 const BuildingDefinition *def = BuildingRegistry::find(placed.buildingId);
2432 if (!def) {
2433 if (reason) *reason = "unknown_building";
2434 return PlacementRestoreStatus::Rejected;
2435 }
2436 if (def->placementKind != placed.placementKind) {
2437 if (reason) *reason = "placement_domain_mismatch";
2438 return PlacementRestoreStatus::Rejected;
2439 }
2440 if (def->placementKind == "free") {
2441 if (!canPlaceFree(world, placed.buildingId, placed.worldX, placed.worldY, 0, reason,
2442 placed.level, placed.rotationDeg))
2443 return PlacementRestoreStatus::Rejected;
2444 PlacedBuilding restored = placed;
2445 restored.placementKind = "free";
2446 applyDefinitionFreeFootprint(restored, *def, *world);
2447 grid::worldToCell(*world->grid_, restored.worldX, restored.worldY,
2448 restored.originCellX, restored.originCellY, world->width_,
2449 world->height_);
2450 restored.supportInstanceIds.clear();
2451 world->buildings_[restored.instanceId] = restored;
2452 world->instanceOrder_.push_back(restored.instanceId);
2453 instanceCounter() = std::max(instanceCounter(), restored.instanceId);
2454 BuildingChangeEvent event;
2455 event.action = "place";
2456 event.worldId = world->getId();
2457 event.buildingId = restored.buildingId;
2458 event.instanceId = restored.instanceId;
2459 event.cellX = restored.originCellX;
2460 event.cellY = restored.originCellY;
2461 event.level = restored.level;
2462 event.rotationDeg = restored.rotationDeg;
2463 event.worldX = restored.worldX;
2464 event.worldY = restored.worldY;
2465 event.elevation = restored.elevation;
2466 event.channel = restored.channel;
2467 if (world->publishEvents_) emit(std::move(event));
2468 return PlacementRestoreStatus::Restored;
2469 }
2470 if (def->placementKind == "corner" || placed.placementKind == "corner") {
2471 if (def->placementKind != "corner" || placed.placementKind != "corner") {
2472 if (reason) *reason = "placement_domain_mismatch";
2473 return PlacementRestoreStatus::Rejected;
2474 }
2475 if (!canPlaceCorner(world, placed.buildingId, placed.corner.x, placed.corner.y, 0,
2476 reason, placed.level))
2477 return PlacementRestoreStatus::Rejected;
2478 PlacedBuilding restored = placed;
2479 restored.originCellX = restored.corner.x;
2480 restored.originCellY = restored.corner.y;
2481 PlacementQuery query;
2482 query.cellX = restored.corner.x;
2483 query.cellY = restored.corner.y;
2484 query.level = restored.level;
2485 restored.supportInstanceIds = collectStructuralSupports(*world, *def, query);
2486 auto &occupancy = world->cornerChannels(restored.level)[def->channel];
2487 const size_t required = size_t(world->width_ + 1) * size_t(world->height_ + 1);
2488 if (occupancy.size() != required) occupancy.assign(required, 0);
2489 occupancy[size_t(restored.corner.y) * size_t(world->width_ + 1) +
2490 size_t(restored.corner.x)] = restored.instanceId;
2491 world->buildings_[restored.instanceId] = restored;
2492 world->instanceOrder_.push_back(restored.instanceId);
2493 instanceCounter() = std::max(instanceCounter(), restored.instanceId);
2494 BuildingChangeEvent event;
2495 event.action = "place";
2496 event.worldId = world->getId();
2497 event.buildingId = restored.buildingId;
2498 event.instanceId = restored.instanceId;
2499 event.cellX = restored.corner.x;
2500 event.cellY = restored.corner.y;
2501 event.level = restored.level;
2502 event.worldX = restored.worldX;
2503 event.worldY = restored.worldY;
2504 event.elevation = restored.elevation;
2505 event.channel = restored.channel;
2506 if (world->publishEvents_) emit(std::move(event));
2507 return PlacementRestoreStatus::Restored;
2508 }
2509 if (def->placementKind == "edge" || placed.placementKind == "edge") {
2510 const char *direction = placed.edge.axis == EdgeAxis::Horizontal ? "north" : "west";
2511 if (!canPlaceEdge(world, placed.buildingId, placed.edge.x, placed.edge.y, direction, 0,
2512 reason, placed.level))
2513 return PlacementRestoreStatus::Rejected;
2514 PlacedBuilding restored = placed;
2515 restored.placementKind = "edge";
2516 const auto curveGroup = world->edgeCurveGroups_.find(restored.edgeCurveGroupId.value);
2517 if (!restored.edgeCurveGroupId || curveGroup == world->edgeCurveGroups_.end() ||
2518 std::find(curveGroup->second.instanceIds.begin(), curveGroup->second.instanceIds.end(),
2519 restored.instanceId) == curveGroup->second.instanceIds.end())
2520 restored.edgeCurveGroupId = {};
2521 // Support links are a derived cell-domain cache and must never be trusted from a snapshot.
2522 restored.supportInstanceIds.clear();
2523 auto &channels = world->edgeChannels(restored.edge.axis, restored.level);
2524 auto &occupancy = channels[def->channel];
2525 const size_t required = restored.edge.axis == EdgeAxis::Horizontal
2526 ? size_t(world->width_) * size_t(world->height_ + 1)
2527 : size_t(world->width_ + 1) * size_t(world->height_);
2528 if (occupancy.size() != required) occupancy.assign(required, 0);
2529 const size_t index = restored.edge.axis == EdgeAxis::Horizontal
2530 ? size_t(restored.edge.y) * size_t(world->width_) + size_t(restored.edge.x)
2531 : size_t(restored.edge.y) * size_t(world->width_ + 1) + size_t(restored.edge.x);
2532 occupancy[index] = restored.instanceId;
2533 world->buildings()[restored.instanceId] = restored;
2534 world->instanceOrder_.push_back(restored.instanceId);
2535 instanceCounter() = std::max(instanceCounter(), restored.instanceId);
2536 BuildingChangeEvent event;
2537 event.action = "place";
2538 event.worldId = world->getId();
2539 event.buildingId = restored.buildingId;
2540 event.instanceId = restored.instanceId;
2541 event.cellX = restored.edge.x;
2542 event.cellY = restored.edge.y;
2543 event.rotationDeg = restored.rotationDeg;
2544 event.worldX = restored.worldX;
2545 event.worldY = restored.worldY;
2546 event.elevation = restored.elevation;
2547 event.channel = restored.channel;
2548 if (world->publishEvents_) emit(std::move(event));
2549 return PlacementRestoreStatus::Restored;
2550 }
2551 const float rotation = normalizeRotation(placed.buildingId, placed.rotationDeg);
2552 if (!canPlaceElev(world, placed.buildingId, placed.originCellX, placed.originCellY,
2553 placed.elevation, rotation, 0, reason, placed.level))
2554 return PlacementRestoreStatus::Rejected;
2555 PlacedBuilding restored = placed;
2556 restored.rotationDeg = rotation;
2557 restored.channel = def->channel;
2558 if (restored.tags.empty()) restored.tags = def->tags;
2559 PlacementQuery supportQuery;
2560 supportQuery.cellX = restored.originCellX;
2561 supportQuery.cellY = restored.originCellY;
2562 supportQuery.level = restored.level;
2563 supportQuery.rotationDeg = restored.rotationDeg;
2564 restored.supportInstanceIds = collectStructuralSupports(*world, *def, supportQuery);
2565 writeOccupancy(*world, *def, restored, restored.instanceId);
2566 world->buildings()[restored.instanceId] = restored;
2567 world->instanceOrder_.push_back(restored.instanceId);
2568 instanceCounter() = std::max(instanceCounter(), restored.instanceId);
2569
2570 BuildingChangeEvent event;
2571 event.action = "place";
2572 event.worldId = world->getId();
2573 event.buildingId = restored.buildingId;
2574 event.instanceId = restored.instanceId;
2575 event.cellX = restored.originCellX;
2576 event.cellY = restored.originCellY;
2577 event.rotationDeg = restored.rotationDeg;
2578 event.worldX = restored.worldX;
2579 event.worldY = restored.worldY;
2580 event.elevation = restored.elevation;
2581 event.channel = restored.channel;
2582 if (world->publishEvents_) emit(std::move(event));
2583 return PlacementRestoreStatus::Restored;
2584}
2585
2586eve::Result<EdgeCurveGroup> PlacementSystem::restoreEdgeCurveGroupExact(
2588 const std::vector<PlacedBuilding> &members) {
2589 if (!world || !group.id || group.controlPoints.size() != 4 || group.subdivisions < 2 ||
2590 group.subdivisions > 4096 || members.empty() ||
2591 group.instanceIds.size() != members.size()) {
2594 "curve group restore requires a world, identity, four controls, valid subdivisions and matching members",
2595 {}, {}, "building.edge-curve-group.restore"));
2596 }
2597 std::unordered_set<int> memberIds;
2598 for (size_t index = 0; index < members.size(); ++index) {
2600 if (member.instanceId <= 0 || member.instanceId != group.instanceIds[index] ||
2601 member.placementKind != "edge" || member.buildingId != group.buildingId ||
2602 member.level != group.level || !memberIds.insert(member.instanceId).second) {
2605 "curve group members must be unique ordered edges with matching definition and level",
2606 std::to_string(member.instanceId), {}, "building.edge-curve-group.restore"));
2607 }
2608 }
2610 if (!std::isfinite(point.x) || !std::isfinite(point.y)) {
2612 eve::DiagnosticCode::InvalidArgument, "curve group controls must be finite", {},
2613 {}, "building.edge-curve-group.restore"));
2614 }
2615 }
2616 if ((!group.surfaceSamples.empty() &&
2617 group.surfaceSamples.size() != static_cast<size_t>(group.subdivisions + 1)) ||
2618 (group.surfaceSamples.empty() &&
2619 (!group.surfaceProviderName.empty() || !group.surfaceId.empty() ||
2620 group.surfaceRevision != 0))) {
2623 "curve group surface metadata requires exactly subdivisions plus one frames", {},
2624 {}, "building.edge-curve-group.restore"));
2625 }
2626 for (const EdgeCurveSurfaceSample &sample : group.surfaceSamples) {
2627 const float normalLength = std::sqrt(sample.normalX * sample.normalX +
2628 sample.normalY * sample.normalY +
2629 sample.normalZ * sample.normalZ);
2630 if (!std::isfinite(sample.worldX) || !std::isfinite(sample.worldY) ||
2631 !std::isfinite(sample.worldZ) || !std::isfinite(normalLength) ||
2632 normalLength <= 1e-5f) {
2635 "curve group surface frames require finite positions and non-zero normals", {},
2636 {}, "building.edge-curve-group.restore"));
2637 }
2638 }
2639
2640 std::unique_ptr<PlacementWorld> candidate = world->cloneState();
2641 if (!candidate || candidate->edgeCurveGroups_.contains(group.id.value)) {
2643 eve::DiagnosticCode::Conflict, "curve group identity is already in use",
2644 std::to_string(group.id.value), {}, "building.edge-curve-group.restore"));
2645 }
2646 for (const PlacedBuilding &member : members) {
2647 std::string reason;
2648 if (restoreExact(candidate.get(), member, &reason) != PlacementRestoreStatus::Restored) {
2651 "curve group member restore was rejected without destination mutation", reason,
2652 {}, "building.edge-curve-group.restore"));
2653 }
2654 }
2655 for (int instanceId : group.instanceIds)
2656 candidate->buildings_.at(instanceId).edgeCurveGroupId = group.id;
2657 candidate->edgeCurveGroups_.emplace(group.id.value, group);
2658 candidate->nextEdgeCurveGroupId_ =
2659 std::max(candidate->nextEdgeCurveGroupId_, group.id.value + 1);
2660 world->swapState(*candidate);
2662}
2663
2664void PlacementSystem::dissolveEdgeCurveGroup(PlacementWorld *world, EdgeCurveGroupId groupId) {
2665 if (!world || !groupId) return;
2666 const auto group = world->edgeCurveGroups_.find(groupId.value);
2667 if (group == world->edgeCurveGroups_.end()) return;
2668 for (int memberId : group->second.instanceIds) {
2669 const auto member = world->buildings_.find(memberId);
2670 if (member != world->buildings_.end() && member->second.edgeCurveGroupId == groupId)
2671 member->second.edgeCurveGroupId = {};
2672 }
2673 world->edgeCurveGroups_.erase(group);
2674}
2675
2676bool PlacementSystem::removeBuildingUnchecked(PlacementWorld *world, int instanceId) {
2677 if (!world || instanceId <= 0) return false;
2678 auto it = world->buildings().find(instanceId);
2679 if (it == world->buildings().end()) return false;
2680
2681 const EdgeCurveGroupId curveGroupId = it->second.edgeCurveGroupId;
2682 BuildingChangeEvent ev;
2683 ev.action = "remove";
2684 ev.worldId = world->getId();
2685 ev.buildingId = it->second.buildingId;
2686 ev.instanceId = instanceId;
2687 ev.cellX = it->second.originCellX;
2688 ev.cellY = it->second.originCellY;
2689 ev.rotationDeg = it->second.rotationDeg;
2690 ev.worldX = it->second.worldX;
2691 ev.worldY = it->second.worldY;
2692 ev.elevation = it->second.elevation;
2693 ev.channel = it->second.channel;
2694
2695 dissolveEdgeCurveGroup(world, curveGroupId);
2696 clearOccupancy(*world, instanceId);
2697 world->buildings().erase(it);
2698 auto &order = world->instanceOrder_;
2699 order.erase(std::remove(order.begin(), order.end(), instanceId), order.end());
2700 if (world->publishEvents_) emit(std::move(ev));
2701 return true;
2702}
2703
2704bool PlacementSystem::moveBuilding(PlacementWorld *world, int instanceId, int cellX, int cellY,
2705 float rotationDeg) {
2706 auto result = moveBuildingResult(world, instanceId, cellX, cellY, rotationDeg);
2707 return result.ok();
2708}
2709
2710bool PlacementSystem::removeBuilding(PlacementWorld *world, int instanceId) {
2711 if (!world || !world->hasBuilding(instanceId) || hasStructuralDependents(*world, instanceId))
2712 return false;
2713 return removeBuildingUnchecked(world, instanceId);
2714}
2715
2717PlacementSystem::removeBuildingCascadeResult(PlacementWorld *world, int instanceId) {
2718 if (!world || instanceId <= 0) {
2721 "cascade removal requires a world and positive instance id",
2722 std::to_string(instanceId), {}, "building.structure"));
2723 }
2724 if (!world->hasBuilding(instanceId)) {
2726 eve::DiagnosticCode::NotFound, "cascade removal source instance was not found",
2727 std::to_string(instanceId), {}, "building.structure"));
2728 }
2729 std::vector<int> ordered;
2730 std::unordered_set<int> visiting;
2731 std::unordered_set<int> visited;
2732 bool cycle = false;
2733 std::function<void(int)> collect = [&](int supportId) {
2734 if (visited.contains(supportId)) return;
2735 if (!visiting.insert(supportId).second) {
2736 cycle = true;
2737 return;
2738 }
2739 for (const auto &[candidateId, placed] : world->buildings_) {
2740 if (std::find(placed.supportInstanceIds.begin(), placed.supportInstanceIds.end(),
2741 supportId) != placed.supportInstanceIds.end())
2742 collect(candidateId);
2743 }
2744 visiting.erase(supportId);
2745 visited.insert(supportId);
2746 ordered.push_back(supportId);
2747 };
2748 collect(instanceId);
2749 if (cycle) {
2751 eve::DiagnosticCode::Conflict, "structural dependency cycle prevents removal",
2752 "support_cycle", {}, "building.structure"));
2753 }
2754
2756 receipt.removed.reserve(ordered.size());
2757 for (int id : ordered) receipt.removed.push_back(world->buildings_.at(id));
2758 const bool publishEvents = world->publishEvents_;
2759 world->publishEvents_ = false;
2760 for (int id : ordered) removeBuildingUnchecked(world, id);
2761 world->publishEvents_ = publishEvents;
2762 if (publishEvents) {
2763 for (const PlacedBuilding &removed : receipt.removed) {
2764 BuildingChangeEvent event;
2765 event.action = "remove";
2766 event.worldId = world->getId();
2767 event.buildingId = removed.buildingId;
2768 event.instanceId = removed.instanceId;
2769 event.cellX = removed.originCellX;
2770 event.cellY = removed.originCellY;
2771 event.level = removed.level;
2772 event.rotationDeg = removed.rotationDeg;
2773 event.worldX = removed.worldX;
2774 event.worldY = removed.worldY;
2775 event.elevation = removed.elevation;
2776 event.channel = removed.channel;
2777 emit(std::move(event));
2778 }
2779 }
2780 return eve::Result<StructuralRemovalReceipt>::success(std::move(receipt));
2781}
2782
2783int PlacementSystem::removeBuildingCascade(PlacementWorld *world, int instanceId) {
2784 auto result = removeBuildingCascadeResult(world, instanceId);
2785 return result.ok() ? int(result.value().removed.size()) : 0;
2786}
2787
2789PlacementSystem::rebuildStructuralLinksResult(PlacementWorld *world) {
2790 if (!world) {
2792 eve::DiagnosticCode::InvalidArgument, "structural rebuild requires a world", {}, {},
2793 "building.structure"));
2794 }
2795 auto candidate = world->cloneState();
2796 if (!candidate) {
2799 "failed to clone structural rebuild candidate", {}, {},
2800 "building.structure"));
2801 }
2802
2804 for (int id : candidate->instanceOrder_) {
2805 auto placedIt = candidate->buildings_.find(id);
2806 if (placedIt == candidate->buildings_.end()) continue;
2807 PlacedBuilding &placed = placedIt->second;
2808 ++receipt.inspectedCount;
2809 const BuildingDefinition *def = BuildingRegistry::find(placed.buildingId);
2810 if (!def) {
2813 "structural rebuild encountered an unknown building definition",
2814 std::to_string(id), {}, "building.structure"));
2815 }
2816
2817 std::vector<int> rebuilt;
2818 if (def->supportMode != "none") {
2819 PlacementQuery query;
2820 query.buildingId = placed.buildingId;
2821 query.cellX = placed.originCellX;
2822 query.cellY = placed.originCellY;
2823 query.level = placed.level;
2824 query.rotationDeg = placed.rotationDeg;
2825 std::string reason;
2826 if (!checkStructuralSupport(*candidate, *def, query, &reason)) {
2829 "structural rebuild found an invalid dependency",
2830 std::to_string(id) + ":" + reason, {},
2831 "building.structure"));
2832 }
2833 rebuilt = collectStructuralSupports(*candidate, *def, query);
2834 }
2835 if (rebuilt != placed.supportInstanceIds) {
2836 placed.supportInstanceIds = std::move(rebuilt);
2837 ++receipt.changedCount;
2838 }
2839 }
2840 world->swapState(*candidate);
2841 return eve::Result<StructuralLinkRebuildReceipt>::success(std::move(receipt));
2842}
2843
2845PlacementSystem::moveBuildingResult(PlacementWorld *world, int instanceId, int cellX, int cellY,
2846 float rotationDeg) {
2847 if (!world || instanceId <= 0) {
2849 eve::DiagnosticCode::InvalidArgument, "move requires a world and positive instance id",
2850 std::to_string(instanceId), {}, "building.edit"));
2851 }
2852 auto it = world->buildings().find(instanceId);
2853 if (it == world->buildings().end()) {
2855 eve::DiagnosticCode::NotFound, "move source instance was not found",
2856 std::to_string(instanceId), {}, "building.edit"));
2857 }
2858
2859 PlacedBuilding &pb = it->second;
2860 if (hasStructuralDependents(*world, instanceId)) {
2862 eve::DiagnosticCode::Conflict, "move would invalidate structural dependents",
2863 "support_in_use", {}, "building.structure"));
2864 }
2865 if (pb.placementKind != "cell") {
2868 "cell move cannot target a non-cell instance",
2869 std::to_string(instanceId), {}, "building.edit"));
2870 }
2871 const float rot =
2872 (rotationDeg < 0.f) ? pb.rotationDeg : normalizeRotation(pb.buildingId, rotationDeg);
2873 std::string reason;
2874 if (!canPlaceElev(world, pb.buildingId, cellX, cellY, pb.elevation, rot, instanceId, &reason,
2875 pb.level)) {
2877 eve::DiagnosticCode::Conflict, "move preflight rejected without mutation", reason, {},
2878 "building.edit"));
2879 }
2880
2881 const BuildingDefinition *def = BuildingRegistry::find(pb.buildingId);
2882 if (!def) {
2884 eve::DiagnosticCode::NotFound, "move definition was not found", pb.buildingId, {},
2885 "building.edit"));
2886 }
2887 PlacementEditReceipt receipt;
2888 receipt.before = pb;
2889
2891 ev.action = "move";
2892 ev.worldId = world->getId();
2893 ev.buildingId = pb.buildingId;
2894 ev.instanceId = instanceId;
2895 ev.otherCellX = pb.originCellX;
2896 ev.otherCellY = pb.originCellY;
2897 ev.cellX = cellX;
2898 ev.cellY = cellY;
2899 ev.rotationDeg = rot;
2900 float px = 0.f, py = 0.f;
2901 world->cellToWorldPlane(cellX, cellY, px, py);
2902 ev.worldX = px;
2903 ev.worldY = py;
2904 ev.elevation = pb.elevation;
2905 ev.channel = pb.channel;
2906
2907 clearOccupancy(*world, instanceId);
2908 pb.originCellX = cellX;
2909 pb.originCellY = cellY;
2910 world->cellToWorldPlane(cellX, cellY, pb.worldX, pb.worldY);
2911 pb.rotationDeg = rot;
2912 PlacementQuery supportQuery;
2913 supportQuery.cellX = pb.originCellX;
2914 supportQuery.cellY = pb.originCellY;
2915 supportQuery.level = pb.level;
2916 supportQuery.rotationDeg = pb.rotationDeg;
2917 pb.supportInstanceIds = collectStructuralSupports(*world, *def, supportQuery);
2918 writeOccupancy(*world, *def, pb, instanceId);
2919 receipt.after = pb;
2920 if (world->publishEvents_) emit(std::move(ev));
2921 return eve::Result<PlacementEditReceipt>::success(std::move(receipt));
2922}
2923
2925PlacementSystem::moveEdgeResult(PlacementWorld *world, int instanceId, int cellX, int cellY,
2926 const std::string &direction) {
2927 if (!world || instanceId <= 0) {
2930 "edge move requires a world and positive instance id", std::to_string(instanceId),
2931 {}, "building.edit"));
2932 }
2933 auto it = world->buildings().find(instanceId);
2934 if (it == world->buildings().end()) {
2936 eve::DiagnosticCode::NotFound, "edge move source instance was not found",
2937 std::to_string(instanceId), {}, "building.edit"));
2938 }
2939 PlacedBuilding &pb = it->second;
2940 if (pb.placementKind != "edge") {
2942 eve::DiagnosticCode::InvalidArgument, "edge move requires an edge instance",
2943 std::to_string(instanceId), {}, "building.edit"));
2944 }
2945 if (hasStructuralDependents(*world, instanceId)) {
2947 eve::DiagnosticCode::Conflict, "edge move would invalidate structural dependents",
2948 "support_in_use", {}, "building.structure"));
2949 }
2950 std::string reason;
2951 if (!canPlaceEdge(world, pb.buildingId, cellX, cellY, direction, instanceId, &reason,
2952 pb.level)) {
2954 eve::DiagnosticCode::Conflict, "edge move preflight rejected without mutation", reason,
2955 {}, "building.edit"));
2956 }
2957 auto addressResult = canonicalEdge(cellX, cellY, direction);
2958 if (!addressResult.ok())
2959 return eve::Result<PlacementEditReceipt>::failure(addressResult.status());
2960 const EdgeAddress target = std::move(addressResult).takeValue();
2961 const BuildingDefinition *def = BuildingRegistry::find(pb.buildingId);
2962 if (!def) {
2964 eve::DiagnosticCode::NotFound, "edge move definition was not found", pb.buildingId, {},
2965 "building.edit"));
2966 }
2967
2968 PlacementEditReceipt receipt;
2969 receipt.before = pb;
2970 BuildingChangeEvent event;
2971 event.action = "move";
2972 event.worldId = world->getId();
2973 event.buildingId = pb.buildingId;
2974 event.instanceId = instanceId;
2975 event.otherCellX = pb.edge.x;
2976 event.otherCellY = pb.edge.y;
2977 event.cellX = target.x;
2978 event.cellY = target.y;
2979 event.channel = pb.channel;
2980
2981 dissolveEdgeCurveGroup(world, pb.edgeCurveGroupId);
2982 clearOccupancy(*world, instanceId);
2983 pb.edge = target;
2984 pb.originCellX = target.x;
2985 pb.originCellY = target.y;
2986 float ax = 0.f, ay = 0.f, bx = 0.f, by = 0.f;
2987 world->cellToWorldPlane(target.x, target.y, ax, ay);
2988 world->cellToWorldPlane(target.x + (target.axis == EdgeAxis::Horizontal ? 1 : 0),
2989 target.y + (target.axis == EdgeAxis::Vertical ? 1 : 0), bx, by);
2990 pb.worldX = (ax + bx) * 0.5f;
2991 pb.worldY = (ay + by) * 0.5f;
2992 pb.rotationDeg = target.axis == EdgeAxis::Horizontal ? 0.f : 90.f;
2993 auto &channels = world->edgeChannels(target.axis, pb.level);
2994 auto &occupancy = channels[def->channel];
2995 const size_t required = target.axis == EdgeAxis::Horizontal
2996 ? size_t(world->width_) * size_t(world->height_ + 1)
2997 : size_t(world->width_ + 1) * size_t(world->height_);
2998 if (occupancy.size() != required) occupancy.assign(required, 0);
2999 const size_t index = target.axis == EdgeAxis::Horizontal
3000 ? size_t(target.y) * size_t(world->width_) + size_t(target.x)
3001 : size_t(target.y) * size_t(world->width_ + 1) + size_t(target.x);
3002 occupancy[index] = instanceId;
3003 event.rotationDeg = pb.rotationDeg;
3004 event.worldX = pb.worldX;
3005 event.worldY = pb.worldY;
3006 event.elevation = pb.elevation;
3007 receipt.after = pb;
3008 if (world->publishEvents_) emit(std::move(event));
3009 return eve::Result<PlacementEditReceipt>::success(std::move(receipt));
3010}
3011
3013PlacementSystem::moveCornerResult(PlacementWorld *world, int instanceId, int vertexX,
3014 int vertexY) {
3015 if (!world || instanceId <= 0) {
3018 "corner move requires a world and positive instance id", std::to_string(instanceId),
3019 {}, "building.edit"));
3020 }
3021 auto found = world->buildings_.find(instanceId);
3022 if (found == world->buildings_.end()) {
3024 eve::DiagnosticCode::NotFound, "corner move source instance was not found",
3025 std::to_string(instanceId), {}, "building.edit"));
3026 }
3027 PlacedBuilding &placed = found->second;
3028 if (placed.placementKind != "corner") {
3030 eve::DiagnosticCode::InvalidArgument, "corner move requires a corner instance",
3031 std::to_string(instanceId), {}, "building.edit"));
3032 }
3033 if (hasStructuralDependents(*world, instanceId)) {
3035 eve::DiagnosticCode::Conflict, "corner move would invalidate structural dependents",
3036 "support_in_use", {}, "building.structure"));
3037 }
3038 std::string reason;
3039 if (!canPlaceCorner(world, placed.buildingId, vertexX, vertexY, instanceId, &reason,
3040 placed.level)) {
3042 eve::DiagnosticCode::Conflict, "corner move preflight rejected without mutation",
3043 reason, {}, "building.edit"));
3044 }
3045 const BuildingDefinition *definition = BuildingRegistry::find(placed.buildingId);
3046 if (!definition) {
3048 eve::DiagnosticCode::NotFound, "corner move definition was not found",
3049 placed.buildingId, {}, "building.edit"));
3050 }
3051
3052 PlacementEditReceipt receipt;
3053 receipt.before = placed;
3054 clearOccupancy(*world, instanceId);
3055 placed.corner = CornerAddress{vertexX, vertexY};
3056 placed.originCellX = vertexX;
3057 placed.originCellY = vertexY;
3058 world->cellToWorldPlane(vertexX, vertexY, placed.worldX, placed.worldY);
3059 PlacementQuery query;
3060 query.cellX = vertexX;
3061 query.cellY = vertexY;
3062 query.level = placed.level;
3063 placed.supportInstanceIds = collectStructuralSupports(*world, *definition, query);
3064 auto &occupancy = world->cornerChannels(placed.level)[definition->channel];
3065 const size_t required = size_t(world->width_ + 1) * size_t(world->height_ + 1);
3066 if (occupancy.size() != required) occupancy.assign(required, 0);
3067 occupancy[size_t(vertexY) * size_t(world->width_ + 1) + size_t(vertexX)] = instanceId;
3068 receipt.after = placed;
3069
3070 BuildingChangeEvent event;
3071 event.action = "move";
3072 event.worldId = world->getId();
3073 event.buildingId = placed.buildingId;
3074 event.instanceId = instanceId;
3075 event.otherCellX = receipt.before.corner.x;
3076 event.otherCellY = receipt.before.corner.y;
3077 event.cellX = vertexX;
3078 event.cellY = vertexY;
3079 event.level = placed.level;
3080 event.worldX = placed.worldX;
3081 event.worldY = placed.worldY;
3082 event.elevation = placed.elevation;
3083 event.channel = placed.channel;
3084 if (world->publishEvents_) emit(std::move(event));
3085 return eve::Result<PlacementEditReceipt>::success(std::move(receipt));
3086}
3087
3089PlacementSystem::moveFreeResult(PlacementWorld *world, int instanceId, float worldX,
3090 float worldY, float elevation, float rotationDeg) {
3091 if (!world || instanceId <= 0) {
3094 "free move requires a world and positive instance id", std::to_string(instanceId),
3095 {}, "building.edit"));
3096 }
3097 auto found = world->buildings_.find(instanceId);
3098 if (found == world->buildings_.end() || found->second.placementKind != "free") {
3100 eve::DiagnosticCode::NotFound, "free move source instance was not found",
3101 std::to_string(instanceId), {}, "building.edit"));
3102 }
3103 PlacedBuilding &placed = found->second;
3104 std::string reason;
3105 if (!canPlaceFree(world, placed.buildingId, worldX, worldY, instanceId, &reason,
3106 placed.level, rotationDeg)) {
3108 eve::DiagnosticCode::Conflict, "free move preflight rejected without mutation",
3109 reason, {}, "building.edit"));
3110 }
3111 PlacementEditReceipt receipt;
3112 receipt.before = placed;
3113 placed.worldX = worldX;
3114 placed.worldY = worldY;
3115 placed.elevation = elevation;
3116 placed.rotationDeg = normalizeRotation(placed.buildingId, rotationDeg);
3117 grid::worldToCell(*world->grid_, worldX, worldY, placed.originCellX,
3118 placed.originCellY, world->width_, world->height_);
3119 receipt.after = placed;
3120 BuildingChangeEvent event;
3121 event.action = "move";
3122 event.worldId = world->getId();
3123 event.buildingId = placed.buildingId;
3124 event.instanceId = instanceId;
3125 event.cellX = placed.originCellX;
3126 event.cellY = placed.originCellY;
3127 event.level = placed.level;
3128 event.rotationDeg = placed.rotationDeg;
3129 event.worldX = placed.worldX;
3130 event.worldY = placed.worldY;
3131 event.elevation = placed.elevation;
3132 event.channel = placed.channel;
3133 if (world->publishEvents_) emit(std::move(event));
3134 return eve::Result<PlacementEditReceipt>::success(std::move(receipt));
3135}
3136
3138PlacementSystem::moveFreeSurfaceResult(PlacementWorld *world, int instanceId,
3139 const SurfacePatch &patch, float rotationDeg) {
3140 if (!world || instanceId <= 0) {
3143 "free surface move requires a world and positive instance id",
3144 std::to_string(instanceId), {}, "building.edit"));
3145 }
3146 auto found = world->buildings_.find(instanceId);
3147 if (found == world->buildings_.end() || found->second.placementKind != "free") {
3149 eve::DiagnosticCode::NotFound, "free surface move source instance was not found",
3150 std::to_string(instanceId), {}, "building.edit"));
3151 }
3152 PlacedBuilding &placed = found->second;
3153 const BuildingDefinition *def = BuildingRegistry::find(placed.buildingId);
3154 if (!def) {
3156 eve::DiagnosticCode::NotFound, "free surface move definition was not found",
3157 placed.buildingId, {}, "building.edit"));
3158 }
3159 const float planeX = patch.anchor.worldX;
3160 const float planeY = world->getGrid().plane == grid::GridPlane::XZ
3161 ? patch.anchor.worldZ
3162 : patch.anchor.worldY;
3163 const float elevation = world->getGrid().plane == grid::GridPlane::XZ
3164 ? patch.anchor.worldY
3165 : patch.anchor.worldZ;
3166 std::string reason;
3167 if (!canPlaceFree(world, placed.buildingId, planeX, planeY, instanceId, &reason,
3168 placed.level, rotationDeg) ||
3170 (def->maxSurfaceHeightDelta >= 0.f &&
3171 patch.heightDelta > def->maxSurfaceHeightDelta)) {
3172 if (reason.empty())
3173 reason = patch.maxSlopeDegrees > def->maxSurfaceSlopeDegrees
3174 ? "surface_slope"
3175 : "surface_height_delta";
3178 "free surface move preflight rejected without mutation", reason, {},
3179 "building.edit"));
3180 }
3181
3182 PlacementEditReceipt receipt;
3183 receipt.before = placed;
3184 placed.worldX = planeX;
3185 placed.worldY = planeY;
3186 placed.elevation = elevation;
3187 placed.rotationDeg = normalizeRotation(placed.buildingId, rotationDeg);
3188 grid::worldToCell(*world->grid_, planeX, planeY, placed.originCellX,
3189 placed.originCellY, world->width_, world->height_);
3190 placed.surfaceId = patch.anchor.surfaceId;
3191 placed.surfaceRevision = patch.anchor.surfaceRevision;
3192 placed.surfaceNormalX = patch.anchor.normalX;
3193 placed.surfaceNormalY = patch.anchor.normalY;
3194 placed.surfaceNormalZ = patch.anchor.normalZ;
3195 placed.surfaceTangentX = patch.anchor.tangentX;
3196 placed.surfaceTangentY = patch.anchor.tangentY;
3197 placed.surfaceTangentZ = patch.anchor.tangentZ;
3198 placed.surfaceSampleCount = static_cast<int>(patch.samples.size());
3199 placed.surfaceMaxSlopeDegrees = patch.maxSlopeDegrees;
3200 placed.surfaceHeightDelta = patch.heightDelta;
3201 receipt.after = placed;
3202
3203 BuildingChangeEvent event;
3204 event.action = "move";
3205 event.worldId = world->getId();
3206 event.buildingId = placed.buildingId;
3207 event.instanceId = instanceId;
3208 event.cellX = placed.originCellX;
3209 event.cellY = placed.originCellY;
3210 event.level = placed.level;
3211 event.rotationDeg = placed.rotationDeg;
3212 event.worldX = placed.worldX;
3213 event.worldY = placed.worldY;
3214 event.elevation = placed.elevation;
3215 event.channel = placed.channel;
3216 if (world->publishEvents_) emit(std::move(event));
3217 return eve::Result<PlacementEditReceipt>::success(std::move(receipt));
3218}
3219
3221PlacementSystem::replaceBuildingResult(PlacementWorld *world, int instanceId,
3222 const std::string &replacementBuildingId) {
3223 if (!world || instanceId <= 0 || replacementBuildingId.empty()) {
3226 "replace requires a world, positive instance id and replacement definition",
3227 replacementBuildingId, {}, "building.edit"));
3228 }
3229 auto it = world->buildings().find(instanceId);
3230 if (it == world->buildings().end()) {
3232 eve::DiagnosticCode::NotFound, "replace source instance was not found",
3233 std::to_string(instanceId), {}, "building.edit"));
3234 }
3235 PlacedBuilding &pb = it->second;
3236 const BuildingDefinition *replacement = BuildingRegistry::find(replacementBuildingId);
3237 if (!replacement) {
3239 eve::DiagnosticCode::NotFound, "replacement definition was not found",
3240 replacementBuildingId, {}, "building.edit"));
3241 }
3242 const bool sourceEdge = pb.placementKind == "edge";
3243 const bool sourceCorner = pb.placementKind == "corner";
3244 const bool sourceFree = pb.placementKind == "free";
3245 if (pb.placementKind != replacement->placementKind) {
3248 "replacement must use the same placement domain as the source",
3249 replacementBuildingId, {}, "building.edit"));
3250 }
3251 if (hasStructuralDependents(*world, instanceId) &&
3252 !replacementPreservesStructuralDependents(*world, instanceId, *replacement)) {
3254 eve::DiagnosticCode::Conflict, "replace would invalidate structural dependents",
3255 "support_in_use", {}, "building.structure"));
3256 }
3257
3258 std::string reason;
3259 if (sourceEdge) {
3260 const char *direction = pb.edge.axis == EdgeAxis::Horizontal ? "north" : "west";
3261 if (!canPlaceEdge(world, replacementBuildingId, pb.edge.x, pb.edge.y, direction, instanceId,
3262 &reason, pb.level)) {
3264 eve::DiagnosticCode::Conflict, "replace preflight rejected without mutation", reason,
3265 {}, "building.edit"));
3266 }
3267 } else if (sourceCorner) {
3268 if (!canPlaceCorner(world, replacementBuildingId, pb.corner.x, pb.corner.y, instanceId,
3269 &reason, pb.level)) {
3271 eve::DiagnosticCode::Conflict, "replace preflight rejected without mutation",
3272 reason, {}, "building.edit"));
3273 }
3274 } else if (sourceFree) {
3275 if (!canPlaceFree(world, replacementBuildingId, pb.worldX, pb.worldY, instanceId,
3276 &reason, pb.level, pb.rotationDeg)) {
3278 eve::DiagnosticCode::Conflict, "replace preflight rejected without mutation",
3279 reason, {}, "building.edit"));
3280 }
3281 } else if (!canPlaceElev(world, replacementBuildingId, pb.originCellX, pb.originCellY,
3282 pb.elevation, pb.rotationDeg, instanceId, &reason, pb.level)) {
3284 eve::DiagnosticCode::Conflict, "replace preflight rejected without mutation", reason, {},
3285 "building.edit"));
3286 }
3287
3288 PlacementEditReceipt receipt;
3289 receipt.before = pb;
3290 BuildingChangeEvent event;
3291 event.action = "replace";
3292 event.worldId = world->getId();
3293 event.otherBuildingId = pb.buildingId;
3294 event.buildingId = replacementBuildingId;
3295 event.instanceId = instanceId;
3296 event.otherCellX = pb.originCellX;
3297 event.otherCellY = pb.originCellY;
3298
3299 dissolveEdgeCurveGroup(world, pb.edgeCurveGroupId);
3300 clearOccupancy(*world, instanceId);
3301 pb.buildingId = replacementBuildingId;
3302 pb.placementKind = replacement->placementKind;
3303 pb.channel = replacement->channel;
3304 pb.tags = replacement->tags;
3305 if (sourceFree) {
3306 applyDefinitionFreeFootprint(pb, *replacement, *world);
3307 }
3308 pb.rotationDeg = sourceEdge ? pb.rotationDeg
3309 : normalizeRotation(replacementBuildingId, pb.rotationDeg);
3310 if (sourceEdge) {
3311 pb.supportInstanceIds.clear();
3312 auto &channels = world->edgeChannels(pb.edge.axis, pb.level);
3313 auto &occupancy = channels[replacement->channel];
3314 const size_t required = pb.edge.axis == EdgeAxis::Horizontal
3315 ? size_t(world->width_) * size_t(world->height_ + 1)
3316 : size_t(world->width_ + 1) * size_t(world->height_);
3317 if (occupancy.size() != required) occupancy.assign(required, 0);
3318 const size_t index = pb.edge.axis == EdgeAxis::Horizontal
3319 ? size_t(pb.edge.y) * size_t(world->width_) + size_t(pb.edge.x)
3320 : size_t(pb.edge.y) * size_t(world->width_ + 1) + size_t(pb.edge.x);
3321 occupancy[index] = instanceId;
3322 } else if (sourceCorner) {
3323 PlacementQuery supportQuery;
3324 supportQuery.cellX = pb.corner.x;
3325 supportQuery.cellY = pb.corner.y;
3326 supportQuery.level = pb.level;
3327 pb.supportInstanceIds = collectStructuralSupports(*world, *replacement, supportQuery);
3328 auto &occupancy = world->cornerChannels(pb.level)[replacement->channel];
3329 const size_t required = size_t(world->width_ + 1) * size_t(world->height_ + 1);
3330 if (occupancy.size() != required) occupancy.assign(required, 0);
3331 occupancy[size_t(pb.corner.y) * size_t(world->width_ + 1) + size_t(pb.corner.x)] =
3332 instanceId;
3333 } else {
3334 PlacementQuery supportQuery;
3335 supportQuery.cellX = pb.originCellX;
3336 supportQuery.cellY = pb.originCellY;
3337 supportQuery.level = pb.level;
3338 supportQuery.rotationDeg = pb.rotationDeg;
3339 pb.supportInstanceIds = collectStructuralSupports(*world, *replacement, supportQuery);
3340 writeOccupancy(*world, *replacement, pb, instanceId);
3341 }
3342 event.cellX = pb.originCellX;
3343 event.cellY = pb.originCellY;
3344 event.rotationDeg = pb.rotationDeg;
3345 event.worldX = pb.worldX;
3346 event.worldY = pb.worldY;
3347 event.elevation = pb.elevation;
3348 event.channel = pb.channel;
3349 receipt.after = pb;
3350 if (world->publishEvents_) emit(std::move(event));
3351 return eve::Result<PlacementEditReceipt>::success(std::move(receipt));
3352}
3353
3354void PlacementSystem::clearBuildings(PlacementWorld *world) {
3355 if (!world) return;
3356 // Collect ids first so remove events fire consistently.
3357 std::vector<int> ids = world->instanceOrder_;
3358 for (int id : ids)
3359 if (world->hasBuilding(id)) removeBuildingCascade(world, id);
3360 world->buildings().clear();
3361 world->edgeCurveGroups_.clear();
3362 world->instanceOrder_.clear();
3363 for (auto &kv : world->allChannels()) {
3364 std::fill(kv.second.begin(), kv.second.end(), 0);
3365 }
3366 for (auto &kv : world->horizontalEdgeChannels_) std::fill(kv.second.begin(), kv.second.end(), 0);
3367 for (auto &kv : world->verticalEdgeChannels_) std::fill(kv.second.begin(), kv.second.end(), 0);
3368 world->cellChannelsByLevel_.clear();
3369 world->horizontalEdgesByLevel_.clear();
3370 world->verticalEdgesByLevel_.clear();
3371}
3372
3373} // namespace eve::building
LogicalId target
double value
Duration start
float w
Definition AnimClip.cpp:738
float y
Definition AnimClip.cpp:738
float x
Definition AnimClip.cpp:738
float z
Definition AnimClip.cpp:738
std::map< std::string, std::vector< Key >, std::less<> > channels
const std::string & s
int mask
float rotationDegrees
building::EdgeCurveGroup group
float cx
Definition CardTypes.cpp:33
float cy
Definition CardTypes.cpp:34
int bz
Definition CaveMesh.cpp:114
int ax
Definition CaveMesh.cpp:113
int ay
Definition CaveMesh.cpp:113
int bx
Definition CaveMesh.cpp:114
float length
Definition CaveMesh.cpp:94
Vec3 anchor
Definition CaveMesh.cpp:90
int az
Definition CaveMesh.cpp:113
int by
Definition CaveMesh.cpp:114
float py
float nx
float ny
glm::vec4 p[6]
Stable, structured diagnostics shared by engine modules.
std::string message
const GltfImportRequest & request
std::uint32_t key
glm::uvec4 ids
float u
Definition Grass.cpp:233
float area
Definition Grass.cpp:62
Pure cell/world projection helpers for GridConfig.
std::array< double, 10 > q
double r
float v
std::int32_t second
std::int32_t c
std::int32_t first
float elevation
int h
std::vector< Colorf > px
std::uint32_t height
std::array< float, 4 > rotation
bool required
std::string name
MeleePoint3 b
Definition MeleeHit.cpp:41
MeleePoint3 a
Definition MeleeHit.cpp:40
std::vector< std::int32_t > order
int level
World3D * world
std::array< PixelCell, kPixelChunkSize *kPixelChunkSize > cells
std::vector< Point > vertices
float radius
float rotationRadians
float halfWidth
float halfHeight
std::string path
Definition PlayHost.cpp:110
std::string id
Definition PlayHost.cpp:108
bool hit
float d
int steps
float t
RoadLaneDirection direction
const RoadEdge * edge
float axisY
Definition RockMesh.cpp:22
float axisX
Definition RockMesh.cpp:22
bool found
int removed
float dy
float dx
std::uint32_t count
Cell cell
bool placed
Battle::Events events
std::size_t cursor
float step
Definition TreeMesh.cpp:314
float size
Definition TreeMesh.cpp:156
float(ui::Theme::* member)[4]
std::map< std::string, std::span< const std::uint8_t > > members
std::set< std::string > visiting
uint32_t index
const UnitySourceAsset & source
double oy
double ox
std::vector< int > edges
int covered
glm::vec3 point
static Diagnostic error(DiagnosticCode code, std::string message, std::string path={}, DiagnosticDetails details={}, std::string source={})
Construct an error diagnostic with the standard error severity.
Definition Diagnostic.h:125
Move-only operation result carrying either a value or Status.
Definition Result.h:155
static Result success(T value)
Construct a successful result owning value.
Definition Result.h:164
static Result failure(Status status)
Construct a failed result from a structured status.
Definition Result.h:175
Structured status and zero or more diagnostics for an operation.
Definition Status.h:68
static Status failure(StatusCode code, Diagnostic diagnostic)
Construct a failed status with one diagnostic.
Definition Status.h:84
static Status success(StatusCode code=StatusCode::Ok)
Construct a successful status with an explicit non-error outcome.
Definition Status.h:81
static const BuildingDefinition * find(const std::string &id)
Finds .
EVENGINE_API_WORLD public API.
Definition Ghost.h:16
float getElevation() const
Returns the elevation.
Definition Ghost.h:45
int getCellY() const
Returns the cell y.
Definition Ghost.h:34
float getWorldY() const
Returns the world y.
Definition Ghost.h:41
int getCellX() const
Returns the cell x.
Definition Ghost.h:32
bool validate(PlacementWorld *world)
对当前姿态做校验,写入 valid_/reason_。
Definition Ghost.cpp:270
float getWorldX() const
Returns the world x.
Definition Ghost.h:39
float getRotationDeg() const
Returns the rotation deg.
Definition Ghost.h:50
std::string getBuildingId() const
Returns the building id.
Definition Ghost.h:27
EVENGINE_API_WORLD public API.
std::function< bool(const PlacementWorld &world, const PlacementQuery &q, std::string *reason)> ValidateFn
static SnapResult snap(const PlacementWorld &world, const std::string &buildingId, float worldX, float worldY)
static void effectiveFootprint(const BuildingDefinition &def, float rotationDeg, int *outW, int *outH)
旋转后的占地宽高(cardinal 90/270 交换)。
std::function< bool(const PlacementWorld &world, float x, float y, PlacementHit *hit)> SurfaceFn
static std::vector< std::string > surfaceNames()
static void registerSurfaceProvider(const std::string &name, SurfaceProviderFn fn)
Register or replace a structured surface provider.
static void registerSurface(const std::string &name, SurfaceFn fn)
static SnapResult snap3D(const PlacementWorld &world, const std::string &buildingId, float worldX, float worldY, float worldZ)
static eve::Result< void > registerHeightfieldSurface(const std::string &name, std::shared_ptr< const HeightfieldSurface > surface)
Register an immutable built-in heightfield and retain it until unregistration.
static PlacementSystem & inst()
Process singleton holding all PlacementSystem state.
static const std::vector< BuildingChangeEvent > & events()
static bool surfaceHit(const PlacementWorld &world, const std::string &name, float x, float y, PlacementHit *hit)
std::function< SnapResult(const PlacementWorld &world, float worldX, float worldY)> SnapFn
static void unregisterChangeHook(const std::string &name)
static void setPlaneSurfaceHeight(float h)
std::function< eve::Result< PlacementHit >(const PlacementWorld &world, float x, float y)> SurfaceProviderFn
Canonical surface provider contract.
static void ensureBuiltins()
确保内置规则已注册(模块首次使用时自动调用)。
static bool hasSurface(const std::string &name)
std::function< void(const BuildingChangeEvent &ev)> ChangeHook
static void pushEvent(BuildingChangeEvent ev)
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< SurfacePatch > sampleSurfacePatch(const PlacementWorld &world, const std::string &buildingId, const std::string &surfaceName, float x, float y, float rotationDegrees=0.f)
Sample the occupied cells of a rotated footprint relative to an exact anchor hit.
static bool hasSnapRule(const std::string &name)
static void unregisterSurface(const std::string &name)
static bool hasChangeHook(const std::string &name)
static void registerSnapRule(const std::string &name, SnapFn fn)
static bool hasValidateRule(const std::string &name)
static eve::Result< void > registerStaticMeshSurface(const std::string &name, std::shared_ptr< const StaticMeshSurface > surface)
Register an immutable BVH-accelerated triangle mesh and retain its snapshot.
static void pollEvents(std::vector< BuildingChangeEvent > &out)
static void unregisterValidateRule(const std::string &name)
static void unregisterSnapRule(const std::string &name)
static void registerChangeHook(const std::string &name, ChangeHook fn)
static SnapResult snapWithMode(const PlacementWorld &world, const std::string &mode, float worldX, float worldY)
static void registerValidateRule(const std::string &name, ValidateFn fn)
格子型建筑放置世界(脚本可直接操作)。
建筑放置模块入口:定义 / 放置世界 / 鬼影 / 变更事件的脚本绑定点。 设计文档:docs/dev/建筑放置系统设计.md
Definition Building.cpp:7
PlacementRestoreStatus
Outcome of restoring an exact placement snapshot.
float lengthSquared(Vec3 value)
Length squared.
void worldToCell(const GridConfig &cfg, float px, float py, int &cx, int &cy, int mapW, int mapH)
Maps planar coordinates to the nearest cell. Staggered/hex layouts use a bounded nearest-neighbor sea...
void rotatedFootprintSize(int w, int h, const std::vector< uint8_t > &mask, int steps, bool hexMode, int &outW, int &outH)
Axis-aligned size of a footprint after rotation.
void foreachRotatedFootprint(int w, int h, const std::vector< uint8_t > &mask, int steps, bool hexMode, const std::function< void(int lx, int ly)> &fn)
Enumerates local cells of a rotated footprint.
bool pointInPolygon(const Vec2 &p, const Polygon &poly)
Point-in-polygon test (ray casting; boundary counts as inside).
double cross(const Vec2 &a, const Vec2 &b)
Cross.
Definition UrbanTypes.h:36
SettlementPipeline::Stage fn
一次成功放置变更的事件(供脚本 poll / C++ hook)。
std::string action
place / remove / move / rotate
建筑模板(进程级注册表中的定义)。
std::string placementKind
Placement domain: cell (default), edge, corner, or free.
std::vector< float > freeFootprintVertices
Optional convex local polygon as x/y pairs in grid-cell units, centered on the free anchor.
std::string channel
占地格子尺寸。
std::string supportMode
Structural support policy: none, cell_below, or corner_below.
float freeFootprintHeightCells
Optional oriented-box height in grid-cell units; positive width and height select OBB collision.
float maxSurfaceHeightDelta
Maximum elevation range across a sampled footprint; negative disables it.
std::string rotationMode
none | cardinal | free。
float maxSurfaceSlopeDegrees
Maximum angle between a footprint normal and the grid-plane up axis.
std::string connectionGroup
Edge topology compatibility group; empty falls back to building id.
std::vector< uint8_t > footprintMask
可选占地掩码,长度 footprintW*footprintH;空表示实心矩形。行主序、原点在最小 x/y。
std::vector< std::string > tags
float freeFootprintWidthCells
Optional oriented-box width in grid-cell units; positive width and height select OBB collision.
Canonical address of one logical grid vertex used by corner objects.
Canonical address of one grid edge.
One cubic Bezier control point in logical grid-vertex space.
Strong world-local identity of one authoritative edge-curve group.
World-owned authoritative description and membership of one committed edge curve.
std::vector< EdgeCurveSurfaceSample > surfaceSamples
Owning subdivisions+1 centerline frames for deterministic surface conformance.
std::string surfaceId
Stable provider-returned surface identity shared by every committed sample.
std::vector< EdgeCurveControlPoint > controlPoints
std::string surfaceProviderName
Provider key used to author surfaceSamples; empty means planar projection.
std::vector< int > instanceIds
One committed 3D centerline frame sampled from a custom placement surface.
已放置的建筑实例。
std::vector< std::string > tags
EdgeAddress edge
Canonical edge address when placementKind is edge.
EdgeCurveGroupId edgeCurveGroupId
Optional link to a world-owned curve group; zero means an ordinary edge.
int level
Authoritative discrete floor coordinate used by occupancy and topology.
std::string placementKind
Placement domain snapshot: cell, edge, corner, or free.
std::vector< int > supportInstanceIds
Derived support links rebuilt from authoritative level/address data.
CornerAddress corner
Canonical grid vertex when placementKind is corner.
Owning before/after receipt produced by one committed placement edit.
校验上下文:传给可插拔规则。
int level
Discrete floor whose occupancy and adjacency are queried.
Owning same-identity surface frames sampled along an analytic edge curve.
std::vector< EdgeCurveSurfaceSample > samples
Atomic result of placing one axis-aligned continuous edge line.
Owning validated edge addresses for one non-mutating path preview.
Atomic placement receipt shared by every built-in pattern kind.
Owning non-mutating expansion of an edge or area pattern.
Owning, renderer-neutral request for one atomic placement pattern.
One sampled point and orthonormal frame on a placement surface.
Surface samples and continuity metrics for one rotated building footprint.
std::vector< SurfacePatchSample > samples
Result metadata for an atomic rebuild of all derived structural links.
Owning record of one committed dependent-first structural removal.
std::vector< PlacedBuilding > removed
Removed instances in the exact order in which occupancy was released.