mirror of
https://github.com/typst/typst
synced 2025-05-14 04:56:26 +08:00
Implement math cancel
function (#793)
This commit is contained in:
parent
4cea7007d0
commit
a4075f8b9b
169
library/src/math/cancel.rs
Normal file
169
library/src/math/cancel.rs
Normal file
@ -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
|
||||||
|
}
|
@ -5,6 +5,7 @@ mod ctx;
|
|||||||
mod accent;
|
mod accent;
|
||||||
mod align;
|
mod align;
|
||||||
mod attach;
|
mod attach;
|
||||||
|
mod cancel;
|
||||||
mod delimited;
|
mod delimited;
|
||||||
mod frac;
|
mod frac;
|
||||||
mod fragment;
|
mod fragment;
|
||||||
@ -20,6 +21,7 @@ mod underover;
|
|||||||
pub use self::accent::*;
|
pub use self::accent::*;
|
||||||
pub use self::align::*;
|
pub use self::align::*;
|
||||||
pub use self::attach::*;
|
pub use self::attach::*;
|
||||||
|
pub use self::cancel::*;
|
||||||
pub use self::delimited::*;
|
pub use self::delimited::*;
|
||||||
pub use self::frac::*;
|
pub use self::frac::*;
|
||||||
pub use self::matrix::*;
|
pub use self::matrix::*;
|
||||||
@ -71,6 +73,7 @@ pub fn module() -> Module {
|
|||||||
math.define("overbrace", OverbraceElem::func());
|
math.define("overbrace", OverbraceElem::func());
|
||||||
math.define("underbracket", UnderbracketElem::func());
|
math.define("underbracket", UnderbracketElem::func());
|
||||||
math.define("overbracket", OverbracketElem::func());
|
math.define("overbracket", OverbracketElem::func());
|
||||||
|
math.define("cancel", CancelElem::func());
|
||||||
|
|
||||||
// Fractions and matrix-likes.
|
// Fractions and matrix-likes.
|
||||||
math.define("frac", FracElem::func());
|
math.define("frac", FracElem::func());
|
||||||
|
@ -45,6 +45,11 @@ impl Point {
|
|||||||
Self { x: self.x.max(other.x), y: self.y.max(other.y) }
|
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.
|
/// Transform the point with the given transformation.
|
||||||
pub fn transform(self, ts: Transform) -> Self {
|
pub fn transform(self, ts: Transform) -> Self {
|
||||||
Self::new(
|
Self::new(
|
||||||
|
BIN
tests/ref/math/cancel.png
Normal file
BIN
tests/ref/math/cancel.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 24 KiB |
34
tests/typ/math/cancel.typ
Normal file
34
tests/typ/math/cancel.typ
Normal file
@ -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) $
|
Loading…
x
Reference in New Issue
Block a user