Skip to content

fix(crane_geometry): 円と線分の交点計算を二次方程式解に置き換え、テストを追加 - #1386

Merged
HansRobo merged 1 commit into
developfrom
fix/geometry-circle-segment-intersection
Sep 17, 2026
Merged

HansRobo merged 1 commit into
developfrom
fix/geometry-circle-segment-intersection

Conversation

@HansRobo

@HansRobo HansRobo commented May 30, 2026

Copy link
Copy Markdown
Member

概要

crane_geometrygetIntersections(Circle, Segment) が、交点として円周上にない点線分上にない点を返していた問題を修正します。垂線足を経由する構成をやめ、媒介変数の二次方程式を直接解く実装に置き換えたうえで、欠落していたテストを追加します。

初版からの変更: 当初は bg::distanceclosest_point に差し替える修正でしたが、検証の結果それでは欠陥の片方しか解消せず、一部のケースでは修正前より悪化することが分かったため、実装を全面的に置き換えました。また初版の本文にあった「本関数は defense_functions.cpp(防御陣形)で実利用されており、防御位置計算に影響していました」という記述は誤りです(下記「影響範囲」参照)。

問題

本関数は「円中心から線分への垂直距離 d」を求め、垂線足から線分方向に弦半長 sqrt(r^2 - d^2) だけ±して 2 交点を算出する構成でした。しかし d の取得が二重に誤っていました。

  1. 半径の減算: bg::distance(circle, segment)circle.hpp のオーバーロードにより max(0, 中心距離 - 半径) を返します。線分が円と交わる場合は常に 0 にクランプされるため、弦半長が常に r になり、垂線足が円中心へ潰れます。
  2. 線分へのクランプ: そもそも「線分への距離」は線分にクランプされるため、垂線足が線分の外に落ちる配置では垂直距離になりません。closest_point(segment, center) に置き換えても 2. は解消しません。

実測(ランダム2万ケース、媒介変数の二次方程式解をオラクル)で「円周上にない」または「線分上にない」点を返した割合:

実装 不正な点を返した割合
develop(1. と 2. の両方) 39.9%
1. のみ修正(本PR初版) 7.4%
本PR(二次方程式解) 0.0%

具体例:

  • C=(-6, 0) r=2.5, seg=(-5, 0.5)-(-4, 3.5)(垂線足が線分外): 初版は (-4.2929, 2.6213) を返すが |p-C| - r = +0.628円周上ですらない。正解は (-4.5, 2.0)
  • C=(2, 0) r=1.5, seg=(0, 0)-(1, 0): 初版は交点0個。実在する交点 (0.5, 0) を取りこぼす(develop は偶然正しく返していた)
  • 接するケース: 初版・develop とも同一点を 2 個返すため、呼出元の case 1:(交点1個)分岐が事実上デッドだった

修正内容

線分を P(t) = first + t * (second - first), t ∈ [0, 1] と媒介変数表示し、|P(t) - center|^2 = radius^2 を直接解きます。

  • クランプに関する前提が一切不要
  • 端点上の交点も t ∈ [0, 1] に自然に含まれる(従来は > 0 の狭義判定で落としていた)
  • 接する場合は重解を 1 点に畳むため、戻り値の個数が交点数と一致する(意味論の変更点
  • a = |dir|^2 > 0 なので交点は始点側から終点側の順に並ぶ

テスト

この不具合が長く残った原因は test_geometry.cppgetIntersections のカバレッジが皆無だったことなので、併せて追加しました。

個別の期待値に加えて、「返る点が円周上にあり、かつ線分上にある」という不変条件を全ケースで検証するヘルパを置いています。これ 1 つで上記 2 系統の欠陥を同時に捕捉できます。シード固定のランダム2000ケースにも適用しています。

検証:

  • closest_point を使う実装(本PR初版)では 6 テストが失敗することを確認(うち 1 件は本PRで新たに規定した並び順の契約による)
  • 本PRの実装で 34 tests, 0 errors, 0 failures, 0 skippedtest_geometry.gtest.xml は 10 → 17 テスト)
  • pre-commit(clang-format / cpplint)通過

