///|
/// A layout score used to compare candidate compositions.
pub(all) struct LayoutMetrics {
occupied_area : Double
path_length : Double
element_count : Int
density : Double
bounds : Rect
} derive(Eq, Debug, ToJson)
///|
pub fn measure_paths(paths : Array[Path], canvas : Rect) -> LayoutMetrics {
let mut occupied = 0.0
let mut length = 0.0
let mut bounds = Rect::new(0.0, 0.0, 0.0, 0.0)
let mut first = true
for path in paths {
let box_ = path.bounds()
if first {
bounds = box_
first = false
} else {
bounds = bounds.union(box_)
}
occupied += box_.width * box_.height
length += path.length()
}
let canvas_area = canvas.width * canvas.height
{
occupied_area: occupied,
path_length: length,
element_count: paths.length(),
density: if canvas_area == 0.0 {
0.0
} else {
occupied / canvas_area
},
bounds,
}
}
///|
/// Returns an evenly spaced set of anchor positions inside a canvas.
pub fn anchor_grid(
canvas : Rect,
columns : Int,
rows : Int,
margin : Double,
) -> Array[Vec2] {
let anchors = []
if columns > 0 && rows > 0 {
let inner = canvas.expand(-margin)
let sx = inner.width / columns.to_double()
let sy = inner.height / rows.to_double()
for row in 0.. Array[Rect] {
let result = []
let mut cursor_x = canvas.x
let mut cursor_y = canvas.y
let mut row_height = 0.0
for item in sizes {
let width = item.0
let height = item.1
if cursor_x + width > canvas.right() && cursor_x > canvas.x {
cursor_x = canvas.x
cursor_y += row_height + gap
row_height = 0.0
}
if cursor_y + height <= canvas.bottom() {
result.push(Rect::new(cursor_x, cursor_y, width, height))
cursor_x += width + gap
if height > row_height {
row_height = height
}
}
}
result
}
///|
/// Distributes points on a spiral with a golden-angle increment.
pub fn golden_spiral(
center : Vec2,
count : Int,
radial_step : Double,
start_angle? : Double = 0.0,
) -> Array[Vec2] {
let result = []
let golden_angle = @math.PI * (3.0 - 5.0.sqrt())
for i in 0.. Double {
let coverage = if metrics.density > 1.0 { 0.0 } else { metrics.density }
let center = metrics.bounds.center()
let canvas_center = canvas.center()
let max_distance = canvas.center().distance(Vec2::new(canvas.x, canvas.y))
let balance = if max_distance == 0.0 {
1.0
} else {
1.0 - center.distance(canvas_center) / max_distance
}
let complexity = if metrics.element_count == 0 {
0.0
} else {
(metrics.path_length / (canvas.width + canvas.height)).min(1.0)
}
(coverage * 0.45 + balance * 0.35 + complexity * 0.2).max(0.0).min(1.0)
}
///|
pub fn normalize_points(points : Array[Vec2], canvas : Rect) -> Array[Vec2] {
let result = []
let box_ = bounds_of_points(points)
if box_.width == 0.0 || box_.height == 0.0 {
result
} else {
let sx = canvas.width / box_.width
let sy = canvas.height / box_.height
let scale = if sx < sy { sx } else { sy }
for point in points {
result.push(
Vec2::new(
canvas.x + (point.x - box_.x) * scale,
canvas.y + (point.y - box_.y) * scale,
),
)
}
result
}
}