Make it possible to use a string binding for Path commands

This was requested a few days ago in the chat and might also present an alternative for #754
This commit is contained in:
Simon Hausmann 2022-01-04 14:16:01 +01:00 committed by Simon Hausmann
parent ac031a45f8
commit 36e07b8801
11 changed files with 139 additions and 111 deletions

View file

@ -37,6 +37,11 @@ public:
{
}
PathData(const SharedString &commands)
: data(cbindgen_private::types::PathData::Commands(commands))
{
}
friend bool operator==(const PathData &a, const PathData &b)
{
if (a.data.tag != b.data.tag)
@ -46,6 +51,8 @@ public:
return a.data.elements._0 == b.data.elements._0;
case cbindgen_private::types::PathData::Tag::Events:
return a.data.events._0 == b.data.events._0 && b.data.events._0 == b.data.events._0;
case cbindgen_private::types::PathData::Tag::Commands:
return a.data.commands._0 == b.data.commands._0;
case cbindgen_private::types::PathData::Tag::None:
return true;
}

View file

@ -277,7 +277,7 @@ accordingly.
SVG is a popular file format for defining scalable graphics, which are often composed of paths. In SVG
paths are composed using [commands](https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/d#path_commands),
which in turn are written in a string literal. In `.60` the path commands are provided to the `commands`
which in turn are written in a string. In `.60` the path commands are provided to the `commands`
property. The following example renders a shape consists of an arc and a rectangle, composed of `line-to`,
`move-to` and `arc` commands:
@ -293,7 +293,7 @@ Example := Path {
The commands are provided in a property:
* **`commands`** (*string): A string literal providing the commands according to the SVG path specification.
* **`commands`** (*string): A string providing the commands according to the SVG path specification.
#### Path Using SVG Path Elements

View file

@ -675,6 +675,7 @@ impl Expression {
Path::Events(events, coordinates) => {
events.iter().chain(coordinates.iter()).for_each(visitor);
}
Path::Commands(commands) => visitor(commands),
},
Expression::StoreLocalVariable { value, .. } => visitor(&**value),
Expression::ReadLocalVariable { .. } => {}
@ -767,6 +768,7 @@ impl Expression {
Path::Events(events, coordinates) => {
events.iter_mut().chain(coordinates.iter_mut()).for_each(visitor);
}
Path::Commands(commands) => visitor(commands),
},
Expression::StoreLocalVariable { value, .. } => visitor(&mut **value),
Expression::ReadLocalVariable { .. } => {}
@ -835,15 +837,13 @@ impl Expression {
Expression::UnaryOp { sub, .. } => sub.is_constant(),
Expression::Array { values, .. } => values.iter().all(Expression::is_constant),
Expression::Struct { values, .. } => values.iter().all(|(_, v)| v.is_constant()),
Expression::PathData(data) => {
if let Path::Elements(elements) = data {
elements
Expression::PathData(data) => match data {
Path::Elements(elements) => elements
.iter()
.all(|element| element.bindings.values().all(|v| v.borrow().is_constant()))
} else {
true
}
}
.all(|element| element.bindings.values().all(|v| v.borrow().is_constant())),
Path::Events(_, _) => true,
Path::Commands(_) => false,
},
Expression::StoreLocalVariable { .. } => false,
// we should somehow find out if this is constant or not
Expression::ReadLocalVariable { .. } => false,
@ -1210,6 +1210,7 @@ pub struct BindingAnalysis {
pub enum Path {
Elements(Vec<PathElement>),
Events(Vec<Expression>, Vec<Expression>),
Commands(Box<Expression>), // expr must evaluate to string
}
#[derive(Debug, Clone)]

View file

@ -1340,6 +1340,9 @@ fn compile_expression(expr: &Expression, ctx: &EvaluationContext) -> TokenStream
};
quote!(sixtyfps::re_exports::PathData::Events(sixtyfps::re_exports::SharedVector::<_>::from_slice(&#events), sixtyfps::re_exports::SharedVector::<_>::from_slice(&#points)))
}
(Type::String, Type::PathData) => {
quote!(sixtyfps::re_exports::PathData::Commands(#f))
}
_ => f,
}
}

View file

@ -962,6 +962,10 @@ fn compile_path(path: &crate::expression_tree::Path, ctx: &ExpressionContext) ->
to: Type::PathData,
}
}
crate::expression_tree::Path::Commands(commands) => llr_Expression::Cast {
from: lower_expression(&commands, &ctx).into(),
to: Type::PathData,
},
}
}

View file

@ -45,7 +45,7 @@ pub fn compile_paths(
let mut elem = elem_.borrow_mut();
let path_data = if let Some(commands_expr) =
let path_data_binding = if let Some(commands_expr) =
elem.bindings.remove("commands").map(RefCell::into_inner)
{
if let Some(path_child) = elem.children.iter().find(|child| {
@ -60,21 +60,66 @@ pub fn compile_paths(
return;
}
let commands = match &commands_expr.expression {
Expression::StringLiteral(commands) => commands,
_ => {
diag.push_error(
"The commands property only accepts string literals".into(),
&*elem,
);
match &commands_expr.expression {
Expression::StringLiteral(commands) => {
if let Some(binding) = compile_path_from_string_literal(commands) {
binding
} else {
diag.push_error("Error parsing SVG commands".into(), &commands_expr);
return;
}
}
expr @ _ if expr.ty() == Type::String => Expression::PathData(
crate::expression_tree::Path::Commands(Box::new(commands_expr.expression)),
)
.into(),
_ => {
diag.push_error("The commands property only accepts strings".into(), &*elem);
return;
}
}
} else {
let new_children = Vec::with_capacity(elem.children.len());
let old_children = std::mem::replace(&mut elem.children, new_children);
let mut path_data = Vec::new();
for child in old_children {
let element_name =
&child.borrow().base_type.as_builtin().native_class.class_name.clone();
if let Some(path_element) = element_types.get(element_name) {
let element_type = match path_element {
Type::Builtin(b) => b.clone(),
_ => panic!(
"Incorrect type registry -- expected built-in type for path element {}",
element_name
),
};
if child.borrow().repeated.is_some() {
diag.push_error(
"Path elements are not supported with `for`-`in` syntax, yet (https://github.com/sixtyfpsui/sixtyfps/issues/754)".into(),
&*child.borrow(),
);
} else {
let bindings = std::mem::take(&mut child.borrow_mut().bindings);
path_data.push(PathElement { element_type, bindings });
}
} else {
elem.children.push(child);
}
}
Expression::PathData(crate::expression_tree::Path::Elements(path_data)).into()
};
elem.bindings.insert("elements".into(), RefCell::new(path_data_binding));
});
}
fn compile_path_from_string_literal(commands: &str) -> Option<BindingExpression> {
let path_builder = lyon_path::Path::builder().with_svg();
let path = lyon_svg::path_utils::build_path(path_builder, commands);
match path {
Ok(path) => {
lyon_svg::path_utils::build_path(path_builder, commands).ok().map(|path| {
let event_enum = crate::typeregister::PATH_EVENT_ENUM.with(|e| e.clone());
let point_type = Type::Struct {
fields: IntoIterator::into_iter([
@ -117,15 +162,9 @@ pub fn compile_paths(
}
lyon_path::Event::End { first: _, last: _, close } => {
if close {
event_enum
.clone()
.try_value_from_string("end_closed")
.unwrap()
event_enum.clone().try_value_from_string("end_closed").unwrap()
} else {
event_enum
.clone()
.try_value_from_string("end_open")
.unwrap()
event_enum.clone().try_value_from_string("end_open").unwrap()
}
}
})
@ -137,62 +176,13 @@ pub fn compile_paths(
.map(|point| Expression::Struct {
ty: point_type.clone(),
values: IntoIterator::into_iter([
(
"x".to_owned(),
Expression::NumberLiteral(point.x as _, Unit::None),
),
(
"y".to_owned(),
Expression::NumberLiteral(point.y as _, Unit::None),
),
("x".to_owned(), Expression::NumberLiteral(point.x as _, Unit::None)),
("y".to_owned(), Expression::NumberLiteral(point.y as _, Unit::None)),
])
.collect(),
})
.collect();
Path::Events(events, points)
}
Err(_) => {
diag.push_error("Error parsing SVG commands".into(), &commands_expr);
return;
}
}
} else {
let new_children = Vec::with_capacity(elem.children.len());
let old_children = std::mem::replace(&mut elem.children, new_children);
let mut path_data = Vec::new();
for child in old_children {
let element_name =
&child.borrow().base_type.as_builtin().native_class.class_name.clone();
if let Some(path_element) = element_types.get(element_name) {
let element_type = match path_element {
Type::Builtin(b) => b.clone(),
_ => panic!(
"Incorrect type registry -- expected built-in type for path element {}",
element_name
),
};
if child.borrow().repeated.is_some() {
diag.push_error(
"Path elements are not supported with `for`-`in` syntax, yet (https://github.com/sixtyfpsui/sixtyfps/issues/754)".into(),
&*child.borrow(),
);
} else {
let bindings = std::mem::take(&mut child.borrow_mut().bindings);
path_data.push(PathElement { element_type, bindings });
}
} else {
elem.children.push(child);
}
}
crate::expression_tree::Path::Elements(path_data)
};
elem.bindings
.insert("elements".into(), RefCell::new(Expression::PathData(path_data).into()));
});
Expression::PathData(Path::Events(events, points)).into()
})
}

View file

@ -12,7 +12,6 @@ TestCase := Rectangle {
property<string> cmds: "M 350 300 L 550 300 ";
Path {
// ^error{The commands property only accepts string literals}
commands: cmds;
}

View file

@ -22,7 +22,7 @@ rtti = []
# Expose C ABI
ffi = []
# Use the standard library
std = ["euclid/std", "once_cell/std", "scoped-tls-hkt", "lyon_path", "lyon_algorithms", "lyon_geom", "instant"]
std = ["euclid/std", "once_cell/std", "scoped-tls-hkt", "lyon_path", "lyon_algorithms", "lyon_geom", "lyon_svg", "instant"]
default = ["std"]
@ -40,6 +40,7 @@ sixtyfps-corelib-macros = { version = "=0.2.0", path = "../corelib_macros" }
lyon_path = { version = "0.17.3" , optional = true }
lyon_algorithms = { version = "0.17.1" , optional = true }
lyon_geom = { version = "0.17.0", optional = true }
lyon_svg = { version = "0.17.2", optional = true }
euclid = { version = "0.22.1", default-features = false }
once_cell = { version = "1.5", default-features = false }
instant = { version = "0.1", features = [ "now" ], optional = true }

View file

@ -292,6 +292,8 @@ pub enum PathData {
/// The Events variant describes the path as a series of low-level events and
/// associated coordinates.
Events(crate::SharedVector<PathEvent>, crate::SharedVector<Point>),
/// The Commands variant describes the path as a series of SVG encoded path commands.
Commands(crate::SharedString),
}
impl Default for PathData {
@ -312,6 +314,13 @@ impl PathData {
PathData::Events(events, coordinates) => {
LyonPathIteratorVariant::FromEvents(events, coordinates)
}
PathData::Commands(commands) => {
let path_builder = lyon_path::Path::builder().with_svg();
LyonPathIteratorVariant::FromPath(
lyon_svg::path_utils::build_path(path_builder, &commands)
.unwrap_or_default(),
)
}
},
transform: Default::default(),
}

View file

@ -1101,6 +1101,13 @@ pub fn convert_path(path: &ExprPath, local_context: &mut EvalLocalContext) -> Pa
ExprPath::Events(events, points) => {
convert_from_lyon_path(events.iter(), points.iter(), local_context)
}
ExprPath::Commands(commands) => {
if let Value::String(commands) = eval_expression(commands, local_context) {
PathData::Commands(commands)
} else {
panic!("binding to path commands does not evalutate to string");
}
}
}
}

View file

@ -156,10 +156,17 @@ Hello := Rectangle {
}
Path {
commands: "M 100 300 Q 150 50 250 150 C 250 300 300 300 300 450 A 50 50 0 1 0 450 450 L 550 300";
commands: ta.pressed ? root.arrow_down_commands : root.funky-shape-commands;
x: 100phx;
y: 500phx;
stroke: black;
stroke_width: 2px;
}
ta := TouchArea {
}
property <string> arrow_down_commands: "M21.8,311.1l84.2-82.1c15.7-15.2,41-15.2,56.7,0l341.1,304.1l333.7-297.5c15.5-15.2,41-15.2,56.6,0l84.3,82.1c15.6,15.2,15.6,40,0,55.2L531.7,771c-15.7,15.3-41,15.3-56.7,0l-6.9-6.7L21.8,366.3C6.1,351,6.1,326.3,21.8,311.1z";
property <string> funky_shape_commands: "M 100 300 Q 150 50 250 150 C 250 300 300 300 300 450 A 50 50 0 1 0 450 450 L 550 300";
}