From 6b9cd6dde329da1d82463c61ee912a0badaff670 Mon Sep 17 00:00:00 2001 From: Ilya Portnov Date: Sun, 6 Nov 2022 10:46:38 +0500 Subject: [PATCH 1/3] Profile node: interpolation command --- utils/curve/knotvector.py | 20 +++- utils/curve/nurbs_solver.py | 8 ++ utils/curve/nurbs_solver_applications.py | 52 +++++----- utils/modules/profile_mk3/interpreter.py | 122 +++++++++++++++++++++++ utils/modules/profile_mk3/parser.py | 13 +++ 5 files changed, 187 insertions(+), 28 deletions(-) diff --git a/utils/curve/knotvector.py b/utils/curve/knotvector.py index 22a1c7fa8..7775150c5 100644 --- a/utils/curve/knotvector.py +++ b/utils/curve/knotvector.py @@ -81,11 +81,27 @@ def linear_resample(tknots, new_count): resampled_tknots = LinearSpline.resample(old_idxs, tknots, new_idxs) return resampled_tknots -def from_tknots(degree, tknots, n_cpts=None): +def add_one_by_resampling(knot_vector, index = None, degree = None): + n = len(knot_vector) + if index is None: + return linear_resample(knot_vector, n+1) + else: + if degree is None: + raise Exception("If index is provided, degree must be provided too") + kv_before = knot_vector[:index] + kv_after = knot_vector[index + degree + 1 :] + kv = linear_resample(knot_vector[index : index + degree + 1], degree+2) + return np.concatenate([kv_before, kv, kv_after]) + +def from_tknots(degree, tknots, include_endpoints=False, n_cpts=None): n = len(tknots) if n_cpts is None: result = [tknots[0]] * (degree+1) - for j in range(1, n - degree): + if include_endpoints: + j_min, j_max = 0, n - degree + 1 + else: + j_min, j_max = 1, n - degree + for j in range(j_min, j_max): u = tknots[j:j+degree].sum() / degree result.append(u) result.extend([tknots[-1]] * (degree+1)) diff --git a/utils/curve/nurbs_solver.py b/utils/curve/nurbs_solver.py index c03d5ce17..40302c96c 100644 --- a/utils/curve/nurbs_solver.py +++ b/utils/curve/nurbs_solver.py @@ -559,6 +559,9 @@ class SvNurbsCurveSolver(SvCurve): def get_control_points(self): return self.to_nurbs().get_control_points() + + def get_knotvector(self): + return self.knotvector def set_curve_weights(self, weights): if len(weights) != self.n_cpts: @@ -647,6 +650,11 @@ class SvNurbsCurveSolver(SvCurve): problem_type, residue, curve = self.solve_ex(implementation = implementation, logger = logger) return curve + def solve_welldetermined(self, implementation = SvNurbsMaths.NATIVE, logger = None): + problem_type, residue, curve = self.solve_ex(problem_types = {SvNurbsCurveSolver.PROBLEM_WELLDETERMINED}, + implementation = implementation, logger = logger) + return curve + def solve_ex(self, problem_types = PROBLEM_ANY, implementation = SvNurbsMaths.NATIVE, logger = None): self._init() diff --git a/utils/curve/nurbs_solver_applications.py b/utils/curve/nurbs_solver_applications.py index 4441ce156..1f8269175 100644 --- a/utils/curve/nurbs_solver_applications.py +++ b/utils/curve/nurbs_solver_applications.py @@ -109,22 +109,7 @@ def approximate_nurbs_curve(degree, n_cpts, points, weights=None, metric='DISTAN solver.add_goal(goal) return solver.solve(implementation=implementation) -def interpolate_nurbs_curve(degree, points, metric='DISTANCE', tknots=None, cyclic=False, implementation=SvNurbsMaths.NATIVE, logger=None): - """ - Interpolate points by a NURBS curve. - - Args: - degree: curve degree (usually 3 or 5). - points: points to be approximated. np.array of shape (n,3). - metric: metric to be used. - tknots: curve parameter values corresponding to points. np.array of - shape (n,). If None, these values will be calculated based on metric. - cyclic: if True, this will generate cyclic (closed) curve. - implementation: NURBS mathematics implementation. - - Returns: - an instance of SvNurbsCurve. - """ +def prepare_solver_for_interpolation(degree, points, metric='DISTANCE', tknots=None, cyclic=False): n_points = len(points) points = np.asarray(points) if points.ndim != 2: @@ -136,25 +121,40 @@ def interpolate_nurbs_curve(degree, points, metric='DISTANCE', tknots=None, cycl points = np.concatenate((points, points[0][np.newaxis])) if tknots is None: tknots = Spline.create_knots(points, metric=metric) - points_goal = SvNurbsCurvePoints(tknots, points, relative=False) solver = SvNurbsCurveSolver(degree=degree, ndim=ndim) - solver.add_goal(points_goal) + solver.add_goal(SvNurbsCurvePoints(tknots, points, relative=False)) if cyclic: k = 1.0/float(degree) tangent = k*(points[1] - points[-2]) solver.add_goal(SvNurbsCurveTangents.single(0.0, tangent)) solver.add_goal(SvNurbsCurveTangents.single(1.0, tangent)) - n_cpts = solver.guess_n_control_points() - t1 = k*tknots[0] + (1-k)*tknots[1] - t2 = k*tknots[-1] + (1-k)*tknots[-2] - tknots = np.insert(tknots, [1,-1], [t1,t2]) - knotvector = sv_knotvector.from_tknots(degree, tknots) - solver.set_curve_params(n_cpts, knotvector) + knotvector = sv_knotvector.from_tknots(degree, tknots, include_endpoints=True) else: knotvector = sv_knotvector.from_tknots(degree, tknots) - n_cpts = solver.guess_n_control_points() - solver.set_curve_params(n_cpts, knotvector) + n_cpts = solver.guess_n_control_points() + solver.set_curve_params(n_cpts, knotvector) + return solver + +def interpolate_nurbs_curve(degree, points, metric='DISTANCE', tknots=None, cyclic=False, implementation=SvNurbsMaths.NATIVE, logger=None): + """ + Interpolate points by a NURBS curve. + + Args: + degree: curve degree (usually 3 or 5). + points: points to be approximated. np.array of shape (n,3). + metric: metric to be used. + tknots: curve parameter values corresponding to points. np.array of + shape (n,). If None, these values will be calculated based on metric. + cyclic: if True, this will generate cyclic (closed) curve. + implementation: NURBS mathematics implementation. + + Returns: + an instance of SvNurbsCurve. + """ + solver = prepare_solver_for_interpolation(degree, points, + metric = metric, tknots = tknots, + cyclic = cyclic) problem_type, residue, curve = solver.solve_ex(problem_types = {SvNurbsCurveSolver.PROBLEM_WELLDETERMINED}, implementation = implementation, logger = logger) diff --git a/utils/modules/profile_mk3/interpreter.py b/utils/modules/profile_mk3/interpreter.py index 2e7be1c41..c253202cf 100644 --- a/utils/modules/profile_mk3/interpreter.py +++ b/utils/modules/profile_mk3/interpreter.py @@ -18,6 +18,7 @@ import ast from math import * +import numpy as np from mathutils.geometry import interpolate_bezier from mathutils import Vector, Matrix @@ -25,8 +26,12 @@ 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 +from sverchok.utils.nurbs_common import SvNurbsMaths from sverchok.utils.curve import SvCircle, SvLine, SvBezierCurve, SvCubicBezierCurve +import sverchok.utils.curve.knotvector as sv_knotvector from sverchok.utils.curve.nurbs import SvNurbsCurve +from sverchok.utils.curve.nurbs_solver import SvNurbsCurveControlPoints +from sverchok.utils.curve.nurbs_solver_applications import prepare_solver_for_interpolation def make_functions_dict(*functions): return dict([(function.__name__, function) for function in functions]) @@ -900,6 +905,115 @@ class ArcTo(Statement): interpreter.has_last_vertex = True +class InterpolatedCurveTo(Statement): + def __init__(self, is_abs, degree, points, num_segments, metric, is_smooth, close): + self.is_abs = is_abs + self.degree = degree + self.points = points + self.metric = metric + self.num_segments = num_segments + self.is_smooth = is_smooth + self.close = close + + def get_variables(self): + variables = set() + variables.update(self.degree.get_variables()) + for point in self.points: + variables.update(point[0].get_variables()) + variables.update(point[1].get_variables()) + if self.num_segments: + variables.update(self.num_segments.get_variables()) + return variables + + def __repr__(self): + letter = "I" if self.is_abs else "i" + points = " ".join(str(point) for point in self.points) + return f"{letter} {self.degree} {points} n={self.num_segments} {self.close}" + + def __eq__(self, other): + return isinstance(other, InterpolatedCurveTo) and \ + self.is_abs == other.is_abs and \ + self.points == other.points and \ + self.degree == other.degree and \ + self.metric == other.metric and \ + self.num_segments == other.num_segments and \ + self.is_smooth == other.is_smooth and \ + self.close == other.close + + def _make_curve(self, interpreter, degree, points): + points = np.array(points) + prev_control_point = None + if degree == 2 and interpreter.prev_quad_bezier_knot is not None: + prev_control_point = interpreter.to3d_np(interpreter.prev_quad_bezier_knot) + elif degree == 3 and interpreter.prev_bezier_knot is not None: + prev_control_point = interpreter.to3d_np(interpreter.prev_bezier_knot) + if self.is_smooth and prev_control_point is not None: + solver = prepare_solver_for_interpolation(degree, + points, + metric = self.metric, + cyclic = self.close) + pos = interpreter.to3d_np(interpreter.position) + new_control_point = pos + (pos - prev_control_point) + print(f"Prev CP: {prev_control_point}, pos: {pos}, new CP: {new_control_point}") + solver.add_goal(SvNurbsCurveControlPoints.single(1, new_control_point, relative=False)) + knotvector = solver.get_knotvector() + n_cpts = solver.guess_n_control_points() + knotvector = sv_knotvector.add_one_by_resampling(knotvector, index=1, degree=degree) + solver.set_curve_params(n_cpts, knotvector) + curve = solver.solve_welldetermined() + else: + curve = SvNurbsMaths.interpolate_curve(SvNurbsMaths.NATIVE, degree, + points, + metric = self.metric, + cyclic = self.close) + return curve + + 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) + + interpolated_points = [[v0[0], v0[1], 0.0]] + for i, pt in enumerate(self.points): + knot = interpreter.calc_vertex(self.is_abs, pt[0], pt[1], variables) + interpreter.new_knot(f"I#.{i}", knot[0], knot[1]) + interpolated_points.append([knot[0], knot[1], 0.0]) + + degree = interpreter.eval_(self.degree, variables) + curve = self._make_curve(interpreter, degree, interpolated_points) + interpreter.new_curve(curve, self) + interpreter.position = knot + + cpts = curve.get_control_points() + if degree == 2: + interpreter.prev_quad_bezier_knot = (cpts[-2][0], cpts[-2][1]) + elif degree == 3: + interpreter.prev_bezier_knot = (cpts[-2][0], cpts[-2][1]) + + if self.num_segments is not None: + r = interpreter.eval_(self.num_segments, variables) + else: + r = interpreter.dflt_num_verts + + t_min, t_max = curve.get_u_bounds() + ts = np.linspace(t_min, t_max, num = r) + points = curve.evaluate_array(ts) + + for point in points[1:]: + v1_index = interpreter.new_vertex(point[0], point[1]) + interpreter.new_edge(v0_index, v1_index) + v0_index = v1_index + + if self.close: + interpreter.close_segment(v1_index) + + interpreter.has_last_vertex = True + class CloseAll(Statement): def __init__(self): pass @@ -1052,6 +1166,14 @@ class Interpreter(object): else: # self.z_axis == 'Z': return Vector((vertex[0], vertex[1], 0)) + def to3d_np(self, vertex): + if self.z_axis == 'X': + return np.array((0, vertex[0], vertex[1])) + elif self.z_axis == 'Y': + return np.array((vertex[0], 0, vertex[1])) + else: # self.z_axis == 'Z': + return np.array((vertex[0], vertex[1], 0)) + def assert_not_closed(self): if self.closed: raise Exception("Path was already closed, will not process any further directives!") diff --git a/utils/modules/profile_mk3/parser.py b/utils/modules/profile_mk3/parser.py index 5176cf2e9..1d6ad56d0 100644 --- a/utils/modules/profile_mk3/parser.py +++ b/utils/modules/profile_mk3/parser.py @@ -194,6 +194,18 @@ def parse_VertLineTo(src): for (is_abs, ys, num_segments, _), rest in parser(src): yield VerticalLineTo(is_abs, ys, num_segments), rest +def parse_Interpolate(src): + parser = sequence( + parse_letter("@I", "@i"), + optional(parse_word("@smooth")), + parse_value, + many(parse_pair, backtracking=True), + optional(parse_parameter("n")), + optional(parse_word("z")), + parse_semicolon) + for (is_abs, smooth, degree, points, num_segments, z, _), rest in parser(src): + yield InterpolatedCurveTo(is_abs, degree, points, num_segments, 'DISTANCE', smooth is not None, z is not None), rest + parse_CloseAll = parse_word("X", CloseAll()) parse_ClosePath = parse_word("x", ClosePath()) @@ -231,6 +243,7 @@ parse_statement = one_of( parse_QuadCurveTo, parse_SmoothQuadCurveTo, parse_ArcTo, + parse_Interpolate, parse_ClosePath, parse_CloseAll ) -- GitLab From 55e5d55a735681440e1f9bf422b68e7a67f59951 Mon Sep 17 00:00:00 2001 From: Ilya Portnov Date: Mon, 28 Nov 2022 23:13:27 +0500 Subject: [PATCH 2/3] Update documentation. --- docs/nodes/script/profile_mk3.rst | 71 +++++++++++++++++++------------ 1 file changed, 43 insertions(+), 28 deletions(-) diff --git a/docs/nodes/script/profile_mk3.rst b/docs/nodes/script/profile_mk3.rst index 7562bf3fd..d0472be73 100644 --- a/docs/nodes/script/profile_mk3.rst +++ b/docs/nodes/script/profile_mk3.rst @@ -10,7 +10,7 @@ by adding some extensions and not supporting some features. Profile definition consists of a series of statements (also called commands). Statements may optionally be separated by semicolons (`;`). -For some commands (namely: `H`/`h`, `V`/`v`) the trailing semicolon is **required**! +For some commands (namely: `H`/`h`, `V`/`v`, `@i`) the trailing semicolon is **required**! There are the following statements supported: @@ -26,33 +26,35 @@ There are the following statements supported: The following segment types are available: -+---------------+-------+--------------------------------------------------------------------------------+ -| name | cmd | parameters | -+===============+=======+================================================================================+ -| MoveTo | M, m | <2v coordinate> | -+---------------+-------+--------------------------------------------------------------------------------+ -| LineTo | L, l | (<2v coordinate>)+ ["n = " num_segments] [z] | -+---------------+-------+--------------------------------------------------------------------------------+ -| HorLineTo | H, h | ()+ ["n = " num_segments] ";" | -+---------------+-------+--------------------------------------------------------------------------------+ -| VertLineTo | V, v | ()+ ["n = " num_segments] ";" | -+---------------+-------+--------------------------------------------------------------------------------+ -| CurveTo | C, c | (<2v control1> <2v control2> <2v knot2>)+ ["n = " num_verts] [z] | -+---------------+-------+--------------------------------------------------------------------------------+ -| SmoothCurveTo | S, s | (<2v control2> <2v knot2>)+ ["n = " num_verts] [z] | -+---------------+-------+--------------------------------------------------------------------------------+ -| QuadCurveTo | Q, q | (<2v control> <2v knot2>)+ ["n = " num_segments] [z] | -+---------------+-------+--------------------------------------------------------------------------------+ -| SmthQuadCurve | T, t | (<2v knot2>)+ ["n = " num_segments] [z] | -+---------------+-------+--------------------------------------------------------------------------------+ -| ArcTo | A, a | <2v rx,ry> <2v x,y> ["n = " num_verts] [z] | -+---------------+-------+--------------------------------------------------------------------------------+ -| ClosePath | x | | -+---------------+-------+--------------------------------------------------------------------------------+ -| CloseAll | X | | -+---------------+-------+--------------------------------------------------------------------------------+ -| comment | # | anything after # is a comment. | -+---------------+-------+--------------------------------------------------------------------------------+ ++---------------+--------+--------------------------------------------------------------------------------+ +| name | cmd | parameters | ++===============+========+================================================================================+ +| MoveTo | M, m | <2v coordinate> | ++---------------+--------+--------------------------------------------------------------------------------+ +| LineTo | L, l | (<2v coordinate>)+ ["n = " num_segments] [z] | ++---------------+--------+--------------------------------------------------------------------------------+ +| HorLineTo | H, h | ()+ ["n = " num_segments] ";" | ++---------------+--------+--------------------------------------------------------------------------------+ +| VertLineTo | V, v | ()+ ["n = " num_segments] ";" | ++---------------+--------+--------------------------------------------------------------------------------+ +| CurveTo | C, c | (<2v control1> <2v control2> <2v knot2>)+ ["n = " num_verts] [z] | ++---------------+--------+--------------------------------------------------------------------------------+ +| SmoothCurveTo | S, s | (<2v control2> <2v knot2>)+ ["n = " num_verts] [z] | ++---------------+--------+--------------------------------------------------------------------------------+ +| QuadCurveTo | Q, q | (<2v control> <2v knot2>)+ ["n = " num_segments] [z] | ++---------------+--------+--------------------------------------------------------------------------------+ +| SmthQuadCurve | T, t | (<2v knot2>)+ ["n = " num_segments] [z] | ++---------------+--------+--------------------------------------------------------------------------------+ +| ArcTo | A, a | <2v rx,ry> <2v x,y> ["n = " num_verts] [z] | ++---------------+--------+--------------------------------------------------------------------------------+ +| Interpolate | @I, @i | ["@smooth"] (<2v point>)+ ["n = " num_segments] [z] ";" | ++---------------+--------+--------------------------------------------------------------------------------+ +| ClosePath | x | | ++---------------+--------+--------------------------------------------------------------------------------+ +| CloseAll | X | | ++---------------+--------+--------------------------------------------------------------------------------+ +| comment | # | anything after # is a comment. | ++---------------+--------+--------------------------------------------------------------------------------+ :: @@ -95,6 +97,19 @@ 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. +`@I` / `@i` command is an extension of SVG syntax. It defines a curve as +interpolating NURBS curve through the provided list of points. Current point, +at which the pen is located at the start of command, will be considered as +first point to be interpolated. If `z` is specified in the end, the curve will +be closed (cyclic); otherwise, this command will define an open curve. If +special keyword, `@smooth`, is specified, then, while computing interpolation +curve, this command will consider previous curve segment, defined by previous +command (C/c, S/s, Q/q, T/t), if it is of the same degree, and draw the curve +in such a way that in the starting point it will have the same tangent vector, +as previous curve had in the end point; in other words, at the meeting point of +segments, the curve will be smooth. Providing `z` and `@smooth` for the same +`@i` command is not supported. + 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. -- GitLab From 7da8ea277726efc582675c9b836bbc979973d86e Mon Sep 17 00:00:00 2001 From: Ilya Portnov Date: Mon, 28 Nov 2022 23:25:09 +0500 Subject: [PATCH 3/3] Add examples to the documentation. --- docs/nodes/script/profile_mk3.rst | 39 +++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/docs/nodes/script/profile_mk3.rst b/docs/nodes/script/profile_mk3.rst index d0472be73..90ad9f6d9 100644 --- a/docs/nodes/script/profile_mk3.rst +++ b/docs/nodes/script/profile_mk3.rst @@ -327,6 +327,45 @@ An example with use of "default" and "let" statements: n = 10 l {- straight_len * cos(phi)}, {straight_len * sin(phi)} +A simple example of `@I` command used to define a quadratic interpolation curve through three points: + +.. image:: https://user-images.githubusercontent.com/284644/204350806-cba83beb-0eae-4ef5-ba42-15442029c6f2.png + +:: + + M R0,0 + @I 2 + {R0 + dR}, {0.5*H} + R0, H ; + +An example of closed interpolation curve: + +.. image:: https://user-images.githubusercontent.com/284644/204350799-6ee7e89f-0e27-45de-b523-7fae9c17eac8.png + +:: + + M 0,0 + @I 3 + 1.5,2 + 2,0 + 0,-3 + -2,0 + -1.5,2 + z ; + +An example of `@smooth` keyword usage, to smoothly continue the previous segment defined by C command: + +.. image:: https://user-images.githubusercontent.com/284644/204351808-26133e22-7b04-44ad-9c0a-40f97339a672.png + +:: + + M 0,0 + C 1,0 1,3 0,3 + @I @smooth 3 + -0.8, 2 + -1, 1 + -2, 0 ; + Gotchas ------- -- GitLab