mod anchors;
mod arrange;
pub(crate) mod chart;
pub(crate) mod drawing;
mod flex;
mod frame;
mod grid;
pub(crate) mod ir;
mod note;
mod page;
pub(crate) mod path_bbox; mod pattern;
mod prim; mod primitives; pub(crate) mod sequence;
mod text;
mod values;
mod wrap;
pub(crate) use anchors::is_pinned;
pub use ir::*;
pub(crate) use text::{approx_height, approx_width};
pub(crate) use values::as_pair;
use crate::error::Error;
use crate::resolve::{NodeKind, Program, ResolvedInst, ResolvedValue};
use crate::routing;
use crate::span::Span;
use flex::Axis;
use arrange::lay_out_container_children;
use frame::{accumulate_extent, finish};
pub fn layout(program: &Program) -> Result<LaidOut, Error> {
sequence::validate(program)?;
if crate::resolve::is_drawing(&program.scene.attrs) {
let (top_nodes, bbox) = drawing::layout_root(program)?;
let routed = routing::route(program, &top_nodes)?;
return finish(program, top_nodes, bbox, routed);
}
let ctx = Ctx {
scale: effective_scale(&program.scene.attrs, 1.0, Span::empty())?,
drawing: false,
};
let mut top_nodes = Vec::with_capacity(program.scene.nodes.len());
for inst in &program.scene.nodes {
top_nodes.push(layout_inst(inst, &child_path("", inst), program, ctx)?);
}
if sequence::is_sequence(&program.scene.attrs) {
let (bbox, links) = sequence::layout_root(&mut top_nodes, program)?;
let mut routed = routing::route(program, &top_nodes)?;
routed.links.extend(links);
return finish(program, top_nodes, bbox, routed);
}
let (bbox, _) = lay_out_container_children(
&mut top_nodes,
&program.scene.attrs,
Span::empty(),
ctx.scale,
)?;
let routed = routing::route(program, &top_nodes)?;
finish(program, top_nodes, bbox, routed)
}
#[derive(Clone, Copy)]
pub(crate) struct Ctx {
pub scale: f64,
pub drawing: bool,
}
impl Ctx {
pub(crate) fn sheet() -> Self {
Ctx {
scale: 1.0,
drawing: false,
}
}
}
#[derive(Clone, Copy, PartialEq)]
pub(crate) enum LineAlign {
Start,
Center,
End,
}
pub(crate) fn line_align_of(knob: Option<&str>) -> LineAlign {
match knob {
Some("start") => LineAlign::Start,
Some("end") => LineAlign::End,
_ => LineAlign::Center,
}
}
pub(crate) fn stamp_line_align(child: &mut PlacedNode, align: LineAlign) {
let word = match align {
LineAlign::Center => return,
LineAlign::Start => "start",
LineAlign::End => "end",
};
if child.kind == NodeKind::Text {
child.attrs.insert(
"line-align",
crate::resolve::ResolvedValue::Ident(word.into()),
);
}
}
pub(crate) fn effective_scale(
attrs: &crate::resolve::AttrMap,
inherited: f64,
span: Span,
) -> Result<f64, Error> {
let own = match attrs.get("px-per-unit") {
Some(v) => Some(v),
None => attrs.get("scale"),
};
match own {
None => Ok(inherited),
Some(v) => match v.as_number() {
Some(s) if s > 0.0 => Ok(s),
_ => Err(Error::at(span, "'scale' must be > 0")),
},
}
}
pub(crate) fn scope_attrs<'a>(
program: &'a Program,
scope: &str,
) -> Option<&'a crate::resolve::AttrMap> {
if scope.is_empty() {
Some(&program.scene.attrs)
} else {
node_at(program, scope).map(|i| &i.attrs)
}
}
pub(super) fn node_at<'a>(program: &'a Program, path: &str) -> Option<&'a ResolvedInst> {
let mut nodes = &program.scene.nodes;
let mut found = None;
for seg in path.split('.') {
let inst = crate::resolve::scene::find_in_scope(nodes, seg, &mut Vec::new())?;
found = Some(inst);
nodes = &inst.children;
}
found
}
fn child_path(parent: &str, inst: &ResolvedInst) -> String {
let Some(id) = inst.id.as_deref() else {
return parent.to_owned();
};
if parent.is_empty() {
id.to_owned()
} else {
format!("{parent}.{id}")
}
}
pub fn extent_hints(laid: &LaidOut, program: &Program) -> Vec<crate::error::Diagnostic> {
fn walk(nodes: &[PlacedNode], out: &mut Vec<crate::error::Diagnostic>) {
for n in nodes {
let is_drawing =
n.attrs.get("px-per-unit").is_some() && !n.type_chain.iter().any(|t| t == "page");
if is_drawing {
let (w, h) = (n.bbox.w(), n.bbox.h());
if w.max(h) > crate::ledger::consts::ABSURD_EXTENT_PX {
let (extent, axis) = if w >= h { (w, "wide") } else { (h, "tall") };
out.push(crate::error::Diagnostic::warn(
n.span,
format!(
"the drawing renders {} px {axis} — 'scale:' is a ratio; a 5 m beam at 1:50 is 'scale: 0.02'",
extent.round()
),
));
}
}
walk(&n.children, out);
}
}
let mut out = Vec::new();
if program.scene.attrs.get("px-per-unit").is_some() {
let (w, h) = (laid.viewbox.w, laid.viewbox.h);
if w.max(h) > crate::ledger::consts::ABSURD_EXTENT_PX {
let (extent, axis) = if w >= h { (w, "wide") } else { (h, "tall") };
out.push(crate::error::Diagnostic::warn(
Span::empty(),
format!(
"the drawing renders {} px {axis} — 'scale:' is a ratio; a 5 m beam at 1:50 is 'scale: 0.02'",
extent.round()
),
));
}
}
walk(&laid.nodes, &mut out);
out
}
pub fn validate_routing(laid: &LaidOut) -> Vec<routing::Violation> {
let mut out = laid.link_report.clone();
out.extend(routing::validate_routing(
&laid.nodes,
&laid.links,
&laid.link_report,
));
out
}
fn layout_inst(
inst: &ResolvedInst,
path: &str,
program: &Program,
ctx: Ctx,
) -> Result<PlacedNode, Error> {
let funcs = &program.funcs;
if inst.attrs.get("break").is_some() && inst.kind != NodeKind::Sketch {
return Err(Error::at(
inst.span,
"'break' cuts a '|sketch|' — draw the profile with the pen",
));
}
if let Some(v) = inst.attrs.get("thread") {
match inst.kind {
NodeKind::Sketch => {}
NodeKind::Oval => {
let pitch = match v {
ResolvedValue::List(items) => match items.as_slice() {
[one] => one.as_number(),
_ => None,
},
one => one.as_number(),
};
if !pitch.is_some_and(|p| p > 0.0) {
return Err(Error::at(
inst.span,
"'thread' takes a segment and its pitch — 'thread: m8 1.5'",
));
}
}
_ => {
return Err(Error::at(
inst.span,
"'thread' dresses a '|sketch|' segment or a round feature",
));
}
}
}
let engine = if chart::is_chart(&inst.attrs) {
Some(chart::layout_chart(inst, funcs)?)
} else if chart::is_pie(&inst.attrs) {
Some(chart::layout_pie(inst)?)
} else if sequence::is_sequence(&inst.attrs) {
Some(sequence::layout_node(inst, path, program)?)
} else if crate::resolve::is_drawing(&inst.attrs) {
Some(drawing::layout_node(inst, path, program, ctx)?)
} else {
None
};
if let Some(mut placed) = engine {
if placed.attrs.get("pattern").is_some() {
let own = effective_scale(&inst.attrs, ctx.scale, inst.span)?;
pattern::expand(&mut placed, own)?;
}
return Ok(placed);
}
if ctx.drawing && drawing::chrome::is_chrome(&inst.attrs) {
return Ok(drawing::chrome::placeholder(inst));
}
let own = effective_scale(&inst.attrs, ctx.scale, inst.span)?;
let part =
ctx.drawing && !owns_layout(&inst.attrs) && !drawing::is_sheet(inst.kind, &inst.type_chain);
let child_ctx = Ctx {
scale: own,
drawing: part,
};
let mut children: Vec<PlacedNode> = Vec::with_capacity(inst.children.len());
for c in &inst.children {
children.push(layout_inst(c, &child_path(path, c), program, child_ctx)?);
}
wrap::apply_max_width(inst, &mut children, own, inst.span)?;
let mut gutters: Vec<Gutter> = Vec::new();
let mut sketch_d: Option<String> = None;
let mut sketch_geo = None;
let bbox = if inst.kind == NodeKind::Sketch {
if !children.is_empty() && !part {
let _ = lay_out_container_children(&mut children, &inst.attrs, inst.span, own)?;
}
let folded = drawing::pen::fold(inst, own)?;
let half = inst.attrs.number("stroke-width").unwrap_or(0.0) / 2.0;
sketch_d = Some(folded.d);
drawing::breaks::fill_chrome(&mut children, &folded.cuts);
drawing::edges::fill(&mut children, "edges", &folded.edges);
drawing::edges::fill(&mut children, "thread", &folded.threads);
sketch_geo = Some(std::sync::Arc::new(drawing::SketchGeo {
segments: folded.segments,
mirrors: folded.mirror_axes,
revolved: folded.revolved,
threads: folded.thread_specs,
outline: folded.subs,
view: folded.view,
}));
folded.geometry.inflate(half)
} else if part {
drawing::part_bbox(inst, own)?
} else if children.is_empty() {
primitives::leaf_bbox(inst, own)?
} else {
let page_attrs;
let arrange_attrs = if page::is_page(&inst.type_chain) {
page_attrs = page::padded_attrs(&inst.attrs, own, inst.span)?;
&page_attrs
} else {
&inst.attrs
};
let (content_bbox, rects) =
lay_out_container_children(&mut children, arrange_attrs, inst.span, own)?;
gutters = rects;
let b = if inst.kind == NodeKind::Icon {
primitives::icon_square_bbox(inst, content_bbox, own)?
} else {
primitives::closed_bbox(inst, content_bbox, own)?
};
let text_only = children.iter().all(|c| c.kind == NodeKind::Text);
const CYL_LABEL_DROP: f64 = 0.03;
let label_drop = match inst.kind {
NodeKind::Cyl => CYL_LABEL_DROP,
_ => 0.0,
};
if label_drop > 0.0 && text_only {
let dy = b.h() * label_drop;
for c in &mut children {
c.cy += dy;
}
}
b
};
if part {
let half = inst.attrs.number("stroke-width").unwrap_or(0.0) / 2.0;
drawing::place_features(&mut children, own, sketch_geo.as_ref().map(|g| &g.view))?;
drawing::chrome::fill(&mut children, bbox.inflate(-half), own);
}
if page::is_page(&inst.type_chain) {
page::finish(&mut children, bbox, own);
}
let rotation = inst.attrs.number("rotate").unwrap_or(0.0);
let mut placed = PlacedNode {
id: inst.id.clone(),
kind: inst.kind,
type_chain: inst.type_chain.clone(),
applied_styles: inst.applied_styles.clone(),
label: inst.label.clone(),
attrs: inst.attrs.clone(),
own_style: inst.own_style.clone(),
markers: inst.markers.clone(),
cx: 0.0,
cy: 0.0,
bbox,
rotation,
children,
gutters,
links: Vec::new(),
sketch: sketch_geo,
origin: (0.0, 0.0),
span: inst.span,
};
if let Some(d) = sketch_d {
placed.attrs.insert("path", ResolvedValue::String(d));
}
if own != 1.0 {
values::scale_points_attr(&mut placed.attrs, own);
}
if placed.kind == NodeKind::Block && placed.type_chain.iter().any(|t| t == "note") {
note::fold(&mut placed);
}
if placed.attrs.get("pattern").is_some() {
pattern::expand(&mut placed, own)?;
}
Ok(placed)
}
fn owns_layout(attrs: &crate::resolve::AttrMap) -> bool {
attrs.get("layout").is_some() || attrs.get("direction").is_some()
}
#[cfg(test)]
mod tests;