Last active
August 12, 2020 08:27
-
-
Save Eterea/e75f2cddcfc01ca7ed39353cfcb2cefc to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#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