diff --git a/core/socket_data.py b/core/socket_data.py index 92cba427277e3e520e6bee636899e6876d4ef4fc..f76d774ca08975084c5b2fe124f847bc1b8dc31b 100644 --- a/core/socket_data.py +++ b/core/socket_data.py @@ -121,6 +121,8 @@ class SvNoDataError(LookupError): def get_message(self): if not self.node and not self.socket: return "SvNoDataError" + elif not self.socket: + return "No data passed for node {}".format(self.node.name) else: return "No data passed into socket `{}'".format(self.socket.name) diff --git a/docs/nodes/generators_extended/profile_mk3.rst b/docs/nodes/generators_extended/profile_mk3.rst new file mode 100644 index 0000000000000000000000000000000000000000..7559dbb2d4588c11aeb087cc008bd10c0d9d7be0 --- /dev/null +++ b/docs/nodes/generators_extended/profile_mk3.rst @@ -0,0 +1,310 @@ +======================= +Profile Parametric Node +======================= + + +**Profile Node** implements a useful subset of the SVG path section commands (see SVG specification_). +Used domain-specific language (DSL) is based on SVG specification, but does not exactly follow it, +by adding some extensions and not supporting some features. + +Profile definition consists of a series of statements (also called commands). + +Statements may optionally be separated by semicolons (`;`). +For some commands (namely: `H`/`h`, `V`/`v`) the trailing semicolon is **required**! + +There are the following statements supported: + +* "default" statement: `default = `. Here `` is any valid python variable identifier, + and `` is a number or expression (see below). This statement declares a default value for the + variable; this value will be used if corresponding input of the node is not connected. +* "let" statement: `let = `. Here `` is any valid python variable identifier, + and `` is a number or expression (see below). This statement declares + a "name binding"; it may be used to calculate some value once and use it in + the following statements several times. Variables defined by "let" statements + will not appear as node inputs. +* Line and curve segment commands - see the table below for details. + +The following segment types are available: + ++---------------+-------+--------------------------------------------------------------------------------+ +| name | cmd | parameters | ++===============+=======+================================================================================+ +| MoveTo | M, m | <2v coordinate> | ++---------------+-------+--------------------------------------------------------------------------------+ +| LineTo | L, l | (<2v coordinate>)+ [z] | ++---------------+-------+--------------------------------------------------------------------------------+ +| HorLineTo | H, h | ()+ ";" | ++---------------+-------+--------------------------------------------------------------------------------+ +| VertLineTo | V, v | ()+ ";" | ++---------------+-------+--------------------------------------------------------------------------------+ +| CurveTo | C, c | (<2v control1> <2v control2> <2v knot2>)+ ["n = " num_verts] [z] | ++---------------+-------+--------------------------------------------------------------------------------+ +| SmoothCurveTo | S, s | (<2v control2> <2v knot2>)+ ["n = " num_verts] [z] | ++---------------+-------+--------------------------------------------------------------------------------+ +| QuadCurveTo | Q, q | (<2v control> <2v knot2>)+ ["n = " num_segments] [z] | ++---------------+-------+--------------------------------------------------------------------------------+ +| SmthQuadCurve | T, t | (<2v knot2>)+ ["n = " num_segments] [z] | ++---------------+-------+--------------------------------------------------------------------------------+ +| ArcTo | A, a | <2v rx,ry> <2v x,y> ["n = " num_verts] [z] | ++---------------+-------+--------------------------------------------------------------------------------+ +| ClosePath | x | | ++---------------+-------+--------------------------------------------------------------------------------+ +| CloseAll | X | | ++---------------+-------+--------------------------------------------------------------------------------+ +| comment | # | anything after # is a comment. | ++---------------+-------+--------------------------------------------------------------------------------+ + +:: + + <> : mandatory field + [] : optional field + 2v : two point vector `a,b` + - no backticks + - a and b can be + - number literals + - lowercase 1-character symbols for variables + (...)+ : this sequence may appear several times + int : means the value will be cast as an int even if you input float + flags generally are 0 or 1. + ["n = " num_verts] : for curve commands, number of subdivisions may be specified. + z : is optional for closing a line + X : as a final command to close the edges (cyclic) [-1, 0] + in addition, if the first and last vertex share coordinate space + the last vertex is dropped and the cycle is made anyway. + # : single line comment prefix + + +Commands starting with capital letters (M, L, C, A) define all coordinates in absolute mode. +Commands starting with lower case letters (m, l, c, a) define all coordinates in relative mode, +i.e. each coordinate is defined with relation to "current pen position". + +Each integer or floating value may be represented as + +* Integer or floating literal (usual python syntax, such as 5 or 7.5) +* Variable name, such as `a` or `b` or `variable_name` +* Negation sign and a variable name, such as `-a` or `-size`. +* Expression enclosed in curly brackets, such as `{a+1}` or `{sin(phi)}` + +ArcTo only take enough parameters to complete one Arc, unlike real SVG command +which take a whole sequence of chained ArcTo commands. The decision +to keep it at one segment type per line is mainly to preserve readability. + +Other curve segments (C/c, S/s, Q/q, T/t) allow to draw several segments with +one command, as well as in SVG; but still, in many cases it is a good idea to +use one segment per command, for readability reasons. + +All curve segment types allow you to specify how many vertices are +used to generate the segment. SVG doesn't let you specify such things, but it +makes sense to allow it for the creation of geometry. + +**Note**: "default" and "let" definitions may use previously defined variables, +or variables expected to be provided as inputs. Just note that these statements +are evaluated in the same order as they follow in the input profile text. + +For examples, see "Examples of usage" section below, or `profile_examples` +directory in Sverchok distribution. + +.. _specification: https://www.w3.org/TR/SVG/paths.html + +Expression syntax +----------------- + +Syntax being used in profile definitions is standard Python's syntax for expressions. +For exact syntax definition, please refer to https://docs.python.org/3/reference/expressions.html. + +In short, you can use usual mathematical operations (`+`, `-`, `*`, `/`, `**` +for power), numbers, variables, parenthesis, and function call, such as +`sin(x)`. + +One difference with Python's syntax is that you can call only restricted number +of Python's functions. Allowed are: + +- Functions from math module: + - acos, acosh, asin, asinh, atan, atan2, + atanh, ceil, copysign, cos, cosh, degrees, + erf, erfc, exp, expm1, fabs, factorial, floor, + fmod, frexp, fsum, gamma, hypot, isfinite, isinf, + isnan, ldexp, lgamma, log, log10, log1p, log2, modf, + pow, radians, sin, sinh, sqrt, tan, tanh, trunc; +- Constants from math module: pi, e; +- Additional functions: abs; +- From mathutlis module: Vector, Matrix; +- Python type conversions: tuple, list. + +This restriction is for security reasons. However, Python's ecosystem does not +guarantee that noone can call some unsafe operations by using some sort of +language-level hacks. So, please be warned that usage of this node with profile +definition obtained from unknown or untrusted source can potentially harm your +system or data. + +Examples of valid expressions are: + +* 1.0 +* x +* {x+1} +* {0.75*X + 0.25*Y} +* {R * sin(phi)} + +Inputs +------ + +Set of inputs for this node depends on expressions used in the profile +definition. Each variable used in profile (except ones declared with "let" +statements) becomes one input. If there are no variables used in profile, then +this node will have no inputs. + +Parameters +---------- + +This node has the following parameters: + +- **Axis**. Available values are **X**, **Y**, **Z**. This parameter specifies + the plane in which the curve will be produced. For example, default value of + **Z** means that all points will belong to XOY plane. +- **File name**. Name of Blender text buffer, containing profile description. +- **Precision**. Number of decimal places used for points coordinates when + generating a profile by **from selection** operator. Default value is 8. This + parameter is only available in the N panel. +- **Curve points count**. Default number of points for curve segment commands. + Default value is 20. This parameter is available only in the N panel. +- **X command threshold**. This parameter provides control over "remove + doubles" functionality of the X command: if the distance between last and + first points is less than this threshold, X command will remove the last + point and connect pre-last point to the first instead. + +Outputs +------- + +This node has the following outputs: + +* **Vertices**. Resulting curve vertices. +* **Edges**. Edges of the resulting curve. +* **Knots**. Knot points of all curve segments (C/c, S/s, Q/q, T/t commands) used in the profile. +* **KnotNames**. Names of all knot points. This output in junction with + **Knots** may be used to display all knots in the 3D view by use of **Viewer + Index** node - this is very useful for debugging of your profile. + +Operators +--------- + +As you know there are three types of curves in Blender - Polylines, Bezier curves and NURBS curves. +This node has one operator button: **from selection**. This operator works only with Bezier curves. +It takes an active Curve object, generates profile description from it and sets up the node +to use this generated profile. You can adjust the profile by editing created Blender's text bufrfer. + +If you want to import other type of curve you have to convert one to Bezier type. +Fortunately it is possible to do in edit mode with button *Set Spline Type* in the *T* panel. +More information about conversion looks `here `_. + +.. image:: https://user-images.githubusercontent.com/28003269/41649336-67dc2d1c-748c-11e8-9989-5b7d8d212b1c.png + +One can also load one of examples, which are provided within Sverchok distribution. For that, +in the **N** panel of Profile node, see "Profile templates" menu. + +Examples +-------- + +If you have experience with SVG paths most of this will be familiar. The +biggest difference is that only the LineTo command accepts many points. It is a +good idea to always start the profile with a M ,. + +:: + + M 0,0 + L a,a b,0 c,0 d,d e,-e + + +the fun bit about this is that all these variables / components can be dynamic + +:: + + M 0,0 + L 0,3 2,3 2,4 + C 2,5 2,5 3,5 n=10 + L 5,5 + C 7,5 7,5 7,3 n=10 + L 7,2 5,0 + X + +or + +:: + + M a,a + L a,b c,b -c,d + C c,e c,e b,e n=g + L e,e + C f,e f,e f,-b n=g + L f,c e,a + X + + +Examples of usage +----------------- + +The node started out as a thought experiment and turned into something quite +useful, you can see how it evolved in the `initial github thread `_ ; +See also `last github thread `_ and examples provided within Sverchok distribution (N panel of the node). + +Example usage: + +.. image:: https://user-images.githubusercontent.com/284644/59453976-8e60f400-8e2a-11e9-8a27-34be6e1fc037.png + +:: + + Q 3,H 6,0 + t 6,0 + t 6,0 + t 0,-6 + t -6,0 + t -6,0 + t -6,0 + t 0,6 + + +.. image:: https://user-images.githubusercontent.com/284644/59548976-f4a35f00-8f6f-11e9-89cd-4c7257e3d753.png + +:: + + C 1,1 2,1 3,0 4,-1 5,-1 6,0 + s 1,2 0,3 -1,5 0,6 + S 1,7 0,6 -1,-1 0,0 n=40 + X + +An example with use of "default" and "let" statements: + +.. image:: https://user-images.githubusercontent.com/284644/59552437-4237c000-8fa0-11e9-91ac-6fd41cae2d73.png + +:: + + default straight_len = 1; + default radius = 0.4; + + let rem = {radius / tan(phi/2)}; + + H straight_len ; + a radius,radius 0 0 1 + {rem * (1 - cos(phi))}, {rem * sin(phi)} + n = 10 + l {- straight_len * cos(phi)}, {straight_len * sin(phi)} + +Gotchas +------- + +The update mechanism doesn't process inputs or anything until the following conditions are satisfied: + +* All inputs have to be connected, except ones that have default values + declared by "default" statements. +* The file field on the Node points to an existing Text File. + + +Keyboard Shortcut to refresh Profile Node +----------------------------------------- + +Updates made to the profile path text file are not propagated automatically to +any nodes that might be reading that file. +To refresh a Profile Node simply hit ``Ctrl+Enter`` In TextEditor while you are +editing the file, or click one of the inputs or output sockets of Profile Node. +There are other ways to refresh (change a value on one of the incoming nodes, +or clicking the sockets of the incoming nodes) + diff --git a/index.md b/index.md index 5bb556f3f7c3f63fd69599efa4121194b90e8352..b309e2cbfd5fc1c5cc2a0481bfe127f2763a6a6a 100644 --- a/index.md +++ b/index.md @@ -36,6 +36,7 @@ Hilbert3dNode HilbertImageNode SvProfileNodeMK2 + SvProfileNodeMK3 SvMeshEvalNode SvGenerativeArtNode SvImageComponentsNode diff --git a/nodes/generators_extended/profile_mk3.py b/nodes/generators_extended/profile_mk3.py new file mode 100644 index 0000000000000000000000000000000000000000..a930eec1030106855a101a94880246fe47c261d8 --- /dev/null +++ b/nodes/generators_extended/profile_mk3.py @@ -0,0 +1,582 @@ +# ##### BEGIN GPL LICENSE BLOCK ##### +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# ##### END GPL LICENSE BLOCK ##### + +import os + +import bpy +from bpy.props import BoolProperty, StringProperty, EnumProperty, FloatProperty, IntProperty + +from sverchok.node_tree import SverchCustomTreeNode +from sverchok.core.socket_data import SvNoDataError +from sverchok.data_structure import updateNode, match_long_repeat +from sverchok.utils.logging import info, debug, warning +from sverchok.utils.sv_update_utils import sv_get_local_path + +from sverchok.utils.modules.profile_mk3.interpreter import Interpreter +from sverchok.utils.modules.profile_mk3.parser import parse_profile + +''' +input like: + + default name = + let name = + + M|m <2v coordinate> + L|l <2v coordinate 1> <2v coordinate 2> <2v coordinate n> [z] + C|c (<2v control1> <2v control2> <2v knot2>)+ ["n = " num_segments] [z] + S|s (<2v control2> <2v knot2>)+ ["n = " num_segments] [z] + Q|q (<2v control> <2v knot2>)+ ["n = " num_segments] [z] + T|t (<2v knot2>)+ ["n = " num_segments] [z] + A|a <2v rx,ry> <2v x,y> ["n = " num_verts] [z] + H|h ... ; + V|v ... ; + X + # + ----- + <> : mandatory field + [] : optional field + (...)+ : something may appear several times + 2v : two point vector `a,b` + - no backticks + - a and b can be number literals or lowercase 1-character symbols for variables + + : means the value will be cast as an int even if you input float + : flags generally are 0 or 1. + z : is optional for closing a line + X : as a final command to close the edges (cyclic) [-1, 0] + in addition, if the first and last vertex share coordinate space + the last vertex is dropped and the cycle is made anyway. + # : single line comment prefix + + Each integer or floating value may be represented as + + * Integer or floating literal (usual python syntax, such as 5 or 7.5) + * Variable name, such as a or b or variable_name + * Negation sign and a variable name, such as `-a` or `-size`. + * Expression enclosed in curly brackets, such as {a+1} or {sin(phi)} + + "default" statement declares a default value for variable: this value will be used + if corresponding input of the node is not connected. + + "let" statement declares a "name binding"; it may be used to calculate some value once + and use it in the following definitions several times. Variable defined by "let" will + not appear as node input! + + "default" and "let" definitions may use previously defined variables, or variables + expected to be provided as inputs. Just note that these statements are evaluated in + the same order as they follow in the input profile text. + + Statements may optionally be separated by semicolons (;). + For some commands (namely: H/h, V/v) the trailing semicolon is *required*! + +''' + +""" +Our DSL has relatively simple BNF: + + ::= * + ::= | + | | | | + | | + | | | | "X" + + ::= "default" "=" + ::= "let" "=" + + ::= ("M" | "m") "," + ::= ... + ::= ... + ::= ... + ::= ... + ::= ... + ::= ... + ::= ("H" | "h") * ";" + ::= ("V" | "v") * ";" + + ::= "{" "}" | | | + ::= Standard Python expression + ::= Python variable identifier + ::= "-" + ::= Python integer or floating-point literal + +""" + +################################# +# "From Selection" Operator +################################# + +# Basically copy-pasted from mk2 +# To understand how it works will take weeks :/ + +class SvPrifilizerMk3(bpy.types.Operator): + """SvPrifilizer""" + bl_idname = "node.sverchok_profilizer_mk3" + bl_label = "SvPrifilizer" + bl_options = {'REGISTER', 'UNDO', 'INTERNAL'} + + nodename = StringProperty(name='nodename') + treename = StringProperty(name='treename') + knotselected = BoolProperty(description='if selected knots than use extended parsing in PN', default=False) + x = BoolProperty(default=True) + y = BoolProperty(default=True) + + + def stringadd(self, x,selected=False): + precision = bpy.data.node_groups[self.treename].nodes[self.nodename].precision + if selected: + if self.x: letterx = '+a' + else: letterx = '' + if self.y: lettery = '+a' + else: lettery = '' + a = '{'+str(round(x[0], precision))+letterx+'}' + ',' + '{'+str(round(x[1], precision))+lettery+'}' + ' ' + self.knotselected = True + else: + a = str(round(x[0], precision)) + ',' + str(round(x[1], precision)) + ' ' + return a + + def curve_points_count(self): + count = bpy.data.node_groups[self.treename].nodes[self.nodename].curve_points_count + return str(count) + + def execute(self, context): + node = bpy.data.node_groups[self.treename].nodes[self.nodename] + precision = node.precision + subdivisions = node.curve_points_count + if not bpy.context.selected_objects: + warning('Pofiler: Select curve!') + self.report({'INFO'}, 'Select CURVE first') + return {'CANCELLED'} + if not bpy.context.selected_objects[0].type == 'CURVE': + warning('Pofiler: NOT a curve selected') + self.report({'INFO'}, 'It is not a curve selected for profiler') + return {'CANCELLED'} + + objs = bpy.context.selected_objects + names = str([o.name for o in objs])[1:-2] + + # test for POLY or NURBS curve types, these are not yet supported + spline_type = objs[0].data.splines[0].type + if spline_type in {'POLY', 'NURBS'}: + msg = 'Pofiler: does not support {0} curve type yet'.format(spline_type) + warning(msg) + self.report({'INFO'}, msg) + return {'CANCELLED'} + + # collect paths + op = [] + clos = [] + for obj in objs: + for spl in obj.data.splines: + op.append(spl.bezier_points) + clos.append(spl.use_cyclic_u) + + # define path to text + values = '# Here is autogenerated values, \n# Please, rename text to avoid data loose.\n' + values += '# Objects are: \n# %a' % (names)+'.\n' + values += '# Object origin should be at 0,0,0. \n' + values += '# Property panel has precision %a \n# and curve subdivision %s.\n\n' % (precision,subdivisions) + # also future output for viewer indices + out_points = [] + out_names = [] + ss = 0 + for ob_points, clo in zip(op,clos): + values += '# Spline %a\n' % (ss) + ss += 1 + # handles preperation + curves_left = [i.handle_left_type for i in ob_points] + curves_right = ['v']+[i.handle_right_type for i in ob_points][:-1] + # first collect C,L values to compile them later per point + types = ['FREE','ALIGNED','AUTO'] + curves = ['C ' if x in types or c in types else 'L ' for x,c in zip(curves_left,curves_right)] + # line for if curve was before line or not + line = False + curve = False + + for i,c in zip(range(len(ob_points)),curves): + co = ob_points[i].co + if not i: + # initial value + values += '\n' + values += 'M ' + co = ob_points[0].co[:] + values += self.stringadd(co,ob_points[0].select_control_point) + values += '\n' + out_points.append(co) + out_names.append(['M.0']) + # pass if first 'M' that was used already upper + continue + + elif c == 'C ': + values += '\n' + values += '#C.'+str(i)+'\n' + values += c + hr = ob_points[i-1].handle_right[:] + hl = ob_points[i].handle_left[:] + # hr[0]hr[1]hl[0]hl[1]co[0]co[1] 20 0 + values += self.stringadd(hr,ob_points[i-1].select_right_handle) + values += self.stringadd(hl,ob_points[i].select_left_handle) + values += self.stringadd(co,ob_points[i].select_control_point) + if curve: + values += '\n' + out_points.append(hr[:]) + out_points.append(hl[:]) + out_points.append(co[:]) + #namecur = ['C.'+str(i)] + out_names.extend([['C.'+str(i)+'h1'],['C.'+str(i)+'h2'],['C.'+str(i)+'k']]) + line = False + curve = True + + elif c == 'L ' and not line: + if curve: + values += '\n' + values += '#L.'+str(i)+'...'+'\n' + values += c + values += self.stringadd(co,ob_points[i].select_control_point) + out_points.append(co[:]) + out_names.append(['L.'+str(i)]) + line = True + curve = False + + elif c == 'L ' and line: + values += self.stringadd(co,ob_points[i].select_control_point) + out_points.append(co[:]) + out_names.append(['L.'+str(i)]) + + if clo: + if ob_points[0].handle_left_type in types or ob_points[-1].handle_right_type in types: + line = False + values += '\n' + values += '#C.'+str(i+1)+'\n' + values += 'C ' + hr = ob_points[-1].handle_right[:] + hl = ob_points[0].handle_left[:] + # hr[0]hr[1]hl[0]hl[1]co[0]co[1] 20 0 + values += self.stringadd(hr,ob_points[-1].select_right_handle) + values += self.stringadd(hl,ob_points[0].select_left_handle) + values += self.stringadd(ob_points[0].co,ob_points[0].select_control_point) + values += ' 0 ' + values += '\n' + out_points.append(hr[:]) + out_points.append(hl[:]) + out_names.extend([['C.'+str(i+1)+'h1'],['C.'+str(i+1)+'h2']]) + # preserving overlapping + #out_points.append(ob_points[0].co[:]) + #out_names.append(['C']) + if not line: + # hacky way till be fixed x for curves not only for lines + values += '# hacky way till be fixed x\n# for curves not only for lines' + values += '\nL ' + self.stringadd(ob_points[0].co,ob_points[0].select_control_point) + values += '\nx \n\n' + else: + values += '\nx \n\n' + + if self.knotselected: + values += '# expression (#+a) added because \n# you selected knots in curve' + self.write_values(self.nodename, values) + #print(values) + node.filename = self.nodename + #print([out_points], [out_names]) + # sharing data to node: + return{'FINISHED'} + + def write_values(self,text,values): + texts = bpy.data.texts.items() + exists = False + for t in texts: + if bpy.data.texts[t[0]].name == text: + exists = True + break + + if not exists: + bpy.data.texts.new(text) + bpy.data.texts[text].clear() + bpy.data.texts[text].write(values) + + +################################# +# Example Files Import +################################# + +sv_path = os.path.dirname(sv_get_local_path()[0]) +profile_template_path = os.path.join(sv_path, 'profile_examples') + +class SvProfileImportMenu(bpy.types.Menu): + bl_label = "Profile templates" + bl_idname = "SvProfileImportMenu" + + def draw(self, context): + if context.active_node: + node = context.active_node + self.path_menu([profile_template_path], "node.sv_profile_import_example") + +class SvProfileImportOperator(bpy.types.Operator): + + bl_idname = "node.sv_profile_import_example" + bl_label = "Profile mk3 load" + bl_options = {'REGISTER', 'UNDO', 'INTERNAL'} + + filepath = bpy.props.StringProperty() + + def execute(self, context): + txt = bpy.data.texts.load(self.filepath) + context.node.filename = os.path.basename(txt.name) + updateNode(context.node, context) + return {'FINISHED'} + +################################# +# Node class +################################# + +class SvProfileNodeMK3(bpy.types.Node, SverchCustomTreeNode): + ''' + Triggers: svg-like 2d profiles + Tooltip: Generate multiple parameteric 2d profiles using SVG like syntax + + SvProfileNode generates one or more profiles / elevation segments using; + assignments, variables, and a string descriptor similar to SVG. + + This node expects simple input, or vectorized input. + - sockets with no input are automatically 0, not None + - The longest input array will be used to extend the shorter ones, using last value repeat. + ''' + + bl_idname = 'SvProfileNodeMK3' + bl_label = 'Profile Parametric Mk3' + bl_icon = 'SYNTAX_ON' + + axis_options = [("X", "X", "", 0), ("Y", "Y", "", 1), ("Z", "Z", "", 2)] + + selected_axis = EnumProperty( + items=axis_options, update=updateNode, name="Type of axis", + description="offers basic axis output vectors X|Y|Z", default="Z") + + def on_update(self, context): + self.adjust_sockets() + updateNode(self, context) + + filename = StringProperty(default="", update=on_update) + + x = BoolProperty(default=True) + y = BoolProperty(default=True) + + precision = IntProperty( + name="Precision", min=0, max=10, default=8, update=updateNode, + description="decimal precision of coordinates when generating profile from selection") + + curve_points_count = IntProperty( + name="Curve points count", min=1, max=100, default=20, update=updateNode, + description="Default number of points on curve segment") + + close_threshold = FloatProperty( + name="X command threshold", min=0, max=1, default=0.0005, precision=6, update=updateNode, + description="If distance between first and last point is less than this, X command will remove the last point") + + def draw_buttons(self, context, layout): + layout.prop(self, 'selected_axis', expand=True) + layout.prop_search(self, 'filename', bpy.data, 'texts', text='', icon='TEXT') + + col = layout.column(align=True) + row = col.row() + do_text = row.operator('node.sverchok_profilizer_mk3', text='from selection') + do_text.nodename = self.name + do_text.treename = self.id_data.name + do_text.x = self.x + do_text.y = self.y + + def draw_buttons_ext(self, context, layout): + self.draw_buttons(context, layout) + + layout.prop(self, "close_threshold") + + layout.label("Profile Generator settings") + layout.prop(self, "precision") + layout.prop(self, "curve_points_count") + row = layout.row(align=True) + row.prop(self, "x",text='x-affect', expand=True) + row.prop(self, "y",text='y-affect', expand=True) + + layout.label("Import Examples") + layout.menu(SvProfileImportMenu.bl_idname) + + def sv_init(self, context): + self.inputs.new('StringsSocket', "a") + + self.outputs.new('VerticesSocket', "Vertices") + self.outputs.new('StringsSocket', "Edges") + self.outputs.new('VerticesSocket', "Knots") + self.outputs.new('StringsSocket', "KnotNames") + + def load_profile(self): + if not self.filename: + return None + internal_file = bpy.data.texts[self.filename] + f = internal_file.as_string() + profile = parse_profile(f) + return profile + + def get_variables(self): + variables = set() + profile = self.load_profile() + if not profile: + return variables + + for statement in profile: + vs = statement.get_variables() + variables.update(vs) + + for statement in profile: + vs = statement.get_hidden_inputs() + variables.difference_update(vs) + + return list(sorted(list(variables))) + + def get_optional_inputs(self, profile): + result = set() + if not profile: + return result + for statement in profile: + vs = statement.get_optional_inputs() + result.update(vs) + return result + + def adjust_sockets(self): + variables = self.get_variables() + #self.debug("adjust_sockets:" + str(variables)) + #self.debug("inputs:" + str(self.inputs.keys())) + for key in self.inputs.keys(): + if key not in variables: + self.debug("Input {} not in variables {}, remove it".format(key, str(variables))) + self.inputs.remove(self.inputs[key]) + for v in variables: + if v not in self.inputs: + self.debug("Variable {} not in inputs {}, add it".format(v, str(self.inputs.keys()))) + self.inputs.new('StringsSocket', v) + + def update(self): + ''' + update analyzes the state of the node and returns if the criteria to start processing + are not met. + ''' + + # keeping the file internal for now. + if not (self.filename in bpy.data.texts): + return + + self.adjust_sockets() + + def get_input(self): + variables = self.get_variables() + result = {} + + for var in variables: + if var in self.inputs and self.inputs[var].is_linked: + result[var] = self.inputs[var].sv_get()[0] + return result + + def extend_out_verts(self, verts): + if self.selected_axis == 'X': + extend = lambda v: (0, v[0], v[1]) + elif self.selected_axis == 'Y': + extend = lambda v: (v[0], 0, v[1]) + else: + extend = lambda v: (v[0], v[1], 0) + return list(map(extend, verts)) + + def process(self): + if not any(o.is_linked for o in self.outputs): + return + + profile = self.load_profile() + optional_inputs = self.get_optional_inputs(profile) + + var_names = self.get_variables() + self.debug("Var_names: %s; optional: %s", var_names, optional_inputs) + inputs = self.get_input() + + result_vertices = [] + result_edges = [] + result_knots = [] + result_names = [] + + if var_names: + input_values = [] + for name in var_names: + try: + input_values.append(inputs[name]) + except KeyError as e: + name = e.args[0] + if name not in optional_inputs: + if name in self.inputs: + raise SvNoDataError(self.inputs[name]) + else: + self.adjust_sockets() + raise SvNoDataError(self.inputs[name]) + else: + input_values.append([None]) + parameters = match_long_repeat(input_values) + else: + parameters = [[[]]] + + input_names = [socket.name for socket in self.inputs if socket.is_linked] + + for values in zip(*parameters): + variables = dict(zip(var_names, values)) + interpreter = Interpreter(self, input_names) + interpreter.interpret(profile, variables) + verts = self.extend_out_verts(interpreter.vertices) + result_vertices.append(verts) + result_edges.append(interpreter.edges) + knots = self.extend_out_verts(interpreter.knots) + result_knots.append(knots) + result_names.append([[name] for name in interpreter.knotnames]) + + self.outputs['Vertices'].sv_set(result_vertices) + self.outputs['Edges'].sv_set(result_edges) + self.outputs['Knots'].sv_set(result_knots) + self.outputs['KnotNames'].sv_set(result_names) + + def storage_set_data(self, storage): + profile = storage['profile'] + filename = storage['params']['filename'] + + bpy.data.texts.new(filename) + bpy.data.texts[filename].clear() + bpy.data.texts[filename].write(profile) + + def storage_get_data(self, storage): + if self.filename and self.filename in bpy.data.texts: + text = bpy.data.texts[self.filename].as_string() + storage['profile'] = text + else: + self.warning("Unknown filename: {}".format(self.filename)) + +classes = [ + SvProfileImportMenu, + SvProfileImportOperator, + SvPrifilizerMk3, + SvProfileNodeMK3 + ] + +def register(): + for name in classes: + bpy.utils.register_class(name) + +def unregister(): + for name in reversed(classes): + bpy.utils.unregister_class(name) + diff --git a/profile_examples/arc_fillet.txt b/profile_examples/arc_fillet.txt new file mode 100644 index 0000000000000000000000000000000000000000..09d3b68d31aeb1678d2a2efad07d99ed96dffc14 --- /dev/null +++ b/profile_examples/arc_fillet.txt @@ -0,0 +1,10 @@ +default straight_len = 1; +default radius = 0.4; + +let rem = {radius / tan(phi/2)}; + +H straight_len ; +a radius,radius 0 0 1 + {rem * (1 - cos(phi))}, {rem * sin(phi)} + n = 10 +l {- straight_len * cos(phi)}, {straight_len * sin(phi)} \ No newline at end of file diff --git a/profile_examples/barrel.txt b/profile_examples/barrel.txt new file mode 100644 index 0000000000000000000000000000000000000000..e907afd99c5dab1931ba3d18a7ed1513eb937c11 --- /dev/null +++ b/profile_examples/barrel.txt @@ -0,0 +1,8 @@ +let handle_x = {width/2}; +let handle_y = {height/8}; + +H {width/2}; +c handle_x,handle_y handle_x,{height-handle_y} 0,height +h -width; +c -handle_x,-handle_y -handle_x,{-height+handle_y} 0,-height +X diff --git a/profile_examples/bevel_line.txt b/profile_examples/bevel_line.txt new file mode 100644 index 0000000000000000000000000000000000000000..c5fb43973aa0fd8921217731fa1846a63e1d1d4c --- /dev/null +++ b/profile_examples/bevel_line.txt @@ -0,0 +1,2 @@ +L d,rad {len-d},rad len,0 + diff --git a/profile_examples/complex_curve.txt b/profile_examples/complex_curve.txt new file mode 100644 index 0000000000000000000000000000000000000000..3efc4591c27ef9f269eb7c5058d7948f8c94aca9 --- /dev/null +++ b/profile_examples/complex_curve.txt @@ -0,0 +1,13 @@ +M -1.0,0.0 +L -1.0,4.0 +l -2,0 +C -5.0,4.0 -5.0,6.0 -3.0,6.0 +l 2,0 +l 0,1 +C -1.0,9.0 1.0,9.0 1.0,7.0 +l 0,-1 +l 2,0 +C 5.0,6.0 5.0,4.0 3.0,4.0 +L 1.0,4.0 1.0,0.0 +X + diff --git a/profile_examples/complex_rectangle.txt b/profile_examples/complex_rectangle.txt new file mode 100644 index 0000000000000000000000000000000000000000..354c62640e65b0a3453dd1da3ec85b535fc96930 --- /dev/null +++ b/profile_examples/complex_rectangle.txt @@ -0,0 +1,24 @@ +default width = 4 +default height = 3 +default radius = 0.5 +default b_width = 2; +default b_height = 0.5 + +let w2 = {width/2 - radius} +let w = {width - 2*radius} +let h = {height - 2*radius} +let dw = {(w - b_width)/2} + +H w2; +q radius,0 radius,radius +v h ; +q 0,radius -radius,radius +h -dw ; +v -b_height ; +h -b_width ; +v b_height ; +h -dw; +q -radius,0 -radius,-radius +v -h; +q 0,-radius radius,-radius +X \ No newline at end of file diff --git a/profile_examples/i-beam.txt b/profile_examples/i-beam.txt new file mode 100644 index 0000000000000000000000000000000000000000..304c8d6527a512a391e9d8cf3bc3f04f7de39096 --- /dev/null +++ b/profile_examples/i-beam.txt @@ -0,0 +1,17 @@ +default bottom_w = 0.6 +default bottom_dx = 0.1 +default bottom_height = 0.3 +default base_height = 1 +default base_w = 0.2 +default top_w = 1.2 +default top_dx = bottom_dx +default top_height = bottom_height +default base_dx = bottom_dx + +H bottom_w ; +l bottom_dx, bottom_height +H base_w ; +l base_dx, base_height +H top_w ; +l top_dx, top_height +H 0 ; \ No newline at end of file diff --git a/profile_examples/pawn.txt b/profile_examples/pawn.txt new file mode 100644 index 0000000000000000000000000000000000000000..11a66c5ee4c092e932830ad2e1406231d58b5f6d --- /dev/null +++ b/profile_examples/pawn.txt @@ -0,0 +1,28 @@ +M 0,0 + +#C.1 +H 1.92243171 ; +#L.2... +L 1.99824715,0.31148303 1.72891283,0.30400151 +#C.4 +C 1.80497479,0.3788166 1.92841959,0.38629809 1.95709872,0.52844679 +#C.5 +C 1.99377036,0.71021062 1.55546045,0.8829388 1.30994821,1.07085621 + +#C.6 +C 1.05147767,1.26869202 0.67236388,1.61017454 0.51690823,2.03073382 + +#C.7 +C 0.35324755,2.47349048 0.28897169,2.84397411 0.31041858,3.18887138 + +#C.8 +C 0.33866882,3.64317679 0.69765115,3.57411408 0.92902559,3.8307848 + +#C.9 +C 1.10597277,4.02707767 0.61380589,3.98830867 0.41398257,3.99238539 + +#C.10 +C 0.67996293,4.1811552 0.78434891,4.38305283 0.78329557,4.66000605 + +#C.11 +C 0.78170198,5.07901382 0.47744593,5.28435373 0.0,5.2843318 diff --git a/profile_examples/quadratic_bubble.txt b/profile_examples/quadratic_bubble.txt new file mode 100644 index 0000000000000000000000000000000000000000..64ea80c085aa1955fff0005b604ee5c875170567 --- /dev/null +++ b/profile_examples/quadratic_bubble.txt @@ -0,0 +1,4 @@ +Q 2,1 4,0 +t 1, -3 -3,-3 +T 0,0 +X \ No newline at end of file diff --git a/profile_examples/quadratic_fillet.txt b/profile_examples/quadratic_fillet.txt new file mode 100644 index 0000000000000000000000000000000000000000..cbd4e0f76be998691b29d1f7d27bb4a3d4b586bf --- /dev/null +++ b/profile_examples/quadratic_fillet.txt @@ -0,0 +1,6 @@ +default straight_len = 1; +default radius = 0.2; + +H straight_len ; +q radius,0 {radius - radius * cos(phi)}, {radius * sin(phi)} +l {- straight_len * cos(phi)}, {straight_len * sin(phi)} \ No newline at end of file diff --git a/profile_examples/quadratic_goggles.txt b/profile_examples/quadratic_goggles.txt new file mode 100644 index 0000000000000000000000000000000000000000..bcc70e222e15bc508623109823b7da9efa3a03d2 --- /dev/null +++ b/profile_examples/quadratic_goggles.txt @@ -0,0 +1,9 @@ +Q {size/2},C size,0 +t size,0 +t size,0 +t 0,-size +t -size,0 +t -size,0 +t -size,0 +t 0,size +X \ No newline at end of file diff --git a/profile_examples/quadratic_sample.txt b/profile_examples/quadratic_sample.txt new file mode 100644 index 0000000000000000000000000000000000000000..a3b2c3f28bce6f56e10c027936da550a3030b083 --- /dev/null +++ b/profile_examples/quadratic_sample.txt @@ -0,0 +1,8 @@ +Q 3,H 6,0 +t 6,0 +t 6,0 +t 0,-6 +t -6,0 +t -6,0 +t -6,0 +t 0,6 \ No newline at end of file diff --git a/profile_examples/rectangle.txt b/profile_examples/rectangle.txt new file mode 100644 index 0000000000000000000000000000000000000000..81b6f42f1af43c8d6f97a2ade1883b3ee76665d2 --- /dev/null +++ b/profile_examples/rectangle.txt @@ -0,0 +1 @@ +l width,0 0,height {-width},0 z diff --git a/profile_examples/rounded_line.txt b/profile_examples/rounded_line.txt new file mode 100644 index 0000000000000000000000000000000000000000..6f066a23b157a6045aae8e4ce25f88afbb6ee69f --- /dev/null +++ b/profile_examples/rounded_line.txt @@ -0,0 +1,10 @@ +let dr = {R - r} +let straight_len = {length - 2*dr} + +v r; +a dr,dr 0 0 0 + dr,dr +h straight_len; +a dr,dr 0 0 0 + dr,-dr +v -r; \ No newline at end of file diff --git a/profile_examples/simple_curve.txt b/profile_examples/simple_curve.txt new file mode 100644 index 0000000000000000000000000000000000000000..62ed2aefe9e2be2235b0d0652c1aea2829c8206b --- /dev/null +++ b/profile_examples/simple_curve.txt @@ -0,0 +1 @@ +C control,control {size-control},control size,0 n=n diff --git a/profile_examples/smooth_arrow.txt b/profile_examples/smooth_arrow.txt new file mode 100644 index 0000000000000000000000000000000000000000..b8a87dba0d47b2d3a1af1cbd0d0ebcd14a025184 --- /dev/null +++ b/profile_examples/smooth_arrow.txt @@ -0,0 +1,4 @@ +C 1,1 2,1 3,0 4,-1 5,-1 6,0 +s 1,2 0,3 -1,5 0,6 +S 1,7 0,6 -1,-1 0,0 n=40 +X \ No newline at end of file diff --git a/profile_examples/smooth_curve.txt b/profile_examples/smooth_curve.txt new file mode 100644 index 0000000000000000000000000000000000000000..3e7c01bc1d6617fc27d2de0dcff7d5d49f642cb5 --- /dev/null +++ b/profile_examples/smooth_curve.txt @@ -0,0 +1,9 @@ +M 0,0 +C 1,1 3,1 4,0 +s 3,-1 4,0 +s 3,1 4,0 +s 1,-3 0,-4 +s -3,-1 -4,0 +s -3,1 -4,0 +s -3,-1 -4,0 + diff --git a/profile_examples/square.txt b/profile_examples/square.txt new file mode 100644 index 0000000000000000000000000000000000000000..06689072acda13cf0bd7a70e53545a2d49111272 --- /dev/null +++ b/profile_examples/square.txt @@ -0,0 +1,3 @@ +M -size, -size +L size,-size size,size -size,size z + diff --git a/profile_examples/square3.txt b/profile_examples/square3.txt new file mode 100644 index 0000000000000000000000000000000000000000..3b1c0abbadc238f1df72db45176d94391490b35c --- /dev/null +++ b/profile_examples/square3.txt @@ -0,0 +1,7 @@ +M -1, 0 +h 1 1 1 ; +v 1 1 1 ; +h -1 -1 -1 ; +v -1 -1 ; +X + diff --git a/profile_examples/symmetric_square.txt b/profile_examples/symmetric_square.txt new file mode 100644 index 0000000000000000000000000000000000000000..86334cbac78522838d68fb77b726b547929fa0cf --- /dev/null +++ b/profile_examples/symmetric_square.txt @@ -0,0 +1,5 @@ +default size = 3; +let half = {size/2}; + +M half,-half +L -half,-half -half,half half,half z \ No newline at end of file diff --git a/profile_examples/zeffi_1.txt b/profile_examples/zeffi_1.txt new file mode 100644 index 0000000000000000000000000000000000000000..0a23406fa7eefec5e1930f694b813f4ef4d0193e --- /dev/null +++ b/profile_examples/zeffi_1.txt @@ -0,0 +1,18 @@ +M -1,0 +L -1,2 +A 1,1 0 0 0 0,3 n=10 +L 2,3 +A 2,2 0 0 1 4,5 +l 0,4 +a 2,3 0 0 0 -2,3 +a 2,3 0 0 1 -2,3 +a 2,2 0 0 1 -2,-2 +l 0,-2 2,-2 -1,0 +c 0,-1 2,1 0,-1 +l -2,0 +c -2,0 0,0 0,-1 +c 0,-1 2,0 0,-1 +a 2,1 0 0 0 -2,-1 +l 0,-2 +X + diff --git a/profile_examples/zeffi_2.txt b/profile_examples/zeffi_2.txt new file mode 100644 index 0000000000000000000000000000000000000000..fc07d7f686b08c4b866eae7221c8b000ed0069ca --- /dev/null +++ b/profile_examples/zeffi_2.txt @@ -0,0 +1,14 @@ +M 0,0 +L 0,3 2,3 2,4 +C 2,5 2,5 3,5 +L 5,5 +C 7,5 7,5 7,3 +L 7,2 6,0 +x +M -1,0 +L -1,3 -2,3 -2,4 +C -2,5 -2,5 -3,5 +L -5,5 +C -7,5 -7,5 -7,3 +L -7,2 -5,0 +x \ No newline at end of file diff --git a/tests/profile_mk3_tests.py b/tests/profile_mk3_tests.py new file mode 100644 index 0000000000000000000000000000000000000000..d03d84ab3b952f6035b7a6e3453e1884775553d3 --- /dev/null +++ b/tests/profile_mk3_tests.py @@ -0,0 +1,103 @@ + +from pathlib import Path +from os.path import basename + +from sverchok.utils.logging import error +from sverchok.utils.testing import * +from sverchok.utils.parsec import parse +from sverchok.utils.modules.profile_mk3.interpreter import * +from sverchok.utils.modules.profile_mk3.parser import * +from sverchok.nodes.generators_extended.profile_mk3 import profile_template_path + +class SimpleTests(SverchokTestCase): + def test_identifier(self): + result = parse(parse_identifier, "z") + self.assertEquals(result, "z") + + def test_expr(self): + string = "{r+1}" + result = parse(parse_value, string) + expected = Expression.from_string(string) + self.assertEquals(result, expected) + + def test_negated_var(self): + string = "-x" + result = parse(parse_value, string) + expected = NegatedVariable("x") + self.assertEquals(result, expected) + +class StatementParseTests(SverchokTestCase): + def test_parse_default(self): + string = "default v1 = 5" + result = parse(parse_statement, string) + expected = Default("v1", Const(5)) + self.assertEquals(result, expected) + + def test_parse_assign(self): + string = "let v2 = v1" + result = parse(parse_statement, string) + expected = Assign("v2", Variable("v1")) + self.assertEquals(result, expected) + + def test_parse_moveto(self): + string = "M x,y" + result = parse(parse_statement, string) + expected = MoveTo(True, Variable("x"), Variable("y")) + self.assertEquals(result, expected) + + def test_parse_lineto(self): + string = "L 1,2 3,4" + result = parse(parse_statement, string) + expected = LineTo(True, [(Const(1), Const(2)), (Const(3), Const(4))], False) + self.assertEquals(result, expected) + + def test_parse_hor_lineto(self): + string = "H 1 2;" + result = parse(parse_statement, string) + expected = HorizontalLineTo(True, [Const(1), Const(2)]) + self.assertEquals(result, expected) + + def test_parse_vert_lineto(self): + string = "V 1 2;" + result = parse(parse_statement, string) + expected = VerticalLineTo(True, [Const(1), Const(2)]) + self.assertEquals(result, expected) + + def test_parse_curveto(self): + string = "C 1,2 3,4 5,6" + result = parse(parse_statement, string) + expected = CurveTo(True, [CurveTo.Segment((Const(1), Const(2)), (Const(3), Const(4)), (Const(5), Const(6)))], None, False) + self.assertEquals(result, expected) + + def test_close_path(self): + string = "x" + result = parse(parse_statement, string) + expected = ClosePath() + self.assertEquals(result, expected) + + def test_close_All(self): + string = "X" + result = parse(parse_statement, string) + expected = CloseAll() + self.assertEquals(result, expected) + + # Other statement types: to be implemented + +class ExamplesParseTests(SverchokTestCase): + """ + This does not actually *import* profile examples into node tree, + it only checks that they parse successfully. + """ + def test_import_examples(self): + examples_set = Path(profile_template_path) + for listed_path in examples_set.iterdir(): + path = str(listed_path) + name = basename(path) + + with self.subTest(file=name): + with open(path, 'r') as f: + info("Checking profile example: %s", name) + profile_text = f.read() + with self.assert_logs_no_errors(): + parse_profile(profile_text) + diff --git a/utils/geom.py b/utils/geom.py index e6de1410abecf9e48e239c4e413a4f8357514d53..c206b36d38ba60bf803b444e5bf86ae42730507f 100644 --- a/utils/geom.py +++ b/utils/geom.py @@ -36,6 +36,7 @@ import bpy import bmesh import mathutils from mathutils import Matrix +from mathutils.geometry import interpolate_bezier from sverchok.utils.sv_bmesh_utils import bmesh_from_pydata from sverchok.utils.sv_bmesh_utils import pydata_from_bmesh @@ -1497,6 +1498,27 @@ def calc_normal(vertices): subnormals = [mathutils.geometry.normal(*triangle) for triangle in triangles] return mathutils.Vector(center(subnormals)) +def interpolate_quadratic_bezier(knot1, handle, knot2, resolution): + """ + Interpolate a quadartic bezier spline segment. + Quadratic bezier curve is defined by two knots (at the beginning and at the + end of segment) and one handle. + + Quadratic bezier curves is a special case of cubic bezier curves, which + are implemented in blender. So this function just converts input data + and calls for interpolate_bezier. + """ + if not isinstance(knot1, mathutils.Vector): + knot1 = mathutils.Vector(knot1) + if not isinstance(knot2, mathutils.Vector): + knot2 = mathutils.Vector(knot2) + if not isinstance(handle, mathutils.Vector): + handle = mathutils.Vector(handle) + + handle1 = knot1 + (2.0/3.0) * (handle - knot1) + handle2 = handle + (1.0/3.0) * (knot2 - handle) + return interpolate_bezier(knot1, handle1, handle2, knot2, resolution) + def multiply_vectors(M, vlist): # (4*4 matrix) X (3*1 vector) diff --git a/utils/modules/profile_mk3/__init__.py b/utils/modules/profile_mk3/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..606a6426a878206fad769036862cc3c2eeff4e50 --- /dev/null +++ b/utils/modules/profile_mk3/__init__.py @@ -0,0 +1,3 @@ +""" +This directory contains auxiliary modules used by profile_mk3 node. +""" diff --git a/utils/modules/profile_mk3/interpreter.py b/utils/modules/profile_mk3/interpreter.py new file mode 100644 index 0000000000000000000000000000000000000000..275359ab4f0e8aeca0366cd38396ebf3af7ca0e1 --- /dev/null +++ b/utils/modules/profile_mk3/interpreter.py @@ -0,0 +1,1021 @@ +# ##### BEGIN GPL LICENSE BLOCK ##### +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# ##### END GPL LICENSE BLOCK ##### + +import ast +from math import * + +from mathutils.geometry import interpolate_bezier +from mathutils import Vector, Matrix + +from sverchok.utils.logging import info, debug, warning +from sverchok.utils.geom import interpolate_quadratic_bezier +from sverchok.utils.sv_curve_utils import Arc + +def make_functions_dict(*functions): + return dict([(function.__name__, function) for function in functions]) + +# Functions +safe_names = make_functions_dict( + # From math module + acos, acosh, asin, asinh, atan, atan2, + atanh, ceil, copysign, cos, cosh, degrees, + erf, erfc, exp, expm1, fabs, factorial, floor, + fmod, frexp, fsum, gamma, hypot, isfinite, isinf, + isnan, ldexp, lgamma, log, log10, log1p, log2, modf, + pow, radians, sin, sinh, sqrt, tan, tanh, trunc, + # Additional functions + abs, + # From mathutlis module + Vector, Matrix, + # Python type conversions + tuple, list, str + ) +# Constants +safe_names['e'] = e +safe_names['pi'] = pi + +########################################## +# Expression classes +########################################## + +class Expression(object): + def __init__(self, expr, string): + self.expr = expr + self.string = string + + def __repr__(self): + return "Expr({})".format(self.string) + + def __eq__(self, other): + # Proper comparasion of ast.Expression would be too complex to implement + # (it is not implemented in the ast module). + return isinstance(other, Expression) and self.string == other.string + + @classmethod + def from_string(cls, string): + try: + string = string[1:][:-1] + expr = ast.parse(string, mode='eval') + return Expression(expr, string) + except Exception as e: + print(e) + print(string) + return None + + def eval_(self, variables): + env = dict() + env.update(safe_names) + env.update(variables) + env["__builtins__"] = {} + return eval(compile(self.expr, "", 'eval'), env) + + def get_variables(self): + result = {node.id for node in ast.walk(self.expr) if isinstance(node, ast.Name)} + return result.difference(safe_names.keys()) + +class Const(Expression): + def __init__(self, value): + self.value = value + + @classmethod + def from_string(cls, string): + try: + return Const( float(string) ) + except ValueError: + return None + + def __repr__(self): + return "Const({})".format(self.value) + + def __eq__(self, other): + return isinstance(other,Const) and self.value == other.value + + def eval_(self, variables): + return self.value + + def get_variables(self): + return set() + +class Variable(Expression): + def __init__(self, name): + self.name = name + + @classmethod + def from_string(cls, string): + return Variable(string) + + def __repr__(self): + return "Variable({})".format(self.name) + + def __eq__(self, other): + return isinstance(other, Variable) and self.name == other.name + + def eval_(self, variables): + value = variables.get(self.name, None) + if value is not None: + return value + else: + raise SyntaxError("Unknown variable: " + self.name) + + def get_variables(self): + return set([self.name]) + +# In general, this does not have very much sense: +# instead of -a one can write {-a}, then it will +# be parsed as Expression and will work fine. +# This is mostly implemented for compatibility +# with older Profile node syntax. +class NegatedVariable(Variable): + @classmethod + def from_string(cls, string): + return NegatedVariable(string) + + def eval_(self, variables): + value = variables.get(self.name, None) + if value is not None: + return -value + else: + raise SyntaxError("Unknown variable: " + self.name) + + def __repr__(self): + return "NegatedVariable({})".format(self.name) + + def __eq__(self, other): + return isinstance(other, NegatedVariable) and self.name == other.name + +############################################ +# Statement classes +# Classes for AST of our DSL +############################################ + +# These classes are responsible for interpretation of specific DSL statements. +# Each of these classes does the following: +# +# * Stores statement parameters (for example, MoveTo stores x and y). +# * defines get_variables() method, which should return a set of all +# variables used by all expressions in the statement. +# * defines interpret() method, which should calculate all vertices and +# edges according to statement parameters, and pass them to the interpreter. + +class Statement(object): + + def get_variables(self): + return set() + + def get_hidden_inputs(self): + return set() + + def get_optional_inputs(self): + return set() + +class MoveTo(Statement): + def __init__(self, is_abs, x, y): + self.is_abs = is_abs + self.x = x + self.y = y + + def __repr__(self): + letter = "M" if self.is_abs else "m" + return "{} {} {}".format(letter, self.x, self.y) + + def __eq__(self, other): + return isinstance(other, MoveTo) and \ + self.is_abs == other.is_abs and \ + self.x == other.x and \ + self.y == other.y + + def get_variables(self): + variables = set() + variables.update(self.x.get_variables()) + variables.update(self.y.get_variables()) + return variables + + def interpret(self, interpreter, variables): + interpreter.assert_not_closed() + interpreter.start_new_segment() + pos = interpreter.calc_vertex(self.is_abs, self.x, self.y, variables) + interpreter.position = pos + interpreter.new_knot("M.#", *pos) + interpreter.has_last_vertex = False + +class LineTo(Statement): + def __init__(self, is_abs, pairs, close): + self.is_abs = is_abs + self.pairs = pairs + self.close = close + + def get_variables(self): + variables = set() + for x, y in self.pairs: + variables.update(x.get_variables()) + variables.update(y.get_variables()) + return variables + + def __repr__(self): + letter = "L" if self.is_abs else "l" + return "{} {} {}".format(letter, self.pairs, self.close) + + def __eq__(self, other): + return isinstance(other, LineTo) and \ + self.is_abs == other.is_abs and \ + self.pairs == other.pairs and \ + self.close == other.close + + def interpret(self, interpreter, variables): + interpreter.assert_not_closed() + interpreter.start_new_segment() + v0 = interpreter.position + if interpreter.has_last_vertex: + v0_index = interpreter.get_last_vertex() + else: + v0_index = interpreter.new_vertex(*v0) + + for i, (x_expr, y_expr) in enumerate(self.pairs): + v1 = interpreter.calc_vertex(self.is_abs, x_expr, y_expr, variables) + interpreter.position = v1 + v1_index = interpreter.new_vertex(*v1) + interpreter.new_edge(v0_index, v1_index) + interpreter.new_knot("L#.{}".format(i), *v1) + v0_index = v1_index + + if self.close: + interpreter.new_edge(v1_index, interpreter.segment_start_index) + + interpreter.has_last_vertex = True + +class HorizontalLineTo(Statement): + def __init__(self, is_abs, xs): + self.is_abs = is_abs + self.xs = xs + + def get_variables(self): + variables = set() + for x in self.xs: + variables.update(x.get_variables()) + return variables + + def __repr__(self): + letter = "H" if self.is_abs else "h" + return "{} {}".format(letter, self.xs) + + def __eq__(self, other): + return isinstance(other, HorizontalLineTo) and \ + self.is_abs == other.is_abs and \ + self.xs == other.xs + + def interpret(self, interpreter, variables): + interpreter.assert_not_closed() + interpreter.start_new_segment() + v0 = interpreter.position + if interpreter.has_last_vertex: + v0_index = interpreter.get_last_vertex() + else: + v0_index = interpreter.new_vertex(*v0) + + for i, x_expr in enumerate(self.xs): + x0,y0 = interpreter.position + x = interpreter.eval_(x_expr, variables) + if not self.is_abs: + x = x0 + x + v1 = (x, y0) + interpreter.position = v1 + v1_index = interpreter.new_vertex(*v1) + interpreter.new_edge(v0_index, v1_index) + interpreter.new_knot("H#.{}".format(i), *v1) + v0_index = v1_index + + interpreter.has_last_vertex = True + +class VerticalLineTo(Statement): + def __init__(self, is_abs, ys): + self.is_abs = is_abs + self.ys = ys + + def get_variables(self): + variables = set() + for y in self.ys: + variables.update(y.get_variables()) + return variables + + def __repr__(self): + letter = "V" if self.is_abs else "v" + return "{} {}".format(letter, self.ys) + + def __eq__(self, other): + return isinstance(other, VerticalLineTo) and \ + self.is_abs == other.is_abs and \ + self.ys == other.ys + + def interpret(self, interpreter, variables): + interpreter.assert_not_closed() + interpreter.start_new_segment() + v0 = interpreter.position + if interpreter.has_last_vertex: + v0_index = interpreter.get_last_vertex() + else: + v0_index = interpreter.new_vertex(*v0) + + for i, y_expr in enumerate(self.ys): + x0,y0 = interpreter.position + y = interpreter.eval_(y_expr, variables) + if not self.is_abs: + y = y0 + y + v1 = (x0, y) + interpreter.position = v1 + v1_index = interpreter.new_vertex(*v1) + interpreter.new_edge(v0_index, v1_index) + interpreter.new_knot("V#.{}".format(i), *v1) + v0_index = v1_index + + interpreter.has_last_vertex = True + +class CurveTo(Statement): + class Segment(object): + def __init__(self, control1, control2, knot2): + self.control1 = control1 + self.control2 = control2 + self.knot2 = knot2 + + def __repr__(self): + return "{} {} {}".format(self.control1, self.control2, self.knot2) + + def __eq__(self, other): + return self.control1 == other.control1 and \ + self.control2 == other.control2 and \ + self.knot2 == other.knot2 + + def __init__(self, is_abs, segments, num_segments, close): + self.is_abs = is_abs + self.segments = segments + self.num_segments = num_segments + self.close = close + + def get_variables(self): + variables = set() + for segment in self.segments: + variables.update(segment.control1[0].get_variables()) + variables.update(segment.control1[1].get_variables()) + variables.update(segment.control2[0].get_variables()) + variables.update(segment.control2[1].get_variables()) + variables.update(segment.knot2[0].get_variables()) + variables.update(segment.knot2[1].get_variables()) + if self.num_segments: + variables.update(self.num_segments.get_variables()) + return variables + + def __repr__(self): + letter = "C" if self.is_abs else "c" + segments = " ".join(str(segment) for segment in self.segments) + return "{} {} n={} {}".format(letter, segments, self.num_segments, self.close) + + def __eq__(self, other): + return isinstance(other, CurveTo) and \ + self.is_abs == other.is_abs and \ + self.segments == other.segments and \ + self.num_segments == other.num_segments and \ + self.close == other.close + + def interpret(self, interpreter, variables): + vec = lambda v: Vector((v[0], v[1], 0)) + + interpreter.assert_not_closed() + interpreter.start_new_segment() + + v0 = interpreter.position + if interpreter.has_last_vertex: + v0_index = interpreter.get_last_vertex() + else: + v0_index = interpreter.new_vertex(*v0) + + knot1 = None + for i, segment in enumerate(self.segments): + # For first segment, knot1 is initial pen position; + # for the following, knot1 is knot2 of previous segment. + if knot1 is None: + knot1 = interpreter.position + else: + knot1 = knot2 + + handle1 = interpreter.calc_vertex(self.is_abs, segment.control1[0], segment.control1[1], variables) + + # In Profile mk2, for "c" handle2 was calculated relative to handle1, + # and knot2 was calculated relative to handle2. + # But in SVG specification, + # >> ... *At the end of the command*, the new current point becomes + # >> the final (x,y) coordinate pair used in the polybézier. + # This is also behaivour of browsers. + + #interpreter.position = handle1 + handle2 = interpreter.calc_vertex(self.is_abs, segment.control2[0], segment.control2[1], variables) + #interpreter.position = handle2 + knot2 = interpreter.calc_vertex(self.is_abs, segment.knot2[0], segment.knot2[1], variables) + # Judging by the behaivour of Inkscape and Firefox, by "end of command" + # SVG spec means "end of segment". + interpreter.position = knot2 + + if self.num_segments is not None: + r = interpreter.eval_(self.num_segments, variables) + else: + r = interpreter.dflt_num_verts + + points = interpolate_bezier(vec(knot1), vec(handle1), vec(handle2), vec(knot2), r) + + interpreter.new_knot("C#.{}.h1".format(i), *handle1) + interpreter.new_knot("C#.{}.h2".format(i), *handle2) + interpreter.new_knot("C#.{}.k".format(i), *knot2) + + interpreter.prev_bezier_knot = handle2 + + for point in points[1:]: + v1_index = interpreter.new_vertex(point.x, point.y) + interpreter.new_edge(v0_index, v1_index) + v0_index = v1_index + + if self.close: + interpreter.new_edge(v1_index, interpreter.segment_start_index) + + interpreter.has_last_vertex = True + +class SmoothCurveTo(Statement): + class Segment(object): + def __init__(self, control2, knot2): + self.control2 = control2 + self.knot2 = knot2 + + def __repr__(self): + return "{} {}".format(self.control2, self.knot2) + + def __eq__(self, other): + return self.control2 == other.control2 and \ + self.knot2 == other.knot2 + + def __init__(self, is_abs, segments, num_segments, close): + self.is_abs = is_abs + self.segments = segments + self.num_segments = num_segments + self.close = close + + def get_variables(self): + variables = set() + for segment in self.segments: + variables.update(segment.control2[0].get_variables()) + variables.update(segment.control2[1].get_variables()) + variables.update(segment.knot2[0].get_variables()) + variables.update(segment.knot2[1].get_variables()) + if self.num_segments: + variables.update(self.num_segments.get_variables()) + return variables + + def __repr__(self): + letter = "S" if self.is_abs else "s" + segments = " ".join(str(segment) for segment in self.segments) + return "{} {} n={} {}".format(letter, segments, self.num_segments, self.close) + + def __eq__(self, other): + return isinstance(other, SmoothCurveTo) and \ + self.is_abs == other.is_abs and \ + self.segments == other.segments and \ + self.num_segments == other.num_segments and \ + self.close == other.close + + def interpret(self, interpreter, variables): + vec = lambda v: Vector((v[0], v[1], 0)) + + interpreter.assert_not_closed() + interpreter.start_new_segment() + + v0 = interpreter.position + if interpreter.has_last_vertex: + v0_index = interpreter.get_last_vertex() + else: + v0_index = interpreter.new_vertex(*v0) + + knot1 = None + for i, segment in enumerate(self.segments): + # For first segment, knot1 is initial pen position; + # for the following, knot1 is knot2 of previous segment. + if knot1 is None: + knot1 = interpreter.position + else: + knot1 = knot2 + + if interpreter.prev_bezier_knot is None: + # If there is no previous command or if the previous command was + # not an C, c, S or s, assume the first control point is coincident + # with the current point. + handle1 = knot1 + else: + # The first control point is assumed to be the reflection of the + # second control point on the previous command relative to the + # current point. + prev_knot_x, prev_knot_y = interpreter.prev_bezier_knot + x0, y0 = knot1 + dx, dy = x0 - prev_knot_x, y0 - prev_knot_y + handle1 = x0 + dx, y0 + dy + + # I assume that handle2 should be relative to knot1, not to handle1. + # interpreter.position = handle1 + handle2 = interpreter.calc_vertex(self.is_abs, segment.control2[0], segment.control2[1], variables) + # interpreter.position = handle2 + knot2 = interpreter.calc_vertex(self.is_abs, segment.knot2[0], segment.knot2[1], variables) + interpreter.position = knot2 + + if self.num_segments is not None: + r = interpreter.eval_(self.num_segments, variables) + else: + r = interpreter.dflt_num_verts + + points = interpolate_bezier(vec(knot1), vec(handle1), vec(handle2), vec(knot2), r) + + interpreter.new_knot("S#.{}.h1".format(i), *handle1) + interpreter.new_knot("S#.{}.h2".format(i), *handle2) + interpreter.new_knot("S#.{}.k".format(i), *knot2) + + interpreter.prev_bezier_knot = handle2 + + for point in points[1:]: + v1_index = interpreter.new_vertex(point.x, point.y) + interpreter.new_edge(v0_index, v1_index) + v0_index = v1_index + + if self.close: + interpreter.new_edge(v1_index, interpreter.segment_start_index) + + interpreter.has_last_vertex = True + +class QuadraticCurveTo(Statement): + class Segment(object): + def __init__(self, control, knot2): + self.control = control + self.knot2 = knot2 + + def __repr__(self): + return "{} {}".format(self.control, self.knot2) + + def __eq__(self, other): + return self.control == other.control and \ + self.knot2 == other.knot2 + + def __init__(self, is_abs, segments, num_segments, close): + self.is_abs = is_abs + self.segments = segments + self.num_segments = num_segments + self.close = close + + def get_variables(self): + variables = set() + for segment in self.segments: + variables.update(segment.control[0].get_variables()) + variables.update(segment.control[1].get_variables()) + variables.update(segment.knot2[0].get_variables()) + variables.update(segment.knot2[1].get_variables()) + if self.num_segments: + variables.update(self.num_segments.get_variables()) + return variables + + def __repr__(self): + letter = "Q" if self.is_abs else "q" + segments = " ".join(str(segment) for segment in self.segments) + return "{} {} n={} {}".format(letter, segments, self.num_segments, self.close) + + def __eq__(self, other): + return isinstance(other, QuadraticCurveTo) and \ + self.is_abs == other.is_abs and \ + self.segments == other.segments and \ + self.num_segments == other.num_segments and \ + self.close == other.close + + def interpret(self, interpreter, variables): + vec = lambda v: Vector((v[0], v[1], 0)) + + interpreter.assert_not_closed() + interpreter.start_new_segment() + + v0 = interpreter.position + if interpreter.has_last_vertex: + v0_index = interpreter.get_last_vertex() + else: + v0_index = interpreter.new_vertex(*v0) + + knot1 = None + for i, segment in enumerate(self.segments): + # For first segment, knot1 is initial pen position; + # for the following, knot1 is knot2 of previous segment. + if knot1 is None: + knot1 = interpreter.position + else: + knot1 = knot2 + + handle = interpreter.calc_vertex(self.is_abs, segment.control[0], segment.control[1], variables) + knot2 = interpreter.calc_vertex(self.is_abs, segment.knot2[0], segment.knot2[1], variables) + interpreter.position = knot2 + + if self.num_segments is not None: + r = interpreter.eval_(self.num_segments, variables) + else: + r = interpreter.dflt_num_verts + + points = interpolate_quadratic_bezier(vec(knot1), vec(handle), vec(knot2), r) + + interpreter.new_knot("Q#.{}.h".format(i), *handle) + interpreter.new_knot("Q#.{}.k".format(i), *knot2) + + interpreter.prev_quad_bezier_knot = handle + + for point in points[1:]: + v1_index = interpreter.new_vertex(point.x, point.y) + interpreter.new_edge(v0_index, v1_index) + v0_index = v1_index + + if self.close: + interpreter.new_edge(v1_index, interpreter.segment_start_index) + + interpreter.has_last_vertex = True + +class SmoothQuadraticCurveTo(Statement): + class Segment(object): + def __init__(self, knot2): + self.knot2 = knot2 + + def __repr__(self): + return str(self.knot2) + + def __eq__(self, other): + return self.knot2 == other.knot2 + + def __init__(self, is_abs, segments, num_segments, close): + self.is_abs = is_abs + self.segments = segments + self.num_segments = num_segments + self.close = close + + def get_variables(self): + variables = set() + for segment in self.segments: + variables.update(segment.knot2[0].get_variables()) + variables.update(segment.knot2[1].get_variables()) + if self.num_segments: + variables.update(self.num_segments.get_variables()) + return variables + + def __repr__(self): + letter = "T" if self.is_abs else "t" + segments = " ".join(str(segment) for segment in self.segments) + return "{} {} n={} {}".format(letter, segments, self.num_segments, self.close) + + def __eq__(self, other): + return isinstance(other, SmoothQuadraticCurveTo) and \ + self.is_abs == other.is_abs and \ + self.segments == other.segments and \ + self.num_segments == other.num_segments and \ + self.close == other.close + + def interpret(self, interpreter, variables): + vec = lambda v: Vector((v[0], v[1], 0)) + + interpreter.assert_not_closed() + interpreter.start_new_segment() + + v0 = interpreter.position + if interpreter.has_last_vertex: + v0_index = interpreter.get_last_vertex() + else: + v0_index = interpreter.new_vertex(*v0) + + knot1 = None + for i, segment in enumerate(self.segments): + # For first segment, knot1 is initial pen position; + # for the following, knot1 is knot2 of previous segment. + if knot1 is None: + knot1 = interpreter.position + else: + knot1 = knot2 + + if interpreter.prev_quad_bezier_knot is None: + # If there is no previous command or if the previous command was + # not a Q, q, T or t, assume the control point is coincident with + # the current point. + handle = knot1 + else: + # The first control point is assumed to be the reflection of the + # second control point on the previous command relative to the + # current point. + prev_knot_x, prev_knot_y = interpreter.prev_quad_bezier_knot + x0, y0 = knot1 + dx, dy = x0 - prev_knot_x, y0 - prev_knot_y + handle = x0 + dx, y0 + dy + + knot2 = interpreter.calc_vertex(self.is_abs, segment.knot2[0], segment.knot2[1], variables) + interpreter.position = knot2 + + if self.num_segments is not None: + r = interpreter.eval_(self.num_segments, variables) + else: + r = interpreter.dflt_num_verts + + points = interpolate_quadratic_bezier(vec(knot1), vec(handle), vec(knot2), r) + + interpreter.new_knot("T#.{}.h".format(i), *handle) + interpreter.new_knot("T#.{}.k".format(i), *knot2) + + interpreter.prev_quad_bezier_knot = handle + + for point in points[1:]: + v1_index = interpreter.new_vertex(point.x, point.y) + interpreter.new_edge(v0_index, v1_index) + v0_index = v1_index + + if self.close: + interpreter.new_edge(v1_index, interpreter.segment_start_index) + + interpreter.has_last_vertex = True + +class ArcTo(Statement): + def __init__(self, is_abs, radii, rot, flag1, flag2, end, num_verts, close): + self.is_abs = is_abs + self.radii = radii + self.rot = rot + self.flag1 = flag1 + self.flag2 = flag2 + self.end = end + self.num_verts = num_verts + self.close = close + + def get_variables(self): + variables = set() + variables.update(self.radii[0].get_variables()) + variables.update(self.radii[1].get_variables()) + variables.update(self.rot.get_variables()) + variables.update(self.flag1.get_variables()) + variables.update(self.flag2.get_variables()) + variables.update(self.end[0].get_variables()) + variables.update(self.end[1].get_variables()) + if self.num_verts: + variables.update(self.num_verts.get_variables()) + return variables + + def __repr__(self): + letter = "A" if self.is_abs else "a" + return "{} {} {} {} {} {} n={} {}".format(letter, self.radii, self.rot, self.flag1, self.flag2, self.end, self.num_verts, self.close) + + def __eq__(self, other): + return isinstance(other, ArcTo) and \ + self.is_abs == other.is_abs and \ + self.radii == other.radii and \ + self.rot == other.rot and \ + self.flag1 == other.flag1 and \ + self.flag2 == other.flag2 and \ + self.end == other.end and \ + self.num_verts == other.num_verts and \ + self.close == other.close + + def interpret(self, interpreter, variables): + interpreter.assert_not_closed() + interpreter.start_new_segment() + + v0 = interpreter.position + if interpreter.has_last_vertex: + v0_index = interpreter.get_last_vertex() + else: + v0_index = interpreter.new_vertex(*v0) + + start = complex(*v0) + rad_x_expr, rad_y_expr = self.radii + rad_x = interpreter.eval_(rad_x_expr, variables) + rad_y = interpreter.eval_(rad_y_expr, variables) + radius = complex(rad_x, rad_y) + xaxis_rot = interpreter.eval_(self.rot, variables) + flag1 = interpreter.eval_(self.flag1, variables) + flag2 = interpreter.eval_(self.flag2, variables) + + # numverts, requires -1 else it means segments (21 verts is 20 segments). + if self.num_verts is not None: + num_verts = interpreter.eval_(self.num_verts, variables) + else: + num_verts = interpreter.dflt_num_verts + num_verts -= 1 + + end = interpreter.calc_vertex(self.is_abs, self.end[0], self.end[1], variables) + end = complex(*end) + + arc = Arc(start, radius, xaxis_rot, flag1, flag2, end) + + theta = 1/num_verts + for i in range(num_verts+1): + v1 = x, y = arc.point(theta * i) + v1_index = interpreter.new_vertex(x, y) + interpreter.new_edge(v0_index, v1_index) + v0_index = v1_index + + interpreter.position = v1 + interpreter.new_knot("A.#", *v1) + if self.close: + interpreter.new_edge(v1_index, interpreter.segment_start_index) + + interpreter.has_last_vertex = True + +class CloseAll(Statement): + def __init__(self): + pass + + def __repr__(self): + return "X" + + def __eq__(self, other): + return isinstance(other, CloseAll) + + def get_variables(self): + return set() + + def interpret(self, interpreter, variables): + interpreter.assert_not_closed() + if not interpreter.has_last_vertex: + info("X statement: no current point, do nothing") + return + + v0 = interpreter.vertices[0] + v1 = interpreter.vertices[-1] + + distance = (Vector(v0) - Vector(v1)).length + + if distance < interpreter.close_threshold: + interpreter.pop_last_vertex() + + v1_index = interpreter.get_last_vertex() + interpreter.new_edge(v1_index, 0) + interpreter.closed = True + +class ClosePath(Statement): + def __init__(self): + pass + + def __repr__(self): + return "x" + + def __eq__(self, other): + return isinstance(other, ClosePath) + + def get_variables(self): + return set() + + def interpret(self, interpreter, variables): + interpreter.assert_not_closed() + if not interpreter.has_last_vertex: + info("X statement: no current point, do nothing") + return + + v0 = interpreter.vertices[interpreter.close_first_index] + v1 = interpreter.vertices[-1] + + distance = (Vector(v0) - Vector(v1)).length + + if distance < interpreter.close_threshold: + interpreter.pop_last_vertex() + + v1_index = interpreter.get_last_vertex() + interpreter.new_edge(v1_index, interpreter.close_first_index) + interpreter.close_first_index = interpreter.next_vertex_index + +class Default(Statement): + def __init__(self, name, value): + self.name = name + self.value = value + + def __repr__(self): + return "default {} = {}".format(self.name, self.value) + + def __eq__(self, other): + return isinstance(other, Default) and \ + self.name == other.name and \ + self.value == other.value + + def get_variables(self): + return self.value.get_variables() + + def get_optional_inputs(self): + return set([self.name]) + + def interpret(self, interpreter, variables): + if self.name in interpreter.defaults: + raise Exception("Value for the `{}' variable has been already assigned!".format(self.name)) + if self.name not in interpreter.input_names: + value = interpreter.eval_(self.value, variables) + interpreter.defaults[self.name] = value + +class Assign(Default): + def __repr__(self): + return "let {} = {}".format(self.name, self.value) + + def __eq__(self, other): + return isinstance(other, Assign) and \ + self.name == other.name and \ + self.value == other.value + + def get_hidden_inputs(self): + return set([self.name]) + +################################# +# DSL Interpreter +################################# + +# This class does the following: +# +# * Stores the "drawing" state, such as "current pen position" +# * Provides API for Statement classes to add vertices, edges to the current +# drawing +# * Contains the interpret() method, which runs the whole interpretation process. + +class Interpreter(object): + def __init__(self, node, input_names): + self.position = (0, 0) + self.next_vertex_index = 0 + self.segment_start_index = 0 + self.segment_number = 0 + self.has_last_vertex = False + self.closed = False + self.close_first_index = 0 + self.prev_bezier_knot = None + self.prev_quad_bezier_knot = None + self.vertices = [] + self.edges = [] + self.knots = [] + self.knotnames = [] + self.dflt_num_verts = node.curve_points_count + self.close_threshold = node.close_threshold + self.defaults = dict() + self.input_names = input_names + + def assert_not_closed(self): + if self.closed: + raise Exception("Path was already closed, will not process any further directives!") + + def relative(self, x, y): + x0, y0 = self.position + return x0+x, y0+y + + def calc_vertex(self, is_abs, x_expr, y_expr, variables): + x = self.eval_(x_expr, variables) + y = self.eval_(y_expr, variables) + if is_abs: + return x,y + else: + return self.relative(x,y) + + def new_vertex(self, x, y): + index = self.next_vertex_index + vertex = (x, y) + self.vertices.append(vertex) + self.next_vertex_index += 1 + return index + + def new_edge(self, v1, v2): + self.edges.append((v1, v2)) + + def new_knot(self, name, x, y): + self.knots.append((x, y)) + name = name.replace("#", str(self.segment_number)) + self.knotnames.append(name) + + def start_new_segment(self): + self.segment_start_index = self.next_vertex_index + self.segment_number += 1 + + def get_last_vertex(self): + return self.next_vertex_index - 1 + + def pop_last_vertex(self): + self.vertices.pop() + self.next_vertex_index -= 1 + is_not_last = lambda e: e[0] != self.next_vertex_index and e[1] != self.next_vertex_index + self.edges = list(filter(is_not_last, self.edges)) + + def eval_(self, expr, variables): + variables_ = self.defaults.copy() + for name in variables: + value = variables[name] + if value is not None: + variables_[name] = value + return expr.eval_(variables_) + + def interpret(self, profile, variables): + if not profile: + return + for statement in profile: + debug("Interpret: %s", statement) + statement.interpret(self, variables) + diff --git a/utils/modules/profile_mk3/parser.py b/utils/modules/profile_mk3/parser.py new file mode 100644 index 0000000000000000000000000000000000000000..702e9445cb480954dca08510f666092bbd19a245 --- /dev/null +++ b/utils/modules/profile_mk3/parser.py @@ -0,0 +1,246 @@ +# ##### BEGIN GPL LICENSE BLOCK ##### +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# ##### END GPL LICENSE BLOCK ##### + +import re + +from sverchok.utils.parsec import * +from sverchok.utils.logging import info, debug, warning +from sverchok.utils.modules.profile_mk3.interpreter import * + +######################################### +# DSL parsing +######################################### + +# Compare these definitions with BNF definition at the top of profile_mk3.py. + +expr_regex = re.compile(r"({[^}]+})\s*", re.DOTALL) + +def parse_expr(src): + for string, rest in parse_regexp(expr_regex)(src): + expr = Expression.from_string(string) + if expr is not None: + yield expr, rest + +identifier_regexp = re.compile(r"[a-zA-Z_][a-zA-Z0-9_]*") + +parse_semicolon = parse_word(";") + +def parse_identifier(src): + for (name, _), rest in sequence(parse_regexp(identifier_regexp), parse_whitespace)(src): + yield name, rest + +def parse_negated_variable(src): + for (_, name, _), rest in sequence(parse_word("-"), parse_regexp(identifier_regexp), parse_whitespace)(src): + yield NegatedVariable(name), rest + +def parse_value(src): + for smth, rest in one_of(parse_number, parse_identifier, parse_negated_variable, parse_expr)(src): + if isinstance(smth, (int, float)): + yield Const(smth), rest + elif isinstance(smth, str): + yield Variable(smth), rest + else: + yield smth, rest + +def parse_pair(src): + parser = sequence(parse_value, parse_word(","), parse_value) + for (x, _, y), rest in parser(src): + yield (x,y), rest + +def parse_letter(absolute, relative): + def parser(src): + for smth, rest in one_of(parse_word(absolute), parse_word(relative))(src): + is_abs = smth == absolute + yield is_abs, rest + return parser + +def parse_MoveTo(src): + parser = sequence(parse_letter("M", "m"), parse_pair, optional(parse_semicolon)) + for (is_abs, (x, y), _), rest in parser(src): + yield MoveTo(is_abs, x, y), rest + +def parse_LineTo(src): + parser = sequence( + parse_letter("L", "l"), + many(parse_pair), + optional(parse_word("z")), + optional(parse_semicolon)) + for (is_abs, pairs, z, _), rest in parser(src): + yield LineTo(is_abs, pairs, z is not None), rest + +def parse_parameter(name): + def parser(src): + for (_, _, value), rest in sequence(parse_word(name), parse_word("="), parse_value)(src): + yield value, rest + return parser + +def parse_CurveTo(src): + + def parse_segment(src): + parser = sequence(parse_pair, parse_pair, parse_pair) + for (control1, control2, knot2), rest in parser(src): + yield CurveTo.Segment(control1, control2, knot2), rest + + parser = sequence( + parse_letter("C", "c"), + many(parse_segment), + optional(parse_parameter("n")), + optional(parse_word("z")), + optional(parse_semicolon) + ) + for (is_abs, segments, num_segments, z, _), rest in parser(src): + yield CurveTo(is_abs, segments, num_segments, z is not None), rest + +def parse_SmoothCurveTo(src): + + def parse_segment(src): + parser = sequence(parse_pair, parse_pair) + for (control2, knot2), rest in parser(src): + yield SmoothCurveTo.Segment(control2, knot2), rest + + parser = sequence( + parse_letter("S", "s"), + many(parse_segment), + optional(parse_parameter("n")), + optional(parse_word("z")), + optional(parse_semicolon) + ) + for (is_abs, segments, num_segments, z, _), rest in parser(src): + yield SmoothCurveTo(is_abs, segments, num_segments, z is not None), rest + +def parse_QuadCurveTo(src): + + def parse_segment(src): + parser = sequence(parse_pair, parse_pair) + for (control, knot2), rest in parser(src): + yield QuadraticCurveTo.Segment(control, knot2), rest + + parser = sequence( + parse_letter("Q", "q"), + many(parse_segment), + optional(parse_parameter("n")), + optional(parse_word("z")), + optional(parse_semicolon) + ) + for (is_abs, segments, num_segments, z, _), rest in parser(src): + yield QuadraticCurveTo(is_abs, segments, num_segments, z is not None), rest + +def parse_SmoothQuadCurveTo(src): + + def parse_segment(src): + for knot2, rest in parse_pair(src): + yield SmoothQuadraticCurveTo.Segment(knot2), rest + + parser = sequence( + parse_letter("T", "t"), + many(parse_segment), + optional(parse_parameter("n")), + optional(parse_word("z")), + optional(parse_semicolon) + ) + for (is_abs, knot2, num_segments, z, _), rest in parser(src): + yield SmoothQuadraticCurveTo(is_abs, knot2, num_segments, z is not None), rest + +def parse_ArcTo(src): + parser = sequence( + parse_letter("A", "a"), + parse_pair, + parse_value, + parse_value, + parse_value, + parse_pair, + optional(parse_parameter("n")), + optional(parse_word("z")), + optional(parse_semicolon) + ) + for (is_abs, radii, rot, flag1, flag2, end, num_verts, z, _), rest in parser(src): + yield ArcTo(is_abs, radii, rot, flag1, flag2, end, num_verts, z is not None), rest + +def parse_HorLineTo(src): + # NB: H/h command MUST end with semicolon, otherwise we will not be able to + # understand where it ends, i.e. does the following letter begin a new statement + # or is it just next X value denoted by variable. + parser = sequence(parse_letter("H", "h"), many(parse_value), parse_semicolon) + for (is_abs, xs, _), rest in parser(src): + yield HorizontalLineTo(is_abs, xs), rest + +def parse_VertLineTo(src): + # NB: V/v command MUST end with semicolon, otherwise we will not be able to + # understand where it ends, i.e. does the following letter begin a new statement + # or is it just next X value denoted by variable. + parser = sequence(parse_letter("V", "v"), many(parse_value), parse_semicolon) + for (is_abs, ys, _), rest in parser(src): + yield VerticalLineTo(is_abs, ys), rest + +parse_CloseAll = parse_word("X", CloseAll()) +parse_ClosePath = parse_word("x", ClosePath()) + +def parse_Default(src): + parser = sequence( + parse_word("default"), + parse_identifier, + parse_word("="), + parse_value, + optional(parse_semicolon) + ) + for (_, name, _, value, _), rest in parser(src): + yield Default(name, value), rest + +def parse_Assign(src): + parser = sequence( + parse_word("let"), + parse_identifier, + parse_word("="), + parse_value, + optional(parse_semicolon) + ) + for (_, name, _, value, _), rest in parser(src): + yield Assign(name, value), rest + +parse_statement = one_of( + parse_Default, + parse_Assign, + parse_MoveTo, + parse_LineTo, + parse_HorLineTo, + parse_VertLineTo, + parse_CurveTo, + parse_SmoothCurveTo, + parse_QuadCurveTo, + parse_SmoothQuadCurveTo, + parse_ArcTo, + parse_ClosePath, + parse_CloseAll + ) + +parse_definition = many(parse_statement) + +def parse_profile(src): + # Strip comments + # (hope noone uses # in expressions) + cleaned = "" + for line in src.split("\n"): + comment_idx = line.find('#') + if comment_idx != -1: + line = line[:comment_idx] + cleaned = cleaned + " " + line + + profile = parse(parse_definition, cleaned) + debug(profile) + return profile + diff --git a/utils/parsec.py b/utils/parsec.py new file mode 100644 index 0000000000000000000000000000000000000000..2ab4ef51726771186bc2a704eac17ed3a9eb284b --- /dev/null +++ b/utils/parsec.py @@ -0,0 +1,220 @@ +# ##### BEGIN GPL LICENSE BLOCK ##### +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# ##### END GPL LICENSE BLOCK ##### + +""" +This module contains a very simplistic framework for parsing DSLs. +Such DSL is used, for example, in the Profile (mk3) node. + +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... + +""" + +import re +from itertools import chain + +number_regex = re.compile(r"(-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?)\s*(.*)", re.DOTALL) +string_regex = re.compile(r"('(?:[^\\']|\\['\\/bfnrt]|\\u[0-9a-fA-F]{4})*?')\s*(.*)", re.DOTALL) +whitespace_regex = re.compile(r"[ \n\r\t]*") + +def return_(src): + """ + Trivial parser, that parses an empty tuple and retains + the input string untouched. + """ + yield (), src + +def sequence(*funcs): + """ + Parser combinator, that applies several parsers in sequence. + For example, if parser_a parses "A" and parser_b parses "B", + then sequence(parser_a, parser_b) parses "AB". + This corresponds to + + in BNF notation. + """ + if len(funcs) == 0: + return return_ + + def parser(src): + for arg1, src in funcs[0](src): + for others, src in sequence(*funcs[1:])(src): + yield (arg1,) + others, src + + return parser + +def one_of(*funcs): + """ + Parser combinator, that tries to apply one of several parsers. + For example, if parser_foo parses "foo" and parser_bar parses "bar", + then one_of(parser_foo, parser_bar) will parse either "foo" or "bar". + This corresponds to + | + in BNF notation. + """ + def parser(src): + generators = [func(src) for func in funcs] + for match in chain(*generators): + yield match + return + return parser + +def many(func): + """ + Parser combinator, that applies one parser as many times as it + could be applied. For example, if parser_a parses "A", then + many(parser_a) parses "A", "AA", "AAA" and so on. + This corresponds to + * + in BNF notation. + """ + def parser(src): + for (value, values), rest in sequence(func, parser)(src): + yield [value] + values, rest + return + + for value, rest in func(src): + yield [value], rest + return parser + +def optional(func): + """ + Parser combinator, that tries to apply specified parser, and + returns None if it can not. + This corresponds to + | "" + in BNF notation. + """ + def parser(src): + met = False + for value, rest in func(src): + yield value, rest + met = True + if not met: + yield None, src + return parser + +def parse_number(src): + """ + Parse an integer or floating-point number. + """ + match = number_regex.match(src) + if match is not None: + number, rest = match.groups() + yield eval(number), rest + +def parse_word(word, value=None): + """ + Parse the specified word and return specified value. + It skips any whitespace that follows the word. + + For example, parse_word("word")("word 123") parses + "word" and retains "123" as the rest of string. + """ + l = len(word) + if value is None: + value = word + + def result(src): + if src.startswith(word): + yield value, src[l:].lstrip() + + result.__name__ = "parse_%s" % word + return result + +def parse_regexp(regexp): + if isinstance(regexp, str): + regexp = re.compile(regexp) + + def parser(src): + match = regexp.match(src) + if match is not None: + try: + result = match.group(1) + except IndexError: + result = match.group(0) + n = match.end() + rest = src[n:] + yield result, rest + + return parser + +parse_whitespace = parse_regexp(whitespace_regex) + +def parse_string(src): + """ + Parse string literal in single quotes. + """ + match = string_regex.match(src) + if match is not None: + string, rest = match.groups() + yield string, rest + +def parse(func, s): + """ + Interface function: apply the parser to the string + and return parser's return value. + If the parser can not parse the string, or can parse + it in more than one way - this function will raise an + exception. + Also it will raise an exception if something remained + in the input string after applying the parser. + """ + s = s.strip() + match = list(func(s)) + if len(match) != 1: + raise ValueError("invalid syntax: " + str(match)) + result, rest = match[0] + if rest.strip(): + raise ValueError("leftover: " + rest) + return result + diff --git a/utils/text_editor_plugins.py b/utils/text_editor_plugins.py index 97937fb69c792e7b52236e86ec12c4dff1b0f92c..2a36a8983a528b6b139bbfce3127680620732908 100644 --- a/utils/text_editor_plugins.py +++ b/utils/text_editor_plugins.py @@ -308,7 +308,7 @@ class SvNodeRefreshFromTextEditor(bpy.types.Operator): node_types = set([ 'SvScriptNode', 'SvScriptNodeMK2', 'SvScriptNodeLite', 'SvProfileNode', 'SvTextInNode', 'SvGenerativeArtNode', - 'SvRxNodeScript', 'SvProfileNodeMK2']) + 'SvRxNodeScript', 'SvProfileNodeMK2', 'SvProfileNodeMK3']) for ng in ngs: nodes = [n for n in ng.nodes if n.bl_idname in node_types]