Skip to content
Snippets Groups Projects
objects.rs 7.5 KiB
Newer Older
use std::collections::HashMap;
alexdevteam's avatar
alexdevteam committed

use xml::attribute::OwnedAttribute;
alexdevteam's avatar
alexdevteam committed

use crate::{
    properties::{parse_properties, Properties},
    util::{get_attrs, map_wrapper, parse_tag, XmlEventResult},
    LayerTile, LayerTileData, MapTilesetGid,
alexdevteam's avatar
alexdevteam committed
};

/// A structure describing an [`Object`]'s shape.
///
/// Also see the [TMX docs](https://doc.mapeditor.org/en/stable/reference/tmx-map-format/#tmx-object).
alexdevteam's avatar
alexdevteam committed
#[derive(Debug, PartialEq, Clone)]
#[allow(missing_docs)]
alexdevteam's avatar
alexdevteam committed
pub enum ObjectShape {
    Rect { width: f32, height: f32 },
    Ellipse { width: f32, height: f32 },
    Polyline { points: Vec<(f32, f32)> },
    Polygon { points: Vec<(f32, f32)> },
    Point(f32, f32),
}

/// Raw data belonging to an object. Used internally and for tile collisions.
///
/// Also see the [TMX docs](https://doc.mapeditor.org/en/stable/reference/tmx-map-format/#tmx-object).
alexdevteam's avatar
alexdevteam committed
#[derive(Debug, PartialEq, Clone)]
pub struct ObjectData {
    tile: Option<LayerTileData>,
    /// The name of the object, which is arbitrary and set by the user.
alexdevteam's avatar
alexdevteam committed
    pub name: String,
    /// The type of the object, which is arbitrary and set by the user.
alexdevteam's avatar
alexdevteam committed
    pub obj_type: String,
    /// The width of the object, if applicable. This refers to the attribute in `object`.
    /// Since it is duplicate or irrelevant information in all cases, use the equivalent
    /// member in [`ObjectShape`] instead.
    #[deprecated(since = "0.10.0", note = "Use [`ObjectShape`] members instead")]
alexdevteam's avatar
alexdevteam committed
    pub width: f32,
    /// The height of the object, if applicable. This refers to the attribute in `object`.
    /// Since it is duplicate or irrelevant information in all cases, use the equivalent
    /// member in [`ObjectShape`] instead.
    #[deprecated(since = "0.10.0", note = "Use [`ObjectShape`] members instead")]
alexdevteam's avatar
alexdevteam committed
    pub height: f32,
    /// The X coordinate of this object in pixels.
alexdevteam's avatar
alexdevteam committed
    pub x: f32,
    /// The Y coordinate of this object in pixels.
alexdevteam's avatar
alexdevteam committed
    pub y: f32,
    /// The clockwise rotation of this object around (x,y) in degrees.
alexdevteam's avatar
alexdevteam committed
    pub rotation: f32,
    /// Whether the object is shown or hidden.
alexdevteam's avatar
alexdevteam committed
    pub visible: bool,
    /// The object's shape.
alexdevteam's avatar
alexdevteam committed
    pub shape: ObjectShape,
    /// The object's custom properties as set by the user.
alexdevteam's avatar
alexdevteam committed
    pub properties: Properties,
}

impl ObjectData {
    /// ID of the object, which is unique per map since Tiled 0.11.
    ///
    /// On older versions this value is defaulted to 0.
    #[inline]
    pub fn id(&self) -> u32 {
        self.id
    }

    /// Returns the data of the tile that this object is referencing, if it exists.
    #[inline]
    pub fn tile_data(&self) -> Option<LayerTileData> {
        self.tile
    }
}

