九宫格面板缩得比边框还小时,中心区域不该翻折

一个背包面板的左边框宽 24、右边框宽 16,展开时一切正常。收起动画把总宽缩到 30,网格仍用 0、24、W-16、W 作为横向切分点,于是得到 0、24、14、30。中心从 24 走回 14,左右边框也重叠了 10 个单位。关闭背面剔除只能让反向区域继续显示,无法把这份网格变回正确的分区。

九宫格面板缩小时的分区保持与翻折风险示意

上图只说明面板缩放时的分区关系,不承担精确尺寸。下面用一个独立 C# 研究工程确定小尺寸行为:源图是一张未旋转、未裁边的矩形贴图,横向 64 像素,左右边框分别占 24 和 16 像素;每布局单位对应一个源像素。位置沿局部轴递增,UV 沿同一方向递增。它验证生成网格之前的分段计算,不假定任何引擎内置九宫格组件采用同样策略。

总宽不足时,三个要求不能同时满足

左右边框原尺寸不变、目标宽度服从布局、各片区不重叠,这三项在 W<40 时无法同时成立。若资产必须保持圆角半径和描边厚度,应由布局限制最小宽度;若收起动画必须经过 30,就需要允许边框变形。本文选择后者,让两侧共享压缩系数,而不是任意截掉一侧。

设换成布局单位后的边框为 a、b,当总和非零时取 k=min(1,W/(a+b))。新的左右宽度是 ka、kb,中心宽度为 W-ka-kb。开篇的系数是 0.75,两侧变成 18 和 12,保留 3:2 比例,中心恰好消失。若分别把每侧限制到 W/2,结果会变成 15 和 15,原图不对称的设计也被改掉了。

正常宽度、错误重叠与比例压缩的切分坐标及 UV

图中错误样本的红色区间是左右边框重叠范围,箭头表示中心逆向;正常与修正样本使用同一布局单位比例。宽度达到 40 时,两条内分界同处 24;继续增大,只增长中间区域。因此本方案在阈值附近位置连续,但导数会改变,不能据此承诺收起动画的视觉速度也连续。

几何边框缩短,源图边框仍要完整取样

源图切分点是 0、24、48、64,归一化后为 0、0.375、0.75、1。面板压缩时,这四个 UV 不随目标边框宽度改变:左侧仍取源图完整的 24 像素范围,只是映射到 18 个布局单位。若把 UV 也乘 0.75,就把“压缩边框”变成了“截取边框的一部分”。

研究工程的 SliceAxis.cs 保存完整核心实现。SliceAxis.Plan 返回位置与 UV 分界,横纵轴各调用一次;QuadCount 展示随后网格遍历使用的非零面积条件。

public readonly record struct AxisCuts(double Length, double Inner0,
    double Inner1, double U0, double U1)
{
    public double[] Positions => [0, Inner0, Inner1, Length];
    public double[] Texcoords => [0, U0, U1, 1];
}

public static class SliceAxis
{
    public static AxisCuts Plan(double length, double sourcePixels,
        double nearPixels, double farPixels, double pixelsPerUnit)
    {
        static bool Valid(double v) => double.IsFinite(v) && v >= 0 && v <= 1e6;
        if (!Valid(length) || !Valid(sourcePixels) || !Valid(nearPixels)
            || !Valid(farPixels) || !Valid(pixelsPerUnit)
            || sourcePixels == 0 || pixelsPerUnit < 1e-6
            || nearPixels + farPixels >= sourcePixels)
            throw new ArgumentOutOfRangeException(nameof(length));
        double borderPixels = nearPixels + farPixels;
        double near, farStart;
        if (borderPixels > 0 && length < borderPixels / pixelsPerUnit)
        {
            // 同比例压缩两侧,使用同一个接缝坐标消除反向中心。
            near = length * (nearPixels / borderPixels);
            farStart = near;
        }
        else
        {
            near = nearPixels / pixelsPerUnit;
            farStart = Math.Max(near, length - farPixels / pixelsPerUnit);
        }
        return new(length, near, farStart,
            nearPixels / sourcePixels, 1 - farPixels / sourcePixels);
    }

