diff --git a/core/update_system_mem.py b/core/update_system_mem.py new file mode 100644 index 0000000000000000000000000000000000000000..77fbce762e1dba26ef1ea0a64651696d54527e7c --- /dev/null +++ b/core/update_system_mem.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 sverchok.core.socket_data import SvNoDataError +from sverchok.utils.logging import debug, exception +from sverchok.utils.profile import profile + + +swich_off_debug = lambda _: None +debug_process = swich_off_debug +#debug = swich_off_debug + +list_debugs = [debug, debug_process] +if False: + for d in list_debugs: + d = swich_off_debug + +# Notes +# Reroute nodes does not have "other" and "set_color" methods. + + +class UpdateDataStructure: + def __init__(self): + self._node_has_changed = dict() + self._sock_has_changed = dict() + self._prop_has_changed = dict() + self._sock_cache = dict() + self._prop_cache = dict() + self._root_nodes = dict() + self._ani_nodes = dict() + self._error_nodes = dict() + + @staticmethod + def _get_id(obj, prop=None): + if prop: + return str(hash(obj.id_data.name + obj.name + prop)) + elif isinstance(obj, bpy.types.NodeSocket): + # avoiding the same names of input and output sockets + position = 'output' if obj.is_output else 'input' + return str(hash(obj.id_data.name + obj.node.name + position + obj.identifier)) + elif isinstance(obj, bpy.types.Node): + return str(hash(obj.id_data.name + obj.name)) + else: + raise TypeError + + @staticmethod + def get_node_props(node): + main = {k: v for k, v in node.items()} + if hasattr(node, 'additional_properties'): + main.update(node.additional_properties()) + return main + + def _update_sock_cache(self, sock): + cache = self._sock_cache + status = self._sock_has_changed + id = self._get_id(sock) + if not sock.is_output and sock.is_linked: + if id not in cache: + cache[id] = sock.links[0].from_node.name + status[id] = True + debug('Sock "{}" in node "{}" was linked'.format(sock.name, sock.node.name)) + elif cache[id] == sock.links[0].from_node.name: + status[id] = False + else: + cache[id] = sock.links[0].from_node.name + status[id] = True + debug('Sock "{}" in node "{}" was relinked'.format(sock.name, sock.node.name)) + elif not sock.is_output: + if id not in cache: + status[id] = False + elif cache[id] == '': + status[id] = False + else: + cache[id] = '' + status[id] = True + debug('Sock "{}" in node "{}" was unlinked'.format(sock.name, sock.node.name)) + + def _update_prop_cache(self, node): + # updating prop_cache and status of properties + cache = self._prop_cache + status = self._prop_has_changed + props = self.get_node_props(node) + + def assign_value(cache, id, value): + try: + cache[id] = value[:] + except TypeError: + cache[id] = value + + for k in props: + v = props[k] + id = self._get_id(node, k) + if id not in cache: + status[id] = True + assign_value(cache, id, v) + debug('Prop "{}" in node "{}" was changed (init)'.format(k, node.name)) + else: + # iterable objects of blender props should be compared one by one + try: + equal = True + for nested_v, nested_c in zip(v, cache[id]): + if nested_v != nested_c: + equal = False + break + except TypeError: + equal = v == cache[id] + finally: + if equal: + status[id] = False + else: + status[id] = True + assign_value(cache, id, v) + debug('Prop "{}" in node "{}" was changed'.format(k, node.name)) + + def has_changed(self, obj, prop=None): + id = self._get_id(obj, prop) + if prop == 'ANY': + prop_names = [k for k in self.get_node_props(obj)] + return True in [self._prop_has_changed[self._get_id(obj, pn)] for pn in prop_names] + elif prop: + return self._prop_has_changed[id] + elif isinstance(obj, bpy.types.NodeSocket): + return self._sock_has_changed[id] + elif isinstance(obj, bpy.types.Node): + return self._node_has_changed.setdefault(id, False) + else: + raise TypeError + + def _set_has_changed(self, obj, change=False, prop=None): + id = self._get_id(obj, prop) + if prop: + self._prop_has_changed[id] = change + elif isinstance(obj, bpy.types.NodeSocket): + self._sock_has_changed[id] = change + elif isinstance(obj, bpy.types.Node): + self._node_has_changed[id] = change + else: + raise TypeError + + def _set_error_node(self, node, err): + self._error_nodes[self._get_id(node)] = err + + def _get_error_node(self, node): + return self._error_nodes.get(self._get_id(node)) + + +class UpdateSystemMem(UpdateDataStructure): + def _get_root_nodes(self, node_tree): + if node_tree.name in self._root_nodes: + return self._root_nodes[node_tree.name] + else: + root_nodes = [] + for node in node_tree.nodes: + if True not in [soc.is_linked for soc in node.inputs]: + root_nodes.append(node) + self._root_nodes[node_tree.name] = root_nodes + return root_nodes + + def _get_animate_nodes(self, node_tree): + if node_tree.name in self._ani_nodes: + return self._ani_nodes[node_tree.name] + else: + ani_nodes = [node for node in node_tree.nodes if node.bl_idname == 'SvFrameInfoNodeMK2'] + self._ani_nodes[node_tree.name] = ani_nodes + return ani_nodes + + @staticmethod + def _get_next_nodes(node): + next_nodes = [] + for soc in node.outputs: + for link in soc.links: + next_nodes.append(link.to_node) + return next_nodes + + def _update_prop_soc_status(self, node_tree): + for node in node_tree.nodes: + self._update_prop_cache(node) + for sock in node.inputs: + self._update_sock_cache(sock) + + def _update_nodes_status(self, start_nodes): + root_nodes = start_nodes[:] # input was not changed, check only property + was_changed_nodes = [] # input was changed + check_nodes = [] # ready for check, should get through all tests + done = set() # if node not in so node was not checked yet + + def set_changed_nodes(node): + nonlocal was_changed_nodes, done, self + next_nodes = [nn for nn in self._get_next_nodes(node) if nn.name not in done] + self._set_has_changed(node, True) + [self._set_has_changed(sock, True) for sock in node.outputs] + was_changed_nodes.extend(next_nodes) + done.add(node.name) + + def set_check_nodes(node): + nonlocal check_nodes, done, self + next_nodes = [nn for nn in self._get_next_nodes(node) if nn.name not in done] + self._set_has_changed(node, False) + [self._set_has_changed(sock, False) for sock in node.outputs] + check_nodes.extend(next_nodes) + done.add(node.name) + + def should_node_changed(node): + nonlocal self + for sock in node.inputs: + if self.has_changed(sock): + debug('"{}" is changed - input sock - "{}" - was relinked'.format(node.name, sock.name)) + return True + if self.has_changed(node, prop='ANY'): + debug('"{}" is changed - a prop was changed'.format(node.name)) + return True + for sock in node.inputs: + if sock.links and self.has_changed(sock.links[0].from_socket): + debug('"{}" is changed - data of "{}" - was changed'.format( + node.name, sock.links[0].from_node.name)) + return True + for sock in node.outputs: + for link in sock.links: + if self.has_changed(link.to_socket): + debug('"{}" is changed - output sock - "{}" - was relinked'.format(node.name, sock.name)) + return True + return False + + while was_changed_nodes or check_nodes or root_nodes: + if was_changed_nodes: + node = was_changed_nodes.pop() + set_changed_nodes(node) + debug('Changed - "{}" - input was changed'.format(node.name)) + elif check_nodes: + node = check_nodes.pop() + if False in [soc.links[0].from_node.name in done for soc in node.inputs if soc.links]: + debug('Cant decide - "{}" - input is undefined'.format(node.name)) + continue + else: + if not should_node_changed(node): + set_check_nodes(node) + debug('Remain - "{}"'.format(node.name)) + else: + set_changed_nodes(node) + + elif root_nodes: + node = root_nodes.pop() + if should_node_changed(node): + set_changed_nodes(node) + else: + set_check_nodes(node) + debug('Remain - "{}"'.format(node.name)) + + def _process_node(self, node): + try: + node.process() + self._set_error_node(node, None) + except Exception as err: + self._set_error_node(node, err) + exception("Node {} had exception: {}".format(node.name, err)) + + def _process_nodes_hard(self, start_nodes): + debug_process('==========Start process nodes==========') + next_nodes = start_nodes[:] + done = set() + while next_nodes: + debug_process('Next nodes in process - "{}"'.format([node.name for node in next_nodes])) + node = next_nodes.pop() + next_nodes.extend([nn for nn in self._get_next_nodes(node) if nn not in done]) + if self.has_changed(node) and hasattr(node, 'process'): + previous_nodes = [sock.links[0].from_node for sock in node.inputs if sock.links] + if True not in [self.has_changed(node) for node in previous_nodes]: + debug_process('Process - "{}"'.format(node.name)) + self._process_node(node) + elif False in [node.name in done for node in previous_nodes]: + debug_process('Node "{}" is skipped - input is undefined'.format(node.name)) + else: + debug_process('Process - "{}"'.format(node.name)) + self._process_node(node) + done.add(node.name) + + def _process_nodes_easy(self, start_nodes): + next_nodes = start_nodes[:] + done = set() + while next_nodes: + node = next_nodes.pop() + next_nodes.extend([nn for nn in self._get_next_nodes(node) if nn not in done]) + self._set_has_changed(node, change=True) + if hasattr(node, 'process'): + self._process_node(node) + done.add(node.name) + + def _set_update_color(self, node): + if self.has_changed(node): + node.set_color((1, 0, 0.3)) + else: + node.set_color() + + @staticmethod + def _set_error_color(node, err): + if isinstance(err, SvNoDataError): + node.set_color(color=(1, 0.3, 0)) + else: + node.set_color(color=(0.8, 0.0, 0)) + + def _set_colors(self, node_tree): + for node in node_tree.nodes: + if hasattr(node, 'set_color'): + err = self._get_error_node(node) + if err: + self._set_error_color(node, err) + else: + self._set_update_color(node) + + + def _update_tree(self, node_tree): + self._root_nodes.pop(node_tree.name, None) + self._ani_nodes.pop(node_tree.name, None) + self._update_prop_soc_status(node_tree) + root_nodes = self._get_root_nodes(node_tree) + self._update_nodes_status(root_nodes) + self._process_nodes_hard(root_nodes) + + def _update_from_node(self, node): + self._update_prop_cache(node) + self._process_nodes_easy([node]) + + def _update_from_nodes(self, nodes): + [self._update_prop_cache(node) for node in nodes] + self._process_nodes_easy(nodes) + + @profile + def update(self, obj=None, props=None): + sv_types = {'SverchCustomTreeType', 'SverchGroupTreeType'} + node_trees = list(ng for ng in bpy.data.node_groups if ng.bl_idname in sv_types and ng.nodes) + self._node_has_changed.clear() + if props == 'ANIMATE': + debug('===========Start animation update================') + ani_nodes = [] + for ng in node_trees: + if ng.sv_process and ng.sv_animate: + ani_nodes.extend([node for node in self._get_animate_nodes(ng)]) + debug('Next animate nodes was found - {}'.format([node.name for node in ani_nodes])) + self._update_from_nodes(ani_nodes) + elif props: + raise ValueError + elif obj is None: + debug('=============Start all trees update===============') + [self._update_tree(ng) for ng in node_trees if ng.sv_process] + elif isinstance(obj, bpy.types.NodeTree): + debug('=============Start "{}" tree update==============='.format(obj.name)) + if obj.sv_process: + self._update_tree(obj) + elif isinstance(obj, bpy.types.Node): + debug('=============Start "{}" node update==============='.format(obj.name)) + if obj.id_data.sv_process: + self._update_from_node(obj) + else: + raise TypeError + [self._set_colors(ng) for ng in node_trees] + + +tree_updater = UpdateSystemMem() diff --git a/data_structure.py b/data_structure.py index fbc32660d15364c5018126cf6c1776e164c1fa21..aabe229c15f1f262385aad35efa6cf4729628c1e 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]] diff --git a/docs/nodes/analyzers/analyzers_index.rst b/docs/nodes/analyzers/analyzers_index.rst index 0d5d54f98379c3993c49a721f1e8532c06603830..d04b26c3dadfa049d923230d1df6b247b937454e 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 @@ -19,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/deformation.rst b/docs/nodes/analyzers/deformation.rst new file mode 100644 index 0000000000000000000000000000000000000000..fdad47f6d47ac9b7083c64a31470546828673bfc --- /dev/null +++ b/docs/nodes/analyzers/deformation.rst @@ -0,0 +1,54 @@ +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 | ++-------------------+---------------+-------------+-----------------------------------------------+ + +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. + +- **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 +---------------- +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 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 diff --git a/docs/nodes/analyzers/path_length.rst b/docs/nodes/analyzers/path_length.rst new file mode 100644 index 0000000000000000000000000000000000000000..b7b9ad23e45fb78ca58d7a432f00b303271a229d --- /dev/null +++ b/docs/nodes/analyzers/path_length.rst @@ -0,0 +1,48 @@ +Path Length +=========== + +Functionality +------------- + +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 +------------------- + + ++------------------+---------------+-------------+--------------------------------------------------+ +| Param | Type | Default | Description | ++==================+===============+=============+==================================================+ +| **Vertices** | Vertices | None | Vertices of the edges / path | ++------------------+---------------+-------------+--------------------------------------------------+ +| **Edges** | Strings | None | Edges referenced to vertices | ++------------------+---------------+-------------+--------------------------------------------------+ +| **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). + +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 length. + + +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 segments + +.. image:: https://user-images.githubusercontent.com/10011941/51251933-c4449e00-199a-11e9-99b8-fa53c8586484.png + :alt: PathLengthDemo2.PNG + +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/docs/nodes/generator/cylinder_mk2.rst b/docs/nodes/generator/cylinder_mk2.rst new file mode 100644 index 0000000000000000000000000000000000000000..117b64c194a236acaf494015ddbdd36b9d9ab580 --- /dev/null +++ b/docs/nodes/generator/cylinder_mk2.rst @@ -0,0 +1,93 @@ +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**, **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. | ++-----------------------+---------+---------+-------------------------------------------------------+ +| **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 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] | [ ] | The scale modulating the radius of the parallels. [1] | ++-----------------------+---------+---------+-------------------------------------------------------+ +| **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. [2] | ++-----------------------+---------+---------+-------------------------------------------------------+ +| **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 +---------- +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/or in 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 cylinder shapes. + +If **Separate** is True, the node will output the parallel sections as separate lists (verts, edges and polys). + + diff --git a/docs/nodes/generator/line_mk3.rst b/docs/nodes/generator/line_mk3.rst new file mode 100644 index 0000000000000000000000000000000000000000..f1827eada643912d5a3e2a9e64133e768aa6a322 --- /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/docs/nodes/generators_extended/super_ellipsoid.rst b/docs/nodes/generators_extended/super_ellipsoid.rst new file mode 100644 index 0000000000000000000000000000000000000000..f0450984b35aef00f5ea600f782045825772a636 --- /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 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. + + +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/docs/nodes/list_main/statistics.rst b/docs/nodes/list_main/statistics.rst new file mode 100644 index 0000000000000000000000000000000000000000..c63a29f195db83090e6d9f6f007e1feb98b442d3 --- /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/docs/nodes/list_mutators/combinatorics.rst b/docs/nodes/list_mutators/combinatorics.rst new file mode 100644 index 0000000000000000000000000000000000000000..67104801dfc301ffd33813bbaa89f0af9c9de06e --- /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/docs/nodes/list_mutators/polygon_sort.rst b/docs/nodes/list_mutators/polygon_sort.rst new file mode 100644 index 0000000000000000000000000000000000000000..e3b844512169dbce76a068d170518f0f297e0059 --- /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/docs/nodes/modifier_change/input_switch.rst b/docs/nodes/modifier_change/input_switch.rst new file mode 100644 index 0000000000000000000000000000000000000000..7c14d16b02696418ac83df1ac83e74d6cd67f830 --- /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/docs/nodes/modifier_change/mesh_switch.rst b/docs/nodes/modifier_change/mesh_switch.rst new file mode 100644 index 0000000000000000000000000000000000000000..d84260e76fae9db4897971e9a3def68140c1d063 --- /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/docs/nodes/modifier_change/smooth.rst b/docs/nodes/modifier_change/smooth.rst index 7b1da4b4ead9d14b5e10f6faee8b9c4f69dd1df5..b9dbbf2e9280066b126366a85fcd6583daeb5be5 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. diff --git a/docs/nodes/number/formula3.rst b/docs/nodes/number/formula3.rst new file mode 100644 index 0000000000000000000000000000000000000000..e05762e4d09bc9c18d14e587571d86af99126ccc --- /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 + diff --git a/docs/nodes/number/mix_inputs.rst b/docs/nodes/number/mix_inputs.rst new file mode 100644 index 0000000000000000000000000000000000000000..06f3964b9b8234da6335fbb022bca70080213dcb --- /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 03d00bcd5af2b6576c53681e64e5d01caa8cc01d..0000000000000000000000000000000000000000 --- 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/docs/nodes/quaternion/quaternion_in.rst b/docs/nodes/quaternion/quaternion_in.rst new file mode 100644 index 0000000000000000000000000000000000000000..6711f68877180276be44e2e6f8a1266fe307157a --- /dev/null +++ b/docs/nodes/quaternion/quaternion_in.rst @@ -0,0 +1,69 @@ +Quaternion In +------------- + +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. + ++===============+================================================================+ +| 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 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 +====== + +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) | ++---------------+----------------------------------+ +| SCALAR-VECTOR | Scalar (float) & Vector (vector) | ++---------------+----------------------------------+ +| 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 0000000000000000000000000000000000000000..392c11923d48286b0a4e93c3266a6a6e2bbf16a8 --- /dev/null +++ b/docs/nodes/quaternion/quaternion_math.rst @@ -0,0 +1,172 @@ +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 | +| QUADRANCE | Q | S | Quadrance of a quaternion | +| 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) + + +* 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 +====== + +**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 0000000000000000000000000000000000000000..8872e5a7704a3cb5ee8610cd52d68719a06d6dd2 --- /dev/null +++ b/docs/nodes/quaternion/quaternion_out.rst @@ -0,0 +1,64 @@ +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] | ++---------------+----------------------------------------------------------------+ +| 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 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). +[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) | ++---------------+----------------------------------+ +| 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/index.md b/index.md index 30a384e3ef4585edf612dbc7bdea6d972025f2f7..076bba081c030c3eaff20ae7bc1a57a27311148b 100644 --- a/index.md +++ b/index.md @@ -10,12 +10,13 @@ > Failing to follow these points will break the node category parser. ## Generator - SvLineNodeMK2 + SvLineNodeMK3 SvPlaneNodeMK2 SvNGonNode SvBoxNode SvCircleNode CylinderNode + SvCylinderNodeMK2 SphereNode SvIcosphereNode SvTorusNode @@ -42,6 +43,7 @@ SvTorusKnotNode SvRingNode SvEllipseNode + SvSuperEllipsoidNode SvSmoothLines ## Analyzers @@ -49,6 +51,7 @@ SvVolumeNode AreaNode DistancePPNode + SvPathLengthNode CentersPolsNodeMK2 CentersPolsNodeMK3 GetNormalsNode @@ -64,6 +67,8 @@ SvProportionalEditNode SvRaycasterLiteNode SvOBJInsolationNode + EvaluateImageNode + SvDeformationNode ## Transforms SvRotationNode @@ -83,6 +88,7 @@ PolygonBoomNode Pols2EdgsNode SvMeshJoinNode + SvMeshSwitchNode --- SvBevelNode SvSubdivideNode @@ -105,6 +111,8 @@ SvVertMaskNode SvTransformSelectNode SvSplitEdgesNode + --- + SvInputSwitchNode ## Modifier Make LineConnectNodeMK2 @@ -134,6 +142,7 @@ SvListModifierNode SvFixEmptyObjectsNode SvDatetimeStrings + SvPolygonSortNode ## List Main ListJoinNode @@ -144,6 +153,7 @@ ListMatchNode ListFuncNode SvListDecomposeNode + SvListStatisticsNode ## List Struct ShiftNodeMK2 @@ -165,6 +175,7 @@ ScalarMathNode SvScalarMathNodeMK2 Formula2Node + SvFormulaNodeMk3 SvExecNodeMod --- GenListRangeIntNode @@ -177,7 +188,7 @@ SvRndNumGen RandomNode SvEasingNode - SvMixNumbersNode + SvMixInputsNode ## Vector GenVectorsNode @@ -310,6 +321,8 @@ SvSetCustomMeshNormals --- SvSpiralNode + SvExportGcodeNode + SvCombinatoricsNode ## Alpha Nodes SvCurveViewerNode @@ -340,4 +353,7 @@ SvOffsetLineNode SvContourNode SvPlanarEdgenetToPolygons - + --- + SvQuaternionOutNode + SvQuaternionInNode + SvQuaternionMathNode diff --git a/json_examples/Architecture/ROADS_LARGE_nikitron_2014.json b/json_examples/Architecture/ROADS_LARGE_nikitron_2014.json index 6d486ed959f76c334f3b7832367ed9e318c0a4d8..230efdff8aef3f0a4671755bf2993c5ebd207cea 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 d856a1c2d9fe9eeb2efe6f1c047949dceca2c835..a07c30e05c45a451dbd1c9403265c0b1e5ba375b 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 9bcc586ecebcf556a2697fb5eb46b4dfca8e57f4..29a658d2eb950d5becd0e2ee83ebb9d62d032a04 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 5c1f1e649fcb9fb088f11e4fda0723fc6907a11e..a1922ed7cf1f884c95c3258ddd92b0e9b023004b 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/node_scripts/templates/elfnor/tree_generator.py b/node_scripts/templates/elfnor/tree_generator.py index 9e5b88d52c104361a3ee828466108a454cbaadc6..dcea60d0fdeff639b6613010e6886f106bbece06 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=[]): diff --git a/node_tree.py b/node_tree.py index 645855b17ad9f43aea129f019a64e2505c8e2d65..133fd94f288474466865862d77df96f97f5f10ec 100644 --- a/node_tree.py +++ b/node_tree.py @@ -50,9 +50,10 @@ 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 +from sverchok.core.update_system_mem import tree_updater from sverchok.utils import get_node_class_reference from sverchok.utils.sv_node_utils import recursive_framed_location_finder @@ -71,6 +72,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 +91,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 +185,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 +205,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 +248,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 +271,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 +300,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 +333,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 +398,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" @@ -537,7 +556,9 @@ class SverchCustomTree(NodeTree, SvNodeTreeCommon): bl_icon = 'RNA' def turn_off_ng(self, context): - process_tree(self) + addon = bpy.context.user_preferences.addons.get(sverchok.__name__) + if addon.preferences.old_update_system: + process_tree(self) # should turn off tree. for now it does by updating it whole # should work something like this @@ -545,10 +566,24 @@ class SverchCustomTree(NodeTree, SvNodeTreeCommon): # for node in outputs: # node.disable() + # with new update system such approach can be done + else: + if self.sv_show: + [setattr(node, 'activate', True) for node in self.nodes if node.bl_idname == 'ViewerNode2'] + else: + [setattr(node, 'activate', False) for node in self.nodes if node.bl_idname == 'ViewerNode2'] + + def update_ng(self, context): + # updating current node tree after switcher "process layout" is pressed on + # mainly from N panel + addon = bpy.context.user_preferences.addons.get(sverchok.__name__) + if not addon.preferences.old_update_system: + tree_updater.update(self) + sv_animate = BoolProperty(name="Animate", default=True, description='Animate this layout') sv_show = BoolProperty(name="Show", default=True, description='Show this layout', update=turn_off_ng) sv_bake = BoolProperty(name="Bake", default=True, description='Bake this layout') - sv_process = BoolProperty(name="Process", default=True, description='Process layout') + sv_process = BoolProperty(name="Process", default=True, description='Process layout', update=update_ng) sv_user_colors = StringProperty(default="") def update(self): @@ -563,20 +598,30 @@ class SverchCustomTree(NodeTree, SvNodeTreeCommon): Process the Sverchok node tree if animation layers show true. For animation callback/handler """ - if self.sv_animate: + addon = bpy.context.user_preferences.addons.get(sverchok.__name__) + + if addon.preferences.old_update_system and self.sv_animate: process_tree(self) + else: + tree_updater.update(props='ANIMATE') def process(self): """ process the Sverchok tree upon editor changes from handler """ - if self.has_changed: - self.build_update_list() - self.has_changed = False - if self.is_frozen(): - return - if self.sv_process: - process_tree(self) + addon = bpy.context.user_preferences.addons.get(sverchok.__name__) + if addon.preferences.old_update_system: + + if self.has_changed: + self.build_update_list() + self.has_changed = False + if self.is_frozen(): + return + if self.sv_process: + process_tree(self) + + else: + tree_updater.update(self) self.has_changed = False @@ -654,11 +699,17 @@ class SverchCustomTreeNode: return override return self._implicit_conversion_policy.get(input_socket_name, DefaultImplicitConversionPolicy) - def set_color(self): - color = color_def.get_color(self.bl_idname) + def set_color(self, color=None): + # calling method without parameters will switch color node to default color + # if such color does not exist for node of such type will switch off coloring node at all + # with parameter color will be set anyway + if color is None: + color = color_def.get_color(self.bl_idname) if color: self.use_custom_color = True self.color = color + else: + self.use_custom_color = False def create_sockets(self): '''Create node input and output sockets from @@ -787,13 +838,18 @@ class SverchCustomTreeNode: if self.id_data.is_frozen(): return - if data_structure.DEBUG_MODE: + addon = bpy.context.user_preferences.addons.get(sverchok.__name__) + + if addon.preferences.old_update_system and data_structure.DEBUG_MODE: a = time.perf_counter() process_from_node(self) b = time.perf_counter() debug("Partial update from node %s in %s", self.name, round(b - a, 4)) - else: + elif addon.preferences.old_update_system: process_from_node(self) + else: + tree_updater.update(self) + elif self.id_data.bl_idname == "SverchGroupTreeType": monad = self.id_data for instance in monad.instances: @@ -804,7 +860,7 @@ class SverchCustomTreeNode: classes = [ SverchCustomTree, VerticesSocket, MatrixSocket, StringsSocket, - SvColorSocket, SvQuaternionSocket, SvDummySocket, + SvColorSocket, SvQuaternionSocket, SvDummySocket, SvSeparatorSocket, SvLinkNewNodeInput, ] diff --git a/nodes/analyzer/deformation.py b/nodes/analyzer/deformation.py new file mode 100644 index 0000000000000000000000000000000000000000..5dba5b4f94b35bcfed1b5c9d5fae717d0f3f8880 --- /dev/null +++ b/nodes/analyzer/deformation.py @@ -0,0 +1,252 @@ +# ##### 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) + elong = dist - dist_rest + + return elong + + +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) + 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: + 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_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_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 + deformation[np_pols[mask, i], pol_id[mask]] += pol_def_to_vert[mask] + + else: + for i in range(pols_sides_max): + deformation[np_pols[:, i], pol_id] += pol_def_to_vert + + return np.sum(deformation, axis=1) + + +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) + defromation_area = area_calc(np_verts_n, area_params) + + return defromation_area - relax_area + + +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) + elong = edge_elongation(np_verts, np_verts_n, np_edges) + result[0].append(elong if gates[4] else elong.tolist()) + if gates[1]: + 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) + 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]: + 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 SvDeformationNode(bpy.types.Node, SverchCustomTreeNode): + ''' + Triggers: Measure deformation + Tooltip: Deformation between to states, edge elong a area variation + ''' + bl_idname = 'SvDeformationNode' + bl_label = 'Deformation' + bl_icon = 'MOD_SIMPLEDEFORM' + + output_numpy = BoolProperty( + name='Output NumPy', description='output NumPy arrays', + default=False, update=updateNode) + + def draw_buttons_ext(self, context, layout): + '''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") + sinw('VerticesSocket', "Distort Verts") + sinw('StringsSocket', "Edges") + sinw('StringsSocket', "Pols") + + 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=[[]]) + 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): + '''main node function called every update''' + so = self.outputs + if not self.ready(): + return + + 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(self.output_numpy) + meshes = self.get_data() + + result = calc_deformations(meshes, gates, result) + + if gates[0]: + so['Edges Def'].sv_set(result[0]) + if gates[1]: + so['Vert Edge Def'].sv_set(result[1]) + if gates[2]: + so['Pols Def'].sv_set(result[2]) + if gates[3]: + so['Vert Pol Def'].sv_set(result[3]) + + +def register(): + '''register class in Blender''' + bpy.utils.register_class(SvDeformationNode) + + +def unregister(): + '''unregister class in Blender''' + bpy.utils.unregister_class(SvDeformationNode) diff --git a/nodes/analyzer/evaluate_image.py b/nodes/analyzer/evaluate_image.py new file mode 100644 index 0000000000000000000000000000000000000000..dd77a94965a034751c475889417a8fbe2245d31e --- /dev/null +++ b/nodes/analyzer/evaluate_image.py @@ -0,0 +1,211 @@ +# ##### 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") + self.outputs.new('StringsSocket', "A") + self.outputs.new('StringsSocket', "BW") + + 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 + bw = [[]] + red = [[]] + alpha = [[]] + green = [[]] + blue = [[]] + + 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 + 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]) + 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) + + +def register(): + bpy.utils.register_class(EvaluateImageNode) + + +def unregister(): + bpy.utils.unregister_class(EvaluateImageNode) diff --git a/nodes/analyzer/kd_tree_edges_mk2.py b/nodes/analyzer/kd_tree_edges_mk2.py index e635bb24e0c3151983d33760658a7db9f7d151c5..2c2a37ebd32dc8662f9875b534ee284ab53d6fd2 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 diff --git a/nodes/analyzer/path_length.py b/nodes/analyzer/path_length.py new file mode 100644 index 0000000000000000000000000000000000000000..203e35bc41c4bb288a2aa0d268dd864827b14f56 --- /dev/null +++ b/nodes/analyzer/path_length.py @@ -0,0 +1,131 @@ +# ##### 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, get_edge_list +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) + 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 SvPathLengthNode(bpy.types.Node, SverchCustomTreeNode): + ''' + Triggers: Path / Edges length + Tooltip: Measures the length of a path or the length of its segments + ''' + bl_idname = 'SvPathLengthNode' + bl_label = 'Path Length' + sv_icon = 'SV_PATH_LENGTH' + + output_numpy = BoolProperty( + name='Output NumPy', description='output NumPy arrays', + default=False, update=updateNode) + + 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, "segment", 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', "Vertices") + sinw('StringsSocket', "Edges") + + sonw('StringsSocket', "Length") + + def get_data(self): + '''get all data from sockets''' + si = self.inputs + + 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['Length'].is_linked): + return + + result = [] + gates = [] + gates.append(self.output_numpy) + gates.append(self.segment) + 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(SvPathLengthNode) + + +def unregister(): + '''unregister class in Blender''' + bpy.utils.unregister_class(SvPathLengthNode) diff --git a/nodes/generator/cylinder_mk2.py b/nodes/generator/cylinder_mk2.py new file mode 100644 index 0000000000000000000000000000000000000000..9a371303438aa077cdd50ab55fb5cba0e0d470c4 --- /dev/null +++ b/nodes/generator/cylinder_mk2.py @@ -0,0 +1,381 @@ +# ##### 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, get_edge_loop) +from sverchok.utils.geom import CubicSpline + +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) + 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=cyclic) + out = spline.eval(samples_array) + verts = out.tolist() + + resampled_profile = [v[1] for v in verts] + + return resampled_profile + + +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 (= number of points in a meridian) + nm : number of meridians (= number of points in a parallel) + h : height + 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 + + rt = rt * s + rb = rb * s + h = h * s + + 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 = [] + 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 # 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 # parallel's total phase (phase + delta twist) + + verts = [] + 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]) + + add_verts(verts) + + return vert_list + + +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 = [] + + 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 make_polys(P, M, cap_bottom, cap_top, separate): + """ + 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 + separate: split the parallels into separate poly lists + """ + poly_list = [] + + 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 = [j for j in reversed(range(M))] + add_poly(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): + """ + Triggers: Cylinder, Tube + Tooltip: Generate cylinder based meshes + """ + bl_idname = 'SvCylinderNodeMK2' + 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) + + 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=update_cylinder) + + phase = FloatProperty( + name='Phase', description="The phase of the cylinder", + default=0.0, update=update_cylinder) + + 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) + + 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' + 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) + 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) + + 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=[[]])[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)) + + 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] + + 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 + + verts_list = [] + edges_list = [] + 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 * au, ph * au, 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 verts_output_linked: + self.outputs['Vertices'].sv_set(verts_list) + + if edges_output_linked: + self.outputs['Edges'].sv_set(edges_list) + + if polys_output_linked: + self.outputs['Polygons'].sv_set(polys_list) + + +def register(): + bpy.utils.register_class(SvCylinderNodeMK2) + + +def unregister(): + bpy.utils.unregister_class(SvCylinderNodeMK2) diff --git a/nodes/generator/image.py b/nodes/generator/image.py index fd1399f9109935773cfb3233b415a69c2e518804..ffb789caed77efe571754301cf12694c658cc3c1 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/generator/line_mk3.py b/nodes/generator/line_mk3.py new file mode 100644 index 0000000000000000000000000000000000000000..cfaefc875b3dcc5b3b6a0722a4878e02a0045e29 --- /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/generators_extended/smooth_lines.py b/nodes/generators_extended/smooth_lines.py index 835ad1af2a983357f97d82be68a29ba4be688d08..61b1084011aa2257d6d2f95d0ee06f36f14452fe 100644 --- a/nodes/generators_extended/smooth_lines.py +++ b/nodes/generators_extended/smooth_lines.py @@ -37,9 +37,18 @@ 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 - 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 +102,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)] diff --git a/nodes/generators_extended/super_ellipsoid.py b/nodes/generators_extended/super_ellipsoid.py new file mode 100644 index 0000000000000000000000000000000000000000..31324fff65f5573ccb85a62faa5f7a625273403c --- /dev/null +++ b/nodes/generators_extended/super_ellipsoid.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 ##### + +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, 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, 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], +} + + +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 (= number of points in a meridian) + nm : number of meridians (= number of points in a parallel) + """ + verts = [] + for p in range(np): + a = (pi / 2 - epsilon) * (2 * p / (np - 1) - 1) + 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) + 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 + + +def make_edges(P, M): + """ + 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 = [] + + # 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) + 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 + + +def make_polys(P, M, cap_bottom, cap_top): + """ + Generate the super-ellipsoid 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 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 + + +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, description="Various presets", + 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_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): + 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' + 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_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): + 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_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): + 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_bottom, self.cap_top) + polys_list.append(polys) + + # outputs + if verts_output_linked: + self.outputs['Vertices'].sv_set(verts_list) + + if edges_output_linked: + self.outputs['Edges'].sv_set(edges_list) + + if polys_output_linked: + self.outputs['Polygons'].sv_set(polys_list) + + +def register(): + bpy.utils.register_class(SvSuperEllipsoidNode) + + +def unregister(): + bpy.utils.unregister_class(SvSuperEllipsoidNode) diff --git a/nodes/list_main/statistics.py b/nodes/list_main/statistics.py new file mode 100644 index 0000000000000000000000000000000000000000..86b033d17b565734513f8bbdf03282310f93b6f0 --- /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/nodes/list_mutators/combinatorics.py b/nodes/list_mutators/combinatorics.py new file mode 100644 index 0000000000000000000000000000000000000000..af4d2184564a08071cb5f0f19d3f5bed172ee078 --- /dev/null +++ b/nodes/list_mutators/combinatorics.py @@ -0,0 +1,167 @@ +# ##### 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 (match_long_repeat, updateNode) + +from itertools import (product, permutations, combinations) + +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)) +} + +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"} + + +class SvCombinatoricsNode(bpy.types.Node, SverchCustomTreeNode): + """ + Triggers: Product, Permutations, Combinations + 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 list inputs this many times', + default=1, min=1, update=updateNode) + + length = IntProperty( + 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', "A") + self.inputs.new('StringsSocket', "B") + + self.outputs.new('StringsSocket', "Result") + + self.update_operation(context) + + def update(self): + ''' Add/remove sockets as A-Z sockets are connected/disconnected ''' + + # not a multiple input 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 selected operation ''' + + 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: + for a in ABC[1:]: # remove all B-Z inputs (keep A) + if a in inputs: + inputs.remove(inputs[a]) + + # 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"}: + inputs["Repeat"].hide_safe = True + if inputs["Length"].hide: + 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 + + 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)) + + # collect the data inputs from all connected AZ sockets + 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]) + else: # PERMUTATIONS / COMBINATIONS + L = inputs["Length"].sv_get()[0] + L = list(map(lambda x: max(0, int(x)), L)) + parameters = match_long_repeat([I, L]) + + 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) + + +def register(): + bpy.utils.register_class(SvCombinatoricsNode) + + +def unregister(): + bpy.utils.unregister_class(SvCombinatoricsNode) diff --git a/nodes/list_mutators/polygon_sort.py b/nodes/list_mutators/polygon_sort.py new file mode 100644 index 0000000000000000000000000000000000000000..b60270d996c072cbfc15c24f300136d4371c280d --- /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) diff --git a/nodes/modifier_change/input_switch.py b/nodes/modifier_change/input_switch.py new file mode 100644 index 0000000000000000000000000000000000000000..39726d9c75326c4714652bac9d1a9c1cc5fe1df4 --- /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/nodes/modifier_change/iterate.py b/nodes/modifier_change/iterate.py index e9cd453f840f66b90133a0b738ed557abdc33536..60ff88b7a7a8eab8c86de4b05f3423893b324586 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(): diff --git a/nodes/modifier_change/mesh_switch.py b/nodes/modifier_change/mesh_switch.py new file mode 100644 index 0000000000000000000000000000000000000000..4c91d8e740ccfe6cbe6fb43468afcb3494b4c5f7 --- /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 + + # 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 + + 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) diff --git a/nodes/number/easing.py b/nodes/number/easing.py index d1c292f925c13d3ae12b7627eac8da1c90bc39a0..5c2392da3456ee55e3013b0af149546a6501c2ca 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/number/formula2.py b/nodes/number/formula2.py index 443679121ad0e4f0e07a467f743793d15d9dcbd1..57be5ff8e20edf54c9414e442ec556d95e885db7 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 new file mode 100644 index 0000000000000000000000000000000000000000..495f45301bff5329a953a2a2987a3514790571b0 --- /dev/null +++ b/nodes/number/formula3.py @@ -0,0 +1,342 @@ +# ##### 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 +from sverchok.utils import logging + +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, 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 least, 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 stack_frame in self.local_vars: + if name in stack_frame: + 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): + """ + Get set of free variables used by formula + """ + string = string.strip() + if not len(string): + return set() + root = ast.parse(string, mode='eval') + visitor = VariableCollector() + visitor.visit(root) + result = visitor.variables + return result.difference(safe_names.keys()) + +# 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) + 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): + """ + 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) + + 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, "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") + + self.outputs.new('StringsSocket', "Result") + + def get_variables(self): + variables = set() + + for formula in self.formulas(): + vs = get_variables(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. + ''' + + if not any(len(formula) for formula in self.formulas()): + 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 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: + 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)) + 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(): + bpy.utils.register_class(SvFormulaNodeMk3) + + +def unregister(): + bpy.utils.unregister_class(SvFormulaNodeMk3) + diff --git a/nodes/number/mix_inputs.py b/nodes/number/mix_inputs.py new file mode 100644 index 0000000000000000000000000000000000000000..a973af3fb4863dc54b410a3e559c45de594196dd --- /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/quaternion/quaternion_in.py b/nodes/quaternion/quaternion_in.py new file mode 100644 index 0000000000000000000000000000000000000000..df8f47d1f8fe42470bb1916136d2e057d23ffd35 --- /dev/null +++ b/nodes/quaternion/quaternion_in.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 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), + ("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 = [ + ('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"], + "SCALARVECTOR": ["Scalar", "Vector"], + "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='The input component format of the quaternion', + 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) + + 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) + + 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', "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' + 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 in {"WXYZ", "SCALARVECTOR"}: + 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 == "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) + 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 0000000000000000000000000000000000000000..fbcfdfc6eff300a15f60123e933ac7461585a059 --- /dev/null +++ b/nodes/quaternion/quaternion_math.py @@ -0,0 +1,284 @@ +# ##### 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) + "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])] + +# 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 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 + 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 == "QUADRANCE": + return lambda q: q.dot(q) + 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: + parameters = match_long_repeat(I) + quaternionList = [operation(params) for params in zip(*parameters)] + + elif self.operation in QQ_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 0000000000000000000000000000000000000000..852835d4ec71fc2f5d5cfaea37002cc5468e8b2e --- /dev/null +++ b/nodes/quaternion/quaternion_out.py @@ -0,0 +1,193 @@ +# ##### 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), + ("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 = [ + ('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"], + "SCALARVECTOR": ["Scalar", "Vector"], + "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='The output component format of the quaternion', + 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") + # 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") + 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 in {"WXYZ", "SCALARVECTOR"}: + 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": + 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"): + 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) diff --git a/nodes/scene/frame_info_mk2.py b/nodes/scene/frame_info_mk2.py index e6192527d9447c7b9659c36028d4efc4dad98a29..ec3dee4db78991ec05b0702a3344326669cf4cd6 100644 --- a/nodes/scene/frame_info_mk2.py +++ b/nodes/scene/frame_info_mk2.py @@ -61,6 +61,14 @@ class SvFrameInfoNodeMK2(bpy.types.Node, SverchCustomTreeNode): row.operator("screen.frame_jump", text="", icon='FF').end = True row.prop(scene, "frame_current", text="") + @staticmethod + def additional_properties(): + # properties that does not related with the node + props = {'frame_currnet': bpy.context.scene.frame_current, + 'frame_end': bpy.context.scene.frame_end, + 'frame_start': bpy.context.scene.frame_start} + return props + def process(self): scene = bpy.context.scene outputs = self.outputs diff --git a/nodes/text/export_gcode.py b/nodes/text/export_gcode.py new file mode 100644 index 0000000000000000000000000000000000000000..eaaacb5869fd7b9d7dffe98961ae554f1429ed84 --- /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/nodes/text/stethoscope_mk2.py b/nodes/text/stethoscope_mk2.py index 6fa5d89a6e511f0b36862557034edca154ffb700..31744a2036daf9c38b7038828ad3c802613cb2fb 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 e267325e1e615478559fa8dd7f436ac6b94b4659..e0854e852293f3d8ba96a536f3be4067bab73252 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/nodes/viz/texture_viewer_lite.py b/nodes/viz/texture_viewer_lite.py index 9363d37719b5b56f1c2680191534be6aedd1cde2..ddb88be9b8d52846d6f3e4b752156fb0f85b81da 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) diff --git a/nodes/viz/viewer_polyline_mk1.py b/nodes/viz/viewer_polyline_mk1.py index ee6f4f164ff435db0d79ee3d35eacaca81166766..a1d940c1bfca2d95e707dca24ee3fbf7cb4db16d 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 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 319ee544901081c68a45b94e03ba071a40bb0756..fe9060b938b46f8a3773cfde8c017af9e0a274fb 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/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/settings.py b/settings.py index a1a0460fa0209c36434f338ae885a31b5ee697ba..68ab33c948aad7cb9f1ca154de01211bdeb1037e 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", @@ -163,10 +163,17 @@ 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') 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') @@ -178,7 +185,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), @@ -213,6 +220,10 @@ class SverchokPreferences(AddonPreferences): dload_archive_name = StringProperty(name="archive name", default="master") dload_archive_path = StringProperty(name="archive path", default="https://github.com/nortikin/sverchok/archive/") + # parameter for switching between old and new update system + old_update_system = BoolProperty(name="Update node tree each time", + description="Old update system", + default=True) def draw(self, context): @@ -223,13 +234,18 @@ class SverchokPreferences(AddonPreferences): col = layout.row().column() col_split = col.split(0.5) + col1 = col_split.column() - col1.label(text="UI:") - col1.prop(self, "show_icons") - col1.prop(self, "over_sized_buttons") - col1.prop(self, "enable_live_objin", text='Enable Live Object-In') - col1.prop(self, "external_editor", text="Ext Editor") - col1.prop(self, "real_sverchok_path", text="Src Directory") + col1.prop(self, 'old_update_system') + col1.separator() + + col1box = col1.box() + col1box.label(text="UI:") + col1box.prop(self, "show_icons") + col1box.prop(self, "over_sized_buttons") + col1box.prop(self, "enable_live_objin", text='Enable Live Object-In') + col1box.prop(self, "external_editor", text="Ext Editor") + col1box.prop(self, "real_sverchok_path", text="Src Directory") col2 = col_split.split().column() col2.label(text="Frame change handler:") @@ -267,9 +283,13 @@ 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.prop(self, 'index_viewer_scale', text='scale') col3 = row_sub1.split().column() col3.label('Location of custom defaults') diff --git a/ui/icons/sv_input_switch.png b/ui/icons/sv_input_switch.png new file mode 100644 index 0000000000000000000000000000000000000000..7ae7d6d1469555515611230fe892f2caad42d2b9 Binary files /dev/null and b/ui/icons/sv_input_switch.png differ diff --git a/ui/icons/sv_mix_inputs.png b/ui/icons/sv_mix_inputs.png new file mode 100644 index 0000000000000000000000000000000000000000..790b24088d842adb38620ec9b802a006c73baa31 Binary files /dev/null and b/ui/icons/sv_mix_inputs.png differ diff --git a/ui/icons/sv_path_length.png b/ui/icons/sv_path_length.png new file mode 100644 index 0000000000000000000000000000000000000000..66c329ef457280de62f094b0a9188a0994ba6977 Binary files /dev/null and b/ui/icons/sv_path_length.png differ diff --git a/ui/index_viewer_draw.py b/ui/index_viewer_draw.py index 44f9a3655b0f6437256f24b1313d75cf39af34ff..49b81b7638a32414767f42049f4bc272e68136a4 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) diff --git a/ui/nodeview_rclick_menu.py b/ui/nodeview_rclick_menu.py index 6ca2cb026b69c699ad0e3ec02c744725d7bfcb6d..682c28fc0157e3ff024a3f0d94e7e2a06c162b28 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': diff --git a/ui/sv_panels.py b/ui/sv_panels.py index d09547585dae9acf106c9bf8ed6e070cf7981014..42000f8f6dbb90d70d7bd15783077b0a984bb9e4 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 ] diff --git a/utils/modules/geom_utils.py b/utils/modules/geom_utils.py index 2529f95bea3224a317824aedf37b3acba3251cf4..1ad0870d450b91b22a1eb69a7ef84835c9ef302a 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 diff --git a/utils/modules/statistics_functions.py b/utils/modules/statistics_functions.py new file mode 100644 index 0000000000000000000000000000000000000000..193c5b61cbe77cbb349afa2ce23f5955ebae154f --- /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 diff --git a/utils/sv_default_macros.py b/utils/sv_default_macros.py index f029230b215e3421a6ecbe4d9f861213df5cad3d..4cbe626825b914973dc536b750784cf200cbcc1a 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,18 +145,60 @@ 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)], - ['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 = [] x, y = context.space_data.cursor_location[:] for node_bl_idname, node_location in needed_nodes: @@ -149,9 +206,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 +216,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 diff --git a/utils/sv_itertools.py b/utils/sv_itertools.py index b47669bb906391d900acdfc68ac1bc94d3ca462f..781c6a39cfe01276c181459973ebf1c1681df084 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: diff --git a/utils/sv_panels_tools.py b/utils/sv_panels_tools.py index f4e9767857fcb39290db0a5168a732528eef29f7..c007a5f77956ecbe552689b962490a2aefddb6a7 100644 --- a/utils/sv_panels_tools.py +++ b/utils/sv_panels_tools.py @@ -22,7 +22,7 @@ import bpy from bpy.props import StringProperty, CollectionProperty, BoolProperty from sverchok.core.update_system import process_tree, build_update_list -from sverchok.node_tree import SverchCustomTreeNode +from sverchok.core.update_system_mem import tree_updater from sverchok.utils.logging import debug, info import sverchok @@ -34,11 +34,15 @@ class SverchokUpdateAll(bpy.types.Operator): bl_options = {'REGISTER', 'UNDO'} def execute(self, context): - sv_ngs = filter(lambda ng:ng.bl_idname == 'SverchCustomTreeType', bpy.data.node_groups) - for ng in sv_ngs: - ng.unfreeze(hard=True) - build_update_list() - process_tree() + addon = bpy.context.user_preferences.addons.get(sverchok.__name__) + if addon.preferences.old_update_system: + sv_ngs = filter(lambda ng:ng.bl_idname == 'SverchCustomTreeType', bpy.data.node_groups) + for ng in sv_ngs: + ng.unfreeze(hard=True) + build_update_list() + process_tree() + else: + tree_updater.update() return {'FINISHED'} @@ -77,10 +81,13 @@ class SverchokUpdateCurrent(bpy.types.Operator): def execute(self, context): ng = bpy.data.node_groups.get(self.node_group) - if ng: + addon = context.user_preferences.addons.get(sverchok.__name__) + if addon.preferences.old_update_system and ng: ng.unfreeze(hard=True) build_update_list(ng) process_tree(ng) + else: + tree_updater.update(ng) return {'FINISHED'}