[Closed] Script in progress: automatic sub-object level
EDIT: The latest version of the script can be found in this post: http://forums.cgsociety.org/showpost.php?p=6594345&postcount=7
OLD:
I’m working on a script that, eventually, will let people work with vertices, edges, or faces, all without having to switch back and forth between sub-object levels. This isn’t something that I would like to have on all the time, but I think it would be useful as an optional modeling mode.
Right now it identifies whether the mouse cursor is within a particular range of a vertex, edge, or face, switches to that sub-object level, and selects the vertex, edge, or face. I have borrowed heavily from other scripts (credit given where due), because a) I’m really not an expert and b) I don’t see a need to reinvent the wheel, just so long as I understand why it rolls.
My next goal is going to be figuring out how to move the selected sub-objects without stopping the script.
Although I’m still early in development on this, I would very much appreciate any comments, criticism, suggestions, etc. that might help me along the way. If you see a way to make something better, please let me know!
global selectEdge, selectVert, theHitPoly
----------------------------------------------------------------
-- Function to determine the distance between a point and a line
-- Special thanks to prettyPixel
----------------------------------------------------------------
fn pointLineDist pA pB pC =
(
local vAB = pB - pA
local vAC = pC - pA
(length (cross vAB vAC)) / (length vAB)
)
------------------------------------------------------
-- Function to convert a specified mesh face to a poly
-- Special thanks to magicm (Martijn van Herk)
------------------------------------------------------
fn meshFaceToPoly poly mesh_face =
(
face_verts = meshop.getVertsUsingFace poly.mesh mesh_face
vert_faces = for item in face_verts collect (polyOp.getFacesUsingVert poly item)
result = (vert_faces[1] * vert_faces[2] * vert_faces[3]) as array
result[1]
)
-----------------------------------------------------------------------
-- Function to determine the closest vertex, edge, or face to the mouse
-- Special thanks to Rivendale (Carl-Mikael Lagnecrantz)
-----------------------------------------------------------------------
fn getMouseVerts =
(
vertCount = polyop.getNumVerts $.baseobject
if vertCount != 0 do
(
--backVerts = polyop.getVertsUsedOnlyByFaces $.baseobject (polyop.getFacesByFlag $.baseobject 8)
-- Get array of non-hidden, front facing vertices
backVerts = (polyop.getVertsByFlag $.baseobject 8)
okVerts = (#{1..vertCount} - backVerts - (polyop.getHiddenVerts $.baseobject)) as array
mp = mouse.pos
gw.setTransform (Matrix3 1)
-- Create an array measuring, in screen space, the distance from the mouse of all non-hidden, front facing vertices
distArray = for i in okVerts collect
(
m = gw.wTranspoint (polyop.getVert $.baseobject i node:$) -- locations of vertices in screen space
distance mp [m.x, m.y]
)
minVertDist = findItem distArray (aMin distArray)
selectVert = undefined
if distArray[minVertDist] < 3
then (selectVert = okVerts[minVertDist])
else
(
vertEdges = polyOp.getEdgesUsingVert $ minVertDist
nVerts = ((polyOp.getVertsUsingEdge $ vertEdges) - #{minVertDist}) as array
nDistArray = for i in nVerts collect
(
m = gw.wTranspoint (polyop.getVert $.baseobject i node:$)
distance mp [m.x, m.y]
)
minNVertDist = findItem nDistArray (aMin nDistArray)
closestNVert = nVerts[minNVertDist]
vertEdges = vertEdges as array
nVertEdges = (polyOp.getEdgesUsingVert $ closestNVert) as array
selectEdge = undefined
for i in nVertEdges do
(
if (findItem vertEdges i) != 0 do selectEdge = i
)
a = gw.wTranspoint (polyOp.getVert $.baseObject minVertDist node:$); a.z = 0
b = gw.wTranspoint (polyOp.getVert $.baseObject closestNVert node:$); b.z =0
c = [mp.x, mp.y, 0]
if (pointLineDist a b c) > 3 do
(
selectEdge = undefined
---------------------------------------------
-- Special thanks to ZeBoxx2 (Richard Annema)
rMGI = rayMeshGridIntersect()
rMGI.initialize 50
rMGI.addNode $
rMGI.buildGrid()
theRay = mapScreenToWorldRay mp
theHitFace = undefined
theHit = rMGI.intersectray theRay.pos theRay.dir false
------------------------------------------------------
if theHit != 0 do
(
theHitFace = rMGI.getHitFace 1
theHitPoly = meshFaceToPoly $ theHitFace
)
rMGI.free()
) -- end if/then/else (pointLineDist a b c) < 6
)--end if/then/else (distArray[minVertDist] < 6)
) -- end if (vertCount != 0)
) -- end function (getMouseVerts)
---------------------------------
tool mouseTest
(
on freeMove do
(
getMouseVerts()
if selectVert != undefined
then (if subObjectLevel != 1 do subObjectLevel = 1; $.baseObject.setSelection #Vertex #{selectVert})
else
(
if selectEdge != undefined
then (if subObjectLevel != 2 do subObjectLevel = 2; $.baseObject.setSelection #Edge #{selectEdge})
else
(
if theHitPoly != undefined
then (if subObjectLevel != 4 do subObjectLevel = 4; $.baseObject.setSelection #Face #{theHitPoly})
else subObjectLevel = 0
)
)
)
on mousePoint clickno do #stop
)
startTool mouseTest
What version of max are you using? Max2010 already has a feature called “Preview Selection” built into editable poly objects that pretty much does what you described… unless I’m missing something.
I’m still using Max 9, but it has the same feature you mentioned. Having looked at it though, I don’t really see a lot of similarity to what I’m trying to do.
What I plan on having is something where you can select and move around vertices, edges, or faces without having to constantly click the 1, 2, and 4 keys.
I don’t have to press any keys with preview selection enabled, I enable it, and then I can click the vertex/edge/face I want and then just start moving it in the viewport… I never have to hit 1,2, 4. There is a slight lag, because it is switching sub object modes for you, but I don’t think I can see any way around that with scripting either.
I’ll try taking a closer look at it tonight. Maybe I’m the one missing something, or maybe something was changed between versions.
I’m guessing the preview selection mode changed, because I still haven’t been able to replicate what I wanted. My next step here is to completely remove the need for switching to sub-object mode. The problem I am having now is getting it to work in screen space. I suspect I will have to define a custom relation between the mouse position and the viewport matrix… anyone have any ideas on that?
(Once again, any more general suggestions are always welcome)
global selectEdge, selectVert, theHitPoly, selectType, selectID, theSelect
----------------------------------------------------------------
-- Function to determine the distance between a point and a line
-- Special thanks to prettyPixel
----------------------------------------------------------------
fn pointLineDist pA pB pC =
(
local vAB = pB - pA
local vAC = pC - pA
(length (cross vAB vAC)) / (length vAB)
)
------------------------------------------------------
-- Function to convert a specified mesh face to a poly
-- Special thanks to magicm (Martijn van Herk)
------------------------------------------------------
fn meshFaceToPoly poly mesh_face =
(
face_verts = meshop.getVertsUsingFace poly.mesh mesh_face
vert_faces = for item in face_verts collect (polyOp.getFacesUsingVert poly item)
result = (vert_faces[1] * vert_faces[2] * vert_faces[3]) as array
result[1]
)
----------------------------------------------------------
-- Function to determine the closest vertex to the mouse
-- Special thanks to Rivendale (Carl-Mikael Lagnecrantz)
----------------------------------------------------------
fn getMouseSel =
(
vertCount = polyop.getNumVerts $.baseobject
if vertCount != 0 do
(
--backVerts = polyop.getVertsUsedOnlyByFaces $.baseobject (polyop.getFacesByFlag $.baseobject 8)
-- Get array of non-hidden, front facing vertices
backVerts = (polyop.getVertsByFlag $.baseobject 8)
okVerts = (#{1..vertCount} - backVerts - (polyop.getHiddenVerts $.baseobject)) as array
mp = mouse.pos
gw.setTransform (Matrix3 1)
-- Create an array measuring, in screen space, the distance from the mouse of all non-hidden, front facing vertices
distArray = for i in okVerts collect
(
m = gw.wTranspoint (polyop.getVert $.baseobject i node:$) -- locations of vertices in screen space
distance mp [m.x, m.y]
)
minVertDist = findItem distArray (aMin distArray)
selectVert = undefined
if distArray[minVertDist] < 3
then
(
selectVert = okVerts[minVertDist]
selectType = #Vertex; selectID = selectVert; theSelect = $.selectedVerts -- set selection info
)
else
(
vertEdges = polyOp.getEdgesUsingVert $ minVertDist
nVerts = ((polyOp.getVertsUsingEdge $ vertEdges) - #{minVertDist}) as array
nDistArray = for i in nVerts collect
(
m = gw.wTranspoint (polyop.getVert $.baseobject i node:$)
distance mp [m.x, m.y]
)
minNVertDist = findItem nDistArray (aMin nDistArray)
closestNVert = nVerts[minNVertDist]
vertEdges = vertEdges as array
nVertEdges = (polyOp.getEdgesUsingVert $ closestNVert) as array
selectEdge = undefined
for i in nVertEdges do
(
if (findItem vertEdges i) != 0 do (selectEdge = i)
)
a = gw.wTranspoint (polyOp.getVert $.baseObject minVertDist node:$); a.z = 0
b = gw.wTranspoint (polyOp.getVert $.baseObject closestNVert node:$); b.z =0
c = [mp.x, mp.y, 0]
if (pointLineDist a b c) > 3
then
(
selectEdge = undefined
---------------------------------------------
-- Special thanks to ZeBoxx2 (Richard Annema)
rMGI = rayMeshGridIntersect()
rMGI.initialize 50
rMGI.addNode $
rMGI.buildGrid()
theRay = mapScreenToWorldRay mp
theHitFace = undefined
theHit = rMGI.intersectray theRay.pos theRay.dir false
------------------------------------------------------
if theHit != 0 do
(
theHitFace = rMGI.getHitFace 1
theHitPoly = meshFaceToPoly $ theHitFace
selectType = #Face; selectID = theHitPoly; theSelect = $.selectedFaces -- set selection info
)
rMGI.free()
)
else (selectType = #Edge; selectID = selectEdge; theSelect = $.selectedEdges) -- set selection info
-- end if/then/else (pointLineDist a b c) > 3
)--end if/then/else (distArray[minVertDist] < 3)
) -- end if (vertCount != 0)
) -- end function (getMouseSel)
---------------------------------
global mpLast
global restartTool = false
tool mouseTest
(
on freeMove do (getMouseSel())
on mousePoint clickno do
(
if clickno == 1 do
(
$.baseObject.setSelection selectType #{selectID}
format "selected % %
" selectType selectID
mpLast = mouse.pos
)
)
on mouseMove clickno do
(
if lbutton == true
then
(
mp = mouse.pos
in coordsys screen (move theSelect [(mp.x - mpLast.x), (mpLast.y - mp.y), 0])
mpLast = mp
)
else (restartTool = true; #stop)
)
on mouseAbort clickno do (restartTool = false)
on stop do (if restartTool == true do startTool mouseTest)
)
startTool mouseTest
Latest version below. My next issues are
-
Getting it to work in a perspective view (the direction of dragging works, but the distances get skewed.)
-
Setting up some kind of incremental undo, rather than jumping to before the script is executed.
try (delete screenGrid) catch()
global screenGrid = grid name:"Screen_Grid" isHidden:true
global selectEdge, selectVert, theHitPoly, selectLevel, selectType, selectID, theSelect, gpLast
global restartTool = false
----------------------------------------------------------------
-- Function to determine the distance between a point and a line
-- Special thanks to prettyPixel
----------------------------------------------------------------
fn pointLineDist pA pB pC =
(
local vAB = pB - pA
local vAC = pC - pA
(length (cross vAB vAC)) / (length vAB)
)
------------------------------------------------------
-- Function to convert a specified mesh face to a poly
-- Special thanks to magicm (Martijn van Herk)
------------------------------------------------------
fn meshFaceToPoly poly mesh_face =
(
face_verts = meshop.getVertsUsingFace poly.mesh mesh_face
vert_faces = for item in face_verts collect (polyOp.getFacesUsingVert poly item)
result = (vert_faces[1] * vert_faces[2] * vert_faces[3]) as array
result[1]
)
----------------------------------------------------------
-- Function to determine the subObject below the mouse
-- Special thanks to Rivendale (Carl-Mikael Lagnecrantz)
----------------------------------------------------------
fn getMouseSel =
(
selectVert = undefined
vertCount = polyop.getNumVerts $.baseobject
if vertCount != 0 do
(
-- Get array of non-hidden, front facing vertices
backVerts = (polyop.getVertsByFlag $.baseobject 8)
okVerts = (#{1..vertCount} - backVerts - (polyop.getHiddenVerts $.baseobject)) as array
mp = mouse.pos
gw.setTransform (Matrix3 1)
-- Create an array measuring, in screen space, the distance from the mouse of all non-hidden, front facing vertices
distArray = for i in okVerts collect
(
m = gw.wTranspoint (polyop.getVert $.baseobject i node:$) -- locations of vertices in screen space
distance mp [m.x, m.y]
)
minVertDist = findItem distArray (aMin distArray)
if distArray[minVertDist] < 6
then
(
selectVert = okVerts[minVertDist] -- nearest vertex
selectLevel = 1; selectType = #Vertex; selectID = selectVert; theSelect = $.selectedVerts -- set selection info
)
else
(
theHitPoly = undefined
-------------------------------------------------------------------------------------
-- Find the next-closest vertex to the mouse and use it to determine the closest edge
vertEdges = polyOp.getEdgesUsingVert $ minVertDist -- all edges containing nearest vertex
nVerts = ((polyOp.getVertsUsingEdge $ vertEdges) - #{minVertDist}) as array -- i.e. "neighbor vertices"
nDistArray = for i in nVerts collect
(
m = gw.wTranspoint (polyop.getVert $.baseobject i node:$)
distance mp [m.x, m.y]
)
minNVertDist = findItem nDistArray (aMin nDistArray)
closestNVert = nVerts[minNVertDist]
vertEdges = vertEdges as array
nVertEdges = (polyOp.getEdgesUsingVert $ closestNVert) as array
for i in nVertEdges do (if (findItem vertEdges i) != 0 do (selectEdge = i))
---------------------------------------------------------------------------
----------------------------------------------------------------------------
-- Find the distance from the mouse to the closest point on the closest edge
a = gw.wTranspoint (polyOp.getVert $.baseObject minVertDist node:$); a.z = 0
b = gw.wTranspoint (polyOp.getVert $.baseObject closestNVert node:$); b.z =0
c = [mp.x, mp.y, 0]
eDist = pointLineDist a b c
---------------------------
if eDist > 4
then
(
selectEdge = undefined
---------------------------------------------
-- Special thanks to ZeBoxx2 (Richard Annema)
rMGI = rayMeshGridIntersect()
rMGI.initialize 50
rMGI.addNode $
rMGI.buildGrid()
theRay = mapScreenToWorldRay mp
theHit = rMGI.intersectray theRay.pos theRay.dir false
------------------------------------------------------
if theHit != 0
then
(
-- Determine which face is under the mouse
theHitFace = rMGI.getHitFace 1
theHitPoly = meshFaceToPoly $ theHitFace
selectLevel = 4; selectType = #Face; selectID = theHitPoly; theSelect = $.selectedFaces -- set selection info
)
else theSelect = undefined
-- end if/then/else (theHit != 0)
rMGI.free() -- clear rayMeshGridIntersect memory
)
else (selectLevel = 2; selectType = #Edge; selectID = selectEdge; theSelect = $.selectedEdges) -- set selection info
)--end if/then/else (distArray[minVertDist] < 6)
) -- end if (vertCount != 0)
$.baseObject.setSelection selectType #{selectID}
--if subObjectLevel != selectLevel do subObjectLevel = selectLevel
) -- end function (getMouseSel)
---------------------------------
-- Function to set a grid equivalent to the viewport
fn updateGrid = (screenGrid.transform = inverse(getViewTM()); screenGrid.pos = [0,0,0])
-- Tool to drag and move the vertices, edges, or faces directly under the mouse
tool dragSubObject
(
local p, createPoint
on start do (activeGrid = screenGrid; updateGrid())
on freeMove do (getMouseSel(); updateGrid())
on mousePoint clickno do (getMouseSel(); if clickno == 1 do (gp = gridPoint; gpLast = gp))
on mouseMove do (updateGrid())
on mouseMove clickno do
(
if lbutton == true -- if the left button is held down
then
(
if theSelect != undefined do
(
-- Move the selected sub-object in accordance with mouse movement
$.baseObject.setSelection selectType #{selectID}
gp = gridPoint
move theSelect ((gp - gpLast) * screenGrid.transform) -- special thanks to Gravey (Joel Hewitt)
gpLast = gp
)
)
else (restartTool = true; #stop) -- stop the mouse tool and prepare to restart it
) -- end handler mouseMove
on mouseAbort clickno do (restartTool = false) -- do not restart mouse tool
on stop do (if restartTool == true do startTool dragSubObject) -- restart mouse tool unless aborted
)
if $ != undefined do startTool dragSubObject
Hi Kevin, I don’t want to sound bad, but if I get it right, the script you are writing is the Tweak instrument included in IC.Shape 2.0. When you select an Editable Poly and are on level 0, the element (vertex, edge, poly) right under the mouse is highlighted and can be freely dragged in screen space, without switching to any sub-level.
If you are doing this for learning purpose, I can tell you I used the RayMeshGridIntersect to perform the hit tests, coupled with a sort of hacked MouseTrack function. The viewport drawing is obtained through a heavily hacked SimpleManipulator Plugin, but you can use gw drawing functions as well, they’re just a bit flickering, but work. You can find sample code about many of the aspects you’re studying on IllusionCatalyst website under MaxScript page.
- Enrico
Wow, that looks like some really cool stuff there, and a lot to take in! I’ll check it out tonight if I get a chance.
As for the RayMeshGridIntersect, if you take a look you’ll see I have already incorporated that into the script. I’m not using MouseTrack, although the tool function seems to work in more or less the same manner. It’s quite likely that you guys found a more efficient way of doing a lot of this stuff than I have though, so I will definitely see what your solutions were.
Right now, as it says, my main concerns are the incremental undo and the perspective viewport. Of the latter, I get the impression that I will find what I need from the MaxScript page on your site!
(I did notice that the user agreement for IC includes the phrase “cannot be sold, included or distributed within any type of retail sales product” … Since I may want to include the tools I am developing as components of a commercial script at some future time, I’m going to have to be careful about this.)
Well, that sort of license is still there from the first version of the script, where the code wasn’t encrypted. I did it because I didn’t like to see months of work ripped apart and copy/pasted without even asking or stating the author… Anyway, if you want, I can provide code about many different sections, the only issue is that IC.Shape is highly integrated and hard to take apart to pick a single instrument.
About the perspective viewport, you can find information in section “Practical Space Mapping for interaction”, it should answer your questions. I guess you only need to adjust the distance by the dot product resulting between the ray cast and the view axis.
The backbone of the “tweaked” MouseTrack function can be found under:“Tracker and Painter Structures”. I made it easier to handle splitting the pipeline into individual clear functions.
The undo integrated into the MouseTrack function is handled directly invoking theHold object from the SDK. A simplified structure can be found under “System Structures”. Add it to your program, and the StepState structure too (filed under “Data Handling Structures”), then in the Tracker function, add the calls:
function tracking nMessage rHit oMesh iMeshFace bShift bCtrl bAlt =
(
local nStep = #continue
nState.stepTo nMessage
if ( ( (nState.prev == #freeMove) and (nState.curr == #mousePoint) ) or \
( (nState.prev == #mousePoint) and (nState.curr == #mousePoint) ) ) then -- left click
(
leftClick &nStep &rHit &oMesh &iMeshFace &bShift &bCtrl &bAlt
)
else if ( ( (nState.prev == #freeMove) and (nState.curr == #mouseMove) ) or \
( (nState.prev == #mousePoint) and (nState.curr == #mouseMove) ) ) then -- start drag
(
[B]theUndo.setHold()[/B]
startDrag &nStep &rHit &oMesh &iMeshFace &bShift &bCtrl &bAlt
)
else if ( (nState.prev == #mouseMove) and (nState.curr == #mouseMove) ) then -- drag
(
drag &nStep &rHit &oMesh &iMeshFace &bShift &bCtrl &bAlt
)
else if ( (nState.prev == #mouseMove) and (nState.curr == #mousePoint) ) then -- end drag
(
endDrag &nStep &rHit &oMesh &iMeshFace &bShift &bCtrl &bAlt
[B]theUndo.getHold "ActionName"[/B]
)
else if (nState.curr == #mouseAbort) then -- right click
(
rightClick &nStep &rHit &oMesh &iMeshFace &bShift &bCtrl &bAlt
)
else -- free move
(
freeMove &nStep &rHit &oMesh &iMeshFace &bShift &bCtrl &bAlt
)
return nStep
),
and you should get an undo step for each stroke. And don’t worry about licenses, I’m the author, do whatever you want with it
- Enrico
I was trying to work out a way to do face highlighting a while ago but never finished it. I’m curious to see how it looks but the download link for IC.Shape seems to be not a link any more… Any chance of an explanation of how you achieve it?