Skip to content

Commit cab5fb8

Browse files
authored
修复第五天教程中角色设置二级菜单重排后的位置同步问题:设置面板会避开任务栏,二级菜单会随面板重新定位,猫爪指针也会同步移动到新的二级菜单位… (#2092)
* 修复第五天教程中角色设置二级菜单重排后的位置同步问题:设置面板会避开任务栏,二级菜单会随面板重新定位,猫爪指针也会同步移动到新的二级菜单位置,并补充对应回归测试。 * Handle day5 settings panel review feedback
1 parent d5c8e0c commit cab5fb8

5 files changed

Lines changed: 264 additions & 5 deletions

File tree

static/avatar-popup-common.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -273,13 +273,13 @@
273273
const currentTop = toNumber(popup.style.top, 0);
274274
let nextTop = currentTop;
275275
if (popupRect.bottom > screenHeight - bottomMargin) {
276-
nextTop -= (popupRect.bottom - (screenHeight - bottomMargin));
276+
nextTop -= toLocalCssPx(popupRect.bottom - (screenHeight - bottomMargin), sidePanelScale);
277277
}
278278
popup.style.top = `${nextTop}px`;
279279

280280
popupRect = popup.getBoundingClientRect();
281281
if (popupRect.top < topMargin) {
282-
popup.style.top = `${toNumber(popup.style.top, 0) + (topMargin - popupRect.top)}px`;
282+
popup.style.top = `${toNumber(popup.style.top, 0) + toLocalCssPx(topMargin - popupRect.top, sidePanelScale)}px`;
283283
}
284284

285285
return { opensLeft };

static/tutorial-settings-tour-flow.test.cjs

Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -316,6 +316,165 @@ test('SettingsTourFlow owns linear day four gaze follow scene body', async () =>
316316
]);
317317
});
318318

