import logging
import numpy as np
from .internal import Shape
DEFAULT_DIRECTIONAL_LIGHTS = (
[ 0.4, -0.4, -0.4 ],
[-0.25, -0.0625, -0.25 ],
[ 0, 0.125, -0.125]
)
logger = logging.getLogger(__name__)
[docs]class Scene:
"""A container to hold and display collections of primitives.
`Scene` keeps track of global information about a set of things to
be rendered and handles configuration of optional (possibly
backend-specific) rendering parameters.
Global information managed by a `Scene` includes the `size` of the
viewing window, `translation` and `rotation` applied to the scene
as a whole, and a `zoom` level.
Primitives can be added to a scene through the `primitives`
argument of the constructor or the `add_primitive`
method. Primitives can be retrieved by iterating over the scene::
for prim in scene:
# (do something with prim)
Primitives can also be accessed in the order they were added to
the scene using list-like syntax::
first_three_prims = scene[:3]
last_prim = scene[-1]
Optional rendering arguments are enabled as *features*, which are
name-value pairs identifying a feature by name and any
configuration of the feature in the value.
"""
def __init__(self, primitives=[], features={}, size=(40, 30),
translation=(0, 0, -50), rotation=(1, 0, 0, 0), zoom=1,
pixel_scale=20, **kwargs):
"""Initialize a `Scene` object.
:param primitives: List of primitives to include in the scene, or a single primitive
:param features: Dictionary mapping names of features to feature configuration options. Options can be single values (which will be converted to `dict(value=given_value)` or dicts.
:param size: Width and height, in scene units, of the viewport (before scaling by `zoom`)
:param translation: (x, y, z) translation to be applied to the scene as a whole after rotating. `x` is to the right, `y` is up, and `z` comes toward you out of the screen.
:param rotation: (r, x, y, z) rotation quaternion to be applied to the scene as a whole.
:param zoom: Zoom scaling factor to be applied to the scene.
:param pixel_scale: Number of pixels per scene unit length.
"""
# map each enabled feature's name to a configuration object
self._enabled_features = dict()
self._primitives = []
self._size = np.ones((2,), dtype=np.float32)
self._translation = np.zeros((3,), dtype=np.float32)
self._rotation = np.array([1, 0, 0, 0], dtype=np.float32)
self.pixel_scale = pixel_scale
self.size = size
self.zoom = zoom
self.translation = translation
self.rotation = rotation
if isinstance(primitives, Shape):
# Convert an individual primitive object to a list of primitives
primitives = [primitives]
for prim in primitives:
self.add_primitive(prim)
if 'directional_light' not in features:
self.enable('directional_light', DEFAULT_DIRECTIONAL_LIGHTS)
for feature in features:
config = features[feature]
if isinstance(config, dict):
if 'name' in config:
raise ValueError('Feature parameters can\'t be named "name"')
self.enable(feature, **config)
else:
self.enable(feature, auto_value=config)
for name in kwargs:
setattr(self, name, kwargs[name])
def __getitem__(self, key):
"""Returns the primitive(s) given an integer index or slice."""
return self._primitives[key]
def __iter__(self):
for prim in self._primitives:
yield prim
def __len__(self):
return len(self._primitives)
@property
def translation(self):
"""(x, y, z) translation to be applied to the scene as a whole after rotating.
`x` is to the right, `y` is up, and `z` comes toward you out
of the screen.
"""
return self._translation
@translation.setter
def translation(self, value):
self._translation[:] = value
for prim in self._primitives:
prim.translation = self._translation
@property
def rotation(self):
"""(r, x, y, z) rotation quaternion to be applied to the scene as a whole."""
return self._rotation
@rotation.setter
def rotation(self, value):
self._rotation[:] = value
for prim in self._primitives:
prim.rotation = self._rotation
if 'link_rotation' in self.enabled_features:
for target in self.get_feature_config('link_rotation')['targets']:
if target is not self and not np.allclose(target.rotation, value):
target.rotation = value
@property
def size(self):
"""Width and height, in scene units, of the viewport."""
return self._size
@size.setter
def size(self, value):
self._size[:] = value
@property
def size_pixels(self):
"""Width and height, in pixels, of the viewport."""
return self.size*self.pixel_scale
@size_pixels.setter
def size_pixels(self, value):
self.size = np.asarray(value, dtype=np.float32)/self.pixel_scale
[docs] def add_primitive(self, primitive):
"""Adds a primitive to the scene."""
self._primitives.append(primitive)
primitive.translation = self.translation
primitive.rotation = self.rotation
[docs] def remove_primitive(self, primitive, strict=True):
"""Removes a primitive from the scene.
:param primitive: primitive to (attempt to) remove
:param strict: If True, raise an IndexError if the primitive was not in the scene
"""
try:
self._primitives.remove(primitive)
except ValueError:
if strict:
raise
@property
def enabled_features(self):
return set(self._enabled_features)
[docs] def get_feature_config(self, name):
"""Return the configuration dictionary for a given feature.
If the feature has not been enabled, return None.
"""
if name in self._enabled_features:
return self._enabled_features[name]
else:
return None
[docs] def enable(self, name, auto_value=None, **parameters):
"""Enable an optional rendering feature.
:param name: Name of the feature to enable
:param auto_value: Shortcut for features with single-value configuration. If given as a positional argument, will be given the default configuration name 'value'.
:param parameters: Keyword arguments specifying additional configuration options for the given feature
"""
if auto_value is not None:
parameters['value'] = auto_value
if name == 'link_rotation':
targets = parameters.setdefault('targets', [])
if 'target' in parameters:
targets.append(parameters['target'])
if 'value' in parameters:
targets.append(parameters['value'])
for target in targets:
target.rotation = self._rotation
self._enabled_features[name] = dict(parameters)
[docs] def disable(self, name, strict=True):
"""Disable an optional rendering feature.
:param name: Name of the feature to disable
:param strict: if True, raise a KeyError if the feature was not enabled
"""
if not strict and name not in self._enabled_features:
return
if name == 'link_rotation':
targets = self.get_feature_config('link_rotation')['targets']
for target in targets:
target._rotation = target._rotation.copy()
del self._enabled_features[name]
[docs] def convert(self, backend, compatibility='warn', **kwargs):
"""Convert this scene and all of its primitives to another backend.
:param backend: Backend plato.draw.* module to use in the new scene
:param compatibility: Behavior when unsupported primitives are encountered: 'warn', 'ignore', or 'error'
:param kwargs: Additional keyword arguments to be passed into the backend `Scene` constructor
"""
scene_kwargs = dict(
features=self._enabled_features, size=self.size, translation=self.translation,
rotation=self.rotation, zoom=self.zoom, pixel_scale=self.pixel_scale
)
scene_kwargs.update(kwargs)
backend_scene = backend.Scene(**scene_kwargs)
for prim in self:
name = type(prim).__name__
try:
backend_cls = getattr(backend, name)
except AttributeError as e:
msg = 'Incompatible primitive {} for backend {}'.format(
name, backend)
if compatibility == 'warn':
logger.warning(msg)
continue
elif compatibility == 'ignore':
continue
else:
raise TypeError(msg)
backend_scene.add_primitive(backend_cls.copy(prim))
return backend_scene