Skip to content

UV 图集与文字方向不变量 ​

UV 真源:packages/dieline/src/uv.ts;断言真源:packages/dieline/test/uv.test.ts。 折叠真源:packages/dieline/src/fold3d.ts 的 foldRectangularDieline(dieline, atlas) → FoldedModel(含 flapClosure), 断言真源:packages/dieline/test/fold3d.test.ts;渲染层:apps/web/src/three/PackPreview.ts(P1-04,状态见文末)。 对应任务 P1-02,ADR-0003 的"每面板一个 mesh face,自带 UV 子矩形"。

口径(背下来,别猜) ​

text
展开图平面:x 右、y 下,单位 mm
3D world:x 右、y 上、z 朝正面观察者;front 在 z = 0,盒体向 −z 延伸
图集 UV:u = x / sheetW,v = 1 − y / sheetH     (three.js CanvasTexture flipY = true)

uv.ts 里存的 uvRect.y 与 canvas 同向(向下),方便 2D 直接取样;给 three.js 用时换算 v = 1 − uvRect.y − uvRect.height。正因为这套 flipY 口径,展开图上"正立"的内容 (文字向上 = −y)贴到 face 后在 3D 里依旧正立、不镜像 —— 这是本模块唯一被反复测试的不变量。

数据结构 ​

名称关键键约束
uvRectSchemax, y, width, height归一化 0…1;width/height 必须 >0
faceSpecSchemawidthMm, heightMm, role, outsideVisible贴图长宽比必须与 face 一致,否则文字被拉伸
panelUvSchemapanelId, faceIndex, panelRectMm, uvRect, facepanelRectMm 是面板 bbox(mm)
uvAtlasSchemadielineId, sheetMm, panels(≥1)一个刀模一份图集

buildUvAtlas(dieline) 对非轴对齐矩形面板直接抛错(面板 … 非轴对齐矩形,暂不支持其 UV 布局), 宁可失败也不产出错误的 UV。当前只支持 rectangular 族。

两个取样函数:uvFromPanelPoint(atlas, panelId, px, py)(面板内 mm 点 → 图集 u/v)、 uvDirection(atlas, panelId, dx, dy)(面板内方向 → du/dv)。

checkUvInvariants 的 6 条断言(返回错误数组,空=通过) ​

#断言为什么是生产问题
1faceIndex 全表唯一重复=两个面板抢同一个 3D face,贴图互相覆盖
2uvRect 不越出图集(容差 1e-9)越界会采到隔壁面板的像素
3UV 长宽比 == face 长宽比(相对容差 1e-6)拉伸会让文字/条码失真,是印前事故
4UV 映射行列式 > 0负号=镜像
5outsideVisible 的面:内容向上方向 (0,−1) 映射后 dv < 0dv ≥ 0 即倒置(flipY 后要朝 3D 上方)
6面板两两不重叠(两轴重叠均 > 1e-6 才算重叠,允许共边)重叠=贴图串味

outsideVisible 在 buildUvAtlas 里的定义是 role === 'side' —— 只有四壁是"外侧可见、要求文字正立不镜像"的面, 顶/底摇盖不参与第 5 条判定。

折叠链测试断言了什么 ​

packages/dieline/test/uv.test.ts 里有一个测试专用的折叠模拟器:把展开图平面点用仿射帧 p_world = M·(x_flat, y_flat) + t 表示,每步折叠是绕世界折线的纯 Rodrigues 旋转(±90°,符号由"折叠后远离方向 必须等于盒型拓扑规定的 target"反推,杜绝手填符号出错)。

它断言:

  1. 固定 4 组参数 + 100 组随机参数全部通过 checkUvInvariants(种子写死在测试里,可复现)。
  2. UV 精确换算:每个面板 uvRect 与 bbox / sheet 在 1e-12 内相等。
  3. 正立不镜像:外侧可见面上 (0,−1) → dv < 0,(1,0) → du > 0,面板中心点落在子矩形内部。
  4. 折叠链 3D 一致性:四壁折叠后位置、上方向(世界 +y)、外法线(right × up)与解析盒体吻合; 顶盖共面于 y = h、底盖共面于 y = 0,每块摇盖覆盖跨度正好 D/2,四块在盒顶中线相遇(这就是"四摇盖"的定义) —— 相遇=盖满,不是留缝,层数差异见 摇盖闭合:双层台阶,不是开口; 胶舌落在 x = 0 平面、z ∈ [−glueMm, 0]、y 跨度 = height。
  5. 折接缝连续:12 条接缝(front→right、right→back、back→left、glue_flap→front、四组侧板→顶盖、四组侧板→底盖) 在 t = 0 / 0.5 / 1 三个位置上的对应点折叠后落在同一世界点(1e-6)。
  6. 拒绝非矩形面板:手动扭曲一个顶点后 buildUvAtlas 抛 非轴对齐矩形。

复跑:

sh
pnpm --config.verify-deps-before-run=false test packages/dieline/test/uv.test.ts

