Skip to content
This repository was archived by the owner on Dec 7, 2018. It is now read-only.

ShapeLayer's geometry semantics are weird #89

Open
andymatuschak opened this issue May 20, 2015 · 7 comments
Open

ShapeLayer's geometry semantics are weird #89

andymatuschak opened this issue May 20, 2015 · 7 comments

Comments

@andymatuschak
Copy link
Contributor

ShapeLayer is built on CAShapeLayer, from which it inherits an odd API detail: the geometry of the layer is distinct from the geometry of the path.

That is to say: if I make a shape layer and add two segments at (0, 0) and (100, 100), the bounds of the layer is unaffected by those segments' positions. Changing the layer's position doesn't change the segments' position and vice-versa. Hit testing in particular becomes a very weird thing!

I propose:

  • ShapeLayer.position { get } returns path.bounds at ShapeLayer.anchorPoint.
  • ShapeLayer.anchorPoint operates relative to path, not the full bounds of the layer.
  • Changing segments changes Layer.anchorPoint so that it reflects ShapeLayer.anchorPoint relative to the new path's bounds.
  • ShapeLayer.position { set } adds newValue's delta to each segment's points.
  • ShapeLayer.bounds { get } returns path.bounds.
  • ShapeLayer.frame { get } transforms the position and bounds overrides described above, not Layer's implementations.
  • ShapeLayer.frame { set } and ShapeLayer.bounds { set } throw an exception if the size of the new Rect is different from the current one.

Segment coordinates are specified in the ShapeLayer's superlayer's coordinate space.

@jbrennan
Copy link
Contributor

  • ShapeLayer.anchorPoint operates relative to path, not the full bounds of the layer.
  • Changing segments changes Layer.anchorPoint so that it reflects ShapeLayer.anchorPoint relative to the new path's bounds.

I think I’m a little clear on the implications of this. What does this mean practically?

@andymatuschak
Copy link
Contributor Author

Practically, this would mean that ShapeLayer.transform would behave as you expect. We need the transform to be applied with respect to the path, not the layer (which we're trying to make geometrically transparent). Consider:

const shapeLayer = new ShapeLayer()
shapeLayer.segments = [
  new Segment(new Point({x: 200, y: 100}),
  new Segment(new Point({x: 300, y: 150}),
]

shapeLayer.frame // => [200, 100, 100, 50]
shapeLayer.position // => [250, 125]
shapeLayer.anchorPoint // => [0.5, 0.5], but as an implementation detail, the CAShapeLayer's anchor point is [0.833333, 0.833333]

shapeLayer.rotationDegrees = 90
shapeLayer.frame // => [225, 75, 50, 100]
shapeLayer.position // => [250, 125]

@jbrennan
Copy link
Contributor

shapeLayer.anchorPoint // => [0.5, 0.5], but as an implementation detail, the CAShapeLayer's anchor point is [0.833333, 0.833333]

Aha, this is the surprising thing. Why is the CAShapeLayer’s anchorPoint non-default here?

@andymatuschak
Copy link
Contributor Author

Ah, because we need the layer's transform to operate about the path's center, not the layer's center.

On May 21, 2015, at 5:55 AM, Jason Brennan [email protected] wrote:

shapeLayer.anchorPoint // => [0.5, 0.5], but as an implementation detail, the CAShapeLayer's anchor point is [0.833333, 0.833333]

Aha, this is the surprising thing. Why is the CAShapeLayer’s anchorPoint non-default here?


Reply to this email directly or view it on GitHub.

@jbrennan
Copy link
Contributor

Hmmm, I thought the intention was to keep the layer’s geometry in sync with the path’s geometry?

@andymatuschak
Copy link
Contributor Author

Well, my plan was to let the CALayer's geometry sit indifferently at the origin of its parent, but your suggestion makes me realize a much simpler alternative—maybe this is what you were already imagining!

public class ShapeLayer {
    // position and anchorPoint are not overridden

    public var segments: [Segment] {
        set {
            _segments = newValue.map { (var segment) in
                segment.point -= self.position
                return segment
            }
        }
        get { 
            return _segments.map { (var segment) in 
                segment.point += self.position
                return segment
            }
        }
    }

    private var _segments: [Segment] {
        didSet {
            layer.path = computePathFromSegments(_segments)
        }
    }

    public override var bounds: Rect {
        get {
            return Rect(origin: super.bounds.origin, size: Size(CGPathGetPathBoundingBox(layer.path).size))
        }
        set {
            precondition(newValue.size == super.bounds.size)
            super.bounds.origin = newValue.origin
        }
    }

    public override var frame: Rect {
        get {
            var bounds = Rect(CGPathGetPathBoundingBox(layer.path))
            var frame = /* apply layer.transform to bounds */
            frame.origin += position
            return frame
        }
        set {
            precondition(newValue.size == super.frame.size)
            position += /* transform (newValue.origin - frame.origin) by layer.transform */
        }
    }
}

@jbrennan
Copy link
Contributor

Yeah this seems about right to me.

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants