20 std::vector<FragmentCollisionRect> result;
22 return fragment.
cells[std::size_t(
y) * std::size_t(
width) + std::size_t(
x)].material !=
27 const std::size_t
start = std::size_t(
y) * std::size_t(
width) + std::size_t(
x);
34 for (;
y + rectHeight <
height; ++rectHeight) {
36 for (
int ox = 0;
ox < rectWidth; ++
ox) {
37 const std::size_t
index = std::size_t(
y + rectHeight) * std::size_t(
width) +
46 for (
int oy = 0;
oy < rectHeight; ++
oy)
47 for (
int ox = 0;
ox < rectWidth; ++
ox)
49 result.push_back({
x,
y, rectWidth, rectHeight});
57 if (
turns == 0)
return result;
60 result.
cells.assign(std::size_t(result.
width) * std::size_t(result.
height), {});
67 }
else if (
turns == 2) {
74 result.
cells[std::size_t(ry) * std::size_t(result.
width) + std::size_t(rx)] =
75 source.cells[std::size_t(
y) * std::size_t(
source.width) + std::size_t(
x)];
83 auto operator<=>(
const GridPoint&)
const =
default;
92 if (
edge.to.x >
edge.from.x)
return 0;
93 if (
edge.to.y >
edge.from.y)
return 1;
94 if (
edge.to.x <
edge.from.x)
return 2;
98int turnRank(
int previous,
int next) {
100 if (
turn == 1)
return 0;
101 if (
turn == 0)
return 1;
102 if (
turn == 3)
return 2;
106bool collinear(GridPoint
a, GridPoint
b, GridPoint
c) {
107 return (
b.x -
a.x) * (
c.y -
b.y) == (
b.y -
a.y) * (
c.x -
b.x);
112 double(
minimum) >= double(std::numeric_limits<int>::min()) + 2.0 &&
113 double(
maximum) <= double(std::numeric_limits<int>::max()) - 2.0;
120 std::uint32_t maximumVertices) {
121 if (maximumVertices < 2)
124 "maximumVertices", {},
"pixelworld_physics"));
126 const std::int64_t wideOriginX = std::int64_t(chunkX) *
size;
127 const std::int64_t wideOriginY = std::int64_t(chunkY) *
size;
128 if (wideOriginX < std::numeric_limits<int>::min() ||
129 wideOriginX +
size > std::numeric_limits<int>::max() ||
130 wideOriginY < std::numeric_limits<int>::min() ||
131 wideOriginY +
size > std::numeric_limits<int>::max())
134 "pixelworld_physics"));
135 const int originX = int(wideOriginX), originY = int(wideOriginY);
136 const auto solid = [&](
int localX,
int localY) {
140 std::vector<GridEdge>
edges;
143 for (
int x = 0;
x <
size; ++
x) {
144 if (!solid(
x,
y))
continue;
145 if (!solid(
x,
y - 1))
edges.push_back({{
x,
y}, {
x + 1,
y}});
146 if (!solid(
x + 1,
y))
edges.push_back({{
x + 1,
y}, {
x + 1,
y + 1}});
147 if (!solid(
x,
y + 1))
edges.push_back({{
x + 1,
y + 1}, {
x,
y + 1}});
148 if (!solid(
x - 1,
y))
edges.push_back({{
x,
y + 1}, {
x,
y}});
150 std::map<GridPoint, std::vector<std::size_t>> outgoing;
153 std::vector<std::uint8_t> used(
edges.size());
154 std::vector<TerrainCollisionContour> contours;
157 if (used[
first])
continue;
169 const auto found = outgoing.find(
end);
170 if (
found == outgoing.end())
break;
173 for (
const std::size_t candidate :
found->
second) {
174 if (used[candidate])
continue;
176 if (rank < bestRank || (rank == bestRank && candidate < next)) {
181 if (next ==
edges.size())
break;
185 while (changed &&
points.size() > (
loop ? 3U : 2U)) {
190 const std::size_t after = (
index + 1) %
points.size();
198 if (
points.size() < (
loop ? 3U : 2U))
continue;
203 "maximumVertices", {},
"pixelworld_physics"));
204 TerrainCollisionContour contour;
206 contour.vertices.reserve(
points.size() * 2);
208 contour.vertices.push_back(
float(
point.x));
209 contour.vertices.push_back(
float(
point.y));
211 contours.push_back(std::move(contour));
218 std::uint32_t maximumCells) {
219 if (!std::isfinite(centerX) || !std::isfinite(centerY) || !std::isfinite(
radius) ||
220 radius <= 0.f || maximumCells == 0)
222 "circle probe parameters are invalid",
223 "probe", {},
"pixelworld_physics"));
224 if (!validCellBounds(centerX -
radius, centerX +
radius) ||
228 "probe", {},
"pixelworld_physics"));
229 const int minX = int(std::floor(centerX -
radius));
230 const int maxX = int(std::floor(centerX +
radius));
231 const int minY = int(std::floor(centerY -
radius));
232 const int maxY = int(std::floor(centerY +
radius));
233 const std::uint64_t
width = std::uint64_t(std::int64_t(maxX) - minX + 1);
234 const std::uint64_t
height = std::uint64_t(std::int64_t(maxY) - minY + 1);
238 "maximumCells", {},
"pixelworld_physics"));
240 for (
int y = minY;
y <= maxY; ++
y)
241 for (
int x = minX;
x <= maxX; ++
x) {
244 const float closestX = std::clamp(centerX,
float(
x),
float(
x + 1));
245 const float closestY = std::clamp(centerY,
float(
y),
float(
y + 1));
246 const float dx = centerX - closestX,
dy = centerY - closestY;
247 const float distanceSquared =
dx *
dx +
dy *
dy;
249 const float distance = std::sqrt(distanceSquared);
258 best.pointX = closestX;
259 best.pointY = closestY;
265 const float left = centerX - float(
x),
right = float(
x + 1) - centerX;
266 const float top = centerY - float(
y),
bottom = float(
y + 1) - centerY;
268 if (nearest ==
left)
best.normalX = -1.f;
269 else if (nearest ==
right)
best.normalX = 1.f;
270 else if (nearest ==
top)
best.normalY = -1.f;
271 else best.normalY = 1.f;
279 float endY,
float radius, std::uint32_t maximumCells) {
280 if (!std::isfinite(startX) || !std::isfinite(startY) || !std::isfinite(endX) ||
281 !std::isfinite(endY) || !std::isfinite(
radius) ||
radius <= 0.f || maximumCells == 0)
283 "circle sweep parameters are invalid",
284 "sweep", {},
"pixelworld_physics"));
285 if (!validCellBounds(std::min(startX, endX) -
radius,
286 std::max(startX, endX) +
radius) ||
287 !validCellBounds(std::min(startY, endY) -
radius,
288 std::max(startY, endY) +
radius))
291 "sweep", {},
"pixelworld_physics"));
293 if (!initial.ok())
return initial;
294 if (initial.value().hit)
return initial;
295 const int minX = int(std::floor(std::min(startX, endX) -
radius));
296 const int maxX = int(std::floor(std::max(startX, endX) +
radius));
297 const int minY = int(std::floor(std::min(startY, endY) -
radius));
298 const int maxY = int(std::floor(std::max(startY, endY) +
radius));
299 const std::uint64_t
width = std::uint64_t(std::int64_t(maxX) - minX + 1);
300 const std::uint64_t
height = std::uint64_t(std::int64_t(maxY) - minY + 1);
304 "maximumCells", {},
"pixelworld_physics"));
305 const float deltaX = endX - startX, deltaY = endY - startY;
308 for (
int y = minY;
y <= maxY; ++
y)
309 for (
int x = minX;
x <= maxX; ++
x) {
312 float cellFraction = 2.f, cellNormalX = 0.f, cellNormalY = 0.f;
313 const auto consider = [&](
float fraction,
float nx,
float ny) {
314 if (fraction >= 0.f && fraction <= 1.f && fraction < cellFraction) {
315 cellFraction = fraction;
321 const float fraction = (float(
x) -
radius - startX) / deltaX;
322 const float centerY = startY + deltaY * fraction;
323 if (centerY >=
float(
y) && centerY <= float(
y + 1))
324 consider(fraction, -1.f, 0.f);
325 }
else if (deltaX < 0.f) {
326 const float fraction = (float(
x + 1) +
radius - startX) / deltaX;
327 const float centerY = startY + deltaY * fraction;
328 if (centerY >=
float(
y) && centerY <= float(
y + 1))
329 consider(fraction, 1.f, 0.f);
332 const float fraction = (float(
y) -
radius - startY) / deltaY;
333 const float centerX = startX + deltaX * fraction;
334 if (centerX >=
float(
x) && centerX <= float(
x + 1))
335 consider(fraction, 0.f, -1.f);
336 }
else if (deltaY < 0.f) {
337 const float fraction = (float(
y + 1) +
radius - startY) / deltaY;
338 const float centerX = startX + deltaX * fraction;
339 if (centerX >=
float(
x) && centerX <= float(
x + 1))
340 consider(fraction, 0.f, 1.f);
342 const float quadraticA = deltaX * deltaX + deltaY * deltaY;
343 if (quadraticA > 0.f) {
350 const Corner corners[] = {
351 {float(
x), float(
y), -1, -1}, {float(
x + 1), float(
y), 1, -1},
352 {float(
x + 1), float(
y + 1), 1, 1}, {float(
x), float(
y + 1), -1, 1}};
353 for (
const Corner corner : corners) {
354 const float offsetX = startX - corner.x,
offsetY = startY - corner.y;
355 const float quadraticB = 2.f * (
offsetX * deltaX +
offsetY * deltaY);
357 const float discriminant = quadraticB * quadraticB - 4.f * quadraticA * quadraticC;
358 if (discriminant < 0.f)
continue;
359 const float fraction =
360 (-quadraticB - std::sqrt(discriminant)) / (2.f * quadraticA);
361 if (fraction < 0.f || fraction > 1.f)
continue;
362 const float hitX = startX + deltaX * fraction;
363 const float hitY = startY + deltaY * fraction;
364 if ((corner.quadrantX < 0 && hitX > corner.x) ||
365 (corner.quadrantX > 0 && hitX < corner.x) ||
366 (corner.quadrantY < 0 && hitY > corner.y) ||
367 (corner.quadrantY > 0 && hitY < corner.y))
369 consider(fraction, (hitX - corner.x) /
radius,
370 (hitY - corner.y) /
radius);
373 if (cellFraction > 1.f)
continue;
374 if (
best.hit && cellFraction >
best.fraction)
continue;
375 if (
best.hit && cellFraction ==
best.fraction &&
376 std::pair(
y,
x) >= std::pair(
best.cellY,
best.cellX))
382 best.fraction = cellFraction;
383 best.pointX = startX + deltaX * cellFraction;
384 best.pointY = startY + deltaY * cellFraction;
385 best.normalX = cellNormalX;
386 best.normalY = cellNormalY;
394 std::vector<FragmentCollisionRect> rectangles)
395 : fragment_(
std::move(fragment)), link_(link), rectangles_(
std::move(rectangles)) {}
399 if (fragment.
width <= 0 || fragment.
height <= 0 || fragment.
id == 0 ||
400 std::uint64_t(fragment.
width) * std::uint64_t(fragment.
height) != fragment.
cells.size())
403 "fragment", {},
"pixelworld_physics"));
409 {},
"pixelworld_physics"));
410 auto rectangles = decompose(fragment);
414 "fragment collision decomposition is empty or exceeds maximumFixtures",
415 "maximumFixtures", {},
"pixelworld_physics"));
419 const float centerX = float(fragment.
originX) + float(fragment.
width) * 0.5f;
420 const float centerY = float(fragment.
originY) + float(fragment.
height) * 0.5f;
421 body =
world.newBody(
"dynamic", centerX, centerY);
423 const float offsetX = float(rect.x) + float(rect.width) * 0.5f - float(fragment.
width) * 0.5f;
424 const float offsetY = float(rect.y) + float(rect.height) * 0.5f - float(fragment.
height) * 0.5f;
435 std::move(fragment), std::move(link).takeValue(), std::move(rectangles))));
436 }
catch (
const std::exception&
error) {
440 {},
"pixelworld_physics"));
444const std::vector<FragmentCollisionRect>& PixelFragmentBody::collisionRects() const noexcept {
448std::uint64_t PixelFragmentBody::fragmentId() const noexcept {
return fragment_.id; }
450bool PixelFragmentBody::isRasterized() const noexcept {
return rasterized_; }
454 if (rasterized_ || physicsReleased_)
457 "state", {},
"pixelworld_physics"));
458 auto resolved = link_.resolve(physicsWorld);
463 {FragmentSettleDisposition::StillAwake, fragment_.id, 0, 0});
465 constexpr double halfPi = 1.57079632679489661923;
466 const int turns = int(std::llround(
double(
body->getAngle()) / halfPi));
467 auto candidate = rotated(fragment_,
turns);
468 const int originX = int(std::lround(
double(
body->getX()) -
double(candidate.width) * 0.5));
469 const int originY = int(std::lround(
double(
body->getY()) -
double(candidate.height) * 0.5));
473 physicsReleased_ =
true;
476 {FragmentSettleDisposition::Rasterized, fragment_.id,
477 rasterized.value().cellsPlaced, ((
turns % 4) + 4) % 4});
482 auto resolved = link_.resolve(physicsWorld);
484 resolved.value()->destroy();
485 physicsReleased_ =
true;
491 std::map<Coord, eve::physics::PhysicsLink>
bodies;
498PixelTerrainCollisionCache::PixelTerrainCollisionCache() : impl_(
std::make_unique<
Impl>()) {}
504 eve::physics::World& physicsWorld, const
eve::pixelworld::PixelWorld& pixelWorld,
505 std::uint32_t maximumFixturesPerChunk) {
506 if (maximumFixturesPerChunk == 0)
509 "maximumFixturesPerChunk", {},
"pixelworld_physics"));
510 const bool samePixelWorld = impl_->pixelWorld == pixelWorld.worldLink();
511 const bool samePhysicsWorld = impl_->physicsWorld == physicsWorld.runtimeHandle();
512 if (!samePhysicsWorld && !impl_->bodies.empty() && !impl_->physicsLifetime.expired())
515 "terrain collision cache must be cleared from its live physics world before rebinding",
"physicsWorld", {},
516 "pixelworld_physics"));
517 const std::uint64_t since = (samePixelWorld && samePhysicsWorld) ? impl_->revision : 0;
518 const auto changed = pixelWorld.snapshotChangedChunks(since);
519 std::set<Impl::Coord> rebuildCoords;
520 for (
const auto& chunk : changed) {
522 rebuildCoords.insert(coord);
523 constexpr Impl::Coord neighbors[] = {{-1, 0}, {1, 0}, {0, -1}, {0, 1}};
524 for (
const auto& [
dx,
dy] : neighbors) {
526 if (impl_->bodies.contains(neighbor)) rebuildCoords.insert(neighbor);
535 std::vector<Candidate> staged;
536 staged.reserve(rebuildCoords.size());
537 const auto destroyStaged = [&] {
538 for (
const Candidate& candidate : staged) {
539 auto resolved = candidate.link.resolve(physicsWorld);
540 if (resolved.ok()) resolved.value()->destroy();
544 for (
const auto& coord : rebuildCoords) {
546 if (!extracted.ok()) {
550 auto contours = std::move(extracted).takeValue();
551 if (contours.size() > maximumFixturesPerChunk) {
555 "chunk", {},
"pixelworld_physics"));
557 if (contours.empty()) {
558 staged.push_back({coord, {}, 0});
563 body = physicsWorld.newBody(
574 staged.push_back({coord, std::move(link).takeValue(),
575 std::uint32_t(contours.size())});
576 }
catch (
const std::exception&
error) {
581 "physics", {},
"pixelworld_physics"));
588 if (!samePhysicsWorld) {
589 impl_->bodies.clear();
590 }
else if (!samePixelWorld) {
591 for (
const auto& [coord, link] : impl_->bodies) {
593 auto resolved = link.resolve(physicsWorld);
594 if (resolved.ok()) resolved.value()->destroy();
597 impl_->bodies.clear();
599 for (
const Candidate& candidate : staged) {
600 const auto old = impl_->bodies.find(candidate.coord);
601 if (old != impl_->bodies.end()) {
602 auto resolved = old->second.resolve(physicsWorld);
603 if (resolved.ok()) resolved.value()->destroy();
604 impl_->bodies.erase(old);
607 if (candidate.fixtures != 0) {
608 impl_->bodies.emplace(candidate.coord, candidate.link);
612 impl_->pixelWorld = pixelWorld.worldLink();
613 impl_->physicsWorld = physicsWorld.runtimeHandle();
614 impl_->physicsLifetime = physicsWorld.lifetimeToken();
615 impl_->revision = pixelWorld.revision();
620 if (impl_->physicsWorld.isValid() && impl_->physicsWorld != physicsWorld.
runtimeHandle())
623 "physicsWorld", {},
"pixelworld_physics"));
624 for (
const auto& [coord, link] : impl_->bodies) {
626 auto resolved = link.resolve(physicsWorld);
627 if (resolved.ok()) resolved.value()->destroy();
629 impl_->bodies.clear();
630 impl_->pixelWorld = {};
632 impl_->physicsLifetime.reset();
std::uint32_t vertexCount
HexCoordinates to
Cell the unit walks towards on this segment.
graphics::Canvas * previous
std::shared_ptr< const std::vector< glm::vec2 > > points
RoadLaneDirection direction
std::map< Cell, int > best
TacticalUnit::TurnResources turn
const UnitySourceAsset & source
static Diagnostic error(DiagnosticCode code, std::string message, std::string path={}, DiagnosticDetails details={}, std::string source={})
Construct an error diagnostic with the standard error severity.
Move-only operation result carrying either a value or Status.
static Result success(T value)
Construct a successful result owning value.
static Result failure(Status status)
Construct a failed result from a structured status.
static constexpr RuntimeHandle invalid() noexcept
Returns the canonical invalid handle.
2D rigid body (Box2D) in pixel-space coordinates. Owned by a World; create shapes with newRectangleFi...
Fixture * newChainFixture(const std::vector< float > &vertices, bool loop=false, float friction=0.2f, float restitution=0.f)
Create an open chain or closed loop from packed local pixel-space XY vertices.
Fixture * newRectangleFixtureAt(float width, float height, float offsetX, float offsetY, float density=1.f, float friction=0.2f, float restitution=0.f)
Creates an offset rectangle fixture in pixel-space units.
Box2D world wrapper (2D physics) with pixel-space coordinates. Handles stepping, gravity,...
PhysicsWorldHandle runtimeHandle() const noexcept
Process-local identity used by PhysicsLink; invalid after destruction.
Sparse, chunked, deterministic 2D falling-material world.
eve::Result< PixelFragmentRasterReceipt > rasterizeFragment(const PixelFragment &fragment, int originX, int originY)
Transactionally rasterize a detached fragment at a new world-space origin.
bool isSolidMaterial(MaterialId material) const noexcept
Query whether a validated material id participates in structural solid collision.
PixelCell getCell(int x, int y) const noexcept
Return the cell at world coordinates; absent chunks read as air.
Owning detached bitmap plus a non-owning, stale-safe dynamic body link.
Incremental static collision projection of authoritative PixelWorld chunks.
~PixelTerrainCollisionCache()
Pixel terrain collision cache.
std::size_t bodyCount() const noexcept
Number of chunks currently owning a non-empty static collision body.
PixelTerrainCollisionCache()
Pixel terrain collision cache.
eve::Result< void > clearPhysics(eve::physics::World &physicsWorld)
Explicitly destroy every resolvable projected body and reset revision tracking.
std::uint64_t sourceRevision() const noexcept
Last authoritative revision fully represented by this cache.
constexpr HexDirection next(HexDirection d) noexcept
The next direction clockwise (NW wraps to NE).
eve::Result< PixelTerrainContact > sweepTerrainCircle(const eve::pixelworld::PixelWorld &pixelWorld, float startX, float startY, float endX, float endY, float radius, std::uint32_t maximumCells)
Sweep a circular character shape continuously through authoritative solid material cells.
eve::Result< std::vector< TerrainCollisionContour > > extractTerrainContours(const eve::pixelworld::PixelWorld &pixelWorld, int chunkX, int chunkY, std::uint32_t maximumVertices)
Extract simplified binary Marching-Squares-equivalent boundary chains for one Chunk.
eve::Result< PixelTerrainContact > probeTerrainCircle(const eve::pixelworld::PixelWorld &pixelWorld, float centerX, float centerY, float radius, std::uint32_t maximumCells)
Probe a circular character contact directly against authoritative solid material cells.
constexpr int kPixelChunkSize
Build metadata (engine git commit, build time, third-party version).
Generation-qualified relationship between one world and one body.
static eve::Result< PhysicsLink > fromBody(const Body &body)
Creates a link from a live 2D body.
Owning material bitmap detached atomically from a PixelWorld.
std::vector< PixelCell > cells
Runtime identity of the world epoch from which a fragment was detached.
Policy for dynamic fragment fixture creation and terrain settlement.
std::uint32_t maximumFixtures
One deterministic axis-aligned fixture produced from a fragment bitmap.
std::weak_ptr< const void > physicsLifetime
eve::pixelworld::PixelWorldLink pixelWorld
std::map< Coord, eve::physics::PhysicsLink > bodies
std::pair< int, int > Coord
One simplified local-space boundary contour generated for a terrain Chunk.
Counters for one transactional dirty-Chunk terrain collision sync.
std::uint32_t bodiesRemoved
std::uint64_t sourceRevision
std::uint32_t chunksRebuilt
std::uint32_t fixturesCreated