1#
2# Copyright 2017 Pixar
3#
4# Licensed under the Apache License, Version 2.0 (the "Apache License")
5# with the following modification; you may not use this file except in
6# compliance with the Apache License and the following modification to it:
7# Section 6. Trademarks. is deleted and replaced with:
8#
9# 6. Trademarks. This License does not grant permission to use the trade
10#    names, trademarks, service marks, or product names of the Licensor
11#    and its affiliates, except as required to comply with Section 4(c) of
12#    the License and to reproduce the content of the NOTICE file.
13#
14# You may obtain a copy of the Apache License at
15#
16#     http://www.apache.org/licenses/LICENSE-2.0
17#
18# Unless required by applicable law or agreed to in writing, software
19# distributed under the Apache License with the above modification is
20# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
21# KIND, either express or implied. See the Apache License for the specific
22# language governing permissions and limitations under the Apache License.
23#
24
25from pxr import Usd, UsdGeom, UsdShade
26from .qt import QtCore
27from .common import Timer, IncludedPurposes
28from pxr.UsdUtils.constantsGroup import ConstantsGroup
29
30class ChangeNotice(ConstantsGroup):
31    NONE = 0
32    RESYNC = 1
33    INFOCHANGES = 2
34
35class RootDataModel(QtCore.QObject):
36    """Data model providing centralized, moderated access to fundamental
37    information used throughout Usdview controllers, data models, and plugins.
38    """
39
40    # Emitted when a new stage is set.
41    signalStageReplaced = QtCore.Signal()
42    signalPrimsChanged = QtCore.Signal(ChangeNotice, ChangeNotice)
43
44    def __init__(self, printTiming=False):
45
46        QtCore.QObject.__init__(self)
47
48        self._stage = None
49        self._printTiming = printTiming
50
51        self._currentFrame = Usd.TimeCode.Default()
52        self._playing = False
53
54        self._bboxCache = UsdGeom.BBoxCache(self._currentFrame,
55            [IncludedPurposes.DEFAULT, IncludedPurposes.PROXY], True)
56        self._xformCache = UsdGeom.XformCache(self._currentFrame)
57
58        self._pcListener = None
59
60    @property
61    def stage(self):
62        """Get the current Usd.Stage object."""
63
64        return self._stage
65
66    @stage.setter
67    def stage(self, value):
68        """Sets the current Usd.Stage object, and emits a signal if it is
69        different from the previous stage.
70        """
71
72        validStage = (value is None) or isinstance(value, Usd.Stage)
73        if not validStage:
74            raise ValueError("Expected USD Stage, got: {}".format(repr(value)))
75
76        if value is not self._stage:
77
78            if self._pcListener:
79                self._pcListener.Revoke()
80                self._pcListener = None
81
82            if value is None:
83                with Timer() as t:
84                    self._stage = None
85                if self._printTiming:
86                    t.PrintTime('close stage')
87            else:
88                self._stage = value
89
90            if self._stage:
91                from pxr import Tf
92                self._pcListener = \
93                    Tf.Notice.Register(Usd.Notice.ObjectsChanged,
94                                       self.__OnPrimsChanged, self._stage)
95
96            self.signalStageReplaced.emit()
97
98    def _emitPrimsChanged(self, primChange, propertyChange):
99        self.signalPrimsChanged.emit(primChange, propertyChange)
100
101    def __OnPrimsChanged(self, notice, sender):
102        primChange = ChangeNotice.NONE
103        propertyChange = ChangeNotice.NONE
104
105        for p in notice.GetResyncedPaths():
106            if p.IsAbsoluteRootOrPrimPath():
107                primChange = ChangeNotice.RESYNC
108            if p.IsPropertyPath():
109                propertyChange = ChangeNotice.RESYNC
110
111        if primChange == ChangeNotice.NONE or propertyChange == ChangeNotice.NONE:
112            for p in notice.GetChangedInfoOnlyPaths():
113                if p.IsPrimPath() and primChange == ChangeNotice.NONE:
114                    primChange = ChangeNotice.INFOCHANGES
115                if p.IsPropertyPath() and propertyChange == ChangeNotice.NONE:
116                    propertyChange = ChangeNotice.INFOCHANGES
117
118        self._emitPrimsChanged(primChange, propertyChange)
119
120    @property
121    def currentFrame(self):
122        """Get a Usd.TimeCode object which represents the current frame being
123        considered in Usdview."""
124
125        return self._currentFrame
126
127    @currentFrame.setter
128    def currentFrame(self, value):
129        """Set the current frame to a new Usd.TimeCode object."""
130
131        if not isinstance(value, Usd.TimeCode):
132            raise ValueError("Expected Usd.TimeCode, got: {}".format(value))
133
134        self._currentFrame = value
135        self._bboxCache.SetTime(self._currentFrame)
136        self._xformCache.SetTime(self._currentFrame)
137
138    @property
139    def playing(self):
140        return self._playing
141
142    @playing.setter
143    def playing(self, value):
144        self._playing = value
145
146    # XXX This method should be removed after bug 114225 is resolved. Changes to
147    #     the stage will then be used to trigger the caches to be cleared, so
148    #     RootDataModel clients will not even need to know the caches exist.
149    def _clearCaches(self):
150        """Clears internal caches of bounding box and transform data. Should be
151        called when the current stage is changed in a way which affects this
152        data."""
153
154        self._bboxCache.Clear()
155        self._xformCache.Clear()
156
157    @property
158    def useExtentsHint(self):
159        """Return True if bounding box calculations use extents hints from
160        prims.
161        """
162
163        return self._bboxCache.GetUseExtentsHint()
164
165    @useExtentsHint.setter
166    def useExtentsHint(self, value):
167        """Set whether whether bounding box calculations should use extents
168        from prims.
169        """
170
171        if not isinstance(value, bool):
172            raise ValueError("useExtentsHint must be of type bool.")
173
174        if value != self._bboxCache.GetUseExtentsHint():
175            # Unfortunate that we must blow the entire BBoxCache, but we have no
176            # other alternative, currently.
177            purposes = self._bboxCache.GetIncludedPurposes()
178            self._bboxCache = UsdGeom.BBoxCache(
179                self._currentFrame, purposes, value)
180
181    @property
182    def includedPurposes(self):
183        """Get the set of included purposes used for bounding box calculations.
184        """
185
186        return set(self._bboxCache.GetIncludedPurposes())
187
188    @includedPurposes.setter
189    def includedPurposes(self, value):
190        """Set a new set of included purposes for bounding box calculations."""
191
192        if not isinstance(value, set):
193            raise ValueError(
194                "Expected set of included purposes, got: {}".format(
195                    repr(value)))
196        for purpose in value:
197            if purpose not in IncludedPurposes:
198                raise ValueError("Unknown included purpose: {}".format(
199                    repr(purpose)))
200
201        self._bboxCache.SetIncludedPurposes(value)
202
203    def computeWorldBound(self, prim):
204        """Compute the world-space bounds of a prim."""
205
206        if not isinstance(prim, Usd.Prim):
207            raise ValueError("Expected Usd.Prim object, got: {}".format(
208                repr(prim)))
209
210        return self._bboxCache.ComputeWorldBound(prim)
211
212    def getLocalToWorldTransform(self, prim):
213        """Compute the transformation matrix of a prim."""
214
215        if not isinstance(prim, Usd.Prim):
216            raise ValueError("Expected Usd.Prim object, got: {}".format(
217                repr(prim)))
218
219        return self._xformCache.GetLocalToWorldTransform(prim)
220
221    def computeBoundMaterial(self, prim, purpose):
222        """Compute the material that the prim is bound to, for the given value
223           of material purpose.
224        """
225        if not isinstance(prim, Usd.Prim):
226            raise ValueError("Expected Usd.Prim object, got: {}".format(
227                repr(prim)))
228        # We don't use the binding cache yet since it isn't exposed to python.
229        return UsdShade.MaterialBindingAPI(
230                prim).ComputeBoundMaterial(purpose)
231