Skip to content

Commit

Permalink
Make shapes immutable
Browse files Browse the repository at this point in the history
  • Loading branch information
valeriyvan committed Jul 17, 2024
1 parent a183b0a commit 858a7a9
Show file tree
Hide file tree
Showing 15 changed files with 422 additions and 218 deletions.
2 changes: 1 addition & 1 deletion Sources/geometrize/Bitmap.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down
62 changes: 30 additions & 32 deletions Sources/geometrize/HillClimb.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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
}
4 changes: 2 additions & 2 deletions Sources/geometrize/Point.swift
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import Foundation

public struct Point<N: Numeric> {
public var x, y: N
public struct Point<N: Numeric & Sendable>: Sendable {
public let x, y: N

public init(x: N, y: N) {
self.x = x
Expand Down
2 changes: 1 addition & 1 deletion Sources/geometrize/Polygon.swift
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import Foundation
import Algorithms

struct Polygon<N: SignedInteger> {
struct Polygon<N: SignedInteger & Sendable> {
var vertices: [Point<N>]

init(vertices: [Point<N>]) throws {
Expand Down
42 changes: 29 additions & 13 deletions Sources/geometrize/Shapes/Circle.swift
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -24,23 +24,39 @@ public final class Circle: Shape {
Circle(strokeWidth: strokeWidth, x: x, y: y, r: r)
}

public func setup(x xRange: ClosedRange<Int>, y yRange: ClosedRange<Int>, 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<Int>,
y yRange: ClosedRange<Int>,
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<Int>, y yRange: ClosedRange<Int>, using generator: inout SplitMix64) {
public func mutate(
x xRange: ClosedRange<Int>,
y yRange: ClosedRange<Int>,
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<Int>, y yRange: ClosedRange<Int>) -> [Scanline] {
Expand Down
53 changes: 37 additions & 16 deletions Sources/geometrize/Shapes/Ellipse.swift
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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<Int>, y yRange: ClosedRange<Int>, 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<Int>,
y yRange: ClosedRange<Int>,
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<Int>, y yRange: ClosedRange<Int>, using generator: inout SplitMix64) {
public func mutate(
x xRange: ClosedRange<Int>,
y yRange: ClosedRange<Int>,
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<Int>, y yRange: ClosedRange<Int>) -> [Scanline] {
Expand Down
43 changes: 30 additions & 13 deletions Sources/geometrize/Shapes/Line.swift
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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<Int>, y yRange: ClosedRange<Int>, using generator: inout SplitMix64) {
public func setup(
x xRange: ClosedRange<Int>,
y yRange: ClosedRange<Int>,
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<Int>, y yRange: ClosedRange<Int>, using generator: inout SplitMix64) {
public func mutate(
x xRange: ClosedRange<Int>,
y yRange: ClosedRange<Int>,
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<Int>, y yRange: ClosedRange<Int>) -> [Scanline] {
Expand Down
32 changes: 22 additions & 10 deletions Sources/geometrize/Shapes/Polyline.swift
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import Foundation

public final class Polyline: Shape {
public var strokeWidth: Double
public var points: [Point<Double>]
public struct Polyline: Shape {
public let strokeWidth: Double
public let points: [Point<Double>]

public init(strokeWidth: Double) {
self.strokeWidth = strokeWidth
Expand All @@ -18,7 +18,11 @@ public final class Polyline: Shape {
Polyline(strokeWidth: strokeWidth, points: points)
}

public func setup(x xRange: ClosedRange<Int>, y yRange: ClosedRange<Int>, using generator: inout SplitMix64) {
public func setup(
x xRange: ClosedRange<Int>,
y yRange: ClosedRange<Int>,
using generator: inout SplitMix64
) -> Polyline {
let range32 = -32...32
let startingPoint = Point(
x: Int._random(in: xRange, using: &generator),
Expand All @@ -33,16 +37,24 @@ public final class Polyline: Shape {
)
)
}
self.points = points
return Polyline(strokeWidth: strokeWidth, points: points)
}

public func mutate(x xRange: ClosedRange<Int>, y yRange: ClosedRange<Int>, using generator: inout SplitMix64) {
public func mutate(
x xRange: ClosedRange<Int>,
y yRange: ClosedRange<Int>,
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<Int>, y yRange: ClosedRange<Int>) -> [Scanline] {
Expand Down
Loading

0 comments on commit 858a7a9

Please sign in to comment.