pub(crate) mod anchors;
mod angle;
mod annotate;
pub(crate) mod breaks;
pub(crate) mod chrome;
mod compose;
mod corner;
mod dims;
pub(crate) mod edges;
mod engine;
pub(crate) mod geometry;
mod leaders;
mod mates;
mod outline;
pub(crate) mod pen;
mod round;
mod threads;
pub(super) use engine::{layout_node, layout_root};
use super::ir::Bbox;
use crate::error::Error;
use crate::resolve::{AttrMap, Program, ResolvedInst, ResolvedValue};
use geometry::P;
pub struct SketchGeo {
pub segments: Vec<(String, Segment)>,
pub mirrors: Vec<geometry::MirrorAxis>,
pub revolved: bool,
pub threads: Vec<(String, f64)>,
pub outline: Vec<geometry::Subpath>,
pub view: breaks::ViewMap,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum Segment {
Point(P),
Edge(P, P),
Arc { mid: P, r: f64 },
Circle { center: P, r: f64 },
}
impl Segment {
pub(super) fn scaled(self, s: f64) -> Self {
let m = |p: P| (p.0 * s, p.1 * s);
match self {
Segment::Point(p) => Segment::Point(m(p)),
Segment::Edge(a, b) => Segment::Edge(m(a), m(b)),
Segment::Arc { mid, r } => Segment::Arc {
mid: m(mid),
r: r * s,
},
Segment::Circle { center, r } => Segment::Circle {
center: m(center),
r: r * s,
},
}
}
}
pub(crate) fn is_drawing(attrs: &AttrMap) -> bool {
matches!(attrs.get("layout"), Some(ResolvedValue::Ident(l)) if l == "drawing")
}
pub(crate) fn is_drawing_scope(program: &Program, scope: &str) -> bool {
super::scope_attrs(program, scope).is_some_and(is_drawing)
}
pub(super) fn rel_path<'a>(path: &'a str, scope: &str) -> &'a str {
path.strip_prefix(scope)
.map(|p| p.trim_start_matches('.'))
.unwrap_or(path)
}
pub(super) fn is_sheet(kind: crate::resolve::NodeKind, type_chain: &[String]) -> bool {
kind == crate::resolve::NodeKind::Text
|| type_chain.iter().any(|t| {
matches!(
t.as_str(),
"note" | "balloon" | "table" | "footnote" | "caption" | "page"
)
})
}
pub(super) fn part_bbox(inst: &ResolvedInst, own: f64) -> Result<Bbox, Error> {
if let Some(ty) = inst
.type_chain
.iter()
.find(|t| *t == "hole" || *t == "pitch-circle")
&& !chrome::is_chrome(&inst.attrs)
{
let Some(w) = inst.attrs.number("width") else {
return Err(Error::at(
inst.span,
format!("'|{ty}|' requires 'width' — its diameter"),
));
};
let sw = inst.attrs.number("stroke-width").unwrap_or(0.0);
return Ok(Bbox::centered(w * own, w * own).inflate(sw / 2.0));
}
super::primitives::leaf_bbox(inst, own)
}
pub(super) fn place_features(
children: &mut [super::PlacedNode],
scale: f64,
view: Option<&breaks::ViewMap>,
) -> Result<(), Error> {
for c in children.iter_mut() {
if chrome::is_chrome(&c.attrs) {
continue;
}
let (dx, dy) = super::anchors::translate(&c.attrs, c.span)?.unwrap_or((0.0, 0.0));
let m = (dx * scale, dy * scale);
let p = match view {
Some(v) => v.map(m),
None => m,
};
c.cx = p.0;
c.cy = p.1;
if let Some(v) = view {
ride_view(c, v, m, p);
}
}
Ok(())
}
fn ride_view(node: &mut super::PlacedNode, v: &breaks::ViewMap, base_model: P, base_disp: P) {
if node.rotation != 0.0
|| super::owns_layout(&node.attrs)
|| node.sketch.as_ref().is_some_and(|g| !g.view.is_identity())
{
return;
}
for c in node.children.iter_mut() {
if chrome::is_chrome(&c.attrs) {
continue;
}
let m = (base_model.0 + c.cx, base_model.1 + c.cy);
let d = v.map(m);
c.cx = d.0 - base_disp.0;
c.cy = d.1 - base_disp.1;
ride_view(c, v, m, d);
}
if node.attrs.get("pattern").is_some() {
let mut bbox = Bbox::empty();
for (i, c) in node.children.iter().enumerate() {
let b = c.bbox.shifted(c.cx, c.cy);
bbox = if i == 0 { b } else { bbox.union(b) };
}
node.bbox = bbox;
}
}
#[cfg(test)]
pub(super) mod testutil {
use super::super::{LaidOut, PlacedNode};
use crate::resolve::NodeKind;
pub fn laid(src: &str) -> LaidOut {
let toks = crate::lexer::lex(src).expect("lex");
let file = crate::syntax::parser::parse(src, &toks).expect("parse");
let lowered = crate::desugar::desugar(&file).expect("desugar");
let program = crate::resolve::resolve_with_theme(&lowered, &[]).expect("resolve");
crate::layout::layout(&program).expect("layout")
}
pub fn layout_err(src: &str) -> String {
let toks = crate::lexer::lex(src).expect("lex");
let file = crate::syntax::parser::parse(src, &toks).expect("parse");
let lowered = crate::desugar::desugar(&file).expect("desugar");
let program = crate::resolve::resolve_with_theme(&lowered, &[]).expect("resolve");
match crate::layout::layout(&program) {
Ok(_) => panic!("expected a layout error"),
Err(e) => e.message,
}
}
pub fn by_id<'a>(nodes: &'a [PlacedNode], id: &str) -> &'a PlacedNode {
fn walk<'a>(nodes: &'a [PlacedNode], id: &str) -> Option<&'a PlacedNode> {
for n in nodes {
if n.id.as_deref() == Some(id) {
return Some(n);
}
if let Some(hit) = walk(&n.children, id) {
return Some(hit);
}
}
None
}
walk(nodes, id).unwrap_or_else(|| panic!("node '{id}' placed"))
}
pub fn texts(nodes: &[PlacedNode]) -> Vec<(String, f64, f64, f64)> {
fn walk(nodes: &[PlacedNode], ox: f64, oy: f64, out: &mut Vec<(String, f64, f64, f64)>) {
for n in nodes {
if n.kind == NodeKind::Text
&& let Some(t) = &n.label
{
out.push((t.clone(), ox + n.cx, oy + n.cy, n.rotation));
}
walk(&n.children, ox + n.cx, oy + n.cy, out);
}
}
let mut out = Vec::new();
walk(nodes, 0.0, 0.0, &mut out);
out
}
pub fn text_at(nodes: &[PlacedNode], content: &str) -> (f64, f64, f64) {
let all = texts(nodes);
let hits: Vec<_> = all.iter().filter(|(t, ..)| t == content).collect();
match hits.as_slice() {
[one] => (one.1, one.2, one.3),
_ => panic!(
"expected one '{content}', found {}: {:?}",
hits.len(),
all.iter().map(|(t, ..)| t).collect::<Vec<_>>()
),
}
}
}