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

31 KiB
Raw Blame History

移除光栅卡硬件陀螺仪 · 实施计划

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.2simulateTilt 入口 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 之前)插入:

// 手动模拟倾斜:归一化 → 度数 转换系数spec §3.5
const NORMALIZED_TO_DEG = 45
  • Step 2: 在 useLenticularCraftTiltPreview 函数体内(return 语句之前)新增 3 个方法

定位到文件末尾 return { ... } 之前(大约 line 210stopTiltPreview 函数定义之后插入:

	// ====================================================================
	//  手动模拟倾斜入口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 个键:

	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 执行:

// 从 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: 提交

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

<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)临时挂载:

<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: 提交
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 / sliderResettouchend 不再 emit (0,0)

  • Step 1: 添加 showSlider / sliderValue props

frontend/components/lenticular/LenticularCard.vuedefineProps({...})line 54-63末尾追加

	// 新增手动滑块接入spec §3.3
	showSlider:  { type: Boolean, default: false },
	sliderValue: { type: Number,  default: 0.5 },
  • Step 2: 在 defineEmits 中添加 update:sliderValue / sliderReset

找到 defineEmits(如果有)或 <script setup> 顶部,添加:

const emit = defineEmits(['simulate', 'update:sliderValue', 'sliderReset'])

注:如果 LenticularCard.vue 原本没有 defineEmits,需要新建一行。

  • Step 3: 导入并使用 ManualTiltSlider

<script setup> 顶部 import 区添加:

import ManualTiltSlider from './ManualTiltSlider.vue'

在模板中(</view> 结束卡片框架之后),追加:

		<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.vueonFrameTouchEnd 函数line 209-212

function onFrameTouchEnd() {
	if (props.skipBuiltInTouch) return
	// 修改前emit('simulate', 0, 0)  ← 与滑块数据驱动冲突
	// 修改后:触摸结束不再强制发 (0,0),由调用方决定
	// spec §4.1 修复方案)
}
  • Step 5: H5 Chrome 验证

打开任意挂载 <LenticularCard> 的页面H5 dev serverDevTools 验证:

// 1. touchend 后卡片不强制回中
//    操作:拖动卡片到右边 → 松开 → 卡片应保持右边位置

// 2. v-model:sliderValue 双向绑定
//    临时挂载dev<LenticularCard :show-slider="true" v-model:sliderValue="..." />
//    操作:拖动滑块 → 卡片视觉跟随
  • Step 6: 提交
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.vuedefineProps({...})line 43-67末尾追加

  // 新增:手动滑块接入(与 LenticularCard 对齐)
  showSlider:  { type: Boolean, default: false },
  sliderValue: { type: Number,  default: 0.5 },
  • Step 2: 在 defineEmits 中添加 update:sliderValue / sliderReset

修改 const emit = defineEmits([...])line 67

const emit = defineEmits(['simulate', 'update:sliderValue', 'sliderReset', 'ready', 'error', 'fpsUpdate'])
  • Step 3: 导入 ManualTiltSlider 并在模板中渲染

<script setup> 顶部添加:

import ManualTiltSlider from './ManualTiltSlider.vue'

模板中(</view> 结束 holo-frame 之后)添加:

    <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

function onTouchEnd() {
  if (props.skipBuiltInTouch) return
  // 修改前emit('simulate', 0, 0)
  // 修改后:触摸结束不再发 (0,0)
}
  • Step 5: 提交
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.vuedefineProps({...})line 22-28imageSrc 后追加:

	showSlider: { type: Boolean, default: false },
  • Step 2: 透传到 LenticularCard

修改模板line 4-12

		<LenticularCard
			:layers="lenticularLayers"
			:transforms="layerTransforms"
			gyro-source="simulation"
			:approximate-preview="true"
			tilt-hint-text="滑动卡片预览光栅效果"
			:show-slider="showSlider"
			@simulate="simulate"
		/>
  • Step 3: 提交
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 / simulateTiltResetTask 3 产出的卡片 v-model 接口
  • Produces: 移除 scheduleTiltStart/stopTiltPreview 调用;加 show-slider触摸 ↔ 滑块 双向同步(用户最新澄清要求)

关键设计:触摸 ↔ 滑块 双向同步

  • 触摸 x ∈ [-1, +1] → 滑块 value ∈ [0, 1]slider = (x + 1) / 2clamp 到 [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

const {
	layerTransforms,
	simulateTilt,
	simulateTiltFromNormalized,
	simulateTiltReset,
} = useLenticularCraftTiltPreview(lenticularLayers)
  • Step 2: 添加 tiltValue 状态(用于 v-model:sliderValue触摸/滑块 single source of truth

lenticularLayers ref 声明line 102之后添加

const tiltValue = ref(0.5)
  • Step 3: 添加 3 个事件处理函数(触摸/滑块双向同步)

tiltValue ref 声明后,添加 3 个函数:

/**
 * 触摸 → 滑块 + 引擎
 * 把触摸 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

						<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

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: 提交
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: 先确认目录无外部引用

grep -rn "imengyu" frontend/ 2>/dev/null

预期:仅 manifest.jsonline 31、33、35useLenticularStudioTilt.jsline 183、194有引用。前者将在 Task 8 清理,后者将在 Task 9 清理。

  • Step 2: 备份确认(防止误删)
ls -la frontend/nativePlugins/imengyu-UniAndroidGyro/

预期

.DS_Store
android/
ios/
package.json

确认是 imengyu 的陀螺仪插件目录。

  • Step 3: 删除目录
rm -rf frontend/nativePlugins/imengyu-UniAndroidGyro/
  • Step 4: 验证删除
ls frontend/nativePlugins/ 2>/dev/null

预期:为空目录,或目录已被 uniapp 工具链清理。

[ -d frontend/nativePlugins/imengyu-UniAndroidGyro ] && echo "STILL EXISTS" || echo "DELETED OK"

预期DELETED OK

  • Step 5: 提交
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,删除以下整段(注意包含外层逗号):

        "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" : {}
                }
            }
        },
        /* 应用发布信息 */

改为只剩:

        /* 应用发布信息 */
  • Step 2: 删除 BODY_SENSORS 权限line 69

删除这一行:

                    "<uses-permission android:name=\"android.permission.BODY_SENSORS\"/>"

注意末尾的逗号:删除后需确认上一行末尾没有多余的逗号(如果原文件 BODY_SENSORS 已经是最后一行,删除后前一行末尾的逗号也要去掉)。

  • Step 3: 删除 NSMotionUsageDescriptionline 82

删除整行:

                    "NSMotionUsageDescription" : "我们需要访问运动传感器以实现光栅卡倾斜预览功能"

注意逗号:如果前一行没有逗号则不需处理;如果删除后 privacyDescription 块为空,需保留 {}

  • Step 4: 验证 JSON 合法
node -e "JSON.parse(require('fs').readFileSync('frontend/manifest.json', 'utf8'))" && echo "JSON OK"

预期JSON OK

  • Step 5: grep 验证
grep -n "imengyu\|BODY_SENSORS\|NSMotionUsageDescription" frontend/manifest.json

预期0 命中

  • Step 6: 提交
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 处引用

grep -rn "useLenticularStudioTilt" frontend/ 2>/dev/null

预期:仅 useLenticularCraftTiltPreview.js 命中 1 处。

  • Step 2: 确认 useOneEuroFilter 仅 1 处引用
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
import { useLenticularStudioTilt } from '@/composables/useLenticularStudioTilt.js'
  1. 函数体内(约 line 176-185
const { start: startTilt, stop: stopTilt } = useLenticularStudioTilt({
    simulate,
    simulateFromSignedDegrees,
    gyroSourceLabel,
    onTiltDriverFallback: () => {
        lastLayerIdx = -1
        dirSign = 0
        dirOppositeStreak = 0
    },
})

以及相关状态变量(let entryTiltTimer = nullcancelScheduledTiltStart / scheduleTiltStart / stopTiltPreview 三个函数定义)。

最终 return 对象同步清理(去掉 scheduleTiltStart / stopTiltPreview / gyroSourceLabel

	return {
		physics,
		layerTransforms,
		simulate,
		relax,
		lockPreviewStill,
		// 新增:手动模拟入口
		simulateTilt,
		simulateTiltFromNormalized,
		simulateTiltReset,
	}
  • Step 4: 删除 useLenticularStudioTilt.js 和 useOneEuroFilter.js
rm frontend/composables/useLenticularStudioTilt.js frontend/composables/useOneEuroFilter.js
  • Step 5: 验证无残留
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: 提交

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 行:

import { ref, reactive, onMounted, onUnmounted } from 'vue'

改为:

import { ref, reactive } from 'vue'
  • Step 2: 删除传感器相关局部状态

删除 line 26-31

  let accelHandler = null
  let accelSmoothed = 0
  let accelBaselineReady = false
  let accelBaseX = 0
  let accelBaseY = 0
  • Step 3: 删除 startGyro / startDeviceOrientation / stopGyro 函数line 44-96

整段删除:

  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: 验证无残留
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: 提交

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 附录)
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: 移动端构建检查(如有真机)
# 触发 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 天。