diff --git a/Sources/geometrize/Bitmap.swift b/Sources/geometrize/Bitmap.swift index c98d1c8..9f0eb41 100644 --- a/Sources/geometrize/Bitmap.swift +++ b/Sources/geometrize/Bitmap.swift @@ -2,7 +2,7 @@ import Foundation /// Helper for working with bitmap data. /// Pixels are ordered line by line, like arrays in C. -public struct Bitmap { // swiftlint:disable:this type_body_length +public struct Bitmap: Sendable { // swiftlint:disable:this type_body_length /// Creates useless empty bitmap. public init() { diff --git a/Sources/geometrize/HillClimb.swift b/Sources/geometrize/HillClimb.swift index 6acc441..3fa3e1e 100644 --- a/Sources/geometrize/HillClimb.swift +++ b/Sources/geometrize/HillClimb.swift @@ -78,26 +78,28 @@ func hillClimb( // swiftlint:disable:this function_parameter_count using generator: inout SplitMix64 ) -> State { let xRange = 0...target.width - 1, yRange = 0...target.height - 1 - var s: State = state.copy() - var bestState: State = state.copy() + var s: State = state + var bestState: State = state var bestEnergy: Double = bestState.score var age: Int = 0 while age < maxAge { - let undo: State = s.mutate(x: xRange, y: yRange, using: &generator) - s.score = energyFunction( - s.shape.rasterize(x: xRange, y: yRange), - s.alpha, - target, - current, - &buffer, - lastScore - ) - let energy: Double = s.score - if energy >= bestEnergy { - s = undo.copy() + let undo = s + let alpha = s.alpha + s = s.mutate(x: xRange, y: yRange, using: &generator) { aShape in + return energyFunction( + aShape.rasterize(x: xRange, y: yRange), + alpha, + target, + current, + &buffer, + lastScore + ) + } + if s.score >= bestEnergy { + s = undo } else { - bestEnergy = energy - bestState = s.copy() + bestEnergy = s.score + bestState = s age = Int.max // TODO: What's the point??? And following increment overflows. } if age == Int.max { @@ -133,35 +135,31 @@ private func bestRandomState( // swiftlint:disable:this function_parameter_count using generator: inout SplitMix64 ) -> State { let xRange = 0...target.width - 1, yRange = 0...target.height - 1 - let shape = shapeCreator(&generator) - shape.setup(x: xRange, y: yRange, using: &generator) - var bestState: State = State(shape: shape, alpha: alpha) - bestState.score = energyFunction( - bestState.shape.rasterize(x: xRange, y: yRange), - bestState.alpha, + let shape = shapeCreator(&generator).setup(x: xRange, y: yRange, using: &generator) + var bestEnergy: Double = energyFunction( + shape.rasterize(x: xRange, y: yRange), + alpha, target, current, &buffer, lastScore ) - var bestEnergy: Double = bestState.score + var bestState: State = State(score: bestEnergy, alpha: alpha, shape: shape) for i in 0...n { - let shape = shapeCreator(&generator) - shape.setup(x: xRange, y: yRange, using: &generator) - var state: State = State(shape: shape, alpha: alpha) - state.score = energyFunction( - state.shape.rasterize(x: xRange, y: yRange), - state.alpha, + let shape = shapeCreator(&generator).setup(x: xRange, y: yRange, using: &generator) + let energy: Double = energyFunction( + shape.rasterize(x: xRange, y: yRange), + alpha, target, current, &buffer, lastScore ) - let energy: Double = state.score + let state: State = State(score: energy, alpha: alpha, shape: shape) if i == 0 || energy < bestEnergy { bestEnergy = energy - bestState = state.copy() + bestState = state } } - return bestState.copy() // TODO: is copy needed here??? + return bestState } diff --git a/Sources/geometrize/Point.swift b/Sources/geometrize/Point.swift index f96a069..14a08df 100644 --- a/Sources/geometrize/Point.swift +++ b/Sources/geometrize/Point.swift @@ -1,7 +1,7 @@ import Foundation -public struct Point { - public var x, y: N +public struct Point: Sendable { + public let x, y: N public init(x: N, y: N) { self.x = x diff --git a/Sources/geometrize/Polygon.swift b/Sources/geometrize/Polygon.swift index 638522e..1165e97 100644 --- a/Sources/geometrize/Polygon.swift +++ b/Sources/geometrize/Polygon.swift @@ -1,7 +1,7 @@ import Foundation import Algorithms -struct Polygon { +struct Polygon { var vertices: [Point] init(vertices: [Point]) throws { diff --git a/Sources/geometrize/Shapes/Circle.swift b/Sources/geometrize/Shapes/Circle.swift index fdff659..913fd26 100644 --- a/Sources/geometrize/Shapes/Circle.swift +++ b/Sources/geometrize/Shapes/Circle.swift @@ -1,10 +1,10 @@ import Foundation -public final class Circle: Shape { - public var strokeWidth: Double - public var x: Double // x-coordinate. - public var y: Double // y-coordinate. - public var r: Double // Radius. +public struct Circle: Shape { + public let strokeWidth: Double + public let x: Double // x-coordinate. + public let y: Double // y-coordinate. + public let r: Double // Radius. public init(strokeWidth: Double) { self.strokeWidth = strokeWidth @@ -24,23 +24,39 @@ public final class Circle: Shape { Circle(strokeWidth: strokeWidth, x: x, y: y, r: r) } - public func setup(x xRange: ClosedRange, y yRange: ClosedRange, using generator: inout SplitMix64) { - x = Double(Int._random(in: xRange, using: &generator)) - y = Double(Int._random(in: yRange, using: &generator)) - r = Double(Int._random(in: 1...32, using: &generator)) + public func setup( + x xRange: ClosedRange, + y yRange: ClosedRange, + using generator: inout SplitMix64 + ) -> Circle { + Circle( + strokeWidth: strokeWidth, + x: Double(Int._random(in: xRange, using: &generator)), + y: Double(Int._random(in: yRange, using: &generator)), + r: Double(Int._random(in: 1...32, using: &generator)) + ) } - public func mutate(x xRange: ClosedRange, y yRange: ClosedRange, using generator: inout SplitMix64) { + public func mutate( + x xRange: ClosedRange, + y yRange: ClosedRange, + using generator: inout SplitMix64 + ) -> Circle { let range16 = -16...16 + var newX, newY, newR: Double switch Int._random(in: 0...1, using: &generator) { case 0: - x = Double((Int(x) + Int._random(in: range16, using: &generator)).clamped(to: xRange)) - y = Double((Int(y) + Int._random(in: range16, using: &generator)).clamped(to: yRange)) + newX = Double((Int(x) + Int._random(in: range16, using: &generator)).clamped(to: xRange)) + newY = Double((Int(y) + Int._random(in: range16, using: &generator)).clamped(to: yRange)) + newR = r case 1: - r = Double((Int(r) + Int._random(in: range16, using: &generator)).clamped(to: 1...xRange.upperBound)) + newX = x + newY = y + newR = Double((Int(r) + Int._random(in: range16, using: &generator)).clamped(to: 1...xRange.upperBound)) default: fatalError("Internal inconsistency") } + return Circle(strokeWidth: strokeWidth, x: newX, y: newY, r: newR) } public func rasterize(x xRange: ClosedRange, y yRange: ClosedRange) -> [Scanline] { diff --git a/Sources/geometrize/Shapes/Ellipse.swift b/Sources/geometrize/Shapes/Ellipse.swift index c5cfcb4..d8972c3 100644 --- a/Sources/geometrize/Shapes/Ellipse.swift +++ b/Sources/geometrize/Shapes/Ellipse.swift @@ -1,11 +1,11 @@ import Foundation -public final class Ellipse: Shape { - public var strokeWidth: Double - public var x: Double // x-coordinate. - public var y: Double // y-coordinate. - public var rx: Double // x-radius. - public var ry: Double // y-radius. +public struct Ellipse: Shape { + public let strokeWidth: Double + public let x: Double // x-coordinate. + public let y: Double // y-coordinate. + public let rx: Double // x-radius. + public let ry: Double // y-radius. public init(strokeWidth: Double) { self.strokeWidth = strokeWidth @@ -27,26 +27,47 @@ public final class Ellipse: Shape { Ellipse(strokeWidth: strokeWidth, x: x, y: y, rx: rx, ry: ry) } - public func setup(x xRange: ClosedRange, y yRange: ClosedRange, using generator: inout SplitMix64) { - x = Double(Int._random(in: xRange, using: &generator)) - y = Double(Int._random(in: yRange, using: &generator)) - rx = Double(Int._random(in: 1...32, using: &generator)) - ry = Double(Int._random(in: 1...32, using: &generator)) + public func setup( + x xRange: ClosedRange, + y yRange: ClosedRange, + using generator: inout SplitMix64 + ) -> Ellipse { + Ellipse( + strokeWidth: strokeWidth, + x: Double(Int._random(in: xRange, using: &generator)), + y: Double(Int._random(in: yRange, using: &generator)), + rx: Double(Int._random(in: 1...32, using: &generator)), + ry: Double(Int._random(in: 1...32, using: &generator)) + ) } - public func mutate(x xRange: ClosedRange, y yRange: ClosedRange, using generator: inout SplitMix64) { + public func mutate( + x xRange: ClosedRange, + y yRange: ClosedRange, + using generator: inout SplitMix64 + ) -> Ellipse { let range16 = -16...16 + let newX, newY, newRx, newRy: Double switch Int._random(in: 0...2, using: &generator) { case 0: - x = Double((Int(x) + Int._random(in: range16, using: &generator)).clamped(to: xRange)) - y = Double((Int(y) + Int._random(in: range16, using: &generator)).clamped(to: yRange)) + newX = Double((Int(x) + Int._random(in: range16, using: &generator)).clamped(to: xRange)) + newY = Double((Int(y) + Int._random(in: range16, using: &generator)).clamped(to: yRange)) + newRx = rx + newRy = ry case 1: - rx = Double((Int(rx) + Int._random(in: range16, using: &generator)).clamped(to: 1...xRange.upperBound)) + newX = x + newY = y + newRx = Double((Int(rx) + Int._random(in: range16, using: &generator)).clamped(to: 1...xRange.upperBound)) + newRy = ry case 2: - ry = Double((Int(ry) + Int._random(in: range16, using: &generator)).clamped(to: 1...yRange.upperBound)) + newX = x + newY = y + newRx = rx + newRy = Double((Int(ry) + Int._random(in: range16, using: &generator)).clamped(to: 1...yRange.upperBound)) default: fatalError("Internal inconsistency") } + return Ellipse(strokeWidth: strokeWidth, x: newX, y: newY, rx: newRx, ry: newRy) } public func rasterize(x xRange: ClosedRange, y yRange: ClosedRange) -> [Scanline] { diff --git a/Sources/geometrize/Shapes/Line.swift b/Sources/geometrize/Shapes/Line.swift index 20ed0d6..8941509 100644 --- a/Sources/geometrize/Shapes/Line.swift +++ b/Sources/geometrize/Shapes/Line.swift @@ -1,8 +1,8 @@ import Foundation -public final class Line: Shape { - public var strokeWidth: Double - public var x1, y1, x2, y2: Double +public struct Line: Shape { + public let strokeWidth: Double + public let x1, y1, x2, y2: Double public init(strokeWidth: Double) { self.strokeWidth = strokeWidth @@ -24,30 +24,47 @@ public final class Line: Shape { Line(strokeWidth: strokeWidth, x1: x1, y1: y1, x2: x2, y2: y2) } - public func setup(x xRange: ClosedRange, y yRange: ClosedRange, using generator: inout SplitMix64) { + public func setup( + x xRange: ClosedRange, + y yRange: ClosedRange, + using generator: inout SplitMix64 + ) -> Line { let range32 = -32...32 let startingPoint = Point( x: Int._random(in: xRange, using: &generator), y: Int._random(in: yRange, using: &generator) ) - x1 = Double((startingPoint.x + Int._random(in: range32, using: &generator)).clamped(to: xRange)) - y1 = Double((startingPoint.y + Int._random(in: range32, using: &generator)).clamped(to: yRange)) - x2 = Double((startingPoint.x + Int._random(in: range32, using: &generator)).clamped(to: xRange)) - y2 = Double((startingPoint.y + Int._random(in: range32, using: &generator)).clamped(to: yRange)) + return Line( + strokeWidth: strokeWidth, + x1: Double((startingPoint.x + Int._random(in: range32, using: &generator)).clamped(to: xRange)), + y1: Double((startingPoint.y + Int._random(in: range32, using: &generator)).clamped(to: yRange)), + x2: Double((startingPoint.x + Int._random(in: range32, using: &generator)).clamped(to: xRange)), + y2: Double((startingPoint.y + Int._random(in: range32, using: &generator)).clamped(to: yRange)) + ) } - public func mutate(x xRange: ClosedRange, y yRange: ClosedRange, using generator: inout SplitMix64) { + public func mutate( + x xRange: ClosedRange, + y yRange: ClosedRange, + using generator: inout SplitMix64 + ) -> Line { let range16 = -16...16 + let newX1, newY1, newX2, newY2: Double switch Int._random(in: 0...1, using: &generator) { case 0: - x1 = Double((Int(x1) + Int._random(in: range16, using: &generator)).clamped(to: xRange)) - y1 = Double((Int(y1) + Int._random(in: range16, using: &generator)).clamped(to: yRange)) + newX1 = Double((Int(x1) + Int._random(in: range16, using: &generator)).clamped(to: xRange)) + newY1 = Double((Int(y1) + Int._random(in: range16, using: &generator)).clamped(to: yRange)) + newX2 = x2 + newY2 = y2 case 1: - x2 = Double((Int(x2) + Int._random(in: range16, using: &generator)).clamped(to: xRange)) - y2 = Double((Int(y2) + Int._random(in: range16, using: &generator)).clamped(to: yRange)) + newX1 = x1 + newY1 = y1 + newX2 = Double((Int(x2) + Int._random(in: range16, using: &generator)).clamped(to: xRange)) + newY2 = Double((Int(y2) + Int._random(in: range16, using: &generator)).clamped(to: yRange)) default: fatalError("Internal inconsistency") } + return Line(strokeWidth: strokeWidth, x1: newX1, y1: newY1, x2: newX2, y2: newY2) } public func rasterize(x xRange: ClosedRange, y yRange: ClosedRange) -> [Scanline] { diff --git a/Sources/geometrize/Shapes/Polyline.swift b/Sources/geometrize/Shapes/Polyline.swift index f5b2c5e..0d1e5a4 100644 --- a/Sources/geometrize/Shapes/Polyline.swift +++ b/Sources/geometrize/Shapes/Polyline.swift @@ -1,8 +1,8 @@ import Foundation -public final class Polyline: Shape { - public var strokeWidth: Double - public var points: [Point] +public struct Polyline: Shape { + public let strokeWidth: Double + public let points: [Point] public init(strokeWidth: Double) { self.strokeWidth = strokeWidth @@ -18,7 +18,11 @@ public final class Polyline: Shape { Polyline(strokeWidth: strokeWidth, points: points) } - public func setup(x xRange: ClosedRange, y yRange: ClosedRange, using generator: inout SplitMix64) { + public func setup( + x xRange: ClosedRange, + y yRange: ClosedRange, + using generator: inout SplitMix64 + ) -> Polyline { let range32 = -32...32 let startingPoint = Point( x: Int._random(in: xRange, using: &generator), @@ -33,16 +37,24 @@ public final class Polyline: Shape { ) ) } - self.points = points + return Polyline(strokeWidth: strokeWidth, points: points) } - public func mutate(x xRange: ClosedRange, y yRange: ClosedRange, using generator: inout SplitMix64) { + public func mutate( + x xRange: ClosedRange, + y yRange: ClosedRange, + using generator: inout SplitMix64 + ) -> Polyline { + let range64 = -64...64 let i = Int._random(in: 0...points.count-1, using: &generator) var point = points[i] - let range64 = -64...64 - point.x = Double((Int(point.x) + Int._random(in: range64, using: &generator)).clamped(to: xRange)) - point.y = Double((Int(point.y) + Int._random(in: range64, using: &generator)).clamped(to: yRange)) - points[i] = point + point = Point( + x: Double((Int(point.x) + Int._random(in: range64, using: &generator)).clamped(to: xRange)), + y: Double((Int(point.y) + Int._random(in: range64, using: &generator)).clamped(to: yRange)) + ) + var newPoints = points + newPoints[i] = point + return Polyline(strokeWidth: strokeWidth, points: newPoints) } public func rasterize(x xRange: ClosedRange, y yRange: ClosedRange) -> [Scanline] { diff --git a/Sources/geometrize/Shapes/QuadraticBezier.swift b/Sources/geometrize/Shapes/QuadraticBezier.swift index 7829123..480c5e5 100644 --- a/Sources/geometrize/Shapes/QuadraticBezier.swift +++ b/Sources/geometrize/Shapes/QuadraticBezier.swift @@ -1,14 +1,14 @@ import Foundation import Algorithms -public final class QuadraticBezier: Shape { - public var strokeWidth: Double - public var x1: Double // First x-coordinate. - public var y1: Double // First y-coordinate. - public var x2: Double // Second x-coordinate. - public var y2: Double // Second y-coordinate. - public var cx: Double // Control point x-coordinate. - public var cy: Double // Control point y-coordinate. +public struct QuadraticBezier: Shape { + public let strokeWidth: Double + public let x1: Double // First x-coordinate. + public let y1: Double // First y-coordinate. + public let x2: Double // Second x-coordinate. + public let y2: Double // Second y-coordinate. + public let cx: Double // Control point x-coordinate. + public let cy: Double // Control point y-coordinate. public init(strokeWidth: Double) { self.strokeWidth = strokeWidth @@ -34,36 +34,69 @@ public final class QuadraticBezier: Shape { QuadraticBezier(strokeWidth: strokeWidth, cx: cx, cy: cy, x1: x1, y1: y1, x2: x2, y2: y2) } - public func setup(x xRange: ClosedRange, y yRange: ClosedRange, using generator: inout SplitMix64) { - cx = Double(Int._random(in: xRange, using: &generator)) - cy = Double(Int._random(in: yRange, using: &generator)) - x1 = Double(Int._random(in: xRange, using: &generator)) - y1 = Double(Int._random(in: yRange, using: &generator)) - x2 = Double(Int._random(in: xRange, using: &generator)) - y2 = Double(Int._random(in: yRange, using: &generator)) + public func setup( + x xRange: ClosedRange, + y yRange: ClosedRange, + using generator: inout SplitMix64 + ) -> QuadraticBezier { + QuadraticBezier( + strokeWidth: strokeWidth, + cx: Double(Int._random(in: xRange, using: &generator)), + cy: Double(Int._random(in: yRange, using: &generator)), + x1: Double(Int._random(in: xRange, using: &generator)), + y1: Double(Int._random(in: yRange, using: &generator)), + x2: Double(Int._random(in: xRange, using: &generator)), + y2: Double(Int._random(in: yRange, using: &generator)) + ) } - public func mutate(x xRange: ClosedRange, y yRange: ClosedRange, using generator: inout SplitMix64) { + public func mutate( + x xRange: ClosedRange, + y yRange: ClosedRange, + using generator: inout SplitMix64 + ) -> QuadraticBezier { let range8 = -8...8 + let newX1, newY1, newX2, newY2, newCx, newCy: Double switch Int._random(in: 0...2, using: &generator) { case 0: - cx = Double((Int(cx) + Int._random(in: range8, using: &generator)) + newX1 = x1 + newY1 = y1 + newX2 = x2 + newY2 = y2 + newCx = Double((Int(cx) + Int._random(in: range8, using: &generator)) .clamped(to: xRange)) - cy = Double((Int(cy) + Int._random(in: range8, using: &generator)) + newCy = Double((Int(cy) + Int._random(in: range8, using: &generator)) .clamped(to: yRange)) case 1: - x1 = Double((Int(x1) + Int._random(in: range8, using: &generator)) + newX1 = Double((Int(x1) + Int._random(in: range8, using: &generator)) .clamped(to: xRange.lowerBound + 1...xRange.upperBound)) - y1 = Double((Int(y1) + Int._random(in: range8, using: &generator)) + newY1 = Double((Int(y1) + Int._random(in: range8, using: &generator)) .clamped(to: yRange.lowerBound + 1...yRange.upperBound)) + newX2 = x2 + newY2 = y2 + newCx = cx + newCy = cy case 2: - x2 = Double((Int(x2) + Int._random(in: range8, using: &generator)) + newX1 = x1 + newY1 = y1 + newX2 = Double((Int(x2) + Int._random(in: range8, using: &generator)) .clamped(to: xRange.lowerBound + 1...xRange.upperBound)) - y2 = Double((Int(y2) + Int._random(in: range8, using: &generator)) + newY2 = Double((Int(y2) + Int._random(in: range8, using: &generator)) .clamped(to: yRange.lowerBound + 1...yRange.upperBound)) + newCx = cx + newCy = cy default: fatalError("Internal inconsistency") } + return QuadraticBezier( + strokeWidth: strokeWidth, + cx: newCx, + cy: newCy, + x1: newX1, + y1: newY1, + x2: newX2, + y2: newY2 + ) } public func rasterize(x xRange: ClosedRange, y yRange: ClosedRange) -> [Scanline] { diff --git a/Sources/geometrize/Shapes/Rectangle.swift b/Sources/geometrize/Shapes/Rectangle.swift index 2101f1f..fd3a0ab 100644 --- a/Sources/geometrize/Shapes/Rectangle.swift +++ b/Sources/geometrize/Shapes/Rectangle.swift @@ -1,11 +1,11 @@ import Foundation // Represents a rectangle. -public final class Rectangle: Shape { - public var strokeWidth: Double - public var x1, y1, x2, y2: Double +public struct Rectangle: Shape { + public let strokeWidth: Double + public let x1, y1, x2, y2: Double - required public init(strokeWidth: Double) { + public init(strokeWidth: Double) { self.strokeWidth = strokeWidth x1 = 0.0 y1 = 0.0 @@ -22,7 +22,7 @@ public final class Rectangle: Shape { } // Rectangle taking whole size of canvas - public convenience init(canvasWidth width: Int, height: Int) { + public init(canvasWidth width: Int, height: Int) { self.init(strokeWidth: 1, x1: 0.0, y1: 0.0, x2: Double(width), y2: Double(height)) } @@ -30,26 +30,43 @@ public final class Rectangle: Shape { Rectangle(strokeWidth: strokeWidth, x1: x1, y1: y1, x2: x2, y2: y2) } - public func setup(x xRange: ClosedRange, y yRange: ClosedRange, using generator: inout SplitMix64) { + public func setup( + x xRange: ClosedRange, + y yRange: ClosedRange, + using generator: inout SplitMix64 + ) -> Rectangle { let range32 = 1...32 - x1 = Double(Int._random(in: xRange, using: &generator)) - y1 = Double(Int._random(in: yRange, using: &generator)) - x2 = Double((Int(x1) + Int._random(in: range32, using: &generator)).clamped(to: xRange)) - y2 = Double((Int(y1) + Int._random(in: range32, using: &generator)).clamped(to: yRange)) + return Rectangle( + strokeWidth: strokeWidth, + x1: Double(Int._random(in: xRange, using: &generator)), + y1: Double(Int._random(in: yRange, using: &generator)), + x2: Double((Int(x1) + Int._random(in: range32, using: &generator)).clamped(to: xRange)), + y2: Double((Int(y1) + Int._random(in: range32, using: &generator)).clamped(to: yRange)) + ) } - public func mutate(x xRange: ClosedRange, y yRange: ClosedRange, using generator: inout SplitMix64) { + public func mutate( + x xRange: ClosedRange, + y yRange: ClosedRange, + using generator: inout SplitMix64 + ) -> Rectangle { let range16 = -16...16 + let newX1, newY1, newX2, newY2: Double switch Int._random(in: 0...1, using: &generator) { case 0: - x1 = Double((Int(x1) + Int._random(in: range16, using: &generator)).clamped(to: xRange)) - y1 = Double((Int(y1) + Int._random(in: range16, using: &generator)).clamped(to: yRange)) + newX1 = Double((Int(x1) + Int._random(in: range16, using: &generator)).clamped(to: xRange)) + newY1 = Double((Int(y1) + Int._random(in: range16, using: &generator)).clamped(to: yRange)) + newX2 = x2 + newY2 = y2 case 1: - x2 = Double((Int(x2) + Int._random(in: range16, using: &generator)).clamped(to: xRange)) - y2 = Double((Int(y2) + Int._random(in: range16, using: &generator)).clamped(to: yRange)) + newX1 = x1 + newY1 = y1 + newX2 = Double((Int(x2) + Int._random(in: range16, using: &generator)).clamped(to: xRange)) + newY2 = Double((Int(y2) + Int._random(in: range16, using: &generator)).clamped(to: yRange)) default: fatalError("Internal inconsistency") } + return Rectangle(strokeWidth: strokeWidth, x1: newX1, y1: newY1, x2: newX2, y2: newY2) } public func rasterize(x xRange: ClosedRange, y yRange: ClosedRange) -> [Scanline] { diff --git a/Sources/geometrize/Shapes/RotatedEllipse.swift b/Sources/geometrize/Shapes/RotatedEllipse.swift index adea12d..b62cad2 100644 --- a/Sources/geometrize/Shapes/RotatedEllipse.swift +++ b/Sources/geometrize/Shapes/RotatedEllipse.swift @@ -1,15 +1,15 @@ import Foundation // Represents a rotated ellipse. -public final class RotatedEllipse: Shape { - public var strokeWidth: Double - public var x: Double // x-coordinate. - public var y: Double // y-coordinate. - public var rx: Double // x-radius. - public var ry: Double // y-radius. - public var angleDegrees: Double // Rotation angle in degrees. +public struct RotatedEllipse: Shape { + public let strokeWidth: Double + public let x: Double // x-coordinate. + public let y: Double // y-coordinate. + public let rx: Double // x-radius. + public let ry: Double // y-radius. + public let angleDegrees: Double // Rotation angle in degrees. - public required init(strokeWidth: Double) { + public init(strokeWidth: Double) { self.strokeWidth = strokeWidth x = 0.0 y = 0.0 @@ -31,32 +31,67 @@ public final class RotatedEllipse: Shape { RotatedEllipse(strokeWidth: strokeWidth, x: x, y: y, rx: rx, ry: ry, angleDegrees: angleDegrees) } - public func setup(x xRange: ClosedRange, y yRange: ClosedRange, using generator: inout SplitMix64) { - x = Double(Int._random(in: xRange, using: &generator)) - y = Double(Int._random(in: yRange, using: &generator)) + public func setup( + x xRange: ClosedRange, + y yRange: ClosedRange, + using generator: inout SplitMix64 + ) -> RotatedEllipse { let range32 = 1...32 - rx = Double(Int._random(in: range32, using: &generator)) - ry = Double(Int._random(in: range32, using: &generator)) - angleDegrees = Double(Int._random(in: 0...360, using: &generator)) + return RotatedEllipse( + strokeWidth: strokeWidth, + x: Double(Int._random(in: xRange, using: &generator)), + y: Double(Int._random(in: yRange, using: &generator)), + rx: Double(Int._random(in: range32, using: &generator)), + ry: Double(Int._random(in: range32, using: &generator)), + angleDegrees: Double(Int._random(in: 0...360, using: &generator)) + ) } - public func mutate(x xRange: ClosedRange, y yRange: ClosedRange, using generator: inout SplitMix64) { + public func mutate( + x xRange: ClosedRange, + y yRange: ClosedRange, + using generator: inout SplitMix64 + ) -> RotatedEllipse { let range16 = -16...16 + var newX, newY, newRx, newRy, newAngleDegrees: Double switch Int._random(in: 0...3, using: &generator) { case 0: - x = Double((Int(x) + Int._random(in: range16, using: &generator)).clamped(to: xRange)) - y = Double((Int(y) + Int._random(in: range16, using: &generator)).clamped(to: yRange)) + newX = Double((Int(x) + Int._random(in: range16, using: &generator)).clamped(to: xRange)) + newY = Double((Int(y) + Int._random(in: range16, using: &generator)).clamped(to: yRange)) + newRx = rx + newRy = ry + newAngleDegrees = angleDegrees case 1: - rx = Double((Int(rx) + Int._random(in: range16, using: &generator)).clamped(to: 1...xRange.upperBound)) + newX = x + newY = y + newRx = Double((Int(rx) + Int._random(in: range16, using: &generator)).clamped(to: 1...xRange.upperBound)) + newRy = ry + newAngleDegrees = angleDegrees case 2: - ry = Double((Int(ry) + Int._random(in: range16, using: &generator)).clamped(to: 1...yRange.upperBound)) + newX = x + newY = y + newRx = rx + newRy = Double((Int(ry) + Int._random(in: range16, using: &generator)).clamped(to: 1...yRange.upperBound)) + newAngleDegrees = angleDegrees case 3: - angleDegrees = Double( + newX = x + newY = y + newRx = rx + newRy = ry + newAngleDegrees = Double( (Int(angleDegrees) + Int._random(in: range16, using: &generator)).clamped(to: 0...360) ) default: fatalError("Internal inconsistency") } + return RotatedEllipse( + strokeWidth: strokeWidth, + x: newX, + y: newY, + rx: newRx, + ry: newRy, + angleDegrees: newAngleDegrees + ) } public func rasterize(x xRange: ClosedRange, y yRange: ClosedRange) -> [Scanline] { diff --git a/Sources/geometrize/Shapes/RotatedRectangle.swift b/Sources/geometrize/Shapes/RotatedRectangle.swift index d6a1265..770983d 100644 --- a/Sources/geometrize/Shapes/RotatedRectangle.swift +++ b/Sources/geometrize/Shapes/RotatedRectangle.swift @@ -1,15 +1,15 @@ import Foundation /// Represents a rotated rectangle. -public final class RotatedRectangle: Shape { - public var strokeWidth: Double - public var x1: Double - public var y1: Double - public var x2: Double - public var y2: Double - public var angleDegrees: Double - - public required init(strokeWidth: Double) { +public struct RotatedRectangle: Shape { + public let strokeWidth: Double + public let x1: Double + public let y1: Double + public let x2: Double + public let y2: Double + public let angleDegrees: Double + + public init(strokeWidth: Double) { self.strokeWidth = strokeWidth x1 = 0.0 y1 = 0.0 @@ -31,29 +31,60 @@ public final class RotatedRectangle: Shape { RotatedRectangle(strokeWidth: strokeWidth, x1: x1, y1: y1, x2: x2, y2: y2, angleDegrees: angleDegrees) } - public func setup(x xRange: ClosedRange, y yRange: ClosedRange, using generator: inout SplitMix64) { - x1 = Double(Int._random(in: xRange, using: &generator)) - y1 = Double(Int._random(in: yRange, using: &generator)) + public func setup( + x xRange: ClosedRange, + y yRange: ClosedRange, + using generator: inout SplitMix64 + ) -> RotatedRectangle { let range32 = 1...32 - x2 = Double((Int(x1) + Int._random(in: range32, using: &generator)).clamped(to: xRange)) - y2 = Double((Int(y1) + Int._random(in: range32, using: &generator)).clamped(to: yRange)) - angleDegrees = Double(Int._random(in: 0...360, using: &generator)) + return RotatedRectangle( + strokeWidth: strokeWidth, + x1: Double(Int._random(in: xRange, using: &generator)), + y1: Double(Int._random(in: yRange, using: &generator)), + x2: Double((Int(x1) + Int._random(in: range32, using: &generator)).clamped(to: xRange)), + y2: Double((Int(y1) + Int._random(in: range32, using: &generator)).clamped(to: yRange)), + angleDegrees: Double(Int._random(in: 0...360, using: &generator)) + ) } - public func mutate(x xRange: ClosedRange, y yRange: ClosedRange, using generator: inout SplitMix64) { + public func mutate( + x xRange: ClosedRange, + y yRange: ClosedRange, + using generator: inout SplitMix64 + ) -> RotatedRectangle { let range16 = -16...16 + let newX1, newY1, newX2, newY2, newAngleDegrees: Double switch Int._random(in: 0...2, using: &generator) { case 0: - x1 = Double((Int(x1) + Int._random(in: range16, using: &generator)).clamped(to: xRange)) - y1 = Double((Int(y1) + Int._random(in: range16, using: &generator)).clamped(to: yRange)) + newX1 = Double((Int(x1) + Int._random(in: range16, using: &generator)).clamped(to: xRange)) + newY1 = Double((Int(y1) + Int._random(in: range16, using: &generator)).clamped(to: yRange)) + newX2 = x2 + newY2 = y2 + newAngleDegrees = angleDegrees case 1: - x2 = Double((Int(x2) + Int._random(in: range16, using: &generator)).clamped(to: xRange)) - y2 = Double((Int(y2) + Int._random(in: range16, using: &generator)).clamped(to: yRange)) + newX1 = x1 + newY1 = y1 + newX2 = Double((Int(x2) + Int._random(in: range16, using: &generator)).clamped(to: xRange)) + newY2 = Double((Int(y2) + Int._random(in: range16, using: &generator)).clamped(to: yRange)) + newAngleDegrees = angleDegrees case 2: - angleDegrees = Double((Int(angleDegrees) + Int._random(in: -4...4, using: &generator)).clamped(to: 0...360)) + newX1 = x1 + newY1 = y1 + newX2 = x2 + newY2 = y2 + newAngleDegrees = Double((Int(angleDegrees) + Int._random(in: -4...4, using: &generator)) + .clamped(to: 0...360)) default: fatalError("Internal inconsistency") } + return RotatedRectangle( + strokeWidth: strokeWidth, + x1: newX1, + y1: newY1, + x2: newX2, + y2: newY2, + angleDegrees: newAngleDegrees + ) } public func rasterize(x xRange: ClosedRange, y yRange: ClosedRange) -> [Scanline] { diff --git a/Sources/geometrize/Shapes/Shape.swift b/Sources/geometrize/Shapes/Shape.swift index 006e431..6e0395c 100644 --- a/Sources/geometrize/Shapes/Shape.swift +++ b/Sources/geometrize/Shapes/Shape.swift @@ -1,19 +1,33 @@ import Foundation -public protocol Shape: AnyObject, CustomStringConvertible { - var strokeWidth: Double { get } - - init(strokeWidth: Double) +public protocol Copyable { func copy() -> Self - func setup(x: ClosedRange, y: ClosedRange, using: inout SplitMix64) +} + +public protocol Mutable { + + func setup(x: ClosedRange, y: ClosedRange, using: inout SplitMix64) -> Self + + func mutate(x: ClosedRange, y: ClosedRange, using: inout SplitMix64) -> Self + +} - func mutate(x: ClosedRange, y: ClosedRange, using: inout SplitMix64) +public protocol Rasterizable { + + var strokeWidth: Double { get } + + init(strokeWidth: Double) func rasterize(x: ClosedRange, y: ClosedRange) -> [Scanline] +} + +public protocol Shape: Sendable, Copyable, Mutable, Rasterizable, CustomStringConvertible { + var isDegenerate: Bool { get } + } extension Shape { diff --git a/Sources/geometrize/Shapes/Triangle.swift b/Sources/geometrize/Shapes/Triangle.swift index eb9ce73..e981cb2 100644 --- a/Sources/geometrize/Shapes/Triangle.swift +++ b/Sources/geometrize/Shapes/Triangle.swift @@ -1,13 +1,13 @@ import Foundation -public final class Triangle: Shape { - public var strokeWidth: Double - public var x1: Double // First x-coordinate. - public var y1: Double // First y-coordinate. - public var x2: Double // Second x-coordinate. - public var y2: Double // Second y-coordinate. - public var x3: Double // Third x-coordinate. - public var y3: Double // Third y-coordinate. +public struct Triangle: Shape { + public let strokeWidth: Double + public let x1: Double // First x-coordinate. + public let y1: Double // First y-coordinate. + public let x2: Double // Second x-coordinate. + public let y2: Double // Second y-coordinate. + public let x3: Double // Third x-coordinate. + public let y3: Double // Third y-coordinate. public init(strokeWidth: Double) { self.strokeWidth = strokeWidth @@ -33,31 +33,56 @@ public final class Triangle: Shape { Triangle(strokeWidth: strokeWidth, x1: x1, y1: y1, x2: x2, y2: y2, x3: x3, y3: y3) } - public func setup(x xRange: ClosedRange, y yRange: ClosedRange, using generator: inout SplitMix64) { - x1 = Double(Int._random(in: xRange, using: &generator)) - y1 = Double(Int._random(in: yRange, using: &generator)) + public func setup( + x xRange: ClosedRange, + y yRange: ClosedRange, + using generator: inout SplitMix64 + ) -> Triangle { let range32 = 1...32 - x2 = Double((Int(x1) + Int._random(in: range32, using: &generator)).clamped(to: xRange)) - y2 = Double((Int(y1) + Int._random(in: range32, using: &generator)).clamped(to: yRange)) - x3 = Double((Int(x1) + Int._random(in: range32, using: &generator)).clamped(to: xRange)) - y3 = Double((Int(y1) + Int._random(in: range32, using: &generator)).clamped(to: yRange)) + return Triangle( + strokeWidth: strokeWidth, + x1: Double(Int._random(in: xRange, using: &generator)), + y1: Double(Int._random(in: yRange, using: &generator)), + x2: Double((Int(x1) + Int._random(in: range32, using: &generator)).clamped(to: xRange)), + y2: Double((Int(y1) + Int._random(in: range32, using: &generator)).clamped(to: yRange)), + x3: Double((Int(x1) + Int._random(in: range32, using: &generator)).clamped(to: xRange)), + y3: Double((Int(y1) + Int._random(in: range32, using: &generator)).clamped(to: yRange)) + ) } - public func mutate(x xRange: ClosedRange, y yRange: ClosedRange, using generator: inout SplitMix64) { + public func mutate( + x xRange: ClosedRange, + y yRange: ClosedRange, + using generator: inout SplitMix64 + ) -> Triangle { let range32 = -32...32 + let newX1, newY1, newX2, newY2, newX3, newY3: Double switch Int._random(in: 0...2, using: &generator) { case 0: - x1 = Double((Int(x1) + Int._random(in: range32, using: &generator)).clamped(to: xRange)) - y1 = Double((Int(y1) + Int._random(in: range32, using: &generator)).clamped(to: yRange)) + newX1 = Double((Int(x1) + Int._random(in: range32, using: &generator)).clamped(to: xRange)) + newY1 = Double((Int(y1) + Int._random(in: range32, using: &generator)).clamped(to: yRange)) + newX2 = x2 + newY2 = y2 + newX3 = x3 + newY3 = y3 case 1: - x2 = Double((Int(x2) + Int._random(in: range32, using: &generator)).clamped(to: xRange)) - y2 = Double((Int(y2) + Int._random(in: range32, using: &generator)).clamped(to: yRange)) + newX1 = x1 + newY1 = y1 + newX2 = Double((Int(x2) + Int._random(in: range32, using: &generator)).clamped(to: xRange)) + newY2 = Double((Int(y2) + Int._random(in: range32, using: &generator)).clamped(to: yRange)) + newX3 = x3 + newY3 = y3 case 2: - x3 = Double((Int(x2) + Int._random(in: range32, using: &generator)).clamped(to: xRange)) - y3 = Double((Int(y2) + Int._random(in: range32, using: &generator)).clamped(to: yRange)) + newX1 = x1 + newY1 = y1 + newX2 = x2 + newY2 = y2 + newX3 = Double((Int(x2) + Int._random(in: range32, using: &generator)).clamped(to: xRange)) + newY3 = Double((Int(y2) + Int._random(in: range32, using: &generator)).clamped(to: yRange)) default: fatalError("Internal inconsistency") } + return Triangle(strokeWidth: strokeWidth, x1: newX1, y1: newY1, x2: newX2, y2: newY2, x3: newX3, y3: newY3) } public func rasterize(x xRange: ClosedRange, y yRange: ClosedRange) -> [Scanline] { diff --git a/Sources/geometrize/State.swift b/Sources/geometrize/State.swift index 5782781..060bd71 100644 --- a/Sources/geometrize/State.swift +++ b/Sources/geometrize/State.swift @@ -1,16 +1,6 @@ import Foundation -struct State { - - /// Creates a new state. - /// - Parameters: - /// - shape: The shape. - /// - alpha: The color alpha of the geometric shape. - init(shape: some Shape, alpha: UInt8) { - self.score = -1 - self.alpha = alpha - self.shape = shape - } +struct State: Sendable { init(score: Double, alpha: UInt8, shape: some Shape) { self.score = score @@ -18,30 +8,25 @@ struct State { self.shape = shape } - func copy() -> State { - State(score: score, alpha: alpha, shape: shape.copy()) - } - /// The score of the state, a measure of the improvement applying the state to the current bitmap will have. - var score: Double // TODO: what is valid range of the score? + let score: Double // TODO: what is valid range of the score? /// The alpha of the shape. - var alpha: UInt8 + let alpha: UInt8 /// The geometric primitive owned by the state. - var shape: any Shape + let shape: any Shape /// Modifies the current state in a random fashion. - /// - Returns: The old state, useful for undoing the mutation or keeping track of previous states. + /// - Returns: new mutated state. mutating func mutate( x xRange: ClosedRange, // TODO: rename y yRange: ClosedRange, // TODO: rename - using generator: inout SplitMix64 + using generator: inout SplitMix64, + score: ((any Shape) -> Double) ) -> State { - let oldState = copy() - shape.mutate(x: xRange, y: yRange, using: &generator) - score = -1 - return oldState + let mutatedShape = shape.mutate(x: xRange, y: yRange, using: &generator) + return State(score: score(mutatedShape), alpha: alpha, shape: mutatedShape) } }