From c00fc14905380da2f3eb6ab9bbb366c096c6a6a2 Mon Sep 17 00:00:00 2001 From: lolstork <137357423+lolstork@users.noreply.github.com> Date: Fri, 25 Aug 2023 02:16:03 -0700 Subject: [PATCH] Initial support for augmented matrices (#1679) --- crates/typst-library/src/math/matrix.rs | 276 +++++++++++++++++++++++- tests/ref/math/matrix.png | Bin 25858 -> 40018 bytes tests/typ/math/matrix.typ | 28 +++ 3 files changed, 293 insertions(+), 11 deletions(-) diff --git a/crates/typst-library/src/math/matrix.rs b/crates/typst-library/src/math/matrix.rs index 3fa53ba09..c913592d5 100644 --- a/crates/typst-library/src/math/matrix.rs +++ b/crates/typst-library/src/math/matrix.rs @@ -1,9 +1,13 @@ +use typst::model::Resolve; + use super::*; const ROW_GAP: Em = Em::new(0.5); const COL_GAP: Em = Em::new(0.5); const VERTICAL_PADDING: Ratio = Ratio::new(0.1); +const DEFAULT_STROKE_THICKNESS: Em = Em::new(0.05); + /// A column vector. /// /// Content in the vector's elements can be aligned with the `&` symbol. @@ -80,6 +84,40 @@ pub struct MatElem { #[default(Some(Delimiter::Paren))] pub delim: Option, + /// Draws augmentation lines in a matrix. + /// + /// - `{none}`: No lines are drawn. + /// - A single number: A vertical augmentation line is drawn + /// after the specified column number. + /// - A dictionary: With a dictionary, multiple augmentation lines can be + /// drawn both horizontally and vertically. Additionally, the style of the + /// lines can be set. The dictionary can contain the following keys: + /// - `hline`: The offsets at which horizontal lines should be drawn. + /// For example, an offset of `2` would result in a horizontal line + /// being drawn after the second row of the matrix. Accepts either an + /// integer for a single line, or an array of integers + /// for multiple lines. + /// - `vline`: The offsets at which vertical lines should be drawn. + /// For example, an offset of `2` would result in a vertical line being + /// drawn after the second column of the matrix. Accepts either an + /// integer for a single line, or an array of integers + /// for multiple lines. + /// - `stroke`: How to stroke the line. See the + /// [line's documentation]($func/line.stroke) + /// for more details. If set to `{auto}`, takes on a thickness of + /// 0.05em and square line caps. + /// + /// ```example + /// $ mat(1, 0, 1; 0, 1, 2; augment: #2) $ + /// ``` + /// + /// ```example + /// $ mat(0, 0, 0; 1, 1, 1; augment: #(hline: 1, stroke: 2pt + green)) $ + /// ``` + #[resolve] + #[fold] + pub augment: Option, + /// An array of arrays with the rows of the matrix. /// /// ```example @@ -118,8 +156,40 @@ pub struct MatElem { impl LayoutMath for MatElem { #[tracing::instrument(skip(ctx))] fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> { + // validate inputs + + let augment = self.augment(ctx.styles()); + + if let Some(aug) = &augment { + for &offset in &aug.hline.0 { + if offset == 0 || offset >= self.rows().len() { + bail!( + self.span(), + "cannot draw a horizontal line after row {} of a matrix with {} rows", + offset, + self.rows().len() + ); + } + } + + let ncols = self.rows().first().map_or(0, |row| row.len()); + + for &offset in &aug.vline.0 { + if offset == 0 || offset >= ncols { + bail!( + self.span(), + "cannot draw a vertical line after column {} of a matrix with {} columns", + offset, + ncols + ); + } + } + } + let delim = self.delim(ctx.styles()); - let frame = layout_mat_body(ctx, &self.rows())?; + + let frame = layout_mat_body(ctx, &self.rows(), augment, self.span())?; + layout_delimiters( ctx, frame, @@ -232,55 +302,151 @@ fn layout_vec_body( } /// Layout the inner contents of a matrix. -fn layout_mat_body(ctx: &mut MathContext, rows: &[Vec]) -> SourceResult { +fn layout_mat_body( + ctx: &mut MathContext, + rows: &[Vec], + augment: Option>, + span: Span, +) -> SourceResult { let row_gap = ROW_GAP.scaled(ctx); let col_gap = COL_GAP.scaled(ctx); + let half_row_gap = row_gap * 0.5; + let half_col_gap = col_gap * 0.5; + + // We provide a default stroke thickness that scales + // with font size to ensure that augmentation lines + // look correct by default at all matrix sizes. + // The line cap is also set to square because it looks more "correct". + let default_stroke_thickness = DEFAULT_STROKE_THICKNESS.scaled(ctx); + let default_stroke = Stroke { + thickness: default_stroke_thickness, + line_cap: LineCap::Square, + ..Default::default() + }; + + let (hline, vline, stroke) = match augment { + Some(v) => { + // need to get stroke here for ownership + let stroke = v.stroke_or(default_stroke); + + (v.hline, v.vline, stroke) + } + _ => (Offsets::default(), Offsets::default(), default_stroke), + }; + let ncols = rows.first().map_or(0, |row| row.len()); let nrows = rows.len(); if ncols == 0 || nrows == 0 { return Ok(Frame::new(Size::zero())); } + // Before the full matrix body can be laid out, the + // individual cells must first be independently laid out + // so we can ensure alignment across rows and columns. + + // This variable stores the maximum ascent and descent for each row. let mut heights = vec![(Abs::zero(), Abs::zero()); nrows]; - ctx.style(ctx.style.for_denominator()); + // We want to transpose our data layout to columns + // before final layout. For efficiency, the columns + // variable is set up here and newly generated + // individual cells are then added to it. let mut cols = vec![vec![]; ncols]; + + ctx.style(ctx.style.for_denominator()); for (row, (ascent, descent)) in rows.iter().zip(&mut heights) { for (cell, col) in row.iter().zip(&mut cols) { let cell = ctx.layout_row(cell)?; + ascent.set_max(cell.ascent()); descent.set_max(cell.descent()); + col.push(cell); } } ctx.unstyle(); - let mut frame = Frame::new(Size::new( - Abs::zero(), - heights.iter().map(|&(a, b)| a + b).sum::() + row_gap * (nrows - 1) as f64, - )); + // For each row, combine maximum ascent and descent into a row height. + // Sum the row heights, then add the total height of the gaps between rows. + let total_height = + heights.iter().map(|&(a, b)| a + b).sum::() + row_gap * (nrows - 1) as f64; + + // Width starts at zero because it can't be calculated until later + let mut frame = Frame::new(Size::new(Abs::zero(), total_height)); + let mut x = Abs::zero(); - for col in cols { + + for (index, col) in cols.into_iter().enumerate() { let AlignmentResult { points, width: rcol } = alignments(&col); + let mut y = Abs::zero(); + for (cell, &(ascent, descent)) in col.into_iter().zip(&heights) { let cell = cell.into_aligned_frame(ctx, &points, Align::Center); let pos = Point::new( if points.is_empty() { x + (rcol - cell.width()) / 2.0 } else { x }, y + ascent - cell.ascent(), ); + frame.push_frame(pos, cell); + y += ascent + descent + row_gap; } - x += rcol + col_gap; + + // Advance to the end of the column + x += rcol; + + // If a vertical line should be inserted after this column + if vline.0.contains(&(index + 1)) { + frame.push( + Point::with_x(x + half_col_gap), + line_item(total_height, true, stroke.clone(), span), + ); + } + + // Advance to the start of the next column + x += col_gap; } - frame.size_mut().x = x - col_gap; + + // Once all the columns are laid out, the total width can be calculated + let total_width = x - col_gap; + + // This allows the horizontal lines to be laid out + for line in hline.0 { + let offset = (heights[0..line].iter().map(|&(a, b)| a + b).sum::() + + row_gap * (line - 1) as f64) + + half_row_gap; + + frame.push( + Point::with_y(offset), + line_item(total_width, false, stroke.clone(), span), + ); + } + + frame.size_mut().x = total_width; Ok(frame) } -/// Layout the outer wrapper around a vector's or matrices' body. +fn line_item(length: Abs, vertical: bool, stroke: Stroke, span: Span) -> FrameItem { + let line_geom = if vertical { + Geometry::Line(Point::with_y(length)) + } else { + Geometry::Line(Point::with_x(length)) + }; + + FrameItem::Shape( + Shape { + geometry: line_geom, + fill: None, + stroke: Some(stroke), + }, + span, + ) +} + +/// Layout the outer wrapper around the body of a vector or matrix. fn layout_delimiters( ctx: &mut MathContext, mut frame: Frame, @@ -312,3 +478,91 @@ fn layout_delimiters( Ok(()) } + +/// Parameters specifying how augmentation lines +/// should be drawn on a matrix. +#[derive(Default, Clone, Hash)] +pub struct Augment { + pub hline: Offsets, + pub vline: Offsets, + pub stroke: Smart>, +} + +impl Augment { + fn stroke_or(&self, fallback: Stroke) -> Stroke { + match &self.stroke { + Smart::Custom(v) => v.clone().unwrap_or(fallback), + _ => fallback, + } + } +} + +impl Resolve for Augment { + type Output = Augment; + + fn resolve(self, styles: StyleChain) -> Self::Output { + Augment { + hline: self.hline, + vline: self.vline, + stroke: self.stroke.resolve(styles), + } + } +} + +impl Fold for Augment { + type Output = Augment; + + fn fold(mut self, outer: Self::Output) -> Self::Output { + self.stroke = self.stroke.fold(outer.stroke); + self + } +} + +cast! { + Augment, + self => { + let stroke = self.stroke.unwrap_or_default(); + + let d = dict! { + "hline" => self.hline.into_value(), + "vline" => self.vline.into_value(), + "stroke" => stroke.into_value() + }; + + d.into_value() + }, + v: usize => Augment { + hline: Offsets::default(), + vline: Offsets(vec![v]), + stroke: Smart::Auto, + }, + mut dict: Dict => { + // need the transpose for the defaults to work + let hline = dict.take("hline").ok().map(Offsets::from_value) + .transpose().unwrap_or_default().unwrap_or_default(); + let vline = dict.take("vline").ok().map(Offsets::from_value) + .transpose().unwrap_or_default().unwrap_or_default(); + + let stroke = dict.take("stroke").ok().map(PartialStroke::from_value) + .transpose()?.map(Smart::Custom).unwrap_or(Smart::Auto); + + Augment { hline, vline, stroke } + }, +} + +cast! { + Augment, + self => self.into_value(), +} + +/// The offsets at which augmentation lines +/// should be drawn on a matrix. +#[derive(Debug, Default, Clone, Hash)] +pub struct Offsets(Vec); + +cast! { + Offsets, + self => self.0.into_value(), + v: usize => Self(vec![v]), + v: Array => Self(v.into_iter().map(Value::cast).collect::>()?), +} diff --git a/tests/ref/math/matrix.png b/tests/ref/math/matrix.png index a14758f7a4ee67dedf4d3b2a3b2698f32a2187f8..530c2b5a5f521d0eea0d03179ffe7e797385eaa1 100644 GIT binary patch delta 15195 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 delta 1032 zcmV+j1o!*WxB`O80gx6C1HuRb0000HL^0g}0A$IrCHY`~w|71vO@?tC@942S)gegg zB2+3tQQ;*I(IF_jBwEr%$e=?hN=3(@Lo_roz35^KETunW;1EQEHixWW8z@649hA%v zK`|Ki^x<{9yln4blXueL`$1%Tbuxdy_wzo_@Atg=cr63__4C4bJYKvEp-_l}&gBjk zn5wI*jYUg;OA8a_ypbnwpYvEO=JWZi{hqx68Lp39w4c69206cftSuS`>wnfb!UyBV z^YG(58T_$6vnvQ4G7v`N1}P(PlEGs*d)Um_TN&(&^vcRgkuuo+q|<4qsLYz|S#m=| zgK@wvx7L=-Wds@SPu#Mf&ZWO_eutNGx(?0t)W0}?%8?Vk11Eizj0{gdKgyGV&Z*fI zgbo=9qj7_jkvPfVG0bFYYim81!SDC;!r9r`;$;{d9OR&L&7rHS%a(yUp-_glwzhK1 z@L-}nPlm|SG>^yU-yDCoKlpWk64_Xs={rm@ohO4mH7b=zB!Uct(YQg%NStKw7_LC< z8NRiDwdJi0;c%E21_FWNWnkZb4o*)`JIh4&7&jKp&COL+RnBD)rb*Pw<(A>b>r3`i zxl7})7I%IHdnvpD0M&cxc_t2l2oSZanu-QHDb{C8P*=&}C z(eWr=hRw}QR^l2P8+QumR4PRQ-QC@7EJjCvN1YWp8qiQmBoeL#w1+Zq`Jd0kd7RzO zQa{)QG%eTqfn$`v#^Pby0Kg}ZBNQ7!?jm` z7ig{C`OtjXfBIDKp`AGru=sGQ_u#qyBX>WxUU_-;-uTV){Z$^y!0*oOD#i_xMRp`Z zS>Q^{j69Rc7zblxWArikOplMxBO@cxXw;Mf1p}w7>&>vs%ge*V!y6kLrT{MXwAed3 zI);XZm=6?IIIPFfS9Sfd$z+lrOHavvctj}m{@c$eBLi(BRty$@FBk{u_3!}^1`IZs&~DFi4aQx+=gjH(GV_zM(A9tYSN^?blVGn1O5Sd=5?v<>xD1?0000