🔲️

Nannou の Rect を Ruby に移植する

2023/11/04に公開

Nannou の Rect を Ruby でも使えるようにしたもの。

コード
rect.rb
require "../ベクトル/vec2"
require "../一次元の範囲/range2"

class Rect
  class << self
    # 指定された x y 座標と w h 次元から Rect を構築する
    def from_x_y_w_h(x, y, w, h)
      new(Range2.from_pos_and_len(x, w), Range2.from_pos_and_len(y, h))
    end

    # 指定された Point と Dimensions から Rect を構築する
    def from_xy_wh(p, s)
      from_x_y_w_h(p.x, p.y, s.x, s.y)
    end

    # 指定された寸法で原点に Rect を構築する
    def from_wh(s)
      from_w_h(s.x, s.y)
    end

    # 指定された幅と高さで原点に Rect を構築する
    def from_w_h(w, h)
      from_x_y_w_h(0.0, 0.0, w, h)
    end

    # 2点の座標から Rect を構築する
    def from_corners(a, b)
      if a.x < b.x
        left, right = a.x, b.x
      else
        left, right = b.x, a.x
      end
      if a.y < b.y
        bottom, top = a.y, b.y
      else
        bottom, top = b.y, a.y
      end
      new(Range2.new(left, right), Range2.new(bottom, top))
    end
  end

  attr_accessor :x
  attr_accessor :y

  def initialize(x, y)
    @x = x
    @y = y
  end

  # x の中央
  def x_middle
    @x.middle
  end

  # y の中央
  def y_middle
    @y.middle
  end

  # 中心
  def x_y
    [x_middle, y_middle]
  end

  # 幅
  def w
    @x.length
  end

  # 高さ
  def h
    @y.length
  end

  # 寸法
  def w_h
    [w, h]
  end

  # 中心と寸法
  def x_y_w_h
    [x_middle, y_middle, w, h]
  end

  # 左上と寸法
  def l_t_w_h
    [left, top, w, h]
  end

  # 左下と寸法
  def l_b_w_h
    [left, bottom, w, h]
  end

  # 長方形の最長辺の長さ
  def length
    [w, h].max
  end

  # Rect の最小の y 値
  def bottom
    @y.absolute.start
  end

  # Rect の最大の y 値
  def top
    @y.absolute.end
  end

  # Rect の最小の x 値
  def left
    @x.absolute.start
  end

  # Rect の最大の x 値
  def right
    @x.absolute.end
  end

  # 辺 (左右下上)
  def l_r_b_t
    [left, right, bottom, top]
  end

  # 境界の中央の xy 位置
  def xy
    V[x_middle, y_middle]
  end

  # Rect の合計寸法
  def wh
    V[w, h]
  end

  # Rect を Point と Dimensions に変換する
  def xy_wh
    [xy, wh]
  end

  # 左上隅位置
  def top_left
    V[left, top]
  end

  # 左下隅位置
  def bottom_left
    V[left, bottom]
  end

  # 右上隅位置
  def top_right
    V[right, top]
  end

  # 右下隅位置
  def bottom_right
    V[right, bottom]
  end

  # 左端の真ん中
  def mid_left
    V[left, y_middle]
  end

  # 上端の真ん中
  def mid_top
    V[x_middle, top]
  end

  # 右端の真ん中
  def mid_right
    V[right, y_middle]
  end

  # 下端の真ん中
  def mid_bottom
    V[x_middle, bottom]
  end

  ################################################################################

  # 指定された Align バリアントに従って、x 軸に沿って self を other に位置を合わせる
  def align_x_of(align, other)
    self.class.new(@x.align_to(align, other.x), @y)
  end

  # 指定された Align バリアントに従って、y 軸に沿って self を other に位置を合わせる
  def align_y_of(align, other)
    self.class.new(@x, @y.align_to(align, other.y))
  end

  # x 軸に沿って、自分の中心を相手の Rect の中心に揃える
  def align_middle_x_of(other)
    self.class.new(@x.align_middle_of(other.x), @y)
  end

  # y 軸に沿って、self の中央を other Rect の中央に揃える
  def align_middle_y_of(other)
    self.class.new(@x, @y.align_middle_of(other.y))
  end

  # 自分を相手の Rect の上端の中央に配置する
  def mid_top_of(other)
    align_middle_x_of(other).align_top_of(other)
  end

  # 自分を相手の Rect の下端の中央に配置する
  def mid_bottom_of(other)
    align_middle_x_of(other).align_bottom_of(other)
  end

  # 自分を相手の Rect の左端の中央に配置する
  def mid_left_of(other)
    align_left_of(other).align_middle_y_of(other)
  end

  # 自分を相手の Rect の右端の中央に配置する
  def mid_right_of(other)
    align_right_of(other).align_middle_y_of(other)
  end

  # 自分を相手の Rect の中央に直接配置する
  def middle_of(other)
    align_middle_x_of(other).align_middle_y_of(other)
  end

  def subdivision_ranges
    x_a = Range2.new(@x.start, x_middle)
    x_b = Range2.new(x_middle, @x.end)
    y_a = Range2.new(@y.start, y_middle)
    y_b = Range2.new(y_middle, @y.end)
    SubdivisionRanges.new(x_a, x_b, y_a, y_b)
  end

  # Rect を x 軸と y 軸に沿って半分に分割し、4 つの細分割を返す
  #
  # サブディビジョンは次の順序で生成されます
  #
  # 1. Bottom left
  # 2. Bottom right
  # 3. Top left
  # 4. Top right
  def subdivisions
    subdivision_ranges.rects
  end

  # 各範囲の大きさが常に正になるように、self を絶対的な Rect に変換する
  def absolute
    self.class.new(@x.absolute, @y.absolute)
  end

  ################################################################################

  # 2 つの Rect が重なる領域を表す Rect
  def overlap(other)
    if x = @x.overlap(other.x) && y = @y.overlap(other.y)
      self.class.new(x, y)
    end
  end

  # 指定された 2 つの Rect セットを包含する Rect
  def max(other)
    self.class.new(@x.max(other.x), @y.max(other.y))
  end

  ################################################################################

  # Rect を x 軸に沿って移動します
  def shift_x(x)
    self.class.new(@x.shift(x), @y)
  end

  # Rect を y 軸に沿って移動します
  def shift_y(y)
    self.class.new(@x, @y.shift(y))
  end

  # 自分の右端を相手の Rect の左端に揃える
  def left_of(other)
    self.class.new(@x.align_before(other.x), @y)
  end

  # 自分の左端を相手の Rect の右端に揃える
  def right_of(other)
    self.class.new(@x.align_after(other.x), @y)
  end

  # 自分の上端を相手の Rectの下端に揃える
  def below(other)
    self.class.new(@x, @y.align_before(other.y))
  end

  # Align self's bottom edge with the top edge of the other Rect.
  def above(other)
    self.class.new(@x, @y.align_after(other.y))
  end

  # 自分の左端を相手の Rect の左端に揃える
  def align_left_of(other)
    self.class.new(@x.align_start_of(other.x), @y)
  end

  # 自分の右端を相手のRectの右端に揃える
  def align_right_of(other)
    self.class.new(@x.align_end_of(other.x), @y)
  end

  # self の下端を other Rect の下端に揃える
  def align_bottom_of(other)
    self.class.new(@x, @y.align_start_of(other.y))
  end

  # 自分の上端を相手の Rect の上端に揃える
  def align_top_of(other)
    self.class.new(@x, @y.align_end_of(other.y))
  end

  # 自分を相手の Rect の左上端に沿って配置する
  def top_left_of(other)
    align_left_of(other).align_top_of(other)
  end

  # 相手の Rect の右上端に沿って自分を配置する
  def top_right_of(other)
    align_right_of(other).align_top_of(other)
  end

  # 自分を相手の Rect の左下の端に沿って配置する
  def bottom_left_of(other)
    align_left_of(other).align_bottom_of(other)
  end

  # 相手の Rect の右下の端に沿って自分を配置する
  def bottom_right_of(other)
    align_right_of(other).align_bottom_of(other)
  end

  # 指定された位置に最も近い角を返す
  CLOSEST_CORNER_TABLE = {
    [:start, :start] => :bottom_left,
    [:start, :end]   => :top_left,
    [:end, :start]   => :bottom_right,
    [:end, :end]     => :top_right,
  }
  def closest_corner(v)
    CLOSEST_CORNER_TABLE.fetch([@x.closest_edge(v.x), @y.closest_edge(v.y)])
  end

  # Rect の四隅
  def corners
    [
      V[@x.start, @y.end],
      V[@x.end,   @y.end],
      V[@x.end,   @y.start],
      V[@x.start, @y.start],
    ]
  end

  # Rect を表す2つの三角形の頂点を返す
  def triangles
    a, b, c, d = corners
    [[a, b, c], [a, c, d]]
  end

  # Rect を指定されたベクトルだけシフトします
  def shift(v)
    shift_x(v.x).shift_y(v.y)
  end

  # 指定された位置が Rectangle に触れているかどうか
  def contains?(v)
    @x.contains?(v.x) && @y.contains?(v.y)
  end

  # 指定された位置が Rect 領域の外側にある場合、その位置に最も近い辺を引き伸ばす
  def stretch_to(v)
    self.class.new(@x.stretch_to_value(v.x), @y.stretch_to_value(v.y))
  end

  # 指定された位置が Rect 領域の外側にある場合、その位置に最も近い辺を引き伸ばす
  def stretch_to_point(v)
    stretch_to(v)
  end

  # 左端にパディングが適用された Rect
  def pad_left(pad)
    self.class.new(@x.pad_start(pad), @y)
  end

  # 右端にパディングが適用された Rect
  def pad_right(pad)
    self.class.new(@x.pad_end(pad), @y)
  end

  # 下端にパディングが適用された四角形
  def pad_bottom(pad)
    self.class.new(@x, @y.pad_start(pad))
  end

  # 上端にパディングが適用された Rect
  def pad_top(pad)
    self.class.new(@x, @y.pad_end(pad))
  end

  # 各辺にある程度のパディング量が適用された Rect
  def pad(pad)
    self.class.new(@x.pad(pad), @y.pad(pad))
  end

  # パディングが適用された Rect
  def padding(padding)
    self.class.new(@x.pad_ends(padding.x.start, padding.x.end), @y.pad_ends(padding.y.start, padding.y.end))
  end

  # x 軸上の指定された位置を基準とした相対位置を持つ Rect を返す
  def relative_to_x(x)
    self.class.new(@x.shift(-x), @y)
  end

  # y 軸上の指定された位置を基準とした相対位置を持つ Rect を返す
  def relative_to_y(y)
    self.class.new(@x, @y.shift(-y))
  end

  # 指定された位置を基準とした相対位置を持つ Rect を返す
  def relative_to(v)
    relative_to_x(v.x).relative_to_y(v.y)
  end

  # X 軸を反転する (別名: Y 軸を中心に反転する)
  def invert_x
    self.class.new(@x.invert, @y)
  end

  # Y 軸を反転する (別名: X 軸を中心に反転する)
  def invert_y
    self.class.new(@x, @y.invert)
  end

  ################################################################################

  def ==(other)
    self.class == other.class && @x == other.x && @y == other.y
  end

  def eql?(other)
    self.class == other.class && @x == other.x && @y == other.y
  end

  def hash
    self.class.hash ^ @x.hash ^ @y.hash
  end

  def <=>(other)
    [self.class, @x, @y] <=> [other.class, other.x, other.y]
  end

  def inspect
    "(#{@x}, #{@y})"
  end

  def to_s
    "(#{@x}, #{@y})"
  end

  SubdivisionRanges = Data.define(:x_a, :x_b, :y_a, :y_b) do
    def rects
      [
        Rect.new(x_a, y_a),
        Rect.new(x_b, y_a),
        Rect.new(x_a, y_b),
        Rect.new(x_b, y_b),
      ]
    end
  end
