# Copyright (c) 2017-2026 Soft8Soft, LLC. All rights reserved.
#
# Use of this software is subject to the terms of the Verge3D license
# agreement provided at the time of installation or download, or which
# otherwise accompanies this software in either electronic or hard copy form.
import fnmatch, os, tempfile, sys, webbrowser

import maya.cmds as cmds
import maya.mel as mel

import customAttrs, export, mayaUtils

import pluginUtils as pU
from pluginUtils.manager import AppManagerConn
from pluginUtils.path import getRoot, getAppManagerHost

EXPORT_GLTF_CMD = 'Verge3DExportGLTF'
SNEAK_PEEK_CMD = 'Verge3DSneakPeek'
APP_MANAGER_CMD = 'Verge3DRunAppManager'
EXPORT_SETTINGS_CMD = 'Verge3DExportSettings'
USER_MANUAL_CMD = 'Verge3DUserManual'

log = pU.log.getLogger('V3D-MY')
join = os.path.join

editorTemplateRegister = set()

try:
    _currentMayaPath
    _defaultExportPath
except:
    _currentMayaPath = ''
    _defaultExportPath = ''

def setDefaultExportPath(path):
    global _defaultExportPath

    _defaultExportPath = os.path.splitext(path)[0]

def getDefaultExportPath():
    global _currentMayaPath, _defaultExportPath

    mayaPath = cmds.file(location=True, query=True)
    if mayaPath == 'unknown':
        mayaPath = ''

    if _defaultExportPath and mayaPath == _currentMayaPath:
        return _defaultExportPath

    _currentMayaPath = mayaPath

    return os.path.splitext(mayaPath)[0]

def exportGLTF(c=None):
    cmds.undoInfo(stateWithoutFlush=False)

    try:
        startingDirectory = getDefaultExportPath()

        filePaths = cmds.fileDialog2(caption='Export glTF', startingDirectory=startingDirectory,
                                     fileFilter='glTF file (*.gltf);;glTF binary file (*.glb)')
        if filePaths:
            exportGLTFPath(filePaths[0])
            setDefaultExportPath(filePaths[0])
    finally:
        cmds.undoInfo(stateWithoutFlush=True)

def exportGLTFPath(filepath=None, sneakPeek=False, selectionOnly=False):

    customAttrs.createSettingsNode()

    name, ext = os.path.splitext(os.path.basename(filepath))

    if ext == '.glb':
        gltfFormat = 'BINARY'
    elif ext == '.html':
        gltfFormat = 'HTML'
    else:
        gltfFormat = 'ASCII'

    filepathBin = name + '.bin'
    filedir = os.path.dirname(filepath) + '/'

    tmpDir = tempfile.mkdtemp(suffix='verge3d')

    exportSettings = {
        'format' : gltfFormat,
        'filepath' : filepath,
        'binaryfilename' : filepathBin,
        'filedirectory': filedir,
        'strip' : True,
        'tmpDir': tmpDir,
        'sneakPeek': sneakPeek,
        'selectionOnly': selectionOnly
    }

    customAttrs.parseSettings(exportSettings)

    progressBar = ProgressBar('Exporting to glTF...')

    exporter = export.GLTFExporter()

    try:
        exporter.run(exportSettings, progressBar)
        return True
    except ProgressCancelled:
        exporter.collector.cleanup()
        exporter.restoreSceneState()
        log.warning('Export cancelled')
        return False

def sneakPeek(c=None):
    cmds.undoInfo(stateWithoutFlush=False)

    try:
        if not AppManagerConn.ping():
            AppManagerConn.start()

        prevDir = AppManagerConn.getPreviewDir(True)

        if exportGLTFPath(join(prevDir, 'sneak_peek.gltf'), True):
            execBrowser(getAppManagerHost('MAYA') +
                    'player/player.html?load=/sneak_peek/sneak_peek.gltf')
    except (PermissionError, FileExistsError):
        log.error('Permission denied when attempting to access the preview folder. '
                  'Make sure that you\'re not running the App Manager under different user.')
    finally:
        cmds.undoInfo(stateWithoutFlush=True)

def execBrowser(url):
    try:
        webbrowser.open(url)
    except BaseException:
        log.error('Failed to open URL: ' + url)

def runAppManager(c=None):
    if not AppManagerConn.ping():
        AppManagerConn.start()
    execBrowser(getAppManagerHost('MAYA'))

def runExportSettings(c=None):
    s = SettingsDialog()
    s.create()

def runUserManual(c=None):
    cmds.launch(webPage=AppManagerConn.getManualURL())

def runReexportAll(c=None):
    s = ReexportDialog()
    s.create()

