Skip to content

Commit

Permalink
Merge pull request #64 from valeriyvan/refactor/ShapeType
Browse files Browse the repository at this point in the history
Get rid of ShapeType enum
  • Loading branch information
valeriyvan authored Oct 10, 2023
2 parents 3b36706 + ddd8d32 commit 79d947e
Show file tree
Hide file tree
Showing 18 changed files with 102 additions and 114 deletions.
32 changes: 20 additions & 12 deletions Sources/geometrize-cli/main.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,6 @@ struct GeometrizeOptions: ParsableArguments {
@Flag(name: .shortAndLong, help: "Verbose output.") var verbose: Bool = false
}

//print("Available shaped: \(ShapeType.allCases.map(\.rawValueCapitalized).joined(by: ", ")).")

let options = GeometrizeOptions.parseOrExit()

let inputUrl = URL(fileURLWithPath: options.inputPath)
Expand Down Expand Up @@ -65,25 +63,35 @@ guard outputUrl.pathExtension.caseInsensitiveCompare("svg") == .orderedSame else
}

// TODO: use ExpressibleByArgument?
let shapeTypes = options.shapeTypes.components(separatedBy: .whitespacesAndNewlines)
let shapesOrNil = shapeTypes.map(ShapeType.init)
let indexOfNil = shapesOrNil.firstIndex(of: nil)
if let indexOfNil = indexOfNil {
print("Not recognised shape type \(shapeTypes[indexOfNil]). Allowed shape types:")
ShapeType.allCases.forEach {
print($0.rawValueCapitalized)
let shapes = options.shapeTypes.components(separatedBy: .whitespacesAndNewlines).shapeTypes()

// Shape.Type is not hashable as all Metatypes. Why, by the way?
// That why we check for nil in this strange way.
var indexOfNil: Int?
for (i, shape) in shapes.enumerated() {
if shape == nil {
indexOfNil = i
break
}
}

if let indexOfNil {
print("Not recognised shape type \(shapes[indexOfNil]!). Allowed shape types:")
allShapeTypes.forEach {
print("\($0)")
}
print("Case insensitive, underscores are ignored, white spaces are delimiters.")
exit(1)
}

let shapes = Set(shapesOrNil.compactMap { $0 })
print("Using shapes: \(shapes.map(\.rawValueCapitalized).joined(separator: ", ")).")
let shapeTypes: [Shape.Type] = shapes.compactMap { $0 }

print("Using shapes: \(shapeTypes.map { "\(type(of: $0))".dropLast(5) /* drop .Type */ }.joined(separator: ", ")).")

let shapeCount: Int = Int(options.shapeCount ?? 100)

let runnerOptions = ImageRunnerOptions(
shapeTypes: shapes,
shapeTypes: shapeTypes,
alpha: 128,
shapeCount: 100,
maxShapeMutations: 100,
Expand Down
4 changes: 2 additions & 2 deletions Sources/geometrize/ImageRunner.swift
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ public struct ImageRunnerShapeBoundsOptions {
public struct ImageRunnerOptions {

/// The shape types that the image runner shall use.
var shapeTypes: Set<ShapeType>
var shapeTypes: [Shape.Type]

/// The alpha/opacity of the shapes (0-255).
var alpha: UInt8
Expand All @@ -53,7 +53,7 @@ public struct ImageRunnerOptions {
/// If zero or do not form a rectangle, the entire target image is used i.e. (0, 0, imageWidth, imageHeight).
var shapeBounds: ImageRunnerShapeBoundsOptions

public init(shapeTypes: Set<ShapeType>, alpha: UInt8, shapeCount: Int, maxShapeMutations: Int, seed: Int, maxThreads: Int, shapeBounds: ImageRunnerShapeBoundsOptions) {
public init(shapeTypes: [Shape.Type], alpha: UInt8, shapeCount: Int, maxShapeMutations: Int, seed: Int, maxThreads: Int, shapeBounds: ImageRunnerShapeBoundsOptions) {
self.shapeTypes = shapeTypes
self.alpha = alpha
self.shapeCount = shapeCount
Expand Down
3 changes: 1 addition & 2 deletions Sources/geometrize/SVGExporter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,10 @@ public struct SVGExporter {
/// - Returns: The SVG shape data for the given shape
func singleShapeData(color: Rgba, shape: any Shape, options: ExportOptions = ExportOptions()) -> String {
var shapeData: String = shapeData(shape: shape, options: options)
let shapeType = shape.type()

var styles: String = "id=\"\(options.itemId)\" "

if [ShapeType.line, ShapeType.polyline, ShapeType.quadraticBezier].contains(shapeType) {
if shape.self is Line || shape.self is Polyline || shape.self is QuadraticBezier {
styles += strokeAttrib(color: color)
styles += " stroke-width=\"1\" fill=\"none\" "
styles += strokeOpacityAttrib(color: color)
Expand Down
2 changes: 1 addition & 1 deletion Sources/geometrize/Scanline.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ public struct Scanline {
self.x1 = x1
self.x2 = x2
if x1 > x2 {
print("Warning: Scanline has x1(\(x1)) > x2(\(x2). This makes scanline invisible..")
print("Warning: Scanline has x1(\(x1)) > x2(\(x2). This makes scanline invisible.")
}
}

Expand Down
21 changes: 7 additions & 14 deletions Sources/geometrize/ShapeCreator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,13 @@ public typealias ShapeCreator = (inout SplitMix64) -> any Shape
/// - Parameters:
/// - types: The types of shapes to create.
/// - Returns: The default shape creator.
public func makeDefaultShapeCreator(types: Set<ShapeType>) -> ShapeCreator {
public func makeDefaultShapeCreator(types: [Shape.Type]) -> ShapeCreator {
return { generator in
let shape: Shape
switch types[types.index(types.startIndex, offsetBy: Int._random(in: 0...types.count - 1, using: &generator))] {
case .rectangle: shape = Rectangle()
case .rotatedRectangle: shape = RotatedRectangle()
case .rotatedEllipse: shape = RotatedEllipse()
case .triangle: shape = Triangle()
case .circle: shape = Circle()
case .ellipse: shape = Ellipse()
case .line: shape = Line()
case .polyline: shape = Polyline()
case .quadraticBezier: shape = QuadraticBezier()
}
return shape
let index = types.index(
types.startIndex,
offsetBy: Int._random(in: 0...types.count - 1, using: &generator)
)
let shapeType = types[index]
return shapeType.init()
}
}
16 changes: 16 additions & 0 deletions Sources/geometrize/Shapes/Bounds.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import Foundation

public struct Bounds {
let xMin: Int
let xMax: Int
let yMin: Int
let yMax: Int

public init(xMin: Int, xMax: Int, yMin: Int, yMax: Int) {
self.xMin = xMin
self.xMax = xMax
self.yMin = yMin
self.yMax = yMax
}
}

4 changes: 0 additions & 4 deletions Sources/geometrize/Shapes/Circle.swift
Original file line number Diff line number Diff line change
Expand Up @@ -67,10 +67,6 @@ public final class Circle: Shape {
return lines
}

public func type() -> ShapeType {
.circle
}

public var isDegenerate: Bool {
r == 0.0
}
Expand Down
4 changes: 0 additions & 4 deletions Sources/geometrize/Shapes/Ellipse.swift
Original file line number Diff line number Diff line change
Expand Up @@ -81,10 +81,6 @@ public final class Ellipse: Shape {
return lines
}

public func type() -> ShapeType {
.ellipse
}

public var isDegenerate: Bool {
rx == 0.0 || ry == 0.0
}
Expand Down
4 changes: 0 additions & 4 deletions Sources/geometrize/Shapes/Line.swift
Original file line number Diff line number Diff line change
Expand Up @@ -64,10 +64,6 @@ public final class Line: Shape {
return lines
}

public func type() -> ShapeType {
.line
}

public var isDegenerate: Bool {
x1 == x2 && y1 == y2
}
Expand Down
4 changes: 0 additions & 4 deletions Sources/geometrize/Shapes/Polyline.swift
Original file line number Diff line number Diff line change
Expand Up @@ -67,10 +67,6 @@ public final class Polyline: Shape {
return lines
}

public func type() -> ShapeType {
.polyline
}

public var description: String {
"Polyline(" + points.map(\.description).joined(separator: ", ") + ")"
}
Expand Down
4 changes: 0 additions & 4 deletions Sources/geometrize/Shapes/QuadraticBezier.swift
Original file line number Diff line number Diff line change
Expand Up @@ -88,10 +88,6 @@ public final class QuadraticBezier: Shape {
return lines
}

public func type() -> ShapeType {
.quadraticBezier
}

public var description: String {
"QuadraticBezier(cx=\(cx), cy=\(cy), x1=\(x1), y1=\(y1), x2=\(x2), y2=\(y2))"
}
Expand Down
4 changes: 0 additions & 4 deletions Sources/geometrize/Shapes/Rectangle.swift
Original file line number Diff line number Diff line change
Expand Up @@ -80,10 +80,6 @@ public final class Rectangle: Shape {
Point<Double>(x: x1, y: y2))
}

public func type() -> ShapeType {
.rectangle
}

public var isDegenerate: Bool {
x1 == x2 || y1 == y2
}
Expand Down
4 changes: 0 additions & 4 deletions Sources/geometrize/Shapes/RotatedEllipse.swift
Original file line number Diff line number Diff line change
Expand Up @@ -83,10 +83,6 @@ public final class RotatedEllipse: Shape {
return points
}

public func type() -> ShapeType {
.rotatedEllipse
}

public var isDegenerate: Bool {
rx == 0.0 || ry == 0.0
}
Expand Down
4 changes: 0 additions & 4 deletions Sources/geometrize/Shapes/RotatedRectangle.swift
Original file line number Diff line number Diff line change
Expand Up @@ -103,10 +103,6 @@ public final class RotatedRectangle: Shape {
return (ul, ur, br, bl)
}

public func type() -> ShapeType {
.rotatedRectangle
}

public var isDegenerate: Bool {
x1 == x2 || y1 == y2
}
Expand Down
70 changes: 24 additions & 46 deletions Sources/geometrize/Shapes/Shape.swift
Original file line number Diff line number Diff line change
@@ -1,49 +1,5 @@
import Foundation

// Specifies the types of shapes that can be used.
// These can be combined to produce images composed of multiple primitive types.
// TODO: put it inside Shape. Or remove completely.
public enum ShapeType: String, CaseIterable {
case rectangle
case rotatedRectangle
case triangle
case ellipse
case rotatedEllipse
case circle
case line
case quadraticBezier
case polyline

public var rawValueCapitalized: String {
let firstUppercased = rawValue.first!.uppercased()
return firstUppercased + rawValue.dropFirst()
}

// Allows rotatedrectangle and rotated_rectangle, casing doesn't matter.
public init?(rawValue: String) {
let allCases = ShapeType.allCases
let all = allCases.map { $0.rawValue.lowercased() }
guard let index = all.firstIndex(of: rawValue.replacingOccurrences(of: "_", with: "").lowercased()) else {
return nil
}
self = allCases[index]
}
}

public struct Bounds {
let xMin: Int
let xMax: Int
let yMin: Int
let yMax: Int

public init(xMin: Int, xMax: Int, yMin: Int, yMax: Int) {
self.xMin = xMin
self.xMax = xMax
self.yMin = yMin
self.yMax = yMax
}
}

public protocol Shape: AnyObject, CustomStringConvertible {
init()

Expand All @@ -55,8 +11,6 @@ public protocol Shape: AnyObject, CustomStringConvertible {

func rasterize(xMin: Int, yMin: Int, xMax: Int, yMax: Int) -> [Scanline]

func type() -> ShapeType

var isDegenerate: Bool { get }
}

Expand All @@ -74,3 +28,27 @@ func == (lhs: any Shape, rhs: any Shape) -> Bool {
default: return false
}
}

public let allShapeTypes: [Shape.Type] = [
Rectangle.self,
RotatedRectangle.self,
Triangle.self,
Circle.self,
Ellipse.self,
RotatedEllipse.self,
Line.self,
Polyline.self,
QuadraticBezier.self
]

public extension Array where Element == String {

func shapeTypes() -> [Shape.Type?] {
let allShapeTypeStrings = allShapeTypes.map { "\(type(of: $0))".dropLast(5).lowercased() } // /* drop .Type */
return self.map {
let needle = $0.lowercased().replacingOccurrences(of: "_", with: "")
return allShapeTypeStrings.firstIndex(of: needle).flatMap { allShapeTypes[$0] }
}
}

}
4 changes: 0 additions & 4 deletions Sources/geometrize/Shapes/Triangle.swift
Original file line number Diff line number Diff line change
Expand Up @@ -80,10 +80,6 @@ public final class Triangle: Shape {
return lines
}

public func type() -> ShapeType {
.triangle
}

public var isDegenerate: Bool {
x1 == x2 && y1 == y2 ||
x1 == x3 && y1 == y3 ||
Expand Down
2 changes: 1 addition & 1 deletion Tests/geometrizeTests/ImageRunnerTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ final class ImageRunnerTests: XCTestCase {
let targetBitmap = Bitmap(width: width, height: height, data: data)

let options = ImageRunnerOptions(
shapeTypes: Set([.rotatedEllipse]),
shapeTypes: [RotatedEllipse.self],
alpha: 128,
shapeCount: 500,
maxShapeMutations: 100,
Expand Down
30 changes: 30 additions & 0 deletions Tests/geometrizeTests/ShapeTypesTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import XCTest
@testable import Geometrize

final class ShapeTypesTests: XCTestCase {

func testEmpty() throws {
XCTAssertTrue([String]().shapeTypes().isEmpty)
}

func testRectangle() throws {
let array = ["rectangle"].shapeTypes()
XCTAssertEqual(array.count, 1)
XCTAssert(array[0]! is Rectangle.Type)
}

func testRotated_Rectangle() throws {
let array = ["Rotated_rectangle"].shapeTypes()
XCTAssertEqual(array.count, 1)
XCTAssert(array[0]! is RotatedRectangle.Type)
}

func testRectangleCircleGarbage() throws {
let array = ["Rectangle", "Circle", "bla-bla-bla"].shapeTypes()
XCTAssertEqual(array.count, 3)
XCTAssert(array[0]! is Rectangle.Type)
XCTAssert(array[1]! is Circle.Type)
XCTAssertNil(array[2])
}

}

0 comments on commit 79d947e

Please sign in to comment.