end

if $0 == __FILE__
  require "rspec/autorun"

  RSpec.configure do |config|
    config.expect_with :test_unit
  end

  describe Rect do
    it "x, y, w, h から作る" do
      a = Rect.from_x_y_w_h(0.0, 0.0, 100.0, 100.0)
      assert { a.x == Range2.new(-50.0, 50.0) }
      assert { a.y == Range2.new(-50.0, 50.0) }
    end

    it "w, h から作る" do
      a = Rect.from_w_h(100.0, 100.0)
      assert { a.x == Range2.new(-50.0, 50.0) }
      assert { a.y == Range2.new(-50.0, 50.0) }
    end

    it "対角から作る" do
      assert { Rect.from_corners(V[-10, 10], V[-10, 10]) == Rect.new(Range2.new(-10, -10), Range2.new(10, 10)) }
    end

    it "基本的な値の取得" do
      a = Rect.from_x_y_w_h(10.0, 20.0, 100.0, 100.0)
      assert { a.x_middle == 10.0 }
      assert { a.y_middle == 20.0 }
      assert { a.w == 100.0 }
      assert { a.h == 100.0 }
      assert { a.x_y == [10.0, 20.0] }
      assert { a.w_h == [100.0, 100.0] }
    end

    it "a を b の座標の align に揃える" do
      a = Rect.from_x_y_w_h(0.0, 0.0, 10.0, 10.0)
      b = Rect.from_x_y_w_h(0.0, 0.0, 100.0, 100.0)
      assert { a.align_x_of(:start, b) == Rect.new(Range2.new(-50.0, -40.0), Range2.new(-5.0, 5.0)) }
      assert { a.align_x_of(:middle, b) == Rect.new(Range2.new(-5.0, 5.0), Range2.new(-5.0, 5.0)) }
      assert { a.align_x_of(:end, b) == Rect.new(Range2.new(40.0, 50.0), Range2.new(-5.0, 5.0)) }

      assert { a.align_y_of(:start, b) == Rect.new(Range2.new(-5.0, 5.0), Range2.new(-50.0, -40.0)) }
      assert { a.align_y_of(:middle, b) == Rect.new(Range2.new(-5.0, 5.0), Range2.new(-5.0, 5.0)) }
      assert { a.align_y_of(:end, b) == Rect.new(Range2.new(-5.0, 5.0), Range2.new(40.0, 50.0)) }

      # a.align_x_of(:middle, b) と a.align_y_of(:middle, b) のショートカット:
      assert { a.align_middle_x_of(b) == Rect.new(Range2.new(-5.0, 5.0), Range2.new(-5.0, 5.0)) }
      assert { a.align_middle_y_of(b) == Rect.new(Range2.new(-5.0, 5.0), Range2.new(-5.0, 5.0)) }

      # a を b の内側の上下左右の辺にくっつける
      assert { a.mid_top_of(b) == Rect.new(Range2.new(-5.0, 5.0), Range2.new(40.0, 50.0)) }
      assert { a.mid_bottom_of(b) == Rect.new(Range2.new(-5.0, 5.0), Range2.new(-50.0, -40.0)) }
      assert { a.mid_left_of(b) == Rect.new(Range2.new(-50.0, -40.0), Range2.new(-5.0, 5.0)) }
      assert { a.mid_right_of(b) == Rect.new(Range2.new(40.0, 50.0), Range2.new(-5.0, 5.0)) }

      # a を b の中心に配置する
      assert { a.middle_of(b) == Rect.new(Range2.new(-5.0, 5.0), Range2.new(-5.0, 5.0)) }
    end

    it "中央で区切って上下左右の範囲を返す" do
      a = Rect.from_x_y_w_h(200.0, 300.0, 10.0, 10.0)
      b = a.subdivision_ranges
      assert { b.y_a == Range2.new(295.0, 300.0) }
      assert { b.y_b == Range2.new(300.0, 305.0) }
      assert { b.x_a == Range2.new(195.0, 200.0) }
      assert { b.x_b == Range2.new(200.0, 205.0) }
    end

    it "内部の方向を正にする" do
      a = Rect.new(Range2.new(1.0, -1.0), Range2.new(1.0, -1.0))
      assert { a == Rect.new(Range2.new(1.0, -1.0), Range2.new(1.0, -1.0)) }
      assert { a.absolute == Rect.new(Range2.new(-1.0, 1.0), Range2.new(-1.0, 1.0)) }
    end

    it "AND 領域" do
      a = Rect.from_x_y_w_h(100.0, 100.0, 100.0, 100.0)
      b = Rect.from_x_y_w_h(150.0, 150.0, 100.0, 100.0)
      assert { a.overlap(b) == Rect.new(Range2.new(100.0, 150.0), Range2.new(100.0, 150.0)) }
    end

    it "OR 領域" do
      a = Rect.from_x_y_w_h(100.0, 100.0, 100.0, 100.0)
      b = Rect.from_x_y_w_h(150.0, 150.0, 100.0, 100.0)
      assert { a.max(b) == Rect.new(Range2.new(50.0, 200.0), Range2.new(50.0, 200.0)) }
    end

    it "辺の座標" do
      a = Rect.from_x_y_w_h(100.0, 100.0, 100.0, 100.0)
      assert { a.left == 50.0 }
      assert { a.right == 150.0 }
      assert { a.bottom == 50.0 }
      assert { a.top == 150.0 }

      # まとめて
      assert { a.l_r_b_t == [50.0, 150.0, 50.0, 150.0] }
    end

    it "x y をそれぞれ移動" do
      a = Rect.from_x_y_w_h(0.0, 0.0, 100.0, 100.0)
      assert { a == Rect.new(Range2.new(-50.0, 50.0), Range2.new(-50.0, 50.0)) }
      assert { a.shift_x(25.0) == Rect.new(Range2.new(-25.0, 75.0), Range2.new(-50.0, 50.0)) }
      assert { a.shift_y(25.0) == Rect.new(Range2.new(-50.0, 50.0), Range2.new(-25.0, 75.0)) }
    end

    describe "相手のどこかに移動する" do
      let(:a) { Rect.from_x_y_w_h(0.0, 0.0, 10.0, 10.0)   }
      let(:b) { Rect.from_x_y_w_h(0.0, 0.0, 100.0, 100.0) }

      it "相手の辺の外側に移動する" do
        assert { a.left_of(b) == Rect.new(Range2.new(-60.0, -50.0), Range2.new(-5.0, 5.0)) }
        assert { a.right_of(b) == Rect.new(Range2.new(50.0, 60.0), Range2.new(-5.0, 5.0)) }
        assert { a.below(b) == Rect.new(Range2.new(-5.0, 5.0), Range2.new(-60.0, -50.0)) }
        assert { a.above(b) == Rect.new(Range2.new(-5.0, 5.0), Range2.new(50.0, 60.0)) }
      end

      it "相手の辺の内側に移動する" do
        assert { a.align_left_of(b) == Rect.new(Range2.new(-50.0, -40.0), Range2.new(-5.0, 5.0)) }
        assert { a.align_right_of(b) == Rect.new(Range2.new(40.0, 50.0), Range2.new(-5.0, 5.0)) }
        assert { a.align_bottom_of(b) == Rect.new(Range2.new(-5.0, 5.0), Range2.new(-50.0, -40.0)) }
        assert { a.align_top_of(b) == Rect.new(Range2.new(-5.0, 5.0), Range2.new(40.0, 50.0)) }
      end

      it "相手の角の内側に移動する" do
        assert { a.top_left_of(b) == Rect.new(Range2.new(-50.0, -40.0), Range2.new(40.0, 50.0)) }
        assert { a.top_right_of(b) == Rect.new(Range2.new(40.0, 50.0), Range2.new(40.0, 50.0)) }
        assert { a.bottom_left_of(b) == Rect.new(Range2.new(-50.0, -40.0), Range2.new(-50.0, -40.0)) }
        assert { a.bottom_right_of(b) == Rect.new(Range2.new(40.0, 50.0), Range2.new(-50.0, -40.0)) }
      end
    end

    it "指定の座標を含むように近い方の辺を広げる(比率が壊れる)" do
      a = Rect.from_x_y_w_h(0.0, 0.0, 10.0, 10.0)
      assert { a.stretch_to_point(V[6.0, 6.0]) == Rect.new(Range2.new(-5.0, 6.0), Range2.new(-5.0, 6.0)) }
    end

    it "指定の座標にいちばん近い角の名前を返す" do
      a = Rect.from_x_y_w_h(0.0, 0.0, 10.0, 10.0)
      assert { a.closest_corner(V[1.0, 1.0]) == :top_right }
      assert { a.closest_corner(V[-1.0, -1.0]) == :bottom_left }
      assert { a.closest_corner(V[-1.0, 1.0]) == :top_left }
      assert { a.closest_corner(V[1.0, -1.0]) == :bottom_right }
    end

    it "角の座標を返す" do
      a = Rect.from_x_y_w_h(0.0, 0.0, 10.0, 10.0)
      assert { a.corners == [[-5.0, 5.0], [5.0, 5.0], [5.0, -5.0], [-5.0, -5.0]] }
    end

    it "四角形を斜めに切ってできる三角形を得る" do
      a = Rect.from_x_y_w_h(0.0, 0.0, 10.0, 10.0)
      assert { a.triangles == [[[-5.0, 5.0], [5.0, 5.0], [5.0, -5.0]], [[-5.0, 5.0], [5.0, -5.0], [-5.0, -5.0]]] }
    end

    it "ベクトルや Point2 から作る" do
      assert { Rect.from_xy_wh(V[0.0, 0.0], V[10.0, 10.0]) == Rect.new(Range2.new(-5.0, 5.0), Range2.new(-5.0, 5.0)) }
      assert { Rect.from_wh(V[10.0, 10.0]) == Rect.new(Range2.new(-5.0, 5.0), Range2.new(-5.0, 5.0)) }
    end

    it "基本的な情報の参照" do
      a = Rect.from_wh(V[100.0, 100.0])
      assert { a.xy == V[0.0, 0.0] }
      assert { a.wh == V[100.0, 100.0] }
      assert { a.xy_wh == [V[0.0, 0.0], V[100.0, 100.0]] }

      assert { a.top_left == V[-50.0, 50.0] }
      assert { a.top_right == V[50.0, 50.0] }
      assert { a.bottom_left == V[-50.0, -50.0] }
      assert { a.bottom_right == V[50.0, -50.0] }

      assert { a.mid_left == V[-50.0, 0.0] }
      assert { a.mid_top == V[0.0, 50.0] }
      assert { a.mid_right == V[50.0, 0.0] }
      assert { a.mid_bottom == V[0.0, -50.0] }
    end

    it "x, y をまとめて移動" do
      a = Rect.from_wh(V[100.0, 100.0])
      assert { a.shift(V[10.0, 10.0]) == Rect.new(Range2.new(-40.0, 60.0), Range2.new(-40.0, 60.0)) }
    end

    it "指定の座標が含まれるか?" do
      a = Rect.from_wh(V[100.0, 100.0])
      assert { a.contains?(V[0.0, 0.0]) == true }
      assert { a.contains?(V[0.0, 51.0]) == false }
    end

    it "指定の座標が含まれるまで近い方を伸ばす" do
      a = Rect.from_wh(V[2.0, 2.0])
      assert { a.stretch_to(V[5.0, 5.0]) == Rect.new(Range2.new(-1.0, 5.0), Range2.new(-1.0, 5.0)) }
    end

    it "左上wh や 左下wh をまとめて得る" do
      a = Rect.from_wh(V[100.0, 100.0])
      assert { a.l_t_w_h == [-50.0, 50.0, 100.0, 100.0] }
      assert { a.l_b_w_h == [-50.0, -50.0, 100.0, 100.0] }
    end

    it "領域を内側に縮小する" do
      a = Rect.from_wh(V[100.0, 100.0])
      assert { a.pad_left(10.0) == Rect.new(Range2.new(-40.0, 50.0), Range2.new(-50.0, 50.0)) }
      assert { a.pad_right(10.0) == Rect.new(Range2.new(-50.0, 40.0), Range2.new(-50.0, 50.0)) }
      assert { a.pad_bottom(10.0) == Rect.new(Range2.new(-50.0, 50.0), Range2.new(-40.0, 50.0)) }
      assert { a.pad_top(10.0) == Rect.new(Range2.new(-50.0, 50.0), Range2.new(-50.0, 40.0)) }
      assert { a.pad(10.0) == Rect.new(Range2.new(-40.0, 40.0), Range2.new(-40.0, 40.0)) }
      p = Rect.new(Range2.new(10.0, 10.0), Range2.new(10.0, 10.0))
      assert { a.padding(p) == Rect.new(Range2.new(-40.0, 40.0), Range2.new(-40.0, 40.0)) }
    end

    it "相対的な範囲を返す" do
      a = Rect.from_wh(V[10.0, 10.0])
      assert { a == Rect.new(Range2.new(-5.0, 5.0), Range2.new(-5.0, 5.0)) }
      assert { a.relative_to_x(10.0) == Rect.new(Range2.new(-15.0, -5.0), Range2.new(-5.0, 5.0)) }
      assert { a.relative_to_y(10.0) == Rect.new(Range2.new(-5.0, 5.0), Range2.new(-15.0, -5.0)) }
      assert { a.relative_to(V[10.0, 10.0]) == Rect.new(Range2.new(-15.0, -5.0), Range2.new(-15.0, -5.0)) }
    end

    it "反転" do
      a = Rect.from_wh(V[2.0, 2.0])
      assert { a == Rect.new(Range2.new(-1.0, 1.0), Range2.new(-1.0, 1.0)) }
      assert { a.invert_x == Rect.new(Range2.new(1.0, -1.0), Range2.new(-1.0, 1.0)) }
      assert { a.invert_y == Rect.new(Range2.new(-1.0, 1.0), Range2.new(1.0, -1.0)) }
    end

    it "==" do
      assert { Rect.from_wh(V[2.0, 2.0]) == Rect.from_wh(V[2.0, 2.0]) }
    end
  end