def initRuntimeCommands(pluginPath):
    manualURL = AppManagerConn.getManualURL()

    if not cmds.runTimeCommand(EXPORT_GLTF_CMD, exists=True):
        cmds.runTimeCommand(EXPORT_GLTF_CMD, label='Verge3D Export to glTF',
                            annotation='Export the scene to glTF 2.0 format',
                            command='import interface; interface.exportGLTF()', helpUrl=manualURL,
                            image=os.path.normpath(os.path.join(pluginPath, 'images', 'shelf_export_gltf.png')),
                            keywords='verge3d;export;web;gltf;webgl;html;browser', default=True)

    if not cmds.runTimeCommand(SNEAK_PEEK_CMD, exists=True):
        cmds.runTimeCommand(SNEAK_PEEK_CMD, label='Verge3D Sneak Peek',
                            annotation='Export to temporary location and preview the scene in Verge3D',
                            command='import interface; interface.sneakPeek()', helpUrl=manualURL,
                            image=os.path.normpath(os.path.join(pluginPath, 'images', 'shelf_sneak_peek.png')),
                            keywords='verge3d;preview;sneak;web;gltf;webgl;html;browser', default=True)

    if not cmds.runTimeCommand(APP_MANAGER_CMD, exists=True):
        cmds.runTimeCommand(APP_MANAGER_CMD, label='Verge3D Run App Manager',
                            annotation='Open Verge3D App Manager in the browser',
                            command='import interface; interface.runAppManager()', helpUrl=manualURL,
                            image=os.path.normpath(os.path.join(pluginPath, 'images', 'shelf_app_manager.png')),
                            keywords='verge3d;app;manager;develop', default=True)

    if not cmds.runTimeCommand(EXPORT_SETTINGS_CMD, exists=True):
        cmds.runTimeCommand(EXPORT_SETTINGS_CMD, label='Verge3D Export Settings',
                            annotation='Open Verge3D Export Settings dialog',
                            command='import interface; interface.runExportSettings()', helpUrl=manualURL,
                            image=os.path.normpath(os.path.join(pluginPath, 'images', 'shelf_export_settings.png')),
                            keywords='verge3d;export;settings', default=True)

    if not cmds.runTimeCommand(USER_MANUAL_CMD, exists=True):
        cmds.runTimeCommand(USER_MANUAL_CMD, label='Verge3D User Manual',
                            annotation='Open Verge3D user manual in the browser',
                            command='import interface; interface.runUserManual()', helpUrl=manualURL,
                            image=mel.eval('languageResourcePath("menuIconHelp.png")'),
                            keywords='verge3d;help', default=True)

def createMenu():
    removeMenu()

    cmds.setParent(mel.eval('$tmp = $gMainWindow'))

    menu = cmds.menu('verge3d', label='Verge3D', tearOff=True)

    cmds.setParent(menu, menu=True)

    cmds.menuItem(label='Export glTF...', runTimeCommand=EXPORT_GLTF_CMD, sourceType='mel',
                  annotation=cmds.runTimeCommand(EXPORT_GLTF_CMD, annotation=True, query=True))

    cmds.menuItem(divider=True)

    cmds.menuItem(label='Sneak Peek', runTimeCommand=SNEAK_PEEK_CMD, sourceType='mel',
                  annotation=cmds.runTimeCommand(SNEAK_PEEK_CMD, annotation=True, query=True))

    cmds.menuItem(label='Run App Manager', runTimeCommand=APP_MANAGER_CMD, sourceType='mel',
                  annotation=cmds.runTimeCommand(APP_MANAGER_CMD, annotation=True, query=True))

    cmds.menuItem(label='Export Settings...', runTimeCommand=EXPORT_SETTINGS_CMD, sourceType='mel',
                  annotation=cmds.runTimeCommand(EXPORT_SETTINGS_CMD, annotation=True, query=True))

    cmds.menuItem(divider=True)

    cmds.menuItem(label='User Manual', runTimeCommand=USER_MANUAL_CMD, sourceType='mel',
                  annotation=cmds.runTimeCommand(USER_MANUAL_CMD, annotation=True, query=True))

    if pU.debug:
        cmds.menuItem(divider=True)
        cmds.menuItem(label='Reexport All', command=runReexportAll)

def removeMenu():
    if cmds.control('verge3d', exists=True):
        cmds.deleteUI('verge3d', menu=True)

