Home Website Youtube GitHub

Sharing a couple of functions i wrote for anim picker

i wanted a bit more control over ui button placement and handles, scaleing is broken in 2020 so was having to do a lot with handles and it was awkward getting things to line up.
i wanted some snapping and the ability to undo. so i started thinking nurbs curves?
so i wrote a couple of functions for getting button as curves, figured it might be useful for others too.

the first function gets the data dictionary from the ui and using the data build a nurbs curve for each button, these curves can then be edited as normal, shapes nodes can be swapped and the the second function over writes the picker data node in the scene, cvs are baked relative to pivots so scale and rotate data on the transform is applied.
i am pretty new to this code so i think i captured all the data and re-apply it on the other end but i may have missed some stuff.
this has only been tested a little on maya 2020.2

import pymel.core as pm
from PySide2 import QtGui
import mgear.core.attribute as attr
import mgear.anim_picker
from mgear.core import pyqt
import json

def extract_picker_data(factor = 0.1, background_fade = 0.5, showCVs=True):
    animPickerUI = pyqt.get_instance(pyqt.get_main_window(), mgear.anim_picker.gui.MainDockWindow)
    data = animPickerUI.get_character_data()

    grp = pm.group(em=True, n='pickerData_extraction')
    for tab in data['tabs']:
        picker_grp = pm.group(em=True, n=tab['name'], p=grp)
        if 'background' in tab['data'].keys():
            ip = pm.imagePlane(n='{}_background'.format(tab['name']))
            ip[0].tz.set(-1)
            ip[0].overrideEnabled.set(1)
            ip[0].overrideDisplayType.set(2)
            ip[1].alphaGain.set(background_fade)
            pm.parent(ip[0], picker_grp)
            q_image = QtGui.QImage(tab['data']['background'])

            ip[1].imageName.set(tab['data']['background'])
            ip[1].width.set(q_image.size().width() * factor)
            ip[1].height.set(q_image.size().height() * factor)

        for item in tab['data']['items']:
            handles = item['handles']
            pos_x, pos_y = item['position']

            item_curve = pm.circle(d=1, s=len(item['handles']), ch=False)[0]
            pm.parent(item_curve, picker_grp)
            pm.closeCurve(item_curve, ch=False, ps=2, rpo=True)
            item_curve.displayHandle.set(1)
            if showCVs:
                item_curve.getShape().dispCV.set(1)

            q_color = QtGui.QColor(*item['color'])
            attr.addColorAttribute(item_curve, 'color', q_color.getRgbF()[:3])
            attr.addAttribute(item_curve, 'alpha', 'long', item['color'][3], minValue=0, maxValue=255)

            item_curve.overrideEnabled.set(1)
            item_curve.overrideRGBColors.set(1)
            item_curve.color >> item_curve.overrideColorRGB
            item_curve.rotatePivot >> item_curve.selectHandle

            if 'controls' in item.keys():
                attr.addAttribute(item_curve, 'controls', 'string', json.dumps(item['controls']))

            if 'action_mode' in item.keys():
                attr.addAttribute(item_curve, 'action_mode', 'bool', item['action_mode'])
                attr.addAttribute(item_curve, 'action_script', 'string', item['action_script'])

            if 'menus' in item.keys():
                attr.addAttribute(item_curve, 'menus', 'string', json.dumps(item['menus']))

            if 'text' in item.keys():
                attr.addAttribute(item_curve, 'text_data', 'string', json.dumps({'text': item['text'],
                                                                                 'text_color': item['text_color'],
                                                                                 'text_size': item['text_size']}))

            for i, (x, y) in enumerate(handles):
                item_curve.getShape().controlPoints[i].set(x * factor, y * factor, 0)
            item_curve.t.set(pos_x * factor, pos_y * factor, 0)

