10#include <unordered_map>
11#include <unordered_set>
17const HouseComponent *pick(
const std::vector<std::reference_wrapper<const HouseComponent>> &
choices,
19 if (
choices.empty())
return nullptr;
21 for (
const auto &choice :
choices) total += choice.get().
weight;
22 std::uniform_int_distribution<int>
roll(1, total);
24 for (
const auto &choice :
choices) {
25 target -= choice.get().weight;
26 if (
target <= 0)
return &choice.get();
31bool has(
const std::vector<std::reference_wrapper<const HouseComponent>> &
values) {
return !
values.empty(); }
33bool allowsRotation(
const HouseComponent& component,
int rotation) {
34 const int normalized = (
rotation % 360 + 360) % 360;
35 return std::any_of(component.rotations.begin(), component.rotations.end(),
36 [&](
int allowed) { return (allowed % 360 + 360) % 360 == normalized; });
39bool isWindowComponent(
const HouseComponent *component) {
40 if (!component)
return false;
41 if (component->id.find(
"window") != std::string::npos)
return true;
42 return std::find(component->tags.begin(), component->tags.end(),
"window") != component->tags.end();
45std::vector<std::reference_wrapper<const HouseComponent>> facadeVariant(
46 const std::vector<std::reference_wrapper<const HouseComponent>> &
choices,
bool wantWindow) {
47 bool hasWindow =
false, hasSolid =
false;
48 for (
const auto &choice :
choices) {
49 if (isWindowComponent(&choice.get()))
53 if (!hasWindow || !hasSolid)
return choices;
54 std::vector<std::reference_wrapper<const HouseComponent>> filtered;
55 for (
const auto &choice :
choices)
56 if (isWindowComponent(&choice.get()) == wantWindow) filtered.push_back(choice);
60bool oneOf(
const std::string &
value, std::initializer_list<const char *>
options) {
61 for (
const char *option :
options) if (
value == option) return true;
86std::vector<uint8_t> footprintMask(
const std::string &
shape,
int width,
int depth,
int inset) {
88 const int minX = inset, minY = inset, maxX =
width - 1 - inset, maxY =
depth - 1 - inset;
89 if (minX > maxX || minY > maxY)
return mask;
90 const int spanX = maxX - minX + 1, spanY = maxY - minY + 1;
91 const int cutX = minX + std::max(1, spanX / 2) - 1;
92 const int cutY = minY + std::max(1, spanY / 2) - 1;
93 const int stemInset = spanX >= 5 ? 1 : 0;
94 for (
int y = minY;
y <= maxY; ++
y)
for (
int x = minX;
x <= maxX; ++
x) {
97 else if (
shape ==
"t_shape")
active =
y <= cutY || (
x >= minX + stemInset &&
x <= maxX - stemInset);
110 for (
size_t i = 0, j = poly.size() - 1; i < poly.size(); j = i++) {
111 const float xi = poly[i].x, yi = poly[i].y;
112 const float xj = poly[j].x, yj = poly[j].y;
113 if (((yi >
py) != (yj >
py)) &&
114 (
px < (xj - xi) * (
py - yi) / (yj - yi + 1e-12f) + xi))
121std::vector<uint8_t> polygonMask(
const std::vector<HousePolygonPoint> &poly,
int width,
int depth,
int inset) {
123 for (
int y = inset;
y <
depth - inset; ++
y)
124 for (
int x = inset;
x <
width - inset; ++
x)
139std::vector<std::reference_wrapper<const HouseComponent>> compatibleOnFace(
141 std::vector<std::reference_wrapper<const HouseComponent>> out;
142 for (
const auto &choice :
choices) {
143 const auto &
c = choice.get();
144 if (!allowsRotation(
c,
rotation))
continue;
147 if (
c.sockets.empty()) {
148 out.push_back(choice);
151 for (
const auto &socket :
c.sockets) {
152 if (rotate(socket.direction,
rotation) == face && !socket.type.empty()) {
153 out.push_back(choice);
162std::optional<std::pair<int, int>> findInteriorCell(
const std::vector<uint8_t> &
mask,
int width,
int depth) {
163 const int dx[] = {0, 1, 0, -1};
164 const int dy[] = {-1, 0, 1, 0};
166 for (
int x = 1;
x <
width - 1; ++
x) {
168 bool interior =
true;
171 if (interior)
return std::pair<int, int>{
x,
y};
176void appendRoomRegions(
int width,
int depth,
const std::vector<uint8_t>&
mask,
const std::string& roomType,
int floorZ,
177 std::vector<HouseRoom>& rooms) {
178 std::vector<uint8_t> claimed(
mask.size(), 0);
184 while (x1 + 1 <
width &&
mask[
size_t(
y *
width + x1 + 1)] && !claimed[
size_t(
y *
width + x1 + 1)]) ++x1;
187 while (extend && y1 + 1 <
depth) {
188 for (
int xx =
x; xx <= x1; ++xx) {
189 if (!
mask[
size_t((y1 + 1) *
width + xx)] || claimed[
size_t((y1 + 1) *
width + xx)]) {
196 rooms.push_back({roomType,
x,
y, floorZ, x1 -
x + 1, y1 -
y + 1});
197 for (
int yy =
y; yy <= y1; ++yy)
198 for (
int xx =
x; xx <= x1; ++xx) claimed[
size_t(yy *
width + xx)] = 1;
205 const std::vector<std::string>& roomTypes, std::mt19937& rng,
206 const std::vector<std::reference_wrapper<const HouseComponent>>& innerWall,
207 const std::vector<std::reference_wrapper<const HouseComponent>>& innerDoor,
208 std::vector<HouseInstance>&
instances, std::vector<HouseRoom>& rooms,
int floorZ) {
211 int area()
const {
return (x1 - x0 + 1) * (y1 - y0 + 1); }
214 for (
const uint8_t
cell :
mask) activeArea +=
cell != 0 ? 1 : 0;
215 const int target = std::max(1,
int(roomTypes.size()));
216 if (
target > std::max(1, activeArea / 3))
218 "required room count cannot fit the active footprint",
219 "requiredRooms", {},
"housegen.generate"));
223 std::vector<uint8_t> claimed(
mask.size(), 0);
224 std::vector<Rect> rects;
230 while (x1 + 1 <
width &&
mask[
size_t(
y *
width + x1 + 1)] && !claimed[
size_t(
y *
width + x1 + 1)]) ++x1;
233 while (extend && y1 + 1 <
depth) {
234 for (
int xx =
x; xx <= x1; ++xx)
235 if (!
mask[
size_t((y1 + 1) *
width + xx)] || claimed[
size_t((y1 + 1) *
width + xx)]) {
241 rects.push_back({
x,
y, x1, y1});
242 for (
int yy =
y; yy <= y1; ++yy)
243 for (
int xx =
x; xx <= x1; ++xx) claimed[
size_t(yy *
width + xx)] = 1;
246 if (
int(rects.size()) >
target)
249 "requested room count cannot represent the irregular footprint without outside cells",
"requiredRooms", {},
250 "housegen.generate"));
252 while (
int(rects.size()) <
target) {
253 auto splitIt = std::max_element(rects.begin(), rects.end(),
254 [](
const Rect&
a,
const Rect&
b) { return a.area() < b.area(); });
255 if (splitIt == rects.end() || splitIt->area() < 2)
258 "requiredRooms", {},
"housegen.generate"));
259 const Rect
source = *splitIt;
260 rects.erase(splitIt);
271 std::sort(rects.begin(), rects.end(),
272 [](
const Rect&
a,
const Rect&
b) { return std::tie(a.y0, a.x0) < std::tie(b.y0, b.x0); });
274 std::vector<int> roomAt(
size_t(
width *
depth), -1);
275 for (
size_t i = 0; i < rects.size(); ++i) {
276 const Rect& rect = rects[i];
277 rooms.push_back({roomTypes[i], rect.x0, rect.y0, floorZ, rect.x1 - rect.x0 + 1, rect.y1 - rect.y0 + 1});
278 for (
int y = rect.y0;
y <= rect.y1; ++
y)
279 for (
int x = rect.x0;
x <= rect.x1; ++
x) roomAt[
size_t(
y *
width +
x)] = int(i);
282 const auto choose = [&](
const auto& components,
int rotation) ->
const HouseComponent* {
283 std::vector<std::reference_wrapper<const HouseComponent>> allowed;
284 for (
const auto& component : components)
285 if (allowsRotation(component.get(),
rotation)) allowed.push_back(component);
286 return pick(allowed, rng);
288 std::unordered_set<std::string> doorBoundaries;
290 const int lo = std::min(roomA, roomB), hi = std::max(roomA, roomB);
291 const std::string
boundary = std::to_string(lo) +
":" + std::to_string(hi) +
":" + std::to_string(
rotation);
292 const bool door = doorBoundaries.insert(
boundary).second;
293 const HouseComponent* component = choose(door ? innerDoor : innerWall,
rotation);
297 std::string(
"no interior ") + (door ?
"door" :
"wall") +
298 " component allows rotation " +
std::to_string(
rotation),
299 "rotations", {},
"housegen.generate"));
306 const int room = roomAt[size_t(
y *
width +
x)];
307 if (room < 0)
continue;
309 const int neighbour = roomAt[size_t(
y *
width +
x + 1)];
310 if (neighbour >= 0 && neighbour != room) {
311 auto emitted = emitBoundary(
x + 1,
y, 90, room, neighbour);
312 if (!emitted.ok())
return emitted;
316 const int neighbour = roomAt[size_t((
y + 1) *
width +
x)];
317 if (neighbour >= 0 && neighbour != room) {
318 auto emitted = emitBoundary(
x,
y + 1, 0, room, neighbour);
319 if (!emitted.ok())
return emitted;
331 if (
r.width < 3 ||
r.depth < 3 ||
r.floors < 1 ||
r.maxAttempts < 1) {
334 "housegen.generate"));
336 if (!std::isfinite(
r.moduleSize) ||
r.moduleSize <= 0.f || !std::isfinite(
r.floorHeight) ||
r.floorHeight <= 0.f)
339 "housegen.generate"));
340 if (!oneOf(
r.footprint, {
"auto",
"rectangle",
"l_shape",
"t_shape",
"polygon"}) ||
341 !oneOf(
r.roof, {
"auto",
"gable",
"flat",
"shed"}) ||
342 !oneOf(
r.entrance, {
"auto",
"north",
"east",
"south",
"west"})) {
344 "unsupported footprint, roof or entrance mode", {}, {},
345 "housegen.generate"));
347 if (
r.footprint ==
"polygon" &&
r.perimeter.size() < 3) {
350 "housegen.generate"));
352 const auto foundation = library_.byCategory(
"foundation",
r.style);
353 const auto floor = library_.byCategory(
"floor",
r.style);
354 const auto wall = library_.byCategory(
"wall",
r.style);
355 const auto door = library_.byCategory(
"door",
r.style);
356 const auto roof = library_.byCategory(
"roof",
r.style);
357 if (!has(foundation) || !has(floor) || !has(wall) || !has(door) || !has(roof)) {
360 "housegen.generate"));
364 if (!
r.style.empty() && !library_.hasCompletePack(
r.style)) {
367 "style pack '" +
r.style +
"' is incomplete (needs foundation, floor, wall, door and roof)", {}, {},
368 "housegen.generate"));
372 const auto stairs = library_.byCategory(
"stairs",
r.style);
373 const auto innerWall = library_.byCategory(
"interior_wall",
r.style);
374 const auto innerDoor = library_.byCategory(
"interior_door",
r.style);
375 const bool partitionsInteriors =
r.requiredRooms.size() > 1;
376 if (partitionsInteriors && (!has(innerWall) || !has(innerDoor)))
379 "requiredRooms", {},
"housegen.generate"));
381 std::mt19937 rng(
r.seed);
382 generated.
seed =
r.seed;
385 static constexpr const char *shapes[] = {
"rectangle",
"l_shape",
"t_shape"};
386 static constexpr const char *roofs[] = {
"gable",
"flat",
"shed"};
388 SocketDirection::South, SocketDirection::West};
390 :
r.footprint ==
"polygon" ?
"polygon"
392 generated.
roofStyle =
r.roof ==
"auto" ? roofs[rng() % 3] :
r.roof;
394 r.entrance ==
"auto" ?
sides[rng() % 4] : directionFromName(
r.entrance);
395 generated.
entranceSide = directionName(entranceDirection);
399 const auto makeMask = [&](
int ins) {
400 return isPolygon ? polygonMask(
r.perimeter,
r.width,
r.depth, 0)
403 const auto baseMask = makeMask(0);
407 std::optional<std::pair<int, int>> stairwell;
408 if (
r.floors > 1 && has(stairs)) {
409 stairwell = findInteriorCell(baseMask,
r.width,
r.depth);
413 {},
"housegen.generate"));
419 std::vector<uint8_t> previousMask;
420 for (
int z = 0;
z <
r.floors; ++
z) {
423 if (!isPolygon &&
z > 0 && inset == 0 &&
r.width > 4 &&
r.depth > 4 && (rng() & 3u) == 0
u)
425 const auto mask = makeMask(inset);
428 if (!previousMask.empty()) {
429 for (
int y = 0;
y <
r.depth; ++
y)
for (
int x = 0;
x <
r.width; ++
x) {
430 if (
active(previousMask,
r.width,
r.depth,
x,
y) &&
432 generated.
instances.push_back({pick(roof, rng)->id,
x,
y,
z, 0});
435 int minX =
r.width, minY =
r.depth, maxX = -1, maxY = -1;
436 for (
int y = 0;
y <
r.depth; ++
y)
for (
int x = 0;
x <
r.width; ++
x)
if (
active(
mask,
r.width,
r.depth,
x,
y)) {
437 minX = std::min(minX,
x); minY = std::min(minY,
y);
438 maxX = std::max(maxX,
x); maxY = std::max(maxY,
y);
440 if (stairwell &&
x == stairwell->first &&
y == stairwell->second) {
441 generated.
instances.push_back({pick(stairs, rng)->id,
x,
y,
z, 0});
444 if (
z == 0) generated.
instances.push_back({pick(foundation, rng)->id,
x,
y,
z, 0});
445 generated.
instances.push_back({pick(floor, rng)->id,
x,
y,
z, 0});
447 if (maxX < minX || maxY < minY)
450 if (!partitionsInteriors)
451 appendRoomRegions(
r.width,
r.depth,
mask,
z == 0 ?
"living" :
"upper",
z, generated.
rooms);
453 using Face = std::tuple<int, int, SocketDirection>;
454 std::vector<Face> faces;
455 std::vector<Face> entranceCandidates;
456 std::unordered_map<int, int> facesPerCell;
457 for (
int y = 0;
y <
r.depth; ++
y)
for (
int x = 0;
x <
r.width; ++
x) {
460 SocketDirection::South, SocketDirection::West};
461 const int dx[] = {0, 1, 0, -1};
462 const int dy[] = {-1, 0, 1, 0};
465 faces.push_back(face);
466 ++facesPerCell[
y *
r.width +
x];
467 if (
z == 0 &&
directions[
side] == entranceDirection) entranceCandidates.push_back(face);
470 std::sort(entranceCandidates.begin(), entranceCandidates.end(), [](
const Face &
a,
const Face &
b) {
471 return std::tie(std::get<1>(a), std::get<0>(a)) < std::tie(std::get<1>(b), std::get<0>(b));
473 Face entranceFace{-1, -1, entranceDirection};
474 if (
z == 0 && !entranceCandidates.empty()) entranceFace = entranceCandidates[entranceCandidates.size() / 2];
475 for (
const auto &faceData : faces) {
476 const auto [
x,
y, face] = faceData;
477 const bool isEntrance =
z == 0 && faceData == entranceFace;
478 const int rotation = directionRotation(face);
479 auto choices = compatibleOnFace(isEntrance ? door : wall, face,
rotation);
481 const bool corner = facesPerCell[
y *
r.width +
x] > 1;
482 const bool besideEntrance =
z == 0 &&
483 std::abs(
x - std::get<0>(entranceFace)) + std::abs(
y - std::get<1>(entranceFace)) <= 1;
484 const int facadeAxis = (face == SocketDirection::North || face == SocketDirection::South) ?
x :
y;
485 const bool rhythmicWindow = ((facadeAxis + int(
r.seed & 1u)) & 1) == 0;
486 choices = facadeVariant(
choices, !corner && !besideEntrance && rhythmicWindow);
488 const auto *selected = pick(
choices, rng);
492 "no socket-compatible " + std::string(isEntrance ?
"door" :
"wall") +
" component", {}, {},
493 "housegen.generate"));
497 if (partitionsInteriors) {
498 auto partitioned = partitionFloor(
r.width,
r.depth,
mask,
r.requiredRooms, rng, innerWall, innerDoor,
502 if (
z + 1 ==
r.floors) {
503 for (
int y = 0;
y <
r.depth; ++
y)
for (
int x = 0;
x <
r.width; ++
x)
505 generated.
instances.push_back({pick(roof, rng)->id,
x,
y,
z + 1, 0});
510 int occupiedWidth =
r.width, occupiedDepth =
r.depth;
512 const auto component = library_.find(instance.componentId);
513 if (!component)
continue;
514 const int rotation = (instance.rotationDeg % 360 + 360) % 360;
515 const bool quarter = (
rotation / 90) % 2 != 0;
517 std::max(occupiedWidth, instance.x + (quarter ? component->get().depth : component->get().width));
519 std::max(occupiedDepth, instance.y + (quarter ? component->get().width : component->get().depth));
521 std::vector<uint8_t> occupiedMask(
size_t(occupiedWidth * occupiedDepth), 0);
522 for (
int y = 0;
y <
r.depth; ++
y)
523 for (
int x = 0;
x <
r.width; ++
x)
524 occupiedMask[
size_t(
y * occupiedWidth +
x)] = baseMask[size_t(
y *
r.width +
x)];
526 const auto component = library_.find(instance.componentId);
527 if (!component)
continue;
528 const int rotation = (instance.rotationDeg % 360 + 360) % 360;
529 const bool quarter = (
rotation / 90) % 2 != 0;
530 const int cellWidth = quarter ? component->get().depth : component->get().width;
531 const int cellDepth = quarter ? component->get().width : component->get().depth;
532 for (
int y = 0;
y < cellDepth; ++
y)
533 for (
int x = 0;
x < cellWidth; ++
x)
534 occupiedMask[
size_t((instance.y +
y) * occupiedWidth + instance.x +
x)] = 1;
539 auto validated = generated.
validate(library_);
540 if (!validated.ok())
return validated;
541 out = std::move(generated);
std::vector< BuildingInstanceSnapshot > instances
std::map< std::string, Var > values
std::array< float, 4 > rotation
std::set< std::string > choices
RoadLaneDirection direction
const SquirrelValueOptions & options
CommandLogBoundary boundary
Anchor rule, see above.
const UnitySourceAsset & source
std::vector< char > inside
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.
static Status success(StatusCode code=StatusCode::Ok)
Construct a successful status with an explicit non-error outcome.
一次生成的房屋布局:实例 + 房间 + 元信息,可 JSON 序列化 / 实例化。
std::string footprintStyle
布局风格结果。
eve::Result< void > validate(const HouseComponentLibrary &library) const
校验布局是否满足组件库规则。
std::vector< HouseRoom > rooms
std::vector< uint8_t > footprintMask
int footprintWidth
Canonical base-floor occupancy mask owned by this layout.
std::vector< HouseInstance > instances
组件实例 / 房间 / 诊断信息。
SocketDirection
组件连接点方向(上下 + 四向)。
bool pointInPolygon(const Vec2 &p, const Polygon &poly)
Point-in-polygon test (ray casting; boundary counts as inside).