From b72083b37b5a55b1d1f13cab0c77bd2f481f262b Mon Sep 17 00:00:00 2001 From: Dealga McArdle Date: Sat, 3 Nov 2018 14:03:23 +0100 Subject: [PATCH 01/60] adresses bug in upstream list modification --- nodes/viz/viewer_polyline_mk1.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/nodes/viz/viewer_polyline_mk1.py b/nodes/viz/viewer_polyline_mk1.py index ee6f4f164..a1d940c1b 100644 --- a/nodes/viz/viewer_polyline_mk1.py +++ b/nodes/viz/viewer_polyline_mk1.py @@ -316,8 +316,8 @@ class SvPolylineViewerNodeMK1(bpy.types.Node, SverchCustomTreeNode): return dataCorrect(data) mverts = get('vertices') - mradii = self.inputs['radii'].sv_get(deepcopy=False) - mtwist = self.inputs['twist'].sv_get(deepcopy=False) + mradii = self.inputs['radii'].sv_get(deepcopy=True) + mtwist = self.inputs['twist'].sv_get(deepcopy=True) mmtrix = get('matrix') # extend all non empty lists to longest of these -- GitLab From c92ebe097d23c2087dd8e6885d6f8f21d6869980 Mon Sep 17 00:00:00 2001 From: zeffii Date: Thu, 8 Nov 2018 15:27:25 +0100 Subject: [PATCH 02/60] add some error prevention --- nodes/generators_extended/smooth_lines.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/nodes/generators_extended/smooth_lines.py b/nodes/generators_extended/smooth_lines.py index 835ad1af2..10a9cc548 100644 --- a/nodes/generators_extended/smooth_lines.py +++ b/nodes/generators_extended/smooth_lines.py @@ -39,7 +39,13 @@ def find_projected_arc_center(p1, p2, b, radius=0.5): focal = (a + c) / 2.0 focal_length = (b-focal).length - angleA = (a-b).angle(c-b) / 2.0 + + try: + angleA = (a-b).angle(c-b) / 2.0 + except ValueError as e: + print('smoothlines encountered non zero length vectors') + # Vector.angle(other): zero length vectors have no valid angle + return None sideA = radius sideB = sideA / tan(angleA) @@ -93,6 +99,8 @@ def spline_points(points, weights, index, params): p2 = Vector(c).lerp(Vector(b), weight_to_use_2)[:] elif params.mode == 'arc': pts = find_projected_arc_center(c, a, b, radius=w2) + if not pts: + return [b] return three_point_arc(pts=pts, num_verts=divs, make_edges=False)[0] return [v[:] for v in bezlerp(p1, b, b, p2, divs)] -- GitLab From 33af0e1f6c425959dd41b96752e61f50949e0118 Mon Sep 17 00:00:00 2001 From: Alessandro Zomparelli Date: Thu, 8 Nov 2018 17:28:06 +0100 Subject: [PATCH 03/60] "Gcode Exporter" and "Evaluate Image" (#2298) * added gcode node * description corrected * fixed continuous extrusion * added credits * added feed movements * new formula * close all shapes * list matches and path preview * text format * removed one output socket * Added Evaluate Image Node and fixed limit in Image Node * removed comments * CLIP and EXTEND tile mode in Evaluate Image Node * removed unnecessary code --- index.md | 3 +- nodes/analyzer/evaluate_image.py | 200 +++++++++++++++++++++ nodes/generator/image.py | 18 +- nodes/text/export_gcode.py | 297 +++++++++++++++++++++++++++++++ utils/sv_itertools.py | 89 ++++++++- 5 files changed, 589 insertions(+), 18 deletions(-) create mode 100644 nodes/analyzer/evaluate_image.py create mode 100644 nodes/text/export_gcode.py diff --git a/index.md b/index.md index 30a384e3e..ba22b0c3d 100644 --- a/index.md +++ b/index.md @@ -64,6 +64,7 @@ SvProportionalEditNode SvRaycasterLiteNode SvOBJInsolationNode + EvaluateImageNode ## Transforms SvRotationNode @@ -310,6 +311,7 @@ SvSetCustomMeshNormals --- SvSpiralNode + SvExportGcodeNode ## Alpha Nodes SvCurveViewerNode @@ -340,4 +342,3 @@ SvOffsetLineNode SvContourNode SvPlanarEdgenetToPolygons - diff --git a/nodes/analyzer/evaluate_image.py b/nodes/analyzer/evaluate_image.py new file mode 100644 index 000000000..66d455d50 --- /dev/null +++ b/nodes/analyzer/evaluate_image.py @@ -0,0 +1,200 @@ +# ##### 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 bpy +from bpy.props import IntProperty, FloatProperty, StringProperty, EnumProperty + +from sverchok.node_tree import SverchCustomTreeNode +from sverchok.data_structure import updateNode, fullList +from math import floor + + +class EvaluateImageNode(bpy.types.Node, SverchCustomTreeNode): + ''' Evaluate Image ''' + bl_idname = 'EvaluateImageNode' + bl_label = 'Evaluate Image' + bl_icon = 'FILE_IMAGE' + + image_name = StringProperty(name='image_name', description='image name', default='', update=updateNode) + + boundary_modes = [ + ("CLIP", "Clip", "", 0), + ("EXTEND", "Extend", "", 1), + ("CYCLIC", "Repeat", "", 2), + ("MIRROR", "Mirror", "", 3)] + + shift_modes = [ + ("NONE", "None", "", 0), + ("ALTERNATE", "Alternate", "", 1), + ("CONSTANT", "Constant", "", 2)] + + shift_mode_U = EnumProperty( + name="U Shift", + description="U Shift", + default="NONE", items=shift_modes, + update=updateNode) + + shift_mode_V = EnumProperty( + name="V Shift", + description="V Shift", + default="NONE", items=shift_modes, + update=updateNode) + + boundU = EnumProperty( + name="U Bounds", + description="U Boundaries", + default="CYCLIC", items=boundary_modes, + update=updateNode) + + boundV = EnumProperty( + name="V Bounds", + description="V Boundaries", + default="CYCLIC", items=boundary_modes, + update=updateNode) + + domU = FloatProperty( + name='U domain', description='U domain', default=1, min=0.00001, + options={'ANIMATABLE'}, update=updateNode) + + domV = FloatProperty( + name='V domain', description='V domain', default=1, min=0.00001, + options={'ANIMATABLE'}, update=updateNode) + + shiftU = FloatProperty( + name='U shift', description='U shift', default=0.5, soft_min=-1, soft_max=1, + options={'ANIMATABLE'}, update=updateNode) + + shiftV = FloatProperty( + name='V shift', description='V shift', default=0, soft_min=-1, soft_max=1, + options={'ANIMATABLE'}, update=updateNode) + + def sv_init(self, context): + self.inputs.new('VerticesSocket', "Verts UV") + self.inputs.new('StringsSocket', "U domain").prop_name = 'domU' + self.inputs.new('StringsSocket', "V domain").prop_name = 'domV' + self.outputs.new('StringsSocket', "R") + self.outputs.new('StringsSocket', "G") + self.outputs.new('StringsSocket', "B") + + def draw_buttons(self, context, layout): + layout.label(text="Image:") + layout.prop_search(self, "image_name", bpy.data, 'images', text="") + + row = layout.row(align=True) + col = row.column(align=True) + col.label(text="Tile U:") + col.prop(self, "boundU", text="") + col.label(text="Shift U:") + col.prop(self, "shift_mode_U", text="") + if self.shift_mode_U != 'NONE': + col.prop(self, "shiftU", text="") + + col = row.column(align=True) + col.label(text="Tile V:") + col.prop(self, "boundV", text="") + col.label(text="Shift V:") + col.prop(self, "shift_mode_V", text="") + if self.shift_mode_V != 'NONE': + col.prop(self, "shiftV", text="") + + + def process(self): + verts = self.inputs['Verts UV'].sv_get() + + inputs, outputs = self.inputs, self.outputs + + # inputs + if inputs['Verts UV'].is_linked: + verts = inputs['Verts UV'].sv_get()[0] + else: + verts = [(0,0,0),(0,1,0),(1,0,0),(1,1,0)] + + if inputs['U domain'].is_linked: + domU = inputs['U domain'].sv_get()[0][0] + else: domU = self.domU + + if inputs['V domain'].is_linked: + domV = inputs['V domain'].sv_get()[0][0] + else: domV = self.domV + + # outputs + red = [[]] + green = [[]] + blue = [[]] + + if outputs['R'].is_linked or outputs['G'].is_linked or outputs['B'].is_linked: + imag = bpy.data.images[self.image_name].pixels[:] + sizeU = bpy.data.images[self.image_name].size[0] + sizeV = bpy.data.images[self.image_name].size[1] + for vert in verts: + vx = vert[0]*(sizeU-1)/domU + vy = vert[1]*sizeV/domV + u = floor(vx) + v = floor(vy) + u0 = u + inside_domain = True + + if self.shift_mode_U == 'ALTERNATE': + if (v//sizeV)%2: u += floor(sizeU*self.shiftU) + if self.shift_mode_U == 'CONSTANT': + u += floor(sizeU*self.shiftU*(v//sizeV)) + if self.boundU == 'CLIP': + inside_domain = 0 <= u < sizeU + elif self.boundU == 'CYCLIC': + u = u%sizeU + elif self.boundU == 'MIRROR': + if (u//sizeU)%2: u = sizeU - 1 - u%(sizeU) + else: u = u%(sizeU) + elif self.boundU == 'EXTEND': + u = max(0,min(u,sizeU-1)) + + + if self.shift_mode_V == 'ALTERNATE': + if (u0//sizeU)%2: v += floor(sizeV*self.shiftV) + if self.shift_mode_V == 'CONSTANT': + v += floor(sizeV*self.shiftV*(u0//sizeU)) + if self.boundV == 'CLIP': + inside_domain = inside_domain and 0 <= v < sizeV + elif self.boundV == 'CYCLIC': + v = v%sizeV + elif self.boundV == 'MIRROR': + if (v//sizeV)%2: v = sizeV - 1 - v%(sizeV) + else: v = v%(sizeV) + elif self.boundV == 'EXTEND': + v = max(0,min(v,sizeV-1)) + + if inside_domain: + index = int(u*4 + v*4*sizeU) + red[0].append(imag[index]) + green[0].append(imag[index+1]) + blue[0].append(imag[index+2]) + else: + red[0].append(0) + green[0].append(0) + blue[0].append(0) + outputs['R'].sv_set(red) + outputs['G'].sv_set(green) + outputs['B'].sv_set(blue) + + +def register(): + bpy.utils.register_class(EvaluateImageNode) + + +def unregister(): + bpy.utils.unregister_class(EvaluateImageNode) diff --git a/nodes/generator/image.py b/nodes/generator/image.py index fd1399f91..ffb789cae 100644 --- a/nodes/generator/image.py +++ b/nodes/generator/image.py @@ -45,19 +45,19 @@ class ImageNode(bpy.types.Node, SverchCustomTreeNode): options={'ANIMATABLE'}, update=updateNode) Xvecs = IntProperty( - name='Xvecs', description='Xvecs', default=10, min=2, max=100, + name='Xvecs', description='Xvecs', default=10, min=2, soft_max=100, options={'ANIMATABLE'}, update=updateNode) Yvecs = IntProperty( - name='Yvecs', description='Yvecs', default=10, min=2, max=100, + name='Yvecs', description='Yvecs', default=10, min=2, soft_max=100, options={'ANIMATABLE'}, update=updateNode) Xstep = FloatProperty( - name='Xstep', description='Xstep', default=1.0, min=0.01, max=100, + name='Xstep', description='Xstep', default=1.0, min=0.01, soft_max=100, options={'ANIMATABLE'}, update=updateNode) Ystep = FloatProperty( - name='Ystep', description='Ystep', default=1.0, min=0.01, max=100, + name='Ystep', description='Ystep', default=1.0, min=0.01, soft_max=100, options={'ANIMATABLE'}, update=updateNode) def sv_init(self, context): @@ -82,12 +82,12 @@ class ImageNode(bpy.types.Node, SverchCustomTreeNode): # inputs if inputs['vecs X'].is_linked: - IntegerX = min(int(inputs['vecs X'].sv_get()[0][0]), 100) + IntegerX = min(int(inputs['vecs X'].sv_get()[0][0]), 1000000) else: IntegerX = int(self.Xvecs) if inputs['vecs Y'].is_linked: - IntegerY = min(int(inputs['vecs Y'].sv_get()[0][0]), 100) + IntegerY = min(int(inputs['vecs Y'].sv_get()[0][0]), 1000000) else: IntegerY = int(self.Yvecs) @@ -109,7 +109,7 @@ class ImageNode(bpy.types.Node, SverchCustomTreeNode): if outputs['edgs'].is_linked: listEdg = [] - + for i in range(IntegerY): for j in range(IntegerX-1): listEdg.append((IntegerX*i+j, IntegerX*i+j+1)) @@ -127,7 +127,7 @@ class ImageNode(bpy.types.Node, SverchCustomTreeNode): listPlg.append((IntegerX*j+i, IntegerX*j+i+1, IntegerX*j+i+IntegerX+1, IntegerX*j+i+IntegerX)) plg = [list(listPlg)] outputs['pols'].sv_set(plg) - + def make_vertices(self, delitelx, delitely, stepx, stepy, image_name): lenx = bpy.data.images[image_name].size[0] @@ -150,7 +150,7 @@ class ImageNode(bpy.types.Node, SverchCustomTreeNode): # каждый пиксель кодируется RGBA, и записан строкой, без разделения на строки и столбцы. middle = (imag[addition]*R+imag[addition+1]*G+imag[addition+2]*B)*imag[addition+3] vertex = [x*stepx[x], y*stepy[y], middle] - vertices.append(vertex) + vertices.append(vertex) addition += int(xcoef*4) return vertices diff --git a/nodes/text/export_gcode.py b/nodes/text/export_gcode.py new file mode 100644 index 000000000..eaaacb586 --- /dev/null +++ b/nodes/text/export_gcode.py @@ -0,0 +1,297 @@ +# ##### 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 ##### + +# made by: Alessandro Zomparelli +# url: www.alessandrozomparelli.com + +import io +import itertools +import pprint +import sverchok + +import bpy, os, mathutils +from numpy import mean +import operator +from math import pi + +from bpy.props import BoolProperty, EnumProperty, StringProperty, FloatProperty, IntProperty + +from sverchok.node_tree import SverchCustomTreeNode, StringsSocket +from sverchok.data_structure import node_id, multi_socket, updateNode +from sverchok.utils.sv_itertools import sv_zip_longest2, flatten, list_of_lists, recurse_verts_fxy, match_longest_lists + +from sverchok.utils.sv_text_io_common import ( + FAIL_COLOR, READY_COLOR, TEXT_IO_CALLBACK, + get_socket_type, + new_output_socket, + name_dict, + text_modes +) + +def convert_to_text(list): + while True: + if type(list) is str: break + elif type(list) in (tuple, list): + try: + list = '\n'.join(list) + break + except: list = list[0] + else: break + return list + +class SvExportGcodeNode(bpy.types.Node, SverchCustomTreeNode): + """ + Triggers: Export gcode from vertices position + Tooltip: Generate a gcode file from a list of vertices + """ + bl_idname = 'SvExportGcodeNode' + bl_label = 'Export Gcode' + bl_icon = 'COPYDOWN' + + last_e = FloatProperty(name="Pull", default=5.0, min=0, soft_max=10) + path_length = FloatProperty(name="Pull", default=5.0, min=0, soft_max=10) + + folder = StringProperty(name="File", default="", subtype='FILE_PATH') + pull = FloatProperty(name="Pull", default=5.0, min=0, soft_max=10) + push = FloatProperty(name="Push", default=5.0, min=0, soft_max=10) + dz = FloatProperty(name="dz", default=2.0, min=0, soft_max=20) + flow_mult = FloatProperty(name="Flow Mult", default=1.0, min=0, soft_max=3) + feed = IntProperty(name="Feed Rate (F)", default=1000, min=0, soft_max=20000) + feed_horizontal = IntProperty(name="Feed Horizontal", default=2000, min=0, soft_max=20000) + feed_vertical = IntProperty(name="Feed Vertical", default=500, min=0, soft_max=20000) + feed = IntProperty(name="Feed Rate (F)", default=1000, min=0, soft_max=20000) + esteps = FloatProperty(name="E Steps/Unit", default=5, min=0, soft_max=100) + start_code = StringProperty(name="Start", default='') + end_code = StringProperty(name="End", default='') + auto_sort = BoolProperty(name="Auto Sort", default=True) + close_all = BoolProperty(name="Close Shapes", default=False) + nozzle = FloatProperty(name="Nozzle", default=0.4, min=0, soft_max=10) + layer_height = FloatProperty(name="Layer Height", default=0.1, min=0, soft_max=10) + filament = FloatProperty(name="Filament (\u03A6)", default=1.75, min=0, soft_max=120) + + gcode_mode = EnumProperty(items=[ + ("CONT", "Continuous", ""), + ("RETR", "Retraction", "") + ], default='CONT', name="Mode") + + def sv_init(self, context): + self.inputs.new('StringsSocket', 'Layer Height',).prop_name = 'layer_height' + self.inputs.new('StringsSocket', 'Flow Mult',).prop_name = 'flow_mult' + self.inputs.new('VerticesSocket', 'Vertices',) + + self.outputs.new('StringsSocket', 'Info',) + self.outputs.new('VerticesSocket', 'Vertices',) + self.outputs.new('StringsSocket', 'Printed Edges',) + self.outputs.new('StringsSocket', 'Travel Edges',) + + def draw_buttons(self, context, layout): + + addon = context.user_preferences.addons.get(sverchok.__name__) + over_sized_buttons = addon.preferences.over_sized_buttons + + col = layout.column(align=True) + row = col.row() + row.prop(self, 'folder', toggle=True, text='') + col = layout.column(align=True) + row = col.row() + row.prop(self, 'gcode_mode', expand=True, toggle=True) + #col = layout.column(align=True) + col = layout.column(align=True) + col.label(text="Extrusion:", icon='MOD_FLUIDSIM') + #col.prop(self, 'esteps') + col.prop(self, 'filament') + col.prop(self, 'nozzle') + col.separator() + col.label(text="Speed (Feed Rate F):", icon='DRIVER') + col.prop(self, 'feed', text='Print') + if self.gcode_mode == 'RETR': + col.prop(self, 'feed_vertical', text='Z Lift') + col.prop(self, 'feed_horizontal', text='Travel') + col.separator() + if self.gcode_mode == 'RETR': + col.label(text="Retraction:", icon='NOCURVE') + col.prop(self, 'pull', text='Retraction') + col.prop(self, 'dz', text='Z Hop') + col.prop(self, 'push', text='Preload') + col.prop(self, 'auto_sort', text="Sort Layers (z)") + col.prop(self, 'close_all') + col.separator() + col.label(text='Custom Code:', icon='SCRIPT') + col.prop_search(self, 'start_code', bpy.data, 'texts') + col.prop_search(self, 'end_code', bpy.data, 'texts') + col.separator() + row = col.row(align=True) + row.scale_y = 4.0 + row.operator(TEXT_IO_CALLBACK, text='Export Gcode').fn_name = 'process' + + def update_socket(self, context): + self.update() + + def process(self): + # manage data + feed = self.feed + feed_v = self.feed_vertical + feed_h = self.feed_horizontal + layer = self.layer_height + layer = self.inputs['Layer Height'].sv_get() + vertices = self.inputs['Vertices'].sv_get() + flow_mult = self.inputs['Flow Mult'].sv_get() + + # data matching + vertices = list_of_lists(vertices) + flow_mult = list_of_lists(flow_mult) + layer = list_of_lists(layer) + vertices, flow_mult, layer = match_longest_lists([vertices, flow_mult, layer]) + print(vertices) + print(layer) + + # open file + if self.folder == '': + folder = '//' + os.path.splitext(bpy.path.basename(bpy.context.blend_data.filepath))[0] + else: + folder = self.folder + if '.gcode' not in folder: folder += '.gcode' + path = bpy.path.abspath(folder) + file = open(path, 'w') + try: + for line in bpy.data.texts[self.start_code].lines: + file.write(line.body + '\n') + except: + pass + + # sort vertices + if self.auto_sort and self.gcode_mode == 'RETR': + sorted_verts = [] + for curve in vertices: + # mean z + listz = [v[2] for v in curve] + meanz = mean(listz) + # store curve and meanz + sorted_verts.append((curve, meanz)) + vertices = [data[0] for data in sorted(sorted_verts, key=lambda height: height[1])] + + # initialize variables + e = 0 + last_vert = mathutils.Vector((0,0,0)) + maxz = 0 + path_length = 0 + + printed_verts = [] + printed_edges = [] + travel_verts = [] + travel_edges = [] + + # write movements + for i in range(len(vertices)): + curve = vertices[i] + first_id = len(printed_verts) + for j in range(len(curve)): + v = curve[j] + v_flow_mult = flow_mult[i][j] + v_layer = layer[i][j] + + # record max z + maxz = max(maxz,v[2]) + + # first point of the gcode + if i == j == 0: + printed_verts.append(v) + file.write('G92 E0 \n') + params = v[:3] + (feed,) + to_write = 'G1 X{0:.4f} Y{1:.4f} Z{2:.4f} F{3:.0f}\n'.format(*params) + file.write(to_write) + else: + # start after retraction + if j == 0 and self.gcode_mode == 'RETR': + params = v[:2] + (maxz+self.dz,) + (feed_h,) + to_write = 'G1 X{0:.4f} Y{1:.4f} Z{2:.4f} F{3:.0f}\n'.format(*params) + file.write(to_write) + e += self.push + params = v[:3] + (feed_v,) + to_write = 'G1 X{0:.4f} Y{1:.4f} Z{2:.4f} F{3:.0f}\n'.format(*params) + file.write(to_write) + file.write( 'G1 E' + format(e, '.4f') + '\n') + printed_verts.append((v[0], v[1], maxz+self.dz)) + travel_edges.append((len(printed_verts)-1, len(printed_verts)-2)) + printed_verts.append(v) + travel_edges.append((len(printed_verts)-1, len(printed_verts)-2)) + # regular extrusion + else: + printed_verts.append(v) + v1 = mathutils.Vector(v) + v0 = mathutils.Vector(curve[j-1]) + dist = (v1-v0).length + print(dist) + area = v_layer * self.nozzle + pi*(v_layer/2)**2 # rectangle + circle + cylinder = pi*(self.filament/2)**2 + flow = area / cylinder + e += dist * v_flow_mult * flow + params = v[:3] + (e,) + to_write = 'G1 X{0:.4f} Y{1:.4f} Z{2:.4f} E{3:.4f}\n'.format(*params) + file.write(to_write) + path_length += dist + printed_edges.append([len(printed_verts)-1, len(printed_verts)-2]) + if self.gcode_mode == 'RETR': + v0 = mathutils.Vector(curve[-1]) + if self.close_all: + #printed_verts.append(v0) + printed_edges.append([len(printed_verts)-1, first_id]) + + v1 = mathutils.Vector(curve[0]) + dist = (v0-v1).length + area = v_layer * self.nozzle + pi*(v_layer/2)**2 # rectangle + circle + cylinder = pi*(self.filament/2)**2 + flow = area / cylinder + e += dist * v_flow_mult * flow + params = v1[:3] + (e,) + to_write = 'G1 X{0:.4f} Y{1:.4f} Z{2:.4f} E{3:.4f}\n'.format(*params) + file.write(to_write) + path_length += dist + v0 = v1 + if i < len(vertices)-1: + e -= self.pull + file.write('G0 E' + format(e, '.4f') + '\n') + params = v0[:2] + (maxz+self.dz,) + (feed_v,) + to_write = 'G1 X{0:.4f} Y{1:.4f} Z{2:.4f} F{3:.0f}\n'.format(*params) + file.write(to_write) + printed_verts.append(v0.to_tuple()) + printed_verts.append((v0.x, v0.y, maxz+self.dz)) + travel_edges.append((len(printed_verts)-1, len(printed_verts)-2)) + + # end code + try: + for line in bpy.data.texts[self.end_code].lines: + file.write(line.body + '\n') + except: + pass + file.close() + print("Saved gcode to " + path) + info = "Extruded Filament: " + format(e, '.2f') + '\n' + info += "Extruded Volume: " + format(e*pi*(self.filament/2)**2, '.2f') + '\n' + info += "Printed Path: " + format(path_length, '.2f') + self.outputs[0].sv_set(info) + self.outputs[1].sv_set(printed_verts) + self.outputs[2].sv_set(printed_edges) + self.outputs[3].sv_set(travel_edges) + +def register(): + bpy.utils.register_class(SvExportGcodeNode) + + +def unregister(): + bpy.utils.unregister_class(SvExportGcodeNode) diff --git a/utils/sv_itertools.py b/utils/sv_itertools.py index b47669bb9..781c6a39c 100644 --- a/utils/sv_itertools.py +++ b/utils/sv_itertools.py @@ -25,7 +25,7 @@ class SvSentinel: class sv_zip_longest: def __init__(self, *args): - self.counter = len(args) + self.counter = len(args) self.iterators = [] for lst in args: fl = lst[-1] @@ -33,7 +33,7 @@ class sv_zip_longest: self.iterators.append(chain(lst, SvSentinel(fl,self), filler)) def __next__(self): - try: + try: if self.counter: return tuple(map(next, self.iterators)) else: @@ -42,8 +42,7 @@ class sv_zip_longest: raise StopIteration def __iter__(self): - return self - + return self def sv_zip_longest2(*args): # by zeffi @@ -51,8 +50,8 @@ def sv_zip_longest2(*args): itrs = [iter(sl) for sl in args] for i in range(longest): yield tuple((next(iterator, args[idx][-1]) for idx, iterator in enumerate(itrs))) - - + + def recurse_fx(l, f): if isinstance(l, (list, tuple)): return [recurse_fx(i, f) for i in l] @@ -61,9 +60,9 @@ def recurse_fx(l, f): def recurse_fxy(l1, l2, f): l1_type = isinstance(l1, (list, tuple)) - l2_type = isinstance(l2, (list, tuple)) + l2_type = isinstance(l2, (list, tuple)) if not (l1_type or l2_type): - return f(l1, l2) + return f(l1, l2) elif l1_type and l2_type: fl = l2[-1] if len(l1) > len(l2) else l1[-1] res = [] @@ -77,6 +76,80 @@ def recurse_fxy(l1, l2, f): return [recurse_fxy(l1, y, f) for y in l2] +def recurse_verts_fxy(l1, l2, f): + l1_type = isinstance(l1, (list)) + l2_type = isinstance(l2, (list)) + if not (l1_type or l2_type): + return f(l1, l2) + elif l1_type and l2_type: + fl = l2[-1] if len(l1) > len(l2) else l1[-1] + res = [] + res_append = res.append + for x, y in zip_longest(l1, l2, fillvalue=fl): + res_append(recurse_verts_fxy(x, y, f)) + return res + elif l1_type and not l2_type: + return [recurse_verts_fxy(x, l2, f) for x in l1] + else: #not l1_type and l2_type + return [recurse_verts_fxy(l1, y, f) for y in l2] + +# append all the elements to one single list +def append_all(l, flat): + if isinstance(l,(list)): + return [append_all(i, flat) for i in l] + else: + flat.append(l) + return l + +# flatten sublists +def flatten(l): + flat = [] + append_all(l, flat) + return flat + +# append all the lists to one single list +def append_lists(l, lists): + if isinstance(l,(list)): + flat_list = True + for i in l: + if isinstance(i,(list)): + flat_list = False + break + if flat_list: + lists.append(l) + return None + else: + return [append_lists(i, lists) for i in l] + else: + lists.append([l]) + return None + +# generate a single list with 1 level lists inside +def list_of_lists(l): + out_list = [] + append_lists(l, out_list) + return out_list + +# works with irregular sublists +def match_longest_lists(lists): + add_level = max([isinstance(l, list) for l in lists]) + if add_level: + for i in range(len(lists)): + l = lists[i] + if not isinstance(l, list): + lists[i] = [l] + length = [len(l) for l in lists] + max_length = max(length) + # extend shorter lists + for l in lists: + for i in range(len(l), max_length): + l.append(l[-1]) + try: + return zip(*[match_longest_lists([l[i] for l in lists]) for i in range(max_length)]) + except: + return lists + + def extend_if_needed(vl, wl, default=0.5): # match wl to correspond with vl try: -- GitLab From 7f8f0966ab2343ecfec1ad21331a0d390dc3f58b Mon Sep 17 00:00:00 2001 From: DolphinDream Date: Tue, 13 Nov 2018 09:57:21 -0600 Subject: [PATCH 04/60] Add default font scale to index viewer node settable via preferences For some screen resolutions (e.g. mac book pro with retina display / double pixel resolution) the index viewer node's font size was too small to read and needed a scale factor of 2x. With this new setting the user can define the desired font scale in the SV preference settings. The default font scale is 1.0. --- settings.py | 9 +++++++-- ui/index_viewer_draw.py | 12 ++++++++++-- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/settings.py b/settings.py index a1a0460fa..7c8f21895 100644 --- a/settings.py +++ b/settings.py @@ -30,7 +30,7 @@ class SverchokPreferences(AddonPreferences): color_def.apply_theme() tab_modes = [(k, k, '', i) for i, k in enumerate(["General", "Node Defaults", "Theme"])] - + selected_tab = bpy.props.EnumProperty( items=tab_modes, description="pick viewing mode", @@ -168,6 +168,9 @@ class SverchokPreferences(AddonPreferences): stethoscope_view_xy_multiplier = FloatProperty( default=1.0, min=0.01, step=0.01, description='default stethoscope scale') + index_viewer_scale = FloatProperty( + default=1.0, min=0.01, step=0.01, description='default index viewer scale') + datafiles = os.path.join(bpy.utils.user_resource('DATAFILES', path='sverchok', create=True)) defaults_location = StringProperty(default=datafiles, description='usually ..data_files\\sverchok\\defaults\\nodes.json') external_editor = StringProperty(description='which external app to invoke to view sources') @@ -178,7 +181,7 @@ class SverchokPreferences(AddonPreferences): def update_log_level(self, context): logging.info("Setting log level to %s", self.log_level) logging.setLevel(self.log_level) - + log_levels = [ ("DEBUG", "Debug", "Debug output", 0), ("INFO", "Information", "Informational output", 1), @@ -270,6 +273,8 @@ class SverchokPreferences(AddonPreferences): box_sub1_col.label('stethoscope mk2 settings') box_sub1_col.prop(self, 'stethoscope_view_scale', text='scale') box_sub1_col.prop(self, 'stethoscope_view_xy_multiplier', text='xy multiplier') + box_sub1_col.label('index viewer settings') + box_sub1_col.prop(self, 'index_viewer_scale', text='scale') col3 = row_sub1.split().column() col3.label('Location of custom defaults') diff --git a/ui/index_viewer_draw.py b/ui/index_viewer_draw.py index 44f9a3655..49b81b763 100644 --- a/ui/index_viewer_draw.py +++ b/ui/index_viewer_draw.py @@ -24,6 +24,7 @@ import blf import bgl from mathutils import Vector +from sverchok.utils.context_managers import sv_preferences from sverchok.data_structure import Vector_generate, Matrix_generate SpaceView3D = bpy.types.SpaceView3D @@ -154,8 +155,15 @@ def draw_callback_px(n_id, draw_verts, draw_edges, draw_faces, draw_matrix, draw display_edge_index = settings['display_edge_index'] display_face_index = settings['display_face_index'] + try: + with sv_preferences() as prefs: + scale = prefs.index_viewer_scale + except: + # print('did not find preferences - you need to save user preferences') + scale = 1.0 + font_id = 0 - text_height = 13 + text_height = int(13.0 * scale) blf.size(font_id, text_height, 72) # should check prefs.dpi region_mid_width = region.width / 2.0 @@ -222,7 +230,7 @@ def draw_callback_px(n_id, draw_verts, draw_edges, draw_faces, draw_matrix, draw if data_edges and display_edge_index: for edge_index, (idx1, idx2) in enumerate(data_edges[obj_index]): - + v1 = Vector(final_verts[idx1]) v2 = Vector(final_verts[idx2]) loc = v1 + ((v2 - v1) / 2) -- GitLab From c5551c9c82f4effc40d97bab6d047b4fd8d95ff1 Mon Sep 17 00:00:00 2001 From: DolphinDream Date: Tue, 13 Nov 2018 18:02:19 -0600 Subject: [PATCH 05/60] Fix render scale and location of easing and texture viewer nodes - made scale and xy multiplier pref settings generic to be used by multiple nodes - add scale/multiplier adjustments to the texture viewer and easing node and update stethoscope node to use the same scale/multiplier --- nodes/number/easing.py | 56 +++++++++++++++++++++-------------- nodes/text/stethoscope_mk2.py | 5 ++-- nodes/viz/texture_viewer.py | 13 ++++++++ settings.py | 16 ++++++---- 4 files changed, 60 insertions(+), 30 deletions(-) diff --git a/nodes/number/easing.py b/nodes/number/easing.py index d1c292f92..5c2392da3 100644 --- a/nodes/number/easing.py +++ b/nodes/number/easing.py @@ -23,6 +23,7 @@ from bpy.props import FloatProperty, EnumProperty, StringProperty, BoolProperty import blf import bgl +from sverchok.utils.context_managers import sv_preferences from sverchok.data_structure import updateNode, node_id from sverchok.node_tree import SverchCustomTreeNode from sverchok.ui import nodeview_bgl_viewer_draw_mk2 as nvBGL2 @@ -38,12 +39,12 @@ for k in sorted(easing_dict.keys()): palette_dict = { "default": ( - (0.243299, 0.590403, 0.836084, 1.00), # back_color + (0.243299, 0.590403, 0.836084, 1.00), # back_color (0.390805, 0.754022, 1.000000, 1.00), # grid_color (1.000000, 0.330010, 0.107140, 1.00) # line_color ), "scope": ( - (0.274677, 0.366253, 0.386430, 1.00), # back_color + (0.274677, 0.366253, 0.386430, 1.00), # back_color (0.423268, 0.558340, 0.584078, 1.00), # grid_color (0.304762, 1.000000, 0.062827, 1.00) # line_color ) @@ -54,40 +55,42 @@ palette_dict = { def simple_grid_xy(x, y, args): func = args[0] back_color, grid_color, line_color = args[1] + scale = args[2] def draw_rect(x=0, y=0, w=30, h=10, color=(0.0, 0.0, 0.0, 1.0)): - bgl.glColor4f(*color) + bgl.glColor4f(*color) bgl.glBegin(bgl.GL_POLYGON) - for coord in [(x, y), (x+w, y), (w+x, y-h), (x, y-h)]: + for coord in [(x, y), (x + w, y), (w + x, y - h), (x, y - h)]: bgl.glVertex2f(*coord) bgl.glEnd() + size = 140 * scale # draw bg fill - draw_rect(x=x, y=y, w=140, h=140, color=back_color) + draw_rect(x=x, y=y, w=size, h=size, color=back_color) # draw grid bgl.glColor4f(*grid_color) num_divs = 8 - offset = 140/num_divs + offset = size / num_divs line_parts_x = [] line_parts_y = [] - for i in range(num_divs+1): - xpos1 = x + (i*offset) + for i in range(num_divs + 1): + xpos1 = x + (i * offset) ypos1 = y - ypos2 = y - 140 + ypos2 = y - size line_parts_x.extend([[xpos1, ypos1], [xpos1, ypos2]]) - ypos = y - (i*offset) - line_parts_y.extend([[x, ypos], [x+140, ypos]]) + ypos = y - (i * offset) + line_parts_y.extend([[x, ypos], [x + size, ypos]]) bgl.glLineWidth(0.8) bgl.glBegin(bgl.GL_LINES) for coord in line_parts_x + line_parts_y: bgl.glVertex2f(*coord) - bgl.glEnd() + bgl.glEnd() # draw graph-line bgl.glColor4f(*line_color) @@ -95,13 +98,11 @@ def simple_grid_xy(x, y, args): bgl.glBegin(bgl.GL_LINE_STRIP) num_points = 100 seg_diff = 1 / num_points - for i in range(num_points+1): - _px = x + ((i * seg_diff) * 140) - _py = y - (1 - func(i * seg_diff) * 140) - 140 + for i in range(num_points + 1): + _px = x + ((i * seg_diff) * size) + _py = y - (1 - func(i * seg_diff) * size) - size bgl.glVertex2f(_px, _py) - bgl.glEnd() - - + bgl.glEnd() class SvEasingNode(bpy.types.Node, SverchCustomTreeNode): @@ -169,18 +170,29 @@ class SvEasingNode(bpy.types.Node, SverchCustomTreeNode): palette = palette_dict.get(self.selected_theme_mode)[:] x, y = [int(j) for j in (self.location + Vector((self.width + 20, 0)))[:]] - + + # adjust render location based on preference multiplier setting + try: + with sv_preferences() as prefs: + multiplier = prefs.render_location_xy_multiplier + scale = prefs.render_scale + except: + # print('did not find preferences - you need to save user preferences') + multiplier = 1.0 + scale = 1.0 + x, y = [x * multiplier, y * multiplier] + draw_data = { 'tree_name': self.id_data.name[:], - 'mode': 'custom_function', + 'mode': 'custom_function', 'custom_function': simple_grid_xy, 'loc': (x, y), - 'args': (easing_func, palette) + 'args': (easing_func, palette, scale) } nvBGL2.callback_enable(n_id, draw_data) def free(self): - nvBGL2.callback_disable(node_id(self)) + nvBGL2.callback_disable(node_id(self)) # reset n_id on copy def copy(self, node): diff --git a/nodes/text/stethoscope_mk2.py b/nodes/text/stethoscope_mk2.py index 6fa5d89a6..31744a203 100644 --- a/nodes/text/stethoscope_mk2.py +++ b/nodes/text/stethoscope_mk2.py @@ -132,7 +132,7 @@ class SvStethoscopeNodeMK2(bpy.types.Node, SverchCustomTreeNode): try: with sv_preferences() as prefs: scale = prefs.stethoscope_view_scale - location_theta = prefs.stethoscope_view_xy_multiplier + location_theta = prefs.render_location_xy_multiplier except: # print('did not find preferences - you need to save user preferences') scale = 1.0 @@ -142,7 +142,6 @@ class SvStethoscopeNodeMK2(bpy.types.Node, SverchCustomTreeNode): data = inputs[0].sv_get(deepcopy=False) self.num_elements = len(data) - if self.selected_mode == 'text-based': props = lambda: None props.line_width = self.line_width @@ -190,7 +189,7 @@ class SvStethoscopeNodeMK2(bpy.types.Node, SverchCustomTreeNode): return try: if not self.inputs[0].other: - nvBGL.callback_disable(node_id(self)) + nvBGL.callback_disable(node_id(self)) except: print('stethoscope update holdout (not a problem)') diff --git a/nodes/viz/texture_viewer.py b/nodes/viz/texture_viewer.py index e267325e1..e0854e852 100644 --- a/nodes/viz/texture_viewer.py +++ b/nodes/viz/texture_viewer.py @@ -26,6 +26,7 @@ from bpy.props import ( FloatProperty, EnumProperty, StringProperty, BoolProperty, IntProperty ) +from sverchok.utils.context_managers import sv_preferences from sverchok.data_structure import updateNode, node_id from sverchok.node_tree import SverchCustomTreeNode from sverchok.ui import nodeview_bgl_viewer_draw_mk2 as nvBGL2 @@ -384,6 +385,18 @@ class SvTextureViewerNode(bpy.types.Node, SverchCustomTreeNode): self.texture[n_id] = name[0] init_texture(width, height, name[0], texture, gl_color_constant) + # adjust render location based on preference multiplier setting + try: + with sv_preferences() as prefs: + multiplier = prefs.render_location_xy_multiplier + scale = prefs.render_scale + except: + # print('did not find preferences - you need to save user preferences') + multiplier = 1.0 + scale = 1.0 + x, y = [x * multiplier, y * multiplier] + width, height =[width * scale, height * scale] + draw_data = { 'tree_name': self.id_data.name[:], 'mode': 'custom_function', diff --git a/settings.py b/settings.py index 7c8f21895..100c9a3fc 100644 --- a/settings.py +++ b/settings.py @@ -163,10 +163,14 @@ class SverchokPreferences(AddonPreferences): enable_live_objin = BoolProperty( description="Objects in edit mode will be updated in object-in Node") + render_scale = FloatProperty( + default=1.0, min=0.01, step=0.01, description='default render scale') + + render_location_xy_multiplier = FloatProperty( + default=1.0, min=0.01, step=0.01, description='default render location xy multiplier') + stethoscope_view_scale = FloatProperty( default=1.0, min=0.01, step=0.01, description='default stethoscope scale') - stethoscope_view_xy_multiplier = FloatProperty( - default=1.0, min=0.01, step=0.01, description='default stethoscope scale') index_viewer_scale = FloatProperty( default=1.0, min=0.01, step=0.01, description='default index viewer scale') @@ -270,10 +274,12 @@ class SverchokPreferences(AddonPreferences): row_sub1 = col.row().split(0.5) box_sub1 = row_sub1.box() box_sub1_col = box_sub1.column(align=True) - box_sub1_col.label('stethoscope mk2 settings') + box_sub1_col.label('Render Scale & Location') + box_sub1_col.prop(self, 'render_location_xy_multiplier', text='xy multiplier') + box_sub1_col.prop(self, 'render_scale', text='scale') + box_sub1_col.label('Stethoscope MK2 settings') box_sub1_col.prop(self, 'stethoscope_view_scale', text='scale') - box_sub1_col.prop(self, 'stethoscope_view_xy_multiplier', text='xy multiplier') - box_sub1_col.label('index viewer settings') + box_sub1_col.label('Index Viewer settings') box_sub1_col.prop(self, 'index_viewer_scale', text='scale') col3 = row_sub1.split().column() -- GitLab From 4a8593656955811836af559487b9cbf220b30060 Mon Sep 17 00:00:00 2001 From: Victor Doval <10011941+vicdoval@users.noreply.github.com> Date: Sat, 17 Nov 2018 21:41:57 +0100 Subject: [PATCH 06/60] line mk3 with AB and OD modes (#2294) * line mk3 with AB and OD modes * updated tests to mk3 * lime mk3 docs --- docs/nodes/generator/line_mk3.rst | 84 ++++++ index.md | 2 +- .../ROADS_LARGE_nikitron_2014.json | 2 +- json_examples/Design/lights_colors.json | 2 +- .../hilbert_to_circle.json | 2 +- json_examples/Shapes/bind_sections_shape.json | 4 +- nodes/generator/line_mk3.py | 240 ++++++++++++++++++ {nodes/generator => old_nodes}/line_mk2.py | 2 + utils/modules/geom_utils.py | 3 + 9 files changed, 335 insertions(+), 6 deletions(-) create mode 100644 docs/nodes/generator/line_mk3.rst create mode 100644 nodes/generator/line_mk3.py rename {nodes/generator => old_nodes}/line_mk2.py (98%) diff --git a/docs/nodes/generator/line_mk3.rst b/docs/nodes/generator/line_mk3.rst new file mode 100644 index 000000000..f1827eada --- /dev/null +++ b/docs/nodes/generator/line_mk3.rst @@ -0,0 +1,84 @@ +Line +==== + +Functionality +------------- + +Line generator creates a series of connected segments based on the number of vertices and the length between them. Just a standard subdivided line along X axis + +Inputs +------ + +All parameters except **Center** are vectorized. They will accept single or multiple values. +Both inputs will accept a single number or an array of them. It also will work an array of arrays:: + + [2] + [2, 4, 6] + [[2], [4]] + +Parameters +---------- + +All parameters except **Center** can be given by the node or an external input. + + ++---------------+---------------+--------------+---------------------------------------------------------+ +| Param | Type | Default | Description | ++===============+===============+==============+=========================================================+ +| **Direction** | Enum | "X" | Ortho direction, "from A to B" or "Origin and Direction"| ++---------------+---------------+--------------+---------------------------------------------------------+ +| **N Verts** | Int | 2 | number of vertices. The minimum is 2 | ++---------------+---------------+--------------+---------------------------------------------------------+ +| **Step** | Float | 1.00 | length between vertices | ++---------------+---------------+--------------+---------------------------------------------------------+ +| **Center** | Boolean     | False       | center line around 0 | ++---------------+---------------+--------------+---------------------------------------------------------+ +| **Normalize** | Boolean     | False       | determine steps by fixing total length line | ++---------------+---------------+--------------+---------------------------------------------------------+ +| **Size** | Float     | 10     | total length of the segment | ++---------------+---------------+--------------+---------------------------------------------------------+ +| **A, O** | Vector     | (0,0,0)     | origin point of line | ++---------------+---------------+--------------+---------------------------------------------------------+ +| **B** | Vector     | (0.5,0.5,0.5)| end point of line | ++---------------+---------------+--------------+---------------------------------------------------------+ +| **D** | Vector     | (1,1,1) | direction of the line | ++---------------+---------------+--------------+---------------------------------------------------------+ + +Outputs +------- + +**Vertices** and **Edges**. Verts and Edges will be generated. Depending on the inputs, the node will generate only one or multiples independent lines. See examples below. + + +Example of usage +---------------- + +.. image:: https://user-images.githubusercontent.com/10011941/47713459-a177d880-dc3a-11e8-935b-a2fa494dc49b.png + :alt: LineDemo1.PNG + +The first example shows just an standard line with 5 vertices and 1.00 ud between them + +.. image:: https://user-images.githubusercontent.com/10011941/47713473-a9377d00-dc3a-11e8-94ab-39095761788c.png + :alt: LineDemo2.PNG + +In this example the step is given by a series of numbers + +.. image:: https://user-images.githubusercontent.com/10011941/47713477-ad639a80-dc3a-11e8-9884-6568326d2a33.png + :alt: LineDemo3.PNG + +You can create multiple lines if input multiple lists + +.. image:: https://user-images.githubusercontent.com/10011941/47713487-b3597b80-dc3a-11e8-996b-17edf1cec9da.png + :alt: LineDemo4.PNG + +The AB mode will output a divided segment for each vector pair, the step can be used to change the proportions of the divisions + +.. image:: https://user-images.githubusercontent.com/10011941/47713488-b3597b80-dc3a-11e8-9e6e-f742d0338ba5.png + :alt: LineDemo5.PNG + +The "OD" mode can be used to visualize normals + +.. image:: https://user-images.githubusercontent.com/10011941/47713490-b3597b80-dc3a-11e8-9b6d-b937c0375ec5.png + :alt: LineDemo5.PNG + +Advanced example using the node to create a paraboloid grid \ No newline at end of file diff --git a/index.md b/index.md index ba22b0c3d..bda1d1523 100644 --- a/index.md +++ b/index.md @@ -10,7 +10,7 @@ > Failing to follow these points will break the node category parser. ## Generator - SvLineNodeMK2 + SvLineNodeMK3 SvPlaneNodeMK2 SvNGonNode SvBoxNode diff --git a/json_examples/Architecture/ROADS_LARGE_nikitron_2014.json b/json_examples/Architecture/ROADS_LARGE_nikitron_2014.json index 6d486ed95..230efdff8 100644 --- a/json_examples/Architecture/ROADS_LARGE_nikitron_2014.json +++ b/json_examples/Architecture/ROADS_LARGE_nikitron_2014.json @@ -659,7 +659,7 @@ "width": 140.0 }, "Line MK2": { - "bl_idname": "SvLineNodeMK2", + "bl_idname": "SvLineNodeMK3", "color": [ 0.0, 0.5, diff --git a/json_examples/Design/lights_colors.json b/json_examples/Design/lights_colors.json index d856a1c2d..a07c30e05 100644 --- a/json_examples/Design/lights_colors.json +++ b/json_examples/Design/lights_colors.json @@ -40,7 +40,7 @@ "width": 140.0 }, "Line MK2": { - "bl_idname": "SvLineNodeMK2", + "bl_idname": "SvLineNodeMK3", "color": [ 0.0, 0.5, diff --git a/json_examples/ParametricModelling/hilbert_to_circle.json b/json_examples/ParametricModelling/hilbert_to_circle.json index 9bcc586ec..29a658d2e 100644 --- a/json_examples/ParametricModelling/hilbert_to_circle.json +++ b/json_examples/ParametricModelling/hilbert_to_circle.json @@ -94,7 +94,7 @@ "width": 140.0 }, "Line": { - "bl_idname": "SvLineNodeMK2", + "bl_idname": "SvLineNodeMK3", "color": [ 0.0, 0.5, diff --git a/json_examples/Shapes/bind_sections_shape.json b/json_examples/Shapes/bind_sections_shape.json index 5c1f1e649..a1922ed7c 100644 --- a/json_examples/Shapes/bind_sections_shape.json +++ b/json_examples/Shapes/bind_sections_shape.json @@ -276,7 +276,7 @@ "width": 391.4091796875 }, "Line MK2": { - "bl_idname": "SvLineNodeMK2", + "bl_idname": "SvLineNodeMK3", "color": [ 0.9200000166893005, 0.9200000166893005, @@ -297,7 +297,7 @@ "width": 140.0 }, "Line MK2.001": { - "bl_idname": "SvLineNodeMK2", + "bl_idname": "SvLineNodeMK3", "color": [ 0.9200000166893005, 0.9200000166893005, diff --git a/nodes/generator/line_mk3.py b/nodes/generator/line_mk3.py new file mode 100644 index 000000000..cfaefc875 --- /dev/null +++ b/nodes/generator/line_mk3.py @@ -0,0 +1,240 @@ +# ##### 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 bpy +from bpy.props import IntProperty, FloatProperty, BoolProperty, EnumProperty, FloatVectorProperty +from sverchok.node_tree import SverchCustomTreeNode +from sverchok.data_structure import updateNode, fullList, match_long_repeat +from sverchok.utils.modules.geom_utils import interp_v3_v3v3, normalize, add_v3_v3v3, sub_v3_v3v3 + +directionItems = [ + ("X", "X", "Along X axis", 0), + ("Y", "Y", "Along Y axis", 1), + ("Z", "Z", "Along Z axis", 2), + ("AB", "AB", "Between 2 points", 3), + ("OD", "OD", "Origin and Direction", 4), + ] + + +def make_line(steps, center, direction, vert_a, vert_b): + if direction == "X": + vec = lambda l: (l, 0.0, 0.0) + elif direction == "Y": + vec = lambda l: (0.0, l, 0.0) + elif direction == "Z": + vec = lambda l: (0.0, 0.0, l) + elif direction in ["AB", "OD"]: + vec = lambda l: interp_v3_v3v3(vert_a, vert_b, l) + + verts = [] + add_vert = verts.append + x = -sum(steps) / 2 if center else 0 + for s in [0.0] + steps: + x = x + s + add_vert(vec(x)) + edges = [[i, i + 1] for i in range(len(steps))] + return verts, edges + + +class SvLineNodeMK3(bpy.types.Node, SverchCustomTreeNode): + """ + Triggers: Line, segment. + Tooltip: Generate line. + """ + bl_idname = 'SvLineNodeMK3' + bl_label = 'Line' + bl_icon = 'GRIP' + + def update_size_socket(self, context): + """ need to do UX transformation before updating node""" + size_socket = self.inputs["Size"] + size_socket.hide_safe = not self.normalize + + updateNode(self, context) + + def update_vect_socket(self, context): + """ need to do UX transformation before updating node""" + si = self.inputs + sd = self.direction + if sd == "OD" and not si[3].name[0] == "O": + si[3].name = "Origin" + si[4].name = "Direction" + si[3].prop_name = 'v3_origin' + si[4].prop_name = 'v3_dir' + elif sd == "AB" and not si[3].name[0] == "A": + si[3].name = "A" + si[4].name = "B" + si[3].prop_name = 'v3_input_0' + si[4].prop_name = 'v3_input_1' + + ortho = sd not in ["AB", "OD"] + if (not ortho and si[3].hide_safe) or ortho: + si[3].hide_safe = ortho + si[4].hide_safe = ortho + + updateNode(self, context) + + direction = EnumProperty( + name="Direction", items=directionItems, + default="X", update=update_vect_socket) + + num = IntProperty( + name='Num Verts', description='Number of Vertices', + default=2, min=2, update=updateNode) + + step = FloatProperty( + name='Step', description='Step length', + default=1.0, update=updateNode) + + center = BoolProperty( + name='Center', description='Center the line', + default=False, update=updateNode) + + normalize = BoolProperty( + name='Normalize', description='Normalize line to size', + default=False, update=update_size_socket) + + size = FloatProperty( + name='Size', description='Size of line', + default=10.0, update=updateNode) + + v3_input_0 = FloatVectorProperty( + name='A', description='Starting point', + size=3, default=(0, 0, 0), + update=updateNode) + + v3_input_1 = FloatVectorProperty( + name='B', description='End point', + size=3, default=(0.5, 0.5, 0.5), + update=updateNode) + + v3_origin = FloatVectorProperty( + name='Origin', description='Origin of line', + size=3, default=(0, 0, 0), + update=updateNode) + + v3_dir = FloatVectorProperty( + name='Direction', description='Direction', + size=3, default=(1, 1, 1), + update=updateNode) + + def set_size_socket(self): + size_socket = self.inputs.new('StringsSocket', "Size") + size_socket.prop_name = 'size' + size_socket.hide_safe = not self.normalize + + def set_vector_sockets(self): + si = self.inputs + si.new('VerticesSocket', "A").prop_name = 'v3_input_0' + si.new('VerticesSocket', "B").prop_name = 'v3_input_1' + si[3].hide_safe = self.direction not in ["AB", " OD"] + si[4].hide_safe = self.direction not in ["AB", " OD"] + + def sv_init(self, context): + si = self.inputs + si.new('StringsSocket', "Num").prop_name = 'num' + si.new('StringsSocket', "Step").prop_name = 'step' + self.set_size_socket() + self.set_vector_sockets() + self.outputs.new('VerticesSocket', "Vertices", "Vertices") + self.outputs.new('StringsSocket', "Edges", "Edges") + + def draw_buttons(self, context, layout): + col = layout.column(align=True) + row = col.row(align=True) + row.prop(self, "direction", expand=True) + row = col.row(align=True) + row.prop(self, "center", toggle=True) + row.prop(self, "normalize", toggle=True) + + def get_data(self): + c, d = self.center, self.direction + input_num = self.inputs["Num"].sv_get() + input_step = self.inputs["Step"].sv_get() + normal_size = [2.0 if c else 1.0] + + if self.normalize: + + normal_size = self.inputs["Size"].sv_get()[0] + + params = [input_num, input_step, normal_size] + + if d in ["AB", "OD"]: + v_a = self.inputs[3].sv_get()[0] + v_b = self.inputs[4].sv_get()[0] + params.append(v_a) + params.append(v_b) + + return match_long_repeat(params) + + def define_steplist(self, step_list, s, n, nor, normal): + + for num in n: + num = max(2, num) + s = s[:(num - 1)] # shorten if needed + fullList(s, num - 1) # extend if needed + step_list.append([S * nor / sum(s) for S in s] if normal else s) + + def process_vectors(self, pts_list, d, va, vb): + if d == "AB" and self.normalize: + vb = add_v3_v3v3(normalize(sub_v3_v3v3(vb, va)), va) + elif d == "OD": + vb = add_v3_v3v3(normalize(vb), va) + pts_list.append((va, vb)) + + def process(self): + if not any(s.is_linked for s in self.outputs): + return + + c, d = self.center, self.direction + step_list = [] + pts_list = [] + verts_out, edges_out = [], [] + normal = self.normalize or d == "AB" + advanced = d in ["AB", "OD"] + params = self.get_data() + if advanced: + for p in zip(*params): + n, s, nor, va, vb = p + self.define_steplist(step_list, s, n, nor, normal) + self.process_vectors(pts_list, d, va, vb) + for s, vc in zip(step_list, pts_list): + r1, r2 = make_line(s, c, d, vc[0], vc[1]) + verts_out.append(r1) + edges_out.append(r2) + else: + for p in zip(*params): + n, s, nor = p + self.define_steplist(step_list, s, n, nor, normal) + for s in step_list: + r1, r2 = make_line(s, c, d, [], []) + verts_out.append(r1) + edges_out.append(r2) + + if self.outputs['Vertices'].is_linked: + self.outputs['Vertices'].sv_set(verts_out) + if self.outputs['Edges'].is_linked: + self.outputs['Edges'].sv_set(edges_out) + + +def register(): + bpy.utils.register_class(SvLineNodeMK3) + + +def unregister(): + bpy.utils.unregister_class(SvLineNodeMK3) diff --git a/nodes/generator/line_mk2.py b/old_nodes/line_mk2.py similarity index 98% rename from nodes/generator/line_mk2.py rename to old_nodes/line_mk2.py index 319ee5449..fe9060b93 100644 --- a/nodes/generator/line_mk2.py +++ b/old_nodes/line_mk2.py @@ -48,6 +48,8 @@ class SvLineNodeMK2(bpy.types.Node, SverchCustomTreeNode): bl_label = 'Line MK2' bl_icon = 'GRIP' + replacement_nodes = [('SvLineNodeMK3', None, None)] + def upgrade_if_needed(self): """ This allows us to keep the node mk2 - on the fly node upgrade""" if "Size" not in self.inputs: diff --git a/utils/modules/geom_utils.py b/utils/modules/geom_utils.py index 2529f95be..1ad0870d4 100644 --- a/utils/modules/geom_utils.py +++ b/utils/modules/geom_utils.py @@ -38,6 +38,9 @@ def normalize(v): def sub_v3_v3v3(a, b): return a[0]-b[0], a[1]-b[1], a[2]-b[2] +def add_v3_v3v3(a, b): + return a[0]+b[0], a[1]+b[1], a[2]+b[2] + def madd_v3_v3v3fl(a, b, f=1.0): return a[0]+b[0]*f, a[1]+b[1]*f, a[2]+b[2]*f -- GitLab From 65f187fa98a967e5095c6a9d5a6bbdf3873606ba Mon Sep 17 00:00:00 2001 From: Dealga McArdle Date: Sun, 18 Nov 2018 11:00:07 +0100 Subject: [PATCH 07/60] small prophylactic for no output --- nodes/analyzer/kd_tree_edges_mk2.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/nodes/analyzer/kd_tree_edges_mk2.py b/nodes/analyzer/kd_tree_edges_mk2.py index e635bb24e..2c2a37ebd 100644 --- a/nodes/analyzer/kd_tree_edges_mk2.py +++ b/nodes/analyzer/kd_tree_edges_mk2.py @@ -63,6 +63,8 @@ class SvKDTreeEdgesNodeMK2(bpy.types.Node, SverchCustomTreeNode): try: verts = inputs['Verts'].sv_get()[0] linked = outputs['Edges'].is_linked + if not linked: + return except (IndexError, KeyError) as e: return -- GitLab From ab50e95716126c13bc746cf0ed41dca600384508 Mon Sep 17 00:00:00 2001 From: Alessandro Zomparelli Date: Sun, 18 Nov 2018 20:52:12 +0100 Subject: [PATCH 08/60] Added "BW" Grayscale and "A" Alpha channel support (#2314) * Added Alpha and BW grayscale * fixed grayscale --- nodes/analyzer/evaluate_image.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/nodes/analyzer/evaluate_image.py b/nodes/analyzer/evaluate_image.py index 66d455d50..2cc58c686 100644 --- a/nodes/analyzer/evaluate_image.py +++ b/nodes/analyzer/evaluate_image.py @@ -90,6 +90,8 @@ class EvaluateImageNode(bpy.types.Node, SverchCustomTreeNode): self.outputs.new('StringsSocket', "R") self.outputs.new('StringsSocket', "G") self.outputs.new('StringsSocket', "B") + self.outputs.new('StringsSocket', "A") + self.outputs.new('StringsSocket', "BW") def draw_buttons(self, context, layout): layout.label(text="Image:") @@ -133,11 +135,13 @@ class EvaluateImageNode(bpy.types.Node, SverchCustomTreeNode): else: domV = self.domV # outputs + bw = [[]] red = [[]] + alpha = [[]] green = [[]] blue = [[]] - if outputs['R'].is_linked or outputs['G'].is_linked or outputs['B'].is_linked: + if outputs['R'].is_linked or outputs['G'].is_linked or outputs['B'].is_linked or outputs['A'].is_linked or outputs['BW'].is_linked: imag = bpy.data.images[self.image_name].pixels[:] sizeU = bpy.data.images[self.image_name].size[0] sizeV = bpy.data.images[self.image_name].size[1] @@ -163,7 +167,6 @@ class EvaluateImageNode(bpy.types.Node, SverchCustomTreeNode): elif self.boundU == 'EXTEND': u = max(0,min(u,sizeU-1)) - if self.shift_mode_V == 'ALTERNATE': if (u0//sizeU)%2: v += floor(sizeV*self.shiftV) if self.shift_mode_V == 'CONSTANT': @@ -183,10 +186,16 @@ class EvaluateImageNode(bpy.types.Node, SverchCustomTreeNode): red[0].append(imag[index]) green[0].append(imag[index+1]) blue[0].append(imag[index+2]) + alpha[0].append(imag[index+3]) else: red[0].append(0) green[0].append(0) blue[0].append(0) + alpha[0].append(0) + if outputs['BW'].is_linked: + bw[0] = [0.2126*r + 0.7152*g + 0.0722*b for r,g,b in zip(red[0], green[0], blue[0])] + outputs['BW'].sv_set(bw) + outputs['A'].sv_set(alpha) outputs['R'].sv_set(red) outputs['G'].sv_set(green) outputs['B'].sv_set(blue) -- GitLab From 065af54b3a5b9e0aecc619c81533420a79f13d7b Mon Sep 17 00:00:00 2001 From: Dealga McArdle Date: Sun, 18 Nov 2018 20:56:39 +0100 Subject: [PATCH 09/60] Update evaluate_image.py --- nodes/analyzer/evaluate_image.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/nodes/analyzer/evaluate_image.py b/nodes/analyzer/evaluate_image.py index 2cc58c686..dd77a9496 100644 --- a/nodes/analyzer/evaluate_image.py +++ b/nodes/analyzer/evaluate_image.py @@ -141,10 +141,12 @@ class EvaluateImageNode(bpy.types.Node, SverchCustomTreeNode): green = [[]] blue = [[]] - if outputs['R'].is_linked or outputs['G'].is_linked or outputs['B'].is_linked or outputs['A'].is_linked or outputs['BW'].is_linked: + if any(socket.is_linked for socket in self.outputs): + imag = bpy.data.images[self.image_name].pixels[:] sizeU = bpy.data.images[self.image_name].size[0] sizeV = bpy.data.images[self.image_name].size[1] + for vert in verts: vx = vert[0]*(sizeU-1)/domU vy = vert[1]*sizeV/domV -- GitLab From 283a6627a736894609e0bc75ae0a5540196ea29a Mon Sep 17 00:00:00 2001 From: DolphinDream Date: Tue, 27 Nov 2018 18:11:23 -0500 Subject: [PATCH 10/60] Optimize the linear edge list generation using caching Profiling the edge generation indicated that it is a slow operation and since this edge list is generated over and over in various nodes it makes sense to cache it. Profiling of various nodes (generating edges) indicated a speedup of almost two orders of magnitude in generating the edges compared to the list comprehension counterpart (e.g. edges = [[i, i+1] for i in range(N)]. With the proposed optimization, the list of edges are stored in an edge cache (e.g. [[0,1], [1,2]... [n-1,n]] .. and the cache is extended as longer edge lists are requested/generated. Any call to get_edge_list will return a subset of this edge cache thus not having to re-generate the same list over and over. Various nodes like the line, spiral, torus knot, ellipse etc, which that generate lines with linear list of edges can benefit substantially from this speedup. To get the linear edge list use: edges = get_edge_list(n) returning: [[0, 1], [1, 2], ... , [n-1, n]] To get the loop edge list use: edges = get_edge_loop(n) returning: [[0, 1], [1, 2], ... , [n-2, n-1], [n-1, 0]] --- data_structure.py | 49 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/data_structure.py b/data_structure.py index fbc32660d..aabe229c1 100755 --- a/data_structure.py +++ b/data_structure.py @@ -927,3 +927,52 @@ def std_links_processing(matcher): return real_process return decorator + + +# EDGE CACHE settings : used to accellerate the (linear) edge list generation +_edgeCache = {} +_edgeCache["main"] = [] # e.g. [[0, 1], [1, 2], ... , [N-1, N]] (extended as needed) + + +def update_edge_cache(n): + """ + Extend the edge list cache to contain at least n edges. + + NOTE: This is called by the get_edge_list to make sure the edge cache is large + enough, but it can also be called preemptively by the nodes prior to making + multiple calls to get_edge_list in order to pre-augment the cache to a known + size and thus accellearate the subsequent calls to get_edge_list as they + will not have to augment the cache with every call. + """ + m = len(_edgeCache["main"]) # current number of edges in the edge cache + if n > m: # requested #edges < cached #edges ? => extend the cache + _edgeCache["main"].extend([[m + i, m + i + 1] for i in range(n - m)]) + + +def get_edge_list(n): + """ + Get the list of n edges connecting n+1 vertices. + + e.g. [[0, 1], [1, 2], ... , [n-1, n]] + + NOTE: This uses an "edge cache" to accellerate the edge list generation. + The cache is extended automatically as needed to satisfy the largest number + of edges within the node tree and it is shared by all nodes using this method. + """ + update_edge_cache(n) # make sure the edge cache is large enough + return _edgeCache["main"][:n] # return a subset list of the edge cache + + +def get_edge_loop(n): + """ + Get the loop list of n edges connecting n vertices. + + e.g. [[0, 1], [1, 2], ... , [n-2, n-1], [n-1, 0]] + + NOTE: This uses an "edge cache" to accellerate the edge list generation. + The cache is extended automatically as needed to satisfy the largest number + of edges within the node tree and it is shared by all nodes using this method. + """ + nn = n - 1 + update_edge_cache(nn) # make sure the edge cache is large enough + return _edgeCache["main"][:nn] + [[nn, 0]] -- GitLab From 503e629940279b8a1835f74f306c46d6bf85b3d5 Mon Sep 17 00:00:00 2001 From: nikitron Date: Mon, 3 Dec 2018 13:20:29 +0300 Subject: [PATCH 11/60] smooth about poligons --- docs/nodes/modifier_change/smooth.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/nodes/modifier_change/smooth.rst b/docs/nodes/modifier_change/smooth.rst index 7b1da4b4e..42f2e3504 100644 --- a/docs/nodes/modifier_change/smooth.rst +++ b/docs/nodes/modifier_change/smooth.rst @@ -13,7 +13,7 @@ This node has the following inputs: - **Vertices** - **Edges** -- **Faces** +- **Faces** - Only triangles and quads poligons. - **VertMask**. Selected vertices to be smoothed. - **Iterations** - **Clip threshold** @@ -26,7 +26,7 @@ Parameters This node has the following parameters: - **X**, **Y**, **Z**. Toggle axes vertices will be smoothed along. By default mesh is smoothed along all axes. -- **Laplacian Smooth**. Toggles smoothing algorithm: when checked, Laplacian smoothing is used; otherwise, simple averaging scheme will be used. By default not checked. +- **Laplacian Smooth**. Toggles smoothing algorithm: when checked, Laplacian smoothing is used; otherwise, simple averaging scheme will be used. By default not checked. __Deal only with tris and quads, not N-gons__. - **Clip X**, **Clip Y**, **Clip Z**. Toggle axes along which "Mirror Clipping" procedure will be applied. This procedure merges vertices that have X/Y/Z coordinate near zero, withing specified threshold. For example, it can merge vertices `(0.01, 3, 5)` and `(- 0.01, 3, 5)` into one vertex `(0, 3, 5)`. These parameters are available only when **Laplacian Smooth** is off. Not checked by default. - **Preserve volume**. If checked, the mesh will be "blown" a bit after smoothing, to preserve its volume. Available only when **Laplacian Smooth** is on. Checked by default. - **Iterations**. Number of smoothing operation iterations. Default value is 1. This parameter can also be provided as input. -- GitLab From f5424a4ca4dfa6a4c9cb8bc4dca799b18c212c5f Mon Sep 17 00:00:00 2001 From: nikitron Date: Mon, 3 Dec 2018 13:21:08 +0300 Subject: [PATCH 12/60] Update smooth.rst --- docs/nodes/modifier_change/smooth.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/nodes/modifier_change/smooth.rst b/docs/nodes/modifier_change/smooth.rst index 42f2e3504..b9dbbf2e9 100644 --- a/docs/nodes/modifier_change/smooth.rst +++ b/docs/nodes/modifier_change/smooth.rst @@ -26,7 +26,7 @@ Parameters This node has the following parameters: - **X**, **Y**, **Z**. Toggle axes vertices will be smoothed along. By default mesh is smoothed along all axes. -- **Laplacian Smooth**. Toggles smoothing algorithm: when checked, Laplacian smoothing is used; otherwise, simple averaging scheme will be used. By default not checked. __Deal only with tris and quads, not N-gons__. +- **Laplacian Smooth**. Toggles smoothing algorithm: when checked, Laplacian smoothing is used; otherwise, simple averaging scheme will be used. By default not checked. **Deal only with tris and quads, not N-gons**. - **Clip X**, **Clip Y**, **Clip Z**. Toggle axes along which "Mirror Clipping" procedure will be applied. This procedure merges vertices that have X/Y/Z coordinate near zero, withing specified threshold. For example, it can merge vertices `(0.01, 3, 5)` and `(- 0.01, 3, 5)` into one vertex `(0, 3, 5)`. These parameters are available only when **Laplacian Smooth** is off. Not checked by default. - **Preserve volume**. If checked, the mesh will be "blown" a bit after smoothing, to preserve its volume. Available only when **Laplacian Smooth** is on. Checked by default. - **Iterations**. Number of smoothing operation iterations. Default value is 1. This parameter can also be provided as input. -- GitLab From 9992a5bb2cb136b8fcb9c5773b851890fd083992 Mon Sep 17 00:00:00 2001 From: DolphinDream Date: Thu, 6 Dec 2018 22:26:03 -0500 Subject: [PATCH 13/60] Add new PolygonSort node to sort polygons by distance, area, and normal angle --- docs/nodes/list_mutators/polygon_sort.rst | 76 ++++++ index.md | 1 + nodes/list_mutators/polygon_sort.py | 289 ++++++++++++++++++++++ 3 files changed, 366 insertions(+) create mode 100644 docs/nodes/list_mutators/polygon_sort.rst create mode 100644 nodes/list_mutators/polygon_sort.py diff --git a/docs/nodes/list_mutators/polygon_sort.rst b/docs/nodes/list_mutators/polygon_sort.rst new file mode 100644 index 000000000..e3b844512 --- /dev/null +++ b/docs/nodes/list_mutators/polygon_sort.rst @@ -0,0 +1,76 @@ +Polygon Sort +============ + +Functionality +------------- + +Polygon Sort node sorts the input polygons by various criteria: distance, area and normal angle. + +Inputs +------ + +All parameters except **Mode** and **Descending** are vectorized and can take single of multiple input values. + +Parameters +---------- + +All parameters except **Mode** and **Descending** can be given by the node or an external input. + ++----------------+-----------+-----------+--------------------------------------------------------+ +| Param | Type | Default | Description | ++================+===========+===========+========================================================+ +| **Mode** | Enum | "D" | The sorting direction mode: | +| | | | P : Sort by the the distance to a point P | +| | | | D : Sort by the projection along a direction D | +| | | | A : Sort by the area of the polygons | +| | | | NP : Sort by the normal angle relative to point P | +| | | | ND : Sort by the normal angle relative to direction D | ++----------------+-----------+-----------+--------------------------------------------------------+ +| **Descending** | Bool  | False   | Sort the polygons in the descending order. | ++----------------+-----------+-----------+--------------------------------------------------------+ +| **Verts** | Vector  | none     | The vertices of the input mesh to be sorted. | ++----------------+-----------+-----------+--------------------------------------------------------+ +| **Polys** | Polygon  | none     | The polygons of the input mesh to be sorted. | ++----------------+-----------+-----------+--------------------------------------------------------+ +| **Point P** | Vector  | (1,0,0) | The reference vector: Point P. [1] | ++----------------+-----------+-----------+--------------------------------------------------------+ +| **Direction** | Vector  | (1,0,0) | The reference vector: Direction D. [1] | ++----------------+-----------+-----------+--------------------------------------------------------+ + +Notes: +[1] : "Point P" is shown in P and NP mode and "Direction" is shown in D and ND mode. These are used for distance and normal angle calculation. The area mode (A) does not use the input sorting vector. + +Outputs +------- + +**Indices** +The indices of the sorted polygons. + +**Vertices** +The input vertices. + +**Polygons** +The sorted polygons. + +**Quantities** +The quantity by which the polygons are sorted. Depending on the selected mode the output quantities are either Distances, Projections, Angles or Areas. + +Note: The output will be generated when the output sockets are connected. + +Modes +----- +The modes corespond to different sorting criteria and each one has a quanitity by which the polygons are sorted. +* P : In this mode the polygons are sorted by the distance from the center of each polygon to the given point P. +* D : In this mode the polygons are sorted by the projection component of polygon center vector along the given direction D. +* A : In this mode the polygons are sorted by the area of the polygons. +* ND : In this mode the polygons are sorted by the angle between the polygon normal and the given direction vector D. +* NP : In this mode the polygons are sorted by the angle between the polygon normal and the vector pointing form the center of the polygon to the given point P. + +Presets +------- +The node provides a set of predefined sort directions: along X, Y and Z. These buttons will set the mode to "D" and the sorting vector (Direction) to one of the X, Y or Z directions: (1,0,0), (0,1,0) and (0,0,1) respectively. The preset buttons are only visible as long as the sorting vector socket is not connected. + +References: +The algorythm to compute the area is based on descriptions found at this address: http://geomalgorithms.com/a01-_area.html#3D%20Polygons + + diff --git a/index.md b/index.md index bda1d1523..12c4a67e8 100644 --- a/index.md +++ b/index.md @@ -135,6 +135,7 @@ SvListModifierNode SvFixEmptyObjectsNode SvDatetimeStrings + SvPolygonSortNode ## List Main ListJoinNode diff --git a/nodes/list_mutators/polygon_sort.py b/nodes/list_mutators/polygon_sort.py new file mode 100644 index 000000000..b60270d99 --- /dev/null +++ b/nodes/list_mutators/polygon_sort.py @@ -0,0 +1,289 @@ +# ##### 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 bpy +from bpy.props import BoolProperty, EnumProperty, FloatVectorProperty, StringProperty +from sverchok.node_tree import SverchCustomTreeNode +from sverchok.data_structure import updateNode, fullList, match_long_repeat +from sverchok.utils.sv_operator_mixins import SvGenericCallbackWithParams +from mathutils import Vector +from math import acos, pi, sqrt + +modeItems = [ + ("P", "Point", "Sort by distance to a point", 0), + ("D", "Direction", "Sort by projection along a direction", 1), + ("A", "Area", "Sort by surface area", 2), + ("NP", "Normal Angle Point", "Sort by normal angle to point", 3), + ("ND", "Normal Angle Direction", "Sort by normal angle to direction", 4)] + +directions = {"X": [1, 0, 0], "Y": [0, 1, 0], "Z": [0, 0, 1]} + +socket_names = {"P": "Point", "D": "Direction", "A": "Area", "NP": "Normal", "ND": "Normal"} +socket_props = {"P": "point_P", "D": "point_D", "A": "point_D", "NP": "point_P", "ND": "point_D"} + +quantity_names = {"P": "Distances", "D": "Distances", "A": "Areas", "NP": "Angles", "ND": "Angles"} + + +def polygon_normal(verts, poly): + ''' The normal of the given polygon ''' + v1 = Vector(verts[poly[0]]) + v2 = Vector(verts[poly[1]]) + v3 = Vector(verts[poly[2]]) + v12 = v2 - v1 + v23 = v3 - v2 + normal = v12.cross(v23) + normal.normalize() + + return list(normal) + + +def polygon_area(verts, poly): + ''' The area of the given polygon ''' + if len(poly) < 3: # not a plane - no area + return 0 + + total = Vector([0, 0, 0]) + N = len(poly) + for i in range(N): + vi1 = Vector(verts[poly[i]]) + vi2 = Vector(verts[poly[(i + 1) % N]]) + prod = vi1.cross(vi2) + total[0] += prod[0] + total[1] += prod[1] + total[2] += prod[2] + + normal = Vector(polygon_normal(verts, poly)) + area = abs(total.dot(normal)) / 2 + + return area + + +def polygon_center(verts, poly): + ''' The center of the given polygon ''' + vx = 0 + vy = 0 + vz = 0 + for v in poly: + vx = vx + verts[v][0] + vy = vy + verts[v][1] + vz = vz + verts[v][2] + n = len(poly) + vx = vx / n + vy = vy / n + vz = vz / n + + return [vx, vy, vz] + + +def polygon_distance_P(verts, poly, P): + ''' The distance from the center of the polygon to the given point ''' + C = polygon_center(verts, poly) + CP = [C[0] - P[0], C[1] - P[1], C[2] - P[2]] + distance = sqrt(CP[0] * CP[0] + CP[1] * CP[1] + CP[2] * CP[2]) + + return distance + + +def polygon_distance_D(verts, poly, D): + ''' The projection of the polygon center vector along the given direction ''' + C = polygon_center(verts, poly) + distance = C[0] * D[0] + C[1] * D[1] + C[2] * D[2] + + return distance + + +def polygon_normal_angle_P(verts, poly, P): + ''' The angle between the polygon normal and the vector from polygon center to given point ''' + N = polygon_normal(verts, poly) + C = polygon_center(verts, poly) + V = [P[0] - C[0], P[1] - C[1], P[2] - C[2]] + v1 = Vector(N) + v2 = Vector(V) + v1.normalize() + v2.normalize() + angle = acos(v1.dot(v2)) # the angle in radians + + return angle + + +def polygon_normal_angle_D(verts, poly, D): + ''' The angle between the polygon normal and the given direction ''' + N = polygon_normal(verts, poly) + v1 = Vector(N) + v2 = Vector(D) + v1.normalize() + v2.normalize() + angle = acos(v1.dot(v2)) # the angle in radians + + return angle + + +class SvPolygonSortNode(bpy.types.Node, SverchCustomTreeNode): + """ + Triggers: Polygon, Sorting + Tooltip: Sort the polygons by various criteria: distance, angle, area. + """ + bl_idname = 'SvPolygonSortNode' + bl_label = 'Polygon Sort' + + def sort_polygons(self, verts, polys, V): + ''' Sort polygons and return sorted polygons indices, poly & quantities ''' + + if self.mode == "D": + quantities = [polygon_distance_D(verts, poly, V) for poly in polys] + elif self.mode == "P": + quantities = [polygon_distance_P(verts, poly, V) for poly in polys] + elif self.mode == "A": + quantities = [polygon_area(verts, poly) for poly in polys] + elif self.mode == "NP": + quantities = [polygon_normal_angle_P(verts, poly, V) for poly in polys] + elif self.mode == "ND": + quantities = [polygon_normal_angle_D(verts, poly, V) for poly in polys] + + IQ = [(i, q) for i, q in enumerate(quantities)] + sortedIQs = sorted(IQ, key=lambda kv: kv[1], reverse=self.descending) + + sortedIndices = [IQ[0] for IQ in sortedIQs] + sortedQuantities = [IQ[1] for IQ in sortedIQs] + sortedPolys = [polys[i] for i in sortedIndices] + + return sortedIndices, sortedPolys, sortedQuantities + + def update_sockets(self, context): + ''' Swap sorting vector input socket to P/D based on selected mode ''' + + s = self.inputs[-1] + s.name = socket_names[self.mode] + s.prop_name = socket_props[self.mode] + + # keep the P/D props values synced when changing mode + if self.mode == "P": + self.point_P = self.point_D + else: # self.mode == "D" + self.point_D = self.point_P + + # update output "Quantities" socket with proper name for the mode + o = self.outputs[-1] + o.name = quantity_names[self.mode] + + def set_direction(self, operator): + self.direction = operator.direction + self.mode = "D" + return {'FINISHED'} + + def update_xyz_direction(self, context): + self.point_D = directions[self.direction] + + def update_mode(self, context): + if self.mode == self.last_mode: + return + + self.last_mode = self.mode + self.update_sockets(context) + updateNode(self, context) + + direction = StringProperty( + name="Direction", default="X", update=update_xyz_direction) + + mode = EnumProperty( + name="Mode", items=modeItems, default="D", update=update_mode) + + last_mode = EnumProperty( + name="Last Mode", items=modeItems, default="D") + + point_P = FloatVectorProperty( + name="Point P", description="Reference point for distance and angle calculation", + size=3, default=(1, 0, 0), update=updateNode) + + point_D = FloatVectorProperty( + name="Direction", description="Reference direction for projection and angle calculation", + size=3, default=(1, 0, 0), update=updateNode) + + descending = BoolProperty( + name="Descending", description="Sort in the descending order", + default=False, update=updateNode) + + def sv_init(self, context): + self.inputs.new('VerticesSocket', "Verts") + self.inputs.new('StringsSocket', "Polys") + self.inputs.new('VerticesSocket', "Direction").prop_name = "point_D" + self.outputs.new('VerticesSocket', "Vertices") + self.outputs.new('StringsSocket', "Polygons") + self.outputs.new('StringsSocket', "Indices") + self.outputs.new('StringsSocket', "Distances") + + def draw_buttons(self, context, layout): + col = layout.column(align=False) + + if not self.inputs[-1].is_linked: + row = col.row(align=True) + for direction in "XYZ": + op = row.operator("node.set_sort_direction", text=direction) + op.direction = direction + + col = layout.column(align=True) + row = col.row(align=True) + row.prop(self, "mode", expand=False, text="") + layout.prop(self, "descending") + + def process(self): + if not any(s.is_linked for s in self.outputs): + return + + inputs = self.inputs + input_v = inputs["Verts"].sv_get() + input_p = inputs["Polys"].sv_get() + input_r = inputs[-1].sv_get()[0] # reference: direction or point + + params = match_long_repeat([input_v, input_p, input_r]) + + iList, vList, pList, qList = [], [], [], [] + for v, p, r in zip(*params): + indices, polys, quantities = self.sort_polygons(v, p, r) + iList.append(indices) + vList.append(v) + pList.append(polys) + qList.append(quantities) + + if self.outputs['Vertices'].is_linked: + self.outputs['Vertices'].sv_set(vList) + if self.outputs['Polygons'].is_linked: + self.outputs['Polygons'].sv_set(pList) + if self.outputs['Indices'].is_linked: + self.outputs['Indices'].sv_set(iList) + if self.outputs[-1].is_linked: + self.outputs[-1].sv_set(qList) # sorting quantities + + +class SvSetSortDirection(bpy.types.Operator, SvGenericCallbackWithParams): + bl_label = "Set sort direction" + bl_idname = "node.set_sort_direction" + bl_description = "Set the sorting direction along X, Y or Z" + + direction = StringProperty(default="X") + fn_name = StringProperty(default="set_direction") + + +def register(): + bpy.utils.register_class(SvSetSortDirection) + bpy.utils.register_class(SvPolygonSortNode) + + +def unregister(): + bpy.utils.unregister_class(SvPolygonSortNode) + bpy.utils.unregister_class(SvSetSortDirection) -- GitLab From 987fc40264ae3a48e535cfb54848003f9019807b Mon Sep 17 00:00:00 2001 From: Dealga McArdle Date: Sun, 9 Dec 2018 15:20:03 +0100 Subject: [PATCH 14/60] fixes lobsided edges (#2330) --- nodes/generators_extended/smooth_lines.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/nodes/generators_extended/smooth_lines.py b/nodes/generators_extended/smooth_lines.py index 10a9cc548..61b108401 100644 --- a/nodes/generators_extended/smooth_lines.py +++ b/nodes/generators_extended/smooth_lines.py @@ -37,6 +37,9 @@ def find_projected_arc_center(p1, p2, b, radius=0.5): b = Vector(b) c = Vector(p2) + a = (a-b).normalized() + b + c = (c-b).normalized() + b + focal = (a + c) / 2.0 focal_length = (b-focal).length -- GitLab From 98b16325d19762179b264883ed210d3d8122a506 Mon Sep 17 00:00:00 2001 From: Victor Doval <10011941+vicdoval@users.noreply.github.com> Date: Thu, 13 Dec 2018 11:50:23 +0100 Subject: [PATCH 15/60] added Tension Node --- index.md | 1 + nodes/analyzer/tension.py | 236 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 237 insertions(+) create mode 100644 nodes/analyzer/tension.py diff --git a/index.md b/index.md index 12c4a67e8..c5640df26 100644 --- a/index.md +++ b/index.md @@ -65,6 +65,7 @@ SvRaycasterLiteNode SvOBJInsolationNode EvaluateImageNode + SvTensionNode ## Transforms SvRotationNode diff --git a/nodes/analyzer/tension.py b/nodes/analyzer/tension.py new file mode 100644 index 000000000..7cea3b2a7 --- /dev/null +++ b/nodes/analyzer/tension.py @@ -0,0 +1,236 @@ +# ##### 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 bpy +from bpy.props import BoolProperty +from sverchok.node_tree import SverchCustomTreeNode +from sverchok.data_structure import updateNode, match_long_repeat, match_long_cycle +import numpy as np + + +def edge_elongation(np_verts, np_verts_n, np_edges): + '''Calculate edge length variation''' + + pairs_edges = np_verts[np_edges, :] + vect_rest = (pairs_edges[:, 0, :] - pairs_edges[:, 1, :]) + dist_rest = np.linalg.norm(vect_rest, axis=1) + pairs = np_verts_n[np_edges, :] + dif_v = pairs[:, 0, :] - pairs[:, 1, :] + dist = np.linalg.norm(dif_v, axis=1) + dif_l = dist - dist_rest + + return dif_l + + +def vert_edge_tension(dif_l, np_verts, np_edges): + '''Redistribute edge length variation to verts''' + x = dif_l[:, np.newaxis] / 2 + v_len = len(np_verts) + tension = np.zeros((v_len, v_len, 1), dtype=np.float32) + tension[np_edges[:, 0], np_edges[:, 1], :] = x + tension[np_edges[:, 1], np_edges[:, 0], :] = x + tension = np.sum(tension, axis=1)[:, 0] + return tension + + +def area_calc_setup(pols): + '''Analyze pols information''' + np_pols = np.array(pols) + p_len = len(pols) + if np_pols.dtype == np.object: + np_len = np.vectorize(len) + pols_sides = np_len(np_pols) + pols_sides_max = np.amax(pols_sides) + pols = match_long_cycle(pols) + np_pols = np.array(pols) + p_non_regular = True + else: + p_non_regular = False + pols_sides = np.array(p_len) + pols_sides_max = len(pols[0]) + + return [np_pols, pols_sides_max, pols_sides, p_len, p_non_regular] + + +def get_normals(v_pols): + '''calculate polygon normals''' + v1 = v_pols[:, 1, :] - v_pols[:, 0, :] + v2 = v_pols[:, 2, :] - v_pols[:, 0, :] + pols_normal = np.cross(v1, v2) + pols_normal_d = np.linalg.norm(pols_normal, axis=1) + + return pols_normal / pols_normal_d[:, np.newaxis] + + +def area_calc(np_verts, area_params): + '''Calculate polygons area''' + np_pols, pols_sides_max, pols_sides, p_len, p_non_regular = area_params + + v_pols = np_verts[np_pols, :] + pols_normal = get_normals(v_pols) + + prod = np.zeros((pols_sides_max, p_len, 3), dtype=np.float32) + if p_non_regular: + + for i in range(pols_sides_max): + mask = pols_sides > i + end_i = (i + 1) % pols_sides_max + prod[i, mask, :] = np.cross(v_pols[mask, i, :], v_pols[mask, end_i, :]) + + prod = np.sum(prod, axis=0) + area = abs(np.sum(prod * pols_normal, axis=1) / 2) + + else: + for i in range(pols_sides_max): + end_i = (i + 1) % pols_sides_max + prod[i, :, :] = np.cross(v_pols[:, i, :], v_pols[:, end_i, :]) + + prod = np.sum(prod, axis=0) + area = abs(np.sum(prod * pols_normal, axis=1) / 2) + + return area + + +def area_to_verts(np_verts, area_params, pols_tension): + '''Redistribute area variation to verts''' + np_pols, pols_sides_max, pols_sides, p_len, advance = area_params + + pol_id = np.arange(p_len) + pol_tens_to_vert = pols_tension / pols_sides + tension = np.zeros((len(np_verts), p_len), dtype=np.float32) + if advance: + for i in range(pols_sides_max): + mask = pols_sides > i + tension[np_pols[mask, i], pol_id[mask]] += pol_tens_to_vert[mask] + + else: + for i in range(pols_sides_max): + tension[np_pols[:, i], pol_id] += pols_tension + + return np.sum(tension, axis=1) + + +def calc_pols_tension(np_verts, np_verts_n, area_params): + relax_area = area_calc(np_verts, area_params) + tension_area = area_calc(np_verts_n, area_params) + return tension_area - relax_area + + +def calc_tensions(meshes, gates, result): + + for vertices, vertices_n, edges, pols in zip(*meshes): + np_verts = np.array(vertices) + np_verts_n = np.array(vertices_n) + if len(edges) > 0 and (gates[0] or gates[1]): + np_edges = np.array(edges) + dif_l = edge_elongation(np_verts, np_verts_n, np_edges) + result[0].append(dif_l if gates[4] else dif_l.tolist()) + if gates[1]: + tension = vert_edge_tension(dif_l, np_verts, np_edges) + result[1].append(tension if gates[4] else tension.tolist()) + + if len(pols) > 0 and (gates[2] or gates[3]): + area_params = area_calc_setup(pols) + pols_tension = calc_pols_tension(np_verts, np_verts_n, area_params) + result[2].append(pols_tension if gates[4] else pols_tension.tolist()) + if gates[3]: + tens_verts_pols = area_to_verts(np_verts, area_params, pols_tension) + result[3].append(tens_verts_pols if gates[4] else tens_verts_pols.tolist()) + + return result + + +class SvTensionNode(bpy.types.Node, SverchCustomTreeNode): + ''' + Triggers: Measure deformation + Tooltip: Measure deformation + ''' + bl_idname = 'SvTensionNode' + bl_label = 'Tension' + bl_icon = 'SNAP_NORMAL' + + output_numpy = BoolProperty( + name='Output NumPy', description='output NumPy arrays', + default=False, update=updateNode) + + def draw_buttons_ext(self, context, layout): + layout.prop(self, "output_numpy", toggle=False) + + def sv_init(self, context): + sinw = self.inputs.new + sonw = self.outputs.new + sinw('VerticesSocket', "Rest Verts") + sinw('VerticesSocket', "Distort Verts") + sinw('StringsSocket', "Edges") + sinw('StringsSocket', "Pols") + + sonw('StringsSocket', "Edges_Tension") + sonw('StringsSocket', "Pols_Tension") + sonw('StringsSocket', "Vert_Edge_T") + sonw('StringsSocket', "Vert_Pol_T") + + def get_data(self): + si = self.inputs + vertices_s = si['Rest Verts'].sv_get(default=[[]]) + vertices_n = si['Distort Verts'].sv_get(default=[[]]) + edges_in = si['Edges'].sv_get(default=[[]]) + pols_in = si['Pols'].sv_get(default=[[]]) + + return match_long_repeat([vertices_s, vertices_n, edges_in, pols_in]) + + def ready(self): + '''check if there are the needed links''' + si = self.inputs + so = self.outputs + ready = any(s.is_linked for s in so) + ready = ready and si[0].is_linked and si[1].is_linked + ready = ready and (si[2].is_linked or si[3].is_linked) + return ready + + def process(self): + so = self.outputs + if not self.ready(): + return + + result = [[], [], [], []] + gates = [] + gates.append(so['Edges_Tension'].is_linked) + gates.append(so['Vert_Edge_T'].is_linked) + gates.append(so['Pols_Tension'].is_linked) + gates.append(so['Vert_Pol_T'].is_linked) + gates.append(self.output_numpy) + meshes = self.get_data() + result = calc_tensions(meshes, gates, result) + + if gates[0]: + so['Edges_Tension'].sv_set(result[0]) + if gates[1]: + so['Vert_Edge_T'].sv_set(result[1]) + if gates[2]: + so['Pols_Tension'].sv_set(result[2]) + if gates[3]: + so['Vert_Pol_T'].sv_set(result[3]) + + +def register(): + bpy.utils.register_class(SvTensionNode) + + +def unregister(): + bpy.utils.unregister_class(SvTensionNode) -- GitLab From 88c92e9c202b9e919c68f3a870001042d3dbd835 Mon Sep 17 00:00:00 2001 From: Kosvor2 Date: Mon, 17 Dec 2018 22:05:42 +0500 Subject: [PATCH 16/60] Update tree_generator.py --- node_scripts/templates/elfnor/tree_generator.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/node_scripts/templates/elfnor/tree_generator.py b/node_scripts/templates/elfnor/tree_generator.py index 9e5b88d52..dcea60d0f 100644 --- a/node_scripts/templates/elfnor/tree_generator.py +++ b/node_scripts/templates/elfnor/tree_generator.py @@ -155,7 +155,7 @@ class SCA: if p not in finished: process.insert(0, p) - mats= [] + mats_out= [] for edge in edges: if ends[edge[1]]: #calculate leaf directions @@ -171,10 +171,8 @@ class SCA: m[2][0:3] = dir3 m[3][0:3] = v1 m.transpose() - mats.append(m) + mats_out.append(m) - mats_out = Matrix_listing(mats) - return verts, edges, ends, br, mats_out def sv_main(npoints=100 , dist=0.05, min_dist=0.05, max_dist=2.0, tip_radius=0.01, trop=[], verts_in=[], verts_start=[]): -- GitLab From 421d2468504e4ac93aa749b0b877d141a8bf28c2 Mon Sep 17 00:00:00 2001 From: DolphinDream Date: Thu, 13 Dec 2018 15:50:13 -0500 Subject: [PATCH 17/60] Add node to compute various statistical quantities, currently supporting the following: Sum Sum Of Squares Product Average Geometric Mean Harmonic Mean Standard Deviation Root Mean Square Skewness Kurtosis Minimum Maximum Median Percentile Histogram --- docs/nodes/list_main/statistics.rst | 66 +++++++++ index.md | 1 + nodes/list_main/statistics.py | 188 ++++++++++++++++++++++++++ utils/modules/statistics_functions.py | 121 +++++++++++++++++ 4 files changed, 376 insertions(+) create mode 100644 docs/nodes/list_main/statistics.rst create mode 100644 nodes/list_main/statistics.py create mode 100644 utils/modules/statistics_functions.py diff --git a/docs/nodes/list_main/statistics.rst b/docs/nodes/list_main/statistics.rst new file mode 100644 index 000000000..c63a29f19 --- /dev/null +++ b/docs/nodes/list_main/statistics.rst @@ -0,0 +1,66 @@ +List Statistics +=============== + +Functionality +------------- + +List Statistics node computes various statistical quantities for the values in a list. + +Inputs +------ + +The **Data** input is expected to be a list of integers / floats or list of lists of integers / floats. +All inputs are vectorized. + +Parameters +---------- + +The **Function** parameter allows to select the statistical function to compute the corresponding statistical quantity for the input values. + ++----------------+---------------------+---------+------------------------------------------+ +| Param | Type | Default | Description | ++================+=====================+=========+==========================================+ +| **Function** | Enum | Average | The statistical function applied to | +| | All Statistics | | the input values. | +| | Sum | | | +| | Sum Of Squares | | For "All Statistics" selection the node | +| | Product | | computes and outputs the statistical | +| | Average | | quantities for all the statistical | +| | Geometric Mean | | functions along with their corresponding | +| | Harmonic Mean | | names. | +| | Standard Deviation | | | +| | Root Mean Square | | | +| | Skewness | | | +| | Kurtosis | | | +| | Minimum | | | +| | Maximum | | | +| | Median | | | +| | Percentile | | | +| | Histogram | | | ++----------------+---------------------+---------+------------------------------------------+ +| **Percentage** | Float | 0.75 | The percentage value for the | +| | | | percentile function. [1] | ++----------------+---------------------+---------+------------------------------------------+ +| **Normalize** | Boolean | False | Flag to normalize the histogram bins | +| | | | to the given normalize size. [2] | ++----------------+---------------------+---------+------------------------------------------+ +| **Bins** | Int | 10 | The number of bins in the histogram. [2] | ++----------------+---------------------+---------+------------------------------------------+ +| **Size** | Float | 10.00 | The normalized size of the histogram.[2] | ++----------------+---------------------+---------+------------------------------------------+ + +Notes: +[1] : The **Percentage** input socket is available only for the **Percentile** function. +[2] : The **Normalize** setting and the **Bins** and **Size** input sockets are available only for the **Histogram** function. + +Outputs +------- +**Name(s)** +The name(s) of the statistical value(s) computed corresponding to the selected statistical function. + +**Value(s)** +The statistical quantity of the input values corresponding to the selected function. For a vectorized input the output values are a series of quantities corresponding to the selected function. + +When "All Statistics" is selected the **Names** and **Values** outputs will list the names and the corresponding values for all the statistical functions. + + diff --git a/index.md b/index.md index 12c4a67e8..eadce3228 100644 --- a/index.md +++ b/index.md @@ -146,6 +146,7 @@ ListMatchNode ListFuncNode SvListDecomposeNode + SvListStatisticsNode ## List Struct ShiftNodeMK2 diff --git a/nodes/list_main/statistics.py b/nodes/list_main/statistics.py new file mode 100644 index 000000000..86b033d17 --- /dev/null +++ b/nodes/list_main/statistics.py @@ -0,0 +1,188 @@ +# ##### 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 bpy +from bpy.props import EnumProperty, IntProperty, FloatProperty, BoolProperty + +from sverchok.node_tree import SverchCustomTreeNode +from sverchok.data_structure import updateNode, match_long_repeat +from sverchok.utils.modules.statistics_functions import * + +functions = { + "ALL STATISTICS": (0, 0), + "SUM": (10, get_sum), + "SUM OF SQUARES": (11, get_sum_of_squares), + "SUM OF INVERSIONS": (12, get_sum_of_inversions), + "PRODUCT": (13, get_product), + "AVERAGE": (14, get_average), + "GEOMETRIC MEAN": (15, get_geometric_mean), + "HARMONIC MEAN": (16, get_harmonic_mean), + "STANDARD DEVIATION": (17, get_standard_deviation), + "ROOT MEAN SQUARE": (18, get_root_mean_square), + "SKEWNESS": (19, get_skewness), + "KURTOSIS": (20, get_kurtosis), + "MINIMUM": (21, get_minimum), + "MAXIMUM": (22, get_maximum), + "MEDIAN": (23, get_median), + "PERCENTILE": (24, get_percentile), + "HISTOGRAM": (25, get_histogram) +} + + +modeItems = [ + ("INT", "Int", "", "", 0), + ("FLOAT", "Float", "", "", 1)] + +functionItems = [(k, k.title(), "", "", s[0]) for k, s in sorted(functions.items(), key=lambda k: k[1][0])] + + +class SvListStatisticsNode(bpy.types.Node, SverchCustomTreeNode): + ''' + Triggers: Sum, Avg, Min, Max + Tooltip: Statistical quantities: sum, average, standard deviation, min, max, product... + ''' + bl_idname = 'SvListStatisticsNode' + bl_label = 'List Statistics' + bl_icon = 'OUTLINER_OB_EMPTY' + + def update_function(self, context): + if self.function == "ALL STATISTICS": + self.inputs["Percentage"].hide_safe = False + self.inputs["Bins"].hide_safe = False + self.inputs["Size"].hide_safe = not self.normalize + self.outputs[0].name = "Names" + self.outputs[1].name = "Values" + else: + for name in ["Percentage", "Bins", "Size"]: + self.inputs[name].hide_safe = True + if self.function == "PERCENTILE": + self.inputs["Percentage"].hide_safe = False + elif self.function == "HISTOGRAM": + self.inputs["Bins"].hide_safe = False + self.inputs["Size"].hide_safe = not self.normalize + + self.outputs[0].name = "Name" + self.outputs[1].name = "Value" + + updateNode(self, context) + + def update_normalize(self, context): + socket = self.inputs["Size"] + socket.hide_safe = not self.normalize + + updateNode(self, context) + + mode = EnumProperty( + name="Mode", items=modeItems, default="FLOAT", update=updateNode) + + function = EnumProperty( + name="Function", items=functionItems, update=update_function) + + percentage = FloatProperty( + name="Percentage", + default=0.75, min=0.0, max=1.0, update=updateNode) + + bins = IntProperty( + name="Bins", + default=10, min=1, update=updateNode) + + normalize = BoolProperty( + name="Normalize", description="Normalize the bins to a normalize size", + default=False, update=update_normalize) + + normalized_size = FloatProperty( + name="Size", description="The normalized size of the bins", + default=10.0, update=updateNode) + + def draw_buttons(self, context, layout): + layout.prop(self, "mode", expand=True) + layout.prop(self, "function", text="") + if self.function in ["HISTOGRAM", "ALL STATISTICS"]: + layout.prop(self, "normalize", toggle=True) + + def sv_init(self, context): + self.width = 150 + self.inputs.new('StringsSocket', "Data") + self.inputs.new('StringsSocket', "Percentage").prop_name = "percentage" + self.inputs.new('StringsSocket', "Bins").prop_name = "bins" + self.inputs.new('StringsSocket', "Size").prop_name = "normalized_size" + self.outputs.new('StringsSocket', "Names") + self.outputs.new('StringsSocket', "Values") + self.function = "AVERAGE" + + def get_statistics_function(self): + return functions[self.function][1] + + def process(self): + outputs = self.outputs + # return if no outputs are connected + if not any(s.is_linked for s in outputs): + return + + inputs = self.inputs + input_D = inputs["Data"].sv_get() + input_P = inputs["Percentage"].sv_get()[0] + input_B = inputs["Bins"].sv_get()[0] + input_S = inputs["Size"].sv_get()[0] + + # sanitize the inputs + input_P = list(map(lambda x: max(0, min(1, x)), input_P)) + input_B = list(map(lambda x: max(1, x), input_B)) + + if self.mode == "INT": + input_P = list(map(lambda x: int(x), input_P)) + + if self.function == "ALL STATISTICS": + functionNames = [fn[0] for fn in functionItems[1:]] + else: + functionNames = [self.function] + + params = match_long_repeat([input_D, input_P, input_B, input_S]) + + allNames = [] + allValues = [] + for functionName in functionNames: + statistics_function = functions[functionName][1] + quantityList = [] + for d, p, b, s in zip(*params): + if functionName == "PERCENTILE": + quantity = statistics_function(d, p) + elif functionName == "HISTOGRAM": + quantity = statistics_function(d, b, self.normalize, s) + else: + quantity = statistics_function(d) + + if functionName != "HISTOGRAM": + if self.mode == "INT": + quantity = int(quantity) + + quantityList.append(quantity) + + allNames.append(functionName) + allValues.append(quantityList) + + outputs[0].sv_set(allNames) + outputs[1].sv_set(allValues) + + +def register(): + bpy.utils.register_class(SvListStatisticsNode) + + +def unregister(): + bpy.utils.unregister_class(SvListStatisticsNode) diff --git a/utils/modules/statistics_functions.py b/utils/modules/statistics_functions.py new file mode 100644 index 000000000..193c5b61c --- /dev/null +++ b/utils/modules/statistics_functions.py @@ -0,0 +1,121 @@ +# ##### 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 ##### + +from functools import reduce +from math import sqrt, floor +import sys + + +def get_sum(values): + return sum(values) + + +def get_sum_of_squares(values): + return sum([v * v for v in values]) + + +def get_sum_of_inversions(values): + return sum([1.0 / v for v in values]) + + +def get_product(values): + return reduce((lambda x, y: x * y), values) + + +def get_average(values): + return sum(values) / len(values) + + +def get_geometric_mean(values): + return pow(get_product(values), 1.0 / len(values)) + + +def get_harmonic_mean(values): + return len(values) / get_sum_of_inversions(values) + + +def get_standard_deviation(values): + a = get_average(values) + return sqrt(sum([(v - a)**2 for v in values])) + + +def get_root_mean_square(values): + return sqrt(get_sum_of_squares(values) / len(values)) + + +def get_skewness(values): + a = get_average(values) + n = len(values) + s = get_standard_deviation(values) + return sum([(v - a)**3 for v in values]) / n / pow(s, 3) + + +def get_kurtosis(values): + a = get_average(values) + n = len(values) + s = get_standard_deviation(values) + return sum([(v - a)**4 for v in values]) / n / pow(s, 4) + + +def get_minimum(values): + return min(values) + + +def get_maximum(values): + return max(values) + + +def get_median(values): + sortedValues = sorted(values) + index = int(floor(len(values) / 2)) + print("index=", index) + if len(values) % 2 == 0: # even number of values ? => take the average of central values + median = (sortedValues[index - 1] + sortedValues[index]) / 2 + else: # odd number of values ? => take the central value + median = sortedValues[index] + + return median + + +def get_percentile(values, percentage): + sortedValues = sorted(values) + index = int(min(int(floor(len(values) * percentage)), len(values) - 1)) + return sortedValues[index] + + +def get_histogram(values, numBins, normalize=False, normalizedSize=10): + minValue = get_minimum(values) + maxValue = get_maximum(values) + + binSize = max((maxValue - minValue) / numBins, sys.float_info.min) + + # initialize the histogram bins + histogram = [0] * numBins + + # populate the histogram bins + for i in range(len(values)): + binIndex = int(min(int(floor((values[i] - minValue) / binSize)), numBins - 1)) + histogram[binIndex] = histogram[binIndex] + 1 + + # normalize histogram ? + if normalize: + binMax = max(histogram) + for i in range(len(histogram)): + histogram[i] = histogram[i] / binMax * normalizedSize + + return histogram -- GitLab From 5f6c38d67ddb5433081da644a24d43a68b563dd1 Mon Sep 17 00:00:00 2001 From: DolphinDream Date: Sun, 23 Dec 2018 15:11:40 -0500 Subject: [PATCH 18/60] Add scale/multiplier to TextureViewerLite node to fix rendering Port the same preference settings for xy/scale into the TVL node to fix the location and the size of the rendering. --- nodes/viz/texture_viewer_lite.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/nodes/viz/texture_viewer_lite.py b/nodes/viz/texture_viewer_lite.py index 9363d3771..ddb88be9b 100644 --- a/nodes/viz/texture_viewer_lite.py +++ b/nodes/viz/texture_viewer_lite.py @@ -24,6 +24,7 @@ from bpy.props import EnumProperty, StringProperty, IntProperty from sverchok.data_structure import updateNode, node_id from sverchok.node_tree import SverchCustomTreeNode from sverchok.ui import nodeview_bgl_viewer_draw_mk2 as nvBGL2 +from sverchok.utils.context_managers import sv_preferences gl_color_list = [ @@ -153,11 +154,25 @@ class SvTextureViewerNodeLite(bpy.types.Node, SverchCustomTreeNode): bgl.glGenTextures(1, name) self.texture[n_id] = name[0] init_texture(width, height, name[0], texture, gl_color_dict.get(colm)) + + # adjust render location based on preference multiplier setting + try: + with sv_preferences() as prefs: + multiplier = prefs.render_location_xy_multiplier + scale = prefs.render_scale + except: + # print('did not find preferences - you need to save user preferences') + multiplier = 1.0 + scale = 1.0 + + x, y = [self.location[0] * multiplier, self.location[1] * multiplier] + width, height =[width * scale, height * scale] + draw_data = { 'tree_name': self.id_data.name, 'mode': 'custom_function', 'custom_function': simple_screen, - 'loc': (self.location[0] + self.width + 20, self.location[1]), + 'loc': (x + self.width*scale + 20, y), 'args': (texture, self.texture[n_id], width, height) } nvBGL2.callback_enable(n_id, draw_data) -- GitLab From ad968de5475124312aa484ca8814c02402435dce Mon Sep 17 00:00:00 2001 From: DolphinDream Date: Wed, 21 Feb 2018 19:20:23 -0500 Subject: [PATCH 19/60] Add new combinatorics node (WIP) The combinatoric operations implemented so far, based on itertools module are: PRODUCT PERMUTATION COMBINATION --- index.md | 1 + nodes/list_mutators/combinatorics.py | 181 +++++++++++++++++++++++++++ 2 files changed, 182 insertions(+) create mode 100644 nodes/list_mutators/combinatorics.py diff --git a/index.md b/index.md index eadce3228..38f112c9a 100644 --- a/index.md +++ b/index.md @@ -314,6 +314,7 @@ --- SvSpiralNode SvExportGcodeNode + SvCombinatoricsNode ## Alpha Nodes SvCurveViewerNode diff --git a/nodes/list_mutators/combinatorics.py b/nodes/list_mutators/combinatorics.py new file mode 100644 index 000000000..a8d89bd57 --- /dev/null +++ b/nodes/list_mutators/combinatorics.py @@ -0,0 +1,181 @@ +# ##### 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 bpy +from bpy.props import BoolProperty, IntProperty, FloatProperty, EnumProperty + +from sverchok.node_tree import SverchCustomTreeNode +from sverchok.data_structure import (match_long_repeat, updateNode) + +from itertools import (product, permutations, combinations, compress) + +operationItems = { + ("PRODUCT", "Product", "", 0), + ("PERMUTATIONS", "Permutations", "", 1), + ("COMBINATIONS", "Combinations", "", 2), +} + +ABC = tuple('ABCDEFGHIJKLMNOPQRSTUVWXYZ') # input socket labels + +multiple_input_operations = { "PRODUCT" } + +class SvCombinatoricsNode(bpy.types.Node, SverchCustomTreeNode): + """ + Triggers: Product, Permutation, Combination + Tooltip: Generate various combinatoric operations + """ + bl_idname = 'SvCombinatoricsNode' + bl_label = 'Combinatorics' + + def update_operation(self, context): + self.label = self.operation.title() + self.update_sockets() + updateNode(self, context) + + operation = EnumProperty( + name="Operation", items=operationItems, + description="Operation type", default="PRODUCT", + update=update_operation) + + repeat = IntProperty( + name='Repeat', description='Repeat the sequence', + default=1, min=1, update=updateNode) + + length = IntProperty( + name='Lenght', description='Lenght of the sequence', + default=1, min=1, update=updateNode) + + def sv_init(self, context): + self.inputs.new('StringsSocket', "Repeat").prop_name = "repeat" + self.inputs.new('StringsSocket', "Length").prop_name = "length" + self.inputs.new('StringsSocket', "Selector") + self.inputs.new('StringsSocket', "A") + self.inputs.new('StringsSocket', "B") + + self.outputs.new('StringsSocket', "Result") + + self.operation = "PRODUCT" + + def update(self): + ''' Add/remove sockets as A-Z sockets are connected/disconnected ''' + + # not a multiple quaternion operation ? => no need to update sockets + if self.operation not in multiple_input_operations: + return + + inputs = self.inputs + + # get all existing A-Z sockets (connected or not) + inputs_AZ = list(filter(lambda s: s.name in ABC, inputs)) + + # last A-Z socket connected ? => add an empty A-Z socket at the end + if inputs_AZ[-1].links: + name = ABC[len(inputs_AZ)] # pick the next letter A to Z + inputs.new("StringsSocket", name) + + else: # last input disconnected ? => remove all but last unconnected + while len(inputs_AZ) > 2 and not inputs_AZ[-2].links: + s = inputs_AZ[-1] + inputs.remove(s) + inputs_AZ.remove(s) + + def update_sockets(self): + ''' Update sockets based on current mode ''' + + inputs = self.inputs + + # update the A-Z input sockets + if self.operation in multiple_input_operations: + if not "B" in inputs: + inputs.new("StringsSocket", "B") + else: + print("remove sockets") + for a in ABC[1:]: # remove all B-Z inputs (keep A) + if a in inputs: + inputs.remove(inputs[a]) + + # update the other sockets (operation specific) + if self.operation in { "PRODUCT" }: + inputs["Repeat"].hide_safe = False + inputs["Length"].hide_safe = True + elif self.operation in { "COMBINATIONS", "PERMUTATIONS" }: + inputs["Repeat"].hide_safe = True + inputs["Length"].hide_safe = False + + def draw_buttons(self, context, layout): + layout.prop(self, "operation", text="") + + def process(self): + outputs = self.outputs + # return if no outputs are connected + if not any(s.is_linked for s in outputs): + return + + # input values lists (single or multi value) + inputs = self.inputs + + all_AZ_sockets = list(filter(lambda s: s.name in ABC, inputs)) + connected_AZ_sockets = list(filter(lambda s: s.is_linked, all_AZ_sockets)) + + I = [] # list of all data inputs (single or multiple) + + # collect the data inputs from all connected AZ sockets + for s in connected_AZ_sockets: + a = s.sv_get()[0] + I.append(a) + + resultList = [] + + if self.operation == "PRODUCT": + R = inputs["Repeat"].sv_get()[0] + R = list(map(lambda x: max(1, int(x)), R)) + parameters = match_long_repeat([[I], R]) + for sequence, r in zip(*parameters): + result = product(*sequence, repeat=r) + result = [list(a) for a in result] + resultList.append(result) + + elif self.operation == "PERMUTATIONS": + L = inputs["Length"].sv_get()[0] + L = list(map(lambda x: max(1, int(x)), L)) + parameters = match_long_repeat([I, L]) + for sequence, l in zip(*parameters): + l = min(l, len(sequence)) + result = permutations(sequence, l) + result = [list(a) for a in result] + resultList.append(result) + + elif self.operation == "COMBINATIONS": + L = inputs["Length"].sv_get()[0] + L = list(map(lambda x: max(1, int(x)), L)) + parameters = match_long_repeat([I, L]) + for sequence, l in zip(*parameters): + l = min(l, len(sequence)) + result = combinations(sequence, l) + result = [list(a) for a in result] + resultList.append(result) + + outputs["Result"].sv_set(resultList) + + +def register(): + bpy.utils.register_class(SvCombinatoricsNode) + + +def unregister(): + bpy.utils.unregister_class(SvCombinatoricsNode) -- GitLab From 321544a3b8500b0cda9ea27b280ae7f2edb36a6f Mon Sep 17 00:00:00 2001 From: DolphinDream Date: Fri, 28 Dec 2018 19:26:31 -0500 Subject: [PATCH 20/60] Add documentation + optimize and cleanup code --- docs/nodes/list_mutators/combinatorics.rst | 142 +++++++++++++++++++++ nodes/list_mutators/combinatorics.py | 86 ++++++------- 2 files changed, 178 insertions(+), 50 deletions(-) create mode 100644 docs/nodes/list_mutators/combinatorics.rst diff --git a/docs/nodes/list_mutators/combinatorics.rst b/docs/nodes/list_mutators/combinatorics.rst new file mode 100644 index 000000000..67104801d --- /dev/null +++ b/docs/nodes/list_mutators/combinatorics.rst @@ -0,0 +1,142 @@ +Combinatorics +============= + +Functionality +------------- + +Combinatorics node performs various combinatoric operations like: **Product**, **Permutations** and **Combinations**. + + +Inputs +------ + +The inputs to the node are a set of lists of any type and a set of control parameters. + +The list inputs in combination to the control parameter inputs (Repeat / Length) are vectorized and the control parameters accept either single or multiple values for vectorization. + +List inputs to the node: +- **A** +- **B** [1] +... +- **Z** [1] + +Notes: +[1] : The multiple list inputs are available for the **Product** operation, all the other operations take one list input. For the **Product** operation as the last list input is connected a new empty input socket will appear to allow other lists to be connected. + + +Parameters +---------- + +The **Operation** parameter allows to select one of following operations: Product, Permutations and Combinations. + +All parameters except **Operation** can be given as an external input. + ++---------------+---------------+----------+--------------------------------------------+ +| Param | Type | Default | Description | ++===============+===============+==========+============================================+ +| **Operation** | Enum: | Product | See details in the Operations section. | +| | Product | | | +| | Permutations | | | +| | Combinations | | | ++---------------+---------------+----------+--------------------------------------------+ +| **Repeat** | Int | 1 | Repeat the input lists this many times [1] | ++---------------+---------------+----------+--------------------------------------------+ +| **Length** | Int | 1 | The number of the elements in the list to | +| | | | operate on [2] | ++---------------+---------------+----------+--------------------------------------------+ +| **A** | List | | The list of elements to operate on. | ++---------------+---------------+----------+--------------------------------------------+ +| **B..Z** | List | | Additional lists to operate on [3] | ++---------------+---------------+----------+--------------------------------------------+ + +Notes: +[1] : The Repeat parameter is only available for the **Product** operation. +[2] : The Length parameter is only available for the **Permutations** and **Combinations** operation. +[3] : Additional lists inputs are available only for the **Product** operation. + +Operations +---------- + +**Product** + +For this operation the node allows an arbitrary number of input lists to be product together as: A x B x .. x Z. The result of the product operation is a list of elements each of size equal to the number of input lists and has all the combinations of elements from the first list, followed by all elements in the second list etc. + +e.g. for two connected list inputs: + +A : ["X", "Y"] +B : [1, 2, 3] + +The result A x B is: + +["X", "Y"] x [1, 2, 3] => [ ["X", 1], ["X", 2], ["X", 3], ["Y", 1], ["Y", 2], ["Y", 3] ] + +The value of the **Repeat** parameter makes the node compute the product of all the connected lists replicated this number of times. + +e.g. for one connected input with repeat value of 2: + +A : ["X", "Y"] +Repeat: 2 + +The result A x A is: + +["X", "Y"] x ["X", "Y"] => [ ["X", "X"], ["X", "Y"], ["Y", "X"], ["Y", "Y"] ] + + +**Permutations** + +For this operation the node take a single input list and generates the permutations of its elements. The **Length** parameter sets the number of elements in the list to be permutated. + +Notes: +* If the **Length** is zero, the node will permute ALL elements in the list. +* The **Length** value is bounded between zero and the length of the input list, so any length values larger than the length of the input list is equivalent to permuting ALL elements in the list. + +e.g. for a list of 3 (mixed) elements: + +A: ["X", 3, (1,1,1)] +L: 3 + +The result is: + +[ + ['X', 3, (1, 1, 1)], + ['X', (1, 1, 1), 3], + [3, 'X', (1, 1, 1)], + [3, (1, 1, 1), 'X'], + [(1, 1, 1), 'X', 3], + [(1, 1, 1), 3, 'X'] +] + +**Combinations** + +For this operation the node takes a single list as input and generates the combinations of its elements taking L number of elements given by the **Length** parameter. + +Notes: +* If the **Length** is zero, the node will combine ALL elements in the list. +* The **Length** value is bounded between zero and the length of the input list, so any length values larger than the length of the input list is equivalent to combining ALL elements. + +e.g. for a list of 4 elements taken 2 elements: + +A : [1, 'X', (1, 2, 3), [1, 3]] +L : 2 + +The result is: + +[ + [1, 'X'], + [1, (1, 2, 3)], + [1, [1, 3]], + ['X', (1, 2, 3)], + ['X', [1, 3]], + [(1, 2, 3), [1, 3]] +] + + +Outputs +------- + +**Result** +The list of product, permutations or combinations. + +The results will be generated only when the **Result** output is connected. + + diff --git a/nodes/list_mutators/combinatorics.py b/nodes/list_mutators/combinatorics.py index a8d89bd57..e48b0b046 100644 --- a/nodes/list_mutators/combinatorics.py +++ b/nodes/list_mutators/combinatorics.py @@ -17,26 +17,29 @@ # ##### END GPL LICENSE BLOCK ##### import bpy -from bpy.props import BoolProperty, IntProperty, FloatProperty, EnumProperty +from bpy.props import IntProperty, EnumProperty from sverchok.node_tree import SverchCustomTreeNode from sverchok.data_structure import (match_long_repeat, updateNode) -from itertools import (product, permutations, combinations, compress) +from itertools import (product, permutations, combinations) -operationItems = { - ("PRODUCT", "Product", "", 0), - ("PERMUTATIONS", "Permutations", "", 1), - ("COMBINATIONS", "Combinations", "", 2), +operations = { + "PRODUCT": (10, lambda s, r: product(*s, repeat=r)), + "PERMUTATIONS": (20, lambda s, l: permutations(s, l)), + "COMBINATIONS": (30, lambda s, l: combinations(s, l)) } -ABC = tuple('ABCDEFGHIJKLMNOPQRSTUVWXYZ') # input socket labels +operationItems = [(k, k.title(), "", s[0]) for k, s in sorted(operations.items(), key=lambda k: k[1][0])] + +ABC = tuple('ABCDEFGHIJKLMNOPQRSTUVWXYZ') # input socket labels + +multiple_input_operations = {"PRODUCT"} -multiple_input_operations = { "PRODUCT" } class SvCombinatoricsNode(bpy.types.Node, SverchCustomTreeNode): """ - Triggers: Product, Permutation, Combination + Triggers: Product, Permutations, Combinations Tooltip: Generate various combinatoric operations """ bl_idname = 'SvCombinatoricsNode' @@ -53,23 +56,22 @@ class SvCombinatoricsNode(bpy.types.Node, SverchCustomTreeNode): update=update_operation) repeat = IntProperty( - name='Repeat', description='Repeat the sequence', + name='Repeat', description='Repeat the list inputs this many times', default=1, min=1, update=updateNode) length = IntProperty( - name='Lenght', description='Lenght of the sequence', - default=1, min=1, update=updateNode) + name='Length', description='Limit the elements to operate on to this value', + default=1, min=0, update=updateNode) def sv_init(self, context): self.inputs.new('StringsSocket', "Repeat").prop_name = "repeat" self.inputs.new('StringsSocket', "Length").prop_name = "length" - self.inputs.new('StringsSocket', "Selector") self.inputs.new('StringsSocket', "A") self.inputs.new('StringsSocket', "B") self.outputs.new('StringsSocket', "Result") - self.operation = "PRODUCT" + self.update_operation(context) def update(self): ''' Add/remove sockets as A-Z sockets are connected/disconnected ''' @@ -95,7 +97,7 @@ class SvCombinatoricsNode(bpy.types.Node, SverchCustomTreeNode): inputs_AZ.remove(s) def update_sockets(self): - ''' Update sockets based on current mode ''' + ''' Update sockets based on selected operation ''' inputs = self.inputs @@ -104,18 +106,19 @@ class SvCombinatoricsNode(bpy.types.Node, SverchCustomTreeNode): if not "B" in inputs: inputs.new("StringsSocket", "B") else: - print("remove sockets") for a in ABC[1:]: # remove all B-Z inputs (keep A) if a in inputs: inputs.remove(inputs[a]) - # update the other sockets (operation specific) - if self.operation in { "PRODUCT" }: - inputs["Repeat"].hide_safe = False + # update the other sockets + if self.operation in {"PRODUCT"}: + if inputs["Repeat"].hide: + inputs["Repeat"].hide_safe = False inputs["Length"].hide_safe = True - elif self.operation in { "COMBINATIONS", "PERMUTATIONS" }: + elif self.operation in {"COMBINATIONS", "PERMUTATIONS"}: inputs["Repeat"].hide_safe = True - inputs["Length"].hide_safe = False + if inputs["Length"].hide: + inputs["Length"].hide_safe = False def draw_buttons(self, context, layout): layout.prop(self, "operation", text="") @@ -126,49 +129,32 @@ class SvCombinatoricsNode(bpy.types.Node, SverchCustomTreeNode): if not any(s.is_linked for s in outputs): return - # input values lists (single or multi value) inputs = self.inputs all_AZ_sockets = list(filter(lambda s: s.name in ABC, inputs)) connected_AZ_sockets = list(filter(lambda s: s.is_linked, all_AZ_sockets)) - I = [] # list of all data inputs (single or multiple) - # collect the data inputs from all connected AZ sockets - for s in connected_AZ_sockets: - a = s.sv_get()[0] - I.append(a) - - resultList = [] + I = [s.sv_get()[0] for s in connected_AZ_sockets] if self.operation == "PRODUCT": R = inputs["Repeat"].sv_get()[0] R = list(map(lambda x: max(1, int(x)), R)) parameters = match_long_repeat([[I], R]) - for sequence, r in zip(*parameters): - result = product(*sequence, repeat=r) - result = [list(a) for a in result] - resultList.append(result) - - elif self.operation == "PERMUTATIONS": + else: # PERMUTATIONS / COMBINATIONS L = inputs["Length"].sv_get()[0] - L = list(map(lambda x: max(1, int(x)), L)) + L = list(map(lambda x: max(0, int(x)), L)) parameters = match_long_repeat([I, L]) - for sequence, l in zip(*parameters): - l = min(l, len(sequence)) - result = permutations(sequence, l) - result = [list(a) for a in result] - resultList.append(result) - elif self.operation == "COMBINATIONS": - L = inputs["Length"].sv_get()[0] - L = list(map(lambda x: max(1, int(x)), L)) - parameters = match_long_repeat([I, L]) - for sequence, l in zip(*parameters): - l = min(l, len(sequence)) - result = combinations(sequence, l) - result = [list(a) for a in result] - resultList.append(result) + function = operations[self.operation][1] + + resultList = [] + for sequence, v in zip(*parameters): + if self.operation in {"PERMUTATIONS", "COMBINATIONS"}: + if v == 0 or v > len(sequence): + v = len(sequence) + result = [list(a) for a in function(sequence, v)] + resultList.append(result) outputs["Result"].sv_set(resultList) -- GitLab From e6251c9e14009fb75c8e2644bc4f136c0f2da779 Mon Sep 17 00:00:00 2001 From: DolphinDream Date: Sat, 29 Dec 2018 11:07:04 -0500 Subject: [PATCH 21/60] Fix comment --- nodes/list_mutators/combinatorics.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nodes/list_mutators/combinatorics.py b/nodes/list_mutators/combinatorics.py index e48b0b046..af4d21845 100644 --- a/nodes/list_mutators/combinatorics.py +++ b/nodes/list_mutators/combinatorics.py @@ -76,7 +76,7 @@ class SvCombinatoricsNode(bpy.types.Node, SverchCustomTreeNode): def update(self): ''' Add/remove sockets as A-Z sockets are connected/disconnected ''' - # not a multiple quaternion operation ? => no need to update sockets + # not a multiple input operation ? => no need to update sockets if self.operation not in multiple_input_operations: return -- GitLab From 57106ac9c2c8d90c37df7b3a6f0f7b3bba7df0bd Mon Sep 17 00:00:00 2001 From: DolphinDream Date: Thu, 20 Dec 2018 23:05:00 -0500 Subject: [PATCH 22/60] Add quaternion nodes - add 3 nodes: Quaternion In, Quaternion Out and Quaternion Math Quaternion In: Provides conversion from various quaternion components to quaternions (selectable via the mode option): - components wxyz - angle + axis - euler angles - matrix Quaternion Out: Provides conversion from quaterions to various quaternion components (selectable via the mode option): - components wxyz - angle + axis - euler angles - matrix Quaternion Math: Provides various quaternion arithmetic operations: ADD SUB MULTIPLY DIVIDE ROTATE DOT DISTANCE NEGATE CONJUGATE INVERT NORMALIZE SCALE MAGNITUDE Note: some operations take multiple quaternion inputs (input list grows as new sockets are connected), two quaternion inputs, single quaternion inputs, or quaternion + scalar input. --- docs/nodes/quaternion/quaternion_in.rst | 64 +++++ docs/nodes/quaternion/quaternion_math.rst | 161 +++++++++++++ docs/nodes/quaternion/quaternion_out.rst | 59 +++++ index.md | 4 + nodes/quaternion/quaternion_in.py | 215 +++++++++++++++++ nodes/quaternion/quaternion_math.py | 281 ++++++++++++++++++++++ nodes/quaternion/quaternion_out.py | 173 +++++++++++++ 7 files changed, 957 insertions(+) create mode 100644 docs/nodes/quaternion/quaternion_in.rst create mode 100644 docs/nodes/quaternion/quaternion_math.rst create mode 100644 docs/nodes/quaternion/quaternion_out.rst create mode 100644 nodes/quaternion/quaternion_in.py create mode 100644 nodes/quaternion/quaternion_math.py create mode 100644 nodes/quaternion/quaternion_out.py diff --git a/docs/nodes/quaternion/quaternion_in.rst b/docs/nodes/quaternion/quaternion_in.rst new file mode 100644 index 000000000..e9c9be730 --- /dev/null +++ b/docs/nodes/quaternion/quaternion_in.rst @@ -0,0 +1,64 @@ +Quaternion In +------------- + +Quaternion In node constructs quaternions based on various input components provided for a selected mode. + + +Modes +===== + +The available **Modes** are: WXYZ, EULER, AXIS-ANGLE & MATRIX. + ++============+================================================================+ +| Mode | Description | ++------------+----------------------------------------------------------------+ +| WXYZ | Converts W, X, Y, Z components into a quaternion. [1] | ++------------+----------------------------------------------------------------+ +| EULER | Converts X, Y, Z Euler angles and an order of rotation | +| | into a quaternion. [2,3] | ++------------+----------------------------------------------------------------+ +| AXIS-ANGLE | Converts an Axis & an Angle of rotation into a quaternion. [2] | ++------------+----------------------------------------------------------------+ +| MATRIX | Converts an orthogonal 4x4 rotation matrix into a quaternion. | ++============+================================================================+ + +Notes: +[1] : For WXYZ the node provides a "Normalize" option to generate a normalized quaternion. +[2] : For EULER and AXIS-ANGLE modes (which take angle input) the node provides an +angle unit conversion to let the angle values be converted to Radians, Degrees or Unities (0-1 range). +[3] : For EULER mode the node provides the option to select the Euler rotation order: +"XYZ", "XZY", "YXZ", "YZX", "ZXY" or "ZYX". + + +Inputs +====== + +The node takes a list of various components, based on the selected mode, and it +constructs the corresponding quaternions. The node is vectorized so the inputs take +a value or a list of values. When multiple lists are connected the node will +extend the length of the connected input lists to match the longest one before computing the list of output quaternions. + +Based on the selected **Mode** the node makes available the corresponding input sockets: + ++============+================================+ +| Mode | Input Sockets (types) | ++------------+--------------------------------+ +| WXYZ | W, X, Y, Z (floats) | ++------------+--------------------------------+ +| EULER | X, Y, Z angles (floats) | ++------------+--------------------------------+ +| AXIS-ANGLE | Axis (Vector) & Angle (float) | ++------------+--------------------------------+ +| MATRIX | Matrix (4x4 matrix) | ++============+================================+ + + +Outputs +======= + +**Quaternions** + +The node outputs a list of one ore more quaternions based on the given input. + +The node only generates the quaternions when the output socket is connected. + diff --git a/docs/nodes/quaternion/quaternion_math.rst b/docs/nodes/quaternion/quaternion_math.rst new file mode 100644 index 000000000..b00c1c5a7 --- /dev/null +++ b/docs/nodes/quaternion/quaternion_math.rst @@ -0,0 +1,161 @@ +Quaternion Math Node +-------------------- + +The Quaternion Math node performs various artithmetic operations on quaternions. + +The available arithmetic operations and their corresponding inputs/outputs are: + ++============+========+========+=====================================+ +| Operation | Input | Output | Description | ++============+========+========+=====================================+ +| ADD | NQ | Q | Add multiple quaternions | +| SUB | QQ | Q | Subtract two quaternions | +| MULTIPLY | NQ | Q | Multiply multiple quaternions | +| DIVIDE | QQ | Q | Divide two quaternions | +| ROTATE | QQ | Q | Rotate a quaternion around another | +| DOT | QQ | S | Dot product two quaternions | +| DISTANCE | QQ | S | Distance between two quaternions | +| NEGATE | Q | Q | Negate a quaternion | +| CONJUGATE | Q | Q | Conjugate a quaternion | +| INVERT | Q | Q | Invert a quaternion | +| NORMALIZE | Q | Q | Normalize a quaternion | +| SCALE | QS | Q | Scale a quaternion by given factor | +| MAGNITUDE | Q | S | Magnitude of a quaternion | ++============+========+========+=====================================+ + +where: + +NQ = arbitrary number of quaternion inputs +QQ = two quaternion inputs +Q = one quaternion input +QS = one quaternion + scalar value +S = scalar value + +For the operations that take multiple quaternion inputs (NQ & QQ) the node provides a PRE / POST option, which lets the node execute the operation on the quaternion inputs in a direct or reverse order. The exceptions to this rule are the ADD, DOT and DISTANCE operations for which the order of quaternions is irrelevant. + +For quaternion inputs A and B: +PRE = A op B +POST = B op A + + +Inputs +====== +The input to the node are lists of quaternions as well as control parameters (like scale etc). For certain operations the node takes arbitrary number of quaternion input lists, for others it takes only two quaternion input lists and for some only one quaternion input list. + +The inputs accept single value quaternions or a list of quaternions. The node is vectorized so it will extend the quaternion lists to match the longest input. + + +Operations +========== + +* ADD : adds the components of two or more quaternions + +q1 = (w1, x1, y1, z1) +q2 = (w2, x2, y2, z2) + +q1 + q2 = (w1 + w2, x1 + x2, y1 + y2, z1 + z1) + + +* SUB : subtracts the components of two quaternions + +q1 = (w1, x1, y1, z1) +q2 = (w2, x2, y2, z2) + +q1 - q2 = (w1 - w2, x1 - x2, y1 - y2, z1 - z2) + + +* MULTIPLY : multiplies two or more quaternions + +q1 = (w1, x1, y1, z1) = (w1, V1), where V1 = (x1, y1, z1) +q2 = (w2, x2, y2, z2) = (w2, V2), where V2 = (x2, y2, z2) + +q1 x q2 = (w1 * w2 - V1 * V2, w1 * V1 + w2 * V2 + V1 x V2) + +where V1 * V2 is dot product of vectors V1 & V2 +and V1 x V2 is the cross product of vectors V1 & V2 + + +* DIVIDE : divide two quaternions (multiply one quaternion with inverse of the other) + +q1 = (w1, x1, y1, z1) +q2 = (w2, x2, y2, z2) + +q1 / q2 = q1 x inverse(q2) + + +* ROTATE : rotates one quaternion around the other quaternion + + +* DOT : the dot product of two quaternions + +q1 = (w1, x1, y1, z1) +q2 = (w2, x2, y2, z2) + +q1 * q2 = w1 * w2 + x1 * x2 + y1 * y2 + z1 * z2 + + +* DISTANCE : the distance between two quaternions + +q1 = (w1, x1, y1, z1) +q2 = (w2, x2, y2, z2) + +Distance(q1, q2) = Magnitude(q1 - q2) + + +* NEGATE : negates a quaternion + +q = (w, x, y, z) + +Negate(q) = (-w, -x, -y, -z) + + +* CONJUGATE : conjugates a quaternion + +q = (w, x, y, z) + +Conjugate(q) = (w, -x, -y, -z) + + +* INVERT : inverts a quaternion + +q = (w, x, y, z) + +Inverse(q) = Conjugate(q) / Magnitude(q)^2 + + +* NORMALIZE : normalizes a quaternion + +q = (w, x, y, z) + +Normalize(q) = (w/m, x/m, y/m, z/m) + +where m = Magnitude(q) + + +* SCALE : scales the components of a quaternion + +q = (w, x, y, z) + +s - (float) the scale factor +sf = (sw, sx, sy, sz) - (array of bools) filters which component is scaled + +S = (s if sw else 1, s if sx else 1, s if sy else 1, s if sz else 1) + +scale(q, S) = (w * Sw, x * Sx, y * Sy, z * Sz) + + +* MAGNITUDE : the magnitude of a quaternion + +q = (w, x, y, z) + +Magnitude(q) = sqrt(w * w + x * x + y * y + z * z) + + +Output +====== + +**Quaternions** or **Values** +Depending on the operation the output to the node is either a quaternion list or scalar value list. + +The node computes the results (quaternions or scalar values) only when the output socket is connected. + diff --git a/docs/nodes/quaternion/quaternion_out.rst b/docs/nodes/quaternion/quaternion_out.rst new file mode 100644 index 000000000..779f9aa0c --- /dev/null +++ b/docs/nodes/quaternion/quaternion_out.rst @@ -0,0 +1,59 @@ +Quaternion Out +-------------- + +Quaternion Out node converts a quaternion into various formats for a selected mode. + +Modes +===== + +The available **Modes** are: WXYZ, EULER, AXIS-ANGLE & MATRIX. + ++============+==============================================================+ +| Mode | Description | ++------------+--------------------------------------------------------------+ +| WXYZ | Converts a quaternion into its W, X, Y, Z components. [1] | ++------------+--------------------------------------------------------------+ +| EULER | Converts a quaternion into X, Y, Z angles corresponding | +| | to the Euler rotation given an Euler rotation order. [2,3] | ++------------+--------------------------------------------------------------+ +| AXIS-ANGLE | Converts a quaternion into the Axis & Angle of rotation. [2] | ++------------+--------------------------------------------------------------+ +| MATRIX | Converts a quaternion into an orthogonal 4x4 rotation matrix.| ++============+==============================================================+ + +Notes: +[1] : For WXYZ the node provides a "Normalize" option to let the input quaternion +be normalized before outputting its components. +[2] : For EULER and AXIS-ANGLE modes, which output angles, the node provides an +angle unit conversion to let the angle output values be converted to Radians, +Degrees or Unities (0-1 range). +[3] : For EULER mode the node provides the option to select the Euler rotation order: +"XYZ", "XZY", "YXZ", "YZX", "ZXY" or "ZYX". + +Inputs +====== + +**Quaternions** +The node takes a list of (one or more) quaternions and based on the selected mode +it converts the quaternions into the corresponding components. + + +Outputs +======= + +Based on the selected **Mode** the node makes available the corresponding output sockets: + ++============+================================+ +| Mode | Output Sockets (types) | ++------------+--------------------------------+ +| WXYZ | W, X, Y, Z (floats) | ++------------+--------------------------------+ +| EULER | X, Y, Z angles (floats) | ++------------+--------------------------------+ +| AXIS-ANGLE | Axis (Vector) & Angle (float) | ++------------+--------------------------------+ +| MATRIX | Matrix (4x4 matrix) | ++============+================================+ + +The node only generates the conversion when the output sockets are connected. + diff --git a/index.md b/index.md index 38f112c9a..3ecc93b0b 100644 --- a/index.md +++ b/index.md @@ -345,3 +345,7 @@ SvOffsetLineNode SvContourNode SvPlanarEdgenetToPolygons + --- + SvQuaternionOutNode + SvQuaternionInNode + SvQuaternionMathNode \ No newline at end of file diff --git a/nodes/quaternion/quaternion_in.py b/nodes/quaternion/quaternion_in.py new file mode 100644 index 000000000..f80b15db9 --- /dev/null +++ b/nodes/quaternion/quaternion_in.py @@ -0,0 +1,215 @@ +# ##### 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 bpy +from bpy.props import EnumProperty, FloatProperty, BoolProperty, StringProperty, FloatVectorProperty +from sverchok.node_tree import SverchCustomTreeNode +from sverchok.data_structure import updateNode, match_long_repeat +from mathutils import Quaternion, Matrix, Euler +from math import pi + + +modeItems = [ + ("WXYZ", "WXYZ", "Convert components into quaternion", 0), + ("EULER", "Euler Angles", "Convert Euler angles into quaternion", 1), + ("AXISANGLE", "Axis Angle", "Convert Axis & Angle into quaternion", 2), + ("MATRIX", "Matrix", "Convert Rotation Matrix into quaternion", 3), +] + +eulerOrderItems = [ + ('XYZ', "XYZ", "", 0), + ('XZY', 'XZY', "", 1), + ('YXZ', 'YXZ', "", 2), + ('YZX', 'YZX', "", 3), + ('ZXY', 'ZXY', "", 4), + ('ZYX', 'ZYX', "", 5) +] + +angleUnitItems = [ + ("RAD", "Rad", "Radians", "", 0), + ("DEG", "Deg", 'Degrees', "", 1), + ("UNI", "Uni", 'Unities', "", 2) +] + +angleConversion = {"RAD": 1.0, "DEG": pi / 180.0, "UNI": 2 * pi} + +idMat = [[tuple(v) for v in Matrix()]] # identity matrix + +input_sockets = { + "WXYZ": ["W", "X", "Y", "Z"], + "EULER": ["Angle X", "Angle Y", "Angle Z"], + "AXISANGLE": ["Angle", "Axis"], + "MATRIX": ["Matrix"] +} + + +class SvQuaternionInNode(bpy.types.Node, SverchCustomTreeNode): + """ + Triggers: Quaternions, In + Tooltip: Generate quaternions from various quaternion components + """ + bl_idname = 'SvQuaternionInNode' + bl_label = 'Quaternion In' + sv_icon = 'SV_COMBINE_IN' + + def update_mode(self, context): + + # hide all input sockets + for k, names in input_sockets.items(): + for name in names: + self.inputs[name].hide_safe = True + + # show mode specific input sockets + for name in input_sockets[self.mode]: + self.inputs[name].hide_safe = False + + updateNode(self, context) + + mode = EnumProperty( + name='Mode', description='Input mode (wxyz, Euler, Axis-Angle, Matrix)', + items=modeItems, default="WXYZ", update=update_mode) + + eulerOrder = EnumProperty( + name="Euler Order", description="Order of the Euler rotations", + default="XYZ", items=eulerOrderItems, update=updateNode) + + angleUnits = EnumProperty( + name="Angle Units", description="Angle units (radians/degrees/unities)", + default="RAD", items=angleUnitItems, update=updateNode) + + component_w = FloatProperty( + name='W', description='W component', + default=0.0, precision=3, update=updateNode) + + component_x = FloatProperty( + name='X', description='X component', + default=0.0, precision=3, update=updateNode) + + component_y = FloatProperty( + name='Y', description='Y component', + default=0.0, precision=3, update=updateNode) + + component_z = FloatProperty( + name='Z', description='Z component', + default=0.0, precision=3, update=updateNode) + + angle_x = FloatProperty( + name='Angle X', description='Rotation angle about X axis', + default=0.0, precision=3, update=updateNode) + + angle_y = FloatProperty( + name='Angle Y', description='Rotation angle about Y axis', + default=0.0, precision=3, update=updateNode) + + angle_z = FloatProperty( + name='Angle Z', description='Rotation angle about Z axis', + default=0.0, precision=3, update=updateNode) + + angle = FloatProperty( + name='Angle', description='Rotation angle about the given axis', + default=0.0, update=updateNode) + + axis = FloatVectorProperty( + name='Axis', description='Axis of rotation', + size=3, default=(1.0, 0.0, 0.0), subtype="XYZ", update=updateNode) + + normalize = BoolProperty( + name='Normalize', description='Normalize the output quaternion', + default=False, update=updateNode) + + def sv_init(self, context): + self.inputs.new('StringsSocket', "W").prop_name = 'component_w' + self.inputs.new('StringsSocket', "X").prop_name = 'component_x' + self.inputs.new('StringsSocket', "Y").prop_name = 'component_y' + self.inputs.new('StringsSocket', "Z").prop_name = 'component_z' + self.inputs.new('StringsSocket', "Angle X").prop_name = 'angle_x' + self.inputs.new('StringsSocket', "Angle Y").prop_name = 'angle_y' + self.inputs.new('StringsSocket', "Angle Z").prop_name = 'angle_z' + self.inputs.new('VerticesSocket', "Axis").prop_name = "axis" + self.inputs.new('StringsSocket', "Angle").prop_name = 'angle' + self.inputs.new('MatrixSocket', "Matrix") + self.outputs.new('SvQuaternionSocket', "Quaternions") + + self.update_mode(context) + + def draw_buttons(self, context, layout): + layout.prop(self, "mode", expand=False, text="") + if self.mode == "EULER": + col = layout.column(align=True) + col.prop(self, "eulerOrder", text="") + if self.mode in {"EULER", "AXISANGLE"}: + row = layout.row(align=True) + row.prop(self, "angleUnits", expand=True) + if self.mode == "WXYZ": + layout.prop(self, "normalize", toggle=True) + + def process(self): + if not self.outputs['Quaternions'].is_linked: + return + + inputs = self.inputs + + quaternionList = [] + + if self.mode == "WXYZ": + I = [inputs[n].sv_get()[0] for n in "WXYZ"] + params = match_long_repeat(I) + for wxyz in zip(*params): + q = Quaternion(wxyz) + if self.normalize: + q.normalize() + quaternionList.append(q) + + elif self.mode == "EULER": + I = [inputs["Angle " + n].sv_get()[0] for n in "XYZ"] + params = match_long_repeat(I) + au = angleConversion[self.angleUnits] + for angleX, angleY, angleZ in zip(*params): + euler = Euler((angleX * au, angleY * au, angleZ * au), self.eulerOrder) + q = euler.to_quaternion() + if self.normalize: + q.normalize() + quaternionList.append(q) + + elif self.mode == "AXISANGLE": + I = [inputs[n].sv_get()[0] for n in {"Axis", "Angle"}] + params = match_long_repeat(I) + au = angleConversion[self.angleUnits] + for axis, angle in zip(*params): + q = Quaternion(axis, angle * au) + if self.normalize: + q.normalize() + quaternionList.append(q) + + elif self.mode == "MATRIX": + input_M = inputs["Matrix"].sv_get(default=idMat) + for m in input_M: + q = Matrix(m).to_quaternion() + if self.normalize: + q.normalize() + quaternionList.append(q) + + self.outputs['Quaternions'].sv_set([quaternionList]) + + +def register(): + bpy.utils.register_class(SvQuaternionInNode) + + +def unregister(): + bpy.utils.unregister_class(SvQuaternionInNode) diff --git a/nodes/quaternion/quaternion_math.py b/nodes/quaternion/quaternion_math.py new file mode 100644 index 000000000..84f0ff3dd --- /dev/null +++ b/nodes/quaternion/quaternion_math.py @@ -0,0 +1,281 @@ +# ##### 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 bpy +from bpy.props import (IntProperty, + FloatProperty, + BoolProperty, + BoolVectorProperty, + EnumProperty) + +from sverchok.node_tree import SverchCustomTreeNode +from sverchok.data_structure import updateNode, match_long_repeat + +from mathutils import Matrix, Quaternion +from functools import reduce + +# list of operations [name: id, input, output, description ] +operations = { + # multiple quaternions => quaternion (NQ or QQ => Q) + "ADD": (10, "NQ", "Q", "Add multiple quaternions"), + "SUB": (11, "QQ", "Q", "Subtract two quaternions"), + "MULTIPLY": (12, "NQ", "Q", "Multiply multiple quaternions"), + "DIVIDE": (13, "QQ", "Q", "Divide two quaternions"), + "ROTATE": (14, "QQ", "Q", "Rotate a quaternion around another"), + # two quaternions => scalar value (QQ => Q) + "DOT": (20, "QQ", "S", "Dot product two quaternions"), + "DISTANCE": (21, "QQ", "S", "Distance between two quaternions"), + # one quaternion => quaternion (Q => Q) + "NEGATE": (30, "Q", "Q", "Negate a quaternion"), + "CONJUGATE": (31, "Q", "Q", "Conjugate a quaternion"), + "INVERT": (32, "Q", "Q", "Invert a quaternion"), + "NORMALIZE": (33, "Q", "Q", "Normalize a quaternion"), + # one quaternion + scalar => quaternion (QS => Q) + "SCALE": (40, "QS", "Q", "Scale a quaternion by given factor"), + # one quaternion => scalar value (Q => S) + "MAGNITUDE": (50, "Q", "S", "Magnitude of a quaternion"), +} + +operationItems = [(k, k.title(), s[3], "", s[0]) for k, s in sorted(operations.items(), key=lambda k: k[1][0])] + +# cache various operation categories +NQ_operations = [n for n in operations if operations[n][1] == "NQ"] +QQ_operations = [n for n in operations if operations[n][1] == "QQ"] +Q_operations = [n for n in operations if operations[n][1] in {"Q", "QS"}] +QS_operations = [n for n in operations if operations[n][1] == "QS"] +output_S_operations = [n for n in operations if operations[n][2] == "S"] +prepost_operations = {"SUB", "MULTIPLY", "DIVIDE", "ROTATE"} + +prePostItems = [ + ("PRE", "Pre", "Calculate A op B", 0), + ("POST", "Post", "Calculate B op A", 1) +] + +id_quat = [Quaternion([1, 0, 0, 0])] +ABC = tuple('ABCDEFGHIJKLMNOPQRSTUVWXYZ') + + +class SvQuaternionMathNode(bpy.types.Node, SverchCustomTreeNode): + """ + Triggers: Quaternions, Math + Tooltip: Compute various arithmetic operations on quaternions + """ + bl_idname = 'SvQuaternionMathNode' + bl_label = 'Quaternion Math' + bl_icon = 'OUTLINER_OB_EMPTY' + + def update_operation(self, context): + self.label = "Quaternion " + self.operation.title() + self.update_sockets() + updateNode(self, context) + + prePost = EnumProperty( + name='Pre Post', + description='Order of operations PRE = A op B vs POST = B op A)', + items=prePostItems, default="PRE", update=updateNode) + + operation = EnumProperty( + name="Operation", + description="Operation to apply on the given quaternions", + items=operationItems, default="MULTIPLY", update=update_operation) + + scale = FloatProperty( + name="Scale", + description="Scale quaternion components by this factor", + default=1.0, update=updateNode) + + scales = BoolVectorProperty( + name="Scales", description="Which individual components to scale", + size=4, subtype="QUATERNION", + default=(True, True, True, True), update=updateNode) + + def sv_init(self, context): + self.inputs.new('StringsSocket', "Scale").prop_name = "scale" + self.inputs.new('SvQuaternionSocket', "A") + self.inputs.new('SvQuaternionSocket', "B") + + self.outputs.new('SvQuaternionSocket', "Quaternion") + self.outputs.new('StringsSocket', "Value") + + self.update_operation(context) + + def update(self): + ''' Add/remove sockets as A-Z sockets are connected/disconnected ''' + + # not a multiple quaternion operation ? => no need to update sockets + if self.operation not in NQ_operations: + return + + inputs = self.inputs + + # get all existing A-Z sockets (connected or not) + inputs_AZ = list(filter(lambda s: s.name in ABC, inputs)) + + # last A-Z socket connected ? => add an empty A-Z socket at the end + if inputs_AZ[-1].links: + name = ABC[len(inputs_AZ)] # pick the next letter A to Z + inputs.new("SvQuaternionSocket", name) + else: # last input disconnected ? => remove all but last unconnected + while len(inputs_AZ) > 2 and not inputs_AZ[-2].links: + s = inputs_AZ[-1] + inputs.remove(s) + inputs_AZ.remove(s) + + def update_sockets(self): + ''' Upate sockets based on selected operation ''' + inputs = self.inputs + + if self.operation in Q_operations: # Q or Q+S operations + for a in ABC[1:]: # remove all B-Z inputs (keep A) + if a in inputs: + inputs.remove(inputs[a]) + elif self.operation in QQ_operations: # Q + Q operations + for a in ABC[2:]: # remove all C-Z inputs (keep A & B) + if a in inputs: + inputs.remove(inputs[a]) + if not "B" in inputs: + inputs.new("SvQuaternionSocket", "B") + else: # multiple Q operations + if not "B" in inputs: + inputs.new("SvQuaternionSocket", "B") + + inputs["Scale"].hide_safe = self.operation != "SCALE" + + outputs = self.outputs + if self.operation in output_S_operations: + outputs["Quaternion"].hide_safe = True + if outputs["Value"].hide: + outputs["Value"].hide_safe = False + else: + if outputs["Quaternion"].hide: + outputs["Quaternion"].hide_safe = False + outputs["Value"].hide_safe = True + + self.update() + + def draw_buttons(self, context, layout): + layout.prop(self, "operation", text="") + if self.operation in prepost_operations: + layout.prop(self, "prePost", expand=True) + if self.operation == "SCALE": + row = layout.row(align=True) + row.prop(self, "scales", text="", toggle=True) + + def operation_negate(self, q): + qn = Quaternion(q) + qn.negate() + return qn + + def operation_rotate(self, q, p): + qr = Quaternion(q) + qr.rotate(p) + return qr + + def get_operation(self): + if self.operation == "ADD": + return lambda l: reduce((lambda q, p: q + p), l) + elif self.operation == "SUB": + return lambda q, p: q - p + elif self.operation == "MULTIPLY": + return lambda l: reduce((lambda q, p: q * p), l) + elif self.operation == "DIVIDE": + return lambda q, p: q * p.inverted() + elif self.operation == "ROTATE": + return self.operation_rotate + elif self.operation == "DOT": + return lambda q, p: q.dot(p) + elif self.operation == "DISTANCE": + return lambda q, p: (p - q).magnitude + elif self.operation == "NEGATE": + return self.operation_negate + elif self.operation == "CONJUGATE": + return lambda q: q.conjugated() + elif self.operation == "INVERT": + return lambda q: q.inverted() + elif self.operation == "NORMALIZE": + return lambda q: q.normalized() + elif self.operation == "SCALE": + return lambda q, s: Quaternion([q[i] * s[i] for i in range(4)]) + elif self.operation == "MAGNITUDE": + return lambda q: q.magnitude + + def process(self): + outputs = self.outputs + if not any(s.is_linked for s in outputs): + return + + inputs = self.inputs + + all_AZ_sockets = list(filter(lambda s: s.name in ABC, inputs)) + connected_AZ_sockets = list(filter(lambda s: s.is_linked, all_AZ_sockets)) + + if len(connected_AZ_sockets) == 0: + return + + # collect the quaternion inputs from all connected AZ sockets + I = [s.sv_get(default=id_quat)[0] for s in connected_AZ_sockets] + + if self.operation in prepost_operations: + if self.prePost == "POST": # A op B : keep input order + I = I[::-1] + + other_sockets = list(filter(lambda s: s.name not in ABC and not s.hide, inputs)) + + # collect the remaning visible inputs + for socket in other_sockets: + values = socket.sv_get()[0] + if socket.name == "Scale": + qs = [] + for s in values: + swxyz = [s if self.scales[i] else 1.0 for i in range(4)] + qs.append(Quaternion(swxyz)) + values = qs + I.append(values) + + operation = self.get_operation() + + if self.operation in NQ_operations: # multiple input operations + parameters = match_long_repeat(I) + quaternionList = [operation(params) for params in zip(*parameters)] + + elif self.operation in QQ_operations: # multiple input operations + parameters = match_long_repeat(I) + quaternionList = [operation(*params) for params in zip(*parameters)] + + elif self.operation == "SCALE": + parameters = match_long_repeat(I) + quaternionList = [operation(*params) for params in zip(*parameters)] + + else: # single input operations + parameters = I[0] # just quaternion values + quaternionList = [operation(a) for a in parameters] + + if self.operation in output_S_operations: + if outputs['Value'].is_linked: + outputs['Value'].sv_set([quaternionList]) + else: # output quaternions + if outputs['Quaternion'].is_linked: + outputs['Quaternion'].sv_set([quaternionList]) + + +def register(): + bpy.utils.register_class(SvQuaternionMathNode) + + +def unregister(): + bpy.utils.unregister_class(SvQuaternionMathNode) diff --git a/nodes/quaternion/quaternion_out.py b/nodes/quaternion/quaternion_out.py new file mode 100644 index 000000000..dcf7d6377 --- /dev/null +++ b/nodes/quaternion/quaternion_out.py @@ -0,0 +1,173 @@ +# ##### 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 bpy +from bpy.props import BoolProperty, FloatVectorProperty, EnumProperty +from sverchok.node_tree import SverchCustomTreeNode +from sverchok.data_structure import updateNode +from mathutils import Quaternion +from math import pi + + +modeItems = [ + ("WXYZ", "WXYZ", "Convert quaternion into components", 0), + ("EULER", "Euler Angles", "Convert quaternion into Euler angles", 1), + ("AXISANGLE", "Axis Angle", "Convert quaternion into Axis & Angle", 2), + ("MATRIX", "Matrix", "Convert quaternion into Rotation Matrix", 3), +] + +eulerOrderItems = [ + ('XYZ', "XYZ", "", 0), + ('XZY', 'XZY', "", 1), + ('YXZ', 'YXZ', "", 2), + ('YZX', 'YZX', "", 3), + ('ZXY', 'ZXY', "", 4), + ('ZYX', 'ZYX', "", 5) +] + +angleUnitItems = [ + ("RAD", "Rad", "Radians", "", 0), + ("DEG", "Deg", 'Degrees', "", 1), + ("UNI", "Uni", 'Unities', "", 2) +] + +angleConversion = {"RAD": 1.0, "DEG": 180.0 / pi, "UNI": 0.5 / pi} + +output_sockets = { + "WXYZ": ["W", "X", "Y", "Z"], + "EULER": ["Angle X", "Angle Y", "Angle Z"], + "AXISANGLE": ["Angle", "Axis"], + "MATRIX": ["Matrix"] +} + + +class SvQuaternionOutNode(bpy.types.Node, SverchCustomTreeNode): + """ + Triggers: Quaternions, Out + Tooltip: Convert quaternions into various quaternion components + """ + bl_idname = 'SvQuaternionOutNode' + bl_label = 'Quaternion Out' + sv_icon = 'SV_COMBINE_OUT' + + def update_mode(self, context): + + # hide all output sockets + for k, names in output_sockets.items(): + for name in names: + self.outputs[name].hide_safe = True + + # show mode specific output sockets + for name in output_sockets[self.mode]: + self.outputs[name].hide_safe = False + + updateNode(self, context) + + mode = EnumProperty( + name='Mode', description='Output mode (wxyz, Euler, Axis-Angle, Matrix)', + items=modeItems, default="WXYZ", update=update_mode) + + eulerOrder = EnumProperty( + name="Euler Order", description="Order of the Euler rotations", + default="XYZ", items=eulerOrderItems, update=updateNode) + + quaternion = FloatVectorProperty( + name="Quaternion", description="Quaternion to convert", + size=4, subtype="QUATERNION", default=(0.0, 0.0, 0.0, 0.0), + update=updateNode) + + angleUnits = EnumProperty( + name="Angle Units", description="Angle units (radians/degrees/unities)", + default="RAD", items=angleUnitItems, update=updateNode) + + normalize = BoolProperty( + name='Normalize', description='Normalize the input quaternion', + default=False, update=updateNode) + + def sv_init(self, context): + self.inputs.new('SvQuaternionSocket', "Quaternions").prop_name = "quaternion" + # component outputs + self.outputs.new('StringsSocket', "W") + self.outputs.new('StringsSocket', "X") + self.outputs.new('StringsSocket', "Y") + self.outputs.new('StringsSocket', "Z") + # euler angle ouputs + self.outputs.new('StringsSocket', "Angle X") + self.outputs.new('StringsSocket', "Angle Y") + self.outputs.new('StringsSocket', "Angle Z") + # axis-angle output + self.outputs.new('VerticesSocket', "Axis") + self.outputs.new('StringsSocket', "Angle") + # matrix ouptut + self.outputs.new('MatrixSocket', "Matrix") + + self.update_mode(context) + + def draw_buttons(self, context, layout): + layout.prop(self, "mode", expand=False, text="") + if self.mode == "EULER": + col = layout.column(align=True) + col.prop(self, "eulerOrder", text="") + if self.mode in {"EULER", "AXISANGLE"}: + row = layout.row(align=True) + row.prop(self, "angleUnits", expand=True) + if self.mode == "WXYZ": + layout.prop(self, "normalize", toggle=True) + + def process(self): + outputs = self.outputs + if not any(s.is_linked for s in outputs): + return + + input_Q = self.inputs['Quaternions'].sv_get()[0] + quaternionList = [Quaternion(q) for q in input_Q] + + if self.mode == "WXYZ": + for i, name in enumerate("WXYZ"): + if outputs[name].is_linked: + outputs[name].sv_set([[q[i] for q in quaternionList]]) + + elif self.mode == "EULER": + au = angleConversion[self.angleUnits] + for i, name in enumerate("XYZ"): + if outputs["Angle " + name].is_linked: + angles = [q.to_euler(self.eulerOrder)[i] * au for q in quaternionList] + outputs["Angle " + name].sv_set([angles]) + + elif self.mode == "AXISANGLE": + if outputs['Axis'].is_linked: + axisList = [tuple(q.axis) for q in quaternionList] + outputs['Axis'].sv_set([axisList]) + + if outputs['Angle'].is_linked: + au = angleConversion[self.angleUnits] + angleList = [q.angle * au for q in quaternionList] + outputs['Angle'].sv_set([angleList]) + + elif self.mode == "MATRIX": + if outputs['Matrix'].is_linked: + matrixList = [q.to_matrix().to_4x4() for q in quaternionList] + outputs['Matrix'].sv_set(matrixList) + + +def register(): + bpy.utils.register_class(SvQuaternionOutNode) + + +def unregister(): + bpy.utils.unregister_class(SvQuaternionOutNode) -- GitLab From c8c8e0faabcaee011691f5ed2160ceb13851074a Mon Sep 17 00:00:00 2001 From: Victor Doval <10011941+vicdoval@users.noreply.github.com> Date: Tue, 1 Jan 2019 22:23:48 +0100 Subject: [PATCH 23/60] Renamed Deformation, cleaned and docs --- docs/nodes/analyzers/analyzers_index.rst | 3 + docs/nodes/analyzers/deformation.rst | 52 ++++++++ nodes/analyzer/{tension.py => deformation.py} | 122 ++++++++++-------- 3 files changed, 124 insertions(+), 53 deletions(-) create mode 100644 docs/nodes/analyzers/deformation.rst rename nodes/analyzer/{tension.py => deformation.py} (64%) diff --git a/docs/nodes/analyzers/analyzers_index.rst b/docs/nodes/analyzers/analyzers_index.rst index 0d5d54f98..8f5788721 100644 --- a/docs/nodes/analyzers/analyzers_index.rst +++ b/docs/nodes/analyzers/analyzers_index.rst @@ -8,7 +8,10 @@ Analyzers area bbox distance_pp + deformation edge_angles + evaluate_image + image_components kd_tree kd_tree_edges_mk2 mesh_filter diff --git a/docs/nodes/analyzers/deformation.rst b/docs/nodes/analyzers/deformation.rst new file mode 100644 index 000000000..acd53b5a0 --- /dev/null +++ b/docs/nodes/analyzers/deformation.rst @@ -0,0 +1,52 @@ +Deformation +===== + +Functionality +------------- + +Deformation node is one of the analyzer type. It is used to get the deformation of one or many meshes between different states. The deformation is measured by the areal variance or the edge elongation + + +Inputs +---------- + +All inputs need to proceed from an external node + + ++-------------------+---------------+-------------+-----------------------------------------------+ +| Input | Type | Default | Description | ++===================+===============+=============+===============================================+ +| **Rest Verts** | Vertices | None | Vertices in relaxed state | ++-------------------+---------------+-------------+-----------------------------------------------+ +| **Distort Verts** | Vertices | None | Vertices in the deformed state | ++-------------------+---------------+-------------+-----------------------------------------------+ +| **Pols** | Strings | None | Polygons referenced to vertices | ++-------------------+---------------+-------------+-----------------------------------------------+ +| **Edges** | Strings | None | Edges referenced to vertices | ++-------------------+---------------+-------------+-----------------------------------------------+ + +Outputs + +------- +**Edges_Def** :the variation of the length of the edges +**Pols_Def** : the variation of the areas of each polygon + +**Vert_Pol_Def**: Each polygon will distribute its surface variation to its vertices. Each vertex will get the sum of the deformations related to itself. +**Vert_Edge_Def**: Each edge will distribute its elongation to its vertices. Each vertex will get the sum of elongations related to itself. + + +Example of usage +---------------- +Different kinds of tension maps can be created using this node, from any of its outputs, here some basic examples: + +.. image:: https://user-images.githubusercontent.com/10011941/50576192-a7da2a80-0e0c-11e9-9be5-e490081822bb.png + :alt: DefromationNode1.PNG + +The elongation can proportional to the tension, in this example the node is used to get one approximation of the needed thickness to sustain the tensions applied to a cloth simulation. +.. image:: https://user-images.githubusercontent.com/10011941/50576196-ba546400-0e0c-11e9-8c5c-15488c9a0d04.png + :alt: DefromationNode2.PNG + +You can compare many different states at the same time. +.. image:: https://user-images.githubusercontent.com/10011941/50576199-d526d880-0e0c-11e9-89cf-12cd8462da41.png + :alt: DefromationNode3.PNG + diff --git a/nodes/analyzer/tension.py b/nodes/analyzer/deformation.py similarity index 64% rename from nodes/analyzer/tension.py rename to nodes/analyzer/deformation.py index 7cea3b2a7..f874eebbe 100644 --- a/nodes/analyzer/tension.py +++ b/nodes/analyzer/deformation.py @@ -33,24 +33,26 @@ def edge_elongation(np_verts, np_verts_n, np_edges): pairs = np_verts_n[np_edges, :] dif_v = pairs[:, 0, :] - pairs[:, 1, :] dist = np.linalg.norm(dif_v, axis=1) - dif_l = dist - dist_rest + elong = dist - dist_rest - return dif_l + return elong -def vert_edge_tension(dif_l, np_verts, np_edges): - '''Redistribute edge length variation to verts''' - x = dif_l[:, np.newaxis] / 2 +def vert_edge_defromation(elong, np_verts, np_edges): + '''Redistribute edge length variation to vertices''' + + x = elong[:, np.newaxis] / 2 v_len = len(np_verts) - tension = np.zeros((v_len, v_len, 1), dtype=np.float32) - tension[np_edges[:, 0], np_edges[:, 1], :] = x - tension[np_edges[:, 1], np_edges[:, 0], :] = x - tension = np.sum(tension, axis=1)[:, 0] - return tension + deformation = np.zeros((v_len, v_len, 1), dtype=np.float32) + deformation[np_edges[:, 0], np_edges[:, 1], :] = x + deformation[np_edges[:, 1], np_edges[:, 0], :] = x + + return np.sum(deformation, axis=1)[:, 0] def area_calc_setup(pols): '''Analyze pols information''' + np_pols = np.array(pols) p_len = len(pols) if np_pols.dtype == np.object: @@ -69,7 +71,8 @@ def area_calc_setup(pols): def get_normals(v_pols): - '''calculate polygon normals''' + '''Calculate polygon normals''' + v1 = v_pols[:, 1, :] - v_pols[:, 0, :] v2 = v_pols[:, 2, :] - v_pols[:, 0, :] pols_normal = np.cross(v1, v2) @@ -80,6 +83,7 @@ def get_normals(v_pols): def area_calc(np_verts, area_params): '''Calculate polygons area''' + np_pols, pols_sides_max, pols_sides, p_len, p_non_regular = area_params v_pols = np_verts[np_pols, :] @@ -107,72 +111,79 @@ def area_calc(np_verts, area_params): return area -def area_to_verts(np_verts, area_params, pols_tension): - '''Redistribute area variation to verts''' +def area_to_verts(np_verts, area_params, pols_deformation): + '''Redistribute area variation to verts.''' + np_pols, pols_sides_max, pols_sides, p_len, advance = area_params pol_id = np.arange(p_len) - pol_tens_to_vert = pols_tension / pols_sides - tension = np.zeros((len(np_verts), p_len), dtype=np.float32) + pol_def_to_vert = pols_deformation / pols_sides + deformation = np.zeros((len(np_verts), p_len), dtype=np.float32) if advance: for i in range(pols_sides_max): mask = pols_sides > i - tension[np_pols[mask, i], pol_id[mask]] += pol_tens_to_vert[mask] + deformation[np_pols[mask, i], pol_id[mask]] += pol_def_to_vert[mask] else: for i in range(pols_sides_max): - tension[np_pols[:, i], pol_id] += pols_tension + deformation[np_pols[:, i], pol_id] += pol_def_to_vert - return np.sum(tension, axis=1) + return np.sum(deformation, axis=1) -def calc_pols_tension(np_verts, np_verts_n, area_params): +def area_variation(np_verts, np_verts_n, area_params): + '''get areas and subtract relaxed area to deformed area''' + relax_area = area_calc(np_verts, area_params) - tension_area = area_calc(np_verts_n, area_params) - return tension_area - relax_area + defromation_area = area_calc(np_verts_n, area_params) + + return defromation_area - relax_area -def calc_tensions(meshes, gates, result): +def calc_deformations(meshes, gates, result): + '''calculate edge elong and polygon area variation''' for vertices, vertices_n, edges, pols in zip(*meshes): np_verts = np.array(vertices) np_verts_n = np.array(vertices_n) if len(edges) > 0 and (gates[0] or gates[1]): np_edges = np.array(edges) - dif_l = edge_elongation(np_verts, np_verts_n, np_edges) - result[0].append(dif_l if gates[4] else dif_l.tolist()) + elong = edge_elongation(np_verts, np_verts_n, np_edges) + result[0].append(elong if gates[4] else elong.tolist()) if gates[1]: - tension = vert_edge_tension(dif_l, np_verts, np_edges) - result[1].append(tension if gates[4] else tension.tolist()) + elong_v = vert_edge_defromation(elong, np_verts, np_edges) + result[1].append(elong_v if gates[4] else elong_v.tolist()) if len(pols) > 0 and (gates[2] or gates[3]): area_params = area_calc_setup(pols) - pols_tension = calc_pols_tension(np_verts, np_verts_n, area_params) - result[2].append(pols_tension if gates[4] else pols_tension.tolist()) + area_var = area_variation(np_verts, np_verts_n, area_params) + result[2].append(area_var if gates[4] else area_var.tolist()) if gates[3]: - tens_verts_pols = area_to_verts(np_verts, area_params, pols_tension) - result[3].append(tens_verts_pols if gates[4] else tens_verts_pols.tolist()) - + area_var_v = area_to_verts(np_verts, area_params, area_var) + result[3].append(area_var_v if gates[4] else area_var_v.tolist()) + return result -class SvTensionNode(bpy.types.Node, SverchCustomTreeNode): +class SvDeformationNode(bpy.types.Node, SverchCustomTreeNode): ''' Triggers: Measure deformation - Tooltip: Measure deformation + Tooltip: Deformation between to states, edge elong a area variation ''' - bl_idname = 'SvTensionNode' - bl_label = 'Tension' + bl_idname = 'SvDeformationNode' + bl_label = 'Deformation' bl_icon = 'SNAP_NORMAL' output_numpy = BoolProperty( name='Output NumPy', description='output NumPy arrays', default=False, update=updateNode) - + def draw_buttons_ext(self, context, layout): - layout.prop(self, "output_numpy", toggle=False) - + '''draw buttons on the N-panel''' + layout.prop(self, "output_numpy", toggle=False) + def sv_init(self, context): + '''create sockets''' sinw = self.inputs.new sonw = self.outputs.new sinw('VerticesSocket', "Rest Verts") @@ -180,12 +191,13 @@ class SvTensionNode(bpy.types.Node, SverchCustomTreeNode): sinw('StringsSocket', "Edges") sinw('StringsSocket', "Pols") - sonw('StringsSocket', "Edges_Tension") - sonw('StringsSocket', "Pols_Tension") - sonw('StringsSocket', "Vert_Edge_T") - sonw('StringsSocket', "Vert_Pol_T") + sonw('StringsSocket', "Edges_Def") + sonw('StringsSocket', "Pols_Def") + sonw('StringsSocket', "Vert_Edge_Def") + sonw('StringsSocket', "Vert_Pol_Def") def get_data(self): + '''get all data from sockets''' si = self.inputs vertices_s = si['Rest Verts'].sv_get(default=[[]]) vertices_n = si['Distort Verts'].sv_get(default=[[]]) @@ -204,33 +216,37 @@ class SvTensionNode(bpy.types.Node, SverchCustomTreeNode): return ready def process(self): + '''main node function called every update''' so = self.outputs if not self.ready(): return result = [[], [], [], []] gates = [] - gates.append(so['Edges_Tension'].is_linked) - gates.append(so['Vert_Edge_T'].is_linked) - gates.append(so['Pols_Tension'].is_linked) - gates.append(so['Vert_Pol_T'].is_linked) + gates.append(so['Edges_Def'].is_linked) + gates.append(so['Vert_Edge_Def'].is_linked) + gates.append(so['Pols_Def'].is_linked) + gates.append(so['Vert_Pol_Def'].is_linked) gates.append(self.output_numpy) meshes = self.get_data() - result = calc_tensions(meshes, gates, result) + + result = calc_deformations(meshes, gates, result) if gates[0]: - so['Edges_Tension'].sv_set(result[0]) + so['Edges_Def'].sv_set(result[0]) if gates[1]: - so['Vert_Edge_T'].sv_set(result[1]) + so['Vert_Edge_Def'].sv_set(result[1]) if gates[2]: - so['Pols_Tension'].sv_set(result[2]) + so['Pols_Def'].sv_set(result[2]) if gates[3]: - so['Vert_Pol_T'].sv_set(result[3]) + so['Vert_Pol_Def'].sv_set(result[3]) def register(): - bpy.utils.register_class(SvTensionNode) + '''register class in Blender''' + bpy.utils.register_class(SvDeformationNode) def unregister(): - bpy.utils.unregister_class(SvTensionNode) + '''unregister class in Blender''' + bpy.utils.unregister_class(SvDeformationNode) -- GitLab From 9490bd29e7f4c70fb57e4b2c9ad207499f960161 Mon Sep 17 00:00:00 2001 From: DolphinDream Date: Mon, 31 Dec 2018 15:30:30 -0500 Subject: [PATCH 24/60] Restructure the MixNumber node as MixInput node + add new types & features The node will mix various other types (not just int/float) hence the name change. Add various types to the MixInput node + new features & update UI - add new types to mix: Vectors, Colors, Quaternions and Matrices. - add SWAP feature to let the input A & B be swapped - add MIRROR feature to mirror the interpolation factor - add custom icon to MixInputs node - relocate old MixNumbers node to old nodes - update documentation --- docs/nodes/number/mix_inputs.rst | 85 +++++ docs/nodes/number/mix_numbers.rst | 88 ----- index.md | 2 +- nodes/number/mix_inputs.py | 375 +++++++++++++++++++++ {nodes/number => old_nodes}/mix_numbers.py | 0 ui/icons/sv_mix_inputs.png | Bin 0 -> 2620 bytes 6 files changed, 461 insertions(+), 89 deletions(-) create mode 100644 docs/nodes/number/mix_inputs.rst delete mode 100644 docs/nodes/number/mix_numbers.rst create mode 100644 nodes/number/mix_inputs.py rename {nodes/number => old_nodes}/mix_numbers.py (100%) create mode 100644 ui/icons/sv_mix_inputs.png diff --git a/docs/nodes/number/mix_inputs.rst b/docs/nodes/number/mix_inputs.rst new file mode 100644 index 000000000..06f3964b9 --- /dev/null +++ b/docs/nodes/number/mix_inputs.rst @@ -0,0 +1,85 @@ +Mix Inputs +========== + +Functionality +------------- + +This node mixes two values of different types using a given factor, a selected interpolation and easing function. + +For a factor of 0.0 it outputs the first value, for the factor of 1.0 it outputs the last value and for any other factor in between 0-1 it outputs an interpolated value between the first and second input value. An exception to this are the "Back" and "Elastic" interpolations which generate output values that are not strictly confined to the first-second value interval, but they will output values that start at first and end at second value. + +Inputs & Parameters +------------------- + +All parameters except for **Mode**, **Interpolation**, **Easing**, **Mirror** and **Swap** can be given by the node or an external input. + +Based on the selected **Mode** the node changes the input and output socket types to match the corresponding type. + +The node is vectorized so the inputs values (A/B) take either a single or a list of values. The node will extend the shortest list to match the longest list before mixing the values in the two lists. + +The node has the following parameters: + ++-------------------+--------------+-------------+-----------------------------------------------------+ +| Parameter | Type | Default | Description | ++===================+==============+=============+=====================================================+ +| **Mode** | Enum: | Float | The type of inputs values to mix. | +| | Int | | | +| | Float | | | +| | Vector | | | +| | Color | | | +| | Matrix | | | +| | Quaternion | | | ++-------------------+--------------+-------------+-----------------------------------------------------+ +| **Interpolation** | Enum: | Linear | Type of interpolation. | +| | Linear | | f(x) ~ x | +| | Sinusoidal | | f(x) ~ sin(x) | +| | Quadratic | | f(x) ~ x*2 | +| | Cubic | | f(x) ~ x^3 | +| | Quadric | | f(x) ~ x^4 | +| | Quintic | | f(x) ~ x^5 | +| | Exponential | | f(x) ~ e^x | +| | Circular | | f(x) ~ sqrt(1-x*x) | +| | Back | | f(x) ~ x*x*x - x*sin(x) | +| | Bounce | | f(x) ~ series of geometric progression parabolas | +| | Elastic | | f(x) ~ sin(x) * e^x | ++-------------------+--------------+-------------+-----------------------------------------------------+ +| **Easing** | Enum | Ease In-Out | Type of easing. | +| | Ease In | | Ease In = slowly departs the starting value | +| | Ease Out | | Ease Out = slowly approaches the ending value | +| | Ease In-Out | | Ease In-Out = slowly departs and approaches values | ++-------------------+--------------+-------------+-----------------------------------------------------+ +| **Mirror** | Bool | False | Mirror the mixing factor around 0.5. | ++-------------------+--------------+-------------+-----------------------------------------------------+ +| **Swap** | Bool | False | Swap the two input values A and B. | ++-------------------+--------------+-------------+-----------------------------------------------------+ +| **Factor** | Float | 0.5 | Mixing factor (between 0.0 and 1.0) | ++-------------------+--------------+-------------+-----------------------------------------------------+ +| **A** | Any type | | Starting value | ++-------------------+--------------+-------------+-----------------------------------------------------+ +| **B** | Any type | | Ending value | ++-------------------+--------------+-------------+-----------------------------------------------------+ + + +Extra Parameters +---------------- +For certain interpolation types the node provides extra parameters on the Property Panel. + +* Exponential + Extra parameters to adjust the base and the exponent of the exponential function. The Defaults are 2 and 10.0. + +* Back + Extra parameters to adjust the scale of the overshoot. The default is 1.0. + +* Bounce + Extra parameters to adjust the attenuation of the bounce and the number of bounces. The defaults are 0.5 and 4. + +* Elastic + Extra parameters to adjust the base and the exponent of the damping oscillation as well as the number of bounces (oscillations). The defaults are 1.6, 6.0 and 6. + + +Outputs +------- + +Based on the selected **Mode** the node outputs the corresponding type value. + + diff --git a/docs/nodes/number/mix_numbers.rst b/docs/nodes/number/mix_numbers.rst deleted file mode 100644 index 03d00bcd5..000000000 --- a/docs/nodes/number/mix_numbers.rst +++ /dev/null @@ -1,88 +0,0 @@ -Mix Numbers -=========== - -Functionality -------------- - -This node mixes two values using a given factor and a selected interpolation and easing functions. - -For a factor of 0.0 it outputs the first value while the factor of 1.0 it outputs the last value. For every factor value between 0-1 it will output a value between the first and second input value. (*) - -Note: -(*) The Back and Elastic interpolations will generate outputs that are not strictly confined to the first-second value interval, but they will output values that start at first and end at second value. - -Inputs & Parameters -------------------- - -All parameters except for **Type**, **Interpolation** and **Easing** can be given by the node or an external input. - -This node has the following parameters: - -+-------------------+---------------+-------------+-----------------------------------------------------+ -| Parameter | Type | Default | Description | -+===================+===============+=============+=====================================================+ -| **Type** | Enum: | Float | Type of inputs values to interpolate. | -| | Int | | When Float is selected the input value1 and value2 | -| | Float | | expect float values | -| | | | When Int is selected the input value1 and value2 | -| | | | expect int values. | -+-------------------+---------------+-------------+-----------------------------------------------------+ -| **Interpolation** | Enum: | Linear | Type of interpolation. | -| | Linear | | f(x) ~ x | -| | Sinusoidal | | f(x) ~ sin(x) | -| | Quadratic | | f(x) ~ x*2 | -| | Cubic | | f(x) ~ x^3 | -| | Quadric | | f(x) ~ x^4 | -| | Quintic | | f(x) ~ x^5 | -| | Exponential | | f(x) ~ e^x | -| | Circular | | f(x) ~ sqrt(1-x*x) | -| | Back | | f(x) ~ x*x*x - x*sin(x) | -| | Bounce | | f(x) ~ series of geometric progression parabolas | -| | Elastic | | f(x) ~ sin(x) * e^x | -+-------------------+---------------+-------------+-----------------------------------------------------+ -| **Easing** | Enum | Ease In-Out | Type of easing. | -| | Ease In | | Ease In = slowly departs the starting value | -| | Ease Out | | Ease Out = slowly approaches the ending value | -| | Ease In-Out | | Ease In-Out = slowly departs and approaches values | -+-------------------+---------------+-------------+-----------------------------------------------------+ -| **Value1** | Int or Float | 0 or 0.0 | Starting value | -+-------------------+---------------+-------------+-----------------------------------------------------+ -| **Value2** | Int or Float | 1 or 1.0 | Ending value | -+-------------------+---------------+-------------+-----------------------------------------------------+ -| **Factor** | Float | 0.5 | Mixing factor (between 0.0 and 1.0) | -+-------------------+---------------+-------------+-----------------------------------------------------+ - -Extra Parameters ----------------- -For certain interpolation types the node provides extra parameters on the property panel. - -* Exponential - Extra parameters to adjust the base and the exponent of the exponential function. The Defaults are 2 and 10.0. - -* Back - Extra parameters to adjust the scale of the overshoot. The default is 1.0. - -* Bounce - Extra parameters to adjust the attenuation of the bounce and the number of bounces. The defaults are 0.5 and 4. - -* Elastic - Extra parameters to adjust the base and the exponent of the damping oscillation as well as the number of bounces (oscillations). - The defaults are 1.6, 6.0 and 6. - -Outputs -------- - -This node has one output: **Value**. - -Inputs and outputs are vectorized, so if series of values is passed to one of inputs, then this node will produce several sequences. - -Example of usage ----------------- - -Given simplest nodes setup: - -# - -you will have something like: - -# diff --git a/index.md b/index.md index 3ecc93b0b..2bbae3202 100644 --- a/index.md +++ b/index.md @@ -180,7 +180,7 @@ SvRndNumGen RandomNode SvEasingNode - SvMixNumbersNode + SvMixInputsNode ## Vector GenVectorsNode diff --git a/nodes/number/mix_inputs.py b/nodes/number/mix_inputs.py new file mode 100644 index 000000000..a973af3fb --- /dev/null +++ b/nodes/number/mix_inputs.py @@ -0,0 +1,375 @@ +# ##### 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 bpy +from bpy.props import IntProperty, FloatProperty, BoolProperty, EnumProperty, FloatVectorProperty +from sverchok.node_tree import SverchCustomTreeNode +from sverchok.data_structure import updateNode, match_long_repeat +from sverchok.data_structure import Matrix_generate +from sverchok.utils.sv_easing_functions import * +from mathutils import Matrix, Quaternion +import re + +idMat = [[tuple(v) for v in Matrix()]] # identity matrix + +input_info = { + 'INT': { + "Index": 0, + "SocketType": "StringsSocket", + "PropType": ('IntProperty', dict()), + "PropDefault": {"A": 0, "B": 1}, + "Mixer": lambda t, v1, v2: (int(v1 * (1 - t) + v2 * t)), + "Inputs": lambda ia, ib: [ia.sv_get()[0], ib.sv_get()[0]], + "Outputs": lambda o, v: o.sv_set([v]) + }, + + 'FLOAT': { + "Index": 1, + "SocketType": "StringsSocket", + "PropType": ('FloatProperty', dict()), + "PropDefault": {"A": 0.0, "B": 1.0}, + "Mixer": lambda t, v1, v2: v1 * (1 - t) + v2 * t, + "Inputs": lambda ia, ib: [ia.sv_get()[0], ib.sv_get()[0]], + "Outputs": lambda o, v: o.sv_set([v]) + }, + + 'VECTOR': { + "Index": 2, + "SocketType": "VerticesSocket", + "PropType": ('FloatVectorProperty', dict(size=3, subtype='XYZ')), + "PropDefault": {"A": (0.0, 0.0, 0.0), "B": (1.0, 1.0, 1.0)}, + "Mixer": lambda t, v1, v2: tuple([v1[i] * (1 - t) + v2[i] * t for i in range(3)]), + "Inputs": lambda ia, ib: [ia.sv_get()[0], ib.sv_get()[0]], + "Outputs": lambda o, v: o.sv_set([v]) + }, + + 'COLOR': { + "Index": 3, + "SocketType": "SvColorSocket", + "PropType": ('FloatVectorProperty', dict(size=4, subtype='COLOR', min=0, max=1)), + "PropDefault": {"A": (0.0, 0.0, 0.0, 1.0), "B": (1.0, 1.0, 1.0, 1.0)}, + "Mixer": lambda t, v1, v2: tuple([v1[i] * (1 - t) + v2[i] * t for i in range(4)]), + "Inputs": lambda ia, ib: [ia.sv_get()[0], ib.sv_get()[0]], + "Outputs": lambda o, v: o.sv_set([v]) + }, + + 'QUATERNION': { + "Index": 4, + "SocketType": "SvQuaternionSocket", + "PropType": ('FloatVectorProperty', dict(size=4, subtype='QUATERNION')), + "PropDefault": {"A": (0.0, 0.0, 0.0, 0.0), "B": (1.0, 1.0, 1.0, 1.0)}, + "Mixer": lambda t, v1, v2: quaternionMix(t, v1, v2), + "Inputs": lambda ia, ib: [ia.sv_get()[0], ib.sv_get()[0]], + "Outputs": lambda o, v: o.sv_set([v]) + }, + + 'MATRIX': { + "Index": 5, + "SocketType": "MatrixSocket", + "Mixer": lambda t, v1, v2: matrixMix(t, v1, v2), + "Inputs": lambda ia, ib: [ia.sv_get(default=idMat), ib.sv_get(default=idMat)], + "Outputs": lambda o, v: o.sv_set(v) + } +} + +interplationItems = [ + ("LINEAR", "Linear", "", "IPO_LINEAR", 0), + ("SINUSOIDAL", "Sinusoidal", "", "IPO_SINE", 1), + ("QUADRATIC", "Quadratic", "", "IPO_QUAD", 2), + ("CUBIC", "Cubic", "", "IPO_CUBIC", 3), + ("QUARTIC", "Quartic", "", "IPO_QUART", 4), + ("QUINTIC", "Quintic", "", "IPO_QUINT", 5), + ("EXPONENTIAL", "Exponential", "", "IPO_EXPO", 6), + ("CIRCULAR", "Circular", "", "IPO_CIRC", 7), + # DYNAMIC effects + ("BACK", "Back", "", "IPO_BACK", 8), + ("BOUNCE", "Bounce", "", "IPO_BOUNCE", 9), + ("ELASTIC", "Elastic", "", "IPO_ELASTIC", 10)] + +easingItems = [ + ("EASE_IN", "Ease In", "", "IPO_EASE_IN", 0), + ("EASE_OUT", "Ease Out", "", "IPO_EASE_OUT", 1), + ("EASE_IN_OUT", "Ease In-Out", "", "IPO_EASE_IN_OUT", 2)] + + +def matrixMix(t, v1, v2): + m1 = Matrix_generate([v1])[0] + m2 = Matrix_generate([v2])[0] + m = m1.lerp(m2, t) + return m + + +def quaternionMix(t, v1, v2): + q1 = Quaternion(v1) + q2 = Quaternion(v2) + q = q1.slerp(q2, max(0, min(t, 1))) + return q + + +def make_prop(mode, label): + ''' Property Factory ''' + description = "Mix " + mode + " value " + label + default = input_info[mode]["PropDefault"][label] + # general parameters + params = dict(name=label, description=description, default=default, update=updateNode) + # add type specific parameters + params.update(input_info[mode]["PropType"][1]) + propType = input_info[mode]["PropType"][0] + return getattr(bpy.props, propType)(**params) + + +class SvMixInputsNode(bpy.types.Node, SverchCustomTreeNode): + """ + Triggers: Interpolate, Slerp + Tooltip: Interpolate between values of various types + """ + bl_idname = 'SvMixInputsNode' + bl_label = 'Mix Inputs' + sv_icon = 'SV_MIX_INPUTS' + + # SV easing based interpolator + def get_interpolator(self): + # get the interpolator function based on selected interpolation and easing + if self.interpolation == "LINEAR": + return LinearInterpolation + else: + ''' This maps the Strings used in the Enumerator properties to the associated function''' + interpolatorName = self.interpolation + "_" + self.easing + interpolatorName = re.sub('SINUSOIDAL', 'sine', interpolatorName) # for the exception + interpolate = globals()[re.sub(r'[_]', '', interpolatorName.lower().title())] + + # setup the interpolator with prepared parameters + if self.interpolation == "EXPONENTIAL": + b = self.exponentialBase + e = self.exponentialExponent + settings = prepareExponentialSettings(b, e) + return lambda v: interpolate(v, settings) + + elif self.interpolation == "BACK": + s = self.backScale + return lambda v: interpolate(v, s) + + elif self.interpolation == "ELASTIC": + n = self.elasticBounces + b = self.elasticBase + e = self.elasticExponent + settings = prepareElasticSettings(n, b, e) + return lambda v: interpolate(v, settings) + + elif self.interpolation == "BOUNCE": + n = self.bounceBounces + a = self.bounceAttenuation + settings = prepareBounceSettings(n, a) + return lambda v: interpolate(v, settings) + + else: + return interpolate + + def update_mode(self, context): + self.update_sockets() + updateNode(self, context) + + typeDict = {input_info[t]["Index"]: t for t in input_info.keys()} + typeItems = [(v, v.title(), "", "", k) for (k, v) in sorted(typeDict.items())] + + mode = EnumProperty( + name="Mode", description="The type of the values to mix", + default="FLOAT", items=typeItems, + update=update_mode) + + # INTERPOLATION settings + interpolation = EnumProperty( + name="Interpolation", description="Interpolation type", + default="LINEAR", items=interplationItems, + update=updateNode) + + easing = EnumProperty( + name="Easing", description="Easing type", + default="EASE_IN_OUT", items=easingItems, + update=updateNode) + + # BACK interpolation settings + backScale = FloatProperty( + name="Scale", description="Back scale", + default=0.5, soft_min=0.0, soft_max=10.0, + update=updateNode) + + # ELASTIC interpolation settings + elasticBase = FloatProperty( + name="Base", description="Elastic base", + default=1.6, soft_min=0.0, soft_max=10.0, + update=updateNode) + + elasticExponent = FloatProperty( + name="Exponent", description="Elastic exponent", + default=6.0, soft_min=0.0, soft_max=10.0, + update=updateNode) + + elasticBounces = IntProperty( + name="Bounces", description="Elastic bounces", + default=6, soft_min=1, soft_max=10, + update=updateNode) + + # EXPONENTIAL interpolation settings + exponentialBase = FloatProperty( + name="Base", description="Exponential base", + default=2.0, soft_min=0.0, soft_max=10.0, + update=updateNode) + + exponentialExponent = FloatProperty( + name="Exponent", description="Exponential exponent", + default=10.0, soft_min=0.0, soft_max=20.0, + update=updateNode) + + # BOUNCE interpolation settings + bounceAttenuation = FloatProperty( + name="Attenuation", description="Bounce attenuation", + default=0.5, soft_min=0.1, soft_max=0.9, + update=updateNode) + + bounceBounces = IntProperty( + name="Bounces", description="Bounce bounces", + default=4, soft_min=1, soft_max=10, + update=updateNode) + + # INPUT sockets props + factor = FloatProperty( + name="Factor", description="Factor value", + default=0.5, min=0.0, max=1.0, + update=updateNode) + + for m in input_info.keys(): # create props for input sockets A/B + if m != "MATRIX": + for l in ['A', 'B']: + attr_name = m.lower() + "_" + l.lower() + vars()[attr_name] = make_prop(m, l) + + mirror = BoolProperty( + name="Mirror", description="Mirror the interplation factor", + default=False, update=updateNode) + + swap = BoolProperty( + name="Swap", description="Swap the two inputs", + default=False, update=updateNode) + + def sv_init(self, context): + self.width = 180 + self.inputs.new('StringsSocket', "f").prop_name = 'factor' + self.inputs.new('StringsSocket', "A").prop_name = 'float_a' + self.inputs.new('StringsSocket', "B").prop_name = 'float_b' + self.outputs.new('StringsSocket', "Float") + self.update_sockets() + + def draw_buttons(self, context, layout): + layout.prop(self, 'mode', text="", expand=False) + row = layout.row(align=True) + row.prop(self, 'interpolation', text="", expand=False) + row.prop(self, 'easing', text="", expand=True) + row = layout.row(align=True) + row.prop(self, 'mirror', toggle=True) + row.prop(self, 'swap', toggle=True) + + def draw_label(self): + if self.mode == "MATRIX": + return "Mix Matrices" + else: + return "Mix " + self.mode.title() + "s" + + def draw_buttons_ext(self, context, layout): + if self.interpolation == "BACK": + layout.column().label(text="Interpolation:") + box = layout.box() + box.prop(self, 'backScale') + + elif self.interpolation == "ELASTIC": + layout.column().label(text="Interpolation:") + box = layout.box() + box.prop(self, 'elasticBase') + box.prop(self, 'elasticExponent') + box.prop(self, 'elasticBounces') + + elif self.interpolation == "EXPONENTIAL": + layout.column().label(text="Interpolation:") + box = layout.box() + box.prop(self, 'exponentialBase') + box.prop(self, 'exponentialExponent') + + elif self.interpolation == "BOUNCE": + layout.column().label(text="Interpolation:") + box = layout.box() + box.prop(self, 'bounceAttenuation') + box.prop(self, 'bounceBounces') + + def get_mixer(self): + return input_info[self.mode]['Mixer'] + + def get_inputs(self): + input_getter = input_info[self.mode]["Inputs"] + f = self.inputs["f"].sv_get()[0] + i, j = [2, 1] if self.swap else [1, 2] # swap inputs ? + [a, b] = input_getter(self.inputs[i], self.inputs[j]) + return [f, a, b] + + def set_ouputs(self, values): + output_setter = input_info[self.mode]["Outputs"] + output_setter(self.outputs[0], values) + + def update_sockets(self): + # replace input and output sockets, linking input socket to corresponding props + new_socket_type = input_info[self.mode]["SocketType"] + for socket in self.inputs[1:]: + if self.mode != "MATRIX": + prop_name = self.mode.lower() + "_" + socket.name.lower() + socket.replace_socket(new_socket_type).prop_name = prop_name + else: + socket.replace_socket(new_socket_type) + + self.outputs[0].replace_socket(new_socket_type, self.mode.title()) + + def process(self): + # return if no outputs are connected + if not any(s.is_linked for s in self.outputs): + return + + # input values lists (single or multi value) + input_factor, input_value1, input_value2 = self.get_inputs() + + parameters = match_long_repeat([input_factor, input_value1, input_value2]) + + interpolate = self.get_interpolator() + + mix = self.get_mixer() + + values = [] + for f, v1, v2 in zip(*parameters): + if self.mirror: + f = 1 - 2 * abs(f - 0.5) + t = interpolate(f) + v = mix(t, v1, v2) + values.append(v) + + self.set_ouputs(values) + + +def register(): + bpy.utils.register_class(SvMixInputsNode) + + +def unregister(): + bpy.utils.unregister_class(SvMixInputsNode) diff --git a/nodes/number/mix_numbers.py b/old_nodes/mix_numbers.py similarity index 100% rename from nodes/number/mix_numbers.py rename to old_nodes/mix_numbers.py diff --git a/ui/icons/sv_mix_inputs.png b/ui/icons/sv_mix_inputs.png new file mode 100644 index 0000000000000000000000000000000000000000..790b24088d842adb38620ec9b802a006c73baa31 GIT binary patch literal 2620 zcmaJ@c{o)28y}RSqO>nE4JIvTR%WXqW(XPkR`hV@%)uD5m>CRlqjGOah>CP8YqoLK zjc!Gm7TVA%ZjrLLSfZ5W=66*3-9LKnd7g8=-|u_g<@0{t_dU-!;Ow|{p7siD6bd!Z z&X(#|yWyT?JBrG_H zZyP0r-J%@bp(p`F=V2_&(VGlUQ%r{Wp z9A`93EQQf#I0^s}O$lf!9Y>@POwEWJ(If(qiYJ)i2^4@pWDsZ!5)u9V!XVM4ya0wP z+va;LkCT9v=}Afr}vH#L_@Kkxr-M2_!s;1RxrKEK(!~6@W-)IPHNA%OELV zBIk=m=qZmNR~#m1Vi2U?QV>cwoF9%wvhPG8l;IVi1W&{f@Iv8KUSG9ka##5OFn+2n zbB~n3cvn~^4wFJiJpv4;jS=qtJJ6IOf`;KF#% z3d0ac1z?Cq1xyKCGk^|4WPk?|0tf`MDTTzPP5b$oy(P_>M6#q(31&1)BGH;|X=6&G z5lKWAiA1zyo06w(?L;y;D1zYW*nA}RcU#JT+A>&D7?g{p?qYGsbQd@WiREHhkXV9d zv8Jj{Lz{3wh%cHFuA3^F_^neB36hCq8W015iAef9BEUnK1Iz+Uxd5F+CIdvO z84aPBPNVX;7(40rmUbXPnp z_Yyd`d(AhmG8md~y~P=wt*#coIK;V#ZMiKgBg;1D*Ut&|4jt#!+mn}A*&Wh5xCFu+ zIK)?BH)1e3S+Qt!?R?AJ{Z2-~i`nxdEzY#HTz=dCr_I^7MmOMrOP?BkKQ6v{-0HF?n*Z{Wv-B=940~uQt4!|M@hI5 z78{_WqmvLZ@Z!MH4PFUNWqy;N&Q?@Z^uBwyh@D5iB6($XwxndkIp53j0~-P0wJJ`- zV({7R+xs)jW5#BQOO?u-2bbdIZ?=SokNkzh;qp>SyvkCGiq_TC)bttX>n~19N=g;7 zcE(rbboDavd}J`p4el6&#`9@AA($e|G~6O4OD&L z@~dHQkBxWNpLjgbR&8(rfpSj%+LZ{^&eiggb=SG<3jZ=Onv;{W=k(|T$7Qdc{$i~8 z?D_Mxs{;0!sajelPJevc^!R+>{h%u8_=u1jt@?1^cc$lq*8cwfwz;~xCpN6tu)<=o ztcHdLr}-BWGBPsGkBp4yC$ySUsg+}&KfA9rGNPfn7AE=f`RlgsX>K-9Q&ao=OCriO z|Kz&7>S{l9d3$@!L3O&&Zrir!mEa9cy0g+^*T_4Pg++CiQmNEhX|QbBe8E%Yg2>27 zXoRE_HP+&9Vc&y=b#@z_zY)de3Q~Jdge|&6{ag~W*BEvM87*ZsWl}#d(BDn z7=~{yyGJ_X55HzJ2US8=EiR5#v{hT>-Zrn@9S2i{_Mpk>3Auyx% z{=LQDQi?vMvwE{_kmtTr+1;u$oD;Nx9y7W+8=8r&(s-#OZn^k=@WF$Wt6X8CM$4R( z++;mRZ*OnP@bx0Mf+vXfIM3B0&vYs`IJ8Rpw%%q1{NNVleGSO;y;&7Z*eK#;(+! zJGb-lF~Q&Y@ebG$jT0lNPCJ8xsDpEN#F@&o!^g9(3{)h`kPdfDHn8v{H&pcX^>L8y zIE*STDA+nWI(n_x=4Dl4TVQI(JhqL^5Fw|mJ}$G==P$i*4gdhb2?wkXCIzIn^=;{vBX4}Iq&pg>UaBzlX(cFFaWBJajHtpy9l%(K(Cb>**jadDZkHzH{@ zbA_9q-+-FRCQGT+kzSq@_nY(La>e`hRUvh}?z-dlvuZgtCQh2Gay`cU$UyJH&eyN+ zbTMWO5+#j=8CqKUN1|h5?qAzZj@Q`vGS+Fu{rY-i?2@{$U1_PQdXd!G`A-8M(v4dP z{{(Bd-SEGD!h|;=+jI=S{ZFIy6pCjicTv}De>>qq1HJh8cy*UxpL1MmtLW&7fuQv^ zy|X-gE`*FS3=N^{eao5`WTaY$hQ8naML6f5#hQX^I}Q~m_%=a+Zd0p?$69u*z(@L| zwzhVmK)+>{`Bqw=FLU$43mqMmWjiaeDNC920&ks7v)`Ag(sw~VJPo@(^d2@#(^G)H z3sl40=NE(%rE=rN!)sp2Y6srD30}_FX`J5jtU~bm_F+S#^m|2xh5P-3>KZI1dU(}4 z@!j_Wlc(lfeAwC9xsR*drg8nC3wL+!wF=yha@%?Z1m!+?kdvFMyNOPZxwKbnmEOy^ z0e4-j?}f80)HuG%yjHE;>Ex+IXZkhmS;fW0c~{Sg+supB`j2^%vgL$VxsO^aBWC9M zda2`jyYKWgpM%tM$4%~1bdK!KC Date: Tue, 1 Jan 2019 22:55:05 +0100 Subject: [PATCH 25/60] Docs format fixed and some more explanations --- docs/nodes/analyzers/deformation.rst | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/docs/nodes/analyzers/deformation.rst b/docs/nodes/analyzers/deformation.rst index acd53b5a0..d1f4079bc 100644 --- a/docs/nodes/analyzers/deformation.rst +++ b/docs/nodes/analyzers/deformation.rst @@ -25,28 +25,31 @@ All inputs need to proceed from an external node | **Edges** | Strings | None | Edges referenced to vertices | +-------------------+---------------+-------------+-----------------------------------------------+ -Outputs +In the N-Panel you can use the toggle **Output NumPy** to get NumPy arrays (makes the node faster) +Outputs ------- -**Edges_Def** :the variation of the length of the edges -**Pols_Def** : the variation of the areas of each polygon +- **Edges_Def** :the variation of the length of the edges. +- **Pols_Def** : the variation of the areas of each polygon. -**Vert_Pol_Def**: Each polygon will distribute its surface variation to its vertices. Each vertex will get the sum of the deformations related to itself. -**Vert_Edge_Def**: Each edge will distribute its elongation to its vertices. Each vertex will get the sum of elongations related to itself. +- **Vert_Pol_Def**: Each polygon will distribute its areal variation to its vertices. Each vertex will get the sum of the deformations related to itself. +- **Vert_Edge_Def**: Each edge will distribute its elongation to its vertices. Each vertex will get the sum of elongations related to itself. -Example of usage +Examples of usage ---------------- Different kinds of tension maps can be created using this node, from any of its outputs, here some basic examples: .. image:: https://user-images.githubusercontent.com/10011941/50576192-a7da2a80-0e0c-11e9-9be5-e490081822bb.png :alt: DefromationNode1.PNG -The elongation can proportional to the tension, in this example the node is used to get one approximation of the needed thickness to sustain the tensions applied to a cloth simulation. +The elongation can be used to get to the tension of a spring system, in this example the node is used to get one approximation of the needed thickness to sustain the tensions applied to a cloth simulation. + .. image:: https://user-images.githubusercontent.com/10011941/50576196-ba546400-0e0c-11e9-8c5c-15488c9a0d04.png :alt: DefromationNode2.PNG You can compare many different states at the same time. + .. image:: https://user-images.githubusercontent.com/10011941/50576199-d526d880-0e0c-11e9-89cf-12cd8462da41.png :alt: DefromationNode3.PNG -- GitLab From 410294726a6e763d5963c354b6d5159d31d26679 Mon Sep 17 00:00:00 2001 From: Victor Doval <10011941+vicdoval@users.noreply.github.com> Date: Tue, 1 Jan 2019 23:15:17 +0100 Subject: [PATCH 26/60] Changed Node Icon --- nodes/analyzer/deformation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nodes/analyzer/deformation.py b/nodes/analyzer/deformation.py index f874eebbe..159935e3d 100644 --- a/nodes/analyzer/deformation.py +++ b/nodes/analyzer/deformation.py @@ -172,7 +172,7 @@ class SvDeformationNode(bpy.types.Node, SverchCustomTreeNode): ''' bl_idname = 'SvDeformationNode' bl_label = 'Deformation' - bl_icon = 'SNAP_NORMAL' + bl_icon = 'MOD_SIMPLEDEFORM' output_numpy = BoolProperty( name='Output NumPy', description='output NumPy arrays', -- GitLab From 7deef78c11def59b90c876a78f8bb6dd4c2c2f98 Mon Sep 17 00:00:00 2001 From: Victor Doval <10011941+vicdoval@users.noreply.github.com> Date: Wed, 2 Jan 2019 14:54:10 +0100 Subject: [PATCH 27/60] Removed underscores from output sockets --- nodes/analyzer/deformation.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/nodes/analyzer/deformation.py b/nodes/analyzer/deformation.py index 159935e3d..5dba5b4f9 100644 --- a/nodes/analyzer/deformation.py +++ b/nodes/analyzer/deformation.py @@ -191,10 +191,10 @@ class SvDeformationNode(bpy.types.Node, SverchCustomTreeNode): sinw('StringsSocket', "Edges") sinw('StringsSocket', "Pols") - sonw('StringsSocket', "Edges_Def") - sonw('StringsSocket', "Pols_Def") - sonw('StringsSocket', "Vert_Edge_Def") - sonw('StringsSocket', "Vert_Pol_Def") + sonw('StringsSocket', "Edges Def") + sonw('StringsSocket', "Pols Def") + sonw('StringsSocket', "Vert Edge Def") + sonw('StringsSocket', "Vert Pol Def") def get_data(self): '''get all data from sockets''' @@ -223,23 +223,23 @@ class SvDeformationNode(bpy.types.Node, SverchCustomTreeNode): result = [[], [], [], []] gates = [] - gates.append(so['Edges_Def'].is_linked) - gates.append(so['Vert_Edge_Def'].is_linked) - gates.append(so['Pols_Def'].is_linked) - gates.append(so['Vert_Pol_Def'].is_linked) + gates.append(so['Edges Def'].is_linked) + gates.append(so['Vert Edge Def'].is_linked) + gates.append(so['Pols Def'].is_linked) + gates.append(so['Vert Pol Def'].is_linked) gates.append(self.output_numpy) meshes = self.get_data() result = calc_deformations(meshes, gates, result) if gates[0]: - so['Edges_Def'].sv_set(result[0]) + so['Edges Def'].sv_set(result[0]) if gates[1]: - so['Vert_Edge_Def'].sv_set(result[1]) + so['Vert Edge Def'].sv_set(result[1]) if gates[2]: - so['Pols_Def'].sv_set(result[2]) + so['Pols Def'].sv_set(result[2]) if gates[3]: - so['Vert_Pol_Def'].sv_set(result[3]) + so['Vert Pol Def'].sv_set(result[3]) def register(): -- GitLab From c983209f08ab0936cf9fb834d52b3febdadea6cf Mon Sep 17 00:00:00 2001 From: Victor Doval <10011941+vicdoval@users.noreply.github.com> Date: Wed, 2 Jan 2019 14:56:44 +0100 Subject: [PATCH 28/60] Removed underscores from Outputs' names --- docs/nodes/analyzers/deformation.rst | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/docs/nodes/analyzers/deformation.rst b/docs/nodes/analyzers/deformation.rst index d1f4079bc..fdad47f6d 100644 --- a/docs/nodes/analyzers/deformation.rst +++ b/docs/nodes/analyzers/deformation.rst @@ -1,5 +1,5 @@ Deformation -===== +=========== Functionality ------------- @@ -8,7 +8,7 @@ Deformation node is one of the analyzer type. It is used to get the deformation Inputs ----------- +------ All inputs need to proceed from an external node @@ -29,11 +29,11 @@ In the N-Panel you can use the toggle **Output NumPy** to get NumPy arrays (make Outputs ------- -- **Edges_Def** :the variation of the length of the edges. -- **Pols_Def** : the variation of the areas of each polygon. +- **Edges Def** :the variation of the length of the edges. +- **Pols Def** : the variation of the areas of each polygon. -- **Vert_Pol_Def**: Each polygon will distribute its areal variation to its vertices. Each vertex will get the sum of the deformations related to itself. -- **Vert_Edge_Def**: Each edge will distribute its elongation to its vertices. Each vertex will get the sum of elongations related to itself. +- **Vert Pol Def**: Each polygon will distribute its areal variation to its vertices. Each vertex will get the sum of the deformations related to itself. +- **Vert Edge Def**: Each edge will distribute its elongation to its vertices. Each vertex will get the sum of elongations related to itself. Examples of usage @@ -52,4 +52,3 @@ You can compare many different states at the same time. .. image:: https://user-images.githubusercontent.com/10011941/50576199-d526d880-0e0c-11e9-89cf-12cd8462da41.png :alt: DefromationNode3.PNG - -- GitLab From 4a6e77b2060dc46ae0b91eb3374896ce3f8f8bc5 Mon Sep 17 00:00:00 2001 From: DolphinDream Date: Sat, 5 Jan 2019 22:43:56 -0500 Subject: [PATCH 29/60] Add Quadrance to Quaternion Math node, fix bug in QIN node and add normalize to QOUT --- docs/nodes/quaternion/quaternion_math.rst | 11 +++++++++++ nodes/quaternion/quaternion_in.py | 2 +- nodes/quaternion/quaternion_math.py | 9 ++++++--- nodes/quaternion/quaternion_out.py | 2 ++ 4 files changed, 20 insertions(+), 4 deletions(-) diff --git a/docs/nodes/quaternion/quaternion_math.rst b/docs/nodes/quaternion/quaternion_math.rst index b00c1c5a7..392c11923 100644 --- a/docs/nodes/quaternion/quaternion_math.rst +++ b/docs/nodes/quaternion/quaternion_math.rst @@ -20,6 +20,7 @@ The available arithmetic operations and their corresponding inputs/outputs are: | INVERT | Q | Q | Invert a quaternion | | NORMALIZE | Q | Q | Normalize a quaternion | | SCALE | QS | Q | Scale a quaternion by given factor | +| QUADRANCE | Q | S | Quadrance of a quaternion | | MAGNITUDE | Q | S | Magnitude of a quaternion | +============+========+========+=====================================+ @@ -144,12 +145,22 @@ S = (s if sw else 1, s if sx else 1, s if sy else 1, s if sz else 1) scale(q, S) = (w * Sw, x * Sx, y * Sy, z * Sz) +* QUADRANCE : the quadreance of a quaternion + +q = (w, x, y, z) + +Quadrance(q) = w * w + x * x + y * y + z * z + +Note: essentially this is the dot product of the quaternion with itself, and also equal to square of the magnitude. + * MAGNITUDE : the magnitude of a quaternion q = (w, x, y, z) Magnitude(q) = sqrt(w * w + x * x + y * y + z * z) +Note: this is essentially the square root of the quadrance (the length of the quaternion). + Output ====== diff --git a/nodes/quaternion/quaternion_in.py b/nodes/quaternion/quaternion_in.py index f80b15db9..c2d848def 100644 --- a/nodes/quaternion/quaternion_in.py +++ b/nodes/quaternion/quaternion_in.py @@ -187,7 +187,7 @@ class SvQuaternionInNode(bpy.types.Node, SverchCustomTreeNode): quaternionList.append(q) elif self.mode == "AXISANGLE": - I = [inputs[n].sv_get()[0] for n in {"Axis", "Angle"}] + I = [inputs[n].sv_get()[0] for n in ["Axis", "Angle"]] params = match_long_repeat(I) au = angleConversion[self.angleUnits] for axis, angle in zip(*params): diff --git a/nodes/quaternion/quaternion_math.py b/nodes/quaternion/quaternion_math.py index 84f0ff3dd..238889eea 100644 --- a/nodes/quaternion/quaternion_math.py +++ b/nodes/quaternion/quaternion_math.py @@ -48,7 +48,8 @@ operations = { # one quaternion + scalar => quaternion (QS => Q) "SCALE": (40, "QS", "Q", "Scale a quaternion by given factor"), # one quaternion => scalar value (Q => S) - "MAGNITUDE": (50, "Q", "S", "Magnitude of a quaternion"), + "QUADRANCE": (50, "Q", "S", "Quadrance of a quaternion"), + "MAGNITUDE": (51, "Q", "S", "Magnitude of a quaternion"), } operationItems = [(k, k.title(), s[3], "", s[0]) for k, s in sorted(operations.items(), key=lambda k: k[1][0])] @@ -211,6 +212,8 @@ class SvQuaternionMathNode(bpy.types.Node, SverchCustomTreeNode): return lambda q: q.normalized() elif self.operation == "SCALE": return lambda q, s: Quaternion([q[i] * s[i] for i in range(4)]) + elif self.operation == "QUADRANCE": + return lambda q: q.dot(q) elif self.operation == "MAGNITUDE": return lambda q: q.magnitude @@ -249,11 +252,11 @@ class SvQuaternionMathNode(bpy.types.Node, SverchCustomTreeNode): operation = self.get_operation() - if self.operation in NQ_operations: # multiple input operations + if self.operation in NQ_operations: parameters = match_long_repeat(I) quaternionList = [operation(params) for params in zip(*parameters)] - elif self.operation in QQ_operations: # multiple input operations + elif self.operation in QQ_operations: parameters = match_long_repeat(I) quaternionList = [operation(*params) for params in zip(*parameters)] diff --git a/nodes/quaternion/quaternion_out.py b/nodes/quaternion/quaternion_out.py index dcf7d6377..4a3eed8fb 100644 --- a/nodes/quaternion/quaternion_out.py +++ b/nodes/quaternion/quaternion_out.py @@ -138,6 +138,8 @@ class SvQuaternionOutNode(bpy.types.Node, SverchCustomTreeNode): quaternionList = [Quaternion(q) for q in input_Q] if self.mode == "WXYZ": + if self.normalize: + quaternionList = [q.normalized() for q in quaternionList] for i, name in enumerate("WXYZ"): if outputs[name].is_linked: outputs[name].sv_set([[q[i] for q in quaternionList]]) -- GitLab From 7414ba61cb8d658ccda460e73737a0c55970e5b9 Mon Sep 17 00:00:00 2001 From: DolphinDream Date: Sun, 6 Jan 2019 10:56:18 -0500 Subject: [PATCH 30/60] Add Scalar-Vector mode to quaternion IN/OUT nodes Scalar-Vector is the same as WXYZ except that the input/output is provided as scalar W + vector (X,Y,Z). This is to help with the work-flow when quaternions are regarded as a (s,v) pair. note: The Scalar-Vector is different thant Axis-Angle mode as for this mode the quaternion is Q = cos(a/2) + sin(a/2) * vector(x,y,z) --- docs/nodes/quaternion/quaternion_in.rst | 57 +++++++++++++----------- docs/nodes/quaternion/quaternion_out.rst | 55 ++++++++++++----------- nodes/quaternion/quaternion_in.py | 31 ++++++++++--- nodes/quaternion/quaternion_math.py | 2 +- nodes/quaternion/quaternion_out.py | 28 +++++++++--- 5 files changed, 111 insertions(+), 62 deletions(-) diff --git a/docs/nodes/quaternion/quaternion_in.rst b/docs/nodes/quaternion/quaternion_in.rst index e9c9be730..6711f6887 100644 --- a/docs/nodes/quaternion/quaternion_in.rst +++ b/docs/nodes/quaternion/quaternion_in.rst @@ -7,28 +7,31 @@ Quaternion In node constructs quaternions based on various input components prov Modes ===== -The available **Modes** are: WXYZ, EULER, AXIS-ANGLE & MATRIX. - -+============+================================================================+ -| Mode | Description | -+------------+----------------------------------------------------------------+ -| WXYZ | Converts W, X, Y, Z components into a quaternion. [1] | -+------------+----------------------------------------------------------------+ -| EULER | Converts X, Y, Z Euler angles and an order of rotation | -| | into a quaternion. [2,3] | -+------------+----------------------------------------------------------------+ -| AXIS-ANGLE | Converts an Axis & an Angle of rotation into a quaternion. [2] | -+------------+----------------------------------------------------------------+ -| MATRIX | Converts an orthogonal 4x4 rotation matrix into a quaternion. | -+============+================================================================+ +The available **Modes** are: WXYZ, SCALAR-VECTOR, EULER, AXIS-ANGLE & MATRIX. + ++===============+================================================================+ +| Mode | Description | ++---------------+----------------------------------------------------------------+ +| WXYZ | Converts W, X, Y, Z components into a quaternion. [1] | ++---------------+----------------------------------------------------------------+ +| SCALAR-VECTOR | Converts Scalar & Vector components into a quaternion. [1] | ++---------------+----------------------------------------------------------------+ +| EULER | Converts X, Y, Z Euler angles and an order of rotation | +| | into a quaternion. [2,3] | ++---------------+----------------------------------------------------------------+ +| AXIS-ANGLE | Converts an Axis & an Angle of rotation into a quaternion. [2] | ++---------------+----------------------------------------------------------------+ +| MATRIX | Converts an orthogonal 4x4 rotation matrix into a quaternion. | ++===============+================================================================+ Notes: -[1] : For WXYZ the node provides a "Normalize" option to generate a normalized quaternion. +[1] : For WXYZ and SCALAR-VECTOR modes the node provides a "Normalize" option to generate a normalized quaternion. All the other modes automatically generate a normalized quaternion. [2] : For EULER and AXIS-ANGLE modes (which take angle input) the node provides an angle unit conversion to let the angle values be converted to Radians, Degrees or Unities (0-1 range). [3] : For EULER mode the node provides the option to select the Euler rotation order: "XYZ", "XZY", "YXZ", "YZX", "ZXY" or "ZYX". +The modes WXYZ and SCALAR-VECTOR are the same except the WXYZ provides 4 floats (W, X, Y and Z) to generate the quaternion, while SCALAR-VECTOR provides a scalar (W) and a vector (X, Y, Z). Inputs ====== @@ -40,17 +43,19 @@ extend the length of the connected input lists to match the longest one before c Based on the selected **Mode** the node makes available the corresponding input sockets: -+============+================================+ -| Mode | Input Sockets (types) | -+------------+--------------------------------+ -| WXYZ | W, X, Y, Z (floats) | -+------------+--------------------------------+ -| EULER | X, Y, Z angles (floats) | -+------------+--------------------------------+ -| AXIS-ANGLE | Axis (Vector) & Angle (float) | -+------------+--------------------------------+ -| MATRIX | Matrix (4x4 matrix) | -+============+================================+ ++===============+==================================+ +| Mode | Input Sockets (types) | ++---------------+----------------------------------+ +| WXYZ | W, X, Y, Z (floats) | ++---------------+----------------------------------+ +| SCALAR-VECTOR | Scalar (float) & Vector (vector) | ++---------------+----------------------------------+ +| EULER | X, Y, Z angles (floats) | ++---------------+----------------------------------+ +| AXIS-ANGLE | Axis (vector) & Angle (float) | ++---------------+----------------------------------+ +| MATRIX | Matrix (4x4 matrix) | ++===============+==================================+ Outputs diff --git a/docs/nodes/quaternion/quaternion_out.rst b/docs/nodes/quaternion/quaternion_out.rst index 779f9aa0c..8872e5a77 100644 --- a/docs/nodes/quaternion/quaternion_out.rst +++ b/docs/nodes/quaternion/quaternion_out.rst @@ -8,22 +8,23 @@ Modes The available **Modes** are: WXYZ, EULER, AXIS-ANGLE & MATRIX. -+============+==============================================================+ -| Mode | Description | -+------------+--------------------------------------------------------------+ -| WXYZ | Converts a quaternion into its W, X, Y, Z components. [1] | -+------------+--------------------------------------------------------------+ -| EULER | Converts a quaternion into X, Y, Z angles corresponding | -| | to the Euler rotation given an Euler rotation order. [2,3] | -+------------+--------------------------------------------------------------+ -| AXIS-ANGLE | Converts a quaternion into the Axis & Angle of rotation. [2] | -+------------+--------------------------------------------------------------+ -| MATRIX | Converts a quaternion into an orthogonal 4x4 rotation matrix.| -+============+==============================================================+ ++===============+================================================================+ +| Mode | Description | ++---------------+----------------------------------------------------------------+ +| WXYZ | Converts a quaternion into its W, X, Y, Z components. [1] | ++---------------+----------------------------------------------------------------+ +| SCALAR-VECTOR | Converts a quaternion into its Scalar & Vector components. [1] | ++---------------+----------------------------------------------------------------+ +| EULER | Converts a quaternion into X, Y, Z angles corresponding | +| | to the Euler rotation given an Euler rotation order. [2,3] | ++---------------+----------------------------------------------------------------+ +| AXIS-ANGLE | Converts a quaternion into the Axis & Angle of rotation. [2] | ++---------------+----------------------------------------------------------------+ +| MATRIX | Converts a quaternion into an orthogonal 4x4 rotation matrix...| ++===============+================================================================+ Notes: -[1] : For WXYZ the node provides a "Normalize" option to let the input quaternion -be normalized before outputting its components. +[1] : For WXYZ and SCALAR-VECTOR modes the node provides a "Normalize" option to normalize the input quaternion before outputting its components. All the other modes automatically normalize the quaternion. [2] : For EULER and AXIS-ANGLE modes, which output angles, the node provides an angle unit conversion to let the angle output values be converted to Radians, Degrees or Unities (0-1 range). @@ -43,17 +44,21 @@ Outputs Based on the selected **Mode** the node makes available the corresponding output sockets: -+============+================================+ -| Mode | Output Sockets (types) | -+------------+--------------------------------+ -| WXYZ | W, X, Y, Z (floats) | -+------------+--------------------------------+ -| EULER | X, Y, Z angles (floats) | -+------------+--------------------------------+ -| AXIS-ANGLE | Axis (Vector) & Angle (float) | -+------------+--------------------------------+ -| MATRIX | Matrix (4x4 matrix) | -+============+================================+ ++===============+==================================+ +| Mode | Output Sockets (types) | ++---------------+----------------------------------+ +| WXYZ | W, X, Y, Z (floats) | ++---------------+----------------------------------+ +| SCALAR-VECTOR | Scalar (float) & Vector (vector) | ++---------------+----------------------------------+ +| EULER | X, Y, Z angles (floats) | ++---------------+----------------------------------+ +| AXIS-ANGLE | Axis (vector) & Angle (float) | ++---------------+----------------------------------+ +| MATRIX | Matrix (4x4 matrix) | ++===============+==================================+ + +The modes WXYZ and SCALAR-VECTOR are the same except the WXYZ mode outputs the components as 4 floats (W, X, Y and Z), while the SCALAR-VECTOR mode outputs the components as a scalar (W) and a vector (XYZ). The node only generates the conversion when the output sockets are connected. diff --git a/nodes/quaternion/quaternion_in.py b/nodes/quaternion/quaternion_in.py index c2d848def..df8f47d1f 100644 --- a/nodes/quaternion/quaternion_in.py +++ b/nodes/quaternion/quaternion_in.py @@ -26,9 +26,10 @@ from math import pi modeItems = [ ("WXYZ", "WXYZ", "Convert components into quaternion", 0), - ("EULER", "Euler Angles", "Convert Euler angles into quaternion", 1), - ("AXISANGLE", "Axis Angle", "Convert Axis & Angle into quaternion", 2), - ("MATRIX", "Matrix", "Convert Rotation Matrix into quaternion", 3), + ("SCALARVECTOR", "Scalar Vector", "Convert Scalar & Vector into quaternion", 1), + ("EULER", "Euler Angles", "Convert Euler angles into quaternion", 2), + ("AXISANGLE", "Axis Angle", "Convert Axis & Angle into quaternion", 3), + ("MATRIX", "Matrix", "Convert Rotation Matrix into quaternion", 4), ] eulerOrderItems = [ @@ -52,6 +53,7 @@ idMat = [[tuple(v) for v in Matrix()]] # identity matrix input_sockets = { "WXYZ": ["W", "X", "Y", "Z"], + "SCALARVECTOR": ["Scalar", "Vector"], "EULER": ["Angle X", "Angle Y", "Angle Z"], "AXISANGLE": ["Angle", "Axis"], "MATRIX": ["Matrix"] @@ -81,7 +83,7 @@ class SvQuaternionInNode(bpy.types.Node, SverchCustomTreeNode): updateNode(self, context) mode = EnumProperty( - name='Mode', description='Input mode (wxyz, Euler, Axis-Angle, Matrix)', + name='Mode', description='The input component format of the quaternion', items=modeItems, default="WXYZ", update=update_mode) eulerOrder = EnumProperty( @@ -108,6 +110,14 @@ class SvQuaternionInNode(bpy.types.Node, SverchCustomTreeNode): name='Z', description='Z component', default=0.0, precision=3, update=updateNode) + scalar = FloatProperty( + name='Scalar', description='Scalar component of the quaternion', + default=0.0, update=updateNode) + + vector = FloatVectorProperty( + name='Vector', description='Vector component of the quaternion', + size=3, default=(0.0, 0.0, 0.0), subtype="XYZ", update=updateNode) + angle_x = FloatProperty( name='Angle X', description='Rotation angle about X axis', default=0.0, precision=3, update=updateNode) @@ -137,6 +147,8 @@ class SvQuaternionInNode(bpy.types.Node, SverchCustomTreeNode): self.inputs.new('StringsSocket', "X").prop_name = 'component_x' self.inputs.new('StringsSocket', "Y").prop_name = 'component_y' self.inputs.new('StringsSocket', "Z").prop_name = 'component_z' + self.inputs.new('StringsSocket', "Scalar").prop_name = 'scalar' + self.inputs.new('VerticesSocket', "Vector").prop_name = "vector" self.inputs.new('StringsSocket', "Angle X").prop_name = 'angle_x' self.inputs.new('StringsSocket', "Angle Y").prop_name = 'angle_y' self.inputs.new('StringsSocket', "Angle Z").prop_name = 'angle_z' @@ -155,7 +167,7 @@ class SvQuaternionInNode(bpy.types.Node, SverchCustomTreeNode): if self.mode in {"EULER", "AXISANGLE"}: row = layout.row(align=True) row.prop(self, "angleUnits", expand=True) - if self.mode == "WXYZ": + if self.mode in {"WXYZ", "SCALARVECTOR"}: layout.prop(self, "normalize", toggle=True) def process(self): @@ -175,6 +187,15 @@ class SvQuaternionInNode(bpy.types.Node, SverchCustomTreeNode): q.normalize() quaternionList.append(q) + elif self.mode == "SCALARVECTOR": + I = [inputs[n].sv_get()[0] for n in ["Scalar", "Vector"]] + params = match_long_repeat(I) + for scalar, vector in zip(*params): + q = Quaternion([scalar, *vector]) + if self.normalize: + q.normalize() + quaternionList.append(q) + elif self.mode == "EULER": I = [inputs["Angle " + n].sv_get()[0] for n in "XYZ"] params = match_long_repeat(I) diff --git a/nodes/quaternion/quaternion_math.py b/nodes/quaternion/quaternion_math.py index 238889eea..fbcfdfc6e 100644 --- a/nodes/quaternion/quaternion_math.py +++ b/nodes/quaternion/quaternion_math.py @@ -128,7 +128,7 @@ class SvQuaternionMathNode(bpy.types.Node, SverchCustomTreeNode): inputs_AZ = list(filter(lambda s: s.name in ABC, inputs)) # last A-Z socket connected ? => add an empty A-Z socket at the end - if inputs_AZ[-1].links: + if inputs_AZ and inputs_AZ[-1].links: name = ABC[len(inputs_AZ)] # pick the next letter A to Z inputs.new("SvQuaternionSocket", name) else: # last input disconnected ? => remove all but last unconnected diff --git a/nodes/quaternion/quaternion_out.py b/nodes/quaternion/quaternion_out.py index 4a3eed8fb..852835d4e 100644 --- a/nodes/quaternion/quaternion_out.py +++ b/nodes/quaternion/quaternion_out.py @@ -26,9 +26,10 @@ from math import pi modeItems = [ ("WXYZ", "WXYZ", "Convert quaternion into components", 0), - ("EULER", "Euler Angles", "Convert quaternion into Euler angles", 1), - ("AXISANGLE", "Axis Angle", "Convert quaternion into Axis & Angle", 2), - ("MATRIX", "Matrix", "Convert quaternion into Rotation Matrix", 3), + ("SCALARVECTOR", "Scalar Vector", "Convert quaternion into Scalar & Vector", 1), + ("EULER", "Euler Angles", "Convert quaternion into Euler angles", 2), + ("AXISANGLE", "Axis Angle", "Convert quaternion into Axis & Angle", 3), + ("MATRIX", "Matrix", "Convert quaternion into Rotation Matrix", 4), ] eulerOrderItems = [ @@ -50,6 +51,7 @@ angleConversion = {"RAD": 1.0, "DEG": 180.0 / pi, "UNI": 0.5 / pi} output_sockets = { "WXYZ": ["W", "X", "Y", "Z"], + "SCALARVECTOR": ["Scalar", "Vector"], "EULER": ["Angle X", "Angle Y", "Angle Z"], "AXISANGLE": ["Angle", "Axis"], "MATRIX": ["Matrix"] @@ -79,7 +81,7 @@ class SvQuaternionOutNode(bpy.types.Node, SverchCustomTreeNode): updateNode(self, context) mode = EnumProperty( - name='Mode', description='Output mode (wxyz, Euler, Axis-Angle, Matrix)', + name='Mode', description='The output component format of the quaternion', items=modeItems, default="WXYZ", update=update_mode) eulerOrder = EnumProperty( @@ -106,6 +108,9 @@ class SvQuaternionOutNode(bpy.types.Node, SverchCustomTreeNode): self.outputs.new('StringsSocket', "X") self.outputs.new('StringsSocket', "Y") self.outputs.new('StringsSocket', "Z") + # scalar-vector output + self.outputs.new('StringsSocket', "Scalar") + self.outputs.new('VerticesSocket', "Vector") # euler angle ouputs self.outputs.new('StringsSocket', "Angle X") self.outputs.new('StringsSocket', "Angle Y") @@ -126,7 +131,7 @@ class SvQuaternionOutNode(bpy.types.Node, SverchCustomTreeNode): if self.mode in {"EULER", "AXISANGLE"}: row = layout.row(align=True) row.prop(self, "angleUnits", expand=True) - if self.mode == "WXYZ": + if self.mode in {"WXYZ", "SCALARVECTOR"}: layout.prop(self, "normalize", toggle=True) def process(self): @@ -140,10 +145,23 @@ class SvQuaternionOutNode(bpy.types.Node, SverchCustomTreeNode): if self.mode == "WXYZ": if self.normalize: quaternionList = [q.normalized() for q in quaternionList] + for i, name in enumerate("WXYZ"): if outputs[name].is_linked: outputs[name].sv_set([[q[i] for q in quaternionList]]) + elif self.mode == "SCALARVECTOR": + if self.normalize: + quaternionList = [q.normalized() for q in quaternionList] + + if outputs['Scalar'].is_linked: + scalarList = [q[0] for q in quaternionList] + outputs['Scalar'].sv_set([scalarList]) + + if outputs['Vector'].is_linked: + vectorList = [tuple(q[1:]) for q in quaternionList] + outputs['Vector'].sv_set([vectorList]) + elif self.mode == "EULER": au = angleConversion[self.angleUnits] for i, name in enumerate("XYZ"): -- GitLab From 04e9a55e5e6c2bd951bc7b5b76ede713d97de1f6 Mon Sep 17 00:00:00 2001 From: Dealga McArdle Date: Tue, 8 Jan 2019 14:47:41 +0100 Subject: [PATCH 31/60] Update nodeview_rclick_menu.py --- ui/nodeview_rclick_menu.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/ui/nodeview_rclick_menu.py b/ui/nodeview_rclick_menu.py index 6ca2cb026..682c28fc0 100644 --- a/ui/nodeview_rclick_menu.py +++ b/ui/nodeview_rclick_menu.py @@ -95,7 +95,12 @@ def add_connection(tree, bl_idname_new_node, offset): elif bl_idname_new_node == 'SvStethoscopeNodeMK2': # we can't determin thru cursor location which socket was nearest the rightclick # maybe in the future.. or if someone does know :) - links.new(outputs[0], inputs[0]) + for socket in outputs: + if socket.hide: + continue + # connect_stethoscope to first visible output socket of active node + links.new(socket, inputs[0]) + break elif bl_idname_new_node == 'ViewerNode2': -- GitLab From 5caad0ae5c4dfbe0b5b2e36cd15b1c2acda3a632 Mon Sep 17 00:00:00 2001 From: Victor Doval <10011941+vicdoval@users.noreply.github.com> Date: Tue, 8 Jan 2019 23:55:14 +0100 Subject: [PATCH 32/60] Fixed typo in deformation Node Committing straight to master because i screw it :D (to fix it as fast as possible) --- index.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/index.md b/index.md index bb65f817e..ad1375526 100644 --- a/index.md +++ b/index.md @@ -65,7 +65,7 @@ SvRaycasterLiteNode SvOBJInsolationNode EvaluateImageNode - SvTensionNode + SvDeformationNode ## Transforms SvRotationNode @@ -349,4 +349,4 @@ --- SvQuaternionOutNode SvQuaternionInNode - SvQuaternionMathNode \ No newline at end of file + SvQuaternionMathNode -- GitLab From eeb194f91bc1d233f1e69434bdb36cf0ce3b054f Mon Sep 17 00:00:00 2001 From: Victor Doval <10011941+vicdoval@users.noreply.github.com> Date: Wed, 9 Jan 2019 01:57:57 +0100 Subject: [PATCH 33/60] added Length Node --- docs/nodes/analyzers/analyzers_index.rst | 1 + docs/nodes/analyzers/length.rst | 38 +++++++ index.md | 1 + nodes/analyzer/length.py | 135 +++++++++++++++++++++++ 4 files changed, 175 insertions(+) create mode 100644 docs/nodes/analyzers/length.rst create mode 100644 nodes/analyzer/length.py diff --git a/docs/nodes/analyzers/analyzers_index.rst b/docs/nodes/analyzers/analyzers_index.rst index 8f5788721..328c2b184 100644 --- a/docs/nodes/analyzers/analyzers_index.rst +++ b/docs/nodes/analyzers/analyzers_index.rst @@ -14,6 +14,7 @@ Analyzers image_components kd_tree kd_tree_edges_mk2 + length mesh_filter mesh_select select_similar diff --git a/docs/nodes/analyzers/length.rst b/docs/nodes/analyzers/length.rst new file mode 100644 index 000000000..488a0d2c9 --- /dev/null +++ b/docs/nodes/analyzers/length.rst @@ -0,0 +1,38 @@ +Length +===== + +Functionality +------------- + +Length node is one of the analyzer type. It is used to get the length of any path, no matter the number of its vertices or its world position. + + +Inputs / Parameters +------------------- + + ++------------------+---------------+-------------+--------------------------------------------------+ +| Param | Type | Default | Description | ++==================+===============+=============+==================================================+ +| **Vertices** | Vertices | None | Vertices of the edges / path | ++------------------+---------------+-------------+--------------------------------------------------+ +| **Edges** | Strings | None | Edges referenced to vertices | ++------------------+---------------+-------------+--------------------------------------------------+ +| **by Edges** | Boolean | True | output individual edges length or the sum of all | ++------------------+---------------+-------------+--------------------------------------------------+ + +In the N-Panel you can use the toggle **Output NumPy** to get NumPy arrays in stead of regular lists (makes the node faster). + +Outputs +------- + +**Length** will be calculated if **Vertices** inputs are linked, if no **Edges** are supplied the node will use the sequence order to calculate de distances. + + +Example of usage +---------------- + +.. image:: https://cloud.githubusercontent.com/assets/5990821/4188452/8f9cbf66-3772-11e4-8735-34462b7da54b.png + :alt: LengthDemo1.PNG + +Measuring a Bender curve with the default vertices, with a higher interpolation and by edges \ No newline at end of file diff --git a/index.md b/index.md index ad1375526..b3d06e835 100644 --- a/index.md +++ b/index.md @@ -49,6 +49,7 @@ SvVolumeNode AreaNode DistancePPNode + SvLengthNode CentersPolsNodeMK2 CentersPolsNodeMK3 GetNormalsNode diff --git a/nodes/analyzer/length.py b/nodes/analyzer/length.py new file mode 100644 index 000000000..2e6a5387a --- /dev/null +++ b/nodes/analyzer/length.py @@ -0,0 +1,135 @@ +# ##### 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 bpy +from bpy.props import BoolProperty +from sverchok.node_tree import SverchCustomTreeNode +from sverchok.data_structure import updateNode, match_long_repeat +import numpy as np + + +def edges_aux(vertices): + '''create auxiliary edges array ''' + v_len = [len(v) for v in vertices] + v_len_max = max(v_len) + np_in = np.arange(v_len_max - 1) + np_edges = np.array([np_in, np_in + 1]).T + + return [np_edges] + + +def edges_length(meshes, gates, result): + '''calculate edges length ''' + + for vertices, edges in zip(*meshes): + np_verts = np.array(vertices) + print(np_verts.shape) + if type(edges[0]) in (list, tuple): + np_edges = np.array(edges) + else: + np_edges = edges[:len(vertices)-1, :] + + vect = np_verts[np_edges[:, 0], :] - np_verts[np_edges[:, 1], :] + length = np.linalg.norm(vect, axis=1) + if not gates[1]: + length = np.sum(length)[np.newaxis] + + result.append(length if gates[0] else length.tolist()) + + return result + + +class SvLengthNode(bpy.types.Node, SverchCustomTreeNode): + ''' + Triggers: Path / Edges length + Tooltip: Deformation between to states, edge elong a area variation + ''' + bl_idname = 'SvLengthNode' + bl_label = 'Length' + bl_icon = 'MOD_SIMPLEDEFORM' + + output_numpy = BoolProperty( + name='Output NumPy', description='output NumPy arrays', + default=False, update=updateNode) + + sum_lengths = BoolProperty( + name='by Edge', description='individual lengths or the sum of them', + default=True, update=updateNode) + + def draw_buttons(self, context, layout): + '''draw buttons on the Node''' + layout.prop(self, "sum_lengths", toggle=False) + + def draw_buttons_ext(self, context, layout): + '''draw buttons on the N-panel''' + self.draw_buttons(context, layout) + layout.prop(self, "output_numpy", toggle=False) + + def sv_init(self, context): + '''create sockets''' + sinw = self.inputs.new + sonw = self.outputs.new + sinw('VerticesSocket', "Verts") + sinw('StringsSocket', "Edges") + + sonw('StringsSocket', "Length") + + def get_data(self): + '''get all data from sockets''' + si = self.inputs + vertices = si['Verts'].sv_get(default=[[]]) + edges_in = si['Edges'].sv_get(default=[[]]) + if len(edges_in[0]) < 1: + edges_in = edges_aux(vertices) + + return match_long_repeat([vertices, edges_in]) + + def ready(self): + '''check if there are the needed links''' + si = self.inputs + so = self.outputs + ready = so[0].is_linked and si[0].is_linked + + return ready + + def process(self): + '''main node function called every update''' + so = self.outputs + if not self.ready(): + return + + result = [] + gates = [] + gates.append(self.output_numpy) + gates.append(self.sum_lengths) + meshes = self.get_data() + + result = edges_length(meshes, gates, result) + + so['Length'].sv_set(result) + + +def register(): + '''register class in Blender''' + bpy.utils.register_class(SvLengthNode) + + +def unregister(): + '''unregister class in Blender''' + bpy.utils.unregister_class(SvLengthNode) -- GitLab From 85891992181226e5e5cb06b6f68c81436353293e Mon Sep 17 00:00:00 2001 From: Victor Doval <10011941+vicdoval@users.noreply.github.com> Date: Wed, 9 Jan 2019 02:17:15 +0100 Subject: [PATCH 34/60] added images to length node docs --- docs/nodes/analyzers/length.rst | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/docs/nodes/analyzers/length.rst b/docs/nodes/analyzers/length.rst index 488a0d2c9..d1b6c336e 100644 --- a/docs/nodes/analyzers/length.rst +++ b/docs/nodes/analyzers/length.rst @@ -26,13 +26,18 @@ In the N-Panel you can use the toggle **Output NumPy** to get NumPy arrays in st Outputs ------- -**Length** will be calculated if **Vertices** inputs are linked, if no **Edges** are supplied the node will use the sequence order to calculate de distances. +**Length**: it will be calculated if **Vertices** inputs is linked. If no **Edges** are supplied the node will use the sequence order to calculate de distances. Example of usage ---------------- -.. image:: https://cloud.githubusercontent.com/assets/5990821/4188452/8f9cbf66-3772-11e4-8735-34462b7da54b.png +.. image:: https://user-images.githubusercontent.com/10011941/50869102-f52d3d80-13b2-11e9-8316-01106c61e8d7.png :alt: LengthDemo1.PNG -Measuring a Bender curve with the default vertices, with a higher interpolation and by edges \ No newline at end of file +Measuring a Bender curve with the default vertices, with a higher interpolation and by edges + +.. image:: https://user-images.githubusercontent.com/10011941/50869357-f317ae80-13b3-11e9-88a0-05888e9bc2c6.png + :alt: LengthDemo2.PNG + +Using the length node to know the linear distance needed to build a 3 meter radius geodesic dome \ No newline at end of file -- GitLab From 745697249337bdd638f4af30fb53509c712f9692 Mon Sep 17 00:00:00 2001 From: Victor Doval <10011941+vicdoval@users.noreply.github.com> Date: Wed, 9 Jan 2019 02:25:38 +0100 Subject: [PATCH 35/60] Corrected typo from length docs --- docs/nodes/analyzers/length.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/nodes/analyzers/length.rst b/docs/nodes/analyzers/length.rst index d1b6c336e..4039b602c 100644 --- a/docs/nodes/analyzers/length.rst +++ b/docs/nodes/analyzers/length.rst @@ -26,7 +26,7 @@ In the N-Panel you can use the toggle **Output NumPy** to get NumPy arrays in st Outputs ------- -**Length**: it will be calculated if **Vertices** inputs is linked. If no **Edges** are supplied the node will use the sequence order to calculate de distances. +**Length**: it will be calculated if **Vertices** input is linked. If no **Edges** are supplied the node will use the sequence order to calculate de distances. Example of usage @@ -40,4 +40,4 @@ Measuring a Bender curve with the default vertices, with a higher interpolation .. image:: https://user-images.githubusercontent.com/10011941/50869357-f317ae80-13b3-11e9-88a0-05888e9bc2c6.png :alt: LengthDemo2.PNG -Using the length node to know the linear distance needed to build a 3 meter radius geodesic dome \ No newline at end of file +Using the length node to know the linear distance needed to build a 3 meter radius geodesic dome -- GitLab From aa05b55440ab671343b08e851543b644d178f98e Mon Sep 17 00:00:00 2001 From: Victor Doval <10011941+vicdoval@users.noreply.github.com> Date: Wed, 9 Jan 2019 12:40:32 +0100 Subject: [PATCH 36/60] minor refactoring on Length node removed ready function and integrate its functionality to the process function --- nodes/analyzer/length.py | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/nodes/analyzer/length.py b/nodes/analyzer/length.py index 2e6a5387a..19b670098 100644 --- a/nodes/analyzer/length.py +++ b/nodes/analyzer/length.py @@ -100,18 +100,11 @@ class SvLengthNode(bpy.types.Node, SverchCustomTreeNode): return match_long_repeat([vertices, edges_in]) - def ready(self): - '''check if there are the needed links''' - si = self.inputs - so = self.outputs - ready = so[0].is_linked and si[0].is_linked - - return ready - def process(self): '''main node function called every update''' + si = self.inputs so = self.outputs - if not self.ready(): + if not (so[0].is_linked and si[0].is_linked): return result = [] -- GitLab From 3e752737ccbbb8af861ef9c6a47745a97dc59af0 Mon Sep 17 00:00:00 2001 From: Victor Doval <10011941+vicdoval@users.noreply.github.com> Date: Wed, 16 Jan 2019 22:21:09 +0100 Subject: [PATCH 37/60] renamed to Path Length --- docs/nodes/analyzers/analyzers_index.rst | 2 +- .../analyzers/{length.rst => path_length.rst} | 23 +++++++----- index.md | 2 +- nodes/analyzer/{length.py => path_length.py} | 37 ++++++++++--------- 4 files changed, 36 insertions(+), 28 deletions(-) rename docs/nodes/analyzers/{length.rst => path_length.rst} (60%) rename nodes/analyzer/{length.py => path_length.py} (81%) diff --git a/docs/nodes/analyzers/analyzers_index.rst b/docs/nodes/analyzers/analyzers_index.rst index 328c2b184..d04b26c3d 100644 --- a/docs/nodes/analyzers/analyzers_index.rst +++ b/docs/nodes/analyzers/analyzers_index.rst @@ -14,7 +14,6 @@ Analyzers image_components kd_tree kd_tree_edges_mk2 - length mesh_filter mesh_select select_similar @@ -23,6 +22,7 @@ Analyzers normals bvh_overlap_polys object_insolation + path_length points_inside_mesh polygons_centers polygons_centers_mk3 diff --git a/docs/nodes/analyzers/length.rst b/docs/nodes/analyzers/path_length.rst similarity index 60% rename from docs/nodes/analyzers/length.rst rename to docs/nodes/analyzers/path_length.rst index 4039b602c..4fa2d7271 100644 --- a/docs/nodes/analyzers/length.rst +++ b/docs/nodes/analyzers/path_length.rst @@ -1,10 +1,10 @@ -Length -===== +Path Length +=========== Functionality ------------- -Length node is one of the analyzer type. It is used to get the length of any path, no matter the number of its vertices or its world position. +Path Length node is one of the analyzer type. It is used to get the length of any path, no matter the number of its vertices or its world position. Inputs / Parameters @@ -18,7 +18,7 @@ Inputs / Parameters +------------------+---------------+-------------+--------------------------------------------------+ | **Edges** | Strings | None | Edges referenced to vertices | +------------------+---------------+-------------+--------------------------------------------------+ -| **by Edges** | Boolean | True | output individual edges length or the sum of all | +| **Segment** | Boolean | True | output the segments lengths or its sum | +------------------+---------------+-------------+--------------------------------------------------+ In the N-Panel you can use the toggle **Output NumPy** to get NumPy arrays in stead of regular lists (makes the node faster). @@ -32,12 +32,17 @@ Outputs Example of usage ---------------- -.. image:: https://user-images.githubusercontent.com/10011941/50869102-f52d3d80-13b2-11e9-8316-01106c61e8d7.png - :alt: LengthDemo1.PNG +.. image:: https://user-images.githubusercontent.com/10011941/51251936-c4449e00-199a-11e9-89a7-557cc7e93731.png + :alt: PathLengthDemo1.PNG Measuring a Bender curve with the default vertices, with a higher interpolation and by edges -.. image:: https://user-images.githubusercontent.com/10011941/50869357-f317ae80-13b3-11e9-88a0-05888e9bc2c6.png - :alt: LengthDemo2.PNG +.. image:: https://user-images.githubusercontent.com/10011941/51251933-c4449e00-199a-11e9-99b8-fa53c8586484.png + :alt: PathLengthDemo2.PNG -Using the length node to know the linear distance needed to build a 3 meter radius geodesic dome +Using the node to know the linear distance needed to build a 3 meter radius geodesic dome + +.. image:: https://user-images.githubusercontent.com/10011941/51251931-c4449e00-199a-11e9-9e75-69ead34fad64.png + :alt: PathLengthDemo2.PNG + +Using the *Path Length* node to place circles of one meter of diameter along a given curve \ No newline at end of file diff --git a/index.md b/index.md index b3d06e835..c1d9b31f9 100644 --- a/index.md +++ b/index.md @@ -49,7 +49,7 @@ SvVolumeNode AreaNode DistancePPNode - SvLengthNode + SvPathLengthNode CentersPolsNodeMK2 CentersPolsNodeMK3 GetNormalsNode diff --git a/nodes/analyzer/length.py b/nodes/analyzer/path_length.py similarity index 81% rename from nodes/analyzer/length.py rename to nodes/analyzer/path_length.py index 19b670098..e225896a5 100644 --- a/nodes/analyzer/length.py +++ b/nodes/analyzer/path_length.py @@ -20,7 +20,7 @@ import bpy from bpy.props import BoolProperty from sverchok.node_tree import SverchCustomTreeNode -from sverchok.data_structure import updateNode, match_long_repeat +from sverchok.data_structure import updateNode, match_long_repeat, get_edge_list import numpy as np @@ -39,7 +39,6 @@ def edges_length(meshes, gates, result): for vertices, edges in zip(*meshes): np_verts = np.array(vertices) - print(np_verts.shape) if type(edges[0]) in (list, tuple): np_edges = np.array(edges) else: @@ -55,26 +54,26 @@ def edges_length(meshes, gates, result): return result -class SvLengthNode(bpy.types.Node, SverchCustomTreeNode): +class SvPathLengthNode(bpy.types.Node, SverchCustomTreeNode): ''' Triggers: Path / Edges length - Tooltip: Deformation between to states, edge elong a area variation + Tooltip: Masseuses the length of a path or the length of it's segments ''' - bl_idname = 'SvLengthNode' - bl_label = 'Length' + bl_idname = 'SvPathLengthNode' + bl_label = 'Path Length' bl_icon = 'MOD_SIMPLEDEFORM' output_numpy = BoolProperty( name='Output NumPy', description='output NumPy arrays', default=False, update=updateNode) - sum_lengths = BoolProperty( - name='by Edge', description='individual lengths or the sum of them', + segment = BoolProperty( + name='Segment', description='Get segments length or the sum of them', default=True, update=updateNode) def draw_buttons(self, context, layout): '''draw buttons on the Node''' - layout.prop(self, "sum_lengths", toggle=False) + layout.prop(self, "segment", toggle=False) def draw_buttons_ext(self, context, layout): '''draw buttons on the N-panel''' @@ -85,7 +84,7 @@ class SvLengthNode(bpy.types.Node, SverchCustomTreeNode): '''create sockets''' sinw = self.inputs.new sonw = self.outputs.new - sinw('VerticesSocket', "Verts") + sinw('VerticesSocket', "Vertices") sinw('StringsSocket', "Edges") sonw('StringsSocket', "Length") @@ -93,24 +92,28 @@ class SvLengthNode(bpy.types.Node, SverchCustomTreeNode): def get_data(self): '''get all data from sockets''' si = self.inputs - vertices = si['Verts'].sv_get(default=[[]]) - edges_in = si['Edges'].sv_get(default=[[]]) - if len(edges_in[0]) < 1: + + vertices = si['Vertices'].sv_get() + + if si['Edges'].is_linked: + edges_in = si['Edges'].sv_get() + else: edges_in = edges_aux(vertices) return match_long_repeat([vertices, edges_in]) + def process(self): '''main node function called every update''' si = self.inputs so = self.outputs - if not (so[0].is_linked and si[0].is_linked): + if not (so['Length'].is_linked): return result = [] gates = [] gates.append(self.output_numpy) - gates.append(self.sum_lengths) + gates.append(self.segment) meshes = self.get_data() result = edges_length(meshes, gates, result) @@ -120,9 +123,9 @@ class SvLengthNode(bpy.types.Node, SverchCustomTreeNode): def register(): '''register class in Blender''' - bpy.utils.register_class(SvLengthNode) + bpy.utils.register_class(SvPathLengthNode) def unregister(): '''unregister class in Blender''' - bpy.utils.unregister_class(SvLengthNode) + bpy.utils.unregister_class(SvPathLengthNode) -- GitLab From 7e17d5a91d5d61ec5fbe1524fa125167871bd884 Mon Sep 17 00:00:00 2001 From: Victor Doval <10011941+vicdoval@users.noreply.github.com> Date: Wed, 16 Jan 2019 22:38:03 +0100 Subject: [PATCH 38/60] update gp macro with Path Length node --- utils/sv_default_macros.py | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/utils/sv_default_macros.py b/utils/sv_default_macros.py index f029230b2..1b3f2c907 100644 --- a/utils/sv_default_macros.py +++ b/utils/sv_default_macros.py @@ -133,13 +133,13 @@ class DefaultMacros(): elif term == 'gp +': needed_nodes = [ ['SvGetAssetProperties', (0.00, 0.00)], - ['SvScriptNodeLite', (250, 55)], - ['SvScalarMathNodeMK2', (430, 115)], - ['Float2IntNode', (600, 50)], - ['SvGenFloatRange', (720, 90)], - ['SvInterpolationNodeMK3', (880, 40)], - ['LineConnectNodeMK2', (1060, -40)], - ['ViewerNode2', (1245, 50)], + ['SvPathLengthNode', (250, 115)], + ['SvScalarMathNodeMK2', (420, 115)], + ['Float2IntNode', (590, 115)], + ['SvGenFloatRange', (760, 115)], + ['SvInterpolationNodeMK3', (930, 115)], + ['LineConnectNodeMK2', (1100, 115)], + ['ViewerNode2', (1290, 115)], ] made_nodes = [] @@ -149,9 +149,8 @@ class DefaultMacros(): n.location = node_location[0] + x, node_location[1] + y made_nodes.append(n) - # Script node lite - snlite = made_nodes[1] - sn_loader(snlite, script_name='path_length.py') + # Path Length + made_nodes[1].segment = False # ID Selector made_nodes[0].Mode = 'grease_pencil' # the rest must be user driven @@ -160,7 +159,7 @@ class DefaultMacros(): # Scalar Math node made_nodes[2].current_op = 'MUL' made_nodes[2].y_ = 2.5 - links.new(made_nodes[1].outputs[0], made_nodes[2].inputs[0]) # snlite-> math + links.new(made_nodes[1].outputs[0], made_nodes[2].inputs[0]) # path length -> math links.new(made_nodes[2].outputs[0], made_nodes[3].inputs[0]) # math -> float # Float2Int node -- GitLab From 31f7b1186edbeea846757f6616d0fc4fe07b5067 Mon Sep 17 00:00:00 2001 From: Victor Doval <10011941+vicdoval@users.noreply.github.com> Date: Fri, 18 Jan 2019 19:54:46 +0100 Subject: [PATCH 39/60] Typos and icon --- docs/nodes/analyzers/path_length.rst | 4 ++-- nodes/analyzer/path_length.py | 4 ++-- ui/icons/sv_path_length.png | Bin 0 -> 1493 bytes 3 files changed, 4 insertions(+), 4 deletions(-) create mode 100644 ui/icons/sv_path_length.png diff --git a/docs/nodes/analyzers/path_length.rst b/docs/nodes/analyzers/path_length.rst index 4fa2d7271..b7b9ad23e 100644 --- a/docs/nodes/analyzers/path_length.rst +++ b/docs/nodes/analyzers/path_length.rst @@ -26,7 +26,7 @@ In the N-Panel you can use the toggle **Output NumPy** to get NumPy arrays in st Outputs ------- -**Length**: it will be calculated if **Vertices** input is linked. If no **Edges** are supplied the node will use the sequence order to calculate de distances. +**Length**: it will be calculated if **Vertices** input is linked. If no **Edges** are supplied the node will use the sequence order to calculate de length. Example of usage @@ -35,7 +35,7 @@ Example of usage .. image:: https://user-images.githubusercontent.com/10011941/51251936-c4449e00-199a-11e9-89a7-557cc7e93731.png :alt: PathLengthDemo1.PNG -Measuring a Bender curve with the default vertices, with a higher interpolation and by edges +Measuring a Bender curve with the default vertices, with a higher interpolation and by segments .. image:: https://user-images.githubusercontent.com/10011941/51251933-c4449e00-199a-11e9-99b8-fa53c8586484.png :alt: PathLengthDemo2.PNG diff --git a/nodes/analyzer/path_length.py b/nodes/analyzer/path_length.py index e225896a5..203e35bc4 100644 --- a/nodes/analyzer/path_length.py +++ b/nodes/analyzer/path_length.py @@ -57,11 +57,11 @@ def edges_length(meshes, gates, result): class SvPathLengthNode(bpy.types.Node, SverchCustomTreeNode): ''' Triggers: Path / Edges length - Tooltip: Masseuses the length of a path or the length of it's segments + Tooltip: Measures the length of a path or the length of its segments ''' bl_idname = 'SvPathLengthNode' bl_label = 'Path Length' - bl_icon = 'MOD_SIMPLEDEFORM' + sv_icon = 'SV_PATH_LENGTH' output_numpy = BoolProperty( name='Output NumPy', description='output NumPy arrays', diff --git a/ui/icons/sv_path_length.png b/ui/icons/sv_path_length.png new file mode 100644 index 0000000000000000000000000000000000000000..66c329ef457280de62f094b0a9188a0994ba6977 GIT binary patch literal 1493 zcmV;`1uFW9P)BJ3(W4BN6e2oB;})_tziC zmpFRxLZkX_?{?1nd(Q9seV*rcJMbSvzmGNkKs3+;bPTN=clhbL{)tk`00a=L)mjdO z{VVJii=`kiFtEF@uuxiBTBNF~N}{8q4Xf4K3Jf1={;7bJmzPU_T)K2g{Qdm}Xb0W{ z#tfB!beqlAEgrCN(IN?&GEKaE9unXtPzcN(ERiZYqkyuqGE+c`Cr5+9IAA3(CT#W- zEY1HG+lWAZC@uB4xVX63{CHxJe0}f%HUpiuk)tH#gT0b-rd@K*Iwby;O#;ZqjT@z> zr>AfC`T6-~M&x^Ps2+ek8Td$P+JqU;zd*vKx3T&M^cjub9tu~c(Rk<}p`oF~#Kh3p z*eG_pUFo`h%;|I{0B!dp;BjCP5CU8QYJnsmBP1$@1#fL9Br3-32fa7CsNVA-hj-`E zd-Fz{QmR?gG(W>IT14b)Tt%t(UMFA!KEqYb+@N`R5x05+aWAh&Y3^au_){qdx2)sZ zxih`MZr}~zx+k)`K>?+JL`6kOK|z5WJ$h71N=jtjym~u>Zr_(9RmMs%N=4NHf>wjF8q%H4@x1XN?Ex>X-Uu78Z zD)2e557)^|8%RJAAT>2L9`m@hwN)$@iv)y>mEq&Yo4YoEUBELyXg^;X@FuQKDYrdN zU@xw0f_}<>_44ux@9yqKDbI-!5fMRsLj#L<< z%%4A>q@*M)77LY?l@u2jV;Bawu6rTiG}(SuO3i9(Ya<{a08bb@b}W~#x3k{f?DqcZ znNu{MIL3vOwVbLfqqV_~uIudEw~w^6G`F>tm6aqXC*yE909FG#%#c1TBJAJ4-y?Zh zSsCZgpC|OO*gI^)$BicEoN;{z}r25K zJvKI$@bGX>pFT}_c{xs}6QBtQ0<22c37jyQ(UCJ49U0B&$QcAqm`rZ;BLH-Cbl~gj zJD`ZLurQjMngC|rZkr1n!98++2cElSw*cX|TJjfw9am(F=H(?y(*)=nNIb#eaEQ0J zx9NNIKq7;I7+hWNUa$dA;+lBQ0kUe Date: Fri, 25 Jan 2019 22:00:16 -0500 Subject: [PATCH 40/60] Add (new) MeshSwitch node The node allows to connect multiple mesh inputs (verts/edges/polys) and to select one of them to output. Useful for changing the input mesh model to be processed by a node tree. New input sockets are added as new meshes are connected to allow the next mesh (verts/edges/polys) to be connected. --- docs/nodes/modifier_change/mesh_switch.rst | 35 +++++++ index.md | 1 + nodes/modifier_change/mesh_switch.py | 103 +++++++++++++++++++++ 3 files changed, 139 insertions(+) create mode 100644 docs/nodes/modifier_change/mesh_switch.rst create mode 100644 nodes/modifier_change/mesh_switch.py diff --git a/docs/nodes/modifier_change/mesh_switch.rst b/docs/nodes/modifier_change/mesh_switch.rst new file mode 100644 index 000000000..d84260e76 --- /dev/null +++ b/docs/nodes/modifier_change/mesh_switch.rst @@ -0,0 +1,35 @@ +Mesh Switch +----------- + +This node allows to switch among an arbitrary number of mesh inputs (verts, edges, polys) connected to the node. + +Inputs +====== + +**Verts A** +Takes a Vertex list from outside nodes. + +**EdgePolys A** +Takes either an Edge or a Poly list from outside nodes. + +The node starts with one set of mesh input sockets ("Verts A" and "EdgePolys A") and will create new mesh sockets whenever the last Verts input socket is connected, thus allowing additional mesh inputs to be connected to the node. Whenever the last Verts input socket is disconnected the node will remove the all disconnected inputs at the end of the input list except the last disconnected set of inputs, thus always allowing one extra mesh to be connected to the node. + +Parameters +========== + +The **Selected** parameter accepts single input values directly from the node or from an outside node. The value is sanitized to be bounded to the range of the connected inputs (0, N-1) and it is also converted via a modulo-N function to wrap around within (0, N-1) range for values larger than N. In other words, as the **Selected** value increases the node essentially cycles through the connected meshes. + ++---------------+---------+---------+-------------------------------------------------+ +| Param | Type | Default | Description | ++===============+=========+=========+=================================================+ +| **Selected** | Int | 0 | The index of the selected mesh to output. | ++---------------+---------+---------+-------------------------------------------------+ + +Outputs +======= + +**Verts** +**EdgePolys** + +When an output socket is connected the node will output the mesh corresponding to the selected index. + diff --git a/index.md b/index.md index c1d9b31f9..566cadcc2 100644 --- a/index.md +++ b/index.md @@ -86,6 +86,7 @@ PolygonBoomNode Pols2EdgsNode SvMeshJoinNode + SvMeshSwitchNode --- SvBevelNode SvSubdivideNode diff --git a/nodes/modifier_change/mesh_switch.py b/nodes/modifier_change/mesh_switch.py new file mode 100644 index 000000000..15490cd2c --- /dev/null +++ b/nodes/modifier_change/mesh_switch.py @@ -0,0 +1,103 @@ +# ##### 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 bpy +from bpy.props import IntProperty + +from sverchok.node_tree import SverchCustomTreeNode +from sverchok.data_structure import updateNode + +ABC = tuple('ABCDEFGHIJKLMNOPQRSTUVWXYZ') + + +class SvMeshSwitchNode(bpy.types.Node, SverchCustomTreeNode): + """ + Triggers: Mesh, Switch, Select + Tooltips: Switch among multiple Verts/Edge/Poly inputs + """ + bl_idname = 'SvMeshSwitchNode' + bl_label = 'Mesh Switch' + + selected = IntProperty( + name="Selected", description="Selected Mesh", + default=0, min=0, update=updateNode) + + def update(self): + inputs = self.inputs + if inputs[-2].links: # last Verts socket linked ? => create more sockets + name = ABC[int(len(inputs) / 2)] # pick the next letter A to Z + inputs.new("VerticesSocket", "Verts " + name) + inputs.new("StringsSocket", "EdgePolys " + name) + else: # last Verts input unlinked ? => remove all but last unlinked + # get the list of Verts socket labels (linked or unlinked) + vertSockets = filter(lambda s: "Verts" in s.name, inputs) + socketLabels = [s.name[-1] for s in vertSockets] # (ABCD) + # get the labels of last unlinked Verts sockets in reverse order + unlinkedSocketLabels = [] + for label in socketLabels[::-1]: # in reverse order (DCBA) + if inputs["Verts " + label].is_linked: + break + else: + unlinkedSocketLabels.append(label) + # delete all unlinked V-EP inputs sockets (except the last ones) + for label in unlinkedSocketLabels[:-1]: + inputs.remove(inputs["Verts " + label]) + inputs.remove(inputs["EdgePolys " + label]) + + def sv_init(self, context): + self.inputs.new('StringsSocket', "Selected").prop_name = "selected" + + self.inputs.new('VerticesSocket', "Verts A") + self.inputs.new('StringsSocket', "EdgePolys A") + + self.outputs.new('VerticesSocket', "Verts") + self.outputs.new('StringsSocket', "EdgePolys") + + def process(self): + # return if no outputs are connected + outputs = self.outputs + if not any(s.is_linked for s in outputs): + return + + inputs = self.inputs + + input_n = inputs["Selected"].sv_get()[0][0] + num = max(1, len(socketLabels)) + n = max(0, int(input_n)) % num + + # get the list of labels of the linked Verts input sockets + vertSockets = filter(lambda s: "Verts" in s.name and s.is_linked, inputs) + socketLabels = [s.name[-1] for s in vertSockets] # (ABCD) + + label = socketLabels[n] # the label of the selected input + + if outputs["Verts"].is_linked: + verts = inputs["Verts " + label].sv_get() + outputs["Verts"].sv_set(verts) + + if outputs["EdgePolys"].is_linked: + edgePolys = inputs["EdgePolys " + label].sv_get(default=[]) + outputs["EdgePolys"].sv_set(edgePolys) + + +def register(): + bpy.utils.register_class(SvMeshSwitchNode) + + +def unregister(): + bpy.utils.unregister_class(SvMeshSwitchNode) -- GitLab From 9f850064a843674ec49835096e2edf9336906d9a Mon Sep 17 00:00:00 2001 From: DolphinDream Date: Fri, 25 Jan 2019 23:01:17 -0500 Subject: [PATCH 41/60] Fix bug in Mesh Switch node I accidentally rearranged code last minute before merging and caused an "variable access before defined" bug :) oops. This update fixes it. --- nodes/modifier_change/mesh_switch.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/nodes/modifier_change/mesh_switch.py b/nodes/modifier_change/mesh_switch.py index 15490cd2c..4c91d8e74 100644 --- a/nodes/modifier_change/mesh_switch.py +++ b/nodes/modifier_change/mesh_switch.py @@ -39,17 +39,17 @@ class SvMeshSwitchNode(bpy.types.Node, SverchCustomTreeNode): def update(self): inputs = self.inputs - if inputs[-2].links: # last Verts socket linked ? => create more sockets + if inputs[-2].links: # last Verts socket linked ? => create more sockets name = ABC[int(len(inputs) / 2)] # pick the next letter A to Z inputs.new("VerticesSocket", "Verts " + name) inputs.new("StringsSocket", "EdgePolys " + name) else: # last Verts input unlinked ? => remove all but last unlinked # get the list of Verts socket labels (linked or unlinked) vertSockets = filter(lambda s: "Verts" in s.name, inputs) - socketLabels = [s.name[-1] for s in vertSockets] # (ABCD) + socketLabels = [s.name[-1] for s in vertSockets] # (ABCD) # get the labels of last unlinked Verts sockets in reverse order unlinkedSocketLabels = [] - for label in socketLabels[::-1]: # in reverse order (DCBA) + for label in socketLabels[::-1]: # in reverse order (DCBA) if inputs["Verts " + label].is_linked: break else: @@ -76,15 +76,15 @@ class SvMeshSwitchNode(bpy.types.Node, SverchCustomTreeNode): inputs = self.inputs + # get the list of labels of the linked Verts input sockets + vertSockets = filter(lambda s: "Verts" in s.name and s.is_linked, inputs) + socketLabels = [s.name[-1] for s in vertSockets] # (ABCD) + input_n = inputs["Selected"].sv_get()[0][0] num = max(1, len(socketLabels)) n = max(0, int(input_n)) % num - # get the list of labels of the linked Verts input sockets - vertSockets = filter(lambda s: "Verts" in s.name and s.is_linked, inputs) - socketLabels = [s.name[-1] for s in vertSockets] # (ABCD) - - label = socketLabels[n] # the label of the selected input + label = socketLabels[n] # the label of the selected input if outputs["Verts"].is_linked: verts = inputs["Verts " + label].sv_get() -- GitLab From 3564ef5b59e5434a6541016b59a3d6b410c4e4dc Mon Sep 17 00:00:00 2001 From: Dealga McArdle Date: Fri, 15 Feb 2019 10:08:07 +0100 Subject: [PATCH 42/60] Update iterate.py --- nodes/modifier_change/iterate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nodes/modifier_change/iterate.py b/nodes/modifier_change/iterate.py index e9cd453f8..60ff88b7a 100644 --- a/nodes/modifier_change/iterate.py +++ b/nodes/modifier_change/iterate.py @@ -181,7 +181,7 @@ class SvIterateNode(bpy.types.Node, SverchCustomTreeNode): if self.outputs['Polygons'].is_linked: self.outputs['Polygons'].sv_set([result_faces]) if self.outputs['Matrices'].is_linked: - self.outputs['Matrices'].sv_set(Matrix_degenerate(result_matrices)) + self.outputs['Matrices'].sv_set(result_matrices) def register(): -- GitLab From 2b43589230e26b7a50867f8b99fd0e12b42417b5 Mon Sep 17 00:00:00 2001 From: DolphinDream Date: Sat, 23 Feb 2019 21:48:47 -0500 Subject: [PATCH 43/60] Add new InputSwitch Node This node allows switching among an arbitrary number of sets of any type of inputs connected to the node. The node is essentially a generalization of the MeshSwitch node allowing any input types (not just verts/polyEdge) along with additional features: - variable input set size - keeps prior connections when changing the set size - add separators between input sets - add distinct labels to input sets (greek/latin letters) - vectorizes the selected parameters (generates multiple output sets if selected gets an array of integers) - New SeparatorSocket was also added to the set of socket types to help with the UI socket visual separation (e.g. group multiple socket into sets). - New macro was added to allow to auto connect multiple selected nodes to the input swith node (via "> sw") --- docs/nodes/modifier_change/input_switch.rst | 72 +++++ index.md | 2 + node_tree.py | 36 ++- nodes/modifier_change/input_switch.py | 332 ++++++++++++++++++++ ui/icons/sv_input_switch.png | Bin 0 -> 2870 bytes utils/sv_default_macros.py | 85 ++++- 6 files changed, 504 insertions(+), 23 deletions(-) create mode 100644 docs/nodes/modifier_change/input_switch.rst create mode 100644 nodes/modifier_change/input_switch.py create mode 100644 ui/icons/sv_input_switch.png diff --git a/docs/nodes/modifier_change/input_switch.rst b/docs/nodes/modifier_change/input_switch.rst new file mode 100644 index 000000000..7c14d16b0 --- /dev/null +++ b/docs/nodes/modifier_change/input_switch.rst @@ -0,0 +1,72 @@ +Input Switch +------------ + +This node allows switching among an arbitrary number of sets of any type of inputs connected to the node. + +Inputs +====== + +When first created, the node has a single **set** of input sockets (e.g. ["Alpha 1", "Alpha 2"]) and it will automatically create new sets of input sockets (e.g. ["Beta 1", "Beta 2"], ["Gamma 1", "Gamma 2"] etc) whenever any socket of the last set is connected, thus always allowing additional set of inputs to be connected to the node. Also, when all the sockets of the last connected set are disconnected the node will remove all but the last disconnected sets, thus always allowing one extra set of inputs to be connected to the node. + +The node takes any type of inputs from the external nodes for its sets. + +The sets are labeled based on the Greek alphabet letters from **Alpha** to **Omega** and the inputs within each set are suffixed with an increasing number starting from 1 (e.g. Alpha 1, Alpha 2, etc). + +**Alpha #** +This first set of inputs always exists, no matter whether any of its input sockets are connected to or not. + +**Beta #** +... +**Omega #** +These sets of inputs are created automatically, one at a time, only as the last set is connected. + +Note: Be advised that while it is possible to connect any arbitrary mixture of input types to any sets (e.g. Vertex & Edge to the Alpha set, and Float & Color to Beta set etc) this combination of mixed sets of inputs may likely be of little to no use. This is because the output of the Input Switch node is likely going to be connected to a node tree that expects specific types of inputs, and therefore when changing the selection of the set, the Input Switch node will output a set of values that may be incompatible with the ones expected by the nodes it connects to. There are exceptions to this rule, of course, for instance: you can connect Vertex & Edge to Alpha set and Vertex & Poly to Beta set and have the output of the Input Switch node connected to a node that expects Vertex and EdgePoly as inputs. In this case, switching the set selection from Alpha to Beta will not break the node-tree since the final node can take either VE or VP as inputs. + + +Parameters +========== + +The **Selected** parameter accepts single input values directly from the node or from an outside node. The value is sanitized to be bounded by the number N of the connected inputs (0, N-1). In fact, the value is converted via a modulo-N function to wrap around within (0, N-1) range for values larger than N. In other words, as the **Selected** value increases, the node essentially cycles through the connected sets as it picks one to be sent to the output. + ++--------------+-------+---------+----------------------------------------------+ +| Param | Type | Default | Description | ++==============+=======+=========+==============================================+ +| **Set Size** | Int | 2 | The number of inputs in a set. [1][2][3] | ++--------------+-------+---------+----------------------------------------------+ +| **Selected** | Int | 0 | The index of the selected set to output. [4] | ++--------------+-------+---------+----------------------------------------------+ + +Notes: +[1] : As the size of the set changes, the corresponding number of inputs are generated for each set with labels in increasing order (e.g. Lambda 1, Lambda 2, ... ). +[2] : When changing the set size, the existing connections of the input & output sockets to the outer nodes are being preserved, unless decreasing the set size renders some of the connected input & output sockets non-existent. +[3] : Currently the node limits the max set size to 9 since it is assumed it's unlikely the user may need sets larger than this. Not putting a limit on the size sets could end up creating very tall nodes. +[4] : The index 0 corresponds to the first set + + +Extra Parameters +================ +**Show Separators** +The sets are separated by an empty socket / line to aid with visually differentiating between sets. The Property Panel has a toggle to turn the separators ON and OFF if desired (default is ON). + +**Greek Labels** +The input sockets in each set have Greek letter names as labels. The Property Panel has a toggle to turn the labels from Greek to Latin if desired (default is Greek). The Latin labels are simply letters A to Z. + + +Outputs +======= + +**Data #** +The number of Data output sockets is the same as the number of inputs in a set (set size) and they correspond to the inputs in the selected set (e.g. for Selected = 0 : Data 1 = Alpha 1, Data 2 = Alpha 2 .. etc) + +When an output socket is connected the node will output the corresponding input in the selected set. + +As the node is vectorized, whenever the **Selected** parameter is given a list of indices each Data output is a list of set input values corresponding to the given indices. (e.g. for Selected = [0, 1, 1] : Data 1 = [Alpha 1, Beta 1, Beta 1], Data 2 = [Alpha 2, Beta 2, Beta 2]). + +Output Socket Types +=================== +The types of the output sockets are automatically updated to correspond to the types of outer nodes connected to the the input sockets of the selected set. If the **Selected** socket is connected, however, the output sockets types will default to StringSockets (green) as the output would consist in this case of a combination of sets which may or may not have the same types for the corresponding inputs. + +Macros +====== +There is a macro that allows to automatically connect multiple selected nodes (e.g. generators) to a switch node and to a ViewerDraw node. To connect the nodes, first select the desired nodes, then search and select "> sw" (short for switch). + diff --git a/index.md b/index.md index 566cadcc2..1ec1c5511 100644 --- a/index.md +++ b/index.md @@ -109,6 +109,8 @@ SvVertMaskNode SvTransformSelectNode SvSplitEdgesNode + --- + SvInputSwitchNode ## Modifier Make LineConnectNodeMK2 diff --git a/node_tree.py b/node_tree.py index 645855b17..ce4a28dae 100644 --- a/node_tree.py +++ b/node_tree.py @@ -50,7 +50,7 @@ from sverchok.core.update_system import ( from sverchok.core.socket_conversions import ( DefaultImplicitConversionPolicy, is_vector_to_matrix - ) +) from sverchok.core.node_defaults import set_defaults_if_defined @@ -71,6 +71,7 @@ socket_colors = { "SvColorSocket": (0.9, 0.8, 0.0, 1.0), "MatrixSocket": (0.2, 0.8, 0.8, 1.0), "SvDummySocket": (0.8, 0.8, 0.8, 0.3), + "SvSeparatorSocket": (0.0, 0.0, 0.0, 0.0), "ObjectSocket": (0.69, 0.74, 0.73, 1.0), "TextSocket": (0.68, 0.85, 0.90, 1), } @@ -89,7 +90,7 @@ class SvSocketCommon: use_quicklink = BoolProperty(default=True) expanded = BoolProperty(default=False) - quicklink_func_name = StringProperty(default="", name="quicklink_func_name") + quicklink_func_name = StringProperty(default="", name="quicklink_func_name") @property def other(self): @@ -183,13 +184,12 @@ class SvSocketCommon: op.new_node_offsety = -30 * self.index def draw(self, context, layout, node, text): - # just handle custom draw..be it input or output. # hasattr may be excessive here if self.bl_idname == 'StringsSocket': if hasattr(self, 'custom_draw') and self.custom_draw: - # does the node have the draw function referred to by + # does the node have the draw function referred to by # the string stored in socket's custom_draw attribute if hasattr(node, self.custom_draw): getattr(node, self.custom_draw)(self, context, layout) @@ -204,7 +204,7 @@ class SvSocketCommon: t = text if not self.is_output: if self.prop_name: - prop = node.rna_type.properties.get(self.prop_name, None) + prop = node.rna_type.properties.get(self.prop_name, None) t = prop.name if prop else text info_text = t + '. ' + SvGetSocketInfo(self) info_text += self.extra_info @@ -247,6 +247,7 @@ class SvSocketCommon: self.node.debug("Trying to convert data for input socket %s by %s", self.name, policy) return policy.convert(self, source_data) + class MatrixSocket(NodeSocket, SvSocketCommon): '''4x4 matrix Socket type''' bl_idname = "MatrixSocket" @@ -269,7 +270,7 @@ class MatrixSocket(NodeSocket, SvSocketCommon): def sv_get(self, default=sentinel, deepcopy=True, implicit_conversions=None): self.num_matrices = 0 if self.is_linked and not self.is_output: - source_data = SvGetSocket(self, deepcopy = True if self.needs_data_conversion() else deepcopy) + source_data = SvGetSocket(self, deepcopy=True if self.needs_data_conversion() else deepcopy) return self.convert_data(source_data, implicit_conversions) elif default is sentinel: @@ -298,7 +299,7 @@ class VerticesSocket(NodeSocket, SvSocketCommon): def sv_get(self, default=sentinel, deepcopy=True, implicit_conversions=None): if self.is_linked and not self.is_output: - source_data = SvGetSocket(self, deepcopy = True if self.needs_data_conversion() else deepcopy) + source_data = SvGetSocket(self, deepcopy=True if self.needs_data_conversion() else deepcopy) return self.convert_data(source_data, implicit_conversions) if self.prop_name: @@ -331,7 +332,7 @@ class SvQuaternionSocket(NodeSocket, SvSocketCommon): def sv_get(self, default=sentinel, deepcopy=True, implicit_conversions=None): if self.is_linked and not self.is_output: - source_data = SvGetSocket(self, deepcopy = True if self.needs_data_conversion() else deepcopy) + source_data = SvGetSocket(self, deepcopy=True if self.needs_data_conversion() else deepcopy) return self.convert_data(source_data, implicit_conversions) if self.prop_name: @@ -396,6 +397,23 @@ class SvDummySocket(NodeSocket, SvSocketCommon): self = new_self +class SvSeparatorSocket(NodeSocket, SvSocketCommon): + ''' Separator Socket used to separate groups of sockets ''' + bl_idname = "SvSeparatorSocket" + bl_label = "Separator Socket" + + prop_name = StringProperty(default='') + + def draw(self, context, layout, node, text): + # layout.label("") + layout.label("——————") + + def remove_links(self): + # print("separator sockets removing links") + for link in self.links: + self.id_data.links.remove(link) + + class StringsSocket(NodeSocket, SvSocketCommon): '''Generic, mostly numbers, socket type''' bl_idname = "StringsSocket" @@ -804,7 +822,7 @@ class SverchCustomTreeNode: classes = [ SverchCustomTree, VerticesSocket, MatrixSocket, StringsSocket, - SvColorSocket, SvQuaternionSocket, SvDummySocket, + SvColorSocket, SvQuaternionSocket, SvDummySocket, SvSeparatorSocket, SvLinkNewNodeInput, ] diff --git a/nodes/modifier_change/input_switch.py b/nodes/modifier_change/input_switch.py new file mode 100644 index 000000000..39726d9c7 --- /dev/null +++ b/nodes/modifier_change/input_switch.py @@ -0,0 +1,332 @@ +# ##### 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 bpy +from bpy.props import IntProperty, BoolProperty + +from sverchok.node_tree import SverchCustomTreeNode +from sverchok.data_structure import updateNode +from collections import OrderedDict + +GREEK_LABELS = ["Alpha", "Beta", "Gamma", "Delta", "Epsilon", + "Zeta", "Eta", "Theta", "Iota", "Kappa", "Lambda", + "Mu", "Nu", "Xi", "Omicron", "Pi", "Rho", "Sigma", + "Tau", "Upsilon", "Phi", "Chi", "Psi", "Omega"] + +LATIN_LABELS = tuple('ABCDEFGHIJKLMNOPQRSTUVWXYZ') + + +class SvInputSwitchNode(bpy.types.Node, SverchCustomTreeNode): + """ + Triggers: Sets, Switch, Select + Tooltip: Switch among multiple input sets + """ + bl_idname = 'SvInputSwitchNode' + bl_label = 'Input Switch' + sv_icon = 'SV_INPUT_SWITCH' + + def label_of_set(self, index): + return GREEK_LABELS[index] if self.greek_labels else LATIN_LABELS[index] + + def update_socket_types(self): + """ Update output socket types to match selected set input socket types""" + self.inhibit_update_calls = True # inhibit calls to update() + + inputs = self.inputs + outputs = self.outputs + + num_linked_sets = self.number_of_linked_sets() + if inputs["Selected"].is_linked or num_linked_sets == 0: + # set all output types to StringSocket when selected socket is connected or no sets are connected + for i in range(self.set_size): + out_label = "Data " + str(i + 1) + if out_label in outputs: + socket = outputs[out_label] + socket.replace_socket("StringsSocket") + + else: # set output socket types to match selected SET input socket types + n = inputs["Selected"].sv_get()[0][0] % num_linked_sets + # find the label of the n'th connected set + linked_sets_labels = [self.label_of_set(i) for i in range(self.number_of_sets()) if self.is_set_linked(i)] + selected_set_label = linked_sets_labels[n] + for i in range(self.set_size): + in_label = selected_set_label + " " + str(i + 1) + out_label = "Data " + str(i + 1) + out_socket_type = "StringsSocket" + if in_label in inputs and inputs[in_label].is_linked: + link = inputs[in_label].links[0] + in_socket_type = link.from_socket.bl_idname + out_socket_type = in_socket_type + else: + out_socket_type = "StringsSocket" + + if out_label in outputs: + socket = outputs[out_label] + socket.replace_socket(out_socket_type) + + self.inhibit_update_calls = False + + self.socket_types_outdated = False + + def update_selected(self, context): + self.update_socket_types() + updateNode(self, context) + + def update_set_sockets(self, context): + """ + Update the input/output sockets in the sets whenever: + * the size of the set changes + * the labels are switched from greek to latin + * the separators are shown/hidden + """ + + self.id_data.freeze(hard=True) # inhibit calls to process() + self.inhibit_update_calls = True # inhibit calls to update() + + inputs = self.inputs + + # RECORD the previous links to the SET input sockets + in_link_map = OrderedDict() + in_set_sockets = [s for s in inputs if s.name != "Selected"] + + # the size of the set prior to this update (needed to find input/set location) + set_size = self.last_set_size + 1 if self.last_show_separators else self.last_set_size + self.last_set_size = self.set_size + self.last_show_separators = self.show_separators + + new_set_index = -1 + last_set_index = -1 + for i, s in enumerate(in_set_sockets): + this_set_index = int(i / set_size) + if s.is_linked: + if this_set_index != last_set_index: + last_set_index = this_set_index + new_set_index = new_set_index + 1 + new_num = s.name[-1:] # extract the number from the label + new_label = self.label_of_set(new_set_index) + " " + new_num + in_link_map[new_label] = [link.from_socket for link in s.links] + + # REMOVE the old SET's inputs sockets + in_socket_names = [s.name for s in inputs if s.name != "Selected"] + for name in in_socket_names: + inputs.remove(inputs[name]) + + # CREATE the SET of input sockets + for n in range(new_set_index + 2): + if self.show_separators: + inputs.new("SvSeparatorSocket", self.label_of_set(n) + " 0") + for i in range(self.set_size): # create the first SET inputs + label = self.label_of_set(n) + " " + str(i + 1) + inputs.new("StringsSocket", label) + + # RECONNECT previously linked SET input sockets (if they still exist) + for sn, from_socket_list in in_link_map.items(): + if sn in inputs: + for from_socket in from_socket_list: + links = inputs[sn].id_data.links + links.new(from_socket, inputs[sn]) + + outputs = self.outputs + + # RECORD the previous links to the SET's output sockets + out_link_map = OrderedDict() + for s in outputs: + if s.is_linked: + out_link_map[s.name] = [link.to_socket for link in s.links] + + # REMOVE the old SET's output sockets + out_socket_names = [s.name for s in outputs] + for name in out_socket_names: + outputs.remove(outputs[name]) + + # CREATE new output sockets + for i in range(self.set_size): # create the SET outputs + label = "Data " + str(i + 1) + outputs.new("StringsSocket", label) + + # RECONNECT previously linked SET output sockets (if they still exist) + for sn, to_socket_list in out_link_map.items(): + if sn in outputs: + for to_socket in to_socket_list: + links = outputs[sn].id_data.links + links.new(outputs[sn], to_socket) + + self.inhibit_update_calls = False + self.id_data.unfreeze(hard=True) + + self.update_socket_types() + + set_size = IntProperty( + name="Set Size", description="Number of inputs in a set", + default=2, min=1, max=9, update=update_set_sockets) + + last_set_size = IntProperty( + name="Last Set Size", description="Last Number of inputs in a set", + default=2, min=1, max=9) + + selected = IntProperty( + name="Selected", description="Selected Set", + default=0, min=0, update=update_selected) + + show_separators = BoolProperty( + name="Show Separators", description="Show separators between sets", + default=True, update=update_set_sockets) + + last_show_separators = BoolProperty( + name="Last Show Separators", description="Last Show separators between sets", + default=True) + + greek_labels = BoolProperty( + name="Greek Labels", description="Use Greek letters for set input socket labels", + default=True, update=update_set_sockets) + + inhibit_update_calls = BoolProperty( + name="Skip Update", description="Flag to inhibit update calls when setting sockets", + default=False) + + socket_types_outdated = BoolProperty( + name="Needs Socket Type Update", + description="Flag to mark when (output) socket types need to be updated", + default=False) + + def get_set_size(self): + return self.set_size + 1 if self.show_separators else self.set_size + + def number_of_sets(self): + return int((len(self.inputs) - 1) / self.get_set_size()) # exclude "Selected" input + + def number_of_linked_sets(self): + num_linked_sets = 0 + for n in range(self.number_of_sets()): + if self.is_set_linked(n): + num_linked_sets = num_linked_sets + 1 + return num_linked_sets + + def is_set_linked(self, index): + return any(s.is_linked for s in self.inputs_in_set(index)) + + def inputs_in_set(self, index): + set_size = self.get_set_size() + i1 = 1 + set_size * index + i2 = 1 + set_size * (index + 1) + return self.inputs[i1:i2] + + def update(self): + """ Update the input sockets when sockets are connected/disconnected """ + + if self.inhibit_update_calls: + return + + # only needs update if the node has all its initial sockets created + set_size = self.get_set_size() + if len(self.inputs) < set_size + 1: + return + + self.id_data.freeze(hard=True) # inhibit calls to process() + self.inhibit_update_calls = True # inhibit calls to update() + + inputs = self.inputs + + # disconnect any separator socket in case any is connected + _ = [s.remove_links() for s in inputs if s.bl_idname == "SvSeparatorSocket"] + + n = self.number_of_sets() + + # is the last set linked ? => create an extra set + if self.is_set_linked(n - 1): + new_set_label = self.label_of_set(n) + # create sockets for the new set with the next label: Alpha to Omega + if self.show_separators: + inputs.new("SvSeparatorSocket", new_set_label + " 0") + for i in range(self.set_size): + inputs.new("StringsSocket", new_set_label + " " + str(i + 1)) + + else: # last set is unlinked ? => remove last unconnected sets except the last + m = n - 1 # start with the last set + while m != 0: # one set must always exist + if self.is_set_linked(m - 1): # previous set connected ? done + break + else: # previous set not connected => remove this set + names = [s.name for s in self.inputs_in_set(m)] + for name in names: + inputs.remove(inputs[name]) + m = m - 1 + + self.inhibit_update_calls = False + self.id_data.unfreeze(hard=True) + + self.socket_types_outdated = True # prompt socket type update + + def draw_buttons(self, context, layout): + layout.prop(self, "set_size") + + def draw_buttons_ext(self, context, layout): + layout.prop(self, "show_separators") + layout.prop(self, "greek_labels") + + def sv_init(self, context): + self.inputs.new('StringsSocket', "Selected").prop_name = "selected" + if self.show_separators: + self.inputs.new("SvSeparatorSocket", self.label_of_set(0) + " 0") + for i in range(self.set_size): # create the first SET inputs & outputs + self.inputs.new("StringsSocket", self.label_of_set(0) + " " + str(i + 1)) + self.outputs.new("StringsSocket", "Data " + str(i + 1)) + + def process(self): + + if self.socket_types_outdated: + self.update_socket_types() + + # return if no outputs are connected + outputs = self.outputs + if not any(s.is_linked for s in outputs): + return + + inputs = self.inputs + + # get the list of LABELS of the LINKED sets (Alpha, Beta...) + linked_set_labels = [self.label_of_set(n) for n in range(self.number_of_sets()) if self.is_set_linked(n)] + num_linked_sets = len(linked_set_labels) # number of linked sets + + if num_linked_sets == 0: # no set to output if no input sets are connected + return + + input_n = inputs["Selected"].sv_get()[0] + selected_sets = [max(0, int(n)) % num_linked_sets for n in input_n] + + # get the sequence of LABELS of the sets selected to be output + sequence_labels = [linked_set_labels[i] for i in selected_sets] + # get the list of UNIQUE LABELS of the sets selected to be output + output_labels = list(set(sequence_labels)) + + for i in range(self.set_size): + out_label = "Data " + str(i + 1) + if outputs[out_label].is_linked: + # read ONCE all the inputs which are going to be output + in_dict = {label: inputs[label + " " + str(i + 1)].sv_get()[0] for label in output_labels} + # create output list based on read inputs and the selected sequence + data_list = [in_dict[label] for label in sequence_labels] + outputs[out_label].sv_set(data_list) + + +def register(): + bpy.utils.register_class(SvInputSwitchNode) + + +def unregister(): + bpy.utils.unregister_class(SvInputSwitchNode) diff --git a/ui/icons/sv_input_switch.png b/ui/icons/sv_input_switch.png new file mode 100644 index 0000000000000000000000000000000000000000..7ae7d6d1469555515611230fe892f2caad42d2b9 GIT binary patch literal 2870 zcmbVOc{o&iA0LtyWh*;14X!L>7GpNcl$jx}LAG$SjF~e`GQ%txOSZ@ot_hJX6{)Ur z3q^XHkgHN673$iBvQ%V=8+UYb-}jIE+&|v)Jm>s=zwh~eKHvR3C&kIZX8Tt4tsoF+ zyX^szvv>us-J7MwPi{oSXYr!OBYW^%*bH76l>>nG)7kz2*fy9N05}6wdU)s~z#IhH zAYi(B@H{B?cp5v{n7XE8EC>z}qd_2Zf*^!S3j%mxe;|O#vVeT37eT;Gx&`ErDFsdm zAp(KS0}&j+CBng#77;|l(IEs&u(<#)CI|+2RIniUD2t01SU|q<;>G(lF$@C!ros!d zfc)i@2gM0YWOD$psWBQ#L!jVb49*yVhNDaoMqnfyfq}tIVQ@4Qj=;mQcq9V+{epb$IDE0>L$YF|$LOn`i)A+#vi)Tx+ zfQa81)0uRQ6(`peQ&RO+}!QNE#Zx&ht-rG7*U;6Nxw^ z&Wemcka1Q-IL2x}nTR4Gh&XGU^*Yv;#pO|1G+^B}Q*8S?7W=PQJdp!Xd2Eg=n|*Y> z3!DPkJT^Cw9Rem2*Q$;M@1;;_OxBuEf2~k|#Z3Y@%wquEn!^qTf6FwU`43(&Fbuj0 z4u^%(OaL4dMZ;jAR4NjHVi+iKpQ33ve+(V+9Z&y%GYAtq1zT&E|7o9fk+>(g_UOzar-O05}X{B%a4dIxlpAafKbuegg3pQ3! zsMw(#?;u_DWFNdCeuuwHjqV;>;9+cFGj>4kH$t^^PTs3MLg`DT$Ze^|Q2XG`lBt80 zKkT`@d8Vp$`eyxyxxP*Bg{Xx>t|)9*)2w-PTf_5K@M}Z@BPud0 z^3rT7&i;a*eHNm4UzGizK_elQApKx!uEgyHsdH8+hGcaNKrtGdh#r=c*xi8J(dO&R6!miX}n93*%F>wrji^!O4#~tNLx2Q=X4kJe5@z za(6B$*K|uLyx^&-h6a}v6x5lMlXFc?RkfS+!ZJTcUG@1w{mS!0TV=2fIOvZVhfTT7 z@cx04jzBmbZ=hORS4Zu;8~(hpv2n-3bnkMpz1vf%*km8cuP4++ORjPo>y&nEc7{|> z=C?uvgG6x`ExVZ4!C;bdr@YAU?RcSJPSbPC!7q6anUJ8?*4D^7ckW0HcXxa2-I)qSnV3jA z)s^o~x6!Jnr{SC1pKVl6swI2#FDI~uM@H+;`)F8i!bK4Hg>}(iBW%LLZiiGwec?ouqA9uBQlP?g zy>I|5Jas=)O{nUZx4Q#URN`S{o2V%BYl%k4%3Fu> zckA0Q()HYry4%=ll9Fsus|I01{Sk9{Rl}+2?_10V5QnZE=y&zJ#Pc6saT=A)DsD== zFj##?eK`7L%^AmvfZaPZa2fovva)*}9UYl=W!V~0UO)ZhC=A%7cRArkL&EfGUM~Nz z>6HEJU*f9FP6a8Q6&*mGSDD?N2sp8c$J%daYW$~U zE@Weih3B?X@W!zOEq#hM3boV6$4BRQT4r%%*ir{?!r)l-(*2g7tZNfFgcEZ8f50Sv zL+>o!Yq@euKF&}@Py4z%BhaL*G zJ!+1;EFFt6b319FaxVcBdc9>)!_5kGcQHd|WyQ`r?PX(%6lk0F5OyN#xCGR}3!!|9 z+&!fu8{kPD8*3{f^7*FF2YQpinmtu~&U9W1Uo9EyH7*%DUi)$8XUoNa!-5KBSsjTH zqk-w;GVfDc?yFbMOuZ>MtFJ#mh6r?h3Z@_RR%lH>3GU`ySR!JlnU?C5!3lB>B`?c!zmrCl?cRht$Qp35j;W0y5O zgRV*`hHGQ7d3tREeA_vKBeC0#{w~3xy|F(z-iI)Ct0z+UqR^vc5EEVTwyEpN;uk}5 z&Gw}YZB`g-*ce-L-(#kdvT?4Sd=s%EP2XcF-{8n>xhS^&+(=6X8Yfj2&*eY_Gpv#~ zK8#9uSjx6{Bd+9CA1#PW%FM$T2fZ&F&AlA9^8|t{cS&>$>nB1p^2$%QHz`bxjRoj_ zjRTdGmL^QERF$haDcWw z`AIU~N&Jj^lia^)Afs5{5Acb#-+=$=Q&}8!6py z^-*85tQ|7Ew-@$?n^+jd%|(5IsyxrmSV+`3RsAsKz0bU%sHPRte;Bg2(|wXUo|Kd% z6bc(Cu?6#8>7bISf-~*SUkp@JxOU1s)7L6X&4^?;8DcomYin(NQQ^^}N8{t;-43jY zfZvtsu}7Ad@J}Dm8$ei&Yf IZsiyMCo?wWE&u=k literal 0 HcmV?d00001 diff --git a/utils/sv_default_macros.py b/utils/sv_default_macros.py index 1b3f2c907..4cbe62682 100644 --- a/utils/sv_default_macros.py +++ b/utils/sv_default_macros.py @@ -26,33 +26,48 @@ from sverchok.utils.sv_update_utils import sv_get_local_path macros = { "> obj vd": { - 'display_name': "active_obj into objlite + vdmk2", - 'file': 'macro', + 'display_name': "active_obj into objlite + vdmk2", + 'file': 'macro', 'ident': ['verbose_macro_handler', 'obj vd']}, "> objs vd": { 'display_name': "multi obj in + vdmk2", - 'file': 'macro', + 'file': 'macro', 'ident': ['verbose_macro_handler', 'objs vd']}, "> zen": { 'display_name': "zen of Sverchok", - 'file': 'macro', + 'file': 'macro', 'ident': ['verbose_macro_handler', 'zen']}, "> sn petal": { 'display_name': "load snlite w/ petalsine", - 'file': 'macro', + 'file': 'macro', 'ident': ['verbose_macro_handler', 'sn petal']}, "> monad info": { 'display_name': "output current idx / total", - 'file': 'macro', + 'file': 'macro', 'ident': ['verbose_macro_handler', 'monad info']}, + "> sw1": { + 'display_name': "connect nodes to switch", + 'file': 'macro', + 'ident': ['verbose_macro_handler', 'switch1']}, + "> sw12": { + 'display_name': "connect nodes to switch", + 'file': 'macro', + 'ident': ['verbose_macro_handler', 'switch12']}, + "> sw13": { + 'display_name': "connect nodes to switch", + 'file': 'macro', + 'ident': ['verbose_macro_handler', 'switch13']}, + "> sw123": { + 'display_name': "connect nodes to switch", + 'file': 'macro', + 'ident': ['verbose_macro_handler', 'switch123']}, "> gp +": { 'display_name': "grease pencil setup", - 'file': 'macro', + 'file': 'macro', 'ident': ['verbose_macro_handler', 'gp +']} } - sv_types = {'SverchCustomTreeType', 'SverchGroupTreeType'} @@ -60,7 +75,7 @@ def sn_loader(snlite, script_name=None): sv_path = os.path.dirname(sv_get_local_path()[0]) snlite_template_path = os.path.join(sv_path, 'node_scripts', 'SNLite_templates') fullpath = os.path.join(snlite_template_path, script_name) - + txt = bpy.data.texts.load(fullpath) snlite.script_name = os.path.basename(txt.name) snlite.load() @@ -87,7 +102,7 @@ class DefaultMacros(): ng = bpy.data.node_groups.new(**ng_params) ng.use_fake_user = True context.space_data.node_tree = ng - operator.report({"WARNING"}, msg_two) + operator.report({"WARNING"}, msg_two) @classmethod def verbose_macro_handler(cls, operator, context, term): @@ -102,7 +117,7 @@ class DefaultMacros(): obj_in_node.dget() vd_node = nodes.new('ViewerNode2') vd_node.location = obj_in_node.location.x + 180, obj_in_node.location.y - + links.new(obj_in_node.outputs[0], vd_node.inputs[0]) links.new(obj_in_node.outputs[2], vd_node.inputs[1]) links.new(obj_in_node.outputs[3], vd_node.inputs[2]) @@ -112,10 +127,10 @@ class DefaultMacros(): obj_in_node.get_objects_from_scene(operator) vd_node = nodes.new('ViewerNode2') vd_node.location = obj_in_node.location.x + 180, obj_in_node.location.y - + links.new(obj_in_node.outputs[0], vd_node.inputs[0]) links.new(obj_in_node.outputs[2], vd_node.inputs[1]) - links.new(obj_in_node.outputs[3], vd_node.inputs[2]) + links.new(obj_in_node.outputs[3], vd_node.inputs[2]) elif term == 'zen': full_url_term = 'https://blenderpython.tumblr.com/post/91951323209/zen-of-sverchok' @@ -130,6 +145,48 @@ class DefaultMacros(): monad_info = nodes.new('SvMonadInfoNode') monad_info.location = x, y + elif "switch" in term: + selected_nodes = context.selected_nodes + # get bounding box of all selected nodes + minx = +1e10 + maxx = -1e10 + miny = +1e10 + maxy = -1e10 + for node in selected_nodes: + minx = min(minx, node.location.x) + maxx = max(maxx, node.location.x + node.width) + miny = min(miny, node.location.y - node.height) + maxy = max(maxy, node.location.y) + + switch_node = nodes.new('SvInputSwitchNode') + switch_node.location = maxx + 100, maxy + + # find out which sockets to connect + socket_numbers = term.replace("switch", "") + if len(socket_numbers) == 1: # one socket + socket_indices = [0] + else: # multiple sockets + socket_indices = [int(n) - 1 for n in socket_numbers] + + switch_node.set_size = len(socket_indices) + + sorted_nodes = sorted(selected_nodes, key=lambda n: n.location.y, reverse=True) + + # link the nodes to InputSwitch node + for i, node in enumerate(sorted_nodes): + label = switch_node.label_of_set(i) + for j, n in enumerate(socket_indices): + links.new(node.outputs[n], switch_node.inputs[label + " " + str(j+1)]) + + if all(node.outputs[0].bl_idname == "VerticesSocket" for node in sorted_nodes): + viewer_node = nodes.new("ViewerNode2") + viewer_node.location = switch_node.location.x + switch_node.width + 100, maxy + + # link the input switch node to the ViewerDraw node + links.new(switch_node.outputs[0], viewer_node.inputs[0]) + if len(socket_indices) > 1: + links.new(switch_node.outputs[1], viewer_node.inputs[1]) + elif term == 'gp +': needed_nodes = [ ['SvGetAssetProperties', (0.00, 0.00)], @@ -141,7 +198,7 @@ class DefaultMacros(): ['LineConnectNodeMK2', (1100, 115)], ['ViewerNode2', (1290, 115)], ] - + made_nodes = [] x, y = context.space_data.cursor_location[:] for node_bl_idname, node_location in needed_nodes: -- GitLab From 154e10e073666b631e5a3de0f230ff96d7ad80a0 Mon Sep 17 00:00:00 2001 From: DolphinDream Date: Mon, 4 Mar 2019 23:35:16 -0500 Subject: [PATCH 44/60] Add new SuperEllipsoid generator node (WIP) --- index.md | 1 + nodes/generator/super_ellipsoid.py | 283 +++++++++++++++++++++++++++++ 2 files changed, 284 insertions(+) create mode 100644 nodes/generator/super_ellipsoid.py diff --git a/index.md b/index.md index 1ec1c5511..2f4fa163c 100644 --- a/index.md +++ b/index.md @@ -16,6 +16,7 @@ SvBoxNode SvCircleNode CylinderNode + SvSuperEllipsoidNode SphereNode SvIcosphereNode SvTorusNode diff --git a/nodes/generator/super_ellipsoid.py b/nodes/generator/super_ellipsoid.py new file mode 100644 index 000000000..6382daf94 --- /dev/null +++ b/nodes/generator/super_ellipsoid.py @@ -0,0 +1,283 @@ +# ##### 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 bpy +from bpy.props import BoolProperty, IntProperty, FloatProperty, EnumProperty + +from sverchok.node_tree import SverchCustomTreeNode +from sverchok.data_structure import (match_long_repeat, updateNode) + +from math import sin, cos, pi + + +def sign(x): return 1 if x >= 0 else -1 + +epsilon = 1e-10 # used to eliminate vertex overlap at the South/North poles + +# name : [ sx, sy, sz, xp, xm, np, nm ] +super_presets = { + " ": [0.0, 0.0, 0.0, 0.0, 0.0, 0, 0], + "SPHERE": [1.0, 1.0, 1.0, 1.0, 1.0, 24, 24], + "CUBE": [1.0, 1.0, 1.0, 0.0, 0.0, 3, 5], + "CYLINDER": [1.0, 1.0, 1.0, 1.0, 0.0, 4, 32], + "OCTOHEDRON": [1.0, 1.0, 1.0, 1.0, 1.0, 3, 4], + "SPINNING TOP": [1.0, 1.0, 1.0, 1.0, 3.0, 24, 24], + "STAR": [1.0, 1.0, 1.0, 4.0, 4.0, 32, 32], + "ROUNDED BIN": [1.0, 1.0, 1.0, 0.5, 0.0, 32, 32], + "ROUNDED CUBE": [1.0, 1.0, 1.0, 0.2, 0.2, 32, 32], + "ROUNDED CYLINDER": [1.0, 1.0, 1.0, 1.0, 0.1, 32, 32], +} + + +def make_verts(sx, sy, sz, xp, xm, np, nm): + """ + Generate the super ellipsoid vertices for the given parameters + sx : scale along x + sx : scale along y + sx : scale along z + xp : parallel exponent + xm : meridian exponent + np : number of parallels + nm : number of meridians + """ + verts = [] + for p in range(np): + a = (pi / 2 - epsilon) * (2 * p / (np - 1) - 1) + cosA = cos(a) + sinA = sin(a) + powCA = pow(abs(cosA), xm) * sign(cosA) + powSA = pow(abs(sinA), xm) * sign(sinA) + for m in range(nm): + b = pi * (2 * m / nm - 1) + cosB = cos(b) + sinB = sin(b) + powCB = pow(abs(cosB), xp) * sign(cosB) + powSB = pow(abs(sinB), xp) * sign(sinB) + + x = sx * powCA * powCB + y = sy * powCA * powSB + z = sz * powSA + verts.append([x, y, z]) + + return verts + + +def make_edges(P, M): + """ + Generate the super ellipsoid edges for the given parameters + P : number of parallels + M : number of meridians + """ + edge_list = [] + + for i in range(P): + for j in range(M - 1): + edge_list.append([i * M + j, i * M + j + 1]) + edge_list.append([(i + 1) * M - 1, i * M]) + + for i in range(P): + for j in range(M): + edge_list.append([i * M + j, (i + 1) * M + j]) + + return edge_list + + +def make_polys(P, M, cap_top, cap_bottom): + """ + Generate the super ellipsoid polygons for the given parameters + P : number of parallels + M : number of meridians + """ + poly_list = [] + + for i in range(P - 1): + for j in range(M - 1): + poly_list.append([i * M + j, i * M + j + 1, (i + 1) * M + j + 1, (i + 1) * M + j]) + poly_list.append([(i + 1) * M - 1, i * M, (i + 1) * M, (i + 2) * M - 1]) + + if cap_top: + cap = [] + for j in range(M): + cap.append(M * (P - 1) + j) + poly_list.append(cap) + + if cap_bottom: + cap = [] + for j in reversed(range(M)): + cap.append(j) + poly_list.append(cap) + + return poly_list + + +class SvSuperEllipsoidNode(bpy.types.Node, SverchCustomTreeNode): + """ + Triggers: Sphere Cube Cylinder Octohedron Star + Tooltip: Generate various Super Ellipsoid shapes. + """ + bl_idname = 'SvSuperEllipsoidNode' + bl_label = 'Super Ellipsoid' + + def update_ellipsoid(self, context): + if self.updating: + return + + self.presets = " " + updateNode(self, context) + + def update_presets(self, context): + self.updating = True + + if self.presets == " ": + self.updating = False + return + + sx, sy, sz, xp, xm, np, nm = super_presets[self.presets] + self.scale_x = sx + self.scale_y = sy + self.scale_z = sz + self.exponent_parallels = xp + self.exponent_meridians = xm + self.number_parallels = np + self.number_meridians = nm + self.cap_bottom = True + self.cap_top = True + + self.updating = False + updateNode(self, context) + + preset_items = [(k, k.title(), "", "", i) for i, (k, v) in enumerate(sorted(super_presets.items()))] + + presets = EnumProperty( + name="Presets", items=preset_items, + update=update_presets) + + scale_x = FloatProperty( + name='Scale X', description="Scale along X", + default=1.0, update=update_ellipsoid) + + scale_y = FloatProperty( + name='Scale Y', description="Scale along Y", + default=1.0, update=update_ellipsoid) + + scale_z = FloatProperty( + name='Scale Z', description="Scale along Z", + default=1.0, update=update_ellipsoid) + + exponent_parallels = FloatProperty( + name='P Exponent', description="Parallel exponent", + default=1.0, min=0.0, update=update_ellipsoid) + + exponent_meridians = FloatProperty( + name='M Exponent', description="Meridian exponent", + default=1.0, min=0.0, update=update_ellipsoid) + + number_parallels = IntProperty( + name='Parallels', description="Number of parallels", + default=10, min=3, update=update_ellipsoid) + + number_meridians = IntProperty( + name='Meridians', description="Number of meridians", + default=10, min=3, update=update_ellipsoid) + + cap_top = BoolProperty( + name='Cap Top', description="Generate top cap", + default=True, update=updateNode) + + cap_bottom = BoolProperty( + name='Cap Bottom', description="Generate bottom cap", + default=True, update=updateNode) + + updating = BoolProperty(default=False) # used for disabling update callback + + def sv_init(self, context): + self.inputs.new('StringsSocket', "SX").prop_name = 'scale_x' + self.inputs.new('StringsSocket', "SY").prop_name = 'scale_y' + self.inputs.new('StringsSocket', "SZ").prop_name = 'scale_z' + self.inputs.new('StringsSocket', "XP").prop_name = 'exponent_parallels' + self.inputs.new('StringsSocket', "XM").prop_name = 'exponent_meridians' + self.inputs.new('StringsSocket', "NP").prop_name = 'number_parallels' + self.inputs.new('StringsSocket', "NM").prop_name = 'number_meridians' + + self.outputs.new('VerticesSocket', "Vertices", "Vertices") + self.outputs.new('StringsSocket', "Edges", "Edges") + self.outputs.new('StringsSocket', "Polygons", "Polygons") + + self.presets = "ROUNDED CUBE" + + def draw_buttons(self, context, layout): + if not self.inputs["XP"].is_linked and not self.inputs["XM"].is_linked: + layout.prop(self, "presets", text="") + + def draw_buttons_ext(self, context, layout): + column = layout.column(align=True) + row = column.row(align=True) + row.prop(self, "cap_top", text="Cap T", toggle=True) + row.prop(self, "cap_bottom", text="Cap B", toggle=True) + + def process(self): + if not any(s.is_linked for s in self.outputs): + return + + inputs = self.inputs + + # read inputs + input_sx = inputs["SX"].sv_get()[0] + input_sy = inputs["SY"].sv_get()[0] + input_sz = inputs["SZ"].sv_get()[0] + input_xp = inputs["XP"].sv_get()[0] + input_xm = inputs["XM"].sv_get()[0] + input_np = inputs["NP"].sv_get()[0] + input_nm = inputs["NM"].sv_get()[0] + + # sanitize inputs + input_np = list(map(lambda a: max(3, a), input_np)) + input_nm = list(map(lambda a: max(3, a), input_nm)) + + params = match_long_repeat([input_sx, input_sy, input_sz, + input_xp, input_xm, + input_np, input_nm]) + + verts_list = [] + edges_list = [] + polys_list = [] + for sx, sy, sz, xp, xm, np, nm in zip(*params): + verts = make_verts(sx, sy, sz, xp, xm, np, nm) + edges = make_edges(np, nm) + polys = make_polys(np, nm, self.cap_top, self.cap_bottom) + verts_list.append(verts) + edges_list.append(edges) + polys_list.append(polys) + + # outputs + if self.outputs['Vertices'].is_linked: + self.outputs['Vertices'].sv_set(verts_list) + + if self.outputs['Edges'].is_linked: + self.outputs['Edges'].sv_set(edges_list) + + if self.outputs['Polygons'].is_linked: + self.outputs['Polygons'].sv_set(polys_list) + + +def register(): + bpy.utils.register_class(SvSuperEllipsoidNode) + + +def unregister(): + bpy.utils.unregister_class(SvSuperEllipsoidNode) -- GitLab From b6133d345bebc0c02172ee023881241530436459 Mon Sep 17 00:00:00 2001 From: DolphinDream Date: Tue, 5 Mar 2019 08:16:32 -0500 Subject: [PATCH 45/60] Various updates - cleanup - optimization - documentation - fixes --- .../generators_extended/super_ellipsoid.rst | 68 ++++++++++++ nodes/generator/super_ellipsoid.py | 100 ++++++++++-------- 2 files changed, 124 insertions(+), 44 deletions(-) create mode 100644 docs/nodes/generators_extended/super_ellipsoid.rst diff --git a/docs/nodes/generators_extended/super_ellipsoid.rst b/docs/nodes/generators_extended/super_ellipsoid.rst new file mode 100644 index 000000000..7206bb13e --- /dev/null +++ b/docs/nodes/generators_extended/super_ellipsoid.rst @@ -0,0 +1,68 @@ +Super Ellipsoid +--------------- + +This node creates various super ellipsoid (3D) shapes based on the super-ellipsoid formula. + +Reference: https://en.wikipedia.org/wiki/Superellipsoid + +Inputs +====== + +- **Scale X** +- **Scale Y** +- **Scale Z** +- **Exponent P** +- **Exponent M** +- **Parallels** +- **Meridians** + + +Parameters +========== + +All parameters, except the **Presets**, accepts single input values directly from the node or from an outside node. The exponent values are sanitized/bounded to values larger than zero and the **Parallels** and **Meridians** inputs are sanitized to integer values of at least 3. + ++-----------------+-------+---------+---------------------------------+ +| Param | Type | Default | Description | ++=================+=======+=========+=================================+ +| **Scale X** | Float | 1.0 | Scale the ellipsoid along X | ++-----------------+-------+---------+---------------------------------+ +| **Scale Y** | Float | 1.0 | Scale the ellipsoid along Y | ++-----------------+-------+---------+---------------------------------+ +| **Scale Z** | Float | 1.0 | Scale the ellipsoid along Z | ++-----------------+-------+---------+---------------------------------+ +| **Exponent P** | Float | [1] | Parallel section exponent | ++-----------------+-------+---------+---------------------------------+ +| **Exponent M** | Float | [1] | Meridian section exponent | ++-----------------+-------+---------+---------------------------------+ +| **Parallels** | Int | [1] | Number of parallel sections | ++-----------------+-------+---------+---------------------------------+ +| **Meridians** | Int | [1] | Number of meridian sections | ++-----------------+-------+---------+---------------------------------+ + +Notes: +[1] : The default values are based on the selected preset. + + +Presets +======= +The node has a set of presets sampling the different shapes the node can generate corresponding primarily to various combinations of the **exponent** parameters. + + +Extra Parameters +================ +**Cap Top** +**Cap Bottom** +These are the South and North pole caps. For non-zero values of the exponents the caps are very small to notice. + +Note: The reason for the caps are to avoid collapsing the vertices at the poles. This preserves the quad mesh topology as well as it results in correct normals being generated at the quads at the poles. The caps are generated by default, but they can be hidden as needed. Particularly these are useful for the shapes when the parallel exponent is close to zero (e.g. cylinders) when the caps are large enough to notice. + + +Outputs +======= +**Verts** +**Edges** +**Polygons** +The outputs are generated once the sockets are connected. +The node is vectorized and it will generate multiple outputs based on the lenght of the inputs. + diff --git a/nodes/generator/super_ellipsoid.py b/nodes/generator/super_ellipsoid.py index 6382daf94..acbd83bf1 100644 --- a/nodes/generator/super_ellipsoid.py +++ b/nodes/generator/super_ellipsoid.py @@ -32,12 +32,16 @@ epsilon = 1e-10 # used to eliminate vertex overlap at the South/North poles # name : [ sx, sy, sz, xp, xm, np, nm ] super_presets = { " ": [0.0, 0.0, 0.0, 0.0, 0.0, 0, 0], - "SPHERE": [1.0, 1.0, 1.0, 1.0, 1.0, 24, 24], + "SPHERE": [1.0, 1.0, 1.0, 1.0, 1.0, 32, 32], "CUBE": [1.0, 1.0, 1.0, 0.0, 0.0, 3, 5], "CYLINDER": [1.0, 1.0, 1.0, 1.0, 0.0, 4, 32], "OCTOHEDRON": [1.0, 1.0, 1.0, 1.0, 1.0, 3, 4], - "SPINNING TOP": [1.0, 1.0, 1.0, 1.0, 3.0, 24, 24], - "STAR": [1.0, 1.0, 1.0, 4.0, 4.0, 32, 32], + "SPINNING TOP": [1.0, 1.0, 1.0, 1.0, 4.0, 32, 32], + "CUBIC CONE": [1.0, 1.0, 1.0, 1.0, 2.0, 32, 32], + "CUBIC BALL": [1.0, 1.0, 1.0, 2.0, 1.0, 32, 32], + "CUSHION": [1.0, 1.0, 0.2, 2.0, 1.0, 32, 32], + "STAR BALL": [1.0, 1.0, 1.0, 4.0, 1.0, 32, 64], + "STAR": [1.0, 1.0, 1.0, 4.0, 4.0, 64, 64], "ROUNDED BIN": [1.0, 1.0, 1.0, 0.5, 0.0, 32, 32], "ROUNDED CUBE": [1.0, 1.0, 1.0, 0.2, 0.2, 32, 32], "ROUNDED CYLINDER": [1.0, 1.0, 1.0, 1.0, 0.1, 32, 32], @@ -46,7 +50,7 @@ super_presets = { def make_verts(sx, sy, sz, xp, xm, np, nm): """ - Generate the super ellipsoid vertices for the given parameters + Generate the super-ellipsoid vertices for the given parameters sx : scale along x sx : scale along y sx : scale along z @@ -58,20 +62,20 @@ def make_verts(sx, sy, sz, xp, xm, np, nm): verts = [] for p in range(np): a = (pi / 2 - epsilon) * (2 * p / (np - 1) - 1) - cosA = cos(a) - sinA = sin(a) - powCA = pow(abs(cosA), xm) * sign(cosA) - powSA = pow(abs(sinA), xm) * sign(sinA) + cos_a = cos(a) + sin_a = sin(a) + pow_ca = pow(abs(cos_a), xm) * sign(cos_a) + pow_sa = pow(abs(sin_a), xm) * sign(sin_a) for m in range(nm): b = pi * (2 * m / nm - 1) - cosB = cos(b) - sinB = sin(b) - powCB = pow(abs(cosB), xp) * sign(cosB) - powSB = pow(abs(sinB), xp) * sign(sinB) - - x = sx * powCA * powCB - y = sy * powCA * powSB - z = sz * powSA + cos_b = cos(b) + sin_b = sin(b) + pow_cb = pow(abs(cos_b), xp) * sign(cos_b) + pow_sb = pow(abs(sin_b), xp) * sign(sin_b) + + x = sx * pow_ca * pow_cb + y = sy * pow_ca * pow_sb + z = sz * pow_sa verts.append([x, y, z]) return verts @@ -79,19 +83,21 @@ def make_verts(sx, sy, sz, xp, xm, np, nm): def make_edges(P, M): """ - Generate the super ellipsoid edges for the given parameters - P : number of parallels - M : number of meridians + Generate the super-ellipsoid edges for the given parameters + P : number of parallels (= number of points in a meridian) + M : number of meridians (= number of points in a parallel) """ edge_list = [] - for i in range(P): - for j in range(M - 1): + # generate parallels edges (close paths) + for i in range(P): # for every point on a meridian + for j in range(M - 1): # for every point on a parallel (minus last) edge_list.append([i * M + j, i * M + j + 1]) - edge_list.append([(i + 1) * M - 1, i * M]) + edge_list.append([(i + 1) * M - 1, i * M]) # close the path - for i in range(P): - for j in range(M): + # generate meridians edges (open paths) + for j in range(M): # for every point on a parallel + for i in range(P - 1): # for every point on a meridian (minus last) edge_list.append([i * M + j, (i + 1) * M + j]) return edge_list @@ -99,7 +105,7 @@ def make_edges(P, M): def make_polys(P, M, cap_top, cap_bottom): """ - Generate the super ellipsoid polygons for the given parameters + Generate the super-ellipsoid polygons for the given parameters P : number of parallels M : number of meridians """ @@ -111,15 +117,11 @@ def make_polys(P, M, cap_top, cap_bottom): poly_list.append([(i + 1) * M - 1, i * M, (i + 1) * M, (i + 2) * M - 1]) if cap_top: - cap = [] - for j in range(M): - cap.append(M * (P - 1) + j) + cap = [M * (P - 1) + j for j in range(M)] poly_list.append(cap) if cap_bottom: - cap = [] - for j in reversed(range(M)): - cap.append(j) + cap = [j for j in reversed(range(M))] poly_list.append(cap) return poly_list @@ -128,7 +130,7 @@ def make_polys(P, M, cap_top, cap_bottom): class SvSuperEllipsoidNode(bpy.types.Node, SverchCustomTreeNode): """ Triggers: Sphere Cube Cylinder Octohedron Star - Tooltip: Generate various Super Ellipsoid shapes. + Tooltip: Generate various Super-Ellipsoid shapes """ bl_idname = 'SvSuperEllipsoidNode' bl_label = 'Super Ellipsoid' @@ -164,7 +166,7 @@ class SvSuperEllipsoidNode(bpy.types.Node, SverchCustomTreeNode): preset_items = [(k, k.title(), "", "", i) for i, (k, v) in enumerate(sorted(super_presets.items()))] presets = EnumProperty( - name="Presets", items=preset_items, + name="Presets", items=preset_items, description="Various presets", update=update_presets) scale_x = FloatProperty( @@ -206,6 +208,7 @@ class SvSuperEllipsoidNode(bpy.types.Node, SverchCustomTreeNode): updating = BoolProperty(default=False) # used for disabling update callback def sv_init(self, context): + self.width = 150 self.inputs.new('StringsSocket', "SX").prop_name = 'scale_x' self.inputs.new('StringsSocket', "SY").prop_name = 'scale_y' self.inputs.new('StringsSocket', "SZ").prop_name = 'scale_z' @@ -246,32 +249,41 @@ class SvSuperEllipsoidNode(bpy.types.Node, SverchCustomTreeNode): input_nm = inputs["NM"].sv_get()[0] # sanitize inputs - input_np = list(map(lambda a: max(3, a), input_np)) - input_nm = list(map(lambda a: max(3, a), input_nm)) + input_xp = list(map(lambda a: max(0.0, a), input_xp)) + input_xm = list(map(lambda a: max(0.0, a), input_xm)) + input_np = list(map(lambda a: max(3, int(a)), input_np)) + input_nm = list(map(lambda a: max(3, int(a)), input_nm)) params = match_long_repeat([input_sx, input_sy, input_sz, input_xp, input_xm, input_np, input_nm]) + verts_output_linked = self.outputs['Vertices'].is_linked + edges_output_linked = self.outputs['Edges'].is_linked + polys_output_linked = self.outputs['Polygons'].is_linked + verts_list = [] edges_list = [] polys_list = [] for sx, sy, sz, xp, xm, np, nm in zip(*params): - verts = make_verts(sx, sy, sz, xp, xm, np, nm) - edges = make_edges(np, nm) - polys = make_polys(np, nm, self.cap_top, self.cap_bottom) - verts_list.append(verts) - edges_list.append(edges) - polys_list.append(polys) + if verts_output_linked: + verts = make_verts(sx, sy, sz, xp, xm, np, nm) + verts_list.append(verts) + if edges_output_linked: + edges = make_edges(np, nm) + edges_list.append(edges) + if polys_output_linked: + polys = make_polys(np, nm, self.cap_top, self.cap_bottom) + polys_list.append(polys) # outputs - if self.outputs['Vertices'].is_linked: + if verts_output_linked: self.outputs['Vertices'].sv_set(verts_list) - if self.outputs['Edges'].is_linked: + if edges_output_linked: self.outputs['Edges'].sv_set(edges_list) - if self.outputs['Polygons'].is_linked: + if polys_output_linked: self.outputs['Polygons'].sv_set(polys_list) -- GitLab From 2564959cdb80027407aa53bab1525e5c9bec673f Mon Sep 17 00:00:00 2001 From: DolphinDream Date: Wed, 6 Mar 2019 00:03:08 -0500 Subject: [PATCH 46/60] Relocate SuperEllipsoid node to extended generators --- index.md | 2 +- nodes/{generator => generators_extended}/super_ellipsoid.py | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename nodes/{generator => generators_extended}/super_ellipsoid.py (100%) diff --git a/index.md b/index.md index 2f4fa163c..1a18ecc2b 100644 --- a/index.md +++ b/index.md @@ -16,7 +16,6 @@ SvBoxNode SvCircleNode CylinderNode - SvSuperEllipsoidNode SphereNode SvIcosphereNode SvTorusNode @@ -43,6 +42,7 @@ SvTorusKnotNode SvRingNode SvEllipseNode + SvSuperEllipsoidNode SvSmoothLines ## Analyzers diff --git a/nodes/generator/super_ellipsoid.py b/nodes/generators_extended/super_ellipsoid.py similarity index 100% rename from nodes/generator/super_ellipsoid.py rename to nodes/generators_extended/super_ellipsoid.py -- GitLab From 7a51adde841ed8903ed70687a2a078da65570770 Mon Sep 17 00:00:00 2001 From: Ilya Portnov Date: Wed, 6 Mar 2019 22:05:16 +0500 Subject: [PATCH 47/60] Formula Mk3 node, take 1... --- index.md | 1 + nodes/number/formula3.py | 177 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 178 insertions(+) create mode 100644 nodes/number/formula3.py diff --git a/index.md b/index.md index 1a18ecc2b..7742b81dd 100644 --- a/index.md +++ b/index.md @@ -174,6 +174,7 @@ ScalarMathNode SvScalarMathNodeMK2 Formula2Node + SvFormulaNodeMk3 SvExecNodeMod --- GenListRangeIntNode diff --git a/nodes/number/formula3.py b/nodes/number/formula3.py new file mode 100644 index 000000000..437151628 --- /dev/null +++ b/nodes/number/formula3.py @@ -0,0 +1,177 @@ +# ##### 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 * + +import bpy +from bpy.props import BoolProperty, StringProperty, EnumProperty, FloatVectorProperty, IntProperty +from mathutils import Vector, Matrix +import json +import io + +from sverchok.node_tree import SverchCustomTreeNode +from sverchok.data_structure import fullList, updateNode, dataCorrect, match_long_repeat + +def make_functions_dict(*functions): + return dict([(function.__name__, function) for function in functions]) + +# Standard functions which for some reasons are not in the math module +def sign(x): + if x < 0: + return -1 + elif x > 0: + return 1 + else: + return 0 + +# 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, sign, + # From mathutlis module + Vector, Matrix, + # Python type conversions + tuple, list + ) +# Constants +safe_names['e'] = e +safe_names['pi'] = pi + + +def get_variables(string): + root = ast.parse(string, mode='eval') + result = {node.id for node in ast.walk(root) if isinstance(node, ast.Name)} + return result.difference(safe_names.keys()) + +# It could be safer... +def safe_eval(string, variables): + env = dict() + env.update(safe_names) + env.update(variables) + env["__builtins__"] = {} + root = ast.parse(string, mode='eval') + return eval(compile(root, "", 'eval'), env) + +class SvFormulaNodeMk3(bpy.types.Node, SverchCustomTreeNode): + """ + Triggers: Formula + Tooltip: Calculate by custom formula. + """ + bl_idname = 'SvFormulaNodeMk3' + bl_label = 'Formula Mk3' + bl_icon = 'OUTLINER_OB_EMPTY' + + def on_update(self, context): + self.adjust_sockets() + updateNode(self, context) + + formula = StringProperty(default = "x+y", update=on_update) + + def draw_buttons(self, context, layout): + layout.prop(self, "formula", text="") + + def sv_init(self, context): + self.inputs.new('StringsSocket', "x") + + self.outputs.new('StringsSocket', "Result") + + def get_variables(self): + variables = set() + if not self.formula: + return variables + + vs = get_variables(self.formula) + 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.formula: + 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] + #self.debug("get_input: {} => {}".format(var, result[var])) + return result + + def process(self): + + if not self.outputs[0].is_linked: + return + + var_names = self.get_variables() + inputs = self.get_input() + + results = [] + + if var_names: + input_values = [inputs.get(name, []) for name in var_names] + parameters = match_long_repeat(input_values) + else: + parameters = [[[]]] + for values in zip(*parameters): + variables = dict(zip(var_names, values)) + + value = safe_eval(self.formula, variables) + results.append(value) + + self.outputs['Result'].sv_set([results]) + + +def register(): + bpy.utils.register_class(SvFormulaNodeMk3) + + +def unregister(): + bpy.utils.unregister_class(SvFormulaNodeMk3) + -- GitLab From 9e3882fec5b123a3b8bd87cb2ee19756bcb408ac Mon Sep 17 00:00:00 2001 From: Ilya Portnov Date: Wed, 6 Mar 2019 23:34:50 +0500 Subject: [PATCH 48/60] Formula mk3: more extensive features. --- nodes/number/formula3.py | 92 +++++++++++++++++++++++++++++++--------- 1 file changed, 72 insertions(+), 20 deletions(-) diff --git a/nodes/number/formula3.py b/nodes/number/formula3.py index 437151628..23c3238fb 100644 --- a/nodes/number/formula3.py +++ b/nodes/number/formula3.py @@ -27,6 +27,7 @@ import io from sverchok.node_tree import SverchCustomTreeNode from sverchok.data_structure import fullList, updateNode, dataCorrect, match_long_repeat +from sverchok.utils import logging def make_functions_dict(*functions): return dict([(function.__name__, function) for function in functions]) @@ -62,18 +63,25 @@ safe_names['pi'] = pi def get_variables(string): + string = string.strip() + if not len(string): + return set() root = ast.parse(string, mode='eval') result = {node.id for node in ast.walk(root) if isinstance(node, ast.Name)} return result.difference(safe_names.keys()) # It could be safer... def safe_eval(string, variables): - env = dict() - env.update(safe_names) - env.update(variables) - env["__builtins__"] = {} - root = ast.parse(string, mode='eval') - return eval(compile(root, "", 'eval'), env) + try: + env = dict() + env.update(safe_names) + env.update(variables) + env["__builtins__"] = {} + root = ast.parse(string, mode='eval') + return eval(compile(root, "", 'eval'), env) + except SyntaxError as e: + logging.exception(e) + raise Exception("Invalid expression syntax: " + str(e)) class SvFormulaNodeMk3(bpy.types.Node, SverchCustomTreeNode): """ @@ -88,10 +96,48 @@ class SvFormulaNodeMk3(bpy.types.Node, SverchCustomTreeNode): self.adjust_sockets() updateNode(self, context) - formula = StringProperty(default = "x+y", update=on_update) + def on_update_dims(self, context): + if self.dimensions < 4: + self.formula4 = "" + if self.dimensions < 3: + self.formula3 = "" + if self.dimensions < 2: + self.formula2 = "" + + self.adjust_sockets() + updateNode(self, context) + + dimensions = IntProperty(name="Dimensions", default=1, min=1, max=4, update=on_update_dims) + + formula1 = StringProperty(default = "x+y", update=on_update) + formula2 = StringProperty(update=on_update) + formula3 = StringProperty(update=on_update) + formula4 = StringProperty(update=on_update) + + separate = BoolProperty(name="Separate", default=False, update=updateNode) + wrap = BoolProperty(name="Wrap", default=False, update=updateNode) + + def formulas(self): + return [self.formula1, self.formula2, self.formula3, self.formula4] + + def formula(self, k): + return self.formulas()[k] def draw_buttons(self, context, layout): - layout.prop(self, "formula", text="") + layout.prop(self, "formula1", text="") + if self.dimensions > 1: + layout.prop(self, "formula2", text="") + if self.dimensions > 2: + layout.prop(self, "formula3", text="") + if self.dimensions > 3: + layout.prop(self, "formula4", text="") + row = layout.row() + row.prop(self, "separate") + row.prop(self, "wrap") + + def draw_buttons_ext(self, context, layout): + layout.prop(self, "dimensions") + self.draw_buttons(context, layout) def sv_init(self, context): self.inputs.new('StringsSocket', "x") @@ -100,11 +146,10 @@ class SvFormulaNodeMk3(bpy.types.Node, SverchCustomTreeNode): def get_variables(self): variables = set() - if not self.formula: - return variables - vs = get_variables(self.formula) - variables.update(vs) + for formula in self.formulas(): + vs = get_variables(formula) + variables.update(vs) return list(sorted(list(variables))) @@ -121,15 +166,13 @@ class SvFormulaNodeMk3(bpy.types.Node, SverchCustomTreeNode): 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.formula: + if not any(len(formula) for formula in self.formulas()): return self.adjust_sockets() @@ -161,11 +204,20 @@ class SvFormulaNodeMk3(bpy.types.Node, SverchCustomTreeNode): parameters = [[[]]] for values in zip(*parameters): variables = dict(zip(var_names, values)) - - value = safe_eval(self.formula, variables) - results.append(value) - - self.outputs['Result'].sv_set([results]) + vector = [] + for formula in self.formulas(): + if formula: + value = safe_eval(formula, variables) + vector.append(value) + if self.separate: + results.append(vector) + else: + results.extend(vector) + + if self.wrap: + results = [results] + + self.outputs['Result'].sv_set(results) def register(): -- GitLab From 7dc3f982596eff15fc31502642699cff20af0cc1 Mon Sep 17 00:00:00 2001 From: Ilya Portnov Date: Thu, 7 Mar 2019 20:33:30 +0500 Subject: [PATCH 49/60] More correct definition of free variables. --- nodes/number/formula3.py | 87 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 85 insertions(+), 2 deletions(-) diff --git a/nodes/number/formula3.py b/nodes/number/formula3.py index 23c3238fb..43bffb371 100644 --- a/nodes/number/formula3.py +++ b/nodes/number/formula3.py @@ -55,19 +55,102 @@ safe_names = make_functions_dict( # From mathutlis module Vector, Matrix, # Python type conversions - tuple, list + tuple, list, str ) # Constants safe_names['e'] = e safe_names['pi'] = pi +# Blender modules +# Consider this not safe for now +# safe_names["bpy"] = bpy + +class VariableCollector(ast.NodeVisitor): + """ + Visitor class to collect free variable names from the expression. + The problem is that one doesn't just select all names from expression: + there can be local-only variables. + + For example, in + + [g*g for g in lst] + + only "lst" should be considered as a free variable, "g" should be not, + as it is bound by list comprehension scope. + + This implementation is not exactly complete (at list, dictionary comprehensions + are not supported yet). But it works for most cases. + + Please refer to ast.NodeVisitor class documentation for general reference. + """ + def __init__(self): + self.variables = set() + # Stack of local variables + # It is not enough to track just a plain set of names, + # since one name can be re-introduced in the nested scope + self.local_vars = [] + + def push(self, local_vars): + self.local_vars.append(local_vars) + + def pop(self): + return self.local_vars.pop() + + def is_local(self, name): + """ + Check if name is local variable + """ + + for item in self.local_vars: + if name in item: + return True + return False + + def visit_SetComp(self, node): + local_vars = set() + for generator in node.generators: + if isinstance(generator.target, ast.Name): + local_vars.add(generator.target.id) + self.push(local_vars) + self.generic_visit(node) + self.pop() + + def visit_ListComp(self, node): + local_vars = set() + for generator in node.generators: + if isinstance(generator.target, ast.Name): + local_vars.add(generator.target.id) + self.push(local_vars) + self.generic_visit(node) + self.pop() + + def visit_Lambda(self, node): + local_vars = set() + arguments = node.args + for arg in arguments.args: + local_vars.add(arg.id) + if arguments.vararg: + local_vars.add(arguments.vararg.arg) + self.push(local_vars) + self.generic_visit(node) + self.pop() + + def visit_Name(self, node): + name = node.id + if not self.is_local(name): + self.variables.add(name) + + self.generic_visit(node) + def get_variables(string): string = string.strip() if not len(string): return set() root = ast.parse(string, mode='eval') - result = {node.id for node in ast.walk(root) if isinstance(node, ast.Name)} + visitor = VariableCollector() + visitor.visit(root) + result = visitor.variables return result.difference(safe_names.keys()) # It could be safer... -- GitLab From d02448b6f690e84e8c5333a68177f75fde554d68 Mon Sep 17 00:00:00 2001 From: Ilya Portnov Date: Thu, 7 Mar 2019 20:36:47 +0500 Subject: [PATCH 50/60] Add documentation --- docs/nodes/number/formula3.rst | 88 ++++++++++++++++++++++++++++++++++ 1 file changed, 88 insertions(+) create mode 100644 docs/nodes/number/formula3.rst diff --git a/docs/nodes/number/formula3.rst b/docs/nodes/number/formula3.rst new file mode 100644 index 000000000..e05762e4d --- /dev/null +++ b/docs/nodes/number/formula3.rst @@ -0,0 +1,88 @@ +Formula Node Mk3 +================ + +Functionality +------------- + +This node allows one to evaluate (almost) arbitrary Python expressions, using inputs as variables. +It is possible to calculate numeric values, construct lists, tuples, vertices and matrices. + +The node allows to evaluate up to 4 formulas for each set of input values. + +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, sign; +- 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 used formulas. Each variable used in formula becomes one input. If there are no variables used in formula, then this node will have no inputs. + +Parameters +---------- + +This node has the following parameters: + +- **Dimensions**. This parameter is available in the N panel only. It defines how many formulas the node will allow to specify and evaluate. Default value is 1. Maximum value is 4. +- **Formula 1** to **Formula 4** input boxes. Formulas theirselve. If no formula is specified, then nothing will be calculated for this dimension. Number of formula input boxes is defined by **Dimensions** parameter. +- **Separate**. If the flag is set, then for each combination of input values, list of values calculated by formula is enclosed in separate list. Usually you will want to uncheck this if you are using only one formula. Usually you will want to check this if you are using more than one formula. Other combinations can be of use in specific cases. Unchecked by default. +- **Wrap**. If checked, then the whole output of the node will be enclosed in additional brackets. Checked by default. + +For example, let's consider the following setup: + +.. image:: https://user-images.githubusercontent.com/284644/53962080-00c78700-410c-11e9-9563-855fca16537a.png + +Then the following combinations of flags are possible: + ++-----------+-----------+--------------------+ +| Separate | Wrap | Result | ++===========+===========+====================+ +| Checked | Checked | [[[1, 3], [2, 4]]] | ++-----------+-----------+--------------------+ +| Checked | Unchecked | [[1, 3], [2, 4]] | ++-----------+-----------+--------------------+ +| Unchecked | Checked | [[1, 3, 2, 4]] | ++-----------+-----------+--------------------+ +| Unchecked | Unchecked | [1, 3, 2, 4] | ++-----------+-----------+--------------------+ + +Outputs +------- + +**Result** - what we got as result. + +Usage examples +-------------- + +.. image:: https://user-images.githubusercontent.com/284644/53965898-dbd71200-4113-11e9-83c7-cb3c7ced8c1e.png + +.. image:: https://user-images.githubusercontent.com/284644/53967764-9f0d1a00-4117-11e9-92e3-a047dbd2981b.png + -- GitLab From 9d3866bcf4b8130bdfdd2d646599ef92f71f93bb Mon Sep 17 00:00:00 2001 From: Ilya Portnov Date: Fri, 8 Mar 2019 12:18:12 +0500 Subject: [PATCH 51/60] Typo. --- nodes/number/formula3.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nodes/number/formula3.py b/nodes/number/formula3.py index 43bffb371..2f646849e 100644 --- a/nodes/number/formula3.py +++ b/nodes/number/formula3.py @@ -78,7 +78,7 @@ class VariableCollector(ast.NodeVisitor): only "lst" should be considered as a free variable, "g" should be not, as it is bound by list comprehension scope. - This implementation is not exactly complete (at list, dictionary comprehensions + This implementation is not exactly complete (at least, dictionary comprehensions are not supported yet). But it works for most cases. Please refer to ast.NodeVisitor class documentation for general reference. -- GitLab From 909e5a558db4ae8e7793eb20966f6811225938d0 Mon Sep 17 00:00:00 2001 From: Ilya Portnov Date: Fri, 8 Mar 2019 12:22:58 +0500 Subject: [PATCH 52/60] Code documentation. --- nodes/number/formula3.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/nodes/number/formula3.py b/nodes/number/formula3.py index 2f646849e..f5cface60 100644 --- a/nodes/number/formula3.py +++ b/nodes/number/formula3.py @@ -101,8 +101,8 @@ class VariableCollector(ast.NodeVisitor): Check if name is local variable """ - for item in self.local_vars: - if name in item: + for stack_frame in self.local_vars: + if name in stack_frame: return True return False @@ -142,8 +142,10 @@ class VariableCollector(ast.NodeVisitor): self.generic_visit(node) - def get_variables(string): + """ + Get set of free variables used by formula + """ string = string.strip() if not len(string): return set() @@ -155,6 +157,10 @@ def get_variables(string): # It could be safer... def safe_eval(string, variables): + """ + Evaluate expression, allowing only functions known to be "safe" + to be used. + """ try: env = dict() env.update(safe_names) -- GitLab From 763ad51309260fbf321c44d82dc732d20f716d94 Mon Sep 17 00:00:00 2001 From: Ilya Portnov Date: Fri, 8 Mar 2019 12:44:06 +0500 Subject: [PATCH 53/60] Support for easy migration from formula2 to formula3. --- nodes/number/formula2.py | 2 ++ nodes/number/formula3.py | 24 ++++++++++++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/nodes/number/formula2.py b/nodes/number/formula2.py index 443679121..57be5ff8e 100644 --- a/nodes/number/formula2.py +++ b/nodes/number/formula2.py @@ -49,6 +49,8 @@ class Formula2Node(bpy.types.Node, SverchCustomTreeNode): base_name = 'n' multi_socket_type = 'StringsSocket' + replacement_nodes = [('SvFormulaNodeMk3', None, None)] + def draw_buttons(self, context, layout): layout.prop(self, "formula", text="") diff --git a/nodes/number/formula3.py b/nodes/number/formula3.py index f5cface60..495f45301 100644 --- a/nodes/number/formula3.py +++ b/nodes/number/formula3.py @@ -276,6 +276,30 @@ class SvFormulaNodeMk3(bpy.types.Node, SverchCustomTreeNode): #self.debug("get_input: {} => {}".format(var, result[var])) return result + def migrate_from(self, old_node): + if old_node.bl_idname == 'Formula2Node': + formula = old_node.formula + # Older formula node allowed only fixed set of + # variables, with names "x", "n[0]" .. "n[100]". + # Other names could not be considered valid. + k = -1 + for socket in old_node.inputs: + name = socket.name + if k == -1: # First socket name was "x" + new_name = name + else: # Other names was "n[k]", which is syntactically not + # a valid python variable name. + # So we replace all occurences of "n[0]" in formula + # with "n0", and so on. + new_name = "n" + str(k) + + logging.info("Replacing %s with %s", name, new_name) + formula = formula.replace(name, new_name) + k += 1 + + self.formula1 = formula + self.wrap = True + def process(self): if not self.outputs[0].is_linked: -- GitLab From 6ebff79ed0509f4a7310104d6bb1893943867709 Mon Sep 17 00:00:00 2001 From: DolphinDream Date: Fri, 8 Mar 2019 09:08:33 -0500 Subject: [PATCH 54/60] Minor comments & code updates to SuperEllipsoid to keep consistent with other nodes --- .../generators_extended/super_ellipsoid.rst | 2 +- nodes/generators_extended/super_ellipsoid.py | 36 ++++++++++--------- 2 files changed, 20 insertions(+), 18 deletions(-) diff --git a/docs/nodes/generators_extended/super_ellipsoid.rst b/docs/nodes/generators_extended/super_ellipsoid.rst index 7206bb13e..f0450984b 100644 --- a/docs/nodes/generators_extended/super_ellipsoid.rst +++ b/docs/nodes/generators_extended/super_ellipsoid.rst @@ -51,8 +51,8 @@ The node has a set of presets sampling the different shapes the node can generat Extra Parameters ================ -**Cap Top** **Cap Bottom** +**Cap Top** These are the South and North pole caps. For non-zero values of the exponents the caps are very small to notice. Note: The reason for the caps are to avoid collapsing the vertices at the poles. This preserves the quad mesh topology as well as it results in correct normals being generated at the quads at the poles. The caps are generated by default, but they can be hidden as needed. Particularly these are useful for the shapes when the parallel exponent is close to zero (e.g. cylinders) when the caps are large enough to notice. diff --git a/nodes/generators_extended/super_ellipsoid.py b/nodes/generators_extended/super_ellipsoid.py index acbd83bf1..31324fff6 100644 --- a/nodes/generators_extended/super_ellipsoid.py +++ b/nodes/generators_extended/super_ellipsoid.py @@ -56,8 +56,8 @@ def make_verts(sx, sy, sz, xp, xm, np, nm): sx : scale along z xp : parallel exponent xm : meridian exponent - np : number of parallels - nm : number of meridians + np : number of parallels (= number of points in a meridian) + nm : number of meridians (= number of points in a parallel) """ verts = [] for p in range(np): @@ -89,13 +89,13 @@ def make_edges(P, M): """ edge_list = [] - # generate parallels edges (close paths) + # generate PARALLELS edges (close paths) for i in range(P): # for every point on a meridian for j in range(M - 1): # for every point on a parallel (minus last) edge_list.append([i * M + j, i * M + j + 1]) edge_list.append([(i + 1) * M - 1, i * M]) # close the path - # generate meridians edges (open paths) + # generate MERIDIANS edges (open paths) for j in range(M): # for every point on a parallel for i in range(P - 1): # for every point on a meridian (minus last) edge_list.append([i * M + j, (i + 1) * M + j]) @@ -103,11 +103,13 @@ def make_edges(P, M): return edge_list -def make_polys(P, M, cap_top, cap_bottom): +def make_polys(P, M, cap_bottom, cap_top): """ Generate the super-ellipsoid polygons for the given parameters - P : number of parallels - M : number of meridians + P : number of parallels (= number of points in a meridian) + M : number of meridians (= number of points in a parallel) + cap_bottom : turn on/off the bottom cap generation + cap_top : turn on/off the top cap generation """ poly_list = [] @@ -116,14 +118,14 @@ def make_polys(P, M, cap_top, cap_bottom): poly_list.append([i * M + j, i * M + j + 1, (i + 1) * M + j + 1, (i + 1) * M + j]) poly_list.append([(i + 1) * M - 1, i * M, (i + 1) * M, (i + 2) * M - 1]) - if cap_top: - cap = [M * (P - 1) + j for j in range(M)] - poly_list.append(cap) - if cap_bottom: cap = [j for j in reversed(range(M))] poly_list.append(cap) + if cap_top: + cap = [(P - 1) * M + j for j in range(M)] + poly_list.append(cap) + return poly_list @@ -197,14 +199,14 @@ class SvSuperEllipsoidNode(bpy.types.Node, SverchCustomTreeNode): name='Meridians', description="Number of meridians", default=10, min=3, update=update_ellipsoid) - cap_top = BoolProperty( - name='Cap Top', description="Generate top cap", - default=True, update=updateNode) - cap_bottom = BoolProperty( name='Cap Bottom', description="Generate bottom cap", default=True, update=updateNode) + cap_top = BoolProperty( + name='Cap Top', description="Generate top cap", + default=True, update=updateNode) + updating = BoolProperty(default=False) # used for disabling update callback def sv_init(self, context): @@ -230,8 +232,8 @@ class SvSuperEllipsoidNode(bpy.types.Node, SverchCustomTreeNode): def draw_buttons_ext(self, context, layout): column = layout.column(align=True) row = column.row(align=True) - row.prop(self, "cap_top", text="Cap T", toggle=True) row.prop(self, "cap_bottom", text="Cap B", toggle=True) + row.prop(self, "cap_top", text="Cap T", toggle=True) def process(self): if not any(s.is_linked for s in self.outputs): @@ -273,7 +275,7 @@ class SvSuperEllipsoidNode(bpy.types.Node, SverchCustomTreeNode): edges = make_edges(np, nm) edges_list.append(edges) if polys_output_linked: - polys = make_polys(np, nm, self.cap_top, self.cap_bottom) + polys = make_polys(np, nm, self.cap_bottom, self.cap_top) polys_list.append(polys) # outputs -- GitLab From 301874bdd790476c05f0b128f2193fa47b34267f Mon Sep 17 00:00:00 2001 From: DolphinDream Date: Sun, 3 Mar 2019 13:21:34 -0500 Subject: [PATCH 55/60] Add new Cylinder node MK2 (WIP) Extend the features of the first cylinder node. This includes: - show/hide top/bottom caps separately - add twist to the parallel cuts - add overall phase to the parallel cuts - add parallel and meridian profiles to modulate the parallel loops and meridian lines - add overall scale - add centering option --- index.md | 1 + nodes/generator/cylinder_mk2.py | 291 ++++++++++++++++++++++++++++++++ 2 files changed, 292 insertions(+) create mode 100644 nodes/generator/cylinder_mk2.py diff --git a/index.md b/index.md index 7742b81dd..076bba081 100644 --- a/index.md +++ b/index.md @@ -16,6 +16,7 @@ SvBoxNode SvCircleNode CylinderNode + SvCylinderNodeMK2 SphereNode SvIcosphereNode SvTorusNode diff --git a/nodes/generator/cylinder_mk2.py b/nodes/generator/cylinder_mk2.py new file mode 100644 index 000000000..13c1726aa --- /dev/null +++ b/nodes/generator/cylinder_mk2.py @@ -0,0 +1,291 @@ +# ##### 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 bpy +from bpy.props import BoolProperty, IntProperty, FloatProperty + +from sverchok.node_tree import SverchCustomTreeNode +from sverchok.data_structure import (match_long_repeat, updateNode) +from sverchok.utils.geom import CubicSpline + +from math import sin, cos, pi, sqrt + +import numpy as np + + +def get_resample_profile(profile, samples, cycle): + ''' Resample 1D array ''' + N = len(profile) + v = [[n / (N - 1), p, 0] for n, p in enumerate(profile)] + samples_array = np.array(samples).clip(0, 1) + spline = CubicSpline(v, metric="POINTS", is_cyclic=cycle) + out = spline.eval(samples_array) + verts = out.tolist() + + resampled_profile = [v[1] for v in verts] + + return resampled_profile + + +def cylinder_verts(rt, rb, np, nm, h, t, ph, s, profile_p, profile_m, flags): + """ + Generate cylinder vertices for the given parameters + rt : top radius + rb : bottom radius + np : number of parallels + nm : number of meridians + h : height + t : twist from bottom to top + ph : phase + s : scale + """ + separate, center, cyclic = flags + + rt = rt * s + rb = rb * s + h = h * s + + # resample PARALLELS profile + resample_p = [m / nm for m in range(nm + 1)] + resampled_profile_p = get_resample_profile(profile_p, resample_p, cyclic) + # resample MERIDIANS profile + resample_m = [p / np for p in range(np + 1)] + resampled_profile_m = get_resample_profile(profile_m, resample_m, False) + + dA = 2.0 * pi / (nm) # angle increment + dH = h / (np - 1) # height increment + dT = t / (np - 1) # twist increment + dZ = - h / 2 if center else 0 # center offset + + vert_list = [] + addVerts = vert_list.append if separate else vert_list.extend + for p in range(np): + f = p / (np - 1) # interpolation factor between rb and rt + r = (rb * (1 - f) + rt * f) + rp = r * resampled_profile_m[p] # modulate radius by meridian profile + z = dZ + dH * p + phase = ph + dT * p # paralle's total phase (phase + delta twist) + + verts = [] + for m in range(nm): + rpm = rp * resampled_profile_p[m] # modulate radius by parallel profile + + a = phase + dA * m + x = rpm * cos(a) + y = rpm * sin(a) + verts.append([x, y, z]) + + addVerts(verts) + + return vert_list + + +def cylinder_edges(P, M): + """ + Generate cylinder edges for the given parameters + P : number of parallels + M : number of meridians + """ + edge_list = [] + + # generate PARALLELS edges (close loops) + for i in range(P): + for j in range(M - 1): + edge_list.append([i * M + j, i * M + j + 1]) + edge_list.append([i * M + M - 1, i * M]) # close the loop + + # generate MERIDIANS edges + for j in range(M): + for i in range(P - 1): + edge_list.append([i * M + j, (i + 1) * M + j]) + + return edge_list + + +def cylinder_polys(P, M, cap_bottom, cap_top): + """ + Generate cylinder polygons for the given parameters + P : number of parallels + M : number of meridians + """ + poly_list = [] + + for i in range(P - 1): + for j in range(M - 1): + poly_list.append([i * M + j, i * M + j + 1, (i + 1) * M + j + 1, (i + 1) * M + j]) + poly_list.append([(i + 1) * M - 1, i * M, (i + 1) * M, (i + 2) * M - 1]) + + if cap_bottom: + cap = [] + for i in reversed(range(M)): + cap.append(i) + poly_list.append(cap) + + if cap_top: + cap = [] + for i in range(M): + cap.append((P - 1) * M + i) + poly_list.append(cap) + + return poly_list + + +class SvCylinderNodeMK2(bpy.types.Node, SverchCustomTreeNode): + ''' Cylinder ''' + bl_idname = 'SvCylinderNodeMK2' + bl_label = 'Cylinder MK2' + bl_icon = 'MESH_CYLINDER' + + radius_t = FloatProperty( + name='Radius T', description="Top radius", + default=1.0, update=updateNode) + + radius_b = FloatProperty( + name='Radius B', description="Bottom radius", + default=1.0, update=updateNode) + + parallels = IntProperty( + name='Parallels', description="Number of parallels", + default=2, min=2, update=updateNode) + + meridians = IntProperty( + name='Meridians', description="Number of meridians", + default=32, min=3, update=updateNode) + + height = FloatProperty( + name='Height', description="The height of the cylinder", + default=2.0, update=updateNode) + + twist = FloatProperty( + name='Twist', description="The twist of the cylinder", + default=0.0, update=updateNode) + + phase = FloatProperty( + name='Phase', description="The phase of the cylinder", + default=0.0, update=updateNode) + + scale = FloatProperty( + name='Scale', description="The scale of the cylinder", + default=1.0, update=updateNode) + + cap_bottom = BoolProperty( + name='Cap Bottom', description="Generate bottom cap", + default=True, update=updateNode) + + cap_top = BoolProperty( + name='Cap Top', description="Generate top cap", + default=True, update=updateNode) + + separate = BoolProperty( + name='Separate', description='Separate UV coords', + default=False, update=updateNode) + + center = BoolProperty( + name='Center', description='Center cylinder around origin', + default=True, update=updateNode) + + cyclic = BoolProperty( + name='Cyclic', description='Parallels profile is cyclic', + default=True, update=updateNode) + + def sv_init(self, context): + self.inputs.new('StringsSocket', "RadiusT").prop_name = 'radius_t' + self.inputs.new('StringsSocket', "RadiusB").prop_name = 'radius_b' + self.inputs.new('StringsSocket', "Parallels").prop_name = 'parallels' + self.inputs.new('StringsSocket', "Meridians").prop_name = 'meridians' + self.inputs.new('StringsSocket', "Height").prop_name = 'height' + self.inputs.new('StringsSocket', "Twist").prop_name = 'twist' + self.inputs.new('StringsSocket', "Phase").prop_name = 'phase' + self.inputs.new('StringsSocket', "Scale").prop_name = 'scale' + self.inputs.new('StringsSocket', "Parallels Profile") + self.inputs.new('StringsSocket', "Meridians Profile") + + self.outputs.new('VerticesSocket', "Vertices", "Vertices") + self.outputs.new('StringsSocket', "Edges", "Edges") + self.outputs.new('StringsSocket', "Polygons", "Polygons") + + def draw_buttons(self, context, layout): + column = layout.column(align=True) + row = column.row(align=True) + row.prop(self, "cap_bottom", text="Cap B", toggle=True) + row.prop(self, "cap_top", text="Cap T", toggle=True) + row = column.row(align=True) + row.prop(self, "separate", toggle=True) + row.prop(self, "center", toggle=True) + + def draw_buttons_ext(self, context, layout): + layout.prop(self, "cyclic", toggle=True) + + def process(self): + if not any(s.is_linked for s in self.outputs): + return + + inputs = self.inputs + + # read inputs + input_RT = inputs["RadiusT"].sv_get()[0] + input_RB = inputs["RadiusB"].sv_get()[0] + input_NP = inputs["Parallels"].sv_get()[0] + input_NM = inputs["Meridians"].sv_get()[0] + input_H = inputs["Height"].sv_get()[0] + input_T = inputs["Twist"].sv_get()[0] + input_PH = inputs["Phase"].sv_get()[0] + input_S = inputs["Scale"].sv_get()[0] + + profile_p = inputs["Parallels Profile"].sv_get(default=[[1, 1]])[0] + profile_m = inputs["Meridians Profile"].sv_get(default=[[1, 1]])[0] + + # sanitize inputs + input_NP = list(map(lambda n: max(2, n), input_NP)) + input_NM = list(map(lambda m: max(3, m), input_NM)) + + params = match_long_repeat([input_RT, input_RB, + input_NP, input_NM, + input_H, input_T, + input_PH, input_S]) + + flags = [self.separate, self.center, self.cyclic] + + verts_list = [] + edges_list = [] + polys_list = [] + for rt, rb, np, nm, h, t, ph, s in zip(*params): + verts = cylinder_verts(rt, rb, np, nm, h, t, ph, s, profile_p, profile_m, flags) + edges = cylinder_edges(np, nm) + polys = cylinder_polys(np, nm, self.cap_bottom, self.cap_top) + verts_list.append(verts) + edges_list.append(edges) + polys_list.append(polys) + + # outputs + if self.outputs['Vertices'].is_linked: + self.outputs['Vertices'].sv_set(verts_list) + + if self.outputs['Edges'].is_linked: + self.outputs['Edges'].sv_set(edges_list) + + if self.outputs['Polygons'].is_linked: + self.outputs['Polygons'].sv_set(polys_list) + + +def register(): + bpy.utils.register_class(SvCylinderNodeMK2) + + +def unregister(): + bpy.utils.unregister_class(SvCylinderNodeMK2) -- GitLab From 50f7d01289380fae8fddc9b95fb3ddda4284f96b Mon Sep 17 00:00:00 2001 From: DolphinDream Date: Mon, 4 Mar 2019 12:17:24 -0500 Subject: [PATCH 56/60] Various optimizations, cleanup + documentation --- docs/nodes/generator/cylinder_mk2.rst | 84 +++++++++++ nodes/generator/cylinder_mk2.py | 198 +++++++++++++++----------- 2 files changed, 195 insertions(+), 87 deletions(-) create mode 100644 docs/nodes/generator/cylinder_mk2.rst diff --git a/docs/nodes/generator/cylinder_mk2.rst b/docs/nodes/generator/cylinder_mk2.rst new file mode 100644 index 000000000..83341f5c6 --- /dev/null +++ b/docs/nodes/generator/cylinder_mk2.rst @@ -0,0 +1,84 @@ +Cylinder MK2 +============ + +Functionality +------------- + +This node generates primarily cylindrical shapes, but its settings allow to create a wide variety of tube like shapes. + +Inputs +------ + +All inputs are vectorized and they will accept single or multiple values. + +- **Radius Top** +- **Radius Bottom** +- **Parallels** +- **Meridians** +- **Height** +- **Twist** +- **Phase** +- **Scale** +- **Parallels Profile** [1] +- **Meridians Profile** [1] + +[1] : The profiles inputs do not take input values from the node, but can take multiple values form the outside nodes. + +Parameters +---------- + +The **Separate**, **Cap T**, **Cap B** and **Center** can be given by the node only. +The * + ++-----------------------+---------+---------+-------------------------------------------------------+ +| Param | Type | Default | Description | ++=======================+=========+=========+=======================================================+ +| **Radius Top** | Float | 1.00 | The radius of the top parallel. | ++-----------------------+---------+---------+-------------------------------------------------------+ +| **Radius Bottom** | Float | 1.00 | The radius of the bottom parallel | ++-----------------------+---------+---------+-------------------------------------------------------+ +| **Parallels** | Int | 2 | The number of parallel lines. | +| | | | This is also the number of points in a meridian. | ++-----------------------+---------+---------+-------------------------------------------------------+ +| **Meridians** | Int | 32 | The number of meridian lines. | +| | | | This is also the number of points in a parallel. | ++-----------------------+---------+---------+-------------------------------------------------------+ +| **Height** | Float | 2.00 | The height of the cylinder. | ++-----------------------+---------+---------+-------------------------------------------------------+ +| **Twist** | Float | 0.0 | The amount of twist of the parallel lines around the | +| | | | z-axis from the bottom parallel to the top parallel. | ++-----------------------+---------+---------+-------------------------------------------------------+ +| **Phase** | Float | 0.0 | The amount of rotation around the z-axis of all the | +| | | | vertices in the mesh. | ++-----------------------+---------+---------+-------------------------------------------------------+ +| **Parallels Profile** | [Float] | [1,1] | The scale modulating the radius of the parallel lines.| ++-----------------------+---------+---------+-------------------------------------------------------+ +| **Meridians Profile** | [Float] | [1,1] | The scale modulating the radius of the meridian lines.| ++-----------------------+---------+---------+-------------------------------------------------------+ +| **Caps T** | Boolean | True | Generate the top cap or not. | ++-----------------------+---------+---------+-------------------------------------------------------+ +| **Caps B** | Boolean | True | Generate the bottom cap or not. | ++-----------------------+---------+---------+-------------------------------------------------------+ +| **Separate** | Boolean | False | Separate the parallels into separate lists. | ++-----------------------+---------+---------+-------------------------------------------------------+ +| **Center** | Boolean | True | Center the cylinder around origin. | ++-----------------------+---------+---------+-------------------------------------------------------+ + + +Extended Parameters +---------- +The Property Panel provides additional parameters. + +**Cyclic** +This parameter is used for treating the connected **Parallels Profile** as a cyclic spline curve and it is useful to ensure smooth parallel curves whenever there's discontinuity in the value and the tangent of the starting/ending points of the profile. + + +Outputs +------- + +**Vertices**, **Edges** and **Polygons**. +All outputs will be generated when the output sockets are connected. +Depending on the type of the inputs the node will generate only one or multiples independant cylinders. +If **Separate** is True, the node will output the parallels as separate lists. + + diff --git a/nodes/generator/cylinder_mk2.py b/nodes/generator/cylinder_mk2.py index 13c1726aa..3d95b9883 100644 --- a/nodes/generator/cylinder_mk2.py +++ b/nodes/generator/cylinder_mk2.py @@ -20,20 +20,19 @@ import bpy from bpy.props import BoolProperty, IntProperty, FloatProperty from sverchok.node_tree import SverchCustomTreeNode -from sverchok.data_structure import (match_long_repeat, updateNode) +from sverchok.data_structure import (match_long_repeat, updateNode, get_edge_loop) from sverchok.utils.geom import CubicSpline -from math import sin, cos, pi, sqrt +from math import sin, cos, pi import numpy as np - -def get_resample_profile(profile, samples, cycle): +def resample_1D_array(profile, samples, cyclic): ''' Resample 1D array ''' N = len(profile) v = [[n / (N - 1), p, 0] for n, p in enumerate(profile)] samples_array = np.array(samples).clip(0, 1) - spline = CubicSpline(v, metric="POINTS", is_cyclic=cycle) + spline = CubicSpline(v, metric="POINTS", is_cyclic=cyclic) out = spline.eval(samples_array) verts = out.tolist() @@ -42,17 +41,19 @@ def get_resample_profile(profile, samples, cycle): return resampled_profile -def cylinder_verts(rt, rb, np, nm, h, t, ph, s, profile_p, profile_m, flags): +def make_verts(rt, rb, np, nm, h, t, ph, s, profile_p, profile_m, flags): """ Generate cylinder vertices for the given parameters rt : top radius rb : bottom radius - np : number of parallels - nm : number of meridians + np : number of parallels (= number of points in a meridian) + nm : number of meridians (= number of points in a parallel) h : height - t : twist from bottom to top - ph : phase - s : scale + t : twist (rotate parallel verts by this angle around Z from bottom to top) + ph : phase (rotate all verts by this angle around Z axis) + s : scale the entire mesh (radii & height) + profile_p : parallels profile + profile_m : meridians profile """ separate, center, cyclic = flags @@ -60,93 +61,109 @@ def cylinder_verts(rt, rb, np, nm, h, t, ph, s, profile_p, profile_m, flags): rb = rb * s h = h * s - # resample PARALLELS profile - resample_p = [m / nm for m in range(nm + 1)] - resampled_profile_p = get_resample_profile(profile_p, resample_p, cyclic) - # resample MERIDIANS profile - resample_m = [p / np for p in range(np + 1)] - resampled_profile_m = get_resample_profile(profile_m, resample_m, False) - - dA = 2.0 * pi / (nm) # angle increment - dH = h / (np - 1) # height increment - dT = t / (np - 1) # twist increment + if len(profile_p) < 2: # no profile given (make profile all ones) + resampled_profile_p = [1] * nm + else: + # resample PARALLELS profile to nm parallel points [0-1] + samples = [m / nm for m in range(nm + 1)] + resampled_profile_p = resample_1D_array(profile_p, samples, cyclic) + + if len(profile_m) < 2: # no profile given (make profile all ones) + resampled_profile_m = [1] * np + else: + # resample MERIDIANS profile to np meridian points [0-1) + samples = [p / (np - 1) for p in range(np)] + resampled_profile_m = resample_1D_array(profile_m, samples, False) + + dA = 2.0 * pi / nm # angle increment from one meridian to the next + dH = h / (np - 1) # height increment from one parallel to the next + dT = t / (np - 1) # twist increment from one parallel to the next dZ = - h / 2 if center else 0 # center offset vert_list = [] - addVerts = vert_list.append if separate else vert_list.extend - for p in range(np): + add_verts = vert_list.append if separate else vert_list.extend + for p in range(np): # for every point on a meridian (traverse the parallels) f = p / (np - 1) # interpolation factor between rb and rt - r = (rb * (1 - f) + rt * f) + r = rb * (1 - f) + rt * f # interpolated radius between bottom and top radii rp = r * resampled_profile_m[p] # modulate radius by meridian profile z = dZ + dH * p - phase = ph + dT * p # paralle's total phase (phase + delta twist) + phase = ph + dT * p # parallel's total phase (phase + delta twist) verts = [] - for m in range(nm): + for m in range(nm): # for every point on a parallel (traverse the meridians) rpm = rp * resampled_profile_p[m] # modulate radius by parallel profile - a = phase + dA * m x = rpm * cos(a) y = rpm * sin(a) verts.append([x, y, z]) - addVerts(verts) + add_verts(verts) return vert_list -def cylinder_edges(P, M): +def make_edges(P, M, separate): """ - Generate cylinder edges for the given parameters - P : number of parallels - M : number of meridians + Generate the cylinder edges for the given parameters + P : number of parallels (= number of points in a meridian) + M : number of meridians (= number of points in a parallel) """ edge_list = [] - # generate PARALLELS edges (close loops) - for i in range(P): - for j in range(M - 1): - edge_list.append([i * M + j, i * M + j + 1]) - edge_list.append([i * M + M - 1, i * M]) # close the loop - - # generate MERIDIANS edges - for j in range(M): - for i in range(P - 1): - edge_list.append([i * M + j, (i + 1) * M + j]) + if separate: # replicate edges in one parallel for every meridian point + edge_list = [get_edge_loop(M)] * P + else: + add_edge = edge_list.append + # generate PARALLELS edges (close paths) + for i in range(P): # for every point on a meridian + for j in range(M - 1): # for every point on a parallel (minus last) + add_edge([i * M + j, i * M + j + 1]) + add_edge([(i + 1) * M - 1, i * M]) # close the path + + # generate MERIDIANS edges (open paths) + for j in range(M): # for every point on a parallel + for i in range(P - 1): # for every point on a meridian (minus last) + add_edge([i * M + j, (i + 1) * M + j]) return edge_list -def cylinder_polys(P, M, cap_bottom, cap_top): +def make_polys(P, M, cap_bottom, cap_top, separate): """ - Generate cylinder polygons for the given parameters - P : number of parallels - M : number of meridians + Generate the cylinder polygons for the given parameters + P : number of parallels (= number of points in a meridian) + M : number of meridians (= number of points in a parallel) + cap_bottom : turn on/off the bottom cap generation + cap_top : turn on/off the top cap generation """ poly_list = [] - for i in range(P - 1): - for j in range(M - 1): - poly_list.append([i * M + j, i * M + j + 1, (i + 1) * M + j + 1, (i + 1) * M + j]) - poly_list.append([(i + 1) * M - 1, i * M, (i + 1) * M, (i + 2) * M - 1]) + if separate: + poly_list = [[list(range(M))]] * P + else: + add_poly = poly_list.append + for i in range(P - 1): + for j in range(M - 1): + add_poly([i * M + j, i * M + j + 1, (i + 1) * M + j + 1, (i + 1) * M + j]) + add_poly([(i + 1) * M - 1, i * M, (i + 1) * M, (i + 2) * M - 1]) - if cap_bottom: - cap = [] - for i in reversed(range(M)): - cap.append(i) - poly_list.append(cap) + if cap_bottom: + cap = [j for j in reversed(range(M))] + add_poly(cap) - if cap_top: - cap = [] - for i in range(M): - cap.append((P - 1) * M + i) - poly_list.append(cap) + if cap_top: + offset = (P - 1) * M + cap = [offset + j for j in range(M)] + add_poly(cap) return poly_list class SvCylinderNodeMK2(bpy.types.Node, SverchCustomTreeNode): - ''' Cylinder ''' + """ + Triggers: Cylinder, Tube + Tooltip: Generate cylinder based meshes + """ bl_idname = 'SvCylinderNodeMK2' bl_label = 'Cylinder MK2' bl_icon = 'MESH_CYLINDER' @@ -238,48 +255,55 @@ class SvCylinderNodeMK2(bpy.types.Node, SverchCustomTreeNode): inputs = self.inputs # read inputs - input_RT = inputs["RadiusT"].sv_get()[0] - input_RB = inputs["RadiusB"].sv_get()[0] - input_NP = inputs["Parallels"].sv_get()[0] - input_NM = inputs["Meridians"].sv_get()[0] - input_H = inputs["Height"].sv_get()[0] - input_T = inputs["Twist"].sv_get()[0] - input_PH = inputs["Phase"].sv_get()[0] - input_S = inputs["Scale"].sv_get()[0] - - profile_p = inputs["Parallels Profile"].sv_get(default=[[1, 1]])[0] - profile_m = inputs["Meridians Profile"].sv_get(default=[[1, 1]])[0] + input_rt = inputs["RadiusT"].sv_get()[0] + input_rb = inputs["RadiusB"].sv_get()[0] + input_np = inputs["Parallels"].sv_get()[0] + input_nm = inputs["Meridians"].sv_get()[0] + input_h = inputs["Height"].sv_get()[0] + input_t = inputs["Twist"].sv_get()[0] + input_ph = inputs["Phase"].sv_get()[0] + input_s = inputs["Scale"].sv_get()[0] + + profile_p = inputs["Parallels Profile"].sv_get(default=[[]])[0] + profile_m = inputs["Meridians Profile"].sv_get(default=[[]])[0] # sanitize inputs - input_NP = list(map(lambda n: max(2, n), input_NP)) - input_NM = list(map(lambda m: max(3, m), input_NM)) + input_np = list(map(lambda n: max(2, n), input_np)) + input_nm = list(map(lambda m: max(3, m), input_nm)) - params = match_long_repeat([input_RT, input_RB, - input_NP, input_NM, - input_H, input_T, - input_PH, input_S]) + params = match_long_repeat([input_rt, input_rb, + input_np, input_nm, + input_h, input_t, + input_ph, input_s]) flags = [self.separate, self.center, self.cyclic] + verts_output_linked = self.outputs['Vertices'].is_linked + edges_output_linked = self.outputs['Edges'].is_linked + polys_output_linked = self.outputs['Polygons'].is_linked + verts_list = [] edges_list = [] polys_list = [] for rt, rb, np, nm, h, t, ph, s in zip(*params): - verts = cylinder_verts(rt, rb, np, nm, h, t, ph, s, profile_p, profile_m, flags) - edges = cylinder_edges(np, nm) - polys = cylinder_polys(np, nm, self.cap_bottom, self.cap_top) - verts_list.append(verts) - edges_list.append(edges) - polys_list.append(polys) + if verts_output_linked: + verts = make_verts(rt, rb, np, nm, h, t, ph, s, profile_p, profile_m, flags) + verts_list.append(verts) + if edges_output_linked: + edges = make_edges(np, nm, self.separate) + edges_list.append(edges) + if polys_output_linked: + polys = make_polys(np, nm, self.cap_bottom, self.cap_top, self.separate) + polys_list.append(polys) # outputs - if self.outputs['Vertices'].is_linked: + if verts_output_linked: self.outputs['Vertices'].sv_set(verts_list) - if self.outputs['Edges'].is_linked: + if edges_output_linked: self.outputs['Edges'].sv_set(edges_list) - if self.outputs['Polygons'].is_linked: + if polys_output_linked: self.outputs['Polygons'].sv_set(polys_list) -- GitLab From d542b3c2509459184c672a52456fb1b8a5cc45ea Mon Sep 17 00:00:00 2001 From: DolphinDream Date: Sat, 9 Mar 2019 20:38:53 -0500 Subject: [PATCH 57/60] Add angle units option --- docs/nodes/generator/cylinder_mk2.rst | 33 +++++++---- nodes/generator/cylinder_mk2.py | 82 ++++++++++++++++++++++++--- 2 files changed, 95 insertions(+), 20 deletions(-) diff --git a/docs/nodes/generator/cylinder_mk2.rst b/docs/nodes/generator/cylinder_mk2.rst index 83341f5c6..117b64c19 100644 --- a/docs/nodes/generator/cylinder_mk2.rst +++ b/docs/nodes/generator/cylinder_mk2.rst @@ -27,15 +27,14 @@ All inputs are vectorized and they will accept single or multiple values. Parameters ---------- -The **Separate**, **Cap T**, **Cap B** and **Center** can be given by the node only. -The * +The **Separate**, **Cap T**, **Cap B**, **Center** and the **Angle Units** can be given by the node only. The other parameters can take values either from the node or the external nodes. +-----------------------+---------+---------+-------------------------------------------------------+ | Param | Type | Default | Description | +=======================+=========+=========+=======================================================+ | **Radius Top** | Float | 1.00 | The radius of the top parallel. | +-----------------------+---------+---------+-------------------------------------------------------+ -| **Radius Bottom** | Float | 1.00 | The radius of the bottom parallel | +| **Radius Bottom** | Float | 1.00 | The radius of the bottom parallel. | +-----------------------+---------+---------+-------------------------------------------------------+ | **Parallels** | Int | 2 | The number of parallel lines. | | | | | This is also the number of points in a meridian. | @@ -45,24 +44,33 @@ The * +-----------------------+---------+---------+-------------------------------------------------------+ | **Height** | Float | 2.00 | The height of the cylinder. | +-----------------------+---------+---------+-------------------------------------------------------+ -| **Twist** | Float | 0.0 | The amount of twist of the parallel lines around the | -| | | | z-axis from the bottom parallel to the top parallel. | +| **Twist** | Float | 0.0 | The amount of rotation around the z-axis of the | +| | | | parallel lines from the bottom to the top parallel. | +-----------------------+---------+---------+-------------------------------------------------------+ | **Phase** | Float | 0.0 | The amount of rotation around the z-axis of all the | | | | | vertices in the mesh. | +-----------------------+---------+---------+-------------------------------------------------------+ -| **Parallels Profile** | [Float] | [1,1] | The scale modulating the radius of the parallel lines.| +| **Parallels Profile** | [Float] | [ ] | The scale modulating the radius of the parallels. [1] | +-----------------------+---------+---------+-------------------------------------------------------+ -| **Meridians Profile** | [Float] | [1,1] | The scale modulating the radius of the meridian lines.| +| **Meridians Profile** | [Float] | [ ] | The scale modulating the radius of the meridians. [1] | +-----------------------+---------+---------+-------------------------------------------------------+ | **Caps T** | Boolean | True | Generate the top cap or not. | +-----------------------+---------+---------+-------------------------------------------------------+ | **Caps B** | Boolean | True | Generate the bottom cap or not. | +-----------------------+---------+---------+-------------------------------------------------------+ -| **Separate** | Boolean | False | Separate the parallels into separate lists. | +| **Separate** | Boolean | False | Separate the parallels into separate lists. [2] | +-----------------------+---------+---------+-------------------------------------------------------+ -| **Center** | Boolean | True | Center the cylinder around origin. | +| **Center** | Boolean | True | Center the cylinder around the origin. | +-----------------------+---------+---------+-------------------------------------------------------+ +| **Angle Units** | Enum | RAD | The **Twist** and **Phase** angles are interpreted as | +| | RAD | | RAD : Radians [0, 2*pi] | +| | DEG | | DEG : Degrees [0, 360] | +| | UNI | | UNI : Unities [0, 1] | ++-----------------------+---------+---------+-------------------------------------------------------+ + +Notes: +[1] : If connected the profiles need to have lists of at least 2 values. +[2] : This splits the verts, edges, and polys into separate parallel sections. Extended Parameters @@ -70,7 +78,7 @@ Extended Parameters The Property Panel provides additional parameters. **Cyclic** -This parameter is used for treating the connected **Parallels Profile** as a cyclic spline curve and it is useful to ensure smooth parallel curves whenever there's discontinuity in the value and the tangent of the starting/ending points of the profile. +This parameter is used for treating the connected **Parallels Profile** as a cyclic spline curve and it is useful to ensure smooth parallel curves whenever there's discontinuity in the value and/or in the tangent of the starting/ending points of the profile. Outputs @@ -78,7 +86,8 @@ Outputs **Vertices**, **Edges** and **Polygons**. All outputs will be generated when the output sockets are connected. -Depending on the type of the inputs the node will generate only one or multiples independant cylinders. -If **Separate** is True, the node will output the parallels as separate lists. +Depending on the type of the inputs the node will generate only one or multiples independant cylinder shapes. + +If **Separate** is True, the node will output the parallel sections as separate lists (verts, edges and polys). diff --git a/nodes/generator/cylinder_mk2.py b/nodes/generator/cylinder_mk2.py index 3d95b9883..9a3713034 100644 --- a/nodes/generator/cylinder_mk2.py +++ b/nodes/generator/cylinder_mk2.py @@ -17,7 +17,7 @@ # ##### END GPL LICENSE BLOCK ##### import bpy -from bpy.props import BoolProperty, IntProperty, FloatProperty +from bpy.props import BoolProperty, IntProperty, FloatProperty, EnumProperty from sverchok.node_tree import SverchCustomTreeNode from sverchok.data_structure import (match_long_repeat, updateNode, get_edge_loop) @@ -27,6 +27,15 @@ from math import sin, cos, pi import numpy as np +angle_unit_items = [ + ("RAD", "Rad", "Radians", "", 0), + ("DEG", "Deg", 'Degrees', "", 1), + ("UNI", "Uni", 'Unities', "", 2) +] + +angle_conversion = {"RAD": 1.0, "DEG": pi / 180.0, "UNI": 2 * pi} + + def resample_1D_array(profile, samples, cyclic): ''' Resample 1D array ''' N = len(profile) @@ -63,15 +72,13 @@ def make_verts(rt, rb, np, nm, h, t, ph, s, profile_p, profile_m, flags): if len(profile_p) < 2: # no profile given (make profile all ones) resampled_profile_p = [1] * nm - else: - # resample PARALLELS profile to nm parallel points [0-1] + else: # resample PARALLELS profile to nm parallel points [0-1] samples = [m / nm for m in range(nm + 1)] resampled_profile_p = resample_1D_array(profile_p, samples, cyclic) if len(profile_m) < 2: # no profile given (make profile all ones) resampled_profile_m = [1] * np - else: - # resample MERIDIANS profile to np meridian points [0-1) + else: # resample MERIDIANS profile to np meridian points [0-1) samples = [p / (np - 1) for p in range(np)] resampled_profile_m = resample_1D_array(profile_m, samples, False) @@ -107,6 +114,7 @@ def make_edges(P, M, separate): Generate the cylinder edges for the given parameters P : number of parallels (= number of points in a meridian) M : number of meridians (= number of points in a parallel) + separate: split the parallels into separate edge lists """ edge_list = [] @@ -135,6 +143,7 @@ def make_polys(P, M, cap_bottom, cap_top, separate): M : number of meridians (= number of points in a parallel) cap_bottom : turn on/off the bottom cap generation cap_top : turn on/off the top cap generation + separate: split the parallels into separate poly lists """ poly_list = [] @@ -168,6 +177,56 @@ class SvCylinderNodeMK2(bpy.types.Node, SverchCustomTreeNode): bl_label = 'Cylinder MK2' bl_icon = 'MESH_CYLINDER' + def update_angles(self, context): + """ Convert angle values to selected angle units """ + if self.last_angle_units == "RAD": + if self.angle_units == "RAD": + au = 1.0 # RAD -> RAD + elif self.angle_units == "DEG": + au = 180.0 / pi # RAD -> DEG + elif self.angle_units == "UNI": + au = 0.5 / pi # RAD -> UNI + + elif self.last_angle_units == "DEG": + if self.angle_units == "RAD": + au = pi / 180 # DEG -> RAD + elif self.angle_units == "DEG": + au = 1.0 # DEG -> DEG + elif self.angle_units == "UNI": + au = 1.0 / 360 # DEG -> UNI + + elif self.last_angle_units == "UNI": + if self.angle_units == "RAD": + au = 2 * pi # UNI -> RAD + elif self.angle_units == "DEG": + au = 360 # UNI -> DEG + elif self.angle_units == "UNI": + au = 1.0 # UNI -> UNI + + self.last_angle_units = self.angle_units + + self.updating = True # inhibit update calls + + self.twist = au * self.twist # convert to current angle units + self.phase = au * self.phase # convert to current angle units + + self.updating = False + updateNode(self, context) + + def update_cylinder(self, context): + if self.updating: + return + + updateNode(self, context) + + angle_units = EnumProperty( + name="Angle Units", description="Angle units (radians/degrees/unities)", + default="RAD", items=angle_unit_items, update=update_angles) + + last_angle_units = EnumProperty( + name="Last Angle Units", description="Last angle units (radians/degrees/unities)", + default="RAD", items=angle_unit_items) # used for updates when changing angle units + radius_t = FloatProperty( name='Radius T', description="Top radius", default=1.0, update=updateNode) @@ -190,11 +249,11 @@ class SvCylinderNodeMK2(bpy.types.Node, SverchCustomTreeNode): twist = FloatProperty( name='Twist', description="The twist of the cylinder", - default=0.0, update=updateNode) + default=0.0, update=update_cylinder) phase = FloatProperty( name='Phase', description="The phase of the cylinder", - default=0.0, update=updateNode) + default=0.0, update=update_cylinder) scale = FloatProperty( name='Scale', description="The scale of the cylinder", @@ -220,6 +279,9 @@ class SvCylinderNodeMK2(bpy.types.Node, SverchCustomTreeNode): name='Cyclic', description='Parallels profile is cyclic', default=True, update=updateNode) + updating = BoolProperty( + name="Updating", description="Flag to inhibit updating", default=False) + def sv_init(self, context): self.inputs.new('StringsSocket', "RadiusT").prop_name = 'radius_t' self.inputs.new('StringsSocket', "RadiusB").prop_name = 'radius_b' @@ -244,6 +306,8 @@ class SvCylinderNodeMK2(bpy.types.Node, SverchCustomTreeNode): row = column.row(align=True) row.prop(self, "separate", toggle=True) row.prop(self, "center", toggle=True) + row = layout.row(align=True) + row.prop(self, "angle_units", expand=True) def draw_buttons_ext(self, context, layout): layout.prop(self, "cyclic", toggle=True) @@ -278,6 +342,8 @@ class SvCylinderNodeMK2(bpy.types.Node, SverchCustomTreeNode): flags = [self.separate, self.center, self.cyclic] + au = angle_conversion[self.angle_units] # angle conversion to radians + verts_output_linked = self.outputs['Vertices'].is_linked edges_output_linked = self.outputs['Edges'].is_linked polys_output_linked = self.outputs['Polygons'].is_linked @@ -287,7 +353,7 @@ class SvCylinderNodeMK2(bpy.types.Node, SverchCustomTreeNode): polys_list = [] for rt, rb, np, nm, h, t, ph, s in zip(*params): if verts_output_linked: - verts = make_verts(rt, rb, np, nm, h, t, ph, s, profile_p, profile_m, flags) + verts = make_verts(rt, rb, np, nm, h, t * au, ph * au, s, profile_p, profile_m, flags) verts_list.append(verts) if edges_output_linked: edges = make_edges(np, nm, self.separate) -- GitLab From 470aff10ab85468da7c7f4058e43665d6fe3f169 Mon Sep 17 00:00:00 2001 From: DolphinDream Date: Tue, 8 Jan 2019 19:10:22 -0500 Subject: [PATCH 58/60] Minor updates to quaternion nodes --- nodes/quaternion/quaternion_in.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/nodes/quaternion/quaternion_in.py b/nodes/quaternion/quaternion_in.py index df8f47d1f..e28a22c74 100644 --- a/nodes/quaternion/quaternion_in.py +++ b/nodes/quaternion/quaternion_in.py @@ -143,17 +143,22 @@ class SvQuaternionInNode(bpy.types.Node, SverchCustomTreeNode): default=False, update=updateNode) def sv_init(self, context): + # component inputs self.inputs.new('StringsSocket', "W").prop_name = 'component_w' self.inputs.new('StringsSocket', "X").prop_name = 'component_x' self.inputs.new('StringsSocket', "Y").prop_name = 'component_y' self.inputs.new('StringsSocket', "Z").prop_name = 'component_z' + # scalar-vector inputs self.inputs.new('StringsSocket', "Scalar").prop_name = 'scalar' self.inputs.new('VerticesSocket', "Vector").prop_name = "vector" + # euler angles inputs self.inputs.new('StringsSocket', "Angle X").prop_name = 'angle_x' self.inputs.new('StringsSocket', "Angle Y").prop_name = 'angle_y' self.inputs.new('StringsSocket', "Angle Z").prop_name = 'angle_z' + # axis-angle inputs self.inputs.new('VerticesSocket', "Axis").prop_name = "axis" self.inputs.new('StringsSocket', "Angle").prop_name = 'angle' + # matrix input self.inputs.new('MatrixSocket', "Matrix") self.outputs.new('SvQuaternionSocket', "Quaternions") -- GitLab From 80779e8a4c324a6227b6a7d5d7f0707e810ed8f2 Mon Sep 17 00:00:00 2001 From: DolphinDream Date: Fri, 7 Dec 2018 22:11:36 -0500 Subject: [PATCH 59/60] Add MK4 version of the line node - reorganize and optimize the code for faster line generation (also uses the optimized edge generation code) - update the UI to make AB and OD the only two modes of the line. - update the UI to make the X/Y/Z presets, instead of modes, to set the V1/V2 vectors to proper x/y/z directions. - the input sockets V1/V2 do not disconnect anymore on setting the line direction to X/Y/Z (since these are just presets of the OD mode). --- docs/nodes/generator/line_mk4.rst | 96 +++++++++++ index.md | 1 + nodes/generator/line_mk4.py | 265 ++++++++++++++++++++++++++++++ 3 files changed, 362 insertions(+) create mode 100644 docs/nodes/generator/line_mk4.rst create mode 100644 nodes/generator/line_mk4.py diff --git a/docs/nodes/generator/line_mk4.rst b/docs/nodes/generator/line_mk4.rst new file mode 100644 index 000000000..0139b0c63 --- /dev/null +++ b/docs/nodes/generator/line_mk4.rst @@ -0,0 +1,96 @@ +Line +==== + +Functionality +------------- + +Line generator creates a series of connected segments based on the number of vertices and the length between them. + +Inputs +------ + +All parameters except **Center**, **Mode** and **Normalize** are vectorized. They will accept single or multiple values. The **Num Verts**, **Steps** and **Size** inputs will accept a single number, an array of numbers or an array of array of numbers, e.g. + + [[2]] + [[2, 4, 6]] + [[2], [4], [6]] + +Parameters +---------- + +All parameters except **Center**, **Mode** and **Normalize** can be given by the node or an external input. + ++---------------+-----------+-----------+--------------------------------------------------------+ +| Param | Type | Default | Description | ++===============+===========+===========+========================================================+ +| **Mode** | Enum | "OD" | The line direction mode: | +| | | | AB : create line from point A to point B | +| | | | OD : create line from origin O, along direction D | ++---------------+-----------+-----------+--------------------------------------------------------+ +| **Num Verts** | Int | 2 | The number of vertices in the line. [1] | ++---------------+-----------+-----------+--------------------------------------------------------+ +| **Step** | Float | 1.00 | The length between vertices. [2] | ++---------------+-----------+-----------+--------------------------------------------------------+ +| **Center** | Boolean  | False     | Center the line around its starting point location. | ++---------------+-----------+-----------+--------------------------------------------------------+ +| **Normalize** | Boolean  | False     | Normalize the line length to the given line size. | ++---------------+-----------+-----------+--------------------------------------------------------+ +| **Size** | Float  | 10.00   | The length of the normalized line. [3] | ++---------------+-----------+-----------+--------------------------------------------------------+ +| **Point A** | Vector  | (0,0,0)   | The starting point of the line. [4] | ++---------------+-----------+-----------+--------------------------------------------------------+ +| **Point B** | Vector  | (1,0,0) | The ending point of the line. [4] | ++---------------+-----------+-----------+--------------------------------------------------------+ +| **Origin** | Vector  | (0,0,0)   | The origin of the line. [5] | ++---------------+-----------+-----------+--------------------------------------------------------+ +| **Direction** | Vector  | (1,0,0) | The direction of the line. [5] | ++---------------+-----------+-----------+--------------------------------------------------------+ + +Notes: +[1] : The minimum number of vertices is 2. +[2] : The number of steps is limited by the Num Verts +[3] : The "Size" input socket is only visible when the "Normalize" flag is enabled. +[4] : Point A and Point B are displayed in AB mode +[5] : Origin and Direction are displayed in OD mode + +Outputs +------- + +**Vertices** and **Edges** will be generated when the output sockets are connected. Depending on the inputs, the node will generate one or multiples independent lines. See examples below. + +Presets +------- +The node provides a set of predefined line directions along X, Y and Z. These buttons will set the mode to **OD**, the **Origin** to (0,0,0) and the **Direction** to one of the X, Y or Z directions: (1,0,0), (0,1,0) and (0,0,1) respectively. The preset buttons are only visible as long as the Point B or Direction input socket is not connected. + +Example of usage +---------------- + +.. image:: https://user-images.githubusercontent.com/10011941/47713459-a177d880-dc3a-11e8-935b-a2fa494dc49b.png + :alt: LineDemo1.PNG + +The first example shows just an standard line with 5 vertices and 1.00 ud between them + +.. image:: https://user-images.githubusercontent.com/10011941/47713473-a9377d00-dc3a-11e8-94ab-39095761788c.png + :alt: LineDemo2.PNG + +In this example the step is given by a series of numbers + +.. image:: https://user-images.githubusercontent.com/10011941/47713477-ad639a80-dc3a-11e8-9884-6568326d2a33.png + :alt: LineDemo3.PNG + +You can create multiple lines if input multiple lists + +.. image:: https://user-images.githubusercontent.com/10011941/47713487-b3597b80-dc3a-11e8-996b-17edf1cec9da.png + :alt: LineDemo4.PNG + +The AB mode will output a divided segment for each vector pair, the step can be used to change the proportions of the divisions + +.. image:: https://user-images.githubusercontent.com/10011941/47713488-b3597b80-dc3a-11e8-9e6e-f742d0338ba5.png + :alt: LineDemo5.PNG + +The "OD" mode can be used to visualize normals + +.. image:: https://user-images.githubusercontent.com/10011941/47713490-b3597b80-dc3a-11e8-9b6d-b937c0375ec5.png + :alt: LineDemo5.PNG + +Advanced example using the node to create a paraboloid grid \ No newline at end of file diff --git a/index.md b/index.md index 076bba081..1aa1b5755 100644 --- a/index.md +++ b/index.md @@ -11,6 +11,7 @@ ## Generator SvLineNodeMK3 + SvLineNodeMK4 SvPlaneNodeMK2 SvNGonNode SvBoxNode diff --git a/nodes/generator/line_mk4.py b/nodes/generator/line_mk4.py new file mode 100644 index 000000000..9ce573969 --- /dev/null +++ b/nodes/generator/line_mk4.py @@ -0,0 +1,265 @@ +# ##### 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 bpy +from bpy.props import IntProperty, FloatProperty, BoolProperty, EnumProperty, FloatVectorProperty, StringProperty +from sverchok.node_tree import SverchCustomTreeNode +from sverchok.data_structure import updateNode, fullList, match_long_repeat, update_edge_cache, get_edge_list +from sverchok.utils.sv_operator_mixins import SvGenericCallbackWithParams +from math import sqrt + +modeItems = [ + ("AB", "AB", "Point A to Point B", 0), + ("OD", "OD", "Origin O in direction D", 1)] + +directions = {"X": [1, 0, 0], "Y": [0, 1, 0], "Z": [0, 0, 1]} + +socket_names = {"A": "Point A", "B": "Point B", "O": "Origin", "D": "Distance"} + + +def get_vector_interpolator(ox, oy, oz, nx, ny, nz): + ''' Get the optimal vector interpolator to speed up the line generation ''' + if nx == 0: + if ny == 0: + if nz == 0: + return lambda l: (ox, oy, oz) + else: + return lambda l: (ox, oy, oz + l * nz) + else: # ny != 0 + if nz == 0: + return lambda l: (ox, oy + l * ny, oz) + else: + return lambda l: (ox, oy + l * ny, oz + l * nz) + else: # nx != 0 + if ny == 0: + if nz == 0: + return lambda l: (ox + l * nx, oy, oz) + else: + return lambda l: (ox + l * nx, oy, oz + l * nz) + else: # ny != 0 + if nz == 0: + return lambda l: (ox + l * nx, oy + l * ny, oz) + else: + return lambda l: (ox + l * nx, oy + l * ny, oz + l * nz) + + +def make_line(steps, size, v1, v2, center, normalize, mode): + # get the scaled direction (based on mode, size & normalize) + if mode == "AB": + nx, ny, nz = [v2[0] - v1[0], v2[1] - v1[1], v2[2] - v1[2]] + else: # mode == "OD": + nx, ny, nz = v2 + + stepsLength = sum(steps) # length of the non-normalized steps + + if normalize: + nn = sqrt(nx * nx + ny * ny + nz * nz) + scale = 1 if nn == 0 else (1 / nn / stepsLength * size) # scale to given size + else: # not normalized + if mode == "AB": + scale = 1 / stepsLength # scale to AB vector size + else: # mode == "OD": + nn = sqrt(nx * nx + ny * ny + nz * nz) + scale = 1 if nn == 0 else (1 / nn) # scale to steps size + + nx, ny, nz = [nx * scale, ny * scale, nz * scale] + + vec = get_vector_interpolator(v1[0], v1[1], v1[2], nx, ny, nz) + + verts = [] + add_vert = verts.append + l = -stepsLength / 2 if center else 0 + for s in [0.0] + steps: + l = l + s + add_vert(vec(l)) + edges = get_edge_list(len(steps)) + + return verts, edges + + +class SvLineNodeMK4(bpy.types.Node, SverchCustomTreeNode): + """ + Triggers: Line, segment. + Tooltip: Generate line between two points or from a point in a direction. + """ + bl_idname = 'SvLineNodeMK4' + bl_label = 'Line' + bl_icon = 'GRIP' + + def update_sockets(self, context): + ''' Swap V1/V2 input sockets to AB/OD based on selected mode ''' + for s, v in zip(self.inputs[-2:], self.mode): + s.name = socket_names[v] + s.prop_name = "point_" + v + + # keep the AB / OB props values synced when changing mode + if self.mode == "AB": + self.point_A = self.point_O + self.point_B = self.point_D + else: + self.point_O = self.point_A + self.point_D = self.point_B + + def set_direction(self, operator): + self.direction = operator.direction + self.mode = "OD" + return {'FINISHED'} + + def update_normalize(self, context): + self.inputs["Size"].hide_safe = not self.normalize + updateNode(self, context) + + def update_xyz_direction(self, context): + self.point_O = [0, 0, 0] + self.point_D = directions[self.direction] + + def update_mode(self, context): + if self.mode == self.last_mode: + return + + self.last_mode = self.mode + self.update_sockets(context) + updateNode(self, context) + + direction = StringProperty( + name="Direction", default="X", update=update_xyz_direction) + + mode = EnumProperty( + name="Mode", items=modeItems, default="OD", update=update_mode) + + last_mode = EnumProperty( + name="Last Mode", items=modeItems, default="OD") + + num = IntProperty( + name="Num Verts", description="Number of Vertices", + default=2, min=2, update=updateNode) + + step = FloatProperty( + name="Step", description="Step length", + default=1.0, update=updateNode) + + center = BoolProperty( + name="Center", description="Center the line", + default=False, update=updateNode) + + normalize = BoolProperty( + name="Normalize", description="Normalize line to size", + default=False, update=update_normalize) + + size = FloatProperty( + name="Size", description="Size of the normalized line", + default=10.0, update=updateNode) + + point_A = FloatVectorProperty( + name="Point A", description="Line's starting point", + size=3, default=(0, 0, 0), update=updateNode) + + point_B = FloatVectorProperty( + name="Point B", description="Line's ending point", + size=3, default=(1, 0, 0), update=updateNode) + + point_O = FloatVectorProperty( + name="Origin", description="Line's origin", + size=3, default=(0, 0, 0), update=updateNode) + + point_D = FloatVectorProperty( + name="Direction", description="Line's direction", + size=3, default=(1, 0, 0), update=updateNode) + + def sv_init(self, context): + self.inputs.new('StringsSocket', "Num").prop_name = 'num' + self.inputs.new('StringsSocket', "Step").prop_name = 'step' + size_socket = self.inputs.new('StringsSocket', "Size") + size_socket.prop_name = 'size' + size_socket.hide_safe = True + self.inputs.new('VerticesSocket', "Origin").prop_name = "point_O" + self.inputs.new('VerticesSocket', "Direction").prop_name = "point_D" + self.outputs.new('VerticesSocket', "Verts", "Verts") + self.outputs.new('StringsSocket', "Edges", "Edges") + + def draw_buttons(self, context, layout): + col = layout.column(align=False) + + if not self.inputs[-1].is_linked: + row = col.row(align=True) + for direction in "XYZ": + op = row.operator("node.set_line_direction", text=direction) + op.direction = direction + + col = layout.column(align=True) + row = col.row(align=True) + row.prop(self, "mode", expand=True) + row = col.row(align=True) + row.prop(self, "center", toggle=True) + row.prop(self, "normalize", toggle=True) + + def process(self): + if not any(s.is_linked for s in self.outputs): + return + + inputs = self.inputs + input_num = inputs["Num"].sv_get() + input_step = inputs["Step"].sv_get() + input_size = inputs["Size"].sv_get()[0] + input_V1 = inputs[-2].sv_get()[0] + input_V2 = inputs[-1].sv_get()[0] + + maxNum = 0 + params = match_long_repeat([input_num, input_step]) + stepsList = [] + for num, steps in zip(*params): + for nn in num: + nn = max(2, nn) + maxNum = max(nn, maxNum) + steps = steps[:nn - 1] # shorten step list if needed + fullList(steps, nn - 1) # extend step list if needed + stepsList.append(steps) + + update_edge_cache(maxNum) # help the edge generator get faster + + c, n, m = [self.center, self.normalize, self.mode] + params = match_long_repeat([stepsList, input_size, input_V1, input_V2]) + vertList, edgeList = [], [] + for steps, size, v1, v2 in zip(*params): + verts, edges = make_line(steps, size, v1, v2, c, n, m) + vertList.append(verts) + edgeList.append(edges) + + if self.outputs['Verts'].is_linked: + self.outputs['Verts'].sv_set(vertList) + if self.outputs['Edges'].is_linked: + self.outputs['Edges'].sv_set(edgeList) + + +class SvSetLineDirection(bpy.types.Operator, SvGenericCallbackWithParams): + bl_label = "Set line direction" + bl_idname = "node.set_line_direction" # dont use sv. + bl_description = "Set the direction of the line along X, Y or Z" + + direction = StringProperty(default="X") + fn_name = StringProperty(default="set_direction") + + +def register(): + bpy.utils.register_class(SvSetLineDirection) + bpy.utils.register_class(SvLineNodeMK4) + + +def unregister(): + bpy.utils.unregister_class(SvLineNodeMK4) + bpy.utils.unregister_class(SvSetLineDirection) -- GitLab From 4df44426a1c8979fa24cfae5d0c783cddf027fdd Mon Sep 17 00:00:00 2001 From: DolphinDream Date: Sat, 19 Jan 2019 16:34:46 -0500 Subject: [PATCH 60/60] Minor updates --- docs/nodes/generator/line_mk4.rst | 50 +++++++++++++++---------------- nodes/generator/line_mk4.py | 6 ++-- 2 files changed, 28 insertions(+), 28 deletions(-) diff --git a/docs/nodes/generator/line_mk4.rst b/docs/nodes/generator/line_mk4.rst index 0139b0c63..a5421e2c6 100644 --- a/docs/nodes/generator/line_mk4.rst +++ b/docs/nodes/generator/line_mk4.rst @@ -20,31 +20,31 @@ Parameters All parameters except **Center**, **Mode** and **Normalize** can be given by the node or an external input. -+---------------+-----------+-----------+--------------------------------------------------------+ -| Param | Type | Default | Description | -+===============+===========+===========+========================================================+ -| **Mode** | Enum | "OD" | The line direction mode: | -| | | | AB : create line from point A to point B | -| | | | OD : create line from origin O, along direction D | -+---------------+-----------+-----------+--------------------------------------------------------+ -| **Num Verts** | Int | 2 | The number of vertices in the line. [1] | -+---------------+-----------+-----------+--------------------------------------------------------+ -| **Step** | Float | 1.00 | The length between vertices. [2] | -+---------------+-----------+-----------+--------------------------------------------------------+ -| **Center** | Boolean  | False     | Center the line around its starting point location. | -+---------------+-----------+-----------+--------------------------------------------------------+ -| **Normalize** | Boolean  | False     | Normalize the line length to the given line size. | -+---------------+-----------+-----------+--------------------------------------------------------+ -| **Size** | Float  | 10.00   | The length of the normalized line. [3] | -+---------------+-----------+-----------+--------------------------------------------------------+ -| **Point A** | Vector  | (0,0,0)   | The starting point of the line. [4] | -+---------------+-----------+-----------+--------------------------------------------------------+ -| **Point B** | Vector  | (1,0,0) | The ending point of the line. [4] | -+---------------+-----------+-----------+--------------------------------------------------------+ -| **Origin** | Vector  | (0,0,0)   | The origin of the line. [5] | -+---------------+-----------+-----------+--------------------------------------------------------+ -| **Direction** | Vector  | (1,0,0) | The direction of the line. [5] | -+---------------+-----------+-----------+--------------------------------------------------------+ ++---------------+---------+---------+-----------------------------------------------------+ +| Param | Type | Default | Description | ++===============+=========+=========+=====================================================+ +| **Mode** | Enum | "OD" | The line direction mode: | +| | | | AB : create line from point A to point B | +| | | | OD : create line from origin O, along direction D | ++---------------+---------+---------+-----------------------------------------------------+ +| **Num Verts** | Int | 2 | The number of vertices in the line. [1] | ++---------------+---------+---------+-----------------------------------------------------+ +| **Step** | Float | 1.00 | The length between vertices. [2] | ++---------------+---------+---------+-----------------------------------------------------+ +| **Center** | Boolean | False   | Center the line around its starting point location. | ++---------------+---------+---------+-----------------------------------------------------+ +| **Normalize** | Boolean | False   | Normalize the line length to the given line size. | ++---------------+---------+---------+-----------------------------------------------------+ +| **Size** | Float | 10.00 | The length of the normalized line. [3] | ++---------------+---------+---------+-----------------------------------------------------+ +| **Point A** | Vector | (0,0,0) | The starting point of the line. [4] | ++---------------+---------+---------+-----------------------------------------------------+ +| **Point B** | Vector | (1,0,0) | The ending point of the line. [4] | ++---------------+---------+---------+-----------------------------------------------------+ +| **Origin** | Vector | (0,0,0) | The origin of the line. [5] | ++---------------+---------+---------+-----------------------------------------------------+ +| **Direction** | Vector | (1,0,0) | The direction of the line. [5] | ++---------------+---------+---------+-----------------------------------------------------+ Notes: [1] : The minimum number of vertices is 2. diff --git a/nodes/generator/line_mk4.py b/nodes/generator/line_mk4.py index 9ce573969..b5be9d4d4 100644 --- a/nodes/generator/line_mk4.py +++ b/nodes/generator/line_mk4.py @@ -29,7 +29,7 @@ modeItems = [ directions = {"X": [1, 0, 0], "Y": [0, 1, 0], "Z": [0, 0, 1]} -socket_names = {"A": "Point A", "B": "Point B", "O": "Origin", "D": "Distance"} +socket_names = {"A": "A", "B": "B", "O": "Origin", "D": "Distance"} def get_vector_interpolator(ox, oy, oz, nx, ny, nz): @@ -94,7 +94,7 @@ def make_line(steps, size, v1, v2, center, normalize, mode): class SvLineNodeMK4(bpy.types.Node, SverchCustomTreeNode): """ - Triggers: Line, segment. + Triggers: Line, Segment Tooltip: Generate line between two points or from a point in a direction. """ bl_idname = 'SvLineNodeMK4' @@ -248,7 +248,7 @@ class SvLineNodeMK4(bpy.types.Node, SverchCustomTreeNode): class SvSetLineDirection(bpy.types.Operator, SvGenericCallbackWithParams): bl_label = "Set line direction" - bl_idname = "node.set_line_direction" # dont use sv. + bl_idname = "node.set_line_direction" bl_description = "Set the direction of the line along X, Y or Z" direction = StringProperty(default="X") -- GitLab