Skip to content

feat(crane_web_debugger): Crane Viewer フルリデザイン(ES module 分割・ドックレイアウト・HUD・リプレイ)#1344

Merged
HansRobo merged 7 commits intodevelopfrom
feat/crane-viewer-redesign
Apr 22, 2026
Merged

feat(crane_web_debugger): Crane Viewer フルリデザイン(ES module 分割・ドックレイアウト・HUD・リプレイ)#1344
HansRobo merged 7 commits intodevelopfrom
feat/crane-viewer-redesign

Conversation

@HansRobo
Copy link
Copy Markdown
Member

概要

crane_web_debugger の viewer を全面リデザイン。モノリシックな viewer.js を ES module に分割し、ドックレイアウト・ロボット HUD・Sparkline・RingBuffer リプレイ・フォント配信修正まで一括実装。

背景・根本原因

旧 viewer.js は単一ファイルに全機能が詰まっており保守性が低かった。また UI がフラットで情報密度が低く、試合中の状況把握に不要な操作が多かった。

変更内容

Phase 1-2: ES module 分割・M3 トークン・動的 viewBox

  • viewer/renderer/ に CanvasRenderer・FieldLayer・RobotHud・SvgPrimitiveParser・SvgPathUtils・ThemeTokens・constants を分離
  • M3 デザイントークン(CSS custom properties)を Canvas 描画に適用
  • world_model.field_info から viewBox を動的更新(Div-A/B 自動対応)

Phase 3: ドックレイアウト・パネル分割・ライブログ

  • viewer/ui/DockLayout.js: 左・右・下ドックをスプリッターで可変リサイズ、localStorage 永続化
  • viewer/ui/LogPanel.js: level フィルタ・テキスト検索付きライブログパネル

Phase 4: ロボット HUD・インタラクション強化

  • viewer/renderer/RobotHud.js: 方向矢印・FSM/planner ラベル・ドリブラー LED・ホバーハロー・マルチセレクト
  • WASD/矢印キーパン・zoom-to-cursor・ツールチップ・Escape 二段確認 HALT

Phase 5: Sparkline・RobotDetail ドックパネル化

  • viewer/ui/Sparkline.js + MetricRing: 位置 X/Y・速度の 60fps リアルタイム Sparkline
  • ロボットカードクリックで詳細パネル展開(Pos/Vel/FSM/Planner/Telemetry リンク)

Phase 6: RingBuffer・TimeScrubber による 60s リプレイ

  • viewer/replay/RingBuffer.js: キーフレーム(1Hz)+ デルタ(60fps)の 2 層バッファで 60 秒巻き戻し
  • viewer/ui/TimeScrubber.js: 下ドックに再生・一時停止・速度変更・スクラブ・LIVE 復帰 UI

refactor: JS 共通化・簡潔化

  • indexBy / applyLayerUpdate を RingBuffer.js から export し、main.js と RingBuffer の重複実装を統一
  • findRobotAtPosition_nearestRobot のラッパーに簡略化
  • tooltip の change-detection で mousemove 時の innerHTML 再パースを回避
  • ダブルクローン解消・コメント削除・プレースホルダ統一

fix: フォント 404 解消

  • Docker volume mount が web/assets/fonts/ を上書きする問題を修正
  • Dockerfile でフォントを /app/fonts/(mount 外)にダウンロード
  • app.py に /fonts 静的配信ルートを追加(FONTS_DIR 環境変数対応)
  • material-symbols-outlined.css のファイル名不一致バグを修正

Phase 1+2 の実装。

## ES module 分割 (Phase 1)
viewer.js (1234行のモノリシックファイル) を viewer/ 配下の ES module に分割:
- renderer/constants.js: 定数群
- renderer/SvgPathUtils.js: parseSvgPath / svgArcToCanvas
- renderer/SvgPrimitiveParser.js: SVGプリミティブパーサー (LRU 40000 に拡張)
- renderer/CanvasRenderer.js: Canvas2D レンダラー
- renderer/ThemeTokens.js: M3 CSS custom property 読み取り
- renderer/FieldLayer.js: viewBox 管理 + turf/grid 描画
- ws/GameControlClient.js: ssl-game-controller WebSocket クライアント
- main.js: CraneViewer エントリポイント

viewer.html の <script src="viewer.js"> を <script type="module" src="viewer/main.js"> に変更。
window.craneViewer エクスポートを維持し、既存インライン onclick との後方互換性を確保。

