use super::super::ir::{Bbox, PlacedNode};
use super::super::{Ctx, child_path, layout_inst, prim};
use super::annotate::Paint;
use super::compose::{fmt, section_title};
use super::dims;
use super::geometry::P;
use crate::error::Error;
use crate::ledger::consts::{
NOTE_OFFSET, PLANE_ARROW_SHAFT, PLANE_LETTER_GAP, PLANE_LETTER_SIZE, PLANE_OVERHANG,
PLANE_THICK_END, PLANE_THICK_WIDTH,
};
use crate::resolve::NodeKind;
use crate::resolve::{Program, ResolvedInst, ResolvedLink, ResolvedValue};
mod plane;
#[cfg(test)]
mod tests;
pub(in crate::layout) use plane::fill_planes;
pub(in crate::layout) fn place_detail_labels(kids: &mut [PlacedNode]) {
for k in kids.iter_mut() {
if !k.type_chain.iter().any(|t| t == "magnifier") {
continue;
}
let off = (k.bbox.w() / 2.0 + NOTE_OFFSET) * std::f64::consts::FRAC_1_SQRT_2;
for c in k.children.iter_mut().filter(|c| c.kind == NodeKind::Text) {
c.cx = off;
c.cy = -off;
}
}
}
pub(super) fn fill_footnote(foot: &mut PlacedNode, title: &str) {
let fs = foot.attrs.number("font-size").unwrap_or(12.0);
let text = prim::text_plain(title, 0.0, 0.0, fs, crate::font::Font::of(&foot.attrs).kind);
foot.bbox = text.bbox;
foot.children = vec![text];
}
pub(in crate::layout) enum OfView<'a> {
Section { letter: String },
Detail {
marker: &'a ResolvedInst,
host: &'a ResolvedInst,
letter: String,
},
}
pub(in crate::layout) fn resolve_of<'a>(
inst: &ResolvedInst,
program: &'a Program,
) -> Result<Option<OfView<'a>>, Error> {
let Some(ResolvedValue::Ident(id)) = inst.attrs.get("of") else {
return Ok(None);
};
let (marker, host) = find_marker(&program.scene.nodes, id, None)
.ok_or_else(|| Error::at(inst.span, format!("'of' finds no marker '{id}'")))?;
let letter = marker_letter(marker);
if marker.type_chain.iter().any(|t| t == "magnifier") {
if host.attrs.get("of").is_some() {
return Err(Error::at(
inst.span,
"a detail magnifies a base view — 'of' can't name a marker inside another sourced view",
));
}
Ok(Some(OfView::Detail {
marker,
host,
letter,
}))
} else if marker.type_chain.iter().any(|t| t == "plane") {
Ok(Some(OfView::Section { letter }))
} else {
Err(Error::at(
inst.span,
format!("'of' names '{id}', not a '|plane|' or '|magnifier|'"),
))
}
}
pub(in crate::layout) fn layout_detail(
inst: &ResolvedInst,
path: &str,
program: &Program,
own: f64,
marker: &ResolvedInst,
host: &ResolvedInst,
letter: &str,
) -> Result<Vec<PlacedNode>, Error> {
let center =
super::super::anchors::translate(&marker.attrs, marker.span)?.unwrap_or((0.0, 0.0));
let diameter = marker
.attrs
.number("width")
.ok_or_else(|| Error::at(marker.span, "'|magnifier|' requires 'width' — its diameter"))?;
let r = diameter / 2.0 * own;
let ctx = Ctx {
scale: own,
drawing: true,
};
let mut clones = Vec::new();
for c in host.children.iter().filter(|c| is_relaid_geometry(c)) {
clones.push(layout_inst(c, &child_path(path, c), program, ctx)?);
}
super::place_features(&mut clones, own, None)?;
for c in &mut clones {
c.cx -= center.0 * own;
c.cy -= center.1 * own;
}
let circle = Bbox::centered(2.0 * r, 2.0 * r);
let links: Vec<&ResolvedLink> = if inst.id.is_some() {
program.links.iter().filter(|w| w.scope == path).collect()
} else {
Vec::new()
};
let mut annotations = super::annotate::lower(&clones, &links, path, own, Some(circle))?;
let mut clip_group = prim::group(clones, Vec::new(), circle);
clip_group.attrs.insert("clip", ResolvedValue::Number(r));
let mut own_kids = Vec::new();
for c in &inst.children {
own_kids.push(layout_inst(c, &child_path(path, c), program, ctx)?);
}
fill_of_title(
&mut own_kids,
"detail",
letter,
inst.attrs.number("scale").unwrap_or(1.0),
);
let mut kids = vec![clip_group, boundary_circle(inst, r)];
kids.append(&mut annotations);
kids.append(&mut own_kids);
Ok(kids)
}
pub(in crate::layout) fn fill_of_title(
kids: &mut [PlacedNode],
kind: &str,
letter: &str,
ratio: f64,
) {
let title = section_title(kind, letter, ratio);
for k in kids
.iter_mut()
.filter(|k| k.attrs.get("of-title").is_some())
{
fill_footnote(k, &title);
}
}
fn boundary_circle(inst: &ResolvedInst, r: f64) -> PlacedNode {
let light = ResolvedValue::LiveVar {
name: "stroke-light".into(),
raw: false,
};
let stroke = match inst.attrs.get("stroke") {
Some(ResolvedValue::Ident(s)) if s == "none" => light,
Some(v) => v.clone(),
None => light,
};
let width = inst
.attrs
.number("stroke-width")
.filter(|w| *w > 0.0)
.unwrap_or(1.0);
let mut c = prim::oval(
0.0,
0.0,
2.0 * r,
2.0 * r,
ResolvedValue::Ident("none".into()),
);
c.attrs.insert("stroke", stroke);
c.attrs.insert("stroke-width", ResolvedValue::Number(width));
c.attrs.insert("fill", ResolvedValue::Ident("none".into()));
c.type_chain.push("magnifier".to_string());
c
}
fn find_marker<'a>(
nodes: &'a [ResolvedInst],
id: &str,
parent: Option<&'a ResolvedInst>,
) -> Option<(&'a ResolvedInst, &'a ResolvedInst)> {
for n in nodes {
if n.id.as_deref() == Some(id)
&& n.type_chain
.iter()
.any(|t| t == "magnifier" || t == "plane")
{
return parent.map(|p| (n, p));
}
if let Some(hit) = find_marker(&n.children, id, Some(n)) {
return Some(hit);
}
}
None
}
fn is_relaid_geometry(inst: &ResolvedInst) -> bool {
!super::is_sheet(inst.kind, &inst.type_chain)
&& !inst
.type_chain
.iter()
.any(|t| t == "plane" || t == "magnifier")
}
fn marker_letter(marker: &ResolvedInst) -> String {
marker
.label
.clone()
.or_else(|| {
marker
.children
.iter()
.find(|c| c.kind == NodeKind::Text)
.and_then(|c| c.label.clone())
})
.unwrap_or_default()
}