class SettingsDialog():
    def __init__(self, winName='window'):
        self.winTitle = 'Verge3D Export Settings'
        self.winName = winName

        self.outlineCtls = []
        self.aoCtls = []
        self.esmDependingCtls = []

    def outlineChanged(self):
        for ctl in self.outlineCtls:
            enabled = cmds.getAttr('v3dSettings.outlineEnabled')
            cmds.control(ctl, edit=True, enable=enabled)

    def aoChanged(self):
        for ctl in self.aoCtls:
            enabled = cmds.getAttr('v3dSettings.aoEnabled')
            cmds.control(ctl, edit=True, enable=enabled)

    def shadowFilteringChanged(self):
        idx = cmds.getAttr('v3dSettings.shadowFiltering')
        enabled = customAttrs.SHADOW_FILTERING_MAP[idx] == 'ESM'
        for ctl in self.esmDependingCtls:
            cmds.control(ctl, edit=True, enable=enabled)

    def create(self):
        customAttrs.createSettingsNode()

        settingsNode = cmds.ls(type='v3dSettings')

        if not settingsNode:
            return

        if cmds.window(self.winName, exists=True):
            cmds.deleteUI(self.winName)

        cmds.window(self.winName, title=self.winTitle, resizeToFitChildren=True)

        tabs = cmds.tabLayout(innerMarginWidth=5, innerMarginHeight=5)

        frame1 = cmds.frameLayout(label='Common', collapsable=True)
        cmds.columnLayout(adjustableColumn=True)

        cmds.attrControlGrp(attribute='v3dSettings.copyright')
        cmds.attrControlGrp(attribute='v3dSettings.bakeText')
        cmds.attrControlGrp(attribute='v3dSettings.lzmaEnabled')
        cmds.attrControlGrp(attribute='v3dSettings.compressTextures')
        cmds.attrControlGrp(attribute='v3dSettings.optimizeAttrs')
        cmds.attrControlGrp(attribute='v3dSettings.aaMethod')
        cmds.attrControlGrp(attribute='v3dSettings.useHDR')
        cmds.attrControlGrp(attribute='v3dSettings.useOIT')
        cmds.attrControlGrp(attribute='v3dSettings.iblEnvironment')

        cmds.setParent('..')
        cmds.setParent('..')

        frame2 = cmds.frameLayout(label='Shadows', collapsable=True)
        cmds.columnLayout(adjustableColumn=True)

        cmds.attrControlGrp(attribute='v3dSettings.shadowFiltering',
                changeCommand=self.shadowFilteringChanged)
        self.esmDependingCtls.append(cmds.attrControlGrp(
                attribute='v3dSettings.esmDistanceScale', enable=False))

        cmds.setParent('..')
        cmds.setParent('..')

        frame3 = cmds.frameLayout(label='Animation', collapsable=True)
        cmds.columnLayout(adjustableColumn=True)

        cmds.attrControlGrp(attribute='v3dSettings.animExport')
        cmds.attrControlGrp(attribute='v3dSettings.animUsePlaybackRange')
        cmds.attrControlGrp(attribute='v3dSettings.animStartWithZero')

        cmds.setParent('..')
        cmds.setParent('..')

        frame4 = cmds.frameLayout(label='Ambient Occlusion', collapsable=True)
        cmds.columnLayout(adjustableColumn=True)

        cmds.attrControlGrp(attribute='v3dSettings.aoEnabled', changeCommand=self.aoChanged)

        self.aoCtls.append(cmds.attrFieldSliderGrp(attribute='v3dSettings.aoDistance', enable=False))
        self.aoCtls.append(cmds.attrFieldSliderGrp(attribute='v3dSettings.aoFactor', enable=False))
        self.aoCtls.append(cmds.attrFieldSliderGrp(attribute='v3dSettings.aoTracePrecision', enable=False))
        self.aoCtls.append(cmds.attrControlGrp(attribute='v3dSettings.aoBentNormals', enable=False))

        cmds.setParent('..')
        cmds.setParent('..')

        frame5 = cmds.frameLayout(label='Outlining Effect', collapsable=True)
        cmds.columnLayout(adjustableColumn=True)

        cmds.attrControlGrp(attribute='v3dSettings.outlineEnabled', changeCommand=self.outlineChanged)

        self.outlineCtls.append(cmds.attrFieldSliderGrp(attribute='v3dSettings.edgeStrength', enable=False))
        self.outlineCtls.append(cmds.attrFieldSliderGrp(attribute='v3dSettings.edgeGlow', enable=False))
        self.outlineCtls.append(cmds.attrFieldSliderGrp(attribute='v3dSettings.edgeThickness', enable=False))
        self.outlineCtls.append(cmds.attrControlGrp(attribute='v3dSettings.pulsePeriod', enable=False))
        self.outlineCtls.append(cmds.attrColorSliderGrp(attribute='v3dSettings.visibleEdgeColor', enable=False))
        self.outlineCtls.append(cmds.attrColorSliderGrp(attribute='v3dSettings.hiddenEdgeColor', enable=False))
        self.outlineCtls.append(cmds.attrControlGrp(attribute='v3dSettings.renderHiddenEdge', enable=False))

        cmds.setParent('..')
        cmds.setParent('..')

        cmds.tabLayout(tabs, edit=True, tabLabel=(
            (frame1, 'Common'),
            (frame2, 'Shadows'),
            (frame3, 'Animation'),
            (frame4, 'AO'),
            (frame5, 'Outlining')
        ))

        self.outlineChanged()
        self.aoChanged()
        self.shadowFilteringChanged()
        cmds.showWindow(self.winName)

