///|
/// Parameters shared by all procedural motif generators.
pub(all) struct GeneratorOptions {
  center : Vec2
  radius : Double
  count : Int
  phase : Double
  jitter : Double
  seed : UInt
} derive(Eq, Debug, ToJson)

///|
pub fn GeneratorOptions::new(
  center : Vec2,
  radius : Double,
  count : Int,
) -> GeneratorOptions {
  { center, radius, count, phase: 0.0, jitter: 0.0, seed: 1U }
}

///|
pub fn GeneratorOptions::with_phase(
  self : GeneratorOptions,
  phase : Double,
) -> GeneratorOptions {
  { ..self, phase, }
}

///|
pub fn GeneratorOptions::with_jitter(
  self : GeneratorOptions,
  jitter : Double,
  seed : UInt,
) -> GeneratorOptions {
  { ..self, jitter, seed }
}

///|
/// Generates a radial ring of points, optionally using deterministic jitter.
pub fn radial_points(options : GeneratorOptions) -> Array[Vec2] {
  let points = []
  let random = Random::new(options.seed)
  if options.count > 0 {
    for i in 0.. Path {
  let path = Path::new()
  if options.count > 1 {
    let total = options.count * 2
    let points = []
    for i in 0.. Path {
  let path = Path::new()
  if options.count > 0 {
    let step = 2.0 * @math.PI / options.count.to_double()
    let first = {
      x: options.center.x + @math.cos(options.phase) * inner_radius,
      y: options.center.y + @math.sin(options.phase) * inner_radius,
    }
    let _ = path.move_to(first.x, first.y)
    for i in 0.. Path {
  let path = Path::new()
  if waves > 0 {
    let steps = waves * 8
    let dx = width / steps.to_double()
    let _ = path.move_to(x, y + height / 2.0)
    for i in 1..<=steps {
      let px = x + i.to_double() * dx
      let py = y +
        height / 2.0 +
        @math.sin(phase + i.to_double() * 2.0 * @math.PI / 8.0) * amplitude
      let _ = path.line_to(px, py)
    }
    for reverse in 1..<=steps {
      let i = steps - reverse
      let px = x + i.to_double() * dx
      let py = y +
        height / 2.0 +
        @math.sin(phase + i.to_double() * 2.0 * @math.PI / 8.0) * amplitude +
        height / 3.0
      let _ = path.line_to(px, py)
    }
    let _ = path.close()
  }
  path
}

///|
/// Generates concentric rings with a fixed number of samples per ring.
pub fn concentric_rings(
  center : Vec2,
  first_radius : Double,
  gap : Double,
  rings : Int,
  samples : Int,
  phase? : Double = 0.0,
) -> Array[Path] {
  let result = []
  for ring in 0.. Array[Path] {
  let result = []
  if cols > 0 && rows > 0 {
    let sx = width / cols.to_double()
    let sy = height / rows.to_double()
    for row in 0.. Array[Rect] {
  let result = []
  let random = Random::new(seed)
  if columns > 0 && rows > 0 {
    let cell_width = width / columns.to_double()
    let cell_height = height / rows.to_double()
    for row in 0..