优化地图交互与文档方案

This commit is contained in:
2026-03-26 12:20:27 +08:00
parent ce25530938
commit d695308a55
9 changed files with 2196 additions and 69 deletions

View File

@@ -55,12 +55,15 @@ const AUTO_ROTATE_EASE = 0.34
const AUTO_ROTATE_SNAP_DEG = 0.1
const AUTO_ROTATE_DEADZONE_DEG = 4
const AUTO_ROTATE_MAX_STEP_DEG = 0.75
const AUTO_ROTATE_HEADING_SMOOTHING = 0.32
const COMPASS_NEEDLE_SMOOTHING = 0.12
const AUTO_ROTATE_HEADING_SMOOTHING = 0.46
const COMPASS_NEEDLE_MIN_SMOOTHING = 0.24
const COMPASS_NEEDLE_MAX_SMOOTHING = 0.56
const SMART_HEADING_BLEND_START_SPEED_KMH = 1.2
const SMART_HEADING_MOVEMENT_SPEED_KMH = 3.0
const SMART_HEADING_MIN_DISTANCE_METERS = 8
const SMART_HEADING_MIN_DISTANCE_METERS = 12
const SMART_HEADING_MAX_ACCURACY_METERS = 25
const SMART_HEADING_MOVEMENT_MIN_SMOOTHING = 0.12
const SMART_HEADING_MOVEMENT_MAX_SMOOTHING = 0.24
const GPS_TRACK_MAX_POINTS = 200
const GPS_TRACK_MIN_STEP_METERS = 3
const MAP_TAP_MOVE_THRESHOLD_PX = 14
@@ -384,6 +387,35 @@ function interpolateAngleDeg(currentDeg: number, targetDeg: number, factor: numb
return normalizeRotationDeg(currentDeg + normalizeAngleDeltaDeg(targetDeg - currentDeg) * factor)
}
function getCompassNeedleSmoothingFactor(currentDeg: number, targetDeg: number): number {
const deltaDeg = Math.abs(normalizeAngleDeltaDeg(targetDeg - currentDeg))
if (deltaDeg <= 4) {
return COMPASS_NEEDLE_MIN_SMOOTHING
}
if (deltaDeg >= 36) {
return COMPASS_NEEDLE_MAX_SMOOTHING
}
const progress = (deltaDeg - 4) / (36 - 4)
return COMPASS_NEEDLE_MIN_SMOOTHING
+ (COMPASS_NEEDLE_MAX_SMOOTHING - COMPASS_NEEDLE_MIN_SMOOTHING) * progress
}
function getMovementHeadingSmoothingFactor(speedKmh: number | null): number {
if (speedKmh === null || !Number.isFinite(speedKmh) || speedKmh <= SMART_HEADING_BLEND_START_SPEED_KMH) {
return SMART_HEADING_MOVEMENT_MIN_SMOOTHING
}
if (speedKmh >= SMART_HEADING_MOVEMENT_SPEED_KMH) {
return SMART_HEADING_MOVEMENT_MAX_SMOOTHING
}
const progress = (speedKmh - SMART_HEADING_BLEND_START_SPEED_KMH)
/ (SMART_HEADING_MOVEMENT_SPEED_KMH - SMART_HEADING_BLEND_START_SPEED_KMH)
return SMART_HEADING_MOVEMENT_MIN_SMOOTHING
+ (SMART_HEADING_MOVEMENT_MAX_SMOOTHING - SMART_HEADING_MOVEMENT_MIN_SMOOTHING) * progress
}
function formatGameSessionStatusText(status: 'idle' | 'running' | 'finished' | 'failed'): string {
if (status === 'running') {
return '进行中'
@@ -705,16 +737,19 @@ export class MapEngine {
autoRotateTimer: number
pendingViewPatch: Partial<MapEngineViewState>
mounted: boolean
diagnosticUiEnabled: boolean
northReferenceMode: NorthReferenceMode
sensorHeadingDeg: number | null
smoothedSensorHeadingDeg: number | null
compassDisplayHeadingDeg: number | null
smoothedMovementHeadingDeg: number | null
autoRotateHeadingDeg: number | null
courseHeadingDeg: number | null
targetAutoRotationDeg: number | null
autoRotateSourceMode: AutoRotateSourceMode
autoRotateCalibrationOffsetDeg: number | null
autoRotateCalibrationPending: boolean
lastStatsUiSyncAt: number
minZoom: number
maxZoom: number
defaultZoom: number
@@ -776,14 +811,18 @@ export class MapEngine {
y,
z,
})
this.setState(this.getTelemetrySensorViewPatch(), true)
if (this.diagnosticUiEnabled) {
this.setState(this.getTelemetrySensorViewPatch(), true)
}
},
onError: (message) => {
this.accelerometerErrorText = `不可用: ${message}`
this.setState({
...this.getTelemetrySensorViewPatch(),
statusText: `加速度计启动失败 (${this.buildVersion})`,
}, true)
if (this.diagnosticUiEnabled) {
this.setState({
...this.getTelemetrySensorViewPatch(),
statusText: `加速度计启动失败 (${this.buildVersion})`,
}, true)
}
},
})
this.compassController = new CompassHeadingController({
@@ -803,10 +842,14 @@ export class MapEngine {
y,
z,
})
this.setState(this.getTelemetrySensorViewPatch(), true)
if (this.diagnosticUiEnabled) {
this.setState(this.getTelemetrySensorViewPatch(), true)
}
},
onError: () => {
this.setState(this.getTelemetrySensorViewPatch(), true)
if (this.diagnosticUiEnabled) {
this.setState(this.getTelemetrySensorViewPatch(), true)
}
},
})
this.deviceMotionController = new DeviceMotionController({
@@ -818,17 +861,21 @@ export class MapEngine {
beta,
gamma,
})
this.setState({
...this.getTelemetrySensorViewPatch(),
autoRotateSourceText: this.getAutoRotateSourceText(),
}, true)
if (this.diagnosticUiEnabled) {
this.setState({
...this.getTelemetrySensorViewPatch(),
autoRotateSourceText: this.getAutoRotateSourceText(),
}, true)
}
if (this.state.orientationMode === 'heading-up' && this.refreshAutoRotateTarget()) {
this.scheduleAutoRotate()
}
},
onError: () => {
this.setState(this.getTelemetrySensorViewPatch(), true)
if (this.diagnosticUiEnabled) {
this.setState(this.getTelemetrySensorViewPatch(), true)
}
},
})
this.locationController = new LocationController({
@@ -840,7 +887,7 @@ export class MapEngine {
gpsTracking: this.locationController.listening,
gpsTrackingText: message,
...this.getLocationControllerViewPatch(),
}, true)
})
},
onError: (message) => {
this.setState({
@@ -848,10 +895,12 @@ export class MapEngine {
gpsTrackingText: message,
...this.getLocationControllerViewPatch(),
statusText: `${message} (${this.buildVersion})`,
}, true)
})
},
onDebugStateChange: () => {
this.setState(this.getLocationControllerViewPatch(), true)
if (this.diagnosticUiEnabled) {
this.setState(this.getLocationControllerViewPatch(), true)
}
},
})
this.heartRateController = new HeartRateInputController({
@@ -872,7 +921,7 @@ export class MapEngine {
heartRateDeviceText: deviceName,
heartRateScanText: this.getHeartRateScanText(),
...this.getHeartRateControllerViewPatch(),
}, true)
})
},
onError: (message) => {
this.clearHeartRateSignal()
@@ -886,7 +935,7 @@ export class MapEngine {
heartRateScanText: this.getHeartRateScanText(),
...this.getHeartRateControllerViewPatch(),
statusText: `${message} (${this.buildVersion})`,
}, true)
})
},
onConnectionChange: (connected, deviceName) => {
if (!connected) {
@@ -906,17 +955,21 @@ export class MapEngine {
heartRateScanText: this.getHeartRateScanText(),
heartRateDiscoveredDevices: this.formatHeartRateDevices(this.heartRateController.discoveredDevices),
...this.getHeartRateControllerViewPatch(),
}, true)
})
},
onDeviceListChange: (devices) => {
this.setState({
heartRateDiscoveredDevices: this.formatHeartRateDevices(devices),
heartRateScanText: this.getHeartRateScanText(),
...this.getHeartRateControllerViewPatch(),
}, true)
if (this.diagnosticUiEnabled) {
this.setState({
heartRateDiscoveredDevices: this.formatHeartRateDevices(devices),
heartRateScanText: this.getHeartRateScanText(),
...this.getHeartRateControllerViewPatch(),
}, true)
}
},
onDebugStateChange: () => {
this.setState(this.getHeartRateControllerViewPatch(), true)
if (this.diagnosticUiEnabled) {
this.setState(this.getHeartRateControllerViewPatch(), true)
}
},
})
this.feedbackDirector = new FeedbackDirector({
@@ -1119,22 +1172,53 @@ export class MapEngine {
this.autoRotateTimer = 0
this.pendingViewPatch = {}
this.mounted = false
this.diagnosticUiEnabled = false
this.northReferenceMode = DEFAULT_NORTH_REFERENCE_MODE
this.sensorHeadingDeg = null
this.smoothedSensorHeadingDeg = null
this.compassDisplayHeadingDeg = null
this.smoothedMovementHeadingDeg = null
this.autoRotateHeadingDeg = null
this.courseHeadingDeg = null
this.targetAutoRotationDeg = null
this.autoRotateSourceMode = 'smart'
this.autoRotateCalibrationOffsetDeg = getMapNorthOffsetDeg(DEFAULT_NORTH_REFERENCE_MODE)
this.autoRotateCalibrationPending = false
this.lastStatsUiSyncAt = 0
}
getInitialData(): MapEngineViewState {
return { ...this.state }
}
setDiagnosticUiEnabled(enabled: boolean): void {
if (this.diagnosticUiEnabled === enabled) {
return
}
this.diagnosticUiEnabled = enabled
if (!enabled) {
return
}
this.setState({
...this.getTelemetrySensorViewPatch(),
...this.getLocationControllerViewPatch(),
...this.getHeartRateControllerViewPatch(),
heartRateDiscoveredDevices: this.formatHeartRateDevices(this.heartRateController.discoveredDevices),
autoRotateSourceText: this.getAutoRotateSourceText(),
visibleTileCount: this.state.visibleTileCount,
readyTileCount: this.state.readyTileCount,
memoryTileCount: this.state.memoryTileCount,
diskTileCount: this.state.diskTileCount,
memoryHitCount: this.state.memoryHitCount,
diskHitCount: this.state.diskHitCount,
networkFetchCount: this.state.networkFetchCount,
cacheHitRateText: this.state.cacheHitRateText,
}, true)
}
getGameInfoSnapshot(): MapEngineGameInfoSnapshot {
const definition = this.gameRuntime.definition
const sessionState = this.gameRuntime.state
@@ -1253,12 +1337,14 @@ export class MapEngine {
this.currentGpsTrack = []
this.currentGpsAccuracyMeters = null
this.currentGpsInsideMap = false
this.smoothedMovementHeadingDeg = null
this.courseOverlayVisible = false
this.setCourseHeading(null)
}
clearStartSessionResidue(): void {
this.currentGpsTrack = []
this.smoothedMovementHeadingDeg = null
this.courseOverlayVisible = false
this.setCourseHeading(null)
}
@@ -1534,7 +1620,7 @@ export class MapEngine {
panelAverageSpeedUnitText: telemetryPresentation.averageSpeedUnitText,
panelAccuracyValueText: telemetryPresentation.accuracyValueText,
panelAccuracyUnitText: telemetryPresentation.accuracyUnitText,
}, true)
})
}
updateSessionTimerLoop(): void {
@@ -1798,6 +1884,7 @@ export class MapEngine {
this.currentGpsPoint = nextPoint
this.currentGpsAccuracyMeters = accuracyMeters
this.updateMovementHeadingDeg()
const gpsWorldPoint = lonLatToWorldTile(nextPoint, this.state.zoom)
const gpsTileX = Math.floor(gpsWorldPoint.x)
@@ -2167,7 +2254,7 @@ export class MapEngine {
compassDeclinationText: formatCompassDeclinationText(this.northReferenceMode),
northReferenceButtonText: formatNorthReferenceButtonText(this.northReferenceMode),
northReferenceText: formatNorthReferenceText(this.northReferenceMode),
compassNeedleDeg: formatCompassNeedleDegForMode(this.northReferenceMode, this.smoothedSensorHeadingDeg),
compassNeedleDeg: formatCompassNeedleDegForMode(this.northReferenceMode, this.compassDisplayHeadingDeg),
...this.getGameViewPatch(gameStatusText),
}
@@ -2683,18 +2770,26 @@ export class MapEngine {
const compassHeadingDeg = getCompassReferenceHeadingDeg(this.northReferenceMode, this.smoothedSensorHeadingDeg)
this.compassDisplayHeadingDeg = this.compassDisplayHeadingDeg === null
? compassHeadingDeg
: interpolateAngleDeg(this.compassDisplayHeadingDeg, compassHeadingDeg, COMPASS_NEEDLE_SMOOTHING)
: interpolateAngleDeg(
this.compassDisplayHeadingDeg,
compassHeadingDeg,
getCompassNeedleSmoothingFactor(this.compassDisplayHeadingDeg, compassHeadingDeg),
)
this.autoRotateHeadingDeg = this.resolveAutoRotateInputHeadingDeg()
this.setState({
sensorHeadingText: formatHeadingText(compassHeadingDeg),
...this.getTelemetrySensorViewPatch(),
compassDeclinationText: formatCompassDeclinationText(this.northReferenceMode),
northReferenceButtonText: formatNorthReferenceButtonText(this.northReferenceMode),
autoRotateSourceText: this.getAutoRotateSourceText(),
compassNeedleDeg: formatCompassNeedleDegForMode(this.northReferenceMode, this.smoothedSensorHeadingDeg),
northReferenceText: formatNorthReferenceText(this.northReferenceMode),
compassNeedleDeg: formatCompassNeedleDegForMode(this.northReferenceMode, this.compassDisplayHeadingDeg),
...(this.diagnosticUiEnabled
? {
sensorHeadingText: formatHeadingText(compassHeadingDeg),
...this.getTelemetrySensorViewPatch(),
compassDeclinationText: formatCompassDeclinationText(this.northReferenceMode),
northReferenceButtonText: formatNorthReferenceButtonText(this.northReferenceMode),
autoRotateSourceText: this.getAutoRotateSourceText(),
northReferenceText: formatNorthReferenceText(this.northReferenceMode),
}
: {}),
})
if (!this.refreshAutoRotateTarget()) {
@@ -2740,7 +2835,7 @@ export class MapEngine {
...this.getTelemetrySensorViewPatch(),
compassDeclinationText: formatCompassDeclinationText(nextMode),
northReferenceButtonText: formatNorthReferenceButtonText(nextMode),
compassNeedleDeg: formatCompassNeedleDegForMode(nextMode, this.smoothedSensorHeadingDeg),
compassNeedleDeg: formatCompassNeedleDegForMode(nextMode, this.compassDisplayHeadingDeg),
autoRotateCalibrationText: formatAutoRotateCalibrationText(false, nextMapNorthOffsetDeg),
},
`${formatNorthReferenceStatusText(nextMode)} (${this.buildVersion})`,
@@ -2759,7 +2854,7 @@ export class MapEngine {
...this.getTelemetrySensorViewPatch(),
compassDeclinationText: formatCompassDeclinationText(nextMode),
northReferenceButtonText: formatNorthReferenceButtonText(nextMode),
compassNeedleDeg: formatCompassNeedleDegForMode(nextMode, this.smoothedSensorHeadingDeg),
compassNeedleDeg: formatCompassNeedleDegForMode(nextMode, this.compassDisplayHeadingDeg),
autoRotateCalibrationText: formatAutoRotateCalibrationText(false, nextMapNorthOffsetDeg),
statusText: `${formatNorthReferenceStatusText(nextMode)} (${this.buildVersion})`,
}, true)
@@ -2780,7 +2875,7 @@ export class MapEngine {
}
}
getMovementHeadingDeg(): number | null {
getRawMovementHeadingDeg(): number | null {
if (!this.currentGpsInsideMap) {
return null
}
@@ -2809,6 +2904,23 @@ export class MapEngine {
return null
}
updateMovementHeadingDeg(): void {
const rawMovementHeadingDeg = this.getRawMovementHeadingDeg()
if (rawMovementHeadingDeg === null) {
this.smoothedMovementHeadingDeg = null
return
}
const smoothingFactor = getMovementHeadingSmoothingFactor(this.telemetryRuntime.state.currentSpeedKmh)
this.smoothedMovementHeadingDeg = this.smoothedMovementHeadingDeg === null
? rawMovementHeadingDeg
: interpolateAngleDeg(this.smoothedMovementHeadingDeg, rawMovementHeadingDeg, smoothingFactor)
}
getMovementHeadingDeg(): number | null {
return this.smoothedMovementHeadingDeg
}
getPreferredSensorHeadingDeg(): number | null {
return this.smoothedSensorHeadingDeg === null
? null
@@ -2959,7 +3071,7 @@ export class MapEngine {
}
applyStats(stats: MapRendererStats): void {
this.setState({
const statsPatch = {
visibleTileCount: stats.visibleTileCount,
readyTileCount: stats.readyTileCount,
memoryTileCount: stats.memoryTileCount,
@@ -2968,7 +3080,27 @@ export class MapEngine {
diskHitCount: stats.diskHitCount,
networkFetchCount: stats.networkFetchCount,
cacheHitRateText: formatCacheHitRate(stats.memoryHitCount, stats.diskHitCount, stats.networkFetchCount),
})
}
if (!this.diagnosticUiEnabled) {
this.state = {
...this.state,
...statsPatch,
}
return
}
const now = Date.now()
if (now - this.lastStatsUiSyncAt < 500) {
this.state = {
...this.state,
...statsPatch,
}
return
}
this.lastStatsUiSyncAt = now
this.setState(statsPatch)
}
setState(patch: Partial<MapEngineViewState>, immediateUi = false): void {