CGPath-based CPTPlotSymbol inverted and distorted - ios

I'm currently working with custom markers on a scatter plot and found myself with an issue that results in CPTPlotSymbol created from a CGPath upside down and distorted.
I've tested the path-creating code in a playground and it works without issues, drawing the path with the correct shape and orientation.
Here's the path drawing code:
private func getOuterPathInRect(rect: CGRect) -> CGPath {
let circlePath: CGPath = {
let p = CGMutablePath()
let topHundred = CGRect(x: 0, y: 0, width: 100, height: 100)
p.addEllipse(in: topHundred)
return p
}()
let arrowPath: CGPath = {
let p = CGMutablePath()
p.move(to: CGPoint(x: rect.midX, y: rect.maxY - 5.0))
p.addLine(to: CGPoint(x: rect.midX - 7.5, y: rect.maxY - 15.0))
p.addLine(to: CGPoint(x: rect.midX + 7.5, y: rect.maxY - 15.0))
return p
}()
let path = CGMutablePath()
path.addPath(circlePath)
path.addPath(arrowPath)
return path
}
And the code that creates the CPTPlotSymbol is:
func symbol(for plot: CPTScatterPlot, record idx: UInt) -> CPTPlotSymbol? {
let index = Int(idx)
guard items[index].requiresMarker else { return nil }
let rect = CGRect(x: 0, y: 0, width: 120, height: 120)
let marker = BallMarkerView()
marker.contentMode = .center
let path = marker.pathIn(rect: rect)
let symbol = CPTPlotSymbol.customPlotSymbol(with: path)
symbol.size = rect.size
symbol.fill = CPTFill(color: CPTColor.red())
return symbol
}
My goal was to use a custom UIView as a marker, but I couldn't find an API to do so, so I resorted to providing a path-based marker and fill it with an image representation of the marker.
Is this the proper way of doing it?
Why is my path being drawn distorted and upside down? The path being upside down could be explained by the difference in the coordinate system between UIKit and CoreGraphics, but that doesn't explain the distorsion.
Thanks!

Because Core Plot shares drawing code between the Mac and iOS, it uses the same drawing coordinate system on both platforms where (0, 0) is the lower-left corner of the drawing canvas. This is flipped from the normal drawing coordinate system on iOS.

Related

How to project 3D Joint Points (Transformation) onto 2D Screen ARKit?

I am trying to project the 3D human joint points onto the iPhone's screen using ARKit.
I am extracting the global transforms:
let rightArmPosition = skeleton.modelTransform(for: ARSkeleton.JointName(rawValue: "right_arm_joint"))!
let rootPosition = skeleton.modelTransform(for: .root)!
I am calculating the offset
let rightOffset = simd_make_float3(rightArmPosition.columns.3)
let rootOffset = simd_make_float3(rootPosition.columns.3)
I am projecting the points
let pMatrix = camera.projectionMatrix
let pRightOffset = camera.projectPoint(rightOffset, orientation: .portrait, viewportSize: CGSize(width: UIScreen.main.bounds.width, height: UIScreen.main.bounds.height))
let pRootOffset = camera.projectPoint(rootOffset, orientation: .portrait, viewportSize: CGSize(width: UIScreen.main.bounds.width, height: UIScreen.main.bounds.height))
humanJointsView.frame = UIScreen.main.bounds
humanJointsView.points = [pRightOffset, pRootOffset]
I am trying to draw the points in the target view:
override func draw(_ rect: CGRect) {
path.removeAllPoints()
self.points.forEach { point in
path = UIBezierPath(ovalIn: CGRect(x: point.x, y: point.y, width: CGFloat(30), height: CGFloat(30)))
UIColor.green.setFill()
path.fill()
}
}
This approach is not working however, where is my mistake?
Thank you!
The right arm and root transforms are in skeleton model space, you need to multiply them with the body anchor transform.
rightArmWorldTransform = body.transform * rightArmPosition

How to find where to draw text inside an irregular shape represented by a UIBezierPath?

