模型穿插检查工具

# -*- coding: utf-8 -*-
import maya.api.OpenMaya as om
import maya.cmds as cmds

# 只能感知模式 且 加上材质自动转换  清除会恢复原来的材质

# =========================
# ✅ 核心检测逻辑 (保持 API 2.0 高速)
# =========================

def get_high_speed_overlap(source_mesh, target_mesh, threshold=0.15):
    sel = om.MSelectionList()
    try:
        sel.add(source_mesh)
        sel.add(target_mesh)
    except: return []

    source_dag, target_dag = sel.getDagPath(0), sel.getDagPath(1)
    source_fn, target_fn = om.MFnMesh(source_dag), om.MFnMesh(target_dag)
    points = source_fn.getPoints(om.MSpace.kWorld)
    normals = source_fn.getVertexNormals(False, om.MSpace.kWorld)
    bad_vtx_indices = []

    cmds.progressWindow(title='模型间检测中...', progress=0, max=len(points), isInterruptable=True)
    for i in range(len(points)):
        if i % 1000 == 0:
            if cmds.progressWindow(query=True, isCancelled=True): break
            cmds.progressWindow(edit=True, progress=i)

        p, n = points[i], normals[i]
        cp, _ = target_fn.getClosestPoint(p, om.MSpace.kWorld)

        if p.distanceTo(cp) < threshold:
            bad_vtx_indices.append(i)
            continue 

        ray_dir = om.MFloatVector(-n.x, -n.y, -n.z)
        hit = target_fn.closestIntersection(
            om.MFloatPoint(p.x + n.x*0.001, p.y + n.y*0.001, p.z + n.z*0.001), 
            ray_dir, om.MSpace.kWorld, 100.0, False)

        if hit and hit[1] < 2.0: 
            bad_vtx_indices.append(i)

    cmds.progressWindow(endProgress=True)
    return bad_vtx_indices

def get_self_intersection_fast(mesh, threshold=0.2):
    sel = om.MSelectionList()
    try: sel.add(mesh)
    except: return []

    fn = om.MFnMesh(sel.getDagPath(0))
    points, (counts, indices) = fn.getPoints(om.MSpace.kWorld), fn.getVertices()
    bad_vertices, offset = set(), 0

    cmds.progressWindow(title='自穿插检测中...', progress=0, max=len(counts), isInterruptable=True)
    for fid, vcnt in enumerate(counts):
        if fid % 500 == 0:
            if cmds.progressWindow(query=True, isCancelled=True): break
            cmds.progressWindow(edit=True, progress=fid)

        f_idx = indices[offset:offset+vcnt]
        offset += vcnt

        center = om.MPoint(
            sum([points[v].x for v in f_idx])/vcnt, 
            sum([points[v].y for v in f_idx])/vcnt, 
            sum([points[v].z for v in f_idx])/vcnt
        )
        norm = fn.getPolygonNormal(fid, om.MSpace.kWorld)
        ray_s = om.MFloatPoint(center.x + norm.x*0.001, center.y + norm.y*0.001, center.z + norm.z*0.001)

        hit = fn.closestIntersection(ray_s, om.MFloatVector(norm), om.MSpace.kWorld, threshold, False)
        if hit and hit[2] != fid:
            for v in f_idx: bad_vertices.add(v)

    cmds.progressWindow(endProgress=True)
    return list(bad_vertices)

# =========================
# ✅ UI 类定义
# =========================