end

早見表

種類 意味 Methods 同類・補足
生成 x, y, w, h から from_x_y_w_h from_xy_wh
生成 w, h から from_w_h from_wh
生成 対角から from_corners
参照 個別 x y w h
参照 配列 x_y w_h
参照 ベクトル型 xy wh xy_wh
参照 まとめて l_r_b_t l_t_w_h l_b_w_h
参照 十字の先端 mid_left mid_top mid_right mid_bottom
参照 left right bottom top
個別 top_left top_right bottom_left bottom_right
まとめて corners
座標に近い角の名前 closest_corner(xy) stretch_to_point の影響を受ける角が分かる
領域 上下左右 subdivision_ranges subdivisions
領域 対角切断時の三角形 triangles
対領域 相手との AND a.overlap(b)
対領域 相手との OR a.max(b)
移動 相手の軸の ? に a.align_x_of(?, b) align_y_of align_middle_of
移動 相手の内側の辺に a.mid_top_of(b) mid_bottom_of mid_left_of mid_right_of
移動 相手の中心に a.middle_of(b)
移動 相手の辺の外に left_of(b) right_of below above
移動 相手の辺の内に align_left_of(b) align_right_of align_bottom_of align_top_of
移動 相手の角の内に top_left_of(b) top_right_of bottom_left_of bottom_right_of
変形 軸を動かす shift(vec) shift_x shift_y
変形 指定座標を覆う stretch_to(vec) stretch_to_point(vec)
変形 縮小 pad pad_left pad_right pad_bottom pad_top padding
向き 反転 invert_x invert_y
向き 正にする absolute
その他 座標が含まれるか? contains?(vec)
その他 相対的な領域を返す relative_to(xy) relative_to_x relative_to_y

