Allow for fitting paths into a given bounding rectangle

... by applying a transformation. This allows designing a path in some
other path design tool and then make it fit using bindings.
This commit is contained in:
Simon Hausmann 2020-07-13 15:38:56 +02:00
parent 79ba943882
commit 992f990fa8
12 changed files with 162 additions and 36 deletions

View file

@ -74,6 +74,8 @@ fn main() {
let path_primitive = rendering_primitives_builder.create(RenderingPrimitive::Path {
x: 50.,
y: 300.,
width: 0.,
height: 0.,
elements: PathData::Elements(SharedArray::from(TRIANGLE_PATH)),
fill_color: Color::from_rgb(0, 128, 255),
stroke_color: Color::BLACK,

View file

@ -929,11 +929,19 @@ fn compute_layout(component: &Rc<Component>) -> Vec<String> {
" auto y = {};",
compile_expression(&path_layout.y_reference, component)
));
res.push(format!(
" auto width = {};",
compile_expression(&path_layout.width_reference, component)
));
res.push(format!(
" auto height = {};",
compile_expression(&path_layout.height_reference, component)
));
res.push(" sixtyfps::PathLayoutData pl { ".into());
res.push(" &path,".to_owned());
res.push(" {items, std::size(items)},".to_owned());
res.push(" x, y".to_owned());
res.push(" x, y, width, height".to_owned());
res.push(" };".to_owned());
res.push(" sixtyfps::solve_path_layout(&pl);".to_owned());
res.push("}".to_owned());

View file

@ -771,6 +771,8 @@ fn compute_layout(component: &Rc<Component>) -> TokenStream {
let x_pos = compile_expression(&*path_layout.x_reference, &component);
let y_pos = compile_expression(&*path_layout.y_reference, &component);
let width = compile_expression(&*path_layout.width_reference, &component);
let height = compile_expression(&*path_layout.width_reference, &component);
layouts.push(quote! {
solve_path_layout(&PathLayoutData {
@ -778,6 +780,8 @@ fn compute_layout(component: &Rc<Component>) -> TokenStream {
elements: &#path,
x: #x_pos,
y: #y_pos,
width: #width,
height: #height,
});
});
}

View file

@ -53,11 +53,15 @@ pub struct PathLayout {
pub elements: Vec<ElementRc>,
pub x_reference: Box<Expression>,
pub y_reference: Box<Expression>,
pub width_reference: Box<Expression>,
pub height_reference: Box<Expression>,
}
impl ExpressionFieldsVisitor for PathLayout {
fn visit_expressions(&mut self, mut visitor: impl FnMut(&mut Expression)) {
visitor(&mut self.x_reference);
visitor(&mut self.y_reference);
visitor(&mut self.width_reference);
visitor(&mut self.height_reference);
}
}

View file

@ -29,17 +29,16 @@ pub fn lower_layouts(component: &Rc<Component>, diag: &mut Diagnostics) {
false
};
let ref_child = child.clone();
let prop_ref = move |name: &'static str| {
Box::new(Expression::PropertyReference(NamedReference {
element: Rc::downgrade(&ref_child),
name: name.into(),
}))
};
let (x_reference, y_reference) = if is_grid_layout || is_path_layout {
(
Box::new(Expression::PropertyReference(NamedReference {
element: Rc::downgrade(&child),
name: "x".into(),
})),
Box::new(Expression::PropertyReference(NamedReference {
element: Rc::downgrade(&child),
name: "y".into(),
})),
)
(prop_ref("x"), prop_ref("y"))
} else {
(Box::new(Expression::Invalid), Box::new(Expression::Invalid))
};
@ -101,6 +100,8 @@ pub fn lower_layouts(component: &Rc<Component>, diag: &mut Diagnostics) {
path: path_elements_expr,
x_reference,
y_reference,
width_reference: prop_ref("width"),
height_reference: prop_ref("height"),
});
continue;
} else {

View file

@ -280,6 +280,8 @@ impl TypeRegister {
let mut path = BuiltinElement::new("Path");
path.properties.insert("x".to_owned(), Type::Float32);
path.properties.insert("y".to_owned(), Type::Float32);
path.properties.insert("width".to_owned(), Type::Float32);
path.properties.insert("height".to_owned(), Type::Float32);
path.properties.insert("fill_color".to_owned(), Type::Color);
path.properties.insert("stroke_color".to_owned(), Type::Color);
path.properties.insert("stroke_width".to_owned(), Type::Float32);
@ -322,6 +324,8 @@ impl TypeRegister {
let mut path_layout = BuiltinElement::new("PathLayout");
path_layout.properties.insert("x".to_owned(), Type::Float32);
path_layout.properties.insert("y".to_owned(), Type::Float32);
path_layout.properties.insert("width".to_owned(), Type::Float32);
path_layout.properties.insert("height".to_owned(), Type::Float32);
path_layout.properties.insert("commands".to_owned(), Type::String);
path_elements.iter().for_each(|elem| {
path_layout

View file

@ -378,13 +378,38 @@ impl<'a> Iterator for ToLyonPathEventIterator<'a> {
impl<'a> ExactSizeIterator for ToLyonPathEventIterator<'a> {}
struct TransformedLyonPathIterator<EventIt> {
it: EventIt,
transform: lyon::math::Transform,
}
impl<EventIt: Iterator<Item = lyon::path::Event<lyon::math::Point, lyon::math::Point>>> Iterator
for TransformedLyonPathIterator<EventIt>
{
type Item = lyon::path::Event<lyon::math::Point, lyon::math::Point>;
fn next(&mut self) -> Option<Self::Item> {
self.it.next().map(|ev| ev.transformed(&self.transform))
}
fn size_hint(&self) -> (usize, Option<usize>) {
self.it.size_hint()
}
}
impl<EventIt: Iterator<Item = lyon::path::Event<lyon::math::Point, lyon::math::Point>>>
ExactSizeIterator for TransformedLyonPathIterator<EventIt>
{
}
/// LyonPathIterator is a data structure that acts as starting point for iterating
/// through the low-level events of a path. If the path was constructed from said
/// events, then it is a very thin abstraction. If the path was created from higher-level
/// elements, then an intermediate lyon path is required/built.
pub struct LyonPathIterator<'a> {
it: LyonPathIteratorVariant<'a>,
transform: Option<lyon::math::Transform>,
}
enum LyonPathIteratorVariant<'a> {
FromPath(lyon::path::Path),
FromEvents(&'a crate::SharedArray<PathEvent>, &'a crate::SharedArray<Point>),
@ -397,17 +422,42 @@ impl<'a> LyonPathIterator<'a> {
) -> Box<dyn Iterator<Item = lyon::path::Event<lyon::math::Point, lyon::math::Point>> + 'a>
{
match &self.it {
LyonPathIteratorVariant::FromPath(path) => Box::new(path.iter()),
LyonPathIteratorVariant::FromPath(path) => self.apply_transform(path.iter()),
LyonPathIteratorVariant::FromEvents(events, coordinates) => {
Box::new(ToLyonPathEventIterator {
Box::new(self.apply_transform(ToLyonPathEventIterator {
events_it: events.iter(),
coordinates_it: coordinates.iter(),
first: coordinates.first(),
last: coordinates.last(),
})
}))
}
}
}
/// This function changes the iterator to be one that applies a transformation to make the path fit into the specified bounds.
pub fn fitted(mut self, width: f32, height: f32) -> LyonPathIterator<'a> {
if width > 0. || height > 0. {
let br = lyon::algorithms::aabb::bounding_rect(self.iter());
self.transform = Some(lyon::algorithms::fit::fit_rectangle(
&br,
&Rect::from_size(Size::new(width, height)),
lyon::algorithms::fit::FitStyle::Min,
));
}
self
}
fn apply_transform(
&'a self,
event_it: impl Iterator<Item = lyon::path::Event<lyon::math::Point, lyon::math::Point>> + 'a,
) -> Box<dyn Iterator<Item = lyon::path::Event<lyon::math::Point, lyon::math::Point>> + 'a>
{
match self.transform {
Some(transform) => Box::new(TransformedLyonPathIterator { it: event_it, transform }),
None => Box::new(event_it),
}
}
}
#[repr(C)]
@ -443,6 +493,7 @@ impl PathData {
LyonPathIteratorVariant::FromEvents(events, coordinates)
}
},
transform: None,
}
}
@ -554,6 +605,8 @@ pub enum RenderingPrimitive {
Path {
x: f32,
y: f32,
width: f32,
height: f32,
elements: crate::PathData,
fill_color: Color,
stroke_color: Color,

View file

@ -268,6 +268,8 @@ pub use crate::abi::datastructures::TouchAreaVTable;
pub struct Path {
pub x: Property<f32>,
pub y: Property<f32>,
pub width: Property<f32>,
pub height: Property<f32>,
pub elements: Property<PathData>,
pub fill_color: Property<Color>,
pub stroke_color: Property<Color>,
@ -291,6 +293,8 @@ impl Item for Path {
RenderingPrimitive::Path {
x: Self::field_offsets().x.apply_pin(self).get(context),
y: Self::field_offsets().y.apply_pin(self).get(context),
width: Self::field_offsets().width.apply_pin(self).get(context),
height: Self::field_offsets().height.apply_pin(self).get(context),
elements: Self::field_offsets().elements.apply_pin(self).get(context),
fill_color: Self::field_offsets().fill_color.apply_pin(self).get(context),
stroke_color: Self::field_offsets().stroke_color.apply_pin(self).get(context),

View file

@ -163,6 +163,8 @@ pub struct PathLayoutData<'a> {
pub items: Slice<'a, PathLayoutItemData<'a>>,
pub x: Coord,
pub y: Coord,
pub width: Coord,
pub height: Coord,
}
#[repr(C)]
@ -178,7 +180,7 @@ pub extern "C" fn solve_path_layout(data: &PathLayoutData) {
use lyon::geom::*;
use lyon::path::iterator::PathIterator;
let path_iter = data.elements.iter();
let path_iter = data.elements.iter().fitted(data.width, data.height);
let tolerance = 0.01;

View file

@ -557,6 +557,12 @@ unsafe extern "C" fn compute_layout(component: ComponentRefPin, eval_context: &E
let component_type =
&*(component.get_vtable() as *const ComponentVTable as *const ComponentDescription);
let resolve_prop_ref = |prop_ref: &expression_tree::Expression| {
eval::eval_expression(&prop_ref, &component_type, eval_context)
.try_into()
.unwrap_or_default()
};
for it in &component_type.original.layout_constraints.borrow().grids {
use sixtyfps_corelib::layout::*;
@ -604,21 +610,13 @@ unsafe extern "C" fn compute_layout(component: ComponentRefPin, eval_context: &E
.unwrap()
};
let x = eval::eval_expression(&it.x_reference, &component_type, eval_context)
.try_into()
.unwrap_or_default();
let y = eval::eval_expression(&it.y_reference, &component_type, eval_context)
.try_into()
.unwrap_or_default();
solve_grid_layout(&GridLayoutData {
row_constraint: Slice::from(row_constraint.as_slice()),
col_constraint: Slice::from(col_constraint.as_slice()),
width: within_prop("width"),
height: within_prop("height"),
x,
y,
x: resolve_prop_ref(&it.x_reference),
y: resolve_prop_ref(&it.y_reference),
cells: Slice::from(cells.as_slice()),
});
}
@ -643,19 +641,13 @@ unsafe extern "C" fn compute_layout(component: ComponentRefPin, eval_context: &E
let path_elements = eval::convert_path(&it.path, component_type, eval_context);
let x = eval::eval_expression(&it.x_reference, &component_type, eval_context)
.try_into()
.unwrap_or_default();
let y = eval::eval_expression(&it.y_reference, &component_type, eval_context)
.try_into()
.unwrap_or_default();
solve_path_layout(&PathLayoutData {
items: Slice::from(items.as_slice()),
elements: &path_elements,
x,
y,
x: resolve_prop_ref(&it.x_reference),
y: resolve_prop_ref(&it.y_reference),
width: resolve_prop_ref(&it.width_reference),
height: resolve_prop_ref(&it.height_reference),
});
}
}

View file

@ -347,6 +347,8 @@ impl RenderingPrimitivesBuilder for GLRenderingPrimitivesBuilder {
RenderingPrimitive::Path {
x: _,
y: _,
width,
height,
elements,
fill_color,
stroke_color,
@ -354,7 +356,7 @@ impl RenderingPrimitivesBuilder for GLRenderingPrimitivesBuilder {
} => {
let mut primitives = SmallVec::new();
let path_iter = elements.iter();
let path_iter = elements.iter().fitted(*width, *height);
if *fill_color != Color::TRANSPARENT {
primitives.extend(

50
tests/cases/path_fit.60 Normal file
View file

@ -0,0 +1,50 @@
TestCase := Rectangle {
Text {
text: "The path and the items below should both fit into the red rectangle";
color: black;
}
fit_rect := Rectangle {
x: 0;
y: 100;
color: red;
width: 700;
height: 500;
Path {
x: 0;
y: 0;
width: fit_rect.width;
height: fit_rect.height;
commands: "M 100 300 Q 150 50 1100 400 Q 1450 500 750 500 Q 1000 600 950 600 C 325 575 350 450 150 550 Q 0 600 100 800 C 250 850 300 600 550 850 C 800 850 850 650 2000 700 ";
stroke_color: black;
stroke_width: 2;
}
PathLayout {
x: 0;
y: 0;
width: fit_rect.width;
height: fit_rect.height;
commands: "M 100 300 Q 150 50 1100 400 Q 1450 500 750 500 Q 1000 600 950 600 C 325 575 350 450 150 550 Q 0 600 100 800 C 250 850 300 600 550 850 C 800 850 850 650 2000 700 ";
Text {
text: "First item";
color: black;
}
Text {
text: "Second item";
color: black;
}
Text {
text: "Third item";
color: black;
}
}
}
}