class ReexportDialog():
    def __init__(self, winName='v3d_reexport'):
        self.winTitle = 'Reexport all Verge3D Assets'
        self.winName = winName

        self.folderUI = None
        self.resaveMayaUI = None
        self.updateCopyrightUI = None
        self.forceGLBUI = None

    def onCancel(self):
        cmds.deleteUI(self.winName, window=True)

    def openMayaScene(self, mayapath, mayapathRel):
        cmds.file(mayapath, open=True, ignoreVersion=True, force=True)
        mayaVer = cmds.about(apiVersion=True) // 10000
        try:
            fileVer = int(cmds.fileInfo('version', query=True)[0])
        except ValueError:
            fileVer = 2000
        if mayaVer < fileVer:
            log.error(f'Failed to open "{mayapathRel}". Using Maya {mayaVer}, file saved in {fileVer}')
            return False
        else:
            return True

    def updateCopyrightAttr(self):
        if cmds.checkBox(self.updateCopyrightUI, query=True, value=True):
            if cmds.ls(type='v3dSettings'):
                cmds.setAttr('v3dSettings.copyright', pU.copyrightLine, type='string')
            else:
                log.warning('Unable to update copyright, no v3dSettings node present')

    def onRun(self):
        folder = cmds.textField(self.folderUI, query=True, text=True)
        resaveMaya = cmds.checkBox(self.resaveMayaUI, query=True, value=True)
        forceGLB = cmds.checkBox(self.forceGLBUI, query=True, value=True)

        apps = join(getRoot(), folder)

        for root, dirs, files in os.walk(apps):
            for name in files:
                if fnmatch.fnmatch(name, '*.mb') or fnmatch.fnmatch(name, '*.ma'):
                    mayapath = os.path.normpath(join(root, name))
                    mayapathRel = os.path.relpath(mayapath, getRoot())

                    gltfpath = pU.path.findExportedAssetPath(mayapath)
                    if gltfpath:
                        if forceGLB:
                            gltfpath = os.path.splitext(gltfpath)[0] + '.glb'

                        print() # empty line improves readability

                        log.info(f'Reexporting "{mayapathRel}"')
                        if self.openMayaScene(mayapath, mayapathRel):
                            self.updateCopyrightAttr()
                            exportGLTFPath(gltfpath)
                            if resaveMaya:
                                log.info(f'Resaving "{mayapathRel}"')
                                cmds.file(save=True, force=True)
                    elif resaveMaya:
                        if self.openMayaScene(mayapath, mayapathRel):
                            log.info(f'Resaving "{mayapathRel}"')
                            self.updateCopyrightAttr()
                            cmds.file(save=True, force=True)

        self.onCancel()

    def create(self):
        if cmds.window(self.winName, exists=True):
            cmds.deleteUI(self.winName)

        cmds.window(self.winName, title=self.winTitle, resizeToFitChildren=True)
        cmds.columnLayout(adjustableColumn=True, rowSpacing=10, margins=10)

        cmds.rowLayout(numberOfColumns=2, adjustableColumn=2)
        cmds.text(label="Folder:")
        self.folderUI = cmds.textField(text="applications")
        cmds.setParent('..')

        self.resaveMayaUI = cmds.checkBox(label="Resave .mb/.ma files", value=False)
        self.updateCopyrightUI = cmds.checkBox(label="Update copyright", value=False)
        self.forceGLBUI = cmds.checkBox(label="Force GLB export", value=False)

        cmds.rowLayout(numberOfColumns=2)
        cmds.button(label="Cancel", width=100, command=lambda *args: self.onCancel())
        cmds.button(label="Run", width=100, command=lambda *args: self.onRun())
        cmds.setParent('..')

        cmds.setParent('..')
        cmds.showWindow(self.winName)

def resetFrameLayout(frameLayoutName, label, new, collapse=False):
    if new:
        cmds.frameLayout(frameLayoutName, label=label, collapse=collapse)
    else:
        cmds.setParent(frameLayoutName)

    children = cmds.frameLayout(frameLayoutName, q=True, childArray=True)
    if children:
        for child in children:
            cmds.deleteUI(child)