コンストラクタ

x, y, w, h から作る
Rect.from_xy_wh(V[0.0, 0.0], V[10.0, 10.0])  # => ((-5.0 -> 5.0), (-5.0 -> 5.0))
Rect.from_x_y_w_h(0.0, 0.0, 100.0, 100.0)    # => ((-50.0 -> 50.0), (-50.0 -> 50.0))
w, h から作る
Rect.from_wh(V[10.0, 10.0])  # => ((-5.0 -> 5.0), (-5.0 -> 5.0))
Rect.from_w_h(100.0, 100.0)  # => ((-50.0 -> 50.0), (-50.0 -> 50.0))
対角から作る
Rect.from_corners(V[-10, 10], V[-10, 10])  # => ((-10 -> -10), (10 -> 10))

基本的な値の取得

a = Rect.from_x_y_w_h(10.0, 20.0, 100.0, 100.0)
x, y
[a.x_middle, a.y_middle]  # => [10.0, 20.0]
a.x_y                     # => [10.0, 20.0]
a.xy                      # => (10.0, 20.0)
w, h
[a.w, a.h]  # => [100.0, 100.0]
a.w_h       # => [100.0, 100.0]
a.wh        # => (100.0, 100.0)
x, y, w, h
a.xy_wh  # => [(10.0, 20.0), (100.0, 100.0)]
a.top_left      # => (-40.0, 70.0)
a.top_right     # => (60.0, 70.0)
a.bottom_left   # => (-40.0, -30.0)
a.bottom_right  # => (60.0, -30.0)
辺の中央
a.mid_left    # => (-40.0, 20.0)
a.mid_top     # => (10.0, 70.0)
a.mid_right   # => (60.0, 20.0)
a.mid_bottom  # => (10.0, -30.0)
a.left    # => -40.0
a.right   # => 60.0
a.bottom  # => -30.0
a.top     # => 70.0
まとめて
a.l_r_b_t  # => [-40.0, 60.0, -30.0, 70.0]
a.l_t_w_h  # => [-40.0, 70.0, 100.0, 100.0]
a.l_b_w_h  # => [-40.0, -30.0, 100.0, 100.0]

整列

a を b の座標の align に揃える
a = Rect.from_wh(V[10.0, 10.0])
b = Rect.from_wh(V[100.0, 100.0])
a.align_x_of(:start, b)   # => ((-50.0 -> -40.0), (-5.0 -> 5.0))
a.align_x_of(:middle, b)  # => ((-5.0 -> 5.0), (-5.0 -> 5.0))
a.align_x_of(:end, b)     # => ((40.0 -> 50.0), (-5.0 -> 5.0))
a.align_y_of(:start, b)   # => ((-5.0 -> 5.0), (-50.0 -> -40.0))
a.align_y_of(:middle, b)  # => ((-5.0 -> 5.0), (-5.0 -> 5.0))
a.align_y_of(:end, b)     # => ((-5.0 -> 5.0), (40.0 -> 50.0))

a.align_x_of(:middle, b)a.align_y_of(:middle, b) のショートカット:

a.align_middle_x_of(b)  # => ((-5.0 -> 5.0), (-5.0 -> 5.0))
a.align_middle_y_of(b)  # => ((-5.0 -> 5.0), (-5.0 -> 5.0))

a を b の内側の上下左右の辺にくっつける

a.mid_top_of(b)     # => ((-5.0 -> 5.0), (40.0 -> 50.0))
a.mid_bottom_of(b)  # => ((-5.0 -> 5.0), (-50.0 -> -40.0))
a.mid_left_of(b)    # => ((-50.0 -> -40.0), (-5.0 -> 5.0))
a.mid_right_of(b)   # => ((40.0 -> 50.0), (-5.0 -> 5.0))