def overwrite_picker_data(factor = 0.1, data_node='PICKER_DATA', deleteCurves=True):
    grp = pm.PyNode('pickerData_extraction')
    new_data = {'tabs': []}
    upscale = lambda x, y: [x * (1 / factor), y * (1 / factor)]

    for tab_grp in grp.listRelatives():
        new_data['tabs'].append({'name': tab_grp.name()})
        new_data['tabs'][-1]['data'] = {'items': []}
        bg_imagePlane = tab_grp.listRelatives(type='imagePlane', ad=True)
        if bg_imagePlane:
            new_data['tabs'][-1]['data']['background'] = bg_imagePlane[0].imageName.get()

        for item_curve in [ic for ic in tab_grp.listRelatives() if ic.getShape().type() != 'imagePlane']:
            item_data = {}
            if item_curve.hasAttr('action_mode'):
                item_data['action_mode'] = True
                item_data['action_script'] = item_curve.action_script.get()

            # color
            q_color = QtGui.QColor()
            q_color.setRgbF(*item_curve.color.get())
            q_color.setAlpha(item_curve.alpha.get())
            item_data['color'] = q_color.getRgb()

            # controls
            if item_curve.hasAttr('controls'):
                item_data['controls'] = json.loads(item_curve.controls.get())

            # position
            item_data['position'] = upscale(*list(item_curve.getPivots(ws=True))[0][:2])

            # handles
            item_curve.f.get()
            handles = []
            for cv in item_curve.cv[:-1 if item_curve.f.get() == 0 else None]:
                x, y = upscale(*cv.getPosition(space='world')[:2])
                handles.append([x - item_data['position'][0], y - item_data['position'][1]])
            item_data['handles'] = handles

            # menus
            if item_curve.hasAttr('menus'):
                item_data['menus'] = json.loads(item_curve.menus.get())
            # text
            if item_curve.hasAttr('text_data'):
                item_data.update(json.loads(item_curve.text_data.get()))

            new_data['tabs'][-1]['data']['items'].append(item_data)
    if deleteCurves:
        pm.delete(grp)
    data_node = pm.PyNode(data_node)
    data_node.picker_datas.set(l=False)
    data_node.picker_datas.set(json.dumps(new_data).replace('true', 'True'))
    data_node.picker_datas.set(l=True)

    animPickerUI = pyqt.get_instance(pyqt.get_main_window(), mgear.anim_picker.gui.MainDockWindow)
    animPickerUI.refresh()
8 Likes

@JimBo_Drew
My man!

That is awesome!

Thank you for sharing this. Lets see how we can integrate this.

Also, on the scaling issue referenced.

1 Like

this is great! Thanks @JimBo_Drew!!!

you’re welcome guys :smiley:

1 Like

Awesome addition! The way we dealt with pickers at Disney was something similar: we can alter some basic transforms in the picker itself, but for more advanced shapes, the button was copied in the viewport to be altered, and then back to the picker.

Thanks for sharing!

3 Likes

so i was having a look at adding the text, and i thought i’d check to see if the text is the same between 4k and 1080p i was right to be suspicious, the text draws different on 4k as you can see below

so i had a quick look in code in:
anim_picker.widgets.picker_widgets.GraphicText().set_size()

font.setPointSizeF(value / pm.mayaDpiSetting(q=True, realScaleValue=True))

this fixed it

i saw in another thread i started about a similar issue @JxE said that pm.mayaDpiSetting is not available on mac and suggested using devicePixelRatio so i tried

pyqt.maya_main_window().devicePixelRatio()

however this returns 1 not the expected 1.5 as set in windows.

2 Likes

That’s odd, was your Maya main window on your 4k monitor when you ran the code? We’re seeing it return the correct scaling on our macs when we run that, but maybe it doesn’t work on Windows?

yeah i ran these two code snippets one after the other same maya session

pyqt.maya_main_window().devicePixelRatio()
# Result: 1 # 

pm.mayaDpiSetting(q=True, realScaleValue=True)
# Result: 1.5 #

It’s possible it’s only telling the difference between retina and non-retina displays and maybe your 4k monitor doesn’t count? https://doc.qt.io/qtforpython/PySide2/QtGui/QScreen.html#PySide2.QtGui.PySide2.QtGui.QScreen.devicePixelRatio

There may be a hint here, but I can’t dig into it too much right now: https://doc.qt.io/qt-5/qscreen.html#details

On iMac 5K screen and on Macbook Pro Retina, I get 2.

For fun, in case it helps, here is everything it returns on the 5K screen.

import maya.OpenMayaUI as omui
from mgear.vendor.Qt import QtCompat
from mgear.vendor.Qt import QtWidgets
main_window_ptr = omui.MQtUtil.mainWindow()
maya_main_window = QtCompat.wrapInstance(long(main_window_ptr), QtWidgets.QWidget)

maya_main_window.colorCount()
// Result: 256 //
maya_main_window.depth()
// Result: 24 //
maya_main_window.devicePixelRatio()
// Result: 2 //
maya_main_window.devicePixelRatioF()
// Result: 2.0 //
maya_main_window.heightMM()
// Result: 329 //
maya_main_window.logicalDpiX()
// Result: 72 //
maya_main_window.logicalDpiY()
// Result: 72 //
maya_main_window.paintingActive()
// Result: False //
maya_main_window.physicalDpiX()
// Result: 109 //
maya_main_window.physicalDpiY()
// Result: 108 //
maya_main_window.widthMM()
// Result: 599 //

so my results here are windows 10 some lg 4k monitor these setting
image

