| /* | 
 |  * Copyright 2013 Google Inc. | 
 |  * | 
 |  * Use of this source code is governed by a BSD-style license that can be | 
 |  * found in the LICENSE file. | 
 |  */ | 
 |  | 
 | // This test only works with the GPU backend. | 
 |  | 
 | #include "gm/gm.h" | 
 | #include "include/core/SkBlendMode.h" | 
 | #include "include/core/SkCanvas.h" | 
 | #include "include/core/SkMatrix.h" | 
 | #include "include/core/SkPaint.h" | 
 | #include "include/core/SkPoint.h" | 
 | #include "include/core/SkPoint3.h" | 
 | #include "include/core/SkRect.h" | 
 | #include "include/core/SkRefCnt.h" | 
 | #include "include/core/SkScalar.h" | 
 | #include "include/core/SkSize.h" | 
 | #include "include/core/SkString.h" | 
 | #include "include/core/SkTypes.h" | 
 | #include "include/gpu/GrRecordingContext.h" | 
 | #include "include/private/GrSharedEnums.h" | 
 | #include "include/private/GrTypesPriv.h" | 
 | #include "include/private/SkColorData.h" | 
 | #include "include/utils/SkRandom.h" | 
 | #include "src/core/SkGeometry.h" | 
 | #include "src/core/SkPointPriv.h" | 
 | #include "src/gpu/GrCaps.h" | 
 | #include "src/gpu/GrDirectContextPriv.h" | 
 | #include "src/gpu/GrGeometryProcessor.h" | 
 | #include "src/gpu/GrMemoryPool.h" | 
 | #include "src/gpu/GrOpFlushState.h" | 
 | #include "src/gpu/GrOpsRenderPass.h" | 
 | #include "src/gpu/GrPaint.h" | 
 | #include "src/gpu/GrProcessorAnalysis.h" | 
 | #include "src/gpu/GrProcessorSet.h" | 
 | #include "src/gpu/GrProgramInfo.h" | 
 | #include "src/gpu/GrRecordingContextPriv.h" | 
 | #include "src/gpu/GrSurfaceDrawContext.h" | 
 | #include "src/gpu/GrUserStencilSettings.h" | 
 | #include "src/gpu/effects/GrBezierEffect.h" | 
 | #include "src/gpu/effects/GrPorterDuffXferProcessor.h" | 
 | #include "src/gpu/geometry/GrPathUtils.h" | 
 | #include "src/gpu/ops/GrDrawOp.h" | 
 | #include "src/gpu/ops/GrMeshDrawOp.h" | 
 | #include "src/gpu/ops/GrOp.h" | 
 | #include "src/gpu/ops/GrSimpleMeshDrawOpHelper.h" | 
 |  | 
 | #include <memory> | 
 | #include <utility> | 
 |  | 
 | class GrAppliedClip; | 
 |  | 
 | namespace skiagm { | 
 |  | 
 | class BezierTestOp : public GrMeshDrawOp { | 
 | public: | 
 |     FixedFunctionFlags fixedFunctionFlags() const override { return FixedFunctionFlags::kNone; } | 
 |  | 
 |     GrProcessorSet::Analysis finalize( | 
 |             const GrCaps& caps, const GrAppliedClip* clip, bool hasMixedSampledCoverage, | 
 |             GrClampType clampType) override { | 
 |         return fProcessorSet.finalize( | 
 |                 fColor, GrProcessorAnalysisCoverage::kSingleChannel, clip, | 
 |                 &GrUserStencilSettings::kUnused, hasMixedSampledCoverage, caps, clampType, &fColor); | 
 |     } | 
 |  | 
 |     void visitProxies(const VisitProxyFunc& func) const override { | 
 |         if (fProgramInfo) { | 
 |             fProgramInfo->visitFPProxies(func); | 
 |         } else { | 
 |             fProcessorSet.visitProxies(func); | 
 |         } | 
 |     } | 
 |  | 
 | protected: | 
 |     BezierTestOp(const SkRect& rect, const SkPMColor4f& color, int32_t classID) | 
 |             : INHERITED(classID) | 
 |             , fRect(rect) | 
 |             , fColor(color) | 
 |             , fProcessorSet(SkBlendMode::kSrc) { | 
 |         this->setBounds(rect, HasAABloat::kYes, IsHairline::kNo); | 
 |     } | 
 |  | 
 |     virtual GrGeometryProcessor* makeGP(const GrCaps& caps, SkArenaAlloc* arena) = 0; | 
 |  | 
 |     GrProgramInfo* programInfo() override { return fProgramInfo; } | 
 |  | 
 |     void onCreateProgramInfo(const GrCaps* caps, | 
 |                              SkArenaAlloc* arena, | 
 |                              const GrSurfaceProxyView& writeView, | 
 |                              GrAppliedClip&& appliedClip, | 
 |                              const GrXferProcessor::DstProxyView& dstProxyView, | 
 |                              GrXferBarrierFlags renderPassXferBarriers, | 
 |                              GrLoadOp colorLoadOp) override { | 
 |         auto gp = this->makeGP(*caps, arena); | 
 |         if (!gp) { | 
 |             return; | 
 |         } | 
 |  | 
 |         GrPipeline::InputFlags flags = GrPipeline::InputFlags::kNone; | 
 |  | 
 |         fProgramInfo = GrSimpleMeshDrawOpHelper::CreateProgramInfo(caps, arena, writeView, | 
 |                                                                    std::move(appliedClip), | 
 |                                                                    dstProxyView, gp, | 
 |                                                                    std::move(fProcessorSet), | 
 |                                                                    GrPrimitiveType::kTriangles, | 
 |                                                                    renderPassXferBarriers, | 
 |                                                                    colorLoadOp, | 
 |                                                                    flags); | 
 |     } | 
 |  | 
 |     void onExecute(GrOpFlushState* flushState, const SkRect& chainBounds) final { | 
 |         if (!fProgramInfo) { | 
 |             this->createProgramInfo(flushState); | 
 |         } | 
 |  | 
 |         if (!fProgramInfo) { | 
 |             return; | 
 |         } | 
 |  | 
 |         flushState->bindPipelineAndScissorClip(*fProgramInfo, chainBounds); | 
 |         flushState->bindTextures(fProgramInfo->geomProc(), nullptr, fProgramInfo->pipeline()); | 
 |         flushState->drawMesh(*fMesh); | 
 |     } | 
 |  | 
 |     const SkRect& rect() const { return fRect; } | 
 |     const SkPMColor4f& color() const { return fColor; } | 
 |  | 
 | protected: | 
 |     GrSimpleMesh*        fMesh = nullptr;  // filled in by the derived classes | 
 |  | 
 | private: | 
 |     SkRect               fRect; | 
 |     SkPMColor4f          fColor; | 
 |     GrProcessorSet       fProcessorSet; | 
 |     GrProgramInfo*       fProgramInfo = nullptr; | 
 |  | 
 |     using INHERITED = GrMeshDrawOp; | 
 | }; | 
 |  | 
 | /** | 
 |  * This GM directly exercises effects that draw Bezier curves in the GPU backend. | 
 |  */ | 
 | class BezierConicTestOp : public BezierTestOp { | 
 | public: | 
 |     DEFINE_OP_CLASS_ID | 
 |  | 
 |     const char* name() const final { return "BezierConicTestOp"; } | 
 |  | 
 |     static GrOp::Owner Make(GrRecordingContext* context, | 
 |                             const SkRect& rect, | 
 |                             const SkPMColor4f& color, | 
 |                             const SkMatrix& klm) { | 
 |         return GrOp::Make<BezierConicTestOp>(context, rect, color, klm); | 
 |     } | 
 |  | 
 | private: | 
 |     friend class ::GrOp; // for ctor | 
 |  | 
 |     BezierConicTestOp(const SkRect& rect, const SkPMColor4f& color, const SkMatrix& klm) | 
 |             : INHERITED(rect, color, ClassID()) | 
 |             , fKLM(klm) {} | 
 |  | 
 |     struct Vertex { | 
 |         SkPoint fPosition; | 
 |         float   fKLM[4]; // The last value is ignored. The effect expects a vec4f. | 
 |     }; | 
 |  | 
 |     GrGeometryProcessor* makeGP(const GrCaps& caps, SkArenaAlloc* arena) final { | 
 |         auto tmp = GrConicEffect::Make(arena, this->color(), SkMatrix::I(), caps, SkMatrix::I(), | 
 |                                        false); | 
 |         if (!tmp) { | 
 |             return nullptr; | 
 |         } | 
 |         SkASSERT(tmp->vertexStride() == sizeof(Vertex)); | 
 |         return tmp; | 
 |     } | 
 |  | 
 |     void onPrepareDraws(Target* target) final { | 
 |         QuadHelper helper(target, sizeof(Vertex), 1); | 
 |         Vertex* verts = reinterpret_cast<Vertex*>(helper.vertices()); | 
 |         if (!verts) { | 
 |             return; | 
 |         } | 
 |         SkRect rect = this->rect(); | 
 |         SkPointPriv::SetRectTriStrip(&verts[0].fPosition, rect, sizeof(Vertex)); | 
 |         for (int v = 0; v < 4; ++v) { | 
 |             SkPoint3 pt3 = {verts[v].fPosition.x(), verts[v].fPosition.y(), 1.f}; | 
 |             fKLM.mapHomogeneousPoints((SkPoint3* ) verts[v].fKLM, &pt3, 1); | 
 |         } | 
 |  | 
 |         fMesh = helper.mesh(); | 
 |     } | 
 |  | 
 |     SkMatrix fKLM; | 
 |  | 
 |     static constexpr int kVertsPerCubic = 4; | 
 |     static constexpr int kIndicesPerCubic = 6; | 
 |  | 
 |     using INHERITED = BezierTestOp; | 
 | }; | 
 |  | 
 |  | 
 | /** | 
 |  * This GM directly exercises effects that draw Bezier curves in the GPU backend. | 
 |  */ | 
 | class BezierConicEffects : public GpuGM { | 
 | public: | 
 |     BezierConicEffects() { | 
 |         this->setBGColor(0xFFFFFFFF); | 
 |     } | 
 |  | 
 | protected: | 
 |     static const int kNumConics = 10; | 
 |     static const int kCellWidth = 128; | 
 |     static const int kCellHeight = 128; | 
 |  | 
 |     SkString onShortName() override { | 
 |         return SkString("bezier_conic_effects"); | 
 |     } | 
 |  | 
 |     SkISize onISize() override { | 
 |         return SkISize::Make(kCellWidth, kNumConics*kCellHeight); | 
 |     } | 
 |  | 
 |     void onDraw(GrRecordingContext* context, GrSurfaceDrawContext* surfaceDrawContext, | 
 |                 SkCanvas* canvas) override { | 
 |  | 
 |         const SkScalar w = kCellWidth, h = kCellHeight; | 
 |         const SkPMColor4f kOpaqueBlack = SkPMColor4f::FromBytes_RGBA(0xff000000); | 
 |  | 
 |         const SkPoint baseControlPts[kNumConics][3] = { | 
 |             { { 0.31f * w, 0.01f * h}, { 0.48f * w, 0.74f * h }, { 0.19f * w, 0.33f * h } }, | 
 |             { { 0.00f * w, 0.07f * h}, { 0.30f * w, 0.70f * h }, { 0.47f * w, 0.37f * h } }, | 
 |             { { 0.15f * w, 0.23f * h}, { 0.49f * w, 0.87f * h }, { 0.85f * w, 0.66f * h } }, | 
 |             { { 0.09f * w, 0.15f * h}, { 0.42f * w, 0.33f * h }, { 0.17f * w, 0.38f * h } }, | 
 |             { { 0.98f * w, 0.54f * h}, { 0.83f * w, 0.91f * h }, { 0.62f * w, 0.40f * h } }, | 
 |             { { 0.96f * w, 0.65f * h}, { 0.03f * w, 0.79f * h }, { 0.24f * w, 0.56f * h } }, | 
 |             { { 0.57f * w, 0.12f * h}, { 0.33f * w, 0.67f * h }, { 0.59f * w, 0.33f * h } }, | 
 |             { { 0.12f * w, 0.72f * h}, { 0.69f * w, 0.85f * h }, { 0.46f * w, 0.32f * h } }, | 
 |             { { 0.27f * w, 0.49f * h}, { 0.41f * w, 0.02f * h }, { 0.11f * w, 0.42f * h } }, | 
 |             { { 0.40f * w, 0.13f * h}, { 0.83f * w, 0.30f * h }, { 0.31f * w, 0.68f * h } }, | 
 |         }; | 
 |         const SkScalar weights[kNumConics] = { 0.62f, 0.01f, 0.95f, 1.48f, 0.37f, | 
 |                                                0.66f, 0.15f, 0.14f, 0.61f, 1.4f }; | 
 |  | 
 |         SkPaint ctrlPtPaint; | 
 |         ctrlPtPaint.setColor(SK_ColorRED); | 
 |  | 
 |         SkPaint choppedPtPaint; | 
 |         choppedPtPaint.setColor(~ctrlPtPaint.getColor() | 0xFF000000); | 
 |  | 
 |         SkPaint polyPaint; | 
 |         polyPaint.setColor(0xffA0A0A0); | 
 |         polyPaint.setStrokeWidth(0); | 
 |         polyPaint.setStyle(SkPaint::kStroke_Style); | 
 |  | 
 |         SkPaint boundsPaint; | 
 |         boundsPaint.setColor(0xff808080); | 
 |         boundsPaint.setStrokeWidth(0); | 
 |         boundsPaint.setStyle(SkPaint::kStroke_Style); | 
 |  | 
 |  | 
 |         for (int row = 0; row < kNumConics; ++row) { | 
 |             SkScalar x = 0; | 
 |             SkScalar y = row * h; | 
 |             SkPoint controlPts[] = { | 
 |                 {x + baseControlPts[row][0].fX, y + baseControlPts[row][0].fY}, | 
 |                 {x + baseControlPts[row][1].fX, y + baseControlPts[row][1].fY}, | 
 |                 {x + baseControlPts[row][2].fX, y + baseControlPts[row][2].fY} | 
 |             }; | 
 |  | 
 |             for (int i = 0; i < 3; ++i) { | 
 |                 canvas->drawCircle(controlPts[i], 6.f, ctrlPtPaint); | 
 |             } | 
 |  | 
 |             canvas->drawPoints(SkCanvas::kPolygon_PointMode, 3, controlPts, polyPaint); | 
 |  | 
 |             SkConic dst[4]; | 
 |             SkMatrix klm; | 
 |             int cnt = ChopConic(controlPts, dst, weights[row]); | 
 |             GrPathUtils::getConicKLM(controlPts, weights[row], &klm); | 
 |  | 
 |             for (int c = 0; c < cnt; ++c) { | 
 |                 SkPoint* pts = dst[c].fPts; | 
 |                 for (int i = 0; i < 3; ++i) { | 
 |                     canvas->drawCircle(pts[i], 3.f, choppedPtPaint); | 
 |                 } | 
 |  | 
 |                 SkRect bounds; | 
 |                 bounds.setBounds(pts, 3); | 
 |  | 
 |                 canvas->drawRect(bounds, boundsPaint); | 
 |  | 
 |                 GrOp::Owner op = BezierConicTestOp::Make(context, bounds, | 
 |                                                          kOpaqueBlack, klm); | 
 |                 surfaceDrawContext->addDrawOp(std::move(op)); | 
 |             } | 
 |         } | 
 |     } | 
 |  | 
 | private: | 
 |     // Uses the max curvature function for quads to estimate | 
 |     // where to chop the conic. If the max curvature is not | 
 |     // found along the curve segment it will return 1 and | 
 |     // dst[0] is the original conic. If it returns 2 the dst[0] | 
 |     // and dst[1] are the two new conics. | 
 |     static int SplitConic(const SkPoint src[3], SkConic dst[2], const SkScalar weight) { | 
 |         SkScalar t = SkFindQuadMaxCurvature(src); | 
 |         if (t == 0 || t == 1) { | 
 |             if (dst) { | 
 |                 dst[0].set(src, weight); | 
 |             } | 
 |             return 1; | 
 |         } else { | 
 |             if (dst) { | 
 |                 SkConic conic; | 
 |                 conic.set(src, weight); | 
 |                 if (!conic.chopAt(t, dst)) { | 
 |                     dst[0].set(src, weight); | 
 |                     return 1; | 
 |                 } | 
 |             } | 
 |             return 2; | 
 |         } | 
 |     } | 
 |  | 
 |     // Calls SplitConic on the entire conic and then once more on each subsection. | 
 |     // Most cases will result in either 1 conic (chop point is not within t range) | 
 |     // or 3 points (split once and then one subsection is split again). | 
 |     static int ChopConic(const SkPoint src[3], SkConic dst[4], const SkScalar weight) { | 
 |         SkConic dstTemp[2]; | 
 |         int conicCnt = SplitConic(src, dstTemp, weight); | 
 |         if (2 == conicCnt) { | 
 |             int conicCnt2 = SplitConic(dstTemp[0].fPts, dst, dstTemp[0].fW); | 
 |             conicCnt = conicCnt2 + SplitConic(dstTemp[1].fPts, &dst[conicCnt2], dstTemp[1].fW); | 
 |         } else { | 
 |             dst[0] = dstTemp[0]; | 
 |         } | 
 |         return conicCnt; | 
 |     } | 
 |  | 
 |     using INHERITED = GM; | 
 | }; | 
 |  | 
 | ////////////////////////////////////////////////////////////////////////////// | 
 |  | 
 | class BezierQuadTestOp : public BezierTestOp { | 
 | public: | 
 |     DEFINE_OP_CLASS_ID | 
 |     const char* name() const override { return "BezierQuadTestOp"; } | 
 |  | 
 |     static GrOp::Owner Make(GrRecordingContext* context, | 
 |                             const SkRect& rect, | 
 |                             const SkPMColor4f& color, | 
 |                             const GrPathUtils::QuadUVMatrix& devToUV) { | 
 |         return GrOp::Make<BezierQuadTestOp>(context, rect, color, devToUV); | 
 |     } | 
 |  | 
 | private: | 
 |     friend class ::GrOp; // for ctor | 
 |  | 
 |     BezierQuadTestOp(const SkRect& rect, const SkPMColor4f& color, | 
 |                      const GrPathUtils::QuadUVMatrix& devToUV) | 
 |             : INHERITED(rect, color, ClassID()) | 
 |             , fDevToUV(devToUV) {} | 
 |  | 
 |     struct Vertex { | 
 |         SkPoint fPosition; | 
 |         float   fKLM[4]; // The last value is ignored. The effect expects a vec4f. | 
 |     }; | 
 |  | 
 |     GrGeometryProcessor* makeGP(const GrCaps& caps, SkArenaAlloc* arena) final { | 
 |         auto tmp = GrQuadEffect::Make(arena, this->color(), SkMatrix::I(), caps, SkMatrix::I(), | 
 |                                       false); | 
 |         if (!tmp) { | 
 |             return nullptr; | 
 |         } | 
 |         SkASSERT(tmp->vertexStride() == sizeof(Vertex)); | 
 |         return tmp; | 
 |     } | 
 |  | 
 |     void onPrepareDraws(Target* target) final { | 
 |         QuadHelper helper(target, sizeof(Vertex), 1); | 
 |         Vertex* verts = reinterpret_cast<Vertex*>(helper.vertices()); | 
 |         if (!verts) { | 
 |             return; | 
 |         } | 
 |         SkRect rect = this->rect(); | 
 |         SkPointPriv::SetRectTriStrip(&verts[0].fPosition, rect, sizeof(Vertex)); | 
 |         fDevToUV.apply(verts, 4, sizeof(Vertex), sizeof(SkPoint)); | 
 |  | 
 |         fMesh = helper.mesh(); | 
 |     } | 
 |  | 
 |     GrPathUtils::QuadUVMatrix fDevToUV; | 
 |  | 
 |     static constexpr int kVertsPerCubic = 4; | 
 |     static constexpr int kIndicesPerCubic = 6; | 
 |  | 
 |     using INHERITED = BezierTestOp; | 
 | }; | 
 |  | 
 | /** | 
 |  * This GM directly exercises effects that draw Bezier quad curves in the GPU backend. | 
 |  */ | 
 | class BezierQuadEffects : public GpuGM { | 
 | public: | 
 |     BezierQuadEffects() { | 
 |         this->setBGColor(0xFFFFFFFF); | 
 |     } | 
 |  | 
 | protected: | 
 |     static const int kNumQuads = 5; | 
 |     static const int kCellWidth = 128; | 
 |     static const int kCellHeight = 128; | 
 |  | 
 |     SkString onShortName() override { | 
 |         return SkString("bezier_quad_effects"); | 
 |     } | 
 |  | 
 |     SkISize onISize() override { | 
 |         return SkISize::Make(kCellWidth, kNumQuads*kCellHeight); | 
 |     } | 
 |  | 
 |     void onDraw(GrRecordingContext* context, GrSurfaceDrawContext* surfaceDrawContext, | 
 |                 SkCanvas* canvas) override { | 
 |  | 
 |         const SkScalar w = kCellWidth, h = kCellHeight; | 
 |         const SkPMColor4f kOpaqueBlack = SkPMColor4f::FromBytes_RGBA(0xff000000); | 
 |  | 
 |         const SkPoint baseControlPts[kNumQuads][3] = { | 
 |             { { 0.31f * w, 0.01f * h}, { 0.48f * w, 0.74f * h }, { 0.19f * w, 0.33f * h } }, | 
 |             { { 0.00f * w, 0.07f * h}, { 0.30f * w, 0.70f * h }, { 0.47f * w, 0.37f * h } }, | 
 |             { { 0.15f * w, 0.23f * h}, { 0.49f * w, 0.87f * h }, { 0.85f * w, 0.66f * h } }, | 
 |             { { 0.09f * w, 0.15f * h}, { 0.42f * w, 0.33f * h }, { 0.17f * w, 0.38f * h } }, | 
 |             { { 0.98f * w, 0.54f * h}, { 0.83f * w, 0.91f * h }, { 0.62f * w, 0.40f * h } }, | 
 |         }; | 
 |  | 
 |         SkPaint ctrlPtPaint; | 
 |         ctrlPtPaint.setColor(SK_ColorRED); | 
 |  | 
 |         SkPaint choppedPtPaint; | 
 |         choppedPtPaint.setColor(~ctrlPtPaint.getColor() | 0xFF000000); | 
 |  | 
 |         SkPaint polyPaint; | 
 |         polyPaint.setColor(0xffA0A0A0); | 
 |         polyPaint.setStrokeWidth(0); | 
 |         polyPaint.setStyle(SkPaint::kStroke_Style); | 
 |  | 
 |         SkPaint boundsPaint; | 
 |         boundsPaint.setColor(0xff808080); | 
 |         boundsPaint.setStrokeWidth(0); | 
 |         boundsPaint.setStyle(SkPaint::kStroke_Style); | 
 |  | 
 |         for (int row = 0; row < kNumQuads; ++row) { | 
 |             SkScalar x = 0; | 
 |             SkScalar y = row * h; | 
 |             SkPoint controlPts[] = { | 
 |                 {x + baseControlPts[row][0].fX, y + baseControlPts[row][0].fY}, | 
 |                 {x + baseControlPts[row][1].fX, y + baseControlPts[row][1].fY}, | 
 |                 {x + baseControlPts[row][2].fX, y + baseControlPts[row][2].fY} | 
 |             }; | 
 |  | 
 |             for (int i = 0; i < 3; ++i) { | 
 |                 canvas->drawCircle(controlPts[i], 6.f, ctrlPtPaint); | 
 |             } | 
 |  | 
 |             canvas->drawPoints(SkCanvas::kPolygon_PointMode, 3, controlPts, polyPaint); | 
 |  | 
 |             SkPoint chopped[5]; | 
 |             int cnt = SkChopQuadAtMaxCurvature(controlPts, chopped); | 
 |  | 
 |             for (int c = 0; c < cnt; ++c) { | 
 |                 SkPoint* pts = chopped + 2 * c; | 
 |  | 
 |                 for (int i = 0; i < 3; ++i) { | 
 |                     canvas->drawCircle(pts[i], 3.f, choppedPtPaint); | 
 |                 } | 
 |  | 
 |                 SkRect bounds; | 
 |                 bounds.setBounds(pts, 3); | 
 |  | 
 |                 canvas->drawRect(bounds, boundsPaint); | 
 |  | 
 |                 GrPathUtils::QuadUVMatrix DevToUV(pts); | 
 |  | 
 |                 GrOp::Owner op = BezierQuadTestOp::Make(context, bounds, | 
 |                                                         kOpaqueBlack, DevToUV); | 
 |                 surfaceDrawContext->addDrawOp(std::move(op)); | 
 |             } | 
 |         } | 
 |     } | 
 |  | 
 | private: | 
 |     using INHERITED = GM; | 
 | }; | 
 |  | 
 | DEF_GM(return new BezierConicEffects;) | 
 | DEF_GM(return new BezierQuadEffects;) | 
 | }  // namespace skiagm |