1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
|
use super::*;
/// Displays a diagonal line over a part of an equation.
///
/// This is commonly used to show the elimination of a term.
///
/// ## Example { #example }
/// ```example
/// >>> #set page(width: 140pt)
/// Here, we can simplify:
/// $ (a dot b dot 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.
///
/// ```example
/// >>> #set page(width: 140pt)
/// $ a + cancel(x, length: #200%)
/// - 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 (pointing to the top left instead
/// of top right).
///
/// ```example
/// >>> #set page(width: 140pt)
/// $ (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`.
///
/// ```example
/// >>> #set page(width: 140pt)
/// $ cancel(Pi, cross: #true) $
/// ```
#[default(false)]
pub cross: bool,
/// How to rotate the cancel line. See the [line's
/// documentation]($func/line.angle) for more details.
///
/// ```example
/// >>> #set page(width: 140pt)
/// $ cancel(Pi, 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
/// >>> #set page(width: 140pt)
/// $ cancel(
/// sum x,
/// stroke: #(
/// paint: red,
/// thickness: 1.5pt,
/// dash: "dashed",
/// ),
/// ) $
/// ```
#[resolve]
#[fold]
#[default(PartialStroke {
// Default stroke has 0.5pt for better visuals.
thickness: Smart::Custom(Abs::pt(0.5)),
..Default::default()
})]
pub stroke: PartialStroke,
}
impl LayoutMath for CancelElem {
fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> {
let body = ctx.layout_fragment(&self.body())?;
// Use the same math class as the body, in order to preserve automatic spacing around it.
let body_class = body.class().unwrap_or(MathClass::Special);
let mut body = body.into_frame();
let styles = ctx.styles();
let body_size = body.size();
let span = self.span();
let length = self.length(styles).resolve(styles);
let stroke = self.stroke(styles).unwrap_or(Stroke {
paint: TextElem::fill_in(styles),
..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).with_class(body_class));
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
}
|