摇盖闭合:双层台阶,不是开口 ​

RSC 四摇盖的每一块深度都固定是盒深的一半(packages/dieline/src/rectangular.mjs 里 flap = D / 2; packages/dieline/src/fold3d.ts 的 deriveBox 在 摇盖高度 ≠ 盒深的一半 时直接抛 四盖无法在中线闭合)。 折好之后顶面不是"留一条宽 = 宽 − 深 的缝",而是盖满、但层数不均:

摇盖伸多少折好后的位置
front_top / back_top(front_bottom / back_bottom 同理)各 D/2,横跨整个盒宽 W在盒顶(盒底)中线相遇 → 该面没有开口
left_top / right_top各 D/2,压在前后摇盖下面沿宽方向两端各 D/2 是双层,中间只剩单层
  • singleLayerMm = max(0, W − D):中间那条只有单层纸的带,仍然要靠胶带或内衬,不许写成"已密封/自动密封";
  • doubleLayerMm = min(W, D):两端双层台阶的宽度;
  • 恒等式 singleLayerMm + doubleLayerMm === W:既不留缝,也不多余重叠;
  • W = D(正方形盒)时四盖交于中心一点,singleLayerMm = 0。

这套口径的唯一真源是 FoldedModel.flapClosure(旧字段名 openingsMm 会被读成"盒顶开了条口",与实物不符,已改掉):

ts
// rectangularBox({ width: 80, height: 120, depth: 50, glueMm: 15 }) → 纸面 275×170 mm、13 块面板
foldRectangularDieline(dl).flapClosure
// → { closedByFrontBack: true, singleLayerMm: 30, doubleLayerMm: 50 }   // 30 + 50 === W(80)

packages/dieline/test/fold3d.test.ts 对固定 4 组参数逐个断言上面三条(含恒等式);另外 100 组随机 W/H/D/G 走 foldInvariantViolations(其中包含摇盖必须盖满整幅宽度、侧盖必须落在前后盖覆盖范围内); 再加一组"把图集 UV 冒充局部 UV"的破坏性用例,确认检查器真的抓得住,而不是只在全绿时才绿。

共面重合处只靠 polygonOffset 排"谁压谁" ​

折叠必然产生两类共面重合:顶/底两层摇盖同在 y = H/y = 0,以及 glue_flap 折进盒内后与左墙同在 x = 0。 apps/web/src/three/PackPreview.ts 先用 resolveCoplanarZOrder 把重合面板分组,然后只给材质加 three.js polygonOffset 深度提示:

  • 顶/底:横跨长边的 front_top / back_top(底盖同理)取 polygonOffsetFactor = -1 在外面, left_top / right_top 取 +1 被压在下;
  • 侧面:左墙 left 在外面,glue_flap 取 +1 贴在它内侧(真实盒子就是粘在一起的);
  • 顶点坐标一行未动、没有塞任何假厚度:盒顶那条中线缝与两端台阶是刀模决定的实物形态,不是渲染 bug。

复跑(纯 CPU 断言 + 浏览器像素级验收):

sh
pnpm --config.verify-deps-before-run=false test packages/dieline/test/fold3d.test.ts apps/web/test/pack-preview.test.ts
node tools/verify-3d-preview.mjs    # 证据写到 docs/verification/three-preview/

诚实边界 ​

  • 折叠链是纯数学模拟器测试,验证的是"几何 + UV 折叠后自洽、文字方向正确",不等于肉眼验收过的 3D 效果。 P1-04(Three.js 预览)的代码已在 apps/web/src/three/PackPreview.ts 落地:每面板一张 canvas 纹理、 脏区只重传改动那块、静止帧零上传零 draw call、几何与折叠全部取自 @apw/dieline 真源; tools/verify-3d-preview.mjs 在本机(ANGLE Metal / Apple M5 Max 真实硬解)跑过 40 项像素级验收, 证据在 docs/verification/three-preview/:四壁贴图四角标记逐点不镜像不倒置、只改一个面板时像素变化范围完全落在该面板投影框内、 静止 30 帧零上传零 draw call、共面处不串色、拖拽 120 帧 p95 0.1ms。 边界照旧:像素验收用的是纯色底 + 角标记的功能夹具,不代表真实 artwork 的美观度; p95 帧时间是本机单次测量、还没进 CI 阈值;单盒型族以外的折叠(异形/自动底)仍未实现。
  • 纸厚折叠、动态折叠(拖动改变盒深)、自动 UV 属 PX-01(外部立项,不进本报价)。
  • uvRotateQuarter / mirrorU 字段在 panelSchema 与 artworkMappingSchema 里都有, 当前生成器恒为 0 / false;真要放倒置或镜像面板,必须先补上对应的方向测试。
  • 3D 材质(纸张/哑光/亮膜/金属)只影响预览,绝不改写印刷源图层(ADR-0003)。

交付前请核对文字、字体、结构与印厂要求,并保留确认版本。