载入中...
搜索中...
未找到
BuildingFx.cpp
浏览该文件的文档.
2
4#include "building/Ghost.h"
8#include "grid/GridConfig.h"
9#include "common/ECS.h"
10#include "graphics/Graphics.h"
11#include "graphics/Mesh.h"
14
15#include <simplesquirrel/simplesquirrel.hpp>
16
17#include <glm/gtc/matrix_transform.hpp>
18#include <glm/gtx/euler_angles.hpp>
19
20#include <algorithm>
21#include <cmath>
22
23namespace eve::buildingfx {
24
25// Color lives in eve::graphics (see graphics/Canvas.h); keep the unqualified form.
27
29
30namespace {
31
32float toFloat(const std::string &s, float fallback) {
33 if (s.empty()) return fallback;
34 try {
35 return std::stof(s);
36 } catch (...) {
37 return fallback;
38 }
39}
40
41graphics::Graphics *gfxOrNull() { return getModInst(graphics, Graphics); }
42
43bool toBool(const std::string &value, bool fallback);
44
45std::string visualValue(const building::BuildingDefinition &definition,
46 const building::PlacedBuilding &placed,
47 const building::PlacementWorld &world, bool use3d,
48 const std::string &key) {
49 const std::string base = use3d ? definition.getVisual3d(key) : definition.getVisual2d(key);
50 if (placed.placementKind != "edge") return base;
51 const std::string variant = building::PlacementSystem::edgeVariant(world, placed.instanceId);
53 const std::string maskKey =
54 "variant." + variant + ".mask." + std::to_string(mask) + "." + key;
55 const std::string variantKey = "variant." + variant + "." + key;
56 const std::string classValue = use3d ? definition.getVisual3d(variantKey, base)
57 : definition.getVisual2d(variantKey, base);
58 return use3d ? definition.getVisual3d(maskKey, classValue)
59 : definition.getVisual2d(maskKey, classValue);
60}
61
62struct TopologyTransform {
63 std::string variant;
64 int mask = 0;
65 float rotationDegrees = 0.f;
66 bool mirrorX = false;
67 bool mirrorZ = false;
68};
69
70TopologyTransform topologyTransform(const building::BuildingDefinition &definition,
71 const building::PlacedBuilding &placed,
72 const building::PlacementWorld &world, bool use3d) {
73 TopologyTransform result;
74 if (placed.placementKind != "edge") return result;
75 result.variant = building::PlacementSystem::edgeVariant(world, placed.instanceId);
77 if (result.variant == "end") result.mirrorX = (result.mask & 0x01) != 0;
78 if (result.variant == "corner") {
79 result.mirrorX = (result.mask & 0x0c) != 0;
80 result.mirrorZ = (result.mask & 0x14) != 0;
81 }
82 result.rotationDegrees =
83 toFloat(visualValue(definition, placed, world, use3d, "rotationDeg"), 0.f);
84 result.mirrorX = toBool(visualValue(definition, placed, world, use3d, "mirrorX"),
85 result.mirrorX);
86 result.mirrorZ = toBool(visualValue(definition, placed, world, use3d, "mirrorZ"),
87 result.mirrorZ);
88 return result;
89}
90
91void placedWorldPosition(const building::PlacedBuilding &placed,
92 const building::PlacementWorld &world, float &x, float &y, float &z) {
93 if (world.getGrid().plane == grid::GridPlane::XZ) {
94 x = placed.worldX;
95 y = placed.elevation + float(placed.level) * world.getFloorHeight();
96 z = placed.worldY;
97 // Cell poses name the minimum footprint corner, while meshes are centered.
98 if (placed.placementKind == "cell") {
99 if (const auto *definition = building::BuildingRegistry::find(placed.buildingId)) {
100 const auto snapMode = definition->snapMode.empty() ? world.getSnapMode() : definition->snapMode;
101 if (snapMode != "grid") return;
102 int width = 1, depth = 1;
104 float startX = 0.f, startZ = 0.f, endX = 0.f, endZ = 0.f;
105 world.cellToWorldPlane(placed.originCellX, placed.originCellY, startX, startZ);
106 world.cellToWorldPlane(placed.originCellX + width, placed.originCellY + depth, endX, endZ);
107 x += (endX - startX) * 0.5f;
108 z += (endZ - startZ) * 0.5f;
109 }
110 }
111 } else {
112 x = placed.worldX;
113 y = placed.worldY;
114 z = placed.elevation + float(placed.level) * world.getFloorHeight();
115 }
116}
117
118bool toBool(const std::string &value, bool fallback) {
119 if (value.empty()) return fallback;
120 if (value == "1" || value == "true" || value == "yes") return true;
121 if (value == "0" || value == "false" || value == "no") return false;
122 return fallback;
123}
124
125float cornerVisualSize(const building::BuildingDefinition &definition,
126 const building::PlacedBuilding &placed,
127 const building::PlacementWorld &world, bool use3d,
128 const std::string &axisKey) {
129 const float fallback = std::min(world.getGrid().cellW, world.getGrid().cellH) * 0.2f;
130 const std::string axisValue = visualValue(definition, placed, world, use3d, axisKey);
131 if (!axisValue.empty()) return toFloat(axisValue, fallback);
132 return toFloat(visualValue(definition, placed, world, use3d, "size"), fallback);
133}
134
135float freeVisualSize(const building::BuildingDefinition &definition,
136 const building::PlacedBuilding &placed,
137 const building::PlacementWorld &world, bool use3d,
138 const std::string &axisKey) {
139 float fallback = placed.freeRadius > 0.f
140 ? placed.freeRadius * 2.f
141 : definition.freeRadiusCells * 2.f *
142 std::min(world.getGrid().cellW, world.getGrid().cellH);
143 if (placed.freeHalfWidth > 0.f && placed.freeHalfHeight > 0.f) {
144 fallback = axisKey == "width" ? placed.freeHalfWidth * 2.f
145 : placed.freeHalfHeight * 2.f;
146 }
147 return toFloat(visualValue(definition, placed, world, use3d, axisKey), fallback);
148}
149
150void applySurfaceRotation(const building::PlacedBuilding &placed, float rotationDegrees,
151 graphics::Renderable3D::Transform3D &transform) {
152 if (placed.surfaceId.empty()) return;
153 const glm::vec3 tangent(placed.surfaceTangentX, placed.surfaceTangentY,
154 placed.surfaceTangentZ);
155 const glm::vec3 normal(placed.surfaceNormalX, placed.surfaceNormalY,
156 placed.surfaceNormalZ);
157 const glm::vec3 bitangent = glm::normalize(glm::cross(tangent, normal));
158 glm::mat4 frame(1.f);
159 frame[0] = glm::vec4(tangent, 0.f);
160 frame[1] = glm::vec4(normal, 0.f);
161 frame[2] = glm::vec4(bitangent, 0.f);
162 frame *= glm::rotate(glm::mat4(1.f), glm::radians(rotationDegrees),
163 glm::vec3(0.f, 1.f, 0.f));
164 glm::extractEulerAngleYXZ(frame, transform.yaw, transform.pitch, transform.roll);
165}
166
167} // namespace
168
171 const std::vector<CurveControlPoint> &controlPoints, int subdivisions, float width,
172 float height, float elevation) {
173 if (controlPoints.size() != 4 || subdivisions < 2 || subdivisions > 4096 ||
174 !std::isfinite(width) || !std::isfinite(height) || !std::isfinite(elevation) ||
175 width <= 0.f || height <= 0.f) {
178 "curve mesh requires four controls, 2..4096 subdivisions, and positive dimensions",
179 {}, {}, "buildingfx.edge-curve-mesh"));
180 }
181 for (const CurveControlPoint &point : controlPoints) {
182 if (!std::isfinite(point.x) || !std::isfinite(point.y)) {
184 eve::DiagnosticCode::InvalidArgument, "curve mesh controls must be finite", {},
185 {}, "buildingfx.edge-curve-mesh"));
186 }
187 }
188 const auto layout = world.getGrid().layout;
192 "continuous curve mesh requires an affine rectangle or isometric grid", {}, {},
193 "buildingfx.edge-curve-mesh"));
194 }
195
196 float originX = 0.f, originY = 0.f;
197 float basisXX = 0.f, basisXY = 0.f;
198 float basisYX = 0.f, basisYY = 0.f;
199 world.cellToWorldPlane(0, 0, originX, originY);
200 world.cellToWorldPlane(1, 0, basisXX, basisXY);
201 world.cellToWorldPlane(0, 1, basisYX, basisYY);
202 basisXX -= originX;
203 basisXY -= originY;
204 basisYX -= originX;
205 basisYY -= originY;
206
207 struct Point {
208 float x = 0.f;
209 float y = 0.f;
210 };
211 const auto evaluate = [&](int sample) {
212 const double t = static_cast<double>(sample) / static_cast<double>(subdivisions);
213 const double u = 1.0 - t;
214 const double gx = u * u * u * controlPoints[0].x +
215 3.0 * u * u * t * controlPoints[1].x +
216 3.0 * u * t * t * controlPoints[2].x +
217 t * t * t * controlPoints[3].x;
218 const double gy = u * u * u * controlPoints[0].y +
219 3.0 * u * u * t * controlPoints[1].y +
220 3.0 * u * t * t * controlPoints[2].y +
221 t * t * t * controlPoints[3].y;
222 return Point{originX + static_cast<float>(gx) * basisXX +
223 static_cast<float>(gy) * basisYX,
224 originY + static_cast<float>(gx) * basisXY +
225 static_cast<float>(gy) * basisYY};
226 };
227 std::vector<Point> samples;
228 std::vector<float> distances(static_cast<size_t>(subdivisions + 1), 0.f);
229 samples.reserve(static_cast<size_t>(subdivisions + 1));
231 mesh.sampleCount = subdivisions + 1;
232 for (int sample = 0; sample <= subdivisions; ++sample) {
233 samples.push_back(evaluate(sample));
234 if (sample == 0) continue;
235 distances[sample] = distances[sample - 1] +
236 std::hypot(samples[sample].x - samples[sample - 1].x,
237 samples[sample].y - samples[sample - 1].y);
238 }
239 mesh.length = distances.back();
240 if (!(mesh.length > 1e-5f)) {
242 eve::DiagnosticCode::InvalidArgument, "curve mesh has zero projected length", {},
243 {}, "buildingfx.edge-curve-mesh"));
244 }
245
246 const bool xz = world.getGrid().plane == grid::GridPlane::XZ;
247 const auto appendVertex = [&](Point point, float vertical, float nx, float ny,
248 float normalVertical, float u, float v) {
249 if (xz) {
250 mesh.positions.insert(mesh.positions.end(), {point.x, elevation + vertical, point.y});
251 mesh.normals.insert(mesh.normals.end(), {nx, normalVertical, ny});
252 } else {
253 mesh.positions.insert(mesh.positions.end(), {point.x, point.y, elevation + vertical});
254 mesh.normals.insert(mesh.normals.end(), {nx, ny, normalVertical});
255 }
256 mesh.uvs.insert(mesh.uvs.end(), {u, v});
257 };
258 mesh.positions.reserve(static_cast<size_t>((subdivisions + 1) * 8 + 8) * 3);
259 mesh.normals.reserve(mesh.positions.capacity());
260 mesh.uvs.reserve(static_cast<size_t>((subdivisions + 1) * 8 + 8) * 2);
261 mesh.indices.reserve(static_cast<size_t>(subdivisions) * 24 + 12);
262 const float halfWidth = width * 0.5f;
263 std::vector<Point> left(samples.size()), right(samples.size()), tangents(samples.size());
264 for (int sample = 0; sample <= subdivisions; ++sample) {
265 const Point before = samples[static_cast<size_t>(sample == 0 ? 0 : sample - 1)];
266 const Point after = samples[static_cast<size_t>(
267 sample == subdivisions ? subdivisions : sample + 1)];
268 float tx = after.x - before.x;
269 float ty = after.y - before.y;
270 const float tangentLength = std::hypot(tx, ty);
271 if (!(tangentLength > 1e-5f)) {
274 "curve mesh contains an unresolved zero-length tangent", std::to_string(sample),
275 {}, "buildingfx.edge-curve-mesh"));
276 }
277 tx /= tangentLength;
278 ty /= tangentLength;
279 tangents[sample] = {tx, ty};
280 const float px = -ty;
281 const float py = tx;
282 left[sample] = {samples[sample].x - px * halfWidth,
283 samples[sample].y - py * halfWidth};
284 right[sample] = {samples[sample].x + px * halfWidth,
285 samples[sample].y + py * halfWidth};
286 const float u = distances[sample] / mesh.length;
287 appendVertex(left[sample], 0.f, -px, -py, 0.f, u, 0.f);
288 appendVertex(left[sample], height, -px, -py, 0.f, u, 1.f);
289 appendVertex(right[sample], 0.f, px, py, 0.f, u, 0.f);
290 appendVertex(right[sample], height, px, py, 0.f, u, 1.f);
291 appendVertex(left[sample], height, 0.f, 0.f, 1.f, u, 0.f);
292 appendVertex(right[sample], height, 0.f, 0.f, 1.f, u, 1.f);
293 appendVertex(left[sample], 0.f, 0.f, 0.f, -1.f, u, 0.f);
294 appendVertex(right[sample], 0.f, 0.f, 0.f, -1.f, u, 1.f);
295 }
296 for (int segment = 0; segment < subdivisions; ++segment) {
297 const uint32_t a = static_cast<uint32_t>(segment * 8);
298 const uint32_t b = a + 8;
299 const uint32_t faces[] = {a, b, b + 1, a, b + 1, a + 1,
300 a + 2, a + 3, b + 3, a + 2, b + 3, b + 2,
301 a + 4, b + 4, b + 5, a + 4, b + 5, a + 5,
302 a + 6, a + 7, b + 7, a + 6, b + 7, b + 6};
303 mesh.indices.insert(mesh.indices.end(), std::begin(faces), std::end(faces));
304 }
305 const auto appendCap = [&](int sample, bool start) {
306 const Point tangent = tangents[static_cast<size_t>(sample)];
307 const float sign = start ? -1.f : 1.f;
308 const uint32_t base = static_cast<uint32_t>(mesh.positions.size() / 3);
309 appendVertex(left[sample], 0.f, tangent.x * sign, tangent.y * sign, 0.f, 0.f, 0.f);
310 appendVertex(right[sample], 0.f, tangent.x * sign, tangent.y * sign, 0.f, 1.f, 0.f);
311 appendVertex(left[sample], height, tangent.x * sign, tangent.y * sign, 0.f, 0.f, 1.f);
312 appendVertex(right[sample], height, tangent.x * sign, tangent.y * sign, 0.f, 1.f, 1.f);
313 const uint32_t capStart[] = {base, base + 2, base + 3, base, base + 3, base + 1};
314 const uint32_t capEnd[] = {base, base + 3, base + 2, base, base + 1, base + 3};
315 const uint32_t *indices = start ? capStart : capEnd;
316 mesh.indices.insert(mesh.indices.end(), indices, indices + 6);
317 };
318 appendCap(0, true);
319 appendCap(subdivisions, false);
320 return eve::Result<CurveMeshData>::success(std::move(mesh));
321}
322
324 const std::vector<CurveSurfaceSample> &samples, float width, float height) {
325 if (samples.size() < 2 || !std::isfinite(width) || !std::isfinite(height) ||
326 width <= 0.f || height <= 0.f)
329 "surface curve mesh requires at least two frames and positive dimensions", {}, {},
330 "buildingfx.surface-curve-mesh"));
331 std::vector<glm::vec3> centers, normals, tangents, laterals;
332 std::vector<float> distances(samples.size(), 0.f);
333 centers.reserve(samples.size());
334 normals.reserve(samples.size());
335 for (const CurveSurfaceSample &sample : samples) {
336 const glm::vec3 center(sample.x, sample.y, sample.z);
337 const glm::vec3 normal(sample.normalX, sample.normalY, sample.normalZ);
338 if (!std::isfinite(center.x) || !std::isfinite(center.y) || !std::isfinite(center.z) ||
339 !std::isfinite(normal.x) || !std::isfinite(normal.y) ||
340 !std::isfinite(normal.z) || glm::length(normal) <= 1e-5f)
343 "surface curve frames require finite positions and non-zero normals", {}, {},
344 "buildingfx.surface-curve-mesh"));
345 centers.push_back(center);
346 normals.push_back(glm::normalize(normal));
347 }
348 for (size_t index = 1; index < centers.size(); ++index)
349 distances[index] = distances[index - 1] + glm::length(centers[index] - centers[index - 1]);
350 if (distances.back() <= 1e-5f)
352 eve::DiagnosticCode::InvalidArgument, "surface curve has zero length", {}, {},
353 "buildingfx.surface-curve-mesh"));
354 for (size_t index = 0; index < centers.size(); ++index) {
355 const glm::vec3 before = centers[index == 0 ? 0 : index - 1];
356 const glm::vec3 after = centers[index + 1 == centers.size() ? index : index + 1];
357 glm::vec3 tangent = after - before;
358 tangent -= normals[index] * glm::dot(tangent, normals[index]);
359 if (glm::length(tangent) <= 1e-5f)
362 "surface curve tangent is parallel to its normal", std::to_string(index), {},
363 "buildingfx.surface-curve-mesh"));
364 tangent = glm::normalize(tangent);
365 tangents.push_back(tangent);
366 laterals.push_back(glm::normalize(glm::cross(normals[index], tangent)));
367 }
369 mesh.sampleCount = static_cast<int>(samples.size());
370 mesh.length = distances.back();
371 const auto append = [&](const glm::vec3 &position, const glm::vec3 &normal, float u,
372 float v) {
373 mesh.positions.insert(mesh.positions.end(), {position.x, position.y, position.z});
374 mesh.normals.insert(mesh.normals.end(), {normal.x, normal.y, normal.z});
375 mesh.uvs.insert(mesh.uvs.end(), {u, v});
376 };
377 const float halfWidth = width * 0.5f;
378 for (size_t index = 0; index < centers.size(); ++index) {
379 const glm::vec3 left = centers[index] - laterals[index] * halfWidth;
380 const glm::vec3 right = centers[index] + laterals[index] * halfWidth;
381 const glm::vec3 leftTop = left + normals[index] * height;
382 const glm::vec3 rightTop = right + normals[index] * height;
383 const float u = distances[index] / mesh.length;
384 append(left, -laterals[index], u, 0.f);
385 append(leftTop, -laterals[index], u, 1.f);
386 append(right, laterals[index], u, 0.f);
387 append(rightTop, laterals[index], u, 1.f);
388 append(leftTop, normals[index], u, 0.f);
389 append(rightTop, normals[index], u, 1.f);
390 append(left, -normals[index], u, 0.f);
391 append(right, -normals[index], u, 1.f);
392 }
393 for (size_t segment = 0; segment + 1 < centers.size(); ++segment) {
394 const uint32_t a = static_cast<uint32_t>(segment * 8);
395 const uint32_t b = a + 8;
396 const uint32_t faces[] = {a, b, b + 1, a, b + 1, a + 1,
397 a + 2, a + 3, b + 3, a + 2, b + 3, b + 2,
398 a + 4, b + 4, b + 5, a + 4, b + 5, a + 5,
399 a + 6, a + 7, b + 7, a + 6, b + 7, b + 6};
400 mesh.indices.insert(mesh.indices.end(), std::begin(faces), std::end(faces));
401 }
402 const auto cap = [&](size_t index, bool start) {
403 const glm::vec3 left = centers[index] - laterals[index] * halfWidth;
404 const glm::vec3 right = centers[index] + laterals[index] * halfWidth;
405 const glm::vec3 capNormal = tangents[index] * (start ? -1.f : 1.f);
406 const uint32_t base = static_cast<uint32_t>(mesh.positions.size() / 3);
407 append(left, capNormal, 0.f, 0.f);
408 append(right, capNormal, 1.f, 0.f);
409 append(left + normals[index] * height, capNormal, 0.f, 1.f);
410 append(right + normals[index] * height, capNormal, 1.f, 1.f);
411 const uint32_t startIndices[] = {base, base + 2, base + 3, base, base + 3, base + 1};
412 const uint32_t endIndices[] = {base, base + 3, base + 2, base, base + 1, base + 3};
413 mesh.indices.insert(mesh.indices.end(), start ? std::begin(startIndices)
414 : std::begin(endIndices),
415 start ? std::end(startIndices) : std::end(endIndices));
416 };
417 cap(0, true);
418 cap(centers.size() - 1, false);
419 return eve::Result<CurveMeshData>::success(std::move(mesh));
420}
421
423 const building::PlacementWorld &world, int instanceId, float width, float height,
424 float elevation) {
425 if (instanceId <= 0) {
427 eve::DiagnosticCode::InvalidArgument, "curve mesh requires a positive instance id",
428 std::to_string(instanceId), {}, "buildingfx.edge-curve-group-mesh"));
429 }
430 auto groupResult = world.edgeCurveGroupForInstance(instanceId);
431 if (!groupResult) return eve::Result<CurveMeshData>::failure(*groupResult.error());
432
433 const building::EdgeCurveGroup &group = groupResult.value();
434 if (!group.surfaceSamples.empty()) {
435 std::vector<CurveSurfaceSample> samples;
436 samples.reserve(group.surfaceSamples.size());
437 for (const building::EdgeCurveSurfaceSample &sample : group.surfaceSamples)
438 samples.push_back({sample.worldX, sample.worldY, sample.worldZ, sample.normalX,
439 sample.normalY, sample.normalZ});
440 return buildSurfaceCurveMesh(samples, width, height);
441 }
442 std::vector<CurveControlPoint> controls;
443 controls.reserve(group.controlPoints.size());
444 for (const building::EdgeCurveControlPoint &point : group.controlPoints)
445 controls.push_back({point.x, point.y});
446 return buildEdgeCurveMesh(world, controls, group.subdivisions, width, height, elevation);
447}
448
449eve::Result<void> BuildingFx::updateEdgeCurvePreview(
450 building::PlacementWorld *world, const std::string &buildingId,
451 const std::vector<CurveControlPoint> &controlPoints, int subdivisions, int level) {
452 const auto state = states_.find(world);
453 const building::BuildingDefinition *definition =
455 if (!world || state == states_.end() || !definition || !is3d(*definition)) {
458 "curve preview requires an attached world and a 3D building definition",
459 buildingId, {}, "buildingfx.edge-curve-preview"));
460 }
461 const float width = toFloat(definition->getVisual3d("thickness"),
462 world->getCellSize() * 0.1f);
463 const float height = toFloat(definition->getVisual3d("height"), 1.f);
464 const float elevation = static_cast<float>(level) * world->getFloorHeight();
465 auto generated = buildEdgeCurveMesh(*world, controlPoints, subdivisions, width, height,
466 elevation);
467 if (!generated.ok()) return eve::Result<void>::failure(*generated.error());
468
469 return presentCurvePreview(state->second, std::move(generated).takeValue(), controlPoints,
470 subdivisions, width, height, elevation);
471}
472
473eve::Result<void> BuildingFx::updateEdgeCurveSurfacePreview(
475 const std::string &surfaceName) {
476 const auto state = states_.find(world);
477 if (!world || state == states_.end() || !session || session->getWorld() != world ||
478 !session->isActive()) {
481 "surface curve preview requires an attached world and its active session", {}, {},
482 "buildingfx.edge-curve-surface-preview"));
483 }
484 const building::BuildingDefinition *definition =
486 if (!definition || !is3d(*definition) ||
487 session->edgeCurveControlPoints().size() != 4) {
490 "surface curve preview requires a 3D edge definition and four controls",
491 session->getBuildingId(), {}, "buildingfx.edge-curve-surface-preview"));
492 }
494 *world, surfaceName, session->edgeCurveControlPoints(), subdivisions);
495 if (!sampled.ok()) return eve::Result<void>::failure(sampled.status());
496 std::vector<CurveSurfaceSample> samples;
497 samples.reserve(sampled.value().samples.size());
498 for (const building::EdgeCurveSurfaceSample &sample : sampled.value().samples)
499 samples.push_back({sample.worldX, sample.worldY, sample.worldZ, sample.normalX,
500 sample.normalY, sample.normalZ});
501 const float width =
502 toFloat(definition->getVisual3d("thickness"), world->getCellSize() * 0.1f);
503 const float height = toFloat(definition->getVisual3d("height"), 1.f);
504 auto generated = buildSurfaceCurveMesh(samples, width, height);
505 if (!generated.ok()) return eve::Result<void>::failure(generated.status());
506 std::vector<CurveControlPoint> controls;
507 controls.reserve(session->edgeCurveControlPoints().size());
509 session->edgeCurveControlPoints())
510 controls.push_back({point.x, point.y});
511 return presentCurvePreview(state->second, std::move(generated).takeValue(),
512 std::move(controls), subdivisions, width, height, 0.f,
513 sampled.value().surfaceId, sampled.value().surfaceRevision);
514}
515
516CurvePreviewUpdateStatus BuildingFx::updateEdgeCurveSurfacePreviewStatus(
518 const std::string &surfaceName) {
519 return updateEdgeCurveSurfacePreview(world, session, subdivisions, surfaceName).ok()
520 ? CurvePreviewUpdateStatus::Updated
521 : CurvePreviewUpdateStatus::Rejected;
522}
523
524eve::Result<void> BuildingFx::presentCurvePreview(
525 WorldState &state, CurveMeshData mesh, std::vector<CurveControlPoint> controls,
526 int subdivisions, float width, float height, float elevation, std::string surfaceId,
527 std::uint64_t surfaceRevision) {
528 WorldState::CurveVisual &preview = state.curvePreview;
529 preview.cpuMesh = std::move(mesh);
530 preview.controlPoints = std::move(controls);
531 preview.subdivisions = subdivisions;
532 preview.width = width;
533 preview.height = height;
534 preview.elevation = elevation;
535 preview.surfaceId = std::move(surfaceId);
536 preview.surfaceRevision = surfaceRevision;
537 preview.active = true;
538 preview.fallbackReason.clear();
539
540 graphics::Graphics *gfx = gfxOrNull();
541 if (!gfx) {
542 preview.fallbackReason = "graphics_unavailable";
543 if (preview.renderable) preview.renderable->meshRenderer()->visible = false;
545 }
546 if (preview.mesh &&
547 !gfx->updateMeshVertices(
548 preview.mesh, preview.cpuMesh.positions.data(), preview.cpuMesh.normals.data(),
549 preview.cpuMesh.uvs.data(), static_cast<int>(preview.cpuMesh.positions.size() / 3),
550 preview.cpuMesh.indices.data(), static_cast<int>(preview.cpuMesh.indices.size())))
551 preview.mesh = nullptr;
552 if (!preview.mesh) {
553 try {
554 preview.mesh = gfx->newMeshFromArrays(
555 preview.cpuMesh.positions.data(), preview.cpuMesh.normals.data(),
556 preview.cpuMesh.uvs.data(),
557 static_cast<int>(preview.cpuMesh.positions.size() / 3),
558 preview.cpuMesh.indices.data(),
559 static_cast<int>(preview.cpuMesh.indices.size()));
560 } catch (...) {
561 preview.mesh = nullptr;
562 }
563 }
564 if (!preview.mesh) {
565 preview.fallbackReason = "mesh_upload_failed";
567 }
568 if (!preview.renderable) preview.renderable = graphics::Renderable3D::create();
569 auto renderer = preview.renderable->meshRenderer();
570 renderer->mesh = preview.mesh;
571 renderer->r = 0.2f;
572 renderer->g = 0.9f;
573 renderer->b = 0.35f;
574 renderer->a = 0.55f;
575 renderer->visible = true;
576 renderer->castShadow = false;
577 renderer->receiveShadow = false;
579}
580
581void BuildingFx::clearEdgeCurvePreview(building::PlacementWorld *world) {
582 const auto state = states_.find(world);
583 if (state == states_.end()) return;
584 WorldState::CurveVisual &preview = state->second.curvePreview;
585 if (preview.renderable) {
586 ecs::DestroyEntity(preview.renderable);
587 preview.renderable = nullptr;
588 }
589 preview.active = false;
590 preview.cpuMesh = {};
591 preview.controlPoints.clear();
592 preview.fallbackReason.clear();
593}
594
595bool BuildingFx::hasEdgeCurvePreview(building::PlacementWorld *world) const {
596 const auto state = states_.find(world);
597 return state != states_.end() && state->second.curvePreview.active &&
598 !state->second.curvePreview.cpuMesh.positions.empty();
599}
600
601std::string BuildingFx::getEdgeCurvePreviewFallbackReason(
603 const auto state = states_.find(world);
604 if (state == states_.end()) return "world_not_attached";
605 return state->second.curvePreview.active
606 ? state->second.curvePreview.fallbackReason
607 : "curve_preview_not_active";
608}
609
610std::string BuildingFx::getEdgeCurvePreviewSurfaceId(
612 const auto state = states_.find(world);
613 if (state == states_.end() || !state->second.curvePreview.active) return {};
614 return state->second.curvePreview.surfaceId;
615}
616
617std::uint64_t BuildingFx::getEdgeCurvePreviewSurfaceRevision(
619 const auto state = states_.find(world);
620 if (state == states_.end() || !state->second.curvePreview.active) return 0;
621 return state->second.curvePreview.surfaceRevision;
622}
623
624bool BuildingFx::attach(building::PlacementWorld *world) {
625 if (!world) return false;
626 states_[world]; // ensure entry
627 return true;
628}
629
630bool BuildingFx::detach(building::PlacementWorld *world) {
631 if (!world) return false;
632 auto it = states_.find(world);
633 if (it == states_.end()) return false;
634 destroyAll(it->second);
635 states_.erase(it);
636 return true;
637}
638
639bool BuildingFx::isAttached(building::PlacementWorld *world) const {
640 return world != nullptr && states_.count(world) > 0;
641}
642
643int BuildingFx::getAttachedCount() const { return int(states_.size()); }
644
645bool BuildingFx::is3d(const building::BuildingDefinition &def) const {
646 return def.renderMode == "3d" || def.renderMode == "3D";
647}
648
649graphics::Mesh *BuildingFx::cubeMesh(graphics::Graphics *gfx) {
650 if (!gfx) return nullptr;
651 if (!cubeMesh_) cubeMesh_ = gfx->newMeshCube(1.f);
652 return cubeMesh_;
653}
654
655void BuildingFx::createVisual(WorldState &st, const building::BuildingDefinition &def,
656 const building::PlacedBuilding &pb,
657 building::PlacementWorld *world, Visual &v, float alpha) {
658 (void)st;
659 const float cellW = world->getGrid().cellW;
660 const float cellH = world->getGrid().cellH;
661 int effW = def.footprintW;
662 int effH = def.footprintH;
663 building::PlacementSystem::effectiveFootprint(def, pb.rotationDeg, &effW, &effH);
664 const TopologyTransform topology = topologyTransform(def, pb, *world, is3d(def));
665 v.topologyVariant = topology.variant;
666 v.topologyMask = topology.mask;
667
668 if (is3d(def)) {
669 auto *r = graphics::Renderable3D::create();
670 float wx = 0.f, wy = 0.f, wz = 0.f;
671 placedWorldPosition(pb, *world, wx, wy, wz);
672 auto tr = r->transform();
673 tr->x = wx + toFloat(def.getVisual3d("offsetX"), 0.f);
674 tr->y = wy + toFloat(def.getVisual3d("offsetY"), 0.f);
675 tr->z = wz + toFloat(def.getVisual3d("offsetZ"), 0.f);
676 const float height = toFloat(visualValue(def, pb, *world, true, "height"), 1.f);
677 const bool edge = pb.placementKind == "edge";
678 const bool corner = pb.placementKind == "corner";
679 const bool free = pb.placementKind == "free";
680 tr->sx = (free ? freeVisualSize(def, pb, *world, true, "width")
681 : corner ? cornerVisualSize(def, pb, *world, true, "width")
682 : edge ? (pb.edge.axis == building::EdgeAxis::Horizontal ? cellW : cellH)
683 : float(def.footprintW) * cellW) *
684 (topology.mirrorX ? -1.f : 1.f);
685 tr->sy = height;
686 tr->sz = (free ? freeVisualSize(def, pb, *world, true, "depth")
687 : corner ? cornerVisualSize(def, pb, *world, true, "depth")
688 : edge ? toFloat(visualValue(def, pb, *world, true, "thickness"), std::min(cellW, cellH) * 0.1f)
689 : float(def.footprintH) * cellH) *
690 (topology.mirrorZ ? -1.f : 1.f);
691 const float visualRotation = pb.rotationDeg + topology.rotationDegrees;
692 const float rad = visualRotation * 3.14159265f / 180.f;
693 if (world->getGrid().plane == grid::GridPlane::XZ) {
694 tr->yaw = rad;
695 } else {
696 tr->roll = rad;
697 }
698 applySurfaceRotation(pb, visualRotation, *tr);
699 auto mr = r->meshRenderer();
700 v.resourceId = visualValue(def, pb, *world, true, "mesh");
701 graphics::Mesh *resolved = nullptr;
702 if (v.resourceId.empty()) {
703 v.fallbackReason = "primitive_default";
704 } else if (!meshResolver_) {
705 v.fallbackReason = "resolver_unavailable";
706 } else {
707 resolved = meshResolver_(v.resourceId);
708 v.fallbackReason = resolved ? std::string{} : "resource_unresolved";
709 }
710 mr->mesh = resolved ? resolved : cubeMesh(gfxOrNull());
711 mr->r = toFloat(visualValue(def, pb, *world, true, "colorR"), 0.62f);
712 mr->g = toFloat(visualValue(def, pb, *world, true, "colorG"), 0.62f);
713 mr->b = toFloat(visualValue(def, pb, *world, true, "colorB"), 0.62f);
714 mr->a = alpha;
715 mr->visible = true;
716 mr->castShadow = true;
717 mr->receiveShadow = true;
718 v.r3d = r;
719 } else {
720 auto *r = graphics::Renderable2D::create();
721 auto tr = r->transform();
722 tr->x = pb.worldX + toFloat(def.getVisual2d("offsetX"), 0.f);
723 tr->y = pb.worldY + toFloat(def.getVisual2d("offsetY"), 0.f);
724 tr->rot = pb.rotationDeg + topology.rotationDegrees;
725 tr->sx = topology.mirrorX ? -1.f : 1.f;
726 tr->sy = topology.mirrorZ ? -1.f : 1.f;
727 auto sp = r->sprite();
728 const bool edge = pb.placementKind == "edge";
729 const bool corner = pb.placementKind == "corner";
730 const bool free = pb.placementKind == "free";
731 sp->width = free ? freeVisualSize(def, pb, *world, false, "width")
732 : corner ? cornerVisualSize(def, pb, *world, false, "width")
733 : edge ? (pb.edge.axis == building::EdgeAxis::Horizontal ? cellW : cellH)
734 : float(effW) * cellW;
735 sp->height = free ? freeVisualSize(def, pb, *world, false, "height")
736 : corner ? cornerVisualSize(def, pb, *world, false, "height")
737 : edge ? toFloat(visualValue(def, pb, *world, false, "thickness"),
738 std::min(cellW, cellH) * 0.1f)
739 : float(effH) * cellH;
740 sp->r = toFloat(visualValue(def, pb, *world, false, "colorR"), 0.72f);
741 sp->g = toFloat(visualValue(def, pb, *world, false, "colorG"), 0.72f);
742 sp->b = toFloat(visualValue(def, pb, *world, false, "colorB"), 0.72f);
743 sp->a = alpha;
744 sp->layer = int(toFloat(def.getVisual2d("layer"), 0.f));
745 sp->visible = true;
746 const std::string texPath = visualValue(def, pb, *world, false, "texture");
747 v.resourceId = texPath;
748 v.fallbackReason = texPath.empty() ? "primitive_default" : std::string{};
749 if (!texPath.empty()) {
750 if (auto *gfx = gfxOrNull()) {
751 if (graphics::Texture *tex = gfx->newTextureFromFile(texPath)) {
752 sp->texture = tex;
753 } else {
754 v.fallbackReason = "resource_unresolved";
755 }
756 } else {
757 v.fallbackReason = "resolver_unavailable";
758 }
759 }
760 v.r2d = r;
761 }
762}
763
764void BuildingFx::updateVisual(const building::BuildingDefinition &def,
765 const building::PlacedBuilding &pb,
766 building::PlacementWorld *world, Visual &v) {
767 const float cellW = world->getGrid().cellW;
768 const float cellH = world->getGrid().cellH;
769 int effW = def.footprintW;
770 int effH = def.footprintH;
771 building::PlacementSystem::effectiveFootprint(def, pb.rotationDeg, &effW, &effH);
772 const TopologyTransform topology = topologyTransform(def, pb, *world, is3d(def));
773 v.topologyVariant = topology.variant;
774 v.topologyMask = topology.mask;
775
776 if (v.r3d) {
777 float wx = 0.f, wy = 0.f, wz = 0.f;
778 placedWorldPosition(pb, *world, wx, wy, wz);
779 auto tr = v.r3d->transform();
780 tr->x = wx + toFloat(def.getVisual3d("offsetX"), 0.f);
781 tr->y = wy + toFloat(def.getVisual3d("offsetY"), 0.f);
782 tr->z = wz + toFloat(def.getVisual3d("offsetZ"), 0.f);
783 const float height = toFloat(visualValue(def, pb, *world, true, "height"), 1.f);
784 const bool edge = pb.placementKind == "edge";
785 const bool corner = pb.placementKind == "corner";
786 const bool free = pb.placementKind == "free";
787 tr->sx = (free ? freeVisualSize(def, pb, *world, true, "width")
788 : corner ? cornerVisualSize(def, pb, *world, true, "width")
789 : edge ? (pb.edge.axis == building::EdgeAxis::Horizontal ? cellW : cellH)
790 : float(def.footprintW) * cellW) *
791 (topology.mirrorX ? -1.f : 1.f);
792 tr->sy = height;
793 tr->sz = (free ? freeVisualSize(def, pb, *world, true, "depth")
794 : corner ? cornerVisualSize(def, pb, *world, true, "depth")
795 : edge ? toFloat(visualValue(def, pb, *world, true, "thickness"), std::min(cellW, cellH) * 0.1f)
796 : float(def.footprintH) * cellH) *
797 (topology.mirrorZ ? -1.f : 1.f);
798 const float visualRotation = pb.rotationDeg + topology.rotationDegrees;
799 const float rad = visualRotation * 3.14159265f / 180.f;
800 if (world->getGrid().plane == grid::GridPlane::XZ) {
801 tr->yaw = rad;
802 } else {
803 tr->roll = rad;
804 }
805 applySurfaceRotation(pb, visualRotation, *tr);
806 auto renderer = v.r3d->meshRenderer();
807 v.resourceId = visualValue(def, pb, *world, true, "mesh");
808 graphics::Mesh *resolved = nullptr;
809 if (v.resourceId.empty()) {
810 v.fallbackReason = "primitive_default";
811 } else if (!meshResolver_) {
812 v.fallbackReason = "resolver_unavailable";
813 } else {
814 resolved = meshResolver_(v.resourceId);
815 v.fallbackReason = resolved ? std::string{} : "resource_unresolved";
816 }
817 renderer->mesh = resolved ? resolved : cubeMesh(gfxOrNull());
818 renderer->r = toFloat(visualValue(def, pb, *world, true, "colorR"), 0.62f);
819 renderer->g = toFloat(visualValue(def, pb, *world, true, "colorG"), 0.62f);
820 renderer->b = toFloat(visualValue(def, pb, *world, true, "colorB"), 0.62f);
821 } else if (v.r2d) {
822 auto tr = v.r2d->transform();
823 tr->x = pb.worldX + toFloat(def.getVisual2d("offsetX"), 0.f);
824 tr->y = pb.worldY + toFloat(def.getVisual2d("offsetY"), 0.f);
825 tr->rot = pb.rotationDeg + topology.rotationDegrees;
826 tr->sx = topology.mirrorX ? -1.f : 1.f;
827 tr->sy = topology.mirrorZ ? -1.f : 1.f;
828 auto sp = v.r2d->sprite();
829 const bool edge = pb.placementKind == "edge";
830 const bool corner = pb.placementKind == "corner";
831 const bool free = pb.placementKind == "free";
832 sp->width = free ? freeVisualSize(def, pb, *world, false, "width")
833 : corner ? cornerVisualSize(def, pb, *world, false, "width")
834 : edge ? (pb.edge.axis == building::EdgeAxis::Horizontal ? cellW : cellH)
835 : float(effW) * cellW;
836 sp->height = free ? freeVisualSize(def, pb, *world, false, "height")
837 : corner ? cornerVisualSize(def, pb, *world, false, "height")
838 : edge ? toFloat(visualValue(def, pb, *world, false, "thickness"),
839 std::min(cellW, cellH) * 0.1f)
840 : float(effH) * cellH;
841 sp->r = toFloat(visualValue(def, pb, *world, false, "colorR"), 0.72f);
842 sp->g = toFloat(visualValue(def, pb, *world, false, "colorG"), 0.72f);
843 sp->b = toFloat(visualValue(def, pb, *world, false, "colorB"), 0.72f);
844 const std::string resourceId = visualValue(def, pb, *world, false, "texture");
845 if (resourceId != v.resourceId) {
846 sp->texture = nullptr;
847 v.resourceId = resourceId;
848 if (resourceId.empty()) {
849 v.fallbackReason = "primitive_default";
850 } else if (auto *gfx = gfxOrNull()) {
851 sp->texture = gfx->newTextureFromFile(resourceId);
852 v.fallbackReason = sp->texture ? std::string{} : "resource_unresolved";
853 } else {
854 v.fallbackReason = "resolver_unavailable";
855 }
856 }
857 }
858}
859
860void BuildingFx::destroyVisual(Visual &v) {
861 if (v.r2d) {
862 ecs::DestroyEntity(v.r2d);
863 v.r2d = nullptr;
864 }
865 if (v.r3d) {
866 ecs::DestroyEntity(v.r3d);
867 v.r3d = nullptr;
868 }
869}
870
871void BuildingFx::setVisible(Visual &v, bool visible) {
872 if (v.r2d) v.r2d->sprite()->visible = visible;
873 if (v.r3d) v.r3d->meshRenderer()->visible = visible;
874}
875
876void BuildingFx::destroyAll(WorldState &st) {
877 for (auto &kv : st.visuals) destroyVisual(kv.second);
878 st.visuals.clear();
879 for (auto &[groupId, curve] : st.curveVisuals) {
880 (void)groupId;
881 if (curve.renderable) ecs::DestroyEntity(curve.renderable);
882 }
883 st.curveVisuals.clear();
884 if (st.curvePreview.renderable) ecs::DestroyEntity(st.curvePreview.renderable);
885 st.curvePreview = {};
886 destroyVisual(st.ghost);
887 destroyVisual(st.cursor);
888 st.ghostBuildingId.clear();
889 for (graphics::Renderable3D *line : st.gridLines) {
890 if (line) ecs::DestroyEntity(line);
891 }
892 st.gridLines.clear();
893 st.gridLineCount = -1;
894 destroyHeatmap(st);
895}
896
897void BuildingFx::destroyHeatmap(WorldState &st) {
898 for (graphics::Renderable3D *cell : st.heatCells3d)
899 if (cell) ecs::DestroyEntity(cell);
900 st.heatCells3d.clear();
901}
902
903void BuildingFx::sync(building::PlacementWorld *world) {
904 if (!world) return;
905 auto it = states_.find(world);
906 if (it == states_.end()) return;
907 WorldState &st = it->second;
908
909 std::unordered_map<int, Visual> next;
910 const int n = world->getBuildingCount();
911 for (int i = 0; i < n; ++i) {
912 const int inst = world->getBuildingInstanceAt(i);
913 auto bit = world->buildings().find(inst);
914 if (bit == world->buildings().end()) continue;
915 const auto &pb = bit->second;
918 if (!def) continue;
919 auto existing = st.visuals.find(inst);
920 if (existing != st.visuals.end()) {
921 next[inst] = existing->second;
922 updateVisual(*def, pb, world, next[inst]);
923 } else {
924 Visual v;
925 createVisual(st, *def, pb, world, v, 1.f);
926 next[inst] = v;
927 }
928 const bool visible =
929 st.levelVisibility == WorldState::LevelVisibility::All ||
930 (st.levelVisibility == WorldState::LevelVisibility::Active &&
931 pb.level == world->getActiveLevel()) ||
932 (st.levelVisibility == WorldState::LevelVisibility::ActiveAndBelow &&
933 pb.level <= world->getActiveLevel());
934 setVisible(next[inst], visible);
935 }
936 for (auto &kv : st.visuals) {
937 if (next.count(kv.first) == 0) destroyVisual(kv.second);
938 }
939 st.visuals = std::move(next);
940 syncCurveVisuals(st, world);
941}
942
943void BuildingFx::syncCurveVisuals(WorldState &st, building::PlacementWorld *world) {
944 for (auto &[groupId, curve] : st.curveVisuals) {
945 (void)groupId;
946 curve.active = false;
947 }
948 for (building::EdgeCurveGroupId groupId : world->edgeCurveGroupIds()) {
949 auto groupResult = world->edgeCurveGroup(groupId);
950 if (!groupResult.ok() || groupResult.value().instanceIds.empty()) continue;
951 const building::EdgeCurveGroup &group = groupResult.value();
952 const auto member = world->buildings().find(group.instanceIds.front());
953 if (member == world->buildings().end()) continue;
954 const building::PlacedBuilding &placed = member->second;
955 const building::BuildingDefinition *definition =
957 if (!definition) continue;
958
959 WorldState::CurveVisual &curve = st.curveVisuals[groupId.value];
960 curve.active = true;
961 curve.memberIds = group.instanceIds;
962 std::vector<CurveControlPoint> controls;
963 for (const building::EdgeCurveControlPoint &point : group.controlPoints)
964 controls.push_back({point.x, point.y});
965 const float width = toFloat(definition->getVisual3d("thickness"),
966 std::min(world->getGrid().cellW,
967 world->getGrid().cellH) *
968 0.1f);
969 const float height = toFloat(definition->getVisual3d("height"), 1.f);
970 const float elevation = placed.elevation +
971 static_cast<float>(placed.level) * world->getFloorHeight();
972 const bool changed = curve.controlPoints != controls ||
973 curve.subdivisions != group.subdivisions || curve.width != width ||
974 curve.height != height || curve.elevation != elevation ||
975 curve.surfaceId != group.surfaceId ||
976 curve.surfaceRevision != group.surfaceRevision;
977 curve.fallbackReason.clear();
978 if (!is3d(*definition)) {
979 curve.fallbackReason = "render_mode_2d";
980 } else if (changed || curve.cpuMesh.positions.empty()) {
981 eve::Result<CurveMeshData> generated = [&]() {
982 if (group.surfaceSamples.empty())
983 return buildEdgeCurveMesh(*world, controls, group.subdivisions, width,
985 std::vector<CurveSurfaceSample> samples;
986 for (const building::EdgeCurveSurfaceSample &sample : group.surfaceSamples)
987 samples.push_back({sample.worldX, sample.worldY, sample.worldZ,
988 sample.normalX, sample.normalY, sample.normalZ});
989 return buildSurfaceCurveMesh(samples, width, height);
990 }();
991 if (!generated.ok()) {
992 curve.fallbackReason = generated.error() ? generated.error()->message()
993 : "mesh_generation_failed";
994 } else {
995 curve.cpuMesh = std::move(generated).takeValue();
996 curve.controlPoints = std::move(controls);
997 curve.subdivisions = group.subdivisions;
998 curve.width = width;
999 curve.height = height;
1000 curve.elevation = elevation;
1001 curve.surfaceId = group.surfaceId;
1002 curve.surfaceRevision = group.surfaceRevision;
1003 }
1004 }
1005 graphics::Graphics *gfx = gfxOrNull();
1006 if (curve.fallbackReason.empty() && !gfx) curve.fallbackReason = "graphics_unavailable";
1007 if (curve.fallbackReason.empty() && changed && curve.mesh) {
1008 if (!gfx->updateMeshVertices(
1009 curve.mesh, curve.cpuMesh.positions.data(), curve.cpuMesh.normals.data(),
1010 curve.cpuMesh.uvs.data(), static_cast<int>(curve.cpuMesh.positions.size() / 3),
1011 curve.cpuMesh.indices.data(), static_cast<int>(curve.cpuMesh.indices.size())))
1012 curve.mesh = nullptr;
1013 }
1014 if (curve.fallbackReason.empty() && !curve.mesh) {
1015 try {
1016 curve.mesh = gfx->newMeshFromArrays(
1017 curve.cpuMesh.positions.data(), curve.cpuMesh.normals.data(),
1018 curve.cpuMesh.uvs.data(), static_cast<int>(curve.cpuMesh.positions.size() / 3),
1019 curve.cpuMesh.indices.data(), static_cast<int>(curve.cpuMesh.indices.size()));
1020 } catch (...) {
1021 curve.mesh = nullptr;
1022 }
1023 if (!curve.mesh) curve.fallbackReason = "mesh_upload_failed";
1024 }
1025 if (curve.fallbackReason.empty() && !curve.renderable) {
1026 curve.renderable = graphics::Renderable3D::create();
1027 curve.renderable->meshRenderer()->mesh = curve.mesh;
1028 }
1029 if (curve.renderable) {
1030 auto renderer = curve.renderable->meshRenderer();
1031 renderer->mesh = curve.mesh;
1032 renderer->r = toFloat(definition->getVisual3d("colorR"), 0.62f);
1033 renderer->g = toFloat(definition->getVisual3d("colorG"), 0.62f);
1034 renderer->b = toFloat(definition->getVisual3d("colorB"), 0.62f);
1035 renderer->a = 1.f;
1036 renderer->castShadow = true;
1037 renderer->receiveShadow = true;
1038 renderer->visible =
1039 curve.fallbackReason.empty() &&
1040 (st.levelVisibility == WorldState::LevelVisibility::All ||
1041 (st.levelVisibility == WorldState::LevelVisibility::Active &&
1042 placed.level == world->getActiveLevel()) ||
1043 (st.levelVisibility == WorldState::LevelVisibility::ActiveAndBelow &&
1044 placed.level <= world->getActiveLevel()));
1045 }
1046 if (curve.fallbackReason.empty()) {
1047 for (int instanceId : group.instanceIds) {
1048 const auto visual = st.visuals.find(instanceId);
1049 if (visual != st.visuals.end()) setVisible(visual->second, false);
1050 }
1051 }
1052 }
1053 for (auto &[groupId, curve] : st.curveVisuals) {
1054 (void)groupId;
1055 if (curve.active) continue;
1056 if (curve.renderable) {
1057 ecs::DestroyEntity(curve.renderable);
1058 curve.renderable = nullptr;
1059 }
1060 }
1061}
1062
1063int BuildingFx::getVisualCount(building::PlacementWorld *world) const {
1064 auto it = states_.find(world);
1065 return it == states_.end() ? 0 : int(it->second.visuals.size());
1066}
1067
1068int BuildingFx::getCurveGroupCount(building::PlacementWorld *world) const {
1069 const auto state = states_.find(world);
1070 if (state == states_.end()) return 0;
1071 return static_cast<int>(std::count_if(
1072 state->second.curveVisuals.begin(), state->second.curveVisuals.end(),
1073 [](const auto &entry) { return entry.second.active; }));
1074}
1075
1076int BuildingFx::getContinuousCurveVisualCount(building::PlacementWorld *world) const {
1077 const auto state = states_.find(world);
1078 if (state == states_.end()) return 0;
1079 return static_cast<int>(std::count_if(
1080 state->second.curveVisuals.begin(), state->second.curveVisuals.end(),
1081 [](const auto &entry) {
1082 return entry.second.active && entry.second.renderable &&
1083 entry.second.fallbackReason.empty();
1084 }));
1085}
1086
1087std::string BuildingFx::getCurveVisualFallbackReason(building::PlacementWorld *world,
1088 int instanceId) const {
1089 const auto state = states_.find(world);
1090 if (state == states_.end()) return "world_not_attached";
1091 for (const auto &[groupId, curve] : state->second.curveVisuals) {
1092 (void)groupId;
1093 if (curve.active &&
1094 std::find(curve.memberIds.begin(), curve.memberIds.end(), instanceId) !=
1095 curve.memberIds.end())
1096 return curve.fallbackReason;
1097 }
1098 return "curve_group_not_found";
1099}
1100
1101void BuildingFx::setLevelVisibilityMode(building::PlacementWorld *world,
1102 const std::string &mode) {
1103 if (!world) return;
1104 auto found = states_.find(world);
1105 if (found == states_.end()) return;
1106 if (mode == "all") {
1107 found->second.levelVisibility = WorldState::LevelVisibility::All;
1108 } else if (mode == "active") {
1109 found->second.levelVisibility = WorldState::LevelVisibility::Active;
1110 } else if (mode == "active_and_below") {
1111 found->second.levelVisibility = WorldState::LevelVisibility::ActiveAndBelow;
1112 } else {
1113 return;
1114 }
1115 sync(world);
1116}
1117
1118std::string BuildingFx::getLevelVisibilityMode(building::PlacementWorld *world) const {
1119 const auto found = states_.find(world);
1120 if (found == states_.end()) return {};
1121 switch (found->second.levelVisibility) {
1122 case WorldState::LevelVisibility::Active: return "active";
1123 case WorldState::LevelVisibility::ActiveAndBelow: return "active_and_below";
1124 default: return "all";
1125 }
1126}
1127
1128bool BuildingFx::isVisualVisible(building::PlacementWorld *world, int instanceId) const {
1129 const auto found = states_.find(world);
1130 if (found == states_.end()) return false;
1131 const auto visual = found->second.visuals.find(instanceId);
1132 if (visual == found->second.visuals.end()) return false;
1133 if (visual->second.r2d) return visual->second.r2d->sprite()->visible;
1134 return visual->second.r3d && visual->second.r3d->meshRenderer()->visible;
1135}
1136
1137std::string BuildingFx::getVisualVariant(building::PlacementWorld *world, int instanceId) const {
1138 const auto state = states_.find(world);
1139 if (state == states_.end()) return {};
1140 const auto visual = state->second.visuals.find(instanceId);
1141 return visual == state->second.visuals.end() ? std::string{}
1142 : visual->second.topologyVariant;
1143}
1144
1145std::string BuildingFx::getVisualResource(building::PlacementWorld *world, int instanceId) const {
1146 const auto state = states_.find(world);
1147 if (state == states_.end()) return {};
1148 const auto visual = state->second.visuals.find(instanceId);
1149 return visual == state->second.visuals.end() ? std::string{} : visual->second.resourceId;
1150}
1151
1152std::string BuildingFx::getVisualFallbackReason(building::PlacementWorld *world,
1153 int instanceId) const {
1154 const auto state = states_.find(world);
1155 if (state == states_.end()) return {};
1156 const auto visual = state->second.visuals.find(instanceId);
1157 return visual == state->second.visuals.end() ? std::string{}
1158 : visual->second.fallbackReason;
1159}
1160
1161void BuildingFx::updateGhost(building::PlacementWorld *world, building::Ghost *ghost) {
1162 if (!world || !ghost) {
1163 hideGhost(world);
1164 return;
1165 }
1166 auto it = states_.find(world);
1167 if (it == states_.end()) return;
1168 WorldState &st = it->second;
1169 const building::BuildingDefinition *def =
1171 if (!def) return;
1172
1173 if (st.ghostBuildingId != ghost->getBuildingId()) {
1174 destroyVisual(st.ghost);
1175 destroyVisual(st.cursor);
1176 st.ghostBuildingId = ghost->getBuildingId();
1177 }
1178 if (!st.ghost.r2d && !st.ghost.r3d) {
1179 // 用 ghost 当前位姿构造临时 PlacedBuilding 以复用 createVisual。
1181 pb.buildingId = ghost->getBuildingId();
1182 pb.placementKind = ghost->getPlacementKind();
1183 pb.originCellX = ghost->getCellX();
1184 pb.originCellY = ghost->getCellY();
1185 pb.corner = {ghost->getCellX(), ghost->getCellY()};
1186 pb.worldX = ghost->getWorldX();
1187 pb.worldY = ghost->getWorldY();
1188 pb.elevation = ghost->getElevation();
1189 pb.rotationDeg = ghost->getRotationDeg();
1190 pb.surfaceId = ghost->getSurfaceId();
1191 pb.surfaceNormalX = ghost->getSurfaceNormalX();
1192 pb.surfaceNormalY = ghost->getSurfaceNormalY();
1193 pb.surfaceNormalZ = ghost->getSurfaceNormalZ();
1194 pb.surfaceTangentX = ghost->getSurfaceTangentX();
1195 pb.surfaceTangentY = ghost->getSurfaceTangentY();
1196 pb.surfaceTangentZ = ghost->getSurfaceTangentZ();
1197 createVisual(st, *def, pb, world, st.ghost, 0.5f);
1198 }
1199 {
1201 pb.buildingId = ghost->getBuildingId();
1202 pb.placementKind = ghost->getPlacementKind();
1203 pb.originCellX = ghost->getCellX();
1204 pb.originCellY = ghost->getCellY();
1205 pb.corner = {ghost->getCellX(), ghost->getCellY()};
1206 pb.worldX = ghost->getWorldX();
1207 pb.worldY = ghost->getWorldY();
1208 pb.elevation = ghost->getElevation();
1209 pb.rotationDeg = ghost->getRotationDeg();
1210 pb.surfaceId = ghost->getSurfaceId();
1211 pb.surfaceNormalX = ghost->getSurfaceNormalX();
1212 pb.surfaceNormalY = ghost->getSurfaceNormalY();
1213 pb.surfaceNormalZ = ghost->getSurfaceNormalZ();
1214 pb.surfaceTangentX = ghost->getSurfaceTangentX();
1215 pb.surfaceTangentY = ghost->getSurfaceTangentY();
1216 pb.surfaceTangentZ = ghost->getSurfaceTangentZ();
1217 updateVisual(*def, pb, world, st.ghost);
1218 }
1219
1220 const bool valid = ghost->isValid();
1221 const float cr = valid ? 0.25f : 0.90f;
1222 const float cg = valid ? 0.85f : 0.25f;
1223 const float cb = valid ? 0.35f : 0.22f;
1224 if (st.ghost.r2d) {
1225 auto sp = st.ghost.r2d->sprite();
1226 sp->r = cr;
1227 sp->g = cg;
1228 sp->b = cb;
1229 }
1230 if (st.ghost.r3d) {
1231 auto mr = st.ghost.r3d->meshRenderer();
1232 mr->r = cr;
1233 mr->g = cg;
1234 mr->b = cb;
1235 }
1236 setVisible(st.ghost, true);
1237
1238 // 占地光标。
1239 if (!st.cursor.r2d && !st.cursor.r3d) {
1240 const float cellW = world->getGrid().cellW;
1241 const float cellH = world->getGrid().cellH;
1242 int effW = def->footprintW;
1243 int effH = def->footprintH;
1245 if (is3d(*def)) {
1246 auto *r = graphics::Renderable3D::create();
1247 auto tr = r->transform();
1249 placed.buildingId = ghost->getBuildingId();
1250 placed.originCellX = ghost->getCellX();
1251 placed.originCellY = ghost->getCellY();
1252 placed.worldX = ghost->getWorldX();
1253 placed.worldY = ghost->getWorldY();
1254 placed.placementKind = ghost->getPlacementKind();
1255 placed.elevation = ghost->getElevation();
1256 placed.rotationDeg = ghost->getRotationDeg();
1257 placed.surfaceId = ghost->getSurfaceId();
1258 placed.surfaceNormalX = ghost->getSurfaceNormalX();
1259 placed.surfaceNormalY = ghost->getSurfaceNormalY();
1260 placed.surfaceNormalZ = ghost->getSurfaceNormalZ();
1261 placed.surfaceTangentX = ghost->getSurfaceTangentX();
1262 placed.surfaceTangentY = ghost->getSurfaceTangentY();
1263 placed.surfaceTangentZ = ghost->getSurfaceTangentZ();
1264 placedWorldPosition(placed, *world, tr->x, tr->y, tr->z);
1265 tr->x += placed.surfaceNormalX * 0.03f;
1266 tr->y += placed.surfaceNormalY * 0.03f;
1267 tr->z += placed.surfaceNormalZ * 0.03f;
1268 if (world->getGrid().plane == grid::GridPlane::XZ) tr->yaw = placed.rotationDeg * 3.14159265f / 180.f;
1269 applySurfaceRotation(placed, placed.rotationDeg, *tr);
1270 tr->sx = placed.placementKind == "free" ? freeVisualSize(*def, placed, *world, true, "width")
1271 : placed.placementKind == "corner" ? cornerVisualSize(*def, placed, *world, true, "width")
1272 : float(def->footprintW) * cellW;
1273 tr->sy = 0.04f;
1274 tr->sz = placed.placementKind == "free" ? freeVisualSize(*def, placed, *world, true, "depth")
1275 : placed.placementKind == "corner" ? cornerVisualSize(*def, placed, *world, true, "depth")
1276 : float(def->footprintH) * cellH;
1277 auto mr = r->meshRenderer();
1278 mr->mesh = cubeMesh(gfxOrNull());
1279 mr->r = cr;
1280 mr->g = cg;
1281 mr->b = cb;
1282 mr->a = 0.35f;
1283 st.cursor.r3d = r;
1284 } else {
1285 auto *r = graphics::Renderable2D::create();
1286 auto tr = r->transform();
1287 tr->x = ghost->getWorldX();
1288 tr->y = ghost->getWorldY();
1289 auto sp = r->sprite();
1291 placed.buildingId = ghost->getBuildingId();
1292 placed.originCellX = ghost->getCellX();
1293 placed.originCellY = ghost->getCellY();
1294 placed.placementKind = ghost->getPlacementKind();
1295 sp->width = placed.placementKind == "free"
1296 ? freeVisualSize(*def, placed, *world, false, "width")
1297 : placed.placementKind == "corner"
1298 ? cornerVisualSize(*def, placed, *world, false, "width")
1299 : float(effW) * cellW;
1300 sp->height = placed.placementKind == "free"
1301 ? freeVisualSize(*def, placed, *world, false, "height")
1302 : placed.placementKind == "corner"
1303 ? cornerVisualSize(*def, placed, *world, false, "height")
1304 : float(effH) * cellH;
1305 sp->r = cr;
1306 sp->g = cg;
1307 sp->b = cb;
1308 sp->a = 0.35f;
1309 st.cursor.r2d = r;
1310 }
1311 } else {
1312 const float cellW = world->getGrid().cellW;
1313 const float cellH = world->getGrid().cellH;
1314 int effW = def->footprintW;
1315 int effH = def->footprintH;
1317 if (st.cursor.r3d) {
1318 auto tr = st.cursor.r3d->transform();
1320 placed.buildingId = ghost->getBuildingId();
1321 placed.originCellX = ghost->getCellX();
1322 placed.originCellY = ghost->getCellY();
1323 placed.worldX = ghost->getWorldX();
1324 placed.worldY = ghost->getWorldY();
1325 placed.placementKind = ghost->getPlacementKind();
1326 placed.elevation = ghost->getElevation();
1327 placed.rotationDeg = ghost->getRotationDeg();
1328 placed.surfaceId = ghost->getSurfaceId();
1329 placed.surfaceNormalX = ghost->getSurfaceNormalX();
1330 placed.surfaceNormalY = ghost->getSurfaceNormalY();
1331 placed.surfaceNormalZ = ghost->getSurfaceNormalZ();
1332 placed.surfaceTangentX = ghost->getSurfaceTangentX();
1333 placed.surfaceTangentY = ghost->getSurfaceTangentY();
1334 placed.surfaceTangentZ = ghost->getSurfaceTangentZ();
1335 placedWorldPosition(placed, *world, tr->x, tr->y, tr->z);
1336 tr->x += placed.surfaceNormalX * 0.03f;
1337 tr->y += placed.surfaceNormalY * 0.03f;
1338 tr->z += placed.surfaceNormalZ * 0.03f;
1339 if (world->getGrid().plane == grid::GridPlane::XZ) tr->yaw = placed.rotationDeg * 3.14159265f / 180.f;
1340 applySurfaceRotation(placed, placed.rotationDeg, *tr);
1341 tr->sx = placed.placementKind == "free" ? freeVisualSize(*def, placed, *world, true, "width")
1342 : placed.placementKind == "corner" ? cornerVisualSize(*def, placed, *world, true, "width")
1343 : float(def->footprintW) * cellW;
1344 tr->sz = placed.placementKind == "free" ? freeVisualSize(*def, placed, *world, true, "depth")
1345 : placed.placementKind == "corner" ? cornerVisualSize(*def, placed, *world, true, "depth")
1346 : float(def->footprintH) * cellH;
1347 auto mr = st.cursor.r3d->meshRenderer();
1348 mr->r = cr;
1349 mr->g = cg;
1350 mr->b = cb;
1351 } else if (st.cursor.r2d) {
1352 auto tr = st.cursor.r2d->transform();
1353 tr->x = ghost->getWorldX();
1354 tr->y = ghost->getWorldY();
1355 auto sp = st.cursor.r2d->sprite();
1357 placed.buildingId = ghost->getBuildingId();
1358 placed.originCellX = ghost->getCellX();
1359 placed.originCellY = ghost->getCellY();
1360 placed.placementKind = ghost->getPlacementKind();
1361 sp->width = placed.placementKind == "free"
1362 ? freeVisualSize(*def, placed, *world, false, "width")
1363 : placed.placementKind == "corner"
1364 ? cornerVisualSize(*def, placed, *world, false, "width")
1365 : float(effW) * cellW;
1366 sp->height = placed.placementKind == "free"
1367 ? freeVisualSize(*def, placed, *world, false, "height")
1368 : placed.placementKind == "corner"
1369 ? cornerVisualSize(*def, placed, *world, false, "height")
1370 : float(effH) * cellH;
1371 sp->r = cr;
1372 sp->g = cg;
1373 sp->b = cb;
1374 }
1375 }
1376 setVisible(st.cursor, true);
1377}
1378
1379void BuildingFx::hideGhost(building::PlacementWorld *world) {
1380 if (!world) return;
1381 auto it = states_.find(world);
1382 if (it == states_.end()) return;
1383 WorldState &st = it->second;
1384 setVisible(st.ghost, false);
1385 setVisible(st.cursor, false);
1386}
1387
1388void BuildingFx::updateAreaPreview(building::PlacementWorld *world,
1390 if (!world) return;
1391 auto stateIt = states_.find(world);
1392 if (stateIt == states_.end()) return;
1393 WorldState &st = stateIt->second;
1394 destroyHeatmap(st);
1395 st.heatCells.clear();
1396 if (!session) return;
1397 const int count = session->getAreaPreviewCount();
1398 st.heatCells.reserve(size_t(count));
1399 for (int i = 0; i < count; ++i)
1400 st.heatCells.push_back({session->getAreaPreviewCellX(i), session->getAreaPreviewCellY(i),
1401 session->getAreaPreviewAccepted(i)});
1402}
1403
1404void BuildingFx::clearAreaPreview(building::PlacementWorld *world) {
1405 auto it = states_.find(world);
1406 if (it == states_.end()) return;
1407 destroyHeatmap(it->second);
1408 it->second.heatCells.clear();
1409}
1410
1411int BuildingFx::getAreaPreviewCount(building::PlacementWorld *world) const {
1412 auto it = states_.find(world);
1413 return it == states_.end() ? 0 : int(it->second.heatCells.size());
1414}
1415
1416bool BuildingFx::getAreaPreviewAccepted(building::PlacementWorld *world, int index) const {
1417 auto it = states_.find(world);
1418 return it != states_.end() && index >= 0 && index < int(it->second.heatCells.size()) &&
1419 it->second.heatCells[size_t(index)].accepted;
1420}
1421
1422void BuildingFx::setGridVisible(building::PlacementWorld *world, bool visible) {
1423 if (!world) return;
1424 states_[world].gridVisible = visible;
1425}
1426
1427bool BuildingFx::getGridVisible(building::PlacementWorld *world) const {
1428 auto it = states_.find(world);
1429 return it != states_.end() && it->second.gridVisible;
1430}
1431
1432void BuildingFx::drawGrid2D(building::PlacementWorld *world, graphics::Graphics *gfx) {
1433 if (!world || !gfx) return;
1434 auto it = states_.find(world);
1435 if (it == states_.end() || !it->second.gridVisible) return;
1436 const float cellW = world->getGrid().cellW;
1437 const float cellH = world->getGrid().cellH;
1438 for (int y = 0; y < world->getHeight(); ++y) {
1439 for (int x = 0; x < world->getWidth(); ++x) {
1440 float px = 0.f, py = 0.f;
1441 world->cellToWorldPlane(x, y, px, py);
1442 gfx->drawSolidRect(px, py, cellW - 1.f, cellH - 1.f,
1443 Color{0.75f, 0.78f, 0.85f, 0.06f});
1444 }
1445 }
1446 for (const WorldState::HeatCell &cell : it->second.heatCells) {
1447 float px = 0.f, py = 0.f;
1448 world->cellToWorldPlane(cell.x, cell.y, px, py);
1449 gfx->drawSolidRect(px, py, cellW - 1.f, cellH - 1.f,
1450 cell.accepted ? Color{0.18f, 0.9f, 0.3f, 0.28f}
1451 : Color{0.95f, 0.16f, 0.12f, 0.34f});
1452 }
1453}
1454
1455void BuildingFx::rebuildGridLines(WorldState &st, building::PlacementWorld *world,
1456 graphics::Graphics *gfx, float height) {
1457 const int w = world->getWidth();
1458 const int h = world->getHeight();
1459 const float cellW = world->getGrid().cellW;
1460 const float cellH = world->getGrid().cellH;
1461 const int want = (w + 1) + (h + 1);
1462 if (int(st.gridLines.size()) == want && st.gridLineCount == want) return;
1463 for (graphics::Renderable3D *line : st.gridLines) {
1464 if (line) ecs::DestroyEntity(line);
1465 }
1466 st.gridLines.clear();
1467 graphics::Mesh *mesh = cubeMesh(gfx);
1468 const bool xz = world->getGrid().plane == grid::GridPlane::XZ;
1469 const float xExtent = float(w) * cellW;
1470 const float zExtent = float(h) * cellH;
1471 for (int x = 0; x <= w; ++x) {
1472 auto *r = graphics::Renderable3D::create();
1473 auto tr = r->transform();
1474 float px = 0.f, py = 0.f;
1475 world->cellToWorldPlane(x, 0, px, py);
1476 tr->x = px;
1477 tr->y = height;
1478 tr->z = xz ? 0.0f : 0.f;
1479 tr->sx = 0.03f;
1480 tr->sy = 0.02f;
1481 tr->sz = zExtent;
1482 auto mr = r->meshRenderer();
1483 mr->mesh = mesh;
1484 mr->r = 0.85f;
1485 mr->g = 0.88f;
1486 mr->b = 0.95f;
1487 mr->a = 0.5f;
1488 st.gridLines.push_back(r);
1489 }
1490 for (int y = 0; y <= h; ++y) {
1491 auto *r = graphics::Renderable3D::create();
1492 auto tr = r->transform();
1493 float px = 0.f, py = 0.f;
1494 world->cellToWorldPlane(0, y, px, py);
1495 tr->x = xz ? 0.0f : 0.f;
1496 tr->y = height;
1497 tr->z = py;
1498 tr->sx = xExtent;
1499 tr->sy = 0.02f;
1500 tr->sz = 0.03f;
1501 auto mr = r->meshRenderer();
1502 mr->mesh = mesh;
1503 mr->r = 0.85f;
1504 mr->g = 0.88f;
1505 mr->b = 0.95f;
1506 mr->a = 0.5f;
1507 st.gridLines.push_back(r);
1508 }
1509 st.gridLineCount = want;
1510}
1511
1512void BuildingFx::drawGrid3D(building::PlacementWorld *world, graphics::Graphics *gfx,
1513 float height) {
1514 if (!world || !gfx) return;
1515 auto it = states_.find(world);
1516 if (it == states_.end()) return;
1517 WorldState &st = it->second;
1518 rebuildGridLines(st, world, gfx, height);
1519 for (graphics::Renderable3D *line : st.gridLines) {
1520 if (line) line->meshRenderer()->visible = st.gridVisible;
1521 }
1522 if (st.heatCells3d.size() != st.heatCells.size()) {
1523 destroyHeatmap(st);
1524 for (const WorldState::HeatCell &cell : st.heatCells) {
1525 auto *r = graphics::Renderable3D::create();
1526 float wx = 0.f, wy = 0.f, wz = 0.f;
1527 world->cellToWorld3D(cell.x, cell.y, height + 0.015f, wx, wy, wz);
1528 auto tr = r->transform();
1529 tr->x = wx;
1530 tr->y = wy;
1531 tr->z = wz;
1532 tr->sx = world->getGrid().cellW * 0.92f;
1533 if (world->getGrid().plane == grid::GridPlane::XZ) {
1534 tr->sy = 0.02f;
1535 tr->sz = world->getGrid().cellH * 0.92f;
1536 } else {
1537 tr->sy = world->getGrid().cellH * 0.92f;
1538 tr->sz = 0.02f;
1539 }
1540 auto mr = r->meshRenderer();
1541 mr->mesh = cubeMesh(gfx);
1542 mr->r = cell.accepted ? 0.18f : 0.95f;
1543 mr->g = cell.accepted ? 0.9f : 0.16f;
1544 mr->b = cell.accepted ? 0.3f : 0.12f;
1545 mr->a = 0.35f;
1546 st.heatCells3d.push_back(r);
1547 }
1548 }
1549 for (graphics::Renderable3D *cell : st.heatCells3d)
1550 if (cell) cell->meshRenderer()->visible = st.gridVisible;
1551}
1552
1553} // namespace eve::buildingfx
double value
Duration start
float w
Definition AnimClip.cpp:738
float y
Definition AnimClip.cpp:738
float x
Definition AnimClip.cpp:738
float z
Definition AnimClip.cpp:738
const std::string & s
bool mirrorZ
bool mirrorX
int mask
float rotationDegrees
std::string variant
building::EdgeCurveGroup group
Vec3 tangent
Definition CaveMesh.cpp:80
float py
float nx
float ny
std::string layout
std::uint32_t key
float u
Definition Grass.cpp:233
glm::vec3 n
Definition Grass.cpp:63
Module-neutral grid topology and sizing (no building/map dependency).
double r
std::vector< std::uint32_t > indices
std::vector< float > normals
float v
HexVec3 left
HexVec3 right
std::int32_t second
float elevation
int h
std::vector< Colorf > px
std::uint32_t height
std::uint32_t width
std::array< float, 3 > position
bool valid
MeleePoint3 b
Definition MeleeHit.cpp:41
MeleePoint3 a
Definition MeleeHit.cpp:40
float tr
Texture * normal
int level
#define Module_IMPL(ModuleName, newExpr)
Definition Module.h:26
#define getModInst(N, T)
Definition Module.h:38
Topology topology
World3D * world
float halfWidth
float t
Renderable3D::MeshRenderer * mr
Mesh * mesh
const RoadEdge * edge
bool found
std::uint32_t count
Cell cell
bool placed
std::optional< InteractionSession > session
Definition Tactics.cpp:39
Heightmap curve
std::vector< TileLayer::Tileset::Visual > visuals
bool visible
float(ui::Theme::* member)[4]
uint32_t index
std::uint32_t depth
glm::vec3 point
float wz
float wx
float wy
const std::string & message() const noexcept
Human-readable explanation.
Definition Diagnostic.h:147
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
const Diagnostic * error() const noexcept
Inspect the first diagnostic, or null when none was supplied.
Definition Result.h:298
static Result success(T value)
Construct a successful result owning value.
Definition Result.h:164
bool ok() const noexcept
Whether this result represents a non-failure outcome.
Definition Result.h:255
static Result failure(Status status)
Construct a failed result from a structured status.
Definition Result.h:175
static const BuildingDefinition * find(const std::string &id)
Finds .
EVENGINE_API_WORLD public API.
Definition Ghost.h:16
float getElevation() const
Returns the elevation.
Definition Ghost.h:45
int getCellY() const
Returns the cell y.
Definition Ghost.h:34
float getWorldY() const
Returns the world y.
Definition Ghost.h:41
int getCellX() const
Returns the cell x.
Definition Ghost.h:32
float getSurfaceTangentX() const
X component of the captured unit surface tangent.
Definition Ghost.h:81
std::string getSurfaceId() const
Surface identity captured by the last successful setFromSurface call.
Definition Ghost.h:71
bool isValid() const
True when valid.
Definition Ghost.h:67
std::string getPlacementKind() const
Current placement domain (cell, edge, corner, or free).
Definition Ghost.h:56
float getSurfaceTangentZ() const
Z component of the captured unit surface tangent.
Definition Ghost.h:85
float getWorldX() const
Returns the world x.
Definition Ghost.h:39
float getRotationDeg() const
Returns the rotation deg.
Definition Ghost.h:50
float getSurfaceTangentY() const
Y component of the captured unit surface tangent.
Definition Ghost.h:83
std::string getBuildingId() const
Returns the building id.
Definition Ghost.h:27
float getSurfaceNormalY() const
Y component of the captured unit surface normal.
Definition Ghost.h:77
float getSurfaceNormalZ() const
Z component of the captured unit surface normal.
Definition Ghost.h:79
float getSurfaceNormalX() const
X component of the captured unit surface normal.
Definition Ghost.h:75
EVENGINE_API_WORLD public API.
static eve::Result< EdgeCurveSurface > sampleEdgeCurveSurface(const PlacementWorld &world, const std::string &surfaceName, const std::vector< EdgeCurvePoint > &controlPoints, int subdivisions)
Sample one same-identity custom surface frame at every analytic curve segment.
static void effectiveFootprint(const BuildingDefinition &def, float rotationDeg, int *outW, int *outH)
旋转后的占地宽高(cardinal 90/270 交换)。
static std::string edgeVariant(const PlacementWorld &world, int instanceId)
Stable visual topology class: isolated/end/straight/corner/tee/cross.
static uint8_t edgeConnectionMask(const PlacementWorld &world, int instanceId)
Compute compatible neighbouring edge topology.
格子型建筑放置世界(脚本可直接操作)。
EVENGINE_API_WORLD public API.
Definition BuildingFx.h:41
static eve::Result< CurveMeshData > buildEdgeCurveMesh(const building::PlacementWorld &world, const std::vector< CurveControlPoint > &controlPoints, int subdivisions, float width, float height, float elevation=0.f)
Generate a continuous rectangular wall extrusion along a cubic Bezier curve.
static eve::Result< CurveMeshData > buildSurfaceCurveMesh(const std::vector< CurveSurfaceSample > &samples, float width, float height)
Extrude a wall from committed world-space surface frames.
static eve::Result< CurveMeshData > buildEdgeCurveGroupMeshForInstance(const building::PlacementWorld &world, int instanceId, float width, float height, float elevation=0.f)
Generate the continuous mesh owned by the curve group containing an edge instance.
Mesh * newMeshCube(float size=1.f)
Procedural cube (edge length size, centered at origin, outward CCW for RH Y-up), with per-face normal...
virtual void drawSolidRect(float x, float y, float w, float h, float r, float g, float b, float a=1.f)
RGBA-float overload matching the script-facing drawSolidRect name.
virtual bool updateMeshVertices(Mesh *mesh, const float *posXYZ, const float *nrmXYZ, const float *uvST, int vertexCount, const uint32_t *indices, int indexCount)=0
In-place update of a mesh's vertex/index data (CPU -> host-visible VBO). Mirrors bakeMeshMorph: the u...
virtual Texture * newTextureFromFile(const std::string &filename)=0
Creates a texture from file. @ownership Caller deletes unless documented otherwise.
virtual Mesh * newMeshFromArrays(const float *posXYZ, const float *nrmXYZ, const float *uvST, int vertexCount, const uint32_t *indices, int indexCount)=0
Upload a triangle mesh from packed CPU arrays. Owned by Graphics. posXYZ required (vertexCount*3)....
GPU mesh handle (+ optional CPU morph targets).
Definition Mesh.h:25
EVENGINE_API_BACKENDS public API.
EdgeAxis
Canonical orientation of an edge in logical grid coordinates.
CurvePreviewUpdateStatus
Script-safe outcome of refreshing a custom-surface curve preview.
Definition BuildingFx.h:38
eve::Color Color
RGBA color used by every graphics draw call. Lives inside eve::graphics so including a graphics heade...
Definition Color.h:13
double sample(const Heightmap &map, double u, double v)
Sample.
int axis(int64_t a, size_t rank)
Axis.
glm::vec4 Color
Render-neutral RGBA color shared by graphics-facing modules.
Definition RenderTypes.h:8
建筑模板(进程级注册表中的定义)。
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< int > instanceIds
One committed 3D centerline frame sampled from a custom placement surface.
已放置的建筑实例。
std::string placementKind
Placement domain snapshot: cell, edge, corner, or free.
CornerAddress corner
Canonical grid vertex when placementKind is corner.
std::string surfaceId
Stable provider-local surface name used to create this placement; empty for unsurfaced placement.
float surfaceTangentX
Unit tangent captured with the normal to define the local placement frame.
float surfaceNormalX
Unit surface normal captured at placement time.
One cubic Bezier control point in logical grid-vertex coordinates.
Definition BuildingFx.h:44
Owning backend-neutral indexed extrusion generated along an edge curve.
Definition BuildingFx.h:51
One committed world-space centerline frame for terrain-conforming extrusion.
Definition BuildingFx.h:60
std::vector< CurveControlPoint > controlPoints
Definition BuildingFx.h:211