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…
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.
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();
newSel.SetSize(nmesh.nume);
newSel.ClearAll();
IMNMeshUtilities8* meshToLoop = static_cast<IMNMeshUtilities8*>( nmesh.GetInterface (IMNMESHUTILITIES8_INTERFACE_ID) );
if (single)
{
meshToLoop->SelectEdgeLoopShift(dir*cnt, newSel);
}else{
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);
mm.GetEdgeSel(currentSel);
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
[b]getEdgeSelectionShift [/b]obj dir action type:<> edges:<bitarray> select:<bool>
where: 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)
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);