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:
So the selecttool seems to interact with the world with the toolmanager..
So.. It looks like SelectTool object is stored in 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
————————————————————
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)