use super::polar::PolarDiagram;
use super::svg::SvgTheme;
use crate::Eulumdat;
const PV_PLANE_DEG: f64 = 0.0;
const PC_PLANE_DEG: f64 = 90.0;
pub fn principal_planes_svg(ldt: &Eulumdat, width: f64, height: f64, theme: &SvgTheme) -> String {
let gutter = 12.0;
let panel_w = ((width - gutter) / 2.0).max(120.0);
let panel_h = height.max(120.0);
let panel_size = panel_w.min(panel_h);
let pv = PolarDiagram::from_eulumdat_for_plane(ldt, PV_PLANE_DEG);
let pc = PolarDiagram::from_eulumdat_for_plane(ldt, PC_PLANE_DEG);
let mut pv = pv;
let mut pc = pc;
let shared_max = pv.scale.scale_max.max(pc.scale.scale_max);
pv.scale.scale_max = shared_max;
pc.scale.scale_max = shared_max;
let pv_inner = inner_svg(&pv.to_svg(panel_size, panel_size, theme));
let pc_inner = inner_svg(&pc.to_svg(panel_size, panel_size, theme));
let mut svg = String::new();
svg.push_str(&format!(
r#"<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 {width} {height}" font-family="{font}">"#,
font = theme.font_family
));
svg.push_str(&format!(
r#"<rect x="0" y="0" width="{width}" height="{height}" fill="{}"/>"#,
theme.background
));
let left_x = (panel_w - panel_size) / 2.0;
svg.push_str(&format!(
r#"<g transform="translate({left_x:.2},0)">{pv_inner}</g>"#,
));
svg.push_str(&format!(
r#"<text x="{x:.1}" y="{y:.1}" fill="{c}" font-size="13" font-weight="bold" text-anchor="middle">PV — along road (C0–C180)</text>"#,
x = panel_w / 2.0,
y = 18.0,
c = theme.text,
));
let right_x_origin = panel_w + gutter;
let right_x = right_x_origin + (panel_w - panel_size) / 2.0;
svg.push_str(&format!(
r#"<g transform="translate({right_x:.2},0)">{pc_inner}</g>"#,
));
svg.push_str(&format!(
r#"<text x="{x:.1}" y="{y:.1}" fill="{c}" font-size="13" font-weight="bold" text-anchor="middle">PC — across road (C90–C270)</text>"#,
x = right_x_origin + panel_w / 2.0,
y = 18.0,
c = theme.text,
));
svg.push_str("</svg>");
svg
}
fn inner_svg(s: &str) -> String {
let start = s
.find("<svg")
.and_then(|i| s[i..].find('>').map(|j| i + j + 1));
let end = s.rfind("</svg>");
match (start, end) {
(Some(a), Some(b)) if a <= b => s[a..b].to_string(),
_ => s.to_string(),
}
}
#[cfg(test)]
mod tests {
use super::*;
fn road_ldt() -> Eulumdat {
let p = "../eulumdat-wasm/templates/road_luminaire.ldt";
let content = std::fs::read_to_string(p).expect("template must exist");
Eulumdat::parse(&content).expect("template must parse")
}
#[test]
fn principal_planes_svg_emits_both_panels() {
let ldt = road_ldt();
let svg = principal_planes_svg(&ldt, 800.0, 420.0, &SvgTheme::default());
assert!(svg.starts_with("<svg"));
assert!(svg.ends_with("</svg>"));
assert!(svg.contains("PV"), "PV caption missing: {}", &svg[..200]);
assert!(svg.contains("PC"), "PC caption missing");
assert!(svg.contains("C0–C180"), "PV plane label missing");
assert!(svg.contains("C90–C270"), "PC plane label missing");
assert!(
svg.matches("<path").count() >= 2,
"expected ≥2 curves, got {}",
svg.matches("<path").count()
);
}
#[test]
fn inner_svg_strips_wrapper() {
let s = r#"<svg xmlns="..." viewBox="0 0 10 10"><circle/></svg>"#;
assert_eq!(inner_svg(s), "<circle/>");
}
#[test]
fn inner_svg_passes_through_when_no_wrapper() {
let s = "no svg here";
assert_eq!(inner_svg(s), s);
}
}