SWIFT OSX NSBezierPath Linewidth property not working - macos

I am using the following code to draw a line graph on a custom NSView
for var index = 0; index < (dataPointsArray.count - 1); index++ {
NSBezierPath().lineWidth = 20.0
NSBezierPath.strokeLineFromPoint(dataPointsArray[index], toPoint: dataPointsArray[index + 1])
}
This snippet is contained within a function that is called by drawRect() within the Custom View.
The line draws correctly within the coordinate system of the view. However, the line is drawn at the same width (one pixel width) regardless of the .lineWidth setting (e.g., 5.0, 10.0, 20.0 etc) which seems to have no impact on the line that is actually drawn).
Is anyone able to advise what might be creating this issue for me. I haven't been able to find a previous question that raises this issue.

NSBezierPath().lineWidth = 20.0
The () means you are initializing a new instance of the class and set its lineWidth to 20.0. You should create a variable and use it to draw your path:
var bezierPath = NSBezierPath()
bezierPath.lineWidth = 20.0
bezierPath.moveToPoint(dataPointsArray[index])
bezierPath.lineToPoint(dataPointsArray[index + 1])

Related

Exclusion rects not shifting text properly with NSTextView/UITextView

I have an NSTextView with some exclusion rects so I can shift the text vertically without having to resort to adding newlines to the string. However, I've noticed that the exclusion rect is rather limited or perhaps even buggy because I cannot shift the text vertically more than ~45% of the textview's height. I have found a workaround where I increase the textview's height by 2x but this feels gross and I'd rather do it "properly"
In the image above, I have three text views (from left to right)
Programmatically without encapsulating it inside an NSScrollView, 2x height
Programmatically with NSScrollView encapsulation. 2x height
Using interface builder, regular height.
The exclusion rect is drawn with a CAShapeLayer and you can see that "hello world new world" isn't properly positioned outside of the exclusion rect.
I tried all three examples to make sure I wasn't missing something with regards to IB default settings or the dynamics of the text view when encapsulated in an NSScrollView (Im new to AppKit and TextKit), however all 3 examples exhibit the same bug.
Code to update the text exclusion rect
(Each time the slider moves on the bottom right, it will update their text rect)
label.stringValue = "excl: \(sender.integerValue): height: \(view.frame.height)"
print(sender.integerValue)
let exclHeight: CGFloat = CGFloat(slider.integerValue)
[aTextView, bTextView, cTextView]
.compactMap { $0 }
.forEach {
let rect = CGRect(
x: 5,
y: 5,
width: aTextView.frame.width - 10,
height: exclHeight)
$0.wantsLayer = true
$0.textContainer?.exclusionPaths.removeAll()
$0.textContainer?.exclusionPaths.append(.init(rect: rect))
$0.layer?.sublayers?.forEach {
$0.removeFromSuperlayer()
}
let layer = CAShapeLayer()
layer.frame = rect
layer.backgroundColor = NSColor.blue.withAlphaComponent(0.2).cgColor
layer.borderWidth = 1
layer.borderColor = NSColor.white.cgColor
$0.layer?.addSublayer(layer)
}
}
The problem seems to be that the exclusionPath is before the first line.
Just playing around with the parameters, a two line sample text with the rect y positioned after the first line works without any problems.
So it looks like the issue is calculating the container height when it starts with a exclusionPaths in -[NSLayoutManager usedRectForTextContainer:]
#interface LOLayoutManager : NSLayoutManager
#end
#implementation LOLayoutManager
- (NSRect)usedRectForTextContainer:(NSTextContainer *)container
{
NSRect rect = [super usedRectForTextContainer:container];
NSRect newRect = NSMakeRect(0, 0, NSMaxX(rect), NSMaxY(rect));
return newRect;
}
#end
Instead of returning y position from the exclusionPaths and the line fragments height, this returns a big rect starting at 0, 0. This should work as long as the NSTextView only contains one text container.

Tooltip for individual points created using THREE.PointCloud in Autodesk forge