maya_main_window.colorCount()
# Result: 256 # 
maya_main_window.depth()
# Result: 32 # 
maya_main_window.devicePixelRatio()
# Result: 1 # 
maya_main_window.devicePixelRatioF()
# Result: 1.0 # 
maya_main_window.heightMM()
# Result: 334 # 
maya_main_window.logicalDpiX()
# Result: 144 # 
maya_main_window.logicalDpiY()
# Result: 144 # 
maya_main_window.paintingActive()
# Result: False # 
maya_main_window.physicalDpiX()
# Result: 163 # 
maya_main_window.physicalDpiY()
# Result: 161 # 
maya_main_window.widthMM()
# Result: 600 # 

the logicalDpi seems to match with what maya setting says maya is but then you get 72, i dunno :crazy_face:

Hey guys, thanks for the information on different machines.

I am testing this solution on anim_pickers and channel master

I have not applied to it to every aspect of the anim_picker ui or channel master, just the parks looking the most affected by the dpi.

2 Likes

Hi JimBo_Drew,

I don’t know how to use the code you posted at the top of the thread but it looks really exciting!!!

Thanks

1 Like

I was testing this out today and realized rotation wasn’t supported. You can read and write rotation by adding:

In extract_picker_data()

        for item in tab['data']['items']:
            rotValue = item['rotation']
            item_curve.rz.set(rotValue)

In overwrite_picker_data()

            item_data['rotation'] = item_curve.rz.get()

Full example:

import pymel.core as pm
from PySide2 import QtGui
import mgear.core.attribute as attr
import mgear.anim_picker
from mgear.core import pyqt
import json


def extract_picker_data(factor = 0.1, background_fade = 0.5, showCVs=True):
    animPickerUI = pyqt.get_instance(pyqt.get_main_window(), mgear.anim_picker.gui.MainDockWindow)
    data = animPickerUI.get_character_data()

    grp = pm.group(em=True, n='pickerData_extraction')
    for tab in data['tabs']:
        picker_grp = pm.group(em=True, n=tab['name'], p=grp)
        if 'background' in tab['data'].keys():
            ip = pm.imagePlane(n='{}_background'.format(tab['name']))
            ip[0].tz.set(-1)
            ip[0].overrideEnabled.set(1)
            ip[0].overrideDisplayType.set(2)
            ip[1].alphaGain.set(background_fade)
            pm.parent(ip[0], picker_grp)
            q_image = QtGui.QImage(tab['data']['background'])

            ip[1].imageName.set(tab['data']['background'])
            ip[1].width.set(q_image.size().width() * factor)
            ip[1].height.set(q_image.size().height() * factor)

        for item in tab['data']['items']:
            handles = item['handles']
            pos_x, pos_y = item['position']
            rotValue = item['rotation']

            item_curve = pm.circle(d=1, s=len(item['handles']), ch=False)[0]
            pm.parent(item_curve, picker_grp)
            pm.closeCurve(item_curve, ch=False, ps=2, rpo=True)
            item_curve.displayHandle.set(1)
            if showCVs:
                item_curve.getShape().dispCV.set(1)

            q_color = QtGui.QColor(*item['color'])
            attr.addColorAttribute(item_curve, 'color', q_color.getRgbF()[:3])
            attr.addAttribute(item_curve, 'alpha', 'long', item['color'][3], minValue=0, maxValue=255)

            item_curve.overrideEnabled.set(1)
            item_curve.overrideRGBColors.set(1)
            item_curve.color >> item_curve.overrideColorRGB
            item_curve.rotatePivot >> item_curve.selectHandle
            
            item_curve.rz.set(rotValue)

            if 'controls' in item.keys():
                attr.addAttribute(item_curve, 'controls', 'string', json.dumps(item['controls']))

            if 'action_mode' in item.keys():
                attr.addAttribute(item_curve, 'action_mode', 'bool', item['action_mode'])
                attr.addAttribute(item_curve, 'action_script', 'string', item['action_script'])

            if 'menus' in item.keys():
                attr.addAttribute(item_curve, 'menus', 'string', json.dumps(item['menus']))

            if 'text' in item.keys():
                attr.addAttribute(item_curve, 'text_data', 'string', json.dumps({'text': item['text'],
                                                                                 'text_color': item['text_color'],
                                                                                 'text_size': item['text_size']}))

            for i, (x, y) in enumerate(handles):
                item_curve.getShape().controlPoints[i].set(x * factor, y * factor, 0)
            item_curve.t.set(pos_x * factor, pos_y * factor, 0)

