diff options
| author | Laurenz <laurmaedje@gmail.com> | 2024-10-27 19:04:55 +0100 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2024-10-27 18:04:55 +0000 |
| commit | be7cfc85d08c545abfac08098b7b33b4bd71f37e (patch) | |
| tree | f4137fa2aaa57babae1f7603a9b2ed7e688f43d8 /crates/typst-layout/src/transforms.rs | |
| parent | b8034a343831e8609aec2ec81eb7eeda57aa5d81 (diff) | |
Split out four new crates (#5302)
Diffstat (limited to 'crates/typst-layout/src/transforms.rs')
| -rw-r--r-- | crates/typst-layout/src/transforms.rs | 246 |
1 files changed, 246 insertions, 0 deletions
diff --git a/crates/typst-layout/src/transforms.rs b/crates/typst-layout/src/transforms.rs new file mode 100644 index 00000000..5ac9f777 --- /dev/null +++ b/crates/typst-layout/src/transforms.rs @@ -0,0 +1,246 @@ +use once_cell::unsync::Lazy; +use typst_library::diag::{bail, SourceResult}; +use typst_library::engine::Engine; +use typst_library::foundations::{Content, Packed, Resolve, Smart, StyleChain}; +use typst_library::introspection::Locator; +use typst_library::layout::{ + Abs, Axes, FixedAlignment, Frame, MoveElem, Point, Ratio, Region, Rel, RotateElem, + ScaleAmount, ScaleElem, Size, SkewElem, Transform, +}; +use typst_utils::Numeric; + +/// Layout the moved content. +#[typst_macros::time(span = elem.span())] +pub fn layout_move( + elem: &Packed<MoveElem>, + engine: &mut Engine, + locator: Locator, + styles: StyleChain, + region: Region, +) -> SourceResult<Frame> { + let mut frame = crate::layout_frame(engine, &elem.body, locator, styles, region)?; + let delta = Axes::new(elem.dx(styles), elem.dy(styles)).resolve(styles); + let delta = delta.zip_map(region.size, Rel::relative_to); + frame.translate(delta.to_point()); + Ok(frame) +} + +/// Layout the rotated content. +#[typst_macros::time(span = elem.span())] +pub fn layout_rotate( + elem: &Packed<RotateElem>, + engine: &mut Engine, + locator: Locator, + styles: StyleChain, + region: Region, +) -> SourceResult<Frame> { + let angle = elem.angle(styles); + let align = elem.origin(styles).resolve(styles); + + // Compute the new region's approximate size. + let is_finite = region.size.is_finite(); + let size = if is_finite { + compute_bounding_box(region.size, Transform::rotate(-angle)).1 + } else { + Size::splat(Abs::inf()) + }; + + measure_and_layout( + engine, + locator, + region, + size, + styles, + elem.body(), + Transform::rotate(angle), + align, + elem.reflow(styles), + ) +} + +/// Layout the scaled content. +#[typst_macros::time(span = elem.span())] +pub fn layout_scale( + elem: &Packed<ScaleElem>, + engine: &mut Engine, + locator: Locator, + styles: StyleChain, + region: Region, +) -> SourceResult<Frame> { + // Compute the new region's approximate size. + let scale = resolve_scale(elem, engine, locator.relayout(), region.size, styles)?; + let size = region + .size + .zip_map(scale, |r, s| if r.is_finite() { Ratio::new(1.0 / s).of(r) } else { r }) + .map(Abs::abs); + + measure_and_layout( + engine, + locator, + region, + size, + styles, + elem.body(), + Transform::scale(scale.x, scale.y), + elem.origin(styles).resolve(styles), + elem.reflow(styles), + ) +} + +/// Resolves scale parameters, preserving aspect ratio if one of the scales +/// is set to `auto`. +fn resolve_scale( + elem: &Packed<ScaleElem>, + engine: &mut Engine, + locator: Locator, + container: Size, + styles: StyleChain, +) -> SourceResult<Axes<Ratio>> { + fn resolve_axis( + axis: Smart<ScaleAmount>, + body: impl Fn() -> SourceResult<Abs>, + styles: StyleChain, + ) -> SourceResult<Smart<Ratio>> { + Ok(match axis { + Smart::Auto => Smart::Auto, + Smart::Custom(amt) => Smart::Custom(match amt { + ScaleAmount::Ratio(ratio) => ratio, + ScaleAmount::Length(length) => { + let length = length.resolve(styles); + Ratio::new(length / body()?) + } + }), + }) + } + + let size = Lazy::new(|| { + let pod = Region::new(container, Axes::splat(false)); + let frame = crate::layout_frame(engine, &elem.body, locator, styles, pod)?; + SourceResult::Ok(frame.size()) + }); + + let x = resolve_axis( + elem.x(styles), + || size.as_ref().map(|size| size.x).map_err(Clone::clone), + styles, + )?; + + let y = resolve_axis( + elem.y(styles), + || size.as_ref().map(|size| size.y).map_err(Clone::clone), + styles, + )?; + + match (x, y) { + (Smart::Auto, Smart::Auto) => { + bail!(elem.span(), "x and y cannot both be auto") + } + (Smart::Custom(x), Smart::Custom(y)) => Ok(Axes::new(x, y)), + (Smart::Auto, Smart::Custom(v)) | (Smart::Custom(v), Smart::Auto) => { + Ok(Axes::splat(v)) + } + } +} + +/// Layout the skewed content. +#[typst_macros::time(span = elem.span())] +pub fn layout_skew( + elem: &Packed<SkewElem>, + engine: &mut Engine, + locator: Locator, + styles: StyleChain, + region: Region, +) -> SourceResult<Frame> { + let ax = elem.ax(styles); + let ay = elem.ay(styles); + let align = elem.origin(styles).resolve(styles); + + // Compute the new region's approximate size. + let size = if region.size.is_finite() { + compute_bounding_box(region.size, Transform::skew(ax, ay)).1 + } else { + Size::splat(Abs::inf()) + }; + + measure_and_layout( + engine, + locator, + region, + size, + styles, + elem.body(), + Transform::skew(ax, ay), + align, + elem.reflow(styles), + ) +} + +/// Applies a transformation to a frame, reflowing the layout if necessary. +#[allow(clippy::too_many_arguments)] +fn measure_and_layout( + engine: &mut Engine, + locator: Locator, + region: Region, + size: Size, + styles: StyleChain, + body: &Content, + transform: Transform, + align: Axes<FixedAlignment>, + reflow: bool, +) -> SourceResult<Frame> { + if reflow { + // Measure the size of the body. + let pod = Region::new(size, Axes::splat(false)); + let frame = crate::layout_frame(engine, body, locator.relayout(), styles, pod)?; + + // Actually perform the layout. + let pod = Region::new(frame.size(), Axes::splat(true)); + let mut frame = crate::layout_frame(engine, body, locator, styles, pod)?; + let Axes { x, y } = align.zip_map(frame.size(), FixedAlignment::position); + + // Compute the transform. + let ts = Transform::translate(x, y) + .pre_concat(transform) + .pre_concat(Transform::translate(-x, -y)); + + // Compute the bounding box and offset and wrap in a new frame. + let (offset, size) = compute_bounding_box(frame.size(), ts); + frame.transform(ts); + frame.translate(offset); + frame.set_size(size); + Ok(frame) + } else { + // Layout the body. + let mut frame = crate::layout_frame(engine, body, locator, styles, region)?; + let Axes { x, y } = align.zip_map(frame.size(), FixedAlignment::position); + + // Compute the transform. + let ts = Transform::translate(x, y) + .pre_concat(transform) + .pre_concat(Transform::translate(-x, -y)); + + // Apply the transform. + frame.transform(ts); + Ok(frame) + } +} + +/// Computes the bounding box and offset of a transformed area. +fn compute_bounding_box(size: Size, ts: Transform) -> (Point, Size) { + let top_left = Point::zero().transform_inf(ts); + let top_right = Point::with_x(size.x).transform_inf(ts); + let bottom_left = Point::with_y(size.y).transform_inf(ts); + let bottom_right = size.to_point().transform_inf(ts); + + // We first compute the new bounding box of the rotated area. + let min_x = top_left.x.min(top_right.x).min(bottom_left.x).min(bottom_right.x); + let min_y = top_left.y.min(top_right.y).min(bottom_left.y).min(bottom_right.y); + let max_x = top_left.x.max(top_right.x).max(bottom_left.x).max(bottom_right.x); + let max_y = top_left.y.max(top_right.y).max(bottom_left.y).max(bottom_right.y); + + // Then we compute the new size of the area. + let width = max_x - min_x; + let height = max_y - min_y; + + (Point::new(-min_x, -min_y), Size::new(width.abs(), height.abs())) +} |
