#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