"""
Module for modifying the rhino document.
This is the only module in this package that actually modifies a Rhino file.
"""
# python standard imports
import json
from contextlib import contextmanager
from collections import defaultdict
# rhino imports
import rhinoscriptsyntax as rs
from Rhino.Geometry import Line
# rhyton imports
from rhyton.main import Rhyton
from rhyton.utils import Format, toList, detectType
[docs]class ElementOverrides:
"""
Class for handling color overrides on Rhino objects.
"""
OVERRIDE_PROGRESS = "Color Overrides..."
[docs] @classmethod
def apply(cls, overrides):
"""
Sets the colors for given elements. The original colors and color sources
will be stored alongside the objects id in the document user text.
When an objects is overridden by rhyton for a second time,
the original color and color source will remain as they are.
Examples::
ElementOverrides.apply(
[
{
"guid": [<guid1>, <guid2>],
"color": <hexvalue1>
},
{
"guid": <guid3>,
"color": <hexvalue2>
}
])
# or
ElementOverrides.apply(
{
"guid": <guid>,
"color": <hexvalue>
})
Args:
overrides (list(dict)): A dictionary or a list of dictionaries.
"""
from rhyton.color import Color
from rhyton.ui import ProgressBar
overrides = toList(overrides)
originalColors = DocumentConfigStorage().get(
Rhyton().extensionOriginalColors, dict())
with ProgressBar(len(overrides), label=cls.OVERRIDE_PROGRESS) as bar:
for override in overrides:
guids = override[Rhyton.GUID]
guids = toList(guids)
for guid in guids:
color = rs.ObjectColor(guid)
color = tuple([color[0], color[1], color[2]])
if not guid in originalColors:
original = dict()
original[Rhyton.COLOR] = Color.RGBtoHEX(color)
original[Rhyton.COLOR_SOURCE] = rs.ObjectColorSource(guid)
originalColors[guid] = original
color = override.get(Rhyton.COLOR, Rhyton.HEX_WHITE)
rs.ObjectColor(
guid,
Color.HEXtoRGB(
override.get(Rhyton.COLOR, Rhyton.HEX_WHITE)))
bar.update()
AffectedElements.save(Rhyton().extensionOriginalColors, originalColors)
[docs] @classmethod
def clear(cls, guids, clearSource=False):
"""
Clear the color overrides for given objects.
The original colors will be restored from the ``Document Text``.
Args:
guids (str): The ids of the objects.
clearSource (bool, optional): If ``True``, the color source will be set to ``0``. Defaults to ``False``.
"""
from rhyton.color import Color
from rhyton.ui import ProgressBar
originalColors = DocumentConfigStorage().get(
Rhyton().extensionOriginalColors, defaultdict())
with ProgressBar(len(guids), label=cls.OVERRIDE_PROGRESS) as bar:
for guid in guids:
hexColor = originalColors.get(guid, dict()).get(Rhyton.COLOR)
if hexColor:
rgbColor = Color.HEXtoRGB(hexColor)
rs.ObjectColor(guid, rgbColor)
if clearSource:
rs.ObjectColorSource(guid, originalColors.get(
guid, dict()).get(Rhyton.COLOR_SOURCE, 0))
bar.update()
AffectedElements.remove(Rhyton().extensionOriginalColors, guids)
[docs]class TextDot:
"""
Class for handling Rhino text dot objects.
"""
[docs] @staticmethod
def add(data, valueKey, aggregate=True, prefixKey=None):
"""
Adds a new text dot to the document.
The textdot location is the center of the bounding box of given guid(s).
Provide a list of guids, if you want to place the text dot
in the middle of multiple objects.
Note:
The input dictionary can contain unrelated keys - they will be ignored.
This allows you to use the same input for ''TextDot.add'' and ''ElementOverrides.apply''
Examples::
TextDot.add(
[
{
"guid": [<guid1>, <guid2>],
"color": <color1>
},
{
"guid": <guid3>,
"color": <color2>
}
])
# or
TextDot.add(
{
"guid": <guid>,
"color": <display color>
})
Args:
dict (list(dict)): A dictionary or list of dictionaires.
valueKey (str): The key to aggregate or count values.
prefix (str, optional): The text to display before the value.
Returns:
list: The input list of dicts with the guids of the text dots added.
"""
from rhyton.color import Color
from rhyton.ui import ProgressBar
data = toList(data)
textDots = dict()
with ProgressBar(len(data), label="Text Dots...") as bar:
for dot in data:
dot[Rhyton.GUID] = toList(dot[Rhyton.GUID])
bBox = rs.BoundingBox(dot[Rhyton.GUID])
if aggregate:
try:
value = ElementUserText.aggregate(
dot[Rhyton.GUID], valueKey)
value = Format.formatNumber(value, valueKey)
except:
value = len(dot[Rhyton.GUID])
else:
value = ElementUserText.getValue(
dot[Rhyton.GUID][0], valueKey)
try:
value = Format.formatNumber(float(value), valueKey)
except:
if value == Rhyton.WHITESPACE:
value = Rhyton.EMPTY
elif value == None:
value = Rhyton.NOT_AVAILABLE
if prefixKey:
value = "{}: {}".format(dot[prefixKey], value)
point = Line(bBox[0], bBox[6]).PointAt(0.5)
textDot = rs.AddTextDot(value, point)
rs.ObjectColor(
textDot,
Color.HEXtoRGB(dot.get(Rhyton.COLOR, Rhyton.HEX_WHITE)))
rs.TextDotFont(textDot, Rhyton.FONT)
rs.TextDotHeight(textDot, 12.0)
dot[Rhyton.GUID].append(str(textDot))
textDots[str(textDot)] = 1
bar.update()
AffectedElements.save(Rhyton().extensionTextdots, textDots)
return data
[docs]class AffectedElements:
"""
Class for handling information on the objects affected by rhyton.
"""
[docs] @staticmethod
def save(flag, data):
"""
Saves the guids of given elements to the document user text.
This allows rhyton to keep track of the elements it's modifying.
The input 'data' should contain information on the previous or new state
of the affected objects alongside their guid:
Example::
{<guid>: {'key1': 'value1'}}
Args:
flag (str): The identifier for the data.
data (dict): A dictionary of guids with a dictionary as values.
"""
existing = DocumentConfigStorage().get(flag, defaultdict())
existing.update(data)
DocumentConfigStorage().save(flag, existing)
[docs] @staticmethod
def remove(flag, guids):
"""
Removes given guids from the document config storage saved under given flag.
Args:
guids (str): A single or a list of Rhino object ids.
"""
guids = toList(guids)
existing = DocumentConfigStorage().get(
flag, defaultdict())
for guid in guids:
if guid in existing:
del existing[guid]
DocumentConfigStorage().save(flag, existing)
[docs]class DocumentConfigStorage:
"""
Class for handling the reading and writing of document user text.
"""
def __init__(self):
self.storageName = Rhyton.RHYTON_CONFIG
self.storage = dict()
raw = rs.GetDocumentUserText(key=self.storageName)
if raw and raw != Rhyton.WHITESPACE:
self.storage = json.loads(raw)
else:
print("INFO: No configuration available.")
[docs] def save(self, flag, data):
"""
Saves the given data under the provided flag in the ``Rhino Document Text``.
All data is saved inside the ``RHYTON_CONFIG`` field.
The input data must be valid JSON.
Args:
flag (str): The identifier for the data.
data (mixed): The data to store.
"""
self.storage[flag] = data
self.storage = dict((k, v) for k, v in self.storage.iteritems() if v)
raw = json.dumps(self.storage, sort_keys=True, ensure_ascii=False)
rs.SetDocumentUserText(self.storageName, raw)
[docs] def get(self, flag, default=None):
"""
Gets the data stored under given flag.
Example::
config = rhyton.DocumentConfigStorage()
item = config.get('name')
Args:
flag (str): The identifier for the data.
default (mixed, optional): An optional default value. Defaults to None.
Returns:
mixed: The data stored under given flag.
"""
return self.storage.get(flag, default)
[docs]class ElementUserText:
"""
Class for handling user text on Rhino objects.
"""
[docs] @staticmethod
def apply(data):
"""
Applies given user text to provided elements.
The expected input format for 'data' is a dictionary containing the guid
as well as at least one user text key:
Examples::
ElementUserText.apply(
[
{
"guid": [<guid1>, <guid2>],
"string_key": "Value",
"number_key": 0
},
{
"guid": <guid3>,
"string_key": "Value",
"number_key": 0
}
])
# or
ElementUserText.apply(
{
"guid": <guid>,
"string_key": "Value",
"number_key": 0
})
Due to Rhino's limitations, all values will be stored as strings.
Args:
data (list(dict)): A list of dictionaries describing the
keyPrefix (str, optional): The prefix for all keys. Defaults to "".
"""
data = toList(data)
for entry in data:
guid = entry[Rhyton.GUID]
del entry[Rhyton.GUID]
for key, value in entry.items():
key = Format.key(Rhyton.DELIMITER.join([Rhyton().extensionName, key]))
rs.SetUserText(
guid,
key=key,
value=Format.value(value))
[docs] @staticmethod
def get(guids, keys=None):
"""
Gets user text from given elements.
Return format::
[
{
"guid": <guid1>,
"example_key1": "example_value1"
"example_key2": "example_value2"
}
]
Args:
guids (list(str)): A list of Rhino objects ids.
keys (list(str), optional): A list of keys. By default, all keys are returned.
Returns:
list: A list of dictionaries.
"""
data = []
for guid in guids:
if not keys:
keys = rs.GetUserText(guid)
keys = toList(keys)
entry = dict()
entry[Rhyton.GUID] = guid
for key in keys:
entry[key] = detectType(rs.GetUserText(guid, key))
data.append(entry)
return data
[docs] @staticmethod
def getKeys(guids):
"""
Gets a complete set of unique user text keys from given objects.
Args:
guids (str): A list of Rhino objects ids.
"""
guids = toList(guids)
keys = set()
for guid in guids:
for key in rs.GetUserText(guid):
keys.add(key)
return keys
[docs] @staticmethod
def getValues(guids, keys=[]):
"""
Gets a complete set of unique user text values from given objects.
Args:
guids (str): A list of Rhino objects ids.
"""
guids = toList(guids)
values = set()
for guid in guids:
if keys:
for key in toList(keys):
value = detectType(rs.GetUserText(guid, key))
values.add(value)
else:
elementKeys = rs.GetUserText(guid)
if elementKeys:
for key in elementKeys:
value = detectType(rs.GetUserText(guid, key))
values.add(value)
return values
[docs] @staticmethod
def getValue(guid, key):
"""
Wrapper function to get user text from an objects
Args:
guid (str): A rhino objects id.
key (str): The key to get the value from.
Returns:
mixed: None if key does not exist,
" " if key has no value,
else: str of value
"""
return detectType(rs.GetUserText(guid, key))
[docs] @staticmethod
def aggregate(guids, keys=[]):
"""
Aggregates user text values from given objects.
Non-numeric values will cause an error.
Args:
guids (str): A list of Rhino objects ids.
keys (list, optional): A list of keys. Defaults to [].
Returns:
float: The sum of all values.
"""
keys = toList(keys)
values = []
for guid in guids:
for key in keys:
value = rs.GetUserText(guid, key)
if value:
values.append(float(value))
return sum(values)
[docs] @staticmethod
def remove(guids, keys):
"""
Removes user text from given objects.
Args:
guids (str): A list of Rhino objects ids.
keys (str): A list of keys.
"""
guids = toList(guids)
for guid in guids:
for key in keys:
rs.SetUserText(guid, key)
[docs]class Group:
"""
Class for handling Rhino groups.
"""
[docs] @staticmethod
def create(guids, groupName=''):
"""
Creates a new group with given name and adds given objects to it.
The groupname will be expanded to prevent ambiguity.
Args:
guids (str): A list or single Rhino object id.
groupName (str): The basename of the group.
"""
import uuid
groupName = Rhyton.DELIMITER.join(
[Rhyton.GROUP, str(groupName), str(uuid.uuid1())])
rs.AddGroup(groupName)
rs.AddObjectsToGroup(guids, groupName)
return groupName
[docs] @staticmethod
def dissolve(guids):
"""
Ungroups the guids' groups and then deletes the group definition.
Delete all text dots among the guids in the process.
Args:
guids (str): A list or a single Rhino object id.
"""
guids = toList(guids)
groupNames = set()
for guid in guids:
groupNames.add(rs.ObjectTopGroup(guid))
if rs.ObjectType(guid) == 8192:
rs.DeleteObject(guid)
for group in groupNames:
rs.DeleteGroup(group)
[docs]class Layer:
"""
Class for handling layer information.
"""
[docs] @staticmethod
def maxHierarchy(guids):
"""
Gets the maximum depth of sublayers for the given list of objects.
A simple layer return 1, a layer with one sublayer returns 2.
Args:
breps (list(str)): A list of Rhino object ids.
Returns:
int: The maximum depth of nested layers.
"""
return max([len(rs.ObjectLayer(guid).split('::')) for guid in guids])
[docs] @staticmethod
def addLayerHierarchy(guids, depth):
"""
Add the layer name for each depth level of sublayers to given entries.
Args:
data (dict): A dictionary or list of dictionaries.
depth (int): The maximum depth of sublayer names to add.
"""
from rhyton.ui import ProgressBar
guids = toList(guids)
with ProgressBar(len(guids), label="Adding Layer Information...") as bar:
for guid in guids:
data = dict()
data[Rhyton.GUID] = guid
objectLayer = rs.ObjectLayer(guid)
fullHierarchy = Rhyton.DELIMITER.join([Rhyton.LAYER_HIERARCHY, Rhyton.NAME])
data[fullHierarchy] = objectLayer
layers = objectLayer.split('::')[:depth]
for index, layer in enumerate(layers, 1):
key = Rhyton.DELIMITER.join([Rhyton.LAYER_HIERARCHY, str(index)])
data[key] = layer
ElementUserText.apply(data)
bar.update()
[docs] @staticmethod
def removeLayerHierarchy(guids):
"""
Removes layer information from given objects user text.
Args:
guids (list(str)): A list of Rhino object ids.
"""
keys = ElementUserText.getKeys(guids)
keys = [k for k in keys if Rhyton.LAYER_HIERARCHY in k]
ElementUserText.remove(guids, keys)
[docs]def GetBreps(filterByTypes=[8, 16, 1073741824]):
"""
Gets the currently selected Rhino objects or asks the user to go get some.
Allowed objects are by default::
8 = Surface
16 = Polysurface
8192 = Text Dot
1073741824 = Extrusion
Returns:
list[str]: A list of Rhino objects ids.
"""
selection = rs.GetObjects(preselect=True, select=True)
if not selection:
return None
breps = [str(b) for b in selection if rs.ObjectType(b) in filterByTypes]
return breps
[docs]def GetFilePath(ExtensionFilter):
"""
Gets a file path from the user.
Args:
ExtensionFilter (str): The file extension filter.
Returns:
str: The file path.
"""
return rs.OpenFileName(filter=ExtensionFilter)
[docs]def GetUnitSystem(abbreviate=False):
"""
Gets the current Rhino units.
Returns:
str: The current Rhino units.
"""
return rs.UnitSystemName(rs.UnitSystem(), abbreviate=abbreviate)