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
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
|
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
/// >>> #set page(width: 140pt)
/// Here, we can simplify:
/// $ (a dot b dot cancel(x)) /
/// cancel(x) $
/// ```
#[elem(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>,
/// Whether the cancel line should be inverted (flipped along the y-axis).
/// For the default angle setting, inverted means the cancel line
/// points 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,
/// Whether 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 much to rotate the cancel line.
///
/// - If `{auto}`, the line assumes the default angle; that is, along the
/// diagonal line of the content box.
/// - If given an angle, the line is rotated by that angle clockwise w.r.t
/// the y-axis.
/// - It given a function `angle => angle`, the line is rotated by the angle
/// returned by that function. The function receives the default angle as
/// its input.
///
/// ```example
/// >>> #set page(width: 140pt)
/// $ cancel(Pi)
/// cancel(Pi, angle: #0deg)
/// cancel(Pi, angle: #45deg)
/// cancel(Pi, angle: #90deg)
/// cancel(1/(1+x), angle: #(a => a + 45deg))
/// cancel(1/(1+x), angle: #(a => a + 90deg)) $
/// ```
pub angle: Smart<CancelAngle>,
/// How to [stroke]($stroke) the cancel line.
///
/// ```example
/// >>> #set page(width: 140pt)
/// $ cancel(
/// sum x,
/// stroke: #(
/// paint: red,
/// thickness: 1.5pt,
/// dash: "dashed",
/// ),
/// ) $
/// ```
#[resolve]
#[fold]
#[default(Stroke {
// Default stroke has 0.5pt for better visuals.
thickness: Smart::Custom(Abs::pt(0.5)),
..Default::default()
})]
pub stroke: Stroke,
}
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(FixedStroke {
paint: TextElem::fill_in(styles),
..Default::default()
});
let invert = self.inverted(styles);
let cross = self.cross(styles);
let angle = self.angle(styles);
let invert_first_line = !cross && invert;
let first_line = draw_cancel_line(
ctx,
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(ctx, length, stroke, true, &angle, body_size, span)?;
body.push_frame(center, second_line);
}
ctx.push(FrameFragment::new(ctx, body).with_class(body_class));
Ok(())
}
}
/// Defines the cancel line.
pub enum CancelAngle {
Angle(Angle),
Func(Func),
}
cast! {
CancelAngle,
self => match self {
Self::Angle(v) => v.into_value(),
Self::Func(v) => v.into_value()
},
v: Angle => CancelAngle::Angle(v),
v: Func => CancelAngle::Func(v),
}
/// Draws a cancel line.
fn draw_cancel_line(
ctx: &mut MathContext,
length_scale: Rel<Abs>,
stroke: FixedStroke,
invert: bool,
angle: &Smart<CancelAngle>,
body_size: Size,
span: Span,
) -> SourceResult<Frame> {
let default = default_angle(body_size);
let mut angle = match angle {
// Non specified angle defaults to the diagonal
Smart::Auto => default,
Smart::Custom(angle) => match angle {
// This specifies the absolute angle w.r.t y-axis clockwise.
CancelAngle::Angle(v) => *v,
// This specifies a function that takes the default angle as input.
CancelAngle::Func(func) => {
func.call_vt(ctx.vt, [default])?.cast().at(span)?
}
},
};
// invert means flipping along the y-axis
if invert {
angle *= -1.0;
}
// same as above, the default length is the diagonal of the body box.
let default_length = body_size.to_point().hypot();
let length = length_scale.relative_to(default_length);
// Draw a vertical line of length and rotate it by angle
let start = Point::new(Abs::zero(), length / 2.0);
let delta = Point::new(Abs::zero(), -length);
let mut frame = Frame::soft(body_size);
frame.push(start, FrameItem::Shape(Geometry::Line(delta).stroked(stroke), span));
// Having the middle of the line at the origin is convenient here.
frame.transform(Transform::rotate(angle));
Ok(frame)
}
/// The default line angle for a body of the given size.
fn default_angle(body: Size) -> Angle {
// The default cancel line is the diagonal.
// We infer the default angle from
// the diagonal w.r.t to the body box.
//
// The returned angle is in the range of [0, Pi/2]
//
// Note that the angle is computed w.r.t to the y-axis
//
// B
// /|
// diagonal / | height
// / |
// / |
// O ----
// width
let (width, height) = (body.x, body.y);
let default_angle = (width / height).atan(); // arctangent (in the range [0, Pi/2])
Angle::rad(default_angle)
}
|