#include "tvectorgl.h" #include "tgl.h" #include "tpalette.h" #include "tproperty.h" #include "tthreadmessage.h" #include "tvectorimage.h" #include "drawutil.h" #include "tcurveutil.h" #include "tstroke.h" #include "tstrokeutil.h" #include "tvectorrenderdata.h" #include "tstrokedeformations.h" #include "tmathutil.h" //Toonz includes #include "toonz/tobjecthandle.h" #include "toonz/txshlevelhandle.h" //TnzTools includes #include "tools/tool.h" #include "tools/toolutils.h" #include "tools/cursors.h" //Qt includes #include // For Qt translation support using namespace ToolUtils; //***************************************************************************** // PumpTool declaration //***************************************************************************** class PumpTool : public TTool { Q_DECLARE_TR_FUNCTIONS(PumpTool) int m_strokeStyleId, m_strokeIndex; //!< Edited stroke indices TStroke *m_inStroke, *m_outStroke; //!< Input/Output strokes std::vector m_splitStrokes; //!< Merging these, m_inStroke is reformed int m_stroke1Idx, m_stroke2Idx; //!< Indices of deformed strokes among split ones TUndo *m_undo; //!< Undo to be added upon non-trivial button up double m_actionW; //!< The action center stroke parameter double m_actionS1, m_actionS2; //!< Action center length in m_stroke double m_actionRadius; //!< Tool action radius in curve length std::vector m_splitPars; //!< Split parameters for action localization std::vector m_cpLenDiff1, m_cpLenDiff2; //!< Distorted CPs' length distances from action center bool m_active; //!< Whether a stroke is currently being edited bool m_enabled; //!< Tells whether the image allows editing bool m_cursorEnabled; //!< Whether the 'pump preview cursor' can be seen bool m_draw; //!< Should be removed...? TPointD m_oldPoint, m_downPoint; //!< Mouse positions upon editing TThickPoint m_cursor; //!< Pump preview cursor data int m_cursorId; double m_errorTol; //!< Allowed approximation error during edit TDoubleProperty m_toolSize; TIntProperty m_accuracy; TPropertyGroup m_prop; public: PumpTool() : TTool("T_Pump"), m_active(false), m_actionW(0), m_strokeIndex((std::numeric_limits::max)()), m_inStroke(0), m_outStroke(0), m_stroke1Idx(-1), m_stroke2Idx(-1), m_cursorEnabled(false), m_cursorId(ToolCursor::PumpCursor), m_actionRadius(1), m_draw(false), m_undo(0), m_toolSize("Size:", 1, 100, 20), m_accuracy("Accuracy:", 0, 100, 40), m_enabled(false) { bind(TTool::VectorImage); m_splitPars.resize(2); m_prop.bind(m_toolSize); m_prop.bind(m_accuracy); } ToolType getToolType() const { return TTool::LevelWriteTool; } TPropertyGroup *getProperties(int targetType) { return &m_prop; } void updateTranslation() { m_toolSize.setQStringName(tr("Size:")); m_accuracy.setQStringName(tr("Accuracy:")); } void onEnter(); void onLeave(); void draw(); void leftButtonDown(const TPointD &pos, const TMouseEvent &e); void leftButtonDrag(const TPointD &pos, const TMouseEvent &e); void leftButtonUp(const TPointD &pos, const TMouseEvent &e); void mouseMove(const TPointD &pos, const TMouseEvent &e); bool moveCursor(const TPointD &pos); int getCursorId() const { return m_cursorId; } void invalidateCursorArea(); void onDeactivate(); private: double actionRadius(double strokeLength); void splitStroke(TStroke *s); TStroke *mergeStrokes(const std::vector &strokes); } PumpToolInstance; //***************************************************************************** // PumpTool implementation //***************************************************************************** void PumpTool::onEnter() { m_draw = true; if (TTool::getApplication()->getCurrentObject()->isSpline() || !(TVectorImageP)getImage(false)) { m_enabled = false; m_cursorId = ToolCursor::CURSOR_NO; } else { m_enabled = true; m_cursorId = ToolCursor::PumpCursor; } } //---------------------------------------------------------------------- void PumpTool::draw() { if (!m_draw || !m_enabled) return; TVectorImageP vi = TImageP(getImage(false)); if (!vi) return; QMutexLocker lock(vi->getMutex()); TPalette *palette = vi->getPalette(); assert(palette); if (m_active) { //Editing with the tool assert(m_outStroke); TRectD bboxD(m_outStroke->getBBox()); TRect bbox(tfloor(bboxD.x0), tfloor(bboxD.y0), tceil(bboxD.x1) - 1, tceil(bboxD.y1) - 1); tglDraw(TVectorRenderData(TAffine(), bbox, palette, 0, true), m_outStroke); } else { //Hovering double w, dist; UINT index; if (m_cursorEnabled) { //Draw cursor glColor3d(1.0, 0.0, 1.0); if (m_cursor.thick > 0) tglDrawCircle(m_cursor, m_cursor.thick); tglDrawCircle(m_cursor, m_cursor.thick + 4 * getPixelSize()); } if (vi->getNearestStroke(m_cursor, w, index, dist, true)) { TStroke *stroke = vi->getStroke(index); double totalLen = stroke->getLength(); double actionLen = actionRadius(totalLen); tglColor(TPixel32::Red); if (totalLen < actionLen || (stroke->isSelfLoop() && totalLen < actionLen + actionLen)) drawStrokeCenterline(*stroke, getPixelSize()); else { int i, chunckIndex1, chunckIndex2; double t, t1, t2, w1, w2; double len = stroke->getLength(w); double len1 = len - actionLen; if (len1 < 0) if (stroke->isSelfLoop()) len1 += totalLen; else len1 = 0; double len2 = len + actionLen; if (len2 > totalLen) if (stroke->isSelfLoop()) len2 -= totalLen; else len2 = totalLen; w1 = stroke->getParameterAtLength(len1); w2 = stroke->getParameterAtLength(len2); int chunkCount = stroke->getChunkCount(); stroke->getChunkAndT(w1, chunckIndex1, t1); stroke->getChunkAndT(w2, chunckIndex2, t2); double step; const TThickQuadratic *q = 0; glBegin(GL_LINE_STRIP); q = stroke->getChunk(chunckIndex1); step = computeStep(*q, getPixelSize()); if (chunckIndex1 == chunckIndex2 && t1 < t2) { for (t = t1; t < t2; t += step) tglVertex(q->getPoint(t)); tglVertex(stroke->getPoint(w2)); glEnd(); return; } for (t = t1; t < 1; t += step) tglVertex(q->getPoint(t)); for (i = chunckIndex1 + 1; i != chunckIndex2; i++) { if (i == chunkCount) i = 0; if (i == chunckIndex2) break; q = stroke->getChunk(i); step = computeStep(*q, getPixelSize()); for (t = 0; t < 1; t += step) tglVertex(q->getPoint(t)); } q = stroke->getChunk(chunckIndex2); step = computeStep(*q, getPixelSize()); for (t = 0; t < t2; t += step) tglVertex(q->getPoint(t)); tglVertex(stroke->getPoint(w2)); glEnd(); } } } } //---------------------------------------------------------------------- void PumpTool::leftButtonDown(const TPointD &pos, const TMouseEvent &) { if (m_active || !m_enabled) return; assert(m_undo == 0); m_active = false; TVectorImageP vi(getImage(true)); if (!vi) return; QMutexLocker lock(vi->getMutex()); // set current point and init parameters m_oldPoint = pos; m_downPoint = pos; m_inStroke = m_outStroke = 0; m_stroke1Idx = m_stroke2Idx = -1; m_splitPars[0] = m_splitPars[1] = -2; m_actionW = 0; m_errorTol = (1.0 - 0.01 * m_accuracy.getValue()) * getPixelSize(); double dist2 = 0.0; int cpCount; int i; UINT index; if (vi->getNearestStroke(pos, m_actionW, index, dist2)) { //A stroke near the pressed point was found - modify it m_active = true; m_strokeIndex = index; m_inStroke = vi->getStroke(m_strokeIndex); m_outStroke = new TStroke(*m_inStroke); double totalLength = m_inStroke->getLength(); TXshSimpleLevel *sl = TTool::getApplication()->getCurrentLevel()->getSimpleLevel(); assert(sl); TFrameId id = getCurrentFid(); //Allocate the modification undo - will be assigned to the undo manager on mouse release m_undo = new UndoModifyStrokeAndPaint(sl, id, m_strokeIndex); //Set the stroke's style to 'none'. This is needed to make the original stroke transparent, //while the deformed one is shown at its place. m_strokeStyleId = m_inStroke->getStyle(); m_inStroke->setStyle(0); if (totalLength <= 0.0) { //Single point case cpCount = m_inStroke->getControlPointCount(); m_cpLenDiff1.resize(cpCount); for (i = 0; i < cpCount; i++) m_cpLenDiff1[i] = 0.0; m_splitStrokes.resize(1); m_splitStrokes[0] = new TStroke(*m_inStroke); m_stroke1Idx = 0; } else //Common strokes - split the stroke according to deformation requirements splitStroke(m_inStroke); } invalidate(); } //---------------------------------------------------------------------- void PumpTool::leftButtonDrag(const TPointD &pos, const TMouseEvent &e) { if (!m_active || !m_enabled) return; TVectorImageP vi(getImage(true)); if (!vi || !m_outStroke) return; QMutexLocker lock(vi->getMutex()); //Revert current deformation, recovering the one from button press delete m_outStroke; //Retrieve cursor's vertical displacement TPointD delta = TPointD(0, (pos - m_downPoint).y); int deltaSign = tsign(delta.y); if (deltaSign == 0) { //Use a copy of the original stroke m_outStroke = new TStroke(*m_inStroke); m_outStroke->setStyle(m_strokeStyleId); invalidate(); return; } //Build deformation upon the original stroke pieces TStroke *stroke1 = 0, *stroke2 = 0; stroke1 = new TStroke(*m_splitStrokes[m_stroke1Idx]); //Deform stroke1 TStrokeThicknessDeformation deformer(stroke1, delta, m_actionS1, m_actionRadius, deltaSign); modifyThickness(*stroke1, deformer, m_cpLenDiff1, deltaSign < 0); if (m_stroke2Idx >= 0) { //Deform stroke2 stroke2 = new TStroke(*m_splitStrokes[m_stroke2Idx]); TStrokeThicknessDeformation deformer2(stroke2, delta, m_actionS2, m_actionRadius, deltaSign); modifyThickness(*stroke2, deformer2, m_cpLenDiff2, deltaSign < 0); } //Apply deformation std::vector splitStrokesCopy(m_splitStrokes); splitStrokesCopy[m_stroke1Idx] = stroke1; if (stroke2) splitStrokesCopy[m_stroke2Idx] = stroke2; m_outStroke = mergeStrokes(splitStrokesCopy); delete stroke1; delete stroke2; invalidate(); } //---------------------------------------------------------------------- void PumpTool::leftButtonUp(const TPointD &pos, const TMouseEvent &) { TVectorImageP vi; if (!m_active || !m_enabled) goto cleanup; vi = TVectorImageP(getImage(true)); if (!vi) goto cleanup; { m_active = false; QMutexLocker lock(vi->getMutex()); //Reset cursor data double t; UINT index; double dist2; if (vi->getNearestStroke(pos, t, index, dist2)) { TStroke *nearestStroke = vi->getStroke(index); if (nearestStroke) m_cursor = nearestStroke->getThickPoint(t); } if (m_outStroke && !areAlmostEqual(m_downPoint, pos, PickRadius * getPixelSize())) { //Accept action //Clone input stroke - it is someway needed by the stroke change notifier... I wonder why... TStroke *oldStroke = new TStroke(*m_inStroke); m_outStroke->swap(*m_inStroke); m_inStroke->invalidate(); delete m_outStroke; m_outStroke = 0; assert(m_undo); TUndoManager::manager()->add(m_undo); m_undo = 0; vi->notifyChangedStrokes(m_strokeIndex, oldStroke); notifyImageChanged(); delete oldStroke; } } cleanup: if (m_inStroke) m_inStroke->setStyle(m_strokeStyleId); //Make the image stroke visible again m_strokeIndex = m_strokeStyleId = -1; clearPointerContainer(m_splitStrokes); delete m_outStroke; m_inStroke = m_outStroke = 0; delete m_undo; m_undo = 0; invalidate(); } //---------------------------------------------------------------------- void PumpTool::invalidateCursorArea() { double r = m_cursor.thick + 6; TPointD d(r, r); invalidate(TRectD(m_cursor - d, m_cursor + d)); } //---------------------------------------------------------------------- void PumpTool::mouseMove(const TPointD &pos, const TMouseEvent &e) { if (m_active || !m_enabled) return; //Cursor preview updates on 3-pixel steps if (tdistance2(pos, m_oldPoint) < 9.0 * sq(getPixelSize())) return; if (!m_draw) m_draw = true; m_oldPoint = pos; if (moveCursor(pos)) { m_cursorEnabled = true; invalidate(); } else m_cursorEnabled = false; invalidate(); } //---------------------------------------------------------------------- bool PumpTool::moveCursor(const TPointD &pos) { TVectorImageP vi(getImage(false)); if (vi) { double t; UINT index; double dist2; if (vi->getNearestStroke(pos, t, index, dist2)) { TStroke *stroke = vi->getStroke(index); if (stroke) { m_cursor = stroke->getThickPoint(t); return true; } } } return false; } //---------------------------------------------------------------------- void PumpTool::onDeactivate() { m_draw = false; if (m_active) { m_active = false; TVectorImageP vi(getImage(true)); assert(!!vi && m_outStroke); if (!vi || !m_outStroke) return; clearPointerContainer(m_splitStrokes); if (m_splitPars[0] == -1) { delete m_outStroke; m_outStroke = 0; } // restore previous style assert(m_strokeIndex >= 0); if (m_strokeIndex >= 0) { TStroke *stroke = vi->getStroke(m_strokeIndex); stroke->setStyle(m_strokeStyleId); } assert(m_undo); delete m_undo; m_undo = 0; invalidate(); m_strokeIndex = -1; m_outStroke = 0; } } //---------------------------------------------------------------------- void PumpTool::onLeave() { if (!m_active) m_draw = false; } //***************************************************************************** // PumpTool privates //***************************************************************************** double PumpTool::actionRadius(double strokeLength) { double toolSize = tmax(m_toolSize.getValue(), 5.0); double toolPercent = toolSize * 0.01; double interpolationVal = pow(toolPercent, 5); double indipendentValue = 7.0 * toolSize; double actionRadius = (indipendentValue) * (1.0 - interpolationVal) + (strokeLength * toolPercent) * interpolationVal; return tmax(actionRadius, indipendentValue); } //---------------------------------------------------------------------- /* Edited strokes are split near the corresponding editing position, in order to localize stroke manipulation. Only the localized part of the stroke will receive CP increase and thickness tuning needed for the tool action. */ void PumpTool::splitStroke(TStroke *s) { assert(m_splitStrokes.empty()); TStroke *stroke1 = 0, *stroke2 = 0; //Build the action radius double totalLength = s->getLength(); m_actionRadius = actionRadius(totalLength); //Get the length at selected point and build the split (length) positions m_actionS1 = s->getLength(m_actionW); double startLen = m_actionS1 - m_actionRadius; double endLen = m_actionS1 + m_actionRadius; //Now, perform splitting int i, cpCount; if ((startLen <= 0 && endLen >= totalLength) || (s->isSelfLoop() && totalLength < (m_actionRadius + m_actionRadius))) { //The whole stroke is included in the action - no split m_splitStrokes.resize(1); m_splitPars[0] = -1; m_splitStrokes[0] = new TStroke(*s); m_stroke1Idx = 0; stroke1 = m_splitStrokes[m_stroke1Idx]; TStrokeThicknessDeformation deformer(s, m_actionS1, m_actionRadius); increaseControlPoints(*stroke1, deformer, getPixelSize()); } else { if (!s->isSelfLoop() || (startLen >= 0.0 && endLen <= totalLength)) { //Regular split positions, in the [0.0, totalLength] range. //Split points at extremities are dealt. m_splitPars[0] = s->getParameterAtLength(tmax(startLen, 0.0)); //Crop in the open case m_splitPars[1] = s->getParameterAtLength(tmin(endLen, totalLength)); if (m_splitPars[0] == 0.0) // the "&& m_splitPars[0] == totalLength" was dealt outside { m_splitStrokes.resize(2); m_splitStrokes[0] = new TStroke; m_splitStrokes[1] = new TStroke; s->split(m_splitPars[1], *(m_splitStrokes[0]), *(m_splitStrokes[1])); m_stroke1Idx = 0; } else { if (m_splitPars[1] == 1.0) { m_splitStrokes.resize(2); m_splitStrokes[0] = new TStroke; m_splitStrokes[1] = new TStroke; s->split(m_splitPars[0], *(m_splitStrokes[0]), *(m_splitStrokes[1])); } else ::splitStroke(*s, m_splitPars, m_splitStrokes); m_stroke1Idx = 1; //Update the edit point to refer to the central stroke piece m_actionS1 -= m_splitStrokes[0]->getLength(); } stroke1 = m_splitStrokes[m_stroke1Idx]; //Apply deformation to the middle piece TStrokeThicknessDeformation deformer(stroke1, m_actionS1, m_actionRadius); increaseControlPoints(*stroke1, deformer, getPixelSize()); m_actionS2 = 0; } else { //Circular 'overflow' case - (exactly) one split point is outside the regular scope. //Since the action diameter is < totalLength, these cases are mutually exclusive. if (startLen < 0) startLen += totalLength; else { endLen -= totalLength; m_actionS1 -= totalLength; } //The deformation must be applied in two distinct strokes, since its //action interval crosses the junction point m_splitPars[0] = s->getParameterAtLength(endLen); m_splitPars[1] = s->getParameterAtLength(startLen); ::splitStroke(*s, m_splitPars, m_splitStrokes); assert(m_splitStrokes.size() >= 3); m_stroke1Idx = 0; m_stroke2Idx = 2; stroke1 = m_splitStrokes[m_stroke1Idx]; stroke2 = m_splitStrokes[m_stroke2Idx]; m_actionS2 = m_actionS1 + stroke2->getLength(); TStrokeThicknessDeformation deformer(stroke1, m_actionS1, m_actionRadius); increaseControlPoints(*stroke1, deformer, getPixelSize()); TStrokeThicknessDeformation deformer2(stroke2, m_actionS2, m_actionRadius); increaseControlPoints(*stroke2, deformer2, getPixelSize()); cpCount = stroke2->getControlPointCount(); m_cpLenDiff2.resize(cpCount); for (i = 0; i < cpCount; ++i) m_cpLenDiff2[i] = stroke2->getLengthAtControlPoint(i) - m_actionS2; } } cpCount = stroke1->getControlPointCount(); m_cpLenDiff1.resize(cpCount); double diff; for (i = 0; i < cpCount; i++) { diff = stroke1->getLengthAtControlPoint(i) - m_actionS1; m_cpLenDiff1[i] = (s->isSelfLoop() && stroke2 && totalLength - diff < diff) ? totalLength - diff : diff; } } //---------------------------------------------------------------------- /* A split stroke must be reassembled before it is output. In particular, it must be ensured that the merge does not add additional CPS at split points, leaving the output seamless. */ TStroke *PumpTool::mergeStrokes(const std::vector &strokes) { assert(strokes.size() > 0); TStroke *mergedStroke; if (strokes.size() > 1) { if (m_errorTol > 0.0) { strokes[m_stroke1Idx]->reduceControlPoints(m_errorTol); if (m_stroke2Idx >= 0) strokes[m_stroke2Idx]->reduceControlPoints(m_errorTol); } //Merge split strokes mergedStroke = merge(strokes); //mergedStroke->reduceControlPoints(0.4*getPixelSize()); //Originally on the whole result... if (m_inStroke->isSelfLoop()) { int cpCount = mergedStroke->getControlPointCount(); TThickPoint p1 = mergedStroke->getControlPoint(0); TThickPoint p2 = mergedStroke->getControlPoint(cpCount - 1); TThickPoint midP = 0.5 * (p1 + p2); mergedStroke->setControlPoint(0, midP); mergedStroke->setControlPoint(cpCount - 1, midP); mergedStroke->setSelfLoop(true); } mergedStroke->outlineOptions() = strokes[0]->outlineOptions(); } else { mergedStroke = new TStroke(*strokes[0]); if (m_errorTol > 0.0) mergedStroke->reduceControlPoints(m_errorTol); } mergedStroke->setStyle(m_strokeStyleId); mergedStroke->invalidate(); return mergedStroke; }