I followed this blog for adding tooltip for each point implemented using Three.PointCloud. I used world2Screen to get the location of individual points and tried using this
elem = document.elementFromPoint(x, y)
but continuously only get canvas as the output (and thus tooltip at a fixed position) instead at the clicked/hovered point.
Anyone who has may be implemented this and knows any work around.
Thanks in advance
According to the blog, the document.elementFromPoint() is used to check if the cursor isn’t over the canvas. If you want to capture the hit point of the mouse clicked one, your own raycaster can help in this case. For example:
var x = ((event.clientX - viewer.canvas.offsetLeft) / viewer.canvas.width) * 2 - 1;
var y = -((event.clientY - viewer.canvas.offsetTop) / viewer.canvas.height) * 2 + 1;
var vector = new THREE.Vector3(x, y, 0.5).unproject(this.camera);
this.raycaster.set(this.camera.position, vector.sub(this.camera.position).normalize());
var nodes = this.raycaster.intersectObject( this.pointCloud );
In the above code snippet, the event object is from the mouse clicking event, see here for detail: https://github.com/wallabyway/markupExt/blob/master/docs/markupExt.js#L48

Can't apply multiple transformation to UIImageView

I can't find out why my transformations are not being applied to a UIImageView in Xamarin iOS.
Here's the code:
CGAffineTransform transform = new CGAffineTransform();
transform.Rotate((float)Math.PI / 2);
transform.Scale(0.8f, 0.8f);
this.imageViewOverlay = new UIImageView
{
ContentMode = UIViewContentMode.Center,
BackgroundColor = UIColor.Clear,
Opaque = false,
Frame = new CGRect(0,0,this.View.Bounds.Width, this.View.Bounds.Height),
Transform = transform
};
The way I was doing it before used to be like
Transform = CGAffineTransform.MakeRotation((float)Math.PI / 2)
But unfortunately when you want to use it twice, the second one is clearing the transformation before.
I'm using
Xamarin.iOS:
Version: 8.6.0.41 (Business Edition)
Operating System:
Mac OS X 10.10.3
XCode: Version 6.3.1 (6D1002)
Thanks !
Your original CGAffineTransform is empty (all 00), default for a struct, and not an identity transform, on top of which you can construct something. IOW that makes all subsequent calls (math ops) return an empty one.
You can either start with a non-empty transform, e.g.
var transform = CGAffineTransform.MakeRotation((float)Math.PI / 2);
transform.Scale(0.8f, 0.8f);
or you can start with an identity transform, e.g.
var transform = CGAffineTransform.MakeIdentity ();
transform.Rotate ((nfloat) Math.PI / 2);
transform.Scale ((nfloat) 0.8f, (nfloat) 0.8f);

Grid like arrangements in UIScrollView(PhotoScroller)

I was using apple's scrollview sample code PhotoScroller for my app using numerous images (and by recycling logic)
in UIScrollView. I implemented that in my app and it works fine.
Now Im working in an app similar to the above, but with the
difference, loading images in grid like view. When I happen to use the
same sample code, every thing works fine except the recycling logic.
I think there is some problem with my frame set which don't tell the
xcode, the visible region.
Please some one temme how to set the visible set for the grid View
structure for scrollview? The code I use is,
CGRect visibleBounds = _scrollView.bounds;
// CGRect gridElementvisibleBounds = CGRectMake(0, 0, 212, 200);
int firstNeededPageIndex = floorf(CGRectGetMinX(visibleBounds) -
CGRectGetWidth(visibleBounds));
int lastNeededPageIndex = floorf((CGRectGetMaxX(visibleBounds)-1) -
CGRectGetWidth(visibleBounds));
firstNeededPageIndex = MAX(firstNeededPageIndex, 0);
lastNeededPageIndex = MIN(lastNeededPageIndex, [self imageCount] - 1);
where _scrollView is the UIScrollView instance that I use and the
gridElement that I use is of frame size (0, 0, 212, 200). The number
of grid elements that occupy the scrollView bounds is
3 x 3 (9).
I don't want to use grid like tableViews(AQGridView, etc,.) since Im gonna load more than 500 images.
Please some one help me finding out the thing that I should correct in
the above code.
I nearly fixed the issue by making use of contentOffset to get the visible area.
Here is the piece of code illustrating what I did to make it working.
int firstNeededPageIndex = ((int)_scrollView.contentOffset.y / 960) * 9;
int lastNeededPageIndex = ((int)_scrollView.contentOffset.y / 960) * 9 + 17;
where I found the visible area by getting the contentOffset.y/960 and got the firstNeededPageIndex as given above.
When the scrollview scrolls, the components of the page that is getting to hide contains 9 elements and the successive page(got by lastNeededPageIndex) that is getting to be visible contains no components.
Hence I made it visible by making 18 objects to the visible area while scrolling.
Hence the objects to be visible while scrolling became 0th object to 17th object.
And the result is whenever the scrollview scrolls, 18 components(0 to 17) in the visible area(got through contentOffSet ) are recycled.

