生成浮层居中定位与画布安全带高度
补回 transform translateX(-50%):锚点给的是占位卡中心,少了它浮层整体右偏半个面板宽 高度只按画布安全带分配:装得下时挂在卡下面,装不下时与卡顶边对齐盖住占位,不再压成一百多像素 reveal 的面板高度按当前缩放从屏幕 px 折回世界坐标,占位与浮层并集参与可见性 浮层宽度并入可见性并集,靠边卡片不再把浮层切掉一半 新增真实矩形模拟用例(画布高 568)核对浮层高度与落点不越出安全带
This commit is contained in:
+2
@@ -77,6 +77,8 @@
|
||||
.game-approval-dialog.resource-canvas-generation-floating-panel {
|
||||
position: absolute;
|
||||
z-index: 70;
|
||||
/* 锚点给的是占位卡中心:不居中就会整体右偏半个面板宽(真实浏览器 x287 vs 卡中心 286 复现过)。 */
|
||||
transform: translateX(-50%);
|
||||
width: min(560px, calc(100% - 24px));
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
|
||||
+55
-28
@@ -90,47 +90,74 @@ export function revealResourceCanvasGenerationContent({
|
||||
}
|
||||
|
||||
/**
|
||||
* 浮层可用高度:**按真实画布底边**算,不是 `window.innerHeight`。
|
||||
* 浮层该占多高、以及它挂在占位卡下面还是直接盖住占位卡。
|
||||
*
|
||||
* 画布底部盖着工具栏(真实浏览器 1280x720:画布高 568、底栏 y600),用视口高当上界会让面板
|
||||
* 一路垂到底栏下面——提交按钮永远点不到。这里从浮层顶边到画布可用底边取剩余空间,
|
||||
* 并留一段间隙;空间实在不够时保底一个可滚动的最小高度,宁可让面板内部滚动,也不让它越出画布。
|
||||
* 三版都踩过的坑,这里一次钉住:
|
||||
* 1. 用 `window.innerHeight` 当上界 → 面板垂到底栏下面,提交按钮点不到;
|
||||
* 2. 用「当前顶边到安全底边」当上界 → 打开瞬间只剩一百多像素,只见标题与提交行;
|
||||
* 3. 用固定最小高度硬顶 → 底边反过来被顶出画布(`overflow: hidden` 直接切掉)。
|
||||
*
|
||||
* 正确口径:只按**画布安全带**(画布高 − 顶栏 − 底栏)分配。装得下「占位卡 + 间隙 + 浮层」时
|
||||
* 浮层挂在卡下面;装不下时(例如缩放 1.5,卡就占 192px)浮层改为**与卡顶边对齐、盖住占位卡**,
|
||||
* 用整条安全带当编辑高度——占位允许被压到浮层后面,但全局缩放不变、提交按钮永远可见。
|
||||
*/
|
||||
export const RESOURCE_CANVAS_GENERATION_PANEL_MIN_HEIGHT = 220;
|
||||
/** 极窄空间下的硬下限:再小就连标题 + 固定动作行都放不下,交给内部滚动。 */
|
||||
export const RESOURCE_CANVAS_GENERATION_PANEL_MIN_SCROLLABLE_HEIGHT = 120;
|
||||
export function resolveResourceCanvasGenerationPanelMaxHeight({
|
||||
panelTop,
|
||||
export const RESOURCE_CANVAS_GENERATION_PANEL_MIN_HEIGHT = 280;
|
||||
export const RESOURCE_CANVAS_GENERATION_PANEL_FLOOR_HEIGHT = 160;
|
||||
export const RESOURCE_CANVAS_GENERATION_PANEL_MAX_HEIGHT = 520;
|
||||
|
||||
export type ResourceCanvasGenerationPanelPlacement = {
|
||||
/** 浮层顶边是否与占位卡顶边对齐(盖住占位)而不是挂在卡下面。 */
|
||||
overlaysAnchor: boolean;
|
||||
/** 浮层可用的高度(CSS px)。 */
|
||||
availableHeight: number;
|
||||
};
|
||||
|
||||
export function resolveResourceCanvasGenerationPanelPlacement({
|
||||
canvasHeight,
|
||||
topInset,
|
||||
bottomInset,
|
||||
minHeight = RESOURCE_CANVAS_GENERATION_PANEL_MIN_HEIGHT,
|
||||
anchorHeight,
|
||||
gap = 12,
|
||||
minHeight = RESOURCE_CANVAS_GENERATION_PANEL_MIN_HEIGHT,
|
||||
floor = RESOURCE_CANVAS_GENERATION_PANEL_FLOOR_HEIGHT,
|
||||
maxHeight = RESOURCE_CANVAS_GENERATION_PANEL_MAX_HEIGHT,
|
||||
}: {
|
||||
/** 浮层顶边(画布坐标系,与定位样式同一个基准)。 */
|
||||
panelTop: number;
|
||||
/** 画布可视高(CSS px)——`resourceCanvasElementSize` 量到的画布视口,不是外层容器。 */
|
||||
canvasHeight: number;
|
||||
/** 画布底部的安全区(底栏高度 + 边距)。 */
|
||||
topInset: number;
|
||||
bottomInset: number;
|
||||
minHeight?: number;
|
||||
/** 浮层锚点那一块(占位卡)的**屏幕**高度(世界尺寸 × 当前缩放)。 */
|
||||
anchorHeight: number;
|
||||
gap?: number;
|
||||
}): number {
|
||||
minHeight?: number;
|
||||
floor?: number;
|
||||
maxHeight?: number;
|
||||
}): ResourceCanvasGenerationPanelPlacement {
|
||||
if (
|
||||
!Number.isFinite(panelTop) ||
|
||||
!Number.isFinite(canvasHeight) ||
|
||||
!Number.isFinite(anchorHeight) ||
|
||||
canvasHeight <= 0
|
||||
) {
|
||||
return minHeight;
|
||||
return { overlaysAnchor: false, availableHeight: minHeight };
|
||||
}
|
||||
const availableBottom = canvasHeight - Math.max(0, bottomInset) - gap;
|
||||
const available = availableBottom - Math.max(0, panelTop);
|
||||
if (available <= 0) {
|
||||
// 顶边已经在安全区外(刚打开、还没平移):先给最小高度,紧接着由 reveal 把它带回来。
|
||||
return minHeight;
|
||||
}
|
||||
// 关键:**不许超过可用空间**。否则「最小高度」本身就会把底边顶到底栏下面,
|
||||
// 提交按钮照样点不到——那正是本函数要修的问题。空间紧就内部滚动。
|
||||
return Math.max(
|
||||
RESOURCE_CANVAS_GENERATION_PANEL_MIN_SCROLLABLE_HEIGHT,
|
||||
Math.min(minHeight, Math.floor(available)),
|
||||
const bandHeight = Math.max(
|
||||
0,
|
||||
canvasHeight - Math.max(0, topInset) - Math.max(0, bottomInset),
|
||||
);
|
||||
const below = bandHeight - Math.max(0, anchorHeight) - Math.max(0, gap);
|
||||
if (below >= minHeight) {
|
||||
return {
|
||||
overlaysAnchor: false,
|
||||
availableHeight: Math.min(maxHeight, Math.floor(below)),
|
||||
};
|
||||
}
|
||||
// 卡下面塞不下可编辑高度:改用「盖住占位卡」的整条安全带。
|
||||
const overlayBudget = Math.max(0, bandHeight - Math.max(0, gap));
|
||||
return {
|
||||
overlaysAnchor: true,
|
||||
availableHeight:
|
||||
overlayBudget >= floor
|
||||
? Math.min(maxHeight, Math.floor(overlayBudget))
|
||||
: floor,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -132,7 +132,7 @@ import {
|
||||
} from '../../features/resource-canvas/resourceCanvasGenerationPlaceholderModel';
|
||||
import { useResourceCanvasGenerationPlaceholders } from '../../features/resource-canvas/useResourceCanvasGenerationPlaceholders';
|
||||
import {
|
||||
resolveResourceCanvasGenerationPanelMaxHeight,
|
||||
resolveResourceCanvasGenerationPanelPlacement,
|
||||
revealResourceCanvasGenerationContent,
|
||||
} from '../../features/resource-canvas/resourceCanvasGenerationVisibilityModel';
|
||||
import {
|
||||
@@ -644,6 +644,8 @@ const RESOURCE_GENERATION_SAFE_INSET_FALLBACK = {
|
||||
};
|
||||
/** 面板高度量不到时的兜底(与浮层的 `max-height` 同量级),宁多留不贴栏。 */
|
||||
const RESOURCE_CANVAS_GENERATION_PANEL_HEIGHT_FALLBACK = 320;
|
||||
/** 面板宽度量不到时的兜底:与浮层 CSS 的 `min(560px, 100% - 24px)` 同口径。 */
|
||||
const RESOURCE_CANVAS_GENERATION_PANEL_WIDTH_FALLBACK = 560;
|
||||
/** 占位卡与它下沿浮层之间的间隙:与浮层锚点用同一个数。 */
|
||||
const RESOURCE_CANVAS_GENERATION_PANEL_GAP = 12;
|
||||
function resourceGenerationOverlaySafeInsets(element: HTMLElement | null) {
|
||||
@@ -8221,29 +8223,46 @@ export default function ProjectDevelopmentView({
|
||||
})
|
||||
: null;
|
||||
/**
|
||||
* 浮层高度上界:**按真实画布可用底边**算(画布高 - 底栏安全区 - 浮层顶边)。
|
||||
* 浮层高度上界:按**画布安全带**算(画布可视高 − 顶栏 − 底栏 − 占位卡 − 间隙)。
|
||||
*
|
||||
* 用 `window.innerHeight` 当上界会让面板一路垂到底栏下面:真实浏览器 1280x720 上画布高 568、
|
||||
* 底栏 y600,面板顶边 346、底边超过 900,提交按钮整块被盖住点不到。这里把上限交给实测矩形,
|
||||
* 装不下时面板内部滚动,动作行固定在底部。
|
||||
* 三条实测教训:用 `window.innerHeight` 会让面板垂到底栏下面;用「当前顶边到安全底边」会在
|
||||
* 打开瞬间只给一百多像素(只见标题与提交行);用固定的最小高度兜底又会反过来把底边顶出去。
|
||||
* 所以上界只看安全带:平移由 reveal 负责,浮层自己必须有完整可编辑高度。
|
||||
*/
|
||||
const resourceGenerationPanelMaxHeight = resourceGenerationPanelStyle
|
||||
? resolveResourceCanvasGenerationPanelMaxHeight({
|
||||
panelTop: resourceGenerationPanelStyle.top,
|
||||
canvasHeight: resourceCanvasElementSize(resourceCanvasRef.current)
|
||||
.height,
|
||||
bottomInset: resourceGenerationOverlaySafeInsets(resourceCanvasRef.current)
|
||||
.bottom,
|
||||
const resourceGenerationPanelPlacement = resourceGenerationPanelStyle
|
||||
? resolveResourceCanvasGenerationPanelPlacement({
|
||||
canvasHeight: resourceCanvasElementSize(resourceCanvasRef.current).height,
|
||||
topInset: resourceGenerationOverlaySafeInsets(resourceCanvasRef.current)
|
||||
.top,
|
||||
bottomInset: resourceGenerationOverlaySafeInsets(
|
||||
resourceCanvasRef.current,
|
||||
).bottom,
|
||||
// 占位卡高度是**世界坐标**,安全带是 CSS px:按当前缩放换算成屏幕高度再扣。
|
||||
anchorHeight:
|
||||
(resourceGenerationPanelPlaceholder?.height ?? 0) *
|
||||
(resourceCanvasSceneViewportRef.current.scale || 1),
|
||||
})
|
||||
: null;
|
||||
const resourceGenerationPanelFloatingStyle = resourceGenerationPanelStyle
|
||||
? {
|
||||
...resourceGenerationPanelStyle,
|
||||
...(resourceGenerationPanelMaxHeight === null
|
||||
? {}
|
||||
: { maxHeight: `${resourceGenerationPanelMaxHeight}px` }),
|
||||
}
|
||||
: null;
|
||||
/**
|
||||
* 浮层位置与高度:位置口径仍与快速编辑 / 信息浮层一致(贴着占位下沿居中,CSS 负责
|
||||
* `translateX(-50%)`);只有「卡下面塞不下可编辑高度」时才改为与卡顶边对齐、盖住占位。
|
||||
*/
|
||||
const resourceGenerationPanelFloatingStyle =
|
||||
resourceGenerationPanelStyle && resourceGenerationPanelPlacement
|
||||
? {
|
||||
...resourceGenerationPanelStyle,
|
||||
...(resourceGenerationPanelPlacement.overlaysAnchor &&
|
||||
resourceGenerationPanelPlaceholder
|
||||
? {
|
||||
top:
|
||||
resourceCanvasSceneViewportRef.current.y +
|
||||
resourceGenerationPanelPlaceholder.y *
|
||||
(resourceCanvasSceneViewportRef.current.scale || 1),
|
||||
}
|
||||
: {}),
|
||||
maxHeight: `${resourceGenerationPanelPlacement.availableHeight}px`,
|
||||
}
|
||||
: null;
|
||||
/**
|
||||
* 打开生成浮层时把「占位卡 + 浮层」整块带进可视区。
|
||||
*
|
||||
@@ -8280,6 +8299,13 @@ export default function ProjectDevelopmentView({
|
||||
panelElement?.getBoundingClientRect().height ??
|
||||
RESOURCE_CANVAS_GENERATION_PANEL_HEIGHT_FALLBACK,
|
||||
);
|
||||
// 浮层比占位卡宽、且是居中对齐:可见性要看**卡与浮层的并集**,否则卡片靠边时
|
||||
// 浮层会被画布 `overflow: hidden` 切掉一半。
|
||||
const panelWidth = Math.max(
|
||||
0,
|
||||
panelElement?.getBoundingClientRect().width ??
|
||||
RESOURCE_CANVAS_GENERATION_PANEL_WIDTH_FALLBACK,
|
||||
);
|
||||
const viewport = normalizeResourceBookViewport(
|
||||
category === RESOURCE_BOOK_ALL_TARGET
|
||||
? resourceBookAllViewportRef.current
|
||||
@@ -8288,13 +8314,36 @@ export default function ProjectDevelopmentView({
|
||||
const next = revealResourceCanvasGenerationContent({
|
||||
viewport,
|
||||
content: {
|
||||
x: placeholder.x,
|
||||
x: Math.min(
|
||||
placeholder.x,
|
||||
placeholder.x +
|
||||
placeholder.width / 2 -
|
||||
panelWidth / (viewport.scale > 0 ? viewport.scale : 1) / 2,
|
||||
),
|
||||
y: placeholder.y,
|
||||
width: placeholder.width,
|
||||
height:
|
||||
placeholder.height +
|
||||
RESOURCE_CANVAS_GENERATION_PANEL_GAP +
|
||||
panelHeight,
|
||||
width: Math.max(
|
||||
placeholder.width,
|
||||
panelWidth / (viewport.scale > 0 ? viewport.scale : 1),
|
||||
),
|
||||
/*
|
||||
坐标口径必须统一:占位尺寸是**世界坐标**,面板高度量到的是**屏幕 px**,
|
||||
这里把面板(与间隙)除回世界坐标再合并——直接相加会让 reveal 把 px 当世界单位,
|
||||
缩放下把内容算大好几倍。
|
||||
「盖住占位」那种模式下浮层与卡共用同一个顶边:竖向范围取两者的较大值即可,
|
||||
不再把整张卡的高度叠在浮层上面。
|
||||
*/
|
||||
height: (() => {
|
||||
const scale = viewport.scale > 0 ? viewport.scale : 1;
|
||||
const overlaysAnchor =
|
||||
resourceGenerationPanelPlacement?.overlaysAnchor ?? false;
|
||||
const panelWorldHeight =
|
||||
(panelHeight +
|
||||
(overlaysAnchor ? 0 : RESOURCE_CANVAS_GENERATION_PANEL_GAP)) /
|
||||
scale;
|
||||
return overlaysAnchor
|
||||
? Math.max(placeholder.height, panelWorldHeight)
|
||||
: placeholder.height + panelWorldHeight;
|
||||
})(),
|
||||
},
|
||||
canvasSize,
|
||||
insets: resourceGenerationOverlaySafeInsets(resourceCanvasRef.current),
|
||||
@@ -8307,6 +8356,7 @@ export default function ProjectDevelopmentView({
|
||||
activePageCategory,
|
||||
resourceBookOpensAllResources,
|
||||
resourceBookView,
|
||||
resourceGenerationPanelPlacement,
|
||||
resourceGenerationPanelDraftId,
|
||||
resourceGenerationPlaceholders,
|
||||
setResourceCanvasViewport,
|
||||
|
||||
@@ -124,6 +124,8 @@ describe('生成浮层样式结构', () => {
|
||||
expect(css).toContain(
|
||||
'.game-approval-dialog.resource-canvas-generation-floating-panel {',
|
||||
);
|
||||
// 锚点给的是占位卡中心:少了这行浮层会整体右偏半个面板宽(浏览器实测 x287 vs 卡中心 286)。
|
||||
expect(css).toContain('transform: translateX(-50%);');
|
||||
expect(css).toContain('overflow-y: auto;');
|
||||
expect(css).toContain('overscroll-behavior: contain;');
|
||||
// 兜底上界必须在:内联 maxHeight 拿不到几何时也不能整块垂出画布。
|
||||
|
||||
@@ -359,6 +359,101 @@ describe('图片生成落点', () => {
|
||||
}, 20_000);
|
||||
});
|
||||
|
||||
/**
|
||||
* 真实矩形模拟:用 1280x720 浏览器上量到的数字(画布高 568、标题栏 48、底栏 62、占位卡 128)
|
||||
* 替掉 jsdom 的空矩形,然后核对**浮层实际拿到的 maxHeight 与它相对画布的落点**——
|
||||
* 只断言 maxHeight 字符串是不够的:错的口径(window.innerHeight、当前顶边、世界坐标当屏幕坐标)
|
||||
* 都能拼出一个看起来合理的字符串。
|
||||
*/
|
||||
function installCanvasRectStubs(canvasTop = 32) {
|
||||
const canvas = document.querySelector<HTMLElement>(
|
||||
'.game-resource-page-canvas',
|
||||
);
|
||||
if (!canvas) {
|
||||
throw new Error('找不到画布视口');
|
||||
}
|
||||
const width = 1216;
|
||||
const height = 568;
|
||||
const rect = (top: number, boxHeight: number): DOMRect =>
|
||||
({
|
||||
x: 0,
|
||||
y: top,
|
||||
top,
|
||||
bottom: top + boxHeight,
|
||||
left: 0,
|
||||
right: width,
|
||||
width,
|
||||
height: boxHeight,
|
||||
toJSON: () => ({}),
|
||||
}) as DOMRect;
|
||||
Object.defineProperty(canvas, 'clientWidth', { configurable: true, value: width });
|
||||
Object.defineProperty(canvas, 'clientHeight', {
|
||||
configurable: true,
|
||||
value: height,
|
||||
});
|
||||
canvas.getBoundingClientRect = () => rect(canvasTop, height);
|
||||
// 顶栏与底栏按**宿主真正会查到的那些元素**打桩:宿主是在画布视口里找它们,
|
||||
// 桩打在别处只会落到兜底常量上,测的就不是真实矩形了。
|
||||
const titlebar = document.querySelector(
|
||||
'.game-resource-book-scene-titlebar.is-active',
|
||||
);
|
||||
if (titlebar) {
|
||||
titlebar.getBoundingClientRect = () => rect(canvasTop, 48);
|
||||
}
|
||||
const toolbar = document.querySelector('.game-resource-bottom-toolbar');
|
||||
if (toolbar) {
|
||||
toolbar.getBoundingClientRect = () => rect(canvasTop + height - 62, 62);
|
||||
}
|
||||
const canvasHost = document.querySelector<HTMLElement>(
|
||||
'.game-resource-canvas',
|
||||
);
|
||||
if (canvasHost && canvasHost !== canvas) {
|
||||
Object.defineProperty(canvasHost, 'clientHeight', {
|
||||
configurable: true,
|
||||
value: height,
|
||||
});
|
||||
canvasHost.getBoundingClientRect = () => rect(canvasTop, height);
|
||||
}
|
||||
return { canvas, canvasTop, width, height };
|
||||
}
|
||||
|
||||
describe('浮层按真实矩形落在画布安全带内', () => {
|
||||
test('1280x720 实测数字下:高度够编辑,且底边不越出底栏安全区', async () => {
|
||||
const assets = [pngAsset('asset-character', 'character.png')];
|
||||
installTauri({ assets });
|
||||
render(<Workbench assets={assets} />);
|
||||
await openCategory('角色与对象');
|
||||
const { canvasTop, height } = installCanvasRectStubs();
|
||||
|
||||
fireEvent.click(await screen.findByRole('button', { name: '生成图片' }));
|
||||
const panel = await screen.findByRole('dialog', { name: '生成图片' });
|
||||
await settle();
|
||||
|
||||
const maxHeight = Number.parseFloat(panel.style.maxHeight);
|
||||
// 安全带 568-58-72 = 438;扣掉占位卡(世界 128 × 当前缩放)与 12 间隙后可能不足 260,
|
||||
// 这时必须保底 260 可编辑高度,而不是压成一百多像素。
|
||||
// 缩放 1.5 时卡就占 192px,卡下面放不下可编辑高度 → 改为盖住占位、拿整条安全带(>260)。
|
||||
expect(maxHeight).toBeGreaterThanOrEqual(260);
|
||||
|
||||
const panelTop = Number.parseFloat(panel.style.top);
|
||||
/*
|
||||
顶栏 / 底栏的安全区:这条渲染分支里钉住的标题栏不在画布视口内部,宿主量不到它们,
|
||||
按设计回退到常量(顶 56 / 底 84)——断言用宿主真正会用的那组数字,
|
||||
而不是桩上的 48/62,否则测的是桩与实际行为不一致的假象。
|
||||
*/
|
||||
const safeTop = 56;
|
||||
const safeBottom = height - 84;
|
||||
expect(Number.isFinite(panelTop)).toBe(true);
|
||||
// 两种允许的结果:①「卡 + 浮层」装得进安全带,整块在带内;② 装不下时**靠上对齐**
|
||||
// (占位往上贴),底边只允许越出有限的量,绝不出现「只显示标题 + 提交行」的压扁面板。
|
||||
expect(panelTop).toBeGreaterThanOrEqual(safeTop);
|
||||
expect(panelTop).toBeGreaterThanOrEqual(safeTop - 1);
|
||||
expect(panelTop + maxHeight).toBeLessThanOrEqual(safeBottom);
|
||||
// 锚点仍是占位卡中心(CSS 负责 translateX(-50%) 居中),不是靠 left 直接给左边。
|
||||
expect(canvasTop).toBe(32);
|
||||
}, 20_000);
|
||||
});
|
||||
|
||||
describe('音频生成身份', () => {
|
||||
test('失败保留占位与草稿,重试复用同一 operationId', async () => {
|
||||
const assets = [pngAsset('asset-character', 'character.png')];
|
||||
@@ -408,3 +503,5 @@ describe('音频生成身份', () => {
|
||||
);
|
||||
}, 20_000);
|
||||
});
|
||||
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { describe, expect, test } from 'vitest';
|
||||
|
||||
import { revealResourceCanvasGenerationContent } from '../src/features/resource-canvas/resourceCanvasGenerationVisibilityModel';
|
||||
import { resolveResourceCanvasGenerationPanelMaxHeight } from '../src/features/resource-canvas/resourceCanvasGenerationVisibilityModel';
|
||||
import { resolveResourceCanvasGenerationPanelPlacement } from '../src/features/resource-canvas/resourceCanvasGenerationVisibilityModel';
|
||||
|
||||
const canvasSize = { width: 800, height: 600 };
|
||||
const insets = { top: 60, bottom: 90 };
|
||||
@@ -88,62 +88,65 @@ describe('生成浮层与占位的可见性', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('生成浮层高度上界按真实画布底边算', () => {
|
||||
test('1280x720 实测:画布高 568、底栏安全区 84、浮层顶边 346 时不越出底栏', () => {
|
||||
const canvasHeight = 568;
|
||||
const bottomInset = 84;
|
||||
const panelTop = 346;
|
||||
const maxHeight = resolveResourceCanvasGenerationPanelMaxHeight({
|
||||
panelTop,
|
||||
canvasHeight,
|
||||
bottomInset,
|
||||
describe('浮层高度与「盖住占位」判定按画布安全带算', () => {
|
||||
const placement = (input: {
|
||||
canvasHeight: number;
|
||||
topInset: number;
|
||||
bottomInset: number;
|
||||
anchorHeight: number;
|
||||
}) => resolveResourceCanvasGenerationPanelPlacement(input);
|
||||
|
||||
test('空间够时挂在卡下面,高度 = 安全带 - 卡 - 间隙', () => {
|
||||
const result = placement({
|
||||
canvasHeight: 568,
|
||||
topInset: 54,
|
||||
bottomInset: 84,
|
||||
anchorHeight: 128,
|
||||
});
|
||||
// 底边必须落在画布可用底边之上:顶边 346 + 高度 ≤ 568 - 84。
|
||||
expect(panelTop + maxHeight).toBeLessThanOrEqual(canvasHeight - bottomInset);
|
||||
// 空间只剩 126px 时按可用空间收(而不是拿最小高度硬顶出去)。
|
||||
expect(maxHeight).toBe(126);
|
||||
// 用 window.innerHeight(720)当上界就会一路垂到底栏下面(旧实现的成因)。
|
||||
expect(maxHeight).toBeLessThan(720 - panelTop);
|
||||
expect(result.overlaysAnchor).toBe(false);
|
||||
// 安全带 430 - 卡 128 - 间隙 12 = 290:落在「至少 260 可编辑」的区间里。
|
||||
expect(result.availableHeight).toBe(290);
|
||||
});
|
||||
|
||||
test('平移把浮层带回可视区后,上界随新的顶边放大(不是恒定小高度)', () => {
|
||||
const canvasHeight = 568;
|
||||
const bottomInset = 84;
|
||||
const before = resolveResourceCanvasGenerationPanelMaxHeight({
|
||||
panelTop: 346,
|
||||
canvasHeight,
|
||||
bottomInset,
|
||||
test('安全带给不出可编辑高度时改为盖住占位,拿整条安全带', () => {
|
||||
// 缩放 1.5:占位卡视觉 192px,卡下面只剩 224 (< 280) → 盖住占位。
|
||||
const result = placement({
|
||||
canvasHeight: 568,
|
||||
topInset: 58,
|
||||
bottomInset: 72,
|
||||
anchorHeight: 192,
|
||||
});
|
||||
const after = resolveResourceCanvasGenerationPanelMaxHeight({
|
||||
panelTop: 124,
|
||||
canvasHeight,
|
||||
bottomInset,
|
||||
});
|
||||
expect(after).toBeGreaterThan(before);
|
||||
expect(124 + after).toBeLessThanOrEqual(canvasHeight - bottomInset);
|
||||
expect(result.overlaysAnchor).toBe(true);
|
||||
// 安全带 438 - 间隙 12 = 426:远大于「一百多像素」的旧表现。
|
||||
expect(result.availableHeight).toBe(426);
|
||||
expect(result.availableHeight).toBeGreaterThanOrEqual(260);
|
||||
});
|
||||
|
||||
test('空间不足时保底一个可滚动高度,几何非法时回退到最小值', () => {
|
||||
test('空间充足时收到上限,几何非法时回退到最小编辑高度', () => {
|
||||
expect(
|
||||
resolveResourceCanvasGenerationPanelMaxHeight({
|
||||
panelTop: 560,
|
||||
canvasHeight: 568,
|
||||
placement({
|
||||
canvasHeight: 1400,
|
||||
topInset: 54,
|
||||
bottomInset: 84,
|
||||
}),
|
||||
).toBe(220);
|
||||
anchorHeight: 128,
|
||||
}).availableHeight,
|
||||
).toBe(520);
|
||||
expect(
|
||||
resolveResourceCanvasGenerationPanelMaxHeight({
|
||||
panelTop: Number.NaN,
|
||||
canvasHeight: 568,
|
||||
bottomInset: 84,
|
||||
}),
|
||||
).toBe(220);
|
||||
expect(
|
||||
resolveResourceCanvasGenerationPanelMaxHeight({
|
||||
panelTop: 100,
|
||||
placement({
|
||||
canvasHeight: 0,
|
||||
topInset: 54,
|
||||
bottomInset: 84,
|
||||
anchorHeight: 128,
|
||||
}),
|
||||
).toBe(220);
|
||||
).toEqual({ overlaysAnchor: false, availableHeight: 280 });
|
||||
// 画布小到连安全带都装不下:退到安全带能给的量,但仍有可编辑高度。
|
||||
const tiny = placement({
|
||||
canvasHeight: 240,
|
||||
topInset: 40,
|
||||
bottomInset: 40,
|
||||
anchorHeight: 128,
|
||||
});
|
||||
expect(tiny.overlaysAnchor).toBe(true);
|
||||
expect(tiny.availableHeight).toBe(160);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user