import vmi
import numpy as np


def read_stl():
    """读取*.stl文件"""
    file_name = vmi.askOpenFile('*.stl')
    if file_name is not None:
        pd = vmi.vtkread_STL(file_name)
        if pd.GetNumberOfCells() > 0:
            return pd


def LeftButtonPress(**kwargs):
    """响应左键按下"""
    if kwargs['picked'] is mesh_prop:  # 判断左键按下时是否在模型上
        pt = view.mouseOnPropCell()  # 获得拾取到的世界坐标点
        path_pts.clear()  # 清空拾取路径点列，开始拾取
        path_pts.append(pt)  # 添加第一个拾取点
        update_path(path_pts)  # 更新拾取路径显示 8


def LeftButtonPressMove(**kwargs):
    """响应左键按下之后移动"""
    if kwargs['picked'] is mesh_prop:  # 左键按下移动时，参数传递的仍然是按下时的对象
        pt = view.mouseOnPropCell()  # 获得拾取到的世界坐标点

        for path_pt in path_pts:  # 遍历拾取路径点
            if (path_pt == pt).all():  # 如果已经存在相同的点，则返回
                return

        update_path([*path_pts, pt])  # 更新拾取路径显示，暂不添加拾取路径点

        if view.mouseOnProp() is mesh_prop:  # 判断鼠标当前位置是否在模型上
            path_pts.append(pt)  # 添加拾取路径点


def LeftButtonPressMoveRelease(**kwargs):
    """响应左键按下移动之后释放"""
    if kwargs['picked'] is mesh_prop:  # 左键按下移动释放时，参数传递的仍然是按下时的对象
        update_plate()  # 左键释放时更新导板


def Wheel(**kwargs):
    """响应滚轮"""
    mesh_prop.visibleToggle()  # 切换模型显示与隐藏


def update_path(pts):
    """更新拾取路径显示"""
    if len(pts) > 1:  # 判断拾取路径点数是否足够构成封闭线框
        path_prop.clone(vmi.ccWire(vmi.ccSegments(pts, True)))  # 构造封闭折线段
    else:
        path_prop.clone()  # 否则清空拾取路径显示
    view.updateInTime()  # 刷新视图


