Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
signalapp
GitHub Repository: signalapp/Signal-iOS
Path: blob/main/Signal/QRCodes/Bitmaps/Bitmaps+LineDrawing.swift
1 views
//
// Copyright 2023 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//

import Foundation

extension Bitmaps {
    /// A drawing composed of a rectilinear series of line segments.
    ///
    /// Grid drawings have origin at the bottom-left, matching the default
    /// CoreGraphics context orientation.
    struct GridDrawing: Equatable {
        struct Segment: Hashable, Equatable, CustomDebugStringConvertible {
            enum Dimension: Equatable {
                case vertical
                case horizontal
            }

            let dimension: Dimension
            let start: Point
            let length: Int

            /// Create a new segment.
            ///
            /// - Parameter length
            /// If this value is equal to one, the start and end of the segment
            /// are the same point.
            init(dimension: Dimension, start: Point, length: Int) {
                self.dimension = dimension
                self.start = start
                self.length = length
            }

            var debugDescription: String {
                return "\n{ \(dimension), \(start), \(length) }"
            }

            /// The end of the segment.
            var end: Point {
                return start(offsetBy: length - 1)
            }

            /// The start of the segment offset by the given amount in the
            /// segment's dimension.
            func start(offsetBy offset: Int) -> Point {
                switch dimension {
                case .horizontal:
                    return Point(x: start.x + offset, y: start.y)
                case .vertical:
                    return Point(x: start.x, y: start.y + offset)
                }
            }
        }

        /// The width of the drawing, in pixels.
        let width: Int

        /// The height of the drawing, in pixels.
        let height: Int

        /// The segments comprising the drawing.
        let segments: Set<Segment>
    }
}

extension Bitmaps.Image {
    private typealias Segment = Bitmaps.GridDrawing.Segment
    private typealias Point = Bitmaps.Point

    /// Merges adjacent pixels in the bitmap to create a line drawing.
    ///
    /// Specifically, returns a set of segments such that:
    /// - For a horizontal segment with start at `{X,Y}` and length `N` all
    ///   pixels in the inclusive range `{X,Y}:{X+N,Y}` are visible.
    /// - For a vertical segment with start at `{X,Y}` and length `N` all pixels
    ///   in the inclusive range `{X,Y}:{X,Y+N}` are visible.
    ///
    /// - Parameter deadzone
    /// A region that should be left clear, defined by the circle inscribed in
    /// the given rect.
    func gridDrawingByMergingAdjacentPixels(
        deadzone: Bitmaps.Rect?,
    ) -> Bitmaps.GridDrawing {
        var segments: [Segment] = []

        for row in 0..<height {
            segments.append(contentsOf: mergedAdjacentPixelsInDimension(
                dimension: .horizontal,
                dimensionIteration: 0...width,
                currentPointBlock: { i in Point(x: i, y: row) },
                pointInDeadzoneBlock: { p in deadzone?.inscribedCircleContains(p) ?? false },
            ))
        }

        for column in 0..<width {
            segments.append(contentsOf: mergedAdjacentPixelsInDimension(
                dimension: .vertical,
                dimensionIteration: 0...height,
                currentPointBlock: { i in Point(x: column, y: i) },
                pointInDeadzoneBlock: { p in deadzone?.inscribedCircleContains(p) ?? false },
            ))
        }

        return Bitmaps.GridDrawing(
            width: width,
            height: height,
            segments: Set(segments),
        )
    }

    private func mergedAdjacentPixelsInDimension(
        dimension: Segment.Dimension,
        dimensionIteration: ClosedRange<Int>,
        currentPointBlock: (_ iterationPoint: Int) -> Point,
        pointInDeadzoneBlock: (_ point: Point) -> Bool,
    ) -> [Segment] {
        var newSegments: [Segment] = []
        var currentSegmentStart: Point?
        var currentSegmentLength: Int?

        for i in dimensionIteration {
            let currentPoint: Point = currentPointBlock(i)

            if
                hasVisiblePixel(at: currentPoint),
                !pointInDeadzoneBlock(currentPoint)
            {
                if currentSegmentStart != nil {
                    // Extend the current segment.
                    currentSegmentLength! += 1
                } else {
                    // Start a new segment.
                    currentSegmentStart = currentPoint
                    currentSegmentLength = 1
                }
            } else if let finishedSegmentStart = currentSegmentStart {
                // End the current segment. This can never be the first iteration.
                newSegments.append(Segment(
                    dimension: dimension,
                    start: finishedSegmentStart,
                    length: currentSegmentLength!,
                ))

                currentSegmentStart = nil
                currentSegmentLength = nil
            }
        }

        return newSegments
    }
}