From 41a6b2aba887ffb1e15e092c2df70eeded84ca9b Mon Sep 17 00:00:00 2001 From: Matthew Cotton Date: Sun, 20 Sep 2026 16:07:17 +1000 Subject: [PATCH] fix(tilemap): anchor tile objects at their bottom-left corner Tiled anchors a tile object at its bottom-left corner, where a plain rectangle object is anchored top-left. The renderer passed the object position straight through as the top-left, so every tile object drew a full height below where the editor showed it, and the taller the object the more obvious the gap. Shift the draw position up by the drawn height, which follows the object's size rather than the tile's natural height so resized objects move by what the editor shows. --- modules/tilemap/src/tile_map_renderer.cpp | 8 +++++- modules/tilemap/tests/src/test_renderer.cpp | 30 ++++++++++++++++++++- 2 files changed, 36 insertions(+), 2 deletions(-) diff --git a/modules/tilemap/src/tile_map_renderer.cpp b/modules/tilemap/src/tile_map_renderer.cpp index 6f210deb..18ecd922 100644 --- a/modules/tilemap/src/tile_map_renderer.cpp +++ b/modules/tilemap/src/tile_map_renderer.cpp @@ -168,7 +168,13 @@ namespace moth::tilemap { scale.y = object.size.y / static_cast(sourceRect.h()); } - drawTileImage(*tileset, resolvedId, object.tile, object.position + offset, object.rotation, scale); + // Tiled anchors a tile object at its bottom-left corner, where a plain + // rectangle object is anchored top-left. Shift up by the drawn height so + // the tile lands where the editor shows it. + FloatVec2 topLeft = object.position + offset; + topLeft.y -= static_cast(sourceRect.h()) * scale.y; + + drawTileImage(*tileset, resolvedId, object.tile, topLeft, object.rotation, scale); } }; diff --git a/modules/tilemap/tests/src/test_renderer.cpp b/modules/tilemap/tests/src/test_renderer.cpp index 09404ff3..22989930 100644 --- a/modules/tilemap/tests/src/test_renderer.cpp +++ b/modules/tilemap/tests/src/test_renderer.cpp @@ -43,6 +43,7 @@ namespace { FloatVec2 position; IntRect sourceRect; FloatVec2 pivot; + FloatVec2 scale; float rotation = 0.0f; bool flipX; bool flipY; @@ -72,6 +73,7 @@ namespace { call.position = transform.position; call.sourceRect = image.GetSourceRect(); call.pivot = pivot; + call.scale = transform.scale; call.rotation = transform.rotation; call.flipX = flipX; call.flipY = flipY; @@ -361,10 +363,36 @@ TEST_CASE("Renderer: draws tile objects from object layers, skipping shapes", "[ REQUIRE(graphics.drawCalls.size() == 2); auto const& objectCall = graphics.drawCalls.back(); REQUIRE(objectCall.position.x == Catch::Approx(8.0f)); - REQUIRE(objectCall.position.y == Catch::Approx(8.0f)); + // A tile object is anchored at its bottom-left corner, so a 16px tile placed + // at y = 8 draws from y = -8. + REQUIRE(objectCall.position.y == Catch::Approx(-8.0f)); REQUIRE(objectCall.sourceRect == MakeRect(0, 0, 16, 16)); } +TEST_CASE("Renderer: a resized tile object is anchored at its scaled bottom-left", "[tilemap][renderer]") { + TileMap map = MakeMap(4, 4); + + ObjectLayer objectLayer; + MapObject tileObject; + tileObject.tile = TileId::FromGid(1); + tileObject.position = { 8.0f, 48.0f }; + tileObject.size = { 32.0f, 32.0f }; // a 16px tile drawn at double size + objectLayer.objects.push_back(tileObject); + map.objectLayers.push_back(objectLayer); + + MockGraphics graphics; + std::vector tilesetImages{ Image(std::make_shared()) }; + + DrawTileMap(graphics, map, tilesetImages, MakeRect(0.0f, 0.0f, 64.0f, 64.0f)); + + REQUIRE(graphics.drawCalls.size() == 1); + auto const& objectCall = graphics.drawCalls.back(); + REQUIRE(objectCall.position.x == Catch::Approx(8.0f)); + // The anchor shift follows the drawn height, not the tile's natural height. + REQUIRE(objectCall.position.y == Catch::Approx(16.0f)); + REQUIRE(objectCall.scale.y == Catch::Approx(2.0f)); +} + TEST_CASE("Renderer: applies layer parallax offset", "[tilemap][renderer]") { TileMap map = MakeMap(1, 1); AddLayer(map, { 1 });