a を b の中心に配置する

a.middle_of(b)  # => ((-5.0 -> 5.0), (-5.0 -> 5.0))

中央で十字に区切る

a = Rect.from_x_y_w_h(200.0, 300.0, 10.0, 10.0)

座標

b = a.subdivision_ranges
b.x_a  # => (195.0 -> 200.0)
b.x_b  # => (200.0 -> 205.0)
b.y_a  # => (295.0 -> 300.0)
b.y_b  # => (300.0 -> 305.0)

領域

a.subdivisions[0]  # => ((195.0 -> 200.0), (295.0 -> 300.0))
a.subdivisions[1]  # => ((200.0 -> 205.0), (295.0 -> 300.0))
a.subdivisions[2]  # => ((195.0 -> 200.0), (300.0 -> 305.0))
a.subdivisions[3]  # => ((200.0 -> 205.0), (300.0 -> 305.0))

内部の方向を正にする

a = Rect.new(Range2.new(1.0, -1.0), Range2.new(1.0, -1.0))
a           # => ((1.0 -> -1.0), (1.0 -> -1.0))
a.absolute  # => ((-1.0 -> 1.0), (-1.0 -> 1.0))

同じ領域でも右下から左上の向きになっている場合がある。それを左上から右下方向に直す。

AND 領域

