Messing around with Pencil2d. Exploring the selection tool.

Over the holidays, my daughter had expressed an interest in learning animation.  I did a little googling an I found a open source package called pencil2d.   She downloaded and tried using it and found it too buggy.  I uses C++/QT which is something I’m familiar with my dabbling with freecad so, I figured what the heck and I downloaded the source from git..
My understanding is that the original developer of pencil dropped the project and it basically forked. Pencil2d is a merge and enhancement effort.
I don’t have a lot of open bandwidth these days, but I’m hoping that contribute a bit to this project and get it to a point, where my daughter can make some use full animations.

From late December to today, it seems like things have improved quite a bit with pencil.   One very annoying thing at the moment is the selection tool.. It seems to be misbehaving…
https://github.com/pencil2d/pencil/issues/414

Basically when copy pasting with the selection tool very bizarre things occur.  It’s sort of hard to explain.  At this point I just want to see if can get a 30K foot understanding of how things are supposed to work.
So I think the problem has to do with thinks interacting  with the selection tool versus the selection tool itself. https://github.com/pencil2d/pencil/blob/master/core_lib/tool/selecttool.h git activity around selecttool.h has been quite.. (last change 25 days ago..) So, need to see how this object is used.
So.. Looking for reference to the SelectTool we get this:

usageOfSelectTool

So the selecttool seems to interact with the world with the toolmanager..

 

 

 

So.. It looks like SelectTool object is stored in mToolSetHash.

mToolSetHash

 

 

 

 

 

QHash looks alot to me like a Dictionary in C#  which contains a key and a value.
The key is an enumeration called ToolType and Value is a pointer to the BaseClass definition of the tools..
Ok.. So the enumeration ToolType is defined here:
https://github.com/pencil2d/pencil/blob/master/core_lib/util/pencildef.h

Ok.. This is very understandable to me… So.. to figure out where the selectTool object is interacting with the rest of pencil2d we need to search on the Enumeration SELECT  (good thing that we’re not doing SQL in this app)
We need to find the references to the enumeration SELECT
Select

 

 

 


————————————————————

