diff --git a/README.md b/README.md index 2baf315..425b984 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ Python-OpenCTM ============== [![Build Status](https://travis-ci.org/lejafar/Python-OpenCTM.svg?branch=master)](https://travis-ci.org/lejafar/Python-OpenCTM) -### Python Interface for the Open-CTM Library +### Python Interface for the Open-CTM File Format Python-OpenCTM is an (unofficial) Python interface for the [OpenCTM](https://github.com/Danny02/OpenCTM) file format. A format that allows a geometry to be compressed to a fraction of comparable file formats (3DS, STL, COLLADA...). @@ -16,15 +16,12 @@ pip install python-openctm ```python import openctm -with openctm.open('foo.ctm') as ctm_file: - vertices = ctm_file.get_vertices() - faces = ctm_file.get_faces() +# read +mesh = openctm.import_mesh('foo.ctm') - result = {'vertices': vertices, - 'faces': faces} +print(mesh.vertices.shape) +# (124, 3) - # get face normals if available - face_normals = ctm_file.get_face_normals() - if face_normals: - result['face_normals'] = face_normals +# write +openctm.export_mesh(mesh, 'bar.ctm') ``` diff --git a/openctm/__init__.py b/openctm/__init__.py index 4178383..622ecfe 100644 --- a/openctm/__init__.py +++ b/openctm/__init__.py @@ -1 +1 @@ -from .io import open, CtmFile +from .io import import_mesh, export_mesh diff --git a/openctm/io.py b/openctm/io.py index c99d05c..7742c82 100644 --- a/openctm/io.py +++ b/openctm/io.py @@ -1,48 +1,80 @@ from .openctm import * import numpy as np -from contextlib import contextmanager -@contextmanager -def open(filename): - ctm_context = ctmNewContext(CTM_IMPORT) - yield CtmFile(ctm_context, filename) - ctmFreeContext(ctm_context) -class CtmFile: +class CTM: + + def __init__(self, _vertices, _faces, _normals=None): + self.vertices = _vertices + self.faces = _faces + self.normals = _normals + + def __eq__(self, other): + return (self.vertices == other.vertices).all() and (self.faces == other.faces).all() + - def __init__(self, ctm_context, filename): - self.ctm = ctm_context - ctmLoad(self.ctm, str(filename).encode('utf-8')) - err = ctmGetError(self.ctm) +def import_mesh(_filename): + ctm_context = ctmNewContext(CTM_IMPORT) + try: + ctmLoad(ctm_context, str(_filename).encode('utf-8')) + err = ctmGetError(ctm_context) if err != CTM_NONE: - raise IOError("Error loading file: " + str(ctmErrorString(err))) + raise IOError("Error loading file: %s" % str(ctmErrorString(err))) + + # read vertices + vertex_count = ctmGetInteger(ctm_context, CTM_VERTEX_COUNT) + vertex_ctm = ctmGetFloatArray(ctm_context, CTM_VERTICES) - def get_vertices(self): - # get vertices - vertex_count = ctmGetInteger(self.ctm, CTM_VERTEX_COUNT) - vertex_ctm = ctmGetFloatArray(self.ctm, CTM_VERTICES) - # use fromiter to avoid loop vertices = np.fromiter(vertex_ctm, dtype=np.float, count=vertex_count * 3).reshape((-1, 3)) - return vertices - def get_faces(self): - # get faces - face_count = ctmGetInteger(self.ctm, CTM_TRIANGLE_COUNT) - face_ctm = ctmGetIntegerArray(self.ctm, CTM_INDICES) + # read faces + face_count = ctmGetInteger(ctm_context, CTM_TRIANGLE_COUNT) + face_ctm = ctmGetIntegerArray(ctm_context, CTM_INDICES) faces = np.fromiter(face_ctm, dtype=np.int, count=face_count * 3).reshape((-1, 3)) - return faces - def get_face_normals(self): - if ctmGetInteger(self.ctm, CTM_HAS_NORMALS) == CTM_TRUE: - normals_ctm = ctmGetFloatArray(self.ctm, CTM_NORMALS) + # read face normals + normals = None + if ctmGetInteger(ctm_context, CTM_HAS_NORMALS) == CTM_TRUE: + normals_ctm = ctmGetFloatArray(ctm_context, CTM_NORMALS) normals = np.fromiter(normals_ctm, dtype=np.float, count=face_count * 3).reshape((-1, 3)) - return normals - # if not available return None + finally: + ctmFreeContext(ctm_context) + + return CTM(vertices, faces, normals) + + +def export_mesh(_ctm, _filename): + ctm_context = ctmNewContext(CTM_EXPORT) + + try: + vertex_count = len(_ctm.vertices) + vertices = _ctm.vertices.reshape((-1, 1)) + p_vertices = ctypes.cast((CTMfloat * vertex_count * 3)(), ctypes.POINTER(CTMfloat)) + for i in range(vertex_count * 3): + p_vertices[i] = CTMfloat(vertices[i]) + + face_count = len(_ctm.faces) + faces = _ctm.faces.reshape((-1, 1)) + p_faces = ctypes.cast((CTMuint * face_count * 3)(), ctypes.POINTER(CTMuint)) + for i in range(face_count * 3): + p_faces[i] = CTMuint(faces[i]) + + if _ctm.normals: + normal_count = len(_ctm.normals) + normals = _ctm.normals.reshape((-1, 1)) + p_normals = ctypes.cast((CTMfloat * normal_count * 3)(), ctypes.POINTER(CTMfloat)) + for i in range(normal_count * 3): + p_normals[i] = CTMfloat(normals[i]) else: - return None + p_normals = None + + ctmDefineMesh(ctm_context, p_vertices, CTMuint(vertex_count), p_faces, CTMuint(face_count), p_normals) + ctmSave(ctm_context, ctypes.c_char_p(_filename.encode('utf-8'))) + finally: + ctmFreeContext(ctm_context) diff --git a/setup.py b/setup.py index 32de520..bf29e69 100644 --- a/setup.py +++ b/setup.py @@ -1,11 +1,10 @@ -from setuptools import setup, Distribution, Extension -import sys +from setuptools import setup, Extension setup( name='python-openctm', - version='1.0.2', - description='Python Interface for the OpenCTM Library', - long_description='Python Interface for the OpenCTM Library', + version='1.0.3', + description='Python Interface for the OpenCTM File Format', + long_description='Python Interface for the OpenCTM File Format', url='https://github.com/lejafar/python-openctm', author='Rafael Hautekiet', author_email='rafael.hautekiet@oqton.ai', @@ -24,6 +23,7 @@ "Programming Language :: Python :: 3.4", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", + "Programming Language :: Python :: 3.7", "Natural Language :: English", "Topic :: Scientific/Engineering", "License :: OSI Approved :: zlib/libpng License", diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test-data/vierkanten_test.ctm b/tests/test-data/squares.ctm similarity index 100% rename from tests/test-data/vierkanten_test.ctm rename to tests/test-data/squares.ctm diff --git a/tests/test-data/vierkanten_test_exported.ctm b/tests/test-data/vierkanten_test_exported.ctm new file mode 100644 index 0000000..892232d Binary files /dev/null and b/tests/test-data/vierkanten_test_exported.ctm differ diff --git a/tests/test_basic.py b/tests/test_basic.py new file mode 100644 index 0000000..02cdf6d --- /dev/null +++ b/tests/test_basic.py @@ -0,0 +1,31 @@ +import unittest +import os + +from openctm import import_mesh, export_mesh + + +class BasicTestSuiteFunctions(unittest.TestCase): + + def setUp(self): + self.file_dir = "tests/test-data/" + + def testImportVertices(self): + mesh = import_mesh("%s/squares.ctm" % self.file_dir) + assert len(mesh.vertices) == 124 + + def testImportFaces(self): + mesh = import_mesh("%s/squares.ctm" % self.file_dir) + assert len(mesh.faces) == 284 + + def testExportImport(self): + original_mesh = import_mesh("%s/squares.ctm" % self.file_dir) + export_mesh(original_mesh, "%s/squares_exported.ctm" % self.file_dir) + + exported_mesh = import_mesh("%s/squares_exported.ctm" % self.file_dir) + + assert original_mesh == exported_mesh + os.remove("%s/squares_exported.ctm" % self.file_dir) + + +if __name__ == '__main__': + unittest.main()