開発ブログ

開発日誌(2) 移動ターゲット対応とシナリオ切替

これは開発日誌(1) の続きです。前回(M0〜MVP-0+)でAstro基盤・JSONスキーマ・CCRsデモを確立しました。今回は「3トラック並行稼働の第1バッチ」として進めた、シミュレーターの移動ターゲット対応とシナリオ切替UIの導入を記録します。

今回やったこと:全体像

  1. CCRm(走行中前方車両)シナリオの追加data/scenarios/euro-ncap/ccrm.json
  2. kinematics.ts の移動ターゲット一般化(相対速度モデル v_rel = max(v_s − v_t, 0))
  3. シナリオ・レジストリの新設src/sim/scenarios.ts
  4. OverheadDemo のシナリオ選択UI化src/components/OverheadDemo.astro

これら4点は並行して進めた3配下(S-content / S-sim / S-scenario)の成果物を統括がまとめて統合・コミットしたものです(commit d75577c)。


CCRmシナリオの追加

data/scenarios/euro-ncap/ccrm.jsonccrs.json に続く2件目のシナリオとして作成しました。

CCRm(Car-to-Car Rear moving)は、自車(VUT)が一定速度で走行中の前方車両(GVT)に追突するシナリオです。CCRs との最大の違いは「相手が動いている」ことで、衝突エネルギーを決めるのは絶対速度ではなく相対速度(自車速度 − ターゲット速度)です。Euro NCAP のプロトコルでは Inter-Urban 帯(幹線道路・高速道路想定)の試験として位置づけられています。

パラメータ定義(抜粋)

keyラベル範囲デフォルト
vut_speed_kmph自車の接近速度30–80 km/h, 5刻み50 km/h
target_speed_kmph前方ターゲット速度0–40 km/h, 5刻み20 km/h
initial_gap_m初期車間距離10–80 m, 1刻み40 m
brake_capability_g想定ブレーキ能力0.3–1.0 g, 0.05刻み0.9 g

CCRs にはなかった target_speed_kmph スライダが加わっています。これによって「相手が 20 km/h のとき」「30 km/h のとき」と動かして相対速度の変化を直感的に確認できます。

データの取り扱いはCCRsと同じ方針です。数値は事実データとして再構成し出典を明記。原文の文章・図表は転載していません。


kinematics.ts の移動ターゲット一般化

src/sim/core/kinematics.ts に相対速度モデルを組み込み、停止・移動ターゲット両対応に拡張しました。

物理モデル

同方向走行を前提として、接近速度(相対速度)を次式で定義します。

v_rel = max(v_s − v_t, 0)
  • v_s:自車速 [m/s]
  • v_t:ターゲット速 [m/s]
  • v_t ≥ v_s なら自車が追いつかず衝突しない(v_rel = 0)

車間の時間発展は gap(t) = gap0 − v_rel · t(0で接触)。TTC = gap / v_rel(v_rel = 0 なら ∞)。

実装は内部ヘルパ relativeSpeedMps(非公開)が担います。

const relativeSpeedMps = (vsMps: number, vtMps: number): number =>
  Math.max(vsMps - vtMps, 0);

後方互換の取り方

既存の呼び出し元(CCRsデモ)を無修正で動かすため、新引数 targetSpeedKmph はすべてオプショナル(省略時 0)にしました。targetSpeedKmph = 0 なら v_rel = v_s となり、旧来の停止対象モードと完全一致します。

lastChanceGapM のシグネチャ変更例:

export const lastChanceGapM = (
  speedKmph: number,
  brakeG: number,
  targetSpeedKmph = 0,   // ← 追加。省略時0で停止対象(後方互換)。
): number => {
  const vRel = relativeSpeedMps(kmphToMps(speedKmph), kmphToMps(targetSpeedKmph));
  const a = brakeDecelMps2(brakeG);
  return a > 0 ? (vRel * vRel) / (2 * a) : Infinity;
};

collisionTimeSrearEndStateAttimeToLastChanceS も同様のパターンで拡張しています。

rearEndStateAt では targetRearX(ターゲット後端の絶対位置)が追加されました。停止対象では gap0M 固定、移動対象では gap0M + v_t · t と時刻に応じて増加します。この値をCanvasの描画座標として直接使うことで、俯瞰デモの位置計算が一本化されました。

// ターゲット後端の絶対位置(一定速 v_t で前進)
const targetRearXraw = inp.gap0M + vT * t;

