import warnings
import numpy as np
from yggdrasil import rapidjson
from yggdrasil.serialize.PlySerialize import PlySerialize, GeometryBase
[docs]class ObjDict(GeometryBase, rapidjson.geometry.ObjWavefront):
r"""Enhanced dictionary class for storing Obj information."""
[docs] @classmethod
def from_shape(cls, shape, d, conversion=1.0): # pragma: lpy
r"""Create a ply dictionary from a PlantGL shape and descritizer.
Args:
scene (openalea.plantgl.scene): Scene that should be descritized.
d (openalea.plantgl.descritizer): Descritizer.
conversion (float, optional): Conversion factor that should be
applied to the vertex positions. Defaults to 1.0.
"""
iobj = super(ObjDict, cls).from_shape(shape, d, conversion=conversion,
_as_obj=True)
if iobj is not None:
# Texcoords
if d.result.texCoordList:
iobj.setdefault('texcoords', [])
for t in d.result.texCoordList:
# TODO: Should the coords be scaled?
iobj['texcoords'].append({'u': t.x, 'v': t.y})
if d.result.texCoordIndexList:
for i, t in enumerate(d.result.texCoordIndexList):
if t[0] < len(iobj['texcoords']):
for j in range(3):
iobj['faces'][i][j]['texcoord_index'] = t[j]
# Normals
if d.result.normalList:
iobj.setdefault('normals', [])
for n in d.result.normalList:
iobj['normals'].append({'i': n.x, 'j': n.y, 'k': n.z})
if d.result.normalIndexList:
for i, n in enumerate(d.result.normalIndexList):
if n[0] < len(iobj['normals']):
for j in range(3):
iobj['faces'][i][j]['normal_index'] = n[j]
return iobj
[docs] def to_geom_args(self, conversion=1.0, name=None): # pragma: lpy
r"""Get arguments for creating a PlantGL geometry.
Args:
conversion (float, optional): Conversion factor that should be
applied to the vertices. Defaults to 1.0.
name (str, optional): Name that should be given to the created
PlantGL symbol. Defaults to None and is ignored.
Returns:
tuple: Class, arguments and keyword arguments for PlantGL geometry.
"""
import openalea.plantgl.all as pgl
smb_class, args, kwargs = super(ObjDict, self).to_geom_args(
conversion=conversion, name=name, _as_obj=True)
index_class = pgl.Index
array_class = pgl.IndexArray
# Texture coords
if self.get('texcoords', []):
obj_texcoords = []
for t in self['texcoords']:
obj_texcoords.append(pgl.Vector2(np.float64(t['u']),
np.float64(t.get('v', 0.0))))
kwargs['texCoordList'] = pgl.Point2Array(obj_texcoords)
obj_ftexcoords = []
for i, f in enumerate(self['faces']):
entry = []
for _f in f:
if 'texcoord_index' not in _f:
if i > 0: # pragma: debug
warnings.warn(("'texcoord_index' missing from face"
+ "%d, texcoord indices will be "
+ "ignored.") % i)
obj_ftexcoords = []
entry = []
break
entry.append(int(_f['texcoord_index']))
if not entry:
break
obj_ftexcoords.append(index_class(*entry))
if obj_ftexcoords:
kwargs['texCoordIndexList'] = array_class(obj_ftexcoords)
# Normals
if self.get('normals', []):
obj_normals = []
for n in self['normals']:
obj_normals.append(pgl.Vector3(np.float64(n['i']),
np.float64(n['j']),
np.float64(n['k'])))
kwargs['normalList'] = pgl.Point3Array(obj_normals)
obj_fnormals = []
for i, f in enumerate(self['faces']):
entry = []
for _f in f:
if 'normal_index' not in _f:
if i > 0: # pragma: debug
warnings.warn(("'normal_index' missing from face"
+ "%d, normal indices will be "
+ "ignored.") % i)
obj_fnormals = []
entry = []
break
entry.append(int(_f['normal_index']))
if not entry:
break
obj_fnormals.append(index_class(*entry))
if obj_fnormals:
kwargs['normalIndexList'] = array_class(obj_fnormals)
return smb_class, args, kwargs
[docs] def apply_scalar_map(self, *args, **kwargs):
r"""Set the color of faces in a 3D object based on a scalar map.
This creates a copy unless no_copy is True.
Args:
scalar_arr (arr): Scalar values that should be mapped to colors
for each face.
color_map (str, optional): The name of the color map that should
be used. Defaults to 'plasma'.
vmin (float, optional): Value that should map to the minimum of the
colormap. Defaults to min(scalar_arr).
vmax (float, optional): Value that should map to the maximum of the
colormap. Defaults to max(scalar_arr).
scaling (str, optional): Scaling that should be used to map the scalar
array onto the colormap. Defaults to 'linear'.
scale_by_area (bool, optional): If True, the elements of the scalar
array will be multiplied by the area of the corresponding face.
If True, vmin and vmax should be in terms of the scaled array.
Defaults to False.
no_copy (bool, optional): If True, the returned object will not be a
copy. Defaults to False.
Returns:
dict: Obj with updated vertex colors.
"""
kwargs['_as_obj'] = True
return super(ObjDict, self).apply_scalar_map(*args, **kwargs)
[docs]class ObjSerialize(PlySerialize):
r"""Class for serializing/deserializing .obj file formats. Reader
adapted from https://www.pygame.org/wiki/OBJFileLoader."""
_seritype = 'obj'
_schema_subtype_description = ('Serialize 3D structures using Obj format.')
default_datatype = {'type': 'obj'}
file_extensions = ['.obj']
[docs] def func_serialize(self, args):
r"""Serialize a message.
Args:
args: List of arguments to be formatted or numpy array to be
serialized.
Returns:
bytes: Serialized message.
"""
assert isinstance(args, ObjDict)
return str(args).strip().encode("utf-8")
[docs] def func_deserialize(self, msg):
r"""Deserialize a message.
Args:
msg (bytes): Message to be deserialized.
Returns:
obj: Deserialized message.
"""
return ObjDict(msg.strip())
[docs] def normalize(self, args):
r"""Normalize a message to conform to the expected datatype.
Args:
args (object): Message arguments.
Returns:
object: Normalized message.
"""
if isinstance(args, ObjDict):
return args
elif self.is_mesh(args):
return ObjDict.from_mesh(
args, prune_duplicates=self.prune_duplicates)
return ObjDict(super(PlySerialize, self).normalize(args))
[docs] @classmethod
def get_testing_options(cls, **kwargs):
r"""Method to return a dictionary of testing options for this class.
Returns:
dict: Dictionary of variables to use for testing.
"""
out = super(ObjSerialize, cls).get_testing_options()
obj = ObjDict(
{'vertices': [{'x': float(0), 'y': float(0), 'z': float(0)},
{'x': float(0), 'y': float(0), 'z': float(1)},
{'x': float(0), 'y': float(1), 'z': float(1)},
{'x': float(1), 'y': float(1), 'z': float(1)}],
'faces': [
[{'vertex_index': int(0)},
{'vertex_index': int(1)},
{'vertex_index': int(2)}],
[{'vertex_index': int(1)},
{'vertex_index': int(2)},
{'vertex_index': int(3)}],
[{'vertex_index': int(0)},
{'vertex_index': int(1)},
{'vertex_index': int(2)},
{'vertex_index': int(3)}]],
'comments': ["Author ygg_auto", "Generated by yggdrasil"]})
out['objects'] = [obj, obj]
out['contents'] = (b'# Author ygg_auto\n'
+ b'# Generated by yggdrasil\n'
+ b'v 0.0 0.0 0.0\n'
+ b'v 0.0 0.0 1.0\n'
+ b'v 0.0 1.0 1.0\n'
+ b'v 1.0 1.0 1.0\n'
+ b'f 1 2 3\n'
+ b'f 2 3 4\n'
+ b'f 1 2 3 4\n'
+ b'# Author ygg_auto\n'
+ b'# Generated by yggdrasil\n'
+ b'v 0.0 0.0 0.0\n'
+ b'v 0.0 0.0 1.0\n'
+ b'v 0.0 1.0 1.0\n'
+ b'v 1.0 1.0 1.0\n'
+ b'f 5 6 7\n'
+ b'f 6 7 8\n'
+ b'f 5 6 7 8')
return out