From dfac7d295d9d9e62c66baa192c3021f9944cf096 Mon Sep 17 00:00:00 2001 From: Dealga McArdle Date: Wed, 7 Feb 2018 13:32:44 +0100 Subject: [PATCH 001/137] Create troubleshooting_nodes.rst --- docs/troubleshooting_nodes.rst | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 docs/troubleshooting_nodes.rst diff --git a/docs/troubleshooting_nodes.rst b/docs/troubleshooting_nodes.rst new file mode 100644 index 000000000..90512a84b --- /dev/null +++ b/docs/troubleshooting_nodes.rst @@ -0,0 +1,34 @@ +Troubleshooting Nodes +===================== + + Because Sverchok is *visual programming* we'll use the same terminology as regular programming to talk about errors/bugs, Sverchok is really a Python library with a node interface. + +**Errors / Error state** + +Sometimes a node enters a state where it fails to produce useful output (error state). Sverchok will change the color of that node to red, as an indication to you of the approximate location of a problem. I say "approximate" because the node that exhibits the error might not be the node causing the error, in that case it's one of the nodes upstream from the node. + +Nodes that enter the error state do so because the internal algorithms of the node tried to process information and couldn't, the node then throws what in programming is called "An Exception" (something didn't compute). + +**Exceptions** + +A variety of reasons can cause exceptions, sometimes you have to solve a sequence of errors. Usually it leads you to understanding the data being sent through the node tree better. This is seriously as good thing, mostly for your productivity. + +**Causes** + +- A node can receives input that it doesn't expect, before starting to process the data. Some nodes will throw an exception as early as possible. + +- A node's internal algorithm arrives at a point in processing the incoming data where the data being used to perform a calculation doesn't make sense. + + in the realm of math. errors like: + - div x by zero + - log of 0 or negative + in the realm of programming data structures: + - trying to divide a list/iterable by a number + - trying to add a list/iterable to a numer + - many many more. If you're a programmer you can imagine this list is long and not worth being exhausted. + +- Nodes that interact directly with Blender objects can be doing so incorrectly. + +**Ways to solve these issues, or at least understand them** + +We have quite a few ways to see the state of the output of a node -- GitLab From 8710bef6b8bdbbcba87172ebd49ca60c268c2ca4 Mon Sep 17 00:00:00 2001 From: Dealga McArdle Date: Wed, 7 Feb 2018 14:06:05 +0100 Subject: [PATCH 002/137] clarify that this section is addressed to users, not new devs trying to solve why their new Node is failing. --- docs/troubleshooting_nodes.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/troubleshooting_nodes.rst b/docs/troubleshooting_nodes.rst index 90512a84b..d3e347704 100644 --- a/docs/troubleshooting_nodes.rst +++ b/docs/troubleshooting_nodes.rst @@ -2,6 +2,9 @@ Troubleshooting Nodes ===================== Because Sverchok is *visual programming* we'll use the same terminology as regular programming to talk about errors/bugs, Sverchok is really a Python library with a node interface. + +For users +--------- **Errors / Error state** -- GitLab From fd1555ff1bc2a7e982b746e64488307164a3ff6a Mon Sep 17 00:00:00 2001 From: zeffii Date: Thu, 22 Feb 2018 15:24:43 +0100 Subject: [PATCH 003/137] add more info to debug text --- docs/troubleshooting_nodes.rst | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/docs/troubleshooting_nodes.rst b/docs/troubleshooting_nodes.rst index d3e347704..dbbeaf056 100644 --- a/docs/troubleshooting_nodes.rst +++ b/docs/troubleshooting_nodes.rst @@ -14,11 +14,11 @@ Nodes that enter the error state do so because the internal algorithms of the no **Exceptions** -A variety of reasons can cause exceptions, sometimes you have to solve a sequence of errors. Usually it leads you to understanding the data being sent through the node tree better. This is seriously as good thing, mostly for your productivity. +A variety of reasons can cause exceptions, sometimes you have to solve a sequence of errors. Usually it leads you to understanding the data being sent through the node tree better. This is seriously a good thing, mostly for your productivity. We have tools to help you see the data coming out or going in to a node. **Causes** -- A node can receives input that it doesn't expect, before starting to process the data. Some nodes will throw an exception as early as possible. +- A node can receive input that it doesn't expect, before starting to process the data. Some nodes will throw an exception as early as possible. - A node's internal algorithm arrives at a point in processing the incoming data where the data being used to perform a calculation doesn't make sense. @@ -30,8 +30,20 @@ A variety of reasons can cause exceptions, sometimes you have to solve a sequenc - trying to add a list/iterable to a numer - many many more. If you're a programmer you can imagine this list is long and not worth being exhausted. -- Nodes that interact directly with Blender objects can be doing so incorrectly. +- Nodes that interact directly with Blender objects can be doing so incorrectly. Usually this is forgiving, but sometimes you will cause Blender to close to the desktop. **Ways to solve these issues, or at least understand them** -We have quite a few ways to see the state of the output of a node +For most "unwanted" situations we can find what their cause is. We have quite a few ways to see the state of the output or input of a node. + +- Stethoscope node: Hook up this node to a stream of data that you want to probe. The node will display the incoming data directly in the NodeView. You might see immediately that there's something up with the data. You might see a single set of brackets, or your data has many more brackets than you expect. You might see that the data is not in the form you expect (extra nesting perhaps). For better info about stethoscope, see the docs that accompany this node. + +- Debug Print node: This node is also one you hookup to the socket data you are interested in seeing. The node however will print the data into the console/cmd window. This node allows you to connect multiple sockets (it will auto generate new input sockets). This lets you see a few sockets' data at a glance. This node is definitely more raw and you are advised to be aware of the amount of data you are passing. Don't give it thousands of vertices. + + - Debug Print and Stethoscope nodes: there are guidelines that are useful to follow when using both nodes. + - if you can't understand a problem, you need to try to reproduce the problem with as little geometry as possible. Dial all the sliders down to minimal values while the problem is still evident, then start debugging. + - switch off the nodes when you don't need them. + +- Debug timings mode: Sometimes you need to know which node is taking the longest to process it's dataload. We have a heatmap mode that gives us a quick way to pinpoint the nodes where sverchok spends more time calculating. + + -- GitLab From d272cb258be3dcefef264a827234a9d90f330b48 Mon Sep 17 00:00:00 2001 From: zeffii Date: Thu, 22 Feb 2018 15:28:11 +0100 Subject: [PATCH 004/137] add more info to debug text 2 --- docs/troubleshooting_nodes.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/troubleshooting_nodes.rst b/docs/troubleshooting_nodes.rst index dbbeaf056..5dbdba8c6 100644 --- a/docs/troubleshooting_nodes.rst +++ b/docs/troubleshooting_nodes.rst @@ -40,7 +40,7 @@ For most "unwanted" situations we can find what their cause is. We have quite a - Debug Print node: This node is also one you hookup to the socket data you are interested in seeing. The node however will print the data into the console/cmd window. This node allows you to connect multiple sockets (it will auto generate new input sockets). This lets you see a few sockets' data at a glance. This node is definitely more raw and you are advised to be aware of the amount of data you are passing. Don't give it thousands of vertices. - - Debug Print and Stethoscope nodes: there are guidelines that are useful to follow when using both nodes. + - some tips if you use either `Debug Print` and `Stethoscope nodes`: - if you can't understand a problem, you need to try to reproduce the problem with as little geometry as possible. Dial all the sliders down to minimal values while the problem is still evident, then start debugging. - switch off the nodes when you don't need them. -- GitLab From b7b94caef2b352ca391aeda6541a4aad5d51872a Mon Sep 17 00:00:00 2001 From: zeffii Date: Thu, 22 Feb 2018 15:34:36 +0100 Subject: [PATCH 005/137] add more info to debug doc --- docs/troubleshooting_nodes.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/troubleshooting_nodes.rst b/docs/troubleshooting_nodes.rst index 5dbdba8c6..b6dc36bd3 100644 --- a/docs/troubleshooting_nodes.rst +++ b/docs/troubleshooting_nodes.rst @@ -36,12 +36,15 @@ A variety of reasons can cause exceptions, sometimes you have to solve a sequenc For most "unwanted" situations we can find what their cause is. We have quite a few ways to see the state of the output or input of a node. +- start blender from a console, and keep it visisble as you work. When a node turns red it will print data into the console/cmd view. Read the error. Contemplate what it means. + - Stethoscope node: Hook up this node to a stream of data that you want to probe. The node will display the incoming data directly in the NodeView. You might see immediately that there's something up with the data. You might see a single set of brackets, or your data has many more brackets than you expect. You might see that the data is not in the form you expect (extra nesting perhaps). For better info about stethoscope, see the docs that accompany this node. - Debug Print node: This node is also one you hookup to the socket data you are interested in seeing. The node however will print the data into the console/cmd window. This node allows you to connect multiple sockets (it will auto generate new input sockets). This lets you see a few sockets' data at a glance. This node is definitely more raw and you are advised to be aware of the amount of data you are passing. Don't give it thousands of vertices. - some tips if you use either `Debug Print` and `Stethoscope nodes`: - if you can't understand a problem, you need to try to reproduce the problem with as little geometry as possible. Dial all the sliders down to minimal values while the problem is still evident, then start debugging. + - (you might have to) disconnect the error node, and "listen" to the data that was going into that node - switch off the nodes when you don't need them. - Debug timings mode: Sometimes you need to know which node is taking the longest to process it's dataload. We have a heatmap mode that gives us a quick way to pinpoint the nodes where sverchok spends more time calculating. -- GitLab From ec8304905184ef705e1e3534634d0273dcdba5ce Mon Sep 17 00:00:00 2001 From: nikitron Date: Thu, 22 Feb 2018 17:53:19 +0300 Subject: [PATCH 006/137] some more debug nodes discription. --- docs/troubleshooting_nodes.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/troubleshooting_nodes.rst b/docs/troubleshooting_nodes.rst index b6dc36bd3..b68f49c01 100644 --- a/docs/troubleshooting_nodes.rst +++ b/docs/troubleshooting_nodes.rst @@ -40,6 +40,10 @@ For most "unwanted" situations we can find what their cause is. We have quite a - Stethoscope node: Hook up this node to a stream of data that you want to probe. The node will display the incoming data directly in the NodeView. You might see immediately that there's something up with the data. You might see a single set of brackets, or your data has many more brackets than you expect. You might see that the data is not in the form you expect (extra nesting perhaps). For better info about stethoscope, see the docs that accompany this node. +- Viewer Text node: When sthetoscope see only first lines of code you can look deeper in digits. Or if you lazy to count data level's nestedness you can look at Viewer Text, bush button and look at Frame or in text file `Sverchok Viewer`. + +- Data shape: As Viewer Text it see levels, but not in terms of user cases. when we utilize data we know that first list is just container, than going `objects` and than data in digits or lists. Here we have plain explanation. + - Debug Print node: This node is also one you hookup to the socket data you are interested in seeing. The node however will print the data into the console/cmd window. This node allows you to connect multiple sockets (it will auto generate new input sockets). This lets you see a few sockets' data at a glance. This node is definitely more raw and you are advised to be aware of the amount of data you are passing. Don't give it thousands of vertices. - some tips if you use either `Debug Print` and `Stethoscope nodes`: -- GitLab From 3797e5f68d4494e249b3a633b80f0b44c6c49ea9 Mon Sep 17 00:00:00 2001 From: zeffii Date: Thu, 22 Feb 2018 17:47:44 +0100 Subject: [PATCH 007/137] add more info to debug doc 3 --- docs/troubleshooting_nodes.rst | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/troubleshooting_nodes.rst b/docs/troubleshooting_nodes.rst index b6dc36bd3..aad7b1f8b 100644 --- a/docs/troubleshooting_nodes.rst +++ b/docs/troubleshooting_nodes.rst @@ -40,13 +40,15 @@ For most "unwanted" situations we can find what their cause is. We have quite a - Stethoscope node: Hook up this node to a stream of data that you want to probe. The node will display the incoming data directly in the NodeView. You might see immediately that there's something up with the data. You might see a single set of brackets, or your data has many more brackets than you expect. You might see that the data is not in the form you expect (extra nesting perhaps). For better info about stethoscope, see the docs that accompany this node. -- Debug Print node: This node is also one you hookup to the socket data you are interested in seeing. The node however will print the data into the console/cmd window. This node allows you to connect multiple sockets (it will auto generate new input sockets). This lets you see a few sockets' data at a glance. This node is definitely more raw and you are advised to be aware of the amount of data you are passing. Don't give it thousands of vertices. +- Debug Print node: This node is also one you hookup to the socket data you are interested in seeing. The node however will print the data into the console/cmd window. This node allows you to connect multiple sockets (it will auto generate new input sockets). This lets you see a few sockets' data at a glance. This node is definitely more raw and you are advised to be aware of the amount of data you are passideg. Don't give it thousands of vertices. - some tips if you use either `Debug Print` and `Stethoscope nodes`: - if you can't understand a problem, you need to try to reproduce the problem with as little geometry as possible. Dial all the sliders down to minimal values while the problem is still evident, then start debugging. - (you might have to) disconnect the error node, and "listen" to the data that was going into that node - switch off the nodes when you don't need them. -- Debug timings mode: Sometimes you need to know which node is taking the longest to process it's dataload. We have a heatmap mode that gives us a quick way to pinpoint the nodes where sverchok spends more time calculating. +- Debug timings mode: + - heatmap: Sometimes you need to know which node is taking the longest to process it's dataload. We have a heatmap mode that gives us a quick way to pinpoint the nodes where sverchok spends more time calculating. + - print timings: this gives an overview of how exactly how much time is spent in each node, you'll get a list of nodes and their time-duration. -- GitLab From a21c717d96a727995d0779096548ebee2f2fce3b Mon Sep 17 00:00:00 2001 From: zeffii Date: Thu, 22 Feb 2018 17:57:52 +0100 Subject: [PATCH 008/137] add more info to debug doc 4 --- docs/troubleshooting_nodes.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/troubleshooting_nodes.rst b/docs/troubleshooting_nodes.rst index 7ef2f6e9f..679e4b825 100644 --- a/docs/troubleshooting_nodes.rst +++ b/docs/troubleshooting_nodes.rst @@ -40,9 +40,9 @@ For most "unwanted" situations we can find what their cause is. We have quite a - Stethoscope node: Hook up this node to a stream of data that you want to probe. The node will display the incoming data directly in the NodeView. You might see immediately that there's something up with the data. You might see a single set of brackets, or your data has many more brackets than you expect. You might see that the data is not in the form you expect (extra nesting perhaps). For better info about stethoscope, see the docs that accompany this node. -- Viewer Text node: When sthetoscope see only first lines of code you can look deeper in digits. Or if you lazy to count data level's nestedness you can look at Viewer Text, bush button and look at Frame or in text file `Sverchok Viewer`. +- Viewer Text node: When sthetoscope sees only the first lines of data, it's possible to see the raw data. To help you quickly see the level of nestedness of your data you can use the Viewer Text, push the view button and look at the `Frame` or in text file `Sverchok Viewer`. -- Data shape: As Viewer Text it see levels, but not in terms of user cases. when we utilize data we know that first list is just container, than going `objects` and than data in digits or lists. Here we have plain explanation. +- Data shape: As Viewer Text it sees levels, but not in terms of user cases. When we utilize data we know that the first list is just a container, inside are the `objects` and then data in digits or lists. Here we have a plain explanation. - Debug Print node: This node is also one you hookup to the socket data you are interested in seeing. The node however will print the data into the console/cmd window. This node allows you to connect multiple sockets (it will auto generate new input sockets). This lets you see a few sockets' data at a glance. This node is definitely more raw and you are advised to be aware of the amount of data you are passing. Don't give it thousands of vertices. -- GitLab From b72083b37b5a55b1d1f13cab0c77bd2f481f262b Mon Sep 17 00:00:00 2001 From: Dealga McArdle Date: Sat, 3 Nov 2018 14:03:23 +0100 Subject: [PATCH 009/137] 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 010/137] 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 011/137] "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 012/137] 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 013/137] 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 014/137] 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 015/137] 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 016/137] 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 017/137] 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 018/137] 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 019/137] 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 020/137] 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 021/137] 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 022/137] 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 023/137] 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 024/137] 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 025/137] 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 026/137] 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 027/137] 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 028/137] 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 029/137] 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 030/137] 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 031/137] 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 032/137] 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 033/137] 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 034/137] 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 035/137] 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 036/137] 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 037/137] 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 038/137] 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 039/137] 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 040/137] 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 041/137] 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 042/137] 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 043/137] 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 044/137] 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 045/137] 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 046/137] 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 047/137] 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 048/137] 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 049/137] 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 050/137] 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 051/137] 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 052/137] 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 053/137] 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 054/137] 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 055/137] 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 056/137] 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 057/137] 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 058/137] 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 059/137] 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 060/137] 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 061/137] 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 062/137] 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 063/137] 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 064/137] 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 065/137] 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 38803f9a3be9647793d0e903ed64352ba1ee8012 Mon Sep 17 00:00:00 2001 From: Durman Date: Fri, 15 Mar 2019 18:42:22 +0400 Subject: [PATCH 066/137] deleting unused operator #2386 --- ui/sv_panels.py | 27 +-------------------------- 1 file changed, 1 insertion(+), 26 deletions(-) diff --git a/ui/sv_panels.py b/ui/sv_panels.py index d09547585..42000f8f6 100644 --- a/ui/sv_panels.py +++ b/ui/sv_panels.py @@ -25,32 +25,8 @@ from sverchok.utils.sv_update_utils import version_and_sha from sverchok.core.update_system import process_from_nodes from sverchok.utils import profile -objects_nodes_set = {'ObjectsNode', 'ObjectsNodeMK2', 'SvObjectsNodeMK3'} - - - -class SverchokUpdateObjectIn(bpy.types.Operator): - """Sverchok update all object in""" - bl_idname = "object.sverchok_update_object_in" - bl_label = "Sverchok update all" - bl_options = {'REGISTER', 'UNDO'} - - def execute(self, context): - obj_nodes = [] - for ng in bpy.data.node_groups: - if ng.bl_idname == 'SverchCustomTreeType': - if ng.sv_process: - nodes = [] - for n in ng.nodes: - if n.bl_idname in objects_nodes_set: - nodes.append(n) - if nodes: - obj_nodes.append(nodes) - - for n in obj_nodes: - process_from_nodes(n) - return {'FINISHED'} +objects_nodes_set = {'ObjectsNode', 'ObjectsNodeMK2', 'SvObjectsNodeMK3'} class Sv3DViewObjInUpdater(bpy.types.Operator, object): @@ -389,7 +365,6 @@ sv_tools_classes = [ Sv3DViewObjInUpdater, SverchokToolsMenu, Sv3DPanel, - SverchokUpdateObjectIn ] -- GitLab From a43692993092bd9db444eb116de45bb31152e8d1 Mon Sep 17 00:00:00 2001 From: nortikin Date: Sat, 23 Mar 2019 22:44:41 +0300 Subject: [PATCH 067/137] some changes network socket client and topo sort and some docs --- docs/nodes/viz/empty_out.rst | 29 ++++++++ docs/nodes/viz/texture_viewer_lite.rst | 39 +++++++++++ docs/nodes/viz/viewer_curves.rst | 60 ++++++++++++++++ docs/nodes/viz/viewer_curves_2d.rst | 60 ++++++++++++++++ docs/nodes/viz/viewer_polyline_mk1.rst | 70 +++++++++++++++++++ docs/nodes/viz/viz_index.rst | 5 ++ .../topo_sort_for_profile1.py | 43 ++++++++++++ nodes/analyzer/image_components.py | 9 ++- nodes/network/udp_client.py | 29 ++++---- 9 files changed, 329 insertions(+), 15 deletions(-) create mode 100644 docs/nodes/viz/empty_out.rst create mode 100644 docs/nodes/viz/texture_viewer_lite.rst create mode 100644 docs/nodes/viz/viewer_curves.rst create mode 100644 docs/nodes/viz/viewer_curves_2d.rst create mode 100644 docs/nodes/viz/viewer_polyline_mk1.rst create mode 100644 node_scripts/SNLite_templates/topo_sort_for_profile1.py diff --git a/docs/nodes/viz/empty_out.rst b/docs/nodes/viz/empty_out.rst new file mode 100644 index 000000000..5655eb1f2 --- /dev/null +++ b/docs/nodes/viz/empty_out.rst @@ -0,0 +1,29 @@ +Empty out +============== + +Functionality +------------- + +Making an Empty in scene from matrix in sverchok + +Inputs +------ + +Matrix + +Parameters +---------- + ++-------------+-----------------------------------------------------------------------------------+ +| Feature | info | ++=============+===================================================================================+ +| Base name | Name for new Empty objects | ++-------------+-----------------------------------------------------------------------------------+ + + +Outputs +------- + +Object - Emptys + + diff --git a/docs/nodes/viz/texture_viewer_lite.rst b/docs/nodes/viz/texture_viewer_lite.rst new file mode 100644 index 000000000..433550b61 --- /dev/null +++ b/docs/nodes/viz/texture_viewer_lite.rst @@ -0,0 +1,39 @@ +Texture Viewer lite +=================== + +Functionality +------------- + +This node allows viewing a list of scalar values and Vectors as a texture, very useful +to display data from fractal, noise nodes and others, before outputting to a viewer_draw_mk2. +Lite version of node Texture Viewer + +Uses OpenGl calls to display the data OR use existing UV image in blender to store texture. + +Inputs +------ + +pixel value - raw of values from 0 to 1 express bw,rgb or rgba data + +Parameters +---------- + ++-------------+-----------------------------------------------------------------------------------+ +| Feature | info | ++=============+===================================================================================+ +| Image/bgl | Output method - to blender existing image with replacement or to screen only | ++-------------+-----------------------------------------------------------------------------------+ +| Width Tex | width of new texture in pixels | ++-------------+-----------------------------------------------------------------------------------+ +| Height Tex | height of new texture in pixels | ++-------------+-----------------------------------------------------------------------------------+ +| bw/rgb/rgba | output black-white/red-green-blue/red-green-blue-alpha images | ++-------------+-----------------------------------------------------------------------------------+ + + + +Outputs +------- + +None + diff --git a/docs/nodes/viz/viewer_curves.rst b/docs/nodes/viz/viewer_curves.rst new file mode 100644 index 000000000..53a097fff --- /dev/null +++ b/docs/nodes/viz/viewer_curves.rst @@ -0,0 +1,60 @@ +Viewer curves +============= + +Functionality +------------- + +Making 3d curves from edges inside blender. Every edge is separate straight spline inside curves. + +Inputs +------ + +- **vertices**. Line vertices +- **edges**. Line edges +- **Matrix**. Matrix for object placement in scene + +Parameters & Features +--------------------- + ++-------------+-----------------------------------------------------------------------------------+ +| Feature | info | ++=============+===================================================================================+ +| UPD | Ability to update objects | ++-------------+-----------------------------------------------------------------------------------+ +| eye/sel/rnd | show/hide, selectable/nope, render/nope object in scene | ++-------------+-----------------------------------------------------------------------------------+ +| Group | Group object in scene r not | ++-------------+-----------------------------------------------------------------------------------+ +| M/D/U | Merge object/duplicate same for matrices or make unique every object | ++-------------+-----------------------------------------------------------------------------------+ +| Name | Name of object in scene. When rename it will leave old named objects in scene | +| | also, when inserting new viewer it automatically choose new name | ++-------------+-----------------------------------------------------------------------------------+ +| Select | You can select/deselect objects in scene | ++-------------+-----------------------------------------------------------------------------------+ +| Material | Choose material for node's objects | ++-------------+-----------------------------------------------------------------------------------+ +| depth radius| Radius for your curve, default 0.2 | ++-------------+-----------------------------------------------------------------------------------+ +| Sresolution | Surface resolution (0 - square section), default 3 | ++-------------+-----------------------------------------------------------------------------------+ + +Parameters extended +------------------- + ++-------------+-----------------------------------------------------------------------------------+ +| Feature | info | ++=============+===================================================================================+ +| RND name | Force to choose random name for objects | ++-------------+-----------------------------------------------------------------------------------+ +| +Material | Make new material for objects | ++-------------+-----------------------------------------------------------------------------------+ + + + +Outputs +------- + +- **Objects**, simply new objects + + diff --git a/docs/nodes/viz/viewer_curves_2d.rst b/docs/nodes/viz/viewer_curves_2d.rst new file mode 100644 index 000000000..795b786ea --- /dev/null +++ b/docs/nodes/viz/viewer_curves_2d.rst @@ -0,0 +1,60 @@ +Viewer curves 2d +================ + +Functionality +------------- + +Making 2d curves from edges inside blender + +Inputs +------ + +- **vertices**. Line vertices +- **edges**. Line edges +- **Matrix**. Matrix for object placement in scene + +Parameters & Features +--------------------- + ++-------------+-----------------------------------------------------------------------------------+ +| Feature | info | ++=============+===================================================================================+ +| UPD | Ability to update objects | ++-------------+-----------------------------------------------------------------------------------+ +| eye/sel/rnd | show/hide, selectable/nope, render/nope object in scene | ++-------------+-----------------------------------------------------------------------------------+ +| Group | Group object in scene r not | ++-------------+-----------------------------------------------------------------------------------+ +| M/D/U | Merge object/duplicate same for matrices or make unique every object | ++-------------+-----------------------------------------------------------------------------------+ +| Name | Name of object in scene. When rename it will leave old named objects in scene | +| | also, when inserting new viewer it automatically choose new name | ++-------------+-----------------------------------------------------------------------------------+ +| Select | You can select/deselect objects in scene | ++-------------+-----------------------------------------------------------------------------------+ +| Material | Choose material for node's objects | ++-------------+-----------------------------------------------------------------------------------+ +| depth radius| Radius for your curve, default 0.2 | ++-------------+-----------------------------------------------------------------------------------+ +| Sresolution | Surface resolution (0 - square section), default 3 | ++-------------+-----------------------------------------------------------------------------------+ + +Parameters extended +------------------- + ++-------------+-----------------------------------------------------------------------------------+ +| Feature | info | ++=============+===================================================================================+ +| RND name | Force to choose random name for objects | ++-------------+-----------------------------------------------------------------------------------+ +| +Material | Make new material for objects | ++-------------+-----------------------------------------------------------------------------------+ + + + +Outputs +------- + +- **Objects**, simply new objects + + diff --git a/docs/nodes/viz/viewer_polyline_mk1.rst b/docs/nodes/viz/viewer_polyline_mk1.rst new file mode 100644 index 000000000..debb0fd9e --- /dev/null +++ b/docs/nodes/viz/viewer_polyline_mk1.rst @@ -0,0 +1,70 @@ +Polyline viewer +=============== + +Functionality +------------- + +Making 2d/3d curves from vertices series + +Inputs +------ + +- **vertices**. Line vertices +- **Matrix**. Matrix for object placement in scene +- **radii**. Radius for your curve, default 0.2 +- **twist**. Rotation of every point of polyline +- **bevel object**. Object as profile for bevel + +Parameters & Features +--------------------- + ++-------------+-----------------------------------------------------------------------------------+ +| Feature | info | ++=============+===================================================================================+ +| UPD | Ability to update objects | ++-------------+-----------------------------------------------------------------------------------+ +| eye/sel/rnd | show/hide, selectable/nope, render/nope object in scene | ++-------------+-----------------------------------------------------------------------------------+ +| Name | Name of object in scene. When rename it will leave old named objects in scene | +| | also, when inserting new viewer it automatically choose new name | ++-------------+-----------------------------------------------------------------------------------+ +| Select | You can select/deselect objects in scene | ++-------------+-----------------------------------------------------------------------------------+ +| Material | Choose material for node's objects | ++-------------+-----------------------------------------------------------------------------------+ +| 3D/2D | make polyline 2d or 3d | ++-------------+-----------------------------------------------------------------------------------+ +| radius | Radius for your curve, default 0.2 | ++-------------+-----------------------------------------------------------------------------------+ +| subdiv | Surface resolution (0 - square section), default 3 | ++-------------+-----------------------------------------------------------------------------------+ +| bspline | Make bspline line with controlling points | ++-------------+-----------------------------------------------------------------------------------+ +| close | Close polyline | ++-------------+-----------------------------------------------------------------------------------+ +| wire | Wire representation | ++-------------+-----------------------------------------------------------------------------------+ +| smooth | Smooth vertices normals | ++-------------+-----------------------------------------------------------------------------------+ +| Multi/Single| Usually many objects, but you can merge them to single | ++-------------+-----------------------------------------------------------------------------------+ + +Parameters extended +------------------- + ++-------------+-----------------------------------------------------------------------------------+ +| Feature | info | ++=============+===================================================================================+ +| RND name | Force to choose random name for objects | ++-------------+-----------------------------------------------------------------------------------+ +| +Material | Make new material for objects | ++-------------+-----------------------------------------------------------------------------------+ + + + +Outputs +------- + +- **Objects**, simply new objects + + diff --git a/docs/nodes/viz/viz_index.rst b/docs/nodes/viz/viz_index.rst index 595bf9612..17427170a 100644 --- a/docs/nodes/viz/viz_index.rst +++ b/docs/nodes/viz/viz_index.rst @@ -9,5 +9,10 @@ Viz viewer_bmesh_mk2 viewer_indices viewer_mk2 + viewer_curves_2d + viewer_curves + viewer_polyline_mk1 metaball_out texture_viewer + texture_viewer_lite + empty_out diff --git a/node_scripts/SNLite_templates/topo_sort_for_profile1.py b/node_scripts/SNLite_templates/topo_sort_for_profile1.py new file mode 100644 index 000000000..b715b6ff8 --- /dev/null +++ b/node_scripts/SNLite_templates/topo_sort_for_profile1.py @@ -0,0 +1,43 @@ +""" +in in_verts v +in in_edges s +out vout v +out eout s +""" + + +def dodo(verts, edges, verts_o,k): + for i in edges: + if k in i: + # this is awesome !! + k = i[int(not i.index(k))] + verts_o.append(verts[k]) + return k, i + return False, False + +def sortof(edges,verts,edges_o,verts_o): + ed = edges[0][1] + k = edges[0][0] + while True: + k, ed = dodo(verts, edges, verts_o,k) + if ed: + edges.remove(ed) + if not ed: + break + edges_o = [[k,k+1] for k in range(len(verts_o)-1)] + edges_o.append([0, len(verts_o)-1]) + #print(edges,ed) + if edges: + return sortof(edges,verts,edges_o,verts_o) + else: + return [edges_o, verts_o] + +if in_verts: + for edges, verts in zip(in_edges, in_verts): + #print(edges) + edges_ = [] + verts_ = [] + edges_o,verts_o = sortof(edges,verts,edges_,verts_) + print(edges_o,verts_o) + eout.append(edges_o) + vout.append(verts_o) diff --git a/nodes/analyzer/image_components.py b/nodes/analyzer/image_components.py index 31e51c190..446e49146 100644 --- a/nodes/analyzer/image_components.py +++ b/nodes/analyzer/image_components.py @@ -304,7 +304,7 @@ class SvImageComponentsNode(bpy.types.Node, SverchCustomTreeNode): if not self.loaded: return - if not outputs['xya'].is_linked: + if not any([outputs['xya'].is_linked,outputs['rgb'].is_linked]): return # upload reload, this avoids errors - still not perfect @@ -314,6 +314,7 @@ class SvImageComponentsNode(bpy.types.Node, SverchCustomTreeNode): node_image = self.node_dict[hash(self)]['node_image'] dict_data = node_image['image'] + #print(node_image) xya = 'xya' rgb = 'rgb' m1 = self.xy_spread @@ -328,7 +329,7 @@ class SvImageComponentsNode(bpy.types.Node, SverchCustomTreeNode): if -0.001 <= m2 <= 0.001: data = dict_data[rgb] else: - tmul = lambda v: (v[0]*m2, v[1]*m2, v[2]*m2) + tmul = lambda v: (v[0]*m1, v[1]*m1, v[2]*m2) data = [tmul(v) for v in dict_data[rgb]] outputs[rgb].sv_set([data]) @@ -369,3 +370,7 @@ def unregister(): bpy.utils.unregister_class(ImageComponentsOps) bpy.utils.unregister_class(SvImageComponentsNode) bpy.utils.unregister_class(svImageImporterOp) + + +if __name__ == '__main__': + register() \ No newline at end of file diff --git a/nodes/network/udp_client.py b/nodes/network/udp_client.py index d39405026..b10532698 100644 --- a/nodes/network/udp_client.py +++ b/nodes/network/udp_client.py @@ -30,7 +30,6 @@ class UdpClientNode(bpy.types.Node, SverchCustomTreeNode): bl_idname = 'UdpClientNode' bl_label = 'UDP Client' - def send_msg(self, context): sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) sock.setblocking(0) @@ -39,11 +38,13 @@ class UdpClientNode(bpy.types.Node, SverchCustomTreeNode): def recv_msg(self): sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) - sock.setblocking(0) + #sock.setblocking(0) + sock.bind((self.ip, self.port)) sock.settimeout(self.timeout) try: - data, _ = sock.recvfrom(self.buffer_size) + data = sock.recv(self.buffer_size) self.receive = data.decode('UTF-8') + print(self.receive) except socket.timeout: print('Timeout') @@ -84,21 +85,23 @@ class UdpClientNode(bpy.types.Node, SverchCustomTreeNode): layout.prop(self, 'timeout', text='Timeout') def sv_init(self, context): - self.inputs.new('StringsSocket', 'send', 'send').prop_name = 'send' - self.outputs.new('StringsSocket', 'receive', 'receive') + self.inputs.new('StringsSocket', 'send').prop_name = 'send' + self.outputs.new('StringsSocket', 'receive') @profile def process(self): if not self.active: return - - print(type(self.send),type(self.ip),type(self.port)) - input_value = self.inputs[0].sv_get() - if self.send != str(input_value): - self.send = str(input_value) - #self.send_msg(bpy.context) - - if self.outputs['receive'].is_linked: + if self.inputs[0].is_linked: + input_value = self.inputs[0].sv_get() + if self.send != str(input_value): + + self.send = str(input_value) + self.send_msg(bpy.context) + print(type(self.send),type(self.ip),type(self.port)) + print('sent message',(self.ip),(self.port)) + + elif self.outputs['receive'].is_linked: self.recv_msg() self.outputs['receive'].sv_set(self.receive) -- GitLab From 4a41cbc0204aa6fdc09e4d8686e4b029814b600d Mon Sep 17 00:00:00 2001 From: nikitron Date: Sun, 24 Mar 2019 13:15:16 +0300 Subject: [PATCH 068/137] some authors added --- __init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/__init__.py b/__init__.py index c70193619..f209af69e 100755 --- a/__init__.py +++ b/__init__.py @@ -34,6 +34,8 @@ # Eleanor Howick (aka elfnor) # Walter Perdan (aka kalwalt) # Marius Giurgi (aka DolphinDream) +# Durman (aka Randum) +# Ivan Prytov (aka Ivan Prytov) # # ***** END GPL LICENSE BLOCK ***** # -- GitLab From b7b19b4887187dec805b694bc55e9d6f0098f055 Mon Sep 17 00:00:00 2001 From: nikitron Date: Sun, 24 Mar 2019 13:16:10 +0300 Subject: [PATCH 069/137] Update README.md --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 1cb2c3b45..1741a4a4e 100644 --- a/README.md +++ b/README.md @@ -168,9 +168,9 @@ Email: sverchok-b3d@yandex.ru - Портнов Илья; - Ховик Элеонора; - Вальтер Пердан; -- Мариус Георгий -- Дурман, -- Портнов Иван +- Мариус Георгий; +- Дурман; +- Прытов Иван; Email: sverchok-b3d@yandex.ru -- GitLab From b4822db0de25aa12033140c12e08dc7f83d95100 Mon Sep 17 00:00:00 2001 From: nortikin Date: Sun, 24 Mar 2019 13:54:34 +0300 Subject: [PATCH 070/137] UDP mk2 node is on the stage --- docs/404_custom.html | 2 +- index.md | 1 + nodes/network/udp_client.py | 39 ++++---- nodes/network/udp_client_mk2.py | 161 ++++++++++++++++++++++++++++++++ 4 files changed, 185 insertions(+), 18 deletions(-) create mode 100644 nodes/network/udp_client_mk2.py diff --git a/docs/404_custom.html b/docs/404_custom.html index 4c2b838bb..7037d3587 100644 --- a/docs/404_custom.html +++ b/docs/404_custom.html @@ -11,7 +11,7 @@
- Sorry! It appears that UVtextures doesn't have a document associated with it. + Sorry! It appears that Polyline Viewer MK1 doesn't have a document associated with it.

Please contact us on the GitHub issue tracker or any of the following social sites: G+, VK or Blenderartists. diff --git a/index.md b/index.md index 076bba081..dfe8cc3f9 100644 --- a/index.md +++ b/index.md @@ -288,6 +288,7 @@ ## Network UdpClientNode + SvUdpClientNodeMK2 ## Beta Nodes SvFormulaShapeNode diff --git a/nodes/network/udp_client.py b/nodes/network/udp_client.py index b10532698..296999f5b 100644 --- a/nodes/network/udp_client.py +++ b/nodes/network/udp_client.py @@ -17,6 +17,7 @@ # ##### END GPL LICENSE BLOCK ##### import socket +import threading, time, ast import bpy from bpy.props import IntProperty, FloatProperty, EnumProperty, StringProperty, BoolProperty @@ -36,17 +37,19 @@ class UdpClientNode(bpy.types.Node, SverchCustomTreeNode): sock.sendto(bytes(self.send, 'UTF-8'), (self.ip, self.port)) - def recv_msg(self): - sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) - #sock.setblocking(0) - sock.bind((self.ip, self.port)) - sock.settimeout(self.timeout) - try: - data = sock.recv(self.buffer_size) - self.receive = data.decode('UTF-8') - print(self.receive) - except socket.timeout: - print('Timeout') + def recv_msg(self,context): + while True: + try: + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.bind((self.ip, self.port)) + sock.settimeout(self.timeout) + data = sock.recv(self.buffer_size) + print(data) + if data: + self.receive = data.decode('UTF-8') + except socket.timeout: + pass + #print('Timeout') send = StringProperty(name='send', @@ -85,8 +88,10 @@ class UdpClientNode(bpy.types.Node, SverchCustomTreeNode): layout.prop(self, 'timeout', text='Timeout') def sv_init(self, context): - self.inputs.new('StringsSocket', 'send').prop_name = 'send' + self.inputs.new('StringsSocket', 'send') #.prop_name = 'send' self.outputs.new('StringsSocket', 'receive') + ev = threading.Thread(target=self.recv_msg, args=(context,)) + ev.start() @profile def process(self): @@ -98,12 +103,12 @@ class UdpClientNode(bpy.types.Node, SverchCustomTreeNode): self.send = str(input_value) self.send_msg(bpy.context) - print(type(self.send),type(self.ip),type(self.port)) - print('sent message',(self.ip),(self.port)) + #print(type(self.send),type(self.ip),type(self.port)) + #print('sent message',(self.ip),(self.port)) elif self.outputs['receive'].is_linked: - self.recv_msg() - self.outputs['receive'].sv_set(self.receive) + #self.recv_msg() + self.outputs['receive'].sv_set(ast.literal_eval(self.receive)) def register(): @@ -114,4 +119,4 @@ def unregister(): bpy.utils.unregister_class(UdpClientNode) if __name__ == '__main__': - register() \ No newline at end of file + register() diff --git a/nodes/network/udp_client_mk2.py b/nodes/network/udp_client_mk2.py new file mode 100644 index 000000000..67591e215 --- /dev/null +++ b/nodes/network/udp_client_mk2.py @@ -0,0 +1,161 @@ +# ##### 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 socket +import threading, time, ast + +import bpy +from bpy.props import IntProperty, FloatProperty, EnumProperty, StringProperty, BoolProperty +from sverchok.node_tree import SverchCustomTreeNode, StringsSocket +from sverchok.utils.profile import profile +from sverchok.data_structure import updateNode + + +class SvUDP(bpy.types.Operator): + + ''' + Making flag of run for threading function active, so + UDP networking can sent and receive something + ''' + + bl_idname = "node.svudp" + bl_label = "Start or stop UDP node" + + fn_name = StringProperty(default='') + node_name = StringProperty(default='') + tree_name = StringProperty(default='') + + def execute(self, context): + """ + returns the operator's 'self' too to allow the code being called to + print from self.report. + """ + if self.tree_name and self.node_name: + ng = bpy.data.node_groups[self.tree_name] + node = ng.nodes[self.node_name] + else: + node = context.node + if self.fn_name == 'Run': + context.scene.svUDPrun = True + ev = threading.Thread(target=node.recv_msg, args=(context,)) + ev.start() + else: + context.scene.svUDPrun = False + return {'FINISHED'} + +class SvUdpClientNodeMK2(bpy.types.Node, SverchCustomTreeNode): + + ''' + You acn send and receive UDP signal, + that will auto parced from string value to list of data + If error accures with ast, than data is wrong. + Follow data structure. At least it has to be [[]] container + ''' + + bl_idname = 'SvUdpClientNodeMK2' + bl_label = 'UDP Client mk2' + + def send_msg(self, context): + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.setblocking(0) + sock.sendto(bytes(self.send, 'UTF-8'), (self.ip, self.port)) + + + def recv_msg(self,context): + while context.scene.svUDPrun == True: + try: + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.bind((self.ip, self.port)) + sock.settimeout(self.timeout) + data = sock.recv(self.buffer_size) + if data: + self.receive = data.decode('UTF-8') + except socket.timeout: + pass + + + send = StringProperty(name='send', + description='Message to send', + default='message', + update=send_msg) + + receive = StringProperty(name='receive', + description='Received message', + default='', + update=updateNode) + + ip = StringProperty(name='ip', + description='IP address of server', + default='127.0.0.1') + + port = IntProperty(name='port', + description='Port number to send message', + default=9250) + + buffer_size = IntProperty(name='buffer_size', + description='Size of buffer', + default=8192) + + timeout = FloatProperty(name='timeout', + description='Timeout (sec)', + default=0.5) + + def draw_buttons(self, context, layout): + if context.scene.svUDPrun == True: + t = 'Stop' + else: + t = 'Run' + layout.operator('node.svudp', text=t).fn_name = t + layout.prop(self, 'ip', text='IP') + layout.prop(self, 'port', text='Port') + layout.prop(self, 'buffer_size', text='Buffer') + layout.prop(self, 'timeout', text='Timeout') + + def sv_init(self, context): + self.inputs.new('StringsSocket', 'send') #.prop_name = 'send' + self.outputs.new('StringsSocket', 'receive') + + + @profile + def process(self): + if not bpy.context.scene.svUDPrun: + return + if self.inputs[0].is_linked: + input_value = self.inputs[0].sv_get() + if self.send != str(input_value): + + self.send = str(input_value) + self.send_msg(bpy.context) + + elif self.outputs['receive'].is_linked: + self.outputs['receive'].sv_set(ast.literal_eval(self.receive)) + + +def register(): + bpy.utils.register_class(SvUdpClientNodeMK2) + bpy.utils.register_class(SvUDP) + bpy.types.Scene.svUDPrun = bpy.props.BoolProperty(False) + + +def unregister(): + del bpy.types.Scene.svUDPrun + bpy.utils.unregister_class(SvUDP) + bpy.utils.unregister_class(SvUdpClientNodeMK2) + +if __name__ == '__main__': + register() -- GitLab From 5b40d9d48ffd87b2af38a0f19fc12ca49e945bce Mon Sep 17 00:00:00 2001 From: nortikin Date: Sun, 24 Mar 2019 13:57:20 +0300 Subject: [PATCH 071/137] move old UDP to old nodes folder --- index.md | 1 - {nodes/network => old_nodes}/udp_client.py | 0 2 files changed, 1 deletion(-) rename {nodes/network => old_nodes}/udp_client.py (100%) diff --git a/index.md b/index.md index dfe8cc3f9..2fa0cc4fa 100644 --- a/index.md +++ b/index.md @@ -287,7 +287,6 @@ ConverterNode ## Network - UdpClientNode SvUdpClientNodeMK2 ## Beta Nodes diff --git a/nodes/network/udp_client.py b/old_nodes/udp_client.py similarity index 100% rename from nodes/network/udp_client.py rename to old_nodes/udp_client.py -- GitLab From b3ec2b9ce60a0bb553058d04d2dac8641d3018e7 Mon Sep 17 00:00:00 2001 From: nortikin Date: Sun, 24 Mar 2019 15:12:55 +0300 Subject: [PATCH 072/137] documentation of UDP node --- docs/nodes/network/network_index.rst | 2 +- docs/nodes/network/udp_client_mk2.rst | 38 +++++++++++++++++++++++++++ 2 files changed, 39 insertions(+), 1 deletion(-) create mode 100644 docs/nodes/network/udp_client_mk2.rst diff --git a/docs/nodes/network/network_index.rst b/docs/nodes/network/network_index.rst index 673ea9c6f..901bbae9e 100644 --- a/docs/nodes/network/network_index.rst +++ b/docs/nodes/network/network_index.rst @@ -4,4 +4,4 @@ Network .. toctree:: :maxdepth: 2 - + udp_client_mk2 diff --git a/docs/nodes/network/udp_client_mk2.rst b/docs/nodes/network/udp_client_mk2.rst new file mode 100644 index 000000000..dbae43d2e --- /dev/null +++ b/docs/nodes/network/udp_client_mk2.rst @@ -0,0 +1,38 @@ +UDP client mk2 +============== + +Functionality +------------- + +Receive or send UDP data to some IP4 adress (if you like it can be extended to ip6). +To make it sent - link input socket (left), configure ip/port and press run. +To make receive - link output socket (right), configure ip/port/bufer/timeout and press run. +To change ip/port - press Stop, change parameters, press Run + +Expression syntax +----------------- + + + +Inputs +------ + +- **send**, data, that you wish to send to Internet (or localhost) to some port + +Parameters +---------- + +This node has the following parameters: + +- **Run/Stop**. Buttons to run stuff or stop stuff. It turns on the threading definition o listen ip/port permanently and letting you send data at node tree changes event (when you change data in input socket or press update all). +- **IP**. ip4 adress to work with. +- **Port**. Port to work with. +- **Buffer**. Size of buffer to receive data in it. If data larger than bufer size in bytes. +- **Timeout**. Periods to cache one data (frequency) to receive. + + +Outputs +------- + +- **receive**. data coming in as string, so ast component of python will interprate result literally. If your data is correct sverchok list data, you not need farthere modification of incoming data. + -- GitLab From 4fb641ef378a4579ec29824c223d0335d06cdc8e Mon Sep 17 00:00:00 2001 From: nortikin Date: Sun, 24 Mar 2019 15:46:44 +0300 Subject: [PATCH 073/137] overall docs overview over time --- docs/issue_template.rst | 4 ++++ docs/main.rst | 2 ++ docs/nodes.rst | 1 + docs/nodes/alpha_nodes/alpha_nodes_index.rst | 4 +++- docs/nodes/analyzers/analyzers_index.rst | 4 +--- docs/nodes/beta_nodes/beta_nodes_index.rst | 14 +++++++++----- docs/nodes/generator/generator_index.rst | 2 ++ .../generators_extended_index.rst | 6 ++++-- docs/nodes/list_main/list_main_index.rst | 1 + docs/nodes/list_mutators/list_mutators_index.rst | 2 ++ .../modifier_change/modifier_change_index.rst | 2 ++ docs/nodes/network/network_index.rst | 1 + docs/nodes/number/number_index.rst | 4 +++- docs/nodes/quaternion/quaternion_index.rst | 10 ++++++++++ docs/nodes/text/text_index.rst | 2 +- docs/nodes/vector/vector_index.rst | 1 + docs/pull_request_template.rst | 4 ++++ 17 files changed, 51 insertions(+), 13 deletions(-) create mode 100644 docs/nodes/quaternion/quaternion_index.rst diff --git a/docs/issue_template.rst b/docs/issue_template.rst index 23661ba0d..ae8104435 100644 --- a/docs/issue_template.rst +++ b/docs/issue_template.rst @@ -1,3 +1,7 @@ +============== +Issue template +============== + ## Problem statement Please describe your problem here. diff --git a/docs/main.rst b/docs/main.rst index cb4158bc9..1866690a9 100644 --- a/docs/main.rst +++ b/docs/main.rst @@ -18,7 +18,9 @@ Contents: nodes contributing contribute_small_things + pull_request_template testing + issue_template Indices and tables diff --git a/docs/nodes.rst b/docs/nodes.rst index 5ceec92b6..6241643ad 100644 --- a/docs/nodes.rst +++ b/docs/nodes.rst @@ -15,6 +15,7 @@ Nodes Number Vector Matrix + Quaternion Logic List Main List Structure diff --git a/docs/nodes/alpha_nodes/alpha_nodes_index.rst b/docs/nodes/alpha_nodes/alpha_nodes_index.rst index d7772b2c4..b7e754866 100644 --- a/docs/nodes/alpha_nodes/alpha_nodes_index.rst +++ b/docs/nodes/alpha_nodes/alpha_nodes_index.rst @@ -5,6 +5,8 @@ Alpha Nodes .. toctree:: :maxdepth: 2 - offset_line + armature_analyzer contour2D + lattice_analyzer + offset_line planar_edgenet_to_polygons diff --git a/docs/nodes/analyzers/analyzers_index.rst b/docs/nodes/analyzers/analyzers_index.rst index d04b26c3d..9611733ff 100644 --- a/docs/nodes/analyzers/analyzers_index.rst +++ b/docs/nodes/analyzers/analyzers_index.rst @@ -10,8 +10,6 @@ Analyzers distance_pp deformation edge_angles - evaluate_image - image_components kd_tree kd_tree_edges_mk2 mesh_filter @@ -26,5 +24,5 @@ Analyzers points_inside_mesh polygons_centers polygons_centers_mk3 - raycast + raycaster_lite volume diff --git a/docs/nodes/beta_nodes/beta_nodes_index.rst b/docs/nodes/beta_nodes/beta_nodes_index.rst index 9dec710b9..fc03bac65 100644 --- a/docs/nodes/beta_nodes/beta_nodes_index.rst +++ b/docs/nodes/beta_nodes/beta_nodes_index.rst @@ -5,11 +5,15 @@ Beta Nodes .. toctree:: :maxdepth: 2 - set_dataobject - Mask_from_Index - select_mesh_verts - find_uv_coord_on_mesh_node - sample_uv_color color_by_formula + deform_by_formula + extrude_separate_lite + find_uv_coord_on_mesh_node + Mask_from_Index spiralNode + subdivide_lite + sample_uv_color + select_mesh_verts + set_dataobject + unsubdivide diff --git a/docs/nodes/generator/generator_index.rst b/docs/nodes/generator/generator_index.rst index 352ac7a61..3afaaa668 100644 --- a/docs/nodes/generator/generator_index.rst +++ b/docs/nodes/generator/generator_index.rst @@ -11,8 +11,10 @@ Generators bricks circle cylinder + cylinder_mk2 image line_mk2 + line_mk3 ngon plane_mk2 random_vector_mk2 diff --git a/docs/nodes/generators_extended/generators_extended_index.rst b/docs/nodes/generators_extended/generators_extended_index.rst index 8db44f90c..b5944deb5 100644 --- a/docs/nodes/generators_extended/generators_extended_index.rst +++ b/docs/nodes/generators_extended/generators_extended_index.rst @@ -6,14 +6,15 @@ Generators Extended :maxdepth: 2 box_rounded + ellipse icosphere generative_art hilbert hilbert3d hilbert_image - hexa_grid_mk1 image_components profile_mk2 + polygon_grid mesh_eval ring script @@ -21,4 +22,5 @@ Generators Extended scripted_intro script_mk2 smooth_lines - \ No newline at end of file + super_ellipsoid + diff --git a/docs/nodes/list_main/list_main_index.rst b/docs/nodes/list_main/list_main_index.rst index 509a7b124..24b33c0b1 100644 --- a/docs/nodes/list_main/list_main_index.rst +++ b/docs/nodes/list_main/list_main_index.rst @@ -12,4 +12,5 @@ List Main levels match sum_mk2 + statistics zip diff --git a/docs/nodes/list_mutators/list_mutators_index.rst b/docs/nodes/list_mutators/list_mutators_index.rst index 69a7f1952..14d3421e3 100644 --- a/docs/nodes/list_mutators/list_mutators_index.rst +++ b/docs/nodes/list_mutators/list_mutators_index.rst @@ -6,3 +6,5 @@ List Mutators :maxdepth: 2 modifier + polygon_sort + combinatorics diff --git a/docs/nodes/modifier_change/modifier_change_index.rst b/docs/nodes/modifier_change/modifier_change_index.rst index aa0cad2db..49537e8f7 100644 --- a/docs/nodes/modifier_change/modifier_change_index.rst +++ b/docs/nodes/modifier_change/modifier_change_index.rst @@ -16,8 +16,10 @@ Modifier Change extrude_region holes_fill iterate + input_switch mesh_join mesh_separate + mesh_switch objects_along_edge bend_along_path bend_along_surface diff --git a/docs/nodes/network/network_index.rst b/docs/nodes/network/network_index.rst index 901bbae9e..37944824f 100644 --- a/docs/nodes/network/network_index.rst +++ b/docs/nodes/network/network_index.rst @@ -4,4 +4,5 @@ Network .. toctree:: :maxdepth: 2 + udp_client_mk2 diff --git a/docs/nodes/number/number_index.rst b/docs/nodes/number/number_index.rst index 55973b752..9aa9b9308 100644 --- a/docs/nodes/number/number_index.rst +++ b/docs/nodes/number/number_index.rst @@ -10,10 +10,12 @@ Number float float_to_int formula2 + formula3 integer list_input - mix_numbers + mix_inputs random + random_num_gen range_float range_int range_map diff --git a/docs/nodes/quaternion/quaternion_index.rst b/docs/nodes/quaternion/quaternion_index.rst new file mode 100644 index 000000000..7a55fce4c --- /dev/null +++ b/docs/nodes/quaternion/quaternion_index.rst @@ -0,0 +1,10 @@ +********** +Quaternion +********** + +.. toctree:: + :maxdepth: 2 + + quaternion_in + quaternion_out + quaternion_math diff --git a/docs/nodes/text/text_index.rst b/docs/nodes/text/text_index.rst index 76b56b96d..287986477 100644 --- a/docs/nodes/text/text_index.rst +++ b/docs/nodes/text/text_index.rst @@ -13,4 +13,4 @@ Text shape text_in_mk2 text_out - viewer_text_mk2 + viewer_text_mk3 diff --git a/docs/nodes/vector/vector_index.rst b/docs/nodes/vector/vector_index.rst index ada5b30ee..303b111e2 100644 --- a/docs/nodes/vector/vector_index.rst +++ b/docs/nodes/vector/vector_index.rst @@ -7,6 +7,7 @@ Vector attractor axis_input_mk2 + color_input drop interpolation_stripes interpolation_mk3 diff --git a/docs/pull_request_template.rst b/docs/pull_request_template.rst index a741616b0..37346fe75 100644 --- a/docs/pull_request_template.rst +++ b/docs/pull_request_template.rst @@ -1,3 +1,7 @@ +===================== +Pull request template +===================== + ## Addressed problem description Please describe what problem does this PR solve. If it was already was discussed in an issue, it is enough to give a link as #number. -- GitLab From addb98e5dad5189b0b7a1f65781662653f108f41 Mon Sep 17 00:00:00 2001 From: nortikin Date: Sun, 24 Mar 2019 15:53:51 +0300 Subject: [PATCH 074/137] docs more --- docs/nodes/network/udp_client_mk2.rst | 28 +++++++++--------- docs/nodes/quaternion/quaternion_in.rst | 37 +++++++++++++----------- docs/nodes/quaternion/quaternion_out.rst | 4 +-- 3 files changed, 36 insertions(+), 33 deletions(-) diff --git a/docs/nodes/network/udp_client_mk2.rst b/docs/nodes/network/udp_client_mk2.rst index dbae43d2e..5bb778904 100644 --- a/docs/nodes/network/udp_client_mk2.rst +++ b/docs/nodes/network/udp_client_mk2.rst @@ -3,32 +3,32 @@ UDP client mk2 Functionality ------------- - -Receive or send UDP data to some IP4 adress (if you like it can be extended to ip6). -To make it sent - link input socket (left), configure ip/port and press run. -To make receive - link output socket (right), configure ip/port/bufer/timeout and press run. -To change ip/port - press Stop, change parameters, press Run - -Expression syntax ------------------ + +Receive or send UDP data to some IP4 adress (if you like it can be extended to ip6). +To make it sent - link input socket (left), configure ip/port and press run. +To make receive - link output socket (right), configure ip/port/bufer/timeout and press run. +To change ip/port - press Stop, change parameters, press Run + +Expression syntax +----------------- Inputs ------ -- **send**, data, that you wish to send to Internet (or localhost) to some port +- **send**, data, that you wish to send to Internet (or localhost) to some port Parameters ---------- This node has the following parameters: -- **Run/Stop**. Buttons to run stuff or stop stuff. It turns on the threading definition o listen ip/port permanently and letting you send data at node tree changes event (when you change data in input socket or press update all). -- **IP**. ip4 adress to work with. -- **Port**. Port to work with. -- **Buffer**. Size of buffer to receive data in it. If data larger than bufer size in bytes. -- **Timeout**. Periods to cache one data (frequency) to receive. +- **Run/Stop**. Buttons to run stuff or stop stuff. It turns on the threading definition o listen ip/port permanently and letting you send data at node tree changes event (when you change data in input socket or press update all). +- **IP**. ip4 adress to work with. Default 127.0.0.1 - localhost. +- **Port**. Port to work with. Default 9250. +- **Buffer**. Size of buffer to receive data in it. If data larger than bufer size it will be error. In bytes. +- **Timeout**. Periods to cache one data (frequency) to receive. Outputs diff --git a/docs/nodes/quaternion/quaternion_in.rst b/docs/nodes/quaternion/quaternion_in.rst index 6711f6887..3ecb68213 100644 --- a/docs/nodes/quaternion/quaternion_in.rst +++ b/docs/nodes/quaternion/quaternion_in.rst @@ -1,17 +1,20 @@ Quaternion In ------------- -Quaternion In node constructs quaternions based on various input components provided for a selected mode. +Quaternion In node constructs quaternions based on various input components provided for a selected mode. Modes ===== -The available **Modes** are: WXYZ, SCALAR-VECTOR, EULER, AXIS-ANGLE & MATRIX. +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] | @@ -25,13 +28,13 @@ The available **Modes** are: WXYZ, SCALAR-VECTOR, EULER, AXIS-ANGLE & MATRIX. +===============+================================================================+ Notes: -[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". +[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). +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 ====== @@ -39,13 +42,13 @@ 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: +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) | +---------------+----------------------------------+ | SCALAR-VECTOR | Scalar (float) & Vector (vector) | @@ -59,11 +62,11 @@ Based on the selected **Mode** the node makes available the corresponding input Outputs -======= +======= -**Quaternions** +**Quaternions** -The node outputs a list of one ore more quaternions based on the given input. +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. +The node only generates the quaternions when the output socket is connected. diff --git a/docs/nodes/quaternion/quaternion_out.rst b/docs/nodes/quaternion/quaternion_out.rst index 8872e5a77..28dbe7baf 100644 --- a/docs/nodes/quaternion/quaternion_out.rst +++ b/docs/nodes/quaternion/quaternion_out.rst @@ -10,7 +10,7 @@ The available **Modes** are: WXYZ, EULER, AXIS-ANGLE & 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] | @@ -46,7 +46,7 @@ Based on the selected **Mode** the node makes available the corresponding output +===============+==================================+ | Mode | Output Sockets (types) | -+---------------+----------------------------------+ ++===============+==================================+ | WXYZ | W, X, Y, Z (floats) | +---------------+----------------------------------+ | SCALAR-VECTOR | Scalar (float) & Vector (vector) | -- GitLab From 4a7efadf7250117612c58dbf86948ea8c2bf2286 Mon Sep 17 00:00:00 2001 From: Victor Doval <10011941+vicdoval@users.noreply.github.com> Date: Sun, 24 Mar 2019 14:23:43 +0100 Subject: [PATCH 075/137] Added Pulga Physics Node --- docs/nodes/alpha_nodes/pulga_physics.rst | 100 +++ index.md | 1 + nodes/modifier_change/pulga_physics.py | 580 +++++++++++++++ utils/pulga_physics_core.py | 868 +++++++++++++++++++++++ 4 files changed, 1549 insertions(+) create mode 100644 docs/nodes/alpha_nodes/pulga_physics.rst create mode 100644 nodes/modifier_change/pulga_physics.py create mode 100644 utils/pulga_physics_core.py diff --git a/docs/nodes/alpha_nodes/pulga_physics.rst b/docs/nodes/alpha_nodes/pulga_physics.rst new file mode 100644 index 000000000..81b376d43 --- /dev/null +++ b/docs/nodes/alpha_nodes/pulga_physics.rst @@ -0,0 +1,100 @@ +Pulga Physics +============= + +Functionality +------------- + +This node creates simulations from input parameters, it is meant to be used in form-finding purposes. +It creates the simulation using the inputed vertices as spheric particles that react to applied forces. + + +Input & Output +-------------- + + ++------------------------+---------------+-------------+-----------------------------------------------+ +| Input | Type | Default | Description | ++========================+===============+=============+===============================================+ +| **Initial_Pos** | Vertices | None | Vertices in original state | ++------------------------+---------------+-------------+-----------------------------------------------+ +| **Iterations** | Vertices | None | Number of iterations of the process | ++------------------------+---------------+-------------+-----------------------------------------------+ +| **Springs** | Strings | None | Edges referenced to vertices | ++------------------------+---------------+-------------+-----------------------------------------------+ +| **Springs Length** | Strings | None | Specify spring rest length, | +| | | | 0 to calculate it from initial position | ++------------------------+---------------+-------------+-----------------------------------------------+ +| **Springs Stiffness** | Strings | None | Springs stiffness constant | ++------------------------+---------------+-------------+-----------------------------------------------+ +| **pins** | Strings | None | Indexes of vertices with fixed position, | +| | | | accepts a vertices mask and index number | ++------------------------+---------------+-------------+-----------------------------------------------+ +| **pins_gola_pos | Vertices | None | Final position of pinned vertices | ++------------------------+---------------+-------------+-----------------------------------------------+ +| **Radius** | Strings | None | Radius of virtual sphere, used to | +| | | | calculate intersections, mass and surface | ++------------------------+---------------+-------------+-----------------------------------------------+ +| **Self Collision** | Strings | None | collision force. | +| | | | Applied when vertex distance < sum Radius | ++------------------------+---------------+-------------+-----------------------------------------------+ +| **Self Attract** | Strings | None | attract force magnitude | ++------------------------+---------------+-------------+-----------------------------------------------+ +| **Self Attract Decay** | Strings | None | attract force decay with distance (power) | +| | | | 0 = no decay, 1 = linear, 2 = quadratic... | ++------------------------+---------------+-------------+-----------------------------------------------+ +| **Grow** | Strings | None | Radius variance factor. | +| | | | Shrink when they intersect, Grow when don't. | ++------------------------+---------------+-------------+-----------------------------------------------+ +| **Min Radius** | Strings | None | Minimum radius limit | ++------------------------+---------------+-------------+-----------------------------------------------+ +| **Max Radius** | Strings | None | Maximum radius limit | ++------------------------+---------------+-------------+-----------------------------------------------+ +| **Pols** | Strings | None | Pols referenced to vertices | +| | | |(used to calculate inflate force) | ++------------------------+---------------+-------------+-----------------------------------------------+ +| **Inflate** | Strings | None | Inflate force magnitude (per surface unit) | ++------------------------+---------------+-------------+-----------------------------------------------+ +| **Initial Velocity** | Vertices | None | Initial vertices velocity | ++------------------------+---------------+-------------+-----------------------------------------------+ +| **Max Velocity** | Strings | None | Maximum vertices velocity | ++------------------------+---------------+-------------+-----------------------------------------------+ +| **Drag Force** | Strings | None | Movement resistance from environment | ++------------------------+---------------+-------------+-----------------------------------------------+ +| **Attractors** | Vertices | None | Attractors origins | ++------------------------+---------------+-------------+-----------------------------------------------+ +| **Attractors Force** | Strings | None | Attractors Force magnitude | ++------------------------+---------------+-------------+-----------------------------------------------+ +| **Attractors Clamp** | Strings | None | Attractors maximum influence distance | ++------------------------+---------------+-------------+-----------------------------------------------+ +| **Attractors Decay** | Strings | None | Decay with distance | +| | | | 0 = no decay, 1 = linear, 2 = quadratic... | ++------------------------+---------------+-------------+-----------------------------------------------+ +| **Random Seed | Strings | None | Random seed number | ++------------------------+---------------+-------------+-----------------------------------------------+ +| **Random Force** | Strings | None | Random force magnitude | ++------------------------+---------------+-------------+-----------------------------------------------+ +| **Random Variation** | Strings | None | Random force variation | ++------------------------+---------------+-------------+-----------------------------------------------+ +| **Density** | Strings | None | Particles Density | ++------------------------+---------------+-------------+-----------------------------------------------+ +| **Gravity** | Vectors | None | Constant forces that are mass independent | ++------------------------+---------------+-------------+-----------------------------------------------+ +| **Wind** | Vertices | None | constant forces that are mass dependent | ++------------------------+---------------+-------------+-----------------------------------------------+ +| **Bounding Box** | Vertices | None | Limits of the system. It will work with the | +| | | | bounding box of the given vectors (min. two) | ++------------------------+---------------+-------------+-----------------------------------------------+ +| **Obstacles** | Vertices | None | Obstacles vertices | ++------------------------+---------------+-------------+-----------------------------------------------+ +| **Obstacles Pols** | Strings | None | Pols referenced to obstacles vertices. | +| | | |(they must be triangles) | ++------------------------+---------------+-------------+-----------------------------------------------+ +| **Obstacles Bounce** | Strings | None | Obstacles Bounce force | ++------------------------+---------------+-------------+-----------------------------------------------+ +Examples +-------- + + +Notes +------- + diff --git a/index.md b/index.md index 076bba081..52a1472ff 100644 --- a/index.md +++ b/index.md @@ -357,3 +357,4 @@ SvQuaternionOutNode SvQuaternionInNode SvQuaternionMathNode + SvPulgaPhysicsNode diff --git a/nodes/modifier_change/pulga_physics.py b/nodes/modifier_change/pulga_physics.py new file mode 100644 index 000000000..b53c0c6e9 --- /dev/null +++ b/nodes/modifier_change/pulga_physics.py @@ -0,0 +1,580 @@ +# ##### 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, StringProperty, BoolProperty, FloatProperty, FloatVectorProperty +from sverchok.node_tree import SverchCustomTreeNode +from sverchok.data_structure import updateNode, node_id, match_long_repeat +from sverchok.utils.pulga_physics_core import pulga_system_init +from numpy import array +import time +from copy import deepcopy +import ast +FILE_NAME = 'pulga_Memory ' + + +def check_past(n_id): + '''read text-block and parse values''' + name = FILE_NAME + n_id + ".txt" + text = bpy.data.texts.get(name) or bpy.data.texts.new(name) + tx = text.as_string() + if len(tx)>1: + return ast.literal_eval(text.as_string()) + else: + return [] + + +def fill_past(p, n_id): + '''write values to text-block''' + name = FILE_NAME + n_id + ".txt" + text = bpy.data.texts.get(name) or bpy.data.texts.new(name) + text.clear() + text.write(''.join(str(p))) + +class SvPulgaPhysicsNode(bpy.types.Node, SverchCustomTreeNode): + ''' + Triggers: Springs, Cloth + Tooltip: Physics Engine + ''' + bl_idname = 'SvPulgaPhysicsNode' + bl_label = 'Pulga Physics' + bl_icon = 'CURVE_NCURVE' + + + iterations = IntProperty( + name='Iterations', description='Number of Iterations', + default=1, min=1, update=updateNode) + + + fixed_len = FloatProperty( + name='Springs Length', description='Specify spring rest length, 0 to calculate it from initial position', + default=0.0, update=updateNode) + spring_k = FloatProperty( + name='Springs Stiffness', description='Springs stiffness constant', + default=0.0, precision=4, + update=updateNode) + + rads_in = FloatProperty( + name='Radius', description='Used to calculate mass, surface and collisions', + default=1.0, update=updateNode) + + self_collision = FloatProperty( + name='Self Collision', description='Collision forces between vertices', + default=0.0, precision=4, update=updateNode) + self_attract = FloatProperty( + name='Self Attract', description='Attraction between vertices', + default=0.0, precision=4, update=updateNode) + attract_decay = FloatProperty( + name='Self Attract Decay', description='0 = no decay, 1 = linear, 2 = quadratic...', + default=0.0, precision=3, update=updateNode) + grow = FloatProperty( + name='Grow', description='Shrink if collide with others / Grow if does not ', + default=0.0, update=updateNode) + min_rad = FloatProperty( + name='Min Radius', description='Do not shrink under this value', + default=0.1, precision=3, update=updateNode) + max_rad = FloatProperty( + name='Max Radius', description='Do not grow over this value', + default=1.0, precision=3, update=updateNode) + + inflate = FloatProperty( + name='Inflate', description='push geometry along the normals proportional to polygon area', + default=1.0, precision=3, update=updateNode) + + initial_vel = FloatVectorProperty( + name='Initial Velocity', description='Initial Velocity', + size=3, default=(0., 0., 0.), + precision=3, update=updateNode) + + max_vel = FloatProperty( + name='Max Velocity', description='Limit maximun speed. 0 = no limit', + default=0.01, precision=3, update=updateNode) + drag_force = FloatProperty( + name='Drag Force', description='Movement resistance from environment', + default=0.0, precision=3, update=updateNode) + force = FloatProperty( + name='Attractors Force', description='Attractors Force magnitude', + default=0.0, precision=3, update=updateNode) + att_clamp = FloatProperty( + name='Attractors Clamp', description='Attractors maximum influence distance', + default=0.0, precision=3, update=updateNode) + att_decay_power = FloatProperty( + name='Attractors Decay', description='Decay with distance 0 = no decay, 1 = linear, 2 = quadratic...', + default=0.0, precision=3, update=updateNode) + + random_seed = IntProperty( + name='Random Seed', description='Random seed number', + default=0, min=0, update=updateNode) + random_force = FloatProperty( + name='Random Force', description='Random force magnitude', + default=0.0, update=updateNode) + random_variation = FloatProperty( + name='Random Variation', description='Random force variation', + default=0.0, min=0, max=1, update=updateNode) + + density = FloatProperty( + name='Density', description='Density', + default=0.1, update=updateNode) + + gravity = FloatVectorProperty( + name='Gravity', description='gravity or other constant forces that are mass independent', + size=3, default=(0., 0., 0.), + precision=4, update=updateNode) + wind = FloatVectorProperty( + name='Wind', description='wind or other constant forces that are mass dependent', + size=3, default=(0., 0., 0.), + precision=4, update=updateNode) + + obstacles_bounce = FloatProperty( + name='Obstacles Bounce', description='Obstacles Bounce', + default=0.1, update=updateNode) + + def handle_accumulative(self, context): + '''start cache''' + n_id = node_id(self) + data = self.node_dict.get(n_id) + if not self.accumulative: + self.accumulative_loaded = False + + for i in range(len(data)): + self.accumulativity_set_data([], i) + self.accumulative_parse = False + + if not data: + self.node_dict[n_id] = {} + data = self.node_dict.get(n_id) + updateNode(self, context) + + def memory_to_file(self, context): + '''bump memory to text-block''' + if self.accumulative_pause or self.accumulative_parse: + n_id = node_id(self) + data = self.node_dict.get(n_id) + out = [] + for i in range(len(data)): + out.append([data[i][0].tolist(), data[i][1].tolist(), data[i][2].tolist()]) + fill_past(out, n_id) + + def memory_to_lists(self): + '''bump memory to output''' + verts = [] + rads = [] + vel = [] + np = self.output_numpy + n_id = node_id(self) + data = self.node_dict.get(n_id) + if type(data) == dict: + for i in range(len(data)): + verts.append(data[i][0] if np else data[i][0].tolist()) + rads.append(data[i][1] if np else data[i][1].tolist()) + vel.append(data[i][2] if np else data[i][2].tolist()) + else: + data = check_past(n_id) + if len(data) > 0: + data = past + for i in range(len(data)): + verts.append(array(data[i][0]) if np else data[i][0]) + rads.append(array(data[i][1]) if np else data[i][1]) + vel.append(array(data[i][2]) if np else data[i][2]) + + + + return verts, rads, vel + + n_id = StringProperty() + node_dict = {} + + accumulative = BoolProperty(name="Accumulative", + description="Accumulate changes every NodeTree update", + default=False, + update=handle_accumulative) + accumulative_loaded = BoolProperty(name="accumulative is ready", + description="there is a cache", + default=False + ) + accumulative_pause = BoolProperty(name="Freeze", + description="Freeze accumulative changes on last update", + default=False, + update=memory_to_file + ) + accumulative_reset = BoolProperty(name="Reset", + description="Restart accumulative memory", + default=False, + update=updateNode) + accumulative_save = BoolProperty(name="Save", + description="Save accumulative memory", + default=False, + update=memory_to_file + ) + accumulative_parse = BoolProperty(name="Pause", + description="Save to file accumulative memory", + default=False, + update=memory_to_file + ) + + def accumulativity_get_data(self): + '''get data form previous update''' + n_id = node_id(self) + data = self.node_dict.get(n_id) + return data + + def accumulativity_set_data(self, cache, cache_id): + '''store data form this update''' + n_id = node_id(self) + data = self.node_dict.get(n_id) + data[cache_id] = cache + + return data + + + prop_dict = { + "Initial_Pos" : (0, '' , 'v', 0), + "Iterations" : (1, 'iterations', 's', 0), + "springs" : (2, '' , 's', 4), + "fixed_len" : (4, 'fixed_len' , 's', 4), + "spring_k" : (5, 'spring_k' , 's', 4), + "pins" : (6, '' , 's', 5), + "pins_goal_pos" : (7, '' , 'v', 5), + "rads_in" : (8, 'rads_in' , 's', 0), + "self_collision": (9, 'self_collision', 's', 1), + "self_attract" : (10, 'self_attract' , 's', 2), + "attract_decay" : (11, 'attract_decay', 's', 2), + "grow" : (12, 'grow' , 's', 3), + "min_rad" : (13, 'min_rad' , 's', 3), + "max_rad" : (14, 'max_rad' , 's', 3), + "Pols" : (15, '' , 's', 7), + "inflate" : (16, 'inflate' , 's', 7), + "Initial Velocity" : (17, 'initial_vel', 'v', 0), + "max_vel" : (18, 'max_vel' , 's', 0), + "drag_force" : (19, 'drag_force' , 's', 6), + "Attractors" : (20, '' , 'v', 8), + "force" : (21, 'force' , 's', 8), + "att_clamp" : (22, 'att_clamp' , 's', 8), + "att_decay_power" : (23, 'att_decay_power' , 's', 8), + "random_seed" : (24, 'random_seed' , 's', 9), + "random_force" : (25, 'random_force' , 's', 9), + "random_variation": (26, 'random_variation', 's', 9), + "Density" : (27, 'density' , 's', 0), + "Gravity" : (28, 'gravity' , 'v', 11), + "Wind" : (29, 'wind' , 'v', 11), + "Bounding Box" : (30, '' , 'v', 12), + "Obstacles" : (31, '' , 'v', 10), + "Obstacles_pols" : (32, '' , 's', 10), + "obstacles_bounce": (33, 'obstacles_bounce', 's', 10), + + } + sorted_props = sorted(prop_dict.items(), key=lambda kv: kv[1]) + prop_ui_groups2 = [[] for i in range(13)] + for input_prop in sorted_props: + prop_ui_groups2[input_prop[1][3]].append(input_prop[0]) + + + + prop_ui_groups = [ + ["self_collision"], + [ "self_attract", "attract_decay"], + [ "grow", "min_rad", "max_rad"], + ["springs", "fixed_len", "spring_k"], + ["pins", "pins_goal_pos"], + ["drag_force"], + ["Pols", "inflate"], + ["Attractors", "force", "att_clamp", "att_decay_power"], + ["random_seed", "random_force", "random_variation"], + ["Obstacles", "Obstacles_pols", "obstacles_bounce"], + ["Gravity", "Wind"], + ["b_box"] + ] + + def update_sockets(self, context): + ''' show and hide gated inputs''' + prop_triggers = [ + self.self_react_M, + self.self_attract_M, + self.fit_M, + self.springs_M, + self.pins_M, + self.drag_M, + self.inflate_M, + self.attract_M, + self.random_M, + self.obstacles_M, + self.world_F_M, + self.bounding_box_M, + ] + si = self.inputs + so = self.outputs + + for i in range(len(prop_triggers)): + if prop_triggers[i]: + if si[self.prop_ui_groups2[i+1][0]].hide_safe: + for p in self.prop_ui_groups2[i+1]: + si[p].hide_safe = False + else: + for p in self.prop_ui_groups2[i+1]: + si[p].hide_safe = True + if self.fit_M: + if so["Rads"].hide_safe: + so["Rads"].hide_safe = False + else: + so["Rads"].hide_safe = True + + updateNode(self, context) + + self_react_M = BoolProperty(name="Self Collision", + description="Self Collision: collision between input vertices as spheres", + default=False, + update=update_sockets) + self_attract_M = BoolProperty(name="Self Attraction", + description="Self Attraction: attract between input vertices as spheres", + default=False, + update=update_sockets) + fit_M = BoolProperty(name="Fit", + description="Fit: shrink if collide with others, grow if not", + default=False, + update=update_sockets) + springs_M = BoolProperty(name="Springs", + description="Use springs forces", + default=False, + update=update_sockets) + pins_M = BoolProperty(name="Pin", + description="Pin (turn to static) mask", + default=False, + update=update_sockets) + inflate_M = BoolProperty(name="Inflate", + description="Inflate (push geometry along the polygons normals proportional to polygon area", + default=False, + update=update_sockets) + drag_M = BoolProperty(name="Drag", + description="Drag force", + default=False, + update=update_sockets) + attract_M = BoolProperty(name="Attractors", + description="Use external attractors", + default=False, + update=update_sockets) + random_M = BoolProperty(name="Random", + description="Random force", + default=False, + update=update_sockets) + bounding_box_M = BoolProperty(name="Bounding Box", + description="System bounding box", + default=False, + update=update_sockets) + world_F_M = BoolProperty(name="World", + description="Constant Forces", + default=False, + update=update_sockets) + obstacles_M = BoolProperty(name="Obstacles", + description="Collision obstacles", + default=False, + update=update_sockets) + + output_numpy = BoolProperty(name="as NumPy", + description="Output NumPy arrays ", + default=False, + update=updateNode) + + def sv_init(self, context): + + '''create sockets''' + self.width = 200 + si = self.inputs.new + so = self.outputs.new + vs, ss = 'VerticesSocket', 'StringsSocket' + + for input_prop in self.sorted_props: + input_type = vs if input_prop[1][2] == 'v' else ss + si(input_type, input_prop[0]).prop_name = input_prop[1][1] + + + so(vs, "Vertices") + so(ss, "Rads") + so(vs, 'Velocity') + + self.update_sockets(context) + + def draw_buttons(self, context, layout): + '''draw buttons on the node''' + c1 = layout.column(align=True) + r0 = c1.row(align=True) + r0.prop(self, "self_react_M", toggle=True) + r0.prop(self, "self_attract_M", toggle=True) + r0.prop(self, "fit_M", toggle=True) + # r0.prop(self, "springs_M", toggle=True) + # r0.prop(self, "pins_M", toggle=True) + # r0.prop(self, "inflate_M", toggle=True) + + r = c1.row(align=True) + r.prop(self, "springs_M", toggle=True) + r.prop(self, "pins_M", toggle=True) + r.prop(self, "inflate_M", toggle=True) + + + r2 = c1.row(align=True) + r2.prop(self, "drag_M", toggle=True) + r2.prop(self, "attract_M", toggle=True) + r2.prop(self, "random_M", toggle=True) + # r2.prop(self, "world_F_M", toggle=True) + # r2.prop(self, "obstacles_M", toggle=True) + # r2.prop(self, "bounding_box_M", toggle=True) + + r3 = c1.row(align=True) + r3.prop(self, "world_F_M", toggle=True) + r3.prop(self, "obstacles_M", toggle=True) + r3.prop(self, "bounding_box_M", toggle=True) + + r4 = layout.column(align=True) + r4.prop(self, "accumulative", toggle=True) + if self.accumulative: + cr = r4.row(align=True) + cr.prop(self, "accumulative_pause", toggle=True) + cr.prop(self, "accumulative_reset", toggle=True) + upd = cr.operator("node.sverchok_update_current", text="Update") + ng_name = context.space_data.node_tree.name + upd.node_group = ng_name + cr.prop(self, "accumulative_parse", toggle=True) + + 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 get_data(self): + '''get all data from sockets''' + si = self.inputs + parameters = [] + for socket in si: + if len(socket.prop_name)>0: + + parameters.append(socket.sv_get()) + else: + parameters.append(socket.sv_get(default=[[]])) + + + return match_long_repeat(parameters) + + def fill_gates_dict(self): + '''redistribute booleans''' + si = self.inputs + gates_dict = {} + gates_dict["accumulate"] = self.accumulative + gates_dict["self_react"] = [self.self_react_M, self.self_attract_M, self.fit_M] + gates_dict["springs"] = [si["springs"].is_linked, si["fixed_len"].is_linked] + gates_dict["pins"] = [si["pins"].is_linked, si["pins_goal_pos"].is_linked] + gates_dict["drag"] = [self.drag_M, self.fit_M] + gates_dict["inflate"] = si["Pols"].is_linked + gates_dict["random"] = self.random_M + gates_dict["attractors"] = si["Attractors"].is_linked + gates_dict["world_f"] = [self.world_F_M, self.fit_M] + gates_dict["Obstacles"] = si["Obstacles"].is_linked and si["Obstacles_pols"].is_linked + gates_dict["b_box"] = si["Bounding Box"].is_linked + gates_dict["output"] = self.output_numpy + gates_dict["apply_f"] = True + + return gates_dict + + def get_local_cache(self, past, data, from_file, temp_id): + '''parse individual cached geometry if there is any''' + cache = [] + if self.accumulative: + if from_file and len(past) > 0: + cache = past[temp_id] + if not from_file and len(data) > 0: + cache = data.get(temp_id, []) + if self.accumulative_pause: + cache2 = deepcopy(cache) + self.accumulativity_set_data(cache2, temp_id) + if self.accumulative_reset: + cache = [] + self.accumulative_reset = False + + return cache + + def get_global_cache(self): + '''read cached geometry if there is any''' + from_file = False + past = [] + data = [] + if self.accumulative: + data = self.accumulativity_get_data() + if type(data) != dict: + from_file = True + n_id = node_id(self) + self.node_dict[n_id] = {} + data = self.node_dict.get(n_id) + past = check_past(n_id) + + return data, past, from_file + + def process(self): + '''main node function called every update''' + tiempo = time.time() + si = self.inputs + so = self.outputs + if not any(socket.is_linked for socket in so): + return + + if not si['Initial_Pos'].is_linked: + return + verts_out = [] + rads_out = [] + velocity_out = [] + + if self.accumulative and self.accumulative_parse: + verts_out, rads_out, velocity_out = self.memory_to_lists() + else: + out_lists = [verts_out, rads_out, velocity_out] + params = self.get_data() + gates_dict = self.fill_gates_dict() + data, past, from_file = self.get_global_cache() + temp_id = 0 + for par in zip(*params): + cache = self.get_local_cache(past, data, from_file, temp_id) + par_dict = {} + for idx, p in enumerate(self.sorted_props): + par_dict[p[0]] = par[idx] + np_verts, np_rad, np_vel = pulga_system_init(par_dict, par, gates_dict, out_lists, cache) + + if self.accumulative and not self.accumulative_pause: + cache = [np_verts, np_rad, np_vel] + self.accumulativity_set_data(cache, temp_id) + + temp_id += 1 + + if so['Vertices'].is_linked: + so['Vertices'].sv_set(verts_out) + if so['Rads'].is_linked: + so['Rads'].sv_set(rads_out) + if so['Velocity'].is_linked: + so['Velocity'].sv_set(velocity_out) + + print(time.time() - tiempo ) + + + +def register(): + '''register class in Blender''' + bpy.utils.register_class(SvPulgaPhysicsNode) + +def unregister(): + '''unregister class in Blender''' + bpy.utils.unregister_class(SvPulgaPhysicsNode) + diff --git a/utils/pulga_physics_core.py b/utils/pulga_physics_core.py new file mode 100644 index 000000000..b1cbac56e --- /dev/null +++ b/utils/pulga_physics_core.py @@ -0,0 +1,868 @@ +import numpy as np + +def cross_indices3(n): + '''create crossed indices''' + + nu = np.sum(np.arange(n, dtype=np.int64)) + ind = np.zeros((nu, 2), dtype=np.int16) + c = 0 + for i in range(n-1): + l = n-i-1 + np_i = np.full(n-i-1, i, dtype=np.int32) + np_j = np.arange(i+1, n, dtype=np.int32) + np_a = np.stack((np_i, np_j), axis=-1) + ind[c:c+l, :] = np_a + c += l + + return ind + + +def numpy_match_long_repeat(p): + '''match list length by repeating last one''' + q = [] + maxl = 0 + for g in p: + maxl = max(maxl, g.shape[0]) + for g in p: + difl = maxl - g.shape[0] + if difl > 0: + rr = np.repeat(g[np.newaxis, -1], difl, axis=0) + g = np.concatenate((g, rr)) + q.append(g) + return q + + +def numpy_fit_long_repeat(p, maxl): + '''match list length by repeating last one or removing end''' + q = [] + for g in p: + difl = maxl - g.shape[0] + if difl > 0: + rr = np.repeat(g[np.newaxis, -1], difl, axis=0) + g = np.concatenate((g, rr)) + if difl < 0: + g = g[:maxl] + q.append(g) + return q + + +def match_cylce(p, n): + '''append cylcing till have n items''' + difference = n - len(p) + for i in range(difference): + p.append(p[i % len(p)]) + + +def expand_pols_numpy(p, len_max): + '''to fit variable sided polygons in one array (cycling)''' + np_match_cycle = np.vectorize(match_cylce) + np_match_cycle(p, len_max) + new_pols = np.zeros((len(p), len_max), dtype=np.int32) + for i in range(len(p)): + new_pols[i, :] = p[i] + return new_pols + + +def self_react(params): + '''behaviors between particles: collide, attract and fit''' + ps, collision, sum_rad, gates, att_params, fit_params = params + use_collide, use_attract, use_grow = gates + indexes = ps.params['indexes'] + if use_grow: + sum_rad = ps.rads[indexes[:, 0]] + ps.rads[indexes[:, 1]] + if use_attract: + att_params[2] = ps.mass[indexes[:, 0]] * ps.mass[indexes[:, 1]] + dif_v = ps.verts[indexes[:, 0], :] - ps.verts[indexes[:, 1], :] + dist = np.linalg.norm(dif_v, axis=1) + mask = sum_rad > dist + + index_inter = indexes[mask] + some_collisions = use_collide and len(index_inter) > 0 + some_attractions = use_attract and(len(index_inter) < len(indexes)) + + if some_collisions or some_attractions: + result = np.zeros((ps.v_len, ps.v_len, 3), dtype=np.float64) + dist_cor = np.clip(dist, 1e-6, 1e4) + normal_v = dif_v/dist_cor[:, np.newaxis] + + if some_collisions: + self_collision_force(result, dist, sum_rad, index_inter, mask, normal_v, collision) + if some_attractions: + #antimask = np.invert(mask) + antimask = True + attract_force(result, dist_cor, antimask, indexes, normal_v, att_params) + + ps.r += np.sum(result, axis=1) + + if use_grow: + fit_force(ps, index_inter, fit_params) + ps.mass = ps.density * np.power(ps.rads, 3) + + +def self_collision_force(result, dist, sum_rad, index_inter, mask, normal_v, self_collision): + '''apply collision forces between particles''' + + id0 = index_inter[:, 0] + id1 = index_inter[:, 1] + + le = dist[mask, np.newaxis] - sum_rad[mask, np.newaxis] + no = normal_v[mask] + variable_coll = len(self_collision) > 1 + sf = self_collision[:, np.newaxis] + len0, len1 = [sf[id1], sf[id0]] if variable_coll else [sf, sf] + + result[id0, id1, :] = -no * le * len0 + result[id1, id0, :] = no * le * len1 + + +def attract_force(result, dist, mask, index, norm_v, att_params): + '''apply attractions between particles''' + attract, att_decay, mass_product = att_params + + dist2 = np.power(dist, att_decay)[mask, np.newaxis] + index_non_inter = index[mask] + id0 = index_non_inter[:, 0] + id1 = index_non_inter[:, 1] + + + direction = norm_v[mask] / dist2 * mass_product[mask, np.newaxis] + variable_att = len(attract) > 1 + + att = attract + len0, len1 = [att[id1], att[id0]] if variable_att else [att, att] + + result[id0, id1, :] = - direction * len0 + result[id1, id0, :] = direction * len1 + + +def fit_force(ps, index_inter, fit_params): + '''the untouched particles will grow, the ones that collide will shrink''' + grow, min_rad, max_rad = fit_params + touch = np.unique(index_inter) + free = np.setdiff1d(np.arange(ps.v_len, dtype=np.int16), touch) + v_grow = len(grow) > 1 + grow_un, grow_tou = [grow[free], grow[touch]] if v_grow else [grow, grow] + ps.rads[free] += grow_un*0.1 + ps.rads[touch] -= grow_tou*0.1 + + ps.rads = np.clip(ps.rads, min_rad, max_rad) + + +def self_react_setup(ps, dicti, forces_composite): + '''prepare self reacting data''' + local_func, local_gates, local_params = dicti + collision, attract, att_decay, grow, min_rad, max_rad = local_params + use_self_collision, use_attract, use_grow = local_gates + + np_collision = np.array(collision) + use_self_collision = use_self_collision and np.any(np_collision > 0) + np_attract = np.array(attract)[:, np.newaxis] + use_attract = use_attract and np.any(np_attract != 0) + np_grow = np.array(grow) + use_grow = use_grow and np.any(np_grow != 0) + + use_self_react = (use_attract or use_self_collision or use_grow) + + if not use_self_react: + return + + ps.params['indexes'] = cross_indices3(ps.v_len) + sum_rad = ps.rads[ps.params['indexes'][:, 0]] + ps.rads[ps.params['indexes'][:, 1]] + + att_params = att_setup(use_attract, ps, np_attract, att_decay) + fit_params = fit_setup(use_grow, np_grow, min_rad, max_rad) + + gates = [use_self_collision, use_attract, use_grow] + params = [ps, np_collision, sum_rad, gates, att_params, fit_params] + forces_composite[0].append(local_func) + forces_composite[1].append(params) + + +def att_setup(use_attract, ps, np_attract, attract_decay): + '''Prepare self-attracting data''' + if use_attract: + np_att_decay = np.array(attract_decay) + indexes = ps.params['indexes'] + mass_product = ps.mass[indexes[:, 0]] * ps.mass[indexes[:, 1]] + att_params = [np_attract, np_att_decay, mass_product] + else: + att_params = [] + + return att_params + + +def fit_setup(use_grow, np_grow, min_rad, max_rad): + '''Prepare fitting data''' + if use_grow: + np_min_rad = np.array(min_rad) + np_max_rad = np.array(max_rad) + fit_params = [np_grow, np_min_rad, np_max_rad] + else: + fit_params = [] + + return fit_params + + +def attractors_setup(ps, dicti, forces_composite): + '''prepare attractors system and data''' + local_func, use_attractors, local_params = dicti + if use_attractors: + attractors, force, att_clamp, att_decay_power = local_params + np_attrac = np.array(attractors) + np_attrac_f = np.array(force) + np_attrac_clamp = np.array(att_clamp) + np_attrac_decay_pow = np.array(att_decay_power) + # use_att_clamp = np_attrac_clamp > 0 + params = [np_attrac, np_attrac_f, np_attrac_clamp, np_attrac_decay_pow] + + forces_composite[0].append(local_func) + forces_composite[1].append([ps] + numpy_match_long_repeat(params)) + + +def attractors_force(params): + '''apply attractors force (as planets)''' + ps, np_attrac, np_attrac_f, np_attrac_clamp, decay = params + v_attract = ps.verts[np.newaxis, :, :] - np_attrac[:, np.newaxis, :] + dist_attract = np.linalg.norm(v_attract, axis=2) + mask = dist_attract > np_attrac_clamp[:, np.newaxis] + mask_true = np.invert(mask) + dist_attract2 = np.power(dist_attract, decay[:, np.newaxis]) + dist_attract_cor = np.clip(dist_attract2[mask_true], 1e-2, 1e4) + + v_attract *= np_attrac_f[:, np.newaxis, np.newaxis] + v_attract_normal = v_attract[mask_true] / dist_attract_cor[:, np.newaxis] + + v_attract[mask_true] = -v_attract_normal + v_attract[mask, :] = 0 + + r_attract = np.sum(v_attract, axis=0) + ps.r += r_attract + + +def calc_rest_length(np_verts, np_springs): + '''calculate edges length''' + pairs_springs = np_verts[np_springs, :] + vect_rest = (pairs_springs[:, 0, :] - pairs_springs[:, 1, :]) + dist_rest = np.linalg.norm(vect_rest, axis=1) + return dist_rest + + +def spring_setup(ps, dicti, forces_composite): + '''prepare spring system and data''' + local_func, local_gates, local_params = dicti + use_springs, use_fix_len = local_gates + if use_springs: + springs, fixed_len, spring_k = local_params + spring_k = np.array(spring_k) + if np.any(spring_k != 0): + springs = np.array(springs) + + if use_fix_len or fixed_len[0] > 0: + dist_rest = fixed_len + else: + dist_rest = calc_rest_length(ps.verts, springs) + spring_params = [ps, springs, dist_rest, spring_k] + forces_composite[0].append(local_func) + forces_composite[1].append(spring_params) + + +def spring_force(spring_params): + '''apply spring forces related to edges resistance''' + ps, np_springs, dist_rest, spring_k = spring_params + + pairs = ps.verts[np_springs, :] + dif_v = pairs[:, 0, :] - pairs[:, 1, :] + dist = np.linalg.norm(dif_v, axis=1) + dif_l = dist - dist_rest + dist[dist == 0] = 1 + normal_v = dif_v / dist[:, np.newaxis] + x = dif_l[:, np.newaxis] + k = spring_k[:, np.newaxis] + + result = np.zeros((ps.v_len, 3), dtype=np.float64) + np.add.at(result, np_springs[:, 0], -normal_v * x * k) + np.add.at(result, np_springs[:, 1], normal_v * x * k) + + ps.r += result + + +def inflate_setup(ps, dicti, forces_composite): + '''prepare data to inflate''' + local_func, use_inflate, local_params = dicti + if use_inflate: + pols, inflate = local_params + 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) + pol_side_max = np.amax(pols_sides) + np_pols = expand_pols_numpy(np_pols, pol_side_max) + p_regular = False + else: + p_regular = True + pols_sides = np.array(p_len) + pol_side_max = len(pols[0]) + + inflate_params = [ps, np_pols, pol_side_max, pols_sides, inflate, p_regular] + forces_composite[0].append(local_func) + forces_composite[1].append(inflate_params) + + +def pols_normals(pol_v, mag): + '''get actual polygons normal with controlled magnitude''' + v1 = pol_v[:, 1, :] - pol_v[:, 0, :] + v2 = pol_v[:, 2, :] - pol_v[:, 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] * mag + + +def inflate_force(inflate_params): + '''apply force based on normals''' + ps, np_pols, pol_side_max, pols_sides, inflate, p_regular = inflate_params + + pol_v = ps.verts[np_pols, :] + pols_normal = pols_normals(pol_v, inflate) + result = np.zeros((ps.v_len, 3), dtype=np.float64) + + if p_regular: + p_area = calc_area(pol_side_max, pol_v, pols_normal)[:, np.newaxis] + for i in range(pol_side_max): + np.add.at(result, np_pols[:, i], pols_normal * p_area) + + else: + p_area = calc_area_var_sides(pol_side_max, pols_sides, pol_v, pols_normal)[:, np.newaxis] + for i in range(pol_side_max): + mask = pols_sides > i + np.add.at(result, np_pols[mask, i], pols_normal[mask] * p_area[mask]) + + ps.r += result + + +def calc_area(pol_side_max, pol_v, pols_normal): + '''calculate polygons area (equal sided polygons)''' + prod = np.zeros((pol_side_max, len(pols_normal), 3), dtype=np.float32) + for i in range(pol_side_max): + prod[i, :, :] = np.cross(pol_v[:, i, :], pol_v[:, (i + 1) % pol_side_max, :]) + tot = np.sum(prod, axis=0) + return abs(np.sum(tot * pols_normal, axis=1) / 2) + + +def calc_area_var_sides(pol_side_max, pols_sides, pol_v, pols_normal): + '''calculate polygons area (variable sided polygons)''' + prod = np.zeros((pol_side_max, len(pols_sides), 3), dtype=np.float32) + for i in range(pol_side_max): + mask = pols_sides > i + prod[i, mask, :] = np.cross(pol_v[mask, i, :], pol_v[mask, (i + 1) % pol_side_max, :]) + + tot = np.sum(prod, axis=0) + return abs(np.sum(tot * pols_normal, axis=1) / 2) + + +def drag_setup(ps, dicti, forces_composite): + '''prepare drag data''' + local_func, local_gates, local_params = dicti + use_drag, use_grow = local_gates + if use_drag: + drag_force = local_params + np_drag_force = np.array(drag_force)[:, np.newaxis] + use_drag = np.any(np_drag_force > 0) + if use_drag: + surf = np.power(ps.rads, 2) + drag_params = [ps, use_grow, np_drag_force, surf] + forces_composite[0].append(local_func) + forces_composite[1].append(drag_params) + + +def drag_force_apply(params): + '''apply drag force (resistance from environment)''' + ps, use_grow, drag_mag, surf = params + vel_mag = np.linalg.norm(ps.vel, axis=1) + vel_mag_zero = vel_mag == 0 + vel_mag[vel_mag_zero] = 1 + # vel_mag2 = vel_mag * vel_mag + vel_mag2 = vel_mag + vel_norm = ps.vel/vel_mag[:, np.newaxis] + if use_grow: + surf = np.power(ps.rads, 2) + drag = -vel_norm * drag_mag * vel_mag2[:, np.newaxis] * surf[:, np.newaxis] + + ps.r += drag + + +def random_setup(ps, dicti, forces_composite): + '''initialize random force''' + local_func, local_gate, local_params = dicti + if local_gate: + random_seed, random_force, random_variation = local_params + np_random_force = np.array(random_force) + if any(np_random_force != 0): + np_random_variation = np.array(random_variation) + random_force = np_random_force[:, np.newaxis] + random_variation = np_random_variation[:, np.newaxis] + random_variate = any(random_variation != 0) + np.random.seed(random_seed[0]) + ps.random_v = random_force * np.random.random((ps.v_len, 3)) - random_force / 2 + random_params = [ps, random_force, random_variate, random_variation] + forces_composite[0].append(local_func) + forces_composite[1].append(random_params) + + +def random_force_apply(params): + '''apply random vectors and change it''' + ps, random_force, random_variate, random_variation = params + if random_variate: + random_var = 2 * random_force * np.random.random((ps.v_len, 3)) - random_force + ps.random_v = ps.random_v * (1 - random_variation) + random_var * random_variation + ps.r += ps.random_v + + +def baricentric_mask(p_triang, edges, np_pol_v, coll_normals, ed_id, rad_axis): + '''helper function to mask which points are inside the triangles''' + edge = edges[:, ed_id, :] + v0 = np_pol_v[:, ed_id, :] + vp0 = p_triang - v0[:, np.newaxis, :] + cross = np.cross(edge[:, np.newaxis, :], vp0) + return np.sum(coll_normals * cross, axis=2) > -rad_axis + + +def collisions_setup(ps, dicti, limits_composite): + '''prepare collision data''' + local_func, use_collide, local_params = dicti + if use_collide: + obstacles, obstacles_pols, obstacles_bounce = local_params + + np_collide_v = np.array(obstacles) + np_collide_pol = np.array(obstacles_pols, dtype=np.int16) + np_pol_v = np_collide_v[np_collide_pol] + collide_v1 = np_pol_v[:, 1, :] - np_pol_v[:, 0, :] + collide_v2 = np_pol_v[:, 2, :] - np_pol_v[:, 0, :] + edges = np.zeros(np_pol_v.shape, dtype=np.float32) + edges[:, 0, :] = collide_v1 + edges[:, 1, :] = np_pol_v[:, 2, :] - np_pol_v[:, 1, :] + edges[:, 2, :] = np_pol_v[:, 0, :] - np_pol_v[:, 2, :] + + coll_norm = collide_normals_get(collide_v1, collide_v2) + coll_p_co = np_pol_v[:, 0, :] + bounce = np.array(obstacles_bounce) + collide_params = [ps, coll_p_co, coll_norm, np_pol_v, bounce, edges] + limits_composite[0].append(local_func) + limits_composite[1].append(collide_params) + + +def collide_normals_get(collide_v1, collide_v2): + '''get normalized normals''' + collide_normals = np.cross(collide_v1, collide_v2) + collide_normals_d = np.linalg.norm(collide_normals, axis=1) + return collide_normals / collide_normals_d[:, np.newaxis] + + +def b_box_coll_filter(np_pol_v, verts, rad): + '''filter intersections by checking bounding box overlap''' + verts_x = verts[np.newaxis, :, :] + rads_x = rad[np.newaxis, :, np.newaxis] + keep = np.amin(np_pol_v, axis=1)[:, np.newaxis, :] < verts_x + rads_x + keep2 = np.amax(np_pol_v, axis=1)[:, np.newaxis, :] > verts_x - rads_x + keep3 = np.any(keep*keep2, axis=0) + keep = np.any(keep*keep2, axis=1) + verts_m = np.all(keep3, axis=1) + # print(keep2.shape, keep3.shape, verts_m.shape,verts_x.shape) + pols_m = np.all(keep, axis=1) + + return pols_m, verts_m + + +def collisions_apply(collide_params): + '''apply collisions with obstacles''' + ps, collide_p_co, collide_normals, pol_v, bounce, edges = collide_params + + pols_m, verts_m = b_box_coll_filter(pol_v, ps.verts, ps.rads) + + verts = ps.verts[verts_m] + rads = ps.rads[verts_m] + vels = ps.vel[verts_m] + index = ps.index[verts_m] + + coll_norm = collide_normals[pols_m, np.newaxis, :] + coll_dist = collision_dist(verts, collide_p_co[pols_m], coll_norm) + coll_inter = np.absolute(coll_dist) - rads[np.newaxis, :] + coll_mask = coll_inter < 0 + if not np.any(coll_mask): + return + + coll_sign = 2*(coll_dist > 0) - 1 + mask_none = np.any(coll_mask, axis=0) + + p_triang = verts[mask_none] + coll_dist[:, mask_none, np.newaxis] * coll_norm + rad_axis = rads[np.newaxis, mask_none] + index = index[mask_none] + coll_mask = coll_mask[:, mask_none] + coll_inter = coll_inter[:, mask_none] + coll_sign = coll_sign[:, mask_none] + vels = vels[mask_none] + + wuv = pts_in_tris(p_triang, edges[pols_m], pol_v[pols_m], coll_norm, rad_axis, coll_mask) + num_int = np.maximum(np.sum(wuv, axis=0), 1)[:, np.newaxis] + + displace = np.zeros((ps.v_len, 3), dtype=np.float32) + velocity = np.zeros((ps.v_len, 3), dtype=np.float32) + displace[index, :] = coll_displace(coll_norm, coll_inter, coll_sign, wuv, num_int) + velocity[index, :] = coll_vel(coll_norm, vels, wuv, num_int) + ps.verts += displace + ps.vel += velocity * bounce + + +def collision_dist(verts, collide_p_co, coll_norm): + '''get collision distance and normal''' + vector_coll = verts[np.newaxis, :, :] - collide_p_co[:, np.newaxis, :] + coll_dist = np.sum(vector_coll * coll_norm, axis=2) + return coll_dist + + +def pts_in_tris(p_triang, edges, pol_v, coll_normals, rad_axis, coll_mask): + '''calculate if points are inside the triangles''' + w = baricentric_mask(p_triang, edges, pol_v, coll_normals, 0, rad_axis) + u = baricentric_mask(p_triang, edges, pol_v, coll_normals, 1, rad_axis) + v = baricentric_mask(p_triang, edges, pol_v, coll_normals, 2, rad_axis) + return w * u * v * coll_mask + + +def coll_displace(coll_normals, coll_inter, coll_sign, wuv, num_int): + '''calculate resultant collision displacement''' + rr = wuv[:, :, np.newaxis] * coll_normals * coll_inter[:, :, np.newaxis]*-coll_sign[:, :, np.newaxis] + return np.sum(rr, axis=0) / num_int + + +def coll_vel(coll_normals, vels, wuv, num_int): + '''calculate resultant collision speed''' + new_vel_mag = np.sum(coll_normals * vels[np.newaxis, :, :], axis=-1) + new_vel = new_vel_mag[:, :, np.newaxis] * coll_normals * wuv[:, :, np.newaxis] + return np.sum(new_vel, axis=0)*-2 / num_int + + +def b_box_setup(ps, dicti, limits_composite): + '''prepare b_box data''' + local_func, use_b_box, b_box = dicti + if use_b_box: + np_bbox = np.array(b_box) + bbox_max = np.amax(np_bbox, axis=0) + bbox_min = np.amin(np_bbox, axis=0) + b_box_params = [ps, bbox_min, bbox_max] + limits_composite[0].append(local_func) + limits_composite[1].append(b_box_params) + + +def b_box_apply(b_box_params): + '''apply hard limit to verts''' + ps, bbox_min, bbox_max = b_box_params + ps.verts = np.clip(ps.verts, bbox_min + ps.rads[:, np.newaxis], bbox_max - ps.rads[:, np.newaxis]) + + +def world_forces_setup(ps, dicti, forces_composite): + '''prepare world forces data''' + local_func, gates, params = dicti + use_world_f, size_change = gates + world_forces, wind = params + if use_world_f: + np_world_f = np.array(world_forces) + np_wind = np.array(wind) + if len(world_forces) > 1: + np_world_f = numpy_fit_long_repeat([np_world_f], ps.v_len)[0] + if not size_change: + np_world_f = np_world_f * ps.mass[:, np.newaxis] + func = local_func[0] + else: + func = local_func[1] + if len(np_wind)> 1: + np_wind = numpy_fit_long_repeat([np_wind], ps.v_len)[0] + + ps.params['gravity'] = np_world_f + ps.params['wind'] = np_wind + forces_composite[0].append(func) + forces_composite[1].append(ps) + + +def world_forces_apply(ps): + '''apply constant forces''' + ps.r += ps.params['gravity'] + ps.params['wind'] + + +def world_forces_apply_var(ps): + '''apply constant forces''' + + ps.r += ps.params['gravity'] * ps.mass[:, np.newaxis] + ps.params['wind'] + + +def pins_setup(ps, dicti, forces_composite): + '''prepare pins data''' + + ps.pins_init(dicti, forces_composite) + + +def pins_apply(ps): + '''apply pin mask''' + ps.apply_pins() + + +def apply_forces_setup(ps, dicti, forces_composite): + '''prepare applying all forces process''' + local_func, _, _ = dicti + forces_composite[0].append(local_func) + forces_composite[1].append(ps) + + +def apply_all_forces(ps): + '''applying forces and reseting resultant''' + ps.apply_forces() + + +def limit_speed(np_vel, max_vel): + ''''constrain speed magniture''' + vel_mag = np.linalg.norm(np_vel, axis=1) + vel_exceded = vel_mag > max_vel + np_vel[vel_exceded] = np_vel[vel_exceded] / vel_mag[vel_exceded, np.newaxis] * max_vel + +FORCE_CHAIN = ["self_react", "springs", "drag", "inflate", "random", "attractors", "world_f", "pins", "apply_f", "b_box", "Obstacles"] + +class PulgaSystem(): + '''Store states''' + verts, rads, vel, density = [[], [], [], []] + v_len = [] + params = {} + def __init__(self, init_params): + self.main_setup(init_params) + self.mass = self.density * np.power(self.rads, 3) + self.random_v = [] + self.r = np.zeros((self.v_len, 3), dtype=np.float64) + self.index = np.arange(self.v_len) + + def main_setup(self, local_params): + '''prepare main data''' + p = self.params + initial_pos, rads_in, initial_vel, max_vel, density = local_params + self.verts = np.array(initial_pos) + self.rads = np.array(rads_in, dtype=np.float64) + self.vel = np.array(initial_vel, dtype=np.float64) + self.v_len = len(self.verts) + p['max_vel'] = np.array(max_vel) + self.rads, self.vel = numpy_fit_long_repeat([self.rads, self.vel], self.v_len) + + if len(p['max_vel']) > 1: + p['max_vel'] = numpy_fit_long_repeat([p['max_vel']], self.v_len)[0] + + self.density = np.array(density) + if len(density) > 1: + self.density = numpy_fit_long_repeat([self.density], self.v_len)[0] + + def hard_update(self, cache, size_change, pins_gates): + '''replace verts, rads and vel (in NumPy)''' + verts, rads, vel = cache + if len(verts) == self.v_len: + if pins_gates[0] and pins_gates[1]: + unpinned = self.params['unpinned'] + self.verts[unpinned] = verts[unpinned] + else: + self.verts = verts + self.vel = vel + if not size_change: + self.rads = rads + + def hard_update_list(self, cache, size_change, pins_gates): + '''replace verts, rads and velocity''' + verts, rads, vel = cache + if type(verts) == list: + if len(verts) == self.v_len: + if pins_gates[0] and pins_gates[1]: + unpinned = self.params['unpinned'] + self.verts[unpinned] = np.array(verts)[unpinned] + else: + self.verts = np.array(verts) + if not size_change: + self.rads = np.array(rads) + self.vel = np.array(vel) + else: + self.hard_update(cache, size_change, pins_gates) + + def apply_forces(self): + '''resultant --> acceleration --> speed --> position''' + acc = self.r / self.mass[:, np.newaxis] + self.vel += acc + + if np.any(self.params['max_vel']) > 0: + limit_speed(self.vel, self.params['max_vel']) + + self.verts += self.vel + self.r[:] = 0 + + def pins_init(self, dicti, forces_composite): + local_func, local_gates, local_params = dicti + pins, pins_goal_pos = local_params + use_pins, use_pins_goal = local_gates + + if not use_pins: + return + + self.params['pins'] = np.array(pins) + + if self.params['pins'].dtype == np.int32: + if len(self.params['pins']) == len(self.verts): + self.params['pins'] = self.params['pins'] == 1 + self.params['unpinned'] = np.invert(self.params['pins']) + else: + self.params['unpinned'] = np.ones(len(self.verts), dtype=np.bool) + self.params['unpinned'][self.params['pins']] = False + + self.vel[self.params['pins'], :] = 0 + + if use_pins_goal: + self.verts[self.params['pins'], :] = np.array(pins_goal_pos) + forces_composite[0].append(local_func) + forces_composite[1].append(self) + return + + + def apply_pins(self): + '''cancel forces on pins''' + self.r[self.params["pins"], :] = 0 + + +FUNC_DICT = { + "self_react": self_react, + "springs": spring_force, + "drag": drag_force_apply, + "inflate": inflate_force, + "random": random_force_apply, + "attractors": attractors_force, + "world_f": (world_forces_apply, world_forces_apply_var), + "pins": PulgaSystem.apply_pins, + "apply_f": PulgaSystem.apply_forces, + "b_box": b_box_apply, + "Obstacles": collisions_apply + + } +INIT_FUNC_DICT = { + "self_react": self_react_setup, + "springs": spring_setup, + "drag": drag_setup, + "inflate": inflate_setup, + "random": random_setup, + "attractors": attractors_setup, + "world_f": world_forces_setup, + "pins": pins_setup, + "apply_f": apply_forces_setup, + "b_box": b_box_setup, + "Obstacles": collisions_setup + + } + + +def fill_gates_dict(gates_dict, gates): + '''redistribute booleans''' + gates_dict["accumulate"] = gates[0] + gates_dict["self_react"] = gates[1:4] + gates_dict["springs"] = gates[4:6] + gates_dict["pins"] = gates[6:8] + gates_dict["drag"] = [gates[8]] + [gates[3]] + gates_dict["inflate"] = gates[9] + gates_dict["random"] = gates[10] + gates_dict["attractors"] = gates[11] + gates_dict["world_f"] = gates[12] + gates_dict["Obstacles"] = gates[13] + gates_dict["b_box"] = gates[14] + gates_dict["output"] = gates[15:] + gates_dict["apply_f"] = True + +PARAMS_GROUPS = { + "main": ("Initial_Pos", "rads_in", 'Initial Velocity', "max_vel", "Density"), + "springs": ("springs", "fixed_len", "spring_k" ), + "pins": ("pins", "pins_goal_pos"), + "self_react": ("self_collision", "self_attract", "attract_decay", "grow", "min_rad", "max_rad"), + "inflate": ("Pols", "inflate"), + "drag": ("drag_force"), + "attractors": ("Attractors", "force", "att_clamp", "att_decay_power"), + "random": ("random_seed", "random_force", "random_variation"), + "world_f": ("Gravity", "Wind"), + "b_box": ("Bounding Box"), + "Obstacles": ("Obstacles", "Obstacles_pols", "obstacles_bounce"), + +} +def fill_params_dict(p_dict, parameters, par): + '''redistribute parameters''' + for x in PARAMS_GROUPS: + if type(PARAMS_GROUPS[x]) == tuple: + p_dict[x] = [par[p] for p in PARAMS_GROUPS[x] ] + else: + p_dict[x] = par[PARAMS_GROUPS[x]] + + + p_dict["apply_f"] = True + + +def local_dict(dictionaries, name): + '''get all related to the name''' + return [dictionaries[0][name], dictionaries[1][name], dictionaries[2][name]] + + +def pulga_system_init(params, parameters, gates, out_lists, cache): + '''the main function of the engine''' + + dictionaries = [FUNC_DICT, gates, {}] + fill_params_dict(dictionaries[2], parameters, params) + force_map = [] + force_parameters = [] + forces_composite = [force_map, force_parameters] + ps = PulgaSystem(dictionaries[2]["main"]) + for force in FORCE_CHAIN: + INIT_FUNC_DICT[force](ps, local_dict(dictionaries, force), forces_composite) + + iterations = parameters[1] + iterations_max = max(iterations) + iterations_rec = [i-1 for i in iterations] + out_params = [iterations_rec, ps, dictionaries[1]["output"], out_lists] + + if dictionaries[1]["accumulate"]: + if len(cache) > 0: + # ps.hard_update(cache) + ps.hard_update_list(cache, gates["self_react"][2], gates["pins"]) + + iterate(iterations_max, force_map, force_parameters, out_params) + + return ps.verts, ps.rads, ps.vel + + +def iterate(iterations_max, force_map, force_parameters, out_params): + ''' execute repeatedly the defined force map''' + num_forces = len(force_map) + for it in range(iterations_max): + for i in range(num_forces): + force_map[i](force_parameters[i]) + output_data(it, out_params) + + +def output_data(it, params): + '''if is pertinent output the data''' + iterations_rec, ps, gate, out_lists = params + record_iteration = it in iterations_rec + if record_iteration: + data_out = prepare_output_data(ps, gate) + record_data(data_out, out_lists) + + +def prepare_output_data(ps, gate): + '''prepare data to output''' + use_numpy_out = gate + + if use_numpy_out: + return [ps.verts, ps.rads, ps.vel] + else: + return [ps.verts.tolist(), ps.rads.tolist(), ps.vel.tolist()] + + +def record_data(data_out, out_lists): + '''save data to main list''' + verts_out, rads_out, velocity_out = out_lists + new_verts, new_rad, new_vel = data_out + verts_out.append(new_verts) + rads_out.append(new_rad) + velocity_out.append(new_vel) -- GitLab From 9dda29467d032f147ea77494c076fae6d0dfba79 Mon Sep 17 00:00:00 2001 From: Victor Doval <10011941+vicdoval@users.noreply.github.com> Date: Sun, 24 Mar 2019 16:06:04 +0100 Subject: [PATCH 076/137] added unique feature to Random Num Gen --- docs/nodes/number/random_num_gen.rst | 3 +++ nodes/number/random_num_gen.py | 26 +++++++++++++++++++++----- 2 files changed, 24 insertions(+), 5 deletions(-) diff --git a/docs/nodes/number/random_num_gen.rst b/docs/nodes/number/random_num_gen.rst index b9d75b351..82e49bf7e 100644 --- a/docs/nodes/number/random_num_gen.rst +++ b/docs/nodes/number/random_num_gen.rst @@ -26,6 +26,9 @@ Inputs & Parameters +----------------+------------------------------------------------------------------------+ | Weights | On "Int" mode. Can be supplied to create a non-uniform distribution | +----------------+------------------------------------------------------------------------+ +| Unique | On "Int" mode. Outputs non-repeated numbers. | +| | The output size will be limited to (High - Low + 1) | ++----------------+------------------------------------------------------------------------+ | Distribution | On "Float" mode many distribution functions can be selected. | | | Beta, Binomial, Chi_square, Exponential, F Distrib., Gamma, Geometric, | | | Gumbel, Laplace, Logistic, Log Normal, Log Series, Negative Binomial, | diff --git a/nodes/number/random_num_gen.py b/nodes/number/random_num_gen.py index a191296fc..1465c3012 100644 --- a/nodes/number/random_num_gen.py +++ b/nodes/number/random_num_gen.py @@ -24,7 +24,7 @@ import bpy from bpy.props import BoolProperty, FloatProperty, IntProperty from sverchok.node_tree import SverchCustomTreeNode -from sverchok.data_structure import updateNode, match_long_repeat +from sverchok.data_structure import updateNode, match_long_repeat, fullList class SvRndNumGen(bpy.types.Node, SverchCustomTreeNode): @@ -165,6 +165,12 @@ class SvRndNumGen(bpy.types.Node, SverchCustomTreeNode): default=False, update=adjust_inputs) + unique = BoolProperty( + name='Unique', + description='Output non-repeated numbers', + default=False, + update=updateNode) + distribute_mode = bpy.props.EnumProperty( name="Distribution", items=distribute_options, @@ -194,7 +200,11 @@ class SvRndNumGen(bpy.types.Node, SverchCustomTreeNode): row = layout.row() row.prop(self, 'type_selected_mode', expand=True) if self.type_selected_mode == "Int": - layout.prop(self, "weighted") + c2 = layout.box() + c1 = c2.column() + c1.prop(self, "unique" ) + c1.prop(self, "weighted") + else: layout.prop(self, "distribute_mode") @@ -212,16 +222,22 @@ class SvRndNumGen(bpy.types.Node, SverchCustomTreeNode): else: size, seed, low, high, weights = params size = max(size, 1) + if self.unique: + size = min(size,high + 1 - low) seed = max(seed, 0) np.random.seed(seed) low, high = sorted([low, high]) + population = range(low, high + 1) + if self.weighted and len(weights) > 0: - population, weights = match_long_repeat([range(low, high+1), weights]) + fullList(weights,size) + weights = weights[:size] + total_weight = sum(weights) weights = [w/total_weight for w in weights] - result = np.random.choice(population, size, p=weights) + result = np.random.choice(population, size, replace=(not self.unique), p=weights) else: - result = np.random.random_integers(low, high, size) + result = np.random.choice(population, size, replace=(not self.unique)) return result -- GitLab From fcc8f7935779cac66a6f8ea31dd054cca13c33c4 Mon Sep 17 00:00:00 2001 From: Victor Doval <10011941+vicdoval@users.noreply.github.com> Date: Sun, 24 Mar 2019 16:08:13 +0100 Subject: [PATCH 077/137] minor cleaning of Random Num Gen --- nodes/number/random_num_gen.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/nodes/number/random_num_gen.py b/nodes/number/random_num_gen.py index 1465c3012..13ef2a866 100644 --- a/nodes/number/random_num_gen.py +++ b/nodes/number/random_num_gen.py @@ -204,7 +204,7 @@ class SvRndNumGen(bpy.types.Node, SverchCustomTreeNode): c1 = c2.column() c1.prop(self, "unique" ) c1.prop(self, "weighted") - + else: layout.prop(self, "distribute_mode") @@ -230,11 +230,10 @@ class SvRndNumGen(bpy.types.Node, SverchCustomTreeNode): population = range(low, high + 1) if self.weighted and len(weights) > 0: - fullList(weights,size) + fullList(weights, size) weights = weights[:size] - total_weight = sum(weights) - weights = [w/total_weight for w in weights] + weights = [w / total_weight for w in weights] result = np.random.choice(population, size, replace=(not self.unique), p=weights) else: result = np.random.choice(population, size, replace=(not self.unique)) -- GitLab From 9c15c324b82179c83f497239b43e305c9925323c Mon Sep 17 00:00:00 2001 From: Victor Doval <10011941+vicdoval@users.noreply.github.com> Date: Sun, 24 Mar 2019 16:57:03 +0100 Subject: [PATCH 078/137] docs example and UI fix of Random Num Gen --- docs/nodes/number/random_num_gen.rst | 8 +++++++- nodes/number/random_num_gen.py | 8 +++----- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/docs/nodes/number/random_num_gen.rst b/docs/nodes/number/random_num_gen.rst index 82e49bf7e..99d5d22a7 100644 --- a/docs/nodes/number/random_num_gen.rst +++ b/docs/nodes/number/random_num_gen.rst @@ -27,7 +27,7 @@ Inputs & Parameters | Weights | On "Int" mode. Can be supplied to create a non-uniform distribution | +----------------+------------------------------------------------------------------------+ | Unique | On "Int" mode. Outputs non-repeated numbers. | -| | The output size will be limited to (High - Low + 1) | +| | The output size will be limited to (Int High - Int Low + 1) | +----------------+------------------------------------------------------------------------+ | Distribution | On "Float" mode many distribution functions can be selected. | | | Beta, Binomial, Chi_square, Exponential, F Distrib., Gamma, Geometric, | @@ -85,3 +85,9 @@ The distribution functions can lead from the default Uniform to a more organic r .. image:: https://user-images.githubusercontent.com/10011941/46135077-a82ebc80-c244-11e8-9616-6e8cb7218726.png :alt: Random_Distribution4.PNG + +The "Unique" toggle will make sure there are not repeated numbers, very usefull with the "List Item Node". + +.. image:: https://user-images.githubusercontent.com/10011941/54881844-9e34f180-4e54-11e9-8c92-3eee832c6958.png + :alt: Random_Distribution_Swerchok_parametric_design_random_sample_unique_example + diff --git a/nodes/number/random_num_gen.py b/nodes/number/random_num_gen.py index 13ef2a866..dc3dd7c3a 100644 --- a/nodes/number/random_num_gen.py +++ b/nodes/number/random_num_gen.py @@ -200,11 +200,9 @@ class SvRndNumGen(bpy.types.Node, SverchCustomTreeNode): row = layout.row() row.prop(self, 'type_selected_mode', expand=True) if self.type_selected_mode == "Int": - c2 = layout.box() - c1 = c2.column() - c1.prop(self, "unique" ) - c1.prop(self, "weighted") - + c1 = layout.row(align=True) + c1.prop(self, "unique", toggle=True ) + c1.prop(self, "weighted", toggle=True) else: layout.prop(self, "distribute_mode") -- GitLab From 2f5d85f7d2880eca8ccec60ada1e26c110a2924c Mon Sep 17 00:00:00 2001 From: Victor Doval <10011941+vicdoval@users.noreply.github.com> Date: Sun, 24 Mar 2019 17:23:21 +0100 Subject: [PATCH 079/137] moved Input switch to logic category --- docs/nodes/{modifier_change => logic}/input_switch.rst | 0 docs/nodes/logic/logic_index.rst | 1 + docs/nodes/modifier_change/modifier_change_index.rst | 1 - index.md | 3 +-- nodes/{modifier_change => logic}/input_switch.py | 0 5 files changed, 2 insertions(+), 3 deletions(-) rename docs/nodes/{modifier_change => logic}/input_switch.rst (100%) rename nodes/{modifier_change => logic}/input_switch.py (100%) diff --git a/docs/nodes/modifier_change/input_switch.rst b/docs/nodes/logic/input_switch.rst similarity index 100% rename from docs/nodes/modifier_change/input_switch.rst rename to docs/nodes/logic/input_switch.rst diff --git a/docs/nodes/logic/logic_index.rst b/docs/nodes/logic/logic_index.rst index fe0a017be..c304fbc8e 100644 --- a/docs/nodes/logic/logic_index.rst +++ b/docs/nodes/logic/logic_index.rst @@ -8,3 +8,4 @@ Logic logic_node neuro_elman switch + input_switch diff --git a/docs/nodes/modifier_change/modifier_change_index.rst b/docs/nodes/modifier_change/modifier_change_index.rst index 49537e8f7..9cbfb9057 100644 --- a/docs/nodes/modifier_change/modifier_change_index.rst +++ b/docs/nodes/modifier_change/modifier_change_index.rst @@ -16,7 +16,6 @@ Modifier Change extrude_region holes_fill iterate - input_switch mesh_join mesh_separate mesh_switch diff --git a/index.md b/index.md index 2fa0cc4fa..7bb5cc67d 100644 --- a/index.md +++ b/index.md @@ -111,8 +111,6 @@ SvVertMaskNode SvTransformSelectNode SvSplitEdgesNode - --- - SvInputSwitchNode ## Modifier Make LineConnectNodeMK2 @@ -230,6 +228,7 @@ ## Logic SvLogicNode SvSwitchNode + SvInputSwitchNode SvNeuroElman1LNode ## Viz diff --git a/nodes/modifier_change/input_switch.py b/nodes/logic/input_switch.py similarity index 100% rename from nodes/modifier_change/input_switch.py rename to nodes/logic/input_switch.py -- GitLab From 3f75540eadf626ba0ec81e59983a437b0a5d8ccf Mon Sep 17 00:00:00 2001 From: nortikin Date: Sun, 24 Mar 2019 22:43:48 +0300 Subject: [PATCH 080/137] try fix bugs --- nodes/network/udp_client_mk2.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/nodes/network/udp_client_mk2.py b/nodes/network/udp_client_mk2.py index 67591e215..f796dbd0f 100644 --- a/nodes/network/udp_client_mk2.py +++ b/nodes/network/udp_client_mk2.py @@ -129,6 +129,8 @@ class SvUdpClientNodeMK2(bpy.types.Node, SverchCustomTreeNode): def sv_init(self, context): self.inputs.new('StringsSocket', 'send') #.prop_name = 'send' self.outputs.new('StringsSocket', 'receive') + ev = threading.Thread(target=self.recv_msg, args=(context,)) + ev.start() @profile -- GitLab From 586e219a203ba731489cb97dea5e93ee8a3fdecd Mon Sep 17 00:00:00 2001 From: Victor Doval <10011941+vicdoval@users.noreply.github.com> Date: Fri, 29 Mar 2019 18:28:48 +0100 Subject: [PATCH 081/137] ui fixing and docs correction --- docs/nodes/alpha_nodes/pulga_physics.rst | 176 ++++++++++++----------- nodes/modifier_change/pulga_physics.py | 108 ++++++-------- utils/pulga_physics_core.py | 62 ++++---- 3 files changed, 163 insertions(+), 183 deletions(-) diff --git a/docs/nodes/alpha_nodes/pulga_physics.rst b/docs/nodes/alpha_nodes/pulga_physics.rst index 81b376d43..c7f2260a5 100644 --- a/docs/nodes/alpha_nodes/pulga_physics.rst +++ b/docs/nodes/alpha_nodes/pulga_physics.rst @@ -6,95 +6,109 @@ Functionality This node creates simulations from input parameters, it is meant to be used in form-finding purposes. It creates the simulation using the inputed vertices as spheric particles that react to applied forces. - +Tne node is a basic NumPy implementation of basic physics system heavily inspired in "The Nature of Code" by Daniel Shiffman +and the Kangoroo Plugin for Grasshopper. Due the nature of the algorithm it can get very intensive, handle with responsibility Input & Output -------------- -+------------------------+---------------+-------------+-----------------------------------------------+ -| Input | Type | Default | Description | -+========================+===============+=============+===============================================+ -| **Initial_Pos** | Vertices | None | Vertices in original state | -+------------------------+---------------+-------------+-----------------------------------------------+ -| **Iterations** | Vertices | None | Number of iterations of the process | -+------------------------+---------------+-------------+-----------------------------------------------+ -| **Springs** | Strings | None | Edges referenced to vertices | -+------------------------+---------------+-------------+-----------------------------------------------+ -| **Springs Length** | Strings | None | Specify spring rest length, | -| | | | 0 to calculate it from initial position | -+------------------------+---------------+-------------+-----------------------------------------------+ -| **Springs Stiffness** | Strings | None | Springs stiffness constant | -+------------------------+---------------+-------------+-----------------------------------------------+ -| **pins** | Strings | None | Indexes of vertices with fixed position, | -| | | | accepts a vertices mask and index number | -+------------------------+---------------+-------------+-----------------------------------------------+ -| **pins_gola_pos | Vertices | None | Final position of pinned vertices | -+------------------------+---------------+-------------+-----------------------------------------------+ -| **Radius** | Strings | None | Radius of virtual sphere, used to | -| | | | calculate intersections, mass and surface | -+------------------------+---------------+-------------+-----------------------------------------------+ -| **Self Collision** | Strings | None | collision force. | -| | | | Applied when vertex distance < sum Radius | -+------------------------+---------------+-------------+-----------------------------------------------+ -| **Self Attract** | Strings | None | attract force magnitude | -+------------------------+---------------+-------------+-----------------------------------------------+ -| **Self Attract Decay** | Strings | None | attract force decay with distance (power) | -| | | | 0 = no decay, 1 = linear, 2 = quadratic... | -+------------------------+---------------+-------------+-----------------------------------------------+ -| **Grow** | Strings | None | Radius variance factor. | -| | | | Shrink when they intersect, Grow when don't. | -+------------------------+---------------+-------------+-----------------------------------------------+ -| **Min Radius** | Strings | None | Minimum radius limit | -+------------------------+---------------+-------------+-----------------------------------------------+ -| **Max Radius** | Strings | None | Maximum radius limit | -+------------------------+---------------+-------------+-----------------------------------------------+ -| **Pols** | Strings | None | Pols referenced to vertices | -| | | |(used to calculate inflate force) | -+------------------------+---------------+-------------+-----------------------------------------------+ -| **Inflate** | Strings | None | Inflate force magnitude (per surface unit) | -+------------------------+---------------+-------------+-----------------------------------------------+ -| **Initial Velocity** | Vertices | None | Initial vertices velocity | -+------------------------+---------------+-------------+-----------------------------------------------+ -| **Max Velocity** | Strings | None | Maximum vertices velocity | -+------------------------+---------------+-------------+-----------------------------------------------+ -| **Drag Force** | Strings | None | Movement resistance from environment | -+------------------------+---------------+-------------+-----------------------------------------------+ -| **Attractors** | Vertices | None | Attractors origins | -+------------------------+---------------+-------------+-----------------------------------------------+ -| **Attractors Force** | Strings | None | Attractors Force magnitude | -+------------------------+---------------+-------------+-----------------------------------------------+ -| **Attractors Clamp** | Strings | None | Attractors maximum influence distance | -+------------------------+---------------+-------------+-----------------------------------------------+ -| **Attractors Decay** | Strings | None | Decay with distance | -| | | | 0 = no decay, 1 = linear, 2 = quadratic... | -+------------------------+---------------+-------------+-----------------------------------------------+ -| **Random Seed | Strings | None | Random seed number | -+------------------------+---------------+-------------+-----------------------------------------------+ -| **Random Force** | Strings | None | Random force magnitude | -+------------------------+---------------+-------------+-----------------------------------------------+ -| **Random Variation** | Strings | None | Random force variation | -+------------------------+---------------+-------------+-----------------------------------------------+ -| **Density** | Strings | None | Particles Density | -+------------------------+---------------+-------------+-----------------------------------------------+ -| **Gravity** | Vectors | None | Constant forces that are mass independent | -+------------------------+---------------+-------------+-----------------------------------------------+ -| **Wind** | Vertices | None | constant forces that are mass dependent | -+------------------------+---------------+-------------+-----------------------------------------------+ -| **Bounding Box** | Vertices | None | Limits of the system. It will work with the | -| | | | bounding box of the given vectors (min. two) | -+------------------------+---------------+-------------+-----------------------------------------------+ -| **Obstacles** | Vertices | None | Obstacles vertices | -+------------------------+---------------+-------------+-----------------------------------------------+ -| **Obstacles Pols** | Strings | None | Pols referenced to obstacles vertices. | -| | | |(they must be triangles) | -+------------------------+---------------+-------------+-----------------------------------------------+ -| **Obstacles Bounce** | Strings | None | Obstacles Bounce force | -+------------------------+---------------+-------------+-----------------------------------------------+ ++------------------------+---------------+-----------------------------------------------+ +| Input | Type | Description | ++========================+===============+===============================================+ +| **Initial_Pos** | Vertices | Vertices in original state | ++------------------------+---------------+-----------------------------------------------+ +| **Iterations** | Vertices | Number of iterations of the process | ++------------------------+---------------+-----------------------------------------------+ +| **Springs** | Strings | Edges referenced to vertices | ++------------------------+---------------+-----------------------------------------------+ +| **Springs Length** | Strings | Specify spring rest length, | +| | | 0 to calculate it from initial position | ++------------------------+---------------+-----------------------------------------------+ +| **Springs Stiffness** | Strings | Springs stiffness constant | ++------------------------+---------------+-----------------------------------------------+ +| **Pins** | Strings | Indexes of vertices with fixed position, | +| | | accepts a vertices mask and index number | ++------------------------+---------------+-----------------------------------------------+ +| **pins_goal_pos | Vertices | Final position of pinned vertices | ++------------------------+---------------+-----------------------------------------------+ +| **Radius** | Strings | Radius of virtual sphere, used to | +| | | calculate intersections, mass and surface | ++------------------------+---------------+-----------------------------------------------+ + +| **Self Collision** | Strings | Collision force. | +| | | Applied when vertex distance < sum Radius | ++------------------------+---------------+-----------------------------------------------+ +| **Self Attract** | Strings | Attract force magnitude. | +| | | Applied when vertex distance > sum Radius | ++------------------------+---------------+-----------------------------------------------+ +| **Self Attract Decay** | Strings | attract force decay with distance (power) | +| | | 0 = no decay, 1 = linear, 2 = quadratic... | ++------------------------+---------------+-----------------------------------------------+ +| **Grow** | Strings | Radius variance factor. | +| | | Shrink when they intersect, Grow when don't. | ++------------------------+---------------+-----------------------------------------------+ +| **Min Radius** | Strings | Minimum radius limit | ++------------------------+---------------+-----------------------------------------------+ +| **Max Radius** | Strings | Maximum radius limit | ++------------------------+---------------+-----------------------------------------------+ +| **Pols** | Strings | Pols referenced to vertices | +| | |(used to calculate inflate force) | ++------------------------+---------------+-----------------------------------------------+ +| **Inflate** | Strings | Inflate force magnitude (per surface unit) | ++------------------------+---------------+-----------------------------------------------+ +| **Initial Velocity** | Vertices | Initial vertices velocity | ++------------------------+---------------+-----------------------------------------------+ +| **Max Velocity** | Strings | Maximum vertices velocity | ++------------------------+---------------+-----------------------------------------------+ +| **Drag Force** | Strings | Movement resistance from environment | ++------------------------+---------------+-----------------------------------------------+ +| **Attractors** | Vertices | Attractors origins | ++------------------------+---------------+-----------------------------------------------+ +| **Attractors Force** | Strings | Attractors Force magnitude | ++------------------------+---------------+-----------------------------------------------+ +| **Attractors Clamp** | Strings | Attractors maximum influence distance | ++------------------------+---------------+-----------------------------------------------+ +| **Attractors Decay** | Strings | Decay with distance | +| | | 0 = no decay, 1 = linear, 2 = quadratic... | ++------------------------+---------------+-----------------------------------------------+ +| **Random Seed | Strings | Random seed number | ++------------------------+---------------+-----------------------------------------------+ +| **Random Force** | Strings | Random force magnitude | ++------------------------+---------------+-----------------------------------------------+ +| **Random Variation** | Strings | Random force variation | ++------------------------+---------------+-----------------------------------------------+ +| **Density** | Strings | Particles Density | ++------------------------+---------------+-----------------------------------------------+ +| **Gravity** | Vectors | Constant forces that are mass independent | ++------------------------+---------------+-----------------------------------------------+ +| **Wind** | Vertices | constant forces that are mass dependent | ++------------------------+---------------+-----------------------------------------------+ +| **Bounding Box** | Vertices | Limits of the system. It will work with the | +| | | bounding box of the given vectors (min. two) | ++------------------------+---------------+-----------------------------------------------+ +| **Obstacles** | Vertices | Obstacles vertices | ++------------------------+---------------+-----------------------------------------------+ +| **Obstacles Pols** | Strings | Pols referenced to obstacles vertices. | +| | |(they must be triangles) | ++------------------------+---------------+-----------------------------------------------+ +| **Obstacles Bounce** | Strings | Obstacles Bounce force | ++------------------------+---------------+-----------------------------------------------+ + +Accumulative: +------------- + +When activated every nodeTree update will use the previous update as the starting point. The update can be triggered by the Upate button or by any other event that triggers regular updates (like playing animation or changing any value) +It offers some options: +**Reset**: Takes back the system to the initial state. +**Update**: Runs one nodetree update +**Pause**: Pauses nodes calculations and ignores ui changes + + Examples -------- Notes ------- - +The node uses one text-block to save the current state when you hit pause or freeze in order to be mantained in case of closing the program. diff --git a/nodes/modifier_change/pulga_physics.py b/nodes/modifier_change/pulga_physics.py index b53c0c6e9..ba5a49f7f 100644 --- a/nodes/modifier_change/pulga_physics.py +++ b/nodes/modifier_change/pulga_physics.py @@ -55,13 +55,12 @@ class SvPulgaPhysicsNode(bpy.types.Node, SverchCustomTreeNode): ''' bl_idname = 'SvPulgaPhysicsNode' bl_label = 'Pulga Physics' - bl_icon = 'CURVE_NCURVE' + bl_icon = 'MOD_PHYSICS' iterations = IntProperty( name='Iterations', description='Number of Iterations', default=1, min=1, update=updateNode) - fixed_len = FloatProperty( name='Springs Length', description='Specify spring rest length, 0 to calculate it from initial position', @@ -77,10 +76,10 @@ class SvPulgaPhysicsNode(bpy.types.Node, SverchCustomTreeNode): self_collision = FloatProperty( name='Self Collision', description='Collision forces between vertices', - default=0.0, precision=4, update=updateNode) + default=0.0, precision=4, step=1e-2, update=updateNode) self_attract = FloatProperty( name='Self Attract', description='Attraction between vertices', - default=0.0, precision=4, update=updateNode) + default=0.0, precision=4, step=1e-2, update=updateNode) attract_decay = FloatProperty( name='Self Attract Decay', description='0 = no decay, 1 = linear, 2 = quadratic...', default=0.0, precision=3, update=updateNode) @@ -187,15 +186,14 @@ class SvPulgaPhysicsNode(bpy.types.Node, SverchCustomTreeNode): vel.append(data[i][2] if np else data[i][2].tolist()) else: data = check_past(n_id) - if len(data) > 0: - data = past + # if len(data) > 0: + # data, past, from_file = self.get_global_cache() + # data = past for i in range(len(data)): verts.append(array(data[i][0]) if np else data[i][0]) rads.append(array(data[i][1]) if np else data[i][1]) vel.append(array(data[i][2]) if np else data[i][2]) - - - + return verts, rads, vel n_id = StringProperty() @@ -245,25 +243,25 @@ class SvPulgaPhysicsNode(bpy.types.Node, SverchCustomTreeNode): prop_dict = { - "Initial_Pos" : (0, '' , 'v', 0), - "Iterations" : (1, 'iterations', 's', 0), - "springs" : (2, '' , 's', 4), - "fixed_len" : (4, 'fixed_len' , 's', 4), - "spring_k" : (5, 'spring_k' , 's', 4), - "pins" : (6, '' , 's', 5), - "pins_goal_pos" : (7, '' , 'v', 5), - "rads_in" : (8, 'rads_in' , 's', 0), - "self_collision": (9, 'self_collision', 's', 1), - "self_attract" : (10, 'self_attract' , 's', 2), - "attract_decay" : (11, 'attract_decay', 's', 2), - "grow" : (12, 'grow' , 's', 3), - "min_rad" : (13, 'min_rad' , 's', 3), - "max_rad" : (14, 'max_rad' , 's', 3), - "Pols" : (15, '' , 's', 7), - "inflate" : (16, 'inflate' , 's', 7), - "Initial Velocity" : (17, 'initial_vel', 'v', 0), - "max_vel" : (18, 'max_vel' , 's', 0), - "drag_force" : (19, 'drag_force' , 's', 6), + "Initial_Pos" : (0, '' , 'v', 0), + "Iterations" : (1, 'iterations', 's', 0), + "Springs" : (2, '' , 's', 4), + "fixed_len" : (4, 'fixed_len' , 's', 4), + "spring_k" : (5, 'spring_k' , 's', 4), + "Pins" : (6, '' , 's', 5), + "Pins Goal Position" : (7, '' , 'v', 5), + "rads_in" : (8, 'rads_in' , 's', 0), + "self_collision" : (9, 'self_collision' , 's', 1), + "self_attract" : (10, 'self_attract' , 's', 2), + "attract_decay" : (11, 'attract_decay' , 's', 2), + "grow" : (12, 'grow' , 's', 3), + "min_rad" : (13, 'min_rad' , 's', 3), + "max_rad" : (14, 'max_rad' , 's', 3), + "Pols" : (15, '' , 's', 7), + "inflate" : (16, 'inflate' , 's', 7), + "Initial Velocity": (17, 'initial_vel' , 'v', 0), + "max_vel" : (18, 'max_vel' , 's', 0), + "drag_force" : (19, 'drag_force' , 's', 6), "Attractors" : (20, '' , 'v', 8), "force" : (21, 'force' , 's', 8), "att_clamp" : (22, 'att_clamp' , 's', 8), @@ -281,27 +279,11 @@ class SvPulgaPhysicsNode(bpy.types.Node, SverchCustomTreeNode): } sorted_props = sorted(prop_dict.items(), key=lambda kv: kv[1]) - prop_ui_groups2 = [[] for i in range(13)] - for input_prop in sorted_props: - prop_ui_groups2[input_prop[1][3]].append(input_prop[0]) - - - - prop_ui_groups = [ - ["self_collision"], - [ "self_attract", "attract_decay"], - [ "grow", "min_rad", "max_rad"], - ["springs", "fixed_len", "spring_k"], - ["pins", "pins_goal_pos"], - ["drag_force"], - ["Pols", "inflate"], - ["Attractors", "force", "att_clamp", "att_decay_power"], - ["random_seed", "random_force", "random_variation"], - ["Obstacles", "Obstacles_pols", "obstacles_bounce"], - ["Gravity", "Wind"], - ["b_box"] - ] + prop_ui_groups = [[] for i in range(13)] + for input_prop in sorted_props: + prop_ui_groups[input_prop[1][3]].append(input_prop[0]) + def update_sockets(self, context): ''' show and hide gated inputs''' prop_triggers = [ @@ -323,11 +305,11 @@ class SvPulgaPhysicsNode(bpy.types.Node, SverchCustomTreeNode): for i in range(len(prop_triggers)): if prop_triggers[i]: - if si[self.prop_ui_groups2[i+1][0]].hide_safe: - for p in self.prop_ui_groups2[i+1]: + if si[self.prop_ui_groups[i+1][0]].hide_safe: + for p in self.prop_ui_groups[i+1]: si[p].hide_safe = False else: - for p in self.prop_ui_groups2[i+1]: + for p in self.prop_ui_groups[i+1]: si[p].hide_safe = True if self.fit_M: if so["Rads"].hide_safe: @@ -337,11 +319,11 @@ class SvPulgaPhysicsNode(bpy.types.Node, SverchCustomTreeNode): updateNode(self, context) - self_react_M = BoolProperty(name="Self Collision", + self_react_M = BoolProperty(name="Collision", description="Self Collision: collision between input vertices as spheres", default=False, update=update_sockets) - self_attract_M = BoolProperty(name="Self Attraction", + self_attract_M = BoolProperty(name="Attraction", description="Self Attraction: attract between input vertices as spheres", default=False, update=update_sockets) @@ -373,7 +355,7 @@ class SvPulgaPhysicsNode(bpy.types.Node, SverchCustomTreeNode): description="Random force", default=False, update=update_sockets) - bounding_box_M = BoolProperty(name="Bounding Box", + bounding_box_M = BoolProperty(name="Boundaries", description="System bounding box", default=False, update=update_sockets) @@ -417,9 +399,6 @@ class SvPulgaPhysicsNode(bpy.types.Node, SverchCustomTreeNode): r0.prop(self, "self_react_M", toggle=True) r0.prop(self, "self_attract_M", toggle=True) r0.prop(self, "fit_M", toggle=True) - # r0.prop(self, "springs_M", toggle=True) - # r0.prop(self, "pins_M", toggle=True) - # r0.prop(self, "inflate_M", toggle=True) r = c1.row(align=True) r.prop(self, "springs_M", toggle=True) @@ -431,9 +410,6 @@ class SvPulgaPhysicsNode(bpy.types.Node, SverchCustomTreeNode): r2.prop(self, "drag_M", toggle=True) r2.prop(self, "attract_M", toggle=True) r2.prop(self, "random_M", toggle=True) - # r2.prop(self, "world_F_M", toggle=True) - # r2.prop(self, "obstacles_M", toggle=True) - # r2.prop(self, "bounding_box_M", toggle=True) r3 = c1.row(align=True) r3.prop(self, "world_F_M", toggle=True) @@ -442,9 +418,10 @@ class SvPulgaPhysicsNode(bpy.types.Node, SverchCustomTreeNode): r4 = layout.column(align=True) r4.prop(self, "accumulative", toggle=True) + if self.accumulative: cr = r4.row(align=True) - cr.prop(self, "accumulative_pause", toggle=True) + # cr.prop(self, "accumulative_pause", toggle=True) cr.prop(self, "accumulative_reset", toggle=True) upd = cr.operator("node.sverchok_update_current", text="Update") ng_name = context.space_data.node_tree.name @@ -477,8 +454,8 @@ class SvPulgaPhysicsNode(bpy.types.Node, SverchCustomTreeNode): gates_dict = {} gates_dict["accumulate"] = self.accumulative gates_dict["self_react"] = [self.self_react_M, self.self_attract_M, self.fit_M] - gates_dict["springs"] = [si["springs"].is_linked, si["fixed_len"].is_linked] - gates_dict["pins"] = [si["pins"].is_linked, si["pins_goal_pos"].is_linked] + gates_dict["Springs"] = [si["Springs"].is_linked, si["fixed_len"].is_linked] + gates_dict["Pins"] = [si["Pins"].is_linked, si["Pins Goal Position"].is_linked] gates_dict["drag"] = [self.drag_M, self.fit_M] gates_dict["inflate"] = si["Pols"].is_linked gates_dict["random"] = self.random_M @@ -526,7 +503,7 @@ class SvPulgaPhysicsNode(bpy.types.Node, SverchCustomTreeNode): def process(self): '''main node function called every update''' - tiempo = time.time() + si = self.inputs so = self.outputs if not any(socket.is_linked for socket in so): @@ -565,8 +542,7 @@ class SvPulgaPhysicsNode(bpy.types.Node, SverchCustomTreeNode): so['Rads'].sv_set(rads_out) if so['Velocity'].is_linked: so['Velocity'].sv_set(velocity_out) - - print(time.time() - tiempo ) + diff --git a/utils/pulga_physics_core.py b/utils/pulga_physics_core.py index b1cbac56e..9ca39da96 100644 --- a/utils/pulga_physics_core.py +++ b/utils/pulga_physics_core.py @@ -1,3 +1,10 @@ +# This file is part of project Sverchok. It's copyrighted by the contributors +# recorded in the version control history of the file, available from +# its original location https://github.com/nortikin/sverchok/commit/master +# +# SPDX-License-Identifier: GPL3 +# License-Filename: LICENSE + import numpy as np def cross_indices3(n): @@ -88,8 +95,7 @@ def self_react(params): if some_collisions: self_collision_force(result, dist, sum_rad, index_inter, mask, normal_v, collision) if some_attractions: - #antimask = np.invert(mask) - antimask = True + antimask = np.invert(mask) attract_force(result, dist_cor, antimask, indexes, normal_v, att_params) ps.r += np.sum(result, axis=1) @@ -124,8 +130,8 @@ def attract_force(result, dist, mask, index, norm_v, att_params): id0 = index_non_inter[:, 0] id1 = index_non_inter[:, 1] - direction = norm_v[mask] / dist2 * mass_product[mask, np.newaxis] + variable_att = len(attract) > 1 att = attract @@ -623,7 +629,7 @@ def limit_speed(np_vel, max_vel): vel_exceded = vel_mag > max_vel np_vel[vel_exceded] = np_vel[vel_exceded] / vel_mag[vel_exceded, np.newaxis] * max_vel -FORCE_CHAIN = ["self_react", "springs", "drag", "inflate", "random", "attractors", "world_f", "pins", "apply_f", "b_box", "Obstacles"] +FORCE_CHAIN = ["self_react", "Springs", "drag", "inflate", "random", "attractors", "world_f", "Pins", "apply_f", "b_box", "Obstacles"] class PulgaSystem(): '''Store states''' @@ -703,20 +709,20 @@ class PulgaSystem(): if not use_pins: return - self.params['pins'] = np.array(pins) + self.params['Pins'] = np.array(pins) - if self.params['pins'].dtype == np.int32: - if len(self.params['pins']) == len(self.verts): - self.params['pins'] = self.params['pins'] == 1 - self.params['unpinned'] = np.invert(self.params['pins']) + if self.params['Pins'].dtype == np.int32: + if len(self.params['Pins']) == len(self.verts): + self.params['Pins'] = self.params['Pins'] == 1 + self.params['unpinned'] = np.invert(self.params['Pins']) else: self.params['unpinned'] = np.ones(len(self.verts), dtype=np.bool) - self.params['unpinned'][self.params['pins']] = False + self.params['unpinned'][self.params['Pins']] = False - self.vel[self.params['pins'], :] = 0 + self.vel[self.params['Pins'], :] = 0 if use_pins_goal: - self.verts[self.params['pins'], :] = np.array(pins_goal_pos) + self.verts[self.params['Pins'], :] = np.array(pins_goal_pos) forces_composite[0].append(local_func) forces_composite[1].append(self) return @@ -724,18 +730,18 @@ class PulgaSystem(): def apply_pins(self): '''cancel forces on pins''' - self.r[self.params["pins"], :] = 0 + self.r[self.params["Pins"], :] = 0 FUNC_DICT = { "self_react": self_react, - "springs": spring_force, + "Springs": spring_force, "drag": drag_force_apply, "inflate": inflate_force, "random": random_force_apply, "attractors": attractors_force, "world_f": (world_forces_apply, world_forces_apply_var), - "pins": PulgaSystem.apply_pins, + "Pins": PulgaSystem.apply_pins, "apply_f": PulgaSystem.apply_forces, "b_box": b_box_apply, "Obstacles": collisions_apply @@ -743,13 +749,13 @@ FUNC_DICT = { } INIT_FUNC_DICT = { "self_react": self_react_setup, - "springs": spring_setup, + "Springs": spring_setup, "drag": drag_setup, "inflate": inflate_setup, "random": random_setup, "attractors": attractors_setup, "world_f": world_forces_setup, - "pins": pins_setup, + "Pins": pins_setup, "apply_f": apply_forces_setup, "b_box": b_box_setup, "Obstacles": collisions_setup @@ -757,26 +763,10 @@ INIT_FUNC_DICT = { } -def fill_gates_dict(gates_dict, gates): - '''redistribute booleans''' - gates_dict["accumulate"] = gates[0] - gates_dict["self_react"] = gates[1:4] - gates_dict["springs"] = gates[4:6] - gates_dict["pins"] = gates[6:8] - gates_dict["drag"] = [gates[8]] + [gates[3]] - gates_dict["inflate"] = gates[9] - gates_dict["random"] = gates[10] - gates_dict["attractors"] = gates[11] - gates_dict["world_f"] = gates[12] - gates_dict["Obstacles"] = gates[13] - gates_dict["b_box"] = gates[14] - gates_dict["output"] = gates[15:] - gates_dict["apply_f"] = True - PARAMS_GROUPS = { "main": ("Initial_Pos", "rads_in", 'Initial Velocity', "max_vel", "Density"), - "springs": ("springs", "fixed_len", "spring_k" ), - "pins": ("pins", "pins_goal_pos"), + "Springs": ("Springs", "fixed_len", "spring_k" ), + "Pins": ("Pins", "Pins Goal Position"), "self_react": ("self_collision", "self_attract", "attract_decay", "grow", "min_rad", "max_rad"), "inflate": ("Pols", "inflate"), "drag": ("drag_force"), @@ -824,7 +814,7 @@ def pulga_system_init(params, parameters, gates, out_lists, cache): if dictionaries[1]["accumulate"]: if len(cache) > 0: # ps.hard_update(cache) - ps.hard_update_list(cache, gates["self_react"][2], gates["pins"]) + ps.hard_update_list(cache, gates["self_react"][2], gates["Pins"]) iterate(iterations_max, force_map, force_parameters, out_params) -- GitLab From 854f00cbcbaa223a3d4ea7466af9f756b7f259d1 Mon Sep 17 00:00:00 2001 From: Victor Doval <10011941+vicdoval@users.noreply.github.com> Date: Sat, 13 Apr 2019 21:32:58 +0200 Subject: [PATCH 082/137] reactions on pins added code cleaning and docs updating --- docs/nodes/alpha_nodes/pulga_physics.rst | 75 +++++-- nodes/modifier_change/pulga_physics.py | 249 +++++++++++------------ utils/pulga_physics_core.py | 75 +++---- 3 files changed, 220 insertions(+), 179 deletions(-) diff --git a/docs/nodes/alpha_nodes/pulga_physics.rst b/docs/nodes/alpha_nodes/pulga_physics.rst index c7f2260a5..49014ca5d 100644 --- a/docs/nodes/alpha_nodes/pulga_physics.rst +++ b/docs/nodes/alpha_nodes/pulga_physics.rst @@ -6,8 +6,9 @@ Functionality This node creates simulations from input parameters, it is meant to be used in form-finding purposes. It creates the simulation using the inputed vertices as spheric particles that react to applied forces. -Tne node is a basic NumPy implementation of basic physics system heavily inspired in "The Nature of Code" by Daniel Shiffman -and the Kangoroo Plugin for Grasshopper. Due the nature of the algorithm it can get very intensive, handle with responsibility + +The node is a basic NumPy implementation of basic physics system heavily inspired in "The Nature of Code" by Daniel Shiffman +and the Kangoroo Plug-in for Grasshopper. Due the nature of the algorithm it can get very intensive, handle with responsibility Input & Output -------------- @@ -30,12 +31,11 @@ Input & Output | **Pins** | Strings | Indexes of vertices with fixed position, | | | | accepts a vertices mask and index number | +------------------------+---------------+-----------------------------------------------+ -| **pins_goal_pos | Vertices | Final position of pinned vertices | +| **Pins Goal Position** | Vertices | Final position of pinned vertices | +------------------------+---------------+-----------------------------------------------+ | **Radius** | Strings | Radius of virtual sphere, used to | | | | calculate intersections, mass and surface | +------------------------+---------------+-----------------------------------------------+ - | **Self Collision** | Strings | Collision force. | | | | Applied when vertex distance < sum Radius | +------------------------+---------------+-----------------------------------------------+ @@ -52,8 +52,8 @@ Input & Output +------------------------+---------------+-----------------------------------------------+ | **Max Radius** | Strings | Maximum radius limit | +------------------------+---------------+-----------------------------------------------+ -| **Pols** | Strings | Pols referenced to vertices | -| | |(used to calculate inflate force) | +| **Pols** | Strings | Pols referenced to vertices. | +| | | (used to calculate inflate force) | +------------------------+---------------+-----------------------------------------------+ | **Inflate** | Strings | Inflate force magnitude (per surface unit) | +------------------------+---------------+-----------------------------------------------+ @@ -72,7 +72,7 @@ Input & Output | **Attractors Decay** | Strings | Decay with distance | | | | 0 = no decay, 1 = linear, 2 = quadratic... | +------------------------+---------------+-----------------------------------------------+ -| **Random Seed | Strings | Random seed number | +| **Random Seed** | Strings | Random seed number | +------------------------+---------------+-----------------------------------------------+ | **Random Force** | Strings | Random force magnitude | +------------------------+---------------+-----------------------------------------------+ @@ -84,13 +84,13 @@ Input & Output +------------------------+---------------+-----------------------------------------------+ | **Wind** | Vertices | constant forces that are mass dependent | +------------------------+---------------+-----------------------------------------------+ -| **Bounding Box** | Vertices | Limits of the system. It will work with the | +| **Bounding Box** | Vertices | Limits of the system. It will work with the | | | | bounding box of the given vectors (min. two) | +------------------------+---------------+-----------------------------------------------+ | **Obstacles** | Vertices | Obstacles vertices | +------------------------+---------------+-----------------------------------------------+ | **Obstacles Pols** | Strings | Pols referenced to obstacles vertices. | -| | |(they must be triangles) | +| | | (they must be triangles) | +------------------------+---------------+-----------------------------------------------+ | **Obstacles Bounce** | Strings | Obstacles Bounce force | +------------------------+---------------+-----------------------------------------------+ @@ -98,17 +98,66 @@ Input & Output Accumulative: ------------- -When activated every nodeTree update will use the previous update as the starting point. The update can be triggered by the Upate button or by any other event that triggers regular updates (like playing animation or changing any value) +When activated every nodeTree update will use the previous update as the starting point. The update can be triggered by the Update button or by any other event that triggers regular updates (like playing animation or changing any value). + It offers some options: + **Reset**: Takes back the system to the initial state. -**Update**: Runs one nodetree update -**Pause**: Pauses nodes calculations and ignores ui changes + +**Update**: Runs one Node-Tree update. + +**Pause**: Pauses nodes calculations and ignores ui changes. Examples -------- +Arranging circles with attraction and collision. + +.. image:: https://user-images.githubusercontent.com/10011941/55254066-3d902500-5257-11e9-9a28-46d3deffcf0b.png + :alt: circle_fitting_pulga_physics_procedural_design.png + +Tensile structures can be studied with collisions and pinned points. + +.. image:: https://user-images.githubusercontent.com/10011941/55254067-3e28bb80-5257-11e9-8988-7e19e8a2462b.png + :alt: textile_structures_pulga_physics_procedural_design.png + +.. image:: https://user-images.githubusercontent.com/10011941/56082937-23da0a80-5e1f-11e9-9b50-611629574cef.png + :alt: textile_cover_pulga_physics_procedural_design.png + + +Traction structures can be converted to compression structures with inverted gravity. + +.. image:: https://user-images.githubusercontent.com/10011941/55254068-3e28bb80-5257-11e9-86b3-2243b4e7ac4e.png + :alt: compression_structures_pulga_physics_procedural_design.png + +Using the caternary as a structural modeling tool: + +.. image:: https://user-images.githubusercontent.com/10011941/56082943-305e6300-5e1f-11e9-811b-c20df2a7a4d2.png + :alt: catenary_cover_pulga_physics_procedural_design.png + +Variable spring stiffness can be used to simulate sewing springs inflatable structures. + +.. image:: https://user-images.githubusercontent.com/10011941/55256836-69fb6f80-525e-11e9-9a1b-21a6eafd0a4e.png + :alt: inflateble_structures_pulga_physics_procedural_design.png + +Trajectories can be traced by supplying the desired iterations as a list. + +.. image:: https://user-images.githubusercontent.com/10011941/55313009-14de7a00-5467-11e9-887e-781d7b4dc025.png + :alt: physics_modeling_pulga_physics_procedural_design.png + +Shooting particles to a attractors field. + +.. image:: https://user-images.githubusercontent.com/10011941/56082940-2b011880-5e1f-11e9-8124-90da02ab7cf5.png + :alt: shooting partcles_pulga_physics_procedural_design.PNG + +The "Pins Reactions" output supply the resultant force on the pins. It can be use to model auxiliary structures. + +.. image:: https://user-images.githubusercontent.com/10011941/56082950-479d5080-5e1f-11e9-87ed-19b9247c07b5.png + :alt: pin_reactions_pulga_physics_procedural_design.png + Notes ------- -The node uses one text-block to save the current state when you hit pause or freeze in order to be mantained in case of closing the program. + +When using accumulative mode the node uses one text-block (called pulga_memory + NodeTree name + Node name .txt) to save the current state when you hit pause in order to be maintained in case of closing the program. diff --git a/nodes/modifier_change/pulga_physics.py b/nodes/modifier_change/pulga_physics.py index ba5a49f7f..065bd46ea 100644 --- a/nodes/modifier_change/pulga_physics.py +++ b/nodes/modifier_change/pulga_physics.py @@ -16,38 +16,35 @@ # # ##### END GPL LICENSE BLOCK ##### - - +import ast +from numpy import array import bpy from bpy.props import IntProperty, StringProperty, BoolProperty, FloatProperty, FloatVectorProperty from sverchok.node_tree import SverchCustomTreeNode from sverchok.data_structure import updateNode, node_id, match_long_repeat from sverchok.utils.pulga_physics_core import pulga_system_init -from numpy import array -import time -from copy import deepcopy -import ast + FILE_NAME = 'pulga_Memory ' -def check_past(n_id): +def check_past_file(location): '''read text-block and parse values''' - name = FILE_NAME + n_id + ".txt" + name = FILE_NAME + location + ".txt" text = bpy.data.texts.get(name) or bpy.data.texts.new(name) tx = text.as_string() - if len(tx)>1: + if len(tx) > 1: return ast.literal_eval(text.as_string()) else: return [] - - -def fill_past(p, n_id): + + +def fill_past_file(p, location): '''write values to text-block''' - name = FILE_NAME + n_id + ".txt" + name = FILE_NAME + location + ".txt" text = bpy.data.texts.get(name) or bpy.data.texts.new(name) text.clear() text.write(''.join(str(p))) - + class SvPulgaPhysicsNode(bpy.types.Node, SverchCustomTreeNode): ''' Triggers: Springs, Cloth @@ -56,7 +53,7 @@ class SvPulgaPhysicsNode(bpy.types.Node, SverchCustomTreeNode): bl_idname = 'SvPulgaPhysicsNode' bl_label = 'Pulga Physics' bl_icon = 'MOD_PHYSICS' - + iterations = IntProperty( name='Iterations', description='Number of Iterations', @@ -73,7 +70,7 @@ class SvPulgaPhysicsNode(bpy.types.Node, SverchCustomTreeNode): rads_in = FloatProperty( name='Radius', description='Used to calculate mass, surface and collisions', default=1.0, update=updateNode) - + self_collision = FloatProperty( name='Self Collision', description='Collision forces between vertices', default=0.0, precision=4, step=1e-2, update=updateNode) @@ -92,23 +89,23 @@ class SvPulgaPhysicsNode(bpy.types.Node, SverchCustomTreeNode): max_rad = FloatProperty( name='Max Radius', description='Do not grow over this value', default=1.0, precision=3, update=updateNode) - - inflate = FloatProperty( + + inflate = FloatProperty( name='Inflate', description='push geometry along the normals proportional to polygon area', default=1.0, precision=3, update=updateNode) - + initial_vel = FloatVectorProperty( name='Initial Velocity', description='Initial Velocity', size=3, default=(0., 0., 0.), precision=3, update=updateNode) - + max_vel = FloatProperty( name='Max Velocity', description='Limit maximun speed. 0 = no limit', default=0.01, precision=3, update=updateNode) drag_force = FloatProperty( name='Drag Force', description='Movement resistance from environment', default=0.0, precision=3, update=updateNode) - force = FloatProperty( + att_force = FloatProperty( name='Attractors Force', description='Attractors Force magnitude', default=0.0, precision=3, update=updateNode) att_clamp = FloatProperty( @@ -140,108 +137,100 @@ class SvPulgaPhysicsNode(bpy.types.Node, SverchCustomTreeNode): name='Wind', description='wind or other constant forces that are mass dependent', size=3, default=(0., 0., 0.), precision=4, update=updateNode) - - obstacles_bounce = FloatProperty( + + obstacles_bounce = FloatProperty( name='Obstacles Bounce', description='Obstacles Bounce', - default=0.1, update=updateNode) + default=0.1, update=updateNode) def handle_accumulative(self, context): '''start cache''' - n_id = node_id(self) - data = self.node_dict.get(n_id) + + data = self.node_cache.get(0) if not self.accumulative: - self.accumulative_loaded = False - for i in range(len(data)): - self.accumulativity_set_data([], i) + self.accumulativity_set_data([], i) self.accumulative_parse = False - + if not data: - self.node_dict[n_id] = {} - data = self.node_dict.get(n_id) + self.node_cache[0] = {} updateNode(self, context) def memory_to_file(self, context): '''bump memory to text-block''' - if self.accumulative_pause or self.accumulative_parse: - n_id = node_id(self) - data = self.node_dict.get(n_id) + if self.accumulative_parse: + location = self.getNodeTree().name + "_"+ self.name + data = self.node_cache.get(0) out = [] for i in range(len(data)): - out.append([data[i][0].tolist(), data[i][1].tolist(), data[i][2].tolist()]) - fill_past(out, n_id) - + out.append([data[i][0].tolist(), data[i][1].tolist(), data[i][2].tolist(), data[i][3].tolist()]) + check_past_file(out, location) + def memory_to_lists(self): '''bump memory to output''' verts = [] rads = [] vel = [] + react = [] np = self.output_numpy - n_id = node_id(self) - data = self.node_dict.get(n_id) + data = self.node_cache.get(0) if type(data) == dict: for i in range(len(data)): verts.append(data[i][0] if np else data[i][0].tolist()) rads.append(data[i][1] if np else data[i][1].tolist()) vel.append(data[i][2] if np else data[i][2].tolist()) + react.append(data[i][3] if np else data[i][3].tolist()) else: - data = check_past(n_id) - # if len(data) > 0: - # data, past, from_file = self.get_global_cache() - # data = past + location = self.getNodeTree().name + "_"+ self.name + data = check_past_file(location) for i in range(len(data)): verts.append(array(data[i][0]) if np else data[i][0]) rads.append(array(data[i][1]) if np else data[i][1]) - vel.append(array(data[i][2]) if np else data[i][2]) - - return verts, rads, vel - - n_id = StringProperty() - node_dict = {} - - accumulative = BoolProperty(name="Accumulative", + vel.append(array(data[i][2]) if np else data[i][2]) + react.append(array(data[i][3]) if np else data[i][3]) + + return verts, rads, vel, react + + def reset_memory(self, context): + if self.accumulative_reset: + if not self.accumulative_parse: + self.node_cache[0] = {} + self.accumulative_reset = False + updateNode(self, context) + + node_cache = {} + + accumulative = BoolProperty( + name="Accumulative", description="Accumulate changes every NodeTree update", default=False, update=handle_accumulative) - accumulative_loaded = BoolProperty(name="accumulative is ready", - description="there is a cache", - default=False - ) - accumulative_pause = BoolProperty(name="Freeze", - description="Freeze accumulative changes on last update", - default=False, - update=memory_to_file - ) - accumulative_reset = BoolProperty(name="Reset", + + accumulative_reset = BoolProperty( + name="Reset", description="Restart accumulative memory", default=False, - update=updateNode) - accumulative_save = BoolProperty(name="Save", - description="Save accumulative memory", - default=False, - update=memory_to_file - ) - accumulative_parse = BoolProperty(name="Pause", - description="Save to file accumulative memory", + update=reset_memory) + + accumulative_parse = BoolProperty( + name="Pause", + description="Pause processing", default=False, update=memory_to_file ) - + def accumulativity_get_data(self): '''get data form previous update''' - n_id = node_id(self) - data = self.node_dict.get(n_id) + data = self.node_cache.get(0) return data - + def accumulativity_set_data(self, cache, cache_id): '''store data form this update''' - n_id = node_id(self) - data = self.node_dict.get(n_id) + data = self.node_cache.get(0) data[cache_id] = cache - + return data - - + + prop_dict = { "Initial_Pos" : (0, '' , 'v', 0), "Iterations" : (1, 'iterations', 's', 0), @@ -263,7 +252,7 @@ class SvPulgaPhysicsNode(bpy.types.Node, SverchCustomTreeNode): "max_vel" : (18, 'max_vel' , 's', 0), "drag_force" : (19, 'drag_force' , 's', 6), "Attractors" : (20, '' , 'v', 8), - "force" : (21, 'force' , 's', 8), + "att_force" : (21, 'att_force' , 's', 8), "att_clamp" : (22, 'att_clamp' , 's', 8), "att_decay_power" : (23, 'att_decay_power' , 's', 8), "random_seed" : (24, 'random_seed' , 's', 9), @@ -279,11 +268,11 @@ class SvPulgaPhysicsNode(bpy.types.Node, SverchCustomTreeNode): } sorted_props = sorted(prop_dict.items(), key=lambda kv: kv[1]) - + prop_ui_groups = [[] for i in range(13)] for input_prop in sorted_props: - prop_ui_groups[input_prop[1][3]].append(input_prop[0]) - + prop_ui_groups[input_prop[1][3]].append(input_prop[0]) + def update_sockets(self, context): ''' show and hide gated inputs''' prop_triggers = [ @@ -302,7 +291,7 @@ class SvPulgaPhysicsNode(bpy.types.Node, SverchCustomTreeNode): ] si = self.inputs so = self.outputs - + for i in range(len(prop_triggers)): if prop_triggers[i]: if si[self.prop_ui_groups[i+1][0]].hide_safe: @@ -310,13 +299,18 @@ class SvPulgaPhysicsNode(bpy.types.Node, SverchCustomTreeNode): si[p].hide_safe = False else: for p in self.prop_ui_groups[i+1]: - si[p].hide_safe = True + si[p].hide_safe = True if self.fit_M: if so["Rads"].hide_safe: so["Rads"].hide_safe = False else: so["Rads"].hide_safe = True - + if self.pins_M: + if so["Pins Reactions"].hide_safe: + so["Pins Reactions"].hide_safe = False + else: + so["Pins Reactions"].hide_safe = True + updateNode(self, context) self_react_M = BoolProperty(name="Collision", @@ -372,23 +366,24 @@ class SvPulgaPhysicsNode(bpy.types.Node, SverchCustomTreeNode): description="Output NumPy arrays ", default=False, update=updateNode) - + def sv_init(self, context): - + '''create sockets''' self.width = 200 si = self.inputs.new so = self.outputs.new vs, ss = 'VerticesSocket', 'StringsSocket' - + for input_prop in self.sorted_props: input_type = vs if input_prop[1][2] == 'v' else ss si(input_type, input_prop[0]).prop_name = input_prop[1][1] - - + + so(vs, "Vertices") - so(ss, "Rads") + so(ss, "Rads") so(vs, 'Velocity') + so(vs, 'Pins Reactions') self.update_sockets(context) @@ -405,12 +400,12 @@ class SvPulgaPhysicsNode(bpy.types.Node, SverchCustomTreeNode): r.prop(self, "pins_M", toggle=True) r.prop(self, "inflate_M", toggle=True) - + r2 = c1.row(align=True) r2.prop(self, "drag_M", toggle=True) r2.prop(self, "attract_M", toggle=True) r2.prop(self, "random_M", toggle=True) - + r3 = c1.row(align=True) r3.prop(self, "world_F_M", toggle=True) r3.prop(self, "obstacles_M", toggle=True) @@ -418,7 +413,7 @@ class SvPulgaPhysicsNode(bpy.types.Node, SverchCustomTreeNode): r4 = layout.column(align=True) r4.prop(self, "accumulative", toggle=True) - + if self.accumulative: cr = r4.row(align=True) # cr.prop(self, "accumulative_pause", toggle=True) @@ -432,7 +427,7 @@ class SvPulgaPhysicsNode(bpy.types.Node, SverchCustomTreeNode): '''draw buttons on the N-panel''' self.draw_buttons(context, layout) layout.prop(self, "output_numpy", toggle=False) - + def get_data(self): '''get all data from sockets''' @@ -440,11 +435,11 @@ class SvPulgaPhysicsNode(bpy.types.Node, SverchCustomTreeNode): parameters = [] for socket in si: if len(socket.prop_name)>0: - + parameters.append(socket.sv_get()) else: parameters.append(socket.sv_get(default=[[]])) - + return match_long_repeat(parameters) @@ -458,31 +453,25 @@ class SvPulgaPhysicsNode(bpy.types.Node, SverchCustomTreeNode): gates_dict["Pins"] = [si["Pins"].is_linked, si["Pins Goal Position"].is_linked] gates_dict["drag"] = [self.drag_M, self.fit_M] gates_dict["inflate"] = si["Pols"].is_linked - gates_dict["random"] = self.random_M + gates_dict["random"] = self.random_M gates_dict["attractors"] = si["Attractors"].is_linked gates_dict["world_f"] = [self.world_F_M, self.fit_M] gates_dict["Obstacles"] = si["Obstacles"].is_linked and si["Obstacles_pols"].is_linked gates_dict["b_box"] = si["Bounding Box"].is_linked gates_dict["output"] = self.output_numpy gates_dict["apply_f"] = True - + return gates_dict def get_local_cache(self, past, data, from_file, temp_id): '''parse individual cached geometry if there is any''' - cache = [] + cache = [] if self.accumulative: if from_file and len(past) > 0: cache = past[temp_id] if not from_file and len(data) > 0: cache = data.get(temp_id, []) - if self.accumulative_pause: - cache2 = deepcopy(cache) - self.accumulativity_set_data(cache2, temp_id) - if self.accumulative_reset: - cache = [] - self.accumulative_reset = False - + return cache def get_global_cache(self): @@ -491,14 +480,14 @@ class SvPulgaPhysicsNode(bpy.types.Node, SverchCustomTreeNode): past = [] data = [] if self.accumulative: - data = self.accumulativity_get_data() + #data = self.accumulativity_get_data() + data = self.node_cache.get(0) if type(data) != dict: from_file = True - n_id = node_id(self) - self.node_dict[n_id] = {} - data = self.node_dict.get(n_id) - past = check_past(n_id) - + self.node_cache[0] = {} + location = self.getNodeTree().name + "_"+ self.name + past = check_past_file(location) + return data, past, from_file def process(self): @@ -508,42 +497,44 @@ class SvPulgaPhysicsNode(bpy.types.Node, SverchCustomTreeNode): so = self.outputs if not any(socket.is_linked for socket in so): return - + if not si['Initial_Pos'].is_linked: return verts_out = [] rads_out = [] velocity_out = [] - + reactions_out = [] + if self.accumulative and self.accumulative_parse: - verts_out, rads_out, velocity_out = self.memory_to_lists() - else: - out_lists = [verts_out, rads_out, velocity_out] - params = self.get_data() + verts_out, rads_out, velocity_out, reactions_out = self.memory_to_lists() + else: + out_lists = [verts_out, rads_out, velocity_out, reactions_out] + params = self.get_data() gates_dict = self.fill_gates_dict() data, past, from_file = self.get_global_cache() - temp_id = 0 + temp_id = 0 for par in zip(*params): cache = self.get_local_cache(past, data, from_file, temp_id) par_dict = {} for idx, p in enumerate(self.sorted_props): - par_dict[p[0]] = par[idx] - np_verts, np_rad, np_vel = pulga_system_init(par_dict, par, gates_dict, out_lists, cache) + par_dict[p[0]] = par[idx] + cache_new = pulga_system_init(par_dict, par, gates_dict, out_lists, cache) + + if self.accumulative: + self.accumulativity_set_data(cache_new, temp_id) - if self.accumulative and not self.accumulative_pause: - cache = [np_verts, np_rad, np_vel] - self.accumulativity_set_data(cache, temp_id) - temp_id += 1 if so['Vertices'].is_linked: - so['Vertices'].sv_set(verts_out) + so['Vertices'].sv_set(verts_out) if so['Rads'].is_linked: so['Rads'].sv_set(rads_out) if so['Velocity'].is_linked: so['Velocity'].sv_set(velocity_out) + if so['Pins Reactions'].is_linked: + so['Pins Reactions'].sv_set(reactions_out) + - def register(): diff --git a/utils/pulga_physics_core.py b/utils/pulga_physics_core.py index 9ca39da96..bdbb1e371 100644 --- a/utils/pulga_physics_core.py +++ b/utils/pulga_physics_core.py @@ -1,7 +1,7 @@ # This file is part of project Sverchok. It's copyrighted by the contributors # recorded in the version control history of the file, available from # its original location https://github.com/nortikin/sverchok/commit/master -# +# # SPDX-License-Identifier: GPL3 # License-Filename: LICENSE @@ -168,10 +168,10 @@ def self_react_setup(ps, dicti, forces_composite): use_grow = use_grow and np.any(np_grow != 0) use_self_react = (use_attract or use_self_collision or use_grow) - + if not use_self_react: return - + ps.params['indexes'] = cross_indices3(ps.v_len) sum_rad = ps.rads[ps.params['indexes'][:, 0]] + ps.rads[ps.params['indexes'][:, 1]] @@ -192,11 +192,11 @@ def att_setup(use_attract, ps, np_attract, attract_decay): mass_product = ps.mass[indexes[:, 0]] * ps.mass[indexes[:, 1]] att_params = [np_attract, np_att_decay, mass_product] else: - att_params = [] - + att_params = [] + return att_params - + def fit_setup(use_grow, np_grow, min_rad, max_rad): '''Prepare fitting data''' if use_grow: @@ -205,7 +205,7 @@ def fit_setup(use_grow, np_grow, min_rad, max_rad): fit_params = [np_grow, np_min_rad, np_max_rad] else: fit_params = [] - + return fit_params @@ -213,12 +213,11 @@ def attractors_setup(ps, dicti, forces_composite): '''prepare attractors system and data''' local_func, use_attractors, local_params = dicti if use_attractors: - attractors, force, att_clamp, att_decay_power = local_params + attractors, att_force, att_clamp, att_decay_power = local_params np_attrac = np.array(attractors) - np_attrac_f = np.array(force) + np_attrac_f = np.array(att_force) np_attrac_clamp = np.array(att_clamp) np_attrac_decay_pow = np.array(att_decay_power) - # use_att_clamp = np_attrac_clamp > 0 params = [np_attrac, np_attrac_f, np_attrac_clamp, np_attrac_decay_pow] forces_composite[0].append(local_func) @@ -387,6 +386,7 @@ def drag_force_apply(params): vel_mag = np.linalg.norm(ps.vel, axis=1) vel_mag_zero = vel_mag == 0 vel_mag[vel_mag_zero] = 1 + # squaring the speed is more accurate but harder to control # vel_mag2 = vel_mag * vel_mag vel_mag2 = vel_mag vel_norm = ps.vel/vel_mag[:, np.newaxis] @@ -473,7 +473,6 @@ def b_box_coll_filter(np_pol_v, verts, rad): keep3 = np.any(keep*keep2, axis=0) keep = np.any(keep*keep2, axis=1) verts_m = np.all(keep3, axis=1) - # print(keep2.shape, keep3.shape, verts_m.shape,verts_x.shape) pols_m = np.all(keep, axis=1) return pols_m, verts_m @@ -588,23 +587,23 @@ def world_forces_setup(ps, dicti, forces_composite): forces_composite[0].append(func) forces_composite[1].append(ps) - + def world_forces_apply(ps): '''apply constant forces''' ps.r += ps.params['gravity'] + ps.params['wind'] - + def world_forces_apply_var(ps): '''apply constant forces''' ps.r += ps.params['gravity'] * ps.mass[:, np.newaxis] + ps.params['wind'] - + def pins_setup(ps, dicti, forces_composite): '''prepare pins data''' ps.pins_init(dicti, forces_composite) - + def pins_apply(ps): '''apply pin mask''' @@ -621,7 +620,7 @@ def apply_forces_setup(ps, dicti, forces_composite): def apply_all_forces(ps): '''applying forces and reseting resultant''' ps.apply_forces() - + def limit_speed(np_vel, max_vel): ''''constrain speed magniture''' @@ -660,10 +659,11 @@ class PulgaSystem(): self.density = np.array(density) if len(density) > 1: self.density = numpy_fit_long_repeat([self.density], self.v_len)[0] + p["Pins Reactions"] = [] def hard_update(self, cache, size_change, pins_gates): '''replace verts, rads and vel (in NumPy)''' - verts, rads, vel = cache + verts, rads, vel, react = cache if len(verts) == self.v_len: if pins_gates[0] and pins_gates[1]: unpinned = self.params['unpinned'] @@ -676,7 +676,7 @@ class PulgaSystem(): def hard_update_list(self, cache, size_change, pins_gates): '''replace verts, rads and velocity''' - verts, rads, vel = cache + verts, rads, vel, react = cache if type(verts) == list: if len(verts) == self.v_len: if pins_gates[0] and pins_gates[1]: @@ -705,12 +705,12 @@ class PulgaSystem(): local_func, local_gates, local_params = dicti pins, pins_goal_pos = local_params use_pins, use_pins_goal = local_gates - + if not use_pins: - return - + return + self.params['Pins'] = np.array(pins) - + if self.params['Pins'].dtype == np.int32: if len(self.params['Pins']) == len(self.verts): self.params['Pins'] = self.params['Pins'] == 1 @@ -718,18 +718,19 @@ class PulgaSystem(): else: self.params['unpinned'] = np.ones(len(self.verts), dtype=np.bool) self.params['unpinned'][self.params['Pins']] = False - + self.vel[self.params['Pins'], :] = 0 - + if use_pins_goal: self.verts[self.params['Pins'], :] = np.array(pins_goal_pos) forces_composite[0].append(local_func) - forces_composite[1].append(self) - return - - + forces_composite[1].append(self) + return + + def apply_pins(self): '''cancel forces on pins''' + self.params["Pins Reactions"] = -self.r[self.params["Pins"]] self.r[self.params["Pins"], :] = 0 @@ -770,12 +771,12 @@ PARAMS_GROUPS = { "self_react": ("self_collision", "self_attract", "attract_decay", "grow", "min_rad", "max_rad"), "inflate": ("Pols", "inflate"), "drag": ("drag_force"), - "attractors": ("Attractors", "force", "att_clamp", "att_decay_power"), + "attractors": ("Attractors", "att_force", "att_clamp", "att_decay_power"), "random": ("random_seed", "random_force", "random_variation"), "world_f": ("Gravity", "Wind"), "b_box": ("Bounding Box"), "Obstacles": ("Obstacles", "Obstacles_pols", "obstacles_bounce"), - + } def fill_params_dict(p_dict, parameters, par): '''redistribute parameters''' @@ -784,7 +785,7 @@ def fill_params_dict(p_dict, parameters, par): p_dict[x] = [par[p] for p in PARAMS_GROUPS[x] ] else: p_dict[x] = par[PARAMS_GROUPS[x]] - + p_dict["apply_f"] = True @@ -813,12 +814,11 @@ def pulga_system_init(params, parameters, gates, out_lists, cache): if dictionaries[1]["accumulate"]: if len(cache) > 0: - # ps.hard_update(cache) ps.hard_update_list(cache, gates["self_react"][2], gates["Pins"]) iterate(iterations_max, force_map, force_parameters, out_params) - return ps.verts, ps.rads, ps.vel + return ps.verts, ps.rads, ps.vel, ps.params["Pins Reactions"] def iterate(iterations_max, force_map, force_parameters, out_params): @@ -844,15 +844,16 @@ def prepare_output_data(ps, gate): use_numpy_out = gate if use_numpy_out: - return [ps.verts, ps.rads, ps.vel] + return [ps.verts, ps.rads, ps.vel, ps.params["Pins Reactions"]] else: - return [ps.verts.tolist(), ps.rads.tolist(), ps.vel.tolist()] + return [ps.verts.tolist(), ps.rads.tolist(), ps.vel.tolist(), ps.params["Pins Reactions"].tolist()] def record_data(data_out, out_lists): '''save data to main list''' - verts_out, rads_out, velocity_out = out_lists - new_verts, new_rad, new_vel = data_out + verts_out, rads_out, velocity_out, reactions_out = out_lists + new_verts, new_rad, new_vel, new_react = data_out verts_out.append(new_verts) rads_out.append(new_rad) velocity_out.append(new_vel) + reactions_out.append(new_react) \ No newline at end of file -- GitLab From fb5639f5342990da6d200cc467ba6bb0c8a2afb9 Mon Sep 17 00:00:00 2001 From: Victor Doval <10011941+vicdoval@users.noreply.github.com> Date: Thu, 18 Apr 2019 21:10:58 +0200 Subject: [PATCH 083/137] transform_select PolyEdge mask added and docs --- .../modifier_change/modifier_change_index.rst | 1 + .../modifier_change/transform_select.rst | 47 +++++ nodes/modifier_change/transform_select.py | 189 +++++++++++------- 3 files changed, 162 insertions(+), 75 deletions(-) create mode 100644 docs/nodes/modifier_change/transform_select.rst diff --git a/docs/nodes/modifier_change/modifier_change_index.rst b/docs/nodes/modifier_change/modifier_change_index.rst index 9cbfb9057..b19e39eb0 100644 --- a/docs/nodes/modifier_change/modifier_change_index.rst +++ b/docs/nodes/modifier_change/modifier_change_index.rst @@ -28,5 +28,6 @@ Modifier Change randomize recalc_normals remove_doubles + transform_select triangulate vertices_mask diff --git a/docs/nodes/modifier_change/transform_select.rst b/docs/nodes/modifier_change/transform_select.rst new file mode 100644 index 000000000..4391c2513 --- /dev/null +++ b/docs/nodes/modifier_change/transform_select.rst @@ -0,0 +1,47 @@ +Transform Select +================ + +Functionality +------------- + +This node splits the vertex data in two groups, applies one different matrix to each group and joins it again +This node is useful mainly when other node generates ngons, especially not-convex ones. + +Inputs +------ + +This node has the following inputs: + +- **Mask**: List of boolean or integer flags. If this input is not connected, a True, False, True.. mask will be created. +- **Verts**: Vertex list. +- **PolyEdge** : It can be Polygon or Edge data. +- **Matrix T** : Matrix applied to the vertex flaged as true. +- **Matrix F** : Matrix applied to the vertex flaged as false. + +Parameters +---------- + +This node has the following parameters: + +- **Mask Type**: Specifies if the supplied mask refers to the Vertex data or to the PoleEdge data + + + +Outputs +------- + +This node has the following outputs: + +- **Vertices**. The whole group of vertices +- **PolyEdge**. A copy of the PolyEdge data supplyed +- **PolyEdge O**. PolyEdge data with vertices which are true and false (index refering to Vertices output) +- **Vertices T**. Only the vertices marked as true +- **PolyEdge T**. PolyEdge data with vertices which are true (index refering to Vertices T output) +- **Vertices F**. Only the vertices marked as false +- **PolyEdge F**. PolyEdge data with vertices which are false (index refering to Vertices F output) + + +Examples of usage +----------------- + + diff --git a/nodes/modifier_change/transform_select.py b/nodes/modifier_change/transform_select.py index ef3441c57..eb4b301f7 100644 --- a/nodes/modifier_change/transform_select.py +++ b/nodes/modifier_change/transform_select.py @@ -16,111 +16,150 @@ # # ##### END GPL LICENSE BLOCK ##### +from itertools import chain import bpy -from bpy.props import IntProperty, FloatProperty, BoolProperty, EnumProperty - +from bpy.props import EnumProperty from mathutils import Matrix, Vector - from sverchok.node_tree import SverchCustomTreeNode -from sverchok.data_structure import updateNode, match_long_repeat, Matrix_generate +from sverchok.data_structure import updateNode, match_long_repeat -maskTypeItems = [("VERTICES", "V", ""), ("EDGES", "E", ""), ("POLYGONS", "P", ""), ] +maskTypeItems = [("VERTICES", "Verts", "Mask refers to Vertices", 0), ("POLY_EDGE", "PolyEdge", "Mask refers to PolyEdge", 1), ] class SvTransformSelectNode(bpy.types.Node, SverchCustomTreeNode): - ''' Transform Select ''' + """ + Triggers: Apply matrix w. mask. + Tooltip: Transform part of geometry. + """ bl_idname = 'SvTransformSelectNode' bl_label = 'Transform Select' + bl_icon = 'EDITMODE_HLT' maskType = EnumProperty( name="Mask Type", description="Mask various mesh components", default="VERTICES", items=maskTypeItems, update=updateNode) def draw_buttons(self, context, layout): + '''draw ui buttons''' layout.prop(self, 'maskType', expand=True) def sv_init(self, context): - self.inputs.new('StringsSocket', "Mask") - self.inputs.new('VerticesSocket', "Vertices") - self.inputs.new('StringsSocket', "PolyEdge") - self.inputs.new('MatrixSocket', "Matrix T") - self.inputs.new('MatrixSocket', "Matrix F") - - self.outputs.new('VerticesSocket', "Vertices") - self.outputs.new('StringsSocket', "PolyEdge") - self.outputs.new('StringsSocket', "PolyEdge O") - self.outputs.new('VerticesSocket', "Vertices T") - self.outputs.new('StringsSocket', "PolyEdge T") - self.outputs.new('VerticesSocket', "Vertices F") - self.outputs.new('StringsSocket', "PolyEdge F") - - def process(self): - # return if no outputs are connected - if not any(s.is_linked for s in self.outputs): - return - + '''define input and output sockets''' + sin = self.inputs.new + son = self.outputs.new + sin('StringsSocket', "Mask") + sin('VerticesSocket', "Vertices") + sin('StringsSocket', "PolyEdge") + sin('MatrixSocket', "Matrix T") + sin('MatrixSocket', "Matrix F") + + son('VerticesSocket', "Vertices") + son('StringsSocket', "PolyEdge") + son('StringsSocket', "PolyEdge O") + son('VerticesSocket', "Vertices T") + son('StringsSocket', "PolyEdge T") + son('VerticesSocket', "Vertices F") + son('StringsSocket', "PolyEdge F") + + def get_data(self): + '''get data from inputs and match it''' inputs = self.inputs - outputs = self.outputs - - identityMatrix = [[tuple(v) for v in Matrix()]] + polys_linked = inputs['PolyEdge'].is_linked input_verts = inputs['Vertices'].sv_get()[0] - input_polys = inputs['PolyEdge'].sv_get()[0] - input_matrixT = inputs['Matrix T'].sv_get(default=identityMatrix) - input_matrixF = inputs['Matrix F'].sv_get(default=identityMatrix) - n = len(input_verts) + input_polys = inputs['PolyEdge'].sv_get(default=[[]])[0] + matrix_true = inputs['Matrix T'].sv_get(default=[Matrix()]) + matrix_false = inputs['Matrix F'].sv_get(default=[Matrix()]) - if inputs['Mask'].is_linked: - input_mask = inputs['Mask'].sv_get()[0][:n] - input_mask = list(map(lambda x: int(x) % 2, input_mask)) - else: # if no mask input, generate a 0,1,0,1 mask - input_mask = ([1, 0] * (int((n + 1) / 2)))[:n] + input_mask = self.get_mask(n, input_polys, polys_linked) + matrix_false = matrix_false[:n] + matrix_true = matrix_true[:n] - matrixF = input_matrixF[:n] - matrixT = input_matrixT[:n] + params = match_long_repeat([input_mask, input_verts, matrix_true, matrix_false]) - params = match_long_repeat([input_mask, input_verts, matrixT, matrixF]) + return params, input_polys, polys_linked - # process vertices - vertListA, vertListT, vertListF = [[], [], []] + def get_mask(self, n, input_polys, polys_linked): + '''get mask and convert it to vertices mask if needed''' + inputs = self.inputs + if self.maskType == "VERTICES" or not polys_linked: + if inputs['Mask'].is_linked: + input_mask = inputs['Mask'].sv_get()[0][:n] + input_mask = list(map(lambda x: int(x) % 2, input_mask)) + else: # if no mask input, generate a 0,1,0,1 mask + input_mask = ([1, 0] * (int((n + 1) / 2)))[:n] + else: + len_poly_edge = len(input_polys) + if inputs['Mask'].is_linked: + input_mask = inputs['Mask'].sv_get()[0][:len_poly_edge] + input_mask = list(map(lambda x: int(x) % 2, input_mask)) + else: # if no mask input, generate a 0,1,0,1 mask + input_mask = ([1, 0] * (int((n + 1) / 2)))[:len_poly_edge] + + masked_pe = [pe for i, pe in enumerate(input_polys) if input_mask[i] == 1] + verts_in_pe = set(chain(*masked_pe)) + input_mask = [(i in verts_in_pe) for i in range(n)] + + return input_mask + + def process_vertices(self, params): + '''apply matrices to vertices''' + vert_all, vert_true, vert_false = [[], [], []] for ma, v, mt, mf in zip(*params): if ma == 1: # do some processing using Matrix T here v = (mt * Vector(v))[:] - vertListT.append(v) + vert_true.append(v) else: # do some processing using Matrix F here v = (mf * Vector(v))[:] - vertListF.append(v) - vertListA.append(v) - - # process polyEdges - vert_indexT = [i for i, m in enumerate(input_mask) if m] - vert_indexF = [i for i, m in enumerate(input_mask) if not m] - vt = {j: i for i, j in enumerate(vert_indexT)} - vf = {j: i for i, j in enumerate(vert_indexF)} - vext = set(vert_indexT) - vexf = set(vert_indexF) - - polyEdgeListA = input_polys - polyEdgeListT, polyEdgeListF, polyEdgeListO = [[], [], []] - - inSetT, inSetF = vext.issuperset, vexf.issuperset - addPET, addPEF, addPEO = polyEdgeListT.append, polyEdgeListF.append, polyEdgeListO.append - for pe in input_polys: - pex = set(pe) - if inSetT(pex): - addPET([vt[i] for i in pe]) - elif inSetF(pex): - addPEF([vf[i] for i in pe]) - else: - addPEO(pe) - - outputs['Vertices'].sv_set([vertListA]) - outputs['PolyEdge'].sv_set([polyEdgeListA]) - outputs['PolyEdge O'].sv_set([polyEdgeListO]) - outputs['Vertices T'].sv_set([vertListT]) - outputs['PolyEdge T'].sv_set([polyEdgeListT]) - outputs['Vertices F'].sv_set([vertListF]) - outputs['PolyEdge F'].sv_set([polyEdgeListF]) + vert_false.append(v) + vert_all.append(v) + + return vert_all, vert_true, vert_false + + def process_poly_edge(self, polys_linked, input_polys, input_mask): + '''split poly_edge data''' + poly_edge_true, poly_edge_false, poly_edge_other = [[], [], []] + if polys_linked: + vert_index_true = [i for i, m in enumerate(input_mask) if m] + vert_index_false = [i for i, m in enumerate(input_mask) if not m] + vt = {j: i for i, j in enumerate(vert_index_true)} + vf = {j: i for i, j in enumerate(vert_index_false)} + + vext = set(vert_index_true) + vexf = set(vert_index_false) + + in_set_true, in_set_false = vext.issuperset, vexf.issuperset + for pe in input_polys: + pex = set(pe) + if in_set_true(pex): + poly_edge_true.append([vt[i] for i in pe]) + elif in_set_false(pex): + poly_edge_false.append([vf[i] for i in pe]) + else: + poly_edge_other.append(pe) + + return poly_edge_true, poly_edge_false, poly_edge_other + + def process(self): + # return if no outputs are connected + if not any(s.is_linked for s in self.outputs): + return + + params, input_polys, polys_linked = self.get_data() + + vert_all, vert_true, vert_false = self.process_vertices(params) + + poly_edge_all = input_polys + poly_edge_true, poly_edge_false, poly_edge_other = self.process_poly_edge(polys_linked, input_polys, params[0]) + + so = self.outputs + so['Vertices'].sv_set([vert_all]) + so['PolyEdge'].sv_set([poly_edge_all]) + so['PolyEdge O'].sv_set([poly_edge_other]) + so['Vertices T'].sv_set([vert_true]) + so['PolyEdge T'].sv_set([poly_edge_true]) + so['Vertices F'].sv_set([vert_false]) + so['PolyEdge F'].sv_set([poly_edge_false]) def register(): -- GitLab From d2236f438ce400246561c827948c7e4e08c97286 Mon Sep 17 00:00:00 2001 From: Victor Doval <10011941+vicdoval@users.noreply.github.com> Date: Thu, 18 Apr 2019 21:21:43 +0200 Subject: [PATCH 084/137] Transform Select examples of use --- docs/nodes/modifier_change/transform_select.rst | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/docs/nodes/modifier_change/transform_select.rst b/docs/nodes/modifier_change/transform_select.rst index 4391c2513..2bcbd8665 100644 --- a/docs/nodes/modifier_change/transform_select.rst +++ b/docs/nodes/modifier_change/transform_select.rst @@ -44,4 +44,10 @@ This node has the following outputs: Examples of usage ----------------- - +Showing the different edges groups: +.. image:: https://user-images.githubusercontent.com/10011941/56385208-c975eb00-621e-11e9-9b6f-6578ac91b704.png + :alt: Transform_Select_edges groups_node.png + +You can input multiple matrices and they will be paired with the verts: +.. image:: https://user-images.githubusercontent.com/10011941/56385210-cda20880-621e-11e9-855f-dd387794873c.png + :alt: Transform_Select_procedural_node.png -- GitLab From 920598914e5f5d5af21812cceb078e1774f5855b Mon Sep 17 00:00:00 2001 From: Victor Doval <10011941+vicdoval@users.noreply.github.com> Date: Thu, 18 Apr 2019 21:33:57 +0200 Subject: [PATCH 085/137] Trasform Select docs fixing typos --- docs/nodes/modifier_change/transform_select.rst | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/nodes/modifier_change/transform_select.rst b/docs/nodes/modifier_change/transform_select.rst index 2bcbd8665..4d0ebadf4 100644 --- a/docs/nodes/modifier_change/transform_select.rst +++ b/docs/nodes/modifier_change/transform_select.rst @@ -4,8 +4,8 @@ Transform Select Functionality ------------- -This node splits the vertex data in two groups, applies one different matrix to each group and joins it again -This node is useful mainly when other node generates ngons, especially not-convex ones. +This node splits the vertex data in two groups, applies one different matrix to each group and joins it again. +It would work as a standard transformation of the selected geometry when working on "Edit Mode". Inputs ------ @@ -34,11 +34,11 @@ This node has the following outputs: - **Vertices**. The whole group of vertices - **PolyEdge**. A copy of the PolyEdge data supplyed -- **PolyEdge O**. PolyEdge data with vertices which are true and false (index refering to Vertices output) +- **PolyEdge O**. PolyEdge data with vertices which are true and false (index referred to "Vertices" output) - **Vertices T**. Only the vertices marked as true -- **PolyEdge T**. PolyEdge data with vertices which are true (index refering to Vertices T output) +- **PolyEdge T**. PolyEdge data with vertices which are true (index referred to "Vertices T" output) - **Vertices F**. Only the vertices marked as false -- **PolyEdge F**. PolyEdge data with vertices which are false (index refering to Vertices F output) +- **PolyEdge F**. PolyEdge data with vertices which are false (index referred to "Vertices F" output) Examples of usage -- GitLab From 26e871471a20f7691a858b4959928fc2eed16466 Mon Sep 17 00:00:00 2001 From: Victor Doval <10011941+vicdoval@users.noreply.github.com> Date: Thu, 18 Apr 2019 22:10:57 +0200 Subject: [PATCH 086/137] Trasform Select docs fixing typos again --- docs/nodes/modifier_change/transform_select.rst | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/nodes/modifier_change/transform_select.rst b/docs/nodes/modifier_change/transform_select.rst index 4d0ebadf4..462cfdb20 100644 --- a/docs/nodes/modifier_change/transform_select.rst +++ b/docs/nodes/modifier_change/transform_select.rst @@ -45,9 +45,11 @@ Examples of usage ----------------- Showing the different edges groups: + .. image:: https://user-images.githubusercontent.com/10011941/56385208-c975eb00-621e-11e9-9b6f-6578ac91b704.png :alt: Transform_Select_edges groups_node.png -You can input multiple matrices and they will be paired with the verts: +You can input multiple matrices and they will be paired with the verts: + .. image:: https://user-images.githubusercontent.com/10011941/56385210-cda20880-621e-11e9-855f-dd387794873c.png :alt: Transform_Select_procedural_node.png -- GitLab From 8f8efb3b85542be295640d20d73dc065c5f94ff9 Mon Sep 17 00:00:00 2001 From: Kosvor2 Date: Sat, 20 Apr 2019 15:43:05 +0500 Subject: [PATCH 087/137] Update socket_data.py --- core/socket_data.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/socket_data.py b/core/socket_data.py index 92cba4272..55f333c27 100644 --- a/core/socket_data.py +++ b/core/socket_data.py @@ -63,7 +63,7 @@ def SvGetSocketInfo(socket): if s_id in socket_data_cache[ng]: data = socket_data_cache[ng][s_id] if data: - return str(len(data)) + return "("+str(len(data)) + " obj)" return '' -- GitLab From fb8bb3e994660bc9246363bc0cd6529ef7ef3e13 Mon Sep 17 00:00:00 2001 From: Victor Doval <10011941+vicdoval@users.noreply.github.com> Date: Sun, 21 Apr 2019 20:05:11 +0200 Subject: [PATCH 088/137] Pulga Physics Examples and pin reactions bug fixed --- docs/nodes/alpha_nodes/alpha_nodes_index.rst | 1 + json_examples/Architecture/Tensile_Cover.json | 1023 +++++++++++++++++ .../circle_fitting_physics.json | 152 +++ utils/pulga_physics_core.py | 3 +- 4 files changed, 1178 insertions(+), 1 deletion(-) create mode 100644 json_examples/Architecture/Tensile_Cover.json create mode 100644 json_examples/ParametricModelling/circle_fitting_physics.json diff --git a/docs/nodes/alpha_nodes/alpha_nodes_index.rst b/docs/nodes/alpha_nodes/alpha_nodes_index.rst index d7772b2c4..3982622d7 100644 --- a/docs/nodes/alpha_nodes/alpha_nodes_index.rst +++ b/docs/nodes/alpha_nodes/alpha_nodes_index.rst @@ -8,3 +8,4 @@ Alpha Nodes offset_line contour2D planar_edgenet_to_polygons + pulga_physics diff --git a/json_examples/Architecture/Tensile_Cover.json b/json_examples/Architecture/Tensile_Cover.json new file mode 100644 index 000000000..3d31db403 --- /dev/null +++ b/json_examples/Architecture/Tensile_Cover.json @@ -0,0 +1,1023 @@ +{ + "export_version": "0.072", + "framed_nodes": {}, + "groups": {}, + "nodes": { + "3pt Arc": { + "bl_idname": "svBasicArcNode", + "color": [ + 0.0, + 0.5, + 0.5 + ], + "custom_socket_props": { + "1": { + "use_expander": false + } + }, + "height": 100.0, + "hide": false, + "label": "", + "location": [ + 771.2139892578125, + -768.0457763671875 + ], + "params": { + "num_verts": 128 + }, + "use_custom_color": true, + "width": 140.0 + }, + "A Number": { + "bl_idname": "SvNumberNode", + "height": 100.0, + "hide": false, + "label": "", + "location": [ + 774.5885009765625, + -906.98291015625 + ], + "params": { + "float_": 1.0099999904632568, + "int_": 0 + }, + "width": 140.0 + }, + "Line.002": { + "bl_idname": "SvLineNodeMK3", + "color": [ + 0.0, + 0.5, + 0.5 + ], + "height": 100.0, + "hide": false, + "label": "", + "location": [ + 1869.468017578125, + -227.23074340820312 + ], + "params": { + "direction": "OD", + "step": 12.069986343383789 + }, + "use_custom_color": true, + "width": 152.5849609375 + }, + "List Input": { + "bl_idname": "SvListInputNode", + "height": 100.0, + "hide": false, + "label": "Pin List", + "location": [ + -207.8728485107422, + -217.38880920410156 + ], + "params": { + "int_": 6, + "int_list": [ + 0, + 1, + 2, + 5, + 6, + 4, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + }, + "width": 140.0 + }, + "List Input.001": { + "bl_idname": "SvListInputNode", + "height": 100.0, + "hide": false, + "label": "Pin split", + "location": [ + -207.8728485107422, + -475.57794189453125 + ], + "params": { + "int_": 6, + "int_list": [ + 0, + 1, + 1, + 0, + 1, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + }, + "width": 140.0 + }, + "List Input.002": { + "bl_idname": "SvListInputNode", + "height": 100.0, + "hide": false, + "label": "", + "location": [ + 421.470703125, + -856.1739501953125 + ], + "params": { + "int_": 3, + "int_list": [ + 0, + 1, + 3, + 2, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + }, + "width": 140.0 + }, + "List Input.003": { + "bl_idname": "SvListInputNode", + "height": 100.0, + "hide": false, + "label": "", + "location": [ + 979.9804077148438, + -928.244384765625 + ], + "params": { + "int_": 2, + "int_list": [ + 0, + 180, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + }, + "width": 140.0 + }, + "List Item": { + "bl_idname": "ListItem2Node", + "height": 100.0, + "hide": false, + "label": "", + "location": [ + 16.375446319580078, + -278.7489318847656 + ], + "params": {}, + "width": 140.0 + }, + "List Item.001": { + "bl_idname": "ListItem2Node", + "height": 100.0, + "hide": false, + "label": "", + "location": [ + 609.845703125, + -766.492919921875 + ], + "params": {}, + "width": 140.0 + }, + "List Item.002": { + "bl_idname": "ListItem2Node", + "height": 100.0, + "hide": false, + "label": "", + "location": [ + 393.3372497558594, + 472.56988525390625 + ], + "params": {}, + "width": 140.0 + }, + "List Mask (out).001": { + "bl_idname": "MaskListNode", + "height": 100.0, + "hide": true, + "label": "", + "location": [ + 1008.141357421875, + -423.2999572753906 + ], + "params": { + "Level": 2 + }, + "width": 140.0 + }, + "List Mask (out).002": { + "bl_idname": "MaskListNode", + "height": 100.0, + "hide": true, + "label": "", + "location": [ + 1017.4197998046875, + -340.83056640625 + ], + "params": { + "Level": 2 + }, + "width": 140.0 + }, + "Math MK2": { + "bl_idname": "SvScalarMathNodeMK2", + "height": 100.0, + "hide": false, + "label": "", + "location": [ + 550.5531005859375, + 472.56988525390625 + ], + "params": { + "y_": 0.5800000429153442 + }, + "width": 140.0 + }, + "Math MK2.001": { + "bl_idname": "SvScalarMathNodeMK2", + "height": 100.0, + "hide": false, + "label": "", + "location": [ + 1506.8905029296875, + -60.17683410644531 + ], + "params": { + "current_op": "SINE", + "input_mode_two": "Float" + }, + "width": 140.0 + }, + "Math MK2.002": { + "bl_idname": "SvScalarMathNodeMK2", + "height": 100.0, + "hide": false, + "label": "", + "location": [ + 1680.4859619140625, + -106.64553833007812 + ], + "params": { + "current_op": "DIV", + "input_mode_two": "Float" + }, + "width": 140.0 + }, + "Matrix in": { + "bl_idname": "SvMatrixGenNodeMK2", + "custom_socket_props": { + "0": { + "expanded": true + } + }, + "height": 100.0, + "hide": false, + "label": "Elevated Pins Matrix", + "location": [ + -207.8728485107422, + -723.6800537109375 + ], + "params": { + "l_": [ + 0.0, + 0.0, + 10.0 + ] + }, + "width": 140.0 + }, + "Matrix in.001": { + "bl_idname": "SvMatrixGenNodeMK2", + "height": 100.0, + "hide": true, + "label": "", + "location": [ + 229.70703125, + -572.1517944335938 + ], + "params": {}, + "width": 140.0 + }, + "Matrix in.003": { + "bl_idname": "SvMatrixGenNodeMK2", + "custom_socket_props": { + "2": { + "expanded": true + } + }, + "height": 100.0, + "hide": true, + "label": "", + "location": [ + 1129.7581787109375, + -894.1826171875 + ], + "params": { + "a_": 90.0, + "r_": [ + 0.0, + 0.0, + 1.0 + ] + }, + "width": 140.0 + }, + "Path Length": { + "bl_idname": "SvPathLengthNode", + "height": 100.0, + "hide": false, + "label": "", + "location": [ + 214.44064331054688, + 472.56988525390625 + ], + "params": {}, + "width": 140.0 + }, + "Polygon Grid": { + "bl_idname": "SvPolygonGridNode", + "height": 100.0, + "hide": false, + "label": "Base Polygon Grid", + "location": [ + -211.82144165039062, + 132.7971954345703 + ], + "params": { + "gridType": "TRIANGLE", + "numx": 2, + "numy": 3, + "radius": 8.0 + }, + "width": 170.0 + }, + "Pulga Physics.001": { + "bl_idname": "SvPulgaPhysicsNode", + "height": 100.0, + "hide": false, + "label": "", + "location": [ + 734.7672729492188, + 281.56048583984375 + ], + "params": { + "drag_M": 1, + "drag_force": 0.029999999329447746, + "iterations": 224, + "max_vel": 0.0, + "pins_M": 1, + "spring_k": 0.019999999552965164, + "springs_M": 1 + }, + "width": 200.0 + }, + "Reroute": { + "bl_idname": "NodeReroute", + "height": 100.0, + "hide": false, + "label": "", + "location": [ + 625.296142578125, + -511.2069396972656 + ], + "params": {}, + "width": 16.0 + }, + "Reroute.002": { + "bl_idname": "NodeReroute", + "height": 100.0, + "hide": false, + "label": "", + "location": [ + 958.3682861328125, + 315.803955078125 + ], + "params": {}, + "width": 16.0 + }, + "Reroute.003": { + "bl_idname": "NodeReroute", + "height": 100.0, + "hide": false, + "label": "", + "location": [ + 233.83367919921875, + 315.803955078125 + ], + "params": {}, + "width": 16.0 + }, + "Subdivide": { + "bl_idname": "SvSubdivideNode", + "height": 100.0, + "hide": false, + "label": "", + "location": [ + 18.563705444335938, + 256.0939025878906 + ], + "params": { + "cuts": 8 + }, + "width": 140.0 + }, + "Transform Select": { + "bl_idname": "SvTransformSelectNode", + "height": 100.0, + "hide": false, + "label": "", + "location": [ + 356.117919921875, + -162.89027404785156 + ], + "params": { + "maskType": "VERTICES" + }, + "width": 140.0 + }, + "UV Connection": { + "bl_idname": "LineConnectNodeMK2", + "height": 100.0, + "hide": false, + "label": "", + "location": [ + 1052.89990234375, + -627.4869384765625 + ], + "params": { + "dir_check": "V_dir", + "slice_check": 0 + }, + "width": 140.0 + }, + "Vector Math": { + "bl_idname": "SvVectorMathNodeMK2", + "height": 100.0, + "hide": false, + "label": "", + "location": [ + 1338.8482666015625, + -41.62611389160156 + ], + "params": { + "current_op": "ANGLE RAD" + }, + "width": 140.0 + }, + "Vector Math.001": { + "bl_idname": "SvVectorMathNodeMK2", + "height": 100.0, + "hide": true, + "label": "", + "location": [ + 937.04638671875, + -873.6484375 + ], + "params": { + "amount": 1.0099999904632568, + "current_op": "SCALAR" + }, + "width": 140.0 + }, + "Vector Rewire": { + "bl_idname": "SvVectorRewire", + "height": 100.0, + "hide": false, + "label": "", + "location": [ + 1169.88916015625, + -35.26878356933594 + ], + "params": { + "scalar": -0.1600002497434616, + "selected_mode_from": "Scalar" + }, + "width": 140.0 + }, + "Vector Rewire.001": { + "bl_idname": "SvVectorRewire", + "height": 100.0, + "hide": false, + "label": "", + "location": [ + 1162.1392822265625, + -176.45098876953125 + ], + "params": { + "scalar": 0.0, + "selected_mode_from": "Scalar" + }, + "width": 140.0 + }, + "Vector out": { + "bl_idname": "VectorsOutNode", + "height": 100.0, + "hide": false, + "label": "", + "location": [ + 1494.76171875, + -176.2838134765625 + ], + "params": {}, + "width": 140.0 + }, + "Viewer Draw.001": { + "bl_idname": "ViewerNode2", + "color": [ + 1.0, + 0.30000001192092896, + 0.0 + ], + "height": 100.0, + "hide": false, + "label": "", + "location": [ + 995.3924560546875, + 291.8858642578125 + ], + "params": { + "activate": 1, + "face_colors": [ + 0.011294033378362656, + 0.02265758253633976, + 0.8990001082420349 + ], + "shading": 1 + }, + "use_custom_color": true, + "width": 140.0 + }, + "Viewer Draw.002": { + "bl_idname": "ViewerNode2", + "color": [ + 1.0, + 0.30000001192092896, + 0.0 + ], + "height": 100.0, + "hide": false, + "label": "", + "location": [ + 2078.14404296875, + -103.44867706298828 + ], + "params": { + "activate": 1, + "edge_colors": [ + 0.33089834451675415, + 0.8990001082420349, + 0.395736426115036 + ], + "edge_width": 4, + "vertex_colors": [ + 0.08003409206867218, + 0.9480001330375671, + 0.0 + ], + "vertex_size": 9.984001159667969 + }, + "use_custom_color": true, + "width": 140.0 + }, + "Viewer Draw.005": { + "bl_idname": "ViewerNode2", + "color": [ + 1.0, + 0.30000001192092896, + 0.0 + ], + "height": 100.0, + "hide": false, + "label": "", + "location": [ + 1234.7142333984375, + -620.2259521484375 + ], + "params": { + "activate": 1, + "edge_colors": [ + 0.8990001082420349, + 0.0031109414994716644, + 0.010177122429013252 + ] + }, + "use_custom_color": true, + "width": 140.0 + } + }, + "update_lists": [ + [ + "Polygon Grid", + 1, + "List Item", + 0 + ], + [ + "List Input", + 0, + "List Item", + 1 + ], + [ + "List Input.001", + "List", + "Reroute", + "Input" + ], + [ + "Polygon Grid", + 1, + "Subdivide", + 0 + ], + [ + "Polygon Grid", + 2, + "Subdivide", + 1 + ], + [ + "Polygon Grid", + 3, + "Subdivide", + 2 + ], + [ + "Subdivide", + 0, + "Path Length", + 0 + ], + [ + "Subdivide", + 1, + "Path Length", + 1 + ], + [ + "Path Length", + 0, + "List Item.002", + 0 + ], + [ + "List Item.002", + 0, + "Math MK2", + 0 + ], + [ + "List Input.001", + 0, + "Transform Select", + 0 + ], + [ + "List Item", + 0, + "Transform Select", + 1 + ], + [ + "Polygon Grid", + 3, + "Transform Select", + 2 + ], + [ + "Matrix in", + 0, + "Transform Select", + 3 + ], + [ + "Matrix in.001", + 0, + "Transform Select", + 4 + ], + [ + "Subdivide", + 0, + "Pulga Physics.001", + 0 + ], + [ + "Subdivide", + 1, + "Pulga Physics.001", + 2 + ], + [ + "Math MK2", + 0, + "Pulga Physics.001", + 3 + ], + [ + "List Input", + 0, + "Pulga Physics.001", + 5 + ], + [ + "Transform Select", + 0, + "Pulga Physics.001", + 6 + ], + [ + "Pulga Physics.001", + 3, + "List Mask (out).002", + 0 + ], + [ + "Reroute", + "Output", + "List Mask (out).002", + "mask" + ], + [ + "List Mask (out).002", + 3, + "Vector Rewire.001", + 0 + ], + [ + "List Mask (out).002", + 3, + "Vector Rewire", + 0 + ], + [ + "Vector Rewire", + 0, + "Vector Math", + 0 + ], + [ + "Vector Rewire.001", + 0, + "Vector Math", + 1 + ], + [ + "Vector Math", + 0, + "Math MK2.001", + 0 + ], + [ + "Transform Select", + 0, + "List Mask (out).001", + 0 + ], + [ + "Reroute", + "Output", + "List Mask (out).001", + "mask" + ], + [ + "List Mask (out).001", + 3, + "Vector out", + 0 + ], + [ + "Vector out", + 2, + "Math MK2.002", + 0 + ], + [ + "Math MK2.001", + 0, + "Math MK2.002", + 1 + ], + [ + "Math MK2.002", + 0, + "Line.002", + 1 + ], + [ + "List Mask (out).001", + 3, + "Line.002", + 3 + ], + [ + "Vector Rewire", + 0, + "Line.002", + 4 + ], + [ + "Transform Select", + 0, + "List Item.001", + 0 + ], + [ + "List Input.002", + 0, + "List Item.001", + 1 + ], + [ + "Subdivide", + "Faces", + "Reroute.003", + "Input" + ], + [ + "List Input.003", + 0, + "Matrix in.003", + 3 + ], + [ + "List Item.001", + 0, + "3pt Arc", + 1 + ], + [ + "3pt Arc", + 0, + "Vector Math.001", + 0 + ], + [ + "A Number", + 0, + "Vector Math.001", + 1 + ], + [ + "3pt Arc", + 0, + "UV Connection", + 0 + ], + [ + "Vector Math.001", + 0, + "UV Connection", + 1 + ], + [ + "UV Connection", + 0, + "Viewer Draw.005", + 0 + ], + [ + "UV Connection", + 1, + "Viewer Draw.005", + 1 + ], + [ + "Matrix in.003", + 0, + "Viewer Draw.005", + 2 + ], + [ + "Reroute.003", + "Output", + "Reroute.002", + "Input" + ], + [ + "Line.002", + 0, + "Viewer Draw.002", + 0 + ], + [ + "Line.002", + 1, + "Viewer Draw.002", + 1 + ], + [ + "Pulga Physics.001", + 0, + "Viewer Draw.001", + 0 + ], + [ + "Reroute.002", + "Output", + "Viewer Draw.001", + "edg_pol" + ] + ] +} \ No newline at end of file diff --git a/json_examples/ParametricModelling/circle_fitting_physics.json b/json_examples/ParametricModelling/circle_fitting_physics.json new file mode 100644 index 000000000..6f2752a95 --- /dev/null +++ b/json_examples/ParametricModelling/circle_fitting_physics.json @@ -0,0 +1,152 @@ +{ + "export_version": "0.072", + "framed_nodes": {}, + "groups": {}, + "nodes": { + "Circle": { + "bl_idname": "SvCircleNode", + "color": [ + 0.0, + 0.5, + 0.5 + ], + "height": 100.0, + "hide": false, + "label": "", + "location": [ + 57.291385650634766, + 429.27984619140625 + ], + "params": { + "mode_": 0 + }, + "use_custom_color": true, + "width": 140.0 + }, + "Plane MK2": { + "bl_idname": "SvPlaneNodeMK2", + "color": [ + 0.0, + 0.5, + 0.5 + ], + "height": 100.0, + "hide": false, + "label": "", + "location": [ + -483.3358459472656, + 243.8773956298828 + ], + "params": { + "normalize": 1, + "numx": 10, + "numy": 10, + "sizex": 10.0 + }, + "use_custom_color": true, + "width": 140.0 + }, + "Pulga Physics": { + "bl_idname": "SvPulgaPhysicsNode", + "height": 100.0, + "hide": false, + "label": "", + "location": [ + -202.40333557128906, + 275.76348876953125 + ], + "params": { + "attract_M": 0, + "attract_decay": 2.0, + "density": 0.429999977350235, + "drag_M": 1, + "drag_force": 0.10000000149011612, + "fit_M": 1, + "grow": 0.009999999776482582, + "iterations": 139, + "max_rad": 2.559999942779541, + "max_vel": 0.009999999776482582, + "self_attract": 0.0024999999441206455, + "self_attract_M": 1, + "self_collision": 0.10000000149011612, + "self_react_M": 1 + }, + "width": 200.0 + }, + "Random Num Gen": { + "bl_idname": "SvRndNumGen", + "height": 100.0, + "hide": false, + "label": "", + "location": [ + -485.1219482421875, + -41.92808532714844 + ], + "params": { + "high_f": 1.0, + "low_f": 0.10000000149011612, + "size": 400, + "type_selected_mode": "Float" + }, + "width": 140.0 + }, + "Viewer Draw": { + "bl_idname": "ViewerNode2", + "color": [ + 1.0, + 0.30000001192092896, + 0.0 + ], + "height": 100.0, + "hide": false, + "label": "", + "location": [ + 271.23095703125, + 406.8813781738281 + ], + "params": { + "activate": 1 + }, + "use_custom_color": true, + "width": 140.0 + } + }, + "update_lists": [ + [ + "Plane MK2", + 0, + "Pulga Physics", + 0 + ], + [ + "Random Num Gen", + 0, + "Pulga Physics", + 7 + ], + [ + "Pulga Physics", + 1, + "Circle", + 0 + ], + [ + "Circle", + 0, + "Viewer Draw", + 0 + ], + [ + "Circle", + 1, + "Viewer Draw", + 1 + ], + [ + "Pulga Physics", + 0, + "Viewer Draw", + 2 + ] + ] +} \ No newline at end of file diff --git a/utils/pulga_physics_core.py b/utils/pulga_physics_core.py index bdbb1e371..1ee51e0da 100644 --- a/utils/pulga_physics_core.py +++ b/utils/pulga_physics_core.py @@ -705,8 +705,9 @@ class PulgaSystem(): local_func, local_gates, local_params = dicti pins, pins_goal_pos = local_params use_pins, use_pins_goal = local_gates - + if not use_pins: + self.params["Pins Reactions"] = np.array([[]]) return self.params['Pins'] = np.array(pins) -- GitLab From ab92f34496f2ea78426f0690d3b10726357ec421 Mon Sep 17 00:00:00 2001 From: "Ilya V. Portnov" Date: Mon, 22 Apr 2019 11:20:00 +0500 Subject: [PATCH 089/137] Update issue_template.rst remove the header - it is unwanted in the github issue --- docs/issue_template.rst | 4 ---- 1 file changed, 4 deletions(-) diff --git a/docs/issue_template.rst b/docs/issue_template.rst index ae8104435..23661ba0d 100644 --- a/docs/issue_template.rst +++ b/docs/issue_template.rst @@ -1,7 +1,3 @@ -============== -Issue template -============== - ## Problem statement Please describe your problem here. -- GitLab From 255fd139dc4a26981cec51184c794a4f9f2ae53f Mon Sep 17 00:00:00 2001 From: "Ilya V. Portnov" Date: Mon, 22 Apr 2019 11:21:09 +0500 Subject: [PATCH 090/137] Update pull_request_template.rst remove header - it is unwanted in the github pull request --- docs/pull_request_template.rst | 4 ---- 1 file changed, 4 deletions(-) diff --git a/docs/pull_request_template.rst b/docs/pull_request_template.rst index 37346fe75..a741616b0 100644 --- a/docs/pull_request_template.rst +++ b/docs/pull_request_template.rst @@ -1,7 +1,3 @@ -===================== -Pull request template -===================== - ## Addressed problem description Please describe what problem does this PR solve. If it was already was discussed in an issue, it is enough to give a link as #number. -- GitLab From 6f4024cecc4ad2a4c7242d4182df890f801875e3 Mon Sep 17 00:00:00 2001 From: Victor Doval <10011941+vicdoval@users.noreply.github.com> Date: Wed, 24 Apr 2019 00:59:14 +0200 Subject: [PATCH 091/137] fixed memory_to_file bug --- nodes/modifier_change/pulga_physics.py | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/nodes/modifier_change/pulga_physics.py b/nodes/modifier_change/pulga_physics.py index 065bd46ea..293c9d36a 100644 --- a/nodes/modifier_change/pulga_physics.py +++ b/nodes/modifier_change/pulga_physics.py @@ -163,7 +163,7 @@ class SvPulgaPhysicsNode(bpy.types.Node, SverchCustomTreeNode): out = [] for i in range(len(data)): out.append([data[i][0].tolist(), data[i][1].tolist(), data[i][2].tolist(), data[i][3].tolist()]) - check_past_file(out, location) + fill_past_file(out, location) def memory_to_lists(self): '''bump memory to output''' @@ -196,6 +196,12 @@ class SvPulgaPhysicsNode(bpy.types.Node, SverchCustomTreeNode): self.node_cache[0] = {} self.accumulative_reset = False updateNode(self, context) + + def update_memory(self, context): + if self.accumulative_update: + self.accumulative_update = False + if not self.accumulative_parse: + updateNode(self, context) node_cache = {} @@ -210,6 +216,12 @@ class SvPulgaPhysicsNode(bpy.types.Node, SverchCustomTreeNode): description="Restart accumulative memory", default=False, update=reset_memory) + + accumulative_update = BoolProperty( + name="Update", + description="Iterate again", + default=False, + update=update_memory) accumulative_parse = BoolProperty( name="Pause", @@ -416,11 +428,8 @@ class SvPulgaPhysicsNode(bpy.types.Node, SverchCustomTreeNode): if self.accumulative: cr = r4.row(align=True) - # cr.prop(self, "accumulative_pause", toggle=True) cr.prop(self, "accumulative_reset", toggle=True) - upd = cr.operator("node.sverchok_update_current", text="Update") - ng_name = context.space_data.node_tree.name - upd.node_group = ng_name + cr.prop(self,"accumulative_update", toggle=True) cr.prop(self, "accumulative_parse", toggle=True) def draw_buttons_ext(self, context, layout): -- GitLab From 0d8501edc955fbbfaa74bbd93afb14103fef66e6 Mon Sep 17 00:00:00 2001 From: Victor Doval <10011941+vicdoval@users.noreply.github.com> Date: Wed, 24 Apr 2019 20:23:13 +0200 Subject: [PATCH 092/137] selected nodes macros --- utils/sv_default_macros.py | 138 ++++++++++++++++++++++++++++++++++--- 1 file changed, 128 insertions(+), 10 deletions(-) diff --git a/utils/sv_default_macros.py b/utils/sv_default_macros.py index 4cbe62682..96a7c72ab 100644 --- a/utils/sv_default_macros.py +++ b/utils/sv_default_macros.py @@ -45,6 +45,34 @@ macros = { 'display_name': "output current idx / total", 'file': 'macro', 'ident': ['verbose_macro_handler', 'monad info']}, + "> multiply *": { + 'display_name': "multiply selected nodes", + 'file': 'macro', + 'ident': ['verbose_macro_handler', 'mathMUL']}, + "> add +": { + 'display_name': "add selected nodes", + 'file': 'macro', + 'ident': ['verbose_macro_handler', 'mathADD']}, + "> sub -": { + 'display_name': "subtract selected nodes", + 'file': 'macro', + 'ident': ['verbose_macro_handler', 'mathSUB']}, + "> join1": { + 'display_name': "selected nodes to List Join", + 'file': 'macro', + 'ident': ['verbose_macro_handler', 'join1']}, + "> join123": { + 'display_name': "selected nodes to List Join", + 'file': 'macro', + 'ident': ['verbose_macro_handler', 'join123']}, + "> join12": { + 'display_name': "selected nodes to List Join", + 'file': 'macro', + 'ident': ['verbose_macro_handler', 'join12']}, + "> join13": { + 'display_name': "selected nodes to List Join", + 'file': 'macro', + 'ident': ['verbose_macro_handler', 'join13']}, "> sw1": { 'display_name': "connect nodes to switch", 'file': 'macro', @@ -80,6 +108,18 @@ def sn_loader(snlite, script_name=None): snlite.script_name = os.path.basename(txt.name) snlite.load() +def nodes_bounding_box(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) + + return minx, maxx, miny, maxy class DefaultMacros(): @@ -147,17 +187,11 @@ class DefaultMacros(): elif "switch" in term: selected_nodes = context.selected_nodes + if not selected_nodes: + operator.report({"ERROR_INVALID_INPUT"}, 'No selected nodes to use') + return {'CANCELLED'} # 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) - + minx, maxx, miny, maxy = nodes_bounding_box(selected_nodes) switch_node = nodes.new('SvInputSwitchNode') switch_node.location = maxx + 100, maxy @@ -187,6 +221,90 @@ class DefaultMacros(): if len(socket_indices) > 1: links.new(switch_node.outputs[1], viewer_node.inputs[1]) + elif "join" in term: + selected_nodes = context.selected_nodes + if not selected_nodes: + operator.report({"ERROR_INVALID_INPUT"}, 'No selected nodes to join') + return {'CANCELLED'} + # get bounding box of all selected nodes + minx, maxx, miny, maxy = nodes_bounding_box(selected_nodes) + # find out which sockets to connect + socket_numbers = term.replace("join", "") + if len(socket_numbers) == 1: # one socket + socket_indices = [int(socket_numbers) - 1] + else: # multiple sockets + socket_indices = [int(n) - 1 for n in socket_numbers] + + join_nodes=[] + for i, s in enumerate(socket_indices): + join_nodes.append(nodes.new('ListJoinNode')) + join_nodes[i].location = maxx + 100, maxy - (180+(22*(len(selected_nodes)))) * i + + sorted_nodes = sorted(selected_nodes, key=lambda n: n.location.y, reverse=True) + + # link the nodes to ListJoin nodes + for i, node in enumerate(sorted_nodes): + for j, n in enumerate(socket_indices): + links.new(node.outputs[n], join_nodes[j].inputs[i]) + + if all(node.outputs[0].bl_idname == "VerticesSocket" for node in sorted_nodes): + viewer_node = nodes.new("ViewerNode2") + viewer_node.location = join_nodes[0].location.x + join_nodes[0].width + 100, maxy + + # link the output switch node to the ViewerDraw node + links.new(join_nodes[0].outputs[0], viewer_node.inputs[0]) + if len(socket_indices) > 1: + links.new(join_nodes[1].outputs[0], viewer_node.inputs[1]) + + elif "math" in term: + selected_nodes = context.selected_nodes + if not selected_nodes: + operator.report({"ERROR_INVALID_INPUT"}, 'No selected nodes to use') + return {'CANCELLED'} + selected_nodes = selected_nodes[:2] + # get bounding box of all selected nodes + minx, maxx, miny, maxy = nodes_bounding_box(selected_nodes) + # find out which sockets to connect + operator = term.replace("math", "") + sorted_nodes = sorted(selected_nodes, key=lambda n: n.location.y, reverse=True) + is_vector = all(node.outputs[0].bl_idname == "VerticesSocket" for node in sorted_nodes) + if operator == 'MUL': + if is_vector: + math_node = nodes.new('SvVectorMathNodeMK2') + math_node.current_op = 'CROSS' + else: + if (sorted_nodes[0].outputs[0].bl_idname == "VerticesSocket"): + math_node = nodes.new('SvVectorMathNodeMK2') + math_node.current_op = 'SCALAR' + elif (sorted_nodes[1].outputs[0].bl_idname == "VerticesSocket"): + math_node = nodes.new('SvVectorMathNodeMK2') + math_node.current_op = 'SCALAR' + sorted_nodes =[sorted_nodes[1],sorted_nodes[0]] + else: + math_node = nodes.new('SvScalarMathNodeMK2') + math_node.location = maxx + 100, maxy + math_node.current_op = operator + else: + if is_vector: + math_node = nodes.new('SvVectorMathNodeMK2') + math_node.current_op = operator + else: + math_node = nodes.new('SvScalarMathNodeMK2') + math_node.current_op = operator + + math_node.location = maxx + 100, maxy + # link the nodes to Math node + for i, node in enumerate(sorted_nodes): + links.new(node.outputs[0], math_node.inputs[i]) + + if is_vector: + viewer_node = nodes.new("ViewerNode2") + viewer_node.location = math_node.location.x + math_node.width + 100, maxy + + # link the output math node to the ViewerDraw node + links.new(math_node.outputs[0], viewer_node.inputs[0]) + + elif term == 'gp +': needed_nodes = [ ['SvGetAssetProperties', (0.00, 0.00)], -- GitLab From de0d6856f1214e0a9ece73079735cbed5be2d4f5 Mon Sep 17 00:00:00 2001 From: "Ilya V. Portnov" Date: Thu, 25 Apr 2019 22:42:10 +0500 Subject: [PATCH 093/137] Update .travis.yml Update link to blender.tar.gz. There is something with blend4web.com's certificate. It will also update blender on travis from 2.79 to 2.79b. Refs #2402 . --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index ce2ac51ef..e9c80303c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -11,7 +11,7 @@ install: then mkdir -p installation cd installation - wget https://www.blend4web.com/blender/release/Blender2.79/blender-2.79-linux-glibc219-x86_64.tar.bz2 + wget https://ftp.nluug.nl/pub/graphics/blender/release/Blender2.79/blender-2.79b-linux-glibc219-x86_64.tar.bz2 tar xf blender-2.79-linux-glibc219-x86_64.tar.bz2 if [ -d blender ] then rm -rf blender -- GitLab From 43c1863ddf53dc7bfb5149562f48447d79bb381e Mon Sep 17 00:00:00 2001 From: "Ilya V. Portnov" Date: Thu, 25 Apr 2019 22:45:04 +0500 Subject: [PATCH 094/137] Update .travis.yml fix archive name refs #2402 --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index e9c80303c..7a1ba4a1e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -12,7 +12,7 @@ install: mkdir -p installation cd installation wget https://ftp.nluug.nl/pub/graphics/blender/release/Blender2.79/blender-2.79b-linux-glibc219-x86_64.tar.bz2 - tar xf blender-2.79-linux-glibc219-x86_64.tar.bz2 + tar xf blender-2.79b-linux-glibc219-x86_64.tar.bz2 if [ -d blender ] then rm -rf blender fi -- GitLab From d7db76e17a71076c440f1f01e39a25ee81407cf3 Mon Sep 17 00:00:00 2001 From: "Ilya V. Portnov" Date: Thu, 25 Apr 2019 22:51:11 +0500 Subject: [PATCH 095/137] Update .travis.yml #2402 --- .travis.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 7a1ba4a1e..948d98d19 100644 --- a/.travis.yml +++ b/.travis.yml @@ -16,8 +16,8 @@ install: if [ -d blender ] then rm -rf blender fi - mv blender-2.79-linux-glibc219-x86_64 blender - rm blender-2.79-linux-glibc219-x86_64.tar.bz2 + mv blender-2.79b-linux-glibc219-x86_64 blender + rm blender-2.79b-linux-glibc219-x86_64.tar.bz2 echo "Current directory after downloading blender: $(pwd)" cd .. fi -- GitLab From d6513be062d2bf633cbab497ae74a8cc808ddbe1 Mon Sep 17 00:00:00 2001 From: Victor Doval <10011941+vicdoval@users.noreply.github.com> Date: Fri, 26 Apr 2019 12:58:33 +0200 Subject: [PATCH 096/137] test fixing --- tests/references/intersecting_planes_result_faces.txt | 2 +- tests/references/intersecting_planes_result_verts.txt | 2 +- tests/references/text_viewer_out.txt | 4 ++-- tests/simple_generator_tests.py | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/references/intersecting_planes_result_faces.txt b/tests/references/intersecting_planes_result_faces.txt index 096e5bca8..0cf6a5bb8 100644 --- a/tests/references/intersecting_planes_result_faces.txt +++ b/tests/references/intersecting_planes_result_faces.txt @@ -1 +1 @@ -[[[0, 1], [1, 2], [4, 5], [6, 7], [8, 9], [10, 11], [13, 14], [14, 15], [0, 4], [4, 8], [8, 12], [1, 5], [5, 9], [6, 10], [10, 14], [3, 7], [7, 11], [11, 15], [16, 17], [17, 18], [18, 19], [20, 21], [21, 22], [25, 26], [26, 27], [28, 29], [29, 30], [30, 31], [16, 20], [20, 24], [17, 21], [25, 29], [18, 22], [26, 30], [23, 27], [27, 31], [2, 32], [32, 3], [5, 33], [33, 6], [24, 34], [34, 28], [9, 35], [35, 10], [21, 35], [35, 25], [12, 34], [34, 13], [22, 33], [33, 26], [19, 32], [32, 23], [9, 36], [36, 13], [2, 37], [37, 6], [22, 37], [37, 23], [24, 36], [36, 25]]] \ No newline at end of file +[[[25, 26], [28, 29], [31, 32], [0, 1], [7, 8], [0, 3], [3, 5], [2, 4], [4, 6], [6, 8], [9, 10], [10, 11], [11, 12], [13, 14], [17, 18], [18, 19], [9, 13], [10, 14], [15, 17], [16, 19], [20, 34], [34, 2], [21, 32], [32, 22], [23, 29], [29, 24], [26, 27], [14, 26], [26, 15], [29, 30], [32, 33], [12, 34], [34, 36], [25, 37], [37, 30], [20, 35], [35, 22], [31, 35], [35, 36], [23, 37], [37, 15], [1, 20], [3, 21], [22, 4], [5, 25], [27, 6], [30, 7], [5, 28], [1, 21], [21, 25], [22, 27], [27, 7], [14, 31], [15, 33], [33, 16], [24, 17], [13, 23], [11, 31], [33, 18], [36, 16]]] \ No newline at end of file diff --git a/tests/references/intersecting_planes_result_verts.txt b/tests/references/intersecting_planes_result_verts.txt index 39bf04be6..aeea91907 100644 --- a/tests/references/intersecting_planes_result_verts.txt +++ b/tests/references/intersecting_planes_result_verts.txt @@ -1 +1 @@ -[[(-5.0, -3.8302221298217773, -3.213937759399414), (-1.6666666269302368, -3.8302221298217773, -3.213937759399414), (1.6666666269302368, -3.8302221298217773, -3.213937759399414), (5.0, -3.8302221298217773, -3.213937759399414), (-5.0, -1.2767406702041626, -1.0713125467300415), (-1.6666666269302368, -1.2767406702041626, -1.0713125467300415), (1.6666666269302368, -1.2767406702041626, -1.0713125467300415), (5.0, -1.2767406702041626, -1.0713125467300415), (-5.0, 1.2767406702041626, 1.0713125467300415), (-1.6666666269302368, 1.2767406702041626, 1.0713125467300415), (1.6666666269302368, 1.2767406702041626, 1.0713125467300415), (5.0, 1.2767406702041626, 1.0713125467300415), (-5.0, 3.8302221298217773, 3.213937759399414), (-1.6666666269302368, 3.8302221298217773, 3.213937759399414), (1.6666666269302368, 3.8302221298217773, 3.213937759399414), (5.0, 3.8302221298217773, 3.213937759399414), (-3.8302221298217773, -5.0, 3.213937759399414), (-1.2767406702041626, -5.0, 1.0713125467300415), (1.2767406702041626, -5.0, -1.0713125467300415), (3.8302221298217773, -5.0, -3.213937759399414), (-3.8302221298217773, -1.6666666269302368, 3.213937759399414), (-1.2767406702041626, -1.6666666269302368, 1.0713125467300415), (1.2767406702041626, -1.6666666269302368, -1.0713125467300415), (3.8302221298217773, -1.6666666269302368, -3.213937759399414), (-3.8302221298217773, 1.6666666269302368, 3.213937759399414), (-1.2767406702041626, 1.6666666269302368, 1.0713125467300415), (1.2767406702041626, 1.6666666269302368, -1.0713125467300415), (3.8302221298217773, 1.6666666269302368, -3.213937759399414), (-3.8302221298217773, 5.0, 3.213937759399414), (-1.2767406702041626, 5.0, 1.0713125467300415), (1.2767406702041626, 5.0, -1.0713125467300415), (3.8302221298217773, 5.0, -3.213937759399414), (3.8302221298217773, -3.8302221298217773, -3.213937759399414), (1.2767406702041626, -1.2767406702041626, -1.0713125467300415), (-3.8302221298217773, 3.8302221298217773, 3.213937759399414), (-1.2767406702041626, 1.2767406702041626, 1.0713125467300415), (-1.6666666269302368, 1.6666666269302368, 1.3984992504119873), (1.6666666269302368, -1.6666665077209473, -1.3984991312026978)]] \ No newline at end of file +[[(-5.0, -3.8302221298217773, -3.213937759399414), (-1.6666666269302368, -3.8302221298217773, -3.213937759399414), (5.0, -3.8302221298217773, -3.213937759399414), (-5.0, -1.2767406702041626, -1.0713125467300415), (5.0, -1.2767406702041626, -1.0713125467300415), (-5.0, 1.2767406702041626, 1.0713125467300415), (5.0, 1.2767406702041626, 1.0713125467300415), (1.6666666269302368, 3.8302221298217773, 3.213937759399414), (5.0, 3.8302221298217773, 3.213937759399414), (-3.8302221298217773, -5.0, 3.213937759399414), (-1.2767406702041626, -5.0, 1.0713125467300415), (1.2767406702041626, -5.0, -1.0713125467300415), (3.8302221298217773, -5.0, -3.213937759399414), (-3.8302221298217773, -1.6666666269302368, 3.213937759399414), (-1.2767406702041626, -1.6666666269302368, 1.0713125467300415), (-1.2767406702041626, 1.6666666269302368, 1.0713125467300415), (3.8302221298217773, 1.6666666269302368, -3.213937759399414), (-1.2767406702041626, 5.0, 1.0713125467300415), (1.2767406702041626, 5.0, -1.0713125467300415), (3.8302221298217773, 5.0, -3.213937759399414), (1.6666666269302368, -3.8302221298217773, -3.213937759399414), (-1.6666666269302368, -1.2767406702041626, -1.0713125467300415), (1.6666666269302368, -1.2767406702041626, -1.0713125467300415), (-3.8302221298217773, 1.6666666269302368, 3.213937759399414), (-3.8302221298217773, 5.0, 3.213937759399414), (-1.6666666269302368, 1.2767406702041626, 1.0713125467300415), (-1.2767406702041626, 1.2767406702041626, 1.0713125467300415), (1.6666666269302368, 1.2767406702041626, 1.0713125467300415), (-5.0, 3.8302221298217773, 3.213937759399414), (-3.8302221298217773, 3.8302221298217773, 3.213937759399414), (-1.6666666269302368, 3.8302221298217773, 3.213937759399414), (1.2767406702041626, -1.6666666269302368, -1.0713125467300415), (1.2767406702041626, -1.2767406702041626, -1.0713125467300415), (1.2767406702041626, 1.6666666269302368, -1.0713125467300415), (3.8302221298217773, -3.8302221298217773, -3.213937759399414), (1.6666666269302368, -1.6666665077209473, -1.3984991312026978), (3.8302221298217773, -1.6666666269302368, -3.213937759399414), (-1.6666666269302368, 1.6666666269302368, 1.3984992504119873)]] \ No newline at end of file diff --git a/tests/references/text_viewer_out.txt b/tests/references/text_viewer_out.txt index 9895f2f35..af992c242 100644 --- a/tests/references/text_viewer_out.txt +++ b/tests/references/text_viewer_out.txt @@ -8,7 +8,7 @@ Socket DATA0; type VERTICES: [0.9807852804032304, 0.19509032201612825, 0.0] [0.9238795325112867, 0.3826834323650898, 0.0] [0.8314696123025452, 0.5555702330196022, 0.0] -[0.7071067811865476, 0.7071067811865475, 0.0] +[0.7071067811865476, 0.7071067811865476, 0.0] [0.5555702330196023, 0.8314696123025452, 0.0] [0.38268343236508984, 0.9238795325112867, 0.0] [0.19509032201612833, 0.9807852804032304, 0.0] @@ -41,7 +41,7 @@ Socket DATA0; type VERTICES: [0.9807852804032304, 0.19509032201612825, 2.0] [0.9238795325112867, 0.3826834323650898, 2.0] [0.8314696123025452, 0.5555702330196022, 2.0] -[0.7071067811865476, 0.7071067811865475, 2.0] +[0.7071067811865476, 0.7071067811865476, 2.0] [0.5555702330196023, 0.8314696123025452, 2.0] [0.38268343236508984, 0.9238795325112867, 2.0] [0.19509032201612833, 0.9807852804032304, 2.0] diff --git a/tests/simple_generator_tests.py b/tests/simple_generator_tests.py index 3c8e4e42e..320ab112e 100644 --- a/tests/simple_generator_tests.py +++ b/tests/simple_generator_tests.py @@ -47,6 +47,6 @@ class NGonNodeTest(NodeProcessTestCase): #data = self.get_output_data("Vertices") #info("NGon: %s", data) - expected_data = [[(1.0, 0.0, 0), (0.30901699437494745, 0.9510565162951535, 0), (-0.8090169943749473, 0.5877852522924732, 0), (-0.8090169943749476, -0.587785252292473, 0), (0.30901699437494723, -0.9510565162951536, 0)]] + expected_data = [[(1.0, 0.0, 0), (0.30901699437494745, 0.9510565162951535, 0), (-0.8090169943749473, 0.5877852522924732, 0), (-0.8090169943749475, -0.587785252292473, 0), (0.30901699437494723, -0.9510565162951536, 0)]] self.assert_output_data_equals("Vertices", expected_data) -- GitLab From abb13a08d17af41f35a5b1b401eac2cea12d006d Mon Sep 17 00:00:00 2001 From: Victor Doval <10011941+vicdoval@users.noreply.github.com> Date: Fri, 26 Apr 2019 13:14:45 +0200 Subject: [PATCH 097/137] text viewer test fixing --- tests/references/text_viewer_out.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/references/text_viewer_out.txt b/tests/references/text_viewer_out.txt index af992c242..9895f2f35 100644 --- a/tests/references/text_viewer_out.txt +++ b/tests/references/text_viewer_out.txt @@ -8,7 +8,7 @@ Socket DATA0; type VERTICES: [0.9807852804032304, 0.19509032201612825, 0.0] [0.9238795325112867, 0.3826834323650898, 0.0] [0.8314696123025452, 0.5555702330196022, 0.0] -[0.7071067811865476, 0.7071067811865476, 0.0] +[0.7071067811865476, 0.7071067811865475, 0.0] [0.5555702330196023, 0.8314696123025452, 0.0] [0.38268343236508984, 0.9238795325112867, 0.0] [0.19509032201612833, 0.9807852804032304, 0.0] @@ -41,7 +41,7 @@ Socket DATA0; type VERTICES: [0.9807852804032304, 0.19509032201612825, 2.0] [0.9238795325112867, 0.3826834323650898, 2.0] [0.8314696123025452, 0.5555702330196022, 2.0] -[0.7071067811865476, 0.7071067811865476, 2.0] +[0.7071067811865476, 0.7071067811865475, 2.0] [0.5555702330196023, 0.8314696123025452, 2.0] [0.38268343236508984, 0.9238795325112867, 2.0] [0.19509032201612833, 0.9807852804032304, 2.0] -- GitLab From d568a74323cc8c00a0f22448a1b99b826c4aaa4d Mon Sep 17 00:00:00 2001 From: Victor Doval <10011941+vicdoval@users.noreply.github.com> Date: Fri, 26 Apr 2019 13:51:25 +0200 Subject: [PATCH 098/137] test ngon test fixing --- tests/simple_generator_tests.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/simple_generator_tests.py b/tests/simple_generator_tests.py index 320ab112e..3c8e4e42e 100644 --- a/tests/simple_generator_tests.py +++ b/tests/simple_generator_tests.py @@ -47,6 +47,6 @@ class NGonNodeTest(NodeProcessTestCase): #data = self.get_output_data("Vertices") #info("NGon: %s", data) - expected_data = [[(1.0, 0.0, 0), (0.30901699437494745, 0.9510565162951535, 0), (-0.8090169943749473, 0.5877852522924732, 0), (-0.8090169943749475, -0.587785252292473, 0), (0.30901699437494723, -0.9510565162951536, 0)]] + expected_data = [[(1.0, 0.0, 0), (0.30901699437494745, 0.9510565162951535, 0), (-0.8090169943749473, 0.5877852522924732, 0), (-0.8090169943749476, -0.587785252292473, 0), (0.30901699437494723, -0.9510565162951536, 0)]] self.assert_output_data_equals("Vertices", expected_data) -- GitLab From 61921c49b50bf864b35f96108c0c992cb1136d1e Mon Sep 17 00:00:00 2001 From: Kosvor2 Date: Mon, 29 Apr 2019 21:49:58 +0500 Subject: [PATCH 099/137] Update socket_data.py --- core/socket_data.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/socket_data.py b/core/socket_data.py index 55f333c27..92cba4272 100644 --- a/core/socket_data.py +++ b/core/socket_data.py @@ -63,7 +63,7 @@ def SvGetSocketInfo(socket): if s_id in socket_data_cache[ng]: data = socket_data_cache[ng][s_id] if data: - return "("+str(len(data)) + " obj)" + return str(len(data)) return '' -- GitLab From 3231a479ec84e431ae9f919356144bcac2cc86c0 Mon Sep 17 00:00:00 2001 From: Victor Doval <10011941+vicdoval@users.noreply.github.com> Date: Wed, 1 May 2019 23:36:07 +0200 Subject: [PATCH 100/137] new linked verts node --- docs/nodes/analyzers/linked_verts.rst | 36 ++++++ index.md | 1 + nodes/analyzer/linked_verts.py | 174 ++++++++++++++++++++++++++ 3 files changed, 211 insertions(+) create mode 100644 docs/nodes/analyzers/linked_verts.rst create mode 100644 nodes/analyzer/linked_verts.py diff --git a/docs/nodes/analyzers/linked_verts.rst b/docs/nodes/analyzers/linked_verts.rst new file mode 100644 index 000000000..5f5a92bef --- /dev/null +++ b/docs/nodes/analyzers/linked_verts.rst @@ -0,0 +1,36 @@ +Linked Verts +===== + +Functionality +------------- + +Area node is one of the analyzer type. It is used the vertices that are connected to each vertex. + +Inputs +------ + +**Vertices**: Vertex list (optional) +**Edges**: Edge data. +**Selection**: Desired index list or mask list. +**Distance**: Distance to input (measured in number of edges in between) + +Parameters +---------- + +**Selection Type**: choose if you input a index list or as mask list in the **Selection** input + + +Outputs +------- + +**Verts Id**: Index of the linked vertices, refering to the **Vertices** input list +**Verts**: Linked verts list. +**Mask**: mask of the linked vertices, refering to the **Vertices** input list + + +Example of usage +---------------- + +.. image:: https://cloud.githubusercontent.com/assets/5990821/4188452/8f9cbf66-3772-11e4-8735-34462b7da54b.png + :alt: AreaDemo1.PNG + diff --git a/index.md b/index.md index d4256e419..281c8d866 100644 --- a/index.md +++ b/index.md @@ -69,6 +69,7 @@ SvOBJInsolationNode EvaluateImageNode SvDeformationNode + SvLinkedVertsNode ## Transforms SvRotationNode diff --git a/nodes/analyzer/linked_verts.py b/nodes/analyzer/linked_verts.py new file mode 100644 index 000000000..8325801ea --- /dev/null +++ b/nodes/analyzer/linked_verts.py @@ -0,0 +1,174 @@ +# ##### 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, EnumProperty +from sverchok.node_tree import SverchCustomTreeNode +from sverchok.data_structure import updateNode, match_long_repeat + + +def all_connexions(edges, item_n, distance, connexions_dict, distance_max): + '''iterative calculation of connexions''' + connexions_list = [] + edges_out = [] + for e in edges: + edge_in = True + for s in e: + if s in item_n: + connexions_list += e + edge_in = False + break + if edge_in: + edges_out.append(e) + connexions_list_clean = list(set(connexions_list) - set(item_n)) + connexions_dict[distance_max - distance + 1] = connexions_list_clean + + if distance > 1: + connexions_list_clean = all_connexions(edges_out, connexions_list_clean, distance - 1, connexions_dict, distance_max) + + return connexions_list_clean + + +def calc_connexions(meshes, gates, result): + '''calculate connexions by creating sets''' + + for vertices, edges, item_n, distance in zip(*meshes): + if gates[3]: + item_n = [i for i, m in enumerate(item_n) if m] + print(item_n) + connexions_dict = {} + connexions_dict[0] = item_n + d_max = max(distance) + _ = all_connexions(edges, item_n, d_max, connexions_dict, d_max) + + out_index = [] + for d in distance: + if d in connexions_dict: + out_index += connexions_dict[d] + + out_index = list(set(out_index)) + + result[0].append(out_index) + + if gates[1]: + connected_v = [] + for i in out_index: + connected_v.append(vertices[i]) + result[1].append(connected_v) + + if gates[2]: + mask = [True if i in out_index else False for i in range(len(vertices))] + result[2].append(mask) + + return result + + +class SvLinkedVertsNode(bpy.types.Node, SverchCustomTreeNode): + ''' + Triggers: Measure deformation + Tooltip: Deformation between to states, edge elong a area variation + ''' + bl_idname = 'SvLinkedVertsNode' + bl_label = 'Linked Verts' + bl_icon = 'MOD_SIMPLEDEFORM' + + selection_type_Items = [ + ("Index", "Index", "Input selected Indexes", 0), + ("Mask", "Mask", "Input selection though mask", 1)] + + item = IntProperty( + name='Selection', description='Selected Items (Index or Mask)', + default=0, update=updateNode) + distance = IntProperty( + name='distance', description='Include subject vertex', + default=1, min=0, update=updateNode) + selection_type = EnumProperty( + name='Selection Mode', description='Input selection type', + items=selection_type_Items, + default="Index", update=updateNode) + + def draw_buttons(self, context, layout): + '''draw buttons on the Node''' + layout.prop(self, "selection_type", expand=False) + + def sv_init(self, context): + '''create sockets''' + sinw = self.inputs.new + sonw = self.outputs.new + sinw('VerticesSocket', "Verts") + sinw('StringsSocket', "Edges") + sinw('StringsSocket', "Item").prop_name = 'item' + sinw('StringsSocket', "Distance").prop_name = 'distance' + + sonw('StringsSocket', "Verts Id") + sonw('VerticesSocket', "Verts") + sonw('StringsSocket', "Mask") + + def get_data(self): + '''get all data from sockets''' + si = self.inputs + vertices_s = si['Verts'].sv_get(default=[[]]) + edges_in = si['Edges'].sv_get(default=[[]]) + item_n = si['Item'].sv_get(default=[[]]) + distance = si['Distance'].sv_get(default=[[]]) + + return match_long_repeat([vertices_s, edges_in, item_n, distance]) + + 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[1].is_linked + return ready + + def process(self): + '''main node function called every update''' + so = self.outputs + si = self.inputs + if not self.ready(): + return + + result = [[], [], [], []] + gates = [] + gates.append(so['Verts Id'].is_linked) + gates.append(so['Verts'].is_linked) + gates.append(so['Mask'].is_linked) + gates.append(self.selection_type == "Mask") + + meshes = self.get_data() + + result = calc_connexions(meshes, gates, result) + + if gates[0]: + so['Verts Id'].sv_set(result[0]) + if gates[1]: + so['Verts'].sv_set(result[1]) + if gates[2]: + so['Mask'].sv_set(result[2]) + + +def register(): + '''register class in Blender''' + bpy.utils.register_class(SvLinkedVertsNode) + + +def unregister(): + '''unregister class in Blender''' + bpy.utils.unregister_class(SvLinkedVertsNode) -- GitLab From d53a7dc92f05cc27cf81be783931f9e90e16de0b Mon Sep 17 00:00:00 2001 From: Victor Doval <10011941+vicdoval@users.noreply.github.com> Date: Mon, 6 May 2019 18:15:59 +0200 Subject: [PATCH 101/137] Linked verts node docs examples --- docs/nodes/analyzers/linked_verts.rst | 36 ++++++++++++++++++--------- 1 file changed, 24 insertions(+), 12 deletions(-) diff --git a/docs/nodes/analyzers/linked_verts.rst b/docs/nodes/analyzers/linked_verts.rst index 5f5a92bef..214663704 100644 --- a/docs/nodes/analyzers/linked_verts.rst +++ b/docs/nodes/analyzers/linked_verts.rst @@ -1,36 +1,48 @@ Linked Verts -===== +============ Functionality ------------- -Area node is one of the analyzer type. It is used the vertices that are connected to each vertex. +Linked Verts node is one of the analyzer type. It is used to get the vertices that are connected to a vertex. It can also get the vertices that are separated by N edges allowing the creation of complex selection patterns. Inputs ------ -**Vertices**: Vertex list (optional) -**Edges**: Edge data. -**Selection**: Desired index list or mask list. -**Distance**: Distance to input (measured in number of edges in between) +- **Vertices**: Vertex list (optional). +- **Edges**: Edge data. +- **Selection**: Desired index list or mask list. +- **Distance**: Distance to input (measured in number of edges in between). Parameters ---------- -**Selection Type**: choose if you input a index list or as mask list in the **Selection** input +**Selection Type**: choose if you input a index list or a mask list in the **Selection** input Outputs ------- -**Verts Id**: Index of the linked vertices, refering to the **Vertices** input list -**Verts**: Linked verts list. -**Mask**: mask of the linked vertices, refering to the **Vertices** input list +- **Verts Id**: Index of the linked vertices, referring to the **Vertices** input list. +- **Verts**: Linked verts list. +- **Mask**: mask of the linked vertices, referring to the **Vertices** input list. Example of usage ---------------- -.. image:: https://cloud.githubusercontent.com/assets/5990821/4188452/8f9cbf66-3772-11e4-8735-34462b7da54b.png - :alt: AreaDemo1.PNG +In this example you get the white vertex when asking for the vertex that are connected to the green dot. +.. image:: https://user-images.githubusercontent.com/10011941/57044482-fd173300-6c6a-11e9-9d74-f6e78b20c934.png + :alt: Linked Verts.PNG + +Using a range of integers as Distance input will expand the selection or the creation of patterns. + +.. image:: https://user-images.githubusercontent.com/10011941/57044498-030d1400-6c6b-11e9-8059-8319227c4df1.png + :alt: Linked Verts.PNG + +.. image:: https://user-images.githubusercontent.com/10011941/57044502-06a09b00-6c6b-11e9-83fb-ef3873a703f6.png + :alt: Linked Verts.PNG + +.. image:: https://user-images.githubusercontent.com/10011941/57044827-25ebf800-6c6c-11e9-8537-b65948cf158e.png + :alt: Linked Verts.PNG \ No newline at end of file -- GitLab From 9e6abea4824ee2e7f7574e44fe40fe73bdca17c8 Mon Sep 17 00:00:00 2001 From: Victor Doval <10011941+vicdoval@users.noreply.github.com> Date: Sun, 12 May 2019 13:05:52 +0200 Subject: [PATCH 102/137] New Regular Solid Node (#2413) * New Regular Solid Node * New Regular Solid Node Docs Examples and TypoFix * Regular Solid Node TypoFix --- .../generators_extended/regular_solid.rst | 56 ++++ index.md | 1 + nodes/generators_extended/regular_solid.py | 273 ++++++++++++++++++ 3 files changed, 330 insertions(+) create mode 100644 docs/nodes/generators_extended/regular_solid.rst create mode 100644 nodes/generators_extended/regular_solid.py diff --git a/docs/nodes/generators_extended/regular_solid.rst b/docs/nodes/generators_extended/regular_solid.rst new file mode 100644 index 000000000..54149752f --- /dev/null +++ b/docs/nodes/generators_extended/regular_solid.rst @@ -0,0 +1,56 @@ +Regular Solid +============= + +Functionality +------------- + +This node is a port to the Regular Solid functions (by dreampainter) now part of the Extra Objects Add-on bundled with Blender +(https://archive.blender.org/wiki/index.php/Extensions:2.6/Py/Scripts/Add_Mesh/Add_Solid/) + +It creates Platonic, Archimedean or Catalan solids + +Inputs & Parameters +------------------- + ++-------------------+-----------------------------------------------+ +| Name | Descriptor | ++===================+===============================================+ +| Preset | Parameters for some hard names | ++-------------------+-----------------------------------------------+ +| Source | Starting point of your solid | ++-------------------+-----------------------------------------------+ +| Snub | Create the snub version | ++-------------------+-----------------------------------------------+ +| Dual | Create the dual of the current solid | ++-------------------+-----------------------------------------------+ +| Keep Size | Keep the whole solid at a constant size | ++-------------------+-----------------------------------------------+ +| Size | Radius of the sphere through the vertices | ++-------------------+-----------------------------------------------+ +| Vertex Truncation | Amount of vertex truncation | ++-------------------+-----------------------------------------------+ +| Edge Truncation | Amount of edge truncation | ++-------------------+-----------------------------------------------+ + +When list are used as input many solids will be created + + +Outputs +------- + +Vertices, Edges and Polygons of the mesh or meshes. + + +Examples +-------- +Some of the shapes that can be generated: + +.. image:: https://user-images.githubusercontent.com/10011941/57547653-3896b900-735f-11e9-8186-8e9491655bf4.png + :alt: Regular_Solid_.png + +Variations of the cube: + +.. image:: https://user-images.githubusercontent.com/10011941/57547665-3df40380-735f-11e9-9112-ea8f2f4bc202.png + :alt: Regular_Solid.png + + diff --git a/index.md b/index.md index 281c8d866..29d760803 100644 --- a/index.md +++ b/index.md @@ -45,6 +45,7 @@ SvEllipseNode SvSuperEllipsoidNode SvSmoothLines + SvRegularSolid ## Analyzers SvBBoxNode diff --git a/nodes/generators_extended/regular_solid.py b/nodes/generators_extended/regular_solid.py new file mode 100644 index 000000000..d8ecc5c97 --- /dev/null +++ b/nodes/generators_extended/regular_solid.py @@ -0,0 +1,273 @@ +# ##### 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 add_mesh_extra_objects.add_mesh_solid import createSolid +from mathutils import Vector +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 +from sverchok.nodes.modifier_change.polygons_to_edges import pols_edges +## This node is a port to the add_mesh_extra_objects.add_mesh_solid createSolid + +class SvRegularSolid(bpy.types.Node, SverchCustomTreeNode): + """ + Triggers: Platonic, Archimedean or Catalan solids. + Tooltip: Add one of the Platonic, Archimedean or Catalan solids. + """ + bl_idname = 'SvRegularSolid' + bl_label = 'Regular Solid' + bl_icon = 'GRIP' + + source = EnumProperty( + items=(("4", "Tetrahedron", ""), + ("6", "Hexahedron", ""), + ("8", "Octahedron", ""), + ("12", "Dodecahedron", ""), + ("20", "Icosahedron", "")), + name="Source", + description="Starting point of your solid", + update=updateNode + ) + size = FloatProperty( + name="Size", + description="Radius of the sphere through the vertices", + min=0.01, + soft_min=0.01, + max=100, + soft_max=100, + default=1.0, + update=updateNode + ) + vTrunc = FloatProperty( + name="Vertex Truncation", + description="Amount of vertex truncation", + min=0.0, + soft_min=0.0, + max=2.0, + soft_max=2.0, + default=0.0, + precision=3, + step=0.5, + update=updateNode + ) + eTrunc = FloatProperty( + name="Edge Truncation", + description="Amount of edge truncation", + min=0.0, + soft_min=0.0, + max=1.0, + soft_max=1.0, + default=0.0, + precision=3, + step=0.2, + update=updateNode + ) + snub = EnumProperty( + items=(("None", "No Snub", ""), + ("Left", "Left Snub", ""), + ("Right", "Right Snub", "")), + name="Snub", + description="Create the snub version", + update=updateNode + ) + dual = BoolProperty( + name="Dual", + description="Create the dual of the current solid", + default=False, + update=updateNode + ) + keepSize = BoolProperty( + name="Keep Size", + description="Keep the whole solid at a constant size", + default=False, + update=updateNode + ) + def updatePreset(self, context): + if self.preset != "0": + # if preset, set preset + if self.previousSetting != self.preset: + using = self.p[self.preset] + self.source = using[0] + self.vTrunc = using[1] + self.eTrunc = using[2] + self.dual = using[3] + self.snub = using[4] + else: + using = self.p[self.preset] + result0 = self.source == using[0] + result1 = abs(self.vTrunc - using[1]) < 0.004 + result2 = abs(self.eTrunc - using[2]) < 0.0015 + result4 = using[4] == self.snub or ((using[4] == "Left") and + self.snub in ["Left", "Right"]) + if (result0 and result1 and result2 and result4): + if self.p[self.previousSetting][3] != self.dual: + if self.preset[0] == "d": + self.preset = self.preset[1:] + else: + self.preset = "d" + self.preset + else: + self.preset = "0" + updateNode(self, context) + + + preset = EnumProperty( + items=(("0", "Custom", ""), + ("t4", "Truncated Tetrahedron", ""), + ("r4", "Cuboctahedron", ""), + ("t6", "Truncated Cube", ""), + ("t8", "Truncated Octahedron", ""), + ("b6", "Rhombicuboctahedron", ""), + ("c6", "Truncated Cuboctahedron", ""), + ("s6", "Snub Cube", ""), + ("r12", "Icosidodecahedron", ""), + ("t12", "Truncated Dodecahedron", ""), + ("t20", "Truncated Icosahedron", ""), + ("b12", "Rhombicosidodecahedron", ""), + ("c12", "Truncated Icosidodecahedron", ""), + ("s12", "Snub Dodecahedron", ""), + ("dt4", "Triakis Tetrahedron", ""), + ("dr4", "Rhombic Dodecahedron", ""), + ("dt6", "Triakis Octahedron", ""), + ("dt8", "Tetrakis Hexahedron", ""), + ("db6", "Deltoidal Icositetrahedron", ""), + ("dc6", "Disdyakis Dodecahedron", ""), + ("ds6", "Pentagonal Icositetrahedron", ""), + ("dr12", "Rhombic Triacontahedron", ""), + ("dt12", "Triakis Icosahedron", ""), + ("dt20", "Pentakis Dodecahedron", ""), + ("db12", "Deltoidal Hexecontahedron", ""), + ("dc12", "Disdyakis Triacontahedron", ""), + ("ds12", "Pentagonal Hexecontahedron", "")), + name="Presets", + description="Parameters for some hard names", + update=updatePreset + ) + + # actual preset values + p = {"t4": ["4", 2 / 3, 0, 0, "None"], + "r4": ["4", 1, 1, 0, "None"], + "t6": ["6", 2 / 3, 0, 0, "None"], + "t8": ["8", 2 / 3, 0, 0, "None"], + "b6": ["6", 1.0938, 1, 0, "None"], + "c6": ["6", 1.0572, 0.585786, 0, "None"], + "s6": ["6", 1.0875, 0.704, 0, "Left"], + "r12": ["12", 1, 0, 0, "None"], + "t12": ["12", 2 / 3, 0, 0, "None"], + "t20": ["20", 2 / 3, 0, 0, "None"], + "b12": ["12", 1.1338, 1, 0, "None"], + "c12": ["20", 0.921, 0.553, 0, "None"], + "s12": ["12", 1.1235, 0.68, 0, "Left"], + "dt4": ["4", 2 / 3, 0, 1, "None"], + "dr4": ["4", 1, 1, 1, "None"], + "dt6": ["6", 2 / 3, 0, 1, "None"], + "dt8": ["8", 2 / 3, 0, 1, "None"], + "db6": ["6", 1.0938, 1, 1, "None"], + "dc6": ["6", 1.0572, 0.585786, 1, "None"], + "ds6": ["6", 1.0875, 0.704, 1, "Left"], + "dr12": ["12", 1, 0, 1, "None"], + "dt12": ["12", 2 / 3, 0, 1, "None"], + "dt20": ["20", 2 / 3, 0, 1, "None"], + "db12": ["12", 1.1338, 1, 1, "None"], + "dc12": ["20", 0.921, 0.553, 1, "None"], + "ds12": ["12", 1.1235, 0.68, 1, "Left"]} + + # previous preset, for User-friendly reasons + previousSetting = "" + + def sv_init(self, context): + si = self.inputs + si.new('StringsSocket', "size").prop_name = 'size' + si.new('StringsSocket', "vTrunc").prop_name = 'vTrunc' + si.new('StringsSocket', "eTrunc").prop_name = 'eTrunc' + + self.outputs.new('VerticesSocket', "Vertices") + self.outputs.new('StringsSocket', "Edges") + self.outputs.new('StringsSocket', "Polygons") + + def draw_buttons(self, context, layout): + col = layout.column(align=False) + col.prop(self, "preset", expand=False) + col.prop(self, "source", expand=False) + col.prop(self, "snub", expand=False) + row = layout.row(align=True) + row.prop(self, "dual", toggle=True) + row.prop(self, "keepSize", toggle=True) + + + def get_data(self): + + size = self.inputs["size"].sv_get() + vTrunc = self.inputs["vTrunc"].sv_get() + eTrunc = self.inputs["eTrunc"].sv_get() + params = [size, vTrunc, eTrunc] + return match_long_repeat(params) + + + + def process(self): + if not any(s.is_linked for s in self.outputs): + return + + + verts_out, edges_out, polys_out = [], [], [] + + params = self.get_data() + get_edges = self.outputs['Edges'].is_linked + + for p in zip(*params): + p = match_long_repeat(p) + for p2 in zip(*p): + size, vTrunc, eTrunc = p2 + + verts, faces = createSolid(self.source, + vTrunc, + eTrunc, + self.dual, + self.snub + ) + + # resize to normal size, or if keepSize, make sure all verts are of length 'size' + if self.keepSize: + rad = size / verts[-1 if self.dual else 0].length + else: + rad = size + verts = [list(i * rad) for i in verts] + + verts_out.append(verts) + polys_out.append(faces) + if get_edges: + edges_out.append(pols_edges([faces], unique_edges=True)[0]) + + + if self.outputs['Vertices'].is_linked: + self.outputs['Vertices'].sv_set(verts_out) + if get_edges: + self.outputs['Edges'].sv_set(edges_out) + if self.outputs['Polygons'].is_linked: + self.outputs['Polygons'].sv_set(polys_out) + + +def register(): + bpy.utils.register_class(SvRegularSolid) + + +def unregister(): + bpy.utils.unregister_class(SvRegularSolid) -- GitLab From 7e225b7e3182188702339185ca933306be8aded1 Mon Sep 17 00:00:00 2001 From: Durman Date: Fri, 17 May 2019 18:07:32 +0400 Subject: [PATCH 103/137] New node "project point to line" - first approach --- index.md | 1 + nodes/analyzer/project_point_to_line.py | 362 ++++++++++++++++++++++++ 2 files changed, 363 insertions(+) create mode 100644 nodes/analyzer/project_point_to_line.py diff --git a/index.md b/index.md index 076bba081..8957b6bab 100644 --- a/index.md +++ b/index.md @@ -357,3 +357,4 @@ SvQuaternionOutNode SvQuaternionInNode SvQuaternionMathNode + SvProjectPointToLine diff --git a/nodes/analyzer/project_point_to_line.py b/nodes/analyzer/project_point_to_line.py new file mode 100644 index 000000000..6a4b5806c --- /dev/null +++ b/nodes/analyzer/project_point_to_line.py @@ -0,0 +1,362 @@ +# ##### 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 numpy as np +from bisect import bisect +from functools import reduce + +import bpy +from bpy.props import IntProperty, EnumProperty, BoolProperty, FloatProperty +from mathutils import kdtree, Vector + +from sverchok.node_tree import SverchCustomTreeNode +from sverchok.data_structure import get_other_socket, updateNode, replace_socket, match_long_repeat +from sverchok.data_structure import match_long_repeat + +''' +Some explanation of the algorithm can be found here: +https://github.com/nortikin/sverchok/issues/2376 +''' + + +# _________useful functions_________ +def is_instance_of_vector(l): + # Check first nested bunch of coordinates is Vector class or not + def get_last_iterable_class(l): + if hasattr(l, '__getitem__'): + res = get_last_iterable_class(l[0]) + if isinstance(res, bool): + return type(l) + else: + return res + else: + return False + return get_last_iterable_class(l) == Vector + + +def calc_nestedness(l, deap=0): + if isinstance(l, (list, tuple)): + return calc_nestedness(l[0], deap + 1) + return deap + + +def flatten_list(l, a=None): + # make list of lists flatten + # https://github.com/nortikin/sverchok/issues/2304#issuecomment-438581948 + if a is None: + a = [] + add = a.append + + _ = [flatten_list(i, a) if isinstance(i, (list, tuple)) else add(i) for i in l] + return a + + +# functions related with main process +def creat_tree(line): + # Create KDTree for line + tree = kdtree.KDTree(len(line)) + [tree.insert(p, i) for i, p in enumerate(line)] + tree.balance() + return tree + + +def get_closerst_point(tree, points): + """ + Will find closest line's point to current points + :param tree: mathutils.kdtree.KDTree + :param points: [point 1, point 2, ...] + point - Vector or (x, y, z) + :return: indexes of closest points in KDTree to points - [int 1, int 2, ...] + len(points) == len(inds) + """ + data = [tree.find(point) for point in points] + cos, inds, dists = zip(*data) + return inds + + +def get_closest_point_to_edge(edge, point): + """ + Will find closest point on current edge + :param edge: [Vector 1, Vector 2] + :param point: Vector + :return project_point_inside_edge: Vector + :return coincidence: coincidence projected point with one of points of the edge - bool + """ + edge_vector = edge[1] - edge[0] + point = point - edge[0] + project_point = point.project(edge_vector) + scalar_mult = project_point.dot(edge_vector) + if scalar_mult < 0: + project_point_inside_edge = edge[0] + coincidence = True + elif project_point.length > edge_vector.length: + project_point_inside_edge = edge[1] + coincidence = True + else: + project_point_inside_edge = project_point + edge[0] + coincidence = False + return project_point_inside_edge, coincidence + + +def get_len_edges_line(line): + # Return lengths of line's edges + return [(v2 - v1).length for v1, v2 in zip(line[:-1], line[1:])] + + +def get_divvec_edge(edge, div_number): + """ + Return points on given edge created by subdivision + :param edge: [Vector 1, Vector 2] + :param div_number: on how many parts edge should be divided - int + :return: [Vector div_1, Vector div_2, ...] + """ + factors = np.linspace(0, 1, num=div_number + 2)[1:-1] + return [edge[0].lerp(edge[1], f) for f in factors] + + +def divide_line(line, resolution, len_edges): + """ + Divide line to smaller edges with equal length + :param line: [Vector 1, Vector 2, ...] + :param resolution: means maximum length of an edge. If edge is longer it should be divided - float + :param len_edges: distance between vector 1 and 2, vector 2 and 3 and so on - [float 1, float 2, ...] + len(line) - 1 == len(len_edges) + :return new_line: [Vector 1, Vector 1.1, Vector 1.2, Vector 2, Vector 2.1, ...] + :return edge_indexes: index of position of Vectors from old line on new line - [int1, int2, ...] + for example, if last vector of an edge has index 1 and this edge is divided on 3 parts new index of last Vector is 3 + """ + len_edges = len_edges if len_edges else get_len_edges_line(line) + new_line = [line[0]] + edge_indexes = [0] + indx = 1 + for (p1, p2), len_e in zip(zip(line[:-1], line[1:]), len_edges): + div_number = round(len_e / resolution) - 1 + if div_number > 0: + new_line.extend(get_divvec_edge((p1, p2), div_number)) + indx += div_number + + new_line.append(p2) + edge_indexes.append(indx) + indx += 1 + return new_line, edge_indexes + + +def preparing_lines(vectors_line, resolution=False): + # Subdivide and create KDTree for given lines + if not is_instance_of_vector(vectors_line): + vectors_line = [[Vector(co) for co in l] for l in vectors_line] + lens_edges = [get_len_edges_line(line) for line in vectors_line] + if resolution: + resolution = resolution if calc_nestedness(resolution) == 1 else flatten_list(resolution) + else: + resolution = [min(lens) for lens in lens_edges] + new_lines, edges_indexes = zip(*[divide_line(line, r, len_e) + for line, len_e, r in zip(*match_long_repeat([vectors_line, lens_edges, resolution]))]) + trees = [creat_tree(line) for line in new_lines] + return new_lines, edges_indexes, trees + + +def find_closest_index(edges_index, ind): + """ + find 3 closest indexes of points exist in initial line + :param edges_index: index of position of Vectors from old line on new line - [int1, int2, ...] + for example, if last vector of an edge has index 1 and this edge is divided on 3 parts new index of last Vector is 3 + :param ind: index of closest point on new line to project point - int + :return new_3ind: indexes of closest 3 points exist on old line that correspond to new line - [int1, int2, int3] + :return old_3ind: indexes of closest 3 points exist on old line that correspond to old line - [int1, int2, int3] + """ + i = bisect(edges_index, ind) + # print('edges -', edges_index) + # print('i -', i) + if i == len(edges_index): + new_3ind = edges_index[i - 3], edges_index[i - 2], edges_index[i - 1] + old_3ind = [i - 3, i - 2, i - 1] + elif i - 1 == 0: + new_3ind = edges_index[i - 1], edges_index[i], edges_index[i + 1] + old_3ind = [i - 1, i, i + 1] + else: + new_3ind = edges_index[i - 2], edges_index[i - 1], edges_index[i] + old_3ind = [i - 2, i - 1, i] + return new_3ind, old_3ind + + +def project_points(new_lines, edges_indexes, kdtrees_from_lines, pr_points): + """ + project points to prepared lines + :param new_lines: prepared lines - [[Vector 1, Vector 2, ...], [Vector 1, ...], ...] + :param edges_indexes: indexes of points of new line that exist on old line -[[int 1, int 2, ...], [int 1, ...], ...] + :param kdtrees_from_lines: KD trees prepared from new_lines - [KDTree 1, KDTree 2, ...] + :param pr_points: points that should be projected - [[Vector 1, Vector 2, ...], [Vector 1, ...], ...] + vector can be a list of 3 numbers + :return project_points: [[Vector 1, Vector 2, ...], [Vector 1, ...], ...] + :return old_indexes: index of point of old line that located before projected point -> + -> [[int 1, int 2, ...], [int 1, ...], ...] + :return coincidence: coincidence projected points with points of old line - [[False, True, ...], [False, ...], ...] + """ + if not is_instance_of_vector(new_lines): + vectors_line = [[Vector(co) for co in l] for l in new_lines] + if not is_instance_of_vector(pr_points): + pr_points = [[Vector(co) for co in l] for l in pr_points] + + clos_ind = [get_closerst_point(tree, points) for tree, points in zip(kdtrees_from_lines, pr_points)] + + project_points = [] + old_indexes = [] + coincidence = [] + for line, inds, points, edges_ind in zip(new_lines, clos_ind, pr_points, edges_indexes): + project_points_nest1 = [] + old_indexes_nest1 = [] + coincidence_nest1 = [] + for ind, point in zip(inds, points): + (new_i1, new_i2, new_i3), (old_i1, old_i2, old_i3) = find_closest_index(edges_ind, ind) + clos_p1, coincidence_p1 = get_closest_point_to_edge((line[new_i1], line[new_i2]), point) + clos_p2, coincidence_p2 = get_closest_point_to_edge((line[new_i2], line[new_i3]), point) + if clos_p1 - point < clos_p2 - point: + project_points_nest1.append(clos_p1) + old_indexes_nest1.append(old_i1) + coincidence_nest1.append(coincidence_p1) + else: + project_points_nest1.append(clos_p2) + old_indexes_nest1.append(old_i2) + coincidence_nest1.append(coincidence_p2) + project_points.append(project_points_nest1) + old_indexes.append(old_indexes_nest1) + coincidence.append(coincidence_nest1) + + return project_points, old_indexes, coincidence + + +def sort_points_along_line(line, points, position, replace): + # sort points in order how how they move from start of line to end + points, position, replace = zip(*sorted(zip(points, position, replace), key=lambda l: l[1])) + new_p, new_pos, new_rep = [], [], [] + for i in range(len(position)): + next_i = i + 1 + while len(position) >= next_i and position[i] == position[next_i]: + next_i += 1 + if next_i - i > 1: + distance = [(proj - line[position[i]]).length for proj in points[i:next_i]] + chunk_dist, chunk_p, chunck_pos, chunk_rep = zip(*sorted( + zip(distance, points, position, replace), key=lambda l: l[0])) + new_p.append(chunk_p) + new_pos.append(chunck_pos) + new_rep.append(chunk_rep) + else: + new_p.append(points[i]) + new_pos.append(position[i]) + new_rep.append(replace[i]) + return new_p, new_pos, new_rep + + +def integrate_points_to_line(line, points, position, replace): + """ + merge projected points with old line + :param line: points of old line - [Point 1, Point 2, ...] + :param points: projected points that should be merged - [Point 1, Point 2, ...] + :param position: indexes of points of old line after which projected points should be pasted - [int1, int2, ...] + :param replace: if projected points coincide with existing points of old line they should replace them -> + -> [False, True, False, ...] + order of lists (points, position and replace) should be related with each other + :return: + """ + points, position, replace = sort_points_along_line(line, points, position, replace) + last_pos = 0 + merged_line = [] + number_added_points = 0 # of input line + for i, p in enumerate(points): + if last_pos == position[i]: # position didn't not move + if replace[i]: + pass + else: + if last_pos <= number_added_points: # position didn't move first time + merged_line.append(line[last_pos:position[i]+1]) + number_added_points += position[i] + 1 - last_pos + else: # position moved + if replace[i]: + merged_line.append([line[last_pos:position[i]]]) + else: + merged_line = [line[position[0] + 1]] + + last_pos = position[i] + + +class SvProjectPointToLine(bpy.types.Node, SverchCustomTreeNode): + """ + Triggers: project point to line + Tooltip: project point to line + + You can do something + """ + bl_idname = 'SvProjectPointToLine' + bl_label = 'Project point to line' + bl_icon = 'MOD_SHRINKWRAP' + + def switch_res_mode(self, context): + if self.set_res: + self.inputs.new('StringsSocket', 'Resolution').prop_name = 'resolution' + else: + self.inputs.remove(self.inputs['Resolution']) + self.process_node(context) + + resolution = FloatProperty(name='resolution', + description='the less the more accurate', + default=1.0, + min=0.01, + step=10, + unit='LENGTH', + update=SverchCustomTreeNode.process_node) + + set_res = BoolProperty(name='Set resolution', + description='Add socket for setting resolution value', + default=False, + update=switch_res_mode) + + def sv_init(self, context): + self.inputs.new('VerticesSocket', 'Vectors_lines') + self.inputs.new('VerticesSocket', 'Project_points') + self.outputs.new('VerticesSocket', 'Points_projected') + self.outputs.new('StringsSocket', 'Belonging') + + def draw_buttons_ext(self, context, layout): + layout.row().prop(self, 'set_res', toggle=True) + + def process(self): + if not (self.inputs['Vectors_lines'].is_linked and self.inputs['Project_points'].is_linked): + return + + v_lines = self.inputs['Vectors_lines'].sv_get() + p_points = self.inputs['Project_points'].sv_get() + if self.set_res: + resn = self.inputs['Resolution'].sv_get() + else: + resn = False + + new_lines, edges_indexes, trees = preparing_lines(v_lines, resn) + projected_points, indexes, coincidence = project_points(new_lines, edges_indexes, trees, p_points) + + self.outputs['Points_projected'].sv_set([[v[:] for v in l] for l in projected_points]) + self.outputs['Belonging'].sv_set([[[i] if c else (i, i+1) for i, c in zip(ind, coin)] + for ind, coin in zip(indexes, coincidence)]) + + +def register(): + bpy.utils.register_class(SvProjectPointToLine) + + +def unregister(): + bpy.utils.unregister_class(SvProjectPointToLine) -- GitLab From 2473b5a80de35399061d6460fcc124b67b734e0b Mon Sep 17 00:00:00 2001 From: Durman Date: Fri, 17 May 2019 22:57:11 +0400 Subject: [PATCH 104/137] add "sorting mask" and improve "belonging" sockets --- nodes/analyzer/project_point_to_line.py | 128 ++++++++++++------------ 1 file changed, 63 insertions(+), 65 deletions(-) diff --git a/nodes/analyzer/project_point_to_line.py b/nodes/analyzer/project_point_to_line.py index 6a4b5806c..50737c524 100644 --- a/nodes/analyzer/project_point_to_line.py +++ b/nodes/analyzer/project_point_to_line.py @@ -103,13 +103,13 @@ def get_closest_point_to_edge(edge, point): scalar_mult = project_point.dot(edge_vector) if scalar_mult < 0: project_point_inside_edge = edge[0] - coincidence = True + coincidence = 1 elif project_point.length > edge_vector.length: project_point_inside_edge = edge[1] - coincidence = True + coincidence = 2 else: project_point_inside_edge = project_point + edge[0] - coincidence = False + coincidence = 0 return project_point_inside_edge, coincidence @@ -181,14 +181,12 @@ def find_closest_index(edges_index, ind): :return old_3ind: indexes of closest 3 points exist on old line that correspond to old line - [int1, int2, int3] """ i = bisect(edges_index, ind) - # print('edges -', edges_index) - # print('i -', i) if i == len(edges_index): - new_3ind = edges_index[i - 3], edges_index[i - 2], edges_index[i - 1] - old_3ind = [i - 3, i - 2, i - 1] + new_3ind = edges_index[i - 2], edges_index[i - 1], None + old_3ind = [i - 2, i - 1, None] elif i - 1 == 0: - new_3ind = edges_index[i - 1], edges_index[i], edges_index[i + 1] - old_3ind = [i - 1, i, i + 1] + new_3ind = None, edges_index[i - 1], edges_index[i] + old_3ind = [None, i - 1, i] else: new_3ind = edges_index[i - 2], edges_index[i - 1], edges_index[i] old_3ind = [i - 2, i - 1, i] @@ -224,16 +222,27 @@ def project_points(new_lines, edges_indexes, kdtrees_from_lines, pr_points): coincidence_nest1 = [] for ind, point in zip(inds, points): (new_i1, new_i2, new_i3), (old_i1, old_i2, old_i3) = find_closest_index(edges_ind, ind) - clos_p1, coincidence_p1 = get_closest_point_to_edge((line[new_i1], line[new_i2]), point) - clos_p2, coincidence_p2 = get_closest_point_to_edge((line[new_i2], line[new_i3]), point) - if clos_p1 - point < clos_p2 - point: + if new_i1 is None: + clos_p2, coincidence_start = get_closest_point_to_edge((line[new_i2], line[new_i3]), point) + project_points_nest1.append(clos_p2) + old_indexes_nest1.append(old_i2) + coincidence_nest1.append(coincidence_start) + elif new_i3 is None: + clos_p1, coincidence_end = get_closest_point_to_edge((line[new_i1], line[new_i2]), point) project_points_nest1.append(clos_p1) old_indexes_nest1.append(old_i1) - coincidence_nest1.append(coincidence_p1) + coincidence_nest1.append(coincidence_end) else: - project_points_nest1.append(clos_p2) - old_indexes_nest1.append(old_i2) - coincidence_nest1.append(coincidence_p2) + clos_p1, coincidence_p1 = get_closest_point_to_edge((line[new_i1], line[new_i2]), point) + clos_p2, coincidence_p2 = get_closest_point_to_edge((line[new_i2], line[new_i3]), point) + if clos_p1 - point < clos_p2 - point: + project_points_nest1.append(clos_p1) + old_indexes_nest1.append(old_i1) + coincidence_nest1.append(coincidence_p1) + else: + project_points_nest1.append(clos_p2) + old_indexes_nest1.append(old_i2) + coincidence_nest1.append(coincidence_p2) project_points.append(project_points_nest1) old_indexes.append(old_indexes_nest1) coincidence.append(coincidence_nest1) @@ -241,58 +250,42 @@ def project_points(new_lines, edges_indexes, kdtrees_from_lines, pr_points): return project_points, old_indexes, coincidence -def sort_points_along_line(line, points, position, replace): +def sort_points_along_line(line, points, position): # sort points in order how how they move from start of line to end - points, position, replace = zip(*sorted(zip(points, position, replace), key=lambda l: l[1])) - new_p, new_pos, new_rep = [], [], [] - for i in range(len(position)): - next_i = i + 1 - while len(position) >= next_i and position[i] == position[next_i]: - next_i += 1 - if next_i - i > 1: - distance = [(proj - line[position[i]]).length for proj in points[i:next_i]] - chunk_dist, chunk_p, chunck_pos, chunk_rep = zip(*sorted( - zip(distance, points, position, replace), key=lambda l: l[0])) - new_p.append(chunk_p) - new_pos.append(chunck_pos) - new_rep.append(chunk_rep) + if not is_instance_of_vector(line): + line = [Vector(co) for co in line] + if not is_instance_of_vector(points): + points = [Vector(co) for co in points] + sorting_mask_temp = np.argsort(position) + sorting_mask = [] + stack = [] + + def handle_stack(real_i=None): + nonlocal stack, sorting_mask + if len(stack) > 1: + distance = [(points[real_i] - line[position[stack[-1]]]).length for real_i in stack] + _, stack = zip(*sorted(zip(distance, stack), key=lambda l: l[0])) + sorting_mask.extend(stack) + stack = [] else: - new_p.append(points[i]) - new_pos.append(position[i]) - new_rep.append(replace[i]) - return new_p, new_pos, new_rep - + sorting_mask.extend(stack) + stack = [] + if real_i is not None: + stack.append(real_i) -def integrate_points_to_line(line, points, position, replace): - """ - merge projected points with old line - :param line: points of old line - [Point 1, Point 2, ...] - :param points: projected points that should be merged - [Point 1, Point 2, ...] - :param position: indexes of points of old line after which projected points should be pasted - [int1, int2, ...] - :param replace: if projected points coincide with existing points of old line they should replace them -> - -> [False, True, False, ...] - order of lists (points, position and replace) should be related with each other - :return: - """ - points, position, replace = sort_points_along_line(line, points, position, replace) - last_pos = 0 - merged_line = [] - number_added_points = 0 # of input line - for i, p in enumerate(points): - if last_pos == position[i]: # position didn't not move - if replace[i]: - pass - else: - if last_pos <= number_added_points: # position didn't move first time - merged_line.append(line[last_pos:position[i]+1]) - number_added_points += position[i] + 1 - last_pos - else: # position moved - if replace[i]: - merged_line.append([line[last_pos:position[i]]]) - else: - merged_line = [line[position[0] + 1]] + for i in range(len(position)): + real_i = sorting_mask_temp[i] + if not stack: + stack.append(real_i) + continue + if len(position) > i and position[stack[-1]] == position[real_i]: + stack.append(real_i) + continue + handle_stack(real_i) + if stack: + handle_stack() - last_pos = position[i] + return sorting_mask class SvProjectPointToLine(bpy.types.Node, SverchCustomTreeNode): @@ -331,6 +324,7 @@ class SvProjectPointToLine(bpy.types.Node, SverchCustomTreeNode): self.inputs.new('VerticesSocket', 'Project_points') self.outputs.new('VerticesSocket', 'Points_projected') self.outputs.new('StringsSocket', 'Belonging') + self.outputs.new('StringsSocket', 'Sorting_mask') def draw_buttons_ext(self, context, layout): layout.row().prop(self, 'set_res', toggle=True) @@ -350,9 +344,13 @@ class SvProjectPointToLine(bpy.types.Node, SverchCustomTreeNode): projected_points, indexes, coincidence = project_points(new_lines, edges_indexes, trees, p_points) self.outputs['Points_projected'].sv_set([[v[:] for v in l] for l in projected_points]) - self.outputs['Belonging'].sv_set([[[i] if c else (i, i+1) for i, c in zip(ind, coin)] + self.outputs['Belonging'].sv_set([[(i) if c == 1 else (i, i+1) if c == 0 else (i+1) for i, c in zip(ind, coin)] for ind, coin in zip(indexes, coincidence)]) + if self.outputs['Sorting_mask'].is_linked: + self.outputs['Sorting_mask'].sv_set([sort_points_along_line(line, p, pos) + for line, p, pos in zip(v_lines, projected_points, indexes)]) + def register(): bpy.utils.register_class(SvProjectPointToLine) -- GitLab From 0e0f08ee0947707d0cf23823ff8dcf520940cfec Mon Sep 17 00:00:00 2001 From: Durman Date: Sat, 18 May 2019 10:08:44 +0400 Subject: [PATCH 105/137] add cyclic of input line --- nodes/analyzer/project_point_to_line.py | 52 +++++++++++++++++++------ 1 file changed, 41 insertions(+), 11 deletions(-) diff --git a/nodes/analyzer/project_point_to_line.py b/nodes/analyzer/project_point_to_line.py index 50737c524..3cd25a0ae 100644 --- a/nodes/analyzer/project_point_to_line.py +++ b/nodes/analyzer/project_point_to_line.py @@ -171,7 +171,7 @@ def preparing_lines(vectors_line, resolution=False): return new_lines, edges_indexes, trees -def find_closest_index(edges_index, ind): +def find_closest_index(edges_index, ind, cyclic=False): """ find 3 closest indexes of points exist in initial line :param edges_index: index of position of Vectors from old line on new line - [int1, int2, ...] @@ -181,19 +181,28 @@ def find_closest_index(edges_index, ind): :return old_3ind: indexes of closest 3 points exist on old line that correspond to old line - [int1, int2, int3] """ i = bisect(edges_index, ind) - if i == len(edges_index): - new_3ind = edges_index[i - 2], edges_index[i - 1], None - old_3ind = [i - 2, i - 1, None] + num = len(edges_index) + if i == num: + if cyclic: + new_3ind = edges_index[i - 2], edges_index[i - 1], edges_index[0] + old_3ind = [num - 2, num - 1, 0] + else: + new_3ind = edges_index[i - 2], edges_index[i - 1], None + old_3ind = [i - 2, i - 1, None] elif i - 1 == 0: - new_3ind = None, edges_index[i - 1], edges_index[i] - old_3ind = [None, i - 1, i] + if cyclic: + new_3ind = edges_index[-1], edges_index[i - 1], edges_index[i] + old_3ind = [num - 1, 0, 1] + else: + new_3ind = None, edges_index[i - 1], edges_index[i] + old_3ind = [None, i - 1, i] else: new_3ind = edges_index[i - 2], edges_index[i - 1], edges_index[i] old_3ind = [i - 2, i - 1, i] return new_3ind, old_3ind -def project_points(new_lines, edges_indexes, kdtrees_from_lines, pr_points): +def project_points(new_lines, edges_indexes, kdtrees_from_lines, pr_points, cyclic=False): """ project points to prepared lines :param new_lines: prepared lines - [[Vector 1, Vector 2, ...], [Vector 1, ...], ...] @@ -221,7 +230,7 @@ def project_points(new_lines, edges_indexes, kdtrees_from_lines, pr_points): old_indexes_nest1 = [] coincidence_nest1 = [] for ind, point in zip(inds, points): - (new_i1, new_i2, new_i3), (old_i1, old_i2, old_i3) = find_closest_index(edges_ind, ind) + (new_i1, new_i2, new_i3), (old_i1, old_i2, old_i3) = find_closest_index(edges_ind, ind, cyclic) if new_i1 is None: clos_p2, coincidence_start = get_closest_point_to_edge((line[new_i2], line[new_i3]), point) project_points_nest1.append(clos_p2) @@ -319,6 +328,11 @@ class SvProjectPointToLine(bpy.types.Node, SverchCustomTreeNode): default=False, update=switch_res_mode) + cyclic = BoolProperty(name='Cyclic', + description='Close input lines', + default=False, + update=SverchCustomTreeNode.process_node) + def sv_init(self, context): self.inputs.new('VerticesSocket', 'Vectors_lines') self.inputs.new('VerticesSocket', 'Project_points') @@ -326,6 +340,9 @@ class SvProjectPointToLine(bpy.types.Node, SverchCustomTreeNode): self.outputs.new('StringsSocket', 'Belonging') self.outputs.new('StringsSocket', 'Sorting_mask') + def draw_buttons(self, context, layout): + layout.row().prop(self, 'cyclic', toggle=True) + def draw_buttons_ext(self, context, layout): layout.row().prop(self, 'set_res', toggle=True) @@ -341,11 +358,24 @@ class SvProjectPointToLine(bpy.types.Node, SverchCustomTreeNode): resn = False new_lines, edges_indexes, trees = preparing_lines(v_lines, resn) - projected_points, indexes, coincidence = project_points(new_lines, edges_indexes, trees, p_points) + projected_points, indexes, coincidence = project_points(new_lines, edges_indexes, trees, p_points, self.cyclic) self.outputs['Points_projected'].sv_set([[v[:] for v in l] for l in projected_points]) - self.outputs['Belonging'].sv_set([[(i) if c == 1 else (i, i+1) if c == 0 else (i+1) for i, c in zip(ind, coin)] - for ind, coin in zip(indexes, coincidence)]) + + bel_temp = [] + for ind, coin, line in zip(indexes, coincidence, v_lines): + bel_nest = [] + for i, c in zip(ind, coin): + if c == 1: + bel_nest.append((i)) + elif c == 2: + bel_nest.append((i+1)) + elif i == len(line)-1: + bel_nest.append((i, 0)) + else: + bel_nest.append((i, i+1)) + bel_temp.append(bel_nest) + self.outputs['Belonging'].sv_set(bel_temp) if self.outputs['Sorting_mask'].is_linked: self.outputs['Sorting_mask'].sv_set([sort_points_along_line(line, p, pos) -- GitLab From 30748ce8d64d9071e7cf5661053adb5964020b02 Mon Sep 17 00:00:00 2001 From: Durman Date: Sat, 18 May 2019 19:27:39 +0400 Subject: [PATCH 106/137] Add documentation and tidy up the code --- .../alpha_nodes/project_point_to_line.rst | 70 +++++++++++++++++++ nodes/analyzer/project_point_to_line.py | 67 ++++++++++++------ 2 files changed, 115 insertions(+), 22 deletions(-) create mode 100644 docs/nodes/alpha_nodes/project_point_to_line.rst diff --git a/docs/nodes/alpha_nodes/project_point_to_line.rst b/docs/nodes/alpha_nodes/project_point_to_line.rst new file mode 100644 index 000000000..cdd977264 --- /dev/null +++ b/docs/nodes/alpha_nodes/project_point_to_line.rst @@ -0,0 +1,70 @@ +Project points to line +====================== + +.. image:: https://user-images.githubusercontent.com/28003269/57965425-316e3d00-7955-11e9-9d2b-016f9ebce9aa.png + +Functionality +------------- +This node allows to find closest point, that belongs to line, to input point. Also the node gives information where the +point located on line. If there are several projected points it possible to sort them in order of vectors of input line. + +Information about working of the algorithm: #2376 + +Inputs +------ + +- **Vectors_line** - sorted points in order from start line to end +- **Project_points** - bunch of points that should be projected +- **resolution** - parameter that can be revealed from N panel + ++----------------+-------+------+--------------------------------------------------------------------------------+ +| Parameters | Type | min | Description | ++================+=======+======+================================================================================+ +| Cyclic | Bool | - | In this mode the node considers the input line is close | ++----------------+-------+------+--------------------------------------------------------------------------------+ +| Set resolution | Bool | - | This parameter located on N panel makes resolution parameter available | ++----------------+-------+------+--------------------------------------------------------------------------------+ +| resolution | float | 0.01 | It is expected in most cases this can remain with default value. Details below | ++----------------+-------+------+--------------------------------------------------------------------------------+ + +**resolution:** +Feature of the algorithm makes necessary to have this parameter. Unfortunately the algorithm can give quite rough +result. The less resolution the more accurate and expensive result. By default resolution is equal to length of +smallest edge of input line. If there is line that have edges and one of them is very small it can slow down the +calculation of the node. If you does not satisfied by result with default value, enabled parameter resolution and +try to decreasing value. + +Outputs +------- + +- **Points_projected** - projected points from input points to input line +- **Belonging** - this socket keeps values which relates projected points with line. This values says where projected point hit the line. Projected point can lay on an edge or coincide with existing point of the line. In first case the value will be in such format [index of first point of edge, index of last point of the edge]. In second case [index of coincided point] +- **Sorting_mask** - also is quite powerful output. This output gives indexes of projected points in order from start input line to end. For example if you have three projected points to a line and they hit line in edges in next order [edge 2, edge 3, edge 1] then sorting mask will have a look like this [2, 0, 1]. It means that for sorting we should take last edge then first edge and then middle edge, so we get list like this [edge 1, edge 2, edge 3]. Benefits of using such sorthing mask is that this mask possible to apply not only to output data of the node but also to input data and other data with the same length and related with project points. + +Examples +-------- +**Project random points to sine:** + +.. image:: https://user-images.githubusercontent.com/28003269/57971033-c7c45200-7999-11e9-85df-d271412cc0f1.png +.. image:: https://user-images.githubusercontent.com/28003269/57971035-c8f57f00-7999-11e9-8d86-2ed2ea6d64aa.png + +**Sort projected points according direction of input line:** + +- green - indexes of points of input line +- redish - indexes of input points from which projection should be done +- blue - indexes of projected points + +.. image:: https://user-images.githubusercontent.com/28003269/57965469-b35e6600-7955-11e9-9325-29362c86eb09.png +.. image:: https://user-images.githubusercontent.com/28003269/57965472-b8bbb080-7955-11e9-8975-0635c9d77230.png + +**Similar to previous example but input points for projection are sorted:** + +.. image:: https://user-images.githubusercontent.com/28003269/57952374-0b697e00-78fe-11e9-942a-69800a947943.png +.. image:: https://user-images.githubusercontent.com/28003269/57952406-2c31d380-78fe-11e9-8f49-31ae0a40c9ba.png + +**Projection one line to another:** + +.. image:: https://user-images.githubusercontent.com/28003269/57965586-464bd000-7957-11e9-9298-5c004bd16442.png +.. image:: https://user-images.githubusercontent.com/28003269/57965649-15b86600-7958-11e9-97ec-cee8d105ba3d.gif +.. image:: https://user-images.githubusercontent.com/28003269/57971245-77022880-799c-11e9-8711-bb6a0bf959fb.png +.. image:: https://user-images.githubusercontent.com/28003269/57965688-924b4480-7958-11e9-9340-95ccec11de3d.png diff --git a/nodes/analyzer/project_point_to_line.py b/nodes/analyzer/project_point_to_line.py index 3cd25a0ae..14be9bd78 100644 --- a/nodes/analyzer/project_point_to_line.py +++ b/nodes/analyzer/project_point_to_line.py @@ -18,14 +18,12 @@ import numpy as np from bisect import bisect -from functools import reduce import bpy from bpy.props import IntProperty, EnumProperty, BoolProperty, FloatProperty from mathutils import kdtree, Vector from sverchok.node_tree import SverchCustomTreeNode -from sverchok.data_structure import get_other_socket, updateNode, replace_socket, match_long_repeat from sverchok.data_structure import match_long_repeat ''' @@ -95,7 +93,10 @@ def get_closest_point_to_edge(edge, point): :param edge: [Vector 1, Vector 2] :param point: Vector :return project_point_inside_edge: Vector - :return coincidence: coincidence projected point with one of points of the edge - bool + :return coincidence: coincidence projected point with one of points of the edge - 0, 1, 2 + 0 - there is no coincidence + 1 - coincidence with first point of edge + 2 - coincidence with second point of edge """ edge_vector = edge[1] - edge[0] point = point - edge[0] @@ -177,6 +178,7 @@ def find_closest_index(edges_index, ind, cyclic=False): :param edges_index: index of position of Vectors from old line on new line - [int1, int2, ...] for example, if last vector of an edge has index 1 and this edge is divided on 3 parts new index of last Vector is 3 :param ind: index of closest point on new line to project point - int + :param cyclic: is line closed - bool :return new_3ind: indexes of closest 3 points exist on old line that correspond to new line - [int1, int2, int3] :return old_3ind: indexes of closest 3 points exist on old line that correspond to old line - [int1, int2, int3] """ @@ -210,10 +212,14 @@ def project_points(new_lines, edges_indexes, kdtrees_from_lines, pr_points, cycl :param kdtrees_from_lines: KD trees prepared from new_lines - [KDTree 1, KDTree 2, ...] :param pr_points: points that should be projected - [[Vector 1, Vector 2, ...], [Vector 1, ...], ...] vector can be a list of 3 numbers + :param cyclic: is line closed - bool :return project_points: [[Vector 1, Vector 2, ...], [Vector 1, ...], ...] :return old_indexes: index of point of old line that located before projected point -> -> [[int 1, int 2, ...], [int 1, ...], ...] - :return coincidence: coincidence projected points with points of old line - [[False, True, ...], [False, ...], ...] + :return coincidence: coincidence projected points with points of old line - [[0, 0, ...], [1, ...], ...] + 0 - there is no coincidence + 1 - coincidence with first point of edge + 2 - coincidence with second point of edge """ if not is_instance_of_vector(new_lines): vectors_line = [[Vector(co) for co in l] for l in new_lines] @@ -260,7 +266,13 @@ def project_points(new_lines, edges_indexes, kdtrees_from_lines, pr_points, cycl def sort_points_along_line(line, points, position): - # sort points in order how how they move from start of line to end + """ + sort points in order how they move from start of line to end + :param line: input line - [Vector 1, Vector 2, ...] + :param points: projected points - [Vector 1, Vector 2, ...] + :param position: index of first vector of edge which projected point belongs - [2, 3, 1, ...] + :return: sorting_mask: indexes of projected points for sorting them in right order + """ if not is_instance_of_vector(line): line = [Vector(co) for co in line] if not is_instance_of_vector(points): @@ -297,12 +309,37 @@ def sort_points_along_line(line, points, position): return sorting_mask +def get_belonging_indexes(indexes, coincidence, v_lines): + """ + according indexes and coincidences create mask that shows which edge or point of line projected point is belonged + :param indexes: index of first vector of edge which projected point belongs - [[2, 3, 1, ...], [2, ...], ...] + :param coincidence: coincidence projected points with points of old line - [[0, 0, ...], [1, ...], ...] + :param v_lines: input line - [[Vector 1, Vector 2, ...], [Vector 1, ...], ...] + :return: indexes of vectors of edge to which projected point belongs or index only of one vector if projected point + coincides with vector [[[0, 1], [1], [1, 2], ...], [[0], [1, 2], ...], ...] + """ + bel_temp = [] + for ind, coin, line in zip(indexes, coincidence, v_lines): + bel_nest = [] + for i, c in zip(ind, coin): + if c == 1: + bel_nest.append([i]) + elif c == 2: + bel_nest.append([i + 1]) + elif i == len(line) - 1: + bel_nest.append((i, 0)) + else: + bel_nest.append((i, i + 1)) + bel_temp.append(bel_nest) + return bel_temp + + class SvProjectPointToLine(bpy.types.Node, SverchCustomTreeNode): """ Triggers: project point to line - Tooltip: project point to line + If there are point and line the node will find closest point on the line to point - You can do something + You can use number of points and lines as many as you wish """ bl_idname = 'SvProjectPointToLine' bl_label = 'Project point to line' @@ -361,21 +398,7 @@ class SvProjectPointToLine(bpy.types.Node, SverchCustomTreeNode): projected_points, indexes, coincidence = project_points(new_lines, edges_indexes, trees, p_points, self.cyclic) self.outputs['Points_projected'].sv_set([[v[:] for v in l] for l in projected_points]) - - bel_temp = [] - for ind, coin, line in zip(indexes, coincidence, v_lines): - bel_nest = [] - for i, c in zip(ind, coin): - if c == 1: - bel_nest.append((i)) - elif c == 2: - bel_nest.append((i+1)) - elif i == len(line)-1: - bel_nest.append((i, 0)) - else: - bel_nest.append((i, i+1)) - bel_temp.append(bel_nest) - self.outputs['Belonging'].sv_set(bel_temp) + self.outputs['Belonging'].sv_set(get_belonging_indexes(indexes, coincidence, v_lines)) if self.outputs['Sorting_mask'].is_linked: self.outputs['Sorting_mask'].sv_set([sort_points_along_line(line, p, pos) -- GitLab From dd87c6e19d0a759b65740640e5df9ba00b6844ff Mon Sep 17 00:00:00 2001 From: Ilya Portnov Date: Sun, 19 May 2019 21:37:06 +0500 Subject: [PATCH 107/137] fractal curve node. --- docs/nodes/modifier_make/fractal_curve.rst | 46 ++++++++ nodes/modifier_make/fractal_curve.py | 119 +++++++++++++++++++++ 2 files changed, 165 insertions(+) create mode 100644 docs/nodes/modifier_make/fractal_curve.rst create mode 100644 nodes/modifier_make/fractal_curve.py diff --git a/docs/nodes/modifier_make/fractal_curve.rst b/docs/nodes/modifier_make/fractal_curve.rst new file mode 100644 index 000000000..fd41d9538 --- /dev/null +++ b/docs/nodes/modifier_make/fractal_curve.rst @@ -0,0 +1,46 @@ +Fractal curve +============= + +Functionality +------------- + +This node generates a fractal curve, by replacing each edge of input curve with a copy of that curve, several times. + +NB 1: Number of vertices in the output curve grows exponentially with number of iterations. + +NB 2: Usually you will want to use curves, for which diameter (distance between +to most distant vertices) is less than distance from the first vertex to the +last. Otherwise, the output curve can grow very large. + +NB 3: Usually you will want to use curves, edges of which have nearly the same length. + +Inputs +------ + +This node has one input: **Vertices** - vertices of input curve. Vertices +should go in the order in which they appear in the curve. + +Parameters +---------- + +This node has the following parameters: + +* **Iterations**. Number of iterations. If zero, then output curve will be + exactly the same as input one. Default value is 3. +* **Min. length**. Minimum length of edge to substitute. Fractal substitution + process will stop for specific edge if it's length became less than specified + value. Minimal value of zero means that fractal substitution process is + stopped only when maximum number of iterations is reached. +* **Precision**. Precision of intermediate calculations (number of decimal + digits). Default value is 8. This parameter is available only in the **N** panel. + +Outputs +------- + +This node has one output: **Vertices** - vertices of the output curve. Vertices +go in the order in which they appear in the curve. You may want to use **UV +Connector** node to draw edges between these vertices. + +Examples of usage +----------------- + diff --git a/nodes/modifier_make/fractal_curve.py b/nodes/modifier_make/fractal_curve.py new file mode 100644 index 000000000..95b14985f --- /dev/null +++ b/nodes/modifier_make/fractal_curve.py @@ -0,0 +1,119 @@ +# ##### 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 operator + +import bpy +from mathutils import Matrix, Vector + +from bpy.props import IntProperty, FloatProperty, EnumProperty +from sverchok.node_tree import SverchCustomTreeNode +from sverchok.data_structure import (Vector_generate, updateNode, + match_long_repeat) +from sverchok.utils.geom import autorotate_householder, autorotate_track, autorotate_diff, diameter + + +class SvFractalCurveNode(bpy.types.Node, SverchCustomTreeNode): + '''Fractal curve''' + bl_idname = 'SvFractalCurveNode' + bl_label = 'Fractal Curve' + bl_icon = 'OUTLINER_OB_EMPTY' + + iterations = IntProperty(name = 'Iterations', + description = 'Number of iterations', + default = 3, min = 0, update = updateNode) + + min_length = FloatProperty(name = 'Min. length', + description = 'Minimum edge length', + default = 0.01, min = 0, update = updateNode) + + precision = IntProperty( + name="Precision", min=0, max=10, default=8, update=updateNode, + description="decimal precision of coordinates for calculations") + + def move_to(self, curve, src, dst): + vector = dst - src + return [vertex + vector for vertex in curve] + + def scale_to(self, curve, src, dst): + coef = dst / src + return [vertex * coef for vertex in curve] + + def rotate_to(self, curve, src, dst): + matrix = autorotate_diff(dst, src) + return [matrix * vertex for vertex in curve] + + def substitute(self, recipient, donor): + line = donor[-1] - donor[0] + result = [] + result.append(donor[0]) + for pair in zip(recipient, recipient[1:]): + new_line = pair[1] - pair[0] + if new_line.length < self.min_length: + result.append(pair[1]) + continue + scaled = self.scale_to(donor, line.length, new_line.length) + rotated = self.rotate_to(scaled, line, new_line) + item = self.move_to(rotated, rotated[0], pair[0]) + result.extend(item[1:]) + if self.precision > 0: + result = [Vector(round(x, self.precision) for x in v) for v in result] + print(result) + return result + + def make_fractal(self, vertices): + result = vertices + for i in range(self.iterations): + result = self.substitute(result, vertices) + result = self.move_to(result, result[0], vertices[0]) + return result + + def draw_buttons(self, context, layout): + layout.prop(self, "iterations") + layout.prop(self, "min_length") + + def draw_buttons_ext(self, context, layout): + self.draw_buttons(context, layout) + layout.prop(self, "precision") + + def sv_init(self, context): + self.inputs.new('VerticesSocket', 'Vertices') + + self.outputs.new('VerticesSocket', 'Vertices') + + def process(self): + if not any(s.is_linked for s in self.outputs): + return + + curves = Vector_generate(self.inputs['Vertices'].sv_get()) + + verts_out = [] + + for curve in curves: + curve_out = self.make_fractal(curve) + verts_out.append([tuple(v) for v in curve_out]) + + self.outputs['Vertices'].sv_set(verts_out) + +def register(): + bpy.utils.register_class(SvFractalCurveNode) + + +def unregister(): + bpy.utils.unregister_class(SvFractalCurveNode) + -- GitLab From 9a5cb4d7212722d53a187bba85e4ee7382fb5cc2 Mon Sep 17 00:00:00 2001 From: Ilya Portnov Date: Sun, 19 May 2019 21:41:41 +0500 Subject: [PATCH 108/137] Update documentation --- docs/nodes/modifier_make/fractal_curve.rst | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/docs/nodes/modifier_make/fractal_curve.rst b/docs/nodes/modifier_make/fractal_curve.rst index fd41d9538..b9f69e332 100644 --- a/docs/nodes/modifier_make/fractal_curve.rst +++ b/docs/nodes/modifier_make/fractal_curve.rst @@ -44,3 +44,15 @@ Connector** node to draw edges between these vertices. Examples of usage ----------------- +Classical example: + +.. image:: https://user-images.githubusercontent.com/284644/57985244-96718200-7a7e-11e9-8559-61a6feb78882.png + +Another example: + +.. image:: https://user-images.githubusercontent.com/284644/57985245-970a1880-7a7e-11e9-82ab-f69d61bd5e1d.png + +This node can process 3D curves as well: + +.. image:: https://user-images.githubusercontent.com/284644/57985246-970a1880-7a7e-11e9-84f3-198244d92df0.png + -- GitLab From b6e63d72063b7d658116403c82c52c8c2c79e984 Mon Sep 17 00:00:00 2001 From: Ilya Portnov Date: Sun, 19 May 2019 21:47:29 +0500 Subject: [PATCH 109/137] Update index.md. --- index.md | 1 + 1 file changed, 1 insertion(+) diff --git a/index.md b/index.md index 29d760803..de97965c0 100644 --- a/index.md +++ b/index.md @@ -128,6 +128,7 @@ SvAdaptiveEdgeNode AdaptivePolsNode SvDuplicateAlongEdgeNode + SvFractalCurveNode SvSolidifyNode SvWireframeNode SvPipeNode -- GitLab From 3a92b63a953c899535719ce9cde8763d163234ff Mon Sep 17 00:00:00 2001 From: Ilya Portnov Date: Wed, 22 May 2019 22:18:24 +0500 Subject: [PATCH 110/137] Fixes / hacks for some corner cases (see comments in the code). --- nodes/modifier_make/fractal_curve.py | 66 ++++++++++++++++++++++++---- 1 file changed, 58 insertions(+), 8 deletions(-) diff --git a/nodes/modifier_make/fractal_curve.py b/nodes/modifier_make/fractal_curve.py index 95b14985f..07fc288a8 100644 --- a/nodes/modifier_make/fractal_curve.py +++ b/nodes/modifier_make/fractal_curve.py @@ -43,7 +43,7 @@ class SvFractalCurveNode(bpy.types.Node, SverchCustomTreeNode): default = 0.01, min = 0, update = updateNode) precision = IntProperty( - name="Precision", min=0, max=10, default=8, update=updateNode, + name="Precision", min=0, max=10, default=7, update=updateNode, description="decimal precision of coordinates for calculations") def move_to(self, curve, src, dst): @@ -54,11 +54,60 @@ class SvFractalCurveNode(bpy.types.Node, SverchCustomTreeNode): coef = dst / src return [vertex * coef for vertex in curve] - def rotate_to(self, curve, src, dst): - matrix = autorotate_diff(dst, src) + def check_plane(self, curve): + # Try to detect if the whole figure belongs to + # one of coordinate axes + epsilon = 10 ** (- self.precision) + if all([abs(v.z) < epsilon for v in curve]): + return 'XY' + elif all([abs(v.x) < epsilon for v in curve]): + return 'YZ' + elif all([abs(v.y) < epsilon for v in curve]): + return 'XZ' + else: + return None + + def calc_rotation(self, curve, src, dst, plane=None): + # Some hacks. + # Problem: if src and/or dst are exactly parallel to + # one of coordinate axes, then Vector.rotation_difference + # sometimes returns a matrix that rotates our vector in + # completely different plane. + # For example, if whole figure lays in XY plane, and + # we are trying to rotate src = (0, 1, 0) into dst = (1, 0, 0), + # then rotation_difference might return us a matrix, which + # rotates (-1, 0, 0) into (0, 0, -1), which is out of XY plane + # ("because why no? it still rotates src into dst"). + # Solution (hack): if whole figure lays in XY plane, then do + # not use general rotation_difference method, calculate + # rotation along Z axis only. + if plane == 'XY': + # Another unobvious hack: Vector.angle_signed method + # works with 2D vectors only (this is not stated in + # it's documentation!). Fortunately, in this particular + # case our vectors are actually 2D. + dst = Vector((dst[0], dst[1])) + src = Vector((src[0], src[1])) + angle = dst.angle_signed(src) + return Matrix.Rotation(angle, 4, 'Z') + elif plane == 'YZ': + dst = Vector((dst[1], dst[2])) + src = Vector((src[1], src[2])) + angle = dst.angle_signed(src) + return Matrix.Rotation(angle, 4, 'X') + elif plane == 'XZ': + dst = Vector((dst[2], dst[0])) + src = Vector((src[2], src[0])) + angle = dst.angle_signed(src) + return Matrix.Rotation(angle, 4, 'Y') + else: + return autorotate_diff(dst, src) + + def rotate_to(self, curve, src, dst, plane=None): + matrix = self.calc_rotation(curve, src, dst, plane) return [matrix * vertex for vertex in curve] - def substitute(self, recipient, donor): + def substitute(self, recipient, donor, plane=None): line = donor[-1] - donor[0] result = [] result.append(donor[0]) @@ -68,18 +117,19 @@ class SvFractalCurveNode(bpy.types.Node, SverchCustomTreeNode): result.append(pair[1]) continue scaled = self.scale_to(donor, line.length, new_line.length) - rotated = self.rotate_to(scaled, line, new_line) + rotated = self.rotate_to(scaled, line, new_line, plane) item = self.move_to(rotated, rotated[0], pair[0]) result.extend(item[1:]) if self.precision > 0: - result = [Vector(round(x, self.precision) for x in v) for v in result] - print(result) + result = [Vector(v.to_tuple(self.precision)) for v in result] return result def make_fractal(self, vertices): result = vertices + plane = self.check_plane(vertices) + print(plane) for i in range(self.iterations): - result = self.substitute(result, vertices) + result = self.substitute(result, vertices, plane) result = self.move_to(result, result[0], vertices[0]) return result -- GitLab From d3949c2275f4ef9c6c56f484e72d504b05301896 Mon Sep 17 00:00:00 2001 From: Ilya Portnov Date: Thu, 23 May 2019 22:29:54 +0500 Subject: [PATCH 111/137] Linear approximation node. --- index.md | 1 + nodes/vector/linear_approx.py | 128 +++++++++++++ utils/geom.py | 342 ++++++++++++++++++++++++++++++++++ 3 files changed, 471 insertions(+) create mode 100644 nodes/vector/linear_approx.py diff --git a/index.md b/index.md index 430c6a10b..89cd9902e 100644 --- a/index.md +++ b/index.md @@ -211,6 +211,7 @@ SvInterpolationNode SvInterpolationNodeMK2 SvInterpolationNodeMK3 + SvLinearApproxNode --- SvHomogenousVectorField SvNoiseNodeMK2 diff --git a/nodes/vector/linear_approx.py b/nodes/vector/linear_approx.py new file mode 100644 index 000000000..0eabfab02 --- /dev/null +++ b/nodes/vector/linear_approx.py @@ -0,0 +1,128 @@ +# ##### 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 +from mathutils import Vector + +from sverchok.node_tree import SverchCustomTreeNode +from sverchok.data_structure import (updateNode) +from sverchok.utils.geom import linear_approximation + +class SvLinearApproxNode(bpy.types.Node, SverchCustomTreeNode): + ''' Linear Approximation ''' + bl_idname = 'SvLinearApproxNode' + bl_label = 'Linear Approximation' + bl_icon = 'OUTLINER_OB_EMPTY' + + modes = [ + ("Line", "Line", "Straight line", 1), + ("Plane", "Plane", "Plane", 2) + ] + + def update_mode(self, context): + self.outputs['Direction'].hide_safe = (self.mode != 'Line') + self.outputs['Normal'].hide_safe = (self.mode != 'Plane') + updateNode(self, context) + + mode = EnumProperty(name = "Approximate by", + items = modes, + default = "Line", + update = update_mode) + + def sv_init(self, context): + self.inputs.new('VerticesSocket', "Vertices") + + self.outputs.new('VerticesSocket', "Center") + self.outputs.new('VerticesSocket', "Normal") + self.outputs.new('VerticesSocket', "Direction") + self.outputs.new('VerticesSocket', "Projections") + self.outputs.new('VerticesSocket', "Diffs") + self.outputs.new('StringsSocket', "Distances") + + def draw_buttons(self, context, layout): + layout.prop(self, 'mode') + + def process(self): + if not any(output.is_linked for output in self.outputs): + return + + vertices_s = self.inputs['Vertices'].sv_get(default=[[]]) + + out_centers = [] + out_normals = [] + out_directions = [] + out_projections = [] + out_diffs = [] + out_distances = [] + + for vertices in vertices_s: + approx = linear_approximation(vertices) + + out_centers.append(approx.center) + + if self.mode == 'Line': + line = approx.most_similar_line() + out_directions.append(tuple(line.direction.normalized())) + + projections = [] + diffs = [] + distances = [] + for vertex in vertices: + projection = line.projection_of_point(vertex) + projections.append(tuple(projection)) + diff = projection - Vector(vertex) + diffs.append(tuple(diff)) + distances.append(diff.length) + + out_projections.append(projections) + out_diffs.append(diffs) + out_distances.append(distances) + + elif self.mode == 'Plane': + plane = approx.most_similar_plane() + out_normals.append(tuple(plane.normal.normalized())) + + projections = [] + diffs = [] + distances = [] + for vertex in vertices: + projection = plane.projection_of_point(vertex) + projections.append(tuple(projection)) + diff = projection - Vector(vertex) + diffs.append(tuple(diff)) + distances.append(diff.length) + + out_projections.append(projections) + out_diffs.append(diffs) + out_distances.append(distances) + + self.outputs['Center'].sv_set([out_centers]) + self.outputs['Normal'].sv_set([out_normals]) + self.outputs['Direction'].sv_set([out_directions]) + self.outputs['Projections'].sv_set(out_projections) + self.outputs['Diffs'].sv_set(out_diffs) + self.outputs['Distances'].sv_set(out_distances) + +def register(): + bpy.utils.register_class(SvLinearApproxNode) + + +def unregister(): + bpy.utils.unregister_class(SvLinearApproxNode) + diff --git a/utils/geom.py b/utils/geom.py index 4ce0ee041..adcaecf83 100644 --- a/utils/geom.py +++ b/utils/geom.py @@ -28,6 +28,7 @@ only for speed, never for aesthetics or line count or cleverness. import math import numpy as np +from numpy import linalg from functools import wraps import time @@ -871,6 +872,347 @@ def diameter(vertices, axis): m = min(xs) return (M-m) +def center(data): + """ + input: data - a list of 3-tuples or numpy array of same shape + output: 3-tuple - arithmetical average of input vertices (barycenter) + """ + array = np.array(data) + n = array.shape[0] + center = array.sum(axis=0) / n + return tuple(center) + +class PlaneEquation(object): + """ + An object, containing the coefficients A, B, C, D in the equation of a + plane: + + A*x + B*y + C*z + D = 0 + """ + def __init__(self, a, b, c, d): + self.a = a + self.b = b + self.c = c + self.d = d + + def __repr__(self): + return "[{}, {}, {}, {}]".format(self.a, self.b, self.c, self.d) + + def __str__(self): + return "{}x + {}y + {}z + {} = 0".format(self.a, self.b, self.c, self.d) + + @classmethod + def from_normal_and_point(cls, normal, point): + a, b, c = tuple(normal) + cx, cy, cz = tuple(point) + d = - (a*cx + b*cy + c*cz) + return PlaneEquation(a, b, c, d) + + @classmethod + def from_three_points(cls, p1, p2, p3): + x1, y1, z1 = p1[0], p1[1], p1[2] + x2, y2, z2 = p2[0], p2[1], p2[2] + x3, y3, z3 = p3[0], p3[1], p3[2] + + a = (y2 - y1)*(z3-z1) - (z2 - z1)*(y3 - y1) + b = - (x2 - x1)*(z3-z1) + (z2 - z1)*(x3 - x1) + c = (x2 - x1)*(y3 - y1) - (y2 - y1)*(x3 - x1) + + return PlaneEquation.from_normal_and_point((a, b, c), p1) + + def normalized(self): + """ + Return equation, which defines exactly the same plane, but with coefficients adjusted so that + + A^2 + B^2 + C^2 = 1 + + holds. + """ + normal = self.normal.length + return PlaneEquation(a/normal, b/normal, c/normal, d/normal) + + def check(self, point, eps=1e-6): + """ + Check if specified point belongs to the plane. + """ + a, b, c, d = self.a, self.b, self.c, self.d + x, y, z = point[0], point[1], point[2] + value = a*x + b*y + c*z + d + return abs(value) < eps + + def evaluate(self, u, v): + p0 = self.nearest_point_to_origin() + a, b, c, d = self.a, self.b, self.c, self.d + v1 = mathutils.Vector((-b, a, c)) + v2 = mathutils.Vector((a, -c, b)) + + return p0 + u*v1 + v*v2 + + @property + def normal(self): + return mathutils.Vector((self.a, self.b, self.c)) + + @normal.setter + def normal(self, normal): + self.a = normal[0] + self.b = normal[1] + self.c = normal[2] + + def nearest_point_to_origin(self): + a, b, c, d = self.a, self.b, self.c, self.d + sqr = a*a + b*b + c*c + return mathutils.Vector(((- a*d)/sqr, (- b*d)/sqr, (- c*d)/sqr)) + + def distance_to_point(self, point): + point_on_plane = self.nearest_point_to_origin() + return mathutils.geometry.distance_point_to_plane(mathutils.Vector(point), point_on_plane, self.normal) + + def intersect_with_line(self, line): + a, b, c, d = self.a, self.b, self.c, self.d + x0, y0, z0 = line.x0, line.y0, line.z0 + + # Here we numerically solve the system of linear equations: + # + # / x - x0 y - y0 z - z0 + # | ------ = ------ = ------, (line) + # / A B C (*) + # ` + # | A x + B y + C z + D = 0 (plane) + # ` + # + # with relation to x, y, z. + # It is possible that any two of A, B, C are equal to zero, + # but not all three of them. + # Depending on which of A, B, C is not zero, we should + # consider different representations of line equation. + # + # For example, if B != 0, we can represent (*) as + # + # B (x - x0) = A (y - y0), + # C (y - y0) = B (z - z0), + # A x + B x + C z + D = 0. + # + # But, if B == 0, then this representation will contain + # two exactly equivalent equations: + # + # 0 = A (y - y0), + # C (y - y0) = 0, + # A x + 0 + C z + D = 0. + # + # In this case, the system will become singular; so + # we must choose another representation of (*) system. + + epsilon = 1e-8 + + if abs(a) > epsilon: + matrix = np.array([ + [b, -a, 0], + [c, 0, -a], + [a, b, c]]) + free = np.array([ + b*x0 - a*y0, + c*x0 - a*z0, + -d]) + elif abs(b) > epsilon: + matrix = np.array([ + [b, -a, 0], + [0, c, -b], + [a, b, c]]) + + free = np.array([ + b*x0 - a*y0, + c*y0 - b*z0, + -d]) + elif abs(c) > epsilon: + matrix = np.array([ + [c, 0, -a], + [0, c, -b], + [a, b, c]]) + free = np.array([ + c*x0 - a*z0, + c*y0 - b*z0, + -d]) + else: + raise Exception("Invalid plane: all coefficients are (nearly) zero: {}, {}, {}".format(a, b, c)) + + if abs(linalg.det(matrix)) < 1e-8: + raise Exception("Plane: {}, line: {}".format(self, line)) + + result = np.linalg.solve(matrix, free) + x, y, z = result[0], result[1], result[2] + return mathutils.Vector((x, y, z)) + + def projection_of_point(self, point): + # Draw a line, which has the same direction vector + # as this plane's normal, and which contains the + # given point. + line = LineEquation(self.a, self.b, self.c, point) + # Then find the intersection of this plane with that line. + return self.intersect_with_line(line) + +class LineEquation(object): + """ + An object, containing the coefficients A, B, C, x0, y0, z0 in the + equation of a line: + + x - x0 y - y0 z - z0 + ------ = ------ = ------, + A B C + """ + + def __init__(self, a, b, c, point): + self.a = a + self.b = b + self.c = c + self.point = point + + @classmethod + def from_two_points(cls, p1, p2): + x1, y1, z1 = p1[0], p1[1], p1[2] + x2, y2, z2 = p2[0], p2[1], p2[2] + + a = x2 - x1 + b = y2 - y1 + c = z2 - z1 + + return LineEquation(a, b, c, p1) + + def check(self, point, eps=1e-6): + """ + Check if the specified point belongs to the line. + """ + a, b, c = self.a, self.b, self.c + x0, y0, z0 = self.x0, self.y0, self.z0 + x, y, z = point[0], point[1], point[2] + + value1 = b * (x - x0) - a * (y - y0) + value2 = c * (y - y0) - b * (z - z0) + + return abs(value1) < eps and abs(value2) < eps + + @property + def x0(self): + return self.point[0] + + @x0.setter + def x0(self, x0): + self.point[0] = x0 + + @property + def y0(self): + return self.point[1] + + @y0.setter + def y0(self, y0): + self.point[1] = y0 + + @property + def z0(self): + return self.point[2] + + @z0.setter + def z0(self, z0): + self.point[2] = z0 + + @property + def direction(self): + return mathutils.Vector((self.a, self.b, self.c)) + + @direction.setter + def direction(self, vector): + self.a = vector[0] + self.b = vector[1] + self.c = vector[2] + + def __repr__(self): + return "[{}, {}, {}, ({}, {}, {})]".format(self.a, self.b, self.c, self.x0, self.y0, self.z0) + + def __str__(self): + return "(x - {})/{} = (y - {})/{} = (z - {})/{}".format(self.x0, self.a, self.y0, self.b, self.z0, self.c) + + def distance_to_point(self, point): + # TODO: there should be more effective way to do this + return self.projection_of_point(point).length + + def projection_of_point(self, point): + # Draw a plane, which has the same normal as + # this line's direction vector, and which contains the + # given point + plane = PlaneEquation.from_normal_and_point(self.direction, point) + # Then find an intersection of that plane with this line. + return plane.intersect_with_line(self) + +class LinearApproximationData(object): + """ + This class contains results of linear approximation calculation. + It's instance is returned by linear_approximation() method. + """ + def __init__(self): + self.center = None + self.eigenvalues = None + self.eigenvectors = None + + def most_similar_plane(self): + """ + Return coefficients of an equation of a plane, which + is the best linear approximation for input vertices. + + output: an instance of PlaneEquation class. + """ + + idx = np.argmin(self.eigenvalues) + normal = self.eigenvectors[:, idx] + return PlaneEquation.from_normal_and_point(normal, self.center) + + def most_similar_line(self): + """ + Return coefficients of an equation of a plane, which + is the best linear approximation for input vertices. + + output: an instance of LineEquation class. + """ + + idx = np.argmax(self.eigenvalues) + eigenvector = self.eigenvectors[:, idx] + a, b, c = tuple(eigenvector) + + return LineEquation(a, b, c, self.center) + +def linear_approximation(data): + """ + Calculate best linear approximation for a list of vertices. + Input vertices can be approximated by a plane or by a line, + or both. + + input: list of 3-tuples. + output: an instance of LinearApproximationData class. + """ + result = LinearApproximationData() + + result.center = cx,cy,cz = center(data) + + xs = [x[0]-cx for x in data] + ys = [x[1]-cy for x in data] + zs = [x[2]-cz for x in data] + + sx2 = sum(x**2 for x in xs) + sy2 = sum(y**2 for y in ys) + sz2 = sum(z**2 for z in zs) + + sxy = sum(x*y for (x,y) in zip(xs,ys)) + sxz = sum(x*z for (x,z) in zip(xs,zs)) + syz = sum(y*z for (y,z) in zip(ys,zs)) + + n = len(data) + + matrix = np.array([ + [sx2, sxy, sxz], + [sxy, sy2, syz], + [sxz, syz, sz2] + ]) + + result.eigenvalues, result.eigenvectors = linalg.eig(matrix) + return result def multiply_vectors(M, vlist): # (4*4 matrix) X (3*1 vector) -- GitLab From 3bc61d7fa954ce6d42e92d24ddbffb774071977f Mon Sep 17 00:00:00 2001 From: Victor Doval <10011941+vicdoval@users.noreply.github.com> Date: Sat, 25 May 2019 14:32:58 +0200 Subject: [PATCH 112/137] New Distance Point Line Node (#2416) * New Distance Point Line Node * New Distance Point Line Node Docs and icon fix * distance point line added tolerance input and list match tools * last corrections on docs * bad class name. Renaming * distance_point_line scale example * distance_point_line scale example typo * distance_point_line docs typo fix --- data_structure.py | 17 +- docs/nodes/analyzers/analyzers_index.rst | 2 + docs/nodes/analyzers/distance_point_line.rst | 88 ++++++++ index.md | 1 + nodes/analyzer/distance_point_line.py | 203 +++++++++++++++++++ ui/nodeview_rclick_menu.py | 8 +- 6 files changed, 313 insertions(+), 6 deletions(-) create mode 100644 docs/nodes/analyzers/distance_point_line.rst create mode 100644 nodes/analyzer/distance_point_line.py diff --git a/data_structure.py b/data_structure.py index aabe229c1..239e6569a 100755 --- a/data_structure.py +++ b/data_structure.py @@ -182,7 +182,22 @@ def sv_zip(*iterables): result.append(elem) yield result - +list_match_modes = [ + ("SHORT", "Match Short", "Match shortest List", 1), + ("CYCLE", "Cycle", "Match longest List by cycling", 2), + ("REPEAT", "Repeat Last", "Match longest List by repeating last item", 3), + ("XREF", "X-Ref", "Cross reference (fast cycle of long)", 4), + ("XREF2", "X-Ref 2", "Cross reference (fast cycle of short)", 5), + ] + +list_match_func = { + "SHORT": match_short, + "CYCLE": match_long_cycle, + "REPEAT": match_long_repeat, + "XREF": match_cross, + "XREF2": match_cross2 + } + ##################################################### ################# list levels magic ################# ##################################################### diff --git a/docs/nodes/analyzers/analyzers_index.rst b/docs/nodes/analyzers/analyzers_index.rst index 9611733ff..fcdd62066 100644 --- a/docs/nodes/analyzers/analyzers_index.rst +++ b/docs/nodes/analyzers/analyzers_index.rst @@ -8,10 +8,12 @@ Analyzers area bbox distance_pp + distance_point_line deformation edge_angles kd_tree kd_tree_edges_mk2 + linked_verts mesh_filter mesh_select select_similar diff --git a/docs/nodes/analyzers/distance_point_line.rst b/docs/nodes/analyzers/distance_point_line.rst new file mode 100644 index 000000000..d0721bff5 --- /dev/null +++ b/docs/nodes/analyzers/distance_point_line.rst @@ -0,0 +1,88 @@ +Distance Point Line +=================== + +Functionality +------------- + +The node is designed to get the distance between a point and one endless straight line. + +The line is defined by a segment of two vectors. + +As an extra results you can get: + +- If the point is in the line + +- If the point is in the segment + +- Which is the closest point in the line + +- If the closest point is in the segment + + +Inputs / Parameters +------------------- + + ++---------------------+-------------+----------------------------------------------------------------------+ +| Param | Type | Description | ++=====================+=============+======================================================================+ +| **Vertices** | Vertices | Points to calculate | ++---------------------+-------------+----------------------------------------------------------------------+ +| **Line Vertices** | Vertices | It will get the first and last vertices's to define the line segment | ++---------------------+-------------+----------------------------------------------------------------------+ +| **Tolerance** | Float | Minimal distance to accept one point is intersecting. | ++---------------------+-------------+----------------------------------------------------------------------+ + +Advanced Parameters +------------------- + +In the N-Panel (and on the right-click menu) you can find: + +**Implementation**: Choose between MathUtils and NumPy (Usually faster) + +**Output NumPy**: to get NumPy arrays in stead of regular lists (makes the node faster). Only in the NumPy implementation. + +**Match List Global**: Define how list with different lengths should be matched. Refers to the matching of groups (one line per group) + +**Match List Local**: Define how list with different lengths should be matched. Refers to the matching of tolerances and vertices + + +Outputs +------- + +**Distance**: Distance to the line. + +**In Segment**: Returns True if point distance is less than tolerance and the point is between the input vertices. + +**In Line**: Returns True if point distance is less than tolerance with input vertices. + +**Closest Point**: Returns the closest point in the line. + +**Closest in Segment**: Returns True if the closest point is between the input vertices. + + +Example of usage +---------------- + +.. image:: https://github.com/vicdoval/sverchok/raw/docs_images/images_for_docs/analyzer/distance_point_line/distance_point_line_sverchok_blender.png + :alt: Distance_point_line_procedural.PNG + +It can be used to create perpendicular lines from input points + +.. image:: https://github.com/vicdoval/sverchok/raw/docs_images/images_for_docs/analyzer/distance_point_line/distance_point_line_sverchok_blender_perpendicular_to_line.png + :alt: Sverchok_Distance_point_line.PNG + +In this example the node is used to separate the points which are at less than two units from the line. + +.. image:: https://github.com/vicdoval/sverchok/raw/docs_images/images_for_docs/analyzer/distance_point_line/distance_point_line_sverchok_blender_procedural.png + :alt: Blender_distance_point_line.PNG + +In this example the Inset Polygon node gets the inset and distance inputs from the distance of the polygon to the line. + +.. image:: https://raw.githubusercontent.com/vicdoval/sverchok/docs_images/images_for_docs/analyzer/distance_point_line/distance_point_line_sverchok_blender_from_polygon.png + :alt: Sverchok_Distance_polygon_line.PNG + +This example uses the node to scale geometry along a custom axis. + +.. image:: https://github.com/vicdoval/sverchok/raw/docs_images/images_for_docs/analyzer/distance_point_line/distance_point_line_sverchok_blender_scale_custom_axis.png + :alt: Sverchok_scale_along_custom_axis.PNG \ No newline at end of file diff --git a/index.md b/index.md index 430c6a10b..dc2fc1a5c 100644 --- a/index.md +++ b/index.md @@ -52,6 +52,7 @@ SvVolumeNode AreaNode DistancePPNode + SvDistancePointLineNode SvPathLengthNode CentersPolsNodeMK2 CentersPolsNodeMK3 diff --git a/nodes/analyzer/distance_point_line.py b/nodes/analyzer/distance_point_line.py new file mode 100644 index 000000000..105370c9e --- /dev/null +++ b/nodes/analyzer/distance_point_line.py @@ -0,0 +1,203 @@ +# ##### 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, FloatProperty +from sverchok.node_tree import SverchCustomTreeNode +from sverchok.data_structure import updateNode, list_match_func, list_match_modes +from mathutils import Vector as V +from mathutils.geometry import intersect_point_line +from numpy import array, all as np_all, newaxis +from numpy.linalg import norm as np_norm + +def compute_distance(point, line, line_end, tolerance): + '''call to the mathuutils function''' + inter_p = intersect_point_line(point, line, line_end) + dist = (inter_p[0] - point).length + segment_percent = inter_p[1] + is_in_line = dist < tolerance + closest_in_segment = 0 <= segment_percent <= 1 + is_in_segment = is_in_line and closest_in_segment + return dist, is_in_segment, is_in_line, list(inter_p[0]), closest_in_segment + + +def compute_distances_mu(line, pts, result, gates, tolerance): + '''calculate all distances with mathuutils''' + line_origin = V(line[0]) + line_end = V(line[-1]) + local_result = [[], [], [], [], []] + for point in pts: + data = compute_distance(V(point), line_origin, line_end, tolerance) + for i, res in enumerate(local_result): + res.append(data[i]) + + for i, res in enumerate(result): + if gates[i]: + res.append(local_result[i]) + + +def compute_distances_np(line, pts, result, gates, tolerance): + '''calculate all distances with NumPy''' + # Adapted from https://math.stackexchange.com/questions/1905533/find-perpendicular-distance-from-point-to-line-in-3d + + np_pts = array(pts) + segment = V(line[-1]) - V(line[0]) + segment_length = segment.length + line_direction = segment / segment_length + vect = np_pts - line[0] + vect_proy = vect.dot(line_direction) + closest_point = line[0] + vect_proy[:, newaxis] * line_direction + dif_v = closest_point - np_pts + dist = np_norm(dif_v, axis=1) + + is_in_segment = [] + is_in_line = [] + closest_in_segment = [] + if gates[4] or gates[1]: + closest_in_segment = np_all([vect_proy >= 0, vect_proy <= segment_length], axis=0) + if gates[1] or gates[2]: + np_tolerance = array(tolerance) + is_in_line = dist < tolerance + if gates[1]: + is_in_segment = np_all([closest_in_segment, is_in_line], axis=0) + + local_result = [dist, is_in_segment, is_in_line, closest_point, closest_in_segment] + + for i, res in enumerate(result): + if gates[i]: + res.append(local_result[i].tolist() if not gates[5] else local_result[i]) + + +class SvDistancePointLineNode(bpy.types.Node, SverchCustomTreeNode): + ''' + Triggers: Perpendicular to segment + Tooltip: Distance Point to line and closest point in the line + ''' + bl_idname = 'SvDistancePointLineNode' + bl_label = 'Distance Point Line' + bl_icon = 'OUTLINER_OB_EMPTY' + sv_icon = 'SV_DISTANCE' + + implentation_modes = [ + ("NumPy", "NumPy", "NumPy", 0), + ("MathUtils", "MathUtils", "MathUtils", 1)] + + compute_distances = { + "NumPy": compute_distances_np, + "MathUtils": compute_distances_mu} + + output_numpy = BoolProperty( + name='Output NumPy', description='Output NumPy arrays', + default=False, update=updateNode) + + implementation = EnumProperty( + name='Implementation', items=implentation_modes, + description='Choose calculation method', + default="NumPy", update=updateNode) + + tolerance = FloatProperty( + name="Tolerance", description='Intersection tolerance', + default=1.0e-6, min=0.0, precision=6, + update=updateNode) + + list_match_global = EnumProperty( + name="Match Global", + description="Behavior on different list lengths, multiple objects level", + items=list_match_modes, default="REPEAT", + update=updateNode) + + list_match_local = EnumProperty( + name="Match Local", + description="Behavior on different list lengths, object level", + items=list_match_modes, default="REPEAT", + update=updateNode) + + def sv_init(self, context): + '''create sockets''' + sinw = self.inputs.new + sonw = self.outputs.new + sinw('VerticesSocket', "Vertices") + sinw('VerticesSocket', "Verts Line") + sinw('StringsSocket', "Tolerance").prop_name = 'tolerance' + + sonw('StringsSocket', "Distance") + sonw('StringsSocket', "In Segment") + sonw('StringsSocket', "In Line") + sonw('VerticesSocket', "Closest Point") + sonw('StringsSocket', "Closest in Segment") + + def draw_buttons_ext(self, context, layout): + '''draw buttons on the N-panel''' + layout.label(text="Implementation:") + layout.prop(self, "implementation", expand=True) + if self.implementation == "NumPy": + layout.prop(self, "output_numpy", toggle=False) + layout.separator() + layout.label(text="List Match:") + layout.prop(self, "list_match_global", text="Global Match", expand=False) + layout.prop(self, "list_match_local", text="Local Match", expand=False) + + def rclick_menu(self, context, layout): + '''right click sv_menu items''' + layout.prop_menu_enum(self, "implementation", text="Implementation") + if self.implementation == "NumPy": + layout.prop(self, "output_numpy", toggle=False) + layout.prop_menu_enum(self, "list_match_global", text="List Match Global") + layout.prop_menu_enum(self, "list_match_local", text="List Match Local") + + + def get_data(self): + '''get all data from sockets and match lengths''' + si = self.inputs + return list_match_func[self.list_match_global]([s.sv_get(default=[[]]) for s in si]) + + def process(self): + '''main node function called every update''' + so = self.outputs + si = self.inputs + if not (any(s.is_linked for s in so) and all(s.is_linked for s in si[:2])): + return + + result = [[] for socket in so] + gates = [socket.is_linked for socket in so] + + gates.append(self.output_numpy) + + group = self.get_data() + main_func = self.compute_distances[self.implementation] + match_func = list_match_func[self.list_match_local] + + for pts, line, tolerance in zip(*group): + if len(tolerance) > 1: + pts, tolerance = match_func([pts, tolerance]) + main_func(line, pts, result, gates, tolerance) + + for i, r in enumerate(result): + if gates[i]: + so[i].sv_set(r) + + +def register(): + '''register class in Blender''' + bpy.utils.register_class(SvDistancePointLineNode) + + +def unregister(): + '''unregister class in Blender''' + bpy.utils.unregister_class(SvDistancePointLineNode) diff --git a/ui/nodeview_rclick_menu.py b/ui/nodeview_rclick_menu.py index 682c28fc0..0cd267ce2 100644 --- a/ui/nodeview_rclick_menu.py +++ b/ui/nodeview_rclick_menu.py @@ -1,7 +1,7 @@ # This file is part of project Sverchok. It's copyrighted by the contributors # recorded in the version control history of the file, available from # its original location https://github.com/nortikin/sverchok/commit/master -# +# # SPDX-License-Identifier: GPL3 # License-Filename: LICENSE @@ -162,17 +162,15 @@ class SvNodeviewRClickMenu(bpy.types.Menu): else: if has_outputs(node): layout.operator("node.sv_deligate_operator", text="Connect ViewerDraw").fn = "vdmk2" - # layout.operator("node.sv_deligate_operator", text="Connect ViewerDraw + IDX").fn="vdmk2 + idxv" + layout.operator("node.sv_deligate_operator", text="Connect stethoscope").fn = "stethoscope" if hasattr(node, "rclick_menu"): + layout.separator() node.rclick_menu(context, layout) else: layout.menu("NODEVIEW_MT_Dynamic_Menu", text='node menu') - if node and len(node.outputs): - layout.operator("node.sv_deligate_operator", text="Connect stethoscope").fn = "stethoscope" - if node and node.bl_idname == 'NodeFrame': # give options for Frame nodes.. col = layout.column(align=True) -- GitLab From 3a6e8821ac88ff10932d18d8f54e3e194d3fbac2 Mon Sep 17 00:00:00 2001 From: Ilya Portnov Date: Sat, 25 May 2019 18:04:57 +0500 Subject: [PATCH 113/137] Minor updates. --- nodes/vector/linear_approx.py | 7 ++++++- utils/geom.py | 24 ++++++++++++++++++++++++ 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/nodes/vector/linear_approx.py b/nodes/vector/linear_approx.py index 0eabfab02..a79b0189a 100644 --- a/nodes/vector/linear_approx.py +++ b/nodes/vector/linear_approx.py @@ -25,7 +25,10 @@ from sverchok.data_structure import (updateNode) from sverchok.utils.geom import linear_approximation class SvLinearApproxNode(bpy.types.Node, SverchCustomTreeNode): - ''' Linear Approximation ''' + """ + Triggers: Linear Approximation + Tooltip: Approximate vertices with straight line or plane. + """ bl_idname = 'SvLinearApproxNode' bl_label = 'Linear Approximation' bl_icon = 'OUTLINER_OB_EMPTY' @@ -55,6 +58,8 @@ class SvLinearApproxNode(bpy.types.Node, SverchCustomTreeNode): self.outputs.new('VerticesSocket', "Diffs") self.outputs.new('StringsSocket', "Distances") + self.update_mode(context) + def draw_buttons(self, context, layout): layout.prop(self, 'mode') diff --git a/utils/geom.py b/utils/geom.py index adcaecf83..6b65ad139 100644 --- a/utils/geom.py +++ b/utils/geom.py @@ -920,6 +920,17 @@ class PlaneEquation(object): return PlaneEquation.from_normal_and_point((a, b, c), p1) + @classmethod + def from_coordinate_plane(cls, plane_name): + if plane_name == 'XY': + return PlaneEquation(0, 0, 1, 0) + elif plane_name == 'YZ': + return PlaneEquation(1, 0, 0, 0) + elif plane_name == 'XZ': + return PlaneEquation(0, 1, 0, 0) + else: + raise Exception("Unknown coordinate plane name") + def normalized(self): """ Return equation, which defines exactly the same plane, but with coefficients adjusted so that @@ -929,6 +940,8 @@ class PlaneEquation(object): holds. """ normal = self.normal.length + if abs(normal) < 1e-8: + raise Exception("Normal of the plane is (nearly) zero: ({}, {}, {})".format(self.a, self.b, self.c)) return PlaneEquation(a/normal, b/normal, c/normal, d/normal) def check(self, point, eps=1e-6): @@ -1077,6 +1090,17 @@ class LineEquation(object): return LineEquation(a, b, c, p1) + @classmethod + def from_coordinate_axis(cls, axis_name): + if axis_name == 'X': + return LineEquation(1, 0, 0, (0, 0, 0)) + elif axis_name == 'Y': + return LineEquation(0, 1, 0, (0, 0, 0)) + elif axis_name == 'Z': + return LineEquation(0, 0, 1, (0, 0, 0)) + else: + raise Exception("Unknown axis name") + def check(self, point, eps=1e-6): """ Check if the specified point belongs to the line. -- GitLab From 4f79536b00b0e973d40d485679a2e1f68afba946 Mon Sep 17 00:00:00 2001 From: Ilya Portnov Date: Sat, 25 May 2019 21:49:16 +0500 Subject: [PATCH 114/137] Updates / fixes. --- utils/geom.py | 151 ++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 135 insertions(+), 16 deletions(-) diff --git a/utils/geom.py b/utils/geom.py index 6b65ad139..ec7c72ad6 100644 --- a/utils/geom.py +++ b/utils/geom.py @@ -40,6 +40,7 @@ from mathutils import Matrix from sverchok.utils.sv_bmesh_utils import bmesh_from_pydata from sverchok.utils.sv_bmesh_utils import pydata_from_bmesh from sverchok.data_structure import match_long_repeat +from sverchok.utils.logging import debug, info identity_matrix = Matrix() @@ -753,7 +754,7 @@ class Spline2D(object): norm = np.linalg.norm(n) if norm != 0: n = n / norm - #print("DU: {}, DV: {}, N: {}".format(du, dv, n)) + #debug("DU: {}, DV: {}, N: {}".format(du, dv, n)) result = tuple(n) self._normal_cache[(u,v)] = result return result @@ -773,7 +774,7 @@ class GenerateLookup(): self.acquire_lookup_table() self.get_buckets() # for idx, (k, v) in enumerate(sorted(self.lookup.items())): - # print(k, v) + # debug(k, v) def find_bucket(self, factor): for bucket_min, bucket_max in zip(self.buckets[:-1], self.buckets[1:]): @@ -953,12 +954,32 @@ class PlaneEquation(object): value = a*x + b*y + c*z + d return abs(value) < eps + def two_vectors(self): + """ + Return two vectors that are parallel two this plane. + Note: the two vectors returned are orthogonal. + + output: (Vector, Vector) + """ + v1 = self.normal.orthogonal() + v2 = v1.cross(self.normal) + return v1, v2 + + def evaluate(self, u, v): + """ + Return a point on the plane by it's UV coordinates. + UV coordinates origin is self.point. + Orientation of UV coordinates system is undefined. + Scale of UV coordinates system is defined by coordinates + of self.normal. One can use plane.normalized().evaluate() + two make sure that the scale of UV coordinates system is 1:1. + + input: two floats. + output: Vector. + """ p0 = self.nearest_point_to_origin() - a, b, c, d = self.a, self.b, self.c, self.d - v1 = mathutils.Vector((-b, a, c)) - v2 = mathutils.Vector((a, -c, b)) - + v1, v2 = self.two_vectors() return p0 + u*v1 + v*v2 @property @@ -972,16 +993,31 @@ class PlaneEquation(object): self.c = normal[2] def nearest_point_to_origin(self): + """ + Returns the point on plane which is the nearest + to the origin (0, 0, 0). + output: Vector. + """ a, b, c, d = self.a, self.b, self.c, self.d sqr = a*a + b*b + c*c return mathutils.Vector(((- a*d)/sqr, (- b*d)/sqr, (- c*d)/sqr)) def distance_to_point(self, point): + """ + Return distance from specified point to this plane. + input: Vector or 3-tuple + output: float. + """ point_on_plane = self.nearest_point_to_origin() return mathutils.geometry.distance_point_to_plane(mathutils.Vector(point), point_on_plane, self.normal) def intersect_with_line(self, line): - a, b, c, d = self.a, self.b, self.c, self.d + """ + Calculate intersection between this plane and specified line. + input: line - an instance of LineEquation. + output: Vector. + """ + a, b, c = line.a, line.b, line.c x0, y0, z0 = line.x0, line.y0, line.z0 # Here we numerically solve the system of linear equations: @@ -990,7 +1026,7 @@ class PlaneEquation(object): # | ------ = ------ = ------, (line) # / A B C (*) # ` - # | A x + B y + C z + D = 0 (plane) + # | Ap x + Bp y + Cp z + Dp = 0 (plane) # ` # # with relation to x, y, z. @@ -1003,48 +1039,50 @@ class PlaneEquation(object): # # B (x - x0) = A (y - y0), # C (y - y0) = B (z - z0), - # A x + B x + C z + D = 0. + # Ap x + Bp x + Cp z + Dp = 0. # # But, if B == 0, then this representation will contain # two exactly equivalent equations: # # 0 = A (y - y0), # C (y - y0) = 0, - # A x + 0 + C z + D = 0. + # Ap x + 0 + Cp z + Dp = 0. # # In this case, the system will become singular; so # we must choose another representation of (*) system. epsilon = 1e-8 + #info("Line: %s", line) + if abs(a) > epsilon: matrix = np.array([ [b, -a, 0], [c, 0, -a], - [a, b, c]]) + [self.a, self.b, self.c]]) free = np.array([ b*x0 - a*y0, c*x0 - a*z0, - -d]) + -self.d]) elif abs(b) > epsilon: matrix = np.array([ [b, -a, 0], [0, c, -b], - [a, b, c]]) + [self.a, self.b, self.c]]) free = np.array([ b*x0 - a*y0, c*y0 - b*z0, - -d]) + -self.d]) elif abs(c) > epsilon: matrix = np.array([ [c, 0, -a], [0, c, -b], - [a, b, c]]) + [self.a, self.b, self.c]]) free = np.array([ c*x0 - a*z0, c*y0 - b*z0, - -d]) + -self.d]) else: raise Exception("Invalid plane: all coefficients are (nearly) zero: {}, {}, {}".format(a, b, c)) @@ -1056,6 +1094,11 @@ class PlaneEquation(object): return mathutils.Vector((x, y, z)) def projection_of_point(self, point): + """ + Return a projection of specified point to this plane. + input: Vector. + output: Vector. + """ # Draw a line, which has the same direction vector # as this plane's normal, and which contains the # given point. @@ -1063,6 +1106,62 @@ class PlaneEquation(object): # Then find the intersection of this plane with that line. return self.intersect_with_line(line) + def intersect_with_plane(self, plane2): + """ + Return an intersection of this plane with another one. + + input: PlaneEquation + output: LineEquation or None, in case two planes are parallel. + """ + if self.is_parallel(plane2): + debug("{} is parallel to {}".format(self, plane2)) + return None + + # We need an arbitrary point on this plane and two vectors. + # Draw two lines in this plane and see for theirs intersection + # with another plane. + p0 = self.nearest_point_to_origin() + v1, v2 = self.two_vectors() + line1 = LineEquation.from_direction_and_point(v1, p0) + line2 = LineEquation.from_direction_and_point(v2, p0) + + # it might be that one of vectors we chose is parallel to plane2 + # (since we are choosing them arbitrarily); but from the way + # we are choosing v1 and v2, we know they are orthogonal. + # So if wee just rotate them by pi/4, they will no longer be + # parallel to plane2. + if plane2.is_parallel(line1) or plane2.is_parallel(line2): + v1_new = v1 + v2 + v2_new = v1 - v2 + debug("{}, {} => {}, {}".format(v1, v2, v1_new, v2_new)) + line1 = LineEquation.from_direction_and_point(v1_new, p0) + line2 = LineEquation.from_direction_and_point(v2_new, p0) + + info("line1: %s", line1) + info("line2: %s", line2) + + p1 = plane2.intersect_with_line(line1) + p2 = plane2.intersect_with_line(line2) + print("{} => {}".format(v1, p1)) + print("{} => {}".format(v2, p2)) + print("Plane2: {}".format(plane2)) + return LineEquation.from_two_points(p1, p2) + + def is_parallel(self, other): + """ + Check if other object is parallel to this plane. + input: PlaneEquation, LineEquation or Vector. + output: boolean. + """ + if isinstance(other, PlaneEquation): + return abs(self.normal.angle(other.normal)) < 1e-8 + elif isinstance(other, LineEquation): + return abs(self.normal.dot(other.direction)) < 1e-8 + elif isinstance(other, mathutils.Vector): + return abs(self.normal.dot(other)) < 1e-8 + else: + raise Exception("Don't know how to check is_parallel for {}".format(type(other))) + class LineEquation(object): """ An object, containing the coefficients A, B, C, x0, y0, z0 in the @@ -1074,6 +1173,9 @@ class LineEquation(object): """ def __init__(self, a, b, c, point): + epsilon = 1e-8 + if abs(a) < epsilon and abs(b) < epsilon and abs(c) < epsilon: + raise Exception("Direction is (nearly) zero: {}, {}, {}".format(a, b, c)) self.a = a self.b = b self.c = c @@ -1081,6 +1183,8 @@ class LineEquation(object): @classmethod def from_two_points(cls, p1, p2): + if (mathutils.Vector(p1) - mathutils.Vector(p2)).length < 1e-8: + raise Exception("Two points are (almost) the same: {}, {}".format(p1, p2)) x1, y1, z1 = p1[0], p1[1], p1[2] x2, y2, z2 = p2[0], p2[1], p2[2] @@ -1090,6 +1194,11 @@ class LineEquation(object): return LineEquation(a, b, c, p1) + @classmethod + def from_direction_and_point(cls, direction, point): + a, b, c = tuple(direction) + return LineEquation(a, b, c, point) + @classmethod def from_coordinate_axis(cls, axis_name): if axis_name == 'X': @@ -1155,10 +1264,20 @@ class LineEquation(object): return "(x - {})/{} = (y - {})/{} = (z - {})/{}".format(self.x0, self.a, self.y0, self.b, self.z0, self.c) def distance_to_point(self, point): + """ + Return the distance between the specified point and this line. + input: Vector or 3-tuple. + output: float. + """ # TODO: there should be more effective way to do this return self.projection_of_point(point).length def projection_of_point(self, point): + """ + Return the projection of the specified point on this line. + input: Vector or 3-tuple. + output: Vector. + """ # Draw a plane, which has the same normal as # this line's direction vector, and which contains the # given point -- GitLab From 12503cf76c39e61262778ff9b1031649914c6da4 Mon Sep 17 00:00:00 2001 From: Ilya Portnov Date: Sat, 25 May 2019 22:23:50 +0500 Subject: [PATCH 115/137] Documentation. --- docs/nodes/vector/linear_approx.rst | 66 +++++++++++++++++++++++++++++ utils/geom.py | 18 +++++--- 2 files changed, 78 insertions(+), 6 deletions(-) create mode 100644 docs/nodes/vector/linear_approx.rst diff --git a/docs/nodes/vector/linear_approx.rst b/docs/nodes/vector/linear_approx.rst new file mode 100644 index 000000000..282e85337 --- /dev/null +++ b/docs/nodes/vector/linear_approx.rst @@ -0,0 +1,66 @@ +Linear Approximation +==================== + +Functionality +------------- + +This node tries to approximate provided set of vertices by either a plane or +a straight line. In other words, it searches for such a plane / line, that all +provided vertices have the minimum distance to it. + +More technically, it searches for a plane or a line A, such that + +.. image:: https://user-images.githubusercontent.com/284644/58372582-e6e25700-7f38-11e9-844f-aaa4fa2bb562.gif + +The plane is represented by a point on the plane and a normal vector. + +The line is represented by a point on the line and a direction vector. + +That point on line or plane is calculated as a geometrical center of all +provided vertices. + +Inputs +------ + +This node has one input: **Vertices** - the vertices to be approximated. + +Parameters +---------- + +This node has one parameter - **Mode**. Two modes are supported: + +* **Line** - approximate vertices by straight line. This is the default mode. +* **Plane** - approximate vertices by a plane. + +Outputs +------- + +This node has the following outputs: + +* **Center** - the point on the line or plane. This is geometrical center of all input vertices. +* **Normal** - the normal vector of a plane. This output is only available in the **Plane** mode. +* **Direction** - the direction vector of a line. This output is only available in the **Line** mode. +* **Projections** - the projections of input vertices to the line or plane. +* **Diffs** - difference vectors between the input vertices and their projections to line or plane. +* **Distances** - distances between the input vertices and their projections to line or plane. + +Examples of usage +----------------- + +The simplest example: approximate 3D curve by a line. Here black curve is a +grease pencil, green one - it's representation in Sverchok, red line - result +of linear approximation of green line. + +.. image:: https://user-images.githubusercontent.com/284644/58330560-8e378f00-7e50-11e9-9bf5-8612c420ed91.png + +A simple example with plane. + +.. image:: https://user-images.githubusercontent.com/284644/58274029-63472f80-7dab-11e9-9c8b-1953633cf2be.png + +The node can calculate approximation for several sets of vertices at once: + +.. image:: https://user-images.githubusercontent.com/284644/58273750-cd130980-7daa-11e9-8f99-3ec57b37965c.png + + +You can also find more examples and some discussion `in the development thread `_. + diff --git a/utils/geom.py b/utils/geom.py index ec7c72ad6..ad7aa7d6c 100644 --- a/utils/geom.py +++ b/utils/geom.py @@ -1137,14 +1137,8 @@ class PlaneEquation(object): line1 = LineEquation.from_direction_and_point(v1_new, p0) line2 = LineEquation.from_direction_and_point(v2_new, p0) - info("line1: %s", line1) - info("line2: %s", line2) - p1 = plane2.intersect_with_line(line1) p2 = plane2.intersect_with_line(line2) - print("{} => {}".format(v1, p1)) - print("{} => {}".format(v2, p2)) - print("Plane2: {}".format(plane2)) return LineEquation.from_two_points(p1, p2) def is_parallel(self, other): @@ -1347,6 +1341,18 @@ def linear_approximation(data): syz = sum(y*z for (y,z) in zip(ys,zs)) n = len(data) + + # This is not that trivial, one can show that + # eigenvalues and eigenvectors of a matrix composed + # this way will provide exactly the solutions of + # least squares problem for input vertices. + # The nice part is that by calculating these values + # we obtain both approximations - by line and by plane - + # at the same time. The eigenvector which corresponds to + # the minimal of eigenvalues will provide a normal for + # the approximating plane. The eigenvector which corresponds + # to the maximal of eigenvalues will provide a direction + # for the approximating line. matrix = np.array([ [sx2, sxy, sxz], -- GitLab From b9451698db21250a6392dd111be540239e35dfcd Mon Sep 17 00:00:00 2001 From: Ilya Portnov Date: Sun, 26 May 2019 12:24:13 +0500 Subject: [PATCH 116/137] Opmitization for distances to multiple points. --- nodes/vector/linear_approx.py | 3 +-- utils/geom.py | 19 +++++++++++++++++++ 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/nodes/vector/linear_approx.py b/nodes/vector/linear_approx.py index a79b0189a..562e89484 100644 --- a/nodes/vector/linear_approx.py +++ b/nodes/vector/linear_approx.py @@ -105,13 +105,12 @@ class SvLinearApproxNode(bpy.types.Node, SverchCustomTreeNode): projections = [] diffs = [] - distances = [] + distances = list(map(float, list(plane.distance_to_points(vertices)))) for vertex in vertices: projection = plane.projection_of_point(vertex) projections.append(tuple(projection)) diff = projection - Vector(vertex) diffs.append(tuple(diff)) - distances.append(diff.length) out_projections.append(projections) out_diffs.append(diffs) diff --git a/utils/geom.py b/utils/geom.py index ad7aa7d6c..0a9f2b73d 100644 --- a/utils/geom.py +++ b/utils/geom.py @@ -1011,6 +1011,25 @@ class PlaneEquation(object): point_on_plane = self.nearest_point_to_origin() return mathutils.geometry.distance_point_to_plane(mathutils.Vector(point), point_on_plane, self.normal) + def distance_to_points(self, points): + """ + Return distances from specified points to this plane. + input: list of 3-tuples, or numpy array of same shape + output: numpy array of floats. + """ + # Distance from (x,y,z) to the plane is given by formula: + # + # | A x + B y + C z + D | + # rho = ------------------------- + # sqrt(A^2 + B^2 + C^2) + # + points = np.array(points) + a, b, c, d = self.a, self.b, self.c, self.d + # (A x + B y + C z) is a scalar product of (x, y, z) and (A, B, C) + numerators = abs(points.dot([a, b, c]) + d) + denominator = math.sqrt(a*a + b*b + c*c) + return numerators / denominator + def intersect_with_line(self, line): """ Calculate intersection between this plane and specified line. -- GitLab From 659b9e788d3ea366ec479239d61a076bd321f7f3 Mon Sep 17 00:00:00 2001 From: Ilya Portnov Date: Sun, 26 May 2019 13:16:50 +0500 Subject: [PATCH 117/137] Unit tests for the new API. --- tests/linear_approximation_tests.py | 125 ++++++++++++++++++++++++++++ utils/geom.py | 14 +++- 2 files changed, 137 insertions(+), 2 deletions(-) create mode 100644 tests/linear_approximation_tests.py diff --git a/tests/linear_approximation_tests.py b/tests/linear_approximation_tests.py new file mode 100644 index 000000000..456d67c66 --- /dev/null +++ b/tests/linear_approximation_tests.py @@ -0,0 +1,125 @@ + +import numpy as np +from sverchok.utils.testing import * +from sverchok.utils.logging import debug, info +from sverchok.utils.geom import PlaneEquation, LineEquation, linear_approximation + +class PlaneTests(SverchokTestCase): + def test_plane_from_three_points(self): + p1 = (1, 0, 0) + p2 = (0, 1, 0) + p3 = (0, 0, 1) + plane = PlaneEquation.from_three_points(p1, p2, p3) + self.assertEquals(plane.a, 1) + self.assertEquals(plane.b, 1) + self.assertEquals(plane.c, 1) + self.assertEquals(plane.d, -1) + + def test_nearest_to_origin(self): + p1 = (1, 0, 0) + p2 = (0, 1, 0) + p3 = (0, 0, 1) + plane = PlaneEquation.from_three_points(p1, p2, p3) + p = plane.nearest_point_to_origin() + self.assert_sverchok_data_equal(tuple(p), (0.3333, 0.3333, 0.3333), precision=4) + + def test_check_yes(self): + plane = PlaneEquation.from_coordinate_plane('XY') + p = (7, 8, 0) + self.assertTrue(plane.check(p)) + + def test_check_no(self): + plane = PlaneEquation.from_coordinate_plane('XY') + p = (7, 8, 1) + self.assertFalse(plane.check(p)) + + def test_two_vectors(self): + p1 = (2, 0, 0) + p2 = (0, 1, 0) + p3 = (0, 0, 2) + plane = PlaneEquation.from_three_points(p1, p2, p3) + + normal = plane.normal + v1, v2 = plane.two_vectors() + + self.assertTrue(abs(normal.dot(v1)) < 1e-8) + self.assertTrue(abs(normal.dot(v2)) < 1e-8) + self.assertTrue(abs(v1.dot(v2)) < 1e-8) + + def test_distance_to_point(self): + plane = PlaneEquation.from_coordinate_plane('XY') + point = (1, 2, 3) + distance = plane.distance_to_point(point) + self.assertEquals(distance, 3) + + def test_distance_to_points(self): + plane = PlaneEquation.from_coordinate_plane('XY') + points = [(1, 2, 3), (4, 5, 6)] + distances = plane.distance_to_points(points) + self.assert_numpy_arrays_equal(distances, np.array([3, 6])) + + def test_intersect_with_line(self): + plane = PlaneEquation.from_coordinate_plane('XY') + line = LineEquation.from_direction_and_point((1, 1, 1), (1, 1, 1)) + point = plane.intersect_with_line(line) + self.assert_sverchok_data_equal(tuple(point), (0, 0, 0)) + + def test_intersect_with_plane(self): + plane1 = PlaneEquation.from_coordinate_plane('XY') + plane2 = PlaneEquation.from_coordinate_plane('XZ') + line = plane1.intersect_with_plane(plane2) + self.assert_sverchok_data_equal(tuple(line.direction.normalized()), (1, 0, 0)) + self.assert_sverchok_data_equal(tuple(line.point), (0, 0, 0)) + +class LineTests(SverchokTestCase): + def test_from_two_points(self): + p1 = (1, 1, 1) + p2 = (3, 3, 3) + line = LineEquation.from_two_points(p1, p2) + self.assert_sverchok_data_equal(tuple(line.direction), (2, 2, 2)) + self.assert_sverchok_data_equal(tuple(line.point), p1) + + def test_check_yes(self): + p1 = (1, 1, 1) + p2 = (3, 3, 3) + line = LineEquation.from_two_points(p1, p2) + p3 = (5, 5, 5) + self.assertTrue(line.check(p3)) + + def test_check_no(self): + p1 = (1, 1, 1) + p2 = (3, 3, 3) + line = LineEquation.from_two_points(p1, p2) + p3 = (5, 5, 6) + self.assertFalse(line.check(p3)) + + def test_distance_to_point(self): + line = LineEquation.from_coordinate_axis('Z') + point = (0, 2, 0) + self.assertEquals(line.distance_to_point(point), 2) + +class LinearApproximationTests(SverchokTestCase): + def test_approximate_line_1(self): + p1 = (0, 0, 0) + p2 = (1, 0, 0) + p3 = (2, 0, 0) + p4 = (3, 0, 0) + line = linear_approximation([p1, p2, p3, p4]).most_similar_line() + self.assert_sverchok_data_equal(tuple(line.direction.normalized()), (1, 0, 0), precision=5) + + def test_approximate_line_2(self): + p1 = (0, -1, 0) + p2 = (1, 1, 0) + p3 = (2, -1, 0) + p4 = (3, 1, 0) + line = linear_approximation([p1, p2, p3, p4]).most_similar_line() + self.assert_sverchok_data_equal(tuple(line.direction), (0.7882054448127747, 0.6154122352600098, 0.0), precision=5) + + def test_approximate_plane(self): + p1 = (0, -1, 0) + p2 = (1, 1, 0) + p3 = (2, -1, 0) + p4 = (3, 1, 0) + plane = linear_approximation([p1, p2, p3, p4]).most_similar_plane() + self.assert_sverchok_data_equal(tuple(plane.normal.normalized()), (0, 0, 1), precision=5) + diff --git a/utils/geom.py b/utils/geom.py index 0a9f2b73d..91fe5fdb9 100644 --- a/utils/geom.py +++ b/utils/geom.py @@ -1141,6 +1141,15 @@ class PlaneEquation(object): # with another plane. p0 = self.nearest_point_to_origin() v1, v2 = self.two_vectors() + # it might be that p0 belongs to plane2; in that case we choose + # another point in the same plane + if plane2.check(p0): + # Since v1 and v2 are orthogonal, it may not be that they are + # both parallel to plane2. + if not plane2.is_parallel(v1): + p0 = p0 + v1 + else: + p0 = p0 + v2 line1 = LineEquation.from_direction_and_point(v1, p0) line2 = LineEquation.from_direction_and_point(v2, p0) @@ -1152,7 +1161,7 @@ class PlaneEquation(object): if plane2.is_parallel(line1) or plane2.is_parallel(line2): v1_new = v1 + v2 v2_new = v1 - v2 - debug("{}, {} => {}, {}".format(v1, v2, v1_new, v2_new)) + info("{}, {} => {}, {}".format(v1, v2, v1_new, v2_new)) line1 = LineEquation.from_direction_and_point(v1_new, p0) line2 = LineEquation.from_direction_and_point(v2_new, p0) @@ -1283,7 +1292,8 @@ class LineEquation(object): output: float. """ # TODO: there should be more effective way to do this - return self.projection_of_point(point).length + projection = self.projection_of_point(point) + return (mathutils.Vector(point) - projection).length def projection_of_point(self, point): """ -- GitLab From 740811d05dac20721099c7efb017037f7ece8a19 Mon Sep 17 00:00:00 2001 From: Ilya Portnov Date: Mon, 27 May 2019 22:40:18 +0500 Subject: [PATCH 118/137] Linear approximation: optimizations. Extend geom.py API with vector-wise operations. --- nodes/vector/linear_approx.py | 11 ++--- tests/linear_approximation_tests.py | 39 +++++++++++++++++ utils/geom.py | 65 +++++++++++++++++++++++++---- 3 files changed, 102 insertions(+), 13 deletions(-) diff --git a/nodes/vector/linear_approx.py b/nodes/vector/linear_approx.py index 562e89484..371323429 100644 --- a/nodes/vector/linear_approx.py +++ b/nodes/vector/linear_approx.py @@ -17,6 +17,7 @@ # ##### END GPL LICENSE BLOCK ##### import bpy +import numpy as np from bpy.props import EnumProperty from mathutils import Vector @@ -106,11 +107,11 @@ class SvLinearApproxNode(bpy.types.Node, SverchCustomTreeNode): projections = [] diffs = [] distances = list(map(float, list(plane.distance_to_points(vertices)))) - for vertex in vertices: - projection = plane.projection_of_point(vertex) - projections.append(tuple(projection)) - diff = projection - Vector(vertex) - diffs.append(tuple(diff)) + projections_np = plane.projection_of_points(vertices) + vertices_np = np.array(vertices) + projections = list(map(tuple, list(projections_np))) + diffs_np = projections_np - vertices_np + diffs = list(map(tuple, list(diffs_np))) out_projections.append(projections) out_diffs.append(diffs) diff --git a/tests/linear_approximation_tests.py b/tests/linear_approximation_tests.py index 456d67c66..9433bb03f 100644 --- a/tests/linear_approximation_tests.py +++ b/tests/linear_approximation_tests.py @@ -52,6 +52,45 @@ class PlaneTests(SverchokTestCase): distance = plane.distance_to_point(point) self.assertEquals(distance, 3) + def test_projection_1(self): + plane = PlaneEquation.from_coordinate_plane('XY') + point = (1, 2, 3) + result = plane.projection_of_point(point) + self.assert_sverchok_data_equal(tuple(result), (1, 2, 0)) + + def test_projection_2(self): + p1 = (1, 0, 0) + p2 = (0, 1, 0) + p3 = (0, 0, 1) + plane = PlaneEquation.from_three_points(p1, p2, p3) + point = (3, 3, 3) + result = plane.projection_of_point(point) + self.assert_sverchok_data_equal(tuple(result), (0.3333, 0.3333, 0.3333), precision=4) + + def test_projection_3(self): + plane = PlaneEquation.from_coordinate_plane('XY') + point1 = (1, 2, 3) + point2 = (4, 5, 6) + point3 = (7, 8, 9) + point4 = (2, 5, 9) + result = plane.projection_of_points([point1, point2, point3, point4]) + expected = np.array([[1, 2, 0], [4, 5, 0], [7, 8, 0], [2, 5, 0]]) + self.assert_numpy_arrays_equal(result, expected) + + def test_projection_4(self): + p1 = (1, 0, 0) + p2 = (0, 1, 0) + p3 = (0, 0, 1) + plane = PlaneEquation.from_three_points(p1, p2, p3) + point1 = (-3, -3, -3) + point2 = (2, 1, 1) + point3 = (1, 1, 2) + point4 = (1, 2, 1) + result = plane.projection_of_points([point1, point2, point3, point4]) + expected = np.array([[0.3333, 0.3333, 0.3333], [1,0,0], [0, 0, 1], [0, 1, 0]]) + info(result) + self.assert_numpy_arrays_equal(result, expected, precision=4) + def test_distance_to_points(self): plane = PlaneEquation.from_coordinate_plane('XY') points = [(1, 2, 3), (4, 5, 6)] diff --git a/utils/geom.py b/utils/geom.py index 91fe5fdb9..b89605455 100644 --- a/utils/geom.py +++ b/utils/geom.py @@ -973,7 +973,7 @@ class PlaneEquation(object): Orientation of UV coordinates system is undefined. Scale of UV coordinates system is defined by coordinates of self.normal. One can use plane.normalized().evaluate() - two make sure that the scale of UV coordinates system is 1:1. + to make sure that the scale of UV coordinates system is 1:1. input: two floats. output: Vector. @@ -1112,18 +1112,67 @@ class PlaneEquation(object): x, y, z = result[0], result[1], result[2] return mathutils.Vector((x, y, z)) + def side_of_point(self, point, eps=1e-8): + """ + Determine the side on which the point is with relation to this plane. + + input: Vector or 3-tuple or numpy array of same shape + output: +1 if the point is at one side of the plane; + -1 if the point is at another side; + 0 if the point belongs to the plane. + "Positive" side of the plane is defined by direction of + normal vector. + """ + a, b, c, d = self.a, self.b, self.c, self.d + x, y, z = point[0], point[1], point[2] + value = a*x + b*y + c*z + d + if abs(value) < eps: + return 0 + elif value > 0: + return +1 + else: + return -1 + + def side_of_points(self, points): + """ + For each point, determine the side on which the point is with relation to this plane. + + input: numpy array of shape (n, 3) + output: numpy array of shape (n,): + +1 if the point is at one side of the plane; + -1 if the point is at another side; + 0 if the point belongs to the plane. + "Positive" side of the plane is defined by direction of + normal vector. + """ + a, b, c, d = self.a, self.b, self.c, self.d + values = points.dot([a,b,c]) + d + return np.sign(values) + def projection_of_point(self, point): """ Return a projection of specified point to this plane. - input: Vector. + input: Vector or 3-tuple. output: Vector. """ - # Draw a line, which has the same direction vector - # as this plane's normal, and which contains the - # given point. - line = LineEquation(self.a, self.b, self.c, point) - # Then find the intersection of this plane with that line. - return self.intersect_with_line(line) + normal = self.normal.normalized() + distance = self.distance_to_point(point) + sign = self.side_of_point(point) + return mathutils.Vector(point) - sign * distance * normal + + def projection_of_points(self, points): + """ + Return projections of specified points to this plane. + input: list of Vector or list of 3-tuples or numpy array of shape (n, 3). + output: numpy array of shape (n, 3). + """ + points = np.array(points) + normal = np.array(self.normal.normalized()) + distances = self.distance_to_points(points) + signs = self.side_of_points(points) + signed_distances = np.multiply(signs, distances) + scaled_normals = np.outer(signed_distances, normal) + return points - scaled_normals def intersect_with_plane(self, plane2): """ -- GitLab From 0afa3d161377df0a2999ad4ac5e87034b932a5c1 Mon Sep 17 00:00:00 2001 From: Victor Doval <10011941+vicdoval@users.noreply.github.com> Date: Thu, 30 May 2019 20:49:55 +0200 Subject: [PATCH 119/137] New Distance Point Plane Node (#2425) * New Distance Point Plane Node * New Distance Point Plane docs index update * added code documentation --- docs/nodes/analyzers/analyzers_index.rst | 1 + docs/nodes/analyzers/distance_point_line.rst | 2 +- docs/nodes/analyzers/distance_point_plane.rst | 80 ++++++ index.md | 1 + nodes/analyzer/distance_point_plane.py | 235 ++++++++++++++++++ 5 files changed, 318 insertions(+), 1 deletion(-) create mode 100644 docs/nodes/analyzers/distance_point_plane.rst create mode 100644 nodes/analyzer/distance_point_plane.py diff --git a/docs/nodes/analyzers/analyzers_index.rst b/docs/nodes/analyzers/analyzers_index.rst index fcdd62066..7be2eae9c 100644 --- a/docs/nodes/analyzers/analyzers_index.rst +++ b/docs/nodes/analyzers/analyzers_index.rst @@ -9,6 +9,7 @@ Analyzers bbox distance_pp distance_point_line + distance_point_plane deformation edge_angles kd_tree diff --git a/docs/nodes/analyzers/distance_point_line.rst b/docs/nodes/analyzers/distance_point_line.rst index d0721bff5..c58666c38 100644 --- a/docs/nodes/analyzers/distance_point_line.rst +++ b/docs/nodes/analyzers/distance_point_line.rst @@ -30,7 +30,7 @@ Inputs / Parameters +---------------------+-------------+----------------------------------------------------------------------+ | **Line Vertices** | Vertices | It will get the first and last vertices's to define the line segment | +---------------------+-------------+----------------------------------------------------------------------+ -| **Tolerance** | Float | Minimal distance to accept one point is intersecting. | +| **Tolerance** | Float | Minimal distance to accept one point is intersecting. | +---------------------+-------------+----------------------------------------------------------------------+ Advanced Parameters diff --git a/docs/nodes/analyzers/distance_point_plane.rst b/docs/nodes/analyzers/distance_point_plane.rst new file mode 100644 index 000000000..155fe9e94 --- /dev/null +++ b/docs/nodes/analyzers/distance_point_plane.rst @@ -0,0 +1,80 @@ +Distance Point Plane +==================== + +.. image:: https://github.com/vicdoval/sverchok/raw/docs_images/images_for_docs/analyzer/distance_point_plane/distance_point_plane_sverchok_blender_nodes.png + :alt: Distance_point_plane_procedural.PNG + +Functionality +------------- + +The node is designed to find the distance between a point and one plane. + +The plane is defined by three points. + +As extra results you can get from the node: + +- If the point is in the triangle. + +- If point is in the plane. + +- Closest point in the plane. + +- If the closest point is inside the triangle. + + + + +Inputs / Parameters +------------------- + + ++---------------------+-------------+---------------------------------------------------------------------------------------------+ +| Param | Type | Description | ++=====================+=============+=============================================================================================+ +| **Vertices** | Vertices | Points to calculate | ++---------------------+-------------+---------------------------------------------------------------------------------------------+ +| **Verts Plane** | Vertices | It will get the first three vertices of the input list to define the triangle and the plane | ++---------------------+-------------+---------------------------------------------------------------------------------------------+ +| **Tolerance** | Float | Minimal distance to accept one point is intersecting. | ++---------------------+-------------+---------------------------------------------------------------------------------------------+ + +Advanced Parameters +------------------- + +In the N-Panel (and on the right-click menu) you can find: + +**Implementation**: Choose between MathUtils and NumPy (Usually faster) + +**Output NumPy**: Get NumPy arrays in stead of regular lists (makes the node faster). Only in the NumPy implementation. + +**Match List Global**: Define how list with different lengths should be matched. Refers to the matching of groups (one plane per group) + +**Match List Local**: Define how list with different lengths should be matched. Refers to the matching of tolerances and vertices + +Outputs +------- + +**Distance**: Distance to the line. + +**In Triangle**: Returns True if the point coplanar and inside the triangle formed by the input vertices + +**In Plane**: Returns True if point is in the same plane as with input vertices. + +**Closest Point**: Returns the closest point in the plane + +**Closest in Triangle**": Returns true if the closest point is is in the same plane as with input vertices. + + +Example of usage +---------------- + +In this example the node is used to split the points depending of the side of the plane. Also the ones that are near to the plane (under intersection tolerance). + +.. image:: https://github.com/vicdoval/sverchok/raw/docs_images/images_for_docs/analyzer/distance_point_plane/distance_point_plane_sverchok_blender_parametric_plane_split.png + :alt: distance_point_plane_sverchok_blender_parametric_plane_split.png + +It can be used to project the geometry over a plane. + +.. image:: https://github.com/vicdoval/sverchok/raw/docs_images/images_for_docs/analyzer/distance_point_plane/distance_point_plane_sverchok_blender_project_on_plane.png + :alt: distance_point_plane_sverchok_blender_project_on_plane.png + diff --git a/index.md b/index.md index 9e593bd8f..639a764a3 100644 --- a/index.md +++ b/index.md @@ -53,6 +53,7 @@ AreaNode DistancePPNode SvDistancePointLineNode + SvDistancePointPlaneNode SvPathLengthNode CentersPolsNodeMK2 CentersPolsNodeMK3 diff --git a/nodes/analyzer/distance_point_plane.py b/nodes/analyzer/distance_point_plane.py new file mode 100644 index 000000000..084529f38 --- /dev/null +++ b/nodes/analyzer/distance_point_plane.py @@ -0,0 +1,235 @@ +# ##### 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, FloatProperty +from sverchok.node_tree import SverchCustomTreeNode +from sverchok.data_structure import updateNode, list_match_func, list_match_modes +from mathutils import Vector as V +from mathutils.geometry import intersect_point_line, distance_point_to_plane, normal, intersect_point_tri +import numpy as np + + +def compute_point_tri_dist(p, plane_origin, plane_a, plane_b, norm, tolerance): + + dist = distance_point_to_plane(p, plane_origin, norm) + closest = p - norm * dist + side = dist > 0 + dist_abs = abs(dist) + is_in_plane = dist_abs < tolerance + closest_in_plane = intersect_point_tri(closest, plane_origin , plane_a, plane_b) + is_in_segment = is_in_plane and closest_in_plane + return dist_abs, is_in_segment, is_in_plane, list(closest), bool(closest_in_plane), side + + +def compute_distances_mu(plane, pts, result, gates, tolerance): + plane_origin = V(plane[0]) + plane_a, plane_b = V(plane[1]), V(plane[2]) + norm = normal([plane_origin, plane_a, plane_b]) + if norm.length == 0: + print("Error: the three points of the plane are aligned. Not valid plane") + local_result = [[] for res in result] + for p in pts: + data = compute_point_tri_dist(V(p), plane_origin, plane_a, plane_b, norm, tolerance) + for i, r in enumerate(local_result): + r.append(data[i]) + + for i, res in enumerate(result): + if gates[i]: + res.append(local_result[i]) + +def barycentric_mask_np(pts, edges, np_pol_v, pol_normals, ed_id, tolerance): + '''Helper function to mask which points are inside the triangles''' + + edge = edges[ed_id, :] + triangle_vert = np_pol_v[ed_id, :] + vert_pts = pts - triangle_vert[np.newaxis, :] + cross = np.cross(edge[ np.newaxis, :], vert_pts) + + return np.sum(pol_normals * cross, axis=1) > -tolerance + +def pts_in_tris_np(pts, edges, pol_v, pol_normals, tolerance): + '''calculate if points are inside the triangles''' + w = barycentric_mask_np(pts, edges, pol_v, pol_normals, 0, tolerance) + u = barycentric_mask_np(pts, edges, pol_v, pol_normals, 1, tolerance) + v = barycentric_mask_np(pts, edges, pol_v, pol_normals, 2, tolerance) + return w * u * v + +def compute_distances_np(plane, pts, result, gates, tolerance): + '''The theory of this function was taken from "Optimizing The Computation Of Barycentric Coordinates" in + https://www.scratchapixel.com/lessons/3d-basic-rendering/ray-tracing-rendering-a-triangle/barycentric-coordinates''' + + np_plane_v = np.array(plane) + v1 = np_plane_v[1,:] - np_plane_v[0,:] + v2 = np_plane_v[2,:] - np_plane_v[0,:] + normals = np.cross(v1, v2) + normals_d = np.linalg.norm(normals, axis=0) + np_tolerance = np.array(tolerance) + if normals_d == 0: + print("Error: the three points of the plane are aligned. Not valid plane") + normals_n = normals / normals_d + edges = np.zeros(np_plane_v.shape, dtype=np.float32) + edges[ 0, :] = v1 + edges[ 1, :] = np_plane_v[ 2, :] - np_plane_v[ 1, :] + edges[ 2, :] = np_plane_v[ 0, :] - np_plane_v[ 2, :] + plane_co = np_plane_v[ 0, :] + np_pts = np.array(pts) + + vector_base = np_pts - plane_co + distance = np.sum(vector_base * normals_n, axis=1) + + closest = np_pts - normals_n[np.newaxis, :] * distance[:,np.newaxis] + side = (distance >= 0 ) if gates[5] else [] + dist_abs = np.abs(distance) + + is_in_triangle = [] + is_in_plane = [] + closest_in_tri = [] + + if gates[4] or gates[1]: + closest_in_tri = pts_in_tris_np(closest, edges, np_plane_v, normals_n, np_tolerance) + if gates[1] or gates[2]: + is_in_plane = dist_abs < np_tolerance + if gates[1]: + is_in_triangle = np.all([closest_in_tri, is_in_plane], axis=0) + + + local_result = [dist_abs, is_in_triangle, is_in_plane, closest, closest_in_tri, side] + + for i, r in enumerate(result): + if gates[i]: + r.append(local_result[i].tolist() if not gates[6] else local_result[i]) + + +class SvDistancePointPlaneNode(bpy.types.Node, SverchCustomTreeNode): + ''' + Triggers: Perpendicular to triangle + Tooltip: Distance Point to plane and closest point in the plane + ''' + bl_idname = 'SvDistancePointPlaneNode' + bl_label = 'Distance Point Plane' + bl_icon = 'OUTLINER_OB_EMPTY' + sv_icon = 'SV_DISTANCE' + + implentation_modes = [ + ("NumPy", "NumPy", "NumPy", 0), + ("MathUtils", "MathUtils", "MathUtils", 1)] + + compute_distances = { + "NumPy": compute_distances_np, + "MathUtils": compute_distances_mu} + + output_numpy = BoolProperty( + name='Output NumPy', description='Output NumPy arrays', + default=False, update=updateNode) + + implementation = EnumProperty( + name='Implementation', items=implentation_modes, + description='Choose calculation method', + default="NumPy", update=updateNode) + + tolerance = FloatProperty( + name="Tolerance", description='Intersection tolerance', + default=1.0e-6, min=0.0, precision=6, + update=updateNode) + + list_match_global = EnumProperty( + name="Match Global", + description="Behavior on different list lengths, multiple objects level", + items=list_match_modes, default="REPEAT", + update=updateNode) + + list_match_local = EnumProperty( + name="Match Local", + description="Behavior on different list lengths, object level", + items=list_match_modes, default="REPEAT", + update=updateNode) + + def sv_init(self, context): + '''create sockets''' + sinw = self.inputs.new + sonw = self.outputs.new + sinw('VerticesSocket', "Verts") + sinw('VerticesSocket', "Verts Plane") + sinw('StringsSocket', "Tolerance").prop_name = 'tolerance' + + sonw('StringsSocket', 'Distance') + sonw('StringsSocket', 'In Triangle') + sonw('StringsSocket', 'In Plane') + sonw('VerticesSocket', 'Closest Point') + sonw('StringsSocket', 'Closest in Triangle') + sonw('StringsSocket', 'Side') + + def draw_buttons_ext(self, context, layout): + '''draw buttons on the N-panel''' + layout.label(text="Implementation:") + layout.prop(self, "implementation", expand=True) + if self.implementation == "NumPy": + layout.prop(self, "output_numpy", toggle=False) + layout.label(text="List Match:") + layout.prop(self, "list_match_global", expand=False) + layout.prop(self, "list_match_local", expand=False) + + def rclick_menu(self, context, layout): + '''right click sv_menu items''' + layout.prop_menu_enum(self, "implementation", text="Implementation") + if self.implementation == "NumPy": + layout.prop(self, "output_numpy", toggle=False) + layout.prop_menu_enum(self, "list_match_global", text="List Match Global") + layout.prop_menu_enum(self, "list_match_local", text="List Match Local") + + + def get_data(self): + '''get all data from sockets''' + si = self.inputs + return list_match_func[self.list_match_global]([sckt.sv_get(default=[[]]) for sckt in si]) + + def process(self): + '''main node function called every update''' + so = self.outputs + si = self.inputs + if not (any(s.is_linked for s in so) and all(s.is_linked for s in si[:2])): + return + + result = [[] for socket in so] + gates = [socket.is_linked for socket in so] + gates.append(self.output_numpy) + + group = self.get_data() + main_func = self.compute_distances[self.implementation] + match_func = list_match_func[self.list_match_local] + + for pts, plane, tolerance in zip(*group): + if len(tolerance)>1: + pts, tolerance = match_func([pts, tolerance]) + main_func(plane, pts, result, gates, tolerance) + + for i, r in enumerate(result): + if gates[i]: + so[i].sv_set(result[i]) + + +def register(): + '''register class in Blender''' + bpy.utils.register_class(SvDistancePointPlaneNode) + + +def unregister(): + '''unregister class in Blender''' + bpy.utils.unregister_class(SvDistancePointPlaneNode) -- GitLab From a96019e8a8cd335fe1dc374a64e7c6ebd017bc14 Mon Sep 17 00:00:00 2001 From: Ilya Portnov Date: Sat, 1 Jun 2019 13:23:35 +0500 Subject: [PATCH 120/137] Fractal curve node vectorization. --- nodes/modifier_make/fractal_curve.py | 41 ++++++++++++++++++++-------- 1 file changed, 29 insertions(+), 12 deletions(-) diff --git a/nodes/modifier_make/fractal_curve.py b/nodes/modifier_make/fractal_curve.py index 07fc288a8..a16f945de 100644 --- a/nodes/modifier_make/fractal_curve.py +++ b/nodes/modifier_make/fractal_curve.py @@ -29,7 +29,10 @@ from sverchok.utils.geom import autorotate_householder, autorotate_track, autoro class SvFractalCurveNode(bpy.types.Node, SverchCustomTreeNode): - '''Fractal curve''' + """ + Triggers: Fractal Curve + Tooltip: Generate fractal (self-repeating) curve + """ bl_idname = 'SvFractalCurveNode' bl_label = 'Fractal Curve' bl_icon = 'OUTLINER_OB_EMPTY' @@ -83,8 +86,7 @@ class SvFractalCurveNode(bpy.types.Node, SverchCustomTreeNode): # rotation along Z axis only. if plane == 'XY': # Another unobvious hack: Vector.angle_signed method - # works with 2D vectors only (this is not stated in - # it's documentation!). Fortunately, in this particular + # works with 2D vectors only. Fortunately, in this particular # case our vectors are actually 2D. dst = Vector((dst[0], dst[1])) src = Vector((src[0], src[1])) @@ -107,13 +109,13 @@ class SvFractalCurveNode(bpy.types.Node, SverchCustomTreeNode): matrix = self.calc_rotation(curve, src, dst, plane) return [matrix * vertex for vertex in curve] - def substitute(self, recipient, donor, plane=None): + def substitute(self, recipient, donor, min_length, plane=None): line = donor[-1] - donor[0] result = [] result.append(donor[0]) for pair in zip(recipient, recipient[1:]): new_line = pair[1] - pair[0] - if new_line.length < self.min_length: + if new_line.length < min_length: result.append(pair[1]) continue scaled = self.scale_to(donor, line.length, new_line.length) @@ -124,24 +126,27 @@ class SvFractalCurveNode(bpy.types.Node, SverchCustomTreeNode): result = [Vector(v.to_tuple(self.precision)) for v in result] return result - def make_fractal(self, vertices): + def make_fractal(self, vertices, iterations, min_length): result = vertices plane = self.check_plane(vertices) print(plane) - for i in range(self.iterations): - result = self.substitute(result, vertices, plane) + for i in range(iterations): + result = self.substitute(result, vertices, min_length, plane) result = self.move_to(result, result[0], vertices[0]) return result def draw_buttons(self, context, layout): - layout.prop(self, "iterations") - layout.prop(self, "min_length") + if 'Iterations' not in self.inputs: + layout.prop(self, "iterations") + layout.prop(self, "min_length") def draw_buttons_ext(self, context, layout): self.draw_buttons(context, layout) layout.prop(self, "precision") def sv_init(self, context): + self.inputs.new('StringsSocket', 'Iterations').prop_name = "iterations" + self.inputs.new('StringsSocket', 'MinLength').prop_name = "min_length" self.inputs.new('VerticesSocket', 'Vertices') self.outputs.new('VerticesSocket', 'Vertices') @@ -151,11 +156,23 @@ class SvFractalCurveNode(bpy.types.Node, SverchCustomTreeNode): return curves = Vector_generate(self.inputs['Vertices'].sv_get()) + if 'Iterations' in self.inputs: + iterations_s = self.inputs['Iterations'].sv_get()[0] + min_length_s = self.inputs['MinLength'].sv_get()[0] + else: + iterations_s = [[self.iterations]] + min_length_s = [[self.min_length]] verts_out = [] - for curve in curves: - curve_out = self.make_fractal(curve) + objects = match_long_repeat([curves, iterations_s, min_length_s]) + + for curve, iterations, min_length in zip(*objects): + if isinstance(iterations, (list, tuple)): + iterations = iterations[0] + if isinstance(min_length, (list, tuple)): + min_length = min_length[0] + curve_out = self.make_fractal(curve, iterations, min_length) verts_out.append([tuple(v) for v in curve_out]) self.outputs['Vertices'].sv_set(verts_out) -- GitLab From 22a9181ce2c981be7bc62684d469dbb886f9b549 Mon Sep 17 00:00:00 2001 From: Ilya Portnov Date: Sat, 1 Jun 2019 13:31:28 +0500 Subject: [PATCH 121/137] Update documentation. --- docs/nodes/modifier_make/fractal_curve.rst | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/docs/nodes/modifier_make/fractal_curve.rst b/docs/nodes/modifier_make/fractal_curve.rst index b9f69e332..cc0d00381 100644 --- a/docs/nodes/modifier_make/fractal_curve.rst +++ b/docs/nodes/modifier_make/fractal_curve.rst @@ -17,8 +17,11 @@ NB 3: Usually you will want to use curves, edges of which have nearly the same l Inputs ------ -This node has one input: **Vertices** - vertices of input curve. Vertices -should go in the order in which they appear in the curve. +This node has the following inputs: + +* **Iterations** - number of iterations. +* **MinLength** - minimum length of edge to substitute. +* **Vertices** - vertices of input curve. Vertices should go in the order in which they appear in the curve. Parameters ---------- @@ -26,11 +29,13 @@ Parameters This node has the following parameters: * **Iterations**. Number of iterations. If zero, then output curve will be - exactly the same as input one. Default value is 3. + exactly the same as input one. Default value is 3. This parameter can also be + provided from input. * **Min. length**. Minimum length of edge to substitute. Fractal substitution process will stop for specific edge if it's length became less than specified value. Minimal value of zero means that fractal substitution process is - stopped only when maximum number of iterations is reached. + stopped only when maximum number of iterations is reached. This parameter can + also be provided from input. Default value is 0.01. * **Precision**. Precision of intermediate calculations (number of decimal digits). Default value is 8. This parameter is available only in the **N** panel. @@ -56,3 +61,7 @@ This node can process 3D curves as well: .. image:: https://user-images.githubusercontent.com/284644/57985246-970a1880-7a7e-11e9-84f3-198244d92df0.png +Vectorization example: + +.. image:: https://user-images.githubusercontent.com/284644/58745914-074a6e00-8471-11e9-889d-f41f416fb744.png + -- GitLab From 808abe765149aee330ded8d4fcd4c29e6ee07ae0 Mon Sep 17 00:00:00 2001 From: Ilya Portnov Date: Sun, 2 Jun 2019 11:52:38 +0500 Subject: [PATCH 122/137] Calculate Mask node. --- index.md | 1 + nodes/list_masks/calc_mask.py | 96 +++++++++++++++++++++++++++++++++++ 2 files changed, 97 insertions(+) create mode 100644 nodes/list_masks/calc_mask.py diff --git a/index.md b/index.md index 639a764a3..d6bd113ff 100644 --- a/index.md +++ b/index.md @@ -140,6 +140,7 @@ MaskListNode SvMaskJoinNode SvMaskConvertNode + SvCalcMaskNode ## List Mutators SvListModifierNode diff --git a/nodes/list_masks/calc_mask.py b/nodes/list_masks/calc_mask.py new file mode 100644 index 000000000..bfb37732a --- /dev/null +++ b/nodes/list_masks/calc_mask.py @@ -0,0 +1,96 @@ +# ##### 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, IntProperty +from sverchok.node_tree import SverchCustomTreeNode, StringsSocket, VerticesSocket +from sverchok.data_structure import updateNode, match_long_repeat, fullList + +class SvCalcMaskNode(bpy.types.Node, SverchCustomTreeNode): + """ + Triggers: Calculate Mask + Tooltip: Calculate mask from two sets of objects + """ + bl_idname = 'SvCalcMaskNode' + bl_label = 'Calculate Mask' + bl_icon = 'OUTLINER_OB_EMPTY' + + level = IntProperty(name = 'Level', + description = "List level to operate on", + min = 0, default = 0, update=updateNode) + + negate = BoolProperty(name = 'Negate', + description = 'Negate mask', update=updateNode) + + ignore_order = BoolProperty(name = 'Ignore order', + description = "Ignore items order while comparing lists", + default = True, update=updateNode) + + def draw_buttons(self, context, layout): + layout.prop(self, 'level') + layout.prop(self, 'negate') + layout.prop(self, 'ignore_order') + + def sv_init(self, context): + self.inputs.new('StringsSocket', "Subset") + self.inputs.new('StringsSocket', "Set") + self.outputs.new('StringsSocket', 'Mask') + + def calc(self, subset_data, set_data, level): + if level == 0: + if not isinstance(subset_data, (tuple, list)): + raise Exception("Specified level is too high for given Subset") + if not isinstance(set_data, (tuple, list)): + raise Exception("Specified level is too high for given Set") + + if self.ignore_order: + if self.negate: + return [set(item) not in map(set, subset_data) for item in set_data] + else: + return [set(item) in map(set, subset_data) for item in set_data] + else: + if self.negate: + return [item not in subset_data for item in set_data] + else: + return [item in subset_data for item in set_data] + else: + sub_objects = match_long_repeat([subset_data, set_data]) + return [self.calc(subset_item, set_item, level - 1) for subset_item, set_item in zip(*sub_objects)] + + def process(self): + + if not any(output.is_linked for output in self.outputs): + return + + subset_s = self.inputs['Subset'].sv_get(default=[[]]) + set_s = self.inputs['Set'].sv_get(default=[[]]) + out_masks = [] + + objects = match_long_repeat([subset_s, set_s]) + for subset, set in zip(*objects): + mask = self.calc(subset, set, self.level) + out_masks.append(mask) + + self.outputs['Mask'].sv_set(out_masks) + +def register(): + bpy.utils.register_class(SvCalcMaskNode) + +def unregister(): + bpy.utils.unregister_class(SvCalcMaskNode) + -- GitLab From 1cf25ee009da0702c3eef580a913019226fcd6e6 Mon Sep 17 00:00:00 2001 From: Ilya Portnov Date: Sun, 2 Jun 2019 12:35:54 +0500 Subject: [PATCH 123/137] Test cases for calc_mask. --- data_structure.py | 36 +++++++++++++++++++++++++++++ nodes/list_masks/calc_mask.py | 25 ++------------------ tests/data_structure_tests.py | 43 +++++++++++++++++++++++++++++++++++ 3 files changed, 81 insertions(+), 23 deletions(-) diff --git a/data_structure.py b/data_structure.py index 239e6569a..7da3eec48 100755 --- a/data_structure.py +++ b/data_structure.py @@ -365,6 +365,42 @@ def describe_data_shape(data): nesting, result = helper(data) return "Level {}: {}".format(nesting, result) +def calc_mask(subset_data, set_data, level=0, negate=False, ignore_order=True): + """ + Calculate mask: for each item in set_data, return True if it is present in subset_data. + The function can work at any specified level. + + subset_data: subset, for example [1] + set_data: set, for example [1, 2, 3] + level: 0 to check immediate members of set and subset; 1 to work with lists of lists and so on. + negate: if True, then result will be negated (True if item of set is not present in subset). + ignore_order: when comparing lists, ignore items order. + + Raises an exception if nesting level of input sets is less than specified level parameter. + + calc_mask([1], [1,2,3]) == [True, False, False]) + calc_mask([1], [1,2,3], negate=True) == [False, True, True] + """ + if level == 0: + if not isinstance(subset_data, (tuple, list)): + raise Exception("Specified level is too high for given Subset") + if not isinstance(set_data, (tuple, list)): + raise Exception("Specified level is too high for given Set") + + if ignore_order and get_data_nesting_level(subset_data) > 1: + if negate: + return [set(item) not in map(set, subset_data) for item in set_data] + else: + return [set(item) in map(set, subset_data) for item in set_data] + else: + if negate: + return [item not in subset_data for item in set_data] + else: + return [item in subset_data for item in set_data] + else: + sub_objects = match_long_repeat([subset_data, set_data]) + return [calc_mask(subset_item, set_item, level - 1, negate, ignore_order) for subset_item, set_item in zip(*sub_objects)] + ##################################################### ################### matrix magic #################### ##################################################### diff --git a/nodes/list_masks/calc_mask.py b/nodes/list_masks/calc_mask.py index bfb37732a..6d246dcdf 100644 --- a/nodes/list_masks/calc_mask.py +++ b/nodes/list_masks/calc_mask.py @@ -19,7 +19,7 @@ import bpy from bpy.props import BoolProperty, EnumProperty, IntProperty from sverchok.node_tree import SverchCustomTreeNode, StringsSocket, VerticesSocket -from sverchok.data_structure import updateNode, match_long_repeat, fullList +from sverchok.data_structure import updateNode, match_long_repeat, fullList, calc_mask class SvCalcMaskNode(bpy.types.Node, SverchCustomTreeNode): """ @@ -51,27 +51,6 @@ class SvCalcMaskNode(bpy.types.Node, SverchCustomTreeNode): self.inputs.new('StringsSocket', "Set") self.outputs.new('StringsSocket', 'Mask') - def calc(self, subset_data, set_data, level): - if level == 0: - if not isinstance(subset_data, (tuple, list)): - raise Exception("Specified level is too high for given Subset") - if not isinstance(set_data, (tuple, list)): - raise Exception("Specified level is too high for given Set") - - if self.ignore_order: - if self.negate: - return [set(item) not in map(set, subset_data) for item in set_data] - else: - return [set(item) in map(set, subset_data) for item in set_data] - else: - if self.negate: - return [item not in subset_data for item in set_data] - else: - return [item in subset_data for item in set_data] - else: - sub_objects = match_long_repeat([subset_data, set_data]) - return [self.calc(subset_item, set_item, level - 1) for subset_item, set_item in zip(*sub_objects)] - def process(self): if not any(output.is_linked for output in self.outputs): @@ -83,7 +62,7 @@ class SvCalcMaskNode(bpy.types.Node, SverchCustomTreeNode): objects = match_long_repeat([subset_s, set_s]) for subset, set in zip(*objects): - mask = self.calc(subset, set, self.level) + mask = calc_mask(subset, set, level=self.level, negate=self.negate, ignore_order=self.ignore_order) out_masks.append(mask) self.outputs['Mask'].sv_set(out_masks) diff --git a/tests/data_structure_tests.py b/tests/data_structure_tests.py index c735fa9ef..91d83e917 100644 --- a/tests/data_structure_tests.py +++ b/tests/data_structure_tests.py @@ -68,3 +68,46 @@ class DataStructureTests(SverchokTestCase): self.subtest_assert_equals(describe_data_shape([1]), 'Level 1: list [1] of int') self.subtest_assert_equals(describe_data_shape([[(1,2,3)]]), 'Level 3: list [1] of list [1] of tuple [3] of int') +class CalcMaskTests(SverchokTestCase): + def test_calc_mask_1(self): + subset = [1] + set = [1, 2, 3] + mask = calc_mask(subset, set, level=0) + expected = [True, False, False] + self.assertEquals(mask, expected) + + def test_calc_mask_2(self): + subset = [1] + set = [1, 2, 3] + mask = calc_mask(subset, set, negate=True) + expected = [False, True, True] + self.assertEquals(mask, expected) + + def test_calc_mask_3(self): + subset = [[1, 2], [3, 4]] + set = [[1, 2], [3, 4], [5, 6]] + mask = calc_mask(subset, set, level=0) + expected = [True, True, False] + self.assertEquals(mask, expected) + + def test_calc_mask_4(self): + subset = [[1, 2], [3, 4]] + set = [[1, 2], [3, 4], [5, 6]] + mask = calc_mask(subset, set, level=1) + expected = [[True, True], [True, True], [False, False]] + self.assertEquals(mask, expected) + + def test_calc_mask_5(self): + subset = [[1], [5,6]] + set = [[1, 2, 3], [7, 8, 9]] + mask = calc_mask(subset, set, level=0) + expected = [False, False] + self.assertEquals(mask, expected) + + def test_calc_mask_6(self): + subset = [[1], [5,6]] + set = [[1, 2, 3], [7, 8, 9]] + mask = calc_mask(subset, set, level=1) + expected = [[True, False, False], [False, False, False]] + self.assertEquals(mask, expected) + -- GitLab From a03a86af94af400ede7a26f3aad58c1bb8541762 Mon Sep 17 00:00:00 2001 From: Ilya Portnov Date: Sun, 2 Jun 2019 12:50:57 +0500 Subject: [PATCH 124/137] Documentation. --- docs/nodes/list_masks/calc_mask.rst | 59 ++++++++++++++++++++++ docs/nodes/list_masks/list_masks_index.rst | 1 + tests/data_structure_tests.py | 7 +++ 3 files changed, 67 insertions(+) create mode 100644 docs/nodes/list_masks/calc_mask.rst diff --git a/docs/nodes/list_masks/calc_mask.rst b/docs/nodes/list_masks/calc_mask.rst new file mode 100644 index 000000000..56b87894f --- /dev/null +++ b/docs/nodes/list_masks/calc_mask.rst @@ -0,0 +1,59 @@ +Calculate Mask +============== + +Functionality +------------- + +This node calculates masks from two input lists (Set and Subset). For each item +in the Set, it returns True if the item is present in Subset, otherwise it +returns False. + +There are nodes, which output, for example, "All faces" and "Processed faces" +or smth like that. To do something with that output, it would be usually more +effective to deal with "All faces" and "Processed faces mask" instead. + +The node can work on different levels of data trees. For example, given Subset += `[[1, 2], [3,4]]` and Set = `[[1, 2], [3, 4], [5, 6]]`: + +* with level = 0 it will output `[True, True, False]` +* with level = 1 it will output `[[True, True], [True, True], [False, False]]` + +Given Subset = `[[1], [5,6]]` and Set = `[[1, 2, 3], [7, 8, 9]]`: + +* with level = 0 it will output `[False, False]` (because, for example, there + is no `[1, 2, 3]` in the `[[1], [5,6]]`) +* with level = 1, it will output `[[True, False, False], [False, False, False]]` + +Inputs +------ + +This node has the following inputs: + +* **Subset**. List of "good" data items to be checked against. +* **Set**. The whole set of data items. + +Parameters +---------- + +This node has the following parameters: + +* **Negate**. If checked, then the resulting mask will be negated. I.e., the + node will output True if item of Set is *not* present in Subset. Unchecked by + default. +* **Ignore order**. If checked, then, while comparing lists, the node will not + take into account the order of items in these lists. For example, is `[1, 2]` + the same as `[2, 1]`? No, if **Ignore order** is not checked. + +Outputs +------- + +This node has only one output: **Mask**. Number of items in this outuput is +equal to number of items in the **Set** input. + +Example of usage +---------------- + +This node can, for example, be used to apply **Inset Special** node iteratively: + +.. image:: https://user-images.githubusercontent.com/284644/58757902-82715a00-852d-11e9-9288-369607f5229d.png + diff --git a/docs/nodes/list_masks/list_masks_index.rst b/docs/nodes/list_masks/list_masks_index.rst index 418035589..aed295f90 100644 --- a/docs/nodes/list_masks/list_masks_index.rst +++ b/docs/nodes/list_masks/list_masks_index.rst @@ -8,4 +8,5 @@ List Masks mask mask_join mask_converter + calc_mask diff --git a/tests/data_structure_tests.py b/tests/data_structure_tests.py index 91d83e917..459ad9e7f 100644 --- a/tests/data_structure_tests.py +++ b/tests/data_structure_tests.py @@ -111,3 +111,10 @@ class CalcMaskTests(SverchokTestCase): expected = [[True, False, False], [False, False, False]] self.assertEquals(mask, expected) + def test_calc_mask_7(self): + subset = [[1, 2], [3, 4]] + set = [[2, 1], [5, 6]] + mask = calc_mask(subset, set, ignore_order=True) + expected = [True, False] + self.assertEquals(mask, expected) + -- GitLab From 917cdd47cd753929bd6571302b8704ea9a38b789 Mon Sep 17 00:00:00 2001 From: Ilya Portnov Date: Sun, 2 Jun 2019 13:37:28 +0500 Subject: [PATCH 125/137] Inset special node: more precise normal calculation. --- nodes/modifier_make/inset_special.py | 26 ++++++++++++++++++++++---- utils/geom.py | 21 +++++++++++++++++++++ 2 files changed, 43 insertions(+), 4 deletions(-) diff --git a/nodes/modifier_make/inset_special.py b/nodes/modifier_make/inset_special.py index d7cc47575..4b497c2f0 100644 --- a/nodes/modifier_make/inset_special.py +++ b/nodes/modifier_make/inset_special.py @@ -22,9 +22,10 @@ import bpy import mathutils from mathutils import Vector -from bpy.props import FloatProperty, FloatVectorProperty, IntProperty +from bpy.props import FloatProperty, FloatVectorProperty, IntProperty, EnumProperty from sverchok.node_tree import SverchCustomTreeNode +from sverchok.utils.geom import calc_normal from sverchok.data_structure import ( updateNode, Vector_generate, repeat_last, fullList) @@ -33,7 +34,7 @@ from sverchok.data_structure import ( ''' very non optimal routines. beware. I know this ''' -def inset_special(vertices, faces, inset_rates, distances, ignores, make_inners): +def inset_special(vertices, faces, inset_rates, distances, ignores, make_inners, normal_mode="Fast"): new_faces = [] new_ignores = [] @@ -115,7 +116,10 @@ def inset_special(vertices, faces, inset_rates, distances, ignores, make_inners) new_verts_prime = [avg_vec.lerp(v, inset_by) for v in verts] if distance: - local_normal = mathutils.geometry.normal(*new_verts_prime[:3]) + if normal_mode == 'Fast': + local_normal = mathutils.geometry.normal(*new_verts_prime[:3]) + else: + local_normal = calc_normal(new_verts_prime) new_verts_prime = [v.lerp(v+local_normal, distance) for v in new_verts_prime] vertices.extend(new_verts_prime) @@ -149,6 +153,11 @@ class SvInsetSpecial(bpy.types.Node, SverchCustomTreeNode): bl_label = 'Inset Special' bl_icon = 'OUTLINER_OB_EMPTY' + normal_modes = [ + ("Fast", "Fast", "Fast algorithm", 0), + ("Exact", "Exact", "Slower, but exact algorithm", 1) + ] + inset = FloatProperty( name='Inset', description='inset amount', @@ -161,6 +170,12 @@ class SvInsetSpecial(bpy.types.Node, SverchCustomTreeNode): ignore = IntProperty(name='Ignore', description='skip polygons', default=0, update=updateNode) make_inner = IntProperty(name='Make Inner', description='Make inner polygon', default=1, update=updateNode) + normal_mode = EnumProperty(name = "Normals", + description = "Normals calculation algorithm", + default = "Fast", + items = normal_modes, + update = updateNode) + # axis = FloatVectorProperty( # name='axis', description='axis relative to normal', # default=(0,0,1), update=updateNode) @@ -180,6 +195,8 @@ class SvInsetSpecial(bpy.types.Node, SverchCustomTreeNode): o.new('StringsSocket', 'ignored') o.new('StringsSocket', 'inset') + def draw_buttons_ext(self, context, layout): + layout.prop(self, "normal_mode") def process(self): i = self.inputs @@ -221,7 +238,8 @@ class SvInsetSpecial(bpy.types.Node, SverchCustomTreeNode): 'inset_rates': inset_rates, 'distances': distance_vals, 'make_inners': make_inners, - 'ignores': ignores + 'ignores': ignores, + 'normal_mode': self.normal_mode } res = inset_special(**func_args) diff --git a/utils/geom.py b/utils/geom.py index b89605455..5a65ef263 100644 --- a/utils/geom.py +++ b/utils/geom.py @@ -1441,6 +1441,27 @@ def linear_approximation(data): result.eigenvalues, result.eigenvectors = linalg.eig(matrix) return result +def calc_normal(vertices): + """ + Calculate normal for a face defined by specified vertices. + For tris or quads, mathutils.geometry.normal() is used. + Ngon will be triangulated, and then the average normal of + all resulting tris will be returned. + + input: list of 3-tuples or list of mathutils.Vector. + output: mathutils.Vector. + """ + n = len(vertices) + vertices = list(map(mathutils.Vector, vertices)) + if n <= 4: + return mathutils.geometry.normal(*vertices) + else: + # Triangluate + triangle_idxs = [[0, k, k+1] for k in range(1, n-1)] + triangles = [[vertices[i] for i in idxs] for idxs in triangle_idxs] + subnormals = [mathutils.geometry.normal(*triangle) for triangle in triangles] + return mathutils.Vector(center(subnormals)) + def multiply_vectors(M, vlist): # (4*4 matrix) X (3*1 vector) -- GitLab From 96c10077f6b3d22d6c249f4073066106524a1029 Mon Sep 17 00:00:00 2001 From: Ilya Portnov Date: Thu, 30 May 2019 20:53:25 +0500 Subject: [PATCH 126/137] Diameter node. --- index.md | 1 + nodes/analyzer/diameter.py | 72 ++++++++++++++++++++++++++++++++++++++ utils/geom.py | 33 ++++++++++++++--- 3 files changed, 101 insertions(+), 5 deletions(-) create mode 100644 nodes/analyzer/diameter.py diff --git a/index.md b/index.md index d6bd113ff..a6168ff70 100644 --- a/index.md +++ b/index.md @@ -49,6 +49,7 @@ ## Analyzers SvBBoxNode + SvDiameterNode SvVolumeNode AreaNode DistancePPNode diff --git a/nodes/analyzer/diameter.py b/nodes/analyzer/diameter.py new file mode 100644 index 000000000..e6971df97 --- /dev/null +++ b/nodes/analyzer/diameter.py @@ -0,0 +1,72 @@ +# ##### 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 itertools import product + +import bpy + +from sverchok.node_tree import SverchCustomTreeNode +from sverchok.data_structure import updateNode, match_long_repeat +from sverchok.utils.geom import diameter + +class SvDiameterNode(bpy.types.Node, SverchCustomTreeNode): + """ + Triggers: Diameter + Tooltip: Calculate diameter of input object + """ + + bl_idname = 'SvDiameterNode' + bl_label = "Diameter" + bl_icon = 'ARROW_LEFTRIGHT' + + def sv_init(self, context): + self.inputs.new('VerticesSocket', 'Vertices') + self.inputs.new('VerticesSocket', 'Direction') + self.outputs.new('StringsSocket', 'Diameter') + + def process(self): + if not self.inputs['Vertices'].is_linked: + return + if not any(s.is_linked for s in self.outputs): + return + + any_direction = not self.inputs['Direction'].is_linked + + out_results = [] + + vertices_s = self.inputs['Vertices'].sv_get(default=[[]]) + directions_s = self.inputs['Direction'].sv_get(default=[[]]) + objects = match_long_repeat([vertices_s, directions_s]) + + for vertices, directions in zip(*objects): + if any_direction: + direction = None + else: + direction = directions[0] + diam = diameter(vertices, direction) + out_results.append([diam]) + + self.outputs['Diameter'].sv_set(out_results) + + +def register(): + bpy.utils.register_class(SvDiameterNode) + +def unregister(): + bpy.utils.unregister_class(SvDiameterNode) + diff --git a/utils/geom.py b/utils/geom.py index 5a65ef263..eeb78f27d 100644 --- a/utils/geom.py +++ b/utils/geom.py @@ -865,13 +865,36 @@ def diameter(vertices, axis): Calculate diameter of set of vertices along specified axis. vertices: list of mathutils.Vector or of 3-tuples of floats. - axis: 0, 1 or 2. + axis: either + * integer: 0, 1 or 2 for X, Y or Z + * string: 'X', 'Y' or 'Z' + * 3-tuple of floats or Vector: any direction + * None: calculate diameter regardless of direction returns float. """ - xs = [vertex[axis] for vertex in vertices] - M = max(xs) - m = min(xs) - return (M-m) + if axis is None: + distances = [(mathutils.Vector(v1) - mathutils.Vector(v2)).length for v1 in vertices for v2 in vertices] + return max(distances) + elif isinstance(axis, tuple) or isinstance(axis, Vector): + axis = mathutils.Vector(axis).normalized() + ds = [mathutils.Vector(vertex).dot(axis) for vertex in vertices] + M = max(ds) + m = min(ds) + return (M-m) + else: + if axis == 'X': + axis == 0 + elif axis == 'Y': + axis == 1 + elif axis == 'Z': + axis = 2 + elif isinstance(axis, str): + raise Exception("Unknown axis: {}".format(axis)) + + xs = [vertex[axis] for vertex in vertices] + M = max(xs) + m = min(xs) + return (M-m) def center(data): """ -- GitLab From 2b572b2c9fc4402e07f0d9a08cfeeff007022fa6 Mon Sep 17 00:00:00 2001 From: Ilya Portnov Date: Sun, 2 Jun 2019 20:49:18 +0500 Subject: [PATCH 127/137] Tests for the diameter function. --- tests/diameter_tests.py | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 tests/diameter_tests.py diff --git a/tests/diameter_tests.py b/tests/diameter_tests.py new file mode 100644 index 000000000..bafbb29f7 --- /dev/null +++ b/tests/diameter_tests.py @@ -0,0 +1,33 @@ + +from math import sqrt +from mathutils import Vector + +from sverchok.utils.logging import error +from sverchok.utils.testing import * +from sverchok.utils.geom import diameter + +class DiameterTests(SverchokTestCase): + def test_diameter_1(self): + p1 = (0, 0, 0) + p2 = (0, 1, 0) + diam = diameter([p1, p2], None) + expected = 1.0 + self.assert_sverchok_data_equal(diam, expected, precision=8) + + def test_diameter_2(self): + p1 = (0, 0, 0) + p2 = (0, 1, 0) + p3 = (1, 0, 0) + diam = diameter([p1, p2, p3], None) + expected = sqrt(2) + self.assert_sverchok_data_equal(diam, expected, precision=8) + + def test_diameter_3(self): + p1 = (0, 0, 0) + p2 = (0, 1, 0) + p3 = (1, 0, 0) + direction = (1, 0, 0) + diam = diameter([p1, p2, p3], direction) + expected = 1 + self.assert_sverchok_data_equal(diam, expected, precision=8) + -- GitLab From 19d3b8294ddd73b561d30a376e839a289a122008 Mon Sep 17 00:00:00 2001 From: Ilya Portnov Date: Sun, 2 Jun 2019 20:59:48 +0500 Subject: [PATCH 128/137] Documentation for the Diameter node. --- docs/nodes/analyzers/analyzers_index.rst | 1 + docs/nodes/analyzers/diameter.rst | 42 ++++++++++++++++++++++++ 2 files changed, 43 insertions(+) create mode 100644 docs/nodes/analyzers/diameter.rst diff --git a/docs/nodes/analyzers/analyzers_index.rst b/docs/nodes/analyzers/analyzers_index.rst index 7be2eae9c..3e3f2ee9c 100644 --- a/docs/nodes/analyzers/analyzers_index.rst +++ b/docs/nodes/analyzers/analyzers_index.rst @@ -7,6 +7,7 @@ Analyzers area bbox + diameter distance_pp distance_point_line distance_point_plane diff --git a/docs/nodes/analyzers/diameter.rst b/docs/nodes/analyzers/diameter.rst new file mode 100644 index 000000000..bf824dd1b --- /dev/null +++ b/docs/nodes/analyzers/diameter.rst @@ -0,0 +1,42 @@ +Diameter +======== + +Functionality +------------- + +This node calculates the diameter of input set of vertices. + +It can calculate diameter in two ways: + +* General diameter, i.e. maximum distance between any two vertices from input set. +* Diameter along specified direction (axis), i.e. the length of the projection + of whole input vertices set to specified direction. + +Inputs +------ + +This node has the following inputs: + +* **Vertices** - vertices to calculate diameter of. This input is mandatory for the node to function. +* **Direction** - direction, along which diameter should be calculated. If this + input is not connected, then the node will calculate "general diameter" of + input vertices set. + +Outputs +------- + +This node has one output: **Diameter** - calculated diameter of vertices set. + +Examples of usage +----------------- + +Suzanne has "general diameter" of 2.73: + +.. image:: https://user-images.githubusercontent.com/284644/58649984-03aad000-8327-11e9-90b8-0c39f328402a.png + +Diameter of Suzanne along some diagonal direction is 2.44. Here the direction +is drawn as green line, and the projection of Suzanne to that direction is +marked with red dots: + +.. image:: https://user-images.githubusercontent.com/284644/58649983-03aad000-8327-11e9-852a-a75d8eb4aad4.png + -- GitLab From a252e8d5aa1caf9e5ca0c70578ed7e7caaff64b0 Mon Sep 17 00:00:00 2001 From: nortikin Date: Sun, 2 Jun 2019 19:20:48 +0300 Subject: [PATCH 129/137] start exodus --- docs/404_custom.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/404_custom.html b/docs/404_custom.html index 7037d3587..a570ff730 100644 --- a/docs/404_custom.html +++ b/docs/404_custom.html @@ -11,7 +11,7 @@
- Sorry! It appears that Polyline Viewer MK1 doesn't have a document associated with it. + Sorry! It appears that Combinatorics doesn't have a document associated with it.

Please contact us on the GitHub issue tracker or any of the following social sites: G+, VK or Blenderartists. -- GitLab From d8aed2220c4058e4091ce9b0c748f4d8dd3a954a Mon Sep 17 00:00:00 2001 From: nortikin Date: Sun, 2 Jun 2019 20:25:23 +0300 Subject: [PATCH 130/137] documented nodes migration at first --- docs/404_custom.html | 2 +- .../set_dataobject.rst | 0 .../color_by_formula.rst | 0 docs/nodes/{vector => color}/color_input.rst | 0 .../{beta_nodes => color}/sample_uv_color.rst | 0 .../spital.rst} | 0 .../extrude_separate_lite.rst | 0 .../subdivide_lite.rst | 0 .../unsubdivide.rst | 0 .../points_from_uv_to_mesh.rst} | 0 .../select_mesh_verts.rst | 0 .../deform_by_formula.rst | 0 index.md | 41 +++++++++++-------- nodes/{text => cnc}/export_gcode.py | 0 nodes/color/__init__.py | 0 nodes/{vector => color}/color_in_mk1.py | 0 nodes/{vector => color}/color_input.py | 0 nodes/{vector => color}/color_out_mk1.py | 0 .../color_uv_texture.py | 0 nodes/{vector => color}/formula_color.py | 0 .../sample_uv_color.py | 0 .../vertex_colors_mk3.py | 0 22 files changed, 25 insertions(+), 18 deletions(-) rename docs/nodes/{beta_nodes => bpy_data}/set_dataobject.rst (100%) rename docs/nodes/{beta_nodes => color}/color_by_formula.rst (100%) rename docs/nodes/{vector => color}/color_input.rst (100%) rename docs/nodes/{beta_nodes => color}/sample_uv_color.rst (100%) rename docs/nodes/{beta_nodes/spiralNode.rst => generators_extended/spital.rst} (100%) rename docs/nodes/{beta_nodes => modifier_change}/extrude_separate_lite.rst (100%) rename docs/nodes/{beta_nodes => modifier_change}/subdivide_lite.rst (100%) rename docs/nodes/{beta_nodes => modifier_change}/unsubdivide.rst (100%) rename docs/nodes/{beta_nodes/find_uv_coord_on_mesh_node.rst => object_nodes/points_from_uv_to_mesh.rst} (100%) rename docs/nodes/{beta_nodes => object_nodes}/select_mesh_verts.rst (100%) rename docs/nodes/{beta_nodes => vector}/deform_by_formula.rst (100%) rename nodes/{text => cnc}/export_gcode.py (100%) create mode 100644 nodes/color/__init__.py rename nodes/{vector => color}/color_in_mk1.py (100%) rename nodes/{vector => color}/color_input.py (100%) rename nodes/{vector => color}/color_out_mk1.py (100%) rename nodes/{object_nodes => color}/color_uv_texture.py (100%) rename nodes/{vector => color}/formula_color.py (100%) rename nodes/{object_nodes => color}/sample_uv_color.py (100%) rename nodes/{object_nodes => color}/vertex_colors_mk3.py (100%) diff --git a/docs/404_custom.html b/docs/404_custom.html index a570ff730..dac4c475a 100644 --- a/docs/404_custom.html +++ b/docs/404_custom.html @@ -11,7 +11,7 @@
- Sorry! It appears that Combinatorics doesn't have a document associated with it. + Sorry! It appears that Export Gcode doesn't have a document associated with it.

Please contact us on the GitHub issue tracker or any of the following social sites: G+, VK or Blenderartists. diff --git a/docs/nodes/beta_nodes/set_dataobject.rst b/docs/nodes/bpy_data/set_dataobject.rst similarity index 100% rename from docs/nodes/beta_nodes/set_dataobject.rst rename to docs/nodes/bpy_data/set_dataobject.rst diff --git a/docs/nodes/beta_nodes/color_by_formula.rst b/docs/nodes/color/color_by_formula.rst similarity index 100% rename from docs/nodes/beta_nodes/color_by_formula.rst rename to docs/nodes/color/color_by_formula.rst diff --git a/docs/nodes/vector/color_input.rst b/docs/nodes/color/color_input.rst similarity index 100% rename from docs/nodes/vector/color_input.rst rename to docs/nodes/color/color_input.rst diff --git a/docs/nodes/beta_nodes/sample_uv_color.rst b/docs/nodes/color/sample_uv_color.rst similarity index 100% rename from docs/nodes/beta_nodes/sample_uv_color.rst rename to docs/nodes/color/sample_uv_color.rst diff --git a/docs/nodes/beta_nodes/spiralNode.rst b/docs/nodes/generators_extended/spital.rst similarity index 100% rename from docs/nodes/beta_nodes/spiralNode.rst rename to docs/nodes/generators_extended/spital.rst diff --git a/docs/nodes/beta_nodes/extrude_separate_lite.rst b/docs/nodes/modifier_change/extrude_separate_lite.rst similarity index 100% rename from docs/nodes/beta_nodes/extrude_separate_lite.rst rename to docs/nodes/modifier_change/extrude_separate_lite.rst diff --git a/docs/nodes/beta_nodes/subdivide_lite.rst b/docs/nodes/modifier_change/subdivide_lite.rst similarity index 100% rename from docs/nodes/beta_nodes/subdivide_lite.rst rename to docs/nodes/modifier_change/subdivide_lite.rst diff --git a/docs/nodes/beta_nodes/unsubdivide.rst b/docs/nodes/modifier_change/unsubdivide.rst similarity index 100% rename from docs/nodes/beta_nodes/unsubdivide.rst rename to docs/nodes/modifier_change/unsubdivide.rst diff --git a/docs/nodes/beta_nodes/find_uv_coord_on_mesh_node.rst b/docs/nodes/object_nodes/points_from_uv_to_mesh.rst similarity index 100% rename from docs/nodes/beta_nodes/find_uv_coord_on_mesh_node.rst rename to docs/nodes/object_nodes/points_from_uv_to_mesh.rst diff --git a/docs/nodes/beta_nodes/select_mesh_verts.rst b/docs/nodes/object_nodes/select_mesh_verts.rst similarity index 100% rename from docs/nodes/beta_nodes/select_mesh_verts.rst rename to docs/nodes/object_nodes/select_mesh_verts.rst diff --git a/docs/nodes/beta_nodes/deform_by_formula.rst b/docs/nodes/vector/deform_by_formula.rst similarity index 100% rename from docs/nodes/beta_nodes/deform_by_formula.rst rename to docs/nodes/vector/deform_by_formula.rst diff --git a/index.md b/index.md index a6168ff70..d699752f4 100644 --- a/index.md +++ b/index.md @@ -32,17 +32,21 @@ SvBoxRoundedNode SvBricksNode SvPolygonGridNode + --- HilbertNode Hilbert3dNode HilbertImageNode + --- SvProfileNodeMK2 SvMeshEvalNode SvGenerativeArtNode SvImageComponentsNode SvScriptNode + --- SvTorusKnotNode SvRingNode SvEllipseNode + SvSpiralNode SvSuperEllipsoidNode SvSmoothLines SvRegularSolid @@ -97,11 +101,14 @@ --- SvBevelNode SvSubdivideNode + SvSubdivideLiteNode + SvUnsubdivideNode SvSmoothNode SvIntersectEdgesNodeMK2 SvOffsetNode SvFillsHoleNode SvTriangulateNode + SvLimitedDissolveMK2 --- SvFlipNormalsNode SvRecalcNormalsNode @@ -110,6 +117,7 @@ SvIterateNode SvExtrudeEdgesNode SvExtrudeSeparateNode + SvExtrudeSeparateLiteNode SvExtrudeRegionNode SvBendAlongPathNode SvBendAlongSurfaceNode @@ -222,6 +230,7 @@ SvVectorFractal SvLacunarityNode SvTurbulenceNode + SvFormulaDeformMK2Node ## Matrix SvMatrixGenNodeMK2 @@ -283,9 +292,19 @@ SvDupliInstancesMK4 SvFCurveInNodeMK1 +## Color + SvColorsInNodeMK1 + SvColorInputNode + SvColorsOutNodeMK1 + SvVertexColorNodeMK3 + SvSampleUVColorNode + SvFormulaColorNode + SvMeshUVColorNode + ## Objects SvVertexGroupNodeMK2 - SvVertexColorNodeMK3 + SvUVPointonMeshNode + SvSelectMeshVerts ## Layout WifiInNode @@ -293,29 +312,20 @@ NodeReroute ConverterNode +## CNC + SvExportGcodeNode + ## Network SvUdpClientNodeMK2 ## Beta Nodes SvFormulaShapeNode SvHeavyTriangulateNode - SvFormulaDeformMK2Node - SvFormulaColorNode - SvMeshUVColorNode - SvUVPointonMeshNode - SvSampleUVColorNode - SvSubdivideLiteNode - SvExtrudeSeparateLiteNode SvBVHnearNewNode - SvUnsubdivideNode - SvLimitedDissolveMK2 SvMetaballOutLiteNode SvArmaturePropsNode SvLatticePropsNode - --- - SvColorsInNodeMK1 - SvColorInputNode - SvColorsOutNodeMK1 + --- SvMatrixNormalNode SvMatrixTrackToNode @@ -324,11 +334,8 @@ SvSculptMaskNode SvGreasePencilStrokes SvTextureViewerNodeLite - SvSelectMeshVerts SvSetCustomMeshNormals --- - SvSpiralNode - SvExportGcodeNode SvCombinatoricsNode ## Alpha Nodes diff --git a/nodes/text/export_gcode.py b/nodes/cnc/export_gcode.py similarity index 100% rename from nodes/text/export_gcode.py rename to nodes/cnc/export_gcode.py diff --git a/nodes/color/__init__.py b/nodes/color/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/nodes/vector/color_in_mk1.py b/nodes/color/color_in_mk1.py similarity index 100% rename from nodes/vector/color_in_mk1.py rename to nodes/color/color_in_mk1.py diff --git a/nodes/vector/color_input.py b/nodes/color/color_input.py similarity index 100% rename from nodes/vector/color_input.py rename to nodes/color/color_input.py diff --git a/nodes/vector/color_out_mk1.py b/nodes/color/color_out_mk1.py similarity index 100% rename from nodes/vector/color_out_mk1.py rename to nodes/color/color_out_mk1.py diff --git a/nodes/object_nodes/color_uv_texture.py b/nodes/color/color_uv_texture.py similarity index 100% rename from nodes/object_nodes/color_uv_texture.py rename to nodes/color/color_uv_texture.py diff --git a/nodes/vector/formula_color.py b/nodes/color/formula_color.py similarity index 100% rename from nodes/vector/formula_color.py rename to nodes/color/formula_color.py diff --git a/nodes/object_nodes/sample_uv_color.py b/nodes/color/sample_uv_color.py similarity index 100% rename from nodes/object_nodes/sample_uv_color.py rename to nodes/color/sample_uv_color.py diff --git a/nodes/object_nodes/vertex_colors_mk3.py b/nodes/color/vertex_colors_mk3.py similarity index 100% rename from nodes/object_nodes/vertex_colors_mk3.py rename to nodes/color/vertex_colors_mk3.py -- GitLab From 3440fce4da484504d6f735daf491e370e3d218de Mon Sep 17 00:00:00 2001 From: nortikin Date: Sun, 2 Jun 2019 21:11:41 +0300 Subject: [PATCH 131/137] menu some and old nodes also --- index.md | 9 +++------ menu.py | 1 - nodes/cnc/__init__.py | 0 {nodes/generator => old_nodes}/cylinder.py | 0 {nodes/number => old_nodes}/float.py | 0 {nodes/number => old_nodes}/integer.py | 0 ui/nodeview_space_menu.py | 4 ++++ utils/sv_extra_search.py | 2 +- 8 files changed, 8 insertions(+), 8 deletions(-) create mode 100644 nodes/cnc/__init__.py rename {nodes/generator => old_nodes}/cylinder.py (100%) rename {nodes/number => old_nodes}/float.py (100%) rename {nodes/number => old_nodes}/integer.py (100%) diff --git a/index.md b/index.md index d699752f4..a00fd82eb 100644 --- a/index.md +++ b/index.md @@ -15,7 +15,6 @@ SvNGonNode SvBoxNode SvCircleNode - CylinderNode SvCylinderNodeMK2 SphereNode SvIcosphereNode @@ -182,8 +181,6 @@ ## Number SvNumberNode - FloatNode - IntegerNode Float2IntNode ScalarMathNode SvScalarMathNodeMK2 @@ -256,6 +253,9 @@ SvTextureViewerNode Sv3DviewPropsNode +## CNC + SvExportGcodeNode + ## Text ViewerNodeTextMK3 SvTextInNodeMK2 @@ -312,9 +312,6 @@ NodeReroute ConverterNode -## CNC - SvExportGcodeNode - ## Network SvUdpClientNodeMK2 diff --git a/menu.py b/menu.py index f2ba7cd0b..5446cfb07 100644 --- a/menu.py +++ b/menu.py @@ -68,7 +68,6 @@ def make_node_cats(): # final append node_cats[category] = temp_list - return node_cats diff --git a/nodes/cnc/__init__.py b/nodes/cnc/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/nodes/generator/cylinder.py b/old_nodes/cylinder.py similarity index 100% rename from nodes/generator/cylinder.py rename to old_nodes/cylinder.py diff --git a/nodes/number/float.py b/old_nodes/float.py similarity index 100% rename from nodes/number/float.py rename to old_nodes/float.py diff --git a/nodes/number/integer.py b/old_nodes/integer.py similarity index 100% rename from nodes/number/integer.py rename to old_nodes/integer.py diff --git a/ui/nodeview_space_menu.py b/ui/nodeview_space_menu.py index 752af9630..ab9af1971 100644 --- a/ui/nodeview_space_menu.py +++ b/ui/nodeview_space_menu.py @@ -120,8 +120,10 @@ class NODEVIEW_MT_Dynamic_Menu(bpy.types.Menu): layout.menu("NODEVIEW_MT_AddListOps", **icon('NLA')) layout.separator() layout.menu("NODEVIEW_MT_AddViz", **icon('RESTRICT_VIEW_OFF')) + layout.menu("NODEVIEW_MT_AddCNC", **icon('MOD_BUILD')) layout.menu("NODEVIEW_MT_AddText") layout.menu("NODEVIEW_MT_AddScene", **icon('SCENE_DATA')) + layout.menu("NODEVIEW_MT_AddColor", **icon('COLOR')) layout.menu("NODEVIEW_MT_AddLayout", **icon("SV_LAYOUT")) layout.menu("NODE_MT_category_SVERCHOK_BPY_Data", icon="BLENDER") layout.separator() @@ -180,8 +182,10 @@ classes = [ make_class('Transforms', "Transforms"), make_class('Analyzers', "Analyzers"), make_class('Viz', "Viz"), + make_class('CNC', "CNC"), make_class('Text', "Text"), make_class('Scene', "Scene"), + make_class('Color', "Color"), make_class('Layout', "Layout"), make_class('Listmain', "List Main"), make_class('Liststruct', "List Struct"), diff --git a/utils/sv_extra_search.py b/utils/sv_extra_search.py index e959881ba..03da2a2a0 100644 --- a/utils/sv_extra_search.py +++ b/utils/sv_extra_search.py @@ -104,7 +104,7 @@ def gather_items(): for item in node_list: if item[0] in {'separator', 'NodeReroute'}: continue - + fx.append((str(idx), ensure_valid_show_string(item), '', idx)) idx += 1 -- GitLab From 4eb0de9e2f790416993f037d4936800a8ab9a645 Mon Sep 17 00:00:00 2001 From: nortikin Date: Mon, 3 Jun 2019 00:45:14 +0300 Subject: [PATCH 132/137] space menu --- ui/nodeview_space_menu.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/ui/nodeview_space_menu.py b/ui/nodeview_space_menu.py index ab9af1971..925f06c16 100644 --- a/ui/nodeview_space_menu.py +++ b/ui/nodeview_space_menu.py @@ -113,18 +113,18 @@ class NODEVIEW_MT_Dynamic_Menu(bpy.types.Menu): layout.menu("NODEVIEW_MT_AddAnalyzers", **icon('VIEWZOOM')) layout.menu("NODEVIEW_MT_AddModifiers", **icon('MODIFIER')) layout.separator() - layout.menu("NODEVIEW_MT_AddNumber") - layout.menu("NODEVIEW_MT_AddVector") - layout.menu("NODEVIEW_MT_AddMatrix") - layout.menu("NODEVIEW_MT_AddLogic", **icon("SV_LOGIC")) + layout.menu("NODEVIEW_MT_AddNumber", **icon('FONTPREVIEW')) + layout.menu("NODEVIEW_MT_AddVector", **icon('MAN_TRANS')) + layout.menu("NODEVIEW_MT_AddMatrix", **icon('AXIS_SIDE')) + layout.menu("NODEVIEW_MT_AddLogic", **icon("FILTER")) layout.menu("NODEVIEW_MT_AddListOps", **icon('NLA')) layout.separator() layout.menu("NODEVIEW_MT_AddViz", **icon('RESTRICT_VIEW_OFF')) layout.menu("NODEVIEW_MT_AddCNC", **icon('MOD_BUILD')) - layout.menu("NODEVIEW_MT_AddText") + layout.menu("NODEVIEW_MT_AddText", **icon('FILE_TEXT')) layout.menu("NODEVIEW_MT_AddScene", **icon('SCENE_DATA')) layout.menu("NODEVIEW_MT_AddColor", **icon('COLOR')) - layout.menu("NODEVIEW_MT_AddLayout", **icon("SV_LAYOUT")) + layout.menu("NODEVIEW_MT_AddLayout", **icon("SPLITSCREEN")) layout.menu("NODE_MT_category_SVERCHOK_BPY_Data", icon="BLENDER") layout.separator() layout.menu("NODEVIEW_MT_AddNetwork", **icon("OOPS")) -- GitLab From 5e05aff5a9010df8bb96afa5d1d7319f38ac4379 Mon Sep 17 00:00:00 2001 From: Victor Doval <10011941+vicdoval@users.noreply.github.com> Date: Mon, 3 Jun 2019 09:38:36 +0200 Subject: [PATCH 133/137] New Barycentric Transform Node (#2426) * New Barycentric Transform Node * Barycentric T egd_pol input and output * faster numpy implementation matching indices instead of matching data --- .../transforms/barycentric_transform.rst | 66 ++++++ index.md | 1 + nodes/transforms/barycentric_transform.py | 200 ++++++++++++++++++ 3 files changed, 267 insertions(+) create mode 100644 docs/nodes/transforms/barycentric_transform.rst create mode 100644 nodes/transforms/barycentric_transform.py diff --git a/docs/nodes/transforms/barycentric_transform.rst b/docs/nodes/transforms/barycentric_transform.rst new file mode 100644 index 000000000..eeb8f9689 --- /dev/null +++ b/docs/nodes/transforms/barycentric_transform.rst @@ -0,0 +1,66 @@ +Barycentric Transform +===================== + +Functionality +------------- + +The node is coded to perform the transformation of one or many vertices according to the relation of two triangles + +Eaxh triangle is defined by three vectors. + + +Inputs / Parameters +------------------- + + ++----------------------+-------------+----------------------------------------------------------------------+ +| Param | Type | Description | ++======================+=============+======================================================================+ +| **Vertices** | Vertices | Points to calculate | ++----------------------+-------------+----------------------------------------------------------------------+ +| **Edg_Pol** | Int Lists | Edges or pols of the input Vertices (optional) | ++----------------------+-------------+----------------------------------------------------------------------+ +| **Verts Tri Source** | Vertices | It will get the first and last vertices's to define the line segment | ++----------------------+-------------+----------------------------------------------------------------------+ +| **Verts Tri Target** | Float | Minimal distance to accept one point is intersecting. | ++----------------------+-------------+----------------------------------------------------------------------+ + +Advanced Parameters +------------------- + +In the N-Panel (and on the right-click menu) you can find: + +**Implementation**: Choose between MathUtils (Faster transforming light meshes) and NumPy (Faster transforming heavy meshes) + +**Output NumPy**: to get NumPy arrays in stead of regular lists (makes the node faster). Only in the NumPy implementation. + +**Match List**: Define how list with different lengths should be matched. Refers to the matching of groups (one tris couple per group) + +Outputs +------- + +**Vertices**: Transformed vectors. + +**Edg_Pol**: A matched copy of the input Edg_Pol data. + + +Example of usage +---------------- + +The node can be used to place geometry over triangular faces. + +.. image:: https://github.com/vicdoval/sverchok/raw/docs_images/images_for_docs/transforms/barycentric_transform/barycentric_transform_sverchok_blender_adaptative_tris.png + :alt: barycentric_transform/barycentric_transform_sverchok_blender_adaptative_tris.png + + +The List Match option can offer different output combinations, in this case Cyclic is used + +.. image:: https://github.com/vicdoval/sverchok/raw/docs_images/images_for_docs/transforms/barycentric_transform/barycentric_transform_sverchok_blender_list_match.png + :alt: barycentric_transform_sverchok_blender_list_match.png + + +Custom triangular Tessellation in this case Cyclic is used to alternate between the input geometry + +.. image:: https://github.com/vicdoval/sverchok/raw/docs_images/images_for_docs/transforms/barycentric_transform/barycentric_transform_sverchok_blender_triangle_tesselation.png + :alt: barycentric_transform_sverchok_blender_list_match.png + diff --git a/index.md b/index.md index a6168ff70..386195aea 100644 --- a/index.md +++ b/index.md @@ -82,6 +82,7 @@ SvMirrorNode MatrixApplyNode SvSimpleDeformNode + SvBarycentricTransformNode ## Modifier Change SvDeleteLooseNode diff --git a/nodes/transforms/barycentric_transform.py b/nodes/transforms/barycentric_transform.py new file mode 100644 index 000000000..a350dc170 --- /dev/null +++ b/nodes/transforms/barycentric_transform.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 ##### + +from numpy import cross, sqrt, zeros, float32, array, dot +from numpy.linalg import norm, inv +from mathutils import Vector as V +from mathutils.geometry import barycentric_transform + +import bpy +from bpy.props import BoolProperty, EnumProperty +from sverchok.node_tree import SverchCustomTreeNode +from sverchok.data_structure import updateNode, list_match_func, list_match_modes + + +def matrix_def(triangle): + '''Creation of Transform matrix from triangle''' + tri0, tri1, tri2 = triangle[0, :], triangle[1, :], triangle[2, :] + tri_normal = cross(tri1 - tri0, tri2 - tri0) + magnitude = norm(tri_normal) + tri_area = 0.5 * magnitude + tri3 = tri0 + (tri_normal / magnitude)* sqrt(tri_area) + + transform_matrix = zeros([3, 3], dtype=float32) + transform_matrix[0, :] = tri0 - tri3 + transform_matrix[1, :] = tri1 - tri3 + transform_matrix[2, :] = tri2 - tri3 + + return transform_matrix, tri3 + +def prepare_source_data(tri_src): + '''Create the inverted Transformation Matrix and 4th point of the tetrahedron''' + inverted_matrix_s = [] + tri3_src = [] + for tri in tri_src: + np_tri = array(tri) + matrix_trasform_s, tri3 = matrix_def(np_tri) + tri3_src.append(tri3) + inverted_matrix_s.append(inv(matrix_trasform_s).T) + + return inverted_matrix_s, tri3_src + + +def prepare_dest_data(tri_dest): + '''Create Transformation Matrix and 4th point of the tetrahedron''' + tri3_dest = [] + matrix_transform_d = [] + for tri in tri_dest: + np_tri = array(tri) + matrix_trasform, tri3 = matrix_def(np_tri) + matrix_transform_d.append(matrix_trasform) + tri3_dest.append(tri3) + + return matrix_transform_d, tri3_dest + +def compute_barycentric_transform_np(params, matched_index, result, out_numpy, edg_pol_data): + '''NumPy Implementation of a barycentric transform''' + verts, egde_pol, tri_src, tri_dest = params + np_verts = [array(v) for v in verts] + inverted_matrix_s, tri3_src = prepare_source_data(tri_src) + matrix_transform_d, tri3_dest = prepare_dest_data(tri_dest) + + + for v_id, edge_id, tri_src_id, tri_dest_id in zip(*matched_index): + + barycentric_co = dot(inverted_matrix_s[tri_src_id], (np_verts[v_id] - tri3_src[tri_src_id]).T) + cartesian_co = dot(barycentric_co.T, matrix_transform_d[tri_dest_id]) + tri3_dest[tri_dest_id] + + result[0].append(cartesian_co if out_numpy else cartesian_co.tolist()) + if edg_pol_data: + result[1].append(egde_pol[edge_id]) + + +def compute_barycentric_transform_mu(params, result): + '''Port to MathUtils barycentric transform function''' + + tri_src = [V(v) for v in params[2]] + tri_dest = [V(v) for v in params[3]] + sub_result = [] + for vert in params[0]: + point = V(vert) + new_vert = barycentric_transform(point, tri_src[0], tri_src[1], tri_src[2], tri_dest[0], tri_dest[1], tri_dest[2]) + sub_result.append(list(new_vert)) + result.append(sub_result) + + +class SvBarycentricTransformNode(bpy.types.Node, SverchCustomTreeNode): + ''' + Triggers: Adaptive Triangles + Tooltip: Adaptive Triangles. Barycentric transformation between triangles. + ''' + bl_idname = 'SvBarycentricTransformNode' + bl_label = 'Barycentric Transform' + bl_icon = 'MESH_DATA' + + implentation_modes = [ + ("NumPy", "NumPy", "Faster to transform heavy meshes", 0), + ("MathUtils", "MathUtils", "Faster to transform light meshes", 1)] + + output_numpy = BoolProperty( + name='Output NumPy', description='Output NumPy arrays', + default=False, update=updateNode) + + implementation = EnumProperty( + name='Implementation', items=implentation_modes, + description='Choose calculation method', + default="NumPy", update=updateNode) + + list_match = EnumProperty( + name="List Match", + description="Behavior on different list lengths, multiple objects level", + items=list_match_modes, default="REPEAT", + update=updateNode) + + def sv_init(self, context): + '''create sockets''' + sinw = self.inputs.new + sonw = self.outputs.new + sinw('VerticesSocket', 'Vertices') + sinw('StringsSocket', 'Edg_Pol') + sinw('VerticesSocket', 'Verts Tri Source') + sinw('VerticesSocket', 'Verts Tri Target') + + sonw('VerticesSocket', 'Vertices') + sonw('StringsSocket', 'Edg_Pol') + + def draw_buttons_ext(self, context, layout): + '''draw buttons on the N-panel''' + layout.prop(self, "implementation", expand=True) + if self.implementation == "NumPy": + layout.prop(self, "output_numpy", toggle=False) + layout.prop(self, "list_match", text="Match Length List", expand=False) + + def rclick_menu(self, context, layout): + '''right click sv_menu items''' + layout.prop_menu_enum(self, "implementation", text="Implementation") + if self.implementation == "NumPy": + layout.prop(self, "output_numpy", toggle=False) + layout.prop_menu_enum(self, "list_match", text="List Match") + + def get_data(self): + '''get all data from sockets''' + return [s.sv_get(default=[[]]) for s in self.inputs] + + + def process(self): + '''main node function called every update''' + outputs = self.outputs + inputs = self.inputs + if not (outputs[0].is_linked and all(s.is_linked for s in inputs[:1] + inputs[2:])): + return + + result = [[], []] + out_numpy = self.output_numpy + edg_pol_data = inputs[1].is_linked and outputs[1].is_linked + params = self.get_data() + + if self.implementation == 'NumPy': + + matched_indexes = list_match_func[self.list_match]([list(range(len(p))) for p in params]) + + compute_barycentric_transform_np(params, matched_indexes, result, out_numpy, edg_pol_data) + + else: + + group = list_match_func[self.list_match](params) + + for params in zip(*group): + compute_barycentric_transform_mu(params, result[0]) + if edg_pol_data: + result[1].append(params[1]) + + outputs[0].sv_set(result[0]) + if edg_pol_data: + outputs[1].sv_set(result[1]) + + + +def register(): + '''register class in Blender''' + bpy.utils.register_class(SvBarycentricTransformNode) + + +def unregister(): + '''unregister class in Blender''' + bpy.utils.unregister_class(SvBarycentricTransformNode) -- GitLab From 5bf63c8bde1525c44f3fbb7fc96be117e60a8fd4 Mon Sep 17 00:00:00 2001 From: nortikin Date: Sat, 8 Jun 2019 14:53:52 +0300 Subject: [PATCH 134/137] menu colors to materials name and BPY to blender name --- index.md | 4 ++-- menu.py | 2 +- nodes/{color => material}/__init__.py | 0 nodes/{color => material}/color_in_mk1.py | 0 nodes/{color => material}/color_input.py | 0 nodes/{color => material}/color_out_mk1.py | 0 nodes/{color => material}/color_uv_texture.py | 0 nodes/{color => material}/formula_color.py | 0 nodes/{color => material}/sample_uv_color.py | 0 nodes/{color => material}/vertex_colors_mk3.py | 0 ui/nodeview_space_menu.py | 6 +++--- 11 files changed, 6 insertions(+), 6 deletions(-) rename nodes/{color => material}/__init__.py (100%) rename nodes/{color => material}/color_in_mk1.py (100%) rename nodes/{color => material}/color_input.py (100%) rename nodes/{color => material}/color_out_mk1.py (100%) rename nodes/{color => material}/color_uv_texture.py (100%) rename nodes/{color => material}/formula_color.py (100%) rename nodes/{color => material}/sample_uv_color.py (100%) rename nodes/{color => material}/vertex_colors_mk3.py (100%) diff --git a/index.md b/index.md index 2a61ba65b..39ab517e2 100644 --- a/index.md +++ b/index.md @@ -267,7 +267,7 @@ SvDebugPrintNode SvStethoscopeNodeMK2 -## BPY Data +## Blender SvGetPropNode SvSetPropNode SvObjRemoteNodeMK2 @@ -293,7 +293,7 @@ SvDupliInstancesMK4 SvFCurveInNodeMK1 -## Color +## Material SvColorsInNodeMK1 SvColorInputNode SvColorsOutNodeMK1 diff --git a/menu.py b/menu.py index 5446cfb07..3dc987bb3 100644 --- a/menu.py +++ b/menu.py @@ -95,7 +95,7 @@ def juggle_and_join(node_cats): node_cats["List Main"].extend(node_refs) objects_cat = node_cats.pop('Objects') - node_cats['BPY Data'].extend(objects_cat) + node_cats['Blender'].extend(objects_cat) # add extended gens to Gens menu gen_ext = node_cats.pop("Generators Extended") diff --git a/nodes/color/__init__.py b/nodes/material/__init__.py similarity index 100% rename from nodes/color/__init__.py rename to nodes/material/__init__.py diff --git a/nodes/color/color_in_mk1.py b/nodes/material/color_in_mk1.py similarity index 100% rename from nodes/color/color_in_mk1.py rename to nodes/material/color_in_mk1.py diff --git a/nodes/color/color_input.py b/nodes/material/color_input.py similarity index 100% rename from nodes/color/color_input.py rename to nodes/material/color_input.py diff --git a/nodes/color/color_out_mk1.py b/nodes/material/color_out_mk1.py similarity index 100% rename from nodes/color/color_out_mk1.py rename to nodes/material/color_out_mk1.py diff --git a/nodes/color/color_uv_texture.py b/nodes/material/color_uv_texture.py similarity index 100% rename from nodes/color/color_uv_texture.py rename to nodes/material/color_uv_texture.py diff --git a/nodes/color/formula_color.py b/nodes/material/formula_color.py similarity index 100% rename from nodes/color/formula_color.py rename to nodes/material/formula_color.py diff --git a/nodes/color/sample_uv_color.py b/nodes/material/sample_uv_color.py similarity index 100% rename from nodes/color/sample_uv_color.py rename to nodes/material/sample_uv_color.py diff --git a/nodes/color/vertex_colors_mk3.py b/nodes/material/vertex_colors_mk3.py similarity index 100% rename from nodes/color/vertex_colors_mk3.py rename to nodes/material/vertex_colors_mk3.py diff --git a/ui/nodeview_space_menu.py b/ui/nodeview_space_menu.py index 925f06c16..1e047df6c 100644 --- a/ui/nodeview_space_menu.py +++ b/ui/nodeview_space_menu.py @@ -123,9 +123,9 @@ class NODEVIEW_MT_Dynamic_Menu(bpy.types.Menu): layout.menu("NODEVIEW_MT_AddCNC", **icon('MOD_BUILD')) layout.menu("NODEVIEW_MT_AddText", **icon('FILE_TEXT')) layout.menu("NODEVIEW_MT_AddScene", **icon('SCENE_DATA')) - layout.menu("NODEVIEW_MT_AddColor", **icon('COLOR')) + layout.menu("NODEVIEW_MT_AddMaterial", **icon('COLOR')) layout.menu("NODEVIEW_MT_AddLayout", **icon("SPLITSCREEN")) - layout.menu("NODE_MT_category_SVERCHOK_BPY_Data", icon="BLENDER") + layout.menu("NODE_MT_category_SVERCHOK_Blender", icon="BLENDER") layout.separator() layout.menu("NODEVIEW_MT_AddNetwork", **icon("OOPS")) layout.menu("NODEVIEW_MT_AddBetas", **icon("SV_BETA")) @@ -185,7 +185,7 @@ classes = [ make_class('CNC', "CNC"), make_class('Text', "Text"), make_class('Scene', "Scene"), - make_class('Color', "Color"), + make_class('Material', "Material"), make_class('Layout', "Layout"), make_class('Listmain', "List Main"), make_class('Liststruct', "List Struct"), -- GitLab From 67745922959a0519e690be7b24c30cad0f0a4686 Mon Sep 17 00:00:00 2001 From: nortikin Date: Sat, 8 Jun 2019 14:58:03 +0300 Subject: [PATCH 135/137] maerial menu item arrangement in menu --- ui/nodeview_space_menu.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/nodeview_space_menu.py b/ui/nodeview_space_menu.py index 1e047df6c..b5304b6e5 100644 --- a/ui/nodeview_space_menu.py +++ b/ui/nodeview_space_menu.py @@ -123,8 +123,8 @@ class NODEVIEW_MT_Dynamic_Menu(bpy.types.Menu): layout.menu("NODEVIEW_MT_AddCNC", **icon('MOD_BUILD')) layout.menu("NODEVIEW_MT_AddText", **icon('FILE_TEXT')) layout.menu("NODEVIEW_MT_AddScene", **icon('SCENE_DATA')) - layout.menu("NODEVIEW_MT_AddMaterial", **icon('COLOR')) layout.menu("NODEVIEW_MT_AddLayout", **icon("SPLITSCREEN")) + layout.menu("NODEVIEW_MT_AddMaterial", **icon('COLOR')) layout.menu("NODE_MT_category_SVERCHOK_Blender", icon="BLENDER") layout.separator() layout.menu("NODEVIEW_MT_AddNetwork", **icon("OOPS")) -- GitLab From 7a6f1b7990bcc7f0deefb48d58bc61f1231f33d0 Mon Sep 17 00:00:00 2001 From: nortikin Date: Sun, 9 Jun 2019 00:44:59 +0300 Subject: [PATCH 136/137] back of integer and float --- {old_nodes => nodes/number}/float.py | 0 {old_nodes => nodes/number}/integer.py | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename {old_nodes => nodes/number}/float.py (100%) rename {old_nodes => nodes/number}/integer.py (100%) diff --git a/old_nodes/float.py b/nodes/number/float.py similarity index 100% rename from old_nodes/float.py rename to nodes/number/float.py diff --git a/old_nodes/integer.py b/nodes/number/integer.py similarity index 100% rename from old_nodes/integer.py rename to nodes/number/integer.py -- GitLab From d1dc56ee163240d72fcae8325d141c7e09f48ab3 Mon Sep 17 00:00:00 2001 From: nortikin Date: Sun, 9 Jun 2019 00:57:34 +0300 Subject: [PATCH 137/137] cylinder go back --- {old_nodes => nodes/generator}/cylinder.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename {old_nodes => nodes/generator}/cylinder.py (100%) diff --git a/old_nodes/cylinder.py b/nodes/generator/cylinder.py similarity index 100% rename from old_nodes/cylinder.py rename to nodes/generator/cylinder.py -- GitLab