Support Tiled 1.12 oblique map orientation#1328
Conversation
- 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>
There was a problem hiding this comment.
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
TMXTileMapto storeskewx/skewymap attributes (defaulting to0) 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.
| // inverse shear: [1, -shearX; -shearY, 1] / det | ||
| const px = (x - this.shearX * y) / this._det; | ||
| const py = (y - this.shearY * x) / this._det; |
There was a problem hiding this comment.
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.
| // 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; |
| // 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); |
There was a problem hiding this comment.
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).
| // 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); |
| /** | ||
| * 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; |
There was a problem hiding this comment.
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.
Summary
TMXObliqueRendererextendingTMXOrthogonalRendererwith 2D shear transformskewx/skewyattributes on<map>element (horizontal/vertical pixel offset per row/column)"oblique"orientation in renderer autodetecttileToPixelCoords/pixelToTileCoordswith inverse shear matrixgetBoundscomputes parallelogram AABBdrawTileapplies skew offsets,drawTileLayeruses inverse-shear culling for visible tile rangeThis completes all Tiled 1.12 features in issue #1254.
Test plan
🤖 Generated with Claude Code