ModelTranslator: QR watertightness gate + quad default

根因(真机排查):QuadRemesher 引擎在激进减面下非确定性地从封闭流形
输入打出孔洞/非流形边(实测同一输入约 85% 次数破碎),即"破碎面"。

修复:QR 导回后比对输入拓扑,若比输入新增开边(孔洞)/非流形边即判破损、
弃用 QR 回退 collapse(topology_acceptable 纯策略+单测;_edge_defects 计数)。
顺带修复水密门在导入后拒绝时未恢复 obj 选中态,导致 collapse 回退时
export use_selection 选不到对象、只导出 4KB 空 FBX 的 bug。

含用户改动:--reducer 默认改为 quad(配合水密门,破碎自动回退,安全),
及其 CLI 默认值测试。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
ud18010
2026-07-23 10:10:45 +08:00
co-authored by Claude Opus 4.8
parent 44060ac3cb
commit 0481f10b8c
6 changed files with 103 additions and 6 deletions
@@ -0,0 +1,39 @@
import os
import sys
import unittest
from unittest import mock
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
import model_decimate
class TestModelDecimateCli(unittest.TestCase):
def test_default_reducer_is_quad(self):
summary = {
"src": "store.fbx",
"tris_before": 49966,
"tris_after": 3000,
"target": 3000,
"reducer": "quad",
"uv": {
"mode": "seam",
"islands": 1,
"fill": 1.0,
"flipped": 0.0,
"overlap": 0.0,
},
"warnings": [],
}
argv = ["model_decimate.py", "store.fbx", "--tris", "3000"]
with mock.patch.object(sys, "argv", argv), \
mock.patch.object(model_decimate, "find_blender", return_value="blender"), \
mock.patch.object(model_decimate, "run_blender_script", return_value=summary) as run:
model_decimate.main()
args = run.call_args.args[2]
self.assertEqual(args[-1], "quad")
if __name__ == "__main__":
unittest.main()
@@ -116,5 +116,30 @@ class TestFindQrEngine(unittest.TestCase):
self.assertIn(os.path.join("5.0", "scripts"), chosen)
class TestTopologyAcceptable(unittest.TestCase):
def test_closed_source_clean_qr_ok(self):
# 封闭输入(0/0) + 水密 QR(0/0) -> 接受
self.assertTrue(qb.topology_acceptable(0, 0, 0, 0))
def test_closed_source_holey_qr_rejected(self):
# 封闭输入被 QR 打出开边 -> 拒绝
self.assertFalse(qb.topology_acceptable(0, 0, 8, 0))
def test_closed_source_nonmanifold_qr_rejected(self):
self.assertFalse(qb.topology_acceptable(0, 0, 0, 1))
def test_open_shell_source_same_ok(self):
# 输入本身是开放壳(10 开边),QR 未新增 -> 接受
self.assertTrue(qb.topology_acceptable(10, 0, 10, 0))
def test_open_shell_source_worse_rejected(self):
# QR 比输入引入更多开边 -> 拒绝
self.assertFalse(qb.topology_acceptable(10, 0, 12, 0))
def test_qr_fewer_defects_ok(self):
# QR 输出反而更少缺陷(不劣于输入)-> 接受
self.assertTrue(qb.topology_acceptable(10, 2, 8, 1))
if __name__ == "__main__":
unittest.main()