diff --git a/data_structure.py b/data_structure.py index fbc32660d15364c5018126cf6c1776e164c1fa21..aabe229c15f1f262385aad35efa6cf4729628c1e 100755 --- a/data_structure.py +++ b/data_structure.py @@ -927,3 +927,52 @@ def std_links_processing(matcher): return real_process return decorator + + +# EDGE CACHE settings : used to accellerate the (linear) edge list generation +_edgeCache = {} +_edgeCache["main"] = [] # e.g. [[0, 1], [1, 2], ... , [N-1, N]] (extended as needed) + + +def update_edge_cache(n): + """ + Extend the edge list cache to contain at least n edges. + + NOTE: This is called by the get_edge_list to make sure the edge cache is large + enough, but it can also be called preemptively by the nodes prior to making + multiple calls to get_edge_list in order to pre-augment the cache to a known + size and thus accellearate the subsequent calls to get_edge_list as they + will not have to augment the cache with every call. + """ + m = len(_edgeCache["main"]) # current number of edges in the edge cache + if n > m: # requested #edges < cached #edges ? => extend the cache + _edgeCache["main"].extend([[m + i, m + i + 1] for i in range(n - m)]) + + +def get_edge_list(n): + """ + Get the list of n edges connecting n+1 vertices. + + e.g. [[0, 1], [1, 2], ... , [n-1, n]] + + NOTE: This uses an "edge cache" to accellerate the edge list generation. + The cache is extended automatically as needed to satisfy the largest number + of edges within the node tree and it is shared by all nodes using this method. + """ + update_edge_cache(n) # make sure the edge cache is large enough + return _edgeCache["main"][:n] # return a subset list of the edge cache + + +def get_edge_loop(n): + """ + Get the loop list of n edges connecting n vertices. + + e.g. [[0, 1], [1, 2], ... , [n-2, n-1], [n-1, 0]] + + NOTE: This uses an "edge cache" to accellerate the edge list generation. + The cache is extended automatically as needed to satisfy the largest number + of edges within the node tree and it is shared by all nodes using this method. + """ + nn = n - 1 + update_edge_cache(nn) # make sure the edge cache is large enough + return _edgeCache["main"][:nn] + [[nn, 0]] diff --git a/docs/nodes/analyzers/analyzers_index.rst b/docs/nodes/analyzers/analyzers_index.rst index 0d5d54f98379c3993c49a721f1e8532c06603830..8f5788721601ebcf566ab6877f5f9268ff9e096c 100644 --- a/docs/nodes/analyzers/analyzers_index.rst +++ b/docs/nodes/analyzers/analyzers_index.rst @@ -8,7 +8,10 @@ Analyzers area bbox distance_pp + deformation edge_angles + evaluate_image + image_components kd_tree kd_tree_edges_mk2 mesh_filter diff --git a/docs/nodes/analyzers/deformation.rst b/docs/nodes/analyzers/deformation.rst new file mode 100644 index 0000000000000000000000000000000000000000..fdad47f6d47ac9b7083c64a31470546828673bfc --- /dev/null +++ b/docs/nodes/analyzers/deformation.rst @@ -0,0 +1,54 @@ +Deformation +=========== + +Functionality +------------- + +Deformation node is one of the analyzer type. It is used to get the deformation of one or many meshes between different states. The deformation is measured by the areal variance or the edge elongation + + +Inputs +------ + +All inputs need to proceed from an external node + + ++-------------------+---------------+-------------+-----------------------------------------------+ +| Input | Type | Default | Description | ++===================+===============+=============+===============================================+ +| **Rest Verts** | Vertices | None | Vertices in relaxed state | ++-------------------+---------------+-------------+-----------------------------------------------+ +| **Distort Verts** | Vertices | None | Vertices in the deformed state | ++-------------------+---------------+-------------+-----------------------------------------------+ +| **Pols** | Strings | None | Polygons referenced to vertices | ++-------------------+---------------+-------------+-----------------------------------------------+ +| **Edges** | Strings | None | Edges referenced to vertices | ++-------------------+---------------+-------------+-----------------------------------------------+ + +In the N-Panel you can use the toggle **Output NumPy** to get NumPy arrays (makes the node faster) + +Outputs +------- +- **Edges Def** :the variation of the length of the edges. +- **Pols Def** : the variation of the areas of each polygon. + +- **Vert Pol Def**: Each polygon will distribute its areal variation to its vertices. Each vertex will get the sum of the deformations related to itself. +- **Vert Edge Def**: Each edge will distribute its elongation to its vertices. Each vertex will get the sum of elongations related to itself. + + +Examples of usage +---------------- +Different kinds of tension maps can be created using this node, from any of its outputs, here some basic examples: + +.. image:: https://user-images.githubusercontent.com/10011941/50576192-a7da2a80-0e0c-11e9-9be5-e490081822bb.png + :alt: DefromationNode1.PNG + +The elongation can be used to get to the tension of a spring system, in this example the node is used to get one approximation of the needed thickness to sustain the tensions applied to a cloth simulation. + +.. image:: https://user-images.githubusercontent.com/10011941/50576196-ba546400-0e0c-11e9-8c5c-15488c9a0d04.png + :alt: DefromationNode2.PNG + +You can compare many different states at the same time. + +.. image:: https://user-images.githubusercontent.com/10011941/50576199-d526d880-0e0c-11e9-89cf-12cd8462da41.png + :alt: DefromationNode3.PNG diff --git a/docs/nodes/generator/line_mk3.rst b/docs/nodes/generator/line_mk3.rst new file mode 100644 index 0000000000000000000000000000000000000000..f1827eada643912d5a3e2a9e64133e768aa6a322 --- /dev/null +++ b/docs/nodes/generator/line_mk3.rst @@ -0,0 +1,84 @@ +Line +==== + +Functionality +------------- + +Line generator creates a series of connected segments based on the number of vertices and the length between them. Just a standard subdivided line along X axis + +Inputs +------ + +All parameters except **Center** are vectorized. They will accept single or multiple values. +Both inputs will accept a single number or an array of them. It also will work an array of arrays:: + + [2] + [2, 4, 6] + [[2], [4]] + +Parameters +---------- + +All parameters except **Center** can be given by the node or an external input. + + ++---------------+---------------+--------------+---------------------------------------------------------+ +| Param | Type | Default | Description | ++===============+===============+==============+=========================================================+ +| **Direction** | Enum | "X" | Ortho direction, "from A to B" or "Origin and Direction"| ++---------------+---------------+--------------+---------------------------------------------------------+ +| **N Verts** | Int | 2 | number of vertices. The minimum is 2 | ++---------------+---------------+--------------+---------------------------------------------------------+ +| **Step** | Float | 1.00 | length between vertices | ++---------------+---------------+--------------+---------------------------------------------------------+ +| **Center** | Boolean     | False       | center line around 0 | ++---------------+---------------+--------------+---------------------------------------------------------+ +| **Normalize** | Boolean     | False       | determine steps by fixing total length line | ++---------------+---------------+--------------+---------------------------------------------------------+ +| **Size** | Float     | 10     | total length of the segment | ++---------------+---------------+--------------+---------------------------------------------------------+ +| **A, O** | Vector     | (0,0,0)     | origin point of line | ++---------------+---------------+--------------+---------------------------------------------------------+ +| **B** | Vector     | (0.5,0.5,0.5)| end point of line | ++---------------+---------------+--------------+---------------------------------------------------------+ +| **D** | Vector     | (1,1,1) | direction of the line | ++---------------+---------------+--------------+---------------------------------------------------------+ + +Outputs +------- + +**Vertices** and **Edges**. Verts and Edges will be generated. Depending on the inputs, the node will generate only one or multiples independent lines. See examples below. + + +Example of usage +---------------- + +.. image:: https://user-images.githubusercontent.com/10011941/47713459-a177d880-dc3a-11e8-935b-a2fa494dc49b.png + :alt: LineDemo1.PNG + +The first example shows just an standard line with 5 vertices and 1.00 ud between them + +.. image:: https://user-images.githubusercontent.com/10011941/47713473-a9377d00-dc3a-11e8-94ab-39095761788c.png + :alt: LineDemo2.PNG + +In this example the step is given by a series of numbers + +.. image:: https://user-images.githubusercontent.com/10011941/47713477-ad639a80-dc3a-11e8-9884-6568326d2a33.png + :alt: LineDemo3.PNG + +You can create multiple lines if input multiple lists + +.. image:: https://user-images.githubusercontent.com/10011941/47713487-b3597b80-dc3a-11e8-996b-17edf1cec9da.png + :alt: LineDemo4.PNG + +The AB mode will output a divided segment for each vector pair, the step can be used to change the proportions of the divisions + +.. image:: https://user-images.githubusercontent.com/10011941/47713488-b3597b80-dc3a-11e8-9e6e-f742d0338ba5.png + :alt: LineDemo5.PNG + +The "OD" mode can be used to visualize normals + +.. image:: https://user-images.githubusercontent.com/10011941/47713490-b3597b80-dc3a-11e8-9b6d-b937c0375ec5.png + :alt: LineDemo5.PNG + +Advanced example using the node to create a paraboloid grid \ No newline at end of file diff --git a/docs/nodes/list_main/statistics.rst b/docs/nodes/list_main/statistics.rst new file mode 100644 index 0000000000000000000000000000000000000000..c63a29f195db83090e6d9f6f007e1feb98b442d3 --- /dev/null +++ b/docs/nodes/list_main/statistics.rst @@ -0,0 +1,66 @@ +List Statistics +=============== + +Functionality +------------- + +List Statistics node computes various statistical quantities for the values in a list. + +Inputs +------ + +The **Data** input is expected to be a list of integers / floats or list of lists of integers / floats. +All inputs are vectorized. + +Parameters +---------- + +The **Function** parameter allows to select the statistical function to compute the corresponding statistical quantity for the input values. + ++----------------+---------------------+---------+------------------------------------------+ +| Param | Type | Default | Description | ++================+=====================+=========+==========================================+ +| **Function** | Enum | Average | The statistical function applied to | +| | All Statistics | | the input values. | +| | Sum | | | +| | Sum Of Squares | | For "All Statistics" selection the node | +| | Product | | computes and outputs the statistical | +| | Average | | quantities for all the statistical | +| | Geometric Mean | | functions along with their corresponding | +| | Harmonic Mean | | names. | +| | Standard Deviation | | | +| | Root Mean Square | | | +| | Skewness | | | +| | Kurtosis | | | +| | Minimum | | | +| | Maximum | | | +| | Median | | | +| | Percentile | | | +| | Histogram | | | ++----------------+---------------------+---------+------------------------------------------+ +| **Percentage** | Float | 0.75 | The percentage value for the | +| | | | percentile function. [1] | ++----------------+---------------------+---------+------------------------------------------+ +| **Normalize** | Boolean | False | Flag to normalize the histogram bins | +| | | | to the given normalize size. [2] | ++----------------+---------------------+---------+------------------------------------------+ +| **Bins** | Int | 10 | The number of bins in the histogram. [2] | ++----------------+---------------------+---------+------------------------------------------+ +| **Size** | Float | 10.00 | The normalized size of the histogram.[2] | ++----------------+---------------------+---------+------------------------------------------+ + +Notes: +[1] : The **Percentage** input socket is available only for the **Percentile** function. +[2] : The **Normalize** setting and the **Bins** and **Size** input sockets are available only for the **Histogram** function. + +Outputs +------- +**Name(s)** +The name(s) of the statistical value(s) computed corresponding to the selected statistical function. + +**Value(s)** +The statistical quantity of the input values corresponding to the selected function. For a vectorized input the output values are a series of quantities corresponding to the selected function. + +When "All Statistics" is selected the **Names** and **Values** outputs will list the names and the corresponding values for all the statistical functions. + + diff --git a/docs/nodes/list_mutators/combinatorics.rst b/docs/nodes/list_mutators/combinatorics.rst new file mode 100644 index 0000000000000000000000000000000000000000..67104801dfc301ffd33813bbaa89f0af9c9de06e --- /dev/null +++ b/docs/nodes/list_mutators/combinatorics.rst @@ -0,0 +1,142 @@ +Combinatorics +============= + +Functionality +------------- + +Combinatorics node performs various combinatoric operations like: **Product**, **Permutations** and **Combinations**. + + +Inputs +------ + +The inputs to the node are a set of lists of any type and a set of control parameters. + +The list inputs in combination to the control parameter inputs (Repeat / Length) are vectorized and the control parameters accept either single or multiple values for vectorization. + +List inputs to the node: +- **A** +- **B** [1] +... +- **Z** [1] + +Notes: +[1] : The multiple list inputs are available for the **Product** operation, all the other operations take one list input. For the **Product** operation as the last list input is connected a new empty input socket will appear to allow other lists to be connected. + + +Parameters +---------- + +The **Operation** parameter allows to select one of following operations: Product, Permutations and Combinations. + +All parameters except **Operation** can be given as an external input. + ++---------------+---------------+----------+--------------------------------------------+ +| Param | Type | Default | Description | ++===============+===============+==========+============================================+ +| **Operation** | Enum: | Product | See details in the Operations section. | +| | Product | | | +| | Permutations | | | +| | Combinations | | | ++---------------+---------------+----------+--------------------------------------------+ +| **Repeat** | Int | 1 | Repeat the input lists this many times [1] | ++---------------+---------------+----------+--------------------------------------------+ +| **Length** | Int | 1 | The number of the elements in the list to | +| | | | operate on [2] | ++---------------+---------------+----------+--------------------------------------------+ +| **A** | List | | The list of elements to operate on. | ++---------------+---------------+----------+--------------------------------------------+ +| **B..Z** | List | | Additional lists to operate on [3] | ++---------------+---------------+----------+--------------------------------------------+ + +Notes: +[1] : The Repeat parameter is only available for the **Product** operation. +[2] : The Length parameter is only available for the **Permutations** and **Combinations** operation. +[3] : Additional lists inputs are available only for the **Product** operation. + +Operations +---------- + +**Product** + +For this operation the node allows an arbitrary number of input lists to be product together as: A x B x .. x Z. The result of the product operation is a list of elements each of size equal to the number of input lists and has all the combinations of elements from the first list, followed by all elements in the second list etc. + +e.g. for two connected list inputs: + +A : ["X", "Y"] +B : [1, 2, 3] + +The result A x B is: + +["X", "Y"] x [1, 2, 3] => [ ["X", 1], ["X", 2], ["X", 3], ["Y", 1], ["Y", 2], ["Y", 3] ] + +The value of the **Repeat** parameter makes the node compute the product of all the connected lists replicated this number of times. + +e.g. for one connected input with repeat value of 2: + +A : ["X", "Y"] +Repeat: 2 + +The result A x A is: + +["X", "Y"] x ["X", "Y"] => [ ["X", "X"], ["X", "Y"], ["Y", "X"], ["Y", "Y"] ] + + +**Permutations** + +For this operation the node take a single input list and generates the permutations of its elements. The **Length** parameter sets the number of elements in the list to be permutated. + +Notes: +* If the **Length** is zero, the node will permute ALL elements in the list. +* The **Length** value is bounded between zero and the length of the input list, so any length values larger than the length of the input list is equivalent to permuting ALL elements in the list. + +e.g. for a list of 3 (mixed) elements: + +A: ["X", 3, (1,1,1)] +L: 3 + +The result is: + +[ + ['X', 3, (1, 1, 1)], + ['X', (1, 1, 1), 3], + [3, 'X', (1, 1, 1)], + [3, (1, 1, 1), 'X'], + [(1, 1, 1), 'X', 3], + [(1, 1, 1), 3, 'X'] +] + +**Combinations** + +For this operation the node takes a single list as input and generates the combinations of its elements taking L number of elements given by the **Length** parameter. + +Notes: +* If the **Length** is zero, the node will combine ALL elements in the list. +* The **Length** value is bounded between zero and the length of the input list, so any length values larger than the length of the input list is equivalent to combining ALL elements. + +e.g. for a list of 4 elements taken 2 elements: + +A : [1, 'X', (1, 2, 3), [1, 3]] +L : 2 + +The result is: + +[ + [1, 'X'], + [1, (1, 2, 3)], + [1, [1, 3]], + ['X', (1, 2, 3)], + ['X', [1, 3]], + [(1, 2, 3), [1, 3]] +] + + +Outputs +------- + +**Result** +The list of product, permutations or combinations. + +The results will be generated only when the **Result** output is connected. + + diff --git a/docs/nodes/list_mutators/polygon_sort.rst b/docs/nodes/list_mutators/polygon_sort.rst new file mode 100644 index 0000000000000000000000000000000000000000..e3b844512169dbce76a068d170518f0f297e0059 --- /dev/null +++ b/docs/nodes/list_mutators/polygon_sort.rst @@ -0,0 +1,76 @@ +Polygon Sort +============ + +Functionality +------------- + +Polygon Sort node sorts the input polygons by various criteria: distance, area and normal angle. + +Inputs +------ + +All parameters except **Mode** and **Descending** are vectorized and can take single of multiple input values. + +Parameters +---------- + +All parameters except **Mode** and **Descending** can be given by the node or an external input. + ++----------------+-----------+-----------+--------------------------------------------------------+ +| Param | Type | Default | Description | ++================+===========+===========+========================================================+ +| **Mode** | Enum | "D" | The sorting direction mode: | +| | | | P : Sort by the the distance to a point P | +| | | | D : Sort by the projection along a direction D | +| | | | A : Sort by the area of the polygons | +| | | | NP : Sort by the normal angle relative to point P | +| | | | ND : Sort by the normal angle relative to direction D | ++----------------+-----------+-----------+--------------------------------------------------------+ +| **Descending** | Bool  | False   | Sort the polygons in the descending order. | ++----------------+-----------+-----------+--------------------------------------------------------+ +| **Verts** | Vector  | none     | The vertices of the input mesh to be sorted. | ++----------------+-----------+-----------+--------------------------------------------------------+ +| **Polys** | Polygon  | none     | The polygons of the input mesh to be sorted. | ++----------------+-----------+-----------+--------------------------------------------------------+ +| **Point P** | Vector  | (1,0,0) | The reference vector: Point P. [1] | ++----------------+-----------+-----------+--------------------------------------------------------+ +| **Direction** | Vector  | (1,0,0) | The reference vector: Direction D. [1] | ++----------------+-----------+-----------+--------------------------------------------------------+ + +Notes: +[1] : "Point P" is shown in P and NP mode and "Direction" is shown in D and ND mode. These are used for distance and normal angle calculation. The area mode (A) does not use the input sorting vector. + +Outputs +------- + +**Indices** +The indices of the sorted polygons. + +**Vertices** +The input vertices. + +**Polygons** +The sorted polygons. + +**Quantities** +The quantity by which the polygons are sorted. Depending on the selected mode the output quantities are either Distances, Projections, Angles or Areas. + +Note: The output will be generated when the output sockets are connected. + +Modes +----- +The modes corespond to different sorting criteria and each one has a quanitity by which the polygons are sorted. +* P : In this mode the polygons are sorted by the distance from the center of each polygon to the given point P. +* D : In this mode the polygons are sorted by the projection component of polygon center vector along the given direction D. +* A : In this mode the polygons are sorted by the area of the polygons. +* ND : In this mode the polygons are sorted by the angle between the polygon normal and the given direction vector D. +* NP : In this mode the polygons are sorted by the angle between the polygon normal and the vector pointing form the center of the polygon to the given point P. + +Presets +------- +The node provides a set of predefined sort directions: along X, Y and Z. These buttons will set the mode to "D" and the sorting vector (Direction) to one of the X, Y or Z directions: (1,0,0), (0,1,0) and (0,0,1) respectively. The preset buttons are only visible as long as the sorting vector socket is not connected. + +References: +The algorythm to compute the area is based on descriptions found at this address: http://geomalgorithms.com/a01-_area.html#3D%20Polygons + + diff --git a/docs/nodes/modifier_change/smooth.rst b/docs/nodes/modifier_change/smooth.rst index 7b1da4b4ead9d14b5e10f6faee8b9c4f69dd1df5..b9dbbf2e9280066b126366a85fcd6583daeb5be5 100644 --- a/docs/nodes/modifier_change/smooth.rst +++ b/docs/nodes/modifier_change/smooth.rst @@ -13,7 +13,7 @@ This node has the following inputs: - **Vertices** - **Edges** -- **Faces** +- **Faces** - Only triangles and quads poligons. - **VertMask**. Selected vertices to be smoothed. - **Iterations** - **Clip threshold** @@ -26,7 +26,7 @@ Parameters This node has the following parameters: - **X**, **Y**, **Z**. Toggle axes vertices will be smoothed along. By default mesh is smoothed along all axes. -- **Laplacian Smooth**. Toggles smoothing algorithm: when checked, Laplacian smoothing is used; otherwise, simple averaging scheme will be used. By default not checked. +- **Laplacian Smooth**. Toggles smoothing algorithm: when checked, Laplacian smoothing is used; otherwise, simple averaging scheme will be used. By default not checked. **Deal only with tris and quads, not N-gons**. - **Clip X**, **Clip Y**, **Clip Z**. Toggle axes along which "Mirror Clipping" procedure will be applied. This procedure merges vertices that have X/Y/Z coordinate near zero, withing specified threshold. For example, it can merge vertices `(0.01, 3, 5)` and `(- 0.01, 3, 5)` into one vertex `(0, 3, 5)`. These parameters are available only when **Laplacian Smooth** is off. Not checked by default. - **Preserve volume**. If checked, the mesh will be "blown" a bit after smoothing, to preserve its volume. Available only when **Laplacian Smooth** is on. Checked by default. - **Iterations**. Number of smoothing operation iterations. Default value is 1. This parameter can also be provided as input. diff --git a/docs/nodes/number/mix_inputs.rst b/docs/nodes/number/mix_inputs.rst new file mode 100644 index 0000000000000000000000000000000000000000..06f3964b9b8234da6335fbb022bca70080213dcb --- /dev/null +++ b/docs/nodes/number/mix_inputs.rst @@ -0,0 +1,85 @@ +Mix Inputs +========== + +Functionality +------------- + +This node mixes two values of different types using a given factor, a selected interpolation and easing function. + +For a factor of 0.0 it outputs the first value, for the factor of 1.0 it outputs the last value and for any other factor in between 0-1 it outputs an interpolated value between the first and second input value. An exception to this are the "Back" and "Elastic" interpolations which generate output values that are not strictly confined to the first-second value interval, but they will output values that start at first and end at second value. + +Inputs & Parameters +------------------- + +All parameters except for **Mode**, **Interpolation**, **Easing**, **Mirror** and **Swap** can be given by the node or an external input. + +Based on the selected **Mode** the node changes the input and output socket types to match the corresponding type. + +The node is vectorized so the inputs values (A/B) take either a single or a list of values. The node will extend the shortest list to match the longest list before mixing the values in the two lists. + +The node has the following parameters: + ++-------------------+--------------+-------------+-----------------------------------------------------+ +| Parameter | Type | Default | Description | ++===================+==============+=============+=====================================================+ +| **Mode** | Enum: | Float | The type of inputs values to mix. | +| | Int | | | +| | Float | | | +| | Vector | | | +| | Color | | | +| | Matrix | | | +| | Quaternion | | | ++-------------------+--------------+-------------+-----------------------------------------------------+ +| **Interpolation** | Enum: | Linear | Type of interpolation. | +| | Linear | | f(x) ~ x | +| | Sinusoidal | | f(x) ~ sin(x) | +| | Quadratic | | f(x) ~ x*2 | +| | Cubic | | f(x) ~ x^3 | +| | Quadric | | f(x) ~ x^4 | +| | Quintic | | f(x) ~ x^5 | +| | Exponential | | f(x) ~ e^x | +| | Circular | | f(x) ~ sqrt(1-x*x) | +| | Back | | f(x) ~ x*x*x - x*sin(x) | +| | Bounce | | f(x) ~ series of geometric progression parabolas | +| | Elastic | | f(x) ~ sin(x) * e^x | ++-------------------+--------------+-------------+-----------------------------------------------------+ +| **Easing** | Enum | Ease In-Out | Type of easing. | +| | Ease In | | Ease In = slowly departs the starting value | +| | Ease Out | | Ease Out = slowly approaches the ending value | +| | Ease In-Out | | Ease In-Out = slowly departs and approaches values | ++-------------------+--------------+-------------+-----------------------------------------------------+ +| **Mirror** | Bool | False | Mirror the mixing factor around 0.5. | ++-------------------+--------------+-------------+-----------------------------------------------------+ +| **Swap** | Bool | False | Swap the two input values A and B. | ++-------------------+--------------+-------------+-----------------------------------------------------+ +| **Factor** | Float | 0.5 | Mixing factor (between 0.0 and 1.0) | ++-------------------+--------------+-------------+-----------------------------------------------------+ +| **A** | Any type | | Starting value | ++-------------------+--------------+-------------+-----------------------------------------------------+ +| **B** | Any type | | Ending value | ++-------------------+--------------+-------------+-----------------------------------------------------+ + + +Extra Parameters +---------------- +For certain interpolation types the node provides extra parameters on the Property Panel. + +* Exponential + Extra parameters to adjust the base and the exponent of the exponential function. The Defaults are 2 and 10.0. + +* Back + Extra parameters to adjust the scale of the overshoot. The default is 1.0. + +* Bounce + Extra parameters to adjust the attenuation of the bounce and the number of bounces. The defaults are 0.5 and 4. + +* Elastic + Extra parameters to adjust the base and the exponent of the damping oscillation as well as the number of bounces (oscillations). The defaults are 1.6, 6.0 and 6. + + +Outputs +------- + +Based on the selected **Mode** the node outputs the corresponding type value. + + diff --git a/docs/nodes/number/mix_numbers.rst b/docs/nodes/number/mix_numbers.rst deleted file mode 100644 index 03d00bcd5af2b6576c53681e64e5d01caa8cc01d..0000000000000000000000000000000000000000 --- a/docs/nodes/number/mix_numbers.rst +++ /dev/null @@ -1,88 +0,0 @@ -Mix Numbers -=========== - -Functionality -------------- - -This node mixes two values using a given factor and a selected interpolation and easing functions. - -For a factor of 0.0 it outputs the first value while the factor of 1.0 it outputs the last value. For every factor value between 0-1 it will output a value between the first and second input value. (*) - -Note: -(*) The Back and Elastic interpolations will generate outputs that are not strictly confined to the first-second value interval, but they will output values that start at first and end at second value. - -Inputs & Parameters -------------------- - -All parameters except for **Type**, **Interpolation** and **Easing** can be given by the node or an external input. - -This node has the following parameters: - -+-------------------+---------------+-------------+-----------------------------------------------------+ -| Parameter | Type | Default | Description | -+===================+===============+=============+=====================================================+ -| **Type** | Enum: | Float | Type of inputs values to interpolate. | -| | Int | | When Float is selected the input value1 and value2 | -| | Float | | expect float values | -| | | | When Int is selected the input value1 and value2 | -| | | | expect int values. | -+-------------------+---------------+-------------+-----------------------------------------------------+ -| **Interpolation** | Enum: | Linear | Type of interpolation. | -| | Linear | | f(x) ~ x | -| | Sinusoidal | | f(x) ~ sin(x) | -| | Quadratic | | f(x) ~ x*2 | -| | Cubic | | f(x) ~ x^3 | -| | Quadric | | f(x) ~ x^4 | -| | Quintic | | f(x) ~ x^5 | -| | Exponential | | f(x) ~ e^x | -| | Circular | | f(x) ~ sqrt(1-x*x) | -| | Back | | f(x) ~ x*x*x - x*sin(x) | -| | Bounce | | f(x) ~ series of geometric progression parabolas | -| | Elastic | | f(x) ~ sin(x) * e^x | -+-------------------+---------------+-------------+-----------------------------------------------------+ -| **Easing** | Enum | Ease In-Out | Type of easing. | -| | Ease In | | Ease In = slowly departs the starting value | -| | Ease Out | | Ease Out = slowly approaches the ending value | -| | Ease In-Out | | Ease In-Out = slowly departs and approaches values | -+-------------------+---------------+-------------+-----------------------------------------------------+ -| **Value1** | Int or Float | 0 or 0.0 | Starting value | -+-------------------+---------------+-------------+-----------------------------------------------------+ -| **Value2** | Int or Float | 1 or 1.0 | Ending value | -+-------------------+---------------+-------------+-----------------------------------------------------+ -| **Factor** | Float | 0.5 | Mixing factor (between 0.0 and 1.0) | -+-------------------+---------------+-------------+-----------------------------------------------------+ - -Extra Parameters ----------------- -For certain interpolation types the node provides extra parameters on the property panel. - -* Exponential - Extra parameters to adjust the base and the exponent of the exponential function. The Defaults are 2 and 10.0. - -* Back - Extra parameters to adjust the scale of the overshoot. The default is 1.0. - -* Bounce - Extra parameters to adjust the attenuation of the bounce and the number of bounces. The defaults are 0.5 and 4. - -* Elastic - Extra parameters to adjust the base and the exponent of the damping oscillation as well as the number of bounces (oscillations). - The defaults are 1.6, 6.0 and 6. - -Outputs -------- - -This node has one output: **Value**. - -Inputs and outputs are vectorized, so if series of values is passed to one of inputs, then this node will produce several sequences. - -Example of usage ----------------- - -Given simplest nodes setup: - -# - -you will have something like: - -# diff --git a/docs/nodes/quaternion/quaternion_in.rst b/docs/nodes/quaternion/quaternion_in.rst new file mode 100644 index 0000000000000000000000000000000000000000..6711f68877180276be44e2e6f8a1266fe307157a --- /dev/null +++ b/docs/nodes/quaternion/quaternion_in.rst @@ -0,0 +1,69 @@ +Quaternion In +------------- + +Quaternion In node constructs quaternions based on various input components provided for a selected mode. + + +Modes +===== + +The available **Modes** are: WXYZ, SCALAR-VECTOR, EULER, AXIS-ANGLE & MATRIX. + ++===============+================================================================+ +| Mode | Description | ++---------------+----------------------------------------------------------------+ +| WXYZ | Converts W, X, Y, Z components into a quaternion. [1] | ++---------------+----------------------------------------------------------------+ +| SCALAR-VECTOR | Converts Scalar & Vector components into a quaternion. [1] | ++---------------+----------------------------------------------------------------+ +| EULER | Converts X, Y, Z Euler angles and an order of rotation | +| | into a quaternion. [2,3] | ++---------------+----------------------------------------------------------------+ +| AXIS-ANGLE | Converts an Axis & an Angle of rotation into a quaternion. [2] | ++---------------+----------------------------------------------------------------+ +| MATRIX | Converts an orthogonal 4x4 rotation matrix into a quaternion. | ++===============+================================================================+ + +Notes: +[1] : For WXYZ and SCALAR-VECTOR modes the node provides a "Normalize" option to generate a normalized quaternion. All the other modes automatically generate a normalized quaternion. +[2] : For EULER and AXIS-ANGLE modes (which take angle input) the node provides an +angle unit conversion to let the angle values be converted to Radians, Degrees or Unities (0-1 range). +[3] : For EULER mode the node provides the option to select the Euler rotation order: +"XYZ", "XZY", "YXZ", "YZX", "ZXY" or "ZYX". + +The modes WXYZ and SCALAR-VECTOR are the same except the WXYZ provides 4 floats (W, X, Y and Z) to generate the quaternion, while SCALAR-VECTOR provides a scalar (W) and a vector (X, Y, Z). + +Inputs +====== + +The node takes a list of various components, based on the selected mode, and it +constructs the corresponding quaternions. The node is vectorized so the inputs take +a value or a list of values. When multiple lists are connected the node will +extend the length of the connected input lists to match the longest one before computing the list of output quaternions. + +Based on the selected **Mode** the node makes available the corresponding input sockets: + ++===============+==================================+ +| Mode | Input Sockets (types) | ++---------------+----------------------------------+ +| WXYZ | W, X, Y, Z (floats) | ++---------------+----------------------------------+ +| SCALAR-VECTOR | Scalar (float) & Vector (vector) | ++---------------+----------------------------------+ +| EULER | X, Y, Z angles (floats) | ++---------------+----------------------------------+ +| AXIS-ANGLE | Axis (vector) & Angle (float) | ++---------------+----------------------------------+ +| MATRIX | Matrix (4x4 matrix) | ++===============+==================================+ + + +Outputs +======= + +**Quaternions** + +The node outputs a list of one ore more quaternions based on the given input. + +The node only generates the quaternions when the output socket is connected. + diff --git a/docs/nodes/quaternion/quaternion_math.rst b/docs/nodes/quaternion/quaternion_math.rst new file mode 100644 index 0000000000000000000000000000000000000000..392c11923d48286b0a4e93c3266a6a6e2bbf16a8 --- /dev/null +++ b/docs/nodes/quaternion/quaternion_math.rst @@ -0,0 +1,172 @@ +Quaternion Math Node +-------------------- + +The Quaternion Math node performs various artithmetic operations on quaternions. + +The available arithmetic operations and their corresponding inputs/outputs are: + ++============+========+========+=====================================+ +| Operation | Input | Output | Description | ++============+========+========+=====================================+ +| ADD | NQ | Q | Add multiple quaternions | +| SUB | QQ | Q | Subtract two quaternions | +| MULTIPLY | NQ | Q | Multiply multiple quaternions | +| DIVIDE | QQ | Q | Divide two quaternions | +| ROTATE | QQ | Q | Rotate a quaternion around another | +| DOT | QQ | S | Dot product two quaternions | +| DISTANCE | QQ | S | Distance between two quaternions | +| NEGATE | Q | Q | Negate a quaternion | +| CONJUGATE | Q | Q | Conjugate a quaternion | +| INVERT | Q | Q | Invert a quaternion | +| NORMALIZE | Q | Q | Normalize a quaternion | +| SCALE | QS | Q | Scale a quaternion by given factor | +| QUADRANCE | Q | S | Quadrance of a quaternion | +| MAGNITUDE | Q | S | Magnitude of a quaternion | ++============+========+========+=====================================+ + +where: + +NQ = arbitrary number of quaternion inputs +QQ = two quaternion inputs +Q = one quaternion input +QS = one quaternion + scalar value +S = scalar value + +For the operations that take multiple quaternion inputs (NQ & QQ) the node provides a PRE / POST option, which lets the node execute the operation on the quaternion inputs in a direct or reverse order. The exceptions to this rule are the ADD, DOT and DISTANCE operations for which the order of quaternions is irrelevant. + +For quaternion inputs A and B: +PRE = A op B +POST = B op A + + +Inputs +====== +The input to the node are lists of quaternions as well as control parameters (like scale etc). For certain operations the node takes arbitrary number of quaternion input lists, for others it takes only two quaternion input lists and for some only one quaternion input list. + +The inputs accept single value quaternions or a list of quaternions. The node is vectorized so it will extend the quaternion lists to match the longest input. + + +Operations +========== + +* ADD : adds the components of two or more quaternions + +q1 = (w1, x1, y1, z1) +q2 = (w2, x2, y2, z2) + +q1 + q2 = (w1 + w2, x1 + x2, y1 + y2, z1 + z1) + + +* SUB : subtracts the components of two quaternions + +q1 = (w1, x1, y1, z1) +q2 = (w2, x2, y2, z2) + +q1 - q2 = (w1 - w2, x1 - x2, y1 - y2, z1 - z2) + + +* MULTIPLY : multiplies two or more quaternions + +q1 = (w1, x1, y1, z1) = (w1, V1), where V1 = (x1, y1, z1) +q2 = (w2, x2, y2, z2) = (w2, V2), where V2 = (x2, y2, z2) + +q1 x q2 = (w1 * w2 - V1 * V2, w1 * V1 + w2 * V2 + V1 x V2) + +where V1 * V2 is dot product of vectors V1 & V2 +and V1 x V2 is the cross product of vectors V1 & V2 + + +* DIVIDE : divide two quaternions (multiply one quaternion with inverse of the other) + +q1 = (w1, x1, y1, z1) +q2 = (w2, x2, y2, z2) + +q1 / q2 = q1 x inverse(q2) + + +* ROTATE : rotates one quaternion around the other quaternion + + +* DOT : the dot product of two quaternions + +q1 = (w1, x1, y1, z1) +q2 = (w2, x2, y2, z2) + +q1 * q2 = w1 * w2 + x1 * x2 + y1 * y2 + z1 * z2 + + +* DISTANCE : the distance between two quaternions + +q1 = (w1, x1, y1, z1) +q2 = (w2, x2, y2, z2) + +Distance(q1, q2) = Magnitude(q1 - q2) + + +* NEGATE : negates a quaternion + +q = (w, x, y, z) + +Negate(q) = (-w, -x, -y, -z) + + +* CONJUGATE : conjugates a quaternion + +q = (w, x, y, z) + +Conjugate(q) = (w, -x, -y, -z) + + +* INVERT : inverts a quaternion + +q = (w, x, y, z) + +Inverse(q) = Conjugate(q) / Magnitude(q)^2 + + +* NORMALIZE : normalizes a quaternion + +q = (w, x, y, z) + +Normalize(q) = (w/m, x/m, y/m, z/m) + +where m = Magnitude(q) + + +* SCALE : scales the components of a quaternion + +q = (w, x, y, z) + +s - (float) the scale factor +sf = (sw, sx, sy, sz) - (array of bools) filters which component is scaled + +S = (s if sw else 1, s if sx else 1, s if sy else 1, s if sz else 1) + +scale(q, S) = (w * Sw, x * Sx, y * Sy, z * Sz) + + +* QUADRANCE : the quadreance of a quaternion + +q = (w, x, y, z) + +Quadrance(q) = w * w + x * x + y * y + z * z + +Note: essentially this is the dot product of the quaternion with itself, and also equal to square of the magnitude. + +* MAGNITUDE : the magnitude of a quaternion + +q = (w, x, y, z) + +Magnitude(q) = sqrt(w * w + x * x + y * y + z * z) + +Note: this is essentially the square root of the quadrance (the length of the quaternion). + + +Output +====== + +**Quaternions** or **Values** +Depending on the operation the output to the node is either a quaternion list or scalar value list. + +The node computes the results (quaternions or scalar values) only when the output socket is connected. + diff --git a/docs/nodes/quaternion/quaternion_out.rst b/docs/nodes/quaternion/quaternion_out.rst new file mode 100644 index 0000000000000000000000000000000000000000..8872e5a7704a3cb5ee8610cd52d68719a06d6dd2 --- /dev/null +++ b/docs/nodes/quaternion/quaternion_out.rst @@ -0,0 +1,64 @@ +Quaternion Out +-------------- + +Quaternion Out node converts a quaternion into various formats for a selected mode. + +Modes +===== + +The available **Modes** are: WXYZ, EULER, AXIS-ANGLE & MATRIX. + ++===============+================================================================+ +| Mode | Description | ++---------------+----------------------------------------------------------------+ +| WXYZ | Converts a quaternion into its W, X, Y, Z components. [1] | ++---------------+----------------------------------------------------------------+ +| SCALAR-VECTOR | Converts a quaternion into its Scalar & Vector components. [1] | ++---------------+----------------------------------------------------------------+ +| EULER | Converts a quaternion into X, Y, Z angles corresponding | +| | to the Euler rotation given an Euler rotation order. [2,3] | ++---------------+----------------------------------------------------------------+ +| AXIS-ANGLE | Converts a quaternion into the Axis & Angle of rotation. [2] | ++---------------+----------------------------------------------------------------+ +| MATRIX | Converts a quaternion into an orthogonal 4x4 rotation matrix...| ++===============+================================================================+ + +Notes: +[1] : For WXYZ and SCALAR-VECTOR modes the node provides a "Normalize" option to normalize the input quaternion before outputting its components. All the other modes automatically normalize the quaternion. +[2] : For EULER and AXIS-ANGLE modes, which output angles, the node provides an +angle unit conversion to let the angle output values be converted to Radians, +Degrees or Unities (0-1 range). +[3] : For EULER mode the node provides the option to select the Euler rotation order: +"XYZ", "XZY", "YXZ", "YZX", "ZXY" or "ZYX". + +Inputs +====== + +**Quaternions** +The node takes a list of (one or more) quaternions and based on the selected mode +it converts the quaternions into the corresponding components. + + +Outputs +======= + +Based on the selected **Mode** the node makes available the corresponding output sockets: + ++===============+==================================+ +| Mode | Output Sockets (types) | ++---------------+----------------------------------+ +| WXYZ | W, X, Y, Z (floats) | ++---------------+----------------------------------+ +| SCALAR-VECTOR | Scalar (float) & Vector (vector) | ++---------------+----------------------------------+ +| EULER | X, Y, Z angles (floats) | ++---------------+----------------------------------+ +| AXIS-ANGLE | Axis (vector) & Angle (float) | ++---------------+----------------------------------+ +| MATRIX | Matrix (4x4 matrix) | ++===============+==================================+ + +The modes WXYZ and SCALAR-VECTOR are the same except the WXYZ mode outputs the components as 4 floats (W, X, Y and Z), while the SCALAR-VECTOR mode outputs the components as a scalar (W) and a vector (XYZ). + +The node only generates the conversion when the output sockets are connected. + diff --git a/index.md b/index.md index 30a384e3ef4585edf612dbc7bdea6d972025f2f7..e48b4a939134b799fcc39666861f6ca01db50657 100644 --- a/index.md +++ b/index.md @@ -10,7 +10,7 @@ > Failing to follow these points will break the node category parser. ## Generator - SvLineNodeMK2 + SvLineNodeMK3 SvPlaneNodeMK2 SvNGonNode SvBoxNode @@ -43,6 +43,7 @@ SvRingNode SvEllipseNode SvSmoothLines + SvHyperCubeNode ## Analyzers SvBBoxNode @@ -64,6 +65,8 @@ SvProportionalEditNode SvRaycasterLiteNode SvOBJInsolationNode + EvaluateImageNode + SvDeformationNode ## Transforms SvRotationNode @@ -134,6 +137,7 @@ SvListModifierNode SvFixEmptyObjectsNode SvDatetimeStrings + SvPolygonSortNode ## List Main ListJoinNode @@ -144,6 +148,7 @@ ListMatchNode ListFuncNode SvListDecomposeNode + SvListStatisticsNode ## List Struct ShiftNodeMK2 @@ -177,7 +182,7 @@ SvRndNumGen RandomNode SvEasingNode - SvMixNumbersNode + SvMixInputsNode ## Vector GenVectorsNode @@ -310,6 +315,8 @@ SvSetCustomMeshNormals --- SvSpiralNode + SvExportGcodeNode + SvCombinatoricsNode ## Alpha Nodes SvCurveViewerNode @@ -340,4 +347,7 @@ SvOffsetLineNode SvContourNode SvPlanarEdgenetToPolygons - + --- + SvQuaternionOutNode + SvQuaternionInNode + SvQuaternionMathNode diff --git a/json_examples/Architecture/ROADS_LARGE_nikitron_2014.json b/json_examples/Architecture/ROADS_LARGE_nikitron_2014.json index 6d486ed959f76c334f3b7832367ed9e318c0a4d8..230efdff8aef3f0a4671755bf2993c5ebd207cea 100644 --- a/json_examples/Architecture/ROADS_LARGE_nikitron_2014.json +++ b/json_examples/Architecture/ROADS_LARGE_nikitron_2014.json @@ -659,7 +659,7 @@ "width": 140.0 }, "Line MK2": { - "bl_idname": "SvLineNodeMK2", + "bl_idname": "SvLineNodeMK3", "color": [ 0.0, 0.5, diff --git a/json_examples/Design/lights_colors.json b/json_examples/Design/lights_colors.json index d856a1c2d9fe9eeb2efe6f1c047949dceca2c835..a07c30e05c45a451dbd1c9403265c0b1e5ba375b 100644 --- a/json_examples/Design/lights_colors.json +++ b/json_examples/Design/lights_colors.json @@ -40,7 +40,7 @@ "width": 140.0 }, "Line MK2": { - "bl_idname": "SvLineNodeMK2", + "bl_idname": "SvLineNodeMK3", "color": [ 0.0, 0.5, diff --git a/json_examples/ParametricModelling/hilbert_to_circle.json b/json_examples/ParametricModelling/hilbert_to_circle.json index 9bcc586ecebcf556a2697fb5eb46b4dfca8e57f4..29a658d2eb950d5becd0e2ee83ebb9d62d032a04 100644 --- a/json_examples/ParametricModelling/hilbert_to_circle.json +++ b/json_examples/ParametricModelling/hilbert_to_circle.json @@ -94,7 +94,7 @@ "width": 140.0 }, "Line": { - "bl_idname": "SvLineNodeMK2", + "bl_idname": "SvLineNodeMK3", "color": [ 0.0, 0.5, diff --git a/json_examples/Shapes/bind_sections_shape.json b/json_examples/Shapes/bind_sections_shape.json index 5c1f1e649fcb9fb088f11e4fda0723fc6907a11e..a1922ed7cf1f884c95c3258ddd92b0e9b023004b 100644 --- a/json_examples/Shapes/bind_sections_shape.json +++ b/json_examples/Shapes/bind_sections_shape.json @@ -276,7 +276,7 @@ "width": 391.4091796875 }, "Line MK2": { - "bl_idname": "SvLineNodeMK2", + "bl_idname": "SvLineNodeMK3", "color": [ 0.9200000166893005, 0.9200000166893005, @@ -297,7 +297,7 @@ "width": 140.0 }, "Line MK2.001": { - "bl_idname": "SvLineNodeMK2", + "bl_idname": "SvLineNodeMK3", "color": [ 0.9200000166893005, 0.9200000166893005, diff --git a/node_scripts/templates/elfnor/tree_generator.py b/node_scripts/templates/elfnor/tree_generator.py index 9e5b88d52c104361a3ee828466108a454cbaadc6..dcea60d0fdeff639b6613010e6886f106bbece06 100644 --- a/node_scripts/templates/elfnor/tree_generator.py +++ b/node_scripts/templates/elfnor/tree_generator.py @@ -155,7 +155,7 @@ class SCA: if p not in finished: process.insert(0, p) - mats= [] + mats_out= [] for edge in edges: if ends[edge[1]]: #calculate leaf directions @@ -171,10 +171,8 @@ class SCA: m[2][0:3] = dir3 m[3][0:3] = v1 m.transpose() - mats.append(m) + mats_out.append(m) - mats_out = Matrix_listing(mats) - return verts, edges, ends, br, mats_out def sv_main(npoints=100 , dist=0.05, min_dist=0.05, max_dist=2.0, tip_radius=0.01, trop=[], verts_in=[], verts_start=[]): diff --git a/nodes/analyzer/deformation.py b/nodes/analyzer/deformation.py new file mode 100644 index 0000000000000000000000000000000000000000..5dba5b4f94b35bcfed1b5c9d5fae717d0f3f8880 --- /dev/null +++ b/nodes/analyzer/deformation.py @@ -0,0 +1,252 @@ +# ##### BEGIN GPL LICENSE BLOCK ##### +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# ##### END GPL LICENSE BLOCK ##### + + +import bpy +from bpy.props import BoolProperty +from sverchok.node_tree import SverchCustomTreeNode +from sverchok.data_structure import updateNode, match_long_repeat, match_long_cycle +import numpy as np + + +def edge_elongation(np_verts, np_verts_n, np_edges): + '''Calculate edge length variation''' + + pairs_edges = np_verts[np_edges, :] + vect_rest = (pairs_edges[:, 0, :] - pairs_edges[:, 1, :]) + dist_rest = np.linalg.norm(vect_rest, axis=1) + pairs = np_verts_n[np_edges, :] + dif_v = pairs[:, 0, :] - pairs[:, 1, :] + dist = np.linalg.norm(dif_v, axis=1) + elong = dist - dist_rest + + return elong + + +def vert_edge_defromation(elong, np_verts, np_edges): + '''Redistribute edge length variation to vertices''' + + x = elong[:, np.newaxis] / 2 + v_len = len(np_verts) + deformation = np.zeros((v_len, v_len, 1), dtype=np.float32) + deformation[np_edges[:, 0], np_edges[:, 1], :] = x + deformation[np_edges[:, 1], np_edges[:, 0], :] = x + + return np.sum(deformation, axis=1)[:, 0] + + +def area_calc_setup(pols): + '''Analyze pols information''' + + np_pols = np.array(pols) + p_len = len(pols) + if np_pols.dtype == np.object: + np_len = np.vectorize(len) + pols_sides = np_len(np_pols) + pols_sides_max = np.amax(pols_sides) + pols = match_long_cycle(pols) + np_pols = np.array(pols) + p_non_regular = True + else: + p_non_regular = False + pols_sides = np.array(p_len) + pols_sides_max = len(pols[0]) + + return [np_pols, pols_sides_max, pols_sides, p_len, p_non_regular] + + +def get_normals(v_pols): + '''Calculate polygon normals''' + + v1 = v_pols[:, 1, :] - v_pols[:, 0, :] + v2 = v_pols[:, 2, :] - v_pols[:, 0, :] + pols_normal = np.cross(v1, v2) + pols_normal_d = np.linalg.norm(pols_normal, axis=1) + + return pols_normal / pols_normal_d[:, np.newaxis] + + +def area_calc(np_verts, area_params): + '''Calculate polygons area''' + + np_pols, pols_sides_max, pols_sides, p_len, p_non_regular = area_params + + v_pols = np_verts[np_pols, :] + pols_normal = get_normals(v_pols) + + prod = np.zeros((pols_sides_max, p_len, 3), dtype=np.float32) + if p_non_regular: + + for i in range(pols_sides_max): + mask = pols_sides > i + end_i = (i + 1) % pols_sides_max + prod[i, mask, :] = np.cross(v_pols[mask, i, :], v_pols[mask, end_i, :]) + + prod = np.sum(prod, axis=0) + area = abs(np.sum(prod * pols_normal, axis=1) / 2) + + else: + for i in range(pols_sides_max): + end_i = (i + 1) % pols_sides_max + prod[i, :, :] = np.cross(v_pols[:, i, :], v_pols[:, end_i, :]) + + prod = np.sum(prod, axis=0) + area = abs(np.sum(prod * pols_normal, axis=1) / 2) + + return area + + +def area_to_verts(np_verts, area_params, pols_deformation): + '''Redistribute area variation to verts.''' + + np_pols, pols_sides_max, pols_sides, p_len, advance = area_params + + pol_id = np.arange(p_len) + pol_def_to_vert = pols_deformation / pols_sides + deformation = np.zeros((len(np_verts), p_len), dtype=np.float32) + if advance: + for i in range(pols_sides_max): + mask = pols_sides > i + deformation[np_pols[mask, i], pol_id[mask]] += pol_def_to_vert[mask] + + else: + for i in range(pols_sides_max): + deformation[np_pols[:, i], pol_id] += pol_def_to_vert + + return np.sum(deformation, axis=1) + + +def area_variation(np_verts, np_verts_n, area_params): + '''get areas and subtract relaxed area to deformed area''' + + relax_area = area_calc(np_verts, area_params) + defromation_area = area_calc(np_verts_n, area_params) + + return defromation_area - relax_area + + +def calc_deformations(meshes, gates, result): + '''calculate edge elong and polygon area variation''' + + for vertices, vertices_n, edges, pols in zip(*meshes): + np_verts = np.array(vertices) + np_verts_n = np.array(vertices_n) + if len(edges) > 0 and (gates[0] or gates[1]): + np_edges = np.array(edges) + elong = edge_elongation(np_verts, np_verts_n, np_edges) + result[0].append(elong if gates[4] else elong.tolist()) + if gates[1]: + elong_v = vert_edge_defromation(elong, np_verts, np_edges) + result[1].append(elong_v if gates[4] else elong_v.tolist()) + + if len(pols) > 0 and (gates[2] or gates[3]): + area_params = area_calc_setup(pols) + area_var = area_variation(np_verts, np_verts_n, area_params) + result[2].append(area_var if gates[4] else area_var.tolist()) + if gates[3]: + area_var_v = area_to_verts(np_verts, area_params, area_var) + result[3].append(area_var_v if gates[4] else area_var_v.tolist()) + + return result + + +class SvDeformationNode(bpy.types.Node, SverchCustomTreeNode): + ''' + Triggers: Measure deformation + Tooltip: Deformation between to states, edge elong a area variation + ''' + bl_idname = 'SvDeformationNode' + bl_label = 'Deformation' + bl_icon = 'MOD_SIMPLEDEFORM' + + output_numpy = BoolProperty( + name='Output NumPy', description='output NumPy arrays', + default=False, update=updateNode) + + def draw_buttons_ext(self, context, layout): + '''draw buttons on the N-panel''' + layout.prop(self, "output_numpy", toggle=False) + + def sv_init(self, context): + '''create sockets''' + sinw = self.inputs.new + sonw = self.outputs.new + sinw('VerticesSocket', "Rest Verts") + sinw('VerticesSocket', "Distort Verts") + sinw('StringsSocket', "Edges") + sinw('StringsSocket', "Pols") + + sonw('StringsSocket', "Edges Def") + sonw('StringsSocket', "Pols Def") + sonw('StringsSocket', "Vert Edge Def") + sonw('StringsSocket', "Vert Pol Def") + + def get_data(self): + '''get all data from sockets''' + si = self.inputs + vertices_s = si['Rest Verts'].sv_get(default=[[]]) + vertices_n = si['Distort Verts'].sv_get(default=[[]]) + edges_in = si['Edges'].sv_get(default=[[]]) + pols_in = si['Pols'].sv_get(default=[[]]) + + return match_long_repeat([vertices_s, vertices_n, edges_in, pols_in]) + + def ready(self): + '''check if there are the needed links''' + si = self.inputs + so = self.outputs + ready = any(s.is_linked for s in so) + ready = ready and si[0].is_linked and si[1].is_linked + ready = ready and (si[2].is_linked or si[3].is_linked) + return ready + + def process(self): + '''main node function called every update''' + so = self.outputs + if not self.ready(): + return + + result = [[], [], [], []] + gates = [] + gates.append(so['Edges Def'].is_linked) + gates.append(so['Vert Edge Def'].is_linked) + gates.append(so['Pols Def'].is_linked) + gates.append(so['Vert Pol Def'].is_linked) + gates.append(self.output_numpy) + meshes = self.get_data() + + result = calc_deformations(meshes, gates, result) + + if gates[0]: + so['Edges Def'].sv_set(result[0]) + if gates[1]: + so['Vert Edge Def'].sv_set(result[1]) + if gates[2]: + so['Pols Def'].sv_set(result[2]) + if gates[3]: + so['Vert Pol Def'].sv_set(result[3]) + + +def register(): + '''register class in Blender''' + bpy.utils.register_class(SvDeformationNode) + + +def unregister(): + '''unregister class in Blender''' + bpy.utils.unregister_class(SvDeformationNode) diff --git a/nodes/analyzer/evaluate_image.py b/nodes/analyzer/evaluate_image.py new file mode 100644 index 0000000000000000000000000000000000000000..dd77a94965a034751c475889417a8fbe2245d31e --- /dev/null +++ b/nodes/analyzer/evaluate_image.py @@ -0,0 +1,211 @@ +# ##### BEGIN GPL LICENSE BLOCK ##### +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# ##### END GPL LICENSE BLOCK ##### + +import bpy +from bpy.props import IntProperty, FloatProperty, StringProperty, EnumProperty + +from sverchok.node_tree import SverchCustomTreeNode +from sverchok.data_structure import updateNode, fullList +from math import floor + + +class EvaluateImageNode(bpy.types.Node, SverchCustomTreeNode): + ''' Evaluate Image ''' + bl_idname = 'EvaluateImageNode' + bl_label = 'Evaluate Image' + bl_icon = 'FILE_IMAGE' + + image_name = StringProperty(name='image_name', description='image name', default='', update=updateNode) + + boundary_modes = [ + ("CLIP", "Clip", "", 0), + ("EXTEND", "Extend", "", 1), + ("CYCLIC", "Repeat", "", 2), + ("MIRROR", "Mirror", "", 3)] + + shift_modes = [ + ("NONE", "None", "", 0), + ("ALTERNATE", "Alternate", "", 1), + ("CONSTANT", "Constant", "", 2)] + + shift_mode_U = EnumProperty( + name="U Shift", + description="U Shift", + default="NONE", items=shift_modes, + update=updateNode) + + shift_mode_V = EnumProperty( + name="V Shift", + description="V Shift", + default="NONE", items=shift_modes, + update=updateNode) + + boundU = EnumProperty( + name="U Bounds", + description="U Boundaries", + default="CYCLIC", items=boundary_modes, + update=updateNode) + + boundV = EnumProperty( + name="V Bounds", + description="V Boundaries", + default="CYCLIC", items=boundary_modes, + update=updateNode) + + domU = FloatProperty( + name='U domain', description='U domain', default=1, min=0.00001, + options={'ANIMATABLE'}, update=updateNode) + + domV = FloatProperty( + name='V domain', description='V domain', default=1, min=0.00001, + options={'ANIMATABLE'}, update=updateNode) + + shiftU = FloatProperty( + name='U shift', description='U shift', default=0.5, soft_min=-1, soft_max=1, + options={'ANIMATABLE'}, update=updateNode) + + shiftV = FloatProperty( + name='V shift', description='V shift', default=0, soft_min=-1, soft_max=1, + options={'ANIMATABLE'}, update=updateNode) + + def sv_init(self, context): + self.inputs.new('VerticesSocket', "Verts UV") + self.inputs.new('StringsSocket', "U domain").prop_name = 'domU' + self.inputs.new('StringsSocket', "V domain").prop_name = 'domV' + self.outputs.new('StringsSocket', "R") + self.outputs.new('StringsSocket', "G") + self.outputs.new('StringsSocket', "B") + self.outputs.new('StringsSocket', "A") + self.outputs.new('StringsSocket', "BW") + + def draw_buttons(self, context, layout): + layout.label(text="Image:") + layout.prop_search(self, "image_name", bpy.data, 'images', text="") + + row = layout.row(align=True) + col = row.column(align=True) + col.label(text="Tile U:") + col.prop(self, "boundU", text="") + col.label(text="Shift U:") + col.prop(self, "shift_mode_U", text="") + if self.shift_mode_U != 'NONE': + col.prop(self, "shiftU", text="") + + col = row.column(align=True) + col.label(text="Tile V:") + col.prop(self, "boundV", text="") + col.label(text="Shift V:") + col.prop(self, "shift_mode_V", text="") + if self.shift_mode_V != 'NONE': + col.prop(self, "shiftV", text="") + + + def process(self): + verts = self.inputs['Verts UV'].sv_get() + + inputs, outputs = self.inputs, self.outputs + + # inputs + if inputs['Verts UV'].is_linked: + verts = inputs['Verts UV'].sv_get()[0] + else: + verts = [(0,0,0),(0,1,0),(1,0,0),(1,1,0)] + + if inputs['U domain'].is_linked: + domU = inputs['U domain'].sv_get()[0][0] + else: domU = self.domU + + if inputs['V domain'].is_linked: + domV = inputs['V domain'].sv_get()[0][0] + else: domV = self.domV + + # outputs + bw = [[]] + red = [[]] + alpha = [[]] + green = [[]] + blue = [[]] + + if any(socket.is_linked for socket in self.outputs): + + imag = bpy.data.images[self.image_name].pixels[:] + sizeU = bpy.data.images[self.image_name].size[0] + sizeV = bpy.data.images[self.image_name].size[1] + + for vert in verts: + vx = vert[0]*(sizeU-1)/domU + vy = vert[1]*sizeV/domV + u = floor(vx) + v = floor(vy) + u0 = u + inside_domain = True + + if self.shift_mode_U == 'ALTERNATE': + if (v//sizeV)%2: u += floor(sizeU*self.shiftU) + if self.shift_mode_U == 'CONSTANT': + u += floor(sizeU*self.shiftU*(v//sizeV)) + if self.boundU == 'CLIP': + inside_domain = 0 <= u < sizeU + elif self.boundU == 'CYCLIC': + u = u%sizeU + elif self.boundU == 'MIRROR': + if (u//sizeU)%2: u = sizeU - 1 - u%(sizeU) + else: u = u%(sizeU) + elif self.boundU == 'EXTEND': + u = max(0,min(u,sizeU-1)) + + if self.shift_mode_V == 'ALTERNATE': + if (u0//sizeU)%2: v += floor(sizeV*self.shiftV) + if self.shift_mode_V == 'CONSTANT': + v += floor(sizeV*self.shiftV*(u0//sizeU)) + if self.boundV == 'CLIP': + inside_domain = inside_domain and 0 <= v < sizeV + elif self.boundV == 'CYCLIC': + v = v%sizeV + elif self.boundV == 'MIRROR': + if (v//sizeV)%2: v = sizeV - 1 - v%(sizeV) + else: v = v%(sizeV) + elif self.boundV == 'EXTEND': + v = max(0,min(v,sizeV-1)) + + if inside_domain: + index = int(u*4 + v*4*sizeU) + red[0].append(imag[index]) + green[0].append(imag[index+1]) + blue[0].append(imag[index+2]) + alpha[0].append(imag[index+3]) + else: + red[0].append(0) + green[0].append(0) + blue[0].append(0) + alpha[0].append(0) + if outputs['BW'].is_linked: + bw[0] = [0.2126*r + 0.7152*g + 0.0722*b for r,g,b in zip(red[0], green[0], blue[0])] + outputs['BW'].sv_set(bw) + outputs['A'].sv_set(alpha) + outputs['R'].sv_set(red) + outputs['G'].sv_set(green) + outputs['B'].sv_set(blue) + + +def register(): + bpy.utils.register_class(EvaluateImageNode) + + +def unregister(): + bpy.utils.unregister_class(EvaluateImageNode) diff --git a/nodes/analyzer/kd_tree_edges_mk2.py b/nodes/analyzer/kd_tree_edges_mk2.py index e635bb24e0c3151983d33760658a7db9f7d151c5..2c2a37ebd32dc8662f9875b534ee284ab53d6fd2 100644 --- a/nodes/analyzer/kd_tree_edges_mk2.py +++ b/nodes/analyzer/kd_tree_edges_mk2.py @@ -63,6 +63,8 @@ class SvKDTreeEdgesNodeMK2(bpy.types.Node, SverchCustomTreeNode): try: verts = inputs['Verts'].sv_get()[0] linked = outputs['Edges'].is_linked + if not linked: + return except (IndexError, KeyError) as e: return diff --git a/nodes/generator/image.py b/nodes/generator/image.py index fd1399f9109935773cfb3233b415a69c2e518804..ffb789caed77efe571754301cf12694c658cc3c1 100644 --- a/nodes/generator/image.py +++ b/nodes/generator/image.py @@ -45,19 +45,19 @@ class ImageNode(bpy.types.Node, SverchCustomTreeNode): options={'ANIMATABLE'}, update=updateNode) Xvecs = IntProperty( - name='Xvecs', description='Xvecs', default=10, min=2, max=100, + name='Xvecs', description='Xvecs', default=10, min=2, soft_max=100, options={'ANIMATABLE'}, update=updateNode) Yvecs = IntProperty( - name='Yvecs', description='Yvecs', default=10, min=2, max=100, + name='Yvecs', description='Yvecs', default=10, min=2, soft_max=100, options={'ANIMATABLE'}, update=updateNode) Xstep = FloatProperty( - name='Xstep', description='Xstep', default=1.0, min=0.01, max=100, + name='Xstep', description='Xstep', default=1.0, min=0.01, soft_max=100, options={'ANIMATABLE'}, update=updateNode) Ystep = FloatProperty( - name='Ystep', description='Ystep', default=1.0, min=0.01, max=100, + name='Ystep', description='Ystep', default=1.0, min=0.01, soft_max=100, options={'ANIMATABLE'}, update=updateNode) def sv_init(self, context): @@ -82,12 +82,12 @@ class ImageNode(bpy.types.Node, SverchCustomTreeNode): # inputs if inputs['vecs X'].is_linked: - IntegerX = min(int(inputs['vecs X'].sv_get()[0][0]), 100) + IntegerX = min(int(inputs['vecs X'].sv_get()[0][0]), 1000000) else: IntegerX = int(self.Xvecs) if inputs['vecs Y'].is_linked: - IntegerY = min(int(inputs['vecs Y'].sv_get()[0][0]), 100) + IntegerY = min(int(inputs['vecs Y'].sv_get()[0][0]), 1000000) else: IntegerY = int(self.Yvecs) @@ -109,7 +109,7 @@ class ImageNode(bpy.types.Node, SverchCustomTreeNode): if outputs['edgs'].is_linked: listEdg = [] - + for i in range(IntegerY): for j in range(IntegerX-1): listEdg.append((IntegerX*i+j, IntegerX*i+j+1)) @@ -127,7 +127,7 @@ class ImageNode(bpy.types.Node, SverchCustomTreeNode): listPlg.append((IntegerX*j+i, IntegerX*j+i+1, IntegerX*j+i+IntegerX+1, IntegerX*j+i+IntegerX)) plg = [list(listPlg)] outputs['pols'].sv_set(plg) - + def make_vertices(self, delitelx, delitely, stepx, stepy, image_name): lenx = bpy.data.images[image_name].size[0] @@ -150,7 +150,7 @@ class ImageNode(bpy.types.Node, SverchCustomTreeNode): # каждый пиксель кодируется RGBA, и записан строкой, без разделения на строки и столбцы. middle = (imag[addition]*R+imag[addition+1]*G+imag[addition+2]*B)*imag[addition+3] vertex = [x*stepx[x], y*stepy[y], middle] - vertices.append(vertex) + vertices.append(vertex) addition += int(xcoef*4) return vertices diff --git a/nodes/generator/line_mk3.py b/nodes/generator/line_mk3.py new file mode 100644 index 0000000000000000000000000000000000000000..cfaefc875b3dcc5b3b6a0722a4878e02a0045e29 --- /dev/null +++ b/nodes/generator/line_mk3.py @@ -0,0 +1,240 @@ +# ##### BEGIN GPL LICENSE BLOCK ##### +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# ##### END GPL LICENSE BLOCK ##### + +import bpy +from bpy.props import IntProperty, FloatProperty, BoolProperty, EnumProperty, FloatVectorProperty +from sverchok.node_tree import SverchCustomTreeNode +from sverchok.data_structure import updateNode, fullList, match_long_repeat +from sverchok.utils.modules.geom_utils import interp_v3_v3v3, normalize, add_v3_v3v3, sub_v3_v3v3 + +directionItems = [ + ("X", "X", "Along X axis", 0), + ("Y", "Y", "Along Y axis", 1), + ("Z", "Z", "Along Z axis", 2), + ("AB", "AB", "Between 2 points", 3), + ("OD", "OD", "Origin and Direction", 4), + ] + + +def make_line(steps, center, direction, vert_a, vert_b): + if direction == "X": + vec = lambda l: (l, 0.0, 0.0) + elif direction == "Y": + vec = lambda l: (0.0, l, 0.0) + elif direction == "Z": + vec = lambda l: (0.0, 0.0, l) + elif direction in ["AB", "OD"]: + vec = lambda l: interp_v3_v3v3(vert_a, vert_b, l) + + verts = [] + add_vert = verts.append + x = -sum(steps) / 2 if center else 0 + for s in [0.0] + steps: + x = x + s + add_vert(vec(x)) + edges = [[i, i + 1] for i in range(len(steps))] + return verts, edges + + +class SvLineNodeMK3(bpy.types.Node, SverchCustomTreeNode): + """ + Triggers: Line, segment. + Tooltip: Generate line. + """ + bl_idname = 'SvLineNodeMK3' + bl_label = 'Line' + bl_icon = 'GRIP' + + def update_size_socket(self, context): + """ need to do UX transformation before updating node""" + size_socket = self.inputs["Size"] + size_socket.hide_safe = not self.normalize + + updateNode(self, context) + + def update_vect_socket(self, context): + """ need to do UX transformation before updating node""" + si = self.inputs + sd = self.direction + if sd == "OD" and not si[3].name[0] == "O": + si[3].name = "Origin" + si[4].name = "Direction" + si[3].prop_name = 'v3_origin' + si[4].prop_name = 'v3_dir' + elif sd == "AB" and not si[3].name[0] == "A": + si[3].name = "A" + si[4].name = "B" + si[3].prop_name = 'v3_input_0' + si[4].prop_name = 'v3_input_1' + + ortho = sd not in ["AB", "OD"] + if (not ortho and si[3].hide_safe) or ortho: + si[3].hide_safe = ortho + si[4].hide_safe = ortho + + updateNode(self, context) + + direction = EnumProperty( + name="Direction", items=directionItems, + default="X", update=update_vect_socket) + + num = IntProperty( + name='Num Verts', description='Number of Vertices', + default=2, min=2, update=updateNode) + + step = FloatProperty( + name='Step', description='Step length', + default=1.0, update=updateNode) + + center = BoolProperty( + name='Center', description='Center the line', + default=False, update=updateNode) + + normalize = BoolProperty( + name='Normalize', description='Normalize line to size', + default=False, update=update_size_socket) + + size = FloatProperty( + name='Size', description='Size of line', + default=10.0, update=updateNode) + + v3_input_0 = FloatVectorProperty( + name='A', description='Starting point', + size=3, default=(0, 0, 0), + update=updateNode) + + v3_input_1 = FloatVectorProperty( + name='B', description='End point', + size=3, default=(0.5, 0.5, 0.5), + update=updateNode) + + v3_origin = FloatVectorProperty( + name='Origin', description='Origin of line', + size=3, default=(0, 0, 0), + update=updateNode) + + v3_dir = FloatVectorProperty( + name='Direction', description='Direction', + size=3, default=(1, 1, 1), + update=updateNode) + + def set_size_socket(self): + size_socket = self.inputs.new('StringsSocket', "Size") + size_socket.prop_name = 'size' + size_socket.hide_safe = not self.normalize + + def set_vector_sockets(self): + si = self.inputs + si.new('VerticesSocket', "A").prop_name = 'v3_input_0' + si.new('VerticesSocket', "B").prop_name = 'v3_input_1' + si[3].hide_safe = self.direction not in ["AB", " OD"] + si[4].hide_safe = self.direction not in ["AB", " OD"] + + def sv_init(self, context): + si = self.inputs + si.new('StringsSocket', "Num").prop_name = 'num' + si.new('StringsSocket', "Step").prop_name = 'step' + self.set_size_socket() + self.set_vector_sockets() + self.outputs.new('VerticesSocket', "Vertices", "Vertices") + self.outputs.new('StringsSocket', "Edges", "Edges") + + def draw_buttons(self, context, layout): + col = layout.column(align=True) + row = col.row(align=True) + row.prop(self, "direction", expand=True) + row = col.row(align=True) + row.prop(self, "center", toggle=True) + row.prop(self, "normalize", toggle=True) + + def get_data(self): + c, d = self.center, self.direction + input_num = self.inputs["Num"].sv_get() + input_step = self.inputs["Step"].sv_get() + normal_size = [2.0 if c else 1.0] + + if self.normalize: + + normal_size = self.inputs["Size"].sv_get()[0] + + params = [input_num, input_step, normal_size] + + if d in ["AB", "OD"]: + v_a = self.inputs[3].sv_get()[0] + v_b = self.inputs[4].sv_get()[0] + params.append(v_a) + params.append(v_b) + + return match_long_repeat(params) + + def define_steplist(self, step_list, s, n, nor, normal): + + for num in n: + num = max(2, num) + s = s[:(num - 1)] # shorten if needed + fullList(s, num - 1) # extend if needed + step_list.append([S * nor / sum(s) for S in s] if normal else s) + + def process_vectors(self, pts_list, d, va, vb): + if d == "AB" and self.normalize: + vb = add_v3_v3v3(normalize(sub_v3_v3v3(vb, va)), va) + elif d == "OD": + vb = add_v3_v3v3(normalize(vb), va) + pts_list.append((va, vb)) + + def process(self): + if not any(s.is_linked for s in self.outputs): + return + + c, d = self.center, self.direction + step_list = [] + pts_list = [] + verts_out, edges_out = [], [] + normal = self.normalize or d == "AB" + advanced = d in ["AB", "OD"] + params = self.get_data() + if advanced: + for p in zip(*params): + n, s, nor, va, vb = p + self.define_steplist(step_list, s, n, nor, normal) + self.process_vectors(pts_list, d, va, vb) + for s, vc in zip(step_list, pts_list): + r1, r2 = make_line(s, c, d, vc[0], vc[1]) + verts_out.append(r1) + edges_out.append(r2) + else: + for p in zip(*params): + n, s, nor = p + self.define_steplist(step_list, s, n, nor, normal) + for s in step_list: + r1, r2 = make_line(s, c, d, [], []) + verts_out.append(r1) + edges_out.append(r2) + + if self.outputs['Vertices'].is_linked: + self.outputs['Vertices'].sv_set(verts_out) + if self.outputs['Edges'].is_linked: + self.outputs['Edges'].sv_set(edges_out) + + +def register(): + bpy.utils.register_class(SvLineNodeMK3) + + +def unregister(): + bpy.utils.unregister_class(SvLineNodeMK3) diff --git a/nodes/generators_extended/hypercube.py b/nodes/generators_extended/hypercube.py new file mode 100644 index 0000000000000000000000000000000000000000..19f69aa86363e6ff7e79810874aaaab7f325b998 --- /dev/null +++ b/nodes/generators_extended/hypercube.py @@ -0,0 +1,492 @@ +# ##### BEGIN GPL LICENSE BLOCK ##### +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# ##### END GPL LICENSE BLOCK ##### + +import bpy +from bpy.props import IntProperty, FloatProperty, BoolProperty, EnumProperty, FloatVectorProperty + +from math import sin, cos, pi, sqrt, radians +from mathutils import Vector, Matrix +import copy + +from sverchok.node_tree import SverchCustomTreeNode +from sverchok.data_structure import updateNode, match_long_repeat +import itertools + +# dictionary to store once the unit/untransformed hypercube verts, edges & polys +_hypercube = {} + + +def flip(n, v): + ''' + Flips the N-th coordinate of the binary N dimensional vector between 0 and 1. + + e.g. for v = [1, 0, 0, 1] => flip(1, v) = [1, 1, 0, 1] + n : 0 1 2 3 0 1 2 3 + ''' + return [(v[i] + 1) % 2 if i == n else v[i] for i in range(len(v))] + + return [] + + +def flipper(v): + ''' + Flips each dimension of the binary N dimensional vector between 0 and 1 + returning the list of all single flips of the given binary vector. + + Note: In essense this is equivalent to generating all vertices in a N + dimensional Hypercube touching a given ND vertex along all dimensions. + + e.g. v = [1, 0, 1] => flipper(v) = [[0, 0, 1], [1, 1, 1], [1, 0, 0]] + ''' + links = [] + for n in range(len(v)): + links.append(flip(n, v)) + return links + + +def edges_touching(v): + ''' + Returns the list of edges connecting the binary N dimensional vertex + of a N dimensional Hypercube to all the adjacent N dimensional vertices + along all dimensions. + + e.g. v = [1, 0, 1] => + edges(v) = [[[1, 0, 1], [0, 0, 1]], [[1, 0, 1], [1, 1, 1]], [[1, 0, 1], [1, 0, 0]]] + ''' + return [list(tup) for tup in itertools.product([v], flipper(v))] + + +def index(v): + ''' + Convert a binary N dimensional vector into a decimal index. + + Note: this essentially indexes the vertices in a Hypercube. + + e.g. v = [1,0,1] => 101 (binary) = 5 (decimal) + ''' + a = 0 + N = len(v) + for i in range(N): + a += v[i] << (N - 1 - i) + return a + + +def edgesIDs_touching(v): + ''' + Return the list of all edges (as a pair of vertex indices) touching a given vertex. + + e.g. v = [1, 0, 1] => + edges(v) = [[[1, 0, 1], [0, 0, 1]], [[1, 0, 1], [1, 1, 1]], [[1, 0, 1], [1, 0, 0]]] + edgesID(v) = [[ 5 , 1 ], [ 5 , 7 ], [ 5 , 4 ]] + ''' + es = edges_touching(v) + return [[index(v1), index(v2)] for v1, v2 in es] + + +def create_cells2(): + vertList = [] + indexList = [] + edgeList = [] + faceList = [] + + indexList = get_cell_verts_IDs(4) + edgeList = get_cell_edges_IDs(4) + + return vertList, indexList, edgeList, faceList + + +def create_cells(): + ''' + Create the Hypercube faces (cells) and indices forming each face + + A cell has 8 vertices (a cube) and corresponds to keeping one of the i,j,k,l constants at 0 or 1. + ''' + vertList = [] + indexList = [] + edgeList = [] + faceList = [] + for i in [0, 1]: + verts = [[i, j, k, l] for j in [0, 1] for k in [0, 1] for l in [0, 1]] + vertList.append(verts) + indices = [index(v) for v in verts] + indexList.append(indices) + edges = [edgesIDs_touching(v) for v in verts] + edgeList.append(edges) + for j in [0, 1]: + verts = [[i, j, k, l] for i in [0, 1] for k in [0, 1] for l in [0, 1]] + vertList.append(verts) + indexList.append(indices) + edges = [edgesIDs_touching(v) for v in verts] + edgeList.append(edges) + for k in [0, 1]: + verts = [[i, j, k, l] for i in [0, 1] for j in [0, 1] for l in [0, 1]] + vertList.append(verts) + indices = [index(v) for v in verts] + indexList.append(indices) + edges = [edgesIDs_touching(v) for v in verts] + edgeList.append(edges) + for l in [0, 1]: + verts = [[i, j, k, l] for i in [0, 1] for j in [0, 1] for k in [0, 1]] + vertList.append(verts) + indices = [index(v) for v in verts] + indexList.append(indices) + edges = [edgesIDs_touching(v) for v in verts] + edgeList.append(edges) + + return vertList, indexList, edgeList, faceList + + +def get_cells_verts(): + cells = [] + cellIDs = [] + cube = [[i1, i2, i3] for i1 in [0, 1] for i2 in [0, 1] for i3 in[0, 1]] + for n in range(4): # 0 1 2 3 (for each dimension) + for b in range(2): # 0 1 (flip side) + cell = copy.deepcopy(cube) + [v.insert(n, b) for v in cell] + cells.append(cell) + ids = [index(v) for v in cell] + cellIDs.append(ids) + return cells, cellIDs + + +def get_verts(n): + return [list(v) for v in list(itertools.product([0, 1], repeat=n))] + + +def get_verts_IDs(n): + verts = get_verts(n) + return [index(v) for v in verts] + + +def get_edges(n): + return [[a, a | 1 << l] for a in range(2**n) for l in range(n) if ~ a & 1 << l] + + +def get_edges_IDs(n): + edges = get_edges(n) + return [[index(v1), index(v2)] for v1, v2 in edges] + + +def clear_bit(v, n): + ''' + Clear the n-th bit in the binary representation of v. + e.g. v = 101101, n = 3 => 100101 + ''' + return v & (1 << n) + + +def set_bit(v, n): + ''' + Set the n-th bit in the binary representation of v. + e.g. v = 101101, n = 1 => 101111 + ''' + return v | (1 << n) + + +def flip_bit(v, n): + ''' + Flip the n-th bit in the binary representation of v. + e.g. v = 101101, n = 3 => 100101 + ''' + return v ^ (1 << n) + + +def insert_bit(v, b, n): + ''' + Insert a bit (0/1) at the n-th bit in the binary representation of v + shifting the higher end bits by one. + e.g. v = 101101, b = 0, n = 2 => 1011001 + ''' + return (v & ~(2**n - 1)) << 1 | b << n | v & (2**n - 1) + + +def vinarize(v, n): + ''' + Convert an integer into a list of its binary representation. + e.g. v = 6, n = 7 => [0, 0, 0, 0, 1, 1, 0] + ''' + return [(v & (1 << l)) >> l for l in reversed(range(n))] + + +def get_cell_verts(n): + verts = get_verts_IDs(n - 1) + cellv = [] + for m in range(n): + for b in [0, 1]: + cell = [] + for v in verts: + # print("m=", m) + # print("b=", b) + # print("v=", vinarize(v, n)) + vx = insert_bit(v, b, m) + # print("vx=", vinarize(vx, n)) + # cell.append(vx) + cell.append(vinarize(vx, n)) + # print("\n") + cellv.append(cell) + return cellv + + +def get_cell_verts_IDs(n): + cells = get_cell_verts(n) + return [[index(v) for v in cell] for cell in cells] + + +def get_cell_edges(n): + edges = get_edges(n - 1) + celle = [] + for m in range(n): + for b in [0, 1]: + cell = [] + for v1, v2 in edges: + # print("m=", m) + # print("b=", b) + # print("v=", vinarize(v, n)) + v1x = insert_bit(v1, b, m) + v2x = insert_bit(v2, b, m) + # print("vx=", vinarize(vx, n)) + # cell.append(vx) + cell.append([vinarize(v1x, n), vinarize(v2x, n)]) + # print("\n") + celle.append(cell) + return celle + + +def get_cell_edges_IDs(n): + cells = get_cell_edges(n) + return [[[index(v1), index(v2)] for v1, v2 in cell] for cell in cells] + + +def generate_hypercube(): + ''' + Generate the unit Hypercube verts, edges, faces, cells. + + Note: This is generated ONCE and cached during the first invocation. + ''' + if _hypercube: + return + + # hypercube = [[i, j, k, l] for i in [0, 1] for j in [0, 1] for k in [0, 1] for l in [0, 1]] # 4D indices + # hypercube = [list(v) for v in itertools.product([0, 1], repeat=4)] + hypercube = list(itertools.product([0, 1], repeat=4)) + + # TODO: find a better (and working) way to do this + edges = [] + faces = [] + # for k in [0, 1]: # tries (and fails) to create the faces with normals pointing to the outside + # for l in [0, 1]: + # faces.append(list(map(hypercube.index, [[i ^ j, j, k, l] for j in [k, k ^ 1] for i in [l, l ^ 1]]))) + # faces.append(list(map(hypercube.index, [[i ^ j, k, j, l] for j in [k, k ^ 1] for i in [l, l ^ 1]]))) + # faces.append(list(map(hypercube.index, [[i ^ j, k, l, j] for j in [k, k ^ 1] for i in [l, l ^ 1]]))) + # faces.append(list(map(hypercube.index, [[k, i ^ j, j, l] for j in [k, k ^ 1] for i in [l, l ^ 1]]))) + # faces.append(list(map(hypercube.index, [[k, i ^ j, l, j] for j in [k, k ^ 1] for i in [l, l ^ 1]]))) + # faces.append(list(map(hypercube.index, [[k, l, i ^ j, j] for j in [k, k ^ 1] for i in [l, l ^ 1]]))) + + # verts = [Vector([x, y, z, w]) for x, y, z, w in hypercube] + verts = [Vector(v) for v in hypercube] + + edges = get_edges(4) + + cells = {} + cells["verts"] = get_cell_verts_IDs(4) + cells["edges"] = get_cell_edges_IDs(4) + cells["vertsIDs"] = [] + cells["faces"] = [] + + # print("***", type(cells)) + names = [":".join(map(str, a)) for a in hypercube] + + # store hypercube's verts, edges, polys & cells in a global dictionary + _hypercube["verts"] = verts + _hypercube["edges"] = edges + _hypercube["faces"] = faces + _hypercube["cells"] = cells + _hypercube["names"] = names + + +def get_hypercube(): + ''' + Get the unit hypercube's verts, edges and polys (generate one if needed) + ''' + if not _hypercube: + generate_hypercube() + + return _hypercube["verts"], _hypercube["edges"], _hypercube["faces"], _hypercube["cells"], _hypercube["names"] + + +class SvHyperCubeNode(bpy.types.Node, SverchCustomTreeNode): + ''' HyperCube ''' + bl_idname = 'SvHyperCubeNode' + bl_label = 'Hypercube' + + def sv_init(self, context): + self.outputs.new('VerticesSocket', "Verts") + self.outputs.new('StringsSocket', "Edges") + self.outputs.new('StringsSocket', "Polys") + + self.outputs.new('StringsSocket', "Cells Verts") + self.outputs.new('StringsSocket', "Cells Verts IDs") + self.outputs.new('StringsSocket', "Cells Edges") + self.outputs.new('StringsSocket', "Cells Faces") + self.outputs.new('StringsSocket', "Vert Names") + + + def process(self): + # return if no outputs are connected + outputs = self.outputs + if not any(s.is_linked for s in outputs): + return + + # input values lists + inputs = self.inputs + + # params = match_long_repeat([input_a1, input_a2, input_a3, input_a4, + # input_a5, input_a6, input_d, input_s, input_t]) + + verts4D, edges, polys, cells, names = get_hypercube() + verts = [list(v) for v in verts4D] + + vertList = [] + edgeList = [] + polyList = [] + # for a1, a2, a3, a4, a5, a6, d, s, t in zip(*params): + # a1 *= aU + # a2 *= aU + # a3 *= aU + # a4 *= aU + # a5 *= aU + # a6 *= aU + # verts = transform_hypercube(verts4D, a1, a2, a3, a4, a5, a6, d, s, t) + # vertList.append(verts) + # edgeList.append(edges) + # polyList.append(polys) + vertList.append(verts) + edgeList.append(edges) + polyList.append(polys) + + # cells, indices, edges, faces = create_cells2() + cellVerts = cells["verts"] + cellVertsIDs = cells["vertsIDs"] + cellEdges = cells["edges"] + cellFaces = cells["faces"] + + + outputs['Verts'].sv_set(vertList) + outputs['Edges'].sv_set(edgeList) + outputs['Polys'].sv_set(polyList) + + outputs['Cells Verts'].sv_set(cellVerts) + outputs['Cells Verts IDs'].sv_set(cellVertsIDs) + outputs['Cells Edges'].sv_set(cellEdges) + outputs['Cells Faces'].sv_set(cellFaces) + + outputs['Vert Names'].sv_set([names]) + + +def register(): + bpy.utils.register_class(SvHyperCubeNode) + + +def unregister(): + bpy.utils.unregister_class(SvHyperCubeNode) + + +''' + TODO: + + matrix multiplication in 4D (4D TRS) + toggle on/off hyper faces + select all edges along a given dimension + hyperplane intersections (verts 0D, edges 1D, faces 2D, cells 3D) + scale, rotate, translate cells of a hypercube (unfolding) + 4D point, 4D line, 4D plane locations in 4D/3D space + 4D camera/projection origin + edge/face/cell centroids + + + verts in a cell k/n (n/n is the whole hypercube) + E(k,n) = 2^k * c(n,k) + for n = 4 + cell 1/4 = 2^1 * c(1,4) = 2 * 4 = 8 : cells/cubes @ (4-1=3 -> 2^3=8 verts) + cell 2/4 = 2^2 * c(2,4) = 4 * 6 = 24 : faces @ (4-2=2 -> 2^2=4 verts) + cell 3/4 = 2^3 * c(3,4) = 8 * 4 = 32 : edges @ (4-3=1 -> 2^1=2 verts) + cell 4/4 = 2^4 * c(4,4) = 16 * 1 = 16 : vertices @ (4-4=0 -> 2^0=1 verts) + + x x x x + 1 1 + 1 1 + 1 1 + 1 1 + 1 1 + 1 1 + + mkdir -p hypercube/cells/faces/edges/verts + mkdir -p hypercube/cells/faces/verts + mkdir -p hypercube/cells/edges/verts + mkdir -p hypercube/cells/verts + + mkdir -p hypercube/faces/edges/verts + mkdir -p hypercube/faces/verts + + mkdir -p hypercube/edges/verts + + mkdir -p hypercube/verts + + C [dimension] : component : count : index range + + H [4] : hypercube : 1 : 0 + │ + ├── C [3] : cells : 8 : 0->7 + │   ├── faces : 6 : 0->23 (F) + │   │   ├── edges : 4 : 0->31 (E) + │   │   │ └── verts : 2 : 0->15 (V) + │   │   └── verts : 4 : 0->15 (V) + │   ├── edges : 12 : 0->31 (E) * + │   │   └── verts : 2 : 0->15 (V) * + │   └── verts : 8 : 0->15 (V) * + │     + ├── F [2] : faces : 24 : 0->23 + │   ├── edges : 4 : 0->31 (E) + │   │   └── verts : 2 : 0->15 (V) + │   └── verts : 4 : 0->15 (V) + │      + ├── E [1] : edges : 32 : 0->31 * + │   └── verts : 2 : 0->15 (V) * + │   + └── V [0] : verts : 16 : 0->15 * + + H(1)C(4)F(2)E(3)V(2) - selection + + 5D Hypercube rotations (10): + + XY + XZ + XW + XT + + YZ + YW + YT + + ZW + ZT + + WT + +''' diff --git a/nodes/generators_extended/smooth_lines.py b/nodes/generators_extended/smooth_lines.py index 835ad1af2a983357f97d82be68a29ba4be688d08..61b1084011aa2257d6d2f95d0ee06f36f14452fe 100644 --- a/nodes/generators_extended/smooth_lines.py +++ b/nodes/generators_extended/smooth_lines.py @@ -37,9 +37,18 @@ def find_projected_arc_center(p1, p2, b, radius=0.5): b = Vector(b) c = Vector(p2) + a = (a-b).normalized() + b + c = (c-b).normalized() + b + focal = (a + c) / 2.0 focal_length = (b-focal).length - angleA = (a-b).angle(c-b) / 2.0 + + try: + angleA = (a-b).angle(c-b) / 2.0 + except ValueError as e: + print('smoothlines encountered non zero length vectors') + # Vector.angle(other): zero length vectors have no valid angle + return None sideA = radius sideB = sideA / tan(angleA) @@ -93,6 +102,8 @@ def spline_points(points, weights, index, params): p2 = Vector(c).lerp(Vector(b), weight_to_use_2)[:] elif params.mode == 'arc': pts = find_projected_arc_center(c, a, b, radius=w2) + if not pts: + return [b] return three_point_arc(pts=pts, num_verts=divs, make_edges=False)[0] return [v[:] for v in bezlerp(p1, b, b, p2, divs)] diff --git a/nodes/list_main/statistics.py b/nodes/list_main/statistics.py new file mode 100644 index 0000000000000000000000000000000000000000..86b033d17b565734513f8bbdf03282310f93b6f0 --- /dev/null +++ b/nodes/list_main/statistics.py @@ -0,0 +1,188 @@ +# ##### BEGIN GPL LICENSE BLOCK ##### +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# ##### END GPL LICENSE BLOCK ##### + +import bpy +from bpy.props import EnumProperty, IntProperty, FloatProperty, BoolProperty + +from sverchok.node_tree import SverchCustomTreeNode +from sverchok.data_structure import updateNode, match_long_repeat +from sverchok.utils.modules.statistics_functions import * + +functions = { + "ALL STATISTICS": (0, 0), + "SUM": (10, get_sum), + "SUM OF SQUARES": (11, get_sum_of_squares), + "SUM OF INVERSIONS": (12, get_sum_of_inversions), + "PRODUCT": (13, get_product), + "AVERAGE": (14, get_average), + "GEOMETRIC MEAN": (15, get_geometric_mean), + "HARMONIC MEAN": (16, get_harmonic_mean), + "STANDARD DEVIATION": (17, get_standard_deviation), + "ROOT MEAN SQUARE": (18, get_root_mean_square), + "SKEWNESS": (19, get_skewness), + "KURTOSIS": (20, get_kurtosis), + "MINIMUM": (21, get_minimum), + "MAXIMUM": (22, get_maximum), + "MEDIAN": (23, get_median), + "PERCENTILE": (24, get_percentile), + "HISTOGRAM": (25, get_histogram) +} + + +modeItems = [ + ("INT", "Int", "", "", 0), + ("FLOAT", "Float", "", "", 1)] + +functionItems = [(k, k.title(), "", "", s[0]) for k, s in sorted(functions.items(), key=lambda k: k[1][0])] + + +class SvListStatisticsNode(bpy.types.Node, SverchCustomTreeNode): + ''' + Triggers: Sum, Avg, Min, Max + Tooltip: Statistical quantities: sum, average, standard deviation, min, max, product... + ''' + bl_idname = 'SvListStatisticsNode' + bl_label = 'List Statistics' + bl_icon = 'OUTLINER_OB_EMPTY' + + def update_function(self, context): + if self.function == "ALL STATISTICS": + self.inputs["Percentage"].hide_safe = False + self.inputs["Bins"].hide_safe = False + self.inputs["Size"].hide_safe = not self.normalize + self.outputs[0].name = "Names" + self.outputs[1].name = "Values" + else: + for name in ["Percentage", "Bins", "Size"]: + self.inputs[name].hide_safe = True + if self.function == "PERCENTILE": + self.inputs["Percentage"].hide_safe = False + elif self.function == "HISTOGRAM": + self.inputs["Bins"].hide_safe = False + self.inputs["Size"].hide_safe = not self.normalize + + self.outputs[0].name = "Name" + self.outputs[1].name = "Value" + + updateNode(self, context) + + def update_normalize(self, context): + socket = self.inputs["Size"] + socket.hide_safe = not self.normalize + + updateNode(self, context) + + mode = EnumProperty( + name="Mode", items=modeItems, default="FLOAT", update=updateNode) + + function = EnumProperty( + name="Function", items=functionItems, update=update_function) + + percentage = FloatProperty( + name="Percentage", + default=0.75, min=0.0, max=1.0, update=updateNode) + + bins = IntProperty( + name="Bins", + default=10, min=1, update=updateNode) + + normalize = BoolProperty( + name="Normalize", description="Normalize the bins to a normalize size", + default=False, update=update_normalize) + + normalized_size = FloatProperty( + name="Size", description="The normalized size of the bins", + default=10.0, update=updateNode) + + def draw_buttons(self, context, layout): + layout.prop(self, "mode", expand=True) + layout.prop(self, "function", text="") + if self.function in ["HISTOGRAM", "ALL STATISTICS"]: + layout.prop(self, "normalize", toggle=True) + + def sv_init(self, context): + self.width = 150 + self.inputs.new('StringsSocket', "Data") + self.inputs.new('StringsSocket', "Percentage").prop_name = "percentage" + self.inputs.new('StringsSocket', "Bins").prop_name = "bins" + self.inputs.new('StringsSocket', "Size").prop_name = "normalized_size" + self.outputs.new('StringsSocket', "Names") + self.outputs.new('StringsSocket', "Values") + self.function = "AVERAGE" + + def get_statistics_function(self): + return functions[self.function][1] + + def process(self): + outputs = self.outputs + # return if no outputs are connected + if not any(s.is_linked for s in outputs): + return + + inputs = self.inputs + input_D = inputs["Data"].sv_get() + input_P = inputs["Percentage"].sv_get()[0] + input_B = inputs["Bins"].sv_get()[0] + input_S = inputs["Size"].sv_get()[0] + + # sanitize the inputs + input_P = list(map(lambda x: max(0, min(1, x)), input_P)) + input_B = list(map(lambda x: max(1, x), input_B)) + + if self.mode == "INT": + input_P = list(map(lambda x: int(x), input_P)) + + if self.function == "ALL STATISTICS": + functionNames = [fn[0] for fn in functionItems[1:]] + else: + functionNames = [self.function] + + params = match_long_repeat([input_D, input_P, input_B, input_S]) + + allNames = [] + allValues = [] + for functionName in functionNames: + statistics_function = functions[functionName][1] + quantityList = [] + for d, p, b, s in zip(*params): + if functionName == "PERCENTILE": + quantity = statistics_function(d, p) + elif functionName == "HISTOGRAM": + quantity = statistics_function(d, b, self.normalize, s) + else: + quantity = statistics_function(d) + + if functionName != "HISTOGRAM": + if self.mode == "INT": + quantity = int(quantity) + + quantityList.append(quantity) + + allNames.append(functionName) + allValues.append(quantityList) + + outputs[0].sv_set(allNames) + outputs[1].sv_set(allValues) + + +def register(): + bpy.utils.register_class(SvListStatisticsNode) + + +def unregister(): + bpy.utils.unregister_class(SvListStatisticsNode) diff --git a/nodes/list_mutators/combinatorics.py b/nodes/list_mutators/combinatorics.py new file mode 100644 index 0000000000000000000000000000000000000000..af4d2184564a08071cb5f0f19d3f5bed172ee078 --- /dev/null +++ b/nodes/list_mutators/combinatorics.py @@ -0,0 +1,167 @@ +# ##### BEGIN GPL LICENSE BLOCK ##### +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# ##### END GPL LICENSE BLOCK ##### + +import bpy +from bpy.props import IntProperty, EnumProperty + +from sverchok.node_tree import SverchCustomTreeNode +from sverchok.data_structure import (match_long_repeat, updateNode) + +from itertools import (product, permutations, combinations) + +operations = { + "PRODUCT": (10, lambda s, r: product(*s, repeat=r)), + "PERMUTATIONS": (20, lambda s, l: permutations(s, l)), + "COMBINATIONS": (30, lambda s, l: combinations(s, l)) +} + +operationItems = [(k, k.title(), "", s[0]) for k, s in sorted(operations.items(), key=lambda k: k[1][0])] + +ABC = tuple('ABCDEFGHIJKLMNOPQRSTUVWXYZ') # input socket labels + +multiple_input_operations = {"PRODUCT"} + + +class SvCombinatoricsNode(bpy.types.Node, SverchCustomTreeNode): + """ + Triggers: Product, Permutations, Combinations + Tooltip: Generate various combinatoric operations + """ + bl_idname = 'SvCombinatoricsNode' + bl_label = 'Combinatorics' + + def update_operation(self, context): + self.label = self.operation.title() + self.update_sockets() + updateNode(self, context) + + operation = EnumProperty( + name="Operation", items=operationItems, + description="Operation type", default="PRODUCT", + update=update_operation) + + repeat = IntProperty( + name='Repeat', description='Repeat the list inputs this many times', + default=1, min=1, update=updateNode) + + length = IntProperty( + name='Length', description='Limit the elements to operate on to this value', + default=1, min=0, update=updateNode) + + def sv_init(self, context): + self.inputs.new('StringsSocket', "Repeat").prop_name = "repeat" + self.inputs.new('StringsSocket', "Length").prop_name = "length" + self.inputs.new('StringsSocket', "A") + self.inputs.new('StringsSocket', "B") + + self.outputs.new('StringsSocket', "Result") + + self.update_operation(context) + + def update(self): + ''' Add/remove sockets as A-Z sockets are connected/disconnected ''' + + # not a multiple input operation ? => no need to update sockets + if self.operation not in multiple_input_operations: + return + + inputs = self.inputs + + # get all existing A-Z sockets (connected or not) + inputs_AZ = list(filter(lambda s: s.name in ABC, inputs)) + + # last A-Z socket connected ? => add an empty A-Z socket at the end + if inputs_AZ[-1].links: + name = ABC[len(inputs_AZ)] # pick the next letter A to Z + inputs.new("StringsSocket", name) + + else: # last input disconnected ? => remove all but last unconnected + while len(inputs_AZ) > 2 and not inputs_AZ[-2].links: + s = inputs_AZ[-1] + inputs.remove(s) + inputs_AZ.remove(s) + + def update_sockets(self): + ''' Update sockets based on selected operation ''' + + inputs = self.inputs + + # update the A-Z input sockets + if self.operation in multiple_input_operations: + if not "B" in inputs: + inputs.new("StringsSocket", "B") + else: + for a in ABC[1:]: # remove all B-Z inputs (keep A) + if a in inputs: + inputs.remove(inputs[a]) + + # update the other sockets + if self.operation in {"PRODUCT"}: + if inputs["Repeat"].hide: + inputs["Repeat"].hide_safe = False + inputs["Length"].hide_safe = True + elif self.operation in {"COMBINATIONS", "PERMUTATIONS"}: + inputs["Repeat"].hide_safe = True + if inputs["Length"].hide: + inputs["Length"].hide_safe = False + + def draw_buttons(self, context, layout): + layout.prop(self, "operation", text="") + + def process(self): + outputs = self.outputs + # return if no outputs are connected + if not any(s.is_linked for s in outputs): + return + + inputs = self.inputs + + all_AZ_sockets = list(filter(lambda s: s.name in ABC, inputs)) + connected_AZ_sockets = list(filter(lambda s: s.is_linked, all_AZ_sockets)) + + # collect the data inputs from all connected AZ sockets + I = [s.sv_get()[0] for s in connected_AZ_sockets] + + if self.operation == "PRODUCT": + R = inputs["Repeat"].sv_get()[0] + R = list(map(lambda x: max(1, int(x)), R)) + parameters = match_long_repeat([[I], R]) + else: # PERMUTATIONS / COMBINATIONS + L = inputs["Length"].sv_get()[0] + L = list(map(lambda x: max(0, int(x)), L)) + parameters = match_long_repeat([I, L]) + + function = operations[self.operation][1] + + resultList = [] + for sequence, v in zip(*parameters): + if self.operation in {"PERMUTATIONS", "COMBINATIONS"}: + if v == 0 or v > len(sequence): + v = len(sequence) + result = [list(a) for a in function(sequence, v)] + resultList.append(result) + + outputs["Result"].sv_set(resultList) + + +def register(): + bpy.utils.register_class(SvCombinatoricsNode) + + +def unregister(): + bpy.utils.unregister_class(SvCombinatoricsNode) diff --git a/nodes/list_mutators/polygon_sort.py b/nodes/list_mutators/polygon_sort.py new file mode 100644 index 0000000000000000000000000000000000000000..b60270d996c072cbfc15c24f300136d4371c280d --- /dev/null +++ b/nodes/list_mutators/polygon_sort.py @@ -0,0 +1,289 @@ +# ##### BEGIN GPL LICENSE BLOCK ##### +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# ##### END GPL LICENSE BLOCK ##### + +import bpy +from bpy.props import BoolProperty, EnumProperty, FloatVectorProperty, StringProperty +from sverchok.node_tree import SverchCustomTreeNode +from sverchok.data_structure import updateNode, fullList, match_long_repeat +from sverchok.utils.sv_operator_mixins import SvGenericCallbackWithParams +from mathutils import Vector +from math import acos, pi, sqrt + +modeItems = [ + ("P", "Point", "Sort by distance to a point", 0), + ("D", "Direction", "Sort by projection along a direction", 1), + ("A", "Area", "Sort by surface area", 2), + ("NP", "Normal Angle Point", "Sort by normal angle to point", 3), + ("ND", "Normal Angle Direction", "Sort by normal angle to direction", 4)] + +directions = {"X": [1, 0, 0], "Y": [0, 1, 0], "Z": [0, 0, 1]} + +socket_names = {"P": "Point", "D": "Direction", "A": "Area", "NP": "Normal", "ND": "Normal"} +socket_props = {"P": "point_P", "D": "point_D", "A": "point_D", "NP": "point_P", "ND": "point_D"} + +quantity_names = {"P": "Distances", "D": "Distances", "A": "Areas", "NP": "Angles", "ND": "Angles"} + + +def polygon_normal(verts, poly): + ''' The normal of the given polygon ''' + v1 = Vector(verts[poly[0]]) + v2 = Vector(verts[poly[1]]) + v3 = Vector(verts[poly[2]]) + v12 = v2 - v1 + v23 = v3 - v2 + normal = v12.cross(v23) + normal.normalize() + + return list(normal) + + +def polygon_area(verts, poly): + ''' The area of the given polygon ''' + if len(poly) < 3: # not a plane - no area + return 0 + + total = Vector([0, 0, 0]) + N = len(poly) + for i in range(N): + vi1 = Vector(verts[poly[i]]) + vi2 = Vector(verts[poly[(i + 1) % N]]) + prod = vi1.cross(vi2) + total[0] += prod[0] + total[1] += prod[1] + total[2] += prod[2] + + normal = Vector(polygon_normal(verts, poly)) + area = abs(total.dot(normal)) / 2 + + return area + + +def polygon_center(verts, poly): + ''' The center of the given polygon ''' + vx = 0 + vy = 0 + vz = 0 + for v in poly: + vx = vx + verts[v][0] + vy = vy + verts[v][1] + vz = vz + verts[v][2] + n = len(poly) + vx = vx / n + vy = vy / n + vz = vz / n + + return [vx, vy, vz] + + +def polygon_distance_P(verts, poly, P): + ''' The distance from the center of the polygon to the given point ''' + C = polygon_center(verts, poly) + CP = [C[0] - P[0], C[1] - P[1], C[2] - P[2]] + distance = sqrt(CP[0] * CP[0] + CP[1] * CP[1] + CP[2] * CP[2]) + + return distance + + +def polygon_distance_D(verts, poly, D): + ''' The projection of the polygon center vector along the given direction ''' + C = polygon_center(verts, poly) + distance = C[0] * D[0] + C[1] * D[1] + C[2] * D[2] + + return distance + + +def polygon_normal_angle_P(verts, poly, P): + ''' The angle between the polygon normal and the vector from polygon center to given point ''' + N = polygon_normal(verts, poly) + C = polygon_center(verts, poly) + V = [P[0] - C[0], P[1] - C[1], P[2] - C[2]] + v1 = Vector(N) + v2 = Vector(V) + v1.normalize() + v2.normalize() + angle = acos(v1.dot(v2)) # the angle in radians + + return angle + + +def polygon_normal_angle_D(verts, poly, D): + ''' The angle between the polygon normal and the given direction ''' + N = polygon_normal(verts, poly) + v1 = Vector(N) + v2 = Vector(D) + v1.normalize() + v2.normalize() + angle = acos(v1.dot(v2)) # the angle in radians + + return angle + + +class SvPolygonSortNode(bpy.types.Node, SverchCustomTreeNode): + """ + Triggers: Polygon, Sorting + Tooltip: Sort the polygons by various criteria: distance, angle, area. + """ + bl_idname = 'SvPolygonSortNode' + bl_label = 'Polygon Sort' + + def sort_polygons(self, verts, polys, V): + ''' Sort polygons and return sorted polygons indices, poly & quantities ''' + + if self.mode == "D": + quantities = [polygon_distance_D(verts, poly, V) for poly in polys] + elif self.mode == "P": + quantities = [polygon_distance_P(verts, poly, V) for poly in polys] + elif self.mode == "A": + quantities = [polygon_area(verts, poly) for poly in polys] + elif self.mode == "NP": + quantities = [polygon_normal_angle_P(verts, poly, V) for poly in polys] + elif self.mode == "ND": + quantities = [polygon_normal_angle_D(verts, poly, V) for poly in polys] + + IQ = [(i, q) for i, q in enumerate(quantities)] + sortedIQs = sorted(IQ, key=lambda kv: kv[1], reverse=self.descending) + + sortedIndices = [IQ[0] for IQ in sortedIQs] + sortedQuantities = [IQ[1] for IQ in sortedIQs] + sortedPolys = [polys[i] for i in sortedIndices] + + return sortedIndices, sortedPolys, sortedQuantities + + def update_sockets(self, context): + ''' Swap sorting vector input socket to P/D based on selected mode ''' + + s = self.inputs[-1] + s.name = socket_names[self.mode] + s.prop_name = socket_props[self.mode] + + # keep the P/D props values synced when changing mode + if self.mode == "P": + self.point_P = self.point_D + else: # self.mode == "D" + self.point_D = self.point_P + + # update output "Quantities" socket with proper name for the mode + o = self.outputs[-1] + o.name = quantity_names[self.mode] + + def set_direction(self, operator): + self.direction = operator.direction + self.mode = "D" + return {'FINISHED'} + + def update_xyz_direction(self, context): + self.point_D = directions[self.direction] + + def update_mode(self, context): + if self.mode == self.last_mode: + return + + self.last_mode = self.mode + self.update_sockets(context) + updateNode(self, context) + + direction = StringProperty( + name="Direction", default="X", update=update_xyz_direction) + + mode = EnumProperty( + name="Mode", items=modeItems, default="D", update=update_mode) + + last_mode = EnumProperty( + name="Last Mode", items=modeItems, default="D") + + point_P = FloatVectorProperty( + name="Point P", description="Reference point for distance and angle calculation", + size=3, default=(1, 0, 0), update=updateNode) + + point_D = FloatVectorProperty( + name="Direction", description="Reference direction for projection and angle calculation", + size=3, default=(1, 0, 0), update=updateNode) + + descending = BoolProperty( + name="Descending", description="Sort in the descending order", + default=False, update=updateNode) + + def sv_init(self, context): + self.inputs.new('VerticesSocket', "Verts") + self.inputs.new('StringsSocket', "Polys") + self.inputs.new('VerticesSocket', "Direction").prop_name = "point_D" + self.outputs.new('VerticesSocket', "Vertices") + self.outputs.new('StringsSocket', "Polygons") + self.outputs.new('StringsSocket', "Indices") + self.outputs.new('StringsSocket', "Distances") + + def draw_buttons(self, context, layout): + col = layout.column(align=False) + + if not self.inputs[-1].is_linked: + row = col.row(align=True) + for direction in "XYZ": + op = row.operator("node.set_sort_direction", text=direction) + op.direction = direction + + col = layout.column(align=True) + row = col.row(align=True) + row.prop(self, "mode", expand=False, text="") + layout.prop(self, "descending") + + def process(self): + if not any(s.is_linked for s in self.outputs): + return + + inputs = self.inputs + input_v = inputs["Verts"].sv_get() + input_p = inputs["Polys"].sv_get() + input_r = inputs[-1].sv_get()[0] # reference: direction or point + + params = match_long_repeat([input_v, input_p, input_r]) + + iList, vList, pList, qList = [], [], [], [] + for v, p, r in zip(*params): + indices, polys, quantities = self.sort_polygons(v, p, r) + iList.append(indices) + vList.append(v) + pList.append(polys) + qList.append(quantities) + + if self.outputs['Vertices'].is_linked: + self.outputs['Vertices'].sv_set(vList) + if self.outputs['Polygons'].is_linked: + self.outputs['Polygons'].sv_set(pList) + if self.outputs['Indices'].is_linked: + self.outputs['Indices'].sv_set(iList) + if self.outputs[-1].is_linked: + self.outputs[-1].sv_set(qList) # sorting quantities + + +class SvSetSortDirection(bpy.types.Operator, SvGenericCallbackWithParams): + bl_label = "Set sort direction" + bl_idname = "node.set_sort_direction" + bl_description = "Set the sorting direction along X, Y or Z" + + direction = StringProperty(default="X") + fn_name = StringProperty(default="set_direction") + + +def register(): + bpy.utils.register_class(SvSetSortDirection) + bpy.utils.register_class(SvPolygonSortNode) + + +def unregister(): + bpy.utils.unregister_class(SvPolygonSortNode) + bpy.utils.unregister_class(SvSetSortDirection) diff --git a/nodes/number/easing.py b/nodes/number/easing.py index d1c292f925c13d3ae12b7627eac8da1c90bc39a0..5c2392da3456ee55e3013b0af149546a6501c2ca 100644 --- a/nodes/number/easing.py +++ b/nodes/number/easing.py @@ -23,6 +23,7 @@ from bpy.props import FloatProperty, EnumProperty, StringProperty, BoolProperty import blf import bgl +from sverchok.utils.context_managers import sv_preferences from sverchok.data_structure import updateNode, node_id from sverchok.node_tree import SverchCustomTreeNode from sverchok.ui import nodeview_bgl_viewer_draw_mk2 as nvBGL2 @@ -38,12 +39,12 @@ for k in sorted(easing_dict.keys()): palette_dict = { "default": ( - (0.243299, 0.590403, 0.836084, 1.00), # back_color + (0.243299, 0.590403, 0.836084, 1.00), # back_color (0.390805, 0.754022, 1.000000, 1.00), # grid_color (1.000000, 0.330010, 0.107140, 1.00) # line_color ), "scope": ( - (0.274677, 0.366253, 0.386430, 1.00), # back_color + (0.274677, 0.366253, 0.386430, 1.00), # back_color (0.423268, 0.558340, 0.584078, 1.00), # grid_color (0.304762, 1.000000, 0.062827, 1.00) # line_color ) @@ -54,40 +55,42 @@ palette_dict = { def simple_grid_xy(x, y, args): func = args[0] back_color, grid_color, line_color = args[1] + scale = args[2] def draw_rect(x=0, y=0, w=30, h=10, color=(0.0, 0.0, 0.0, 1.0)): - bgl.glColor4f(*color) + bgl.glColor4f(*color) bgl.glBegin(bgl.GL_POLYGON) - for coord in [(x, y), (x+w, y), (w+x, y-h), (x, y-h)]: + for coord in [(x, y), (x + w, y), (w + x, y - h), (x, y - h)]: bgl.glVertex2f(*coord) bgl.glEnd() + size = 140 * scale # draw bg fill - draw_rect(x=x, y=y, w=140, h=140, color=back_color) + draw_rect(x=x, y=y, w=size, h=size, color=back_color) # draw grid bgl.glColor4f(*grid_color) num_divs = 8 - offset = 140/num_divs + offset = size / num_divs line_parts_x = [] line_parts_y = [] - for i in range(num_divs+1): - xpos1 = x + (i*offset) + for i in range(num_divs + 1): + xpos1 = x + (i * offset) ypos1 = y - ypos2 = y - 140 + ypos2 = y - size line_parts_x.extend([[xpos1, ypos1], [xpos1, ypos2]]) - ypos = y - (i*offset) - line_parts_y.extend([[x, ypos], [x+140, ypos]]) + ypos = y - (i * offset) + line_parts_y.extend([[x, ypos], [x + size, ypos]]) bgl.glLineWidth(0.8) bgl.glBegin(bgl.GL_LINES) for coord in line_parts_x + line_parts_y: bgl.glVertex2f(*coord) - bgl.glEnd() + bgl.glEnd() # draw graph-line bgl.glColor4f(*line_color) @@ -95,13 +98,11 @@ def simple_grid_xy(x, y, args): bgl.glBegin(bgl.GL_LINE_STRIP) num_points = 100 seg_diff = 1 / num_points - for i in range(num_points+1): - _px = x + ((i * seg_diff) * 140) - _py = y - (1 - func(i * seg_diff) * 140) - 140 + for i in range(num_points + 1): + _px = x + ((i * seg_diff) * size) + _py = y - (1 - func(i * seg_diff) * size) - size bgl.glVertex2f(_px, _py) - bgl.glEnd() - - + bgl.glEnd() class SvEasingNode(bpy.types.Node, SverchCustomTreeNode): @@ -169,18 +170,29 @@ class SvEasingNode(bpy.types.Node, SverchCustomTreeNode): palette = palette_dict.get(self.selected_theme_mode)[:] x, y = [int(j) for j in (self.location + Vector((self.width + 20, 0)))[:]] - + + # adjust render location based on preference multiplier setting + try: + with sv_preferences() as prefs: + multiplier = prefs.render_location_xy_multiplier + scale = prefs.render_scale + except: + # print('did not find preferences - you need to save user preferences') + multiplier = 1.0 + scale = 1.0 + x, y = [x * multiplier, y * multiplier] + draw_data = { 'tree_name': self.id_data.name[:], - 'mode': 'custom_function', + 'mode': 'custom_function', 'custom_function': simple_grid_xy, 'loc': (x, y), - 'args': (easing_func, palette) + 'args': (easing_func, palette, scale) } nvBGL2.callback_enable(n_id, draw_data) def free(self): - nvBGL2.callback_disable(node_id(self)) + nvBGL2.callback_disable(node_id(self)) # reset n_id on copy def copy(self, node): diff --git a/nodes/number/mix_inputs.py b/nodes/number/mix_inputs.py new file mode 100644 index 0000000000000000000000000000000000000000..a973af3fb4863dc54b410a3e559c45de594196dd --- /dev/null +++ b/nodes/number/mix_inputs.py @@ -0,0 +1,375 @@ +# ##### BEGIN GPL LICENSE BLOCK ##### +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# ##### END GPL LICENSE BLOCK ##### + + +import bpy +from bpy.props import IntProperty, FloatProperty, BoolProperty, EnumProperty, FloatVectorProperty +from sverchok.node_tree import SverchCustomTreeNode +from sverchok.data_structure import updateNode, match_long_repeat +from sverchok.data_structure import Matrix_generate +from sverchok.utils.sv_easing_functions import * +from mathutils import Matrix, Quaternion +import re + +idMat = [[tuple(v) for v in Matrix()]] # identity matrix + +input_info = { + 'INT': { + "Index": 0, + "SocketType": "StringsSocket", + "PropType": ('IntProperty', dict()), + "PropDefault": {"A": 0, "B": 1}, + "Mixer": lambda t, v1, v2: (int(v1 * (1 - t) + v2 * t)), + "Inputs": lambda ia, ib: [ia.sv_get()[0], ib.sv_get()[0]], + "Outputs": lambda o, v: o.sv_set([v]) + }, + + 'FLOAT': { + "Index": 1, + "SocketType": "StringsSocket", + "PropType": ('FloatProperty', dict()), + "PropDefault": {"A": 0.0, "B": 1.0}, + "Mixer": lambda t, v1, v2: v1 * (1 - t) + v2 * t, + "Inputs": lambda ia, ib: [ia.sv_get()[0], ib.sv_get()[0]], + "Outputs": lambda o, v: o.sv_set([v]) + }, + + 'VECTOR': { + "Index": 2, + "SocketType": "VerticesSocket", + "PropType": ('FloatVectorProperty', dict(size=3, subtype='XYZ')), + "PropDefault": {"A": (0.0, 0.0, 0.0), "B": (1.0, 1.0, 1.0)}, + "Mixer": lambda t, v1, v2: tuple([v1[i] * (1 - t) + v2[i] * t for i in range(3)]), + "Inputs": lambda ia, ib: [ia.sv_get()[0], ib.sv_get()[0]], + "Outputs": lambda o, v: o.sv_set([v]) + }, + + 'COLOR': { + "Index": 3, + "SocketType": "SvColorSocket", + "PropType": ('FloatVectorProperty', dict(size=4, subtype='COLOR', min=0, max=1)), + "PropDefault": {"A": (0.0, 0.0, 0.0, 1.0), "B": (1.0, 1.0, 1.0, 1.0)}, + "Mixer": lambda t, v1, v2: tuple([v1[i] * (1 - t) + v2[i] * t for i in range(4)]), + "Inputs": lambda ia, ib: [ia.sv_get()[0], ib.sv_get()[0]], + "Outputs": lambda o, v: o.sv_set([v]) + }, + + 'QUATERNION': { + "Index": 4, + "SocketType": "SvQuaternionSocket", + "PropType": ('FloatVectorProperty', dict(size=4, subtype='QUATERNION')), + "PropDefault": {"A": (0.0, 0.0, 0.0, 0.0), "B": (1.0, 1.0, 1.0, 1.0)}, + "Mixer": lambda t, v1, v2: quaternionMix(t, v1, v2), + "Inputs": lambda ia, ib: [ia.sv_get()[0], ib.sv_get()[0]], + "Outputs": lambda o, v: o.sv_set([v]) + }, + + 'MATRIX': { + "Index": 5, + "SocketType": "MatrixSocket", + "Mixer": lambda t, v1, v2: matrixMix(t, v1, v2), + "Inputs": lambda ia, ib: [ia.sv_get(default=idMat), ib.sv_get(default=idMat)], + "Outputs": lambda o, v: o.sv_set(v) + } +} + +interplationItems = [ + ("LINEAR", "Linear", "", "IPO_LINEAR", 0), + ("SINUSOIDAL", "Sinusoidal", "", "IPO_SINE", 1), + ("QUADRATIC", "Quadratic", "", "IPO_QUAD", 2), + ("CUBIC", "Cubic", "", "IPO_CUBIC", 3), + ("QUARTIC", "Quartic", "", "IPO_QUART", 4), + ("QUINTIC", "Quintic", "", "IPO_QUINT", 5), + ("EXPONENTIAL", "Exponential", "", "IPO_EXPO", 6), + ("CIRCULAR", "Circular", "", "IPO_CIRC", 7), + # DYNAMIC effects + ("BACK", "Back", "", "IPO_BACK", 8), + ("BOUNCE", "Bounce", "", "IPO_BOUNCE", 9), + ("ELASTIC", "Elastic", "", "IPO_ELASTIC", 10)] + +easingItems = [ + ("EASE_IN", "Ease In", "", "IPO_EASE_IN", 0), + ("EASE_OUT", "Ease Out", "", "IPO_EASE_OUT", 1), + ("EASE_IN_OUT", "Ease In-Out", "", "IPO_EASE_IN_OUT", 2)] + + +def matrixMix(t, v1, v2): + m1 = Matrix_generate([v1])[0] + m2 = Matrix_generate([v2])[0] + m = m1.lerp(m2, t) + return m + + +def quaternionMix(t, v1, v2): + q1 = Quaternion(v1) + q2 = Quaternion(v2) + q = q1.slerp(q2, max(0, min(t, 1))) + return q + + +def make_prop(mode, label): + ''' Property Factory ''' + description = "Mix " + mode + " value " + label + default = input_info[mode]["PropDefault"][label] + # general parameters + params = dict(name=label, description=description, default=default, update=updateNode) + # add type specific parameters + params.update(input_info[mode]["PropType"][1]) + propType = input_info[mode]["PropType"][0] + return getattr(bpy.props, propType)(**params) + + +class SvMixInputsNode(bpy.types.Node, SverchCustomTreeNode): + """ + Triggers: Interpolate, Slerp + Tooltip: Interpolate between values of various types + """ + bl_idname = 'SvMixInputsNode' + bl_label = 'Mix Inputs' + sv_icon = 'SV_MIX_INPUTS' + + # SV easing based interpolator + def get_interpolator(self): + # get the interpolator function based on selected interpolation and easing + if self.interpolation == "LINEAR": + return LinearInterpolation + else: + ''' This maps the Strings used in the Enumerator properties to the associated function''' + interpolatorName = self.interpolation + "_" + self.easing + interpolatorName = re.sub('SINUSOIDAL', 'sine', interpolatorName) # for the exception + interpolate = globals()[re.sub(r'[_]', '', interpolatorName.lower().title())] + + # setup the interpolator with prepared parameters + if self.interpolation == "EXPONENTIAL": + b = self.exponentialBase + e = self.exponentialExponent + settings = prepareExponentialSettings(b, e) + return lambda v: interpolate(v, settings) + + elif self.interpolation == "BACK": + s = self.backScale + return lambda v: interpolate(v, s) + + elif self.interpolation == "ELASTIC": + n = self.elasticBounces + b = self.elasticBase + e = self.elasticExponent + settings = prepareElasticSettings(n, b, e) + return lambda v: interpolate(v, settings) + + elif self.interpolation == "BOUNCE": + n = self.bounceBounces + a = self.bounceAttenuation + settings = prepareBounceSettings(n, a) + return lambda v: interpolate(v, settings) + + else: + return interpolate + + def update_mode(self, context): + self.update_sockets() + updateNode(self, context) + + typeDict = {input_info[t]["Index"]: t for t in input_info.keys()} + typeItems = [(v, v.title(), "", "", k) for (k, v) in sorted(typeDict.items())] + + mode = EnumProperty( + name="Mode", description="The type of the values to mix", + default="FLOAT", items=typeItems, + update=update_mode) + + # INTERPOLATION settings + interpolation = EnumProperty( + name="Interpolation", description="Interpolation type", + default="LINEAR", items=interplationItems, + update=updateNode) + + easing = EnumProperty( + name="Easing", description="Easing type", + default="EASE_IN_OUT", items=easingItems, + update=updateNode) + + # BACK interpolation settings + backScale = FloatProperty( + name="Scale", description="Back scale", + default=0.5, soft_min=0.0, soft_max=10.0, + update=updateNode) + + # ELASTIC interpolation settings + elasticBase = FloatProperty( + name="Base", description="Elastic base", + default=1.6, soft_min=0.0, soft_max=10.0, + update=updateNode) + + elasticExponent = FloatProperty( + name="Exponent", description="Elastic exponent", + default=6.0, soft_min=0.0, soft_max=10.0, + update=updateNode) + + elasticBounces = IntProperty( + name="Bounces", description="Elastic bounces", + default=6, soft_min=1, soft_max=10, + update=updateNode) + + # EXPONENTIAL interpolation settings + exponentialBase = FloatProperty( + name="Base", description="Exponential base", + default=2.0, soft_min=0.0, soft_max=10.0, + update=updateNode) + + exponentialExponent = FloatProperty( + name="Exponent", description="Exponential exponent", + default=10.0, soft_min=0.0, soft_max=20.0, + update=updateNode) + + # BOUNCE interpolation settings + bounceAttenuation = FloatProperty( + name="Attenuation", description="Bounce attenuation", + default=0.5, soft_min=0.1, soft_max=0.9, + update=updateNode) + + bounceBounces = IntProperty( + name="Bounces", description="Bounce bounces", + default=4, soft_min=1, soft_max=10, + update=updateNode) + + # INPUT sockets props + factor = FloatProperty( + name="Factor", description="Factor value", + default=0.5, min=0.0, max=1.0, + update=updateNode) + + for m in input_info.keys(): # create props for input sockets A/B + if m != "MATRIX": + for l in ['A', 'B']: + attr_name = m.lower() + "_" + l.lower() + vars()[attr_name] = make_prop(m, l) + + mirror = BoolProperty( + name="Mirror", description="Mirror the interplation factor", + default=False, update=updateNode) + + swap = BoolProperty( + name="Swap", description="Swap the two inputs", + default=False, update=updateNode) + + def sv_init(self, context): + self.width = 180 + self.inputs.new('StringsSocket', "f").prop_name = 'factor' + self.inputs.new('StringsSocket', "A").prop_name = 'float_a' + self.inputs.new('StringsSocket', "B").prop_name = 'float_b' + self.outputs.new('StringsSocket', "Float") + self.update_sockets() + + def draw_buttons(self, context, layout): + layout.prop(self, 'mode', text="", expand=False) + row = layout.row(align=True) + row.prop(self, 'interpolation', text="", expand=False) + row.prop(self, 'easing', text="", expand=True) + row = layout.row(align=True) + row.prop(self, 'mirror', toggle=True) + row.prop(self, 'swap', toggle=True) + + def draw_label(self): + if self.mode == "MATRIX": + return "Mix Matrices" + else: + return "Mix " + self.mode.title() + "s" + + def draw_buttons_ext(self, context, layout): + if self.interpolation == "BACK": + layout.column().label(text="Interpolation:") + box = layout.box() + box.prop(self, 'backScale') + + elif self.interpolation == "ELASTIC": + layout.column().label(text="Interpolation:") + box = layout.box() + box.prop(self, 'elasticBase') + box.prop(self, 'elasticExponent') + box.prop(self, 'elasticBounces') + + elif self.interpolation == "EXPONENTIAL": + layout.column().label(text="Interpolation:") + box = layout.box() + box.prop(self, 'exponentialBase') + box.prop(self, 'exponentialExponent') + + elif self.interpolation == "BOUNCE": + layout.column().label(text="Interpolation:") + box = layout.box() + box.prop(self, 'bounceAttenuation') + box.prop(self, 'bounceBounces') + + def get_mixer(self): + return input_info[self.mode]['Mixer'] + + def get_inputs(self): + input_getter = input_info[self.mode]["Inputs"] + f = self.inputs["f"].sv_get()[0] + i, j = [2, 1] if self.swap else [1, 2] # swap inputs ? + [a, b] = input_getter(self.inputs[i], self.inputs[j]) + return [f, a, b] + + def set_ouputs(self, values): + output_setter = input_info[self.mode]["Outputs"] + output_setter(self.outputs[0], values) + + def update_sockets(self): + # replace input and output sockets, linking input socket to corresponding props + new_socket_type = input_info[self.mode]["SocketType"] + for socket in self.inputs[1:]: + if self.mode != "MATRIX": + prop_name = self.mode.lower() + "_" + socket.name.lower() + socket.replace_socket(new_socket_type).prop_name = prop_name + else: + socket.replace_socket(new_socket_type) + + self.outputs[0].replace_socket(new_socket_type, self.mode.title()) + + def process(self): + # return if no outputs are connected + if not any(s.is_linked for s in self.outputs): + return + + # input values lists (single or multi value) + input_factor, input_value1, input_value2 = self.get_inputs() + + parameters = match_long_repeat([input_factor, input_value1, input_value2]) + + interpolate = self.get_interpolator() + + mix = self.get_mixer() + + values = [] + for f, v1, v2 in zip(*parameters): + if self.mirror: + f = 1 - 2 * abs(f - 0.5) + t = interpolate(f) + v = mix(t, v1, v2) + values.append(v) + + self.set_ouputs(values) + + +def register(): + bpy.utils.register_class(SvMixInputsNode) + + +def unregister(): + bpy.utils.unregister_class(SvMixInputsNode) diff --git a/nodes/quaternion/quaternion_in.py b/nodes/quaternion/quaternion_in.py new file mode 100644 index 0000000000000000000000000000000000000000..e28a22c74a64f659b727bfb4b408273ae5a03115 --- /dev/null +++ b/nodes/quaternion/quaternion_in.py @@ -0,0 +1,241 @@ +# ##### BEGIN GPL LICENSE BLOCK ##### +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# ##### END GPL LICENSE BLOCK ##### + +import bpy +from bpy.props import EnumProperty, FloatProperty, BoolProperty, StringProperty, FloatVectorProperty +from sverchok.node_tree import SverchCustomTreeNode +from sverchok.data_structure import updateNode, match_long_repeat +from mathutils import Quaternion, Matrix, Euler +from math import pi + + +modeItems = [ + ("WXYZ", "WXYZ", "Convert components into quaternion", 0), + ("SCALARVECTOR", "Scalar Vector", "Convert Scalar & Vector into quaternion", 1), + ("EULER", "Euler Angles", "Convert Euler angles into quaternion", 2), + ("AXISANGLE", "Axis Angle", "Convert Axis & Angle into quaternion", 3), + ("MATRIX", "Matrix", "Convert Rotation Matrix into quaternion", 4), +] + +eulerOrderItems = [ + ('XYZ', "XYZ", "", 0), + ('XZY', 'XZY', "", 1), + ('YXZ', 'YXZ', "", 2), + ('YZX', 'YZX', "", 3), + ('ZXY', 'ZXY', "", 4), + ('ZYX', 'ZYX', "", 5) +] + +angleUnitItems = [ + ("RAD", "Rad", "Radians", "", 0), + ("DEG", "Deg", 'Degrees', "", 1), + ("UNI", "Uni", 'Unities', "", 2) +] + +angleConversion = {"RAD": 1.0, "DEG": pi / 180.0, "UNI": 2 * pi} + +idMat = [[tuple(v) for v in Matrix()]] # identity matrix + +input_sockets = { + "WXYZ": ["W", "X", "Y", "Z"], + "SCALARVECTOR": ["Scalar", "Vector"], + "EULER": ["Angle X", "Angle Y", "Angle Z"], + "AXISANGLE": ["Angle", "Axis"], + "MATRIX": ["Matrix"] +} + + +class SvQuaternionInNode(bpy.types.Node, SverchCustomTreeNode): + """ + Triggers: Quaternions, In + Tooltip: Generate quaternions from various quaternion components + """ + bl_idname = 'SvQuaternionInNode' + bl_label = 'Quaternion In' + sv_icon = 'SV_COMBINE_IN' + + def update_mode(self, context): + + # hide all input sockets + for k, names in input_sockets.items(): + for name in names: + self.inputs[name].hide_safe = True + + # show mode specific input sockets + for name in input_sockets[self.mode]: + self.inputs[name].hide_safe = False + + updateNode(self, context) + + mode = EnumProperty( + name='Mode', description='The input component format of the quaternion', + items=modeItems, default="WXYZ", update=update_mode) + + eulerOrder = EnumProperty( + name="Euler Order", description="Order of the Euler rotations", + default="XYZ", items=eulerOrderItems, update=updateNode) + + angleUnits = EnumProperty( + name="Angle Units", description="Angle units (radians/degrees/unities)", + default="RAD", items=angleUnitItems, update=updateNode) + + component_w = FloatProperty( + name='W', description='W component', + default=0.0, precision=3, update=updateNode) + + component_x = FloatProperty( + name='X', description='X component', + default=0.0, precision=3, update=updateNode) + + component_y = FloatProperty( + name='Y', description='Y component', + default=0.0, precision=3, update=updateNode) + + component_z = FloatProperty( + name='Z', description='Z component', + default=0.0, precision=3, update=updateNode) + + scalar = FloatProperty( + name='Scalar', description='Scalar component of the quaternion', + default=0.0, update=updateNode) + + vector = FloatVectorProperty( + name='Vector', description='Vector component of the quaternion', + size=3, default=(0.0, 0.0, 0.0), subtype="XYZ", update=updateNode) + + angle_x = FloatProperty( + name='Angle X', description='Rotation angle about X axis', + default=0.0, precision=3, update=updateNode) + + angle_y = FloatProperty( + name='Angle Y', description='Rotation angle about Y axis', + default=0.0, precision=3, update=updateNode) + + angle_z = FloatProperty( + name='Angle Z', description='Rotation angle about Z axis', + default=0.0, precision=3, update=updateNode) + + angle = FloatProperty( + name='Angle', description='Rotation angle about the given axis', + default=0.0, update=updateNode) + + axis = FloatVectorProperty( + name='Axis', description='Axis of rotation', + size=3, default=(1.0, 0.0, 0.0), subtype="XYZ", update=updateNode) + + normalize = BoolProperty( + name='Normalize', description='Normalize the output quaternion', + default=False, update=updateNode) + + def sv_init(self, context): + # component inputs + self.inputs.new('StringsSocket', "W").prop_name = 'component_w' + self.inputs.new('StringsSocket', "X").prop_name = 'component_x' + self.inputs.new('StringsSocket', "Y").prop_name = 'component_y' + self.inputs.new('StringsSocket', "Z").prop_name = 'component_z' + # scalar-vector inputs + self.inputs.new('StringsSocket', "Scalar").prop_name = 'scalar' + self.inputs.new('VerticesSocket', "Vector").prop_name = "vector" + # euler angles inputs + self.inputs.new('StringsSocket', "Angle X").prop_name = 'angle_x' + self.inputs.new('StringsSocket', "Angle Y").prop_name = 'angle_y' + self.inputs.new('StringsSocket', "Angle Z").prop_name = 'angle_z' + # axis-angle inputs + self.inputs.new('VerticesSocket', "Axis").prop_name = "axis" + self.inputs.new('StringsSocket', "Angle").prop_name = 'angle' + # matrix input + self.inputs.new('MatrixSocket', "Matrix") + self.outputs.new('SvQuaternionSocket', "Quaternions") + + self.update_mode(context) + + def draw_buttons(self, context, layout): + layout.prop(self, "mode", expand=False, text="") + if self.mode == "EULER": + col = layout.column(align=True) + col.prop(self, "eulerOrder", text="") + if self.mode in {"EULER", "AXISANGLE"}: + row = layout.row(align=True) + row.prop(self, "angleUnits", expand=True) + if self.mode in {"WXYZ", "SCALARVECTOR"}: + layout.prop(self, "normalize", toggle=True) + + def process(self): + if not self.outputs['Quaternions'].is_linked: + return + + inputs = self.inputs + + quaternionList = [] + + if self.mode == "WXYZ": + I = [inputs[n].sv_get()[0] for n in "WXYZ"] + params = match_long_repeat(I) + for wxyz in zip(*params): + q = Quaternion(wxyz) + if self.normalize: + q.normalize() + quaternionList.append(q) + + elif self.mode == "SCALARVECTOR": + I = [inputs[n].sv_get()[0] for n in ["Scalar", "Vector"]] + params = match_long_repeat(I) + for scalar, vector in zip(*params): + q = Quaternion([scalar, *vector]) + if self.normalize: + q.normalize() + quaternionList.append(q) + + elif self.mode == "EULER": + I = [inputs["Angle " + n].sv_get()[0] for n in "XYZ"] + params = match_long_repeat(I) + au = angleConversion[self.angleUnits] + for angleX, angleY, angleZ in zip(*params): + euler = Euler((angleX * au, angleY * au, angleZ * au), self.eulerOrder) + q = euler.to_quaternion() + if self.normalize: + q.normalize() + quaternionList.append(q) + + elif self.mode == "AXISANGLE": + I = [inputs[n].sv_get()[0] for n in ["Axis", "Angle"]] + params = match_long_repeat(I) + au = angleConversion[self.angleUnits] + for axis, angle in zip(*params): + q = Quaternion(axis, angle * au) + if self.normalize: + q.normalize() + quaternionList.append(q) + + elif self.mode == "MATRIX": + input_M = inputs["Matrix"].sv_get(default=idMat) + for m in input_M: + q = Matrix(m).to_quaternion() + if self.normalize: + q.normalize() + quaternionList.append(q) + + self.outputs['Quaternions'].sv_set([quaternionList]) + + +def register(): + bpy.utils.register_class(SvQuaternionInNode) + + +def unregister(): + bpy.utils.unregister_class(SvQuaternionInNode) diff --git a/nodes/quaternion/quaternion_math.py b/nodes/quaternion/quaternion_math.py new file mode 100644 index 0000000000000000000000000000000000000000..fbcfdfc6eff300a15f60123e933ac7461585a059 --- /dev/null +++ b/nodes/quaternion/quaternion_math.py @@ -0,0 +1,284 @@ +# ##### BEGIN GPL LICENSE BLOCK ##### +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# ##### END GPL LICENSE BLOCK ##### + +import bpy +from bpy.props import (IntProperty, + FloatProperty, + BoolProperty, + BoolVectorProperty, + EnumProperty) + +from sverchok.node_tree import SverchCustomTreeNode +from sverchok.data_structure import updateNode, match_long_repeat + +from mathutils import Matrix, Quaternion +from functools import reduce + +# list of operations [name: id, input, output, description ] +operations = { + # multiple quaternions => quaternion (NQ or QQ => Q) + "ADD": (10, "NQ", "Q", "Add multiple quaternions"), + "SUB": (11, "QQ", "Q", "Subtract two quaternions"), + "MULTIPLY": (12, "NQ", "Q", "Multiply multiple quaternions"), + "DIVIDE": (13, "QQ", "Q", "Divide two quaternions"), + "ROTATE": (14, "QQ", "Q", "Rotate a quaternion around another"), + # two quaternions => scalar value (QQ => Q) + "DOT": (20, "QQ", "S", "Dot product two quaternions"), + "DISTANCE": (21, "QQ", "S", "Distance between two quaternions"), + # one quaternion => quaternion (Q => Q) + "NEGATE": (30, "Q", "Q", "Negate a quaternion"), + "CONJUGATE": (31, "Q", "Q", "Conjugate a quaternion"), + "INVERT": (32, "Q", "Q", "Invert a quaternion"), + "NORMALIZE": (33, "Q", "Q", "Normalize a quaternion"), + # one quaternion + scalar => quaternion (QS => Q) + "SCALE": (40, "QS", "Q", "Scale a quaternion by given factor"), + # one quaternion => scalar value (Q => S) + "QUADRANCE": (50, "Q", "S", "Quadrance of a quaternion"), + "MAGNITUDE": (51, "Q", "S", "Magnitude of a quaternion"), +} + +operationItems = [(k, k.title(), s[3], "", s[0]) for k, s in sorted(operations.items(), key=lambda k: k[1][0])] + +# cache various operation categories +NQ_operations = [n for n in operations if operations[n][1] == "NQ"] +QQ_operations = [n for n in operations if operations[n][1] == "QQ"] +Q_operations = [n for n in operations if operations[n][1] in {"Q", "QS"}] +QS_operations = [n for n in operations if operations[n][1] == "QS"] +output_S_operations = [n for n in operations if operations[n][2] == "S"] +prepost_operations = {"SUB", "MULTIPLY", "DIVIDE", "ROTATE"} + +prePostItems = [ + ("PRE", "Pre", "Calculate A op B", 0), + ("POST", "Post", "Calculate B op A", 1) +] + +id_quat = [Quaternion([1, 0, 0, 0])] +ABC = tuple('ABCDEFGHIJKLMNOPQRSTUVWXYZ') + + +class SvQuaternionMathNode(bpy.types.Node, SverchCustomTreeNode): + """ + Triggers: Quaternions, Math + Tooltip: Compute various arithmetic operations on quaternions + """ + bl_idname = 'SvQuaternionMathNode' + bl_label = 'Quaternion Math' + bl_icon = 'OUTLINER_OB_EMPTY' + + def update_operation(self, context): + self.label = "Quaternion " + self.operation.title() + self.update_sockets() + updateNode(self, context) + + prePost = EnumProperty( + name='Pre Post', + description='Order of operations PRE = A op B vs POST = B op A)', + items=prePostItems, default="PRE", update=updateNode) + + operation = EnumProperty( + name="Operation", + description="Operation to apply on the given quaternions", + items=operationItems, default="MULTIPLY", update=update_operation) + + scale = FloatProperty( + name="Scale", + description="Scale quaternion components by this factor", + default=1.0, update=updateNode) + + scales = BoolVectorProperty( + name="Scales", description="Which individual components to scale", + size=4, subtype="QUATERNION", + default=(True, True, True, True), update=updateNode) + + def sv_init(self, context): + self.inputs.new('StringsSocket', "Scale").prop_name = "scale" + self.inputs.new('SvQuaternionSocket', "A") + self.inputs.new('SvQuaternionSocket', "B") + + self.outputs.new('SvQuaternionSocket', "Quaternion") + self.outputs.new('StringsSocket', "Value") + + self.update_operation(context) + + def update(self): + ''' Add/remove sockets as A-Z sockets are connected/disconnected ''' + + # not a multiple quaternion operation ? => no need to update sockets + if self.operation not in NQ_operations: + return + + inputs = self.inputs + + # get all existing A-Z sockets (connected or not) + inputs_AZ = list(filter(lambda s: s.name in ABC, inputs)) + + # last A-Z socket connected ? => add an empty A-Z socket at the end + if inputs_AZ and inputs_AZ[-1].links: + name = ABC[len(inputs_AZ)] # pick the next letter A to Z + inputs.new("SvQuaternionSocket", name) + else: # last input disconnected ? => remove all but last unconnected + while len(inputs_AZ) > 2 and not inputs_AZ[-2].links: + s = inputs_AZ[-1] + inputs.remove(s) + inputs_AZ.remove(s) + + def update_sockets(self): + ''' Upate sockets based on selected operation ''' + inputs = self.inputs + + if self.operation in Q_operations: # Q or Q+S operations + for a in ABC[1:]: # remove all B-Z inputs (keep A) + if a in inputs: + inputs.remove(inputs[a]) + elif self.operation in QQ_operations: # Q + Q operations + for a in ABC[2:]: # remove all C-Z inputs (keep A & B) + if a in inputs: + inputs.remove(inputs[a]) + if not "B" in inputs: + inputs.new("SvQuaternionSocket", "B") + else: # multiple Q operations + if not "B" in inputs: + inputs.new("SvQuaternionSocket", "B") + + inputs["Scale"].hide_safe = self.operation != "SCALE" + + outputs = self.outputs + if self.operation in output_S_operations: + outputs["Quaternion"].hide_safe = True + if outputs["Value"].hide: + outputs["Value"].hide_safe = False + else: + if outputs["Quaternion"].hide: + outputs["Quaternion"].hide_safe = False + outputs["Value"].hide_safe = True + + self.update() + + def draw_buttons(self, context, layout): + layout.prop(self, "operation", text="") + if self.operation in prepost_operations: + layout.prop(self, "prePost", expand=True) + if self.operation == "SCALE": + row = layout.row(align=True) + row.prop(self, "scales", text="", toggle=True) + + def operation_negate(self, q): + qn = Quaternion(q) + qn.negate() + return qn + + def operation_rotate(self, q, p): + qr = Quaternion(q) + qr.rotate(p) + return qr + + def get_operation(self): + if self.operation == "ADD": + return lambda l: reduce((lambda q, p: q + p), l) + elif self.operation == "SUB": + return lambda q, p: q - p + elif self.operation == "MULTIPLY": + return lambda l: reduce((lambda q, p: q * p), l) + elif self.operation == "DIVIDE": + return lambda q, p: q * p.inverted() + elif self.operation == "ROTATE": + return self.operation_rotate + elif self.operation == "DOT": + return lambda q, p: q.dot(p) + elif self.operation == "DISTANCE": + return lambda q, p: (p - q).magnitude + elif self.operation == "NEGATE": + return self.operation_negate + elif self.operation == "CONJUGATE": + return lambda q: q.conjugated() + elif self.operation == "INVERT": + return lambda q: q.inverted() + elif self.operation == "NORMALIZE": + return lambda q: q.normalized() + elif self.operation == "SCALE": + return lambda q, s: Quaternion([q[i] * s[i] for i in range(4)]) + elif self.operation == "QUADRANCE": + return lambda q: q.dot(q) + elif self.operation == "MAGNITUDE": + return lambda q: q.magnitude + + def process(self): + outputs = self.outputs + if not any(s.is_linked for s in outputs): + return + + inputs = self.inputs + + all_AZ_sockets = list(filter(lambda s: s.name in ABC, inputs)) + connected_AZ_sockets = list(filter(lambda s: s.is_linked, all_AZ_sockets)) + + if len(connected_AZ_sockets) == 0: + return + + # collect the quaternion inputs from all connected AZ sockets + I = [s.sv_get(default=id_quat)[0] for s in connected_AZ_sockets] + + if self.operation in prepost_operations: + if self.prePost == "POST": # A op B : keep input order + I = I[::-1] + + other_sockets = list(filter(lambda s: s.name not in ABC and not s.hide, inputs)) + + # collect the remaning visible inputs + for socket in other_sockets: + values = socket.sv_get()[0] + if socket.name == "Scale": + qs = [] + for s in values: + swxyz = [s if self.scales[i] else 1.0 for i in range(4)] + qs.append(Quaternion(swxyz)) + values = qs + I.append(values) + + operation = self.get_operation() + + if self.operation in NQ_operations: + parameters = match_long_repeat(I) + quaternionList = [operation(params) for params in zip(*parameters)] + + elif self.operation in QQ_operations: + parameters = match_long_repeat(I) + quaternionList = [operation(*params) for params in zip(*parameters)] + + elif self.operation == "SCALE": + parameters = match_long_repeat(I) + quaternionList = [operation(*params) for params in zip(*parameters)] + + else: # single input operations + parameters = I[0] # just quaternion values + quaternionList = [operation(a) for a in parameters] + + if self.operation in output_S_operations: + if outputs['Value'].is_linked: + outputs['Value'].sv_set([quaternionList]) + else: # output quaternions + if outputs['Quaternion'].is_linked: + outputs['Quaternion'].sv_set([quaternionList]) + + +def register(): + bpy.utils.register_class(SvQuaternionMathNode) + + +def unregister(): + bpy.utils.unregister_class(SvQuaternionMathNode) diff --git a/nodes/quaternion/quaternion_out.py b/nodes/quaternion/quaternion_out.py new file mode 100644 index 0000000000000000000000000000000000000000..852835d4ec71fc2f5d5cfaea37002cc5468e8b2e --- /dev/null +++ b/nodes/quaternion/quaternion_out.py @@ -0,0 +1,193 @@ +# ##### BEGIN GPL LICENSE BLOCK ##### +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# ##### END GPL LICENSE BLOCK ##### + +import bpy +from bpy.props import BoolProperty, FloatVectorProperty, EnumProperty +from sverchok.node_tree import SverchCustomTreeNode +from sverchok.data_structure import updateNode +from mathutils import Quaternion +from math import pi + + +modeItems = [ + ("WXYZ", "WXYZ", "Convert quaternion into components", 0), + ("SCALARVECTOR", "Scalar Vector", "Convert quaternion into Scalar & Vector", 1), + ("EULER", "Euler Angles", "Convert quaternion into Euler angles", 2), + ("AXISANGLE", "Axis Angle", "Convert quaternion into Axis & Angle", 3), + ("MATRIX", "Matrix", "Convert quaternion into Rotation Matrix", 4), +] + +eulerOrderItems = [ + ('XYZ', "XYZ", "", 0), + ('XZY', 'XZY', "", 1), + ('YXZ', 'YXZ', "", 2), + ('YZX', 'YZX', "", 3), + ('ZXY', 'ZXY', "", 4), + ('ZYX', 'ZYX', "", 5) +] + +angleUnitItems = [ + ("RAD", "Rad", "Radians", "", 0), + ("DEG", "Deg", 'Degrees', "", 1), + ("UNI", "Uni", 'Unities', "", 2) +] + +angleConversion = {"RAD": 1.0, "DEG": 180.0 / pi, "UNI": 0.5 / pi} + +output_sockets = { + "WXYZ": ["W", "X", "Y", "Z"], + "SCALARVECTOR": ["Scalar", "Vector"], + "EULER": ["Angle X", "Angle Y", "Angle Z"], + "AXISANGLE": ["Angle", "Axis"], + "MATRIX": ["Matrix"] +} + + +class SvQuaternionOutNode(bpy.types.Node, SverchCustomTreeNode): + """ + Triggers: Quaternions, Out + Tooltip: Convert quaternions into various quaternion components + """ + bl_idname = 'SvQuaternionOutNode' + bl_label = 'Quaternion Out' + sv_icon = 'SV_COMBINE_OUT' + + def update_mode(self, context): + + # hide all output sockets + for k, names in output_sockets.items(): + for name in names: + self.outputs[name].hide_safe = True + + # show mode specific output sockets + for name in output_sockets[self.mode]: + self.outputs[name].hide_safe = False + + updateNode(self, context) + + mode = EnumProperty( + name='Mode', description='The output component format of the quaternion', + items=modeItems, default="WXYZ", update=update_mode) + + eulerOrder = EnumProperty( + name="Euler Order", description="Order of the Euler rotations", + default="XYZ", items=eulerOrderItems, update=updateNode) + + quaternion = FloatVectorProperty( + name="Quaternion", description="Quaternion to convert", + size=4, subtype="QUATERNION", default=(0.0, 0.0, 0.0, 0.0), + update=updateNode) + + angleUnits = EnumProperty( + name="Angle Units", description="Angle units (radians/degrees/unities)", + default="RAD", items=angleUnitItems, update=updateNode) + + normalize = BoolProperty( + name='Normalize', description='Normalize the input quaternion', + default=False, update=updateNode) + + def sv_init(self, context): + self.inputs.new('SvQuaternionSocket', "Quaternions").prop_name = "quaternion" + # component outputs + self.outputs.new('StringsSocket', "W") + self.outputs.new('StringsSocket', "X") + self.outputs.new('StringsSocket', "Y") + self.outputs.new('StringsSocket', "Z") + # scalar-vector output + self.outputs.new('StringsSocket', "Scalar") + self.outputs.new('VerticesSocket', "Vector") + # euler angle ouputs + self.outputs.new('StringsSocket', "Angle X") + self.outputs.new('StringsSocket', "Angle Y") + self.outputs.new('StringsSocket', "Angle Z") + # axis-angle output + self.outputs.new('VerticesSocket', "Axis") + self.outputs.new('StringsSocket', "Angle") + # matrix ouptut + self.outputs.new('MatrixSocket', "Matrix") + + self.update_mode(context) + + def draw_buttons(self, context, layout): + layout.prop(self, "mode", expand=False, text="") + if self.mode == "EULER": + col = layout.column(align=True) + col.prop(self, "eulerOrder", text="") + if self.mode in {"EULER", "AXISANGLE"}: + row = layout.row(align=True) + row.prop(self, "angleUnits", expand=True) + if self.mode in {"WXYZ", "SCALARVECTOR"}: + layout.prop(self, "normalize", toggle=True) + + def process(self): + outputs = self.outputs + if not any(s.is_linked for s in outputs): + return + + input_Q = self.inputs['Quaternions'].sv_get()[0] + quaternionList = [Quaternion(q) for q in input_Q] + + if self.mode == "WXYZ": + if self.normalize: + quaternionList = [q.normalized() for q in quaternionList] + + for i, name in enumerate("WXYZ"): + if outputs[name].is_linked: + outputs[name].sv_set([[q[i] for q in quaternionList]]) + + elif self.mode == "SCALARVECTOR": + if self.normalize: + quaternionList = [q.normalized() for q in quaternionList] + + if outputs['Scalar'].is_linked: + scalarList = [q[0] for q in quaternionList] + outputs['Scalar'].sv_set([scalarList]) + + if outputs['Vector'].is_linked: + vectorList = [tuple(q[1:]) for q in quaternionList] + outputs['Vector'].sv_set([vectorList]) + + elif self.mode == "EULER": + au = angleConversion[self.angleUnits] + for i, name in enumerate("XYZ"): + if outputs["Angle " + name].is_linked: + angles = [q.to_euler(self.eulerOrder)[i] * au for q in quaternionList] + outputs["Angle " + name].sv_set([angles]) + + elif self.mode == "AXISANGLE": + if outputs['Axis'].is_linked: + axisList = [tuple(q.axis) for q in quaternionList] + outputs['Axis'].sv_set([axisList]) + + if outputs['Angle'].is_linked: + au = angleConversion[self.angleUnits] + angleList = [q.angle * au for q in quaternionList] + outputs['Angle'].sv_set([angleList]) + + elif self.mode == "MATRIX": + if outputs['Matrix'].is_linked: + matrixList = [q.to_matrix().to_4x4() for q in quaternionList] + outputs['Matrix'].sv_set(matrixList) + + +def register(): + bpy.utils.register_class(SvQuaternionOutNode) + + +def unregister(): + bpy.utils.unregister_class(SvQuaternionOutNode) diff --git a/nodes/text/export_gcode.py b/nodes/text/export_gcode.py new file mode 100644 index 0000000000000000000000000000000000000000..eaaacb5869fd7b9d7dffe98961ae554f1429ed84 --- /dev/null +++ b/nodes/text/export_gcode.py @@ -0,0 +1,297 @@ +# ##### BEGIN GPL LICENSE BLOCK ##### +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# ##### END GPL LICENSE BLOCK ##### + +# made by: Alessandro Zomparelli +# url: www.alessandrozomparelli.com + +import io +import itertools +import pprint +import sverchok + +import bpy, os, mathutils +from numpy import mean +import operator +from math import pi + +from bpy.props import BoolProperty, EnumProperty, StringProperty, FloatProperty, IntProperty + +from sverchok.node_tree import SverchCustomTreeNode, StringsSocket +from sverchok.data_structure import node_id, multi_socket, updateNode +from sverchok.utils.sv_itertools import sv_zip_longest2, flatten, list_of_lists, recurse_verts_fxy, match_longest_lists + +from sverchok.utils.sv_text_io_common import ( + FAIL_COLOR, READY_COLOR, TEXT_IO_CALLBACK, + get_socket_type, + new_output_socket, + name_dict, + text_modes +) + +def convert_to_text(list): + while True: + if type(list) is str: break + elif type(list) in (tuple, list): + try: + list = '\n'.join(list) + break + except: list = list[0] + else: break + return list + +class SvExportGcodeNode(bpy.types.Node, SverchCustomTreeNode): + """ + Triggers: Export gcode from vertices position + Tooltip: Generate a gcode file from a list of vertices + """ + bl_idname = 'SvExportGcodeNode' + bl_label = 'Export Gcode' + bl_icon = 'COPYDOWN' + + last_e = FloatProperty(name="Pull", default=5.0, min=0, soft_max=10) + path_length = FloatProperty(name="Pull", default=5.0, min=0, soft_max=10) + + folder = StringProperty(name="File", default="", subtype='FILE_PATH') + pull = FloatProperty(name="Pull", default=5.0, min=0, soft_max=10) + push = FloatProperty(name="Push", default=5.0, min=0, soft_max=10) + dz = FloatProperty(name="dz", default=2.0, min=0, soft_max=20) + flow_mult = FloatProperty(name="Flow Mult", default=1.0, min=0, soft_max=3) + feed = IntProperty(name="Feed Rate (F)", default=1000, min=0, soft_max=20000) + feed_horizontal = IntProperty(name="Feed Horizontal", default=2000, min=0, soft_max=20000) + feed_vertical = IntProperty(name="Feed Vertical", default=500, min=0, soft_max=20000) + feed = IntProperty(name="Feed Rate (F)", default=1000, min=0, soft_max=20000) + esteps = FloatProperty(name="E Steps/Unit", default=5, min=0, soft_max=100) + start_code = StringProperty(name="Start", default='') + end_code = StringProperty(name="End", default='') + auto_sort = BoolProperty(name="Auto Sort", default=True) + close_all = BoolProperty(name="Close Shapes", default=False) + nozzle = FloatProperty(name="Nozzle", default=0.4, min=0, soft_max=10) + layer_height = FloatProperty(name="Layer Height", default=0.1, min=0, soft_max=10) + filament = FloatProperty(name="Filament (\u03A6)", default=1.75, min=0, soft_max=120) + + gcode_mode = EnumProperty(items=[ + ("CONT", "Continuous", ""), + ("RETR", "Retraction", "") + ], default='CONT', name="Mode") + + def sv_init(self, context): + self.inputs.new('StringsSocket', 'Layer Height',).prop_name = 'layer_height' + self.inputs.new('StringsSocket', 'Flow Mult',).prop_name = 'flow_mult' + self.inputs.new('VerticesSocket', 'Vertices',) + + self.outputs.new('StringsSocket', 'Info',) + self.outputs.new('VerticesSocket', 'Vertices',) + self.outputs.new('StringsSocket', 'Printed Edges',) + self.outputs.new('StringsSocket', 'Travel Edges',) + + def draw_buttons(self, context, layout): + + addon = context.user_preferences.addons.get(sverchok.__name__) + over_sized_buttons = addon.preferences.over_sized_buttons + + col = layout.column(align=True) + row = col.row() + row.prop(self, 'folder', toggle=True, text='') + col = layout.column(align=True) + row = col.row() + row.prop(self, 'gcode_mode', expand=True, toggle=True) + #col = layout.column(align=True) + col = layout.column(align=True) + col.label(text="Extrusion:", icon='MOD_FLUIDSIM') + #col.prop(self, 'esteps') + col.prop(self, 'filament') + col.prop(self, 'nozzle') + col.separator() + col.label(text="Speed (Feed Rate F):", icon='DRIVER') + col.prop(self, 'feed', text='Print') + if self.gcode_mode == 'RETR': + col.prop(self, 'feed_vertical', text='Z Lift') + col.prop(self, 'feed_horizontal', text='Travel') + col.separator() + if self.gcode_mode == 'RETR': + col.label(text="Retraction:", icon='NOCURVE') + col.prop(self, 'pull', text='Retraction') + col.prop(self, 'dz', text='Z Hop') + col.prop(self, 'push', text='Preload') + col.prop(self, 'auto_sort', text="Sort Layers (z)") + col.prop(self, 'close_all') + col.separator() + col.label(text='Custom Code:', icon='SCRIPT') + col.prop_search(self, 'start_code', bpy.data, 'texts') + col.prop_search(self, 'end_code', bpy.data, 'texts') + col.separator() + row = col.row(align=True) + row.scale_y = 4.0 + row.operator(TEXT_IO_CALLBACK, text='Export Gcode').fn_name = 'process' + + def update_socket(self, context): + self.update() + + def process(self): + # manage data + feed = self.feed + feed_v = self.feed_vertical + feed_h = self.feed_horizontal + layer = self.layer_height + layer = self.inputs['Layer Height'].sv_get() + vertices = self.inputs['Vertices'].sv_get() + flow_mult = self.inputs['Flow Mult'].sv_get() + + # data matching + vertices = list_of_lists(vertices) + flow_mult = list_of_lists(flow_mult) + layer = list_of_lists(layer) + vertices, flow_mult, layer = match_longest_lists([vertices, flow_mult, layer]) + print(vertices) + print(layer) + + # open file + if self.folder == '': + folder = '//' + os.path.splitext(bpy.path.basename(bpy.context.blend_data.filepath))[0] + else: + folder = self.folder + if '.gcode' not in folder: folder += '.gcode' + path = bpy.path.abspath(folder) + file = open(path, 'w') + try: + for line in bpy.data.texts[self.start_code].lines: + file.write(line.body + '\n') + except: + pass + + # sort vertices + if self.auto_sort and self.gcode_mode == 'RETR': + sorted_verts = [] + for curve in vertices: + # mean z + listz = [v[2] for v in curve] + meanz = mean(listz) + # store curve and meanz + sorted_verts.append((curve, meanz)) + vertices = [data[0] for data in sorted(sorted_verts, key=lambda height: height[1])] + + # initialize variables + e = 0 + last_vert = mathutils.Vector((0,0,0)) + maxz = 0 + path_length = 0 + + printed_verts = [] + printed_edges = [] + travel_verts = [] + travel_edges = [] + + # write movements + for i in range(len(vertices)): + curve = vertices[i] + first_id = len(printed_verts) + for j in range(len(curve)): + v = curve[j] + v_flow_mult = flow_mult[i][j] + v_layer = layer[i][j] + + # record max z + maxz = max(maxz,v[2]) + + # first point of the gcode + if i == j == 0: + printed_verts.append(v) + file.write('G92 E0 \n') + params = v[:3] + (feed,) + to_write = 'G1 X{0:.4f} Y{1:.4f} Z{2:.4f} F{3:.0f}\n'.format(*params) + file.write(to_write) + else: + # start after retraction + if j == 0 and self.gcode_mode == 'RETR': + params = v[:2] + (maxz+self.dz,) + (feed_h,) + to_write = 'G1 X{0:.4f} Y{1:.4f} Z{2:.4f} F{3:.0f}\n'.format(*params) + file.write(to_write) + e += self.push + params = v[:3] + (feed_v,) + to_write = 'G1 X{0:.4f} Y{1:.4f} Z{2:.4f} F{3:.0f}\n'.format(*params) + file.write(to_write) + file.write( 'G1 E' + format(e, '.4f') + '\n') + printed_verts.append((v[0], v[1], maxz+self.dz)) + travel_edges.append((len(printed_verts)-1, len(printed_verts)-2)) + printed_verts.append(v) + travel_edges.append((len(printed_verts)-1, len(printed_verts)-2)) + # regular extrusion + else: + printed_verts.append(v) + v1 = mathutils.Vector(v) + v0 = mathutils.Vector(curve[j-1]) + dist = (v1-v0).length + print(dist) + area = v_layer * self.nozzle + pi*(v_layer/2)**2 # rectangle + circle + cylinder = pi*(self.filament/2)**2 + flow = area / cylinder + e += dist * v_flow_mult * flow + params = v[:3] + (e,) + to_write = 'G1 X{0:.4f} Y{1:.4f} Z{2:.4f} E{3:.4f}\n'.format(*params) + file.write(to_write) + path_length += dist + printed_edges.append([len(printed_verts)-1, len(printed_verts)-2]) + if self.gcode_mode == 'RETR': + v0 = mathutils.Vector(curve[-1]) + if self.close_all: + #printed_verts.append(v0) + printed_edges.append([len(printed_verts)-1, first_id]) + + v1 = mathutils.Vector(curve[0]) + dist = (v0-v1).length + area = v_layer * self.nozzle + pi*(v_layer/2)**2 # rectangle + circle + cylinder = pi*(self.filament/2)**2 + flow = area / cylinder + e += dist * v_flow_mult * flow + params = v1[:3] + (e,) + to_write = 'G1 X{0:.4f} Y{1:.4f} Z{2:.4f} E{3:.4f}\n'.format(*params) + file.write(to_write) + path_length += dist + v0 = v1 + if i < len(vertices)-1: + e -= self.pull + file.write('G0 E' + format(e, '.4f') + '\n') + params = v0[:2] + (maxz+self.dz,) + (feed_v,) + to_write = 'G1 X{0:.4f} Y{1:.4f} Z{2:.4f} F{3:.0f}\n'.format(*params) + file.write(to_write) + printed_verts.append(v0.to_tuple()) + printed_verts.append((v0.x, v0.y, maxz+self.dz)) + travel_edges.append((len(printed_verts)-1, len(printed_verts)-2)) + + # end code + try: + for line in bpy.data.texts[self.end_code].lines: + file.write(line.body + '\n') + except: + pass + file.close() + print("Saved gcode to " + path) + info = "Extruded Filament: " + format(e, '.2f') + '\n' + info += "Extruded Volume: " + format(e*pi*(self.filament/2)**2, '.2f') + '\n' + info += "Printed Path: " + format(path_length, '.2f') + self.outputs[0].sv_set(info) + self.outputs[1].sv_set(printed_verts) + self.outputs[2].sv_set(printed_edges) + self.outputs[3].sv_set(travel_edges) + +def register(): + bpy.utils.register_class(SvExportGcodeNode) + + +def unregister(): + bpy.utils.unregister_class(SvExportGcodeNode) diff --git a/nodes/text/stethoscope_mk2.py b/nodes/text/stethoscope_mk2.py index 6fa5d89a6e511f0b36862557034edca154ffb700..31744a2036daf9c38b7038828ad3c802613cb2fb 100644 --- a/nodes/text/stethoscope_mk2.py +++ b/nodes/text/stethoscope_mk2.py @@ -132,7 +132,7 @@ class SvStethoscopeNodeMK2(bpy.types.Node, SverchCustomTreeNode): try: with sv_preferences() as prefs: scale = prefs.stethoscope_view_scale - location_theta = prefs.stethoscope_view_xy_multiplier + location_theta = prefs.render_location_xy_multiplier except: # print('did not find preferences - you need to save user preferences') scale = 1.0 @@ -142,7 +142,6 @@ class SvStethoscopeNodeMK2(bpy.types.Node, SverchCustomTreeNode): data = inputs[0].sv_get(deepcopy=False) self.num_elements = len(data) - if self.selected_mode == 'text-based': props = lambda: None props.line_width = self.line_width @@ -190,7 +189,7 @@ class SvStethoscopeNodeMK2(bpy.types.Node, SverchCustomTreeNode): return try: if not self.inputs[0].other: - nvBGL.callback_disable(node_id(self)) + nvBGL.callback_disable(node_id(self)) except: print('stethoscope update holdout (not a problem)') diff --git a/nodes/viz/texture_viewer.py b/nodes/viz/texture_viewer.py index e267325e1e615478559fa8dd7f436ac6b94b4659..e0854e852293f3d8ba96a536f3be4067bab73252 100644 --- a/nodes/viz/texture_viewer.py +++ b/nodes/viz/texture_viewer.py @@ -26,6 +26,7 @@ from bpy.props import ( FloatProperty, EnumProperty, StringProperty, BoolProperty, IntProperty ) +from sverchok.utils.context_managers import sv_preferences from sverchok.data_structure import updateNode, node_id from sverchok.node_tree import SverchCustomTreeNode from sverchok.ui import nodeview_bgl_viewer_draw_mk2 as nvBGL2 @@ -384,6 +385,18 @@ class SvTextureViewerNode(bpy.types.Node, SverchCustomTreeNode): self.texture[n_id] = name[0] init_texture(width, height, name[0], texture, gl_color_constant) + # adjust render location based on preference multiplier setting + try: + with sv_preferences() as prefs: + multiplier = prefs.render_location_xy_multiplier + scale = prefs.render_scale + except: + # print('did not find preferences - you need to save user preferences') + multiplier = 1.0 + scale = 1.0 + x, y = [x * multiplier, y * multiplier] + width, height =[width * scale, height * scale] + draw_data = { 'tree_name': self.id_data.name[:], 'mode': 'custom_function', diff --git a/nodes/viz/texture_viewer_lite.py b/nodes/viz/texture_viewer_lite.py index 9363d37719b5b56f1c2680191534be6aedd1cde2..ddb88be9b8d52846d6f3e4b752156fb0f85b81da 100644 --- a/nodes/viz/texture_viewer_lite.py +++ b/nodes/viz/texture_viewer_lite.py @@ -24,6 +24,7 @@ from bpy.props import EnumProperty, StringProperty, IntProperty from sverchok.data_structure import updateNode, node_id from sverchok.node_tree import SverchCustomTreeNode from sverchok.ui import nodeview_bgl_viewer_draw_mk2 as nvBGL2 +from sverchok.utils.context_managers import sv_preferences gl_color_list = [ @@ -153,11 +154,25 @@ class SvTextureViewerNodeLite(bpy.types.Node, SverchCustomTreeNode): bgl.glGenTextures(1, name) self.texture[n_id] = name[0] init_texture(width, height, name[0], texture, gl_color_dict.get(colm)) + + # adjust render location based on preference multiplier setting + try: + with sv_preferences() as prefs: + multiplier = prefs.render_location_xy_multiplier + scale = prefs.render_scale + except: + # print('did not find preferences - you need to save user preferences') + multiplier = 1.0 + scale = 1.0 + + x, y = [self.location[0] * multiplier, self.location[1] * multiplier] + width, height =[width * scale, height * scale] + draw_data = { 'tree_name': self.id_data.name, 'mode': 'custom_function', 'custom_function': simple_screen, - 'loc': (self.location[0] + self.width + 20, self.location[1]), + 'loc': (x + self.width*scale + 20, y), 'args': (texture, self.texture[n_id], width, height) } nvBGL2.callback_enable(n_id, draw_data) diff --git a/nodes/viz/viewer_polyline_mk1.py b/nodes/viz/viewer_polyline_mk1.py index ee6f4f164ff435db0d79ee3d35eacaca81166766..a1d940c1bfca2d95e707dca24ee3fbf7cb4db16d 100644 --- a/nodes/viz/viewer_polyline_mk1.py +++ b/nodes/viz/viewer_polyline_mk1.py @@ -316,8 +316,8 @@ class SvPolylineViewerNodeMK1(bpy.types.Node, SverchCustomTreeNode): return dataCorrect(data) mverts = get('vertices') - mradii = self.inputs['radii'].sv_get(deepcopy=False) - mtwist = self.inputs['twist'].sv_get(deepcopy=False) + mradii = self.inputs['radii'].sv_get(deepcopy=True) + mtwist = self.inputs['twist'].sv_get(deepcopy=True) mmtrix = get('matrix') # extend all non empty lists to longest of these diff --git a/nodes/generator/line_mk2.py b/old_nodes/line_mk2.py similarity index 98% rename from nodes/generator/line_mk2.py rename to old_nodes/line_mk2.py index 319ee544901081c68a45b94e03ba071a40bb0756..fe9060b938b46f8a3773cfde8c017af9e0a274fb 100644 --- a/nodes/generator/line_mk2.py +++ b/old_nodes/line_mk2.py @@ -48,6 +48,8 @@ class SvLineNodeMK2(bpy.types.Node, SverchCustomTreeNode): bl_label = 'Line MK2' bl_icon = 'GRIP' + replacement_nodes = [('SvLineNodeMK3', None, None)] + def upgrade_if_needed(self): """ This allows us to keep the node mk2 - on the fly node upgrade""" if "Size" not in self.inputs: diff --git a/nodes/number/mix_numbers.py b/old_nodes/mix_numbers.py similarity index 100% rename from nodes/number/mix_numbers.py rename to old_nodes/mix_numbers.py diff --git a/settings.py b/settings.py index a1a0460fa0209c36434f338ae885a31b5ee697ba..100c9a3fcb715c71c9a761463f47e52e962751b7 100644 --- a/settings.py +++ b/settings.py @@ -30,7 +30,7 @@ class SverchokPreferences(AddonPreferences): color_def.apply_theme() tab_modes = [(k, k, '', i) for i, k in enumerate(["General", "Node Defaults", "Theme"])] - + selected_tab = bpy.props.EnumProperty( items=tab_modes, description="pick viewing mode", @@ -163,10 +163,17 @@ class SverchokPreferences(AddonPreferences): enable_live_objin = BoolProperty( description="Objects in edit mode will be updated in object-in Node") + render_scale = FloatProperty( + default=1.0, min=0.01, step=0.01, description='default render scale') + + render_location_xy_multiplier = FloatProperty( + default=1.0, min=0.01, step=0.01, description='default render location xy multiplier') + stethoscope_view_scale = FloatProperty( default=1.0, min=0.01, step=0.01, description='default stethoscope scale') - stethoscope_view_xy_multiplier = FloatProperty( - default=1.0, min=0.01, step=0.01, description='default stethoscope scale') + + index_viewer_scale = FloatProperty( + default=1.0, min=0.01, step=0.01, description='default index viewer scale') datafiles = os.path.join(bpy.utils.user_resource('DATAFILES', path='sverchok', create=True)) defaults_location = StringProperty(default=datafiles, description='usually ..data_files\\sverchok\\defaults\\nodes.json') @@ -178,7 +185,7 @@ class SverchokPreferences(AddonPreferences): def update_log_level(self, context): logging.info("Setting log level to %s", self.log_level) logging.setLevel(self.log_level) - + log_levels = [ ("DEBUG", "Debug", "Debug output", 0), ("INFO", "Information", "Informational output", 1), @@ -267,9 +274,13 @@ class SverchokPreferences(AddonPreferences): row_sub1 = col.row().split(0.5) box_sub1 = row_sub1.box() box_sub1_col = box_sub1.column(align=True) - box_sub1_col.label('stethoscope mk2 settings') + box_sub1_col.label('Render Scale & Location') + box_sub1_col.prop(self, 'render_location_xy_multiplier', text='xy multiplier') + box_sub1_col.prop(self, 'render_scale', text='scale') + box_sub1_col.label('Stethoscope MK2 settings') box_sub1_col.prop(self, 'stethoscope_view_scale', text='scale') - box_sub1_col.prop(self, 'stethoscope_view_xy_multiplier', text='xy multiplier') + box_sub1_col.label('Index Viewer settings') + box_sub1_col.prop(self, 'index_viewer_scale', text='scale') col3 = row_sub1.split().column() col3.label('Location of custom defaults') diff --git a/ui/icons/sv_mix_inputs.png b/ui/icons/sv_mix_inputs.png new file mode 100644 index 0000000000000000000000000000000000000000..790b24088d842adb38620ec9b802a006c73baa31 Binary files /dev/null and b/ui/icons/sv_mix_inputs.png differ diff --git a/ui/index_viewer_draw.py b/ui/index_viewer_draw.py index 44f9a3655b0f6437256f24b1313d75cf39af34ff..49b81b7638a32414767f42049f4bc272e68136a4 100644 --- a/ui/index_viewer_draw.py +++ b/ui/index_viewer_draw.py @@ -24,6 +24,7 @@ import blf import bgl from mathutils import Vector +from sverchok.utils.context_managers import sv_preferences from sverchok.data_structure import Vector_generate, Matrix_generate SpaceView3D = bpy.types.SpaceView3D @@ -154,8 +155,15 @@ def draw_callback_px(n_id, draw_verts, draw_edges, draw_faces, draw_matrix, draw display_edge_index = settings['display_edge_index'] display_face_index = settings['display_face_index'] + try: + with sv_preferences() as prefs: + scale = prefs.index_viewer_scale + except: + # print('did not find preferences - you need to save user preferences') + scale = 1.0 + font_id = 0 - text_height = 13 + text_height = int(13.0 * scale) blf.size(font_id, text_height, 72) # should check prefs.dpi region_mid_width = region.width / 2.0 @@ -222,7 +230,7 @@ def draw_callback_px(n_id, draw_verts, draw_edges, draw_faces, draw_matrix, draw if data_edges and display_edge_index: for edge_index, (idx1, idx2) in enumerate(data_edges[obj_index]): - + v1 = Vector(final_verts[idx1]) v2 = Vector(final_verts[idx2]) loc = v1 + ((v2 - v1) / 2) diff --git a/ui/nodeview_rclick_menu.py b/ui/nodeview_rclick_menu.py index 6ca2cb026b69c699ad0e3ec02c744725d7bfcb6d..682c28fc0157e3ff024a3f0d94e7e2a06c162b28 100644 --- a/ui/nodeview_rclick_menu.py +++ b/ui/nodeview_rclick_menu.py @@ -95,7 +95,12 @@ def add_connection(tree, bl_idname_new_node, offset): elif bl_idname_new_node == 'SvStethoscopeNodeMK2': # we can't determin thru cursor location which socket was nearest the rightclick # maybe in the future.. or if someone does know :) - links.new(outputs[0], inputs[0]) + for socket in outputs: + if socket.hide: + continue + # connect_stethoscope to first visible output socket of active node + links.new(socket, inputs[0]) + break elif bl_idname_new_node == 'ViewerNode2': diff --git a/utils/modules/geom_utils.py b/utils/modules/geom_utils.py index 2529f95bea3224a317824aedf37b3acba3251cf4..1ad0870d450b91b22a1eb69a7ef84835c9ef302a 100644 --- a/utils/modules/geom_utils.py +++ b/utils/modules/geom_utils.py @@ -38,6 +38,9 @@ def normalize(v): def sub_v3_v3v3(a, b): return a[0]-b[0], a[1]-b[1], a[2]-b[2] +def add_v3_v3v3(a, b): + return a[0]+b[0], a[1]+b[1], a[2]+b[2] + def madd_v3_v3v3fl(a, b, f=1.0): return a[0]+b[0]*f, a[1]+b[1]*f, a[2]+b[2]*f diff --git a/utils/modules/statistics_functions.py b/utils/modules/statistics_functions.py new file mode 100644 index 0000000000000000000000000000000000000000..193c5b61cbe77cbb349afa2ce23f5955ebae154f --- /dev/null +++ b/utils/modules/statistics_functions.py @@ -0,0 +1,121 @@ +# ##### BEGIN GPL LICENSE BLOCK ##### +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# ##### END GPL LICENSE BLOCK ##### + +from functools import reduce +from math import sqrt, floor +import sys + + +def get_sum(values): + return sum(values) + + +def get_sum_of_squares(values): + return sum([v * v for v in values]) + + +def get_sum_of_inversions(values): + return sum([1.0 / v for v in values]) + + +def get_product(values): + return reduce((lambda x, y: x * y), values) + + +def get_average(values): + return sum(values) / len(values) + + +def get_geometric_mean(values): + return pow(get_product(values), 1.0 / len(values)) + + +def get_harmonic_mean(values): + return len(values) / get_sum_of_inversions(values) + + +def get_standard_deviation(values): + a = get_average(values) + return sqrt(sum([(v - a)**2 for v in values])) + + +def get_root_mean_square(values): + return sqrt(get_sum_of_squares(values) / len(values)) + + +def get_skewness(values): + a = get_average(values) + n = len(values) + s = get_standard_deviation(values) + return sum([(v - a)**3 for v in values]) / n / pow(s, 3) + + +def get_kurtosis(values): + a = get_average(values) + n = len(values) + s = get_standard_deviation(values) + return sum([(v - a)**4 for v in values]) / n / pow(s, 4) + + +def get_minimum(values): + return min(values) + + +def get_maximum(values): + return max(values) + + +def get_median(values): + sortedValues = sorted(values) + index = int(floor(len(values) / 2)) + print("index=", index) + if len(values) % 2 == 0: # even number of values ? => take the average of central values + median = (sortedValues[index - 1] + sortedValues[index]) / 2 + else: # odd number of values ? => take the central value + median = sortedValues[index] + + return median + + +def get_percentile(values, percentage): + sortedValues = sorted(values) + index = int(min(int(floor(len(values) * percentage)), len(values) - 1)) + return sortedValues[index] + + +def get_histogram(values, numBins, normalize=False, normalizedSize=10): + minValue = get_minimum(values) + maxValue = get_maximum(values) + + binSize = max((maxValue - minValue) / numBins, sys.float_info.min) + + # initialize the histogram bins + histogram = [0] * numBins + + # populate the histogram bins + for i in range(len(values)): + binIndex = int(min(int(floor((values[i] - minValue) / binSize)), numBins - 1)) + histogram[binIndex] = histogram[binIndex] + 1 + + # normalize histogram ? + if normalize: + binMax = max(histogram) + for i in range(len(histogram)): + histogram[i] = histogram[i] / binMax * normalizedSize + + return histogram diff --git a/utils/sv_itertools.py b/utils/sv_itertools.py index b47669bb906391d900acdfc68ac1bc94d3ca462f..781c6a39cfe01276c181459973ebf1c1681df084 100644 --- a/utils/sv_itertools.py +++ b/utils/sv_itertools.py @@ -25,7 +25,7 @@ class SvSentinel: class sv_zip_longest: def __init__(self, *args): - self.counter = len(args) + self.counter = len(args) self.iterators = [] for lst in args: fl = lst[-1] @@ -33,7 +33,7 @@ class sv_zip_longest: self.iterators.append(chain(lst, SvSentinel(fl,self), filler)) def __next__(self): - try: + try: if self.counter: return tuple(map(next, self.iterators)) else: @@ -42,8 +42,7 @@ class sv_zip_longest: raise StopIteration def __iter__(self): - return self - + return self def sv_zip_longest2(*args): # by zeffi @@ -51,8 +50,8 @@ def sv_zip_longest2(*args): itrs = [iter(sl) for sl in args] for i in range(longest): yield tuple((next(iterator, args[idx][-1]) for idx, iterator in enumerate(itrs))) - - + + def recurse_fx(l, f): if isinstance(l, (list, tuple)): return [recurse_fx(i, f) for i in l] @@ -61,9 +60,9 @@ def recurse_fx(l, f): def recurse_fxy(l1, l2, f): l1_type = isinstance(l1, (list, tuple)) - l2_type = isinstance(l2, (list, tuple)) + l2_type = isinstance(l2, (list, tuple)) if not (l1_type or l2_type): - return f(l1, l2) + return f(l1, l2) elif l1_type and l2_type: fl = l2[-1] if len(l1) > len(l2) else l1[-1] res = [] @@ -77,6 +76,80 @@ def recurse_fxy(l1, l2, f): return [recurse_fxy(l1, y, f) for y in l2] +def recurse_verts_fxy(l1, l2, f): + l1_type = isinstance(l1, (list)) + l2_type = isinstance(l2, (list)) + if not (l1_type or l2_type): + return f(l1, l2) + elif l1_type and l2_type: + fl = l2[-1] if len(l1) > len(l2) else l1[-1] + res = [] + res_append = res.append + for x, y in zip_longest(l1, l2, fillvalue=fl): + res_append(recurse_verts_fxy(x, y, f)) + return res + elif l1_type and not l2_type: + return [recurse_verts_fxy(x, l2, f) for x in l1] + else: #not l1_type and l2_type + return [recurse_verts_fxy(l1, y, f) for y in l2] + +# append all the elements to one single list +def append_all(l, flat): + if isinstance(l,(list)): + return [append_all(i, flat) for i in l] + else: + flat.append(l) + return l + +# flatten sublists +def flatten(l): + flat = [] + append_all(l, flat) + return flat + +# append all the lists to one single list +def append_lists(l, lists): + if isinstance(l,(list)): + flat_list = True + for i in l: + if isinstance(i,(list)): + flat_list = False + break + if flat_list: + lists.append(l) + return None + else: + return [append_lists(i, lists) for i in l] + else: + lists.append([l]) + return None + +# generate a single list with 1 level lists inside +def list_of_lists(l): + out_list = [] + append_lists(l, out_list) + return out_list + +# works with irregular sublists +def match_longest_lists(lists): + add_level = max([isinstance(l, list) for l in lists]) + if add_level: + for i in range(len(lists)): + l = lists[i] + if not isinstance(l, list): + lists[i] = [l] + length = [len(l) for l in lists] + max_length = max(length) + # extend shorter lists + for l in lists: + for i in range(len(l), max_length): + l.append(l[-1]) + try: + return zip(*[match_longest_lists([l[i] for l in lists]) for i in range(max_length)]) + except: + return lists + + def extend_if_needed(vl, wl, default=0.5): # match wl to correspond with vl try: