///|
pub fn Rect::with_origin(self : Rect, x : Int, y : Int) -> Rect {
  { ..self, x, y }
}

///|
pub fn Rect::with_size(self : Rect, w : Int, h : Int) -> Rect {
  { ..self, w, h }
}

///|
pub fn Rect::move_to(self : Rect, x : Int, y : Int) -> Rect {
  { ..self, x, y }
}

///|
pub fn Rect::grow_width(self : Rect, amount : Int) -> Rect {
  { ..self, w: self.w + amount }
}

///|
pub fn Rect::grow_height(self : Rect, amount : Int) -> Rect {
  { ..self, h: self.h + amount }
}

///|
pub fn Rect::shrink_width(self : Rect, amount : Int) -> Rect {
  { ..self, w: self.w - amount }
}

///|
pub fn Rect::shrink_height(self : Rect, amount : Int) -> Rect {
  { ..self, h: self.h - amount }
}

///|
pub fn Rect::area_with_padding(self : Rect, padding : Int) -> Int {
  (self.w + padding * 2) * (self.h + padding * 2)
}

///|
pub fn Rect::fits_inside(self : Rect, bounds : Rect) -> Bool {
  bounds.contains_rect(self)
}

///|
pub fn Rect::center_x(self : Rect) -> Int {
  self.x + self.w / 2
}

///|
pub fn Rect::center_y(self : Rect) -> Int {
  self.y + self.h / 2
}

///|
pub fn Rect::right_edge(self : Rect) -> Int {
  self.right()
}

///|
pub fn Rect::bottom_edge(self : Rect) -> Int {
  self.bottom()
}

///|
pub fn Rect::perimeter(self : Rect) -> Int {
  (self.w + self.h) * 2
}

///|
pub fn Rect::is_square(self : Rect) -> Bool {
  self.w == self.h
}

///|
pub fn Rect::longest_edge(self : Rect) -> Int {
  if self.w > self.h {
    self.w
  } else {
    self.h
  }
}

///|
pub fn Rect::shortest_edge(self : Rect) -> Int {
  if self.w < self.h {
    self.w
  } else {
    self.h
  }
}

///|
pub fn Rect::aspect_ratio(self : Rect) -> Double {
  if self.h == 0 {
    0.0
  } else {
    self.w.to_double() / self.h.to_double()
  }
}

///|
pub fn Rect::translated_area(self : Rect, dx : Int, dy : Int) -> Int {
  self.translate(dx, dy).area()
}

///|
pub fn Rect::expanded_area(self : Rect, amount : Int) -> Int {
  self.expand(amount).area()
}

///|
pub fn SourceFrame::display_width(self : SourceFrame) -> Int {
  self.source_size.w
}

///|
pub fn SourceFrame::display_height(self : SourceFrame) -> Int {
  self.source_size.h
}

///|
pub fn SourceFrame::trimmed_width(self : SourceFrame) -> Int {
  self.frame.w
}

///|
pub fn SourceFrame::trimmed_height(self : SourceFrame) -> Int {
  self.frame.h
}

///|
pub fn SourceFrame::trim_left(self : SourceFrame) -> Int {
  self.sprite_source_size.x
}

///|
pub fn SourceFrame::trim_top(self : SourceFrame) -> Int {
  self.sprite_source_size.y
}

///|
pub fn SourceFrame::trim_right(self : SourceFrame) -> Int {
  self.source_size.w - self.sprite_source_size.right()
}

///|
pub fn SourceFrame::trim_bottom(self : SourceFrame) -> Int {
  self.source_size.h - self.sprite_source_size.bottom()
}

///|
pub fn SourceFrame::is_untrimmed(self : SourceFrame) -> Bool {
  !self.trimmed
}

///|
pub fn SourceFrame::box_count(self : SourceFrame) -> Int {
  self.boxes.length()
}

///|
pub fn SourceFrame::has_boxes(self : SourceFrame) -> Bool {
  self.boxes.length() > 0
}

///|
pub fn SourceFrame::frame_area(self : SourceFrame) -> Int {
  self.frame.area()
}

///|
pub fn SourceFrame::source_area(self : SourceFrame) -> Int {
  self.source_size.area()
}

///|
pub fn SourceFrame::trim_ratio(self : SourceFrame) -> Double {
  frame_area_ratio(self)
}

///|
pub fn SourceFrame::duration_seconds(self : SourceFrame) -> Double {
  self.duration.to_double() / 1000.0
}

///|
pub fn SourceFrame::has_box_named(self : SourceFrame, name : String) -> Bool {
  self.boxes.exists(fn(box) { box.name == name })
}

///|
pub fn SourceFrame::box_names(self : SourceFrame) -> Array[String] {
  self.boxes.map(fn(box) { box.name })
}

///|
pub fn SourceFrame::box_area(self : SourceFrame) -> Int {
  self.boxes.fold(init=0, (total, box) => total + box.rect.area())
}

///|
pub fn SourceFrame::source_rect(self : SourceFrame) -> Rect {
  { x: 0, y: 0, w: self.source_size.w, h: self.source_size.h }
}

///|
pub fn SourceFrame::trimmed_rect(self : SourceFrame) -> Rect {
  self.sprite_source_size
}

///|
pub fn SourceFrame::frame_center(self : SourceFrame) -> Point {
  self.frame.center()
}

///|
pub fn SourceFrame::source_center(self : SourceFrame) -> Point {
  self.source_rect().center()
}

///|
pub fn SourceFrame::has_collision(self : SourceFrame) -> Bool {
  self.boxes.exists(fn(box) { box.name.contains("collision") })
}

///|
pub fn SourceFrame::is_square(self : SourceFrame) -> Bool {
  self.source_size.w == self.source_size.h
}

///|
pub fn SourceFrame::duration_or(self : SourceFrame, fallback : Int) -> Int {
  if self.duration > fallback {
    self.duration
  } else {
    fallback
  }
}

///|
pub fn SourceFrame::box_named(
  self : SourceFrame,
  name : String,
) -> CollisionBox? {
  match self.boxes.search_by(fn(box) { box.name == name }) {
    Some(index) => Some(self.boxes[index])
    None => None
  }
}

///|
pub fn SourceFrame::max_box_area(self : SourceFrame) -> Int {
  self.boxes.fold(init=0, (best, box) => {
    if box.rect.area() > best {
      box.rect.area()
    } else {
      best
    }
  })
}

///|
pub fn SourceFrame::min_box_area(self : SourceFrame) -> Int {
  self.boxes.fold(init=0, (best, box) => {
    if best == 0 || box.rect.area() < best {
      box.rect.area()
    } else {
      best
    }
  })
}

///|
pub fn SourceFrame::occupancy(self : SourceFrame) -> Double {
  if self.source_size.area() == 0 {
    0.0
  } else {
    self.frame.area().to_double() / self.source_size.area().to_double()
  }
}

///|
pub fn SourceFrame::is_portrait(self : SourceFrame) -> Bool {
  self.source_size.h > self.source_size.w
}

///|
pub fn SourceFrame::is_landscape(self : SourceFrame) -> Bool {
  self.source_size.w > self.source_size.h
}

///|
pub fn SpriteSheet::frame_count(self : SpriteSheet) -> Int {
  self.frames.length()
}

///|
pub fn SpriteSheet::tag_count(self : SpriteSheet) -> Int {
  self.tags.length()
}

///|
pub fn SpriteSheet::slice_count(self : SpriteSheet) -> Int {
  self.slices.length()
}

///|
pub fn SpriteSheet::image_name(self : SpriteSheet) -> String {
  self.image
}

///|
pub fn SpriteSheet::has_tag(self : SpriteSheet, name : String) -> Bool {
  self.tags.exists(fn(tag) { tag.name == name })
}

///|
pub fn SpriteSheet::has_slice(self : SpriteSheet, name : String) -> Bool {
  self.slices.exists(fn(slice) { slice.name == name })
}

///|
pub fn SpriteSheet::first_frame(self : SpriteSheet) -> SourceFrame? {
  self.frames.get(0)
}

///|
pub fn SpriteSheet::last_frame(self : SpriteSheet) -> SourceFrame? {
  self.frames.get(self.frames.length() - 1)
}

///|
pub fn SpriteSheet::total_frame_area(self : SpriteSheet) -> Int {
  self.frames.fold(init=0, (total, frame) => total + frame.frame.area())
}

///|
pub fn SpriteSheet::total_source_area(self : SpriteSheet) -> Int {
  self.frames.fold(init=0, (total, frame) => total + frame.source_size.area())
}

///|
pub fn SpriteSheet::total_box_count(self : SpriteSheet) -> Int {
  self.frames.fold(init=0, (total, frame) => total + frame.boxes.length())
}

///|
pub fn SpriteSheet::average_duration(self : SpriteSheet) -> Double {
  average_frame_duration(self)
}

///|
pub fn SpriteSheet::has_trimmed_frames(self : SpriteSheet) -> Bool {
  self.frames.exists(fn(frame) { frame.trimmed })
}

///|
pub fn SpriteSheet::has_rotated_frames(self : SpriteSheet) -> Bool {
  self.frames.exists(fn(frame) { frame.rotated })
}

///|
pub fn SpriteSheet::frame_names_with_prefix(
  self : SpriteSheet,
  prefix : String,
) -> Array[String] {
  sheet_find_frames(self, prefix).map(fn(frame) { frame.filename })
}

///|
pub fn SpriteSheet::tag_names_sorted(self : SpriteSheet) -> Array[String] {
  let names = sheet_tag_names(self)
  names.sort()
  names
}

///|
pub fn SpriteSheet::valid_frame_count(self : SpriteSheet) -> Int {
  self.frames
  .filter(fn(frame) { frame.duration > 0 && !frame.frame.is_empty() })
  .length()
}

///|
pub fn SpriteSheet::empty_frame_count(self : SpriteSheet) -> Int {
  self.frames.filter(fn(frame) { frame.frame.is_empty() }).length()
}

///|
pub fn SpriteSheet::animation_duration(self : SpriteSheet) -> Int {
  total_duration(self)
}

///|
pub fn SpriteSheet::sheet_area(self : SpriteSheet) -> Int {
  self.size.area()
}

///|
pub fn SpriteSheet::has_animation(self : SpriteSheet) -> Bool {
  self.tags.length() > 0
}

///|
pub fn SpriteSheet::has_nine_patch(self : SpriteSheet) -> Bool {
  self.slices.exists(fn(slice) { slice.center is Some(_) })
}

///|
pub fn SpriteSheet::frame_at_or_first(
  self : SpriteSheet,
  index : Int,
) -> SourceFrame? {
  match self.frame_at(index) {
    Some(frame) => Some(frame)
    None => self.first_frame()
  }
}

///|
pub fn SpriteSheet::contains_image(self : SpriteSheet, image : String) -> Bool {
  self.image == image
}

///|
pub fn SpriteSheet::frame_index_or(
  self : SpriteSheet,
  name : String,
  fallback : Int,
) -> Int {
  match sheet_frame_index(self, name) {
    Some(index) => index
    None => fallback
  }
}

///|
pub fn AnimationClip::frame_count(self : AnimationClip) -> Int {
  self.frames.length()
}

///|
pub fn AnimationClip::duration_at(self : AnimationClip, index : Int) -> Int? {
  self.durations.get(index)
}

///|
pub fn AnimationClip::frame_at(self : AnimationClip, index : Int) -> String? {
  self.frames.get(index)
}

///|
pub fn AnimationClip::contains_frame(
  self : AnimationClip,
  name : String,
) -> Bool {
  self.frames.contains(name)
}

///|
pub fn AnimationClip::first_frame(self : AnimationClip) -> String? {
  self.frames.get(0)
}

///|
pub fn AnimationClip::last_frame(self : AnimationClip) -> String? {
  self.frames.get(self.frames.length() - 1)
}

///|
pub fn AnimationClip::duration_sum(self : AnimationClip) -> Int {
  self.total_duration()
}

///|
pub fn AnimationClip::max_duration(self : AnimationClip) -> Int {
  self.durations.fold(init=0, (best, value) => {
    if value > best {
      value
    } else {
      best
    }
  })
}

///|
pub fn AnimationClip::min_duration(self : AnimationClip) -> Int {
  self.durations.fold(init=0, (best, value) => {
    if best == 0 || value < best {
      value
    } else {
      best
    }
  })
}

///|
pub fn AnimationClip::average_duration(self : AnimationClip) -> Double {
  if self.durations.length() == 0 {
    0.0
  } else {
    self.total_duration().to_double() / self.durations.length().to_double()
  }
}

///|
pub fn AnimationClip::unique_frame_count(self : AnimationClip) -> Int {
  let names : Array[String] = []
  for name in self.frames {
    if !names.contains(name) {
      names.push(name)
    }
  }
  names.length()
}

///|
pub fn AnimationClip::is_empty(self : AnimationClip) -> Bool {
  self.frames.length() == 0
}

///|
pub fn AnimationClip::is_single_frame(self : AnimationClip) -> Bool {
  self.frames.length() == 1
}

///|
pub fn AnimationClip::has_duration(self : AnimationClip, minimum : Int) -> Bool {
  self.durations.exists(fn(value) { value > minimum })
}

///|
pub fn AnimationClip::frame_index(self : AnimationClip, name : String) -> Int? {
  self.frames.search_by(fn(value) { value == name })
}

///|
pub fn AnimationClip::frame_names(self : AnimationClip) -> Array[String] {
  self.frames.copy()
}

///|
pub fn AnimationClip::durations_copy(self : AnimationClip) -> Array[Int] {
  self.durations.copy()
}

///|
pub fn AnimationClip::loop_count_for(self : AnimationClip, total : Int) -> Int {
  if self.total_duration() == 0 {
    0
  } else {
    total / self.total_duration()
  }
}

///|
pub fn AnimationClip::is_forward(self : AnimationClip) -> Bool {
  self.direction == Forward
}

///|
pub fn AnimationClip::is_reverse(self : AnimationClip) -> Bool {
  self.direction == Reverse
}

///|
pub fn AnimationClip::is_pingpong(self : AnimationClip) -> Bool {
  self.direction == PingPong
}

///|
pub fn AnimationClip::has_repeated_frame(self : AnimationClip) -> Bool {
  self.frames.length() != self.unique_frame_count()
}

///|
pub fn AnimationClip::duration_at_or(
  self : AnimationClip,
  index : Int,
  fallback : Int,
) -> Int {
  match self.duration_at(index) {
    Some(value) => value
    None => fallback
  }
}

///|
pub fn AnimationClip::frame_at_or(
  self : AnimationClip,
  index : Int,
  fallback : String,
) -> String {
  match self.frame_at(index) {
    Some(value) => value
    None => fallback
  }
}

///|
pub fn AtlasPlan::width(self : AtlasPlan) -> Int {
  self.size.w
}

///|
pub fn AtlasPlan::height(self : AtlasPlan) -> Int {
  self.size.h
}

///|
pub fn AtlasPlan::frame_count(self : AtlasPlan) -> Int {
  self.frames.length()
}

///|
pub fn AtlasPlan::area(self : AtlasPlan) -> Int {
  self.size.area()
}

///|
pub fn AtlasPlan::occupancy_percent(self : AtlasPlan) -> Double {
  self.occupancy * 100.0
}

///|
pub fn AtlasPlan::has_frame(self : AtlasPlan, name : String) -> Bool {
  self.frames.exists(fn(frame) { frame.filename == name })
}

///|
pub fn AtlasPlan::frame_index(self : AtlasPlan, name : String) -> Int? {
  self.frames.search_by(fn(frame) { frame.filename == name })
}

///|
pub fn AtlasPlan::first_frame(self : AtlasPlan) -> PackedFrame? {
  self.frames.get(0)
}

///|
pub fn AtlasPlan::last_frame(self : AtlasPlan) -> PackedFrame? {
  self.frames.get(self.frames.length() - 1)
}

///|
pub fn AtlasPlan::is_empty(self : AtlasPlan) -> Bool {
  self.frames.length() == 0
}

///|
pub fn AtlasPlan::fits_size(self : AtlasPlan, size : Size) -> Bool {
  self.size.w <= size.w && self.size.h <= size.h
}

///|
pub fn AtlasPlan::waste_area(self : AtlasPlan) -> Int {
  self.size.area() - self.packed_area()
}

///|
pub fn AtlasPlan::waste_ratio(self : AtlasPlan) -> Double {
  if self.area() == 0 {
    0.0
  } else {
    self.waste_area().to_double() / self.area().to_double()
  }
}

///|
pub fn AtlasPlan::frame_names(self : AtlasPlan) -> Array[String] {
  self.frames.map(fn(frame) { frame.filename })
}

///|
pub fn AtlasPlan::source_area(self : AtlasPlan) -> Int {
  self.frames.fold(init=0, (total, frame) => total + frame.source.area())
}

///|
pub fn AtlasPlan::duration_sum(self : AtlasPlan) -> Int {
  self.frames.fold(init=0, (total, frame) => total + frame.duration)
}

///|
pub fn AtlasPlan::max_width(self : AtlasPlan) -> Int {
  self.frames.fold(init=0, (best, frame) => {
    if frame.packed.w > best {
      frame.packed.w
    } else {
      best
    }
  })
}

///|
pub fn AtlasPlan::max_height(self : AtlasPlan) -> Int {
  self.frames.fold(init=0, (best, frame) => {
    if frame.packed.h > best {
      frame.packed.h
    } else {
      best
    }
  })
}