Created by: portnov
Addressed problem description
Current Profile node has the following problems:
- Very restricted expression syntax - for example, one cannot even use
sin
orabs
- Restricted variable names - only one-letter names
- Comments can start only in the beginning of the line
- Knots/KnotsNames outputs work only when "from selection" feature is used, nor for any user-provided profile.
- Old-style "a la Basic interpreter" implementation makes it hard to maintain and add new features.
Solution description
This is in fact a re-implementation of most of the Profile mk2 node:
- I wrote a very simplistic parsing framework, based on combinatorical parsing. This makes parsing code much simpler to read and maintain. Framework is in
sverchok.utils.parsec
module. It can be used by other parts of Sverchok if someone wants to implement another kind of DSL for specific node. - Split parsing and interpretation to different classes and stages of work.
- Trailing comments are supported.
- Input sockets handling is implemented more like "mesh expression" node.
- Each segment provides data about it's knots, so Knots/KnotNames outputs work for any profile, not only when "from selection" is used.
- I added some profile examples into
profile_examples
directory, and an import operator to node's N panel (similar to scripted nodes). - As for "from selection" operator... well, for now I can not understand how exactly it works. It is mostly copy-pasted from mk2 implementation.
Parsing framework
This framework implements the approach known as "combinatorial parsing", see https://en.wikipedia.org/wiki/Parser_combinator for starters.
A parser is a function, that takes a string to be parsed, and yields a pair: a parsed object and the rest of the string. It can yield several pairs if the string can be parsed in several ways.
We say that parser succeeds or applies if it yields at least one pair.
We say that parser fails if it does not yield anything.
If a call of parser("var = value")
yields, for example, a pair
(Variable("var"), "= value")
, we say that the parser returned a
Variable("var")
object; it consumed the "var "
string and retained
"= value"
string to be parsed by subsequential parsers.
The common pattern
parser = ...
for value, rest in parser(src):
if ...
yield ..., rest
means: apply parser, then analyze returned value, then yield something and the rest of the string.
A parser combinator is a function that takes one or several parsers, and returns another parser. The returned parser is somehow combined from the parsers provided. Parser combinator may, for example, apply several parsers sequentionally, or try one parser and then try another, or something like that.
This module provides minimalistic set of standard parsers and parser combinators.
It still has poor error detection/recovery. Maybe some day...
Profile description DSL
It did not change much since mk2. The most notable changes are:
- Long variable names are supported
- Large part of Python's expression syntax is supported, including some restricted subset of standard functions
- Expressions which are more complex than just variable name or variable name with negation, have to be enclosed in curly brackets, for example
{a+1}
or{rho * cos(phi)}
. - New commands supported: H/h, V/v, S/s, Q/q, T/t.
- C/c, S/s, Q/q, T/t commands support multiple segments, as in SVG.
- Possibility to set default values for variables ("default" statement) and to introduce temporary variables, expressed via others, for later usage ("let" statement).
- Statements may optionally be separated with semicolons (
;
). For some commands (namely: H/h, V/v) trailing semicolon is required!
In general, I had the following ideas in mind while creating new version of this DSL:
- Compatibility with previous version would be good where possible, but is not exactly necessary, since there will be some migration process anyway.
- Better compatibility with SVG specification - we are still far from it, but nearer then before.
- More developer-friendly: long names, more lenient spaces and comments, more functions available in expressions, temporary variables ("let") and default values.
Our DSL has relatively simple BNF:
<Profile> ::= <Statement> *
<Statement> ::= <Default> | <Assign>
| <MoveTo> | <LineTo> | <CurveTo> | <SmoothLineTo>
| <QuadCurveTo> | <SmoothQuadCurveTo>
| <ArcTo> | <HorLineTo> | <VertLineTo> | "X"
<Default> ::= "default" <Variable> "=" <Value>
<Assign> ::= "let" <Variable> "=" <Value>
<MoveTo> ::= ("M" | "m") <Value> "," <Value>
<LineTo> ::= ...
<CurveTo> ::= ...
<SmoothCurveTo> ::= ...
<QuadCurveTo> ::= ...
<SmoothQuadCurveTo> ::= ...
<ArcTo> ::= ...
<HorLineTo> ::= ("H" | "h") <Value> * ";"
<VertLineTo> ::= ("V" | "v") <Value> * ";"
<Value> ::= "{" <Expression> "}" | <Variable> | <NegatedVariable> | <Const>
<Expression> ::= Standard Python expression
<Variable> ::= Python variable identifier
<NegatedVariable> ::= "-" <Variable>
<Const> ::= Python integer or floating-point literal
Profile Mk2 Backwards Compatibility
In some simple cases, profiles made for mk2 will work with mk3 without modifications. However, there are the following incompatibilities:
- In Mk2, C/c commands had two last additional parameters — "num_verts" and "even_spread". In Mk3, "even_spread" is completely removed, and "num_verts" is now optional, and is specified after "n=", if you need it. The simplest solution: remove last two parameters of each C/c command.
- In Mk2, A/a commands had last additional argument — "num_segments". In mk3, this parameter is optional and specified after "n=", if you need it. The simplest solution: just remove last parameter of each A/a command.
- In Mk2, in "c" (relative curveTo) command, handle2 was calculated relative to handle1, and knot2 was calculated relative to handle2. This contradicts SVG specification, so it was changed in Mk3. In Mk3, as per SVG spec, handle1, handle2 and knot2 are all calculated relative to knot1. There is no such simple fix for this: you have to recalculate your parameters of "c" commands. I hope that there were not so much people that used "c" command, as it is not so easy to use (and was even harder in mk2).
Preflight checklist
Put an x letter in each brackets when you're done this item:
-
Code changes complete. -
Code documentation complete. -
Documentation for users complete (or not required, if user never sees these changes). -
Manual testing done. -
Unit-tests implemented. -
Ready for merge.