Search code examples
vectorcalayernsview

What CALayer draw(in ctx:) coordinates should be used to draw in the correct point with the correct scale?


I have a NSView which uses CALayers to created content and want to generate pdf output in vector format. Firstly is this possible, and secondly what is the logical coordinate system that Apple refers to in their documentation, and lastly how are you supposed to get CALayers default background and border properties to draw ? If I use these properties then they are not drawn into the context and if I draw the border then this duplicates the property settings. It's a bit confusing as to when you should or shouldn't use these properties, or whether one should use CALayers at all when creating vector output e.g. pdf document.

The view has the following layers:

  • The views layer which is larger than the drawing area
  • A drawing layer which contains an image and sublayers
  • Sublayers which have some vector drawing including text

The NSView class and CALayer subclasses are listed below.

The view layer and the drawing layer are drawn in the correct locations but all the drawing layer subviews are drawn in the wrong place and are the wrong size.

I assume this is because the drawing layer has a transform applied to is and the drawing below is not taking that into account. Is this the issue and how would I apply the layers transforms to the CGContext - if this is the solution - to get things in the right place?

class DrawingView: NSView {

    override func draw(_ dirtyRect: NSRect) {
        super.draw(dirtyRect)
        guard let context = NSGraphicsContext.current?.cgContext else {
            return
        }
        // In theory should generate vector graphics
        self.layer?.draw(in: context)
        //Or
        // Generates bitmap image
        //self.layer?.render(in: context)
    }
}

class DrawingLayer: CALayer {

    override func draw(in ctx: CGContext) {
        drawBorder(in: ctx)
        if let subs = self.sublayers {
            for sub in subs {
                sub.draw(in: ctx)
            }
        }
    }
    func drawBorder(in ctx: CGContext){
        let rect = self.frame

        if let background = self.backgroundColor {
            ctx.setFillColor(background)
            ctx.fill(rect)
        }
        if let borders = self.borderColor {
            ctx.setStrokeColor(borders)
            ctx.setLineWidth(self.borderWidth)
            ctx.stroke(rect)
        }
    }
}

Solution

  • Here is my solution - using different function to draw in PDF. I am still not sure I understand why the system doesn't behave consistently when drawing to pdf and to the screen.

    EDIT: - Part of the problem it seems is that I was using the layers frame for calculating size/position in the draw() functions but when a layer gets transformed its frame changes size and shape to fit the original rotated rectangle !! So tip - save your original rectangle elsewhere and then your drawings won't suddenly go weird when you apply a rotation or use bounds!!

    EDIT2: - So render(in:) works, "kind of" - some 3D transforms get ignored, such as CATransform3DMakeTranslation(). Rotation transform seems to work fortunately so just make sure you set the correct origin to avoid the need for the translate transform.

    When drawing in the pdf the NSView's draw(dirtyRect:) function is called by the CALayers draw(in:) functions don't automatically get called so it seems you have to call them all the way up the hierarchy yourself, remembering to translate the coordinate system for each sublayer - if they are drawing in their local coordinate systems. And be aware that this does not guarantee vector output in the pdf - it seems some things like text gets rendered as bitmaps. If anyone has an explanation for how things behave and why please explain.

    In any event I have just fallen back to using the CALayer.render(in:) function which causes the full layer hierarchy to be rendered - and still with some vector and some bitmapped elements !

    class DrawingView: NSView {
    
        var drawingLayer: DrawingLayer? {
            return self.layer?.sublayers?[0] as? DrawingLayer
        }
    
        override func draw(_ dirtyRect: NSRect) {
            //super.draw(dirtyRect)
            guard let context = NSGraphicsContext.current?.cgContext else {
                return
            }
            // In theory should generate vector graphics
            self.drawingLayer?.drawPdf(in: context)
    
            //Or
            // Generates bitmap image
            //self.layer?.render(in: context)
        }
    }
    
    class DrawingLayer: CALayer {
    
       var fillColor: CGColor?
       var lineColor: CGColor?
       var lineWidth: CGFloat = 0.5
        var scale: CGFloat = 1.0
    
        // No need for this since we don't need to call draw for each sublayer
        // that gets done for us!
        override func draw(in ctx: CGContext) {
            print("drawing DrawingLayer  \(name ?? "")")
    
    //        if let subs = self.sublayers {
    //            for sub in subs {
    //                sub.draw(in: ctx)
    //            }
    //        }
        }
        func drawPdf(in ctx: CGContext) {
    
    
            ctx.saveGState()
            ctx.translateBy(x: self.frame.minX, y:  self.frame.minY)
    
           if let subs = self.sublayers {
               for sub in subs {
    
                   (sub as! NormalLayer).drawPdf(in: ctx)
               }
           }
    
            ctx.restoreGState()
        }
    }
    class NormalLayer: CALayer {
    
        var fillColor: CGColor?
           var lineColor: CGColor?
           var lineWidth: CGFloat = 0.5
        var scale: CGFloat = 1.0
    
        override func draw(in ctx: CGContext) {
            drawBorder(in: ctx)
        }
        func drawBorder(in ctx: CGContext){
            let bds = self.bounds
            let rect = CGRect(x: bds.minX*scale, y: bds.minY*scale, width: bds.width*scale, height: bds.height*scale)
            print("drawing NormalLayer \(name ?? "")  \(rect)")
    
            if let background = self.fillColor {
                ctx.setFillColor(background)
                ctx.fill(rect)
            }
            if let borders = self.lineColor {
                ctx.setStrokeColor(borders)
                ctx.setLineWidth(self.lineWidth)
                ctx.stroke(rect)
            }
        }
        func drawPdf(in ctx: CGContext) {
            ctx.saveGState()
            ctx.translateBy(x: self.frame.minX, y:  self.frame.minY)
            drawBorder(in: ctx)
            ctx.restoreGState()
        }
    }