mirror of
https://github.com/juce-framework/JUCE.git
synced 2026-01-09 23:34:20 +00:00
1678 lines
53 KiB
C++
1678 lines
53 KiB
C++
/*
|
|
==============================================================================
|
|
|
|
This file is part of the JUCE framework.
|
|
Copyright (c) Raw Material Software Limited
|
|
|
|
JUCE is an open source framework subject to commercial or open source
|
|
licensing.
|
|
|
|
By downloading, installing, or using the JUCE framework, or combining the
|
|
JUCE framework with any other source code, object code, content or any other
|
|
copyrightable work, you agree to the terms of the JUCE End User Licence
|
|
Agreement, and all incorporated terms including the JUCE Privacy Policy and
|
|
the JUCE Website Terms of Service, as applicable, which will bind you. If you
|
|
do not agree to the terms of these agreements, we will not license the JUCE
|
|
framework to you, and you must discontinue the installation or download
|
|
process and cease use of the JUCE framework.
|
|
|
|
JUCE End User Licence Agreement: https://juce.com/legal/juce-8-licence/
|
|
JUCE Privacy Policy: https://juce.com/juce-privacy-policy
|
|
JUCE Website Terms of Service: https://juce.com/juce-website-terms-of-service/
|
|
|
|
Or:
|
|
|
|
You may also use this code under the terms of the AGPLv3:
|
|
https://www.gnu.org/licenses/agpl-3.0.en.html
|
|
|
|
THE JUCE FRAMEWORK IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL
|
|
WARRANTIES, WHETHER EXPRESSED OR IMPLIED, INCLUDING WARRANTY OF
|
|
MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE, ARE DISCLAIMED.
|
|
|
|
==============================================================================
|
|
*/
|
|
|
|
#include "../../Application/jucer_Headers.h"
|
|
#include "jucer_PaintElementPath.h"
|
|
#include "../Properties/jucer_PositionPropertyBase.h"
|
|
#include "jucer_PaintElementUndoableAction.h"
|
|
#include "../jucer_UtilityFunctions.h"
|
|
|
|
//==============================================================================
|
|
class ChangePointAction : public PaintElementUndoableAction <PaintElementPath>
|
|
{
|
|
public:
|
|
ChangePointAction (PathPoint* const point,
|
|
const int pointIndex,
|
|
const PathPoint& newValue_)
|
|
: PaintElementUndoableAction <PaintElementPath> (point->owner),
|
|
index (pointIndex),
|
|
newValue (newValue_),
|
|
oldValue (*point)
|
|
{
|
|
}
|
|
|
|
ChangePointAction (PathPoint* const point,
|
|
const PathPoint& newValue_)
|
|
: PaintElementUndoableAction <PaintElementPath> (point->owner),
|
|
index (point->owner->indexOfPoint (point)),
|
|
newValue (newValue_),
|
|
oldValue (*point)
|
|
{
|
|
}
|
|
|
|
bool perform() override
|
|
{
|
|
return changeTo (newValue);
|
|
}
|
|
|
|
bool undo() override
|
|
{
|
|
return changeTo (oldValue);
|
|
}
|
|
|
|
private:
|
|
const int index;
|
|
PathPoint newValue, oldValue;
|
|
|
|
PathPoint* getPoint() const
|
|
{
|
|
PathPoint* p = getElement()->getPoint (index);
|
|
jassert (p != nullptr);
|
|
return p;
|
|
}
|
|
|
|
bool changeTo (const PathPoint& value) const
|
|
{
|
|
showCorrectTab();
|
|
|
|
if (auto* const path = getElement())
|
|
{
|
|
if (auto* const p = path->getPoint (index))
|
|
{
|
|
const auto typeChanged = (p->type != value.type);
|
|
*p = value;
|
|
p->owner = path;
|
|
|
|
if (typeChanged)
|
|
path->pointListChanged();
|
|
|
|
path->changed();
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
};
|
|
|
|
|
|
//==============================================================================
|
|
class PathWindingModeProperty : public ChoicePropertyComponent,
|
|
private ChangeListener
|
|
{
|
|
public:
|
|
explicit PathWindingModeProperty (PaintElementPath* const owner_)
|
|
: ChoicePropertyComponent ("winding rule"),
|
|
owner (owner_)
|
|
{
|
|
choices.add ("Non-zero winding");
|
|
choices.add ("Even/odd winding");
|
|
|
|
owner->getDocument()->addChangeListener (this);
|
|
}
|
|
|
|
~PathWindingModeProperty() override
|
|
{
|
|
owner->getDocument()->removeChangeListener (this);
|
|
}
|
|
|
|
void setIndex (int newIndex) override { owner->setNonZeroWinding (newIndex == 0, true); }
|
|
int getIndex() const override { return owner->isNonZeroWinding() ? 0 : 1; }
|
|
|
|
private:
|
|
void changeListenerCallback (ChangeBroadcaster*) override { refresh(); }
|
|
|
|
PaintElementPath* const owner;
|
|
};
|
|
|
|
|
|
//==============================================================================
|
|
PaintElementPath::PaintElementPath (PaintRoutine* pr)
|
|
: ColouredElement (pr, "Path", true, true),
|
|
nonZeroWinding (true)
|
|
{
|
|
}
|
|
|
|
PaintElementPath::~PaintElementPath()
|
|
{
|
|
}
|
|
|
|
static int randomPos (int size)
|
|
{
|
|
return size / 4 + Random::getSystemRandom().nextInt (size / 4) - size / 8;
|
|
}
|
|
|
|
void PaintElementPath::setInitialBounds (int w, int h)
|
|
{
|
|
String s;
|
|
|
|
int x = randomPos (w);
|
|
int y = randomPos (h);
|
|
|
|
s << "s "
|
|
<< x << " " << y << " l "
|
|
<< (x + 30) << " " << (y + 50) << " l "
|
|
<< (x - 30) << " " << (y + 50) << " x";
|
|
|
|
restorePathFromString (s);
|
|
}
|
|
|
|
//==============================================================================
|
|
int PaintElementPath::getBorderSize() const
|
|
{
|
|
return isStrokePresent ? 1 + roundToInt (strokeType.stroke.getStrokeThickness())
|
|
: 0;
|
|
}
|
|
|
|
Rectangle<int> PaintElementPath::getCurrentBounds (const Rectangle<int>& parentArea) const
|
|
{
|
|
updateStoredPath (getDocument()->getComponentLayout(), parentArea);
|
|
|
|
Rectangle<float> r (path.getBounds());
|
|
|
|
const int borderSize = getBorderSize();
|
|
|
|
return Rectangle<int> ((int) r.getX() - borderSize,
|
|
(int) r.getY() - borderSize,
|
|
(int) r.getWidth() + borderSize * 2,
|
|
(int) r.getHeight() + borderSize * 2);
|
|
}
|
|
|
|
void PaintElementPath::setCurrentBounds (const Rectangle<int>& b,
|
|
const Rectangle<int>& parentArea,
|
|
const bool /*undoable*/)
|
|
{
|
|
Rectangle<int> newBounds (b);
|
|
newBounds.setSize (jmax (1, newBounds.getWidth()),
|
|
jmax (1, newBounds.getHeight()));
|
|
|
|
const Rectangle<int> current (getCurrentBounds (parentArea));
|
|
|
|
if (newBounds != current)
|
|
{
|
|
const int borderSize = getBorderSize();
|
|
|
|
const int dx = newBounds.getX() - current.getX();
|
|
const int dy = newBounds.getY() - current.getY();
|
|
|
|
const double scaleStartX = current.getX() + borderSize;
|
|
const double scaleStartY = current.getY() + borderSize;
|
|
const double scaleX = (newBounds.getWidth() - borderSize * 2) / (double) (current.getWidth() - borderSize * 2);
|
|
const double scaleY = (newBounds.getHeight() - borderSize * 2) / (double) (current.getHeight() - borderSize * 2);
|
|
|
|
for (int i = 0; i < points.size(); ++i)
|
|
{
|
|
PathPoint* const destPoint = points.getUnchecked (i);
|
|
PathPoint p (*destPoint);
|
|
|
|
for (int j = p.getNumPoints(); --j >= 0;)
|
|
rescalePoint (p.pos[j], dx, dy,
|
|
scaleX, scaleY,
|
|
scaleStartX, scaleStartY,
|
|
parentArea);
|
|
|
|
perform (new ChangePointAction (destPoint, i, p), "Move path");
|
|
}
|
|
}
|
|
}
|
|
|
|
void PaintElementPath::rescalePoint (RelativePositionedRectangle& pos, int dx, int dy,
|
|
double scaleX, double scaleY,
|
|
double scaleStartX, double scaleStartY,
|
|
const Rectangle<int>& parentArea) const
|
|
{
|
|
double x, y, w, h;
|
|
pos.getRectangleDouble (x, y, w, h, parentArea, getDocument()->getComponentLayout());
|
|
|
|
x = (x - scaleStartX) * scaleX + scaleStartX + dx;
|
|
y = (y - scaleStartY) * scaleY + scaleStartY + dy;
|
|
|
|
pos.updateFrom (x, y, w, h, parentArea, getDocument()->getComponentLayout());
|
|
}
|
|
|
|
//==============================================================================
|
|
static void drawArrow (Graphics& g, const Point<float> p1, const Point<float> p2)
|
|
{
|
|
g.drawArrow (Line<float> (p1.x, p1.y, (p1.x + p2.x) * 0.5f, (p1.y + p2.y) * 0.5f), 1.0f, 8.0f, 10.0f);
|
|
g.drawLine (p1.x + (p2.x - p1.x) * 0.49f, p1.y + (p2.y - p1.y) * 0.49f, p2.x, p2.y);
|
|
}
|
|
|
|
void PaintElementPath::draw (Graphics& g, const ComponentLayout* layout, const Rectangle<int>& parentArea)
|
|
{
|
|
updateStoredPath (layout, parentArea);
|
|
path.setUsingNonZeroWinding (nonZeroWinding);
|
|
|
|
fillType.setFillType (g, getDocument(), parentArea);
|
|
g.fillPath (path);
|
|
|
|
if (isStrokePresent)
|
|
{
|
|
strokeType.fill.setFillType (g, getDocument(), parentArea);
|
|
g.strokePath (path, getStrokeType().stroke);
|
|
}
|
|
}
|
|
|
|
void PaintElementPath::drawExtraEditorGraphics (Graphics& g, const Rectangle<int>& relativeTo)
|
|
{
|
|
ComponentLayout* layout = getDocument()->getComponentLayout();
|
|
|
|
for (int i = 0; i < points.size(); ++i)
|
|
{
|
|
PathPoint* const p = points.getUnchecked (i);
|
|
|
|
const int numPoints = p->getNumPoints();
|
|
|
|
if (numPoints > 0)
|
|
{
|
|
if (owner->getSelectedPoints().isSelected (p))
|
|
{
|
|
g.setColour (Colours::red);
|
|
Point<float> p1, p2;
|
|
|
|
if (numPoints > 2)
|
|
{
|
|
p1 = p->pos[1].toXY (relativeTo, layout);
|
|
p2 = p->pos[2].toXY (relativeTo, layout);
|
|
drawArrow (g, p1, p2);
|
|
}
|
|
|
|
if (numPoints > 1)
|
|
{
|
|
p1 = p->pos[0].toXY (relativeTo, layout);
|
|
p2 = p->pos[1].toXY (relativeTo, layout);
|
|
drawArrow (g, p1, p2);
|
|
}
|
|
|
|
p2 = p->pos[0].toXY (relativeTo, layout);
|
|
|
|
if (const PathPoint* const nextPoint = points [i - 1])
|
|
{
|
|
p1 = nextPoint->pos [nextPoint->getNumPoints() - 1].toXY (relativeTo, layout);
|
|
drawArrow (g, p1, p2);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void PaintElementPath::resized()
|
|
{
|
|
ColouredElement::resized();
|
|
}
|
|
|
|
void PaintElementPath::parentSizeChanged()
|
|
{
|
|
repaint();
|
|
}
|
|
|
|
//==============================================================================
|
|
void PaintElementPath::mouseDown (const MouseEvent& e)
|
|
{
|
|
if (e.mods.isPopupMenu() || ! owner->getSelectedElements().isSelected (this))
|
|
mouseDownOnSegment = -1;
|
|
else
|
|
mouseDownOnSegment = findSegmentAtXY (getX() + e.x, getY() + e.y);
|
|
|
|
if (points [mouseDownOnSegment] != nullptr)
|
|
mouseDownSelectSegmentStatus = owner->getSelectedPoints().addToSelectionOnMouseDown (points [mouseDownOnSegment], e.mods);
|
|
else
|
|
ColouredElement::mouseDown (e);
|
|
}
|
|
|
|
void PaintElementPath::mouseDrag (const MouseEvent& e)
|
|
{
|
|
if (mouseDownOnSegment < 0)
|
|
ColouredElement::mouseDrag (e);
|
|
}
|
|
|
|
void PaintElementPath::mouseUp (const MouseEvent& e)
|
|
{
|
|
if (points[mouseDownOnSegment] == nullptr)
|
|
ColouredElement::mouseUp (e);
|
|
else
|
|
owner->getSelectedPoints().addToSelectionOnMouseUp (points [mouseDownOnSegment],
|
|
e.mods, false, mouseDownSelectSegmentStatus);
|
|
}
|
|
|
|
//==============================================================================
|
|
void PaintElementPath::changed()
|
|
{
|
|
ColouredElement::changed();
|
|
lastPathBounds = Rectangle<int>();
|
|
}
|
|
|
|
void PaintElementPath::pointListChanged()
|
|
{
|
|
changed();
|
|
siblingComponentsChanged();
|
|
}
|
|
|
|
//==============================================================================
|
|
void PaintElementPath::getEditableProperties (Array <PropertyComponent*>& props, bool multipleSelected)
|
|
{
|
|
if (multipleSelected)
|
|
return;
|
|
|
|
props.add (new PathWindingModeProperty (this));
|
|
getColourSpecificProperties (props);
|
|
}
|
|
|
|
//==============================================================================
|
|
static String positionToPairOfValues (const RelativePositionedRectangle& position,
|
|
const ComponentLayout* layout)
|
|
{
|
|
String x, y, w, h;
|
|
positionToCode (position, layout, x, y, w, h);
|
|
return castToFloat (x) + ", " + castToFloat (y);
|
|
}
|
|
|
|
void PaintElementPath::fillInGeneratedCode (GeneratedCode& code, String& paintMethodCode)
|
|
{
|
|
if (fillType.isInvisible() && (strokeType.isInvisible() || ! isStrokePresent))
|
|
return;
|
|
|
|
const String pathVariable ("internalPath" + String (code.getUniqueSuffix()));
|
|
|
|
const ComponentLayout* layout = code.document->getComponentLayout();
|
|
|
|
code.privateMemberDeclarations
|
|
<< "juce::Path " << pathVariable << ";\n";
|
|
|
|
String r;
|
|
bool somePointsAreRelative = false;
|
|
|
|
if (! nonZeroWinding)
|
|
r << pathVariable << ".setUsingNonZeroWinding (false);\n";
|
|
|
|
for (auto* p : points)
|
|
{
|
|
switch (p->type)
|
|
{
|
|
case Path::Iterator::startNewSubPath:
|
|
r << pathVariable << ".startNewSubPath (" << positionToPairOfValues (p->pos[0], layout) << ");\n";
|
|
somePointsAreRelative = somePointsAreRelative || ! p->pos[0].rect.isPositionAbsolute();
|
|
break;
|
|
|
|
case Path::Iterator::lineTo:
|
|
r << pathVariable << ".lineTo (" << positionToPairOfValues (p->pos[0], layout) << ");\n";
|
|
somePointsAreRelative = somePointsAreRelative || ! p->pos[0].rect.isPositionAbsolute();
|
|
break;
|
|
|
|
case Path::Iterator::quadraticTo:
|
|
r << pathVariable << ".quadraticTo (" << positionToPairOfValues (p->pos[0], layout)
|
|
<< ", " << positionToPairOfValues (p->pos[1], layout) << ");\n";
|
|
|
|
somePointsAreRelative = somePointsAreRelative || ! p->pos[0].rect.isPositionAbsolute();
|
|
somePointsAreRelative = somePointsAreRelative || ! p->pos[1].rect.isPositionAbsolute();
|
|
break;
|
|
|
|
case Path::Iterator::cubicTo:
|
|
r << pathVariable << ".cubicTo (" << positionToPairOfValues (p->pos[0], layout)
|
|
<< ", " << positionToPairOfValues (p->pos[1], layout)
|
|
<< ", " << positionToPairOfValues (p->pos[2], layout) << ");\n";
|
|
|
|
somePointsAreRelative = somePointsAreRelative || ! p->pos[0].rect.isPositionAbsolute();
|
|
somePointsAreRelative = somePointsAreRelative || ! p->pos[1].rect.isPositionAbsolute();
|
|
somePointsAreRelative = somePointsAreRelative || ! p->pos[2].rect.isPositionAbsolute();
|
|
break;
|
|
|
|
case Path::Iterator::closePath:
|
|
r << pathVariable << ".closeSubPath();\n";
|
|
break;
|
|
|
|
default:
|
|
jassertfalse;
|
|
break;
|
|
}
|
|
}
|
|
|
|
r << '\n';
|
|
|
|
if (somePointsAreRelative)
|
|
code.getCallbackCode (String(), "void", "resized()", false)
|
|
<< pathVariable << ".clear();\n" << r;
|
|
else
|
|
code.constructorCode << r;
|
|
|
|
String s;
|
|
s << "{\n"
|
|
<< " float x = 0, y = 0;\n";
|
|
|
|
if (! fillType.isInvisible())
|
|
s << " " << fillType.generateVariablesCode ("fill");
|
|
|
|
if (isStrokePresent && ! strokeType.isInvisible())
|
|
s << " " << strokeType.fill.generateVariablesCode ("stroke");
|
|
|
|
s << " //[UserPaintCustomArguments] Customize the painting arguments here..\n"
|
|
<< customPaintCode
|
|
<< " //[/UserPaintCustomArguments]\n";
|
|
|
|
RelativePositionedRectangle zero;
|
|
|
|
if (! fillType.isInvisible())
|
|
{
|
|
s << " ";
|
|
fillType.fillInGeneratedCode ("fill", zero, code, s);
|
|
s << " g.fillPath (" << pathVariable << ", juce::AffineTransform::translation (x, y));\n";
|
|
}
|
|
|
|
if (isStrokePresent && ! strokeType.isInvisible())
|
|
{
|
|
s << " ";
|
|
strokeType.fill.fillInGeneratedCode ("stroke", zero, code, s);
|
|
s << " g.strokePath (" << pathVariable << ", " << strokeType.getPathStrokeCode() << ", juce::AffineTransform::translation (x, y));\n";
|
|
}
|
|
|
|
s << "}\n\n";
|
|
|
|
paintMethodCode += s;
|
|
}
|
|
|
|
void PaintElementPath::applyCustomPaintSnippets (StringArray& snippets)
|
|
{
|
|
customPaintCode.clear();
|
|
|
|
if (! snippets.isEmpty() && (! fillType.isInvisible() || (isStrokePresent && ! strokeType.isInvisible())))
|
|
{
|
|
customPaintCode = snippets[0];
|
|
snippets.remove (0);
|
|
}
|
|
}
|
|
|
|
//==============================================================================
|
|
XmlElement* PaintElementPath::createXml() const
|
|
{
|
|
XmlElement* e = new XmlElement (getTagName());
|
|
position.applyToXml (*e);
|
|
addColourAttributes (e);
|
|
e->setAttribute ("nonZeroWinding", nonZeroWinding);
|
|
e->addTextElement (pathToString());
|
|
|
|
return e;
|
|
}
|
|
|
|
bool PaintElementPath::loadFromXml (const XmlElement& xml)
|
|
{
|
|
if (xml.hasTagName (getTagName()))
|
|
{
|
|
position.restoreFromXml (xml, position);
|
|
loadColourAttributes (xml);
|
|
nonZeroWinding = xml.getBoolAttribute ("nonZeroWinding", true);
|
|
|
|
restorePathFromString (xml.getAllSubText().trim());
|
|
|
|
return true;
|
|
}
|
|
|
|
jassertfalse;
|
|
return false;
|
|
}
|
|
|
|
//==============================================================================
|
|
void PaintElementPath::createSiblingComponents()
|
|
{
|
|
ColouredElement::createSiblingComponents();
|
|
|
|
for (int i = 0; i < points.size(); ++i)
|
|
{
|
|
switch (points.getUnchecked (i)->type)
|
|
{
|
|
case Path::Iterator::startNewSubPath:
|
|
siblingComponents.add (new PathPointComponent (this, i, 0));
|
|
break;
|
|
case Path::Iterator::lineTo:
|
|
siblingComponents.add (new PathPointComponent (this, i, 0));
|
|
break;
|
|
case Path::Iterator::quadraticTo:
|
|
siblingComponents.add (new PathPointComponent (this, i, 0));
|
|
siblingComponents.add (new PathPointComponent (this, i, 1));
|
|
break;
|
|
case Path::Iterator::cubicTo:
|
|
siblingComponents.add (new PathPointComponent (this, i, 0));
|
|
siblingComponents.add (new PathPointComponent (this, i, 1));
|
|
siblingComponents.add (new PathPointComponent (this, i, 2));
|
|
break;
|
|
case Path::Iterator::closePath:
|
|
break;
|
|
|
|
default:
|
|
jassertfalse; break;
|
|
}
|
|
}
|
|
|
|
for (int i = 0; i < siblingComponents.size(); ++i)
|
|
{
|
|
getParentComponent()->addAndMakeVisible (siblingComponents.getUnchecked (i));
|
|
siblingComponents.getUnchecked (i)->updatePosition();
|
|
}
|
|
}
|
|
|
|
|
|
String PaintElementPath::pathToString() const
|
|
{
|
|
String s;
|
|
|
|
for (int i = 0; i < points.size(); ++i)
|
|
{
|
|
const PathPoint* const p = points.getUnchecked (i);
|
|
|
|
switch (p->type)
|
|
{
|
|
case Path::Iterator::startNewSubPath:
|
|
s << "s " << p->pos[0].toString() << ' ';
|
|
break;
|
|
case Path::Iterator::lineTo:
|
|
s << "l " << p->pos[0].toString() << ' ';
|
|
break;
|
|
case Path::Iterator::quadraticTo:
|
|
s << "q " << p->pos[0].toString()
|
|
<< ' ' << p->pos[1].toString() << ' ';
|
|
break;
|
|
case Path::Iterator::cubicTo:
|
|
s << "c " << p->pos[0].toString()
|
|
<< ' ' << p->pos[1].toString() << ' '
|
|
<< ' ' << p->pos[2].toString() << ' ';
|
|
break;
|
|
case Path::Iterator::closePath:
|
|
s << "x ";
|
|
break;
|
|
|
|
default:
|
|
jassertfalse; break;
|
|
}
|
|
}
|
|
|
|
return s.trimEnd();
|
|
}
|
|
|
|
void PaintElementPath::restorePathFromString (const String& s)
|
|
{
|
|
points.clear();
|
|
|
|
StringArray tokens;
|
|
tokens.addTokens (s, false);
|
|
tokens.trim();
|
|
tokens.removeEmptyStrings();
|
|
|
|
for (int i = 0; i < tokens.size(); ++i)
|
|
{
|
|
std::unique_ptr<PathPoint> p (new PathPoint (this));
|
|
|
|
if (tokens[i] == "s")
|
|
{
|
|
p->type = Path::Iterator::startNewSubPath;
|
|
p->pos [0] = RelativePositionedRectangle();
|
|
p->pos [0].rect = PositionedRectangle (tokens [i + 1] + " " + tokens [i + 2]);
|
|
i += 2;
|
|
}
|
|
else if (tokens[i] == "l")
|
|
{
|
|
p->type = Path::Iterator::lineTo;
|
|
p->pos [0] = RelativePositionedRectangle();
|
|
p->pos [0].rect = PositionedRectangle (tokens [i + 1] + " " + tokens [i + 2]);
|
|
i += 2;
|
|
}
|
|
else if (tokens[i] == "q")
|
|
{
|
|
p->type = Path::Iterator::quadraticTo;
|
|
p->pos [0] = RelativePositionedRectangle();
|
|
p->pos [0].rect = PositionedRectangle (tokens [i + 1] + " " + tokens [i + 2]);
|
|
p->pos [1] = RelativePositionedRectangle();
|
|
p->pos [1].rect = PositionedRectangle (tokens [i + 3] + " " + tokens [i + 4]);
|
|
i += 4;
|
|
}
|
|
else if (tokens[i] == "c")
|
|
{
|
|
p->type = Path::Iterator::cubicTo;
|
|
p->pos [0] = RelativePositionedRectangle();
|
|
p->pos [0].rect = PositionedRectangle (tokens [i + 1] + " " + tokens [i + 2]);
|
|
p->pos [1] = RelativePositionedRectangle();
|
|
p->pos [1].rect = PositionedRectangle (tokens [i + 3] + " " + tokens [i + 4]);
|
|
p->pos [2] = RelativePositionedRectangle();
|
|
p->pos [2].rect = PositionedRectangle (tokens [i + 5] + " " + tokens [i + 6]);
|
|
i += 6;
|
|
}
|
|
else if (tokens[i] == "x")
|
|
{
|
|
p->type = Path::Iterator::closePath;
|
|
}
|
|
else
|
|
continue;
|
|
|
|
points.add (p.release());
|
|
}
|
|
}
|
|
|
|
void PaintElementPath::setToPath (const Path& newPath)
|
|
{
|
|
points.clear();
|
|
|
|
Path::Iterator i (newPath);
|
|
|
|
while (i.next())
|
|
{
|
|
std::unique_ptr<PathPoint> p (new PathPoint (this));
|
|
p->type = i.elementType;
|
|
|
|
if (i.elementType == Path::Iterator::startNewSubPath)
|
|
{
|
|
p->pos [0].rect.setX (i.x1);
|
|
p->pos [0].rect.setY (i.y1);
|
|
}
|
|
else if (i.elementType == Path::Iterator::lineTo)
|
|
{
|
|
p->pos [0].rect.setX (i.x1);
|
|
p->pos [0].rect.setY (i.y1);
|
|
}
|
|
else if (i.elementType == Path::Iterator::quadraticTo)
|
|
{
|
|
p->pos [0].rect.setX (i.x1);
|
|
p->pos [0].rect.setY (i.y1);
|
|
p->pos [1].rect.setX (i.x2);
|
|
p->pos [1].rect.setY (i.y2);
|
|
}
|
|
else if (i.elementType == Path::Iterator::cubicTo)
|
|
{
|
|
p->pos [0].rect.setX (i.x1);
|
|
p->pos [0].rect.setY (i.y1);
|
|
p->pos [1].rect.setX (i.x2);
|
|
p->pos [1].rect.setY (i.y2);
|
|
p->pos [2].rect.setX (i.x3);
|
|
p->pos [2].rect.setY (i.y3);
|
|
}
|
|
else if (i.elementType == Path::Iterator::closePath)
|
|
{
|
|
}
|
|
else
|
|
{
|
|
continue;
|
|
}
|
|
|
|
points.add (p.release());
|
|
}
|
|
}
|
|
|
|
void PaintElementPath::updateStoredPath (const ComponentLayout* layout, const Rectangle<int>& relativeTo) const
|
|
{
|
|
if (lastPathBounds != relativeTo && ! relativeTo.isEmpty())
|
|
{
|
|
lastPathBounds = relativeTo;
|
|
path.clear();
|
|
|
|
for (int i = 0; i < points.size(); ++i)
|
|
{
|
|
const PathPoint* const p = points.getUnchecked (i);
|
|
|
|
switch (p->type)
|
|
{
|
|
case Path::Iterator::startNewSubPath:
|
|
path.startNewSubPath (p->pos[0].toXY (relativeTo, layout));
|
|
break;
|
|
|
|
case Path::Iterator::lineTo:
|
|
path.lineTo (p->pos[0].toXY (relativeTo, layout));
|
|
break;
|
|
|
|
case Path::Iterator::quadraticTo:
|
|
path.quadraticTo (p->pos[0].toXY (relativeTo, layout),
|
|
p->pos[1].toXY (relativeTo, layout));
|
|
break;
|
|
|
|
case Path::Iterator::cubicTo:
|
|
path.cubicTo (p->pos[0].toXY (relativeTo, layout),
|
|
p->pos[1].toXY (relativeTo, layout),
|
|
p->pos[2].toXY (relativeTo, layout));
|
|
break;
|
|
|
|
case Path::Iterator::closePath:
|
|
path.closeSubPath();
|
|
break;
|
|
|
|
default:
|
|
jassertfalse; break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
//==============================================================================
|
|
class ChangeWindingAction : public PaintElementUndoableAction <PaintElementPath>
|
|
{
|
|
public:
|
|
ChangeWindingAction (PaintElementPath* const path, const bool newValue_)
|
|
: PaintElementUndoableAction <PaintElementPath> (path),
|
|
newValue (newValue_),
|
|
oldValue (path->isNonZeroWinding())
|
|
{
|
|
}
|
|
|
|
bool perform() override
|
|
{
|
|
showCorrectTab();
|
|
getElement()->setNonZeroWinding (newValue, false);
|
|
return true;
|
|
}
|
|
|
|
bool undo() override
|
|
{
|
|
showCorrectTab();
|
|
getElement()->setNonZeroWinding (oldValue, false);
|
|
return true;
|
|
}
|
|
|
|
private:
|
|
bool newValue, oldValue;
|
|
};
|
|
|
|
void PaintElementPath::setNonZeroWinding (const bool nonZero, const bool undoable)
|
|
{
|
|
if (nonZero != nonZeroWinding)
|
|
{
|
|
if (undoable)
|
|
{
|
|
perform (new ChangeWindingAction (this, nonZero), "Change path winding rule");
|
|
}
|
|
else
|
|
{
|
|
nonZeroWinding = nonZero;
|
|
changed();
|
|
}
|
|
}
|
|
}
|
|
|
|
bool PaintElementPath::isSubpathClosed (int index) const
|
|
{
|
|
for (int i = index + 1; i < points.size(); ++i)
|
|
{
|
|
if (points.getUnchecked (i)->type == Path::Iterator::closePath)
|
|
return true;
|
|
|
|
if (points.getUnchecked (i)->type == Path::Iterator::startNewSubPath)
|
|
break;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
//==============================================================================
|
|
void PaintElementPath::setSubpathClosed (int index, const bool closed, const bool undoable)
|
|
{
|
|
if (closed != isSubpathClosed (index))
|
|
{
|
|
for (int i = index + 1; i < points.size(); ++i)
|
|
{
|
|
PathPoint* p = points.getUnchecked (i);
|
|
|
|
if (p->type == Path::Iterator::closePath)
|
|
{
|
|
jassert (! closed);
|
|
|
|
deletePoint (i, undoable);
|
|
return;
|
|
}
|
|
|
|
if (p->type == Path::Iterator::startNewSubPath)
|
|
{
|
|
jassert (closed);
|
|
|
|
PathPoint* pp = addPoint (i - 1, undoable);
|
|
|
|
PathPoint p2 (*pp);
|
|
p2.type = Path::Iterator::closePath;
|
|
perform (new ChangePointAction (pp, p2), "Close subpath");
|
|
return;
|
|
}
|
|
}
|
|
|
|
jassert (closed);
|
|
|
|
PathPoint* p = addPoint (points.size() - 1, undoable);
|
|
PathPoint p2 (*p);
|
|
p2.type = Path::Iterator::closePath;
|
|
perform (new ChangePointAction (p, p2), "Close subpath");
|
|
}
|
|
}
|
|
|
|
//==============================================================================
|
|
class AddPointAction : public PaintElementUndoableAction <PaintElementPath>
|
|
{
|
|
public:
|
|
AddPointAction (PaintElementPath* path, int pointIndexToAddItAfter_)
|
|
: PaintElementUndoableAction <PaintElementPath> (path),
|
|
indexAdded (-1),
|
|
pointIndexToAddItAfter (pointIndexToAddItAfter_)
|
|
{
|
|
}
|
|
|
|
bool perform() override
|
|
{
|
|
showCorrectTab();
|
|
|
|
if (auto* const path = getElement())
|
|
{
|
|
if (auto* const p = path->addPoint (pointIndexToAddItAfter, false))
|
|
{
|
|
indexAdded = path->indexOfPoint (p);
|
|
jassert (indexAdded >= 0);
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool undo() override
|
|
{
|
|
showCorrectTab();
|
|
|
|
PaintElementPath* const path = getElement();
|
|
jassert (path != nullptr);
|
|
|
|
path->deletePoint (indexAdded, false);
|
|
return true;
|
|
}
|
|
|
|
int indexAdded;
|
|
|
|
private:
|
|
int pointIndexToAddItAfter;
|
|
};
|
|
|
|
PathPoint* PaintElementPath::addPoint (int pointIndexToAddItAfter, const bool undoable)
|
|
{
|
|
if (undoable)
|
|
{
|
|
AddPointAction* action = new AddPointAction (this, pointIndexToAddItAfter);
|
|
perform (action, "Add path point");
|
|
return points [action->indexAdded];
|
|
}
|
|
|
|
double x1 = 20.0, y1 = 20.0, x2, y2;
|
|
|
|
ComponentLayout* layout = getDocument()->getComponentLayout();
|
|
const Rectangle<int> area (((PaintRoutineEditor*) getParentComponent())->getComponentArea());
|
|
|
|
if (points [pointIndexToAddItAfter] != nullptr)
|
|
points [pointIndexToAddItAfter]->pos [points [pointIndexToAddItAfter]->getNumPoints() - 1].getXY (x1, y1, area, layout);
|
|
else if (points[0] != nullptr)
|
|
points[0]->pos[0].getXY (x1, y1, area, layout);
|
|
|
|
x2 = x1 + 50.0;
|
|
y2 = y1 + 50.0;
|
|
|
|
if (points [pointIndexToAddItAfter + 1] != nullptr)
|
|
{
|
|
if (points [pointIndexToAddItAfter + 1]->type == Path::Iterator::closePath
|
|
|| points [pointIndexToAddItAfter + 1]->type == Path::Iterator::startNewSubPath)
|
|
{
|
|
int i = pointIndexToAddItAfter;
|
|
while (i > 0)
|
|
if (points [--i]->type == Path::Iterator::startNewSubPath)
|
|
break;
|
|
|
|
if (i != pointIndexToAddItAfter)
|
|
points [i]->pos[0].getXY (x2, y2, area, layout);
|
|
}
|
|
else
|
|
{
|
|
points [pointIndexToAddItAfter + 1]->pos[0].getXY (x2, y2, area, layout);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
int i = pointIndexToAddItAfter + 1;
|
|
while (i > 0)
|
|
if (points [--i]->type == Path::Iterator::startNewSubPath)
|
|
break;
|
|
|
|
points[i]->pos[0].getXY (x2, y2, area, layout);
|
|
}
|
|
|
|
PathPoint* const p = new PathPoint (this);
|
|
|
|
p->type = Path::Iterator::lineTo;
|
|
p->pos[0].rect.setX ((x1 + x2) * 0.5f);
|
|
p->pos[0].rect.setY ((y1 + y2) * 0.5f);
|
|
|
|
points.insert (pointIndexToAddItAfter + 1, p);
|
|
|
|
pointListChanged();
|
|
return p;
|
|
}
|
|
|
|
//==============================================================================
|
|
class DeletePointAction : public PaintElementUndoableAction <PaintElementPath>
|
|
{
|
|
public:
|
|
DeletePointAction (PaintElementPath* const path, const int indexToRemove_)
|
|
: PaintElementUndoableAction <PaintElementPath> (path),
|
|
indexToRemove (indexToRemove_),
|
|
oldValue (*path->getPoint (indexToRemove))
|
|
{
|
|
}
|
|
|
|
bool perform() override
|
|
{
|
|
showCorrectTab();
|
|
|
|
PaintElementPath* const path = getElement();
|
|
jassert (path != nullptr);
|
|
|
|
path->deletePoint (indexToRemove, false);
|
|
return path != nullptr;
|
|
}
|
|
|
|
bool undo() override
|
|
{
|
|
showCorrectTab();
|
|
|
|
PaintElementPath* const path = getElement();
|
|
jassert (path != nullptr);
|
|
|
|
PathPoint* p = path->addPoint (indexToRemove - 1, false);
|
|
*p = oldValue;
|
|
|
|
return path != nullptr;
|
|
}
|
|
|
|
int indexToRemove;
|
|
|
|
private:
|
|
PathPoint oldValue;
|
|
};
|
|
|
|
void PaintElementPath::deletePoint (int pointIndex, const bool undoable)
|
|
{
|
|
if (undoable)
|
|
{
|
|
perform (new DeletePointAction (this, pointIndex), "Delete path point");
|
|
}
|
|
else
|
|
{
|
|
PathPoint* const p = points [pointIndex];
|
|
|
|
if (p != nullptr && pointIndex > 0)
|
|
{
|
|
owner->getSelectedPoints().deselect (p);
|
|
owner->getSelectedPoints().changed (true);
|
|
|
|
points.remove (pointIndex);
|
|
pointListChanged();
|
|
}
|
|
}
|
|
}
|
|
|
|
//==============================================================================
|
|
bool PaintElementPath::getPoint (int index, int pointNumber, double& x, double& y, const Rectangle<int>& parentArea) const
|
|
{
|
|
const PathPoint* const p = points [index];
|
|
|
|
if (p == nullptr)
|
|
{
|
|
x = y = 0;
|
|
return false;
|
|
}
|
|
|
|
if (pointNumber >= PathPoint::maxRects)
|
|
{
|
|
jassertfalse;
|
|
x = y = 0;
|
|
return false;
|
|
}
|
|
|
|
jassert (pointNumber < 3 || p->type == Path::Iterator::cubicTo);
|
|
jassert (pointNumber < 2 || p->type == Path::Iterator::cubicTo || p->type == Path::Iterator::quadraticTo);
|
|
|
|
p->pos [pointNumber].getXY (x, y, parentArea, getDocument()->getComponentLayout());
|
|
return true;
|
|
}
|
|
|
|
int PaintElementPath::findSegmentAtXY (int x, int y) const
|
|
{
|
|
double x1, y1, x2, y2, x3, y3, lastX = 0.0, lastY = 0.0, subPathStartX = 0.0, subPathStartY = 0.0;
|
|
|
|
ComponentLayout* const layout = getDocument()->getComponentLayout();
|
|
const Rectangle<int> area (((PaintRoutineEditor*) getParentComponent())->getComponentArea());
|
|
|
|
int subpathStartIndex = 0;
|
|
|
|
float thickness = 10.0f;
|
|
if (isStrokePresent)
|
|
thickness = jmax (thickness, strokeType.stroke.getStrokeThickness());
|
|
|
|
for (int i = 0; i < points.size(); ++i)
|
|
{
|
|
Path segmentPath;
|
|
PathPoint* const p = points.getUnchecked (i);
|
|
|
|
switch (p->type)
|
|
{
|
|
case Path::Iterator::startNewSubPath:
|
|
p->pos[0].getXY (lastX, lastY, area, layout);
|
|
subPathStartX = lastX;
|
|
subPathStartY = lastY;
|
|
subpathStartIndex = i;
|
|
break;
|
|
|
|
case Path::Iterator::lineTo:
|
|
p->pos[0].getXY (x1, y1, area, layout);
|
|
|
|
segmentPath.addLineSegment (Line<float> ((float) lastX, (float) lastY, (float) x1, (float) y1), thickness);
|
|
if (segmentPath.contains ((float) x, (float) y))
|
|
return i;
|
|
|
|
lastX = x1;
|
|
lastY = y1;
|
|
break;
|
|
|
|
case Path::Iterator::quadraticTo:
|
|
p->pos[0].getXY (x1, y1, area, layout);
|
|
p->pos[1].getXY (x2, y2, area, layout);
|
|
|
|
segmentPath.startNewSubPath ((float) lastX, (float) lastY);
|
|
segmentPath.quadraticTo ((float) x1, (float) y1, (float) x2, (float) y2);
|
|
PathStrokeType (thickness).createStrokedPath (segmentPath, segmentPath);
|
|
|
|
if (segmentPath.contains ((float) x, (float) y))
|
|
return i;
|
|
|
|
lastX = x2;
|
|
lastY = y2;
|
|
break;
|
|
|
|
case Path::Iterator::cubicTo:
|
|
p->pos[0].getXY (x1, y1, area, layout);
|
|
p->pos[1].getXY (x2, y2, area, layout);
|
|
p->pos[2].getXY (x3, y3, area, layout);
|
|
|
|
segmentPath.startNewSubPath ((float) lastX, (float) lastY);
|
|
segmentPath.cubicTo ((float) x1, (float) y1, (float) x2, (float) y2, (float) x3, (float) y3);
|
|
PathStrokeType (thickness).createStrokedPath (segmentPath, segmentPath);
|
|
|
|
if (segmentPath.contains ((float) x, (float) y))
|
|
return i;
|
|
|
|
lastX = x3;
|
|
lastY = y3;
|
|
break;
|
|
|
|
case Path::Iterator::closePath:
|
|
segmentPath.addLineSegment (Line<float> ((float) lastX, (float) lastY, (float) subPathStartX, (float) subPathStartY), thickness);
|
|
if (segmentPath.contains ((float) x, (float) y))
|
|
return subpathStartIndex;
|
|
|
|
lastX = subPathStartX;
|
|
lastY = subPathStartY;
|
|
break;
|
|
|
|
default:
|
|
jassertfalse; break;
|
|
}
|
|
}
|
|
|
|
return -1;
|
|
}
|
|
|
|
//==============================================================================
|
|
void PaintElementPath::movePoint (int index, int pointNumber,
|
|
double newX, double newY,
|
|
const Rectangle<int>& parentArea,
|
|
const bool undoable)
|
|
{
|
|
if (PathPoint* const p = points [index])
|
|
{
|
|
PathPoint newPoint (*p);
|
|
jassert (pointNumber < 3 || p->type == Path::Iterator::cubicTo);
|
|
jassert (pointNumber < 2 || p->type == Path::Iterator::cubicTo || p->type == Path::Iterator::quadraticTo);
|
|
|
|
if (pointNumber >= PathPoint::maxRects)
|
|
{
|
|
jassertfalse;
|
|
return;
|
|
}
|
|
|
|
RelativePositionedRectangle& pr = newPoint.pos [pointNumber];
|
|
|
|
double x, y, w, h;
|
|
pr.getRectangleDouble (x, y, w, h, parentArea, getDocument()->getComponentLayout());
|
|
pr.updateFrom (newX, newY, w, h, parentArea, getDocument()->getComponentLayout());
|
|
|
|
if (undoable)
|
|
{
|
|
perform (new ChangePointAction (p, index, newPoint), "Move path point");
|
|
}
|
|
else
|
|
{
|
|
*p = newPoint;
|
|
changed();
|
|
}
|
|
}
|
|
}
|
|
|
|
RelativePositionedRectangle PaintElementPath::getPoint (int index, int pointNumber) const
|
|
{
|
|
if (pointNumber >= PathPoint::maxRects)
|
|
{
|
|
jassertfalse;
|
|
return RelativePositionedRectangle();
|
|
}
|
|
|
|
if (PathPoint* const p = points [index])
|
|
{
|
|
jassert (pointNumber < 3 || p->type == Path::Iterator::cubicTo);
|
|
jassert (pointNumber < 2 || p->type == Path::Iterator::cubicTo || p->type == Path::Iterator::quadraticTo);
|
|
|
|
return p->pos [pointNumber];
|
|
}
|
|
|
|
jassertfalse;
|
|
return RelativePositionedRectangle();
|
|
}
|
|
|
|
void PaintElementPath::setPoint (int index, int pointNumber, const RelativePositionedRectangle& newPos, const bool undoable)
|
|
{
|
|
if (pointNumber >= PathPoint::maxRects)
|
|
{
|
|
jassertfalse;
|
|
return;
|
|
}
|
|
|
|
if (PathPoint* const p = points [index])
|
|
{
|
|
PathPoint newPoint (*p);
|
|
|
|
jassert (pointNumber < 3 || p->type == Path::Iterator::cubicTo);
|
|
jassert (pointNumber < 2 || p->type == Path::Iterator::cubicTo || p->type == Path::Iterator::quadraticTo);
|
|
|
|
if (newPoint.pos [pointNumber] != newPos)
|
|
{
|
|
newPoint.pos [pointNumber] = newPos;
|
|
|
|
if (undoable)
|
|
{
|
|
perform (new ChangePointAction (p, index, newPoint), "Change path point position");
|
|
}
|
|
else
|
|
{
|
|
*p = newPoint;
|
|
changed();
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
jassertfalse;
|
|
}
|
|
}
|
|
|
|
//==============================================================================
|
|
class PathPointTypeProperty : public ChoicePropertyComponent,
|
|
private ChangeListener
|
|
{
|
|
public:
|
|
PathPointTypeProperty (PaintElementPath* const owner_,
|
|
const int index_)
|
|
: ChoicePropertyComponent ("point type"),
|
|
owner (owner_),
|
|
index (index_)
|
|
{
|
|
choices.add ("Start of sub-path");
|
|
choices.add ("Line");
|
|
choices.add ("Quadratic");
|
|
choices.add ("Cubic");
|
|
|
|
owner->getDocument()->addChangeListener (this);
|
|
}
|
|
|
|
~PathPointTypeProperty() override
|
|
{
|
|
owner->getDocument()->removeChangeListener (this);
|
|
}
|
|
|
|
void setIndex (int newIndex) override
|
|
{
|
|
Path::Iterator::PathElementType type = Path::Iterator::startNewSubPath;
|
|
|
|
switch (newIndex)
|
|
{
|
|
case 0: type = Path::Iterator::startNewSubPath; break;
|
|
case 1: type = Path::Iterator::lineTo; break;
|
|
case 2: type = Path::Iterator::quadraticTo; break;
|
|
case 3: type = Path::Iterator::cubicTo; break;
|
|
default: jassertfalse; break;
|
|
}
|
|
|
|
const Rectangle<int> area (((PaintRoutineEditor*) owner->getParentComponent())->getComponentArea());
|
|
owner->getPoint (index)->changePointType (type, area, true);
|
|
}
|
|
|
|
int getIndex() const override
|
|
{
|
|
if (const auto* const p = owner->getPoint (index))
|
|
{
|
|
switch (p->type)
|
|
{
|
|
case Path::Iterator::startNewSubPath: return 0;
|
|
case Path::Iterator::lineTo: return 1;
|
|
case Path::Iterator::quadraticTo: return 2;
|
|
case Path::Iterator::cubicTo: return 3;
|
|
case Path::Iterator::closePath: break;
|
|
default: jassertfalse; break;
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
private:
|
|
void changeListenerCallback (ChangeBroadcaster*) override
|
|
{
|
|
refresh();
|
|
}
|
|
|
|
PaintElementPath* const owner;
|
|
const int index;
|
|
};
|
|
|
|
//==============================================================================
|
|
class PathPointPositionProperty : public PositionPropertyBase
|
|
{
|
|
public:
|
|
PathPointPositionProperty (PaintElementPath* const owner_,
|
|
const int index_, const int pointNumber_,
|
|
const String& name,
|
|
ComponentPositionDimension dimension_)
|
|
: PositionPropertyBase (owner_, name, dimension_, false, false,
|
|
owner_->getDocument()->getComponentLayout()),
|
|
owner (owner_),
|
|
index (index_),
|
|
pointNumber (pointNumber_)
|
|
{
|
|
owner->getDocument()->addChangeListener (this);
|
|
}
|
|
|
|
~PathPointPositionProperty() override
|
|
{
|
|
owner->getDocument()->removeChangeListener (this);
|
|
}
|
|
|
|
void setPosition (const RelativePositionedRectangle& newPos) override
|
|
{
|
|
owner->setPoint (index, pointNumber, newPos, true);
|
|
}
|
|
|
|
RelativePositionedRectangle getPosition() const override
|
|
{
|
|
return owner->getPoint (index, pointNumber);
|
|
}
|
|
|
|
private:
|
|
PaintElementPath* const owner;
|
|
const int index, pointNumber;
|
|
};
|
|
|
|
//==============================================================================
|
|
class PathPointClosedProperty : public ChoicePropertyComponent,
|
|
private ChangeListener
|
|
{
|
|
public:
|
|
PathPointClosedProperty (PaintElementPath* const owner_, const int index_)
|
|
: ChoicePropertyComponent ("openness"),
|
|
owner (owner_),
|
|
index (index_)
|
|
{
|
|
owner->getDocument()->addChangeListener (this);
|
|
|
|
choices.add ("Subpath is closed");
|
|
choices.add ("Subpath is open-ended");
|
|
}
|
|
|
|
~PathPointClosedProperty() override
|
|
{
|
|
owner->getDocument()->removeChangeListener (this);
|
|
}
|
|
|
|
void changeListenerCallback (ChangeBroadcaster*) override
|
|
{
|
|
refresh();
|
|
}
|
|
|
|
void setIndex (int newIndex) override
|
|
{
|
|
owner->setSubpathClosed (index, newIndex == 0, true);
|
|
}
|
|
|
|
int getIndex() const override
|
|
{
|
|
return owner->isSubpathClosed (index) ? 0 : 1;
|
|
}
|
|
|
|
private:
|
|
PaintElementPath* const owner;
|
|
const int index;
|
|
};
|
|
|
|
//==============================================================================
|
|
class AddNewPointProperty : public ButtonPropertyComponent
|
|
{
|
|
public:
|
|
AddNewPointProperty (PaintElementPath* const owner_, const int index_)
|
|
: ButtonPropertyComponent ("new point", false),
|
|
owner (owner_),
|
|
index (index_)
|
|
{
|
|
}
|
|
|
|
void buttonClicked() override
|
|
{
|
|
owner->addPoint (index, true);
|
|
}
|
|
|
|
String getButtonText() const override { return "Add new point"; }
|
|
|
|
private:
|
|
PaintElementPath* const owner;
|
|
const int index;
|
|
};
|
|
|
|
|
|
//==============================================================================
|
|
PathPoint::PathPoint (PaintElementPath* const owner_)
|
|
: owner (owner_)
|
|
{
|
|
}
|
|
|
|
PathPoint::PathPoint (const PathPoint& other)
|
|
: owner (other.owner),
|
|
type (other.type)
|
|
{
|
|
pos [0] = other.pos [0];
|
|
pos [1] = other.pos [1];
|
|
pos [2] = other.pos [2];
|
|
}
|
|
|
|
PathPoint& PathPoint::operator= (const PathPoint& other)
|
|
{
|
|
owner = other.owner;
|
|
type = other.type;
|
|
pos [0] = other.pos [0];
|
|
pos [1] = other.pos [1];
|
|
pos [2] = other.pos [2];
|
|
return *this;
|
|
}
|
|
|
|
PathPoint::~PathPoint()
|
|
{
|
|
}
|
|
|
|
int PathPoint::getNumPoints() const
|
|
{
|
|
if (type == Path::Iterator::cubicTo) return 3;
|
|
if (type == Path::Iterator::quadraticTo) return 2;
|
|
if (type == Path::Iterator::closePath) return 0;
|
|
|
|
return 1;
|
|
}
|
|
|
|
PathPoint PathPoint::withChangedPointType (const Path::Iterator::PathElementType newType,
|
|
const Rectangle<int>& parentArea) const
|
|
{
|
|
PathPoint p (*this);
|
|
|
|
if (newType != p.type)
|
|
{
|
|
int oldNumPoints = getNumPoints();
|
|
p.type = newType;
|
|
int numPoints = p.getNumPoints();
|
|
|
|
if (numPoints != oldNumPoints)
|
|
{
|
|
double lastX, lastY;
|
|
double x, y, w, h;
|
|
|
|
p.pos [numPoints - 1] = p.pos [oldNumPoints - 1];
|
|
p.pos [numPoints - 1].getRectangleDouble (x, y, w, h, parentArea, owner->getDocument()->getComponentLayout());
|
|
|
|
const int index = owner->indexOfPoint (this);
|
|
|
|
if (PathPoint* lastPoint = owner->getPoint (index - 1))
|
|
{
|
|
lastPoint->pos [lastPoint->getNumPoints() - 1]
|
|
.getRectangleDouble (lastX, lastY, w, h, parentArea, owner->getDocument()->getComponentLayout());
|
|
}
|
|
else
|
|
{
|
|
jassertfalse;
|
|
lastX = x;
|
|
lastY = y;
|
|
}
|
|
|
|
for (int i = 0; i < numPoints - 1; ++i)
|
|
{
|
|
p.pos[i] = p.pos [numPoints - 1];
|
|
|
|
p.pos[i].updateFrom (lastX + (x - lastX) * (i + 1) / numPoints,
|
|
lastY + (y - lastY) * (i + 1) / numPoints,
|
|
w, h,
|
|
parentArea,
|
|
owner->getDocument()->getComponentLayout());
|
|
}
|
|
}
|
|
}
|
|
|
|
return p;
|
|
}
|
|
|
|
void PathPoint::changePointType (const Path::Iterator::PathElementType newType,
|
|
const Rectangle<int>& parentArea, const bool undoable)
|
|
{
|
|
if (newType != type)
|
|
{
|
|
if (undoable)
|
|
{
|
|
owner->perform (new ChangePointAction (this, withChangedPointType (newType, parentArea)),
|
|
"Change path point type");
|
|
}
|
|
else
|
|
{
|
|
*this = withChangedPointType (newType, parentArea);
|
|
owner->pointListChanged();
|
|
}
|
|
}
|
|
}
|
|
|
|
void PathPoint::getEditableProperties (Array<PropertyComponent*>& props, bool multipleSelected)
|
|
{
|
|
if (multipleSelected)
|
|
return;
|
|
|
|
auto index = owner->indexOfPoint (this);
|
|
jassert (index >= 0);
|
|
|
|
switch (type)
|
|
{
|
|
case Path::Iterator::startNewSubPath:
|
|
props.add (new PathPointPositionProperty (owner, index, 0, "x", PositionPropertyBase::componentX));
|
|
props.add (new PathPointPositionProperty (owner, index, 0, "y", PositionPropertyBase::componentY));
|
|
|
|
props.add (new PathPointClosedProperty (owner, index));
|
|
props.add (new AddNewPointProperty (owner, index));
|
|
break;
|
|
|
|
case Path::Iterator::lineTo:
|
|
props.add (new PathPointTypeProperty (owner, index));
|
|
props.add (new PathPointPositionProperty (owner, index, 0, "x", PositionPropertyBase::componentX));
|
|
props.add (new PathPointPositionProperty (owner, index, 0, "y", PositionPropertyBase::componentY));
|
|
props.add (new AddNewPointProperty (owner, index));
|
|
break;
|
|
|
|
case Path::Iterator::quadraticTo:
|
|
props.add (new PathPointTypeProperty (owner, index));
|
|
props.add (new PathPointPositionProperty (owner, index, 0, "control pt x", PositionPropertyBase::componentX));
|
|
props.add (new PathPointPositionProperty (owner, index, 0, "control pt y", PositionPropertyBase::componentY));
|
|
props.add (new PathPointPositionProperty (owner, index, 1, "x", PositionPropertyBase::componentX));
|
|
props.add (new PathPointPositionProperty (owner, index, 1, "y", PositionPropertyBase::componentY));
|
|
props.add (new AddNewPointProperty (owner, index));
|
|
break;
|
|
|
|
case Path::Iterator::cubicTo:
|
|
props.add (new PathPointTypeProperty (owner, index));
|
|
props.add (new PathPointPositionProperty (owner, index, 0, "control pt1 x", PositionPropertyBase::componentX));
|
|
props.add (new PathPointPositionProperty (owner, index, 0, "control pt1 y", PositionPropertyBase::componentY));
|
|
props.add (new PathPointPositionProperty (owner, index, 1, "control pt2 x", PositionPropertyBase::componentX));
|
|
props.add (new PathPointPositionProperty (owner, index, 1, "control pt2 y", PositionPropertyBase::componentY));
|
|
props.add (new PathPointPositionProperty (owner, index, 2, "x", PositionPropertyBase::componentX));
|
|
props.add (new PathPointPositionProperty (owner, index, 2, "y", PositionPropertyBase::componentY));
|
|
props.add (new AddNewPointProperty (owner, index));
|
|
break;
|
|
|
|
case Path::Iterator::closePath:
|
|
break;
|
|
|
|
default:
|
|
jassertfalse;
|
|
break;
|
|
}
|
|
}
|
|
|
|
void PathPoint::deleteFromPath()
|
|
{
|
|
owner->deletePoint (owner->indexOfPoint (this), true);
|
|
}
|
|
|
|
//==============================================================================
|
|
PathPointComponent::PathPointComponent (PaintElementPath* const path_,
|
|
const int index_,
|
|
const int pointNumber_)
|
|
: ElementSiblingComponent (path_),
|
|
path (path_),
|
|
routine (path_->getOwner()),
|
|
index (index_),
|
|
pointNumber (pointNumber_),
|
|
selected (false)
|
|
{
|
|
setSize (11, 11);
|
|
setRepaintsOnMouseActivity (true);
|
|
|
|
selected = routine->getSelectedPoints().isSelected (path_->getPoint (index));
|
|
routine->getSelectedPoints().addChangeListener (this);
|
|
}
|
|
|
|
PathPointComponent::~PathPointComponent()
|
|
{
|
|
routine->getSelectedPoints().removeChangeListener (this);
|
|
}
|
|
|
|
void PathPointComponent::updatePosition()
|
|
{
|
|
const Rectangle<int> area (((PaintRoutineEditor*) getParentComponent())->getComponentArea());
|
|
jassert (getParentComponent() != nullptr);
|
|
|
|
double x, y;
|
|
path->getPoint (index, pointNumber, x, y, area);
|
|
|
|
setCentrePosition (roundToInt (x),
|
|
roundToInt (y));
|
|
}
|
|
|
|
void PathPointComponent::showPopupMenu()
|
|
{
|
|
}
|
|
|
|
void PathPointComponent::paint (Graphics& g)
|
|
{
|
|
if (isMouseOverOrDragging())
|
|
g.fillAll (Colours::red);
|
|
|
|
if (selected)
|
|
{
|
|
g.setColour (Colours::red);
|
|
g.drawRect (getLocalBounds());
|
|
}
|
|
|
|
g.setColour (Colours::white);
|
|
g.fillRect (getWidth() / 2 - 3, getHeight() / 2 - 3, 7, 7);
|
|
|
|
g.setColour (Colours::black);
|
|
|
|
if (pointNumber < path->getPoint (index)->getNumPoints() - 1)
|
|
g.drawRect (getWidth() / 2 - 2, getHeight() / 2 - 2, 5, 5);
|
|
else
|
|
g.fillRect (getWidth() / 2 - 2, getHeight() / 2 - 2, 5, 5);
|
|
}
|
|
|
|
void PathPointComponent::mouseDown (const MouseEvent& e)
|
|
{
|
|
dragging = false;
|
|
|
|
if (e.mods.isPopupMenu())
|
|
{
|
|
showPopupMenu();
|
|
return; // this may be deleted now..
|
|
}
|
|
|
|
dragX = getX() + getWidth() / 2;
|
|
dragY = getY() + getHeight() / 2;
|
|
|
|
mouseDownSelectStatus = routine->getSelectedPoints().addToSelectionOnMouseDown (path->getPoint (index), e.mods);
|
|
|
|
owner->getDocument()->beginTransaction();
|
|
}
|
|
|
|
void PathPointComponent::mouseDrag (const MouseEvent& e)
|
|
{
|
|
if (! e.mods.isPopupMenu())
|
|
{
|
|
if (selected && ! dragging)
|
|
dragging = e.mouseWasDraggedSinceMouseDown();
|
|
|
|
if (dragging)
|
|
{
|
|
const Rectangle<int> area (((PaintRoutineEditor*) getParentComponent())->getComponentArea());
|
|
int x = dragX + e.getDistanceFromDragStartX() - area.getX();
|
|
int y = dragY + e.getDistanceFromDragStartY() - area.getY();
|
|
|
|
if (JucerDocument* const document = owner->getDocument())
|
|
{
|
|
x = document->snapPosition (x);
|
|
y = document->snapPosition (y);
|
|
}
|
|
|
|
owner->getDocument()->getUndoManager().undoCurrentTransactionOnly();
|
|
path->movePoint (index, pointNumber, x + area.getX(), y + area.getY(), area, true);
|
|
}
|
|
}
|
|
}
|
|
|
|
void PathPointComponent::mouseUp (const MouseEvent& e)
|
|
{
|
|
routine->getSelectedPoints().addToSelectionOnMouseUp (path->getPoint (index),
|
|
e.mods, dragging,
|
|
mouseDownSelectStatus);
|
|
}
|
|
|
|
void PathPointComponent::changeListenerCallback (ChangeBroadcaster* source)
|
|
{
|
|
ElementSiblingComponent::changeListenerCallback (source);
|
|
|
|
const bool nowSelected = routine->getSelectedPoints().isSelected (path->getPoint (index));
|
|
|
|
if (nowSelected != selected)
|
|
{
|
|
selected = nowSelected;
|
|
repaint();
|
|
|
|
if (Component* parent = getParentComponent())
|
|
parent->repaint();
|
|
}
|
|
}
|