use crate::geometry::primitives::Point;
use crate::geometry::shapes::Polygon;
use crate::geometry::traits::{DiagramShape, Polygonize};
use crate::plotting::clip::{polygon_clip_many, ClipOperation};
use crate::plotting::regions::{
classify_into_pieces, decompose_regions_with, poi_with_holes, RegionPolygons,
};
use crate::spec::{Combination, DiagramSpec};
use std::collections::HashMap;
#[derive(Debug, Clone, Copy)]
pub struct PlotOptions {
pub n_vertices: usize,
pub label_precision: f64,
pub sliver_threshold: f64,
}
impl Default for PlotOptions {
fn default() -> Self {
Self {
n_vertices: 200,
label_precision: 0.01,
sliver_threshold: 1e-3,
}
}
}
#[derive(Debug, Clone)]
pub struct PlotData {
pub regions: RegionPolygons,
pub region_anchors: HashMap<String, Point>,
pub region_areas: HashMap<String, f64>,
pub set_anchors: HashMap<String, Point>,
pub shape_outlines: HashMap<String, Polygon>,
}
impl PlotData {
pub fn pieces_for(&self, combination: &str) -> Option<&Vec<crate::plotting::RegionPiece>> {
let combo: Combination = combination.parse().unwrap();
self.regions.get(&combo)
}
pub fn from_shapes<S>(
shapes: &[S],
spec: &DiagramSpec,
container: Option<&crate::geometry::shapes::Rectangle>,
options: PlotOptions,
) -> PlotData
where
S: DiagramShape + Polygonize,
{
build_plot_data(shapes, spec, container, options)
}
}
pub(crate) fn build_plot_data<S>(
shapes: &[S],
spec: &DiagramSpec,
container: Option<&crate::geometry::shapes::Rectangle>,
options: PlotOptions,
) -> PlotData
where
S: DiagramShape + Polygonize,
{
let regions = decompose_regions_with(
shapes,
spec.set_names(),
spec,
container,
options.n_vertices,
options.sliver_threshold,
);
let region_anchors_combo = regions.label_points(options.label_precision);
let region_anchors: HashMap<String, Point> = region_anchors_combo
.iter()
.map(|(combo, p)| (combo.to_string(), *p))
.collect();
let region_areas: HashMap<String, f64> = regions
.areas()
.into_iter()
.map(|(combo, a)| (combo.to_string(), a))
.collect();
let outline_vec: Vec<Polygon> = shapes
.iter()
.map(|s| s.polygonize(options.n_vertices))
.collect();
let set_anchors = compute_set_anchors(
&outline_vec,
spec.set_names(),
®ions,
®ion_anchors_combo,
options.label_precision,
);
let shape_outlines: HashMap<String, Polygon> = spec
.set_names()
.iter()
.zip(outline_vec.iter())
.map(|(name, poly)| (name.clone(), poly.clone()))
.collect();
PlotData {
regions,
region_anchors,
region_areas,
set_anchors,
shape_outlines,
}
}
fn compute_set_anchors(
shape_outlines: &[Polygon],
set_names: &[String],
regions: &RegionPolygons,
region_anchors: &HashMap<Combination, Point>,
precision: f64,
) -> HashMap<String, Point> {
let mut result = HashMap::new();
if shape_outlines.is_empty() {
return result;
}
for (i, name) in set_names.iter().enumerate() {
if i >= shape_outlines.len() {
continue;
}
let mut others_union: Vec<Polygon> = Vec::new();
for (j, other) in shape_outlines.iter().enumerate() {
if j == i {
continue;
}
if others_union.is_empty() {
others_union.push(other.clone());
} else {
others_union = polygon_clip_many(&others_union, other, ClipOperation::Union);
}
}
let exclusive_rings: Vec<Polygon> = if others_union.is_empty() {
vec![shape_outlines[i].clone()]
} else {
let mut acc = vec![shape_outlines[i].clone()];
for clip in &others_union {
acc = polygon_clip_many(&acc, clip, ClipOperation::Difference);
if acc.is_empty() {
break;
}
}
acc
};
let pieces = classify_into_pieces(exclusive_rings);
let shape_area = shape_outlines[i].area();
let max_piece_area = pieces.iter().map(|p| p.area()).fold(0.0_f64, f64::max);
let kept: Vec<_> = if shape_area > 0.0 && max_piece_area < shape_area * 1e-3 {
Vec::new()
} else {
pieces.into_iter().filter(|p| p.area() > 0.0).collect()
};
let anchor = poi_with_holes(&kept, precision).map(|(p, _)| p);
let anchor =
anchor.or_else(|| largest_containing_region_anchor(name, regions, region_anchors));
let anchor = anchor.or_else(|| {
let (p, _) = shape_outlines[i].pole_of_inaccessibility_with_distance(precision);
Some(p)
});
if let Some(point) = anchor {
result.insert(name.clone(), point);
}
}
result
}
fn largest_containing_region_anchor(
name: &str,
regions: &RegionPolygons,
region_anchors: &HashMap<Combination, Point>,
) -> Option<Point> {
let mut best: Option<(&Combination, f64)> = None;
for (combo, polys) in regions.iter() {
if !combo.sets().iter().any(|s| s == name) {
continue;
}
let area: f64 = polys.iter().map(|p| p.area()).sum();
if best.map(|(_, a)| area > a).unwrap_or(true) {
best = Some((combo, area));
}
}
best.and_then(|(combo, _)| region_anchors.get(combo).copied())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::fitter::Fitter;
use crate::geometry::shapes::Circle;
use crate::spec::{DiagramSpecBuilder, InputType};
#[test]
fn test_plot_options_default() {
let opts = PlotOptions::default();
assert_eq!(opts.n_vertices, 200);
assert!((opts.label_precision - 0.01).abs() < 1e-12);
}
#[test]
fn test_clip_difference_with_nested_circle_returns_two_rings() {
use crate::geometry::primitives::Point;
use crate::geometry::shapes::Circle;
use crate::geometry::traits::Polygonize;
use crate::plotting::clip::{polygon_clip, ClipOperation};
let a = Circle::new(Point::new(0.0, 0.0), 5.0).polygonize(64);
let b = Circle::new(Point::new(0.0, 0.0), 1.0).polygonize(32);
let result = polygon_clip(&a, &b, ClipOperation::Difference);
let mut pos = 0;
let mut neg = 0;
for r in &result {
let mut s = 0.0;
let v = r.vertices();
for i in 0..v.len() {
let j = (i + 1) % v.len();
s += v[i].x() * v[j].y() - v[j].x() * v[i].y();
}
if s > 0.0 {
pos += 1;
} else if s < 0.0 {
neg += 1;
}
}
eprintln!(
"polygon_clip(A=r5, B=r1, Difference) returned {} rings: {} CCW (outer), {} CW (hole)",
result.len(),
pos,
neg
);
for (i, r) in result.iter().enumerate() {
let v = r.vertices();
let mut s = 0.0;
for j in 0..v.len() {
let k = (j + 1) % v.len();
s += v[j].x() * v[k].y() - v[k].x() * v[j].y();
}
eprintln!(
" ring {}: {} verts, signed_area={:.3}, first vertex=({:.3}, {:.3})",
i,
v.len(),
s / 2.0,
v[0].x(),
v[0].y()
);
}
assert!(pos >= 1, "expected at least one outer (CCW) ring");
assert!(neg >= 1, "expected at least one hole (CW) ring");
}
#[test]
fn test_set_anchor_avoids_nested_inner_sets() {
let spec = DiagramSpecBuilder::new()
.set("A", 30.0)
.intersection(&["A", "B"], 3.0)
.intersection(&["A", "C"], 3.0)
.intersection(&["A", "D"], 3.0)
.intersection(&["A", "B", "C"], 0.6)
.intersection(&["A", "B", "D"], 0.6)
.intersection(&["A", "C", "D"], 0.6)
.intersection(&["A", "B", "C", "D"], 1.0)
.input_type(InputType::Exclusive)
.build()
.unwrap();
let layout = Fitter::<Circle>::new(&spec).seed(1).fit().unwrap();
let plot = layout.plot_data(&spec, PlotOptions::default());
let a_anchor = plot.set_anchors.get("A").expect("missing label for A");
let a_circle = layout.shape_for_set("A").unwrap();
for inner in ["B", "C", "D"] {
let c = layout.shape_for_set(inner).unwrap();
let dx = a_anchor.x() - c.center().x();
let dy = a_anchor.y() - c.center().y();
assert!(
dx * dx + dy * dy > c.radius() * c.radius(),
"label A at ({:.3}, {:.3}) overlaps inner set {} (center=({:.3}, {:.3}), r={:.3})",
a_anchor.x(),
a_anchor.y(),
inner,
c.center().x(),
c.center().y(),
c.radius(),
);
}
let dx = a_anchor.x() - a_circle.center().x();
let dy = a_anchor.y() - a_circle.center().y();
assert!(dx * dx + dy * dy <= a_circle.radius() * a_circle.radius());
}
#[test]
fn test_plot_data_two_circles() {
let spec = DiagramSpecBuilder::new()
.set("A", 5.0)
.set("B", 3.0)
.intersection(&["A", "B"], 1.0)
.input_type(InputType::Exclusive)
.build()
.unwrap();
let layout = Fitter::<Circle>::new(&spec).seed(42).fit().unwrap();
let plot = layout.plot_data(&spec, PlotOptions::default());
for combo in plot.regions.iter().map(|(c, _)| c) {
assert!(plot.region_anchors.contains_key(&combo.to_string()));
}
assert_eq!(plot.shape_outlines.len(), spec.set_names().len());
for name in spec.set_names() {
assert!(plot.set_anchors.contains_key(name));
}
}
}