1/* 2 * Copyright 2012 Google Inc. 3 * 4 * Use of this source code is governed by a BSD-style license that can be 5 * found in the LICENSE file. 6 */ 7 8#include "SkLinearGradient.h" 9 10static inline int repeat_bits(int x, const int bits) { 11 return x & ((1 << bits) - 1); 12} 13 14static inline int repeat_8bits(int x) { 15 return x & 0xFF; 16} 17 18// Visual Studio 2010 (MSC_VER=1600) optimizes bit-shift code incorrectly. 19// See http://code.google.com/p/skia/issues/detail?id=472 20#if defined(_MSC_VER) && (_MSC_VER >= 1600) 21#pragma optimize("", off) 22#endif 23 24static inline int mirror_bits(int x, const int bits) { 25 if (x & (1 << bits)) { 26 x = ~x; 27 } 28 return x & ((1 << bits) - 1); 29} 30 31static inline int mirror_8bits(int x) { 32 if (x & 256) { 33 x = ~x; 34 } 35 return x & 255; 36} 37 38#if defined(_MSC_VER) && (_MSC_VER >= 1600) 39#pragma optimize("", on) 40#endif 41 42static SkMatrix pts_to_unit_matrix(const SkPoint pts[2]) { 43 SkVector vec = pts[1] - pts[0]; 44 SkScalar mag = vec.length(); 45 SkScalar inv = mag ? SkScalarInvert(mag) : 0; 46 47 vec.scale(inv); 48 SkMatrix matrix; 49 matrix.setSinCos(-vec.fY, vec.fX, pts[0].fX, pts[0].fY); 50 matrix.postTranslate(-pts[0].fX, -pts[0].fY); 51 matrix.postScale(inv, inv); 52 return matrix; 53} 54 55/////////////////////////////////////////////////////////////////////////////// 56 57SkLinearGradient::SkLinearGradient(const SkPoint pts[2], const Descriptor& desc) 58 : SkGradientShaderBase(desc, pts_to_unit_matrix(pts)) 59 , fStart(pts[0]) 60 , fEnd(pts[1]) { 61} 62 63SkFlattenable* SkLinearGradient::CreateProc(SkReadBuffer& buffer) { 64 DescriptorScope desc; 65 if (!desc.unflatten(buffer)) { 66 return NULL; 67 } 68 SkPoint pts[2]; 69 pts[0] = buffer.readPoint(); 70 pts[1] = buffer.readPoint(); 71 return SkGradientShader::CreateLinear(pts, desc.fColors, desc.fPos, desc.fCount, 72 desc.fTileMode, desc.fGradFlags, desc.fLocalMatrix); 73} 74 75void SkLinearGradient::flatten(SkWriteBuffer& buffer) const { 76 this->INHERITED::flatten(buffer); 77 buffer.writePoint(fStart); 78 buffer.writePoint(fEnd); 79} 80 81size_t SkLinearGradient::contextSize() const { 82 return sizeof(LinearGradientContext); 83} 84 85SkShader::Context* SkLinearGradient::onCreateContext(const ContextRec& rec, void* storage) const { 86 return SkNEW_PLACEMENT_ARGS(storage, LinearGradientContext, (*this, rec)); 87} 88 89SkLinearGradient::LinearGradientContext::LinearGradientContext( 90 const SkLinearGradient& shader, const ContextRec& rec) 91 : INHERITED(shader, rec) 92{ 93 unsigned mask = SkMatrix::kTranslate_Mask | SkMatrix::kScale_Mask; 94 if ((fDstToIndex.getType() & ~mask) == 0) { 95 // when we dither, we are (usually) not const-in-Y 96 if ((fFlags & SkShader::kHasSpan16_Flag) && !rec.fPaint->isDither()) { 97 // only claim this if we do have a 16bit mode (i.e. none of our 98 // colors have alpha), and if we are not dithering (which obviously 99 // is not const in Y). 100 fFlags |= SkShader::kConstInY16_Flag; 101 } 102 } 103} 104 105#define NO_CHECK_ITER \ 106 do { \ 107 unsigned fi = SkGradFixedToFixed(fx) >> SkGradientShaderBase::kCache32Shift; \ 108 SkASSERT(fi <= 0xFF); \ 109 fx += dx; \ 110 *dstC++ = cache[toggle + fi]; \ 111 toggle = next_dither_toggle(toggle); \ 112 } while (0) 113 114namespace { 115 116typedef void (*LinearShadeProc)(TileProc proc, SkGradFixed dx, SkGradFixed fx, 117 SkPMColor* dstC, const SkPMColor* cache, 118 int toggle, int count); 119 120// Linear interpolation (lerp) is unnecessary if there are no sharp 121// discontinuities in the gradient - which must be true if there are 122// only 2 colors - but it's cheap. 123void shadeSpan_linear_vertical_lerp(TileProc proc, SkGradFixed dx, SkGradFixed fx, 124 SkPMColor* SK_RESTRICT dstC, 125 const SkPMColor* SK_RESTRICT cache, 126 int toggle, int count) { 127 // We're a vertical gradient, so no change in a span. 128 // If colors change sharply across the gradient, dithering is 129 // insufficient (it subsamples the color space) and we need to lerp. 130 unsigned fullIndex = proc(SkGradFixedToFixed(fx)); 131 unsigned fi = fullIndex >> SkGradientShaderBase::kCache32Shift; 132 unsigned remainder = fullIndex & ((1 << SkGradientShaderBase::kCache32Shift) - 1); 133 134 int index0 = fi + toggle; 135 int index1 = index0; 136 if (fi < SkGradientShaderBase::kCache32Count - 1) { 137 index1 += 1; 138 } 139 SkPMColor lerp = SkFastFourByteInterp(cache[index1], cache[index0], remainder); 140 index0 ^= SkGradientShaderBase::kDitherStride32; 141 index1 ^= SkGradientShaderBase::kDitherStride32; 142 SkPMColor dlerp = SkFastFourByteInterp(cache[index1], cache[index0], remainder); 143 sk_memset32_dither(dstC, lerp, dlerp, count); 144} 145 146void shadeSpan_linear_clamp(TileProc proc, SkGradFixed dx, SkGradFixed fx, 147 SkPMColor* SK_RESTRICT dstC, 148 const SkPMColor* SK_RESTRICT cache, 149 int toggle, int count) { 150 SkClampRange range; 151 range.init(fx, dx, count, 0, SkGradientShaderBase::kCache32Count - 1); 152 range.validate(count); 153 154 if ((count = range.fCount0) > 0) { 155 sk_memset32_dither(dstC, 156 cache[toggle + range.fV0], 157 cache[next_dither_toggle(toggle) + range.fV0], 158 count); 159 dstC += count; 160 } 161 if ((count = range.fCount1) > 0) { 162 int unroll = count >> 3; 163 fx = range.fFx1; 164 for (int i = 0; i < unroll; i++) { 165 NO_CHECK_ITER; NO_CHECK_ITER; 166 NO_CHECK_ITER; NO_CHECK_ITER; 167 NO_CHECK_ITER; NO_CHECK_ITER; 168 NO_CHECK_ITER; NO_CHECK_ITER; 169 } 170 if ((count &= 7) > 0) { 171 do { 172 NO_CHECK_ITER; 173 } while (--count != 0); 174 } 175 } 176 if ((count = range.fCount2) > 0) { 177 sk_memset32_dither(dstC, 178 cache[toggle + range.fV1], 179 cache[next_dither_toggle(toggle) + range.fV1], 180 count); 181 } 182} 183 184void shadeSpan_linear_mirror(TileProc proc, SkGradFixed dx, SkGradFixed fx, 185 SkPMColor* SK_RESTRICT dstC, 186 const SkPMColor* SK_RESTRICT cache, 187 int toggle, int count) { 188 do { 189 unsigned fi = mirror_8bits(SkGradFixedToFixed(fx) >> 8); 190 SkASSERT(fi <= 0xFF); 191 fx += dx; 192 *dstC++ = cache[toggle + fi]; 193 toggle = next_dither_toggle(toggle); 194 } while (--count != 0); 195} 196 197void shadeSpan_linear_repeat(TileProc proc, SkGradFixed dx, SkGradFixed fx, 198 SkPMColor* SK_RESTRICT dstC, 199 const SkPMColor* SK_RESTRICT cache, 200 int toggle, int count) { 201 do { 202 unsigned fi = repeat_8bits(SkGradFixedToFixed(fx) >> 8); 203 SkASSERT(fi <= 0xFF); 204 fx += dx; 205 *dstC++ = cache[toggle + fi]; 206 toggle = next_dither_toggle(toggle); 207 } while (--count != 0); 208} 209 210} 211 212void SkLinearGradient::LinearGradientContext::shadeSpan(int x, int y, SkPMColor* SK_RESTRICT dstC, 213 int count) { 214 SkASSERT(count > 0); 215 216 const SkLinearGradient& linearGradient = static_cast<const SkLinearGradient&>(fShader); 217 218 SkPoint srcPt; 219 SkMatrix::MapXYProc dstProc = fDstToIndexProc; 220 TileProc proc = linearGradient.fTileProc; 221 const SkPMColor* SK_RESTRICT cache = fCache->getCache32(); 222 int toggle = init_dither_toggle(x, y); 223 224 if (fDstToIndexClass != kPerspective_MatrixClass) { 225 dstProc(fDstToIndex, SkIntToScalar(x) + SK_ScalarHalf, 226 SkIntToScalar(y) + SK_ScalarHalf, &srcPt); 227 SkGradFixed dx, fx = SkScalarToGradFixed(srcPt.fX); 228 229 if (fDstToIndexClass == kFixedStepInX_MatrixClass) { 230 SkFixed dxStorage[1]; 231 (void)fDstToIndex.fixedStepInX(SkIntToScalar(y), dxStorage, NULL); 232 // todo: do we need a real/high-precision value for dx here? 233 dx = SkFixedToGradFixed(dxStorage[0]); 234 } else { 235 SkASSERT(fDstToIndexClass == kLinear_MatrixClass); 236 dx = SkScalarToGradFixed(fDstToIndex.getScaleX()); 237 } 238 239 LinearShadeProc shadeProc = shadeSpan_linear_repeat; 240 if (0 == dx) { 241 shadeProc = shadeSpan_linear_vertical_lerp; 242 } else if (SkShader::kClamp_TileMode == linearGradient.fTileMode) { 243 shadeProc = shadeSpan_linear_clamp; 244 } else if (SkShader::kMirror_TileMode == linearGradient.fTileMode) { 245 shadeProc = shadeSpan_linear_mirror; 246 } else { 247 SkASSERT(SkShader::kRepeat_TileMode == linearGradient.fTileMode); 248 } 249 (*shadeProc)(proc, dx, fx, dstC, cache, toggle, count); 250 } else { 251 SkScalar dstX = SkIntToScalar(x); 252 SkScalar dstY = SkIntToScalar(y); 253 do { 254 dstProc(fDstToIndex, dstX, dstY, &srcPt); 255 unsigned fi = proc(SkScalarToFixed(srcPt.fX)); 256 SkASSERT(fi <= 0xFFFF); 257 *dstC++ = cache[toggle + (fi >> kCache32Shift)]; 258 toggle = next_dither_toggle(toggle); 259 dstX += SK_Scalar1; 260 } while (--count != 0); 261 } 262} 263 264SkShader::BitmapType SkLinearGradient::asABitmap(SkBitmap* bitmap, 265 SkMatrix* matrix, 266 TileMode xy[]) const { 267 if (bitmap) { 268 this->getGradientTableBitmap(bitmap); 269 } 270 if (matrix) { 271 matrix->preConcat(fPtsToUnit); 272 } 273 if (xy) { 274 xy[0] = fTileMode; 275 xy[1] = kClamp_TileMode; 276 } 277 return kLinear_BitmapType; 278} 279 280SkShader::GradientType SkLinearGradient::asAGradient(GradientInfo* info) const { 281 if (info) { 282 commonAsAGradient(info); 283 info->fPoint[0] = fStart; 284 info->fPoint[1] = fEnd; 285 } 286 return kLinear_GradientType; 287} 288 289static void dither_memset16(uint16_t dst[], uint16_t value, uint16_t other, 290 int count) { 291 if (reinterpret_cast<uintptr_t>(dst) & 2) { 292 *dst++ = value; 293 count -= 1; 294 SkTSwap(value, other); 295 } 296 297 sk_memset32((uint32_t*)dst, (value << 16) | other, count >> 1); 298 299 if (count & 1) { 300 dst[count - 1] = value; 301 } 302} 303 304#define NO_CHECK_ITER_16 \ 305 do { \ 306 unsigned fi = SkGradFixedToFixed(fx) >> SkGradientShaderBase::kCache16Shift; \ 307 SkASSERT(fi < SkGradientShaderBase::kCache16Count); \ 308 fx += dx; \ 309 *dstC++ = cache[toggle + fi]; \ 310 toggle = next_dither_toggle16(toggle); \ 311 } while (0) 312 313namespace { 314 315typedef void (*LinearShade16Proc)(TileProc proc, SkGradFixed dx, SkGradFixed fx, 316 uint16_t* dstC, const uint16_t* cache, 317 int toggle, int count); 318 319void shadeSpan16_linear_vertical(TileProc proc, SkGradFixed dx, SkGradFixed fx, 320 uint16_t* SK_RESTRICT dstC, 321 const uint16_t* SK_RESTRICT cache, 322 int toggle, int count) { 323 // we're a vertical gradient, so no change in a span 324 unsigned fi = proc(SkGradFixedToFixed(fx)) >> SkGradientShaderBase::kCache16Shift; 325 SkASSERT(fi < SkGradientShaderBase::kCache16Count); 326 dither_memset16(dstC, cache[toggle + fi], 327 cache[next_dither_toggle16(toggle) + fi], count); 328} 329 330void shadeSpan16_linear_clamp(TileProc proc, SkGradFixed dx, SkGradFixed fx, 331 uint16_t* SK_RESTRICT dstC, 332 const uint16_t* SK_RESTRICT cache, 333 int toggle, int count) { 334 SkClampRange range; 335 range.init(fx, dx, count, 0, SkGradientShaderBase::kCache32Count - 1); 336 range.validate(count); 337 338 if ((count = range.fCount0) > 0) { 339 dither_memset16(dstC, 340 cache[toggle + range.fV0], 341 cache[next_dither_toggle16(toggle) + range.fV0], 342 count); 343 dstC += count; 344 } 345 if ((count = range.fCount1) > 0) { 346 int unroll = count >> 3; 347 fx = range.fFx1; 348 for (int i = 0; i < unroll; i++) { 349 NO_CHECK_ITER_16; NO_CHECK_ITER_16; 350 NO_CHECK_ITER_16; NO_CHECK_ITER_16; 351 NO_CHECK_ITER_16; NO_CHECK_ITER_16; 352 NO_CHECK_ITER_16; NO_CHECK_ITER_16; 353 } 354 if ((count &= 7) > 0) { 355 do { 356 NO_CHECK_ITER_16; 357 } while (--count != 0); 358 } 359 } 360 if ((count = range.fCount2) > 0) { 361 dither_memset16(dstC, 362 cache[toggle + range.fV1], 363 cache[next_dither_toggle16(toggle) + range.fV1], 364 count); 365 } 366} 367 368void shadeSpan16_linear_mirror(TileProc proc, SkGradFixed dx, SkGradFixed fx, 369 uint16_t* SK_RESTRICT dstC, 370 const uint16_t* SK_RESTRICT cache, 371 int toggle, int count) { 372 do { 373 unsigned fi = mirror_bits(SkGradFixedToFixed(fx) >> SkGradientShaderBase::kCache16Shift, 374 SkGradientShaderBase::kCache16Bits); 375 SkASSERT(fi < SkGradientShaderBase::kCache16Count); 376 fx += dx; 377 *dstC++ = cache[toggle + fi]; 378 toggle = next_dither_toggle16(toggle); 379 } while (--count != 0); 380} 381 382void shadeSpan16_linear_repeat(TileProc proc, SkGradFixed dx, SkGradFixed fx, 383 uint16_t* SK_RESTRICT dstC, 384 const uint16_t* SK_RESTRICT cache, 385 int toggle, int count) { 386 do { 387 unsigned fi = repeat_bits(SkGradFixedToFixed(fx) >> SkGradientShaderBase::kCache16Shift, 388 SkGradientShaderBase::kCache16Bits); 389 SkASSERT(fi < SkGradientShaderBase::kCache16Count); 390 fx += dx; 391 *dstC++ = cache[toggle + fi]; 392 toggle = next_dither_toggle16(toggle); 393 } while (--count != 0); 394} 395} 396 397static bool fixed_nearly_zero(SkFixed x) { 398 return SkAbs32(x) < (SK_Fixed1 >> 12); 399} 400 401void SkLinearGradient::LinearGradientContext::shadeSpan16(int x, int y, 402 uint16_t* SK_RESTRICT dstC, int count) { 403 SkASSERT(count > 0); 404 405 const SkLinearGradient& linearGradient = static_cast<const SkLinearGradient&>(fShader); 406 407 SkPoint srcPt; 408 SkMatrix::MapXYProc dstProc = fDstToIndexProc; 409 TileProc proc = linearGradient.fTileProc; 410 const uint16_t* SK_RESTRICT cache = fCache->getCache16(); 411 int toggle = init_dither_toggle16(x, y); 412 413 if (fDstToIndexClass != kPerspective_MatrixClass) { 414 dstProc(fDstToIndex, SkIntToScalar(x) + SK_ScalarHalf, 415 SkIntToScalar(y) + SK_ScalarHalf, &srcPt); 416 SkGradFixed dx, fx = SkScalarToGradFixed(srcPt.fX); 417 418 if (fDstToIndexClass == kFixedStepInX_MatrixClass) { 419 SkFixed dxStorage[1]; 420 (void)fDstToIndex.fixedStepInX(SkIntToScalar(y), dxStorage, NULL); 421 // todo: do we need a real/high-precision value for dx here? 422 dx = SkFixedToGradFixed(dxStorage[0]); 423 } else { 424 SkASSERT(fDstToIndexClass == kLinear_MatrixClass); 425 dx = SkScalarToGradFixed(fDstToIndex.getScaleX()); 426 } 427 428 LinearShade16Proc shadeProc = shadeSpan16_linear_repeat; 429 if (fixed_nearly_zero(SkGradFixedToFixed(dx))) { 430 shadeProc = shadeSpan16_linear_vertical; 431 } else if (SkShader::kClamp_TileMode == linearGradient.fTileMode) { 432 shadeProc = shadeSpan16_linear_clamp; 433 } else if (SkShader::kMirror_TileMode == linearGradient.fTileMode) { 434 shadeProc = shadeSpan16_linear_mirror; 435 } else { 436 SkASSERT(SkShader::kRepeat_TileMode == linearGradient.fTileMode); 437 } 438 (*shadeProc)(proc, dx, fx, dstC, cache, toggle, count); 439 } else { 440 SkScalar dstX = SkIntToScalar(x); 441 SkScalar dstY = SkIntToScalar(y); 442 do { 443 dstProc(fDstToIndex, dstX, dstY, &srcPt); 444 unsigned fi = proc(SkScalarToFixed(srcPt.fX)); 445 SkASSERT(fi <= 0xFFFF); 446 447 int index = fi >> kCache16Shift; 448 *dstC++ = cache[toggle + index]; 449 toggle = next_dither_toggle16(toggle); 450 451 dstX += SK_Scalar1; 452 } while (--count != 0); 453 } 454} 455 456#if SK_SUPPORT_GPU 457 458#include "gl/builders/GrGLProgramBuilder.h" 459#include "SkGr.h" 460 461///////////////////////////////////////////////////////////////////// 462 463class GrGLLinearGradient : public GrGLGradientEffect { 464public: 465 466 GrGLLinearGradient(const GrProcessor&) {} 467 468 virtual ~GrGLLinearGradient() { } 469 470 virtual void emitCode(GrGLFPBuilder*, 471 const GrFragmentProcessor&, 472 const char* outputColor, 473 const char* inputColor, 474 const TransformedCoordsArray&, 475 const TextureSamplerArray&) override; 476 477 static void GenKey(const GrProcessor& processor, const GrGLSLCaps&, GrProcessorKeyBuilder* b) { 478 b->add32(GenBaseGradientKey(processor)); 479 } 480 481private: 482 483 typedef GrGLGradientEffect INHERITED; 484}; 485 486///////////////////////////////////////////////////////////////////// 487 488class GrLinearGradient : public GrGradientEffect { 489public: 490 491 static GrFragmentProcessor* Create(GrContext* ctx, 492 const SkLinearGradient& shader, 493 const SkMatrix& matrix, 494 SkShader::TileMode tm) { 495 return SkNEW_ARGS(GrLinearGradient, (ctx, shader, matrix, tm)); 496 } 497 498 virtual ~GrLinearGradient() { } 499 500 const char* name() const override { return "Linear Gradient"; } 501 502 virtual void getGLProcessorKey(const GrGLSLCaps& caps, 503 GrProcessorKeyBuilder* b) const override { 504 GrGLLinearGradient::GenKey(*this, caps, b); 505 } 506 507 GrGLFragmentProcessor* createGLInstance() const override { 508 return SkNEW_ARGS(GrGLLinearGradient, (*this)); 509 } 510 511private: 512 GrLinearGradient(GrContext* ctx, 513 const SkLinearGradient& shader, 514 const SkMatrix& matrix, 515 SkShader::TileMode tm) 516 : INHERITED(ctx, shader, matrix, tm) { 517 this->initClassID<GrLinearGradient>(); 518 } 519 GR_DECLARE_FRAGMENT_PROCESSOR_TEST; 520 521 typedef GrGradientEffect INHERITED; 522}; 523 524///////////////////////////////////////////////////////////////////// 525 526GR_DEFINE_FRAGMENT_PROCESSOR_TEST(GrLinearGradient); 527 528GrFragmentProcessor* GrLinearGradient::TestCreate(SkRandom* random, 529 GrContext* context, 530 const GrDrawTargetCaps&, 531 GrTexture**) { 532 SkPoint points[] = {{random->nextUScalar1(), random->nextUScalar1()}, 533 {random->nextUScalar1(), random->nextUScalar1()}}; 534 535 SkColor colors[kMaxRandomGradientColors]; 536 SkScalar stopsArray[kMaxRandomGradientColors]; 537 SkScalar* stops = stopsArray; 538 SkShader::TileMode tm; 539 int colorCount = RandomGradientParams(random, colors, &stops, &tm); 540 SkAutoTUnref<SkShader> shader(SkGradientShader::CreateLinear(points, 541 colors, stops, colorCount, 542 tm)); 543 SkPaint paint; 544 GrColor paintColor; 545 GrFragmentProcessor* fp; 546 SkAssertResult(shader->asFragmentProcessor(context, paint, 547 GrTest::TestMatrix(random), NULL, 548 &paintColor, &fp)); 549 return fp; 550} 551 552///////////////////////////////////////////////////////////////////// 553 554void GrGLLinearGradient::emitCode(GrGLFPBuilder* builder, 555 const GrFragmentProcessor& fp, 556 const char* outputColor, 557 const char* inputColor, 558 const TransformedCoordsArray& coords, 559 const TextureSamplerArray& samplers) { 560 const GrLinearGradient& ge = fp.cast<GrLinearGradient>(); 561 this->emitUniforms(builder, ge); 562 SkString t = builder->getFragmentShaderBuilder()->ensureFSCoords2D(coords, 0); 563 t.append(".x"); 564 this->emitColor(builder, ge, t.c_str(), outputColor, inputColor, samplers); 565} 566 567///////////////////////////////////////////////////////////////////// 568 569bool SkLinearGradient::asFragmentProcessor(GrContext* context, const SkPaint& paint, 570 const SkMatrix& viewm, const SkMatrix* localMatrix, 571 GrColor* paintColor, GrFragmentProcessor** fp) const { 572 SkASSERT(context); 573 574 SkMatrix matrix; 575 if (!this->getLocalMatrix().invert(&matrix)) { 576 return false; 577 } 578 if (localMatrix) { 579 SkMatrix inv; 580 if (!localMatrix->invert(&inv)) { 581 return false; 582 } 583 matrix.postConcat(inv); 584 } 585 matrix.postConcat(fPtsToUnit); 586 587 *paintColor = SkColor2GrColorJustAlpha(paint.getColor()); 588 *fp = GrLinearGradient::Create(context, *this, matrix, fTileMode); 589 590 return true; 591} 592 593#else 594 595bool SkLinearGradient::asFragmentProcessor(GrContext*, const SkPaint&, const SkMatrix&, 596 const SkMatrix*, GrColor*, 597 GrFragmentProcessor**) const { 598 SkDEBUGFAIL("Should not call in GPU-less build"); 599 return false; 600} 601 602#endif 603 604#ifndef SK_IGNORE_TO_STRING 605void SkLinearGradient::toString(SkString* str) const { 606 str->append("SkLinearGradient ("); 607 608 str->appendf("start: (%f, %f)", fStart.fX, fStart.fY); 609 str->appendf(" end: (%f, %f) ", fEnd.fX, fEnd.fY); 610 611 this->INHERITED::toString(str); 612 613 str->append(")"); 614} 615#endif 616