import ddg
import json
import re
import numpy as np
[docs]class HalfedgeEncoder(json.JSONEncoder):
def __init__(self, vertex_attrs=[], edge_attrs=[], face_attrs=[],
**kwargs):
super(HalfedgeEncoder, self).__init__(**kwargs)
self.vertex_slots = ['edge']
self.vertex_attrs = vertex_attrs
self.edge_slots = ['pre', 'nex', 'opp', 'head', 'face']
self.edge_attrs = edge_attrs
self.face_slots = ['edge']
self.face_attrs = face_attrs
[docs] def default(self, obj):
if isinstance(obj, ddg.halfedge.Surface):
obj.verts.add_attribute('json_idx')
obj.edges.add_attribute('json_idx')
obj.faces.add_attribute('json_idx')
for i, v in enumerate(obj.verts):
v.json_idx = i
for i, e in enumerate(obj.edges):
e.json_idx = i
for i, f in enumerate(obj.faces):
f.json_idx = i
obj_dict = dict()
obj_dict['verts'] = self._create_dicts(obj, obj.verts,
self.vertex_slots,
self.vertex_attrs)
obj_dict['edges'] = self._create_dicts(obj, obj.edges,
self.edge_slots,
self.edge_attrs)
obj_dict['faces'] = self._create_dicts(obj, obj.faces,
self.face_slots,
self.face_attrs)
return obj_dict
return json.JSONEncoder.default(self, obj)
@staticmethod
def _create_dicts(surface, nodes, slots, attrs):
nodes_dicts = []
for v in nodes:
node_dict = {}
for slot in slots:
node_dict[slot] = HalfedgeEncoder._get_index(surface,
getattr(v, slot))
for attr in attrs:
node_attribute = getattr(nodes, attr)
value = node_attribute[v]
if isinstance(value, np.ndarray):
node_dict[attr] = tuple(value)
else:
node_dict[attr] = value
nodes_dicts.append(node_dict)
return nodes_dicts
@staticmethod
def _get_index(surface, value):
if isinstance(value, surface.verts):
return value.json_idx
if isinstance(value, surface.edges):
return value.json_idx
if isinstance(value, surface.faces):
return value.json_idx
if value is None:
return -1
return value
[docs]def to_json_string(surface, vertex_attrs=[], edge_attrs=[], face_attrs=[],
write_index=False):
"""
Create a json string from a given halfedge surface. Additional
attributes to
be included in the json string need to be specified explicitly in the
respective
dictionaries. All the references to vertices, edges, and faces are
realized using
integer indices. The `None` face corresponds to `-1`
Parameters
----------
surface : halfedge surface
The surface to be converted to a json string
vertex_attrs : list of strings
vertex attributes to be contained in the json string in addition to
`edge`
edge_attrs
edge attributes to be contained in the json string in addition to
`pre`, `nex`,
`opp`, `head`, `face`
face_attrs
face attributes to be contained in the json string in addition to
`edge`
Returns
-------
string
"""
if write_index:
halfedge_encoder = HalfedgeEncoder(
vertex_attrs + ['json_idx'],
edge_attrs + ['json_idx'],
face_attrs + ['json_idx'])
else:
halfedge_encoder = HalfedgeEncoder(vertex_attrs, edge_attrs,
face_attrs, indent=2)
return halfedge_encoder.encode(surface)
[docs]def write_json_file(surface, filename, vertex_attrs=[], edge_attrs=[],
face_attrs=[]):
"""
Create a file in json format from a given halfedge surface. Additional
attributes
to be included in the json string need to be specified explicitly in the
respective
dictionaries. All the references to vertices, edges, and faces are
realized using
integer indices. The `None` face corresponds to `-1`
Parameters
----------
surface : halfedge surface
The surface to be converted to a json string
filename : string
file to be written
vertex_attrs : list of strings
vertex attributes to be contained in the json string in addition to
`edge`
edge_attrs
edge attributes to be contained in the json string in addition to
`pre`, `nex`,
`opp`, `head`, `face`
face_attrs
face attributes to be contained in the json string in addition to
`edge`
"""
with open(filename, 'w') as outfile:
outfile.write(to_json_string(surface,
vertex_attrs, edge_attrs, face_attrs))
[docs]def read_json_file(filename):
"""
Create a surface from a json file. All keys will be mapped to respective
attribute
of the vertices, edges, and faces.
Parameters
----------
filename: string
file containing a json representing a halfedge surface
Returns
-------
surface
Raises
------
ValueError
If required attributes are missing in the json string
"""
with open(filename, 'r') as f:
d = json.load(f)
_validate_surface_dict(d)
return _dict_to_surf(d)
[docs]def parse_json_string(json_string):
"""
Create a surface from a json string. All keys will be mapped to
respective attribute
of the vertices, edges, and faces.
Parameters
----------
json_string : string
formatted string containing a halfedge surface
Returns
-------
surface
Raises
------
ValueError
If required attributes are missing in the json string
"""
json_dict = json.loads(json_string)
_validate_surface_dict(json_dict)
return _dict_to_surf(json_dict)
def _validate_surface_dict(json_dict):
_validate_node_dict(json_dict["verts"], ["edge"])
_validate_node_dict(json_dict["edges"],
["pre", "nex", "opp", "head", "face"])
_validate_node_dict(json_dict["faces"], ["edge"])
def _validate_node_dict(nodes, required_attributes):
for n in nodes:
missing_attributes = required_attributes - n.keys()
if len(missing_attributes) > 0:
raise ValueError(
'Halfedge surface dictionary does not contain required '
'attributes ' + str(missing_attributes) + '.')
def _dict_to_surf(d):
s = ddg.halfedge.Surface()
vertices = _create_nodes_with_attributes(d, "verts", s.verts)
edges = _create_nodes_with_attributes(d, "edges", s.edges)
faces = _create_nodes_with_attributes(d, "faces", s.faces)
_assign_node_attributes(zip(vertices, d["verts"]), s.verts, vertices,
edges, faces)
_assign_node_attributes(zip(edges, d["edges"]), s.edges, vertices, edges,
faces)
_assign_node_attributes(zip(faces, d["faces"]), s.faces, vertices, edges,
faces)
return s
def _assign_node_attributes(pairs, nodes, vertices, edges, faces):
for node, json_node in pairs:
for k in json_node.keys():
if (k == "pre") or (k == "nex") or (k == "opp") or (k == "edge"):
setattr(node, k, edges[json_node[k]])
elif k == "face":
if json_node[k] < 0:
node.face = None
else:
node.face = faces[json_node[k]]
elif k == "head":
node.head = vertices[json_node[k]]
else:
node_attribute = getattr(nodes, k)
node_attribute[node] = json_node[k] if not isinstance(json_node[k], list) else np.array(json_node[k])
def _create_nodes_with_attributes(d, node_name, nodes):
new_nodes = [nodes() for vd in d[node_name]]
json_node = d[node_name][0]
for attribute in json_node.keys():
if not hasattr(nodes, attribute):
nodes.add_attribute(attribute)
return new_nodes
[docs]def surface_to_ifs_json(surface, vertex_attrs=['co']):
encoder = SurfaceToIFSEncoder(vertex_attrs)
return encoder.encode(surface)
[docs]class SurfaceToIFSEncoder(json.JSONEncoder):
def __init__(self, vertex_attrs=['co'], **kwargs):
super(SurfaceToIFSEncoder, self).__init__(**kwargs)
self.vertex_attrs = vertex_attrs
[docs] def default(self, surface):
if isinstance(surface, ddg.halfedge.Surface):
surface_dict = {}
surface.verts.add_attribute('json_idx')
for i, v in enumerate(surface.verts):
v.json_idx = i
surface_dict['faces'] = []
for f in surface.faces:
vertex_index_list = tuple(
[v.json_idx for v in ddg.datastructures.halfedge.get.get_vertices(f)])
surface_dict['faces'].append(vertex_index_list)
if len(self.vertex_attrs) != 0:
surface_dict['verts'] = []
for v in surface.verts:
vertex_dict = {}
for attribute in self.vertex_attrs:
v_attribute = getattr(surface.verts, attribute)
value = v_attribute[v]
vertex_dict[attribute] = value.tolist()
surface_dict['verts'].append(vertex_dict)
return surface_dict
return json.JSONEncoder.default(self, surface)
[docs]class ObjEncoder(object):
"""
Class to convert obj file to a ddg.halfedge.Surface or an IndexedFaceSet.
self.indexedfs is an IndexedFaceSet constructed of the obj file.
self.halfedgeds is a Surface with halfedge attributes "vt" and "vn" (if
given).
"""
def __init__(self, filename):
self.filename = filename
self.filetype = None
self.indexedfs = None
self.halfedgeds = None
self.v = []
self.vt = []
self.vn = []
self.f = []
self.vt_face_idx = []
self.vn_face_idx = []
self._read_obj()
def _read_obj(self):
f = open(self.filename, "r", encoding="utf8", errors='ignore')
for line in f:
if line.startswith(('v', 'vt', 'vn')):
getattr(self, line.split()[0]).append([float(s) for s in re.findall(r"-?\d+(?:\.\d+)?", line)])
if line.startswith("f"):
self.f.append(line[1:])
f.close()
if self.vt == [] and self.vn == []: self.filetype = "0"
if self.vt != [] and self.vn == []: self.filetype = "T"
if self.vt == [] and self.vn != []: self.filetype = "N"
if self.vt != [] and self.vn != []: self.filetype = "TN"
for face in self.f:
if self.filetype == "T":
self.vt_face_idx.append([int(s[1:]) - 1 for s in re.findall(r"/\d+", face)])
if self.filetype == "N":
self.vn_face_idx.append([int(s[1:]) - 1 for s in re.findall(r"/\d+", face)])
if self.filetype == "TN":
self.vt_face_idx.append([int(s[1:-1]) - 1 for s in re.findall(r"/\d+/", face)])
self.vn_face_idx.append([int(s.split('/')[-1]) - 1 for s in re.findall(r"/\d+/\d+", face)])
if not self.f:
raise ValueError(
"Faces in the input file do not have any of the specified "
"forms")
self.f = [[int(s[1:]) - 1 for s in re.findall(r" \d+", face)] for face in self.f]
def _set_vt_and_vn(self):
if self.filetype == "T":
self.halfedgeds.edges.add_attribute("vt")
self.halfedgeds.edges.add_attribute("vt_index")
if self.filetype == "N":
self.halfedgeds.edges.add_attribute("vn")
self.halfedgeds.edges.add_attribute("vn_index")
if self.filetype == "TN":
self.halfedgeds.edges.add_attribute("vt")
self.halfedgeds.edges.add_attribute("vn")
self.halfedgeds.edges.add_attribute("vt_index")
self.halfedgeds.edges.add_attribute("vn_index")
for face in self.halfedgeds.faces:
edge = face.edge
selfface = self.f[face.ifs_face_index]
while edge.head.ifs_index != selfface[0]:
edge = edge.nex
for i, edg in enumerate(ddg.datastructures.halfedge.get.get_edge_loop(edge)):
j = i
if edge.nex.head.ifs_index != selfface[
1]: # edgeloop is reverse orientation to the given
# vertex order
j = -i
if self.filetype == "T":
edg.vt_index = self.vt_face_idx[face.ifs_face_index][j]
edg.vt = self.vt[edg.vt_index]
if self.filetype == "N":
edg.vn_index = self.vn_face_idx[face.ifs_face_index][j]
edg.vn = self.vn[edg.vn_index]
if self.filetype == "TN":
edg.vt_index = self.vt_face_idx[face.ifs_face_index][j]
edg.vt = self.vt[edg.vt_index]
edg.vn_index = self.vn_face_idx[face.ifs_face_index][j]
edg.vn = self.vn[edg.vn_index]
[docs] def initialize_indexedfs(self):
"""
Creates an IndexedFaceSet as self.indexedfs of the given obj file.
"""
faceSet = ddg.indexedfaceset.ifs.IndexedFaceSet(self.f)
self.indexedfs = faceSet
[docs] def initialize_halfedgeds(self):
"""
Creates a surface as self.halfedgeds of the given obj file.
Sets attributes "vn" and "vt" to edges pointing to the corresponding
vertex if given.
"""
if not self.indexedfs:
self.initialize_indexedfs()
hds = ddg.indexedfaceset.utils.indexed_face_set_to_surface(self.indexedfs)
hds.verts.add_attribute("co")
for vert in hds.verts:
vert.co = np.array(self.v[vert.ifs_index])
self.halfedgeds = hds
if self.filetype != "0":
self._set_vt_and_vn()
[docs]def obj_to_ifs(filename):
"""
Converts an obj file to an IndexedFaceSet.
Parameters
----------
filename: (path and) name of the obj file
Returns
-------
IndexedFaceSet specified by the given obj file
"""
encoder = ObjEncoder(filename)
encoder.initialize_indexedfs()
return encoder.indexedfs
[docs]def obj_to_surface(filename):
"""
| Converts an obj file to a Surface with halfedge attributes "vt" and "vn"
| (if given).
| Obj file with given v and faces of the form f 1 2 3
| or given v,vt and faces of the form f 1/1 2/1 3/1
| or given v,vn and faces of the form f 1//1 2//1 3//1
| or given v,vt,vn and faces of the form f 1/1/2 2/1/2 3/1/2.
Parameters
----------
filename: (path and) name of the obj file
Returns
-------
Surface specified by the given obj file
"""
encoder = ObjEncoder(filename)
encoder.initialize_halfedgeds()
return encoder.halfedgeds
[docs]class SurfaceToObjEncoder(object):
"""
Class to convert a ddg.halfedge.Surface or an IndexedFaceSet to an obj file.
self.hds encodes the given surface.
"""
def __init__(self, surface, filename='obj_file.obj', co_attr='co'):
self.hds = surface
self.filename = filename
self.filetype = None
self.co = co_attr
self.vt = {}
self.vn = {}
self._initialize_vt_and_vn()
def _initialize_vt_and_vn(self):
for e in self.hds.edges:
if hasattr(e, 'vt'):
self.vt[tuple(e.vt)] = -1
if hasattr(e, 'vn'):
self.vn[tuple(e.vn)] = -1
if self.vt == {} and self.vn == {}:
self.filetype = "0"
if self.vt != {} and self.vn == {}:
self.filetype = "T"
if self.vt == {} and self.vn != {}:
self.filetype = "N"
if self.vt != {} and self.vn != {}:
self.filetype = "TN"
self.vt = {vt: i for i, vt in enumerate(sorted(self.vt.keys()))}
self.vn = {vn: i for i, vn in enumerate(sorted(self.vn.keys()))}
[docs] def write_obj(self):
with open(self.filename, 'w+') as f:
f.write('Obj file created by halfedge data structure \n\n')
for v in self.hds.verts:
if not hasattr(v, self.co):
raise ValueError('Vertex has not the specified coordinate attribute')
f.write(
'v {0} {1} {2}\n'.format(getattr(v, self.co)[0], getattr(v, self.co)[1], getattr(v, self.co)[2]))
f.write('\n')
for attr in ['vt', 'vn']:
for coords in sorted(getattr(self, attr), key=getattr(self, attr).get):
line = ' '.join(str(x) for x in coords)
f.write('{0} {1}\n'.format(attr, line))
if getattr(self, attr) != {}: f.write('\n')
for face in self.hds.faces:
f.write('f')
for e in ddg.datastructures.halfedge.get.get_edge_loop(face.edge):
if self.filetype == '0':
f.write(' {0}'.format(e.pre.head.index + 1))
if self.filetype == 'T':
f.write(' {0}/{1}'.format(e.pre.head.index + 1, self.vt[tuple(e.pre.vt)] + 1))
if self.filetype == 'N':
f.write(' {0}//{1}'.format(e.pre.head.index + 1, self.vn[tuple(e.pre.vn)] + 1))
if self.filetype == 'TN':
f.write(' {0}/{1}/{2}'.format(e.pre.head.index + 1, self.vt[tuple(e.pre.vt)] + 1,
self.vn[tuple(e.pre.vn)] + 1))
f.write('\n')
[docs]def surface_to_obj(surface, filename='obj_file.obj', co_attr='co'):
"""
| Converts a ddg.halfedge.Surface to an obj file.
| If 'vn' and/or 'vt' are given as halfegde attributes, they will be
| written in the obj file format.
| The vertices need to have coordinates, by default as the attribute 'co'.
| Results in Obj file with given v and faces of the form f 1 2 3
| or given v,vt and faces of the form f 1/1 2/1 3/1
| or given v,vn and faces of the form f 1//1 2//1 3//1
| or given v,vt,vn and faces of the form f 1/1/2 2/1/2 3/1/2.
| Vertices will be ordered by indices and vt and vn coordinates in increasing order.
Parameters
----------
surface: ddg.halfedge.Surface
halfedge data structure to be converted
filename : str (default='obj_file.obj')
Name for the resulting obj file
a file path can be specified here
default is 'obj_file' which will be saved in the cwd
co_attr : str (default='co')
string of the vertx attribute that encodes the coordinates
Notes
-----
* Indexing in obj files starts at 1.
"""
encoder = SurfaceToObjEncoder(surface, filename, co_attr)
encoder.write_obj()