mirror of
https://github.com/darlinghq/darling-cocotron.git
synced 2024-11-27 14:00:22 +00:00
1479 lines
47 KiB
Objective-C
1479 lines
47 KiB
Objective-C
/*------------------------------------------------------------------------
|
||
*
|
||
* Derivative of the OpenVG 1.0.1 Reference Implementation
|
||
* -------------------------------------
|
||
*
|
||
* Copyright (c) 2007 The Khronos Group Inc.
|
||
*
|
||
* Permission is hereby granted, free of charge, to any person obtaining a
|
||
* copy of this software and /or associated documentation files
|
||
* (the "Materials "), to deal in the Materials without restriction,
|
||
* including without limitation the rights to use, copy, modify, merge,
|
||
* publish, distribute, sublicense, and/or sell copies of the Materials,
|
||
* and to permit persons to whom the Materials are furnished to do so,
|
||
* subject to the following conditions:
|
||
*
|
||
* The above copyright notice and this permission notice shall be included
|
||
* in all copies or substantial portions of the Materials.
|
||
*
|
||
* THE MATERIALS ARE PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||
* DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
||
* OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE MATERIALS OR
|
||
* THE USE OR OTHER DEALINGS IN THE MATERIALS.
|
||
*
|
||
*-------------------------------------------------------------------*/
|
||
|
||
#import "VGPath.h"
|
||
#import <Onyx2D/VGmath.h>
|
||
|
||
static inline void RI_SWAP(O2Float *a, O2Float *b) { O2Float tmp = *a; *a = *b; *b = tmp; }
|
||
static inline O2Float RI_RAD_TO_DEG(O2Float a) { return (O2Float)(a * 180.0f/ M_PI); }
|
||
|
||
static inline O2Point Vector2Negate(O2Point result){
|
||
return O2PointMake(-result.x,-result.y);
|
||
}
|
||
|
||
static inline O2Float Vector2Length(O2Point v){
|
||
return sqrt((double)v.x*(double)v.x+(double)v.y*(double)v.y);
|
||
}
|
||
|
||
static inline BOOL Vector2IsEqual(O2Point v1,O2Point v2 ){
|
||
return (v1.x == v2.x) && (v1.y == v2.y);
|
||
}
|
||
|
||
static inline BOOL Vector2IsZero(O2Point v){
|
||
return (v.x == 0.0f) && (v.y == 0.0f);
|
||
}
|
||
|
||
static inline O2Point Vector2MultiplyByFloat(O2Point v,O2Float f){
|
||
return O2PointMake(v.x*f,v.y*f);
|
||
}
|
||
|
||
static inline O2Point Vector2Add(O2Point v1,O2Point v2 ){
|
||
return O2PointMake(v1.x+v2.x, v1.y+v2.y);
|
||
}
|
||
|
||
//if v is a zero vector, returns a zero vector
|
||
static inline O2Point Vector2Normalize(O2Point v){
|
||
double l = (double)v.x*(double)v.x+(double)v.y*(double)v.y;
|
||
|
||
if( l != 0.0 )
|
||
l = 1.0 / sqrt(l);
|
||
|
||
return O2PointMake((O2Float)((double)v.x * l), (O2Float)((double)v.y * l));
|
||
}
|
||
|
||
static inline O2Point Vector2PerpendicularCW(O2Point v){
|
||
return O2PointMake(v.y, -v.x);
|
||
}
|
||
|
||
static inline O2Point Vector2PerpendicularCCW(O2Point v){
|
||
return O2PointMake(-v.y, v.x);
|
||
}
|
||
|
||
static inline O2Point Vector2Perpendicular(O2Point v, BOOL cw){
|
||
if(cw)
|
||
return O2PointMake(v.y, -v.x);
|
||
|
||
return O2PointMake(-v.y, v.x);
|
||
}
|
||
|
||
|
||
enum VertexFlags {
|
||
START_SUBPATH = (1<<0),
|
||
END_SUBPATH = (1<<1),
|
||
START_SEGMENT = (1<<2),
|
||
END_SEGMENT = (1<<3),
|
||
CLOSE_SUBPATH = (1<<4),
|
||
IMPLICIT_CLOSE_SUBPATH = (1<<5)
|
||
};
|
||
|
||
typedef struct Vertex {
|
||
O2Point userPosition;
|
||
O2Point userTangent;
|
||
O2Float pathLength;
|
||
unsigned int flags;
|
||
} Vertex;
|
||
|
||
//data produced by tessellation
|
||
typedef struct VertexIndex {
|
||
int start;
|
||
int end;
|
||
} VertexIndex;
|
||
|
||
typedef struct {
|
||
O2Point p;
|
||
O2Point t;
|
||
O2Point ccw;
|
||
O2Point cw;
|
||
O2Float pathLength;
|
||
unsigned int flags;
|
||
BOOL inDash;
|
||
} StrokeVertex;
|
||
|
||
static inline StrokeVertex StrokeVertexInit(){
|
||
StrokeVertex result;
|
||
|
||
result.p=O2PointMake(0,0);
|
||
result.t=O2PointMake(0,0);
|
||
result.ccw=O2PointMake(0,0);
|
||
result.cw=O2PointMake(0,0);
|
||
result.pathLength=0;
|
||
result.flags=0;
|
||
result.inDash=NO;
|
||
|
||
return result;
|
||
}
|
||
|
||
#define RI_FLOAT_MAX FLT_MAX
|
||
|
||
/*-------------------------------------------------------------------*//*!
|
||
* \brief Form a reliable normalized average of the two unit input vectors.
|
||
* The average always lies to the given direction from the first
|
||
* vector.
|
||
* \param u0, u1 Unit input vectors.
|
||
* \param cw True if the average should be clockwise from u0, NO if
|
||
* counterclockwise.
|
||
* \return Average of the two input vectors.
|
||
* \note
|
||
*//*-------------------------------------------------------------------*/
|
||
|
||
static O2Point unitAverageWithDirection(O2Point u0, O2Point u1) {
|
||
O2Point u =Vector2MultiplyByFloat(Vector2Add(u0 , u1), 0.5f);
|
||
O2Point n0 = Vector2PerpendicularCCW(u0);
|
||
|
||
if( Vector2Dot(u, u) > 0.25f ){
|
||
//the average is long enough and thus reliable
|
||
if( Vector2Dot(n0, u1) < 0.0f )
|
||
u = Vector2Negate(u); //choose the larger angle
|
||
}
|
||
else {
|
||
// the average is too short, use the average of the normals to the vectors instead
|
||
O2Point n1 = Vector2PerpendicularCW(u1);
|
||
u = Vector2MultiplyByFloat(Vector2Add(n0 , n1), 0.5f);
|
||
}
|
||
|
||
u = Vector2Negate(u);
|
||
|
||
return Vector2Normalize(u);
|
||
}
|
||
|
||
/*-------------------------------------------------------------------*//*!
|
||
* \brief Form a reliable normalized average of the two unit input vectors.
|
||
* The average lies on the side where the angle between the input
|
||
* vectors is less than 180 degrees.
|
||
* \param u0, u1 Unit input vectors.
|
||
* \return Average of the two input vectors.
|
||
* \note
|
||
*//*-------------------------------------------------------------------*/
|
||
|
||
static O2Point unitAverage(O2Point u0, O2Point u1){
|
||
O2Point u =Vector2MultiplyByFloat(Vector2Add(u0 , u1), 0.5f);
|
||
|
||
if( Vector2Dot(u, u) < 0.25f ){
|
||
// the average is unreliable, use the average of the normals to the vectors instead
|
||
O2Point n0 = Vector2PerpendicularCCW(u0);
|
||
O2Point n1 = Vector2PerpendicularCW(u1);
|
||
u = Vector2MultiplyByFloat(Vector2Add(n0 , n1) , 0.5f);
|
||
if( Vector2Dot(n1, u0) < 0.0f )
|
||
u = Vector2Negate(u);
|
||
}
|
||
|
||
return Vector2Normalize(u);
|
||
}
|
||
|
||
// Interpolate the given unit tangent vectors to the given direction on a unit circle.
|
||
|
||
static O2Point circularLerpWithDirection(O2Point t0, O2Point t1, O2Float ratio) {
|
||
O2Point u0 = t0, u1 = t1;
|
||
O2Float l0 = 0.0f, l1 = 1.0f;
|
||
int i;
|
||
|
||
for(i=0;i<8;i++) {
|
||
O2Point n = unitAverageWithDirection(u0, u1);
|
||
O2Float l = 0.5f * (l0 + l1);
|
||
if( ratio < l ){
|
||
u1 = n;
|
||
l1 = l;
|
||
}
|
||
else {
|
||
u0 = n;
|
||
l0 = l;
|
||
}
|
||
}
|
||
|
||
return u0;
|
||
}
|
||
|
||
// Interpolate the given unit tangent vectors on a unit circle. Smaller angle between the vectors is used.
|
||
|
||
static O2Point circularLerp(O2Point t0, O2Point t1, O2Float ratio){
|
||
O2Point u0 = t0, u1 = t1;
|
||
O2Float l0 = 0.0f, l1 = 1.0f;
|
||
int i;
|
||
|
||
for(i=0;i<8;i++) {
|
||
O2Point n = unitAverage(u0, u1);
|
||
O2Float l = 0.5f * (l0 + l1);
|
||
if( ratio < l ){
|
||
u1 = n;
|
||
l1 = l;
|
||
}
|
||
else {
|
||
u0 = n;
|
||
l0 = l;
|
||
}
|
||
}
|
||
|
||
return u0;
|
||
}
|
||
|
||
@implementation VGPath
|
||
|
||
-initWithKGPath:(O2Path *)path {
|
||
_path=[path retain];
|
||
self->m_userMinx=0.0f;
|
||
self->m_userMiny=0.0f;
|
||
self->m_userMaxx=0.0f;
|
||
self->m_userMaxy=0.0f;
|
||
self->_vertexCount=0;
|
||
self->_vertexCapacity=2;
|
||
self->_vertices=NSZoneMalloc(NULL,self->_vertexCapacity*sizeof(Vertex));
|
||
self->_segmentToVertexCapacity=2;
|
||
self->_segmentToVertex=NSZoneMalloc(NULL,self->_segmentToVertexCapacity*sizeof(VertexIndex));
|
||
return self;
|
||
}
|
||
|
||
-(void)dealloc {
|
||
[_path release];
|
||
NSZoneFree(NULL,self->_vertices);
|
||
NSZoneFree(NULL,self->_segmentToVertex);
|
||
[super dealloc];
|
||
}
|
||
|
||
/// Given a path segment type, returns the number of coordinates it uses.
|
||
|
||
int O2PathElementTypeToNumCoordinates(O2PathElementType segment){
|
||
RI_ASSERT(((int)segment) >= 0 && ((int)segment) <= 4);
|
||
static const int coords[5] = {1,1,2,3,0};
|
||
return coords[(int)segment];
|
||
}
|
||
|
||
// Computes the number of coordinates a segment sequence uses.
|
||
|
||
int VGPathCountNumCoordinates(const uint8_t* segments, int numSegments){
|
||
RI_ASSERT(segments);
|
||
RI_ASSERT(numSegments >= 0);
|
||
|
||
int coordinates = 0;
|
||
int i;
|
||
for(i=0;i<numSegments;i++)
|
||
coordinates += O2PathElementTypeToNumCoordinates((O2PathElementType)segments[i]);
|
||
|
||
return coordinates;
|
||
}
|
||
|
||
// Tessellates a path for filling and appends resulting edges to a context.
|
||
|
||
void VGPathFill(VGPath *self,O2AffineTransform pathToSurface, O2Context_builtin *context){
|
||
|
||
VGPathTessellateIfNeeded(self);
|
||
|
||
O2Point p0=O2PointMake(0,0);
|
||
int i;
|
||
|
||
for(i=0;i<self->_vertexCount;i++){
|
||
O2Point p1 = O2PointApplyAffineTransform(self->_vertices[i].userPosition,pathToSurface );
|
||
|
||
if(!(self->_vertices[i].flags & START_SEGMENT)){
|
||
//in the middle of a segment
|
||
O2DContextAddEdge(context,p0, p1);
|
||
}
|
||
|
||
p0 = p1;
|
||
}
|
||
}
|
||
|
||
/* Smoothly interpolates between two StrokeVertices. Positions
|
||
are interpolated linearly, while tangents are interpolated
|
||
on a unit circle. Stroking is implemented so that overlapping
|
||
geometry doesnt cancel itself when filled with nonzero rule.
|
||
The resulting polygons are closed. */
|
||
|
||
|
||
static inline O2Point O2PointApplyAffineTransformNoTranslate(O2Point point,O2AffineTransform xform){
|
||
O2Point result;
|
||
|
||
result.x=xform.a*point.x+xform.c*point.y;
|
||
result.y=xform.b*point.x+xform.d*point.y;
|
||
|
||
return result;
|
||
}
|
||
|
||
void VGPathInterpolateStroke(O2AffineTransform pathToSurface, O2Context_builtin *context,StrokeVertex v0,StrokeVertex v1, O2Float strokeWidth){
|
||
O2Point ppccw = O2PointApplyAffineTransform(v0.ccw,pathToSurface);
|
||
O2Point ppcw = O2PointApplyAffineTransform(v0.cw,pathToSurface);
|
||
O2Point endccw = O2PointApplyAffineTransform(v1.ccw,pathToSurface);
|
||
O2Point endcw = O2PointApplyAffineTransform(v1.cw,pathToSurface);
|
||
|
||
const O2Float tessellationAngle = 5.0f;
|
||
|
||
O2Float angle = RI_RAD_TO_DEG((O2Float)acos(RI_CLAMP(Vector2Dot(v0.t, v1.t), -1.0f, 1.0f))) / tessellationAngle;
|
||
int samples = RI_INT_MAX((int)ceil(angle), 1);
|
||
|
||
O2Point pnccw = ppccw;
|
||
O2Point pncw = ppcw;
|
||
|
||
if(samples==1){
|
||
/* If there is only one sample then the line is close to straight, we use the tangent of one of the vertices
|
||
instead of interpolating and just make a box.
|
||
*/
|
||
|
||
// O2Float t = 1;
|
||
// O2Point tangent = circularLerp(v0.t, v1.t, t);
|
||
O2Point v0p=O2PointApplyAffineTransform(v0.p,pathToSurface);
|
||
O2Point v1p=O2PointApplyAffineTransform(v1.p,pathToSurface);
|
||
O2Point n=O2PointApplyAffineTransformNoTranslate(Vector2MultiplyByFloat(Vector2Normalize(Vector2PerpendicularCCW(v1.t)) , strokeWidth * 0.5f),pathToSurface);
|
||
|
||
O2Point npccw = Vector2Add(v0p, n);
|
||
O2Point npcw = Vector2Subtract(v0p, n);
|
||
O2Point nnccw = Vector2Add(v1p,n);
|
||
O2Point nncw = Vector2Subtract(v1p , n);
|
||
|
||
O2DContextAddEdge(context,npccw, nnccw);
|
||
O2DContextAddEdge(context,nnccw, nncw);
|
||
O2DContextAddEdge(context,nncw, npcw);
|
||
O2DContextAddEdge(context,npcw, npccw);
|
||
#if 0
|
||
if(Vector2Dot(n,v0.t) <= 0.0f){
|
||
O2DContextAddEdge(context,pnccw, npcw);
|
||
O2DContextAddEdge(context,npcw, pncw);
|
||
O2DContextAddEdge(context,pncw, npccw);
|
||
O2DContextAddEdge(context,npccw, pnccw);
|
||
}
|
||
else {
|
||
O2DContextAddEdge(context,pnccw, npccw);
|
||
O2DContextAddEdge(context,npccw, pncw);
|
||
O2DContextAddEdge(context,pncw, npcw);
|
||
O2DContextAddEdge(context,npcw, pnccw);
|
||
}
|
||
|
||
ppccw = npccw;
|
||
ppcw = npcw;
|
||
pnccw = nnccw;
|
||
pncw = nncw;
|
||
|
||
//connect the last segment to the end coordinates
|
||
n = Vector2PerpendicularCCW(v1.t);
|
||
|
||
if(Vector2Dot(n,tangent) <= 0.0f){
|
||
O2DContextAddEdge(context,pnccw, endcw);
|
||
O2DContextAddEdge(context,endcw, pncw);
|
||
O2DContextAddEdge(context,pncw, endccw);
|
||
O2DContextAddEdge(context,endccw, pnccw);
|
||
}
|
||
else {
|
||
O2DContextAddEdge(context,pnccw, endccw);
|
||
O2DContextAddEdge(context,endccw, pncw);
|
||
O2DContextAddEdge(context,pncw, endcw);
|
||
O2DContextAddEdge(context,endcw, pnccw);
|
||
}
|
||
#endif
|
||
}
|
||
else {
|
||
O2Point prev = v0.p;
|
||
O2Point prevt = v0.t;
|
||
int j;
|
||
|
||
for(j=0;j<samples;j++){
|
||
O2Float t = (O2Float)(j+1) / (O2Float)samples;
|
||
O2Point position = Vector2Add(Vector2MultiplyByFloat(v0.p , (1.0f - t)) , Vector2MultiplyByFloat(v1.p ,t));
|
||
O2Point tangent = circularLerp(v0.t, v1.t, t);
|
||
O2Point n = Vector2MultiplyByFloat(Vector2Normalize(Vector2PerpendicularCCW(tangent)) , strokeWidth * 0.5f);
|
||
|
||
if(j == samples-1)
|
||
position = v1.p;
|
||
|
||
O2Point npccw = O2PointApplyAffineTransform(Vector2Add(prev, n),pathToSurface);
|
||
O2Point npcw = O2PointApplyAffineTransform(Vector2Subtract(prev, n),pathToSurface);
|
||
O2Point nnccw = O2PointApplyAffineTransform(Vector2Add(position,n),pathToSurface);
|
||
O2Point nncw = O2PointApplyAffineTransform(Vector2Subtract(position , n),pathToSurface);
|
||
|
||
O2DContextAddEdge(context,npccw, nnccw);
|
||
O2DContextAddEdge(context,nnccw, nncw);
|
||
O2DContextAddEdge(context,nncw, npcw);
|
||
O2DContextAddEdge(context,npcw, npccw);
|
||
|
||
if(Vector2Dot(n,prevt) <= 0.0f){
|
||
O2DContextAddEdge(context,pnccw, npcw);
|
||
O2DContextAddEdge(context,npcw, pncw);
|
||
O2DContextAddEdge(context,pncw, npccw);
|
||
O2DContextAddEdge(context,npccw, pnccw);
|
||
}
|
||
else {
|
||
O2DContextAddEdge(context,pnccw, npccw);
|
||
O2DContextAddEdge(context,npccw, pncw);
|
||
O2DContextAddEdge(context,pncw, npcw);
|
||
O2DContextAddEdge(context,npcw, pnccw);
|
||
}
|
||
|
||
ppccw = npccw;
|
||
ppcw = npcw;
|
||
pnccw = nnccw;
|
||
pncw = nncw;
|
||
prev = position;
|
||
prevt = tangent;
|
||
}
|
||
//connect the last segment to the end coordinates
|
||
O2Point n = Vector2PerpendicularCCW(v1.t);
|
||
|
||
if(Vector2Dot(n,prevt) <= 0.0f){
|
||
O2DContextAddEdge(context,pnccw, endcw);
|
||
O2DContextAddEdge(context,endcw, pncw);
|
||
O2DContextAddEdge(context,pncw, endccw);
|
||
O2DContextAddEdge(context,endccw, pnccw);
|
||
}
|
||
else {
|
||
O2DContextAddEdge(context,pnccw, endccw);
|
||
O2DContextAddEdge(context,endccw, pncw);
|
||
O2DContextAddEdge(context,pncw, endcw);
|
||
O2DContextAddEdge(context,endcw, pnccw);
|
||
}
|
||
|
||
}
|
||
|
||
}
|
||
|
||
// Generate edges for stroke caps. Resulting polygons are closed.
|
||
|
||
void VGPathDoCap(O2AffineTransform pathToSurface, O2Context_builtin *context,StrokeVertex v, O2Float strokeWidth, O2LineCap capStyle){
|
||
O2Point ccwt = O2PointApplyAffineTransform(v.ccw,pathToSurface);
|
||
O2Point cwt = O2PointApplyAffineTransform(v.cw,pathToSurface);
|
||
|
||
switch(capStyle){
|
||
|
||
case kO2LineCapButt:
|
||
break;
|
||
|
||
case kO2LineCapRound: {
|
||
const O2Float tessellationAngle = 5.0f;
|
||
|
||
O2Float angle = 180.0f / tessellationAngle;
|
||
|
||
int samples = (int)ceil(angle);
|
||
O2Float step = 1.0f / samples;
|
||
O2Float t = step;
|
||
O2Point u0 = Vector2Normalize(Vector2Subtract(v.ccw,v.p));
|
||
O2Point u1 = Vector2Normalize(Vector2Subtract(v.cw,v.p));
|
||
O2Point prev = ccwt;
|
||
O2DContextAddEdge(context,cwt, ccwt);
|
||
int j;
|
||
|
||
for(j=1;j<samples;j++){
|
||
#if 1
|
||
O2Point vp=O2PointApplyAffineTransform(v.p,pathToSurface);
|
||
O2Point next=O2PointApplyAffineTransformNoTranslate(Vector2MultiplyByFloat(circularLerpWithDirection(u0, u1, t) , strokeWidth * 0.5f),pathToSurface);
|
||
|
||
next=Vector2Add(vp,next);
|
||
|
||
#else
|
||
O2Point next = Vector2Add(v.p , Vector2MultiplyByFloat(circularLerpWithDirection(u0, u1, t) , strokeWidth * 0.5f));
|
||
next = O2PointApplyAffineTransform(next,pathToSurface);
|
||
#endif
|
||
|
||
O2DContextAddEdge(context,prev, next);
|
||
prev = next;
|
||
t += step;
|
||
}
|
||
O2DContextAddEdge(context,prev, cwt);
|
||
break;
|
||
}
|
||
|
||
case kO2LineCapSquare: {
|
||
O2Point t = v.t;
|
||
t=Vector2Normalize(t);
|
||
O2Point vccw=O2PointApplyAffineTransform(v.ccw,pathToSurface);
|
||
O2Point vcw=O2PointApplyAffineTransform(v.cw,pathToSurface);
|
||
O2Point strokeRadius=O2PointApplyAffineTransformNoTranslate(Vector2MultiplyByFloat(t , strokeWidth * 0.5f),pathToSurface);
|
||
|
||
O2Point ccws = Vector2Add(vccw , strokeRadius);
|
||
O2Point cws = Vector2Add(vcw , strokeRadius);
|
||
O2DContextAddEdge(context,cwt, ccwt);
|
||
O2DContextAddEdge(context,ccwt, ccws);
|
||
O2DContextAddEdge(context,ccws, cws);
|
||
O2DContextAddEdge(context,cws, cwt);
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
|
||
// Generate edges for stroke joins. Resulting polygons are closed.
|
||
|
||
void VGPathDoJoin(O2AffineTransform pathToSurface, O2Context_builtin *context, StrokeVertex v0, StrokeVertex v1, O2Float strokeWidth, O2LineJoin joinStyle, O2Float miterLimit){
|
||
O2Point ccw0t = O2PointApplyAffineTransform(v0.ccw,pathToSurface);
|
||
O2Point cw0t = O2PointApplyAffineTransform(v0.cw,pathToSurface);
|
||
O2Point ccw1t = O2PointApplyAffineTransform(v1.ccw,pathToSurface);
|
||
O2Point cw1t = O2PointApplyAffineTransform(v1.cw,pathToSurface);
|
||
O2Point m0t = O2PointApplyAffineTransform(v0.p,pathToSurface);
|
||
O2Point m1t = O2PointApplyAffineTransform(v1.p,pathToSurface);
|
||
|
||
O2Point tccw = Vector2Subtract(v1.ccw,v0.ccw);
|
||
O2Point s, e, m, st, et;
|
||
BOOL cw;
|
||
|
||
if( Vector2Dot(tccw, v0.t) > 0.0f )
|
||
{ //draw ccw miter (draw from point 0 to 1)
|
||
s = ccw0t;
|
||
e = ccw1t;
|
||
st = v0.t;
|
||
et = v1.t;
|
||
m = ccw0t;
|
||
cw = NO;
|
||
O2DContextAddEdge(context,m0t, ccw0t);
|
||
O2DContextAddEdge(context,ccw1t, m1t);
|
||
O2DContextAddEdge(context,m1t, m0t);
|
||
}
|
||
else
|
||
{ //draw cw miter (draw from point 1 to 0)
|
||
s = cw1t;
|
||
e = cw0t;
|
||
st = v1.t;
|
||
et = v0.t;
|
||
m = cw0t;
|
||
cw = YES;
|
||
O2DContextAddEdge(context,cw0t, m0t);
|
||
O2DContextAddEdge(context,m1t, cw1t);
|
||
O2DContextAddEdge(context,m0t, m1t);
|
||
}
|
||
|
||
switch(joinStyle)
|
||
{
|
||
case kO2LineJoinMiter:
|
||
{
|
||
O2Float theta = (O2Float)acos(RI_CLAMP(Vector2Dot(v0.t, Vector2Negate(v1.t)), -1.0f, 1.0f));
|
||
O2Float miterLengthPerStrokeWidth = 1.0f / (O2Float)sin(theta*0.5f);
|
||
if( miterLengthPerStrokeWidth < miterLimit )
|
||
{ //miter
|
||
O2Float l = (O2Float)cos(theta*0.5f) * miterLengthPerStrokeWidth * (strokeWidth * 0.5f);
|
||
l = RI_MIN(l, RI_FLOAT_MAX); //force finite
|
||
|
||
O2Point c = Vector2Add(m , O2PointApplyAffineTransformNoTranslate(Vector2MultiplyByFloat(v0.t, l),pathToSurface));
|
||
|
||
O2DContextAddEdge(context,s, c);
|
||
O2DContextAddEdge(context,c, e);
|
||
break;
|
||
}
|
||
|
||
// bevel
|
||
// dropthrough
|
||
}
|
||
case kO2LineJoinBevel:
|
||
O2DContextAddEdge(context,s, e);
|
||
break;
|
||
|
||
case kO2LineJoinRound:
|
||
{
|
||
const O2Float tessellationAngle = 5.0f;
|
||
|
||
O2Point prev = s;
|
||
O2Float angle = RI_RAD_TO_DEG((O2Float)acos(RI_CLAMP(Vector2Dot(st, et), -1.0f, 1.0f))) / tessellationAngle;
|
||
int samples = (int)ceil(angle);
|
||
if( samples )
|
||
{
|
||
O2Float step = 1.0f / samples;
|
||
O2Float t = step;
|
||
int j;
|
||
for(j=1;j<samples;j++)
|
||
{
|
||
O2Point position = O2PointApplyAffineTransform(Vector2Add(Vector2MultiplyByFloat(v0.p , (1.0f - t)) , Vector2MultiplyByFloat(v1.p , t)),pathToSurface);
|
||
O2Point tangent = circularLerpWithDirection(st, et, t);
|
||
|
||
O2Point strokeRadius=O2PointApplyAffineTransformNoTranslate(Vector2MultiplyByFloat(Vector2Normalize(Vector2Perpendicular(tangent, cw)) , strokeWidth * 0.5f),pathToSurface);
|
||
O2Point next = Vector2Add(position , strokeRadius);
|
||
|
||
O2DContextAddEdge(context,prev, next);
|
||
prev = next;
|
||
t += step;
|
||
}
|
||
}
|
||
O2DContextAddEdge(context,prev, e);
|
||
break;
|
||
}
|
||
|
||
}
|
||
}
|
||
|
||
// Tessellate a path, apply stroking, dashing, caps and joins, and append resulting edges to a context.
|
||
|
||
void VGPathStroke(VGPath *self,O2AffineTransform pathToSurface, O2Context_builtin *context, const O2Float* dashPattern,int dashPatternSize, O2Float dashPhase, BOOL dashPhaseReset, O2Float strokeWidth, O2LineCap capStyle, O2LineJoin joinStyle, O2Float miterLimit){
|
||
RI_ASSERT(strokeWidth >= 0.0f);
|
||
RI_ASSERT(miterLimit >= 1.0f);
|
||
|
||
VGPathTessellateIfNeeded(self);
|
||
|
||
if(!self->_vertexCount)
|
||
return;
|
||
|
||
BOOL dashing = YES;
|
||
|
||
if( dashPatternSize & 1 )
|
||
dashPatternSize--; //odd number of dash pattern entries, discard the last one
|
||
O2Float dashPatternLength = 0.0f;
|
||
int i;
|
||
for(i=0;i<dashPatternSize;i++)
|
||
dashPatternLength += RI_MAX(dashPattern[i], 0.0f);
|
||
if(!dashPatternSize || dashPatternLength == 0.0f )
|
||
dashing = NO;
|
||
dashPatternLength = RI_MIN(dashPatternLength, RI_FLOAT_MAX);
|
||
|
||
//walk along the path
|
||
//stop at the next event which is either:
|
||
//-path vertex
|
||
//-dash stop
|
||
//for robustness, decisions based on geometry are done only once.
|
||
//inDash keeps track whether the last point was in dash or not
|
||
|
||
//loop vertex events
|
||
|
||
O2Float nextDash = 0.0f;
|
||
int d = 0;
|
||
BOOL inDash = YES;
|
||
StrokeVertex v0=StrokeVertexInit(), v1=StrokeVertexInit(), vs=StrokeVertexInit();
|
||
|
||
if(!dashing){
|
||
|
||
for(i=0;i<self->_vertexCount;i++){
|
||
const Vertex v = self->_vertices[i];
|
||
v1.p = v.userPosition;
|
||
v1.t = v.userTangent;
|
||
|
||
v1.ccw = Vector2Add(v1.p , Vector2MultiplyByFloat(Vector2Normalize(Vector2PerpendicularCCW(v1.t)) , strokeWidth * 0.5f));
|
||
v1.cw = Vector2Add(v1.p , Vector2MultiplyByFloat(Vector2Normalize(Vector2PerpendicularCW(v1.t)) , strokeWidth * 0.5f));
|
||
v1.pathLength = v.pathLength;
|
||
v1.flags = v.flags;
|
||
|
||
//process the vertex event
|
||
if(v.flags & START_SEGMENT) {
|
||
if(v.flags & START_SUBPATH)
|
||
vs = v1; //save the subpath start point
|
||
else {
|
||
if( v.flags & IMPLICIT_CLOSE_SUBPATH ) { //do caps for the start and end of the current subpath
|
||
VGPathDoCap(pathToSurface, context, v0, strokeWidth, capStyle); //end cap
|
||
|
||
StrokeVertex vi = vs;
|
||
vi.t = Vector2Negate(vi.t);
|
||
RI_SWAP(&vi.ccw.x, &vi.cw.x);
|
||
RI_SWAP(&vi.ccw.y, &vi.cw.y);
|
||
VGPathDoCap(pathToSurface, context, vi, strokeWidth, capStyle); //start cap
|
||
}
|
||
else { //join two segments
|
||
VGPathDoJoin(pathToSurface, context, v0, v1, strokeWidth, joinStyle, miterLimit);
|
||
}
|
||
}
|
||
}
|
||
else { //in the middle of a segment
|
||
if( !(v.flags & IMPLICIT_CLOSE_SUBPATH) ) { //normal segment, do stroking
|
||
VGPathInterpolateStroke(pathToSurface, context, v0, v1, strokeWidth);
|
||
}
|
||
}
|
||
|
||
if((v.flags & END_SEGMENT) && (v.flags & CLOSE_SUBPATH)) { //join start and end of the current subpath
|
||
VGPathDoJoin(pathToSurface, context, v1, vs, strokeWidth, joinStyle, miterLimit);
|
||
}
|
||
|
||
v0 = v1;
|
||
}
|
||
}
|
||
else {
|
||
|
||
for(i=0;i<self->_vertexCount;i++)
|
||
{
|
||
//read the next vertex
|
||
const Vertex v = self->_vertices[i];
|
||
v1.p = v.userPosition;
|
||
v1.t = v.userTangent;
|
||
RI_ASSERT(!Vector2IsZero(v1.t)); //don't allow zero tangents
|
||
v1.ccw = Vector2Add(v1.p , Vector2MultiplyByFloat(Vector2Normalize(Vector2PerpendicularCCW(v1.t)) , strokeWidth * 0.5f));
|
||
v1.cw = Vector2Add(v1.p , Vector2MultiplyByFloat(Vector2Normalize(Vector2PerpendicularCW(v1.t)) , strokeWidth * 0.5f));
|
||
v1.pathLength = v.pathLength;
|
||
v1.flags = v.flags;
|
||
v1.inDash = dashing ? inDash : YES; //NOTE: for other than START_SEGMENT vertices inDash will be updated after dashing
|
||
|
||
//process the vertex event
|
||
if(v.flags & START_SEGMENT)
|
||
{
|
||
if(v.flags & START_SUBPATH)
|
||
{
|
||
if( dashing )
|
||
{ //initialize dashing by finding which dash or gap the first point of the path lies in
|
||
if(dashPhaseReset || i == 0)
|
||
{
|
||
d = 0;
|
||
inDash = YES;
|
||
nextDash = v1.pathLength - RI_MOD(dashPhase, dashPatternLength);
|
||
for(;;)
|
||
{
|
||
O2Float prevDash = nextDash;
|
||
nextDash = prevDash + RI_MAX(dashPattern[d], 0.0f);
|
||
if(nextDash >= v1.pathLength)
|
||
break;
|
||
|
||
if( d & 1 )
|
||
inDash = YES;
|
||
else
|
||
inDash = NO;
|
||
d = (d+1) % dashPatternSize;
|
||
}
|
||
v1.inDash = inDash;
|
||
//the first point of the path lies between prevDash and nextDash
|
||
//d in the index of the next dash stop
|
||
//inDash is YES if the first point is in a dash
|
||
}
|
||
}
|
||
vs = v1; //save the subpath start point
|
||
}
|
||
else
|
||
{
|
||
if( v.flags & IMPLICIT_CLOSE_SUBPATH )
|
||
{ //do caps for the start and end of the current subpath
|
||
if( v0.inDash )
|
||
VGPathDoCap(pathToSurface, context, v0, strokeWidth, capStyle); //end cap
|
||
if( vs.inDash )
|
||
{
|
||
StrokeVertex vi = vs;
|
||
vi.t = Vector2Negate(vi.t);
|
||
RI_SWAP(&vi.ccw.x, &vi.cw.x);
|
||
RI_SWAP(&vi.ccw.y, &vi.cw.y);
|
||
VGPathDoCap(pathToSurface, context, vi, strokeWidth, capStyle); //start cap
|
||
}
|
||
}
|
||
else
|
||
{ //join two segments
|
||
RI_ASSERT(v0.inDash == v1.inDash);
|
||
if( v0.inDash )
|
||
VGPathDoJoin(pathToSurface, context, v0, v1, strokeWidth, joinStyle, miterLimit);
|
||
}
|
||
}
|
||
}
|
||
else
|
||
{ //in the middle of a segment
|
||
if( !(v.flags & IMPLICIT_CLOSE_SUBPATH) )
|
||
{ //normal segment, do stroking
|
||
if( dashing )
|
||
{
|
||
StrokeVertex prevDashVertex = v0; //dashing of the segment starts from the previous vertex
|
||
|
||
if(nextDash + 10000.0f * dashPatternLength < v1.pathLength)
|
||
NSLog(@"too many dashes");
|
||
|
||
//loop dash events until the next vertex event
|
||
//zero length dashes are handled as a special case since if they hit the vertex,
|
||
//we want to include their starting point to this segment already in order to generate a join
|
||
int numDashStops = 0;
|
||
while(nextDash < v1.pathLength || (nextDash <= v1.pathLength && dashPattern[(d+1) % dashPatternSize] == 0.0f))
|
||
{
|
||
O2Float edgeLength = v1.pathLength - v0.pathLength;
|
||
O2Float ratio = 0.0f;
|
||
if(edgeLength > 0.0f)
|
||
ratio = (nextDash - v0.pathLength) / edgeLength;
|
||
StrokeVertex nextDashVertex=StrokeVertexInit();
|
||
nextDashVertex.p = Vector2Add(Vector2MultiplyByFloat(v0.p , (1.0f - ratio)) , Vector2MultiplyByFloat(v1.p , ratio));
|
||
nextDashVertex.t = circularLerp(v0.t, v1.t, ratio);
|
||
nextDashVertex.ccw = Vector2Add(nextDashVertex.p , Vector2MultiplyByFloat(Vector2Normalize(Vector2PerpendicularCCW(nextDashVertex.t)) , strokeWidth * 0.5f));
|
||
nextDashVertex.cw = Vector2Add(nextDashVertex.p , Vector2MultiplyByFloat(Vector2Normalize(Vector2PerpendicularCW(nextDashVertex.t)) , strokeWidth * 0.5f));
|
||
|
||
if( inDash )
|
||
{ //stroke from prevDashVertex -> nextDashVertex
|
||
if( numDashStops )
|
||
{ //prevDashVertex is not the start vertex of the segment, cap it (start vertex has already been joined or capped)
|
||
StrokeVertex vi = prevDashVertex;
|
||
vi.t = Vector2Negate(vi.t);
|
||
RI_SWAP(&vi.ccw.x, &vi.cw.x);
|
||
RI_SWAP(&vi.ccw.y, &vi.cw.y);
|
||
VGPathDoCap(pathToSurface, context, vi, strokeWidth, capStyle);
|
||
}
|
||
VGPathInterpolateStroke(pathToSurface, context, prevDashVertex, nextDashVertex, strokeWidth);
|
||
VGPathDoCap(pathToSurface, context, nextDashVertex, strokeWidth, capStyle); //end cap
|
||
}
|
||
prevDashVertex = nextDashVertex;
|
||
|
||
if( d & 1 )
|
||
{ //dash starts
|
||
RI_ASSERT(!inDash);
|
||
inDash = YES;
|
||
}
|
||
else
|
||
{ //dash ends
|
||
RI_ASSERT(inDash);
|
||
inDash = NO;
|
||
}
|
||
d = (d+1) % dashPatternSize;
|
||
nextDash += RI_MAX(dashPattern[d], 0.0f);
|
||
numDashStops++;
|
||
}
|
||
|
||
if( inDash )
|
||
{ //stroke prevDashVertex -> v1
|
||
if( numDashStops )
|
||
{ //prevDashVertex is not the start vertex of the segment, cap it (start vertex has already been joined or capped)
|
||
StrokeVertex vi = prevDashVertex;
|
||
vi.t = Vector2Negate(vi.t);
|
||
RI_SWAP(&vi.ccw.x, &vi.cw.x);
|
||
RI_SWAP(&vi.ccw.y, &vi.cw.y);
|
||
VGPathDoCap(pathToSurface, context, vi, strokeWidth, capStyle);
|
||
}
|
||
VGPathInterpolateStroke(pathToSurface, context, prevDashVertex, v1, strokeWidth);
|
||
//no cap, leave path open
|
||
}
|
||
|
||
v1.inDash = inDash; //update inDash status of the segment end point
|
||
}
|
||
else //no dashing, just interpolate segment end points
|
||
VGPathInterpolateStroke(pathToSurface, context, v0, v1, strokeWidth);
|
||
}
|
||
}
|
||
|
||
if((v.flags & END_SEGMENT) && (v.flags & CLOSE_SUBPATH))
|
||
{ //join start and end of the current subpath
|
||
if( v1.inDash && vs.inDash )
|
||
VGPathDoJoin(pathToSurface, context, v1, vs, strokeWidth, joinStyle, miterLimit);
|
||
else
|
||
{ //both start and end are not in dash, cap them
|
||
if( v1.inDash )
|
||
VGPathDoCap(pathToSurface, context, v1, strokeWidth, capStyle); //end cap
|
||
if( vs.inDash )
|
||
{
|
||
StrokeVertex vi = vs;
|
||
vi.t = Vector2Negate(vi.t);
|
||
RI_SWAP(&vi.ccw.x, &vi.cw.x);
|
||
RI_SWAP(&vi.ccw.y, &vi.cw.y);
|
||
VGPathDoCap(pathToSurface, context, vi, strokeWidth, capStyle); //start cap
|
||
}
|
||
}
|
||
}
|
||
|
||
v0 = v1;
|
||
}
|
||
}
|
||
}
|
||
|
||
// Tessellates a path, and returns a position and a tangent on the path given a distance along the path.
|
||
|
||
void VGPathGetPointAlong(VGPath *self,int startIndex, int numSegments, O2Float distance, O2Point *p, O2Point *t){
|
||
RI_ASSERT(startIndex >= 0 && startIndex + numSegments <= self->_numberOfElements && numSegments > 0);
|
||
|
||
VGPathTessellateIfNeeded(self);
|
||
|
||
RI_ASSERT(startIndex >= 0 && startIndex < self->_numberOfElements);
|
||
RI_ASSERT(startIndex + numSegments >= 0 && startIndex + numSegments <= self->_numberOfElements);
|
||
|
||
int startVertex = self->_segmentToVertex[startIndex].start;
|
||
int endVertex = self->_segmentToVertex[startIndex + numSegments - 1].end;
|
||
|
||
if(!self->_vertexCount || (startVertex == -1 && endVertex == -1))
|
||
{ // no vertices in the tessellated path. The path is empty or consists only of zero-length segments.
|
||
*p=O2PointMake(0,0);
|
||
*t=O2PointMake(1,0);
|
||
return;
|
||
}
|
||
if(startVertex == -1)
|
||
startVertex = 0;
|
||
|
||
RI_ASSERT(startVertex >= 0 && startVertex < self->_vertexCount);
|
||
RI_ASSERT(endVertex >= 0 && endVertex < self->_vertexCount);
|
||
|
||
distance += self->_vertices[startVertex].pathLength; //map distance to the range of the whole path
|
||
|
||
if(distance <= self->_vertices[startVertex].pathLength)
|
||
{ //return the first point of the path
|
||
*p = self->_vertices[startVertex].userPosition;
|
||
*t = self->_vertices[startVertex].userTangent;
|
||
return;
|
||
}
|
||
|
||
if(distance >= self->_vertices[endVertex].pathLength)
|
||
{ //return the last point of the path
|
||
*p = self->_vertices[endVertex].userPosition;
|
||
*t = self->_vertices[endVertex].userTangent;
|
||
return;
|
||
}
|
||
|
||
//search for the segment containing the distance
|
||
int s;
|
||
for(s=startIndex;s<startIndex+numSegments;s++)
|
||
{
|
||
int start = self->_segmentToVertex[s].start;
|
||
int end = self->_segmentToVertex[s].end;
|
||
if(start < 0)
|
||
start = 0;
|
||
if(end < 0)
|
||
end = 0;
|
||
RI_ASSERT(start >= 0 && start < self->_vertexCount);
|
||
RI_ASSERT(end >= 0 && end < self->_vertexCount);
|
||
|
||
if(distance >= self->_vertices[start].pathLength && distance < self->_vertices[end].pathLength)
|
||
{ //segment contains the queried distance
|
||
int i;
|
||
for(i=start;i<end;i++)
|
||
{
|
||
Vertex v0 = self->_vertices[i];
|
||
Vertex v1 = self->_vertices[i+1];
|
||
if(distance >= v0.pathLength && distance < v1.pathLength)
|
||
{ //segment found, interpolate linearly between its end points
|
||
O2Float edgeLength = v1.pathLength - v0.pathLength;
|
||
RI_ASSERT(edgeLength > 0.0f);
|
||
O2Float r = (distance - v0.pathLength) / edgeLength;
|
||
*p = Vector2Add(Vector2MultiplyByFloat(v0.userPosition , (1.0f - r)) , Vector2MultiplyByFloat(v1.userPosition , r));
|
||
*t = Vector2Add(Vector2MultiplyByFloat(v0.userTangent,(1.0f - r)) , Vector2MultiplyByFloat(v1.userTangent,r));
|
||
return;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
RI_ASSERT(0); //point not found (should never get here)
|
||
}
|
||
|
||
// Tessellates a path, and computes its length.
|
||
|
||
O2Float VGPathGetLength(VGPath *self,int startIndex, int numSegments){
|
||
RI_ASSERT(startIndex >= 0 && startIndex + numSegments <= self->_numberOfElements && numSegments > 0);
|
||
|
||
VGPathTessellateIfNeeded(self);
|
||
|
||
RI_ASSERT(startIndex >= 0 && startIndex < self->_numberOfElements);
|
||
RI_ASSERT(startIndex + numSegments >= 0 && startIndex + numSegments <= self->_numberOfElements);
|
||
|
||
int startVertex = self->_segmentToVertex[startIndex].start;
|
||
int endVertex = self->_segmentToVertex[startIndex + numSegments - 1].end;
|
||
|
||
if(!self->_vertexCount)
|
||
return 0.0f;
|
||
|
||
O2Float startPathLength = 0.0f;
|
||
if(startVertex >= 0)
|
||
{
|
||
RI_ASSERT(startVertex >= 0 && startVertex < self->_vertexCount);
|
||
startPathLength = self->_vertices[startVertex].pathLength;
|
||
}
|
||
O2Float endPathLength = 0.0f;
|
||
if(endVertex >= 0)
|
||
{
|
||
RI_ASSERT(endVertex >= 0 && endVertex < self->_vertexCount);
|
||
endPathLength = self->_vertices[endVertex].pathLength;
|
||
}
|
||
|
||
return endPathLength - startPathLength;
|
||
}
|
||
|
||
// Tessellates a path, and computes its bounding box in user space.
|
||
|
||
void VGPathGetPathBounds(VGPath *self,O2Float *minx, O2Float *miny, O2Float *maxx, O2Float *maxy){
|
||
VGPathTessellateIfNeeded(self);
|
||
|
||
if(self->_vertexCount)
|
||
{
|
||
*minx = self->m_userMinx;
|
||
*miny = self->m_userMiny;
|
||
*maxx = self->m_userMaxx;
|
||
*maxy = self->m_userMaxy;
|
||
}
|
||
else
|
||
{
|
||
*minx = *miny = 0;
|
||
*maxx = *maxy = -1;
|
||
}
|
||
}
|
||
|
||
// Tessellates a path, and computes its bounding box in surface space.
|
||
|
||
void VGPathGetPathTransformedBounds(VGPath *self,O2AffineTransform pathToSurface, O2Float *minx, O2Float *miny, O2Float *maxx, O2Float *maxy){
|
||
|
||
VGPathTessellateIfNeeded(self);
|
||
|
||
if(self->_vertexCount==0) {
|
||
*minx = *miny = 0;
|
||
*maxx = *maxy = -1;
|
||
}
|
||
else {
|
||
O2Point p0=O2PointMake(self->m_userMinx, self->m_userMiny);
|
||
O2Point p1=O2PointMake(self->m_userMinx, self->m_userMaxy);
|
||
O2Point p2=O2PointMake(self->m_userMaxx, self->m_userMaxy);
|
||
O2Point p3=O2PointMake(self->m_userMaxx, self->m_userMiny);
|
||
p0 = O2PointApplyAffineTransform(p0,pathToSurface);
|
||
p1 = O2PointApplyAffineTransform(p1,pathToSurface);
|
||
p2 = O2PointApplyAffineTransform(p2,pathToSurface);
|
||
p3 = O2PointApplyAffineTransform(p3,pathToSurface);
|
||
|
||
*minx = RI_MIN(RI_MIN(RI_MIN(p0.x, p1.x), p2.x), p3.x);
|
||
*miny = RI_MIN(RI_MIN(RI_MIN(p0.y, p1.y), p2.y), p3.y);
|
||
*maxx = RI_MAX(RI_MAX(RI_MAX(p0.x, p1.x), p2.x), p3.x);
|
||
*maxy = RI_MAX(RI_MAX(RI_MAX(p0.y, p1.y), p2.y), p3.y);
|
||
}
|
||
}
|
||
|
||
// Adds a vertex to a tessellated path.
|
||
|
||
void VGPathAddVertex(VGPath *self,O2Point p, O2Point t, O2Float pathLength, unsigned int flags){
|
||
RI_ASSERT(!Vector2IsZero(t));
|
||
|
||
Vertex v;
|
||
v.pathLength = pathLength;
|
||
v.userPosition = p;
|
||
|
||
v.userTangent = t;
|
||
v.flags = flags;
|
||
|
||
if(self->_vertexCount+1>self->_vertexCapacity){
|
||
self->_vertexCapacity*=2;
|
||
self->_vertices=(Vertex *)NSZoneRealloc(NULL,self->_vertices,self->_vertexCapacity*sizeof(Vertex));
|
||
}
|
||
self->_vertices[self->_vertexCount++]=v;
|
||
|
||
self->m_userMinx = RI_MIN(self->m_userMinx, v.userPosition.x);
|
||
self->m_userMiny = RI_MIN(self->m_userMiny, v.userPosition.y);
|
||
self->m_userMaxx = RI_MAX(self->m_userMaxx, v.userPosition.x);
|
||
self->m_userMaxy = RI_MAX(self->m_userMaxy, v.userPosition.y);
|
||
}
|
||
|
||
// Adds an edge to a tessellated path.
|
||
|
||
void VGPathAddEdge(VGPath *self,O2Point p0, O2Point p1, O2Point t0, O2Point t1, unsigned int startFlags, unsigned int endFlags){
|
||
O2Float pathLength = 0.0f;
|
||
|
||
RI_ASSERT(!Vector2IsZero(t0) && !Vector2IsZero(t1));
|
||
|
||
//segment midpoints are shared between edges
|
||
if( startFlags & START_SEGMENT )
|
||
{
|
||
if(self->_vertexCount > 0)
|
||
pathLength = self->_vertices[self->_vertexCount-1].pathLength;
|
||
|
||
VGPathAddVertex(self,p0, t0, pathLength, startFlags);
|
||
}
|
||
|
||
//other than implicit close paths (caused by a MOVE_TO) add to path length
|
||
if( !(endFlags & IMPLICIT_CLOSE_SUBPATH) )
|
||
{
|
||
//NOTE: with extremely large coordinates the floating point path length is infinite
|
||
O2Float l = Vector2Length(Vector2Subtract(p1,p0));
|
||
pathLength = self->_vertices[self->_vertexCount-1].pathLength + l;
|
||
pathLength = RI_MIN(pathLength, RI_FLOAT_MAX);
|
||
}
|
||
|
||
VGPathAddVertex(self,p1, t1, pathLength, endFlags);
|
||
}
|
||
|
||
// Tessellates a close-path segment.
|
||
|
||
void VGPathAddEndPath(VGPath *self,O2Point p0, O2Point p1, BOOL subpathHasGeometry, unsigned int flags){
|
||
if(!subpathHasGeometry)
|
||
{ //single vertex
|
||
O2Point t=O2PointMake(1.0f,0.0f);
|
||
VGPathAddEdge(self,p0, p1, t, t, START_SEGMENT | START_SUBPATH, END_SEGMENT | END_SUBPATH);
|
||
VGPathAddEdge(self,p0, p1, Vector2Negate(t), Vector2Negate(t), IMPLICIT_CLOSE_SUBPATH | START_SEGMENT, IMPLICIT_CLOSE_SUBPATH | END_SEGMENT);
|
||
return;
|
||
}
|
||
//the subpath contains segment commands that have generated geometry
|
||
|
||
//add a close path segment to the start point of the subpath
|
||
RI_ASSERT(self->_vertexCount > 0);
|
||
self->_vertices[self->_vertexCount-1].flags |= END_SUBPATH;
|
||
|
||
O2Point t = Vector2Normalize(Vector2Subtract(p1,p0));
|
||
if(Vector2IsZero(t))
|
||
t = self->_vertices[self->_vertexCount-1].userTangent; //if the segment is zero-length, use the tangent of the last segment end point so that proper join will be generated
|
||
RI_ASSERT(!Vector2IsZero(t));
|
||
|
||
VGPathAddEdge(self,p0, p1, t, t, flags | START_SEGMENT, flags | END_SEGMENT);
|
||
}
|
||
|
||
// Tessellates a line-to segment.
|
||
|
||
BOOL VGPathAddLineTo(VGPath *self,O2Point p0, O2Point p1, BOOL subpathHasGeometry){
|
||
if(Vector2IsEqual(p0 ,p1))
|
||
return NO; //discard zero-length segments
|
||
|
||
//compute end point tangents
|
||
O2Point t = Vector2Normalize(Vector2Subtract(p1,p0));
|
||
RI_ASSERT(!Vector2IsZero(t));
|
||
|
||
unsigned int startFlags = START_SEGMENT;
|
||
if(!subpathHasGeometry)
|
||
startFlags |= START_SUBPATH;
|
||
VGPathAddEdge(self,p0, p1, t, t, startFlags, END_SEGMENT);
|
||
return YES;
|
||
}
|
||
|
||
// Tessellates a quad-to segment.
|
||
|
||
/*
|
||
Given a quadratic B<>zier curve with control points (x0, y0), (x1, y1), and (x2, y2), an identical cubic B<>zier curve may be formed using the control points (x0, y0), (x0 + 2*x1, y0 + 2*y1)/3, (x2 + 2*x1, y2 + 2*y1)/3, (x2, y2)
|
||
*/
|
||
|
||
BOOL VGPathAddQuadTo(VGPath *self,O2Point p0, O2Point p1, O2Point p2, BOOL subpathHasGeometry){
|
||
if(Vector2IsEqual(p0,p1) && Vector2IsEqual(p0,p2))
|
||
{
|
||
RI_ASSERT(Vector2IsEqual(p1,p2));
|
||
return NO; //discard zero-length segments
|
||
}
|
||
|
||
//compute end point tangents
|
||
|
||
O2Point incomingTangent = Vector2Normalize(Vector2Subtract(p1,p0));
|
||
O2Point outgoingTangent = Vector2Normalize(Vector2Subtract(p2,p1));
|
||
if(Vector2IsEqual(p0,p1))
|
||
incomingTangent = Vector2Normalize(Vector2Subtract(p2,p0));
|
||
if(Vector2IsEqual(p1,p2))
|
||
outgoingTangent = Vector2Normalize(Vector2Subtract(p2 ,p0));
|
||
RI_ASSERT(!Vector2IsZero(incomingTangent) && !Vector2IsZero(outgoingTangent));
|
||
|
||
unsigned int startFlags = START_SEGMENT;
|
||
if(!subpathHasGeometry)
|
||
startFlags |= START_SUBPATH;
|
||
|
||
const int segments = 256;
|
||
O2Point pp = p0;
|
||
O2Point tp = incomingTangent;
|
||
unsigned int prevFlags = startFlags;
|
||
int i;
|
||
for(i=1;i<segments;i++)
|
||
{
|
||
O2Float t = (O2Float)i / (O2Float)segments;
|
||
O2Float u = 1.0f-t;
|
||
O2Point pn = Vector2Add(Vector2Add(Vector2MultiplyByFloat(p0,u*u) , Vector2MultiplyByFloat(p1,2.0f*t*u)),Vector2MultiplyByFloat(p2,t*t));
|
||
O2Point tn = Vector2Add(Vector2Add(Vector2MultiplyByFloat(p0,(-1.0f+t)), Vector2MultiplyByFloat(p1,(1.0f-2.0f*t))),Vector2MultiplyByFloat(p2,t));
|
||
tn = Vector2Normalize(tn);
|
||
if(Vector2IsZero(tn))
|
||
tn = tp;
|
||
|
||
VGPathAddEdge(self,pp, pn, tp, tn, prevFlags, 0);
|
||
|
||
pp = pn;
|
||
tp = tn;
|
||
prevFlags = 0;
|
||
}
|
||
VGPathAddEdge(self,pp, p2, tp, outgoingTangent, prevFlags, END_SEGMENT);
|
||
return YES;
|
||
}
|
||
|
||
// Tessellates a cubic-to segment.
|
||
#if 0
|
||
// Bezier to lines from: Windows Graphics Programming by Feng Yuan
|
||
static void bezier(VGPath *self,double x1,double y1,double x2, double y2,double x3,double y3,double x4,double y4,unsigned *prevFlags,O2Point *pp,O2Point *tp){
|
||
// Ax+By+C=0 is the line (x1,y1) (x4,y4);
|
||
double A=y4-y1;
|
||
double B=x1-x4;
|
||
double C=y1*(x4-x1)-x1*(y4-y1);
|
||
double AB=A*A+B*B;
|
||
|
||
if((A*x2+B*y2+C)*(A*x2+B*y2+C)<AB && (A*x3+B*y3+C)*(A*x3+B*y3+C)<AB){
|
||
O2Point v0=O2PointMake(x1,y1);
|
||
O2Point v1=O2PointMake(x4,y4);
|
||
O2Point t = Vector2Normalize(Vector2Subtract(v1,v0));
|
||
|
||
VGPathAddEdge(self,v0, v1, *tp, t, *prevFlags, 0);
|
||
*prevFlags=0;
|
||
*pp=v1;
|
||
*tp=t;
|
||
return;
|
||
}
|
||
else {
|
||
double x12=x1+x2;
|
||
double y12=y1+y2;
|
||
double x23=x2+x3;
|
||
double y23=y2+y3;
|
||
double x34=x3+x4;
|
||
double y34=y3+y4;
|
||
double x1223=x12+x23;
|
||
double y1223=y12+y23;
|
||
double x2334=x23+x34;
|
||
double y2334=y23+y34;
|
||
double x=x1223+x2334;
|
||
double y=y1223+y2334;
|
||
|
||
bezier(self,x1,y1,x12/2,y12/2,x1223/4,y1223/4,x/8,y/8,prevFlags,pp,tp);
|
||
bezier(self,x/8,y/8,x2334/4,y2334/4,x34/2,y34/2,x4,y4,prevFlags,pp,tp);
|
||
}
|
||
}
|
||
#endif
|
||
|
||
BOOL VGPathAddCubicTo(VGPath *self,O2Point p0, O2Point p1, O2Point p2, O2Point p3, BOOL subpathHasGeometry){
|
||
if(Vector2IsEqual(p0,p1) && Vector2IsEqual(p0,p2) && Vector2IsEqual(p0 ,p3))
|
||
{
|
||
RI_ASSERT(Vector2IsEqual(p1 , p2) && Vector2IsEqual(p1 , p3) && Vector2IsEqual(p2 , p3));
|
||
return NO; //discard zero-length segments
|
||
}
|
||
|
||
//compute end point tangents
|
||
O2Point incomingTangent = Vector2Normalize(Vector2Subtract(p1, p0));
|
||
O2Point outgoingTangent = Vector2Normalize(Vector2Subtract(p3, p2));
|
||
if(Vector2IsEqual(p0 , p1))
|
||
{
|
||
incomingTangent = Vector2Normalize(Vector2Subtract(p2 ,p0));
|
||
if(Vector2IsEqual(p1, p2))
|
||
incomingTangent = Vector2Normalize(Vector2Subtract(p3,p0));
|
||
}
|
||
if(Vector2IsEqual(p2, p3))
|
||
{
|
||
outgoingTangent = Vector2Normalize(Vector2Subtract(p3 ,p1));
|
||
if(Vector2IsEqual(p1, p2))
|
||
outgoingTangent = Vector2Normalize(Vector2Subtract(p3,p0));
|
||
}
|
||
RI_ASSERT(!Vector2IsZero(incomingTangent) && !Vector2IsZero(outgoingTangent));
|
||
|
||
unsigned int startFlags = START_SEGMENT;
|
||
if(!subpathHasGeometry)
|
||
startFlags |= START_SUBPATH;
|
||
|
||
#if 0
|
||
// This is the proper idea, subdivide until flatness tolerance is acheived
|
||
// However, it does not compensate for the CTM/device matrix, so it can produce grossly flat curves
|
||
// Also CRASHES under circumstances when a coordinate is NaN (view had NaN y)
|
||
unsigned int prevFlags = startFlags;
|
||
O2Point pp = p0;
|
||
O2Point tp = incomingTangent;
|
||
bezier(self,p0.x,p0.y,p1.x,p1.y,p2.x,p2.y,p3.x,p3.y,&prevFlags,&pp,&tp);
|
||
VGPathAddEdge(self,pp, p3, tp, outgoingTangent, prevFlags, END_SEGMENT);
|
||
#else
|
||
const int segments = 256;
|
||
O2Point pp = p0;
|
||
O2Point tp = incomingTangent;
|
||
unsigned int prevFlags = startFlags;
|
||
int i;
|
||
for(i=1;i<segments;i++)
|
||
{
|
||
O2Float t = (O2Float)i / (O2Float)segments;
|
||
O2Float u = 1.0f-t;
|
||
O2Point pn = Vector2Add(Vector2Add(Vector2Add(Vector2MultiplyByFloat(p0,u*u*u), Vector2MultiplyByFloat(p1,3.0f*t*u*u)) ,Vector2MultiplyByFloat(p2,3.0f*t*t*u)),Vector2MultiplyByFloat(p3,t*t*t));
|
||
O2Point tn = Vector2Add(Vector2Add(Vector2Add(Vector2MultiplyByFloat(p0,(-1.0f + 2.0f*t - t*t)) , Vector2MultiplyByFloat(p1,(1.0f - 4.0f*t + 3.0f*t*t))) , Vector2MultiplyByFloat(p2,(2.0f*t - 3.0f*t*t) )) ,Vector2MultiplyByFloat(p3,t*t));
|
||
tn = Vector2Normalize(tn);
|
||
if(Vector2IsZero(tn))
|
||
tn = tp;
|
||
|
||
VGPathAddEdge(self,pp, pn, tp, tn, prevFlags, 0);
|
||
|
||
pp = pn;
|
||
tp = tn;
|
||
prevFlags = 0;
|
||
}
|
||
VGPathAddEdge(self,pp, p3, tp, outgoingTangent, prevFlags, END_SEGMENT);
|
||
#endif
|
||
return YES;
|
||
}
|
||
|
||
// Tessellates a path.
|
||
|
||
/* tessellation output format: A list of vertices describing the
|
||
path tessellated into line segments and relevant aspects of the
|
||
input data. Each path segment has a start vertex, a number of
|
||
internal vertices (possibly zero), and an end vertex. The start
|
||
and end of segments and subpaths have been flagged, as well as
|
||
implicit and explicit close subpath segments. */
|
||
|
||
void VGPathTessellateIfNeeded(VGPath *self){
|
||
if( self->_vertexCount > 0 )
|
||
return; //already tessellated
|
||
|
||
self->m_userMinx = RI_FLOAT_MAX;
|
||
self->m_userMiny = RI_FLOAT_MAX;
|
||
self->m_userMaxx = -RI_FLOAT_MAX;
|
||
self->m_userMaxy = -RI_FLOAT_MAX;
|
||
|
||
{
|
||
unsigned numberOfElements=O2PathNumberOfElements(self->_path);
|
||
if(self->_segmentToVertexCapacity<numberOfElements){
|
||
self->_segmentToVertexCapacity=numberOfElements;
|
||
self->_segmentToVertex=NSZoneRealloc(NULL,self->_segmentToVertex,self->_segmentToVertexCapacity*sizeof(VertexIndex));
|
||
}
|
||
|
||
int coordIndex = 0;
|
||
O2Point s=O2PointMake(0,0); //the beginning of the current subpath
|
||
O2Point o=O2PointMake(0,0); //the last point of the previous segment
|
||
O2Point p=O2PointMake(0,0); //the last internal control point of the previous segment, if the segment was a (regular or smooth) quadratic or cubic Bezier, or else the last point of the previous segment
|
||
|
||
//tessellate the path segments
|
||
coordIndex = 0;
|
||
s=O2PointMake(0,0);
|
||
o=O2PointMake(0,0);
|
||
p=O2PointMake(0,0);
|
||
BOOL subpathHasGeometry = NO;
|
||
O2PathElementType prevSegment = kO2PathElementMoveToPoint;
|
||
int i;
|
||
const unsigned char *elements=O2PathElements(self->_path);
|
||
const O2Point *points=O2PathPoints(self->_path);
|
||
|
||
for(i=0;i<numberOfElements;i++)
|
||
{
|
||
O2PathElementType segment = elements[i];
|
||
int coords = O2PathElementTypeToNumCoordinates(segment);
|
||
self->_segmentToVertex[i].start = self->_vertexCount;
|
||
|
||
switch(segment)
|
||
{
|
||
case kO2PathElementCloseSubpath:
|
||
{
|
||
RI_ASSERT(coords == 0);
|
||
VGPathAddEndPath(self,o, s, subpathHasGeometry, CLOSE_SUBPATH);
|
||
p = s;
|
||
o = s;
|
||
subpathHasGeometry = NO;
|
||
break;
|
||
}
|
||
|
||
case kO2PathElementMoveToPoint:
|
||
{
|
||
RI_ASSERT(coords == 1);
|
||
O2Point c=points[coordIndex];
|
||
if(prevSegment != kO2PathElementMoveToPoint && prevSegment != kO2PathElementCloseSubpath)
|
||
VGPathAddEndPath(self,o, s, subpathHasGeometry, IMPLICIT_CLOSE_SUBPATH);
|
||
s = c;
|
||
p = c;
|
||
o = c;
|
||
subpathHasGeometry = NO;
|
||
break;
|
||
}
|
||
|
||
case kO2PathElementAddLineToPoint:
|
||
{
|
||
RI_ASSERT(coords == 1);
|
||
O2Point c=points[coordIndex];
|
||
if(VGPathAddLineTo(self,o, c, subpathHasGeometry))
|
||
subpathHasGeometry = YES;
|
||
p = c;
|
||
o = c;
|
||
break;
|
||
}
|
||
|
||
case kO2PathElementAddQuadCurveToPoint:
|
||
{
|
||
RI_ASSERT(coords == 2);
|
||
O2Point c0=points[coordIndex];
|
||
O2Point c1=points[coordIndex+1];
|
||
if(VGPathAddQuadTo(self,o, c0, c1, subpathHasGeometry))
|
||
subpathHasGeometry = YES;
|
||
p = c0;
|
||
o = c1;
|
||
break;
|
||
}
|
||
|
||
case kO2PathElementAddCurveToPoint:
|
||
{
|
||
RI_ASSERT(coords == 3);
|
||
O2Point c0=points[coordIndex+0];
|
||
O2Point c1=points[coordIndex+1];
|
||
O2Point c2=points[coordIndex+2];
|
||
if(VGPathAddCubicTo(self,o, c0, c1, c2, subpathHasGeometry))
|
||
subpathHasGeometry = YES;
|
||
p = c1;
|
||
o = c2;
|
||
break;
|
||
}
|
||
|
||
}
|
||
|
||
if(self->_vertexCount > self->_segmentToVertex[i].start)
|
||
{ //segment produced vertices
|
||
self->_segmentToVertex[i].end = self->_vertexCount - 1;
|
||
}
|
||
else
|
||
{ //segment didn't produce vertices (zero-length segment). Ignore it.
|
||
self->_segmentToVertex[i].start = self->_segmentToVertex[i].end = self->_vertexCount-1;
|
||
}
|
||
prevSegment = segment;
|
||
coordIndex += coords;
|
||
}
|
||
|
||
//add an implicit MOVE_TO to the end to close the last subpath.
|
||
//if the subpath contained only zero-length segments, this produces the necessary geometry to get it stroked
|
||
// and included in path bounds. The geometry won't be included in the pointAlongPath query.
|
||
if(prevSegment != kO2PathElementMoveToPoint && prevSegment != kO2PathElementCloseSubpath)
|
||
VGPathAddEndPath(self,o, s, subpathHasGeometry, IMPLICIT_CLOSE_SUBPATH);
|
||
|
||
#if 0 // DEBUG
|
||
//check that the flags are correct
|
||
int prev = -1;
|
||
BOOL subpathStarted = NO;
|
||
BOOL segmentStarted = NO;
|
||
for(int i=0;i<self->_vertexCount;i++)
|
||
{
|
||
Vertex v = self->_vertices[i];
|
||
|
||
if(v.flags & START_SUBPATH)
|
||
{
|
||
RI_ASSERT(!subpathStarted);
|
||
RI_ASSERT(v.flags & START_SEGMENT);
|
||
RI_ASSERT(!(v.flags & END_SUBPATH));
|
||
RI_ASSERT(!(v.flags & END_SEGMENT));
|
||
RI_ASSERT(!(v.flags & CLOSE_SUBPATH));
|
||
RI_ASSERT(!(v.flags & IMPLICIT_CLOSE_SUBPATH));
|
||
subpathStarted = YES;
|
||
}
|
||
|
||
if(v.flags & START_SEGMENT)
|
||
{
|
||
RI_ASSERT(subpathStarted || (v.flags & CLOSE_SUBPATH) || (v.flags & IMPLICIT_CLOSE_SUBPATH));
|
||
RI_ASSERT(!segmentStarted);
|
||
RI_ASSERT(!(v.flags & END_SUBPATH));
|
||
RI_ASSERT(!(v.flags & END_SEGMENT));
|
||
segmentStarted = YES;
|
||
}
|
||
|
||
if( v.flags & CLOSE_SUBPATH )
|
||
{
|
||
RI_ASSERT(segmentStarted);
|
||
RI_ASSERT(!subpathStarted);
|
||
RI_ASSERT((v.flags & START_SEGMENT) || (v.flags & END_SEGMENT));
|
||
RI_ASSERT(!(v.flags & IMPLICIT_CLOSE_SUBPATH));
|
||
RI_ASSERT(!(v.flags & START_SUBPATH));
|
||
RI_ASSERT(!(v.flags & END_SUBPATH));
|
||
}
|
||
if( v.flags & IMPLICIT_CLOSE_SUBPATH )
|
||
{
|
||
RI_ASSERT(segmentStarted);
|
||
RI_ASSERT(!subpathStarted);
|
||
RI_ASSERT((v.flags & START_SEGMENT) || (v.flags & END_SEGMENT));
|
||
RI_ASSERT(!(v.flags & CLOSE_SUBPATH));
|
||
RI_ASSERT(!(v.flags & START_SUBPATH));
|
||
RI_ASSERT(!(v.flags & END_SUBPATH));
|
||
}
|
||
|
||
if( prev >= 0 )
|
||
{
|
||
RI_ASSERT(segmentStarted);
|
||
RI_ASSERT(subpathStarted || ((self->_vertices[prev].flags & CLOSE_SUBPATH) && (self->_vertices[i].flags & CLOSE_SUBPATH)) ||
|
||
((self->_vertices[prev].flags & IMPLICIT_CLOSE_SUBPATH) && (self->_vertices[i].flags & IMPLICIT_CLOSE_SUBPATH)));
|
||
}
|
||
|
||
prev = i;
|
||
if(v.flags & END_SEGMENT)
|
||
{
|
||
RI_ASSERT(subpathStarted || (v.flags & CLOSE_SUBPATH) || (v.flags & IMPLICIT_CLOSE_SUBPATH));
|
||
RI_ASSERT(segmentStarted);
|
||
RI_ASSERT(!(v.flags & START_SUBPATH));
|
||
RI_ASSERT(!(v.flags & START_SEGMENT));
|
||
segmentStarted = NO;
|
||
prev = -1;
|
||
}
|
||
|
||
if(v.flags & END_SUBPATH)
|
||
{
|
||
RI_ASSERT(subpathStarted);
|
||
RI_ASSERT(v.flags & END_SEGMENT);
|
||
RI_ASSERT(!(v.flags & START_SUBPATH));
|
||
RI_ASSERT(!(v.flags & START_SEGMENT));
|
||
RI_ASSERT(!(v.flags & CLOSE_SUBPATH));
|
||
RI_ASSERT(!(v.flags & IMPLICIT_CLOSE_SUBPATH));
|
||
subpathStarted = NO;
|
||
}
|
||
}
|
||
#endif //RI_DEBUG
|
||
}
|
||
|
||
}
|
||
|
||
@end
|