def overwrite_picker_data(factor = 0.1, data_node='PICKER_DATA', deleteCurves=True):
    grp = pm.PyNode('pickerData_extraction')
    new_data = {'tabs': []}
    upscale = lambda x, y: [x * (1 / factor), y * (1 / factor)]

    for tab_grp in grp.listRelatives():
        new_data['tabs'].append({'name': tab_grp.name()})
        new_data['tabs'][-1]['data'] = {'items': []}
        bg_imagePlane = tab_grp.listRelatives(type='imagePlane', ad=True)
        if bg_imagePlane:
            new_data['tabs'][-1]['data']['background'] = bg_imagePlane[0].imageName.get()

        for item_curve in [ic for ic in tab_grp.listRelatives() if ic.getShape().type() != 'imagePlane']:
            item_data = {}
            if item_curve.hasAttr('action_mode'):
                item_data['action_mode'] = True
                item_data['action_script'] = item_curve.action_script.get()

            # color
            q_color = QtGui.QColor()
            q_color.setRgbF(*item_curve.color.get())
            q_color.setAlpha(item_curve.alpha.get())
            item_data['color'] = q_color.getRgb()

            # controls
            if item_curve.hasAttr('controls'):
                item_data['controls'] = json.loads(item_curve.controls.get())

            # position
            item_data['position'] = upscale(*list(item_curve.getPivots(ws=True))[0][:2])
            item_data['rotation'] = item_curve.rz.get()

            # handles
            item_curve.f.get()
            handles = []
            for cv in item_curve.cv[:-1 if item_curve.f.get() == 0 else None]:
                x, y = upscale(*cv.getPosition(space='world')[:2])
                handles.append([x - item_data['position'][0], y - item_data['position'][1]])
            item_data['handles'] = handles

            # menus
            if item_curve.hasAttr('menus'):
                item_data['menus'] = json.loads(item_curve.menus.get())
            # text
            if item_curve.hasAttr('text_data'):
                item_data.update(json.loads(item_curve.text_data.get()))

            new_data['tabs'][-1]['data']['items'].append(item_data)
    if deleteCurves:
        pm.delete(grp)
    data_node = pm.PyNode(data_node)
    data_node.picker_datas.set(l=False)
    data_node.picker_datas.set(json.dumps(new_data).replace('true', 'True'))
    data_node.picker_datas.set(l=True)

    animPickerUI = pyqt.get_instance(pyqt.get_main_window(), mgear.anim_picker.gui.MainDockWindow)
    animPickerUI.refresh()
1 Like

@Moustache_Animation to use this, copy the entire script and run it. This will define the two functions. You could also save the script somewhere in your scripts directory and use “import”.

Then, to make the curves, run this:
extract_picker_data(factor = 0.1, background_fade = 0.5, showCVs=True)

To put the curves back into your data, make sure you use the correct PICKER_DATA node name, if you are using a different name:
overwrite_picker_data(factor = 0.1, data_node='PICKER_DATA', deleteCurves=True)

Note: The script will fail on the default template biped picker. Because some of the buttons have 2 vertices. The text buttons that are between the space-switch buttons have 2 vertices. If you turn the “Vtx Count” up to 4 on those buttons, it won’t fail anymore.

2 Likes

that’s cool chris, i didn’t know about the 2 vert buttons, totally makes sense that would fail the script though, i will change it to create a curve instead of a circle, i’ve cleaned up the way i store data if it isn’t need by maya also so it shouldn’t miss things if they are not explicitly called like it done here

btw, I just noticed I did something wrong with my rotation. It works, but the 2nd time you build the curves, the curves have double rotation. I guess because your code reads the world orientation of the points, and then I rotate the transform above that.

Hey @JimBo_Drew

I finally got around to adding the additions you shared. Thank you again for that.
Convert to and from curves · Issue #44 · mgear-dev/anim_picker (github.com)

4 Likes

as the title says i have done a bunch of clean up to these functions things seem to work better now, I was having some issues with certain cases, but i think i have fixed all the bugs I found.
now rotations are not baked into verts when you bring the curves back.

I was getting a error when creating 2 point curves after doing some digging I see that this is supposed to create a circle but there was a bug preventing it from working, so i fixed that and implemented support for these circles in maya curves
added some attrs from adjusting background image size on tab group, this group can be move and scaled for convince how ever when you bring the data back this is ignored.
curves are no longer closed as this was causing issues, now if the first and last cv are the same the last is ignored

I have put in a pull request for these changes, let me know if i messed that up as i avery unfamilar with github.

2 Likes

Hi @JimBo_Drew I hope you don’t mind! But I merged this to the old thread so it is easier to find the previous discussion.

I love this tool of yours! It makes editing the picker so easy and intuitive! Looking forward to the changes! :green_heart: