Skip to content

Instantly share code, notes, and snippets.

@Eterea
Last active August 12, 2020 08:25
Show Gist options
  • Save Eterea/0a9ea75a643ab05880ee3a735229d33b to your computer and use it in GitHub Desktop.
Save Eterea/0a9ea75a643ab05880ee3a735229d33b to your computer and use it in GitHub Desktop.
#python
# ------------------------------------------------------------------------------------------------
# NAME: etr_distanceCalculatorPro.py
# VERS: 1.1.2
# DATE: August 12, 2020
#
# MADE: Cristobal Vila, etereaestudios.com
# With snippets kindly shared by Shawn Frueh, shawnfrueh.com/me/ and also 'robberyman'
#
# USES: Script to calculate distances from selected verts/edges/polygons/items
# No matter these components are from different meshes,
# and also with procedural, deformed or animated ones
#
# CASE: - If only 1 vert/item is selected, we get the World Coordinates in 'X,Y,Z' fields
# - With various verts/edges/items selected (in order) we get the TOTAL accumulated distance
# - Note that, with polygons, we get '0' in 'TOTAL'
# - For all these cases (except 1 vert/item), we get the Bounding Box sizes in 'X,Y,Z'
# - 'Longest' will facilitate you the lecture of longest bbox size, just that
# - 'Selected' field is insteresting to be sure 'what' and 'how many' you selected
# - 'Copy ETR to Nudge Move' will copy the BBox data to our Nudge Move Palette
# — There is also a 'Copy ABS to Nudge Move' for those cases you use that native palette
# ------------------------------------------------------------------------------------------------
import lx
import lxu
import math
import modo
# ------------------------------------------------------------------------------------------------
#
# FUNCTIONS
#
# ------------------------------------------------------------------------------------------------
# To know our selection mode
# ------------------------------------------------------------------------------------------------
def selmode(*types):
if not types:
types = ('vertex', 'edge', 'polygon', 'item', 'pivot', 'center', 'ptag')
for t in types:
if lx.eval("select.typeFrom %s;vertex;edge;polygon;item;pivot;center;ptag ?" %t):
return t
mySelMode = selmode()
components = ['vertex', 'edge', 'polygon']
# Dialog for when user is in wrong mode or nothing is selected. And some advices
# ------------------------------------------------------------------------------------------------
def fn_dialogAdvice():
lx.eval("dialog.setup info")
lx.eval("dialog.title {Eterea Distance Calculator PRO}")
lx.eval("dialog.msg {Be sure to select at least 1 vert, 1 edge, 1 poly or 1 item (or more).\n\
- If only 1 vert/item is selected, we get the World Coordinates in 'X,Y,Z' fields.\n\
- With various verts/edges/items selected (in order) we get the TOTAL accumulated distance.\n\
- Note that, with polygons, we get '0' in 'TOTAL'.\n\
- For all these cases (except 1 vert/item), we get the Bounding Box sizes in 'X,Y,Z'.\n\
- You can select from any mesh, standard or procedural, deformed, animated, whatever.}")
lx.eval("dialog.open")
lx.eval("user.value etr_gi_selected (none)")
sys.exit()
"""
------------------------------------------------------------------------------------------------
SUPER VERTS POSITION CALCULATOR
------------------------------------------------------------------------------------------------
Next pure Python API snippet kindly shared by Shawn Frueh:
https://foundry-modo.slack.com/archives/C6F918JEB/p1596930293212700
THIS IS WHAT REALLY MAKES THIS TOOL SO POWERFULL. A BIG THANKS TO SHAWN FOR THIS!
------------------------------------------------------------------------------------------------
Here is a pure API method that uses the selection service to get all selected points on any object in Modo.
This will include points that are selected even in ghost mode of the procedural stack.
When getting the selection mesh, it should be the "deformed" version that you are seeing in the view-port,
thus working with procedurals. If you want to also handle global positions you will have to multiply the point
by it's items world matix. I hope this helps! Tried to break it down as simple as possible.
I does look like a lot of code but the joy is once you wrap this bad boy up into a package/library
that you just import you never have to worry about it again. That's essentially where this came from.
I have a method that's a bit more advanced than this that also handles the other elements.
"""
def fn_get_selVertsPos(world=False):
# Args: world (bool): If true, return the world positions, else return local
# Returns: list(Vector3): List with XYZ positions for each selected vert
# Initialize the selection service
SELECTION_SERVICE = lxu.service.Selection()
# Get the vertex selection int type: 1447383640
vertex_selection_type = SELECTION_SERVICE.LookupType(lx.symbol.sSELTYP_VERTEX)
# Get the selection object: lxu.object.SelectionType
vertex_selection_object = SELECTION_SERVICE.Allocate(lx.symbol.sSELTYP_VERTEX)
# Convert that type into a packet so that we can access the selection data
vertex_packet = lx.object.VertexPacketTranslation(vertex_selection_object)
# Get the total count of selected points
selected_vertex_count = SELECTION_SERVICE.Count(vertex_selection_type)
position_list = []
# Iterate over each point and get some data. These points could be coming from multiple meshes
# and so you will need to check the item they be
for vertex in range(selected_vertex_count):
# Get a pointer to the vertex data in the given index.
vertex_pointer = SELECTION_SERVICE.ByIndex(vertex_selection_type, vertex)
# If we don't get a pointer, skip to the next index in the loop.
# This is necessary to prevent any crashes.
if not vertex_pointer:
continue
# Get the id data from the pointer: ex. 495128304
vertex_id = vertex_packet.Vertex(vertex_pointer)
# Get the item the point belongs to: lxu.object.Item
selection_item = vertex_packet.Item(vertex_pointer)
# Get the matrix channel index
matrix_index = selection_item.ChannelLookup("worldMatrix")
# Prep the selection item to be read at the current time. You want those animated points!
selection_item.ReadEvaluated(SELECTION_SERVICE.GetTime())
# Get the matrix from the channel as a COM object and convert it to a matrix object.
item_world_matrix = lx.object.Matrix(selection_item.ChannelValue(matrix_index))
# Get the mesh item the point belongs to: lxu.object.Mesh
selection_mesh = vertex_packet.Mesh(vertex_pointer)
# Get the point accessor of the mesh item: lxu.object.Point
point_item = selection_mesh.PointAccessor()
# Select the vertex in the accessor so that we can query any Point data.
point_item.Select(vertex_id)
# Get the position of the internally selected point. This point is relative to the mesh
# So if the mesh is not zero you will need to multiply this position by the transform
# data of the item it belongs to.
localPosition = point_item.Pos()
# Get the world position of the point by multiplying it by the world matrix of it's item.
worldPosition = item_world_matrix.MultiplyVector(point_item.Pos())
# Deliver world or local depending on world argument ('true', 'false')
if world:
position_list.append(worldPosition)
else:
position_list.append(localPosition)
return position_list
# Calculate the bounding box for selected vertices. This will gives us the BBox per axis, separately.
# ------------------------------------------------------------------------------------------------
def fn_calculateVertsBBox(axis):
selVerts_pos_axis = []
for vert_pos in selVerts_pos:
selVerts_pos_axis.append(vert_pos[axis])
selVerts_pos_axis.sort()
distAxis = ( selVerts_pos_axis[-1] - selVerts_pos_axis[0] )
return distAxis
# Calculate the bounding box for selected items.
# ------------------------------------------------------------------------------------------------
def fn_calculateItemsBBox(axis):
item_pos = []
for element in selItems:
item_pos_single = lx.eval("query sceneservice item.worldPos ? " + str(element))
if not item_pos_single[axis] in item_pos:
item_pos.append(item_pos_single[axis])
item_pos.sort()
distAxis = ( item_pos[-1] - item_pos[0] )
return distAxis
# Define 'smart' rounds depending on ranges (because Modo shows distances in a 'smart' way, using mm, cm, m, etc)
# ------------------------------------------------------------------------------------------------
def fn_smartround(number):
if number < 0.001: # From 0 to 1mm
return round(number, 8)
elif number >= 0.001 and number < 0.01: # From 1mm to 1cm
return round(number, 5)
elif number >= 0.01 and number < 1: # From 1cm to 1m
return round(number, 4)
elif number >= 1 and number < 1000: # From 1m to 1km
return round(number, 2)
else: # From 1km to infinite
return round(number, -1)
# ------------------------------------------------------------------------------------------------
#
# PRELIMINARY STUFF
#
# ------------------------------------------------------------------------------------------------
# Start defining our distance variables as zero
distT = 0 # Total (accumulated, for verts, edges and items)
distX = 0 # BBox X
distY = 0 # BBox Y
distZ = 0 # BBox Z
distM = 0 # Maximum of XYZ
# To show '(none)' when nothing is selected
selected = '(none)'
# Check User Value for when 'Round' is enabled:
roundcheck = lx.eval("user.value etr_gi_roundnumber ?")
# ------------------------------------------------------------------------------------------------
# ------------------------------------------------------------------------------------------------
#
# LETS GO
#
# ------------------------------------------------------------------------------------------------
# ------------------------------------------------------------------------------------------------
# Initialize the selection service
SELECTION_SERVICE = lxu.service.Selection()
# --------------------------------------------------------------------------------------------------
#
# IF POLYS ARE SELECTED
#
# --------------------------------------------------------------------------------------------------
if mySelMode == 'polygon':
# Get the polygon selection int type: 1347374169
polygon_selection_type = SELECTION_SERVICE.LookupType(lx.symbol.sSELTYP_POLYGON)
# Get the total count of selected polygons
selPolys = SELECTION_SERVICE.Count(polygon_selection_type)
selected = str(selPolys) + '/polys'
if selPolys == 0:
fn_dialogAdvice()
lx.eval("select.convert vertex")
selVerts_pos = fn_get_selVertsPos(world=True)
# --------------------------------------------------------------------------------------------------
#
# IF EDGES ARE SELECTED
#
# --------------------------------------------------------------------------------------------------
elif mySelMode == 'edge':
"""
------------------------------------------------------------------------------------------------
SUPER EDGE LENGTH CALCULATOR
------------------------------------------------------------------------------------------------
Next pure Python API snippet is a mix between the one shared by Shawn Frueh:
https://foundry-modo.slack.com/archives/C6F918JEB/p1596930293212700
And also this one shared by 'robberyman':
https://foundry-modo.slack.com/archives/C6F918JEB/p1597076342264900
With this we can calculate the total accumulated length for all selected edges, no matter
these ones belong to different item meshes, procedural, deformed, animated... ALL!
"""
# Get the edge selection int type
edge_selection_type = SELECTION_SERVICE.LookupType(lx.symbol.sSELTYP_EDGE)
# Get the selection object: lxu.object.SelectionType
edge_selection_object = SELECTION_SERVICE.Allocate(lx.symbol.sSELTYP_EDGE)
# Convert that type into a packet so that we can access the selection data
edge_translation_packet = lx.object.EdgePacketTranslation(edge_selection_object)
# Get the total count of selected points
selected_edge_count = SELECTION_SERVICE.Count(edge_selection_type)
if selected_edge_count == 0:
fn_dialogAdvice()
selected = str(selected_edge_count) + '/edges'
# Iterate over each edge and get some data. These edges could be coming from multiple meshes
# and so you will need to check the item they be
for edge_index in range(selected_edge_count):
# Get a pointer to the vertex data in the given index.
edge_pointer = SELECTION_SERVICE.ByIndex(edge_selection_type, edge_index)
# If we don't get a pointer, skip to the next index in the loop.
# This is necessary to prevent any crashes.
if not edge_pointer:
continue
# Get the item the edge belongs to: lxu.object.Item
selection_item = edge_translation_packet.Item(edge_pointer)
item = lx.object.Item(selection_item)
# Get the matrix channel index
matrix_index = selection_item.ChannelLookup("worldMatrix")
# Prep the selection item to be read at the current time. You want those animated points!
selection_item.ReadEvaluated(SELECTION_SERVICE.GetTime())
# Get the matrix from the channel as a COM object and convert it to a matrix object.
item_world_matrix = lx.object.Matrix(selection_item.ChannelValue(matrix_index))
# Get the mesh item the edge belongs to: lxu.object.Mesh
selection_mesh = edge_translation_packet.Mesh(edge_pointer)
mesh = lx.object.Mesh(selection_mesh)
# IDs for the edge endpoints,
aPoint, bPoint = edge_translation_packet.Vertices(edge_pointer)
point = mesh.PointAccessor()
# Select and get the position of boths points. This will be relative to the mesh
# So if the mesh is not zero you will need to multiply this position by the transform data
# of the item it belongs to. We do this by multiplying it by the world matrix of it's item.
point.Select(aPoint)
posA = item_world_matrix.MultiplyVector(point.Pos())
point.Select(bPoint)
posB = item_world_matrix.MultiplyVector(point.Pos())
# Now lets calculate the individual X, Y, Z distances between both points
ABX = posA[0] - posB[0]
ABY = posA[1] - posB[1]
ABZ = posA[2] - posB[2]
AB_distance = math.sqrt(ABX**2 + ABY**2 + ABZ**2) # Pythagoras Theorem
distT += AB_distance #Accumulate all distances through for loops
lx.eval("select.convert vertex")
selVerts_pos = fn_get_selVertsPos(world=True)
# --------------------------------------------------------------------------------------------------
#
# IF POINTS ARE SELECTED
#
# --------------------------------------------------------------------------------------------------
elif mySelMode == 'vertex':
# Get the vertex selection int type: 1447383640
vertex_selection_type = SELECTION_SERVICE.LookupType(lx.symbol.sSELTYP_VERTEX)
# Get the total count of selected points
selVerts = SELECTION_SERVICE.Count(vertex_selection_type)
selected = str(selVerts) + '/verts'
# Dialog and exit is less than 2 points are selected
if selVerts == 0:
fn_dialogAdvice()
selVerts_pos = fn_get_selVertsPos(world=True)
# For loop to get to total ACCUMULATED distances between pairs of points
for A, B in zip(selVerts_pos, selVerts_pos[1:]): # Using 'zip' to loop between consecutive pairs of verts
ABX = A[0] - B[0]
ABY = A[1] - B[1]
ABZ = A[2] - B[2]
AB_distance = math.sqrt(ABX**2 + ABY**2 + ABZ**2) # Pythagoras Theorem
distT += AB_distance #Accumulate all distances through for loops
if selVerts == 1:
singleVert_position = selVerts_pos[0]
singleVert_posX = singleVert_position[0]
singleVert_posY = singleVert_position[1]
singleVert_posZ = singleVert_position[2]
# --------------------------------------------------------------------------------------------------
#
# IF ITEMS ARE SELECTED
#
# --------------------------------------------------------------------------------------------------
elif mySelMode == 'item':
selItems = lx.evalN("query sceneservice selection ? all")
selected = str(len(selItems)) + '/items'
if len(selItems) == 0:
fn_dialogAdvice()
# For loop to get to total ACCUMULATED distances between pairs of items
for A, B in zip(selItems, selItems[1:]): # Using 'zip' to loop between consecutive pairs of items
posA = lx.eval("query sceneservice item.worldPos ? %s" % A)
posB = lx.eval("query sceneservice item.worldPos ? %s" % B)
ABX = posA[0] - posB[0]
ABY = posA[1] - posB[1]
ABZ = posA[2] - posB[2]
AB_distance = math.sqrt(ABX**2 + ABY**2 + ABZ**2) # Pythagoras Theorem
distT = abs(distT + AB_distance)
if len(selItems) == 1:
singleItem_position = lx.eval("query sceneservice item.worldPos ? %s" % selItems[0])
singleItem_posX = singleItem_position[0]
singleItem_posY = singleItem_position[1]
singleItem_posZ = singleItem_position[2]
# --------------------------------------------------------------------------------------------------
#
# FOR ANY OTHER SELECTION MODE (center, pivot...)
#
# --------------------------------------------------------------------------------------------------
else:
fn_dialogAdvice()
# --------------------------------------------------------------------------------------------------
#
# COLLECT EVERYTHING AND APPLY CALCULATION FUNCTION
#
# --------------------------------------------------------------------------------------------------
# This is for components (verts, edges or poly selection)
if mySelMode in components:
distX = fn_calculateVertsBBox(0)
distY = fn_calculateVertsBBox(1)
distZ = fn_calculateVertsBBox(2)
# This specific case is for when only 1 vert is selected. Then we deliver the world coordinates
if mySelMode == 'vertex' and selVerts == 1:
distX = singleVert_posX
distY = singleVert_posY
distZ = singleVert_posZ
# This is for item selections
if mySelMode == 'item':
# Query our selected items
selItems = lx.evalN("query sceneservice selection ? all")
# Apply our function to get the BBOX of our selected items and max value
distX = fn_calculateItemsBBox(0)
distY = fn_calculateItemsBBox(1)
distZ = fn_calculateItemsBBox(2)
# This specific case is for when only 1 item is selected. Then we deliver the world coordinates
if mySelMode == 'item' and len(selItems) == 1:
distX = singleItem_posX
distY = singleItem_posY
distZ = singleItem_posZ
# Determine the max value
distM = max(distX, distY, distZ)
# If 'Round Check' is enabled:
if roundcheck == 1:
distT = fn_smartround(distT)
distX = fn_smartround(distX)
distY = fn_smartround(distY)
distZ = fn_smartround(distZ)
distM = fn_smartround(distM)
# Return back to the original selection mode
lx.eval("select.typeFrom %s" % mySelMode)
# --------------------------------------------------------------------------------------------------
#
# PASS ALL THIS INFO TO OUR USER VALUES
#
# --------------------------------------------------------------------------------------------------
#
# BUG: LACK OF PRECISION WITH USER VALUES DEFINED ON A CFG vs DEFINED ON A SCRIPT
#
# If you define a 'userValue' through an external config file (CFG) you will suffer a nasty
# limitation with precision: no more than 2 decimals will show (!)
#
# Instead, defining the lifetime of your user value as 'temporary', inside a script,
# this limitation disappear and you are allowed to use up to 4 decimals.
#
# Example: using a script the value will be printed '67.4955cm'. Using a 'config', it will be '67.5cm'
#
# On the other hand, using a userValue for 'Distance' defined on a CFG you will never
# be able to introduce a really small distance, like 15um. It will round to 0.
#
# For these reasons these user values are defined here, instead of using an external CGF.
#
# DISCUSED HERE:
# https://community.foundry.com/discuss/topic/91732
# --------------------------------------------------------------------------------------------------
# Lets query if our userValues are created or not
etr_gi_tdist_query = lx.eval("query scriptsysservice userValue.isDefined ? etr_gi_tdist")
etr_gi_xdist_query = lx.eval("query scriptsysservice userValue.isDefined ? etr_gi_xdist")
etr_gi_ydist_query = lx.eval("query scriptsysservice userValue.isDefined ? etr_gi_ydist")
etr_gi_zdist_query = lx.eval("query scriptsysservice userValue.isDefined ? etr_gi_zdist")
etr_gi_mdist_query = lx.eval("query scriptsysservice userValue.isDefined ? etr_gi_mdist")
# Create or update userValues depending on previous queries
if etr_gi_tdist_query == 0:
lx.eval("user.defNew etr_gi_tdist distance temporary")
lx.eval("user.value etr_gi_tdist %s" % distT)
else:
lx.eval("user.value etr_gi_tdist %s" % distT)
if etr_gi_xdist_query == 0:
lx.eval("user.defNew etr_gi_xdist distance temporary")
lx.eval("user.value etr_gi_xdist %s" % distX)
else:
lx.eval("user.value etr_gi_xdist %s" % distX)
if etr_gi_ydist_query == 0:
lx.eval("user.defNew etr_gi_ydist distance temporary")
lx.eval("user.value etr_gi_ydist %s" % distY)
else:
lx.eval("user.value etr_gi_ydist %s" % distY)
if etr_gi_zdist_query == 0:
lx.eval("user.defNew etr_gi_zdist distance temporary")
lx.eval("user.value etr_gi_zdist %s" % distZ)
else:
lx.eval("user.value etr_gi_zdist %s" % distZ)
if etr_gi_mdist_query == 0:
lx.eval("user.defNew etr_gi_mdist distance temporary")
lx.eval("user.value etr_gi_mdist %s" % distM)
else:
lx.eval("user.value etr_gi_mdist %s" % distM)
lx.eval("user.value etr_gi_selected %s" % selected)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment