summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--library/src/math/cancel.rs169
-rw-r--r--library/src/math/mod.rs3
-rw-r--r--src/geom/point.rs5
-rw-r--r--tests/ref/math/cancel.pngbin0 -> 24698 bytes
-rw-r--r--tests/typ/math/cancel.typ34
5 files changed, 211 insertions, 0 deletions
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<Length>,
+
+ /// 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<Abs>,
+ 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
--- /dev/null
+++ b/tests/ref/math/cancel.png
Binary files 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) $