use super::super::ir::{Bbox, PlacedNode};
use super::geometry::{MirrorAxis, P, dist};
use super::{Segment, chrome};
use crate::ast::Side;
use crate::error::Error;
use crate::resolve::{ResolvedEndpoint, ResolvedValue};
pub(super) enum Spot {
Origin,
Side(Side),
Corner(P),
Center,
Segment(Segment),
}
pub(super) struct Anchor<'a> {
pub child: usize,
pub node: &'a PlacedNode,
pub origin: P,
pub model_origin: P,
pub rot: f64,
pub spot: Spot,
}
pub(super) fn resolve<'a>(
kids: &'a [PlacedNode],
scope: &str,
ep: &ResolvedEndpoint,
noun: &str,
) -> Result<Anchor<'a>, Error> {
let rel = super::rel_path(&ep.path, scope);
let mut segs = rel.split('.');
let first = segs.next().expect("an endpoint path is non-empty");
let first_hops = descend(kids, first)
.ok_or_else(|| Error::at(ep.span, format!("{noun} endpoint '{rel}' not placed")))?;
let child = kids
.iter()
.position(|k| std::ptr::eq(k, first_hops[0]))
.expect("descend starts among kids");
let mut hops = first_hops.into_iter();
let mut node = hops.next().expect("a descent is non-empty");
let mut origin = (node.cx, node.cy);
let mut model_origin = origin;
let mut rot = node.rotation;
let mut view: Option<(&super::breaks::ViewMap, P, P)> = None;
let mut step = |node: &mut &'a PlacedNode,
next: &'a PlacedNode,
view: &mut Option<(&'a super::breaks::ViewMap, P, P)>| {
if let Some(geo) = node.sketch.as_ref()
&& !geo.view.is_identity()
{
*view = Some((&geo.view, (0.0, 0.0), (0.0, 0.0)));
}
let d_local = (next.cx, next.cy);
let m_local = match view {
Some((v, bm, bd)) => {
let da = (bd.0 + d_local.0, bd.1 + d_local.1);
let ma = v.unmap(da);
let local = (ma.0 - bm.0, ma.1 - bm.1);
*bm = ma;
*bd = da;
local
}
None => d_local,
};
let local = rotated(d_local, rot);
origin = (origin.0 + local.0, origin.1 + local.1);
let m = rotated(m_local, rot);
model_origin = (model_origin.0 + m.0, model_origin.1 + m.1);
if next.rotation != 0.0 {
*view = None;
}
rot += next.rotation;
*node = next;
};
for next in hops.by_ref() {
step(&mut node, next, &mut view);
}
for seg in segs {
let more = (node.attrs.get("pattern").is_none())
.then(|| descend(&node.children, seg))
.flatten()
.ok_or_else(|| {
Error::at(
ep.span,
format!(
"'{rel}' sits inside a 'pattern:' — per-copy features are deferred (SPEC 23)"
),
)
})?;
for next in more {
step(&mut node, next, &mut view);
}
}
let last = rel.rsplit('.').next().expect("non-empty");
let spot = spot(node, ep, last)?;
Ok(Anchor {
child,
node,
origin,
model_origin,
rot,
spot,
})
}
fn descend<'a>(kids: &'a [PlacedNode], seg: &str) -> Option<Vec<&'a PlacedNode>> {
if let Some(k) = kids.iter().find(|k| k.id.as_deref() == Some(seg)) {
return Some(vec![k]);
}
for anon in kids.iter().filter(|k| k.id.is_none()) {
if let Some(mut chain) = descend(&anon.children, seg) {
chain.insert(0, anon);
return Some(chain);
}
}
None
}
fn spot(node: &PlacedNode, ep: &ResolvedEndpoint, node_name: &str) -> Result<Spot, Error> {
if let Some(side) = ep.side {
return Ok(Spot::Side(side));
}
let Some(point) = &ep.point else {
return Ok(Spot::Origin);
};
let d = std::f64::consts::FRAC_1_SQRT_2;
match point.as_str() {
"center" => return Ok(Spot::Center),
"top-left" => return Ok(Spot::Corner((-d, -d))),
"top-right" => return Ok(Spot::Corner((d, -d))),
"bottom-left" => return Ok(Spot::Corner((-d, d))),
"bottom-right" => return Ok(Spot::Corner((d, d))),
_ => {}
}
let segments = node
.sketch
.as_ref()
.map(|s| s.segments.as_slice())
.unwrap_or(&[]);
let Some((_, segment)) = segments.iter().find(|(n, _)| n == point) else {
let mut msg = format!("no segment ':{point}' on '{node_name}'");
let mut near: Vec<&str> = segments.iter().map(|(n, _)| n.as_str()).collect();
near.sort_by_key(|n| usize::abs_diff(n.len(), point.len()));
let near: Vec<String> = near.iter().take(2).map(|n| format!("':{n}'")).collect();
if !near.is_empty() {
msg.push_str(&format!("; did you mean {}?", near.join(", ")));
}
return Err(Error::at(ep.span, msg));
};
let view = &node.sketch.as_ref().expect("segments imply a sketch").view;
Ok(Spot::Segment(view.segment(*segment)))
}
impl Anchor<'_> {
pub fn feature(&self) -> &PlacedNode {
if self.node.attrs.get("pattern").is_some()
&& let Some(copy) = self
.node
.children
.iter()
.find(|c| !chrome::is_chrome(&c.attrs))
{
return copy;
}
self.node
}
pub fn geometry_box(&self) -> Bbox {
let f = self.feature();
let half = f.attrs.number("stroke-width").unwrap_or(0.0) / 2.0;
f.bbox.inflate(-half)
}
pub fn local_point(&self) -> P {
let g = self.geometry_box();
let (cx, cy) = ((g.min_x + g.max_x) / 2.0, (g.min_y + g.max_y) / 2.0);
match &self.spot {
Spot::Origin => (0.0, 0.0),
Spot::Center => (cx, cy),
Spot::Side(side) => match side {
Side::Top => (cx, g.min_y),
Side::Bottom => (cx, g.max_y),
Side::Left => (g.min_x, cy),
Side::Right => (g.max_x, cy),
},
Spot::Corner((dx, dy)) => (
if *dx < 0.0 { g.min_x } else { g.max_x },
if *dy < 0.0 { g.min_y } else { g.max_y },
),
Spot::Segment(p) => match *p {
Segment::Point(p) => p,
Segment::Arc { mid, .. } => mid,
Segment::Circle { center, .. } => center,
Segment::Edge(a, b) => ((a.0 + b.0) / 2.0, (a.1 + b.1) / 2.0),
},
}
}
pub fn point(&self) -> P {
self.to_world(self.local_point())
}
pub fn model_point(&self) -> P {
self.model_world(self.local_point())
}
pub fn model_world(&self, local_disp: P) -> P {
let r = rotated(self.unmap_local(local_disp), self.rot);
(self.model_origin.0 + r.0, self.model_origin.1 + r.1)
}
fn view(&self) -> Option<&super::breaks::ViewMap> {
let v = &self.feature().sketch.as_ref()?.view;
(!v.is_identity()).then_some(v)
}
pub fn map_local(&self, p: P) -> P {
self.view().map_or(p, |v| v.map(p))
}
pub fn unmap_local(&self, p: P) -> P {
self.view().map_or(p, |v| v.unmap(p))
}
pub fn outward(&self) -> Option<P> {
let local = match &self.spot {
Spot::Side(side) => match side {
Side::Top => (0.0, -1.0),
Side::Bottom => (0.0, 1.0),
Side::Left => (-1.0, 0.0),
Side::Right => (1.0, 0.0),
},
Spot::Segment(Segment::Edge(a, b)) => {
let len = dist(*a, *b).max(1e-9);
let t = ((b.0 - a.0) / len, (b.1 - a.1) / len);
(t.1, -t.0)
}
_ => return None,
};
Some(rotated(local, self.rot))
}
pub fn direction(&self) -> Option<P> {
if let Spot::Segment(Segment::Edge(a, b)) = &self.spot {
let len = dist(*a, *b).max(1e-9);
return Some(rotated(((b.0 - a.0) / len, (b.1 - a.1) / len), self.rot));
}
if let Spot::Side(side) = &self.spot {
let along = match side {
Side::Top | Side::Bottom => (1.0, 0.0),
Side::Left | Side::Right => (0.0, 1.0),
};
return Some(rotated(along, self.rot));
}
if matches!(self.spot, Spot::Origin | Spot::Center)
&& self.feature().kind == crate::resolve::NodeKind::Line
&& let Ok(Some(pts)) = super::super::primitives::attr_points(
&self.feature().attrs,
"points",
self.node.span,
)
&& pts.len() >= 2
{
let (a, b) = (pts[0], pts[pts.len() - 1]);
let len = dist(a, b).max(1e-9);
return Some(rotated(((b.0 - a.0) / len, (b.1 - a.1) / len), self.rot));
}
None
}
pub fn round_diameter(&self) -> Option<f64> {
if let Spot::Segment(p) = &self.spot {
return match p {
Segment::Circle { r, .. } => Some(2.0 * r),
_ => None,
};
}
let f = self.feature();
if f.kind != crate::resolve::NodeKind::Oval {
return None;
}
let g = self.geometry_box();
((g.w() - g.h()).abs() < 1e-6).then(|| g.w())
}
pub fn mirrors(&self) -> &[MirrorAxis] {
self.node
.sketch
.as_ref()
.map(|s| s.mirrors.as_slice())
.unwrap_or(&[])
}
pub fn revolved(&self) -> bool {
self.node.sketch.as_ref().is_some_and(|s| s.revolved)
}
pub fn pattern_count(&self) -> Option<usize> {
let ResolvedValue::Call(call) = self.node.attrs.get("pattern")? else {
return None;
};
let num = |i: usize| call.args.get(i).and_then(ResolvedValue::as_number);
match call.name.as_str() {
"grid" => Some((num(0)? as usize) * (num(1)? as usize)),
"radial" => Some(num(0)? as usize),
_ => None,
}
}
pub fn to_world(&self, p: P) -> P {
let r = rotated(p, self.rot);
(self.origin.0 + r.0, self.origin.1 + r.1)
}
pub fn to_local(&self, p: P) -> P {
rotated((p.0 - self.origin.0, p.1 - self.origin.1), -self.rot)
}
}
pub(super) fn spell(ep: &ResolvedEndpoint, scope: &str) -> String {
let mut s = super::rel_path(&ep.path, scope).to_string();
if let Some(side) = ep.side {
s.push(':');
s.push_str(match side {
Side::Top => "top",
Side::Bottom => "bottom",
Side::Left => "left",
Side::Right => "right",
});
} else if let Some(p) = &ep.point {
s.push(':');
s.push_str(p);
}
s
}
pub(super) fn rotated(p: P, deg: f64) -> P {
if deg == 0.0 {
return p;
}
let (s, c) = deg.to_radians().sin_cos();
(p.0 * c - p.1 * s, p.0 * s + p.1 * c)
}