Skip to content

Instantly share code, notes, and snippets.

@Eterea
Last active August 12, 2020 08:27
Show Gist options
  • Save Eterea/e75f2cddcfc01ca7ed39353cfcb2cefc to your computer and use it in GitHub Desktop.
Save Eterea/e75f2cddcfc01ca7ed39353cfcb2cefc to your computer and use it in GitHub Desktop.
#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