シナリオ・レジストリの新設

src/sim/scenarios.ts を新設しました。目的は「シミュレーターとUIが参照するシナリオの一覧を一箇所で管理し、1行追加で新シナリオをデモに載せられるようにする」ことです。

拡張構造の核心

// ── シナリオ登録(ここに import を追加していく) ────────────────────────
import ccrs from '../../data/scenarios/euro-ncap/ccrs.json';
import ccrm from '../../data/scenarios/euro-ncap/ccrm.json';
// ────────────────────────────────────────────────────────────────────────

export const scenarios: ScenarioSummary[] = [
  ccrs as ScenarioSummary,
  ccrm as ScenarioSummary,
];

新シナリオを追加するときは、この import ブロックに1行足して配列に加えるだけです。OverheadDemoはこの配列を参照しているので、UI側の変更は不要です。

getScenarioByIdlistScenarios の2つのヘルパ関数も提供しています。前者はIDで1件取得、後者はナビゲーション用の概要リスト(id / title / short_code / category)を返します。


OverheadDemo のシナリオ選択UI化

src/components/OverheadDemo.astro を「単一シナリオ固定」から「シナリオ選択可能な汎用デモ」に再構築しました。

データ受け渡し方法

Astroのサーバー/クライアント境界をまたいでシナリオ一覧を渡すため、<script type="application/json"> タグに全データを埋め込み、クライアント側で JSON.parse して取り出しています。

<!-- ビルド時:scenarios 配列をJSONとして埋め込む -->
<script type="application/json" id="scenarios-data" set:html={JSON.stringify(scenarios)} />
<script type="application/json" id="default-scenario-id" set:html={JSON.stringify(defaultScenario.id)} />
// 実行時:クライアントJSで取り出す
const scenarios: Scenario[] = JSON.parse(
  document.getElementById('scenarios-data')!.textContent!,
);

スライダをクライアント側で再生成する設計

シナリオごとにパラメータの種類・数・範囲が異なります(CCRsには target_speed_kmph がないが、CCRmにはある)。そのためスライダはビルド時に固定せず、シナリオ切替のたびにクライアント側の buildControls() 関数でゼロから生成しなおす設計にしました。

function buildControls() {
  slidersContainer.innerHTML = '';           // 既存スライダを全消去
  for (const p of current.parameters ?? []) {
    // パラメータ定義からスライダ要素を動的生成
    const label = document.createElement('label');
    label.innerHTML = `
      <input type="range" id="param-${p.key}"
        min="${p.min}" max="${p.max}" step="${p.step}" value="${p.default}" />
      <output id="out-${p.key}">${p.default} ${p.unit}</output>`;
    slidersContainer.appendChild(label);
  }
  // 出典・回避原理の注記も再生成
}

シナリオ切替時(selectchange イベント)に buildControls() を呼び、続けて syncTimeSlider()draw() を実行してUIを整合させます。

HUDの「追突リスクなし」判定

OverheadDemo のHUD更新ロジックに「相対速度が0以下のケース」の判定を追加しました(decision_log.md #006 の品質判断)。

} else if (!Number.isFinite(st.ttc)) {
  // 相対速度 0 以下(相手が同速以上)=そもそも追突しない
  v.textContent = '追突リスクなし(接近せず)';
  v.classList.add('ok');
}

CCRmデモでターゲット速度を自車速度以上に設定すると、この判定が表示されます。


検証の記録

以下の3ケースを rearEndStateAt の計算値で手動確認しました。

ケース1:停止対象(CCRs)の回帰確認

自車速: 40 km/h = 11.111 m/s
ターゲット速: 0(省略)
初期車間: 40 m
ブレーキ能力: 0.9 g

v_rel = 11.111 m/s
TTC = 40 / 11.111 = 3.60 s
最終制動点の必要車間 = 11.111² / (2 × 0.9 × 9.81) = 7.00 m
→ 40 m > 7.00 m なので t=0 時点では回避可能

targetSpeedKmph を省略した場合と targetSpeedKmph = 0 を明示した場合で同じ結果になることを確認済みです(後方互換)。

ケース2:移動対象(CCRm)の相対速度計算

自車速: 50 km/h = 13.889 m/s
ターゲット速: 20 km/h = 5.556 m/s
初期車間: 40 m
ブレーキ能力: 0.9 g

