Kivyの座標変換はややこしい

# はじめに


例えば以下のようなコードがあったとします。

```python3
from kivy.app import runTouchApp
from kivy.lang import Builder


root = Builder.load_string(r'''
FloatLayout:
    RelativeLayout:
        size_hint: 0.8, 0.8,
        pos_hint: {'center_x': 0.5, 'center_y': 0.5, }
        GridLayout:
            pos_hint: {'x': 0, 'y': 0, }
            cols: 2
            Button:
                id: button1
                text: '1'
            Widget:
                id: trigger
            Widget:
            Button:
                id: button2
                text: '2'
    RelativeLayout:
        size_hint: 0.8, 0.8,
        pos_hint: {'right': 1, 'top': 1, }
        id: renderer
''')

runTouchApp(root)
```

![coordinate_system.00.png](https://qiita-image-store.s3.amazonaws.com/0/144673/c79c0664-3402-fd5f-acfc-4089884e09bb.png)

Widget階層が少し複雑なので[Widgetの境界を可視化](https://qiita.com/gotta_dive_into_python/items/4391fee6e224f46003ad)させた時の結果も貼っておきます。
![coordinate_system.01.png](https://qiita-image-store.s3.amazonaws.com/0/144673/557ba92b-b55b-20af-efd2-0a57b99314cf.png)

ここで`button1`の中央から`button2`の中央へ線を引く処理を`renderer`に行わせたい時、どのようなコードを書きますか？もし素直に以下のように書いてしまうと

```
    RelativeLayout:
        size_hint: 0.8, 0.8,
        pos_hint: {'right': 1, 'top': 1, }
        id: renderer
        canvas:
            Line:
                points: [*button1.center, *button2.center, ]
```

期待する結果
![coordinate_system.04.png](https://qiita-image-store.s3.amazonaws.com/0/144673/a43748a7-1969-73e0-94de-5faa3edf7daf.png)

実際の結果
![coordinate_system.02.png](https://qiita-image-store.s3.amazonaws.com/0/144673/ad6f2fde-aaeb-0af3-2042-cafc706c1be2.png)

となり、失敗してしまいます。

また`trigger`の`on_touch_down`イベントが起きた時に、Windowの左下からマウスカーソルの位置へ線を引く処理を`renderer`に行わせたい時、どのようなコードを書きますか？これも素直に以下のように書いてしまうと

```
#:import Line kivy.graphics.Line

            Widget:
                id: trigger
                on_touch_down: renderer.canvas.add(Line(points=[0, 0, *args[1].pos, ]))
```

期待する結果
![coordinate_system.05.png](https://qiita-image-store.s3.amazonaws.com/0/144673/5035c481-d5d9-e4e4-4cac-5270f499db42.png)

実際の結果
![coordinate_system.03.png](https://qiita-image-store.s3.amazonaws.com/0/144673/65ff21b2-49fb-e164-09ce-78a13710ca76.png)

と失敗してしまいます。

これらは全て、得た座標をそのまま`Line`の`points`に渡しているのが原因で、正しい結果を得るためにはちゃんと座標変換を行う必要があります。この記事では上の問題を解くと共にKivyの座標の仕組みについての私の理解を述べたいと思います。

# ちょっとした逃げ道

その前にですが実は上のコードは、RelativeLayoutを全てFloatLayoutに置き換える事で期待する結果を得られます。 **Widget階層の中に`RelativeLayout系のWidget`が一つもない時は全ての座標が同じ座標系で表されているため、一切の変換の必要が無くなるのです。** `RelativeLayout系のWidget`を全く使わないというのはとても大きな制約ではありますが、座標変換が分からない時の逃げ道として役に立つかもしれません。

# 語彙

最初に幾つか本記事で使う言葉の定義します。

### 祖先

以下のようなWidget階層がある時、本記事では

![coordinate_system.ancestors.png](https://qiita-image-store.s3.amazonaws.com/0/144673/d46583f6-8fe0-f5da-78e4-7347acb2ce66.png)

- rootとBはCの祖先
- rootはAの祖先
- rootはBの祖先

と表現します。公式ドキュメントでは`parent stack`に相当する言葉です。

### RelativeLayout系のWidget

RelativeLayoutの子の位置指定は、RelatveLayoutの位置から相対的になります。これはRelativeLayoutが座標の原点を変えているからです。このように座標系を弄るWidgetは他にも`Scatter`,`ScrollView`があり、公式ではそれらを纏めて`RelativeLayout type widgets`と言っています。本記事ではこれを`RelativeLayout系のWidget`或いは`Relative系のWidget`と言うことにします。

### Window座標系

Windowの左下を原点とする座標系で、特に座標系を弄るWidgetが無い時の既定の座標系です。公式では`window coordinates`。


### Local座標系

公式では`local coordinates`。
Local座標系はWindow座標系とは違い、基準とするWidgetにより変わります。私は初めLocal座標系を各Widgetの左下を原点とする座標系なのだと思っていました。なにせ名前に`local`と入っているわけですから。ところが次のコードを見てください。

```python
from kivy.app import runTouchApp
from kivy.lang import Builder

root = Builder.load_string(r'''
FloatLayout:
    FloatLayout:
        id: hoge
        pos: 100, 100
        on_touch_down: print(self.to_widget(80, 80))
''')

runTouchApp(root)
```

`to_widget()`はWindow座標をLocal座標に変換するMethodです。この場合`hoge`の`to_widget()`を呼んでいるので、**Window座標(80,80)**を**hogeのLocal座標**に変換しています。私の予想する出力は`(-20,-20)`でした。hogeの位置はWindow座標(100,100)で、Window座標(80,80)はそこから(-20,-20)の位置にあるからです。しかし実際は`(80,80)`でした。どうやらLocal座標系は私の思っている概念とは違うようです。

色々調べた結果KivyにおけるLocal座標系は以下のように決まるという結論に至りました。

- **自身が`非Relative系のWidget`の時、自身のLocal座標系は親のLocal座標系に等しい。**
- **自身が`RelativeLayout`の時、自身の位置がLocal座標系の原点になる。** (話を単純にするためにRelativeLayoutに限定していますが、他の`Relative系のWidget`でも考え方は同じです。)

例えば以下のWidget階層がある時

```
FloatLayout:
    id: _1
    RelativeLayout:
        id: _2
        pos: 10, 10
        FloatLayout:
            id: _3
            pos: 1, 1
            FloatLayout:
                id: _4
                pos: 1, 1
                RelativeLayout:
                    id: _5
                    pos: 30, 30
                    Widget:
                        id: _6
                        pos: 1, 1
```

各WidgetのLocal座標系の原点は

| Widgetのid | Relative系か? | 自身の位置をWindow座標系で表すと | Local座標系の原点の位置をWindow座標系で表すと |
|:----------:|:------------:|:-----------------------------:|:------------------------------------:|
|_1   |No   |0, 0    |0, 0     |
|_2   |Yes  |10, 10  |10, 10   |
|_3   |No   |11, 11  |10, 10   |
|_4   |No   |11, 11  |10, 10   |
|_5   |Yes  |40, 40  |40, 40   |
|_6   |No   |41, 41  |40, 40   |

となります。

### 親の座標系

親のLocal座標系の事を略して`親の座標系`或いは`親座標系`と言うことにします。(公式では`parent coordinates`)。何故この言葉を用意したかというと、一番よく使うからです。例えば

- 普段何気なく使っている **Widgetの座標を表すプロパティ(pos, x, y, center, center_x, center_y, top, right)は全て持ち主の親のLocal座標系です** 。
- また **`on_touch_xxx系`のイベントで得られる座標も`bind`したWidgetの親のLocal座標系です。**
- **`collide_point()`へ渡す引数もMethodの持ち主の親のLocal座標系です。**

`on_touch_xxx系`で得た座標をそのまま`collide_point()`へ渡せるのはこの為でもあります。両方共同じ親のLocal座標系なため、何の座標変換もいらないのです。

# 冒頭の問題を解く

Local座標系が分かったので冒頭で述べた問題を解いていこうと思います。

## 再掲

```python
from kivy.app import runTouchApp
from kivy.lang import Builder


root = Builder.load_string(r'''
FloatLayout:
    RelativeLayout:
        size_hint: 0.8, 0.8,
        pos_hint: {'center_x': 0.5, 'center_y': 0.5, }
        GridLayout:
            pos_hint: {'x': 0, 'y': 0, }
            cols: 2
            Button:
                id: button1
                text: '1'
            Widget:
                id: trigger
            Widget:
            Button:
                id: button2
                text: '2'
    RelativeLayout:
        size_hint: 0.8, 0.8,
        pos_hint: {'right': 1, 'top': 1, }
        id: renderer
''')

runTouchApp(root)
```

![coordinate_system.00.png](https://qiita-image-store.s3.amazonaws.com/0/144673/c79c0664-3402-fd5f-acfc-4089884e09bb.png)

## 一つ目の問題

> `button1`の中央から`button2`の中央へ線を引く処理を`renderer`に行わせたい時、どのようなコードを書きますか？

ここで考えないといけないのは`button1.center`と`button2.center`が何の座標系で表されていて、`renderer`のcanvas内では何の座標系を使わないといけないのかです。前者はもう既に分かっています。親座標系の説明で書いたとおり、位置を表すプロパティは親座標系であるため、この場合はそれぞれ**button1の親座標系**,**button2の親座標系**という事になります。(因みに`button1`と`button2`は親が同じなので`button1の親座標系`と`button2の親座標系`は同じ物です)。後者なんですが、これは**`renderer`のLocal座標系**になります。canvasが何の座標系になっているかは基本的に

- `canvas.before`と`canvas`はLocal座標系
- `canvas.after`は親座標系

で、これに当てはまらない物は私は今の所ScrollView(もちろん派生Classも含む)しか知りません。

### to_xxx()系Method

それぞれが何の座標系なのか分かったので、後は変換するだけです。Kivyは変換を行うためのMethodとして以下の４つを提供してくれているので順番に見ていきます。

#### to_window()

既定では親座標をWindow座標に変換し、引数`initial`にFalseを渡した時だけLocal座標をWindow座標に変換します。例えば何かのWidget`hoge`がある時、`hoge.to_window(0, 0)`は **hogeの親座標(0, 0)** を **Window座標** に変換した結果を返し、`hoge.to_window(0, 0, initial=False)`は **hogeのLocal座標(0, 0)** を **Window座標** に変換した結果を返します。 ~~実は公式の説明では~~

> ~~Transform local coordinates to window coordinates. See relativelayout for details on the coordinate systems.~~

~~と書いてあり、これは訳すと「Local座標をWindow座標に変換する。詳しくは...」になり、私の説明とは異なります。私は[ソース][widget_to_xxx]を読んだりTestコードを書いたりしてそうとしか思えなかったので上の様に書きましたが、気になる人は自分で確かめるのが良いと思います。~~
(追記 2019/08/19: 現在は[doc][widget_to_xxx]は修正され、私の説明と同等の物になっています)

#### to_widget()

Window座標をLocal座標に変換します。例: `hoge.to_widget(0, 0)`は **Window座標(0,0)** を **hogeのLocal座標** に変換した結果を返す。

#### to_parent()

Local座標を親座標に変換します。例: `hoge.to_parent(0, 0)`は **hogeのLocal座標(0,0)** を **hogeの親座標** に変換した結果を返す。

#### to_local()

親座標をLocal座標に変換します。例: `hoge.to_local(0, 0)`は **hogeの親座標(0,0)** を **hogeのLocal座標** に変換した結果を返す。

### 一つ目の問題の続き

変換系Methodが分かったところで問題の続きをやりたいと思います。やらなければいけないのは **button1の親座標(button1.center)** と **button2の親座標(button2.center)** を **rendererのLocal座標** に変換することです。直接変換するのは無理なので

- button1の親座標(button1.center) -> Window座標 -> rendererのLocal座標
- button2の親座標(button2.center) -> Window座標 -> rendererのLocal座標

と一度Window座標を経由させます。

```
    RelativeLayout:
        size_hint: 0.8, 0.8,
        pos_hint: {'right': 1, 'top': 1, }
        id: renderer
        canvas:
            Line:
                points:
                    [
                    *self.to_widget(*button1.to_window(*button1.center)),
                    *self.to_widget(*button2.to_window(*button2.center)),
                    ]
```

結果
![coordinate_system.04.png](https://qiita-image-store.s3.amazonaws.com/0/144673/a43748a7-1969-73e0-94de-5faa3edf7daf.png)

## 二つ目の問題

> `trigger`の`on_touch_down`イベントが起きた時に、Windowの左下からマウスカーソルの位置へ線を引く処理を`renderer`に行わせたい時、どのようなコードを書きますか？

親座標系の説明で書いたとおりon_touch_xxx系のイベントで得られる座標は親座標系です。なのでこの問題では

- Window座標(0, 0) -> rendererのLocal座標
- triggerの親座標(touch.pos) -> Window座標 -> rendererのLocal座標

という変換をする事になります。

```
#:import Line kivy.graphics.Line

            Widget:
                id: trigger
                on_touch_down:
                    renderer.canvas.add(Line(points=[
                    *renderer.to_widget(0, 0),
                    *renderer.to_widget(*self.to_window(*args[1].pos)),
                    ]))
```

結果
![coordinate_system.05.png](https://qiita-image-store.s3.amazonaws.com/0/144673/5035c481-d5d9-e4e4-4cac-5270f499db42.png)

ちなみにKv言語を用いない場合は以下の様になります。

```python3
from kivy.graphics import Line

trigger = root.ids.trigger
renderer = root.ids.renderer

trigger.bind(on_touch_down=(
    lambda __, touch: renderer.canvas.add(
        Line(points=[
            *renderer.to_widget(0, 0),
            *renderer.to_widget(*trigger.to_window(*touch.pos)),
        ])
    )
))
```

## `canvas.after`を使う場合

上の例ではどちらも`canvas`を使いましたが、代わりに`canvas.after`を使う場合は以下のようになります。

<details><summary>一つ目の問題</summary><div>

```text
    RelativeLayout:
        size_hint: 0.8, 0.8,
        pos_hint: {'right': 1, 'top': 1, }
        id: renderer
        canvas.after:  # 相違点
            Line:
                points:
                    [
                    *self.parent.to_widget(*button1.to_window(*button1.center)),  # 相違点
                    *self.parent.to_widget(*button2.to_window(*button2.center)),  # 相違点
                    ]
```
</div></details>
<details><summary>二つ目の問題</summary><div>

```python3
from kivy.graphics import Line

trigger = root.ids.trigger
renderer = root.ids.renderer

trigger.bind(on_touch_down=(
    lambda __, touch: renderer.canvas.after.add(  # 相違点
        Line(points=[
            *renderer.parent.to_widget(0, 0),  # 相違点
            *renderer.parent.to_widget(*trigger.to_window(*touch.pos)),  # 相違点
        ])
    )
))
```
</div></details>

`canvas.after`では親座標系を使わないといけないのが紛らわしいです。

# 最後に

座標系は私が最も理解に時間を要したKivyの概念です。`to_window()`のドキュメントの罠さえ無ければ、もっと早く理解できていたと思いますが...。正直最も速く理解する方法はソースコードを読むことだと思います。というのも全部とても短くて分かりやすいコードだからです。以下にLinkを貼るので参考にどうぞ。

##### [kivy/uix/widget.py][widget_to_xxx]

全てのWidgetの基本Classである`kivy.uix.widget.Widget`のコードです。`非Relative系のWidget`はいっさい上の四つのMethodを上書きしていないので、全ての`非Relative系のWidget`の実装でもあります。

##### [kivy/uix/relativelayout.py](https://github.com/kivy/kivy/blob/master/kivy/uix/relativelayout.py#L283-L287)

`Relative系のWidget`はどれも`to_parent()`と`to_local()`だけを上書きしています。`Scatter`も`ScrollView`もやっている事の本質は`RelativeLayout`と同じなので、`RelativeLayout`のコードを読むだけで十分だと思います。

##### [kivy/core/window/\_\_init\_\_py](https://github.com/kivy/kivy/blob/a432e0d73a983b94b768b90f1be7a16895d04670/kivy/core/window/__init__.py#L1310-L1314)

`Widget.to_window()`と`Widget.to_widget()`は内部で親の同名のMethodを呼び出していますが、その呼び出しの連鎖の行き着く先がこれです。

[widget_to_xxx]:https://github.com/kivy/kivy/blob/3d87584b963a79465ed52e09f4de17395c6aae60/kivy/uix/widget.py#L955-L1000