Reputation: 1640
I experienced that for some NSBezierPath
s SCNShape
seems to be unable to draw a shape.
The path is created only using line(to:)
.
//...set up scene...
//Create path (working)
let path = NSBezierPath()
path.move(to: CGPoint.zero)
path.line(to: NSMakePoint(0.000000, 0.000000))
path.line(to: NSMakePoint(0.011681, 0.029526))
// more points ...
path.close()
// Make a 3D shape (not working)
let shape = SCNShape(path: path, extrusionDepth: 10)
shape.firstMaterial?.diffuse.contents = NSColor.green
let node = SCNNode(geometry: shape)
root.addChildNode(node)
For verifying that the general process of creating a SCNShape
is correct, I also drew a blue shape that only differs by having different points. The blue shape gets drawn, the green shape doesn't.
You can find a playground containing the full example here. In the example you should be able to see a green and a blue shape in assistant editor. But only the blue shape gets drawn.
Do you have any idea why the green shape is not shown?
Upvotes: 3
Views: 998
Reputation: 6278
In no way does this compare to Rikster's answer, but there is another way to prevent this kind of problem. It's a commercial way, and there's probably freeware apps that do similar, but this is one I'm used to using, that does this quite well.
What is 'this' that I'm talking about?
The conversion of drawings to code, by an app called PaintCode. This will allow you to see your paths and be sure they have none of the conflicts that Rickster pointed out are your issue.
Check it out here: https://www.paintcodeapp.com/
Other options are listed in answers here: How to import/parse SVG into UIBezierpaths, NSBezierpaths, CGPaths?
Upvotes: 2
Reputation: 126107
The short story: your path has way more points than it needs to, leading you to unexpected, hard to find geometric problems.
Note this bit in the documentation:
The result of extruding a self-intersecting path is undefined.
As it turns out, somewhere in the first 8 or so points, your "curve" makes enough of a turn the wrong way that the line closing the path (between the first point in the path 0,0
, and the last point 32.366829, 29.713470
) intersects the rest of the path. Here's an attempt at making it visible by excluding all but the first few points and the last point from a playground render (see that tiny little zigzag in the bottom left corner):
And at least on some SceneKit versions/renderers, when it tries to make a mesh out of a self-intersecting path it just gives up and makes nothing.
However, you really don't need that many points to make your path look good. Here it is if you use 1x, 1/5x, and 1/10x as many points:
If you exclude enough points overall, and/or skip the few at the beginning that make your curve zag where it should zig, SceneKit renders the shape just fine:
Some tips from diagnosing the problem:
When working with lots of coordinate data like this, I like to use ExpressibleByArrayLiteral
so I can easily build an array of lots of points/vectors/etc:
extension CGPoint: ExpressibleByArrayLiteral {
public init(arrayLiteral elements: CGFloat...) {
precondition(elements.count == 2)
self.init(x: elements.first!, y: elements.last!)
}
}
var points: [CGPoint] = [
[0.000000, 0.000000],
[0.011681, 0.029526],
// ...
]
That gets me an array (and a lot less typing out things like NSPointMake
over and over), so I can slice and dice the data to figure out what's wrong with it. (For example, one of my early theories was that there might be something about negative coordinates, so I did some map
and min()
to find the most-negative X and Y values, then some more map
to make an array where all points are offset by a constant amount.)
Now, to make paths using arrays of points, I make an extension on NSBezierPath
:
extension NSBezierPath {
convenience init(linesBetween points: [CGPoint], stride: Int = 1) {
precondition(points.count > 1)
self.init()
move(to: points.first! )
for i in Swift.stride(from: 1, to: points.count, by: stride) {
line(to: points[i])
}
}
}
With this, I can easily create paths from not just entire arrays of points, but also...
paths that skip parts of the original array (with the stride
parameter)
let path5 = NSBezierPath(linesBetween: points, stride: 5)
let path10 = NSBezierPath(linesBetween: points, stride: 10)
(This is handy for generating playground previews a bit more quickly, too.)
paths that use some chunk or slice of the original array
let zigzag = NSBezierPath(linesBetween: Array(points.prefix(to:10)) + [points.last!])
let lopOffBothEnds = NSBezierPath(linesBetween: Array(points[1 ..< points.count < 1]))
Or both... the winning entry (in the screenshot above) is:
let path = NSBezierPath(linesBetween: Array(points.suffix(from: 10)), stride: 5)
You can get a (marginally) better render out of having more points in your path, but an even better way to do it would be to make a path out of curves instead of lines. For extra credit, try extending the NSBezierPath(linesBetween:)
initializer above to add curves by keeping every n
th point as part of the path while using a couple of the intermediary points as control handles. (It's no general purpose auto trace algorithm, but might be good enough for cases like this.)
Upvotes: 8