Edit_Poly LoopSelect behavior


For the life of me, I can’t find the logic (if there’s any) behind LoopSelect behavior in the Edit_Poly modifier. It should pretty much map to SetLoopShift behavior in Editable_Poly, the parameters are the same, the functions called behind the scenes are named the same. Yet it seems to give me totally unpredictable results (and it’s not based on if I hold ctrl, shift or alt when running it, I tried clicking on the Evaluate All in script editor with the same results). Here’s what happens when repeatedly running $.SetLoopShift -1 false true with editable poly:

Nice and repeatable addition in one direction, cool. With Edit_Poly using i.LoopSelect -1 false true[/i] either nothing happens at all or one neighboring edge gets selected and that’s it. Funnily enough, if you keep alternating -1 with 1 to select up and down, it will work properly (almost, whether the first one will select anything is a lottery, but after that, it’s stable):

As a sanity check, I tried the same with EpModSetLoopShift but the behavior was the same as with LoopSelect


do not know why…but if I use -2 in place of -1…it’s faaaaaaar more stable !!!

(or 2 instead of 1 if you want to go positive)

Basically, abs > 1 is more stable


i remember this unstable behavior too. that’s why i rewrote this piece of code in my mxs extension


I think while trying to implement this…the unfortunate thing is the programmer prolly did it one drunken night and got his knickers in a twist…

Shifting one or all edge loops by three using all possible argument combinations:



Good to have a confirmation from you. Did you use EpModUpdateLoopEdgeSelection at all or did you rewrite it from scratch?


Close but not quite.

There are issues with certain edges, open edges and it doesnot always play well if you alternate between it and the EditPoly UI spinner.

    try destroydialog ::RO_LOOP catch()
    rollout RO_LOOP "" width:72
        button   bt1  "+1" width:48 height:32
        button   bt2  "-1" width:48 height:32
        checkbox chk1 "Add"
        local lastdir
        local count = 1
        fn Loop dir:1 =
            md = modPanel.getCurrentObject()
            if lastdir != dir do count = 1
            lastdir = dir

            sel = md.GetSelection #Edge
            md.LoopSelect (count*dir) true false
            count = amin (count += 1) 3
            if chk1.checked or keyboard.controlPressed do md.SetSelection #Edge (sel + md.GetSelection #Edge)
        on bt1 pressed do Loop dir:1
        on bt2 pressed do Loop dir:-1
    createdialog RO_LOOP


Close enough to be useful most of the time, I really like it, thanks!
Worth noting that the problem after using the UI is also present when switching between different objects with different modifiers since it’s not enough to just reset values if the current object changes but you’d have to keep a list of previous modifiers with their current values.


the sdk base is :

           IMNMeshUtilities8* meshToStep = static_cast<IMNMeshUtilities8*>(mesh.GetInterface( IMNMESHUTILITIES8_INTERFACE_ID )); 
           // or // meshToStep->SelectEdgeRingShift(dir,newSel);

it works correct on this level


Yes, it has a weird behavior. Just posted it so perhaps you could find something else, but I wouldn’t actually use it for anything as it is.

Here is a C++/MXS code that may work. I would plan better the C++ method, but currently it is flexible enough to be modified from MXS. Tested on Max 2014

