Files
AIC-Project/docs/superpowers/plans/2026-07-17-rizomuv-unwrap.md
T

613 lines
24 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# RizomUV UV 展开接入实现计划
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** `model_decimate.py` 默认用 RizomUVRizomUVLink 驱动)做 UV 自动切缝/展开/打包,不可用或质量门不达标自动回退 seam/smart。
**Architecture:** 三步管线——Blender 减面(`none` 模式不展 UV)→ 系统 Python 经官方 RizomUVLink 驱动 rizomuv.exeAutoSeam→Cut→Unfold→Pack→Save 覆盖 FBX)→ Blender `bl_uvgate.py` 量 UV 指标做质量门,不达标就地 seam 重展。设计文档:`docs/superpowers/specs/2026-07-17-rizomuv-unwrap-design.md`
**Tech Stack:** Python 3.12(系统)、RizomUVLinkRizomUV 2025 安装自带,MIT`rizomuvlink_python312.pyd`)、Blender 5.0 headless。
**已核实的 API 事实**(来自 `C:\Program Files\RizomUV2025\RizomUVLink\RizomUVLinkBase.py` docstring 与 `doc/index.html`):
- `CRizomUVLink()` 方法:`RunRizomUV(exePath=...)`(启动实例并连接,返回端口)、`Load/Select/Cut/Unfold/Pack/Save/Quit(params)`;异常类型 `CZEx`
- 自动切缝 = `Select` 边工作集 + `Auto` 表:`Auto.QuasiDevelopable`(标准版叫 MOSAIC`Developability` 0~1 为分割力度)、`Auto.HandleCutter`(切柄)、`Auto.PipesCutter`(连洞)、`Auto.SkeletonUnoverlap`(防重叠后处理)、`Auto.FlatteningMode`0=unfold)、`Auto.Quality`
- `Cut``{"PrimType": "Edge", "WorkingSet": "Visible"}` 按选中边切缝
- `Unfold` 参数:`BorderIntersections``TriangleFlips`bool,防边界交叉/翻转)、`Iterations`
- `Pack` 参数:顶层 `RootGroup`/`WorkingSet`/`ProcessTileSelection`/`Translate`/`RecursionDepth``Global` 表内 `PaddingSize`(岛间距,UV 单位)、`MarginSize`tile 边距,UV 单位)、`MapResolution`int)、`Rotate.Step`(度)、`Scaling.Mode`int 枚举,顺序 0=OFF、1=SET_3D_AREA、2=AVG_RATIO_OF_MOVABLE_ISLANDS、3=AVG_RATIO_OF_GROUP;文档明言首次展开后建议 AVG_RATIO_OF_GROUP
- 注册表查找(官方 README):`HKLM\SOFTWARE\Rizom Lab\RizomUV VS RS 202X.Y` 的默认值键 `rizomuv.exe`——**实测该值是不带后缀的安装前缀**(如 `C:\Program Files\RizomUV2025\rizomuv`),官方代码只取 `dirname` 用,须拼回 `rizomuv.exe`
- 本机安装:`C:\Program Files\RizomUV2025\rizomuv.exe`
**风险提示(执行者注意)**`Select.Auto`/`Pack.Global` 的个别键名若被 RizomUV 拒绝(CZEx 报 unknown parameter),以 `link.Select.__doc__` / `link.Pack.__doc__` 打印出的参数名为准就地修正,不要猜。
---
### Task 1: bl_decimate.py——质量门纯函数 `uv_gate_ok` + `none` 展开模式
**Files:**
- Modify: `Tools/ModelTranslator/bl_decimate.py`(阈值常量附近 + `_do_unwrap` + `main`
- Test: `Tools/ModelTranslator/tests/test_bl_decimate.py`
- [ ] **Step 1: 写失败测试**
`tests/test_bl_decimate.py` 末尾(`if __name__` 之前,若无该块则文件末尾)追加:
```python
class TestUvGateOk(unittest.TestCase):
def test_clean_passes(self):
self.assertTrue(bd.uv_gate_ok(0.0, 0.0))
def test_boundary_passes(self):
self.assertTrue(bd.uv_gate_ok(bd.UV_FLIP_TOL, bd.UV_OVERLAP_TOL))
def test_flip_exceeds_fails(self):
self.assertFalse(bd.uv_gate_ok(0.03, 0.0))
def test_overlap_exceeds_fails(self):
self.assertFalse(bd.uv_gate_ok(0.0, 0.09))
def test_overlap_none_treated_as_zero(self):
self.assertTrue(bd.uv_gate_ok(0.01, None))
```
- [ ] **Step 2: 跑测试确认失败**
```bash
cd "D:/UD/AI/AIC#Project/Tools/ModelTranslator" && python -m unittest tests.test_bl_decimate -v 2>&1 | tail -15
```
预期:5 个新用例 ERROR`AttributeError: module 'bl_decimate' has no attribute 'uv_gate_ok'`),旧用例全过。
- [ ] **Step 3: 实现 `uv_gate_ok` 并让 `_do_unwrap` 复用**
`bl_decimate.py``UV_OVERLAP_TOL` 常量定义之后加:
```python
def uv_gate_ok(flipped, overlap):
"""UV 质量门:翻转与重叠占比都在阈值内。overlap 为 Noneop 不可用)按 0 处理。"""
return flipped <= UV_FLIP_TOL and (overlap or 0.0) <= UV_OVERLAP_TOL
```
`_do_unwrap` 中原判断:
```python
m = _collect_uv_metrics(obj)
overlap = m["overlap"] or 0.0
if m["flipped"] <= UV_FLIP_TOL and overlap <= UV_OVERLAP_TOL:
```
改为:
```python
m = _collect_uv_metrics(obj)
if uv_gate_ok(m["flipped"], m["overlap"]):
```
- [ ] **Step 4: 跑测试确认通过**
```bash
cd "D:/UD/AI/AIC#Project/Tools/ModelTranslator" && python -m unittest tests.test_bl_decimate -v 2>&1 | tail -5
```
预期:`OK`
- [ ] **Step 5: 加 `none` 展开模式**
`bl_decimate.py``main()` 中,`uv_info = _do_unwrap(obj, unwrap_mode, warnings)` 一行改为:
```python
if unwrap_mode == "none":
# rizom 管线:跳过展开,UV 由后续 RizomUV 步骤生成
_clear_uv_layers(obj.data)
uv_info = {"mode": "none", "islands": 0, "fill": 0.0,
"flipped": 0.0, "overlap": None}
else:
uv_info = _do_unwrap(obj, unwrap_mode, warnings)
```
文件头 docstring 的调用说明 `[seam|smart]` 改为 `[seam|smart|none]`
- [ ] **Step 6: 冒烟验证 none 模式**
```bash
cd "D:/UD/AI/AIC#Project/Tools/ModelTranslator" && "D:/tools/blender-5.0.0-windows-x64/blender.exe" -b --factory-startup --python bl_decimate.py -- src/store.fbx /tmp/store_none.fbx 3000 none 2>&1 | grep MT_SUMMARY
```
预期:`"uv": {"mode": "none", ...}`,无报错。
- [ ] **Step 7: Commit**
```bash
cd "D:/UD/AI/AIC#Project" && git add Tools/ModelTranslator/bl_decimate.py Tools/ModelTranslator/tests/test_bl_decimate.py && git commit -m "ModelTranslator: 质量门抽纯函数 uv_gate_ok + 减面 none 模式(rizom 管线前置)"
```
---
### Task 2: rizom_unwrap.py——exe 发现(纯逻辑 + 注册表)
**Files:**
- Create: `Tools/ModelTranslator/rizom_unwrap.py`
- Test: `Tools/ModelTranslator/tests/test_rizom_unwrap.py`
- [ ] **Step 1: 写失败测试**
新建 `tests/test_rizom_unwrap.py`
```python
import os
import sys
import unittest
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
import rizom_unwrap as ru
class TestCandidatePaths(unittest.TestCase):
def test_priority_order(self):
self.assertEqual(ru.candidate_paths("a.exe", "b.exe", "c.exe"),
["a.exe", "b.exe", "c.exe", ru.DEFAULT_RIZOM])
def test_none_entries_skipped(self):
self.assertEqual(ru.candidate_paths(None, None, None), [ru.DEFAULT_RIZOM])
def test_env_only(self):
self.assertEqual(ru.candidate_paths(None, "b.exe", None),
["b.exe", ru.DEFAULT_RIZOM])
class TestFindRizomuv(unittest.TestCase):
def test_missing_exe_raises(self):
os.environ.pop("RIZOMUV_EXE", None)
with self.assertRaises(ru.RizomError):
ru.find_rizomuv("Z:/no/such/rizomuv.exe", use_registry=False,
default="Z:/no/default.exe")
if __name__ == "__main__":
unittest.main()
```
- [ ] **Step 2: 跑测试确认失败**
```bash
cd "D:/UD/AI/AIC#Project/Tools/ModelTranslator" && python -m unittest tests.test_rizom_unwrap -v 2>&1 | tail -8
```
预期:`ModuleNotFoundError: No module named 'rizom_unwrap'`
- [ ] **Step 3: 实现路径发现**
新建 `rizom_unwrap.py`
```python
"""RizomUV 自动展开:官方 RizomUVLink 驱动 rizomuv.exe 做 AutoSeam/Unfold/Pack。
仅在系统 Python 中使用(不进 Blender);任何不可用/失败抛 RizomError
由 model_decimate.py 回退 Blender seam 展开。"""
import os
import sys
import threading
DEFAULT_RIZOM = r"C:\Program Files\RizomUV2025\rizomuv.exe"
MAP_RES = 2048 # Pack 目标贴图分辨率
PADDING_PX = 4 # 岛间距(像素)= bl_decimate.PACK_MARGIN(0.002) * 2048
TIMEOUT_S = 300 # 单条命令超时
class RizomError(Exception):
"""RizomUV 不可用或处理失败(上层据此回退)。"""
def candidate_paths(cli_arg=None, env_val=None, reg_val=None, default=DEFAULT_RIZOM):
"""rizomuv.exe 候选路径,优先级:CLI 参数 > 环境变量 > 注册表 > 默认安装位置。"""
return [p for p in (cli_arg, env_val, reg_val, default) if p]
def registry_rizom_path():
"""官方 README 的注册表查找(HKLM\\SOFTWARE\\Rizom Lab\\RizomUV VS RS 202X.Y),
新版本优先;找不到或非 Windows 返回 None。"""
try:
import winreg
except ImportError:
return None
for i in range(9, 1, -1):
for j in range(10, -1, -1):
if i == 2 and j < 2:
continue
key_path = "SOFTWARE\\Rizom Lab\\RizomUV VS RS 202%d.%d" % (i, j)
try:
key = winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, key_path)
return winreg.QueryValue(key, "rizomuv.exe")
except OSError:
continue
return None
def find_rizomuv(cli_arg=None, use_registry=True, default=DEFAULT_RIZOM):
"""返回第一个真实存在的 rizomuv.exe;都不存在抛 RizomError。"""
reg = registry_rizom_path() if use_registry else None
cands = candidate_paths(cli_arg, os.environ.get("RIZOMUV_EXE"), reg, default)
for p in cands:
if os.path.isfile(p):
return p
raise RizomError("找不到 rizomuv.exe(尝试过:%s" % "; ".join(cands))
```
- [ ] **Step 4: 跑测试确认通过**
```bash
cd "D:/UD/AI/AIC#Project/Tools/ModelTranslator" && python -m unittest tests.test_rizom_unwrap -v 2>&1 | tail -5
```
预期:`OK`4 个用例)。
- [ ] **Step 5: Commit**
```bash
cd "D:/UD/AI/AIC#Project" && git add Tools/ModelTranslator/rizom_unwrap.py Tools/ModelTranslator/tests/test_rizom_unwrap.py && git commit -m "ModelTranslator: rizom_unwrap exe 发现(CLI>env>注册表>默认,官方 README 口径)"
```
---
### Task 3: rizom_unwrap.py——RizomUVLink 展开驱动
**Files:**
- Modify: `Tools/ModelTranslator/rizom_unwrap.py`(文件末尾追加)
无法对真实 RizomUV 进程写单测;本任务以「对 store 低模跑通全流程」为验收(Step 3)。
- [ ] **Step 1: 实现驱动**
`rizom_unwrap.py` 末尾追加:
```python
def _import_link(exe):
"""从 RizomUV 安装目录动态引入官方 RizomUVLink 模块(随安装自带,MIT)。"""
link_dir = os.path.join(os.path.dirname(exe), "RizomUVLink")
if not os.path.isdir(link_dir):
raise RizomError("RizomUVLink 目录不存在:%s" % link_dir)
if link_dir not in sys.path:
sys.path.insert(0, link_dir)
try:
import RizomUVLink
return RizomUVLink
except Exception as e: # pyd 与 Python 版本不匹配等
raise RizomError("RizomUVLink 导入失败:%s" % e)
def _call(label, fn, params, timeout=TIMEOUT_S):
"""带超时执行一条 RizomUV 命令;超时或 CZEx 抛 RizomError。"""
out, err = [], []
def run():
try:
out.append(fn(params))
except Exception as e:
err.append(e)
t = threading.Thread(target=run, daemon=True)
t.start()
t.join(timeout)
if t.is_alive():
raise RizomError("%s 超时(%ds" % (label, timeout))
if err:
raise RizomError("%s 失败:%s" % (label, err[0]))
return out[0] if out else None
def rizom_unwrap(fbx_path, exe, map_res=MAP_RES, padding_px=PADDING_PX):
"""AutoSeam 全自动切缝 -> Unfold -> Pack -> 覆盖保存 fbx_path。失败抛 RizomError。"""
mod = _import_link(exe)
link = mod.CRizomUVLink()
try:
link.RunRizomUV(exePath=exe)
except Exception as e:
raise RizomError("RizomUV 启动失败(license/环境):%s" % e)
fbx_abs = os.path.abspath(fbx_path)
pad_uv = float(padding_px) / map_res
try:
_call("Load", link.Load, {"File.Path": fbx_abs, "File.XYZ": True})
# AutoSeamMOSAIC(QuasiDevelopable) + 切柄/连洞 + 防重叠后处理
_call("Select(AutoSeam)", link.Select, {
"PrimType": "Edge", "WorkingSet": "Visible",
"Select": True, "ResetBefore": True,
"Auto": {
"QuasiDevelopable": {"Developability": 0.5, "IslandPolyNBMin": 1,
"AreaMinRatio": 0.0, "FitCones": False,
"Straighten": True},
"HandleCutter": True,
"PipesCutter": True,
"SkeletonUnoverlap": {},
"FlatteningMode": 0,
"FlatteningUnfoldParams": {"BorderIntersections": True,
"TriangleFlips": True},
"StoreCoordsUVW": True,
"Quality": 0.5,
}})
_call("Cut", link.Cut, {"PrimType": "Edge", "WorkingSet": "Visible"})
_call("Unfold", link.Unfold, {"PrimType": "Edge", "WorkingSet": "Visible",
"BorderIntersections": True,
"TriangleFlips": True})
# Scaling.Mode=3 (AVG_RATIO_OF_GROUP):官方文档建议首次展开后用该模式统一密度
_call("Pack", link.Pack, {
"RootGroup": "RootGroup", "WorkingSet": "Visible",
"ProcessTileSelection": False, "RecursionDepth": 1,
"Translate": True,
"Global": {"MapResolution": map_res,
"PaddingSize": pad_uv, "MarginSize": pad_uv,
"Rotate": {"Step": 90.0},
"Scaling": {"Mode": 3}}})
_call("Save", link.Save, {"File.Path": fbx_abs})
finally:
try:
link.Quit({})
except Exception:
pass
```
- [ ] **Step 2: 确认既有单测未被破坏**
```bash
cd "D:/UD/AI/AIC#Project/Tools/ModelTranslator" && python -m unittest tests.test_rizom_unwrap tests.test_bl_decimate 2>&1 | tail -3
```
预期:`OK`
- [ ] **Step 3: 真机冒烟(临时脚本,不入库)**
```bash
cd "D:/UD/AI/AIC#Project/Tools/ModelTranslator" && cp src/store_low.fbx /tmp/rz_smoke.fbx && python -c "
from rizom_unwrap import find_rizomuv, rizom_unwrap
rizom_unwrap('/tmp/rz_smoke.fbx', find_rizomuv())
print('SMOKE OK')"
```
预期:RizomUV 窗口短暂出现后关闭,打印 `SMOKE OK``/tmp/rz_smoke.fbx` 被覆盖(mtime 更新)。
若某条命令 CZEx 报未知参数:打印对应 `link.<方法>.__doc__` 查正确键名后修正代码,禁止瞎猜重试。
- [ ] **Step 4: Commit**
```bash
cd "D:/UD/AI/AIC#Project" && git add Tools/ModelTranslator/rizom_unwrap.py && git commit -m "ModelTranslator: RizomUVLink 驱动 AutoSeam/Unfold/Pack(带 300s 超时,失败抛 RizomError"
```
---
### Task 4: bl_uvgate.py——UV 质量门(不达标就地 seam 重展)
**Files:**
- Create: `Tools/ModelTranslator/bl_uvgate.py`
纯函数(门限判断)已在 Task 1 覆盖单测;本脚本是 Blender 编排层,以 Step 2/3 冒烟为验收。
- [ ] **Step 1: 实现**
新建 `bl_uvgate.py`
```python
"""Blender 内运行:校验低模 FBX 的 UV 质量门;无 UV 或不达标就地 seam 重展并覆盖导出。
调用:blender -b --factory-startup --python bl_uvgate.py -- <low.fbx> [mode_label]
mode_label 仅用于日志/回退警告中标注上游展开方式(默认 rizom)。"""
import os
import sys
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
def main():
import bpy
import json
from bl_decimate import (join_meshes, uv_gate_ok, _collect_uv_metrics,
_do_unwrap)
argv = sys.argv[sys.argv.index("--") + 1:]
fbx = argv[0]
mode_label = argv[1] if len(argv) > 1 else "rizom"
warnings = []
bpy.ops.wm.read_factory_settings(use_empty=True)
bpy.ops.import_scene.fbx(filepath=fbx)
meshes = [o for o in bpy.data.objects if o.type == 'MESH']
if not meshes:
print("MT_SUMMARY " + json.dumps({"error": "FBX 中没有 mesh"},
ensure_ascii=False))
return
obj = join_meshes(meshes)
if obj.data.uv_layers:
m = _collect_uv_metrics(obj)
if uv_gate_ok(m["flipped"], m["overlap"]):
m["mode"] = mode_label
print("MT_SUMMARY " + json.dumps(
{"uv": m, "reexported": False, "warnings": warnings},
ensure_ascii=False))
return
warnings.append("%s UV 质量不达标(翻转 %.1f%% 重叠 %s),回退 seam"
% (mode_label, m["flipped"] * 100,
"%.1f%%" % (m["overlap"] * 100)
if m["overlap"] is not None else "未知"))
else:
warnings.append("%s 输出无 UV,回退 seam" % mode_label)
m = _do_unwrap(obj, "seam", warnings) # 内部自带 smart 回退
if m["mode"] == "seam":
m["mode"] = "seam_fallback"
bpy.ops.object.select_all(action='DESELECT')
obj.select_set(True)
bpy.ops.export_scene.fbx(filepath=os.path.abspath(fbx), use_selection=True,
path_mode='STRIP', embed_textures=False)
print("MT_SUMMARY " + json.dumps(
{"uv": m, "reexported": True, "warnings": warnings},
ensure_ascii=False))
if __name__ == "__main__" and "--" in sys.argv:
main()
```
- [ ] **Step 2: 冒烟——达标路径**
用 Task 3 冒烟产物(RizomUV 已展开的 FBX):
```bash
cd "D:/UD/AI/AIC#Project/Tools/ModelTranslator" && "D:/tools/blender-5.0.0-windows-x64/blender.exe" -b --factory-startup --python bl_uvgate.py -- /tmp/rz_smoke.fbx rizom 2>&1 | grep MT_SUMMARY
```
预期:`"mode": "rizom"``"reexported": false`,翻转/重叠数值在阈值内。
- [ ] **Step 3: 冒烟——回退路径(无 UV)**
```bash
cd "D:/UD/AI/AIC#Project/Tools/ModelTranslator" && "D:/tools/blender-5.0.0-windows-x64/blender.exe" -b --factory-startup --python bl_decimate.py -- src/store.fbx /tmp/gate_noneuv.fbx 3000 none 2>&1 | grep MT_SUMMARY && "D:/tools/blender-5.0.0-windows-x64/blender.exe" -b --factory-startup --python bl_uvgate.py -- /tmp/gate_noneuv.fbx rizom 2>&1 | grep MT_SUMMARY
```
预期:第二条输出 `"mode": "seam_fallback"`(或 `smart_fallback`)、`"reexported": true`、警告含"输出无 UV,回退 seam"。
- [ ] **Step 4: Commit**
```bash
cd "D:/UD/AI/AIC#Project" && git add Tools/ModelTranslator/bl_uvgate.py && git commit -m "ModelTranslator: bl_uvgate UV 质量门——rizom 产物校验,无 UV/不达标就地 seam 重展"
```
---
### Task 5: model_decimate.py——rizom 默认编排
**Files:**
- Modify: `Tools/ModelTranslator/model_decimate.py`
- [ ] **Step 1: 实现编排**
`model_decimate.py` 全文改为:
```python
"""FBX 减面 CLI:减到指定三角面数并重展 UV(默认 RizomUV 自动展开),输出 <名>_low.fbx。
用法:python model_decimate.py src/well1500.fbx --tris 5000 [-o dir]
[--unwrap rizom|seam|smart] [--rizom exe] [--blender exe]"""
import argparse
import os
from mt_run import find_blender, run_blender_script
def main():
ap = argparse.ArgumentParser(description=__doc__)
ap.add_argument("input", help="FBX 文件")
ap.add_argument("--tris", type=int, required=True, help="目标三角面数")
ap.add_argument("-o", "--out", default=None, help="输出目录(默认源文件同目录)")
ap.add_argument("--unwrap", choices=("rizom", "seam", "smart"), default="rizom",
help="UV 展开:rizom=RizomUV 自动切缝(默认,不可用回退 seam),"
"seam=锐边接缝整岛展开,smart=Smart UV Project")
ap.add_argument("--rizom", default=None, help="rizomuv.exe 路径(默认自动发现)")
ap.add_argument("--blender", default=None)
args = ap.parse_args()
if args.tris <= 0:
ap.error("--tris 必须为正整数")
blender = find_blender(args.blender)
name = os.path.splitext(os.path.basename(args.input))[0]
outdir = args.out or os.path.dirname(os.path.abspath(args.input))
out_fbx = os.path.join(outdir, name + "_low.fbx")
if args.unwrap == "rizom":
# 三步:Blender 减面(不展UV-> RizomUV 展开 -> Blender 质量门(不达标就地 seam 重展)
s = run_blender_script(blender, "bl_decimate.py",
[args.input, out_fbx, str(args.tris), "none"])
warnings = list(s["warnings"])
from rizom_unwrap import RizomError, find_rizomuv, rizom_unwrap
try:
rizom_unwrap(out_fbx, find_rizomuv(args.rizom))
except RizomError as e:
warnings.append("RizomUV 不可用:%s" % e)
g = run_blender_script(blender, "bl_uvgate.py", [out_fbx, "rizom"])
s["uv"] = g["uv"]
s["warnings"] = warnings + g["warnings"]
else:
s = run_blender_script(blender, "bl_decimate.py",
[args.input, out_fbx, str(args.tris), args.unwrap])
print("== %s: %d -> %d 面(目标 %d-> %s" %
(s["src"], s["tris_before"], s["tris_after"], s["target"], out_fbx))
u = s["uv"]
overlap = "%.1f%%" % (u["overlap"] * 100) if u["overlap"] is not None else "n/a"
print(" UV: 模式=%s 岛数=%d 利用率=%.1f%% 翻转=%.1f%% 重叠=%s" %
(u["mode"], u["islands"], u["fill"] * 100, u["flipped"] * 100, overlap))
for w in s["warnings"]:
print(" [警告] %s" % w)
if __name__ == "__main__":
main()
```
- [ ] **Step 2: 全量单测**
```bash
cd "D:/UD/AI/AIC#Project/Tools/ModelTranslator" && python -m unittest discover -s tests -p "test_*.py" 2>&1 | tail -3
```
预期:`OK`
- [ ] **Step 3: 端到端冒烟——rizom 默认路径**
```bash
cd "D:/UD/AI/AIC#Project/Tools/ModelTranslator" && python model_decimate.py src/store10k.fbx --tris 10000
```
预期:`UV: 模式=rizom ...`,无"回退"警告;记录岛数/利用率/翻转/重叠,与旧 smart_fallback 基线(岛 1405 / 利用率 36.4%)对比,利用率应明显更高。
- [ ] **Step 4: 端到端冒烟——回退路径**
```bash
cd "D:/UD/AI/AIC#Project/Tools/ModelTranslator" && python model_decimate.py src/store.fbx --tris 3000 --rizom Z:/no/such.exe
```
预期:警告含"RizomUV 不可用"`UV: 模式=seam_fallback ...` 且指标达标(store 3000 面 seam 基线:岛 388 / 利用率 54.9% / 翻转 0.2% / 重叠 6.2%)。
- [ ] **Step 5: Commit**
```bash
cd "D:/UD/AI/AIC#Project" && git add Tools/ModelTranslator/model_decimate.py && git commit -m "ModelTranslator: 减面默认 RizomUV 展开(三步编排,失败/不达标回退 seam)"
```
---
### Task 6: 烘焙链路验证 + README
**Files:**
- Modify: `Tools/ModelTranslator/README.md`(依赖节 + model_decimate 说明)
- [ ] **Step 1: rizom 产物烘焙冒烟**
```bash
cd "D:/UD/AI/AIC#Project/Tools/ModelTranslator" && python model_bake.py src/store10k.fbx src/store10k_low.fbx
```
预期:五张贴图全出、无警告。用 Read 工具查看 `out_bake/store10k/store10k_color.png`:岛内无黑斑/脏色。
- [ ] **Step 2: 更新 README**
`README.md` 「依赖」节 Blender 条目后加:
```markdown
- RizomUV 2025+(可选,UV 展开质量更好;默认路径 `C:\Program Files\RizomUV2025\rizomuv.exe`,也可用 `--rizom` 参数或环境变量 `RIZOMUV_EXE` 指定;未安装自动回退 Blender 展开)
```
「减面与高低模烘焙」节中 model_decimate.py 的说明整句改为:
```markdown
- **model_decimate.py**:多 mesh 自动 join;三角化后 Decimate(collapse) 减到 `--tris`(±3%,最多 2 轮修正);旧 UV 全删后重展——默认 **RizomUV 自动切缝展开**(官方 RizomUVLink 驱动:MOSAIC 分割 + 切柄/连洞 + Pack,会短暂弹出 RizomUV 窗口、占一个 license),RizomUV 不可用或质量门不达标(翻转 >2% 或重叠 >8%)自动回退锐边 seam 展开(再不达标回退 Smart UV Project);`--unwrap seam|smart` 可跳过 RizomUV;输出 `<名>_low.fbx``-o` 改目录),日志报 UV 岛数/利用率
```
- [ ] **Step 3: Commit**
```bash
cd "D:/UD/AI/AIC#Project" && git add Tools/ModelTranslator/README.md && git commit -m "ModelTranslator: README 补 RizomUV 展开说明(store10k 冒烟对比通过)"
```
---
## 自审记录
- Spec 覆盖:默认 rizomTask 5)、AutoSeamTask 3)、回退链 rizom→seam→smartTask 4/5)、exe 发现四级(Task 2)、超时(Task 3 `_call`)、无 UV 视同不达标(Task 4)、日志口径(Task 5 沿用)、README(Task 6)、单测+冒烟+回退冒烟(Task 1/2/5/6)——全覆盖
- 占位符扫描:无 TBD/省略;所有代码块完整
- 类型一致性:`uv_gate_ok(flipped, overlap)`Task 1 定义,Task 4 使用)、`RizomError/find_rizomuv/rizom_unwrap`Task 2/3 定义,Task 5 导入)、`candidate_paths(cli, env, reg, default)` 签名测试与实现一致