/* ============================================================================== This file is part of the JUCE 6 technical preview. Copyright (c) 2020 - Raw Material Software Limited You may use this code under the terms of the GPL v3 (see www.gnu.org/licenses). For this technical preview, this file is not subject to commercial licensing. JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR 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 { public: ChangePointAction (PathPoint* const point, const int pointIndex, const PathPoint& newValue_) : PaintElementUndoableAction (point->owner), index (pointIndex), newValue (newValue_), oldValue (*point) { } ChangePointAction (PathPoint* const point, const PathPoint& newValue_) : PaintElementUndoableAction (point->owner), index (point->owner->indexOfPoint (point)), newValue (newValue_), oldValue (*point) { } bool perform() { return changeTo (newValue); } bool undo() { 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(); PaintElementPath* const path = getElement(); jassert (path != nullptr); PathPoint* const p = path->getPoint (index); jassert (p != nullptr); const bool typeChanged = (p->type != value.type); *p = value; p->owner = path; if (typeChanged) path->pointListChanged(); path->changed(); return true; } }; //============================================================================== class PathWindingModeProperty : public ChoicePropertyComponent, public 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() { owner->getDocument()->removeChangeListener (this); } void setIndex (int newIndex) { owner->setNonZeroWinding (newIndex == 0, true); } int getIndex() const { return owner->isNonZeroWinding() ? 0 : 1; } void changeListenerCallback (ChangeBroadcaster*) { refresh(); } private: 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 PaintElementPath::getCurrentBounds (const Rectangle& parentArea) const { updateStoredPath (getDocument()->getComponentLayout(), parentArea); Rectangle r (path.getBounds()); const int borderSize = getBorderSize(); return Rectangle ((int) r.getX() - borderSize, (int) r.getY() - borderSize, (int) r.getWidth() + borderSize * 2, (int) r.getHeight() + borderSize * 2); } void PaintElementPath::setCurrentBounds (const Rectangle& b, const Rectangle& parentArea, const bool /*undoable*/) { Rectangle newBounds (b); newBounds.setSize (jmax (1, newBounds.getWidth()), jmax (1, newBounds.getHeight())); const Rectangle 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& 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 p1, const Point p2) { g.drawArrow (Line (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& 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& 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 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(); } void PaintElementPath::pointListChanged() { changed(); siblingComponentsChanged(); } //============================================================================== void PaintElementPath::getEditableProperties (Array & 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 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 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& 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 { public: ChangeWindingAction (PaintElementPath* const path, const bool newValue_) : PaintElementUndoableAction (path), newValue (newValue_), oldValue (path->isNonZeroWinding()) { } bool perform() { showCorrectTab(); getElement()->setNonZeroWinding (newValue, false); return true; } bool undo() { 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 { public: AddPointAction (PaintElementPath* path, int pointIndexToAddItAfter_) : PaintElementUndoableAction (path), indexAdded (-1), pointIndexToAddItAfter (pointIndexToAddItAfter_) { } bool perform() { showCorrectTab(); PaintElementPath* const path = getElement(); jassert (path != nullptr); PathPoint* const p = path->addPoint (pointIndexToAddItAfter, false); jassert (p != nullptr); indexAdded = path->indexOfPoint (p); jassert (indexAdded >= 0); return true; } bool undo() { 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 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 { public: DeletePointAction (PaintElementPath* const path, const int indexToRemove_) : PaintElementUndoableAction (path), indexToRemove (indexToRemove_), oldValue (*path->getPoint (indexToRemove)) { } bool perform() { showCorrectTab(); PaintElementPath* const path = getElement(); jassert (path != nullptr); path->deletePoint (indexToRemove, false); return path != nullptr; } bool undo() { 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& parentArea) const { const PathPoint* const p = points [index]; if (p == nullptr) { 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 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) 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) 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& 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); 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 (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 (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, public 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() { owner->getDocument()->removeChangeListener (this); } void setIndex (int newIndex) { 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 area (((PaintRoutineEditor*) owner->getParentComponent())->getComponentArea()); owner->getPoint (index)->changePointType (type, area, true); } int getIndex() const { const PathPoint* const p = owner->getPoint (index); jassert (p != nullptr); 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; } void changeListenerCallback (ChangeBroadcaster*) { refresh(); } private: 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() { owner->getDocument()->removeChangeListener (this); } void setPosition (const RelativePositionedRectangle& newPos) { owner->setPoint (index, pointNumber, newPos, true); } RelativePositionedRectangle getPosition() const { 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() { owner->getDocument()->removeChangeListener (this); } void changeListenerCallback (ChangeBroadcaster*) { refresh(); } void setIndex (int newIndex) { owner->setSubpathClosed (index, newIndex == 0, true); } int getIndex() const { 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() { owner->addPoint (index, true); } String getButtonText() const { 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& 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->points.indexOf (this); if (PathPoint* lastPoint = owner->points [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& 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& props, bool multipleSelected) { if (multipleSelected) return; auto index = owner->points.indexOf (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->points.indexOf (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_->points [index]); routine->getSelectedPoints().addChangeListener (this); } PathPointComponent::~PathPointComponent() { routine->getSelectedPoints().removeChangeListener (this); } void PathPointComponent::updatePosition() { const Rectangle 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->points [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 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->points [index], e.mods, dragging, mouseDownSelectStatus); } void PathPointComponent::changeListenerCallback (ChangeBroadcaster* source) { ElementSiblingComponent::changeListenerCallback (source); const bool nowSelected = routine->getSelectedPoints().isSelected (path->points [index]); if (nowSelected != selected) { selected = nowSelected; repaint(); if (Component* parent = getParentComponent()) parent->repaint(); } }