void KoPathSegmentTypeCommand::redo()
{
    KUndo2Command::redo();
    QList<KoPathPointData>::const_iterator it(m_pointDataList.constBegin());
    for (; it != m_pointDataList.constEnd(); ++it) {
        KoPathShape * pathShape = it->pathShape;
        pathShape->update();

        KoPathSegment segment = pathShape->segmentByIndex(it->pointIndex);

        if (m_segmentType == Curve) {
            // we change type to curve -> set control point positions
            QPointF pointDiff = segment.second()->point() - segment.first()->point();
            segment.first()->setControlPoint2(segment.first()->point() + pointDiff / 3.0);
            segment.second()->setControlPoint1(segment.first()->point() + pointDiff * 2.0 / 3.0);
        } else {
            // we are changing type to line -> remove control points
            segment.first()->removeControlPoint2();
            segment.second()->removeControlPoint1();
        }

        pathShape->normalize();
        pathShape->update();
    }
}
QPointF RoundCornersCommand::tangentAtEnd( const KoPathSegment &s )
{
    QList<QPointF> cp = s.controlPoints();
    QPointF tn = cp[cp.count()-2] - cp.last();
    qreal length = sqrt( tn.x()*tn.x() + tn.y()*tn.y() );
    return tn / length;
}
示例#3
0
QPointF RoundCornersCommand::tangentAtStart(const KoPathSegment &s)
{
    QVector<QPointF> cp = s.controlPoints();
    QPointF tn = cp[1] - cp.first();
    qreal length = sqrt(tn.x() * tn.x() + tn.y() * tn.y());
    return tn / length;
}
示例#4
0
void KoPathPointInsertCommand::redo()
{
    QUndoCommand::redo();
    for (int i = m_pointDataList.size() - 1; i >= 0; --i) {
        KoPathPointData pointData = m_pointDataList.at(i);
        KoPathShape * pathShape = pointData.pathShape;

        KoPathSegment segment = pathShape->segmentByIndex(pointData.pointIndex);

        ++pointData.pointIndex.second;

        if (segment.first()->activeControlPoint2()) {
            QPointF controlPoint2 = segment.first()->controlPoint2();
            qSwap(controlPoint2, m_controlPoints[i].first);
            segment.first()->setControlPoint2(controlPoint2);
        }

        if (segment.second()->activeControlPoint1()) {
            QPointF controlPoint1 = segment.second()->controlPoint1();
            qSwap(controlPoint1, m_controlPoints[i].second);
            segment.second()->setControlPoint1(controlPoint1);
        }

        pathShape->insertPoint(m_points.at(i), pointData.pointIndex);
        pathShape->update();
    }
    m_deletePoints = false;
}
void TestSegmentTypeCommand::changeToLine()
{
    KoPathShape path;
    path.moveTo( QPointF(0,0) );
    path.curveTo( QPointF(25,25), QPointF(75,25), QPointF(100,0) );

    KoPathPointData segment(&path, KoPathPointIndex(0,0));
    QList<KoPathPointData> segments;
    segments.append(segment);

    // get first segment
    KoPathSegment s = path.segmentByIndex(KoPathPointIndex(0,0));

    KoPathSegmentTypeCommand cmd(segments, KoPathSegmentTypeCommand::Line);

    QVERIFY(s.first()->activeControlPoint2());
    QVERIFY(s.second()->activeControlPoint1());

    cmd.redo();

    QVERIFY(!s.first()->activeControlPoint2());
    QVERIFY(!s.second()->activeControlPoint1());

    cmd.undo();

    QVERIFY(s.first()->activeControlPoint2());
    QVERIFY(s.second()->activeControlPoint1());
}
void KoPathSegmentTypeCommand::undo()
{
    KUndo2Command::undo();
    for (int i = 0; i < m_pointDataList.size(); ++i) {
        const KoPathPointData & pd = m_pointDataList.at(i);
        pd.pathShape->update();
        KoPathSegment segment = pd.pathShape->segmentByIndex(pd.pointIndex);
        const SegmentTypeData segmentData(m_segmentData.at(i));

        if (m_segmentType == Line) {
            // change type back to curve -> reactivate control points and their positions
            segment.first()->setControlPoint2(pd.pathShape->documentToShape(segmentData.m_controlPoint2));
            segment.second()->setControlPoint1(pd.pathShape->documentToShape(segmentData.m_controlPoint1));
        } else {
            // change back to line -> remove control points
            segment.first()->removeControlPoint2();
            segment.second()->removeControlPoint1();
        }

        segment.first()->setProperties(segmentData.m_properties2);
        segment.second()->setProperties(segmentData.m_properties1);

        pd.pathShape->normalize();
        pd.pathShape->update();
    }
}
示例#7
0
KoPathPointInsertCommand::KoPathPointInsertCommand(const QList<KoPathPointData> & pointDataList, qreal insertPosition, QUndoCommand *parent)
        : QUndoCommand(parent)
        , m_deletePoints(true)
{
    if (insertPosition < 0)
        insertPosition = 0;
    if (insertPosition > 1)
        insertPosition = 1;

    //TODO the list needs to be sorted

    QList<KoPathPointData>::const_iterator it(pointDataList.begin());
    for (; it != pointDataList.end(); ++it) {
        KoPathShape * pathShape = it->pathShape;

        KoPathSegment segment = pathShape->segmentByIndex(it->pointIndex);

        // should not happen but to be sure
        if (! segment.isValid())
            continue;

        m_pointDataList.append(*it);
        
        QPair<KoPathSegment, KoPathSegment> splitSegments = segment.splitAt( insertPosition );
        
        KoPathPoint * split1 = splitSegments.first.second();
        KoPathPoint * split2 = splitSegments.second.first();
        KoPathPoint * splitPoint = new KoPathPoint( pathShape, split1->point() );
        if( split1->activeControlPoint1() )
            splitPoint->setControlPoint1(split1->controlPoint1());
        if( split2->activeControlPoint2() )
            splitPoint->setControlPoint2(split2->controlPoint2());
        
        m_points.append(splitPoint);
        QPointF cp1 = splitSegments.first.first()->controlPoint2();
        QPointF cp2 = splitSegments.second.second()->controlPoint1();
        m_controlPoints.append(QPair<QPointF, QPointF>(cp1, cp2));
    }
}
示例#8
0
void KoPathSegmentChangeStrategy::handleMouseMove(const QPointF &mouseLocation, Qt::KeyboardModifiers modifiers)
{
    m_tool->canvas()->updateCanvas(m_tool->canvas()->snapGuide()->boundingRect());
    QPointF snappedPosition = m_tool->canvas()->snapGuide()->snap(mouseLocation, modifiers);
    m_tool->canvas()->updateCanvas(m_tool->canvas()->snapGuide()->boundingRect());

    QPointF localPos = m_path->documentToShape(snappedPosition);

    if (m_segment.degree() == 1) {
        // line segment is converted to a curve
        KoPathSegmentTypeCommand cmd(m_pointData1, KoPathSegmentTypeCommand::Curve);
        cmd.redo();
    }

    QPointF move1, move2;

    if (m_segment.degree() == 2) {
        // interpolate quadratic segment between segment start, mouse position and segment end
        KoPathSegment ipol = KoPathSegment::interpolate(m_segment.first()->point(),
                                                         localPos,
                                                         m_segment.second()->point(),
                                                         m_segmentParam);
        if (ipol.isValid()) {
            move1 = move2 = ipol.controlPoints()[1] - m_segment.controlPoints()[1];
        }
    }
    else if (m_segment.degree() == 3) {
        /*
        * method from inkscape, original method and idea borrowed from Simon Budig
        * <*****@*****.**> and the GIMP
        * cf. app/vectors/gimpbezierstroke.c, gimp_bezier_stroke_point_move_relative()
        *
        * feel good is an arbitrary parameter that distributes the delta between handles
        * if t of the drag point is less than 1/6 distance form the endpoint only
        * the corresponding handle is adjusted. This matches the behavior in GIMP
        */
        const qreal t = m_segmentParam;
        qreal feel_good;
        if (t <= 1.0 / 6.0)
            feel_good = 0;
        else if (t <= 0.5)
            feel_good = (pow((6 * t - 1) / 2.0, 3)) / 2;
        else if (t <= 5.0 / 6.0)
            feel_good = (1 - pow((6 * (1-t) - 1) / 2.0, 3)) / 2 + 0.5;
        else
            feel_good = 1;

        QPointF lastLocalPos = m_path->documentToShape(m_lastPosition);
        QPointF delta = localPos - lastLocalPos;
        move2 = ((1-feel_good)/(3*t*(1-t)*(1-t))) * delta;
        move1 = (feel_good/(3*t*t*(1-t))) * delta;
    }

    m_path->update();
    if(m_segment.first()->activeControlPoint2()) {
        KoPathControlPointMoveCommand cmd(m_pointData1, move2, KoPathPoint::ControlPoint2);
        cmd.redo();
    }
    if(m_segment.second()->activeControlPoint1()) {
        KoPathControlPointMoveCommand cmd(m_pointData2, move1, KoPathPoint::ControlPoint1);
        cmd.redo();
    }
    m_path->normalize();
    m_path->update();

    m_ctrlPoint1Move += move1;
    m_ctrlPoint2Move += move2;

    // save last mouse position
    m_lastPosition = mouseLocation;
}
void RoundCornersCommand::roundPath()
{
    /*
    * This algorithm is worked out by <kudling AT kde DOT org> to produce similar results as
    * the "round corners" algorithms found in other applications. Neither code nor
    * algorithms from any 3rd party is used though.
    *
    * We want to replace all corners with round corners having "radius" m_radius.
    * The algorithm doesn't really produce circular arcs, but that's ok since
    * the algorithm achieves nice looking results and is generic enough to be applied
    * to all kind of paths.
    * Note also, that this algorithm doesn't touch smooth joins (in the sense of
    * KoPathPoint::isSmooth() ).
    *
    * We'll manipulate the input path for bookkeeping purposes and construct a new
    * temporary path in parallel. We finally replace the input path with the new path.
    *
    *
    * Without restricting generality, let's assume the input path is closed and
    * contains segments which build a rectangle.
    *
    *           2
    *    O------------O
    *    |            |        Numbers reflect the segments' order
    *   3|            |1       in the path. We neglect the "begin"
    *    |            |        segment here.
    *    O------------O
    *           0
    *
    * There are three unique steps to process. The second step is processed
    * many times in a loop.
    *
    * 1) Begin
    *    -----
    *    Split the first segment of the input path (called "path[0]" here)
    *    at parameter t
    *
    *        t = path[0]->param( m_radius )
    *
    *    and move newPath to this new knot. If current segment is too small
    *    (smaller than 2 * m_radius), we always set t = 0.5 here and in the further
    *    steps as well.
    *
    *    path:                 new path:
    *
    *           2
    *    O------------O
    *    |            |
    *  3 |            | 1                    The current segment is marked with "#"s.
    *    |            |
    *    O##O#########O        ...O
    *           0                     0
    *
    * 2) Loop
    *    ----
    *    The loop step is iterated over all segments. After each appliance the index n
    *    is incremented and the loop step is reapplied until no untouched segment is left.
    *
    *    Split the current segment path[n] of the input path at parameter t
    *
    *        t = path[n]->param( path[n]->length() - m_radius )
    *
    *    and add the first subsegment of the curent segment to newPath.
    *
    *    path:                 new path:
    *
    *           2
    *    O------------O
    *    |            |
    *  3 |            | 1
    *    |            |
    *    O--O######O##O           O------O...
    *           0                     0
    *
    *    Now make the second next segment (the original path[1] segment in our example)
    *    the current one. Split it at parameter t
    *
    *        t = path[n]->param( m_radius )
    *
    *    path:                 new path:
    *
    *           2
    *    O------------O
    *    |            #
    *  3 |            O 1
    *    |            #
    *    O--O------O--O           O------O...
    *           0                     0
    *
    *    Make the first subsegment of the current segment the current one.
    *
    *    path:                 new path:
    *
    *           2
    *    O------------O
    *    |            |
    *  3 |            O 1                   O
    *    |            #                    /.1
    *    O--O------O--O           O------O...
    *           0                     0
    *
    * 3) End
    *    ---
    *
    *    path:                 new path:
    *
    *           2                     4
    *    O--O------O--O        5 .O------O. 3
    *    |            |         /          \
    *  3 O            O 1    6 O            O 2
    *    |            |      7 .\          /
    *    O--O------O--O        ...O------O. 1
    *           0                     0
    */

    // TODO: not sure if we should only touch flat segment joins as the original algorithm

    m_path->clear();

    int subpathCount = m_copy->subpathCount();
    for( int subpathIndex = 0; subpathIndex < subpathCount; ++subpathIndex )
    {
        int pointCount = m_copy->pointCountSubpath( subpathIndex );
        if( ! pointCount )
            continue;

        // check if we have sufficient number of points
        if( pointCount < 3 )
        {
            // copy the only segment
            KoPathSegment s = m_copy->segmentByIndex( KoPathPointIndex( subpathIndex, 0 ) );
            m_path->moveTo( m_copy->pointByIndex( KoPathPointIndex( subpathIndex, 0 ) )->point() );
            addSegment( m_path, s );

            continue;
        }

        KoPathSegment prevSeg = m_copy->segmentByIndex( KoPathPointIndex( subpathIndex, pointCount-1 ) );
        KoPathSegment nextSeg = m_copy->segmentByIndex( KoPathPointIndex( subpathIndex, 0 ) );
        KoPathSegment lastSeg;

        KoPathPoint * currPoint = nextSeg.first();
        KoPathPoint * firstPoint = 0;
        KoPathPoint * lastPoint = 0;

        // check if first path point is a smooth join with the closing segment
        bool firstPointIsCorner = m_copy->isClosedSubpath( subpathIndex ) 
                && ! currPoint->isSmooth( prevSeg.first(), nextSeg.second() );

        // Begin: take care of the first path point
        if( firstPointIsCorner )
        {
            // split the previous segment at length - radius
            qreal prevLength = prevSeg.length();
            qreal prevSplit = prevLength > m_radius ? prevSeg.paramAtLength( prevLength-m_radius ) : 0.5;
            QPair<KoPathSegment,KoPathSegment> prevParts = prevSeg.splitAt( prevSplit );

            // split the next segment at radius
            qreal nextLength = nextSeg.length();
            qreal nextSplit = nextLength > m_radius ? nextSeg.paramAtLength( m_radius ) : 0.5;
            QPair<KoPathSegment,KoPathSegment> nextParts = nextSeg.splitAt( nextSplit );

            // calculate smooth tangents
            QPointF P0 = prevParts.first.second()->point();
            QPointF P3 = nextParts.first.second()->point();
            qreal tangentLength1 = 0.5 * QLineF( P0, currPoint->point() ).length();
            qreal tangentLength2 = 0.5 * QLineF( P3, currPoint->point() ).length();
            QPointF P1 = P0 - tangentLength1 * tangentAtEnd( prevParts.first );
            QPointF P2 = P3 - tangentLength2 * tangentAtStart( nextParts.second );

            // start the subpath
            firstPoint = m_path->moveTo( prevParts.second.first()->point() );
            // connect the split points with curve
            // TODO: shall we create a correct arc?
            m_path->curveTo( P1, P2, P3 );

            prevSeg = nextParts.second;
            lastSeg = prevParts.first;
        }
        else
        {
            firstPoint = m_path->moveTo( currPoint->point() );
            prevSeg = nextSeg;
        }

        // Loop:
        for( int pointIndex = 1; pointIndex < pointCount; ++pointIndex )
        {
            nextSeg = m_copy->segmentByIndex( KoPathPointIndex( subpathIndex, pointIndex ) );
            if( ! nextSeg.isValid() )
                break;

            currPoint = nextSeg.first();
            if( ! currPoint )
                continue;

            if( currPoint->isSmooth( prevSeg.first(), nextSeg.second() ) )
            {
                // the current point has a smooth join, so we can add the previous segment
                // to our new path
                addSegment( m_path, prevSeg );
                prevSeg = nextSeg;
            }
            else
            {
                // split the previous segment at length - radius
                qreal prevLength = prevSeg.length();
                qreal prevSplit = prevLength > m_radius ? prevSeg.paramAtLength( prevLength-m_radius ) : 0.5;
                QPair<KoPathSegment,KoPathSegment> prevParts = prevSeg.splitAt( prevSplit );

                // add the remaining part up to the split point of the pervious segment
                lastPoint = addSegment( m_path, prevParts.first );

                // split the next segment at radius
                qreal nextLength = nextSeg.length();
                qreal nextSplit = nextLength > m_radius ? nextSeg.paramAtLength( m_radius ) : 0.5;
                QPair<KoPathSegment,KoPathSegment> nextParts = nextSeg.splitAt( nextSplit );

                // calculate smooth tangents
                QPointF P0 = prevParts.first.second()->point();
                QPointF P3 = nextParts.first.second()->point();
                qreal tangentLength1 = 0.5 * QLineF( P0, currPoint->point() ).length();
                qreal tangentLength2 = 0.5 * QLineF( P3, currPoint->point() ).length();
                QPointF P1 = P0 - tangentLength1 * tangentAtEnd( prevParts.first );
                QPointF P2 = P3 - tangentLength2 * tangentAtStart( nextParts.second );

                // connect the split points with curve
                // TODO: shall we create a correct arc?
                lastPoint = m_path->curveTo( P1, P2, P3 );

                prevSeg = nextParts.second;
            }

        }

        // End: take care of the last path point
        if( firstPointIsCorner )
        {
            // construct the closing segment
            lastPoint->setProperty( KoPathPoint::CloseSubpath );
            firstPoint->setProperty( KoPathPoint::CloseSubpath );
            switch( lastSeg.degree() )
            {
                case 1:
                    lastPoint->removeControlPoint2();
                    firstPoint->removeControlPoint1();
                    break;
                case 2:
                    if( lastSeg.first()->activeControlPoint2() )
                    {
                        lastPoint->setControlPoint2( lastSeg.first()->controlPoint2() );
                        firstPoint->removeControlPoint1();
                    }
                    else
                    {
                        lastPoint->removeControlPoint2();
                        firstPoint->setControlPoint1( lastSeg.second()->controlPoint1() );
                    }
                    break;
                case 3:
                    lastPoint->setControlPoint2( lastSeg.first()->controlPoint2() );
                    firstPoint->setControlPoint1( lastSeg.second()->controlPoint1() );
                    break;
            }
        }
        else
        {
            // add the last remaining segment
            addSegment( m_path, prevSeg );
        }
    }
}
void KoPathPointTypeCommand::redo()
{
    KUndo2Command::redo();
    repaint(false);
    m_additionalPointData.clear();

    QList<PointData>::iterator it(m_oldPointData.begin());
    for (; it != m_oldPointData.end(); ++it) {
        KoPathPoint *point = it->m_pointData.pathShape->pointByIndex(it->m_pointData.pointIndex);
        KoPathPoint::PointProperties properties = point->properties();

        switch (m_pointType) {
        case Line: {
            point->removeControlPoint1();
            point->removeControlPoint2();
            break;
        }
        case Curve: {
            KoPathPointIndex pointIndex = it->m_pointData.pointIndex;
            KoPathPointIndex prevIndex;
            KoPathPointIndex nextIndex;
            KoPathShape * path = it->m_pointData.pathShape;
            // get previous path node
            if (pointIndex.second > 0)
                prevIndex = KoPathPointIndex(pointIndex.first, pointIndex.second - 1);
            else if (pointIndex.second == 0 && path->isClosedSubpath(pointIndex.first))
                prevIndex = KoPathPointIndex(pointIndex.first, path->subpathPointCount(pointIndex.first) - 1);
            // get next node
            if (pointIndex.second < path->subpathPointCount(pointIndex.first) - 1)
                nextIndex = KoPathPointIndex(pointIndex.first, pointIndex.second + 1);
            else if (pointIndex.second < path->subpathPointCount(pointIndex.first) - 1
                     && path->isClosedSubpath(pointIndex.first))
                nextIndex = KoPathPointIndex(pointIndex.first, 0);

            KoPathPoint * prevPoint = path->pointByIndex(prevIndex);
            KoPathPoint * nextPoint = path->pointByIndex(nextIndex);

            if (prevPoint && ! point->activeControlPoint1() && appendPointData(KoPathPointData(path, prevIndex))) {
                KoPathSegment cubic = KoPathSegment(prevPoint, point).toCubic();
                if (prevPoint->activeControlPoint2()) {
                    prevPoint->setControlPoint2(cubic.first()->controlPoint2());
                    point->setControlPoint1(cubic.second()->controlPoint1());
                } else
                    point->setControlPoint1(cubic.second()->controlPoint1());
            }
            if (nextPoint && ! point->activeControlPoint2() && appendPointData(KoPathPointData(path, nextIndex))) {
                KoPathSegment cubic = KoPathSegment(point, nextPoint).toCubic();
                if (nextPoint->activeControlPoint1()) {
                    point->setControlPoint2(cubic.first()->controlPoint2());
                    nextPoint->setControlPoint1(cubic.second()->controlPoint1());
                } else
                    point->setControlPoint2(cubic.first()->controlPoint2());
            }
            break;
        }
        case Symmetric: {
            properties &= ~KoPathPoint::IsSmooth;
            properties |= KoPathPoint::IsSymmetric;

            // calculate vector from node point to first control point and normalize it
            QPointF directionC1 = point->controlPoint1() - point->point();
            qreal dirLengthC1 = sqrt(directionC1.x() * directionC1.x() + directionC1.y() * directionC1.y());
            directionC1 /= dirLengthC1;
            // calculate vector from node point to second control point and normalize it
            QPointF directionC2 = point->controlPoint2() - point->point();
            qreal dirLengthC2 = sqrt(directionC2.x() * directionC2.x() + directionC2.y() * directionC2.y());
            directionC2 /= dirLengthC2;
            // calculate the average distance of the control points to the node point
            qreal averageLength = 0.5 * (dirLengthC1 + dirLengthC2);
            // compute position of the control points so that they lie on a line going through the node point
            // the new distance of the control points is the average distance to the node point
            point->setControlPoint1(point->point() + 0.5 * averageLength * (directionC1 - directionC2));
            point->setControlPoint2(point->point() + 0.5 * averageLength * (directionC2 - directionC1));
        }
        break;
        case Smooth: {
            properties &= ~KoPathPoint::IsSymmetric;
            properties |= KoPathPoint::IsSmooth;

            // calculate vector from node point to first control point and normalize it
            QPointF directionC1 = point->controlPoint1() - point->point();
            qreal dirLengthC1 = sqrt(directionC1.x() * directionC1.x() + directionC1.y() * directionC1.y());
            directionC1 /= dirLengthC1;
            // calculate vector from node point to second control point and normalize it
            QPointF directionC2 = point->controlPoint2() - point->point();
            qreal dirLengthC2 = sqrt(directionC2.x() * directionC2.x() + directionC2.y() * directionC2.y());
            directionC2 /= dirLengthC2;
            // compute position of the control points so that they lie on a line going through the node point
            // the new distance of the control points is the average distance to the node point
            point->setControlPoint1(point->point() + 0.5 * dirLengthC1 * (directionC1 - directionC2));
            point->setControlPoint2(point->point() + 0.5 * dirLengthC2 * (directionC2 - directionC1));
        }
        break;
        case Corner:
        default:
            properties &= ~KoPathPoint::IsSymmetric;
            properties &= ~KoPathPoint::IsSmooth;
            break;
        }
        point->setProperties(properties);
    }
    repaint(true);
}
void KoPathSegmentTypeCommand::initialize(const QList<KoPathPointData> & pointDataList)
{
    QList<KoPathPointData>::const_iterator it(pointDataList.begin());
    for (; it != pointDataList.end(); ++it) {
        KoPathSegment segment = it->pathShape->segmentByIndex(it->pointIndex);
        if (segment.isValid()) {
            if (m_segmentType == Curve) {
                // don not change segment if already a curve
                if (segment.first()->activeControlPoint2() || segment.second()->activeControlPoint1())
                    continue;
            } else {
                // do not change segment if already a line
                if (! segment.first()->activeControlPoint2() && ! segment.second()->activeControlPoint1())
                    continue;
            }

            m_pointDataList.append(*it);
            SegmentTypeData segmentData;

            KoPathShape * pathShape = segment.first()->parent();

            // we are changing a curve to a line -> save control point positions
            if (m_segmentType == Line) {
                segmentData.m_controlPoint2 = pathShape->shapeToDocument(segment.first()->controlPoint2());
                segmentData.m_controlPoint1 = pathShape->shapeToDocument(segment.second()->controlPoint1());
            }
            // save point properties
            segmentData.m_properties2 = segment.first()->properties();
            segmentData.m_properties1 = segment.second()->properties();
            m_segmentData.append(segmentData);
        }
    }

    if (m_segmentType == Curve) {
        setText(i18nc("(qtundo-format)", "Change segments to curves"));
    } else {
        setText(i18nc("(qtundo-format)", "Change segments to lines"));
    }
}