From 9ee10650478385e06897fe35a78ede9f58e87f56 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thorbj=C3=B8rn=20Lindeijer?= Date: Thu, 12 Feb 2026 07:50:08 +0100 Subject: [PATCH 1/5] docs: Grammar (#4212) --- docs/manual/using-commands.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/manual/using-commands.rst b/docs/manual/using-commands.rst index 0623c880cb..4903849f3c 100644 --- a/docs/manual/using-commands.rst +++ b/docs/manual/using-commands.rst @@ -4,9 +4,9 @@ Using Commands The Command Button allows you to create and run shell commands (other programs) from Tiled. -You may setup as many commands as you like. This is useful if you edit +You may set up as many commands as you like. This is useful if you edit maps for multiple games and you want to set up a command for each game. -Or you could setup multiple commands for the same game that load +Or you could set up multiple commands for the same game that load different checkpoints or configurations. The Command Button From 3f41f4a9e2062d976cde7e291e2047b66b886401 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 12 Feb 2026 07:56:45 +0100 Subject: [PATCH 2/5] GitHub Actions: Bump some actions to Node.js 24 (#4304) * Bump actions/upload-artifact from 5 to 6 * Bump actions/download-artifact from 6 to 7 --- .github/workflows/docs.yml | 2 +- .github/workflows/macos-latest.yml | 2 +- .github/workflows/packages.yml | 12 ++++++------ 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 03558dd446..59a2c257b1 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -17,7 +17,7 @@ jobs: docs-folder: "docs/" - name: Upload documentation - uses: actions/upload-artifact@v5 + uses: actions/upload-artifact@v6 with: name: Tiled-Manual-HTML path: docs/_build/html/ diff --git a/.github/workflows/macos-latest.yml b/.github/workflows/macos-latest.yml index c196567a45..dcace2ae7d 100644 --- a/.github/workflows/macos-latest.yml +++ b/.github/workflows/macos-latest.yml @@ -55,7 +55,7 @@ jobs: popd - name: Upload artifact - uses: actions/upload-artifact@v5 + uses: actions/upload-artifact@v6 with: name: Tiled.app path: Tiled-*-macos.zip diff --git a/.github/workflows/packages.yml b/.github/workflows/packages.yml index ccd8a8fe3a..96330ebdbe 100644 --- a/.github/workflows/packages.yml +++ b/.github/workflows/packages.yml @@ -154,7 +154,7 @@ jobs: ./linuxdeploy-x86_64.AppImage --appdir AppDir --custom-apprun=dist/linux/AppRun --exclude-library "*libpython3*" --plugin qt --output appimage - name: Upload Tiled.AppImage - uses: actions/upload-artifact@v5 + uses: actions/upload-artifact@v6 with: name: Tiled-${{ needs.version.outputs.version }}_Linux_x86_64.AppImage path: Tiled-${{ needs.version.outputs.version }}_Linux_x86_64.AppImage @@ -184,7 +184,7 @@ jobs: uses: snapcore/action-build@v1 - name: Upload snap artifact - uses: actions/upload-artifact@v5 + uses: actions/upload-artifact@v6 with: name: tiled_amd64.snap path: tiled_*_amd64.snap @@ -318,7 +318,7 @@ jobs: ditto -c -k --sequesterRsrc --keepParent install/Tiled.app Tiled-${{ needs.version.outputs.version }}_macOS-${{ matrix.version_suffix }}.zip - name: Upload Tiled.app - uses: actions/upload-artifact@v5 + uses: actions/upload-artifact@v6 with: name: Tiled-${{ needs.version.outputs.version }}_macOS-${{ matrix.version_suffix }}.app path: Tiled-${{ needs.version.outputs.version }}_macOS-${{ matrix.version_suffix }}.zip @@ -426,13 +426,13 @@ jobs: mv release/installer*/Tiled-*.msi ./Tiled-${{ needs.version.outputs.version }}_${{ matrix.filename_suffix }}.msi - name: Upload Tiled installer - uses: actions/upload-artifact@v5 + uses: actions/upload-artifact@v6 with: name: Tiled-${{ needs.version.outputs.version }}_${{ matrix.filename_suffix }}.msi path: Tiled-${{ needs.version.outputs.version }}_*.msi - name: Upload Tiled archive - uses: actions/upload-artifact@v5 + uses: actions/upload-artifact@v6 with: name: Tiled-${{ needs.version.outputs.version }}_${{ matrix.filename_suffix }}.zip path: release/install-root/* @@ -448,7 +448,7 @@ jobs: steps: - name: Download all artifacts - uses: actions/download-artifact@v6 + uses: actions/download-artifact@v7 - name: Create release id: create_release From 995456f334152284ddff7c6ebfd4cbb48e87041f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thorbj=C3=B8rn=20Lindeijer?= Date: Fri, 23 Jan 2026 14:50:41 +0100 Subject: [PATCH 3/5] Add Oblique map orientation Introduce the "oblique" orientation with per-axis skew (skewx/skewy) attributes. Updated DTD, XSD, JSON, TMX and Lua formats, added ObliqueRenderer implementation, and integrated the new orientation option and related properties in the UI and undo system. Skew X/Y properties are displayed as a single "Skew" property in the Properties view. Also fixed the map view to repaint when the 'Render Order' property is changed. Closes #2917 --- NEWS.md | 1 + docs/map.dtd | 4 +- docs/map.xsd | 3 + docs/reference/json-map-format.rst | 4 +- docs/reference/tmx-changelog.rst | 4 + docs/reference/tmx-map-format.rst | 7 +- src/libtiled/isometricrenderer.cpp | 2 +- src/libtiled/libtiled.qbs | 2 + src/libtiled/map.cpp | 4 + src/libtiled/map.h | 35 +- src/libtiled/mapreader.cpp | 4 + src/libtiled/maprenderer.cpp | 3 + src/libtiled/maptovariantconverter.cpp | 5 + src/libtiled/mapwriter.cpp | 5 + src/libtiled/obliquerenderer.cpp | 551 +++++++++++++++++++++++++ src/libtiled/obliquerenderer.h | 85 ++++ src/libtiled/varianttomapconverter.cpp | 2 + src/plugins/lua/luaplugin.cpp | 5 + src/tiled/changemapproperty.h | 15 + src/tiled/editablemap.cpp | 16 + src/tiled/editablemap.h | 19 +- src/tiled/mapitem.cpp | 7 +- src/tiled/mapscene.cpp | 3 + src/tiled/newmapdialog.cpp | 6 + src/tiled/propertieswidget.cpp | 21 +- src/tiled/undocommands.h | 1 + 26 files changed, 802 insertions(+), 12 deletions(-) create mode 100644 src/libtiled/obliquerenderer.cpp create mode 100644 src/libtiled/obliquerenderer.h diff --git a/NEWS.md b/NEWS.md index 2c588a27ee..8057430e32 100644 --- a/NEWS.md +++ b/NEWS.md @@ -3,6 +3,7 @@ * Rewritten Properties view to enable direct widget interaction (#4045) * Added support for lists in custom properties (#1493) * Added capsule object shape (by Jocelyn, #2153) +* Added Oblique map orientation, skewing X and/or Y axis (#2917) * Allow filtering tilesets by name in the tileset dock (with dogboydog, #4239) * Allow changing the values of number inputs using expressions (with dogboydog, #4234) * Added support for SVG 1.2 / CSS blending modes to layers (#3932) diff --git a/docs/map.dtd b/docs/map.dtd index c64372921d..25274591aa 100644 --- a/docs/map.dtd +++ b/docs/map.dtd @@ -17,12 +17,14 @@ xmlns:xsi CDATA #IMPLIED xsi:schemaLocation CDATA #IMPLIED version CDATA #REQUIRED - orientation (orthogonal | isometric | staggered | hexagonal | shifted) #REQUIRED + orientation (orthogonal | isometric | oblique | staggered | hexagonal | shifted) #REQUIRED renderorder (right-down | right-up | left-down | left-up) width CDATA #REQUIRED height CDATA #REQUIRED tilewidth CDATA #REQUIRED tileheight CDATA #REQUIRED + skewx CDATA #IMPLIED + skewy CDATA #IMPLIED > diff --git a/docs/map.xsd b/docs/map.xsd index 0d649d7f74..02741d933c 100644 --- a/docs/map.xsd +++ b/docs/map.xsd @@ -104,6 +104,7 @@ + @@ -362,6 +363,8 @@ + + diff --git a/docs/reference/json-map-format.rst b/docs/reference/json-map-format.rst index 3b316854f1..7651e94246 100644 --- a/docs/reference/json-map-format.rst +++ b/docs/reference/json-map-format.rst @@ -28,11 +28,13 @@ Map layers, array, "Array of :ref:`Layers `" nextlayerid, int, "Auto-increments for each layer" nextobjectid, int, "Auto-increments for each placed object" - orientation, string, "``orthogonal``, ``isometric``, ``staggered`` or ``hexagonal``" + orientation, string, "``orthogonal``, ``isometric``, ``oblique``, ``staggered`` or ``hexagonal``" parallaxoriginx, double, "X coordinate of the parallax origin in pixels (since 1.8, default: 0)" parallaxoriginy, double, "Y coordinate of the parallax origin in pixels (since 1.8, default: 0)" properties, array, "Array of :ref:`Properties `" renderorder, string, "``right-down`` (the default), ``right-up``, ``left-down`` or ``left-up`` (currently only supported for orthogonal maps)" + skewx, int, "X offset applied per tile row (oblique maps only)" + skewy, int, "Y offset applied per tile column (oblique maps only)" staggeraxis, string, "``x`` or ``y`` (staggered / hexagonal maps only)" staggerindex, string, "``odd`` or ``even`` (staggered / hexagonal maps only)" tiledversion, string, "The Tiled version used to save the file" diff --git a/docs/reference/tmx-changelog.rst b/docs/reference/tmx-changelog.rst index 3855063b1a..a05f370c76 100644 --- a/docs/reference/tmx-changelog.rst +++ b/docs/reference/tmx-changelog.rst @@ -9,6 +9,10 @@ Tiled 1.12 - Added ``mode`` attribute on :ref:`tmx-layer` to specific its blend mode. +- Added ``oblique`` to the supported values for the ``orientation`` attribute + on the :ref:`tmx-map` element, along with the ``skewx`` and ``skewy`` + attributes for configuring the per-row/column skew in pixels. + - Added capsule object shape. Same parameters as rectangular objects, but marked as capsule with a child element: diff --git a/docs/reference/tmx-map-format.rst b/docs/reference/tmx-map-format.rst index df50932cd0..96e1464210 100644 --- a/docs/reference/tmx-map-format.rst +++ b/docs/reference/tmx-map-format.rst @@ -48,7 +48,7 @@ in what changed between Tiled versions. 1.0.1). May be a date (for snapshot builds). (optional) - **class:** The class of this map (since 1.9, defaults to ""). - **orientation:** Map orientation. Tiled supports "orthogonal", - "isometric", "staggered" and "hexagonal" (since 0.11). + "isometric", "oblique", "staggered" and "hexagonal" (since 0.11). - **renderorder:** The order in which tiles on tile layers are rendered. Valid values are ``right-down`` (the default), ``right-up``, ``left-down`` and ``left-up``. In all cases, the map is drawn @@ -59,6 +59,8 @@ in what changed between Tiled versions. - **height:** The map height in tiles. - **tilewidth:** The width of a tile. - **tileheight:** The height of a tile. +- **skewx:** For oblique maps, the pixel offset per tile row. +- **skewy:** For oblique maps, the pixel offset per tile column. - **hexsidelength:** Only for hexagonal maps. Determines the width or height (depending on the staggered axis) of the tile's edge, in pixels. @@ -98,7 +100,8 @@ order in which these layers appear is the order in which the layers are rendered by Tiled. The ``staggered`` orientation refers to an isometric map using staggered -axes. +axes. The ``oblique`` orientation uses a skewed grid of parallelogram-shaped +tiles. The tilesets used by the map should always be listed before the layers. diff --git a/src/libtiled/isometricrenderer.cpp b/src/libtiled/isometricrenderer.cpp index 3f85f3feec..16cc1f10b9 100644 --- a/src/libtiled/isometricrenderer.cpp +++ b/src/libtiled/isometricrenderer.cpp @@ -30,9 +30,9 @@ #include "map.h" #include "mapobject.h" +#include "objectgroup.h" #include "tile.h" #include "tilelayer.h" -#include "objectgroup.h" #include diff --git a/src/libtiled/libtiled.qbs b/src/libtiled/libtiled.qbs index 06db522c02..cf7d9a0772 100644 --- a/src/libtiled/libtiled.qbs +++ b/src/libtiled/libtiled.qbs @@ -138,6 +138,8 @@ DynamicLibrary { "mapwriter.h", "minimaprenderer.cpp", "minimaprenderer.h", + "obliquerenderer.cpp", + "obliquerenderer.h", "object.cpp", "object.h", "objectgroup.cpp", diff --git a/src/libtiled/map.cpp b/src/libtiled/map.cpp index 45fc178d61..1726f4555b 100644 --- a/src/libtiled/map.cpp +++ b/src/libtiled/map.cpp @@ -578,6 +578,8 @@ QString Tiled::orientationToString(Map::Orientation orientation) return QStringLiteral("staggered"); case Map::Hexagonal: return QStringLiteral("hexagonal"); + case Map::Oblique: + return QStringLiteral("oblique"); } return QString(); } @@ -593,6 +595,8 @@ Map::Orientation Tiled::orientationFromString(const QString &string) orientation = Map::Staggered; } else if (string == QLatin1String("hexagonal")) { orientation = Map::Hexagonal; + } else if (string == QLatin1String("oblique")) { + orientation = Map::Oblique; } return orientation; } diff --git a/src/libtiled/map.h b/src/libtiled/map.h index 5e4ddd3833..f4f96c553b 100644 --- a/src/libtiled/map.h +++ b/src/libtiled/map.h @@ -83,6 +83,7 @@ class TILEDSHARED_EXPORT Map : public Object HexSideLengthProperty, StaggerAxisProperty, StaggerIndexProperty, + SkewProperty, ParallaxOriginProperty, OrientationProperty, RenderOrderProperty, @@ -97,14 +98,16 @@ class TILEDSHARED_EXPORT Map : public Object * Orthogonal map is using rectangular tiles that are aligned on a * straight grid. An Isometric map uses diamond shaped tiles that are * aligned on an isometric projected grid. A Hexagonal map uses hexagon - * shaped tiles that fit into each other by shifting every other row. + * shaped tiles that fit into each other by shifting every other row. An + * Oblique map uses a skewed grid of parallelogram-shaped tiles. */ enum Orientation { Unknown, Orthogonal, Isometric, Staggered, - Hexagonal + Hexagonal, + Oblique }; /** @@ -160,6 +163,8 @@ class TILEDSHARED_EXPORT Map : public Object int hexSideLength = 0; StaggerAxis staggerAxis = StaggerY; StaggerIndex staggerIndex = StaggerOdd; + int skewX = 0; + int skewY = 0; QPointF parallaxOrigin; QColor backgroundColor; }; @@ -226,6 +231,12 @@ class TILEDSHARED_EXPORT Map : public Object void setStaggerIndex(StaggerIndex staggerIndex); void invertStaggerIndex(); + int skewX() const; + void setSkewX(int skewX); + + int skewY() const; + void setSkewY(int skewY); + QPointF parallaxOrigin() const; void setParallaxOrigin(const QPointF ¶llaxOrigin); @@ -506,6 +517,26 @@ inline void Map::invertStaggerIndex() mParameters.staggerIndex = static_cast(!mParameters.staggerIndex); } +inline int Map::skewX() const +{ + return mParameters.skewX; +} + +inline void Map::setSkewX(int skewX) +{ + mParameters.skewX = skewX; +} + +inline int Map::skewY() const +{ + return mParameters.skewY; +} + +inline void Map::setSkewY(int skewY) +{ + mParameters.skewY = skewY; +} + inline QPointF Map::parallaxOrigin() const { return mParameters.parallaxOrigin; diff --git a/src/libtiled/mapreader.cpp b/src/libtiled/mapreader.cpp index ee58d5708a..2d0bad71d9 100644 --- a/src/libtiled/mapreader.cpp +++ b/src/libtiled/mapreader.cpp @@ -270,6 +270,10 @@ std::unique_ptr MapReaderPrivate::readMap() mapParameters.staggerIndex = staggerIndexFromString(staggerIndex); bool ok; + if (const int skewX = atts.value(QLatin1String("skewx")).toInt(&ok); ok) + mapParameters.skewX = skewX; + if (const int skewY = atts.value(QLatin1String("skewy")).toInt(&ok); ok) + mapParameters.skewY = skewY; if (const qreal parallaxOriginX = atts.value(QLatin1String("parallaxoriginx")).toDouble(&ok); ok) mapParameters.parallaxOrigin.setX(parallaxOriginX); if (const qreal parallaxOriginY = atts.value(QLatin1String("parallaxoriginy")).toDouble(&ok); ok) diff --git a/src/libtiled/maprenderer.cpp b/src/libtiled/maprenderer.cpp index 8c63eda834..49ecd404ec 100644 --- a/src/libtiled/maprenderer.cpp +++ b/src/libtiled/maprenderer.cpp @@ -32,6 +32,7 @@ #include "isometricrenderer.h" #include "map.h" #include "mapobject.h" +#include "obliquerenderer.h" #include "objectgroup.h" #include "orthogonalrenderer.h" #include "staggeredrenderer.h" @@ -350,6 +351,8 @@ std::unique_ptr MapRenderer::create(const Map *map) return std::make_unique(map); case Map::Hexagonal: return std::make_unique(map); + case Map::Oblique: + return std::make_unique(map); default: return std::make_unique(map); } diff --git a/src/libtiled/maptovariantconverter.cpp b/src/libtiled/maptovariantconverter.cpp index 603a6639a2..fd3a451ae8 100644 --- a/src/libtiled/maptovariantconverter.cpp +++ b/src/libtiled/maptovariantconverter.cpp @@ -100,6 +100,11 @@ QVariant MapToVariantConverter::toVariant(const Map &map, const QDir &mapDir) mapVariant[QStringLiteral("staggerindex")] = staggerIndexToString(map.staggerIndex()); } + if (map.skewX()) + mapVariant[QStringLiteral("skewx")] = map.skewX(); + if (map.skewY()) + mapVariant[QStringLiteral("skewy")] = map.skewY(); + if (!map.parallaxOrigin().isNull()) { mapVariant[QStringLiteral("parallaxoriginx")] = map.parallaxOrigin().x(); mapVariant[QStringLiteral("parallaxoriginy")] = map.parallaxOrigin().y(); diff --git a/src/libtiled/mapwriter.cpp b/src/libtiled/mapwriter.cpp index d90e00f85c..3d0e104a31 100644 --- a/src/libtiled/mapwriter.cpp +++ b/src/libtiled/mapwriter.cpp @@ -229,6 +229,11 @@ void MapWriterPrivate::writeMap(QXmlStreamWriter &w, const Map &map) staggerIndexToString(map.staggerIndex())); } + if (map.skewX()) + w.writeAttribute(QStringLiteral("skewx"), QString::number(map.skewX())); + if (map.skewY()) + w.writeAttribute(QStringLiteral("skewy"), QString::number(map.skewY())); + if (!map.parallaxOrigin().isNull()) { w.writeAttribute(QStringLiteral("parallaxoriginx"), QString::number(map.parallaxOrigin().x())); w.writeAttribute(QStringLiteral("parallaxoriginy"), QString::number(map.parallaxOrigin().y())); diff --git a/src/libtiled/obliquerenderer.cpp b/src/libtiled/obliquerenderer.cpp new file mode 100644 index 0000000000..3be5be36f6 --- /dev/null +++ b/src/libtiled/obliquerenderer.cpp @@ -0,0 +1,551 @@ +/* + * obliquerenderer.cpp + * Copyright 2025, Thorbjørn Lindeijer + * + * This file is part of libtiled. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE CONTRIBUTORS ``AS IS'' AND ANY EXPRESS OR + * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO + * EVENT SHALL THE CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; + * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR + * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include "obliquerenderer.h" + +#include "map.h" +#include "mapobject.h" +#include "objectgroup.h" +#include "tile.h" +#include "tilelayer.h" + +#include + +#include + +using namespace Tiled; + +QRect ObliqueRenderer::boundingRect(const QRect &rect) const +{ + const QPolygonF polygon = tileRectToScreenPolygon(rect); + return polygon.boundingRect().toAlignedRect(); +} + +QRectF ObliqueRenderer::boundingRect(const MapObject *object) const +{ + if (object->shape() == MapObject::Text) { + QRectF bounds { pixelToScreenCoords(object->position()), object->size() }; + bounds.translate(-alignmentOffset(bounds, object->alignment(map()))); + return bounds; + } + + if (object->shape() == MapObject::Point) { + const qreal extraSpace = qMax(objectLineWidth() / 2, qreal(1)); + return shape(object).boundingRect() + .adjusted(-extraSpace, + -extraSpace, + extraSpace, + extraSpace); + } + + QRectF bounds = object->bounds(); + bounds.translate(-alignmentOffset(bounds, object->alignment(map()))); + + if (!object->cell().isEmpty()) { + if (const Tile *tile = object->cell().tile()) { + QPointF tileOffset = tile->offset(); + const QSize tileSize = tile->size(); + if (!tileSize.isNull()) { + const QSizeF scale { + bounds.width() / tileSize.width(), + bounds.height() / tileSize.height() + }; + tileOffset.rx() *= scale.width(); + tileOffset.ry() *= scale.height(); + } + tileOffset = transform().map(tileOffset); + bounds.translate(tileOffset); + } + + QPainterPath path; + path.addRect(bounds); + return transform().map(path).boundingRect().adjusted(-1, -1, 1, 1); + } + + const qreal extraSpace = qMax(objectLineWidth(), qreal(1)); + return shape(object).boundingRect() + .adjusted(-extraSpace, + -extraSpace, + extraSpace + 1, + extraSpace + 1); +} + +QPainterPath ObliqueRenderer::shape(const MapObject *object) const +{ + QPainterPath path; + + QRectF bounds = object->bounds(); + bounds.translate(-alignmentOffset(bounds, object->alignment(map()))); + + switch (object->shape()) { + case MapObject::Rectangle: { + if (bounds.isNull()) { + path.addRect(object->x() - 10, object->y() - 10, 20, 20); + } else { + if (const Tile *tile = object->cell().tile()) { + QPointF tileOffset = tile->offset(); + const QSize tileSize = tile->size(); + if (!tileSize.isNull()) { + const QSizeF scale { + bounds.width() / tileSize.width(), + bounds.height() / tileSize.height() + }; + tileOffset.rx() *= scale.width(); + tileOffset.ry() *= scale.height(); + } + bounds.translate(tileOffset); + } + + path.addRect(bounds); + } + break; + } + case MapObject::Polygon: + case MapObject::Polyline: { + const QPointF &pos = object->position(); + QPolygonF polygon = object->polygon().translated(pos); + + if (object->shape() == MapObject::Polygon && !polygon.isEmpty()) + polygon.append(polygon.first()); + + path.addPolygon(polygon); + break; + } + case MapObject::Ellipse: { + if (bounds.isNull()) + path.addEllipse(bounds.topLeft(), 10, 10); + else + path.addEllipse(bounds); + break; + } + case MapObject::Capsule: { + const qreal rad = std::min(std::abs(bounds.height()), + std::abs(bounds.width())) / 2; + if (rad == 0.) + path.addEllipse(bounds.topLeft(), 10, 10); + else + path.addRoundedRect(bounds, rad, rad, Qt::SizeMode::AbsoluteSize); + break; + } + case MapObject::Point: + return pointShape(object->position()); // don't transform shape of points + case MapObject::Text: + path.addRect(bounds); + break; + } + + return transform().map(path); +} + +QPainterPath ObliqueRenderer::interactionShape(const MapObject *object) const +{ + QPainterPath path; + + switch (object->shape()) { + case MapObject::Polyline: { + const QPointF &pos = object->position(); + const QPolygonF polygon = object->polygon().translated(pos); + for (int i = 1; i < polygon.size(); ++i) { + const QPointF start = pixelToScreenCoords(polygon[i - 1]); + const QPointF end = pixelToScreenCoords(polygon[i]); + path.addPolygon(lineToPolygon(start, end)); + } + path.setFillRule(Qt::WindingFill); + break; + } + case MapObject::Rectangle: + case MapObject::Polygon: + case MapObject::Ellipse: + case MapObject::Capsule: + case MapObject::Text: + path = shape(object); + break; + case MapObject::Point: + path = pointInteractionShape(object); + break; + } + + return path; +} + +void ObliqueRenderer::drawGrid(QPainter *painter, const QRectF &rect, + QColor gridColor, QSize gridMajor) const +{ + const int tileWidth = map()->tileWidth(); + const int tileHeight = map()->tileHeight(); + + if (tileWidth <= 0 || tileHeight <= 0) + return; + + const QRectF pixelRect = screenToPixelRect(rect); + int startX = qFloor(pixelRect.left() / tileWidth); + int startY = qFloor(pixelRect.top() / tileHeight); + int endX = qCeil(pixelRect.right() / tileWidth); + int endY = qCeil(pixelRect.bottom() / tileHeight); + + // Return immediately when there is nothing to draw + if (startX > endX || startY > endY) + return; + + if (!map()->infinite()) { + startX = qMax(0, startX); + startY = qMax(0, startY); + endX = qMin(endX, map()->width()); + endY = qMin(endY, map()->height()); + } + + if (startX > endX || startY > endY) + return; + + QPen gridPen, majorGridPen; + setupGridPens(painter->device(), gridColor, gridPen, majorGridPen, qMin(tileWidth, tileHeight), gridMajor); + + for (int y = startY; y <= endY; ++y) { + const QPointF start = tileToScreenCoords(startX, y); + const QPointF end = tileToScreenCoords(endX, y); + + painter->setPen(gridMajor.height() != 0 && y % gridMajor.height() == 0 ? majorGridPen : gridPen); + painter->drawLine(start, end); + } + + for (int x = startX; x <= endX; ++x) { + const QPointF start = tileToScreenCoords(x, startY); + const QPointF end = tileToScreenCoords(x, endY); + + painter->setPen(gridMajor.width() != 0 && x % gridMajor.width() == 0 ? majorGridPen : gridPen); + painter->drawLine(start, end); + } +} + +void ObliqueRenderer::drawTileLayer(const RenderTileCallback &renderTile, + const QRectF &exposed) const +{ + const int tileWidth = map()->tileWidth(); + const int tileHeight = map()->tileHeight(); + + if (tileWidth <= 0 || tileHeight <= 0) + return; + + const QRectF pixelRect = screenToPixelRect(exposed); + int startX = qFloor(pixelRect.left() / tileWidth); + int startY = qFloor(pixelRect.top() / tileHeight); + int endX = qCeil(pixelRect.right() / tileWidth); + int endY = qCeil(pixelRect.bottom() / tileHeight); + + if (!map()->infinite()) { + startX = qMax(0, startX); + startY = qMax(0, startY); + endX = qMin(endX, map()->width()); + endY = qMin(endY, map()->height()); + } + + int incX = 1; + int incY = 1; + switch (map()->renderOrder()) { + case Map::RightUp: + std::swap(startY, endY); + incY = -1; + break; + case Map::LeftDown: + std::swap(startX, endX); + incX = -1; + break; + case Map::LeftUp: + std::swap(startX, endX); + std::swap(startY, endY); + incX = -1; + incY = -1; + break; + case Map::RightDown: + break; + } + + if (incX > 0) { + if (startX > endX) + return; + } else if (startX < endX) { + std::swap(startX, endX); + } + + if (incY > 0) { + if (startY > endY) + return; + } else if (startY < endY) { + std::swap(startY, endY); + } + + endX += incX; + endY += incY; + + for (int y = startY; y != endY; y += incY) { + for (int x = startX; x != endX; x += incX) + renderTile(QPoint(x, y), tileToScreenCoords(x, y + 1)); + } +} + +void ObliqueRenderer::drawTileSelection(QPainter *painter, + const QRegion ®ion, + const QColor &color, + const QRectF &exposed) const +{ + QPainterPath path; + + for (const QRect &r : region) { + QPolygonF polygon = tileRectToScreenPolygon(r); + if (QRectF(polygon.boundingRect()).intersects(exposed)) + path.addPolygon(polygon); + } + + QColor penColor(color); + penColor.setAlpha(255); + + QPen pen(penColor); + pen.setCosmetic(true); + + painter->setPen(pen); + painter->setBrush(color); + painter->setRenderHint(QPainter::Antialiasing, true); + painter->drawPath(path.simplified()); +} + +void ObliqueRenderer::drawMapObject(QPainter *painter, + const MapObject *object, + const MapObjectColors &colors) const +{ + painter->save(); + + QPen pen(Qt::black); + pen.setCosmetic(true); + + const Cell &cell = object->cell(); + + if (!cell.isEmpty()) { + QRectF bounds { pixelToScreenCoords(object->position()), object->size() }; + bounds.translate(-alignmentOffset(bounds, object->alignment(map()))); + + CellRenderer(painter, this, object->objectGroup()->effectiveTintColor()) + .render(cell, bounds.topLeft(), bounds.size()); + + if (testFlag(ShowTileObjectOutlines)) { + if (const Tile *tile = object->cell().tile()) { + QPointF tileOffset = tile->offset(); + const QSize tileSize = tile->size(); + if (!tileSize.isNull()) { + const QSizeF scale { + bounds.width() / tileSize.width(), + bounds.height() / tileSize.height() + }; + tileOffset.rx() *= scale.width(); + tileOffset.ry() *= scale.height(); + } + tileOffset = transform().map(tileOffset); + bounds.translate(tileOffset); + } + + pen.setStyle(Qt::SolidLine); + painter->setRenderHint(QPainter::Antialiasing, false); + painter->setBrush(Qt::NoBrush); + painter->setPen(pen); + painter->drawRect(bounds); + pen.setStyle(Qt::DotLine); + pen.setColor(colors.main); + painter->setPen(pen); + painter->drawRect(bounds); + } + } else if (object->shape() == MapObject::Text) { + QRectF bounds { pixelToScreenCoords(object->position()), object->size() }; + bounds.translate(-alignmentOffset(bounds, object->alignment(map()))); + + const auto &textData = object->textData(); + painter->setFont(textData.font); + painter->setPen(textData.color); + painter->drawText(bounds, textData.text, textData.textOption()); + } else if (object->shape() == MapObject::Point) { + painter->translate(pixelToScreenCoords(object->position())); + painter->setRenderHint(QPainter::Antialiasing); + drawPointObject(painter, colors.main); + } else { + const qreal lineWidth = objectLineWidth(); + const qreal scale = painterScale(); + const QPointF shadowOffset(0, (lineWidth == 0 ? 1 : lineWidth) / scale); + + QBrush brush = colors.fill.isValid() ? QBrush(colors.fill) : QBrush(Qt::NoBrush); + + pen.setJoinStyle(Qt::RoundJoin); + pen.setCapStyle(Qt::RoundCap); + pen.setWidthF(lineWidth); + + QPen colorPen(pen); + colorPen.setColor(colors.main); + + painter->setRenderHint(QPainter::Antialiasing); + + QRectF bounds = object->bounds(); + bounds.translate(-alignmentOffset(bounds, object->alignment(map()))); + + switch (object->shape()) { + case MapObject::Rectangle: + case MapObject::Ellipse: + case MapObject::Capsule: { + QPainterPath path; + if (object->shape() == MapObject::Rectangle) { + if (bounds.isNull()) + bounds = QRectF(QPointF(-10, -10), QSizeF(20, 20)); + path.addRect(bounds); + } else if (object->shape() == MapObject::Ellipse) { + if (bounds.isNull()) + bounds = QRectF(QPointF(-10, -10), QSizeF(20, 20)); + path.addEllipse(bounds); + } else { + if (bounds.isNull()) + bounds = QRectF(QPointF(-10, -10), QSizeF(20, 20)); + const qreal rad = std::min(std::abs(bounds.height()), + std::abs(bounds.width())) / 2; + path.addRoundedRect(bounds, rad, rad); + } + + path = transform().map(path); + + painter->setPen(pen); + painter->setBrush(Qt::NoBrush); + painter->drawPath(path.translated(shadowOffset)); + + painter->setPen(colorPen); + painter->setBrush(brush); + painter->drawPath(path); + break; + } + case MapObject::Polygon: + case MapObject::Polyline: { + const QPointF &pos = object->position(); + const QPolygonF polygon = object->polygon().translated(pos); + QPolygonF screenPolygon = pixelToScreenCoords(polygon); + const QPointF pointPos = screenPolygon.isEmpty() ? QPointF() + : screenPolygon.first(); + + QPen thickPen(pen); + QPen thickColorPen(colorPen); + thickPen.setWidthF(thickPen.widthF() * 4); + thickColorPen.setWidthF(thickColorPen.widthF() * 4); + + painter->setPen(pen); + if (object->shape() == MapObject::Polygon) + painter->drawPolygon(screenPolygon.translated(shadowOffset)); + else + painter->drawPolyline(screenPolygon.translated(shadowOffset)); + painter->setPen(thickPen); + painter->drawPoint(pointPos + shadowOffset); + + painter->setPen(colorPen); + painter->setBrush(brush); + + if (object->shape() == MapObject::Polygon) + painter->drawPolygon(screenPolygon); + else + painter->drawPolyline(screenPolygon); + painter->setPen(thickColorPen); + painter->drawPoint(pointPos); + break; + } + case MapObject::Text: + case MapObject::Point: + break; + } + } + + painter->restore(); +} + + +QPointF ObliqueRenderer::screenToTileCoords(qreal x, qreal y) const +{ + const QPointF pixel = screenToPixelCoords(x, y); + return pixelToTileCoords(pixel); +} + +QPointF ObliqueRenderer::tileToScreenCoords(qreal x, qreal y) const +{ + return pixelToScreenCoords(tileToPixelCoords(x, y)); +} + +QPointF ObliqueRenderer::screenToPixelCoords(qreal x, qreal y) const +{ + bool ok = false; + const QTransform inv = transform().inverted(&ok); + if (!ok) + return QPointF(x, y); + return inv.map(QPointF(x, y)); +} + +QPointF ObliqueRenderer::pixelToScreenCoords(qreal x, qreal y) const +{ + return transform().map(QPointF(x, y)); +} + +QTransform ObliqueRenderer::transform() const +{ + const qreal tileWidth = map()->tileWidth(); + const qreal tileHeight = map()->tileHeight(); + if (tileWidth == 0 || tileHeight == 0) + return QTransform(); + + const qreal shearX = map()->skewX() / tileHeight; + const qreal shearY = map()->skewY() / tileWidth; + QTransform transform; + transform.shear(shearX, shearY); + return transform; +} + +QRectF ObliqueRenderer::screenToPixelRect(const QRectF &rect) const +{ + bool ok = false; + const QTransform inv = transform().inverted(&ok); + if (!ok) + return rect; + + QPolygonF polygon; + polygon << inv.map(rect.topLeft()) + << inv.map(rect.topRight()) + << inv.map(rect.bottomRight()) + << inv.map(rect.bottomLeft()); + return polygon.boundingRect(); +} + +QPolygonF ObliqueRenderer::tileRectToScreenPolygon(const QRect &rect) const +{ + // Since tileToScreenCoords returns the top-left of the tile, we need to + // extend the bottom-right of the rect. + const auto r = rect.adjusted(0, 0, 1, 1); + + QPolygonF polygon; + polygon << tileToScreenCoords(r.topLeft()) + << tileToScreenCoords(r.topRight()) + << tileToScreenCoords(r.bottomRight()) + << tileToScreenCoords(r.bottomLeft()); + return polygon; +} diff --git a/src/libtiled/obliquerenderer.h b/src/libtiled/obliquerenderer.h new file mode 100644 index 0000000000..88e6799439 --- /dev/null +++ b/src/libtiled/obliquerenderer.h @@ -0,0 +1,85 @@ +/* + * obliquerenderer.h + * Copyright 2025, Thorbjørn Lindeijer + * + * This file is part of libtiled. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE CONTRIBUTORS ``AS IS'' AND ANY EXPRESS OR + * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO + * EVENT SHALL THE CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; + * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR + * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#pragma once + +#include "orthogonalrenderer.h" + +namespace Tiled { + +/** + * An oblique map renderer. + * + * Oblique maps use a skewed grid where the X and/or Y axis is projected diagonally. + */ +class TILEDSHARED_EXPORT ObliqueRenderer final : public OrthogonalRenderer +{ +public: + ObliqueRenderer(const Map *map) : OrthogonalRenderer(map) {} + + QRect boundingRect(const QRect &rect) const override; + + QRectF boundingRect(const MapObject *object) const override; + QPainterPath shape(const MapObject *object) const override; + QPainterPath interactionShape(const MapObject *object) const override; + + void drawGrid(QPainter *painter, const QRectF &rect, + QColor gridColor, QSize gridMajor) const override; + + using MapRenderer::drawTileLayer; + void drawTileLayer(const RenderTileCallback &renderTile, + const QRectF &exposed) const override; + + void drawTileSelection(QPainter *painter, + const QRegion ®ion, + const QColor &color, + const QRectF &exposed) const override; + + void drawMapObject(QPainter *painter, + const MapObject *object, + const MapObjectColors &colors) const override; + + using MapRenderer::screenToTileCoords; + QPointF screenToTileCoords(qreal x, qreal y) const override; + + using MapRenderer::tileToScreenCoords; + QPointF tileToScreenCoords(qreal x, qreal y) const override; + + using MapRenderer::screenToPixelCoords; + QPointF screenToPixelCoords(qreal x, qreal y) const override; + + using MapRenderer::pixelToScreenCoords; + QPointF pixelToScreenCoords(qreal x, qreal y) const override; + +private: + QTransform transform() const; + QRectF screenToPixelRect(const QRectF &rect) const; + QPolygonF tileRectToScreenPolygon(const QRect &rect) const; +}; + +} // namespace Tiled diff --git a/src/libtiled/varianttomapconverter.cpp b/src/libtiled/varianttomapconverter.cpp index 4e83959751..afede7ab43 100644 --- a/src/libtiled/varianttomapconverter.cpp +++ b/src/libtiled/varianttomapconverter.cpp @@ -80,6 +80,8 @@ std::unique_ptr VariantToMapConverter::toMap(const QVariant &variant, mapParameters.hexSideLength = variantMap[QStringLiteral("hexsidelength")].toInt(); mapParameters.staggerAxis = staggerAxisFromString(staggerAxis); mapParameters.staggerIndex = staggerIndexFromString(staggerIndex); + mapParameters.skewX = variantMap[QStringLiteral("skewx")].toInt(); + mapParameters.skewY = variantMap[QStringLiteral("skewy")].toInt(); bool ok; const qreal parallaxOriginX = variantMap[QStringLiteral("parallaxoriginx")].toDouble(&ok); diff --git a/src/plugins/lua/luaplugin.cpp b/src/plugins/lua/luaplugin.cpp index c088302b66..101306eb11 100644 --- a/src/plugins/lua/luaplugin.cpp +++ b/src/plugins/lua/luaplugin.cpp @@ -245,6 +245,11 @@ void LuaWriter::writeMap(const Map *map) mWriter.writeEndTable(); } + if (map->skewX()) + mWriter.writeKeyAndValue("skewx", map->skewX()); + if (map->skewY()) + mWriter.writeKeyAndValue("skewy", map->skewY()); + const QColor &backgroundColor = map->backgroundColor(); if (backgroundColor.isValid()) writeColor("backgroundcolor", backgroundColor); diff --git a/src/tiled/changemapproperty.h b/src/tiled/changemapproperty.h index 3ea19d0237..385ba6208c 100644 --- a/src/tiled/changemapproperty.h +++ b/src/tiled/changemapproperty.h @@ -28,6 +28,7 @@ #include #include +#include namespace Tiled { @@ -98,6 +99,19 @@ struct MapParallaxOrigin } }; +struct MapSkew +{ + using Type = QPoint; + + static void set(Map *map, Type value) { map->setSkewX(value.x()); map->setSkewY(value.y()); } + static Type get(const Map *map) { return { map->skewX(), map->skewY() }; } + static int undoId() { return Cmd_ChangeMapSkew; } + static Map::Property property() { return Map::SkewProperty; } + static QString undoName() { + return QCoreApplication::translate("Undo Commands", "Change Skew"); + } +}; + struct MapOrientation { using Type = Map::Orientation; @@ -221,6 +235,7 @@ using ChangeMapChunkSize = ChangeMapProperty; using ChangeMapStaggerAxis = ChangeMapProperty; using ChangeMapStaggerIndex = ChangeMapProperty; using ChangeMapParallaxOrigin = ChangeMapProperty; +using ChangeMapSkew = ChangeMapProperty; using ChangeMapOrientation = ChangeMapProperty; using ChangeMapRenderOrder = ChangeMapProperty; using ChangeMapLayerDataFormat = ChangeMapProperty; diff --git a/src/tiled/editablemap.cpp b/src/tiled/editablemap.cpp index 170ea6cbc5..5f43da8f37 100644 --- a/src/tiled/editablemap.cpp +++ b/src/tiled/editablemap.cpp @@ -557,6 +557,22 @@ void EditableMap::setStaggerIndex(StaggerIndex value) map()->setStaggerIndex(static_cast(value)); } +void EditableMap::setSkewX(int value) +{ + if (auto doc = mapDocument()) + push(new ChangeMapSkew(doc, QPoint(value, map()->skewY()))); + else if (!checkReadOnly()) + map()->setSkewX(value); +} + +void EditableMap::setSkewY(int value) +{ + if (auto doc = mapDocument()) + push(new ChangeMapSkew(doc, QPoint(map()->skewX(), value))); + else if (!checkReadOnly()) + map()->setSkewY(value); +} + void EditableMap::setParallaxOrigin(const QPointF ¶llaxOrigin) { if (auto doc = mapDocument()) diff --git a/src/tiled/editablemap.h b/src/tiled/editablemap.h index 50dce3406b..a69c1e6fb7 100644 --- a/src/tiled/editablemap.h +++ b/src/tiled/editablemap.h @@ -49,6 +49,8 @@ class EditableMap final : public EditableAsset Q_PROPERTY(int hexSideLength READ hexSideLength WRITE setHexSideLength) Q_PROPERTY(StaggerAxis staggerAxis READ staggerAxis WRITE setStaggerAxis) Q_PROPERTY(StaggerIndex staggerIndex READ staggerIndex WRITE setStaggerIndex) + Q_PROPERTY(int skewX READ skewX WRITE setSkewX) + Q_PROPERTY(int skewY READ skewY WRITE setSkewY) Q_PROPERTY(QPointF parallaxOrigin READ parallaxOrigin WRITE setParallaxOrigin) Q_PROPERTY(Orientation orientation READ orientation WRITE setOrientation) Q_PROPERTY(RenderOrder renderOrder READ renderOrder WRITE setRenderOrder) @@ -71,7 +73,8 @@ class EditableMap final : public EditableAsset Orthogonal, Isometric, Staggered, - Hexagonal + Hexagonal, + Oblique }; Q_ENUM(Orientation) @@ -127,6 +130,8 @@ class EditableMap final : public EditableAsset int hexSideLength() const; StaggerAxis staggerAxis() const; StaggerIndex staggerIndex() const; + int skewX() const; + int skewY() const; QPointF parallaxOrigin() const; Orientation orientation() const; RenderOrder renderOrder() const; @@ -192,6 +197,8 @@ class EditableMap final : public EditableAsset void setHexSideLength(int value); void setStaggerAxis(StaggerAxis value); void setStaggerIndex(StaggerIndex value); + void setSkewX(int value); + void setSkewY(int value); void setParallaxOrigin(const QPointF ¶llaxOrigin); void setOrientation(Orientation value); void setRenderOrder(RenderOrder value); @@ -289,6 +296,16 @@ inline EditableMap::StaggerIndex EditableMap::staggerIndex() const return static_cast(map()->staggerIndex()); } +inline int EditableMap::skewX() const +{ + return map()->skewX(); +} + +inline int EditableMap::skewY() const +{ + return map()->skewY(); +} + inline QPointF EditableMap::parallaxOrigin() const { return map()->parallaxOrigin(); diff --git a/src/tiled/mapitem.cpp b/src/tiled/mapitem.cpp index 95d1618046..2704343254 100644 --- a/src/tiled/mapitem.cpp +++ b/src/tiled/mapitem.cpp @@ -399,6 +399,7 @@ void MapItem::documentChanged(const ChangeEvent &change) case Map::HexSideLengthProperty: case Map::StaggerAxisProperty: case Map::StaggerIndexProperty: + case Map::SkewProperty: case Map::ParallaxOriginProperty: case Map::OrientationProperty: mapChanged(); @@ -485,9 +486,9 @@ void MapItem::mapChanged() World *world = worldDocument->world(); if (world->canBeModified()) { const QRect currentRectInWorld = world->mapRect(mapFileName); - QRect resizedRect = mapDocument()->renderer()->mapBoundingRect(); - if (currentRectInWorld.size() != resizedRect.size()) { - resizedRect.translate(currentRectInWorld.topLeft()); + const QSize newSize = mapDocument()->renderer()->mapBoundingRect().size(); + if (currentRectInWorld.size() != newSize) { + const QRect resizedRect(currentRectInWorld.topLeft(), newSize); auto undoStack = worldDocument->undoStack(); undoStack->push(new SetMapRectCommand(worldDocument.data(), mapFileName, resizedRect)); } diff --git a/src/tiled/mapscene.cpp b/src/tiled/mapscene.cpp index d9609b1d80..cf04b3ba4b 100644 --- a/src/tiled/mapscene.cpp +++ b/src/tiled/mapscene.cpp @@ -411,6 +411,9 @@ void MapScene::changeEvent(const ChangeEvent &change) case Map::BackgroundColorProperty: updateBackgroundColor(); break; + case Map::RenderOrderProperty: + update(); + break; default: break; } diff --git a/src/tiled/newmapdialog.cpp b/src/tiled/newmapdialog.cpp index 0840f21302..1d4966cae1 100644 --- a/src/tiled/newmapdialog.cpp +++ b/src/tiled/newmapdialog.cpp @@ -86,6 +86,7 @@ NewMapDialog::NewMapDialog(QWidget *parent) : mUi->orientation->addItem(tr("Orthogonal"), QVariant::fromValue(Map::Orthogonal)); mUi->orientation->addItem(tr("Isometric"), QVariant::fromValue(Map::Isometric)); + mUi->orientation->addItem(tr("Oblique"), QVariant::fromValue(Map::Oblique)); mUi->orientation->addItem(tr("Isometric (Staggered)"), QVariant::fromValue(Map::Staggered)); mUi->orientation->addItem(tr("Hexagonal (Staggered)"), QVariant::fromValue(Map::Hexagonal)); @@ -150,6 +151,9 @@ MapDocumentPtr NewMapDialog::createMap() mapParameters.tileHeight = session::mapTileHeight; mapParameters.infinite = !session::fixedSize; + if (mapParameters.orientation == Map::Oblique) + mapParameters.skewX = mapParameters.tileWidth / 2; + auto map = std::make_unique(mapParameters); map->setLayerDataFormat(session::layerDataFormat); @@ -190,6 +194,8 @@ void NewMapDialog::refreshPixelSize() mapParameters.height = mUi->mapHeight->value(); mapParameters.tileWidth = mUi->tileWidth->value(); mapParameters.tileHeight = mUi->tileHeight->value(); + if (mapParameters.orientation == Map::Oblique) + mapParameters.skewX = mapParameters.tileWidth / 2; const Map map(mapParameters); const QSize size = MapRenderer::create(&map)->mapBoundingRect().size(); diff --git a/src/tiled/propertieswidget.cpp b/src/tiled/propertieswidget.cpp index ece10d9d1b..1da9c2a4e2 100644 --- a/src/tiled/propertieswidget.cpp +++ b/src/tiled/propertieswidget.cpp @@ -88,11 +88,13 @@ template<> EnumData enumData() return {{ QCoreApplication::translate("Tiled::NewMapDialog", "Orthogonal"), QCoreApplication::translate("Tiled::NewMapDialog", "Isometric"), + QCoreApplication::translate("Tiled::NewMapDialog", "Oblique"), QCoreApplication::translate("Tiled::NewMapDialog", "Isometric (Staggered)"), QCoreApplication::translate("Tiled::NewMapDialog", "Hexagonal (Staggered)") }, { Map::Orthogonal, Map::Isometric, + Map::Oblique, Map::Staggered, Map::Hexagonal, }}; @@ -796,6 +798,16 @@ class MapProperties : public ObjectProperties push(new ChangeMapStaggerIndex(mapDocument(), value)); }); + mSkewProperty = new PointProperty( + tr("Skew"), + [this] { + return QPoint(map()->skewX(), map()->skewY()); + }, + [this](const QPoint &value) { + push(new ChangeMapSkew(mapDocument(), value)); + }); + mSkewProperty->setSuffix(tr(" px")); + mParallaxOriginProperty = new PointFProperty( tr("Parallax Origin"), [this] { @@ -861,6 +873,7 @@ class MapProperties : public ObjectProperties mMapProperties->addProperty(mHexSideLengthProperty); mMapProperties->addProperty(mStaggerAxisProperty); mMapProperties->addProperty(mStaggerIndexProperty); + mMapProperties->addProperty(mSkewProperty); mMapProperties->addSeparator(); mMapProperties->addProperty(mParallaxOriginProperty); mMapProperties->addSeparator(); @@ -901,6 +914,9 @@ class MapProperties : public ObjectProperties case Map::StaggerIndexProperty: emit mStaggerIndexProperty->valueChanged(); break; + case Map::SkewProperty: + emit mSkewProperty->valueChanged(); + break; case Map::ParallaxOriginProperty: emit mParallaxOriginProperty->valueChanged(); break; @@ -935,7 +951,9 @@ class MapProperties : public ObjectProperties mHexSideLengthProperty->setEnabled(orientation == Map::Hexagonal); mStaggerAxisProperty->setEnabled(stagger); mStaggerIndexProperty->setEnabled(stagger); - mRenderOrderProperty->setEnabled(orientation == Map::Orthogonal); + mSkewProperty->setEnabled(orientation == Map::Oblique); + mRenderOrderProperty->setEnabled(orientation == Map::Orthogonal || + orientation == Map::Oblique); mChunkSizeProperty->setEnabled(map()->infinite()); switch (map()->layerDataFormat()) { @@ -970,6 +988,7 @@ class MapProperties : public ObjectProperties IntProperty *mHexSideLengthProperty; Property *mStaggerAxisProperty; Property *mStaggerIndexProperty; + PointProperty *mSkewProperty; Property *mParallaxOriginProperty; Property *mLayerDataFormatProperty; Property *mCompressionLevelProperty; diff --git a/src/tiled/undocommands.h b/src/tiled/undocommands.h index 7ca9c594fe..0bf6e38cf9 100644 --- a/src/tiled/undocommands.h +++ b/src/tiled/undocommands.h @@ -53,6 +53,7 @@ enum UndoCommands { Cmd_ChangeMapOrientation, Cmd_ChangeMapParallaxOrigin, Cmd_ChangeMapRenderOrder, + Cmd_ChangeMapSkew, Cmd_ChangeMapStaggerAxis, Cmd_ChangeMapStaggerIndex, Cmd_ChangeMapTileSize, From b10f02e414df5eb2f51502bebc3ed4f7f7ffd7cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thorbj=C3=B8rn=20Lindeijer?= Date: Wed, 4 Feb 2026 18:26:56 +0100 Subject: [PATCH 4/5] docs: Added page covering map orientations --- docs/index.rst | 1 + docs/manual/introduction.rst | 13 ++-- docs/manual/maps.rst | 126 +++++++++++++++++++++++++++++++++++ 3 files changed, 134 insertions(+), 6 deletions(-) create mode 100644 docs/manual/maps.rst diff --git a/docs/index.rst b/docs/index.rst index 6c2f7675c2..f59d675e85 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -13,6 +13,7 @@ Tiled Documentation manual/introduction manual/projects + manual/maps manual/layers manual/editing-tile-layers manual/objects diff --git a/docs/manual/introduction.rst b/docs/manual/introduction.rst index 6ccf3b2840..809828097d 100644 --- a/docs/manual/introduction.rst +++ b/docs/manual/introduction.rst @@ -11,8 +11,8 @@ your level with extra information used by the game. Tiled focuses on general flexibility while trying to stay intuitive.** In terms of tile maps, it supports straight rectangular tile layers, but -also projected isometric, staggered isometric and staggered hexagonal -layers. A tileset can be either a single image containing many tiles, or +also projected isometric, staggered isometric, staggered hexagonal and +oblique layers. A tileset can be either a single image containing many tiles, or it can be a collection of individual images. In order to support certain depth faking techniques, tiles and layers can be offset by a custom distance and their rendering order can be configured. @@ -93,10 +93,11 @@ following dialog will pop up: New Map Here, we choose the initial map size, tile size, orientation, tile layer -format, tile render order (only supported for *Orthogonal* maps) and whether -the map is :doc:`infinite ` or not. All of these things -can be changed later as needed, so it's not important to get it all right the -first time. +format, tile render order (only supported for *Orthogonal* and *Oblique* maps) +and whether the map is :doc:`infinite ` or not. For more +details on map orientations and related options, see :doc:`maps `. All of +these things can be changed later as needed, so it's not important to get it +all right the first time. .. note:: diff --git a/docs/manual/maps.rst b/docs/manual/maps.rst new file mode 100644 index 0000000000..929ceb3907 --- /dev/null +++ b/docs/manual/maps.rst @@ -0,0 +1,126 @@ +Editing Maps +============ + +A map is a top-level asset file that contains :doc:`layers ` and references :doc:`tileset +files ` (which can optionally be embedded). It defines the coordinate system, tile +size, orientation, render order and other map-wide configuration options that affect how the content +is displayed and exported. + +Creating a Map +-------------- + +You can create a new map via *File -> New -> New Map…*. The New Map dialog +lets you set: + +- **Orientation** (see :ref:`map-orientations`) +- **Tile Layer Format** (the storage format for tile data) +- **Render Order** (only for orthogonal and oblique maps) +- **Map Size** (fixed width/height or infinite) +- **Tile Size** (tile width/height in pixels) + +All of these options can be changed later in the **Properties** panel, via +*Map -> Map Properties…*. + +.. _map-orientations: + +Map Orientations +---------------- + +The map orientation controls how the tile grid is projected and how tiles are +positioned relative to each other. Tiled supports the following orientations. + +Orthogonal +~~~~~~~~~~ + +The classic top-down grid where rectangular tiles are arranged in straight rows +and columns. This is the most straightforward orientation. + +Isometric +~~~~~~~~~ + +Isometric maps are projected to give a 3D-like appearance. Tiles are still arranged in a grid, but +are drawn as diamonds (though Tiled doesn't transform your art, you'll need to provide the diamond +shaped tiles). Object positions are stored in a projected coordinate space (see +:ref:`object-layer-introduction`). + +Isometric (Staggered) +~~~~~~~~~~~~~~~~~~~~~ + +Staggered isometric maps also use diamond-shaped tiles, but the grid is +staggered every other row or column. The exact staggering is controlled by the +**Stagger Axis** and **Stagger Index** map properties. + +This orientation allows a map based on isometric tiles to still have an overall rectangular shape. + +Hexagonal (Staggered) +~~~~~~~~~~~~~~~~~~~~~ + +Hexagonal maps use hex tiles arranged in a staggered grid. Like staggered +isometric maps, the staggering is controlled by **Stagger Axis** and +**Stagger Index**. Hexagonal maps also support a **Hex Side Length** that +defines the length of the straight edges of the hex tile. + +The following table shows how **Stagger Axis** relates to the two common hex +tile orientations: + +.. list-table:: + :header-rows: 1 + :widths: 30 70 + + * - Stagger Axis + - Hex Tile Orientation + * - X + - Pointy-top + * - Y + - Straight-top + + +.. raw:: html + +
New in Tiled 1.12
+ +Oblique +~~~~~~~ + +Oblique maps apply a skew transform to achieve a pseudo-3D projection. The amount of skew is +controlled by the **Skew** map property, in pixels. + +**Skew X** determines the horizontal offset applied for each row, while **Skew Y** determines the +vertical offset applied for each column. + +Map Properties +-------------- + +The following properties are especially relevant when configuring a map. + +Map Size (Fixed vs. Infinite) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Maps can be **fixed size** or **infinite**. A fixed map has a set width and +height in tiles, while an infinite map has an auto-growing canvas that expands +as you paint. The choice impacts how tile layers are stored. + +For details on working with infinite maps and converting between the two, see +:doc:`using-infinite-maps`. + +Tile Size +~~~~~~~~~ + +The tile width and height define the size of each tile in pixels. These values affect the tile grid +and the map bounds, but does not affect the size at which tiles are rendered (unless the relevant +tileset has **Tile Render Size** set to **Map Grid Size**). + +Parallax Origin +~~~~~~~~~~~~~~~ + +The parallax origin defines the reference point for :ref:`parallax scrolling +factor ` on layers. It is stored per map and defaults to +(0, 0), which is the top-left of the map's bounding box. + +Tile Layer Format +~~~~~~~~~~~~~~~~~ + +The tile layer format determines how tile data is stored when saving or +exporting the map. Some formats are more compact or faster to parse than +others. You can change the format at any time in the **Properties** panel, +via *Map -> Map Properties…*. From ff8ae50b3401c606a15efaa05237ff99a7a24dbe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thorbj=C3=B8rn=20Lindeijer?= Date: Wed, 4 Feb 2026 19:55:55 +0100 Subject: [PATCH 5/5] docs: Updated a number of new/since badges Everything before Tiled 1.11 is no longer considered "new". --- docs/manual/automapping.md | 4 ++-- docs/manual/custom-properties.rst | 4 ++-- docs/manual/editing-tile-layers.rst | 4 ++-- docs/manual/export-defold.rst | 4 ++-- docs/manual/export-tscn.rst | 6 +++--- docs/manual/objects.rst | 2 +- docs/manual/preferences.rst | 2 +- docs/manual/scripting.rst | 4 ++-- docs/manual/using-commands.rst | 2 +- 9 files changed, 16 insertions(+), 16 deletions(-) diff --git a/docs/manual/automapping.md b/docs/manual/automapping.md index 319c5f7d01..9af935cf2e 100644 --- a/docs/manual/automapping.md +++ b/docs/manual/automapping.md @@ -230,7 +230,7 @@ IgnoreHexRotate120 : This boolean layer property can be added to `input` and `inputnot` layers to also match 120-degree rotated tiles on hexagonal maps. However, note that Automapping currently does not really work for hexagonal maps since it does not take into account the staggered axis. (outputProbability)= -Probability {bdg-primary}`New in Tiled 1.10` +Probability {bdg-secondary-line}`Since Tiled 1.10` : This float layer property can be added to `output` layers to control the probability that a given output index will be chosen. The probabilities for each output index are relative to one another, and default to 1.0. For example, if you have outputA with probability 2 and outputB with probability 0.5, A will be chosen four times as often as B. If multiple output layers with the same index have their **Probability** set, the last (top-most) layer's probability will be used. {bdg-secondary-line}`Since Tiled 1.9` @@ -265,7 +265,7 @@ NoOverlappingOutput : When set to `true`, the output of a rule is not allowed to overlap other outputs of the same rule (defaults to `false`). (IgnoreLock)= -IgnoreLock {bdg-primary}`New in Tiled 1.10` +IgnoreLock {bdg-secondary-line}`Since Tiled 1.10` : Since Tiled 1.10, Automapping rules no longer modify locked layers. Set this property to `true` to ignore the lock. This can be useful when you have layers that are only changed by rules and want to keep them locked. All these options can also be set on the rule map itself, in which case they apply as defaults for all rules, which can then be overridden for specific rules by placing rectangle objects. diff --git a/docs/manual/custom-properties.rst b/docs/manual/custom-properties.rst index b20379ff5d..e00d40cb01 100644 --- a/docs/manual/custom-properties.rst +++ b/docs/manual/custom-properties.rst @@ -51,7 +51,7 @@ quickly jump to the referenced object. .. raw:: html -
New in Tiled 1.8
+
Since Tiled 1.8
.. _custom-property-types: @@ -105,7 +105,7 @@ names. Classes can have members referring to other classes. .. raw:: html -
New in Tiled 1.9
+
Since Tiled 1.9
Each data type has a "Class" property, which can be used to refer to a custom class. The members of this class will then be directly available as custom diff --git a/docs/manual/editing-tile-layers.rst b/docs/manual/editing-tile-layers.rst index eb019d91e0..9967b929af 100644 --- a/docs/manual/editing-tile-layers.rst +++ b/docs/manual/editing-tile-layers.rst @@ -116,7 +116,7 @@ tile or pattern. .. raw:: html -
Since Tiled 1.10.2
+
Since Tiled 1.10.2
- Holding ``Alt`` draws the rectangle or ellipse centered around the starting location. @@ -170,7 +170,7 @@ by clicking on one of the tool buttons in the Tool Options toolbar. .. raw:: html -
Since Tiled 1.12
+
New in Tiled 1.12
While selecting an area, the following modifiers can be used: diff --git a/docs/manual/export-defold.rst b/docs/manual/export-defold.rst index 9b798d9e2b..a57f081078 100644 --- a/docs/manual/export-defold.rst +++ b/docs/manual/export-defold.rst @@ -12,7 +12,7 @@ It only supports tile layers and only a single tileset may be used. .. raw:: html -
New in Tiled 1.9.2
+
Since Tiled 1.9.2
Custom Properties ^^^^^^^^^^^^^^^^^ @@ -69,7 +69,7 @@ Custom Properties .. raw:: html -
New in Tiled 1.10
+
Since Tiled 1.10
* A custom float property named "z" can be added to tile layers to manually specify their ``z`` value. diff --git a/docs/manual/export-tscn.rst b/docs/manual/export-tscn.rst index 40453b4b86..27c3a5c992 100644 --- a/docs/manual/export-tscn.rst +++ b/docs/manual/export-tscn.rst @@ -2,7 +2,7 @@ .. raw:: html -
New in Tiled 1.10
+
Since Tiled 1.10
Godot 4 ======= @@ -64,7 +64,7 @@ Tile Properties .. raw:: html -
New in Tiled 1.10.2
+
Since Tiled 1.10.2
All custom properties set on tiles will get exported as `Custom Data Layers `__ @@ -93,7 +93,7 @@ overwritten every time the map is exported. .. raw:: html -
Since Tiled 1.11
+
New in Tiled 1.11
Object Properties ~~~~~~~~~~~~~~~~~ diff --git a/docs/manual/objects.rst b/docs/manual/objects.rst index 3a391bf436..039b35589c 100644 --- a/docs/manual/objects.rst +++ b/docs/manual/objects.rst @@ -81,7 +81,7 @@ collision shape needs to represent a circle or ellipse. .. raw:: html -
Since Tiled 1.12
+
New in Tiled 1.12
.. _insert-capsule-tool: diff --git a/docs/manual/preferences.rst b/docs/manual/preferences.rst index d82bfbf461..322f824ef2 100644 --- a/docs/manual/preferences.rst +++ b/docs/manual/preferences.rst @@ -206,7 +206,7 @@ as they depend on the system. .. raw:: html -
New in Tiled 1.10
+
Since Tiled 1.10
Custom Interface Font ~~~~~~~~~~~~~~~~~~~~~ diff --git a/docs/manual/scripting.rst b/docs/manual/scripting.rst index 7e7b5cddc3..4c4c54d27f 100644 --- a/docs/manual/scripting.rst +++ b/docs/manual/scripting.rst @@ -80,7 +80,7 @@ startup. .. raw:: html -
Since Tiled 1.8
+
Since Tiled 1.8
When using the ``.mjs`` extension, script files are loaded as `JavaScript modules`_. They will then be able to use the `import`_ and `export`_ statements @@ -110,7 +110,7 @@ script expressions. .. raw:: html -
Since Tiled 1.9
+
Since Tiled 1.9
.. _script-cli: diff --git a/docs/manual/using-commands.rst b/docs/manual/using-commands.rst index 4903849f3c..700902d8fd 100644 --- a/docs/manual/using-commands.rst +++ b/docs/manual/using-commands.rst @@ -100,7 +100,7 @@ the following variables: .. raw:: html -
New in Tiled 1.9
+
Since Tiled 1.9
``%worldfile`` the full path of the world the current map is part of, if any.