#!/usr/bin/env python
# -*- coding: utf-8 -*-
from collections import namedtuple
from collections.abc import Sequence
import numbers
import math
import re
import warnings
from traitlets import (
Unicode, Int, CInt, Instance, Enum, List, Dict, Float, CFloat,
Bool, Tuple, Undefined, TraitError, Union, TraitType, CaselessStrEnum
)
from ipywidgets import widget_serialization
from ipydatawidgets import DataUnion, NDArrayWidget, shape_constraints
import numpy as np
def _castable_namedtuple(typename, field_names):
base = namedtuple('%s_base' % typename, field_names)
def new_new(cls, *args, **kwargs):
if not kwargs and len(args) == 1 and isinstance(args, Sequence):
return base.__new__(cls, *args[0], **kwargs)
return base.__new__(cls, *args, **kwargs)
return type(typename, (base,), {'__new__': new_new})
def _ieee_to_json(value, owner):
if isinstance(value, numbers.Real):
# cast out-of-range floats to their reprs
if math.isnan(value) or math.isinf(value):
return repr(value)
return value
ieee_float_serializers = {
'to_json': _ieee_to_json,
}
[docs]class IEEEFloat(CFloat):
def __init__(self, default_value=Undefined, **kwargs):
super().__init__(default_value=default_value, **kwargs)
self.metadata.setdefault("to_json", _ieee_to_json)
def _ieee_tuple_to_json(value, owner):
if value is None:
return value
return [_ieee_to_json(e, owner) for e in value]
[docs]class Vector2(Tuple):
"""A trait for a 2-tuple corresponding to a three.js Vector2.
"""
default_value = (0, 0)
info_text = 'a two-element vector'
def __init__(self, trait=Undefined, default_value=Undefined, **kwargs):
if trait is Undefined:
trait = IEEEFloat()
if default_value is Undefined:
default_value = self.default_value
else:
self.default_value = default_value
super(Vector2, self).__init__(*(trait, trait), default_value=default_value, **kwargs)
if isinstance(trait, IEEEFloat):
self.metadata.setdefault("to_json", _ieee_tuple_to_json)
[docs]class Vector3(Tuple):
"""A trait for a 3-tuple corresponding to a three.js Vector3.
"""
default_value = (0, 0, 0)
info_text = 'a three-element vector'
def __init__(self, trait=Undefined, default_value=Undefined, **kwargs):
if trait is Undefined:
trait = IEEEFloat()
if default_value is Undefined:
default_value = self.default_value
else:
self.default_value = default_value
super(Vector3, self).__init__(*(trait, trait, trait), default_value=default_value, **kwargs)
if isinstance(trait, IEEEFloat):
self.metadata.setdefault("to_json", _ieee_tuple_to_json)
[docs]class Vector4(Tuple):
"""A trait for a 4-tuple corresponding to a three.js Vector4.
"""
default_value = (0, 0, 0, 0)
info_text = 'a four-element vector'
def __init__(self, trait=Undefined, default_value=Undefined, **kwargs):
if trait is Undefined:
trait = IEEEFloat()
if default_value is Undefined:
default_value = self.default_value
else:
self.default_value = default_value
super(Vector4, self).__init__(*(trait, trait, trait, trait), default_value=default_value, **kwargs)
if isinstance(trait, IEEEFloat):
self.metadata.setdefault("to_json", _ieee_tuple_to_json)
[docs]class Matrix3(Tuple):
"""A trait for a 9-tuple corresponding to a three.js Matrix3.
"""
default_value = (
1, 0, 0,
0, 1, 0,
0, 0, 1
)
info_text = 'a three-by-three matrix (9 element tuple)'
def __init__(self, trait=Undefined, default_value=Undefined, **kwargs):
if trait is Undefined:
trait = IEEEFloat()
if default_value is Undefined:
default_value = self.default_value
else:
self.default_value = default_value
super(Matrix3, self).__init__(*((trait,) * 9), default_value=default_value, **kwargs)
if isinstance(trait, IEEEFloat):
self.metadata.setdefault("to_json", _ieee_tuple_to_json)
[docs]class Matrix4(Tuple):
"""A trait for a 16-tuple corresponding to a three.js Matrix4.
"""
default_value = (
1, 0, 0, 0,
0, 1, 0, 0,
0, 0, 1, 0,
0, 0, 0, 1
)
info_text = 'a four-by-four matrix (16 element tuple)'
def __init__(self, trait=Undefined, default_value=Undefined, **kwargs):
if trait is Undefined:
trait = IEEEFloat()
if default_value is Undefined:
default_value = self.default_value
else:
self.default_value = default_value
super(Matrix4, self).__init__(*((trait,) * 16), default_value=default_value, **kwargs)
if isinstance(trait, IEEEFloat):
self.metadata.setdefault("to_json", _ieee_tuple_to_json)
def _face_to_json(value, owner):
if value is None:
return None
value = list(value)
if value[3] is not None:
normal = list(value[3])
for i, v in enumerate(normal):
if isinstance(v, tuple):
normal[i] = _ieee_tuple_to_json(v, owner)
else:
normal[i] = _ieee_to_json(v, owner)
value[3] = normal
return value
[docs]class Face3(Tuple):
"""A trait for a named tuple corresponding to a three.js Face3.
Accepts named tuples with the field names:
('a', 'b', 'c', 'normal', 'color', 'materialIndex')
"""
klass = _castable_namedtuple('Face3', ('a', 'b', 'c', 'normal', 'color', 'materialIndex'))
_cast_types = (list, tuple)
info_text = 'a named tuple representing a Face3'
def __init__(self, **kwargs):
super(Face3, self).__init__(
CInt(), # a - Vertex A index.
CInt(), # b - Vertex B index.
CInt(), # c - Vertex C index.
Union([ # normal - (optional) Face normal (Vector3) or array of 3 vertex normals.
Vector3(allow_none=True),
Tuple((Vector3(),) * 3),
]),
Union([ # color - (optional) Face color or array of vertex colors.
Unicode(allow_none=True),
Tuple((Unicode(),) * 3),
]),
CInt(allow_none=True), # materialIndex - (optional) which index of an array of materials to associate with the face.
default_value=(0, 0, 0, None, None, None)
)
self.metadata.setdefault("to_json", _face_to_json)
def _euler_to_json(value, owner):
if value is None:
return None
return _ieee_tuple_to_json(value[:3], owner) + [value[3]]
[docs]class Euler(Tuple):
"""A trait for a set of Euler angles.
Expressed as a tuple of tree floats (the angles), and the order as a string.
See the three.js docs for futher details.
"""
info_text = 'a set of Euler angles'
default_value = (0, 0, 0, 'XYZ')
_accepted_orders = ['XYZ', 'YZX', 'ZXY', 'XZY', 'YXZ', 'ZYX']
def __init__(self, default_value=Undefined, **kwargs):
if default_value is Undefined:
default_value = self.default_value
else:
self.default_value = default_value
super(Euler, self).__init__(
IEEEFloat(), IEEEFloat(), IEEEFloat(),
Enum(self._accepted_orders, self._accepted_orders[0]),
default_value=default_value , **kwargs)
self.metadata.setdefault("to_json", _euler_to_json)
[docs]class WebGLDataUnion(DataUnion):
"""A trait that accepts either a numpy array, or an NDArrayWidget reference.
Also constrains the use of 64-bit arrays, as this is not supported by WebGL.
"""
[docs] def validate(self, obj, value):
was_original_array = isinstance(value, np.ndarray)
value = super(WebGLDataUnion, self).validate(obj, value)
array = value.array if isinstance(value, NDArrayWidget) else value
dtype_str = str(array.dtype) if array is not Undefined else ''
if dtype_str == 'float64' or dtype_str.endswith('int64'):
if isinstance(value, NDArrayWidget):
raise TraitError(
'Cannot use a %s data widget as a WebGL source.' %
(dtype_str,))
else:
# 64-bit not supported, coerce to 32-bit
# If original was another array, warn about casting,
# as it might otherwise silently increase memory usage:
if was_original_array:
warnings.warn('64-bit data types not supported for WebGL '
'data, casting to 32-bit.')
value = value.astype(dtype_str.replace('64', '32'))
return value
[docs]class Uninitialized:
"""Placeholder sentinel used while waiting for a initialization via sync"""
pass
_widget_to_json = widget_serialization['to_json']
def _serialize_uninitialized(value, owner):
if isinstance(value, Uninitialized):
return 'uninitialized'
return _widget_to_json(value, owner)
unitialized_serialization = {
'to_json': _serialize_uninitialized,
'from_json': widget_serialization['from_json'],
}
UninitializedSentinel = Uninitialized()
# Color trait. In process of being upstreamed to ipywidgets
_color_names = ['aliceblue', 'antiquewhite', 'aqua', 'aquamarine', 'azure', 'beige', 'bisque', 'black', 'blanchedalmond', 'blue', 'blueviolet', 'brown', 'burlywood', 'cadetblue', 'chartreuse', 'chocolate', 'coral', 'cornflowerblue', 'cornsilk', 'crimson', 'cyan', 'darkblue', 'darkcyan', 'darkgoldenrod', 'darkgray', 'darkgreen', 'darkkhaki', 'darkmagenta', 'darkolivegreen', 'darkorange', 'darkorchid', 'darkred', 'darksalmon', 'darkseagreen', 'darkslateblue', 'darkslategray', 'darkturquoise', 'darkviolet', 'deeppink', 'deepskyblue', 'dimgray', 'dodgerblue', 'firebrick', 'floralwhite', 'forestgreen', 'fuchsia', 'gainsboro', 'ghostwhite', 'gold', 'goldenrod', 'gray', 'green', 'greenyellow', 'honeydew', 'hotpink', 'indianred ', 'indigo ', 'ivory', 'khaki', 'lavender', 'lavenderblush', 'lawngreen', 'lemonchiffon', 'lightblue', 'lightcoral', 'lightcyan', 'lightgoldenrodyellow', 'lightgray', 'lightgreen', 'lightpink', 'lightsalmon', 'lightseagreen', 'lightskyblue', 'lightslategray', 'lightsteelblue', 'lightyellow', 'lime', 'limegreen', 'linen', 'magenta', 'maroon', 'mediumaquamarine', 'mediumblue', 'mediumorchid', 'mediumpurple', 'mediumseagreen', 'mediumslateblue', 'mediumspringgreen', 'mediumturquoise', 'mediumvioletred', 'midnightblue', 'mintcream', 'mistyrose', 'moccasin', 'navajowhite', 'navy', 'oldlace', 'olive', 'olivedrab', 'orange', 'orangered', 'orchid', 'palegoldenrod', 'palegreen', 'paleturquoise', 'palevioletred', 'papayawhip', 'peachpuff', 'peru', 'pink', 'plum', 'powderblue', 'purple', 'rebeccapurple', 'red', 'rosybrown', 'royalblue', 'saddlebrown', 'salmon', 'sandybrown', 'seagreen', 'seashell', 'sienna', 'silver', 'skyblue', 'slateblue', 'slategray', 'snow', 'springgreen', 'steelblue', 'tan', 'teal', 'thistle', 'tomato', 'transparent', 'turquoise', 'violet', 'wheat', 'white', 'whitesmoke', 'yellow', 'yellowgreen']
_color_re = re.compile(r'#[a-fA-F0-9]{3}(?:[a-fA-F0-9]{3})?$')
_color_hexa_re = re.compile(r'^#[a-fA-F0-9]{4}(?:[a-fA-F0-9]{4})?$')
_color_frac_percent = r'\s*(\d+(\.\d*)?|\.\d+)?%?\s*'
_color_int_percent = r'\s*\d+%?\s*'
_color_rgb = r'rgb\({ip},{ip},{ip}\)'
_color_rgba = r'rgba\({ip},{ip},{ip},{fp}\)'
_color_hsl = r'hsl\({fp},{fp},{fp}\)'
_color_hsla = r'hsla\({fp},{fp},{fp},{fp}\)'
_color_rgbhsl_re = re.compile('({0})|({1})|({2})|({3})'.format(
_color_rgb, _color_rgba, _color_hsl, _color_hsla
).format(ip=_color_int_percent, fp=_color_frac_percent))
[docs]class Color(Unicode):
"""A string holding a valid HTML color such as 'blue', '#060482', '#A80'"""
info_text = 'a valid HTML color'
default_value = Undefined
[docs] def validate(self, obj, value):
if value is None and self.allow_none:
return value
if isinstance(value, str):
if value.lower() in _color_names or _color_re.match(value):
return value
elif _color_hexa_re.match(value) or _color_rgbhsl_re.match(value):
return value
self.error(obj, value)