diff --git a/src/geom/angle.rs b/src/geom/angle.rs index ef3276e82..4e08a518a 100644 --- a/src/geom/angle.rs +++ b/src/geom/angle.rs @@ -40,6 +40,16 @@ impl Angle { (self.0).0 } + /// Get the sine of this angle. + pub fn sin(self) -> f64 { + self.to_rad().sin() + } + + /// Get the cosine of this angle. + pub fn cos(self) -> f64 { + self.to_rad().cos() + } + /// Create an angle from a value in a unit. pub fn with_unit(val: f64, unit: AngularUnit) -> Self { Self(Scalar(val * unit.raw_scale())) diff --git a/src/geom/point.rs b/src/geom/point.rs index 6d77507b4..7ab0d3753 100644 --- a/src/geom/point.rs +++ b/src/geom/point.rs @@ -49,6 +49,12 @@ impl Point { } } +impl From> for Point { + fn from(spec: Spec) -> Self { + Self::new(spec.x, spec.y) + } +} + impl Get for Point { type Component = Length; diff --git a/src/library/graphics/line.rs b/src/library/graphics/line.rs new file mode 100644 index 000000000..141abb08b --- /dev/null +++ b/src/library/graphics/line.rs @@ -0,0 +1,61 @@ +use crate::library::prelude::*; + +/// Display a line without affecting the layout. +#[derive(Debug, Hash)] +pub struct LineNode(Spec, Spec); + +#[node] +impl LineNode { + /// How the stroke the line. + pub const STROKE: Smart = Smart::Auto; + /// The line's thickness. + pub const THICKNESS: Length = Length::pt(1.0); + + fn construct(_: &mut Context, args: &mut Args) -> TypResult { + let origin = args.named::>("origin")?.unwrap_or_default(); + let to = match args.named::>("to")? { + Some(to) => to.zip(origin).map(|(to, from)| to - from), + None => { + let length = + args.named::("length")?.unwrap_or(Length::cm(1.0).into()); + let angle = args.named::("angle")?.unwrap_or_default(); + + let x = angle.cos() * length; + let y = angle.sin() * length; + + Spec::new(x, y) + } + }; + + Ok(Content::inline(Self(origin, to))) + } +} + +impl Layout for LineNode { + fn layout( + &self, + _: &mut Context, + regions: &Regions, + styles: StyleChain, + ) -> TypResult>> { + let target = regions.expand.select(regions.first, Size::zero()); + let mut frame = Frame::new(target); + + let thickness = styles.get(Self::THICKNESS); + let stroke = Some(Stroke { + paint: styles.get(Self::STROKE).unwrap_or(Color::BLACK.into()), + thickness, + }); + + let resolved_origin = + self.0.zip(regions.base).map(|(l, b)| Linear::resolve(l, b)); + let resolved_to = self.1.zip(regions.base).map(|(l, b)| Linear::resolve(l, b)); + + let geometry = Geometry::Line(resolved_to.into()); + + let shape = Shape { geometry, fill: None, stroke }; + frame.prepend(resolved_origin.into(), Element::Shape(shape)); + + Ok(vec![Arc::new(frame)]) + } +} diff --git a/src/library/graphics/mod.rs b/src/library/graphics/mod.rs index 353f09ca8..e9a6188f3 100644 --- a/src/library/graphics/mod.rs +++ b/src/library/graphics/mod.rs @@ -2,10 +2,12 @@ mod hide; mod image; +mod line; mod shape; mod transform; pub use self::image::*; pub use hide::*; +pub use line::*; pub use shape::*; pub use transform::*; diff --git a/src/library/mod.rs b/src/library/mod.rs index 8f00e5fe0..88f003c2d 100644 --- a/src/library/mod.rs +++ b/src/library/mod.rs @@ -53,6 +53,7 @@ pub fn new() -> Scope { // Graphics. std.def_node::("image"); + std.def_node::("line"); std.def_node::("rect"); std.def_node::("square"); std.def_node::("ellipse"); @@ -170,3 +171,14 @@ castable! { Expected: "content", Value::Content(content) => content.pack(), } + +castable! { + Spec, + Expected: "two-dimensional length array", + Value::Array(array) => { + let e = "point array must contain exactly two entries"; + let a = array.get(0).map_err(|_| e)?.clone().cast::()?; + let b = array.get(1).map_err(|_| e)?.clone().cast::()?; + Spec::new(a, b) + }, +} diff --git a/tests/ref/graphics/line.png b/tests/ref/graphics/line.png new file mode 100644 index 000000000..49f05f0d7 Binary files /dev/null and b/tests/ref/graphics/line.png differ diff --git a/tests/typ/graphics/line.typ b/tests/typ/graphics/line.typ new file mode 100644 index 000000000..d89c703a0 --- /dev/null +++ b/tests/typ/graphics/line.typ @@ -0,0 +1,47 @@ +// Test lines + +--- +// Default line. +#line() + +--- +// Test the to argument. +{ + line(to: (10pt, 0pt)) + line(origin: (0pt, 10pt), to: (0pt, 0pt)) + line(to: (15pt, 15pt)) +} +#v(.5cm) + +--- + +#set page(fill: rgb("0B1026")) +#set line(stroke: white) + +#let star(width, ..args) = box(width: width, height: width)[ + #set text(spacing: 0%) + #set line(..args) + + #line(length: +30%, origin: (09.0%, 02%)) + #line(length: +30%, origin: (38.7%, 02%), angle: -72deg) + #line(length: +30%, origin: (57.5%, 02%), angle: 252deg) + #line(length: +30%, origin: (57.3%, 02%)) + #line(length: -30%, origin: (88.0%, 02%), angle: -36deg) + #line(length: +30%, origin: (73.3%, 48%), angle: 252deg) + #line(length: -30%, origin: (73.5%, 48%), angle: 36deg) + #line(length: +30%, origin: (25.4%, 48%), angle: -36deg) + #line(length: +30%, origin: (25.6%, 48%), angle: -72deg) + #line(length: +32%, origin: (8.50%, 02%), angle: 34deg) +] + +#grid(columns: (1fr, ) * 3, ..((star(20pt, thickness: .5pt), ) * 9)) + +--- +// Test errors. + +// Error: 11-18 point array must contain exactly two entries +#line(to: (50pt,)) + +--- +// Error: 15-27 expected relative length, found angle +#line(origin: (3deg, 10pt), length: 5cm)