diff --git a/library/src/math/cancel.rs b/library/src/math/cancel.rs new file mode 100644 index 00000000..0ea7b1d2 --- /dev/null +++ b/library/src/math/cancel.rs @@ -0,0 +1,169 @@ +use super::*; + +/// Displays a diagonal line over a part of an equation. +/// +/// ## Example +/// ```example +/// Here, we can simplify: +/// $ (a dot.c b dot.c cancel(x)) / cancel(x) $ +/// ``` +/// +/// Display: Cancel +/// Category: math +#[element(LayoutMath)] +pub struct CancelElem { + /// The content over which the line should be placed. + #[required] + pub body: Content, + + /// The length of the line, relative to the length of the diagonal spanning + /// the whole element being "cancelled". A value of `{100%}` would then have + /// the line span precisely the element's diagonal. + /// + /// Defaults to `{100% + 3pt}`. + /// + /// ```example + /// $ a + cancel(x, length: #200%) - b - cancel(x, length: #200%) $ + /// ``` + #[default(Rel::new(Ratio::one(), Abs::pt(3.0).into()))] + pub length: Rel, + + /// If the cancel line should be inverted (heading northwest instead of + /// northeast). + /// + /// Defaults to `{false}`. + /// + /// ```example + /// $ (a cancel((b + c), inverted: #true)) / cancel(b + c, inverted: #true) $ + /// ``` + #[default(false)] + pub inverted: bool, + + /// If two opposing cancel lines should be drawn, forming a cross over the + /// element. Overrides `inverted`. + /// + /// Defaults to `{false}`. + /// + /// ```example + /// $ cancel(x, cross: #true) $ + /// ``` + #[default(false)] + pub cross: bool, + + /// Rotate the cancel line by a certain angle. See the + /// [line's documentation]($func/line.angle) for more details. + /// + /// ```example + /// $ cancel(x, rotation: #30deg) $ + /// ``` + #[default(Angle::zero())] + pub rotation: Angle, + + /// How to stroke the cancel line. See the + /// [line's documentation]($func/line.stroke) for more details. + /// + /// ```example + /// $ cancel(x, stroke: #{red + 1.5pt}) $ + /// ``` + #[resolve] + #[fold] + pub stroke: PartialStroke, +} + +impl LayoutMath for CancelElem { + fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> { + let mut body = ctx.layout_frame(&self.body())?; + + let styles = ctx.styles(); + let body_size = body.size(); + let span = self.span(); + let length = self.length(styles).resolve(styles); + + // Default stroke has 0.5pt for better visuals. + let stroke = self.stroke(styles).unwrap_or(Stroke { + paint: TextElem::fill_in(styles), + thickness: Abs::pt(0.5), + ..Default::default() + }); + + let invert = self.inverted(styles); + let cross = self.cross(styles); + let angle = self.rotation(styles); + + let invert_first_line = !cross && invert; + let first_line = draw_cancel_line( + length, + stroke.clone(), + invert_first_line, + angle, + body_size, + span, + ); + + // The origin of our line is the very middle of the element. + let center = body_size.to_point() / 2.0; + body.push_frame(center, first_line); + + if cross { + // Draw the second line. + let second_line = + draw_cancel_line(length, stroke, true, angle, body_size, span); + + body.push_frame(center, second_line); + } + + ctx.push(FrameFragment::new(ctx, body)); + + Ok(()) + } +} + +/// Draws a cancel line. +fn draw_cancel_line( + length: Rel, + stroke: Stroke, + invert: bool, + angle: Angle, + body_size: Size, + span: Span, +) -> Frame { + // B + // /| + // diagonal / | height + // / | + // / | + // O ---- + // width + let diagonal = body_size.to_point().hypot(); + let length = length.relative_to(diagonal); + let (width, height) = (body_size.x, body_size.y); + let mid = body_size / 2.0; + + // Scale the amount needed such that the cancel line has the given 'length' + // (reference length, or 100%, is the whole diagonal). + // Scales from the center. + let scale = length.to_raw() / diagonal.to_raw(); + + // invert horizontally if 'invert' was given + let scale_x = scale * if invert { -1.0 } else { 1.0 }; + let scale_y = scale; + let scales = Axes::new(scale_x, scale_y); + + // Draw a line from bottom left to top right of the given element, where the + // origin represents the very middle of that element, that is, a line from + // (-width / 2, height / 2) with length components (width, -height) (sign is + // inverted in the y-axis). After applying the scale, the line will have the + // correct length and orientation (inverted if needed). + let start = Axes::new(-mid.x, mid.y).zip(scales).map(|(l, s)| l * s); + let delta = Axes::new(width, -height).zip(scales).map(|(l, s)| l * s); + + let mut frame = Frame::new(body_size); + frame.push( + start.to_point(), + FrameItem::Shape(Geometry::Line(delta.to_point()).stroked(stroke), span), + ); + + // Having the middle of the line at the origin is convenient here. + frame.transform(Transform::rotate(angle)); + frame +} diff --git a/library/src/math/mod.rs b/library/src/math/mod.rs index 9c8a9cdf..48fe52d2 100644 --- a/library/src/math/mod.rs +++ b/library/src/math/mod.rs @@ -5,6 +5,7 @@ mod ctx; mod accent; mod align; mod attach; +mod cancel; mod delimited; mod frac; mod fragment; @@ -20,6 +21,7 @@ mod underover; pub use self::accent::*; pub use self::align::*; pub use self::attach::*; +pub use self::cancel::*; pub use self::delimited::*; pub use self::frac::*; pub use self::matrix::*; @@ -71,6 +73,7 @@ pub fn module() -> Module { math.define("overbrace", OverbraceElem::func()); math.define("underbracket", UnderbracketElem::func()); math.define("overbracket", OverbracketElem::func()); + math.define("cancel", CancelElem::func()); // Fractions and matrix-likes. math.define("frac", FracElem::func()); diff --git a/src/geom/point.rs b/src/geom/point.rs index b31ea296..e7811e1e 100644 --- a/src/geom/point.rs +++ b/src/geom/point.rs @@ -45,6 +45,11 @@ impl Point { Self { x: self.x.max(other.x), y: self.y.max(other.y) } } + /// The distance between this point and the origin. + pub fn hypot(self) -> Abs { + Abs::raw(self.x.to_raw().hypot(self.y.to_raw())) + } + /// Transform the point with the given transformation. pub fn transform(self, ts: Transform) -> Self { Self::new( diff --git a/tests/ref/math/cancel.png b/tests/ref/math/cancel.png new file mode 100644 index 00000000..96b29c45 Binary files /dev/null and b/tests/ref/math/cancel.png differ diff --git a/tests/typ/math/cancel.typ b/tests/typ/math/cancel.typ new file mode 100644 index 00000000..315cc7d4 --- /dev/null +++ b/tests/typ/math/cancel.typ @@ -0,0 +1,34 @@ +// Tests the cancel() function. + +--- +// Inline +$a + 5 + cancel(x) + b - cancel(x)$ + +$c + (a dot.c cancel(b dot.c c))/(cancel(b dot.c c))$ + +--- +// Display +#set page(width: auto) +$ a + b + cancel(b + c) - cancel(b) - cancel(c) - 5 + cancel(6) - cancel(6) $ +$ e + (a dot.c cancel((b + c + d)))/(cancel(b + c + d)) $ + +--- +// Inverted +$a + cancel(x, inverted: #true) - cancel(x, inverted: #true) + 10 + cancel(y) - cancel(y)$ +$ x + cancel("abcdefg", inverted: #true) $ + +--- +// Cross +$a + cancel(b + c + d, cross: #true, stroke: #red) + e$ +$ a + cancel(b + c + d, cross: #true) + e $ + +--- +// Resized and styled +#set page(width: 200pt, height: auto) +$a + cancel(x, length: #200%) - cancel(x, length: #50%, stroke: #{red + 1.1pt})$ +$ b + cancel(x, length: #150%) - cancel(a + b + c, length: #50%, stroke: #{blue + 1.2pt}) $ + +--- +// Rotated +$x + cancel(y, rotation: #90deg) - cancel(z, rotation: #135deg)$ +$ e + cancel((j + e)/(f + e)) - cancel((j + e)/(f + e), rotation: #30deg) $