Making pie-shaped button components

Hello,

I want to make a series of buttons where every button is like the slice of a pie. The Juce logo is a good example of what I’m looking for (where you can click on each section as a diffierent button). I also saw that @eyalamir made something similar in Beat Scholar–this is pretty much exactly what I want to do.

I can sort of imagine how to do it using AffineTransform and hitTest. But I’ve never worked with radial geometry before, and I’m worried that what I would come up with would be very convoluted. Can anyone share tips or some sample code so that I can make sure I’m on the right path?

It is pretty involved and I don’t have any public code to share with you - these are really things that are in the core of our product and not things that are public.

JUCE does have some math helpers that could help you with the math part like point.getPointOnCircumference().

As for HitTest - that could maybe work but we don’t use it for other reasons.
For example, when we have complicated drag interactions with the different circle slices where you have gestures dragging across a few circles. In those cases, we can’t allow a single circle or circle slice to take over the mouseEvent.

Like Eyal said, there are helpers, and I think you don’t need to get your geometry books out.

For the angles there is also stuff in juce::Point.

The devil will be in the detail, like where does the click end up, can you swipe along components (spoiler: you cannot).

Just start going and ask when you cannot find a method.

The simple way is to convert to polar coordinates and test if the radius is outside the bounds of the containing circle, and calculate which slice of the pie the angle lies in otherwise. Here’s a quick example (untested, but I think this is correct but you might want to run and double check the boundary conditions to be sure). There might be a bug if you hit exactly (0, y).

/// Given a pie with N slices, get the index of the slice that the point (x, y) is
/// contained within assuming slices are oriented with slice 0 between the angles
/// [0, 2pi/N) and the radius of the circle is 1.0. 
std::optional<int> getSliceForCoordinate(float x, float y, int N) 
{
  auto twoPi = 2.0f * 3.141592653589793f;
 
  // Convert to polar.
  auto R     = sqrtf(x * x + y * y);
  auto theta = atan2(y, x);
  
  // If the radius is outside the circle, return none.
  if (R >= 1.0f)
  {
    return {};
  }
  
  // Get the slice that the angle resides in.
  auto n = floorf(static_cast<float>(N) * (theta / twoPi));

  // Return the result.
  return std::make_optional(static_cast<int>(n));
}

If you want to make the circle bigger then adjust the radius test. If you want to rotate it so the slices are oriented differently you have a few options, from rotating the input coordinates using a transform, adding an offset to the calculation and using remainder to keep it in the range [0, 2pi], etc.

It helps to draw a picture and do the math by hand, and write some tests for the function.

Indeed, if you remember geometry from school, no need for juce.
But if you use JUCE for everything else, then why write stuff yourself:

bool hitTest(int x, int y) override
{
    juce::Point<float> point (x, y);
    auto centre = getLocalBounds().getCentre().toFloat();
    // check outside
    if ((point - centre).length() > radius)
        return false;

    angle = juce::radiansToDegrees (centre.getAngleToPoint (point));

    // check if in segment 0 to 30 degrees
    return angle >= 0.0f && angle < 30.0f;
}

// or drawing:
void paint (juce::Graphics& g) override
{
    juce::Path p;
    p.addPieSlice (getLocalBounds().toFloat(), 
        juce::degreesToRadians (0.0f),
        juce::degreesToRadians (30.0f),
        0.0f);

    if (selected)
        g.fillPath (p);
    else
        g.strokePath (p, juce::PathStrokeType (2.0f));
}

All untested…

Because it’s simpler, smaller, faster, stateless, easier to test, and portable. Why use a framework for simple things when you don’t have to?

Because the framework is hopefully tested, the code is more readable, less prone for copy paste bugs.

Anyway, all ways lead to Rome.