Example #1
0
class ImageBase(helpers.SizeMixin):
    """Image base class

    This defines the basic image API, shared by
    :class:`~tmxlib.image_base.Image` and
    :class:`~tmxlib.image_base.ImageRegion`.

    Pixels are represented as (r, g, b, a) float tuples, with components in the
    range of 0 to 1.
    """
    x, y = helpers.unpacked_properties('top_left')

    def __getitem__(self, pos):
        """Get a pixel or region

        With a pair of integers, this returns a pixel via
        :meth:`~tmxlib.image_base.Image.get_pixel`:

        :param pos: pair of integers, (x, y)
        :return: pixel at (x, y) as a (r, g, b, a) float tuple

        With a pair of slices, returns a sub-image:

        :param pos: pair of slices, (left:right, top:bottom)
        :return: a :class:`~tmxlib.image_base.ImageRegion`
        """
        x, y = pos
        try:
            left = x.start
            right = x.stop
            top = y.start
            bottom = y.stop
        except AttributeError:
            return self.get_pixel(x, y)
        else:
            for c in x, y:
                if c.step not in (None, 1):
                    raise ValueError('step not supported for slicing images')
            left, top = self._wrap_coords(
                0 if left is None else left,
                0 if top is None else top)
            right, bottom = self._wrap_coords(
                self.width if right is None else right,
                self.height if bottom is None else bottom)
            left = _clamp(left, 0, self.width)
            right = _clamp(right, left, self.width)
            top = _clamp(top, 0, self.height)
            bottom = _clamp(bottom, top, self.height)
            return ImageRegion(self, (left, top), (right - left, bottom - top))

    def _parent_info(self):
        """Return (x offset, y offset, immutable image)

        Used to make sure the parents of ImageRegion is always an Image,
        not another region or a canvas.
        """
        return 0, 0, self
Example #2
0
class DrawImageCommand(DrawCommand):
    """Command to draw an image

    init arguments that become attributes:

        .. attribute:: image

            The image to draw

        .. attribute:: pos

            Position at which to draw the image.
            Will also available as ``x`` and ``y`` attributes.
    """
    x, y = helpers.unpacked_properties('pos')

    def __init__(self, image, pos=(0, 0), opacity=1):
        self.image = image
        self.pos = pos
        self.opacity = opacity

    def draw(self, canvas):
        canvas.draw_image(self.image, self.pos,
                          opacity=self.opacity)
