/* // Copyright (c) 2021-2025 Timothy Schoen // For information on usage and redistribution, and for a DISCLAIMER OF ALL // WARRANTIES, see the file, "LICENSE.txt," in this distribution. */ #include #include #include "Utility/Config.h" #include "Utility/Fonts.h" #include "ObjectGrid.h" #include "Object.h" #include "Canvas.h" #include "PluginEditor.h" #include "Connection.h" #include "CanvasViewport.h" ObjectGrid::ObjectGrid(Canvas* cnv) : cnv(cnv) , updater(cnv) { gridEnabled = SettingsFile::getInstance()->getProperty("grid_enabled"); gridType = SettingsFile::getInstance()->getProperty("grid_type"); gridSize = SettingsFile::getInstance()->getProperty("grid_size"); } void ObjectGrid::positionNewObject(Object* newObject, Point mousePosition) { if (ModifierKeys::getCurrentModifiers().isShiftDown() || gridType == 0 || !gridEnabled) { return; } newObject->originalBounds = newObject->getBounds(); ScopedValueSetter toleranceSetter(objectTolerance, 15); auto offset = performMove(newObject, {0, 0}); auto nb = newObject->getObjectBounds() + offset; if (newObject->gui) newObject->gui->setPdBounds(nb); else newObject->setObjectBounds(nb); clearIndicators(false); } SmallArray ObjectGrid::getSnappableObjects(Object const* draggedObject) { auto const& cnv = draggedObject->cnv; if (!cnv->viewport) return { }; SmallArray snappable; auto const viewBounds = cnv->viewport->getViewArea(); for (auto* object : cnv->objects) { if (draggedObject == object || object->isSelected() || !viewBounds.intersects(object->getBounds().toFloat())) continue; // don't look at dragged object, selected objects, or objects that are outside of view bounds snappable.add(object); } auto centre = draggedObject->getBounds().getCentre(); snappable.sort([centre](Object const* a, Object const* b) { auto const distA = a->getBounds().getCentre().getDistanceFrom(centre); auto const distB = b->getBounds().getCentre().getDistanceFrom(centre); return distA > distB; }); return snappable; } void ObjectGrid::startLineFadeAnimation(int idx, float ms, float targetAlpha) { lineAnimators[idx].complete(); lineTargetAlpha[idx] = targetAlpha; lineAnimators[idx] = ValueAnimatorBuilder { } .withDurationMs(ms) .withEasing(Easings::createEaseOut()) .withValueChangedCallback([this, idx](float v) { lineAlpha[idx] = makeAnimationLimits(lineAlpha[idx], lineTargetAlpha[idx]).lerp(v); auto const lineArea = cnv->editor->nvgSurface.getLocalArea(cnv, Rectangle(lines[idx].getStart(), lines[idx].getEnd()).expanded(2)); cnv->editor->nvgSurface.invalidateArea(lineArea); }) .build(); updater.addAnimator(lineAnimators[idx], [this, idx]() { lines[idx] = { }; }); lineAnimators[idx].start(); } void ObjectGrid::settingsChanged(String const& name, var const& value) { if (name == "grid_type") { gridType = static_cast(value); } if (name == "grid_enabled") { gridEnabled = static_cast(value); } if (name == "grid_size") { gridSize = static_cast(value); } } Point ObjectGrid::performMove(Object* toDrag, Point dragOffset) { if (ModifierKeys::getCurrentModifiers().isShiftDown() || gridType == 0 || !gridEnabled) { clearIndicators(true); return dragOffset; } auto [snapGrid, snapEdges, snapCentres] = std::tuple { gridType & 1, gridType & 2, gridType & 4 }; auto snappable = getSnappableObjects(toDrag); Point distance; Line verticalIndicator, horizontalIndicator; bool connectionSnapped = false; // Check for straight connections to snap to if (snapEdges) { for (auto* connection : toDrag->getConnections()) { if (connection->inobj == toDrag) { if (!snappable.contains(connection->outobj)) continue; auto outletBounds = connection->outobj->getBounds() + connection->outlet->getPosition(); auto inletBounds = connection->inobj->originalBounds + dragOffset + connection->inlet->getPosition(); outletBounds = outletBounds.withSize(12, 12); inletBounds = inletBounds.withSize(8, 8); if (outletBounds.getY() > inletBounds.getY()) continue; auto snapDistance = inletBounds.getX() - outletBounds.getX(); if (std::abs(snapDistance) < connectionTolerance) { distance.x = -snapDistance; horizontalIndicator = { outletBounds.getX() - 2, outletBounds.getBottom() + 3, outletBounds.getX() - 2, connection->inobj->getY() - 3 }; connectionSnapped = true; } break; } if (connection->outobj == toDrag) { if (!snappable.contains(connection->inobj)) continue; auto inletBounds = connection->inobj->getBounds() + connection->inlet->getPosition(); auto outletBounds = connection->outobj->originalBounds + dragOffset + connection->outlet->getPosition(); outletBounds = outletBounds.withSize(12, 12); inletBounds = inletBounds.withSize(8, 8); if (outletBounds.getY() > inletBounds.getY()) continue; auto snapDistance = inletBounds.getX() - outletBounds.getX(); if (std::abs(snapDistance) < connectionTolerance) { distance.x = snapDistance; horizontalIndicator = { inletBounds.getX() - 2, connection->outobj->getBottom() + 3, inletBounds.getX() - 2, inletBounds.getY() - 3 }; connectionSnapped = true; } break; } } } auto desiredBounds = toDrag->originalBounds.reduced(Object::margin) + dragOffset; bool objectSnapped = false; // Check for relative object snap for (auto* object : snappable) { auto b1 = object->getBounds().reduced(Object::margin); auto topDiff = b1.getY() - desiredBounds.getY(); auto bottomDiff = b1.getBottom() - desiredBounds.getBottom(); auto leftDiff = b1.getX() - desiredBounds.getX(); auto rightDiff = b1.getRight() - desiredBounds.getRight(); auto vCentreDiff = b1.getCentreY() - desiredBounds.getCentreY(); auto hCentreDiff = b1.getCentreX() - desiredBounds.getCentreX(); if (snapEdges && std::abs(topDiff) < objectTolerance) { verticalIndicator = getObjectIndicatorLine(Top, b1, desiredBounds.withY(b1.getY())); distance.y = topDiff; objectSnapped = true; } else if (snapEdges && std::abs(bottomDiff) < objectTolerance) { verticalIndicator = getObjectIndicatorLine(Bottom, b1, desiredBounds.withBottom(b1.getBottom())); distance.y = bottomDiff; objectSnapped = true; } else if (snapCentres && std::abs(vCentreDiff) < objectTolerance) { verticalIndicator = getObjectIndicatorLine(VerticalCentre, b1, desiredBounds.withCentre({ desiredBounds.getCentreX(), b1.getCentreY() })); distance.y = vCentreDiff; objectSnapped = true; } // Skip horizontal snap if we've already found a connection snap if (!connectionSnapped) { if (snapEdges && std::abs(leftDiff) < objectTolerance) { horizontalIndicator = getObjectIndicatorLine(Left, b1, desiredBounds.withX(b1.getX())); distance.x = leftDiff; objectSnapped = true; } else if (snapEdges && std::abs(rightDiff) < objectTolerance) { horizontalIndicator = getObjectIndicatorLine(Right, b1, desiredBounds.withRight(b1.getRight())); distance.x = rightDiff; objectSnapped = true; } else if (snapCentres && std::abs(hCentreDiff) < objectTolerance) { horizontalIndicator = getObjectIndicatorLine(HorizontalCentre, b1, desiredBounds.withCentre({ b1.getCentreX(), desiredBounds.getCentreY() })); distance.x = hCentreDiff; objectSnapped = true; } } } // Snap to absolute grid if (snapGrid && !objectSnapped && !connectionSnapped) { Point newPos = toDrag->originalBounds.reduced(Object::margin).getPosition() + dragOffset; newPos.setX(floor(newPos.getX() / static_cast(gridSize) + 1) * gridSize); newPos.x += toDrag->cnv->canvasOrigin.x % gridSize - 1; dragOffset.x = newPos.x - toDrag->originalBounds.reduced(Object::margin).getX() - gridSize; newPos.setY(floor(newPos.getY() / static_cast(gridSize) + 1) * gridSize); newPos.y += toDrag->cnv->canvasOrigin.y % gridSize - 1; dragOffset.y = newPos.y - toDrag->originalBounds.reduced(Object::margin).getY() - gridSize; } if (objectSnapped || connectionSnapped) { setIndicator(0, verticalIndicator); setIndicator(1, horizontalIndicator); return dragOffset + distance; } clearIndicators(true); return dragOffset; } Point ObjectGrid::performResize(Object* toDrag, Point dragOffset, Rectangle newResizeBounds) { if (ModifierKeys::getCurrentModifiers().isShiftDown() || gridType == 0 || !gridEnabled) { clearIndicators(true); return dragOffset; } auto [snapGrid, snapEdges, snapCentres] = std::tuple { gridType & 1, gridType & 2, gridType & 4 }; auto limits = [&]() -> Rectangle { return { Canvas::infiniteCanvasSize, Canvas::infiniteCanvasSize }; }(); auto resizeZone = toDrag->resizeZone; auto isDraggingTop = resizeZone.isDraggingTopEdge(); auto isDraggingBottom = resizeZone.isDraggingBottomEdge(); auto isDraggingLeft = resizeZone.isDraggingLeftEdge(); auto isDraggingRight = resizeZone.isDraggingRightEdge(); if (auto* constrainer = toDrag->getConstrainer()) { // Not great that we need to do this, but otherwise we don't really know the object bounds for sure constrainer->checkBounds(newResizeBounds, toDrag->originalBounds, limits, isDraggingTop, isDraggingLeft, isDraggingBottom, isDraggingRight); } // Returns non-zero if the object has a fixed ratio auto ratio = 0.0; if (auto* constrainer = toDrag->getConstrainer()) { ratio = constrainer->getFixedAspectRatio(); } auto desiredBounds = newResizeBounds.reduced(Object::margin); auto actualBounds = toDrag->getBounds().reduced(Object::margin); if (snapEdges) { Line verticalIndicator, horizontalIndicator; Point distance; bool snapped = false; // Check for objects to relative snap to for (auto* object : getSnappableObjects(toDrag)) { auto b1 = object->getBounds().reduced(Object::margin); float topDiff = b1.getY() - desiredBounds.getY(); float bottomDiff = b1.getBottom() - desiredBounds.getBottom(); float leftDiff = b1.getX() - desiredBounds.getX(); float rightDiff = b1.getRight() - desiredBounds.getRight(); if (isDraggingTop && std::abs(topDiff) < objectTolerance) { verticalIndicator = getObjectIndicatorLine(Top, b1, actualBounds.withY(b1.getY())); if (ratio != 0) { if (isDraggingRight) distance.x = round(-topDiff * ratio); if (isDraggingLeft) distance.x = round(topDiff * ratio); } distance.y = topDiff; snapped = true; } else if (isDraggingBottom && std::abs(bottomDiff) < objectTolerance) { verticalIndicator = getObjectIndicatorLine(Bottom, b1, actualBounds.withBottom(b1.getBottom())); if (ratio != 0) { if (isDraggingRight) distance.x = round(bottomDiff * ratio); if (isDraggingLeft) distance.x = round(-bottomDiff * ratio); } distance.y = bottomDiff; snapped = true; } if (approximatelyEqual(ratio, 0.0) || !snapped) { if (isDraggingLeft && std::abs(leftDiff) < objectTolerance) { horizontalIndicator = getObjectIndicatorLine(Left, b1, actualBounds.withX(b1.getX())); if (ratio != 0) { if (isDraggingBottom) distance.y = round(-leftDiff / ratio); if (isDraggingTop) distance.y = round(leftDiff / ratio); } distance.x = leftDiff; snapped = true; } else if (isDraggingRight && std::abs(rightDiff) < objectTolerance) { horizontalIndicator = getObjectIndicatorLine(Right, b1, actualBounds.withRight(b1.getRight())); if (ratio != 0) { if (isDraggingBottom) distance.y = round(rightDiff / ratio); if (isDraggingTop) distance.y = round(-rightDiff / ratio); } distance.x = rightDiff; snapped = true; } } } if (snapped) { setIndicator(0, verticalIndicator); setIndicator(1, horizontalIndicator); return dragOffset + distance; } } if (snapGrid) { Point newPosTopLeft = toDrag->originalBounds.reduced(Object::margin).getTopLeft() + dragOffset; Point newPosBotRight = toDrag->originalBounds.reduced(Object::margin).getBottomRight() + dragOffset; if (isDraggingTop) { auto newY = roundToInt(newPosTopLeft.getY() / gridSize + 1) * gridSize; dragOffset.y = newY - toDrag->originalBounds.reduced(Object::margin).getY() - gridSize; dragOffset.y += toDrag->cnv->canvasOrigin.y % gridSize + 1; } if (isDraggingBottom) { auto newY = roundToInt(newPosBotRight.getY() / gridSize + 1) * gridSize; dragOffset.y = newY - toDrag->originalBounds.reduced(Object::margin).getBottom() - gridSize; dragOffset.y += toDrag->cnv->canvasOrigin.y % gridSize + 1; } if (isDraggingLeft) { auto newX = roundToInt(newPosTopLeft.getX() / gridSize + 1) * gridSize; dragOffset.x = newX - toDrag->originalBounds.reduced(Object::margin).getX() - gridSize; dragOffset.x += toDrag->cnv->canvasOrigin.x % gridSize + 1; } if (isDraggingRight) { auto newX = roundToInt(newPosBotRight.getX() / gridSize + 1) * gridSize; dragOffset.x = newX - toDrag->originalBounds.reduced(Object::margin).getRight() - gridSize; dragOffset.x += toDrag->cnv->canvasOrigin.x % gridSize + 1; } } clearIndicators(true); return dragOffset; } // Calculates the path of the grid lines Line ObjectGrid::getObjectIndicatorLine(Side const side, Rectangle b1, Rectangle b2) { // When snapping from both sides, we need to shorten the lines to prevent artifacts (because the line will follow mouse position on the opposite axis) if (side == Top || side == Bottom || side == VerticalCentre) { b2 = b2.reduced(2, 0); } else { b2 = b2.reduced(0, 2); } switch (side) { case Left: { if (b1.getY() > b2.getY()) { return { b2.getTopLeft(), b1.getBottomLeft() }; } return { b1.getTopLeft(), b2.getBottomLeft() }; } case Right: { if (b1.getY() > b2.getY()) { return { b2.getTopRight(), b1.getBottomRight() }; } return { b1.getTopRight(), b2.getBottomRight() }; } case Top: { if (b1.getX() > b2.getX()) { return { b2.getTopLeft(), b1.getTopRight() }; } return { b1.getTopLeft(), b2.getTopRight() }; } case Bottom: { if (b1.getX() > b2.getX()) { return { b2.getBottomLeft(), b1.getBottomRight() }; } return { b1.getBottomLeft(), b2.getBottomRight() }; } case VerticalCentre: { if (b1.getX() > b2.getX()) { return { b2.getX(), b2.getCentreY(), b1.getRight(), b1.getCentreY() }; } return { b1.getX(), b1.getCentreY(), b2.getRight(), b2.getCentreY() }; } case HorizontalCentre: { if (b1.getY() > b2.getY()) { return { b2.getCentreX(), b2.getY(), b1.getCentreX(), b1.getBottom() }; } return { b1.getCentreX(), b1.getY(), b2.getCentreX(), b2.getBottom() }; } } return { }; } void ObjectGrid::clearIndicators(bool const fast) { float const lineFadeMs = fast ? 50 : 300; if (lineTargetAlpha[0] != 0.0f || lineTargetAlpha[1] != 0.0f) { startLineFadeAnimation(0, lineFadeMs, 0.0f); startLineFadeAnimation(1, lineFadeMs, 0.0f); } } void ObjectGrid::setIndicator(int const idx, Line const line) { auto const lineIsEmpty = line.getLength() == 0; if (lineIsEmpty && line != lines[idx]) { startLineFadeAnimation(idx, 50.f, 0.0f); } else if (line != lines[idx]) { lineTargetAlpha[idx] = 1.0f; lineAlpha[idx] = 1.0f; auto lineArea = cnv->editor->nvgSurface.getLocalArea(cnv, Rectangle(line.getStart(), line.getEnd()).expanded(2)); if (lines[idx].getLength() != 0) { auto const oldLineArea = cnv->editor->nvgSurface.getLocalArea(cnv, Rectangle(lines[idx].getStart(), lines[idx].getEnd()).expanded(2)); lineArea = lineArea.getUnion(oldLineArea); } cnv->editor->nvgSurface.invalidateArea(lineArea); lines[idx] = line; } } void ObjectGrid::render(NVGcontext* nvg) { if (lines[0].getLength() != 0) { nvgStrokeColor(nvg, nvgColour(PlugDataColours::gridLineColour.withAlpha(lineAlpha[0]))); nvgStrokeWidth(nvg, 1.0f); nvgBeginPath(nvg); nvgMoveTo(nvg, lines[0].getStartX(), lines[0].getStartY()); nvgLineTo(nvg, lines[0].getEndX(), lines[0].getEndY()); nvgStroke(nvg); } if (lines[1].getLength() != 0) { nvgStrokeColor(nvg, nvgColour(PlugDataColours::gridLineColour.withAlpha(lineAlpha[1]))); nvgStrokeWidth(nvg, 1.0f); nvgBeginPath(nvg); nvgMoveTo(nvg, lines[1].getStartX(), lines[1].getStartY()); nvgLineTo(nvg, lines[1].getEndX(), lines[1].getEndY()); nvgStroke(nvg); } }