Module: Vizcore::DSL::LayoutHelpers

Included in:
LayerBuilder
Defined in:
lib/vizcore/dsl/layout_helpers.rb

Overview

Reusable layout helper methods for DSL shape point generation.

Instance Method Summary collapse

Instance Method Details

#circle_pack(count:, radius:, min_distance: nil, origin: [0, 0]) ⇒ Array<Array<Float>>

Generate points packed in concentric rings up to target count.

Parameters:

  • count (Integer)

    target point count

  • radius (Numeric)

    outer packing radius

  • min_distance (Numeric, nil) (defaults to: nil)

    approximate distance between neighboring points

  • origin (Array<Numeric>) (defaults to: [0, 0])

    origin point [x, y]

Returns:

  • (Array<Array<Float>>)

Raises:

  • (ArgumentError)


141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
# File 'lib/vizcore/dsl/layout_helpers.rb', line 141

def circle_pack(count:, radius:, min_distance: nil, origin: [0, 0])
  target_count = normalize_positive_integer(count, :count)
  outer_radius = normalize_non_negative_number(radius, :radius)
  if target_count == 1
    return [normalize_xy_pair(origin, :origin)]
  end

  min_distance = if min_distance.nil?
    outer_radius.to_f / Math.sqrt(target_count)
  else
    normalize_positive_number(min_distance, :min_distance)
  end

  origin_x, origin_y = normalize_xy_pair(origin, :origin)

  points = [ [origin_x, origin_y] ]
  ring = 1
  while points.length < target_count
    ring_radius = min_distance * ring
    break if ring_radius > outer_radius

    ring_capacity = [[(2.0 * Math::PI * ring_radius / min_distance).round, 6].max, 1].max
    per_ring = [ring_capacity, target_count - points.length].min
    angle_step = (Math::PI * 2.0) / per_ring
    0.upto(per_ring - 1) do |index|
      angle = index * angle_step
      points << [
        normalize_layout_coordinate(origin_x + Math.cos(angle) * ring_radius),
        normalize_layout_coordinate(origin_y + Math.sin(angle) * ring_radius)
      ]
      break if points.length >= target_count
    end

    ring += 1
  end

  return points if points.length >= target_count
  raise ArgumentError, "circle_pack cannot place #{target_count} points within radius #{outer_radius}"
end

#grid(count:, columns: nil, rows: nil, spacing: 1.0, spacing_x: nil, spacing_y: nil, center: true, origin: [0, 0]) ⇒ Array<Array<Float>>

Generate a rectangular grid of points.

Parameters:

  • count (Integer)

    total number of points to generate

  • columns (Integer, nil) (defaults to: nil)

    optional grid column count

  • rows (Integer, nil) (defaults to: nil)

    optional grid row count

  • spacing (Numeric) (defaults to: 1.0)

    spacing used for x/y when axis spacing is omitted

  • spacing_x (Numeric, nil) (defaults to: nil)

    optional override for x spacing

  • spacing_y (Numeric, nil) (defaults to: nil)

    optional override for y spacing

  • center (Boolean) (defaults to: true)

    whether to center the grid around the origin

  • origin (Array<Numeric>) (defaults to: [0, 0])

    origin point [x, y]

Returns:

  • (Array<Array<Float>>)


18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
# File 'lib/vizcore/dsl/layout_helpers.rb', line 18

def grid(count:, columns: nil, rows: nil, spacing: 1.0, spacing_x: nil, spacing_y: nil, center: true, origin: [0, 0])
  point_count = normalize_positive_integer(count, :count)

  columns = normalize_positive_integer(columns, :columns) if columns
  rows = normalize_positive_integer(rows, :rows) if rows

  if columns.nil? && rows.nil?
    raise ArgumentError, "grid requires columns or rows"
  end

  columns ||= Float(point_count) / rows.to_f
  rows ||= Float(point_count) / columns.to_f

  columns = columns.ceil
  rows = rows.ceil

  if columns <= 0 || rows <= 0
    raise ArgumentError, "grid columns and rows must be positive"
  end

  x_step = normalize_positive_number(spacing_x || spacing, :spacing_x)
  y_step = normalize_positive_number(spacing_y || spacing, :spacing_y)

  origin_x, origin_y = normalize_xy_pair(origin, :origin)

  x_offset = center && columns > 1 ? -x_step * (columns - 1) / 2.0 : 0.0
  y_offset = center && rows > 1 ? -y_step * (rows - 1) / 2.0 : 0.0

  points = []
  rows.times do |row|
    columns.times do |column|
      break if points.length >= point_count

      points << [
        origin_x + x_offset + (column * x_step),
        origin_y + y_offset + (row * y_step)
      ]
    end
  end

  points
end

#radial(count:, radius:, start_angle: -90.0,, span: 360.0, radius_jitter: 0.0, seed: nil, origin: [0, 0]) ⇒ Array<Array<Float>>

Generate points evenly distributed around a circle.