class SmartOverlapUI:
    def __init__(self):
        self.win_name = "SmartOverlapUI_V3"
        self.last_mesh = None
        self.last_bad_vtx = []
        self.original_materials = {} # 用于存储原始材质:{ mesh_name: shading_group }
        self.temp_lambert_sg = "OverlapCheck_Lambert_SG"
        self.create_ui()

    def create_ui(self):
        if cmds.window(self.win_name, exists=True): 
            cmds.deleteUI(self.win_name)

        self.win = cmds.window(self.win_name, title="晴空穿插检查工具", widthHeight=(350, 280), sizeable=True)
        self.root_form = cmds.formLayout(numberOfDivisions=100)

        self.top_col = cmds.columnLayout(adjustableColumn=True, rowSpacing=5)
        cmds.text(l="智能感知模式开启 (含材质自动转换)", fn="boldLabelFont", height=30, bgc=[0.25, 0.25, 0.25])
        cmds.text(l=" • 检测时自动转为灰色 Lambert\n • 点击 [清除颜色] 恢复原始材质", al="left", height=40)
        # cmds.text(l="智能感知模式开启", fn="boldLabelFont", height=30, bgc=[0.25, 0.25, 0.25])
        cmds.text(l=" • 选 1 个:检测自穿插\n • 选 2 个:前选被检,后选参考", al="left", height=40)
        cmds.setParent(self.root_form)

        self.mid_col = cmds.columnLayout(adjustableColumn=True, rowSpacing=15)
        cmds.separator(style="in", h=10)
        self.threshold_fld = cmds.floatSliderGrp(label='敏感度: ', field=True, minValue=0, maxValue=1.0, 
                                                value=0.15, step=0.01, columnWidth3=[60, 50, 180])
        cmds.setParent(self.root_form)

        self.run_btn = cmds.button(label="🚀 执行检测", height=50, bgc=[0.32, 0.52, 0.72], command=self.smart_run)
        self.clear_btn = cmds.button(label="清除颜色并恢复材质", height=35, command=self.clear_all_and_restore)
        self.select_btn = cmds.button(label="选问题点", height=35, command=self.select_last_bad)

        cmds.formLayout(self.root_form, edit=True,
            attachForm=[
                (self.top_col, 'top', 5), (self.top_col, 'left', 10), (self.top_col, 'right', 10),
                (self.mid_col, 'left', 10), (self.mid_col, 'right', 10),
                (self.run_btn, 'left', 10), (self.run_btn, 'right', 10),
                (self.clear_btn, 'left', 10), (self.clear_btn, 'bottom', 15),
                (self.select_btn, 'right', 10), (self.select_btn, 'bottom', 15)
            ],
            attachControl=[
                (self.mid_col, 'top', 5, self.top_col),
                (self.run_btn, 'top', 10, self.mid_col),
                (self.clear_btn, 'top', 10, self.run_btn),
                (self.select_btn, 'top', 10, self.run_btn)
            ],
            attachPosition=[
                (self.clear_btn, 'right', 5, 50),
                (self.select_btn, 'left', 5, 50)
            ]
        )
        cmds.showWindow(self.win)

    def _setup_temp_material(self):
        """ 创建临时的灰色 Lambert 材质组 """
        if not cmds.objExists(self.temp_lambert_sg):
            shader = cmds.shadingNode('lambert', asShader=True, name="OverlapCheck_Lambert")
            cmds.setAttr(shader + ".color", 0.5, 0.5, 0.5, type="double3")
            self.temp_lambert_sg = cmds.sets(renderable=True, noSurfaceShader=True, empty=True, name=self.temp_lambert_sg)
            cmds.connectAttr(shader + ".outColor", self.temp_lambert_sg + ".surfaceShader")

    def _apply_temp_material(self, mesh):
        """ 记录并应用临时材质 """
        self._setup_temp_material()
        # 获取物体当前的 ShadingEngine (SG)
        shapes = cmds.listRelatives(mesh, shapes=True, fullPath=True)
        if shapes:
            sgs = cmds.listConnections(shapes[0], destination=True, type='shadingEngine')
            if sgs:
                self.original_materials[mesh] = sgs[0] # 记录第一个材质球
                cmds.sets(mesh, edit=True, forceElement=self.temp_lambert_sg)

    def clear_all_and_restore(self, *args):
        """ 清除顶点色并恢复原始材质 """
        sel = cmds.ls(sl=True, long=True) or ([self.last_mesh] if self.last_mesh else [])
        for obj in [o for o in sel if cmds.objExists(o)]:
            # 1. 清除顶点颜色
            cmds.polyColorPerVertex(obj, remove=True)
            cmds.setAttr(obj + ".displayColors", 0)

            # 2. 恢复材质
            if obj in self.original_materials:
                sg = self.original_materials[obj]
                if cmds.objExists(sg):
                    cmds.sets(obj, edit=True, forceElement=sg)
                del self.original_materials[obj]

        # 清理字典中已删除的物体
        self.original_materials = {k: v for k, v in self.original_materials.items() if cmds.objExists(k)}

    def select_last_bad(self, *args):
        if self.last_bad_vtx: cmds.select(self.last_bad_vtx)
        else: cmds.warning("没有发现可疑顶点。")

    def smart_run(self, *args):
        sel = cmds.ls(sl=True, long=True)
        val = cmds.floatSliderGrp(self.threshold_fld, query=True, value=True)
        if not sel:
            cmds.warning("请在场景中选择模型!")
            return

        src = sel[0]
        self.last_mesh = src

        # 检测前:应用 Lambert 材质
        self._apply_temp_material(src)

        # 智能判定模式
        if len(sel) == 1:
            bad_indices = get_self_intersection_fast(src, val)
        else:
            bad_indices = get_high_speed_overlap(src, sel[1], val)

        if bad_indices:
            cmds.setAttr(src + ".displayColors", 1)
            # 背景刷成深灰,让红点更明显
            cmds.polyColorPerVertex(src, colorRGB=[0.2, 0.2, 0.2], alpha=1)
            self.last_bad_vtx = ["{}.vtx[{}]".format(src, i) for i in bad_indices]
            cmds.polyColorPerVertex(self.last_bad_vtx, colorRGB=[1, 0, 0], alpha=1)
            cmds.confirmDialog(title='结果', message='检测到 %d 个穿插点\n已临时切换材质,点击[清除]按钮可恢复。' % len(bad_indices))
        else:
            self.last_bad_vtx = []
            # 如果没问题,直接恢复材质
            self.clear_all_and_restore()
            cmds.confirmDialog(title='结果', message='干净!没有发现穿插。')

# =========================
# ✅ 启动函数
# =========================

def main():
    return SmartOverlapUI()

if __name__ == "__main__":
    import __main__
    __main__.main = main
    main()