def animationSettingsUINew(nodeAttr):
    animationSettingsUI(nodeAttr, True)

def animationSettingsUIReplace(nodeAttr):
    animationSettingsUI(nodeAttr, False)

def animationSettingsUI(nodeAttr, new):

    origParent = cmds.setParent(q=True)

    resetFrameLayout('AEV3DAnimationSettings', 'Animation', new)

    ctlRepInf = None
    ctlRepCnt = None
    ctlsCustomRange = []

    def animLoopChanged():
        animLoop = customAttrs.ANIM_LOOP_MAP[cmds.getAttr(nodeAttr+'v3d.animLoop')]
        animRepeatInf = cmds.getAttr(nodeAttr+'v3d.animRepeatInfinite')

        cmds.control(ctlRepInf, edit=True, enable=(animLoop != 'ONCE'))
        cmds.control(ctlRepCnt, edit=True, enable=(animLoop != 'ONCE' and not animRepeatInf))

    def animCustomRangeChanged():
        customRange = cmds.getAttr(nodeAttr+'v3d.animUseCustomRange')
        for ctl in ctlsCustomRange:
            cmds.control(ctl, edit=True, enable=customRange)

    cmds.attrControlGrp(attribute=nodeAttr+'v3d.animAuto')

    cmds.attrControlGrp(attribute=nodeAttr+'v3d.animLoop', changeCommand=animLoopChanged)

    ctlRepInf = cmds.attrControlGrp(attribute=nodeAttr+'v3d.animRepeatInfinite', changeCommand=animLoopChanged)
    ctlRepCnt = cmds.attrControlGrp(attribute=nodeAttr+'v3d.animRepeatCount')
    cmds.attrControlGrp(attribute=nodeAttr+'v3d.animOffset')

    cmds.separator()

    cmds.attrControlGrp(attribute=nodeAttr+'v3d.animUseCustomRange', changeCommand=animCustomRangeChanged)

    ctlsCustomRange.append(cmds.attrControlGrp(attribute=nodeAttr+'v3d.animCustomRangeFrom'))
    ctlsCustomRange.append(cmds.attrControlGrp(attribute=nodeAttr+'v3d.animCustomRangeTo'))

    cmds.attrControlGrp(attribute=nodeAttr+'v3d.animSkeletonRoot')

    cmds.setParent(origParent)

    animLoopChanged()
    animCustomRangeChanged()

def advancedRenderUINew(nodeAttr):
    advancedRenderUI(nodeAttr, True)

def advancedRenderUIReplace(nodeAttr):
    advancedRenderUI(nodeAttr, False)

def advancedRenderUI(nodeAttr, new):

    origParent = cmds.setParent(q=True)

    resetFrameLayout('AEV3DAdvRender', 'Advanced Rendering', new, True)

    cmds.attrControlGrp(attribute=nodeAttr+'v3d.hidpiCompositing')
    cmds.attrControlGrp(attribute=nodeAttr+'v3d.fixOrthoZoom')

    cmds.separator()

    cmds.text(label='Fit to Camera Edge')

    cmds.attrControlGrp(attribute=nodeAttr+'v3d.canvasFitX')
    cmds.attrControlGrp(attribute=nodeAttr+'v3d.canvasFitY')
    cmds.attrControlGrp(attribute=nodeAttr+'v3d.canvasFitShape')
    cmds.attrControlGrp(attribute=nodeAttr+'v3d.canvasFitOffset')

    cmds.separator()

    breakCtls = []

    def canvasBreakChanged():
        enabled = cmds.getAttr(nodeAttr+'v3d.canvasBreakEnabled')
        for ctl in breakCtls:
            cmds.control(ctl, edit=True, enable=enabled)

    cmds.attrControlGrp(attribute=nodeAttr+'v3d.canvasBreakEnabled', changeCommand=canvasBreakChanged)

    breakCtls.append(cmds.attrControlGrp(attribute=nodeAttr+'v3d.canvasBreakMinWidth'))
    breakCtls.append(cmds.attrControlGrp(attribute=nodeAttr+'v3d.canvasBreakMaxWidth'))
    breakCtls.append(cmds.attrControlGrp(attribute=nodeAttr+'v3d.canvasBreakMinHeight'))
    breakCtls.append(cmds.attrControlGrp(attribute=nodeAttr+'v3d.canvasBreakMaxHeight'))
    breakCtls.append(cmds.attrControlGrp(attribute=nodeAttr+'v3d.canvasBreakOrientation'))

    canvasBreakChanged()

    cmds.setParent(origParent)

