@ray-js/ipc-player-integration 0.0.50-beta.3 → 0.0.50-beta.5
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/features/initPlayerWidgets/index.d.ts +2 -1
- package/lib/features/initPlayerWidgets/index.js +2 -1
- 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/lensSkill.d.ts +8 -0
- package/lib/widgets/zoomTurntable/lensSkill.js +31 -10
- package/lib/widgets/zoomTurntable/useLensZoom.d.ts +1 -1
- package/lib/widgets/zoomTurntable/useLensZoom.js +310 -31
- package/lib/widgets/zoomTurntable/zoomTurntableEntry.d.ts +3 -2
- package/lib/widgets/zoomTurntable/zoomTurntableEntry.js +2 -1
- package/package.json +1 -1
|
@@ -14,7 +14,8 @@ export type InitPlayerWidgetsOptions = {
|
|
|
14
14
|
showRealTimeMagnification?: boolean;
|
|
15
15
|
/**
|
|
16
16
|
* 允许展示焦距转盘入口(长按图标弹出转盘)。
|
|
17
|
-
*
|
|
17
|
+
* 仅为功能开关;实际渲染还要宿主提供 ty.ipc.switchChannel、且 ipc_multi_zoom 解析出 cnt > 1,
|
|
18
|
+
* 单镜头设备与老宿主上传 true 也不出现。
|
|
18
19
|
*/
|
|
19
20
|
showZoomTurntable?: boolean;
|
|
20
21
|
directionControlProps?: Partial<React.ComponentProps<typeof FullScreen>['directionControlProps']>;
|
|
@@ -53,7 +53,8 @@ export async function initPlayerWidgets(ctx, options) {
|
|
|
53
53
|
}
|
|
54
54
|
}
|
|
55
55
|
// 焦距转盘入口:这里只放开占位,真正是否渲染由 widget 内部按
|
|
56
|
-
// ipc_multi_zoom
|
|
56
|
+
// 「宿主有 ty.ipc.switchChannel」+「ipc_multi_zoom 解析出 cnt > 1」决定
|
|
57
|
+
// (单镜头设备、不支持切镜头的老宿主都不出现)
|
|
57
58
|
if (options.showZoomTurntable) {
|
|
58
59
|
const zoomTurntableIndex = newDefaultBottomLeftContent.findIndex(item => item.id === 'ZoomTurntableEntry');
|
|
59
60
|
if (zoomTurntableIndex !== -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);
|
|
@@ -56,6 +56,14 @@ export declare const isZoomSkillValid: (skill?: ZoomSkill | null) => boolean;
|
|
|
56
56
|
* @param value DP 值;内部 mock 常量是对象、DP 上报是 string,两条来源共用同一校验链路
|
|
57
57
|
*/
|
|
58
58
|
export declare const parseZoomSkill: (value?: unknown) => ZoomSkill | null;
|
|
59
|
+
/**
|
|
60
|
+
* 宿主是否支持切镜头下发。
|
|
61
|
+
*
|
|
62
|
+
* 转盘的每一种交互(点 tab 切镜头、拖转盘跨界、手势触底下切)最终都落到 switchChannel 上,
|
|
63
|
+
* 方法不存在时转盘画得出来却切不动,只会让用户以为坏了 —— 故整个入口不渲染
|
|
64
|
+
* (门控在 useLensZoom 的 multiLensReady 里,与 cnt > 1 同级)。
|
|
65
|
+
*/
|
|
66
|
+
export declare const isSwitchChannelSupported: () => boolean;
|
|
59
67
|
/**
|
|
60
68
|
* 切换物理镜头。channel 取上报的子镜头 id 而非数组下标 —— 固件可上报非连续 id。
|
|
61
69
|
* 返回 false(不支持 / 失败)时由 useLensZoom 回滚读数,无回调另有 SWITCH_LENS_TIMEOUT 兜底。
|
|
@@ -111,13 +111,34 @@ export const parseZoomSkill = value => {
|
|
|
111
111
|
|
|
112
112
|
/** ty.ipc.switchChannel 的最小签名:ty 类型包尚未收录,本地声明避免调用点退化成 any */
|
|
113
113
|
|
|
114
|
+
/** 「宿主不支持 switchChannel」只提示一次:判定会被 render 反复调用,不去重会刷屏 */
|
|
115
|
+
let switchChannelMissingLogged = false;
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* 宿主是否支持切镜头下发。
|
|
119
|
+
*
|
|
120
|
+
* 转盘的每一种交互(点 tab 切镜头、拖转盘跨界、手势触底下切)最终都落到 switchChannel 上,
|
|
121
|
+
* 方法不存在时转盘画得出来却切不动,只会让用户以为坏了 —— 故整个入口不渲染
|
|
122
|
+
* (门控在 useLensZoom 的 multiLensReady 里,与 cnt > 1 同级)。
|
|
123
|
+
*/
|
|
124
|
+
export const isSwitchChannelSupported = () => {
|
|
125
|
+
var _ty;
|
|
126
|
+
const supported = typeof ((_ty = ty) === null || _ty === void 0 || (_ty = _ty.ipc) === null || _ty === void 0 ? void 0 : _ty.switchChannel) === 'function';
|
|
127
|
+
if (!supported && !switchChannelMissingLogged) {
|
|
128
|
+
switchChannelMissingLogged = true;
|
|
129
|
+
console.warn('[zoomTurntable] 宿主不支持 ty.ipc.switchChannel,焦距转盘入口不展示');
|
|
130
|
+
dumpChannelApis();
|
|
131
|
+
}
|
|
132
|
+
return supported;
|
|
133
|
+
};
|
|
134
|
+
|
|
114
135
|
/**
|
|
115
136
|
* 切换物理镜头。channel 取上报的子镜头 id 而非数组下标 —— 固件可上报非连续 id。
|
|
116
137
|
* 返回 false(不支持 / 失败)时由 useLensZoom 回滚读数,无回调另有 SWITCH_LENS_TIMEOUT 兜底。
|
|
117
138
|
*/
|
|
118
139
|
export const switchLens = (devId, lens) => {
|
|
119
|
-
var
|
|
120
|
-
const switchChannel = (
|
|
140
|
+
var _ty2;
|
|
141
|
+
const switchChannel = (_ty2 = ty) === null || _ty2 === void 0 || (_ty2 = _ty2.ipc) === null || _ty2 === void 0 ? void 0 : _ty2.switchChannel;
|
|
121
142
|
// 入参与回调结果都打出来:真机上「切没切成」只能靠这条链路判断
|
|
122
143
|
const params = {
|
|
123
144
|
deviceId: devId,
|
|
@@ -223,8 +244,8 @@ const pickChannel = payload => {
|
|
|
223
244
|
* 返回 null(不支持 / 失败 / 超时)时调用方保持现状,不重置镜头。
|
|
224
245
|
*/
|
|
225
246
|
export const queryCurrentLens = devId => {
|
|
226
|
-
var
|
|
227
|
-
const queryCurrentChannel = (
|
|
247
|
+
var _ty3;
|
|
248
|
+
const queryCurrentChannel = (_ty3 = ty) === null || _ty3 === void 0 || (_ty3 = _ty3.ipc) === null || _ty3 === void 0 ? void 0 : _ty3.queryCurrentChannel;
|
|
228
249
|
if (typeof queryCurrentChannel !== 'function') {
|
|
229
250
|
console.warn('[zoomTurntable] 宿主不支持 ty.ipc.queryCurrentChannel');
|
|
230
251
|
return Promise.resolve(null);
|
|
@@ -273,8 +294,8 @@ export const queryCurrentLens = devId => {
|
|
|
273
294
|
* 切镜头的收尾(画面归 1 倍 + 刷读数)以该事件为准,不支持时只能退回「switchChannel 成功即收尾」。
|
|
274
295
|
*/
|
|
275
296
|
export const isChannelChangeSupported = () => {
|
|
276
|
-
var
|
|
277
|
-
return typeof ((
|
|
297
|
+
var _ty4;
|
|
298
|
+
return typeof ((_ty4 = ty) === null || _ty4 === void 0 || (_ty4 = _ty4.ipc) === null || _ty4 === void 0 ? void 0 : _ty4.onChannelChange) === 'function';
|
|
278
299
|
};
|
|
279
300
|
|
|
280
301
|
/**
|
|
@@ -282,8 +303,8 @@ export const isChannelChangeSupported = () => {
|
|
|
282
303
|
* 真机上「事件为什么不来」第一步就是确认宿主到底提供了哪个名字。
|
|
283
304
|
*/
|
|
284
305
|
export const dumpChannelApis = () => {
|
|
285
|
-
var
|
|
286
|
-
const ipc = (
|
|
306
|
+
var _ty5;
|
|
307
|
+
const ipc = (_ty5 = ty) === null || _ty5 === void 0 ? void 0 : _ty5.ipc;
|
|
287
308
|
if (!ipc) {
|
|
288
309
|
console.warn('[zoomTurntable] ty.ipc 不存在', `typeof ty=${typeof ty}`);
|
|
289
310
|
return;
|
|
@@ -321,9 +342,9 @@ export const subscribeChannelChange = (devId, handler) => {
|
|
|
321
342
|
console.log('[zoomTurntable] 已订阅 ty.ipc.onChannelChange', devId);
|
|
322
343
|
ty.ipc.onChannelChange(listener);
|
|
323
344
|
return () => {
|
|
324
|
-
var
|
|
345
|
+
var _ty6;
|
|
325
346
|
console.log('[zoomTurntable] 解绑 channelChange 监听', devId);
|
|
326
|
-
if (typeof ((
|
|
347
|
+
if (typeof ((_ty6 = ty) === null || _ty6 === void 0 || (_ty6 = _ty6.ipc) === null || _ty6 === void 0 ? void 0 : _ty6.offChannelChange) === 'function') {
|
|
327
348
|
ty.ipc.offChannelChange(listener);
|
|
328
349
|
} else {
|
|
329
350
|
console.warn('[zoomTurntable] 宿主无 offChannelChange,监听无法解绑');
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { ComponentConfigProps } from '../../interface';
|
|
2
2
|
type Options = ComponentConfigProps & {
|
|
3
|
-
/**
|
|
3
|
+
/** 业务侧的功能开关;实际是否渲染仍由宿主 switchChannel 能力 + ipc_multi_zoom 的 cnt 决定 */
|
|
4
4
|
showZoomTurntable?: boolean;
|
|
5
5
|
/** 跳过能力加载:Entry 常驻负责加载,Dock 作为消费方传 true 避免重复解析 */
|
|
6
6
|
skipLoad?: boolean;
|
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
import { useEffect, useRef, useState } from 'react';
|
|
1
|
+
import { useEffect, useMemo, useRef, useState } from 'react';
|
|
2
2
|
import { useMemoizedFn } from 'ahooks';
|
|
3
3
|
import { useStore } from '../../ctx/store';
|
|
4
4
|
import { PlayState } from '../../interface';
|
|
5
5
|
import { useDpState } from '../../hooks/useDpState';
|
|
6
6
|
import { setScaleMultipleEvent, zoomLevelChange } from '../../ui/constant';
|
|
7
|
-
import { MOCK_ZOOM_SKILL, MULTI_ZOOM_DP_CODE, dumpChannelApis, isChannelChangeSupported, isZoomSkillValid, lensCount, parseZoomSkill, queryCurrentLens, showSmallMonitor, subscribeChannelChange, switchLens } from './lensSkill';
|
|
7
|
+
import { MOCK_ZOOM_SKILL, MULTI_ZOOM_DP_CODE, dumpChannelApis, isChannelChangeSupported, isSwitchChannelSupported, 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);
|
|
@@ -126,9 +210,18 @@ export const useLensZoom = options => {
|
|
|
126
210
|
};
|
|
127
211
|
}, [event]);
|
|
128
212
|
|
|
213
|
+
// ---------------- 宿主能力 ----------------
|
|
214
|
+
/**
|
|
215
|
+
* 宿主是否支持 ty.ipc.switchChannel。宿主能力在运行期不会变,求一次即可 ——
|
|
216
|
+
* 放 useMemo 而不是 render 期直接调,免得 typeof 判定与首次告警日志跟着每次渲染跑。
|
|
217
|
+
*
|
|
218
|
+
* 转盘的每一种交互最终都落到这个方法上,它不存在时转盘切不动,故整个入口不展示。
|
|
219
|
+
*/
|
|
220
|
+
const switchChannelSupported = useMemo(() => isSwitchChannelSupported(), []);
|
|
221
|
+
|
|
129
222
|
// ---------------- 能力加载 ----------------
|
|
130
223
|
useEffect(() => {
|
|
131
|
-
if (skipLoad || !devId || !showZoomTurntable) return undefined;
|
|
224
|
+
if (skipLoad || !devId || !showZoomTurntable || !switchChannelSupported) return undefined;
|
|
132
225
|
const raw = multiZoomDpValue;
|
|
133
226
|
console.log('[zoomTurntable] ipc_multi_zoom 上报值', JSON.stringify(raw !== null && raw !== void 0 ? raw : null));
|
|
134
227
|
const skill = parseZoomSkill(raw);
|
|
@@ -139,10 +232,14 @@ export const useLensZoom = options => {
|
|
|
139
232
|
}
|
|
140
233
|
setZoomSkill(skill);
|
|
141
234
|
return undefined;
|
|
142
|
-
}, [devId, showZoomTurntable, multiZoomDpValue, skipLoad]);
|
|
235
|
+
}, [devId, showZoomTurntable, switchChannelSupported, multiZoomDpValue, skipLoad]);
|
|
143
236
|
|
|
144
|
-
/**
|
|
145
|
-
|
|
237
|
+
/**
|
|
238
|
+
* 多镜头能力就绪:宿主提供 ty.ipc.switchChannel,且 ipc_multi_zoom 解析出的 cnt > 1。
|
|
239
|
+
* 两条缺一个入口都不出现 —— 单镜头设备没有可切的镜头,
|
|
240
|
+
* 老宿主则是切不动(见 isSwitchChannelSupported)。
|
|
241
|
+
*/
|
|
242
|
+
const multiLensReady = showZoomTurntable && switchChannelSupported && lensCount(zoomSkill) > 1;
|
|
146
243
|
|
|
147
244
|
// 手势缩放上限按当前镜头换算后下发给播放器:maxScaleMultiple 是 d 口径,
|
|
148
245
|
// 直接用画面绝对上限(如 6.0)会让画面放到该镜头区间之外。切镜头后跟着更新。
|
|
@@ -238,42 +335,79 @@ export const useLensZoom = options => {
|
|
|
238
335
|
});
|
|
239
336
|
const beginDrag = useMemoizedFn(value => {
|
|
240
337
|
draggingRef.current = true;
|
|
338
|
+
draggingStore.set(devId, true);
|
|
241
339
|
setCurM(value);
|
|
242
|
-
|
|
340
|
+
// 切镜头进行中(拖动跨界会在手指还按着时切):画面归收尾流程管,这里只更新读数
|
|
341
|
+
if (!isSwitching(devId)) previewZoom(value);
|
|
342
|
+
scheduleDragCross(value);
|
|
243
343
|
});
|
|
244
344
|
|
|
245
345
|
/**
|
|
246
|
-
* 镜头已切到 index
|
|
346
|
+
* 镜头已切到 index 后的收尾:把画面落到目标倍率上(**补差值**),读数与 tab 同步。
|
|
347
|
+
*
|
|
247
348
|
* 正常路径由 channelChange 触发(等设备真正切完再动画面,中间态更干净);
|
|
248
349
|
* 宿主不支持该事件时,退回 switchChannel 成功即收尾。
|
|
350
|
+
*
|
|
351
|
+
* @param targetM 目标画面绝对倍率。本端下发的切换都会带(从 pendingSwitch 取),
|
|
352
|
+
* 落位就是用户实际要的那个值:转盘拖到 2.5 就落 2.5、手势停在 3.5 就落 3.5、
|
|
353
|
+
* 触底下切落「上一镜头上限 − 1 step」。**不带**时才归该镜头的光学正倍数 ——
|
|
354
|
+
* 只有外部来源(设备侧 / 其他端切镜头)走这条,那时本端没有目标值可言。
|
|
355
|
+
*
|
|
356
|
+
* 早期设计是「切镜头一律归 1 倍、不补差值」,那会让下切必然掉到新镜头的 basicRatio:
|
|
357
|
+
* 从 3.0 往下拖到 2.5,切到短焦后画面直接变成 1.0,跨度大到像是坏了。
|
|
249
358
|
*/
|
|
250
|
-
const settleLensSwitched = useMemoizedFn(index => {
|
|
251
|
-
var
|
|
252
|
-
const
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
359
|
+
const settleLensSwitched = useMemoizedFn((index, targetM) => {
|
|
360
|
+
var _skill$lenses;
|
|
361
|
+
const skill = skillRef.current;
|
|
362
|
+
const lens = skill === null || skill === void 0 || (_skill$lenses = skill.lenses) === null || _skill$lenses === void 0 ? void 0 : _skill$lenses[index];
|
|
363
|
+
if (!skill || !lens) return;
|
|
364
|
+
// 目标值夹进新镜头区间:跨界落点可能略微越界(容差、浮点毛刺)
|
|
365
|
+
const [lo, hi] = lensRange(skill, index);
|
|
366
|
+
const finalM = targetM == null ? lens.basicRatio : Math.min(hi, Math.max(lo, targetM));
|
|
367
|
+
const d = toScaleMultiple(skill, index, finalM);
|
|
368
|
+
const isBase = d <= D_MIN + D_MATCH_EPS;
|
|
369
|
+
console.log('[scaleMultiple] zoomTurntable 切镜头收尾', `lensIndex=${index}`, `finalM=${finalM.toFixed(2)}`, `d=${d.toFixed(3)}`, isBase ? '(归正倍数)' : '(补差值)', `lastD=${getLastD(devId)}`);
|
|
370
|
+
|
|
371
|
+
// 状态先落地:读数 / tab 立刻对齐,不等画面下发走完 P2P
|
|
372
|
+
syncToLens(index, d);
|
|
257
373
|
markSwitching(-1);
|
|
258
|
-
|
|
259
|
-
|
|
374
|
+
settledAtStore.set(devId, Date.now());
|
|
375
|
+
floorHitStore.delete(devId);
|
|
376
|
+
// 小窗跟最终 d 走:补差值后 d > 1(画面不在正倍数上),小窗该开着 ——
|
|
377
|
+
// 旧实现无条件 applySmallMonitor(false),补差值后就与画面状态不符了
|
|
378
|
+
syncSmallMonitorByD(d);
|
|
260
379
|
event.emit(zoomTurntableLensChange, {
|
|
261
380
|
lensId: lens.id,
|
|
262
381
|
lensIndex: index,
|
|
263
|
-
value:
|
|
382
|
+
value: finalM
|
|
264
383
|
});
|
|
265
384
|
event.emit(zoomTurntableZoomChange, {
|
|
266
|
-
curM:
|
|
267
|
-
d
|
|
385
|
+
curM: finalM,
|
|
386
|
+
d,
|
|
268
387
|
lensIndex: index
|
|
269
388
|
});
|
|
389
|
+
if (isBase) {
|
|
390
|
+
// 归正倍数:与旧行为一致,立即下发
|
|
391
|
+
event.emit(setScaleMultipleEvent, D_MIN);
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
// 补差值:新流刚起来,立刻打 scaleMultiple 有被吞的风险,延一拍;
|
|
395
|
+
// 这段窗口内的复位帧回读由 settleApplyStore 挡掉
|
|
396
|
+
settleApplyStore.set(devId, {
|
|
397
|
+
d,
|
|
398
|
+
expireAt: Date.now() + SETTLE_APPLY_TIMEOUT
|
|
399
|
+
});
|
|
400
|
+
setTimeout(() => {
|
|
401
|
+
console.log('[scaleMultiple] zoomTurntable 补差值下发', d, `lensIndex=${index}`);
|
|
402
|
+
event.emit(setScaleMultipleEvent, d);
|
|
403
|
+
}, POST_SWITCH_APPLY_DELAY);
|
|
270
404
|
});
|
|
271
405
|
|
|
272
406
|
/** 把目标倍率落到指定镜头上:同镜头内下发本地缩放,跨镜头则切物理镜头并归 1 倍 */
|
|
273
407
|
const applyToLens = useMemoizedFn(async (nextIndex, targetM) => {
|
|
274
|
-
var _skill$
|
|
408
|
+
var _skill$lenses2;
|
|
275
409
|
const skill = skillRef.current;
|
|
276
|
-
if (!(skill !== null && skill !== void 0 && (_skill$
|
|
410
|
+
if (!(skill !== null && skill !== void 0 && (_skill$lenses2 = skill.lenses) !== null && _skill$lenses2 !== void 0 && _skill$lenses2[nextIndex])) return;
|
|
277
411
|
setCurM(targetM);
|
|
278
412
|
const prevIndex = lensIndexRef.current;
|
|
279
413
|
if (nextIndex === prevIndex) {
|
|
@@ -290,6 +424,9 @@ export const useLensZoom = options => {
|
|
|
290
424
|
return;
|
|
291
425
|
}
|
|
292
426
|
|
|
427
|
+
// Entry / Dock 同一帧判出同一次切换时只放一次过(读数已在上面各自更新过)
|
|
428
|
+
if (!claimSwitch(devId, nextIndex)) return;
|
|
429
|
+
|
|
293
430
|
// 切物理镜头:从下发到 channelChange 回来这段屏蔽回读
|
|
294
431
|
console.log('[zoomTurntable] 切换镜头', `${prevIndex} -> ${nextIndex}`, `channel=${skill.lenses[nextIndex].id}`, `targetM=${targetM.toFixed(2)}`);
|
|
295
432
|
takePendingSwitch(devId); // 上一次没结算完的直接作废,以最新这次为准
|
|
@@ -305,6 +442,7 @@ export const useLensZoom = options => {
|
|
|
305
442
|
pendingSwitchStore.set(devId, {
|
|
306
443
|
seq,
|
|
307
444
|
index: nextIndex,
|
|
445
|
+
targetM,
|
|
308
446
|
timer: setTimeout(() => {
|
|
309
447
|
pendingSwitchStore.delete(devId);
|
|
310
448
|
markSwitching(-1);
|
|
@@ -328,7 +466,7 @@ export const useLensZoom = options => {
|
|
|
328
466
|
// 那时设备已经真的切过去了,画面不会先跳一下再稳定
|
|
329
467
|
if (!isChannelChangeSupported()) {
|
|
330
468
|
takePendingSwitch(devId);
|
|
331
|
-
settleLensSwitched(nextIndex);
|
|
469
|
+
settleLensSwitched(nextIndex, targetM);
|
|
332
470
|
return;
|
|
333
471
|
}
|
|
334
472
|
|
|
@@ -345,10 +483,49 @@ export const useLensZoom = options => {
|
|
|
345
483
|
// 有到达但没有「镜头切换到位」→ 被 deviceId 或 channel 校验挡掉了
|
|
346
484
|
console.warn('[zoomTurntable] 等 channelChange 超时,按下发结果收尾', `${prevIndex} -> ${nextIndex}`, `channel=${skill.lenses[nextIndex].id}`, `${CHANNEL_SETTLE_TIMEOUT}ms`, `onChannelChange 可用=${isChannelChangeSupported()}`);
|
|
347
485
|
dumpChannelApis();
|
|
348
|
-
settleLensSwitched(nextIndex);
|
|
486
|
+
settleLensSwitched(nextIndex, targetM);
|
|
349
487
|
}, CHANNEL_SETTLE_TIMEOUT);
|
|
350
488
|
});
|
|
351
489
|
|
|
490
|
+
/**
|
|
491
|
+
* 拖动跨镜头临界:过界后**手停住** DRAG_CROSS_SWITCH_DELAY 就地切镜头,画面跟着拖动走。
|
|
492
|
+
*
|
|
493
|
+
* previewZoom 把下发值夹在当前镜头区间内,所以拖过界之后画面是「冻」住的 ——
|
|
494
|
+
* 只等松手才切的话,从 3.0 往下拖到 2.5 的一路上画面纹丝不动,手感像是没响应。
|
|
495
|
+
* 逐帧切也不行:switchChannel 是 P2P、几百毫秒级,逐帧下发会把通道打爆。
|
|
496
|
+
* 折中成 trailing 防抖:值一直在变就一直不切,停住才切。
|
|
497
|
+
*
|
|
498
|
+
* 切换期间 draggingRef 保持 true(见 onChannelChange 里的说明),松手后 applyZoom
|
|
499
|
+
* 再按最终值落一次 —— 那时镜头通常已经对了,走的是同镜头内下发 d 的分支。
|
|
500
|
+
*/
|
|
501
|
+
const scheduleDragCross = useMemoizedFn(value => {
|
|
502
|
+
const clearPending = () => {
|
|
503
|
+
if (dragCrossTimerRef.current) {
|
|
504
|
+
clearTimeout(dragCrossTimerRef.current);
|
|
505
|
+
dragCrossTimerRef.current = undefined;
|
|
506
|
+
}
|
|
507
|
+
};
|
|
508
|
+
const skill = skillRef.current;
|
|
509
|
+
if (!skill) return;
|
|
510
|
+
// 回到当前镜头区间内 / 已在切换中:撤掉待切
|
|
511
|
+
if (matchLens(skill, value) === lensIndexRef.current || isSwitching(devId)) {
|
|
512
|
+
clearPending();
|
|
513
|
+
return;
|
|
514
|
+
}
|
|
515
|
+
clearPending();
|
|
516
|
+
dragCrossTimerRef.current = setTimeout(() => {
|
|
517
|
+
dragCrossTimerRef.current = undefined;
|
|
518
|
+
// 已松手 → 交给 applyZoom 落地,不在这儿抢
|
|
519
|
+
if (!draggingRef.current || isSwitching(devId)) return;
|
|
520
|
+
const cur = skillRef.current;
|
|
521
|
+
if (!cur) return;
|
|
522
|
+
const nextIndex = matchLens(cur, value);
|
|
523
|
+
if (nextIndex === lensIndexRef.current) return;
|
|
524
|
+
console.log('[zoomTurntable] 拖动跨界停留,就地切镜头', `${lensIndexRef.current} -> ${nextIndex}`, `targetM=${value.toFixed(2)}`);
|
|
525
|
+
applyToLens(nextIndex, value);
|
|
526
|
+
}, DRAG_CROSS_SWITCH_DELAY);
|
|
527
|
+
});
|
|
528
|
+
|
|
352
529
|
/**
|
|
353
530
|
* 拖动状态兜底复位。
|
|
354
531
|
*
|
|
@@ -359,11 +536,22 @@ export const useLensZoom = options => {
|
|
|
359
536
|
*/
|
|
360
537
|
const endDrag = useMemoizedFn(() => {
|
|
361
538
|
draggingRef.current = false;
|
|
539
|
+
draggingStore.delete(devId);
|
|
540
|
+
if (dragCrossTimerRef.current) {
|
|
541
|
+
clearTimeout(dragCrossTimerRef.current);
|
|
542
|
+
dragCrossTimerRef.current = undefined;
|
|
543
|
+
}
|
|
362
544
|
});
|
|
363
545
|
|
|
364
546
|
/** 拖动结束:按倍率匹配镜头后落地 */
|
|
365
547
|
const applyZoom = useMemoizedFn(async targetM => {
|
|
366
548
|
draggingRef.current = false;
|
|
549
|
+
draggingStore.delete(devId);
|
|
550
|
+
// 松手即是最终值,待切的那次作废,直接按最终值走
|
|
551
|
+
if (dragCrossTimerRef.current) {
|
|
552
|
+
clearTimeout(dragCrossTimerRef.current);
|
|
553
|
+
dragCrossTimerRef.current = undefined;
|
|
554
|
+
}
|
|
367
555
|
const skill = skillRef.current;
|
|
368
556
|
if (!skill) return;
|
|
369
557
|
await applyToLens(matchLens(skill, targetM), targetM);
|
|
@@ -371,11 +559,12 @@ export const useLensZoom = options => {
|
|
|
371
559
|
|
|
372
560
|
/** 点击倍率 tab:直接切到该镜头并归位到其光学倍数;不走 matchLens,免得容差判到相邻镜头 */
|
|
373
561
|
const switchToLens = useMemoizedFn(async index => {
|
|
374
|
-
var _skill$
|
|
562
|
+
var _skill$lenses3;
|
|
375
563
|
const skill = skillRef.current;
|
|
376
|
-
const lens = skill === null || skill === void 0 || (_skill$
|
|
564
|
+
const lens = skill === null || skill === void 0 || (_skill$lenses3 = skill.lenses) === null || _skill$lenses3 === void 0 ? void 0 : _skill$lenses3[index];
|
|
377
565
|
if (!lens) return;
|
|
378
566
|
draggingRef.current = false;
|
|
567
|
+
draggingStore.delete(devId);
|
|
379
568
|
await applyToLens(index, lens.basicRatio);
|
|
380
569
|
});
|
|
381
570
|
|
|
@@ -406,9 +595,11 @@ export const useLensZoom = options => {
|
|
|
406
595
|
return; // 外部来源的重复事件
|
|
407
596
|
}
|
|
408
597
|
console.log('[zoomTurntable] 镜头切换到位', `${lensIndexRef.current} -> ${index}`, pending ? '(本端下发)' : '(外部来源)');
|
|
409
|
-
//
|
|
410
|
-
|
|
411
|
-
|
|
598
|
+
// 只有外部来源(设备侧 / 其他端)才作废进行中的拖动 —— 读数得以设备为准。
|
|
599
|
+
// 本端下发的那次不能动 draggingRef:拖动跨界会在**手指还按着**的时候切镜头,
|
|
600
|
+
// 置 false 会放开回读屏蔽,回读帧和拖动帧互相打架,指针会抖。
|
|
601
|
+
if (!pending) draggingRef.current = false;
|
|
602
|
+
settleLensSwitched(index, pending === null || pending === void 0 ? void 0 : pending.targetM);
|
|
412
603
|
});
|
|
413
604
|
useEffect(() => {
|
|
414
605
|
// 订没订上必须能从日志看出来:没订上时切镜头一定走超时回滚,两者表象一样
|
|
@@ -434,15 +625,94 @@ export const useLensZoom = options => {
|
|
|
434
625
|
gestureTimerRef.current = setTimeout(() => {
|
|
435
626
|
gestureTimerRef.current = undefined;
|
|
436
627
|
const skill = skillRef.current;
|
|
437
|
-
if (!skill || draggingRef.current || isSwitching(devId)) return;
|
|
628
|
+
if (!skill || draggingRef.current || isDraggingAnywhere(devId) || isSwitching(devId)) return;
|
|
438
629
|
const nextIndex = matchLens(skill, value);
|
|
439
630
|
if (nextIndex === lensIndexRef.current) return;
|
|
440
631
|
console.log('[zoomTurntable] 手势跨临界点,切换镜头', `curM=${value.toFixed(2)}`, `${lensIndexRef.current} -> ${nextIndex}`);
|
|
441
632
|
applyToLens(nextIndex, value);
|
|
442
633
|
}, GESTURE_SWITCH_DEBOUNCE);
|
|
443
634
|
});
|
|
635
|
+
|
|
636
|
+
/**
|
|
637
|
+
* 触底继续缩 → 往下切一个镜头(trailing 防抖,等手指停下再切)。
|
|
638
|
+
*
|
|
639
|
+
* 现象:在非首个镜头上把画面缩到该镜头的光学正倍数后(d = 1,协议下限),用户继续
|
|
640
|
+
* 双指往里收,播放器的 d 已经夹住动不了,画面纹丝不动,读数也停在 basicRatio。
|
|
641
|
+
* 此时 zoomLevelChange 仍然按帧推 d = 1 上来 —— 这就是唯一可用的信号
|
|
642
|
+
* (detail.scaleMode「自由缩放状态标识」实测不准,不能拿来判定)。
|
|
643
|
+
*
|
|
644
|
+
* 落位取「上一个镜头区间上限 − 1 step」(如 3.0 → 2.9):那正是画面能继续变小的第一格,
|
|
645
|
+
* 换算到短焦上是 d ≈ 3.85,用户接着往里收就能顺畅地一路缩到 1.0。
|
|
646
|
+
*/
|
|
647
|
+
const scheduleDownSwitch = useMemoizedFn(() => {
|
|
648
|
+
var _downSwitchAtStore$ge;
|
|
649
|
+
if (Date.now() - ((_downSwitchAtStore$ge = downSwitchAtStore.get(devId)) !== null && _downSwitchAtStore$ge !== void 0 ? _downSwitchAtStore$ge : 0) < DOWN_SWITCH_COOLDOWN) return;
|
|
650
|
+
if (downTimerRef.current) clearTimeout(downTimerRef.current);
|
|
651
|
+
downTimerRef.current = setTimeout(() => {
|
|
652
|
+
var _skill$lenses$target$, _skill$lenses4;
|
|
653
|
+
downTimerRef.current = undefined;
|
|
654
|
+
const skill = skillRef.current;
|
|
655
|
+
const index = lensIndexRef.current;
|
|
656
|
+
if (!skill || index <= 0) return;
|
|
657
|
+
if (draggingRef.current || isDraggingAnywhere(devId) || isSwitching(devId)) return;
|
|
658
|
+
// 防抖这 500ms 里可能又被放大了,切之前再确认画面确实还夹在正倍数上
|
|
659
|
+
if (getLastD(devId) > D_MIN + D_MATCH_EPS) {
|
|
660
|
+
floorHitStore.delete(devId);
|
|
661
|
+
return;
|
|
662
|
+
}
|
|
663
|
+
const target = index - 1;
|
|
664
|
+
const [, upper] = lensRange(skill, target);
|
|
665
|
+
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;
|
|
666
|
+
const targetM = upper - stepOfTarget;
|
|
667
|
+
downSwitchAtStore.set(devId, Date.now());
|
|
668
|
+
floorHitStore.delete(devId);
|
|
669
|
+
console.log('[zoomTurntable] 触底继续缩,向下切镜头', `${index} -> ${target}`, `targetM=${targetM.toFixed(2)}`);
|
|
670
|
+
applyToLens(target, targetM);
|
|
671
|
+
}, GESTURE_SWITCH_DEBOUNCE);
|
|
672
|
+
});
|
|
673
|
+
|
|
674
|
+
/**
|
|
675
|
+
* 触底计数。返回 true 表示已排下切,本帧不再走上界 / 防抖那套判定。
|
|
676
|
+
* 只由驱动方计数 —— 两份 hook 都数会让阈值形同减半。
|
|
677
|
+
*/
|
|
678
|
+
const handleFloorHit = useMemoizedFn(d => {
|
|
679
|
+
var _settledAtStore$get;
|
|
680
|
+
if (!isAutoSwitchDriver) return false;
|
|
681
|
+
// 拖动中:落地由 applyZoom / 跨界停留负责,触底那套整体让路
|
|
682
|
+
if (isDraggingAnywhere(devId)) {
|
|
683
|
+
floorHitStore.delete(devId);
|
|
684
|
+
return false;
|
|
685
|
+
}
|
|
686
|
+
// 不在正倍数上:正常缩放,清计数
|
|
687
|
+
if (d > D_MIN + D_MATCH_EPS) {
|
|
688
|
+
floorHitStore.delete(devId);
|
|
689
|
+
return false;
|
|
690
|
+
}
|
|
691
|
+
// 首个镜头已经是倍率域下界,再缩没有镜头可切
|
|
692
|
+
if (lensIndexRef.current <= 0) return false;
|
|
693
|
+
const now = Date.now();
|
|
694
|
+
// 收尾自身会引发一到两帧 d = 1 回读,静默期内不计数,否则刚切上去又被切回来
|
|
695
|
+
if (now - ((_settledAtStore$get = settledAtStore.get(devId)) !== null && _settledAtStore$get !== void 0 ? _settledAtStore$get : 0) < FLOOR_HIT_SETTLE_GUARD) return false;
|
|
696
|
+
const prev = floorHitStore.get(devId);
|
|
697
|
+
const count = prev && now - prev.lastAt <= FLOOR_HIT_WINDOW ? prev.count + 1 : 1;
|
|
698
|
+
floorHitStore.set(devId, {
|
|
699
|
+
count,
|
|
700
|
+
lastAt: now
|
|
701
|
+
});
|
|
702
|
+
// 每一帧都打:真机上「触底还缩不缩得动」全靠这条日志判断 ——
|
|
703
|
+
// 如果原生只在 d 真正变化时才推事件,这里会永远停在 count=1、下切一次都不会触发,
|
|
704
|
+
// 那就说明这条信号不成立,得改由 APP 侧补「手势下界」参数(协议 §4.3 第 5 条)
|
|
705
|
+
console.log('[zoomTurntable] 触底回读', `count=${count}/${FLOOR_HIT_COUNT}`, `lensIndex=${lensIndexRef.current}`);
|
|
706
|
+
// 第一帧是「正常缩到正倍数」,从第二帧起才是「夹住了还在缩」
|
|
707
|
+
if (count < FLOOR_HIT_COUNT) return false;
|
|
708
|
+
console.log('[zoomTurntable] 画面已夹在正倍数仍有缩放回读,排下切', `count=${count}`);
|
|
709
|
+
scheduleDownSwitch();
|
|
710
|
+
return true;
|
|
711
|
+
});
|
|
444
712
|
const onZoomLevelChange = useMemoizedFn(d => {
|
|
445
713
|
if (!(d > 0)) return;
|
|
714
|
+
// 补差值下发期间原生画面会先复位到 1 倍,那一帧不能让它把读数拽回镜头正倍数
|
|
715
|
+
if (shouldDropSettleEcho(devId, d)) return;
|
|
446
716
|
setLastD(devId, d);
|
|
447
717
|
// 切镜头途中的中间态不算数(收尾时统一关掉),拖动中画面还没动
|
|
448
718
|
if (draggingRef.current || isSwitching(devId)) return;
|
|
@@ -458,6 +728,13 @@ export const useLensZoom = options => {
|
|
|
458
728
|
lensIndex: lensIndexRef.current
|
|
459
729
|
});
|
|
460
730
|
|
|
731
|
+
// 触底继续缩:往下切一个镜头(下界,与下面的上界对称)
|
|
732
|
+
if (handleFloorHit(d)) return;
|
|
733
|
+
if (!isAutoSwitchDriver) return;
|
|
734
|
+
// 拖动中:previewZoom 把下发值夹在当前镜头区间内,回读会一直贴着区间边界,
|
|
735
|
+
// 不让路的话「触到上界立即切」会和拖动跨界停留同时切镜头
|
|
736
|
+
if (isDraggingAnywhere(devId)) return;
|
|
737
|
+
|
|
461
738
|
// 画面已经顶到当前镜头区间的上界(也就是下一个镜头的光学倍数,如 1x 镜头放到 3.0):
|
|
462
739
|
// 这个边界值在归属上属于下一个镜头,必须立刻切过去。
|
|
463
740
|
// 走 trailing 防抖的话,那 500ms 里 tab 会挂着「下一镜头的值」显示在当前镜头上。
|
|
@@ -484,6 +761,8 @@ export const useLensZoom = options => {
|
|
|
484
761
|
useEffect(() => {
|
|
485
762
|
return () => {
|
|
486
763
|
if (gestureTimerRef.current) clearTimeout(gestureTimerRef.current);
|
|
764
|
+
if (downTimerRef.current) clearTimeout(downTimerRef.current);
|
|
765
|
+
if (dragCrossTimerRef.current) clearTimeout(dragCrossTimerRef.current);
|
|
487
766
|
};
|
|
488
767
|
}, []);
|
|
489
768
|
const [min, max] = zoomSkill ? zoomBounds(zoomSkill) : [D_MIN, D_MIN];
|
|
@@ -3,7 +3,7 @@ import { ComponentConfigProps } from '../../interface';
|
|
|
3
3
|
import './zoomTurntableEntry.less';
|
|
4
4
|
type Props = ComponentConfigProps & {
|
|
5
5
|
className?: string;
|
|
6
|
-
/**
|
|
6
|
+
/** 业务侧的功能开关;实际是否渲染仍由宿主 switchChannel 能力 + ipc_multi_zoom 的 cnt 决定 */
|
|
7
7
|
showZoomTurntable?: boolean;
|
|
8
8
|
/** 长按判定时长,默认 500ms */
|
|
9
9
|
longPressDuration?: number;
|
|
@@ -12,7 +12,8 @@ type Props = ComponentConfigProps & {
|
|
|
12
12
|
};
|
|
13
13
|
/**
|
|
14
14
|
* 焦距转盘入口:左下角工具条中的图标,长按弹出转盘。
|
|
15
|
-
*
|
|
15
|
+
* 仅「宿主支持 ty.ipc.switchChannel」且 cnt > 1 的多镜头设备渲染;
|
|
16
|
+
* 单镜头设备倍率交互仍由 RealTimeMagnification 承担。
|
|
16
17
|
*/
|
|
17
18
|
export declare const ZoomTurntableEntry: (props: Props) => React.JSX.Element | null;
|
|
18
19
|
export {};
|
|
@@ -19,7 +19,8 @@ import { FORCE_SHOW_ZOOM_TIP, LONG_PRESS_MS, MOVE_TOLERANCE, TIP_RESHOW_DELAY, g
|
|
|
19
19
|
import './zoomTurntableEntry.less';
|
|
20
20
|
/**
|
|
21
21
|
* 焦距转盘入口:左下角工具条中的图标,长按弹出转盘。
|
|
22
|
-
*
|
|
22
|
+
* 仅「宿主支持 ty.ipc.switchChannel」且 cnt > 1 的多镜头设备渲染;
|
|
23
|
+
* 单镜头设备倍率交互仍由 RealTimeMagnification 承担。
|
|
23
24
|
*/
|
|
24
25
|
export const ZoomTurntableEntry = props => {
|
|
25
26
|
var _props$event;
|