summaryrefslogtreecommitdiff
path: root/library
diff options
context:
space:
mode:
authorPg Biel <9021226+PgBiel@users.noreply.github.com>2023-04-21 06:28:46 -0300
committerGitHub <noreply@github.com>2023-04-21 11:28:46 +0200
commita4075f8b9b387f4e0aea9cc0ee11ca62c99a648d (patch)
tree1090c82c7d7522d5750a6e7d31a91b3f88a3b53b /library
parent4cea7007d0d4f152097d753be3020c736e96cca0 (diff)
Implement math `cancel` function (#793)
Diffstat (limited to 'library')
-rw-r--r--library/src/math/cancel.rs169
-rw-r--r--library/src/math/mod.rs3
2 files changed, 172 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());