Source code for rhyton.color

"""
Module for color operations.
"""
# python standard imports
import os
import json
import random
import colorsys
from itertools import repeat
from collections import defaultdict

# rhyton imports
from rhyton.main import Rhyton
from rhyton.document import DocumentConfigStorage, ElementUserText, ElementOverrides

[docs]class Color: """ Class for basic color operations. """
[docs] @staticmethod def HSVtoRGB(hsv): """ Convert a color from hsv to rgb. Args: hsv (tuple): A color in hsv format. Returns: tuple: A color in rgb format """ return tuple(round(i * 255) for i in colorsys.hsv_to_rgb(hsv[0], hsv[1], hsv[2]))
[docs] @staticmethod def RGBtoHEX(rgb): """ Convert an RGB color to HEX. Args: rgb (tuple): The RGB color. Returns: str: The HEX color """ return '%02x%02x%02x' % rgb
[docs] @staticmethod def HEXtoRGB(hexColor): """ Converts a hex color string to rgb. Args: hexColor (sting): The hex color Returns: tuple: The rgb color """ hexColor = hexColor.lstrip('#') return tuple(int(hexColor[i:i+2], 16) for i in (0, 2, 4))
[docs]class ColorScheme: """ Class for handling relationships between labels and colors. """
[docs] def __init__(self): """ Inits a new ColorScheme instance. """ self.flag = Rhyton().extensionColorSchemes self.schemes = DocumentConfigStorage().get( self.flag, defaultdict()) self.defaultColors = [ '#F44336', '#E91E63', '#9C27B0', '#673AB7', '#3F51B5', '#2196F3', '#03A9F4', '#00BCD4', '#009688', '#4CAF50', '#8BC34A', '#CDDC39', '#FFEB3B', '#FFC107', '#FF9800', '#FF5722', '#795548', '#607D8B' ] self.additionalColors = [ '#D32F2F', '#C2185B', '#7B1FA2', '#512DA8', '#303F9F', '#1976D2', '#0288D1', '#0097A7', '#00796B', '#388E3C', '#689F38', '#AFB42B', '#FBC02D', '#FFA000', '#F57C00', '#E64A19', '#5D4037', '#616161', '#455A64' ] self.extendedColors = self.defaultColors + self.additionalColors
[docs] @staticmethod def toJSON(data, path): """ Write color scheme to json Args: data (dict): The data to export path (str): The output file path. Returns: string: The json filepath """ directory = os.path.dirname(path) if not os.path.exists(directory): os.makedirs(directory) with open(path, 'w') as f: json.dump(data, f) return path
[docs] @staticmethod def fromJSON(path): """ Reads a color scheme from json. Args: path (string): The json file Returns: dict: The color scheme """ with open(path, 'r') as f: scheme = json.load(f) return scheme
[docs] @staticmethod def apply(guids, schemeName): """ Applies a rhyton color scheme to given objects. Updates the color scheme with new keys and colors. Args: guids (str): A list of Rhino objects ids schemeName (string): The name of the color scheme Returns: dict: Same return as :func:`rhyton.document.ElementUserText.getValues` but with key "color" added. """ keys = ElementUserText.getValues(guids, keys=schemeName) colorScheme = ColorScheme() keyColors = colorScheme.schemes.get(schemeName) if not keyColors: keyColors = colorScheme.generate(keys) if not keyColors: return None colorScheme.save(schemeName, keyColors) else: colorScheme.update(schemeName, keys) keyColors = colorScheme.schemes.get(schemeName) objectData = ElementUserText.get(guids, keys=schemeName) for entry in objectData: value = entry.get(schemeName) if value: entry[Rhyton.COLOR] = keyColors[value] else: entry[Rhyton.COLOR] = Rhyton.HEX_WHITE entry[schemeName] = Rhyton.NOT_AVAILABLE ElementOverrides.apply(objectData) return objectData
[docs] @staticmethod def applyGradient(guids, schemeName, gradient): """ Applies a rhyton color gradient to given objects. Args: guids (str): A list of guids. schemeName (str): The name of the color scheme. gradient (list): Two RGB colors: [start, end] """ rawValues = ElementUserText.getValues(guids, keys=schemeName) values = sorted(rawValues) keyColors = ColorScheme().generate(values, gradient=gradient) objectData = ElementUserText.get(guids, keys=schemeName) for entry in objectData: value = entry.get(schemeName) if value and value != Rhyton.WHITESPACE: entry[Rhyton.COLOR] = keyColors[value] ElementOverrides.apply(objectData) return objectData
[docs] def generate(self, keys, excludeColors=None, gradient=False): """ Generates a new color scheme. A color scheme has the following layout:: { <schemeName>: {"key1": <hexcolor>} } Args: schemeName (string): The name of the color scheme keys (string): A set of keys excludeColors (string, optional): A list of colors to exclude gradient (int, optional): A tuple with start and end color Returns: dict: A color scheme """ if not gradient: colors = ColorScheme().getColors(len(keys), excludeColors) elif gradient: colorsRGB = Gradient.betweenRgbColors(len(keys), gradient[0], gradient[1]) colors = [Color.RGBtoHEX(rgb) for rgb in colorsRGB] keyColors = dict() for value, color in zip(sorted(keys), colors): keyColors[value] = color return keyColors
[docs] def update(self, schemeName, keys): """ Updates a given color scheme with new keys and default colors and saves the changes to the DocumentConfigStorage. Args: scheme (dict): The color scheme to update keys (set): The keys to add Returns: dict: The updated color scheme """ oldKeys = set(self.schemes[schemeName].keys()) newKeys = list(set(keys).difference(oldKeys)) if newKeys: excludeColors = self.schemes[schemeName].values() tempKeyColors = ColorScheme().generate( newKeys, excludeColors=excludeColors) if not tempKeyColors: return None self.schemes[schemeName].update(tempKeyColors) DocumentConfigStorage().save(self.flag, self.schemes)
[docs] def save(self, schemeName, keyValues): """ Saves a single color scheme to the rhyton DocumentConfigStorage. Color schemes are stored as follows:: { <extensionName>.colorSchemes: { <schemeName1>: {"key1": "value1"}, <schemeName2>: {"key1": "value1"} } } Args: schemeName (str): The name of the color scheme keyValues (dict): The keys and colors associated with the name """ scheme = dict() scheme[schemeName] = keyValues self.schemes.update(scheme) DocumentConfigStorage().save(self.flag, self.schemes)
[docs] def delete(self, schemeName): """ Deletes a color scheme from the rhyton DocumenConfigStorage. Args: scheme (dict): A color scheme """ if schemeName in self.schemes: del self.schemes[schemeName] DocumentConfigStorage().save(self.flag, self.schemes)
[docs] def getColors(self, count, excludeColors=[]): """ Gets a given amount of colors. Args: count (int): The number of colors to get excludeColors (string, optional): List of colors to exclude. Defaults to None. Returns: string: A list of colors """ self.defaultColors = self._filterColors( excludeColors, self.defaultColors) self.extendedColors = self._filterColors( excludeColors, self.extendedColors) if count <= len(self.defaultColors): availableColors = self.defaultColors elif count <= len(self.extendedColors): availableColors = self.extendedColors elif count >= len(self.extendedColors) and count < 100: hsvColors = ColorRange(count).getHSV() availableColors = [] for hsvColor in hsvColors: rgbColor = Color.HSVtoRGB(hsvColor) hexColor = Color.RGBtoHEX(rgbColor) availableColors.append(hexColor) else: print('Too many keys, colors are indistiguishable.') return None colors = random.sample(availableColors, count) return colors
def _filterColors(self, excludeColors, colors): """ Filters a list of colors. Args: excludeColors (list): A list of colors to exclude. colors (list): A list of colors to filter. Returns: lsit: A filtered list of colors. """ if excludeColors: availableColors = filter( lambda color: color not in excludeColors, colors) return availableColors else: return colors
[docs]class ColorRange: """ Class for working with color ranges. """
[docs] def __init__(self, count, min=0, max=100): """ Inits a new ColorRange instance. Accepted values:: 0 <= min < 100 1 < max <= 100 count < max - min """ if 0 <= min and min < 100: self.min = min else: return None if 1 < max and max <= 100: self.max = max else: return None if count < max - min: self.count = count else: return None self.range = max - min if not self.count <= self.range: print('Count bigger than range.') return None
[docs] def getHSV(self): """ Gets a list of colors in hsv format. Returns: list: A list of hsv colors """ hsv = [] for i in [ x * 0.01 for x in range( self.min, self.max, (self.range / self.count))]: hsv.append((i, 0.5, 0.9)) return hsv
[docs]class Gradient: """ Class for working with gradients. """
[docs] @classmethod def betweenRgbColors(cls, count, start, end): """ Create a range of colors by interpolating the individual r, g, b values. Args: count (int): The total amout of colors to return. start (tuple): The start color. end (tuple): The end color. """ rangeR = cls._getRange(start[0], end[0], count) rangeG = cls._getRange(start[1], end[1], count) rangeB = cls._getRange(start[2], end[2], count) return tuple(zip(rangeR, rangeG, rangeB))
@staticmethod def _getRange(start, end, count): """ Gets a range of values. Will only return positive values. Args: start (int): The start value. end (int): The end value. count (int): The total amount of values to return. Returns: list: A list of values. """ if start == end: return repeat(start, count) step = (end - start) / count increments = [] for i in range(count): increments.append(abs(int(start))) start += step return increments