// Moonlight - Rendering Utilities (Pure Functions)
// SVG レンダリング用の純粋関数群
// ============================================================
// テキストサイズ計算
// ============================================================
///|
/// テキストヒットエリアのサイズを計算
/// 文字数とフォントサイズからヒットエリアの幅と高さを推定する
pub fn calc_text_hit_area(
content : String,
font_size : Double,
) -> (Double, Double) {
// 文字幅係数(等幅フォント想定、実際のフォントにより異なる)
let char_width_ratio = 0.55
let line_height_ratio = 1.2
let min_width = font_size * 2.0
let min_height = 24.0
if content.contains("\n") {
// 複数行: 各行の長さを計算
let lines : Array[String] = content
.split("\n")
.map(fn(sv) { sv.to_string() })
.collect()
let line_count = lines.length()
let line_height = font_size * line_height_ratio
let total_height = line_count.to_double() * line_height
// 最長行の文字数を取得
let max_line_len = lines.fold(init=0, fn(acc, line) {
let len = line.iter().count()
if len > acc {
len
} else {
acc
}
})
let estimated_width = max_line_len.to_double() *
font_size *
char_width_ratio
let w = if estimated_width < 40.0 { 40.0 } else { estimated_width }
let h = if total_height < min_height { min_height } else { total_height }
(w, h)
} else {
// 単一行: 文字数から幅を推定
let char_count = content.iter().count()
let estimated_width = char_count.to_double() * font_size * char_width_ratio
let w = if estimated_width < min_width {
min_width
} else {
estimated_width
}
(w, font_size * line_height_ratio)
}
}
///|
/// テキストのバウンディングボックスを計算
/// 中央揃えを考慮した位置とサイズを返す
pub fn calc_text_bounds(
content : String,
x : Double,
y : Double,
font_size : Double,
) -> BoundingBox {
let (width, height) = calc_text_hit_area(content, font_size)
// テキストは中央揃えなので、位置を調整
{ x: x - width / 2.0, y: y - height / 2.0, width, height }
}
// ============================================================
// スタイル属性変換
// ============================================================
///|
/// スタイルをSVG属性のリストに変換
pub fn style_to_svg_attrs(style : Style) -> Array[(String, String)] {
let attrs : Array[(String, String)] = []
// fill
match style.fill {
Some(v) => attrs.push(("fill", v))
None => attrs.push(("fill", "none"))
}
// stroke
if style.stroke is Some(v) {
attrs.push(("stroke", v))
}
// stroke-width
if style.stroke_width is Some(v) {
attrs.push(("stroke-width", num_str(v)))
}
// opacity
if style.opacity is Some(v) {
attrs.push(("opacity", num_str(v)))
}
// stroke-dasharray
if style.stroke_dasharray is Some(v) {
attrs.push(("stroke-dasharray", v))
}
attrs
}
///|
/// 矢印マーカー属性を取得
pub fn get_marker_attrs(style : Style) -> Array[(String, String)] {
let attrs : Array[(String, String)] = []
if style.marker_start is Some(Arrow) {
attrs.push(("marker-start", "url(#arrow-start-context)"))
}
if style.marker_end is Some(Arrow) {
attrs.push(("marker-end", "url(#arrow-end-context)"))
}
attrs
}
// ============================================================
// 要素更新の純粋関数
// ============================================================
///|
/// 要素の位置を更新した新しい要素を返す
pub fn update_element_position(
el : Element,
new_x : Double,
new_y : Double,
) -> Element {
{ ..el, x: new_x, y: new_y }
}
///|
/// ラインの端点を更新した新しい要素を返す
pub fn update_line_endpoint(
el : Element,
endpoint : String,
new_x : Double,
new_y : Double,
) -> Element {
match el.shape {
Line(_, _) =>
if endpoint == "start" {
// 始点を更新
{ ..el, x: new_x, y: new_y }
} else {
// 終点を更新
{ ..el, shape: Line(new_x, new_y) }
}
_ => el // Line 以外は変更しない
}
}
///|
/// 要素配列から指定IDの要素を更新
pub fn update_element_in_array(
elements : Array[Element],
id : String,
updater : (Element) -> Element,
) -> Array[Element] {
elements
.iter()
.map(fn(el) { if el.id == id { updater(el) } else { el } })
.collect()
}
///|
/// 複数の要素を一括更新
pub fn update_elements_in_array(
elements : Array[Element],
updates : Array[(String, (Element) -> Element)],
) -> Array[Element] {
let update_map : Map[String, (Element) -> Element] = {}
for pair in updates {
let (id, updater) = pair
update_map[id] = updater
}
elements
.iter()
.map(fn(el) {
match update_map.get(el.id) {
Some(updater) => updater(el)
None => el
}
})
.collect()
}
// ============================================================
// 座標計算ユーティリティ
// ============================================================
///|
/// 2点間の距離を計算
pub fn calc_distance(
x1 : Double,
y1 : Double,
x2 : Double,
y2 : Double,
) -> Double {
let dx = x2 - x1
let dy = y2 - y1
(dx * dx + dy * dy).sqrt()
}
///|
/// 点が円内にあるかチェック
pub fn point_in_circle(
px : Double,
py : Double,
cx : Double,
cy : Double,
radius : Double,
) -> Bool {
calc_distance(px, py, cx, cy) <= radius
}
///|
/// 点が矩形内にあるかチェック
pub fn point_in_rect(
px : Double,
py : Double,
rx : Double,
ry : Double,
width : Double,
height : Double,
) -> Bool {
px >= rx && px <= rx + width && py >= ry && py <= ry + height
}
///|
/// 線分と点の最短距離を計算
pub fn point_to_line_distance(
px : Double,
py : Double,
x1 : Double,
y1 : Double,
x2 : Double,
y2 : Double,
) -> Double {
let dx = x2 - x1
let dy = y2 - y1
let len_sq = dx * dx + dy * dy
if len_sq == 0.0 {
// 線分が点の場合
return calc_distance(px, py, x1, y1)
}
// 線分上の最近点のパラメータ t を計算
let t = ((px - x1) * dx + (py - y1) * dy) / len_sq
let t_clamped = if t < 0.0 { 0.0 } else if t > 1.0 { 1.0 } else { t }
// 最近点の座標
let nearest_x = x1 + t_clamped * dx
let nearest_y = y1 + t_clamped * dy
calc_distance(px, py, nearest_x, nearest_y)
}