载入中...
搜索中...
未找到
TerrainMultiTile.cpp
浏览该文件的文档.
2
3#include <algorithm>
4#include <cmath>
5#include <limits>
6#include <unordered_set>
10
11namespace eve::procgen {
12using namespace raster_detail;
13namespace {
14struct Candidate {
15 Heightmap* target;
16 Heightmap value;
17};
18struct Topology {
19 int width = 0;
20 int height = 0;
21 double spacingX = 0;
22 double spacingZ = 0;
23 double referenceX = 0;
24 double referenceZ = 0;
25};
26bool finite(double v) { return std::isfinite(v); }
27bool selected(const TerrainHeightTile& tile, bool worldMap, const std::unordered_set<std::string>& allowed) {
28 return tile.worldMap == worldMap && (allowed.empty() || allowed.contains(tile.name));
29}
30Result<TerrainMultiTileReport> fail(const char* message) {
32}
33Result<Topology> validateTopology(const std::vector<TerrainHeightTile>& tiles) {
34 if (tiles.empty())
36 Diagnostic::error(DiagnosticCode::InvalidArgument, "terrain.multitile: at least one tile required"));
37 Topology topology;
38 std::unordered_set<Heightmap*> targets;
39 for (const auto& tile : tiles) {
40 if (tile.name.empty() || !tile.heightmap || !validRaster(*tile.heightmap) || !finite(tile.originX) ||
41 !finite(tile.originZ) || !finite(tile.width) || !finite(tile.depth) || tile.width <= 0 || tile.depth <= 0 ||
42 tile.heightmap->getWidth() < 2 || tile.heightmap->getHeight() < 2 || !targets.insert(tile.heightmap).second)
44 DiagnosticCode::InvalidArgument, "terrain.multitile: valid named distinct height tiles required"));
45 if (!topology.width) {
46 topology.width = tile.heightmap->getWidth();
47 topology.height = tile.heightmap->getHeight();
48 topology.spacingX = tile.width / double(topology.width - 1);
49 topology.spacingZ = tile.depth / double(topology.height - 1);
50 topology.referenceX = tile.originX;
51 topology.referenceZ = tile.originZ;
52 } else if (tile.heightmap->getWidth() != topology.width || tile.heightmap->getHeight() != topology.height ||
53 std::abs(tile.width / double(topology.width - 1) - topology.spacingX) >
54 std::abs(topology.spacingX) * 1e-9 + 1e-12 ||
55 std::abs(tile.depth / double(topology.height - 1) - topology.spacingZ) >
56 std::abs(topology.spacingZ) * 1e-9 + 1e-12) {
59 "terrain.multitile: matching heightmap resolution and world pixel size required"));
60 }
61 const double tileX = (tile.originX - topology.referenceX) / tile.width;
62 const double tileZ = (tile.originZ - topology.referenceZ) / tile.depth;
63 if (std::abs(tileX - std::round(tileX)) > 1e-8 || std::abs(tileZ - std::round(tileZ)) > 1e-8)
65 DiagnosticCode::InvalidArgument, "terrain.multitile: tile origins must align to whole tile spans"));
66 }
68}
69} // namespace
70
72 const std::vector<TerrainOperationTile>& tiles, const TerrainStampSettings& settings,
73 TerrainOperationDomain domain, bool worldMapOperation, const std::vector<std::string>& validTerrainNames) {
74 if (tiles.empty() || !finite(settings.centerX) || !finite(settings.centerZ) || !finite(settings.width) ||
75 !finite(settings.depth) || !finite(settings.rotation) || settings.width <= 0 || settings.depth <= 0)
76 return fail("terrain.multitile.window: tiles and finite positive world operation required");
77 const bool heightmap = domain == TerrainOperationDomain::Heightmap;
78 if (!heightmap && domain != TerrainOperationDomain::Texture && domain != TerrainOperationDomain::Tree &&
81 return fail("terrain.multitile.window: unknown operation domain");
82 const int seam = heightmap ? 1 : 0;
83 std::unordered_set<std::string> allowed;
84 for (const auto& name : validTerrainNames)
85 if (name.empty() || !allowed.insert(name).second)
86 return fail("terrain.multitile.window: allow-list names must be nonempty and unique");
87
88 int resolutionX = 0, resolutionY = 0;
89 double spacingX = 0, spacingZ = 0, referenceX = 0, referenceZ = 0;
90 std::unordered_set<std::string> names;
91 for (const auto& tile : tiles) {
92 if (tile.name.empty() || !names.insert(tile.name).second || !finite(tile.originX) || !finite(tile.originZ) ||
93 !finite(tile.width) || !finite(tile.depth) || tile.width <= 0 || tile.depth <= 0 ||
94 tile.resolutionX <= seam || tile.resolutionY <= seam)
95 return fail("terrain.multitile.window: valid uniquely named tile descriptors required");
96 if (!resolutionX) {
97 resolutionX = tile.resolutionX;
98 resolutionY = tile.resolutionY;
99 spacingX = tile.width / double(resolutionX - seam);
100 spacingZ = tile.depth / double(resolutionY - seam);
101 referenceX = tile.originX;
102 referenceZ = tile.originZ;
103 } else if (tile.resolutionX != resolutionX || tile.resolutionY != resolutionY ||
104 std::abs(tile.width / double(resolutionX - seam) - spacingX) >
105 std::abs(spacingX) * 1e-9 + 1e-12 ||
106 std::abs(tile.depth / double(resolutionY - seam) - spacingZ) >
107 std::abs(spacingZ) * 1e-9 + 1e-12) {
108 return fail("terrain.multitile.window: matching resolution and world pixel size required");
109 }
110 const double tileX = (tile.originX - referenceX) / tile.width;
111 const double tileZ = (tile.originZ - referenceZ) / tile.depth;
112 if (std::abs(tileX - std::round(tileX)) > 1e-8 || std::abs(tileZ - std::round(tileZ)) > 1e-8)
113 return fail("terrain.multitile.window: tile origins must align to whole tile spans");
114 }
115
116 const double c = std::abs(std::cos(settings.rotation)), s = std::abs(std::sin(settings.rotation));
117 const double halfX = (c * settings.width + s * settings.depth) * 0.5;
118 const double halfZ = (s * settings.width + c * settings.depth) * 0.5;
119 const double minX = settings.centerX - halfX, maxX = settings.centerX + halfX;
120 const double minZ = settings.centerZ - halfZ, maxZ = settings.centerZ + halfZ;
121 if (!finite(minX) || !finite(maxX) || !finite(minZ) || !finite(maxZ))
122 return fail("terrain.multitile.window: rotated operation bounds overflow");
123 const double minPixelX = std::floor((minX - referenceX) / spacingX);
124 const double minPixelY = std::floor((minZ - referenceZ) / spacingZ);
125 const double maxPixelX = std::ceil((maxX - referenceX) / spacingX);
126 const double maxPixelY = std::ceil((maxZ - referenceZ) / spacingZ);
127 for (double value : {minPixelX, minPixelY, maxPixelX, maxPixelY})
128 if (value < std::numeric_limits<int>::min() || value > std::numeric_limits<int>::max())
129 return fail("terrain.multitile.window: operation pixel rectangle exceeds integer range");
130
132 report.operationX = int(minPixelX);
133 report.operationY = int(minPixelY);
134 const long long width64 = static_cast<long long>(int(maxPixelX)) - report.operationX + seam;
135 const long long height64 = static_cast<long long>(int(maxPixelY)) - report.operationY + seam;
136 if (width64 < 0 || height64 < 0 || width64 > std::numeric_limits<int>::max() ||
137 height64 > std::numeric_limits<int>::max())
138 return fail("terrain.multitile.window: operation pixel extent exceeds integer range");
139 report.operationWidth = int(width64);
140 report.operationHeight = int(height64);
141 const long long operationMaxX = static_cast<long long>(report.operationX) + report.operationWidth;
142 const long long operationMaxY = static_cast<long long>(report.operationY) + report.operationHeight;
143
144 for (const auto& tile : tiles) {
145 if (tile.worldMap != worldMapOperation || (!allowed.empty() && !allowed.contains(tile.name))) continue;
146 if (!(minX < tile.originX + tile.width && maxX > tile.originX && minZ < tile.originZ + tile.depth &&
147 maxZ > tile.originZ))
148 continue;
149 const long long coordinateX = std::llround((tile.originX - referenceX) / spacingX);
150 const long long coordinateY = std::llround((tile.originZ - referenceZ) / spacingZ);
151 const long long localX = std::max(0LL, static_cast<long long>(report.operationX) - coordinateX);
152 const long long localY = std::max(0LL, static_cast<long long>(report.operationY) - coordinateY);
153 const long long localMaxX = std::min(static_cast<long long>(resolutionX), operationMaxX - coordinateX);
154 const long long localMaxY = std::min(static_cast<long long>(resolutionY), operationMaxY - coordinateY);
155 const long long affectedWidth = std::max(0LL, localMaxX - localX);
156 const long long affectedHeight = std::max(0LL, localMaxY - localY);
157 for (long long value : {coordinateX, coordinateY, localX, localY, affectedWidth, affectedHeight})
158 if (value < std::numeric_limits<int>::min() || value > std::numeric_limits<int>::max())
159 return fail("terrain.multitile.window: affected pixel rectangle exceeds integer range");
160 if (!affectedWidth || !affectedHeight) continue;
161 report.mappings.push_back({tile.name, int(localX), int(localY),
162 int(localX + coordinateX - report.operationX),
163 int(localY + coordinateY - report.operationY), int(affectedWidth),
164 int(affectedHeight)});
165 }
166 report.affectedTiles = int(report.mappings.size());
167 return Result<TerrainMultiTileReport>::success(std::move(report));
168}
169
171 const std::vector<TerrainHeightTile>& tiles, const Heightmap& stamp, const TerrainStampSettings& settings,
172 const Heightmap& localMask, const Heightmap& globalMask, bool worldMapOperation,
173 const std::vector<std::string>& validTerrainNames) {
174 if (tiles.empty() || !validRaster(stamp) || !validRaster(localMask) || !validRaster(globalMask))
175 return fail("terrain.multitile: tiles and finite operation rasters required");
176 if (!finite(settings.centerX) || !finite(settings.centerZ) || !finite(settings.width) ||
177 !finite(settings.depth) || !finite(settings.rotation) || settings.width <= 0 || settings.depth <= 0)
178 return fail("terrain.multitile: finite positive world operation required");
179 std::unordered_set<std::string> allowed;
180 for (const auto& name : validTerrainNames)
181 if (name.empty() || !allowed.insert(name).second)
182 return fail("terrain.multitile: allow-list names must be nonempty and unique");
183
184 auto topologyResult = validateTopology(tiles);
185 if (!topologyResult.ok()) return Result<TerrainMultiTileReport>::failure(topologyResult.status());
186 const auto& [referenceWidth, referenceHeight, spacingX, spacingZ, referenceX, referenceZ] =
187 topologyResult.value();
188
189 const double c = std::abs(std::cos(settings.rotation)), s = std::abs(std::sin(settings.rotation));
190 const double halfX = (c * settings.width + s * settings.depth) * 0.5;
191 const double halfZ = (s * settings.width + c * settings.depth) * 0.5;
192 const double minX = settings.centerX - halfX, maxX = settings.centerX + halfX;
193 const double minZ = settings.centerZ - halfZ, maxZ = settings.centerZ + halfZ;
194 if (!finite(minX) || !finite(maxX) || !finite(minZ) || !finite(maxZ))
195 return fail("terrain.multitile: rotated operation bounds overflow");
196
198 report.operationX = int(std::floor((minX - referenceX) / spacingX));
199 report.operationY = int(std::floor((minZ - referenceZ) / spacingZ));
200 const double opMaxXd = std::ceil((maxX - referenceX) / spacingX);
201 const double opMaxYd = std::ceil((maxZ - referenceZ) / spacingZ);
202 for (double v : {double(report.operationX), double(report.operationY), opMaxXd, opMaxYd})
203 if (v < std::numeric_limits<int>::min() || v > std::numeric_limits<int>::max())
204 return fail("terrain.multitile: operation pixel rectangle exceeds integer range");
205 const int opMaxX = int(opMaxXd), opMaxY = int(opMaxYd);
206 if (opMaxX < report.operationX || opMaxY < report.operationY)
207 return fail("terrain.multitile: invalid operation pixel rectangle");
208 const long long operationWidth = static_cast<long long>(opMaxX) - report.operationX + 1;
209 const long long operationHeight = static_cast<long long>(opMaxY) - report.operationY + 1;
210 if (operationWidth > std::numeric_limits<int>::max() || operationHeight > std::numeric_limits<int>::max())
211 return fail("terrain.multitile: operation pixel extent exceeds integer range");
212 report.operationWidth = int(operationWidth);
213 report.operationHeight = int(operationHeight);
214
215 std::vector<Candidate> candidates;
216 for (const auto& tile : tiles) {
217 if (!selected(tile, worldMapOperation, allowed)) continue;
218 // Strict area overlap excludes a neighbor that only touches the brush AABB at its border.
219 if (!(minX < tile.originX + tile.width && maxX > tile.originX &&
220 minZ < tile.originZ + tile.depth && maxZ > tile.originZ)) continue;
221 const long long coordinateX = std::llround((tile.originX - referenceX) / spacingX);
222 const long long coordinateY = std::llround((tile.originZ - referenceZ) / spacingZ);
223 if (coordinateX < std::numeric_limits<int>::min() || coordinateX > std::numeric_limits<int>::max() ||
224 coordinateY < std::numeric_limits<int>::min() || coordinateY > std::numeric_limits<int>::max())
225 return fail("terrain.multitile: tile pixel coordinate exceeds integer range");
226 const auto localX64 = std::max(0LL, static_cast<long long>(report.operationX) - coordinateX);
227 const auto localY64 = std::max(0LL, static_cast<long long>(report.operationY) - coordinateY);
228 const auto localMaxX64 = std::min(static_cast<long long>(referenceWidth),
229 static_cast<long long>(opMaxX) - coordinateX + 1);
230 const auto localMaxY64 = std::min(static_cast<long long>(referenceHeight),
231 static_cast<long long>(opMaxY) - coordinateY + 1);
232 if (localX64 > std::numeric_limits<int>::max() || localY64 > std::numeric_limits<int>::max() ||
233 localMaxX64 < std::numeric_limits<int>::min() || localMaxY64 < std::numeric_limits<int>::min())
234 return fail("terrain.multitile: affected pixel rectangle exceeds integer range");
235 const int localX = int(localX64), localY = int(localY64);
236 const int localMaxX = int(localMaxX64), localMaxY = int(localMaxY64);
237 const int width = std::max(0, localMaxX - localX), height = std::max(0, localMaxY - localY);
238 if (!width || !height) continue;
239 report.mappings.push_back({tile.name, localX, localY,
240 localX + int(coordinateX) - report.operationX,
241 localY + int(coordinateY) - report.operationY, width, height});
242 TerrainStampSettings localSettings = settings;
243 localSettings.originX = tile.originX; localSettings.originZ = tile.originZ;
244 localSettings.spacingX = spacingX; localSettings.spacingZ = spacingZ;
245 Heightmap candidate = *tile.heightmap;
246 auto changed = applyTerrainStamp(candidate, stamp, localSettings, localMask, globalMask);
247 if (!changed.ok()) return Result<TerrainMultiTileReport>::failure(changed.status());
248 if (report.changedSamples > std::numeric_limits<int>::max() - changed.value())
249 return fail("terrain.multitile: changed sample count exceeds integer range");
250 report.changedSamples += changed.value();
251 candidates.push_back({tile.heightmap, std::move(candidate)});
252 }
253 report.affectedTiles = int(candidates.size());
254 for (auto& candidate : candidates) *candidate.target = std::move(candidate.value);
255 return Result<TerrainMultiTileReport>::success(std::move(report));
256}
257
260 if (!terrainA.heightmap || !terrainB.heightmap || terrainA.heightmap == terrainB.heightmap ||
261 !validRaster(*terrainA.heightmap) || !validRaster(*terrainB.heightmap) ||
262 terrainA.heightmap->getWidth() != terrainB.heightmap->getWidth() ||
263 terrainA.heightmap->getHeight() != terrainB.heightmap->getHeight() ||
264 terrainA.heightmap->getWidth() != terrainA.heightmap->getHeight() ||
265 !finite(terrainA.originX) || !finite(terrainA.originZ) || !finite(terrainA.width) ||
266 !finite(terrainA.depth) || !finite(terrainB.originX) || !finite(terrainB.originZ) ||
267 !finite(terrainB.width) || !finite(terrainB.depth) || terrainA.width <= 0 || terrainA.depth <= 0 ||
268 terrainB.width <= 0 || terrainB.depth <= 0 || settings.extraSeamSize < 1 ||
269 settings.extraSeamSize >= terrainA.heightmap->getWidth() || !std::isfinite(settings.maxDifference) ||
270 settings.maxDifference < 0)
272 DiagnosticCode::InvalidArgument, "terrain.stitch: distinct matching square tiles and valid seam required"));
273 const int resolution = terrainA.heightmap->getWidth();
274 const double spacingX = terrainA.width / double(resolution - 1);
275 const double spacingZ = terrainA.depth / double(resolution - 1);
276 if (std::abs(spacingX - terrainB.width / double(resolution - 1)) > 1e-6 ||
277 std::abs(spacingZ - terrainB.depth / double(resolution - 1)) > 1e-6)
279 "terrain.stitch: matching sample spacing required"));
280 enum class Direction { North, South, West, East };
281 Direction direction;
282 const double dx = terrainA.originX - terrainB.originX;
283 const double dz = terrainA.originZ - terrainB.originZ;
284 if (std::abs(dx) > std::abs(dz)) direction = dx > 0 ? Direction::West : Direction::East;
285 else direction = dz > 0 ? Direction::South : Direction::North;
286 const double edgeError = direction == Direction::North
287 ? std::abs(terrainA.originZ + terrainA.depth - terrainB.originZ)
288 : direction == Direction::South
289 ? std::abs(terrainB.originZ + terrainB.depth - terrainA.originZ)
290 : direction == Direction::East
291 ? std::abs(terrainA.originX + terrainA.width - terrainB.originX)
292 : std::abs(terrainB.originX + terrainB.width - terrainA.originX);
293 const double edgeSpacing = direction == Direction::North || direction == Direction::South ? spacingZ : spacingX;
294 if (edgeError > edgeSpacing * 0.01)
296 Diagnostic::error(DiagnosticCode::InvalidArgument, "terrain.stitch: tiles do not share an edge"));
297 const bool northSouth = direction == Direction::North || direction == Direction::South;
298 const double alongDelta = northSouth ? terrainA.originX - terrainB.originX
299 : terrainA.originZ - terrainB.originZ;
300 const double alongSpacing = northSouth ? spacingX : spacingZ;
301 const int offset = static_cast<int>(std::lround(std::abs(alongDelta) / alongSpacing));
302 const int seamLength = resolution - offset;
303 if (seamLength <= 0 || std::abs(std::abs(alongDelta) / alongSpacing - offset) > 1e-5)
305 "terrain.stitch: overlapping grid-aligned edge required"));
306 const int startA = alongDelta < 0 ? offset : 0;
307 const int startB = alongDelta > 0 ? offset : 0;
308 Heightmap nextA = *terrainA.heightmap, nextB = *terrainB.heightmap;
309 const int seam = settings.extraSeamSize;
310 auto coord = [&](Direction side, bool first, int along, int depth) {
311 const int p = (first ? startA : startB) + along;
312 switch (side) {
313 case Direction::North: return std::pair{p, resolution - 1 - depth};
314 case Direction::South: return std::pair{p, depth};
315 case Direction::East: return std::pair{resolution - 1 - depth, p};
316 case Direction::West: return std::pair{depth, p};
317 }
318 return std::pair{0, 0};
319 };
320 const Direction opposite = direction == Direction::North ? Direction::South
321 : direction == Direction::South ? Direction::North
322 : direction == Direction::East ? Direction::West
323 : Direction::East;
324 for (int along = 0; along < seamLength; ++along) {
325 const auto [aInnerX, aInnerY] = coord(direction, true, along, seam);
326 const auto [bInnerX, bInnerY] = coord(opposite, false, along, seam);
327 const float aEnd = terrainA.heightmap->height(aInnerX, aInnerY);
328 const float bEnd = terrainB.heightmap->height(bInnerX, bInnerY);
329 for (int d = 1; d < seam * 2; ++d) {
330 if (d == seam) continue;
331 const float linearT = seam == 1 ? 0.5F : float(d - 1) / float(seam * 2 - 2);
332 const float linear = std::lerp(aEnd, bEnd, linearT);
333 if (d < seam) {
334 const auto [x, y] = coord(direction, true, along, seam - d);
335 nextA.setHeight(x, y, std::lerp(terrainA.heightmap->height(x, y), linear, float(d) / seam));
336 } else {
337 const int depth = d - seam;
338 const auto [x, y] = coord(opposite, false, along, depth);
339 nextB.setHeight(x, y,
340 std::lerp(linear, terrainB.heightmap->height(x, y), float(depth) / seam));
341 }
342 }
343 const auto [aNearX, aNearY] = coord(direction, true, along, 1);
344 const auto [bNearX, bNearY] = coord(opposite, false, along, 1);
345 const float joined = (nextA.height(aNearX, aNearY) + nextB.height(bNearX, bNearY)) * 0.5F;
346 const auto [aEdgeX, aEdgeY] = coord(direction, true, along, 0);
347 const auto [bEdgeX, bEdgeY] = coord(opposite, false, along, 0);
348 nextA.setHeight(aEdgeX, aEdgeY, joined);
349 nextB.setHeight(bEdgeX, bEdgeY, joined);
350 }
351 int changed = 0;
352 for (std::size_t i = 0; i < nextA.data().size(); ++i) {
353 changed += nextA.data()[i] != terrainA.heightmap->data()[i];
354 changed += nextB.data()[i] != terrainB.heightmap->data()[i];
355 }
356 *terrainA.heightmap = std::move(nextA);
357 *terrainB.heightmap = std::move(nextB);
358 return Result<int>::success(changed);
359}
360
362 struct Tile {
363 std::string name;
367 };
368 std::vector<Tile> tiles;
370 struct Snapshot {
371 std::vector<Heightmap> heightmaps;
373 };
374 std::vector<Snapshot> history;
375 int cursor = 0;
377 Snapshot result;
378 result.heightmaps.reserve(tiles.size());
379 for (const auto& tile : tiles) result.heightmaps.push_back(tile.heightmap);
380 result.report = last;
381 return result;
382 }
383 void restore(const Snapshot& snapshot) {
384 for (size_t i = 0; i < tiles.size(); ++i) tiles[i].heightmap = snapshot.heightmaps[i];
386 }
387};
388
393
394Result<int> TerrainMultiTileWorkspace::addTile(const std::string& name, const Heightmap& heightmap, double originX,
395 double originZ, double width, double depth, bool worldMap) {
396 if (!impl_ || name.empty() || !validRaster(heightmap) || heightmap.getWidth() < 2 || heightmap.getHeight() < 2 ||
397 !finite(originX) || !finite(originZ) || !finite(width) || !finite(depth) || width <= 0 || depth <= 0)
399 "terrain.multitile.workspace: valid tile required"));
400 if (std::any_of(impl_->tiles.begin(), impl_->tiles.end(), [&](const auto& tile) { return tile.name == name; }))
402 "terrain.multitile.workspace: duplicate tile name"));
403 if (impl_->history.size() > 1)
405 "terrain.multitile.workspace: topology is fixed after stamping"));
406 auto candidate = std::make_unique<Impl>(*impl_);
407 candidate->tiles.push_back({name, heightmap, originX, originZ, width, depth, worldMap});
408 // Validate the complete topology now, including resolution, pixel size and origin alignment.
409 std::vector<TerrainHeightTile> descriptors;
410 descriptors.reserve(candidate->tiles.size());
411 for (auto& tile : candidate->tiles)
412 descriptors.push_back({tile.name, &tile.heightmap, tile.originX, tile.originZ, tile.width, tile.depth,
413 tile.worldMap});
414 auto checked = validateTopology(descriptors);
415 if (!checked.ok()) return Result<int>::failure(checked.status());
416 candidate->history.clear();
417 candidate->history.push_back(candidate->snapshot());
418 candidate->cursor = 0;
419 impl_.swap(candidate);
420 return Result<int>::success(int(impl_->tiles.size()));
421}
422
423Result<int> TerrainMultiTileWorkspace::stamp(const Heightmap& stampRaster, const TerrainStampSettings& settings,
424 const Heightmap& localMask, const Heightmap& globalMask,
425 bool worldMapOperation) {
426 if (!impl_)
428 "terrain.multitile.workspace: moved-from workspace"));
429 auto candidate = std::make_unique<Impl>(*impl_);
430 std::vector<TerrainHeightTile> descriptors;
431 descriptors.reserve(candidate->tiles.size());
432 for (auto& tile : candidate->tiles)
433 descriptors.push_back({tile.name, &tile.heightmap, tile.originX, tile.originZ, tile.width, tile.depth,
434 tile.worldMap});
435 auto result = applyTerrainStampMultiTile(descriptors, stampRaster, settings, localMask, globalMask,
436 worldMapOperation);
437 if (!result.ok()) return Result<int>::failure(result.status());
438 candidate->last = std::move(result.value());
439 candidate->history.resize(size_t(candidate->cursor + 1));
440 candidate->history.push_back(candidate->snapshot());
441 ++candidate->cursor;
442 const int changed = candidate->last.changedSamples;
443 impl_.swap(candidate);
444 return Result<int>::success(changed);
445}
446
447Result<int> TerrainMultiTileWorkspace::copyTile(const std::string& name, Heightmap& output) const {
448 if (!impl_ || !validRaster(output))
450 "terrain.multitile.workspace: valid output required"));
451 auto found = std::find_if(impl_->tiles.begin(), impl_->tiles.end(), [&](const auto& tile) { return tile.name == name; });
452 if (found == impl_->tiles.end() || output.getWidth() != found->heightmap.getWidth() ||
453 output.getHeight() != found->heightmap.getHeight())
455 "terrain.multitile.workspace: named matching output required"));
456 return publish(output, found->heightmap.data());
457}
458Result<int> TerrainMultiTileWorkspace::stitch(const std::string& terrainA, const std::string& terrainB,
460 if (!impl_ || terrainA == terrainB)
462 Diagnostic::error(DiagnosticCode::InvalidArgument, "terrain.multitile.workspace: two tile names required"));
463 auto candidate = std::make_unique<Impl>(*impl_);
464 auto findTile = [&](const std::string& name) {
465 return std::find_if(candidate->tiles.begin(), candidate->tiles.end(),
466 [&](const auto& tile) { return tile.name == name; });
467 };
468 auto a = findTile(terrainA), b = findTile(terrainB);
469 if (a == candidate->tiles.end() || b == candidate->tiles.end())
471 Diagnostic::error(DiagnosticCode::InvalidArgument, "terrain.multitile.workspace: stitch tile not found"));
472 TerrainHeightTile descA{a->name, &a->heightmap, a->originX, a->originZ, a->width, a->depth, a->worldMap};
473 TerrainHeightTile descB{b->name, &b->heightmap, b->originX, b->originZ, b->width, b->depth, b->worldMap};
474 auto result = stitchTerrainHeightmaps(descA, descB, settings);
475 if (!result.ok()) return result;
476 candidate->last = {};
477 candidate->last.affectedTiles = 2;
478 candidate->last.changedSamples = result.value();
479 candidate->history.resize(static_cast<std::size_t>(candidate->cursor + 1));
480 candidate->history.push_back(candidate->snapshot());
481 ++candidate->cursor;
482 impl_.swap(candidate);
483 return result;
484}
485int TerrainMultiTileWorkspace::getTileCount() const noexcept { return impl_ ? int(impl_->tiles.size()) : 0; }
486int TerrainMultiTileWorkspace::getLastAffectedTiles() const noexcept { return impl_ ? impl_->last.affectedTiles : 0; }
487int TerrainMultiTileWorkspace::getLastChangedSamples() const noexcept { return impl_ ? impl_->last.changedSamples : 0; }
488int TerrainMultiTileWorkspace::getLastMappingCount() const noexcept {
489 return impl_ ? int(impl_->last.mappings.size()) : 0;
490}
491Result<int> TerrainMultiTileWorkspace::undo() {
492 if (!impl_ || impl_->cursor <= 0)
494 "terrain.multitile.workspace: no undo available"));
495 auto candidate = std::make_unique<Impl>(*impl_);
496 int changed = 0;
497 const auto& snapshot = candidate->history[size_t(candidate->cursor - 1)];
498 for (size_t t = 0; t < candidate->tiles.size(); ++t)
499 for (size_t i = 0; i < candidate->tiles[t].heightmap.data().size(); ++i)
500 changed += candidate->tiles[t].heightmap.data()[i] != snapshot.heightmaps[t].data()[i];
501 --candidate->cursor;
502 candidate->restore(snapshot);
503 impl_.swap(candidate);
504 return Result<int>::success(changed);
505}
506Result<int> TerrainMultiTileWorkspace::redo() {
507 if (!impl_ || impl_->cursor < 0 || size_t(impl_->cursor + 1) >= impl_->history.size())
509 "terrain.multitile.workspace: no redo available"));
510 auto candidate = std::make_unique<Impl>(*impl_);
511 int changed = 0;
512 const auto& snapshot = candidate->history[size_t(candidate->cursor + 1)];
513 for (size_t t = 0; t < candidate->tiles.size(); ++t)
514 for (size_t i = 0; i < candidate->tiles[t].heightmap.data().size(); ++i)
515 changed += candidate->tiles[t].heightmap.data()[i] != snapshot.heightmaps[t].data()[i];
516 ++candidate->cursor;
517 candidate->restore(snapshot);
518 impl_.swap(candidate);
519 return Result<int>::success(changed);
520}
521int TerrainMultiTileWorkspace::getOperationCount() const noexcept {
522 return impl_ && !impl_->history.empty() ? int(impl_->history.size() - 1) : 0;
523}
524int TerrainMultiTileWorkspace::getAppliedCount() const noexcept { return impl_ ? impl_->cursor : 0; }
525} // namespace eve::procgen
LogicalId target
double value
float y
Definition AnimClip.cpp:738
float x
Definition AnimClip.cpp:738
std::string output
const std::string & s
glm::vec4 p[6]
std::string message
bool seam
float v
std::int32_t c
std::int32_t first
std::uint32_t height
std::uint32_t width
size_t offset
std::string name
MeleePoint3 b
Definition MeleeHit.cpp:41
MeleePoint3 a
Definition MeleeHit.cpp:40
bool finite
Topology topology
float d
float t
RoadLaneDirection direction
bool found
float dz
float dx
ecs::EntityHandle side
TerrainThermalSettings settings
Heightmap stamp
double referenceX
double spacingZ
double spacingX
double referenceZ
float size
Definition TreeMesh.cpp:156
std::uint32_t depth
std::vector< WfcTile > tiles
Definition WfcSimple.cpp:22
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
In-memory terrain heightmap: a dense float grid (row-major, index = y * width + x) materialized from ...
Definition Heightmap.h:21
int getHeight() const
Returns the height.
Definition Heightmap.cpp:17
const std::vector< float > & data() const
Data.
Definition Heightmap.h:48
float height(int x, int y) const
Height.
Definition Heightmap.cpp:28
void setHeight(int x, int y, float h)
Sets the height.
Definition Heightmap.cpp:23
int getWidth() const
Returns the width.
Definition Heightmap.cpp:16
Owning script-safe collection of height tiles with atomic multi-tile operations. Tiles are copied on ...
~TerrainMultiTileWorkspace()
Terrain multi tile workspace.
TerrainMultiTileWorkspace()
Terrain multi tile workspace.
Result< int > publish(Heightmap &target, std::vector< float > candidate)
Publish.
bool validRaster(const Heightmap &map)
Valid raster.
Result< TerrainMultiTileReport > applyTerrainStampMultiTile(const std::vector< TerrainHeightTile > &tiles, const Heightmap &stamp, const TerrainStampSettings &settings, const Heightmap &localMask, const Heightmap &globalMask, bool worldMapOperation, const std::vector< std::string > &validTerrainNames)
Apply one world-space stamp to all selected intersecting height tiles atomically.
Result< TerrainMultiTileReport > mapTerrainOperationMultiTile(const std::vector< TerrainOperationTile > &tiles, const TerrainStampSettings &settings, TerrainOperationDomain domain, bool worldMapOperation, const std::vector< std::string > &validTerrainNames)
Calculate Pcg-compatible local and shared pixel rectangles for any multi-terrain raster domain.
Result< int > stitchTerrainHeightmaps(const TerrainHeightTile &terrainA, const TerrainHeightTile &terrainB, const TerrainHeightStitchSettings &settings)
Stitch the overlapping shared edge of two grid-aligned height tiles with Pcg's seam interpolation.
Result< int > applyTerrainStamp(Heightmap &target, const Heightmap &stamp, const TerrainStampSettings &s, const Heightmap &localMask, const Heightmap &globalMask)
Apply a rotated, bilinearly sampled stamp; publish only after all checks succeed.
TerrainOperationDomain
Pcg multi-terrain pixel domain; only heightmaps share the border sample between adjacent tiles.
Pcg terrain-height seam controls; maxDifference is retained for source-profile compatibility.
Borrowed mutable height tile participating in one synchronous world operation.
Completed multi-tile stamp statistics and Pcg-compatible affected-pixel mappings.
std::vector< TerrainAffectedPixels > mappings
Value configuration for a rectangular stamp in world X/Z coordinates.