319+
test('SettingsTourFlow refreshes visible day five character panel before panic narration', async () => {
320+
const calls = [];
321+
const characterSettingsPanel = { id: 'character-settings-panel' };
322+
const characterSettingsButton = { id: 'character-settings-button' };
323+
const director = {
324+
sceneRunId: 17,
325+
destroyed: false,
326+
angryExitTriggered: false,
327+
currentStep: 'day5-panic',
328+
overlay: {
329+
clearActionSpotlight() {
330+
calls.push(['clear-action']);
331+
},
332+
clearPersistentSpotlight() {
333+
calls.push(['clear-persistent']);
334+
}
335+
},
336+
prepareNarration(scene) {
337+
calls.push(['prepare', scene.id]);
338+
return { text: 'line', voiceKey: 'voice', canHandleSceneButtons: false, actionWaitPromise: null };
339+
},
340+
getCharacterSettingsSidePanel() {
341+
calls.push(['get-panel']);
342+
return characterSettingsPanel;
343+
},
344+
isElementVisible(panel) {
345+
calls.push(['visible', panel.id]);
346+
return true;
347+
},
348+
ensureAvatarFloatingSettingsSidePanel(panelId) {
349+
calls.push(['ensure-panel', panelId]);
350+
return Promise.resolve(characterSettingsPanel);
351+
},
352+
getDay5CharacterSettingsButtonTarget() {
353+
calls.push(['get-button']);
354+
return characterSettingsButton;
355+
},
356+
refreshAvatarFloatingSettingsPanelLayout(panel) {
357+
calls.push(['refresh-layout', panel.id]);
358+
},
359+
applyGuideHighlights(config) {
360+
calls.push(['highlight', config.key, config.primary.id, config.persistent.id]);
361+
},
362+
moveCursorToElement(element, durationMs, options) {
363+
const normalizedOptions = options || {};
364+
calls.push(['move', element.id, durationMs, normalizedOptions.exactDuration]);
365+
return Promise.resolve(true);
366+
},
367+
enableInterrupts(step) {
368+
calls.push(['interrupts', step]);
369+
},
370+
createNarrationPromise(scene, text, voiceKey) {
371+
calls.push(['narration', scene.id, text, voiceKey]);
372+
return Promise.resolve();
373+
},
374+
getAvatarFloatingNarrationDurationMs(voiceKey, text) {
375+
calls.push(['duration', voiceKey, text]);
376+
return 900;
377+
},
378+
getElementRect(target) {
379+
calls.push(['rect', target.id]);
380+
return { left: 10, top: 20, width: 100, height: 200 };
381+
},
382+
runSettingsPeekPanicPerformance(options) {
383+
calls.push(['panic', options.targetRect.width, options.totalDurationMs, options.runId]);
384+
return Promise.resolve();
385+
},
386+
isStopping() {
387+
return false;
388+
},
389+
collapseCharacterSettingsSidePanel() {
390+
calls.push(['collapse-character']);
391+
},
392+
finalizeScene(sceneRunId, options) {
393+
calls.push(['finalize', sceneRunId, options.index, options.total]);
394+
return Promise.resolve(true);
395+
}
396+
};
397+
const flow = new SettingsTourFlow(director);
398+
399+
const result = await flow.play({ id: 'day5_character_panic' }, {
400+
sceneRunId: 17,
401+
index: 2,
402+
total: 4
403+
});
404+
405+
assert.equal(result, true);
406+
assert.deepEqual(calls, [
407+
['prepare', 'day5_character_panic'],
408+
['get-panel'],
409+
['visible', 'character-settings-panel'],
410+
['get-button'],
411+
['refresh-layout', 'character-settings-panel'],
412+
['highlight', 'day5_character_panic-character-settings-panel', 'character-settings-panel', 'character-settings-button'],
413+
['move', 'character-settings-panel', 0, true],
414+
['interrupts', 'day5-panic'],
415+
['narration', 'day5_character_panic', 'line', 'voice'],
416+
['duration', 'voice', 'line'],
417+
['rect', 'character-settings-panel'],
418+
['panic', 100, 900, 17],
419+
['clear-action'],
420+
['clear-persistent'],
421+
['collapse-character'],
422+
['finalize', 17, 2, 4]
423+
]);
424+
assert.equal(calls.some((call) => call[0] === 'ensure-panel'), false);
425+
});
426+
427+
test('SettingsTourFlow stops day five panic scene after stale async panel ensure', async () => {
428+
const calls = [];
429+
const characterSettingsPanel = { id: 'character-settings-panel' };
430+
const director = {
431+
sceneRunId: 17,
432+
destroyed: false,
433+
angryExitTriggered: false,
434+
currentStep: 'day5-panic',
435+
prepareNarration(scene) {
436+
calls.push(['prepare', scene.id]);
437+
return { text: 'line', voiceKey: 'voice', canHandleSceneButtons: false, actionWaitPromise: null };
438+
},
439+
getCharacterSettingsSidePanel() {
440+
calls.push(['get-panel']);
441+
return null;
442+
},
443+
ensureAvatarFloatingSettingsSidePanel(panelId) {
444+
calls.push(['ensure-panel', panelId]);
445+
this.sceneRunId = 18;
446+
return Promise.resolve(characterSettingsPanel);
447+
},
448+
getDay5CharacterSettingsButtonTarget() {
449+
calls.push(['get-button']);
450+
return { id: 'character-settings-button' };
451+
},
452+
refreshAvatarFloatingSettingsPanelLayout(panel) {
453+
calls.push(['refresh-layout', panel && panel.id]);
454+
},
455+
applyGuideHighlights(config) {
456+
calls.push(['highlight', config.key]);
457+
},
458+
isStopping() {
459+
return false;
460+
}
461+
};
462+
const flow = new SettingsTourFlow(director);
463+
464+
const result = await flow.play({ id: 'day5_character_panic' }, {
465+
sceneRunId: 17,
466+
index: 2,
467+
total: 4
468+
});
469+
470+
assert.equal(result, false);
471+
assert.deepEqual(calls, [
472+
['prepare', 'day5_character_panic'],
473+
['get-panel'],
474+
['ensure-panel', 'character-settings']
475+
]);
476+
});
477+
319478
test('SettingsTourFlow delegates narration and finalize to the director', async () => {
320479
const calls = [];
321480
const director = {

static/tutorial/core/settings-tour-flow.js

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -401,6 +401,9 @@
401401
return false;
402402
}
403403
await director.openSettingsPanel();
404+
if (typeof director.positionManagedPanelNow === 'function') {
405+
director.positionManagedPanelNow('settings');
406+
}
404407
if (this.isSceneStale(sceneRunId)) {
405408
return false;
406409
}
@@ -424,6 +427,9 @@
424427

425428
characterSettingsPanel = await director.ensureAvatarFloatingSettingsSidePanel('character-settings')
426429
|| characterSettingsPanel;
430+
if (typeof director.refreshAvatarFloatingSettingsPanelLayout === 'function') {
431+
director.refreshAvatarFloatingSettingsPanelLayout(characterSettingsPanel);
432+
}
427433
await this.tourPanel(scene, sceneRunId, characterSettingsPanel, narrationPromise, {
428434
key: scene.id + '-character-settings-panel',
429435
persistent: settingsButton || null
@@ -439,15 +445,36 @@
439445
const narration = this.prepareNarration(scene);
440446
const { text, voiceKey } = narration;
441447

442-
const characterSettingsPanel = director.getCharacterSettingsSidePanel()
443-
|| await director.ensureAvatarFloatingSettingsSidePanel('character-settings');
448+
let characterSettingsPanel = director.getCharacterSettingsSidePanel();
449+
const hasVisibleCharacterPanel = characterSettingsPanel && (
450+
typeof director.isElementVisible !== 'function'
451+
|| director.isElementVisible(characterSettingsPanel)
452+
);
453+
if (!hasVisibleCharacterPanel) {
454+
characterSettingsPanel = await director.ensureAvatarFloatingSettingsSidePanel('character-settings')
455+
|| characterSettingsPanel;
456+
if (this.isSceneStale(sceneRunId)) {
457+
return false;
458+
}
459+
}
444460
const characterSettingsButton = director.getDay5CharacterSettingsButtonTarget();
461+
if (characterSettingsPanel && typeof director.refreshAvatarFloatingSettingsPanelLayout === 'function') {
462+
director.refreshAvatarFloatingSettingsPanelLayout(characterSettingsPanel);
463+
}
445464
if (characterSettingsPanel) {
446465
director.applyGuideHighlights({
447466
key: scene.id + '-character-settings-panel',
448467
persistent: characterSettingsButton || null,
449468
primary: characterSettingsPanel
450469
});
470+
if (typeof director.moveCursorToElement === 'function') {
471+
await director.moveCursorToElement(characterSettingsPanel, 0, {
472+
exactDuration: true
473+
});
474+
if (this.isSceneStale(sceneRunId)) {
475+
return false;
476+
}
477+
}
451478
}
452479
director.enableInterrupts(director.currentStep);
453480

static/tutorial/yui-guide/director.js

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6049,6 +6049,31 @@
60496049
return hidden;
60506050
}
60516051

6052+
positionAvatarFloatingSidePanelNow(panel) {
6053+
const targetPanel = panel || null;
6054+
const anchor = targetPanel && targetPanel._anchorElement ? targetPanel._anchorElement : null;
6055+
const popupUi = window.AvatarPopupUI || null;
6056+
if (!targetPanel || !anchor || !popupUi || typeof popupUi.positionSidePanel !== 'function') {
6057+
return false;
6058+
}
6059+
6060+
try {
6061+
popupUi.positionSidePanel(targetPanel, anchor);
6062+
return true;
6063+
} catch (error) {
6064+
console.warn('[YuiGuide] positionAvatarFloatingSidePanelNow 失败:', error);
6065+
return false;
6066+
}
6067+
}
6068+
6069+
refreshAvatarFloatingSettingsPanelLayout(panel) {
6070+
const popupPositioned = this.positionManagedPanelNow('settings');
6071+
const sidePanelPositioned = panel && this.isElementVisible(panel)
6072+
? this.positionAvatarFloatingSidePanelNow(panel)
6073+
: false;
6074+
return popupPositioned || sidePanelPositioned;
6075+
}
6076+
60526077
forceHideAvatarFloatingGuideManagedSurfaces() {
60536078
this.forceHideManagedPanel('settings');
60546079
this.forceHideManagedPanel('agent');
@@ -6077,6 +6102,9 @@
60776102
return false;
60786103
}
60796104
const targetAnchor = anchor || panel._anchorElement || null;
6105+
if (targetAnchor) {
6106+
this.refreshAvatarFloatingSettingsPanelLayout(panel);
6107+
}
60806108
this.collapseAvatarFloatingSidePanelsExcept(panel);
60816109
if (typeof panel._expand === 'function') {
60826110
if (panel._hoverCollapseTimer) {
@@ -6106,12 +6134,17 @@
61066134
if (!opened || this.isStopping()) {
61076135
return null;
61086136
}
6137+
this.positionManagedPanelNow('settings');
61096138
const panel = await this.waitForElement(() => this.getAvatarFloatingSidePanel(type), 1200);
61106139
if (!panel) {
61116140
return null;
61126141
}
61136142
this.sidebarPauseController.trackPanel(panel);
6114-
return (await this.expandAvatarFloatingSidePanel(panel, panel._anchorElement || null)) ? panel : null;
6143+
const expanded = await this.expandAvatarFloatingSidePanel(panel, panel._anchorElement || null);
6144+
if (expanded) {
6145+
this.refreshAvatarFloatingSettingsPanelLayout(panel);
6146+
}
6147+
return expanded ? panel : null;
61156148
}
61166149

61176150
async ensureAvatarFloatingAgentSidePanel(toggleId) {

static/tutorial/yui-guide/page-handoff.js

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -629,6 +629,43 @@
629629
return !!(sidePanel && sidePanel.style.display === 'flex' && sidePanel.style.opacity !== '0');
630630
}
631631

632+
function collapseSettingSidePanels() {
633+
document.querySelectorAll([
634+
'[data-neko-sidepanel-type="chat-settings"]',
635+
'[data-neko-sidepanel-type="animation-settings"]',
636+
'[data-neko-sidepanel-type="character-settings"]',
637+
'[data-neko-sidepanel-type="interval-proactive-chat"]',
638+
'[data-neko-sidepanel-type="interval-proactive-vision"]'
639+
].join(',')).forEach(function (panel) {
640+
if (!panel) return;
641+
if (panel._hoverCollapseTimer) {
642+
clearTimeout(panel._hoverCollapseTimer);
643+
panel._hoverCollapseTimer = null;
644+
}
645+
if (panel._collapseTimeout) {
646+
clearTimeout(panel._collapseTimeout);
647+
panel._collapseTimeout = null;
648+
}
649+
if (panel._expandFrameId) {
650+
const cancelFrame = window.cancelAnimationFrame || function () {};
651+
cancelFrame(panel._expandFrameId);
652+
panel._expandFrameId = null;
653+
}
654+
if (typeof panel._stopHoverPointerTracking === 'function') {
655+
panel._stopHoverPointerTracking();
656+
}
657+
if (typeof panel._collapse === 'function') {
658+
panel._collapse();
659+
return;
660+
}
661+
panel.style.transition = 'none';
662+
panel.style.opacity = '0';
663+
panel.style.display = 'none';
664+
panel.style.pointerEvents = 'none';
665+
panel.style.transition = '';
666+
});
667+
}
668+
632669
function getAgentSidePanelAction(toggleId, actionId) {
633670
if (!toggleId || !actionId) return null;
634671
return document.getElementById('neko-sidepanel-action-' + toggleId + '-' + actionId);
@@ -1012,6 +1049,8 @@
10121049

10131050
return openSettingsPanel().then(function (opened) {
10141051
if (!opened) return false;
1052+
collapseSettingSidePanels();
1053+
positionFloatingPopupNow('settings', prefix);
10151054

10161055
var el = document.getElementById(prefix + '-menu-' + menuId);
10171056
if (!el) {
@@ -1022,6 +1061,7 @@
10221061
if (typeof el.scrollIntoView === 'function') {
10231062
el.scrollIntoView({ block: 'nearest', behavior: 'instant' });
10241063
}
1064+
positionFloatingPopupNow('settings', prefix);
10251065

10261066
return true;
10271067
});

0 commit comments

Comments
 (0)