Core Text's CTFramesetterSuggestFrameSizeWithConstraints() returns incorrect size every time

According to the docs, CTFramesetterSuggestFrameSizeWithConstraints () "determines the frame size needed for a string range".
Unfortunately the size returned by this function is never accurate. Here is what I am doing:
NSAttributedString *string = [[[NSAttributedString alloc] initWithString:#"lorem ipsum" attributes:nil] autorelease];
CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString((CFAttributedStringRef) string);
CGSize textSize = CTFramesetterSuggestFrameSizeWithConstraints(framesetter, CFRangeMake(0,0), NULL, CGSizeMake(rect.size.width, CGFLOAT_MAX), NULL);
The returned size always has the correct width calculated, however the height is always slightly shorter than what is expected.
Is this the correct way to use this method?
Is there any other way to layout Core Text?
Seems I am not the only one to run into problems with this method. See https://devforums.apple.com/message/181450.
Edit:
I measured the same string with Quartz using sizeWithFont:, supplying the same font to both the attributed string, and to Quartz. Here are the measurements I received:
Core Text: 133.569336 x 16.592285
Quartz: 135.000000 x 31.000000
try this.. seem to work:
+(CGFloat)heightForAttributedString:(NSAttributedString *)attrString forWidth:(CGFloat)inWidth
{
CGFloat H = 0;
// Create the framesetter with the attributed string.
CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString( (CFMutableAttributedStringRef) attrString);
CGRect box = CGRectMake(0,0, inWidth, CGFLOAT_MAX);
CFIndex startIndex = 0;
CGMutablePathRef path = CGPathCreateMutable();
CGPathAddRect(path, NULL, box);
// Create a frame for this column and draw it.
CTFrameRef frame = CTFramesetterCreateFrame(framesetter, CFRangeMake(startIndex, 0), path, NULL);
// Start the next frame at the first character not visible in this frame.
//CFRange frameRange = CTFrameGetVisibleStringRange(frame);
//startIndex += frameRange.length;
CFArrayRef lineArray = CTFrameGetLines(frame);
CFIndex j = 0, lineCount = CFArrayGetCount(lineArray);
CGFloat h, ascent, descent, leading;
for (j=0; j < lineCount; j++)
{
CTLineRef currentLine = (CTLineRef)CFArrayGetValueAtIndex(lineArray, j);
CTLineGetTypographicBounds(currentLine, &ascent, &descent, &leading);
h = ascent + descent + leading;
NSLog(#"%f", h);
H+=h;
}
CFRelease(frame);
CFRelease(path);
CFRelease(framesetter);
return H;
}
For a single line frame, try this:
line = CTLineCreateWithAttributedString((CFAttributedStringRef) string);
CGFloat ascent;
CGFloat descent;
CGFloat width = CTLineGetTypographicBounds(line, &ascent, &descent, NULL);
CGFloat height = ascent+descent;
CGSize textSize = CGSizeMake(width,height);
For multiline frames, you also need to add the line's lead (see a sample code in Core Text Programming Guide)
For some reason, CTFramesetterSuggestFrameSizeWithConstraints() is using the difference in ascent and descent to calculate the height:
CGFloat wrongHeight = ascent-descent;
CGSize textSize = CGSizeMake(width, wrongHeight);
It could be a bug?
I'm having some other problems with the width of the frame; It's worth checking out as it only shows in special cases. See this question for more.
The problem is that you have to apply a paragraph style to the text before you measure it. If you don't then you get the default leading of 0.0. I provided a code sample for how to do this in my answer to a duplicate of this question here https://stackoverflow.com/a/10019378/1313863.
ing.conti's answer but in Swift 4:
var H:CGFloat = 0
// Create the framesetter with the attributed string.
let framesetter = CTFramesetterCreateWithAttributedString(attributedString as! CFMutableAttributedString)
let box:CGRect = CGRect.init(x: 0, y: 0, width: width, height: CGFloat.greatestFiniteMagnitude)
let startIndex:CFIndex = 0
let path:CGMutablePath = CGMutablePath()
path.addRect(box)
// Create a frame for this column and draw it.
let frame:CTFrame = CTFramesetterCreateFrame(framesetter, CFRangeMake(startIndex, 0), path, nil)
// Start the next frame at the first character not visible in this frame.
//CFRange frameRange = CTFrameGetVisibleStringRange(frame);
//startIndex += frameRange.length;
let lineArray:CFArray = CTFrameGetLines(frame)
let lineCount:CFIndex = CFArrayGetCount(lineArray)
var h:CGFloat = 0
var ascent:CGFloat = 0
var descent:CGFloat = 0
var leading:CGFloat = 0
for j in 0..<lineCount {
let currentLine = unsafeBitCast(CFArrayGetValueAtIndex(lineArray, j), to: CTLine.self)
CTLineGetTypographicBounds(currentLine, &ascent, &descent, &leading)
h = ascent + descent + leading;
H+=h;
}
return H;
I did try and keep it as 1:1 with the Objective C code but Swift is not as nice when handling pointers so some changes were required for casting.
I also did some benchmarks comparing this code (and it's ObjC counterpart) to another height methods. As a heads up, I used a HUGE and very complex attributed string as input and also did it on the sim so the times themselves are meaningless however the relative speeds are correct.
Runtime for 1000 iterations (ms) BoundsForRect: 8909.763097763062
Runtime for 1000 iterations (ms) layoutManager: 7727.7010679244995
Runtime for 1000 iterations (ms) CTFramesetterSuggestFrameSizeWithConstraints: 1968.9229726791382
Runtime for 1000 iterations (ms) CTFramesetterCreateFrame ObjC: 1941.6030206680298
Runtime for 1000 iterations (ms) CTFramesetterCreateFrame-Swift: 1912.694974899292
It might seem strange but I found that if you use ceil function first and then add +1 to the height it will always work. Many third party APIs use this trick.
Resurrecting.
When initially determining where lines should be placed within a frame, Core Text seems to massage the ascent+descent for the purposes of line origin calculation. In particular, it seems like 0.2*(ascent+descent) is added to the ascent, and then both the descent and resultant ascent are modified by floor(x + 0.5), and then the baseline positions are calculated based on these adjusted ascents and descents. Both of these steps are affected by certain conditions whose nature I am not sure, and I also already forgot at which point paragraph styles are taken into account, despite only looking into it a few days ago.
I've already resigned to just considering a line to start at its baseline and not trying to figure out what the actual lines land at. Unfortunately, this still does not seem to be enough: paragraph styles are not reflected in CTLineGetTypographicBounds(), and some fonts like Klee that have nonzero leadings wind up crossing the path rect! Not sure what to do about this... probably for another question.
UPDATE
It seems CTLineGetBoundsWithOptions(line, 0) does get the proper line bounds, but not quite fully: there's a gap between lines, and with some fonts (Klee again) the gap is negative and the lines overlap... Not sure what to do about this. :| At least we're slightly closer??
And even then it still does not take paragraph styles into consideration >:|
CTLineGetBoundsWithOptions() is not listed on Apple's documentation site, possibly due to a bug in the current version of their documentation generator. It is a fully documented API, however — you'll find it in the header files and it was discussed at length at WWDC 2012 session 226.
None of the options are relevant to us: they reduce the bounds rect by taking certain font design choices into consideration (or increase the bounds rect randomly, in the case of the new kCTLineBoundsIncludeLanguageExtents). One useful option in general, though, is kCTLineBoundsUseGlyphPathBounds, which is equivalent to CTLineGetImageBounds() but without needing to specify a CGContext (and thus without being subject to an existing text matrix or CTM).
After weeks of trying everything, any combination possible, I made a break through and found something that works. This issue seems to be more prominent on macOS than on iOS, but still appears on both.
What worked for me was to use a CATextLayer instead of a NSTextField (on macOS) or a UILabel (on iOS).
And using boundingRect(with:options:context:) instead of CTFramesetterSuggestFrameSizeWithConstraints. Even though in theory the latter should be more lower level than the former, and I was assuming would be more precise, the game changer turns out to be NSString.DrawingOptions.usesDeviceMetrics.
The frame size suggested fits like a charm.
Example:
let attributedString = NSAttributedString(string: "my string")
let maxWidth = CGFloat(300)
let size = attributedString.boundingRect(
with: .init(width: maxWidth,
height: .greatestFiniteMagnitude),
options: [
.usesFontLeading,
.usesLineFragmentOrigin,
.usesDeviceMetrics])
let textLayer = CATextLayer()
textLayer.frame = .init(origin: .zero, size: size)
textLayer.contentsScale = 2 // for retina
textLayer.isWrapped = true // for multiple lines
textLayer.string = attributedString
Then you can add the CATextLayer to any NSView/UIView.
macOS
let view = NSView()
view.wantsLayer = true
view.layer?.addSublayer(textLayer)
iOS
let view = UIView()
view.layer.addSublayer(textLayer)

Resources