In my app, I have a bunch of UIBezierPaths that I import from SVG files using this thing, which all represent irregular shapes. I want to draw texts inside the shapes so as to "label" those shapes. Here's an example:
I want to draw the text "A" inside the shape. Basically, this'd be done in the draw(_:) method of a custom UIView, and I expect to call a method with a signature such as:
func drawText(_ text: String, in path: UIBezierPath, fontSize: CGFloat) {
// draws text, or do nothing if there is not enough space
}
After some looking online, I found this post, which seems to always draw the string in the centre of the image. This is not what I want. If I put the A in the centre of the bounding box of the UIBezierPath (which I know I can get with UIBezierPath.bounds), it would be outside of the shape. This is probably because the shape is concave.
Note that the texts are quite short so I don't need to worry about line wrapping and stuff.
Of course, there are lots of places inside the shape I could put the "A" in, I just want a solution that chooses any one place that can show the text in a reasonable size. One way I have thought of is to find the largest rectangle that can be inscribed in the shape, and then draw the text in the centre of that rectangle. I've found this post that does this in Matlab, and it seems like a really computation-intensive to do... I'm not sure this is a good solution.
How should I implement drawText(_:in:fontSize:)?
To avoid being an XY question, here's some background:
The shapes I'm handling are actually borders of administrative regions. In other words, I'm drawing a map. I don't want to use existing map APIs because I want my map to look very crude. It's only going to show the borders of administrative regions, and labels on them. So surely I could just use whatever algorithm the map APIs are using to draw the labels on their maps, right?
This question is not a duplicate of this, as I know I can do that with UIBezierPath.contains. I'm trying to find a point. It's not a duplicate of this either, as my question is about drawing text inside a path, not on.
TextKit was built for tasks like this. You can create an array of paths outside of your bezier shape path and then set it as your textView's exclusionPaths:
textView.textContainer.exclusionPaths = [pathsAroundYourBezier];
Keep in mind that the exclusion paths are paths where text in the container will not be displayed. Apple documentation here: https://developer.apple.com/documentation/uikit/nstextcontainer/1444569-exclusionpaths
UPDATE DUE TO BUGS WITH EXCLUSION PATHS AT THE BEGINNING OF TEXTVIEW'S:
I've come up with a way to find where in a path text can fit.
Usage:
let pathVisible = rectPathSharpU(CGSize(width: 100, height: 125), origin: CGPoint(x: 0, y: 0))
let shapeLayer = CAShapeLayer()
shapeLayer.path = pathVisible.cgPath
shapeLayer.strokeColor = UIColor.green.cgColor
shapeLayer.backgroundColor = UIColor.clear.cgColor
shapeLayer.lineWidth = 3
self.view.layer.addSublayer(shapeLayer)
let path = rectPathSharpU(CGSize(width: 100, height: 125), origin: CGPoint(x: 0, y: 0))
let fittingRect = findFirstRect(path: path, thatFits: "A".size())!
print("fittingRect: \(fittingRect)")
let label = UILabel.init(frame: fittingRect)
label.text = "A"
self.view.addSubview(label)
Output:
There may be cases with curved paths that will need to be taken into account, perhaps by iterating through every y point in a path bounds until a sizable space is found.
The function to find the first fitting rect:
func findFirstRect(path: UIBezierPath, thatFits: CGSize) -> CGRect? {
let points = path.cgPath.points
allPoints: for point in points {
var checkpoint = point
var size = CGSize(width: 0, height: 0)
thisPoint: while size.width <= path.bounds.width {
if path.contains(checkpoint) && path.contains(CGPoint.init(x: checkpoint.x + thatFits.width, y: checkpoint.y + thatFits.height)) {
return CGRect(x: checkpoint.x, y: checkpoint.y, width: thatFits.width, height: thatFits.height)
} else {
checkpoint.x += 1
size.width += 1
continue thisPoint
}
}
}
return nil
}
Extension for finding string size:
extension String {
func size(width:CGFloat = 220.0, font: UIFont = UIFont.systemFont(ofSize: 17.0, weight: .regular)) -> CGSize {
let label:UILabel = UILabel(frame: CGRect(x: 0, y: 0, width: width, height: CGFloat.greatestFiniteMagnitude))
label.numberOfLines = 0
label.lineBreakMode = NSLineBreakMode.byWordWrapping
label.font = font
label.text = self
label.sizeToFit()
return CGSize(width: label.frame.width, height: label.frame.height)
}
}
Creating the test path:
func rectPathSharpU(_ size: CGSize, origin: CGPoint) -> UIBezierPath {
// Initialize the path.
let path = UIBezierPath()
// Specify the point that the path should start get drawn.
path.move(to: CGPoint(x: origin.x, y: origin.y))
// add lines to path
path.addLine(to: CGPoint(x: (size.width / 3) + origin.x, y: (size.height / 3 * 2) + origin.y))
path.addLine(to: CGPoint(x: (size.width / 3 * 2) + origin.x, y: (size.height / 3 * 2) + origin.y))
path.addLine(to: CGPoint(x: size.width + origin.x, y: origin.y))
path.addLine(to: CGPoint(x: (size.width) + origin.x, y: size.height + origin.y))
path.addLine(to: CGPoint(x: origin.x, y: size.height + origin.y))
// Close the path. This will create the last line automatically.
path.close()
return path
}
If this doesn't work for paths with a lot of arcs like your picture example, please post the actual path data so I can test with that.
Bonus: I also created a function to find the widest section of a symmetric path, though height isn't taken into account. Though it may be useful:
func findWidestY(path: UIBezierPath) -> CGRect {
var widestSection = CGRect(x: 0, y: 0, width: 0, height: 0)
let points = path.cgPath.points
allPoints: for point in points {
var checkpoint = point
var size = CGSize(width: 0, height: 0)
thisPoint: while size.width <= path.bounds.width {
if path.contains(checkpoint) {
checkpoint.x += 1
size.width += 1
continue thisPoint
} else {
if size.width > widestSection.width {
widestSection = CGRect(x: point.x, y: point.y, width: size.width, height: 1)
}
break thisPoint
}
}
}
return widestSection
}

