#python | |
# ------------------------------------------------------------------------------------------------ | |
# NAME: etr_distanceCalculator.py | |
# VERS: 2.2.1 | |
# DATE: August 12, 2020 | |
# | |
# MADE: Cristobal Vila, etereaestudios.com | |
# | |
# USES: Script to calculate distances from selected verts/edges/polygons/items | |
# All selected components must below to the same item mesh (no multi-mesh selections) | |
# Procedural, deformed or animated meshes are NOT supported. | |
# | |
# 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 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 BASIC}") | |
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\ | |
- Procedural, deformed, animated or multiple meshes are NOT supported.}") | |
lx.eval("dialog.open") | |
lx.eval("user.value etr_gi_selected (none)") | |
sys.exit() | |
# Calculate the bounding box for selected vertices. This will gives us the BBox per axis, separately. | |
# I will use this procedure, described by Muha here: https://community.foundry.com/discuss/topic/69758 | |
# ------------------------------------------------------------------------------------------------ | |
def fn_calculateVertsBBox(axis): | |
#Create empty lists for vert positions on this given axis | |
vert_pos = [] | |
# Next step is to enter the loop "for element in vertex_index:": | |
# Inside the loop the script asks for each index position. | |
# The position will be a tuple with 3 entries for x, y and z positions. | |
# Last step is to check if a position already exists (to clean things), | |
# there are often vertices in a polygon that have for example the same hight. | |
# The check is done with the "if not" statement. | |
# The command ".append" will append the position into the list. | |
for element in selVerts: | |
vert_pos_single = lx.eval("query layerservice vert.wpos ? " + str(element)) | |
if not vert_pos_single[axis] in vert_pos: | |
vert_pos.append(vert_pos_single[axis]) | |
# After this loop all we need to do is to sort our list: | |
vert_pos.sort() | |
# Now we have at place 1 (which is place 0) the smallest position and in the last place the largest one. | |
# The last place is usually accessed by mylist[len(mylist]-1] but python gives you a more elegant solution with -1. | |
# With this we can calculate our particular axis distance as a simple substraction: | |
distAxis = ( vert_pos[-1] - vert_pos[0] ) | |
return distAxis | |
# Calculate the bounding box for selected items. This could be unified with previous function, since we can | |
# consider 'verts' and 'items' as similar things to be measured. But I defined 2 separate functions, for clarity | |
# ------------------------------------------------------------------------------------------------ | |
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 ?") | |
# This will unselect anything selected in the Shader Tree, since this could give errors in some circumstances | |
n = lx.eval("query sceneservice txLayer.N ?") # Using all here is meaningless, as far as I know. | |
for i in xrange(n): # xrange is faster for large numbers, and you don't have to specify a lower bound of 0 for range either - it will default to 0. | |
if lx.eval1("query sceneservice txLayer.isSelected ? %s" % i): | |
myID = lx.eval1("query sceneservice txLayer.id ? %s" % i) | |
lx.eval("select.subItem %s remove" % myID) | |
# This will select the main item mesh, where your components are selected, in the Item List. Again: to avoid possible errors | |
if mySelMode in components: | |
myMeshIX = lx.eval("query layerservice layer.index ? main") | |
myMeshID = lx.eval("query layerservice layer.id ? %s" % myMeshIX) | |
lx.eval("select.subItem %s set mesh;locator" % myMeshID) | |
# -------------------------------------------------------------------------------------------------- | |
# | |
# IF POINTS ARE SELECTED | |
# | |
# -------------------------------------------------------------------------------------------------- | |
if mySelMode == 'vertex': | |
selVerts = lx.evalN("query layerservice verts ? selected") | |
selected = str(len(selVerts)) + '/verts' | |
# Dialog and exit is less than 2 points are selected | |
if len(selVerts) == 0: | |
fn_dialogAdvice() | |
# For loop to get to total ACCUMULATED distances between pairs of points | |
for A, B in zip(selVerts, selVerts[1:]): # Using 'zip' to loop between consecutive pairs of verts | |
posA = lx.eval("query layerservice vert.wpos ? %s" % A) | |
posB = lx.eval("query layerservice vert.wpos ? %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 += AB_distance | |
if len(selVerts) == 1: | |
singleVert_position = lx.eval("query layerservice vert.wpos ? %s" % selVerts[0]) | |
singleVert_posX = singleVert_position[0] | |
singleVert_posY = singleVert_position[1] | |
singleVert_posZ = singleVert_position[2] | |
# -------------------------------------------------------------------------------------------------- | |
# | |
# IF EDGES ARE SELECTED | |
# | |
# -------------------------------------------------------------------------------------------------- | |
elif mySelMode == 'edge': | |
selEdges = lx.evalN("query layerservice edges ? selected") | |
selected = str(len(selEdges)) + '/edges' | |
if len(selEdges) == 0: | |
fn_dialogAdvice() | |
# Get to total ACCUMULATED distances for all selected edges | |
distT = sum(lx.eval("query layerservice edge.length ? %s" % edge) for edge in selEdges) | |
lx.eval("select.convert vertex") | |
# -------------------------------------------------------------------------------------------------- | |
# | |
# IF POLYS ARE SELECTED | |
# | |
# -------------------------------------------------------------------------------------------------- | |
elif mySelMode == 'polygon': | |
selPolys = lx.evalN("query layerservice polys ? selected") | |
selected = str(len(selPolys)) + '/polys' | |
if len(selPolys) == 0: | |
fn_dialogAdvice() | |
lx.eval("select.convert vertex") | |
# -------------------------------------------------------------------------------------------------- | |
# | |
# 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: | |
# Query our selected verts (after converting our selection to verts, for edges and polys) | |
selVerts = lx.evalN("query layerservice verts ? selected") | |
# Apply our function to get the BBOX of our selected points and max value | |
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 len(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