Example #3
0
class MapObject(helpers.LayerElementMixin):
    """A map object: something that's not placed on the fixed grid

    Has several subclasses.

    Can be either a "tile object", which has an associated tile much like a
    map-tile, or a regular (non-tile) object that has a settable size.

    init arguments, which become attributes:

        .. attribute:: layer

            The layer this object is on

        .. attribute:: pixel_pos

            The pixel coordinates

        .. attribute:: pixel_size

            Size of this object, as a (width, height) tuple, in pixels.

            Only one of ``pixel_size`` and ``size`` may be specified.

        .. attribute:: size

            Size of this object, as a (width, height) tuple, in units of map
            tiles.

        .. attribute:: name

            Name of the object. A string (or unicode)

        .. attribute:: type

            Type of the object. A string (or unicode). No semantics attached.

        .. attribute:: id

            Unique numeric ID of the object.

    Other attributes:

        .. attribute:: objtype

            Type of the object: ``'rectangle'``, ``'tile'`` or ``'ellipse'``

        .. attribute:: properties

            Dict of string (or unicode) keys & values for custom data

        .. attribute:: pos

            Position of the object in tile coordinates, as a (x, y) float tuple

        .. attribute:: map

            The map associated with this object

    Unpacked position attributes:

        .. attribute:: x
        .. attribute:: y
        .. attribute:: pixel_x
        .. attribute:: pixel_y
    """
    # XXX: Implement `id` -- "Even if an object is deleted, no object ever gets
    # the same ID."
    pixel_x, pixel_y = helpers.unpacked_properties('pixel_pos')

    def __init__(self,
                 layer,
                 pixel_pos,
                 *,
                 name=None,
                 type=None,
                 id=None,
                 template=None):
        self.layer = layer
        self.pixel_pos = pixel_pos
        self.name = name
        self.type = type
        self.properties = {}
        self.id = id
        self.template = template

    @property
    def pos(self):
        return (self.pixel_pos[0] / self.layer.map.tile_width,
                self.pixel_pos[1] / self.layer.map.tile_height - 1)

    @pos.setter
    def pos(self, value):
        x, y = value
        y += 1
        self.pixel_pos = (x * self.layer.map.tile_width,
                          y * self.layer.map.tile_height)

    def to_dict(self, y=None):
        """Export to a dict compatible with Tiled's JSON plugin"""
        if y is None:
            y = self.pixel_y
        d = dict(
            name=self.name or '',
            type=self.type or '',
            x=self.pixel_x,
            y=y,
            visible=True,
            properties=self.properties,
        )
        return d

    @classmethod
    def from_dict(cls, dct, layer):
        """Import from a dict compatible with Tiled's JSON plugin"""
        if dct.get('ellipse', False):
            return EllipseObject.from_dict(dct, layer)
        elif dct.get('polygon', False):
            return PolygonObject.from_dict(dct, layer)
        elif dct.get('polyline', False):
            return PolylineObject.from_dict(dct, layer)
        else:
            return RectangleObject.from_dict(dct, layer)

    @classmethod
    def _dict_helper(cls, dct, layer, **kwargs):
        helpers.assert_item(dct, 'visible', True)
        self = cls(layer=layer,
                   pixel_pos=(dct.pop('x'), dct.pop('y')),
                   name=dct.pop('name', None),
                   type=dct.pop('type', None),
                   **kwargs)
        self.properties.update(dct.pop('properties', {}))
        return self
