Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ pub struct DocumentMetadata {
pub group_layers: HashSet<LayerNodeIdentifier>,
/// Each group's targets composed with its descendants' into its space.
composed_targets: HashMap<LayerNodeIdentifier, ComposedTargets>,
/// The whole document's bounds, one slot per way of combining them (see [`Self::document_bounds`]), each computed on first query after a change.
document_bounds: [OnceLock<Option<[DVec2; 2]>>; 4],
/// Per-layer text frame from item 0's `editor:text_frame` attribute (`DAffine2` mapping the (0, 0)-(1, 1) unit square onto the frame).
/// The Text tool composes this with `transform_to_viewport(layer)` to position its drag cage.
pub text_frames: HashMap<LayerNodeIdentifier, DAffine2>,
Expand Down Expand Up @@ -259,9 +261,16 @@ impl DocumentMetadata {
.flat_map(move |viewport_to_group| group.children(self).map(move |child| (child, viewport_to_group * self.transform_to_viewport(child))))
}

/// Forgets every group's composed targets, after the layers' targets, transforms, or structure change.
/// Forgets every group's composed targets and the document's bounds, after the layers' targets, transforms, or structure change.
pub fn reset_composed_targets(&mut self) {
self.composed_targets = self.group_layers.iter().map(|&group| (group, ComposedTargets::default())).collect();
self.document_bounds = Default::default();
}

/// The whole document's bounds with or without artboards and strokes, from `compute` on the first query after the layers change.
pub fn document_bounds(&self, include_artboards: bool, with_stroke: bool, compute: impl FnOnce() -> Option<[DVec2; 2]>) -> Option<[DVec2; 2]> {
let slot = usize::from(include_artboards) + 2 * usize::from(with_stroke);
*self.document_bounds[slot].get_or_init(compute)
}

/// Whether `hit` accepts a click target of the layer or of any descendant, each passed with its own layer's transform to document space.
Expand All @@ -283,22 +292,32 @@ impl DocumentMetadata {

/// Get the bounding box of the click target of the specified layer in the specified transform space
pub fn bounding_box_with_transform(&self, layer: LayerNodeIdentifier, transform: DAffine2) -> Option<[DVec2; 2]> {
let own_bounds = self
.own_visual_targets(layer)
.into_iter()
.flatten()
.filter_map(|click_target| click_target.bounding_box_with_transform(transform))
.reduce(Quad::combine_bounds);
let mut bounds = None;
self.extend_bounds_with_transform(layer, transform, &mut bounds);
bounds
}

/// Grows `bounds` to hold the layer's visual targets and, for a group, its descendants', each bounded in its own space.
fn extend_bounds_with_transform(&self, layer: LayerNodeIdentifier, transform: DAffine2, bounds: &mut Option<[DVec2; 2]>) {
for target in self.own_visual_targets(layer).into_iter().flatten() {
// A target whose loose bounds lie within the bounds so far cannot extend them, so its tight bounds are never computed
if let (Some([min, max]), Some(loose)) = (*bounds, target.bounding_box()) {
let [loose_min, loose_max] = (transform * Quad::from_box(loose)).bounding_box();
if loose_min.cmpge(min).all() && loose_max.cmple(max).all() {
continue;
}
}

if !self.group_layers.contains(&layer) {
return own_bounds;
if let Some(tight) = target.bounding_box_with_transform(transform) {
*bounds = Some(bounds.map_or(tight, |bounds| Quad::combine_bounds(bounds, tight)));
}
}

// Each descendant is bounded in its own space, without composing copies of its targets
self.children_in_group_space(layer)
.filter_map(|(child, child_to_layer)| self.bounding_box_with_transform(child, transform * child_to_layer))
.chain(own_bounds)
.reduce(Quad::combine_bounds)
if self.group_layers.contains(&layer) {
for (child, child_to_layer) in self.children_in_group_space(layer) {
self.extend_bounds_with_transform(child, transform * child_to_layer, bounds);
}
}
}

/// Calculate the corners of the bounding box but with a nonzero size.
Expand Down Expand Up @@ -754,6 +773,29 @@ mod tests {
assert!(generations.iter().enumerate().all(|(index, generation)| !generations[..index].contains(generation)));
}

// The document's bounds are computed once per way of combining them, and again once the layers change
#[test]
fn document_bounds_stay_cached_until_the_layers_change() {
let mut metadata = DocumentMetadata::default();
let computed = std::cell::Cell::new(0);
let compute = || {
computed.set(computed.get() + 1);
Some([DVec2::ZERO, DVec2::ONE])
};

metadata.document_bounds(true, false, compute);
metadata.document_bounds(true, false, compute);
assert_eq!(computed.get(), 1);

metadata.document_bounds(false, false, compute);
metadata.document_bounds(true, true, compute);
assert_eq!(computed.get(), 3);

metadata.mark_changed();
metadata.document_bounds(true, false, compute);
assert_eq!(computed.get(), 4);
}

#[test]
fn test_tree() {
let mut metadata = DocumentMetadata::default();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -824,34 +824,60 @@ impl NodeNetworkInterface {

/// Calculates the document bounds in document space
pub fn document_bounds_document_space(&self, include_artboards: bool) -> Option<[DVec2; 2]> {
self.combined_document_bounds(include_artboards, |metadata, layer| metadata.bounding_box_document(layer))
self.document_metadata.document_bounds(include_artboards, false, || {
self.combined_document_bounds(include_artboards, |metadata, layer| metadata.bounding_box_document(layer))
})
}

/// The union of every layer's bounds, with a layer inside a clipping artboard contributing only its part within the artboard.
/// A layer's bounds hold its descendants', so only the outermost layers below the root and each artboard are bounded, in O(layers).
fn combined_document_bounds(&self, include_artboards: bool, layer_bounds: impl Fn(&DocumentMetadata, LayerNodeIdentifier) -> Option<[DVec2; 2]>) -> Option<[DVec2; 2]> {
self.document_metadata
.all_layers()
.filter(|layer| include_artboards || !self.is_artboard(&layer.to_node(), &[]))
.filter_map(|layer| {
// A layer clipped by its artboard contributes only the intersection of the two bounds
if !self.is_artboard(&layer.to_node(), &[])
&& let Some(artboard_node_identifier) = layer
.ancestors(self.document_metadata())
.find(|ancestor| *ancestor != LayerNodeIdentifier::ROOT_PARENT && self.is_artboard(&ancestor.to_node(), &[]))
&& let Some(artboard) = self.document_node(&artboard_node_identifier.to_node(), &[])
&& let Some(clip_input) = artboard.inputs.get(ARTBOARD_CLIP_INPUT_INDEX)
&& let NodeInput::Value { tagged_value, .. } = clip_input
&& tagged_value.clone().deref() == &TaggedValue::Bool(true)
{
return Some(Quad::clip(
layer_bounds(&self.document_metadata, layer).unwrap_or_default(),
self.document_metadata.bounding_box_document(artboard_node_identifier).unwrap_or_default(),
));
let metadata = &self.document_metadata;
let is_finite = |[min, max]: &[DVec2; 2]| min.is_finite() && max.is_finite();

let mut combined: Option<[DVec2; 2]> = None;
let mut add = |bounds: [DVec2; 2]| combined = Some(combined.map_or(bounds, |combined| Quad::combine_bounds(combined, bounds)));

// Each layer with the bounds of the nearest artboard above it, if that one clips its content
let mut pending: Vec<(LayerNodeIdentifier, Option<[DVec2; 2]>)> = LayerNodeIdentifier::ROOT_PARENT.children(metadata).map(|layer| (layer, None)).collect();
while let Some((layer, clip)) = pending.pop() {
if self.is_artboard(&layer.to_node(), &[]) {
let clips_content = self.artboard_clips_content(layer);

if include_artboards && let Some(bounds) = layer_bounds(metadata, layer).filter(is_finite) {
add(bounds);

// Content clipped to the artboard lies within the bounds just added
if clips_content {
continue;
}
}
layer_bounds(&self.document_metadata, layer)
})
// Skip any layer bounds containing NaN to avoid poisoning the combined result
.filter(|[min, max]| min.is_finite() && max.is_finite())
.reduce(Quad::combine_bounds)

let child_clip = clips_content.then(|| metadata.bounding_box_document(layer).unwrap_or_default());
pending.extend(layer.children(metadata).map(|child| (child, child_clip)));
continue;
}

// Bounds containing NaN would poison the combined result, so the layer's descendants are bounded instead
match layer_bounds(metadata, layer).filter(is_finite) {
Some(bounds) => {
let clipped = clip.map_or(bounds, |clip| Quad::clip(bounds, clip));
if is_finite(&clipped) {
add(clipped);
}
}
None => pending.extend(layer.children(metadata).map(|child| (child, clip))),
}
}

combined
}

/// Whether the artboard layer clips the content inside it to its bounds.
fn artboard_clips_content(&self, artboard: LayerNodeIdentifier) -> bool {
self.document_node(&artboard.to_node(), &[])
.and_then(|artboard| artboard.inputs.get(ARTBOARD_CLIP_INPUT_INDEX))
.is_some_and(|clip_input| matches!(clip_input, NodeInput::Value { tagged_value, .. } if tagged_value.clone().deref() == &TaggedValue::Bool(true)))
}

pub fn document_bounds_viewport_space(&self, include_artboards: bool) -> Option<[DVec2; 2]> {
Expand All @@ -864,7 +890,9 @@ impl NodeNetworkInterface {
/// Calculates the document bounds in document space, expanding vector layer bounds to include the rendered
/// stroke width. Used for export so the output canvas captures strokes that overflow the path geometry.
pub fn document_bounds_document_space_with_stroke(&self, include_artboards: bool) -> Option<[DVec2; 2]> {
self.combined_document_bounds(include_artboards, |metadata, layer| metadata.bounding_box_document_with_stroke(layer))
self.document_metadata.document_bounds(include_artboards, true, || {
self.combined_document_bounds(include_artboards, |metadata, layer| metadata.bounding_box_document_with_stroke(layer))
})
}

/// Calculates the selected layer bounds in document space
Expand Down
Loading