def cameraSettingsUINew(nodeAttr):
    cameraSettingsUI(nodeAttr, True)

def cameraSettingsUIReplace(nodeAttr):
    cameraSettingsUI(nodeAttr, False)

def cameraSettingsUI(nodeAttr, new):

    origParent = cmds.setParent(q=True)

    resetFrameLayout('AEV3DCameraSettingsControls', 'Controls', new)

    genericCtls = []
    orbitCtls = []
    fpsCtls = []

    def controlsChanged():

        controls = customAttrs.CAMERA_CONTROLS_MAP[cmds.getAttr(nodeAttr+'v3d.controls')]

        for ctl in genericCtls:
            cmds.control(ctl, edit=True, enable=(controls!='NONE'))

        for ctl in orbitCtls:
            cmds.control(ctl, edit=True, enable=(controls=='ORBIT'))

        for ctl in fpsCtls:
            cmds.control(ctl, edit=True, enable=(controls=='FIRST_PERSON'))

    cmds.attrControlGrp(attribute=nodeAttr+'v3d.controls', changeCommand=controlsChanged)

    genericCtls.append(cmds.attrControlGrp(attribute=nodeAttr+'v3d.panningEnabled'))
    genericCtls.append(cmds.attrControlGrp(attribute=nodeAttr+'v3d.rotateSpeed', label='Rotation Speed'))
    genericCtls.append(cmds.attrControlGrp(attribute=nodeAttr+'v3d.moveSpeed', label='Movement Speed'))

    cmds.separator()

    orbitCtls.append(cmds.text(label='Perspective Distance Limits'))
    orbitCtls.append(cmds.attrControlGrp(attribute=nodeAttr+'v3d.minDist', label='Min'))
    orbitCtls.append(cmds.attrControlGrp(attribute=nodeAttr+'v3d.maxDist', label='Max'))

    orbitCtls.append(cmds.text(label='Orthographic Zoom Limits'))
    orbitCtls.append(cmds.attrControlGrp(attribute=nodeAttr+'v3d.minZoom', label='Min'))
    orbitCtls.append(cmds.attrControlGrp(attribute=nodeAttr+'v3d.maxZoom', label='Max'))

    cmds.separator()
    orbitCtls.append(cmds.text(label='Vertical Angle Limits'))
    orbitCtls.append(cmds.attrControlGrp(attribute=nodeAttr+'v3d.minAngle', label='Min'))
    orbitCtls.append(cmds.attrControlGrp(attribute=nodeAttr+'v3d.maxAngle', label='Max'))

    cmds.separator()
    orbitCtls.append(cmds.text(label='Horizontal Angle Limits'))
    orbitCtls.append(cmds.attrControlGrp(attribute=nodeAttr+'v3d.minAzimuthAngle', label='Min'))
    orbitCtls.append(cmds.attrControlGrp(attribute=nodeAttr+'v3d.maxAzimuthAngle', label='Max'))

    cmds.separator()
    fpsCtls.append(cmds.text(label='First-Person Camera'))

    collMatBtn = None

    def assignFpsCollisionMaterial(*args):
        sel = cmds.ls(selection=True, dagObjects=True, shapes=True)
        if sel:
            shadingEngine = cmds.listConnections(sel, type='shadingEngine')
            materials = cmds.ls(cmds.listConnections(shadingEngine), materials=True)
            if materials:
                mat = materials[0]
                cmds.connectAttr(mat + '.message', nodeAttr+'v3d.fpsCollisionMaterial', force=True)
                cmds.button(collMatBtn, edit=True, label=mayaUtils.getName(mat))

    cmds.rowLayout(numberOfColumns=2, adjustableColumn=2, columnAlign=(1, 'right'))

    fpsCtls.append(cmds.text(label='Collison Material'))

    conn = cmds.listConnections(nodeAttr+'v3d.fpsCollisionMaterial')
    if conn:
        collMatBtn = cmds.button(label=mayaUtils.getName(conn[0]), command=assignFpsCollisionMaterial)
    else:
        collMatBtn = cmds.button(label='Select Material', command=assignFpsCollisionMaterial)

    fpsCtls.append(collMatBtn)

    cmds.setParent('..')

    fpsCtls.append(cmds.attrControlGrp(attribute=nodeAttr+'v3d.fpsGazeLevel', label='Gaze Level'))
    fpsCtls.append(cmds.attrControlGrp(attribute=nodeAttr+'v3d.fpsStoryHeight', label='Story Height'))
    fpsCtls.append(cmds.attrControlGrp(attribute=nodeAttr+'v3d.enablePointerLock', label='Enable Pointer Lock'))

    cmds.setParent(origParent)

    controlsChanged()

    resetFrameLayout('AEV3DCameraSettingsRendering', 'Rendering', new)
    cmds.attrControlGrp(attribute=nodeAttr+'v3d.renderBackground')

    cmds.setParent(origParent)