    public static int QuadCount(AxisCuts x, AxisCuts y)
    {
        int count = 0;
        double[] px = x.Positions, py = y.Positions;
        for (int j = 0; j < 3; j++)
            for (int i = 0; i < 3; i++)
                if (px[i + 1] > px[i] && py[j + 1] > py[j]) count++;
        return count;
    }
}

压缩分支让两个内分界直接共享同一个值,不分别算完再期待浮点结果刚好相等。正常分支的 Math.Max 防止阈值附近舍入产生负中心。输入同时带入源像素和每单位像素数,使布局单位换算只发生一次;若每单位对应两个像素,同样宽 30 的面板得到 0、12、22、30,此时根本不需要压缩。

二维网格按两轴分段的笛卡尔积生成,每格使用对应的四组位置与 UV。两个轴都正常时最多九格;横轴中心消失后剩六格;两个轴的中心都消失后剩四格。目标宽度为零时没有任何有效格子。代码的数量统计不生成顶点或索引,实际 Mesh 发射器应采用同样的正宽、正高判断再写入三角形。

这里有一个容易遗漏的顶点共享限制:压缩后两个内分界位置相同,UV 却分别是 0.375 和 0.75。左右片区接缝可以共用坐标,不能据此合并成同一个带 UV 的顶点。源图中心被省略后,两侧纹理直接相邻,接缝是否美观取决于资产内容;几何不重叠并不保证颜色连续。

用分段参照检查整个尺寸范围

Program.cs 的 NegativeCenterRegression 固定验证旧实现中心宽为 -10,修正后为零。GridPartition 遍历目标宽度整数 0 至 120、两侧源边框整数 0 至 20,每单位两个像素,共 53,361 组;源宽保持 64,因此每组都有合法源中心。参照用 decimal 算两侧缩放宽度,再对照 double 实现,另外检查分界单调、端点封闭、总宽守恒和 UV 不变。

2026-09-27 在 .NET 9.0.3 下完成 Release 构建,零警告、零错误。将研究工程目录设为 $LAB_ROOT 后执行:

dotnet build "$LAB_ROOT/SliceLab.csproj" -c Release --no-restore
dotnet run --project "$LAB_ROOT/SliceLab.csproj" -c Release --no-build
build_exit=0 run_exit=0
passed=19 failed=0 skipped=0 elapsed_ms=65.2336
grid_samples=53361 max_partition_residual=0
normal_positions=[0,24,84,100]
naive_positions=[0,24,14,30]
compressed_positions=[0,18,18,30]
uv=[0,0.375,0.75,1]
quads_normal_one_collapsed_both_collapsed_zero_width=[9,6,4,0]

新建工程需先完成依赖还原。19 项检查还覆盖零边框、单侧边框、阈值相邻浮点数、非有限输入与非法源切分;网格遍历是其中一项,不另算五万多个独立测试。耗时只统计测试段,不包含构建或进程启动,也不表示实际 UI 绘制成本。

当前接口拒绝源边框总和达到或超过源尺寸。前者没有可拉伸的源中心,后者连源分区都已经重叠;若产品需要无中心的框线资源,应给它独立的资产合同,不能让当前插值路径默默猜测。输入上限和像素密度下限只是这份探针的数值范围,尚未穷举所有浮点组合。旋转图集、像素对齐、过滤、Mask 和 GPU 光栅都没有进入验证。

收起到宽 30 的背包面板现在可以得到不翻折的分段,但内容仍可能放不下。布局系统决定是否允许继续缩小,资产定义源切片,网格计算只执行已经选定的压缩政策;不要让绘制层为了保住边框擅自扩大布局矩形。如果圆角在压缩后不能接受,应回到最小尺寸或隐藏策略,而不是继续修改 UV 掩盖几何约束。