Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Export maps #78

Open
wants to merge 17 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 23 additions & 1 deletion packages/tiled/lib/src/chunk.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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<int> data;

int x;
Expand Down Expand Up @@ -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,
),
},
);
}
}
37 changes: 37 additions & 0 deletions packages/tiled/lib/src/common/color.dart
Original file line number Diff line number Diff line change
@@ -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<String> {
static int _sub(int hex, int index) => (hex >> index * 8) & 0x000000ff;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not store the hex value in ColorData and have getters that returns the bit-shifted values back when needed? export then becomes => _hex.toRadixString(16).padLeft(8, '0'). My assumption right now is this will increase the size of memory.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes you are right that would be more efficient!


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;
}
12 changes: 11 additions & 1 deletion packages/tiled/lib/src/common/frame.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ part of tiled;
/// * tileid: The local ID of a tile within the parent <tileset>.
/// * 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;

Expand All @@ -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(),
},
{},
);
}
20 changes: 19 additions & 1 deletion packages/tiled/lib/src/common/gid.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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<int> {
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;

Expand Down Expand Up @@ -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();
}
17 changes: 17 additions & 0 deletions packages/tiled/lib/src/common/iterable.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
part of tiled;

extension Grouping<V> on Iterable<V> {
Map<K, List<V>> groupBy<K>(K Function(V value) key) {
final out = <K, List<V>>{};
for (final v in this) {
final k = key(v);
if (!out.containsKey(k)) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pick your poison; but since you have "List" as non-nullable, the first is shorter.

  (foo[1] ??= []).add('one');
  foo.putIfAbsent(2, ()=>[]).add('two');

out[k] = [v];
} else {
out[k]!.add(v);
}
}

return out;
}
}
6 changes: 6 additions & 0 deletions packages/tiled/lib/src/common/map.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
part of tiled;

extension Null<K, V> on Map<K, V?> {
Map<K, V> nonNulls() =>
{for (final e in entries.where((e) => e.value is V)) e.key: e.value as V};
}
26 changes: 23 additions & 3 deletions packages/tiled/lib/src/common/property.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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<T> {
class Property<T> with Exportable {
String name;
PropertyType type;
T value;
Expand All @@ -37,7 +37,7 @@ class Property<T> {
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'),
);

Expand Down Expand Up @@ -86,6 +86,19 @@ class Property<T> {
);
}
}

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
Expand Down Expand Up @@ -156,14 +169,17 @@ class ObjectProperty extends Property<int> {
}

/// [value] is the color
class ColorProperty extends Property<Color> {
class ColorProperty extends Property<ColorData> {
final String hexValue;

ColorProperty({
required super.name,
required super.value,
required this.hexValue,
}) : super(type: PropertyType.color);

@override
ExportValue get exportValue => value;
}

/// [value] is the string text
Expand All @@ -180,6 +196,10 @@ class FileProperty extends Property<String> {
required super.name,
required super.value,
}) : super(type: PropertyType.file);

@override
ExportValue get exportValue =>
value.isNotEmpty ? value.toExport() : '.'.toExport();
}

/// [value] is the integer number
Expand Down
17 changes: 16 additions & 1 deletion packages/tiled/lib/src/common/tiled_image.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
},
);
}
100 changes: 100 additions & 0 deletions packages/tiled/lib/src/exporter/export_element.dart
Original file line number Diff line number Diff line change
@@ -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<String, ExportValue> fields;
final Map<String, ExportObject> children;
final CustomProperties? properties;

const ExportElement(
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd like for these to be named parameters. It would make the earlier calls to ExportElement easier to read, and bonus, you could default values (e.g. chilren = {}) and make the call sites look cleaner.

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<ExportResolver>
implements ExportObject {
ExportList(Iterable<ExportResolver> base) : super(base.toList());

ExportList.from(Iterable<Exportable> source)
: super(
source.map((e) => e).toList(),
);
}
Loading