void ScribbleArea::paintEvent( QPaintEvent* event )
{
    //qCDebug( mLog ) << "Paint event!" << QDateTime::currentDateTime() << event->rect();

    if ( !mMouseInUse || currentTool()->type() == MOVE || currentTool()->type() == HAND )
    {
        // --- we retrieve the canvas from the cache; we create it if it doesn't exist
        int curIndex = mEditor->currentFrame();
        int frameNumber = mEditor->layers()->LastFrameAtFrame( curIndex );

        QString strCachedFrameKey = "frame" + QString::number( frameNumber );
        if ( !QPixmapCache::find( strCachedFrameKey, mCanvas ) )
        {
            drawCanvas( mEditor->currentFrame(), event->rect() );
            QPixmapCache::insert( strCachedFrameKey, mCanvas );
            //qDebug() << "Repaint canvas!";
        }
    }

    if ( currentTool()->type() == MOVE )
    {
        Layer* layer = mEditor->layers()->currentLayer();
        Q_CHECK_PTR( layer );
        if ( layer->type() == Layer::VECTOR )
        {
            auto vecLayer = static_cast< LayerVector* >( layer );
            vecLayer->getLastVectorImageAtFrame( mEditor->currentFrame(), 0 )->setModified( true );
        }
    }

    QPainter painter( this );

    // paints the canvas
    painter.setWorldMatrixEnabled( false );
    //painter.setTransform( transMatrix ); // FIXME: drag canvas by hand

    painter.drawPixmap( QPoint( 0, 0 ), mCanvas );


    Layer* layer = mEditor->layers()->currentLayer();

    if ( !editor()->playback()->isPlaying() )    // we don't need to display the following when the animation is playing
    {
        if ( layer->type() == Layer::VECTOR )
        {
            VectorImage *vectorImage = ( ( LayerVector * )layer )->getLastVectorImageAtFrame( mEditor->currentFrame(), 0 );

            switch ( currentTool()->type() )
            {
                case SMUDGE:
                case HAND:
                {
                    painter.save();
                    painter.setWorldMatrixEnabled( false );
                    painter.setRenderHint( QPainter::Antialiasing, false );
                    // ----- paints the edited elements
                    QPen pen2( Qt::black, 0.5, Qt::SolidLine, Qt::RoundCap, Qt::RoundJoin );
                    painter.setPen( pen2 );
                    QColor colour;
                    // ------------ vertices of the edited curves
                    colour = QColor( 200, 200, 200 );
                    painter.setBrush( colour );
                    for ( int k = 0; k < vectorSelection.curve.size(); k++ )
                    {
                        int curveNumber = vectorSelection.curve.at( k );

                        for ( int vertexNumber = -1; vertexNumber < vectorImage->getCurveSize( curveNumber ); vertexNumber++ )
                        {
                            QPointF vertexPoint = vectorImage->getVertex( curveNumber, vertexNumber );
                            QRectF rectangle( mEditor->view()->mapCanvasToScreen( vertexPoint ) - QPointF( 3.0, 3.0 ), QSizeF( 7, 7 ) );
                            if ( rect().contains( mEditor->view()->mapCanvasToScreen( vertexPoint ).toPoint() ) )
                            {
                                painter.drawRect( rectangle );
                            }
                        }
                    }
                    // ------------ selected vertices of the edited curves
                    colour = QColor( 100, 100, 255 );
                    painter.setBrush( colour );
                    for ( int k = 0; k < vectorSelection.vertex.size(); k++ )
                    {
                        VertexRef vertexRef = vectorSelection.vertex.at( k );
                        QPointF vertexPoint = vectorImage->getVertex( vertexRef );
                        QRectF rectangle0 = QRectF( mEditor->view()->mapCanvasToScreen( vertexPoint ) - QPointF( 3.0, 3.0 ), QSizeF( 7, 7 ) );
                        painter.drawRect( rectangle0 );
                    }
                    // ----- paints the closest vertices
                    colour = QColor( 255, 0, 0 );
                    painter.setBrush( colour );
                    if ( vectorSelection.curve.size() > 0 )
                    {
                        for ( int k = 0; k < mClosestVertices.size(); k++ )
                        {
                            VertexRef vertexRef = mClosestVertices.at( k );
                            QPointF vertexPoint = vectorImage->getVertex( vertexRef );

                            QRectF rectangle = QRectF( mEditor->view()->mapCanvasToScreen( vertexPoint ) - QPointF( 3.0, 3.0 ), QSizeF( 7, 7 ) );
                            painter.drawRect( rectangle );
                        }
                    }
                    painter.restore();
                    break;
                }

                case MOVE:
                {
                    // ----- paints the closest curves
                    mBufferImg->clear();
                    QColor colour = QColor( 100, 100, 255, 50 );
                    QPen pen2( colour, 3, Qt::SolidLine, Qt::RoundCap, Qt::RoundJoin );


                    for ( int k = 0; k < mClosestCurves.size(); k++ )
                    {
                        float scale = mEditor->view()->scaling(); // FIXME: check whether it's correct (det = area?)

                        int idx = mClosestCurves[ k ];
                        if ( vectorImage->m_curves.size() <= idx )
                        {
                            // safety check
                            continue;
                        }
                        BezierCurve myCurve = vectorImage->m_curves[ mClosestCurves[ k ] ];
                        if ( myCurve.isPartlySelected() )
                        {
                            myCurve.transform( selectionTransformation );
                        }
                        QPainterPath path = myCurve.getStrokedPath( 1.2 / scale, false );
                        mBufferImg->drawPath( mEditor->view()->mapCanvasToScreen( path ),
                                              pen2,
                                              colour,
                                              QPainter::CompositionMode_SourceOver,
                                              mPrefs->isOn( SETTING::ANTIALIAS ) );
                    }
                    break;
                }
            } // end siwtch
        }

        // paints the  buffer image
        if ( mEditor->layers()->currentLayer() != NULL )
        {
            painter.setOpacity( 1.0 );
            if ( mEditor->layers()->currentLayer()->type() == Layer::BITMAP )
            {
                painter.setWorldMatrixEnabled( true );
                painter.setTransform( mEditor->view()->getView() );
            }
            else if ( mEditor->layers()->currentLayer()->type() == Layer::VECTOR )
            {
                painter.setWorldMatrixEnabled( false );
            }

            //qCDebug( mLog ) << "BufferRect" << mBufferImg->bounds();

            mBufferImg->paintImage( painter );
        }

        // paints the selection outline
        if ( somethingSelected && ( myTempTransformedSelection.isValid() || mMoveMode == ROTATION ) ) // @revise
        {
            // outline of the transformed selection
            painter.setWorldMatrixEnabled( false );
            painter.setOpacity( 1.0 );
            QPolygon tempRect = mEditor->view()->getView().mapToPolygon( myTempTransformedSelection.normalized().toRect() );

            Layer* layer = mEditor->layers()->currentLayer();
            if ( layer != NULL )
            {
                if ( layer->type() == Layer::BITMAP )
                {
                    painter.setBrush( Qt::NoBrush );
                    painter.setPen( Qt::DashLine );
                }
                if ( layer->type() == Layer::VECTOR )
                {
                    painter.setBrush( QColor( 0, 0, 0, 20 ) );
                    painter.setPen( Qt::gray );
                }
                painter.drawPolygon( tempRect );

                if ( layer->type() != Layer::VECTOR || currentTool()->type() != SELECT )
                {
                    painter.setPen( Qt::SolidLine );
                    painter.setBrush( QBrush( Qt::gray ) );
                    painter.drawRect( tempRect.point( 0 ).x() - 3, tempRect.point( 0 ).y() - 3, 6, 6 );
                    painter.drawRect( tempRect.point( 1 ).x() - 3, tempRect.point( 1 ).y() - 3, 6, 6 );
                    painter.drawRect( tempRect.point( 2 ).x() - 3, tempRect.point( 2 ).y() - 3, 6, 6 );
                    painter.drawRect( tempRect.point( 3 ).x() - 3, tempRect.point( 3 ).y() - 3, 6, 6 );
                }
            }
        }
    }

    // clips to the frame of the camera
    if ( mPrefs->isOn( SETTING::CAMERABORDER ) )
    {
        QRect rect = ( ( LayerCamera * )mEditor->object()->getLayer(mEditor->layers()->getLastCameraLayer()) )->getViewRect();
        rect.translate( width() / 2, height() / 2 );
        painter.setWorldMatrixEnabled( false );
        painter.setPen( Qt::NoPen );
        painter.setBrush( QColor( 0, 0, 0, 160 ) );
        painter.drawRect( QRect( 0, 0, width(), ( height() - rect.height() ) / 2 ) );
        painter.drawRect( QRect( 0, rect.bottom(), width(), ( height() - rect.height() ) / 2 ) );
        painter.drawRect( QRect( 0, rect.top(), ( width() - rect.width() ) / 2, rect.height() - 1 ) );
        painter.drawRect( QRect( ( width() + rect.width() ) / 2, rect.top(), (( width() - rect.width() ) / 2) + 1, rect.height() - 1 ) );
        painter.setPen( Qt::black );
        painter.setBrush( Qt::NoBrush );
        painter.drawRect( QRect(rect.x(), rect.y(), rect.width() - 1, rect.height() - 1) );
    }

    // outlines the frame of the viewport
#ifdef _DEBUG
    painter.setWorldMatrixEnabled( false );
    painter.setPen( QPen( Qt::gray, 2 ) );
    painter.setBrush( Qt::NoBrush );
    painter.drawRect( QRect( 0, 0, width(), height() ) );
#endif

    event->accept();
}

 

