载入中...
搜索中...
未找到
HouseGenerator.cpp
浏览该文件的文档.
3
4#include <algorithm>
5#include <cmath>
6#include <functional>
7#include <optional>
8#include <random>
9#include <tuple>
10#include <unordered_map>
11#include <unordered_set>
12#include <utility>
13
14namespace eve::housegen {
15namespace {
16
17const HouseComponent *pick(const std::vector<std::reference_wrapper<const HouseComponent>> &choices,
18 std::mt19937 &rng) {
19 if (choices.empty()) return nullptr;
20 int total = 0;
21 for (const auto &choice : choices) total += choice.get().weight;
22 std::uniform_int_distribution<int> roll(1, total);
23 int target = roll(rng);
24 for (const auto &choice : choices) {
25 target -= choice.get().weight;
26 if (target <= 0) return &choice.get();
27 }
28 return &choices.back().get();
29}
30
31bool has(const std::vector<std::reference_wrapper<const HouseComponent>> &values) { return !values.empty(); }
32
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; });
37}
38
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();
43}
44
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()))
50 hasWindow = true;
51 else hasSolid = true;
52 }
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);
57 return filtered;
58}
59
60bool oneOf(const std::string &value, std::initializer_list<const char *> options) {
61 for (const char *option : options) if (value == option) return true;
62 return false;
63}
64
65SocketDirection directionFromName(const std::string &name) {
66 if (name == "east") return SocketDirection::East;
67 if (name == "south") return SocketDirection::South;
68 if (name == "west") return SocketDirection::West;
70}
71
72std::string directionName(SocketDirection direction) {
73 if (direction == SocketDirection::East) return "east";
74 if (direction == SocketDirection::South) return "south";
75 if (direction == SocketDirection::West) return "west";
76 return "north";
77}
78
79int directionRotation(SocketDirection direction) {
80 if (direction == SocketDirection::East) return 90;
81 if (direction == SocketDirection::South) return 180;
82 if (direction == SocketDirection::West) return 270;
83 return 0;
84}
85
86std::vector<uint8_t> footprintMask(const std::string &shape, int width, int depth, int inset) {
87 std::vector<uint8_t> mask(size_t(width * depth), 0);
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) {
95 bool active = true;
96 if (shape == "l_shape") active = !(x > cutX && y > cutY);
97 else if (shape == "t_shape") active = y <= cutY || (x >= minX + stemInset && x <= maxX - stemInset);
98 if (active) mask[size_t(y * width + x)] = 1;
99 }
100 return mask;
101}
102
103bool active(const std::vector<uint8_t> &mask, int width, int depth, int x, int y) {
104 return x >= 0 && y >= 0 && x < width && y < depth && mask[size_t(y * width + x)] != 0;
105}
106
108bool pointInPolygon(float px, float py, const std::vector<HousePolygonPoint> &poly) {
109 bool inside = false;
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))
115 inside = !inside;
116 }
117 return inside;
118}
119
121std::vector<uint8_t> polygonMask(const std::vector<HousePolygonPoint> &poly, int width, int depth, int inset) {
122 std::vector<uint8_t> mask(size_t(width * depth), 0);
123 for (int y = inset; y < depth - inset; ++y)
124 for (int x = inset; x < width - inset; ++x)
125 if (pointInPolygon(float(x) + 0.5f, float(y) + 0.5f, poly))
126 mask[size_t(y * width + x)] = 1;
127 return mask;
128}
129
134 side = (side + degrees / 90) % 4;
137}
138
139std::vector<std::reference_wrapper<const HouseComponent>> compatibleOnFace(
140 const std::vector<std::reference_wrapper<const HouseComponent>> &choices, SocketDirection face, int rotation) {
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;
145 // Components without sockets are intentionally wildcard-compatible. This preserves
146 // compatibility with small legacy kits while new player kits opt into strict sockets.
147 if (c.sockets.empty()) {
148 out.push_back(choice);
149 continue;
150 }
151 for (const auto &socket : c.sockets) {
152 if (rotate(socket.direction, rotation) == face && !socket.type.empty()) {
153 out.push_back(choice);
154 break;
155 }
156 }
157 }
158 return out;
159}
160
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};
165 for (int y = 1; y < depth - 1; ++y)
166 for (int x = 1; x < width - 1; ++x) {
167 if (!active(mask, width, depth, x, y)) continue;
168 bool interior = true;
169 for (int side = 0; side < 4; ++side)
170 interior = interior && active(mask, width, depth, x + dx[side], y + dy[side]);
171 if (interior) return std::pair<int, int>{x, y};
172 }
173 return std::nullopt;
174}
175
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);
179 for (int y = 0; y < depth; ++y) {
180 for (int x = 0; x < width; ++x) {
181 const size_t origin = size_t(y * width + x);
182 if (!mask[origin] || claimed[origin]) continue;
183 int x1 = x;
184 while (x1 + 1 < width && mask[size_t(y * width + x1 + 1)] && !claimed[size_t(y * width + x1 + 1)]) ++x1;
185 int y1 = y;
186 bool extend = true;
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)]) {
190 extend = false;
191 break;
192 }
193 }
194 if (extend) ++y1;
195 }
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;
199 }
200 }
201}
202
204eve::Result<void> partitionFloor(int width, int depth, const std::vector<uint8_t>& mask,
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) {
209 struct Rect {
210 int x0, y0, x1, y1;
211 int area() const { return (x1 - x0 + 1) * (y1 - y0 + 1); }
212 };
213 int activeArea = 0;
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"));
220 if (target <= 1) return eve::Result<void>::success();
221
222 // Greedily decompose the mask into non-overlapping, fully active rectangles.
223 std::vector<uint8_t> claimed(mask.size(), 0);
224 std::vector<Rect> rects;
225 for (int y = 0; y < depth; ++y) {
226 for (int x = 0; x < width; ++x) {
227 const size_t origin = size_t(y * width + x);
228 if (!mask[origin] || claimed[origin]) continue;
229 int x1 = x;
230 while (x1 + 1 < width && mask[size_t(y * width + x1 + 1)] && !claimed[size_t(y * width + x1 + 1)]) ++x1;
231 int y1 = y;
232 bool extend = true;
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)]) {
236 extend = false;
237 break;
238 }
239 if (extend) ++y1;
240 }
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;
244 }
245 }
246 if (int(rects.size()) > target)
249 "requested room count cannot represent the irregular footprint without outside cells", "requiredRooms", {},
250 "housegen.generate"));
251
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)
257 eve::DiagnosticCode::Unsupported, "required room count cannot fit the active footprint",
258 "requiredRooms", {}, "housegen.generate"));
259 const Rect source = *splitIt;
260 rects.erase(splitIt);
261 if ((source.x1 - source.x0) >= (source.y1 - source.y0) && source.x0 < source.x1) {
262 const int cut = source.x0 + (source.x1 - source.x0) / 2;
263 rects.push_back({source.x0, source.y0, cut, source.y1});
264 rects.push_back({cut + 1, source.y0, source.x1, source.y1});
265 } else {
266 const int cut = source.y0 + (source.y1 - source.y0) / 2;
267 rects.push_back({source.x0, source.y0, source.x1, cut});
268 rects.push_back({source.x0, cut + 1, source.x1, source.y1});
269 }
270 }
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); });
273
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);
280 }
281
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);
287 };
288 std::unordered_set<std::string> doorBoundaries;
289 const auto emitBoundary = [&](int x, int y, int rotation, int roomA, int roomB) -> eve::Result<void> {
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);
294 if (!component)
297 std::string("no interior ") + (door ? "door" : "wall") +
298 " component allows rotation " + std::to_string(rotation),
299 "rotations", {}, "housegen.generate"));
300 instances.push_back({component->id, x, y, floorZ, rotation});
302 };
303
304 for (int y = 0; y < depth; ++y) {
305 for (int x = 0; x < width; ++x) {
306 const int room = roomAt[size_t(y * width + x)];
307 if (room < 0) continue;
308 if (x + 1 < width) {
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;
313 }
314 }
315 if (y + 1 < depth) {
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;
320 }
321 }
322 }
323 }
325}
326
327} // namespace
328
329eve::Result<void> HouseGenerator::generate(const HouseRequest &r, HouseLayout &out) const {
330 HouseLayout generated;
331 if (r.width < 3 || r.depth < 3 || r.floors < 1 || r.maxAttempts < 1) {
333 eve::DiagnosticCode::InvalidArgument, "house needs a 3x3 plot, at least one floor and one attempt", {}, {},
334 "housegen.generate"));
335 }
336 if (!std::isfinite(r.moduleSize) || r.moduleSize <= 0.f || !std::isfinite(r.floorHeight) || r.floorHeight <= 0.f)
338 eve::DiagnosticCode::InvalidArgument, "moduleSize and floorHeight must be finite and positive", {}, {},
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"));
346 }
347 if (r.footprint == "polygon" && r.perimeter.size() < 3) {
349 eve::DiagnosticCode::InvalidArgument, "polygon footprint needs at least 3 perimeter points", {}, {},
350 "housegen.generate"));
351 }
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)) {
359 eve::DiagnosticCode::NotFound, "library needs foundation, floor, wall, door and roof categories", {}, {},
360 "housegen.generate"));
361 }
362 // A named style must be a complete pack; otherwise per-category selection would
363 // silently mix it with unstyled components into a broken house.
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"));
369 }
370 // Interior partition and stairwell categories are optional: interior walls/doors are used
371 // only for an explicit multi-room request, stairs only for multi-floor houses.
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)))
378 eve::DiagnosticCode::NotFound, "multi-room generation needs interior_wall and interior_door categories",
379 "requiredRooms", {}, "housegen.generate"));
380
381 std::mt19937 rng(r.seed);
382 generated.seed = r.seed;
383 generated.moduleSize = r.moduleSize;
384 generated.floorHeight = r.floorHeight;
385 static constexpr const char *shapes[] = {"rectangle", "l_shape", "t_shape"};
386 static constexpr const char *roofs[] = {"gable", "flat", "shed"};
387 static constexpr SocketDirection sides[] = {SocketDirection::North, SocketDirection::East,
388 SocketDirection::South, SocketDirection::West};
389 generated.footprintStyle = r.footprint == "auto" ? shapes[rng() % 3]
390 : r.footprint == "polygon" ? "polygon"
391 : r.footprint;
392 generated.roofStyle = r.roof == "auto" ? roofs[rng() % 3] : r.roof;
393 const SocketDirection entranceDirection =
394 r.entrance == "auto" ? sides[rng() % 4] : directionFromName(r.entrance);
395 generated.entranceSide = directionName(entranceDirection);
396
397 // A multi-floor house needs a vertical stair column through an interior cell.
398 const bool isPolygon = generated.footprintStyle == "polygon";
399 const auto makeMask = [&](int ins) {
400 return isPolygon ? polygonMask(r.perimeter, r.width, r.depth, 0)
401 : footprintMask(generated.footprintStyle, r.width, r.depth, ins);
402 };
403 const auto baseMask = makeMask(0);
404 generated.footprintWidth = r.width;
405 generated.footprintDepth = r.depth;
406 generated.footprintMask = baseMask;
407 std::optional<std::pair<int, int>> stairwell;
408 if (r.floors > 1 && has(stairs)) {
409 stairwell = findInteriorCell(baseMask, r.width, r.depth);
410 if (!stairwell)
412 eve::DiagnosticCode::Unsupported, "multi-floor house needs an interior cell for a stairwell", {},
413 {}, "housegen.generate"));
414 }
415
416 // Grammar pass: construct a connected footprint mask, then emit one module per exposed face.
417 // Upper masks are monotonically inset, preserving a direct vertical support chain.
418 int inset = 0;
419 std::vector<uint8_t> previousMask;
420 for (int z = 0; z < r.floors; ++z) {
421 // Upper floors may step inward, but can never expand again above an inset floor. This
422 // gives every floor cell a direct support chain to the foundation.
423 if (!isPolygon && z > 0 && inset == 0 && r.width > 4 && r.depth > 4 && (rng() & 3u) == 0u)
424 inset = 1;
425 const auto mask = makeMask(inset);
426 // Any lower-floor cell not covered by this floor becomes a roof terrace/canopy at the
427 // current level. Thus every floor cell is covered by either another floor or a roof.
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) &&
431 !active(mask, r.width, r.depth, x, y))
432 generated.instances.push_back({pick(roof, rng)->id, x, y, z, 0});
433 }
434 }
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);
439 // The stair column replaces foundation+floor on every level it passes through.
440 if (stairwell && x == stairwell->first && y == stairwell->second) {
441 generated.instances.push_back({pick(stairs, rng)->id, x, y, z, 0});
442 continue;
443 }
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});
446 }
447 if (maxX < minX || maxY < minY)
449 eve::DiagnosticCode::Failed, "footprint collapsed after inset", {}, {}, "housegen.generate"));
450 if (!partitionsInteriors)
451 appendRoomRegions(r.width, r.depth, mask, z == 0 ? "living" : "upper", z, generated.rooms);
452
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) {
458 if (!active(mask, r.width, r.depth, x, y)) continue;
459 const SocketDirection directions[] = {SocketDirection::North, SocketDirection::East,
460 SocketDirection::South, SocketDirection::West};
461 const int dx[] = {0, 1, 0, -1};
462 const int dy[] = {-1, 0, 1, 0};
463 for (int side = 0; side < 4; ++side) if (!active(mask, r.width, r.depth, x + dx[side], y + dy[side])) {
464 Face face{x, y, directions[side]};
465 faces.push_back(face);
466 ++facesPerCell[y * r.width + x];
467 if (z == 0 && directions[side] == entranceDirection) entranceCandidates.push_back(face);
468 }
469 }
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));
472 });
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);
480 if (!isEntrance) {
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);
487 }
488 const auto *selected = pick(choices, rng);
489 if (!selected) {
492 "no socket-compatible " + std::string(isEntrance ? "door" : "wall") + " component", {}, {},
493 "housegen.generate"));
494 }
495 generated.instances.push_back({selected->id, x, y, z, rotation});
496 }
497 if (partitionsInteriors) {
498 auto partitioned = partitionFloor(r.width, r.depth, mask, r.requiredRooms, rng, innerWall, innerDoor,
499 generated.instances, generated.rooms, z);
500 if (!partitioned.ok()) return eve::Result<void>::failure(partitioned.status());
501 }
502 if (z + 1 == r.floors) {
503 for (int y = 0; y < r.depth; ++y) for (int x = 0; x < r.width; ++x)
504 if (active(mask, r.width, r.depth, x, y))
505 generated.instances.push_back({pick(roof, rng)->id, x, y, z + 1, 0});
506 }
507 previousMask = mask;
508 }
509 // Expand the canonical footprint to every cell occupied by rotated multi-cell components.
510 int occupiedWidth = r.width, occupiedDepth = r.depth;
511 for (const HouseInstance& instance : generated.instances) {
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;
516 occupiedWidth =
517 std::max(occupiedWidth, instance.x + (quarter ? component->get().depth : component->get().width));
518 occupiedDepth =
519 std::max(occupiedDepth, instance.y + (quarter ? component->get().width : component->get().depth));
520 }
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)];
525 for (const HouseInstance& instance : generated.instances) {
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;
535 }
536 generated.footprintWidth = occupiedWidth;
537 generated.footprintDepth = occupiedDepth;
538 generated.footprintMask = std::move(occupiedMask);
539 auto validated = generated.validate(library_);
540 if (!validated.ok()) return validated;
541 out = std::move(generated);
543}
544
545} // namespace eve::housegen
LogicalId target
double value
bool & active
float y
Definition AnimClip.cpp:738
float x
Definition AnimClip.cpp:738
float z
Definition AnimClip.cpp:738
int mask
std::vector< BuildingInstanceSnapshot > instances
float degrees
Definition CardTypes.cpp:35
float py
std::map< std::string, Var > values
ShaderImageInput shape
float u
Definition Grass.cpp:233
float area
Definition Grass.cpp:62
double r
std::int32_t c
std::vector< Colorf > px
std::uint32_t width
std::array< float, 4 > rotation
std::set< std::string > choices
std::string name
MeleePoint3 b
Definition MeleeHit.cpp:41
MeleePoint3 a
Definition MeleeHit.cpp:40
size_t directions
Definition OnnxLstm.cpp:29
V3 origin
Definition RoadBake.cpp:138
RoadLaneDirection direction
float dy
float dx
const SquirrelValueOptions & options
Cell cell
CommandLogBoundary boundary
Anchor rule, see above.
ecs::EntityHandle side
int sides
Definition TreeMesh.cpp:322
const UnitySourceAsset & source
std::uint32_t depth
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.
Definition Diagnostic.h:125
Move-only operation result carrying either a value or Status.
Definition Result.h:155
static Result success(T value)
Construct a successful result owning value.
Definition Result.h:164
static Result failure(Status status)
Construct a failed result from a structured status.
Definition Result.h:175
static Status success(StatusCode code=StatusCode::Ok)
Construct a successful status with an explicit non-error outcome.
Definition Status.h:81
一次生成的房屋布局:实例 + 房间 + 元信息,可 JSON 序列化 / 实例化。
Definition HouseLayout.h:23
std::string footprintStyle
布局风格结果。
Definition HouseLayout.h:30
eve::Result< void > validate(const HouseComponentLibrary &library) const
校验布局是否满足组件库规则。
std::vector< HouseRoom > rooms
Definition HouseLayout.h:39
std::vector< uint8_t > footprintMask
Definition HouseLayout.h:36
float moduleSize
生成参数回显。
Definition HouseLayout.h:27
int footprintWidth
Canonical base-floor occupancy mask owned by this layout.
Definition HouseLayout.h:34
std::vector< HouseInstance > instances
组件实例 / 房间 / 诊断信息。
Definition HouseLayout.h:38
SocketDirection
组件连接点方向(上下 + 四向)。
bool pointInPolygon(const Vec2 &p, const Polygon &poly)
Point-in-polygon test (ray casting; boundary counts as inside).
布局中的单个组件实例(位置 + 旋转)。
一次房屋生成请求(参数集)。