use crate::model::Bounds;
use super::super::{fmt, fmt_string, svg_emitted_bounds_from_svg};
use super::model::ArchitectureModelAccess;
use super::root::ArchitectureRootOpen;
pub(super) struct ArchitectureRootViewportContext<'a, M: ArchitectureModelAccess> {
pub(super) out: String,
pub(super) diagram_id: &'a str,
pub(super) model: &'a M,
pub(super) root_open: ArchitectureRootOpen,
pub(super) content_bounds: Option<Bounds>,
pub(super) padding_px: f64,
pub(super) icon_size_px: f64,
pub(super) use_max_width: bool,
pub(super) apply_root_overrides: bool,
pub(super) trust_content_bounds: bool,
}
#[derive(Debug, Clone, Copy)]
struct ArchitectureRootViewportProfile {
groups_len: usize,
edges_len: usize,
service_count: usize,
junction_count: usize,
has_inverse_arrow_mesh_edge: bool,
}
impl ArchitectureRootViewportProfile {
fn from_model<M: ArchitectureModelAccess>(model: &M) -> Self {
let groups_len = model.groups_len();
let edges_len = model.edges_len();
let service_count = model.services().count();
let junction_count = model.junctions().count();
let is_arrow_mesh_profile =
groups_len == 0 && service_count == 5 && junction_count == 0 && edges_len == 8;
let has_inverse_arrow_mesh_edge = is_arrow_mesh_profile
&& model
.edges()
.any(|edge| edge.lhs_dir == 'L' && edge.rhs_dir == 'B');
Self {
groups_len,
edges_len,
service_count,
junction_count,
has_inverse_arrow_mesh_edge,
}
}
fn skips_height_snap(self) -> bool {
self.groups_len == 0
&& self.service_count == 5
&& self.junction_count == 0
&& self.edges_len == 8
&& !self.has_inverse_arrow_mesh_edge
}
}
#[derive(Debug, Clone, Copy)]
struct ArchitectureRootViewport {
min_x: f64,
min_y: f64,
width: f64,
height: f64,
}
impl ArchitectureRootViewport {
fn view_box_attr(self) -> String {
format!(
"{} {} {} {}",
fmt(self.min_x),
fmt(self.min_y),
fmt(self.width),
fmt(self.height)
)
}
}
fn architecture_root_bbox_from_svg(
out: &str,
content_bounds: Option<Bounds>,
icon_size_px: f64,
trust_content_bounds: bool,
) -> Bounds {
let content_bounds_fallback = content_bounds.as_ref().cloned().unwrap_or(Bounds {
min_x: 0.0,
min_y: 0.0,
max_x: icon_size_px,
max_y: icon_size_px,
});
if trust_content_bounds && content_bounds.is_some() {
return content_bounds_fallback;
}
let mut bounds = svg_emitted_bounds_from_svg(out).unwrap_or(content_bounds_fallback);
if let Some(content_bounds) = content_bounds {
bounds.min_x = bounds.min_x.min(content_bounds.min_x);
bounds.min_y = bounds.min_y.min(content_bounds.min_y);
bounds.max_x = bounds.max_x.max(content_bounds.max_x);
bounds.max_y = bounds.max_y.max(content_bounds.max_y);
}
bounds
}
fn architecture_root_viewport_from_bbox(
bounds: &Bounds,
padding_px: f64,
profile: ArchitectureRootViewportProfile,
) -> ArchitectureRootViewport {
let mut viewport = ArchitectureRootViewport {
min_x: bounds.min_x - padding_px,
min_y: bounds.min_y - padding_px,
width: ((bounds.max_x - bounds.min_x) + 2.0 * padding_px).max(1.0),
height: ((bounds.max_y - bounds.min_y) + 2.0 * padding_px).max(1.0),
};
viewport.min_x = (viewport.min_x as f32) as f64;
viewport.min_y = (viewport.min_y as f32) as f64;
viewport.width = (viewport.width as f32) as f64;
if !profile.skips_height_snap() {
viewport.height = (viewport.height as f32) as f64;
}
viewport
}
pub(super) fn finalize_architecture_root_viewport<M: ArchitectureModelAccess>(
ctx: ArchitectureRootViewportContext<'_, M>,
) -> String {
let ArchitectureRootViewportContext {
mut out,
diagram_id,
model,
root_open,
content_bounds,
padding_px,
icon_size_px,
use_max_width,
apply_root_overrides,
trust_content_bounds,
} = ctx;
let b =
architecture_root_bbox_from_svg(&out, content_bounds, icon_size_px, trust_content_bounds);
let profile = ArchitectureRootViewportProfile::from_model(model);
let viewport = architecture_root_viewport_from_bbox(&b, padding_px, profile);
let mut view_box_attr = viewport.view_box_attr();
let mut max_w_attr = fmt_string(viewport.width);
if apply_root_overrides
&& let Some((override_viewbox, override_max_w)) =
crate::generated::architecture_root_overrides_11_12_2::lookup_architecture_root_viewport_override(diagram_id)
&& std::env::var_os("MERMAN_DISABLE_ROOT_VIEWPORT_OVERRIDES").is_none()
{
view_box_attr = override_viewbox.to_string();
max_w_attr = override_max_w.to_string();
}
let mut replacements: Vec<(usize, std::ops::Range<usize>, &str)> =
Vec::with_capacity(if use_max_width { 2 } else { 1 });
replacements.push((
root_open.viewbox_placeholder_range.start,
root_open.viewbox_placeholder_range,
view_box_attr.as_str(),
));
if use_max_width && let Some(range) = root_open.max_width_placeholder_range {
replacements.push((range.start, range, max_w_attr.as_str()));
}
replacements.sort_by_key(|(start, _, _)| std::cmp::Reverse(*start));
for (_, range, replacement) in replacements {
out.replace_range(range, replacement);
}
out
}
#[cfg(test)]
mod tests {
use super::*;
fn bounds(min_x: f64, min_y: f64, max_x: f64, max_y: f64) -> Bounds {
Bounds {
min_x,
min_y,
max_x,
max_y,
}
}
fn profile(
groups_len: usize,
edges_len: usize,
service_count: usize,
junction_count: usize,
has_inverse_arrow_mesh_edge: bool,
) -> ArchitectureRootViewportProfile {
ArchitectureRootViewportProfile {
groups_len,
edges_len,
service_count,
junction_count,
has_inverse_arrow_mesh_edge,
}
}
#[test]
fn architecture_root_viewport_snaps_xy_width_and_height_to_f32_lattice() {
let b = bounds(1.123456789, 2.123456789, 111.987654321, 222.987654321);
let padding = 40.0;
let viewport =
architecture_root_viewport_from_bbox(&b, padding, profile(1, 2, 3, 4, false));
let raw_min_x = b.min_x - padding;
let raw_min_y = b.min_y - padding;
let raw_width = (b.max_x - b.min_x) + 2.0 * padding;
let raw_height = (b.max_y - b.min_y) + 2.0 * padding;
assert_eq!(viewport.min_x, (raw_min_x as f32) as f64);
assert_eq!(viewport.min_y, (raw_min_y as f32) as f64);
assert_eq!(viewport.width, (raw_width as f32) as f64);
assert_eq!(viewport.height, (raw_height as f32) as f64);
}
#[test]
fn architecture_root_viewport_preserves_non_inverse_arrow_mesh_height_precision() {
let b = bounds(0.25, 1.5, 100.75, 201.123456789);
let padding = 40.0;
let raw_height = (b.max_y - b.min_y) + 2.0 * padding;
assert_ne!(raw_height, (raw_height as f32) as f64);
let viewport =
architecture_root_viewport_from_bbox(&b, padding, profile(0, 8, 5, 0, false));
assert_eq!(viewport.height, raw_height);
assert_eq!(
viewport.width,
(((b.max_x - b.min_x) + 2.0 * padding) as f32) as f64
);
}
#[test]
fn architecture_root_viewport_snaps_inverse_arrow_mesh_height() {
let b = bounds(0.25, 1.5, 100.75, 201.123456789);
let padding = 40.0;
let raw_height = (b.max_y - b.min_y) + 2.0 * padding;
let viewport = architecture_root_viewport_from_bbox(&b, padding, profile(0, 8, 5, 0, true));
assert_eq!(viewport.height, (raw_height as f32) as f64);
}
#[test]
fn architecture_root_bbox_uses_content_bounds_when_svg_bbox_is_unavailable() {
let content = bounds(-10.0, -20.0, 30.0, 40.0);
let b = architecture_root_bbox_from_svg("<not-svg", Some(content.clone()), 80.0, false);
assert_eq!(b.min_x, content.min_x);
assert_eq!(b.min_y, content.min_y);
assert_eq!(b.max_x, content.max_x);
assert_eq!(b.max_y, content.max_y);
}
#[test]
fn architecture_root_bbox_can_trust_accumulated_content_bounds() {
let content = bounds(1.0, 2.0, 3.0, 4.0);
let svg = r#"<svg><rect x="-100" y="-200" width="300" height="400"/></svg>"#;
let b = architecture_root_bbox_from_svg(svg, Some(content.clone()), 80.0, true);
assert_eq!(b.min_x, content.min_x);
assert_eq!(b.min_y, content.min_y);
assert_eq!(b.max_x, content.max_x);
assert_eq!(b.max_y, content.max_y);
}
#[test]
fn architecture_root_bbox_scans_svg_when_content_bounds_are_not_trusted() {
let content = bounds(1.0, 2.0, 3.0, 4.0);
let svg = r#"<svg><rect x="-100" y="-200" width="300" height="400"/></svg>"#;
let b = architecture_root_bbox_from_svg(svg, Some(content), 80.0, false);
assert_eq!(b.min_x, -100.0);
assert_eq!(b.min_y, -200.0);
assert_eq!(b.max_x, 200.0);
assert_eq!(b.max_y, 200.0);
}
}