Skip to content

Support Tiled 1.12 oblique map orientation#1328

Merged
obiot merged 1 commit intomasterfrom
feature/oblique-orientation
Mar 28, 2026
Merged

Support Tiled 1.12 oblique map orientation#1328
obiot merged 1 commit intomasterfrom
feature/oblique-orientation

Conversation

@obiot
Copy link
Copy Markdown
Member

@obiot obiot commented Mar 28, 2026

Summary

  • New TMXObliqueRenderer extending TMXOrthogonalRenderer with 2D shear transform
  • Parse skewx/skewy attributes on <map> element (horizontal/vertical pixel offset per row/column)
  • Register "oblique" orientation in renderer autodetect
  • Coordinate transforms with shear: tileToPixelCoords/pixelToTileCoords with inverse shear matrix
  • getBounds computes parallelogram AABB
  • drawTile applies skew offsets, drawTileLayer uses inverse-shear culling for visible tile range
  • Add oblique example map with gravel/hole tiles to tiled map loader example

This completes all Tiled 1.12 features in issue #1254.

Test plan

  • All 1848 tests pass (19 new oblique renderer tests)
  • Oblique example map renders correctly (visually verified, matches Tiled rendering)
  • Tests cover: skew storage, shear computation, coordinate transforms (positive/negative/both/zero skew), inverse transforms, getBounds with skew, canRender accept/reject, drawTile skew offset, zero-skew matches orthogonal, vector reuse

🤖 Generated with Claude Code

- New TMXObliqueRenderer extending TMXOrthogonalRenderer with 2D shear
- Parse skewx/skewy attributes on map element
- Register "oblique" orientation in autodetect
- Coordinate transforms: tileToPixelCoords/pixelToTileCoords with shear
- getBounds accounts for parallelogram shape
- drawTile/drawTileLayer with skew offsets and inverse-shear culling
- Add oblique example map with gravel/hole tiles to tiled map loader
- Bump supported version to include Tiled 1.12 oblique maps

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings March 28, 2026 02:41
@obiot obiot merged commit 2c8ce96 into master Mar 28, 2026
10 checks passed
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds support for Tiled 1.12 “oblique” map orientation to melonJS tilemap rendering, including skew-based coordinate transforms, renderer autodetection, tests, and an updated tiledMapLoader example.

Changes:

  • Introduces TMXObliqueRenderer (shear/skew-based renderer extending the orthogonal renderer) and registers it in TMX renderer autodetect.
  • Extends TMXTileMap to store skewx/skewy map attributes (defaulting to 0) for oblique maps.
  • Adds renderer unit tests plus an oblique example map and assets to the tiledMapLoader example.

Reviewed changes

Copilot reviewed 8 out of 18 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
packages/melonjs/src/level/tiled/renderer/TMXObliqueRenderer.js New oblique renderer implementing skew/shear transforms, bounds computation, and culling/draw logic.
packages/melonjs/src/level/tiled/renderer/autodetect.js Registers "oblique" orientation in renderer factory.
packages/melonjs/src/level/tiled/TMXTileMap.js Stores skewx/skewy on the map object; updates orientation docstring.
packages/melonjs/tests/tmxrenderer.spec.js Adds unit tests for oblique renderer transforms, bounds, canRender, and drawTile offsets.
packages/melonjs/CHANGELOG.md Documents oblique map support as a new TMX feature.
packages/examples/src/examples/tiledMapLoader/resources.ts Adds oblique map and required images to the tiledMapLoader example resource list and level selector.
packages/examples/src/examples/tiledMapLoader/assets/map/oblique.tmx New example TMX map using orientation="oblique" and skewx.
packages/examples/src/examples/tiledMapLoader/assets/map/oblique/gravel.png Example tileset image asset.
packages/examples/src/examples/tiledMapLoader/assets/map/oblique/hole.png Example tileset image asset.
packages/examples/src/examples/tiledMapLoader/assets/map/oblique/wall.png Additional example image asset.
packages/examples/src/examples/tiledMapLoader/assets/map/oblique/wall_a.png Additional example image asset.
packages/examples/src/examples/tiledMapLoader/assets/map/oblique/wall_v.png Additional example image asset.
packages/examples/public/assets/tiledMapLoader/map/oblique.tmx Public copy of the oblique example TMX map.
packages/examples/public/assets/tiledMapLoader/map/oblique/gravel.png Public copy of example tileset image asset.
packages/examples/public/assets/tiledMapLoader/map/oblique/hole.png Public copy of example tileset image asset.
packages/examples/public/assets/tiledMapLoader/map/oblique/wall.png Public copy of additional example image asset.
packages/examples/public/assets/tiledMapLoader/map/oblique/wall_a.png Public copy of additional example image asset.
packages/examples/public/assets/tiledMapLoader/map/oblique/wall_v.png Public copy of additional example image asset.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +94 to +96
// inverse shear: [1, -shearX; -shearY, 1] / det
const px = (x - this.shearX * y) / this._det;
const py = (y - this.shearY * x) / this._det;
Copy link

