diff --git a/packages/tiled/lib/src/chunk.dart b/packages/tiled/lib/src/chunk.dart index e306604..ffdf192 100644 --- a/packages/tiled/lib/src/chunk.dart +++ b/packages/tiled/lib/src/chunk.dart @@ -17,7 +17,7 @@ part of tiled; /// The data inside is a compressed (encoded) representation of a list /// (that sequentially represents a matrix) of integers representing /// [Gid]s. -class Chunk { +class Chunk with Exportable { List data; int x; @@ -59,4 +59,26 @@ class Chunk { return Chunk(data: data, x: x, y: y, width: width, height: height); } + + @override + ExportElement export({FileEncoding? encoding, Compression? compression}) { + final common = { + 'x': x.toExport(), + 'y': y.toExport(), + 'width': width.toExport(), + 'height': height.toExport(), + }; + + return ExportElement( + 'chunk', + common, + { + 'data': ExportTileData( + data: data, + compression: compression, + encoding: encoding, + ), + }, + ); + } } diff --git a/packages/tiled/lib/src/common/color.dart b/packages/tiled/lib/src/common/color.dart new file mode 100644 index 0000000..cb44af6 --- /dev/null +++ b/packages/tiled/lib/src/common/color.dart @@ -0,0 +1,37 @@ +part of tiled; + +/// Basic data class holding a Color in ARGB format. +/// This can be converted to dart:ui's Color using the flame_tiled package +class ColorData extends ExportValue { + static int _sub(int hex, int index) => (hex >> index * 8) & 0x000000ff; + + final int red; + final int green; + final int blue; + final int alpha; + + /// Parses the Color from an int using the lower 32-bits and tiled's format: 0xaarrggbb + ColorData.hex(int hex) + : alpha = _sub(hex, 3), + red = _sub(hex, 2), + green = _sub(hex, 1), + blue = _sub(hex, 0); + + const ColorData.rgb(this.red, this.green, this.blue, [this.alpha = 255]) + : assert(red >= 0 && red <= 255), + assert(green >= 0 && green <= 255), + assert(blue >= 0 && blue <= 255), + assert(alpha >= 0 && alpha <= 255); + + static String _hex(int value) { + return value.toRadixString(16).padLeft(2, '0'); + } + + String get export => '#${_hex(alpha)}${_hex(red)}${_hex(green)}${_hex(blue)}'; + + @override + String get json => export; + + @override + String get xml => export; +} diff --git a/packages/tiled/lib/src/common/frame.dart b/packages/tiled/lib/src/common/frame.dart index 72e4633..c545172 100644 --- a/packages/tiled/lib/src/common/frame.dart +++ b/packages/tiled/lib/src/common/frame.dart @@ -8,7 +8,7 @@ part of tiled; /// * tileid: The local ID of a tile within the parent . /// * duration: How long (in milliseconds) this frame should be displayed /// before advancing to the next frame. -class Frame { +class Frame with Exportable { int tileId; int duration; @@ -22,4 +22,14 @@ class Frame { tileId: parser.getInt('tileid'), duration: parser.getInt('duration'), ); + + @override + ExportResolver export() => ExportElement( + 'frame', + { + 'tileid': tileId.toExport(), + 'duration': duration.toExport(), + }, + {}, + ); } diff --git a/packages/tiled/lib/src/common/gid.dart b/packages/tiled/lib/src/common/gid.dart index 69fdd34..247a288 100644 --- a/packages/tiled/lib/src/common/gid.dart +++ b/packages/tiled/lib/src/common/gid.dart @@ -32,12 +32,17 @@ part of tiled; /// When rendering a tile, the order of operation matters. The diagonal flip /// (x/y axis swap) is done first, followed by the horizontal and vertical /// flips. -class Gid { +class Gid extends ExportValue { static const int flippedHorizontallyFlag = 0x80000000; static const int flippedVerticallyFlag = 0x40000000; static const int flippedDiagonallyFlag = 0x20000000; static const int flippedAntiDiagonallyFlag = 0x10000000; + static const int flagBits = flippedHorizontallyFlag | + flippedVerticallyFlag | + flippedDiagonallyFlag | + flippedAntiDiagonallyFlag; + final int tile; final Flips flips; @@ -86,4 +91,17 @@ class Gid { }); }); } + + int export() => + (tile & ~flagBits) | + (flips.horizontally ? flippedHorizontallyFlag : 0) | + (flips.vertically ? flippedVerticallyFlag : 0) | + (flips.diagonally ? flippedDiagonallyFlag : 0) | + (flips.antiDiagonally ? flippedAntiDiagonallyFlag : 0); + + @override + int get json => export(); + + @override + String get xml => export().toString(); } diff --git a/packages/tiled/lib/src/common/iterable.dart b/packages/tiled/lib/src/common/iterable.dart new file mode 100644 index 0000000..443f448 --- /dev/null +++ b/packages/tiled/lib/src/common/iterable.dart @@ -0,0 +1,17 @@ +part of tiled; + +extension Grouping on Iterable { + Map> groupBy(K Function(V value) key) { + final out = >{}; + for (final v in this) { + final k = key(v); + if (!out.containsKey(k)) { + out[k] = [v]; + } else { + out[k]!.add(v); + } + } + + return out; + } +} diff --git a/packages/tiled/lib/src/common/map.dart b/packages/tiled/lib/src/common/map.dart new file mode 100644 index 0000000..ca34fba --- /dev/null +++ b/packages/tiled/lib/src/common/map.dart @@ -0,0 +1,6 @@ +part of tiled; + +extension Null on Map { + Map nonNulls() => + {for (final e in entries.where((e) => e.value is V)) e.key: e.value as V}; +} diff --git a/packages/tiled/lib/src/common/property.dart b/packages/tiled/lib/src/common/property.dart index b52e9ac..4758f1c 100644 --- a/packages/tiled/lib/src/common/property.dart +++ b/packages/tiled/lib/src/common/property.dart @@ -12,7 +12,7 @@ part of tiled; /// (default string is “”, default number is 0, default boolean is “false”, /// default color is #00000000, default file is “.” (the current file’s /// parent directory)) -class Property { +class Property with Exportable { String name; PropertyType type; T value; @@ -37,7 +37,7 @@ class Property { case PropertyType.color: return ColorProperty( name: name, - value: parser.getColor('value', defaults: const Color(0x00000000)), + value: parser.getColor('value', defaults: ColorData.hex(0x00000000)), hexValue: parser.getString('value', defaults: '#00000000'), ); @@ -86,6 +86,19 @@ class Property { ); } } + + ExportValue get exportValue => value.toString().toExport(); + + @override + ExportElement export() => ExportElement( + 'property', + { + 'name': name.toExport(), + 'type': type.name.toExport(), + 'value': exportValue, + }, + {}, + ); } /// A wrapper for a Tiled property set @@ -156,7 +169,7 @@ class ObjectProperty extends Property { } /// [value] is the color -class ColorProperty extends Property { +class ColorProperty extends Property { final String hexValue; ColorProperty({ @@ -164,6 +177,9 @@ class ColorProperty extends Property { required super.value, required this.hexValue, }) : super(type: PropertyType.color); + + @override + ExportValue get exportValue => value; } /// [value] is the string text @@ -180,6 +196,10 @@ class FileProperty extends Property { required super.name, required super.value, }) : super(type: PropertyType.file); + + @override + ExportValue get exportValue => + value.isNotEmpty ? value.toExport() : '.'.toExport(); } /// [value] is the integer number diff --git a/packages/tiled/lib/src/common/tiled_image.dart b/packages/tiled/lib/src/common/tiled_image.dart index ed4bdad..eef2db2 100644 --- a/packages/tiled/lib/src/common/tiled_image.dart +++ b/packages/tiled/lib/src/common/tiled_image.dart @@ -18,7 +18,7 @@ part of tiled; /// when the image changes) /// * height: The image height in pixels (optional) @immutable -class TiledImage { +class TiledImage with Exportable { final String? source; final String? format; final int? width; @@ -53,4 +53,19 @@ class TiledImage { @override int get hashCode => source.hashCode; + + @override + ExportElement export() => ExportElement( + 'image', + { + 'width': width?.toExport(), + 'height': height?.toExport(), + 'format': format?.toExport(), + 'source': source?.toExport(), + 'trans': trans?.toExport(), + }.nonNulls(), + { + // missing data, tiled.dart does not support embedded images + }, + ); } diff --git a/packages/tiled/lib/src/exporter/export_element.dart b/packages/tiled/lib/src/exporter/export_element.dart new file mode 100644 index 0000000..255e25e --- /dev/null +++ b/packages/tiled/lib/src/exporter/export_element.dart @@ -0,0 +1,100 @@ +part of tiled; + +/// Base type for all other types. This is necessary to allow for Lists +abstract class ExportObject {} + +/// Base type for everything that returns a single exported value +abstract class ExportResolver implements ExportObject { + /// Creates an XmlNode representing the value + XmlNode exportXml(); + + /// Creates an JsonObject representing this value + JsonObject exportJson(); +} + +/// Element/object for exporting with default structure +class ExportElement implements ExportResolver { + final String name; + final Map fields; + final Map children; + final CustomProperties? properties; + + const ExportElement( + this.name, + this.fields, + this.children, [ + this.properties, + ]); + + @override + XmlElement exportXml() => XmlElement( + XmlName(name), + fields.entries.map((e) => XmlAttribute(XmlName(e.key), e.value.xml)), + [ + ...children.values.expand((e) { + if (e is ExportList) { + return e.map((e) => e.exportXml()); + } + + if (e is ExportResolver) { + return [e.exportXml()]; + } + + throw 'Bad State: ExportObject switch should have been exhaustive'; + }), + if (properties != null && properties!.isNotEmpty) + XmlElement( + XmlName('properties'), + [], + properties!.map((e) => e.exportXml()).toList(), + ), + ], + ); + + @override + JsonMap exportJson() => JsonMap({ + ...fields.map((key, value) => MapEntry(key, value.exportJson())), + ...children.map((key, e) { + if (e is ExportList) { + return MapEntry(key, JsonList(e.map((e) => e.exportJson()))); + } + + if (e is ExportResolver) { + return MapEntry(key, e.exportJson()); + } + + throw 'Bad State: ExportChild switch should have been exhaustive'; + }), + if (properties != null) + 'properties': JsonList( + properties!.map((e) => e.exportJson()), + ), + }); +} + +/// Splits export tree according to the type of export using an ExportResolver +/// for each. +class ExportFormatSpecific implements ExportResolver { + final ExportResolver xml; + final ExportResolver json; + + const ExportFormatSpecific({required this.xml, required this.json}); + + @override + JsonObject exportJson() => json.exportJson(); + + @override + XmlNode exportXml() => xml.exportXml(); +} + +/// List of ExportResolvers. This is used to get Lists into json, while simply +/// expanding in xml +class ExportList extends DelegatingList + implements ExportObject { + ExportList(Iterable base) : super(base.toList()); + + ExportList.from(Iterable source) + : super( + source.map((e) => e).toList(), + ); +} diff --git a/packages/tiled/lib/src/exporter/export_value.dart b/packages/tiled/lib/src/exporter/export_value.dart new file mode 100644 index 0000000..71cab78 --- /dev/null +++ b/packages/tiled/lib/src/exporter/export_value.dart @@ -0,0 +1,82 @@ +part of tiled; + +/// Export tree leafs. Used for exporting values like strings, bools, etc. +abstract class ExportValue implements ExportResolver { + const ExportValue(); + + String get xml; + + T get json; + + @override + XmlNode exportXml() => XmlText(xml); + + @override + JsonValue exportJson() => JsonValue(json); +} + +/// Literally exports the given value +class ExportLiteral extends ExportValue { + final T value; + + const ExportLiteral(this.value); + + @override + T get json => value; + + @override + String get xml => value.toString(); +} + +/// Conversion to export +extension ExportableString on String { + ExportValue toExport() => ExportLiteral(this); +} + +/// Conversion to export +extension ExportableNum on num { + ExportValue toExport() => ExportLiteral(this); +} + +/// Encodes bool as 0 and 1 +class _ExportableBool extends ExportValue { + final bool value; + + _ExportableBool(this.value); + + @override + bool get json => value; + + @override + String get xml => (value ? 1 : 0).toString(); +} + +/// Conversion to export +extension ExportableBool on bool { + ExportValue toExport() => _ExportableBool(this); +} + +/// Encodes a list of [Point]s as a json array or space seperated xml string +class _ExportablePointList extends ExportValue>> { + final List points; + + _ExportablePointList(this.points); + + @override + List> get json => points + .map( + (e) => { + 'x': e.x, + 'y': e.y, + }, + ) + .toList(); + + @override + String get xml => points.map((e) => '${e.x},${e.y}').join(' '); +} + +/// Conversion to export +extension ExportablePointList on List { + ExportValue toExport() => _ExportablePointList(this); +} diff --git a/packages/tiled/lib/src/exporter/exportable.dart b/packages/tiled/lib/src/exporter/exportable.dart new file mode 100644 index 0000000..2d97fda --- /dev/null +++ b/packages/tiled/lib/src/exporter/exportable.dart @@ -0,0 +1,18 @@ +part of tiled; + +/// Interface implemented by classes that can be exported. Instead of +/// implementing [ExportResolver]'s methods on every class this mixins allows +/// for these methods to be supplied by a proxy ExportResolver. +/// +/// This is the default mechanism as the xml/json structures are fairly +/// predictable. +abstract class Exportable implements ExportResolver { + /// Returns the proxy usually an [ExportElement] + ExportResolver export(); + + @override + XmlNode exportXml() => export().exportXml(); + + @override + JsonObject exportJson() => export().exportJson(); +} diff --git a/packages/tiled/lib/src/exporter/json.dart b/packages/tiled/lib/src/exporter/json.dart new file mode 100644 index 0000000..99d0358 --- /dev/null +++ b/packages/tiled/lib/src/exporter/json.dart @@ -0,0 +1,29 @@ +part of tiled; + +/// Basic object in json structure, either a [JsonElement] or a [JsonValue] +abstract class JsonObject {} + +/// Basic elements, eiter a [JsonList] or a [JsonMap]. +/// Elements can be the root of a json file. +abstract class JsonElement implements JsonObject {} + +/// List in json +class JsonList extends DelegatingList implements JsonElement { + JsonList([Iterable? base]) : super(base?.toList() ?? []); + + List get value => this; +} + +/// Map in json +class JsonMap extends DelegatingMap implements JsonElement { + JsonMap([super.base = const {}]); + + Map get value => this; +} + +/// Arbitrary value that is a json type or it will be encoded using [jsonEncode] +class JsonValue implements JsonObject { + final T value; + + JsonValue(this.value); +} diff --git a/packages/tiled/lib/src/exporter/tile_data.dart b/packages/tiled/lib/src/exporter/tile_data.dart new file mode 100644 index 0000000..4975d50 --- /dev/null +++ b/packages/tiled/lib/src/exporter/tile_data.dart @@ -0,0 +1,86 @@ +part of tiled; + +/// Export element encoding the tile data +class ExportTileData with Exportable { + final FileEncoding? encoding; + final Compression? compression; + final List data; + + const ExportTileData({ + required this.data, + required this.encoding, + required this.compression, + }); + + @override + ExportResolver export() { + String? encodedData; + switch (encoding) { + case null: + break; + case FileEncoding.csv: + encodedData = encodeCsv(); + break; + case FileEncoding.base64: + encodedData = encodeBase64(); + break; + } + + return ExportFormatSpecific( + xml: ExportElement( + 'data', + { + if (encoding != null) 'encoding': encoding!.name.toExport(), + if (compression != null) 'compression': compression!.name.toExport(), + }, + { + if (encodedData == null) + 'tiles': exportTiles() + else + 'data': encodedData.toExport(), + }, + ), + json: ExportLiteral(data), + ); + } + + /// Exports tiles for xml elements + ExportList exportTiles() => ExportList( + data.map( + (gid) => ExportElement( + 'tile', + {'gid': gid.toExport()}, + {}, + ), + ), + ); + + /// Encodes tiles as csv + String encodeCsv() => data.join(', '); + + /// Encodes tiles as base64 + String encodeBase64() { + // Conversion to Uint8List + final uint32 = Uint32List.fromList(data); + final uint8 = uint32.buffer.asUint8List(); + + // Compression + List compressed; + switch (compression) { + case Compression.zlib: + compressed = const ZLibEncoder().encode(uint8); + break; + case Compression.gzip: + compressed = GZipEncoder().encode(uint8)!; + break; + case Compression.zstd: + throw UnsupportedError('zstd is an unsupported compression'); + case null: + compressed = uint8; + break; + } + + // encoding + return base64Encode(compressed); + } +} diff --git a/packages/tiled/lib/src/layer.dart b/packages/tiled/lib/src/layer.dart index ccff08d..ba64d2d 100644 --- a/packages/tiled/lib/src/layer.dart +++ b/packages/tiled/lib/src/layer.dart @@ -32,7 +32,7 @@ part of tiled; /// Defaults to 1. (since 1.5) /// /// Can contain at most one: , -abstract class Layer { +abstract class Layer with Exportable { /// Incremental ID - unique across all layers int? id; @@ -77,11 +77,11 @@ abstract class Layer { /// any graphics drawn by this layer or any child layers (optional). String? tintColorHex; - /// [Color] that is multiplied with any graphics drawn by this layer or any + /// [ColorData] that is multiplied with any graphics drawn by this layer or any /// child layers (optional). /// /// Parsed from [tintColorHex], will be null if parsing fails for any reason. - Color? tintColor; + ColorData? tintColor; /// The opacity of the layer as a value from 0 to 1. Defaults to 1. double opacity; @@ -350,6 +350,9 @@ abstract class Layer { } return uint32; } + + @override + ExportElement export(); } class TileLayer extends Layer { @@ -417,10 +420,45 @@ class TileLayer extends Layer { } return Gid.generate(data, width, height); } + + @override + ExportElement export() => ExportElement( + 'layer', + { + 'class': class_?.toExport(), + 'name': name.toExport(), + 'height': height.toExport(), + 'width': width.toExport(), + 'x': x.toExport(), + 'y': y.toExport(), + 'opacity': opacity.toExport(), + 'type': type.name.toExport(), + 'visible': visible.toExport(), + 'compression': (data == null ? compression : null)?.name.toExport(), + }.nonNulls(), + { + if (chunks != null) + 'chunks': ExportList( + chunks!.map( + (e) => e.export( + encoding: encoding, + compression: compression, + ), + ), + ), + if (data != null) + 'data': ExportTileData( + data: data!, + compression: compression, + encoding: encoding, + ), + }, + properties, + ); } class ObjectGroup extends Layer { - static const defaultColor = Color.fromARGB(255, 160, 160, 164); + static const defaultColor = ColorData.rgb(160, 160, 164); static const defaultColorHex = '%a0a0a4'; /// topdown (default) or index (indexOrder). @@ -433,12 +471,12 @@ class ObjectGroup extends Layer { /// this group. (defaults to gray (“#a0a0a4”)) String colorHex; - /// [Color] used to display the objects in this group. + /// [ColorData] used to display the objects in this group. /// (defaults to gray (“#a0a0a4”)) /// /// Parsed from [colorHex], will be fallback to [defaultColor] if parsing /// fails for any reason. - Color color; + ColorData color; ObjectGroup({ super.id, @@ -464,6 +502,32 @@ class ObjectGroup extends Layer { }) : super( type: LayerType.objectGroup, ); + + @override + ExportElement export() => ExportElement( + 'objectgroup', + { + 'id': id?.toExport(), + 'name': name.toExport(), + 'class': class_?.toExport(), + 'type': type.name.toExport(), + 'x': x.toExport(), + 'y': y.toExport(), + 'color': color, + 'tintcolor': tintColor, + 'opacity': opacity.toExport(), + 'visible': (visible ? 1 : 0).toExport(), + 'offsetx': offsetX.toExport(), + 'offsety': offsetY.toExport(), + 'parallaxx': parallaxX.toExport(), + 'parallaxy': parallaxY.toExport(), + 'draworder': drawOrder.name.toExport(), + }.nonNulls(), + { + 'objects': ExportList.from(objects), + }, + properties, + ); } class ImageLayer extends Layer { @@ -474,11 +538,11 @@ class ImageLayer extends Layer { /// (optional). String? transparentColorHex; - /// [Color] to be rendered as transparent (optional). + /// [ColorData] to be rendered as transparent (optional). /// /// Parsed from [transparentColorHex], will be null if parsing fails for any /// reason. - Color? transparentColor; + ColorData? transparentColor; /// Whether or not to repeat the image on the X-axis bool repeatX; @@ -511,6 +575,32 @@ class ImageLayer extends Layer { }) : super( type: LayerType.imageLayer, ); + + @override + ExportElement export() => ExportElement( + 'imagelayer', + { + 'id': id?.toExport(), + 'name': name.toExport(), + 'class': class_?.toExport(), + 'type': type.name.toExport(), + 'x': x.toExport(), + 'y': y.toExport(), + 'tintcolor': tintColor, + 'opacity': opacity.toExport(), + 'visible': (visible ? 1 : 0).toExport(), + 'offsetx': offsetX.toExport(), + 'offsety': offsetY.toExport(), + 'parallaxx': parallaxX.toExport(), + 'parallaxy': parallaxY.toExport(), + 'repeatx': repeatX.toExport(), + 'repeaty': repeatY.toExport(), + }.nonNulls(), + { + 'image': image, + }, + properties, + ); } class Group extends Layer { @@ -538,4 +628,26 @@ class Group extends Layer { }) : super( type: LayerType.imageLayer, ); + + @override + ExportElement export() => ExportElement( + 'group', + { + 'id': id?.toExport(), + 'name': name.toExport(), + 'class': class_?.toExport(), + 'type': type.name.toExport(), + 'tintcolor': tintColor, + 'opacity': opacity.toExport(), + 'visible': (visible ? 1 : 0).toExport(), + 'offsetx': offsetX.toExport(), + 'offsety': offsetY.toExport(), + 'parallaxx': parallaxX.toExport(), + 'parallaxy': parallaxY.toExport(), + }.nonNulls(), + { + 'layers': ExportList.from(layers), + }, + properties, + ); } diff --git a/packages/tiled/lib/src/objects/text.dart b/packages/tiled/lib/src/objects/text.dart index 5ac76e3..4e8599b 100644 --- a/packages/tiled/lib/src/objects/text.dart +++ b/packages/tiled/lib/src/objects/text.dart @@ -37,7 +37,7 @@ part of tiled; /// /// If the text is larger than the object’s bounds, it is clipped to the bounds /// of the object. -class Text { +class Text with Exportable { String fontFamily; int pixelSize; String color; @@ -84,4 +84,21 @@ class Text { kerning: parser.getBool('kerning', defaults: true), wrap: parser.getBool('wrap', defaults: false), ); + + @override + ExportElement export() => ExportElement('text', { + 'fontfamily': fontFamily.toExport(), + 'pixelsize': pixelSize.toExport(), + 'wrap': wrap.toExport(), + 'color': color.toExport(), + 'bold': bold.toExport(), + 'italic': italic.toExport(), + 'underline': underline.toExport(), + 'strikeout': strikeout.toExport(), + 'kerning': kerning.toExport(), + 'halign': hAlign.name.toExport(), + 'valign': vAlign.name.toExport(), + }, { + 'text': text.toExport(), + }); } diff --git a/packages/tiled/lib/src/objects/tiled_object.dart b/packages/tiled/lib/src/objects/tiled_object.dart index 8cf4ce8..4b18ee0 100644 --- a/packages/tiled/lib/src/objects/tiled_object.dart +++ b/packages/tiled/lib/src/objects/tiled_object.dart @@ -43,7 +43,7 @@ part of tiled; /// /// Can contain at most one: , (since 0.9), /// (since 1.1), , , (since 1.0) -class TiledObject { +class TiledObject with Exportable { int id; String name; String type; @@ -93,9 +93,13 @@ class TiledObject { }); bool get isPolyline => polyline.isNotEmpty; + bool get isPolygon => polygon.isNotEmpty; + bool get isPoint => point; + bool get isEllipse => ellipse; + bool get isRectangle => rectangle; factory TiledObject.parse(Parser parser) { @@ -170,4 +174,61 @@ class TiledObject { }, ); } + + @override + ExportResolver export() { + final common = { + 'id': id.toExport(), + 'name': name.toExport(), + 'type': type.toExport(), + 'x': x.toExport(), + 'y': y.toExport(), + 'width': width.toExport(), + 'height': height.toExport(), + 'rotation': rotation.toExport(), + 'gid': gid?.toExport(), + 'visible': visible.toExport(), + // 'template': not supported, not possible to support currently, see #73 + }.nonNulls(); + + return ExportFormatSpecific( + xml: ExportElement( + 'object', + common, + { + if (ellipse) 'ellipse': const ExportElement('ellipse', {}, {}), + if (point) 'point': const ExportElement('point', {}, {}), + if (polygon.isNotEmpty) + 'polygon': ExportElement( + 'polygon', + { + 'points': polygon.toExport(), + }, + {}, + ), + if (polyline.isNotEmpty) + 'polyline': ExportElement( + 'polyline', + { + 'points': polygon.toExport(), + }, + {}, + ), + if (text != null) 'text': text!, + }, + properties), + json: ExportElement( + 'object', + { + ...common, + if (ellipse) 'ellipse': ellipse.toExport(), + if (point) 'point': point.toExport(), + if (polygon.isNotEmpty) 'polygon': polygon.toExport(), + if (polyline.isNotEmpty) 'polyline': polyline.toExport(), + }, + {}, + properties, + ), + ); + } } diff --git a/packages/tiled/lib/src/parser.dart b/packages/tiled/lib/src/parser.dart index 3cd278a..2bbafd5 100644 --- a/packages/tiled/lib/src/parser.dart +++ b/packages/tiled/lib/src/parser.dart @@ -197,7 +197,7 @@ abstract class Parser { return result; } - Color? getColorOrNull(String name, {Color? defaults}) { + ColorData? getColorOrNull(String name, {ColorData? defaults}) { final tiledColor = getStringOrNull(name); // Tiled colors are stored as either ARGB or RGB hex values, so we can @@ -212,13 +212,13 @@ abstract class Parser { } if (colorValue != null) { - return Color(colorValue); + return ColorData.hex(colorValue); } else { return defaults; } } - Color getColor(String name, {Color? defaults}) { + ColorData getColor(String name, {ColorData? defaults}) { final result = getColorOrNull(name, defaults: defaults); if (result == null) { throw ParsingException(name, null, 'Missing required color field'); diff --git a/packages/tiled/lib/src/tiled_map.dart b/packages/tiled/lib/src/tiled_map.dart index f976762..9ee666c 100644 --- a/packages/tiled/lib/src/tiled_map.dart +++ b/packages/tiled/lib/src/tiled_map.dart @@ -55,7 +55,7 @@ part of tiled; /// /// Can contain any number: , , , , /// (since 1.0), (since 1.3) -class TiledMap { +class TiledMap with Exportable { TileMapType type; String version; String? tiledVersion; @@ -74,12 +74,12 @@ class TiledMap { /// behind all other layers (optional). String? backgroundColorHex; - /// [Color] to be rendered as a solid color behind all other layers + /// [ColorData] to be rendered as a solid color behind all other layers /// (optional). /// /// Parsed from [backgroundColorHex], will be null if parsing fails for any /// reason. - Color? backgroundColor; + ColorData? backgroundColor; int compressionLevel; @@ -358,4 +358,37 @@ class TiledMap { properties: properties, ); } + + @override + ExportElement export() => ExportElement( + 'map', + { + 'version': '1.10'.toExport(), + 'type': type.name.toExport(), + + 'orientation': orientation?.name.toExport(), + 'renderorder': renderOrder.name.toExport(), + 'compressionlevel': '-1'.toExport(), + + 'width': width.toExport(), + 'height': height.toExport(), + 'tilewidth': tileWidth.toExport(), + 'tileheight': tileHeight.toExport(), + + 'hexsidelength': hexSideLength?.toExport(), + 'staggeraxis': staggerAxis?.name.toExport(), + 'staggerindex': staggerIndex?.name.toExport(), + // 'parallaxoriginx': , 'parallaxoriginy': , Not supplied by this class + + 'backgroundcolor': backgroundColor, + 'nextlayerid': nextLayerId?.toExport(), + 'nextobjectid': nextObjectId?.toExport(), + 'infinite': infinite.toExport(), + }.nonNulls(), + { + 'tilesets': ExportList.from(tilesets), + 'layers': ExportList.from(layers), + }, + properties, + ); } diff --git a/packages/tiled/lib/src/tileset/grid.dart b/packages/tiled/lib/src/tileset/grid.dart index 3dc26cf..8c90812 100644 --- a/packages/tiled/lib/src/tileset/grid.dart +++ b/packages/tiled/lib/src/tileset/grid.dart @@ -12,7 +12,7 @@ part of tiled; /// /// This element is only used in case of isometric orientation, and determines /// how tile overlays for terrain and collision information are rendered. -class Grid { +class Grid with Exportable { int width; int height; GridOrientation orientation; @@ -29,4 +29,15 @@ class Grid { height: parser.getInt('height'), orientation: parser.getGridOrientation('orientation'), ); + + @override + ExportResolver export() => ExportElement( + 'grid', + { + 'orientation': orientation.name.toExport(), + 'width': width.toExport(), + 'height': height.toExport(), + }, + {}, + ); } diff --git a/packages/tiled/lib/src/tileset/tile.dart b/packages/tiled/lib/src/tileset/tile.dart index c2c1c74..8a04033 100644 --- a/packages/tiled/lib/src/tileset/tile.dart +++ b/packages/tiled/lib/src/tileset/tile.dart @@ -18,7 +18,7 @@ part of tiled; /// /// Can contain at most one: , (since 0.9), , /// . -class Tile { +class Tile with Exportable { int localId; String? type; double probability; @@ -27,7 +27,7 @@ class Tile { List terrain; TiledImage? image; - Rect? imageRect; + Rectangle? imageRect; Layer? objectGroup; List animation; CustomProperties properties; @@ -50,38 +50,88 @@ class Tile { /// Will be same as [type]. String? get class_ => type; - Tile.parse(Parser parser) - : this( - localId: parser.getInt('id'), + factory Tile.parse(Parser parser) { + final image = parser.getSingleChildOrNullAs('image', TiledImage.parse); + final x = parser.getDoubleOrNull('x'); + final y = parser.getDoubleOrNull('y'); + final width = parser.getDoubleOrNull('width'); + final height = parser.getDoubleOrNull('height'); - /// Tiled 1.9 "type" has been moved to "class" - type: - parser.getStringOrNull('class') ?? parser.getStringOrNull('type'), + final imageRect = [image, x, y, width, height].contains(null) + ? null + : Rectangle(x!, y!, width!, height!); - probability: parser.getDouble('probability', defaults: 0), - terrain: parser - .getStringOrNull('terrain') - ?.split(',') - .map((str) => str.isEmpty ? null : int.parse(str)) - .toList() ?? - [], - image: parser.getSingleChildOrNullAs('image', TiledImage.parse), - imageRect: Rect.fromLTWH( - parser.getDoubleOrNull('x') ?? 0, - parser.getDoubleOrNull('y') ?? 0, - parser.getDoubleOrNull('width') ?? 0, - parser.getDoubleOrNull('height') ?? 0, - ), - objectGroup: - parser.getSingleChildOrNullAs('objectgroup', Layer.parse), - animation: parser.formatSpecificParsing( - (json) => json.getChildrenAs('animation', Frame.parse), - (xml) => - xml - .getSingleChildOrNull('animation') - ?.getChildrenAs('frame', Frame.parse) ?? - [], - ), - properties: parser.getProperties(), - ); + return Tile( + localId: parser.getInt('id'), + + /// Tiled 1.9 "type" has been moved to "class" + type: parser.getStringOrNull('class') ?? parser.getStringOrNull('type'), + + probability: parser.getDouble('probability', defaults: 0), + terrain: parser + .getStringOrNull('terrain') + ?.split(',') + .map((str) => str.isEmpty ? null : int.parse(str)) + .toList() ?? + [], + image: image, + imageRect: imageRect, + objectGroup: parser.getSingleChildOrNullAs('objectgroup', Layer.parse), + animation: parser.formatSpecificParsing( + (json) => json.getChildrenAs('animation', Frame.parse), + (xml) => + xml + .getSingleChildOrNull('animation') + ?.getChildrenAs('frame', Frame.parse) ?? + [], + ), + properties: parser.getProperties(), + ); + } + + @override + ExportResolver export() { + final fields = { + 'id': localId.toExport(), + 'class': type?.toExport(), + 'probability': probability.toExport(), + 'x': imageRect?.left.toExport(), + 'y': imageRect?.top.toExport(), + 'width': imageRect?.width.toExport(), + 'height': imageRect?.height.toExport(), + }.nonNulls(); + + final children = { + 'image': image, + 'objectgroup': objectGroup, + }.nonNulls(); + + return ExportFormatSpecific( + xml: ExportElement( + 'tile', + fields, + { + ...children, + if (animation.isNotEmpty) + 'animations': ExportElement( + 'animation', + {}, + { + 'frames': ExportList.from(animation), + }, + ), + }, + properties, + ), + json: ExportElement( + 'tile', + fields, + { + ...children, + 'animation': ExportList.from(animation), + }, + properties, + ), + ); + } } diff --git a/packages/tiled/lib/src/tileset/tile_offset.dart b/packages/tiled/lib/src/tileset/tile_offset.dart index 3b6c985..94c5f19 100644 --- a/packages/tiled/lib/src/tileset/tile_offset.dart +++ b/packages/tiled/lib/src/tileset/tile_offset.dart @@ -11,7 +11,7 @@ part of tiled; /// This element is used to specify an offset in pixels, to be applied when /// drawing a tile from the related tileset. /// When not present, no offset is applied. -class TileOffset { +class TileOffset with Exportable { int x; int y; @@ -25,4 +25,14 @@ class TileOffset { x: parser.getInt('x', defaults: 0), y: parser.getInt('y', defaults: 0), ); + + @override + ExportResolver export() => ExportElement( + 'tileoffset', + { + 'x': x.toExport(), + 'y': y.toExport(), + }, + {}, + ); } diff --git a/packages/tiled/lib/src/tileset/tileset.dart b/packages/tiled/lib/src/tileset/tileset.dart index b26584e..d62cda0 100644 --- a/packages/tiled/lib/src/tileset/tileset.dart +++ b/packages/tiled/lib/src/tileset/tileset.dart @@ -42,7 +42,7 @@ part of tiled; /// (since 1.5) /// /// Can contain any number: -class Tileset { +class Tileset with Exportable { int? firstGid; String? source; String? name; @@ -230,7 +230,7 @@ class Tileset { final tiles = []; for (var i = 0; i < tileCount; ++i) { - Rect? imageRect; + Rectangle? imageRect; if (columns != null && columns != 0 && @@ -239,7 +239,7 @@ class Tileset { final x = (i % columns) * tileWidth; final y = i ~/ columns * tileHeight; - imageRect = Rect.fromLTWH( + imageRect = Rectangle( x.toDouble(), y.toDouble(), tileWidth.toDouble(), @@ -256,9 +256,81 @@ class Tileset { if (tile.localId >= tiles.length) { tiles.add(tile); } else { + final generated = tiles[tile.localId]; + if (tile.imageRect == null) tile.imageRect = generated.imageRect; tiles[tile.localId] = tile; } } return tiles; } + + @override + ExportResolver export() => source == null + ? _export(false) + : ExportElement( + 'tileset', + { + 'firstgid': firstGid!.toExport(), + 'source': source!.toExport(), + }.nonNulls(), + {}, + ); + + ExportResolver exportExternal() => _export(true); + + ExportResolver _export(bool external) { + final fields = { + if (!external) 'firstgid': firstGid!.toExport(), + 'name': name!.toExport(), + 'class': type.name.toExport(), + 'type': type.name.toExport(), + + 'tilewidth': tileWidth?.toExport(), + 'tileheight': tileHeight?.toExport(), + 'spacing': spacing.toExport(), + 'margin': margin.toExport(), + + 'tilecount': tileCount?.toExport(), + 'columns': columns?.toExport(), + 'objectalignment': objectAlignment.name.toExport(), + // 'tilerendersize': , Not supported by this class + // 'fillmode': , Not supported by this class + }.nonNulls(); + + final common = { + 'image': image, + 'tiles': ExportList.from(tiles), + 'tileoffset': tileOffset, + 'grid': grid, + // 'terraintypes': , DEPRECATED + // 'transformations': ExportList.from(transformations), Not supported by this class + }.nonNulls(); + + final wangsets = ExportElement( + 'wangsets', + {}, + {'wangsets': ExportList.from(wangSets)}, + properties, + ); + + return ExportFormatSpecific( + xml: ExportElement( + 'tileset', + fields, + { + ...common, + if (wangSets.isNotEmpty) 'wangsets': wangsets, + }, + properties, + ), + json: ExportElement( + 'tileset', + fields, + { + ...common, + 'wangsets': ExportList.from(wangSets), + }, + ), + ); + } } diff --git a/packages/tiled/lib/src/tileset/wang/wang_color.dart b/packages/tiled/lib/src/tileset/wang/wang_color.dart index c5661f3..bd5fef7 100644 --- a/packages/tiled/lib/src/tileset/wang/wang_color.dart +++ b/packages/tiled/lib/src/tileset/wang/wang_color.dart @@ -13,7 +13,7 @@ part of tiled; /// others in case of multiple options. (defaults to 0) /// /// Can contain at most one: -class WangColor { +class WangColor with Exportable { String name; String color; int tile; @@ -37,4 +37,18 @@ class WangColor { probability: parser.getDouble('probability', defaults: 0), properties: parser.getProperties(), ); + + @override + ExportResolver export() => ExportElement( + 'wangcolor', + { + 'name': name.toExport(), + // 'class': , Not supported by this class + 'color': color.toExport(), + 'tile': tile.toExport(), + 'probability': probability.toExport(), + }, + {}, + properties, + ); } diff --git a/packages/tiled/lib/src/tileset/wang/wang_set.dart b/packages/tiled/lib/src/tileset/wang/wang_set.dart index 4a25355..5657533 100644 --- a/packages/tiled/lib/src/tileset/wang/wang_set.dart +++ b/packages/tiled/lib/src/tileset/wang/wang_set.dart @@ -15,7 +15,7 @@ part of tiled; /// Can contain up to 255: (since Tiled 1.5) /// /// Can contain any number: -class WangSet { +class WangSet with Exportable { String name; int tile; List cornerColors; @@ -53,4 +53,22 @@ class WangSet { properties: parser.getProperties(), ); } + + @override + ExportResolver export() { + final colors = cornerColors..addAll(edgeColors); + + return ExportElement( + 'wangset', + { + 'name': name.toExport(), + 'tile': tile.toExport(), + }, + { + 'colors': ExportList.from(colors), + 'wangtiles': ExportList.from(wangTiles), + }, + properties, + ); + } } diff --git a/packages/tiled/lib/src/tileset/wang/wang_tile.dart b/packages/tiled/lib/src/tileset/wang/wang_tile.dart index a069d5d..b530265 100644 --- a/packages/tiled/lib/src/tileset/wang/wang_tile.dart +++ b/packages/tiled/lib/src/tileset/wang/wang_tile.dart @@ -19,7 +19,7 @@ part of tiled; /// * hflip: Whether the tile is flipped horizontally (removed in Tiled 1.5). /// * vflip: Whether the tile is flipped vertically (removed in Tiled 1.5). /// * dflip: Whether the tile is flipped on its diagonal (removed in Tiled 1.5). -class WangTile { +class WangTile with Exportable { int tileId; List wangId; bool hFlip; @@ -60,4 +60,31 @@ class WangTile { } return uint32; } + + @override + ExportResolver export() { + final common = { + 'tileid': tileId.toExport(), + }; + + // Flips are deprecated! + return ExportFormatSpecific( + xml: ExportElement( + 'wangtile', + { + ...common, + 'wangid': wangId.join(',').toExport(), + }, + {}, + ), + json: ExportElement( + 'wangtile', + { + ...common, + 'wangid': ExportLiteral>(wangId), + }, + {}, + ), + ); + } } diff --git a/packages/tiled/lib/tiled.dart b/packages/tiled/lib/tiled.dart index 4907cc5..eb9dec0 100644 --- a/packages/tiled/lib/tiled.dart +++ b/packages/tiled/lib/tiled.dart @@ -4,7 +4,6 @@ import 'dart:collection'; import 'dart:convert'; import 'dart:math' show Rectangle; import 'dart:typed_data'; -import 'dart:ui'; import 'package:archive/archive.dart'; import 'package:collection/collection.dart'; @@ -12,13 +11,21 @@ import 'package:meta/meta.dart'; import 'package:xml/xml.dart'; part 'src/chunk.dart'; +part 'src/exporter/exportable.dart'; +part 'src/exporter/tile_data.dart'; +part 'src/exporter/export_value.dart'; +part 'src/exporter/export_element.dart'; +part 'src/exporter/json.dart'; part 'src/common/enums.dart'; part 'src/common/flips.dart'; part 'src/common/frame.dart'; part 'src/common/gid.dart'; +part 'src/common/iterable.dart'; +part 'src/common/map.dart'; part 'src/common/point.dart'; part 'src/common/property.dart'; part 'src/common/tiled_image.dart'; +part 'src/common/color.dart'; part 'src/editor_setting/chunk_size.dart'; part 'src/editor_setting/editor_setting.dart'; part 'src/editor_setting/export.dart'; diff --git a/packages/tiled/pubspec.yaml b/packages/tiled/pubspec.yaml index ca1c5c8..b61f9ab 100644 --- a/packages/tiled/pubspec.yaml +++ b/packages/tiled/pubspec.yaml @@ -1,5 +1,5 @@ name: tiled -version: 0.10.1 +version: 0.11.0 description: A Dart Tiled library. Parse your TMX files into useful representations. Compatible with Flame. homepage: https://github.com/flame-engine/tiled.dart @@ -8,13 +8,10 @@ environment: dependencies: archive: ^3.3.0 collection: ^1.16.0 - flutter: - sdk: flutter meta: ^1.7.0 xml: ^6.1.0 dev_dependencies: dartdoc: ^6.0.1 - flame_lint: ^0.2.0 - flutter_test: - sdk: flutter + flame_lint: ^1.1.1 + test: ^1.24.8 diff --git a/packages/tiled/test/color.dart b/packages/tiled/test/color.dart new file mode 100644 index 0000000..3bd7a38 --- /dev/null +++ b/packages/tiled/test/color.dart @@ -0,0 +1,25 @@ +import 'dart:math'; + +import 'package:test/expect.dart'; +import 'package:test/scaffolding.dart'; +import 'package:tiled/tiled.dart'; + +void main() { + group('ColorData.hex', () { + test('parse', () { + final random = Random(); + final red = random.nextInt(256); + final green = random.nextInt(256); + final blue = random.nextInt(256); + final alpha = random.nextInt(256); + + final hex = alpha << 24 | red << 16 | green << 8 | blue << 0; + final data = ColorData.hex(hex); + + expect(data.alpha, equals(alpha)); + expect(data.red, equals(red)); + expect(data.green, equals(green)); + expect(data.blue, equals(blue)); + }); + }); +} diff --git a/packages/tiled/test/complexmap_infinite_test.dart b/packages/tiled/test/complexmap_infinite_test.dart index 5ab581c..ca5bf49 100644 --- a/packages/tiled/test/complexmap_infinite_test.dart +++ b/packages/tiled/test/complexmap_infinite_test.dart @@ -1,6 +1,6 @@ import 'dart:io'; -import 'package:flutter_test/flutter_test.dart'; +import 'package:test/test.dart'; import 'package:tiled/tiled.dart'; void main() { diff --git a/packages/tiled/test/exporter/map.dart b/packages/tiled/test/exporter/map.dart new file mode 100644 index 0000000..37d37fa --- /dev/null +++ b/packages/tiled/test/exporter/map.dart @@ -0,0 +1,78 @@ +import 'package:test/test.dart'; +import 'package:tiled/tiled.dart'; +import 'package:xml/xml.dart'; + +void main() { + late TiledMap export; + setUp( + () => export = TiledMap( + width: 576, + height: 432, + tileWidth: 48, + tileHeight: 48, + orientation: MapOrientation.hexagonal, + renderOrder: RenderOrder.leftUp, + hexSideLength: 24, + staggerAxis: StaggerAxis.y, + staggerIndex: StaggerIndex.even, + backgroundColor: ColorData.hex(0xaa252627), + nextLayerId: 24, + nextObjectId: 56, + infinite: false, + layers: [ + TileLayer(name: 'test1', width: 48, height: 48), + TileLayer(name: 'test2', width: 48, height: 48), + TileLayer(name: 'test3', width: 48, height: 48), + ], + tilesets: [ + Tileset(firstGid: 1, source: 'xyz.png'), + Tileset(firstGid: 20, source: 'xyz.png'), + ], + ), + ); + + group('Exporter - Map', () { + test( + 'Xml', + () => testSuite(XmlParser(export.exportXml() as XmlElement)), + ); + test( + 'Json', + () => testSuite(JsonParser(export.exportJson() as Map)), + ); + }); +} + +void testSuite(Parser export) { + void attribute(String name, String expected) => + expect(export.getString(name), equals(expected)); + + attribute('height', '432'); + attribute('width', '576'); + attribute('tilewidth', '48'); + attribute('tileheight', '48'); + attribute('version', '1.10'); + attribute('type', TileMapType.map.name); + attribute('orientation', MapOrientation.hexagonal.name); + attribute('renderorder', RenderOrder.leftUp.name); + attribute('hexsidelength', '24'); + attribute('staggeraxis', StaggerAxis.y.name); + attribute('staggerindex', StaggerIndex.even.name); + attribute('backgroundcolor', '#aa252627'); + attribute('nextlayerid', '24'); + attribute('nextobjectid', '56'); + attribute( + 'infinite', export.formatSpecificParsing((p0) => 'false', (p0) => '0')); + + final layers = export.formatSpecificParsing( + (e) => e.getChildren('layers'), + (e) => e.getChildren('layer'), + ); + expect(layers.length, equals(3)); + + final tilesets = export.formatSpecificParsing( + (e) => e.getChildren('tilesets'), + (e) => e.getChildren('tileset'), + ); + expect(tilesets.length, equals(2)); +} diff --git a/packages/tiled/test/exporter/properties.dart b/packages/tiled/test/exporter/properties.dart new file mode 100644 index 0000000..e035809 --- /dev/null +++ b/packages/tiled/test/exporter/properties.dart @@ -0,0 +1,37 @@ +import 'package:collection/collection.dart'; +import 'package:test/test.dart'; +import 'package:tiled/tiled.dart'; +import 'package:xml/xml.dart'; + +void main() { + late TiledObject export; + late CustomProperties byName; + setUp(() { + byName = CustomProperties( + [ + StringProperty(name: 'test_string', value: 'test'), + ].groupFoldBy((e) => e.name, (old, e) => e), + ); + + export = TiledObject(id: 5, properties: byName); + }); + + void testSuite(Parser export) { + final properties = export.getProperties(); + expect(properties.length, equals(byName.length)); + + for (final property in properties) { + final match = byName + .where((e) => e.name == property.name && e.value == property.value); + expect(match.length, equals(1)); + } + } + + group('Exporter - CustomProperties', () { + test('Xml', () => testSuite(XmlParser(export.exportXml() as XmlElement))); + test( + 'Json', + () => + testSuite(JsonParser(export.exportJson() as Map))); + }); +} diff --git a/packages/tiled/test/exporter/tile_data.dart b/packages/tiled/test/exporter/tile_data.dart new file mode 100644 index 0000000..d1c4887 --- /dev/null +++ b/packages/tiled/test/exporter/tile_data.dart @@ -0,0 +1,53 @@ +import 'dart:math'; + +import 'package:test/expect.dart'; +import 'package:test/scaffolding.dart'; +import 'package:tiled/tiled.dart'; + +void main() { + late List testData; + + setUp(() { + testData = List.generate(1000, (index) => Random().nextInt(1 << 32)); + }); + + group('base64', () { + void encodeDecode(List testData, Compression? compression) { + final encoder = ExportTileData( + data: testData, + encoding: FileEncoding.base64, + compression: compression, + ); + + final encoded = encoder.encodeBase64(); + final unencoded = Layer.parseLayerData( + JsonParser({'data': encoded}), + FileEncoding.base64, + compression, + ); + + expect(unencoded, orderedEquals(testData)); + } + + test('base64-uncompressed', () => encodeDecode(testData, null)); + test('base64-gzip', () => encodeDecode(testData, Compression.gzip)); + test('base64-zlib', () => encodeDecode(testData, Compression.zlib)); + }); + + test('csv', () { + final encoder = ExportTileData( + data: testData, + encoding: FileEncoding.csv, + compression: null, + ); + + final encoded = encoder.encodeCsv(); + final unencoded = Layer.parseLayerData( + JsonParser({'data': encoded}), + FileEncoding.csv, + null, + ); + + expect(unencoded, orderedEquals(testData)); + }); +} diff --git a/packages/tiled/test/exporter/xml.dart b/packages/tiled/test/exporter/xml.dart new file mode 100644 index 0000000..6685396 --- /dev/null +++ b/packages/tiled/test/exporter/xml.dart @@ -0,0 +1,66 @@ +import 'package:test/test.dart'; +import 'package:xml/xml.dart'; + +class XmlDeepMatcher extends Matcher { + final XmlElement expected; + + XmlDeepMatcher(this.expected); + XmlDeepMatcher.parse(String xml) + : expected = XmlDocument.parse(xml).rootElement..normalize(); + + @override + Description describe(Description description) => + description..add(expected.toString()); + + @override + bool matches(dynamic item, Map matchState) { + if (item is! XmlElement) { + return false; + } + return _match(item, expected); + } + + bool _match(XmlElement a, XmlElement b) { + return a.localName == b.localName && + _deepIterable( + a.attributes, + b.attributes, + (a, b) => + a.localName == b.localName && + a.attributeType.name == b.attributeType.name && + a.value.trim() == b.value.trim(), + ) && + _deepIterable(_cleanNodes(a.children), _cleanNodes(b.children), (a, b) { + if (a.runtimeType != b.runtimeType) { + return false; + } + + if (a is XmlText) { + return a.value.trim() == b.value!.trim(); + } else if (a is XmlElement) { + return _match(a, b as XmlElement); + } else { + return false; + } + }); + } + + bool _deepIterable( + Iterable a, + Iterable b, + bool Function(T a, T b) equals, + ) { + if (a.length != b.length) { + return false; + } + return a.every((aa) => b.any((bb) => equals(aa, bb))); + } + + Iterable _cleanNodes(Iterable list) => list.where( + (e) => + e is! XmlCDATA && + e is! XmlComment && + e is! XmlDeclaration && + e is! XmlProcessing, + ); +} diff --git a/packages/tiled/test/image_layer_test.dart b/packages/tiled/test/image_layer_test.dart index da3a9c6..3b52721 100644 --- a/packages/tiled/test/image_layer_test.dart +++ b/packages/tiled/test/image_layer_test.dart @@ -1,7 +1,7 @@ import 'dart:io'; -import 'package:flutter_test/flutter_test.dart'; import 'package:tiled/tiled.dart'; +import 'package:test/test.dart'; void main() { late TiledMap map; diff --git a/packages/tiled/test/isometric_staggered_test.dart b/packages/tiled/test/isometric_staggered_test.dart index 00e9e33..4e13832 100644 --- a/packages/tiled/test/isometric_staggered_test.dart +++ b/packages/tiled/test/isometric_staggered_test.dart @@ -1,6 +1,6 @@ import 'dart:io'; -import 'package:flutter_test/flutter_test.dart'; +import 'package:test/test.dart'; import 'package:tiled/tiled.dart'; void main() { diff --git a/packages/tiled/test/isometric_test.dart b/packages/tiled/test/isometric_test.dart index 66c61bc..9c28c6a 100644 --- a/packages/tiled/test/isometric_test.dart +++ b/packages/tiled/test/isometric_test.dart @@ -1,6 +1,6 @@ import 'dart:io'; -import 'package:flutter_test/flutter_test.dart'; +import 'package:test/test.dart'; import 'package:tiled/tiled.dart'; void main() { diff --git a/packages/tiled/test/layer_test.dart b/packages/tiled/test/layer_test.dart index 984b46f..d967704 100644 --- a/packages/tiled/test/layer_test.dart +++ b/packages/tiled/test/layer_test.dart @@ -1,7 +1,6 @@ import 'dart:io'; -import 'dart:ui'; -import 'package:flutter_test/flutter_test.dart'; +import 'package:test/test.dart'; import 'package:tiled/tiled.dart'; void main() { @@ -75,7 +74,7 @@ void main() { expect(layer.tintColorHex, equals('#ffaabb')); expect( layer.tintColor, - equals(Color(int.parse('0xffffaabb'))), + equals(ColorData.hex(int.parse('0xffffaabb'))), ); }); }); diff --git a/packages/tiled/test/map_test.dart b/packages/tiled/test/map_test.dart index fd86eed..2288cc5 100644 --- a/packages/tiled/test/map_test.dart +++ b/packages/tiled/test/map_test.dart @@ -1,4 +1,4 @@ -import 'package:flutter_test/flutter_test.dart'; +import 'package:test/test.dart'; import 'package:tiled/tiled.dart'; void main() { diff --git a/packages/tiled/test/object_group_test.dart b/packages/tiled/test/object_group_test.dart index d0692cf..0287622 100644 --- a/packages/tiled/test/object_group_test.dart +++ b/packages/tiled/test/object_group_test.dart @@ -1,7 +1,6 @@ import 'dart:io'; -import 'dart:ui'; -import 'package:flutter_test/flutter_test.dart'; +import 'package:test/test.dart'; import 'package:tiled/tiled.dart'; void main() { @@ -31,7 +30,7 @@ void main() { expect(objectGroup.colorHex, equals('#555500')); expect( objectGroup.color, - equals(Color(int.parse('0xff555500'))), + equals(ColorData.hex(int.parse('0xff555500'))), ); }); diff --git a/packages/tiled/test/overflow_bug_test.dart b/packages/tiled/test/overflow_bug_test.dart index 481c888..27e62b7 100644 --- a/packages/tiled/test/overflow_bug_test.dart +++ b/packages/tiled/test/overflow_bug_test.dart @@ -2,7 +2,7 @@ import 'dart:io'; -import 'package:flutter_test/flutter_test.dart'; +import 'package:test/test.dart'; import 'package:tiled/tiled.dart'; void main() { diff --git a/packages/tiled/test/parser_compare_test.dart b/packages/tiled/test/parser_compare_test.dart index ab052d0..4e8b421 100644 --- a/packages/tiled/test/parser_compare_test.dart +++ b/packages/tiled/test/parser_compare_test.dart @@ -1,6 +1,6 @@ import 'dart:io'; -import 'package:flutter_test/flutter_test.dart'; +import 'package:test/test.dart'; import 'package:tiled/tiled.dart'; void main() { diff --git a/packages/tiled/test/parser_test.dart b/packages/tiled/test/parser_test.dart index 519d5b7..442d783 100644 --- a/packages/tiled/test/parser_test.dart +++ b/packages/tiled/test/parser_test.dart @@ -1,8 +1,7 @@ import 'dart:io'; import 'dart:math' show Rectangle; -import 'dart:ui'; -import 'package:flutter_test/flutter_test.dart'; +import 'package:test/test.dart'; import 'package:tiled/tiled.dart'; import 'package:xml/xml.dart'; @@ -44,7 +43,7 @@ void main() { expect(map.backgroundColorHex, equals('#ccddaaff')); expect( map.backgroundColor, - equals(Color(int.parse('0xccddaaff'))), + equals(ColorData.hex(int.parse('0xccddaaff'))), ); }); @@ -89,7 +88,7 @@ void main() { ); expect( properties.getValue('multiline string'), - equals('Hello,\nWorld'), + equals('Hello,\r\nWorld'), ); expect( properties.getValue('integer property'), @@ -100,8 +99,8 @@ void main() { equals('#00112233'), ); expect( - properties.getValue('color property'), - equals(const Color(0x00112233)), + properties.getValue('color property'), + equals(ColorData.hex(0x00112233)), ); expect( properties.getValue('float property'), diff --git a/packages/tiled/test/rectangle_map_test.dart b/packages/tiled/test/rectangle_map_test.dart index 7e70418..797bb5b 100644 --- a/packages/tiled/test/rectangle_map_test.dart +++ b/packages/tiled/test/rectangle_map_test.dart @@ -1,6 +1,6 @@ import 'dart:io'; -import 'package:flutter_test/flutter_test.dart'; +import 'package:test/test.dart'; import 'package:tiled/tiled.dart'; void main() { diff --git a/packages/tiled/test/tile_test.dart b/packages/tiled/test/tile_test.dart index db85d8e..c91e049 100644 --- a/packages/tiled/test/tile_test.dart +++ b/packages/tiled/test/tile_test.dart @@ -1,7 +1,7 @@ import 'dart:io'; -import 'dart:ui'; +import 'dart:math'; -import 'package:flutter_test/flutter_test.dart'; +import 'package:test/test.dart'; import 'package:tiled/tiled.dart'; import 'package:xml/xml.dart'; @@ -58,8 +58,8 @@ void main() { final tile1 = tileset1.tiles.firstWhere((t) => t.localId == 0); final tile2 = tileset1.tiles.firstWhere((t) => t.localId == 1); - expect(tile1.imageRect, const Rect.fromLTWH(64, 96, 32, 32)); - expect(tile2.imageRect, const Rect.fromLTWH(0, 0, 20, 20)); + expect(tile1.imageRect, const Rectangle(64, 96, 32, 32)); + expect(tile2.imageRect, const Rectangle(0, 0, 20, 20)); }); test( @@ -70,10 +70,10 @@ void main() { final tile3 = tileset2.tiles.firstWhere((t) => t.localId == 129); final tile4 = tileset2.tiles.firstWhere((t) => t.localId == 11); - expect(tile1.imageRect, const Rect.fromLTWH(112, 48, 16, 16)); - expect(tile2.imageRect, const Rect.fromLTWH(64, 96, 16, 16)); - expect(tile3.imageRect, const Rect.fromLTWH(160, 112, 16, 16)); - expect(tile4.imageRect, const Rect.fromLTWH(176, 0, 16, 16)); + expect(tile1.imageRect, const Rectangle(112, 48, 16, 16)); + expect(tile2.imageRect, const Rectangle(64, 96, 16, 16)); + expect(tile3.imageRect, const Rectangle(160, 112, 16, 16)); + expect(tile4.imageRect, const Rectangle(176, 0, 16, 16)); }, ); }, diff --git a/packages/tiled/test/tileset_test.dart b/packages/tiled/test/tileset_test.dart index 756709b..516fd97 100644 --- a/packages/tiled/test/tileset_test.dart +++ b/packages/tiled/test/tileset_test.dart @@ -1,6 +1,6 @@ import 'dart:io'; -import 'package:flutter_test/flutter_test.dart'; +import 'package:test/test.dart'; import 'package:tiled/tiled.dart'; import 'package:xml/xml.dart'; @@ -57,7 +57,9 @@ void main() { test( 'first objectgroup object = ellipsis', () => expect( - ((tileset.tiles.first.objectGroup as ObjectGroup?)!.objects.first) + (tileset.tiles.first.objectGroup as ObjectGroup?)! + .objects + .first .isEllipse, true, ), diff --git a/packages/tiled/test/tmx_object_test.dart b/packages/tiled/test/tmx_object_test.dart index f58d90a..8579ed6 100644 --- a/packages/tiled/test/tmx_object_test.dart +++ b/packages/tiled/test/tmx_object_test.dart @@ -1,6 +1,6 @@ import 'dart:io'; -import 'package:flutter_test/flutter_test.dart'; +import 'package:test/test.dart'; import 'package:tiled/tiled.dart'; void main() {