a = Rect.from_x_y_w_h(100.0, 100.0, 100.0, 100.0)
b = Rect.from_x_y_w_h(150.0, 150.0, 100.0, 100.0)
a.overlap(b)  # => ((100.0 -> 150.0), (100.0 -> 150.0))

OR 領域

a = Rect.from_x_y_w_h(100.0, 100.0, 100.0, 100.0)
b = Rect.from_x_y_w_h(150.0, 150.0, 100.0, 100.0)
a.max(b)  # => ((50.0 -> 200.0), (50.0 -> 200.0))

x y をそれぞれ移動

a = Rect.from_wh(V[100.0, 100.0])
a                # => ((-50.0 -> 50.0), (-50.0 -> 50.0))
a.shift_x(25.0)  # => ((-25.0 -> 75.0), (-50.0 -> 50.0))
a.shift_y(25.0)  # => ((-50.0 -> 50.0), (-25.0 -> 75.0))

相手のどこかに移動する

a = Rect.from_wh(V[10.0, 10.0])
b = Rect.from_wh(V[100.0, 100.0])
辺の外側 (below: 下, above: 上)
a.left_of(b)   # => ((-60.0 -> -50.0), (-5.0 -> 5.0))
a.right_of(b)  # => ((50.0 -> 60.0), (-5.0 -> 5.0))
a.below(b)     # => ((-5.0 -> 5.0), (-60.0 -> -50.0))
a.above(b)     # => ((-5.0 -> 5.0), (50.0 -> 60.0))
辺の内側
a.align_left_of(b)    # => ((-50.0 -> -40.0), (-5.0 -> 5.0))
a.align_right_of(b)   # => ((40.0 -> 50.0), (-5.0 -> 5.0))
a.align_bottom_of(b)  # => ((-5.0 -> 5.0), (-50.0 -> -40.0))
a.align_top_of(b)     # => ((-5.0 -> 5.0), (40.0 -> 50.0))
角の内側
a.top_left_of(b)      # => ((-50.0 -> -40.0), (40.0 -> 50.0))
a.top_right_of(b)     # => ((40.0 -> 50.0), (40.0 -> 50.0))
a.bottom_left_of(b)   # => ((-50.0 -> -40.0), (-50.0 -> -40.0))
a.bottom_right_of(b)  # => ((40.0 -> 50.0), (-50.0 -> -40.0))

