"""QR 桥纯逻辑:引擎发现、面数换算、settings 生成、progress 解析。不 import bpy。 减面可选后端 Quad Remesher 用——headless 直接调 Engine/xremesh.exe,绕开 modal 操作符。""" import glob import os QR_TIMEOUT = 120.0 # 引擎轮询超时(秒) def find_qr_engine(): """定位 QuadRemesher 的 xremesh.exe;找不到返回 None。 顺序:QR_ENGINE 环境变量(权威:显式设了就以它为准,无效即 None,不再自动发现) -> 标准 addon 路径(取字典序最大目录,当前 Blender 5.x 下即最高版本)。""" env = os.environ.get("QR_ENGINE") if env: return env if os.path.isfile(env) else None appdata = os.environ.get("APPDATA") if not appdata: return None pattern = os.path.join( appdata, "Blender Foundation", "Blender", "*", "scripts", "addons", "QuadRemesher", "Engine", "xremesh.exe") hits = sorted(glob.glob(pattern)) return hits[-1] if hits else None def tris_to_target_quads(tris): """三角面数换算 QR 目标四边形数:1 quad ≈ 2 tris,至少 1。""" return max(1, int(tris) // 2) def build_settings(in_fbx, out_fbx, prog, target_quads, adaptive=50, exact=False, hard_edges=True): """生成 RetopoSettings.txt 文本(xremesh.exe -s 读取)。""" lines = [ "HostApp=Blender", 'FileIn="%s"' % in_fbx, 'FileOut="%s"' % out_fbx, 'ProgressFile="%s"' % prog, "TargetQuadCount=%d" % target_quads, "CurvatureAdaptivness=%d" % adaptive, "ExactQuadCount=%d" % (1 if exact else 0), "UseVertexColorMap=False", "UseMaterialIds=0", "UseIndexedNormals=0", "AutoDetectHardEdges=%d" % (1 if hard_edges else 0), ] return "\n".join(lines) + "\n" def parse_progress(text): """解析 progress.txt -> (state, msg)。 首行 '2'=success;<0=error(次行为文本);空/分数/非法=running。""" lines = text.splitlines() if not lines: return ("running", "") try: v = float(lines[0]) except ValueError: return ("running", "") if v == 2: return ("success", "") if v < 0: return ("error", lines[1] if len(lines) > 1 else "") return ("running", "") def topology_acceptable(src_boundary, src_nonmanifold, out_boundary, out_nonmanifold): """QR 输出拓扑是否可接受:不比输入引入更多开边(孔洞)/非流形边。 QR 引擎在激进减面下会非确定性地打出孔洞——封闭输入(0/0)时要求输出也 0/0, 输入本身开放(壳)时按其基线放行;否则判破损,交上层回退 collapse。""" return out_boundary <= src_boundary and out_nonmanifold <= src_nonmanifold