Example #4
0
class Map(fileio.ReadWriteBase, helpers.SizeMixin):
    """A tile map, tmxlib's core class

    init arguments, which become attributes:

        .. attribute:: size

            a (height, width) pair specifying the size of the map, in tiles

        .. attribute:: tile_size

            a pair specifying the size of one tile, in pixels

        .. attribute:: orientation

            The orientation of the map (``'orthogonal'``, ``'isometric'``,
            or ``'staggered'``)

        .. attribute:: background_color

            The background color for the map, as a triple of floats (0..1)

    Other attributes:

        .. attribute:: tilesets

            A :class:`TilesetList` of tilesets this map uses

        .. attribute:: layers

            A :class:`LayerList` of layers this map uses

        .. attribute:: properties

            A dict of properties, with string (or unicode) keys & values

        .. attribute:: pixel_size

            The size of the map, in pixels. Not settable directly: use
            `size` and `tile_size` for that.

        .. attribute:: end_gid

            The first GID that is not available for tiles.
            This is the end_gid for the map's last tileset.

    Unpacked size attributes:

        Each "size" property has corresponding "width" and "height" properties.

        .. attribute:: height
        .. attribute:: width
        .. attribute:: tile_height
        .. attribute:: tile_width
        .. attribute:: pixel_height
        .. attribute:: pixel_width


    """
    _rw_obj_type = 'map'

    tile_width, tile_height = helpers.unpacked_properties('tile_size')
    pixel_width, pixel_height = helpers.unpacked_properties('pixel_size')

    # XXX: Fully implement, test, and document base_path:
    #   This should be used for saving, so that relative paths work as
    #   correctly as they can.
    #   And it's not just here...
    def __init__(self,
                 size,
                 tile_size,
                 orientation='orthogonal',
                 background_color=None,
                 base_path=None,
                 render_order=None):
        self.orientation = orientation
        self.size = size
        self.tile_size = tile_size
        self.tilesets = tileset.TilesetList(self)
        self.layers = layer.LayerList(self)
        self.background_color = background_color
        self.properties = {}
        self.base_path = base_path
        self.render_order = render_order

    @property
    def pixel_size(self):
        return self.width * self.tile_width, self.height * self.tile_height

    @property
    def end_gid(self):
        try:
            last_tileset = self.tilesets[-1]
        except IndexError:
            return 0
        else:
            return last_tileset.end_gid(self)

    def add_layer(self, name, before=None, after=None, layer_class=None):
        """Add an empty layer with the given name to the map.

        By default, the new layer is added at the end of the layer list.
        A different position may be specified with either of the `before` or
        `after` arguments, which may be integer indices or names.

        layer_class defaults to TileLayer
        """
        if not layer_class:
            layer_class = layer.TileLayer
        new_layer = layer_class(self, name)
        if after is not None:
            if before is not None:
                raise ValueError("Can't specify both before and after")
            self.layers.insert_after(after, new_layer)
        elif before is not None:
            self.layers.insert(before, new_layer)
        else:
            self.layers.append(new_layer)
        return new_layer

    def add_tile_layer(self, name, before=None, after=None):
        """Add an empty tile layer with the given name to the map.

        See add_layer
        """
        return self.add_layer(name, before, after, layer_class=layer.TileLayer)

    def add_object_layer(self, name, before=None, after=None):
        """Add an empty object layer with the given name to the map.

        See add_layer
        """
        return self.add_layer(name,
                              before,
                              after,
                              layer_class=layer.ObjectLayer)

    def add_image_layer(self, name, image, before=None, after=None):
        """Add an image layer with the given name and image to the map.

        See add_layer
        """
        new_layer = self.add_layer(name,
                                   before,
                                   after,
                                   layer_class=layer.ImageLayer)
        new_layer.image = image
        return new_layer

    def all_tiles(self):
        """Yield all tiles in the map, including tile objects
        """
        for layer in self.layers:
            for tile in layer.all_tiles():
                yield tile

    def all_objects(self):
        """Yield all objects in the map
        """
        for layer in self.layers:
            for obj in layer.all_objects():
                yield obj

    def get_tiles(self, x, y):
        """For each tile layer, yield the tile at the given position.
        """
        for layer in self.layers:
            if layer.type == 'tiles':
                yield layer[x, y]

    def check_consistency(self):
        """Check that this map is okay.

        Most checks are done when reading a map, but if more are required,
        call this method after reading.
        This will do a more expensive check than what's practical from within
        readers.
        """
        large_gid = self.end_gid
        for tile in self.all_tiles():
            assert tile.gid < large_gid

    def generate_draw_commands(self):
        return itertools.chain.from_iterable(layer.generate_draw_commands()
                                             for layer in self.layers
                                             if layer.visible)

    def render(self):
        from tmxlib.canvas import Canvas
        canvas = Canvas(
            self.pixel_size,
            #color=self.background_color,
            commands=self.generate_draw_commands())
        return canvas

    def _repr_png_(self):
        return self.render()._repr_png_()

    def to_dict(self):
        """Export to a dict compatible with Tiled's JSON plugin

        You can use e.g. a JSON or YAML library to write such a dict to a file.
        """
        d = dict(
            height=self.height,
            width=self.width,
            tileheight=self.tile_height,
            tilewidth=self.tile_width,
            orientation=self.orientation,
            properties=self.properties,
            version=1,
            layers=[la.to_dict() for la in self.layers],
            tilesets=[t.to_dict(map=self) for t in self.tilesets],
        )
        if self.background_color:
            d['backgroundcolor'] = '#' + fileio.to_hexcolor(
                self.background_color)
        return d

    @helpers.from_dict_method
    def from_dict(cls, dct, base_path=None):
        """Import from a dict compatible with Tiled's JSON plugin

        Use e.g. a JSON or YAML library to read such a dict from a file.

        :param dct: Dictionary with data
        :param base_path: Base path of the file, for loading linked resources
        """
        if dct.pop('version', 1) != 1:
            raise ValueError('tmxlib only supports Tiled JSON version 1')
        self = cls(
            size=(dct.pop('width'), dct.pop('height')),
            tile_size=(dct.pop('tilewidth'), dct.pop('tileheight')),
            orientation=dct.pop('orientation', 'orthogonal'),
        )
        if base_path:
            self.base_path = base_path
        background_color = dct.pop('backgroundcolor', None)
        if background_color:
            self.background_color = fileio.from_hexcolor(background_color)
        self.properties = dct.pop('properties')
        self.tilesets = [
            tileset.Tileset.from_dict(d, base_path)
            for d in dct.pop('tilesets')
        ]
        self.layers = [
            layer.Layer.from_dict(d, self) for d in dct.pop('layers')
        ]
        self.properties.update(dct.pop('properties', {}))
        return self
