1
2/*
3 * Copyright 2012 Google Inc.
4 *
5 * Use of this source code is governed by a BSD-style license that can be
6 * found in the LICENSE file.
7 */
8
9#include "SkRadialGradient.h"
10#include "SkRadialGradient_Table.h"
11
12#define kSQRT_TABLE_BITS    11
13#define kSQRT_TABLE_SIZE    (1 << kSQRT_TABLE_BITS)
14
15#if defined(SK_BUILD_FOR_WIN32) && defined(SK_DEBUG)
16
17#include <stdio.h>
18
19void SkRadialGradient_BuildTable() {
20    // build it 0..127 x 0..127, so we use 2^15 - 1 in the numerator for our "fixed" table
21
22    FILE* file = ::fopen("SkRadialGradient_Table.h", "w");
23    SkASSERT(file);
24    ::fprintf(file, "static const uint8_t gSqrt8Table[] = {\n");
25
26    for (int i = 0; i < kSQRT_TABLE_SIZE; i++) {
27        if ((i & 15) == 0) {
28            ::fprintf(file, "\t");
29        }
30
31        uint8_t value = SkToU8(SkFixedSqrt(i * SK_Fixed1 / kSQRT_TABLE_SIZE) >> 8);
32
33        ::fprintf(file, "0x%02X", value);
34        if (i < kSQRT_TABLE_SIZE-1) {
35            ::fprintf(file, ", ");
36        }
37        if ((i & 15) == 15) {
38            ::fprintf(file, "\n");
39        }
40    }
41    ::fprintf(file, "};\n");
42    ::fclose(file);
43}
44
45#endif
46
47namespace {
48
49// GCC doesn't like using static functions as template arguments.  So force these to be non-static.
50inline SkFixed mirror_tileproc_nonstatic(SkFixed x) {
51    return mirror_tileproc(x);
52}
53
54inline SkFixed repeat_tileproc_nonstatic(SkFixed x) {
55    return repeat_tileproc(x);
56}
57
58void rad_to_unit_matrix(const SkPoint& center, SkScalar radius,
59                               SkMatrix* matrix) {
60    SkScalar    inv = SkScalarInvert(radius);
61
62    matrix->setTranslate(-center.fX, -center.fY);
63    matrix->postScale(inv, inv);
64}
65
66typedef void (* RadialShade16Proc)(SkScalar sfx, SkScalar sdx,
67        SkScalar sfy, SkScalar sdy,
68        uint16_t* dstC, const uint16_t* cache,
69        int toggle, int count);
70
71void shadeSpan16_radial_clamp(SkScalar sfx, SkScalar sdx,
72        SkScalar sfy, SkScalar sdy,
73        uint16_t* SK_RESTRICT dstC, const uint16_t* SK_RESTRICT cache,
74        int toggle, int count) {
75    const uint8_t* SK_RESTRICT sqrt_table = gSqrt8Table;
76
77    /* knock these down so we can pin against +- 0x7FFF, which is an
78       immediate load, rather than 0xFFFF which is slower. This is a
79       compromise, since it reduces our precision, but that appears
80       to be visually OK. If we decide this is OK for all of our cases,
81       we could (it seems) put this scale-down into fDstToIndex,
82       to avoid having to do these extra shifts each time.
83    */
84    SkFixed fx = SkScalarToFixed(sfx) >> 1;
85    SkFixed dx = SkScalarToFixed(sdx) >> 1;
86    SkFixed fy = SkScalarToFixed(sfy) >> 1;
87    SkFixed dy = SkScalarToFixed(sdy) >> 1;
88    // might perform this check for the other modes,
89    // but the win will be a smaller % of the total
90    if (dy == 0) {
91        fy = SkPin32(fy, -0xFFFF >> 1, 0xFFFF >> 1);
92        fy *= fy;
93        do {
94            unsigned xx = SkPin32(fx, -0xFFFF >> 1, 0xFFFF >> 1);
95            unsigned fi = (xx * xx + fy) >> (14 + 16 - kSQRT_TABLE_BITS);
96            fi = SkFastMin32(fi, 0xFFFF >> (16 - kSQRT_TABLE_BITS));
97            fx += dx;
98            *dstC++ = cache[toggle +
99                            (sqrt_table[fi] >> SkGradientShaderBase::kSqrt16Shift)];
100            toggle = next_dither_toggle16(toggle);
101        } while (--count != 0);
102    } else {
103        do {
104            unsigned xx = SkPin32(fx, -0xFFFF >> 1, 0xFFFF >> 1);
105            unsigned fi = SkPin32(fy, -0xFFFF >> 1, 0xFFFF >> 1);
106            fi = (xx * xx + fi * fi) >> (14 + 16 - kSQRT_TABLE_BITS);
107            fi = SkFastMin32(fi, 0xFFFF >> (16 - kSQRT_TABLE_BITS));
108            fx += dx;
109            fy += dy;
110            *dstC++ = cache[toggle +
111                            (sqrt_table[fi] >> SkGradientShaderBase::kSqrt16Shift)];
112            toggle = next_dither_toggle16(toggle);
113        } while (--count != 0);
114    }
115}
116
117template <SkFixed (*TileProc)(SkFixed)>
118void shadeSpan16_radial(SkScalar fx, SkScalar dx, SkScalar fy, SkScalar dy,
119                        uint16_t* SK_RESTRICT dstC, const uint16_t* SK_RESTRICT cache,
120                        int toggle, int count) {
121    do {
122        const SkFixed dist = SkFloatToFixed(sk_float_sqrt(fx*fx + fy*fy));
123        const unsigned fi = TileProc(dist);
124        SkASSERT(fi <= 0xFFFF);
125        *dstC++ = cache[toggle + (fi >> SkGradientShaderBase::kCache16Shift)];
126        toggle = next_dither_toggle16(toggle);
127        fx += dx;
128        fy += dy;
129    } while (--count != 0);
130}
131
132void shadeSpan16_radial_mirror(SkScalar fx, SkScalar dx, SkScalar fy, SkScalar dy,
133                               uint16_t* SK_RESTRICT dstC, const uint16_t* SK_RESTRICT cache,
134                               int toggle, int count) {
135    shadeSpan16_radial<mirror_tileproc_nonstatic>(fx, dx, fy, dy, dstC, cache, toggle, count);
136}
137
138void shadeSpan16_radial_repeat(SkScalar fx, SkScalar dx, SkScalar fy, SkScalar dy,
139                               uint16_t* SK_RESTRICT dstC, const uint16_t* SK_RESTRICT cache,
140                               int toggle, int count) {
141    shadeSpan16_radial<repeat_tileproc_nonstatic>(fx, dx, fy, dy, dstC, cache, toggle, count);
142}
143
144}  // namespace
145
146/////////////////////////////////////////////////////////////////////
147
148SkRadialGradient::SkRadialGradient(const SkPoint& center, SkScalar radius,
149                                   const Descriptor& desc)
150    : SkGradientShaderBase(desc),
151      fCenter(center),
152      fRadius(radius)
153{
154    // make sure our table is insync with our current #define for kSQRT_TABLE_SIZE
155    SkASSERT(sizeof(gSqrt8Table) == kSQRT_TABLE_SIZE);
156
157    rad_to_unit_matrix(center, radius, &fPtsToUnit);
158}
159
160void SkRadialGradient::shadeSpan16(int x, int y, uint16_t* dstCParam,
161                         int count) {
162    SkASSERT(count > 0);
163
164    uint16_t* SK_RESTRICT dstC = dstCParam;
165
166    SkPoint             srcPt;
167    SkMatrix::MapXYProc dstProc = fDstToIndexProc;
168    TileProc            proc = fTileProc;
169    const uint16_t* SK_RESTRICT cache = this->getCache16();
170    int                 toggle = init_dither_toggle16(x, y);
171
172    if (fDstToIndexClass != kPerspective_MatrixClass) {
173        dstProc(fDstToIndex, SkIntToScalar(x) + SK_ScalarHalf,
174                             SkIntToScalar(y) + SK_ScalarHalf, &srcPt);
175
176        SkScalar sdx = fDstToIndex.getScaleX();
177        SkScalar sdy = fDstToIndex.getSkewY();
178
179        if (fDstToIndexClass == kFixedStepInX_MatrixClass) {
180            SkFixed storage[2];
181            (void)fDstToIndex.fixedStepInX(SkIntToScalar(y),
182                                           &storage[0], &storage[1]);
183            sdx = SkFixedToScalar(storage[0]);
184            sdy = SkFixedToScalar(storage[1]);
185        } else {
186            SkASSERT(fDstToIndexClass == kLinear_MatrixClass);
187        }
188
189        RadialShade16Proc shadeProc = shadeSpan16_radial_repeat;
190        if (SkShader::kClamp_TileMode == fTileMode) {
191            shadeProc = shadeSpan16_radial_clamp;
192        } else if (SkShader::kMirror_TileMode == fTileMode) {
193            shadeProc = shadeSpan16_radial_mirror;
194        } else {
195            SkASSERT(SkShader::kRepeat_TileMode == fTileMode);
196        }
197        (*shadeProc)(srcPt.fX, sdx, srcPt.fY, sdy, dstC,
198                     cache, toggle, count);
199    } else {    // perspective case
200        SkScalar dstX = SkIntToScalar(x);
201        SkScalar dstY = SkIntToScalar(y);
202        do {
203            dstProc(fDstToIndex, dstX, dstY, &srcPt);
204            unsigned fi = proc(SkScalarToFixed(srcPt.length()));
205            SkASSERT(fi <= 0xFFFF);
206
207            int index = fi >> (16 - kCache16Bits);
208            *dstC++ = cache[toggle + index];
209            toggle = next_dither_toggle16(toggle);
210
211            dstX += SK_Scalar1;
212        } while (--count != 0);
213    }
214}
215
216SkShader::BitmapType SkRadialGradient::asABitmap(SkBitmap* bitmap,
217    SkMatrix* matrix, SkShader::TileMode* xy) const {
218    if (bitmap) {
219        this->getGradientTableBitmap(bitmap);
220    }
221    if (matrix) {
222        matrix->setScale(SkIntToScalar(kCache32Count),
223                         SkIntToScalar(kCache32Count));
224        matrix->preConcat(fPtsToUnit);
225    }
226    if (xy) {
227        xy[0] = fTileMode;
228        xy[1] = kClamp_TileMode;
229    }
230    return kRadial_BitmapType;
231}
232
233SkShader::GradientType SkRadialGradient::asAGradient(GradientInfo* info) const {
234    if (info) {
235        commonAsAGradient(info);
236        info->fPoint[0] = fCenter;
237        info->fRadius[0] = fRadius;
238    }
239    return kRadial_GradientType;
240}
241
242SkRadialGradient::SkRadialGradient(SkFlattenableReadBuffer& buffer)
243    : INHERITED(buffer),
244      fCenter(buffer.readPoint()),
245      fRadius(buffer.readScalar()) {
246}
247
248void SkRadialGradient::flatten(SkFlattenableWriteBuffer& buffer) const {
249    this->INHERITED::flatten(buffer);
250    buffer.writePoint(fCenter);
251    buffer.writeScalar(fRadius);
252}
253
254namespace {
255
256inline bool radial_completely_pinned(int fx, int dx, int fy, int dy) {
257    // fast, overly-conservative test: checks unit square instead
258    // of unit circle
259    bool xClamped = (fx >= SK_FixedHalf && dx >= 0) ||
260                    (fx <= -SK_FixedHalf && dx <= 0);
261    bool yClamped = (fy >= SK_FixedHalf && dy >= 0) ||
262                    (fy <= -SK_FixedHalf && dy <= 0);
263
264    return xClamped || yClamped;
265}
266
267// Return true if (fx * fy) is always inside the unit circle
268// SkPin32 is expensive, but so are all the SkFixedMul in this test,
269// so it shouldn't be run if count is small.
270inline bool no_need_for_radial_pin(int fx, int dx,
271                                          int fy, int dy, int count) {
272    SkASSERT(count > 0);
273    if (SkAbs32(fx) > 0x7FFF || SkAbs32(fy) > 0x7FFF) {
274        return false;
275    }
276    if (fx*fx + fy*fy > 0x7FFF*0x7FFF) {
277        return false;
278    }
279    fx += (count - 1) * dx;
280    fy += (count - 1) * dy;
281    if (SkAbs32(fx) > 0x7FFF || SkAbs32(fy) > 0x7FFF) {
282        return false;
283    }
284    return fx*fx + fy*fy <= 0x7FFF*0x7FFF;
285}
286
287#define UNPINNED_RADIAL_STEP \
288    fi = (fx * fx + fy * fy) >> (14 + 16 - kSQRT_TABLE_BITS); \
289    *dstC++ = cache[toggle + \
290                    (sqrt_table[fi] >> SkGradientShaderBase::kSqrt32Shift)]; \
291    toggle = next_dither_toggle(toggle); \
292    fx += dx; \
293    fy += dy;
294
295typedef void (* RadialShadeProc)(SkScalar sfx, SkScalar sdx,
296        SkScalar sfy, SkScalar sdy,
297        SkPMColor* dstC, const SkPMColor* cache,
298        int count, int toggle);
299
300// On Linux, this is faster with SkPMColor[] params than SkPMColor* SK_RESTRICT
301void shadeSpan_radial_clamp(SkScalar sfx, SkScalar sdx,
302        SkScalar sfy, SkScalar sdy,
303        SkPMColor* SK_RESTRICT dstC, const SkPMColor* SK_RESTRICT cache,
304        int count, int toggle) {
305    // Floating point seems to be slower than fixed point,
306    // even when we have float hardware.
307    const uint8_t* SK_RESTRICT sqrt_table = gSqrt8Table;
308    SkFixed fx = SkScalarToFixed(sfx) >> 1;
309    SkFixed dx = SkScalarToFixed(sdx) >> 1;
310    SkFixed fy = SkScalarToFixed(sfy) >> 1;
311    SkFixed dy = SkScalarToFixed(sdy) >> 1;
312    if ((count > 4) && radial_completely_pinned(fx, dx, fy, dy)) {
313        unsigned fi = SkGradientShaderBase::kCache32Count - 1;
314        sk_memset32_dither(dstC,
315            cache[toggle + fi],
316            cache[next_dither_toggle(toggle) + fi],
317            count);
318    } else if ((count > 4) &&
319               no_need_for_radial_pin(fx, dx, fy, dy, count)) {
320        unsigned fi;
321        // 4x unroll appears to be no faster than 2x unroll on Linux
322        while (count > 1) {
323            UNPINNED_RADIAL_STEP;
324            UNPINNED_RADIAL_STEP;
325            count -= 2;
326        }
327        if (count) {
328            UNPINNED_RADIAL_STEP;
329        }
330    } else  {
331        // Specializing for dy == 0 gains us 25% on Skia benchmarks
332        if (dy == 0) {
333            unsigned yy = SkPin32(fy, -0xFFFF >> 1, 0xFFFF >> 1);
334            yy *= yy;
335            do {
336                unsigned xx = SkPin32(fx, -0xFFFF >> 1, 0xFFFF >> 1);
337                unsigned fi = (xx * xx + yy) >> (14 + 16 - kSQRT_TABLE_BITS);
338                fi = SkFastMin32(fi, 0xFFFF >> (16 - kSQRT_TABLE_BITS));
339                *dstC++ = cache[toggle + (sqrt_table[fi] >>
340                    SkGradientShaderBase::kSqrt32Shift)];
341                toggle = next_dither_toggle(toggle);
342                fx += dx;
343            } while (--count != 0);
344        } else {
345            do {
346                unsigned xx = SkPin32(fx, -0xFFFF >> 1, 0xFFFF >> 1);
347                unsigned fi = SkPin32(fy, -0xFFFF >> 1, 0xFFFF >> 1);
348                fi = (xx * xx + fi * fi) >> (14 + 16 - kSQRT_TABLE_BITS);
349                fi = SkFastMin32(fi, 0xFFFF >> (16 - kSQRT_TABLE_BITS));
350                *dstC++ = cache[toggle + (sqrt_table[fi] >>
351                    SkGradientShaderBase::kSqrt32Shift)];
352                toggle = next_dither_toggle(toggle);
353                fx += dx;
354                fy += dy;
355            } while (--count != 0);
356        }
357    }
358}
359
360// Unrolling this loop doesn't seem to help (when float); we're stalling to
361// get the results of the sqrt (?), and don't have enough extra registers to
362// have many in flight.
363template <SkFixed (*TileProc)(SkFixed)>
364void shadeSpan_radial(SkScalar fx, SkScalar dx, SkScalar fy, SkScalar dy,
365                      SkPMColor* SK_RESTRICT dstC, const SkPMColor* SK_RESTRICT cache,
366                      int count, int toggle) {
367    do {
368        const SkFixed dist = SkFloatToFixed(sk_float_sqrt(fx*fx + fy*fy));
369        const unsigned fi = TileProc(dist);
370        SkASSERT(fi <= 0xFFFF);
371        *dstC++ = cache[toggle + (fi >> SkGradientShaderBase::kCache32Shift)];
372        toggle = next_dither_toggle(toggle);
373        fx += dx;
374        fy += dy;
375    } while (--count != 0);
376}
377
378void shadeSpan_radial_mirror(SkScalar fx, SkScalar dx, SkScalar fy, SkScalar dy,
379                             SkPMColor* SK_RESTRICT dstC, const SkPMColor* SK_RESTRICT cache,
380                             int count, int toggle) {
381    shadeSpan_radial<mirror_tileproc_nonstatic>(fx, dx, fy, dy, dstC, cache, count, toggle);
382}
383
384void shadeSpan_radial_repeat(SkScalar fx, SkScalar dx, SkScalar fy, SkScalar dy,
385                             SkPMColor* SK_RESTRICT dstC, const SkPMColor* SK_RESTRICT cache,
386                             int count, int toggle) {
387    shadeSpan_radial<repeat_tileproc_nonstatic>(fx, dx, fy, dy, dstC, cache, count, toggle);
388}
389
390}  // namespace
391
392void SkRadialGradient::shadeSpan(int x, int y,
393                                SkPMColor* SK_RESTRICT dstC, int count) {
394    SkASSERT(count > 0);
395
396    SkPoint             srcPt;
397    SkMatrix::MapXYProc dstProc = fDstToIndexProc;
398    TileProc            proc = fTileProc;
399    const SkPMColor* SK_RESTRICT cache = this->getCache32();
400    int toggle = init_dither_toggle(x, y);
401
402    if (fDstToIndexClass != kPerspective_MatrixClass) {
403        dstProc(fDstToIndex, SkIntToScalar(x) + SK_ScalarHalf,
404                             SkIntToScalar(y) + SK_ScalarHalf, &srcPt);
405        SkScalar sdx = fDstToIndex.getScaleX();
406        SkScalar sdy = fDstToIndex.getSkewY();
407
408        if (fDstToIndexClass == kFixedStepInX_MatrixClass) {
409            SkFixed storage[2];
410            (void)fDstToIndex.fixedStepInX(SkIntToScalar(y),
411                                           &storage[0], &storage[1]);
412            sdx = SkFixedToScalar(storage[0]);
413            sdy = SkFixedToScalar(storage[1]);
414        } else {
415            SkASSERT(fDstToIndexClass == kLinear_MatrixClass);
416        }
417
418        RadialShadeProc shadeProc = shadeSpan_radial_repeat;
419        if (SkShader::kClamp_TileMode == fTileMode) {
420            shadeProc = shadeSpan_radial_clamp;
421        } else if (SkShader::kMirror_TileMode == fTileMode) {
422            shadeProc = shadeSpan_radial_mirror;
423        } else {
424            SkASSERT(SkShader::kRepeat_TileMode == fTileMode);
425        }
426        (*shadeProc)(srcPt.fX, sdx, srcPt.fY, sdy, dstC, cache, count, toggle);
427    } else {    // perspective case
428        SkScalar dstX = SkIntToScalar(x);
429        SkScalar dstY = SkIntToScalar(y);
430        do {
431            dstProc(fDstToIndex, dstX, dstY, &srcPt);
432            unsigned fi = proc(SkScalarToFixed(srcPt.length()));
433            SkASSERT(fi <= 0xFFFF);
434            *dstC++ = cache[fi >> SkGradientShaderBase::kCache32Shift];
435            dstX += SK_Scalar1;
436        } while (--count != 0);
437    }
438}
439
440/////////////////////////////////////////////////////////////////////
441
442#if SK_SUPPORT_GPU
443
444#include "GrTBackendEffectFactory.h"
445
446class GrGLRadialGradient : public GrGLGradientEffect {
447public:
448
449    GrGLRadialGradient(const GrBackendEffectFactory& factory,
450                       const GrDrawEffect&) : INHERITED (factory) { }
451    virtual ~GrGLRadialGradient() { }
452
453    virtual void emitCode(GrGLShaderBuilder*,
454                          const GrDrawEffect&,
455                          EffectKey,
456                          const char* outputColor,
457                          const char* inputColor,
458                          const TransformedCoordsArray&,
459                          const TextureSamplerArray&) SK_OVERRIDE;
460
461    static EffectKey GenKey(const GrDrawEffect& drawEffect, const GrGLCaps&) {
462        return GenBaseGradientKey(drawEffect);
463    }
464
465private:
466
467    typedef GrGLGradientEffect INHERITED;
468
469};
470
471/////////////////////////////////////////////////////////////////////
472
473class GrRadialGradient : public GrGradientEffect {
474public:
475    static GrEffectRef* Create(GrContext* ctx,
476                               const SkRadialGradient& shader,
477                               const SkMatrix& matrix,
478                               SkShader::TileMode tm) {
479        AutoEffectUnref effect(SkNEW_ARGS(GrRadialGradient, (ctx, shader, matrix, tm)));
480        return CreateEffectRef(effect);
481    }
482
483    virtual ~GrRadialGradient() { }
484
485    static const char* Name() { return "Radial Gradient"; }
486    virtual const GrBackendEffectFactory& getFactory() const SK_OVERRIDE {
487        return GrTBackendEffectFactory<GrRadialGradient>::getInstance();
488    }
489
490    typedef GrGLRadialGradient GLEffect;
491
492private:
493    GrRadialGradient(GrContext* ctx,
494                     const SkRadialGradient& shader,
495                     const SkMatrix& matrix,
496                     SkShader::TileMode tm)
497        : INHERITED(ctx, shader, matrix, tm) {
498    }
499
500    GR_DECLARE_EFFECT_TEST;
501
502    typedef GrGradientEffect INHERITED;
503};
504
505/////////////////////////////////////////////////////////////////////
506
507GR_DEFINE_EFFECT_TEST(GrRadialGradient);
508
509GrEffectRef* GrRadialGradient::TestCreate(SkRandom* random,
510                                          GrContext* context,
511                                          const GrDrawTargetCaps&,
512                                          GrTexture**) {
513    SkPoint center = {random->nextUScalar1(), random->nextUScalar1()};
514    SkScalar radius = random->nextUScalar1();
515
516    SkColor colors[kMaxRandomGradientColors];
517    SkScalar stopsArray[kMaxRandomGradientColors];
518    SkScalar* stops = stopsArray;
519    SkShader::TileMode tm;
520    int colorCount = RandomGradientParams(random, colors, &stops, &tm);
521    SkAutoTUnref<SkShader> shader(SkGradientShader::CreateRadial(center, radius,
522                                                                 colors, stops, colorCount,
523                                                                 tm));
524    SkPaint paint;
525    return shader->asNewEffect(context, paint);
526}
527
528/////////////////////////////////////////////////////////////////////
529
530void GrGLRadialGradient::emitCode(GrGLShaderBuilder* builder,
531                                  const GrDrawEffect&,
532                                  EffectKey key,
533                                  const char* outputColor,
534                                  const char* inputColor,
535                                  const TransformedCoordsArray& coords,
536                                  const TextureSamplerArray& samplers) {
537    this->emitUniforms(builder, key);
538    SkString t("length(");
539    t.append(builder->ensureFSCoords2D(coords, 0));
540    t.append(")");
541    this->emitColor(builder, t.c_str(), key, outputColor, inputColor, samplers);
542}
543
544/////////////////////////////////////////////////////////////////////
545
546GrEffectRef* SkRadialGradient::asNewEffect(GrContext* context, const SkPaint&) const {
547    SkASSERT(NULL != context);
548
549    SkMatrix matrix;
550    if (!this->getLocalMatrix().invert(&matrix)) {
551        return NULL;
552    }
553    matrix.postConcat(fPtsToUnit);
554    return GrRadialGradient::Create(context, *this, matrix, fTileMode);
555}
556
557#else
558
559GrEffectRef* SkRadialGradient::asNewEffect(GrContext*, const SkPaint&) const {
560    SkDEBUGFAIL("Should not call in GPU-less build");
561    return NULL;
562}
563
564#endif
565
566#ifdef SK_DEVELOPER
567void SkRadialGradient::toString(SkString* str) const {
568    str->append("SkRadialGradient: (");
569
570    str->append("center: (");
571    str->appendScalar(fCenter.fX);
572    str->append(", ");
573    str->appendScalar(fCenter.fY);
574    str->append(") radius: ");
575    str->appendScalar(fRadius);
576    str->append(" ");
577
578    this->INHERITED::toString(str);
579
580    str->append(")");
581}
582#endif
583