From e0f9f811b7bbba0f781f580f7edc6d84c041026d Mon Sep 17 00:00:00 2001 From: Suzumiya Date: Thu, 17 Sep 2026 19:34:17 +0800 Subject: [PATCH] =?UTF-8?q?=E7=94=9F=E6=88=90=E6=B5=AE=E5=B1=82=E5=B1=85?= =?UTF-8?q?=E4=B8=AD=E5=AE=9A=E4=BD=8D=E4=B8=8E=E7=94=BB=E5=B8=83=E5=AE=89?= =?UTF-8?q?=E5=85=A8=E5=B8=A6=E9=AB=98=E5=BA=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 补回 transform translateX(-50%):锚点给的是占位卡中心,少了它浮层整体右偏半个面板宽 高度只按画布安全带分配:装得下时挂在卡下面,装不下时与卡顶边对齐盖住占位,不再压成一百多像素 reveal 的面板高度按当前缩放从屏幕 px 折回世界坐标,占位与浮层并集参与可见性 浮层宽度并入可见性并集,靠边卡片不再把浮层切掉一半 新增真实矩形模拟用例(画布高 568)核对浮层高度与落点不越出安全带 --- .../resourceCanvasGenerationPanel.css | 2 + ...resourceCanvasGenerationVisibilityModel.ts | 83 +++++++++----- .../src/view/project-development/index.tsx | 102 +++++++++++++----- ...nvasGenerationFloatingPanelChrome.test.tsx | 2 + .../resourceCanvasGenerationLanding.test.tsx | 97 +++++++++++++++++ ...resourceCanvasGenerationVisibility.test.ts | 95 ++++++++-------- 6 files changed, 281 insertions(+), 100 deletions(-) diff --git a/apps/ai-game-creator-shell/src/features/resource-canvas/resourceCanvasGenerationPanel.css b/apps/ai-game-creator-shell/src/features/resource-canvas/resourceCanvasGenerationPanel.css index 89d33eb10..7d19a99ec 100644 --- a/apps/ai-game-creator-shell/src/features/resource-canvas/resourceCanvasGenerationPanel.css +++ b/apps/ai-game-creator-shell/src/features/resource-canvas/resourceCanvasGenerationPanel.css @@ -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; diff --git a/apps/ai-game-creator-shell/src/features/resource-canvas/resourceCanvasGenerationVisibilityModel.ts b/apps/ai-game-creator-shell/src/features/resource-canvas/resourceCanvasGenerationVisibilityModel.ts index f82d1a951..b70d3ac3c 100644 --- a/apps/ai-game-creator-shell/src/features/resource-canvas/resourceCanvasGenerationVisibilityModel.ts +++ b/apps/ai-game-creator-shell/src/features/resource-canvas/resourceCanvasGenerationVisibilityModel.ts @@ -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, + }; } diff --git a/apps/ai-game-creator-shell/src/view/project-development/index.tsx b/apps/ai-game-creator-shell/src/view/project-development/index.tsx index ed5e31867..572b887d9 100644 --- a/apps/ai-game-creator-shell/src/view/project-development/index.tsx +++ b/apps/ai-game-creator-shell/src/view/project-development/index.tsx @@ -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, diff --git a/apps/ai-game-creator-shell/tests/resourceCanvasGenerationFloatingPanelChrome.test.tsx b/apps/ai-game-creator-shell/tests/resourceCanvasGenerationFloatingPanelChrome.test.tsx index 3c197d513..a559ebca6 100644 --- a/apps/ai-game-creator-shell/tests/resourceCanvasGenerationFloatingPanelChrome.test.tsx +++ b/apps/ai-game-creator-shell/tests/resourceCanvasGenerationFloatingPanelChrome.test.tsx @@ -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 拿不到几何时也不能整块垂出画布。 diff --git a/apps/ai-game-creator-shell/tests/resourceCanvasGenerationLanding.test.tsx b/apps/ai-game-creator-shell/tests/resourceCanvasGenerationLanding.test.tsx index 8d1b6b23c..49c7dd962 100644 --- a/apps/ai-game-creator-shell/tests/resourceCanvasGenerationLanding.test.tsx +++ b/apps/ai-game-creator-shell/tests/resourceCanvasGenerationLanding.test.tsx @@ -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( + '.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( + '.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(); + 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); }); + + diff --git a/apps/ai-game-creator-shell/tests/resourceCanvasGenerationVisibility.test.ts b/apps/ai-game-creator-shell/tests/resourceCanvasGenerationVisibility.test.ts index 59349c6f7..d9270e2e6 100644 --- a/apps/ai-game-creator-shell/tests/resourceCanvasGenerationVisibility.test.ts +++ b/apps/ai-game-creator-shell/tests/resourceCanvasGenerationVisibility.test.ts @@ -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); }); });