v_rel = 13.889 − 5.556 = 8.333 m/s
TTC = 40 / 8.333 = 4.80 s
最終制動点の必要車間 = 8.333² / (2 × 0.9 × 9.81) = 3.94 m
→ 40 m > 3.94 m なので t=0 時点では回避可能

CCRs(同自車速・同初期車間)と比べて相対速度が下がる分、必要制動距離も短くなります(7.00 m → 3.94 m)。これはデモのスライダで「ターゲット速度を上げると最終制動点の橙破線が自車側に近づく」として可視化されます。

ケース3:追突リスクなし(端ケース)

自車速: 30 km/h
ターゲット速: 30 km/h 以上(例: 40 km/h)

v_rel = max(30 − 40, 0) × (1/3.6) = 0
TTC = Infinity(∞)
→ HUD に「追突リスクなし(接近せず)」を表示

max(v_s − v_t, 0) のクランプが意図どおりに効いていることを確認しました。


つまずきと工夫

スクリーンショット取得が安定しなかった

ブラウザUIの機能確認にスクリーンショットを活用しようとしましたが、開発環境での自動取得が安定しませんでした。今回はブラウザを開かず、rearEndStateAt の返り値を具体的な数値で手計算してロジックを直接検証する方法(上記ケース1〜3)で代替しました。

デモの視覚的な表示(車両描画・橙破線の位置・HUD値)については、ビルド後のローカルプレビュー(npm run preview)でブラウザを手動操作して目視確認しています。

スライダ再生成の設計判断

当初、スライダをHTMLに静的に展開してシナリオ切替時に hidden/display で切り替える案を検討しました。しかしシナリオが増えるたびにHTMLが肥大化し、追加のたびにコンポーネントを修正する必要があります。パラメータ定義(JSONスキーマの parameters[])から動的生成する設計にすることで、「scenarios.ts に1行追加すれば新シナリオがデモに載る」という目標が達成できます。動的生成のデメリット(DOM操作コスト)は切替時1回限りなので問題ありません。


運用面の記録

今回のバッチは3配下(S-content / S-sim / S-scenario)を並行稼働させました。各配下は自分の書込境界(S-contentは content/blog/、S-simは src/sim/src/components/、S-scenarioは data/ 等)のみを編集し、git操作・コミットはすべて統括が一括で実施しています(commit d75577c)。

この運用の目的は2つです。

  1. 競合回避:配下が独立して git を触ると、同一ファイル(OverheadDemo.astro等)の変更が競合する可能性があります。統括が差分を確認してから統合することで競合を防ぎます。
  2. 品質確認の集約:著作権・スキーマ準拠・設計整合性の最終確認を統括に集約します。今回は「追突リスクなし」判定の追加も統括の品質判断(L2相当)として記録されています(governance/decision_log.md #006)。

現在のリポジトリ構成(第1バッチ完了時点)

crash-prevention-site/
├── src/
│   ├── components/
│   │   └── OverheadDemo.astro   # シナリオ選択UI・移動ターゲット描画に再構築
│   └── sim/
│       ├── core/
│       │   └── kinematics.ts    # 移動ターゲット対応に拡張(v_rel モデル)
│       └── scenarios.ts         # シナリオ・レジストリ(新設)
├── data/
│   └── scenarios/
│       └── euro-ncap/
│           ├── ccrs.json        # CCRs(停止対象)
│           └── ccrm.json        # CCRm(移動対象)← 今回追加
└── content/
    └── blog/
        ├── 2026-06-07-devlog-01.md
        └── 2026-06-07-devlog-02.md  ← このファイル

次にやること

第1バッチ完了を受けて、以下の方向が見えてきました。

テーマ方向
シナリオDB拡充CCRs/CCRm に続く Euro NCAP / JNCAP シナリオの構造化
シミュレーター Phase2センサ検知可視化(検知距離・反応遅延の追加)
公開判断Cloudflare Pages へのデプロイ・GitHub 公開リポジトリ化(別途ユーザー承認)

シナリオが増えるたびに src/sim/scenarios.ts への import 1行追加でデモが更新される基盤が整いました。次のシナリオ追加はこの構造の初めての実証になります。


出典:data/scenarios/euro-ncap/ccrm.json(Euro NCAP AEB C2C Test Protocol、確認日 2026-06-07)。CCRm の試験速度帯・ターゲット速度・オーバーラップ率等の数値は事実データとして再構成したものです。原文の文章・図表は転載していません。

← 開発ブログの一覧へ