Ok, Here's the meat of it. I don't think this is as sophisticated as it could be, but it works pretty well for what I need right now. It only does left-to-right text and I haven't tried it with mixed line- heights (though I see no reason it wouldn't work, within the limitations others have discovered with NSLayoutManager). Also, this makes no attempt to take the actual text content into account, relying entirely on default word breaking, etc. - as a result it can force word breaks in unusual places depending on the shape.

So, those caveats aside, here's the code. It relies on a category of NSBezierPath to do the actual line splitting, which invokes the utility method Intersection2() to determine the intersection of the line with the bezier itself, and a subclass of NSTextContainer which is now very simple indeed...

// utility method:

NSPoint Intersection2( const NSPoint p1, const NSPoint p2, const NSPoint p3, const NSPoint p4 )
{
// return the intersecting point of two lines SEGMENTS p1-p2 and p3- p4, whose end points are given. If the segments are parallel or non- intersecting, // the result is -1,-1. Uses an alternative algorithm from Intersection() - this is faster and more usable. This only returns a // point if the two segments actually intersect - it doesn't project the lines.
        // This algorithm from Paul Bourke, 
http://local.wasp.uwa.edu.au/~pbourke/geometry/lineline2d/
        
        float d = (p4.y - p3.y)*(p2.x - p1.x) - (p4.x - p3.x)*(p2.y-p1.y);
        
        // if d is 0, then lines are parallel and don't intersect
        
        if ( d == 0.0 )
                return NSMakePoint( -1, -1 );
                
float ua = ((p4.x - p3.x)*(p1.y - p3.y) - (p4.y - p3.y)*(p1.x - p3.x))/d; float ub = ((p2.x - p1.x)*(p1.y - p3.y) - (p2.y - p1.y)*(p1.x - p3.x))/d;
        
        if( ua >= 0.0 && ua <= 1.0 && ub >= 0.0 && ub <= 1.0 )
        {
                // segments do intersect
        
                NSPoint ip;
        
                ip.x = p1.x + ua*(p2.x - p1.x);
                ip.y = p1.y + ub*(p2.y - p1.y);
        
                return ip;
        }
        else
                return NSMakePoint( -1, -1 );
}

// sort function called from code in NSBezierPath (TextLayout)

static int      SortPointsHorizontally( id value1, id value2, void* context )
{
        #pragma unused(context)
        NSPoint a, b;
        
        a = [value1 pointValue];
        b = [value2 pointValue];
        
        if( a.x > b.x )
                return NSOrderedDescending;
        else if ( a.x < b.x )
                return NSOrderedAscending;
        else
                return NSOrderedSame;
}

// part of category NSBezierPath (TextLayout):

@implementation NSBezierPath (TextLayout)

