Upgrade to Pro — share decks privately, control downloads, hide ads and more …

幾何アルゴリズムで なめらかなピン操作を / iOSDC Japan 2026 / smoot...

Sponsored · Ship Features Fearlessly Turn features on and off without deploys. Used by thousands of Ruby developers.
Avatar for nade nade
September 12, 2026

幾何アルゴリズムで なめらかなピン操作を / iOSDC Japan 2026 / smoothpin

2026/09/12 15:35〜Track Bレギュラートーク(20分)
https://fortee.jp/iosdc-japan-2026/proposal/3c119e7a-235a-4073-b00f-6024063e229c

Avatar for nade

nade

September 12, 2026

More Decks by nade

Other Decks in Technology

Transcript

  1. 自己紹介 永野 一馬 / なで(@nade_kaz) 2025年3月〜 newmo。タクシー配車アプリと、点呼・給与・勤怠などタク シー会社のシステム DX iOS

    だけでなく Web・Backend も / なで肩 ⇨ なで / 数学は専門外 新大阪駅で実証実験中(〜10月):事前予約で newmo 専用乗り場から並ば ずに乗れます。大阪にお越しの際はぜひ タクシー配車アプリ「newmo」 2024年12月 大阪から提供開始 自動点呼「newmo点呼」 国交省認定 2025/7・2026/2 新大阪駅 予約タクシー実証実験 JR西日本と 2026/8〜10。自動運転との 結節を見据えて 2
  2. 今日の話の元ネタ tech.newmo.me ・ 2025-12-15 Coding Agentと幾何学アルゴ リズムを活用した地図上の滑らか なピン操作の実現 tech.newmo.me ・

    2026-02-24 【お蔵入り】ライドシェアド ライバーアプリのSwiftUI作 品集! tech.newmo.me ・ 2025-12-11 プロダクトチーム全員でAIエ ージェント縛りの開発を一週 間試しました 今日の話の元ネタ 3
  3. 今日のテーマ 幾何アルゴリズム 🤔 ❌ ❌ 数学ガチ勢の 地図 SDK の 講義?

    使い方講座? ❌ AI に全部 書かせた話? ⇨ 地図 UX を、幾何(= 座標変換や図形計算)で 「なめらか」にする話 5
  4. なぜ、いま幾何なのか? 1 幾何は 20 世紀に枯れた知識 凸包も Graham Scan も 1972

    年。実装例が豊富で、Agent の精度が高い 2 純粋なロジックは TDD と相性がいい 成功・失敗が明確。期待値をテストで書き、実装は Agent に任せる 3 Agent から数学を学ぶのは楽しい 「なぜこの式?」をその場で聞ける。手計算より Agent の方が正しかったことも 6
  5. 実現①:現実世界の座標で、地図上に結ぶ 描画済みの領域(タイル) 画面 画面外にも描いている 現在地 現在地とピン(緯度経度)を、地図側 の Polyline で結ぶ 現実世界

    ⇨ 地図 素直。でも重い 地図は、画面より大きな巨大なスクロ ールビュー。見えない所まで描く そこに、指を動かすたびに変わる線を 描き直す ⇨ 全体の更新に巻き込まれ る 14
  6. そもそも「座標」って、どこにあるんだっけ? 現実世界 緯度・経度 実際の場所 ⇄ 地図 位置・ポリゴン タイルの上 ⇄ スマホ画面

    ピクセル(CGPoint) 見えている範囲だけ 現実世界 ⇨ 地図 をやめて、現実世界 ⇨ 画面 で解く 15
  7. 実現②:現実世界 ⇨ 画面の座標へ(軽量化) 現在地(緯度経度)を、地図の投影で画面上の CGPoint に変換 地図の上に別レイヤーを重ね、SwiftUI の Path で点線を描く

    地図ビューには何も渡さない ⇨ 地図は再描画されない 描くのは画面の中だけ。現在地が画面外なら、画面枠との交点まで 16
  8. 参考:【数学B】直線のベクトル方程式 媒介変数 t P(t) = C + t × (T

    − C) 画面(x = xmax の辺) ピン(画面の中心) T 現在地 t C から T に向かって「何割」進んだか(0 〜 1) P(t) その割合の位置にある点 C C(t = 0) t = 0.5 枠に当たる t T(t = 1) つまり 上下左右の辺ごとに「その辺に着く t」 を計算し、いちばん小さい t(最初にぶつかる辺) で線を止める 18
  9. 画面枠との交点 — コード — // 画面枠との交点比率 t を x, y

    それぞれで求め、小さい方を採用 let tx = dx == 0 ? .infinity : ((dx > 0 ? bounds.maxX : bounds.minX) - center.x) / dx let ty = dy == 0 ? .infinity : ((dy > 0 ? bounds.maxY : bounds.minY) - center.y) / dy let t = min(tx, ty) return CGPoint(x: center.x + dx * t, y: center.y + dy * t) 19
  10. 参考:【中3】三平方の定理と単位ベクトル ベクトルの大きさ・単位ベクトル C(ピン) ここから描く(r + 線幅) |v| = √(vx²+vy²) e(長さ1)

    T(現在地) vx |v| = √(vx² + vy²), e = v ÷ |v| 現在地からピンへ向かう矢印(横 vx、縦 vy) |v| その矢印の長さ(三平方の定理) e 長さを 1 にした矢印。向きだけを表す r 現在地マーカーの半径 v vy つまり 始点 = 現在地 + e × (r + 線幅)。「ピ ンの方向に、r + 線幅 だけ進んだ点」から線を 描く 21
  11. マーカーを避ける — コード — // 現在地 → ピン方向の単位ベクトル let ux

    = v.x / length, uy = v.y / length // マーカー半径+線幅ぶん、ピン側へ寄せた点を始点に let adjusted = Self.currentLocationPinRadius + Self.lineWidth return CGPoint(x: target.x - ux * adjusted, y: target.y - uy * adjusted) 22
  12. じゃあ、自動で外に押し出したい 置けない場所に来たら、 置ける場所へそっと逃がす どこへ? ⇨ いちばん近い、エリアの外 いつ? ⇨ 指を離した時 どうやって?

    ⇨ 地図上の幾何で解く 非推奨エリ 置ける場所に収まった 指を離すと、 いちばん近い外へ地図がすっと動く 29
  13. 押し出しは3ステップ 1 各辺への最近点(垂線の足)⇨ いち ばん近い辺を選ぶ 2 押し出す向き:ピン ⇨ 足 の直線

    (辺に垂直で、外向き) 3 その向きに一定距離ずらす 禁止エリア ピン 垂線の足 32
  14. 参考:【数学B】内積と正射影 正射影ベクトル (P − A) · (B − A) t=

    , |B − A|² P(ピン) AH = t · AB A(t = 0) B(t = 1) H(垂線の足) H = A + t × (B − A) 辺の両端の点 P ピン H P から辺に下ろした垂線の足。辺の上で P にいちばん近 い点 t A から H までが、AB の「何割」か · 内積。x 同士と y 同士を掛けて足す A, B つまり t が 0〜1 なら H は辺の上。0 より小さ ければ A、1 より大きければ B がいちばん近い ⇨ max(0, min(1, t)) 33
  15. 最近点は t を 0…1 に丸めるだけ // 線分 start→end 上への射影パラメータ t

    を 0...1 に clamp let t = max(0, min(1, (px * dx + py * dy) / lengthSquared)) // 線分上の最近点(垂線の足)。t が 0 か 1 なら端点 let foot = CLLocationCoordinate2D( latitude: start.latitude + t * dy, longitude: start.longitude + t * dx ) 辺ごとに計算 ⇨ 距離が最小の辺を採用 34
  16. 押し出す向き 通常 ⇨ ピン → 足 の直線 境界線上 ⇨ 辺の法線

    辺に垂直で、外向き 直線の長さ 0 ⇨ 向きが決まらない ピン(辺の上) ピン(エリア内) 2方向ある ⇨ 外側の方 境界線上(共有辺・隙間ゼロ・再帰の途中)では直線の長さが 0 ⇨ 辺に垂直な向きのうち、外側になる方を選ぶ 35
  17. 参考:【中学】垂直な直線と法線ベクトル 法線ベクトル に垂直な矢印は (−d , d ) または (d ,

    −d ) d = (dx, dy) ( dy , − dx ) 2方向ある ⇨ 外側の方を採用 y x y x 辺に沿った矢印(横 dx、縦 dy) 法線 辺に垂直な矢印。x と y を入れ替えて片方の符号を反 転すると 90° 回る ± 垂直な矢印は表と裏の 2 本ある d d = ( dx , dy ) (−dy, dx) つまり 2 本のうち、少し進んでみてポリゴンの 外に出る方を採用。コードでは辺の方位角 ± 90° で同じことをしている 36
  18. 押し出す向き — コード — // 境界のすぐ近くでは法線、離れていれば直線 if distanceBetween(coordinate, foot) <

    1.0 { calculateNormalHeading(at: foot, on: path) ?? 0 } else { computeHeading(from: coordinate, to: foot) } 境界線上の判定は「足まで 1m 未満」。法線は2方向あるので、少し進んで外側になる方を採用 37
  19. どれくらい逃がす? — メートルを緯度経度に戻す — 「2m 逃がす」の 2m は距離。緯度経度は角度 足の位置と向きは、緯度経度を平面とみなして計算できる (数十

    m の範囲なら誤差は無視できる) でも「その向きに 2m」は、緯度経度にそのまま足せない 方位角と距離から、球面上で 2m 先の緯度経度を求めてカメラに渡す 最後は必ず緯度経度に戻す。地図を動かす指示は、緯度経度でしか出せない 38
  20. 参考:【数学II】弧度法と弧の長さ l = Rθ l=R×θ θR l = Rθ 緯度・経度は「角度」

    地球:R ≈ 6,371 km ⇔ θ=l÷R 地球の半径。約 6,371 km θ 中心角。ラジアン(360° = 2π) l 弧の長さ = 地面の上を進む距離 R つまり 緯度経度は「角度」。2 m 進みたいなら θ = 2 ÷ 6,371,000 ラジアンだけ角度を動か す。コードの distance / R がこれ 39
  21. 発想:回避領域を動的に広げる 「押し出し先がまた禁止エリアなら、そこも取り込む」 1 2 3 4 ピンが A ⇨ A

    の外へ押し出す 押し出し先が B ⇨ A+B を統合 統合エリアの境界から、もう一度 入らなくなるまで再帰(上限5回) 外殻の外へ、一発で脱出 A+B を凸包で1つに 凸包(Convex Hull)= 点の集まりを輪ゴム で囲んだ外形 42
  22. そもそも凸包アルゴリズムの歴史 凸包の歴史① 1972 Graham Scan 極角でソート ⇨ 左折だけ残す。O(N log N)

    凸包の歴史② 1973 ギフト包装法(Gift wrapping) 包み紙を巻くように外周をたどる。簡単、O(N·h) 凸包の歴史③ 1979 Monotone chain(Andrew) x でソート ⇨ 上下の殻を作る。Graham の派生 参照:Wikipedia「Graham scan」「Gift wrapping algorithm」/ 50年前に枯れている(前提 1) 43
  23. 検討:統合の仕方 1 ポリゴン結合(Polygon Union)— 断念 凹みは保てる。でも低速、自己交差で壊れやすい 2 凹包(Concave Hull)— 断念

    大小さまざまなエリアでは壊れやすい 3 凸包(Convex Hull)— 採用 O(N log N)、枯れている。凹みは消える 4 外接矩形(Bounding Box)— 断念 最速だが L 字を潰しすぎ 44
  24. 注意点:凸包は凹みを埋める 凸包は、凹みを埋めてしまう 膨らみ(Bulge) 本当は置けるのに避ける A 点線 = A+B の凸包 B

    影響が出るのは「押し出しでループし た時」だけ 直接そこに置けば、制御は走らない 「自動で逃がす時だけ、少し広めに避 ける」割り切り 46
  25. コード(骨格・簡略化) // 通過した禁止エリアを凸包で1つの境界に let unified = createUnifiedPath(from: accumulated) let foot

    = findClosestEdge(to: origin, on: unified).foot let next = computeOffset(from: foot, distance: 2, heading: h) // 押し出し先が、まだ別の禁止エリアの中? 安全なら終了 let hits = allRestricted.filter { $0.contains(next) } if hits.isEmpty { return next } // 見つかったエリアを蓄積して再帰(起点は変えない・上限5回) return push(origin, accumulated: accumulated + hits, depth: depth + 1) 47
  26. 参考:【数学B】外積の符号と回転の向き 2次元の外積(行列式) c b 右折 ⇨ 負 ⇨ 捨てる a

    左折 ⇨ 正 ⇨ 残す c b a (bx − ax)(cy − ay) − (by − ay)(cx − ax) 順番に並んだ 3 つの点 外積 a→b と a→c の 2 本の矢印が「どちら回りか」を表 す数。三角形の面積公式 ½|x1y2 − x2y1| の中身 a, b, c つまり 正なら反時計回り(左折)で残す、負な ら時計回り(右折)で捨てる、0 なら一直線。極 角は atan2 で求めてソート 49
  27. Graham Scan — コード — // 起点を基準に極角でソート(O(N log N) の肝)

    let sorted = points .filter { distanceBetween($0, start) >= 0.1 } .sorted { polarAngle($0) < polarAngle($1) } // atan2 // 左折だけ残す(O(N))。外積の符号で右折を落とす return sorted.reduce(into: [start]) { hull, p in removeRightTurns(&hull, newPoint: p) hull.append(p) } 50
  28. Before / After — 隣接エリアの押し出し — Before:今いるエリアだけ見る A B After:入った先を取り込む(凸包)

    A B C C 6 回目:A の外へ ⇨ B の中 …終わらない 3 回目:起点から A+B+C の凸包の外へ ⇨ 脱出 同じ位置から開始。Before は A と B を往復し続ける After は起点を固定したまま凸包を広げ、3 回目で外へ 51
  29. 体験の変化 — 従来 vs 現在 — 課題 現在地を見失う 点線の描画 非推奨エリア

    隣接エリア 従来 ピンだけ動く 地図側で描いて重い・消える 押してからエラー、どこがセーフか不明 往復して止まらない 現在 点線で結ぶ 画面座標で、画面内だけ描く 置ける場所へ、そっと押し出す 凸包で統合、一発で脱出 53