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.
pub fn clear(&mut self) {
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 crate::diag::{with_alternative, StrResult};
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::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> {
fn is(value: &Value) -> bool {
matches!(value, Value::None) || T::is(value)
@ -609,112 +675,84 @@ impl<T: Cast> Cast for Smart<T> {
impl<T> Cast for Sides<T>
where
T: Cast + Default + Clone,
T: Cast + Default + Copy,
{
fn is(value: &Value) -> bool {
matches!(value, Value::Dict(_)) || T::is(value)
}
fn cast(value: Value) -> StrResult<Self> {
match value {
Value::Dict(dict) => {
for (key, _) in &dict {
if !matches!(
key.as_str(),
"left" | "top" | "right" | "bottom" | "x" | "y" | "rest"
) {
return Err(format!("unexpected key {key:?}"));
}
}
fn cast(mut value: Value) -> StrResult<Self> {
if let Value::Dict(dict) = &mut value {
let mut take = |key| dict.take(key).map(T::cast).transpose();
let sides = Sides {
left: dict.get("left").or(dict.get("x")),
top: dict.get("top").or(dict.get("y")),
right: dict.get("right").or(dict.get("x")),
bottom: dict.get("bottom").or(dict.get("y")),
};
let rest = take("rest")?;
let x = take("x")?.or(rest);
let y = take("y")?.or(rest);
let sides = Sides {
left: take("left")?.or(x),
top: take("top")?.or(y),
right: take("right")?.or(x),
bottom: take("bottom")?.or(y),
};
Ok(sides.map(|side| {
side.or(dict.get("rest"))
.cloned()
.and_then(T::cast)
.unwrap_or_default()
}))
if let Some((key, _)) = dict.iter().next() {
return Err(format!("unexpected key {key:?}"));
}
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(
msg,
"dictionary with any of `left`, `top`, `right`, `bottom`, \
"dictionary with any of \
`left`, `top`, `right`, `bottom`, \
`x`, `y`, or `rest` as keys",
)
}),
})
}
}
}
dynamic! {
Dir: "direction",
}
impl<T> Cast for Corners<T>
where
T: Cast + Default + Copy,
{
fn is(value: &Value) -> bool {
matches!(value, Value::Dict(_)) || T::is(value)
}
dynamic! {
Regex: "regular expression",
}
fn cast(mut value: Value) -> StrResult<Self> {
if let Value::Dict(dict) = &mut value {
let mut take = |key| dict.take(key).map(T::cast).transpose();
dynamic! {
Group: "group",
}
let rest = take("rest")?;
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! {
usize,
Expected: "non-negative integer",
Value::Int(int) => int.try_into().map_err(|_| {
if int < 0 {
"must be at least zero"
if let Some((key, _)) = dict.iter().next() {
return Err(format!("unexpected key {key:?}"));
}
Ok(corners.map(Option::unwrap_or_default))
} 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)]

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 align;
mod angle;
mod corners;
mod dir;
mod em;
mod fraction;
@ -22,6 +23,7 @@ mod transform;
pub use align::*;
pub use angle::*;
pub use corners::*;
pub use dir::*;
pub use em::*;
pub use fraction::*;

View File

@ -7,13 +7,13 @@ use std::mem;
pub struct RoundedRect {
/// The size of the rectangle.
pub size: Size,
/// The radius at each side.
pub radius: Sides<Length>,
/// The radius at each corner.
pub radius: Corners<Length>,
}
impl RoundedRect {
/// 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 }
}
@ -73,20 +73,20 @@ impl RoundedRect {
let mut always_continuous = true;
for side in [Side::Top, Side::Right, Side::Bottom, Side::Left] {
let is_continuous = strokes.get(side) == strokes.get(side.next_cw());
connection = connection.advance(is_continuous && side != Side::Left);
always_continuous &= is_continuous;
let continuous = strokes.get(side) == strokes.get(side.next_cw());
connection = connection.advance(continuous && side != Side::Left);
always_continuous &= continuous;
draw_side(
&mut path,
side,
self.size,
self.radius.get(side.next_ccw()),
self.radius.get(side),
self.radius.get(side.start_corner()),
self.radius.get(side.end_corner()),
connection,
);
if !is_continuous {
if !continuous {
res.push((mem::take(&mut path), strokes.get(side)));
}
}
@ -109,8 +109,8 @@ fn draw_side(
path: &mut Path,
side: Side,
size: Size,
radius_left: Length,
radius_right: Length,
start_radius: Length,
end_radius: Length,
connection: Connection,
) {
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());
// 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(
p1 + Point::new(
-angle_left.sin() * radius_left,
(1.0 - angle_left.cos()) * radius_left,
-angle_left.sin() * start_radius,
(1.0 - angle_left.cos()) * start_radius,
),
Point::new(radius_left, radius_left),
Point::new(start_radius, start_radius),
p1,
);
let p2 = Point::with_x(length - radius_right);
let p2 = Point::with_x(length - end_radius);
let mut arc2 = bezier_arc(
p2,
Point::new(length - radius_right, radius_right),
Point::new(length - end_radius, end_radius),
p2 + Point::new(
angle_right.sin() * radius_right,
(1.0 - angle_right.cos()) * radius_right,
angle_right.sin() * end_radius,
(1.0 - angle_right.cos()) * end_radius,
),
);
@ -152,16 +152,16 @@ fn draw_side(
arc2 = arc2.map(|x| x.transform(transform));
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.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]);
}
}

View File

@ -48,17 +48,17 @@ impl<T> Sides<T> {
/// Zip two instances into an instance.
pub fn zip<F, V, W>(self, other: Sides<V>, mut f: F) -> Sides<W>
where
F: FnMut(T, V, Side) -> W,
F: FnMut(T, V) -> W,
{
Sides {
left: f(self.left, other.left, Side::Left),
top: f(self.top, other.top, Side::Top),
right: f(self.right, other.right, Side::Right),
bottom: f(self.bottom, other.bottom, Side::Bottom),
left: f(self.left, other.left),
top: f(self.top, other.top),
right: f(self.right, other.right),
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> {
[&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.
pub fn axis(self) -> SpecAxis {
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.
#[property(resolve, fold)]
pub const OUTSET: Sides<Option<Relative<RawLength>>> = Sides::splat(Relative::zero());
/// How much to round the shape's corners.
#[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> {
let size = match S {

View File

@ -5,7 +5,7 @@ use std::sync::Arc;
use super::{Interruption, NodeId, StyleChain};
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::structure::{EnumNode, ListNode};
use crate::library::text::ParNode;
@ -191,12 +191,15 @@ impl<T: Resolve> Resolve for Sides<T> {
type Output = Sides<T::Output>;
fn resolve(self, styles: StyleChain) -> Self::Output {
Sides {
left: self.left.resolve(styles),
right: self.right.resolve(styles),
top: self.top.resolve(styles),
bottom: self.bottom.resolve(styles),
}
self.map(|v| v.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>;
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>>;
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>>>;
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.
#rect(width: 2cm, radius: 60%)
#rect(width: 1cm, radius: (x: 5pt, y: 10pt))
#rect(width: 1.25cm, radius: (left: 2pt, top: 5pt, right: 8pt, bottom: 11pt))
#rect(width: 1cm, radius: (left: 10pt, right: 5pt))
#rect(width: 1.25cm, radius: (
top-left: 2pt,
top-right: 5pt,
bottom-right: 8pt,
bottom-left: 11pt
))
// Different strokes.
[
@ -54,3 +59,11 @@ Use the `*const T` pointer or the `&mut T` reference.
---
// Error: 15-38 unexpected key "cake"
#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)