Make radius configuration unconfusing

This commit is contained in:
Laurenz 2022-06-14 15:07:13 +02:00
parent 6832ca2a26
commit 7a6c2cce77
10 changed files with 346 additions and 127 deletions

View File

@ -76,6 +76,11 @@ impl Dict {
} }
} }
/// Remove the value if the dictionary contains the given key.
pub fn take(&mut self, key: &str) -> Option<Value> {
Arc::make_mut(&mut self.0).remove(key)
}
/// Clear the dictionary. /// Clear the dictionary.
pub fn clear(&mut self) { pub fn clear(&mut self) {
if Arc::strong_count(&self.0) == 1 { if Arc::strong_count(&self.0) == 1 {

View File

@ -8,7 +8,8 @@ use std::sync::Arc;
use super::{ops, Args, Array, Dict, Func, RawLength, Regex}; use super::{ops, Args, Array, Dict, Func, RawLength, Regex};
use crate::diag::{with_alternative, StrResult}; use crate::diag::{with_alternative, StrResult};
use crate::geom::{ use crate::geom::{
Angle, Color, Dir, Em, Fraction, Length, Paint, Ratio, Relative, RgbaColor, Sides, Angle, Color, Corners, Dir, Em, Fraction, Length, Paint, Ratio, Relative, RgbaColor,
Sides,
}; };
use crate::library::text::RawNode; use crate::library::text::RawNode;
use crate::model::{Content, Group, Layout, LayoutNode, Pattern}; use crate::model::{Content, Group, Layout, LayoutNode, Pattern};
@ -516,6 +517,71 @@ impl<T: Cast> Cast<Spanned<Value>> for Spanned<T> {
} }
} }
dynamic! {
Dir: "direction",
}
dynamic! {
Regex: "regular expression",
}
dynamic! {
Group: "group",
}
castable! {
usize,
Expected: "non-negative integer",
Value::Int(int) => int.try_into().map_err(|_| {
if int < 0 {
"must be at least zero"
} else {
"number too large"
}
})?,
}
castable! {
NonZeroUsize,
Expected: "positive integer",
Value::Int(int) => int
.try_into()
.and_then(|int: usize| int.try_into())
.map_err(|_| if int <= 0 {
"must be positive"
} else {
"number too large"
})?,
}
castable! {
Paint,
Expected: "color",
Value::Color(color) => Paint::Solid(color),
}
castable! {
String,
Expected: "string",
Value::Str(string) => string.into(),
}
castable! {
LayoutNode,
Expected: "content",
Value::None => Self::default(),
Value::Str(text) => Content::Text(text).pack(),
Value::Content(content) => content.pack(),
}
castable! {
Pattern,
Expected: "function, string or regular expression",
Value::Func(func) => Self::Node(func.node()?),
Value::Str(text) => Self::text(&text),
@regex: Regex => Self::Regex(regex.clone()),
}
impl<T: Cast> Cast for Option<T> { impl<T: Cast> Cast for Option<T> {
fn is(value: &Value) -> bool { fn is(value: &Value) -> bool {
matches!(value, Value::None) || T::is(value) matches!(value, Value::None) || T::is(value)
@ -609,112 +675,84 @@ impl<T: Cast> Cast for Smart<T> {
impl<T> Cast for Sides<T> impl<T> Cast for Sides<T>
where where
T: Cast + Default + Clone, T: Cast + Default + Copy,
{ {
fn is(value: &Value) -> bool { fn is(value: &Value) -> bool {
matches!(value, Value::Dict(_)) || T::is(value) matches!(value, Value::Dict(_)) || T::is(value)
} }
fn cast(value: Value) -> StrResult<Self> { fn cast(mut value: Value) -> StrResult<Self> {
match value { if let Value::Dict(dict) = &mut value {
Value::Dict(dict) => { let mut take = |key| dict.take(key).map(T::cast).transpose();
for (key, _) in &dict {
if !matches!(
key.as_str(),
"left" | "top" | "right" | "bottom" | "x" | "y" | "rest"
) {
return Err(format!("unexpected key {key:?}"));
}
}
let rest = take("rest")?;
let x = take("x")?.or(rest);
let y = take("y")?.or(rest);
let sides = Sides { let sides = Sides {
left: dict.get("left").or(dict.get("x")), left: take("left")?.or(x),
top: dict.get("top").or(dict.get("y")), top: take("top")?.or(y),
right: dict.get("right").or(dict.get("x")), right: take("right")?.or(x),
bottom: dict.get("bottom").or(dict.get("y")), bottom: take("bottom")?.or(y),
}; };
Ok(sides.map(|side| { if let Some((key, _)) = dict.iter().next() {
side.or(dict.get("rest")) return Err(format!("unexpected key {key:?}"));
.cloned()
.and_then(T::cast)
.unwrap_or_default()
}))
} }
v => T::cast(v).map(Sides::splat).map_err(|msg| {
Ok(sides.map(Option::unwrap_or_default))
} else {
T::cast(value).map(Self::splat).map_err(|msg| {
with_alternative( with_alternative(
msg, msg,
"dictionary with any of `left`, `top`, `right`, `bottom`, \ "dictionary with any of \
`left`, `top`, `right`, `bottom`, \
`x`, `y`, or `rest` as keys", `x`, `y`, or `rest` as keys",
) )
}), })
} }
} }
} }
dynamic! { impl<T> Cast for Corners<T>
Dir: "direction", where
} T: Cast + Default + Copy,
{
fn is(value: &Value) -> bool {
matches!(value, Value::Dict(_)) || T::is(value)
}
dynamic! { fn cast(mut value: Value) -> StrResult<Self> {
Regex: "regular expression", if let Value::Dict(dict) = &mut value {
} let mut take = |key| dict.take(key).map(T::cast).transpose();
dynamic! { let rest = take("rest")?;
Group: "group", let left = take("left")?.or(rest);
} let top = take("top")?.or(rest);
let right = take("right")?.or(rest);
let bottom = take("bottom")?.or(rest);
let corners = Corners {
top_left: take("top-left")?.or(top).or(left),
top_right: take("top-right")?.or(top).or(right),
bottom_right: take("bottom-right")?.or(bottom).or(right),
bottom_left: take("bottom-left")?.or(bottom).or(left),
};
castable! { if let Some((key, _)) = dict.iter().next() {
usize, return Err(format!("unexpected key {key:?}"));
Expected: "non-negative integer", }
Value::Int(int) => int.try_into().map_err(|_| {
if int < 0 { Ok(corners.map(Option::unwrap_or_default))
"must be at least zero"
} else { } else {
"number too large" T::cast(value).map(Self::splat).map_err(|msg| {
with_alternative(
msg,
"dictionary with any of \
`top-left`, `top-right`, `bottom-right`, `bottom-left`, \
`left`, `top`, `right`, `bottom`, or `rest` as keys",
)
})
}
} }
})?,
}
castable! {
NonZeroUsize,
Expected: "positive integer",
Value::Int(int) => int
.try_into()
.and_then(|int: usize| int.try_into())
.map_err(|_| if int <= 0 {
"must be positive"
} else {
"number too large"
})?,
}
castable! {
Paint,
Expected: "color",
Value::Color(color) => Paint::Solid(color),
}
castable! {
String,
Expected: "string",
Value::Str(string) => string.into(),
}
castable! {
LayoutNode,
Expected: "content",
Value::None => Self::default(),
Value::Str(text) => Content::Text(text).pack(),
Value::Content(content) => content.pack(),
}
castable! {
Pattern,
Expected: "function, string or regular expression",
Value::Func(func) => Self::Node(func.node()?),
Value::Str(text) => Self::text(&text),
@regex: Regex => Self::Regex(regex.clone()),
} }
#[cfg(test)] #[cfg(test)]

122
src/geom/corners.rs Normal file
View File

@ -0,0 +1,122 @@
use super::*;
/// A container with components for the four corners of a rectangle.
#[derive(Debug, Default, Copy, Clone, Eq, PartialEq, Hash)]
pub struct Corners<T> {
/// The value for the top left corner.
pub top_left: T,
/// The value for the top right corner.
pub top_right: T,
/// The value for the bottom right corner.
pub bottom_right: T,
/// The value for the bottom left corner.
pub bottom_left: T,
}
impl<T> Corners<T> {
/// Create a new instance from the four components.
pub const fn new(top_left: T, top_right: T, bottom_right: T, bottom_left: T) -> Self {
Self {
top_left,
top_right,
bottom_right,
bottom_left,
}
}
/// Create an instance with four equal components.
pub fn splat(value: T) -> Self
where
T: Clone,
{
Self {
top_left: value.clone(),
top_right: value.clone(),
bottom_right: value.clone(),
bottom_left: value,
}
}
/// Map the individual fields with `f`.
pub fn map<F, U>(self, mut f: F) -> Corners<U>
where
F: FnMut(T) -> U,
{
Corners {
top_left: f(self.top_left),
top_right: f(self.top_right),
bottom_right: f(self.bottom_right),
bottom_left: f(self.bottom_left),
}
}
/// Zip two instances into an instance.
pub fn zip<F, V, W>(self, other: Corners<V>, mut f: F) -> Corners<W>
where
F: FnMut(T, V) -> W,
{
Corners {
top_left: f(self.top_left, other.top_left),
top_right: f(self.top_right, other.top_right),
bottom_right: f(self.bottom_right, other.bottom_right),
bottom_left: f(self.bottom_left, other.bottom_left),
}
}
/// An iterator over the corners, starting with the top left corner,
/// clockwise.
pub fn iter(&self) -> impl Iterator<Item = &T> {
[
&self.top_left,
&self.top_right,
&self.bottom_right,
&self.bottom_left,
]
.into_iter()
}
/// Whether all sides are equal.
pub fn is_uniform(&self) -> bool
where
T: PartialEq,
{
self.top_left == self.top_right
&& self.top_right == self.bottom_right
&& self.bottom_right == self.bottom_left
}
}
impl<T> Get<Corner> for Corners<T> {
type Component = T;
fn get(self, corner: Corner) -> T {
match corner {
Corner::TopLeft => self.top_left,
Corner::TopRight => self.top_right,
Corner::BottomRight => self.bottom_right,
Corner::BottomLeft => self.bottom_left,
}
}
fn get_mut(&mut self, corner: Corner) -> &mut T {
match corner {
Corner::TopLeft => &mut self.top_left,
Corner::TopRight => &mut self.top_right,
Corner::BottomRight => &mut self.bottom_right,
Corner::BottomLeft => &mut self.bottom_left,
}
}
}
/// The four corners of a rectangle.
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
pub enum Corner {
/// The top left corner.
TopLeft,
/// The top right corner.
TopRight,
/// The bottom right corner.
BottomRight,
/// The bottom left corner.
BottomLeft,
}

View File

@ -4,6 +4,7 @@
mod macros; mod macros;
mod align; mod align;
mod angle; mod angle;
mod corners;
mod dir; mod dir;
mod em; mod em;
mod fraction; mod fraction;
@ -22,6 +23,7 @@ mod transform;
pub use align::*; pub use align::*;
pub use angle::*; pub use angle::*;
pub use corners::*;
pub use dir::*; pub use dir::*;
pub use em::*; pub use em::*;
pub use fraction::*; pub use fraction::*;

View File

@ -7,13 +7,13 @@ use std::mem;
pub struct RoundedRect { pub struct RoundedRect {
/// The size of the rectangle. /// The size of the rectangle.
pub size: Size, pub size: Size,
/// The radius at each side. /// The radius at each corner.
pub radius: Sides<Length>, pub radius: Corners<Length>,
} }
impl RoundedRect { impl RoundedRect {
/// Create a new rounded rectangle. /// Create a new rounded rectangle.
pub fn new(size: Size, radius: Sides<Length>) -> Self { pub fn new(size: Size, radius: Corners<Length>) -> Self {
Self { size, radius } Self { size, radius }
} }
@ -73,20 +73,20 @@ impl RoundedRect {
let mut always_continuous = true; let mut always_continuous = true;
for side in [Side::Top, Side::Right, Side::Bottom, Side::Left] { for side in [Side::Top, Side::Right, Side::Bottom, Side::Left] {
let is_continuous = strokes.get(side) == strokes.get(side.next_cw()); let continuous = strokes.get(side) == strokes.get(side.next_cw());
connection = connection.advance(is_continuous && side != Side::Left); connection = connection.advance(continuous && side != Side::Left);
always_continuous &= is_continuous; always_continuous &= continuous;
draw_side( draw_side(
&mut path, &mut path,
side, side,
self.size, self.size,
self.radius.get(side.next_ccw()), self.radius.get(side.start_corner()),
self.radius.get(side), self.radius.get(side.end_corner()),
connection, connection,
); );
if !is_continuous { if !continuous {
res.push((mem::take(&mut path), strokes.get(side))); res.push((mem::take(&mut path), strokes.get(side)));
} }
} }
@ -109,8 +109,8 @@ fn draw_side(
path: &mut Path, path: &mut Path,
side: Side, side: Side,
size: Size, size: Size,
radius_left: Length, start_radius: Length,
radius_right: Length, end_radius: Length,
connection: Connection, connection: Connection,
) { ) {
let angle_left = Angle::deg(if connection.prev { 90.0 } else { 45.0 }); let angle_left = Angle::deg(if connection.prev { 90.0 } else { 45.0 });
@ -118,23 +118,23 @@ fn draw_side(
let length = size.get(side.axis()); let length = size.get(side.axis());
// The arcs for a border of the rectangle along the x-axis, starting at (0,0). // The arcs for a border of the rectangle along the x-axis, starting at (0,0).
let p1 = Point::with_x(radius_left); let p1 = Point::with_x(start_radius);
let mut arc1 = bezier_arc( let mut arc1 = bezier_arc(
p1 + Point::new( p1 + Point::new(
-angle_left.sin() * radius_left, -angle_left.sin() * start_radius,
(1.0 - angle_left.cos()) * radius_left, (1.0 - angle_left.cos()) * start_radius,
), ),
Point::new(radius_left, radius_left), Point::new(start_radius, start_radius),
p1, p1,
); );
let p2 = Point::with_x(length - radius_right); let p2 = Point::with_x(length - end_radius);
let mut arc2 = bezier_arc( let mut arc2 = bezier_arc(
p2, p2,
Point::new(length - radius_right, radius_right), Point::new(length - end_radius, end_radius),
p2 + Point::new( p2 + Point::new(
angle_right.sin() * radius_right, angle_right.sin() * end_radius,
(1.0 - angle_right.cos()) * radius_right, (1.0 - angle_right.cos()) * end_radius,
), ),
); );
@ -152,16 +152,16 @@ fn draw_side(
arc2 = arc2.map(|x| x.transform(transform)); arc2 = arc2.map(|x| x.transform(transform));
if !connection.prev { if !connection.prev {
path.move_to(if radius_left.is_zero() { arc1[3] } else { arc1[0] }); path.move_to(if start_radius.is_zero() { arc1[3] } else { arc1[0] });
} }
if !radius_left.is_zero() { if !start_radius.is_zero() {
path.cubic_to(arc1[1], arc1[2], arc1[3]); path.cubic_to(arc1[1], arc1[2], arc1[3]);
} }
path.line_to(arc2[0]); path.line_to(arc2[0]);
if !connection.next && !radius_right.is_zero() { if !connection.next && !end_radius.is_zero() {
path.cubic_to(arc2[1], arc2[2], arc2[3]); path.cubic_to(arc2[1], arc2[2], arc2[3]);
} }
} }

View File

@ -48,17 +48,17 @@ impl<T> Sides<T> {
/// Zip two instances into an instance. /// Zip two instances into an instance.
pub fn zip<F, V, W>(self, other: Sides<V>, mut f: F) -> Sides<W> pub fn zip<F, V, W>(self, other: Sides<V>, mut f: F) -> Sides<W>
where where
F: FnMut(T, V, Side) -> W, F: FnMut(T, V) -> W,
{ {
Sides { Sides {
left: f(self.left, other.left, Side::Left), left: f(self.left, other.left),
top: f(self.top, other.top, Side::Top), top: f(self.top, other.top),
right: f(self.right, other.right, Side::Right), right: f(self.right, other.right),
bottom: f(self.bottom, other.bottom, Side::Bottom), bottom: f(self.bottom, other.bottom),
} }
} }
/// An iterator over the sides. /// An iterator over the sides, starting with the left side, clockwise.
pub fn iter(&self) -> impl Iterator<Item = &T> { pub fn iter(&self) -> impl Iterator<Item = &T> {
[&self.left, &self.top, &self.right, &self.bottom].into_iter() [&self.left, &self.top, &self.right, &self.bottom].into_iter()
} }
@ -157,6 +157,21 @@ impl Side {
} }
} }
/// The first corner of the side in clockwise order.
pub fn start_corner(self) -> Corner {
match self {
Self::Left => Corner::BottomLeft,
Self::Top => Corner::TopLeft,
Self::Right => Corner::TopRight,
Self::Bottom => Corner::BottomRight,
}
}
/// The second corner of the side in clockwise order.
pub fn end_corner(self) -> Corner {
self.next_cw().start_corner()
}
/// Return the corresponding axis. /// Return the corresponding axis.
pub fn axis(self) -> SpecAxis { pub fn axis(self) -> SpecAxis {
match self { match self {

View File

@ -33,9 +33,11 @@ impl<const S: ShapeKind> ShapeNode<S> {
/// How much to extend the shape's dimensions beyond the allocated space. /// How much to extend the shape's dimensions beyond the allocated space.
#[property(resolve, fold)] #[property(resolve, fold)]
pub const OUTSET: Sides<Option<Relative<RawLength>>> = Sides::splat(Relative::zero()); pub const OUTSET: Sides<Option<Relative<RawLength>>> = Sides::splat(Relative::zero());
/// How much to round the shape's corners. /// How much to round the shape's corners.
#[property(skip, resolve, fold)] #[property(skip, resolve, fold)]
pub const RADIUS: Sides<Option<Relative<RawLength>>> = Sides::splat(Relative::zero()); pub const RADIUS: Corners<Option<Relative<RawLength>>> =
Corners::splat(Relative::zero());
fn construct(_: &mut Machine, args: &mut Args) -> TypResult<Content> { fn construct(_: &mut Machine, args: &mut Args) -> TypResult<Content> {
let size = match S { let size = match S {

View File

@ -5,7 +5,7 @@ use std::sync::Arc;
use super::{Interruption, NodeId, StyleChain}; use super::{Interruption, NodeId, StyleChain};
use crate::eval::{RawLength, Smart}; use crate::eval::{RawLength, Smart};
use crate::geom::{Length, Numeric, Relative, Sides, Spec}; use crate::geom::{Corners, Length, Numeric, Relative, Sides, Spec};
use crate::library::layout::PageNode; use crate::library::layout::PageNode;
use crate::library::structure::{EnumNode, ListNode}; use crate::library::structure::{EnumNode, ListNode};
use crate::library::text::ParNode; use crate::library::text::ParNode;
@ -191,12 +191,15 @@ impl<T: Resolve> Resolve for Sides<T> {
type Output = Sides<T::Output>; type Output = Sides<T::Output>;
fn resolve(self, styles: StyleChain) -> Self::Output { fn resolve(self, styles: StyleChain) -> Self::Output {
Sides { self.map(|v| v.resolve(styles))
left: self.left.resolve(styles),
right: self.right.resolve(styles),
top: self.top.resolve(styles),
bottom: self.bottom.resolve(styles),
} }
}
impl<T: Resolve> Resolve for Corners<T> {
type Output = Corners<T::Output>;
fn resolve(self, styles: StyleChain) -> Self::Output {
self.map(|v| v.resolve(styles))
} }
} }
@ -252,7 +255,7 @@ where
type Output = Sides<T::Output>; type Output = Sides<T::Output>;
fn fold(self, outer: Self::Output) -> Self::Output { fn fold(self, outer: Self::Output) -> Self::Output {
self.zip(outer, |inner, outer, _| inner.fold(outer)) self.zip(outer, |inner, outer| inner.fold(outer))
} }
} }
@ -260,7 +263,7 @@ impl Fold for Sides<Option<Relative<Length>>> {
type Output = Sides<Relative<Length>>; type Output = Sides<Relative<Length>>;
fn fold(self, outer: Self::Output) -> Self::Output { fn fold(self, outer: Self::Output) -> Self::Output {
self.zip(outer, |inner, outer, _| inner.unwrap_or(outer)) self.zip(outer, |inner, outer| inner.unwrap_or(outer))
} }
} }
@ -268,7 +271,26 @@ impl Fold for Sides<Option<Smart<Relative<RawLength>>>> {
type Output = Sides<Smart<Relative<RawLength>>>; type Output = Sides<Smart<Relative<RawLength>>>;
fn fold(self, outer: Self::Output) -> Self::Output { fn fold(self, outer: Self::Output) -> Self::Output {
self.zip(outer, |inner, outer, _| inner.unwrap_or(outer)) self.zip(outer, |inner, outer| inner.unwrap_or(outer))
}
}
impl<T> Fold for Corners<T>
where
T: Fold,
{
type Output = Corners<T::Output>;
fn fold(self, outer: Self::Output) -> Self::Output {
self.zip(outer, |inner, outer| inner.fold(outer))
}
}
impl Fold for Corners<Option<Relative<Length>>> {
type Output = Corners<Relative<Length>>;
fn fold(self, outer: Self::Output) -> Self::Output {
self.zip(outer, |inner, outer| inner.unwrap_or(outer))
} }
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 16 KiB

View File

@ -30,8 +30,13 @@
// Rounded corners. // Rounded corners.
#rect(width: 2cm, radius: 60%) #rect(width: 2cm, radius: 60%)
#rect(width: 1cm, radius: (x: 5pt, y: 10pt)) #rect(width: 1cm, radius: (left: 10pt, right: 5pt))
#rect(width: 1.25cm, radius: (left: 2pt, top: 5pt, right: 8pt, bottom: 11pt)) #rect(width: 1.25cm, radius: (
top-left: 2pt,
top-right: 5pt,
bottom-right: 8pt,
bottom-left: 11pt
))
// Different strokes. // Different strokes.
[ [
@ -54,3 +59,11 @@ Use the `*const T` pointer or the `&mut T` reference.
--- ---
// Error: 15-38 unexpected key "cake" // Error: 15-38 unexpected key "cake"
#rect(radius: (left: 10pt, cake: 5pt)) #rect(radius: (left: 10pt, cake: 5pt))
---
// Error: 15-21 expected stroke or none or dictionary with any of `left`, `top`, `right`, `bottom`, `x`, `y`, or `rest` as keys or auto, found array
#rect(stroke: (1, 2))
---
// Error: 15-19 expected relative length or none or dictionary with any of `top-left`, `top-right`, `bottom-right`, `bottom-left`, `left`, `top`, `right`, `bottom`, or `rest` as keys, found color
#rect(radius: blue)