def_visible_primitive(LoopSelect, "LoopSelect");
Value* LoopSelect_cf(Value** arg_list, int count)
    check_arg_count_with_keys(LoopSelect, 3, count);
    int  dir    = (arg_list[0]->to_int()<0) ? -1 : 1;
    int  cnt    = arg_list[1]->to_int();
    BOOL single = arg_list[2]->to_bool();

    BaseObject* base = GetCOREInterface()->GetCurEditObject();

    if (base->ClassID() == EDIT_POLY_MODIFIER_CLASS_ID)
        BitArray newSel;

        EPolyMod13* ep = (EPolyMod13*) base->GetInterface(EPOLY_MOD13_INTERFACE);
        MNMesh &nmesh = *ep->EpModGetMesh();


        IMNMeshUtilities8* meshToLoop = static_cast<IMNMeshUtilities8*>( nmesh.GetInterface (IMNMESHUTILITIES8_INTERFACE_ID) );

        if (single)
            meshToLoop->SelectEdgeLoopShift(dir*cnt, newSel);
            for (int j=0; j<cnt; j++) meshToLoop->SelectEdgeLoopShift(dir*(j+1), newSel);

        return new BitArrayValue(newSel);
    return &undefined;

    try destroydialog ::RO_LOOP catch()

    rollout RO_LOOP "" width:88
        button       bt1        "+"           pos:[ 8, 8] width:32 height:24
        button       bt2        "-"           pos:[48, 8] width:32 height:24
        spinner      sp_count   "Count:"      pos:[ 8,40] fieldwidth:28 type:#integer range:[1,100,1]
        checkbox     chk_single "Single Edge" pos:[ 8,64]
        radiobuttons rb_action  ""            pos:[ 8,84] labels:#("Move", "Add", "Remove")
        fn LoopEdgeSelection dir =
            md = modPanel.getCurrentObject()
            if not iskindof md Edit_Poly do return()
            oldSel = md.GetSelection #Edge
            count  = sp_count.value
            single = chk_single.checked
            result = case rb_action.state of
                1: LoopSelect dir count true
                2: oldSel + (LoopSelect  dir count single)
                3: oldSel * (LoopSelect -dir count true)
            undo "Edge Loop"on md.SetSelection #Edge result

        on bt1 pressed do LoopEdgeSelection  1
        on bt2 pressed do LoopEdgeSelection -1

    createdialog RO_LOOP


Perfect, that settles it. Since I don’t have to care about older versions and I kinda prefer C# whenever I can use it, this is, here’s my version (I think NativePointer was called handle before and BitArray.Create wasn’t there before max 2016 so it definitely won’t work on older max):

