@ray-js/ipc-player-integration 0.0.50-beta.3 → 0.0.50-beta.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/widgets/zoomTurntable/constants.d.ts +42 -0
- package/lib/widgets/zoomTurntable/constants.js +50 -0
- package/lib/widgets/zoomTurntable/lensModel.d.ts +10 -2
- package/lib/widgets/zoomTurntable/lensModel.js +24 -7
- package/lib/widgets/zoomTurntable/lensModel.test.js +20 -8
- package/lib/widgets/zoomTurntable/useLensZoom.js +291 -25
- package/package.json +1 -1
|
@@ -57,6 +57,48 @@ export declare const QUERY_LENS_TIMEOUT = 3000;
|
|
|
57
57
|
* 待 APP 提供 zoomFree 的「手势是否结束」参数后可改为由该参数触发。
|
|
58
58
|
*/
|
|
59
59
|
export declare const GESTURE_SWITCH_DEBOUNCE = 500;
|
|
60
|
+
/**
|
|
61
|
+
* 切镜头到位后,补下发目标倍率(d > 1)的延时(ms)。
|
|
62
|
+
*
|
|
63
|
+
* 新镜头的流刚起来,立刻打 scaleMultiple 有被原生层吞掉的风险;这段延时里的中间态回读
|
|
64
|
+
* 由 settleApplyStore 挡住,读数不会先闪回镜头正倍数。归正倍数(d = 1)不走延时,与旧行为一致。
|
|
65
|
+
*/
|
|
66
|
+
export declare const POST_SWITCH_APPLY_DELAY = 200;
|
|
67
|
+
/**
|
|
68
|
+
* 补差值下发后,等回读追上目标 d 的最长时间(ms)。
|
|
69
|
+
* 超时解除中间态屏蔽 —— 原生层没按目标值回读时(量化、失败),读数不能被永久冻住。
|
|
70
|
+
*/
|
|
71
|
+
export declare const SETTLE_APPLY_TIMEOUT = 3000;
|
|
72
|
+
/**
|
|
73
|
+
* 触底判定:连续多少帧 d ≈ 1 才认为「用户还在继续往下缩」。
|
|
74
|
+
*
|
|
75
|
+
* 取 2:第一帧是「正常缩到该镜头的光学正倍数」,从第二帧起 d 已经夹在协议下限动不了,
|
|
76
|
+
* 却仍有回读推上来,说明手指还在往里收。取 1 会把正常的「缩到正倍数」误判成越界。
|
|
77
|
+
*/
|
|
78
|
+
export declare const FLOOR_HIT_COUNT = 2;
|
|
79
|
+
/** 触底计数的有效窗口(ms):两帧间隔超过它就不算连续,计数重置 */
|
|
80
|
+
export declare const FLOOR_HIT_WINDOW = 1200;
|
|
81
|
+
/**
|
|
82
|
+
* 切镜头收尾后的触底静默期(ms)。
|
|
83
|
+
* 收尾自身的 emit(setScaleMultipleEvent, 1.0) 会引发一到两帧 d = 1 回读,
|
|
84
|
+
* 不挡掉就会被算进触底计数,刚切上去又被切回来。
|
|
85
|
+
*/
|
|
86
|
+
export declare const FLOOR_HIT_SETTLE_GUARD = 800;
|
|
87
|
+
/** 相邻两次「触底自动下切镜头」的冷却(ms):一次长手势不该连着切两级 */
|
|
88
|
+
export declare const DOWN_SWITCH_COOLDOWN = 800;
|
|
89
|
+
/**
|
|
90
|
+
* 同一次自动切镜头的跨实例去重窗口(ms)。
|
|
91
|
+
* Entry / Dock 各跑一份 useLensZoom,都订阅 zoomLevelChange,会同时判出同一次切换 ——
|
|
92
|
+
* 不去重就是两发 switchChannel。
|
|
93
|
+
*/
|
|
94
|
+
export declare const SWITCH_DEDUPE_WINDOW = 300;
|
|
95
|
+
/**
|
|
96
|
+
* 转盘拖动跨镜头临界后,手停住多久(ms)就地切镜头。
|
|
97
|
+
*
|
|
98
|
+
* 逐帧切不行(switchChannel 是 P2P,几百毫秒级,会把通道打爆),只等松手也不行
|
|
99
|
+
* (拖过界的一路上画面是「冻」的)。折中:过界后手停住这么久就切,画面跟着拖动走。
|
|
100
|
+
*/
|
|
101
|
+
export declare const DRAG_CROSS_SWITCH_DELAY = 600;
|
|
60
102
|
/** dock 层级,需高于四角容器的 z-index: 100 */
|
|
61
103
|
export declare const ZOOM_DOCK_Z_INDEX = 120;
|
|
62
104
|
/**
|
|
@@ -72,6 +72,56 @@ export const QUERY_LENS_TIMEOUT = 3000;
|
|
|
72
72
|
*/
|
|
73
73
|
export const GESTURE_SWITCH_DEBOUNCE = 500;
|
|
74
74
|
|
|
75
|
+
/**
|
|
76
|
+
* 切镜头到位后,补下发目标倍率(d > 1)的延时(ms)。
|
|
77
|
+
*
|
|
78
|
+
* 新镜头的流刚起来,立刻打 scaleMultiple 有被原生层吞掉的风险;这段延时里的中间态回读
|
|
79
|
+
* 由 settleApplyStore 挡住,读数不会先闪回镜头正倍数。归正倍数(d = 1)不走延时,与旧行为一致。
|
|
80
|
+
*/
|
|
81
|
+
export const POST_SWITCH_APPLY_DELAY = 200;
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* 补差值下发后,等回读追上目标 d 的最长时间(ms)。
|
|
85
|
+
* 超时解除中间态屏蔽 —— 原生层没按目标值回读时(量化、失败),读数不能被永久冻住。
|
|
86
|
+
*/
|
|
87
|
+
export const SETTLE_APPLY_TIMEOUT = 3000;
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* 触底判定:连续多少帧 d ≈ 1 才认为「用户还在继续往下缩」。
|
|
91
|
+
*
|
|
92
|
+
* 取 2:第一帧是「正常缩到该镜头的光学正倍数」,从第二帧起 d 已经夹在协议下限动不了,
|
|
93
|
+
* 却仍有回读推上来,说明手指还在往里收。取 1 会把正常的「缩到正倍数」误判成越界。
|
|
94
|
+
*/
|
|
95
|
+
export const FLOOR_HIT_COUNT = 2;
|
|
96
|
+
|
|
97
|
+
/** 触底计数的有效窗口(ms):两帧间隔超过它就不算连续,计数重置 */
|
|
98
|
+
export const FLOOR_HIT_WINDOW = 1200;
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* 切镜头收尾后的触底静默期(ms)。
|
|
102
|
+
* 收尾自身的 emit(setScaleMultipleEvent, 1.0) 会引发一到两帧 d = 1 回读,
|
|
103
|
+
* 不挡掉就会被算进触底计数,刚切上去又被切回来。
|
|
104
|
+
*/
|
|
105
|
+
export const FLOOR_HIT_SETTLE_GUARD = 800;
|
|
106
|
+
|
|
107
|
+
/** 相邻两次「触底自动下切镜头」的冷却(ms):一次长手势不该连着切两级 */
|
|
108
|
+
export const DOWN_SWITCH_COOLDOWN = 800;
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* 同一次自动切镜头的跨实例去重窗口(ms)。
|
|
112
|
+
* Entry / Dock 各跑一份 useLensZoom,都订阅 zoomLevelChange,会同时判出同一次切换 ——
|
|
113
|
+
* 不去重就是两发 switchChannel。
|
|
114
|
+
*/
|
|
115
|
+
export const SWITCH_DEDUPE_WINDOW = 300;
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* 转盘拖动跨镜头临界后,手停住多久(ms)就地切镜头。
|
|
119
|
+
*
|
|
120
|
+
* 逐帧切不行(switchChannel 是 P2P,几百毫秒级,会把通道打爆),只等松手也不行
|
|
121
|
+
* (拖过界的一路上画面是「冻」的)。折中:过界后手停住这么久就切,画面跟着拖动走。
|
|
122
|
+
*/
|
|
123
|
+
export const DRAG_CROSS_SWITCH_DELAY = 600;
|
|
124
|
+
|
|
75
125
|
/** dock 层级,需高于四角容器的 z-index: 100 */
|
|
76
126
|
export const ZOOM_DOCK_Z_INDEX = 120;
|
|
77
127
|
|
|
@@ -33,8 +33,16 @@ export declare const toleranceOf: (skill: ZoomSkill, index: number) => number;
|
|
|
33
33
|
export declare const DEFAULT_STEP = 0.1;
|
|
34
34
|
/**
|
|
35
35
|
* 画面倍率 → 目标镜头下标。
|
|
36
|
-
*
|
|
37
|
-
*
|
|
36
|
+
*
|
|
37
|
+
* **严格命中优先**:半开区间 [lo, hi),最后一个镜头取闭区间 —— 2.9 判短焦、3.0 判长焦。
|
|
38
|
+
* 临界点本身仍归大镜头(3.0 是长焦的 basicRatio),但临界点**以下**不再被吸上去。
|
|
39
|
+
*
|
|
40
|
+
* 曾用「双向 ±step 容差 + 同时命中取大镜头」,那会让 [2.9, 3.0) 整条带子粘在长焦上:
|
|
41
|
+
* 转盘拖到 2.9 松手时 matchLens 判回长焦、不切镜头,而长焦画面最小只能到 3.0,
|
|
42
|
+
* 于是读数显示 2.9、画面停在 3.0,两者对不上。触底下切的目标值正好落在这条带子里,
|
|
43
|
+
* 所以这个吸附必须去掉。
|
|
44
|
+
*
|
|
45
|
+
* 容差退化为兜底:只在严格命中不到时用(浮点毛刺、越界值),此时才「同时命中取大镜头」。
|
|
38
46
|
*/
|
|
39
47
|
export declare const matchLens: (skill: ZoomSkill, curM: number) => number;
|
|
40
48
|
/** 由镜头 id 反查下标 */
|
|
@@ -68,20 +68,37 @@ export const DEFAULT_STEP = 0.1;
|
|
|
68
68
|
|
|
69
69
|
/**
|
|
70
70
|
* 画面倍率 → 目标镜头下标。
|
|
71
|
-
*
|
|
72
|
-
*
|
|
71
|
+
*
|
|
72
|
+
* **严格命中优先**:半开区间 [lo, hi),最后一个镜头取闭区间 —— 2.9 判短焦、3.0 判长焦。
|
|
73
|
+
* 临界点本身仍归大镜头(3.0 是长焦的 basicRatio),但临界点**以下**不再被吸上去。
|
|
74
|
+
*
|
|
75
|
+
* 曾用「双向 ±step 容差 + 同时命中取大镜头」,那会让 [2.9, 3.0) 整条带子粘在长焦上:
|
|
76
|
+
* 转盘拖到 2.9 松手时 matchLens 判回长焦、不切镜头,而长焦画面最小只能到 3.0,
|
|
77
|
+
* 于是读数显示 2.9、画面停在 3.0,两者对不上。触底下切的目标值正好落在这条带子里,
|
|
78
|
+
* 所以这个吸附必须去掉。
|
|
79
|
+
*
|
|
80
|
+
* 容差退化为兜底:只在严格命中不到时用(浮点毛刺、越界值),此时才「同时命中取大镜头」。
|
|
73
81
|
*/
|
|
74
82
|
export const matchLens = (skill, curM) => {
|
|
75
|
-
let hit = 0;
|
|
76
83
|
const count = lensCount(skill);
|
|
84
|
+
if (count <= 0) return 0;
|
|
85
|
+
for (let i = 0; i < count; i++) {
|
|
86
|
+
const [lo, hi] = lensRange(skill, i);
|
|
87
|
+
const isLast = i === count - 1;
|
|
88
|
+
if (curM >= lo && (isLast ? curM <= hi : curM < hi)) return i;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// 严格命中不到:用各镜头自己的 step 作容差吸附,同时命中取大镜头
|
|
92
|
+
let hit = -1;
|
|
77
93
|
for (let i = 0; i < count; i++) {
|
|
78
94
|
const [lo, hi] = lensRange(skill, i);
|
|
79
95
|
const tolerance = toleranceOf(skill, i);
|
|
80
|
-
if (curM >= lo - tolerance && curM <= hi + tolerance)
|
|
81
|
-
hit = i;
|
|
82
|
-
}
|
|
96
|
+
if (curM >= lo - tolerance && curM <= hi + tolerance) hit = i;
|
|
83
97
|
}
|
|
84
|
-
return hit;
|
|
98
|
+
if (hit >= 0) return hit;
|
|
99
|
+
|
|
100
|
+
// 彻底在倍率域之外:夹到最近的一端
|
|
101
|
+
return curM < lensRange(skill, 0)[0] ? 0 : count - 1;
|
|
85
102
|
};
|
|
86
103
|
|
|
87
104
|
/** 由镜头 id 反查下标 */
|
|
@@ -167,16 +167,20 @@ describe('镜头匹配', () => {
|
|
|
167
167
|
expect(matchLens(DOUBLE, 4.0)).toBe(1);
|
|
168
168
|
expect(matchLens(DOUBLE, 6.0)).toBe(1);
|
|
169
169
|
});
|
|
170
|
-
it('
|
|
171
|
-
//
|
|
170
|
+
it('临界点本身归大镜头,避免横跳', () => {
|
|
171
|
+
// 半开区间 [lo, hi):3.0 是 lens1 的下界,归长焦
|
|
172
172
|
expect(matchLens(DOUBLE, 3.0)).toBe(1);
|
|
173
173
|
});
|
|
174
|
-
it('
|
|
175
|
-
|
|
176
|
-
|
|
174
|
+
it('临界点以下不再被吸到大镜头:2.9 判短焦', () => {
|
|
175
|
+
// 旧实现双向 ±step 容差会让 [2.9, 3.0) 整条带子粘在长焦上,于是
|
|
176
|
+
// 转盘拖到 2.9 松手时不切镜头、画面被夹在 3.0,读数与画面对不上。
|
|
177
|
+
// 触底下切的落位(上限 − 1 step)正好在这条带子里,必须判成短焦。
|
|
178
|
+
expect(matchLens(DOUBLE, 2.9)).toBe(0);
|
|
179
|
+
expect(matchLens(DOUBLE, 2.99)).toBe(0);
|
|
177
180
|
expect(matchLens(DOUBLE, 3.1)).toBe(1);
|
|
178
181
|
});
|
|
179
|
-
it('
|
|
182
|
+
it('容差退化为兜底:只在严格命中不到(越界)时吸附', () => {
|
|
183
|
+
// 区间内一律严格命中,step 多粗都不影响归属
|
|
180
184
|
const coarse = {
|
|
181
185
|
cnt: 2,
|
|
182
186
|
thresholds: [1.0, 3.0, 6.0],
|
|
@@ -194,7 +198,7 @@ describe('镜头匹配', () => {
|
|
|
194
198
|
]
|
|
195
199
|
};
|
|
196
200
|
expect(toleranceOf(coarse, 1)).toBe(0.5);
|
|
197
|
-
expect(matchLens(coarse, 2.6)).toBe(
|
|
201
|
+
expect(matchLens(coarse, 2.6)).toBe(0);
|
|
198
202
|
expect(matchLens(coarse, 2.4)).toBe(0);
|
|
199
203
|
const noStep = {
|
|
200
204
|
cnt: 2,
|
|
@@ -210,14 +214,22 @@ describe('镜头匹配', () => {
|
|
|
210
214
|
}]
|
|
211
215
|
};
|
|
212
216
|
expect(toleranceOf(noStep, 1)).toBe(DEFAULT_MATCH_TOLERANCE);
|
|
213
|
-
expect(matchLens(noStep, 2.9)).toBe(
|
|
217
|
+
expect(matchLens(noStep, 2.9)).toBe(0);
|
|
214
218
|
expect(matchLens(noStep, 2.8)).toBe(0);
|
|
219
|
+
|
|
220
|
+
// 越界值:严格命中不到,用容差吸到相邻镜头
|
|
221
|
+
expect(matchLens(DOUBLE, 6.05)).toBe(1);
|
|
222
|
+
expect(matchLens(DOUBLE, 0.95)).toBe(0);
|
|
223
|
+
// 彻底在域外:夹到最近的一端
|
|
224
|
+
expect(matchLens(DOUBLE, 0.1)).toBe(0);
|
|
225
|
+
expect(matchLens(DOUBLE, 99)).toBe(1);
|
|
215
226
|
});
|
|
216
227
|
it('三目:跨两次临界点', () => {
|
|
217
228
|
expect(matchLens(TRIPLE, 1.5)).toBe(0);
|
|
218
229
|
expect(matchLens(TRIPLE, 4.0)).toBe(1);
|
|
219
230
|
expect(matchLens(TRIPLE, 9.0)).toBe(2);
|
|
220
231
|
expect(matchLens(TRIPLE, 6.0)).toBe(2); // 临界归大
|
|
232
|
+
expect(matchLens(TRIPLE, 5.9)).toBe(1); // 临界以下留在中焦
|
|
221
233
|
});
|
|
222
234
|
it('镜头 id 反查下标', () => {
|
|
223
235
|
expect(indexOfLensId(DOUBLE, 1)).toBe(1);
|
|
@@ -6,7 +6,7 @@ import { useDpState } from '../../hooks/useDpState';
|
|
|
6
6
|
import { setScaleMultipleEvent, zoomLevelChange } from '../../ui/constant';
|
|
7
7
|
import { MOCK_ZOOM_SKILL, MULTI_ZOOM_DP_CODE, dumpChannelApis, isChannelChangeSupported, isZoomSkillValid, lensCount, parseZoomSkill, queryCurrentLens, showSmallMonitor, subscribeChannelChange, switchLens } from './lensSkill';
|
|
8
8
|
import { DEFAULT_STEP, D_MIN, buildMarks, buildTickValues, indexOfLensId, lensRange, matchLens, maxScaleMultiple, toDisplayZoom, toScaleMultiple, zoomBounds } from './lensModel';
|
|
9
|
-
import { CHANNEL_SETTLE_TIMEOUT, D_MATCH_EPS, GESTURE_SWITCH_DEBOUNCE, SWITCH_LENS_TIMEOUT, ZOOM_PREVIEW_THROTTLE, zoomTurntableLensChange, zoomTurntableSwitching, zoomTurntableZoomChange } from './constants';
|
|
9
|
+
import { CHANNEL_SETTLE_TIMEOUT, DOWN_SWITCH_COOLDOWN, DRAG_CROSS_SWITCH_DELAY, D_MATCH_EPS, FLOOR_HIT_COUNT, FLOOR_HIT_SETTLE_GUARD, FLOOR_HIT_WINDOW, GESTURE_SWITCH_DEBOUNCE, POST_SWITCH_APPLY_DELAY, SETTLE_APPLY_TIMEOUT, SWITCH_DEDUPE_WINDOW, SWITCH_LENS_TIMEOUT, ZOOM_PREVIEW_THROTTLE, zoomTurntableLensChange, zoomTurntableSwitching, zoomTurntableZoomChange } from './constants';
|
|
10
10
|
/**
|
|
11
11
|
* 内部调试开关:置 true 时用 MOCK_ZOOM_SKILL 顶替 ipc_multi_zoom 的上报值。
|
|
12
12
|
*
|
|
@@ -42,6 +42,37 @@ const pendingSwitchStore = new Map();
|
|
|
42
42
|
*/
|
|
43
43
|
const smallMonitorStore = new Map();
|
|
44
44
|
|
|
45
|
+
/**
|
|
46
|
+
* 切镜头收尾后正在补下发的目标 d(按 devId)。
|
|
47
|
+
*
|
|
48
|
+
* 收尾时读数 / tab 已经按目标值落地,画面却要等 setScaleMultiple 走完才到位;
|
|
49
|
+
* 中间原生层会先把画面复位到 1 倍并回读一帧。不挡掉的话读数会先闪回镜头正倍数再跳到目标值。
|
|
50
|
+
*/
|
|
51
|
+
const settleApplyStore = new Map();
|
|
52
|
+
|
|
53
|
+
/** 触底计数(按 devId):画面已夹在 d = 1、用户还在继续缩小的连续帧数 */
|
|
54
|
+
const floorHitStore = new Map();
|
|
55
|
+
|
|
56
|
+
/** 最近一次切镜头收尾的时间(按 devId):收尾自身的 d = 1 回读不算触底 */
|
|
57
|
+
const settledAtStore = new Map();
|
|
58
|
+
|
|
59
|
+
/** 最近一次「触底自动下切」的时间(按 devId):冷却用 */
|
|
60
|
+
const downSwitchAtStore = new Map();
|
|
61
|
+
|
|
62
|
+
/** 最近一次切镜头下发(按 devId):Entry / Dock 两份 hook 判出同一次切换时去重 */
|
|
63
|
+
const switchReqStore = new Map();
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* 是否有转盘正在被拖动(按 devId)。
|
|
67
|
+
*
|
|
68
|
+
* 组件内的 draggingRef 只挡自己那份的回读,管不到别的实例:拖动中 previewZoom 会把
|
|
69
|
+
* 夹住的 d 连续下发,回读到 Entry 那份就成了「一直在推 d = 1」,正好被触底判定当成
|
|
70
|
+
* 「用户还在往下缩」,于是拖动落地和触底下切会同时切镜头。拖动路径自带落地逻辑,
|
|
71
|
+
* 手势那套自动切镜头必须整体让路,所以这个标记要跨实例共享。
|
|
72
|
+
*/
|
|
73
|
+
const draggingStore = new Map();
|
|
74
|
+
const isDraggingAnywhere = devId => draggingStore.get(devId) === true;
|
|
75
|
+
|
|
45
76
|
/** 自增序号:连着切到同一个镜头时,靠它认出「pending 还是不是我这次的」 */
|
|
46
77
|
let switchSeq = 0;
|
|
47
78
|
/** 是否正在切镜头(屏蔽回读用) */
|
|
@@ -55,6 +86,46 @@ const takePendingSwitch = devId => {
|
|
|
55
86
|
return pending;
|
|
56
87
|
};
|
|
57
88
|
|
|
89
|
+
/**
|
|
90
|
+
* 补差值下发期间的中间态回读要丢掉。
|
|
91
|
+
*
|
|
92
|
+
* 只丢「d ≈ 1」这一种 —— 那是原生层切完通道后画面自己的复位帧。ui.tsx 里
|
|
93
|
+
* 「先对齐 actual 再设 target」产生的那一帧已被它自己的 alignPending 抑制,不会到这儿。
|
|
94
|
+
* 收到任何非复位值就认为补差值已落地,解除屏蔽。
|
|
95
|
+
*/
|
|
96
|
+
const shouldDropSettleEcho = (devId, d) => {
|
|
97
|
+
const applying = settleApplyStore.get(devId);
|
|
98
|
+
if (!applying) return false;
|
|
99
|
+
if (Date.now() > applying.expireAt) {
|
|
100
|
+
// 原生层没按目标值回读(量化 / 下发失败),不能把读数永久冻住
|
|
101
|
+
console.warn('[zoomTurntable] 补差值回读超时,解除中间态屏蔽', applying.d);
|
|
102
|
+
settleApplyStore.delete(devId);
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
|
+
if (Math.abs(d - D_MIN) <= D_MATCH_EPS) return true;
|
|
106
|
+
settleApplyStore.delete(devId);
|
|
107
|
+
return false;
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* 抢一次切镜头的下发权。
|
|
112
|
+
* Entry / Dock 都订阅 zoomLevelChange,同一帧回读会让两份 hook 判出同一次切换;
|
|
113
|
+
* 不去重就是两发 switchChannel(第二发还会把第一发的 pending 作废,读数更乱)。
|
|
114
|
+
*/
|
|
115
|
+
const claimSwitch = (devId, index) => {
|
|
116
|
+
const last = switchReqStore.get(devId);
|
|
117
|
+
const now = Date.now();
|
|
118
|
+
if (last && last.index === index && now - last.at < SWITCH_DEDUPE_WINDOW) {
|
|
119
|
+
console.log('[zoomTurntable] 同一次切镜头已由另一实例下发,跳过', index);
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
122
|
+
switchReqStore.set(devId, {
|
|
123
|
+
index,
|
|
124
|
+
at: now
|
|
125
|
+
});
|
|
126
|
+
return true;
|
|
127
|
+
};
|
|
128
|
+
|
|
58
129
|
/**
|
|
59
130
|
* 焦距能力与双层缩放状态机。
|
|
60
131
|
* zoomSkill / zoomLensIndex 存在 ctx 共享 atom 上,Entry / Dock / RealTimeMagnification 读同一份。
|
|
@@ -101,6 +172,19 @@ export const useLensZoom = options => {
|
|
|
101
172
|
const gestureTimerRef = useRef();
|
|
102
173
|
/** 拖动中连续下发的节流时间戳 */
|
|
103
174
|
const previewAtRef = useRef(0);
|
|
175
|
+
/** 触底下切的 trailing 防抖(等手指停下再切,免得和正在进行的手势打架) */
|
|
176
|
+
const downTimerRef = useRef();
|
|
177
|
+
/** 拖动跨界后「手停住」的 trailing 防抖 */
|
|
178
|
+
const dragCrossTimerRef = useRef();
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* 自动切镜头(手势触底 / 跨临界)的驱动方:只由能力加载方(Entry)负责。
|
|
182
|
+
*
|
|
183
|
+
* Entry / Dock 各跑一份 hook,都订阅 zoomLevelChange。触底判定是**按帧计数**的,
|
|
184
|
+
* 两份都计数会让阈值形同减半;Entry 常驻(转盘展开时它只是被 CSS 藏起来,没卸载),
|
|
185
|
+
* 拿它当唯一驱动方最稳。拖动路径反过来只有 Dock 有,各管一段互不重叠。
|
|
186
|
+
*/
|
|
187
|
+
const isAutoSwitchDriver = !skipLoad;
|
|
104
188
|
const lensIndexRef = useRef(zoomLensIndex);
|
|
105
189
|
lensIndexRef.current = zoomLensIndex;
|
|
106
190
|
const skillRef = useRef(zoomSkill);
|
|
@@ -238,42 +322,79 @@ export const useLensZoom = options => {
|
|
|
238
322
|
});
|
|
239
323
|
const beginDrag = useMemoizedFn(value => {
|
|
240
324
|
draggingRef.current = true;
|
|
325
|
+
draggingStore.set(devId, true);
|
|
241
326
|
setCurM(value);
|
|
242
|
-
|
|
327
|
+
// 切镜头进行中(拖动跨界会在手指还按着时切):画面归收尾流程管,这里只更新读数
|
|
328
|
+
if (!isSwitching(devId)) previewZoom(value);
|
|
329
|
+
scheduleDragCross(value);
|
|
243
330
|
});
|
|
244
331
|
|
|
245
332
|
/**
|
|
246
|
-
* 镜头已切到 index
|
|
333
|
+
* 镜头已切到 index 后的收尾:把画面落到目标倍率上(**补差值**),读数与 tab 同步。
|
|
334
|
+
*
|
|
247
335
|
* 正常路径由 channelChange 触发(等设备真正切完再动画面,中间态更干净);
|
|
248
336
|
* 宿主不支持该事件时,退回 switchChannel 成功即收尾。
|
|
337
|
+
*
|
|
338
|
+
* @param targetM 目标画面绝对倍率。本端下发的切换都会带(从 pendingSwitch 取),
|
|
339
|
+
* 落位就是用户实际要的那个值:转盘拖到 2.5 就落 2.5、手势停在 3.5 就落 3.5、
|
|
340
|
+
* 触底下切落「上一镜头上限 − 1 step」。**不带**时才归该镜头的光学正倍数 ——
|
|
341
|
+
* 只有外部来源(设备侧 / 其他端切镜头)走这条,那时本端没有目标值可言。
|
|
342
|
+
*
|
|
343
|
+
* 早期设计是「切镜头一律归 1 倍、不补差值」,那会让下切必然掉到新镜头的 basicRatio:
|
|
344
|
+
* 从 3.0 往下拖到 2.5,切到短焦后画面直接变成 1.0,跨度大到像是坏了。
|
|
249
345
|
*/
|
|
250
|
-
const settleLensSwitched = useMemoizedFn(index => {
|
|
251
|
-
var
|
|
252
|
-
const
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
346
|
+
const settleLensSwitched = useMemoizedFn((index, targetM) => {
|
|
347
|
+
var _skill$lenses;
|
|
348
|
+
const skill = skillRef.current;
|
|
349
|
+
const lens = skill === null || skill === void 0 || (_skill$lenses = skill.lenses) === null || _skill$lenses === void 0 ? void 0 : _skill$lenses[index];
|
|
350
|
+
if (!skill || !lens) return;
|
|
351
|
+
// 目标值夹进新镜头区间:跨界落点可能略微越界(容差、浮点毛刺)
|
|
352
|
+
const [lo, hi] = lensRange(skill, index);
|
|
353
|
+
const finalM = targetM == null ? lens.basicRatio : Math.min(hi, Math.max(lo, targetM));
|
|
354
|
+
const d = toScaleMultiple(skill, index, finalM);
|
|
355
|
+
const isBase = d <= D_MIN + D_MATCH_EPS;
|
|
356
|
+
console.log('[scaleMultiple] zoomTurntable 切镜头收尾', `lensIndex=${index}`, `finalM=${finalM.toFixed(2)}`, `d=${d.toFixed(3)}`, isBase ? '(归正倍数)' : '(补差值)', `lastD=${getLastD(devId)}`);
|
|
357
|
+
|
|
358
|
+
// 状态先落地:读数 / tab 立刻对齐,不等画面下发走完 P2P
|
|
359
|
+
syncToLens(index, d);
|
|
257
360
|
markSwitching(-1);
|
|
258
|
-
|
|
259
|
-
|
|
361
|
+
settledAtStore.set(devId, Date.now());
|
|
362
|
+
floorHitStore.delete(devId);
|
|
363
|
+
// 小窗跟最终 d 走:补差值后 d > 1(画面不在正倍数上),小窗该开着 ——
|
|
364
|
+
// 旧实现无条件 applySmallMonitor(false),补差值后就与画面状态不符了
|
|
365
|
+
syncSmallMonitorByD(d);
|
|
260
366
|
event.emit(zoomTurntableLensChange, {
|
|
261
367
|
lensId: lens.id,
|
|
262
368
|
lensIndex: index,
|
|
263
|
-
value:
|
|
369
|
+
value: finalM
|
|
264
370
|
});
|
|
265
371
|
event.emit(zoomTurntableZoomChange, {
|
|
266
|
-
curM:
|
|
267
|
-
d
|
|
372
|
+
curM: finalM,
|
|
373
|
+
d,
|
|
268
374
|
lensIndex: index
|
|
269
375
|
});
|
|
376
|
+
if (isBase) {
|
|
377
|
+
// 归正倍数:与旧行为一致,立即下发
|
|
378
|
+
event.emit(setScaleMultipleEvent, D_MIN);
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
// 补差值:新流刚起来,立刻打 scaleMultiple 有被吞的风险,延一拍;
|
|
382
|
+
// 这段窗口内的复位帧回读由 settleApplyStore 挡掉
|
|
383
|
+
settleApplyStore.set(devId, {
|
|
384
|
+
d,
|
|
385
|
+
expireAt: Date.now() + SETTLE_APPLY_TIMEOUT
|
|
386
|
+
});
|
|
387
|
+
setTimeout(() => {
|
|
388
|
+
console.log('[scaleMultiple] zoomTurntable 补差值下发', d, `lensIndex=${index}`);
|
|
389
|
+
event.emit(setScaleMultipleEvent, d);
|
|
390
|
+
}, POST_SWITCH_APPLY_DELAY);
|
|
270
391
|
});
|
|
271
392
|
|
|
272
393
|
/** 把目标倍率落到指定镜头上:同镜头内下发本地缩放,跨镜头则切物理镜头并归 1 倍 */
|
|
273
394
|
const applyToLens = useMemoizedFn(async (nextIndex, targetM) => {
|
|
274
|
-
var _skill$
|
|
395
|
+
var _skill$lenses2;
|
|
275
396
|
const skill = skillRef.current;
|
|
276
|
-
if (!(skill !== null && skill !== void 0 && (_skill$
|
|
397
|
+
if (!(skill !== null && skill !== void 0 && (_skill$lenses2 = skill.lenses) !== null && _skill$lenses2 !== void 0 && _skill$lenses2[nextIndex])) return;
|
|
277
398
|
setCurM(targetM);
|
|
278
399
|
const prevIndex = lensIndexRef.current;
|
|
279
400
|
if (nextIndex === prevIndex) {
|
|
@@ -290,6 +411,9 @@ export const useLensZoom = options => {
|
|
|
290
411
|
return;
|
|
291
412
|
}
|
|
292
413
|
|
|
414
|
+
// Entry / Dock 同一帧判出同一次切换时只放一次过(读数已在上面各自更新过)
|
|
415
|
+
if (!claimSwitch(devId, nextIndex)) return;
|
|
416
|
+
|
|
293
417
|
// 切物理镜头:从下发到 channelChange 回来这段屏蔽回读
|
|
294
418
|
console.log('[zoomTurntable] 切换镜头', `${prevIndex} -> ${nextIndex}`, `channel=${skill.lenses[nextIndex].id}`, `targetM=${targetM.toFixed(2)}`);
|
|
295
419
|
takePendingSwitch(devId); // 上一次没结算完的直接作废,以最新这次为准
|
|
@@ -305,6 +429,7 @@ export const useLensZoom = options => {
|
|
|
305
429
|
pendingSwitchStore.set(devId, {
|
|
306
430
|
seq,
|
|
307
431
|
index: nextIndex,
|
|
432
|
+
targetM,
|
|
308
433
|
timer: setTimeout(() => {
|
|
309
434
|
pendingSwitchStore.delete(devId);
|
|
310
435
|
markSwitching(-1);
|
|
@@ -328,7 +453,7 @@ export const useLensZoom = options => {
|
|
|
328
453
|
// 那时设备已经真的切过去了,画面不会先跳一下再稳定
|
|
329
454
|
if (!isChannelChangeSupported()) {
|
|
330
455
|
takePendingSwitch(devId);
|
|
331
|
-
settleLensSwitched(nextIndex);
|
|
456
|
+
settleLensSwitched(nextIndex, targetM);
|
|
332
457
|
return;
|
|
333
458
|
}
|
|
334
459
|
|
|
@@ -345,10 +470,49 @@ export const useLensZoom = options => {
|
|
|
345
470
|
// 有到达但没有「镜头切换到位」→ 被 deviceId 或 channel 校验挡掉了
|
|
346
471
|
console.warn('[zoomTurntable] 等 channelChange 超时,按下发结果收尾', `${prevIndex} -> ${nextIndex}`, `channel=${skill.lenses[nextIndex].id}`, `${CHANNEL_SETTLE_TIMEOUT}ms`, `onChannelChange 可用=${isChannelChangeSupported()}`);
|
|
347
472
|
dumpChannelApis();
|
|
348
|
-
settleLensSwitched(nextIndex);
|
|
473
|
+
settleLensSwitched(nextIndex, targetM);
|
|
349
474
|
}, CHANNEL_SETTLE_TIMEOUT);
|
|
350
475
|
});
|
|
351
476
|
|
|
477
|
+
/**
|
|
478
|
+
* 拖动跨镜头临界:过界后**手停住** DRAG_CROSS_SWITCH_DELAY 就地切镜头,画面跟着拖动走。
|
|
479
|
+
*
|
|
480
|
+
* previewZoom 把下发值夹在当前镜头区间内,所以拖过界之后画面是「冻」住的 ——
|
|
481
|
+
* 只等松手才切的话,从 3.0 往下拖到 2.5 的一路上画面纹丝不动,手感像是没响应。
|
|
482
|
+
* 逐帧切也不行:switchChannel 是 P2P、几百毫秒级,逐帧下发会把通道打爆。
|
|
483
|
+
* 折中成 trailing 防抖:值一直在变就一直不切,停住才切。
|
|
484
|
+
*
|
|
485
|
+
* 切换期间 draggingRef 保持 true(见 onChannelChange 里的说明),松手后 applyZoom
|
|
486
|
+
* 再按最终值落一次 —— 那时镜头通常已经对了,走的是同镜头内下发 d 的分支。
|
|
487
|
+
*/
|
|
488
|
+
const scheduleDragCross = useMemoizedFn(value => {
|
|
489
|
+
const clearPending = () => {
|
|
490
|
+
if (dragCrossTimerRef.current) {
|
|
491
|
+
clearTimeout(dragCrossTimerRef.current);
|
|
492
|
+
dragCrossTimerRef.current = undefined;
|
|
493
|
+
}
|
|
494
|
+
};
|
|
495
|
+
const skill = skillRef.current;
|
|
496
|
+
if (!skill) return;
|
|
497
|
+
// 回到当前镜头区间内 / 已在切换中:撤掉待切
|
|
498
|
+
if (matchLens(skill, value) === lensIndexRef.current || isSwitching(devId)) {
|
|
499
|
+
clearPending();
|
|
500
|
+
return;
|
|
501
|
+
}
|
|
502
|
+
clearPending();
|
|
503
|
+
dragCrossTimerRef.current = setTimeout(() => {
|
|
504
|
+
dragCrossTimerRef.current = undefined;
|
|
505
|
+
// 已松手 → 交给 applyZoom 落地,不在这儿抢
|
|
506
|
+
if (!draggingRef.current || isSwitching(devId)) return;
|
|
507
|
+
const cur = skillRef.current;
|
|
508
|
+
if (!cur) return;
|
|
509
|
+
const nextIndex = matchLens(cur, value);
|
|
510
|
+
if (nextIndex === lensIndexRef.current) return;
|
|
511
|
+
console.log('[zoomTurntable] 拖动跨界停留,就地切镜头', `${lensIndexRef.current} -> ${nextIndex}`, `targetM=${value.toFixed(2)}`);
|
|
512
|
+
applyToLens(nextIndex, value);
|
|
513
|
+
}, DRAG_CROSS_SWITCH_DELAY);
|
|
514
|
+
});
|
|
515
|
+
|
|
352
516
|
/**
|
|
353
517
|
* 拖动状态兜底复位。
|
|
354
518
|
*
|
|
@@ -359,11 +523,22 @@ export const useLensZoom = options => {
|
|
|
359
523
|
*/
|
|
360
524
|
const endDrag = useMemoizedFn(() => {
|
|
361
525
|
draggingRef.current = false;
|
|
526
|
+
draggingStore.delete(devId);
|
|
527
|
+
if (dragCrossTimerRef.current) {
|
|
528
|
+
clearTimeout(dragCrossTimerRef.current);
|
|
529
|
+
dragCrossTimerRef.current = undefined;
|
|
530
|
+
}
|
|
362
531
|
});
|
|
363
532
|
|
|
364
533
|
/** 拖动结束:按倍率匹配镜头后落地 */
|
|
365
534
|
const applyZoom = useMemoizedFn(async targetM => {
|
|
366
535
|
draggingRef.current = false;
|
|
536
|
+
draggingStore.delete(devId);
|
|
537
|
+
// 松手即是最终值,待切的那次作废,直接按最终值走
|
|
538
|
+
if (dragCrossTimerRef.current) {
|
|
539
|
+
clearTimeout(dragCrossTimerRef.current);
|
|
540
|
+
dragCrossTimerRef.current = undefined;
|
|
541
|
+
}
|
|
367
542
|
const skill = skillRef.current;
|
|
368
543
|
if (!skill) return;
|
|
369
544
|
await applyToLens(matchLens(skill, targetM), targetM);
|
|
@@ -371,11 +546,12 @@ export const useLensZoom = options => {
|
|
|
371
546
|
|
|
372
547
|
/** 点击倍率 tab:直接切到该镜头并归位到其光学倍数;不走 matchLens,免得容差判到相邻镜头 */
|
|
373
548
|
const switchToLens = useMemoizedFn(async index => {
|
|
374
|
-
var _skill$
|
|
549
|
+
var _skill$lenses3;
|
|
375
550
|
const skill = skillRef.current;
|
|
376
|
-
const lens = skill === null || skill === void 0 || (_skill$
|
|
551
|
+
const lens = skill === null || skill === void 0 || (_skill$lenses3 = skill.lenses) === null || _skill$lenses3 === void 0 ? void 0 : _skill$lenses3[index];
|
|
377
552
|
if (!lens) return;
|
|
378
553
|
draggingRef.current = false;
|
|
554
|
+
draggingStore.delete(devId);
|
|
379
555
|
await applyToLens(index, lens.basicRatio);
|
|
380
556
|
});
|
|
381
557
|
|
|
@@ -406,9 +582,11 @@ export const useLensZoom = options => {
|
|
|
406
582
|
return; // 外部来源的重复事件
|
|
407
583
|
}
|
|
408
584
|
console.log('[zoomTurntable] 镜头切换到位', `${lensIndexRef.current} -> ${index}`, pending ? '(本端下发)' : '(外部来源)');
|
|
409
|
-
//
|
|
410
|
-
|
|
411
|
-
|
|
585
|
+
// 只有外部来源(设备侧 / 其他端)才作废进行中的拖动 —— 读数得以设备为准。
|
|
586
|
+
// 本端下发的那次不能动 draggingRef:拖动跨界会在**手指还按着**的时候切镜头,
|
|
587
|
+
// 置 false 会放开回读屏蔽,回读帧和拖动帧互相打架,指针会抖。
|
|
588
|
+
if (!pending) draggingRef.current = false;
|
|
589
|
+
settleLensSwitched(index, pending === null || pending === void 0 ? void 0 : pending.targetM);
|
|
412
590
|
});
|
|
413
591
|
useEffect(() => {
|
|
414
592
|
// 订没订上必须能从日志看出来:没订上时切镜头一定走超时回滚,两者表象一样
|
|
@@ -434,15 +612,94 @@ export const useLensZoom = options => {
|
|
|
434
612
|
gestureTimerRef.current = setTimeout(() => {
|
|
435
613
|
gestureTimerRef.current = undefined;
|
|
436
614
|
const skill = skillRef.current;
|
|
437
|
-
if (!skill || draggingRef.current || isSwitching(devId)) return;
|
|
615
|
+
if (!skill || draggingRef.current || isDraggingAnywhere(devId) || isSwitching(devId)) return;
|
|
438
616
|
const nextIndex = matchLens(skill, value);
|
|
439
617
|
if (nextIndex === lensIndexRef.current) return;
|
|
440
618
|
console.log('[zoomTurntable] 手势跨临界点,切换镜头', `curM=${value.toFixed(2)}`, `${lensIndexRef.current} -> ${nextIndex}`);
|
|
441
619
|
applyToLens(nextIndex, value);
|
|
442
620
|
}, GESTURE_SWITCH_DEBOUNCE);
|
|
443
621
|
});
|
|
622
|
+
|
|
623
|
+
/**
|
|
624
|
+
* 触底继续缩 → 往下切一个镜头(trailing 防抖,等手指停下再切)。
|
|
625
|
+
*
|
|
626
|
+
* 现象:在非首个镜头上把画面缩到该镜头的光学正倍数后(d = 1,协议下限),用户继续
|
|
627
|
+
* 双指往里收,播放器的 d 已经夹住动不了,画面纹丝不动,读数也停在 basicRatio。
|
|
628
|
+
* 此时 zoomLevelChange 仍然按帧推 d = 1 上来 —— 这就是唯一可用的信号
|
|
629
|
+
* (detail.scaleMode「自由缩放状态标识」实测不准,不能拿来判定)。
|
|
630
|
+
*
|
|
631
|
+
* 落位取「上一个镜头区间上限 − 1 step」(如 3.0 → 2.9):那正是画面能继续变小的第一格,
|
|
632
|
+
* 换算到短焦上是 d ≈ 3.85,用户接着往里收就能顺畅地一路缩到 1.0。
|
|
633
|
+
*/
|
|
634
|
+
const scheduleDownSwitch = useMemoizedFn(() => {
|
|
635
|
+
var _downSwitchAtStore$ge;
|
|
636
|
+
if (Date.now() - ((_downSwitchAtStore$ge = downSwitchAtStore.get(devId)) !== null && _downSwitchAtStore$ge !== void 0 ? _downSwitchAtStore$ge : 0) < DOWN_SWITCH_COOLDOWN) return;
|
|
637
|
+
if (downTimerRef.current) clearTimeout(downTimerRef.current);
|
|
638
|
+
downTimerRef.current = setTimeout(() => {
|
|
639
|
+
var _skill$lenses$target$, _skill$lenses4;
|
|
640
|
+
downTimerRef.current = undefined;
|
|
641
|
+
const skill = skillRef.current;
|
|
642
|
+
const index = lensIndexRef.current;
|
|
643
|
+
if (!skill || index <= 0) return;
|
|
644
|
+
if (draggingRef.current || isDraggingAnywhere(devId) || isSwitching(devId)) return;
|
|
645
|
+
// 防抖这 500ms 里可能又被放大了,切之前再确认画面确实还夹在正倍数上
|
|
646
|
+
if (getLastD(devId) > D_MIN + D_MATCH_EPS) {
|
|
647
|
+
floorHitStore.delete(devId);
|
|
648
|
+
return;
|
|
649
|
+
}
|
|
650
|
+
const target = index - 1;
|
|
651
|
+
const [, upper] = lensRange(skill, target);
|
|
652
|
+
const stepOfTarget = (_skill$lenses$target$ = (_skill$lenses4 = skill.lenses) === null || _skill$lenses4 === void 0 || (_skill$lenses4 = _skill$lenses4[target]) === null || _skill$lenses4 === void 0 ? void 0 : _skill$lenses4.step) !== null && _skill$lenses$target$ !== void 0 ? _skill$lenses$target$ : DEFAULT_STEP;
|
|
653
|
+
const targetM = upper - stepOfTarget;
|
|
654
|
+
downSwitchAtStore.set(devId, Date.now());
|
|
655
|
+
floorHitStore.delete(devId);
|
|
656
|
+
console.log('[zoomTurntable] 触底继续缩,向下切镜头', `${index} -> ${target}`, `targetM=${targetM.toFixed(2)}`);
|
|
657
|
+
applyToLens(target, targetM);
|
|
658
|
+
}, GESTURE_SWITCH_DEBOUNCE);
|
|
659
|
+
});
|
|
660
|
+
|
|
661
|
+
/**
|
|
662
|
+
* 触底计数。返回 true 表示已排下切,本帧不再走上界 / 防抖那套判定。
|
|
663
|
+
* 只由驱动方计数 —— 两份 hook 都数会让阈值形同减半。
|
|
664
|
+
*/
|
|
665
|
+
const handleFloorHit = useMemoizedFn(d => {
|
|
666
|
+
var _settledAtStore$get;
|
|
667
|
+
if (!isAutoSwitchDriver) return false;
|
|
668
|
+
// 拖动中:落地由 applyZoom / 跨界停留负责,触底那套整体让路
|
|
669
|
+
if (isDraggingAnywhere(devId)) {
|
|
670
|
+
floorHitStore.delete(devId);
|
|
671
|
+
return false;
|
|
672
|
+
}
|
|
673
|
+
// 不在正倍数上:正常缩放,清计数
|
|
674
|
+
if (d > D_MIN + D_MATCH_EPS) {
|
|
675
|
+
floorHitStore.delete(devId);
|
|
676
|
+
return false;
|
|
677
|
+
}
|
|
678
|
+
// 首个镜头已经是倍率域下界,再缩没有镜头可切
|
|
679
|
+
if (lensIndexRef.current <= 0) return false;
|
|
680
|
+
const now = Date.now();
|
|
681
|
+
// 收尾自身会引发一到两帧 d = 1 回读,静默期内不计数,否则刚切上去又被切回来
|
|
682
|
+
if (now - ((_settledAtStore$get = settledAtStore.get(devId)) !== null && _settledAtStore$get !== void 0 ? _settledAtStore$get : 0) < FLOOR_HIT_SETTLE_GUARD) return false;
|
|
683
|
+
const prev = floorHitStore.get(devId);
|
|
684
|
+
const count = prev && now - prev.lastAt <= FLOOR_HIT_WINDOW ? prev.count + 1 : 1;
|
|
685
|
+
floorHitStore.set(devId, {
|
|
686
|
+
count,
|
|
687
|
+
lastAt: now
|
|
688
|
+
});
|
|
689
|
+
// 每一帧都打:真机上「触底还缩不缩得动」全靠这条日志判断 ——
|
|
690
|
+
// 如果原生只在 d 真正变化时才推事件,这里会永远停在 count=1、下切一次都不会触发,
|
|
691
|
+
// 那就说明这条信号不成立,得改由 APP 侧补「手势下界」参数(协议 §4.3 第 5 条)
|
|
692
|
+
console.log('[zoomTurntable] 触底回读', `count=${count}/${FLOOR_HIT_COUNT}`, `lensIndex=${lensIndexRef.current}`);
|
|
693
|
+
// 第一帧是「正常缩到正倍数」,从第二帧起才是「夹住了还在缩」
|
|
694
|
+
if (count < FLOOR_HIT_COUNT) return false;
|
|
695
|
+
console.log('[zoomTurntable] 画面已夹在正倍数仍有缩放回读,排下切', `count=${count}`);
|
|
696
|
+
scheduleDownSwitch();
|
|
697
|
+
return true;
|
|
698
|
+
});
|
|
444
699
|
const onZoomLevelChange = useMemoizedFn(d => {
|
|
445
700
|
if (!(d > 0)) return;
|
|
701
|
+
// 补差值下发期间原生画面会先复位到 1 倍,那一帧不能让它把读数拽回镜头正倍数
|
|
702
|
+
if (shouldDropSettleEcho(devId, d)) return;
|
|
446
703
|
setLastD(devId, d);
|
|
447
704
|
// 切镜头途中的中间态不算数(收尾时统一关掉),拖动中画面还没动
|
|
448
705
|
if (draggingRef.current || isSwitching(devId)) return;
|
|
@@ -458,6 +715,13 @@ export const useLensZoom = options => {
|
|
|
458
715
|
lensIndex: lensIndexRef.current
|
|
459
716
|
});
|
|
460
717
|
|
|
718
|
+
// 触底继续缩:往下切一个镜头(下界,与下面的上界对称)
|
|
719
|
+
if (handleFloorHit(d)) return;
|
|
720
|
+
if (!isAutoSwitchDriver) return;
|
|
721
|
+
// 拖动中:previewZoom 把下发值夹在当前镜头区间内,回读会一直贴着区间边界,
|
|
722
|
+
// 不让路的话「触到上界立即切」会和拖动跨界停留同时切镜头
|
|
723
|
+
if (isDraggingAnywhere(devId)) return;
|
|
724
|
+
|
|
461
725
|
// 画面已经顶到当前镜头区间的上界(也就是下一个镜头的光学倍数,如 1x 镜头放到 3.0):
|
|
462
726
|
// 这个边界值在归属上属于下一个镜头,必须立刻切过去。
|
|
463
727
|
// 走 trailing 防抖的话,那 500ms 里 tab 会挂着「下一镜头的值」显示在当前镜头上。
|
|
@@ -484,6 +748,8 @@ export const useLensZoom = options => {
|
|
|
484
748
|
useEffect(() => {
|
|
485
749
|
return () => {
|
|
486
750
|
if (gestureTimerRef.current) clearTimeout(gestureTimerRef.current);
|
|
751
|
+
if (downTimerRef.current) clearTimeout(downTimerRef.current);
|
|
752
|
+
if (dragCrossTimerRef.current) clearTimeout(dragCrossTimerRef.current);
|
|
487
753
|
};
|
|
488
754
|
}, []);
|
|
489
755
|
const [min, max] = zoomSkill ? zoomBounds(zoomSkill) : [D_MIN, D_MIN];
|