impl ObjectData {
    /// If it is known that the object has no tile images in it (i.e. collision data)
    /// then we can pass in [`None`] as the tilesets
    pub(crate) fn new(
        parser: &mut impl Iterator<Item = XmlEventResult>,
alexdevteam's avatar
alexdevteam committed
        attrs: Vec<OwnedAttribute>,
        tilesets: Option<&[MapTilesetGid]>,
        let ((id, tile, n, t, w, h, v, r), (x, y)) = get_attrs!(
alexdevteam's avatar
alexdevteam committed
            attrs,
            optionals: [
                ("id", id, |v:String| v.parse().ok()),
                ("gid", tile, |v:String| v.parse().ok()
                                            .and_then(|bits| LayerTileData::from_bits(bits, tilesets?))),
alexdevteam's avatar
alexdevteam committed
                ("name", name, |v:String| v.parse().ok()),
                ("type", obj_type, |v:String| v.parse().ok()),
                ("width", width, |v:String| v.parse().ok()),
                ("height", height, |v:String| v.parse().ok()),
                ("visible", visible, |v:String| v.parse().ok().map(|x:i32| x == 1)),
                ("rotation", rotation, |v:String| v.parse().ok()),
            ],
            required: [
                ("x", x, |v:String| v.parse().ok()),
                ("y", y, |v:String| v.parse().ok()),
            ],
            Error::MalformedAttributes("objects must have an x and a y number".to_string())
alexdevteam's avatar
alexdevteam committed
        );
        let visible = v.unwrap_or(true);
        let width = w.unwrap_or(0f32);
        let height = h.unwrap_or(0f32);
        let rotation = r.unwrap_or(0f32);
alexdevteam's avatar
alexdevteam committed
        let id = id.unwrap_or(0u32);
        let name = n.unwrap_or_default();
        let obj_type = t.unwrap_or_default();
alexdevteam's avatar
alexdevteam committed
        let mut shape = None;
        let mut properties = HashMap::new();

        parse_tag!(parser, "object", {
            "ellipse" => |_| {
                shape = Some(ObjectShape::Ellipse {
                    width,
                    height,
alexdevteam's avatar
alexdevteam committed
                });
                Ok(())
            },
            "polyline" => |attrs| {
                shape = Some(ObjectData::new_polyline(attrs)?);
alexdevteam's avatar
alexdevteam committed
                Ok(())
            },
            "polygon" => |attrs| {
                shape = Some(ObjectData::new_polygon(attrs)?);
alexdevteam's avatar
alexdevteam committed
                Ok(())
            },
            "point" => |_| {
                shape = Some(ObjectShape::Point(x, y));
alexdevteam's avatar
alexdevteam committed
                Ok(())
            },
            "properties" => |_| {
                properties = parse_properties(parser)?;
                Ok(())
            },
        });

        let shape = shape.unwrap_or(ObjectShape::Rect { width, height });
alexdevteam's avatar
alexdevteam committed

        #[allow(deprecated)]
        Ok(ObjectData {
            id,
            tile,
            name,
            obj_type,
            width,
            height,
            rotation,
            visible,
            shape,
            properties,
alexdevteam's avatar
alexdevteam committed
        })
    }
alexdevteam's avatar
alexdevteam committed

impl ObjectData {
    fn new_polyline(attrs: Vec<OwnedAttribute>) -> Result<ObjectShape> {
Alejandro Perea's avatar
Alejandro Perea committed
        let s = get_attrs!(
alexdevteam's avatar
alexdevteam committed
            attrs,
            required: [
                ("points", points, Some),
alexdevteam's avatar
alexdevteam committed
            ],
            Error::MalformedAttributes("A polyline must have points".to_string())
alexdevteam's avatar
alexdevteam committed
        );
        let points = ObjectData::parse_points(s)?;
        Ok(ObjectShape::Polyline { points })
alexdevteam's avatar
alexdevteam committed
    }

    fn new_polygon(attrs: Vec<OwnedAttribute>) -> Result<ObjectShape> {
Alejandro Perea's avatar
Alejandro Perea committed
        let s = get_attrs!(
alexdevteam's avatar
alexdevteam committed
            attrs,
            required: [
                ("points", points, Some),
alexdevteam's avatar
alexdevteam committed
            ],
            Error::MalformedAttributes("A polygon must have points".to_string())
alexdevteam's avatar
alexdevteam committed
        );
        let points = ObjectData::parse_points(s)?;
        Ok(ObjectShape::Polygon { points })
alexdevteam's avatar
alexdevteam committed
    }

    fn parse_points(s: String) -> Result<Vec<(f32, f32)>> {
alexdevteam's avatar
alexdevteam committed
        let pairs = s.split(' ');
        pairs
            .map(|point| point.split(','))
            .map(|components| {
                let v: Vec<&str> = components.collect();
                if v.len() != 2 {
                    return Err(Error::MalformedAttributes(
                        "one of a polyline's points does not have an x and y coordinate"
                            .to_string(),
                    ));
                }
                let (x, y) = (v[0].parse().ok(), v[1].parse().ok());
                match (x, y) {
                    (Some(x), Some(y)) => Ok((x, y)),
                    _ => Err(Error::MalformedAttributes(
                        "one of polyline's points does not have i32eger coordinates".to_string(),
                    )),
                }
            })
            .collect()
alexdevteam's avatar
alexdevteam committed
    }
}
map_wrapper!(
    #[doc = "Wrapper over an [`ObjectData`] that contains both a reference to the data as well as
    to the map it is contained in."]
    Object => ObjectData
);

impl<'map> Object<'map> {
    /// Returns the tile that the object is using as image, if any.
    pub fn get_tile(&self) -> Option<LayerTile<'map>> {
            .as_ref()
            .map(|tile| LayerTile::new(self.map, tile))
    }