From 3b5c4501af0df376fbde2e4d4e7e52f686e161fb Mon Sep 17 00:00:00 2001 From: zhangyan Date: Thu, 26 Mar 2026 17:58:42 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=8C=E5=96=84=E8=AE=BE=E7=BD=AE=E9=9D=A2?= =?UTF-8?q?=E6=9D=BF=E5=B9=B6=E6=95=B4=E7=90=86=E5=8A=A8=E7=94=BB=E9=98=B6?= =?UTF-8?q?=E6=AE=B5=E6=80=BB=E7=BB=93?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- GeminiAnlysis.md | 50 +++++ MyToDo.md | 0 animation-pipeline-summary.md | 192 ++++++++++++++++++ miniprogram/engine/map/mapEngine.ts | 38 ++++ miniprogram/pages/map/map.ts | 290 +++++++++++++++++++++++----- miniprogram/pages/map/map.wxml | 272 +++++++++++++++++--------- miniprogram/pages/map/map.wxss | 69 ++++++- project.config.json | 2 +- 8 files changed, 773 insertions(+), 140 deletions(-) create mode 100644 GeminiAnlysis.md create mode 100644 MyToDo.md create mode 100644 animation-pipeline-summary.md diff --git a/GeminiAnlysis.md b/GeminiAnlysis.md new file mode 100644 index 0000000..a13de7a --- /dev/null +++ b/GeminiAnlysis.md @@ -0,0 +1,50 @@ +# CMR-Mini 项目深度分析报告 (GeminiAnalysis.md) + +## 1. 项目定位与核心愿景 +**CMR-Mini** 是一个运行在微信小程序环境中的高性能**定向越野 (Orienteering)** 实时竞赛/练习引擎。其核心竞争力在于通过自研的 **WebGL 地图渲染管线** 提供流畅的地图交互,并结合高精度多传感器融合技术(GPS、罗盘、心率、加速度计等)实现精准的运动反馈。 + +## 2. 核心系统架构分析 + +### 2.1 地图渲染引擎 (Map Engine) +* **渲染技术**:采用 `Single WebGL Pipeline`。相比微信原生地图组件,具有更高的定制化能力,特别是在“Heading-Up”(朝向朝上)模式下的性能表现。 +* **瓦片管理**:通过 `TileStore` 实现三级缓存(内存 -> 磁盘 -> 网络),并支持 `tilePersistentCache`。 +* **投影逻辑**:采用 `WGS84 -> WorldTile -> Camera -> Screen` 的标准 GIS 变换链,能够精准处理地理坐标到屏幕像素的映射。 + +### 2.2 传感器融合系统 (Sensor System) +* **CompassHeadingController**:核心逻辑在于罗盘数据 (`wx.onCompassChange`) 与设备姿态 (`wx.onDeviceMotionChange`) 的协同。 +* **LocationController**:支持真实 GPS 数据与 Mock 模拟器(通过 WebSocket 连接 `mock-gps-sim` 工具)的无缝切换。 +* **TelemetryRuntime**:实现了运动参数的实时计算,包括速度、距离目标点距离、心率分区等指标。 + +### 2.3 游戏逻辑与规则 (Game Logic) +* **GameRuntime**:驱动对局状态机,支持“顺序赛 (Classic Sequential)”与“积分赛 (Score-O)”。 +* **PunchPolicy**:实现了自动进入检查点范围触发、手动打点、跳过点位等业务逻辑。 + +## 3. 指北针 (Compass) 平滑度瓶颈分析 +根据目前的实现,指北针的卡顿感主要源于以下三个层面: + +1. **采样频率与插值逻辑**: + * 目前使用 `interpolateHeadingDeg` 进行线性差值,且 `ABSOLUTE_HEADING_CORRECTION` 为固定系数 (0.44)。这种静态系数在“静态微调”时显得不够敏锐,在“快速旋转”时又显得滞后。 +2. **Android/iOS 差异化丢帧**: + * Android 传感器回调频率不稳定。 + * 逻辑中对 `direction` 进行了严格的数值有效性判断,若系统由于硬件抖动返回短时异常值,会导致视觉上的“跳帧”。 +3. **UI 同步周期限制**: + * `MapEngine` 的 `UI_SYNC_INTERVAL_MS` 设置为 80ms,这意味着视觉反馈的最高帧率仅为 12.5Hz,远低于屏幕刷新率,导致指针转动不够丝滑。 + +## 4. 优化技术路线建议 + +### 4.1 引入指数加权移动平均 (EWMA) 的动态系数 +建议根据旋转角速度动态调整平滑系数。当检测到瞬时角位移较大时,降低平滑度以追求响应速度;当位移较小时,增加平滑度以过滤手抖带来的噪声。 + +### 4.2 视觉平滑:使用 CSS Transform 或 WebGL 帧间补偿 +目前数据是由控制器下发到 UI 的。建议: +* **方案 A (推荐)**:在 UI 层(`.wxml`/`.wxss`)利用 `transition: transform 0.1s linear;` 实现视觉层面的自动补帧。 +* **方案 B**:在 WebGL 渲染循环内进行帧间插值,将数据的 12.5Hz 提升到 渲染循环的 60Hz。 + +### 4.3 预测与死区 (Dead-zone) 过滤 +在 `CompassHeadingController` 中加入微小位移的死区过滤逻辑,避免由于硬件高频微小抖动导致的视图高频重绘,降低系统功耗的同时提升视觉稳定性。 + +## 5. 结论 +CMR-Mini 已经建立了一个非常坚实的专业定向越野引擎基础。后续的优化重点应从“功能的实现”转向“交互的极致平滑”,特别是针对指北针这类核心导向组件,需要更精细化的信号处理策略。 + +--- +*Generated by Gemini CLI Analysis Tool* diff --git a/MyToDo.md b/MyToDo.md new file mode 100644 index 0000000..e69de29 diff --git a/animation-pipeline-summary.md b/animation-pipeline-summary.md new file mode 100644 index 0000000..46f2280 --- /dev/null +++ b/animation-pipeline-summary.md @@ -0,0 +1,192 @@ +# 动画体系阶段性小结 + +## 1. 当前定位 + +目前动画体系已经从“页面里临时加 class”的阶段,进入了**有主链、有分层、有性能分级**的阶段。 + +当前主链可以概括为: + +- 事件触发 +- `feedbackConfig` +- `UiEffectDirector` +- `FeedbackDirector` +- `MapEngine` +- 页面层 / 渲染层消费 + +也就是: + +**事件 -> 效果配置 -> 宿主提交 -> 页面 / Renderer 落地** + +这说明动画已经不再是零散实现,而开始进入架构化管理。 + +--- + +## 2. 已经完成的内容 + +### 2.1 HUD 动效 + +已经完成: + +- 打点成功后的 `进度` 动效 +- 打点成功后的 `点距` 动效 +- HUD 数字轻量过渡: + - 计时 + - 里程 + - 速度 + - 心率 + +这些动效已经接入正式链路,不是页面单独临时处理。 + +### 2.2 地图空间动画 + +已经完成: + +- 当前目标点状态强调 +- 可打点状态强调 +- 已完成点状态过渡 +- 已跳过点灰态与标记 +- 开始点 / 终点完成后的 settle 外环 +- 轻量地图 pulse + +### 2.3 局部 UI / Stage 动效 + +已经完成: + +- 轻量 stage flash +- 顶部提示和局部反馈的基础动画承载链 + +### 2.4 动画性能分级 + +已经完成 2 级动画分级: + +- `standard` +- `lite` + +当前 `lite` 的主要策略包括: + +- 减少 pulse 层数 +- 降低几何分段 +- 降低渲染动画频率 +- 关闭部分 HUD 动画 +- 关闭或减弱某些 stage/UI 动效 + +这意味着动画体系已经开始考虑**低端机表现**,不是只追求效果。 + +--- + +## 3. 当前架构上的价值 + +动画体系现在已经带来了几个明确收益: + +- 动效不再散落在多个页面细节里 +- 高频状态变化有了统一反馈语言 +- 地图状态和 HUD 状态开始形成一致体验 +- 性能分级已经进入体系,可服务低端机 + +从架构角度看,这意味着: + +**动画已经成为正式能力层,而不是临时视觉补丁。** + +--- + +## 4. 当前还不够完整的地方 + +虽然主链已经成型,但当前还没有完全形成“动画字典”和完整 profile 体系。 + +目前仍然存在这些不足: + +- 哪些事件触发哪些动画,还没有整理成统一字典 +- 部分高频状态切换还不够连续 +- `跳点` 已有逻辑和状态,但动画语言还不完整 +- 危险/高压状态动画还没有正式开始 +- 动画 profile 还没有真正配置化 + +所以当前阶段可以定义为: + +**第一阶段后半段:主链已成型,但还需要把高频体验打磨完整。** + +--- + +## 5. 下一阶段最值得做的事情 + +### 5.1 先整理动画字典 + +建议先把动画按事件梳理出来,例如: + +- `session_started` +- `control_ready` +- `control_completed:start` +- `control_completed:control` +- `control_completed:finish` +- `control_skipped` +- `gps_lock_changed` +- `guidance_state_changed` +- `heart_rate_zone_changed` + +并明确每个事件对应: + +- 地图动画 +- HUD 动画 +- UI 动画 +- `lite` 下是否保留 + +这一步是当前最值得优先完成的工作。 + +### 5.2 补完整“目标状态切换连续感” + +继续打磨: + +- 当前目标 +- 进入可打点 +- 打点成功 +- 切到下一个目标 + +让这一整段切换更连贯、更有节奏。 + +### 5.3 补齐“跳点”动画 + +建议下一步把跳点也正式纳入动画体系: + +- 跳点确认后 +- 当前点灰化 +- 下一个目标接管强调 +- HUD 给出轻量反馈 + +### 5.4 再做危险 / 高压反馈 + +这部分适合进入下一阶段: + +- 高心率反馈 +- 危险区反馈 +- 幽灵追逐反馈 +- 边缘呼吸 / 紧张感动效 + +这条线很适合后续玩法扩展。 + +--- + +## 6. 建议的实施顺序 + +推荐继续推进的顺序: + +1. 动画字典整理 +2. 目标切换连续感补齐 +3. 跳点动画补齐 +4. 危险 / 高压状态动画 +5. 更进一步的配置化 profile + +--- + +## 7. 结论 + +当前动画体系已经是一个明确的阶段性成果: + +- 有主链 +- 有分层 +- 有高频核心动画 +- 有性能分级 + +接下来最该做的不是“继续零散加动画”,而是: + +**把现有能力收成动画字典,并优先打磨目标切换与跳点这两条高频体验链。** + diff --git a/miniprogram/engine/map/mapEngine.ts b/miniprogram/engine/map/mapEngine.ts index 45e2936..b43a99b 100644 --- a/miniprogram/engine/map/mapEngine.ts +++ b/miniprogram/engine/map/mapEngine.ts @@ -59,6 +59,7 @@ const AUTO_ROTATE_MAX_STEP_DEG = 0.75 const AUTO_ROTATE_HEADING_SMOOTHING = 0.46 const COMPASS_NEEDLE_FRAME_MS = 16 const COMPASS_NEEDLE_SNAP_DEG = 0.08 +const COMPASS_BOOTSTRAP_RETRY_DELAY_MS = 700 const COMPASS_TUNING_PRESETS: Record mounted: boolean diagnosticUiEnabled: boolean @@ -838,6 +840,7 @@ export class MapEngine { smoothedSensorHeadingDeg: number | null compassDisplayHeadingDeg: number | null targetCompassDisplayHeadingDeg: number | null + lastCompassSampleAt: number compassSource: 'compass' | 'motion' | null compassTuningProfile: CompassTuningProfile smoothedMovementHeadingDeg: number | null @@ -1282,6 +1285,7 @@ export class MapEngine { this.viewSyncTimer = 0 this.autoRotateTimer = 0 this.compassNeedleTimer = 0 + this.compassBootstrapRetryTimer = 0 this.pendingViewPatch = {} this.mounted = false this.diagnosticUiEnabled = false @@ -1290,6 +1294,7 @@ export class MapEngine { this.smoothedSensorHeadingDeg = null this.compassDisplayHeadingDeg = null this.targetCompassDisplayHeadingDeg = null + this.lastCompassSampleAt = 0 this.compassSource = null this.compassTuningProfile = 'balanced' this.smoothedMovementHeadingDeg = null @@ -1406,6 +1411,7 @@ export class MapEngine { this.clearViewSyncTimer() this.clearAutoRotateTimer() this.clearCompassNeedleTimer() + this.clearCompassBootstrapRetryTimer() this.clearPunchFeedbackTimer() this.clearContentCardTimer() this.clearMapPulseTimer() @@ -1424,6 +1430,11 @@ export class MapEngine { handleAppShow(): void { this.feedbackDirector.setAppAudioMode('foreground') + if (this.mounted) { + this.lastCompassSampleAt = 0 + this.compassController.start() + this.scheduleCompassBootstrapRetry() + } } handleAppHide(): void { @@ -2351,7 +2362,9 @@ export class MapEngine { }) this.syncRenderer() this.accelerometerErrorText = null + this.lastCompassSampleAt = 0 this.compassController.start() + this.scheduleCompassBootstrapRetry() this.gyroscopeController.start() this.deviceMotionController.start() } @@ -2980,6 +2993,8 @@ export class MapEngine { } handleCompassHeading(headingDeg: number): void { + this.lastCompassSampleAt = Date.now() + this.clearCompassBootstrapRetryTimer() this.applyHeadingSample(headingDeg, 'compass') } @@ -3584,6 +3599,29 @@ export class MapEngine { } } + clearCompassBootstrapRetryTimer(): void { + if (this.compassBootstrapRetryTimer) { + clearTimeout(this.compassBootstrapRetryTimer) + this.compassBootstrapRetryTimer = 0 + } + } + + scheduleCompassBootstrapRetry(): void { + this.clearCompassBootstrapRetryTimer() + if (!this.mounted) { + return + } + + this.compassBootstrapRetryTimer = setTimeout(() => { + this.compassBootstrapRetryTimer = 0 + if (!this.mounted || this.lastCompassSampleAt > 0) { + return + } + this.compassController.stop() + this.compassController.start() + }, COMPASS_BOOTSTRAP_RETRY_DELAY_MS) as unknown as number + } + syncCompassDisplayState(): void { this.setState({ compassNeedleDeg: formatCompassNeedleDegForMode(this.northReferenceMode, this.compassDisplayHeadingDeg), diff --git a/miniprogram/pages/map/map.ts b/miniprogram/pages/map/map.ts index 4ae3d00..3a6331b 100644 --- a/miniprogram/pages/map/map.ts +++ b/miniprogram/pages/map/map.ts @@ -29,15 +29,37 @@ type ScaleRulerMajorMarkData = { topPx: number label: string } -type SideButtonMode = 'all' | 'left' | 'right' | 'hidden' +type SideButtonMode = 'shown' | 'hidden' type SideActionButtonState = 'muted' | 'default' | 'active' +type SideButtonPlacement = 'left' | 'right' type CenterScaleRulerAnchorMode = 'screen-center' | 'compass-center' type UserNorthReferenceMode = 'magnetic' | 'true' +type CompassTuningProfile = 'smooth' | 'balanced' | 'responsive' +type SettingLockKey = + | 'lockAnimationLevel' + | 'lockSideButtonPlacement' + | 'lockAutoRotate' + | 'lockCompassTuning' + | 'lockScaleRulerVisible' + | 'lockScaleRulerAnchor' + | 'lockNorthReference' + | 'lockHeartRateDevice' type StoredUserSettings = { animationLevel?: AnimationLevel + autoRotateEnabled?: boolean + compassTuningProfile?: CompassTuningProfile northReferenceMode?: UserNorthReferenceMode + sideButtonPlacement?: SideButtonPlacement showCenterScaleRuler?: boolean centerScaleRulerAnchorMode?: CenterScaleRulerAnchorMode + lockAnimationLevel?: boolean + lockSideButtonPlacement?: boolean + lockAutoRotate?: boolean + lockCompassTuning?: boolean + lockScaleRulerVisible?: boolean + lockScaleRulerAnchor?: boolean + lockNorthReference?: boolean + lockHeartRateDevice?: boolean } type MapPageData = MapEngineViewState & { showDebugPanel: boolean @@ -68,6 +90,16 @@ type MapPageData = MapEngineViewState & { compassTicks: CompassTickData[] compassLabels: CompassLabelData[] sideButtonMode: SideButtonMode + sideButtonPlacement: SideButtonPlacement + autoRotateEnabled: boolean + lockAnimationLevel: boolean + lockSideButtonPlacement: boolean + lockAutoRotate: boolean + lockCompassTuning: boolean + lockScaleRulerVisible: boolean + lockScaleRulerAnchor: boolean + lockNorthReference: boolean + lockHeartRateDevice: boolean sideToggleIconSrc: string sideButton2Class: string sideButton4Class: string @@ -334,12 +366,45 @@ function loadStoredUserSettings(): StoredUserSettings { if (normalized.northReferenceMode === 'magnetic' || normalized.northReferenceMode === 'true') { settings.northReferenceMode = normalized.northReferenceMode } + if (typeof normalized.autoRotateEnabled === 'boolean') { + settings.autoRotateEnabled = normalized.autoRotateEnabled + } + if (normalized.compassTuningProfile === 'smooth' || normalized.compassTuningProfile === 'balanced' || normalized.compassTuningProfile === 'responsive') { + settings.compassTuningProfile = normalized.compassTuningProfile + } + if (normalized.sideButtonPlacement === 'left' || normalized.sideButtonPlacement === 'right') { + settings.sideButtonPlacement = normalized.sideButtonPlacement + } if (typeof normalized.showCenterScaleRuler === 'boolean') { settings.showCenterScaleRuler = normalized.showCenterScaleRuler } if (normalized.centerScaleRulerAnchorMode === 'screen-center' || normalized.centerScaleRulerAnchorMode === 'compass-center') { settings.centerScaleRulerAnchorMode = normalized.centerScaleRulerAnchorMode } + if (typeof normalized.lockAnimationLevel === 'boolean') { + settings.lockAnimationLevel = normalized.lockAnimationLevel + } + if (typeof normalized.lockSideButtonPlacement === 'boolean') { + settings.lockSideButtonPlacement = normalized.lockSideButtonPlacement + } + if (typeof normalized.lockAutoRotate === 'boolean') { + settings.lockAutoRotate = normalized.lockAutoRotate + } + if (typeof normalized.lockCompassTuning === 'boolean') { + settings.lockCompassTuning = normalized.lockCompassTuning + } + if (typeof normalized.lockScaleRulerVisible === 'boolean') { + settings.lockScaleRulerVisible = normalized.lockScaleRulerVisible + } + if (typeof normalized.lockScaleRulerAnchor === 'boolean') { + settings.lockScaleRulerAnchor = normalized.lockScaleRulerAnchor + } + if (typeof normalized.lockNorthReference === 'boolean') { + settings.lockNorthReference = normalized.lockNorthReference + } + if (typeof normalized.lockHeartRateDevice === 'boolean') { + settings.lockHeartRateDevice = normalized.lockHeartRateDevice + } return settings } catch { return {} @@ -351,26 +416,24 @@ function persistStoredUserSettings(settings: StoredUserSettings) { wx.setStorageSync(USER_SETTINGS_STORAGE_KEY, settings) } catch {} } + +function toggleStoredSettingLock(settings: StoredUserSettings, key: SettingLockKey): StoredUserSettings { + return { + ...settings, + [key]: !settings[key], + } +} function buildSideButtonVisibility(mode: SideButtonMode) { return { sideButtonMode: mode, - showLeftButtonGroup: mode === 'all' || mode === 'left' || mode === 'right', - showRightButtonGroups: mode === 'all' || mode === 'right', - showBottomDebugButton: mode !== 'hidden', + showLeftButtonGroup: mode === 'shown', + showRightButtonGroups: false, + showBottomDebugButton: true, } } function getNextSideButtonMode(currentMode: SideButtonMode): SideButtonMode { - if (currentMode === 'all') { - return 'left' - } - if (currentMode === 'left') { - return 'right' - } - if (currentMode === 'right') { - return 'hidden' - } - return 'left' + return currentMode === 'shown' ? 'hidden' : 'shown' } function buildCompassTicks(): CompassTickData[] { const ticks: CompassTickData[] = [] @@ -409,9 +472,6 @@ function getFallbackStageRect(): MapEngineStageRect { } function getSideToggleIconSrc(mode: SideButtonMode): string { - if (mode === 'left') { - return '../../assets/btn_more2.png' - } if (mode === 'hidden') { return '../../assets/btn_more1.png' } @@ -641,6 +701,15 @@ Page({ hudPanelIndex: 0, configSourceText: '顺序赛配置', centerScaleRulerAnchorMode: 'screen-center', + autoRotateEnabled: false, + lockAnimationLevel: false, + lockSideButtonPlacement: false, + lockAutoRotate: false, + lockCompassTuning: false, + lockScaleRulerVisible: false, + lockScaleRulerAnchor: false, + lockNorthReference: false, + lockHeartRateDevice: false, gameInfoTitle: '当前游戏', gameInfoSubtitle: '未开始', gameInfoLocalRows: [], @@ -653,6 +722,7 @@ Page({ panelDistanceUnitText: '', panelProgressText: '0/0', showPunchHintBanner: true, + sideButtonPlacement: 'left', gameSessionStatus: 'idle', gameModeText: '顺序赛', gpsLockEnabled: false, @@ -730,9 +800,9 @@ Page({ centerScaleRulerMajorMarks: [], compassTicks: buildCompassTicks(), compassLabels: buildCompassLabels(), - ...buildSideButtonVisibility('left'), + ...buildSideButtonVisibility('shown'), ...buildSideButtonState({ - sideButtonMode: 'left', + sideButtonMode: 'shown', showGameInfoPanel: false, showSystemSettingsPanel: false, showCenterScaleRuler: false, @@ -787,6 +857,9 @@ Page({ } as MapPageData const derivedPatch: Partial = {} + if (typeof nextPatch.orientationMode === 'string') { + nextData.autoRotateEnabled = nextPatch.orientationMode === 'heading-up' + } if ( this.data.showCenterScaleRuler && hasAnyPatchKey(nextPatch as Record, CENTER_SCALE_RULER_DEP_KEYS) @@ -886,9 +959,19 @@ Page({ if (storedUserSettings.animationLevel) { mapEngine.handleSetAnimationLevel(storedUserSettings.animationLevel) } + const initialAutoRotateEnabled = storedUserSettings.autoRotateEnabled !== false + if (initialAutoRotateEnabled) { + mapEngine.handleSetHeadingUpMode() + } else { + mapEngine.handleSetManualMode() + } + if (storedUserSettings.compassTuningProfile) { + mapEngine.handleSetCompassTuningProfile(storedUserSettings.compassTuningProfile) + } if (storedUserSettings.northReferenceMode) { mapEngine.handleSetNorthReferenceMode(storedUserSettings.northReferenceMode) } + const initialSideButtonPlacement = storedUserSettings.sideButtonPlacement || 'left' mapEngine.setDiagnosticUiEnabled(false) centerScaleRulerInputCache = { @@ -914,6 +997,16 @@ Page({ hudPanelIndex: 0, configSourceText: '顺序赛配置', centerScaleRulerAnchorMode: initialCenterScaleRulerAnchorMode, + autoRotateEnabled: initialAutoRotateEnabled, + lockAnimationLevel: !!storedUserSettings.lockAnimationLevel, + lockSideButtonPlacement: !!storedUserSettings.lockSideButtonPlacement, + lockAutoRotate: !!storedUserSettings.lockAutoRotate, + lockCompassTuning: !!storedUserSettings.lockCompassTuning, + lockScaleRulerVisible: !!storedUserSettings.lockScaleRulerVisible, + lockScaleRulerAnchor: !!storedUserSettings.lockScaleRulerAnchor, + lockNorthReference: !!storedUserSettings.lockNorthReference, + lockHeartRateDevice: !!storedUserSettings.lockHeartRateDevice, + sideButtonPlacement: initialSideButtonPlacement, gameInfoTitle: '当前游戏', gameInfoSubtitle: '未开始', gameInfoLocalRows: [], @@ -996,9 +1089,9 @@ Page({ stageFxClass: '', compassTicks: buildCompassTicks(), compassLabels: buildCompassLabels(), - ...buildSideButtonVisibility('left'), + ...buildSideButtonVisibility('shown'), ...buildSideButtonState({ - sideButtonMode: 'left', + sideButtonMode: 'shown', showGameInfoPanel: false, showSystemSettingsPanel: false, showCenterScaleRuler: initialShowCenterScaleRuler, @@ -1218,24 +1311,6 @@ Page({ } }, - handleSetCompassTuningSmooth() { - if (mapEngine) { - mapEngine.handleSetCompassTuningProfile('smooth') - } - }, - - handleSetCompassTuningBalanced() { - if (mapEngine) { - mapEngine.handleSetCompassTuningProfile('balanced') - } - }, - - handleSetCompassTuningResponsive() { - if (mapEngine) { - mapEngine.handleSetCompassTuningProfile('responsive') - } - }, - handleAutoRotateCalibrate() { if (mapEngine) { mapEngine.handleAutoRotateCalibrate() @@ -1338,11 +1413,14 @@ Page({ } }, - handleClearPreferredHeartRateDevice() { - if (mapEngine) { - mapEngine.handleClearPreferredHeartRateDevice() - } - }, + handleClearPreferredHeartRateDevice() { + if (this.data.lockHeartRateDevice) { + return + } + if (mapEngine) { + mapEngine.handleClearPreferredHeartRateDevice() + } + }, handleDebugHeartRateBlue() { if (mapEngine) { @@ -1462,6 +1540,7 @@ Page({ const localRows = snapshot.localRows.concat([ { label: '比例尺开关', value: this.data.showCenterScaleRuler ? '开启' : '关闭' }, { label: '比例尺锚点', value: this.data.centerScaleRulerAnchorMode === 'compass-center' ? '指北针圆心' : '屏幕中心' }, + { label: '按钮习惯', value: this.data.sideButtonPlacement === 'right' ? '右手' : '左手' }, { label: '比例尺可见', value: this.data.centerScaleRulerVisible ? 'true' : 'false' }, { label: '比例尺中心X', value: `${this.data.centerScaleRulerCenterXPx}px` }, { label: '比例尺零点Y', value: `${this.data.centerScaleRulerZeroYPx}px` }, @@ -1575,7 +1654,7 @@ Page({ handleSystemSettingsPanelTap() {}, handleSetAnimationLevelStandard() { - if (!mapEngine) { + if (this.data.lockAnimationLevel || !mapEngine) { return } mapEngine.handleSetAnimationLevel('standard') @@ -1586,7 +1665,7 @@ Page({ }, handleSetAnimationLevelLite() { - if (!mapEngine) { + if (this.data.lockAnimationLevel || !mapEngine) { return } mapEngine.handleSetAnimationLevel('lite') @@ -1596,8 +1675,89 @@ Page({ }) }, + handleSetSideButtonPlacementLeft() { + if (this.data.lockSideButtonPlacement) { + return + } + this.setData({ + sideButtonPlacement: 'left', + }) + persistStoredUserSettings({ + ...loadStoredUserSettings(), + sideButtonPlacement: 'left', + }) + }, + + handleSetSideButtonPlacementRight() { + if (this.data.lockSideButtonPlacement) { + return + } + this.setData({ + sideButtonPlacement: 'right', + }) + persistStoredUserSettings({ + ...loadStoredUserSettings(), + sideButtonPlacement: 'right', + }) + }, + + handleSetAutoRotateEnabledOn() { + if (this.data.lockAutoRotate || !mapEngine) { + return + } + mapEngine.handleSetHeadingUpMode() + persistStoredUserSettings({ + ...loadStoredUserSettings(), + autoRotateEnabled: true, + }) + }, + + handleSetAutoRotateEnabledOff() { + if (this.data.lockAutoRotate || !mapEngine) { + return + } + mapEngine.handleSetManualMode() + persistStoredUserSettings({ + ...loadStoredUserSettings(), + autoRotateEnabled: false, + }) + }, + + handleSetCompassTuningSmooth() { + if (this.data.lockCompassTuning || !mapEngine) { + return + } + mapEngine.handleSetCompassTuningProfile('smooth') + persistStoredUserSettings({ + ...loadStoredUserSettings(), + compassTuningProfile: 'smooth', + }) + }, + + handleSetCompassTuningBalanced() { + if (this.data.lockCompassTuning || !mapEngine) { + return + } + mapEngine.handleSetCompassTuningProfile('balanced') + persistStoredUserSettings({ + ...loadStoredUserSettings(), + compassTuningProfile: 'balanced', + }) + }, + + handleSetCompassTuningResponsive() { + if (this.data.lockCompassTuning || !mapEngine) { + return + } + mapEngine.handleSetCompassTuningProfile('responsive') + persistStoredUserSettings({ + ...loadStoredUserSettings(), + compassTuningProfile: 'responsive', + }) + }, + handleSetNorthReferenceMagnetic() { - if (!mapEngine) { + if (this.data.lockNorthReference || !mapEngine) { return } mapEngine.handleSetNorthReferenceMode('magnetic') @@ -1608,7 +1768,7 @@ Page({ }, handleSetNorthReferenceTrue() { - if (!mapEngine) { + if (this.data.lockNorthReference || !mapEngine) { return } mapEngine.handleSetNorthReferenceMode('true') @@ -1618,6 +1778,18 @@ Page({ }) }, + handleToggleSettingLock(event: WechatMiniprogram.TouchEvent) { + const key = event.currentTarget.dataset.key as SettingLockKey | undefined + if (!key) { + return + } + const nextValue = !this.data[key] + this.setData({ + [key]: nextValue, + } as Record) + persistStoredUserSettings(toggleStoredSettingLock(loadStoredUserSettings(), key)) + }, + handleOverlayTouch() {}, handlePunchAction() { @@ -1674,16 +1846,24 @@ Page({ } }, handleToggleMapRotateMode() { - if (!mapEngine) { + if (!mapEngine || this.data.lockAutoRotate) { return } if (this.data.orientationMode === 'heading-up') { mapEngine.handleSetManualMode() + persistStoredUserSettings({ + ...loadStoredUserSettings(), + autoRotateEnabled: false, + }) return } mapEngine.handleSetHeadingUpMode() + persistStoredUserSettings({ + ...loadStoredUserSettings(), + autoRotateEnabled: true, + }) }, handleToggleDebugPanel() { const nextShowDebugPanel = !this.data.showDebugPanel @@ -1788,6 +1968,9 @@ Page({ }, handleSetCenterScaleRulerVisibleOn() { + if (this.data.lockScaleRulerVisible) { + return + } this.applyCenterScaleRulerSettings(true, this.data.centerScaleRulerAnchorMode) persistStoredUserSettings({ ...loadStoredUserSettings(), @@ -1797,6 +1980,9 @@ Page({ }, handleSetCenterScaleRulerVisibleOff() { + if (this.data.lockScaleRulerVisible) { + return + } this.applyCenterScaleRulerSettings(false, this.data.centerScaleRulerAnchorMode) persistStoredUserSettings({ ...loadStoredUserSettings(), @@ -1806,6 +1992,9 @@ Page({ }, handleSetCenterScaleRulerAnchorScreenCenter() { + if (this.data.lockScaleRulerAnchor) { + return + } this.applyCenterScaleRulerSettings(this.data.showCenterScaleRuler, 'screen-center') persistStoredUserSettings({ ...loadStoredUserSettings(), @@ -1815,6 +2004,9 @@ Page({ }, handleSetCenterScaleRulerAnchorCompassCenter() { + if (this.data.lockScaleRulerAnchor) { + return + } this.applyCenterScaleRulerSettings(this.data.showCenterScaleRuler, 'compass-center') persistStoredUserSettings({ ...loadStoredUserSettings(), diff --git a/miniprogram/pages/map/map.wxml b/miniprogram/pages/map/map.wxml index 752e29b..8280b9e 100644 --- a/miniprogram/pages/map/map.wxml +++ b/miniprogram/pages/map/map.wxml @@ -85,36 +85,30 @@ × - + - + - 1 - 2 - 3 - - - - - 5 - 6 - 7 - 8 - 9 - 10 - - - - - 12 - 13 - 14 - 15 + + + + + + + @@ -295,77 +289,179 @@ - - + + + + + @@ -429,6 +525,7 @@ Sensors 定位、罗盘与心率带连接状态 + 定位 GPS {{gpsTrackingText}} @@ -469,6 +566,7 @@ Mock Speed {{mockSpeedText}} + 心率 Heart Rate {{heartRateStatusText}} @@ -532,6 +630,7 @@ Mock BPM {{mockHeartRateText}} + 方向 Heading Mode {{orientationModeText}} @@ -560,11 +659,6 @@ Compass Tune {{compassTuningProfileText}} - - 顺滑 - 平衡 - 跟手 - Accel {{accelerometerText}} diff --git a/miniprogram/pages/map/map.wxss b/miniprogram/pages/map/map.wxss index 3864139..b1aa2ae 100644 --- a/miniprogram/pages/map/map.wxss +++ b/miniprogram/pages/map/map.wxss @@ -305,10 +305,17 @@ .map-side-toggle { position: absolute; - left: 24rpx; z-index: 19; } +.map-side-toggle--left { + left: 24rpx; +} + +.map-side-toggle--right { + right: 24rpx; +} + .map-side-column { position: absolute; display: flex; @@ -322,6 +329,10 @@ left: 24rpx; } +.map-side-column--right-group { + right: 24rpx; +} + .map-side-column--left-group { padding-top: 106rpx; } @@ -1417,6 +1428,18 @@ margin-bottom: 12rpx; } +.debug-section__header-row { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 16rpx; +} + +.debug-section__header-main { + flex: 1; + min-width: 0; +} + .debug-section__title { font-size: 24rpx; line-height: 1.2; @@ -1433,6 +1456,36 @@ color: #6a826f; } +.debug-section__lock { + min-width: 76rpx; + height: 52rpx; + padding: 0 16rpx; + flex: none; + display: flex; + align-items: center; + justify-content: center; + border-radius: 999rpx; + background: rgba(233, 242, 228, 0.92); + box-shadow: inset 0 0 0 1rpx rgba(22, 48, 32, 0.08); +} + +.debug-section__lock--active { + background: rgba(45, 106, 79, 0.18); + box-shadow: inset 0 0 0 2rpx rgba(45, 106, 79, 0.18); +} + +.debug-section__lock-text { + font-size: 20rpx; + line-height: 1; + font-weight: 700; + color: #45624b; + letter-spacing: 1rpx; +} + +.debug-section__lock--active .debug-section__lock-text { + color: #2d6a4f; +} + .info-panel__row { display: flex; align-items: flex-start; @@ -1593,6 +1646,16 @@ .debug-section .control-row:last-child { margin-bottom: 0; } + +.debug-group-title { + margin-top: 18rpx; + margin-bottom: 8rpx; + font-size: 20rpx; + font-weight: 800; + letter-spacing: 2rpx; + color: #6a826f; + text-transform: uppercase; +} .control-row--triple .control-chip { font-size: 23rpx; } @@ -1624,6 +1687,10 @@ color: #f7fbf2; } +.control-chip--disabled { + opacity: 0.48; +} + diff --git a/project.config.json b/project.config.json index bbcb439..9ead79b 100644 --- a/project.config.json +++ b/project.config.json @@ -13,7 +13,7 @@ }, "coverView": false, "postcss": false, - "minified": false, + "minified": true, "enhance": false, "showShadowRootInWxmlPanel": false, "packNpmRelationList": [],