topfans/docs/superpowers/plans/2026-07-06-remove-hardware-gyro.md
2026-07-06 21:20:24 +08:00

1014 lines
31 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 移除光栅卡硬件陀螺仪 · 实施计划
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** 彻底移除光栅卡/全息卡对硬件陀螺仪/加速度计的依赖(含 Android 原生插件),改用触摸拖拽 + 手动滑块双输入,简化 ~28KB 硬件传感器代码、消除 iOS/Android 权限摩擦。
**Architecture:**`useLenticularCraftTiltPreview` 暴露 `simulateTilt(dx, dy)`(度)作为唯一倾斜入口;触摸拖拽通过 `simulateTiltFromNormalized` 转换;新增独立组件 `<ManualTiltSlider>` 通过卡片内置 `showSlider` prop 透出v-model:sliderValue 双向绑定。所有硬件相关代码、依赖、权限、原生插件全部删除。
**Tech Stack:** uni-app 3.x + Vue 3 组合式 API + uni-app 原生 `<slider>` 组件H5 Chrome 作为开发期验证平台。
## Global Constraints
- **测试原则**:前端不写 Vitestmemory `share-impl-test-policy.md`),所有验证通过 H5 Chrome 手动 + DevTools console + `console.log` 完成
- **平台目标**:以 app-plusAndroid + iOS为主H5 同样支持H5 验证为开发期主线
- **包大小目标**:删除 ≥28KB 硬件代码 + 1 个原生插件目录
- **提交规范**:每个 Task 一个 commitcommit 末尾追加 `Co-Authored-By: Claude <noreply@anthropic.com>`
- **分支**:当前在 `feat/actvity`(或显示为 `feat/len`**不要** checkout 到 main
- **数值约定**spec §3.5
- 触摸 emit 实际 [-1.1, +1.1],有效 [-1, +1]
- 滑块 value: 0..1,中位 0.5 = 0°
- `simulateTilt(dx, dy)`: -45..+45 度
- 滑块→度: `dx = (v - 0.5) * 90`
- 触摸→度: `dx = x * 45`
- **错误处理**spec §4.2`simulateTilt` 入口 `Number.isFinite()` 校验;`layersRef.length < 2` 兜底保留
- **Phase 5 grep 校验**必须 0 命中`useLenticularStudioTilt` / `useOneEuroFilter` / `imengyu-UniAndroidGyro` / `BODY_SENSORS` / `NSMotionUsageDescription`
---
## File Structure
| 文件 | 操作 | 职责 |
|------|------|------|
| `frontend/composables/useLenticularCraftTiltPreview.js` | | 新增 `simulateTilt*` 入口最终删除 `useLenticularStudioTilt` 引用 |
| `frontend/composables/useLenticularStudioTilt.js` | | 28KB 硬件传感器模块 |
| `frontend/composables/useOneEuroFilter.js` | | 仅被 StudioTilt 使用 |
| `frontend/composables/useHolographicPreview.js` | | DeviceOrientationEvent 路径保留 `simulate(x, y)` |
| `frontend/components/lenticular/ManualTiltSlider.vue` | 新增 | 独立滑块组件 |
| `frontend/components/lenticular/LenticularCard.vue` | | slider 接入 + touchend 不发 (0,0) |
| `frontend/components/lenticular/HolographicCard.vue` | | 同上 |
| `frontend/components/lenticular/CastloveLenticularPreview.vue` | | 透传 show-slider prop |
| `frontend/pages/castlove/lenticular/lenticular-result.vue` | | show-slider + simulate 接收器 + scheduleTiltStart |
| `frontend/nativePlugins/imengyu-UniAndroidGyro/` | | 原生插件目录AAR + iOS Framework |
| `frontend/manifest.json` | | nativePlugins + BODY_SENSORS + NSMotionUsageDescription |
---
## Task 1: 在 useLenticularCraftTiltPreview 新增手动倾斜入口
**Files:**
- Modify: `frontend/composables/useLenticularCraftTiltPreview.js:176-213`
**Interfaces:**
- Consumes: 现有的 `useLenticularStudioTilt`仍保留运行 Task 不动
- Produces: 新增 3 个公共方法 `simulateTilt(dx, dy)` / `simulateTiltFromNormalized(x, y)` / `simulateTiltReset()`
- [ ] **Step 1: 在 useLenticularCraftTiltPreview 顶部添加数值常量**
打开 `frontend/composables/useLenticularCraftTiltPreview.js`在第 12 行附近`FLOW_PHYSICS` 之前插入
```js
// 手动模拟倾斜:归一化 → 度数 转换系数spec §3.5
const NORMALIZED_TO_DEG = 45
```
- [ ] **Step 2: 在 `useLenticularCraftTiltPreview` 函数体内(`return` 语句之前)新增 3 个方法**
定位到文件末尾 `return { ... }` 之前大约 line 210 `stopTiltPreview` 函数定义之后插入
```js
// ====================================================================
// 手动模拟倾斜入口spec §3.1
// ====================================================================
/**
* 滑块/外部入口:直接传度数(-45..+45
* 内部转给 simulateFromSignedDegrees 走现有离散档位逻辑
*/
function simulateTilt(dx, dy) {
if (!Number.isFinite(dx) || !Number.isFinite(dy)) {
console.warn('[CraftTilt] simulateTilt: non-finite input', dx, dy)
return
}
simulateFromSignedDegrees(dx, dy)
}
/**
* 触摸入口:-1..+1 归一化转度数(系数 45
* 由 LenticularCard.vue / HolographicCard.vue 的 @simulate 事件触发
*/
function simulateTiltFromNormalized(x, y) {
const dx = x * NORMALIZED_TO_DEG
const dy = (y != null ? y : 0) * NORMALIZED_TO_DEG
simulateTilt(dx, dy)
}
/**
* 滑块点"复位"时调用:归零 + 清空离散档位状态
*/
function simulateTiltReset() {
lockPreviewStill()
}
```
- [ ] **Step 3: 在 return 对象中暴露这 3 个方法**
修改文件末尾的 `return { ... }` line 215-225添加这 3 个键
```js
return {
physics,
layerTransforms,
simulate,
relax,
gyroSourceLabel,
scheduleTiltStart,
stopTiltPreview,
lockPreviewStill,
// 新增手动模拟入口spec §3.1
simulateTilt,
simulateTiltFromNormalized,
simulateTiltReset,
}
```
- [ ] **Step 4: H5 Chrome 手动验证(在 DevTools console**
启动 H5 dev server具体命令见项目 README打开 `lenticular-result` 页面浏览器 DevTools console 执行
```js
// 从 Vue 组件实例拿 useLenticularCraftTiltPreview 返回的 simulateTilt
//(具体取法:组件实例 proxy 上找,或者在 setup 顶层临时挂到 window 调试)
window.__test_simulateTilt?.(45, 0) // 应触发右边硬切
window.__test_simulateTilt?.(0, 0) // 应触发归中
window.__test_simulateTilt?.(NaN, 0) // 应 console.warn 不崩
```
> 注:如果没有 `window.__test_*` 暴露,可以临时在组件 setup 末尾加 `window.__test_simulateTilt = simulateTilt` 调试,验证完删除。
**预期**
- 前两次调用卡片视觉有响应γ ±1
- 第三次DevTools console 出现 `[CraftTilt] simulateTilt: non-finite input` 警告卡片无变化
- [ ] **Step 5: 提交**
```bash
git add frontend/composables/useLenticularCraftTiltPreview.js
git commit -m "feat(craft-tilt): 新增 simulateTilt 手动模拟入口
- simulateTilt(dx, dy) 单位度,校验 NaN/Infinity
- simulateTiltFromNormalized(x, y) 触摸归一化转度(系数 45
- simulateTiltReset() 滑块复位用
Co-Authored-By: Claude <noreply@anthropic.com>"
```
---
## Task 2: 新增 ManualTiltSlider.vue 独立组件
**Files:**
- Create: `frontend/components/lenticular/ManualTiltSlider.vue`
**Interfaces:**
- Consumes: 独立组件
- Produces: `<ManualTiltSlider value disabled showReset min max step @change @reset>`
- [ ] **Step 1: 创建组件文件,写入完整模板和 script**
新建 `frontend/components/lenticular/ManualTiltSlider.vue`
```vue
<template>
<view v-if="showReset" class="tilt-slider-row">
<view v-if="showReset" class="tilt-slider-reset" @tap="handleReset">
<text class="tilt-slider-reset-icon"></text>
</view>
<slider
class="tilt-slider"
:min="min"
:max="max"
:step="step"
:value="value"
:disabled="disabled"
:activeColor="activeColor"
:backgroundColor="backgroundColor"
:show-value="false"
@change="handleChange"
/>
</view>
</template>
<script setup>
const props = defineProps({
value: { type: Number, default: 0.5, validator: v => v >= 0 && v <= 1 },
disabled: { type: Boolean, default: false },
showReset: { type: Boolean, default: true },
min: { type: Number, default: 0 },
max: { type: Number, default: 1 },
step: { type: Number, default: 0.01 },
activeColor: { type: String, default: '#6E7AFF' },
backgroundColor: { type: String, default: '#E5E7EB' },
})
const emit = defineEmits<{
change: [v: number]
reset: []
}>()
function handleChange(e) {
const v = Number(e.detail.value)
if (!Number.isFinite(v)) {
console.warn('[ManualTiltSlider] non-finite value', e.detail.value)
return
}
if (v < 0 || v > 1) {
console.warn('[ManualTiltSlider] value out of [0,1]', v)
return
}
emit('change', v)
}
function handleReset() {
emit('reset')
}
</script>
<style scoped>
.tilt-slider-row {
display: flex;
align-items: center;
gap: 12rpx;
padding: 8rpx 16rpx;
height: 48rpx;
}
.tilt-slider-reset {
width: 32rpx;
height: 32rpx;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.tilt-slider-reset-icon {
font-size: 28rpx;
color: #6B7280;
}
.tilt-slider {
flex: 1;
min-width: 0;
}
</style>
```
- [ ] **Step 2: 临时挂载到 H5 测试页面验证**
找一个测试用页面 `pages/index/index.vue`临时挂载
```vue
<ManualTiltSlider :value="0.5" @change="v => console.log('change', v)" @reset="console.log('reset')" />
```
H5 Chrome 打开该页面验证
- 拖动滑块 console 打印 `change 0.42` 之类的数值
- 点击 按钮 console 打印 `reset`
- 滑块无法拖到 0..1 范围外uniapp `<slider>` 组件自带 clamp
- [ ] **Step 3: 还原测试挂载**
如果步骤 2 加了临时代码删除或还原
- [ ] **Step 4: 提交**
```bash
git add frontend/components/lenticular/ManualTiltSlider.vue
git commit -m "feat(lenticular): 新增 ManualTiltSlider 独立组件
- uniapp <slider> 包装0..1 双向绑定
- 内置 ↺ 复位按钮
- value validator 拦截 0..1 范围外输入
Co-Authored-By: Claude <noreply@anthropic.com>"
```
---
## Task 3: LenticularCard.vue 加 slider 接入 + 改 touchend 行为
**Files:**
- Modify: `frontend/components/lenticular/LenticularCard.vue:54-63, 209-212`
**Interfaces:**
- Consumes: Task 2 产出的 `<ManualTiltSlider>`
- Produces: 新增 props `showSlider` / `sliderValue`新增 emit `update:sliderValue` / `sliderReset`touchend 不再 emit (0,0)
- [ ] **Step 1: 添加 showSlider / sliderValue props**
`frontend/components/lenticular/LenticularCard.vue` `defineProps({...})` line 54-63末尾追加
```js
// 新增手动滑块接入spec §3.3
showSlider: { type: Boolean, default: false },
sliderValue: { type: Number, default: 0.5 },
```
- [ ] **Step 2: 在 defineEmits 中添加 update:sliderValue / sliderReset**
找到 `defineEmits`如果有 `<script setup>` 顶部添加
```js
const emit = defineEmits(['simulate', 'update:sliderValue', 'sliderReset'])
```
> 注:如果 LenticularCard.vue 原本没有 `defineEmits`,需要新建一行。
- [ ] **Step 3: 导入并使用 ManualTiltSlider**
`<script setup>` 顶部 `import` 区添加
```js
import ManualTiltSlider from './ManualTiltSlider.vue'
```
在模板中`</view>` 结束卡片框架之后),追加:
```vue
<ManualTiltSlider
v-if="showSlider"
:value="sliderValue"
@change="v => emit('update:sliderValue', v)"
@reset="emit('sliderReset')"
/>
```
- [ ] **Step 4: 修复 touchend emit (0,0) bug**
修改 `frontend/components/lenticular/LenticularCard.vue``onFrameTouchEnd` 函数line 209-212
```js
function onFrameTouchEnd() {
if (props.skipBuiltInTouch) return
// 修改前emit('simulate', 0, 0) ← 与滑块数据驱动冲突
// 修改后:触摸结束不再强制发 (0,0),由调用方决定
// spec §4.1 修复方案)
}
```
- [ ] **Step 5: H5 Chrome 验证**
打开任意挂载 `<LenticularCard>` 的页面H5 dev serverDevTools 验证:
```js
// 1. touchend 后卡片不强制回中
// 操作:拖动卡片到右边 → 松开 → 卡片应保持右边位置
// 2. v-model:sliderValue 双向绑定
// 临时挂载dev<LenticularCard :show-slider="true" v-model:sliderValue="..." />
// 操作:拖动滑块 → 卡片视觉跟随
```
- [ ] **Step 6: 提交**
```bash
git add frontend/components/lenticular/LenticularCard.vue
git commit -m "feat(lenticular-card): 接入 ManualTiltSlider + 修 touchend 行为
- 新增 showSlider / sliderValue props
- 新增 update:sliderValue / sliderReset emitsv-model 配套)
- 触摸结束不再 emit (0,0),与滑块数据驱动相容
Co-Authored-By: Claude <noreply@anthropic.com>"
```
---
## Task 4: HolographicCard.vue 同步 Task 3 改动
**Files:**
- Modify: `frontend/components/lenticular/HolographicCard.vue:43-67, 129-132`
**Interfaces:** 与 Task 3 相同的 prop / emit 接口
- [ ] **Step 1: 添加 props**
`HolographicCard.vue``defineProps({...})`line 43-67末尾追加
```js
// 新增:手动滑块接入(与 LenticularCard 对齐)
showSlider: { type: Boolean, default: false },
sliderValue: { type: Number, default: 0.5 },
```
- [ ] **Step 2: 在 defineEmits 中添加 update:sliderValue / sliderReset**
修改 `const emit = defineEmits([...])`line 67
```js
const emit = defineEmits(['simulate', 'update:sliderValue', 'sliderReset', 'ready', 'error', 'fpsUpdate'])
```
- [ ] **Step 3: 导入 ManualTiltSlider 并在模板中渲染**
`<script setup>` 顶部添加:
```js
import ManualTiltSlider from './ManualTiltSlider.vue'
```
模板中(`</view>` 结束 `holo-frame` 之后)添加:
```vue
<ManualTiltSlider
v-if="showSlider"
:value="sliderValue"
@change="v => emit('update:sliderValue', v)"
@reset="emit('sliderReset')"
/>
```
- [ ] **Step 4: 修复 touchend emit (0,0) bug**
修改 `onTouchEnd` 函数line 129-132
```js
function onTouchEnd() {
if (props.skipBuiltInTouch) return
// 修改前emit('simulate', 0, 0)
// 修改后:触摸结束不再发 (0,0)
}
```
- [ ] **Step 5: 提交**
```bash
git add frontend/components/lenticular/HolographicCard.vue
git commit -m "feat(holographic-card): 接入 ManualTiltSlider + 修 touchend 行为
- 与 LenticularCard 对齐的 showSlider / sliderValue / v-model 接口
- 触摸结束不再 emit (0,0)
Co-Authored-By: Claude <noreply@anthropic.com>"
```
---
## Task 5: CastloveLenticularPreview.vue 加 show-slider prop 透传
**Files:**
- Modify: `frontend/components/lenticular/CastloveLenticularPreview.vue:4-14`
**Interfaces:**
- Consumes: Task 3 产出的 `<LenticularCard :show-slider :slider-value>`
- Produces: 透传 prop 让调用方控制
- [ ] **Step 1: 添加 showSlider prop**
修改 `CastloveLenticularPreview.vue``defineProps({...})`line 22-28`imageSrc` 后追加:
```js
showSlider: { type: Boolean, default: false },
```
- [ ] **Step 2: 透传到 LenticularCard**
修改模板line 4-12
```vue
<LenticularCard
:layers="lenticularLayers"
:transforms="layerTransforms"
gyro-source="simulation"
:approximate-preview="true"
tilt-hint-text="滑动卡片预览光栅效果"
:show-slider="showSlider"
@simulate="simulate"
/>
```
- [ ] **Step 3: 提交**
```bash
git add frontend/components/lenticular/CastloveLenticularPreview.vue
git commit -m "feat(castlove-preview): 透传 showSlider prop
Co-Authored-By: Claude <noreply@anthropic.com>"
```
---
## Task 6: lenticular-result.vue 切换到手动模式
**Files:**
- Modify: `frontend/pages/castlove/lenticular/lenticular-result.vue:31-34, 66, 102-109`
**Interfaces:**
- Consumes: Task 1 产出的 `simulateTilt / simulateTiltFromNormalized / simulateTiltReset`Task 3 产出的卡片 v-model 接口
- Produces: 移除 `scheduleTiltStart/stopTiltPreview` 调用;加 show-slider**触摸 ↔ 滑块 双向同步**(用户最新澄清要求)
**关键设计:触摸 ↔ 滑块 双向同步**
- 触摸 x ∈ [-1, +1] → 滑块 value ∈ [0, 1]`slider = (x + 1) / 2`clamp 到 [0,1]
- 滑块 value ∈ [0, 1] → 触摸角度:滑块拖动时 `simulateTilt((v - 0.5) * 90, 0)`,让卡片视觉跟到那个角度
- 双向都通过 `tiltValue` 这个 ref 作为 single source of truth
- [ ] **Step 1: 从 useLenticularCraftTiltPreview 解构 simulateTilt / simulateTiltFromNormalized / simulateTiltReset**
修改 `useLenticularCraftTiltPreview` 解构line 102-109
```js
const {
layerTransforms,
simulateTilt,
simulateTiltFromNormalized,
simulateTiltReset,
} = useLenticularCraftTiltPreview(lenticularLayers)
```
- [ ] **Step 2: 添加 tiltValue 状态(用于 v-model:sliderValue触摸/滑块 single source of truth**
`lenticularLayers` ref 声明line 102之后添加
```js
const tiltValue = ref(0.5)
```
- [ ] **Step 3: 添加 3 个事件处理函数(触摸/滑块双向同步)**
`tiltValue` ref 声明后,添加 3 个函数:
```js
/**
* 触摸 → 滑块 + 引擎
* 把触摸 x 投影到滑块 0..1(用户最新要求:手滑时滑块也动)
*/
function onTouchSimulate(x, y) {
const sliderPos = Math.max(0, Math.min(1, (x + 1) / 2))
tiltValue.value = sliderPos
simulateTiltFromNormalized(x, y)
}
/**
* 滑块 → 引擎tiltValue 已被 v-model 自动更新,这里只驱动引擎)
* 用户最新要求:滑块滑动,相关的手滑/卡片也到那个角度
*/
function onSliderChange(v) {
const dx = (v - 0.5) * 90
simulateTilt(dx, 0)
}
/**
* 复位按钮:滑块回 0.5 + 引擎回中
*/
function onSliderReset() {
tiltValue.value = 0.5
simulateTiltReset()
}
```
- [ ] **Step 4: 改 LenticularCard 标签(接 3 个新事件处理)**
修改 `<LenticularCard>` 标签line 31-34
```vue
<LenticularCard v-if="lenticularLayers.length > 0" class="craft-lenticular-card"
:layers="lenticularLayers" :transforms="layerTransforms"
:show-slider="true" v-model:slider-value="tiltValue"
tilt-hint-text="晃动或滑动查看" :shimmer-mid-opacity="0.16"
@simulate="onTouchSimulate"
@update:slider-value="onSliderChange"
@slider-reset="onSliderReset" />
```
注意改动点:
-`:skip-built-in-touch="true"`(让触摸事件生效)
-`:show-slider="true"`
-`v-model:slider-value="tiltValue"`(双向)
- `@simulate` 改为 `onTouchSimulate`(同步触摸 → 滑块 + 引擎)
-`@update:slider-value="onSliderChange"`(同步滑块 → 引擎)
-`@slider-reset="onSliderReset"`(复位)
-`tilt-hint-text` 文案
- [ ] **Step 5: 删除 scheduleTiltStart / stopTiltPreview / lockPreviewStill 引用**
搜索整个 `lenticular-result.vue`
```bash
grep -n "scheduleTiltStart\|stopTiltPreview\|gyroSourceLabel" frontend/pages/castlove/lenticular/lenticular-result.vue
```
预期命中 3-4 处line 107、713、769、774、778 等)。逐处删除:
- 解构中的 `gyroSourceLabel / scheduleTiltStart / stopTiltPreview` 已通过 Step 1 覆盖
- 函数调用处的 `scheduleTiltStart();``stopTiltPreview();` 直接删除
- 模板中如有 `{{ gyroSourceLabel }}` 引用也删除
- [ ] **Step 6: H5 Chrome 端到端验证(重点验证双向同步)**
H5 dev server 打开 `lenticular-result` 页面:
1. 卡片正常显示
2. **手滑 → 滑块跟动**:拖动卡片到最右 → 滑块应自动滑到右边(接近 1.0
3. **手滑 → 滑块跟动(中间)**:拖动卡片到中心 → 滑块应回到 0.5
4. **手滑 → 卡片**:拖动卡片到最左 → 滑块到 0、卡片到最左
5. **滑块 → 卡片**:拖动滑块到 0.7 → 卡片光栅应到对应位置
6. **松开不强制回中**:手指松开后滑块保持在拖到的位置(不回 0.5
7. **复位**:点击 ↺ → 滑块回 0.5,卡片回中间
8. DevTools console 无 error
- [ ] **Step 7: 提交**
```bash
git add frontend/pages/castlove/lenticular/lenticular-result.vue
git commit -m "feat(lenticular-result): 切换为手动倾斜模式 + 触摸滑块双向同步
- @simulate → onTouchSimulate触摸 x 投影到滑块 + 驱动引擎
- @update:slider-value → onSliderChange滑块拖动驱动引擎
- @slider-reset → onSliderReset复位滑块+引擎
- v-model:slider-value 双向
- 删 3 处 scheduleTiltStart / stopTiltPreview / gyroSourceLabel 引用
- 触摸结束不强制回中
Co-Authored-By: Claude <noreply@anthropic.com>"
```
---
## Task 7: 删原生插件目录 imengyu-UniAndroidGyro
**Files:**
- Delete: `frontend/nativePlugins/imengyu-UniAndroidGyro/`(含 android/ ios/ package.json .DS_Store
- [ ] **Step 1: 先确认目录无外部引用**
```bash
grep -rn "imengyu" frontend/ 2>/dev/null
```
**预期**:仅 `manifest.json`line 31、33、35`useLenticularStudioTilt.js`line 183、194有引用。前者将在 Task 8 清理,后者将在 Task 9 清理。
- [ ] **Step 2: 备份确认(防止误删)**
```bash
ls -la frontend/nativePlugins/imengyu-UniAndroidGyro/
```
**预期**
```
.DS_Store
android/
ios/
package.json
```
确认是 imengyu 的陀螺仪插件目录。
- [ ] **Step 3: 删除目录**
```bash
rm -rf frontend/nativePlugins/imengyu-UniAndroidGyro/
```
- [ ] **Step 4: 验证删除**
```bash
ls frontend/nativePlugins/ 2>/dev/null
```
**预期**:为空目录,或目录已被 `uniapp` 工具链清理。
```bash
[ -d frontend/nativePlugins/imengyu-UniAndroidGyro ] && echo "STILL EXISTS" || echo "DELETED OK"
```
**预期**`DELETED OK`
- [ ] **Step 5: 提交**
```bash
git add -A frontend/nativePlugins/
git commit -m "chore(native-plugin): 移除 imengyu-UniAndroidGyro 陀螺仪插件
- 28KB AAR + iOS Framework + package.json 全部删除
- 与 useLenticularStudioTilt.js 同步Task 9 跟进)
Co-Authored-By: Claude <noreply@anthropic.com>"
```
---
## Task 8: 改 manifest.json删 3 段)
**Files:**
- Modify: `frontend/manifest.json:30-44, 69, 82`
- [ ] **Step 1: 删除 nativePlugins.imengyu-UniAndroidGyro 段line 30-44**
打开 `frontend/manifest.json`,删除以下整段(注意包含外层逗号):
```json
"nativePlugins" : {
"imengyu-UniAndroidGyro" : {
"__plugin_info__" : {
"name" : "imengyu-UniAndroidGyro",
"description" : "APP端陀螺仪数据采集",
"platforms" : "Android,iOS",
"url" : "",
"android_package_name" : "",
"ios_bundle_id" : "",
"isCloud" : false,
"bought" : -1,
"pid" : "",
"parameters" : {}
}
}
},
/* 应用发布信息 */
```
改为只剩:
```json
/* 应用发布信息 */
```
- [ ] **Step 2: 删除 BODY_SENSORS 权限line 69**
删除这一行:
```xml
"<uses-permission android:name=\"android.permission.BODY_SENSORS\"/>"
```
注意末尾的逗号:删除后需确认上一行末尾**没有**多余的逗号(如果原文件 BODY_SENSORS 已经是最后一行,删除后前一行末尾的逗号也要去掉)。
- [ ] **Step 3: 删除 NSMotionUsageDescriptionline 82**
删除整行:
```json
"NSMotionUsageDescription" : "我们需要访问运动传感器以实现光栅卡倾斜预览功能"
```
注意逗号:如果前一行没有逗号则不需处理;如果删除后 `privacyDescription` 块为空,需保留 `{}`
- [ ] **Step 4: 验证 JSON 合法**
```bash
node -e "JSON.parse(require('fs').readFileSync('frontend/manifest.json', 'utf8'))" && echo "JSON OK"
```
**预期**`JSON OK`
- [ ] **Step 5: grep 验证**
```bash
grep -n "imengyu\|BODY_SENSORS\|NSMotionUsageDescription" frontend/manifest.json
```
**预期**0 命中
- [ ] **Step 6: 提交**
```bash
git add frontend/manifest.json
git commit -m "chore(manifest): 移除 imengyu 插件注册与传感器权限
- 删 nativePlugins.imengyu-UniAndroidGyro 段30-44 行)
- 删 BODY_SENSORS 权限69 行)
- 删 NSMotionUsageDescription 隐私描述82 行)
Co-Authored-By: Claude <noreply@anthropic.com>"
```
---
## Task 9: 删 useLenticularStudioTilt.js + useOneEuroFilter.js + 清理 CraftTiltPreview 引用
**Files:**
- Delete: `frontend/composables/useLenticularStudioTilt.js`
- Delete: `frontend/composables/useOneEuroFilter.js`
- Modify: `frontend/composables/useLenticularCraftTiltPreview.js:12-13, 176-185, 215-225`
**Interfaces:**
- Consumes: Task 1 留下的 simulateTilt* 入口
- Produces: 不再依赖硬件传感器路径
- [ ] **Step 1: 确认 useLenticularStudioTilt 仅 1 处引用**
```bash
grep -rn "useLenticularStudioTilt" frontend/ 2>/dev/null
```
**预期**:仅 `useLenticularCraftTiltPreview.js` 命中 1 处。
- [ ] **Step 2: 确认 useOneEuroFilter 仅 1 处引用**
```bash
grep -rn "useOneEuroFilter" frontend/ 2>/dev/null
```
**预期**:仅 `useLenticularStudioTilt.js` 命中 1 处(即将删除,所以最终 0 命中)。
- [ ] **Step 3: 从 useLenticularCraftTiltPreview 移除 useLenticularStudioTilt 引用**
打开 `frontend/composables/useLenticularCraftTiltPreview.js`,删除以下 2 处:
1. **顶部 import**(约 line 12-13
```js
import { useLenticularStudioTilt } from '@/composables/useLenticularStudioTilt.js'
```
2. **函数体内**(约 line 176-185
```js
const { start: startTilt, stop: stopTilt } = useLenticularStudioTilt({
simulate,
simulateFromSignedDegrees,
gyroSourceLabel,
onTiltDriverFallback: () => {
lastLayerIdx = -1
dirSign = 0
dirOppositeStreak = 0
},
})
```
以及相关状态变量(`let entryTiltTimer = null` 和 `cancelScheduledTiltStart / scheduleTiltStart / stopTiltPreview` 三个函数定义)。
最终 `return` 对象同步清理(去掉 `scheduleTiltStart / stopTiltPreview / gyroSourceLabel`
```js
return {
physics,
layerTransforms,
simulate,
relax,
lockPreviewStill,
// 新增:手动模拟入口
simulateTilt,
simulateTiltFromNormalized,
simulateTiltReset,
}
```
- [ ] **Step 4: 删除 useLenticularStudioTilt.js 和 useOneEuroFilter.js**
```bash
rm frontend/composables/useLenticularStudioTilt.js frontend/composables/useOneEuroFilter.js
```
- [ ] **Step 5: 验证无残留**
```bash
grep -rn "useLenticularStudioTilt\|useOneEuroFilter" frontend/ 2>/dev/null
```
**预期**0 命中
- [ ] **Step 6: H5 Chrome 验证 lenticular-result 仍正常**
打开 `lenticular-result` 页面,验证:
- 卡片正常显示
- 触摸 + 滑块都工作
- DevTools console 无 `useLenticularStudioTilt is not defined` 错误
- [ ] **Step 7: 提交**
```bash
git add frontend/composables/
git commit -m "refactor(craft-tilt): 删除硬件传感器模块
- 删 useLenticularStudioTilt.js (28KB, 894 行)
- 删 useOneEuroFilter.js (75 行)
- useLenticularCraftTiltPreview 移除对二者的引用
- 保留手动模拟入口 simulateTilt / simulateTiltFromNormalized / simulateTiltReset
Co-Authored-By: Claude <noreply@anthropic.com>"
```
---
## Task 10: 清理 useHolographicPreview.js DeviceOrientationEvent 路径
**Files:**
- Modify: `frontend/composables/useHolographicPreview.js:7, 26-31, 44-96, 102-105`
**Interfaces:**
- Consumes: 现有 `simulate(x, y)`
- Produces: 删除所有传感器路径,`simulate(x, y)` 成为唯一驱动入口
- [ ] **Step 1: 删除 import 中的 onUnmounted如果不再使用**
打开 `frontend/composables/useHolographicPreview.js`
第 7 行:
```js
import { ref, reactive, onMounted, onUnmounted } from 'vue'
```
改为:
```js
import { ref, reactive } from 'vue'
```
- [ ] **Step 2: 删除传感器相关局部状态**
删除 line 26-31
```js
let accelHandler = null
let accelSmoothed = 0
let accelBaselineReady = false
let accelBaseX = 0
let accelBaseY = 0
```
- [ ] **Step 3: 删除 startGyro / startDeviceOrientation / stopGyro 函数line 44-96**
整段删除:
```js
function startGyro() { ... }
function startDeviceOrientation() { ... }
function stopGyro() { ... }
```
- [ ] **Step 4: 简化 onMounted 钩子(如果保留)**
如果 onMounted 不再需要任何操作,删除整个 `onMounted(() => { ... })`line 102-105。否则保留空函数体。
- [ ] **Step 5: 清理 onUnmounted**
删除 `onUnmounted(() => { stopGyro() })`line 105
- [ ] **Step 6: 从 return 对象中删除 startGyro / stopGyro / gyroSource**
修改末尾 `return { ... }`line 107-113删除 `gyroSource, startGyro, stopGyro` 三项。保留 `physics, viewAngle, isWebGLReady, hasWebGLError, fps, simulate, relax, onWebGLReady, onWebGLError, onFPSUpdate, detectPerformanceTier, HOLO_PERFORMANCE_PRESETS`
- [ ] **Step 7: 验证无残留**
```bash
grep -n "DeviceOrientation\|requestPermission\|accelHandler\|startGyro\|stopGyro\|gyroSource" frontend/composables/useHolographicPreview.js
```
**预期**0 命中
- [ ] **Step 8: H5 Chrome 验证全息卡**
如有任何页面挂载 `<HolographicCard>`grep 验证H5 dev server 打开该页面:
- 卡片正常显示
- 触摸拖拽有效
- DevTools console 无 `DeviceOrientationEvent is not defined` 之类的错误
- [ ] **Step 9: 提交**
```bash
git add frontend/composables/useHolographicPreview.js
git commit -m "refactor(holographic): 移除 DeviceOrientationEvent 路径
- 删 startGyro / startDeviceOrientation / stopGyro
- 删 accelHandler / accelBaselineReady 等基线追踪状态
- simulate(x, y) 成为唯一驱动入口
- 保留 onMounted / onUnmounted 钩子(如果外部有需要可保留,否则删)
Co-Authored-By: Claude <noreply@anthropic.com>"
```
---
## Task 11: 终验 — grep 校验清单 + 验收清单
**Files:** 无(验证性任务)
- [ ] **Step 1: 全量 grep 校验spec 附录)**
```bash
grep -rn "useLenticularStudioTilt" frontend/ 2>/dev/null
grep -rn "useOneEuroFilter" frontend/ 2>/dev/null
grep -rn "imengyu-UniAndroidGyro" frontend/ 2>/dev/null
grep -rn "imengyu" frontend/ 2>/dev/null
grep -rn "BODY_SENSORS" frontend/ 2>/dev/null
grep -rn "NSMotionUsageDescription" frontend/ 2>/dev/null
```
**预期****全部 0 命中**
- [ ] **Step 2: H5 Chrome 完整验收清单**
启动 H5 dev server打开 `lenticular-result` 页面,按 spec §4.3 验收清单逐项过:
1. 打开页面,卡片显示正常
2. **触摸拖拽**:手指在卡片上左右拖动,卡片光栅效果跟随
3. **松开不回中**:触摸结束卡片不强制回中(验证 4.1 修复)
4. **滑块拖动**:拖动滑块 0 → 1卡片光栅从最左切到最右
5. **复位按钮**:点击 ↺,滑块回 0.5,卡片回中间
6. **组合**:拖动滑块到 0.3,然后触摸卡片到最右 → 触摸胜出
7. `lenticular-create` / `lenticular-thinking` 页面无回归(无报错)
- [ ] **Step 3: 移动端构建检查(如有真机)**
```bash
# 触发 HBuilderX 重新打包 App
# 真机扫描:
adb logcat | grep -iE "sensors|gyroscope|accelerometer" | head -20
# 预期:无 SENSOR_SERVICE / BODY_SENSORS 申请日志
```
iOS 真机:扫描 `console.app` 或 Xcode device log**不应**出现 `NSMotionUsageDescription` 弹窗。
- [ ] **Step 4: 提交验收报告(如需要)**
验收不通过则 revert 对应 Task通过则无需新 commit结束于前一个 Task。
---
## 执行建议
**Task 1 → 2 → 3 → 4 → 5 → 6 → 7 → 8 → 9 → 10 → 11** 顺序执行。前 6 个 Task 是新增/修改(每步可独立 revertTask 7-10 是清理(不可逆但可整体 revertTask 11 是终验。
预计开发 2-3 小时QA 1 天。