14#include <unordered_set>
20float wrapDeg(
float deg) {
21 deg = std::fmod(deg, 360.f);
22 if (deg < 0.f) deg += 360.f;
26int cardinalQuarter(
float rotationDeg) {
27 const float d = wrapDeg(rotationDeg);
28 int q = int(std::lround(
d / 90.f)) % 4;
33float length3(
float x,
float y,
float z) {
return std::sqrt(
x *
x +
y *
y +
z *
z); }
48 bool isPolygon()
const {
return vertices.size() >= 3; }
51FreeFootprint definitionFreeFootprint(
const BuildingDefinition &definition,
52 const PlacementWorld &
world,
float x,
float 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));
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}};
76 result.rotationRadians = wrapDeg(rotationDeg) * 3.14159265358979323846f / 180.f;
80void applyDefinitionFreeFootprint(PlacedBuilding &
placed,
const BuildingDefinition &definition,
81 const PlacementWorld &
world) {
82 const FreeFootprint footprint =
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);
97FreeFootprint placedFreeFootprint(
const PlacedBuilding &
placed) {
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}};
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);
122 result.push_back({footprint.x +
point.x *
c -
point.y *
s,
128 const auto vertices = worldVertices(polygon);
130 for (
size_t i = 0; i <
vertices.size(); ++i) {
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;
137 else if (cross * winding < 0.f)
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;
148 auto circlePolygonOverlap = [](
const FreeFootprint &circle,
const FreeFootprint &polygon) {
150 const auto vertices = worldVertices(polygon);
151 const float radiusSquared = circle.radius * circle.radius;
152 for (
size_t i = 0; i <
vertices.size(); ++i) {
155 const float edgeX =
b.x -
a.x;
156 const float edgeY =
b.y -
a.y;
159 ? std::clamp(((circle.x -
a.x) * edgeX +
160 (circle.y -
a.y) * edgeY) /
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;
172 if (!
a.isPolygon())
return circlePolygonOverlap(
a,
b);
173 if (!
b.isPolygon())
return circlePolygonOverlap(
b,
a);
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) {
181 const float axisX = -(
q.y -
p.y);
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);
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);
197 if (aMax <= bMin || bMax <= aMin)
return false;
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;
210void cross3(
float ax,
float ay,
float az,
float bx,
float by,
float bz,
float &
x,
float &
y,
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)) {
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;
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) {
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);
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);
267std::unordered_map<std::string, PlacementSystem::ValidateFn> &PlacementSystem::validateRules() {
268 return inst().validateRules_;
271std::unordered_map<std::string, PlacementSystem::SnapFn> &PlacementSystem::snapRules() {
272 return inst().snapRules_;
275std::unordered_map<std::string, PlacementSystem::ChangeHook> &PlacementSystem::changeHooks() {
276 return inst().changeHooks_;
279std::unordered_map<std::string, PlacementSystem::SurfaceFn> &PlacementSystem::surfaces() {
280 return inst().surfaces_;
283std::unordered_map<std::string, PlacementSystem::SurfaceProviderFn> &
284PlacementSystem::surfaceProviders() {
285 return inst().surfaceProviders_;
288std::vector<BuildingChangeEvent> &PlacementSystem::eventQueue() {
289 return inst().eventQueue_;
292int &PlacementSystem::instanceCounter() {
293 return inst().instanceCounter_;
296bool &PlacementSystem::builtinsReady() {
297 return inst().builtinsReady_;
301 if (
name.empty() || !
fn)
return;
302 validateRules()[
name] = std::move(
fn);
306 validateRules().erase(
name);
311 return validateRules().count(
name) > 0;
315 if (
name.empty() || !
fn)
return;
316 snapRules()[
name] = std::move(
fn);
323 return snapRules().count(
name) > 0;
327 if (
name.empty() || !
fn)
return;
328 changeHooks()[
name] = std::move(
fn);
334 return changeHooks().count(
name) > 0;
338 if (
name.empty() || !
fn)
return;
346 name, {},
"building.surface"));
353 if (
name.empty() || !
fn)
return;
354 surfaceProviders()[
name] = std::move(
fn);
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"));
367 return surface->sample(
world,
x,
y);
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"));
382 return surface->sample(
world,
x,
y);
388 surfaces().erase(
name);
389 surfaceProviders().erase(
name);
394 return surfaceProviders().count(
name) > 0;
399 if (!
hit)
return false;
401 if (!result.ok())
return false;
402 *
hit = std::move(result).takeValue();
410 const std::string resolvedName =
name.empty() ?
"plane" :
name;
411 auto it = surfaceProviders().find(resolvedName);
412 if (it == surfaceProviders().
end() || !it->second) {
415 resolvedName, {},
"building.surface"));
417 auto result = it->second(
world,
x,
y);
420 if (!normalizeSurfaceFrame(
hit)) {
423 "placement surface returned a non-finite position or zero normal", resolvedName,
424 {},
"building.surface"));
426 if (
hit.surfaceId.empty())
hit.surfaceId = resolvedName;
432 const std::string &surfaceName,
float x,
float y,
438 {},
"building.surface-patch"));
446 patch.
anchor = std::move(anchorResult).takeValue();
449 world.cellToWorldPlane(snapped.
cellX, snapped.
cellY, originX, originY);
451 float minHeight = std::numeric_limits<float>::max();
452 float maxHeight = std::numeric_limits<float>::lowest();
456 return std::acos(std::clamp(upDot, -1.f, 1.f)) * 180.f / 3.14159265358979323846f;
461 const auto appendSample = [&](
float sampleX,
float sampleY,
int cellX,
int cellY,
463 if (failed)
return false;
471 failureStatus = sample.status();
474 hit = std::move(sample).takeValue();
481 "footprint crosses a surface identity or revision boundary",
hit.surfaceId,
482 {},
"building.surface-patch"));
487 minHeight = std::min(minHeight,
height);
488 maxHeight = std::max(maxHeight,
height);
490 patch.
samples.push_back({cellX, cellY, std::move(
hit)});
497 const FreeFootprint footprint = definitionFreeFootprint(
500 for (
const auto &
point : worldVertices(footprint)) {
505 if (!appendSample(
point.x,
point.y, cellX, cellY,
false))
break;
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);
523 buildingId, {},
"building.surface-patch"));
531 std::vector<std::string> names;
532 names.reserve(surfaceProviders().
size());
533 for (
const auto &kv : surfaceProviders()) names.push_back(kv.first);
541float &PlacementSystem::planeSurfaceHeight() {
542 return inst().planeSurfaceHeight_;
546 if (builtinsReady())
return;
547 builtinsReady() =
true;
556 world.cellToWorldPlane(
cx,
cy,
r.worldX,
r.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);
574 r.cellX =
world.worldToCellX(worldX);
575 r.cellY =
world.worldToCellY(worldY);
581 std::string *reason) {
584 if (reason) *reason =
"unknown_building";
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;
596 std::string *reason) {
599 if (reason) *reason =
"unknown_building";
602 return checkBoundsAndOccupancy(
world, *def,
q,
false, reason);
606 std::string *reason) {
609 if (reason) *reason =
"unknown_building";
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;
622 const float h = planeSurfaceHeight();
635 hit.surfaceId =
"plane";
646 eventQueue().clear();
654 eventQueue().push_back(ev);
655 for (
auto &kv : changeHooks()) {
656 if (kv.second) kv.second(ev);
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");
668 if (it != rules.end() && it->second)
return it->second(
world, worldX, worldY);
671 r.cellY =
world.worldToCellY(worldY);
672 r.worldX =
world.cellToWorldX(
r.cellX);
673 r.worldY =
world.cellToWorldY(
r.cellY);
678 float worldX,
float worldY) {
680 std::string mode =
world.getSnapMode();
682 if (!def->snapMode.empty()) mode = def->snapMode;
688 float worldX,
float worldY,
float worldZ) {
704 const std::string mode = def ? def->
rotationMode :
"cardinal";
705 if (mode ==
"none")
return 0.f;
706 if (mode ==
"free")
return wrapDeg(rotationDeg);
708 const float d = wrapDeg(rotationDeg);
709 const int steps = int(std::lround(
d / 60.f)) % 6;
713 return float(cardinalQuarter(rotationDeg) * 90);
721 const int steps = int(std::lround(wrapDeg(rotationDeg) / 60.f)) % 6;
725 const int q = cardinalQuarter(rotationDeg);
726 if (
q == 1 ||
q == 3) std::swap(
w,
h);
733 int originCellY,
float rotationDeg,
734 const std::function<
bool(
int cx,
int cy)> &
fn) {
735 if (!
fn)
return false;
738 hex ? int(std::lround(wrapDeg(rotationDeg) / 60.f)) % 6 : cardinalQuarter(rotationDeg);
741 [&](
int lx,
int ly) {
742 if (!fn(originCellX + lx, originCellY + ly)) ok = false;
750 std::string *reason) {
753 if (!world.inBounds(cx, cy)) {
754 if (reason) *reason =
"out_of_bounds";
758 if (checkOccupancy) {
760 if (occ != 0 && occ !=
q.excludeInstanceId) {
761 if (reason) *reason =
"occupied";
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;
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";
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";
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;
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);
811 auto isFootprint = [&](
int x,
int y) {
812 return std::find(
cells.begin(),
cells.end(), std::make_pair(
x,
y)) !=
cells.end();
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};
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;
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);
835 if (
const BuildingDefinition *odef =
836 BuildingRegistry::find(pb.buildingId)) {
837 hit = odef->hasTag(def.requireAdjacentTag);
840 if (
hit) tagOk =
true;
844 if (needTerrain && !terrainOk) {
845 if (
world.getTerrain(
nx,
ny) == def.requireAdjacentTerrain) terrainOk =
true;
847 if (tagOk && terrainOk)
return true;
851 if (needTag && !tagOk) {
852 if (reason) *reason =
"adjacency_tag";
855 if (needTerrain && !terrainOk) {
856 if (reason) *reason =
"adjacency_terrain";
862bool PlacementSystem::runValidate(
const PlacementWorld &
world,
const BuildingDefinition &def,
863 const PlacementQuery &
q, std::string *reason) {
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");
872 if (it == rules.end() || !it->second) {
873 if (reason) *reason =
"validate_rejected";
876 return it->second(
world,
q, reason);
880 int cellY,
float elevation,
float rotationDeg,
881 int excludeInstanceId, std::string *reason,
int level) {
883 if (definition && definition->placementKind !=
"cell") {
885 if (definition->placementKind ==
"edge")
886 *reason =
"edge_requires_edge_address";
887 else if (definition->placementKind ==
"corner")
888 *reason =
"corner_requires_corner_address";
890 *reason =
"free_requires_free_address";
899 q.rotationDeg = normalizeRotation(buildingId, rotationDeg);
900 if (
world)
world->cellToWorldPlane(cellX, cellY,
q.worldX,
q.worldY);
902 q.excludeInstanceId = excludeInstanceId;
903 return canPlaceQuery(
world,
q, reason);
907 std::string *reason) {
908 if (
q.surfaceSampleCount <= 0)
return true;
910 if (reason) *reason =
"surface_slope";
915 if (reason) *reason =
"surface_height_delta";
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";
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";
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";
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()) {
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));
955 if (!supported && reason) *reason =
"support_missing";
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);
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);
980bool PlacementSystem::hasStructuralDependents(
const PlacementWorld &
world,
int instanceId) {
981 for (
const auto &[
id,
placed] :
world.buildings_) {
983 if (std::find(
placed.supportInstanceIds.begin(),
placed.supportInstanceIds.end(),
984 instanceId) !=
placed.supportInstanceIds.end())
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;
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) |
1006 for (
const auto &[candidateId, dependent] :
world.buildings_) {
1008 if (std::find(dependent.supportInstanceIds.begin(),
1009 dependent.supportInstanceIds.end(), instanceId) ==
1010 dependent.supportInstanceIds.end())
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)
1022 if (dependentDef->supportMode !=
"cell_below" || replacement.placementKind !=
"cell")
1025 foreachFootprintCell(*dependentDef, dependent.originCellX, dependent.originCellY,
1026 dependent.rotationDeg, [&](
int x,
int y) {
1027 if (world.getAnyOccupantAtLevel(x, y, dependent.level - 1) !=
1030 const uint64_t key = (uint64_t(uint32_t(x)) << 32U) | uint32_t(y);
1031 covered = replacementCells.contains(key);
1039bool PlacementSystem::canPlaceQuery(PlacementWorld *
world,
const PlacementQuery &
q,
1040 std::string *reason) {
1043 if (reason) *reason =
"no_world";
1046 const BuildingDefinition *def = BuildingRegistry::find(
q.buildingId);
1048 if (reason) *reason =
"unknown_building";
1051 return runValidate(*
world, *def,
q, reason);
1055 int cellY,
float rotationDeg,
int excludeInstanceId,
1056 std::string *reason) {
1057 return canPlaceElev(
world, buildingId, cellX, cellY, 0.f, rotationDeg, excludeInstanceId,
1067 edge.axis = EdgeAxis::Horizontal;
1069 edge.axis = EdgeAxis::Horizontal;
1072 edge.axis = EdgeAxis::Vertical;
1074 edge.axis = EdgeAxis::Vertical;
1085 int cellY,
const std::string &
direction,
int excludeInstanceId,
1086 std::string *reason,
int level) {
1088 if (reason) *reason =
"no_world";
1093 if (reason) *reason =
"unknown_building";
1097 if (reason) *reason =
"not_edge_building";
1100 auto result = canonicalEdge(cellX, cellY,
direction);
1102 if (reason) *reason =
"invalid_edge_direction";
1106 const bool inBounds =
edge.
axis == EdgeAxis::Horizontal
1112 if (reason) *reason =
"edge_out_of_bounds";
1115 const int resolvedLevel =
1116 level == std::numeric_limits<int>::min() ?
world->getActiveLevel() :
level;
1118 const size_t index =
edge.axis == EdgeAxis::Horizontal
1120 : size_t(
edge.y) * size_t(
world->width_ + 1) + size_t(
edge.x);
1123 const int occupant = channelIt !=
channels->end() &&
index < channelIt->second.size()
1124 ? channelIt->second[
index]
1126 if (occupant != 0 && occupant != excludeInstanceId) {
1127 if (reason) *reason =
"edge_occupied";
1134 int cellY,
const std::string &
direction) {
1135 if (!canPlaceEdge(
world, buildingId, cellX, cellY,
direction, 0,
nullptr))
return 0;
1137 auto address = canonicalEdge(cellX, cellY,
direction);
1138 if (!def || !address.ok())
return 0;
1143 placed.buildingId = buildingId;
1144 placed.placementKind =
"edge";
1151 float ax = 0.f,
ay = 0.f,
bx = 0.f,
by = 0.f;
1153 world->cellToWorldPlane(
edge.x + (
edge.axis == EdgeAxis::Horizontal ? 1 : 0),
1154 edge.y + (
edge.axis == EdgeAxis::Vertical ? 1 : 0),
bx,
by);
1157 placed.rotationDeg =
edge.axis == EdgeAxis::Horizontal ? 0.f : 90.f;
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_);
1165 const size_t index =
edge.axis == EdgeAxis::Horizontal
1167 : size_t(
edge.y) * size_t(
world->width_ + 1) + size_t(
edge.x);
1170 world->instanceOrder_.push_back(
placed.instanceId);
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;
1188 int vertexX,
int vertexY,
int excludeInstanceId,
1189 std::string *reason,
int level) {
1191 if (reason) *reason =
"no_world";
1196 if (reason) *reason =
"unknown_building";
1200 if (reason) *reason =
"not_corner_building";
1203 if (vertexX < 0 || vertexX >
world->width_ || vertexY < 0 || vertexY >
world->height_) {
1204 if (reason) *reason =
"corner_out_of_bounds";
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";
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);
1226 int vertexX,
int vertexY) {
1228 if (!canPlaceCorner(
world, buildingId, vertexX, vertexY, 0, &reason)) {
1231 {},
"building.corner"));
1237 "building.corner"));
1242 placed.buildingId = buildingId;
1243 placed.placementKind =
"corner";
1245 placed.originCellX = vertexX;
1246 placed.originCellY = vertexY;
1252 supportQuery.
cellX = vertexX;
1253 supportQuery.
cellY = vertexY;
1255 placed.supportInstanceIds = collectStructuralSupports(*
world, *def, supportQuery);
1260 occupancy[size_t(vertexY) * size_t(
world->width_ + 1) + size_t(vertexX)] =
1263 world->instanceOrder_.push_back(
placed.instanceId);
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));
1281 int vertexX,
int vertexY) {
1282 auto result = placeCornerResult(
world, buildingId, vertexX, vertexY);
1283 return result.ok() ? result.value().instanceId : 0;
1287 float worldX,
float worldY,
int excludeInstanceId,
1288 std::string *reason,
int level,
float rotationDeg) {
1290 if (reason) *reason =
"no_world";
1295 if (reason) *reason =
"unknown_building";
1299 if (reason) *reason =
"not_free_building";
1302 if (!std::isfinite(worldX) || !std::isfinite(worldY)) {
1303 if (reason) *reason =
"free_invalid_anchor";
1310 if (cellX < 0 || cellX >=
world->width_ || cellY < 0 || cellY >=
world->height_) {
1311 if (reason) *reason =
"free_out_of_bounds";
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" ||
1323 if (freeFootprintsOverlap(placedFreeFootprint(
placed), candidate)) {
1324 if (reason) *reason =
"free_overlap";
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);
1341 return placed.placementKind ==
"free" &&
1342 freeFootprintContainsInternal(placedFreeFootprint(
placed), worldX, worldY);
1347 float worldX,
float worldY,
float elevation,
1348 float rotationDeg) {
1350 if (!canPlaceFree(
world, buildingId, worldX, worldY, 0, &reason,
1351 std::numeric_limits<int>::min(), rotationDeg)) {
1354 {},
"building.free"));
1359 placed.buildingId = buildingId;
1360 placed.placementKind =
"free";
1365 placed.rotationDeg = normalizeRotation(buildingId, rotationDeg);
1366 applyDefinitionFreeFootprint(
placed, *def, *
world);
1372 world->instanceOrder_.push_back(
placed.instanceId);
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));
1393 const std::string &buildingId,
1398 {}, {},
"building.free"));
1404 "free surface placement requires a free definition", buildingId, {},
1415 if (!canPlaceFree(
world, buildingId, planeX, planeY, 0, &reason,
1416 std::numeric_limits<int>::min(), rotationDeg) ||
1423 :
"surface_height_delta";
1426 "free surface placement rejected without mutation", reason, {},
1432 placed.buildingId = buildingId;
1433 placed.placementKind =
"free";
1438 placed.rotationDeg = normalizeRotation(buildingId, rotationDeg);
1439 applyDefinitionFreeFootprint(
placed, *def, *
world);
1452 placed.surfaceSampleCount =
static_cast<int>(patch.
samples.size());
1456 world->instanceOrder_.push_back(
placed.instanceId);
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));
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;
1483 const auto placedIt =
world.buildings_.find(instanceId);
1484 if (placedIt ==
world.buildings_.end() || placedIt->second.placementKind !=
"edge")
return 0;
1492 const bool inBounds =
edge.axis == EdgeAxis::Horizontal
1497 if (!
channels || !inBounds)
return 0;
1499 if (channelIt ==
channels->end())
return 0;
1500 const size_t index =
edge.axis == EdgeAxis::Horizontal
1502 : size_t(
edge.y) * size_t(
world.width_ + 1) + size_t(
edge.x);
1503 return index < channelIt->second.size() ? channelIt->second[
index] : 0;
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;
1514 return group == otherGroup;
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};
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};
1535 for (
int i = 0; i < 6; ++i)
1536 if (connects(candidates[i]))
mask |= uint8_t(1u << i);
1541 const uint8_t
mask = edgeConnectionMask(
world, instanceId);
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";
1554 const std::vector<CornerAddress> &
vertices) {
1558 "edge path preview requires a world and at least two vertices", buildingId, {},
1559 "building.edge-path"));
1562 if (!definition || definition->placementKind !=
"edge") {
1565 "edge path preview requires an edge building definition", buildingId, {},
1566 "building.edge-path"));
1571 std::unordered_set<std::string> uniqueEdges;
1572 for (
size_t i = 1; i <
vertices.size(); ++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"));
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"));
1596 const char *
direction = horizontal ?
"north" :
"west";
1601 "edge path preflight rejected without mutation", reason, {},
1602 "building.edge-path"));
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"));
1621 if (!std::isfinite(
point.x) || !std::isfinite(
point.y)) {
1624 {},
"building.edge-curve"));
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) {
1644 {}, {},
"building.edge-curve"));
1647 {
static_cast<int>(std::lround(
x)),
static_cast<int>(std::lround(
y))});
1650 auto first = roundedPoint(0);
1653 std::vector<CornerAddress>
vertices;
1655 for (
int sample = 1; sample <= subdivisions; ++sample) {
1656 auto targetResult = roundedPoint(sample);
1657 if (!targetResult.ok())
1664 if (remainingX != 0 &&
1665 (remainingY == 0 || std::abs(remainingX) >= std::abs(remainingY)))
1666 cursor.x += remainingX > 0 ? 1 : -1;
1668 cursor.y += remainingY > 0 ? 1 : -1;
1670 if (
vertices.size() - 1 > kMaxRasterEdges) {
1673 "edge curve rasterization exceeds 65536 unit edges", {}, {},
1674 "building.edge-curve"));
1681 "edge curve rasterization must cover at least one unit edge", {}, {},
1682 "building.edge-curve"));
1689 const std::vector<EdgeCurvePoint> &controlPoints,
int subdivisions) {
1690 auto vertices = sampleEdgeCubicBezier(controlPoints, subdivisions);
1692 return previewEdgePath(
world, buildingId, std::move(
vertices).takeValue());
1697 const std::vector<EdgeCurvePoint> &controlPoints,
int subdivisions) {
1698 auto preview = previewEdgeCubicBezier(
world, buildingId, controlPoints, subdivisions);
1700 return commitEdgePath(
world, std::move(preview).takeValue(), &controlPoints, subdivisions);
1705 const std::vector<EdgeCurvePoint> &controlPoints,
int subdivisions) {
1706 auto validated = sampleEdgeCubicBezier(controlPoints, subdivisions);
1708 if (surfaceName.empty())
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;
1714 world.cellToWorldPlane(1, 0, xx, xy);
1715 world.cellToWorldPlane(0, 1, yx, yy);
1722 result.
samples.reserve(
static_cast<size_t>(subdivisions + 1));
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);
1748 "curve crosses a surface identity or revision boundary",
hit.surfaceId, {},
1749 "building.edge-curve-surface"));
1758PlacementSystem::placeEdgeCubicBezierOnSurface(
1760 const std::vector<EdgeCurvePoint> &controlPoints,
int subdivisions,
1761 const std::string &surfaceName) {
1765 {}, {},
"building.edge-curve-surface"));
1766 auto preview = previewEdgeCubicBezier(
world, buildingId, controlPoints, subdivisions);
1768 auto surface = sampleEdgeCurveSurface(*
world, surfaceName, controlPoints, subdivisions);
1770 return commitEdgePath(
world, std::move(preview).takeValue(), &controlPoints, subdivisions,
1776 int startVertexX,
int startVertexY,
int endVertexX,
1778 auto path = placeEdgePath(
world, buildingId,
1779 {{startVertexX, startVertexY}, {endVertexX, endVertexY}});
1788 const std::vector<CornerAddress> &
vertices) {
1789 auto previewResult = previewEdgePath(
world, buildingId,
vertices);
1790 if (!previewResult.ok())
1792 return commitEdgePath(
world, std::move(previewResult).takeValue());
1797 const std::vector<EdgeCurvePoint> *curveControls,
int curveSubdivisions,
1798 const EdgeCurveSurface *curveSurface) {
1800 const std::vector<EdgeAddress> &
edges = preview.edges;
1804 EdgePathPlacement result;
1805 result.instanceIds.reserve(
edges.size());
1806 std::vector<BuildingChangeEvent>
events;
1810 placed.instanceId = nextInstanceId();
1811 placed.buildingId = preview.buildingId;
1812 placed.placementKind =
"edge";
1814 placed.edgeCurveGroupId = curveGroupId;
1817 placed.level = preview.level;
1820 float ax = 0.f,
ay = 0.f,
bx = 0.f,
by = 0.f;
1822 world->cellToWorldPlane(
edge.x + (
edge.axis == EdgeAxis::Horizontal ? 1 : 0),
1823 edge.
y + (
edge.axis == EdgeAxis::Vertical ? 1 : 0),
bx,
by);
1826 placed.rotationDeg =
edge.axis == EdgeAxis::Horizontal ? 0.f : 90.f;
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_);
1834 const size_t index =
edge.axis == EdgeAxis::Horizontal
1839 world->instanceOrder_.push_back(
placed.instanceId);
1840 result.instanceIds.push_back(
placed.instanceId);
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));
1856 if (curveControls) {
1857 EdgeCurveGroup
group;
1872 if (
world->publishEvents_)
1873 for (BuildingChangeEvent &event :
events) emit(
std::move(event));
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()) {
1884 buildingId, {},
"building.area"));
1886 const BuildingDefinition *def = BuildingRegistry::find(buildingId);
1887 if (!def || def->placementKind ==
"edge") {
1890 buildingId, {},
"building.area"));
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));
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;
1913 cell.reason =
"area_candidate_overlap";
1914 ++preview.rejectedCount;
1916 cell.accepted =
true;
1917 ++preview.acceptedCount;
1918 reserved.insert(footprint.begin(), footprint.end());
1920 preview.cells.push_back(std::move(
cell));
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);
1939 int centerCellX,
int centerCellY,
int radius,
float rotationDeg) {
1943 std::to_string(
radius), {},
"building.area"));
1945 std::vector<std::pair<int, int>> anchors;
1948 if ((
x - centerCellX) * (
x - centerCellX) + (
y - centerCellY) * (
y - centerCellY) <=
1950 anchors.emplace_back(
x,
y);
1951 return previewArea(
world, buildingId, anchors, rotationDeg);
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);
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;
1972 return previewArea(
world, buildingId, anchors, rotationDeg);
1978 const auto failure = [&](std::string
message) {
1981 "building.pattern"));
1983 const auto corners = [&]() ->
eve::Result<std::vector<CornerAddress>> {
1984 std::vector<CornerAddress> result;
1985 result.reserve(
request.points.size());
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"));
1996 {
static_cast<int>(std::lround(
point.x)),
static_cast<int>(std::lround(
point.y))});
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");
2018 preview.
edge = std::move(
edge).takeValue();
2021 case PatternKind::EdgeCubicBezier: {
2022 if (
request.points.size() != 4)
2023 return failure(
"cubic edge pattern requires exactly four control points");
2027 if (!
request.surfaceName.empty()) {
2028 auto surface = sampleEdgeCurveSurface(*
world,
request.surfaceName,
2033 preview.
edge = std::move(
edge).takeValue();
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");
2047 auto area =
request.kind == PatternKind::RectangleFill
2048 ? previewRectangle(
world, buildingId,
a.x,
a.y,
b.x,
b.y,
2050 : previewRectangleOutline(
world, buildingId,
a.x,
a.y,
b.x,
b.y,
2053 preview.
area = std::move(
area).takeValue();
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");
2068 preview.
area = std::move(
area).takeValue();
2078 auto expanded = previewPattern(
world, buildingId,
request);
2081 result.
preview = std::move(expanded).takeValue();
2082 if (
request.kind == PatternKind::EdgeLine ||
request.kind == PatternKind::EdgePath) {
2086 }
else if (
request.kind == PatternKind::EdgeCubicBezier) {
2088 if (
request.surfaceName.empty())
2092 auto surface = sampleEdgeCurveSurface(*
world,
request.surfaceName,
2100 request.subdivisions, &frames);
2114 if (!
world || preview.rejectedCount != 0 || preview.cells.empty()) {
2117 std::to_string(preview.rejectedCount), {},
"building.area"));
2119 const BuildingDefinition *def = BuildingRegistry::find(preview.buildingId);
2125 AreaPlacement placement;
2126 placement.preview = std::move(preview);
2127 std::vector<BuildingChangeEvent>
events;
2128 for (
const AreaCellPreview &
cell : placement.preview.
cells) {
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));
2162 if (
world->publishEvents_)
2163 for (BuildingChangeEvent &event :
events) emit(
std::move(event));
2169 int minCellX,
int minCellY,
int maxCellX,
int maxCellY,
2170 float rotationDeg) {
2171 auto preview = previewRectangle(
world, buildingId, minCellX, minCellY, maxCellX, maxCellY,
2174 return commitArea(
world, std::move(preview).takeValue());
2179 int centerCellY,
int radius,
float rotationDeg) {
2180 auto preview = previewBrush(
world, buildingId, centerCellX, centerCellY,
radius, rotationDeg);
2182 return commitArea(
world, std::move(preview).takeValue());
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);
2193 return commitArea(
world, std::move(preview).takeValue());
2199 foreachFootprintCell(def,
placed.originCellX,
placed.originCellY,
placed.rotationDeg,
2200 [&](
int cx,
int cy) {
2201 if (world.inBounds(cx, cy)) {
2203 size_t(cy) * size_t(world.getWidth()) + size_t(cx);
2204 if (idx < occ.size()) occ[idx] = instanceId;
2210void PlacementSystem::clearOccupancy(PlacementWorld &
world,
int instanceId) {
2212 for (
int &
v :
world.occupancy()) {
2213 if (
v == instanceId)
v = 0;
2215 for (
auto &kv :
world.allChannels()) {
2217 if (
v == instanceId)
v = 0;
2220 for (
auto &kv :
world.horizontalEdgeChannels_) {
2222 if (
v == instanceId)
v = 0;
2224 for (
auto &kv :
world.verticalEdgeChannels_) {
2226 if (
v == instanceId)
v = 0;
2228 for (
auto &kv :
world.cornerChannels_) {
2230 if (
v == instanceId)
v = 0;
2232 for (
auto &
level :
world.cellChannelsByLevel_)
2236 for (
auto &levels : {&
world.horizontalEdgesByLevel_, &
world.verticalEdgesByLevel_})
2237 for (
auto &
level : *levels)
2248 int cellY,
float rotationDeg) {
2250 if (!
world)
return 0;
2254 const float rot = normalizeRotation(buildingId, rotationDeg);
2255 if (!canPlace(
world, buildingId, cellX, cellY, rot, 0,
nullptr))
return 0;
2291 if (
world->publishEvents_) emit(std::move(ev));
2296 float worldX,
float worldY,
float rotationDeg) {
2298 if (!
world)
return 0;
2300 const int id = placeAt(
world, buildingId,
s.cellX,
s.cellY, rotationDeg);
2301 if (
id <= 0)
return 0;
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;
2312 float worldX,
float worldY,
float worldZ,
2313 float rotationDeg) {
2315 if (!
world)
return 0;
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;
2329 if (!
world || !ghost)
return 0;
2334 const char *
direction = ghost->edge_.
axis == EdgeAxis::Horizontal ?
"north" :
"west";
2338 if (ghost->placementKind_ !=
"corner")
return 0;
2342 if (ghost->placementKind_ !=
"free")
return 0;
2343 if (!ghost->surfaceId_.empty()) {
2362 patch.
samples.resize(
static_cast<size_t>(std::max(ghost->surfaceSampleCount_, 0)));
2366 ghost->rotationDeg_);
2367 return result.ok() ? std::move(result).takeValue().instanceId : 0;
2370 ghost->elevation_, ghost->rotationDeg_);
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_;
2401 placed.supportInstanceIds = collectStructuralSupports(*
world, *def, supportQuery);
2405 world->instanceOrder_.push_back(
placed.instanceId);
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;
2425 std::string *reason) {
2428 if (reason) *reason =
"instance_id_conflict";
2429 return PlacementRestoreStatus::Rejected;
2433 if (reason) *reason =
"unknown_building";
2434 return PlacementRestoreStatus::Rejected;
2437 if (reason) *reason =
"placement_domain_mismatch";
2438 return PlacementRestoreStatus::Rejected;
2443 return PlacementRestoreStatus::Rejected;
2446 applyDefinitionFreeFootprint(restored, *def, *
world);
2453 instanceCounter() = std::max(instanceCounter(), restored.
instanceId);
2456 event.worldId =
world->getId();
2461 event.level = restored.
level;
2463 event.worldX = restored.
worldX;
2464 event.worldY = restored.
worldY;
2466 event.channel = restored.
channel;
2467 if (
world->publishEvents_) emit(std::move(event));
2468 return PlacementRestoreStatus::Restored;
2472 if (reason) *reason =
"placement_domain_mismatch";
2473 return PlacementRestoreStatus::Rejected;
2477 return PlacementRestoreStatus::Rejected;
2482 query.cellX = restored.
corner.
x;
2483 query.cellY = restored.
corner.
y;
2484 query.level = restored.
level;
2489 occupancy[size_t(restored.
corner.
y) * size_t(
world->width_ + 1) +
2493 instanceCounter() = std::max(instanceCounter(), restored.
instanceId);
2496 event.worldId =
world->getId();
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;
2505 event.channel = restored.
channel;
2506 if (
world->publishEvents_) emit(std::move(event));
2507 return PlacementRestoreStatus::Restored;
2510 const char *
direction =
placed.edge.axis == EdgeAxis::Horizontal ?
"north" :
"west";
2513 return PlacementRestoreStatus::Rejected;
2518 std::find(curveGroup->second.instanceIds.begin(), curveGroup->second.instanceIds.end(),
2519 restored.
instanceId) == curveGroup->second.instanceIds.end())
2526 ? size_t(
world->width_) * size_t(
world->height_ + 1)
2527 : size_t(
world->width_ + 1) * size_t(
world->height_);
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);
2535 instanceCounter() = std::max(instanceCounter(), restored.
instanceId);
2538 event.worldId =
world->getId();
2541 event.cellX = restored.
edge.
x;
2542 event.cellY = restored.
edge.
y;
2544 event.worldX = restored.
worldX;
2545 event.worldY = restored.
worldY;
2547 event.channel = restored.
channel;
2548 if (
world->publishEvents_) emit(std::move(event));
2549 return PlacementRestoreStatus::Restored;
2554 return PlacementRestoreStatus::Rejected;
2568 instanceCounter() = std::max(instanceCounter(), restored.
instanceId);
2572 event.worldId =
world->getId();
2578 event.worldX = restored.
worldX;
2579 event.worldY = restored.
worldY;
2581 event.channel = restored.
channel;
2582 if (
world->publishEvents_) emit(std::move(event));
2583 return PlacementRestoreStatus::Restored;
2588 const std::vector<PlacedBuilding> &
members) {
2594 "curve group restore requires a world, identity, four controls, valid subdivisions and matching members",
2595 {}, {},
"building.edge-curve-group.restore"));
2597 std::unordered_set<int> memberIds;
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"));
2610 if (!std::isfinite(
point.x) || !std::isfinite(
point.y)) {
2613 {},
"building.edge-curve-group.restore"));
2623 "curve group surface metadata requires exactly subdivisions plus one frames", {},
2624 {},
"building.edge-curve-group.restore"));
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"));
2640 std::unique_ptr<PlacementWorld> candidate =
world->cloneState();
2641 if (!candidate || candidate->edgeCurveGroups_.contains(
group.
id.
value)) {
2644 std::to_string(
group.
id.
value), {},
"building.edge-curve-group.restore"));
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"));
2656 candidate->buildings_.at(instanceId).edgeCurveGroupId =
group.
id;
2658 candidate->nextEdgeCurveGroupId_ =
2659 std::max(candidate->nextEdgeCurveGroupId_,
group.
id.
value + 1);
2660 world->swapState(*candidate);
2665 if (!
world || !groupId)
return;
2667 if (
group ==
world->edgeCurveGroups_.end())
return;
2669 const auto member =
world->buildings_.find(memberId);
2670 if (
member !=
world->buildings_.end() &&
member->second.edgeCurveGroupId == groupId)
2671 member->second.edgeCurveGroupId = {};
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;
2681 const EdgeCurveGroupId curveGroupId = it->second.edgeCurveGroupId;
2682 BuildingChangeEvent ev;
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;
2695 dissolveEdgeCurveGroup(
world, curveGroupId);
2696 clearOccupancy(*
world, instanceId);
2697 world->buildings().erase(it);
2700 if (
world->publishEvents_) emit(std::move(ev));
2705 float rotationDeg) {
2706 auto result = moveBuildingResult(
world, instanceId, cellX, cellY, rotationDeg);
2711 if (!
world || !
world->hasBuilding(instanceId) || hasStructuralDependents(*
world, instanceId))
2713 return removeBuildingUnchecked(
world, instanceId);
2718 if (!
world || instanceId <= 0) {
2721 "cascade removal requires a world and positive instance id",
2722 std::to_string(instanceId), {},
"building.structure"));
2724 if (!
world->hasBuilding(instanceId)) {
2727 std::to_string(instanceId), {},
"building.structure"));
2729 std::vector<int> ordered;
2731 std::unordered_set<int> visited;
2733 std::function<void(
int)> collect = [&](
int supportId) {
2734 if (visited.contains(supportId))
return;
2735 if (!
visiting.insert(supportId).second) {
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);
2745 visited.insert(supportId);
2746 ordered.push_back(supportId);
2748 collect(instanceId);
2752 "support_cycle", {},
"building.structure"));
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) {
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;
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));
2784 auto result = removeBuildingCascadeResult(
world, instanceId);
2785 return result.ok() ? int(result.value().removed.size()) : 0;
2793 "building.structure"));
2795 auto candidate =
world->cloneState();
2799 "failed to clone structural rebuild candidate", {}, {},
2800 "building.structure"));
2804 for (
int id : candidate->instanceOrder_) {
2805 auto placedIt = candidate->buildings_.find(
id);
2806 if (placedIt == candidate->buildings_.end())
continue;
2813 "structural rebuild encountered an unknown building definition",
2814 std::to_string(
id), {},
"building.structure"));
2817 std::vector<int> rebuilt;
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;
2826 if (!checkStructuralSupport(*candidate, *def, query, &reason)) {
2829 "structural rebuild found an invalid dependency",
2830 std::to_string(
id) +
":" + reason, {},
2831 "building.structure"));
2833 rebuilt = collectStructuralSupports(*candidate, *def, query);
2835 if (rebuilt !=
placed.supportInstanceIds) {
2836 placed.supportInstanceIds = std::move(rebuilt);
2840 world->swapState(*candidate);
2846 float rotationDeg) {
2847 if (!
world || instanceId <= 0) {
2850 std::to_string(instanceId), {},
"building.edit"));
2852 auto it =
world->buildings().find(instanceId);
2853 if (it ==
world->buildings().end()) {
2856 std::to_string(instanceId), {},
"building.edit"));
2860 if (hasStructuralDependents(*
world, instanceId)) {
2863 "support_in_use", {},
"building.structure"));
2868 "cell move cannot target a non-cell instance",
2869 std::to_string(instanceId), {},
"building.edit"));
2900 float px = 0.f,
py = 0.f;
2901 world->cellToWorldPlane(cellX, cellY,
px,
py);
2907 clearOccupancy(*
world, instanceId);
2918 writeOccupancy(*
world, *def, pb, instanceId);
2920 if (
world->publishEvents_) emit(std::move(ev));
2927 if (!
world || instanceId <= 0) {
2930 "edge move requires a world and positive instance id", std::to_string(instanceId),
2931 {},
"building.edit"));
2933 auto it =
world->buildings().find(instanceId);
2934 if (it ==
world->buildings().end()) {
2937 std::to_string(instanceId), {},
"building.edit"));
2943 std::to_string(instanceId), {},
"building.edit"));
2945 if (hasStructuralDependents(*
world, instanceId)) {
2948 "support_in_use", {},
"building.structure"));
2955 {},
"building.edit"));
2957 auto addressResult = canonicalEdge(cellX, cellY,
direction);
2958 if (!addressResult.ok())
2972 event.worldId =
world->getId();
2974 event.instanceId = instanceId;
2975 event.otherCellX = pb.
edge.
x;
2976 event.otherCellY = pb.
edge.
y;
2982 clearOccupancy(*
world, instanceId);
2986 float ax = 0.f,
ay = 0.f,
bx = 0.f,
by = 0.f;
2988 world->cellToWorldPlane(
target.x + (
target.axis == EdgeAxis::Horizontal ? 1 : 0),
2996 ? size_t(
world->width_) * size_t(
world->height_ + 1)
2997 : size_t(
world->width_ + 1) * size_t(
world->height_);
2999 const size_t index =
target.axis == EdgeAxis::Horizontal
3002 occupancy[
index] = instanceId;
3004 event.worldX = pb.
worldX;
3005 event.worldY = pb.
worldY;
3008 if (
world->publishEvents_) emit(std::move(event));
3015 if (!
world || instanceId <= 0) {
3018 "corner move requires a world and positive instance id", std::to_string(instanceId),
3019 {},
"building.edit"));
3021 auto found =
world->buildings_.find(instanceId);
3025 std::to_string(instanceId), {},
"building.edit"));
3028 if (
placed.placementKind !=
"corner") {
3031 std::to_string(instanceId), {},
"building.edit"));
3033 if (hasStructuralDependents(*
world, instanceId)) {
3036 "support_in_use", {},
"building.structure"));
3039 if (!canPlaceCorner(
world,
placed.buildingId, vertexX, vertexY, instanceId, &reason,
3043 reason, {},
"building.edit"));
3049 placed.buildingId, {},
"building.edit"));
3054 clearOccupancy(*
world, instanceId);
3056 placed.originCellX = vertexX;
3057 placed.originCellY = vertexY;
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];
3067 occupancy[size_t(vertexY) * size_t(
world->width_ + 1) + size_t(vertexX)] = instanceId;
3072 event.worldId =
world->getId();
3073 event.buildingId =
placed.buildingId;
3074 event.instanceId = instanceId;
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));
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"));
3097 auto found =
world->buildings_.find(instanceId);
3098 if (
found ==
world->buildings_.end() ||
found->second.placementKind !=
"free") {
3101 std::to_string(instanceId), {},
"building.edit"));
3105 if (!canPlaceFree(
world,
placed.buildingId, worldX, worldY, instanceId, &reason,
3106 placed.level, rotationDeg)) {
3109 reason, {},
"building.edit"));
3116 placed.rotationDeg = normalizeRotation(
placed.buildingId, rotationDeg);
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));
3140 if (!
world || instanceId <= 0) {
3143 "free surface move requires a world and positive instance id",
3144 std::to_string(instanceId), {},
"building.edit"));
3146 auto found =
world->buildings_.find(instanceId);
3147 if (
found ==
world->buildings_.end() ||
found->second.placementKind !=
"free") {
3150 std::to_string(instanceId), {},
"building.edit"));
3157 placed.buildingId, {},
"building.edit"));
3167 if (!canPlaceFree(
world,
placed.buildingId, planeX, planeY, instanceId, &reason,
3168 placed.level, rotationDeg) ||
3175 :
"surface_height_delta";
3178 "free surface move preflight rejected without mutation", reason, {},
3187 placed.rotationDeg = normalizeRotation(
placed.buildingId, rotationDeg);
3198 placed.surfaceSampleCount =
static_cast<int>(patch.
samples.size());
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));
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"));
3229 auto it =
world->buildings().find(instanceId);
3230 if (it ==
world->buildings().end()) {
3233 std::to_string(instanceId), {},
"building.edit"));
3236 const BuildingDefinition *replacement = BuildingRegistry::find(replacementBuildingId);
3240 replacementBuildingId, {},
"building.edit"));
3248 "replacement must use the same placement domain as the source",
3249 replacementBuildingId, {},
"building.edit"));
3251 if (hasStructuralDependents(*
world, instanceId) &&
3252 !replacementPreservesStructuralDependents(*
world, instanceId, *replacement)) {
3255 "support_in_use", {},
"building.structure"));
3260 const char *
direction = pb.
edge.
axis == EdgeAxis::Horizontal ?
"north" :
"west";
3262 &reason, pb.
level)) {
3265 {},
"building.edit"));
3267 }
else if (sourceCorner) {
3269 &reason, pb.
level)) {
3272 reason, {},
"building.edit"));
3274 }
else if (sourceFree) {
3275 if (!canPlaceFree(
world, replacementBuildingId, pb.
worldX, pb.
worldY, instanceId,
3279 reason, {},
"building.edit"));
3291 event.
action =
"replace";
3292 event.worldId =
world->getId();
3294 event.buildingId = replacementBuildingId;
3295 event.instanceId = instanceId;
3300 clearOccupancy(*
world, instanceId);
3306 applyDefinitionFreeFootprint(pb, *replacement, *
world);
3309 : normalizeRotation(replacementBuildingId, pb.
rotationDeg);
3315 ? size_t(
world->width_) * size_t(
world->height_ + 1)
3316 : size_t(
world->width_ + 1) * size_t(
world->height_);
3321 occupancy[
index] = instanceId;
3322 }
else if (sourceCorner) {
3340 writeOccupancy(*
world, *replacement, pb, instanceId);
3345 event.worldX = pb.
worldX;
3346 event.worldY = pb.
worldY;
3350 if (
world->publishEvents_) emit(std::move(event));
3357 std::vector<int>
ids =
world->instanceOrder_;
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);
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();
std::map< std::string, std::vector< Key >, std::less<> > channels
building::EdgeCurveGroup group
Stable, structured diagnostics shared by engine modules.
const GltfImportRequest & request
Pure cell/world projection helpers for GridConfig.
std::array< double, 10 > q
std::array< float, 4 > rotation
std::array< PixelCell, kPixelChunkSize *kPixelChunkSize > cells
std::vector< Point > vertices
RoadLaneDirection direction
float(ui::Theme::* member)[4]
std::map< std::string, std::span< const std::uint8_t > > members
std::set< std::string > visiting
const UnitySourceAsset & source
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.
Move-only operation result carrying either a value or Status.
static Result success(T value)
Construct a successful result owning value.
static Result failure(Status status)
Construct a failed result from a structured status.
Structured status and zero or more diagnostics for an operation.
static Status failure(StatusCode code, Diagnostic diagnostic)
Construct a failed status with one diagnostic.
static Status success(StatusCode code=StatusCode::Ok)
Construct a successful status with an explicit non-error outcome.
static const BuildingDefinition * find(const std::string &id)
Finds .
EVENGINE_API_WORLD public API.
float getElevation() const
Returns the elevation.
int getCellY() const
Returns the cell y.
float getWorldY() const
Returns the world y.
int getCellX() const
Returns the cell x.
bool validate(PlacementWorld *world)
对当前姿态做校验,写入 valid_/reason_。
float getWorldX() const
Returns the world x.
float getRotationDeg() const
Returns the rotation deg.
std::string getBuildingId() const
Returns the building id.
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 float getPlaneSurfaceHeight()
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 clearEvents()
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 int nextInstanceId()
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
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.
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.
std::vector< int > instanceIds
Owning validated edge addresses for one non-mutating path preview.
std::vector< EdgeAddress > edges
Atomic placement receipt shared by every built-in pattern kind.
std::vector< int > instanceIds
Owning non-mutating expansion of an edge or area pattern.
Owning, renderer-neutral request for one atomic placement pattern.
std::vector< EdgeCurvePoint > points
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.