diff --git a/web/src/components/SplitsTable.tsx b/web/src/components/SplitsTable.tsx index 36309de..22c66fc 100644 --- a/web/src/components/SplitsTable.tsx +++ b/web/src/components/SplitsTable.tsx @@ -2,11 +2,7 @@ import type { SolveMove } from '../api' import { useHoldFrame } from '../settings/hooks' import { STAGE_LABELS, type Split } from '../solve/cfop' import { isRegrip, notationTokens, type HoldChange, type Holds, type NotationToken } from '../solve/holds' -import { formatTime } from '../solve/stats' - -function tps(moves: number, ms: number): string { - return ms > 0 ? (moves / (ms / 1000)).toFixed(2) : '—' -} +import { formatTime, formatTps } from '../solve/stats' /** 持ち替えの所要時間(秒)と、ワイドムーブかもしれないことの説明。 */ function rotationTitle(c: HoldChange): string { @@ -77,7 +73,7 @@ export function SplitsTable({ {formatTime(ms, 3)} {totalMs > 0 ? `${Math.round((ms / totalMs) * 100)}%` : '—'} {s.move_count} - {tps(s.move_count, ms)} + {formatTps(s.move_count, ms)} {holds && {rots.length ? `${rots.length}(${(rotMs / 1000).toFixed(2)})` : '—'}} {tokens.length ? : (skip)} diff --git a/web/src/components/TimerDisplay.tsx b/web/src/components/TimerDisplay.tsx index d49f157..3c85338 100644 --- a/web/src/components/TimerDisplay.tsx +++ b/web/src/components/TimerDisplay.tsx @@ -1,9 +1,9 @@ import { useEffect, useState } from 'react' import { INSPECTION_DNF_MS, INSPECTION_MS, type SessionState } from '../solve/session' -import { effectiveTime, formatTime } from '../solve/stats' +import { effectiveTime, formatTime, formatTps } from '../solve/stats' /** - * 大きなタイマー表示。計測中は経過時間、それ以外は直前の記録。 + * 大きなタイマー表示。計測中は経過時間、それ以外は直前の記録と、その手数・TPS。 * lastMoveCount は直前の記録の手数(回転ログを持たない中継の画面で渡す。省くと回転ログの長さ)。 */ export function TimerDisplay({ state, lastMoveCount }: { state: SessionState; lastMoveCount?: number }) { @@ -25,6 +25,8 @@ export function TimerDisplay({ state, lastMoveCount }: { state: SessionState; la let time: string let hint: string let mod = '' + // 手数(直前の記録なら TPS も)。インスペクション中は空にする(行は残して高さを変えない) + let stats = '' if (inspecting) { // インスペクション:残り秒数。15秒を過ぎたら +2、17秒を過ぎたら DNF const elapsed = Math.max(0, now - readyAt) @@ -41,21 +43,21 @@ export function TimerDisplay({ state, lastMoveCount }: { state: SessionState; la hint = 'インスペクション中:最初の回転でスタート' } else if (phase === 'running' && startedAt != null) { time = formatTime(Math.max(0, now - startedAt), 2) - hint = `${moveCount}手${state.inspectionPenalty ? `(インスペクション超過:${state.inspectionPenalty})` : ''}` + stats = `${moveCount}手` + hint = state.inspectionPenalty ? `インスペクション超過:${state.inspectionPenalty}` : '' } else { time = last ? formatTime(effectiveTime(last.total_ms, last.penalty), 3) : '0.000' - hint = - phase === 'ready' - ? '準備OK:最初の回転でスタート' - : phase === 'scrambling' - ? 'スクランブル中' - : last - ? `${lastMoveCount ?? last.moves?.length ?? 0}手` - : '' + hint = phase === 'ready' ? '準備OK:最初の回転でスタート' : phase === 'scrambling' ? 'スクランブル中' : '' + if (last) { + const moves = lastMoveCount ?? last.moves?.length ?? 0 + // TPS は +2 を含めない実測の時間で割る + stats = `${moves}手 ・ ${formatTps(moves, last.total_ms)} TPS` + } } return (
{time}
+
{stats}
{hint}
) diff --git a/web/src/index.css b/web/src/index.css index 407e1e7..3fc9ddd 100644 --- a/web/src/index.css +++ b/web/src/index.css @@ -705,6 +705,15 @@ code { color: var(--mute); } +.timer-stats { + min-height: 1.6em; + margin-top: 0.3em; + font-family: var(--font-num); + font-size: 1.25em; + font-weight: 600; + font-variant-numeric: tabular-nums; +} + .timer-hint { min-height: 1.6em; color: var(--mute); diff --git a/web/src/pages/SolveDetailPage.tsx b/web/src/pages/SolveDetailPage.tsx index 5982200..67f0d6a 100644 --- a/web/src/pages/SolveDetailPage.tsx +++ b/web/src/pages/SolveDetailPage.tsx @@ -21,7 +21,7 @@ import { ReplayPlayer } from '../replay/ReplayPlayer' import { isRegrip, notationTokens } from '../solve/holds' import { useHoldFrame, useSettings } from '../settings/hooks' import { computeSplits, COLOR_LABELS } from '../solve/cfop' -import { effectiveTime, formatTime, type Penalty } from '../solve/stats' +import { effectiveTime, formatTime, formatTps, type Penalty } from '../solve/stats' import { formatDate, isFullSolve, MODE_LABELS, multiTitle } from '../format' /** ソルブ詳細:段階タイム、スクランブル、解法、ペナルティ・メモ編集。 */ @@ -72,7 +72,6 @@ function SolveDetail({ solve, onChange }: { solve: Solve; onChange: (s: Solve) = } const moves = solve.moves ?? [] - const tps = solve.total_ms > 0 ? (moves.length / (solve.total_ms / 1000)).toFixed(2) : '—' return (
@@ -87,7 +86,7 @@ function SolveDetail({ solve, onChange }: { solve: Solve; onChange: (s: Solve) =
#{solve.id} ・ {formatDate(solve.solved_at)} ・ {MODE_LABELS[solve.mode]} {solve.case_id && ` ${solve.case_id}`} - {solve.step && ` ${solve.step}`} ・ {moves.length}手 ・ {tps} TPS + {solve.step && ` ${solve.step}`} ・ {moves.length}手 ・ {formatTps(moves.length, solve.total_ms)} TPS