Adding the same layer in a subview changes its visual position

I added a subview (with a black border) in a view and centered it.
Then I generate 2 identical triangles with CAShapeLayer and add one to the subview and the other to the main view.
Here is the visual result in Playground where we can see that the green triangle is totally off and should have been centered.
And here is the code:
let view = UIView()
let borderedView = UIView()
var containedFrame = CGRect(x: 0, y: 0, width: 100, height: 100)
func setupUI() {
view.frame = CGRect(x: 0, y: 0, width: 300, height: 600)
view.backgroundColor = .white
borderedView.frame = containedFrame
borderedView.center = view.center
borderedView.backgroundColor = .clear
borderedView.layer.borderColor = UIColor.black.cgColor
borderedView.layer.borderWidth = 1
view.addSubview(borderedView)
setupTriangles()
}
private func setupTriangles() {
view.layer.addSublayer(createTriangle(color: .red)) // RED triangle
borderedView.layer.addSublayer(createTriangle(color: .green)) // GREEN triangle
}
private func createTriangle(color: UIColor) -> CAShapeLayer {
let layer = CAShapeLayer()
let bezierPath = UIBezierPath()
bezierPath.move(to: CGPoint(x: 0, y: 0))
bezierPath.addLine(to: CGPoint(x: -containedFrame.width, y: 0))
bezierPath.addLine(to: CGPoint(x: 0, y: -containedFrame.height))
bezierPath.close()
layer.position = borderedView.center
layer.path = bezierPath.cgPath
layer.fillColor = color.cgColor
return layer
}
Note: All position (of view, the borderedView and both triangles) are the same (150.0, 300.0)
Question: Why is the green layer not in the right position?
#DuncanC is right that each view has its own coordinate system. Your problem is this line:
layer.position = borderedView.center
That sets the layer's position to the center of the frame for the borderedView which is in the coordinate system of view. When you create the green triangle, it needs to use the coordinate system of borderedView.
You can fix this by passing the view to your createTriangle function, and then use the center of the bounds of that view as the layer position:
private func setupTriangles() {
view.layer.addSublayer(createTriangle(color: .red, for: view)) // RED triangle
borderedView.layer.addSublayer(createTriangle(color: .green, for: borderedView)) // GREEN triangle
}
private func createTriangle(color: UIColor, for view: UIView) -> CAShapeLayer {
let layer = CAShapeLayer()
let bezierPath = UIBezierPath()
bezierPath.move(to: CGPoint(x: 0, y: 0))
bezierPath.addLine(to: CGPoint(x: -containedFrame.width, y: 0))
bezierPath.addLine(to: CGPoint(x: 0, y: -containedFrame.height))
bezierPath.close()
layer.position = CGPoint(x: view.bounds.midX, y: view.bounds.midY)
layer.path = bezierPath.cgPath
layer.fillColor = color.cgColor
return layer
}
Note: When you do this, the green triangle appears directly below the red one, so it isn't visible.
Every view/layer uses the coordinate system of it's superview/superlayer. If you add a layer to self.view.layer, it will be positioned in self.view.layer's coordinate system. If you add a layer to borderedView.layer, it will be in borderedView.layer's coordinate system.
Think of the view/layer hierarchy as stacks of pieces of graph paper. You place a new piece of paper on the current piece (the superview/layer) in the current piece's coordinates system, but then if you draw on the new view/layer, or add new views/layer inside that one, you use the new view/layer's coordinate system.

PDFKit iOS 11: How to change the line width of Ink annotation?

