OK, here goes. This code is not well tested at all. It seems to work in the simple cases I have used it for so far, but there are probably problems with it.
Also, this code was written before I started using of 070, so it doesnt use the nice new ofPolyline class.
The header file, ofxPolygonStroker.h:
#ifndef _OFX_POLYGONSTROKER
#define _OFX_POLYGONSTROKER
#include "ofPoint.h"
#include <vector>
namespace NSPolygonStroker {
typedef std::vector<ofPoint> polygonContour;
typedef std::vector<polygonContour> polygon;
enum EJoinType {
kJoinType_Miter,
// kJoinType_Round, // Doesn't work, we cannot return anything other than straight lines
kJoinType_Bevel
};
enum ECapType {
kCapType_Butt,
kCapType_Square
// kCapType_Round // Doesn't work, we cannot return anything other than straight lines
};
polygon CreateStrokedPolygon(const polygon& polygonToStroke, bool closePolygon, float strokeWidth, EJoinType joinType = kJoinType_Miter, ECapType capType = kCapType_Butt, float miterLimit = 10.0f);
} // namespace NSPolygonStroker
#endif // ndef _OFX_POLYGONSTROKER
And the implementation file, ofxPolygonStroker.cpp:
#include "ofxPolygonStroker.h"
#include <cstring>
#include <cassert>
#include <cmath>
#include <ApplicationServices/ApplicationServices.h>
namespace NSPolygonStroker {
static CGContextRef GetWorkingContext() {
static CGContextRef sWorkingContext = NULL;
if (sWorkingContext == NULL) {
CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
sWorkingContext = CGBitmapContextCreate(NULL, 16, 16, 8, 16 * 4, colorSpace, kCGBitmapByteOrderDefault | kCGImageAlphaNoneSkipFirst);
CGColorSpaceRelease(colorSpace);
colorSpace = NULL;
}
assert(sWorkingContext != NULL);
return sWorkingContext;
}
static void MyPathApplier(void* info, const CGPathElement* element) {
assert(info != NULL);
assert(element != NULL);
polygon& polygonToMutate = *reinterpret_cast<polygon*>(info);
switch (element->type) {
case kCGPathElementMoveToPoint:
polygonToMutate.push_back(polygonContour(1, ofPoint(element->points[0].x, element->points[0].y)));
break;
case kCGPathElementAddLineToPoint:
assert(polygonToMutate.size() > 0);
polygonToMutate.back().push_back(ofPoint(element->points[0].x, element->points[0].y));
break;
case kCGPathElementAddQuadCurveToPoint:
case kCGPathElementAddCurveToPoint:
// Should never happen, we only use straight edge polygons, no curves
assert(false);
break;
case kCGPathElementCloseSubpath:
assert(polygonToMutate.size() > 0);
polygonToMutate.back().push_back(polygonToMutate.back().front());
break;
default:
// What is this?
assert(false);
}
}
polygon CreateStrokedPolygon(const polygon& polygonToStroke, bool closePolygon, float strokeWidth, EJoinType joinType, ECapType capType, float miterLimit) {
CGContextRef context = GetWorkingContext();
// Set up context attributes
CGContextSetLineWidth(context, strokeWidth);
CGContextSetMiterLimit(context, miterLimit);
switch (joinType) {
case kJoinType_Miter:
CGContextSetLineJoin(context, kCGLineJoinMiter);
break;
/*
case kJoinType_Round:
CGContextSetLineJoin(context, kCGLineJoinRound);
break;
*/
case kJoinType_Bevel:
CGContextSetLineJoin(context, kCGLineJoinBevel);
break;
default:
assert(false);
}
switch (capType) {
case kCapType_Butt:
CGContextSetLineCap(context, kCGLineCapButt);
break;
case kCapType_Square:
CGContextSetLineCap(context, kCGLineCapSquare);
break;
/*
case kCapType_Round:
CGContextSetLineCap(context, kCGLineCapRound);
break;
*/
default:
assert(false);
}
// Define the polygon path(s)
CGContextBeginPath(context);
for (std::size_t contourIndex = 0; contourIndex < polygonToStroke.size(); contourIndex++) {
const polygonContour& contour = polygonToStroke[contourIndex];
for (std::size_t vertexIndex = 0; vertexIndex < contour.size(); vertexIndex++) {
const ofPoint& point = contour[vertexIndex];
if (vertexIndex == 0) {
CGContextMoveToPoint(context, point.x, point.y);
}
else {
CGContextAddLineToPoint(context, point.x, point.y);
}
}
if (closePolygon) {
CGContextClosePath(context);
}
}
// Modify the path to a stroked path
CGContextReplacePathWithStrokedPath(context);
// Copy the path from the context
CGPathRef strokedPath = CGContextCopyPath(context);
assert(strokedPath != NULL);
// Clear the current path
CGContextBeginPath(context);
polygon resultPolygon;
CGPathApply(strokedPath, &resultPolygon, &MyPathApplier);
CGPathRelease(strokedPath);
return resultPolygon;
}
} // namespace NSPolygonStroker
And finally, sample usage for generating and drawing a stroked rectangle with 10 pixels thick lines:
#include "ofxPolygonStroker.h"
// In you draw function
NSPolygonStroker::polygonContour rectContour;
rectContour.push_back(ofPoint(100, 100));
rectContour.push_back(ofPoint(200, 100));
rectContour.push_back(ofPoint(200, 200));
rectContour.push_back(ofPoint(100, 200));
NSPolygonStroker::polygon rectPolygon;
rectPolygon.push_back(rectContour);
NSPolygonStroker::polygon strokedPolygon = NSPolygonStroker::CreateStrokedPolygon(rectPolygon, true, 10.0f, NSPolygonStroker::kJoinType_Miter, NSPolygonStroker::kCapType_Butt, 2.0f);
ofSetPolyMode(OF_POLY_WINDING_NONZERO);
ofBeginShape();
for (std::size_t contourIndex = 0; contourIndex < strokedPolygon.size(); contourIndex++) {
const NSPolygonStroker::polygonContour& contour = strokedPolygon[contourIndex];
for (std::size_t vertexIndex = 0; vertexIndex < contour.size(); vertexIndex++) {
const ofPoint& vertex = contour[vertexIndex];
ofVertex(vertex.x, vertex.y);
}
if (contourIndex < (strokedPolygon.size() - 1)) {
ofNextContour();
}
}
ofEndShape();
// Restore the poly mode to the default
ofSetPolyMode(OF_POLY_WINDING_ODD);
The example will generate two polygon contours: an outer that will be filled, and an inner that forms a hole in the outer contour. Note that the generated contours are messy and contains lots of redundant points, but it seems to work. 
HTH
/Jesper