32float toFloat(
const std::string &
s,
float fallback) {
33 if (
s.empty())
return fallback;
43bool toBool(
const std::string &
value,
bool fallback);
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;
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);
62struct TopologyTransform {
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;
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;
82 result.rotationDegrees =
83 toFloat(visualValue(definition,
placed,
world, use3d,
"rotationDeg"), 0.f);
84 result.mirrorX = toBool(visualValue(definition,
placed,
world, use3d,
"mirrorX"),
86 result.mirrorZ = toBool(visualValue(definition,
placed,
world, use3d,
"mirrorZ"),
91void placedWorldPosition(
const building::PlacedBuilding &
placed,
92 const building::PlacementWorld &
world,
float &
x,
float &
y,
float &
z) {
98 if (
placed.placementKind ==
"cell") {
100 const auto snapMode = definition->snapMode.empty() ?
world.getSnapMode() : definition->snapMode;
101 if (snapMode !=
"grid")
return;
104 float startX = 0.f, startZ = 0.f, endX = 0.f, endZ = 0.f;
107 x += (endX - startX) * 0.5f;
108 z += (endZ - startZ) * 0.5f;
118bool toBool(
const std::string &
value,
bool fallback) {
119 if (
value.empty())
return fallback;
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);
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
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;
147 return toFloat(visualValue(definition,
placed,
world, use3d, axisKey), fallback);
151 graphics::Renderable3D::Transform3D &transform) {
152 if (
placed.surfaceId.empty())
return;
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);
163 glm::vec3(0.f, 1.f, 0.f));
164 glm::extractEulerAngleYXZ(frame, transform.yaw, transform.pitch, transform.roll);
171 const std::vector<CurveControlPoint> &controlPoints,
int subdivisions,
float width,
173 if (controlPoints.size() != 4 || subdivisions < 2 || subdivisions > 4096 ||
178 "curve mesh requires four controls, 2..4096 subdivisions, and positive dimensions",
179 {}, {},
"buildingfx.edge-curve-mesh"));
182 if (!std::isfinite(
point.x) || !std::isfinite(
point.y)) {
185 {},
"buildingfx.edge-curve-mesh"));
192 "continuous curve mesh requires an affine rectangle or isometric grid", {}, {},
193 "buildingfx.edge-curve-mesh"));
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);
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};
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));
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);
239 mesh.length = distances.back();
240 if (!(
mesh.length > 1e-5f)) {
243 {},
"buildingfx.edge-curve-mesh"));
247 const auto appendVertex = [&](Point
point,
float vertical,
float nx,
float ny,
248 float normalVertical,
float u,
float v) {
251 mesh.normals.insert(
mesh.normals.end(), {
nx, normalVertical,
ny});
254 mesh.normals.insert(
mesh.normals.end(), {
nx,
ny, normalVertical});
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);
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"));
279 tangents[sample] = {tx, ty};
280 const float px = -ty;
286 const float u = distances[sample] /
mesh.length;
287 appendVertex(
left[sample], 0.f, -
px, -
py, 0.f,
u, 0.f);
289 appendVertex(
right[sample], 0.f,
px,
py, 0.f,
u, 0.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);
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));
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);
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};
319 appendCap(subdivisions,
false);
324 const std::vector<CurveSurfaceSample> &samples,
float width,
float height) {
325 if (samples.size() < 2 || !std::isfinite(
width) || !std::isfinite(
height) ||
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());
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) ||
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);
350 if (distances.back() <= 1e-5f)
353 "buildingfx.surface-curve-mesh"));
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;
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"));
370 mesh.length = distances.back();
371 const auto append = [&](
const glm::vec3 &
position,
const glm::vec3 &
normal,
float u,
383 const float u = distances[
index] /
mesh.length;
385 append(leftTop, -laterals[
index],
u, 1.f);
387 append(rightTop, laterals[
index],
u, 1.f);
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));
402 const auto cap = [&](
size_t index,
bool start) {
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);
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));
418 cap(centers.size() - 1,
false);
425 if (instanceId <= 0) {
428 std::to_string(instanceId), {},
"buildingfx.edge-curve-group-mesh"));
430 auto groupResult =
world.edgeCurveGroupForInstance(instanceId);
434 if (!
group.surfaceSamples.empty()) {
435 std::vector<CurveSurfaceSample> samples;
436 samples.reserve(
group.surfaceSamples.size());
438 samples.push_back({sample.worldX, sample.worldY, sample.worldZ, sample.normalX,
439 sample.normalY, sample.normalZ});
442 std::vector<CurveControlPoint> controls;
443 controls.reserve(
group.controlPoints.size());
451 const std::vector<CurveControlPoint> &controlPoints,
int subdivisions,
int level) {
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"));
461 const float width = toFloat(definition->getVisual3d(
"thickness"),
462 world->getCellSize() * 0.1f);
463 const float height = toFloat(definition->getVisual3d(
"height"), 1.f);
465 auto generated = buildEdgeCurveMesh(*
world, controlPoints, subdivisions,
width,
height,
469 return presentCurvePreview(
state->second, std::move(generated).takeValue(), controlPoints,
475 const std::string &surfaceName) {
481 "surface curve preview requires an attached world and its active session", {}, {},
482 "buildingfx.edge-curve-surface-preview"));
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"));
494 *
world, surfaceName,
session->edgeCurveControlPoints(), subdivisions);
496 std::vector<CurveSurfaceSample> samples;
497 samples.reserve(sampled.value().samples.size());
499 samples.push_back({sample.worldX, sample.worldY, sample.worldZ, sample.normalX,
500 sample.normalY, sample.normalZ});
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);
506 std::vector<CurveControlPoint> controls;
507 controls.reserve(
session->edgeCurveControlPoints().size());
509 session->edgeCurveControlPoints())
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);
518 const std::string &surfaceName) {
519 return updateEdgeCurveSurfacePreview(
world,
session, subdivisions, surfaceName).ok()
520 ? CurvePreviewUpdateStatus::Updated
521 : CurvePreviewUpdateStatus::Rejected;
525 WorldState &state, CurveMeshData
mesh, std::vector<CurveControlPoint> controls,
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;
535 preview.surfaceId = std::move(surfaceId);
536 preview.surfaceRevision = surfaceRevision;
537 preview.active =
true;
538 preview.fallbackReason.clear();
542 preview.fallbackReason =
"graphics_unavailable";
543 if (preview.renderable) preview.renderable->meshRenderer()->visible =
false;
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;
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()));
561 preview.mesh =
nullptr;
565 preview.fallbackReason =
"mesh_upload_failed";
568 if (!preview.renderable) preview.renderable = graphics::Renderable3D::create();
569 auto renderer = preview.renderable->meshRenderer();
570 renderer->mesh = preview.mesh;
575 renderer->visible =
true;
576 renderer->castShadow =
false;
577 renderer->receiveShadow =
false;
583 if (
state == states_.end())
return;
597 return state != states_.end() &&
state->second.curvePreview.active &&
598 !
state->second.curvePreview.cpuMesh.positions.empty();
601std::string BuildingFx::getEdgeCurvePreviewFallbackReason(
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";
610std::string BuildingFx::getEdgeCurvePreviewSurfaceId(
613 if (
state == states_.end() || !
state->second.curvePreview.active)
return {};
614 return state->second.curvePreview.surfaceId;
617std::uint64_t BuildingFx::getEdgeCurvePreviewSurfaceRevision(
620 if (
state == states_.end() || !
state->second.curvePreview.active)
return 0;
621 return state->second.curvePreview.surfaceRevision;
625 if (!
world)
return false;
631 if (!
world)
return false;
632 auto it = states_.find(
world);
633 if (it == states_.end())
return false;
634 destroyAll(it->second);
640 return world !=
nullptr && states_.count(
world) > 0;
643int BuildingFx::getAttachedCount()
const {
return int(states_.size()); }
650 if (!gfx)
return nullptr;
655void BuildingFx::createVisual(WorldState &st,
const building::BuildingDefinition &def,
656 const building::PlacedBuilding &pb,
657 building::PlacementWorld *
world, Visual &
v,
float alpha) {
659 const float cellW =
world->getGrid().cellW;
660 const float cellH =
world->getGrid().cellH;
661 int effW = def.footprintW;
662 int effH = def.footprintH;
664 const TopologyTransform
topology = topologyTransform(def, pb, *
world, is3d(def));
669 auto *
r = graphics::Renderable3D::create();
670 float wx = 0.f,
wy = 0.f,
wz = 0.f;
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) *
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) *
691 const float visualRotation = pb.rotationDeg +
topology.rotationDegrees;
692 const float rad = visualRotation * 3.14159265f / 180.f;
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";
707 resolved = meshResolver_(
v.resourceId);
708 v.fallbackReason = resolved ? std::string{} :
"resource_unresolved";
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);
716 mr->castShadow =
true;
717 mr->receiveShadow =
true;
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;
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")
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);
744 sp->layer = int(toFloat(def.getVisual2d(
"layer"), 0.f));
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()) {
754 v.fallbackReason =
"resource_unresolved";
757 v.fallbackReason =
"resolver_unavailable";
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;
772 const TopologyTransform
topology = topologyTransform(def, pb, *
world, is3d(def));
777 float wx = 0.f,
wy = 0.f,
wz = 0.f;
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")
790 : float(def.footprintW) * cellW) *
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) *
798 const float visualRotation = pb.rotationDeg +
topology.rotationDegrees;
799 const float rad = visualRotation * 3.14159265f / 180.f;
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";
814 resolved = meshResolver_(
v.resourceId);
815 v.fallbackReason = resolved ? std::string{} :
"resource_unresolved";
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);
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;
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")
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()) {
852 v.fallbackReason = sp->texture ? std::string{} :
"resource_unresolved";
854 v.fallbackReason =
"resolver_unavailable";
860void BuildingFx::destroyVisual(Visual &
v) {
862 ecs::DestroyEntity(
v.r2d);
866 ecs::DestroyEntity(
v.r3d);
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;
876void BuildingFx::destroyAll(WorldState &st) {
879 for (
auto &[groupId,
curve] : st.curveVisuals) {
881 if (
curve.renderable) ecs::DestroyEntity(
curve.renderable);
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) {
892 st.gridLines.clear();
893 st.gridLineCount = -1;
897void BuildingFx::destroyHeatmap(WorldState &st) {
898 for (graphics::Renderable3D *
cell : st.heatCells3d)
900 st.heatCells3d.clear();
905 auto it = states_.find(
world);
906 if (it == states_.end())
return;
907 WorldState &st = it->second;
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;
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]);
925 createVisual(st, *def, pb,
world,
v, 1.f);
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);
936 for (
auto &kv : st.visuals) {
937 if (next.count(kv.first) == 0) destroyVisual(kv.second);
939 st.visuals = std::move(next);
940 syncCurveVisuals(st,
world);
944 for (
auto &[groupId,
curve] : st.curveVisuals) {
946 curve.active =
false;
949 auto groupResult =
world->edgeCurveGroup(groupId);
950 if (!groupResult.ok() || groupResult.value().instanceIds.empty())
continue;
957 if (!definition)
continue;
959 WorldState::CurveVisual &
curve = st.curveVisuals[groupId.value];
962 std::vector<CurveControlPoint> controls;
965 const float width = toFloat(definition->getVisual3d(
"thickness"),
966 std::min(
world->getGrid().cellW,
967 world->getGrid().cellH) *
969 const float height = toFloat(definition->getVisual3d(
"height"), 1.f);
971 static_cast<float>(
placed.level) *
world->getFloorHeight();
972 const bool changed =
curve.controlPoints != controls ||
977 curve.fallbackReason.clear();
978 if (!is3d(*definition)) {
979 curve.fallbackReason =
"render_mode_2d";
980 }
else if (changed ||
curve.cpuMesh.positions.empty()) {
985 std::vector<CurveSurfaceSample> samples;
986 for (
const building::EdgeCurveSurfaceSample &sample :
group.surfaceSamples)
989 return buildSurfaceCurveMesh(samples,
width,
height);
991 if (!generated.
ok()) {
993 :
"mesh_generation_failed";
995 curve.cpuMesh = std::move(generated).takeValue();
996 curve.controlPoints = std::move(controls);
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;
1014 if (
curve.fallbackReason.empty() && !
curve.mesh) {
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()));
1021 curve.mesh =
nullptr;
1023 if (!
curve.mesh)
curve.fallbackReason =
"mesh_upload_failed";
1025 if (
curve.fallbackReason.empty() && !
curve.renderable) {
1026 curve.renderable = graphics::Renderable3D::create();
1027 curve.renderable->meshRenderer()->mesh =
curve.mesh;
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);
1036 renderer->castShadow =
true;
1037 renderer->receiveShadow =
true;
1039 curve.fallbackReason.empty() &&
1040 (st.levelVisibility == WorldState::LevelVisibility::All ||
1041 (st.levelVisibility == WorldState::LevelVisibility::Active &&
1043 (st.levelVisibility == WorldState::LevelVisibility::ActiveAndBelow &&
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);
1053 for (
auto &[groupId,
curve] : st.curveVisuals) {
1055 if (
curve.active)
continue;
1056 if (
curve.renderable) {
1057 ecs::DestroyEntity(
curve.renderable);
1058 curve.renderable =
nullptr;
1064 auto it = states_.find(
world);
1065 return it == states_.end() ? 0 : int(it->second.visuals.size());
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; }));
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();
1088 int instanceId)
const {
1090 if (
state == states_.end())
return "world_not_attached";
1091 for (
const auto &[groupId,
curve] :
state->second.curveVisuals) {
1094 std::find(
curve.memberIds.begin(),
curve.memberIds.end(), instanceId) !=
1095 curve.memberIds.end())
1096 return curve.fallbackReason;
1098 return "curve_group_not_found";
1102 const std::string &mode) {
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;
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";
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;
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;
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;
1153 int instanceId)
const {
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;
1162 if (!
world || !ghost) {
1166 auto it = states_.find(
world);
1167 if (it == states_.end())
return;
1168 WorldState &st = it->second;
1174 destroyVisual(st.ghost);
1175 destroyVisual(st.cursor);
1178 if (!st.ghost.r2d && !st.ghost.r3d) {
1197 createVisual(st, *def, pb,
world, st.ghost, 0.5f);
1217 updateVisual(*def, pb,
world, st.ghost);
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;
1225 auto sp = st.ghost.r2d->sprite();
1231 auto mr = st.ghost.r3d->meshRenderer();
1236 setVisible(st.ghost,
true);
1239 if (!st.cursor.r2d && !st.cursor.r3d) {
1240 const float cellW =
world->getGrid().cellW;
1241 const float cellH =
world->getGrid().cellH;
1246 auto *
r = graphics::Renderable3D::create();
1247 auto tr =
r->transform();
1265 tr->x +=
placed.surfaceNormalX * 0.03f;
1266 tr->y +=
placed.surfaceNormalY * 0.03f;
1267 tr->z +=
placed.surfaceNormalZ * 0.03f;
1270 tr->sx =
placed.placementKind ==
"free" ? freeVisualSize(*def,
placed, *
world,
true,
"width")
1271 :
placed.placementKind ==
"corner" ? cornerVisualSize(*def,
placed, *
world,
true,
"width")
1274 tr->sz =
placed.placementKind ==
"free" ? freeVisualSize(*def,
placed, *
world,
true,
"depth")
1275 :
placed.placementKind ==
"corner" ? cornerVisualSize(*def,
placed, *
world,
true,
"depth")
1277 auto mr =
r->meshRenderer();
1278 mr->mesh = cubeMesh(gfxOrNull());
1285 auto *
r = graphics::Renderable2D::create();
1286 auto tr =
r->transform();
1289 auto sp =
r->sprite();
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;
1312 const float cellW =
world->getGrid().cellW;
1313 const float cellH =
world->getGrid().cellH;
1317 if (st.cursor.r3d) {
1318 auto tr = st.cursor.r3d->transform();
1336 tr->x +=
placed.surfaceNormalX * 0.03f;
1337 tr->y +=
placed.surfaceNormalY * 0.03f;
1338 tr->z +=
placed.surfaceNormalZ * 0.03f;
1341 tr->sx =
placed.placementKind ==
"free" ? freeVisualSize(*def,
placed, *
world,
true,
"width")
1342 :
placed.placementKind ==
"corner" ? cornerVisualSize(*def,
placed, *
world,
true,
"width")
1344 tr->sz =
placed.placementKind ==
"free" ? freeVisualSize(*def,
placed, *
world,
true,
"depth")
1345 :
placed.placementKind ==
"corner" ? cornerVisualSize(*def,
placed, *
world,
true,
"depth")
1347 auto mr = st.cursor.r3d->meshRenderer();
1351 }
else if (st.cursor.r2d) {
1352 auto tr = st.cursor.r2d->transform();
1355 auto sp = st.cursor.r2d->sprite();
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;
1376 setVisible(st.cursor,
true);
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);
1391 auto stateIt = states_.find(
world);
1392 if (stateIt == states_.end())
return;
1393 WorldState &st = stateIt->second;
1395 st.heatCells.clear();
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)});
1405 auto it = states_.find(
world);
1406 if (it == states_.end())
return;
1407 destroyHeatmap(it->second);
1408 it->second.heatCells.clear();
1412 auto it = states_.find(
world);
1413 return it == states_.end() ? 0 : int(it->second.heatCells.size());
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;
1428 auto it = states_.find(
world);
1429 return it != states_.end() && it->second.gridVisible;
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;
1443 Color{0.75f, 0.78f, 0.85f, 0.06f});
1447 float px = 0.f,
py = 0.f;
1450 cell.accepted ?
Color{0.18f, 0.9f, 0.3f, 0.28f}
1451 :
Color{0.95f, 0.16f, 0.12f, 0.34f});
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;
1464 if (
line) ecs::DestroyEntity(
line);
1466 st.gridLines.clear();
1467 graphics::Mesh *
mesh = cubeMesh(gfx);
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;
1478 tr->z = xz ? 0.0f : 0.f;
1482 auto mr =
r->meshRenderer();
1488 st.gridLines.push_back(
r);
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;
1495 tr->x = xz ? 0.0f : 0.f;
1501 auto mr =
r->meshRenderer();
1507 st.gridLines.push_back(
r);
1509 st.gridLineCount = want;
1514 if (!
world || !gfx)
return;
1515 auto it = states_.find(
world);
1516 if (it == states_.end())
return;
1517 WorldState &st = it->second;
1520 if (
line)
line->meshRenderer()->visible = st.gridVisible;
1522 if (st.heatCells3d.size() != st.heatCells.size()) {
1525 auto *
r = graphics::Renderable3D::create();
1526 float wx = 0.f,
wy = 0.f,
wz = 0.f;
1528 auto tr =
r->transform();
1532 tr->sx =
world->getGrid().cellW * 0.92f;
1535 tr->sz =
world->getGrid().cellH * 0.92f;
1537 tr->sy =
world->getGrid().cellH * 0.92f;
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;
1546 st.heatCells3d.push_back(
r);
1550 if (
cell)
cell->meshRenderer()->visible = st.gridVisible;