def update_plate():
    if len(path_pts) > 1:  # 判断拾取点列是否有效
        cs = view.cameraCS()  # 视角坐标系，cs.axis(2)表示垂直射入屏幕方向

        # 构造初始投影面
        lmax = vmi.pdOBB_Pts(mesh_pd)['size'][0]
        pts = [pt - lmax * cs.axis(2) for pt in path_pts]  # 相对路径点上升足够距离
        top_pd = vmi.pdTriangulate_Polyline(pts)  # 初步三角网格化多边形内部
        top_pd = vmi.pdRemesh(top_pd)  # 网格重划分，使其致密均匀

        # 投影构造下端面
        bottom_pd = vmi.pdRayCast_Pd(top_pd, cs.axis(2), mesh_pd, path_pts[0])  # 投影面上的所有网格点投影到模型表面
        bottom_pd = vmi.pdRemesh(bottom_pd)  # 网格重划分，消除投影造成的拉伸变形
        bottom_pts = vmi.pdBorder_Pts(bottom_pd)  # 获得顺次连接的边界点
        bottom_wire = vmi.ccWire(vmi.ccSegments(bottom_pts, True))  # 构造线，用于放样构造侧面
        bottom_face = vmi.ccSh_Pd(bottom_pd)  # 构造面，用于缝合实体

        # 反向投影构造上端面
        pt_z = {cs.inv().mpt(pt)[2]: pt for pt in bottom_pts}  # 将下端面在视角坐标系中的z坐标构成索引
        zmin, zmax = min(*pt_z), max(*pt_z)  # 计算z坐标的最小值和最大值
        top_pt = pt_z[zmin] - 2 * cs.axis(2)  # 最小值增加一个最小厚度，计算上端面内一点
        top_pts, top_center = [], np.zeros(3)  # 计算上端面边界点和中心
        for pt in bottom_pts:  # 遍历下端面边界点
            pt = vmi.ptOnPlane(pt, top_pt, cs.axis(2))  # 将下端面点投影到上端面
            top_pts.append(pt)  # 收集上端面边界点
            top_center += pt / len(bottom_pts)  # 累加计算上端面中心

        # 构造上端面
        top_wire = vmi.ccWire(vmi.ccSegments(top_pts, True))  # 构造线，用于放样构造侧面
        top_face = vmi.ccFace(top_wire)  # 构造面，用于缝合实体

        # 构造匹配板
        profile = vmi.ccLoft([top_wire, bottom_wire], True, True)  # 放样构造侧面
        plate_solid = vmi.ccSolid(vmi.ccSew([top_face, profile, bottom_face]))  # 缝合构造实体

        # 构造引导孔外圆柱
        hole_outer_radius = 3  # 半径
        hole_outer_height = 10  # 高度
        hole_outer_center = top_center  # 圆台定位到匹配板上端面中心
        hole_outer_solid = vmi.ccCylinder(hole_outer_radius,
                                          hole_outer_height,
                                          hole_outer_center,
                                          -cs.axis(2))  # 构造圆柱体

        # 构造引导孔内圆柱
        hole_inner_radius = 1  # 半径
        hold_depth = zmax - zmin + 2 + hole_outer_height  # 深度，保证切除穿透匹配板
        hole_inner_center = top_center - hole_outer_height * cs.axis(2)  # 圆孔定位到圆台上端面中心
        hole_inner_solid = vmi.ccCylinder(hole_inner_radius,
                                          hold_depth,
                                          hole_inner_center,
                                          cs.axis(2))  # 构造圆柱体

        # 融合匹配板和引导孔构造导板
        plate_solid = vmi.ccBoolean_Union([plate_solid, hole_outer_solid])  # 布尔加匹配板和圆台
        plate_solid = vmi.ccBoolean_Difference([plate_solid, hole_inner_solid])  # 再布尔减圆孔

        plate_prop.clone(plate_solid)  # 更新导板显示
        path_prop.clone(bottom_wire)  # 更新拾取路径
    else:
        plate_prop.clone()  # 拾取路径无效时清空导板显示
    view.updateInTime()  # 刷新视图


if __name__ == '__main__':
    mesh_pd = read_stl()  # 读取STL文件为个性化匹配的目标模型
    if mesh_pd is None:  # 判断用户是否有效读取了模型文件
        vmi.appexit()  # 清理并退出程序

    view = vmi.View()  # 视图
    view.mouse['NoButton']['Wheel'] = Wheel  # 连接鼠标滚轮

    # 面网格模型
    mesh_prop = vmi.PolyActor(view, rgb=[1, 1, 0.6], pickable=True, point=2)
    mesh_prop.mouse['LeftButton']['Press'] = LeftButtonPress  # 连接鼠标左键按下
    mesh_prop.mouse['LeftButton']['PressMove'] = LeftButtonPressMove  # 连接鼠标左键按下移动
    mesh_prop.mouse['LeftButton']['PressMoveRelease'] = LeftButtonPressMoveRelease  # 连接鼠标左键按下移动释放
    mesh_prop.mouse['NoButton']['Wheel'] = Wheel  # 连接鼠标滚轮
    mesh_prop.clone(mesh_pd)  # 更新模型显示

    # 拾取路径点列
    path_pts = []

    # 拾取路径显示
    path_prop = vmi.PolyActor(view, rgb=[1, 0.4, 0.4], line=3)

    # 导板显示
    plate_prop = vmi.PolyActor(view, rgb=[0.4, 0.6, 1])

    view.cameraFit()  # 自动调整视图的视野范围
    vmi.appexec(view)  # 执行主窗口程序
    vmi.appexit()  # 清理并退出程序
