Source code for c4dtools.utils

# coding: utf-8
#
# Copyright (c) 2012-2013, Niklas Rosenstein
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
#
# 1. Redistributions of source code must retain the above copyright
#    notice, this list of conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the above copyright
#    notice, this list of conditions and the following disclaimer in
#    the documentation and/or other materials provided with the
#    distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
#
# The views and conclusions contained in the software and
# documentation are those of the authors and should not be interpreted
# as representing official policies,  either expressed or implied, of
# the FreeBSD Project.
r"""
c4dtools.utils
~~~~~~~~~~~~~~
"""

import os
import sys
import c4d
import time
import threading
import collections

# =============================================================================
#                                Path operations
# =============================================================================

[docs]def change_suffix(filename, new_suffix): r""" Replaces the suffix of the passed filename with *new_suffix*. """ index = filename.rfind('.') if index >= 0: filename = filename[:index] return '%s.%s' % (filename, new_suffix)
[docs]def file_changed(original, copy): r""" Returns True when the filename pointed by *original* was modified before the last time *copy* was modified, False otherwise. """ return os.path.getmtime(original) > os.path.getmtime(copy) # ============================================================================= # Vector operations # =============================================================================
[docs]def vmin(dest, test): r""" For each component of the vectors *dest* and *test*, this function writes the lower value of each pairs into the respective component of *dest*. """ if test.x < dest.x: dest.x = test.x if test.y < dest.y: dest.y = test.y if test.z < dest.z: dest.z = test.z
[docs]def vmax(dest, test): r""" For each component of the vectors *dest* and *test*, this function writes the upper value of each pairs into the respective component of *dest*. """ if test.x > dest.x: dest.x = test.x if test.y > dest.y: dest.y = test.y if test.z > dest.z: dest.z = test.z
[docs]def vbbmid(vectors): r""" Returns the mid-point of the bounding box spanned by the list of vectors. This is different to the arithmetic middle of the points. Returns: :class:`c4d.Vector` """ if not vectors: return c4d.Vector(0) min = c4d.Vector(vectors[0]) max = c4d.Vector(min) for v in vectors: vmin(min, v) vmax(max, v) return (min + max) * 0.5 # ============================================================================= # Several utilities # =============================================================================
[docs]def clsname(obj): r""" Return the name of the class of the passed object. This is a shortcut for ``obj.__class__.__name__``. """ return obj.__class__.__name__
[docs]def candidates(value, obj, callback=lambda vref, vcmp, kcmp: vref == vcmp): r""" Searches for *value* in *obj* and returns a list of all keys where the callback returns True, being passed *value* as first argument, the value to compare it with as the second argument and the name of the attribute as the third. Returns: list of str """ results = [] for k, v in vars(obj).iteritems(): if callback(value, v, k): results.append(k) return results
[docs]def ensure_type(x, *types, **kwargs): r""" New in 1.2.5. This function is similar to the built-in :func:`isinstance` function in Python. It accepts an instance of a class as first argument, namely *x*, and checks if it is an instance of one of the passed types. The types must not be encapsulated in a tuple (which is in contrast to the :func:`isinstance` method). This function raises a :class:`TypeError` exception with a proper error message when *x* is not an instance of the passed *\*types*. *Changed in 1.2.8*: Renamed from *assert_type* to *ensure_type*. Added *\*\*kwargs* parameter. Pass ``name`` as keyword-argument for adding the parameter name that was wrong in the message. """ name = kwargs.pop('name', None) for k, v in kwargs: raise TypeError("unexpected keyword argument '%r'" % k) if not types: pass elif not isinstance(x, types): names = [] for t in types: names.append(t.__module__ + '.' + t.__name__) if len(names) > 1: message = 'xpected instance of %s, got %s' first = ', '.join(names[:-1]) + ' or ' + names[-1] else: message = 'xpected instance of type %s, got %s' first = names[0] if name: message = 'Invalid type for parameter %r, e' % name + message else: message = 'E' + message cls = x.__class__ message = message % (first, cls.__module__ + '.' + cls.__name__) raise TypeError(message)
assert_type = ensure_type # Backwards compatibility for < 1.2.8
[docs]def ensure_value(x, *values, **kwargs): r""" New in 1.2.8. This function checks if the value *x* is in *\*values*. If this does not result in True, :class:`ValueError` is raised. Pass ``name`` as keyword-argument to specify the parameter name that was given wrong in the message. """ name = kwargs.pop('name', None) for k in kwargs: raise TypeError("unexpected keyword argument %r" % k) if not values: pass if x not in values: if len(values) > 1: message = 'Possible values are %r' % list(values) else: message = 'Expected value is %r' % values[0] if name: message = 'Invalid value for parameter %r, ' + message message = message % name raise ValueError(message)
[docs]def get_root_module(modname, suffixes='pyc pyo py'.split()): r""" New in 1.2.6. Returns the root-file or folder of a module filename. The return-value is a tuple of ``(root_path, is_file)``. """ dirname, basename = os.path.split(modname) # Check if the module-filename is part of a Python package. in_package = False for sufx in suffixes: init_mod = os.path.join(dirname, '__init__.%s' % sufx) if os.path.exists(init_mod): in_package = True break # Go on recursively if the module is in a package or return the # module path and if it is a file. if in_package: return get_root_module(dirname) else: return os.path.normpath(modname), os.path.isfile(modname) # ============================================================================= # Decorators # =============================================================================
[docs]def func_attr(**attrs): r""" New in 1.2.6. This decorator must be called, passing attributes to be stored in the decorated function. """ def wrapper(func): for k, v in attrs.iteritems(): setattr(func, k, v) return func return wrapper # ============================================================================= # Cinema 4D related stuff, making common things easy # =============================================================================
[docs]def flush_console(id=13957): r""" Flushes the Cinema 4D console. """ c4d.CallCommand(id)
[docs]def update_editor(): r""" A shortcut for .. code-block:: python c4d.DrawViews( c4d.DRAWFLAGS_ONLY_ACTIVE_VIEW | c4d.DRAWFLAGS_NO_THREAD | c4d.DRAWFLAGS_NO_REDUCTION | c4d.DRAWFLAGS_STATICBREAK) Can be used to update the editor, useful for going through the frames of a document and doing backing or similar stuff. :Returns: The return-value of :func:`c4d.DrawViews`. """ return c4d.DrawViews( c4d.DRAWFLAGS_ONLY_ACTIVE_VIEW | c4d.DRAWFLAGS_NO_THREAD | c4d.DRAWFLAGS_NO_REDUCTION | c4d.DRAWFLAGS_STATICBREAK)
[docs]def iter_container(container, callback=None, level=0): r""" Iterate over the passed *container* being a :class:`c4d.BaseContainer` instance recursively. The *callback* will be called for each key-value pair. The default callback will print out the values in the container. The *callback* is passed the containers key, value and stack-level. :Returns: None :Raises: TypeError when *callback* is not callable. """ if not callback: def callback(key, value, level): sys.stdout.write(level * ' ') print key, if isinstance(value, c4d.BaseContainer): print "Container:" else: print ":", value if not isinstance(callback, collections.Callable): raise TypeError('expected callback to be a callable instance.') for key, value in container: callback(key, value, level) if isinstance(value, c4d.BaseContainer): iter_container(value, callback, level + 1)
[docs]def current_state_to_object(op, container=c4d.BaseContainer()): r""" Makes the passed `c4d.BaseObject` instance an editable object by applieng a modeling command on it. The returned object's hierarchy will consist of Null-Objects and editable objects only. *container* is an argument assigned a new c4d.BaseContainer to optimize speed and memory usage, but you can pass your own c4d.BaseContainer in case you want to pass any additional information to the c4d.utils.SendModelingCommand function. Example code for the Script Manager: .. code-block:: python import c4dtools def main(): obj = c4dtools.utils.current_state_to_object(op) doc.InsertObject(obj) c4d.EventAdd() main() Raises: TypeError if *op* is not a c4d.BaseObject instance. Returns: c4d.BaseObject """ if not isinstance(op, c4d.BaseObject): raise TypeError('expected c4d.BaseObject, got %s.' % op.__class__.__name__) doc = op.GetDocument() if not doc: csto_doc = c4d.documents.BaseDocument() csto_doc.InsertObject(op) else: csto_doc = doc result = c4d.utils.SendModelingCommand( c4d.MCOMMAND_CURRENTSTATETOOBJECT, [op], c4d.MODELINGCOMMANDMODE_ALL, container, csto_doc)[0] if not doc: op.Remove() return result
[docs]def join_polygon_objects(objects, dest_mat=None): r""" New in 1.2.8. This function creates one polygon object from the passed list *objects* containing :class:`c4d.PolygonObject` instances. Any other type of object is ignored. The returned polygon-object is located at the global world center. # TODO: Add description for parameters. """ # Filter all polygon-objects from the passed sequence. objects = filter(lambda x: x.CheckType(c4d.Opolygon), objects) if not objects: return None if dest_mat is None: dest_mat = objects[0].GetMg() # Merge points and polygons into single lists and collect # all tags. points = [] polys = [] tags = [] for obj in objects: mg = obj.GetMg() * ~dest_mat point_offset = len(points) for poly in obj.GetAllPolygons(): poly.a += point_offset poly.b += point_offset poly.c += point_offset poly.d += point_offset polys.append(poly) for point in obj.GetAllPoints(): point = point * mg points.append(point) tags.extend(obj.GetTags()) # No points, no luck. if not points: return None # Create a polygon-object from the points and polygons. object = c4d.PolygonObject(len(points), len(polys)) object.SetAllPoints(points) for i, poly in enumerate(polys): object.SetPolygon(i, poly) # Tell the object about the change. object.Message(c4d.MSG_UPDATE) # Create a list of unique tags for the object. new_tags = [] for tag in tags: # Skip variable tags as they do not match the new # number of datasets anymore. if tag.CheckType(c4d.Tvariable): continue # Check if such a tag already exists. exists = False for ex_tag in new_tags: if ex_tag.CheckType(tag.GetType()): exists = ex_tag.GetDataInstance() == tag.GetDataInstance() if exists: break if not exists: new_tags.append(tag.GetClone(c4d.COPYFLAGS_0)) for tag in new_tags: object.InsertTag(tag) object.SetMg(dest_mat) return object
[docs]def serial_info(): r""" New in 1.2.7. Returns serial-information of the user. Returns ``(sinfo, is_multi)``. *is_multi* indicates whether the *sinfo* is a multilicense information or not. """ is_multi = True sinfo = c4d.GeGetSerialInfo(c4d.SERIALINFO_MULTILICENSE) if not sinfo['nr']: is_multi = False sinfo = c4d.GeGetSerialInfo(c4d.SERIALINFO_CINEMA4D) return sinfo, is_multi
[docs]def get_shader_bitmap(shader, irs=None): r""" A bitmap can be retrieved from a :class:`c4d.BaseShader` instance of type ``Xbitmap`` using its :meth:`~c4d.BaseShader.GetBitmap` method. This method must however be wrapped in calls to :func:`~c4d.BaseShader.InitRender` and :func:`~c4d.BaseShader.FreeRender`. This function initializes rendering of the passed *shader*, retrieves the bitmap and frees it. :Return: :class:`c4d.BaseBitmap` or ``None``. """ if not irs: irs = render.InitRenderStruct() if shader.InitRender(irs) != c4d.INITRENDERRESULT_OK: return None bitmap = shader.GetBitmap() shader.FreeRender() return bitmap
[docs]def get_material_objects(doc): r""" New in 1.2.6. This function goes through the complete object hierarchy of the passed :class:`c4d.BaseDocument` and all materials with the objects that carry a texture-tag with that material. The returnvalue is an :class:`AtomDict` instance. The keys of the dictionary-like object are the materials in the document, their associated values are lists of :class:`c4d.BaseObject`. Note that an object *can* occure twice in the same list when the object has two tags with the same material on it. :param doc: :class:`c4d.BaseDocument` :return: :class:`AtomDict` """ data = AtomDict() def callback(op): for tag in op.GetTags(): if tag.CheckType(c4d.Ttexture): mat = tag[c4d.TEXTURETAG_MATERIAL] if not mat: continue data.setdefault(mat, []).append(op) for child in op.GetChildren(): callback(child) for obj in doc.GetObjects(): callback(obj) return data
[docs]def bl_iterator(obj, safe=False): r""" New in 1.2.8. Yields the passed object and all following objects in the hierarchy (retrieved via :func:`~c4d.BaseList2D.GetNext`). When the *safe* parameter is True, the next object will be retrieved before yielding to allow the yielded object to be moved in the hierarchy and iteration continues as if the object was not moved in hierarchy. """ if safe: while obj: next = obj.GetNext() yield obj obj = next else: while obj: yield obj obj = obj.GetNext() # ============================================================================= # Utility classes # =============================================================================
[docs]class AtomDict(object): r""" New in 1.2.6. This class implements a subset of the dictionary interface but without the requirement of the :func:`__hash__` method to be implemented. It is using comparing objects directly with the ``==`` operator instead. """ def __init__(self): super(AtomDict, self).__init__() self.__data = [] def __getitem__(self, x): self.__get_item(x)[1] def __setitem__(self, x, v): try: self.__get_item(x)[1] = v except KeyError: self.__data.append([x, v]) def __iter__(self): return self.iterkeys() def __repr__(self): content = ', '.join('%r: %r' % (k, v) for (k, v) in self.__data) return 'AtomDict({%s})' % content def __contains__(self, key): try: self.__get_item(key) return True except KeyError: return False def __get_item(self, x): for item in self.__data: if item[0] == x: return item raise KeyError(x) def setdefault(self, x, v): try: item = self.__get_item(x) except KeyError: item = [x, v] self.__data.append(item) return item[1] def keys(self): return list(self.iterkeys()) def values(self): return list(self.itervalues()) def items(self): return list(self.iteritems()) def iterkeys(self): for key, value in self.__data: yield key def itervalues(self): for key, value in self.__data: yield value def iteritems(self): for key, value in self.__data: yield (key, value) def get(self, key, default=None): try: return self.__get_item(key)[1] except KeyError: return default def set(self, key, value): self.__setitem__(key, value) def pop(self, key, *args): try: item = self.__get_item(key) self.__data.remove(item) return item[1] except KeyError: if not args: raise else: return args[0]
[docs]class Importer(object): r""" Use this class to enable importing modules from specific directories independent from ``sys.path``. .. attribute:: high_priority When this value is True, the paths defined in the importer are prepended to the original paths in ``sys.path``. If False, they will be appended. Does only have an effect when :attr:`use_sys_path` is True. .. attribute:: use_sys_path When this value is ``True``, the original paths from ``sys.path`` are used additionally to the paths defined in the imported. """ def __init__(self, high_priority=False, use_sys_path=True): super(Importer, self).__init__() self.path = [] self.use_sys_path = use_sys_path self.high_priority = high_priority
[docs] def add(self, *paths): r""" Add the passed strings to the search-path for importing modules. Raises TypeError if non-string object was passed. Passed paths are automatically expanded. """ new_paths = [] for path in paths: if not isinstance(path, basestring): raise TypeError('passed argument must be string.') path = os.path.expanduser(path) new_paths.append(os.path.normpath(path)) self.path.extend(new_paths)
[docs] def is_local(self, module): r""" Returns True if the passed module object can be found in the paths defined in the importer, False if not. """ if not hasattr(module, '__file__'): return False modpath = os.path.dirname(get_root_module(module.__file__)[0]) return modpath in self.path
[docs] def import_(self, name): r""" Import the module with the given name from the directories added to the Importer. The loaded module will not be inserted into `sys.modules`. """ prev_path = sys.path if self.use_sys_path: if self.high_priority: sys.path = self.path + sys.path else: sys.path = sys.path + self.path prev_modules = sys.modules.copy() # Remove any existing modules with the passed name from # sys.modules. for k in sys.modules.keys(): if k == name or k.startswith('%s.' % name): del sys.modules[k] try: m = __import__(name) for n in name.split('.')[1:]: m = getattr(m, n) return m except: raise finally: sys.path = prev_path # Restore the old module configuration. Only modules that have # not been in sys.path before will be removed. for k, v in sys.modules.items(): if k not in prev_modules and self.is_local(v) or not v: del sys.modules[k] else: sys.modules[k] = v
[docs]class Watch(object): r""" Utility class for measuring execution-time of code-blocks implementing the with-interface. >>> with Watch() as w: ... time_intensive_code() ... >>> w.delta 4.32521002 """ def __init__(self): super(Watch, self).__init__() self.reset() def __enter__(self): self.start() return self def __exit__(self, type, value, traceback): self.stop() def _watch_not_started(self, action): raise RuntimeError('Watch must be started before action "%s".' % action)
[docs] def start(self): r""" Start the time measuring. """ self._start = time.time() self._stop = -1
[docs] def stop(self): r""" Stop the time measuring. :Raises: :class:`RuntimeError` when :func:`Watch.start` was not called before. """ if self._start < 0: self._watch_not_started('stop') self._stop = time.time()
[docs] def reset(self): r""" Reset the information and return the Watch into a state where time measuring has not been started yet. """ self._start = -1 self._stop = -1
@property
[docs] def delta(self): r""" Returns the difference between the time Watch.start() has been and Watch.stop() has been called. If Watch.stop() has not yet been invoked, the current time is used. Raises: RuntimeError when Watch.start() has not been called before. """ if self._start < 0: self._watch_not_started('delta') if self._stop < 0: stop = time.time() else: stop = self._stop return stop - self._start
@property
[docs] def started(self): r""" Returns True when the Watch has been started. """ return self._start >= 0
@property
[docs] def stopped(self): r""" Returns True when the Watch has been stopped. """ return self._stop >= 0
[docs]class FileChangedChecker(object): r""" This class keeps track of a file on the local filesystem and tell you if the specific file has changed since the last time the FileChangedChecker was asked this question. It can run in two modes: pushing or pulling. With pulling, we mean to request the information whether the file has changed or not. With pushing, we mean to get notified when the file has changed. Pushing comes in connection with threading. Only one thread can be run from on FileChangedChecker instance. Example for pulling: .. code-block:: python f = FileChangedChecker(my_filename) # ... if f.has_changed(): print ("File %s has changed since `f` has been created." % f.filename) Example for pushing: .. code-block:: python f = FileChangedChecker(my_filename) def worker(f): print ("File %s has changed since `f` has been created." % f.filename) f.run_in_background(worker) """
[docs] class Worker(threading.Thread): r""" This is the worker class instantiated by the FileChangedChecker to run in the background. """ def __init__(self, checker, callback, period): super(FileChangedChecker.Worker, self).__init__() self.checker = checker self.callback = callback self.period = period self.running = False self.terminated = True # threading.Thread def run(self): self.running = True self.terminated = False while self.running: if self.checker.has_changed(): self.callback(self.checker) time.sleep(self.period) self.terminated = True self.checker.worker_ended(self)
def __init__(self, filename): r""" Initialize the instance with a valid filename. Raises: OSError if the passed filename does not point to a valid file on the local filesystem. """ super(FileChangedChecker, self).__init__() self.filename = filename self.last_time = os.path.getmtime(self.filename) self.active_worker = None self.disposing_workers = [] def __del__(self): if self.active_worker: self.stop_background() @property def filename(self): return self._filename @filename.setter def filename(self, filename): if not os.path.isfile(filename): msg = 'the passed filename does not point to an existing file.' raise OSError(msg) self._filename = filename
[docs] def has_changed(self): r""" This method returns True when file pointed to by the stored filename has changed since the last time the FileChangedChecker was asked for. Note: When the file has not changed since the initialization of the FileChangedChecker instance, this method will return False (i.e. the first call to this function will tell you the file has *not* changed). """ new_time = os.path.getmtime(self.filename) if new_time > self.last_time: self.last_time = new_time return True return False
[docs] def run_in_background(self, callback, period=0.5): r""" Start a new thread invoking the callable *callback* passing the FileChangedChecker instance when the file it points to has changed. *period* defines the time in seconds that passes until the next time the thread checks for the change. Raises: RuntimeError when a thread is still running for this instance. """ if self.active_worker: raise RuntimeError('cannot start multiple threads for ' 'FileChangedChecker instance.') worker = FileChangedChecker.Worker(self, callback, period) worker.start() self.active_worker = worker
[docs] def stop_background(self, join=False): r""" Stops the background operation started with ``run_in_background()``. When *join* evaluates to True, this method will wait until the thread has stopped working before terminating. Raises: RuntimeError when no thread has been started yet. """ if not self.active_worker: raise RuntimeError('no running thread to stop.') self.disposing_workers.append(self.active_worker) self.active_worker.running = False self.active_worker = None
[docs] def worker_ended(self, worker): r""" Not public. This method is called by a FileChangedChecker.Worker instance to dispose the worker from the list of not yet disposed workers. Raises: RuntimeError if *worker* is not in the list of not yet disposed workers. TypeError if *worker* is not an instance of FileChangedChecker.Worker. """ if not isinstance(worker, FileChangedChecker.Worker): raise TypeError('expected FileChangedChecker.Worker instance.') if not worker in self.disposing_workers: raise RuntimeError('worker not found, could not be removed.') self.disposing_workers.remove(worker)
[docs]class Filename(object): r""" A wrapper class for the os.path module. """ def __init__(self, filename): super(Filename, self).__init__() self.filename = os.path.normpath(filename) def __repr__(self): return 'Filename("%s")' % self.filename def __str__(self): return self.filename def __add__(self, other): if isinstance(other, Filename): other = other.filename return self.join(self.filename, other) def __radd__(self, other): if isinstance(other, Filename): other = other.filename return self.new_instance(other).join(self.filename) def __eq__(self, other): if isinstance(other, Filename): other = other.filename return os.path.samefile(self.filename, other) def new_instance(self, filename): return Filename(filename) def join(self, *parts): return self.new_instance(os.path.join(self.filename, *parts)) def dirname(self): return self.new_instance(os.path.dirname(self.filename)) def split(self): a, b = os.path.split(self.filename) return (self.new_instance(a), self.new_instance(b)) def splitdrive(self): a, b = os.path.splitdrive(self.filename) return (self.new_instance(a), self.new_instance(b)) def basename(self): return self.new_instance(os.path.basename(self.filename)) def exists(self): return os.path.exists(self.filename) def lexists(self): return os.path.lexists(self.filename) def isfile(self): return os.path.isfile(self.filename) def isdir(self): return os.path.isdir(self.filename) def isabs(self): return os.path.isabs(self.filename) def islink(self): return os.path.islink(self.filename) def ismount(self): return os.path.ismount(self.filename)
[docs] def suffix(self, new_suffix=None): r""" Called with no arguments, this method returns the suffix of the filename. When passing a string, the suffix will be exchanged to the passed suffix. Raises: TypeError when *new_suffix* is not None and not a string. """ if new_suffix: if not isinstance(new_suffix, basestring): raise TypeError('expected str, got %s.' % clsname(new_suffix)) self.filename = change_suffix(self.filename, new_suffix) else: index = self.filename.rfind('.') if index < 0: return '' else: return self.filename[index + 1:]
def getatime(self): return os.path.getatime(self.filename) def getmtime(self): return os.path.getmtime(self.filename) def getctime(self): return os.path.getctime(self.filename) def getsize(self): return os.path.getsize(self.filename)
[docs] def iglob(self, *glob_exts): r""" Returns a generator yielding all filenames that were found by globbing the filename joined with the passed *\*glob_exts*. """ for ext in glob_exts: for filename in glob.iglob(os.path.join(self.filename, ext)): yield filename
[docs]class PolygonObjectInfo(object): r""" New in 1.2.5. This class stores the points and polygons of a polygon-object and additionally computes it's normals and polygon-midpoints. """ def __init__(self): super(PolygonObjectInfo, self).__init__() self.points = [] self.polygons = [] self.normals = [] self.midpoints = [] self.pointcount = 0 self.polycount = 0
[docs] def init(self, op): r""" Initialize the instance. *op* must be a :class:`c4d.PolygonObject` instance. """ ensure_type(op, c4d.PolygonObject) points = op.GetAllPoints() polygons = op.GetAllPolygons() normals = [] midpoints = [] for p in polygons: a, b, c, d = points[p.a], points[p.b], points[p.c], points[p.d] # Compute the polygon's normal vector. normal = (a - b).Cross(a - d) normal.Normalize() # Compute the mid-point of the polygon. midpoint = a + b + c if p.c == p.d: midpoint *= 1.0 / 3 else: midpoint += d midpoint *= 1.0 / 4 normals.append(normal) midpoints.append(midpoint) self.points = points self.polygons = polygons self.normals = normals self.midpoints = midpoints self.pointcount = len(points) self.polycount = len(polygons)