fun drawing bug

I have a plugin that draws an array of polygons. The fill color of each is either white, grey, or a color computed based off the plugin state. Each polygon can have a unique color, though, in practice there should probably only be a couple dozen unique colors. There are only 50 or so polygons. To draw them, I simply iterate over the array and make the expected nanovg calls in the widgets draw method.

The grey and white fills are handled correctly; however, the uniquely computed colors are not: every polygon is filled with the same color; which color that is depends on the state, but it’s always one of the unique colors I expect.

I’ve debugged this draw call numerous ways: I’ve confirmed that for a given state, I have a map of <Polygon, NVGcolor> that contains the expected number of unique NVColors. I simply apply the fill like so:

for (const auto &poly : polygons)
    {

      nvgStrokeColor(vg, nvgRGBf(0.88, 0.88, 0.88));
      nvgStrokeWidth(vg, 1.0);
      nvgLineJoin(vg, NVG_MITER);

      // Start drawing a path
      nvgBeginPath(vg);
[...]
     DEBUG("filling color: %s", nvcolor_to_string(fillColors[poly]).c_str());
    // Close the path and fill
    nvgClosePath(vg);
    nvgFillColor(vg, fillColors[poly]);
    nvgStroke(vg);
    nvgFill(vg);

and I’ve confirmed that more than one color is set:

[7.115 debug src/AmB-Tonnetz.cpp:376 draw] filling color: 0.71, 0.79, 0.82
[7.115 debug src/AmB-Tonnetz.cpp:376 draw] filling color: 0.71, 0.79, 0.82
[7.115 debug src/AmB-Tonnetz.cpp:376 draw] filling color: 0.71, 0.79, 0.82
[7.115 debug src/AmB-Tonnetz.cpp:376 draw] filling color: 0.90, 1.00, 0.90
[7.115 debug src/AmB-Tonnetz.cpp:376 draw] filling color: 0.71, 0.79, 0.82
[7.115 debug src/AmB-Tonnetz.cpp:376 draw] filling color: 0.71, 0.79, 0.82

However, all polys remain the same color. Are there any tricks to debugging this further? FWIW, I have tried filling with gradients but the result is the same.

This is compiled on darwin, arm_64 using clang.

EDIT: “However, all polys remain the same color.” > All polys not clicked and therefore grey. The grey fill works as expected.

EDIT2: for anyone coming to this thread: there’s something in the color computation code that seems to clobber the nvgColorFill values. I’m fairly mystified since I don’t manage any allocations manually and my 3 lines of eigen code are stock. I might just rewrite this whole thing in rust.

1 Like

Try moving the begin path to before the set color

Is your code runnable on GitHub?

It is probably not the issue, but just to double check, what is the type inside the fillColors array?

edit: sorry, re-read the post, fillColors is the map? Are you sure using the poly as a key works as you think?

Oh yeah good guess Print the rgb values

Edit: Oh except op does that :slight_smile:

Yeah I see :wink: but I was thinking don’t some of the different color types do different things, but maybe they stringify the same?

The log output has floats, but NanoVG says this:

// Colors in NanoVG are stored as unsigned ints in ABGR format.

The type in the map values should be NVGcolor

I think if it wasn’t there would be an error, but like I said, just want to double check :man_shrugging:

I haven’t used NanoVG in a while but are you sure nvgClosePath is in the right place? On a quick look I see some code examples that seem to perform fills before closing paths. This is just a hunch.

I wonder if the color that all the polys get happens to be the color associated with the final poly that you draw? That might be a useful thing to pin down (and it might be related to path closure as well). If it’s not the final poly, is it the first poly? Maybe manually clobber the first call with red and the last call with green and see if there’s consistent behaviour.

Also, welcome to the forum!

1 Like

This could be it, I just went to check how I use NanoVG in Purfenator, this works for me, so try this order:

1 Like

Yeah I agree

Either it is (1) the color object is of the wrong type or (2) your begin end close fill call order needs tweaking as most likely

There’s a small likelihood set of (3) but I would start by moving calls around. It’s all super stateful (of course! It’s a drawing so I)

1 Like

Exactly–and stateful in a way where often TIMTOWTDI, which does not always make debugging easier!

so from that example it looks like maybe nvgFillColor needs to be called while the path is open?

Colors are set via the nvgCalls, the f variant expects float

NVGcolor nearlyWhiteGreen = nvgRGBf(0.9f, 1.0f, 0.9f);
NVGcolor deepBlue = nvgRGBf(0.0f, 0.0f, 0.5f);
polyFillColor = nvgLerpRGBA(deepBlue, nearlyWhiteGreen, euclideanD);

It appears that the call order does not matter in this case:

     // Close the path and fill
      nvgFillColor(vg, fillColors[poly]);
      nvgClosePath(vg);
      nvgStroke(vg);
      nvgFill(vg);

Does not correct the problem.

Nor does:

// Start drawing a path
      nvgBeginPath(vg);
      nvgFillColor(vg, fillColors[poly]);

Nothing seems particularly wrong from the segments you’ve shown. If you run in a debugger is state->fill correct, that’s how nvg works internally.

But I have a sneaking suspicion the problem is outside the fragments you shared. Are you sure your polygons don’t overlap for instance? Hard to tell without it being runnable

1 Like

is this your own func? what does it do?

Yes, and the nvgLerpRGBA returns the NVGcolor type that you want, but in the interests of tracking down the issue, try changing to use the non-float version and see if there is any difference.

Also this code does not show how you are adding the color value into the map, perhaps that is an area you could investigate more? Try substituting the color from the map with some hard coded color values and see if the polygons use those?

Clicked polygons are shaded grey. This works as expected; there’s no issues with the fill.

The grey, clicked polygons work as expected and the log of the colors in the map indicates that there are at least 2 colors that should be visible in every draw call not including grey and white.

(I also moved the colors to the map as a sanity check, previously I computed the colors inline and the issue was the same. It really looks like nanovg is clobbering a variable somehow. I’ll paste the whole draw function below.)

Here’s the whole draw function for reference:

void draw(Vec widgetSize, const std::map<Vertex, std::string> &labelMap, std::map<std::vector<Vertex>, TIP> &tips,
            NVGcontext *vg)

  {
    // Check if NanoVG context is valid
    if (!vg)
    {
      std::cerr << "NanoVG context is not initialized." << std::endl;
      return;
    }
    nvgSave(vg);

    // Determine x, y scaling for the drawing
    float xScale =
        (widgetSize.x - padding) / (maxExtent.first - minExtent.first);
    float yScale =
        (widgetSize.y - padding) / (maxExtent.second - minExtent.second);

    float xOffset = widgetSize.x / 2;
    float yOffset = widgetSize.y / 2;
    std::map<Polygon, NVGcolor> fillColors;
    for (const auto &poly : polygons)
    {
      bool isClicked = false;
      NVGcolor polyFillColor = nvgRGBf(1.0f, 1.0f, 1.0f);
      std::lock_guard<std::mutex>
          guard(coordinatesMutex);

      // Check if the polygon is clicked
      if (!poly.vertexCoords.empty() && !coordinatesOn.empty())
      {
        isClicked = std::all_of(
            poly.vertexCoords.begin(), poly.vertexCoords.end(),
            [this](const Vertex &v)
            {
              return std::find(coordinatesOn.begin(), coordinatesOn.end(), v) !=
                     coordinatesOn.end();
            });
      }
      if (!coordinatesOn.empty())
      {
        const TIP thisTip = tips[poly.vertexCoords];
        const float euclideanD = thisTip.euclideanDistanceTo(tipOn);
        // These logs indicate that there are various values of euclideanD in the loop.
        DEBUG("Euclidean distance: %f", euclideanD);

        NVGcolor nearlyWhiteGreen = nvgRGBf(0.9f, 1.0f, 0.9f);
        NVGcolor deepBlue = nvgRGBf(0.0f, 0.0f, 0.5f);
        if (euclideanD > 0.7)
        {
          polyFillColor = nvgRGB(255, 0, 0); // Red color
        }
        else
        {
          polyFillColor = nvgLerpRGBA(deepBlue, nearlyWhiteGreen, euclideanD);
        }
      }
      else
      {
        polyFillColor = nvgRGBA(255, 255, 255, 255);
      }
      const NVGcolor isClickedFillColor =
          isClicked
              ? nvgRGBA(200, 200, 200, 255)
              : polyFillColor; // Grey if clicked, distance based chroma otherwise}
      fillColors[poly] = isClickedFillColor;
    }
    if (!coordinatesOn.empty())
    {
      std::set<NVGcolor> uniqueColors;
      for (const auto &colorPair : fillColors)
      {
        uniqueColors.insert(colorPair.second);
      }
      int uniqueColorCount = uniqueColors.size();
      DEBUG("Unique color count: %d", uniqueColorCount);
    }

    std::set<Vertex> seenVertices;
    // Iterate over each polygon
    for (const auto &poly : polygons)
    {

      nvgStrokeColor(vg, nvgRGBf(0.88, 0.88, 0.88));
      nvgStrokeWidth(vg, 1.0);
      nvgLineJoin(vg, NVG_MITER);

      // Start drawing a path
      nvgBeginPath(vg);
      // These logs indicate there are more than one color being accessed accross the loop by fillColors[poly]
      DEBUG("filling color: %s", nvcolor_to_string(fillColors[poly]).c_str());
      nvgFillColor(vg, fillColors[poly]);
      bool firstVertex = true;

      // Draw each vertex of the polygon
      float x, y;
      for (const auto &vert : poly.faceVerts)
      {
        x = xOffset + padding + vert.first * xScale;
        y = yOffset + padding + vert.second * yScale;

        if (firstVertex)
        {
          nvgMoveTo(vg, x, y);
          firstVertex = false;
        }
        else
        {
          nvgLineTo(vg, x, y);
        }
      }

      // Close the path and fill
      nvgClosePath(vg);
      nvgStroke(vg);
      nvgFill(vg);

      float r = 10;
    }
    // draw labels for each vertex
    float r = 7;

    for (const auto &poly : polygons)
    {
      for (size_t i = 0; i < poly.vertexCoords.size(); i++)
      {
        const Vertex &vertex = poly.vertexCoords[i];
        const auto &face = poly.faceVerts[i];

        auto it = labelMap.find(vertex);
        if (it != labelMap.end() &&
            seenVertices.find(vertex) == seenVertices.end())
        {
          float x = xOffset + padding + face.first * xScale;
          float y = yOffset + padding + face.second * yScale;

          // Draw a small white circle with grey stroke
          nvgBeginPath(vg);
          nvgStrokeColor(vg, nvgRGBf(0.88, 0.88, 0.88));
          nvgStrokeWidth(vg, 1.0);
          nvgCircle(vg, x, y, r);
          nvgFillColor(vg, nvgRGBA(255, 255, 255, 255)); // White fill
          nvgClosePath(vg);
          nvgFill(vg);
          nvgStroke(vg);
          centerRuledLabel(vg, x - (r / 2), y, r, it->second.c_str(), 11);
          seenVertices.insert(vertex);
          if (fillColors[poly].r < 1.0 || fillColors[poly].g > 0.0 || fillColors[poly].b > 0.0)
          {
            // Calculate the center of the polygon to place the label
            float labelX = 0.0f;
            float labelY = 0.0f;
            float r = 10;
            int vertexCount = 0;
            for (const auto &vert : poly.faceVerts)
            {
              labelX += xOffset + padding + vert.first * xScale;
              labelY += yOffset + padding + vert.second * yScale;
              vertexCount++;
            }
            labelX /= vertexCount;
            labelY /= vertexCount;

            // Set the label with the color string
            std::string colorLabel = nvcolor_to_string(fillColors[poly]);
            centerRuledLabel(vg, labelX - (r / 2), labelY, r, colorLabel.c_str(), 11);
          }
        }
      }
    }
    nvgRestore(vg);
  }

EDIT: updated

std::string nvcolor_to_string(const NVGcolor &color)
{
  std::ostringstream stream;
  stream << std::fixed << std::setprecision(2);
  stream << color.r << ", ";
  stream << color.g << ", ";
  stream << color.b;
  return stream.str();
}

What happens when you fill before stroke? One hunch is that a fill or stroke consumes the path.

No change. The grey-on-clicked fill works as expected for all polys.