影響範囲

getIntersections(Circle, Segment) の唯一の呼出元は crane_sessions/src/defense_functions.cppgetDefenseArcPoints ですが、その getDefenseArcPoints 自体が ibis_ws/src 全体から一度も呼ばれていない dead code です。したがって本修正による実行時の挙動変化はありません。防御位置計算に現存する不具合もありません。

getDefenseArcPoints を残すか削除するかは別PRで整理します。

別途対応(本PRの範囲外)

Circle は Boost.Geometry に point_tag として登録されているため、引数順で distance の意味が変わり、何も警告されません。

  • bg::distance(circle, p) → 表面距離(0クランプ)… circle.hpp の自前オーバーロード
  • bg::distance(p, circle) → 中心距離 … Boost の point-point フォールバック

Capsulesegment_tag)も同形です。現状の呼出元はたまたま正しい側を使っていますが、本件の根本原因はここにあります。ただし test_geometry.cpp が表面距離の意味論をピン留めしているため、削除ではなく boost::geometry 名前空間の外へのリネームが安全です。別issueとして切り出します。

🤖 Generated with Claude Code

@HansRobo
HansRobo force-pushed the fix/geometry-circle-segment-intersection branch from e73ffee to 8b9ef88 Compare September 4, 2026 03:53
@HansRobo
HansRobo force-pushed the fix/geometry-circle-segment-intersection branch 2 times, most recently from 4e6b7ca to 2d192fd Compare September 17, 2026 09:08
@HansRobo HansRobo changed the title fix(crane_geometry): getIntersections(Circle,Segment)の交点計算破綻を修正 fix(crane_geometry): 円と線分の交点計算を二次方程式解に置き換え、テストを追加 Sep 17, 2026
getIntersections(Circle, Segment) は「円中心から線分への垂直距離」を前提に
垂線足から弦半長だけ±する構成だったが、その距離の取得が二重に誤っていた。

1. bg::distance(circle, segment) は circle.hpp のオーバーロードにより
   max(0, 中心距離 - 半径) を返す。線分が円と交わる場合は常に 0 にクランプされ、
   弦半長が常に radius になり垂線足が円中心へ潰れる。
2. そもそも「線分への距離」は線分にクランプされるため、垂線足が線分の外に
   落ちる配置では垂直距離にならない。closest_point(segment, center) に
   置き換えても 2. は解消しない。

実測(ランダム2万ケース、媒介変数の二次方程式解をオラクルとする)で
「円周上にない」または「線分上にない」点を返した割合は、
develop の実装(1. と 2. の両方)が 39.9%、
1. のみを修正して closest_point を使う実装でも 7.4% 残る。

線分を P(t) = first + t * (second - first), t in [0, 1] と媒介変数表示して
|P(t) - center|^2 = radius^2 を直接解く実装に置き換える。クランプに関する
前提が一切不要になり、端点上の交点も t in [0, 1] に自然に含まれる。
接する場合は重解を 1 点に畳むため、戻り値の個数が交点数と一致する
(従来は同一点を 2 個返していた)。交点は始点側から終点側の順に並ぶ。

この不具合が長く残った原因は test_geometry.cpp に getIntersections の
カバレッジが皆無だったことなので、併せてテストを追加する。個別の期待値に
加えて「返る点が円周上にあり、かつ線分上にある」という不変条件を全ケースで
検証するヘルパを置き、シード固定のランダムケースにも適用する。
closest_point を使う実装では 6 テストが失敗することを確認済み
(うち 1 件は本コミットで新たに規定した並び順の契約による)。

なお唯一の呼出元 getDefenseArcPoints は現在どこからも呼ばれていないため、
本修正による実行時の挙動変化はない。同関数の扱いは別途整理する。
@HansRobo
HansRobo force-pushed the fix/geometry-circle-segment-intersection branch from 2d192fd to 373f7a5 Compare September 17, 2026 09:24
@HansRobo
HansRobo merged commit e9483bc into develop Sep 17, 2026
8 checks passed
@HansRobo
HansRobo deleted the fix/geometry-circle-segment-intersection branch September 17, 2026 09:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant