Has anyone used ClipperLib inside JUCE, and how do you convert back/forth to a juce::Path?

http://www.angusj.com/delphi/clipper.php

I’m trying to test out the ClipperLib in JUCE, in a vanilla GUI app.

I’ve included the clipper.cpp and clipper.hpp files in the project.

Has anyone used this lib, and can you explain how to go from one of their Paths objects to a juce::Path?

I’m looking at this code example from their website, which I have wrapped into a function.


void MainComponent::doClipTest()
{
    Paths subj(2), clip(1), solution;
    
    //define outer blue 'subject' polygon
    subj[0] <<
    IntPoint(180,200) << IntPoint(260,200) <<
    IntPoint(260,150) << IntPoint(180,150);
    
    //define subject's inner triangular 'hole' (with reverse orientation)
    subj[1] <<
    IntPoint(215,160) << IntPoint(230,190) << IntPoint(200,190);
    
    //define orange 'clipping' polygon
    clip[0] <<
    IntPoint(190,210) << IntPoint(240,210) <<
    IntPoint(240,130) << IntPoint(190,130);
    
    //draw input polygons with user-defined routine ...
    DrawPolygons(subj, 0x160000FF, 0x600000FF); //blue
    DrawPolygons(clip, 0x20FFFF00, 0x30FF0000); //orange
    
    //perform intersection ...
    Clipper c;
    c.AddPaths(subj, ptSubject, true);
    c.AddPaths(clip, ptClip, true);
    c.Execute(ctIntersection, solution, pftNonZero, pftNonZero);
    
    //draw solution with user-defined routine ...
    DrawPolygons(solution, 0x3000FF00, 0xFF006600); //solution shaded green
}

The idea is it calls a “user-defined function” (i.e. one that I have to supply) DrawPolygons that draws the results of the operation. It seems to me that I should take their ClipperLib::Paths object and turn it into a juce::Path and then draw it, but I’m wondering if anyone has done this; before I try to reinvent the wheel.

I once wrote this, but am not using it in production. The juce path needs to be flattened first using PathFlatteningIterator, to only contain line sections. In my case I knew my path only has lineTo, so I didn’t put that into the method. scaleFact is needed as clipper is Integer-only.

#include "clipper/Clipper.hpp"

Path clipperPathToJucePath(const ClipperLib::Path &p, float scaleFact = 1.f / 16.f) {
    Path result;
    if (p.size() > 0) {
        result.startNewSubPath(p[0].X*scaleFact, p[0].Y*scaleFact);
        for (int i = 1; i < int(p.size()); ++i) {
            result.lineTo(p[i].X*scaleFact, p[i].Y*scaleFact);
        }
        result.closeSubPath();
    }
    return result;
};

ClipperLib::Path jucePolygonPathToClipperPath(const Path &p, float scaleFact = 16.f) {
    ClipperLib::Path result;

    Path::Iterator iter(p);
    while (iter.next()) {
        if (iter.elementType == Path::Iterator::PathElementType::closePath) {
            break;
        } else if (iter.elementType == Path::Iterator::PathElementType::startNewSubPath) {
            result << ClipperLib::IntPoint((ClipperLib::cInt)(iter.x1*scaleFact), (ClipperLib::cInt)(iter.y1*scaleFact));
        } else if (iter.elementType == Path::Iterator::PathElementType::lineTo) {
            result << ClipperLib::IntPoint((ClipperLib::cInt)(iter.x1*scaleFact), (ClipperLib::cInt)(iter.y1*scaleFact));
        } else {
            jassert(false);
        }
    }
    return result;
}
1 Like

Thank you! That was enough to get me going; I made their example work.

PS: how do you flatten a path using PathFlatteningIterator? I can’t seem to find any examples of that.

I have a JUCE Path with quadratic curves in it… not seeing how to prepare it for ClipperLib yet…