Example #5
0
class TilesetTile(object):
    """Reference to a tile within a tileset

    init arguents, which become attributes:

        .. attribute:: tileset

            the tileset this tile belongs to

        .. attribute:: number

            the number of the tile

    Other attributes:

        .. attribute:: pixel_size

            The size of the tile, in pixels. Also available as
            (``pixel_width``, ``pixel_height``).

        .. attribute:: properties

            A string-to-string dictionary holding custom properties of the tile

        .. attribute:: image

            Image this tile uses. Most often this will be a
            :class:`region <~tmxlib.image_base.ImageRegion>` of the tileset's
            image.

        .. attribute:: terrain_indices

            List of indices to the tileset's terrain list for individual
            corners of the tile. See the TMX documentation for details.

        .. attribute:: terrains

            Tuple of terrains for individual corners of the tile. If no
            terrain is given, None is used instead.

        .. attribute:: probability

            The probability that this tile will be chosen among others with the
            same terrain information. May be None.
    """
    pixel_width, pixel_height = helpers.unpacked_properties('pixel_size')

    def __init__(self, tileset, number):
        self.tileset = tileset
        self.number = number

    def gid(self, map):
        """Return the GID of this tile for a given map

        The GID is a map-specific identifier unique for any tileset-tile
        the map uses.
        """
        return self.tileset.first_gid(map) + self.number

    @property
    def pixel_size(self):
        return self.image.size

    @property
    def properties(self):
        return self.tileset.tile_attributes[self.number].setdefault(
            'properties', {})

    @properties.setter
    def properties(self, v):
        self.tileset.tile_attributes[self.number]['properties'] = v

    @property
    def probability(self):
        return self.tileset.tile_attributes[self.number].setdefault(
            'probability', None)

    @probability.setter
    def probability(self, v):
        self.tileset.tile_attributes[self.number]['probability'] = v

    @property
    def terrain_indices(self):
        return self.tileset.tile_attributes[self.number].setdefault(
            'terrain_indices', [])

    @terrain_indices.setter
    def terrain_indices(self, v):
        self.tileset.tile_attributes[self.number]['terrain_indices'] = v

    def __eq__(self, other):
        try:
            other_number = other.number
            other_tileset = other.tileset
        except AttributeError:
            return False
        return self.number == other_number and self.tileset is other_tileset

    def __hash__(self):
        return hash(('tmxlib tileset tile', self.number, self.tileset))

    def __ne__(self, other):
        return not (self == other)

    def __repr__(self):
        return '<TilesetTile #%s of %s at 0x%x>' % (
            self.number, self.tileset.name, id(self))

    @property
    def image(self):
        return self.tileset.tile_image(self.number)

    @image.setter
    def image(self, value):
        return self.tileset.set_tile_image(self.number, value)

    def get_pixel(self, x, y):
        """Get a pixel at the specified location.

        Pixels are returned as RGBA 4-tuples.
        """
        return self.image.get_pixel(x, y)

    @property
    def terrains(self):
        result = []
        for index in self.terrain_indices:
            try:
                result.append(self.tileset.terrains[index])
            except (IndexError, KeyError):
                result.append(None)
        return tuple(result)