Parameters:

  • count (Integer)

    number of points

  • radius (Numeric)

    circle radius

  • start_angle (Numeric) (defaults to: -90.0,)

    start angle in degrees

  • span (Numeric) (defaults to: 360.0)

    angular span in degrees

  • radius_jitter (Numeric) (defaults to: 0.0)

    random jitter applied per-point

  • seed (Integer, nil) (defaults to: nil)

    deterministic jitter seed

  • origin (Array<Numeric>) (defaults to: [0, 0])

    origin point [x, y]

Returns:

  • (Array<Array<Float>>)


71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
# File 'lib/vizcore/dsl/layout_helpers.rb', line 71

def radial(count:, radius:, start_angle: -90.0, span: 360.0, radius_jitter: 0.0, seed: nil, origin: [0, 0])
  point_count = normalize_positive_integer(count, :count)
  radius = normalize_non_negative_number(radius, :radius)
  span = Float(span)
  start = degrees_to_radians(start_angle)
  jitter = normalize_non_negative_number(radius_jitter, :radius_jitter)
  random = Random.new(Integer(seed || 0))

  origin_x, origin_y = normalize_xy_pair(origin, :origin)

  points = []
  point_count.times do |index|
    ratio = point_count == 1 ? 0.0 : index.to_f / point_count.to_f
    angle = start + (ratio * span) * Math::PI / 180.0
    jitter_amount = jitter.zero? ? 0.0 : (random.rand * 2.0 - 1.0) * jitter
    scaled_radius = radius + jitter_amount
    points << [
      normalize_layout_coordinate(origin_x + Math.cos(angle) * scaled_radius),
      normalize_layout_coordinate(origin_y + Math.sin(angle) * scaled_radius)
    ]
  end

  points
end

#scatter(count:, width: nil, height: nil, radius: nil, seed: 0, origin: [0, 0]) ⇒ Array<Array<Float>>

Generate pseudo-random points inside a box or circle.

Parameters:

  • count (Integer)

    number of points

  • width (Numeric, nil) (defaults to: nil)

    box width

  • height (Numeric, nil) (defaults to: nil)

    box height

  • radius (Numeric, nil) (defaults to: nil)

    circular radius alternative to width/height

  • seed (Integer, nil) (defaults to: 0)

    deterministic random seed

  • origin (Array<Numeric>) (defaults to: [0, 0])

    origin point [x, y]

Returns:

  • (Array<Array<Float>>)


190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
# File 'lib/vizcore/dsl/layout_helpers.rb', line 190

def scatter(count:, width: nil, height: nil, radius: nil, seed: 0, origin: [0, 0])
  point_count = normalize_positive_integer(count, :count)

  origin_x, origin_y = normalize_xy_pair(origin, :origin)
  random = Random.new(Integer(seed))

  if width.nil? && height.nil? && radius.nil?
    width = 100.0
    height = 100.0
  end

  if radius && !width && !height
    scatter_radius = normalize_non_negative_number(radius, :radius)
    if scatter_radius.zero?
      return Array.new(point_count) { [origin_x, origin_y] }
    end
  elsif width && height
    width = normalize_positive_number(width, :width)
    height = normalize_positive_number(height, :height)
  else
    raise ArgumentError, "scatter requires width and height together, or radius"
  end

  points = []
  point_count.times do
    if radius
      points << sample_scatter_radius(random, scatter_radius, origin_x, origin_y)
    else
      points << [
        origin_x + (random.rand - 0.5) * width,
        origin_y + (random.rand - 0.5) * height
      ]
    end
  end
  points
end

#spiral(count:, radius:, turns: 2.0, start_radius: 0.0, start_angle: -90.0,, origin: [0, 0]) ⇒ Array<Array<Float>>

Generate spiral points from center outward.

Parameters:

  • count (Integer)

    number of points

  • radius (Numeric)

    outer radius

  • turns (Numeric) (defaults to: 2.0)

    number of turns

  • start_radius (Numeric) (defaults to: 0.0)

    inner radius

  • start_angle (Numeric) (defaults to: -90.0,)

    start angle in degrees

  • origin (Array<Numeric>) (defaults to: [0, 0])

    origin point [x, y]

Returns:

  • (Array<Array<Float>>)


105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
# File 'lib/vizcore/dsl/layout_helpers.rb', line 105

def spiral(count:, radius:, turns: 2.0, start_radius: 0.0, start_angle: -90.0, origin: [0, 0])
  point_count = normalize_positive_integer(count, :count)
  outer_radius = normalize_non_negative_number(radius, :radius)
  start_r = normalize_non_negative_number(start_radius, :start_radius)
  turns = Float(turns)
  start = degrees_to_radians(start_angle)

  if outer_radius < start_r
    raise ArgumentError, "spiral radius must be greater than or equal to start_radius"
  end

  radius_delta = outer_radius - start_r
  full_turns = turns
  origin_x, origin_y = normalize_xy_pair(origin, :origin)

  points = []
  point_count.times do |index|
    ratio = point_count == 1 ? 0.0 : index.to_f / (point_count - 1).to_f
    current_radius = start_r + (radius_delta * ratio)
    angle = start + ratio * full_turns * Math::PI * 2.0
    points << [
      normalize_layout_coordinate(origin_x + Math.cos(angle) * current_radius),
      normalize_layout_coordinate(origin_y + Math.sin(angle) * current_radius)
    ]
  end

  points
end