def meshSettingsUINew(nodeAttr):
    meshSettingsUI(nodeAttr, True)

def meshSettingsUIReplace(nodeAttr):
    meshSettingsUI(nodeAttr, False)

def meshSettingsUI(nodeAttr, new):

    origParent = cmds.setParent(q=True)

    resetFrameLayout('AEV3DMeshSettings', 'Rendering', new)

    cmds.attrControlGrp(attribute=nodeAttr+'v3d.renderOrder')
    cmds.attrControlGrp(attribute=nodeAttr+'v3d.frustumCulling')

    cmds.setParent(origParent)

def lightSettingsUINew(nodeAttr):
    lightSettingsUI(nodeAttr, True)

def lightSettingsUIReplace(nodeAttr):
    lightSettingsUI(nodeAttr, False)

def lightSettingsUI(nodeAttr, new):

    origParent = cmds.setParent(q=True)

    resetFrameLayout('AEV3DLightSettings', 'Shadows', new)

    cmds.attrControlGrp(attribute=nodeAttr+'v3d.esmExponent')

    if cmds.attributeQuery('csmCount', node=nodeAttr+'v3d.', exists=True):
        cmds.separator()
        cmds.text(label='Cascaded Shadow Maps')

        cmds.attrControlGrp(attribute=nodeAttr+'v3d.csmCount')
        cmds.attrControlGrp(attribute=nodeAttr+'v3d.csmFade')
        cmds.attrControlGrp(attribute=nodeAttr+'v3d.csmDistribution')
        cmds.attrControlGrp(attribute=nodeAttr+'v3d.csmLightMargin')

    cmds.setParent(origParent)

def lineSettingsUINew(nodeAttr):
    lineSettingsUI(nodeAttr, True)

def lineSettingsUIReplace(nodeAttr):
    lineSettingsUI(nodeAttr, False)

def lineSettingsUI(nodeAttr, new):

    origParent = cmds.setParent(q=True)

    resetFrameLayout('AEV3DLineSettings', 'Line Rendering', new)

    lineCtls = []

    def lineSettingsChanged():
        enabled = cmds.getAttr(nodeAttr+'v3d.enableLineRendering')
        for ctl in lineCtls:
            cmds.control(ctl, edit=True, enable=enabled)

    cmds.attrControlGrp(attribute=nodeAttr+'v3d.enableLineRendering', changeCommand=lineSettingsChanged)
    lineCtls.append(cmds.attrColorSliderGrp(attribute=nodeAttr+'v3d.lineColor', label='Line Color'))
    if cmds.attributeQuery('lineWidthV3D', node=nodeAttr, exists=True):
        lineCtls.append(cmds.attrControlGrp(attribute=nodeAttr+'v3d.lineWidthV3D', label='Line Width (px)'))
    else:
        lineCtls.append(cmds.attrControlGrp(attribute=nodeAttr+'v3d.lineWidth', label='Line Width (px)'))
    lineCtls.append(cmds.attrControlGrp(attribute=nodeAttr+'v3d.lineResolutionSteps', label='Resolution Steps'))

    cmds.setParent(origParent)

    lineSettingsChanged()

def materialSettingsUINew(nodeAttr):
    materialSettingsUI(nodeAttr, True)

def materialSettingsUIReplace(nodeAttr):
    materialSettingsUI(nodeAttr, False)

def materialSettingsUI(nodeAttr, new):

    transHackCtl = None
    depthWriteCtl = None

    def alphaModeChanged():
        nonlocal transHackCtl, depthWriteCtl

        idx = cmds.getAttr(nodeAttr+'v3d.alphaMode')

        if customAttrs.ALPHA_MODE_MAP[idx] == 'OPAQUE':
            cmds.control(depthWriteCtl, edit=True, enable=False)
        else:
            cmds.control(depthWriteCtl, edit=True, enable=True)

        if customAttrs.ALPHA_MODE_MAP[idx] == 'BLEND':
            cmds.control(transHackCtl, edit=True, enable=True)
        else:
            cmds.control(transHackCtl, edit=True, enable=False)

    origParent = cmds.setParent(q=True)

    resetFrameLayout('AEV3DMaterialSettings', 'Custom Settings', new)

    cmds.attrControlGrp(attribute=nodeAttr+'v3d.alphaMode', changeCommand=alphaModeChanged)
    transHackCtl = cmds.attrControlGrp(attribute=nodeAttr+'v3d.transparencyHack')
    cmds.attrControlGrp(attribute=nodeAttr+'v3d.twoSided')
    depthWriteCtl = cmds.attrControlGrp(attribute=nodeAttr+'v3d.depthWrite')
    cmds.attrControlGrp(attribute=nodeAttr+'v3d.depthTest')
    cmds.attrControlGrp(attribute=nodeAttr+'v3d.dithering')

    if cmds.objectType(nodeAttr) in customAttrs.MATERIALS_WITH_GLTF_COMPAT:
        cmds.attrControlGrp(attribute=nodeAttr+'v3d.gltfCompat')

    cmds.setParent(origParent)

    alphaModeChanged()

