Skip to content

Instantly share code, notes, and snippets.

@Peter-Schorn
Last active February 4, 2022 01:03
Show Gist options
  • Select an option

  • Save Peter-Schorn/09d6a002b07d59efeb403abab97fa2ee to your computer and use it in GitHub Desktop.

Select an option

Save Peter-Schorn/09d6a002b07d59efeb403abab97fa2ee to your computer and use it in GitHub Desktop.

Revisions

  1. Peter-Schorn revised this gist Feb 4, 2022. 1 changed file with 549 additions and 149 deletions.
    698 changes: 549 additions & 149 deletions XShape.swift
    Original file line number Diff line number Diff line change
    @@ -1,228 +1,628 @@
    import SwiftUI

    /**
    An X shape.

    To ensure the shape is square, add the following modifier:
    ```
    .aspectRatio(1, contentMode: .fit)
    ```
    */
    public struct XShape: InsettableShape {

    /// The thickness of the legs.
    public enum Thickness {

    /// An absolute thickness
    case absolute(CGFloat)

    /**
    A relative Thickness. Must be between 0 and 1.

    This will be multiplied by the smallest dimension of the bounding
    frame to compute the absolute thickness
    */
    case relative(CGFloat)

    }

    /// The thickness of the legs.
    public let thickness: Thickness

    public init(thickness: Thickness = .relative(0.1)) {
    /**
    The angle between the horizontal and the right leg.

    If `nil`, then the angle will be equal to that of the angle between the
    horizonal and diagonal line starting from the bottom left
    corner and extending towards the top right corner.
    */
    public let legsAngle: Angle?

    private var inset: CGFloat = 0

    /**
    Creates an Xshape.

    - Parameters:
    - thickness: The thickness of the legs.
    - legsAngle: The angle between the horizontal and the right leg. If `nil`, then the angle will be equal to equal to that of the angle
    between the horizonal and diagonal line starting from the bottom
    left corner and extending towards the top right corner.
    */
    public init(
    thickness: Thickness = .relative(0.1),
    legsAngle: Angle? = nil
    ) {
    if case .relative(let relativeThickness) = thickness {
    precondition(
    (0...1).contains(relativeThickness),
    """
    relative thickness must be between 0 and 1 \
    (got \(relativeThickness))"
    """
    )
    }
    self.thickness = thickness
    if let legsAngle = legsAngle {
    let degrees = legsAngle.degrees
    precondition(
    (0...90).contains(degrees),
    "legsAngle must be between 0 and 90 degrees (got \(degrees))"
    )
    }
    self.legsAngle = legsAngle
    }

    private var inset: CGFloat = 0

    public func inset(by amount: CGFloat) -> Self {
    var shape = self
    shape.inset += amount
    return shape
    }

    public func path(in rect: CGRect) -> Path {

    // Inset the shape
    let rect = rect.insetBy(dx: self.inset, dy: self.inset)
    return self.pathInRectCore(rect)
    }

    /// The thickness of the legs.
    private func legsWidth(rect: CGRect) -> CGFloat {
    let minDimension = min(rect.width, rect.height)
    switch self.thickness {
    case .absolute(let thickness):
    return min(thickness, minDimension)
    case .relative(let thickness):
    return min(minDimension * thickness, minDimension)
    }
    }

    /// The angle from the horizontal to the diagonal line starting at the
    /// bottom left corner and extending to the top right corner.
    private func diagonalAngle(rect: CGRect) -> Angle {
    let radians = atan(rect.height / rect.width)
    return .radians(radians)
    }

    /// The angle from the horizontal to the right leg.
    private func legsAngle(rect: CGRect) -> Angle {

    // Make the rect square, centered on the inset rect
    let squareRect: CGRect
    if let legsAngle = self.legsAngle {
    return legsAngle
    }
    return self.diagonalAngle(rect: rect)
    }

    /// The offset of the four vertices near the center *from* the center of the
    /// shape.
    private func centerVerticesOffset(
    legsAngle: Double,
    rect: CGRect,
    legsWidth: CGFloat
    ) -> CGVector {
    let angle = Double.pi / 2 - 2 * legsAngle
    let dy = (legsWidth / cos(angle)) * sin(legsAngle)
    let dx = (legsWidth / cos(angle)) * cos(legsAngle)
    return CGVector(dx: abs(dx), dy: abs(dy))
    }

    /// The offset of the corner vertices from the corner circle center.
    private func cornerVerticesOffset(
    legsAngle: Double,
    rect: CGRect,
    circleRadius: CGFloat
    ) -> CGVector {
    let dx = circleRadius * sin(legsAngle)
    let dy = circleRadius * cos(legsAngle)
    return CGVector(dx: abs(dx), dy: abs(dy))
    }

    /// The offset of the center of the circle near the corners of the bounding
    /// frame *from* the center of the frame.
    private func cornerCircleOffsetFromCenter(
    legsAngle: Double,
    rect: CGRect,
    circleRadius: CGFloat
    ) -> CGVector {

    if rect.width == rect.height {
    squareRect = rect
    var dx: CGFloat
    var dy: CGFloat

    let diagonalAngle = self.diagonalAngle(rect: rect).radians

    if legsAngle <= diagonalAngle {
    dx = rect.width / 2 - circleRadius
    dy = dx * tan(legsAngle)
    if abs(dy) + circleRadius > rect.height / 2 {
    dy = rect.height / 2 - circleRadius
    dx = dy / tan(legsAngle)
    }
    }
    else /* if legsAngle > diagonalAngle */ {
    dy = rect.height / 2 - circleRadius
    dx = dy / tan(legsAngle)
    if abs(dx) + circleRadius > rect.width / 2 {
    dx = rect.width / 2 - circleRadius
    dy = dx * tan(legsAngle)
    }
    }

    else {
    let smallestDimension = min(rect.width, rect.height)

    squareRect = CGRect(
    x: (rect.width - smallestDimension) / 2,
    y: (rect.height - smallestDimension) / 2,
    width: smallestDimension,
    height: smallestDimension
    )
    return CGVector(dx: abs(dx), dy: abs(dy))

    }

    /**
    Finds the angle between two points on the perimeter of a circle.

    [Source](https://math.stackexchange.com/a/185844/825630)
    */
    private func angleBetweenPoints(
    point1: CGPoint,
    point2: CGPoint,
    radius: CGFloat
    ) -> Angle {

    /// The distance between the two points squared
    let distance2 =
    pow(point2.x - point1.x, 2) +
    pow(point2.y - point1.y, 2)


    /// 2r^2
    let r22 = (2 * pow(radius, 2))

    let radians = acos((r22 - distance2) / r22)

    return .radians(radians)
    }

    /**
    Finds the points that intersect two circles.

    If the circles only intersect at one point, then the second point will be
    `nil`.

    If both circles have the same center point, then returns `nil`.

    Sources:

    [gist](https://gist.github.com/jupdike/bfe5eb23d1c395d8a0a1a4ddd94882ac)

    [stackexchange](https://math.stackexchange.com/a/1367732/825630)

    - Parameters:
    - center1: The center of the first circle.
    - radius1: The radius of the first circle.
    - center2: The center of the second circle.
    - radius2: The radius of the second circle.
    */
    private func intersectingPointsOfCircles(
    center1: CGPoint,
    radius1: CGFloat,
    center2: CGPoint,
    radius2: CGFloat
    ) -> (CGPoint, CGPoint?)? {

    if center1 == center2 {
    // If the centers are the same and the radii are the same, then the
    // circles intersect at an infinite number of points, so return
    // `nil`.
    //
    // If the centers are the same, but the radii are different, then
    // there can't be any intersecting points, so also return `nil`.
    return nil
    }

    let centerDx = center1.x - center2.x
    let centerDy = center1.y - center2.y

    /// The distance between the centers of the circles
    let d = sqrt(pow(centerDx, 2) + pow(centerDy, 2))

    if abs(radius1 - radius2) > d || d > radius1 + radius2 {
    return nil
    }

    return self.pathInRectCore(squareRect)
    let d2 = d * d
    let d4 = d2 * d2
    let a = (radius1 * radius1 - radius2 * radius2) / (2 * d2)
    let r2r2 = (radius1 * radius1 - radius2 * radius2)
    let c = sqrt(
    2 * (radius1 * radius1 + radius2 * radius2) /
    d2 - (r2r2 * r2r2) / d4 - 1
    )

    let fx = (center1.x + center2.x) / 2 + a * (center2.x - center1.x)
    let gx = c * (center2.y - center1.y) / 2
    let ix1 = fx + gx
    let ix2 = fx - gx

    let fy = (center1.y + center2.y) / 2 + a * (center2.y - center1.y)
    let gy = c * (center1.x - center2.x) / 2
    let iy1 = fy + gy
    let iy2 = fy - gy

    // if gy == 0 and gx == 0, then the circles are tangent and there
    // is only one solution

    let intersectingPoint1 = CGPoint(x: ix1, y: iy1)
    let intersectingPoint2 = CGPoint(x: ix2, y: iy2)

    if intersectingPoint1 == intersectingPoint2 {
    return (intersectingPoint1, nil)
    }
    return (intersectingPoint1, intersectingPoint2)

    }

    private func intersectingPointsOfCircles(
    center1: CGPoint,
    radius1: CGFloat,
    center2: CGPoint
    ) -> (CGPoint, CGPoint?)? {
    self.intersectingPointsOfCircles(
    center1: center1,
    radius1: radius1,
    center2: center2,
    radius2: radius1
    )
    }

    /// The rect is already inset and square
    // MARK: Path In Rect Core

    /// The rect is already inset.
    private func pathInRectCore(_ rect: CGRect) -> Path {

    var path = Path()

    let lineWidth: CGFloat
    /// Radians

    switch thickness {
    case .absolute(let thickness):
    lineWidth = thickness
    case .relative(let thickness):
    lineWidth = rect.width * thickness
    }
    let legsWidth = self.legsWidth(rect: rect)

    if lineWidth == 0 {
    if legsWidth == 0 {
    return path
    }

    /// Radians
    let legsAngle = self.legsAngle(rect: rect).radians

    let minDimension = min(rect.width, rect.height)

    /// 0 and 90 degrees
    let rightAngles = [0, Double.pi / 2]
    let legsAngleIs0Or90 = rightAngles.contains(legsAngle)

    if legsWidth >= minDimension && !legsAngleIs0Or90 {
    return Circle()
    // so that the path always starts at the top left corner
    .rotation(.degrees(180))
    .path(in: rect)
    }

    let circleRadius = lineWidth / 2
    let circleRadius = legsWidth / 2

    let cornerCircleOffsetFromCenter = self.cornerCircleOffsetFromCenter(
    legsAngle: legsAngle,
    rect: rect,
    circleRadius: circleRadius
    )

    let centerVerticesOffset = self.centerVerticesOffset(
    legsAngle: legsAngle,
    rect: rect,
    legsWidth: legsWidth
    )

    let cornerVerticesOffset = self.cornerVerticesOffset(
    legsAngle: legsAngle,
    rect: rect,
    circleRadius: circleRadius
    )

    // MARK: Top Left
    let topLeftCircleCenter = CGPoint(
    x: rect.midX - cornerCircleOffsetFromCenter.dx,
    y: rect.midY - cornerCircleOffsetFromCenter.dy
    )
    var topLeftCorner1 = CGPoint(
    x: topLeftCircleCenter.x - cornerVerticesOffset.dx,
    y: topLeftCircleCenter.y + cornerVerticesOffset.dy
    )
    var topLeftCorner2 = CGPoint(
    x: topLeftCircleCenter.x + cornerVerticesOffset.dx,
    y: topLeftCircleCenter.y - cornerVerticesOffset.dy
    )
    var topLeftStartAngle = Double.pi / 2 + legsAngle
    var topLeftEndAngle = topLeftStartAngle + Double.pi

    // MARK: Top Center
    var topCenter = rect.center
    topCenter.y -= centerVerticesOffset.dy

    // MARK: Top Right
    let topRightCircleCenter = CGPoint(
    x: rect.midX + cornerCircleOffsetFromCenter.dx,
    y: rect.midY - cornerCircleOffsetFromCenter.dy
    )
    var topRightCorner1 = CGPoint(
    x: topRightCircleCenter.x - cornerVerticesOffset.dx,
    y: topRightCircleCenter.y - cornerVerticesOffset.dy
    )
    // var topRightCorner2 = CGPoint(
    // x: topRightCircleCenter.x + cornerVerticesOffset.dx,
    // y: topRightCircleCenter.y + cornerVerticesOffset.dy
    // )
    var topRightStartAngle = 3 * Double.pi / 2 - legsAngle
    var topRightEndAngle = topRightStartAngle + Double.pi

    // MARK: Right Center
    var rightCenter = rect.center
    rightCenter.x += centerVerticesOffset.dx

    // MARK: Bottom Right
    let bottomRightCircleCenter = CGPoint(
    x: rect.midX + cornerCircleOffsetFromCenter.dx,
    y: rect.midY + cornerCircleOffsetFromCenter.dy
    )
    var bottomRightCorner1 = CGPoint(
    x: bottomRightCircleCenter.x + cornerVerticesOffset.dx,
    y: bottomRightCircleCenter.y - cornerVerticesOffset.dy
    )
    // var bottomRightCorner2 = CGPoint(
    // x: bottomRightCircleCenter.x - cornerVerticesOffset.dx,
    // y: bottomRightCircleCenter.y + cornerVerticesOffset.dy
    // )
    var bottomRightStartAngle = 3 * Double.pi / 2 + legsAngle
    var bottomRightEndAngle = bottomRightStartAngle + Double.pi

    // MARK: Bottom Center
    var bottomCenter = rect.center
    bottomCenter.y += centerVerticesOffset.dy

    // MARK: Bottom Left
    let bottomLeftCircleCenter = CGPoint(
    x: rect.midX - cornerCircleOffsetFromCenter.dx,
    y: rect.midY + cornerCircleOffsetFromCenter.dy
    )
    var bottomLeftCorner1 = CGPoint(
    x: bottomLeftCircleCenter.x + cornerVerticesOffset.dx,
    y: bottomLeftCircleCenter.y + cornerVerticesOffset.dy
    )
    var bottomLeftCorner2 = CGPoint(
    x: bottomLeftCircleCenter.x - cornerVerticesOffset.dx,
    y: bottomLeftCircleCenter.y - cornerVerticesOffset.dy
    )
    var bottomLeftStartAngle = Double.pi / 2 - legsAngle
    var bottomLeftEndAngle = bottomLeftStartAngle + Double.pi

    // MARK: Left Center
    var leftCenter = rect.center
    leftCenter.x -= centerVerticesOffset.dx

    let drawLeftCenter = bottomLeftCorner2.y > topLeftCorner1.y &&
    !legsAngleIs0Or90

    let triangleLeg = sqrt(pow(lineWidth / 2, 2) / 2)
    let drawTopCenter = topLeftCorner2.x < topRightCorner1.x &&
    !legsAngleIs0Or90

    // let drawRightCenter = topRightCorner2.y < bottomRightCorner1.y &&
    // !legsAngleIs0Or90
    let drawRightCenter = drawLeftCenter

    // let drawBottomCenter = bottomRightCorner2.x > bottomLeftCorner1.x &&
    // !legsAngleIs0Or90
    let drawBottomCenter = drawTopCenter

    /// The offset from the center for each of the points
    /// near the center of the shape.
    let centerOffset = sqrt(pow(lineWidth, 2) / 2)
    // MARK: - Draw -

    do {
    // MARK: Top Left
    let topLeftCircleCenter = CGPoint(
    x: rect.minX + lineWidth / 2,
    y: rect.minY + lineWidth / 2
    )
    let topLeftCorner1 = CGPoint(
    x: topLeftCircleCenter.x - triangleLeg,
    y: topLeftCircleCenter.y + triangleLeg
    )
    // let topLeftCorner2 = CGPoint(
    // x: topLeftCircleCenter.x + triangleLeg,
    // y: topLeftCircleCenter.y - triangleLeg
    // )
    path.move(to: topLeftCorner1)
    path.addArc(
    center: topLeftCircleCenter,
    radius: circleRadius,
    startAngle: .degrees(135),
    endAngle: .degrees(315),
    clockwise: false
    )
    // MARK: Prevent Overlapping Arcs
    if !legsAngleIs0Or90 {

    if !drawLeftCenter {

    // prevent arcs from overlapping on the left
    let intersectingPointsLeft = self.intersectingPointsOfCircles(
    center1: bottomLeftCircleCenter,
    radius1: circleRadius,
    center2: topLeftCircleCenter
    )
    if let intersectingPoints = intersectingPointsLeft {
    let leftIntersectingPoint: CGPoint
    if let p2 = intersectingPoints.1,
    p2.x < intersectingPoints.0.x {
    leftIntersectingPoint = p2
    }
    else {
    leftIntersectingPoint = intersectingPoints.0
    }

    let angle = self.angleBetweenPoints(
    point1: topLeftCorner1,
    point2: leftIntersectingPoint,
    radius: circleRadius
    )
    topLeftStartAngle += angle.radians
    topLeftCorner1 = leftIntersectingPoint
    bottomLeftCorner2 = leftIntersectingPoint

    bottomLeftEndAngle -= angle.radians

    // prevent arcs from overlapping on the right
    let rightIntersectingPoint = CGPoint(
    x: 2 * rect.midX - leftIntersectingPoint.x,
    y: leftIntersectingPoint.y
    )

    topRightEndAngle -= angle.radians
    bottomRightCorner1 = rightIntersectingPoint
    // topRightCorner2 = rightIntersectingPoint
    bottomRightStartAngle += angle.radians

    }


    }

    if !drawTopCenter {
    // prevent arcs from overlapping on the top
    let intersectingPointsTop = self.intersectingPointsOfCircles(
    center1: topLeftCircleCenter,
    radius1: circleRadius,
    center2: topRightCircleCenter
    )
    if let intersectingPoints = intersectingPointsTop {
    let topIntersectingPoint: CGPoint
    if let p2 = intersectingPoints.1,
    p2.y < intersectingPoints.0.y {
    topIntersectingPoint = p2
    }
    else {
    topIntersectingPoint = intersectingPoints.0
    }

    let angle = self.angleBetweenPoints(
    point1: topIntersectingPoint,
    point2: topLeftCorner2,
    radius: circleRadius
    )
    topLeftEndAngle -= angle.radians
    topRightCorner1 = topIntersectingPoint
    topLeftCorner2 = topIntersectingPoint

    topRightStartAngle += angle.radians

    // prevent arcs from overlapping on the bottom
    let bottomIntersectingPoint = CGPoint(
    x: topIntersectingPoint.x,
    y: 2 * rect.midY - topIntersectingPoint.y
    )

    bottomRightEndAngle -= angle.radians
    bottomLeftCorner1 = bottomIntersectingPoint
    // bottomRightCorner2 = bottomIntersectingPoint
    bottomLeftStartAngle += angle.radians

    }
    }

    }

    do {
    // MARK: Top Center
    var center = rect.center
    center.y -= centerOffset
    path.addLine(to: center)

    }
    // MARK: Top Left

    path.move(to: topLeftCorner1)
    // path.addLine(to: topLeftCircleCenter)
    // path.addLine(to: topLeftCorner2)

    do {
    // MARK: Top Right
    let topRightCircleCenter = CGPoint(
    x: rect.maxX - lineWidth / 2,
    y: rect.minY + lineWidth / 2
    )
    let topRightCorner1 = CGPoint(
    x: topRightCircleCenter.x - triangleLeg,
    y: topRightCircleCenter.y - triangleLeg
    )
    // let topRightCorner2 = CGPoint(
    // x: topRightCircleCenter.x + triangleLeg,
    // y: topRightCircleCenter.y + triangleLeg
    // )
    path.addArc(
    center: topLeftCircleCenter,
    radius: circleRadius,
    startAngle: .radians(topLeftStartAngle),
    endAngle: .radians(topLeftEndAngle),
    clockwise: false
    )

    // MARK: Top Center
    if drawTopCenter {
    path.addLine(to: topCenter)
    }

    // MARK: Top Right
    if legsAngle != Double.pi / 2 {
    path.addLine(to: topRightCorner1)
    // path.addLine(to: topRightCircleCenter)
    // path.addLine(to: topRightCorner2)
    path.addArc(
    center: topRightCircleCenter,
    radius: circleRadius,
    startAngle: .degrees(225),
    endAngle: .degrees(45),
    startAngle: .radians(topRightStartAngle),
    endAngle: .radians(topRightEndAngle),
    clockwise: false
    )
    }


    do {
    // MARK: Right Center
    var center = rect.center
    center.x += centerOffset
    path.addLine(to: center)
    }

    do {
    // MARK: Bottom Right
    let bottomRightCircleCenter = CGPoint(
    x: rect.maxX - lineWidth / 2,
    y: rect.maxY - lineWidth / 2
    )
    let bottomRightCorner1 = CGPoint(
    x: bottomRightCircleCenter.x + triangleLeg,
    y: bottomRightCircleCenter.y - triangleLeg
    )
    // let bottomRightCorner2 = CGPoint(
    // x: bottomRightCircleCenter.x - triangleLeg,
    // y: bottomRightCircleCenter.y + triangleLeg
    // )
    path.addLine(to: bottomRightCorner1)
    path.addArc(
    center: bottomRightCircleCenter,
    radius: circleRadius,
    startAngle: .degrees(315),
    endAngle: .degrees(135),
    clockwise: false
    )
    // If the angle is zero, then we only need to draw the top half
    // of each leg
    if legsAngle == 0 {
    // close the path by adding a line back to the starting point
    path.addLine(to: topLeftCorner1)
    path.closeSubpath()
    return path
    }

    do {
    // MARK: Bottom Center
    var center = rect.center
    center.y += centerOffset
    path.addLine(to: center)
    // MARK: Right Center
    if drawRightCenter {
    path.addLine(to: rightCenter)
    }

    do {
    // MARK: Bottom Left
    let bottomLeftCircleCenter = CGPoint(
    x: rect.minX + lineWidth / 2,
    y: rect.maxY - lineWidth / 2
    )
    let bottomLeftCorner1 = CGPoint(
    x: bottomLeftCircleCenter.x + triangleLeg,
    y: bottomLeftCircleCenter.y + triangleLeg
    )
    // let bottomLeftCorner2 = CGPoint(
    // x: bottomLeftCircleCenter.x - triangleLeg,
    // y: bottomLeftCircleCenter.y - triangleLeg
    // )
    // MARK: Bottom Right
    path.addLine(to: bottomRightCorner1)
    // path.addLine(to: bottomRightCircleCenter)
    // path.addLine(to: bottomRightCorner2)
    path.addArc(
    center: bottomRightCircleCenter,
    radius: circleRadius,
    startAngle: .radians(bottomRightStartAngle),
    endAngle: .radians(bottomRightEndAngle),
    clockwise: false
    )

    // MARK: Bottom Center
    if drawBottomCenter {
    path.addLine(to: bottomCenter)
    }

    // MARK: Bottom Left
    if legsAngle != Double.pi / 2 {
    path.addLine(to: bottomLeftCorner1)
    // path.addLine(to: bottomLeftCircleCenter)
    // path.addLine(to: bottomLeftCorner2)
    path.addArc(
    center: bottomLeftCircleCenter,
    radius: circleRadius,
    startAngle: .degrees(45),
    endAngle: .degrees(225),
    startAngle: .radians(bottomLeftStartAngle),
    endAngle: .radians(bottomLeftEndAngle),
    clockwise: false
    )
    }

    do {
    // MARK: Left Center
    var center = rect.center
    center.x -= centerOffset
    path.addLine(to: center)
    // MARK: Left Center
    if drawLeftCenter {
    path.addLine(to: leftCenter)
    }

    return path

    // close the path by adding a line back to the starting point
    path.addLine(to: topLeftCorner1)
    path.closeSubpath()

    return path

    }

    }


    extension CGRect {

    var center: CGPoint {
    CGPoint(x: self.midX, y: self.midY)
    }

    }

    struct XShape_Previews: PreviewProvider {
    static var previews: some View {

    XShape()
    .frame(width: 200, height: 200)
    .padding(2)
    .border(Color.green.opacity(0.5), width: 2)
    .padding()

    }
    }
    }
  2. Peter-Schorn revised this gist Jan 27, 2022. 1 changed file with 1 addition and 1 deletion.
    2 changes: 1 addition & 1 deletion XShape.swift
    Original file line number Diff line number Diff line change
    @@ -218,7 +218,7 @@ extension CGRect {
    struct XShape_Previews: PreviewProvider {
    static var previews: some View {

    XShape(thickness: .absolute(20))
    XShape()
    .frame(width: 200, height: 200)
    .padding(2)
    .border(Color.green.opacity(0.5), width: 2)
  3. Peter-Schorn created this gist Jan 27, 2022.
    228 changes: 228 additions & 0 deletions XShape.swift
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,228 @@
    import SwiftUI

    public struct XShape: InsettableShape {

    public enum Thickness {
    case absolute(CGFloat)
    case relative(CGFloat)
    }

    public let thickness: Thickness

    public init(thickness: Thickness = .relative(0.1)) {
    self.thickness = thickness
    }

    private var inset: CGFloat = 0

    public func inset(by amount: CGFloat) -> Self {
    var shape = self
    shape.inset += amount
    return shape
    }

    public func path(in rect: CGRect) -> Path {

    // Inset the shape
    let rect = rect.insetBy(dx: self.inset, dy: self.inset)

    // Make the rect square, centered on the inset rect
    let squareRect: CGRect

    if rect.width == rect.height {
    squareRect = rect
    }

    else {
    let smallestDimension = min(rect.width, rect.height)

    squareRect = CGRect(
    x: (rect.width - smallestDimension) / 2,
    y: (rect.height - smallestDimension) / 2,
    width: smallestDimension,
    height: smallestDimension
    )
    }

    return self.pathInRectCore(squareRect)

    }

    /// The rect is already inset and square
    private func pathInRectCore(_ rect: CGRect) -> Path {

    var path = Path()

    let lineWidth: CGFloat

    switch thickness {
    case .absolute(let thickness):
    lineWidth = thickness
    case .relative(let thickness):
    lineWidth = rect.width * thickness
    }

    if lineWidth == 0 {
    return path
    }

    let circleRadius = lineWidth / 2

    let triangleLeg = sqrt(pow(lineWidth / 2, 2) / 2)

    /// The offset from the center for each of the points
    /// near the center of the shape.
    let centerOffset = sqrt(pow(lineWidth, 2) / 2)

    do {
    // MARK: Top Left
    let topLeftCircleCenter = CGPoint(
    x: rect.minX + lineWidth / 2,
    y: rect.minY + lineWidth / 2
    )
    let topLeftCorner1 = CGPoint(
    x: topLeftCircleCenter.x - triangleLeg,
    y: topLeftCircleCenter.y + triangleLeg
    )
    // let topLeftCorner2 = CGPoint(
    // x: topLeftCircleCenter.x + triangleLeg,
    // y: topLeftCircleCenter.y - triangleLeg
    // )
    path.move(to: topLeftCorner1)
    path.addArc(
    center: topLeftCircleCenter,
    radius: circleRadius,
    startAngle: .degrees(135),
    endAngle: .degrees(315),
    clockwise: false
    )

    }

    do {
    // MARK: Top Center
    var center = rect.center
    center.y -= centerOffset
    path.addLine(to: center)

    }

    do {
    // MARK: Top Right
    let topRightCircleCenter = CGPoint(
    x: rect.maxX - lineWidth / 2,
    y: rect.minY + lineWidth / 2
    )
    let topRightCorner1 = CGPoint(
    x: topRightCircleCenter.x - triangleLeg,
    y: topRightCircleCenter.y - triangleLeg
    )
    // let topRightCorner2 = CGPoint(
    // x: topRightCircleCenter.x + triangleLeg,
    // y: topRightCircleCenter.y + triangleLeg
    // )
    path.addLine(to: topRightCorner1)
    path.addArc(
    center: topRightCircleCenter,
    radius: circleRadius,
    startAngle: .degrees(225),
    endAngle: .degrees(45),
    clockwise: false
    )
    }


    do {
    // MARK: Right Center
    var center = rect.center
    center.x += centerOffset
    path.addLine(to: center)
    }

    do {
    // MARK: Bottom Right
    let bottomRightCircleCenter = CGPoint(
    x: rect.maxX - lineWidth / 2,
    y: rect.maxY - lineWidth / 2
    )
    let bottomRightCorner1 = CGPoint(
    x: bottomRightCircleCenter.x + triangleLeg,
    y: bottomRightCircleCenter.y - triangleLeg
    )
    // let bottomRightCorner2 = CGPoint(
    // x: bottomRightCircleCenter.x - triangleLeg,
    // y: bottomRightCircleCenter.y + triangleLeg
    // )
    path.addLine(to: bottomRightCorner1)
    path.addArc(
    center: bottomRightCircleCenter,
    radius: circleRadius,
    startAngle: .degrees(315),
    endAngle: .degrees(135),
    clockwise: false
    )
    }

    do {
    // MARK: Bottom Center
    var center = rect.center
    center.y += centerOffset
    path.addLine(to: center)
    }

    do {
    // MARK: Bottom Left
    let bottomLeftCircleCenter = CGPoint(
    x: rect.minX + lineWidth / 2,
    y: rect.maxY - lineWidth / 2
    )
    let bottomLeftCorner1 = CGPoint(
    x: bottomLeftCircleCenter.x + triangleLeg,
    y: bottomLeftCircleCenter.y + triangleLeg
    )
    // let bottomLeftCorner2 = CGPoint(
    // x: bottomLeftCircleCenter.x - triangleLeg,
    // y: bottomLeftCircleCenter.y - triangleLeg
    // )
    path.addLine(to: bottomLeftCorner1)
    path.addArc(
    center: bottomLeftCircleCenter,
    radius: circleRadius,
    startAngle: .degrees(45),
    endAngle: .degrees(225),
    clockwise: false
    )
    }

    do {
    // MARK: Left Center
    var center = rect.center
    center.x -= centerOffset
    path.addLine(to: center)
    }

    return path

    }

    }

    extension CGRect {

    var center: CGPoint {
    CGPoint(x: self.midX, y: self.midY)
    }

    }

    struct XShape_Previews: PreviewProvider {
    static var previews: some View {

    XShape(thickness: .absolute(20))
    .frame(width: 200, height: 200)
    .padding(2)
    .border(Color.green.opacity(0.5), width: 2)
    .padding()

    }
    }