Source code for abcview.io

#! /usr/bin/env python
#-******************************************************************************
#
# Copyright (c) 2012-2013,
#  Sony Pictures Imageworks Inc. and
#  Industrial Light & Magic, a division of Lucasfilm Entertainment Company Ltd.
#
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are
# met:
# *       Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# *       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.
# *       Neither the name of Sony Pictures Imageworks, nor
# Industrial Light & Magic, nor the names of their contributors may be used
# to endorse or promote products derived from this software without specific
# prior written permission.
#
# 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.
#
#-******************************************************************************

import os
import sys
import time

import imath
import alembic
from abcview import config, log
from abcview.utils import get_object
from abcview.utils import json

__doc__ = """
The IO module handles serialization and deserialization of the assembled 
scenes and sessions. The hierarchy structure basically consists of a 
top-level Session object that contains children items which can be either
Scenes, Sessions, Cameras or ICAmeras. Each item in the hierarchy has
a properties attribute for storing custom attributes. Only Scenes and ICameras
reference Alembic archives. 

Session hierarchy: ::

   Session
       |- Properties
       |- Cameras
       `- Items
             `- Scene
                  |- Properties
                  `- file.abc

Sessions can also reference other session files.
"""

__all__ = ["Scene", "Session", "Camera", "ICamera",
           "AbcViewError", "Mode", ]

class Mode:
    OFF = 0
    BOUNDS = 1
    POINT = 2
    LINE = 3
    FILL = 4

class AbcViewError(Exception):
    pass

def DictListUpdate( lis1, lis2):
    for aLis1 in lis1:
        if aLis1 not in lis2:
            lis2.append(aLis1)
    return lis2

class Base(object):
    def __init__(self):
        self.loaded = True
        self.instance = 1
        self.properties = {}

    def __repr__(self):
        return "<%s \"%s\">" % (self.type(), self.name)

    def _get_name(self):
        raise NotImplementedError("implement in subclass")

    def _set_name(self, value):
        raise NotImplementedError("implement in subclass")

    name = property(_get_name, _set_name, doc="name")

    @classmethod
    def type(cls):
        return cls.__name__

    def serialize(self):
        raise NotImplementedError("must be implemented in a subclass")

    @classmethod
    def deserialize(self):
        raise NotImplementedError("must be implemented in a subclass")

class FileBase(Base):
    SERIALIZE = []
    EXT = None
    def __init__(self, filepath, parent=None):
        super(FileBase, self).__init__()
        self.__filepath = filepath
        self.__name = "Unnamed"
        self.parent = parent

    def is_archive(self):
        return self.fileext == Scene.EXT

    def _get_name(self):
        return self.__name

    def _set_name(self, value):
        self.__name = value

    name = property(_get_name, _set_name, doc="name")

    def _get_filepath(self):
        return self.__filepath

    def _set_filepath(self, value):
        if value is not None:
            self.__filepath = value
            self.__name = os.path.basename(value)

    filepath = property(_get_filepath, _set_filepath, doc="file path")

    def _get_filetype(self):
        return self.__class__.__name__

    def _set_filetype(self, value):
        raise NotImplementedError("File type is immutable")

    filetype = property(_get_filetype, _set_filetype, doc="file type")

    def _get_fileext(self):
        if self.filepath:
            return self.filepath.split(".")[-1]
        return None

    def _set_fileext(self, value):
        raise NotImplementedError("File ext is immutable")

    fileext = property(_get_fileext, _set_fileext, doc="file extension")

    def serialize(self):
        raise NotImplementedError

[docs]class Scene(FileBase): """ Represents a single item in an AbcView file (either .abc or .io file) """ EXT = "abc" def __init__(self, filepath=None): super(Scene, self).__init__(filepath) self.filepath = os.path.abspath(filepath) def __repr__(self): return "<%s \"%s\">" % (self.type(), self.name) ## some convenience properties def _get_translate(self): return self.properties.get("translate", (0, 0, 0)) def _set_translate(self, value): self.properties["translate"] = value translate = property(_get_translate, _set_translate, doc="translate property") def _get_rotate(self): return self.properties.get("rotate", (0, 0, 0, 0)) def _set_rotate(self, value): self.properties["rotate"] = value rotate = property(_get_rotate, _set_rotate, doc="rotation property") def _get_scale(self): return self.properties.get("scale", (1, 1, 1)) def _set_scale(self, value): self.properties["scale"] = value scale = property(_get_scale, _set_scale, doc="scale property") def _get_mode(self): return self.properties.get("mode", -1) def _set_mode(self, value): self.properties["mode"] = value mode = property(_get_mode, _set_mode, doc="GL polygon mode property") def _get_color(self): return self.properties.get("color", (0.5, 0.5, 0.5)) def _set_color(self, value): self.properties["color"] = value color = property(_get_color, _set_color, doc="color to display in viewer") def serialize(self): return { "filepath": self.filepath, "instance": self.instance, "loaded": self.loaded, "name": self.name, "properties": self.properties, } @classmethod
[docs] def deserialize(cls, data): """ Deserializes an Alembic scene from json data. """ item = cls(data.get("filepath")) item.name = data.get("name", "Unnamed") item.loaded = data.get("loaded", True) item.instance = data.get("instance", 1) item.properties = data.get("properties", {}) return item
class CameraBase(Base): """ Base class for camera objects. """ def __init__(self, name): self.__name = name # draw toggles self.__auto_frame = False self.__draw_bounds = False self.__draw_hud = False self.__draw_labels = False self.__draw_grid = True self.__draw_normals = False self.__draw_mode = Mode.LINE # fixed aspect ratio toggle self.__fixed = False # draw visible objects only self.__visible = True def _get_name(self): return self.__name def _set_name(self, value): self.__name = value name = property(_get_name, _set_name, doc="Camera name") def _get_draw_grid(self): return self.__draw_grid def _set_draw_grid(self, force=None): if force is not None: self.__draw_grid = force else: self.__draw_grid = not self.__draw_grid draw_grid = property(_get_draw_grid, _set_draw_grid, doc="draw the scene grid") def _get_draw_normals(self): return self.__draw_normals def _set_draw_normals(self, force=None): if force is not None: self.__draw_normals = force else: self.__draw_normals = not self.__draw_normals draw_normals = property(_get_draw_normals, _set_draw_normals, doc="draw normals") def _get_draw_bounds(self): return self.__draw_bounds def _set_draw_bounds(self, force=None): if force is not None: self.__draw_bounds = force else: self.__draw_bounds = not self.__draw_bounds draw_bounds = property(_get_draw_bounds, _set_draw_bounds, doc="draw bounding boxes") def _get_draw_labels(self): return self.__draw_labels def _set_draw_labels(self, force=None): if force is not None: self.__draw_labels = force else: self.__draw_labels = not self.__draw_labels draw_labels = property(_get_draw_labels, _set_draw_labels, doc="draw scene labels") def _get_draw_hud(self): return self.__draw_hud def _set_draw_hud(self, force=None): if force is not None: self.__draw_hud = force else: self.__draw_hud = not self.__draw_hud draw_hud = property(_get_draw_hud, _set_draw_hud, doc="draw hud info") def _get_auto_frame(self): return self.__auto_frame def _set_auto_frame(self, force=None): if force is not None: self.__auto_frame = force else: self.__auto_frame = not self._auto_frame auto_frame = property(_get_auto_frame, _set_auto_frame, doc="automatically frame scene or selected") def _get_draw_mode(self): return self.__draw_mode def _set_draw_mode(self, mode): self.__draw_mode = mode mode = property(_get_draw_mode, _set_draw_mode, doc="global drawing mode") def _get_fixed(self): return self.__fixed def _set_fixed(self, force=None): if force is not None: self.__fixed = force else: self.__fixed = not self.__fixed fixed = property(_get_fixed, _set_fixed, doc="draw fixed aspect ratio") def _get_visible(self): return self.__visible def _set_visible(self, force=None): if force is not None: self.__visible = force else: self.__visible = not self.__visible visible = property(_get_visible, _set_visible, doc="draw visible objects only")
[docs]class Camera(CameraBase): """ AbcView API Camera object. Camera attributes are not animatable as opposed to Alembic ICamera attributes. Acting de/serialization layer for GLCamera objects. """ SERIALIZE = ["translation", "rotation", "scale", "aspect_ratio", "fovx", "fovy", "near", "far", "center", "fixed", "mode", "draw_hud", "draw_grid", "draw_normals", "draw_bounds", "draw_labels", "name", "loaded", "visible"] def __init__(self, name, loaded=False): """ :param name: camera name """ super(Camera, self).__init__(name) self.loaded = loaded @classmethod def type(cls): return "Camera" def serialize(self): d = { "type": self.type(), } for attr in self.SERIALIZE: val = getattr(self, attr, None) if val is not None: if attr in ["translation", "rotation", "scale"]: d[attr] = [val[0], val[1], val[2]] else: d[attr] = val return d @classmethod def deserialize(cls, params): cam = cls(name=params.get("name"), loaded=params.get("loaded")) for attr in cls.SERIALIZE: val = params.get(attr) if attr in ["translation", "rotation", "scale"]: val = imath.V3d(*val) setattr(cam, attr, val) return cam
[docs]class ICamera(CameraBase): """ Alembic ICamera de/serialization wrapper class. Use this class for loading Alembic ICameras. Setting up a basic scene with a camera :: # imports >>> from abcview.io import Session, ICamera >>> from abcview.utils import get_object # create session, add scene file >>> session = Session() >>> session.add_file("scene.abc") # add icamera wrapped in ICamera IO class >>> session.add_camera(ICamera(get_object("shotcam.abc", "ShotCam") loaded=True)) # save session >>> session.save("scene.io") """ SERIALIZE = ["fixed", "mode", "draw_hud", "draw_grid", "draw_normals", "draw_labels", "name", "loaded", "draw_bounds", "visible"] def __init__(self, icamera, loaded=False): """ :param icamera: Alembic ICamera object """ self.icamera = icamera self.loaded = loaded self.__schema = None super(ICamera, self).__init__(self._get_name) def __repr__(self): return "<%s \"%s\">" % (self.__class__.__name__, self.name) @classmethod def type(cls): return "ICamera" def _not_settable(self, value): log.debug("ICamera name is immutable") def _get_name(self): return self.icamera.getName() name = property(_get_name, _not_settable, doc="Camera name") def schema(self): self.icamera = alembic.AbcGeom.ICamera( self.icamera.getParent(), self.name) if self.__schema is None and self.icamera: self.__schema = self.icamera.getSchema() return self.__schema def _ixform_sample(self, seconds): """ Returns icamera's parent xform sample :param seconds: time in secs (derives index) """ cp = self.icamera.getParent() xform = alembic.AbcGeom.IXform(cp.getParent(), cp.getName()) xs = xform.getSchema() ts = xs.getTimeSampling() index = ts.getNearIndex(seconds, xs.getNumSamples()) return xs.getValue(index) def _icamera_sample(self, seconds): """ Returns icamera's sample :param seconds: time in secs (derives index) """ ts = self.schema().getTimeSampling() index = ts.getNearIndex(seconds, self.schema().getNumSamples()) return self.schema().getValue(index) def translation(self, seconds=0): return self._ixform_sample(seconds).getTranslation() def rotation(self, seconds=0): samp = self._ixform_sample(seconds) return imath.V3d(samp.getXRotation(), samp.getYRotation(), samp.getZRotation()) def scale(self, seconds=0): return self._ixform_sample(seconds).getScale() def near(self, seconds=0): return self._icamera_sample(seconds).getNearClippingPlane() def far(self, seconds=0): return self._icamera_sample(seconds).getFarClippingPlane() def fovx(self, seconds=0): return self._icamera_sample(seconds).getFieldOfView()
[docs] def aspect_ratio(self, seconds=0): """ From Alembic/AbcGeom/CameraSample.h: The amount the camera's lens compresses the image horizontally (width / height aspect ratio) """ samp = self._icamera_sample(seconds) return (samp.getHorizontalAperture() / samp.getVerticalAperture()) \ * samp.getLensSqueezeRatio()
def screen_window(self, seconds=0): win = self._icamera_sample(seconds).getScreenWindow() return win["left"], win["bottom"], win["left"], win["right"] def serialize(self): d = { "filepath": self.icamera.getArchive().getName(), "fullname": self.icamera.getFullName(), "type": self.type(), } for attr in self.SERIALIZE: val = getattr(self, attr, None) if val is not None: d[attr] = val return d @classmethod def deserialize(cls, params): cam = cls(get_object( params.get("filepath"), params.get("fullname") ), loaded=params.get("loaded") ) for attr in cls.SERIALIZE: setattr(cam, attr, params.get(attr)) return cam
[docs]class Session(FileBase): """ AbcView API Session object. Top level container layer that holds properties and child session or scene objects. De/serialization layer for AbcView sessions. """ EXT = "io" def __init__(self, filepath=None): super(Session, self).__init__(filepath) self.clear() if filepath and os.path.isfile(filepath): self.load(filepath) def __contains__(self, item): if type(item) in [str, unicode]: item = Scene(item) return item.filepath in [i.filepath for i in self.__items] def _get_items(self): return self.__items def _set_items(self): raise NotImplementedError("Use add_item() or add_file()") items = property(_get_items, _set_items, doc="child items") def _get_cameras(self): return [camera for camera in self.__cameras.values()] def _set_cameras(self): raise NotImplementedError("Use add_camera()") cameras = property(_get_cameras, _set_cameras, doc="cameras")
[docs] def add_item(self, item): """ Adds and item to the session :param item: Scene or Session object """ log.debug("[%s.add_item] %s" % (self, item)) found_instances = [i.filepath for i in self.items if i.filepath == item.filepath] item.instance = len(found_instances) + 1 self.__items.append(item)
[docs] def remove_item(self, item): """ Removes an item from the session :param item: Scene or Session object """ log.debug("[%s.remove_item] %s" % (self, item)) if item in self.__items: self.__items.remove(item) else: log.debug("Item not in session: %s" % item)
[docs] def add_file(self, filepath): """ Adds a filepath to the session :param filepath: path to file """ log.debug("[%s.add_file] %s" % (self, filepath)) if filepath.endswith(self.EXT): item = Session(filepath) elif filepath.endswith(Scene.EXT): item = Scene(filepath) else: raise AbcViewError("Unsupported file type: %s" % filepath) self.add_item(item) return item
[docs] def add_camera(self, camera): """ :param: GLCamera """ log.debug("[%s.add_camera] %s" % (self, camera)) if camera.name not in self.__cameras: self.__cameras[camera.name] = camera
[docs] def remove_camera(self, camera): """ :param: GLCamera """ log.debug("[%s.remove_camera] %s" % (self, camera)) if camera.name in self.__cameras: del self.__cameras[camera.name]
[docs] def set_camera(self, camera): """ Sets the "active" camera for a given session. :param: GLCamera """ log.debug("[%s.set_camera] %s" % (self, camera)) if camera.name not in self.__cameras: self.__cameras[camera.name] = camera for name, cam in self.__cameras.items(): cam.loaded = False self.__cameras[camera.name].loaded = True
[docs] def serialize(self): """ Serializes the session object to a JSON dict. """ def _serialize(item): if item.type() == "Session": return { "filepath": item.filepath, "instance": item.instance, "name": item.name, "loaded": item.loaded, "properties": item.properties, } else: return item.serialize() return { "items": [_serialize(item) for item in self.items], }
[docs] def is_dirty(self): """ Change that requires saving. """ return self.__dirty
def make_dirty(self): self.__dirty = True def make_clean(self): self.__dirty = False def clear(self): self.version = config.__version__ self.program = config.__prog__ self.properties = {} self.date = time.time() self.min_time = 0 self.max_time = 0 self.current_time = 0 self.frames_per_second = 24.0 # stores objects that need special handling self.__cameras = {} self.__items = [] self.make_clean()
[docs] def merge(self, session): """ Merges a given session into this session. :param session: Session object to merge in """ self.min_time = session.min_time self.max_time = session.max_time self.current_time = session.current_time # merge items self.__items = DictListUpdate(self.items, session.items) # merge cameras self.__cameras.update(session.cameras) # merge properties self.properties.update(session.properties)
[docs] def walk(self): """ Recursive generator that yields Session, Scene and Camera objects. Adds a .session attribute to each item. :yield: Session, Scene or Camera objects """ for item in self.items + self.cameras: if item.type() == Session.type(): item.session = self yield item for child in item.walk(): child.session = item yield child else: item.session = self yield item
[docs] def load(self, filepath=None): """ Loads a session .io file. """ if filepath is None and self.filepath: filepath = self.filepath elif filepath: self.filepath = filepath else: raise AbcViewError("File path not set") # metadata and properties state = json.load(open(filepath, "r")) self.version = state.get("app").get("version") self.program = state.get("app").get("program") self.date = state.get("date") self.instance = state.get("instance", 1) self.properties = state.get("properties") self.frames_per_second = state.get("frames_per_second", self.frames_per_second) self.min_time = state.get("min_time", self.min_time) self.max_time = state.get("max_time", self.max_time) self.current_time = state.get("current_time", self.current_time) # cameras for camera in state.get("cameras"): if camera.get("type") == Camera.type(): self.add_camera(Camera.deserialize(camera)) elif camera.get("type") == ICamera.type(): self.add_camera(ICamera.deserialize(camera)) # items data = state.get("data") for d in data.get("items"): fp = str(d.get("filepath")) if fp.endswith(Scene.EXT): self.add_item(Scene.deserialize(d)) elif fp.endswith(Session.EXT): item = Session(fp) self.add_item(item)
[docs] def save(self, filepath=None): """ Saves a session to a .io file. """ if filepath is None and self.filepath: filepath = self.filepath if not filepath: raise AbcViewError("File path not set") elif not filepath.endswith(self.EXT): filepath += self.EXT self.filepath = filepath self.date = time.time() log.debug("[%s.save] %s" % (self, filepath)) state = { "app": { "program": self.program, "version": self.version, "module": os.path.dirname(__file__), }, "env": { "user": os.environ.get("USER", os.environ.get("USERNAME")), "host": os.environ.get("HOST", os.environ.get("HOSTNAME")), "platform": sys.platform, }, "date": self.date, "min_time": self.min_time, "max_time": self.max_time, "current_time": self.current_time, "frames_per_second": self.frames_per_second, "properties": self.properties, "cameras": [camera.serialize() for camera in self.__cameras.values()], "data": self.serialize() } json.dump(state, open(filepath, "w"), sort_keys=True, indent=4)