(rebase) avoid dist and binary
This commit is contained in:
248
src/utils/CoordinateMapper.ts
Normal file
248
src/utils/CoordinateMapper.ts
Normal file
@@ -0,0 +1,248 @@
|
||||
/**
|
||||
* 坐标映射工具类 - 用于测试和验证坐标转换的准确性
|
||||
*/
|
||||
|
||||
interface DeviceInfo {
|
||||
density: number
|
||||
densityDpi: number
|
||||
hasNavigationBar: boolean
|
||||
aspectRatio: number
|
||||
realScreenSize: { width: number; height: number }
|
||||
appScreenSize: { width: number; height: number }
|
||||
navigationBarSize: { width: number; height: number }
|
||||
}
|
||||
|
||||
interface CoordinateTestResult {
|
||||
success: boolean
|
||||
accuracy: number // 准确度百分比
|
||||
avgError: number // 平均误差(像素)
|
||||
maxError: number // 最大误差(像素)
|
||||
testPoints: Array<{
|
||||
input: { x: number; y: number }
|
||||
expected: { x: number; y: number }
|
||||
actual: { x: number; y: number }
|
||||
error: number
|
||||
}>
|
||||
}
|
||||
|
||||
export class CoordinateMapper {
|
||||
/**
|
||||
* 测试坐标映射准确性
|
||||
*/
|
||||
static testCoordinateMapping(
|
||||
deviceWidth: number,
|
||||
deviceHeight: number,
|
||||
displayWidth: number,
|
||||
displayHeight: number,
|
||||
deviceInfo?: DeviceInfo
|
||||
): CoordinateTestResult {
|
||||
console.log('🧪 开始坐标映射准确性测试')
|
||||
|
||||
// 生成测试点
|
||||
const testPoints = [
|
||||
// 屏幕角落
|
||||
{ x: 0, y: 0 },
|
||||
{ x: deviceWidth - 1, y: 0 },
|
||||
{ x: 0, y: deviceHeight - 1 },
|
||||
{ x: deviceWidth - 1, y: deviceHeight - 1 },
|
||||
|
||||
// 屏幕中心
|
||||
{ x: deviceWidth / 2, y: deviceHeight / 2 },
|
||||
|
||||
// 常见按钮位置
|
||||
{ x: deviceWidth * 0.8, y: deviceHeight * 0.9 }, // 右下角按钮
|
||||
{ x: deviceWidth * 0.5, y: deviceHeight * 0.8 }, // 底部中央按钮
|
||||
{ x: deviceWidth * 0.2, y: deviceHeight * 0.1 }, // 左上角按钮
|
||||
|
||||
// 键盘区域
|
||||
{ x: deviceWidth * 0.25, y: deviceHeight * 0.6 },
|
||||
{ x: deviceWidth * 0.5, y: deviceHeight * 0.6 },
|
||||
{ x: deviceWidth * 0.75, y: deviceHeight * 0.6 },
|
||||
|
||||
// 导航栏区域(如果有)
|
||||
...(deviceInfo?.hasNavigationBar ? [
|
||||
{ x: deviceWidth * 0.2, y: deviceHeight + 50 },
|
||||
{ x: deviceWidth * 0.5, y: deviceHeight + 50 },
|
||||
{ x: deviceWidth * 0.8, y: deviceHeight + 50 }
|
||||
] : [])
|
||||
]
|
||||
|
||||
const results: CoordinateTestResult['testPoints'] = []
|
||||
let totalError = 0
|
||||
let maxError = 0
|
||||
|
||||
for (const point of testPoints) {
|
||||
// 模拟正向转换:设备坐标 → 显示坐标
|
||||
const displayCoords = this.deviceToDisplay(point.x, point.y, deviceWidth, deviceHeight, displayWidth, displayHeight)
|
||||
|
||||
// 模拟反向转换:显示坐标 → 设备坐标
|
||||
const backToDevice = this.displayToDevice(displayCoords.x, displayCoords.y, deviceWidth, deviceHeight, displayWidth, displayHeight, deviceInfo)
|
||||
|
||||
// 计算误差
|
||||
const error = Math.sqrt(Math.pow(point.x - backToDevice.x, 2) + Math.pow(point.y - backToDevice.y, 2))
|
||||
totalError += error
|
||||
maxError = Math.max(maxError, error)
|
||||
|
||||
results.push({
|
||||
input: point,
|
||||
expected: point,
|
||||
actual: backToDevice,
|
||||
error
|
||||
})
|
||||
}
|
||||
|
||||
const avgError = totalError / testPoints.length
|
||||
const accuracy = Math.max(0, 100 - (avgError / Math.min(deviceWidth, deviceHeight) * 100))
|
||||
|
||||
const result: CoordinateTestResult = {
|
||||
success: avgError < 5, // 平均误差小于5像素认为成功
|
||||
accuracy,
|
||||
avgError,
|
||||
maxError,
|
||||
testPoints: results
|
||||
}
|
||||
|
||||
console.log('🧪 坐标映射测试结果:', {
|
||||
success: result.success,
|
||||
accuracy: `${accuracy.toFixed(2)}%`,
|
||||
avgError: `${avgError.toFixed(2)}px`,
|
||||
maxError: `${maxError.toFixed(2)}px`,
|
||||
testPointsCount: testPoints.length
|
||||
})
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* 设备坐标转换为显示坐标
|
||||
*/
|
||||
private static deviceToDisplay(
|
||||
deviceX: number,
|
||||
deviceY: number,
|
||||
deviceWidth: number,
|
||||
deviceHeight: number,
|
||||
displayWidth: number,
|
||||
displayHeight: number
|
||||
): { x: number; y: number } {
|
||||
const scaleX = displayWidth / deviceWidth
|
||||
const scaleY = displayHeight / deviceHeight
|
||||
const scale = Math.min(scaleX, scaleY)
|
||||
|
||||
const actualDisplayWidth = deviceWidth * scale
|
||||
const actualDisplayHeight = deviceHeight * scale
|
||||
const offsetX = (displayWidth - actualDisplayWidth) / 2
|
||||
const offsetY = (displayHeight - actualDisplayHeight) / 2
|
||||
|
||||
return {
|
||||
x: deviceX * scale + offsetX,
|
||||
y: deviceY * scale + offsetY
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示坐标转换为设备坐标(增强版本)
|
||||
*/
|
||||
private static displayToDevice(
|
||||
displayX: number,
|
||||
displayY: number,
|
||||
deviceWidth: number,
|
||||
deviceHeight: number,
|
||||
displayWidth: number,
|
||||
displayHeight: number,
|
||||
deviceInfo?: DeviceInfo
|
||||
): { x: number; y: number } {
|
||||
const scaleX = displayWidth / deviceWidth
|
||||
const scaleY = displayHeight / deviceHeight
|
||||
const scale = Math.min(scaleX, scaleY)
|
||||
|
||||
const actualDisplayWidth = deviceWidth * scale
|
||||
const actualDisplayHeight = deviceHeight * scale
|
||||
const offsetX = (displayWidth - actualDisplayWidth) / 2
|
||||
const offsetY = (displayHeight - actualDisplayHeight) / 2
|
||||
|
||||
const adjustedX = displayX - offsetX
|
||||
const adjustedY = displayY - offsetY
|
||||
|
||||
let deviceX = adjustedX / scale
|
||||
let deviceY = adjustedY / scale
|
||||
|
||||
// 应用设备特性修正
|
||||
if (deviceInfo) {
|
||||
const density = deviceInfo.density
|
||||
if (density > 2) {
|
||||
deviceX = Math.round(deviceX * 10) / 10
|
||||
deviceY = Math.round(deviceY * 10) / 10
|
||||
} else {
|
||||
deviceX = Math.round(deviceX)
|
||||
deviceY = Math.round(deviceY)
|
||||
}
|
||||
|
||||
// 导航栏区域修正
|
||||
if (deviceInfo.hasNavigationBar && deviceInfo.navigationBarSize.height > 0) {
|
||||
const navBarHeight = deviceInfo.navigationBarSize.height
|
||||
const threshold = deviceHeight - navBarHeight
|
||||
if (deviceY > threshold) {
|
||||
deviceY = Math.min(deviceY, deviceHeight + Math.min(navBarHeight, 150))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
x: Math.max(0, Math.min(deviceWidth, deviceX)),
|
||||
y: Math.max(0, Math.min(deviceHeight, deviceY))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成坐标映射诊断报告
|
||||
*/
|
||||
static generateDiagnosticReport(
|
||||
deviceWidth: number,
|
||||
deviceHeight: number,
|
||||
displayWidth: number,
|
||||
displayHeight: number,
|
||||
deviceInfo?: DeviceInfo
|
||||
): string {
|
||||
const testResult = this.testCoordinateMapping(deviceWidth, deviceHeight, displayWidth, displayHeight, deviceInfo)
|
||||
|
||||
let report = `📊 坐标映射诊断报告\n`
|
||||
report += `==========================================\n`
|
||||
report += `设备分辨率: ${deviceWidth}x${deviceHeight}\n`
|
||||
report += `显示分辨率: ${displayWidth}x${displayHeight}\n`
|
||||
report += `设备信息: ${deviceInfo ? '已提供' : '未提供'}\n`
|
||||
report += `\n`
|
||||
report += `测试结果:\n`
|
||||
report += `- 测试状态: ${testResult.success ? '✅ 通过' : '❌ 失败'}\n`
|
||||
report += `- 准确度: ${testResult.accuracy.toFixed(2)}%\n`
|
||||
report += `- 平均误差: ${testResult.avgError.toFixed(2)}px\n`
|
||||
report += `- 最大误差: ${testResult.maxError.toFixed(2)}px\n`
|
||||
report += `- 测试点数: ${testResult.testPoints.length}\n`
|
||||
report += `\n`
|
||||
|
||||
if (!testResult.success) {
|
||||
report += `❌ 问题点分析:\n`
|
||||
testResult.testPoints
|
||||
.filter(p => p.error > 5)
|
||||
.forEach((p, i) => {
|
||||
report += ` ${i + 1}. 输入(${p.input.x.toFixed(1)}, ${p.input.y.toFixed(1)}) → 输出(${p.actual.x.toFixed(1)}, ${p.actual.y.toFixed(1)}) 误差: ${p.error.toFixed(2)}px\n`
|
||||
})
|
||||
}
|
||||
|
||||
if (deviceInfo) {
|
||||
report += `\n📱 设备特征:\n`
|
||||
report += `- 密度: ${deviceInfo.density}\n`
|
||||
report += `- DPI: ${deviceInfo.densityDpi}\n`
|
||||
report += `- 长宽比: ${deviceInfo.aspectRatio.toFixed(3)}\n`
|
||||
report += `- 虚拟按键: ${deviceInfo.hasNavigationBar ? '是' : '否'}\n`
|
||||
if (deviceInfo.hasNavigationBar) {
|
||||
report += `- 导航栏尺寸: ${deviceInfo.navigationBarSize.width}x${deviceInfo.navigationBarSize.height}\n`
|
||||
}
|
||||
}
|
||||
|
||||
report += `==========================================\n`
|
||||
|
||||
return report
|
||||
}
|
||||
}
|
||||
|
||||
export default CoordinateMapper
|
||||
160
src/utils/CoordinateMappingConfig.ts
Normal file
160
src/utils/CoordinateMappingConfig.ts
Normal file
@@ -0,0 +1,160 @@
|
||||
/**
|
||||
* 坐标映射配置系统 - 渐进式功能启用
|
||||
*/
|
||||
|
||||
export interface CoordinateMappingConfig {
|
||||
// 基础功能(始终启用)
|
||||
enableBasicMapping: boolean
|
||||
|
||||
// 高精度功能(可选启用)
|
||||
enableSubPixelPrecision: boolean
|
||||
enableNavigationBarDetection: boolean
|
||||
enableDensityCorrection: boolean
|
||||
enableAspectRatioCorrection: boolean
|
||||
|
||||
// 调试功能
|
||||
enableDetailedLogging: boolean
|
||||
enableCoordinateValidation: boolean
|
||||
enablePerformanceMonitoring: boolean
|
||||
|
||||
// 容错设置
|
||||
fallbackToBasicOnError: boolean
|
||||
maxCoordinateError: number // 最大允许的坐标误差(像素)
|
||||
|
||||
// 设备特定优化
|
||||
enableDeviceSpecificOptimizations: boolean
|
||||
minimumScreenSize: { width: number; height: number }
|
||||
maximumScreenSize: { width: number; height: number }
|
||||
}
|
||||
|
||||
export const DEFAULT_COORDINATE_MAPPING_CONFIG: CoordinateMappingConfig = {
|
||||
// 基础功能
|
||||
enableBasicMapping: true,
|
||||
|
||||
// 高精度功能(逐步启用)
|
||||
enableSubPixelPrecision: false, // 🚩 第一阶段:关闭
|
||||
enableNavigationBarDetection: true, // ✅ 相对安全
|
||||
enableDensityCorrection: true, // ✅ 相对安全
|
||||
enableAspectRatioCorrection: true, // ✅ 相对安全
|
||||
|
||||
// 调试功能
|
||||
enableDetailedLogging: false, // 默认关闭,需要时开启
|
||||
enableCoordinateValidation: true,
|
||||
enablePerformanceMonitoring: false,
|
||||
|
||||
// 容错设置
|
||||
fallbackToBasicOnError: true,
|
||||
maxCoordinateError: 10, // 允许10像素误差
|
||||
|
||||
// 设备特定优化
|
||||
enableDeviceSpecificOptimizations: false, // 🚩 第一阶段:关闭
|
||||
minimumScreenSize: { width: 240, height: 320 },
|
||||
maximumScreenSize: { width: 4096, height: 8192 }
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据设备特征自动调整配置
|
||||
*/
|
||||
export function createOptimizedConfig(
|
||||
deviceWidth: number,
|
||||
deviceHeight: number,
|
||||
_userAgent?: string
|
||||
): CoordinateMappingConfig {
|
||||
const config = { ...DEFAULT_COORDINATE_MAPPING_CONFIG }
|
||||
|
||||
// 基于屏幕尺寸调整
|
||||
const screenArea = deviceWidth * deviceHeight
|
||||
const aspectRatio = Math.max(deviceWidth, deviceHeight) / Math.min(deviceWidth, deviceHeight)
|
||||
|
||||
// 高分辨率设备可以启用亚像素精度
|
||||
if (screenArea > 2073600) { // 1080p以上
|
||||
config.enableSubPixelPrecision = true
|
||||
}
|
||||
|
||||
// 长屏设备通常有导航栏
|
||||
if (aspectRatio > 2.0) {
|
||||
config.enableNavigationBarDetection = true
|
||||
}
|
||||
|
||||
// 超大屏幕需要更高的容错性
|
||||
if (deviceWidth > 1440 || deviceHeight > 2560) {
|
||||
config.maxCoordinateError = 15
|
||||
}
|
||||
|
||||
return config
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证坐标映射配置的安全性
|
||||
*/
|
||||
export function validateConfig(config: CoordinateMappingConfig): {
|
||||
isValid: boolean
|
||||
warnings: string[]
|
||||
errors: string[]
|
||||
} {
|
||||
const warnings: string[] = []
|
||||
const errors: string[] = []
|
||||
|
||||
// 检查基础配置
|
||||
if (!config.enableBasicMapping) {
|
||||
errors.push('基础坐标映射不能被禁用')
|
||||
}
|
||||
|
||||
// 检查容错设置
|
||||
if (config.maxCoordinateError < 1) {
|
||||
warnings.push('坐标误差容忍度过低,可能导致映射失败')
|
||||
}
|
||||
|
||||
if (config.maxCoordinateError > 50) {
|
||||
warnings.push('坐标误差容忍度过高,可能影响精度')
|
||||
}
|
||||
|
||||
// 检查屏幕尺寸范围
|
||||
if (config.minimumScreenSize.width < 100 || config.minimumScreenSize.height < 100) {
|
||||
errors.push('最小屏幕尺寸设置过小')
|
||||
}
|
||||
|
||||
if (config.maximumScreenSize.width > 10000 || config.maximumScreenSize.height > 10000) {
|
||||
warnings.push('最大屏幕尺寸设置过大')
|
||||
}
|
||||
|
||||
return {
|
||||
isValid: errors.length === 0,
|
||||
warnings,
|
||||
errors
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取功能启用状态的摘要
|
||||
*/
|
||||
export function getConfigSummary(config: CoordinateMappingConfig): string {
|
||||
const enabledFeatures: string[] = []
|
||||
const disabledFeatures: string[] = []
|
||||
|
||||
const features = [
|
||||
{ key: 'enableSubPixelPrecision', name: '亚像素精度' },
|
||||
{ key: 'enableNavigationBarDetection', name: '导航栏检测' },
|
||||
{ key: 'enableDensityCorrection', name: '密度修正' },
|
||||
{ key: 'enableAspectRatioCorrection', name: '长宽比修正' },
|
||||
{ key: 'enableDeviceSpecificOptimizations', name: '设备特定优化' }
|
||||
]
|
||||
|
||||
features.forEach(feature => {
|
||||
if (config[feature.key as keyof CoordinateMappingConfig]) {
|
||||
enabledFeatures.push(feature.name)
|
||||
} else {
|
||||
disabledFeatures.push(feature.name)
|
||||
}
|
||||
})
|
||||
|
||||
let summary = `坐标映射配置摘要:\n`
|
||||
summary += `✅ 已启用: ${enabledFeatures.join(', ') || '无'}\n`
|
||||
summary += `❌ 已禁用: ${disabledFeatures.join(', ') || '无'}\n`
|
||||
summary += `🎯 误差容忍: ${config.maxCoordinateError}px\n`
|
||||
summary += `🔧 回退模式: ${config.fallbackToBasicOnError ? '启用' : '禁用'}`
|
||||
|
||||
return summary
|
||||
}
|
||||
|
||||
export default DEFAULT_COORDINATE_MAPPING_CONFIG
|
||||
425
src/utils/SafeCoordinateMapper.ts
Normal file
425
src/utils/SafeCoordinateMapper.ts
Normal file
@@ -0,0 +1,425 @@
|
||||
/**
|
||||
* 安全的坐标映射工具类 - 渐进式增强
|
||||
*
|
||||
* 设计原则:
|
||||
* 1. 基础功能始终可用
|
||||
* 2. 高级功能可选启用
|
||||
* 3. 错误自动回退到基础功能
|
||||
* 4. 详细的性能监控和错误日志
|
||||
*/
|
||||
|
||||
import {
|
||||
type CoordinateMappingConfig,
|
||||
DEFAULT_COORDINATE_MAPPING_CONFIG,
|
||||
validateConfig
|
||||
} from './CoordinateMappingConfig'
|
||||
|
||||
export interface DeviceInfo {
|
||||
density: number
|
||||
densityDpi: number
|
||||
hasNavigationBar: boolean
|
||||
aspectRatio: number
|
||||
realScreenSize: { width: number; height: number }
|
||||
appScreenSize: { width: number; height: number }
|
||||
navigationBarSize: { width: number; height: number }
|
||||
}
|
||||
|
||||
export interface CoordinateMappingResult {
|
||||
x: number
|
||||
y: number
|
||||
metadata: {
|
||||
scale: number
|
||||
density: number
|
||||
actualDisplayArea: { width: number; height: number }
|
||||
offset: { x: number; y: number }
|
||||
aspectRatioDiff: number
|
||||
processingTime: number
|
||||
method: string
|
||||
errors: string[]
|
||||
warnings: string[]
|
||||
}
|
||||
}
|
||||
|
||||
export class SafeCoordinateMapper {
|
||||
private config: CoordinateMappingConfig
|
||||
private performanceStats: {
|
||||
totalMappings: number
|
||||
successfulMappings: number
|
||||
failedMappings: number
|
||||
averageProcessingTime: number
|
||||
errorRates: { [key: string]: number }
|
||||
}
|
||||
|
||||
constructor(config?: Partial<CoordinateMappingConfig>) {
|
||||
this.config = { ...DEFAULT_COORDINATE_MAPPING_CONFIG, ...config }
|
||||
this.performanceStats = {
|
||||
totalMappings: 0,
|
||||
successfulMappings: 0,
|
||||
failedMappings: 0,
|
||||
averageProcessingTime: 0,
|
||||
errorRates: {}
|
||||
}
|
||||
|
||||
// 验证配置
|
||||
const validation = validateConfig(this.config)
|
||||
if (!validation.isValid) {
|
||||
console.error('SafeCoordinateMapper配置无效:', validation.errors)
|
||||
throw new Error('Invalid coordinate mapping configuration')
|
||||
}
|
||||
|
||||
if (validation.warnings.length > 0) {
|
||||
console.warn('SafeCoordinateMapper配置警告:', validation.warnings)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 安全的坐标映射 - 主要入口点
|
||||
*/
|
||||
mapCoordinates(
|
||||
canvasX: number,
|
||||
canvasY: number,
|
||||
canvasElement: HTMLCanvasElement,
|
||||
deviceWidth: number,
|
||||
deviceHeight: number,
|
||||
deviceInfo?: DeviceInfo
|
||||
): CoordinateMappingResult | null {
|
||||
const startTime = performance.now()
|
||||
const errors: string[] = []
|
||||
const warnings: string[] = []
|
||||
|
||||
try {
|
||||
this.performanceStats.totalMappings++
|
||||
|
||||
// 🔒 阶段1:基础验证
|
||||
const basicValidation = this.validateBasicInputs(
|
||||
canvasX, canvasY, canvasElement, deviceWidth, deviceHeight
|
||||
)
|
||||
if (!basicValidation.isValid) {
|
||||
errors.push(...basicValidation.errors)
|
||||
warnings.push(...basicValidation.warnings)
|
||||
|
||||
if (this.config.fallbackToBasicOnError) {
|
||||
return this.performBasicMapping(
|
||||
canvasX, canvasY, canvasElement, deviceWidth, deviceHeight,
|
||||
startTime, 'basic_fallback', errors, warnings
|
||||
)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
// 🔒 阶段2:基础坐标映射
|
||||
const basicResult = this.performBasicMapping(
|
||||
canvasX, canvasY, canvasElement, deviceWidth, deviceHeight,
|
||||
startTime, 'basic', errors, warnings
|
||||
)
|
||||
|
||||
if (!basicResult) {
|
||||
this.performanceStats.failedMappings++
|
||||
return null
|
||||
}
|
||||
|
||||
// 🔒 阶段3:渐进式增强
|
||||
const enhancedResult = this.applyEnhancements(
|
||||
basicResult, deviceInfo, deviceWidth, deviceHeight
|
||||
)
|
||||
|
||||
// 🔒 阶段4:最终验证和统计
|
||||
const finalResult = this.validateAndFinalizeMappingResult(
|
||||
enhancedResult, deviceWidth, deviceHeight, startTime
|
||||
)
|
||||
|
||||
this.performanceStats.successfulMappings++
|
||||
this.updatePerformanceStats(finalResult.metadata.processingTime)
|
||||
|
||||
return finalResult
|
||||
|
||||
} catch (error) {
|
||||
this.performanceStats.failedMappings++
|
||||
const errorMessage = `坐标映射失败: ${error instanceof Error ? error.message : 'Unknown error'}`
|
||||
console.error(errorMessage, error)
|
||||
|
||||
if (this.config.fallbackToBasicOnError) {
|
||||
return this.performBasicMapping(
|
||||
canvasX, canvasY, canvasElement, deviceWidth, deviceHeight,
|
||||
startTime, 'error_fallback', [errorMessage], warnings
|
||||
)
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 🔒 基础输入验证
|
||||
*/
|
||||
private validateBasicInputs(
|
||||
canvasX: number,
|
||||
canvasY: number,
|
||||
canvasElement: HTMLCanvasElement,
|
||||
deviceWidth: number,
|
||||
deviceHeight: number
|
||||
): { isValid: boolean; errors: string[]; warnings: string[] } {
|
||||
const errors: string[] = []
|
||||
const warnings: string[] = []
|
||||
|
||||
// 检查基础参数
|
||||
if (!canvasElement || !canvasElement.parentElement) {
|
||||
errors.push('Canvas元素或其容器不存在')
|
||||
}
|
||||
|
||||
if (deviceWidth <= 0 || deviceHeight <= 0) {
|
||||
errors.push(`设备尺寸无效: ${deviceWidth}×${deviceHeight}`)
|
||||
}
|
||||
|
||||
if (canvasX < 0 || canvasY < 0) {
|
||||
warnings.push('Canvas坐标为负数')
|
||||
}
|
||||
|
||||
// 检查设备尺寸范围
|
||||
if (deviceWidth < this.config.minimumScreenSize.width ||
|
||||
deviceHeight < this.config.minimumScreenSize.height) {
|
||||
warnings.push('设备尺寸过小')
|
||||
}
|
||||
|
||||
if (deviceWidth > this.config.maximumScreenSize.width ||
|
||||
deviceHeight > this.config.maximumScreenSize.height) {
|
||||
warnings.push('设备尺寸过大')
|
||||
}
|
||||
|
||||
return { isValid: errors.length === 0, errors, warnings }
|
||||
}
|
||||
|
||||
/**
|
||||
* 🔒 基础坐标映射 - 始终可用的核心功能
|
||||
*/
|
||||
private performBasicMapping(
|
||||
canvasX: number,
|
||||
canvasY: number,
|
||||
canvasElement: HTMLCanvasElement,
|
||||
deviceWidth: number,
|
||||
deviceHeight: number,
|
||||
startTime: number,
|
||||
method: string,
|
||||
errors: string[],
|
||||
warnings: string[]
|
||||
): CoordinateMappingResult | null {
|
||||
try {
|
||||
const container = canvasElement.parentElement!
|
||||
const containerRect = container.getBoundingClientRect()
|
||||
|
||||
const displayWidth = containerRect.width
|
||||
const displayHeight = containerRect.height
|
||||
|
||||
// 基础缩放计算
|
||||
const scaleX = displayWidth / deviceWidth
|
||||
const scaleY = displayHeight / deviceHeight
|
||||
const scale = Math.min(scaleX, scaleY)
|
||||
|
||||
// 基础显示区域计算
|
||||
const actualDisplayWidth = deviceWidth * scale
|
||||
const actualDisplayHeight = deviceHeight * scale
|
||||
const offsetX = (displayWidth - actualDisplayWidth) / 2
|
||||
const offsetY = (displayHeight - actualDisplayHeight) / 2
|
||||
|
||||
// 基础坐标转换
|
||||
const adjustedCanvasX = canvasX - offsetX
|
||||
const adjustedCanvasY = canvasY - offsetY
|
||||
|
||||
// 检查是否在显示区域内
|
||||
if (adjustedCanvasX < 0 || adjustedCanvasX > actualDisplayWidth ||
|
||||
adjustedCanvasY < 0 || adjustedCanvasY > actualDisplayHeight) {
|
||||
return null
|
||||
}
|
||||
|
||||
const deviceX = adjustedCanvasX / scale
|
||||
const deviceY = adjustedCanvasY / scale
|
||||
|
||||
// 基础坐标限制
|
||||
const clampedX = Math.max(0, Math.min(deviceWidth, Math.round(deviceX)))
|
||||
const clampedY = Math.max(0, Math.min(deviceHeight, Math.round(deviceY)))
|
||||
|
||||
const processingTime = performance.now() - startTime
|
||||
|
||||
return {
|
||||
x: clampedX,
|
||||
y: clampedY,
|
||||
metadata: {
|
||||
scale,
|
||||
density: 1.0, // 基础密度
|
||||
actualDisplayArea: { width: actualDisplayWidth, height: actualDisplayHeight },
|
||||
offset: { x: offsetX, y: offsetY },
|
||||
aspectRatioDiff: Math.abs((deviceWidth / deviceHeight) - (displayWidth / displayHeight)),
|
||||
processingTime,
|
||||
method,
|
||||
errors: [...errors],
|
||||
warnings: [...warnings]
|
||||
}
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('基础坐标映射失败:', error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 🔒 渐进式增强 - 根据配置启用高级功能
|
||||
*/
|
||||
private applyEnhancements(
|
||||
basicResult: CoordinateMappingResult,
|
||||
deviceInfo?: DeviceInfo,
|
||||
deviceWidth?: number,
|
||||
deviceHeight?: number
|
||||
): CoordinateMappingResult {
|
||||
const enhanced = { ...basicResult }
|
||||
|
||||
try {
|
||||
// 🔧 增强1:密度修正
|
||||
if (this.config.enableDensityCorrection && deviceInfo) {
|
||||
enhanced.metadata.density = deviceInfo.density
|
||||
enhanced.metadata.method += '+density'
|
||||
|
||||
// 高密度设备的亚像素精度
|
||||
if (this.config.enableSubPixelPrecision && deviceInfo.density > 2) {
|
||||
enhanced.x = Math.round(enhanced.x * 10) / 10
|
||||
enhanced.y = Math.round(enhanced.y * 10) / 10
|
||||
enhanced.metadata.method += '+subpixel'
|
||||
}
|
||||
}
|
||||
|
||||
// 🔧 增强2:导航栏检测
|
||||
if (this.config.enableNavigationBarDetection && deviceInfo && deviceHeight) {
|
||||
if (deviceInfo.hasNavigationBar && deviceInfo.navigationBarSize?.height > 0) {
|
||||
const navBarHeight = deviceInfo.navigationBarSize.height
|
||||
const navigationBarThreshold = deviceHeight - navBarHeight
|
||||
|
||||
if (enhanced.y > navigationBarThreshold) {
|
||||
enhanced.y = Math.min(enhanced.y, deviceHeight + Math.min(navBarHeight, 150))
|
||||
enhanced.metadata.method += '+navbar'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 🔧 增强3:长宽比修正
|
||||
if (this.config.enableAspectRatioCorrection && deviceInfo) {
|
||||
const aspectRatioDiff = Math.abs(deviceInfo.aspectRatio - (deviceWidth! / deviceHeight!))
|
||||
if (aspectRatioDiff > 0.1) {
|
||||
enhanced.metadata.warnings.push(`长宽比差异较大: ${aspectRatioDiff.toFixed(3)}`)
|
||||
}
|
||||
}
|
||||
|
||||
// 🔧 增强4:设备特定优化
|
||||
if (this.config.enableDeviceSpecificOptimizations && deviceInfo && deviceWidth && deviceHeight) {
|
||||
const screenArea = deviceWidth * deviceHeight
|
||||
if (screenArea > 2073600) { // 1080p以上
|
||||
enhanced.metadata.method += '+highres'
|
||||
}
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
enhanced.metadata.errors.push(`增强处理失败: ${error instanceof Error ? error.message : 'Unknown error'}`)
|
||||
}
|
||||
|
||||
return enhanced
|
||||
}
|
||||
|
||||
/**
|
||||
* 🔒 最终验证和结果处理
|
||||
*/
|
||||
private validateAndFinalizeMappingResult(
|
||||
result: CoordinateMappingResult,
|
||||
deviceWidth: number,
|
||||
deviceHeight: number,
|
||||
startTime: number
|
||||
): CoordinateMappingResult {
|
||||
// 最终坐标验证
|
||||
if (this.config.enableCoordinateValidation) {
|
||||
const errorX = Math.abs(result.x - Math.round(result.x))
|
||||
const errorY = Math.abs(result.y - Math.round(result.y))
|
||||
|
||||
if (errorX > this.config.maxCoordinateError || errorY > this.config.maxCoordinateError) {
|
||||
result.metadata.warnings.push(`坐标误差过大: (${errorX.toFixed(2)}, ${errorY.toFixed(2)})`)
|
||||
}
|
||||
}
|
||||
|
||||
// 确保坐标在边界内
|
||||
result.x = Math.max(0, Math.min(deviceWidth, result.x))
|
||||
result.y = Math.max(0, Math.min(deviceHeight, result.y))
|
||||
|
||||
// 更新处理时间
|
||||
result.metadata.processingTime = performance.now() - startTime
|
||||
|
||||
// 详细日志
|
||||
if (this.config.enableDetailedLogging) {
|
||||
console.log('SafeCoordinateMapper映射完成:', {
|
||||
coordinates: { x: result.x, y: result.y },
|
||||
metadata: result.metadata,
|
||||
config: this.config,
|
||||
stats: this.performanceStats
|
||||
})
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新性能统计
|
||||
*/
|
||||
private updatePerformanceStats(processingTime: number) {
|
||||
if (this.config.enablePerformanceMonitoring) {
|
||||
const totalTime = this.performanceStats.averageProcessingTime * (this.performanceStats.successfulMappings - 1)
|
||||
this.performanceStats.averageProcessingTime = (totalTime + processingTime) / this.performanceStats.successfulMappings
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取性能统计报告
|
||||
*/
|
||||
getPerformanceReport(): string {
|
||||
const successRate = this.performanceStats.totalMappings > 0
|
||||
? (this.performanceStats.successfulMappings / this.performanceStats.totalMappings * 100).toFixed(1)
|
||||
: '0'
|
||||
|
||||
return `
|
||||
SafeCoordinateMapper性能报告:
|
||||
📊 总映射次数: ${this.performanceStats.totalMappings}
|
||||
✅ 成功次数: ${this.performanceStats.successfulMappings}
|
||||
❌ 失败次数: ${this.performanceStats.failedMappings}
|
||||
📈 成功率: ${successRate}%
|
||||
⏱️ 平均处理时间: ${this.performanceStats.averageProcessingTime.toFixed(2)}ms
|
||||
🔧 回退模式: ${this.config.fallbackToBasicOnError ? '启用' : '禁用'}
|
||||
`
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新配置
|
||||
*/
|
||||
updateConfig(newConfig: Partial<CoordinateMappingConfig>) {
|
||||
const updatedConfig = { ...this.config, ...newConfig }
|
||||
const validation = validateConfig(updatedConfig)
|
||||
|
||||
if (!validation.isValid) {
|
||||
console.error('配置更新失败:', validation.errors)
|
||||
return false
|
||||
}
|
||||
|
||||
this.config = updatedConfig
|
||||
console.log('SafeCoordinateMapper配置已更新')
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置性能统计
|
||||
*/
|
||||
resetPerformanceStats() {
|
||||
this.performanceStats = {
|
||||
totalMappings: 0,
|
||||
successfulMappings: 0,
|
||||
failedMappings: 0,
|
||||
averageProcessingTime: 0,
|
||||
errorRates: {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default SafeCoordinateMapper
|
||||
Reference in New Issue
Block a user