figcraft/crates/render/src/export_depth.rs

70 lines
2.5 KiB
Rust

//! Preflight for vector exports, including subtrees rendered into embedded images.
use figcraft_doc::{Document, NodeId};
/// Maximum descendant depth for SVG/PDF export; each selected root is at depth zero.
pub const MAX_EXPORT_DEPTH: usize = 256;
/// Exporting this visible subtree would silently omit content at the traversal limit.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct ExportDepthError {
pub root: NodeId,
pub node: NodeId,
}
impl std::fmt::Display for ExportDepthError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"cannot export layer {}: layer {} exceeds the maximum export depth of {MAX_EXPORT_DEPTH}; select an inner layer or reduce nesting",
self.root, self.node
)
}
}
impl std::error::Error for ExportDepthError {}
/// Refuse a visible descendant deeper than [`MAX_EXPORT_DEPTH`] before computing bounds or
/// drawing. Hidden subtrees are not exported. Masks, boolean operands and rasterised effects
/// still depend on their descendants, so they participate in the same preflight.
///
/// The walk uses one iterator per ancestor (bounded auxiliary memory), rather than recursion or
/// collecting a whole subtree. A cycle also reaches the depth limit and is refused.
pub fn check_export_depth(doc: &Document, roots: &[NodeId]) -> Result<(), ExportDepthError> {
for root in roots {
let mut stack = vec![(std::slice::from_ref(root).iter(), 0)];
while let Some((siblings, depth)) = stack.last_mut() {
let Some(&id) = siblings.next() else {
stack.pop();
continue;
};
let depth = *depth;
let Some(node) = doc.node(id) else { continue };
if !node.visible {
continue;
}
if depth > MAX_EXPORT_DEPTH {
return Err(ExportDepthError { root: *root, node: id });
}
if !node.children.is_empty() {
stack.push((node.children.iter(), depth + 1));
}
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use figcraft_doc::NodeKind;
#[test]
fn export_depth_preflight_refuses_a_cyclic_subtree() {
let mut doc = Document::new("cycle");
let root = doc.create(doc.first_page().unwrap(), NodeKind::Group, "root", 0.0, 0.0, 1.0, 1.0).unwrap();
doc.get_mut(root).unwrap().children.push(root);
assert_eq!(check_export_depth(&doc, &[root]), Err(ExportDepthError { root, node: root }));
}
}