EDIT (later): I think I figured it out, something like:

    
       // flatten the incoming path p
        PathFlatteningIterator it (p, AffineTransform(), .1f);
        
        Path flat;   // new flattened path
        
        while (it.next())
        {
            if (it.subPathIndex == 0)   // first line segment
            {
                flat.startNewSubPath (it.x1, it.y1);
            }
            
            flat.lineTo(it.x2, it.y2);

            if (it.closesSubPath)   // last point
            {
                flat.closeSubPath();
            }
        }
constexpr auto scaleFactor = 10000;

using ClipPath = ClipperLib::Path;
using ClipPaths = ClipperLib::Paths;
using Clipper = ClipperLib::Clipper;
using IntPoint = ClipperLib::IntPoint;

//==============================================================================
class PolygonClipper
{
public:
    PolygonClipper() = default;
    ~PolygonClipper() = default;

	void addPath(const juce::Path& pathToAdd)
	{
		try
		{
			addPathSubject(pathToAdd);
		}
		catch (const ClipperLib::clipperException& e)
		{
			DBG(e.what());
			jassertfalse;
		}

	}

	juce::Path getUnion()
	{
		ClipPaths clipResults;
		clipper.Execute(ClipperLib::ctUnion, clipResults, ClipperLib::pftNonZero, ClipperLib::pftNonZero);

		juce::Path path;

		// We already know how much points we need
		{
			int totalNumPoints = 0;
		
			for (const auto& clipResult : clipResults)
				totalNumPoints += static_cast<int>(clipResult.size());

			const auto numSubPaths = static_cast<int>(clipResults.size());

			// 3 Coordinates per point + extra start/close for each sub path
			const int numToAllocate = totalNumPoints * 3 + numSubPaths * 6;
			path.preallocateSpace(numToAllocate);
		}

		// Create line segments from clipped paths.
		for(auto& clipResult : clipResults)
		{
			const auto numPoints = clipResult.size();
			if (numPoints >= 2)
			{
				const auto& start = clipResult[0];
				
				path.startNewSubPath(static_cast<float>(start.X), static_cast<float>(start.Y));

				for (int i = 1; i < numPoints - 1; ++i)
				{
					const auto& point = clipResult[i];
					path.lineTo(static_cast<float>(point.X), static_cast<float>(point.Y));
				}

				const auto& last = clipResult[numPoints - 1];
				path.lineTo(static_cast<float>(last.X), static_cast<float>(last.Y));

				path.closeSubPath();
			}
		}

		// Undo scaling
		{
			const auto transform = juce::AffineTransform::scale(1.0f / static_cast<float>(scaleFactor));
			path.applyTransform(transform);
		}

		return path;
	}

private:
	void addPathSubject(const juce::Path& pathToAdd)
    {
        // Flatten and scale path. Round to Int, since the Clipper only works with Integers.
		ClipPath shape;
		shape.reserve(256);

		const auto transform = juce::AffineTransform::scale(scaleFactor);
		juce::PathFlatteningIterator iterator(pathToAdd, transform);

		while (iterator.next())
		{
			auto x1 = juce::roundToInt(iterator.x1);
			auto x2 = juce::roundToInt(iterator.x2);
			auto y1 = juce::roundToInt(iterator.y1);
			auto y2 = juce::roundToInt(iterator.y2);

			shape.push_back(IntPoint(x1, y1));
			shape.push_back(IntPoint(x2, y2));

			if (iterator.closesSubPath)
			{
				clipper.AddPath(shape, ClipperLib::ptSubject, true);
				shape.clear();
			}
		}
    }

private:
    Clipper clipper;
};

While doing experiments with Clipper and Polygon Triangulation I created this lil helper class. It creates an union. Add other methods for your desired operation. Be aware that clipper uses Integer points, so you have to scale everything to get enough float precision. applyTransform is used here for this purpose.

Also btw. Adding a subject can throw an exception, so make sure to catch it. Other operations don’t throw if I remember correctly.

Also something noteworthy: If your path contains holes (the winding order stuff), you have to use ClipperLib::PolyTree instead of ClipperLib::Paths to represent it.