RoomPlanでスキャンした部屋の2D図面を表示する

RoomPlanがどういったものか知りたい方は公式や、以下の記事を参考にしてください。

https://developer.apple.com/jp/augmented-reality/roomplan/

https://qiita.com/kitanote/items/a914069a11503707fd4c

## 参考記事

以下のフォーラムの内容をベースに解説します。

https://developer.apple.com/forums/thread/713409

## CapturedRoomのデータの確認

[CapturedRoom](https://developer.apple.com/documentation/roomplan/capturedroom)をJSONエンコードすると以下のようなJSONになっています。

今回重要になってくるのが、dimensions（寸法）、transform（変換行列）の2つです。

``` room.json
{
    ... walls以外は省略 ...
    "walls": [
        {
            "category": {
                "wall": {}
            },
            "confidence": {
                "high": {}
            },
            "dimensions": [
                3.4638471603393555,
                2.3485732078552246,
                0
            ],
            "completedEdges": [],
            "parentIdentifier": null,
            "identifier": "3EFAE2ED-4537-4E74-B9B8-50DA8645184A",
            "curve": null,
            "transform": [
                0.4371742904186249,
                0,
                -0.8993767499923706,
                0,
                0,
                1,
                0,
                0,
                0.8993766903877258,
                0,
                0.4371742904186249,
                0,
                -1.9961872100830078,
                -0.15500573813915253,
                0.18577753007411957,
                1
            ]
        }
    ]
}
```

CapturedRoomをエンコードするコードは以下。

``` encode.swift
func toString(room: CapturedRoom) -> String {
    let encoder = JSONEncoder()
    if let encoded = try? encoder.encode(room) {
        return String(data: encoded, encoding: .utf8)!
    }
    return ""
}
```

エンコードした値を使ってCapturedRoomをイニシャライズできます。これで検証のたびに、毎回スキャンしてCapturedRoom生成しなくてよくなります。

``` decode.swift
let roomStr = "encodeした文字列"
let jsonData = roomStr.data(using: .utf8)!
let decoder = JSONDecoder()
let capturedRoom = try? decoder.decode(CapturedRoom.self, from: jsonData)
```


## 2Dに描画する

今回壁を描画するにあって、必要な要素は「壁の寸法」「壁の回転角」「壁の中心位置」の3つです。
1つめの寸法はdimensionsから算出することが可能です。

``` simd.swift
let surfacePath = CGMutablePath()
let span = CGFloat(dimensions.x) * mapScale / 2
surfacePath.move(to: CGPoint(x: -span, y: 0))
surfacePath.addLine(to: CGPoint(x: span, y: 0))
```

回転角、中心位置はtransformから算出することができます。

``` simd.swift
extension simd_float4x4 {
    var eulerAngles: simd_float3 {
        simd_float3(
            x: atan2(self[2][1], self[2][2]),
            y: atan2(self[0][2], self[0][0]),
            z: atan2(self[0][1], self[1][1])
        )
    }
    var position: simd_float3 {
        simd_float3(x: self.columns.3.x,
                    y: self.columns.3.y,
                    z: self.columns.3.z
        )
    }
}
```

フォーラムのコードをそのまま実行すると部屋を下から見た間取り図になります。
上からみた間取り図が一般的なので `position.y`, `zRotation` を以下のように変更しました。

``` wall.swift
wallShape.position.x =  CGFloat(wall.transform.position.x) * 200
wallShape.position.y = -CGFloat(wall.transform.position.z) * 200
wallShape.zRotation  = -CGFloat(wall.transform.eulerAngles.y)
```

ここまでのコードを反映すると以下のような間取り図が描画できます。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/510204/4dc39e8f-029a-c008-3882-ab0c7e5d4f18.jpeg" width=50%>

スキャンで生成された3Dモデルはこんな感じ。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/510204/dc1e08d4-4480-64a0-0415-6515a22ebbb8.jpeg" width=50%>


30秒ほどのスキャンでこのようなモデルと間取りができるのは素晴らしいのですが、
部屋全体が傾いていたり、右下の壁がが歪んでいたりしています。

壁の歪みなどはスキャンのやり方（正面から適切な距離をとって測る等）によって変わるため、ある程度はプログラムで補正することができます。

## 補正してみた間取り図

SceneKit上で壁の歪みなどを補正した結果が以下になります。
壁をブロックで描くことで、より間取り図っぽく仕上がりました。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/510204/92970add-7377-8725-39db-78fe8dd123b9.jpeg" width=50%>

## 開発したアプリ

10/24にRoomPlanを使ったアプリをストアに公開しました。

https://lp-measure.remodela.jp/

先ほどの部屋をリモデラメジャーで出力した画像は以下のようになっています。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/510204/9b7190d3-3810-1278-26e1-6d6ae15ad36b.jpeg" width=50%>