def textureSettingsUINew(nodeAttr):
    textureSettingsUI(nodeAttr, True)

def textureSettingsUIReplace(nodeAttr):
    textureSettingsUI(nodeAttr, False)

def textureSettingsUI(nodeAttr, new):

    origParent = cmds.setParent(q=True)

    resetFrameLayout('AEV3DTextureSettings', 'Custom Settings', new)

    cmds.attrControlGrp(attribute=nodeAttr+'v3d.anisotropy')
    cmds.attrControlGrp(attribute=nodeAttr+'v3d.compressionMethod')

    cmds.setParent(origParent)

def customAttrEditorUI(nodeName):
    global editorTemplateRegister

    type = cmds.objectType(nodeName)

    if type in editorTemplateRegister:
        return

    editorTemplateRegister.add(type)

    if type == 'transform' or type == 'joint':
        cmds.editorTemplate(beginLayout='Verge3D', collapse=True)
        cmds.editorTemplate('', callCustom=[animationSettingsUINew, animationSettingsUIReplace])
        cmds.editorTemplate('', callCustom=[advancedRenderUINew, advancedRenderUIReplace])
        cmds.editorTemplate(endLayout=True)

    elif type == 'camera':
        cmds.editorTemplate(beginLayout='Verge3D', collapse=True)
        cmds.editorTemplate('', callCustom=[cameraSettingsUINew, cameraSettingsUIReplace])
        cmds.editorTemplate(endLayout=True)

    elif type == 'mesh':
        cmds.editorTemplate(beginLayout='Verge3D', collapse=True)
        cmds.editorTemplate('', callCustom=[meshSettingsUINew, meshSettingsUIReplace])
        cmds.editorTemplate('', callCustom=[lineSettingsUINew, lineSettingsUIReplace])
        cmds.editorTemplate(endLayout=True)

    elif type in ['directionalLight', 'pointLight', 'spotLight', 'areaLight', 'aiAreaLight']:
        cmds.editorTemplate(beginLayout='Verge3D', collapse=True)
        cmds.editorTemplate('', callCustom=[lightSettingsUINew, lightSettingsUIReplace])
        cmds.editorTemplate(endLayout=True)

    elif type in customAttrs.LINE_RENDERING_OBJECTS:
        cmds.editorTemplate(beginLayout='Verge3D', collapse=True)
        cmds.editorTemplate('', callCustom=[lineSettingsUINew, lineSettingsUIReplace])
        cmds.editorTemplate(endLayout=True)

    elif type in customAttrs.SUPPORTED_MATERIALS:
        cmds.editorTemplate(beginLayout='Verge3D', collapse=True)
        cmds.editorTemplate('', callCustom=[materialSettingsUINew, materialSettingsUIReplace])
        cmds.editorTemplate(endLayout=True)

    elif type == 'file':
        cmds.editorTemplate(beginLayout='Verge3D', collapse=True)
        cmds.editorTemplate('', callCustom=[textureSettingsUINew, textureSettingsUIReplace])
        cmds.editorTemplate(endLayout=True)

class ProgressCancelled(Exception):
    """Exception raised when the user cancels the main progress bar."""
    def __init__(self):
        pass

class ProgressBar():

    def __init__(self, status):
        self.bar = mel.eval('$tmp = $gMainProgressBar')

        cmds.progressBar(self.bar, edit=True, beginProgress=True, isInterruptable=True)
        cmds.progressBar(self.bar, edit=True, endProgress=True)

        cmds.progressBar(self.bar, edit=True, beginProgress=True,
                         isInterruptable=True, status=status, maxValue=100)

    def update(self, progress, status=''):
        if cmds.progressBar(self.bar, query=True, isCancelled=True):
            raise ProgressCancelled()

        if status:
            cmds.progressBar(self.bar, edit=True, progress=progress, status=status)
        else:
            cmds.progressBar(self.bar, edit=True, progress=progress)

    def __del__(self):
        cmds.progressBar(self.bar, edit=True, endProgress=True)