This seems like its doing something with rotation? (I’ve heard this mentioned)

————————————————————

Void ToolBoxWidget::selectOn()
{
    editor()->tools()->setCurrentTool( SELECT );

    deselectAllTools();
    selectButton->setChecked(true);
}

Ok.. I think this is fired to select the gui tool (this might be worth looking at later

void ToolBoxWidget::setCurrentTool( ToolType toolType )
{
    switch(toolType)
    {
    case PENCIL:
        emit pencilOn();
        break;
    case ERASER:
        emit eraserOn();
        break;
    case SELECT:
        emit selectOn();
        break;
    case MOVE:
        emit moveOn();
        break;
    case HAND:
        emit handOn();
        break;
    case SMUDGE:
        emit smudgeOn();
        break;
    case PEN:
        emit penOn();
        break;
    case POLYLINE:
        emit polylineOn();
        break;
    case BUCKET:
        emit bucketOn();
        break;
    case EYEDROPPER:
        emit eyedropperOn();
        break;
    case BRUSH:
        emit brushOn();
        break;
    default:
        break;
    }
}

Ok.. it looks like we’re just selecting the  Selection tool to be the default object here.

———————————————————–

bool ToolManager::init()
{
    mIsSwitchedToEraser = false;

    mToolSetHash.insert( PEN, new PenTool );
    mToolSetHash.insert( PENCIL, new PencilTool );
    mToolSetHash.insert( BRUSH, new BrushTool );
    mToolSetHash.insert( ERASER, new EraserTool );
    mToolSetHash.insert( BUCKET, new BucketTool );
    mToolSetHash.insert( EYEDROPPER, new EyedropperTool );
    mToolSetHash.insert( HAND, new HandTool );
    mToolSetHash.insert( MOVE, new MoveTool );
    mToolSetHash.insert( POLYLINE, new PolylineTool );
    mToolSetHash.insert( SELECT, new SelectTool );
    mToolSetHash.insert( SMUDGE, new SmudgeTool );

    foreach( BaseTool* pTool, mToolSetHash.values() )
    {
        pTool->initialize( editor() );
    }

    setDefaultTool();

    return true;
}

I can see putting some break points to get a look at the call stack at a few stops at some point

—————————————————————————–

QString BaseTool::TypeName( ToolType type )
{
    static QMap<ToolType, QString>* map = NULL;

    if ( map == NULL )
    {
        map = new QMap<ToolType, QString>();
        map->insert( PENCIL, "Pencil" );
        map->insert( ERASER, "Eraser" );
        map->insert( SELECT, "Select" );
        map->insert( MOVE, "Move" );
        map->insert( HAND, "Hand" );
        map->insert( SMUDGE, "Smudge" );
        map->insert( PEN, "Pen" );
        map->insert( POLYLINE, "Polyline" );
        map->insert( BUCKET, "Bucket" );
        map->insert( EYEDROPPER, "Eyedropper" );
        map->insert( BRUSH, "Brush" );
    }

    return map->value( type );
}

Can’t see anything be an issue here with what I’m working on.

————————————————

void MoveTool::mouseMoveEvent( QMouseEvent *event )
{
    Layer* layer = mEditor->layers()->currentLayer();
    if ( layer == NULL ) {
        return;
    }

    if ( layer->type() != Layer::BITMAP && layer->type() != Layer::VECTOR )
    {
        return;
    }

    if ( event->buttons() & Qt::LeftButton )   // the user is also pressing the mouse (dragging)
    {
        if ( mScribbleArea->somethingSelected )     // there is something selected
        {
            qreal xOffset = mScribbleArea->mOffset.x();
            qreal yOffset = mScribbleArea->mOffset.y();

            if ( event->modifiers() == Qt::ShiftModifier )    // (makes resize proportional, move linear)
            {
                qreal factor = mScribbleArea->mySelection.width() / mScribbleArea->mySelection.height();

                if (mScribbleArea->mMoveMode == ScribbleArea::TOPLEFT || mScribbleArea->mMoveMode == ScribbleArea::BOTTOMRIGHT) {
                    yOffset = xOffset / factor;
                }
                else if (mScribbleArea->mMoveMode == ScribbleArea::TOPRIGHT || mScribbleArea->mMoveMode == ScribbleArea::BOTTOMLEFT) {
                    yOffset = -(xOffset / factor);
                }
                else if (mScribbleArea->mMoveMode == ScribbleArea::MIDDLE) {

                    qreal absX = xOffset;
                    if (absX < 0) {absX = -absX;}

                    qreal absY = yOffset;
                    if (absY < 0) {absY = -absY;}

                    if (absX > absY) {
                        yOffset = 0;
                    }
                    if (absY > absX) {
                        xOffset = 0;
                    }
                }
            }

            switch ( mScribbleArea->mMoveMode )
            {
            case ScribbleArea::MIDDLE:
                if ( QLineF( getLastPressPixel(), getCurrentPixel() ).length() > 4 )
                {
                    mScribbleArea->myTempTransformedSelection = mScribbleArea->myTransformedSelection.translated( QPointF(xOffset, yOffset) );
                }
                break;

            case ScribbleArea::TOPRIGHT:
                mScribbleArea->myTempTransformedSelection =
                        mScribbleArea->myTransformedSelection.adjusted( 0, yOffset, xOffset, 0 );
                break;


            case ScribbleArea::TOPLEFT:
                mScribbleArea->myTempTransformedSelection =
                        mScribbleArea->myTransformedSelection.adjusted( xOffset, yOffset, 0, 0 );
                break;

                // TOPRIGHT XXX

            case ScribbleArea::BOTTOMLEFT:
                mScribbleArea->myTempTransformedSelection =
                        mScribbleArea->myTransformedSelection.adjusted( xOffset, 0, 0, yOffset );
                break;

            case ScribbleArea::BOTTOMRIGHT:
                mScribbleArea->myTempTransformedSelection =
                        mScribbleArea->myTransformedSelection.adjusted( 0, 0, xOffset, yOffset );
                break;
            case ScribbleArea::ROTATION:
                mScribbleArea->myTempTransformedSelection =
                        mScribbleArea->myTransformedSelection; // @ necessary?
                mScribbleArea->myRotatedAngle = getCurrentPixel().x() - getLastPressPixel().x();
                //qDebug() << "rotation" << m_pScribbleArea->myRotatedAngle;
                break;
            }

            mScribbleArea->calculateSelectionTransformation();

            mScribbleArea->paintTransformedSelection();
        }
        else     // there is nothing selected
        {
            // we switch to the select tool
            mEditor->tools()->setCurrentTool( SELECT );
            mScribbleArea->mMoveMode = ScribbleArea::MIDDLE;
            mScribbleArea->mySelection.setTopLeft( getLastPoint() );
            mScribbleArea->mySelection.setBottomRight( getLastPoint() );
            mScribbleArea->setSelection( mScribbleArea->mySelection, true );
        }
    }
    else     // the user is moving the mouse without pressing it
    {
        if ( layer->type() == Layer::VECTOR )
        {
            auto layerVector = static_cast< LayerVector* >( layer );
            VectorImage* pVecImg = layerVector->getLastVectorImageAtFrame( mEditor->currentFrame(), 0 );
            mScribbleArea->mClosestCurves = pVecImg->getCurvesCloseTo( getCurrentPoint(), mScribbleArea->tol / mEditor->view()->scaling() );
        }
        mScribbleArea->update();
    }
}

bool MoveTool::keyPressEvent(QKeyEvent *event)
{
    switch ( event->key() ) {
    case Qt::Key_Escape:
        cancelChanges();
        break;
    default:
        break;
    }

    // Follow the generic behaviour anyway
    //
    return false;
}

This is interesting and I think will need disection at some point.

—————————————————————

ToolType SelectTool::type()
{
    return SELECT;
}

This is a virtual functions defined in the baseclasstool and overwritten in

—————————————————-

enum ToolType
{
    INVALID_TOOL = -1,
    PENCIL = 0,
    ERASER,
    SELECT,
    MOVE,
    HAND,
    SMUDGE,
    PEN,
    POLYLINE,
    BUCKET,
    EYEDROPPER,
    BRUSH
};

The enumeration which defines SELECT.

Basically.. I just want to how the select object is use… I’m not trying to fix anything yet.

There’s a lot going on here and hopefully this will save me a little time later on.
One thing that I need to study is the copy paste mechanism…
(One thing that I notice is the duplicate frame F6 has a behavior that is weird at first but essenetially correct when you think about it.(Well maybe) When you hit f6 you get the image put into the new frame with the selection box around it.   If you draw on the frame and hit F6 the new stuff you just drew doesn’t copy over…  you need to hit escape and then F6… (I’m thinking this will come in handy at some point)

 

This entry was posted in Uncategorized and tagged . Bookmark the permalink.

Leave a Reply

Your email address will not be published. Required fields are marked *