if not isKindOf EPolyMod dotNetObject do 
    local compilerParams = dotNetObject "System.CodeDom.Compiler.CompilerParameters" #(
        getDir #maxRoot + "Autodesk.Max.dll",
        getDir #maxRoot + "MaxPlusDotNet.dll", "System.Core.dll",
        getDir #maxRoot + "\bin\assemblies\Autodesk.Max.Wrappers.dll")
    compilerParams.GenerateInMemory = true

    local compilerResults = (dotNetObject "Microsoft.CSharp.CSharpCodeProvider").CompileAssemblyFromSource compilerParams #(
        "using System;
        using Autodesk.Max;
        using System.Linq;
        using System.Collections.Generic;
        using Wrappers = Autodesk.Max.Wrappers;
        using Core = Autodesk.Max.MaxPlus.Core;
        using Constants = Autodesk.Max.MaxPlus.Constants;
        using InterfaceIds = Autodesk.Max.MaxPlus.InterfaceIds;

        internal static class IBitArrayExtensions {
            // https://ephere.com/autodesk/max/forums/general/thread_2527.html
            public static IBitArray BitwiseOr(this IBitArray A, IBitArray B) {
                if (A.Size > B.Size) B.SetSize(A.Size, 1); else A.SetSize(B.Size, 1);
                return B.BitwiseXor(A.BitwiseXor(A.BitwiseAnd(B)));

            public static IEnumerable<int> ToEnumerable(this IBitArray ba) {
                for (int i = 0; i < ba.Size; i++) if (ba[i] > 0) yield return i;

        class EPolyMod {
            private static readonly IGlobal Global = GlobalInterface.Instance;
            private static readonly IInterface_ID EPolyModInterfaceID = GetInterfaceID(InterfaceIds.EpolyMod);
            private static readonly IInterface_ID IMNMeshUtilities8 = GetInterfaceID(InterfaceIds.Imnmeshutilities8);

            private static IInterface_ID GetInterfaceID(Autodesk.Max.MaxPlus.Interface_ID id) {
                return Global.Interface_ID.Create(id.GetPartA(), id.GetPartB());

            private static IBaseInterface GetInterface(UIntPtr handle, IInterface_ID interfaceID) {
                return Global.Animatable.GetAnimByHandle(handle).GetInterface(interfaceID);

            public static int[] GetShiftedLoop(UIntPtr epmHandle, int dir, int count, bool moveOnly, bool add) {
                var ePoly = (IEPolyMod)GetInterface(epmHandle, EPolyModInterfaceID);
                var mm = ePoly.EpModGetMesh(null);
                var marshaller = Wrappers.CustomMarshalerIMNMeshUtilities8.GetInstance(string.Empty);
                var mmu = (IIMNMeshUtilities8)marshaller.MarshalNativeToManaged((mm.GetInterface(IMNMeshUtilities8) as Autodesk.Max.Wrappers.BaseInterface).INativeObject__NativePointer);

                var newSel = Global.BitArray.Create(mm.Nume);
                var currentSel = Global.BitArray.Create(mm.Nume);

                if (moveOnly || !add) mmu.SelectEdgeLoopShift(dir * count, newSel);
                else for (int shift = 1; shift <= count; shift++)
                    mmu.SelectEdgeLoopShift(dir * shift, newSel);
                if (!moveOnly) newSel = add ? currentSel.BitwiseOr(newSel) : currentSel.BitwiseAnd(newSel);

                return newSel.IsEmpty ? new int[0] : newSel.ToEnumerable().Select(i => i + 1).ToArray();
    for err = 0 to compilerResults.errors.count - 1 do print (compilerResults.errors.item[err].ToString())
    ::EPolyMod = compilerResults.CompiledAssembly.CreateInstance "EPolyMod"

fn setLoopShift poly loopShift moveOnly add = if isKindOf poly Edit_Poly do
    local count = abs loopShift
    local dir = loopShift / count

    local newSel = EPolyMod.GetShiftedLoop (getHandleByAnim poly) dir count moveOnly add as bitArray

    poly.SetSelection #Edge #{}
    poly.Select #Edge newSel
setLoopShift (modPanel.getCurrentObject()) -3 off on

An old question

why is Epoly Modifier only? You can use this interface for Editable Poly object as well


[b]getEdgeSelectionShift [/b]obj dir action type:<> edges:<bitarray> select:<bool> 

obj - editable poly or edit poly modifier
dir - direction and number steps
action - #move or #shrink or #grow
type - #loop and #ring
edges - list of old edges (default - current selection)
select - select or not select new edges
returns - new edge selection

here is a logic of my function… (sorry but i can’t share the code because of a little trick i use, which changes everything and makes it better than MAX)


but I can explain where the problem is:

try to make a cylinder and select full stack of edges for all loops including cap loops

shift them

you should see that cap edges shift-move in the opposite direction than all other… it’s just an example, but you can meet this issue very often with poly objects.
the idea is to shift-move them all in the same direction (it doesn’t matter forward or backward).

another problem is similar - make for all loops(rings) shift-shrink and shift-grow in the same direction.


Pretty much because of the necessity of MXS SetSelection in edit_poly case… Makes it kinda wonky, and EPMeshSetEdgeFlags doesn’t seem to play well with the MN_EDITPOLY_OP_SELECT flag


You can see the same with a default plane, for example, if you select just two opposite open edges. They will shift in opposite directions.

The algorithm seems to be based on two rules:

  • Loop edges clusters.
  • Edges vertices order. Where for each cluster, the lower index edge is the one that leads the direction in which the loop will move.

So if the edge verts are #(1,2) the loop will move in one direction and if they are #(2,1) it will move in the opposite direction. As soon as you add an edge to a cluster, it could change the direction if it has the lower edge index and reversed vertices order compared with the previous leading edge.

With a custom algorithm, you can’t predict in which direction it will move, but you can make all the clusters to move in the same direction.


Vojtech, do you know if it is possible to cast the IIMNMeshUtilities8 interface from MXS?
This line:

var mmu = (IIMNMeshUtilities8)marshaller. MarshalNativeToManaged((mm. GetInterface(IMNMeshUtilities8) as Autodesk.Max.Wrappers.BaseInterface). INativeObject__NativePointer);


that’s what i do.


I don’t know of a way to do that
maybe by (ab)using Convert.ChangeType to replace casts it could work?