diff --git a/__init__.py b/__init__.py index 04554282dd..7e8abf0fba 100755 --- a/__init__.py +++ b/__init__.py @@ -69,7 +69,7 @@ from sverchok.core import reload_event, handle_reload_event from sverchok.utils import utils_modules from sverchok.ui import ui_modules -from sverchok.ui.nodeview_add_menu import perform_menu_monkey_patch + from sverchok.utils.profile import profiling_startup imported_modules = init_architecture(__name__, utils_modules, ui_modules) @@ -92,7 +92,7 @@ def register(): if reload_event: data_structure.RELOAD_EVENT = True menu.reload_menu() - # perform_menu_monkey_patch() <-- this hijacks other custom node trees + def unregister(): sverchok.utils.clear_node_classes() diff --git a/core/sockets.py b/core/sockets.py index 4c23aacd5b..42541633e3 100644 --- a/core/sockets.py +++ b/core/sockets.py @@ -666,6 +666,7 @@ class SvMatrixSocket(NodeSocket, SvSocketCommon): color = (0.2, 0.8, 0.8, 1.0) quick_link_to_node = 'SvMatrixInNodeMK4' + nesting_level: IntProperty(default=1) def do_flatten(self, data): return flatten_data(data, 1, data_types=(Matrix,)) @@ -814,7 +815,7 @@ class SvColorSocket(NodeSocket, SvSocketCommon): default_property: FloatVectorProperty(default=(0, 0, 0, 1), size=4, subtype='COLOR', min=0, max=1, update=process_from_socket) expanded: BoolProperty(default=False) # for minimizing showing socket property - + nesting_level: IntProperty(default=3) def draw_property(self, layout, prop_origin=None, prop_name='default_property'): if prop_origin is None: prop_origin = self @@ -837,6 +838,9 @@ def draw_group_property(self, layout, text, interface_socket): layout.prop(self, 'default_property', text=text) else: layout.label(text=text) + + def do_flat_topology(self, data): + return flatten_data(data, 3) class SvDummySocket(NodeSocket, SvSocketCommon): '''Dummy Socket for sockets awaiting assignment of type''' @@ -1232,6 +1236,10 @@ class SvLinkNewNodeInput(bpy.types.Operator): bl_idname = "node.sv_quicklink_new_node_input" bl_label = "Add a new node to the left" + @classmethod + def poll(cls, context): + return hasattr(context, 'socket') + def execute(self, context): tree, node, socket = context.node.id_data, context.node, context.socket diff --git a/docs/nodes/analyzer/analyzer_index.rst b/docs/nodes/analyzer/analyzer_index.rst index d11be2fe19..c2f2e0e683 100644 --- a/docs/nodes/analyzer/analyzer_index.rst +++ b/docs/nodes/analyzer/analyzer_index.rst @@ -23,10 +23,11 @@ Analyzers kd_tree_path linked_verts mesh_filter - mesh_select + mesh_select_mk2 select_similar proportional normals + nearest_point_on_mesh bvh_overlap_polys object_insolation path_length_2 diff --git a/docs/nodes/analyzer/mesh_select.rst b/docs/nodes/analyzer/mesh_select_mk2.rst similarity index 100% rename from docs/nodes/analyzer/mesh_select.rst rename to docs/nodes/analyzer/mesh_select_mk2.rst diff --git a/docs/nodes/analyzer/nearest_point_on_mesh.rst b/docs/nodes/analyzer/nearest_point_on_mesh.rst new file mode 100644 index 0000000000..d076b69f4c --- /dev/null +++ b/docs/nodes/analyzer/nearest_point_on_mesh.rst @@ -0,0 +1,53 @@ +Nearest Point on Mesh +===================== + +Functionality +------------- + +Finds the closest point in a specified mesh. + +Inputs +------ + +Vertices, Faces: Base mesh for the search +Points: Points to query +Distance: Maximum query distance (only for Nearest in Range mode) + +Parameters +---------- + +*Mode*: + - Nearest: Nearest point on the mesh surface + - Nearest in range: Nearest points on the mesh within a range (one per face) + +*Flat Output*: (only in Nearest in Range) Flattens the list of every vertex to have only a list for every inputted list. + +*Safe Check*: (in N-Panel) When disabled polygon indices referring to unexisting points will crash Blender. Not performing this check makes node faster + +Outputs +------- + +*Location*: Position of the closest point in the mesh + +*Normal*: mesh normal at closets point + +*Index*: Face index of the closest point + +*Distance*: Distance from the queried point to the closest point + +Examples +-------- + +Used as skin-wrap modifier: + +.. image:: https://user-images.githubusercontent.com/10011941/111774583-f3733480-88af-11eb-9559-78392166b00c.png + + +Determine which polygons are nearer than a distance: + +.. image:: https://user-images.githubusercontent.com/10011941/111812010-f255fd80-88d7-11eb-8f48-de67716dd93a.png + + +Placing objects on mesh: + +.. image:: https://user-images.githubusercontent.com/10011941/111810852-bf5f3a00-88d6-11eb-9cff-eb2a6c18a01a.png diff --git a/docs/nodes/list_main/statistics.rst b/docs/nodes/list_main/statistics.rst index c63a29f195..236fd5fd15 100644 --- a/docs/nodes/list_main/statistics.rst +++ b/docs/nodes/list_main/statistics.rst @@ -4,7 +4,7 @@ List Statistics Functionality ------------- -List Statistics node computes various statistical quantities for the values in a list. +List Statistics node computes various statistical quantities for the input values. Inputs ------ @@ -17,42 +17,55 @@ 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] | -+----------------+---------------------+---------+------------------------------------------+ ++----------------+----------------------+---------+-------------------------------------------+ +| Param | Type | Default | Description | ++================+======================+=========+===========================================+ +| **Function** | Enum | Average | The statistical function applied to | +| | All Statistics | | the input values. | +| | Selected Statistics | | | +| | | | | +| | Sum | | | +| | Sum Of Squares | | | +| | Sum Of Inverse | | 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. | +| | Variance | | | +| | Standard Deviation | | | +| | Standard Error | | | +| | Root Mean Square | | For "Selected Statistics" selection the | +| | Skewness | | node computes and outputs the statistical | +| | Kurtosis | | quantities for the selected statistical | +| | Minimum | | functions along with their corresponding | +| | Maximum | | names. | +| | Range | | | +| | Median | | | +| | Percentile | | | +| | Histogram | | | +| | Count | | | ++----------------+----------------------+---------+-------------------------------------------+ +| **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. +Extra Parameters +---------------- +The Property Panel contains additional settings to configure the statistics drawn in the node editor: font, text color, text scale and floating point presicion of the displayed statistics, the x/y offsest of the displayed statistics relative to the node and a setting for toggling the statistics names between full names and abreviations. + +The fonts used for this node are monospace fonts for best text alignment. + Outputs ------- **Name(s)** diff --git a/docs/nodes/matrix/matrix_index.rst b/docs/nodes/matrix/matrix_index.rst index 879b209567..a60b1f8f1f 100644 --- a/docs/nodes/matrix/matrix_index.rst +++ b/docs/nodes/matrix/matrix_index.rst @@ -11,7 +11,7 @@ Matrix input interpolation matrix_in_mk4 - matrix_out + matrix_out_mk2 matrix_math matrix_track_to shear diff --git a/docs/nodes/matrix/matrix_out.rst b/docs/nodes/matrix/matrix_out.rst deleted file mode 100644 index 189d4df369..0000000000 --- a/docs/nodes/matrix/matrix_out.rst +++ /dev/null @@ -1,2 +0,0 @@ -Matrix In & Out -=============== \ No newline at end of file diff --git a/docs/nodes/matrix/matrix_out_mk2.rst b/docs/nodes/matrix/matrix_out_mk2.rst new file mode 100644 index 0000000000..466490df63 --- /dev/null +++ b/docs/nodes/matrix/matrix_out_mk2.rst @@ -0,0 +1,79 @@ +Matrix Out +========== + +Functionality +------------- + +Matrix Out node converts a 4x4 matrix into its location, rotation and scale components. The rotation component can be represented in various formats: quaternion, axis-angle or Euler angles. + + +Modes +----- + +The available **Modes** are: EULER, AXIS-ANGLE & QUATERNION. These specify +how the output *rotation component* of the matrix is going to be represented. + +Regardless of the selected mode the node always outputs the **Location** and the **Scale** components of the 4x4 matrix. + ++------------+---------------------------------------------------------------------------------------+ +| Mode | Description | ++============+=======================================================================================+ +| EULER | Converts the rotation component of the matrix into X, Y, Z angles | +| | corresponding to the Euler rotation given an Euler rotation order. [1,2] | ++------------+---------------------------------------------------------------------------------------+ +| AXIS-ANGLE | Converts the rotation component of the matrix into the Axis & Angle of rotation. [1] | ++------------+---------------------------------------------------------------------------------------+ +| QUATERNION | Converts the rotation component of the matrix into a quaternion. | ++------------+---------------------------------------------------------------------------------------+ + +Notes: +[1] : 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). +[2] : For EULER mode the node provides the option to select the Euler rotation order: "XYZ", "XZY", "YXZ", "YZX", "ZXY" or "ZYX". + + +Inputs +------ + +**Matrix** +The node takes a list of (one or more) matrices and based on the selected mode +it converts the matrices into the corresponding components. + + +Extra Parameters +---------------- +A set of extra parameters are available on the property panel. +These parameters do not receive external input. + ++-----------------+----------+---------+--------------------------------------+ +| Extra Param | Type | Default | Description | ++=================+==========+=========+======================================+ +| **Angle Units** | Enum | DEGREES | Interprets the angle values based on | +| | RADIANS | | the selected angle units: | +| | DEGREES | | Radians = 0 - 2pi | +| | UNITIES | | Degrees = 0 - 360 | +| | | | Unities = 0 - 1 | ++-----------------+----------+---------+--------------------------------------+ + + +Outputs +======= + +Based on the selected **Mode** the node makes available the corresponding output sockets: + ++------------+-----------------------------------------+ +| Mode | Output Sockets (types) | ++============+=========================================+ +| | Location and Scale components (vectors) | ++------------+-----------------------------------------+ +| EULER | X, Y, Z angles (floats) [1] | ++------------+-----------------------------------------+ +| AXIS-ANGLE | Axis (vector) & Angle (float) [1] | ++------------+-----------------------------------------+ +| QUATERNION | Quaternion | ++------------+-----------------------------------------+ + +Notes: +[1] : The angles are by default in DEGREES. The Property Panel has option to set angle units as: RADIANS, DEGREES or UNITIES. + +The node only generates the conversion when the output sockets are connected. + diff --git a/docs/nodes/number/curve_mapper.rst b/docs/nodes/number/curve_mapper.rst index 75ae9189b8..ee239b86fa 100644 --- a/docs/nodes/number/curve_mapper.rst +++ b/docs/nodes/number/curve_mapper.rst @@ -30,6 +30,8 @@ This node has the following outputs: always lying in XOY plane along the OX axis. The domain of the curve is defined by **Min X** and **Max X** parameters, which are defined in the curve editor widget. +* **Control Points: Location over the XOY Plane of the control points of the widget. + It can be used as a 2D slider. Examples -------- @@ -40,9 +42,8 @@ Basic range remapping: Using the node to define the column profile: -.. image:: https://raw.githubusercontent.com/vicdoval/sverchok/docs_images/images_for_docs/number/Curve%20Mapper/curve_mapper_sverchok__blender_example_2.png +.. image:: https://raw.githubusercontent.com/vicdoval/sverchok/docs_images/images_for_docs/number/Curve%20Mapper/curve_mapper_sverchok__blender_example_2.png Example of the Curve output usage: .. image:: https://user-images.githubusercontent.com/284644/80520701-4051d200-89a3-11ea-92fd-2f2f2004e4e7.png - diff --git a/docs/nodes/number/easing.rst b/docs/nodes/number/easing.rst index 9aa52718a3..e8bd747c62 100644 --- a/docs/nodes/number/easing.rst +++ b/docs/nodes/number/easing.rst @@ -12,8 +12,11 @@ https://zeffii.github.io/docs_easing_node/ the original development thread is here: https://github.com/nortikin/sverchok/issues/695 -You can see kind of curve, because we draw it to the right of the node. -add the node to the tree, and scroll through the list of options. +You can see the kind of easing curve because we draw it to the right of the node. +Add the node to the tree and connect something useful to the input, and scroll through the list of easing curve types. + +|visual_viewport| -.. |visual_easing| image:: https://user-images.githubusercontent.com/619340/82451459-51779580-9aae-11ea-9dce-9a4dc1236014.png +.. |visual_easing| image:: https://user-images.githubusercontent.com/619340/82451459-51779580-9aae-11ea-9dce-9a4dc1236014.png +.. |visual_viewport| image:: https://user-images.githubusercontent.com/619340/111627552-76808600-87ef-11eb-9929-f9295d766623.png diff --git a/docs/nodes/object_nodes/getsetprop_mk2.rst b/docs/nodes/object_nodes/getsetprop_mk2.rst index f97a5b1322..25ee403769 100644 --- a/docs/nodes/object_nodes/getsetprop_mk2.rst +++ b/docs/nodes/object_nodes/getsetprop_mk2.rst @@ -22,19 +22,33 @@ There are also convenience aliases. Instead of writing ``bpy.data.objects['Cube' "mats": "bpy.data.materials", "M": "bpy.data.materials", "meshes": "bpy.data.meshes", - "texts": "bpy.data.texts" + "texts": "bpy.data.texts", + "nodes": node_tree.nodes, + "ng": "bpy.data.node_groups" } useful info for path lookups: Many properties can be right-clicked and have the option to "copy data path" (to your clipboard for pasting), this can help reduce some console probing / documentation reading. -Usually, however, you will need to provide the start of the path yourself. For example: if you copy the path to one of the Color properties in a ColorRamp of a shader, then following will be be copied to the clipboard: +Usually, however, you will need to provide the start of the path yourself. You must provide an explicit path to a distinct node tree. For example: if you copy the path to one of the Color properties in a ColorRamp of a shader, then following will be be copied to the clipboard: ``node_tree.nodes["ColorRamp"].color_ramp.elements[0].color`` , this is not the full path, you will need to add the path to the ``node_tree``, something like: ``bpy.data.materials['Material'].node_tree.nodes["ColorRamp"].color_ramp.elements[0].color``, then the node will know what your intention is. +Alias ``nodes`` +--------------- + +The alias ``nodes`` allows you to reference nodes in the current node tree (ie, a sverchok node tree) by writing:: + + nodes["your node"].inputs[0].default_int_property + +Alias ``ng`` +------------ + +If you are referencing a different sverchok nodetree, you write ``ng["NodeTreeName"].nodes["NodeName]..etc`` + Input ----- diff --git a/docs/nodes/object_nodes/object_nodes_index.rst b/docs/nodes/object_nodes/object_nodes_index.rst index fd040ac0e3..452dd416e7 100644 --- a/docs/nodes/object_nodes/object_nodes_index.rst +++ b/docs/nodes/object_nodes/object_nodes_index.rst @@ -16,3 +16,4 @@ Objects assign_materials material_index set_custom_uv_map + set_loop_normals diff --git a/docs/nodes/object_nodes/set_loop_normals.rst b/docs/nodes/object_nodes/set_loop_normals.rst new file mode 100644 index 0000000000..6bc372b7d3 --- /dev/null +++ b/docs/nodes/object_nodes/set_loop_normals.rst @@ -0,0 +1,47 @@ +Set loop normals +================ + +.. image:: https://user-images.githubusercontent.com/28003269/111056673-62a8ed00-849a-11eb-86b8-faa4bb111f16.png + +Functionality +------------- +The nodes allowed to set custom normals to corners of an input mesh. +Most of the time it should be used together with `Origins` nodes which can calculate vertices normals. + +Category +-------- + +BPY Data -> Set loop normals + +Inputs +------ + +- **Objects** - Blender mesh objects +- **Vert normals** - normals for each input vertices +- **Faces** - indexes pointing to vertex normals +- **Matrix** - optional, fro UV transformation + +Outputs +------- + +- **Objects** - Blender mesh objects + +Parameters +---------- + +- **Normalized** - It will normalize input normals. It's convenient because if normals are not normalized the result can looks weird. + +Usage +----- + +Smooth normals + +.. image:: https://user-images.githubusercontent.com/28003269/111041776-65342400-8453-11eb-98a7-95b1fcb7eb8e.png + +Flat normals + +.. image:: https://user-images.githubusercontent.com/28003269/111041779-66fde780-8453-11eb-8de0-92ac014266b9.png + +Custom normals based on edges angle + +.. image:: https://user-images.githubusercontent.com/28003269/111056842-c1229b00-849b-11eb-9257-1080cd43f3b7.png \ No newline at end of file diff --git a/docs/nodes/solid/chamfer_solid.rst b/docs/nodes/solid/chamfer_solid.rst index 977d245649..22ae3c133b 100644 --- a/docs/nodes/solid/chamfer_solid.rst +++ b/docs/nodes/solid/chamfer_solid.rst @@ -33,4 +33,4 @@ Notes - The Distance A and Distance B depends on the orientation of the edge. -- If Distance A or Distance B are to big the node will became in Error state and will not perform the operation. +- If Distance A or Distance B are too big the node will became in Error state and will not perform the operation. diff --git a/docs/nodes/solid/general_fuse.rst b/docs/nodes/solid/general_fuse.rst index 42cf7e71d2..70312d13d6 100644 --- a/docs/nodes/solid/general_fuse.rst +++ b/docs/nodes/solid/general_fuse.rst @@ -20,8 +20,8 @@ parts, one application of "Solid General Fuse" will give you better performance compared to many "Solid Boolean" applications. To illustrate what exactly this node does, it's simpler to draw some 2D -pictures first. Let's say we have a circle (object number 0), a square (object -number 1) and a triangle (object number 2): +pictures first. Let's say we have a circle (object number 0), a triangle (object +number 1) and a square (object number 2): .. image:: https://user-images.githubusercontent.com/284644/94195404-65079280-fecc-11ea-8ec7-73b8b357c063.png (Figure 1) @@ -47,8 +47,8 @@ and 1, i.e. we remove the intersection of the circle and the triangle: .. image:: https://user-images.githubusercontent.com/284644/94196223-7f8e3b80-fecd-11ea-975e-b20b193f4e62.png (Figure 3) -Or, let's leave only parts, for which the set of source objects is ``[0,1]``, -``[1,2]``, or ``[0,1,2]``: +Or, let's leave only parts, for which the set of source objects is ``[0,2]``, +``[0,1,2]``, and ``[1,2]``: .. image:: https://user-images.githubusercontent.com/284644/94196342-a482ae80-fecd-11ea-91ef-6d564e325491.png (Figure 4) diff --git a/docs/nodes/spatial/random_points_on_mesh.rst b/docs/nodes/spatial/random_points_on_mesh.rst index 54757616db..bc1f125136 100644 --- a/docs/nodes/spatial/random_points_on_mesh.rst +++ b/docs/nodes/spatial/random_points_on_mesh.rst @@ -31,6 +31,7 @@ This node has the following parameters: * **Surface**. Generate points on the surface of the mesh. * **Volume**. Generate points inside the volume of the mesh. The mesh is expected to represent a closed volume in this case. + * **Edges**. Generate points on the edges of the mesh. The default option is **Surface**. @@ -40,12 +41,28 @@ This node has the following parameters: **Face weight** input). If not checked, then the number of points on each face will be only defined by **Face weight** input. Checked by default. +- **All Triangles**. Enable if the input mesh is made only of triangles + (makes node faster). Available in Surfaces and Volume modes (in N-Panel) + +- **Safe Check**. Disabling it will make node faster but polygon indices + referring to unexisting points will crash Blender. Only available in Volume Mode. + (in N-Panel) + +- **Implementation**. Offers two implementations: + * **Numpy**. Faster + * **Mathutils**. Old implementation. Slower. + Only available in Surface Mode (in N-Panel) + +- **Ouput Numpy**. Output NumPy arrays in stead of regular list (makes node faster) + Only available in Surface and Edges modes (in N-Panel) + + Outputs ------- - **Verts** - random vertices on mesh -- **Face index** - indexes of faces to which random vertices lays. This input - is available only when **Mode** parameter is set to **Surface**. +- **Face / Edges index** - indexes of faces/edges to which random vertices lays. This input + is available only when **Mode** parameter is set to **Surface** or **Edges**. Examples -------- diff --git a/docs/nodes/vector/interpolation_mk3.rst b/docs/nodes/vector/interpolation_mk3.rst index 754517063b..c1ecfbf2a9 100644 --- a/docs/nodes/vector/interpolation_mk3.rst +++ b/docs/nodes/vector/interpolation_mk3.rst @@ -1,5 +1,6 @@ Vector Interpolation ==================== + Functionality ------------- @@ -9,23 +10,41 @@ Performs linear or cubic spline interpolation based on input points by creating Input & Output -------------- -+--------+----------+-------------------------------------------+ -| socket | name | Description | -+========+==========+===========================================+ -| input | Vertices | Points to interpolate | -+--------+----------+-------------------------------------------+ -| input | t | Value to interpolate | -+--------+----------+-------------------------------------------+ -| output | Vertices | Interpolated points | -+--------+----------+-------------------------------------------+ - ++--------+----------+------------------------------------------------+ +| socket | name | Description | ++========+==============+============================================+ +| input | Vertices | Points to interpolate | ++--------+--------------+--------------------------------------------+ +| input | t | Value to interpolate | ++--------+--------------+--------------------------------------------+ +| output | Vertices | Interpolated points | ++--------+--------------+--------------------------------------------+ +| output | Tangent | Tangents at outputted vertices | ++--------+--------------+--------------------------------------------+ +| output | Tangent Norm | Normalized Tangents at outputted vertices | ++--------+--------------+--------------------------------------------+ + +Paramters +--------- + + - *Mode* : Interpolation method. Can be Linear or Cubic + - *Cyclic*: Treat the input vertices as a cyclic path. + - *Int Range*: When activated the node will expect a Interger Value in the 't' input and will create a range from 0 to 1 with the inputted steps. + - *End Point*: (Only when Int Range is activated) If active the generated range will exclude 1. Usefull when the value 0 and 1 of the interpolation is the same + +Extra Parameters +---------------- + + - *Knot Mode*: Used for different cubic interpolations. Can be 'Manhattan', 'Euclidan', 'Points' and 'Chebyshev' + - *List Match*: How List should be matched + - *Ouput Numpy*: Ouputs numpy arrays in stead of regular python lists (makes node faster) Examples -------- .. image:: https://cloud.githubusercontent.com/assets/619340/4185874/ca99927c-375b-11e4-8cc8-451456bfb194.png :alt: interpol-simple.png -Sine interpolated from 5 points. The input points are shown with numbers. +Sine interpolated from 5 points. The input points are shown with numbers. .. image:: https://cloud.githubusercontent.com/assets/619340/4185875/ca9f56ee-375b-11e4-83fd-a746c8cc690b.png :alt: interpol-surface.png @@ -36,4 +55,3 @@ Notes ------- The node doesn't extrapolate. Values outside of ``[0, 1]`` are ignored. -It doesn't support cyclic interpolation (TODO). diff --git a/docs/shortcuts.rst b/docs/shortcuts.rst index 757e278291..689cba0b40 100644 --- a/docs/shortcuts.rst +++ b/docs/shortcuts.rst @@ -8,6 +8,14 @@ This is a collection of shortcuts useful in the Sverchok node tree, some are Ble **Shift + A** - Add node menu +**Numbers 1 to 5** - Partial add node menus + + - 1: Basic Data Types: Number, Vector, Matrix, Color, List and Dictionary + - 2: Mesh: Generators, Transforms, Modifiers and CAD + - 3: Advanced Objects: Curve, Surface, Field, Spatial, Pulga Physics, Alpha and Beta + - 4: Connection: Viz, Text, Scene, Exchange, BPY Data, Network and SVG + - 5: Sv Interface: Layout, Monad, Group and Presets + **Shift + S** - Add solids node menu (needs FreeCad) **Alt + Space** - Add node search menu diff --git a/index.md b/index.md index e55b0b45c9..497bae905d 100644 --- a/index.md +++ b/index.md @@ -284,6 +284,7 @@ SvKDTreeNodeMK2 SvKDTreeEdgesNodeMK2 SvKDTreePathNode + SvNearestPointOnMeshNode SvBvhOverlapNodeNew SvMeshFilterNode SvEdgeAnglesNode @@ -302,7 +303,7 @@ SvInscribedCircleNode SvSteinerEllipseNode --- - SvMeshSelectNode + SvMeshSelectNodeMk2 SvSelectSimilarNode SvChessSelection @@ -527,7 +528,7 @@ ## Matrix SvMatrixInNodeMK4 - MatrixOutNode + SvMatrixOutNodeMK2 SvMatrixApplyJoinNode SvIterateNode MatrixDeformNode @@ -623,6 +624,7 @@ SvPointOnMeshNodeMK2 SvOBJRayCastNodeMK2 SvSCNRayCastNodeMK2 + SvSetLoopNormalsNode ## Scene SvObjectsNodeMK3 @@ -715,7 +717,6 @@ SvSampleUVColorNode SvSubdivideLiteNode SvExtrudeSeparateLiteNode - SvBVHnearNewNode SvUnsubdivideNode SvLimitedDissolveMK2 SvArmaturePropsNode @@ -725,7 +726,7 @@ SvSelectMeshVerts SvSetCustomMeshNormals --- - SvCombinatoricsNode + SvCombinatoricsNode ## Alpha Nodes SvBManalyzinNode diff --git a/json_examples/Advanced/Candy.json b/json_examples/Advanced/Candy.json index 3aa2f09e48..da768b497d 100644 --- a/json_examples/Advanced/Candy.json +++ b/json_examples/Advanced/Candy.json @@ -755,7 +755,7 @@ "width": 140.0 }, "Select mesh elements by location": { - "bl_idname": "SvMeshSelectNode", + "bl_idname": "SvMeshSelectNodeMk2", "custom_socket_props": { "3": { "prop": [ @@ -1117,4 +1117,4 @@ 0 ] ] -} \ No newline at end of file +} diff --git a/json_examples/Advanced/Pineapple.json b/json_examples/Advanced/Pineapple.json index c0c2a9442d..96e1b41e08 100644 --- a/json_examples/Advanced/Pineapple.json +++ b/json_examples/Advanced/Pineapple.json @@ -64,7 +64,7 @@ "Vertex color mk3.001": "Frame.006" }, "groups": { - "Monad": "{\"export_version\": \"0.10\", \"framed_nodes\": {}, \"groups\": {}, \"nodes\": {\"Group Inputs Exp\": {\"bl_idname\": \"SvGroupInputsNodeExp\", \"height\": 100.0, \"width\": 140.0, \"label\": \"\", \"hide\": false, \"location\": [1028.47900390625, -0.3426551818847656], \"params\": {\"node_kind\": \"outputs\"}, \"custom_socket_props\": {}, \"color\": [0.8308190107345581, 0.911391019821167, 0.7545620203018188], \"use_custom_color\": true, \"outputs\": [[\"Vertices\", \"SvVerticesSocket\"], [\"Movement Vectors\", \"SvVerticesSocket\"], [\"Polygons\", \"SvStringsSocket\"], [\"Center\", \"SvVerticesSocket\"], [\"Radius\", \"SvStringsSocket\"]]}, \"Group Outputs Exp\": {\"bl_idname\": \"SvGroupOutputsNodeExp\", \"height\": 100.0, \"width\": 140.0, \"label\": \"\", \"hide\": false, \"location\": [1871.47509765625, -0.3426551818847656], \"params\": {\"node_kind\": \"inputs\"}, \"custom_socket_props\": {}, \"color\": [0.8308190107345581, 0.911391019821167, 0.7545620203018188], \"use_custom_color\": true, \"inputs\": [[\"Vertices\", \"SvVerticesSocket\"]]}, \"Move\": {\"bl_idname\": \"SvMoveNodeMk3\", \"height\": 100.0, \"width\": 140.0, \"label\": \"\", \"hide\": false, \"location\": [1691.47509765625, -18.33763885498047], \"params\": {\"movement_vectors\": [0.0, 0.0, 0.20000000298023224]}, \"custom_socket_props\": {\"1\": {\"expanded\": true}}}, \"Select mesh elements by location\": {\"bl_idname\": \"SvMeshSelectNode\", \"height\": 100.0, \"width\": 140.0, \"label\": \"\", \"hide\": false, \"location\": [1238.47900390625, 17.652328491210938], \"params\": {\"mode\": \"BySphere\", \"radius\": 0.6000000238418579, \"include_partial\": true}, \"custom_socket_props\": {\"3\": {\"prop\": [0.0, 0.0, 1.0]}, \"4\": {\"prop\": [0.0, 0.0, -1.0], \"expanded\": true}}}, \"Proportional Edit Falloff\": {\"bl_idname\": \"SvProportionalEditNode\", \"height\": 100.0, \"width\": 166.3173828125, \"label\": \"\", \"hide\": false, \"location\": [1466.7452392578125, -14.141777992248535], \"params\": {\"radius\": 0.699999988079071, \"falloff_type\": \"sharp\"}, \"custom_socket_props\": {}}}, \"update_lists\": [[\"Move\", 0, \"Group Outputs Exp\", 0], [\"Group Inputs Exp\", 0, \"Move\", 0], [\"Group Inputs Exp\", 1, \"Move\", 1], [\"Proportional Edit Falloff\", 0, \"Move\", 2], [\"Group Inputs Exp\", 0, \"Select mesh elements by location\", 0], [\"Group Inputs Exp\", 2, \"Select mesh elements by location\", 2], [\"Group Inputs Exp\", 3, \"Select mesh elements by location\", 4], [\"Group Inputs Exp\", 4, \"Select mesh elements by location\", 6], [\"Group Inputs Exp\", 0, \"Proportional Edit Falloff\", 0], [\"Select mesh elements by location\", 0, \"Proportional Edit Falloff\", 1]], \"bl_idname\": \"SverchGroupTreeType\", \"cls_bl_idname\": \"SvGroupNodeMonad_140486095450048\"}" + "Monad": "{\"export_version\": \"0.10\", \"framed_nodes\": {}, \"groups\": {}, \"nodes\": {\"Group Inputs Exp\": {\"bl_idname\": \"SvGroupInputsNodeExp\", \"height\": 100.0, \"width\": 140.0, \"label\": \"\", \"hide\": false, \"location\": [1028.47900390625, -0.3426551818847656], \"params\": {\"node_kind\": \"outputs\"}, \"custom_socket_props\": {}, \"color\": [0.8308190107345581, 0.911391019821167, 0.7545620203018188], \"use_custom_color\": true, \"outputs\": [[\"Vertices\", \"SvVerticesSocket\"], [\"Movement Vectors\", \"SvVerticesSocket\"], [\"Polygons\", \"SvStringsSocket\"], [\"Center\", \"SvVerticesSocket\"], [\"Radius\", \"SvStringsSocket\"]]}, \"Group Outputs Exp\": {\"bl_idname\": \"SvGroupOutputsNodeExp\", \"height\": 100.0, \"width\": 140.0, \"label\": \"\", \"hide\": false, \"location\": [1871.47509765625, -0.3426551818847656], \"params\": {\"node_kind\": \"inputs\"}, \"custom_socket_props\": {}, \"color\": [0.8308190107345581, 0.911391019821167, 0.7545620203018188], \"use_custom_color\": true, \"inputs\": [[\"Vertices\", \"SvVerticesSocket\"]]}, \"Move\": {\"bl_idname\": \"SvMoveNodeMk3\", \"height\": 100.0, \"width\": 140.0, \"label\": \"\", \"hide\": false, \"location\": [1691.47509765625, -18.33763885498047], \"params\": {\"movement_vectors\": [0.0, 0.0, 0.20000000298023224]}, \"custom_socket_props\": {\"1\": {\"expanded\": true}}}, \"Select mesh elements by location\": {\"bl_idname\": \"SvMeshSelectNodeMk2\", \"height\": 100.0, \"width\": 140.0, \"label\": \"\", \"hide\": false, \"location\": [1238.47900390625, 17.652328491210938], \"params\": {\"mode\": \"BySphere\", \"radius\": 0.6000000238418579, \"include_partial\": true}, \"custom_socket_props\": {\"3\": {\"prop\": [0.0, 0.0, 1.0]}, \"4\": {\"prop\": [0.0, 0.0, -1.0], \"expanded\": true}}}, \"Proportional Edit Falloff\": {\"bl_idname\": \"SvProportionalEditNode\", \"height\": 100.0, \"width\": 166.3173828125, \"label\": \"\", \"hide\": false, \"location\": [1466.7452392578125, -14.141777992248535], \"params\": {\"radius\": 0.699999988079071, \"falloff_type\": \"sharp\"}, \"custom_socket_props\": {}}}, \"update_lists\": [[\"Move\", 0, \"Group Outputs Exp\", 0], [\"Group Inputs Exp\", 0, \"Move\", 0], [\"Group Inputs Exp\", 1, \"Move\", 1], [\"Proportional Edit Falloff\", 0, \"Move\", 2], [\"Group Inputs Exp\", 0, \"Select mesh elements by location\", 0], [\"Group Inputs Exp\", 2, \"Select mesh elements by location\", 2], [\"Group Inputs Exp\", 3, \"Select mesh elements by location\", 4], [\"Group Inputs Exp\", 4, \"Select mesh elements by location\", 6], [\"Group Inputs Exp\", 0, \"Proportional Edit Falloff\", 0], [\"Select mesh elements by location\", 0, \"Proportional Edit Falloff\", 1]], \"bl_idname\": \"SverchGroupTreeType\", \"cls_bl_idname\": \"SvGroupNodeMonad_140486095450048\"}" }, "nodes": { "3pt Arc": { @@ -1898,4 +1898,4 @@ 4 ] ] -} \ No newline at end of file +} diff --git a/json_examples/Architecture/Coliseum.json b/json_examples/Architecture/Coliseum.json index 15309a6937..2922b00049 100644 --- a/json_examples/Architecture/Coliseum.json +++ b/json_examples/Architecture/Coliseum.json @@ -683,7 +683,7 @@ "width": 140.0 }, "Select mesh elements by location": { - "bl_idname": "SvMeshSelectNode", + "bl_idname": "SvMeshSelectNodeMk2", "custom_socket_props": { "3": { "prop": [ @@ -1067,4 +1067,4 @@ 2 ] ] -} \ No newline at end of file +} diff --git a/json_examples/Architecture/ProfileBuilding.json b/json_examples/Architecture/ProfileBuilding.json index 2fff9c9ebb..1172e8a10d 100644 --- a/json_examples/Architecture/ProfileBuilding.json +++ b/json_examples/Architecture/ProfileBuilding.json @@ -537,7 +537,7 @@ "width": 16.0 }, "Select mesh elements by location": { - "bl_idname": "SvMeshSelectNode", + "bl_idname": "SvMeshSelectNodeMk2", "custom_socket_props": { "3": { "expanded": true, @@ -561,7 +561,7 @@ "width": 140.0 }, "Select mesh elements by location.002": { - "bl_idname": "SvMeshSelectNode", + "bl_idname": "SvMeshSelectNodeMk2", "custom_socket_props": { "3": { "expanded": true, @@ -835,4 +835,4 @@ 0 ] ] -} \ No newline at end of file +} diff --git a/json_examples/Design/Adaptive_per_face.json b/json_examples/Design/Adaptive_per_face.json index bdb755e021..9d64d7431f 100644 --- a/json_examples/Design/Adaptive_per_face.json +++ b/json_examples/Design/Adaptive_per_face.json @@ -417,7 +417,7 @@ "width": 16.0 }, "Select mesh elements by location": { - "bl_idname": "SvMeshSelectNode", + "bl_idname": "SvMeshSelectNodeMk2", "custom_socket_props": { "3": { "expanded": true, @@ -450,7 +450,7 @@ "width": 140.0 }, "Select mesh elements by location.001": { - "bl_idname": "SvMeshSelectNode", + "bl_idname": "SvMeshSelectNodeMk2", "custom_socket_props": { "3": { "expanded": true, @@ -908,4 +908,4 @@ 3 ] ] -} \ No newline at end of file +} diff --git a/json_examples/Shapes/Blender_logo.json b/json_examples/Shapes/Blender_logo.json index 6b27718687..401b2bb4a4 100644 --- a/json_examples/Shapes/Blender_logo.json +++ b/json_examples/Shapes/Blender_logo.json @@ -300,7 +300,7 @@ "width": 140.0 }, "Select mesh elements by location": { - "bl_idname": "SvMeshSelectNode", + "bl_idname": "SvMeshSelectNodeMk2", "custom_socket_props": { "3": { "expanded": true, @@ -603,4 +603,4 @@ 1 ] ] -} \ No newline at end of file +} diff --git a/json_examples/Shapes/Donut.json b/json_examples/Shapes/Donut.json index 332565c20f..cd28ca2325 100644 --- a/json_examples/Shapes/Donut.json +++ b/json_examples/Shapes/Donut.json @@ -5,7 +5,7 @@ "Matrix In": "Tangent Move", "Matrix Math": "Tangent Move", "Matrix Track To": "Tangent Move", - "Matrix out": "Tangent Move", + "Matrix Out": "Tangent Move", "Mesh viewer": "Output", "Move": "Bottom Flat", "Move.002": "Tangent Move", @@ -139,8 +139,8 @@ }, "width": 140.0 }, - "Matrix out": { - "bl_idname": "MatrixOutNode", + "Matrix Out": { + "bl_idname": "SvMatrixOutNodeMK2", "height": 100.0, "hide": false, "label": "", @@ -495,7 +495,7 @@ "width": 140.0 }, "Select mesh elements by location": { - "bl_idname": "SvMeshSelectNode", + "bl_idname": "SvMeshSelectNodeMk2", "custom_socket_props": { "3": { "prop": [ @@ -527,7 +527,7 @@ "width": 140.0 }, "Select mesh elements by location.001": { - "bl_idname": "SvMeshSelectNode", + "bl_idname": "SvMeshSelectNodeMk2", "custom_socket_props": { "3": { "expanded": true, @@ -860,7 +860,7 @@ [ "Matrix Math", 0, - "Matrix out", + "Matrix Out", 0 ], [ diff --git a/json_examples/Shapes/SverchokLogo.json b/json_examples/Shapes/SverchokLogo.json index 1f65a178f0..04b9e44be6 100644 --- a/json_examples/Shapes/SverchokLogo.json +++ b/json_examples/Shapes/SverchokLogo.json @@ -697,7 +697,7 @@ "width": 400.0 }, "Select mesh elements by location": { - "bl_idname": "SvMeshSelectNode", + "bl_idname": "SvMeshSelectNodeMk2", "custom_socket_props": { "3": { "prop": [ @@ -1205,4 +1205,4 @@ 1 ] ] -} \ No newline at end of file +} diff --git a/json_examples/Shapes/Technical_ring_with_holes.json b/json_examples/Shapes/Technical_ring_with_holes.json index 3f98690dd0..fb13cf423f 100644 --- a/json_examples/Shapes/Technical_ring_with_holes.json +++ b/json_examples/Shapes/Technical_ring_with_holes.json @@ -289,7 +289,7 @@ "width": 140.0 }, "Select mesh elements by location": { - "bl_idname": "SvMeshSelectNode", + "bl_idname": "SvMeshSelectNodeMk2", "custom_socket_props": { "3": { "expanded": true, @@ -526,4 +526,4 @@ 2 ] ] -} \ No newline at end of file +} diff --git a/menu.py b/menu.py index e512084c97..e2b49530e6 100644 --- a/menu.py +++ b/menu.py @@ -485,7 +485,7 @@ def make_categories(): items=node_items)) node_count += len(nodes) node_categories.append(SverchNodeCategory("SVERCHOK_MONAD", "Monad", items=sv_group_items)) - + SverchNodeItem.new('SvMonadInfoNode') return node_categories, node_count, original_categories def register_node_panels(identifier, std_menu): @@ -593,11 +593,10 @@ def unregister_node_panels(): def reload_menu(): menu, node_count, original_categories = make_categories() - if 'SVERCHOK' in nodeitems_utils._node_categories: + if hasattr(bpy.types, "SV_PT_NodesTPanel"): unregister_node_panels() - nodeitems_utils.unregister_node_categories("SVERCHOK") unregister_node_add_operators() - nodeitems_utils.register_node_categories("SVERCHOK", menu) + register_node_panels("SVERCHOK", menu) register_node_add_operators() @@ -632,10 +631,8 @@ def register(): global logger logger = getLogger("menu") menu, node_count, original_categories = make_categories() - if 'SVERCHOK' in nodeitems_utils._node_categories: + if hasattr(bpy.types, "SV_PT_NodesTPanel"): unregister_node_panels() - nodeitems_utils.unregister_node_categories("SVERCHOK") - nodeitems_utils.register_node_categories("SVERCHOK", menu) categories = [(category.identifier, category.name, category.name, i) for i, category in enumerate(menu)] bpy.types.Scene.sv_selected_category = bpy.props.EnumProperty( @@ -656,9 +653,7 @@ def register(): print(f"sv: {node_count} nodes.") def unregister(): - if 'SVERCHOK' in nodeitems_utils._node_categories: - unregister_node_panels() - nodeitems_utils.unregister_node_categories("SVERCHOK") + unregister_node_panels() unregister_node_add_operators() bpy.utils.unregister_class(SvResetNodeSearchOperator) del bpy.types.Scene.sv_selected_category diff --git a/node_tree.py b/node_tree.py index e58af361e5..baa69632e8 100644 --- a/node_tree.py +++ b/node_tree.py @@ -184,7 +184,7 @@ def on_draft_mode_changed(self, context): sv_show_error_details : BoolProperty( name = "Show error details", description = "Display exception stack in the node view as well", - default = False, + default = False, update=lambda s, c: process_tree(s), options=set()) @@ -322,6 +322,11 @@ def init(self, context): sys.stderr.write('ERROR: %s\n' % str(err)) self.set_color() + def sv_new_input(self, socket_type, name, **attrib_dict): + socket = self.inputs.new(socket_type, name) + for att in attrib_dict: + setattr(socket, att, attrib_dict[att]) + def free(self): """ This method is not supposed to be overriden in specific nodes. @@ -603,7 +608,7 @@ def migrate_from(self, old_node): def get_and_set_gl_scale_info(self, origin=None): # todo, probably openGL viewers should have its own mixin class """ - This function is called in sv_init in nodes that draw GL instructions to the nodeview, + This function is called in sv_init in nodes that draw GL instructions to the nodeview, the nodeview scale and dpi differs between users and must be queried to get correct nodeview x,y and dpi scale info. """ diff --git a/nodes/analyzer/bbox_mk3.py b/nodes/analyzer/bbox_mk3.py index 11ada8f559..669ab8d0b2 100644 --- a/nodes/analyzer/bbox_mk3.py +++ b/nodes/analyzer/bbox_mk3.py @@ -17,24 +17,132 @@ # ##### END GPL LICENSE BLOCK ##### from itertools import product - +import numpy as np import bpy from bpy.props import BoolVectorProperty, EnumProperty from mathutils import Matrix from sverchok.node_tree import SverchCustomTreeNode +from sverchok.utils.nodes_mixins.recursive_nodes import SvRecursiveNode + from sverchok.data_structure import dataCorrect, updateNode +EDGES = [ + (0, 1), (1, 3), (3, 2), (2, 0), # bottom edges + (4, 5), (5, 7), (7, 6), (6, 4), # top edges + (0, 4), (1, 5), (2, 6), (3, 7) # sides +] +def generate_matrix(maxmin, dims, to_2d): + center = [(u+v)*.5 for u, v in maxmin[:dims]] + scale = [(u-v) for u, v in maxmin[:dims]] + if to_2d: + center += [0] + scale += [1] + mat = Matrix.Translation(center) + for i, sca in enumerate(scale): + mat[i][i] = sca + return mat + +def generate_mean_np(verts, dims, to_2d): + avr = (np.sum(verts[:, :dims], axis=0)/len(verts)).tolist() + if to_2d: + avr += [0] + return [avr] + +def generate_mean(verts, dims, to_2d): + avr = list(map(sum, zip(*verts))) + avr = [n/len(verts) for n in avr[:dims]] + if to_2d: + avr += [0] + return [avr] + +def bounding_box(verts, + box_dimensions='2D', + output_verts=True, + output_mat=True, + output_mean=True, + output_limits=True): + ''' + verts expects a list of level 3 [[[0,0,0],[1,1,1]..],[]] + returns per sublist: + verts_out: vertices of the bounding box + edges_out: edges of the bounding box + mean_out: mean of all vertcies + mat_out: Matrix that would transform a box of 1 unit into the bbox + *min_vals, Min X, Y and Z of the bounding box + *max_vals, Max X, Y and Z of the bounding box + *size_vals Size X, Y and Z of the bounding box + ''' + verts_out = [] + edges_out = [] + edges = EDGES + + mat_out = [] + mean_out = [] + min_vals = [[], [], []] + max_vals = [[], [], []] + size_vals = [[], [], []] + to_2d = box_dimensions == '2D' + dims = int(box_dimensions[0]) + calc_maxmin = output_mat or output_verts or output_limits + + for vec in verts: + if calc_maxmin: + if isinstance(vec, np.ndarray): + np_vec = vec + else: + np_vec = np.array(vec) + bbox_max = np.amax(np_vec, axis=0) + bbox_min = np.amin(np_vec, axis=0) + maxmin = np.concatenate([bbox_max, bbox_min]).reshape(2,3).T.tolist() + + if output_verts: + out = list(product(*reversed(maxmin))) + v_out = [l[::-1] for l in out[::-1]] + if to_2d: + verts_out.append([[v[0], v[1], 0] for v in v_out[:4]]) + edges = edges[:4] + else: + verts_out.append(v_out) + edges_out.append(edges) + + if output_mat: + mat_out.append(generate_matrix(maxmin, dims, to_2d)) + + if output_mean: + if calc_maxmin: + mean_out.append(generate_mean_np(np_vec, dims, to_2d)) + else: + if isinstance(vec, np.ndarray): + mean_out.append(generate_mean_np(vec, dims, to_2d)) + else: + mean_out.append(generate_mean(vec, dims, to_2d)) + + if output_limits: + for i in range(dims): + min_vals[i].append([maxmin[i][1]]) + max_vals[i].append([maxmin[i][0]]) + size_vals[i].append([maxmin[i][0] - maxmin[i][1]]) + + return (verts_out, + edges_out, + mean_out, + mat_out, + *min_vals, + *max_vals, + *size_vals) -class SvBBoxNodeMk3(bpy.types.Node, SverchCustomTreeNode): + +class SvBBoxNodeMk3(bpy.types.Node, SverchCustomTreeNode, SvRecursiveNode): """ Triggers: Bbox 2D or 3D Tooltip: Get vertices bounding box (vertices, sizes, center) """ bl_idname = 'SvBBoxNodeMk3' bl_label = 'Bounding box' - bl_icon = 'NONE' + bl_icon = 'SHADING_BBOX' sv_icon = 'SV_BOUNDING_BOX' + def update_sockets(self, context): bools = [self.min_list, self.max_list, self.size_list] dims = int(self.box_dimensions[0]) @@ -81,7 +189,7 @@ def draw_buttons(self, context, layout): def sv_init(self, context): son = self.outputs.new - self.inputs.new('SvVerticesSocket', 'Vertices') + self.inputs.new('SvVerticesSocket', 'Vertices').is_mandatory = True son('SvVerticesSocket', 'Vertices') son('SvStringsSocket', 'Edges') @@ -97,95 +205,20 @@ def sv_init(self, context): def migrate_from(self, old_node): self.box_dimensions = old_node.dimensions - def generate_matrix(self, maxmin, dims, to_2d): - center = [(u+v)*.5 for u, v in maxmin[:dims]] - scale = [(u-v) for u, v in maxmin[:dims]] - if to_2d: - center += [0] - scale += [1] - mat = Matrix.Translation(center) - for i, sca in enumerate(scale): - mat[i][i] = sca - return mat - - def generate_mean(self, verts, dims, to_2d): - avr = list(map(sum, zip(*verts))) - avr = [n/len(verts) for n in avr[:dims]] - if to_2d: - avr += [0] - return [avr] - - def process(self): - if not self.inputs['Vertices'].is_linked: - return - if not any(s.is_linked for s in self.outputs): - return - has_mat_out = bool(self.outputs['Center'].is_linked) - has_mean = bool(self.outputs['Mean'].is_linked) - has_vert_out = bool(self.outputs['Vertices'].is_linked) - - verts = self.inputs['Vertices'].sv_get(deepcopy=False) - verts = dataCorrect(verts, nominal_dept=2) - has_limits = any(s.is_linked for s in self.outputs[4:]) - if verts: - verts_out = [] - edges_out = [] - edges = [ - (0, 1), (1, 3), (3, 2), (2, 0), # bottom edges - (4, 5), (5, 7), (7, 6), (6, 4), # top edges - (0, 4), (1, 5), (2, 6), (3, 7) # sides - ] - - mat_out = [] - mean_out = [] - min_vals = [[], [], []] - max_vals = [[], [], []] - size_vals = [[], [], []] - to_2d = self.box_dimensions == '2D' - dims = int(self.box_dimensions[0]) - - for vec in verts: - if has_mat_out or has_vert_out or has_limits: - maxmin = list(zip(map(max, *vec), map(min, *vec))) - if has_vert_out: - out = list(product(*reversed(maxmin))) - v_out = [l[::-1] for l in out[::-1]] - if to_2d: - verts_out.append([[v[0], v[1], 0] for v in v_out[:4]]) - edges = edges[:4] - else: - verts_out.append(v_out) - edges_out.append(edges) - - if has_mat_out: - mat_out.append(self.generate_matrix(maxmin, dims, to_2d)) - - if has_mean: - mean_out.append(self.generate_mean(vec, dims, to_2d)) - - if has_limits: - for i in range(dims): - min_vals[i].append([maxmin[i][1]]) - max_vals[i].append([maxmin[i][0]]) - size_vals[i].append([maxmin[i][0] - maxmin[i][1]]) - - if has_vert_out: - self.outputs['Vertices'].sv_set(verts_out) - - if self.outputs['Edges'].is_linked: - self.outputs['Edges'].sv_set(edges_out) - - if has_mean: - self.outputs['Mean'].sv_set(mean_out) - - if self.outputs['Center'].is_linked: - self.outputs['Center'].sv_set(mat_out) - - vals = [min_vals, max_vals, size_vals] - for j in range(3): - for i, socket in enumerate(self.outputs[4+3*j:7+3*j]): - if socket.is_linked: - socket.sv_set(vals[j][i]) + def process_data(self, params): + + verts = params[0] + output_mat = self.outputs['Center'].is_linked + output_mean = self.outputs['Mean'].is_linked + output_verts = self.outputs['Vertices'].is_linked + output_limits = any(s.is_linked for s in self.outputs[4:]) + return bounding_box(verts, + box_dimensions=self.box_dimensions, + output_verts=output_verts, + output_mat=output_mat, + output_mean=output_mean, + output_limits=output_limits) + def register(): diff --git a/nodes/analyzer/mesh_select_mk2.py b/nodes/analyzer/mesh_select_mk2.py new file mode 100644 index 0000000000..4ed7c1f72d --- /dev/null +++ b/nodes/analyzer/mesh_select_mk2.py @@ -0,0 +1,379 @@ +# ##### 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 math + +from mathutils import Vector, Matrix, kdtree + +import bpy +from bpy.props import IntProperty, FloatProperty, BoolProperty, EnumProperty +import numpy as np +from sverchok.node_tree import SverchCustomTreeNode +from sverchok.data_structure import updateNode, match_long_repeat, describe_data_shape, numpy_match_long_repeat, repeat_last_for_length +from sverchok.utils.logging import info, debug +from sverchok.utils.math import np_dot +from sverchok.utils.sv_bmesh_utils import bmesh_from_pydata, pydata_from_bmesh +from sverchok.nodes.analyzer.normals import calc_mesh_normals +from sverchok.utils.nodes_mixins.recursive_nodes import SvRecursiveNode + +def select_verts_by_faces(faces, verts): + flat_index_list = {idx for face in faces for idx in face} + return [v in flat_index_list for v in range(len(verts))] + +def select_edges_by_verts(verts_mask, edges, include_partial): + if isinstance(verts_mask, np.ndarray): + return select_edges_by_verts_numpy(verts_mask, edges, include_partial) + return select_edges_by_verts_python(verts_mask, edges, include_partial) + +def select_edges_by_verts_python(verts_mask, edges, include_partial): + result = [] + for u,v in edges: + if include_partial: + ok = verts_mask[u] or verts_mask[v] + else: + ok = verts_mask[u] and verts_mask[v] + result.append(ok) + return result + +def select_edges_by_verts_numpy(verts_mask, edges, include_partial): + if include_partial: + result = np.any(verts_mask[np.array(edges)], axis=1) + else: + result = np.all(verts_mask[np.array(edges)], axis=1) + + return result.tolist() + +def select_faces_by_verts(verts_mask, faces, include_partial): + result = [] + for face in faces: + if include_partial: + ok = any(verts_mask[i] for i in face) + else: + ok = all(verts_mask[i] for i in face) + result.append(ok) + return result + +def map_percent(values, percent): + maxv = np.amax(values) + minv = np.amin(values) + if maxv <= minv: + return maxv + return maxv - percent * (maxv - minv) * 0.01 + +def by_side(verts, direction, percent): + np_verts = np.array(verts) + np_dir = np.array(direction) + np_dir, np_percent = numpy_match_long_repeat([np_dir, np.array(percent)]) + values = np_dot(np_verts[:,np.newaxis], np_dir[np.newaxis,:], axis=2) + threshold = map_percent(values, np_percent) + out_verts_mask = np.any(values >= threshold, axis=1) + return out_verts_mask + +def by_bbox(vertices, points, radius): + np_rads = np.array(radius) + np_verts = np.array(vertices) + np_bbox = np.array(points) + bbox_max = np.amax(np_bbox, axis=0) + bbox_min = np.amin(np_bbox, axis=0) + min_mask = np_verts > bbox_min - np_rads[:, np.newaxis] + max_mask = np_verts < bbox_max + np_rads[:, np.newaxis] + out_verts_mask = np.all(np.all([min_mask, max_mask], axis=0), axis=1) + + return out_verts_mask + +def by_sphere(vertices, centers, radius): + + if len(centers) == 1: + center = centers[0] + out_verts_mask = np.linalg.norm(np.array(vertices)-np.array(center)[np.newaxis,:], axis=1)<= radius[0] + else: + # build KDTree + tree = kdtree.KDTree(len(centers)) + for i, v in enumerate(centers): + tree.insert(v, i) + tree.balance() + + out_verts_mask = [] + rad = repeat_last_for_length(radius, len(centers)) + for vertex in vertices: + _, idx, rho = tree.find(vertex) + mask = rho <= rad[idx] + out_verts_mask.append(mask) + return out_verts_mask + + +def by_plane(vertices, center, radius, direction): + + np_verts = np.array(vertices) + np_center = np.array(center) + np_normal = np.array(direction) + normal_length= np.linalg.norm(np_normal, axis=1) + np_normal /= normal_length[:, np.newaxis] + np_rad = np.array(radius) + np_center, np_normal, np_rad = numpy_match_long_repeat([np_center, np_normal, np_rad]) + vs = np_verts[np.newaxis, :, :] - np_center[:, np.newaxis,:] + distance = np.abs(np.sum(vs * np_normal[:,np.newaxis, :], axis=2)) + out_verts_mask = np.any(distance < np_rad[:,np.newaxis], axis=0) + + return out_verts_mask + + +def by_cylinder(vertices, center, radius, direction): + np_vertices = np.array(vertices) + np_location = np.array(center) + np_direction = np.array(direction) + np_direction = np_direction/np.linalg.norm(np_direction, axis=1)[:, np.newaxis] + np_radius = np.array(radius) + np_location, np_direction, np_radius = numpy_match_long_repeat([np_location, np_direction, np_radius]) + v_attract = np_vertices[np.newaxis, :, :] - np_location[:, np.newaxis, :] + vect_proy = np_dot(v_attract, np_direction[:, np.newaxis, :], axis=2) + + closest_point = np_location[:, np.newaxis, :] + vect_proy[:, :, np.newaxis] * np_direction[:, np.newaxis, :] + + dif_v = closest_point - np_vertices[np.newaxis, :, :] + dist_attract = np.linalg.norm(dif_v, axis=2) + out_verts_mask = np.any(dist_attract < np_radius[:, np.newaxis], axis=0) + + return out_verts_mask + +def by_normal(vertices, edges, faces, percent, direction): + face_normals, _ = calc_mesh_normals(vertices, edges, faces) + np_verts = np.array(face_normals) + np_dir = np.array(direction) + np_dir, np_percent = numpy_match_long_repeat([np_dir, np.array(percent)]) + values = np_dot(np_verts[:, np.newaxis], np_dir[np.newaxis,:], axis=2) + threshold = map_percent(values, np_percent) + out_face_mask = np.any(values >= threshold, axis=1) + + return out_face_mask + +def by_outside(vertices, edges, faces, percent, center): + face_normals, _ = calc_mesh_normals(vertices, edges, faces) + center = Vector(center[0]) + + def get_center(face): + verts = [Vector(vertices[i]) for i in face] + result = Vector((0,0,0)) + for v in verts: + result += v + return (1.0/float(len(verts))) * result + + values = [] + for face, normal in zip(faces, face_normals): + face_center = get_center(face) + direction = face_center - center + dirlength = direction.length + if dirlength > 0: + value = math.pi - direction.angle(normal) + else: + value = math.pi + values.append(value) + threshold = map_percent(values, percent[0]) + + out_face_mask = [(value >= threshold) for value in values] + + return out_face_mask + + +def by_edge_dir(vertices, edges, percent, direction): + + np_verts = np.array(vertices) + np_edges = np.array(edges) + edges_v = np_verts[np_edges] + edges_dir = edges_v[:, 1 , :] - edges_v[:, 0 ,:] + np_dir = np.array(direction) + np_dir /= np.linalg.norm(np_dir, axis=1)[:, np.newaxis] + edges_dir /= np.linalg.norm(edges_dir, axis=1)[:, np.newaxis] + np_percent = np.array(percent) + np_dir, np_percent = numpy_match_long_repeat([np_dir, np_percent]) + + values = np.abs(np_dot(edges_dir[:, np.newaxis], np_dir[np.newaxis, :], axis=2)) + threshold = map_percent(values, np_percent) + + out_edges_mask = np.any(values >= threshold, axis=1) + + return out_edges_mask + +def edge_modes_select(params, mode='BySide', v_mask=True, e_mask=True, p_mask=True, include_partial=True): + vertices, edges, faces, direction, center, percent, radius = params + if mode == 'EdgeDir': + out_edges_mask = by_edge_dir(vertices, edges, percent, direction) + + out_edges = [edge for (edge, mask) in zip (edges, out_edges_mask) if mask] + + out_verts_mask = select_verts_by_faces(out_edges, vertices) if v_mask or p_mask else [] + + out_faces_mask = select_faces_by_verts(out_verts_mask, faces, False) if p_mask else [] + + return out_verts_mask, out_edges_mask.tolist() if isinstance(out_edges_mask, np.ndarray) else out_edges_mask, out_faces_mask + + + +def face_modes_select(params, mode='BySide', v_mask=True, e_mask=True, p_mask=True, include_partial=True): + vertices, edges, faces, direction, center, percent, radius = params + if mode == 'ByNormal': + out_faces_mask = by_normal(vertices, edges, faces, percent, direction) + _include_partial = False + if mode == 'Outside': + out_faces_mask = by_outside(vertices, edges, faces, percent, center) + _include_partial = include_partial + + out_faces = [face for (face, mask) in zip(faces, out_faces_mask) if mask] + out_verts_mask = select_verts_by_faces(out_faces, vertices) if v_mask or e_mask else [] + out_edges_mask = select_edges_by_verts(out_verts_mask, edges, _include_partial) if e_mask else [] + + return out_verts_mask, out_edges_mask, out_faces_mask.tolist() if (isinstance(out_faces_mask, np.ndarray) and p_mask) else out_faces_mask + +def vert_modes_select(params, mode='BySide', v_mask=True, e_mask=True, p_mask=True, include_partial=True): + verts, edges, faces, direction, center, percent, radius = params + if mode == 'BySide': + out_verts_mask = by_side(verts, direction, percent) + if mode == 'BBox': + out_verts_mask = by_bbox(verts, center, radius) + if mode == 'BySphere': + out_verts_mask = by_sphere(verts, center, radius) + if mode == 'ByPlane': + out_verts_mask = by_plane(verts, center, radius, direction) + if mode == 'ByCylinder': + out_verts_mask = by_cylinder(verts, center, radius, direction) + if e_mask: + out_edges_mask = select_edges_by_verts(out_verts_mask, edges, include_partial) + else: + out_edges_mask = [] + if p_mask: + out_faces_mask = select_faces_by_verts(out_verts_mask, faces, include_partial) + else: + out_faces_mask = [] + + return out_verts_mask.tolist() if (isinstance(out_verts_mask, np.ndarray) and v_mask) else out_verts_mask, out_edges_mask, out_faces_mask + +VERT_MODES = ['BySide', 'BBox', 'BySphere', 'ByPlane', 'ByCylinder'] +EDGE_MODES = ['EdgeDir'] +FACE_MODES = ['ByNormal', 'Outside'] + +class SvMeshSelectNodeMk2(bpy.types.Node, SverchCustomTreeNode, SvRecursiveNode): + ''' + Triggers: Mask by geometry + Tooltip: Select vertices, edges, faces by geometric criteria + ''' + bl_idname = 'SvMeshSelectNodeMk2' + bl_label = 'Select mesh elements' + bl_icon = 'UV_SYNC_SELECT' + + modes = [ + ("BySide", "By side", "Select specified side of mesh", 0), + ("ByNormal", "By normal direction", "Select faces with normal in specified direction", 1), + ("BySphere", "By center and radius", "Select vertices within specified distance from center", 2), + ("ByPlane", "By plane", "Select vertices within specified distance from plane defined by point and normal vector", 3), + ("ByCylinder", "By cylinder", "Select vertices within specified distance from straight line defined by point and direction vector", 4), + ("EdgeDir", "By edge direction", "Select edges that are nearly parallel to specified direction", 5), + ("Outside", "Normal pointing outside", "Select faces with normals pointing outside", 6), + ("BBox", "By bounding box", "Select vertices within bounding box of specified points", 7) + ] + + def update_mode(self, context): + self.inputs['Radius'].hide_safe = (self.mode not in ['BySphere', 'ByPlane', 'ByCylinder', 'BBox']) + self.inputs['Center'].hide_safe = (self.mode not in ['BySphere', 'ByPlane', 'ByCylinder', 'Outside', 'BBox']) + self.inputs['Percent'].hide_safe = (self.mode not in ['BySide', 'ByNormal', 'EdgeDir', 'Outside']) + self.inputs['Direction'].hide_safe = (self.mode not in ['BySide', 'ByNormal', 'ByPlane', 'ByCylinder', 'EdgeDir']) + + updateNode(self, context) + + mode: EnumProperty(name="Mode", + items=modes, + default='ByNormal', + update=update_mode) + + include_partial: BoolProperty(name="Include partial selection", + description="Include partially selected edges/faces", + default=False, + update=updateNode) + + percent: FloatProperty(name="Percent", + default=1.0, + min=0.0, max=100.0, + update=updateNode) + + radius: FloatProperty(name="Radius", default=1.0, min=0.0, update=updateNode) + + def draw_buttons(self, context, layout): + layout.prop(self, 'mode') + if self.mode not in ['ByNormal', 'EdgeDir']: + layout.prop(self, 'include_partial') + def draw_buttons_ext(self, context, layout): + self.draw_buttons(context, layout) + layout.prop(self, 'list_match') + + def rclick_menu(self, context, layout): + layout.prop_menu_enum(self, "list_match", text="List Match") + + def sv_init(self, context): + self.inputs.new('SvVerticesSocket', "Vertices").is_mandatory = True + eds = self.inputs.new('SvStringsSocket', "Edges") + eds.nesting_level = 3 + pols = self.inputs.new('SvStringsSocket', "Polygons") + pols.nesting_level = 3 + d = self.inputs.new('SvVerticesSocket', "Direction") + d.use_prop = True + d.default_property = (0.0, 0.0, 1.0) + d.nesting_level = 3 + c = self.inputs.new('SvVerticesSocket', "Center") + c.use_prop = True + c.default_property = (0.0, 0.0, 0.0) + c.nesting_level = 3 + perc = self.inputs.new('SvStringsSocket', 'Percent') + perc.prop_name = 'percent' + perc.nesting_level = 2 + rad = self.inputs.new('SvStringsSocket', 'Radius') + rad.prop_name = 'radius' + rad.nesting_level = 2 + + self.outputs.new('SvStringsSocket', 'VerticesMask') + self.outputs.new('SvStringsSocket', 'EdgesMask') + self.outputs.new('SvStringsSocket', 'FacesMask') + + self.update_mode(context) + + def process_data(self, params): + result = [[] for s in self.outputs] + if self.mode in VERT_MODES: + func = vert_modes_select + elif self.mode in EDGE_MODES: + func = edge_modes_select + else: + func = face_modes_select + + for local_params in zip(*params): + masks = func( + local_params, + mode=self.mode, + v_mask=self.outputs[0].is_linked, + e_mask=self.outputs[1].is_linked, + p_mask=self.outputs[2].is_linked, + include_partial=self.include_partial + ) + [r.append(r_local) for r, r_local in zip(result, masks)] + return result + + +def register(): + bpy.utils.register_class(SvMeshSelectNodeMk2) + + +def unregister(): + bpy.utils.unregister_class(SvMeshSelectNodeMk2) diff --git a/nodes/analyzer/nearest_point_on_mesh.py b/nodes/analyzer/nearest_point_on_mesh.py new file mode 100644 index 0000000000..e61e2099f7 --- /dev/null +++ b/nodes/analyzer/nearest_point_on_mesh.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 ##### +from itertools import cycle +import bpy +from bpy.props import EnumProperty, BoolProperty +from sverchok.node_tree import SverchCustomTreeNode +from sverchok.data_structure import updateNode +from sverchok.utils.bvh_tree import bvh_tree_from_polygons +from sverchok.utils.nodes_mixins.recursive_nodes import SvRecursiveNode + +def take_third(elem): + return elem[2] + +def append_multiple(container, data): + for r, rl in zip(container, data): + r.append(rl) + +def translate_data(data): + try: + return data[0][:], data[1][:], data[2], data[3] + except TypeError: + return [], [], -1, -1 + + +def svmesh_to_bvh_lists(vsock, fsock, safe_check): + for vertices, polygons in zip(vsock, fsock): + yield bvh_tree_from_polygons(vertices, polygons, all_triangles=False, epsilon=0.0, safe_check=safe_check) + +def nearest_point_in_mesh(verts, faces, points, safe_check=True): + '''Expects multiple objects lists (level of nesting 3)''' + output = [[] for i in range(4)] + for bvh, pts in zip(svmesh_to_bvh_lists(verts, faces, safe_check), points): + res_local = list(zip(*[translate_data(bvh.find_nearest(P)) for P in pts])) + append_multiple(output, res_local) + + return output + +def nearest_in_range(verts, faces, points, distance, safe_check=True, flat_output=True): + ''' + verts, faces and points: Expects multiple objects lists (level of nesting 3) + distace: expects a list with level of nesting of 2 + ''' + output = [[] for i in range(4)] + for bvh, pts, dist in zip(svmesh_to_bvh_lists(verts, faces, safe_check), points, distance): + + res_local = [[] for i in range(4)] + + for pt, d in zip(pts, cycle(dist)): + res = bvh.find_nearest_range(pt, d) + #claning results: + res = sorted(res, key=take_third) + unique = [] + if flat_output: + for r in res: + if not r[2] in unique: + unique.append(r[2]) + append_multiple(res_local, translate_data(r)) + + else: + sub_res_local = [[] for i in range(4)] + for r in res: + if not r[2] in unique: + unique.append(r[2]) + append_multiple(sub_res_local, translate_data(r)) + + append_multiple(res_local, sub_res_local) + + append_multiple(output, res_local) + + return output + + +class SvNearestPointOnMeshNode(bpy.types.Node, SverchCustomTreeNode, SvRecursiveNode): + """ + Triggers: BVH Closest Point + Tooltip: Find nearest point on mesh surfaces + """ + bl_idname = 'SvNearestPointOnMeshNode' + bl_label = 'Nearest Point on Mesh' + bl_icon = 'OUTLINER_OB_EMPTY' + sv_icon = 'SV_POINT_ON_MESH' + + modes = [ + ("find_nearest", "Nearest", "", 0), + ("find_nearest_range", "Nearest in range", "", 1), + ] + + def update_sockets(self, context): + self.inputs['Distance'].hide_safe = self.mode == 'find_nearest' + updateNode(self, context) + + mode: EnumProperty( + name="Mode", items=modes, + default='find_nearest', + update=update_sockets) + + safe_check: BoolProperty( + name='Safe Check', + description='When disabled polygon indices refering to unexisting points will crash Blender but makes node faster', + default=True) + + flat_output: BoolProperty( + name='Flat Output', + description='Ouput a single list for every list in stead of a list of lists', + default=True, + update=updateNode) + + def draw_buttons(self, context, layout): + layout.prop(self, 'mode') + if self.mode == 'find_nearest_range': + layout.prop(self, 'flat_output') + + def draw_buttons_ext(self, context, layout): + layout.prop(self, 'list_match') + layout.prop(self, 'mode') + layout.prop(self, 'safe_check') + + def sv_init(self, context): + si = self.inputs.new + so = self.outputs.new + si('SvVerticesSocket', 'Verts') + si('SvStringsSocket', 'Faces').nesting_level = 3 + for s in self.inputs[:2]: + s.is_mandatory = True + si('SvVerticesSocket', 'Points').use_prop = True + d = si('SvStringsSocket', 'Distance') + d.use_prop = True + d.default_property = 10.0 + d.hide_safe = True + + so('SvVerticesSocket', 'Location') + so('SvVerticesSocket', 'Normal') + so('SvStringsSocket', 'Index') + so('SvStringsSocket', 'Distance') + + + def process_data(self, params): + verts, faces, points, distance = params + if self.mode == 'find_nearest': + return nearest_point_in_mesh(verts, faces, points, + safe_check=self.safe_check) + else: + return nearest_in_range(verts, faces, points, distance, + safe_check=self.safe_check, + flat_output=self.flat_output) + +def register(): + bpy.utils.register_class(SvNearestPointOnMeshNode) + + +def unregister(): + bpy.utils.unregister_class(SvNearestPointOnMeshNode) diff --git a/nodes/analyzer/points_inside_mesh.py b/nodes/analyzer/points_inside_mesh.py index 38f212276f..b84652f8d7 100644 --- a/nodes/analyzer/points_inside_mesh.py +++ b/nodes/analyzer/points_inside_mesh.py @@ -37,6 +37,7 @@ def generate_random_unitvectors(): seed_set(140230) return [random_unit_vector() for i in range(6)] + directions = generate_random_unitvectors() @@ -118,6 +119,7 @@ def get_points_in_mesh_2D(verts, faces, points, normal, eps=0.0): mask_totals.append(inside) return mask_totals + def get_points_in_mesh_2D_clip(verts, faces, points, normal, clip_distance, eps=0.0, matchig_method='REPEAT'): mask_totals = [] bvh = BVHTree.FromPolygons(verts, faces, all_triangles=False, epsilon=eps) @@ -138,11 +140,11 @@ def get_points_in_mesh_2D_clip(verts, faces, points, normal, clip_distance, eps= mask_totals.append(inside) return mask_totals + class SvPointInside(bpy.types.Node, SverchCustomTreeNode): """ Triggers: Mask verts with geom Tooltip: Mask points inside geometry in 2D or 3D - """ bl_idname = 'SvPointInside' bl_label = 'Points Inside Mesh' @@ -159,7 +161,7 @@ def update_sockets(self, context): self.inputs.remove(self.inputs['Plane Normal']) if self.dimensions_mode == '2D' and self.limit_max_dist and len(self.inputs) < 5: self.inputs.new('SvStringsSocket', 'Max Dist').prop_name = 'max_dist' - elif self.dimensions_mode == '3D' or not self.limit_max_dist: + elif self.dimensions_mode == '3D' or not self.limit_max_dist: if 'Max Dist' in self.inputs: self.inputs.remove(self.inputs['Max Dist']) @@ -172,9 +174,11 @@ def update_sockets(self, context): name='Normal', description='Plane Normal', size=3, default=(0, 0, 1), update=updateNode) + max_dist: FloatProperty( name='Max Distance', description='Maximum valid distance', default=10.0, update=updateNode) + limit_max_dist: BoolProperty( name='Limit Proyection', description='Limit projection distance', default=False, update=update_sockets) @@ -208,11 +212,15 @@ def update_sockets(self, context): update=updateNode) def sv_init(self, context): + self.width = 160 self.inputs.new('SvVerticesSocket', 'verts') self.inputs.new('SvStringsSocket', 'faces') self.inputs.new('SvVerticesSocket', 'points') self.outputs.new('SvStringsSocket', 'mask') - self.outputs.new('SvVerticesSocket', 'verts') + # self.outputs.new('SvVerticesSocket', 'Inside Vertices') # to be used in MK2 + s = self.outputs.new('SvVerticesSocket', 'verts') # to be removed in MK2 + s.label = "Inside Vertices" # to be removed in MK2 + self.outputs.new('SvVerticesSocket', 'Outside Vertices') self.update_sockets(context) def draw_buttons(self, context, layout): @@ -251,6 +259,7 @@ def get_data(self): # general options params.append(cycle([self.epsilon_bvh])) # special options and main_func + if self.dimensions_mode == '3D': if self.selected_algo == 'algo_1': main_func = are_inside @@ -280,10 +289,19 @@ def process(self): self.outputs['mask'].sv_set(mask) if self.outputs['verts'].is_linked: - out_verts = [] + # if self.outputs['Inside Vertices'].is_linked: # to be used in MK2 + verts = [] for masked, pts_in in zip(mask, params[2]): - out_verts.append([p for m, p in zip(masked, pts_in) if m]) - self.outputs['verts'].sv_set(out_verts) + verts.append([p for m, p in zip(masked, pts_in) if m]) + self.outputs['verts'].sv_set(verts) # to be removed in MK2 + # self.outputs['Inside Vertices'].sv_set(verts) # to be used in MK2 + + if 'Outside Vertices' in self.outputs: # to be removed in MK2 + if self.outputs['Outside Vertices'].is_linked: + verts = [] + for masked, pts_in in zip(mask, params[2]): + verts.append([p for m, p in zip(masked, pts_in) if not m]) + self.outputs['Outside Vertices'].sv_set(verts) def register(): diff --git a/nodes/generator/icosphere.py b/nodes/generator/icosphere.py index 8306762619..2ed6afd1c3 100644 --- a/nodes/generator/icosphere.py +++ b/nodes/generator/icosphere.py @@ -18,14 +18,16 @@ from math import pi, sqrt import bpy +from bpy.props import IntProperty, FloatProperty, BoolVectorProperty import bmesh -from bpy.props import IntProperty, FloatProperty, EnumProperty from mathutils import Matrix, Vector from sverchok.node_tree import SverchCustomTreeNode -from sverchok.data_structure import updateNode, list_match_modes, list_match_func -from sverchok.utils.sv_bmesh_utils import bmesh_from_pydata, pydata_from_bmesh +from sverchok.data_structure import updateNode +from sverchok.utils.sv_bmesh_utils import numpy_data_from_bmesh from sverchok.utils.math import from_cylindrical +from sverchok.utils.nodes_mixins.recursive_nodes import SvRecursiveNode + def icosahedron_cylindrical(r): @@ -78,7 +80,7 @@ def icosahedron(r): vertices = [from_cylindrical(rho, phi, z, 'radians') for rho, phi, z in vertices] return vertices, edges, faces -class SvIcosphereNode(bpy.types.Node, SverchCustomTreeNode): +class SvIcosphereNode(bpy.types.Node, SverchCustomTreeNode, SvRecursiveNode): "IcoSphere primitive" bl_idname = 'SvIcosphereNode' @@ -113,13 +115,18 @@ def get_subdivisions(self): name = "Radius", default=1.0, min=0.0, update=updateNode) - - list_match: EnumProperty( - name="List Match", - description="Behavior on different list lengths, object level", - items=list_match_modes, default="REPEAT", - update=updateNode) - + + # list_match: EnumProperty( + # name="List Match", + # description="Behavior on different list lengths, object level", + # items=list_match_modes, default="REPEAT", + # update=updateNode) + out_np: BoolVectorProperty( + name="Ouput Numpy", + description="Output NumPy arrays slows this node but may improve performance of nodes it is connected to", + default=(False, False, False), + size=3, update=updateNode) + def sv_init(self, context): self['subdivisions'] = 2 @@ -133,22 +140,23 @@ def sv_init(self, context): def draw_buttons_ext(self, context, layout): layout.prop(self, "subdivisions_max") layout.prop(self, "list_match") + layout.label(text="Ouput Numpy:") + r = layout.row(align=True) + for i in range(3): + r.prop(self, "out_np", index=i, text=self.outputs[i].name, toggle=True) - def process(self): - # return if no outputs are connected - if not any(s.is_linked for s in self.outputs): - return - - subdivisions_s = self.inputs['Subdivisions'].sv_get()[0] - radius_s = self.inputs['Radius'].sv_get()[0] + def pre_setup(self): + for s in self.inputs: + s.nesting_level = 1 + s.pre_processing = 'ONE_ITEM' + def process_data(self, params): out_verts = [] out_edges = [] out_faces = [] - objects = list_match_func[self.list_match]([subdivisions_s, radius_s]) - for subdivisions, radius in zip(*objects): + for subdivisions, radius in zip(*params): if subdivisions == 0: # In this case we just return the icosahedron verts, edges, faces = icosahedron(radius) @@ -161,19 +169,20 @@ def process(self): subdivisions = self.subdivisions_max bm = bmesh.new() - bmesh.ops.create_icosphere(bm, - subdivisions = subdivisions, - diameter = radius) - verts, edges, faces = pydata_from_bmesh(bm) + bmesh.ops.create_icosphere( + bm, + subdivisions=subdivisions, + diameter=radius) + + verts, edges, faces, _ = numpy_data_from_bmesh(bm, self.out_np) bm.free() out_verts.append(verts) out_edges.append(edges) out_faces.append(faces) - self.outputs['Vertices'].sv_set(out_verts) - self.outputs['Edges'].sv_set(out_edges) - self.outputs['Faces'].sv_set(out_faces) + return out_verts, out_edges, out_faces + def register(): bpy.utils.register_class(SvIcosphereNode) diff --git a/nodes/generator/line_mk4.py b/nodes/generator/line_mk4.py index c50d79b9bc..83ad086db2 100644 --- a/nodes/generator/line_mk4.py +++ b/nodes/generator/line_mk4.py @@ -24,7 +24,7 @@ from bpy.props import IntProperty, FloatProperty, BoolProperty, EnumProperty, FloatVectorProperty from sverchok.node_tree import SverchCustomTreeNode -from sverchok.data_structure import updateNode, numpy_list_match_modes, iter_list_match_func +from sverchok.data_structure import updateNode, numpy_list_match_modes, iter_list_match_func, list_match_func Directions = namedtuple('Directions', ['x', 'y', 'z', 'op', 'od']) @@ -57,16 +57,18 @@ def make_line(numbers=None, steps=None, sizes=None, verts_or=None, verts_dir=Non :return: np.array of vertices, np.array of edges """ line_number = max(len(numbers), len(sizes), len(steps), len(verts_or), len(verts_dir)) - list_match_f = iter_list_match_func[list_match_mode] + list_match_f = list_match_func[list_match_mode] params = list_match_f([numbers, steps, sizes, verts_or, verts_dir]) + vert_number = sum([v_number if v_number > 1 else 2 for _, v_number in zip(range(line_number), params[0])]) + verts_lines = np.empty((vert_number, 3)) edges_lines = [] num_added_verts = 0 indexes = iter(range(int(1e+100))) - for i_line, n, st, size, vor, vdir in zip(range(line_number), *params): + for n, st, size, vor, vdir in zip(*params): vor, vdir = get_corner_points(dir_mode, center, vor, vdir, get_len_line(size_mode, n, size, st)) line_verts = generate_verts(vor, vdir, n) edges_lines.extend([(i, i + 1) for i, _ in zip(indexes, line_verts[:-1])]) diff --git a/nodes/list_main/statistics.py b/nodes/list_main/statistics.py index 821d08e822..7056459f9d 100644 --- a/nodes/list_main/statistics.py +++ b/nodes/list_main/statistics.py @@ -17,38 +17,126 @@ # ##### END GPL LICENSE BLOCK ##### import bpy -from bpy.props import EnumProperty, IntProperty, FloatProperty, BoolProperty +import blf +import os + +from bpy.props import (BoolProperty, + BoolVectorProperty, + EnumProperty, + FloatProperty, + FloatVectorProperty, + IntProperty, + StringProperty) from sverchok.node_tree import SverchCustomTreeNode -from sverchok.data_structure import updateNode, match_long_repeat +from sverchok.data_structure import node_id, 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) +from sverchok.utils.sv_path_utils import get_fonts_path +from sverchok.settings import get_dpi_factor +from sverchok.ui import bgl_callback_nodeview as nvBGL +from mathutils import Vector +from math import ceil +from pprint import pprint + +selectors = { # Selectors of Statistical Functions + "ALL_STATISTICS": (0, "ALL", 0), # select ALL statistical quantities + "SELECTED_STATISTICS": (1, "SEL", 0) # select SOME statistical quantities } +functions = { # Statistical Functions > name : (index, abreviation, function) + "SUM": (10, "SUM", get_sum), + "SUM_OF_SQUARES": (11, "SOS", get_sum_of_squares), + "SUM_OF_INVERSIONS": (12, "SOI", get_sum_of_inversions), + "PRODUCT": (13, "PRD", get_product), + "AVERAGE": (14, "AVG", get_average), + "GEOMETRIC_MEAN": (15, "GEM", get_geometric_mean), + "HARMONIC_MEAN": (16, "HAM", get_harmonic_mean), + "VARIANCE": (17, "VAR", get_variance), + "STANDARD_DEVIATION": (18, "STD", get_standard_deviation), + "STANDARD_ERROR": (19, "STE", get_standard_error), + "ROOT_MEAN_SQUARE": (20, "RMS", get_root_mean_square), + "SKEWNESS": (21, "SKW", get_skewness), + "KURTOSIS": (22, "KUR", get_kurtosis), + "MINIMUM": (23, "MIN", get_minimum), + "MAXIMUM": (24, "MAX", get_maximum), + "RANGE": (25, "RNG", get_range), + "MEDIAN": (26, "MED", get_median), + "PERCENTILE": (27, "PER", get_percentile), + "HISTOGRAM": (28, "HIS", get_histogram), + "COUNT": (29, "CNT", get_count) +} -modeItems = [ +mode_items = [ ("INT", "Int", "", "", 0), ("FLOAT", "Float", "", "", 1)] -functionItems = [(k, k.replace("_", " ").title(), "", "", s[0]) for k, s in sorted(functions.items(), key=lambda k: k[1][0])] +function_items = [(k, k.replace("_", " ").title(), "", "", s[0]) for k, s in sorted({**selectors, **functions}.items(), key=lambda k: k[1][0])] + +# cache these for faster abreviation->index and index->abreviation access +# aTi: SUM->0 , SOS->1 ... CNT->19 and iTa: 0->SUM , 1->SOS ... 19->CNT +aTi = {v[1]: i for i, v in enumerate(sorted(functions.values(), key=lambda v: v[0]))} +iTa = {i: v[1] for i, v in enumerate(sorted(functions.values(), key=lambda v: v[0]))} +abreviations = [(i, name) for i, name in sorted(iTa.items(), key=lambda i: i)] + +loaded_fonts = {} + +font_names = { + "OCR-A": "OCR-A.ttf", + "LARABIE": "larabiefont rg.ttf", + "SHARE_TECH_MONO": "ShareTechMono-Regular.ttf", + "DATA_LATIN": "DataLatin.ttf", + "DIGITAL_7": "digital-7 (mono).ttf", + "DROID_SANS_MONO": "DroidSansMono.ttf", + "ENVY_CODE": "Envy Code R.ttf", + "FIRA_CODE": "FiraCode-VF.ttf", + "HACK": "Hack-Regular.ttf", + "MONOF55": "monof55.ttf", + "MONOID_RETINA": "Monoid-Retina.ttf", + "SAX_MONO": "SaxMono.ttf", +} + +font_items = [(k, k.replace("_", " ").title(), "", "", i) for i, k in enumerate(font_names.keys())] + + +def load_fonts(): + ''' Load fonts ''' + loaded_fonts["main"] = {} + + fonts_path = get_fonts_path() + + # load fonts and create dictionary of Font Name : Font ID + for font_name, font_filename in font_names.items(): + font_filepath = os.path.join(fonts_path, font_filename) + font_id = blf.load(font_filepath) + if font_id >= 0: + loaded_fonts["main"][font_name] = font_id + else: # font not found or could not load? => use blender default font + loaded_fonts["main"][font_name] = 0 + + +def get_text_drawing_location(node): + ''' Get the text drawing location relative to the node location ''' + location = Vector((node.absolute_location)) + location = location + Vector((node.width + 20, -20)) # shift right of the node + location = location + Vector((10*node.text_offset_x, # apply user offset + 10*node.text_offset_y)) + + location = location * get_dpi_factor() # dpi scale adjustment + + return list(location) + + +class SvStatisticsCallback(bpy.types.Operator): + bl_label = "Statistics Callback" + bl_idname = "sv.statistics_callback" + bl_description = "Callback wrapper class used for operator callbacks" + + function_name: StringProperty() # name of the function to call + + def execute(self, context): + n = context.node + getattr(n, self.function_name)(context) + return {"FINISHED"} class SvListStatisticsNode(bpy.types.Node, SverchCustomTreeNode): @@ -58,16 +146,26 @@ class SvListStatisticsNode(bpy.types.Node, SverchCustomTreeNode): ''' bl_idname = 'SvListStatisticsNode' bl_label = 'List Statistics' - bl_icon = 'OUTLINER_OB_EMPTY' + bl_icon = 'SEQ_HISTOGRAM' sv_icon = 'SV_LIST_STADISTICS' def update_function(self, context): - if self.function == "ALL STATISTICS": + ''' Update sockets and such when the function selection changes ''' + 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" + + elif self.function == "SELECTED_STATISTICS": + for name in ["Percentage", "Bins", "Size"]: + self.inputs[name].hide_safe = True + if self["selected_quantities"][aTi["PER"]]: # PERCENTILE + self.inputs["Percentage"].hide_safe = False + if self["selected_quantities"][aTi["HIS"]]: # HISTOGRAM + self.inputs["Bins"].hide_safe = False + self.inputs["Size"].hide_safe = not self.normalize else: for name in ["Percentage", "Bins", "Size"]: self.inputs[name].hide_safe = True @@ -80,44 +178,146 @@ def update_function(self, context): self.outputs[0].name = "Name" self.outputs[1].name = "Value" + self.label = self.function.replace("_", " ").title() updateNode(self, context) def update_normalize(self, context): - socket = self.inputs["Size"] - socket.hide_safe = not self.normalize + self.inputs["Size"].hide_safe = not self.normalize + updateNode(self, context) + def update_show_statistics(self, context): + nvBGL.callback_disable(node_id(self)) # clear draw updateNode(self, context) - mode : EnumProperty( - name="Mode", items=modeItems, default="FLOAT", update=updateNode) + show_statistics: BoolProperty( + name="Show Statistics", default=False, update=update_show_statistics) + + text_color: FloatVectorProperty( + name="Text Color", description='Text color', subtype='COLOR', + size=3, min=0.0, max=1.0, default=(0.5, 0.3, 1.0), update=updateNode) + + text_scale: FloatProperty( + name="Text Scale", description='Scale the text size', + default=1.0, min=0.1, update=updateNode) + + text_offset_x: FloatProperty( + name="Offset X", description='Offset text along x (10x pixel units)', + default=0.0, update=updateNode) + + text_offset_y: FloatProperty( + name="Offset Y", description='Offset text along y (10x pixel units)', + default=0.0, update=updateNode) + + selected_font: EnumProperty( + name="Font", items=font_items, default="HACK", update=updateNode) + + precision: IntProperty( + name="Precision", + description="Floating point precision of the displayed statistics values", + min=0, default=2, update=updateNode) - function : EnumProperty( - name="Function", items=functionItems, update=update_function) + mode: EnumProperty( + name="Mode", items=mode_items, default="FLOAT", update=updateNode) - percentage : FloatProperty( + function: EnumProperty( + name="Function", description="Selected statistical function(s)", + items=function_items, update=update_function) + + percentage: FloatProperty( name="Percentage", + description="Percentage value for the percentile statistics", default=0.75, min=0.0, max=1.0, update=updateNode) - bins : IntProperty( - name="Bins", + bins: IntProperty( + name="Bins", description="Number of bins in the histogram", default=10, min=1, update=updateNode) - normalize : BoolProperty( + normalize: BoolProperty( name="Normalize", description="Normalize the bins to a normalize size", default=False, update=update_normalize) - normalized_size : FloatProperty( + normalized_size: FloatProperty( name="Size", description="The normalized size of the bins", default=10.0, update=updateNode) + abreviate_names: BoolProperty( + name="Abreviate Names", description="Abreviate the statistics quantity names", + default=False, update=updateNode) + + def toggle_all(self, context): + self["selected_quantities"] = [self.toggled] * len(functions) + self.toggled = not self.toggled + + toggled: BoolProperty( + name="Toggled", description="Statistics toggled", + default=False, update=update_function) + + def get_array(self): + return self["selected_quantities"] + + def set_array(self, values): + self["selected_quantities"] = values + + selected_quantities: BoolVectorProperty( + name="Selected Quantities", description="Toggle statistic quantities on/off", + size=len(functions), get=get_array, set=set_array, update=update_function) + + quantities_expanded: BoolProperty( + name="Expand Quantities", description="Expand the list of statistical quantities", + default=True) + def draw_buttons(self, context, layout): layout.prop(self, "mode", expand=True) - layout.prop(self, "function", text="") - if self.function in ["HISTOGRAM", "ALL STATISTICS"]: + + col = layout.column(align=True) + row = col.row(align=True) + row.prop(self, "function", text="") + row.prop(self, 'show_statistics', text='', icon='ALIGN_FLUSH') + + if self.function == "SELECTED_STATISTICS": + box = col.box() + split = box.split(factor=0.8, align=True) + + c1 = split.column(align=True) + toggle_button = c1.operator(SvStatisticsCallback.bl_idname, text="Toggle All") + toggle_button.function_name = "toggle_all" + + c2 = split.column(align=True) + if self.quantities_expanded: + c2.prop(self, "quantities_expanded", icon='TRIA_UP', text='') + # draw the toggle buttons for the selected quantities + N = int(ceil(len(abreviations)/4)) # break list into 4 columns + col = box.column(align=True) + split = col.split(factor=1/4, align=True) + for i, name in abreviations: + if i % N == 0: + col = split.column(align=True) + col.prop(self, "selected_quantities", toggle=True, index=i, text=name) + else: + c2.prop(self, "quantities_expanded", icon='TRIA_DOWN', text="") + # histogram selected? => show the normalize button + if self.function in ["HISTOGRAM", "ALL_STATISTICS"] or \ + self.function == "SELECTED_STATISTICS" and self["selected_quantities"][aTi["HIS"]]: layout.prop(self, "normalize", toggle=True) + def draw_buttons_ext(self, context, layout): + box = layout.box() + # font/text settings + box.label(text="Font & Text Settings") + box.prop(self, "selected_font", text="Font") + box.prop(self, "text_color") + box.prop(self, "text_scale") + box.prop(self, "abreviate_names") + col = box.column(align=True) + col.prop(self, "text_offset_x") + col.prop(self, "text_offset_y") + layout.prop(self, "precision") + + def selected_font_id(self): + return loaded_fonts["main"][self.selected_font] + def sv_init(self, context): - self.width = 150 + self.width = 160 self.inputs.new('SvStringsSocket', "Data") self.inputs.new('SvStringsSocket', "Percentage").prop_name = "percentage" self.inputs.new('SvStringsSocket', "Bins").prop_name = "bins" @@ -126,17 +326,127 @@ def sv_init(self, context): self.outputs.new('SvStringsSocket', "Values") self.function = "AVERAGE" + self["selected_quantities"] = [True] * len(functions) + def get_statistics_function(self): - return functions[self.function][1] + return functions[self.function][2] # (0=index, 1=abreviation, 2=function) + + def draw_statistics(self, names, values): + """ Draw the statistics in the node editor + + The statistics data can be either single or vectorized. + * single: + [ [sum], [avg], ... [[b1, b2, .. bN]] ] + * vectorized: + [ [sum1... sumM], [avg1... avgM], ... [[b11... b1N]... [bM1... bMN]] ] + + Q: how can we tell statistics are simple or vectorized? + A: if the values[0] is a list with length > 1 then it's vectorized + """ + nvBGL.callback_disable(node_id(self)) # clear drawing + + if len(values) == 0: + return + + if self.show_statistics: + max_width = max(len(name) for name in names) # used for text alignment + + info = [] + + # for now let's treat single and vectorized separately (optimize later) + is_vectorized = len(values[0]) > 1 + + if is_vectorized: + for n, v in zip(names, values): + if n in ["Histogram", "HIS"]: + line_format = "{0:>{x}} : [{1}]" + histogram_lines = [] + for a in v: # for each histogram set + if self.mode == "FLOAT": + value_format = "{:.{p}f}" + else: + value_format = "{}" + histogram_values = ", ".join([value_format.format(aa, p=self.precision) for aa in a]) + histogram_lines.append("[{}]".format(histogram_values)) + line = line_format.format(n, ", ".join(histogram_lines), x=max_width) + info.append(line) + + else: + line_format = "{0:>{x}} : [{1}]" + if n in ["Count", "CNT"]: + value_format = "{}" + else: + if self.mode == "FLOAT": + value_format = "{:.{p}f}" + else: + value_format = "{}" + + value_line = ", ".join([value_format.format(vv, p=self.precision) for vv in v]) + line = line_format.format(n, value_line, x=max_width) + + info.append(line) + else: # single values + for n, v in zip(names, values): + if n in ["Histogram", "HIS"]: + # print("drawing histogram") + line_format = "{0:>{x}} : [{1}]" + if self.normalize: + value_format = "{:.{p}f}" + else: + value_format = "{}" + histogram_values = ", ".join([value_format.format(a, p=self.precision) for a in v[0]]) + line = line_format.format(n, histogram_values, x=max_width) + info.append(line) + else: + if n in ["Count", "CNT"]: + line_format = "{0:>{x}} : {1}" + else: + if self.mode == "FLOAT": + line_format = "{0:>{x}} : {1:.{p}f}" + else: + line_format = "{0:>{x}} : {1}" + + line = line_format.format(n, v[0], x=max_width, p=self.precision) + + info.append(line) + + draw_data = { + 'tree_name': self.id_data.name[:], + 'node_name': self.name[:], + 'content': info, + 'location': get_text_drawing_location, + 'color': self.text_color[:], + 'scale': self.text_scale * get_dpi_factor(), + 'font_id': int(self.selected_font_id()) + } + + nvBGL.callback_enable(node_id(self), draw_data) + + def sv_free(self): + nvBGL.callback_disable(node_id(self)) def process(self): + inputs = self.inputs outputs = self.outputs - # return if no outputs are connected - if not any(s.is_linked for s in outputs): + + # no inputs are connected ? => return + if not any(s.is_linked for s in inputs): + nvBGL.callback_disable(node_id(self)) # clear drawing return - inputs = self.inputs - input_D = inputs["Data"].sv_get() + # no outputs are connected or statistics are not shown? => return + if not self.show_statistics: + if not any(s.is_linked for s in outputs): + nvBGL.callback_disable(node_id(self)) # clear drawing + return + + input_D = inputs["Data"].sv_get(default=[[]]) + + if len(input_D) == 0 or any([len(d) == 0 for d in input_D]): + outputs[0].sv_set([[]]) + outputs[1].sv_set([[]]) + raise Exception("Input data contains empty lists") + input_P = inputs["Percentage"].sv_get()[0] input_B = inputs["Bins"].sv_get()[0] input_S = inputs["Size"].sv_get()[0] @@ -148,42 +458,66 @@ def process(self): if self.mode == "INT": input_P = list(map(lambda x: int(x), input_P)) + # determine the list of functions to generate statistics for if self.function == "ALL_STATISTICS": - functionNames = [fn[0] for fn in functionItems[1:]] - else: - functionNames = [self.function] + function_names = list(functions.keys()) + + elif self.function == "SELECTED_STATISTICS": + function_names = [] + for i, f in enumerate(functions.keys()): + if self["selected_quantities"][i]: + function_names.append(f) + + else: # one statistical quantity + function_names = [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 = [] + all_names = [] + all_values = [] + for function_name in function_names: + statistics_function = functions[function_name][2] # (0=index, 1=abreviation, 2=function) + quantity_list = [] for d, p, b, s in zip(*params): - if functionName == "PERCENTILE": + if function_name == "PERCENTILE": quantity = statistics_function(d, p) - elif functionName == "HISTOGRAM": + elif function_name == "HISTOGRAM": quantity = statistics_function(d, b, self.normalize, s) else: quantity = statistics_function(d) - if functionName != "HISTOGRAM": + if function_name != "HISTOGRAM": if self.mode == "INT": quantity = int(quantity) - quantityList.append(quantity) + quantity_list.append(quantity) + + if self.abreviate_names: + name = functions[function_name][1] # (0=index, 1=abreviation, 2=function) + else: + name = function_name.replace("_", " ").title() - allNames.append(functionName) - allValues.append(quantityList) + all_names.append(name) + all_values.append(quantity_list) - outputs[0].sv_set(allNames) - outputs[1].sv_set(allValues) + if outputs[0].is_linked: + outputs[0].sv_set(all_names) + if outputs[1].is_linked: + outputs[1].sv_set(all_values) + + self.draw_statistics(all_names, all_values) def register(): + bpy.utils.register_class(SvStatisticsCallback) bpy.utils.register_class(SvListStatisticsNode) + load_fonts() def unregister(): bpy.utils.unregister_class(SvListStatisticsNode) + bpy.utils.unregister_class(SvStatisticsCallback) + + +if __name__ == '__main__': + register() diff --git a/nodes/list_masks/index_to_mask.py b/nodes/list_masks/index_to_mask.py index 977a729680..9f7371af9b 100644 --- a/nodes/list_masks/index_to_mask.py +++ b/nodes/list_masks/index_to_mask.py @@ -32,12 +32,20 @@ name="Data masking", description="Use data to define mask length", default=False)) + node.props.is_topo_mask = NodeProperties( bpy_props=BoolProperty( name="Topo mask", description="data consists of verts or polygons / edges. " "Otherwise the two vertices will be masked as [[[T, T, T], [F, F, F]]] instead of [[T, F]]", default=False)) + +node.props.output_numpy = NodeProperties( + bpy_props=BoolProperty( + name="Output NumPy", + description="Output Numpy arrays in stead of regular python lists", + default=False)) + node.props.index = NodeProperties(bpy_props=IntProperty(name="Index")) node.props.mask_size = NodeProperties(bpy_props=IntProperty(name='Mask Length', default=10, min=2)) @@ -76,6 +84,13 @@ def draw_buttons(self, context, layout): if self.data_to_mask: col.prop(self, "is_topo_mask", toggle=True) + def draw_buttons_ext(self, context, layout): + self.draw_buttons(context, layout) + layout.prop(self, 'output_numpy') + + def rclick_menu(self, context, layout): + layout.prop(self, 'output_numpy') + def process(self): if not node.props.data_to_mask: mask = np.zeros(node.inputs.mask_size[0], dtype=bool) @@ -87,4 +102,7 @@ def process(self): mask = np.zeros_like(node.inputs.data_to_mask, dtype=bool) mask[node.inputs.index] = True - node.outputs.mask = mask.tolist() + if node.props.output_numpy: + node.outputs.mask = mask + else: + node.outputs.mask = mask.tolist() diff --git a/nodes/list_mutators/unique_items.py b/nodes/list_mutators/unique_items.py index b76bee29e9..814123d896 100644 --- a/nodes/list_mutators/unique_items.py +++ b/nodes/list_mutators/unique_items.py @@ -18,6 +18,7 @@ import bpy import numpy as np +from mathutils import Matrix, Quaternion from sverchok.node_tree import SverchCustomTreeNode from sverchok.data_structure import updateNode, changable_sockets @@ -31,7 +32,7 @@ def recursive_unique_items(data, level, linked_outputs, output_numpy): iterable = isinstance(data[0], (list, tuple, np.ndarray)) if not level or not iterable: np_data = np.array(data) - if np_data.dtype == object: + if np_data.dtype == object or isinstance(data[0], (Matrix, Quaternion)): unique, unique_indices, unique_inverse, unique_count = python_unique(data) else: unique, unique_indices, unique_inverse, unique_count = numpy_unique(data, linked_outputs, output_numpy) diff --git a/nodes/matrix/matrix_out_mk2.py b/nodes/matrix/matrix_out_mk2.py new file mode 100644 index 0000000000..43f81370a1 --- /dev/null +++ b/nodes/matrix/matrix_out_mk2.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 EnumProperty +from sverchok.node_tree import SverchCustomTreeNode +from sverchok.data_structure import updateNode +from sverchok.utils.sv_transform_helper import AngleUnits, SvAngleHelper +from sverchok.utils.nodes_mixins.recursive_nodes import SvRecursiveNode +from mathutils import Matrix + + +mode_items = [ + ("QUATERNION", "Quaternion", "Convert rotation component of the matrix into Quaternion", 0), + ("EULER", "Euler Angles", "Convert rotation component of the matrix into Euler angles", 1), + ("AXISANGLE", "Axis Angle", "Convert rotation component of the matrix into Axis & Angle", 2), +] + +output_sockets = { + "QUATERNION": ["Quaternion"], + "EULER": ["Angle X", "Angle Y", "Angle Z"], + "AXISANGLE": ["Angle", "Axis"], +} + + +class SvMatrixOutNodeMK2(bpy.types.Node, SverchCustomTreeNode, SvAngleHelper, SvRecursiveNode): + """ + Triggers: Matrix, Out + Tooltip: Convert a matrix into its location, scale & rotation components + """ + bl_idname = 'SvMatrixOutNodeMK2' + bl_label = 'Matrix Out' + bl_icon = 'OUTLINER_OB_EMPTY' + sv_icon = 'SV_MATRIX_OUT' + + flat_output: bpy.props.BoolProperty( + name="Flat Quaternions output", + description="Flatten Quaternions output by list-joining level 1", + default=True, update=updateNode) + + def migrate_from(self, old_node): + ''' Migration from old nodes (attributes mapping) ''' + if old_node.bl_idname == "MatrixOutNode": + self.angle_units = AngleUnits.DEGREES + self.last_angle_units = AngleUnits.DEGREES + + def migrate_props_pre_relink(self, old_node): + self.update_sockets() + + def rclick_menu(self, context, layout): + layout.prop(self, "flat_output", text="Flat Output", expand=False) + self.node_replacement_menu(context, layout) + + def update_sockets(self): + # hide all the mode related output sockets + for k, names in output_sockets.items(): + for name in names: + self.outputs[name].hide_safe = True + + # show the output sockets specific to the current mode + for name in output_sockets[self.mode]: + self.outputs[name].hide_safe = False + + def update_mode(self, context): + self.update_sockets() + updateNode(self, context) + + mode : EnumProperty( + name='Mode', description='The output component format of the Matrix', + items=mode_items, default="AXISANGLE", update=update_mode) + + def sv_init(self, context): + self.sv_new_input('SvMatrixSocket', "Matrix", is_mandatory=True, nesting_level=2) + # translation and scale outputs + self.outputs.new('SvVerticesSocket', "Location") + self.outputs.new('SvVerticesSocket', "Scale") + # quaternion output + self.outputs.new('SvQuaternionSocket', "Quaternion") + # euler angles ouputs + self.outputs.new('SvStringsSocket', "Angle X") + self.outputs.new('SvStringsSocket', "Angle Y") + self.outputs.new('SvStringsSocket', "Angle Z") + # axis-angle output + self.outputs.new('SvVerticesSocket', "Axis") + self.outputs.new('SvStringsSocket', "Angle") + + self.update_mode(context) + + def draw_buttons(self, context, layout): + layout.prop(self, "mode", expand=False, text="") + + if self.mode == "EULER": + self.draw_angle_euler_buttons(context, layout) + + def draw_buttons_ext(self, context, layout): + if self.mode in {"EULER", "AXISANGLE"}: + self.draw_angle_units_buttons(context, layout) + elif self.mode == 'QUATERNION': + layout.prop(self, 'flat_output') + + def process_data(self, params): + input_M = params[0] + outputs = self.outputs + # decompose matrices into: Translation, Rotation (quaternion) and Scale + result = [] + for mat_list in input_M: + location_list = [] + quaternion_list = [] # rotations (as quaternions) + scale_list = [] + angles = [[], [], []] + axis_list, angle_list = [], [] + for m in mat_list: + T, R, S = m.decompose() + location_list.append(list(T)) + quaternion_list.append(R) + scale_list.append(list(S)) + + if self.mode == "EULER": + # conversion factor from radians to the current angle units + au = self.angle_conversion_factor(AngleUnits.RADIANS, self.angle_units) + + for i, name in enumerate("XYZ"): + if outputs["Angle " + name].is_linked: + angles[i] = [q.to_euler(self.euler_order)[i] * au for q in quaternion_list] + elif self.mode == "AXISANGLE": + if outputs['Axis'].is_linked: + axis_list = [tuple(q.axis) for q in quaternion_list] + + if outputs['Angle'].is_linked: + # conversion factor from radians to the current angle units + au = self.angle_conversion_factor(AngleUnits.RADIANS, self.angle_units) + angle_list = [q.angle * au for q in quaternion_list] + + result.append([location_list, scale_list, quaternion_list, *angles, axis_list, angle_list]) + + if self.mode == 'QUATERNION': + output_data = list(zip(*result)) + if len(output_data[2]) == 1 and self.flat_output: + output_data[2] = output_data[2][0] + return output_data + + return list(zip(*result)) + + + + +def register(): + bpy.utils.register_class(SvMatrixOutNodeMK2) + + +def unregister(): + bpy.utils.unregister_class(SvMatrixOutNodeMK2) diff --git a/nodes/modifier_change/edgenet_to_paths.py b/nodes/modifier_change/edgenet_to_paths.py index d03091d1b4..f6e4c9299c 100644 --- a/nodes/modifier_change/edgenet_to_paths.py +++ b/nodes/modifier_change/edgenet_to_paths.py @@ -196,10 +196,10 @@ def rclick_menu(self, context, layout): self.draw_buttons_ext(context, layout) def sv_init(self, context): self.inputs.new('SvVerticesSocket', 'Vertices') - self.inputs.new('SvStringsSocket', 'Egdes') + self.inputs.new('SvStringsSocket', 'Edges') self.outputs.new('SvVerticesSocket', 'Vertices') - self.outputs.new('SvStringsSocket', 'Egdes') + self.outputs.new('SvStringsSocket', 'Edges') self.outputs.new('SvStringsSocket', 'Vert Indexes') self.outputs.new('SvStringsSocket', 'Edge Indexes') self.outputs.new('SvStringsSocket', 'Cyclic') @@ -208,8 +208,8 @@ def sv_init(self, context): def process(self): if not any(s.is_linked for s in self.outputs): return - verts = self.inputs['Vertices'].sv_get(deepcopy=False) - edges = self.inputs['Egdes'].sv_get(deepcopy=False) + verts = self.inputs[0].sv_get(deepcopy=False) + edges = self.inputs[1].sv_get(deepcopy=False) verts_out = [] edge_out = [] v_index_out = [] @@ -236,8 +236,8 @@ def process(self): new_e_index(ed_index) new_cyclic(cyclic) - self.outputs['Vertices'].sv_set(verts_out) - self.outputs['Egdes'].sv_set(edge_out) + self.outputs[0].sv_set(verts_out) + self.outputs[1].sv_set(edge_out) self.outputs['Vert Indexes'].sv_set(v_index_out) self.outputs['Edge Indexes'].sv_set(e_index_out) self.outputs['Cyclic'].sv_set([cyclic_out] if self.join else cyclic_out) diff --git a/nodes/modifier_change/subdivide_mk2.py b/nodes/modifier_change/subdivide_mk2.py index 3c02ff5bf5..fbb30e2db6 100644 --- a/nodes/modifier_change/subdivide_mk2.py +++ b/nodes/modifier_change/subdivide_mk2.py @@ -303,6 +303,8 @@ def process(self): face_data_matched = numpy_full_list(face_data, len(faces)).tolist() else: face_data_matched = repeat_last_for_length(face_data, len(faces)) + else: + face_data_matched =[] bm = bmesh_from_pydata( vertices, edges, faces, diff --git a/nodes/number/curve_mapper.py b/nodes/number/curve_mapper.py index 5d88e8e7bd..d7ab210955 100644 --- a/nodes/number/curve_mapper.py +++ b/nodes/number/curve_mapper.py @@ -31,7 +31,8 @@ get_valid_node, CURVE_NODE_TYPE, set_rgb_curve, - get_rgb_curve) + get_rgb_curve, + get_points_from_curve) from sverchok.utils.curve import SvScalarFunctionCurve @@ -71,6 +72,7 @@ def sv_init(self, context): self.outputs.new('SvStringsSocket', "Value") self.outputs.new('SvCurveSocket', "Curve") + self.outputs.new('SvVerticesSocket', "Control Points") _ = get_evaluator(node_group_name, self._get_curve_node_name()) @@ -111,6 +113,9 @@ def process(self): curve = SvScalarFunctionCurve(evaluate) curve.u_bounds = (curve_node.mapping.clip_min_x, curve_node.mapping.clip_max_x) self.outputs['Curve'].sv_set([curve]) + if 'Control Points' in self.outputs: + points = get_points_from_curve(node_group_name, curve_node_name) + self.outputs['Control Points'].sv_set(points) # no outputs, end early. if not outputs['Value'].is_linked: @@ -138,8 +143,8 @@ def save_to_json(self, node_data: dict): node_data['curve_data'] = data_json_str def sv_copy(self, other): - ''' - self: is the new node, other: is the old node + ''' + self: is the new node, other: is the old node by the time this function is called the new node has a new empty n_id a new n_id will be generated as a side effect of _get_curve_node_name ''' diff --git a/nodes/object_nodes/getsetprop_mk2.py b/nodes/object_nodes/getsetprop_mk2.py index 20f4e0cb62..12f44d8334 100644 --- a/nodes/object_nodes/getsetprop_mk2.py +++ b/nodes/object_nodes/getsetprop_mk2.py @@ -47,14 +47,22 @@ def parse_to_path(p): ''' if isinstance(p, ast.Attribute): - return parse_to_path(p.value)+[("attr", p.attr)] + return parse_to_path(p.value) + [("attr", p.attr)] + elif isinstance(p, ast.Subscript): + if isinstance(p.slice.value, ast.Num): - return parse_to_path(p.value) + [("key", p.slice.value.n)] + return parse_to_path(p.value) + [("key", p.slice.value.n)] + elif isinstance(p.slice.value, (float, int)): + return parse_to_path(p.value) + [("key", p.slice.value)] elif isinstance(p.slice.value, ast.Str): return parse_to_path(p.value) + [("key", p.slice.value.s)] + elif isinstance(p.slice.value, str): + return parse_to_path(p.value) + [("key", p.slice.value)] + elif isinstance(p, ast.Name): return [("name", p.id)] + else: raise NameError @@ -71,16 +79,25 @@ def get_object(path): curr_object = curr_object[value] return curr_object -def apply_alias(eval_str): +def apply_alias(eval_str, nodetree=None): ''' - apply standard aliases - will raise error if it isn't an bpy path ''' if not eval_str.startswith("bpy."): + + # special case for the nodes alias, end early + if eval_str.startswith("nodes") and nodetree: + string_path_to_current_tree = f'bpy.data.node_groups["{nodetree.name}"].nodes' + eval_str = eval_str.replace("nodes", string_path_to_current_tree, 1) + return eval_str + + # all other aliases for alias, expanded in aliases.items(): if eval_str.startswith(alias): eval_str = eval_str.replace(alias, expanded, 1) break + if not eval_str.startswith("bpy."): raise NameError return eval_str @@ -141,7 +158,9 @@ def assign_data(obj, data): "mats": "bpy.data.materials", "M": "bpy.data.materials", "meshes": "bpy.data.meshes", - "texts": "bpy.data.texts" + "texts": "bpy.data.texts", + "ng": "bpy.data.node_groups" + # "nodes": None , this is directly handled in the apply_alias function } types = { @@ -159,7 +178,7 @@ class SvPropNodeMixin(): @property def obj(self): - eval_str = apply_alias(self.prop_name) + eval_str = apply_alias(self.prop_name, nodetree=self.id_data) ast_path = ast.parse(eval_str) path = parse_to_path(ast_path.body[0].value) return get_object(path) @@ -270,8 +289,11 @@ def draw_buttons(self, context, layout): def process(self): + if len(self.inputs) == 0: + return + data = self.inputs[0].sv_get() - eval_str = apply_alias(self.prop_name) + eval_str = apply_alias(self.prop_name, nodetree=self.id_data) ast_path = ast.parse(eval_str) path = parse_to_path(ast_path.body[0].value) obj = get_object(path) @@ -281,6 +303,7 @@ def process(self): try: if isinstance(obj, (int, float, bpy_prop_array)): + obj = get_object(path[:-1]) p_type, value = path[-1] if p_type == "attr": diff --git a/nodes/object_nodes/set_loop_normals.py b/nodes/object_nodes/set_loop_normals.py new file mode 100644 index 0000000000..5ac9aba45b --- /dev/null +++ b/nodes/object_nodes/set_loop_normals.py @@ -0,0 +1,61 @@ +# This file is part of project Sverchok. It's copyrighted by the contributors +# recorded in the version control history of the file, available from +# its original location https://github.com/nortikin/sverchok/commit/master +# +# SPDX-License-Identifier: GPL3 +# License-Filename: LICENSE + + +import bpy +from mathutils import Vector + +from sverchok.node_tree import SverchCustomTreeNode +from sverchok.data_structure import updateNode, repeat_last + + +class SvSetLoopNormalsNode(SverchCustomTreeNode, bpy.types.Node): + """ + Triggers: set loops normals + + Adding custom normals for input object + Should be used together with Origins node + """ + bl_idname = 'SvSetLoopNormalsNode' + bl_label = 'Set loop normals' + bl_icon = 'NORMALS_VERTEX' + + normalize: bpy.props.BoolProperty(name="Normalize", default=True, description="Normalize input normals", + update=updateNode) + + def draw_buttons(self, context, layout): + layout.prop(self, 'normalize') + + def sv_init(self, context): + self.inputs.new('SvObjectSocket', 'Object') + self.outputs.new('SvObjectSocket', "Object") + self.inputs.new('SvVerticesSocket', "Vert normals") + self.inputs.new('SvStringsSocket', "Faces") + + def process(self): + objects = self.inputs['Object'].sv_get(deepcopy=False, default=[]) + + v_normals = self.inputs['Vert normals'].sv_get(deepcopy=False, default=[]) + faces = self.inputs['Faces'].sv_get(deepcopy=False, default=[]) + + for obj, v_ns, fs in zip(objects, repeat_last(v_normals), repeat_last(faces)): + obj.data.use_auto_smooth = True + + n_per_loop = [(0, 0, 0) for _ in range(len(obj.data.loops))] + for me_p, f in zip(obj.data.polygons, fs): + for l_i, f_i in zip(range(me_p.loop_start, me_p.loop_start + me_p.loop_total), repeat_last(f)): + try: + normal = v_ns[f_i] + except IndexError: + normal = v_ns[-1] + n_per_loop[l_i] = Vector(normal).normalized() if self.normalize else normal + obj.data.normals_split_custom_set(n_per_loop) + + self.outputs['Object'].sv_set(objects) + + +register, unregister = bpy.utils.register_classes_factory([SvSetLoopNormalsNode]) diff --git a/nodes/scene/collection_picker_mk1.py b/nodes/scene/collection_picker_mk1.py index 46c518e9d6..17b5b526ba 100644 --- a/nodes/scene/collection_picker_mk1.py +++ b/nodes/scene/collection_picker_mk1.py @@ -1,7 +1,7 @@ # This file is part of project Sverchok. It's copyrighted by the contributors # recorded in the version control history of the file, available from # its original location https://github.com/nortikin/sverchok/commit/master -# +# # SPDX-License-Identifier: GPL3 # License-Filename: LICENSE @@ -10,17 +10,18 @@ from sverchok.node_tree import SverchCustomTreeNode from sverchok.utils.nodes_mixins.sv_animatable_nodes import SvAnimatableNode from sverchok.data_structure import updateNode +from bpy.props import BoolProperty; # pylint: disable=w0613 # pylint: disable=c0111 # pylint: disable=c0103 class SvCollectionPicker(bpy.types.Node, SverchCustomTreeNode, SvAnimatableNode): - + """ Triggers: SvCollectionPicker - Tooltip: - + Tooltip: + A short description for reader of node code """ @@ -34,6 +35,18 @@ def find_collections(self, object): collection: bpy.props.PointerProperty( name="collection name", poll=find_collections, type=bpy.types.Collection, update=updateNode) + sort_object: BoolProperty( + name="Sort Objects", description="Sort objects by name", + default=True, update=updateNode) + + show_all_objects: bpy.props.BoolProperty( + name="Show All Objects", description="Show all objects in the hierarchy of collections", + default=False, update=updateNode) + + show_only_visible: bpy.props.BoolProperty( + name="Show Only Visible", description="Show only the visible objects", + default=False, update=updateNode) + def sv_init(self, context): self.outputs.new("SvObjectSocket", "Objects") @@ -41,15 +54,31 @@ def draw_buttons(self, context, layout): self.draw_animatable_buttons(layout, icon_only=True) col = layout.column() col.prop_search(self, 'collection', bpy.data, 'collections', text='', icon='GROUP') + layout.prop(self, "show_all_objects") + layout.prop(self, "show_only_visible") + layout.prop(self, "sort_object"); def process(self): found_objects = [] if self.collection: - found_objects = self.collection.objects[:] or [] + if self.show_all_objects: + found_objects = bpy.data.collections[self.collection.name].all_objects[:] or [] + else: + found_objects = self.collection.objects[:] or [] - self.outputs['Objects'].sv_set(found_objects) + if self.show_only_visible: + found_objects = [obj for obj in found_objects if obj.visible_get()] + + if self.sort_object: + items = [(obj.name, obj) for obj in found_objects] + items = sorted(items, key=lambda x: x[0], reverse=False) + found_objects = [item[1] for item in items] + self.outputs['Objects'].sv_set(found_objects) classes = [SvCollectionPicker] register, unregister = bpy.utils.register_classes_factory(classes) + +if __name__ == '__main__': + register() diff --git a/nodes/spatial/random_points_on_mesh.py b/nodes/spatial/random_points_on_mesh.py index c526910188..9f9e5c56f4 100644 --- a/nodes/spatial/random_points_on_mesh.py +++ b/nodes/spatial/random_points_on_mesh.py @@ -13,14 +13,18 @@ import bpy from mathutils import Vector -from mathutils.bvhtree import BVHTree from mathutils.geometry import tessellate_polygon, area_tri from sverchok.node_tree import SverchCustomTreeNode -from sverchok.data_structure import updateNode, throttle_and_update_node +from sverchok.data_structure import updateNode, throttle_and_update_node, numpy_full_list +from sverchok.utils.bvh_tree import bvh_tree_from_polygons from sverchok.utils.geom import calc_bounds from sverchok.utils.sv_mesh_utils import point_inside_mesh +def np_calc_tris_areas(v_pols): + perp = np.cross(v_pols[:, 1]- v_pols[:, 0], v_pols[:, 2]- v_pols[:, 0])/2 + return np.linalg.norm(perp, axis=1)/2 + class SocketProperties(NamedTuple): name: str socket_type: str @@ -49,12 +53,17 @@ class InputData(NamedTuple): class NodeProperties(NamedTuple): proportional: bool - mode : str + mode: str + all_triangles: bool + implementation: str + safe_check: bool + out_np: tuple MAX_ITERATIONS = 1000 -def populate_mesh(verts, faces, count, seed): - bvh = BVHTree.FromPolygons(verts, faces) +def populate_mesh(verts, faces, count, seed, all_triangles, safe_check): + + bvh = bvh_tree_from_polygons(verts, faces, all_triangles=all_triangles, epsilon=0.0, safe_check=safe_check) np.random.seed(seed) x_min, x_max, y_min, y_max, z_min, z_max = calc_bounds(verts) low = np.array([x_min, y_min, z_min]) @@ -66,7 +75,7 @@ def populate_mesh(verts, faces, count, seed): if iterations > MAX_ITERATIONS: raise Exception("Iterations limit is reached") max_pts = max(count, count-done) - points = np.random.uniform(low, high, size=(max_pts,3)).tolist() + points = np.random.uniform(low, high, size=(max_pts, 3)).tolist() points = [p for p in points if point_inside_mesh(bvh, p)] n = len(points) result.extend(points) @@ -78,26 +87,102 @@ def populate_mesh(verts, faces, count, seed): def node_process(inputs: InputData, properties: NodeProperties): if properties.mode == 'SURFACE': - me = TriangulatedMesh([Vector(co) for co in inputs.verts], inputs.faces) + me = TriangulatedMesh(inputs.verts, inputs.faces, properties.all_triangles, properties.implementation) + if properties.proportional: me.use_even_points_distribution() if inputs.face_weight: me.set_custom_face_weights(inputs.face_weight) - return me.generate_random_points(inputs.number[0], inputs.seed[0]) # todo [0] <-- ?! + if properties.implementation == 'NUMPY': + return me.generate_random_points_np(inputs.number[0], inputs.seed[0], properties.out_np) + return me.generate_random_points(inputs.number[0], inputs.seed[0]) + + elif properties.mode == 'VOLUME': + return populate_mesh(inputs.verts, inputs.faces, + inputs.number[0], inputs.seed[0], + properties.all_triangles, properties.safe_check) + else: # 'EDGES' + return random_points_on_edges(inputs.verts, inputs.faces, inputs.face_weight, + inputs.number[0], inputs.seed[0], + properties.proportional, properties.out_np) + +def verts_edges(verts, edges): + if isinstance(verts, np.ndarray): + np_verts = verts + else: + np_verts = np.array(verts) + if isinstance(edges, np.ndarray): + np_edges = edges else: - return populate_mesh(inputs.verts, inputs.faces, inputs.number[0], inputs.seed[0]) + np_edges = np.array(edges) + + return np_verts[np_edges] + +def get_weights(edges_dir, input_weights, proportional): + if proportional: + edge_length = np.linalg.norm(edges_dir, axis=1) + if len(input_weights) > 0: + edges_weights = numpy_full_list(input_weights, len(edges_dir)) * edge_length + weights = edges_weights/np.sum(edges_weights) + else: + weights = edge_length/np.sum(edge_length) + + else: + if len(input_weights) > 0: + edges_weights = numpy_full_list(input_weights, len(edges_dir)) + weights = edges_weights/np.sum(edges_weights) + else: + weights = None + + return weights + +def random_points_on_edges(verts: List[List[float]], + edges: List[List[int]], + input_weights: List[float], + random_points_total: int, + seed: int, + proportional: bool, + out_np: List[bool]): + + v_edges = verts_edges(verts, edges) + edges_dir = v_edges[:, 1] - v_edges[:, 0] + weights = get_weights(edges_dir, input_weights, proportional) + np.random.seed(seed) + + chosen_edges = np.random.choice(np.arange(len(edges)), + random_points_total, + replace=True, + p=weights) + + edges_with_points, points_total_per_edge = np.unique(chosen_edges, return_counts=True) + + t_s = np.random.uniform(low=0, high=1, size=random_points_total) + direc = np.repeat(edges_dir[edges_with_points], points_total_per_edge, axis=0) + orig = np.repeat(v_edges[edges_with_points, 0], points_total_per_edge, axis=0) + + random_points = orig + direc * t_s[:, np.newaxis] + + return (random_points if out_np[0] else random_points.tolist(), + chosen_edges if out_np[1] else chosen_edges.tolist()) + class TriangulatedMesh: - def __init__(self, verts: List[Vector], faces: List[List[int]]): - self._verts = verts - self._faces = faces - self._face_weights = None + def __init__(self, verts: List[List[float]], faces: List[List[int]], all_triangles: bool, implementation: str): + if implementation == 'NUMPY': + self._verts = verts + else: + self._verts = [Vector(v) for v in verts] - self._tri_faces = [] + self._faces = faces + self._face_weights = [] self._tri_face_areas = [] - self._old_face_indexes_per_tri = [] - - self._triangulate() + if all_triangles: + self._tri_faces = faces + self._old_face_indexes_per_tri = list(range(len(faces))) + else: + self._tri_faces = [] + self._old_face_indexes_per_tri = [] + self._triangulate() def use_even_points_distribution(self, even=True): self._face_weights = self.tri_face_areas if even else None @@ -105,10 +190,45 @@ def use_even_points_distribution(self, even=True): def set_custom_face_weights(self, custom_weights): weights_per_tri = self._face_attrs_to_tri_face_attrs(custom_weights) if self._face_weights: - self._face_weights *= weights_per_tri # can be troubles if set custom weights several times + self._face_weights = [f*w for f, w in zip(self._face_weights, weights_per_tri)] else: self._face_weights = weights_per_tri + def generate_random_points_np(self, + random_points_total: int, + seed: int, + out_np: Tuple[bool, bool]) -> Tuple[list, list]: + + np.random.seed(seed) + faces_with_points, points_total_per_face = self._distribute_points_np(random_points_total) + random_points = [] + old_face_indexes_per_point = [] + u1 = np.random.uniform(low=0, high=1, size=random_points_total) + u2 = np.random.uniform(low=0, high=1, size=random_points_total) + mask = (u1 + u2) > 1 + u1[mask] = 1 - u1[mask] + mask = (u1+u2) > 1 + u2[mask] = 1 - u2[mask] + + if isinstance(self._tri_faces, np.ndarray): + np_faces = self._tri_faces[faces_with_points] + else: + np_faces = np.array(self._tri_faces)[faces_with_points] + if isinstance(self._verts, np.ndarray): + v_pols = np.repeat(self._verts[np_faces], points_total_per_face, axis=0) + else: + v_pols = np.repeat(np.array(self._verts)[np_faces], points_total_per_face, axis=0) + + side1 = v_pols[:, 1, :] - v_pols[:, 0, :] + side2 = v_pols[:, 2, :] - v_pols[:, 0, :] + + random_points = v_pols[:, 0, :] + side1 * u1[:, np.newaxis] + side2 * u2[:, np.newaxis] + + old_face_indexes_per_point = np.repeat(np.array(self._old_face_indexes_per_tri)[faces_with_points], points_total_per_face, axis=0) + + return (random_points if out_np[0] else random_points.tolist(), + old_face_indexes_per_point if out_np[1] else old_face_indexes_per_point.tolist()) + def generate_random_points(self, random_points_total: int, seed: int) -> Tuple[list, list]: random.seed(seed) points_total_per_face = self._distribute_points(random_points_total) @@ -125,7 +245,11 @@ def generate_random_points(self, random_points_total: int, seed: int) -> Tuple[l @property def tri_face_areas(self): if not self._tri_face_areas: - self._tri_face_areas = [area_tri(*[self._verts[i] for i in f]) for f in self._tri_faces] + if isinstance(self._verts, np.ndarray): + self._tri_face_areas = np_calc_tris_areas(self._verts[np.array(self._tri_faces)]) + else: + self._tri_face_areas = [area_tri(*[self._verts[i] for i in f]) for f in self._tri_faces] + return self._tri_face_areas def _distribute_points(self, random_points_total: int) -> List[int]: @@ -136,26 +260,50 @@ def _distribute_points(self, random_points_total: int) -> List[int]: points_total_per_face[i] += 1 return points_total_per_face + def _distribute_points_np(self, random_points_total: int) -> List[int]: + # generate list of numbers which mean how many points should be created on face + + if len(self._face_weights) != 0: + weights = np.array(self._face_weights, dtype='float') + weights /= np.sum(weights) + else: + weights = None + chosen_faces = np.random.choice( + np.arange(len(self._tri_faces)), + random_points_total, + replace=True, + p=weights) + + return np.unique(chosen_faces, return_counts=True) + def _triangulate(self): # generate list of triangle faces and list of indexes which points to initial faces for each new triangle + verts = self._verts + tri_faces_add = self._tri_faces.append + old_face_index_add = self._old_face_indexes_per_tri.append for i, f in enumerate(self._faces): - face_verts = [[self._verts[i] for i in f]] - # [[v1,v2,v3,v4]] - face_verts - for tri_face in tessellate_polygon(face_verts): - self._tri_faces.append([f[itf] for itf in tri_face]) - self._old_face_indexes_per_tri.append(i) + if len(f) == 3: + tri_faces_add(f) + old_face_index_add(i) + else: + face_verts = [[verts[i] for i in f]] + # [[v1,v2,v3,v4]] - face_verts + for tri_face in tessellate_polygon(face_verts): + tri_faces_add([f[itf] for itf in tri_face]) + old_face_index_add(i) @staticmethod def _get_random_vectors_on_tri(v1, v2, v3, number): # returns random vertices for given triangle out = [] + side1 = v2 - v1 + side2 = v3 - v1 for _ in range(number): u1 = random.random() u2 = random.random() u1 = u1 if u1 + u2 <= 1 else 1 - u1 u2 = u2 if u1 + u2 <= 1 else 1 - u2 - side1 = v2 - v1 - side2 = v3 - v1 + out.append(v1 + side1 * u1 + side2 * u2) return out @@ -166,44 +314,125 @@ def _face_attrs_to_tri_face_attrs(self, values): class SvRandomPointsOnMesh(bpy.types.Node, SverchCustomTreeNode): """ Triggers: random points vertices - - distribute points on given mesh - points are created evenly according area faces - based on Blender function - tessellate_polygon + Tooltip: distribute points on given mesh """ bl_idname = 'SvRandomPointsOnMesh' bl_label = 'Random points on mesh' sv_icon = 'SV_RANDOM_NUM_GEN' - points_number: bpy.props.IntProperty(name='Number', default=10, description="Number of random points", - update=updateNode) - seed: bpy.props.IntProperty(name='Seed', update=updateNode) + viewer_map = [ + ("SvViewerDrawMk4", [60, 0]) + ], [ + ([0, 0], [1, 0]) + ] + + points_number: bpy.props.IntProperty( + name='Number', + default=10, + description="Number of random points", + update=updateNode) + + seed: bpy.props.IntProperty( + name='Seed', + update=updateNode) proportional: bpy.props.BoolProperty( - name="Proportional", - description="If checked, then number of points at each face is proportional to the area of the face", - default=True, - update=updateNode) + name="Proportional", + description="If checked, then number of points at each face is proportional to the area of the face", + default=True, + update=updateNode) @throttle_and_update_node def update_sockets(self, context): - self.outputs['Face index'].hide_safe = self.mode != 'SURFACE' - - modes = [ - ('SURFACE', "Surface", "Surface", 0), - ('VOLUME', "Volume", "Volume", 1) + self.outputs['Face index'].hide_safe = self.mode == 'VOLUME' + self.inputs['Face weight'].hide_safe = self.mode == 'VOLUME' + self.outputs[1].label = 'Edge index' if self.mode == 'EDGES' else 'Face index' + self.inputs[1].label = 'Edges' if self.mode == 'EDGES' else 'Faces' + self.inputs[2].label = 'Edge Weight' if self.mode == 'EDGES' else 'Face Weight' + + modes = [('SURFACE', "Surface", "Surface", 0), + ('VOLUME', "Volume", "Volume", 1), + ('EDGES', "Edges", "Edges", 2), + ] + + mode: bpy.props.EnumProperty( + name="Mode", + items=modes, + default='SURFACE', + update=update_sockets) + + all_triangles: bpy.props.BoolProperty( + name="All Triangles", + description="Enable if the input mesh is made only of triangles (makes node faster)", + default=False, + update=updateNode) + + safe_check: bpy.props.BoolProperty( + name='Safe Check', + description='When disabled polygon indices referring to unexisting points will crash Blender but makes node faster', + default=True) + + implementations = [ + ('NUMPY', "NumPy", "Faster", 0), + ('MATHUTILS', "MathUtils", "Old implementation", 1) ] + implementation: bpy.props.EnumProperty( + name="Implementation", + items=implementations, + default='NUMPY', + update=updateNode) + + out_np: bpy.props.BoolVectorProperty( + name="Ouput Numpy", + description="Output NumPy arrays", + default=(False, False), + size=2, update=updateNode) - mode : bpy.props.EnumProperty( - name = "Mode", - items = modes, - default = 'SURFACE', - update=update_sockets) - def draw_buttons(self, context, layout): + layout.prop(self, "mode", text='') + if self.mode != 'VOLUME': + layout.prop(self, "proportional") + + def draw_buttons_ext(self, context, layout): layout.prop(self, "mode", text='') if self.mode == 'SURFACE': layout.prop(self, "proportional") + layout.prop(self, "all_triangles") + layout.prop(self, "implementation") + if self.implementation == 'NUMPY': + b = layout.box() + b.label(text='Output Numpy') + r = b.row() + r.prop(self, "out_np", index=0, text='Verts', toggle=True) + r.prop(self, "out_np", index=1, text='Face index', toggle=True) + elif self.mode == 'VOLUME': + layout.prop(self, "all_triangles") + layout.prop(self, "safe_check") + else: + layout.prop(self, "proportional") + b = layout.box() + b.label(text='Output Numpy') + r = b.row() + r.prop(self, "out_np", index=0, text='Verts', toggle=True) + r.prop(self, "out_np", index=1, text='Edge index', toggle=True) + + def rclick_menu(self, context, layout): + layout.prop_menu_enum(self, "mode") + if self.mode == 'SURFACE': + layout.prop(self, "proportional") + layout.prop(self, "all_triangles") + layout.prop_menu_enum(self, "implementation") + if self.implementation == 'NUMPY': + layout.label(text='Output Numpy') + layout.prop(self, "out_np", index=0, text='Verts') + layout.prop(self, "out_np", index=1, text='Face index') + elif self.mode == 'EDGES': + layout.prop(self, "proportional") + layout.label(text='Output Numpy') + layout.prop(self, "out_np", index=0, text='Verts') + layout.prop(self, "out_np", index=1, text='Edge index') + else: + layout.prop(self, "all_triangles") def sv_init(self, context): [self.inputs.new(p.socket_type, p.name) for p in INPUT_CONFIG] @@ -216,7 +445,12 @@ def process(self): if not all([self.inputs['Verts'].is_linked, self.inputs['Faces'].is_linked]): return - props = NodeProperties(self.proportional, self.mode) + props = NodeProperties(self.proportional, + self.mode, + self.all_triangles, + self.implementation, + self.safe_check, + self.out_np) out = [node_process(inputs, props) for inputs in self.get_input_data_iterator(INPUT_CONFIG)] [s.sv_set(data) for s, data in zip(self.outputs, zip(*out))] diff --git a/nodes/surface/deconstruct_surface.py b/nodes/surface/deconstruct_surface.py index 0c0d235d6e..0ca8ad0c19 100644 --- a/nodes/surface/deconstruct_surface.py +++ b/nodes/surface/deconstruct_surface.py @@ -71,9 +71,9 @@ def deconstruct(self, surface): if hasattr(nurbs, 'get_weights'): weights = nurbs.get_weights() if self.split_points: - weights = weights.flatten().tolist() - else: weights = weights.tolist() + else: + weights = weights.flatten().tolist() else: weights = [] @@ -160,4 +160,3 @@ def register(): def unregister(): bpy.utils.unregister_class(SvDeconstructSurfaceNode) - diff --git a/nodes/text/string_tools.py b/nodes/text/string_tools.py index ae850f6384..54c755bbb3 100644 --- a/nodes/text/string_tools.py +++ b/nodes/text/string_tools.py @@ -51,6 +51,7 @@ def find_all(text, chars): out.append(f) index=f+1 return out + def find_all_slice(text, chars, start, end): out =[] index=start @@ -64,10 +65,14 @@ def find_all_slice(text, chars, start, end): index=f+1 return out +def number_to_string(data, precision): + return ("{:." + str(precision) + "f}").format(float(data)) + func_dict = { "---------------OPS" : "#---------------------------------------------------#", "to_string": (0, str, ('t t'), "To String"), "to_number": (1, eval, ('t s'), "To Number"), + "num_to_str": (3, number_to_string , ('ss t'), "Number To String", ('Precision',)), "join": (5, lambda x, y: ''.join([x,y]), ('tt t'), "Join"), "join_all": (6, join, ('tb t'), "Join All", ('Add Break Lines',)), "split": (10, split, ('tcs t'), "Split", ('Spliter', 'Max Split')), diff --git a/nodes/vector/interpolation_mk3.py b/nodes/vector/interpolation_mk3.py index d6c982d277..809d31bd7c 100644 --- a/nodes/vector/interpolation_mk3.py +++ b/nodes/vector/interpolation_mk3.py @@ -22,19 +22,24 @@ from bpy.props import EnumProperty, FloatProperty, BoolProperty, IntProperty from sverchok.node_tree import SverchCustomTreeNode +from sverchok.utils.nodes_mixins.recursive_nodes import SvRecursiveNode + from sverchok.data_structure import updateNode, dataCorrect, repeat_last from sverchok.utils.geom import LinearSpline, CubicSpline -def make_range(number): +def make_range(number, end_point): if number in {0, 1, 2} or number < 0: return [0.0] - return np.linspace(0.0, 1.0, num=number, endpoint=True).tolist() + return np.linspace(0.0, 1.0, num=number, endpoint=end_point).tolist() -class SvInterpolationNodeMK3(bpy.types.Node, SverchCustomTreeNode): - '''Advanced Vect. Interpolation''' +class SvInterpolationNodeMK3(bpy.types.Node, SverchCustomTreeNode, SvRecursiveNode): + """ + Triggers: Interp. Vector List + Tooltip: Interpolate a list of vertices in a linear or cubic fashion + """ bl_idname = 'SvInterpolationNodeMK3' bl_label = 'Vector Interpolation' bl_icon = 'OUTLINER_OB_EMPTY' @@ -44,9 +49,21 @@ def wrapped_updateNode(self, context): self.inputs['Interval'].prop_name = 'int_in' if self.infer_from_integer_input else 't_in' self.process_node(context) - t_in: FloatProperty(name="t", default=.5, min=0, max=1, precision=5, update=updateNode) - int_in: IntProperty(name="int in", default=10, min=3, update=updateNode) - h: FloatProperty(default=.001, precision=5, update=updateNode) + t_in: FloatProperty( + name="t", + default=.5, + min=0, max=1, + precision=5, + update=updateNode) + int_in: IntProperty( + name="int in", + default=10, + min=3, + update=updateNode) + h: FloatProperty( + default=.001, + precision=5, + update=updateNode) modes = [('SPL', 'Cubic', "Cubic Spline", 0), ('LIN', 'Linear', "Linear Interpolation", 1)] @@ -58,10 +75,30 @@ def wrapped_updateNode(self, context): ('CHEBYSHEV', 'Chebyshev', "Chebyshev distance", 3)] knot_mode: EnumProperty( - name='Knot Mode', default="DISTANCE", items=knot_modes, update=updateNode) - - is_cyclic: BoolProperty(name="Cyclic", default=False, update=updateNode) - infer_from_integer_input: BoolProperty(name="IntRange", default=False, update=wrapped_updateNode) + name='Knot Mode', + default="DISTANCE", + items=knot_modes, + update=updateNode) + + is_cyclic: BoolProperty( + name="Cyclic", + default=False, + update=updateNode) + + infer_from_integer_input: BoolProperty( + name="Int Range", + default=False, + update=wrapped_updateNode) + + end_point: BoolProperty( + name="End Point", + default=True, + update=updateNode) + + output_numpy: BoolProperty( + name='Output NumPy', + description='Output NumPy arrays', + default=False, update=updateNode) def sv_init(self, context): self.inputs.new('SvVerticesSocket', 'Vertices') @@ -74,70 +111,63 @@ def draw_buttons(self, context, layout): layout.prop(self, 'mode', expand=True) row = layout.row(align=True) row.prop(self, 'is_cyclic', toggle=True) - row.prop(self, 'infer_from_integer_input',toggle=True) + row.prop(self, 'infer_from_integer_input', toggle=True) + if self.infer_from_integer_input: + layout.prop(self, 'end_point') + def rclick_menu(self, context, layout): + layout.prop_menu_enum(self, "list_match", text="List Match") def draw_buttons_ext(self, context, layout): + layout.prop(self, 'list_match') + self.draw_buttons(context, layout) layout.prop(self, 'h') layout.prop(self, 'knot_mode') + layout.prop(self, 'output_numpy') - def process(self): + def pre_setup(self): + self.inputs['Vertices'].is_mandatory = True + if self.infer_from_integer_input: + self.inputs['Interval'].nesting_level = 1 + self.inputs['Interval'].pre_processing = 'ONE_ITEM' + else: + self.inputs['Interval'].nesting_level = 2 + self.inputs['Interval'].pre_processing = 'NONE' - if not any((s.is_linked for s in self.outputs)): - return + def process_data(self, params): + verts, t_ins = params calc_tanget = self.outputs['Tanget'].is_linked or self.outputs['Unit Tanget'].is_linked norm_tanget = self.outputs['Unit Tanget'].is_linked - h = self.h - - if self.inputs['Vertices'].is_linked: - verts = self.inputs['Vertices'].sv_get() - verts = dataCorrect(verts) - t_ins = self.inputs['Interval'].sv_get() - + verts_out, tanget_out, norm_tanget_out = [], [], [] + for v, t_in in zip(verts, t_ins): if self.infer_from_integer_input: - t_ins = [make_range(int(value)) for value in t_ins[0]] - - if len(t_ins) > len(verts): - new_verts = verts[:] - for i in range(len(t_ins) - len(verts)): - new_verts.append(verts[-1]) - verts = new_verts - - verts_out = [] - tanget_out = [] - norm_tanget_out = [] - for v, t_in in zip(verts, repeat_last(t_ins)): - + t_corr = make_range(int(t_in), self.end_point) + else: t_corr = np.array(t_in).clip(0, 1) - if self.mode == 'LIN': - spline = LinearSpline(v, metric = self.knot_mode, is_cyclic = self.is_cyclic) - out = spline.eval(t_corr) - verts_out.append(out.tolist()) - - if calc_tanget: - tanget_out.append(spline.tangent(t_corr).tolist()) - - else: # SPL - spline = CubicSpline(v, metric = self.knot_mode, is_cyclic = self.is_cyclic) - out = spline.eval(t_corr) - verts_out.append(out.tolist()) - if calc_tanget: - tangent = spline.tangent(t_corr, h) - if norm_tanget: - norm = np.linalg.norm(tangent, axis=1) - norm_tanget_out.append((tangent / norm[:, np.newaxis]).tolist()) - tanget_out.append(tangent.tolist()) - - outputs = self.outputs - if outputs['Vertices'].is_linked: - outputs['Vertices'].sv_set(verts_out) - if outputs['Tanget'].is_linked: - outputs['Tanget'].sv_set(tanget_out) - if outputs['Unit Tanget'].is_linked: - outputs['Unit Tanget'].sv_set(norm_tanget_out) + if self.mode == 'LIN': + spline = LinearSpline(v, metric=self.knot_mode, is_cyclic=self.is_cyclic) + out = spline.eval(t_corr) + verts_out.append(out if self.output_numpy else out.tolist()) + + if calc_tanget: + tanget_out.append(spline.tangent(t_corr) if self.output_numpy else spline.tangent(t_corr).tolist()) + + else: # SPL + spline = CubicSpline(v, metric=self.knot_mode, is_cyclic=self.is_cyclic) + out = spline.eval(t_corr) + verts_out.append(out if self.output_numpy else out.tolist()) + if calc_tanget: + tangent = spline.tangent(t_corr, h) + if norm_tanget: + norm = np.linalg.norm(tangent, axis=1) + tangent_norm = tangent / norm[:, np.newaxis] + norm_tanget_out.append(tangent_norm if self.output_numpy else tangent_norm.tolist()) + tanget_out.append(tangent if self.output_numpy else tangent.tolist()) + + return verts_out, tanget_out, norm_tanget_out def register(): diff --git a/nodes/analyzer/bvh_nearest_new.py b/old_nodes/bvh_nearest_new.py similarity index 97% rename from nodes/analyzer/bvh_nearest_new.py rename to old_nodes/bvh_nearest_new.py index 37dddf6f73..edd149a84e 100644 --- a/nodes/analyzer/bvh_nearest_new.py +++ b/old_nodes/bvh_nearest_new.py @@ -29,7 +29,7 @@ class SvBVHnearNewNode(bpy.types.Node, SverchCustomTreeNode): bl_label = 'bvh_nearest' bl_icon = 'OUTLINER_OB_EMPTY' sv_icon = 'SV_POINT_ON_MESH' - + replacement_nodes =[('SvNearestPointOnMeshNode', None, None)] modes = [ ("find_nearest", "nearest", "", 0), ("find_nearest_range", "nearest in range", "", 1), diff --git a/nodes/matrix/matrix_out.py b/old_nodes/matrix_out.py similarity index 90% rename from nodes/matrix/matrix_out.py rename to old_nodes/matrix_out.py index 5c55f6130b..8db528e6b8 100644 --- a/nodes/matrix/matrix_out.py +++ b/old_nodes/matrix_out.py @@ -30,6 +30,12 @@ class MatrixOutNode(bpy.types.Node, SverchCustomTreeNode): bl_icon = 'OUTLINER_OB_EMPTY' sv_icon = 'SV_MATRIX_OUT' + replacement_nodes = [('SvMatrixOutNodeMK2', None, dict(Rotation="Axis"))] + + def rclick_menu(self, context, layout): + layout.prop(self, "flat_output", text="Flat Output", expand=False) + self.node_replacement_menu(context, layout) + def sv_init(self, context): self.outputs.new('SvVerticesSocket', "Location") self.outputs.new('SvVerticesSocket', "Scale") diff --git a/nodes/analyzer/mesh_select.py b/old_nodes/mesh_select.py similarity index 99% rename from nodes/analyzer/mesh_select.py rename to old_nodes/mesh_select.py index b3d3c9bffc..cf683da4da 100644 --- a/nodes/analyzer/mesh_select.py +++ b/old_nodes/mesh_select.py @@ -34,7 +34,7 @@ class SvMeshSelectNode(bpy.types.Node, SverchCustomTreeNode): bl_idname = 'SvMeshSelectNode' bl_label = 'Select mesh elements by location' bl_icon = 'UV_SYNC_SELECT' - + replacement_nodes = [('SvMeshSelectNodeMk2', None, None)] modes = [ ("BySide", "By side", "Select specified side of mesh", 0), ("ByNormal", "By normal direction", "Select faces with normal in specified direction", 1), diff --git a/settings.py b/settings.py index 4df964c4d6..0da9287d3f 100644 --- a/settings.py +++ b/settings.py @@ -10,6 +10,7 @@ from sverchok.core import update_system from sverchok.utils import logging from sverchok.utils.sv_gist_tools import TOKEN_HELP_URL +from sverchok.utils.sv_extra_addons import draw_extra_addons from sverchok.ui import color_def from sverchok.ui.utils import message_on_layout @@ -402,6 +403,7 @@ def update_log_level(self, context): # updating sverchok dload_archive_name: StringProperty(name="archive name", default="master") # default = "master" dload_archive_path: StringProperty(name="archive path", default="https://github.com/nortikin/sverchok/archive/") + available_new_version: bpy.props.BoolProperty(default=False) FreeCAD_folder: StringProperty( name="FreeCAD python 3.7 folder", @@ -584,6 +586,7 @@ def draw_freecad_ops(): if any(package.module is None for package in sv_dependencies.values()): box.operator('wm.url_open', text="Read installation instructions for missing dependencies").url = "https://github.com/nortikin/sverchok/wiki/Dependencies" + draw_extra_addons(layout) def draw(self, context): @@ -613,7 +616,7 @@ def draw(self, context): row1.operator('wm.url_open', text='Sverchok home page').url = 'http://nikitron.cc.ua/blend_scripts.html' row1.operator('wm.url_open', text='Documentation').url = 'http://nikitron.cc.ua/sverch/html/main.html' - if context.scene.sv_new_version: + if self.available_new_version: row1.operator('node.sverchok_update_addon', text='Upgrade Sverchok addon') else: row1.operator('node.sverchok_check_for_upgrades_wsha', text='Check for new version') diff --git a/tests/docs_tests.py b/tests/docs_tests.py index 87eb5f6e7d..028b4cb73a 100644 --- a/tests/docs_tests.py +++ b/tests/docs_tests.py @@ -153,7 +153,6 @@ def test_node_docs_existance(self): symmetrize.py vd_attr_node_mk2.py scalar_field_point.py -bvh_nearest_new.py quads_to_nurbs.py location.py sun_position.py""".split("\n") diff --git a/tests/json_import_tests.py b/tests/json_import_tests.py index 1352a12f24..c737c5c64a 100644 --- a/tests/json_import_tests.py +++ b/tests/json_import_tests.py @@ -62,9 +62,7 @@ def test_mesh_expr_import(self): class ExamplesImportTest(SverchokTestCase): def test_import_examples(self): - examples_path = Path(sverchok.__file__).parent / 'json_examples' - - for category_name in example_categories_names(): + for examples_path, category_name in example_categories_names(): info("Opening Dir named: %s", category_name) diff --git a/ui/development.py b/ui/development.py index b876f999c5..f360f021b9 100644 --- a/ui/development.py +++ b/ui/development.py @@ -21,7 +21,7 @@ import subprocess import webbrowser import socket - +import inspect import bpy from bpy.props import StringProperty, CollectionProperty, BoolProperty, FloatProperty @@ -35,6 +35,8 @@ from sverchok.ui.presets import get_presets, SverchPresetReplaceOperator, SvSaveSelected, node_supports_presets from sverchok.nodes.__init__ import nodes_dict from sverchok.settings import PYPATH +from sverchok.utils.extra_categories import external_node_docs + def displaying_sverchok_nodes(context): return context.space_data.tree_type in {'SverchCustomTreeType', 'SverchGroupTreeType'} @@ -95,6 +97,7 @@ def get_docs_filepath(string_dir, filename): ) return filepath + class SvViewHelpForNode(bpy.types.Operator): """ Open docs on site, on local PC or on github """ bl_idname = "node.view_node_help" @@ -106,6 +109,10 @@ def execute(self, context): n = context.active_node string_dir = remapper.get(n.bl_idname) + if not string_dir: #external node + print('external_node') + return external_node_docs(self, n, self.kind) + filename = n.__module__.split('.')[-1] if filename in ('mask','mask_convert','mask_join'): string_dir = 'list_masks' @@ -232,7 +239,6 @@ def view_source_external(self, prefs, fpath): def get_filepath_from_node(self, n): """ get full filepath on disk for a given node reference """ - import inspect return inspect.getfile(n.__class__) class SV_MT_LoadPresetMenu(bpy.types.Menu): diff --git a/ui/fonts/DataLatin.ttf b/ui/fonts/DataLatin.ttf new file mode 100644 index 0000000000..9194efc292 Binary files /dev/null and b/ui/fonts/DataLatin.ttf differ diff --git a/ui/fonts/DroidSansMono.ttf b/ui/fonts/DroidSansMono.ttf new file mode 100644 index 0000000000..a007071944 Binary files /dev/null and b/ui/fonts/DroidSansMono.ttf differ diff --git a/ui/fonts/Envy Code R.ttf b/ui/fonts/Envy Code R.ttf new file mode 100644 index 0000000000..e8e4a6045f Binary files /dev/null and b/ui/fonts/Envy Code R.ttf differ diff --git a/ui/fonts/FiraCode-VF.ttf b/ui/fonts/FiraCode-VF.ttf new file mode 100644 index 0000000000..4694e50d1b Binary files /dev/null and b/ui/fonts/FiraCode-VF.ttf differ diff --git a/ui/fonts/Hack-Regular.ttf b/ui/fonts/Hack-Regular.ttf new file mode 100644 index 0000000000..92a90cb06e Binary files /dev/null and b/ui/fonts/Hack-Regular.ttf differ diff --git a/ui/fonts/Monoid-Retina.ttf b/ui/fonts/Monoid-Retina.ttf new file mode 100644 index 0000000000..ec881731ac Binary files /dev/null and b/ui/fonts/Monoid-Retina.ttf differ diff --git a/ui/fonts/OCR-A.ttf b/ui/fonts/OCR-A.ttf new file mode 100644 index 0000000000..0ddaa227c0 Binary files /dev/null and b/ui/fonts/OCR-A.ttf differ diff --git a/ui/fonts/SaxMono.ttf b/ui/fonts/SaxMono.ttf new file mode 100644 index 0000000000..76c77d6411 Binary files /dev/null and b/ui/fonts/SaxMono.ttf differ diff --git a/ui/fonts/ShareTechMono-Regular.ttf b/ui/fonts/ShareTechMono-Regular.ttf new file mode 100644 index 0000000000..8e6e84bfbd Binary files /dev/null and b/ui/fonts/ShareTechMono-Regular.ttf differ diff --git a/ui/fonts/digital-7 (mono).ttf b/ui/fonts/digital-7 (mono).ttf new file mode 100644 index 0000000000..a481b97b4f Binary files /dev/null and b/ui/fonts/digital-7 (mono).ttf differ diff --git a/ui/fonts/larabiefont rg.ttf b/ui/fonts/larabiefont rg.ttf new file mode 100644 index 0000000000..a187f38940 Binary files /dev/null and b/ui/fonts/larabiefont rg.ttf differ diff --git a/ui/fonts/monof55.ttf b/ui/fonts/monof55.ttf new file mode 100644 index 0000000000..9aebf80094 Binary files /dev/null and b/ui/fonts/monof55.ttf differ diff --git a/ui/nodeview_add_menu.py b/ui/nodeview_add_menu.py deleted file mode 100644 index 1277801c53..0000000000 --- a/ui/nodeview_add_menu.py +++ /dev/null @@ -1,63 +0,0 @@ -# -*- coding: utf-8 -*- -# ##### 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 ##### - -''' -zeffii 2020. -''' - -import bpy -import nodeitems_utils -from bpy.app.translations import contexts as i18n_contexts - -def is_sverchok_editor(context): - sv_tree_types = {'SverchCustomTreeType', 'SverchGroupTreeType'} - tree_type = context.space_data.tree_type - if tree_type in sv_tree_types: - return True - -class SVNODE_MT_add(bpy.types.Menu): - bl_space_type = 'NODE_EDITOR' - bl_label = "Add" - bl_translation_context = i18n_contexts.operator_default - - def draw(self, context): - layout = self.layout - - if is_sverchok_editor(context): - layout.operator_context = 'INVOKE_REGION_WIN' - layout.operator("node.sv_extra_search", text="Search", icon='OUTLINER_DATA_FONT') - else: - layout.operator_context = 'INVOKE_DEFAULT' - props = layout.operator("node.add_search", text="Search...", icon='VIEWZOOM') - props.use_transform = True - - layout.separator() - - # actual node submenus are defined by draw functions from node categories - nodeitems_utils.draw_node_categories_menu(self, context) - -def perform_menu_monkey_patch(): - # replace the default Add menu, and update it with sverchok specific search - bpy.types.NODE_MT_add.draw = SVNODE_MT_add.draw - - # replace the default Node Categories menu with our implementation - NC = nodeitems_utils._node_categories - SV = NC['SVERCHOK'] - SVcatlist, sv_draw, SvCats = SV - NC['SVERCHOK'] = (SVcatlist, bpy.types.NODEVIEW_MT_Dynamic_Menu.draw, SvCats) diff --git a/ui/nodeview_keymaps.py b/ui/nodeview_keymaps.py index 9b9e9a9c88..c5fd52260e 100644 --- a/ui/nodeview_keymaps.py +++ b/ui/nodeview_keymaps.py @@ -194,6 +194,23 @@ def add_keymap(): kmi.properties.name = "NODEVIEW_MT_Dynamic_Menu" nodeview_keymaps.append((km, kmi)) + # numbers 1 to 5 for partial menus + kmi = km.keymap_items.new('wm.call_menu', 'ONE', 'PRESS') + kmi.properties.name = "NODEVIEW_MT_Basic_Data_Partial_Menu" + nodeview_keymaps.append((km, kmi)) + kmi = km.keymap_items.new('wm.call_menu', 'TWO', 'PRESS') + kmi.properties.name = "NODEVIEW_MT_Mesh_Partial_Menu" + nodeview_keymaps.append((km, kmi)) + kmi = km.keymap_items.new('wm.call_menu', 'THREE', 'PRESS') + kmi.properties.name = "NODEVIEW_MT_Advanced_Objects_Partial_Menu" + nodeview_keymaps.append((km, kmi)) + kmi = km.keymap_items.new('wm.call_menu', 'FOUR', 'PRESS') + kmi.properties.name = "NODEVIEW_MT_Connection_Partial_Menu" + nodeview_keymaps.append((km, kmi)) + kmi = km.keymap_items.new('wm.call_menu', 'FIVE', 'PRESS') + kmi.properties.name = "NODEVIEW_MT_UI_tools_Partial_Menu" + nodeview_keymaps.append((km, kmi)) + # Shift + S | show custom menu kmi = km.keymap_items.new('wm.call_menu', 'S', 'PRESS', shift=True) kmi.properties.name = "NODEVIEW_MT_Solids_Special_Menu" diff --git a/ui/nodeview_rclick_menu.py b/ui/nodeview_rclick_menu.py index cbf0cf806b..7632904d14 100644 --- a/ui/nodeview_rclick_menu.py +++ b/ui/nodeview_rclick_menu.py @@ -100,6 +100,56 @@ def get_output_sockets_map(node): def offset_node_location(existing_node, new_node, offset): new_node.location = existing_node.location.x + offset[0] + existing_node.width, existing_node.location.y + offset[1] + +def conect_to_3d_viewer(tree): + if hasattr(tree.nodes.active, 'viewer_map'): + view_node(tree) + else: + add_connection(tree, bl_idname_new_node="SvViewerDrawMk4", offset=[60, 0]) + +def view_node(tree): + '''viewer map is a node attribute to inform to the operator how to visualize + the node data + it is a list with two items. + The first item is a list with tuples, every tuple need to have the node bl_idanme and offset to the previous node + The second item is a list with tuples, every tuple indicates a link. + The link is defined by two pairs of numbers, refering to output and input + The first number of every pair indicates the node being 0 the active node 1 the first needed node and so on + The second nmber of every pair indicates de socket index. + + So to say: create a Viewer Draw with a offset of 60,0 and connect the first output to the vertices input + the node would need to have this: + + viewer_map = [ + ("SvViewerDrawMk4", [60, 0]) + ], [ + ([0, 0], [1, 0]) + ] + + ''' + nodes = tree.nodes + links = tree.links + existing_node = nodes.active + node_list = [existing_node] + output_map = existing_node.viewer_map + + previous_state = tree.sv_process + tree.sv_process = False + + for node in output_map[0]: + bl_idname_new_node, offset = node + new_node = nodes.new(bl_idname_new_node) + apply_default_preset(new_node) + offset_node_location(node_list[-1], new_node, offset) + frame_adjust(node_list[-1], new_node) + node_list.append(new_node) + for link in output_map[1]: + output_s, input_s = link + links.new(node_list[output_s[0]].outputs[output_s[1]], + node_list[input_s[0]].inputs[input_s[1]]) + tree.sv_process = previous_state + tree.update() + def add_connection(tree, bl_idname_new_node, offset): nodes = tree.nodes @@ -200,7 +250,8 @@ def execute(self, context): tree = context.space_data.edit_tree if self.fn == 'vdmk2': - add_connection(tree, bl_idname_new_node="SvViewerDrawMk4", offset=[60, 0]) + conect_to_3d_viewer(tree) + elif self.fn == 'vdmk2 + idxv': add_connection(tree, bl_idname_new_node=["SvViewerDrawMk4", "IndexViewerNode"], offset=[180, 0]) elif self.fn == '+idxv': diff --git a/ui/nodeview_space_menu.py b/ui/nodeview_space_menu.py index f797fe31ab..2c665c66c9 100644 --- a/ui/nodeview_space_menu.py +++ b/ui/nodeview_space_menu.py @@ -26,11 +26,20 @@ import bpy -from sverchok.menu import make_node_cats, draw_add_node_operator, is_submenu_call, get_submenu_call_name, compose_submenu_name +from sverchok.menu import ( + make_node_cats, + draw_add_node_operator, + is_submenu_call, + get_submenu_call_name, + compose_submenu_name, + draw_node_ops as monad_node_ops) + from sverchok.utils import get_node_class_reference -from sverchok.utils.extra_categories import get_extra_categories +from sverchok.utils.extra_categories import get_extra_categories, extra_category_providers +from sverchok.utils.context_managers import sv_preferences from sverchok.ui.sv_icons import node_icon, icon, get_icon_switch, custom_icon from sverchok.ui import presets +import nodeitems_utils # from nodeitems_utils import _node_categories sv_tree_types = {'SverchCustomTreeType', 'SverchGroupTreeType'} @@ -73,9 +82,9 @@ def category_has_nodes(cat_name): ["NODEVIEW_MT_AddViz", 'RESTRICT_VIEW_OFF'], ["NODEVIEW_MT_AddText", 'TEXT'], ["NODEVIEW_MT_AddScene", 'SCENE_DATA'], - ["NODEVIEW_MT_AddExchange", 'SCENE_DATA'], + ["NODEVIEW_MT_AddExchange", 'ARROW_LEFTRIGHT'], ["NODEVIEW_MT_AddLayout", 'NODETREE'], - ["NODE_MT_category_SVERCHOK_BPY_Data", "BLENDER"], + ["NODEVIEW_MT_AddBPYData", "BLENDER"], ["separator"], ["NODEVIEW_MT_AddScript", "WORDWRAP_ON"], ["NODEVIEW_MT_AddNetwork", "SYSTEM"], @@ -163,8 +172,8 @@ def draw(self, context): layout = self.layout layout.operator_context = 'INVOKE_REGION_WIN' - if self.bl_idname == 'NODEVIEW_MT_Dynamic_Menu': - layout.operator("node.sv_extra_search", text="Search", icon='OUTLINER_DATA_FONT') + # if self.bl_idname == 'NODEVIEW_MT_Dynamic_Menu': + layout.operator("node.sv_extra_search", text="Search", icon='OUTLINER_DATA_FONT') for item in menu_structure: if item[0] == 'separator': @@ -182,26 +191,30 @@ def draw(self, context): # print('AA', globals()[item[0]].bl_label) layout.menu(item[0], **icon(item[1])) - extra_categories = get_extra_categories() - if extra_categories: - layout.separator() - for category in extra_categories: - layout.menu("NODEVIEW_MT_EX_" + category.identifier) + if extra_category_providers: + for provider in extra_category_providers: + if hasattr(provider, 'use_custom_menu') and provider.use_custom_menu: + layout.menu(provider.custom_menu) + else: + for category in provider.get_categories(): + layout.menu("NODEVIEW_MT_EX_" + category.identifier) -class NODEVIEW_MT_Solids_Special_Menu(bpy.types.Menu): - bl_label = "Solids" - @classmethod - def poll(cls, context): - tree_type = context.space_data.tree_type - if tree_type in sv_tree_types: - #menu_prefs['show_icons'] = get_icon_switch() - # print('showing', menu_prefs['show_icons']) - return True + +class NodePatialMenuTemplate(bpy.types.Menu): + bl_label = "" + items = [] def draw(self, context): layout = self.layout layout.operator_context = 'INVOKE_REGION_WIN' - layout_draw_categories(self.layout, self.bl_label, node_cats[self.bl_label]) + for i in self.items: + item = menu_structure[i] + layout.menu(item[0], **icon(item[1])) +# quick class factory. +def make_partial_menu_class(name, bl_label, items): + name = f'NODEVIEW_MT_{name}_Partial_Menu' + clazz = type(name, (NodePatialMenuTemplate,), {'bl_label': bl_label, 'items':items}) + return clazz class NODEVIEW_MT_AddGenerators(bpy.types.Menu): bl_label = "Generator" @@ -211,6 +224,14 @@ def draw(self, context): layout_draw_categories(self.layout, self.bl_label, node_cats[self.bl_label]) layout.menu("NODEVIEW_MT_AddGeneratorsExt", **icon('PLUGIN')) +class NODEVIEW_MT_AddBPYData(bpy.types.Menu): + bl_label = "BPY Data" + + def draw(self, context): + layout = self.layout + layout_draw_categories(self.layout, self.bl_label, node_cats['BPY Data']) + layout_draw_categories(self.layout, self.bl_label, node_cats['Objects']) + class NODEVIEW_MT_AddModifiers(bpy.types.Menu): bl_label = "Modifiers" @@ -276,6 +297,41 @@ def draw(self, context): layout.operator('node.add_node_output_input', text="Group output").node_type = 'output' layout.operator('node.add_group_tree_from_selected') +class NODE_MT_category_SVERCHOK_MONAD(bpy.types.Menu): + bl_label = "Monad" + label = 'Monad' + + def draw(self, context): + + if context is None: + return + space = context.space_data + if not space: + return + ntree = space.edit_tree + if not ntree: + return + layout = self.layout + + monad_node_ops(self, layout, context) + + if ntree.bl_idname == "SverchGroupTreeType": + draw_add_node_operator(layout, "SvMonadInfoNode") + layout.separator() + + for monad in context.blend_data.node_groups: + if monad.bl_idname != "SverchGroupTreeType": + continue + if monad.name == ntree.name: + continue + # make sure class exists + cls_ref = get_node_class_reference(monad.cls_bl_idname) + + if cls_ref and monad.cls_bl_idname and monad.cls_bl_idname: + op = layout.operator('node.add_node', text=monad.name) + op.type = monad.cls_bl_idname + op.use_transform = True + extra_category_menu_classes = dict() @@ -308,8 +364,10 @@ def draw(self, context): NODEVIEW_MT_AddListOps, NODEVIEW_MT_AddModifiers, NODEVIEW_MT_AddGenerators, + NODEVIEW_MT_AddBPYData, NODEVIEW_MT_AddPresetOps, NODE_MT_category_SVERCHOK_GROUP, + NODE_MT_category_SVERCHOK_MONAD, # like magic. # make | NODEVIEW_MT_Add + class name , menu name make_class('GeneratorsExt', "Generators Extended"), @@ -349,17 +407,36 @@ def draw(self, context): make_class('SVG', "SVG"), make_class('Betas', "Beta Nodes"), make_class('Alphas', "Alpha Nodes"), - # NODEVIEW_MT_Solids_Special_Menu + + # make | NODEVIEW_MT_ + class name +_Partial_Menu , menu name, menu items + make_partial_menu_class('Basic_Data', 'Basic Data Types (1)', range(12, 20)), + make_partial_menu_class('Mesh', 'Mesh (2)', [1, 7, 8, 9, 10]), + make_partial_menu_class('Advanced_Objects', 'Advanced Objects (3)', [2, 3, 4, 5, 6, 28, 30, 32, 33]), + make_partial_menu_class('Connection', 'Connection (4)', [21, 22, 23, 24, 26, 29, 31]), + make_partial_menu_class('UI_tools', 'SV Interface (5)', [25, 35, 36, 37]) + ] +def sv_draw_menu(self, context): + + tree_type = context.space_data.tree_type + if not tree_type in sv_tree_types: + return + layout = self.layout + layout.operator_context = "INVOKE_DEFAULT" + + if not any([(g.bl_idname in sv_tree_types) for g in bpy.data.node_groups]): + layout.operator("node.new_node_tree", text="New Sverchok Node Tree", icon="RNA_ADD") + return + + NODEVIEW_MT_Dynamic_Menu.draw(self, context) def register(): - #global menu_class_by_title - #menu_class_by_title = dict() for category in presets.get_category_names(): make_preset_category_menu(category) for class_name in classes: bpy.utils.register_class(class_name) + bpy.types.NODE_MT_add.append(sv_draw_menu) def unregister(): global menu_class_by_title @@ -369,5 +446,5 @@ def unregister(): for category in presets.get_category_names(): if category in preset_category_menus: bpy.utils.unregister_class(preset_category_menus[category]) - + bpy.types.NODE_MT_add.remove(sv_draw_menu) menu_class_by_title = dict() diff --git a/ui/sv_examples_menu.py b/ui/sv_examples_menu.py index 517b829899..c46377b8f1 100644 --- a/ui/sv_examples_menu.py +++ b/ui/sv_examples_menu.py @@ -1,7 +1,7 @@ # This file is part of project Sverchok. It's copyrighted by the contributors # recorded in the version control history of the file, available from # its original location https://github.com/nortikin/sverchok/commit/master -# +# # SPDX-License-Identifier: GPL3 # License-Filename: LICENSE @@ -38,27 +38,39 @@ def poll(cls, context): return False def draw(self, context): - for category_name in example_categories_names(): - self.layout.menu("SV_MT_PyMenu_" + category_name) + for path, category_name in example_categories_names(): + self.layout.menu("SV_MT_PyMenu_" + category_name.replace(' ', '_')) -def make_submenu_classes(category_name): +def make_submenu_classes(path, category_name): """Generator of submenus classes""" def draw_submenu(self, context): """Draw Svershok template submenu""" - category_path = Path(sverchok.__file__).parent / 'json_examples' / category_name + category_path = path / category_name self.path_menu(searchpaths=[str(category_path)], operator='node.tree_importer_silent') - class_name = "SV_MT_PyMenu_" + category_name + class_name = "SV_MT_PyMenu_" + category_name.replace(' ', '_') + return type(class_name, (bpy.types.Menu,), {'bl_label': category_name, 'draw': draw_submenu, 'bl_idname': class_name}) +extra_examples= dict() +def add_extra_examples(provider, path): + global extra_examples + extra_examples[provider] = path def example_categories_names(): examples_path = Path(sverchok.__file__).parent / 'json_examples' + names = [] for category_path in examples_path.iterdir(): if category_path.is_dir(): - yield category_path.name + names.append((examples_path, category_path.name)) + for c in extra_examples: + for category_path in extra_examples[c].iterdir(): + if category_path.is_dir(): + names.append((extra_examples[c], category_path.name)) + for name in names: + yield name class SvNodeTreeImporterSilent(bpy.types.Operator): @@ -88,7 +100,7 @@ def execute(self, context): def register(): - submenu_classes = (make_submenu_classes(category_name) for category_name in example_categories_names()) + submenu_classes = (make_submenu_classes(path, category_name) for path, category_name in example_categories_names()) _ = [bpy.utils.register_class(cls) for cls in chain(classes, submenu_classes)] bpy.types.NODE_HT_header.append(node_examples_pulldown) diff --git a/ui/sv_icons.py b/ui/sv_icons.py index e4c271bb27..86eb46f3aa 100644 --- a/ui/sv_icons.py +++ b/ui/sv_icons.py @@ -18,7 +18,10 @@ def init(self, custom_icons): if self.provider_inited: return for icon_id, path in self.provider.get_icons(): - custom_icons.load(icon_id, path, "IMAGE") + try: + custom_icons.load(icon_id, path, "IMAGE") + except KeyError: + pass _icon_providers = dict() diff --git a/ui/sv_panel_display_nodes.py b/ui/sv_panel_display_nodes.py index 7fae7adf08..d49c19400e 100644 --- a/ui/sv_panel_display_nodes.py +++ b/ui/sv_panel_display_nodes.py @@ -21,62 +21,72 @@ from sverchok.utils.context_managers import sv_preferences from sverchok.menu import make_node_cats +from sverchok.settings import get_dpi_factor from sverchok.utils.dummy_nodes import is_dependent from pprint import pprint +from sverchok.utils.logging import debug +from collections import namedtuple -DEBUG = False - -_all_categories = {} # cache for the node categories +_node_category_cache = {} # cache for the node categories _spawned_nodes = {} # cache for the spawned nodes -constrainLayoutItems = [ - ("WIDTH", "Width", "", "", 0), - ("HEIGHT", "Height", "", "", 1), - ("ASPECT", "Aspect", "", "", 2)] +constrain_layout_items = [ + ("WIDTH", "Width", "", "ARROW_LEFTRIGHT", 0), + ("HEIGHT", "Height", "", "EMPTY_SINGLE_ARROW", 1), + ("ASPECT", "Aspect", "", "FULLSCREEN_ENTER", 2)] + +node_alignment_items = [ + ("LEFT", "Left", "", "ALIGN_LEFT", 0), + ("CENTER", "Center", "", "ALIGN_CENTER", 1), + ("RIGHT", "Right", "", "ALIGN_RIGHT", 2)] + +NodeItem = namedtuple('NodeItem', 'width height name node') class Bin(object): - ''' Container for items that keep a running sum ''' + ''' Container for items (of NodeType) that keep a running sum ''' - def __init__(self): + def __init__(self): # start with an empty bin self.items = [] - self.width = 0 - self.height = 0 + self.width = 0 # current width of the bin + self.height = 0 # current height of the bin - def append(self, item): + def append(self, item): # add item and update the bin's height and width self.items.append(item) - self.width = max(self.width, item[0][0]) - self.height += item[0][1] + self.width = max(self.width, item.width) + self.height += item.height def __str__(self): ''' Printable representation ''' return 'Bin(w/h=%d/%d, items=%s)' % (self.width, self.height, str(self.items)) -def binpack(nodes, maxValue): +def binpack(nodes, max_bin_height, spacing=0): + ''' Add nodes to the bins of given max bin height and spacing ''' if nodes: - # print("there are %d nodes to bin pack" % (len(nodes))) + debug("There are %d nodes to bin pack" % (len(nodes))) for node in nodes: if node == None: - print("WOW. a None node in the spawned nodes???") + debug("WARNING: a None node in the spawned nodes???") else: - print("there are no nodes to bin pack") + debug("WARNING: there are no nodes to bin pack!!!") return [] - # first, sort the items in the decreasing height order - items = [(node.dimensions, node.bl_idname, node) for node in nodes] - items = sorted(items, key=lambda x: x[0][1], reverse=True) + scale = 1.0 / get_dpi_factor() # dpi adjustment scale + items = [NodeItem(node.dimensions.x * scale, node.dimensions.y * scale, node.bl_idname, node) for node in nodes] + items = sorted(items, key=lambda item: item.height, reverse=True) bins = [] for item in items: - # try to fit the item into the first bin that is not full - for bin in bins: - if bin.height + item[0][1] <= maxValue: # bin not full ? => add item - # print 'Adding', item, 'to', bin + # try to fit the next item into the first bin that is not yet full + for n, bin in enumerate(bins): # check all the bins created so far + if bin.height + len(bin.items) * spacing + item.height <= max_bin_height: + # bin not full ? => add item + debug("ADDING node <%s> to bin #%d" % (item.name, n)) bin.append(item) - break - else: # item didn't fit into any bin, start a new bin - # print 'Making new bin for', item + break # proceed to the next item + else: # item didn't fit into any bin ? => add it to a new bin + debug('ADDING node <%s> to new bin' % (item.name)) bin = Bin() bin.append(item) bins.append(bin) @@ -84,44 +94,66 @@ def binpack(nodes, maxValue): return bins +def should_display_node(name): + if name == "separator" or '@' in name or name == "SvFormulaNodeMk5": + # if name == "separator" or is_dependent(name) or '@' in name: + return False + else: + return True + + def cache_node_categories(): - ''' Cache category names, nodes and enum items ''' - if _all_categories: + """ + Cache category names, nodes and enum items + + Creates the structure: + + categories + + names [ Generator, Surface, ... ] + + items [ ("Generator", "generator", "", 1) ... ] + + {category} [ All, Generators, Surfaces ... ] + + nodes [ SvLine, SvTorus ... ] + """ + + if _node_category_cache: return node_categories = make_node_cats() categories = node_categories.keys() - _all_categories["categories"] = {} - _all_categories["categories"]["names"] = list(categories) - _all_categories["categories"]["names"].append("All") - _all_categories["categories"]["All"] = {} - _all_categories["categories"]["All"]["nodes"] = [] + + debug("categories = %s" % list(categories)) + + _node_category_cache["categories"] = {} + _node_category_cache["categories"]["names"] = list(categories) + _node_category_cache["categories"]["names"].append("All") + _node_category_cache["categories"]["All"] = {} + _node_category_cache["categories"]["All"]["nodes"] = [] for category in categories: - # print("adding category: ", category) + debug("ADDING category: %s" % category) nodes = [n for l in node_categories[category] for n in l] - _all_categories["categories"][category] = {} - _all_categories["categories"][category]["nodes"] = nodes - _all_categories["categories"]["All"]["nodes"].extend(nodes) + nodes = list(filter(lambda node: should_display_node(node), nodes)) + _node_category_cache["categories"][category] = {} + _node_category_cache["categories"][category]["nodes"] = nodes + _node_category_cache["categories"]["All"]["nodes"].extend(nodes) - categoryItems = [] - categoryItems.append(("All", "All", "", 0)) + category_items = [] + category_items.append(("All", "All", "", 0)) for i, category in enumerate(categories): - categoryItem = (category, category.title(), "", i + 1) - categoryItems.append(categoryItem) + category_item = (category, category.title(), "", i + 1) + category_items.append(category_item) - _all_categories["categories"]["items"] = categoryItems + _node_category_cache["categories"]["items"] = category_items - # pprint(_all_categories) + # pprint(_node_category_cache) def get_category_names(): cache_node_categories() - return _all_categories["categories"]["names"] + return _node_category_cache["categories"]["names"] def get_nodes_in_category(category): cache_node_categories() - return _all_categories["categories"][category]["nodes"] + return _node_category_cache["categories"][category]["nodes"] def get_spawned_nodes(): @@ -135,13 +167,15 @@ def add_spawned_node(context, name): if not _spawned_nodes: _spawned_nodes["main"] = [] - # print("adding spawned node: ", name) - tree = context.space_data.edit_tree - node = tree.nodes.new(name) + debug("ADDING spawned node: %s" % name) - _spawned_nodes["main"].append(node) + tree = context.space_data.edit_tree - return node + try: + node = tree.nodes.new(name) + _spawned_nodes["main"].append(node) + except: + print("EXCEPTION: failed to spawn node with name: ", name) def remove_spawned_nodes(context): @@ -150,19 +184,26 @@ def remove_spawned_nodes(context): return nodes = _spawned_nodes["main"] + N = len(nodes) tree = context.space_data.edit_tree - if DEBUG: - print("There are %d previously spawned nodes to remove" % (len(nodes))) + debug("There are %d previously spawned nodes to remove" % N) + + for i, node in enumerate(nodes): + try: + if node != None: + debug("REMOVING spawned node %d of %d : %s" % (i+1, N, node.bl_idname)) + else: + debug("REMOVING spawned node %d of %d : None" % (i+1, N)) + + except: + print("EXCEPTION: failed to remove spaned node (debug bad access)") - for node in nodes: - if DEBUG: - print("removing spawned node") try: tree.nodes.remove(node) except: - print("exception: failed to remove node from tree") + print("EXCEPTION: failed to remove node from tree") del _spawned_nodes["main"] @@ -182,12 +223,26 @@ def execute(self, context): return {'FINISHED'} +class SvViewAllNodes(bpy.types.Operator): + bl_label = "View All Nodes" + bl_idname = "sv.view_all_nodes" + bl_description = "View all the spawned nodes in the current category" + + def execute(self, context): + bpy.ops.node.select_all(action="SELECT") + bpy.ops.node.view_selected() + for i in range(7): + bpy.ops.view2d.zoom_in() + # bpy.ops.node.select(deselect_all=True) + + return {'FINISHED'} + + class SvDisplayNodePanelProperties(bpy.types.PropertyGroup): def navigate_category(self, direction): - ''' Navigate to Prev or Next category ''' - if DEBUG: - print("Navigate to PREV or NEXT category") + ''' Navigate to PREV or NEXT category ''' + debug("Navigate to PREV or NEXT category") categories = get_category_names() @@ -202,133 +257,201 @@ def navigate_category(self, direction): def category_items(self, context): ''' Get the items to display in the category enum property ''' cache_node_categories() - return _all_categories["categories"]["items"] + return _node_category_cache["categories"]["items"] def arrange_nodes(self, context): ''' Arrange the nodes in current category (using bin-packing) ''' try: nodes = get_spawned_nodes() - if DEBUG: - print("arranging %d nodes" % (len(nodes))) - - max_node_width = 0 - max_node_height = 0 - max_node_area = 0 - for node in nodes: - w = node.dimensions[0] - h = node.dimensions[1] - max_node_width = max(max_node_width, w) - max_node_height = max(max_node_height, h) - max_node_area = max(max_node_height, w * h) + + debug("ARRANGING %d nodes constrained by %s" % (len(nodes), self.constrain_layout)) + + scale = 1.0 / get_dpi_factor() # dpi adjustment scale + + max_node_width = max([node.dimensions.x * scale for node in nodes]) if self.constrain_layout == "HEIGHT": - bins = binpack(nodes, self.grid_height) + # prioritize the layout height to fit all nodes => variable width + bins = binpack(nodes, self.grid_height, self.grid_y_space) elif self.constrain_layout == "WIDTH": - # find the height that has total width less than max width - found = False - height = 100 - while not found: - bins = binpack(nodes, height) - # find max width for current layout - totalWidth = 0 - for bin in bins: - totalWidth = totalWidth + bin.width - - if DEBUG: - print("For height= %d total width = %d" % (height, totalWidth)) - - if totalWidth > max(max_node_width, self.grid_width): - height = height + 10 - # try again with larger height - else: # found it - found = True - else: - # find the height and width closest to the user aspect ratio + # find the height that gives the desired width + max_tries = 11 + num_steps = 0 + min_h = 0 + max_h = 2 * (sum([node.dimensions.y * scale for node in nodes]) + (len(nodes)-1)*self.grid_height) + while num_steps < max_tries: + num_steps = num_steps + 1 + + bin_height = 0.5 * (min_h + max_h) # middle of the height interval + + # get the packed bins for the next bin height + bins = binpack(nodes, bin_height, self.grid_y_space) + + # find the total width for current bin layout + totalWidth = sum([bin.width for bin in bins]) + # add the spacing between bins + totalWidth = totalWidth + self.grid_x_space * (len(bins)-1) + + debug("{0} : min_h = {1:.2f} : max_h = {2:.2f}".format(num_steps, min_h, max_h)) + debug("For bin height = %d total width = %d (%d bins)" % (bin_height, totalWidth, len(bins))) + + delta = abs((self.grid_width - totalWidth)/self.grid_width) + + debug("{0} : target = {1:.2f} : current = {2:.2f} : delta % = {3:.2f}".format( + num_steps, self.grid_width, totalWidth, delta)) + + if delta < 0.1: # converged ? + break + + else: # not found ? => binary search + if self.grid_width < totalWidth: # W < w (make h bigger) + min_h = bin_height + else: # W > w (make h smaller) + max_h = bin_height + + debug("*** FOUND solution in %d steps" % num_steps) + debug("* {} bins of height {} : width {} : space {} ".format(len(bins), + int(bin_height), + int(totalWidth), + (len(bins)-1)*self.grid_x_space + )) + + else: # self.constrain_layout == "ASPECT" + # find the height and width closest to the grid aspect ratio target_aspect = self.grid_width / self.grid_height - found = False - height = 100 - while not found: - bins = binpack(nodes, height) - # find max width for current layout - totalWidth = 0 - for bin in bins: - totalWidth = totalWidth + bin.width + max_tries = 11 + num_steps = 0 + min_h = 0 + max_h = 2 * sum([node.dimensions.y * scale for node in nodes]) + while num_steps < max_tries: + num_steps = num_steps + 1 - if DEBUG: - print("For height= %d total width = %d" % (height, totalWidth)) + bin_height = 0.5 * (min_h + max_h) # middle of the height interval - current_aspect = totalWidth / height + # get the packed bins for the next bin height + bins = binpack(nodes, bin_height, self.grid_y_space) - if current_aspect > target_aspect: - height = height + 10 - # try again with larger height - else: # found it - found = True + # find the max width for current layout + totalWidth = sum([bin.width for bin in bins]) + # add the spacing between bins + totalWidth = totalWidth + self.grid_x_space * (len(bins)-1) - # arrange the nodes in the bins on the grid + debug("{0} : min_h = {1:.2f} : max_h = {2:.2f}".format(num_steps, min_h, max_h)) + debug("For bin height = %d total width = %d" % (bin_height, totalWidth)) + + current_aspect = totalWidth / bin_height + + delta_aspect = abs(current_aspect - target_aspect) + + debug("{0} : target = {1:.2f} : current = {2:.2f} : delta = {3:.2f}".format( + num_steps, target_aspect, current_aspect, delta_aspect)) + + if delta_aspect < 0.1: # converged ? + break + + else: # not found ? => binary search + if target_aspect < current_aspect: # W/H < w/h (make h bigger) + min_h = bin_height + else: # W/H > w/h (make h smaller) + max_h = bin_height + + debug("*** FOUND solution in %d steps" % num_steps) + debug("* {} bins of height {} : width {} : space {} ".format(len(bins), + int(bin_height), + int(totalWidth), + (len(bins)-1)*self.grid_x_space + )) + + max_idname = max([len(node.bl_idname) for node in nodes]) + + # ARRANGE the nodes in the bins on the grid x = 0 for bin in bins: - max_x_width = max([item[0][0] for item in bin.items]) y = 0 for item in bin.items: - node_width = item[0][0] - node_height = item[0][1] - node_name = item[1] - node = item[2] - if DEBUG: - print("node = ", node_name, " : ", node_width, " x ", node_height) - node.location[0] = x + 0.5 * (max_x_width - node_width) + node_width = item.width + node_height = item.height + node_name = item.name + node = item.node + if self.node_alignment == "LEFT": + node.location[0] = x + elif self.node_alignment == "RIGHT": + node.location[0] = x + (bin.width - node_width) + else: # CENTER + node.location[0] = x + 0.5 * (bin.width - node_width) node.location[1] = y - y = y - node_height - self.grid_y_space - x = x + max_x_width + self.grid_x_space + + debug("node = {0:>{x}} : W, H ({1:.1f}, {2:.1f}) & X, Y ({3:.1f}, {4:.1f})".format( + node_name, + node.dimensions.x * scale, node.dimensions.y * scale, + node.location.x, node.location.y, + x=max_idname)) + + y = y - (item.height + self.grid_y_space) + x = x + (bin.width + self.grid_x_space) + except Exception as e: - print('well.. some exception occurred:', str(e)) + print('EXCEPTION: arranging nodes failed:', str(e)) def create_nodes(self, context): # remove the previously spawned nodes remove_spawned_nodes(context) node_names = get_nodes_in_category(self.category) + node_names.sort(reverse=False) - if DEBUG: - print("* current category : ", self.category) - print("* nodes in category : ", node_names) + debug("* current category : %s" % self.category) + debug("* nodes in category : %s" % node_names) N = len(node_names) - # print('There are <%d> nodes in this category' % (N)) + debug('There are <%d> nodes in category <%s>' % (N, self.category)) + if N == 0: return - for i, node_name in enumerate(node_names): - name = node_name + for i, name in enumerate(node_names): if name == "separator": + debug("SKIPPING separator node") continue if is_dependent(name): + debug("SKIPPING dependent node %d of %d : %s" % (i+1, N, name)) continue if '@' in name: + debug("SKIPPING subcategory node: %s" % name) continue - if DEBUG: - print("Spawning Node %d : %s" % (i, name)) - node = add_spawned_node(context, name) + debug("SPAWNING node %d of %d : %s" % (i+1, N, name)) + + add_spawned_node(context, name) # force redraw to update the node dimensions - bpy.ops.wm.redraw_timer(type='DRAW_WIN_SWAP', iterations=1) + bpy.ops.wm.redraw_timer(type='DRAW_WIN', iterations=1) + bpy.context.area.tag_redraw() def update_category(self, context): - ''' - Spawn all nodes in the selected category - note: all previously spawned nodes are removed first - ''' + """ + Spawn all nodes in the selected category + note: all previously spawned nodes are removed first + """ self.create_nodes(context) self.arrange_nodes(context) + # update the total/hidden node count (e.g. nodes with dependency are hidden) + node_names = get_nodes_in_category(self.category) + self.total_num_nodes = len(node_names) + other_node_names = [name for name in node_names if is_dependent(name)] + self.hidden_num_nodes = len(other_node_names) + constrain_layout: EnumProperty( name="Constrain Layout", default="ASPECT", - items=constrainLayoutItems, update=arrange_nodes) + items=constrain_layout_items, update=arrange_nodes) + + node_alignment: EnumProperty( + name="Node Alignment", default="CENTER", + items=node_alignment_items, update=arrange_nodes) category: EnumProperty( name="Category", @@ -350,6 +473,16 @@ def update_category(self, context): name="Grid Y spacing", default=20, update=arrange_nodes) + total_num_nodes: IntProperty( + name="Total number of nodes", + description="Total number of nodes in the current category", + default=0) + + hidden_num_nodes: IntProperty( + name="Hidden number of nodes", + description="Number of nodes hidden in the current category", + default=0) + class SV_PT_DisplayNodesPanel(bpy.types.Panel): bl_idname = "SV_PT_DisplayNodesPanel" @@ -376,34 +509,56 @@ def poll(cls, context): def draw(self, context): layout = self.layout displayProps = context.space_data.node_tree.displayNodesProps - split = layout.split(factor=0.7, align=True) + box = layout.box() + split = box.split(factor=0.7, align=True) c1 = split.column(align=True) c2 = split.column(align=True) c1.prop(displayProps, "category", text="") row = c2.row(align=True) row.operator("sv.navigate_category", icon="PLAY_REVERSE", text=" ").direction = 0 row.operator("sv.navigate_category", icon="PLAY", text=" ").direction = 1 - row = layout.row() + row = box.row() + row.operator("sv.view_all_nodes") + # node count info + label = "Total: {} - Shown: {} - Hidden: {}".format(displayProps.total_num_nodes, + displayProps.total_num_nodes - + displayProps.hidden_num_nodes, + displayProps.hidden_num_nodes) + box.label(text=label) + # grid settings + box = layout.box() + col = box.column(align=True) + row = col.row() row.prop(displayProps, 'constrain_layout', expand=True) - col = layout.column(align=True) + row = col.row() + row.prop(displayProps, 'node_alignment', expand=True) + col = box.column(align=True) col.prop(displayProps, 'grid_width') col.prop(displayProps, 'grid_height') - col = layout.column(align=True) + col = box.column(align=True) col.prop(displayProps, 'grid_x_space') col.prop(displayProps, 'grid_y_space') +classes = [SvNavigateCategory, SvViewAllNodes, SV_PT_DisplayNodesPanel, SvDisplayNodePanelProperties] + + def register(): - bpy.utils.register_class(SvNavigateCategory) - bpy.utils.register_class(SV_PT_DisplayNodesPanel) - bpy.utils.register_class(SvDisplayNodePanelProperties) + for cls in classes: + bpy.utils.register_class(cls) + bpy.types.NodeTree.displayNodesProps = PointerProperty( name="displayNodesProps", type=SvDisplayNodePanelProperties) + cache_node_categories() def unregister(): del bpy.types.NodeTree.displayNodesProps - bpy.utils.unregister_class(SV_PT_DisplayNodesPanel) - bpy.utils.unregister_class(SvDisplayNodePanelProperties) - bpy.utils.unregister_class(SvNavigateCategory) + + for cls in classes: + bpy.utils.unregister_class(cls) + + +if __name__ == '__main__': + register() diff --git a/ui/sv_panels.py b/ui/sv_panels.py index d79f024ec6..d8b31864ae 100644 --- a/ui/sv_panels.py +++ b/ui/sv_panels.py @@ -131,15 +131,15 @@ class SV_PT_SverchokUtilsPanel(SverchokPanels, bpy.types.Panel): def draw(self, context): col = self.layout.column() col.operator('node.sv_show_latest_commits') - - if context.scene.sv_new_version: - col_alert = self.layout.column() - col_alert.alert = True - col_alert.operator("node.sverchok_update_addon", text='Upgrade Sverchok addon') - else: - col.operator("node.sverchok_check_for_upgrades_wsha", text='Check for updates') - with sv_preferences() as prefs: + if prefs.available_new_version: + col_alert = self.layout.column() + col_alert.alert = True + col_alert.operator("node.sverchok_update_addon", text='Upgrade Sverchok addon') + else: + col.operator("node.sverchok_check_for_upgrades_wsha", text='Check for updates') + + # with sv_preferences() as prefs: if prefs.developer_mode: col.operator("node.sv_run_pydoc") @@ -319,7 +319,6 @@ def register(): bpy.utils.register_class(class_name) bpy.types.Scene.ui_list_selected_tree = bpy.props.IntProperty() # Pointer to selected item in list of trees - bpy.types.Scene.sv_new_version = bpy.props.BoolProperty(default=False) bpy.types.NODE_HT_header.append(node_show_tree_mode) bpy.types.VIEW3D_HT_header.append(view3d_show_live_mode) @@ -327,7 +326,6 @@ def register(): def unregister(): del bpy.types.Scene.ui_list_selected_tree - del bpy.types.Scene.sv_new_version for class_name in reversed(sv_tools_classes): bpy.utils.unregister_class(class_name) diff --git a/utils/__init__.py b/utils/__init__.py index 5c8596dd91..e78439bdf3 100644 --- a/utils/__init__.py +++ b/utils/__init__.py @@ -17,7 +17,7 @@ def unregister_node_class(class_ref): def register_node_classes_factory(node_class_references, ops_class_references=None): """ - + !!!! Unless you are testing/developing a node, you do not need to use this. ever. !!!! @@ -53,16 +53,16 @@ def register(): def unregister(): for cls in reversed(ops_class_references): - bpy.utils.unregister_class(cls) + bpy.utils.unregister_class(cls) for cls in reversed(node_class_references): unregister_node_class(cls) return register, unregister def auto_gather_node_classes(start_module = None): - """ - this produces a dict with mapping from bl_idname to class reference at runtime - f.ex + """ + this produces a dict with mapping from bl_idname to class reference at runtime + f.ex node_classes = {SvBMeshViewerMk2: , .... } """ @@ -109,12 +109,12 @@ def app_handler_ops(append=None, remove=None): (operation, handler_dict) = ('append', append) if append else ('remove', remove) for handler_name, handler_function in handler_dict.items(): handler = getattr(bpy.app.handlers, handler_name) - + # bpy.app.handlers..(function_name) getattr(handler, operation)(handler_function) # try: - # names = lambda d: [f" {k} -> {v.__name__}" for k, v in d.items()] + # names = lambda d: [f" {k} -> {v.__name__}" for k, v in d.items()] # listed = "\n".join(names(handler_dict)) # except Exception as err: # print('error while listing event handlers', err) @@ -146,5 +146,8 @@ def app_handler_ops(append=None, remove=None): # geom 2d tools "geom_2d.lin_alg", "geom_2d.dcel", "geom_2d.dissolve_mesh", "geom_2d.merge_mesh", "geom_2d.intersections", "geom_2d.make_monotone", "geom_2d.sort_mesh", "geom_2d.dcel_debugger", - "quad_grid" + "quad_grid", + # extra addons + "sv_extra_addons" + ] diff --git a/utils/bvh_tree.py b/utils/bvh_tree.py new file mode 100644 index 0000000000..9d8ad98dd4 --- /dev/null +++ b/utils/bvh_tree.py @@ -0,0 +1,20 @@ +# This file is part of project Sverchok. It's copyrighted by the contributors +# recorded in the version control history of the file, available from +# its original location https://github.com/nortikin/sverchok/commit/master +# +# SPDX-License-Identifier: GPL3 +# License-Filename: LICENSE + +from mathutils.bvhtree import BVHTree + +def bvh_safe_check(verts, pols): + len_v = len(verts) + for p in pols: + for c in p: + if c > len_v: + raise Exception(f"Index {c} should be less than vertices length ({len_v})") + +def bvh_tree_from_polygons(vertices, polygons, all_triangles=False, epsilon=0.0, safe_check=True): + if safe_check: + bvh_safe_check(vertices, polygons) + return BVHTree.FromPolygons(vertices, polygons, all_triangles=all_triangles, epsilon=epsilon) diff --git a/utils/context_managers.py b/utils/context_managers.py index 2527c12fff..436481b880 100644 --- a/utils/context_managers.py +++ b/utils/context_managers.py @@ -18,3 +18,19 @@ def sv_preferences(): addon = bpy.context.preferences.addons.get(sverchok.__name__) if addon and hasattr(addon, "preferences"): yield addon.preferences + +@contextmanager +def addon_preferences(addon_name): + ''' + use this whenever you need set or get content of the preferences class + usage + from sverchok.utils.context_managers import addon_preferences + ... + with addon_preferences(addon_name) as prefs: + print(prefs.) + + addon_name sverchok passing sverchok.__name__ + ''' + addon = bpy.context.preferences.addons.get(addon_name) + if addon and hasattr(addon, "preferences"): + yield addon.preferences diff --git a/utils/extra_categories.py b/utils/extra_categories.py index 8785fa261a..e6f2d0419f 100644 --- a/utils/extra_categories.py +++ b/utils/extra_categories.py @@ -17,6 +17,11 @@ # # ##### END GPL LICENSE BLOCK ##### +import os +from os.path import exists, isfile +import inspect +import webbrowser + extra_category_providers = [] def register_extra_category_provider(provider): @@ -40,3 +45,29 @@ def get_extra_categories(): result.extend(provider.get_categories()) return result +def external_node_docs(operator, node, kind): + + node_file_path = inspect.getfile(node.__class__) + separator = node_file_path.rfind('nodes') + local_file_path = os.path.join(node_file_path[0:separator], 'docs', node_file_path[separator:-2]+'rst') + + valid = exists(local_file_path) and isfile(local_file_path) + + if valid: + if kind == 'offline': + webbrowser.open(local_file_path) + else: + f_direc = node.__module__.split('.') + + extra_cats = extra_category_providers + for cat in extra_cats: + + if f_direc[0] in cat.identifier.lower(): + if hasattr(cat, 'docs'): + link = cat.docs+'/'+ f_direc[1]+'/'+ f_direc[2]+'/'+ f_direc[3] + '.rst' + print('Docs found ', cat.identifier, cat.docs) + webbrowser.open(link) + break + return {'FINISHED'} + operator.report({'ERROR'}, "Not available documentation") + return {'CANCELLED'} diff --git a/utils/modules/statistics_functions.py b/utils/modules/statistics_functions.py index 193c5b61cb..c111185d8f 100644 --- a/utils/modules/statistics_functions.py +++ b/utils/modules/statistics_functions.py @@ -21,6 +21,10 @@ import sys +def get_count(values): + return len(values) + + def get_sum(values): return sum(values) @@ -30,7 +34,10 @@ def get_sum_of_squares(values): def get_sum_of_inversions(values): - return sum([1.0 / v for v in values]) + try: + return sum([1.0 / v for v in values]) + except ZeroDivisionError: + return 0 def get_product(values): @@ -46,12 +53,23 @@ def get_geometric_mean(values): def get_harmonic_mean(values): - return len(values) / get_sum_of_inversions(values) + try: + return len(values) / get_sum_of_inversions(values) + except ZeroDivisionError: + return 0 -def get_standard_deviation(values): +def get_variance(values): a = get_average(values) - return sqrt(sum([(v - a)**2 for v in values])) + return sum([(v - a)**2 for v in values]) / len(values) + + +def get_standard_deviation(values): + return sqrt(get_variance(values)) + + +def get_standard_error(values): + return get_standard_deviation(values) / sqrt(len(values)) def get_root_mean_square(values): @@ -62,14 +80,20 @@ 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) + try: + return sum([(v - a)**3 for v in values]) / n / pow(s, 3) + except ZeroDivisionError: + return 0 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) + try: + return sum([(v - a)**4 for v in values]) / n / pow(s, 4) + except ZeroDivisionError: + return 0 def get_minimum(values): @@ -80,10 +104,13 @@ def get_maximum(values): return max(values) +def get_range(values): + return max(values) - min(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 diff --git a/utils/sv_bmesh_utils.py b/utils/sv_bmesh_utils.py index bc9123bc7d..2641282774 100644 --- a/utils/sv_bmesh_utils.py +++ b/utils/sv_bmesh_utils.py @@ -155,7 +155,7 @@ def add_mesh_to_bmesh(bm, verts, edges=None, faces=None, sv_index_name=None, upd def numpy_data_from_bmesh(bm, out_np, face_data=None): if out_np[0]: - verts = np.array([v.co[:] for v in bm.verts]) + verts = np.array([v.co for v in bm.verts]) else: verts = [v.co[:] for v in bm.verts] if out_np[1]: @@ -850,4 +850,3 @@ def recalc_normals(verts, edges, faces, loop=False): verts, edges, faces = pydata_from_bmesh(bm) bm.free() return verts, edges, faces - diff --git a/utils/sv_extra_addons.py b/utils/sv_extra_addons.py new file mode 100644 index 0000000000..edc6279715 --- /dev/null +++ b/utils/sv_extra_addons.py @@ -0,0 +1,183 @@ +# ##### 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 +import os +import urllib +import urllib.request +from zipfile import ZipFile +import addon_utils + + +import bpy +import sverchok +from sverchok.utils import sv_requests as requests + + + +EXTRA_ADDONS = { +'Sverchok Extra': { + 'name': 'sverchok_extra', + 'description': 'Experimental Nodes on Surfaces, Curves, Solids and Data managment', + 'archive_link':'https://github.com/portnov/sverchok-extra/archive/', + 'branch': 'master', + 'self_instalable': True + }, +'Sverchok Open3d': { + 'name': 'sverchok_open3d', + 'description': 'Point Cloud and Triangle Mesh nodes', + 'archive_link':'https://github.com/vicdoval/sverchok-open3d/archive/', + 'branch': 'master', + 'self_instalable': True + } +} + +ARCHIVE_LINK = 'https://github.com/nortikin/sverchok/archive/' +MASTER_BRANCH_NAME = 'master' + +def sv_get_local_path(): + script_paths = os.path.normpath(os.path.dirname(__file__)) + addons_path = os.path.split(os.path.dirname(script_paths))[0] + + return addons_path + +bl_addons_path = sv_get_local_path() +import importlib + + +def get_addons_folder(): + addons ={} + for p in bpy.utils.script_paths(): + p2 = os.path.join(p, 'addons') + if os.path.exists(p2): + addons.update({name: os.path.join(p2, name) for name in os.listdir(p2) if os.path.isdir(os.path.join(p2, name))}) + return addons + + +ADDON_LIST = get_addons_folder() + +def draw_extra_addons(layout): + for pretty_name in EXTRA_ADDONS: + addon = EXTRA_ADDONS[pretty_name] + + b = layout.box() + b.label(text=pretty_name) + if addon['name'] in ADDON_LIST: + addon_name = addon['name'] + elif addon['name'].replace('_', '-') in ADDON_LIST: + addon_name = addon['name'].replace('_', '-') + elif addon['name']+'-'+ addon['branch'] in ADDON_LIST: + addon_name = addon['name']+'-'+ addon['branch'] + elif addon['name'].replace('_', '-')+'-'+ addon['branch'] in ADDON_LIST: + addon_name = addon['name'].replace('_', '-')+'-'+ addon['branch'] + else: + addon_name = False + print(addon_name) + if addon_name: + loaded_default, loaded_state = addon_utils.check(addon_name) + if loaded_state: + op = b.operator('preferences.addon_disable') + op.module = addon_name + try: + module = importlib.import_module(addon['name']) + if hasattr(module, 'settings'): + if hasattr(module.settings, 'draw_in_sv_prefs'): + module.settings.draw_in_sv_prefs(b) + if hasattr(module.settings, 'update_addon_ui'): + row = b.row() + module.settings.update_addon_ui(row) + except ModuleNotFoundError: + addon_name = False + else: + op = b.operator('preferences.addon_enable') + op.module = addon_name + if not addon_name: + b.label(text=addon['description']) + op = b.operator('node.sverchok_download_addon', text=f'Download {pretty_name}') + op.master_branch_name = addon['branch'] + op.archive_link = addon['archive_link'] + +class SverchokDownloadExtraAddon(bpy.types.Operator): + """ Download Sverchok Extra Addon. After completion press F8 to reload addons or restart Blender """ + bl_idname = "node.sverchok_download_addon" + bl_label = "Sverchok update addon" + bl_options = {'REGISTER'} + + master_branch_name: bpy.props.StringProperty(default=MASTER_BRANCH_NAME) + archive_link: bpy.props.StringProperty(default=ARCHIVE_LINK) + + def execute(self, context): + global ADDON_LIST + os.curdir = bl_addons_path + os.chdir(os.curdir) + + # wm = bpy.context.window_manager should be this i think.... + wm = bpy.data.window_managers[0] + wm.progress_begin(0, 100) + wm.progress_update(20) + + # dload_archive_path, dload_archive_name = get_archive_path(self.addon_name) + + try: + branch_name = self.master_branch_name + branch_origin = self.archive_link + zipname = '{0}.zip'.format(branch_name) + url = branch_origin + zipname + + to_path = os.path.normpath(os.path.join(os.curdir, zipname)) + + print('> obtaining: [{0}]\n> sending to path: [{1}]'.format(url, to_path)) + # return {'CANCELLED'} + + file = requests.urlretrieve(url, to_path) + wm.progress_update(50) + except Exception as err: + self.report({'ERROR'}, "Cannot get archive from Internet") + print(err) + wm.progress_end() + return {'CANCELLED'} + + try: + err = 0 + ZipFile(file[0]).extractall(path=os.curdir, members=None, pwd=None) + wm.progress_update(90) + err = 1 + os.remove(file[0]) + err = 2 + wm.progress_update(100) + wm.progress_end() + bpy.ops.preferences.addon_refresh() + ADDON_LIST = get_addons_folder() + self.report({'INFO'}, "Unzipped, reload addons with F8 button, maybe restart Blender") + except: + self.report({'ERROR'}, "Cannot extract files errno {0}".format(str(err))) + wm.progress_end() + os.remove(file[0]) + return {'CANCELLED'} + + return {'FINISHED'} + + + +def register(): + + bpy.utils.register_class(SverchokDownloadExtraAddon) + + +def unregister(): + + bpy.utils.unregister_class(SverchokDownloadExtraAddon) diff --git a/utils/sv_manual_curves_utils.py b/utils/sv_manual_curves_utils.py index 17d82184ac..7fdecfae8e 100644 --- a/utils/sv_manual_curves_utils.py +++ b/utils/sv_manual_curves_utils.py @@ -109,7 +109,7 @@ def get_rgb_curve(group_name, node_name): def set_rgb_curve(data_dict, curve_node_name): ''' - stores RGB Curves data into json + stores RGB Curves data into json ''' group_name = data_dict['group_name'] @@ -130,3 +130,18 @@ def set_rgb_curve(data_dict, curve_node_name): curve.points[pidx].handle_type = handle_type curve.points[pidx].location = location node.mapping.update() + +def get_points_from_curve(group_name, node_name): + ''' + get control points from curve + ''' + node_groups = bpy.data.node_groups + group = node_groups.get(group_name) + node = group.nodes.get(node_name) + + out_list = [] + + points = node.mapping.curves[-1].points + out_list.append([[p.location[0], p.location[1], 0] for p in points]) + + return out_list diff --git a/utils/sv_path_utils.py b/utils/sv_path_utils.py new file mode 100644 index 0000000000..d7c7bb5405 --- /dev/null +++ b/utils/sv_path_utils.py @@ -0,0 +1,41 @@ +# ##### 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 os + + +def get_addons_path(): + script_paths = os.path.normpath(os.path.dirname(__file__)) + addons_path = os.path.split(os.path.dirname(script_paths))[0] + return addons_path + + +def get_sverchok_path(): + script_paths = os.path.normpath(os.path.dirname(__file__)) # /path/to/sverchock/utils + sverchok_path = os.path.dirname(script_paths) # /path/to/sverchock + return sverchok_path + + +def get_icons_path(): + icons_path = os.path.join(get_sverchok_path(), "ui", "icons") + return icons_path + + +def get_fonts_path(): + fonts_path = os.path.join(get_sverchok_path(), "ui", "fonts") + return fonts_path diff --git a/utils/sv_stethoscope_helper.py b/utils/sv_stethoscope_helper.py index 73d4aa3efa..b0ff476f72 100644 --- a/utils/sv_stethoscope_helper.py +++ b/utils/sv_stethoscope_helper.py @@ -1,7 +1,7 @@ # This file is part of project Sverchok. It's copyrighted by the contributors # recorded in the version control history of the file, available from # its original location https://github.com/nortikin/sverchok/commit/master -# +# # SPDX-License-Identifier: GPL3 # License-Filename: LICENSE @@ -28,12 +28,12 @@ def draw_text_data(data): lines = data.get('content', 'no data') x, y = get_sane_xy(data) - + x, y = int(x), int(y) r, g, b = data.get('color', (0.1, 0.1, 0.1)) font_id = data.get('font_id', 0) scale = data.get('scale', 1.0) - + text_height = 15 * scale line_height = 14 * scale @@ -42,7 +42,7 @@ def draw_text_data(data): ypos = y for line in lines: - blf.position(0, x, ypos, 0) + blf.position(font_id, x, ypos, 0) blf.draw(font_id, line) ypos -= int(line_height * 1.3) @@ -59,7 +59,7 @@ def draw_graphical_data(data): return blf.size(font_id, int(text_height), 72) - + def draw_text(color, xpos, ypos, line): r, g, b = color blf.color(font_id, r, g, b, 1.0) # bgl.glColor3f(*color) @@ -78,7 +78,7 @@ def draw_text(color, xpos, ypos, line): tx, _ = draw_text(color, gfx_x, y_pos, f"{kind_of_item} of {num_items} items") gfx_x += (tx + 5) - + content_dict = defaultdict(int) for item in line: content_dict[type(item).__name__] += 1 diff --git a/utils/sv_update_utils.py b/utils/sv_update_utils.py index 7b56b643a9..543a712877 100644 --- a/utils/sv_update_utils.py +++ b/utils/sv_update_utils.py @@ -24,8 +24,16 @@ import bpy import sverchok from sverchok.utils import sv_requests as requests - +from sverchok.utils.context_managers import sv_preferences, addon_preferences # pylint: disable=w0141 +ADDON_NAME = sverchok.__name__ +COMMITS_LINK = 'https://api.github.com/repos/nortikin/sverchok/commits' + +SHA_FILE = 'sv_shafile.sv' +SHA_DOWNLOADED = 'sv_sha_downloaded.sv' + +ARCHIVE_LINK = 'https://github.com/nortikin/sverchok/archive/' +MASTER_BRANCH_NAME = 'master' def sv_get_local_path(): script_paths = os.path.normpath(os.path.dirname(__file__)) @@ -37,64 +45,64 @@ def sv_get_local_path(): -def get_sha_filepath(filename='sv_shafile.sv'): - """ the act if calling this function should produce a file called +def get_sha_filepath(filename=SHA_FILE, addon_name=ADDON_NAME): + """ the act if calling this function should produce a file called ../datafiles/sverchok/sv_shafile.sv (or sv_sha_downloaded.sv) the location of datafiles is common for Blender apps and defined internally for each OS. returns: the path of this file """ - dirpath = os.path.join(bpy.utils.user_resource('DATAFILES', path='sverchok', create=True)) + dirpath = os.path.join(bpy.utils.user_resource('DATAFILES', path=addon_name, create=True)) fullpath = os.path.join(dirpath, filename) - + # create fullpath if it doesn't exist if not os.path.exists(fullpath): with open(fullpath, 'w') as _: pass - + return fullpath -def latest_github_sha(): +def latest_github_sha(commits_link): """ get sha produced by latest commit on github sha = latest_github_sha() print(sha) """ - r = requests.get('https://api.github.com/repos/nortikin/sverchok/commits') + r = requests.get(commits_link) json_obj = r.json() return os.path.basename(json_obj[0]['commit']['url']) -def latest_local_sha(filename='sv_shafile.sv'): +def latest_local_sha(filename=SHA_FILE, addon_name=ADDON_NAME): """ get previously stored sha, if any. finding no local sha will return empty string reads from ../datafiles/sverchok/sv_shafile.sv """ - filepath = get_sha_filepath(filename) + filepath = get_sha_filepath(filename, addon_name=addon_name) with open(filepath) as p: return p.read() -def write_latest_sha_to_local(sha_value='', filename='sv_shafile.sv'): - """ write the content of sha_value to +def write_latest_sha_to_local(sha_value='', filename=SHA_FILE, addon_name=ADDON_NAME): + """ write the content of sha_value to ../datafiles/sverchok/sv_shafile.sv """ - filepath = get_sha_filepath(filename) + filepath = get_sha_filepath(filename, addon_name=addon_name) with open(filepath, 'w') as p: p.write(sha_value) -def make_version_sha(): +def make_version_sha(addon_name=ADDON_NAME): """ Generate a string to represent sverchok version including sha if found returns: 0.5.9.13 (a3bcd34) (or something like that) """ sha_postfix = '' - sha = latest_local_sha(filename='sv_sha_downloaded.sv') + sha = latest_local_sha(filename=SHA_DOWNLOADED, addon_name=addon_name) if sha: sha_postfix = " (" + sha[:7] + ")" @@ -108,38 +116,38 @@ class SverchokCheckForUpgradesSHA(bpy.types.Operator): bl_idname = "node.sverchok_check_for_upgrades_wsha" bl_label = "Sverchok check for new minor version" bl_options = {'REGISTER'} - + addon_name: bpy.props.StringProperty(default=ADDON_NAME) + commits_link: bpy.props.StringProperty(default=COMMITS_LINK) def execute(self, context): report = self.report - context.scene.sv_new_version = False - - local_sha = latest_local_sha() - latest_sha = latest_github_sha() - - # this logic can be simplified. - if not local_sha: - context.scene.sv_new_version = True - else: - if not local_sha == latest_sha: - context.scene.sv_new_version = True - - write_latest_sha_to_local(sha_value=latest_sha) - downloaded_sha = latest_local_sha(filename='sv_sha_downloaded.sv') - - if not downloaded_sha == latest_sha: - context.scene.sv_new_version = True - - if context.scene.sv_new_version: - report({'INFO'}, "New commits available, update at own risk ({0})".format(latest_sha[:7])) - else: - report({'INFO'}, "No new commits to download") + + local_sha = latest_local_sha(addon_name=self.addon_name) + latest_sha = latest_github_sha(self.commits_link) + + with addon_preferences(self.addon_name) as prefs: + prefs.available_new_version = False + if not local_sha: + prefs.available_new_version = True + else: + if not local_sha == latest_sha: + prefs.available_new_version = True + + write_latest_sha_to_local(sha_value=latest_sha, addon_name=self.addon_name) + downloaded_sha = latest_local_sha(filename=SHA_DOWNLOADED, addon_name=self.addon_name) + if not downloaded_sha == latest_sha: + prefs.available_new_version = True + + if prefs.available_new_version: + report({'INFO'}, "New commits available, update at own risk ({0})".format(latest_sha[:7])) + else: + report({'INFO'}, "No new commits to download") + return {'FINISHED'} -def get_archive_path(): - from sverchok.utils.context_managers import sv_preferences - with sv_preferences() as prefs: +def get_archive_path(addon_name): + with addon_preferences(addon_name) as prefs: return prefs.dload_archive_path, prefs.dload_archive_name @@ -148,6 +156,9 @@ class SverchokUpdateAddon(bpy.types.Operator): bl_idname = "node.sverchok_update_addon" bl_label = "Sverchok update addon" bl_options = {'REGISTER'} + addon_name: bpy.props.StringProperty(default=ADDON_NAME) + master_branch_name: bpy.props.StringProperty(default=MASTER_BRANCH_NAME) + archive_link: bpy.props.StringProperty(default=ARCHIVE_LINK) def execute(self, context): @@ -159,11 +170,11 @@ def execute(self, context): wm.progress_begin(0, 100) wm.progress_update(20) - dload_archive_path, dload_archive_name = get_archive_path() + dload_archive_path, dload_archive_name = get_archive_path(self.addon_name) try: - branch_name = dload_archive_name or 'master' - branch_origin = dload_archive_path or 'https://github.com/nortikin/sverchok/archive/' + branch_name = dload_archive_name or self.master_branch_name + branch_origin = dload_archive_path or self.archive_link zipname = '{0}.zip'.format(branch_name) url = branch_origin + zipname @@ -179,7 +190,7 @@ def execute(self, context): print(err) wm.progress_end() return {'CANCELLED'} - + try: err = 0 ZipFile(file[0]).extractall(path=os.curdir, members=None, pwd=None) @@ -187,7 +198,8 @@ def execute(self, context): err = 1 os.remove(file[0]) err = 2 - bpy.context.scene.sv_new_version = False + with addon_preferences(self.addon_name) as prefs: + prefs.available_new_version = False wm.progress_update(100) wm.progress_end() self.report({'INFO'}, "Unzipped, reload addons with F8 button, maybe restart Blender") @@ -198,8 +210,9 @@ def execute(self, context): return {'CANCELLED'} # write to both sv_sha_download and sv_shafile.sv - write_latest_sha_to_local(sha_value=latest_local_sha(), filename='sv_sha_downloaded.sv') - write_latest_sha_to_local(sha_value=latest_local_sha()) + lastest_local_sha = latest_local_sha(addon_name=self.addon_name) + write_latest_sha_to_local(sha_value=lastest_local_sha, filename=SHA_DOWNLOADED, addon_name=self.addon_name) + write_latest_sha_to_local(sha_value=lastest_local_sha, addon_name=self.addon_name) return {'FINISHED'} @@ -207,9 +220,10 @@ class SvPrintCommits(bpy.types.Operator): """ show latest commits in info panel, and terminal """ bl_idname = "node.sv_show_latest_commits" bl_label = "Show latest commits" + commits_link: bpy.props.StringProperty(default=COMMITS_LINK) def execute(self, context): - r = requests.get('https://api.github.com/repos/nortikin/sverchok/commits') + r = requests.get(self.commits_link) json_obj = r.json() for i in range(5): commit = json_obj[i]['commit']