📐

坂道の上にある三角形の底辺と対辺の求め方

2023/09/04に公開

事前にわかっているのは a b c の位置だけで

a = V[400.0, 320.0]
b = V[681.9077862357725, 217.39395700229937]
c = V[443.4120444167326, 73.79806174694798]

そこから三角形 a c d の底辺と対辺の長さおよび点 d の位置を求めたい。

ここからベクトル ab ac は

ab = b - a  # => (281.9077862357725, -102.60604299770063)
ac = c - a  # => (43.41204441673261, -246.20193825305202)

で求まる。ただ直感的でないので日本語にしておく。

坂道 = ab  # => (281.9077862357725, -102.60604299770063)
斜辺 = ac  # => (43.41204441673261, -246.20193825305202)

底辺を求める

坂道を正規化して内積の公式 x1 * x2 + y1 * y2 に当てはめると底辺の長さがわかる。内積のメソッド名は一般的に dot, inner_product, dot_product などが使われる。

坂道.normalize.dot(斜辺)  # => 125.00000000000003

dot の中身は掛け算に似た感じになっているので右辺と左辺を入れ替えてもい。

斜辺.dot(坂道.normalize)  # => 125.00000000000003

ただ斜辺の方を正規化してしまうと坂道と斜辺の関係が逆になり、坂道を斜辺に投影したときの長さが求まり、わけがわからなくなるので注意する。

坂道.dot(斜辺.normalize)  # => 150.0

坂道の単位ベクトルに底辺の長さを掛けると底辺が求まる。

坂道.normalize * 坂道.normalize.dot(斜辺)  # => (117.46157759823858, -42.75251791570861)

最後に必要なら底辺の開始点 a を足せば点 d が求まる。

a + 坂道.normalize * 坂道.normalize.dot(斜辺)  # => (517.4615775982386, 277.2474820842914)

高速化と謎の公式

底辺こと線分 ad を求める方法はずっと

坂道.normalize * 坂道.normalize.dot(斜辺)  # => (117.46157759823858, -42.75251791570861)

しかないと思っていたのだが glam のコードは

坂道 * 坂道.dot(斜辺) / 坂道.length_squared  # => (117.46157759823858, -42.7525179157086)

となっている。前者は正規化の過程で平方根を求めているのに対して後者は四則演算のみで済んでいるので後者の方が圧倒的に速い。ただこの公式がどこからやってきたのかがずっとわからなかった。のだけどこちらの講座で謎が解けた。

要点をまとめると、まずこの絶対的な公式がある。

  1. 底辺 = (底辺の長さ / 坂道の長さ) * 坂道
  2. 底辺の長さ = 斜辺の長さ * cosθ
  3. 斜辺と坂道の内積 = 斜辺の長さ * 坂道の長さ * cosθ

(2) の cosθ に (3) の cosθ を代入すると

  • 底辺の長さ = 斜辺の長さ * (斜辺と坂道の内積 / 斜辺の長さ * 坂道の長さ)

になり、単純化すると

  • 底辺の長さ = 斜辺と坂道の内積 / 坂道の長さ

になるので、それを (1) の式に代入すると

  • 底辺 = ((斜辺と坂道の内積 / 坂道の長さ) / 坂道の長さ) * 坂道

になり、単純化すると

  • 底辺 = (斜辺と坂道の内積 / 坂道の長さの二乗) * 坂道

になり、それをそのままコードにすると

斜辺.dot(坂道) / 坂道.length_squared * 坂道  # => (117.46157759823856, -42.7525179157086)

となって変形させると

坂道 * 坂道.dot(斜辺) / 坂道.length_squared  # => (117.46157759823858, -42.7525179157086)

となり、それは glam のコードと同じなのだった。

対辺の長さを求める

内積によって点 d が求まったのだから点 c との距離を求めれば対辺の長さになる。

(c - d).length  # => 216.50635094610965

ただ外積を使うと一発で求められる。坂道を正規化し外積の公式 x1 * y2 - x2 * y1 に当てはめる。外積のメソッド名は一般的に cross, cross_product, perp_dot の名前が使われるのでこうなる。

坂道.normalize.cross(斜辺)  # => -216.50635094610965

ここでおもしろいのが負になっているところで坂道を堺に正負が反転する。距離だけではなくどちら側にあるのかまでわかる。

補足。内積の場合は掛け算だったので右辺と左辺関係なかったが、外積は引き算が含まれるので右辺と左辺を間違えると符号が逆になってしまう。そこだけ注意する。

斜辺.cross(坂道.normalize)  # => 216.50635094610965

点 e の求め方

a + 斜辺 - 底辺  # => (325.950466818494, 116.55057966265659)

c d の位置関係を a に反映すると e が求まる。

まとめ

底辺の長さ = 坂道.normalize.dot(斜辺)    # => 125.00000000000003
対辺の長さ = 坂道.normalize.cross(斜辺)  # => -216.50635094610965
# 遅い方法
底辺 = 坂道.normalize * 底辺の長さ  # => (117.46157759823858, -42.75251791570861)
# 速い方法
底辺 = 坂道 * 坂道.dot(斜辺) / 坂道.length_squared  # => (117.46157759823858, -42.7525179157086)
点d = a + 底辺       # => (517.4615775982386, 277.2474820842914)
点e = a + 斜辺 - 底辺  # => (325.950466818494, 116.55057966265659)

ライブラリ glam を使った場合

底辺 = 斜辺.project_onto(坂道)      # => (117.46157759823858, -42.7525179157086)
底d = a + 斜辺.project_onto(坂道)  # => (517.4615775982386, 277.2474820842914)
点e = a + 斜辺.reject_from(坂道)   # => (325.950466818494, 116.55057966265659)

どこの長さを知りたいか?

  • 横 → 内積
  • 縦 → 外積

参照

コード
class App < Base
  def initialize
    super

    self.width, self.height = V[800, 400]

    a = window_wh * V[0.50, 0.80]
    b = a + V.from_angle(-20.deg_to_rad).clamp_length_min(300)
    c = a + (b - a).rotate(-60.deg_to_rad).clamp_length_max(250)

    @points.concat([a, b, c])
  end

  def draw
    super

    a, b, c = @points

    ab = b - a
    ac = c - a

    if false
      d = a + ab.normalize * ab.normalize.dot(ac)
    else
      d = a + ab * ab.dot(ac) / ab.length_squared
    end

    e = a + ac.reject_from(ab)

    vputs "a: #{a.round}"
    vputs "b: #{b.round}"
    vputs "c: #{c.round}"
    vputs "d: #{d.round}"
    vputs "e: #{e.round}"

    vputs "ab: #{(b - a).round}"
    vputs "ac: #{(c - a).round}"
    vputs "ad: #{(d - a).round}"

    vputs "底辺adの長さ: #{ab.normalize.dot(ac).round}"
    vputs "対辺cdの長さ: #{ab.normalize.cross(ac).round}"
    vputs "角度: #{ab.angle_between(ac).rad_to_deg.round}"

    arrow_head(c, d, "d")
    arrow_head(a, e, "e")

    line_draw(c, d, color: :grey)
    line_draw(c, e, color: :grey)
    line_draw(a, e, color: :grey)

    vector_draw(a, b, " ", "b")
    vector_draw(a, c, "a", "c")

    point_draw(d)
    point_draw(e)
  end

  show
end

Discussion