- (NSArray*)    intersectingPointsForHorizontalLineAtY:(float) yPosition
{
// given a y value within the bounds of the bezier path, this returns an array of points (as NSValues) which are the intersection of // a horizontal line extending across the full width of the shape at y and the curve boundary itself. This works by approximating the curve as a series // of straight lines and testing each one for intersection with the line at y. This is the primitive method used to determine line layout // rectangles - a series of calls to this is needed for each line (incrementing y by the lineheight) and then rects forming from the // resulting points. See also -lineFragmentRectsForFixedLineheight: and -lineFragmentRectForProposedRect:remainingRect:datumOffset:

// note - might be a performance killer for complex shapes - caller could consider caching the result as long as the curve remains unchanged
        
        if([self isEmpty])
                return nil;             // nothing here, so bail
                
        NSRect br = [self bounds];
        
// see if y is within the bounds - if not, there can't be any intersecting points so we can bail now.
        
        if( yPosition < NSMinY( br ) || yPosition > NSMaxY( br ))
                return nil;
                
// set up the points defining the horizontal line crossing the whole shape at y:
        
        br = NSInsetRect( br, -1, -1 );
        
        NSPoint hla, hlb;
        
        hla.y = hlb.y = yPosition;
        hla.x = NSMinX( br ) - 1;
        hlb.x = NSMaxX( br) + 1;
        
// we can use a relatively coarse flatness for more speed - exact precision isn't needed for text layout.
        
        float savedFlatness = [self flatness];
        [self setFlatness:6.0]; 
        NSBezierPath*   flatpath = [self bezierPathByFlatteningPath];
        [self setFlatness:savedFlatness];
        
        NSMutableArray*         result = [NSMutableArray array];
        int                     i, m = [flatpath elementCount];
        NSBezierPathElement     lm;
        NSPoint                 fp, lp, ap, ip;
        
        for( i = 0; i < m; ++i )
        {
                lm = [flatpath elementAtIndex:i associatedPoints:&ap];
                
                if ( lm == NSMoveToBezierPathElement )
                        fp = lp = ap;
                else
                {
                        if( lm == NSClosePathBezierPathElement )
                                ap = fp;
                        
                        // does this element intersect the horizontal line?

                        ip = Intersection2( ap, lp, hla, hlb );
                        lp = ap;
                
// if the result is {-1,-1}, lines don't intersect. The intersection point may also fall outside the bounds,
                        // so we discard that result as well.
                        
                        if( NSEqualPoints( ip, NSMakePoint( -1, -1 )))
                                continue;
                                
                        if ( NSPointInRect( ip, br ))
                                [result addObject:[NSValue valueWithPoint:ip]];
                }
        }
        
        // if the result is not empty, sort the points into order horizontally
        
        if([result count] > 0 )
        {
                [result sortUsingFunction:SortPointsHorizontally context:NULL];
                
// if the result is odd, it means that we don't have a closed path shape at the line position - // i.e. there's an open endpoint. So to ensure that we return an even number of items (or none),
                // delete the last item to make the result even.
                
                if(([result count] & 1) == 1)
                {
                        [result removeLastObject];
                        
                        if([result count] == 0 )
                                result = nil;
                }
        }
        else
                result = nil;   // nothing found, so just return nil
        
        return result;
}


- (NSRect) lineFragmentRectForProposedRect:(NSRect) aRect remainingRect:(NSRect*) rem
{
return [self lineFragmentRectForProposedRect:aRect remainingRect:rem datumOffset:0];
}


- (NSRect) lineFragmentRectForProposedRect:(NSRect) aRect remainingRect:(NSRect*) rem datumOffset:(float) dOffset
{
// this offsets <proposedRect> to the right to the next even-numbered intersection point, setting its length to the difference // between that point and the next. That part is the return value. If there are any further points, the remainder is set to // the rest of the rect. This allows this method to be called directly by a NSTextContainer subclass

        // The datum offset is a value
// between -0.5 and +0.5 that specifies where in the line's height is used to find the shape's intersections at that y value. // A value of 0 means use the centre of the line, -0.5 the top, and +0.5 the bottom. An alternative approach might fetch both // top and bottom of the line and use the inner points of the pair to ensure that it's impossible for a glyph to stray outside // the edges of the curve - in practice this is simpler and almost as good and you can add a little bit of line padding to the
        // text container to keep glyphs inside the curve if necessary anyway.
        
        float od = LIMIT( dOffset, -0.5, +0.5 ) + 0.5; // offset in 0..1
        
        NSRect result;
        
        result.origin.y = NSMinY( aRect );
        result.size.height = NSHeight( aRect );
        
        float y = NSMinY( aRect ) + ( od * NSHeight( aRect ));
        
// find the intersection points - these are returned pre-sorted left to right and are
        // guaranteed to contain an even number of points (or none at all)
        
        NSArray*        thePoints = [self 
intersectingPointsForHorizontalLineAtY:y];
        NSPoint         p1, p2;
        int             ptIndex, ptCount;
        
        ptCount = [thePoints count];
        
// get the next even-numbered intersection point starting at the left edge of proposed rect.
        
        for( ptIndex = 0; ptIndex < ptCount; ptIndex += 2 )
        {
                p1 = [[thePoints objectAtIndex:ptIndex] pointValue];
        
                // index always even, so it's a left edge
                
                if( p1.x >= aRect.origin.x )
                {
                        // this is the main rect to return - find the 
right-hand edge
                        
                        p2 = [[thePoints objectAtIndex:ptIndex + 1] pointValue];
                
                        result.origin.x = p1.x;
                        result.size.width = p2.x - p1.x;
                        
                        // and this is the remainder - just offset the proposed 
rect to start
// where the main rect now ends. Next call will look ahead from this point.
                        
                        if( rem != nil )
                        {
                                aRect.origin.x = p2.x;
                                *rem = aRect;
                        }
                        
                        return result;
                }
        }
        
// if we went through all the points and there were no more following the left edge of proposedRect, then there's no
        // more space on this line, so return zero rect.
        
        result = NSZeroRect;
         if ( rem != nil )
                *rem = NSZeroRect;

        return result;
}


@end

// subclass of NSTextContainer:
// naturally this must be added to an NSLayoutManager instance to actually implement the text flowed into the bezier shape

@interface DKBezierTextContainer : NSTextContainer
{
        NSBezierPath*   mPath;
}

- (void)        setBezierPath:(NSBezierPath*) aPath;

@end


@implementation DKBezierTextContainer


- (void)        setBezierPath:(NSBezierPath*) aPath
{
// copy the path and store it offset to its top, left corner - this avoids
        // having to align the proposed rects to the path for every call.
        
        NSRect pb = [aPath bounds];
        NSAffineTransform* tfm = [NSAffineTransform transform];
        
        [tfm translateXBy:-pb.origin.x yBy:-pb.origin.y];
        aPath = [tfm transformBezierPath:aPath];
        
        [aPath retain];
        [mPath release];
        mPath = aPath;
}


- (BOOL)        isSimpleRegularTextContainer
{
        return (mPath == nil);
}


- (NSRect)      lineFragmentRectForProposedRect:(NSRect) proposedRect
                        sweepDirection:(NSLineSweepDirection) sweepDirection
                        movementDirection:(NSLineMovementDirection) 
movementDirection
                        remainingRect:(NSRectPointer) remainingRect
{
        if( mPath == nil )
return [super lineFragmentRectForProposedRect:proposedRect sweepDirection:sweepDirection movementDirection:movementDirection remainingRect:remainingRect];
        else
return [mPath lineFragmentRectForProposedRect:proposedRect remainingRect:remainingRect];
}


- (void)        dealloc
{
        [mPath release];
        [super dealloc];
}


@end


On 11 May 2008, at 12:20 am, John Joyce wrote:

Graham, that would be very cool to have posted here!
If nothing else, it will be in the archive for others who run into it.
(I'm in agreement about the clarity of a large number of docs... there often seems to be a circular relationship of what you need to understand before being able to understand docs on many topics.) Otherwise, it would make a great blog post with the graphics you linked earlier too.
_______________________________________________

Cocoa-dev mailing list (Cocoa-dev@lists.apple.com)

Please do not post admin requests or moderator comments to the list.
Contact the moderators at cocoa-dev-admins(at)lists.apple.com

Help/Unsubscribe/Update your Subscription:
http://lists.apple.com/mailman/options/cocoa-dev/graham.cox%40bigpond.com

This email sent to [EMAIL PROTECTED]

_______________________________________________

Cocoa-dev mailing list (Cocoa-dev@lists.apple.com)

Please do not post admin requests or moderator comments to the list.
Contact the moderators at cocoa-dev-admins(at)lists.apple.com

Help/Unsubscribe/Update your Subscription:
http://lists.apple.com/mailman/options/cocoa-dev/archive%40mail-archive.com

This email sent to [EMAIL PROTECTED]

Reply via email to