## M3 デザイントークン徹底適用 (Phase 2)
- m3e-theme.css に --md-sys-color-field-turf / field-grid トークンを追加
- Canvas 描画の turf (#1a5c1a) / grid (#2d8a2d) / オーバーレイ色をすべて
  ThemeTokens 経由の M3 トークンに置き換え
- prefers-color-scheme 変化・html[class] 変化で自動再取得

## field_info 動的 viewBox (Phase 2)
- world_model.field_info (length/width) を受信して FieldLayer.viewBox を動的更新
- div-A (12×9m) / div-B (9×6m) 等のフィールドサイズに自動対応
- フィールドサイズ変化時のみ pan/zoom リセット
- viewBox 変化時に SvgPrimitiveParser のキャッシュを無効化(% テキスト正確変換)

## その他改善
- zoom-to-cursor 対応 (ホイールズームがカーソル位置中心)
- ロボットカードのプランナー名を6文字で切り詰めていたバグを修正 (full 表示)
- Escape → HALT を確認ダイアログ付きに変更 (2秒以内に再押下で確定)
## レイアウト刷新
- viewer.html を CSS Grid ドックレイアウトに全面書き換え
  - 左ドック: Game Info / Robots / Game Control / Sim Control
  - 右ドック: Layer Control / Log Panel
  - 下ドック: Phase 6 (TimeScrubber) 用プレースホルダー
- DockLayout.js: スプリッタドラッグリサイズ + localStorage (cv.dockLayout.v1) 永続化
  - CSS var --dock-left-w / --dock-right-w / --dock-bottom-h で制御
  - 折り畳みボタン (data-dock-toggle) でアイコンストライプに収納

## インライン onclick → delegated listener 移行
- GC ボタン全ての onclick 属性を data-action 属性に統一
  (gc-command / gc-goals / gc-card-yellow / gc-card-red / gc-next-stage /
   ball-place / sim-edit / sim-reset-ball / sim-set-endpoint)
- main.js に setupDelegatedListeners() を追加
- 生 hex 色を --md-sys-color-team-yellow / team-blue トークンに置き換え

## 生きたログパネル (LogPanel.js)
- 差分 append DOM (上限1000件)
- level チェックボックス5段 (info/warn/error/action/metric) + テキスト検索
- data-level + CSS セレクタ display:none でフィルタ (DOM 再描画ゼロ)
- 自動スクロール (手動スクロールで自動無効化)
- WebSocket 接続/切断/エラー/コマンド発行をリアルタイム表示

## m3e-theme.css
- --md-sys-color-team-yellow / on-team-yellow / team-blue / on-team-blue を追加
## RobotHud.js (新規)
- 方向矢印: SVG座標系でθに追従、先端V字矢頭、dribble_power > 0 時に色変更
- 選択ハロー: primary=24mm実線 alpha0.8 / secondary(Ctrl+Click)=8mm破線 / hover=6mm破線
- ドリブラーLED: ball_sensor 真→accent / 偽→muted の15mm円
- FSM/Planner ラベル: planning_factors[0].name + planner_name をロボット下方に表示

## インタラクション強化 (main.js)
- Ctrl+Click: _multiSelect Set でセカンダリ選択切り替え
- Escape: マルチセレクトがある場合はまず解除 (既存モード解除より後方)
- WASD / 矢印キー パン: rAF ループで滑らか、Shift 3倍速
  (テキスト入力中は無効化)
- robot_feedback ハンドラ追加 (ball_sensor 等)

## ホバーツールチップ (main.js)
- mousemove で ROBOT_HIT_RADIUS_M*2 以内の最寄りロボットを検出
- HTML overlay div (#robot-hover-tooltip) をカーソル位置に追従
  内容: ID / Pos(x,y,θ) / FSM / Planner
- mouseleave でツールチップを非表示 + hover ハロー消去
- fieldToClientCoords() ヘルパーを追加
## Sparkline.js (新規)
- 純 Canvas2D、最大 5 系列、DPR 対応 (width/height オプション)
- setSeries([{ color, data: Float32Array }]) + render(tokens) API
- ゼロ軸 (正負跨ぎ時のみ表示)、末端マーカ円
- MetricRing クラス: 10Hz × 60s = 600 サンプルのリングバッファ

## RobotDetail をドックパネル化 (viewer.html + main.js)
- モーダルダイアログ → 右ドック内インラインパネル (#robot-detail-inline) に変更
- ロボットカードクリックで右ドック RobotDetail セクションに表示
- 表示内容: Mode / Planner / FSM / Pos / θ / Vel / ω / Target / Telemetry リンク
- rAF ループで Pos X / Pos Y / |Vel| の sparkline をリアルタイム更新
- 閉じるボタン (#btn-robot-detail-close) で非表示 + sparkline ループ停止

## MetricRing 更新
- handleWorldModel で robots_ours の x/y/vx/vy から MetricRing に push
- 各ロボット独立に 180 サンプル (10Hz × 18s) を保持
…ase 6)

## RingBuffer.js (新規: replay/)
- Two-tier: keyframe 1Hz (svg_data 受信時) + delta (svg_update/world_model/control_targets)
- 60 秒ウィンドウ (設定可能)、古いエントリを自動 prune
- seek(tsMs): 直前キーフレーム復元 + デルタ適用で任意フレームを再現
- layerStore は Map 深コピーで独立保持

## TimeScrubber.js (新規: ui/)
- 下ドック 44px。レイアウト: [⏮][⏯][⏭] [速度選択] [====●====] [-2.4s] [LIVE]
- `<input type="range">` スライダー + ドラッグで seek
- キー: [ / ] = ±500ms、Space = 再生/停止、End = LIVE 復帰
- 再生エンジン: rAF で indexMs += dt*speed、最新フレームに到達で LIVE 自動復帰
- LIVE モードでは viewer._replayMode=false でリアルタイム表示を維持

## main.js / viewer.html
- handleSvgData: RingBuffer.addKeyframe で定期スナップショット
- handleSvgUpdate / handleWorldModel / handleControlTargets: RingBuffer.addDelta で tee
- _replayMode=true の間は Store 更新をスキップ (RingBuffer にのみ書き込む)
- DockLayout のデフォルト bottom を 44px に変更 (TimeScrubber 表示)
- viewer.html に #time-scrubber-container を配置
RingBuffer.js に indexBy・applyLayerUpdate を共通ヘルパとして追加し、
main.js・RingBuffer の両所で重複していた実装を統一した。

変更内容:
- indexBy(arr, keyName): 配列→キー付きオブジェクトの変換ヘルパを追加
  robots_ours/theirs・control_targets の変換箇所で利用(5 箇所)
- applyLayerUpdate(layerStore, upd): レイヤー状態変更ロジックを抽出
  replace/append/clear の分岐が main.js と RingBuffer で乖離するリスクを解消
- findRobotAtPosition: _nearestRobot の薄いラッパーに簡略化(重複実装を削除)
- _updateTooltip: _hoveredTooltipId で変化検出し、同一ロボット上での
  mousemove による innerHTML 再パースを回避
- coalesceLayerUpdates: replace/append-after-clear の [...]clone を除去し
  downstream の applyLayerUpdate に一本化(ダブルクローン解消)
- updateGcPanel: ts['YELLOW'] → ts.YELLOW、プレースホルダを '--' に統一
- WHAT コメント 5 箇所削除
ホストの web/ ディレクトリを volume mount すると Docker ビルド時に
ダウンロードしたフォントが上書きされ 404 になる問題を修正した。

変更内容:
- Dockerfile: --output-dir /app/fonts を追加し、フォントを
  volume mount 対象外の /app/fonts/ に配置するよう変更
- app.py: FONTS_DIR 環境変数(デフォルト /app/fonts)の
  ディレクトリを /fonts URL として StaticFiles で配信
- *.html (4 ファイル): フォント参照パスを相対 assets/fonts/ から
  絶対パス /fonts/ に変更
- download_fonts.py: キー名 "material-symbols" を
  "material-symbols-outlined" に修正(生成 CSS 名が HTML 参照名と
  一致しておらず常に 404 になっていたバグを同時修正)
@HansRobo HansRobo force-pushed the feat/crane-viewer-redesign branch from c9a7ff2 to 113fb57 Compare April 22, 2026 12:19
const newZoom = Math.min(Math.max(this.zoomLevel * factor, 0.1), 5.0);
const zoomRatio = newZoom / this.zoomLevel;
// パンオフセットを調整してカーソル下のフィールド座標を固定
const fl = this.fieldLayer;
@HansRobo HansRobo merged commit 3e966e4 into develop Apr 22, 2026
10 checks passed
@HansRobo HansRobo deleted the feat/crane-viewer-redesign branch April 22, 2026 12:24
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