I'm drawing some ink annotations on a PDF file using PDFKit. But I can't change the width of the lines. I thought that doing:
let path = UIBezierPath()
path.lineWidth = 20 // important line
path.move(to: originPoint)
path.addLine(...)
annotation.add(path)
would be enough since modifying the lineWidth of a Bezier path works when drawing in Core Graphics. But here, it does not change anything, so how to change the line width of an annotation ?
Use border property of PDFAnnotation to change thickness of UIBezierPath added to it.
let p = UIBezierPath()
p.move(to: CGPoint(x: 400, y: 200))
p.addLine(to: CGPoint(x: 500, y: 100))
p.addLine(to: CGPoint(x: 400, y: 0))
p.close()
let b = PDFBorder()
b.lineWidth = 10.0
let pageBounds = page.bounds(for: .artBox)
let inkAnnotation = PDFAnnotation(bounds: pageBounds, forType: PDFAnnotationSubtype.ink, withProperties: nil)
inkAnnotation.add(p)
inkAnnotation.border = b
inkAnnotation.color = .green
page.addAnnotation(inkAnnotation)

Changing the shape of a UIView border

I am wondering how I would go about implementing a wave-like border of a UIView. Is this possible through UIView's alone? Or would creating this appearance through a UIImageView be the way to go?
An example might be something similar to:
Thanks for your help!
Here is a code based solution that doesn't require any images. This creates a custom view using UIBezierPath to create the sine waves.
import UIKit
class WavyView: UIView {
override init(frame: CGRect) {
super.init(frame: frame)
}
required init?(coder aDecoder: NSCoder) {
return nil // TODO
}
override func draw(_ rect: CGRect) {
// Fill the whole background with the darkest blue color
UIColor(red: 0.329, green: 0.718, blue: 0.875, alpha: 1).set()
let bg = UIBezierPath(rect: rect)
bg.fill()
// Add the first sine wave filled with a very transparent white
let top1: CGFloat = 17.0
let wave1 = wavyPath(rect: CGRect(x: 0, y: top1, width: frame.width, height: frame.height - top1), periods: 1.5, amplitude: 21, start: 0.55)
UIColor(white: 1.0, alpha: 0.1).set()
wave1.fill()
// Add the second sine wave over the first
let top2: CGFloat = 34.0
let wave2 = wavyPath(rect: CGRect(x: 0, y: top2, width: frame.width, height: frame.height - top2), periods: 1.5, amplitude: 21, start: 0.9)
UIColor(white: 1.0, alpha: 0.15).set()
wave2.fill()
// Add the text
let paraAttrs = NSMutableParagraphStyle()
paraAttrs.alignment = .center
let textRect = CGRect(x: 0, y: frame.maxY - 64, width: frame.width, height: 24)
let textAttrs = [NSFontAttributeName: UIFont.boldSystemFont(ofSize: 20), NSForegroundColorAttributeName: UIColor(white: 1.0, alpha: 0.9), NSParagraphStyleAttributeName: paraAttrs]
("New user? Register here." as NSString).draw(in: textRect, withAttributes: textAttrs)
}
// This creates the desired sine wave bezier path
// rect is the area to fill with the sine wave
// periods is how may sine waves fit across the width of the frame
// amplitude is the height in points of the sine wave
// start is an offset in wavelengths for the left side of the sine wave
func wavyPath(rect: CGRect, periods: Double, amplitude: Double, start: Double) -> UIBezierPath {
let path = UIBezierPath()
// start in the bottom left corner
path.move(to: CGPoint(x: rect.minX, y: rect.maxY))
let radsPerPoint = Double(rect.width) / periods / 2.0 / Double.pi
let radOffset = start * 2 * Double.pi
let xOffset = Double(rect.minX)
let yOffset = Double(rect.minY) + amplitude
// This loops through the width of the frame and calculates and draws each point along the size wave
// Adjust the "by" value as needed. A smaller value gives smoother curve but takes longer to draw. A larger value is quicker but gives a rougher curve.
for x in stride(from: 0, to: Double(rect.width), by: 6) {
let rad = Double(x) / radsPerPoint + radOffset
let y = sin(rad) * amplitude
path.addLine(to: CGPoint(x: x + xOffset, y: y + yOffset))
}
// Add the last point on the sine wave at the right edge
let rad = Double(rect.width) / radsPerPoint + radOffset
let y = sin(rad) * amplitude
path.addLine(to: CGPoint(x: Double(rect.maxX), y: y + yOffset))
// Add line from the end of the sine wave to the bottom right corner
path.addLine(to: CGPoint(x: rect.maxX, y: rect.maxY))
// Close the path
path.close()
return path
}
}
// This creates the view with the same size as the image posted in the question
let wavy = WavyView(frame: CGRect(x: 0, y: 0, width: 502, height: 172))
The result of running this code in a Swift playground gives the following:
Obviously you can adjust any of the values in the code above to tweak the result.
The easiest approach would be to use a UIImageView. However, it is also possible by creating a custom border for the UIView but that will require a lot of code to draw the shapes.

Resources