Example #6
0
class Tileset(fileio.ReadWriteBase):
    """Base class for a tileset: bank of tiles a map can use.

    There are two kinds of tilesets: external and internal.
    Internal tilesets are specific to a map, and their contents are saved
    inside the map file.
    External tilesets are saved to their own file, so they may be shared
    between several maps.
    (Of course, any tileset can be shared between maps at the Python level;
    this distinction only applies to what happens on disk.)
    External tilesets have the file path in their `source` attribute;
    internal ones have `source` set to None.

    tmxlib will try to ensure that each external tileset gets only loaded once,
    an the resulting Python objects are shared. See :meth:`ReadWriteBase.open`
    for more information.

    init arguments, which become attributes:

        .. attribute:: name

            Name of the tileset

        .. attribute:: tile_size:

            A (width, height) pair giving the size of a tile in this
            tileset. In cases where a tileset can have unequally sized tiles,
            the tile size is not defined. This means that this property should
            not be used unless working with a specific subclass that defines
            tile_size better.

        .. attribute:: source

            For external tilesets, the file name for this tileset. None for
            internal ones.

    Other attributes:

        .. attribute:: properties

            A dict with string (or unicode) keys and values.
            Note that the official TMX format does not support tileset
            properties (`yet <https://github.com/bjorn/tiled/issues/77>`_),
            so editors like Tiled will remove these. (tmxlib saves and loads
            them just fine, however.)

        .. attribute:: terrains

            A :class:`~tmxlib.terrain.TerrainList` of terrains belonging to
            this tileset.
            Note that tileset tiles reference these by index, and the indices
            are currently not updated when the TerrainList is modified.
            This may change in the future.

        .. attribute:: tile_offset

            An offset in pixels to be applied when drawing a tile from this
            tileset.

    Unpacked versions of tuple attributes:

        .. attribute:: tile_width
        .. attribute:: tile_height
        .. attribute:: tile_offset_x
        .. attribute:: tile_offset_y

    """
    # XXX: When Serializers are official, include note for shared=True: (This
    # will only work if all the tilesets are loaded with the same Serializer.)
    column_count = None
    _rw_obj_type = 'tileset'
    tile_class = TilesetTile

    tile_offset_x, tile_offset_y = helpers.unpacked_properties('tile_offset')

    def __init__(self, name, tile_size):
        self.name = name
        self.tile_size = tile_size
        self.properties = {}
        self.terrains = terrain.TerrainList()
        self.tiles = {}
        self.tile_attributes = collections.defaultdict(dict)
        self.tile_offset = 0, 0

    def __getitem__(self, n):
        """Get tileset tile with the given number.

        Supports negative indices by wrapping around, as one would expect.
        """
        if n >= 0:
            try:
                tile = self.tiles[n]
            except KeyError:
                tile = self.tiles[n] = self.tile_class(self, n)
            return tile
        else:
            return self[len(self) + n]

    def __len__(self):
        """Return the number of tiles in this tileset.

        Subclasses need to override this method.
        """
        raise NotImplementedError('Tileset.__len__ is abstract')

    def __iter__(self):
        """Iterate through tiles in this tileset.
        """
        for i in range(len(self)):
            yield self[i]

    def first_gid(self, map):
        """Return the first gid used by this tileset in the given map
        """
        num = 1
        for tileset in map.tilesets:
            if tileset is self:
                return num
            else:
                num += len(tileset)
        error = helpers.TilesetNotInMapError('Tileset not in map')
        error.tileset = self
        raise error

    def end_gid(self, map):
        """Return the first gid after this tileset in the given map
        """
        return self.first_gid(map) + len(self)

    def tile_image(self, number):
        """Return the image used by the given tile.

        Usually this will be a region of a larger image.

        Subclasses need to override this method.
        """
        raise NotImplementedError('Tileset.tile_image')

    @property
    def tile_width(self):
        """Width of a tile in this tileset. See `size` in the class docstring.
        """
        return self.tile_size[0]

    @tile_width.setter
    def tile_width(self, value):
        self.tile_size = value, self.tile_size[1]

    @property
    def tile_height(self):
        """Height of a tile in this tileset. See `size` in the class docstring.
        """
        return self.tile_size[1]

    @tile_height.setter
    def tile_height(self, value):
        self.tile_size = self.tile_size[0], value

    def __repr__(self):
        return '<%s %r at 0x%x>' % (type(self).__name__, self.name, id(self))

    def to_dict(self, **kwargs):
        """Export to a dict compatible with Tiled's JSON plugin"""
        d = dict(
            name=self.name,
            properties=self.properties,
            tilewidth=self.tile_width,
            tileheight=self.tile_height,
        )
        if 'map' in kwargs:
            d['firstgid'] = self.first_gid(kwargs['map'])
        tile_properties = {}
        tiles = collections.defaultdict(dict)
        for tile in self:
            number = str(tile.number)
            if tile.properties:
                tile_properties[number] = tile.properties
            if tile.probability is not None:
                tiles[number]['probability'] = tile.probability
            if tile.terrain_indices:
                tiles[number]['terrain'] = list(tile.terrain_indices)
            if getattr(tile.image, 'source', None):
                tiles[number]['image'] = tile.image.source
        if tile_properties:
            d['tileproperties'] = tile_properties
        if tiles:
            d['tiles'] = dict(tiles)
        if self.terrains:
            d['terrains'] = [{
                'name': t.name,
                'tile': t.tile.number
            } for t in self.terrains]
        if any(self.tile_offset):
            d['tileoffset'] = {
                'x': self.tile_offset_x,
                'y': self.tile_offset_y
            }
        return d

    @classmethod
    def from_dict(cls, dct, base_path=None):
        """Import from a dict compatible with Tiled's JSON plugin"""
        if 'image' in dct:
            return ImageTileset.from_dict(dct, base_path)
        else:
            return IndividualTileTileset.from_dict(dct, base_path)

    def _fill_from_dict(self, dct, base_path):
        dct.pop('firstgid', None)
        if base_path:
            self.base_path = base_path
        self.properties.update(dct.pop('properties', {}))
        for number, properties in dct.pop('tileproperties', {}).items():
            self[int(number)].properties.update(properties)
        tile_info = dct.pop('tiles', {})
        for number in sorted(tile_info, key=int):
            attrs = dict(tile_info[number])
            number = int(number)
            probability = attrs.pop('probability', None)
            if probability is not None:
                self[number].probability = probability
            terrain_indices = attrs.pop('terrain', None)
            if terrain_indices is not None:
                self[number].terrain_indices = terrain_indices
            if number > len(tile_info):
                raise ValueError()
            while 0 <= len(self) <= number:
                self._append_placeholder()
            filename = attrs.pop('image', None)
            if filename:
                self[number].image = image.open(filename)
                if base_path:
                    self[number].image.base_path = base_path
            if attrs:
                raise ValueError('Extra tile attributes: %s' %
                                 ', '.join(attrs))
        for terrain in dct.pop('terrains', []):
            terrain = dict(terrain)
            self.terrains.append_new(terrain.pop('name'),
                                     self[int(terrain.pop('tile'))])
            assert not terrain
        tileoffset = dct.pop('tileoffset', None)
        if tileoffset:
            self.tile_offset = tileoffset['x'], tileoffset['y']
        dct.pop('margin', None)
        dct.pop('spacing', None)