diff --git a/Sources/geometrize-cli/main.swift b/Sources/geometrize-cli/main.swift index 90016fa..a1ddd9c 100644 --- a/Sources/geometrize-cli/main.swift +++ b/Sources/geometrize-cli/main.swift @@ -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) @@ -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, diff --git a/Sources/geometrize/ImageRunner.swift b/Sources/geometrize/ImageRunner.swift index 3bd69cf..022a6ce 100644 --- a/Sources/geometrize/ImageRunner.swift +++ b/Sources/geometrize/ImageRunner.swift @@ -32,7 +32,7 @@ public struct ImageRunnerShapeBoundsOptions { public struct ImageRunnerOptions { /// The shape types that the image runner shall use. - var shapeTypes: Set + var shapeTypes: [Shape.Type] /// The alpha/opacity of the shapes (0-255). var alpha: UInt8 @@ -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, 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 diff --git a/Sources/geometrize/SVGExporter.swift b/Sources/geometrize/SVGExporter.swift index cc30725..df3d3f8 100644 --- a/Sources/geometrize/SVGExporter.swift +++ b/Sources/geometrize/SVGExporter.swift @@ -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) diff --git a/Sources/geometrize/Scanline.swift b/Sources/geometrize/Scanline.swift index 955a1b7..4b21da7 100644 --- a/Sources/geometrize/Scanline.swift +++ b/Sources/geometrize/Scanline.swift @@ -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.") } } diff --git a/Sources/geometrize/ShapeCreator.swift b/Sources/geometrize/ShapeCreator.swift index dda6073..cb355ef 100644 --- a/Sources/geometrize/ShapeCreator.swift +++ b/Sources/geometrize/ShapeCreator.swift @@ -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) -> 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() } } diff --git a/Sources/geometrize/Shapes/Bounds.swift b/Sources/geometrize/Shapes/Bounds.swift new file mode 100644 index 0000000..8bba72c --- /dev/null +++ b/Sources/geometrize/Shapes/Bounds.swift @@ -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 + } +} + diff --git a/Sources/geometrize/Shapes/Circle.swift b/Sources/geometrize/Shapes/Circle.swift index abe2527..fb9c735 100644 --- a/Sources/geometrize/Shapes/Circle.swift +++ b/Sources/geometrize/Shapes/Circle.swift @@ -67,10 +67,6 @@ public final class Circle: Shape { return lines } - public func type() -> ShapeType { - .circle - } - public var isDegenerate: Bool { r == 0.0 } diff --git a/Sources/geometrize/Shapes/Ellipse.swift b/Sources/geometrize/Shapes/Ellipse.swift index ecaabf0..d7ea774 100644 --- a/Sources/geometrize/Shapes/Ellipse.swift +++ b/Sources/geometrize/Shapes/Ellipse.swift @@ -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 } diff --git a/Sources/geometrize/Shapes/Line.swift b/Sources/geometrize/Shapes/Line.swift index 77361cd..8a821bf 100644 --- a/Sources/geometrize/Shapes/Line.swift +++ b/Sources/geometrize/Shapes/Line.swift @@ -64,10 +64,6 @@ public final class Line: Shape { return lines } - public func type() -> ShapeType { - .line - } - public var isDegenerate: Bool { x1 == x2 && y1 == y2 } diff --git a/Sources/geometrize/Shapes/Polyline.swift b/Sources/geometrize/Shapes/Polyline.swift index 50842bb..2e0f737 100644 --- a/Sources/geometrize/Shapes/Polyline.swift +++ b/Sources/geometrize/Shapes/Polyline.swift @@ -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: ", ") + ")" } diff --git a/Sources/geometrize/Shapes/QuadraticBezier.swift b/Sources/geometrize/Shapes/QuadraticBezier.swift index 4452c08..34dd099 100644 --- a/Sources/geometrize/Shapes/QuadraticBezier.swift +++ b/Sources/geometrize/Shapes/QuadraticBezier.swift @@ -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))" } diff --git a/Sources/geometrize/Shapes/Rectangle.swift b/Sources/geometrize/Shapes/Rectangle.swift index d2396d2..15b304e 100644 --- a/Sources/geometrize/Shapes/Rectangle.swift +++ b/Sources/geometrize/Shapes/Rectangle.swift @@ -80,10 +80,6 @@ public final class Rectangle: Shape { Point(x: x1, y: y2)) } - public func type() -> ShapeType { - .rectangle - } - public var isDegenerate: Bool { x1 == x2 || y1 == y2 } diff --git a/Sources/geometrize/Shapes/RotatedEllipse.swift b/Sources/geometrize/Shapes/RotatedEllipse.swift index 8f06110..370f7d5 100644 --- a/Sources/geometrize/Shapes/RotatedEllipse.swift +++ b/Sources/geometrize/Shapes/RotatedEllipse.swift @@ -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 } diff --git a/Sources/geometrize/Shapes/RotatedRectangle.swift b/Sources/geometrize/Shapes/RotatedRectangle.swift index 6154414..95c3604 100644 --- a/Sources/geometrize/Shapes/RotatedRectangle.swift +++ b/Sources/geometrize/Shapes/RotatedRectangle.swift @@ -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 } diff --git a/Sources/geometrize/Shapes/Shape.swift b/Sources/geometrize/Shapes/Shape.swift index aa7cdc6..26b2e68 100644 --- a/Sources/geometrize/Shapes/Shape.swift +++ b/Sources/geometrize/Shapes/Shape.swift @@ -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() @@ -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 } } @@ -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] } + } + } + +} diff --git a/Sources/geometrize/Shapes/Triangle.swift b/Sources/geometrize/Shapes/Triangle.swift index a64c162..48c331a 100644 --- a/Sources/geometrize/Shapes/Triangle.swift +++ b/Sources/geometrize/Shapes/Triangle.swift @@ -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 || diff --git a/Tests/geometrizeTests/ImageRunnerTests.swift b/Tests/geometrizeTests/ImageRunnerTests.swift index ad552b1..a03cf13 100644 --- a/Tests/geometrizeTests/ImageRunnerTests.swift +++ b/Tests/geometrizeTests/ImageRunnerTests.swift @@ -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, diff --git a/Tests/geometrizeTests/ShapeTypesTests.swift b/Tests/geometrizeTests/ShapeTypesTests.swift new file mode 100644 index 0000000..33eb773 --- /dev/null +++ b/Tests/geometrizeTests/ShapeTypesTests.swift @@ -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]) + } + +}