Add SkParticleValue to allow further customization of curve behavior

All curves (and path affectors) are driven by an SkParticleValue. The
value can derive its value from the current defaults (age of particle
or effect), or explicitly choose the other one, a random value, or any
other particle value. Values can be range adjusted and support repeat,
clamp, and mirror tiling.

Also fixed some more issues related to resource path in the slide GUI.

Bug: skia:
Change-Id: I4755018d5b57ae2d5ec400d541055ca4fb542978
Reviewed-on: https://skia-review.googlesource.com/c/skia/+/196760
Reviewed-by: Brian Osman <brianosman@google.com>
Commit-Queue: Brian Osman <brianosman@google.com>
diff --git a/modules/particles/src/SkCurve.cpp b/modules/particles/src/SkCurve.cpp
index 3dc7819..5dcfb60d 100644
--- a/modules/particles/src/SkCurve.cpp
+++ b/modules/particles/src/SkCurve.cpp
@@ -7,6 +7,7 @@
 
 #include "SkCurve.h"
 
+#include "SkParticleData.h"
 #include "SkRandom.h"
 #include "SkReflected.h"
 
@@ -78,9 +79,11 @@
     }
 }
 
-SkScalar SkCurve::eval(SkScalar x, SkRandom& random) const {
+SkScalar SkCurve::eval(const SkParticleUpdateParams& params, SkParticleState& ps) const {
     SkASSERT(fSegments.count() == fXValues.count() + 1);
 
+    float x = fInput.eval(params, ps);
+
     int i = 0;
     for (; i < fXValues.count(); ++i) {
         if (x <= fXValues[i]) {
@@ -98,12 +101,13 @@
 
     // Always pull t and negate here, so that the stable generator behaves consistently, even if
     // our segments use an inconsistent feature-set.
-    SkScalar t = random.nextF();
-    bool negate = random.nextBool();
+    SkScalar t = ps.fRandom.nextF();
+    bool negate = ps.fRandom.nextBool();
     return fSegments[i].eval(segmentX, t, negate);
 }
 
 void SkCurve::visitFields(SkFieldVisitor* v) {
+    v->visit("Input", fInput);
     v->visit("XValues", fXValues);
     v->visit("Segments", fSegments);
 
@@ -117,10 +121,10 @@
     }
 }
 
-SkColor4f SkColorCurveSegment::eval(SkScalar x, SkRandom& random) const {
+SkColor4f SkColorCurveSegment::eval(SkScalar x, SkScalar t) const {
     SkColor4f result = eval_segment(fMin, x, fType);
     if (fRanged) {
-        result = result + (eval_segment(fMax, x, fType) - result) * random.nextF();
+        result = result + (eval_segment(fMax, x, fType) - result) * t;
     }
     return result;
 }
@@ -148,9 +152,11 @@
     }
 }
 
-SkColor4f SkColorCurve::eval(SkScalar x, SkRandom& random) const {
+SkColor4f SkColorCurve::eval(const SkParticleUpdateParams& params, SkParticleState& ps) const {
     SkASSERT(fSegments.count() == fXValues.count() + 1);
 
+    float x = fInput.eval(params, ps);
+
     int i = 0;
     for (; i < fXValues.count(); ++i) {
         if (x <= fXValues[i]) {
@@ -165,10 +171,11 @@
         segmentX = rangeMin;
     }
     SkASSERT(0.0f <= segmentX && segmentX <= 1.0f);
-    return fSegments[i].eval(segmentX, random);
+    return fSegments[i].eval(segmentX, ps.fRandom.nextF());
 }
 
 void SkColorCurve::visitFields(SkFieldVisitor* v) {
+    v->visit("Input", fInput);
     v->visit("XValues", fXValues);
     v->visit("Segments", fSegments);
 
diff --git a/modules/particles/src/SkParticleAffector.cpp b/modules/particles/src/SkParticleAffector.cpp
index 9904ca2..655fa29 100644
--- a/modules/particles/src/SkParticleAffector.cpp
+++ b/modules/particles/src/SkParticleAffector.cpp
@@ -17,13 +17,8 @@
 
 #include "sk_tool_utils.h"
 
-constexpr SkFieldVisitor::EnumStringMapping gParticleFrameMapping[] = {
-    { kWorld_ParticleFrame,    "World" },
-    { kLocal_ParticleFrame,    "Local" },
-    { kVelocity_ParticleFrame, "Velocity" },
-};
-
-void SkParticleAffector::apply(SkParticleUpdateParams& params, SkParticleState ps[], int count) {
+void SkParticleAffector::apply(const SkParticleUpdateParams& params,
+                               SkParticleState ps[], int count) {
     if (fEnabled) {
         this->onApply(params, ps, count);
     }
@@ -33,23 +28,6 @@
     v->visit("Enabled", fEnabled);
 }
 
-static inline SkVector get_heading(const SkParticleState& ps, SkParticleFrame frame) {
-    switch (frame) {
-        case kLocal_ParticleFrame:
-            return ps.fPose.fHeading;
-        case kVelocity_ParticleFrame: {
-            SkVector heading = ps.fVelocity.fLinear;
-            if (!heading.normalize()) {
-                heading.set(0, -1);
-            }
-            return heading;
-        }
-        case kWorld_ParticleFrame:
-        default:
-            return SkVector{ 0, -1 };
-    }
-}
-
 class SkLinearVelocityAffector : public SkParticleAffector {
 public:
     SkLinearVelocityAffector(const SkCurve& angle = 0.0f,
@@ -63,14 +41,14 @@
 
     REFLECTED(SkLinearVelocityAffector, SkParticleAffector)
 
-    void onApply(SkParticleUpdateParams& params, SkParticleState ps[], int count) override {
+    void onApply(const SkParticleUpdateParams& params, SkParticleState ps[], int count) override {
         for (int i = 0; i < count; ++i) {
-            float angle = fAngle.eval(ps[i].fAge, ps[i].fRandom);
+            float angle = fAngle.eval(params, ps[i]);
             SkScalar c_local, s_local = SkScalarSinCos(SkDegreesToRadians(angle), &c_local);
-            SkVector heading = get_heading(ps[i], static_cast<SkParticleFrame>(fFrame));
+            SkVector heading = ps[i].getFrameHeading(static_cast<SkParticleFrame>(fFrame));
             SkScalar c = heading.fX * c_local - heading.fY * s_local;
             SkScalar s = heading.fX * s_local + heading.fY * c_local;
-            float strength = fStrength.eval(ps[i].fAge, ps[i].fRandom);
+            float strength = fStrength.eval(params, ps[i]);
             SkVector force = { c * strength, s * strength };
             if (fForce) {
                 ps[i].fVelocity.fLinear += force * params.fDeltaTime;
@@ -103,9 +81,9 @@
 
     REFLECTED(SkAngularVelocityAffector, SkParticleAffector)
 
-    void onApply(SkParticleUpdateParams& params, SkParticleState ps[], int count) override {
+    void onApply(const SkParticleUpdateParams& params, SkParticleState ps[], int count) override {
         for (int i = 0; i < count; ++i) {
-            float strength = fStrength.eval(ps[i].fAge, ps[i].fRandom);
+            float strength = fStrength.eval(params, ps[i]);
             if (fForce) {
                 ps[i].fVelocity.fAngular += strength * params.fDeltaTime;
             } else {
@@ -133,7 +111,7 @@
 
     REFLECTED(SkPointForceAffector, SkParticleAffector)
 
-    void onApply(SkParticleUpdateParams& params, SkParticleState ps[], int count) override {
+    void onApply(const SkParticleUpdateParams& params, SkParticleState ps[], int count) override {
         for (int i = 0; i < count; ++i) {
             SkVector toPoint = fPoint - ps[i].fPose.fPosition;
             SkScalar lenSquare = toPoint.dot(toPoint);
@@ -165,11 +143,11 @@
 
     REFLECTED(SkOrientationAffector, SkParticleAffector)
 
-    void onApply(SkParticleUpdateParams& params, SkParticleState ps[], int count) override {
+    void onApply(const SkParticleUpdateParams& params, SkParticleState ps[], int count) override {
         for (int i = 0; i < count; ++i) {
-            float angle = fAngle.eval(ps[i].fAge, ps[i].fRandom);
+            float angle = fAngle.eval(params, ps[i]);
             SkScalar c_local, s_local = SkScalarSinCos(SkDegreesToRadians(angle), &c_local);
-            SkVector heading = get_heading(ps[i], static_cast<SkParticleFrame>(fFrame));
+            SkVector heading = ps[i].getFrameHeading(static_cast<SkParticleFrame>(fFrame));
             ps[i].fPose.fHeading.set(heading.fX * c_local - heading.fY * s_local,
                                      heading.fX * s_local + heading.fY * c_local);
         }
@@ -197,7 +175,7 @@
 
     REFLECTED(SkPositionInCircleAffector, SkParticleAffector)
 
-    void onApply(SkParticleUpdateParams& params, SkParticleState ps[], int count) override {
+    void onApply(const SkParticleUpdateParams& params, SkParticleState ps[], int count) override {
         for (int i = 0; i < count; ++i) {
             SkVector v;
             do {
@@ -205,9 +183,8 @@
                 v.fY = ps[i].fRandom.nextSScalar1();
             } while (v.dot(v) > 1);
 
-            SkPoint center = { fX.eval(ps[i].fAge, ps[i].fRandom),
-                               fY.eval(ps[i].fAge, ps[i].fRandom) };
-            SkScalar radius = fRadius.eval(ps[i].fAge, ps[i].fRandom);
+            SkPoint center = { fX.eval(params, ps[i]), fY.eval(params, ps[i]) };
+            SkScalar radius = fRadius.eval(params, ps[i]);
             ps[i].fPose.fPosition = center + (v * radius);
             if (fSetHeading) {
                 if (!v.normalize()) {
@@ -235,22 +212,23 @@
 
 class SkPositionOnPathAffector : public SkParticleAffector {
 public:
-    SkPositionOnPathAffector(const char* path = "", bool setHeading = true, bool random = true)
+    SkPositionOnPathAffector(const char* path = "", bool setHeading = true,
+                             SkParticleValue input = SkParticleValue())
             : fPath(path)
-            , fSetHeading(setHeading)
-            , fRandom(random) {
+            , fInput(input)
+            , fSetHeading(setHeading) {
         this->rebuild();
     }
 
     REFLECTED(SkPositionOnPathAffector, SkParticleAffector)
 
-    void onApply(SkParticleUpdateParams& params, SkParticleState ps[], int count) override {
+    void onApply(const SkParticleUpdateParams& params, SkParticleState ps[], int count) override {
         if (fContours.empty()) {
             return;
         }
 
         for (int i = 0; i < count; ++i) {
-            float t = fRandom ? ps[i].fRandom.nextF() : ps[i].fAge;
+            float t = fInput.eval(params, ps[i]);
             SkScalar len = fTotalLength * t;
             int idx = 0;
             while (idx < fContours.count() && len > fContours[idx]->length()) {
@@ -271,8 +249,8 @@
         SkString oldPath = fPath;
 
         SkParticleAffector::visitFields(v);
+        v->visit("Input", fInput);
         v->visit("SetHeading", fSetHeading);
-        v->visit("Random", fRandom);
         v->visit("Path", fPath);
 
         if (fPath != oldPath) {
@@ -281,9 +259,9 @@
     }
 
 private:
-    SkString fPath;
-    bool     fSetHeading;
-    bool     fRandom;
+    SkString        fPath;
+    SkParticleValue fInput;
+    bool            fSetHeading;
 
     void rebuild() {
         SkPath path;
@@ -309,24 +287,24 @@
 class SkPositionOnTextAffector : public SkParticleAffector {
 public:
     SkPositionOnTextAffector(const char* text = "", SkScalar fontSize = 96, bool setHeading = true,
-                             bool random = true)
+                             SkParticleValue input = SkParticleValue())
             : fText(text)
             , fFontSize(fontSize)
-            , fSetHeading(setHeading)
-            , fRandom(random) {
+            , fInput(input)
+            , fSetHeading(setHeading) {
         this->rebuild();
     }
 
     REFLECTED(SkPositionOnTextAffector, SkParticleAffector)
 
-    void onApply(SkParticleUpdateParams& params, SkParticleState ps[], int count) override {
+    void onApply(const SkParticleUpdateParams& params, SkParticleState ps[], int count) override {
         if (fContours.empty()) {
             return;
         }
 
         // TODO: Refactor to share code with PositionOnPathAffector
         for (int i = 0; i < count; ++i) {
-            float t = fRandom ? ps[i].fRandom.nextF() : ps[i].fAge;
+            float t = fInput.eval(params, ps[i]);
             SkScalar len = fTotalLength * t;
             int idx = 0;
             while (idx < fContours.count() && len > fContours[idx]->length()) {
@@ -348,8 +326,8 @@
         SkScalar oldSize = fFontSize;
 
         SkParticleAffector::visitFields(v);
+        v->visit("Input", fInput);
         v->visit("SetHeading", fSetHeading);
-        v->visit("Random", fRandom);
         v->visit("Text", fText);
         v->visit("FontSize", fFontSize);
 
@@ -359,10 +337,10 @@
     }
 
 private:
-    SkString fText;
-    SkScalar fFontSize;
-    bool     fSetHeading;
-    bool     fRandom;
+    SkString        fText;
+    SkScalar        fFontSize;
+    SkParticleValue fInput;
+    bool            fSetHeading;
 
     void rebuild() {
         fTotalLength = 0;
@@ -394,9 +372,9 @@
 
     REFLECTED(SkSizeAffector, SkParticleAffector)
 
-    void onApply(SkParticleUpdateParams& params, SkParticleState ps[], int count) override {
+    void onApply(const SkParticleUpdateParams& params, SkParticleState ps[], int count) override {
         for (int i = 0; i < count; ++i) {
-            ps[i].fPose.fScale = fCurve.eval(ps[i].fAge, ps[i].fRandom);
+            ps[i].fPose.fScale = fCurve.eval(params, ps[i]);
         }
     }
 
@@ -415,9 +393,9 @@
 
     REFLECTED(SkFrameAffector, SkParticleAffector)
 
-    void onApply(SkParticleUpdateParams& params, SkParticleState ps[], int count) override {
+    void onApply(const SkParticleUpdateParams& params, SkParticleState ps[], int count) override {
         for (int i = 0; i < count; ++i) {
-            ps[i].fFrame = fCurve.eval(ps[i].fAge, ps[i].fRandom);
+            ps[i].fFrame = fCurve.eval(params, ps[i]);
         }
     }
 
@@ -437,9 +415,9 @@
 
     REFLECTED(SkColorAffector, SkParticleAffector)
 
-    void onApply(SkParticleUpdateParams& params, SkParticleState ps[], int count) override {
+    void onApply(const SkParticleUpdateParams& params, SkParticleState ps[], int count) override {
         for (int i = 0; i < count; ++i) {
-            ps[i].fColor = fCurve.eval(ps[i].fAge, ps[i].fRandom);
+            ps[i].fColor = fCurve.eval(params, ps[i]);
         }
     }
 
diff --git a/modules/particles/src/SkParticleEffect.cpp b/modules/particles/src/SkParticleEffect.cpp
index d1f7b64..2b6a88e 100644
--- a/modules/particles/src/SkParticleEffect.cpp
+++ b/modules/particles/src/SkParticleEffect.cpp
@@ -64,8 +64,15 @@
         this->setCapacity(fParams->fMaxCount);
     }
 
+    float effectAge = static_cast<float>((now - fSpawnTime) / fParams->fEffectDuration);
+    effectAge = fLooping ? fmodf(effectAge, 1.0f) : SkTPin(effectAge, 0.0f, 1.0f);
+
     SkParticleUpdateParams updateParams;
     updateParams.fDeltaTime = deltaTime;
+    updateParams.fEffectAge = effectAge;
+
+    // During spawn, values that refer to kAge_Source get the *effect* age
+    updateParams.fAgeSource = SkParticleValue::kEffectAge_Source;
 
     // Advance age for existing particles, and remove any that have reached their end of life
     for (int i = 0; i < fCount; ++i) {
@@ -85,18 +92,12 @@
     fSpawnRemainder = desired - numToSpawn;
     numToSpawn = SkTPin(numToSpawn, 0, fParams->fMaxCount - fCount);
     if (numToSpawn) {
-        // This isn't "particle" t, it's effect t.
-        float t = static_cast<float>((now - fSpawnTime) / fParams->fEffectDuration);
-        t = fLooping ? fmodf(t, 1.0f) : SkTPin(t, 0.0f, 1.0f);
+        const int spawnBase = fCount;
 
         for (int i = 0; i < numToSpawn; ++i) {
             // Mutate our SkRandom so each particle definitely gets a different stable generator
             fRandom.nextU();
-
-            // Temporarily set our age to the *effect* age, so spawn affectors are driven by that
-            fParticles[fCount].fAge = t;
-            fParticles[fCount].fInvLifetime =
-                    sk_ieee_float_divide(1.0f, fParams->fLifetime.eval(t, fRandom));
+            fParticles[fCount].fAge = 0.0f;
             fParticles[fCount].fPose.fPosition = { 0.0f, 0.0f };
             fParticles[fCount].fPose.fHeading = { 0.0f, -1.0f };
             fParticles[fCount].fPose.fScale = 1.0f;
@@ -104,19 +105,21 @@
             fParticles[fCount].fVelocity.fAngular = 0.0f;
             fParticles[fCount].fColor = { 1.0f, 1.0f, 1.0f, 1.0f };
             fParticles[fCount].fFrame = 0.0f;
-
             fParticles[fCount].fRandom = fStableRandoms[fCount] = fRandom;
             fCount++;
         }
 
-        // Apply spawn affectors, then reset our age to 0 (the *particle* age)
+        // Apply spawn affectors
         for (auto affector : fParams->fSpawnAffectors) {
             if (affector) {
-                affector->apply(updateParams, fParticles + (fCount - numToSpawn), numToSpawn);
+                affector->apply(updateParams, fParticles + spawnBase, numToSpawn);
             }
         }
-        for (int i = fCount - numToSpawn; i < fCount; ++i) {
-            fParticles[i].fAge = 0.0f;
+
+        // Now compute particle lifetimes (so the curve can refer to spawn-computed source values)
+        for (int i = spawnBase; i < fCount; ++i) {
+            fParticles[i].fInvLifetime =
+                sk_ieee_float_divide(1.0f, fParams->fLifetime.eval(updateParams, fParticles[i]));
         }
     }
 
@@ -125,6 +128,9 @@
         fParticles[i].fRandom = fStableRandoms[i];
     }
 
+    // During update, values that refer to kAge_Source get the *particle* age
+    updateParams.fAgeSource = SkParticleValue::kParticleAge_Source;
+
     // Apply update rules
     for (auto affector : fParams->fUpdateAffectors) {
         if (affector) {
@@ -163,3 +169,116 @@
     fCapacity = capacity;
     fCount = SkTMin(fCount, fCapacity);
 }
+
+constexpr SkFieldVisitor::EnumStringMapping gValueSourceMapping[] = {
+    { SkParticleValue::kAge_Source,          "Age" },
+    { SkParticleValue::kRandom_Source,       "Random" },
+    { SkParticleValue::kParticleAge_Source,  "ParticleAge" },
+    { SkParticleValue::kEffectAge_Source,    "EffectAge" },
+    { SkParticleValue::kPositionX_Source,    "PositionX" },
+    { SkParticleValue::kPositionY_Source,    "PositionY" },
+    { SkParticleValue::kHeadingX_Source,     "HeadingX" },
+    { SkParticleValue::kHeadingY_Source,     "HeadingY" },
+    { SkParticleValue::kScale_Source,        "Scale" },
+    { SkParticleValue::kVelocityX_Source,    "VelocityX" },
+    { SkParticleValue::kVelocityY_Source,    "VelocityY" },
+    { SkParticleValue::kRotation_Source,     "Rotation" },
+    { SkParticleValue::kColorR_Source,       "ColorR" },
+    { SkParticleValue::kColorG_Source,       "ColorG" },
+    { SkParticleValue::kColorB_Source,       "ColorB" },
+    { SkParticleValue::kColorA_Source,       "ColorA" },
+    { SkParticleValue::kSpriteFrame_Source,  "SpriteFrame" },
+};
+
+constexpr SkFieldVisitor::EnumStringMapping gValueTileModeMapping[] = {
+    { SkParticleValue::kClamp_TileMode,  "Clamp" },
+    { SkParticleValue::kRepeat_TileMode, "Repeat" },
+    { SkParticleValue::kMirror_TileMode, "Mirror" },
+};
+
+static bool source_needs_frame(int source) {
+    switch (source) {
+        case SkParticleValue::kHeadingX_Source:
+        case SkParticleValue::kHeadingY_Source:
+        case SkParticleValue::kVelocityX_Source:
+        case SkParticleValue::kVelocityY_Source:
+            return true;
+        default:
+            return false;
+    }
+}
+
+void SkParticleValue::visitFields(SkFieldVisitor* v) {
+    v->visit("Source", fSource, gValueSourceMapping, SK_ARRAY_COUNT(gValueSourceMapping));
+    if (source_needs_frame(fSource)) {
+        v->visit("Frame", fFrame, gParticleFrameMapping, SK_ARRAY_COUNT(gParticleFrameMapping));
+    }
+    v->visit("TileMode", fTileMode, gValueTileModeMapping, SK_ARRAY_COUNT(gValueTileModeMapping));
+    v->visit("Left", fLeft);
+    v->visit("Right", fRight);
+
+    // Re-compute cached evaluation parameters
+    fScale = sk_float_isfinite(1.0f / (fRight - fLeft)) ? 1.0f / (fRight - fLeft) : 0;
+    fBias = -fLeft * fScale;
+}
+
+float SkParticleValue::getSourceValue(const SkParticleUpdateParams& params,
+                                      SkParticleState& ps) const {
+    switch ((kAge_Source == fSource) ? params.fAgeSource : fSource) {
+        // Do all the simple (non-frame-dependent) sources first:
+        case kRandom_Source:      return ps.fRandom.nextF();
+        case kParticleAge_Source: return ps.fAge;
+        case kEffectAge_Source:   return params.fEffectAge;
+
+        case kPositionX_Source:   return ps.fPose.fPosition.fX;
+        case kPositionY_Source:   return ps.fPose.fPosition.fY;
+        case kScale_Source:       return ps.fPose.fScale;
+        case kRotation_Source:    return ps.fVelocity.fAngular;
+
+        case kColorR_Source:      return ps.fColor.fR;
+        case kColorG_Source:      return ps.fColor.fG;
+        case kColorB_Source:      return ps.fColor.fB;
+        case kColorA_Source:      return ps.fColor.fA;
+        case kSpriteFrame_Source: return ps.fFrame;
+    }
+
+    SkASSERT(source_needs_frame(fSource));
+    SkVector frameUp = ps.getFrameHeading(static_cast<SkParticleFrame>(fFrame));
+    SkVector frameRight = { -frameUp.fY, frameUp.fX };
+
+    switch (fSource) {
+        case kHeadingX_Source:  return ps.fPose.fHeading.dot(frameRight);
+        case kHeadingY_Source:  return ps.fPose.fHeading.dot(frameUp);
+        case kVelocityX_Source: return ps.fVelocity.fLinear.dot(frameRight);
+        case kVelocityY_Source: return ps.fVelocity.fLinear.dot(frameUp);
+    }
+
+    SkDEBUGFAIL("Unreachable");
+    return 0.0f;
+}
+
+float SkParticleValue::eval(const SkParticleUpdateParams& params, SkParticleState& ps) const {
+    float v = this->getSourceValue(params, ps);
+    v = (v * fScale) + fBias;
+
+    switch (fTileMode) {
+        case kClamp_TileMode:
+            v = SkTPin(v, 0.0f, 1.0f);
+            break;
+        case kRepeat_TileMode:
+            v = sk_float_mod(v, 1.0f);
+            if (v < 0) {
+                v += 1.0f;
+            }
+            break;
+        case kMirror_TileMode:
+            v = sk_float_mod(v, 2.0f);
+            if (v < 0) {
+                v += 2.0f;
+            }
+            v = 1.0f - sk_float_abs(v - 1.0f);
+            break;
+    }
+
+    return v;
+}
diff --git a/modules/particles/src/SkReflected.cpp b/modules/particles/src/SkReflected.cpp
index 48458cc..4b781d9 100644
--- a/modules/particles/src/SkReflected.cpp
+++ b/modules/particles/src/SkReflected.cpp
@@ -7,6 +7,8 @@
 
 #include "SkReflected.h"
 
+#include "SkCurve.h"
+
 SkSTArray<16, const SkReflected::Type*, true> SkReflected::gTypes;
 
 void SkReflected::VisitTypes(std::function<void(const Type*)> visitor, const Type* baseType) {
@@ -16,3 +18,9 @@
         }
     }
 }
+
+void SkFieldVisitor::visit(const char* name, SkCurve& c) {
+    this->enterObject(name);
+    c.visitFields(this);
+    this->exitObject();
+}