Copilot AI Mar 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pixelToTileCoords (and drawTileLayer culling) divide by this._det = 1 - shearX * shearY without guarding against 0/near-0. For skew values where shearX * shearY is 1 (or numerically close), this will produce Infinity/NaN tile coordinates and can cascade into invalid culling ranges (potentially very large loops) or broken picking. Please add an explicit non-invertible-matrix guard (e.g., clamp det away from zero with an epsilon and/or provide a safe fallback behavior with a warning/error) before doing the inverse transform.

Suggested change
// inverse shear: [1, -shearX; -shearY, 1] / det
const px = (x - this.shearX * y) / this._det;
const py = (y - this.shearY * x) / this._det;
// compute a safe determinant to avoid division by zero / near-zero
let safeDet = this._det;
if (!Number.isFinite(safeDet)) {
// fallback to a neutral determinant if the stored value is invalid
safeDet = 1;
} else {
const EPS = 1e-8;
if (Math.abs(safeDet) < EPS) {
const originalDet = safeDet;
safeDet = safeDet >= 0 ? EPS : -EPS;
if (typeof console !== "undefined" && typeof console.warn === "function") {
console.warn(
"TMXObliqueRenderer: shear matrix is nearly singular in pixelToTileCoords; clamping determinant from",
originalDet,
"to",
safeDet
);
}
}
}
// inverse shear: [1, -shearX; -shearY, 1] / det
const px = (x - this.shearX * y) / safeDet;
const py = (y - this.shearY * x) / safeDet;

Copilot uses AI. Check for mistakes.
Comment on lines +75 to +82
// corners: (0,0), (w, skewY*cols), (skewX*rows, h), (w+skewX*rows, h+skewY*cols)
const sx = this.skewX * rows;
const sy = this.skewY * cols;

const minX = Math.min(0, sx, sy * this.shearX, w + sx);
const minY = Math.min(0, sy, sx * this.shearY, h + sy);
const maxX = Math.max(0, w, w + sx, sx);
const maxY = Math.max(0, h, h + sy, sy);
Copy link

Copilot AI Mar 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

getBounds() computes minX/minY using terms like sy * this.shearX and sx * this.shearY, which are not coordinates of any of the sheared rectangle’s corners. With mixed-sign skews this can significantly overestimate the AABB (e.g., skewX>0 and skewY<0), inflating the map bounds and affecting camera/world sizing. Consider computing the AABB strictly from the 4 transformed corners of the base rectangle: (0,0), (w, sy), (sx, h), (w+sx, h+sy).

Suggested change
// corners: (0,0), (w, skewY*cols), (skewX*rows, h), (w+skewX*rows, h+skewY*cols)
const sx = this.skewX * rows;
const sy = this.skewY * cols;
const minX = Math.min(0, sx, sy * this.shearX, w + sx);
const minY = Math.min(0, sy, sx * this.shearY, h + sy);
const maxX = Math.max(0, w, w + sx, sx);
const maxY = Math.max(0, h, h + sy, sy);
// corners (in pixels): (0,0), (w, sy), (sx, h), (w+sx, h+sy)
const sx = this.skewX * rows;
const sy = this.skewY * cols;
const minX = Math.min(0, w, sx, w + sx);
const maxX = Math.max(0, w, sx, w + sx);
const minY = Math.min(0, sy, h, h + sy);
const maxY = Math.max(0, sy, h, h + sy);

Copilot uses AI. Check for mistakes.
Comment on lines 181 to 186
/**
* the map orientation type. melonJS supports “orthogonal”, “isometric”, “staggered” and “hexagonal”.
* the map orientation type. melonJS supports “orthogonal”, “isometric”, “staggered”, “hexagonal” and “oblique”.
* @type {string}
* @default "orthogonal"
* @default orthogonal
*/
this.orientation = data.orientation;
Copy link

Copilot AI Mar 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The JSDoc @default value uses curly quotes (“orthogonal”) instead of normal ASCII quotes ("orthogonal"). This makes the generated documentation inconsistent and can be awkward to search/copy. Please switch back to standard quotes for the default value.

Copilot uses AI. Check for mistakes.
@obiot obiot deleted the feature/oblique-orientation branch April 11, 2026 12:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants