From 4b9f8b4f25ef8a9b7bb3702e8b084302c081e24b Mon Sep 17 00:00:00 2001 From: Ilya Portnov Date: Wed, 12 Jun 2019 20:25:12 +0500 Subject: [PATCH 01/47] Support ctrl+enter. --- utils/text_editor_plugins.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/utils/text_editor_plugins.py b/utils/text_editor_plugins.py index f27b3c253..845a8064c 100644 --- a/utils/text_editor_plugins.py +++ b/utils/text_editor_plugins.py @@ -70,7 +70,7 @@ class SvNodeRefreshFromTextEditor(bpy.types.Operator): node_types = set([ 'SvScriptNode', 'SvScriptNodeMK2', 'SvScriptNodeLite', 'SvProfileNode', 'SvTextInNode', 'SvGenerativeArtNode', - 'SvRxNodeScript', 'SvProfileNodeMK2', 'SvVDExperimental']) + 'SvRxNodeScript', 'SvProfileNodeMK2', 'SvVDExperimental', 'SvProfileNodeMK3']) for ng in ngs: nodes = [n for n in ng.nodes if n.bl_idname in node_types] -- GitLab From 5e467b8a3607c2a18941d3b0dee222b6d805113c Mon Sep 17 00:00:00 2001 From: Ilya Portnov Date: Wed, 12 Jun 2019 21:17:09 +0500 Subject: [PATCH 02/47] Support importing standard examples. --- nodes/generators_extended/profile_mk3.py | 970 +++++++++++++++++++++++ 1 file changed, 970 insertions(+) create mode 100644 nodes/generators_extended/profile_mk3.py diff --git a/nodes/generators_extended/profile_mk3.py b/nodes/generators_extended/profile_mk3.py new file mode 100644 index 000000000..2bc1af532 --- /dev/null +++ b/nodes/generators_extended/profile_mk3.py @@ -0,0 +1,970 @@ +# ##### 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 itertools import chain +import ast +from math import * +import os + +import bpy +from bpy.props import BoolProperty, StringProperty, EnumProperty, FloatVectorProperty, IntProperty +from mathutils.geometry import interpolate_bezier +from mathutils import Vector + +from sverchok.node_tree import SverchCustomTreeNode +from sverchok.core.socket_data import SvNoDataError +from sverchok.data_structure import fullList, updateNode, dataCorrect, match_long_repeat +from sverchok.utils.parsec import * +from sverchok.utils.logging import info, debug, warning +from sverchok.utils.sv_curve_utils import Arc +from sverchok.utils.sv_update_utils import sv_get_local_path + +''' +input like: + + M|m <2v coordinate> + L|l <2v coordinate 1> <2v coordinate 2> <2v coordinate n> [z] + C|c <2v control1> <2v control2> <2v knot2> [z] + A|a <2v rx,ry> <2v x,y> [z] + X + # + ----- + <> : mandatory field + [] : optional field + 2v : two point vector `a,b` + - no space between , + - 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 + * Expression enclosed in curly brackets, such as {a+1} or {sin(phi)} + +''' + +""" +Our DSL has relatively simple BNF: + + ::= * + ::= | | | | "X" + + ::= ("M" | "m") "," + ::= ... + ::= ... + ::= ... + + ::= "'" "'" | | + ::= Standard Python expression + ::= Python variable identifier + ::= Python integer or floating-point literal + +""" + +########################################## +# Expression classes +########################################## + +class Expression(object): + def __init__(self, expr, string): + self.expr = expr + self.string = string + + safe_names = dict(sin=sin, cos=cos, pi=pi, sqrt=sqrt) + + def __repr__(self): + return "Expr({})".format(self.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(self.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(self.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 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 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]) + +############################################ +# Statement classes +# Classes for AST of our DSL +############################################ + +class Statement(object): + pass + +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 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 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 CurveTo(Statement): + def __init__(self, is_abs, control1, control2, knot2, num_segments, even_spread, close): + self.is_abs = is_abs + self.control1 = control1 + self.control2 = control2 + self.knot2 = knot2 + self.num_segments = num_segments + self.even_spread = even_spread + self.close = close + + def get_variables(self): + variables = set() + variables.update(self.control1[0].get_variables()) + variables.update(self.control1[1].get_variables()) + variables.update(self.control2[0].get_variables()) + variables.update(self.control2[1].get_variables()) + variables.update(self.knot2[0].get_variables()) + variables.update(self.knot2[1].get_variables()) + variables.update(self.num_segments.get_variables()) + variables.update(self.even_spread.get_variables()) + return variables + + def __repr__(self): + letter = "C" if self.is_abs else "c" + return "{} {} {} {} {} {} {}".format(letter, self.control1, self.control2, self.knot2, self.num_segments, self.even_spread, self.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 = interpreter.position + handle1 = interpreter.calc_vertex(self.is_abs, self.control1[0], self.control1[1], variables) + interpreter.position = handle1 + handle2 = interpreter.calc_vertex(self.is_abs, self.control2[0], self.control2[1], variables) + interpreter.position = handle2 + knot2 = interpreter.calc_vertex(self.is_abs, self.knot2[0], self.knot2[1], variables) + interpreter.position = knot2 + + r = self.num_segments.eval_(variables) + s = self.even_spread.eval_(variables) # not used yet + + points = interpolate_bezier(vec(knot1), vec(handle1), vec(handle2), vec(knot2), r) + + interpreter.new_knot("C#.h1", *handle1) + interpreter.new_knot("C#.h2", *handle2) + interpreter.new_knot("C#.k", *knot2) + + 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()) + variables.update(self.num_verts.get_variables()) + return variables + + def __repr__(self): + letter = "A" if self.is_abs else "a" + return "{} {} {} {} {} {} {} {}".format(letter, self.radii, self.rot, self.flag1, self.flag2, self.end, self.num_verts, self.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 = rad_x_expr.eval_(variables) + rad_y = rad_y_expr.eval_(variables) + radius = complex(rad_x, rad_y) + xaxis_rot = self.rot.eval_(variables) + flag1 = self.flag1.eval_(variables) + flag2 = self.flag2.eval_(variables) + + # numverts, requires -1 else it means segments (21 verts is 20 segments). + num_verts = self.num_verts.eval_(variables) + 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 Close(Statement): + def __init__(self): + pass + + def __repr__(self): + return "X" + + def get_variables(self): + return set() + + def interpret(self, interpreter, variables): + if not interpreter.has_last_vertex: + info("X statement: no current point, do nothing") + return + v1_index = interpreter.get_last_vertex() + interpreter.new_edge(v1_index, 0) + interpreter.closed = True + +######################################### +# DSL parsing +######################################### + +# Compare these definitions with BNF definition at the top + +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_]*") + +def parse_identifier(src): + for (name, _), rest in sequence(parse_regexp(identifier_regexp), parse_whitespace)(src): + yield name, rest + +def parse_value(src): + for smth, rest in one_of(parse_number, parse_identifier, 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) + 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"))) + for (is_abs, pairs, z), rest in parser(src): + yield LineTo(is_abs, pairs, z is not None), rest + +def parse_CurveTo(src): + parser = sequence( + parse_letter("C", "c"), + parse_pair, + parse_pair, + parse_pair, + parse_value, + parse_value, + optional(parse_word("z")) + ) + for (is_abs, control1, control2, knot2, num_segments, even_spread, z), rest in parser(src): + yield CurveTo(is_abs, control1, control2, knot2, num_segments, even_spread, 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, + parse_value, + optional(parse_word("z")) + ) + 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 + +parse_Close = parse_word("X", Close()) + +parse_statement = one_of(parse_MoveTo, parse_LineTo, parse_CurveTo, parse_ArcTo, parse_Close) + +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) + return profile + +################################# +# DSL Interpreter +################################# + +class Interpreter(object): + def __init__(self): + self.position = (0, 0) + self.previous_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.vertices = [] + self.edges = [] + self.knots = [] + self.knotnames = [] + + 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 = x_expr.eval_(variables) + y = y_expr.eval_(variables) + if is_abs: + return x,y + else: + return self.relative(x,y) + + def new_vertex(self, x, y): + index = self.next_vertex_index + self.vertices.append((x, y)) + 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 interpret(self, profile, variables): + if not profile: + return [], [] + for statement in profile: + statement.interpret(self, variables) + +################################# +# "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) + values += self.curve_points_count() + values += ' 0 ' + 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 += self.curve_points_count() + 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") + + 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.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) + + return list(sorted(list(variables))) + + 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 + + var_names = self.get_variables() + inputs = self.get_input() + + result_vertices = [] + result_edges = [] + result_knots = [] + result_names = [] + + profile = self.load_profile() + + if var_names: + try: + input_values = [inputs[name] for name in var_names] + except KeyError as e: + name = e.args[0] + if name in self.inputs: + raise SvNoDataError(self.inputs[name]) + else: + self.adjust_sockets() + raise SvNoDataError(self.inputs[name]) + parameters = match_long_repeat(input_values) + else: + parameters = [[[]]] + + for values in zip(*parameters): + variables = dict(zip(var_names, values)) + interpreter = Interpreter() + 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) + +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) + -- GitLab From 063eb7516d5e492dd9ca3adc7f0bc73e658eaa38 Mon Sep 17 00:00:00 2001 From: Ilya Portnov Date: Wed, 12 Jun 2019 21:17:23 +0500 Subject: [PATCH 03/47] Add some profile examples. --- profile_examples/complex_curve.txt | 13 +++++++++++++ profile_examples/rectangle.txt | 1 + profile_examples/rounded_line.txt | 2 ++ profile_examples/simple_curve.txt | 1 + profile_examples/square.txt | 3 +++ profile_examples/zeffi_1.txt | 18 ++++++++++++++++++ 6 files changed, 38 insertions(+) create mode 100644 profile_examples/complex_curve.txt create mode 100644 profile_examples/rectangle.txt create mode 100644 profile_examples/rounded_line.txt create mode 100644 profile_examples/simple_curve.txt create mode 100644 profile_examples/square.txt create mode 100644 profile_examples/zeffi_1.txt diff --git a/profile_examples/complex_curve.txt b/profile_examples/complex_curve.txt new file mode 100644 index 000000000..35749d2df --- /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 20 0 +l 2,0 +l 0,1 +C -1.0,9.0 1.0,9.0 1.0,7.0 20 0 +l 0,-1 +l 2,0 +C 5.0,6.0 5.0,4.0 3.0,4.0 20 0 +L 1.0,4.0 1.0,0.0 +X + diff --git a/profile_examples/rectangle.txt b/profile_examples/rectangle.txt new file mode 100644 index 000000000..81b6f42f1 --- /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 000000000..c5fb43973 --- /dev/null +++ b/profile_examples/rounded_line.txt @@ -0,0 +1,2 @@ +L d,rad {len-d},rad len,0 + diff --git a/profile_examples/simple_curve.txt b/profile_examples/simple_curve.txt new file mode 100644 index 000000000..b3ec48f37 --- /dev/null +++ b/profile_examples/simple_curve.txt @@ -0,0 +1 @@ +C control,control {size-control},control size,0 n 0 diff --git a/profile_examples/square.txt b/profile_examples/square.txt new file mode 100644 index 000000000..06689072a --- /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/zeffi_1.txt b/profile_examples/zeffi_1.txt new file mode 100644 index 000000000..a4d01b80f --- /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 10 +L 2,3 +A 2,2 0 0 1 4,5 10 +l 0,4 +a 2,3 0 0 0 -2,3 10 +a 2,3 0 0 1 -2,3 10 +a 2,2 0 0 1 -2,-2 10 +l 0,-2 2,-2 -1,0 +c 0,-1 2,1 0,-1 10 0 +l -2,0 +c -2,0 0,0 0,-1 10 0 +c 0,-1 2,0 0,-1 10 0 +a 2,1 0 0 0 -2,-1 10 +l 0,-2 +X + -- GitLab From a61a9faf4d19c772200bc0a3fee2f9417b39261a Mon Sep 17 00:00:00 2001 From: Ilya Portnov Date: Wed, 12 Jun 2019 21:33:36 +0500 Subject: [PATCH 04/47] Support "negated variable" syntax. --- nodes/generators_extended/profile_mk3.py | 29 ++++++++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/nodes/generators_extended/profile_mk3.py b/nodes/generators_extended/profile_mk3.py index 2bc1af532..ecd3f7d48 100644 --- a/nodes/generators_extended/profile_mk3.py +++ b/nodes/generators_extended/profile_mk3.py @@ -79,9 +79,10 @@ Our DSL has relatively simple BNF: ::= ... ::= ... - ::= "'" "'" | | + ::= "'" "'" | | | ::= Standard Python expression ::= Python variable identifier + ::= "-" ::= Python integer or floating-point literal """ @@ -163,6 +164,26 @@ class Variable(Expression): 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) + ############################################ # Statement classes # Classes for AST of our DSL @@ -406,8 +427,12 @@ 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_expr)(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): -- GitLab From c543a403390eeea8371280a5c859bcdf2b0452f6 Mon Sep 17 00:00:00 2001 From: Ilya Portnov Date: Wed, 12 Jun 2019 21:40:10 +0500 Subject: [PATCH 05/47] Start documentation. --- .../nodes/generators_extended/profile_mk3.rst | 164 ++++++++++++++++++ nodes/generators_extended/profile_mk3.py | 1 + 2 files changed, 165 insertions(+) create mode 100644 docs/nodes/generators_extended/profile_mk3.rst diff --git a/docs/nodes/generators_extended/profile_mk3.rst b/docs/nodes/generators_extended/profile_mk3.rst new file mode 100644 index 000000000..10b61d7f8 --- /dev/null +++ b/docs/nodes/generators_extended/profile_mk3.rst @@ -0,0 +1,164 @@ +======================= +Profile Parametric Node +======================= + + +**Profile Node** implements a useful subset of the SVG path section commands. +Currently the following segment types are available: + ++---------+------+---------------------------------------------------------------------------------+ +| name | cmd | parameters | ++=========+======+=================================================================================+ +| MoveTo | M, m| <2v coordinate> | ++---------+------+---------------------------------------------------------------------------------+ +| LineTo | L, l| <2v coordinate 1> <2v coordinate 2> <2v coordinate n> [z] | ++---------+------+---------------------------------------------------------------------------------+ +| CurveTo | C, c| <2v control1> <2v control2> <2v knot2> [z] | ++---------+------+---------------------------------------------------------------------------------+ +| ArcTo | A, a| <2v rx,ry> <2v x,y> [z] | ++---------+------+---------------------------------------------------------------------------------+ +| Close | X | | ++---------+------+---------------------------------------------------------------------------------+ +| comment | # | anything after # is a comment. | ++---------+------+---------------------------------------------------------------------------------+ + +:: + + <> : mandatory field + [] : optional field + 2v : two point vector `a,b` + - no space between , + - no backticks + - a and b can be + - number literals + - lowercase 1-character symbols for variables + int : 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 + + +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)}` + +For examples, see "Examples of usage" section below, or `profile_examples` directory in Sverchok distribution. + +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, + generated by **from selection** operator (see num_verts in description + above). Default value is 20. This parameter is available only in the N panel. + +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, and we always start the profile with a M ,. + +:: + + M 0,0 + L a,a b,0 c,0 d,d e,-e + + +CurveTo and ArcTo only take enough parameters to complete one Curve or Arc, +unlike real SVG commands which take a whole sequence of chained CurveTo or ArcTo commands. The decision to keep +it at one segment type per line is mainly to preserve readability. + +The CurveTo and ArcTo 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. + +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 10 0 + L 5,5 + C 7,5 7,5 7,3 10 0 + L 7,2 5,0 + X + +or + +:: + + M a,a + L a,b c,b -c,d + C c,e c,e b,e g 0 + L e,e + C f,e f,e f,-b g 0 + L f,c e,a + X + + +More Info +--------- + +The node started out as a thought experiment and turned into something quite useful, you can see how it evolved in the `github thread `_ + +Example usage: + +.. image:: https://cloud.githubusercontent.com/assets/619340/3905771/193b5d86-22ec-11e4-93e5-724863a30bbc.png + + +.. image:: https://cloud.githubusercontent.com/assets/619340/3895396/81f3b96c-224d-11e4-9ca7-f07756f40a0e.png + + +Gotchas +------- + +The update mechanism doesn't process inputs or anything until the following conditions are satisfied: + + * Profile Node has at least one input socket connected + * 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/nodes/generators_extended/profile_mk3.py b/nodes/generators_extended/profile_mk3.py index ecd3f7d48..1f8a3f74e 100644 --- a/nodes/generators_extended/profile_mk3.py +++ b/nodes/generators_extended/profile_mk3.py @@ -64,6 +64,7 @@ input like: * 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)} ''' -- GitLab From 118f12691370a9009bddcc5f73bd938d569ab977 Mon Sep 17 00:00:00 2001 From: Ilya Portnov Date: Wed, 12 Jun 2019 23:04:03 +0500 Subject: [PATCH 06/47] Support for H/h, V/v commands. --- .../nodes/generators_extended/profile_mk3.rst | 3 + nodes/generators_extended/profile_mk3.py | 147 ++++++++++++++++-- 2 files changed, 139 insertions(+), 11 deletions(-) diff --git a/docs/nodes/generators_extended/profile_mk3.rst b/docs/nodes/generators_extended/profile_mk3.rst index 10b61d7f8..7a0f0e727 100644 --- a/docs/nodes/generators_extended/profile_mk3.rst +++ b/docs/nodes/generators_extended/profile_mk3.rst @@ -54,6 +54,9 @@ Each integer or floating value may be represented as For examples, see "Examples of usage" section below, or `profile_examples` directory in Sverchok distribution. +Statements may optionally be separated by semicolons (`;`). +For some commands (namely: `H`/`h`, `V`/`v`) the trailing semicolon is **required**! + Parameters ---------- diff --git a/nodes/generators_extended/profile_mk3.py b/nodes/generators_extended/profile_mk3.py index 1f8a3f74e..bc00ba16e 100644 --- a/nodes/generators_extended/profile_mk3.py +++ b/nodes/generators_extended/profile_mk3.py @@ -42,6 +42,8 @@ input like: L|l <2v coordinate 1> <2v coordinate 2> <2v coordinate n> [z] C|c <2v control1> <2v control2> <2v knot2> [z] A|a <2v rx,ry> <2v x,y> [z] + H|h ... ; + V|v ... ; X # ----- @@ -66,6 +68,9 @@ input like: * 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)} + + Statements may optionally be separated by semicolons (;). + For some commands (namely: H/h, V/v) the trailing semicolon is *required*! ''' @@ -73,14 +78,17 @@ input like: Our DSL has relatively simple BNF: ::= * - ::= | | | | "X" + ::= | | | + | | | "X" ::= ("M" | "m") "," ::= ... ::= ... ::= ... + ::= ("H" | "h") * + ::= ("V" | "v") * - ::= "'" "'" | | | + ::= "{" "}" | | | ::= Standard Python expression ::= Python variable identifier ::= "-" @@ -190,6 +198,15 @@ class NegatedVariable(Variable): # 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): pass @@ -256,6 +273,82 @@ class LineTo(Statement): 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 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 = x_expr.eval_(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 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 = y_expr.eval_(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): def __init__(self, is_abs, control1, control2, knot2, num_segments, even_spread, close): self.is_abs = is_abs @@ -424,6 +517,8 @@ def parse_expr(src): 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 @@ -454,13 +549,17 @@ def parse_letter(absolute, relative): return parser def parse_MoveTo(src): - parser = sequence(parse_letter("M", "m"), parse_pair) - for (is_abs, (x, y)), rest in parser(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"))) - for (is_abs, pairs, z), rest in parser(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_CurveTo(src): @@ -471,9 +570,10 @@ def parse_CurveTo(src): parse_pair, parse_value, parse_value, - optional(parse_word("z")) + optional(parse_word("z")), + optional(parse_semicolon)) ) - for (is_abs, control1, control2, knot2, num_segments, even_spread, z), rest in parser(src): + for (is_abs, control1, control2, knot2, num_segments, even_spread, z, _), rest in parser(src): yield CurveTo(is_abs, control1, control2, knot2, num_segments, even_spread, z is not None), rest def parse_ArcTo(src): @@ -485,14 +585,38 @@ def parse_ArcTo(src): parse_value, parse_pair, parse_value, - optional(parse_word("z")) + optional(parse_word("z")), + optional(parse_semicolon)) ) - for (is_abs, radii, rot, flag1, flag2, end, num_verts, z), rest in parser(src): + 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_Close = parse_word("X", Close()) -parse_statement = one_of(parse_MoveTo, parse_LineTo, parse_CurveTo, parse_ArcTo, parse_Close) +parse_statement = one_of( + parse_MoveTo, + parse_LineTo, + parse_HorLineTo, + parse_VertLineTo, + parse_CurveTo, + parse_ArcTo, + parse_Close) parse_definition = many(parse_statement) @@ -507,6 +631,7 @@ def parse_profile(src): cleaned = cleaned + " " + line profile = parse(parse_definition, cleaned) + info(profile) return profile ################################# -- GitLab From 04b461a66b50bb0c3c9b2736719a40bc1d30e92d Mon Sep 17 00:00:00 2001 From: Ilya Portnov Date: Wed, 12 Jun 2019 23:04:30 +0500 Subject: [PATCH 07/47] Add an example of H/V commands usage. --- profile_examples/square3.txt | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 profile_examples/square3.txt diff --git a/profile_examples/square3.txt b/profile_examples/square3.txt new file mode 100644 index 000000000..3b1c0abba --- /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 + -- GitLab From a30ee6f383a4ca1ee0f53d0e6132616643df7b72 Mon Sep 17 00:00:00 2001 From: Ilya Portnov Date: Wed, 12 Jun 2019 23:06:11 +0500 Subject: [PATCH 08/47] update documentation. --- docs/nodes/generators_extended/profile_mk3.rst | 4 ++++ nodes/generators_extended/profile_mk3.py | 4 ++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/docs/nodes/generators_extended/profile_mk3.rst b/docs/nodes/generators_extended/profile_mk3.rst index 7a0f0e727..380734971 100644 --- a/docs/nodes/generators_extended/profile_mk3.rst +++ b/docs/nodes/generators_extended/profile_mk3.rst @@ -13,6 +13,10 @@ Currently the following segment types are available: +---------+------+---------------------------------------------------------------------------------+ | LineTo | L, l| <2v coordinate 1> <2v coordinate 2> <2v coordinate n> [z] | +---------+------+---------------------------------------------------------------------------------+ +| HorLineTo | H, h | ... ; | ++---------+------+---------------------------------------------------------------------------------+ +| VertLineTo | V, v | ... ; | ++---------+------+---------------------------------------------------------------------------------+ | CurveTo | C, c| <2v control1> <2v control2> <2v knot2> [z] | +---------+------+---------------------------------------------------------------------------------+ | ArcTo | A, a| <2v rx,ry> <2v x,y> [z] | diff --git a/nodes/generators_extended/profile_mk3.py b/nodes/generators_extended/profile_mk3.py index bc00ba16e..0de226b31 100644 --- a/nodes/generators_extended/profile_mk3.py +++ b/nodes/generators_extended/profile_mk3.py @@ -85,8 +85,8 @@ Our DSL has relatively simple BNF: ::= ... ::= ... ::= ... - ::= ("H" | "h") * - ::= ("V" | "v") * + ::= ("H" | "h") * ";" + ::= ("V" | "v") * ";" ::= "{" "}" | | | ::= Standard Python expression -- GitLab From 12704e6f78ca9d73428136391965ddb5f5cddbb0 Mon Sep 17 00:00:00 2001 From: Ilya Portnov Date: Wed, 12 Jun 2019 23:48:38 +0500 Subject: [PATCH 09/47] Support S/s commands. --- .../nodes/generators_extended/profile_mk3.rst | 2 + nodes/generators_extended/profile_mk3.py | 106 +++++++++++++++++- profile_examples/smooth_curve.txt | 6 + 3 files changed, 109 insertions(+), 5 deletions(-) create mode 100644 profile_examples/smooth_curve.txt diff --git a/docs/nodes/generators_extended/profile_mk3.rst b/docs/nodes/generators_extended/profile_mk3.rst index 380734971..ea99494ca 100644 --- a/docs/nodes/generators_extended/profile_mk3.rst +++ b/docs/nodes/generators_extended/profile_mk3.rst @@ -19,6 +19,8 @@ Currently the following segment types are available: +---------+------+---------------------------------------------------------------------------------+ | CurveTo | C, c| <2v control1> <2v control2> <2v knot2> [z] | +---------+------+---------------------------------------------------------------------------------+ +| SmoothCurveTo | S, s | <2v control2> <2v knot2> [z] | ++---------+------+---------------------------------------------------------------------------------+ | ArcTo | A, a| <2v rx,ry> <2v x,y> [z] | +---------+------+---------------------------------------------------------------------------------+ | Close | X | | diff --git a/nodes/generators_extended/profile_mk3.py b/nodes/generators_extended/profile_mk3.py index 0de226b31..3f7e4e658 100644 --- a/nodes/generators_extended/profile_mk3.py +++ b/nodes/generators_extended/profile_mk3.py @@ -41,6 +41,7 @@ input like: M|m <2v coordinate> L|l <2v coordinate 1> <2v coordinate 2> <2v coordinate n> [z] C|c <2v control1> <2v control2> <2v knot2> [z] + S|s <2v control2> <2v knot2> [z] A|a <2v rx,ry> <2v x,y> [z] H|h ... ; V|v ... ; @@ -78,14 +79,15 @@ input like: Our DSL has relatively simple BNF: ::= * - ::= | | | - | | | "X" + ::= | | | + | | | | "X" ::= ("M" | "m") "," ::= ... ::= ... + ::= ... ::= ... - ::= ("H" | "h") * ";" + ::= ("H" | "h") * ";" ::= ("V" | "v") * ";" ::= "{" "}" | | | @@ -404,6 +406,8 @@ class CurveTo(Statement): interpreter.new_knot("C#.h2", *handle2) interpreter.new_knot("C#.k", *knot2) + interpreter.prev_curve_knot = handle2 + for point in points[1:]: v1_index = interpreter.new_vertex(point.x, point.y) interpreter.new_edge(v0_index, v1_index) @@ -483,6 +487,84 @@ class ArcTo(Statement): interpreter.has_last_vertex = True +class SmoothCurveTo(Statement): + def __init__(self, is_abs, control2, knot2, num_segments, close): + self.is_abs = is_abs + self.control2 = control2 + self.knot2 = knot2 + self.num_segments = num_segments + self.close = close + + def get_variables(self): + variables = set() + variables.update(self.control2[0].get_variables()) + variables.update(self.control2[1].get_variables()) + variables.update(self.knot2[0].get_variables()) + variables.update(self.knot2[1].get_variables()) + variables.update(self.num_segments.get_variables()) + return variables + + def __repr__(self): + letter = "S" if self.is_abs else "s" + return "{} {} {} {} {}".format(letter, self.control2, self.knot2, self.num_segments, self.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 = interpreter.position + + if interpreter.prev_curve_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_curve_knot + x0, y0 = knot1 + dx, dy = x0 - prev_knot_x, y0 - prev_knot_y + handle1 = x0 + dx, y0 + dy + + # FIXME: it is not clear for me from SVG specification: for "s", + # should handle2 be calculated relatively to knot1 or relatively to handle1? + # I assume that it should be relative to knot1. + # interpreter.position = handle1 + handle2 = interpreter.calc_vertex(self.is_abs, self.control2[0], self.control2[1], variables) + interpreter.position = handle2 + knot2 = interpreter.calc_vertex(self.is_abs, self.knot2[0], self.knot2[1], variables) + interpreter.position = knot2 + + r = self.num_segments.eval_(variables) + + points = interpolate_bezier(vec(knot1), vec(handle1), vec(handle2), vec(knot2), r) + + interpreter.new_knot("S#.h1", *handle1) + interpreter.new_knot("S#.h2", *handle2) + interpreter.new_knot("S#.k", *knot2) + + interpreter.prev_curve_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 Close(Statement): def __init__(self): pass @@ -571,11 +653,23 @@ def parse_CurveTo(src): parse_value, parse_value, optional(parse_word("z")), - optional(parse_semicolon)) + optional(parse_semicolon) ) for (is_abs, control1, control2, knot2, num_segments, even_spread, z, _), rest in parser(src): yield CurveTo(is_abs, control1, control2, knot2, num_segments, even_spread, z is not None), rest +def parse_SmoothCurveTo(src): + parser = sequence( + parse_letter("S", "s"), + parse_pair, + parse_pair, + parse_value, + optional(parse_word("z")), + optional(parse_semicolon) + ) + for (is_abs, control2, knot2, num_segments, z, _), rest in parser(src): + yield SmoothCurveTo(is_abs, control2, knot2, num_segments, z is not None), rest + def parse_ArcTo(src): parser = sequence( parse_letter("A", "a"), @@ -586,7 +680,7 @@ def parse_ArcTo(src): parse_pair, parse_value, optional(parse_word("z")), - optional(parse_semicolon)) + 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 @@ -615,6 +709,7 @@ parse_statement = one_of( parse_HorLineTo, parse_VertLineTo, parse_CurveTo, + parse_SmoothCurveTo, parse_ArcTo, parse_Close) @@ -647,6 +742,7 @@ class Interpreter(object): self.segment_number = 0 self.has_last_vertex = False self.closed = False + self.prev_curve_knot = None self.vertices = [] self.edges = [] self.knots = [] diff --git a/profile_examples/smooth_curve.txt b/profile_examples/smooth_curve.txt new file mode 100644 index 000000000..2323d7f3f --- /dev/null +++ b/profile_examples/smooth_curve.txt @@ -0,0 +1,6 @@ +M 0,0 +C 1,1 3,1 4,0 20 0 +s 3,-1 1,1 20 +s 3,1 1,-1 20 +s 1,-3 -1,-1 20 + -- GitLab From 21436af1f6cf5b165d1f0c30b9cc5e0e2adaf533 Mon Sep 17 00:00:00 2001 From: Ilya Portnov Date: Thu, 13 Jun 2019 00:03:43 +0500 Subject: [PATCH 10/47] Fix table in the documentation. --- .../nodes/generators_extended/profile_mk3.rst | 42 +++++++++---------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/docs/nodes/generators_extended/profile_mk3.rst b/docs/nodes/generators_extended/profile_mk3.rst index ea99494ca..816503d3b 100644 --- a/docs/nodes/generators_extended/profile_mk3.rst +++ b/docs/nodes/generators_extended/profile_mk3.rst @@ -6,27 +6,27 @@ Profile Parametric Node **Profile Node** implements a useful subset of the SVG path section commands. Currently the following segment types are available: -+---------+------+---------------------------------------------------------------------------------+ -| name | cmd | parameters | -+=========+======+=================================================================================+ -| MoveTo | M, m| <2v coordinate> | -+---------+------+---------------------------------------------------------------------------------+ -| LineTo | L, l| <2v coordinate 1> <2v coordinate 2> <2v coordinate n> [z] | -+---------+------+---------------------------------------------------------------------------------+ -| HorLineTo | H, h | ... ; | -+---------+------+---------------------------------------------------------------------------------+ -| VertLineTo | V, v | ... ; | -+---------+------+---------------------------------------------------------------------------------+ -| CurveTo | C, c| <2v control1> <2v control2> <2v knot2> [z] | -+---------+------+---------------------------------------------------------------------------------+ -| SmoothCurveTo | S, s | <2v control2> <2v knot2> [z] | -+---------+------+---------------------------------------------------------------------------------+ -| ArcTo | A, a| <2v rx,ry> <2v x,y> [z] | -+---------+------+---------------------------------------------------------------------------------+ -| Close | X | | -+---------+------+---------------------------------------------------------------------------------+ -| comment | # | anything after # is a comment. | -+---------+------+---------------------------------------------------------------------------------+ ++---------------+-------+------------------------------------------------------------------------------+ +| name | cmd | parameters | ++===============+=======+==============================================================================+ +| MoveTo | M, m | <2v coordinate> | ++---------------+-------+------------------------------------------------------------------------------+ +| LineTo | L, l | <2v coordinate 1> <2v coordinate 2> <2v coordinate n> [z] | ++---------------+-------+------------------------------------------------------------------------------+ +| HorLineTo | H, h | ... ; | ++---------------+-------+------------------------------------------------------------------------------+ +| VertLineTo | V, v | ... ; | ++---------------+-------+------------------------------------------------------------------------------+ +| CurveTo | C, c | <2v control1> <2v control2> <2v knot2> [z] | ++---------------+-------+------------------------------------------------------------------------------+ +| SmoothCurveTo | S, s | <2v control2> <2v knot2> [z] | ++---------------+-------+------------------------------------------------------------------------------+ +| ArcTo | A, a | <2v rx,ry> <2v x,y> [z] | ++---------------+-------+------------------------------------------------------------------------------+ +| Close | X | | ++---------------+-------+------------------------------------------------------------------------------+ +| comment | # | anything after # is a comment. | ++---------------+-------+------------------------------------------------------------------------------+ :: -- GitLab From d57b3bb7f08f982ab83fb87a3a8ad296c228b3f2 Mon Sep 17 00:00:00 2001 From: Ilya Portnov Date: Thu, 13 Jun 2019 01:07:20 +0500 Subject: [PATCH 11/47] Fix "c"/"s" implementation according to SVG specification. --- nodes/generators_extended/profile_mk3.py | 18 ++++++++++++------ profile_examples/smooth_curve.txt | 9 ++++++--- 2 files changed, 18 insertions(+), 9 deletions(-) diff --git a/nodes/generators_extended/profile_mk3.py b/nodes/generators_extended/profile_mk3.py index 3f7e4e658..b0d255a6a 100644 --- a/nodes/generators_extended/profile_mk3.py +++ b/nodes/generators_extended/profile_mk3.py @@ -391,9 +391,17 @@ class CurveTo(Statement): knot1 = interpreter.position handle1 = interpreter.calc_vertex(self.is_abs, self.control1[0], self.control1[1], variables) - interpreter.position = handle1 + + # 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, self.control2[0], self.control2[1], variables) - interpreter.position = handle2 + #interpreter.position = handle2 knot2 = interpreter.calc_vertex(self.is_abs, self.knot2[0], self.knot2[1], variables) interpreter.position = knot2 @@ -536,12 +544,10 @@ class SmoothCurveTo(Statement): dx, dy = x0 - prev_knot_x, y0 - prev_knot_y handle1 = x0 + dx, y0 + dy - # FIXME: it is not clear for me from SVG specification: for "s", - # should handle2 be calculated relatively to knot1 or relatively to handle1? - # I assume that it should be relative to knot1. + # I assume that handle2 should be relative to knot1, not to handle1. # interpreter.position = handle1 handle2 = interpreter.calc_vertex(self.is_abs, self.control2[0], self.control2[1], variables) - interpreter.position = handle2 + # interpreter.position = handle2 knot2 = interpreter.calc_vertex(self.is_abs, self.knot2[0], self.knot2[1], variables) interpreter.position = knot2 diff --git a/profile_examples/smooth_curve.txt b/profile_examples/smooth_curve.txt index 2323d7f3f..14ecc2563 100644 --- a/profile_examples/smooth_curve.txt +++ b/profile_examples/smooth_curve.txt @@ -1,6 +1,9 @@ M 0,0 C 1,1 3,1 4,0 20 0 -s 3,-1 1,1 20 -s 3,1 1,-1 20 -s 1,-3 -1,-1 20 +s 3,-1 4,0 20 +s 3,1 4,0 20 +s 1,-3 0,-4 20 +s -3,-1 -4,0 20 +s -3,1 -4,0 20 +s -3,-1 -4,0 20 -- GitLab From 36a49c3689818ddc43866ced1c4a790633f4eee9 Mon Sep 17 00:00:00 2001 From: Ilya Portnov Date: Thu, 13 Jun 2019 20:59:43 +0500 Subject: [PATCH 12/47] Remove dummy "even_spread" component from C/c commands. --- nodes/generators_extended/profile_mk3.py | 13 ++++--------- profile_examples/complex_curve.txt | 6 +++--- profile_examples/simple_curve.txt | 2 +- profile_examples/smooth_curve.txt | 2 +- profile_examples/zeffi_1.txt | 6 +++--- 5 files changed, 12 insertions(+), 17 deletions(-) diff --git a/nodes/generators_extended/profile_mk3.py b/nodes/generators_extended/profile_mk3.py index b0d255a6a..f29cac8a3 100644 --- a/nodes/generators_extended/profile_mk3.py +++ b/nodes/generators_extended/profile_mk3.py @@ -352,13 +352,12 @@ class VerticalLineTo(Statement): interpreter.has_last_vertex = True class CurveTo(Statement): - def __init__(self, is_abs, control1, control2, knot2, num_segments, even_spread, close): + def __init__(self, is_abs, control1, control2, knot2, num_segments, close): self.is_abs = is_abs self.control1 = control1 self.control2 = control2 self.knot2 = knot2 self.num_segments = num_segments - self.even_spread = even_spread self.close = close def get_variables(self): @@ -370,12 +369,11 @@ class CurveTo(Statement): variables.update(self.knot2[0].get_variables()) variables.update(self.knot2[1].get_variables()) variables.update(self.num_segments.get_variables()) - variables.update(self.even_spread.get_variables()) return variables def __repr__(self): letter = "C" if self.is_abs else "c" - return "{} {} {} {} {} {} {}".format(letter, self.control1, self.control2, self.knot2, self.num_segments, self.even_spread, self.close) + return "{} {} {} {} {} {}".format(letter, self.control1, self.control2, self.knot2, self.num_segments, self.close) def interpret(self, interpreter, variables): vec = lambda v: Vector((v[0], v[1], 0)) @@ -406,7 +404,6 @@ class CurveTo(Statement): interpreter.position = knot2 r = self.num_segments.eval_(variables) - s = self.even_spread.eval_(variables) # not used yet points = interpolate_bezier(vec(knot1), vec(handle1), vec(handle2), vec(knot2), r) @@ -657,12 +654,11 @@ def parse_CurveTo(src): parse_pair, parse_pair, parse_value, - parse_value, optional(parse_word("z")), optional(parse_semicolon) ) - for (is_abs, control1, control2, knot2, num_segments, even_spread, z, _), rest in parser(src): - yield CurveTo(is_abs, control1, control2, knot2, num_segments, even_spread, z is not None), rest + for (is_abs, control1, control2, knot2, num_segments, z, _), rest in parser(src): + yield CurveTo(is_abs, control1, control2, knot2, num_segments, z is not None), rest def parse_SmoothCurveTo(src): parser = sequence( @@ -913,7 +909,6 @@ class SvPrifilizerMk3(bpy.types.Operator): values += self.stringadd(hl,ob_points[i].select_left_handle) values += self.stringadd(co,ob_points[i].select_control_point) values += self.curve_points_count() - values += ' 0 ' if curve: values += '\n' out_points.append(hr[:]) diff --git a/profile_examples/complex_curve.txt b/profile_examples/complex_curve.txt index 35749d2df..bfa83f7d3 100644 --- a/profile_examples/complex_curve.txt +++ b/profile_examples/complex_curve.txt @@ -1,13 +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 20 0 +C -5.0,4.0 -5.0,6.0 -3.0,6.0 20 l 2,0 l 0,1 -C -1.0,9.0 1.0,9.0 1.0,7.0 20 0 +C -1.0,9.0 1.0,9.0 1.0,7.0 20 l 0,-1 l 2,0 -C 5.0,6.0 5.0,4.0 3.0,4.0 20 0 +C 5.0,6.0 5.0,4.0 3.0,4.0 20 L 1.0,4.0 1.0,0.0 X diff --git a/profile_examples/simple_curve.txt b/profile_examples/simple_curve.txt index b3ec48f37..361633ad8 100644 --- a/profile_examples/simple_curve.txt +++ b/profile_examples/simple_curve.txt @@ -1 +1 @@ -C control,control {size-control},control size,0 n 0 +C control,control {size-control},control size,0 n diff --git a/profile_examples/smooth_curve.txt b/profile_examples/smooth_curve.txt index 14ecc2563..41afb6721 100644 --- a/profile_examples/smooth_curve.txt +++ b/profile_examples/smooth_curve.txt @@ -1,5 +1,5 @@ M 0,0 -C 1,1 3,1 4,0 20 0 +C 1,1 3,1 4,0 20 s 3,-1 4,0 20 s 3,1 4,0 20 s 1,-3 0,-4 20 diff --git a/profile_examples/zeffi_1.txt b/profile_examples/zeffi_1.txt index a4d01b80f..1077f630c 100644 --- a/profile_examples/zeffi_1.txt +++ b/profile_examples/zeffi_1.txt @@ -8,10 +8,10 @@ a 2,3 0 0 0 -2,3 10 a 2,3 0 0 1 -2,3 10 a 2,2 0 0 1 -2,-2 10 l 0,-2 2,-2 -1,0 -c 0,-1 2,1 0,-1 10 0 +c 0,-1 2,1 0,-1 10 l -2,0 -c -2,0 0,0 0,-1 10 0 -c 0,-1 2,0 0,-1 10 0 +c -2,0 0,0 0,-1 10 +c 0,-1 2,0 0,-1 10 a 2,1 0 0 0 -2,-1 10 l 0,-2 X -- GitLab From b28bec7a2c6a0311dcd1dca4922d8288167e8dab Mon Sep 17 00:00:00 2001 From: Ilya Portnov Date: Thu, 13 Jun 2019 21:30:28 +0500 Subject: [PATCH 13/47] Change synax of "num_verts" parameter of curve commands. --- nodes/generators_extended/profile_mk3.py | 44 +++++++++++++++++------- profile_examples/complex_curve.txt | 6 ++-- profile_examples/simple_curve.txt | 2 +- 3 files changed, 35 insertions(+), 17 deletions(-) diff --git a/nodes/generators_extended/profile_mk3.py b/nodes/generators_extended/profile_mk3.py index f29cac8a3..e2b72dea5 100644 --- a/nodes/generators_extended/profile_mk3.py +++ b/nodes/generators_extended/profile_mk3.py @@ -40,7 +40,7 @@ input like: M|m <2v coordinate> L|l <2v coordinate 1> <2v coordinate 2> <2v coordinate n> [z] - C|c <2v control1> <2v control2> <2v knot2> [z] + C|c <2v control1> <2v control2> <2v knot2> [z] S|s <2v control2> <2v knot2> [z] A|a <2v rx,ry> <2v x,y> [z] H|h ... ; @@ -368,7 +368,8 @@ class CurveTo(Statement): variables.update(self.control2[1].get_variables()) variables.update(self.knot2[0].get_variables()) variables.update(self.knot2[1].get_variables()) - variables.update(self.num_segments.get_variables()) + if self.num_segments: + variables.update(self.num_segments.get_variables()) return variables def __repr__(self): @@ -403,7 +404,10 @@ class CurveTo(Statement): knot2 = interpreter.calc_vertex(self.is_abs, self.knot2[0], self.knot2[1], variables) interpreter.position = knot2 - r = self.num_segments.eval_(variables) + if self.num_segments is not None: + r = self.num_segments.eval_(variables) + else: + r = interpreter.dflt_num_verts points = interpolate_bezier(vec(knot1), vec(handle1), vec(handle2), vec(knot2), r) @@ -443,7 +447,8 @@ class ArcTo(Statement): variables.update(self.flag2.get_variables()) variables.update(self.end[0].get_variables()) variables.update(self.end[1].get_variables()) - variables.update(self.num_verts.get_variables()) + if self.num_verts: + variables.update(self.num_verts.get_variables()) return variables def __repr__(self): @@ -470,7 +475,10 @@ class ArcTo(Statement): flag2 = self.flag2.eval_(variables) # numverts, requires -1 else it means segments (21 verts is 20 segments). - num_verts = self.num_verts.eval_(variables) + if self.num_verts is not None: + num_verts = self.num_verts.eval_(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) @@ -506,7 +514,8 @@ class SmoothCurveTo(Statement): variables.update(self.control2[1].get_variables()) variables.update(self.knot2[0].get_variables()) variables.update(self.knot2[1].get_variables()) - variables.update(self.num_segments.get_variables()) + if self.num_segments: + variables.update(self.num_segments.get_variables()) return variables def __repr__(self): @@ -548,7 +557,10 @@ class SmoothCurveTo(Statement): knot2 = interpreter.calc_vertex(self.is_abs, self.knot2[0], self.knot2[1], variables) interpreter.position = knot2 - r = self.num_segments.eval_(variables) + if self.num_segments is not None: + r = self.num_segments.eval_(variables) + else: + r = interpreter.dflt_num_verts points = interpolate_bezier(vec(knot1), vec(handle1), vec(handle2), vec(knot2), r) @@ -647,13 +659,19 @@ def parse_LineTo(src): 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): parser = sequence( parse_letter("C", "c"), parse_pair, parse_pair, parse_pair, - parse_value, + optional(parse_parameter("n")), optional(parse_word("z")), optional(parse_semicolon) ) @@ -665,7 +683,7 @@ def parse_SmoothCurveTo(src): parse_letter("S", "s"), parse_pair, parse_pair, - parse_value, + optional(parse_parameter("n")), optional(parse_word("z")), optional(parse_semicolon) ) @@ -680,7 +698,7 @@ def parse_ArcTo(src): parse_value, parse_value, parse_pair, - parse_value, + optional(parse_parameter("n")), optional(parse_word("z")), optional(parse_semicolon) ) @@ -736,9 +754,8 @@ def parse_profile(src): ################################# class Interpreter(object): - def __init__(self): + def __init__(self, node): self.position = (0, 0) - self.previous_position = (0, 0) self.next_vertex_index = 0 self.segment_start_index = 0 self.segment_number = 0 @@ -749,6 +766,7 @@ class Interpreter(object): self.edges = [] self.knots = [] self.knotnames = [] + self.dflt_num_verts = node.curve_points_count def assert_not_closed(self): if self.closed: @@ -1187,7 +1205,7 @@ class SvProfileNodeMK3(bpy.types.Node, SverchCustomTreeNode): for values in zip(*parameters): variables = dict(zip(var_names, values)) - interpreter = Interpreter() + interpreter = Interpreter(self) interpreter.interpret(profile, variables) verts = self.extend_out_verts(interpreter.vertices) result_vertices.append(verts) diff --git a/profile_examples/complex_curve.txt b/profile_examples/complex_curve.txt index bfa83f7d3..3efc4591c 100644 --- a/profile_examples/complex_curve.txt +++ b/profile_examples/complex_curve.txt @@ -1,13 +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 20 +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 20 +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 20 +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/simple_curve.txt b/profile_examples/simple_curve.txt index 361633ad8..62ed2aefe 100644 --- a/profile_examples/simple_curve.txt +++ b/profile_examples/simple_curve.txt @@ -1 +1 @@ -C control,control {size-control},control size,0 n +C control,control {size-control},control size,0 n=n -- GitLab From 0c1607b0f037c306f517d5dbbb403874287a893c Mon Sep 17 00:00:00 2001 From: Ilya Portnov Date: Thu, 13 Jun 2019 21:33:55 +0500 Subject: [PATCH 14/47] Update documentation. --- .../nodes/generators_extended/profile_mk3.rst | 50 +++++++++---------- nodes/generators_extended/profile_mk3.py | 6 +-- 2 files changed, 28 insertions(+), 28 deletions(-) diff --git a/docs/nodes/generators_extended/profile_mk3.rst b/docs/nodes/generators_extended/profile_mk3.rst index 816503d3b..3c5290296 100644 --- a/docs/nodes/generators_extended/profile_mk3.rst +++ b/docs/nodes/generators_extended/profile_mk3.rst @@ -6,27 +6,27 @@ Profile Parametric Node **Profile Node** implements a useful subset of the SVG path section commands. Currently the following segment types are available: -+---------------+-------+------------------------------------------------------------------------------+ -| name | cmd | parameters | -+===============+=======+==============================================================================+ -| MoveTo | M, m | <2v coordinate> | -+---------------+-------+------------------------------------------------------------------------------+ -| LineTo | L, l | <2v coordinate 1> <2v coordinate 2> <2v coordinate n> [z] | -+---------------+-------+------------------------------------------------------------------------------+ -| HorLineTo | H, h | ... ; | -+---------------+-------+------------------------------------------------------------------------------+ -| VertLineTo | V, v | ... ; | -+---------------+-------+------------------------------------------------------------------------------+ -| CurveTo | C, c | <2v control1> <2v control2> <2v knot2> [z] | -+---------------+-------+------------------------------------------------------------------------------+ -| SmoothCurveTo | S, s | <2v control2> <2v knot2> [z] | -+---------------+-------+------------------------------------------------------------------------------+ -| ArcTo | A, a | <2v rx,ry> <2v x,y> [z] | -+---------------+-------+------------------------------------------------------------------------------+ -| Close | X | | -+---------------+-------+------------------------------------------------------------------------------+ -| comment | # | anything after # is a comment. | -+---------------+-------+------------------------------------------------------------------------------+ ++---------------+-------+--------------------------------------------------------------------------------+ +| name | cmd | parameters | ++===============+=======+================================================================================+ +| MoveTo | M, m | <2v coordinate> | ++---------------+-------+--------------------------------------------------------------------------------+ +| LineTo | L, l | <2v coordinate 1> <2v coordinate 2> <2v coordinate n> [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] | ++---------------+-------+--------------------------------------------------------------------------------+ +| ArcTo | A, a | <2v rx,ry> <2v x,y> ["n = " num_verts] [z] | ++---------------+-------+--------------------------------------------------------------------------------+ +| Close | X | | ++---------------+-------+--------------------------------------------------------------------------------+ +| comment | # | anything after # is a comment. | ++---------------+-------+--------------------------------------------------------------------------------+ :: @@ -121,9 +121,9 @@ 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 10 0 + C 2,5 2,5 3,5 n=10 L 5,5 - C 7,5 7,5 7,3 10 0 + C 7,5 7,5 7,3 n=10 L 7,2 5,0 X @@ -133,9 +133,9 @@ or M a,a L a,b c,b -c,d - C c,e c,e b,e g 0 + C c,e c,e b,e n=g L e,e - C f,e f,e f,-b g 0 + C f,e f,e f,-b n=g L f,c e,a X diff --git a/nodes/generators_extended/profile_mk3.py b/nodes/generators_extended/profile_mk3.py index e2b72dea5..a48ef02ef 100644 --- a/nodes/generators_extended/profile_mk3.py +++ b/nodes/generators_extended/profile_mk3.py @@ -40,9 +40,9 @@ input like: M|m <2v coordinate> L|l <2v coordinate 1> <2v coordinate 2> <2v coordinate n> [z] - C|c <2v control1> <2v control2> <2v knot2> [z] - S|s <2v control2> <2v knot2> [z] - A|a <2v rx,ry> <2v x,y> [z] + C|c <2v control1> <2v control2> <2v knot2> ["n = " num_segments] [z] + S|s <2v control2> <2v knot2> ["n = " num_segments] [z] + A|a <2v rx,ry> <2v x,y> ["n = " num_verts] [z] H|h ... ; V|v ... ; X -- GitLab From 67fc4a079a08b0f79fa6fb359e093548e044e6f4 Mon Sep 17 00:00:00 2001 From: Ilya Portnov Date: Thu, 13 Jun 2019 22:16:45 +0500 Subject: [PATCH 15/47] Implement Q/q, T/t commands (quadratic bezier) --- nodes/generators_extended/profile_mk3.py | 177 +++++++++++++++++++++-- profile_examples/quadratic_sample.txt | 8 + utils/geom.py | 24 ++- 3 files changed, 199 insertions(+), 10 deletions(-) create mode 100644 profile_examples/quadratic_sample.txt diff --git a/nodes/generators_extended/profile_mk3.py b/nodes/generators_extended/profile_mk3.py index a48ef02ef..7f4e1cd8e 100644 --- a/nodes/generators_extended/profile_mk3.py +++ b/nodes/generators_extended/profile_mk3.py @@ -32,6 +32,7 @@ from sverchok.core.socket_data import SvNoDataError from sverchok.data_structure import fullList, updateNode, dataCorrect, match_long_repeat from sverchok.utils.parsec import * from sverchok.utils.logging import info, debug, warning +from sverchok.utils.geom import interpolate_quadratic_bezier from sverchok.utils.sv_curve_utils import Arc from sverchok.utils.sv_update_utils import sv_get_local_path @@ -51,7 +52,6 @@ input like: <> : mandatory field [] : optional field 2v : two point vector `a,b` - - no space between , - no backticks - a and b can be number literals or lowercase 1-character symbols for variables @@ -374,7 +374,7 @@ class CurveTo(Statement): def __repr__(self): letter = "C" if self.is_abs else "c" - return "{} {} {} {} {} {}".format(letter, self.control1, self.control2, self.knot2, self.num_segments, self.close) + return "{} {} {} {} n={} {}".format(letter, self.control1, self.control2, self.knot2, self.num_segments, self.close) def interpret(self, interpreter, variables): vec = lambda v: Vector((v[0], v[1], 0)) @@ -415,7 +415,7 @@ class CurveTo(Statement): interpreter.new_knot("C#.h2", *handle2) interpreter.new_knot("C#.k", *knot2) - interpreter.prev_curve_knot = handle2 + interpreter.prev_bezier_knot = handle2 for point in points[1:]: v1_index = interpreter.new_vertex(point.x, point.y) @@ -453,7 +453,7 @@ class ArcTo(Statement): def __repr__(self): letter = "A" if self.is_abs else "a" - return "{} {} {} {} {} {} {} {}".format(letter, self.radii, self.rot, self.flag1, self.flag2, self.end, self.num_verts, self.close) + return "{} {} {} {} {} {} n={} {}".format(letter, self.radii, self.rot, self.flag1, self.flag2, self.end, self.num_verts, self.close) def interpret(self, interpreter, variables): interpreter.assert_not_closed() @@ -520,7 +520,7 @@ class SmoothCurveTo(Statement): def __repr__(self): letter = "S" if self.is_abs else "s" - return "{} {} {} {} {}".format(letter, self.control2, self.knot2, self.num_segments, self.close) + return "{} {} {} n={} {}".format(letter, self.control2, self.knot2, self.num_segments, self.close) def interpret(self, interpreter, variables): vec = lambda v: Vector((v[0], v[1], 0)) @@ -536,7 +536,7 @@ class SmoothCurveTo(Statement): knot1 = interpreter.position - if interpreter.prev_curve_knot is None: + 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. @@ -545,7 +545,7 @@ class SmoothCurveTo(Statement): # 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_curve_knot + 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 @@ -568,7 +568,140 @@ class SmoothCurveTo(Statement): interpreter.new_knot("S#.h2", *handle2) interpreter.new_knot("S#.k", *knot2) - interpreter.prev_curve_knot = handle2 + 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): + def __init__(self, is_abs, control, knot2, num_segments, close): + self.is_abs = is_abs + self.control = control + self.knot2 = knot2 + self.num_segments = num_segments + self.close = close + + def get_variables(self): + variables = set() + variables.update(self.control[0].get_variables()) + variables.update(self.control[1].get_variables()) + variables.update(self.knot2[0].get_variables()) + variables.update(self.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" + return "{} {} {} n={} {}".format(letter, self.control, self.knot2, self.num_segments, self.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 = interpreter.position + handle = interpreter.calc_vertex(self.is_abs, self.control[0], self.control[1], variables) + knot2 = interpreter.calc_vertex(self.is_abs, self.knot2[0], self.knot2[1], variables) + interpreter.position = knot2 + + if self.num_segments is not None: + r = self.num_segments.eval_(variables) + else: + r = interpreter.dflt_num_verts + + points = interpolate_quadratic_bezier(vec(knot1), vec(handle), vec(knot2), r) + + interpreter.new_knot("Q#.h", *handle) + interpreter.new_knot("Q#.k", *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): + def __init__(self, is_abs, knot2, num_segments, close): + self.is_abs = is_abs + self.knot2 = knot2 + self.num_segments = num_segments + self.close = close + + def get_variables(self): + variables = set() + variables.update(self.knot2[0].get_variables()) + variables.update(self.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" + return "{} {} n={} {}".format(letter, self.knot2, self.num_segments, self.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 = interpreter.position + + 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, self.knot2[0], self.knot2[1], variables) + interpreter.position = knot2 + + if self.num_segments is not None: + r = self.num_segments.eval_(variables) + else: + r = interpreter.dflt_num_verts + + points = interpolate_quadratic_bezier(vec(knot1), vec(handle), vec(knot2), r) + + interpreter.new_knot("T#.h", *handle) + interpreter.new_knot("T#.k", *knot2) + + interpreter.prev_quad_bezier_knot = handle for point in points[1:]: v1_index = interpreter.new_vertex(point.x, point.y) @@ -690,6 +823,29 @@ def parse_SmoothCurveTo(src): for (is_abs, control2, knot2, num_segments, z, _), rest in parser(src): yield SmoothCurveTo(is_abs, control2, knot2, num_segments, z is not None), rest +def parse_QuadCurveTo(src): + parser = sequence( + parse_letter("Q", "q"), + parse_pair, + parse_pair, + optional(parse_parameter("n")), + optional(parse_word("z")), + optional(parse_semicolon) + ) + for (is_abs, control, knot2, num_segments, z, _), rest in parser(src): + yield QuadraticCurveTo(is_abs, control, knot2, num_segments, z is not None), rest + +def parse_SmoothQuadCurveTo(src): + parser = sequence( + parse_letter("T", "t"), + parse_pair, + 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"), @@ -730,6 +886,8 @@ parse_statement = one_of( parse_VertLineTo, parse_CurveTo, parse_SmoothCurveTo, + parse_QuadCurveTo, + parse_SmoothQuadCurveTo, parse_ArcTo, parse_Close) @@ -761,7 +919,8 @@ class Interpreter(object): self.segment_number = 0 self.has_last_vertex = False self.closed = False - self.prev_curve_knot = None + self.prev_bezier_knot = None + self.prev_quad_bezier_knot = None self.vertices = [] self.edges = [] self.knots = [] diff --git a/profile_examples/quadratic_sample.txt b/profile_examples/quadratic_sample.txt new file mode 100644 index 000000000..a909f3598 --- /dev/null +++ b/profile_examples/quadratic_sample.txt @@ -0,0 +1,8 @@ +Q 3,3 6,0 +q 3,-3 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/utils/geom.py b/utils/geom.py index 48e49a99a..74a41a9a9 100644 --- a/utils/geom.py +++ b/utils/geom.py @@ -37,6 +37,7 @@ import mathutils from mathutils import Matrix, Vector from mathutils.geometry import interpolate_bezier, intersect_line_line, intersect_point_line + from sverchok.utils.sv_bmesh_utils import bmesh_from_pydata from sverchok.utils.sv_bmesh_utils import pydata_from_bmesh from sverchok.data_structure import match_long_repeat @@ -873,6 +874,27 @@ def diameter(vertices, axis): return (M-m) +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) @@ -936,4 +958,4 @@ def distance_line_line(line_a, line_b, result, gates, tolerance): for i, res in enumerate(result): if gates[i]: - res.append([local_result[i]]) \ No newline at end of file + res.append([local_result[i]]) -- GitLab From 201845cba107dd1dda734b7eab50f4d579ff63b8 Mon Sep 17 00:00:00 2001 From: Ilya Portnov Date: Thu, 13 Jun 2019 22:25:53 +0500 Subject: [PATCH 16/47] info -> debug --- nodes/generators_extended/profile_mk3.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/nodes/generators_extended/profile_mk3.py b/nodes/generators_extended/profile_mk3.py index 7f4e1cd8e..5de5a3bf3 100644 --- a/nodes/generators_extended/profile_mk3.py +++ b/nodes/generators_extended/profile_mk3.py @@ -43,6 +43,8 @@ input like: 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 ... ; @@ -80,12 +82,15 @@ Our DSL has relatively simple BNF: ::= * ::= | | | + | | | | | | "X" ::= ("M" | "m") "," ::= ... ::= ... ::= ... + ::= ... + ::= ... ::= ... ::= ("H" | "h") * ";" ::= ("V" | "v") * ";" @@ -904,7 +909,7 @@ def parse_profile(src): cleaned = cleaned + " " + line profile = parse(parse_definition, cleaned) - info(profile) + debug(profile) return profile ################################# -- GitLab From 2dea7788f5a65376004e8c98dac9f19431f5d5d9 Mon Sep 17 00:00:00 2001 From: Ilya Portnov Date: Thu, 13 Jun 2019 22:27:21 +0500 Subject: [PATCH 17/47] Update example. --- profile_examples/quadratic_sample.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/profile_examples/quadratic_sample.txt b/profile_examples/quadratic_sample.txt index a909f3598..a3b2c3f28 100644 --- a/profile_examples/quadratic_sample.txt +++ b/profile_examples/quadratic_sample.txt @@ -1,5 +1,5 @@ -Q 3,3 6,0 -q 3,-3 6,0 +Q 3,H 6,0 +t 6,0 t 6,0 t 0,-6 t -6,0 -- GitLab From e2bfc7300bf10d484fed356ead137168700177ab Mon Sep 17 00:00:00 2001 From: Ilya Portnov Date: Thu, 13 Jun 2019 22:56:01 +0500 Subject: [PATCH 18/47] X command: support "remove doubles" functionality. --- .../nodes/generators_extended/profile_mk3.rst | 5 ++- nodes/generators_extended/profile_mk3.py | 37 +++++++++++++++++-- 2 files changed, 38 insertions(+), 4 deletions(-) diff --git a/docs/nodes/generators_extended/profile_mk3.rst b/docs/nodes/generators_extended/profile_mk3.rst index 3c5290296..1a149362d 100644 --- a/docs/nodes/generators_extended/profile_mk3.rst +++ b/docs/nodes/generators_extended/profile_mk3.rst @@ -33,7 +33,6 @@ Currently the following segment types are available: <> : mandatory field [] : optional field 2v : two point vector `a,b` - - no space between , - no backticks - a and b can be - number literals @@ -78,6 +77,10 @@ This node has the following parameters: - **Curve points count**. Default number of points for curve segment commands, generated by **from selection** operator (see num_verts in description above). 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. Operators --------- diff --git a/nodes/generators_extended/profile_mk3.py b/nodes/generators_extended/profile_mk3.py index 5de5a3bf3..115d204c2 100644 --- a/nodes/generators_extended/profile_mk3.py +++ b/nodes/generators_extended/profile_mk3.py @@ -23,7 +23,7 @@ from math import * import os import bpy -from bpy.props import BoolProperty, StringProperty, EnumProperty, FloatVectorProperty, IntProperty +from bpy.props import BoolProperty, StringProperty, EnumProperty, FloatProperty, IntProperty from mathutils.geometry import interpolate_bezier from mathutils import Vector @@ -729,9 +729,19 @@ class Close(Statement): 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 @@ -916,6 +926,13 @@ def parse_profile(src): # 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): self.position = (0, 0) @@ -931,6 +948,7 @@ class Interpreter(object): self.knots = [] self.knotnames = [] self.dflt_num_verts = node.curve_points_count + self.close_threshold = node.close_threshold def assert_not_closed(self): if self.closed: @@ -950,7 +968,8 @@ class Interpreter(object): def new_vertex(self, x, y): index = self.next_vertex_index - self.vertices.append((x, y)) + vertex = (x, y) + self.vertices.append(vertex) self.next_vertex_index += 1 return index @@ -969,9 +988,15 @@ class Interpreter(object): 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 interpret(self, profile, variables): if not profile: - return [], [] + return for statement in profile: statement.interpret(self, variables) @@ -1243,6 +1268,10 @@ class SvProfileNodeMK3(bpy.types.Node, SverchCustomTreeNode): 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') @@ -1258,6 +1287,8 @@ class SvProfileNodeMK3(bpy.types.Node, SverchCustomTreeNode): 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") -- GitLab From 17875c8d430986d8a11be2f28c37ef469437485c Mon Sep 17 00:00:00 2001 From: Ilya Portnov Date: Fri, 14 Jun 2019 00:11:08 +0500 Subject: [PATCH 19/47] Add example. --- profile_examples/i-beam.txt | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 profile_examples/i-beam.txt diff --git a/profile_examples/i-beam.txt b/profile_examples/i-beam.txt new file mode 100644 index 000000000..1bcc11812 --- /dev/null +++ b/profile_examples/i-beam.txt @@ -0,0 +1,7 @@ +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 -- GitLab From e40580d5b39ca7322260a2d29dcbfc135d466257 Mon Sep 17 00:00:00 2001 From: Ilya Portnov Date: Fri, 14 Jun 2019 20:41:29 +0500 Subject: [PATCH 20/47] Extend list of available functions. --- nodes/generators_extended/profile_mk3.py | 30 ++++++++++++++++++++---- 1 file changed, 26 insertions(+), 4 deletions(-) diff --git a/nodes/generators_extended/profile_mk3.py b/nodes/generators_extended/profile_mk3.py index 115d204c2..34db5387b 100644 --- a/nodes/generators_extended/profile_mk3.py +++ b/nodes/generators_extended/profile_mk3.py @@ -25,7 +25,7 @@ import os import bpy from bpy.props import BoolProperty, StringProperty, EnumProperty, FloatProperty, IntProperty from mathutils.geometry import interpolate_bezier -from mathutils import Vector +from mathutils import Vector, Matrix from sverchok.node_tree import SverchCustomTreeNode from sverchok.core.socket_data import SvNoDataError @@ -103,6 +103,29 @@ Our DSL has relatively simple BNF: """ +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 ########################################## @@ -112,7 +135,6 @@ class Expression(object): self.expr = expr self.string = string - safe_names = dict(sin=sin, cos=cos, pi=pi, sqrt=sqrt) def __repr__(self): return "Expr({})".format(self.string) @@ -130,14 +152,14 @@ class Expression(object): def eval_(self, variables): env = dict() - env.update(self.safe_names) + 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(self.safe_names.keys()) + return result.difference(safe_names.keys()) class Const(Expression): def __init__(self, value): -- GitLab From bd175df334c3da56cd2398185fc28f6a9e689d7a Mon Sep 17 00:00:00 2001 From: Ilya Portnov Date: Fri, 14 Jun 2019 21:07:18 +0500 Subject: [PATCH 21/47] Add examples. --- profile_examples/arc_fillet.txt | 5 +++++ profile_examples/quadratic_fillet.txt | 3 +++ 2 files changed, 8 insertions(+) create mode 100644 profile_examples/arc_fillet.txt create mode 100644 profile_examples/quadratic_fillet.txt diff --git a/profile_examples/arc_fillet.txt b/profile_examples/arc_fillet.txt new file mode 100644 index 000000000..9f1f361a4 --- /dev/null +++ b/profile_examples/arc_fillet.txt @@ -0,0 +1,5 @@ +H straight_len ; +a radius,radius {degrees(phi)} 0 1 + {(radius / tan(phi/2)) * (1 - cos(phi))}, {(radius / tan(phi/2)) * sin(phi)} + n = 10 +l {- straight_len * cos(phi)}, {straight_len * sin(phi)} \ 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 000000000..eeaf8cb99 --- /dev/null +++ b/profile_examples/quadratic_fillet.txt @@ -0,0 +1,3 @@ +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 -- GitLab From a1775fae6299f8b58a50bc9234b67d1cd4baa847 Mon Sep 17 00:00:00 2001 From: Ilya Portnov Date: Fri, 14 Jun 2019 21:14:55 +0500 Subject: [PATCH 22/47] Support JSON export/import. --- nodes/generators_extended/profile_mk3.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/nodes/generators_extended/profile_mk3.py b/nodes/generators_extended/profile_mk3.py index 34db5387b..ea83b0ffc 100644 --- a/nodes/generators_extended/profile_mk3.py +++ b/nodes/generators_extended/profile_mk3.py @@ -1436,6 +1436,21 @@ class SvProfileNodeMK3(bpy.types.Node, SverchCustomTreeNode): 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, -- GitLab From 9be752c4410bbeeefdc148f1e7361b7c49903f34 Mon Sep 17 00:00:00 2001 From: Ilya Portnov Date: Fri, 14 Jun 2019 22:34:13 +0500 Subject: [PATCH 23/47] Add example. --- profile_examples/pawn.txt | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 profile_examples/pawn.txt diff --git a/profile_examples/pawn.txt b/profile_examples/pawn.txt new file mode 100644 index 000000000..11a66c5ee --- /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 -- GitLab From bd2ade65b53c263f1cad99e39f024ac7c827c35d Mon Sep 17 00:00:00 2001 From: Ilya Portnov Date: Fri, 14 Jun 2019 22:34:24 +0500 Subject: [PATCH 24/47] fix in "from selection" operator. --- nodes/generators_extended/profile_mk3.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/nodes/generators_extended/profile_mk3.py b/nodes/generators_extended/profile_mk3.py index ea83b0ffc..6bc161914 100644 --- a/nodes/generators_extended/profile_mk3.py +++ b/nodes/generators_extended/profile_mk3.py @@ -1137,7 +1137,6 @@ class SvPrifilizerMk3(bpy.types.Operator): 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) - values += self.curve_points_count() if curve: values += '\n' out_points.append(hr[:]) @@ -1176,7 +1175,6 @@ class SvPrifilizerMk3(bpy.types.Operator): 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 += self.curve_points_count() values += ' 0 ' values += '\n' out_points.append(hr[:]) -- GitLab From 01fcb673486efcc17009bb767f0a45ce2229d3d9 Mon Sep 17 00:00:00 2001 From: Ilya Portnov Date: Fri, 14 Jun 2019 22:49:06 +0500 Subject: [PATCH 25/47] Add example. Update examples. --- profile_examples/quadratic_goggles.txt | 9 +++++++++ profile_examples/smooth_curve.txt | 14 +++++++------- profile_examples/zeffi_1.txt | 18 +++++++++--------- 3 files changed, 25 insertions(+), 16 deletions(-) create mode 100644 profile_examples/quadratic_goggles.txt diff --git a/profile_examples/quadratic_goggles.txt b/profile_examples/quadratic_goggles.txt new file mode 100644 index 000000000..bcc70e222 --- /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/smooth_curve.txt b/profile_examples/smooth_curve.txt index 41afb6721..3e7c01bc1 100644 --- a/profile_examples/smooth_curve.txt +++ b/profile_examples/smooth_curve.txt @@ -1,9 +1,9 @@ M 0,0 -C 1,1 3,1 4,0 20 -s 3,-1 4,0 20 -s 3,1 4,0 20 -s 1,-3 0,-4 20 -s -3,-1 -4,0 20 -s -3,1 -4,0 20 -s -3,-1 -4,0 20 +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/zeffi_1.txt b/profile_examples/zeffi_1.txt index 1077f630c..0a23406fa 100644 --- a/profile_examples/zeffi_1.txt +++ b/profile_examples/zeffi_1.txt @@ -1,18 +1,18 @@ M -1,0 L -1,2 -A 1,1 0 0 0 0,3 10 +A 1,1 0 0 0 0,3 n=10 L 2,3 -A 2,2 0 0 1 4,5 10 +A 2,2 0 0 1 4,5 l 0,4 -a 2,3 0 0 0 -2,3 10 -a 2,3 0 0 1 -2,3 10 -a 2,2 0 0 1 -2,-2 10 +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 10 +c 0,-1 2,1 0,-1 l -2,0 -c -2,0 0,0 0,-1 10 -c 0,-1 2,0 0,-1 10 -a 2,1 0 0 0 -2,-1 10 +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 -- GitLab From 1bed5e4a8ca8798583926575731f943b8895df81 Mon Sep 17 00:00:00 2001 From: Ilya Portnov Date: Sat, 15 Jun 2019 11:10:55 +0500 Subject: [PATCH 26/47] Support multiple segments for C/c, S/s commands. Support multisegment for Q/q, T/t commands. --- nodes/generators_extended/profile_mk3.py | 513 +++++++++++++---------- 1 file changed, 301 insertions(+), 212 deletions(-) diff --git a/nodes/generators_extended/profile_mk3.py b/nodes/generators_extended/profile_mk3.py index 6bc161914..4db860d1f 100644 --- a/nodes/generators_extended/profile_mk3.py +++ b/nodes/generators_extended/profile_mk3.py @@ -379,29 +379,38 @@ class VerticalLineTo(Statement): interpreter.has_last_vertex = True class CurveTo(Statement): - def __init__(self, is_abs, control1, control2, knot2, num_segments, close): + 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 __init__(self, is_abs, segments, num_segments, close): self.is_abs = is_abs - self.control1 = control1 - self.control2 = control2 - self.knot2 = knot2 + self.segments = segments self.num_segments = num_segments self.close = close def get_variables(self): variables = set() - variables.update(self.control1[0].get_variables()) - variables.update(self.control1[1].get_variables()) - variables.update(self.control2[0].get_variables()) - variables.update(self.control2[1].get_variables()) - variables.update(self.knot2[0].get_variables()) - variables.update(self.knot2[1].get_variables()) + 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" - return "{} {} {} {} n={} {}".format(letter, self.control1, self.control2, self.knot2, self.num_segments, self.close) + segments = " ".join(str(segment) for segment in self.segments) + return "{} {} n={} {}".format(letter, segments, self.num_segments, self.close) def interpret(self, interpreter, variables): vec = lambda v: Vector((v[0], v[1], 0)) @@ -415,74 +424,88 @@ class CurveTo(Statement): else: v0_index = interpreter.new_vertex(*v0) - knot1 = interpreter.position - handle1 = interpreter.calc_vertex(self.is_abs, self.control1[0], self.control1[1], variables) + 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 - # 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. + handle1 = interpreter.calc_vertex(self.is_abs, segment.control1[0], segment.control1[1], variables) - #interpreter.position = handle1 - handle2 = interpreter.calc_vertex(self.is_abs, self.control2[0], self.control2[1], variables) - #interpreter.position = handle2 - knot2 = interpreter.calc_vertex(self.is_abs, self.knot2[0], self.knot2[1], variables) - interpreter.position = knot2 + # 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. - if self.num_segments is not None: - r = self.num_segments.eval_(variables) - else: - r = interpreter.dflt_num_verts + #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) - points = interpolate_bezier(vec(knot1), vec(handle1), vec(handle2), vec(knot2), r) + if self.num_segments is not None: + r = self.num_segments.eval_(variables) + else: + r = interpreter.dflt_num_verts - interpreter.new_knot("C#.h1", *handle1) - interpreter.new_knot("C#.h2", *handle2) - interpreter.new_knot("C#.k", *knot2) + points = interpolate_bezier(vec(knot1), vec(handle1), vec(handle2), vec(knot2), r) - interpreter.prev_bezier_knot = handle2 + interpreter.new_knot("C#.{}.h1".format(i), *handle1) + interpreter.new_knot("C#.{}.h2".format(i), *handle2) + interpreter.new_knot("C#.{}.k".format(i), *knot2) - 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 + 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 + + interpreter.position = knot2 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): +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 __init__(self, is_abs, segments, num_segments, 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.segments = segments + self.num_segments = num_segments 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()) + 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 = "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) + 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 interpret(self, interpreter, variables): + vec = lambda v: Vector((v[0], v[1], 0)) + interpreter.assert_not_closed() interpreter.start_new_segment() @@ -492,62 +515,90 @@ class ArcTo(Statement): else: v0_index = interpreter.new_vertex(*v0) - start = complex(*v0) - rad_x_expr, rad_y_expr = self.radii - rad_x = rad_x_expr.eval_(variables) - rad_y = rad_y_expr.eval_(variables) - radius = complex(rad_x, rad_y) - xaxis_rot = self.rot.eval_(variables) - flag1 = self.flag1.eval_(variables) - flag2 = self.flag2.eval_(variables) - - # numverts, requires -1 else it means segments (21 verts is 20 segments). - if self.num_verts is not None: - num_verts = self.num_verts.eval_(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) + 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) + + if self.num_segments is not None: + r = self.num_segments.eval_(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 - 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 = knot2 - 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 SmoothCurveTo(Statement): - def __init__(self, is_abs, control2, knot2, num_segments, close): +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 __init__(self, is_abs, segments, num_segments, close): self.is_abs = is_abs - self.control2 = control2 - self.knot2 = knot2 + self.segments = segments self.num_segments = num_segments self.close = close def get_variables(self): variables = set() - variables.update(self.control2[0].get_variables()) - variables.update(self.control2[1].get_variables()) - variables.update(self.knot2[0].get_variables()) - variables.update(self.knot2[1].get_variables()) + 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 = "S" if self.is_abs else "s" - return "{} {} {} n={} {}".format(letter, self.control2, self.knot2, self.num_segments, self.close) + 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 interpret(self, interpreter, variables): vec = lambda v: Vector((v[0], v[1], 0)) @@ -561,73 +612,69 @@ class SmoothCurveTo(Statement): else: v0_index = interpreter.new_vertex(*v0) - knot1 = interpreter.position + 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, self.control2[0], self.control2[1], variables) - # interpreter.position = handle2 - knot2 = interpreter.calc_vertex(self.is_abs, self.knot2[0], self.knot2[1], variables) - interpreter.position = 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) - if self.num_segments is not None: - r = self.num_segments.eval_(variables) - else: - r = interpreter.dflt_num_verts + if self.num_segments is not None: + r = self.num_segments.eval_(variables) + else: + r = interpreter.dflt_num_verts - points = interpolate_bezier(vec(knot1), vec(handle1), vec(handle2), vec(knot2), r) + points = interpolate_quadratic_bezier(vec(knot1), vec(handle), vec(knot2), r) - interpreter.new_knot("S#.h1", *handle1) - interpreter.new_knot("S#.h2", *handle2) - interpreter.new_knot("S#.k", *knot2) + interpreter.new_knot("Q#.{}.h".format(i), *handle) + interpreter.new_knot("Q#.{}.k".format(i), *knot2) - interpreter.prev_bezier_knot = handle2 + 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 + 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 + + interpreter.position = knot2 if self.close: interpreter.new_edge(v1_index, interpreter.segment_start_index) interpreter.has_last_vertex = True -class QuadraticCurveTo(Statement): - def __init__(self, is_abs, control, knot2, num_segments, close): +class SmoothQuadraticCurveTo(Statement): + class Segment(object): + def __init__(self, knot2): + self.knot2 = knot2 + + def __repr__(self): + return str(self.knot2) + + def __init__(self, is_abs, segments, num_segments, close): self.is_abs = is_abs - self.control = control - self.knot2 = knot2 + self.segments = segments self.num_segments = num_segments self.close = close def get_variables(self): variables = set() - variables.update(self.control[0].get_variables()) - variables.update(self.control[1].get_variables()) - variables.update(self.knot2[0].get_variables()) - variables.update(self.knot2[1].get_variables()) + 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 = "Q" if self.is_abs else "q" - return "{} {} {} n={} {}".format(letter, self.control, self.knot2, self.num_segments, self.close) + 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 interpret(self, interpreter, variables): vec = lambda v: Vector((v[0], v[1], 0)) @@ -641,55 +688,84 @@ class QuadraticCurveTo(Statement): else: v0_index = interpreter.new_vertex(*v0) - knot1 = interpreter.position - handle = interpreter.calc_vertex(self.is_abs, self.control[0], self.control[1], variables) - knot2 = interpreter.calc_vertex(self.is_abs, self.knot2[0], self.knot2[1], variables) - interpreter.position = knot2 + 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) + + if self.num_segments is not None: + r = self.num_segments.eval_(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.num_segments is not None: - r = self.num_segments.eval_(variables) - else: - r = interpreter.dflt_num_verts - - points = interpolate_quadratic_bezier(vec(knot1), vec(handle), vec(knot2), r) - - interpreter.new_knot("Q#.h", *handle) - interpreter.new_knot("Q#.k", *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 + interpreter.position = knot2 if self.close: interpreter.new_edge(v1_index, interpreter.segment_start_index) interpreter.has_last_vertex = True -class SmoothQuadraticCurveTo(Statement): - def __init__(self, is_abs, knot2, num_segments, close): +class ArcTo(Statement): + def __init__(self, is_abs, radii, rot, flag1, flag2, end, num_verts, close): self.is_abs = is_abs - self.knot2 = knot2 - self.num_segments = num_segments + 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.knot2[0].get_variables()) - variables.update(self.knot2[1].get_variables()) - if self.num_segments: - variables.update(self.num_segments.get_variables()) + 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 = "T" if self.is_abs else "t" - return "{} {} n={} {}".format(letter, self.knot2, self.num_segments, self.close) + 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 interpret(self, interpreter, variables): - vec = lambda v: Vector((v[0], v[1], 0)) - interpreter.assert_not_closed() interpreter.start_new_segment() @@ -699,42 +775,36 @@ class SmoothQuadraticCurveTo(Statement): else: v0_index = interpreter.new_vertex(*v0) - knot1 = interpreter.position - - 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, self.knot2[0], self.knot2[1], variables) - interpreter.position = knot2 + start = complex(*v0) + rad_x_expr, rad_y_expr = self.radii + rad_x = rad_x_expr.eval_(variables) + rad_y = rad_y_expr.eval_(variables) + radius = complex(rad_x, rad_y) + xaxis_rot = self.rot.eval_(variables) + flag1 = self.flag1.eval_(variables) + flag2 = self.flag2.eval_(variables) - if self.num_segments is not None: - r = self.num_segments.eval_(variables) + # numverts, requires -1 else it means segments (21 verts is 20 segments). + if self.num_verts is not None: + num_verts = self.num_verts.eval_(variables) else: - r = interpreter.dflt_num_verts - - points = interpolate_quadratic_bezier(vec(knot1), vec(handle), vec(knot2), r) + num_verts = interpreter.dflt_num_verts + num_verts -= 1 - interpreter.new_knot("T#.h", *handle) - interpreter.new_knot("T#.k", *knot2) + end = interpreter.calc_vertex(self.is_abs, self.end[0], self.end[1], variables) + end = complex(*end) - interpreter.prev_quad_bezier_knot = handle + arc = Arc(start, radius, xaxis_rot, flag1, flag2, end) - for point in points[1:]: - v1_index = interpreter.new_vertex(point.x, point.y) + 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) @@ -836,46 +906,65 @@ def parse_parameter(name): 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"), - parse_pair, - parse_pair, - parse_pair, + many(parse_segment), optional(parse_parameter("n")), optional(parse_word("z")), optional(parse_semicolon) ) - for (is_abs, control1, control2, knot2, num_segments, z, _), rest in parser(src): - yield CurveTo(is_abs, control1, control2, knot2, num_segments, z is not None), rest + 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"), - parse_pair, - parse_pair, + many(parse_segment), optional(parse_parameter("n")), optional(parse_word("z")), optional(parse_semicolon) ) - for (is_abs, control2, knot2, num_segments, z, _), rest in parser(src): - yield SmoothCurveTo(is_abs, control2, knot2, num_segments, z is not None), rest + 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"), - parse_pair, - parse_pair, + many(parse_segment), optional(parse_parameter("n")), optional(parse_word("z")), optional(parse_semicolon) ) - for (is_abs, control, knot2, num_segments, z, _), rest in parser(src): - yield QuadraticCurveTo(is_abs, control, knot2, num_segments, z is not None), rest + 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"), - parse_pair, + many(parse_segment), optional(parse_parameter("n")), optional(parse_word("z")), optional(parse_semicolon) -- GitLab From cd9cab1d70279fab16fb000adaf8843fc3428c22 Mon Sep 17 00:00:00 2001 From: Ilya Portnov Date: Sat, 15 Jun 2019 12:43:14 +0500 Subject: [PATCH 27/47] Add examples. --- profile_examples/quadratic_bubble.txt | 4 ++++ profile_examples/smooth_arrow.txt | 4 ++++ 2 files changed, 8 insertions(+) create mode 100644 profile_examples/quadratic_bubble.txt create mode 100644 profile_examples/smooth_arrow.txt diff --git a/profile_examples/quadratic_bubble.txt b/profile_examples/quadratic_bubble.txt new file mode 100644 index 000000000..64ea80c08 --- /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/smooth_arrow.txt b/profile_examples/smooth_arrow.txt new file mode 100644 index 000000000..b8a87dba0 --- /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 -- GitLab From d89b8a128916ad7f7dc263aed7602ab4eb0ef17c Mon Sep 17 00:00:00 2001 From: Ilya Portnov Date: Sat, 15 Jun 2019 12:59:36 +0500 Subject: [PATCH 28/47] Update documentation. Update documentation. --- .../nodes/generators_extended/profile_mk3.rst | 137 ++++++++++++++---- nodes/generators_extended/profile_mk3.py | 9 +- 2 files changed, 117 insertions(+), 29 deletions(-) diff --git a/docs/nodes/generators_extended/profile_mk3.rst b/docs/nodes/generators_extended/profile_mk3.rst index 1a149362d..163663721 100644 --- a/docs/nodes/generators_extended/profile_mk3.rst +++ b/docs/nodes/generators_extended/profile_mk3.rst @@ -3,7 +3,10 @@ Profile Parametric Node ======================= -**Profile Node** implements a useful subset of the SVG path section commands. +**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. + Currently the following segment types are available: +---------------+-------+--------------------------------------------------------------------------------+ @@ -11,15 +14,19 @@ Currently the following segment types are available: +===============+=======+================================================================================+ | MoveTo | M, m | <2v coordinate> | +---------------+-------+--------------------------------------------------------------------------------+ -| LineTo | L, l | <2v coordinate 1> <2v coordinate 2> <2v coordinate n> [z] | +| LineTo | L, l | (<2v coordinate>)+ [z] | ++---------------+-------+--------------------------------------------------------------------------------+ +| HorLineTo | H, h | ()+ ";" | +---------------+-------+--------------------------------------------------------------------------------+ -| HorLineTo | H, h | ... ; | +| VertLineTo | V, v | ()+ ";" | +---------------+-------+--------------------------------------------------------------------------------+ -| VertLineTo | V, v | ... ; | +| CurveTo | C, c | (<2v control1> <2v control2> <2v knot2>)+ ["n = " num_verts] [z] | +---------------+-------+--------------------------------------------------------------------------------+ -| CurveTo | C, c | <2v control1> <2v control2> <2v knot2> ["n = " num_verts] [z] | +| SmoothCurveTo | S, s | (<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] | +---------------+-------+--------------------------------------------------------------------------------+ @@ -37,8 +44,10 @@ Currently the following segment types are available: - 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 @@ -57,11 +66,64 @@ Each integer or floating value may be represented as * 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. + For examples, see "Examples of usage" section below, or `profile_examples` directory in Sverchok distribution. Statements may optionally be separated by semicolons (`;`). For some commands (namely: `H`/`h`, `V`/`v`) the trailing semicolon is **required**! +.. _specification: https://www.w3.org/TR/SVG/paths.html + +Expression syntax +----------------- + +Syntax being used for formulas 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 JSON 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 becomes one input. If there are no +variables used in profile, then this node will have no inputs. + Parameters ---------- @@ -74,14 +136,25 @@ This node has the following parameters: - **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, - generated by **from selection** operator (see num_verts in description - above). Default value is 20. This parameter is available only 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 --------- @@ -102,8 +175,9 @@ 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, and we always start the profile with a M ,. +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 ,. :: @@ -111,13 +185,6 @@ LineTo command accepts many points, and we always start the profile with a M `_ +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://cloud.githubusercontent.com/assets/619340/3905771/193b5d86-22ec-11e4-93e5-724863a30bbc.png +.. 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://cloud.githubusercontent.com/assets/619340/3895396/81f3b96c-224d-11e4-9ca7-f07756f40a0e.png +.. 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 Gotchas ------- @@ -168,9 +254,10 @@ The update mechanism doesn't process inputs or anything until the following cond 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, +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/nodes/generators_extended/profile_mk3.py b/nodes/generators_extended/profile_mk3.py index 4db860d1f..cc17b7403 100644 --- a/nodes/generators_extended/profile_mk3.py +++ b/nodes/generators_extended/profile_mk3.py @@ -41,10 +41,10 @@ input like: 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] + 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 ... ; @@ -53,6 +53,7 @@ input like: ----- <> : 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 -- GitLab From d2713e10b4b8e710d7c6af13f5b16b681ac16a1b Mon Sep 17 00:00:00 2001 From: Ilya Portnov Date: Sat, 15 Jun 2019 19:01:44 +0500 Subject: [PATCH 29/47] Support default values and let-expressions. --- nodes/generators_extended/profile_mk3.py | 137 ++++++++++++++++++----- 1 file changed, 109 insertions(+), 28 deletions(-) diff --git a/nodes/generators_extended/profile_mk3.py b/nodes/generators_extended/profile_mk3.py index cc17b7403..c1999a240 100644 --- a/nodes/generators_extended/profile_mk3.py +++ b/nodes/generators_extended/profile_mk3.py @@ -238,7 +238,15 @@ class NegatedVariable(Variable): # edges according to statement parameters, and pass them to the interpreter. class Statement(object): - pass + + 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): @@ -329,7 +337,7 @@ class HorizontalLineTo(Statement): for i, x_expr in enumerate(self.xs): x0,y0 = interpreter.position - x = x_expr.eval_(variables) + x = interpreter.eval_(x_expr, variables) if not self.is_abs: x = x0 + x v1 = (x, y0) @@ -367,7 +375,7 @@ class VerticalLineTo(Statement): for i, y_expr in enumerate(self.ys): x0,y0 = interpreter.position - y = y_expr.eval_(variables) + y = interpreter.eval_(y_expr, variables) if not self.is_abs: y = y0 + y v1 = (x0, y) @@ -449,7 +457,7 @@ class CurveTo(Statement): knot2 = interpreter.calc_vertex(self.is_abs, segment.knot2[0], segment.knot2[1], variables) if self.num_segments is not None: - r = self.num_segments.eval_(variables) + r = interpreter.eval_(self.num_segments, variables) else: r = interpreter.dflt_num_verts @@ -546,7 +554,7 @@ class SmoothCurveTo(Statement): knot2 = interpreter.calc_vertex(self.is_abs, segment.knot2[0], segment.knot2[1], variables) if self.num_segments is not None: - r = self.num_segments.eval_(variables) + r = interpreter.eval_(self.num_segments, variables) else: r = interpreter.dflt_num_verts @@ -626,7 +634,7 @@ class QuadraticCurveTo(Statement): knot2 = interpreter.calc_vertex(self.is_abs, segment.knot2[0], segment.knot2[1], variables) if self.num_segments is not None: - r = self.num_segments.eval_(variables) + r = interpreter.eval_(self.num_segments, variables) else: r = interpreter.dflt_num_verts @@ -715,7 +723,7 @@ class SmoothQuadraticCurveTo(Statement): knot2 = interpreter.calc_vertex(self.is_abs, segment.knot2[0], segment.knot2[1], variables) if self.num_segments is not None: - r = self.num_segments.eval_(variables) + r = interpreter.eval_(self.num_segments, variables) else: r = interpreter.dflt_num_verts @@ -778,16 +786,16 @@ class ArcTo(Statement): start = complex(*v0) rad_x_expr, rad_y_expr = self.radii - rad_x = rad_x_expr.eval_(variables) - rad_y = rad_y_expr.eval_(variables) + rad_x = interpreter.eval_(rad_x_expr, variables) + rad_y = interpreter.eval_(rad_y_expr, variables) radius = complex(rad_x, rad_y) - xaxis_rot = self.rot.eval_(variables) - flag1 = self.flag1.eval_(variables) - flag2 = self.flag2.eval_(variables) + 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 = self.num_verts.eval_(variables) + num_verts = interpreter.eval_(self.num_verts, variables) else: num_verts = interpreter.dflt_num_verts num_verts -= 1 @@ -839,6 +847,34 @@ class Close(Statement): interpreter.new_edge(v1_index, 0) interpreter.closed = True +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 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 get_hidden_inputs(self): + return set([self.name]) + ######################################### # DSL parsing ######################################### @@ -1006,7 +1042,19 @@ def parse_VertLineTo(src): parse_Close = parse_word("X", Close()) +def parse_Default(src): + parser = sequence(parse_word("default"), parse_identifier, parse_word("="), parse_value, 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, 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, @@ -1046,7 +1094,7 @@ def parse_profile(src): # * Contains the interpret() method, which runs the whole interpretation process. class Interpreter(object): - def __init__(self, node): + def __init__(self, node, input_names): self.position = (0, 0) self.next_vertex_index = 0 self.segment_start_index = 0 @@ -1061,6 +1109,8 @@ class Interpreter(object): 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: @@ -1071,8 +1121,8 @@ class Interpreter(object): return x0+x, y0+y def calc_vertex(self, is_abs, x_expr, y_expr, variables): - x = x_expr.eval_(variables) - y = y_expr.eval_(variables) + x = self.eval_(x_expr, variables) + y = self.eval_(y_expr, variables) if is_abs: return x,y else: @@ -1106,10 +1156,19 @@ class Interpreter(object): 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) ################################# @@ -1435,7 +1494,20 @@ class SvProfileNodeMK3(bpy.types.Node, SverchCustomTreeNode): 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() @@ -1484,7 +1556,11 @@ class SvProfileNodeMK3(bpy.types.Node, SverchCustomTreeNode): 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 = [] @@ -1492,25 +1568,30 @@ class SvProfileNodeMK3(bpy.types.Node, SverchCustomTreeNode): result_knots = [] result_names = [] - profile = self.load_profile() - if var_names: - try: - input_values = [inputs[name] for name in var_names] - except KeyError as e: - name = e.args[0] - if name in self.inputs: - raise SvNoDataError(self.inputs[name]) - else: - self.adjust_sockets() - raise SvNoDataError(self.inputs[name]) + 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) + interpreter = Interpreter(self, input_names) interpreter.interpret(profile, variables) verts = self.extend_out_verts(interpreter.vertices) result_vertices.append(verts) -- GitLab From ec7aee992528ebe1d4b4191269feae1ab756df3c Mon Sep 17 00:00:00 2001 From: Ilya Portnov Date: Sat, 15 Jun 2019 19:02:05 +0500 Subject: [PATCH 30/47] Update examples. Update documentation. Add example. --- .../nodes/generators_extended/profile_mk3.rst | 56 ++++++++++++++++--- nodes/generators_extended/profile_mk3.py | 36 +++++++++++- profile_examples/arc_fillet.txt | 9 ++- profile_examples/quadratic_fillet.txt | 3 + profile_examples/symmetric_square.txt | 5 ++ 5 files changed, 95 insertions(+), 14 deletions(-) create mode 100644 profile_examples/symmetric_square.txt diff --git a/docs/nodes/generators_extended/profile_mk3.rst b/docs/nodes/generators_extended/profile_mk3.rst index 163663721..5be54b1eb 100644 --- a/docs/nodes/generators_extended/profile_mk3.rst +++ b/docs/nodes/generators_extended/profile_mk3.rst @@ -7,7 +7,24 @@ Profile Parametric Node 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. -Currently the following segment types are available: +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 | @@ -78,10 +95,12 @@ 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. -For examples, see "Examples of usage" section below, or `profile_examples` directory in Sverchok distribution. +**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. -Statements may optionally be separated by semicolons (`;`). -For some commands (namely: `H`/`h`, `V`/`v`) the trailing semicolon is **required**! +For examples, see "Examples of usage" section below, or `profile_examples` +directory in Sverchok distribution. .. _specification: https://www.w3.org/TR/SVG/paths.html @@ -121,8 +140,9 @@ Inputs ------ Set of inputs for this node depends on expressions used in the profile -definition. Each variable used in profile becomes one input. If there are no -variables used in profile, then this node will have no inputs. +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 ---------- @@ -210,8 +230,8 @@ or X -More Info ---------- +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 `_ ; @@ -242,12 +262,30 @@ Example usage: 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: - * Profile Node has at least one input socket connected + * 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. diff --git a/nodes/generators_extended/profile_mk3.py b/nodes/generators_extended/profile_mk3.py index c1999a240..6e5069ed5 100644 --- a/nodes/generators_extended/profile_mk3.py +++ b/nodes/generators_extended/profile_mk3.py @@ -39,6 +39,9 @@ from sverchok.utils.sv_update_utils import sv_get_local_path ''' 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] @@ -72,6 +75,17 @@ input like: * 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*! @@ -82,10 +96,14 @@ input like: Our DSL has relatively simple BNF: ::= * - ::= | | | + ::= | + | | | | | | | | | | "X" + ::= "default" "=" + ::= "let" "=" + ::= ("M" | "m") "," ::= ... ::= ... @@ -1043,12 +1061,24 @@ def parse_VertLineTo(src): parse_Close = parse_word("X", Close()) def parse_Default(src): - parser = sequence(parse_word("default"), parse_identifier, parse_word("="), parse_value, parse_semicolon) + 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, parse_semicolon) + 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 diff --git a/profile_examples/arc_fillet.txt b/profile_examples/arc_fillet.txt index 9f1f361a4..09d3b68d3 100644 --- a/profile_examples/arc_fillet.txt +++ b/profile_examples/arc_fillet.txt @@ -1,5 +1,10 @@ +default straight_len = 1; +default radius = 0.4; + +let rem = {radius / tan(phi/2)}; + H straight_len ; -a radius,radius {degrees(phi)} 0 1 - {(radius / tan(phi/2)) * (1 - cos(phi))}, {(radius / tan(phi/2)) * sin(phi)} +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/quadratic_fillet.txt b/profile_examples/quadratic_fillet.txt index eeaf8cb99..cbd4e0f76 100644 --- a/profile_examples/quadratic_fillet.txt +++ b/profile_examples/quadratic_fillet.txt @@ -1,3 +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/symmetric_square.txt b/profile_examples/symmetric_square.txt new file mode 100644 index 000000000..86334cbac --- /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 -- GitLab From 30c11aa27ffd2c721368800783b07d6d25eccdcf Mon Sep 17 00:00:00 2001 From: Ilya Portnov Date: Sat, 15 Jun 2019 20:15:35 +0500 Subject: [PATCH 31/47] Split ProfileMk3 node implementation into several modules. --- nodes/generators_extended/profile_mk3.py | 1093 +--------------------- utils/modules/profile_mk3/__init__.py | 3 + utils/modules/profile_mk3/interpreter.py | 888 ++++++++++++++++++ utils/modules/profile_mk3/parser.py | 243 +++++ 4 files changed, 1138 insertions(+), 1089 deletions(-) create mode 100644 utils/modules/profile_mk3/__init__.py create mode 100644 utils/modules/profile_mk3/interpreter.py create mode 100644 utils/modules/profile_mk3/parser.py diff --git a/nodes/generators_extended/profile_mk3.py b/nodes/generators_extended/profile_mk3.py index 6e5069ed5..a930eec10 100644 --- a/nodes/generators_extended/profile_mk3.py +++ b/nodes/generators_extended/profile_mk3.py @@ -16,26 +16,20 @@ # # ##### END GPL LICENSE BLOCK ##### -import re -from itertools import chain -import ast -from math import * import os import bpy from bpy.props import BoolProperty, StringProperty, EnumProperty, FloatProperty, IntProperty -from mathutils.geometry import interpolate_bezier -from mathutils import Vector, Matrix from sverchok.node_tree import SverchCustomTreeNode from sverchok.core.socket_data import SvNoDataError -from sverchok.data_structure import fullList, updateNode, dataCorrect, match_long_repeat -from sverchok.utils.parsec import * +from sverchok.data_structure import updateNode, match_long_repeat from sverchok.utils.logging import info, debug, warning -from sverchok.utils.geom import interpolate_quadratic_bezier -from sverchok.utils.sv_curve_utils import Arc 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: @@ -122,1085 +116,6 @@ Our DSL has relatively simple BNF: """ -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) - - @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 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 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) - -############################################ -# 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 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 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 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 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 __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 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) - - 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 - - interpreter.position = knot2 - - 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 __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 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) - - 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 - - interpreter.position = knot2 - - 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 __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 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) - - 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 - - interpreter.position = knot2 - - 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 __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 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) - - 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 - - interpreter.position = knot2 - - 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 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 Close(Statement): - def __init__(self): - pass - - def __repr__(self): - return "X" - - 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 Default(Statement): - def __init__(self, name, value): - self.name = name - self.value = value - - def __repr__(self): - return "default {} = {}".format(self.name, self.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 get_hidden_inputs(self): - return set([self.name]) - -######################################### -# DSL parsing -######################################### - -# Compare these definitions with BNF definition at the top - -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_Close = parse_word("X", Close()) - -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_Close) - -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 - -################################# -# 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.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) - ################################# # "From Selection" Operator ################################# diff --git a/utils/modules/profile_mk3/__init__.py b/utils/modules/profile_mk3/__init__.py new file mode 100644 index 000000000..606a6426a --- /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 000000000..e5b1270b1 --- /dev/null +++ b/utils/modules/profile_mk3/interpreter.py @@ -0,0 +1,888 @@ +# ##### 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) + + @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 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 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) + +############################################ +# 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 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 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 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 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 __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 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) + + 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 + + interpreter.position = knot2 + + 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 __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 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) + + 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 + + interpreter.position = knot2 + + 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 __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 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) + + 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 + + interpreter.position = knot2 + + 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 __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 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) + + 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 + + interpreter.position = knot2 + + 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 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 Close(Statement): + def __init__(self): + pass + + def __repr__(self): + return "X" + + 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 Default(Statement): + def __init__(self, name, value): + self.name = name + self.value = value + + def __repr__(self): + return "default {} = {}".format(self.name, self.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 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.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 000000000..223eeea76 --- /dev/null +++ b/utils/modules/profile_mk3/parser.py @@ -0,0 +1,243 @@ +# ##### 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 + +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_Close = parse_word("X", Close()) + +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_Close) + +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 + -- GitLab From 8b948cc3939b7bdb61a3d3215c143803ec74af82 Mon Sep 17 00:00:00 2001 From: Ilya Portnov Date: Sat, 15 Jun 2019 21:08:42 +0500 Subject: [PATCH 32/47] Add unit tests. --- tests/profile_mk3_tests.py | 91 ++++++++++++++++++++ utils/modules/profile_mk3/interpreter.py | 105 ++++++++++++++++++++++- 2 files changed, 195 insertions(+), 1 deletion(-) create mode 100644 tests/profile_mk3_tests.py diff --git a/tests/profile_mk3_tests.py b/tests/profile_mk3_tests.py new file mode 100644 index 000000000..2e540ea1f --- /dev/null +++ b/tests/profile_mk3_tests.py @@ -0,0 +1,91 @@ + +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) + + # 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/modules/profile_mk3/interpreter.py b/utils/modules/profile_mk3/interpreter.py index e5b1270b1..7a4c8a094 100644 --- a/utils/modules/profile_mk3/interpreter.py +++ b/utils/modules/profile_mk3/interpreter.py @@ -58,10 +58,14 @@ class Expression(object): 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: @@ -98,6 +102,9 @@ class Const(Expression): 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 @@ -115,6 +122,9 @@ class Variable(Expression): 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: @@ -145,6 +155,9 @@ class NegatedVariable(Variable): 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 @@ -180,6 +193,12 @@ class MoveTo(Statement): 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()) @@ -211,6 +230,12 @@ class LineTo(Statement): 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() @@ -248,6 +273,11 @@ class HorizontalLineTo(Statement): 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() @@ -286,6 +316,11 @@ class VerticalLineTo(Statement): 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() @@ -319,6 +354,11 @@ class CurveTo(Statement): 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 @@ -343,6 +383,13 @@ class CurveTo(Statement): 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)) @@ -412,6 +459,10 @@ class SmoothCurveTo(Statement): 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 @@ -434,6 +485,13 @@ class SmoothCurveTo(Statement): 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)) @@ -509,6 +567,10 @@ class QuadraticCurveTo(Statement): 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 @@ -531,6 +593,13 @@ class QuadraticCurveTo(Statement): 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)) @@ -587,6 +656,9 @@ class SmoothQuadraticCurveTo(Statement): 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 @@ -607,6 +679,13 @@ class SmoothQuadraticCurveTo(Statement): 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)) @@ -696,6 +775,17 @@ class ArcTo(Statement): 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() @@ -748,6 +838,9 @@ class Close(Statement): def __repr__(self): return "X" + def __eq__(self, other): + return isinstance(other, Close) + def get_variables(self): return set() @@ -777,6 +870,11 @@ class Default(Statement): 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() @@ -794,6 +892,11 @@ 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]) -- GitLab From e805073de4d8eff8c473c91fe45766db957459e7 Mon Sep 17 00:00:00 2001 From: Ilya Portnov Date: Sat, 15 Jun 2019 21:26:08 +0500 Subject: [PATCH 33/47] minor comment update. --- utils/modules/profile_mk3/parser.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/utils/modules/profile_mk3/parser.py b/utils/modules/profile_mk3/parser.py index 223eeea76..f48b288f4 100644 --- a/utils/modules/profile_mk3/parser.py +++ b/utils/modules/profile_mk3/parser.py @@ -26,7 +26,7 @@ from sverchok.utils.modules.profile_mk3.interpreter import * # DSL parsing ######################################### -# Compare these definitions with BNF definition at the top +# Compare these definitions with BNF definition at the top of profile_mk3.py. expr_regex = re.compile(r"({[^}]+})\s*", re.DOTALL) -- GitLab From b22b31f7b0627dce759b9be5030929e4accf3321 Mon Sep 17 00:00:00 2001 From: Ilya Portnov Date: Sat, 15 Jun 2019 23:16:09 +0500 Subject: [PATCH 34/47] Allow several X commands. --- utils/modules/profile_mk3/interpreter.py | 20 +++----------------- 1 file changed, 3 insertions(+), 17 deletions(-) diff --git a/utils/modules/profile_mk3/interpreter.py b/utils/modules/profile_mk3/interpreter.py index 7a4c8a094..a7b45e0bd 100644 --- a/utils/modules/profile_mk3/interpreter.py +++ b/utils/modules/profile_mk3/interpreter.py @@ -206,7 +206,6 @@ class MoveTo(Statement): 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 @@ -237,7 +236,6 @@ class LineTo(Statement): 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: @@ -279,7 +277,6 @@ class HorizontalLineTo(Statement): 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: @@ -322,7 +319,6 @@ class VerticalLineTo(Statement): 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: @@ -393,7 +389,6 @@ class CurveTo(Statement): 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 @@ -495,7 +490,6 @@ class SmoothCurveTo(Statement): 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 @@ -603,7 +597,6 @@ class QuadraticCurveTo(Statement): 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 @@ -689,7 +682,6 @@ class SmoothQuadraticCurveTo(Statement): 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 @@ -787,7 +779,6 @@ class ArcTo(Statement): self.close == other.close def interpret(self, interpreter, variables): - interpreter.assert_not_closed() interpreter.start_new_segment() v0 = interpreter.position @@ -845,7 +836,6 @@ class Close(Statement): 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 @@ -859,8 +849,8 @@ class Close(Statement): interpreter.pop_last_vertex() v1_index = interpreter.get_last_vertex() - interpreter.new_edge(v1_index, 0) - interpreter.closed = True + 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): @@ -918,7 +908,7 @@ class Interpreter(object): 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 = [] @@ -930,10 +920,6 @@ class Interpreter(object): 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 -- GitLab From c6ab60dab4214b1e4cf30c22486f8a277b619b83 Mon Sep 17 00:00:00 2001 From: Ilya Portnov Date: Sat, 15 Jun 2019 23:16:43 +0500 Subject: [PATCH 35/47] Add examples. update example. --- profile_examples/complex_rectangle.txt | 24 ++++++++++++++++++++++++ profile_examples/i-beam.txt | 10 ++++++++++ profile_examples/zeffi_2.txt | 14 ++++++++++++++ 3 files changed, 48 insertions(+) create mode 100644 profile_examples/complex_rectangle.txt create mode 100644 profile_examples/zeffi_2.txt diff --git a/profile_examples/complex_rectangle.txt b/profile_examples/complex_rectangle.txt new file mode 100644 index 000000000..354c62640 --- /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 index 1bcc11812..304c8d652 100644 --- a/profile_examples/i-beam.txt +++ b/profile_examples/i-beam.txt @@ -1,3 +1,13 @@ +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 ; diff --git a/profile_examples/zeffi_2.txt b/profile_examples/zeffi_2.txt new file mode 100644 index 000000000..39b3a856d --- /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 -- GitLab From a33720d63fbb5b325df25978e73e92b70a2904eb Mon Sep 17 00:00:00 2001 From: Ilya Portnov Date: Sat, 15 Jun 2019 23:36:58 +0500 Subject: [PATCH 36/47] Differentiate X and x commands. --- profile_examples/zeffi_2.txt | 4 +- utils/modules/profile_mk3/interpreter.py | 50 +++++++++++++++++++++++- utils/modules/profile_mk3/parser.py | 7 +++- 3 files changed, 55 insertions(+), 6 deletions(-) diff --git a/profile_examples/zeffi_2.txt b/profile_examples/zeffi_2.txt index 39b3a856d..fc07d7f68 100644 --- a/profile_examples/zeffi_2.txt +++ b/profile_examples/zeffi_2.txt @@ -4,11 +4,11 @@ C 2,5 2,5 3,5 L 5,5 C 7,5 7,5 7,3 L 7,2 6,0 -X +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 +x \ No newline at end of file diff --git a/utils/modules/profile_mk3/interpreter.py b/utils/modules/profile_mk3/interpreter.py index a7b45e0bd..502b4724c 100644 --- a/utils/modules/profile_mk3/interpreter.py +++ b/utils/modules/profile_mk3/interpreter.py @@ -206,6 +206,7 @@ class MoveTo(Statement): 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 @@ -236,6 +237,7 @@ class LineTo(Statement): 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: @@ -277,6 +279,7 @@ class HorizontalLineTo(Statement): 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: @@ -319,6 +322,7 @@ class VerticalLineTo(Statement): 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: @@ -389,6 +393,7 @@ class CurveTo(Statement): 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 @@ -490,6 +495,7 @@ class SmoothCurveTo(Statement): 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 @@ -597,6 +603,7 @@ class QuadraticCurveTo(Statement): 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 @@ -682,6 +689,7 @@ class SmoothQuadraticCurveTo(Statement): 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 @@ -779,6 +787,7 @@ class ArcTo(Statement): self.close == other.close def interpret(self, interpreter, variables): + interpreter.assert_not_closed() interpreter.start_new_segment() v0 = interpreter.position @@ -822,7 +831,7 @@ class ArcTo(Statement): interpreter.has_last_vertex = True -class Close(Statement): +class CloseAll(Statement): def __init__(self): pass @@ -830,12 +839,13 @@ class Close(Statement): return "X" def __eq__(self, other): - return isinstance(other, Close) + 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 @@ -845,6 +855,37 @@ class Close(Statement): 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() @@ -908,6 +949,7 @@ class Interpreter(object): 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 @@ -920,6 +962,10 @@ class Interpreter(object): 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 diff --git a/utils/modules/profile_mk3/parser.py b/utils/modules/profile_mk3/parser.py index f48b288f4..702e9445c 100644 --- a/utils/modules/profile_mk3/parser.py +++ b/utils/modules/profile_mk3/parser.py @@ -187,7 +187,8 @@ def parse_VertLineTo(src): for (is_abs, ys, _), rest in parser(src): yield VerticalLineTo(is_abs, ys), rest -parse_Close = parse_word("X", Close()) +parse_CloseAll = parse_word("X", CloseAll()) +parse_ClosePath = parse_word("x", ClosePath()) def parse_Default(src): parser = sequence( @@ -223,7 +224,9 @@ parse_statement = one_of( parse_QuadCurveTo, parse_SmoothQuadCurveTo, parse_ArcTo, - parse_Close) + parse_ClosePath, + parse_CloseAll + ) parse_definition = many(parse_statement) -- GitLab From 531d5fac038fc67ee115765a213c67323332ed22 Mon Sep 17 00:00:00 2001 From: Ilya Portnov Date: Sat, 15 Jun 2019 23:38:59 +0500 Subject: [PATCH 37/47] Add tests. --- tests/profile_mk3_tests.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/tests/profile_mk3_tests.py b/tests/profile_mk3_tests.py index 2e540ea1f..d03d84ab3 100644 --- a/tests/profile_mk3_tests.py +++ b/tests/profile_mk3_tests.py @@ -69,6 +69,18 @@ class StatementParseTests(SverchokTestCase): 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): -- GitLab From 8ae8a3b2fbd05382d4a9c739ba7cb9b87aab9f02 Mon Sep 17 00:00:00 2001 From: Ilya Portnov Date: Sun, 16 Jun 2019 11:47:09 +0500 Subject: [PATCH 38/47] Update documentation. Add/update examples. --- .../nodes/generators_extended/profile_mk3.rst | 39 ++++++++++++------- profile_examples/barrel.txt | 8 ++++ profile_examples/bevel_line.txt | 2 + profile_examples/rounded_line.txt | 10 ++++- 4 files changed, 43 insertions(+), 16 deletions(-) create mode 100644 profile_examples/barrel.txt create mode 100644 profile_examples/bevel_line.txt diff --git a/docs/nodes/generators_extended/profile_mk3.rst b/docs/nodes/generators_extended/profile_mk3.rst index 5be54b1eb..7559dbb2d 100644 --- a/docs/nodes/generators_extended/profile_mk3.rst +++ b/docs/nodes/generators_extended/profile_mk3.rst @@ -47,7 +47,9 @@ The following segment types are available: +---------------+-------+--------------------------------------------------------------------------------+ | ArcTo | A, a | <2v rx,ry> <2v x,y> ["n = " num_verts] [z] | +---------------+-------+--------------------------------------------------------------------------------+ -| Close | X | | +| ClosePath | x | | ++---------------+-------+--------------------------------------------------------------------------------+ +| CloseAll | X | | +---------------+-------+--------------------------------------------------------------------------------+ | comment | # | anything after # is a comment. | +---------------+-------+--------------------------------------------------------------------------------+ @@ -78,10 +80,10 @@ 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)}` +* 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 @@ -107,12 +109,15 @@ directory in Sverchok distribution. Expression syntax ----------------- -Syntax being used for formulas is standard Python's syntax for expressions. +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)`. +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: +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, @@ -126,15 +131,19 @@ One difference with Python's syntax is that you can call only restricted number - 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 JSON definition obtained from unknown or untrusted source can potentially harm your system or data. +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) +* {x+1} +* {0.75*X + 0.25*Y} +* {R * sin(phi)} Inputs ------ @@ -284,9 +293,9 @@ 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. +* 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 diff --git a/profile_examples/barrel.txt b/profile_examples/barrel.txt new file mode 100644 index 000000000..e907afd99 --- /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 000000000..c5fb43973 --- /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/rounded_line.txt b/profile_examples/rounded_line.txt index c5fb43973..6f066a23b 100644 --- a/profile_examples/rounded_line.txt +++ b/profile_examples/rounded_line.txt @@ -1,2 +1,10 @@ -L d,rad {len-d},rad len,0 +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 -- GitLab From ff22a141a480dbbea61967c17185e52cc040ac49 Mon Sep 17 00:00:00 2001 From: Ilya Portnov Date: Sun, 16 Jun 2019 22:22:16 +0500 Subject: [PATCH 39/47] Bugfix (tested versus Inkscape and Firefox). --- utils/modules/profile_mk3/interpreter.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/utils/modules/profile_mk3/interpreter.py b/utils/modules/profile_mk3/interpreter.py index 502b4724c..275359ab4 100644 --- a/utils/modules/profile_mk3/interpreter.py +++ b/utils/modules/profile_mk3/interpreter.py @@ -424,6 +424,9 @@ class CurveTo(Statement): 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) @@ -443,8 +446,6 @@ class CurveTo(Statement): interpreter.new_edge(v0_index, v1_index) v0_index = v1_index - interpreter.position = knot2 - if self.close: interpreter.new_edge(v1_index, interpreter.segment_start_index) @@ -532,6 +533,7 @@ class SmoothCurveTo(Statement): 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) @@ -551,8 +553,6 @@ class SmoothCurveTo(Statement): interpreter.new_edge(v0_index, v1_index) v0_index = v1_index - interpreter.position = knot2 - if self.close: interpreter.new_edge(v1_index, interpreter.segment_start_index) @@ -623,6 +623,7 @@ class QuadraticCurveTo(Statement): 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) @@ -641,8 +642,6 @@ class QuadraticCurveTo(Statement): interpreter.new_edge(v0_index, v1_index) v0_index = v1_index - interpreter.position = knot2 - if self.close: interpreter.new_edge(v1_index, interpreter.segment_start_index) @@ -722,6 +721,7 @@ class SmoothQuadraticCurveTo(Statement): 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) @@ -740,8 +740,6 @@ class SmoothQuadraticCurveTo(Statement): interpreter.new_edge(v0_index, v1_index) v0_index = v1_index - interpreter.position = knot2 - if self.close: interpreter.new_edge(v1_index, interpreter.segment_start_index) -- GitLab From 419b0d984e6b1ac2da9ff0f3ade19f4c57c99ad1 Mon Sep 17 00:00:00 2001 From: Ilya Portnov Date: Sat, 21 Sep 2019 10:49:34 +0500 Subject: [PATCH 40/47] File was not picked up by cherry-pick... --- utils/parsec.py | 226 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 226 insertions(+) create mode 100644 utils/parsec.py diff --git a/utils/parsec.py b/utils/parsec.py new file mode 100644 index 000000000..0bc1725f9 --- /dev/null +++ b/utils/parsec.py @@ -0,0 +1,226 @@ +# ##### 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, backtracking=False): + """ + 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. + If backtracking is set to False, then the parser will iterate + as far as it can, even if consequential parsers will fail then. + With backtracking set to True, the parser will be able to go back + if it sees that some of consequencing parsers will fail. + """ + def parser(src): + for (value, values), rest in sequence(func, parser)(src): + yield [value] + values, rest + # Stop on first possible parsing variant? + if not backtracking: + 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 + -- GitLab From c25b73e5cfb4a3799a4f6eaa6129fa2660320576 Mon Sep 17 00:00:00 2001 From: Ilya Portnov Date: Sat, 21 Sep 2019 10:52:47 +0500 Subject: [PATCH 41/47] Update node to 2.80 standards --- index.md | 1 + nodes/generators_extended/profile_mk3.py | 42 ++++++++++++------------ 2 files changed, 22 insertions(+), 21 deletions(-) diff --git a/index.md b/index.md index 18e470996..dd211049b 100644 --- a/index.md +++ b/index.md @@ -37,6 +37,7 @@ Hilbert3dNode HilbertImageNode SvProfileNodeMK2 + SvProfileNodeMK3 SvMeshEvalNode SvGenerativeArtNode SvImageComponentsNode diff --git a/nodes/generators_extended/profile_mk3.py b/nodes/generators_extended/profile_mk3.py index a930eec10..821208f43 100644 --- a/nodes/generators_extended/profile_mk3.py +++ b/nodes/generators_extended/profile_mk3.py @@ -129,11 +129,11 @@ class SvPrifilizerMk3(bpy.types.Operator): 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) + 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): @@ -330,7 +330,7 @@ class SvProfileImportOperator(bpy.types.Operator): bl_label = "Profile mk3 load" bl_options = {'REGISTER', 'UNDO', 'INTERNAL'} - filepath = bpy.props.StringProperty() + filepath : bpy.props.StringProperty() def execute(self, context): txt = bpy.data.texts.load(self.filepath) @@ -361,7 +361,7 @@ class SvProfileNodeMK3(bpy.types.Node, SverchCustomTreeNode): axis_options = [("X", "X", "", 0), ("Y", "Y", "", 1), ("Z", "Z", "", 2)] - selected_axis = EnumProperty( + selected_axis : EnumProperty( items=axis_options, update=updateNode, name="Type of axis", description="offers basic axis output vectors X|Y|Z", default="Z") @@ -369,20 +369,20 @@ class SvProfileNodeMK3(bpy.types.Node, SverchCustomTreeNode): self.adjust_sockets() updateNode(self, context) - filename = StringProperty(default="", update=on_update) + filename : StringProperty(default="", update=on_update) - x = BoolProperty(default=True) - y = BoolProperty(default=True) + x : BoolProperty(default=True) + y : BoolProperty(default=True) - precision = IntProperty( + 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( + 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( + 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") @@ -403,23 +403,23 @@ class SvProfileNodeMK3(bpy.types.Node, SverchCustomTreeNode): layout.prop(self, "close_threshold") - layout.label("Profile Generator settings") + layout.label(text="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.label(text="Import Examples") layout.menu(SvProfileImportMenu.bl_idname) def sv_init(self, context): - self.inputs.new('StringsSocket', "a") + self.inputs.new('SvStringsSocket', "a") - self.outputs.new('VerticesSocket', "Vertices") - self.outputs.new('StringsSocket', "Edges") - self.outputs.new('VerticesSocket', "Knots") - self.outputs.new('StringsSocket', "KnotNames") + self.outputs.new('SvVerticesSocket', "Vertices") + self.outputs.new('SvStringsSocket', "Edges") + self.outputs.new('SvVerticesSocket', "Knots") + self.outputs.new('SvStringsSocket', "KnotNames") def load_profile(self): if not self.filename: @@ -465,7 +465,7 @@ class SvProfileNodeMK3(bpy.types.Node, SverchCustomTreeNode): 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) + self.inputs.new('SvStringsSocket', v) def update(self): ''' -- GitLab From 38c54281c0765508fc749e3b3f95490564488bc4 Mon Sep 17 00:00:00 2001 From: Ilya Portnov Date: Tue, 18 Jun 2019 21:25:22 +0500 Subject: [PATCH 42/47] Update documentation. --- docs/nodes/generators_extended/profile_mk3.rst | 10 ++++++---- nodes/generators_extended/profile_mk3.py | 6 +++--- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/docs/nodes/generators_extended/profile_mk3.rst b/docs/nodes/generators_extended/profile_mk3.rst index 7559dbb2d..5116236d5 100644 --- a/docs/nodes/generators_extended/profile_mk3.rst +++ b/docs/nodes/generators_extended/profile_mk3.rst @@ -31,11 +31,11 @@ The following segment types are available: +===============+=======+================================================================================+ | MoveTo | M, m | <2v coordinate> | +---------------+-------+--------------------------------------------------------------------------------+ -| LineTo | L, l | (<2v coordinate>)+ [z] | +| LineTo | L, l | (<2v coordinate>)+ ["n = " num_segments] [z] | +---------------+-------+--------------------------------------------------------------------------------+ -| HorLineTo | H, h | ()+ ";" | +| HorLineTo | H, h | ()+ ["n = " num_segments] ";" | +---------------+-------+--------------------------------------------------------------------------------+ -| VertLineTo | V, v | ()+ ";" | +| VertLineTo | V, v | ()+ ["n = " num_segments] ";" | +---------------+-------+--------------------------------------------------------------------------------+ | CurveTo | C, c | (<2v control1> <2v control2> <2v knot2>)+ ["n = " num_verts] [z] | +---------------+-------+--------------------------------------------------------------------------------+ @@ -66,7 +66,9 @@ The following segment types are available: (...)+ : 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. + ["n = " num_verts] : for curve and line commands, number of subdivisions may be specified. + For curve commands, default number of segments is specified in node settings (in the N panel). + For line commands, default number of segments is always 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 diff --git a/nodes/generators_extended/profile_mk3.py b/nodes/generators_extended/profile_mk3.py index 821208f43..3ccb485b6 100644 --- a/nodes/generators_extended/profile_mk3.py +++ b/nodes/generators_extended/profile_mk3.py @@ -37,14 +37,14 @@ input like: let name = M|m <2v coordinate> - L|l <2v coordinate 1> <2v coordinate 2> <2v coordinate n> [z] + L|l <2v coordinate 1> <2v coordinate 2> <2v coordinate n> ["n = " num_segments] [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 ... ; + H|h ... ["n = " num_segments] ; + V|v ... ["n = " num_segments] ; X # ----- -- GitLab From 58a70c0aa497fedd3f43012f479453e3aa66644e Mon Sep 17 00:00:00 2001 From: Ilya Portnov Date: Tue, 18 Jun 2019 21:28:22 +0500 Subject: [PATCH 43/47] Update tests. --- tests/profile_mk3_tests.py | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/tests/profile_mk3_tests.py b/tests/profile_mk3_tests.py index d03d84ab3..ea792fe33 100644 --- a/tests/profile_mk3_tests.py +++ b/tests/profile_mk3_tests.py @@ -48,19 +48,37 @@ class StatementParseTests(SverchokTestCase): 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) + expected = LineTo(True, [(Const(1), Const(2)), (Const(3), Const(4))], None, False) + self.assertEquals(result, expected) + + def test_parse_lineto_n(self): + string = "L 1,2 3,4 n=10" + result = parse(parse_statement, string) + expected = LineTo(True, [(Const(1), Const(2)), (Const(3), Const(4))], Const(10), 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)]) + expected = HorizontalLineTo(True, [Const(1), Const(2)], None) + self.assertEquals(result, expected) + + def test_parse_hor_lineto_n(self): + string = "H 1 2 n=10;" + result = parse(parse_statement, string) + expected = HorizontalLineTo(True, [Const(1), Const(2)], Const(10)) 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)]) + expected = VerticalLineTo(True, [Const(1), Const(2)], None) + self.assertEquals(result, expected) + + def test_parse_vert_lineto_n(self): + string = "V 1 2 n=10;" + result = parse(parse_statement, string) + expected = VerticalLineTo(True, [Const(1), Const(2)], Const(10)) self.assertEquals(result, expected) def test_parse_curveto(self): -- GitLab From 35ffffafc359f581c2a094343c567b4db621be77 Mon Sep 17 00:00:00 2001 From: Ilya Portnov Date: Tue, 18 Jun 2019 21:41:39 +0500 Subject: [PATCH 44/47] Minor bugfix. --- utils/modules/profile_mk3/interpreter.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/utils/modules/profile_mk3/interpreter.py b/utils/modules/profile_mk3/interpreter.py index 275359ab4..d1453d8e9 100644 --- a/utils/modules/profile_mk3/interpreter.py +++ b/utils/modules/profile_mk3/interpreter.py @@ -183,6 +183,22 @@ class Statement(object): def get_optional_inputs(self): return set() + def _interpolate(self, v0, v1, num_segments): + if num_segments is None or num_segments <= 1: + return [v0, v1] + dx_total, dy_total = v1[0] - v0[0], v1[1] - v0[1] + dx, dy = dx_total / float(num_segments), dy_total / float(num_segments) + x, y = v0 + dt = 1.0 / float(num_segments) + result = [] + t = 0 + for i in range(round(num_segments)): + result.append((x,y)) + x = x + dx + y = y + dy + result.append(v1) + return result + class MoveTo(Statement): def __init__(self, is_abs, x, y): self.is_abs = is_abs -- GitLab From b97b24acce04c5cae74cf936c1656550ec4abafc Mon Sep 17 00:00:00 2001 From: Ilya Portnov Date: Tue, 18 Jun 2019 21:41:46 +0500 Subject: [PATCH 45/47] Update examples. --- profile_examples/barrel.txt | 6 ++++-- profile_examples/symmetric_square.txt | 4 +++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/profile_examples/barrel.txt b/profile_examples/barrel.txt index e907afd99..be7c44f7a 100644 --- a/profile_examples/barrel.txt +++ b/profile_examples/barrel.txt @@ -1,8 +1,10 @@ +default caps_divisions = 4 let handle_x = {width/2}; let handle_y = {height/8}; -H {width/2}; +M {-width/2},0 +h width n=caps_divisions; c handle_x,handle_y handle_x,{height-handle_y} 0,height -h -width; +h -width n=caps_divisions; c -handle_x,-handle_y -handle_x,{-height+handle_y} 0,-height X diff --git a/profile_examples/symmetric_square.txt b/profile_examples/symmetric_square.txt index 86334cbac..478b40027 100644 --- a/profile_examples/symmetric_square.txt +++ b/profile_examples/symmetric_square.txt @@ -1,5 +1,7 @@ default size = 3; +default n = 4 let half = {size/2}; M half,-half -L -half,-half -half,half half,half z \ No newline at end of file +L -half,-half -half,half half,half half,-half n=n +X \ No newline at end of file -- GitLab From 8a90887cf125ce784b93f0513eebc683855ccf54 Mon Sep 17 00:00:00 2001 From: Ilya Portnov Date: Sat, 21 Sep 2019 11:03:11 +0500 Subject: [PATCH 46/47] Update from master --- utils/modules/profile_mk3/interpreter.py | 79 +++++++++++++++++------- utils/modules/profile_mk3/parser.py | 23 ++++--- 2 files changed, 72 insertions(+), 30 deletions(-) diff --git a/utils/modules/profile_mk3/interpreter.py b/utils/modules/profile_mk3/interpreter.py index d1453d8e9..5aa9e069f 100644 --- a/utils/modules/profile_mk3/interpreter.py +++ b/utils/modules/profile_mk3/interpreter.py @@ -230,9 +230,10 @@ class MoveTo(Statement): interpreter.has_last_vertex = False class LineTo(Statement): - def __init__(self, is_abs, pairs, close): + def __init__(self, is_abs, pairs, num_segments, close): self.is_abs = is_abs self.pairs = pairs + self.num_segments = num_segments self.close = close def get_variables(self): @@ -240,16 +241,19 @@ class LineTo(Statement): for x, y in self.pairs: variables.update(x.get_variables()) variables.update(y.get_variables()) + if self.num_segments: + variables.update(self.num_segments.get_variables()) return variables def __repr__(self): letter = "L" if self.is_abs else "l" - return "{} {} {}".format(letter, self.pairs, self.close) + return "{} {} n={} {}".format(letter, self.pairs, self.num_segments, self.close) def __eq__(self, other): return isinstance(other, LineTo) and \ self.is_abs == other.is_abs and \ self.pairs == other.pairs and \ + self.num_segments == other.num_segments and \ self.close == other.close def interpret(self, interpreter, variables): @@ -257,41 +261,52 @@ class LineTo(Statement): interpreter.start_new_segment() v0 = interpreter.position if interpreter.has_last_vertex: - v0_index = interpreter.get_last_vertex() + prev_index = interpreter.get_last_vertex() else: - v0_index = interpreter.new_vertex(*v0) + prev_index = interpreter.new_vertex(*v0) + + if self.num_segments is not None: + num_segments = interpreter.eval_(self.num_segments, variables) + else: + num_segments = None 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) + for vertex in self._interpolate(v0, v1, num_segments)[1:]: + v_index = interpreter.new_vertex(*vertex) + interpreter.new_edge(prev_index, v_index) + prev_index = v_index + v0 = v1 interpreter.new_knot("L#.{}".format(i), *v1) - v0_index = v1_index if self.close: - interpreter.new_edge(v1_index, interpreter.segment_start_index) + interpreter.new_edge(v_index, interpreter.segment_start_index) interpreter.has_last_vertex = True class HorizontalLineTo(Statement): - def __init__(self, is_abs, xs): + def __init__(self, is_abs, xs, num_segments): self.is_abs = is_abs self.xs = xs + self.num_segments = num_segments def get_variables(self): variables = set() for x in self.xs: variables.update(x.get_variables()) + if self.num_segments: + variables.update(self.num_segments.get_variables()) return variables def __repr__(self): letter = "H" if self.is_abs else "h" - return "{} {}".format(letter, self.xs) + return "{} {} n={};".format(letter, self.xs, self.num_segments) def __eq__(self, other): return isinstance(other, HorizontalLineTo) and \ self.is_abs == other.is_abs and \ + self.num_segments == other.num_segments and \ self.xs == other.xs def interpret(self, interpreter, variables): @@ -299,9 +314,14 @@ class HorizontalLineTo(Statement): interpreter.start_new_segment() v0 = interpreter.position if interpreter.has_last_vertex: - v0_index = interpreter.get_last_vertex() + prev_index = interpreter.get_last_vertex() else: - v0_index = interpreter.new_vertex(*v0) + prev_index = interpreter.new_vertex(*v0) + + if self.num_segments is not None: + num_segments = interpreter.eval_(self.num_segments, variables) + else: + num_segments = None for i, x_expr in enumerate(self.xs): x0,y0 = interpreter.position @@ -310,31 +330,39 @@ class HorizontalLineTo(Statement): x = x0 + x v1 = (x, y0) interpreter.position = v1 - v1_index = interpreter.new_vertex(*v1) - interpreter.new_edge(v0_index, v1_index) + verts = self._interpolate(v0, v1, num_segments) + #debug("V0 %s, v1 %s, N %s => %s", v0, v1, num_segments, verts) + for vertex in verts[1:]: + v_index = interpreter.new_vertex(*vertex) + interpreter.new_edge(prev_index, v_index) + prev_index = v_index + v0 = v1 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): + def __init__(self, is_abs, ys, num_segments): self.is_abs = is_abs self.ys = ys + self.num_segments = num_segments def get_variables(self): variables = set() for y in self.ys: variables.update(y.get_variables()) + if self.num_segments: + variables.update(self.num_segments.get_variables()) return variables def __repr__(self): letter = "V" if self.is_abs else "v" - return "{} {}".format(letter, self.ys) + return "{} {} n={};".format(letter, self.ys, self.num_segments) def __eq__(self, other): return isinstance(other, VerticalLineTo) and \ self.is_abs == other.is_abs and \ + self.num_segments == other.num_segments and \ self.ys == other.ys def interpret(self, interpreter, variables): @@ -342,9 +370,14 @@ class VerticalLineTo(Statement): interpreter.start_new_segment() v0 = interpreter.position if interpreter.has_last_vertex: - v0_index = interpreter.get_last_vertex() + prev_index = interpreter.get_last_vertex() else: - v0_index = interpreter.new_vertex(*v0) + prev_index = interpreter.new_vertex(*v0) + + if self.num_segments is not None: + num_segments = interpreter.eval_(self.num_segments, variables) + else: + num_segments = None for i, y_expr in enumerate(self.ys): x0,y0 = interpreter.position @@ -353,10 +386,12 @@ class VerticalLineTo(Statement): y = y0 + y v1 = (x0, y) interpreter.position = v1 - v1_index = interpreter.new_vertex(*v1) - interpreter.new_edge(v0_index, v1_index) + for vertex in self._interpolate(v0, v1, num_segments)[1:]: + v_index = interpreter.new_vertex(*vertex) + interpreter.new_edge(prev_index, v_index) + prev_index = v_index + v0 = v1 interpreter.new_knot("V#.{}".format(i), *v1) - v0_index = v1_index interpreter.has_last_vertex = True diff --git a/utils/modules/profile_mk3/parser.py b/utils/modules/profile_mk3/parser.py index 702e9445c..72bcc7365 100644 --- a/utils/modules/profile_mk3/parser.py +++ b/utils/modules/profile_mk3/parser.py @@ -78,10 +78,11 @@ def parse_LineTo(src): parser = sequence( parse_letter("L", "l"), many(parse_pair), + optional(parse_parameter("n")), 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 + for (is_abs, pairs, num_segments, z, _), rest in parser(src): + yield LineTo(is_abs, pairs, num_segments, z is not None), rest def parse_parameter(name): def parser(src): @@ -175,17 +176,23 @@ 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 + parser = sequence(parse_letter("H", "h"), + many(parse_value, backtracking=True), + optional(parse_parameter("n")), + parse_semicolon) + for (is_abs, xs, num_segments, _), rest in parser(src): + yield HorizontalLineTo(is_abs, xs, num_segments), 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 + parser = sequence(parse_letter("V", "v"), + many(parse_value, backtracking=True), + optional(parse_parameter("n")), + parse_semicolon) + for (is_abs, ys, num_segments, _), rest in parser(src): + yield VerticalLineTo(is_abs, ys, num_segments), rest parse_CloseAll = parse_word("X", CloseAll()) parse_ClosePath = parse_word("x", ClosePath()) -- GitLab From 4b31913c5c75cc0a921b97f8b024bb96cae4b33a Mon Sep 17 00:00:00 2001 From: Ilya Portnov Date: Sat, 21 Sep 2019 11:06:14 +0500 Subject: [PATCH 47/47] Update menu name to 2.80 standard. --- nodes/generators_extended/profile_mk3.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nodes/generators_extended/profile_mk3.py b/nodes/generators_extended/profile_mk3.py index 3ccb485b6..f35cfdb2a 100644 --- a/nodes/generators_extended/profile_mk3.py +++ b/nodes/generators_extended/profile_mk3.py @@ -317,7 +317,7 @@ profile_template_path = os.path.join(sv_path, 'profile_examples') class SvProfileImportMenu(bpy.types.Menu): bl_label = "Profile templates" - bl_idname = "SvProfileImportMenu" + bl_idname = "SV_MT_ProfileImportMenu" def draw(self, context): if context.active_node: -- GitLab