Re: Problem getting subclass of NSTextContainer working properly [SOLVED]
Re: Problem getting subclass of NSTextContainer working properly [SOLVED]
- Subject: Re: Problem getting subclass of NSTextContainer working properly [SOLVED]
- From: Graham Cox <email@hidden>
- Date: Sun, 11 May 2008 21:27:53 +1000
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 (email@hidden)
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:
This email sent to email@hidden
_______________________________________________
Cocoa-dev mailing list (email@hidden)
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:
This email sent to email@hidden