"""
Module for interacting with the user.
Provides ready-made functions that can be used by buttons in any extension.
"""
# python standard imports
import os
from datetime import datetime
# rhino imports
import rhinoscriptsyntax as rs
# rhyton imports
from rhyton.main import Rhyton
from rhyton.export import CsvExporter, JsonExporter
from rhyton.document import GetBreps, ElementUserText, Group, TextDot, GetFilePath
from rhyton.document import DocumentConfigStorage, ElementOverrides, Layer
from rhyton.utils import Format, groupGuidsBy
[docs]class Visualize:
"""
Class for visualizing user text on Rhino objects.
"""
[docs] @classmethod
def byGroup(cls):
"""
Visualizes a set of Rhino objects and their user text:
The user input 'Parameter to Group By' is used for coloring
and grouping objects, as well as for building sub-totals of the
user-selected 'Parameter to Summarize'.
Places text dots with the value for each group.
"""
from rhyton.color import ColorScheme
breps = GetBreps()
if not breps:
return
with Layer.hierarchyInformation(breps):
keys = ElementUserText.getKeys(breps)
if not keys:
SelectionWindow.showWarning('No user text found on selected objects.')
return
selectedKey = SelectionWindow.show(
keys, message='Select Parameter to Group By:')
if not selectedKey:
return
selectedValue = SelectionWindow.show(
keys, message='Select Parameter to Summarize')
if not selectedValue:
return
cls.reset()
rs.EnableRedraw(False)
objectData = ColorScheme.apply(breps, selectedKey)
objectData = groupGuidsBy(objectData, [selectedKey, Rhyton.COLOR])
objectData = TextDot.add(
objectData, selectedValue, prefixKey=selectedKey)
for item in objectData:
Group.create(item[Rhyton.GUID], item[selectedKey])
rs.UnselectAllObjects()
rs.EnableRedraw(True)
[docs] @classmethod
def sumTotal(cls):
"""
Visualizes the total for selected parameter.
Groups selected objects and places a text dot to display the total.
Non-number parameter values will result in a simple object count.
"""
from rhyton.color import ColorScheme
breps = GetBreps()
if not breps:
return
keys = ElementUserText.getKeys(breps)
if not keys:
SelectionWindow.showWarning('No user text found on selected objects.')
return
selectedKey = SelectionWindow.show(
keys, message='Select Parameter to Calculate Total:')
if not selectedKey:
return
cls.reset()
rs.EnableRedraw(False)
objectData = {}
objectData[Rhyton.GUID] = breps
objectData[Rhyton.COLOR] = ColorScheme().getColors(1)[0]
ElementOverrides.apply(objectData)
objectData = TextDot.add(objectData, selectedKey)
for item in objectData:
Group.create(item[Rhyton.GUID])
rs.UnselectAllObjects()
rs.EnableRedraw(True)
[docs] @classmethod
def byValue(cls):
"""
Visualizes the value of selected parameter for each object individually.
Applies a user-defined color gradient to the values.
"""
from rhyton.color import ColorScheme
breps = GetBreps()
if not breps:
return
keys = ElementUserText.getKeys(breps)
if not keys:
SelectionWindow.showWarning('No user text found on selected objects.')
return
selectedKey = SelectionWindow.show(
options=keys, message='Select Parameter to visualize:')
if not selectedKey:
return
color = rs.GetColor(Rhyton.STANDARD_COLOR_1)
if not color:
return
colorStart = [color[0], color[1], color[2]]
color = rs.GetColor(Rhyton.STANDARD_COLOR_2)
if not color:
return
colorEnd = [color[0], color[1], color[2]]
cls.reset()
rs.EnableRedraw(False)
objectData = ColorScheme.applyGradient(
breps, selectedKey, [colorStart, colorEnd])
objectData = TextDot.add(
objectData, selectedKey, aggregate=False)
for item in objectData:
Group.create(item[Rhyton.GUID])
rs.UnselectAllObjects()
rs.EnableRedraw(True)
[docs] @staticmethod
def reset(clearSource=False):
"""
Resets the visualization for 'all' or 'selected' objects.
Ungroups visualized objects.
"""
preSelection = rs.SelectedObjects()
resetAll = 'select'
if not preSelection:
choices = {
"Yes, reset all.": 'reset',
"No wait, let me select!": 'select'}
resetAll = SelectionWindow.show(
choices, message='Reset all visualizations?')
if resetAll == 'select':
breps = GetBreps(filterByTypes=[8, 16, 8192, 1073741824])
if not breps:
return
rs.EnableRedraw(False)
ElementOverrides.clear(breps, clearSource=clearSource)
Group.dissolve(breps)
elif resetAll == 'reset':
rs.EnableRedraw(False)
data = DocumentConfigStorage().get(
Rhyton().extensionOriginalColors, dict())
if not data:
print('ERROR: No info about original colors available, select elements and try again.')
guids = data.keys()
# check if guids are still valid
guids = [guid for guid in guids if rs.IsObject(guid)]
Group.dissolve(guids)
ElementOverrides.clear(guids, clearSource=clearSource)
textDots = DocumentConfigStorage().get(
Rhyton().extensionTextdots, dict()).keys()
rs.DeleteObjects(textDots)
DocumentConfigStorage().save(Rhyton().extensionTextdots, None)
rs.EnableRedraw(True)
[docs]class ColorSchemeEditor:
[docs] def __init__(self):
"""
Inits a new ColorSchemeEditor Instance. Asks the user to select
a color scheme and opens a dialog to edit the colors.
"""
from rhyton.color import ColorScheme
schemeName = self.showSchemes()
if not schemeName:
return
keyValues = self.showColors(schemeName)
if not keyValues:
return
ColorScheme().save(schemeName, keyValues)
[docs] @staticmethod
def showSchemes():
"""
Ask the user to select a color scheme.
Returns:
str: The name of the selected color scheme.
"""
from rhyton.color import ColorScheme
schemes = ColorScheme().schemes.keys()
if not schemes:
SelectionWindow.showWarning("No color schemes available, Use 'Visualize Data by Grouping' first.")
return
return SelectionWindow.show(
ColorScheme().schemes.keys(), message="Select Color Scheme:")
[docs] @staticmethod
def showColors(schemeName):
"""
Presents the user a dialog to edit the colors of a color scheme.
Args:
schemeName (str): The name of the color scheme to edit.
"""
from rhyton.color import ColorScheme
scheme = ColorScheme().schemes.get(schemeName)
return SelectionWindow.dictBox(scheme, message=schemeName)
[docs] @staticmethod
def importScheme():
pass
[docs] @staticmethod
def exportScheme():
pass
[docs]class Export:
[docs] def __init__(self):
"""
Inits a new export Instance. Asks the user to select the export format,
gets memorized checkbox values for available keys. Presents those
to the user to select the keys for export. Stores checkbox states
and exports keys to selected output format.
"""
breps = GetBreps()
if not breps:
return
exportMethod = SelectionWindow.show(
[Rhyton.CSV, Rhyton.JSON],
message='Select export format:')
if not exportMethod:
return
with Layer.hierarchyInformation(breps):
flag = '.'.join([Rhyton().extensionName, Rhyton.EXPORT_CHECKBOXES])
selectedKeys = self.getExportKeys(flag, breps)
if not selectedKeys:
return
if exportMethod == Rhyton.CSV:
self.toCSV(breps, selectedKeys)
elif exportMethod == Rhyton.JSON:
self.toJSON(breps, selectedKeys)
[docs] def toCSV(self, guids, keys):
"""
Exports the values for provided keys to CSV.
Opens the new file.
Args:
guids (list(str)): A list of Rhino object ids.
keys (list(str)): A list of document user text keys to export.
"""
data = ElementUserText.get(guids, keys)
file = CsvExporter.write(data)
os.startfile(file)
[docs] def toJSON(self, guids, keys):
"""
Exports the values for provided keys to JSON.
Opens the new file.
Args:
guids (list(str)): A list of Rhino object ids.
keys (list(str)): A list of document user text keys to export.
"""
data = ElementUserText.get(guids, keys)
file = JsonExporter.write(data)
os.startfile(file)
[docs] @classmethod
def getExportKeys(cls, flag, guids):
"""
Presents a checkbox to the user to pick the user text keys for export.
The checkbox states are stored in the document config storage and
are used a the default values for the next time the dialog is shown.
Args:
flag (str): The identifier for the default values in the document config storage.
guids (str): A list of Rhino object ids.
Returns:
list(str): A list of user text keys.
"""
keys = sorted(list(ElementUserText.getKeys(guids)))
options = cls.getCheckboxDefaults(flag, keys=keys)
selectedOptions = SelectionWindow.showBoxes(options)
if not selectedOptions:
return
cls.setCheckboxDefaults(flag, selectedOptions)
selectedKeys = [key[0] for key in selectedOptions if key[1] == True]
return selectedKeys
[docs] @staticmethod
def getCheckboxDefaults(flag, keys=[]):
"""
Loads export checkbox defaults from the document config storage for given keys.
If no default is available in the document config storage, <True> will
be used.
Args:
flag (str): The identifier for the default values in the document config storage.
keys (list(str)): A list of keys to get default values for.
Returns:
tuple: A list of tuples indicating the defaults for given values.
"""
defaults = DocumentConfigStorage().get(flag, dict())
if keys:
for key in keys:
if not key in defaults:
defaults[key] = True
defaults = [(k, v) for k, v in defaults.items() if k in keys]
else:
defaults = [(k, v) for k, v in defaults.items()]
return defaults
[docs] @staticmethod
def setCheckboxDefaults(flag, newDefaults):
"""
Updates the document config storage with new export checkbox defaults
for the current extension.
Args:
flag (str): The identifier for the default values in the document config storage.
defaults (tuple): A list of tuples indicating the default per value.
"""
defaults = DocumentConfigStorage().get(flag, dict())
newDefaults = dict((k, v) for k, v in newDefaults)
defaults.update(newDefaults)
DocumentConfigStorage().save(flag, defaults)
[docs]class Settings(Rhyton):
"""
Class for handling extension settings.
"""
def __init__(self, extensionName):
super(Settings, self).__init__(extensionName)
"""
Inits a new Settings instance.
Presents a UI to the user that shows the current settings and
allows to change them.
"""
inValidInput = True
while inValidInput:
res = SelectionWindow.dictBox(
options=self.settings, message=self.extensionSettings)
if res:
try:
int(res[self.ROUNDING_DECIMALS_NAME])
inValidInput = False
except:
pass
else:
inValidInput = False
self.saveSettings(res)
[docs]class SelectionWindow:
"""
Wrapper class for Rhino user interfaces.
"""
[docs] @staticmethod
def show(options, message=None):
"""
Shows a list box to the user that allows to select from a
list of options.
Args:
options (mixed): A list of strings or dict
(shows keys to user, returns value).
message (str, optional): The message to the user. Defaults to None.
Returns:
mixed: The value of the selected key from the input dictionary or the selected item from the input list.
"""
if not type(options) == dict:
options = dict((i, i) for i in options)
res = rs.ListBox(
sorted(options.keys()),
message,
title=Rhyton().extensionName.title(),
default=options.keys()[0])
if res:
return options[res]
[docs] @staticmethod
def showBoxes(options, message=None):
"""
Shows a checkbox list to the user that allows to select from
multiple items.
Example input/output::
[("option1", True), ("option2", False)]
The returns are formatted as shown above.
Args:
options (list(tuple)): A list of tuples with pre-defined checkbox states
message (str, optional): The message to the user. Defaults to None.
Returns:
list(tuple): A list of tuples indicating the name and state of each checkbox.
"""
return rs.CheckListBox(
sorted(options),
message,
title=Rhyton().extensionName.title())
[docs] @staticmethod
def dictBox(options, message=None):
"""
Show a dictionary-style list box to the user.
Args:
options (dict): The key, value pairs.
message (str, optional): The message to the user. Defaults to None.
"""
res = rs.PropertyListBox(
[Format.value(k) for k in options.keys()],
options.values(),
message,
title=Rhyton().extensionName.title())
if res:
return dict((k, v) for k, v in zip(options.keys(), res))
[docs] @staticmethod
def showWarning(message):
"""
Shows a warning to the user.
Args:
message (str): The message to the user.
"""
rs.MessageBox(message, 48, Rhyton().extensionName.title())
[docs]class Powerbi:
"""
Class for opening and updating PowerBI.
"""
CUSTOM_TEMPLATE = "Load Custom Template"
POWERBI_TEMPLATE = '.template'
POWERBI_DATAFILE = Rhyton.HDM_DT_DIR + '/RhinoToolbarExtensions/powerbi.json'
POWERBI_TEMPLATES_DIR = Rhyton.HDM_DT_DIR + '/RhinoToolbarExtensions/powerbi-templates'
POWERBI_TEMPLATES_EXTENSION = '.pbit'
TIMESTAMP = "timestamp"
# fixed keys are necessary to ensure the powerbi visuals do not break
VIZ_KEY = "visualization_parameter"
[docs] @classmethod
def show(cls):
"""
This method is used to start PowerBI.
It checks if PowerBI is already running and if not, it opens it.
The user can select a pre-defined template or load a custom template.
When a pre-defined template is selected, certain parameters are fixed
to ensure that the query and visuals in powerbi do not break.
The user is asked to select a parameter to visualize which is then
renamend to meet the PowerBI template requirements.
The data is then written to a json file and PowerBI is opened.
"""
pbiRunning = cls._processExists('PBIDesktop.exe')
if pbiRunning:
print("powerbi already running")
return
template = cls._pickTemplate()
if not template:
return
config = dict()
config[cls.POWERBI_TEMPLATE] = template
if template == cls.CUSTOM_TEMPLATE:
template = GetFilePath(cls.POWERBI_TEMPLATES_EXTENSION)
breps = GetBreps()
if not breps:
return
data = cls._getData(breps)
if not data:
return
else:
breps = GetBreps()
if not breps:
return
allKeys = ElementUserText.getKeys(breps)
if not allKeys:
SelectionWindow.showWarning('No user text found on selected objects.')
return
vizKey = SelectionWindow.show(
allKeys, message="Select Parameter to Visualize:")
config[cls.VIZ_KEY] = vizKey
fixedKeys = cls.fixedKeys()
fixedKeys.append(vizKey)
data = cls._getData(breps, fixedKeys=fixedKeys, vizKey=vizKey)
if not data:
return
templateFlag = ''.join([
Rhyton().extensionName,
Rhyton.POWERBI,
cls.POWERBI_TEMPLATE])
DocumentConfigStorage().save(templateFlag, config)
JsonExporter.write(data, file=cls.POWERBI_DATAFILE)
os.startfile(template)
[docs] @classmethod
def update(cls):
"""
This method is used to update PowerBI.
It gets the current PowerBI template and chooses the correct method for
updating the data.
The data is then written to a json file and PowerBI is opened.
"""
templateFlag = ''.join([
Rhyton().extensionName,
Rhyton.POWERBI,
cls.POWERBI_TEMPLATE])
config = DocumentConfigStorage().get(templateFlag)
if config[cls.POWERBI_TEMPLATE] == cls.CUSTOM_TEMPLATE:
breps = GetBreps()
if not breps:
return
data = cls._getData(breps)
if not data:
return
else:
breps = GetBreps()
if not breps:
return
vizKey = config.get(cls.VIZ_KEY)
fixedKeys = cls.fixedKeys()
fixedKeys.append(vizKey)
data = cls._getData(breps, fixedKeys=fixedKeys, vizKey=vizKey)
if not data:
return
JsonExporter.append(data, cls.POWERBI_DATAFILE)
@classmethod
def _pickTemplate(cls):
"""
This method is used to pick a PowerBI template.
It checks if the PowerBI template directory exists. If not, it is created.
It then searches for all files with the extension ``.pbit`` and adds them
to a list of templates. The user is then asked to select a template.
The seleceted templates is returned.
"""
if not os.path.exists(cls.POWERBI_TEMPLATES_DIR):
os.makedirs(cls.POWERBI_TEMPLATES_DIR)
files = cls.absoluteFilePaths(cls.POWERBI_TEMPLATES_DIR)
templates = [os.path.abspath(f) for f in files if f.endswith(cls.POWERBI_TEMPLATES_EXTENSION)]
templateNames = [os.path.basename(t).replace(cls.POWERBI_TEMPLATES_EXTENSION, '') for t in templates]
options = dict((k, v) for k, v in zip(templateNames, templates))
options[cls.CUSTOM_TEMPLATE] = cls.CUSTOM_TEMPLATE
return SelectionWindow.show(options, message="Pick PowerBI Template:")
@classmethod
def _getData(cls, guids, fixedKeys=[], vizKey=None):
"""
This method is used to get the data for PowerBI.
It temporarily adds layer information to the object user text.
If no fixed keys are provided, the user is asked to select the keys
for export. It then gets the data for the selected keys.
The data for the visualization parameter is renamed to meet the PowerBI
template requirements. All extension prefixes are removed from the keys.
Args:
guids (list(str)): A list of Rhino objects ids.
fixedKeys (list, optional): A list of keys that need to be exported.
Defaults to [].
vizKey (str, optional): The key of the data used for visualization.
Defaults to None.
Returns:
dict: The data for PowerBI.
"""
with Layer.hierarchyInformation(guids):
flag = '.'.join([Rhyton().extensionPowerbi, Rhyton.EXPORT_CHECKBOXES])
if fixedKeys:
selectedKeys = fixedKeys
else:
selectedKeys = Export.getExportKeys(flag, guids)
if not selectedKeys:
return
data = ElementUserText.get(guids, selectedKeys)
timeStamp = datetime.now().strftime("%Y/%m/%d %H:%M:%S")
prefix = Rhyton().extensionName + Rhyton.DELIMITER
for d in data:
if vizKey and vizKey in d:
d[cls.VIZ_KEY] = d.pop(vizKey)
for key in d.keys():
if Rhyton().extensionName in key:
keyNew = key.replace(prefix, '')
d[keyNew] = d.pop(key)
d[cls.TIMESTAMP] = timeStamp
return data
@staticmethod
def _processExists(processName):
"""
This method is used to check if a process is running or not.
Args:
processName (str): The name of the process.
Returns:
bool: True if the process is running, False otherwise.
"""
import subprocess
# checks if a process is running or not
call = 'TASKLIST', '/FI', 'imagename eq %s' % processName
# use buildin check_output right away
si = subprocess.STARTUPINFO()
si.dwFlags |= subprocess.STARTF_USESHOWWINDOW
output = subprocess.check_output(call).decode()
# check in last line for process name
lastLine = output.strip().split('\r\n')[-1]
# because Fail message could be translated
return lastLine.lower().startswith(processName.lower())
[docs] @staticmethod
def absoluteFilePaths(directory):
"""
This method is used to get all absolute file paths in a directory.
Args:
directory (str): The directory to search.
Yields:
list(str): The absolute file paths.
"""
for dirpath,_,filenames in os.walk(directory):
for f in filenames:
yield os.path.abspath(os.path.join(dirpath, f))
[docs] @staticmethod
def fixedKeys():
"""
Generates fixed keys when needed. This cannot be done as a
class variable because the extension name might change.
Yields:
list(str): The fixed keys.
"""
name = Rhyton().extensionName
return [
Rhyton.DELIMITER.join([name, Rhyton.LAYER_HIERARCHY, Rhyton.NAME]),
Rhyton.DELIMITER.join([name, Rhyton.LAYER_HIERARCHY, "1"]),
Rhyton.DELIMITER.join([name, Rhyton.LAYER_HIERARCHY, "2"]),
Rhyton.DELIMITER.join([name, Rhyton.LAYER_HIERARCHY, "3"])]
[docs]class ProgressBar():
"""
This class is used to create and update a progress bar.
"""
[docs] def __init__(self, upper, label="Calculating...", lower=1):
"""
The constructor for the ProgressBar class.
Args:
upper (int): The upper limit of the progress bar.
label (str, optional): The text to display in the progress bar. Defaults to "Calculating...".
lower (int, optional): the lower limit of the progress bar. Defaults to 1.
"""
self.upper = upper
self.label = label
self.lower = lower
self.position = 0
[docs] def __enter__(self):
"""
This method is used to show the progress bar.
Returns:
object: The progress bar object.
"""
rs.StatusBarProgressMeterShow(
self.label, self.lower, self.upper, embed_label=True, show_percent=True)
return self.__class__(self.upper)
[docs] def __exit__(self, exc_type, exc_val, traceback):
"""
This method is used to hide the progress bar.
Args:
exc_type (_type_): _description_
exc_val (_type_): _description_
traceback (_type_): _description_
"""
rs.StatusBarProgressMeterHide()
[docs] def update(self):
"""
This method is used to update the progress bar.
The poistions is automatically incremented by 1
each time the method is called.
"""
self.position += 1
rs.StatusBarProgressMeterUpdate(self.position, absolute=True)