指定の座標を含むように近い方の辺を広げる

a = Rect.from_wh(V[10.0, 10.0])
a.stretch_to_point(V[6.0, 6.0])  # => ((-5.0 -> 6.0), (-5.0 -> 6.0))

指定の座標にいちばん近い角の名前を返す

a = Rect.from_wh(V[10.0, 10.0])
a.closest_corner(V[1.0, 1.0])    # => :top_right
a.closest_corner(V[-1.0, -1.0])  # => :bottom_left
a.closest_corner(V[-1.0, 1.0])   # => :top_left
a.closest_corner(V[1.0, -1.0])   # => :bottom_right

角の座標を返す

a = Rect.from_wh(V[10.0, 10.0])
a.corners  # => [(-5.0, 5.0), (5.0, 5.0), (5.0, -5.0), (-5.0, -5.0)]

左上から右下に切ってできる2つの三角形を返す

a = Rect.from_wh(V[10.0, 10.0])
a.triangles[0]  # => [(-5.0, 5.0), (5.0, 5.0), (5.0, -5.0)]
a.triangles[1]  # => [(-5.0, 5.0), (5.0, -5.0), (-5.0, -5.0)]

x, y をまとめて移動

a = Rect.from_wh(V[100.0, 100.0])
a.shift(V[10.0, 10.0])  # => ((-40.0 -> 60.0), (-40.0 -> 60.0))

指定の座標が含まれるか?

a = Rect.from_wh(V[100.0, 100.0])
a.contains?(V[0.0, 50.0])  # => true
a.contains?(V[0.0, 51.0])  # => false

指定の座標が含まれるまで近い方を伸ばす

a = Rect.from_wh(V[2.0, 2.0])
a.stretch_to(V[5.0, 5.0])  # => ((-1.0 -> 5.0), (-1.0 -> 5.0))

領域を内側に縮小する

a = Rect.from_wh(V[100.0, 100.0])
a.pad_left(10.0)    # => ((-40.0 -> 50.0), (-50.0 -> 50.0))
a.pad_right(10.0)   # => ((-50.0 -> 40.0), (-50.0 -> 50.0))
a.pad_bottom(10.0)  # => ((-50.0 -> 50.0), (-40.0 -> 50.0))
a.pad_top(10.0)     # => ((-50.0 -> 50.0), (-50.0 -> 40.0))
a.pad(10.0)         # => ((-40.0 -> 40.0), (-40.0 -> 40.0))
b = Rect.new(Range2.new(10.0, 10.0), Range2.new(10.0, 10.0))
a.padding(b)        # => ((-40.0 -> 40.0), (-40.0 -> 40.0))

相対的な範囲を返す

a = Rect.from_wh(V[10.0, 10.0])
a                             # => ((-5.0 -> 5.0), (-5.0 -> 5.0))
a.relative_to_x(10.0)         # => ((-15.0 -> -5.0), (-5.0 -> 5.0))
a.relative_to_y(10.0)         # => ((-5.0 -> 5.0), (-15.0 -> -5.0))
a.relative_to(V[10.0, 10.0])  # => ((-15.0 -> -5.0), (-15.0 -> -5.0))

反転

a = Rect.from_wh(V[2.0, 2.0])
a           # => ((-1.0 -> 1.0), (-1.0 -> 1.0))
a.invert_x  # => ((1.0 -> -1.0), (-1.0 -> 1.0))
a.invert_y  # => ((-1.0 -> 1.0), (1.0 -> -1.0))

関連

https://zenn.dev/megeton/articles/fc94eb1fece2c5

Discussion