diff --git a/core/socket_conversions.py b/core/socket_conversions.py index b128eae8e3..940d466752 100644 --- a/core/socket_conversions.py +++ b/core/socket_conversions.py @@ -16,7 +16,9 @@ # # ##### END GPL LICENSE BLOCK ##### -from sverchok.data_structure import get_other_socket +from sverchok.data_structure import get_other_socket, get_data_nesting_level +from sverchok.utils.field.vector import SvMatrixVectorField, SvConstantVectorField +from sverchok.utils.field.scalar import SvConstantScalarField from mathutils import Matrix, Quaternion from numpy import ndarray @@ -45,6 +47,18 @@ def is_matrix_to_quaternion(self): def is_quaternion_to_matrix(self): return cross_test_socket(self, 'q', 'm') +def is_matrix_to_vfield(socket): + other = get_other_socket(socket) + return other.bl_idname == 'SvMatrixSocket' and socket.bl_idname == 'SvVectorFieldSocket' + +def is_vertex_to_vfield(socket): + other = get_other_socket(socket) + return other.bl_idname == 'SvVerticesSocket' and socket.bl_idname == 'SvVectorFieldSocket' + +def is_string_to_sfield(socket): + other = get_other_socket(socket) + return other.bl_idname == 'SvStringsSocket' and socket.bl_idname == 'SvScalarFieldSocket' + # --- @@ -119,6 +133,30 @@ def get_all(data): get_all(data) return [locations] +def matrices_to_vfield(data): + if isinstance(data, Matrix): + return SvMatrixVectorField(data) + elif isinstance(data, (list, tuple)): + return [matrices_to_vfield(item) for item in data] + else: + raise TypeError("Unexpected data type from Matrix socket: %s" % type(data)) + +def vertices_to_vfield(data): + if isinstance(data, (tuple, list)) and len(data) == 3 and isinstance(data[0], (float, int)): + return SvConstantVectorField(data) + elif isinstance(data, (list, tuple)): + return [vertices_to_vfield(item) for item in data] + else: + raise TypeError("Unexpected data type from Vertex socket: %s" % type(data)) + +def numbers_to_sfield(data): + if isinstance(data, (int, float)): + return SvConstantScalarField(data) + elif isinstance(data, (list, tuple)): + return [numbers_to_sfield(item) for item in data] + else: + raise TypeError("Unexpected data type from String socket: %s" % type(data)) + class ImplicitConversionProhibited(Exception): def __init__(self, socket): super().__init__() @@ -201,3 +239,19 @@ def quaternions_to_matrices(cls, socket, source_data): @classmethod def matrices_to_quaternions(cls, socket, source_data): return get_quaternions_from_matrices(source_data) + +class FieldImplicitConversionPolicy(DefaultImplicitConversionPolicy): + @classmethod + def convert(cls, socket, source_data): + if is_matrix_to_vfield(socket): + return matrices_to_vfield(source_data) + elif is_vertex_to_vfield(socket): + return vertices_to_vfield(source_data) + elif is_string_to_sfield(socket): + level = get_data_nesting_level(source_data) + if level > 2: + raise TypeError("Too high data nesting level for Number -> Scalar Field conversion: %s" % level) + return numbers_to_sfield(source_data) + else: + super().convert(socket, source_data) + diff --git a/core/sockets.py b/core/sockets.py index cefb6faa0d..d3de804beb 100644 --- a/core/sockets.py +++ b/core/sockets.py @@ -23,7 +23,11 @@ from bpy.props import StringProperty, BoolProperty, FloatVectorProperty, IntProperty, FloatProperty from bpy.types import NodeTree, NodeSocket -from sverchok.core.socket_conversions import DefaultImplicitConversionPolicy, is_vector_to_matrix +from sverchok.core.socket_conversions import ( + DefaultImplicitConversionPolicy, + FieldImplicitConversionPolicy, + is_vector_to_matrix + ) from sverchok.core.socket_data import ( SvGetSocketInfo, SvGetSocket, SvSetSocket, @@ -35,6 +39,9 @@ socket_id, replace_socket) +from sverchok.utils.field.scalar import SvConstantScalarField +from sverchok.utils.field.vector import SvMatrixVectorField, SvConstantVectorField + socket_colors = { "SvStringsSocket": (0.6, 1.0, 0.6, 1.0), "SvVerticesSocket": (0.9, 0.6, 0.2, 1.0), @@ -591,6 +598,97 @@ def sv_get(self, default=sentinel, deepcopy=True): def draw_color(self, context, node): return self.dynamic_color +class SvSurfaceSocket(NodeSocket, SvSocketCommon): + bl_idname = "SvSurfaceSocket" + bl_label = "Surface Socket" + + def get_prop_data(self): + return {} + + def draw_color(self, context, node): + return (0.4, 0.2, 1.0, 1.0) + + def sv_get(self, default=sentinel, deepcopy=True, implicit_conversions=None): + if self.is_linked and not self.is_output: + source_data = SvGetSocket(self, deepcopy=True if self.needs_data_conversion() else deepcopy) + return self.convert_data(source_data, implicit_conversions) + + if self.prop_name: + return [[getattr(self.node, self.prop_name)[:]]] + elif default is sentinel: + raise SvNoDataError(self) + else: + return default + +class SvCurveSocket(NodeSocket, SvSocketCommon): + bl_idname = "SvCurveSocket" + bl_label = "Curve Socket" + + def get_prop_data(self): + return {} + + def draw_color(self, context, node): + return (0.5, 0.6, 1.0, 1.0) + + def sv_get(self, default=sentinel, deepcopy=True, implicit_conversions=None): + if self.is_linked and not self.is_output: + source_data = SvGetSocket(self, deepcopy=True if self.needs_data_conversion() else deepcopy) + return self.convert_data(source_data, implicit_conversions) + + if self.prop_name: + return [[getattr(self.node, self.prop_name)[:]]] + elif default is sentinel: + raise SvNoDataError(self) + else: + return default + +class SvScalarFieldSocket(NodeSocket, SvSocketCommon): + bl_idname = "SvScalarFieldSocket" + bl_label = "Scalar Field Socket" + + def get_prop_data(self): + return {} + + def draw_color(self, context, node): + return (0.9, 0.4, 0.1, 1.0) + + def sv_get(self, default=sentinel, deepcopy=True, implicit_conversions=None): + if implicit_conversions is None: + implicit_conversions = FieldImplicitConversionPolicy + if self.is_linked and not self.is_output: + source_data = SvGetSocket(self, deepcopy=True if self.needs_data_conversion() else deepcopy) + return self.convert_data(source_data, implicit_conversions) + + if self.prop_name: + return [[getattr(self.node, self.prop_name)[:]]] + elif default is sentinel: + raise SvNoDataError(self) + else: + return default + +class SvVectorFieldSocket(NodeSocket, SvSocketCommon): + bl_idname = "SvVectorFieldSocket" + bl_label = "Vector Field Socket" + + def get_prop_data(self): + return {} + + def draw_color(self, context, node): + return (0.1, 0.1, 0.9, 1.0) + + def sv_get(self, default=sentinel, deepcopy=True, implicit_conversions=None): + if implicit_conversions is None: + implicit_conversions = FieldImplicitConversionPolicy + if self.is_linked and not self.is_output: + source_data = SvGetSocket(self, deepcopy=True if self.needs_data_conversion() else deepcopy) + return self.convert_data(source_data, implicit_conversions) + + if self.prop_name: + return [[getattr(self.node, self.prop_name)[:]]] + elif default is sentinel: + raise SvNoDataError(self) + else: + return default """ type_map_to/from are used to get the bl_idname from a single letter @@ -621,7 +719,9 @@ def draw_color(self, context, node): classes = [ SvVerticesSocket, SvMatrixSocket, SvStringsSocket, SvColorSocket, SvQuaternionSocket, SvDummySocket, SvSeparatorSocket, - SvTextSocket, SvObjectSocket, SvDictionarySocket, SvChameleonSocket + SvTextSocket, SvObjectSocket, SvDictionarySocket, SvChameleonSocket, + SvSurfaceSocket, SvCurveSocket, SvScalarFieldSocket, SvVectorFieldSocket ] register, unregister = bpy.utils.register_classes_factory(classes) + diff --git a/docs/curves.rst b/docs/curves.rst new file mode 100644 index 0000000000..753bbfdb98 --- /dev/null +++ b/docs/curves.rst @@ -0,0 +1,95 @@ + +Curve +----- + +Sverchok uses the term **Curve** mostly in the same way as it is used in mathematics. + +From the user perspective, a Curve object is just a (more or less smooth) curve +laying in 3D space, which goes from one point (start) to another point (end). +The start and the end point of the curve may coincide, in this case we say the +curve is **closed**, or **cyclic**. + +Let's state some properties of the Curve object, which will define our +understanding of this term in context of Sverchok: + +* A curve is a finite, one-dimensional object, existing in 3D space. +* Every curve must have exactly two endpoints. No more, no less. +* If the endpoints coincide, the curve is considered to be closed. +* There may be no gaps on the interior of a curve, as that would result in more + than two endpoints. +* There may be no branching points on the curve, except where an endpoint is + coincident with some interior point of the curve. + +Mathematically, a Curve is a set of points in 3D space, which can be defined as +a codomain of some function from R to R^3; i.e. the function, which maps some +real number to a vector in 3D space. We will be considering only "good enough" +functions; more exactly, such function must be continuous, and have at least 3 +derivatives at (mostly) each point. + +It is important to understand, that each curve can be defined by more than one +function (which is called parameterization of the curve). We usually use the +one which is most fitting our goals in specific task. + +Usually we use the letter **t** for curve parameter; in some case the letter **u** is used. + +For example, let's consider a straight line segment, which is beginning at `(0, +0, 0)` and ending at `(1, 1, 1)`. The following parameterizations all define +the same line: + +A) + + x(t) = t + + y(t) = t + + z(t) = t + +B) + x(t) = t^2 + + y(t) = t^2 + + z(t) = t^2 + +C) + + x(t) = t^3 + + y(t) = t^3 + + z(t) = t^3 + +As you understand, we can write down as many equations for it as we want. + +Different parametrizations of the same curve can have different values of **t** +parameter corresponding to the beginning and the end of the curve. For example, + +D) + x(t) = t - 1 + + y(t) = t - 1 + + z(t) = t - 1 + +defines the parametrization, for which `t = 1` corresponds to the beginning of +the segment, and `t = 2` corresponds to the end of the segment. The range of +the curve parameter which corresponds to the curve from it's beginning to the +end is called **curve domain**. + +Another important thing to understand is that the value of curve parameter at +some point has nothing to do with the length of the curve. With our straight +line example, if we consider A) parametrization at the point of `t = 0.5`, it +will be `(0.5, 0.5, 0.5)`, i.e. the middle of the segment. But, if we take the +same segment with B) parametrization, `t = 0.5` will give us `(0.25, 0.25, +0.25)`, which is not the middle of the segment at all. + +Among all possible parametrizations of a curve, one is distinguished. It is +called **natural parametrization**. The natural parametrization of the curve +has the property: certain change in **t** parameter corresponds to exactly the +same change in curve length. Each curve has exactly one natural +parametrization. + +Since Blender has mostly mesh-based approach to modelling, as well as Sverchok, +to "visualize" the Curve object, you have to convert it to mesh. It is usually +done by use of "Evaluate Curve" node, or "Curve Length Parameter" node. + diff --git a/docs/fields.rst b/docs/fields.rst new file mode 100644 index 0000000000..762e1c7f13 --- /dev/null +++ b/docs/fields.rst @@ -0,0 +1,32 @@ + +Scalar Fields +------------- + +A scalar field is a mathematical object, which is defined as a function from R^3 to R, i.e. a mapping which maps each point in a 3D space to some number. Such objects are quite common in mathematics and in physics too; for example, you can take a field of temperatures (which maps each point to the temperature in that point). + +Technically, scalar field is defined as Python class, which can calculate some number for a provided point in space. Note that the definition of the field function can be quite complex, but it does not mean that that complex definition will be executed for all points in 3D space — only for points for which it is actually required to know the value. + +Sverchok can generate such fields by several ways, including user-provided formulas; it can execute some mathematical operations on them (such as addition or multiplication). + +Vector Fields +------------- + +A vector field is a mathematical object, which is defined as a function from R^3 to R^3, i.e. a mapping which maps each point in 3D space into a 3D vector. Such objects are very common in mathematics and in physics; for example, consider the field of some force, for example the gravitation field, which defines the gravitation force vector for each point in space. + +Note that when we are talking about vector fields, there are two possible ways to interpret their values: + +1. to think that the vector, which is defined by vector field in point P, starts in point P and ends in some point P'; +2. to think that the vector which is defined by vector field in point P, starts in the origin and ends in some point Q. + +In physics, the first approach is the most common, and it is mostly used by Sverchok. + +One can note, that both approaches are easily convertible: if you have a field, which maps point P to a vector from 0 to Q, then you can say that you have a field which maps point P to a vector from P to (P+Q). Or the other way around, if you have a field which maps point P to a vector from P to P', then you can say that you have a field which maps point P to a vector from 0 to (P' - P). + +"Apply vector field" node follows the first approach, i.e. for each provided point P it returns the point to which P would be mapped if we understand that the vector VectorField(P) starts at P. Mathematically, it returns `P + VectorField(P)`. In most cases, you will want to use this node instead of "evaluate vector field". + +"Evaluate vector field" node, on the other hand, follows the second approach, i.e. for each point P it returns VectorField(P). + +Technically, vector field is a Python class that can be asked to return a vector for any 3D point. Note that the definition of the field function can be quite complex, but it does not mean that that complex definition will be executed for all points in 3D space — only for points for which it is actually required to know the value. + +Sverchok can generate such fields by several ways, including user-provided formulas; it can execute some mathematical operations on them (such as addition or multiplication). Vector field can be applied to some set of vertices to receive another one — i.e., deform some mesh by field. There are also several ways to convert vector fields to scalar fields (for example, you can take a norm of the vector field, or use divergence differential operator). + diff --git a/docs/geometry.rst b/docs/geometry.rst index a84ac8246b..557c76927b 100644 --- a/docs/geometry.rst +++ b/docs/geometry.rst @@ -11,6 +11,14 @@ further study:: List, Index, Vector, Vertex, Edge, Polygon, Normal, Transformation, and Matrix. +Also, Sverchok uses the following terms to generate and modify geometrical entities: + +* Curve +* Surface +* Scalar Field +* Vector Field + +Although the terms by theirselve are of common use and most probably you know them, there are some things we have to discuss about them in Sverchok context. They will be discussed in corresponding paragraphs below. List ---- @@ -182,9 +190,3 @@ Ready? I think this broadly covers the things you should be comfortable with before Sverchok will make sense. - -Sverchok --------- - -This section will introduce you to a selection of nodes that can be combined -to create renderable geometry. Starting with the simple Plane generator diff --git a/docs/induction.rst b/docs/induction.rst index 203c141800..7d6e93e955 100644 --- a/docs/induction.rst +++ b/docs/induction.rst @@ -6,6 +6,9 @@ Introduction to Sverchok You have installed the addon, if not then read the installation notes. If you've ever used Nodes for anything in Blender, Cycles / Compositor Nodes feel free to continue straight to Unit 01 if you see the RNA icon in the list of NodeView types. +This section will introduce you to a selection of nodes that can be combined +to create renderable geometry. Starting with the simple Plane generator + Unit 00 - Introduction to NodeView and 3DView --------------------------------------------- diff --git a/docs/main.rst b/docs/main.rst index 9fc787c9b0..1213e62e79 100644 --- a/docs/main.rst +++ b/docs/main.rst @@ -15,6 +15,9 @@ Contents: installation geometry + curves + surfaces + fields induction panels nodes diff --git a/docs/nodes.rst b/docs/nodes.rst index 5dfbed7589..10665c1c9a 100644 --- a/docs/nodes.rst +++ b/docs/nodes.rst @@ -7,6 +7,9 @@ Nodes Generators Generators Extended + Curves + Surfaces + Fields Transforms Analyzers Modifier Change diff --git a/docs/nodes/curve/apply_field_to_curve.rst b/docs/nodes/curve/apply_field_to_curve.rst new file mode 100644 index 0000000000..fa48905231 --- /dev/null +++ b/docs/nodes/curve/apply_field_to_curve.rst @@ -0,0 +1,41 @@ +Apply Field to Curve +==================== + +Functionality +------------- + +This node generates a Curve object by taking another Curve and "bending" it +according to some Vector Field. More precisely, it generates a curve, each +point of which is calculated as `x + Coefficient * VectorPoint(x)`. + +Curve domain: the same as the domain of curve being deformed. + +Inputs +------ + +This node has the following inputs: + +* **Field**. Vector field to be applied to the curve. This input is mandatory. +* **Curve**. Curve to be "bent" by vector field. This input is mandatory. +* **Coefficient**. Vector field application coefficient (0 means vector field + will have no effect). The default value is 1.0. + +Parameters +---------- + +This node has no parameters. + +Outputs +------- + +This node has the following output: + +* **Curve**. The curve modified by vector field. + +Example of usage +---------------- + +Several Line curves modified by Noise vector field: + +.. image:: https://user-images.githubusercontent.com/284644/77443601-fd816500-6e0c-11ea-9ed2-0516eba95951.png + diff --git a/docs/nodes/curve/cast_curve.rst b/docs/nodes/curve/cast_curve.rst new file mode 100644 index 0000000000..e55714f68b --- /dev/null +++ b/docs/nodes/curve/cast_curve.rst @@ -0,0 +1,63 @@ +Cast Curve +========== + +Functionality +------------- + +This node generates a Curve object by casting (projecting) another Curve onto one of predefined shapes. + +Curve domain: the same as the curve being projected. + +Inputs +------ + +This node has the following inputs: + +* **Curve**. Curve to be projected. This input is mandatory. +* **Center**. The meaning of this input depends on **Target form** parameter: + + * for **Plane**, this is a point on the plane; + * for **Shpere**, this is the center of the sphere; + * for **Cylinder**, this is a point on cylinder's axis line. + +* **Direction**. This parameter is available only when **Target form** parameter is set to **Plane** or **Cylinder**. It's meaning depends on target form: + + * for **Plane**, this is the normal direction of the plane; + * for **Cyinder**, this is the directing vector of cylinder's axis line. + +* **Radius**. This parameter is available only when **Target form** parameter is set to **Sphere** or **Cylinder. It's meaning depends on target form: + + * for **Sphere**, this is the radius of the sphere; + * for **Cylinder**, this is the radius of the cylinder. + +* **Coefficient**. Casting effect coefficient. 0 means no effect, 1.0 means + output the curve on the target form. Use other values for linear + interpolation or linear extrapolation. The default value is 1.0. + +Parameters +---------- + +This node has the following parameter: + +* **Target form**. The available forms are: + + * **Plane** is defined by **Center** (a point on the plane) and **Direction** (plane normal vector direction). + * **Sphere** is defined by **Center** of the sphere and **Radius**. + * **Cylinder** is defined by **Center** (a point on cylinder's axis), + **Direction** (directing vector of the cylinder's axis) and **Radius** of + the cylinder. + +Outputs +------- + +This node has the following output: + +* **Curve**. The casted curve. + +Example of usage +---------------- + +A line and the same line casted onto the unit sphere: + +.. image:: https://user-images.githubusercontent.com/284644/77565225-ba46f500-6ee5-11ea-95ea-1baa8555d024.png + diff --git a/docs/nodes/curve/circle.rst b/docs/nodes/curve/circle.rst new file mode 100644 index 0000000000..18e613867f --- /dev/null +++ b/docs/nodes/curve/circle.rst @@ -0,0 +1,51 @@ +Circle (Curve) +============== + +Funcitonality +------------- + +This node generates a Curve object, which represents a circle, or an arc of the circle. + +Specifics of curve parametrization: the T parameter is proportional to curve +length, and equals to the angle along the circle arc. + +Curve domain: defined in node's inputs, by default from 0 to 2*pi. + +Behavior when trying to evaluate curve outside of it's boundaries: returns +corresponding point on the circle. + +Inputs +------ + +This node has the following inputs: + +* **Center**. A matrix defining the location of the circle. This may be used to + move, scale or rotate the curve. By default, the center of matrix is at the + origin, and the circle is laying in the XOY plane. +* **Radius**. Circle radius. The default value is 1.0. +* **T Min**. Minimum value of the curve parameter. The default value is 0.0. +* **T Max**. Maximum value of the curve parameter. The default value is 2*pi. + +Parameters +---------- + +This node does not have parameters. + +Outputs +------- + +This node has one output: + +* **Curve**. The circle (or arc) curve. + +Examples of usage +----------------- + +Simple use: + +.. image:: https://user-images.githubusercontent.com/284644/77347101-27794f80-6d59-11ea-9bcd-182c918222cc.png + +Use together with Extrude node to create a surafce: + +.. image:: https://user-images.githubusercontent.com/284644/77347798-344a7300-6d5a-11ea-88c2-e25e12be74b9.png + diff --git a/docs/nodes/curve/concat_curves.rst b/docs/nodes/curve/concat_curves.rst new file mode 100644 index 0000000000..e592360c85 --- /dev/null +++ b/docs/nodes/curve/concat_curves.rst @@ -0,0 +1,41 @@ +Concatenate Curves +================== + +Functionality +------------- + +This node composes one Curve object from several Curve objects, by "glueing" +their ends. It assumes that end points of the curves being concatenated are +already coinciding. You can make the node check this fact additionaly. + +Curve domain: summed up from domains of curves being concatenated. + +Inputs +------ + +This node has the following input: + +* **Curves**. A list of curves to be concatenated. This input is mandatory. + +Parameters +---------- + +This node has the following parameters: + +* **Check coincidence**. If enabled, then the node will check that the end points of curves being concatenated do actually coincide (within threshold). If they do not, the node will give an error (become red), and the processing will stop. +* **Max distance**. Maximum distance between end points of the curves, which is allowable to decide that they actually coincide. The default value is 0.001. This parameter is only available if **Check coincidence** parameter is enabled. + +Outputs +------- + +This node has the following output: + +* **Curve**. The resulting concatenated curve. + +Example of usage +---------------- + +Make single curve from two segments of line and an arc: + +.. image:: https://user-images.githubusercontent.com/284644/77555651-50c0e980-6ed9-11ea-915f-8eb490c5904f.png + diff --git a/docs/nodes/curve/cubic_spline.rst b/docs/nodes/curve/cubic_spline.rst new file mode 100644 index 0000000000..f6cfda4029 --- /dev/null +++ b/docs/nodes/curve/cubic_spline.rst @@ -0,0 +1,64 @@ +Cubic Spline +============ + +Functionality +------------- + +This node generates a cubic spline interpolating curve, i.e. a 3rd degree curve +which goes through the specified vertices. The curve can be closed or not. + +Curve domain / parameterization specifics: depends on **Metrics** parameter. +Curve domain will be equal to sum of distanes between the control points (in +the order they are provided) in the specified metric. For example, if +**Metric** is set to **Points**, then curve domain will be from 0 to number of +points. + +Inputs +------ + +This node has the following input: + +* **Vertices**. The points through which it is required to plot the curve. This input is mandatory. + +Parameters +---------- + +This node has the following parameters: + +* **Cyclic**. If checked, the node will generate a cyclic (closed) curve. Unchecked by default. +* **Metric**. This parameter is available in the N panel only. This defines the metric used to calculate curve's T parameter values corresponding to specified curve points. The available values are: + + * Manhattan + * Euclidian + * Points (just number of points from the beginning) + * Chebyshev. + +The default value is Euclidian. + +Outputs +------- + +This node has the following output: + +* **Curve**. The generated curve. + +Examples of usage +----------------- + +Smooth curve through some random points: + +.. image:: https://user-images.githubusercontent.com/284644/77845087-6f6afd00-71c5-11ea-9062-77c195a512ce.png + +The same with **Cyclic** checked: + +.. image:: https://user-images.githubusercontent.com/284644/77845088-709c2a00-71c5-11ea-85a9-c090776c6c96.png + +These examples had Metric set to Euclidian (default). Since **Eval Curve** node +generates evenly-distributed values of the T parameter, the number of points at +each segment is proportional to the distance between points. The next example +is with Metric set to Points: + +.. image:: https://user-images.githubusercontent.com/284644/77845090-7134c080-71c5-11ea-8c6b-10d04a95cf87.png + +In this case, number of points at each segment is the same. + diff --git a/docs/nodes/curve/curvature.rst b/docs/nodes/curve/curvature.rst new file mode 100644 index 0000000000..0f4827b90e --- /dev/null +++ b/docs/nodes/curve/curvature.rst @@ -0,0 +1,41 @@ +Curve Curvature +=============== + +Functionality +------------- + +This node calculates curve's curvature_ at certain value of the curve's T +parameter. It can also calculate curve's curvature radius and the center of +osculating circle. + +.. _curvature: https://en.wikipedia.org/wiki/Curvature#Space_curves + +Inputs +------ + +This node has the following inputs: + +* **Curve**. The curve to measure curvature for. This input is mandatory. +* **T**. The value of curve's T parameter to measure curvature at. The default value is 0.5. + +Parameters +---------- + +This node does not have parameters. + +Outputs +------- + +This node has the following outputs: + +* **Curvature**. Curvature value. +* **Radius**. Curvature radius value. +* **Center**. This contains the center of osculating circle as well as circle's orientation. + +Example of usage +---------------- + +Calculate and display curvature at several points of some random curve: + +.. image:: https://user-images.githubusercontent.com/284644/78502370-470d7080-777a-11ea-9ee7-648946c89ab5.png + diff --git a/docs/nodes/curve/curve_formula.rst b/docs/nodes/curve/curve_formula.rst new file mode 100644 index 0000000000..8680df91b2 --- /dev/null +++ b/docs/nodes/curve/curve_formula.rst @@ -0,0 +1,95 @@ +Curve Formula +============= + +Functionality +------------- + +This node generates a curve, defined by some user-provided formula. + +The formula should map the curve's T parameters into one of supported 3D coordinate systems: (X, Y, Z), (Rho, Phi, Z) or (Rho, Phi, Theta). + +It is possible to use additional parameters in the formula, they will become inputs of the node. + +Curve domain / parametrization specifics: defined by node settings. + +Expression syntax +----------------- + +Syntax being used for formulas is standard Python's syntax for expressions. +For exact syntax definition, please refer to https://docs.python.org/3/reference/expressions.html. + +In short, you can use usual mathematical operations (`+`, `-`, `*`, `/`, `**` for power), numbers, variables, parenthesis, and function call, such as `sin(x)`. + +One difference with Python's syntax is that you can call only restricted number of Python's functions. Allowed are: + +- Functions from math module: + - acos, acosh, asin, asinh, atan, atan2, + atanh, ceil, copysign, cos, cosh, degrees, + erf, erfc, exp, expm1, fabs, factorial, floor, + fmod, frexp, fsum, gamma, hypot, isfinite, isinf, + isnan, ldexp, lgamma, log, log10, log1p, log2, modf, + pow, radians, sin, sinh, sqrt, tan, tanh, trunc; +- Constants from math module: pi, e; +- Additional functions: abs, sign; +- From mathutlis module: Vector, Matrix; +- Python type conversions: tuple, list, dict. + +This restriction is for security reasons. However, Python's ecosystem does not guarantee that noone can call some unsafe operations by using some sort of language-level hacks. So, please be warned that usage of this node with JSON definition obtained from unknown or untrusted source can potentially harm your system or data. + +Examples of valid expressions are: + +* 1.0 +* x +* x+1 +* 0.75*X + 0.25*Y +* R * sin(phi) + +Inputs +------ + +This node has the following inputs: + +* **TMin**. Minimum value of the curve's T parameter (corresponding to the beginning of the curve). The default value is 0.0. +* **TMax**. Maximum value of the curve's T parameter (corresponding to the end of the curve). The default value is 2*pi. + +Each variable used in formulas, except for `t`, also becomes an additional input. + +Parameters +---------- + +This node has the following parameters: + +* **Formula 1**, **Formula 2**, **Formula 3**. Formulas for 3 components + defining curve points in the used coordinate system. Default values define + simple helix curve in the carthesian coordinates. +* **Output**. This defined the coordinate system being used, and thus it + defines the exact meaing of formula parameters. The available modes are: + + * **Carthesian**. Three formulas will define correspondingly X, Y and Z coordinates. + * **Cylindrical**. Three formulas will define correspondingly Rho, Phi and Z coordinates. + * **Spherical**. Three formulas will define correspondingly Rho, Phi and Theta coordinates. + + The default mode is **Carthesian**. + +Outputs +------- + +This node has the following output: + +* **Curve**. The generated curve. + +Examples of usage +----------------- + +The default example - a helix: + +.. image:: https://user-images.githubusercontent.com/284644/77849062-473dc700-71e2-11ea-9722-c07fc32fda2c.png + +Another example - Viviani's curve (an intersection of a sphere with a cylinder): + +.. image:: https://user-images.githubusercontent.com/284644/77351539-bf7a3780-6d5f-11ea-8070-6ff58cdd7143.png + +A spiral in spherical coordinates: + +.. image:: https://user-images.githubusercontent.com/284644/77849156-f8446180-71e2-11ea-9b46-fc2a4db63f29.png + diff --git a/docs/nodes/curve/curve_frame.rst b/docs/nodes/curve/curve_frame.rst new file mode 100644 index 0000000000..8cf4b0d059 --- /dev/null +++ b/docs/nodes/curve/curve_frame.rst @@ -0,0 +1,55 @@ +Curve Frame +=========== + +Functionaltiy +------------- + +This node calculates a reference frame of a curve (also known as Frenet_ frame) +for the given value of curve's T parameter. Basically, the node allowes one to +place some object at the curve, by aligning the object with curve's "natural" +orientation. + +.. _Frenet: https://en.wikipedia.org/wiki/Frenet%E2%80%93Serret_formulas + +Note that Frenet frame of the curve rotates along curve's tangent according to +curve's own torsion. Thus, if you place something by this frame, the result can +be somewhat twisted. If you want to minimize the twist, you may wish to use +**Zero-Twist Frame** node. + +Inputs +------ + +This node has the following inputs: + +* **Curve**. The curve to calculate frame for. This input is mandatory. +* **T**. The value of curve's T parameter. The default value is 0.5. + +Parameters +---------- + +This node has the following parameter: + +* **Join**. If checked, the node will output the single list of matrices, + joined from any number of input curves provided. Otherwise, the node will + output a separate list of matrices for each input curve. Checked by default. + +Outputs +------- + +This node has the following outputs: + +* **Matrix**. The matrix defining the Frenet frame for the curve at the specified value of T parameter. The location component of the matrix is the point of the curve. Z axis of the matrix points along curve's tangent. +* **Normal**. The direction of curve's main normal at the specified value of T parameter. +* **Binormal**. The direction of curve's binormal at the specified value of T parameter. + +Examples of usage +----------------- + +Visualize curve's frame at some points: + +.. image:: https://user-images.githubusercontent.com/284644/78504334-eb48e480-7785-11ea-81cb-6987e67830b0.png + +Use these frames to put cubes along the curve, aligning them along curve's natural orientation: + +.. image:: https://user-images.githubusercontent.com/284644/78504337-ed12a800-7785-11ea-9a6c-1427ced45d55.png + diff --git a/docs/nodes/curve/curve_index.rst b/docs/nodes/curve/curve_index.rst new file mode 100644 index 0000000000..0aab68e152 --- /dev/null +++ b/docs/nodes/curve/curve_index.rst @@ -0,0 +1,32 @@ +******* +Curves +******* + +.. toctree:: + :maxdepth: 2 + + line + circle + polyline + cubic_spline + fillet_polyline + curve_formula + apply_field_to_curve + concat_curves + curve_length + length_parameter + curvature + torsion + curve_frame + zero_twist_frame + endpoints + iso_uv_curve + surface_boundary + curve_on_surface + curve_lerp + cast_curve + curve_range + flip_curve + curve_segment + eval_curve + diff --git a/docs/nodes/curve/curve_length.rst b/docs/nodes/curve/curve_length.rst new file mode 100644 index 0000000000..417d78d34b --- /dev/null +++ b/docs/nodes/curve/curve_length.rst @@ -0,0 +1,68 @@ +Curve Length +============ + +Functionality +------------- + +This node calculates the length of the curve. It also can calculate the length +of certain segment of the curve within specified range of curve's T parameter. + +The curve's length is calculated numerically, by subdividing the curve in many +straight segments and summing their lengths. The more segments you subdivide +the curve in, the more precise the length will be, but the more time it will +take to calculate. So the node gives you control on the number of subdivisions. + +Inputs +------ + +This node has the following inputs: + +* **Curve**. The curve being measured. This input is mandatory. +* **TMin**. The minimum value of the T parameter of the measured segment. If + **T Mode** parameter is set to **Relative**, then reasonable values for this + input are within `[0 .. 1]` range. Otherwise, reasonable values are defined + by the curve domain. The default value is 0.0. +* **TMax**. The maximum value of the T parameter of the measured segment. If + **T Mode** parameter is set to **Relative**, then reasonable values for this + input are within `[0 .. 1]` range. Otherwise, reasonable values are defined + by the curve domain. The default value is 1.0. +* **Resolution**. The number of segments to subdivide the curve in to calculate + the length. The bigger the value, the more precise the calculation will be, + but the more time it will take. The default value is 50. + +Parameters +---------- + +This node has the following parameter: + +* **T mode**. This defines units in which **TMin**, **TMax** parameters are measured: + + * **Absolute**. The parameters will be the actual values of curve's T + parameter. To calculate the length of the whole curve, you will have to set + **TMin** and **TMax** to the ends of curve's domain. + * **Relative**. The parameters values will be rescaled, so that with **TMin** + set to 0.0 and **TMax** set to 1.0 the node will calculate the length of + the whole curve. + +Outputs +------- + +This node has the following output: + +* **Length**. The length of the curve (or it's segment). + +Examples of usage +----------------- + +The length of a unit circle is 2*pi: + +.. image:: https://user-images.githubusercontent.com/284644/77850952-6b53d500-71ef-11ea-80fe-07815a5c7e1d.png + +Calculate length of some smooth curve: + +.. image:: https://user-images.githubusercontent.com/284644/77849699-01cfc880-71e7-11ea-97b2-9229e0f9c630.png + +Take some points on the curve (with even steps in T) and calculate length from the beginning of the curve to each point: + +.. image:: https://user-images.githubusercontent.com/284644/77849701-0300f580-71e7-11ea-89a7-197f7778da71.png + diff --git a/docs/nodes/curve/curve_lerp.rst b/docs/nodes/curve/curve_lerp.rst new file mode 100644 index 0000000000..d5f09a2e62 --- /dev/null +++ b/docs/nodes/curve/curve_lerp.rst @@ -0,0 +1,39 @@ +Curve Lerp +========== + +Functionality +------------- + +This node generates a linear interpolation ("lerp") between two curves. More +precisely, it generates a curve, each point of which is calculated by linear +interpolation of two corresponding points on two other curves. + +If the coefficient value provided is outside `[0 .. 1]` range, then the node +will calculate linear extrapolation instead of interpolation. + +Inputs +------ + +This node has the following inputs: + +* **Curve1**. The first curve. This input is mandatory. +* **Curve2**. The second curve. This input is mandatory. +* **Coefficient**. The interpolation coefficient. When it equals to 0, the + resulting curve will be the same as **Curve1**. When the coefficient is 1.0, + the resulting curve will be the same as **Curve2**. The default value is 0.5, + which is something in the middle. + +Outputs +------- + +This node has the following output: + +* **Curve**. The interpolated curve. + +Example of usage +---------------- + +Several curves calculated as linear interpolation between half a circle and a straight line segment: + +.. image:: https://user-images.githubusercontent.com/284644/78507545-fdcd1900-7799-11ea-98d4-17bf8109396a.png + diff --git a/docs/nodes/curve/curve_on_surface.rst b/docs/nodes/curve/curve_on_surface.rst new file mode 100644 index 0000000000..4d23500366 --- /dev/null +++ b/docs/nodes/curve/curve_on_surface.rst @@ -0,0 +1,48 @@ +Curve on Surface +================ + +Functionality +------------- + +This node generates a (3D) curve by laying another (2D) curve into some +surface's UV space. In other words, it takes a curve, considers it as being +placed in 2D space of U and V parameters, and generates another curve by +evaluating the surface at U/V coordinates generated by the first curve. One may +say that this node draws the curve on the surface. + +Inputs +------ + +This node has the following inputs: + +* **Curve**. The curve to be drawn on the surface. This input is mandatory. +* **Surface**. The surface, on which to draw the curve. This input is mandatory. + +Parameters +---------- + +This node has the following parameter: + +* **Curve plane**. The coordinate plane, in which the input curve lies. This + defines the pair of coordinates being used. The available values are **XY**, + **YZ** and **XZ**. For example, if **XY** is selected, then Z coordinates of + the input curve's points will be ignored. The default value is **XY**. + +Outputs +------- + +This node has the following output: + +* **Curve**. The curve drawn on the surface. + +Examples of usage +----------------- + +Generate some surface and draw a circle on it: + +.. image:: https://user-images.githubusercontent.com/284644/78508319-5ce15c80-779f-11ea-92a8-48e9ea65450c.png + +It is possible to use such a curve, for example, to place cubes along it: + +.. image:: https://user-images.githubusercontent.com/284644/78508386-f6a90980-779f-11ea-9156-edd05500a0f8.png + diff --git a/docs/nodes/curve/curve_range.rst b/docs/nodes/curve/curve_range.rst new file mode 100644 index 0000000000..9883f65655 --- /dev/null +++ b/docs/nodes/curve/curve_range.rst @@ -0,0 +1,31 @@ +Curve Domain +============ + +Functionality +------------- + +This node outputs the domain of the curve, i.e. the range of values the curve's T parameter is allowed to take. + +Inputs +------ + +This node has the following input: + +* **Curve**. The curve to be measured. This input is mandatory. + +Outputs +------- + +This node has the following outputs: + +* **TMin**. The minimal allowed value of curve's T parameter. +* **TMax**. The maximum allowed value of curve's T parameter. +* **Range**. The length of curve's domain; this equals to the difference **TMax** - **TMin**. + +Example of usage +---------------- + +The domain of circle curve is from 0 to 2*pi: + +.. image:: https://user-images.githubusercontent.com/284644/78507901-792fca00-779c-11ea-90a7-e3c1cfecf39b.png + diff --git a/docs/nodes/curve/curve_segment.rst b/docs/nodes/curve/curve_segment.rst new file mode 100644 index 0000000000..774cc5ee6e --- /dev/null +++ b/docs/nodes/curve/curve_segment.rst @@ -0,0 +1,51 @@ +Curve Segment +============= + +Functionality +------------- + +This node generates a curve which is defined as a subset of the input curve +with a smaller range of allowed T parameter values. In other words, the output +curve is the same as input one, but with restricted range of T values allowed. + +Output curve domain: defined by node inputs. + +Output curve parametrization: the same as of input curve. + +Inputs +------ + +This node has the following inputs: + +* **Curve**. The curve to be cut. This input is mandatory. +* **TMin**. The value of curve's T parameter, at which the new curve should start. The default value is 0.2. +* **TMax**. The value of curve's T parameter, at which the new curve should end. The default value is 0.8. + +Parameters +---------- + +This node has the following parameter: + +* **Rescale to 0..1**. If checked, then the generated curve will have the + domain (allowed range of T parameter values) of `[0.0 .. 1.0]`. Otherwise, + the domain of generated curve will be defined by node's inputs, i.e. `[TMin + .. TMax]`. Unchecked by default. + +Outputs +------- + +This node has the following output: + +* **Segment**. The resulting curve segment. + +Examples of usage +----------------- + +Generate some random curve (green) and take a subset from it with T values from 0.2 to 0.8 (red): + +.. image:: https://user-images.githubusercontent.com/284644/78508073-99ac5400-779d-11ea-9a01-1d8824934ae4.png + +Generate several segments of the same curve with different ranges of T parameter, to generate a dashed line: + +.. image:: https://user-images.githubusercontent.com/284644/78508072-987b2700-779d-11ea-9b53-6490db257aed.png + diff --git a/docs/nodes/curve/endpoints.rst b/docs/nodes/curve/endpoints.rst new file mode 100644 index 0000000000..7e35e44020 --- /dev/null +++ b/docs/nodes/curve/endpoints.rst @@ -0,0 +1,32 @@ +Curve Endpoints +=============== + +Functionality +------------- + +This node outputs starting and ending points of the curve. + +Note that for closed curves the start and the end point will coincide. + +Inputs +------ + +This node has the following input: + +* **Curve**. The curve to calculate endpoints of. This input is mandatory. + +Outputs +------- + +This node has the following outputs: + +* **Start**. The starting point of the curve. +* **End**. The end point of the curve. + +Example of usage +---------------- + +Visualize end points of some curve: + +.. image:: https://user-images.githubusercontent.com/284644/78505629-20593500-778e-11ea-998b-53b5b87925d3.png + diff --git a/docs/nodes/curve/eval_curve.rst b/docs/nodes/curve/eval_curve.rst new file mode 100644 index 0000000000..2e6eedc155 --- /dev/null +++ b/docs/nodes/curve/eval_curve.rst @@ -0,0 +1,52 @@ +Evaluate Curve +============== + +Functionality +------------- + +This node calculates the point on the curve at a given value of curve +parameter. It can also automatically calculate a set of points at a series of +evenly distributed values of curve parameter. + +You will be using this node a lot, to visualize any curve, or to convert it to mesh. + +Inputs +------ + +This node has the following inputs: + +* **Curve**. Curve to be evaluated. This input is mandatory. +* **T**. The value of curve parameter to calculate the point on the curve for. + This input is available only when **Mode** parameter is set to **Manual**. + Sensible range values for this input corresponds to the domain of the curve + provided in the **Curve** input. +* **Samples**. Number of curve parameter values to calculate the curve points + for. This input is available only when **Mode** parameter is set to **Auto**. + The default value is 50. + +Parameters +---------- + +This node has the following parameter: + +* **Mode**: + + * **Automatic**. Calculate curve points for the set of curve parameter values which are evenly distributed within curve domain. + * **Manual**. Calculate curve point for the provided value of curve parameter. + +Outputs +------- + +This node has the following outputs: + +* **Vertices**. The calculated points on the curve. +* **Edges**. Edges between the calculated points. This output is only available when the **Mode** parameter is set to **Auto**. +* **Tangents**. Curve tangent vectors for each value of curve parameter. + +Examples of usage +----------------- + +This node used for line visualization: + +.. image:: https://user-images.githubusercontent.com/284644/77443595-fc503800-6e0c-11ea-9340-6473785a6a51.png + diff --git a/docs/nodes/curve/fillet_polyline.rst b/docs/nodes/curve/fillet_polyline.rst new file mode 100644 index 0000000000..aa53ec520f --- /dev/null +++ b/docs/nodes/curve/fillet_polyline.rst @@ -0,0 +1,72 @@ +Fillet Polyline +=============== + +Functionality +------------- + +This node generates a filleted polyline curve, i.e. a curve consisting of +segments of straight lines and circular arcs to connect them. The curve may be +closed or not. + +The curve is generated by plotting a polyline thorugh the points, and then +rounding all corners by replacing them with circular arc. + +Curve domain / parametrization specifics: depends on **Even domains** +parameter. If checked, then each straight segment, as well as each arc, will be +given a domain of length 1.0; so the total domain of the curve will be equal to +number of all segments - straight and circular. If not checked, then each +straight segment will be given a domain equal to segment's lengths, and each +arc will be given a domain equal to it's angle. + +Inputs +------ + +This node has the following input: + +* **Vertices**. The points controlling the curve; the polyline will go through + these points, and then will be filleted. This input is mandatory. +* **Radius**. Fillet arc radius. This input can be provided with separate + radius for each control point. The default value is 0.2. + +Parameters +---------- + +This node has the following parameters: + +* **Concatenate**. If checked, then all straight and arc segments will be + concatenated into a single curve. Otherwise, each segment will be output as a + separate curve object. Checked by default. +* **Even domains**. If checked, give each segment a domain of length 1. This + parameter is only available if **Concatenate** parameter is checked. + Unchecked by default. +* **Cyclic**. If checked, the node will generate a cyclic (closed) curve. Unchecked by default. + +Outputs +------- + +This node has the following outputs: + +* **Curve**. The generated curve (or curves). +* **Centers**. Centers of circles used to make fillet arcs. These are matrices, + since this output provides not only centers, but also orientation of the + circles. + +Examples of usage +----------------- + +Filleted polyline through some random points: + +.. image:: https://user-images.githubusercontent.com/284644/77845646-f4581580-71c9-11ea-9d16-b0ac95046441.png + +The same but with **Cyclic** parameter checked: + +.. image:: https://user-images.githubusercontent.com/284644/77845647-f5894280-71c9-11ea-95eb-b630c135dd7f.png + +The same but with **Even domains** checked: + +.. image:: https://user-images.githubusercontent.com/284644/77845648-f621d900-71c9-11ea-8000-31c2ddbd20ac.png + +In the last case, the number of points at each straight segment and arc segment +is the same, since **Eval Curve** node generates evenly-spaced values of the T +parameter. + diff --git a/docs/nodes/curve/flip_curve.rst b/docs/nodes/curve/flip_curve.rst new file mode 100644 index 0000000000..148310f7d9 --- /dev/null +++ b/docs/nodes/curve/flip_curve.rst @@ -0,0 +1,22 @@ +Flip Curve +========== + +Functionality +------------- + +This node generates a curve by inverting the direciton of parametrization of another curve; in other words, it generates the curve identical to provided one, but with the opposite direction. + +Inputs +------ + +This node has the following input: + +* **Curve**. The curve to be flipped. This input is mandatory. + +Outputs +------- + +This node has the following output: + +* **Curve**. The flipped curve. + diff --git a/docs/nodes/curve/iso_uv_curve.rst b/docs/nodes/curve/iso_uv_curve.rst new file mode 100644 index 0000000000..86a10793bb --- /dev/null +++ b/docs/nodes/curve/iso_uv_curve.rst @@ -0,0 +1,37 @@ +Iso U/V Curve +============= + +Functionality +------------- + +This node generates a curve on the surface, which is defined by setting either +U or V surface parameter to some fixed value and letting the other parameter +slide along it's domain. + +Inputs +------ + +This node has the following inputs: + +* **Surface**. The surface to generate curves on. This input is mandatory. +* **Value**. The value of U or V surface parameter to generate curve at. The default value is 0.5. + +Outputs +------- + +This node has the following outputs: + +* **UCurve**. The curve obtained by setting the U parameter of the surface to + **Value** and letting V slide along it's domain. So, this curve is elongated + along surface's V direction. +* **VCurve**. The curve obtained by setting the V parameter of the surface to + **Value** and letting U slide along it's domain. So, this curve is elongated + along surface's U direction. + +Example of usage +---------------- + +Generate some surface and then draw curves along it's V direction: + +.. image:: https://user-images.githubusercontent.com/284644/78507210-3a981080-7798-11ea-84b8-d4e6e7d66803.png + diff --git a/docs/nodes/curve/length_parameter.rst b/docs/nodes/curve/length_parameter.rst new file mode 100644 index 0000000000..6676465efe --- /dev/null +++ b/docs/nodes/curve/length_parameter.rst @@ -0,0 +1,88 @@ +Curve Length Parameter +====================== + +Functionality +------------- + +It worth reminding that a Curve object is defined as a function from some set +of T values into some points in 3D space; and, since the function is more or +less arbitrary, the change of T parameter is not always proportional to length +of the path along the curve - in fact, it is rarely proportional. For many +curves, small changes of T parameter can move a point a long way along the +curve in one parts of the curve, and very small way in other parts. + +This node calculates the point on the curve by it's "length parameter" (also +called "natural parameter"); i.e., for given number L, it calculates the point +P on the curve such that the length of curve segment from beginning to P equals +to L. + +The node can also calculate many such points at once for evenly spaced values +of L. This way, one can use this node to split the curve into evenly-sized +segments. + +The curve's length is calculated numerically, by subdividing the curve in many +straight segments and summing their lengths. The more segments you subdivide +the curve in, the more precise the length will be, but the more time it will +take to calculate. So the node gives you control on the number of subdivisions. + + +Inputs +------ + +This node has the following inputs: + +* **Curve**. The curve being measured. This input is mandatory. +* **Resolution**. The number of segments to subdivide the curve in to calculate + the length. The bigger the value, the more precise the calculation will be, + but the more time it will take. The default value is 50. +* **Length**. The value of length parameter to evaluate the curve at. The + default value is 0.5. This input is available only if **Mode** parameter is + set to **Manual**. +* **Samples**. Number of length parameter values to calculate the curve points + at. The node will calculate evenly spaced values of the length parameter from + zero to the full length of the curve. The default value is 50. This input is + only available if **Mode** parameter is set to **Auto**. + +Parameters +---------- + +This node has the following parameters: + +* **Mode**. This defines how the values of length parameter will be provided. + The available options are: + + * **Auto**. Values of length parameters will be calculated as evenly spaced + values from zero to the full length of the curve. The number of the values + is controlled by the **Samples** input. + * **Manual**. Values of length parameter will be provided in the **Length** input. + + The default value is **Auto**. + +* **Interpolation mode**. This defines the interpolation method used for + calculating of points inside the segments in which the curve is split + according to **Resolution** parameters. The available values are **Cubic** + and **Linear**. Cubic methods gives more precision, but takes more time for + calculations. The default value is **Cubic**. This parameter is available in + the N panel only. + +Outputs +------- + +This node has the following outputs: + +* **T**. Values of curve's T parameter which correspond to the specified values + of length parameter. +* **Vertices**. Calculated points on the curve which correspond to the + specified values of length parameter. + +Example of usage +---------------- + +Two exemplars of Archimedian spiral: + +.. image:: https://user-images.githubusercontent.com/284644/77854328-14f09180-7203-11ea-9192-028621be3d95.png + +The one on the left is drawn with points according to evenly-spaced values of T +parameter; the one of the right is drawn with points spread with equal length +of the path between them. + diff --git a/docs/nodes/curve/line.rst b/docs/nodes/curve/line.rst new file mode 100644 index 0000000000..6955ddaa82 --- /dev/null +++ b/docs/nodes/curve/line.rst @@ -0,0 +1,52 @@ +Line (Curve) +============ + +Functionality +------------- + +This node generates a Curve object, which is a segment of straight line between two points. + +Curve domain: defined in node's inputs, by default from 0 to 1. + +Behavior when trying to evaluate curve outside of it's boundaries: returns +corresponding point on the line. + +Inputs +------ + +This node has the following inputs: + +* **Point1**. The first point on the line (the beginning of the curve, if **UMin** is set to 0). +* **Point2**. The second point on the line (the end of the curve, if **UMax is set to 1). This input is available only if **Mode** parameter is set to **Two points**. +* **Direction**. Directing vector of the line. This input is available only when **Mode** parameter is set to **Point and direction**. +* **UMin**. Minimum value of curve parameter. The default value is 0.0. +* **UMax**. Maximum value of curve parameter. The default value is 1.0. + +Parameters +---------- + +This node has the following parameter: + +* **Mode**: + + * **Two points**: line is defined by two points on the line. + * **Point and direction**: line is defined by one point on the line and the directing vector. + +Outputs +------- + +This node has the following output: + +* **Curve**. The line curve. + +Examples of usage +----------------- + +Trivial example: + +.. image:: https://user-images.githubusercontent.com/284644/77443595-fc503800-6e0c-11ea-9340-6473785a6a51.png + +Generate several lines, and bend them according to noise vector field: + +.. image:: https://user-images.githubusercontent.com/284644/77443601-fd816500-6e0c-11ea-9ed2-0516eba95951.png + diff --git a/docs/nodes/curve/polyline.rst b/docs/nodes/curve/polyline.rst new file mode 100644 index 0000000000..66935b8ca2 --- /dev/null +++ b/docs/nodes/curve/polyline.rst @@ -0,0 +1,65 @@ +Polyline +======== + +Functionality +------------- + +This node geneates a polyline (polygonal chain), i.e. a curve consisting of +segments of straight lines between the specified points. The polyline may be +closed or not. + +Curve domain / parameterization specifics: depends on **Metrics** parameter. +Curve domain will be equal to sum of distanes between the control points (in +the order they are provided) in the specified metric. For example, if +**Metric** is set to **Points**, then curve domain will be from 0 to number of +points. + +Inputs +------ + +This node has the following input: + +* **Vertices**. The points through which it is required to plot the curve. This input is mandatory. + +Parameters +---------- + +This node has the following parameters: + +* **Cyclic**. If checked, the node will generate a cyclic (closed) curve. Unchecked by default. +* **Metric**. This parameter is available in the N panel only. This defines the metric used to calculate curve's T parameter values corresponding to specified curve points. The available values are: + + * Manhattan + * Euclidian + * Points (just number of points from the beginning) + * Chebyshev. + +The default value is Euclidian. + +Outputs +------- + +This node has the following output: + +* **Curve**. The generated curve. + +Examples of usage +----------------- + +Polyline through some random points: + +.. image:: https://user-images.githubusercontent.com/284644/77845220-8bbb6980-71c6-11ea-96f2-5e25bd8c36f1.png + +The same with **Cyclic** checked: + +.. image:: https://user-images.githubusercontent.com/284644/77845221-8cec9680-71c6-11ea-8f5a-1558b32a2e8b.png + +These examples had Metric set to Euclidian (default). Since **Eval Curve** node +generates evenly-distributed values of the T parameter, the number of points at +each segment is proportional to the distance between points. The next example +is with Metric set to Points: + +.. image:: https://user-images.githubusercontent.com/284644/77845222-8d852d00-71c6-11ea-986c-0d7bbde0814d.png + +In this case, number of points at each segment is the same. + diff --git a/docs/nodes/curve/surface_boundary.rst b/docs/nodes/curve/surface_boundary.rst new file mode 100644 index 0000000000..bec14c8067 --- /dev/null +++ b/docs/nodes/curve/surface_boundary.rst @@ -0,0 +1,51 @@ +Surface Boundary +================ + +Functionality +------------- + +This node outputs the curve (or curves) which represent the boundaries of some surface. The supported types of surfaces are: + +* Plain (plane-like); for such surfaces, the boundary is one closed curve (in + many cases it will have four non-smooth points). +* Closed in U or in V direction (cylinder-like). For such surface, the boundary + is represented by two closed curves at two open sides of the surface. + +If the surface is closed in both U and V direction (torus-like), then it will not have any boundary. + +Inputs +------ + +This node has the following input: + +* **Surface**. The surface to calculate boundary for. This input is mandatory. + +Parameters +---------- + +This node has the following parameter: + +* **Cyclic**. This defines whether the surface is closed in some directions. The available options are: + + * **Plain**. The surface is not closed in any direction, so it has single closed curve as a boundary. + * **U Cyclic**. The surface is closed along the U direction. It has two closed curves as boundary. + * **V Cyclic**. The surface is closed along the V direction. + +Outputs +------- + +This node has the following output: + +* **Boundary**. Curve or curves representing surface's boundary. + +Examples of usage +----------------- + +Visualize the boundary of some random plane-like surface: + +.. image:: https://user-images.githubusercontent.com/284644/78506070-b8581e00-7790-11ea-9af1-2c3c84264dc8.png + +Visualize the boundary of cylinder-like surface: + +.. image:: https://user-images.githubusercontent.com/284644/78506074-b9894b00-7790-11ea-9b5b-714a9fc79927.png + diff --git a/docs/nodes/curve/torsion.rst b/docs/nodes/curve/torsion.rst new file mode 100644 index 0000000000..7e1688d4c3 --- /dev/null +++ b/docs/nodes/curve/torsion.rst @@ -0,0 +1,40 @@ +Curve Torsion +============= + +Functionality +------------- + +This node calculates the torsion_ value of a curve at the certain value of curve's T paramter. + +.. _torsion: https://en.wikipedia.org/wiki/Torsion_of_a_curve + +Inputs +------ + +This node has the following inputs: + +* **Curve**. The curve to measure torsion for. This input is mandatory. +* **T**. The value of curve's T parameter to measure torsion at. The default value is 0.5. + +Parameters +---------- + +This node does not have parameters. + +Outputs +------- + +This node has the following output: + +* **Torsion**. The torsion value. + +Example of usage +---------------- + +Calculate torsion value at several points of some random curve: + +.. image:: https://user-images.githubusercontent.com/284644/78502538-12e67f80-777b-11ea-8ba6-d4d1d6360ce2.png + +Note that calculating torsion at end points of a curve, or at some other +signular points of the curve may give unusable results. + diff --git a/docs/nodes/curve/zero_twist_frame.rst b/docs/nodes/curve/zero_twist_frame.rst new file mode 100644 index 0000000000..b597363032 --- /dev/null +++ b/docs/nodes/curve/zero_twist_frame.rst @@ -0,0 +1,63 @@ +Curve Zero-Twist Frame +====================== + +Functionality +------------- + +This node calculates a reference frame along the curve, which has minimal +rotation around curve's tangent while moving along the curve. Such a frame is +calculated by rotating curve's Frenet_ frame around curve's tangent (in the +negative direction). More precisely, an indefinite integral of curve's torsion_ +is calculated to get the total rotation of Frenet frame around the tangent +while moving from curve's starting point to any other point. + +The indefinite integral is calculated numerically, by subdividing the domain of +the curve in many pieces. The more pieces are used, the more precise the +calculation will be, but the more time it will take. So, this node allows one +to specify the number of curve subdivisions to be used. + +.. _Frenet: https://en.wikipedia.org/wiki/Frenet%E2%80%93Serret_formulas +.. _torsion: https://en.wikipedia.org/wiki/Torsion_of_a_curve + +Inputs +------ + +This node has the following inputs: + +* **Curve**. The curve to calculate frame for. This input is mandatory. +* **Resolution**. The number of segments to subdivide the curve in to calculate the torsion integral. The bigger the value is, the more precise calculation will be, but the more time it will take. The default value is 50. +* **T**. The value of curve's T parameter to calculate the frame at. The default value is 0.5. + +Parameters +---------- + +This node has the following parameter: + +* **Join**. If checked, the node will output the single list of matrices, + joined from any number of input curves provided. Otherwise, the node will + output a separate list of matrices for each input curve. Checked by default. + +Outputs +------- + +This node has the following outputs: + +* **CumulativeTorsion**. Total angle of curve's Frenet frame rotation around + curve's tangent, when moving from curve's starting point to the specified T + parameter value — i.e., the indefinite integral of curve's torsion. The angle + is expressed in radians. +* **Matrix**. Curve's zero-twist frame at specified value of the T parameter. + The location component of the matrix is the point of the curve. Z axis of the + matrix points along curve's tangent. + +Example of usage +---------------- + +Use zero-twist frames to put cubes along the curve: + +.. image:: https://user-images.githubusercontent.com/284644/78504364-20edcd80-7786-11ea-831e-80db9be81e2b.png + +Compare that to the use of curve's Frenet frames: + +.. image:: https://user-images.githubusercontent.com/284644/78504337-ed12a800-7785-11ea-9a6c-1427ced45d55.png + diff --git a/docs/nodes/field/attractor_field.rst b/docs/nodes/field/attractor_field.rst new file mode 100644 index 0000000000..03d71130be --- /dev/null +++ b/docs/nodes/field/attractor_field.rst @@ -0,0 +1,105 @@ +Attractor Field +=============== + +Functionality +------------- + +This node generates a Vector Field and a Scalar Field, which are calculated as +force attracting points towards some objects. Several types of attractor +objects are supported. Several physics-like falloff laws are supported. + +The scalar field generated equals to the norm of the vector field - i.e., the amplitude of the attracting force. + +Inputs +------ + +This node has the following inputs: + +* **Center**. The exact meaning of this input depends on the **Attractor type** parameter: + + * If attractor type is **Point**, then this is the attracting point itself; + * if attractor type is **Line**, then this is the point lying on the attracting line; + * if attractor type is **Plane**, then this is the point lying on the attracting plane. + * if attractor type is **Mesh**, then this is the set of mesh vertices. + + In **Point** mode, it is possible to provide several attracting points. The default value is `(0, 0, 0)` (origin). + +* **Direction**. The exact meaning of this input depends on the **Attractor type** parameter: + + * if attractor type is **Line**, then this is the directing vector of the line; + * if attractor type is **Plane**, then this is the normal vector of the plane. + * with other attractor types, this input is not available. + + The default value is `(0, 0, 1)` (Z axis). + +* **Faces**. The faces of the attracting mesh. This input is available only + when **Attractor type** parameter is set to **Mesh**. +* **Amplitude**. The attracting force amplitude. The default value is 0.5. +* **Coefficient**. The coefficient used in the attracting force falloff + calculation formula. This input is only available when the **Falloff type** + is set to **Inverse exponent** or **Gauss**. The default value is 0.5. + +Parameters +---------- + +This node has the following parameters: + +* **Attractor type**. The type of attractor object being used. The available values are: + + * **Point** is defined in the corresponding input. + * **Line** is defined by a point and the directing vector. + * **Plane** is defined by a point and the normal vector. + * **Mesh** is defined by vertices and faces. + + The default value is **Point**. + +* **Points mode**. This determines how the distance is calculated when multiple attraction centers are provided. This parameter is only available when the **Attractor type** parameter is set to **Point**. The available values are: + + * **Average**. Calculate the average of the attracting forces towards the + provided centers. This mode is used in physics. This option is the default + one. + * **Nearest**. Use only the force of the attraction towards the nearest attraction center. + +* **Signed**. This parameter is available only when **Attractor type** + parameter is set to **Mesh**. If checked, then the resulting scalar field + will be signed: it will have positive values at the one side of the mesh + (into which the mesh normals are pointing), and negative values at the other + side of the mesh. Otherwise, the scalar field will have positive values + everywhere. This flag does not affect the calculated vector field. Unchecked + by default. +* **Falloff type**. The force falloff type to be used. The available values are: + + * **None - R**. Do not use falloff: the force amplitude is proportional to the distance from the attractor object (grows with the distance). + * **Inverse - 1/R**. Calculate the force value as 1/R. + * **Inverse square - 1/R^2**. Calculate the force value as `1/R^2`. This law is most commonly used in physics. + * **Inverse cubic - 1/R^3**. Calculate the force value as `1/R^3`. + * **Inverse exponent - Exp(-R)**. Calculate the force value as `Exp(- K*R)`. + * **Gauss - Exp(-R^2/2)**. Calculate the force value as `Exp(- K * R^2/2)`. + + The default option is **None**. +* **Clamp**. If checked, then the amplitude of attracting force vector will be + restricted with the distance to attractor object. Unchecked by default. + +Outputs +------- + +This node has the following outputs: + +* **VField**. Vector field of the attracting force. +* **SField**. Scalar field of the attracting force (amplitude of the attracting force). + +Examples of usage +----------------- + +The attraction field of one point visualized: + +.. image:: https://user-images.githubusercontent.com/284644/79471192-b8bba900-801b-11ea-829e-2b003d9000da.png + +The attraction field of Z axis visualized: + +.. image:: https://user-images.githubusercontent.com/284644/79471186-b78a7c00-801b-11ea-8926-3cc14b792220.png + +The attraction field of a point applied to several planes: + +.. image:: https://user-images.githubusercontent.com/284644/79471194-b9543f80-801b-11ea-89dc-3b631659f1b2.png + diff --git a/docs/nodes/field/bend_along_surface.rst b/docs/nodes/field/bend_along_surface.rst new file mode 100644 index 0000000000..022adfdb07 --- /dev/null +++ b/docs/nodes/field/bend_along_surface.rst @@ -0,0 +1,60 @@ +Bend Along Surface Field +======================== + +Functionality +------------- + +This node generates a Vector Field, which bends some part of 3D space along the provided Surface object. + +Inputs +------ + +This node has the following inputs: + +* **Surface**. The surface to bend the space along. This input is mandatory. +* **Src U Min**, **Src U Max**. Minimum and maximum value of the first of + orientation coordinates, which define the part of space to be bent. For + example, if the **Object vertical axis** parameter is set to **Z**, then these + are minimum and maximum values of X coordinates. Default values are -1 and 1. +* **Src V Min**, **Src V Max**. Minimum and maximum value of the second of + orientation coordinates, which define the part of space to be bent. For + example, if the **Object vertical axis** parameter is set to **Z**, then these + are minimum and maximum values of Y coordinates. Default values are -1 and 1. + +The field bends the part of space, which is between **Src U Min** and **Src U +Max** by one axis, and between **Src V Min** and **Src V Max** by another axis. +For example, with default settings, the source part of space is the space +between X = -1, X = 1, Y = -1, Y = 1. The behaviour of the field outside of +these bounds is not guaranteed. + +Parameters +---------- + +This node has the following parameters: + +* **Object vertical axis**. This defines which axis of the source space should + be mapped to the normal axis of the surface. The available values are X, Y + and Z. The default value is Z. This means that XOY plane will be mapped onto + the surface. +* **Auto scale**. If checked, scale the source space along the vertical axis, + trying to match the scale coefficient for two other axes. Otherwise, the + space will not be scaled along the vertical axis. Unchecked by default. +* **Flip surface**. This parameter is only available in the node's N panel. If + checked, then the surface will be considered as flipped (turned upside down), + so the vector field will also turn the space upside down. Unchecked by + default. + +Outputs +------- + +This node has the following output: + +* **Field**. The generated bending vector field. + +Example of usage +---------------- + +Generate a rectangular grid of cubes, and bend it along formula-specified surface: + +.. image:: https://user-images.githubusercontent.com/284644/79602628-42df3c80-8104-11ea-80c3-09be659d54f8.png + diff --git a/docs/nodes/field/compose_vector_field.rst b/docs/nodes/field/compose_vector_field.rst new file mode 100644 index 0000000000..66001b1ce0 --- /dev/null +++ b/docs/nodes/field/compose_vector_field.rst @@ -0,0 +1,42 @@ +Compose Vector Field +==================== + +Functionality +------------- + +This node generates a Vector Field by composing it from three Scalar Fields which represent different coordinates of the vectors. + +Inputs +------ + +Names of the inputs depend on coordinate system defined in the **Coordinates** parameter: + +* **X** / **Rho** / **Rho**. The first scalar field. The input is mandatory. +* **Y** / **Phi** / **Phi**. The second scalar field. The input is mandatory. +* **Z** / **Z** / **Theta**. The third scalar field. The input is mandatory. + +Parameters +---------- + +This node has the following parameter: + +* **Coordinates**. This defines the coordinate system being used. The available modes are: + + * **Carthesian**. Compose the vector field from X, Y and Z fields. + * **Cylindrical**. Compose the vector field from Rho, Phi and Z fields. + * **Spherical**. Compose the vector field from Rho, Phi and Theta fields. + +Outputs +------- + +This node has the following output: + +* **Field**. The generated vector field. + +Example of usage +---------------- + +Compose the vector field from three attraction fields: + +.. image:: https://user-images.githubusercontent.com/284644/79472827-c07c4d00-801d-11ea-8ada-7494115955cc.png + diff --git a/docs/nodes/field/curve_bend_field.rst b/docs/nodes/field/curve_bend_field.rst new file mode 100644 index 0000000000..6736b361fc --- /dev/null +++ b/docs/nodes/field/curve_bend_field.rst @@ -0,0 +1,100 @@ +Bend Along Curve Field +====================== + +Functionality +------------- + +This node generates a Vector Field, which bends some part of 3D space along the +provided Curve object. + +It is in general not a trivial task to rotate a 3D object along a vector, +because there are always 2 other axes of object and it is not clear where +should they be directed to. So, this node supports 3 different algorithms of +object rotation calculation. In many simple cases, all these algorithms will +give exactly the same result. But in more complex setups, or in some corner +cases, results can be very different. So, just try all algorithms and see which +one fits you better. + +Inputs +------ + +This node has the following inputs: + +* **Curve**. The curve to bend the space along. This input is mandatory. +* **Src T Min**. The minimum value of the orientation coordinate, where the + bending should begin. For example, if **Orientation axis** parameter is set + to Z, this is the minimum value of Z coordinate. The default value is -1.0. +* **Src T Max**. The maximum value of the orientation coordinate, where the + bending should end. For example, if **Orientation axis** parameter is set to + Z, this is the maximum value of Z coordinate. The default value is 1.0. +* **Resolution**. The number of samples along the curve, used to calculate + curve length parameter. This input is only available when **Scale along + curve** parameter is set to **Curve length**. The higher the value is, the + more precise is the calculation, but more time it is going to take. The + default value is 50. + +The field bends the part of space which is between **Src T Min** and **Src T +Max**, along the curve. For example, with default settings, the source part of +space is the space between Z = -1 and Z = 1. The behaviour of the field outside +of these bounds is not guaranteed. + +Parameters +---------- + +This node has the following parameters: + +* **Orientation**. Which axis of the source space should be elongated along the + curve. The available values are X, Y and Z. The default value is Z. When the + **Algorithm** parameter is set to **Zero-Twist** or **Frenet**, the only + available option is Z. +* **Scale all axis**. If checked, all three axis of the source space will be + scaled by the same amount as is required to fit the space between **Src T + Min** and **Src T Max** to the curve length. Otherwise, only orientation axis + will be scaled. Checked by default. +- **Algorithm**. Rotation calculation algorithm. Available values are: + + * Householder: calculate rotation by using Householder's reflection matrix + (see Wikipedia_ article). + * Tracking: use the same algorithm as in Blender's "TrackTo" kinematic + constraint. This algorithm gives you a bit more flexibility comparing to + other, by allowing to select the Up axis. + * Rotation difference: calculate rotation as rotation difference between two + vectors. + * Frenet: rotate the space according to curve's Frenet frame. + * Zero-Twist: rotate the space according to curve's "zero-twist" frame. + + Default value is Householder. + +* **Up axis**. Axis of donor object that should point up in result. This + parameter is available only when Tracking algorithm is selected. Value of + this parameter must differ from **Orientation** parameter, otherwise you will + get an error. Default value is X. +* **Scale along curve**. This defines how the scaling of the space along the + curve is to be calculated. The available options are: + + * **Curve parameter**. Scale the space proportional to curve's T parameter. + * **Curve length**. Scale the space proportional to curve's length. This + usually gives more natural results, but takes more time to compute. + + The default option is **Curve parameter**. + +.. _Wikipedia: https://en.wikipedia.org/wiki/QR_decomposition#Using_Householder_reflections + +Outputs +------- + +This node has the following output: + +* **Field**. The generated bending vector field. + +Examples of usage +----------------- + +Bend a cube along some closed curve: + +.. image:: https://user-images.githubusercontent.com/284644/79593221-93e73480-80f4-11ea-8c14-7f1511b1bd7b.png + +It is possible to use one field to bend several objects: + +.. image:: https://user-images.githubusercontent.com/284644/79593228-95186180-80f4-11ea-930f-59f3f124da63.png + diff --git a/docs/nodes/field/decompose_vector_field.rst b/docs/nodes/field/decompose_vector_field.rst new file mode 100644 index 0000000000..bac6895de8 --- /dev/null +++ b/docs/nodes/field/decompose_vector_field.rst @@ -0,0 +1,44 @@ +Decompose Vector Field +====================== + +Functionality +------------- + +This node generates three Scalar Fields by decomposing a Vector Field into it's coordinates. + +Inputs +------ + +This node has the following input: + +* **Field**. The vector field to be decomposed. The input is mandatory. + +Parameters +---------- + +This node has the following parameter: + +* **Coordinates**. This defines the coordinate system to be used. The available options are: + + * **Carthesian**. The vector field is decomposed into X, Y and Z fields. + * **Cylindrical**. The vector field is decomposed into Rho, Phi and Z fields. + * **Spherical**. The vector field is decomposed into Rho, Phi and Theta fields. + + The default mode is **Carthesian**. + +Outputs +------- + +The names of the outputs depend on the **Coordinates** parameter: + +* **X** / **Rho** / **Rho**. The first scalar field. +* **Y** / **Phi** / **Phi**. The second scalar field. +* **Z** / **Z** / **Theta**. The third scalar field. + +Example of usage +---------------- + +Use only one component of some attraction vector field to scale the cubes: + +.. image:: https://user-images.githubusercontent.com/284644/79474322-ae9ba980-801f-11ea-9cb0-0d6d5085e22a.png + diff --git a/docs/nodes/field/differential_operations.rst b/docs/nodes/field/differential_operations.rst new file mode 100644 index 0000000000..f642126677 --- /dev/null +++ b/docs/nodes/field/differential_operations.rst @@ -0,0 +1,52 @@ +Field Differential Operations +============================= + +Functionality +------------- + +This node calculates Vector or Scalar Field by performing one of supported +differential operations on the fields provided as inputs. + +Note that the differentiation is done numerically, and so there is always some +calculation error. The error can be minimizing by adjusting the "step" +parameter. + +Inputs +------ + +This node has the following inputs: + +* **SFieldA**. Scalar field to operate on. This input is mandatory when available. +* **VFieldA**. Vector field to operate on. This input is mandatory when available. + +The availability of the inputs is defined by the selected operation. + +Parameters +---------- + +This node has the following parameters: + +* **Operation**. The differential operation to perform. The available operations are: + + * **Gradient**. Calculate the gradient of the scalar field. The result is a vector field. + * **Divergence**. Calculate the divergence of the vector field. The result is a scalar field. + * **Laplacian**. Calculate the Laplace operator on the scalar field. The result is a scalar field. + * **Rotor**. Calculate the rotor operator on the vector field. The result is a vector field. + +* **Step**. Derivatives calculation step. The default value is 0.001. Bigger values give smoother fields. + +Outputs +------- + +This node has the following outputs: + +* **SFieldB**. The resulting scalar field. +* **VFieldB**. The resulting vector field. + +Examples of usage +----------------- + +Generate a scalar field, calculate it's gradient, and apply that to a cube: + +.. image:: https://user-images.githubusercontent.com/284644/79501054-deaa7300-8046-11ea-8cf6-d01471cb1eea.png + diff --git a/docs/nodes/field/field_index.rst b/docs/nodes/field/field_index.rst new file mode 100644 index 0000000000..64fb6533ec --- /dev/null +++ b/docs/nodes/field/field_index.rst @@ -0,0 +1,27 @@ +******* +Field +******* + +.. toctree:: + :maxdepth: 2 + + attractor_field + scalar_field_formula + vector_field_formula + noise_vfield + voronoi_field + image_field + curve_bend_field + bend_along_surface + compose_vector_field + decompose_vector_field + scalar_field_math + vector_field_math + differential_operations + merge_scalar_fields + vector_field_graph + vector_field_lines + scalar_field_eval + vector_field_eval + vector_field_apply + diff --git a/docs/nodes/field/image_field.rst b/docs/nodes/field/image_field.rst new file mode 100644 index 0000000000..193298508b --- /dev/null +++ b/docs/nodes/field/image_field.rst @@ -0,0 +1,47 @@ +Image Field +=========== + +Functionality +------------- + +This node generates Vector Fields and Scalar Fields based on different representations of colors in the Blender's Image object. + +The image is supposed to lie in one of coordinate planes. For example, if the +image lies in XOY plane, and we fix some pair of X and Y coordinates (X0, Y0), +then the values of the field will be the same in all points (X0, Y0, Z) for any +Z. + +Inputs +------ + +This node does not have inputs. + +Parameters +---------- + +This node has the following parameters: + +* **Image plane**. The coordinate plane in which the image is supposed to be + placed. The available values are XY, YZ and XZ. The default value is XY. +* **Image**. Blender's Image object to be turned into Field. + +Outputs +------- + +This node has the following outputs: + +* **RGB**. Vector field. Coordinates of the vectors are defined by RGB components of colors in the image. +* **HSV**. Vector field. Coordinates of the vectors are defined by HSV components of colors in the image. +* **Red**, **Green**, **Blue**. Scalar fields defined by RGB components of colors in the image. +* **Hue**, **Saturation**, **Value**. Scalar fields defined by HSV components of colors in the image. +* **Alpha**. Scalar field defined by the alpha (transparency) channel of the image. +* **RGB Average**. Scalar field defined as average of R, G and B channels of the image. +* **Luminosity**. Scalar field defined as luminosity channel of the image. + +Example of usage +---------------- + +Simple example: + +.. image:: https://user-images.githubusercontent.com/284644/79607518-c309a000-810c-11ea-86fa-be7c16043b32.png + diff --git a/docs/nodes/field/merge_scalar_fields.rst b/docs/nodes/field/merge_scalar_fields.rst new file mode 100644 index 0000000000..130558ecae --- /dev/null +++ b/docs/nodes/field/merge_scalar_fields.rst @@ -0,0 +1,47 @@ +Join Scalar Fields +================== + +Functionality +------------- + +This node joins (merges) a list of Scalar Field objects by one of supported +mathematical operations, to generate a new Scalar Field. + +Inputs +------ + +This node has the following input: + +* **Fields**. The list of scalar fields to be merged. + +Parameters +---------- + +This node has the following parameter: + +* **Mode**. This defines the operation used to calculate the new field. The supported operations are: + + * **Minimum**. Take the minimal value of all fields: MIN(S1(X), S2(X), ...). + * **Maximum**. Take the maximum value of all fields: MAX(S1(X), S2(X), ...) + * **Average**. Take the average (mean) value of all fields: (S1(X) + S2(X) + ...) / N. + * **Sum**. Take the sum of all fields: S1(X) + S2(X) + ... + * **Voronoi**. Take the difference between two smallest field values: ABS(S1(X) - S2(X)). + +Outputs +------- + +This node has the following output: + +* **Field**. The generated scalar field. + +Examples of usage +----------------- + +Take the minimum of several attraction fields: + +.. image:: https://user-images.githubusercontent.com/284644/79610315-bdfb1f80-8111-11ea-886d-030538a50e5d.png + +The same with Voronoi mode: + +.. image:: https://user-images.githubusercontent.com/284644/79610367-d834fd80-8111-11ea-9d3b-95b671256904.png + diff --git a/docs/nodes/field/noise_vfield.rst b/docs/nodes/field/noise_vfield.rst new file mode 100644 index 0000000000..b80e006531 --- /dev/null +++ b/docs/nodes/field/noise_vfield.rst @@ -0,0 +1,47 @@ +Noise Vector Field +================== + +Functionality +------------- + +This node generates a Vector Field, which is defined by one of several noise types supported. + +Inputs +------ + +This node has the following input: + +* **Seed**. The noise seed. + +Parameters +---------- + +This node has the following parameter: + +* **Type**. The type of noise. The available values are: + + * **Blender** + * **Perlin Original** + * **Perlin New** + * **Voronoi F1** + * **Voronoi F2** + * **Voronoi F3** + * **Voronoi F4** + * **Voronoi F2F1** + * **Voronoi Crackle** + * **Cellnoise** + +Outputs +------- + +This node has the following output: + +* **Noise**. The generated vector field. + +Example of usage +---------------- + +Visualize the noise field: + +.. image:: https://user-images.githubusercontent.com/284644/79488796-b8c7a300-8033-11ea-956c-fc66819a0aed.png + diff --git a/docs/nodes/field/scalar_field_eval.rst b/docs/nodes/field/scalar_field_eval.rst new file mode 100644 index 0000000000..d3ba9fe5bd --- /dev/null +++ b/docs/nodes/field/scalar_field_eval.rst @@ -0,0 +1,30 @@ +Evaluate Scalar Field +===================== + +Functionality +------------- + +This node calculates the value of the provided Scalar Field at the specified point. + +Inputs +------ + +This node has the following inputs: + +* **Field**. The field to be evaluated. This input is mandatory. +* **Vertices**. The points at which the field is to be evaluated. The default value is `(0, 0, 0)`. + +Outputs +------- + +This node has the following output: + +* **Value**. The values of the field at the specified points. + +Example of usage +---------------- + +Use the values of scalar field to scale the cubes: + +.. image:: https://user-images.githubusercontent.com/284644/79475246-dc352280-8020-11ea-8f8d-ddd3d6ca7a73.png + diff --git a/docs/nodes/field/scalar_field_formula.rst b/docs/nodes/field/scalar_field_formula.rst new file mode 100644 index 0000000000..d03b77af25 --- /dev/null +++ b/docs/nodes/field/scalar_field_formula.rst @@ -0,0 +1,90 @@ +Scalar Field Formula +==================== + +Functionality +------------- + +This node generates a Scalar Field, defined by some user-provided formula. + +The formula should map the coordinates of the point in 3D space (in one of supported coordinate systems) into some number. + +It is possible to use additional parameters in the formula, they will become inputs of the node. + +It is also possible to use the value of some other Scalar Field in the same point in the formula. + +Expression syntax +----------------- + +Syntax being used for formulas is standard Python's syntax for expressions. +For exact syntax definition, please refer to https://docs.python.org/3/reference/expressions.html. + +In short, you can use usual mathematical operations (`+`, `-`, `*`, `/`, `**` for power), numbers, variables, parenthesis, and function call, such as `sin(x)`. + +One difference with Python's syntax is that you can call only restricted number of Python's functions. Allowed are: + +- Functions from math module: + - acos, acosh, asin, asinh, atan, atan2, + atanh, ceil, copysign, cos, cosh, degrees, + erf, erfc, exp, expm1, fabs, factorial, floor, + fmod, frexp, fsum, gamma, hypot, isfinite, isinf, + isnan, ldexp, lgamma, log, log10, log1p, log2, modf, + pow, radians, sin, sinh, sqrt, tan, tanh, trunc; +- Constants from math module: pi, e; +- Additional functions: abs, sign; +- From mathutlis module: Vector, Matrix; +- Python type conversions: tuple, list, dict. + +This restriction is for security reasons. However, Python's ecosystem does not guarantee that noone can call some unsafe operations by using some sort of language-level hacks. So, please be warned that usage of this node with JSON definition obtained from unknown or untrusted source can potentially harm your system or data. + +Examples of valid expressions are: + +* 1.0 +* x +* x+1 +* 0.75*X + 0.25*Y +* R * sin(phi) + +Inputs +------ + +This node has the following input: + +* **Field**. A scalar field, whose values can be used in the formula. This input is required only if the formula involves the **S** variable. + +Each variable used in the formula, except for `S` and the coordinate variables, also becomes additional input. + +The following variables are considered to be point coordinates: + +* For Carthesian mode: `x`, `y` and `z`; +* For Cylindrical mode: `rho`, `phi` and `z`; +* For Spherical mode: `rho`, `phi` and `theta`. + +Parameters +---------- + +This node has the following parameters: + +* **Input**. This defines the coordinate system being used. The available + values are **Carhtesian**, **Cylindrical** and **Spherical**. The default + value is **Carthesian**. +* **Formula**. The formula which defines the scalar field. The default formula + is `x*x + y*y + z*z`. + +Outputs +------- + +This node has the following output: + +* **Field**. The generated scalar field. + +Examples of usage +----------------- + +Use the scalar field, defined by formula in in cylindrical coordinates, to scale some spheres: + +.. image:: https://user-images.githubusercontent.com/284644/79490204-ef9eb880-8035-11ea-810d-59f9a98ebd5f.png + +The same formula in spherical coordinates: + +.. image:: https://user-images.githubusercontent.com/284644/79490196-ee6d8b80-8035-11ea-874a-1d126b5c46b1.png + diff --git a/docs/nodes/field/scalar_field_math.rst b/docs/nodes/field/scalar_field_math.rst new file mode 100644 index 0000000000..d66cdf05e3 --- /dev/null +++ b/docs/nodes/field/scalar_field_math.rst @@ -0,0 +1,51 @@ +Scalar Field Math +================= + +Functionality +------------- + +This node generates a Scalar Field by executing one of supported mathematical operations on the Scalar Fields provided as inputs. + +Inputs +------ + +This node has the following inputs: + +* **FieldA**. The first scalar field. The input is mandatory when available. +* **FieldB**. The second scalar field. The input is mandatory when available. + +Parameters +---------- + +This node has the following parameter: + +* **Operation**. This defines the mathematical operation to perform. The available operations are: + + * **Add**. Add two scalar fields. + * **Sub**. Substract one scalar field from another. + * **Multiply**. Multiply two scalar fields. + * **Minimum**. Create a scalar field, the value of which is calculated as + minimal of values of two scalar fields at the same point. + * **Maximum**. Create a scalar field, the value of which is calculated as + maximal of values of two scalar fields at the same point. + * **Average**. Arithmetic mean (average) of two scalar fields - (A + B)/2. + * **Negate**. Negate the scalar field values. + +Outputs +------- + +The node has the following output: + +* **FieldC**. The resulting scalar field. + +Examples of usage +----------------- + +Example #1: + +.. image:: https://user-images.githubusercontent.com/284644/79497591-60979d80-8041-11ea-9c23-1e0d6be96708.png + +Example #2: + +.. image:: https://user-images.githubusercontent.com/284644/79497828-cbe16f80-8041-11ea-957f-54c011afe3a3.png + diff --git a/docs/nodes/field/vector_field_apply.rst b/docs/nodes/field/vector_field_apply.rst new file mode 100644 index 0000000000..6e9c17fdb6 --- /dev/null +++ b/docs/nodes/field/vector_field_apply.rst @@ -0,0 +1,44 @@ +Apply Vector Field +================== + +Functionality +------------- + +This node applies the Vector Field to provided points. More precisely, given +the vector field VF (which is a function from points to vectors) and a point X, +it calculates new point as `X + K * VF(X)`. + +This node can also apply the field iteratively, by calculating `X + K*VF(X) + +K*VF(X + K*VF(X)) + ...`. In other words, it can apply the field to the result +of the first application, and repeat that several times. + +Inputs +------ + +This node has the following inputs: + +* **Field**. The vector field to be applied. This input is mandatory. +* **Vertices**. The points to which the field is to be applied. The default value is `(0, 0, 0)`. +* **Coefficient**. The vector field application coefficient. The default value is 1.0. +* **Iterations**. Number of vector field application iterations. For example, 2 + will mean that the node returns the result of field application to the result + of first application. The default value is 1. + +Outputs +------- + +This node has the following output: + +* **Vertices**. The result of the vector field application to the original points. + +Examples of usage +----------------- + +Apply noise vector field to the points of straight line segment: + +.. image:: https://user-images.githubusercontent.com/284644/79487691-15c25980-8032-11ea-93e9-51f9b54bd36e.png + +Apply the same field to the same points, by only by a small amount; then apply the same field to the resulting points, and repeat that 10 times: + +.. image:: https://user-images.githubusercontent.com/284644/79487987-7b164a80-8032-11ea-8197-c78314843ffa.png + diff --git a/docs/nodes/field/vector_field_eval.rst b/docs/nodes/field/vector_field_eval.rst new file mode 100644 index 0000000000..089a211e27 --- /dev/null +++ b/docs/nodes/field/vector_field_eval.rst @@ -0,0 +1,36 @@ +Evaluate Vector Field +===================== + +Functionality +------------- + +This node calculates the values of the provided Vector Field at the given +points. More precisely, given the field VF (which is a function from vectors to +vectors), and point X, it calculates the vector VF(X). + +Inputs +------ + +This node has the following inputs: + +* **Field**. The vector field to be evaluated. This input is mandatory. +* **Vertices**. The points at which to evaluate the field. The default value is `(0, 0, 0)`. + +Outputs +------- + +This node has the following output: + +* **Vectors**. The vectors calculated at the provided points. + +Examples of usage +----------------- + +Replace each point of straight line segment with the result of noise vector field evaluation at that point: + +.. image:: https://user-images.githubusercontent.com/284644/79476391-5c0fbc80-8022-11ea-9457-1babe56f4388.png + +Visualize vector field vectors by connecting original points of the line segment and the points obtained by moving the original points by the results of vector field evaluation: + +.. image:: https://user-images.githubusercontent.com/284644/79476395-5d40e980-8022-11ea-846b-68da09ed2e41.png + diff --git a/docs/nodes/field/vector_field_formula.rst b/docs/nodes/field/vector_field_formula.rst new file mode 100644 index 0000000000..296c1086e1 --- /dev/null +++ b/docs/nodes/field/vector_field_formula.rst @@ -0,0 +1,98 @@ +Vector Field Formula +==================== + +Functionality +------------- + +This node generates a Vector Field, defined by some user-provided formula. + +The formula should map the coordinates of the point in 3D space into some vector in 3D space. Both original points and the resulting vector can be expressed in one of supported coordinate systems. + +It is possible to use additional parameters in the formula, they will become inputs of the node. + +It is also possible to use the value of some other Vector Field in the same point in the formula. + +Expression syntax +----------------- + +Syntax being used for formulas is standard Python's syntax for expressions. +For exact syntax definition, please refer to https://docs.python.org/3/reference/expressions.html. + +In short, you can use usual mathematical operations (`+`, `-`, `*`, `/`, `**` for power), numbers, variables, parenthesis, and function call, such as `sin(x)`. + +One difference with Python's syntax is that you can call only restricted number of Python's functions. Allowed are: + +- Functions from math module: + - acos, acosh, asin, asinh, atan, atan2, + atanh, ceil, copysign, cos, cosh, degrees, + erf, erfc, exp, expm1, fabs, factorial, floor, + fmod, frexp, fsum, gamma, hypot, isfinite, isinf, + isnan, ldexp, lgamma, log, log10, log1p, log2, modf, + pow, radians, sin, sinh, sqrt, tan, tanh, trunc; +- Constants from math module: pi, e; +- Additional functions: abs, sign; +- From mathutlis module: Vector, Matrix; +- Python type conversions: tuple, list, dict. + +This restriction is for security reasons. However, Python's ecosystem does not guarantee that noone can call some unsafe operations by using some sort of language-level hacks. So, please be warned that usage of this node with JSON definition obtained from unknown or untrusted source can potentially harm your system or data. + +Examples of valid expressions are: + +* 1.0 +* x +* x+1 +* 0.75*X + 0.25*Y +* R * sin(phi) + +Inputs +------ + +This node has the following input: + +* **Field**. A vector field, whose values can be used in the formula. This + input is required only if the formula involves the **V** variable. + +Each variable used in the formula, except for `V` and the coordinate variables, also becomes additional input. + +The following variables are considered to be point coordinates: + +* For Carthesian input mode: `x`, `y` and `z`; +* For Cylindrical input mode: `rho`, `phi` and `z`; +* For Spherical input mode: `rho`, `phi` and `theta`. + +Parameters +---------- + +This node has the following parameters: + +* **Input**. This defines the coordinate system being used for the input + points. The available values are **Carhtesian**, **Cylindrical** and + **Spherical**. The default value is **Carthesian**. +* **Formula1**, **Formula2**, **Formula3**. Three formulas defining the + respective coordinate / components of the resulting vectors: X, Y, Z, or Rho, + Phi, Z, or Rho, Phi, Theta, depending on the **Output** parameter. The + default formulas are `-y`, `x` and `z`, which defines the field which rotates + the whole space 90 degrees around the Z axis. +* **Output**. This defines the coordinate system in which the resulting vectors + are expressed. The available values are **Carhtesian**, **Cylindrical** and + **Spherical**. The default value is **Carthesian**. + +Outputs +------- + +This node has the following output: + +* **Field**. The generated vector field. + +Examples of usage +----------------- + +Some vector field defined by a formula in Spherical coordinates: + +.. image:: https://user-images.githubusercontent.com/284644/79491540-0a722c80-8038-11ea-914f-0221e5e75f68.png + +Similar field applied to some box: + +.. image:: https://user-images.githubusercontent.com/284644/79491547-0ba35980-8038-11ea-89fc-063982ea65cd.png + + diff --git a/docs/nodes/field/vector_field_graph.rst b/docs/nodes/field/vector_field_graph.rst new file mode 100644 index 0000000000..349cced9df --- /dev/null +++ b/docs/nodes/field/vector_field_graph.rst @@ -0,0 +1,53 @@ +Vector Field Graph +================== + +Functionality +------------- + +This node visualizes a Vector Field object by generating arrows (which +represent vectors) from points of original space to the results of vector field +application to those original points. + +The original points are generated as carthesian grid in 3D space. + +This node is mainly intended for visualization. + +Inputs +------ + +This node has the following inputs: + +* **Field**. The vector field to be visualized. This input is mandatory. +* **Bounds**. The list of points which define the area of space, in which the + field is to be visualized. Only the bounding box of these points is used. +* **Scale**. The scale of arrows to be generated. The default value is 1.0. +* **SamplesX**, **SamplesY**, **SamplesZ**. The number of samples of carthesian + grid, from which the arrows are to be generated. The default value is 10. + +Parameters +---------- + +This node has the following parameters: + +* **Auto scale**. Select the scale of arrows automatically, so that the largest + arrows are not bigger than the distance between carthesian grid points. + Checked by default. +* **Join**. If checked, then all arrows will be merged into one mesh object. + Otherwise, separate object will be generated for each arrow. Checked by + default. + +Outputs +------- + +This node has the following outputs: + +* **Vertices**. The vertices of the arrows. +* **Edges**. The edges of the arrows. + +Example of usage +---------------- + +Visualize some vector field: + +.. image:: https://user-images.githubusercontent.com/284644/79471192-b8bba900-801b-11ea-829e-2b003d9000da.png + diff --git a/docs/nodes/field/vector_field_lines.rst b/docs/nodes/field/vector_field_lines.rst new file mode 100644 index 0000000000..761b65f9bc --- /dev/null +++ b/docs/nodes/field/vector_field_lines.rst @@ -0,0 +1,52 @@ +Vector Field Lines +================== + +Functionality +------------- + +This node visualizes a Vector Field object by generating lines of that field. More precisely, given the point X and field VF, the node does the following: + +* takes original point X +* Applies the field to it with small coefficient, to create a point X1 = X + K * VF(X) +* Applies the field to X1 with small coefficient, to create a point X2 = X1 + K * VF(X1) +* And so on, repeating some number of times. + +And then the edges are created between these points. When the coefficient is +small enough, and the number of iterations is big enough, such lines represent +trajectories of material points, when they are moved by some force field. + +Inputs +------ + +This node has the following inputs: + +* **Field**. The vector field to be visualized. This input is mandatory. +* **Vertices**. The points at which to start drawing vector field lines. This input is mandatory. +* **Step**. Vector field application coefficient. If **Normalize** parameter is + checked, then this coefficient is divided by vector norm. The default value + is 0.1. +* **Iterations**. The number of iterations. The default value is 10. + +Parameters +---------- + +This node has the following parameters: + +* **Normalize**. If checked, then all edges of the generated lines will have + the same length (defined by **Steps** input). Otherwise, length of segments + will be proportional to vector norms. Checked by default. +* **Join**. If checked, join all lines into single mesh object. Checked by default. + +Outputs +------- + +* **Vertices**. The vertices of generated lines. +* **Edges**. The edges of generated lines. + +Example of usage +---------------- + +Visualize some vector field: + +.. image:: https://user-images.githubusercontent.com/284644/79495842-a56e0500-803e-11ea-91ed-611abf181ec2.png + diff --git a/docs/nodes/field/vector_field_math.rst b/docs/nodes/field/vector_field_math.rst new file mode 100644 index 0000000000..dcf47a13ed --- /dev/null +++ b/docs/nodes/field/vector_field_math.rst @@ -0,0 +1,81 @@ +Vector Field Math +================= + +Functionality +------------- + +This node generates a Vector Field and/or Scalar Field by executing one of +supported mathematical operations between the provided Vector and / or Scalar +fields. + +Inputs +------ + +This node has the following inputs: + +* **VFieldA**. The first vector field. This input is mandatory when available. +* **VFieldB**. The second vector field. This input is mandatory when available. +* **SFieldB**. The scalar field. This input is mandatory when available. + +The availability of the inputs is defined by the mathematical operation being +used. Input names are adjusted corresponding to the selected operation. + +Parameters +---------- + +This node has the following parameter: + +* **Operation**. This defines the mathematical operation to execute. The following operations are available: + + * **Add**. Calculate vector (coordinate-wise) sum of two vector fields - VFieldA + VFieldB. + * **Sub**. Calculate vector (coordinate-wise) difference between two vector fields - VFieldA - VFieldB. + * **Average**. Calculate the average between two vector fields - (VFieldA + VFieldB) / 2. + * **Scalar Product**. Calculate scalar (dot) product of two vector fields. + * **Vector Product**. Calculate vector (cross) product of two vector fields. + * **Multiply Scalar**. Multiply the vectors of vector field by scalar values of scalar field. + * **Projection decomposition**. Project the vectors of the first vector field + to the vectors of the second vector field ("basis"); output the component + of the first vector field which is colinear to the basis ("Projection") and + the residual component ("Coprojection"). + * **Composition VB(VA(X))**. Functional composition of two vector fields; the + resulting vector is calculated by evaluating the first vector field, and + then evaluating the second vector field at the resulting point of the first + evaluation. + * **Composition SB(VA(X))**. Functional composition of vector field and a + scalar field. The result is a scalar field. The resulting scalar is + calculated by first evaluating the vector field at original point, and then + evaluating the scalar field at the resulting point. + * **Norm**. Calculate the norm (length) of vector field vectors. The result is a scalar field. + * **Lerp A -> B**. Linear interpolation between two vector fields. The + interpolation coefficient is defined by a scalar field. The result is a + vector field. + * **Relative -> Absolute**. Given the vector field VF, return the vector field which maps point X to `X + VF(X)`. + * **Absolute -> Relative**. Given the vector field VF, return the vector field which maps point X to `VF(X) - X`. + +Outputs +------- + +This node has the following outputs: + +* **VFieldC**. The first vector field result of the calculation. +* **SFieldC**. The scalar field result of the calculation. +* **VFieldD**. The second vector field result of the calculation. + +The availability of the oututs is defined by the mathematical operation being +used. Output names are adjusted corresponding to the selected operation. + +Examples of usage +----------------- + +Make a vector field as difference of two attraction fields: + +.. image:: https://user-images.githubusercontent.com/284644/79495842-a56e0500-803e-11ea-91ed-611abf181ec2.png + +Make a vector field as a vector product of noise field and an attraction field: + +.. image:: https://user-images.githubusercontent.com/284644/79495812-9be49d00-803e-11ea-8ea0-9f9cfd7dc01e.png + +Apply such a field to a plane: + +.. image:: https://user-images.githubusercontent.com/284644/79495805-9ab37000-803e-11ea-9fb4-4eff7839cd23.png + diff --git a/docs/nodes/field/voronoi_field.rst b/docs/nodes/field/voronoi_field.rst new file mode 100644 index 0000000000..8636a8b6ee --- /dev/null +++ b/docs/nodes/field/voronoi_field.rst @@ -0,0 +1,49 @@ +Voronoi Field +============= + +Functionality +------------- + +This node generates a Vector Field and a Scalar Field, which are defined +according to the Voronoi diagram for the specified set of points ("voronoi +sites"): + +* The scalar field value for some point is calculated as the difference between + distances from this point to two nearest Voronoi sites; +* The vector field vector for some point points from this point towards the + nearest Voronoi site. The absolute value of this vector is equal to the value + of Voronoi scalar field in this point. + +So, the set of points (planar areas), where scalar field equals to zero, is +exactly the Voronoi diagram for the specified set of sites. + +Inputs +------ + +This node has the following input: + +* **Vertices**. The set of points (sites) to form Voronoi field for. This input is mandatory. + +Outputs +------- + +This node has the following outputs: + +* **SField**. Voronoi scalar field. +* **VField**. Voronoi vector field. + +Examples of usage +----------------- + +Use Voronoi scalar field of three points (marked with red spheres) to scale blue spheres: + +.. image:: https://user-images.githubusercontent.com/284644/79604012-cf8afa00-8106-11ea-9283-0856cd0a0a6c.png + +Visualize the lines of corresponding vector field: + +.. image:: https://user-images.githubusercontent.com/284644/79604015-d0bc2700-8106-11ea-9f5d-621fdc900fcb.png + +Apply the vector field to some box: + +.. image:: https://user-images.githubusercontent.com/284644/79604016-d0bc2700-8106-11ea-8863-dbb9af6ab8b3.png + diff --git a/docs/nodes/surface/apply_field_to_surface.rst b/docs/nodes/surface/apply_field_to_surface.rst new file mode 100644 index 0000000000..581f606755 --- /dev/null +++ b/docs/nodes/surface/apply_field_to_surface.rst @@ -0,0 +1,40 @@ +Apply Field to Surface +====================== + +Functionality +------------- + +This node generates a Surface by applying some vector field to another Surface. +More precisely, it generates the surface, points of which are calculated as `X ++ K * VF(X)`, where X is the point of initial surface, VF is the vector field, + and K is some coefficient. + +Surface domain: the same as of initial surface. + +Inputs +------ + +This node has the following inputs: + +* **Field**. Vector field to apply. This input is mandatory. +* **Surface**. The surface to apply the vector field to. This input is mandatory. +* **Coefficient**. The "strength" coefficient. Value of 0 will output the surface as it was. The default value is 1.0. + +Outputs +------- + +This node has the following output: + +* **Surface**. The generated surface. + +Examples of usage +----------------- + +Noise field applied to the sphereical surface: + +.. image:: https://user-images.githubusercontent.com/284644/79371205-6ae86780-7f6d-11ea-80f7-3bf5ca71c7cf.png + +An example with more complex field: + +.. image:: https://user-images.githubusercontent.com/284644/79371656-23aea680-7f6e-11ea-97e1-1ac22cb194a1.png + diff --git a/docs/nodes/surface/curve_lerp.rst b/docs/nodes/surface/curve_lerp.rst new file mode 100644 index 0000000000..8043273c18 --- /dev/null +++ b/docs/nodes/surface/curve_lerp.rst @@ -0,0 +1,41 @@ +Linear Surface +============== + +Functionality +------------- + +This node generates a Surface as a linear interpolation of two Curve objects. + +Along U parameter, such surface forms a curve calculated as a linear interpolation of two curves. +Along V parameter, such surface is always a straight line. + +Surface domain: In U direction - from 0 to 1. In V direction - defined by node inputs, by default from 0 to 1. V = 0 corresponds to the first curve; V = 1 corresponds to the second curve. + +Inputs +------ + +This node has the following inputs: + +* **Curve1**. The first curve to interpolate. This input is mandatory. +* **Curve2**. The second curve to interpolate. This input is mandatory. +* **VMin**. The minimum value of curve V parameter. The default value is 0. +* **VMax**. The maximum value of curve V parameter. The default value is 1. + +Outputs +------- + +This node has the following output: + +* **Surface**. The generated surface. + +Examples of usage +----------------- + +Generate a linear surface between a triangle and a hexagon: + +.. image:: https://user-images.githubusercontent.com/284644/79353388-6e232980-7f54-11ea-87a8-b08d78ea34ff.png + +Generate a linear surface between two cubic splines: + +.. image:: https://user-images.githubusercontent.com/284644/79353383-6cf1fc80-7f54-11ea-855b-ec782edf2c5f.png + diff --git a/docs/nodes/surface/evaluate_surface.rst b/docs/nodes/surface/evaluate_surface.rst new file mode 100644 index 0000000000..cf319a6914 --- /dev/null +++ b/docs/nodes/surface/evaluate_surface.rst @@ -0,0 +1,108 @@ +Evaluate Surface +================ + +Functionality +------------- + +This node calculates points on the Surface corresponding to provided values of +U and V surface parameters. The values of U and V can be either provided +explicitly, or generated by the node to generate carthesian (rectangular) grid. + +Inputs +------ + +This node has the following inputs: + +* **Surface**. The surface to evaluate. This input is mandatory. +* **U**, **V**. Values of U and V surface parameter. These inputs are available + only when the **Evaluate** parameter is set to **Explicit**, and **Input + mode** parameter is set to **Separate**. The default value is 0.5. +* **Vertices**. Points at which the surface should be evaluated. Only two of + three coordinates will be used; the coordinates used are defined by + **Orientation** parameter. This input is available and mandatory if the + **Evaluation mode** parameter is set to **Explicit**, and **Input mode** is + set to **Vertices**. +* **SamplesU**, **SamplesV**. Number of samples along U and V direction, used + to generate carthesian grid. These inputs are available only when the + **Evaluate** parameter is set to **Grid**. The default value is 25. + +Parameters +---------- + +This node has the following parameters: + +* **Evaluate**. This determines how the values of surface's U and V parameters + are defined. The available options are: + + * **Explicit**. U and V parameters are to be provided in inputs of this node. + Depending on **Input mode** parameter, either **U** and **V** inputs, or + **Vertices** input will be used. + * **Grid**. The node will generate evenly-spaced carthesian grid with number + of samples along U and V direction defined by **SamplesU** and **SamplesV** + inputs, correspondingly. + + The default mode is **Grid**. + +* **Input mode**. This parameter is available only when **Evaluate** parameter + is set to **Explicit**. The available options are: + + * **Separate**. The values of U and V surface parameters will be provided in + **U** and **V** inputs, correspondingly. + * **Vertices**. The values of U and V surface parameters will be provided in + **Vertices** input; only two of three coordinates of the input vertices + will be used. + + The default mode is **Separate**. + +* **Input orientation**. This parameter is available only when **Evaluate** + parameter is set to **Explicit**, and **Input mode** parameter is set to + **Vertices**. This defines which coordinates of vertices provided in the + **Vertices** input will be used. The available options are XY, YZ and XZ. For + example, if this is set to XY, then the X coordinate of vertices will be used + as surface U parameter, and Y coordinate will be used as V parameter. The + default value is XY. +* **Clamp**. This parameter is available only when **Evaluate** parameter is + set to **Explicit**. This defines how the node will process the values of + surface U and V parameters which are out of the surface's domain. The + available options are: + + * **As is**. Do not do anything special, just pass the parameters to the + surface calculation algorithm as they are. The behaviour of the surface + when the values of parameters provided are out of domain depends on + specific surface: some will just calculate points by the same formula, + others will give an error. + * **Clamp**. Restrict the parameter values to the surface domain: replace + values that are greater than the higher bound with higher bound value, + replace values that are smaller than the lower bound with the lower bound + value. For example, if the surface domain along U direction is `[0 .. 1]`, + and the value of U parameter is 1.05, calculate the point of the surface + at U = 1.0. + * **Wrap**. Wrap the parameter values to be within the surface domain, i.e. + take the values modulo domain. For example, if the surface domain along U + direction is `[0 .. 1]`, and the value of U parameter is 1.05, evaluate + the surface at U = 0.05. + + The default mode is **As is**. + +Outputs +------- + +This node has the following outputs: + +* **Vertices**. The calculated points on the surface. +* **Edges**. The edges of carthesian grid on the surface. This output is only + available when the **Evaluate** parameter is set to **Grid**. +* **Faces**. The faces of carthesian grid on the surface. This output is only + available when the **Evaluate** parameter is set to **Grid**. + +Examples of usage +----------------- + +The node used in **Grid** node to generate rectangular grid on the plane: + +.. image:: https://user-images.githubusercontent.com/284644/78699409-4b25c380-791d-11ea-8671-2b304e108ed1.png + +An example of **Explicit** mode usage: + +.. image:: https://user-images.githubusercontent.com/284644/79249080-2dfd7180-7e96-11ea-810a-4a188536adc0.png + diff --git a/docs/nodes/surface/extrude_curve.rst b/docs/nodes/surface/extrude_curve.rst new file mode 100644 index 0000000000..593805c852 --- /dev/null +++ b/docs/nodes/surface/extrude_curve.rst @@ -0,0 +1,61 @@ +Extrude Curve Along Curve +========================= + +Functionality +------------- + +This node generates a Surface object by extruding one Curve (called "profile") along another Curve (called "Extrusion"). +The Profile curve may optionally be rotated while extruding, to make result look more naturally. + +Surface domain: Along U direction - the same as of "profile" curve; along V +direction - the same as of "extrusion" curve. + +Inputs +------ + +This node has the following inputs: + +* **Profile**. The profile curve (one which is to be extruded). This input is mandatory. +* **Extrusion**. The extrusion curve (the curve along which the profile is to be extruded). This input is mandatory. +* **Resolution**. Number of samples for **Zero-Twist** rotation algorithm + calculation. The more the number is, the more precise the calculation is, but + the slower. The default value is 50. This input is only available when + **Algorithm** parameter is set to **Zero-Twist**. + +Parameters +---------- + +This node has the following parameter: + +* **Algorithm**. Profile curve rotation calculation algorithm. The available options are: + + * **None**. Do not rotate the profile curve, just extrude it as it is. This mode is the default one. + * **Frenet**. Rotate the profile curve according to Frenet frame of the extrusion curve. + * **Zero-Twist**. Rotate the profile curve according to "zero-twist" frame of the extrusion curve. + +Outputs +------- + +This node has the following output: + +* **Surface**. The generated surface. + +Examples of usage +----------------- + +"None" algorithm works fine in many simple cases: + +.. image:: https://user-images.githubusercontent.com/284644/79357796-eb04d200-7f59-11ea-8ef1-cb35ebb0083e.png + +It becomes not so good if the extrusion curve has some rotations: + +.. image:: https://user-images.githubusercontent.com/284644/79357777-e5a78780-7f59-11ea-8f08-ba309965b67c.png + +Similar case with "Frenet" algorithm: + +.. image:: https://user-images.githubusercontent.com/284644/79357785-e809e180-7f59-11ea-976a-a2bc32388ee0.png + +The same with "Zero-Twist" algorithm: + +.. image:: https://user-images.githubusercontent.com/284644/79357791-e93b0e80-7f59-11ea-9f8f-e74e1eead4cb.png + diff --git a/docs/nodes/surface/extrude_point.rst b/docs/nodes/surface/extrude_point.rst new file mode 100644 index 0000000000..17c13adbea --- /dev/null +++ b/docs/nodes/surface/extrude_point.rst @@ -0,0 +1,40 @@ +Extrude to Point +================ + +Functionality +------------- + +This node generates a Surface by extruding some Curve (called "profile") +towards one point (also called "tip"). So the resulting surface is, generally +speaking, a conical surface. + +Surface domain: along U direction - the same as of "profile" curve; along V +direction - from 0 to 1. V = 0 corresponds to the initial position of profile +curve; V = 1 corresponds to the tip point. + +Inputs +------ + +This node has the following inputs: + +* **Profile**. The profile curve. This input is mandatory. +* **Point**. The point towards the profile is to be extruded. The default value is `(0, 0, 0)` (origin). + +Outputs +------- + +This node has the following output: + +* **Surface**. The generated surface. + +Examples of usage +----------------- + +Extrude a pentagonal polyline towards one point, to make a pyramid: + +.. image:: https://user-images.githubusercontent.com/284644/77252629-d133e000-6c76-11ea-9a04-f760827bf659.png + +It is possible to use this node to fill planar (or semi-planar) curves with some grid: + +.. image:: https://user-images.githubusercontent.com/284644/79359647-4c2da500-7f5c-11ea-8903-26beb236bbac.png + diff --git a/docs/nodes/surface/extrude_vector.rst b/docs/nodes/surface/extrude_vector.rst new file mode 100644 index 0000000000..52b3bc9150 --- /dev/null +++ b/docs/nodes/surface/extrude_vector.rst @@ -0,0 +1,34 @@ +Extrude Curve Along Vector +========================== + +Functionality +------------- + +This node generates a Surface by extruding a Curve (called "profile") along some vector. + +Surface domain: along U direction - the same as of profile curve; along V +direction - from 0 to 1. V = 0 corresponds to the initial position of profile +curve. + +Inputs +------ + +This node has the following inputs: + +* **Profile**. The curve to be extruded. This input is mandatory. +* **Vector**. The vector along which the curve must be extruded. The default value is `(0, 0, 1)`. + +Outputs +------- + +This node has the following output: + +* **Surface**. The generated surface. + +Example of usage +---------------- + +Extrude some cubic spline along a vector: + +.. image:: https://user-images.githubusercontent.com/284644/79358827-2ce24800-7f5b-11ea-91ec-a5df4762e610.png + diff --git a/docs/nodes/surface/interpolating_surface.rst b/docs/nodes/surface/interpolating_surface.rst new file mode 100644 index 0000000000..4458e9e8c7 --- /dev/null +++ b/docs/nodes/surface/interpolating_surface.rst @@ -0,0 +1,52 @@ +Surface from Curves +=================== + +Functionality +------------- + +This node generates a Surface object by interpolating several Curves. Two interpolation modes are supported: linear and cubic. + +Surface domain: from 0 to 1 in both directions. + +Inputs +------ + +This node has the following inputs: + +* **Curves**. The list of curves to interpolate between. For Linear + interpolation, at least two curves are required; for Cubic interpolation, at + least three curves are required. + +Parameters +---------- + +This node has the following parameters: + +* **Interpolation mode**. The available values are **Linear** and **Cubic**. + The default mode is **Linear**. +* **Cyclic**. This defines whether the surface should be cyclic (closed) in the + U direction - i.e., should the node create an interpolation between the last + curve and the first one. Unchecked by default. + +Outputs +------- + +This node has the following output: + +* **Surface**. The generated surface. + +Examples of usage +----------------- + +Linear interpolation between several polylines: + +.. image:: https://user-images.githubusercontent.com/284644/79368209-771df600-7f68-11ea-9d65-90257d257bd0.png + +The same but with Cubic interpolation: + +.. image:: https://user-images.githubusercontent.com/284644/79368213-784f2300-7f68-11ea-80c0-059469ba14d9.png + +Cubic interpolation between several curves (generated by trigonometric formula): + +.. image:: https://user-images.githubusercontent.com/284644/79369070-d0d2f000-7f69-11ea-97f6-b71d33f49b97.png + diff --git a/docs/nodes/surface/plane.rst b/docs/nodes/surface/plane.rst new file mode 100644 index 0000000000..d73b363aa5 --- /dev/null +++ b/docs/nodes/surface/plane.rst @@ -0,0 +1,60 @@ +Plane (Surface) +=============== + +Functionality +------------- + +This node generate a Surface object, which represents a rectangular segment of plane. + +Surface domain: defined by node parameters. + +Surface parametrization: Point = P0 + u*V1 + v*V1 + +Inputs +------ + +This node has the following inputs: + +* **Point1**. The first point on the plane. This point will correspond to U = 0 and V = 0. The default value is `(0, 0, 0)`. +* **Point2**. The second point on the plane. This point will correspond to U = + 1 and V = 0. This input is only available when **Mode** parameter is set to + **Three points**. The default value is `(1, 0, 0)`. +* **Point3**. The third point on the plane. This point will correspond to U = 0 + and V = 1. This input is only available when **Mode** parameter is set to + **Three points**. The default value is `(0, 1, 0)`. +* **Normal**. The normal direction of the plane. This input is only available + when **Mode** parameter is set to **Point and normal**. The default value is + `(0, 0, 1)`. +* **U Min**, **U Max**. Minimum and maximum values of surface's U parameter. + Default values are 0 and 1. +* **V Min**, **V Max**. Minimum and maximum values of surface's V parameter. + Default values are 0 and 1. + +Parameters +---------- + +This node has the following parameter: + +* **Mode**. This determines how the plane is specified. The available options are: + + * **Three points** + * **Point and normal** + +Outputs +------- + +This node has the following output: + +* **Surface**. The Surface object of the plane. + +Examples of usage +----------------- + +Default settings: + +.. image:: https://user-images.githubusercontent.com/284644/78699409-4b25c380-791d-11ea-8671-2b304e108ed1.png + +It is possible to generate a plane with non-rectangular parametrization, if three points provided do not make a right angle: + +.. image:: https://user-images.githubusercontent.com/284644/78699412-4bbe5a00-791d-11ea-87c9-78c7bbe4ed78.png + diff --git a/docs/nodes/surface/revolution_surface.rst b/docs/nodes/surface/revolution_surface.rst new file mode 100644 index 0000000000..3cd3437d43 --- /dev/null +++ b/docs/nodes/surface/revolution_surface.rst @@ -0,0 +1,48 @@ +Revolution Surface +================== + +Functionality +------------- + +Given a curve, this node generates a surface which is made by revolving +(rotating) this curve along some axis vector. Many surfaces of revolution, such +as spheres, cylinders, cones may be made with this node. + +Note that the profile curve does not have to be planar (flat). + +Surface domain: in U direction - the same as profile curve; in V direction - defined by node inputs. +Zero (0) value of V corresponds to initial position of the profile curve. + +Inputs +------ + +This node has the following inputs: + +* **Profile**. The profile curve, i.e. the curve which is going to be rotated + to make a surface. This input is mandatory. +* **Point**. A point on the rotation axis. The default value is `(0, 0, 0)`. +* **Direction**. The direction of the rotation axis. The default value is `(0, 0, 1)` (Z axis). +* **AngleFrom**. The minimal value of rotation angle to rotate the curve from; + i.e. the surface's V parameter minimal value. The default value is 0.0. +* **AngleTo**. The maximum value of rotation angle to rotate the curve to; i.e. + the surface's V parameter maximum value. The default value is 2*pi (full + circle). + +Outputs +------- + +This node has the following output: + +* **Surface**. The generated surface of revolution. + +Examples of usage +----------------- + +Rotate a line segment around Z axis to make a conical surface: + +.. image:: https://user-images.githubusercontent.com/284644/78705373-cdff4c00-7926-11ea-943a-42eaa6ba8241.png + +Rotate some cubic spline around X axis: + +.. image:: https://user-images.githubusercontent.com/284644/78705377-cf307900-7926-11ea-8d30-9fd707c42ab6.png + diff --git a/docs/nodes/surface/sphere.rst b/docs/nodes/surface/sphere.rst new file mode 100644 index 0000000000..771fe18fa9 --- /dev/null +++ b/docs/nodes/surface/sphere.rst @@ -0,0 +1,45 @@ +Sphere (Surface) +================ + +Functionality +------------- + +This node generates a spherical Surface object. Several parametrizations are available: + +* Default - corresponding to spherical coordinates formula +* Equirectangular +* Lambert +* Gall Stereographic + +For formulas, please refer to Wikipedia: https://en.wikipedia.org/wiki/List_of_map_projections + +Inputs +------ + +This node has the following inputs: + +* **Center**. The center of the sphere. The default value is `(0, 0, 0)` (origin). +* **Radius**. Sphere radius. The default value is 1.0. +* **Theta1**. Theta1 parameter of the Equirectangular projection. This input is only available when the **Projection** parameter is set to **Equirectangular**. The default value is pi/4. + +Parameters +---------- + +This node has the following parameter: + +* **Projection**. This defines the parametrization to be used. + +Outputs +------- + +This node has the following output: + +* **Surface**. The generated spherical surface. + +Example of usage +---------------- + +The default parametrization: + +.. image:: https://user-images.githubusercontent.com/284644/79351826-93169d00-7f52-11ea-99ad-bb904f62ce6c.png + diff --git a/docs/nodes/surface/subdomain.rst b/docs/nodes/surface/subdomain.rst new file mode 100644 index 0000000000..3dcb9e986c --- /dev/null +++ b/docs/nodes/surface/subdomain.rst @@ -0,0 +1,38 @@ +Surface Subdomain +================= + +Functionality +------------- + +This node generates a Surface, which is defined as a subset of the input +Surface with a smaller range of allowed U and V parameter values. In other +words, the output surface is the same as input one, but with restricted range +of U and V values allowed. + +Output surface domain: defined by node inputs. + +Output surface parametrization: the same as of input surface. + +Inputs +------ + +This node has the following inputs: + +* **Surface**. The surface to cut. This input is mandatory. +* **UMin**, **UMax**. The minimum and maximum values of the new surface's U parameter. +* **VMin**, **VMax**. The minimum and maximum values of the new surface's V parameter. + +Outputs +------- + +This node has the following output: + +* **Surface**. The new surface (the subset of the input surface). + +Example of usage +---------------- + +The formula-generated surface (green) and a subdomain of it (red one): + +.. image:: https://user-images.githubusercontent.com/284644/79385737-83af4800-7f82-11ea-96e4-f4fb779646db.png + diff --git a/docs/nodes/surface/surface_domain.rst b/docs/nodes/surface/surface_domain.rst new file mode 100644 index 0000000000..4a019625b3 --- /dev/null +++ b/docs/nodes/surface/surface_domain.rst @@ -0,0 +1,32 @@ +Surface Domain +============== + +Functionality +------------- + +This node outputs the domain of the Surface, i.e. the range of values the surface's U and V parameters are allowed to take. + +Inputs +------ + +This node has the following input: + +* **Surface**. The surface to be measured. This input is mandatory. + +Outputs +------- + +This node has the following outputs: + +* **UMin**, **UMax**. Minimum and maximum allowed values of surface's U parameter. +* **URange**. The length of surface's domain along U direction; this equals to the difference **UMax** - **UMin**. +* **VMin**, **VRange**. Minimum and maximum allowed values of surface's V parameter. +* **VRange**. The length of surface's domain along V direction; this equals to the difference **VMax** - **VMin**. + +Example of usage +---------------- + +Applied to the sphere with Equirectangular parametrization: + +.. image:: https://user-images.githubusercontent.com/284644/79386265-50b98400-7f83-11ea-94bb-90d30a7c710a.png + diff --git a/docs/nodes/surface/surface_formula.rst b/docs/nodes/surface/surface_formula.rst new file mode 100644 index 0000000000..1118c96a90 --- /dev/null +++ b/docs/nodes/surface/surface_formula.rst @@ -0,0 +1,91 @@ +Surface Formula +=============== + +Funcitonality +------------- + +This node generates a Surface, defined by some user-provided formula. + +The formula should map the curve's U and V parameters into one of supported 3D coordinate systems: (X, Y, Z), (Rho, Phi, Z) or (Rho, Phi, Theta). + +It is possible to use additional parameters in the formula, they will become inputs of the node. + +Surface domain / parametrization specifics: defined by node settings. + +Expression syntax +----------------- + +Syntax being used for formulas is standard Python's syntax for expressions. +For exact syntax definition, please refer to https://docs.python.org/3/reference/expressions.html. + +In short, you can use usual mathematical operations (`+`, `-`, `*`, `/`, `**` for power), numbers, variables, parenthesis, and function call, such as `sin(x)`. + +One difference with Python's syntax is that you can call only restricted number of Python's functions. Allowed are: + +- Functions from math module: + - acos, acosh, asin, asinh, atan, atan2, + atanh, ceil, copysign, cos, cosh, degrees, + erf, erfc, exp, expm1, fabs, factorial, floor, + fmod, frexp, fsum, gamma, hypot, isfinite, isinf, + isnan, ldexp, lgamma, log, log10, log1p, log2, modf, + pow, radians, sin, sinh, sqrt, tan, tanh, trunc; +- Constants from math module: pi, e; +- Additional functions: abs, sign; +- From mathutlis module: Vector, Matrix; +- Python type conversions: tuple, list, dict. + +This restriction is for security reasons. However, Python's ecosystem does not guarantee that noone can call some unsafe operations by using some sort of language-level hacks. So, please be warned that usage of this node with JSON definition obtained from unknown or untrusted source can potentially harm your system or data. + +Examples of valid expressions are: + +* 1.0 +* x +* x+1 +* 0.75*X + 0.25*Y +* R * sin(phi) + +Inputs +------ + +This node has the following inputs: + +* **UMin**, **UMax**. Minimum and maximum values of the surface's U parameter. +* **VMin**, **VMax**. Minimum and maximum values of the surface's V parameter. + +Each variable used in formulas, except for `u` and `v`, also becomes an additional input. + +Parameters +---------- + +This node has the following parameters: + +* **Formula 1**, **Formula 2**, **Formula 3**. Formulas for 3 components + defining surface points in the used coordinate system. Default values define + a torodial surface. +* **Output**. This defined the coordinate system being used, and thus it + defines the exact meaing of formula parameters. The available modes are: + + * **Carthesian**. Three formulas will define correspondingly X, Y and Z coordinates. + * **Cylindrical**. Three formulas will define correspondingly Rho, Phi and Z coordinates. + * **Spherical**. Three formulas will define correspondingly Rho, Phi and Theta coordinates. + + The default mode is **Carthesian**. + +Outputs +------- + +This node has the following output: + +* **Curve**. The generated curve. + +Examples of usage +----------------- + +The default parameters - a torus: + +.. image:: https://user-images.githubusercontent.com/284644/79387280-d558d200-7f84-11ea-9a28-68b5299ce8ec.png + +An example of cylindrical coordinates usage: + +.. image:: https://user-images.githubusercontent.com/284644/79387284-d689ff00-7f84-11ea-922f-bf28efcd7e53.png + diff --git a/docs/nodes/surface/surface_index.rst b/docs/nodes/surface/surface_index.rst new file mode 100644 index 0000000000..f0f634f167 --- /dev/null +++ b/docs/nodes/surface/surface_index.rst @@ -0,0 +1,24 @@ +******** +Surface +******** + +.. toctree:: + :maxdepth: 2 + + plane + sphere + surface_formula + curve_lerp + surface_lerp + interpolating_surface + revolution_surface + taper_sweep + extrude_vector + extrude_point + extrude_curve + apply_field_to_surface + surface_domain + subdomain + tessellate_trim + evaluate_surface + diff --git a/docs/nodes/surface/surface_lerp.rst b/docs/nodes/surface/surface_lerp.rst new file mode 100644 index 0000000000..d948d2db28 --- /dev/null +++ b/docs/nodes/surface/surface_lerp.rst @@ -0,0 +1,36 @@ +Surface Lerp +============ + +Functionality +------------- + +This node generates a Surface by calculating the linear interpolation ("lerp") between two other Surfaces. + +Surface domain: from 0 to 1 in both directions. + +Inputs +------ + +This node has the following inputs: + +* **Surface1**. The first surface for the interpolation. This input is mandatory. +* **Surface2**. The second surface for the interpolation. This input is mandatory. +* **Coefficient**. Interpolation coefficient. Value of 0 will generate the + surface equivalent to Surface1. Value of 1 will generate the surface + equivalent to Surface2. The default value is 0.5 (in the middle of two + surfaces). + +Outputs +------- + +This node has the following output: + + * **Surface**. The interpolated surface. + +Example of usage +---------------- + +Interpolation of some random surfaces: + +.. image:: https://user-images.githubusercontent.com/284644/79361263-7f713380-7f5e-11ea-89de-60c74c4e1594.png + diff --git a/docs/nodes/surface/taper_sweep.rst b/docs/nodes/surface/taper_sweep.rst new file mode 100644 index 0000000000..b899ee10c4 --- /dev/null +++ b/docs/nodes/surface/taper_sweep.rst @@ -0,0 +1,42 @@ +Taper Sweep Surface +=================== + +Functionality +------------- + +This node generates a surface by sweeping a curve (called "profile") along an +axis. While the curve is moved, it is scaled according to another curve (called +"taper curve"). + +This node is a generalization of "revolution surface" node: when the "profile" +curve is a circle, this node will generate a revolution surface from "taper" +curve. + +Domain / parametrization specifics: Domain along U parameter is the same as +domain of the "profile" curve. Domain along V parameter is the same as the +domain of the "taper" curve. + +Inputs +------ + +This node has the following inputs: + +* **Profile**. The profile curve. This input is mandatory. +* **Taper**. The taper curve. This input is mandatory. +* **Point**. The point on the taper axis line. The default value is `(0, 0, 0)`. +* **Direction**. The directing vector of the taper axis line. The default value is `(0, 0, 1)` (Z axis). + +Outputs +------- + +This node has the following output: + +* **Surface**. The generated surface. + +Example of usage +---------------- + +The taper curve is generated by "filleting" some line; the profile curve is filleted square: + +.. image:: https://user-images.githubusercontent.com/284644/79348542-8ee88080-7f4e-11ea-972b-50b9d6909734.png + diff --git a/docs/nodes/surface/tessellate_trim.rst b/docs/nodes/surface/tessellate_trim.rst new file mode 100644 index 0000000000..048864d771 --- /dev/null +++ b/docs/nodes/surface/tessellate_trim.rst @@ -0,0 +1,63 @@ +Tessellate & Trim Surface +========================= + +Functionality +------------- + +This node "visualizes" the Surface object (turns it into a mesh), by drawing a carthesian (rectangular) grid on it and then cutting (trimming) that grid with the specified Curve object. + +The provided trimming curve is supposed to be planar (flat), and be defined in the surface's U/V coordinates frame. + +Note that this node is supported since Blender 2.81 only. It will not work in Blender 2.80. + +Inputs +------ + +This node has the following inputs: + +* **Surface**. The surface to tessellate. This input is mandatory. +* **TrimCurve**. The curve used to trim the surface. This input is mandatory. +* **SamplesU**. The number of tessellation samples along the surface's U direction. The default value is 25. +* **SamplesV**. The number of tessellation samples along the surface's V direction. The default value is 25. +* **SamplesT**. The number of points to evaluate the trimming curve at. The default value is 100. + +Parameters +---------- + +This node has the following parameters: + +* **Curve plane**. The coordinate plane in which the trimming curve is lying. + The available options are XY, YZ and XZ. The third coordinate is just + ignored. For example, if XY is selected, then X coordinates of the curve's + points will be used as surface's U parameter, and Y coordinates of the + curve's points will be used as surface's V parameter. The default option is + XY. +* **Cropping mode**. This defines which part of the surface to output: + + * **Inner** - generate the part of the surface which is inside the trimming curve; + * **Outer** - generate the part of the surface which is outside of the + trimming curve (make a surface with a hole in it). + + The default option is Inner. + +* **Accuracy**. This parameter is available in the node's N panel only. This defines the precision of the calculation. The default value is 5. Usually you do not have to change this value. + +Outputs +------- + +This node has the following outputs: + +* **Vertices**. The vertices of the tessellated surface. +* **Faces**. The faces of the tessellated surface. + +Examples of usage +----------------- + +Trim some (formula-generated) surface with a circle: + +.. image:: https://user-images.githubusercontent.com/284644/79388812-72b50580-7f87-11ea-9eab-2fd205b632d8.png + +Cut a circular hole in the surface: + +.. image:: https://user-images.githubusercontent.com/284644/79388815-73e63280-7f87-11ea-9bc9-de200fce3c59.png + diff --git a/docs/surfaces.rst b/docs/surfaces.rst new file mode 100644 index 0000000000..b990e60017 --- /dev/null +++ b/docs/surfaces.rst @@ -0,0 +1,53 @@ + +Surface +------- + +Sverchok uses the term **Surface** mostly in the same way it is used in mathematics. + +From the user perspective, a Surface object is just some (more or less smooth) +surface laying in 3D space, which has some boundary. It may appear that +boundary of the surface from one side coincides with the boundary of the +surface from another side; in such case we say the surface is **closed**, or +**cyclic** in certain direction. Examples of closed surfaces are: + +* Cylindrical surface (closed in one direction); +* Toroidal surface (closed in two directions); +* Sphereical surface (closed in two directions). + +The simples example of non-closed (open) surface is a unit square. + +You will find that the Surface object has a lot in common with Curve object. +One may say, that Surface is almost the same as Curve, just it is a 2D object +rather than 1D. + +Mathematically, a Surface is a set of points in 3D space, which can be defined +as a codomain of some function from R^2 to R^3; i.e. the function, which maps +each point on 2D plane to some point in 3D space. We will be considering only +"good enough" functions - continuous and having at least one derivative at each +point. + +It is important to understand, that each surface can be defined by more than +one function (which is called parametrization of curve). We usually use the one +which is most fitting our goals in specific task. + +We usually use the letters **u** and **v** for curve parameters. + +Excercise for the reader: write down several possible parametrization for the +unit square surface, which has corners `(0, 0, 0)`, `(0, 1, 0)`, `(1, 1, 0)`, +`(1, 0, 0)`. + +The range of the surface parameters corresponding to the whole surface within +it's boundaries (in specific parametrization) is called **surface domain**. The +same surface can have different domains in different parametrizations. + +Similar to curves, the values of surface parameters have nothing to do with +distances or areas, which are covered by points on the surface. + +Since Blender has mostly a mesh-based approach to modelling, as well as +Sverchok, to "visualize" the Surface object you have to convert it to mesh. It +is usually done by use of "Evaluate Surface" node. + +It is also possible to "visualize" the surface by use of "Tessellate & Trim" +node. This node allows one to tessellate the part of surface, trimmed by some +curve. + diff --git a/index.md b/index.md index b27c9f99f4..dd0738a791 100644 --- a/index.md +++ b/index.md @@ -51,6 +51,72 @@ SvPentagonTilerNode SvSpiralNodeMK2 +## Curves + SvExLineCurveNode + SvExCircleNode + SvExCurveFormulaNode + SvExPolylineNode + SvExFilletPolylineNode + SvExCubicSplineNode + SvExApplyFieldToCurveNode + SvExCastCurveNode + SvExIsoUvCurveNode + SvExSurfaceBoundaryNode + SvExCurveOnSurfaceNode + SvExCurveLerpCurveNode + SvExConcatCurvesNode + SvExFlipCurveNode + SvExCurveSegmentNode + SvExCurveRangeNode + SvExCurveEndpointsNode + SvExCurveLengthNode + SvExCurveFrameNode + SvExCurveCurvatureNode + SvExCurveTorsionNode + SvExCurveZeroTwistFrameNode + SvExCurveLengthParameterNode + SvExEvalCurveNode + +## Surfaces + SvExPlaneSurfaceNode + SvExSphereNode + SvExSurfaceFormulaNode + SvInterpolatingSurfaceNode + SvExRevolutionSurfaceNode + SvExTaperSweepSurfaceNode + SvExExtrudeCurveVectorNode + SvExExtrudeCurveCurveSurfaceNode + SvExExtrudeCurvePointNode + SvExCurveLerpNode + SvExSurfaceLerpNode + SvExSurfaceDomainNode + SvExSurfaceSubdomainNode + SvExApplyFieldToSurfaceNode + SvExTessellateTrimSurfaceNode + SvExEvalSurfaceNode + +## Fields + SvExScalarFieldFormulaNode + SvExVectorFieldFormulaNode + SvExComposeVectorFieldNode + SvExDecomposeVectorFieldNode + SvExScalarFieldPointNode + SvExAttractorFieldNode + SvExImageFieldNode + SvExScalarFieldMathNode + SvExMergeScalarFieldsNode + SvExScalarFieldEvaluateNode + SvExVectorFieldEvaluateNode + SvExVectorFieldApplyNode + SvExVectorFieldMathNode + SvExNoiseVectorFieldNode + SvExVoronoiFieldNode + SvExBendAlongCurveFieldNode + SvExBendAlongSurfaceFieldNode + SvExFieldDiffOpsNode + SvExVectorFieldGraphNode + SvExVectorFieldLinesNode + ## Analyzers SvBBoxNodeMk2 SvComponentAnalyzerNode diff --git a/json_examples/Fields/Noise_by_Attractor.json b/json_examples/Fields/Noise_by_Attractor.json new file mode 100644 index 0000000000..2408c77465 --- /dev/null +++ b/json_examples/Fields/Noise_by_Attractor.json @@ -0,0 +1,228 @@ +{ + "export_version": "0.079", + "framed_nodes": {}, + "groups": {}, + "nodes": { + "Apply Vector Field": { + "bl_idname": "SvExVectorFieldApplyNode", + "custom_socket_props": { + "1": { + "prop": [ + 0.0, + 0.0, + 0.0 + ], + "use_prop": true + } + }, + "height": 100.0, + "hide": false, + "label": "", + "location": [ + 444.5661926269531, + 87.91435241699219 + ], + "params": { + "coefficient": 1.0, + "iterations": 1 + }, + "width": 140.0 + }, + "Attractor Field.001": { + "bl_idname": "SvExAttractorFieldNode", + "custom_socket_props": { + "0": { + "expanded": true, + "prop": [ + 0.0, + 0.0, + 0.0 + ], + "use_prop": true + }, + "1": { + "expanded": true, + "prop": [ + 0.0, + 0.0, + 1.0 + ], + "use_prop": true + } + }, + "height": 100.0, + "hide": false, + "label": "", + "location": [ + -262.32928466796875, + 211.523193359375 + ], + "params": { + "amplitude": 0.5, + "attractor_type": "Line", + "coefficient": 2.0, + "falloff_type": "gauss" + }, + "width": 140.0 + }, + "Box.001": { + "bl_idname": "SvBoxNodeMk2", + "color": [ + 0.0, + 0.5, + 0.5 + ], + "height": 100.0, + "hide": false, + "label": "", + "location": [ + -38.772281646728516, + 107.41472625732422 + ], + "params": { + "Divx": 4, + "Divy": 4, + "Divz": 4, + "Size": 0.5 + }, + "use_custom_color": true, + "width": 140.0 + }, + "Matrix Apply": { + "bl_idname": "SvMatrixApplyJoinNode", + "height": 100.0, + "hide": false, + "label": "", + "location": [ + 230.62704467773438, + 43.78866195678711 + ], + "params": {}, + "width": 140.0 + }, + "Noise Vector Field": { + "bl_idname": "SvExNoiseVectorFieldNode", + "height": 100.0, + "hide": false, + "label": "", + "location": [ + 5.63525390625, + 247.97918701171875 + ], + "params": {}, + "width": 140.0 + }, + "Vector Field Math.001": { + "bl_idname": "SvExVectorFieldMathNode", + "height": 100.0, + "hide": false, + "label": "", + "location": [ + 209.77171325683594, + 245.20779418945312 + ], + "params": { + "operation": "MUL" + }, + "width": 153.2376708984375 + }, + "Vector P Field": { + "bl_idname": "SvHomogenousVectorField", + "height": 100.0, + "hide": false, + "label": "", + "location": [ + -30.303882598876953, + -153.09471130371094 + ], + "params": { + "sizex__": 2.5, + "sizey__": 2.5, + "sizez__": 2.5, + "xdim__": 5, + "ydim__": 5, + "zdim__": 5 + }, + "width": 140.0 + }, + "Viewer Draw Mk3": { + "bl_idname": "SvVDExperimental", + "color": [ + 1.0, + 0.30000001192092896, + 0.0 + ], + "height": 100.0, + "hide": false, + "label": "", + "location": [ + 637.34716796875, + 118.8923110961914 + ], + "params": { + "activate": 1, + "display_edges": 0, + "display_verts": 0, + "selected_draw_mode": "facet" + }, + "use_custom_color": true, + "width": 140.0 + } + }, + "update_lists": [ + [ + "Box.001", + 0, + "Matrix Apply", + 0 + ], + [ + "Box.001", + 2, + "Matrix Apply", + 2 + ], + [ + "Vector P Field", + 0, + "Matrix Apply", + 3 + ], + [ + "Noise Vector Field", + 0, + "Vector Field Math.001", + 0 + ], + [ + "Attractor Field.001", + 1, + "Vector Field Math.001", + 2 + ], + [ + "Vector Field Math.001", + 0, + "Apply Vector Field", + 0 + ], + [ + "Matrix Apply", + 0, + "Apply Vector Field", + 1 + ], + [ + "Apply Vector Field", + 0, + "Viewer Draw Mk3", + 0 + ], + [ + "Matrix Apply", + 2, + "Viewer Draw Mk3", + 2 + ] + ] +} \ No newline at end of file diff --git a/json_examples/Fields/Projection_Decomposition.json b/json_examples/Fields/Projection_Decomposition.json new file mode 100644 index 0000000000..d993b17cd4 --- /dev/null +++ b/json_examples/Fields/Projection_Decomposition.json @@ -0,0 +1,334 @@ +{ + "export_version": "0.079", + "framed_nodes": {}, + "groups": {}, + "nodes": { + "Apply Vector Field": { + "bl_idname": "SvExVectorFieldApplyNode", + "custom_socket_props": { + "1": { + "prop": [ + 0.0, + 0.0, + 0.0 + ], + "use_prop": true + } + }, + "height": 100.0, + "hide": false, + "label": "", + "location": [ + 454.9917297363281, + 194.8975830078125 + ], + "params": { + "coefficient": 0.5, + "iterations": 1 + }, + "width": 140.0 + }, + "Apply Vector Field.001": { + "bl_idname": "SvExVectorFieldApplyNode", + "custom_socket_props": { + "1": { + "prop": [ + 0.0, + 0.0, + 0.0 + ], + "use_prop": true + } + }, + "height": 100.0, + "hide": false, + "label": "", + "location": [ + 455.71661376953125, + 378.1544189453125 + ], + "params": { + "coefficient": 0.5, + "iterations": 1 + }, + "width": 140.0 + }, + "Attractor Field.001": { + "bl_idname": "SvExAttractorFieldNode", + "custom_socket_props": { + "0": { + "expanded": true, + "prop": [ + 0.0, + 0.0, + 0.0 + ], + "use_prop": true + }, + "1": { + "expanded": true, + "prop": [ + 0.0, + 0.0, + 1.0 + ], + "use_prop": true + } + }, + "height": 100.0, + "hide": false, + "label": "", + "location": [ + -262.32928466796875, + 211.523193359375 + ], + "params": { + "amplitude": 0.5, + "attractor_type": "Plane", + "coefficient": 2.0, + "falloff_type": "NONE" + }, + "width": 140.0 + }, + "Box.001": { + "bl_idname": "SvBoxNodeMk2", + "color": [ + 0.0, + 0.5, + 0.5 + ], + "height": 100.0, + "hide": false, + "label": "", + "location": [ + -38.772281646728516, + 107.41472625732422 + ], + "params": { + "Divx": 4, + "Divy": 4, + "Divz": 4, + "Size": 0.5 + }, + "use_custom_color": true, + "width": 140.0 + }, + "Matrix Apply": { + "bl_idname": "SvMatrixApplyJoinNode", + "height": 100.0, + "hide": false, + "label": "", + "location": [ + 207.16952514648438, + 59.44474411010742 + ], + "params": {}, + "width": 140.0 + }, + "Move": { + "bl_idname": "SvMoveNodeMk3", + "custom_socket_props": { + "1": { + "expanded": true + } + }, + "height": 100.0, + "hide": false, + "label": "", + "location": [ + 680.3553466796875, + 406.6930847167969 + ], + "params": { + "movement_vectors": [ + 4.0, + 0.0, + 0.0 + ] + }, + "width": 140.0 + }, + "Noise Vector Field": { + "bl_idname": "SvExNoiseVectorFieldNode", + "height": 100.0, + "hide": false, + "label": "", + "location": [ + 5.63525390625, + 247.97918701171875 + ], + "params": {}, + "width": 140.0 + }, + "Vector Field Math.001": { + "bl_idname": "SvExVectorFieldMathNode", + "height": 100.0, + "hide": false, + "label": "", + "location": [ + 209.77171325683594, + 245.20779418945312 + ], + "params": { + "operation": "TANG" + }, + "width": 153.2376708984375 + }, + "Vector P Field": { + "bl_idname": "SvHomogenousVectorField", + "height": 100.0, + "hide": false, + "label": "", + "location": [ + -30.303882598876953, + -153.09471130371094 + ], + "params": { + "sizex__": 2.5, + "sizey__": 2.5, + "sizez__": 2.5, + "xdim__": 5, + "ydim__": 5, + "zdim__": 5 + }, + "width": 140.0 + }, + "Viewer Draw Mk3": { + "bl_idname": "SvVDExperimental", + "color": [ + 1.0, + 0.30000001192092896, + 0.0 + ], + "height": 100.0, + "hide": false, + "label": "", + "location": [ + 864.103271484375, + 148.89981079101562 + ], + "params": { + "activate": 1, + "display_edges": 0, + "display_verts": 0, + "selected_draw_mode": "facet" + }, + "use_custom_color": true, + "width": 140.0 + }, + "Viewer Draw Mk3.001": { + "bl_idname": "SvVDExperimental", + "color": [ + 1.0, + 0.30000001192092896, + 0.0 + ], + "height": 100.0, + "hide": false, + "label": "", + "location": [ + 880.3553466796875, + 406.6930847167969 + ], + "params": { + "display_edges": 0, + "display_verts": 0, + "face_color": [ + 0.8100003600120544, + 0.5805560350418091, + 0.23866428434848785, + 1.0 + ], + "selected_draw_mode": "facet" + }, + "use_custom_color": true, + "width": 140.0 + } + }, + "update_lists": [ + [ + "Noise Vector Field", + 0, + "Vector Field Math.001", + 0 + ], + [ + "Attractor Field.001", + 0, + "Vector Field Math.001", + 1 + ], + [ + "Box.001", + 0, + "Matrix Apply", + 0 + ], + [ + "Box.001", + 2, + "Matrix Apply", + 2 + ], + [ + "Vector P Field", + 0, + "Matrix Apply", + 3 + ], + [ + "Vector Field Math.001", + 2, + "Apply Vector Field.001", + 0 + ], + [ + "Matrix Apply", + 0, + "Apply Vector Field.001", + 1 + ], + [ + "Apply Vector Field.001", + 0, + "Move", + 0 + ], + [ + "Move", + 0, + "Viewer Draw Mk3.001", + 0 + ], + [ + "Matrix Apply", + 2, + "Viewer Draw Mk3.001", + 2 + ], + [ + "Vector Field Math.001", + 0, + "Apply Vector Field", + 0 + ], + [ + "Matrix Apply", + 0, + "Apply Vector Field", + 1 + ], + [ + "Apply Vector Field", + 0, + "Viewer Draw Mk3", + 0 + ], + [ + "Matrix Apply", + 2, + "Viewer Draw Mk3", + 2 + ] + ] +} \ No newline at end of file diff --git a/nodes/curve/__init__.py b/nodes/curve/__init__.py new file mode 100644 index 0000000000..143f486c05 --- /dev/null +++ b/nodes/curve/__init__.py @@ -0,0 +1 @@ +# __init__.py diff --git a/nodes/curve/apply_field_to_curve.py b/nodes/curve/apply_field_to_curve.py new file mode 100644 index 0000000000..7a12388a81 --- /dev/null +++ b/nodes/curve/apply_field_to_curve.py @@ -0,0 +1,57 @@ + +import numpy as np + +import bpy +from bpy.props import FloatProperty, EnumProperty, BoolProperty, IntProperty + +from sverchok.node_tree import SverchCustomTreeNode, throttled +from sverchok.data_structure import updateNode, zip_long_repeat, fullList + +from sverchok.utils.curve import SvDeformedByFieldCurve + +class SvApplyFieldToCurveNode(bpy.types.Node, SverchCustomTreeNode): + """ + Triggers: Apply field to curve + Tooltip: Apply vector field to curve + """ + bl_idname = 'SvExApplyFieldToCurveNode' + bl_label = 'Apply Field to Curve' + bl_icon = 'CURVE_NCURVE' + bl_icon = 'OUTLINER_OB_EMPTY' + sv_icon = 'SV_CURVE_VFIELD' + + coefficient : FloatProperty( + name = "Coefficient", + default = 1.0, + update=updateNode) + + def sv_init(self, context): + self.inputs.new('SvVectorFieldSocket', "Field") + self.inputs.new('SvCurveSocket', "Curve") + self.inputs.new('SvStringsSocket', "Coefficient").prop_name = 'coefficient' + self.outputs.new('SvCurveSocket', "Curve") + + def process(self): + if not any(socket.is_linked for socket in self.outputs): + return + + curve_s = self.inputs['Curve'].sv_get() + field_s = self.inputs['Field'].sv_get() + coeff_s = self.inputs['Coefficient'].sv_get() + + curve_out = [] + for curve, field, coeff in zip_long_repeat(curve_s, field_s, coeff_s): + if isinstance(coeff, (list, tuple)): + coeff = coeff[0] + + new_curve = SvDeformedByFieldCurve(curve, field, coeff) + curve_out.append(new_curve) + + self.outputs['Curve'].sv_set(curve_out) + +def register(): + bpy.utils.register_class(SvApplyFieldToCurveNode) + +def unregister(): + bpy.utils.unregister_class(SvApplyFieldToCurveNode) + diff --git a/nodes/curve/cast_curve.py b/nodes/curve/cast_curve.py new file mode 100644 index 0000000000..11783b17d7 --- /dev/null +++ b/nodes/curve/cast_curve.py @@ -0,0 +1,109 @@ + +import numpy as np + +import bpy +from bpy.props import FloatProperty, EnumProperty, BoolProperty, IntProperty + +from sverchok.node_tree import SverchCustomTreeNode, throttled +from sverchok.data_structure import updateNode, zip_long_repeat, ensure_nesting_level + +from sverchok.utils.curve import SvCurve, SvCastCurveToPlane, SvCastCurveToSphere, SvCastCurveToCylinder + +class SvCastCurveNode(bpy.types.Node, SverchCustomTreeNode): + """ + Triggers: Cast Curve to Shape + Tooltip: Cast (project) a curve to the plane, sphere or cylindrical surface + """ + bl_idname = 'SvExCastCurveNode' + bl_label = 'Cast Curve' + bl_icon = 'OUTLINER_OB_EMPTY' + sv_icon = 'SV_CAST_CURVE' + + coefficient : FloatProperty( + name = "Coefficient", + default = 1.0, + update=updateNode) + + radius : FloatProperty( + name = "Radius", + default = 1.0, + update=updateNode) + + forms = [ + ('PLANE', "Plane", "Plane defined by point and normal vector", 0), + ('SPHERE', "Sphere", "Sphere defined by center and radius", 1), + ('CYLINDER', "Cylinder", "Cylinder defined by center, direction and radius", 2) + ] + + @throttled + def update_sockets(self, context): + self.inputs['Direction'].hide_safe = self.form == 'SPHERE' + self.inputs['Radius'].hide_safe = self.form == 'PLANE' + + form : EnumProperty( + name = "Target form", + items = forms, + default = 'PLANE', + update = update_sockets) + + def sv_init(self, context): + self.inputs.new('SvCurveSocket', "Curve") + + p = self.inputs.new('SvVerticesSocket', "Center") + p.use_prop = True + p.prop = (0.0, 0.0, 0.0) + + p = self.inputs.new('SvVerticesSocket', "Direction") + p.use_prop = True + p.prop = (0.0, 0.0, 1.0) + + self.inputs.new('SvStringsSocket', "Radius").prop_name = 'radius' + self.inputs.new('SvStringsSocket', "Coefficient").prop_name = 'coefficient' + self.outputs.new('SvCurveSocket', "Curve") + self.update_sockets(context) + + def draw_buttons(self, context, layout): + layout.label(text="Target form:") + layout.prop(self, 'form', text='') + + def process(self): + if not any(socket.is_linked for socket in self.outputs): + return + + curve_s = self.inputs['Curve'].sv_get() + center_s = self.inputs['Center'].sv_get() + direction_s = self.inputs['Direction'].sv_get() + radius_s = self.inputs['Radius'].sv_get() + coeff_s = self.inputs['Coefficient'].sv_get() + + if isinstance(curve_s[0], SvCurve): + curve_s = [curve_s] + center_s = ensure_nesting_level(center_s, 3) + direction_s = ensure_nesting_level(direction_s, 3) + radius_s = ensure_nesting_level(radius_s, 2) + coeff_s = ensure_nesting_level(coeff_s, 2) + + curves_out = [] + for curves, centers, directions, radiuses, coeffs in zip_long_repeat(curve_s, center_s, direction_s, radius_s, coeff_s): + for curve, center, direction, radius, coeff in zip_long_repeat(curves, centers, directions, radiuses, coeffs): + if self.form == 'PLANE': + new_curve = SvCastCurveToPlane(curve, np.array(center), + np.array(direction), coeff) + elif self.form == 'SPHERE': + new_curve = SvCastCurveToSphere(curve, np.array(center), + radius, coeff) + elif self.form == 'CYLINDER': + new_curve = SvCastCurveToCylinder(curve, np.array(center), + np.array(direction), radius, coeff) + else: + raise Exception("Unsupported target form") + curves_out.append(new_curve) + + self.outputs['Curve'].sv_set(curves_out) + +def register(): + bpy.utils.register_class(SvCastCurveNode) + +def unregister(): + bpy.utils.unregister_class(SvCastCurveNode) + diff --git a/nodes/curve/circle.py b/nodes/curve/circle.py new file mode 100644 index 0000000000..cba0217618 --- /dev/null +++ b/nodes/curve/circle.py @@ -0,0 +1,72 @@ + +import numpy as np +from math import pi + +from mathutils import Matrix +import bpy +from bpy.props import FloatProperty, EnumProperty, BoolProperty, IntProperty + +from sverchok.node_tree import SverchCustomTreeNode, throttled +from sverchok.data_structure import updateNode, zip_long_repeat, ensure_nesting_level + +from sverchok.utils.curve import SvCircle + +class SvCircleNode(bpy.types.Node, SverchCustomTreeNode): + """ + Triggers: Circle + Tooltip: Generate circular curve + """ + bl_idname = 'SvExCircleNode' + bl_label = 'Circle (Curve)' + bl_icon = 'MESH_CIRCLE' + + radius : FloatProperty( + name = "Radius", + default = 1.0, + update = updateNode) + + t_min : FloatProperty( + name = "T Min", + default = 0.0, + update = updateNode) + + t_max : FloatProperty( + name = "T Max", + default = 2*pi, + update = updateNode) + + def sv_init(self, context): + self.inputs.new('SvMatrixSocket', "Center") + self.inputs.new('SvStringsSocket', "Radius").prop_name = 'radius' + self.inputs.new('SvStringsSocket', "TMin").prop_name = 't_min' + self.inputs.new('SvStringsSocket', "TMax").prop_name = 't_max' + self.outputs.new('SvCurveSocket', "Curve") + + def process(self): + if not any(socket.is_linked for socket in self.outputs): + return + + center_s = self.inputs['Center'].sv_get(default=[Matrix()]) + radius_s = self.inputs['Radius'].sv_get() + t_min_s = self.inputs['TMin'].sv_get() + t_max_s = self.inputs['TMax'].sv_get() + radius_s = ensure_nesting_level(radius_s, 2) + t_min_s = ensure_nesting_level(t_min_s, 2) + t_max_s = ensure_nesting_level(t_max_s, 2) + center_s = ensure_nesting_level(center_s, 2, data_types=(Matrix,)) + + curves_out = [] + for centers, radiuses, t_mins, t_maxs in zip_long_repeat(center_s, radius_s, t_min_s, t_max_s): + for center, radius, t_min, t_max in zip_long_repeat(centers, radiuses, t_mins, t_maxs): + curve = SvCircle(center, radius) + curve.u_bounds = (t_min, t_max) + curves_out.append(curve) + + self.outputs['Curve'].sv_set(curves_out) + +def register(): + bpy.utils.register_class(SvCircleNode) + +def unregister(): + bpy.utils.unregister_class(SvCircleNode) + diff --git a/nodes/curve/concat_curves.py b/nodes/curve/concat_curves.py new file mode 100644 index 0000000000..ef949ade5c --- /dev/null +++ b/nodes/curve/concat_curves.py @@ -0,0 +1,74 @@ + +import numpy as np + +import bpy +from bpy.props import FloatProperty, EnumProperty, BoolProperty, IntProperty + +from sverchok.node_tree import SverchCustomTreeNode, throttled +from sverchok.data_structure import updateNode, zip_long_repeat, ensure_nesting_level +from sverchok.utils.curve import SvCurve, SvConcatCurve + +class SvConcatCurvesNode(bpy.types.Node, SverchCustomTreeNode): + """ + Triggers: Concatenate Curves + Tooltip: Concatenate several curves into one + """ + bl_idname = 'SvExConcatCurvesNode' + bl_label = 'Concat Curves' + bl_icon = 'OUTLINER_OB_EMPTY' + sv_icon = 'SV_CONCAT_CURVES' + + check : BoolProperty( + name = "Check coincidence", + default = False, + update = updateNode) + + max_rho : FloatProperty( + name = "Max. distance", + min = 0.0, + default = 0.001, + precision = 4, + update = updateNode) + + def draw_buttons(self, context, layout): + layout.prop(self, 'check') + if self.check: + layout.prop(self, 'max_rho') + + def sv_init(self, context): + self.inputs.new('SvCurveSocket', "Curves") + self.outputs.new('SvCurveSocket', "Curve") + + def run_check(self, curves): + for idx, (curve1, curve2) in enumerate(zip(curves, curves[1:])): + _, t_max_1 = curve1.get_u_bounds() + t_min_2, _ = curve2.get_u_bounds() + end1 = curve1.evaluate(t_max_1) + begin2 = curve2.evaluate(t_min_2) + distance = np.linalg.norm(begin2 - end1) + if distance > self.max_rho: + raise Exception("Distance between the end of {}'th curve and the start of {}'th curve is {} - too much".format(idx, idx+1, distance)) + + def process(self): + if not any(socket.is_linked for socket in self.outputs): + return + + curve_s = self.inputs['Curves'].sv_get() + if isinstance(curve_s[0], SvCurve): + curve_s = [curve_s] + + curves_out = [] + for curves in curve_s: + if self.check: + self.run_check(curves) + new_curve = SvConcatCurve(curves) + curves_out.append(new_curve) + + self.outputs['Curve'].sv_set(curves_out) + +def register(): + bpy.utils.register_class(SvConcatCurvesNode) + +def unregister(): + bpy.utils.unregister_class(SvConcatCurvesNode) + diff --git a/nodes/curve/cubic_spline.py b/nodes/curve/cubic_spline.py new file mode 100644 index 0000000000..2377698ad1 --- /dev/null +++ b/nodes/curve/cubic_spline.py @@ -0,0 +1,69 @@ + +import bpy +from bpy.props import FloatProperty, EnumProperty, BoolProperty, IntProperty + +from sverchok.node_tree import SverchCustomTreeNode, throttled +from sverchok.data_structure import updateNode, zip_long_repeat +from sverchok.utils.geom import LinearSpline, CubicSpline +from sverchok.utils.curve import SvSplineCurve + +class SvCubicSplineNode(bpy.types.Node, SverchCustomTreeNode): + """ + Triggers: Cubic Spline + Tooltip: Generate cubic interpolation curve + """ + bl_idname = 'SvExCubicSplineNode' + bl_label = 'Cubic Spline' + bl_icon = 'CON_SPLINEIK' + + is_cyclic : BoolProperty(name = "Cyclic", + description = "Whether the spline is cyclic", + default = False, + update=updateNode) + + metrics = [ + ('MANHATTAN', 'Manhattan', "Manhattan distance metric", 0), + ('DISTANCE', 'Euclidan', "Eudlcian distance metric", 1), + ('POINTS', 'Points', "Points based", 2), + ('CHEBYSHEV', 'Chebyshev', "Chebyshev distance", 3)] + + metric: EnumProperty(name='Metric', + description = "Knot mode", + default="DISTANCE", items=metrics, + update=updateNode) + + def draw_buttons(self, context, layout): + layout.prop(self, "is_cyclic", toggle=True) + + def draw_buttons_ext(self, context, layout): + self.draw_buttons(context, layout) + layout.prop(self, 'metric') + + def sv_init(self, context): + self.inputs.new('SvVerticesSocket', "Vertices") + self.outputs.new('SvCurveSocket', "Curve") + + def build_spline(self, path): + spline = CubicSpline(path, metric = self.metric, is_cyclic = self.is_cyclic) + return spline + + def process(self): + if not any(o.is_linked for o in self.outputs): + return + + vertices_s = self.inputs['Vertices'].sv_get(default=[[]]) + + out_curves = [] + for vertices in vertices_s: + spline = self.build_spline(vertices) + curve = SvSplineCurve(spline) + out_curves.append(curve) + + self.outputs['Curve'].sv_set(out_curves) + +def register(): + bpy.utils.register_class(SvCubicSplineNode) + +def unregister(): + bpy.utils.unregister_class(SvCubicSplineNode) + diff --git a/nodes/curve/curvature.py b/nodes/curve/curvature.py new file mode 100644 index 0000000000..2f1fd5406c --- /dev/null +++ b/nodes/curve/curvature.py @@ -0,0 +1,86 @@ +import numpy as np + +from mathutils import Matrix, Vector +import bpy +from bpy.props import FloatProperty, EnumProperty, BoolProperty, IntProperty + +from sverchok.node_tree import SverchCustomTreeNode, throttled +from sverchok.data_structure import updateNode, zip_long_repeat, fullList + +class SvCurveCurvatureNode(bpy.types.Node, SverchCustomTreeNode): + """ + Triggers: Curve Curvature + Tooltip: Calculate curvature of the curve + """ + bl_idname = 'SvExCurveCurvatureNode' + bl_label = 'Curve Curvature' + bl_icon = 'CURVE_NCURVE' + + t_value : FloatProperty( + name = "T", + default = 0.5, + update = updateNode) + + def sv_init(self, context): + self.inputs.new('SvCurveSocket', "Curve") + self.inputs.new('SvStringsSocket', "T").prop_name = 't_value' + self.outputs.new('SvStringsSocket', "Curvature") + self.outputs.new('SvStringsSocket', "Radius") + self.outputs.new('SvMatrixSocket', 'Center') + + def process(self): + if not any(socket.is_linked for socket in self.outputs): + return + + curve_s = self.inputs['Curve'].sv_get() + ts_s = self.inputs['T'].sv_get() + + center_out = [] + curvature_out = [] + radius_out = [] + for curve, ts in zip_long_repeat(curve_s, ts_s): + ts = np.array(ts) + + verts = curve.evaluate_array(ts) + + curvatures = curve.curvature_array(ts) + radiuses = 1.0 / curvatures + + tangents = curve.tangent_array(ts) + tangents = tangents / np.linalg.norm(tangents, axis=1)[np.newaxis].T + binormals = curve.binormal_array(ts) + normals = curve.main_normal_array(ts) + + radius_vectors = radiuses[np.newaxis].T * normals + centers = verts + radius_vectors + + matrices_np = np.dstack((-normals, tangents, binormals)) + matrices_np = np.transpose(matrices_np, axes=(0,2,1)) + dets = np.linalg.det(matrices_np) + good_idx = abs(dets) > 1e-6 + matrices_np[good_idx] = np.linalg.inv(matrices_np[good_idx]) + + new_matrices = [] + for ok, matrix_np, center in zip(good_idx, matrices_np, centers): + if ok: + matrix = Matrix(matrix_np.tolist()).to_4x4() + matrix.translation = Vector(center) + new_matrices.append(matrix) + else: + matrix = Matrix.Translation(center) + new_matrices.append(matrix) + + center_out.append(new_matrices) + radius_out.append(radiuses.tolist()) + curvature_out.append(curvatures.tolist()) + + self.outputs['Center'].sv_set(center_out) + self.outputs['Curvature'].sv_set(curvature_out) + self.outputs['Radius'].sv_set(radius_out) + +def register(): + bpy.utils.register_class(SvCurveCurvatureNode) + +def unregister(): + bpy.utils.unregister_class(SvCurveCurvatureNode) + diff --git a/nodes/curve/curve_formula.py b/nodes/curve/curve_formula.py new file mode 100644 index 0000000000..ce53965384 --- /dev/null +++ b/nodes/curve/curve_formula.py @@ -0,0 +1,170 @@ + +import numpy as np +import math + +import bpy +from bpy.props import FloatProperty, EnumProperty, BoolProperty, IntProperty, StringProperty + +from sverchok.node_tree import SverchCustomTreeNode, throttled +from sverchok.data_structure import updateNode, zip_long_repeat, fullList, match_long_repeat, ensure_nesting_level +from sverchok.utils.modules.eval_formula import get_variables, sv_compile, safe_eval_compiled +from sverchok.utils.logging import info, exception +from sverchok.utils.math import from_cylindrical, from_spherical, to_cylindrical, to_spherical +from sverchok.utils.math import coordinate_modes +from sverchok.utils.curve import SvLambdaCurve + +class SvCurveFormulaNode(bpy.types.Node, SverchCustomTreeNode): + """ + Triggers: Curve Formula + Tooltip: Generate curve by formula + """ + bl_idname = 'SvExCurveFormulaNode' + bl_label = 'Curve Formula' + bl_icon = 'OUTLINER_OB_EMPTY' + sv_icon = 'SV_CURVE_FORMULA' + + @throttled + def on_update(self, context): + self.adjust_sockets() + + formula1: StringProperty( + name = "Formula", + default = "cos(t)", + update = on_update) + + formula2: StringProperty( + name = "Formula", + default = "sin(t)", + update = on_update) + + formula3: StringProperty( + name = "Formula", + default = "t/(2*pi)", + update = on_update) + + output_mode : EnumProperty( + name = "Coordinates", + items = coordinate_modes, + default = 'XYZ', + update = updateNode) + + t_min : FloatProperty( + name = "T Min", + default = 0, + update = updateNode) + + t_max : FloatProperty( + name = "T Max", + default = 2*math.pi, + update = updateNode) + + def sv_init(self, context): + self.inputs.new('SvStringsSocket', 'TMin').prop_name = 't_min' + self.inputs.new('SvStringsSocket', 'TMax').prop_name = 't_max' + self.outputs.new('SvCurveSocket', 'Curve') + + def draw_buttons(self, context, layout): + layout.prop(self, "formula1", text="") + layout.prop(self, "formula2", text="") + layout.prop(self, "formula3", text="") + layout.label(text="Output:") + layout.prop(self, "output_mode", expand=True) + + def make_function(self, variables): + compiled1 = sv_compile(self.formula1) + compiled2 = sv_compile(self.formula2) + compiled3 = sv_compile(self.formula3) + + if self.output_mode == 'XYZ': + def out_coordinates(x, y, z): + return x, y, z + elif self.output_mode == 'CYL': + def out_coordinates(rho, phi, z): + return from_cylindrical(rho, phi, z, mode='radians') + else: # SPH + def out_coordinates(rho, phi, theta): + return from_spherical(rho, phi, theta, mode='radians') + + def function(t): + variables.update(dict(t=t)) + v1 = safe_eval_compiled(compiled1, variables) + v2 = safe_eval_compiled(compiled2, variables) + v3 = safe_eval_compiled(compiled3, variables) + return np.array(out_coordinates(v1, v2, v3)) + + return function + + def get_coordinate_variables(self): + return {'t'} + + def get_variables(self): + variables = set() + for formula in [self.formula1, self.formula2, self.formula3]: + new_vars = get_variables(formula) + variables.update(new_vars) + variables.difference_update(self.get_coordinate_variables()) + return list(sorted(list(variables))) + + def adjust_sockets(self): + variables = self.get_variables() + for key in self.inputs.keys(): + if key not in variables and key not in {'TMin', 'TMax'}: + self.debug("Input {} not in variables {}, remove it".format(key, str(variables))) + self.inputs.remove(self.inputs[key]) + for v in variables: + if v not in self.inputs: + self.debug("Variable {} not in inputs {}, add it".format(v, str(self.inputs.keys()))) + self.inputs.new('SvStringsSocket', v) + + def update(self): + if not self.formula1 and not self.formula2 and not self.formula3: + return + self.adjust_sockets() + + def get_input(self): + variables = self.get_variables() + inputs = {} + + for var in variables: + if var in self.inputs and self.inputs[var].is_linked: + inputs[var] = self.inputs[var].sv_get() + return inputs + + def process(self): + if not any(socket.is_linked for socket in self.outputs): + return + + t_min_s = self.inputs['TMin'].sv_get() + t_max_s = self.inputs['TMax'].sv_get() + t_min_s = ensure_nesting_level(t_min_s, 2) + t_max_s = ensure_nesting_level(t_max_s, 2) + + var_names = self.get_variables() + inputs = self.get_input() + input_values = [inputs.get(name, [[0]]) for name in var_names] + if var_names: + parameters = match_long_repeat([t_min_s, t_max_s] + input_values) + else: + parameters = [t_min_s, t_max_s] + + curves_out = [] + for t_mins, t_maxs, *objects in zip(*parameters): + if var_names: + var_values_s = zip_long_repeat(t_mins, t_maxs, *objects) + else: + var_values_s = zip_long_repeat(t_mins, t_maxs) + for t_min, t_max, *var_values in var_values_s: + variables = dict(zip(var_names, var_values)) + function = self.make_function(variables.copy()) + new_curve = SvLambdaCurve(function) + new_curve.u_bounds = (t_min, t_max) + curves_out.append(new_curve) + + self.outputs['Curve'].sv_set(curves_out) + +def register(): + bpy.utils.register_class(SvCurveFormulaNode) + +def unregister(): + bpy.utils.unregister_class(SvCurveFormulaNode) + diff --git a/nodes/curve/curve_frame.py b/nodes/curve/curve_frame.py new file mode 100644 index 0000000000..d043d2c784 --- /dev/null +++ b/nodes/curve/curve_frame.py @@ -0,0 +1,78 @@ +import numpy as np + +from mathutils import Matrix, Vector +import bpy +from bpy.props import FloatProperty, EnumProperty, BoolProperty, IntProperty + +from sverchok.node_tree import SverchCustomTreeNode, throttled +from sverchok.data_structure import updateNode, zip_long_repeat + +class SvCurveFrameNode(bpy.types.Node, SverchCustomTreeNode): + """ + Triggers: Curve Frame + Tooltip: Calculate (Frenet) frame matrix at any point of the curve + """ + bl_idname = 'SvExCurveFrameNode' + bl_label = 'Curve Frame' + bl_icon = 'OUTLINER_OB_EMPTY' + sv_icon = 'SV_CURVE_FRAME' + + t_value : FloatProperty( + name = "T", + default = 0.5, + update = updateNode) + + join : BoolProperty( + name = "Join", + description = "If enabled, join generated lists of matrices; otherwise, output separate list of matrices for each curve", + default = True, + update = updateNode) + + def draw_buttons(self, context, layout): + layout.prop(self, 'join', toggle=True) + + def sv_init(self, context): + self.inputs.new('SvCurveSocket', "Curve") + self.inputs.new('SvStringsSocket', "T").prop_name = 't_value' + self.outputs.new('SvMatrixSocket', 'Matrix') + self.outputs.new('SvVerticesSocket', 'Normal') + self.outputs.new('SvVerticesSocket', 'Binormal') + + def process(self): + if not any(socket.is_linked for socket in self.outputs): + return + + curve_s = self.inputs['Curve'].sv_get() + ts_s = self.inputs['T'].sv_get() + + matrix_out = [] + normals_out = [] + binormals_out = [] + for curve, ts in zip_long_repeat(curve_s, ts_s): + ts = np.array(ts) + + verts = curve.evaluate_array(ts) + matrices_np, normals, binormals = curve.frame_array(ts) + new_matrices = [] + for matrix_np, point in zip(matrices_np, verts): + matrix = Matrix(matrix_np.tolist()).to_4x4() + matrix.translation = Vector(point) + new_matrices.append(matrix) + + if self.join: + matrix_out.extend(new_matrices) + else: + matrix_out.append(new_matrices) + normals_out.append(normals.tolist()) + binormals_out.append(binormals.tolist()) + + self.outputs['Matrix'].sv_set(matrix_out) + self.outputs['Normal'].sv_set(normals_out) + self.outputs['Binormal'].sv_set(binormals_out) + +def register(): + bpy.utils.register_class(SvCurveFrameNode) + +def unregister(): + bpy.utils.unregister_class(SvCurveFrameNode) + diff --git a/nodes/curve/curve_length.py b/nodes/curve/curve_length.py new file mode 100644 index 0000000000..fa43767cc0 --- /dev/null +++ b/nodes/curve/curve_length.py @@ -0,0 +1,100 @@ +import numpy as np + +from mathutils import Matrix +import bpy +from bpy.props import FloatProperty, EnumProperty, BoolProperty, IntProperty + +from sverchok.node_tree import SverchCustomTreeNode, throttled +from sverchok.data_structure import updateNode, zip_long_repeat, ensure_nesting_level + +class SvCurveLengthNode(bpy.types.Node, SverchCustomTreeNode): + """ + Triggers: Curve Length + Tooltip: Calculate length of the curve or it's segment + """ + bl_idname = 'SvExCurveLengthNode' + bl_label = 'Curve Length' + bl_icon = 'OUTLINER_OB_EMPTY' + sv_icon = 'SV_CURVE_LENGTH' + + resolution : IntProperty( + name = 'Resolution', + min = 1, + default = 50, + update = updateNode) + + t_min : FloatProperty( + name = "T Min", + default = 0.0, + update = updateNode) + + t_max : FloatProperty( + name = "T Max", + default = 1.0, + update = updateNode) + + modes = [ + ('ABS', "Absolute", "Use absolute values of T", 0), + ('REL', "Relative", "Use relative values of T", 1) + ] + + mode : EnumProperty( + name = "T Mode", + default = 'ABS', + items = modes, + update = updateNode) + + def sv_init(self, context): + self.inputs.new('SvCurveSocket', "Curve") + self.inputs.new('SvStringsSocket', "TMin").prop_name = 't_min' + self.inputs.new('SvStringsSocket', "TMax").prop_name = 't_max' + self.inputs.new('SvStringsSocket', "Resolution").prop_name = 'resolution' + self.outputs.new('SvStringsSocket', "Length") + + def draw_buttons(self, context, layout): + layout.label(text='T mode:') + layout.prop(self, 'mode', expand=True) + + def process(self): + if not any(socket.is_linked for socket in self.outputs): + return + + curves = self.inputs['Curve'].sv_get() + t_min_s = self.inputs['TMin'].sv_get() + t_max_s = self.inputs['TMax'].sv_get() + resolution_s = self.inputs['Resolution'].sv_get() + + t_min_s = ensure_nesting_level(t_min_s, 2) + t_max_s = ensure_nesting_level(t_max_s, 2) + resolution_s = ensure_nesting_level(resolution_s, 2) + + length_out = [] + for curve, t_mins, t_maxs, resolutions in zip_long_repeat(curves, t_min_s, t_max_s, resolution_s): + for t_min, t_max, resolution in zip_long_repeat(t_mins, t_maxs, resolutions): + if self.mode == 'REL': + curve_t_min, curve_t_max = curve.get_u_bounds() + curve_t_range = curve_t_max - curve_t_min + t_min = t_min * curve_t_range + curve_t_min + t_max = t_max * curve_t_range + curve_t_min + + if t_min >= t_max: + length = 0.0 + else: + # "resolution" is for whole range of curve; + # take only part of it which corresponds to t_min...t_max segment. + curve_t_min, curve_t_max = curve.get_u_bounds() + resolution = int(resolution * (t_max - t_min) / (curve_t_max - curve_t_min)) + if resolution < 1: + resolution = 1 + length = curve.calc_length(t_min, t_max, resolution) + + length_out.append([length]) + + self.outputs['Length'].sv_set(length_out) + +def register(): + bpy.utils.register_class(SvCurveLengthNode) + +def unregister(): + bpy.utils.unregister_class(SvCurveLengthNode) + diff --git a/nodes/curve/curve_lerp.py b/nodes/curve/curve_lerp.py new file mode 100644 index 0000000000..49b04d81ac --- /dev/null +++ b/nodes/curve/curve_lerp.py @@ -0,0 +1,59 @@ + +import numpy as np + +import bpy +from bpy.props import FloatProperty, EnumProperty, BoolProperty, IntProperty + +from sverchok.node_tree import SverchCustomTreeNode, throttled +from sverchok.data_structure import updateNode, zip_long_repeat, ensure_nesting_level +from sverchok.utils.logging import info, exception +from sverchok.utils.curve import SvCurve, SvCurveLerpCurve + +class SvCurveLerpCurveNode(bpy.types.Node, SverchCustomTreeNode): + """ + Triggers: Curve Lerp + Tooltip: Generate a curve by linear interpolation of two curves + """ + bl_idname = 'SvExCurveLerpCurveNode' + bl_label = 'Curve Lerp' + bl_icon = 'MOD_THICKNESS' + + coefficient : FloatProperty( + name = "Coefficient", + default = 0.5, + update=updateNode) + + def sv_init(self, context): + self.inputs.new('SvCurveSocket', "Curve1") + self.inputs.new('SvCurveSocket', "Curve2") + self.inputs.new('SvStringsSocket', "Coefficient").prop_name = 'coefficient' + self.outputs.new('SvCurveSocket', "Curve") + + def process(self): + if not any(socket.is_linked for socket in self.outputs): + return + + curve1_s = self.inputs['Curve1'].sv_get() + curve2_s = self.inputs['Curve2'].sv_get() + coeff_s = self.inputs['Coefficient'].sv_get() + + if isinstance(curve1_s[0], SvCurve): + curve1_s = [curve1_s] + if isinstance(curve2_s[0], SvCurve): + curve2_s = [curve2_s] + coeff_s = ensure_nesting_level(coeff_s, 2) + + curves_out = [] + for curve1s, curve2s, coeffs in zip_long_repeat(curve1_s, curve2_s, coeff_s): + for curve1, curve2, coeff in zip_long_repeat(curve1s, curve2s, coeffs): + curve = SvCurveLerpCurve(curve1, curve2, coeff) + curves_out.append(curve) + + self.outputs['Curve'].sv_set(curves_out) + +def register(): + bpy.utils.register_class(SvCurveLerpCurveNode) + +def unregister(): + bpy.utils.unregister_class(SvCurveLerpCurveNode) + diff --git a/nodes/curve/curve_on_surface.py b/nodes/curve/curve_on_surface.py new file mode 100644 index 0000000000..59c480d33e --- /dev/null +++ b/nodes/curve/curve_on_surface.py @@ -0,0 +1,76 @@ + +import numpy as np + +import bpy +from bpy.props import FloatProperty, EnumProperty, BoolProperty, IntProperty + +from sverchok.node_tree import SverchCustomTreeNode, throttled +from sverchok.data_structure import updateNode, zip_long_repeat, ensure_nesting_level +from sverchok.utils.curve import SvCurve, SvCurveOnSurface +from sverchok.utils.surface import SvSurface + +class SvCurveOnSurfaceNode(bpy.types.Node, SverchCustomTreeNode): + """ + Triggers: Curve on Surface + Tooltip: Generate a curve in UV space of the surface + """ + bl_idname = 'SvExCurveOnSurfaceNode' + bl_label = 'Curve on Surface' + bl_icon = 'OUTLINER_OB_EMPTY' + sv_icon = 'SV_CURVE_ON_SURFACE' + + planes = [ + ("XY", "XY", "XOY plane", 0), + ("YZ", "YZ", "YOZ plane", 1), + ("XZ", "XZ", "XOZ plane", 2) + ] + + curve_plane: EnumProperty( + name="Curve plane", description="Curve plane", + default="XY", items=planes, update=updateNode) + + @property + def curve_axis(self): + plane = self.curve_plane + if plane == 'XY': + return 2 + elif plane == 'YZ': + return 0 + else: + return 1 + + def sv_init(self, context): + self.inputs.new('SvCurveSocket', 'Curve') + self.inputs.new('SvSurfaceSocket', "Surface") + self.outputs.new('SvCurveSocket', "Curve") + + def draw_buttons(self, context, layout): + layout.label(text="Curve plane:") + layout.prop(self, 'curve_plane', expand=True) + + def process(self): + if not any(socket.is_linked for socket in self.outputs): + return + + curve_s = self.inputs['Curve'].sv_get() + surface_s = self.inputs['Surface'].sv_get() + + if isinstance(curve_s[0], SvCurve): + curve_s = [curve_s] + if isinstance(surface_s[0], SvSurface): + surface_s = [surface_s] + + curves_out = [] + for curves, surfaces in zip_long_repeat(curve_s, surface_s): + for curve, surface in zip_long_repeat(curves, surfaces): + new_curve = SvCurveOnSurface(curve, surface, self.curve_axis) + curves_out.append(new_curve) + + self.outputs['Curve'].sv_set(curves_out) + +def register(): + bpy.utils.register_class(SvCurveOnSurfaceNode) + +def unregister(): + bpy.utils.unregister_class(SvCurveOnSurfaceNode) + diff --git a/nodes/curve/curve_range.py b/nodes/curve/curve_range.py new file mode 100644 index 0000000000..de8028104c --- /dev/null +++ b/nodes/curve/curve_range.py @@ -0,0 +1,59 @@ +import bpy +from bpy.props import FloatProperty, EnumProperty, BoolProperty, IntProperty + +from sverchok.node_tree import SverchCustomTreeNode, throttled +from sverchok.data_structure import updateNode, zip_long_repeat, ensure_nesting_level +from sverchok.utils.curve import SvCurve + +class SvCurveRangeNode(bpy.types.Node, SverchCustomTreeNode): + """ + Triggers: Curve Domain / Range + Tooltip: Output minimum and maximum values of T parameter allowed by the curve + """ + bl_idname = 'SvExCurveRangeNode' + bl_label = 'Curve Domain' + bl_icon = 'OUTLINER_OB_EMPTY' + sv_icon = 'SV_CURVE_DOMAIN' + + def sv_init(self, context): + self.inputs.new('SvCurveSocket', "Curve") + self.outputs.new('SvStringsSocket', "TMin") + self.outputs.new('SvStringsSocket', "TMax") + self.outputs.new('SvStringsSocket', "Range") + + def process(self): + if not any(socket.is_linked for socket in self.outputs): + return + + curve_s = self.inputs['Curve'].sv_get() + t_min_out = [] + t_max_out = [] + range_out = [] + + if isinstance(curve_s[0], SvCurve): + curve_s = [curve_s] + + for curves in curve_s: + t_min_new = [] + t_max_new = [] + range_new = [] + for curve in curves: + t_min, t_max = curve.get_u_bounds() + t_range = t_max - t_min + t_min_new.append(t_min) + t_max_new.append(t_max) + range_new.append(t_range) + t_min_out.append(t_min_new) + t_max_out.append(t_max_new) + range_out.append(range_new) + + self.outputs['TMin'].sv_set(t_min_out) + self.outputs['TMax'].sv_set(t_max_out) + self.outputs['Range'].sv_set(range_out) + +def register(): + bpy.utils.register_class(SvCurveRangeNode) + +def unregister(): + bpy.utils.unregister_class(SvCurveRangeNode) + diff --git a/nodes/curve/curve_segment.py b/nodes/curve/curve_segment.py new file mode 100644 index 0000000000..c93cb85b93 --- /dev/null +++ b/nodes/curve/curve_segment.py @@ -0,0 +1,71 @@ + +import numpy as np + +import bpy +from bpy.props import FloatProperty, EnumProperty, BoolProperty, IntProperty + +from sverchok.node_tree import SverchCustomTreeNode, throttled +from sverchok.data_structure import updateNode, zip_long_repeat, ensure_nesting_level +from sverchok.utils.curve import SvCurve, SvCurveSegment + +class SvCurveSegmentNode(bpy.types.Node, SverchCustomTreeNode): + """ + Triggers: Curve Segment + Tooltip: Generate a curve as a segment of another curve + """ + bl_idname = 'SvExCurveSegmentNode' + bl_label = 'Curve Segment' + bl_icon = 'OUTLINER_OB_EMPTY' + sv_icon = 'SV_CURVE_SEGMENT' + + t_min : FloatProperty( + name = "T Min", + default = 0.2, + update = updateNode) + + t_max : FloatProperty( + name = "T Max", + default = 0.8, + update = updateNode) + + rescale : BoolProperty( + name = "Rescale to 0..1", + default = False, + update = updateNode) + + def sv_init(self, context): + self.inputs.new('SvCurveSocket', "Curve") + self.inputs.new('SvStringsSocket', "TMin").prop_name = 't_min' + self.inputs.new('SvStringsSocket', "TMax").prop_name = 't_max' + self.outputs.new('SvCurveSocket', "Segment") + + def draw_buttons(self, context, layout): + layout.prop(self, "rescale", toggle=True) + + def process(self): + if not any(socket.is_linked for socket in self.outputs): + return + + curve_s = self.inputs['Curve'].sv_get() + tmin_s = self.inputs['TMin'].sv_get() + tmax_s = self.inputs['TMax'].sv_get() + + tmin_s = ensure_nesting_level(tmin_s, 2) + tmax_s = ensure_nesting_level(tmax_s, 2) + if isinstance(curve_s[0], SvCurve): + curve_s = [curve_s] + + curve_out = [] + for curves, tmins, tmaxs in zip_long_repeat(curve_s, tmin_s, tmax_s): + for curve, t_min, t_max in zip_long_repeat(curves, tmins, tmaxs): + new_curve = SvCurveSegment(curve, t_min, t_max, self.rescale) + curve_out.append(new_curve) + + self.outputs['Segment'].sv_set(curve_out) + +def register(): + bpy.utils.register_class(SvCurveSegmentNode) + +def unregister(): + bpy.utils.unregister_class(SvCurveSegmentNode) + diff --git a/nodes/curve/endpoints.py b/nodes/curve/endpoints.py new file mode 100644 index 0000000000..36965188c4 --- /dev/null +++ b/nodes/curve/endpoints.py @@ -0,0 +1,53 @@ +import bpy +from bpy.props import FloatProperty, EnumProperty, BoolProperty, IntProperty + +from sverchok.node_tree import SverchCustomTreeNode, throttled +from sverchok.data_structure import updateNode, zip_long_repeat, ensure_nesting_level +from sverchok.utils.curve import SvCurve + +class SvCurveEndpointsNode(bpy.types.Node, SverchCustomTreeNode): + """ + Triggers: Curve End Points + Tooltip: Output two endpoints of the curve + """ + bl_idname = 'SvExCurveEndpointsNode' + bl_label = 'Curve Endpoints' + bl_icon = 'OUTLINER_OB_EMPTY' + sv_icon = 'SV_CURVE_ENDPOINTS' + + def sv_init(self, context): + self.inputs.new('SvCurveSocket', "Curve") + self.outputs.new('SvVerticesSocket', "Start") + self.outputs.new('SvVerticesSocket', "End") + + def process(self): + if not any(socket.is_linked for socket in self.outputs): + return + + curve_s = self.inputs['Curve'].sv_get() + if isinstance(curve_s[0], SvCurve): + curve_s = [curve_s] + + start_out = [] + end_out = [] + for curves in curve_s: + start_new = [] + end_new = [] + for curve in curves: + t_min, t_max = curve.get_u_bounds() + start = curve.evaluate(t_min).tolist() + end = curve.evaluate(t_max).tolist() + start_new.append(start) + end_new.append(end) + start_out.append(start_new) + end_out.append(end_new) + + self.outputs['Start'].sv_set(start_out) + self.outputs['End'].sv_set(end_out) + +def register(): + bpy.utils.register_class(SvCurveEndpointsNode) + +def unregister(): + bpy.utils.unregister_class(SvCurveEndpointsNode) + diff --git a/nodes/curve/eval_curve.py b/nodes/curve/eval_curve.py new file mode 100644 index 0000000000..e426d29558 --- /dev/null +++ b/nodes/curve/eval_curve.py @@ -0,0 +1,112 @@ + +import numpy as np + +import bpy +from bpy.props import FloatProperty, EnumProperty, BoolProperty, IntProperty + +from sverchok.node_tree import SverchCustomTreeNode, throttled +from sverchok.data_structure import updateNode, zip_long_repeat, ensure_nesting_level +from sverchok.utils.curve import SvCurve + +class SvEvalCurveNode(bpy.types.Node, SverchCustomTreeNode): + """ + Triggers: Evaluate Curve + Tooltip: Evaluate Curve + """ + bl_idname = 'SvExEvalCurveNode' + bl_label = 'Evaluate Curve' + bl_icon = 'CURVE_NCURVE' + + modes = [ + ('AUTO', "Automatic", "Evaluate the curve at evenly spaced points", 0), + ('MANUAL', "Manual", "Evaluate the curve at specified points", 1) + ] + + @throttled + def update_sockets(self, context): + self.inputs['T'].hide_safe = self.eval_mode != 'MANUAL' + self.inputs['Samples'].hide_safe = self.eval_mode != 'AUTO' + + eval_mode : EnumProperty( + name = "Mode", + items = modes, + default = 'AUTO', + update = update_sockets) + + sample_size : IntProperty( + name = "Samples", + default = 50, + min = 4, + update = updateNode) + + t_value : FloatProperty( + name = "T", + default = 0.5, + update = updateNode) + + def draw_buttons(self, context, layout): + layout.prop(self, 'eval_mode', expand=True) + + def sv_init(self, context): + self.inputs.new('SvCurveSocket', "Curve") + self.inputs.new('SvStringsSocket', "T").prop_name = 't_value' + self.inputs.new('SvStringsSocket', "Samples").prop_name = 'sample_size' + self.outputs.new('SvVerticesSocket', "Vertices") + self.outputs.new('SvStringsSocket', "Edges") + self.outputs.new('SvVerticesSocket', "Tangents") + self.update_sockets(context) + + def process(self): + if not any(socket.is_linked for socket in self.outputs): + return + + curve_s = self.inputs['Curve'].sv_get() + ts_s = self.inputs['T'].sv_get(default=[[]]) + samples_s = self.inputs['Samples'].sv_get(default=[[]]) + + need_tangent = self.outputs['Tangents'].is_linked + + if isinstance(curve_s[0], SvCurve): + curve_s = [curve_s] + + ts_s = ensure_nesting_level(ts_s, 3) + samples_s = ensure_nesting_level(samples_s, 2) + + verts_out = [] + edges_out = [] + tangents_out = [] + for curves, ts_i, samples_i in zip_long_repeat(curve_s, ts_s, samples_s): + if self.eval_mode == 'AUTO': + ts_i = [None] + else: + samples_i = [None] + for curve, ts, samples in zip_long_repeat(curves, ts_i, samples_i): + if self.eval_mode == 'AUTO': + t_min, t_max = curve.get_u_bounds() + ts = np.linspace(t_min, t_max, num=samples, dtype=np.float64) + else: + ts = np.array(ts) + #ts = np.array(ts)[np.newaxis].T + #print(ts.shape) + + new_verts = curve.evaluate_array(ts) + new_verts = new_verts.tolist() + n = len(ts) + new_edges = [(i,i+1) for i in range(n-1)] + + verts_out.append(new_verts) + edges_out.append(new_edges) + if need_tangent: + new_tangents = curve.tangent_array(ts).tolist() + tangents_out.append(new_tangents) + + self.outputs['Vertices'].sv_set(verts_out) + self.outputs['Edges'].sv_set(edges_out) + self.outputs['Tangents'].sv_set(tangents_out) + +def register(): + bpy.utils.register_class(SvEvalCurveNode) + +def unregister(): + bpy.utils.unregister_class(SvEvalCurveNode) + diff --git a/nodes/curve/fillet_polyline.py b/nodes/curve/fillet_polyline.py new file mode 100644 index 0000000000..ce3ca031bb --- /dev/null +++ b/nodes/curve/fillet_polyline.py @@ -0,0 +1,124 @@ + +import numpy as np + +import bpy +from bpy.props import FloatProperty, EnumProperty, BoolProperty, IntProperty + +from sverchok.node_tree import SverchCustomTreeNode, throttled +from sverchok.data_structure import updateNode, zip_long_repeat, repeat_last_for_length, ensure_nesting_level +from sverchok.utils.logging import info, exception +from sverchok.utils.curve import SvLine, SvConcatCurve +from sverchok.utils.fillet import calc_fillet + +class SvFilletPolylineNode(bpy.types.Node, SverchCustomTreeNode): + """ + Triggers: Arc Fillet Polyline + Tooltip: Generate a polyline with arc fillets + """ + bl_idname = 'SvExFilletPolylineNode' + bl_label = 'Fillet Polyline' + bl_icon = 'OUTLINER_OB_EMPTY' + sv_icon = 'SV_FILLET_POLYLINE' + + radius : FloatProperty( + name = "Radius", + min = 0.0, + default = 0.2, + update = updateNode) + + concat : BoolProperty( + name = "Concatenate", + default = True, + update = updateNode) + + cyclic : BoolProperty( + name = "Cyclic", + default = False, + update = updateNode) + + scale_to_unit : BoolProperty( + name = "Even domains", + description = "Give each segment and each arc equal T parameter domain of [0; 1]", + default = False, + update = updateNode) + + def draw_buttons(self, context, layout): + layout.prop(self, "concat", toggle=True) + if self.concat: + layout.prop(self, "scale_to_unit", toggle=True) + layout.prop(self, "cyclic", toggle=True) + + def sv_init(self, context): + self.inputs.new('SvVerticesSocket', "Vertices") + self.inputs.new('SvStringsSocket', "Radius").prop_name = 'radius' + self.outputs.new('SvCurveSocket', "Curve") + self.outputs.new('SvMatrixSocket', "Centers") + + def make_curve(self, vertices, radiuses): + if self.cyclic: + last_fillet = calc_fillet(vertices[-1], vertices[0], vertices[1], radiuses[0]) + prev_edge_start = last_fillet.p2 + radiuses = radiuses[1:] + [radiuses[0]] + corners = list(zip(vertices, vertices[1:], vertices[2:], radiuses)) + corners.append((vertices[-2], vertices[-1], vertices[0], radiuses[-1])) + corners.append((vertices[-1], vertices[0], vertices[1], radiuses[0])) + else: + prev_edge_start = vertices[0] + corners = zip(vertices, vertices[1:], vertices[2:], radiuses) + + curves = [] + centers = [] + for v1, v2, v3, radius in corners: + fillet = calc_fillet(v1, v2, v3, radius) + edge_direction = np.array(fillet.p1) - np.array(prev_edge_start) + edge_len = np.linalg.norm(edge_direction) + edge = SvLine(prev_edge_start, edge_direction / edge_len) + edge.u_bounds = (0.0, edge_len) + arc = fillet.get_curve() + prev_edge_start = fillet.p2 + curves.append(edge) + curves.append(arc) + centers.append(fillet.matrix) + + if not self.cyclic: + edge_direction = np.array(vertices[-1]) - np.array(prev_edge_start) + edge_len = np.linalg.norm(edge_direction) + edge = SvLine(prev_edge_start, edge_direction / edge_len) + edge.u_bounds = (0.0, edge_len) + curves.append(edge) + + if self.concat: + concat = SvConcatCurve(curves, scale_to_unit = self.scale_to_unit) + return concat, centers + else: + return curves, centers + + def process(self): + if not any(socket.is_linked for socket in self.outputs): + return + + verts_s = self.inputs['Vertices'].sv_get() + radius_s = self.inputs['Radius'].sv_get() + + verts_s = ensure_nesting_level(verts_s, 3) + radius_s = ensure_nesting_level(radius_s, 2) + + curves_out = [] + centers_out = [] + for vertices, radiuses in zip_long_repeat(verts_s, radius_s): + if len(vertices) < 3: + raise Exception("At least three vertices are required to make a fillet") + radiuses = repeat_last_for_length(radiuses, len(vertices)) + curve, centers = self.make_curve(vertices, radiuses) + curves_out.append(curve) + centers_out.append(centers) + + self.outputs['Curve'].sv_set(curves_out) + self.outputs['Centers'].sv_set(centers_out) + +def register(): + bpy.utils.register_class(SvFilletPolylineNode) + +def unregister(): + bpy.utils.unregister_class(SvFilletPolylineNode) + diff --git a/nodes/curve/flip_curve.py b/nodes/curve/flip_curve.py new file mode 100644 index 0000000000..5de5341104 --- /dev/null +++ b/nodes/curve/flip_curve.py @@ -0,0 +1,54 @@ + +import numpy as np + +import bpy +from bpy.props import FloatProperty, EnumProperty, BoolProperty, IntProperty + +from sverchok.node_tree import SverchCustomTreeNode, throttled +from sverchok.data_structure import updateNode, zip_long_repeat, ensure_nesting_level +from sverchok.utils.curve import SvCurve, SvFlipCurve + +class SvFlipCurveNode(bpy.types.Node, SverchCustomTreeNode): + """ + Triggers: Flip Curve + Tooltip: Reverse parameterization of the curve - swap the beginning and the end of the curve + """ + bl_idname = 'SvExFlipCurveNode' + bl_label = 'Flip Curve' + bl_icon = 'OUTLINER_OB_EMPTY' + sv_icon = 'SV_FLIP_CURVE' + + def sv_init(self, context): + self.inputs.new('SvCurveSocket', "Curve") + self.outputs.new('SvCurveSocket', "Curve") + + def process(self): + if not any(socket.is_linked for socket in self.outputs): + return + + curve_s = self.inputs['Curve'].sv_get() + if isinstance(curve_s[0], SvCurve): + out_level = 1 + curve_s = [curve_s] + else: + out_level = 2 + + curves_out = [] + for curves in curve_s: + new_curves = [] + for curve in curves: + new_curve = SvFlipCurve(curve) + new_curves.append(new_curve) + if out_level == 1: + curves_out.extend(new_curves) + else: + curves_out.append(new_curve) + + self.outputs['Curve'].sv_set(curves_out) + +def register(): + bpy.utils.register_class(SvFlipCurveNode) + +def unregister(): + bpy.utils.unregister_class(SvFlipCurveNode) + diff --git a/nodes/curve/iso_uv_curve.py b/nodes/curve/iso_uv_curve.py new file mode 100644 index 0000000000..6a5a3d5ba4 --- /dev/null +++ b/nodes/curve/iso_uv_curve.py @@ -0,0 +1,61 @@ + +import numpy as np + +import bpy +from bpy.props import FloatProperty, EnumProperty, BoolProperty, IntProperty + +from sverchok.node_tree import SverchCustomTreeNode, throttled +from sverchok.data_structure import updateNode, zip_long_repeat, ensure_nesting_level +from sverchok.utils.curve import SvCurve, SvIsoUvCurve +from sverchok.utils.surface import SvSurface + +class SvIsoUvCurveNode(bpy.types.Node, SverchCustomTreeNode): + """ + Triggers: Iso UV Curve + Tooltip: Generate a curve which is characterized by constant value of U or V parameter in surface's UV space + """ + bl_idname = 'SvExIsoUvCurveNode' + bl_label = 'Iso U/V Curve' + bl_icon = 'OUTLINER_OB_EMPTY' + sv_icon = 'SV_UV_ISO_CURVE' + + value : FloatProperty( + name = "Value", + default = 0.5, + update = updateNode) + + def sv_init(self, context): + self.inputs.new('SvSurfaceSocket', "Surface") + self.inputs.new('SvStringsSocket', "Value").prop_name = 'value' + self.outputs.new('SvCurveSocket', "UCurve") + self.outputs.new('SvCurveSocket', "VCurve") + + def process(self): + if not any(socket.is_linked for socket in self.outputs): + return + + surface_s = self.inputs['Surface'].sv_get() + value_s = self.inputs['Value'].sv_get() + + if isinstance(surface_s[0], SvSurface): + surface_s = [surface_s] + value_s = ensure_nesting_level(value_s, 2) + + u_curves_out = [] + v_curves_out = [] + for surfaces, values in zip_long_repeat(surface_s, value_s): + for surface, value in zip_long_repeat(surfaces, values): + u_curve = SvIsoUvCurve(surface, 'V', value) + v_curve = SvIsoUvCurve(surface, 'U', value) + u_curves_out.append(u_curve) + v_curves_out.append(v_curve) + + self.outputs['UCurve'].sv_set(u_curves_out) + self.outputs['VCurve'].sv_set(v_curves_out) + +def register(): + bpy.utils.register_class(SvIsoUvCurveNode) + +def unregister(): + bpy.utils.unregister_class(SvIsoUvCurveNode) + diff --git a/nodes/curve/length_parameter.py b/nodes/curve/length_parameter.py new file mode 100644 index 0000000000..2224bf31be --- /dev/null +++ b/nodes/curve/length_parameter.py @@ -0,0 +1,124 @@ +import numpy as np + +from mathutils import Matrix +import bpy +from bpy.props import FloatProperty, EnumProperty, BoolProperty, IntProperty + +from sverchok.node_tree import SverchCustomTreeNode, throttled +from sverchok.data_structure import updateNode, zip_long_repeat, ensure_nesting_level +from sverchok.utils.curve import SvCurveLengthSolver + +class SvCurveLengthParameterNode(bpy.types.Node, SverchCustomTreeNode): + """ + Triggers: Curve Length Parameter + Tooltip: Solve curve length (natural) parameter + """ + bl_idname = 'SvExCurveLengthParameterNode' + bl_label = 'Curve Length Parameter' + bl_icon = 'OUTLINER_OB_EMPTY' + sv_icon = 'SV_CURVE_LENGTH_P' + + resolution : IntProperty( + name = 'Resolution', + min = 1, + default = 50, + update = updateNode) + + length : FloatProperty( + name = "Length", + min = 0.0, + default = 0.5, + update = updateNode) + + modes = [('SPL', 'Cubic', "Cubic Spline", 0), + ('LIN', 'Linear', "Linear Interpolation", 1)] + + mode: EnumProperty(name='Interpolation mode', default="SPL", items=modes, update=updateNode) + + @throttled + def update_sockets(self, context): + self.inputs['Length'].hide_safe = self.eval_mode != 'MANUAL' + self.inputs['Samples'].hide_safe = self.eval_mode != 'AUTO' + + eval_modes = [ + ('AUTO', "Automatic", "Evaluate the curve at evenly spaced points", 0), + ('MANUAL', "Manual", "Evaluate the curve at specified points", 1) + ] + + eval_mode : EnumProperty( + name = "Mode", + items = eval_modes, + default = 'AUTO', + update = update_sockets) + + sample_size : IntProperty( + name = "Samples", + default = 50, + min = 4, + update = updateNode) + + def sv_init(self, context): + self.inputs.new('SvCurveSocket', "Curve") + self.inputs.new('SvStringsSocket', "Resolution").prop_name = 'resolution' + self.inputs.new('SvStringsSocket', "Length").prop_name = 'length' + self.inputs.new('SvStringsSocket', "Samples").prop_name = 'sample_size' + self.outputs.new('SvStringsSocket', "T") + self.outputs.new('SvVerticesSocket', "Vertices") + self.update_sockets(context) + + def draw_buttons(self, context, layout): + layout.prop(self, 'mode', expand=True) + + def draw_buttons_ext(self, context, layout): + self.draw_buttons(context, layout) + layout.prop(self, 'eval_mode', expand=True) + + def process(self): + + if not any((s.is_linked for s in self.outputs)): + return + + need_eval = self.outputs['Vertices'].is_linked + + curves = self.inputs['Curve'].sv_get() + resolution_s = self.inputs['Resolution'].sv_get() + length_s = self.inputs['Length'].sv_get() + samples_s = self.inputs['Samples'].sv_get(default=[[]]) + + length_s = ensure_nesting_level(length_s, 2) + + ts_out = [] + verts_out = [] + for curve, resolution, input_lengths, samples, in zip_long_repeat(curves, resolution_s, length_s, samples_s): + if self.eval_mode == 'AUTO': + if isinstance(samples, (list, tuple)): + samples = samples[0] + + if isinstance(resolution, (list, tuple)): + resolution = resolution[0] + + solver = SvCurveLengthSolver(curve) + solver.prepare(self.mode, resolution) + + if self.eval_mode == 'AUTO': + total_length = solver.get_total_length() + input_lengths = np.linspace(0.0, total_length, num = samples) + else: + input_lengths = np.array(input_lengths) + + ts = solver.solve(input_lengths) + + ts_out.append(ts.tolist()) + if need_eval: + verts = curve.evaluate_array(ts).tolist() + verts_out.append(verts) + + self.outputs['T'].sv_set(ts_out) + self.outputs['Vertices'].sv_set(verts_out) + +def register(): + bpy.utils.register_class(SvCurveLengthParameterNode) + +def unregister(): + bpy.utils.unregister_class(SvCurveLengthParameterNode) + diff --git a/nodes/curve/line.py b/nodes/curve/line.py new file mode 100644 index 0000000000..a1e28dcb64 --- /dev/null +++ b/nodes/curve/line.py @@ -0,0 +1,100 @@ + +import numpy as np + +import bpy +from bpy.props import FloatProperty, EnumProperty, BoolProperty, IntProperty + +from sverchok.node_tree import SverchCustomTreeNode, throttled +from sverchok.data_structure import updateNode, zip_long_repeat, ensure_nesting_level +from sverchok.utils.curve import SvLine + +class SvLineCurveNode(bpy.types.Node, SverchCustomTreeNode): + """ + Triggers: Line Segment + Tooltip: Generate straight line curve object + """ + bl_idname = 'SvExLineCurveNode' + bl_label = 'Line (Curve)' + bl_icon = 'GRIP' + sv_icon = 'SV_LINE' + + modes = [ + ('DIR', "Point and direction", "Point and direction", 0), + ('AB', "Two points", "Two points", 1) + ] + + @throttled + def update_sockets(self, context): + self.inputs['Point2'].hide_safe = self.mode != 'AB' + self.inputs['Direction'].hide_safe = self.mode != 'DIR' + + mode : EnumProperty( + name = "Mode", + items = modes, + default = 'DIR', + update = update_sockets) + + u_min : FloatProperty( + name = "U Min", + default = 0.0, + update = updateNode) + + + u_max : FloatProperty( + name = "U Max", + default = 1.0, + update = updateNode) + + def sv_init(self, context): + p = self.inputs.new('SvVerticesSocket', "Point1") + p.use_prop = True + p.prop = (0.0, 0.0, 0.0) + p = self.inputs.new('SvVerticesSocket', "Point2") + p.use_prop = True + p.prop = (1.0, 0.0, 0.0) + p = self.inputs.new('SvVerticesSocket', "Direction") + p.use_prop = True + p.prop = (1.0, 0.0, 0.0) + self.inputs.new('SvStringsSocket', "UMin").prop_name = 'u_min' + self.inputs.new('SvStringsSocket', "UMax").prop_name = 'u_max' + self.outputs.new('SvCurveSocket', "Curve") + self.update_sockets(context) + + def draw_buttons(self, context, layout): + layout.prop(self, "mode", text="") + + def process(self): + if not any(socket.is_linked for socket in self.outputs): + return + + point1_s = self.inputs['Point1'].sv_get() + point2_s = self.inputs['Point2'].sv_get() + direction_s = self.inputs['Direction'].sv_get() + u_min_s = self.inputs['UMin'].sv_get() + u_max_s = self.inputs['UMax'].sv_get() + + point1_s = ensure_nesting_level(point1_s, 3) + point2_s = ensure_nesting_level(point2_s, 3) + direction_s = ensure_nesting_level(direction_s, 3) + u_min_s = ensure_nesting_level(u_min_s, 2) + u_max_s = ensure_nesting_level(u_max_s, 2) + + curves_out = [] + for point1s, point2s, directions, u_mins, u_maxs in zip_long_repeat(point1_s, point2_s, direction_s, u_min_s, u_max_s): + for point1, point2, direction, u_min, u_max in zip_long_repeat(point1s, point2s, directions, u_mins, u_maxs): + point1 = np.array(point1) + if self.mode == 'AB': + direction = np.array(point2) - point1 + + line = SvLine(point1, direction) + line.u_bounds = (u_min, u_max) + curves_out.append(line) + + self.outputs['Curve'].sv_set(curves_out) + +def register(): + bpy.utils.register_class(SvLineCurveNode) + +def unregister(): + bpy.utils.unregister_class(SvLineCurveNode) + diff --git a/nodes/curve/polyline.py b/nodes/curve/polyline.py new file mode 100644 index 0000000000..ba4375bcc2 --- /dev/null +++ b/nodes/curve/polyline.py @@ -0,0 +1,70 @@ + +import bpy +from bpy.props import FloatProperty, EnumProperty, BoolProperty, IntProperty + +from sverchok.node_tree import SverchCustomTreeNode, throttled +from sverchok.data_structure import updateNode, zip_long_repeat +from sverchok.utils.geom import LinearSpline, CubicSpline +from sverchok.utils.curve import SvSplineCurve + +class SvPolylineNode(bpy.types.Node, SverchCustomTreeNode): + """ + Triggers: Polyline + Tooltip: Generate segments of straight lines to connect several points + """ + bl_idname = 'SvExPolylineNode' + bl_label = 'Polyline' + bl_icon = 'OUTLINER_OB_EMPTY' + sv_icon = 'SV_POLYLINE' + + is_cyclic : BoolProperty(name = "Cyclic", + description = "Whether the polyline is cyclic", + default = False, + update=updateNode) + + metrics = [ + ('MANHATTAN', 'Manhattan', "Manhattan distance metric", 0), + ('DISTANCE', 'Euclidan', "Eudlcian distance metric", 1), + ('POINTS', 'Points', "Points based", 2), + ('CHEBYSHEV', 'Chebyshev', "Chebyshev distance", 3)] + + metric: EnumProperty(name='Metric', + description = "Knot mode", + default="DISTANCE", items=metrics, + update=updateNode) + + def draw_buttons(self, context, layout): + layout.prop(self, "is_cyclic", toggle=True) + + def draw_buttons_ext(self, context, layout): + self.draw_buttons(context, layout) + layout.prop(self, 'metric') + + def sv_init(self, context): + self.inputs.new('SvVerticesSocket', "Vertices") + self.outputs.new('SvCurveSocket', "Curve") + + def build_spline(self, path): + spline = LinearSpline(path, metric = self.metric, is_cyclic = self.is_cyclic) + return spline + + def process(self): + if not any(o.is_linked for o in self.outputs): + return + + vertices_s = self.inputs['Vertices'].sv_get(default=[[]]) + + out_curves = [] + for vertices in vertices_s: + spline = self.build_spline(vertices) + curve = SvSplineCurve(spline) + out_curves.append(curve) + + self.outputs['Curve'].sv_set(out_curves) + +def register(): + bpy.utils.register_class(SvPolylineNode) + +def unregister(): + bpy.utils.unregister_class(SvPolylineNode) + diff --git a/nodes/curve/surface_boundary.py b/nodes/curve/surface_boundary.py new file mode 100644 index 0000000000..c03cab2ed8 --- /dev/null +++ b/nodes/curve/surface_boundary.py @@ -0,0 +1,80 @@ + +import numpy as np + +import bpy +from bpy.props import FloatProperty, EnumProperty, BoolProperty, IntProperty + +from sverchok.node_tree import SverchCustomTreeNode, throttled +from sverchok.data_structure import updateNode, zip_long_repeat, ensure_nesting_level +from sverchok.utils.curve import SvCurve, SvIsoUvCurve, SvConcatCurve +from sverchok.utils.surface import SvSurface + +class SvSurfaceBoundaryNode(bpy.types.Node, SverchCustomTreeNode): + """ + Triggers: Surface Boundary + Tooltip: Generate a curve from curve's boundary + """ + bl_idname = 'SvExSurfaceBoundaryNode' + bl_label = 'Surface Boundary' + bl_icon = 'OUTLINER_OB_EMPTY' + sv_icon = 'SV_SURFACE_BOUNDARY' + + modes = [ + ('NO', "Plain", "Non-cyclic surface (similar to plane)", 0), + ('U', "U Cyclic", "The surface is cyclic in the U direction", 1), + ('V', "V Cyclic", "The surface is cyclic in the V direction", 2) + ] + + cyclic_mode : EnumProperty( + name = "Cyclic surface", + items = modes, + default = 'NO', + update = updateNode) + + def sv_init(self, context): + self.inputs.new('SvSurfaceSocket', "Surface") + self.outputs.new('SvCurveSocket', "Boundary") + + def draw_buttons(self, context, layout): + layout.label(text="Cyclic:") + layout.prop(self, 'cyclic_mode', text='') + + def process(self): + if not any(socket.is_linked for socket in self.outputs): + return + + surface_s = self.inputs['Surface'].sv_get() + if isinstance(surface_s[0], SvSurface): + surface_s = [surface_s] + + curves_out = [] + for surfaces in surface_s: + for surface in surfaces: + u_min, u_max = surface.get_u_min(), surface.get_u_max() + v_min, v_max = surface.get_v_min(), surface.get_v_max() + if self.cyclic_mode == 'NO': + curve1 = SvIsoUvCurve(surface, 'V', v_min, flip=False) + curve2 = SvIsoUvCurve(surface, 'U', u_max, flip=False) + curve3 = SvIsoUvCurve(surface, 'V', v_max, flip=True) + curve4 = SvIsoUvCurve(surface, 'U', u_min, flip=True) + new_curves = [SvConcatCurve([curve1, curve2, curve3, curve4])] + elif self.cyclic_mode == 'U': + curve1 = SvIsoUvCurve(surface, 'V', v_max, flip=False) + curve2 = SvIsoUvCurve(surface, 'V', v_min, flip=False) + new_curves = [curve1, curve2] + elif self.cyclic_mode == 'V': + curve1 = SvIsoUvCurve(surface, 'U', u_max, flip=False) + curve2 = SvIsoUvCurve(surface, 'U', u_min, flip=False) + new_curves = [curve1, curve2] + else: + raise Exception("Unsupported mode") + curves_out.append(new_curves) + + self.outputs['Boundary'].sv_set(curves_out) + +def register(): + bpy.utils.register_class(SvSurfaceBoundaryNode) + +def unregister(): + bpy.utils.unregister_class(SvSurfaceBoundaryNode) + diff --git a/nodes/curve/torsion.py b/nodes/curve/torsion.py new file mode 100644 index 0000000000..0fd92ca740 --- /dev/null +++ b/nodes/curve/torsion.py @@ -0,0 +1,49 @@ +import numpy as np + +from mathutils import Matrix, Vector +import bpy +from bpy.props import FloatProperty, EnumProperty, BoolProperty, IntProperty + +from sverchok.node_tree import SverchCustomTreeNode, throttled +from sverchok.data_structure import updateNode, zip_long_repeat + +class SvCurveTorsionNode(bpy.types.Node, SverchCustomTreeNode): + """ + Triggers: Curve Torsion / Twist + Tooltip: Calculate torsion of the curve at given parameter value + """ + bl_idname = 'SvExCurveTorsionNode' + bl_label = 'Curve Torsion' + bl_icon = 'CURVE_NCURVE' + + t_value : FloatProperty( + name = "T", + default = 0.5, + update = updateNode) + + def sv_init(self, context): + self.inputs.new('SvCurveSocket', "Curve") + self.inputs.new('SvStringsSocket', "T").prop_name = 't_value' + self.outputs.new('SvStringsSocket', "Torsion") + + def process(self): + if not any(socket.is_linked for socket in self.outputs): + return + + curve_s = self.inputs['Curve'].sv_get() + ts_s = self.inputs['T'].sv_get() + + torsion_out = [] + for curve, ts in zip_long_repeat(curve_s, ts_s): + ts = np.array(ts) + torsions = curve.torsion_array(ts) + torsion_out.append(torsions.tolist()) + + self.outputs['Torsion'].sv_set(torsion_out) + +def register(): + bpy.utils.register_class(SvCurveTorsionNode) + +def unregister(): + bpy.utils.unregister_class(SvCurveTorsionNode) + diff --git a/nodes/curve/zero_twist_frame.py b/nodes/curve/zero_twist_frame.py new file mode 100644 index 0000000000..90cc7fef8a --- /dev/null +++ b/nodes/curve/zero_twist_frame.py @@ -0,0 +1,91 @@ +import numpy as np + +from mathutils import Matrix, Vector +import bpy +from bpy.props import FloatProperty, EnumProperty, BoolProperty, IntProperty + +from sverchok.node_tree import SverchCustomTreeNode, throttled +from sverchok.data_structure import updateNode, zip_long_repeat + +class SvCurveZeroTwistFrameNode(bpy.types.Node, SverchCustomTreeNode): + """ + Triggers: Curve Zero-Twist Frame + Tooltip: Calculate Zero-Twist Perpendicular frame for curve + """ + bl_idname = 'SvExCurveZeroTwistFrameNode' + bl_label = 'Curve Zero-Twist Frame' + bl_icon = 'OUTLINER_OB_EMPTY' + sv_icon = 'SV_CURVE_FRAME' + + resolution : IntProperty( + name = "Resolution", + min = 10, default = 50, + update = updateNode) + + t_value : FloatProperty( + name = "T", + default = 0.5, + update = updateNode) + + join : BoolProperty( + name = "Join", + description = "If enabled, join generated lists of matrices; otherwise, output separate list of matrices for each curve", + default = True, + update = updateNode) + + def draw_buttons(self, context, layout): + layout.prop(self, 'join', toggle=True) + + def sv_init(self, context): + self.inputs.new('SvCurveSocket', "Curve") + self.inputs.new('SvStringsSocket', "Resolution").prop_name = 'resolution' + self.inputs.new('SvStringsSocket', "T").prop_name = 't_value' + self.outputs.new('SvStringsSocket', "CumulativeTorsion") + self.outputs.new('SvMatrixSocket', 'Matrix') + + def process(self): + if not any(socket.is_linked for socket in self.outputs): + return + + curve_s = self.inputs['Curve'].sv_get() + ts_s = self.inputs['T'].sv_get() + resolution_s = self.inputs['Resolution'].sv_get() + + torsion_out = [] + matrix_out = [] + for curve, resolution, ts in zip_long_repeat(curve_s, resolution_s, ts_s): + if isinstance(resolution, (list, tuple)): + resolution = resolution[0] + + ts = np.array(ts) + + vectors = curve.evaluate_array(ts) + matrices_np, normals, binormals = curve.frame_array(ts) + + curve.pre_calc_torsion_integral(resolution) + integral = curve.torsion_integral(ts) + + new_matrices = [] + for matrix_np, point, angle in zip(matrices_np, vectors, integral): + frenet_matrix = Matrix(matrix_np.tolist()).to_4x4() + rotation_matrix = Matrix.Rotation(-angle, 4, 'Z') + #print("Z:", rotation_matrix) + matrix = frenet_matrix @ rotation_matrix + matrix.translation = Vector(point) + new_matrices.append(matrix) + + torsion_out.append(integral.tolist()) + if self.join: + matrix_out.extend(new_matrices) + else: + matrix_out.append(new_matrices) + + self.outputs['CumulativeTorsion'].sv_set(torsion_out) + self.outputs['Matrix'].sv_set(matrix_out) + +def register(): + bpy.utils.register_class(SvCurveZeroTwistFrameNode) + +def unregister(): + bpy.utils.unregister_class(SvCurveZeroTwistFrameNode) + diff --git a/nodes/field/__init__.py b/nodes/field/__init__.py new file mode 100644 index 0000000000..143f486c05 --- /dev/null +++ b/nodes/field/__init__.py @@ -0,0 +1 @@ +# __init__.py diff --git a/nodes/field/attractor_field.py b/nodes/field/attractor_field.py new file mode 100644 index 0000000000..3ef422f24a --- /dev/null +++ b/nodes/field/attractor_field.py @@ -0,0 +1,187 @@ + +import numpy as np + +import bpy +from bpy.props import FloatProperty, EnumProperty, BoolProperty, IntProperty, StringProperty +from mathutils import kdtree +from mathutils import bvhtree + +from sverchok.node_tree import SverchCustomTreeNode, throttled +from sverchok.data_structure import updateNode, zip_long_repeat, fullList, match_long_repeat +from sverchok.utils.logging import info, exception + +from sverchok.utils.field.scalar import (SvScalarFieldPointDistance, + SvMergedScalarField, SvKdtScalarField, + SvLineAttractorScalarField, SvPlaneAttractorScalarField, + SvBvhAttractorScalarField) +from sverchok.utils.field.vector import (SvVectorFieldPointDistance, + SvAverageVectorField, SvKdtVectorField, + SvLineAttractorVectorField, SvPlaneAttractorVectorField, + SvBvhAttractorVectorField) +from sverchok.utils.math import falloff_types, falloff_array + +class SvAttractorFieldNode(bpy.types.Node, SverchCustomTreeNode): + """ + Triggers: Attractor Field + Tooltip: Generate scalar and vector attraction fields + """ + bl_idname = 'SvExAttractorFieldNode' + bl_label = 'Attractor Field' + bl_icon = 'OUTLINER_OB_EMPTY' + sv_icon = 'SV_EX_ATTRACT' + + @throttled + def update_type(self, context): + self.inputs['Direction'].hide_safe = (self.attractor_type in ['Point', 'Mesh']) + self.inputs['Amplitude'].hide_safe = (self.falloff_type == 'NONE') + self.inputs['Coefficient'].hide_safe = (self.falloff_type not in ['inverse_exp', 'gauss']) + self.inputs['Faces'].hide_safe = (self.attractor_type != 'Mesh') + + falloff_type: EnumProperty( + name="Falloff type", items=falloff_types, default='NONE', update=update_type) + + amplitude: FloatProperty( + name="Amplitude", default=0.5, min=0.0, update=updateNode) + + coefficient: FloatProperty( + name="Coefficient", default=0.5, update=updateNode) + + clamp: BoolProperty( + name="Clamp", description="Restrict coefficient with R", default=False, update=updateNode) + + types = [ + ("Point", "Point", "Attraction to single or multiple points", 0), + ("Line", "Line", "Attraction to straight line", 1), + ("Plane", "Plane", "Attraction to plane", 2), + ("Mesh", "Mesh", "Attraction to mesh", 3) + ] + + attractor_type: EnumProperty( + name="Attractor type", items=types, default='Point', update=update_type) + + point_modes = [ + ('AVG', "Average", "Use average distance to all attraction centers", 0), + ('MIN', "Nearest", "Use minimum distance to any of attraction centers", 1) + ] + + point_mode : EnumProperty( + name = "Points mode", + description = "How to define the distance when multiple attraction centers are used", + items = point_modes, + default = 'AVG', + update = updateNode) + + signed : BoolProperty( + name = "Signed", + default = False, + update = updateNode) + + def sv_init(self, context): + d = self.inputs.new('SvVerticesSocket', "Center") + d.use_prop = True + d.prop = (0.0, 0.0, 0.0) + + d = self.inputs.new('SvVerticesSocket', "Direction") + d.use_prop = True + d.prop = (0.0, 0.0, 1.0) + + self.inputs.new('SvStringsSocket', 'Faces') + self.inputs.new('SvStringsSocket', 'Amplitude').prop_name = 'amplitude' + self.inputs.new('SvStringsSocket', 'Coefficient').prop_name = 'coefficient' + + self.outputs.new('SvVectorFieldSocket', "VField") + self.outputs.new('SvScalarFieldSocket', "SField") + self.update_type(context) + + def draw_buttons(self, context, layout): + layout.prop(self, 'attractor_type') + if self.attractor_type == 'Point': + layout.prop(self, 'point_mode') + elif self.attractor_type == 'Mesh': + layout.prop(self, 'signed', toggle=True) + layout.prop(self, 'falloff_type') + layout.prop(self, 'clamp') + + def to_point(self, centers, falloff): + n = len(centers) + if n == 1: + sfield = SvScalarFieldPointDistance(centers[0], falloff=falloff) + vfield = SvVectorFieldPointDistance(centers[0], falloff=falloff) + elif self.point_mode == 'AVG': + sfields = [SvScalarFieldPointDistance(center, falloff=falloff) for center in centers] + sfield = SvMergedScalarField('AVG', sfields) + vfields = [SvVectorFieldPointDistance(center, falloff=falloff) for center in centers] + vfield = SvAverageVectorField(vfields) + else: # MIN + kdt = kdtree.KDTree(len(centers)) + for i, v in enumerate(centers): + kdt.insert(v, i) + kdt.balance() + vfield = SvKdtVectorField(kdt=kdt, falloff=falloff) + sfield = SvKdtScalarField(kdt=kdt, falloff=falloff) + return vfield, sfield + + def to_line(self, center, direction, falloff): + sfield = SvLineAttractorScalarField(np.array(center), np.array(direction), falloff=falloff) + vfield = SvLineAttractorVectorField(np.array(center), np.array(direction), falloff=falloff) + return vfield, sfield + + def to_plane(self, center, direction, falloff): + sfield = SvPlaneAttractorScalarField(np.array(center), np.array(direction), falloff=falloff) + vfield = SvPlaneAttractorVectorField(np.array(center), np.array(direction), falloff=falloff) + return vfield, sfield + + def to_mesh(self, verts, faces, falloff): + bvh = bvhtree.BVHTree.FromPolygons(verts, faces) + sfield = SvBvhAttractorScalarField(bvh=bvh, falloff=falloff, signed=self.signed) + vfield = SvBvhAttractorVectorField(bvh=bvh, falloff=falloff) + return vfield, sfield + + def process(self): + if not any(socket.is_linked for socket in self.outputs): + return + + center_s = self.inputs['Center'].sv_get() + faces_s = self.inputs['Faces'].sv_get(default=[[]]) + directions_s = self.inputs['Direction'].sv_get() + amplitudes_s = self.inputs['Amplitude'].sv_get() + coefficients_s = self.inputs['Coefficient'].sv_get() + + vfields_out = [] + sfields_out = [] + + objects = zip_long_repeat(center_s, faces_s, directions_s, amplitudes_s, coefficients_s) + for centers, faces, direction, amplitude, coefficient in objects: + if isinstance(amplitude, (list, tuple)): + amplitude = amplitude[0] + if isinstance(coefficient, (list, tuple)): + coefficient = coefficient[0] + + if self.falloff_type == 'NONE': + falloff_func = None + else: + falloff_func = falloff_array(self.falloff_type, amplitude, coefficient, self.clamp) + + if self.attractor_type == 'Point': + vfield, sfield = self.to_point(centers, falloff_func) + elif self.attractor_type == 'Line': + vfield, sfield = self.to_line(centers[0], direction[0], falloff_func) + elif self.attractor_type == 'Plane': + vfield, sfield = self.to_plane(centers[0], direction[0], falloff_func) + elif self.attractor_type == 'Mesh': + vfield, sfield = self.to_mesh(centers, faces, falloff_func) + else: + raise Exception("not implemented yet") + + vfields_out.append(vfield) + sfields_out.append(sfield) + + self.outputs['SField'].sv_set(sfields_out) + self.outputs['VField'].sv_set(vfields_out) + +def register(): + bpy.utils.register_class(SvAttractorFieldNode) + +def unregister(): + bpy.utils.unregister_class(SvAttractorFieldNode) + diff --git a/nodes/field/bend_along_surface.py b/nodes/field/bend_along_surface.py new file mode 100644 index 0000000000..d2c8301e40 --- /dev/null +++ b/nodes/field/bend_along_surface.py @@ -0,0 +1,124 @@ + +from sverchok.utils.logging import info, exception + +import numpy as np +from math import sqrt + +import bpy +from bpy.props import FloatProperty, EnumProperty, BoolProperty, IntProperty + +from sverchok.node_tree import SverchCustomTreeNode, throttled +from sverchok.data_structure import updateNode, zip_long_repeat, ensure_nesting_level +from sverchok.utils.geom import diameter + +from sverchok.utils.field.vector import SvBendAlongSurfaceField + +class SvBendAlongSurfaceFieldNode(bpy.types.Node, SverchCustomTreeNode): + """ + Triggers: Bend surface + Tooltip: Generate a vector field which bends the space along the given surface. + """ + bl_idname = 'SvExBendAlongSurfaceFieldNode' + bl_label = 'Bend Along Surface Field' + bl_icon = 'OUTLINER_OB_EMPTY' + sv_icon = 'SV_BEND_SURFACE_FIELD' + + axes = [ + ("X", "X", "X axis", 1), + ("Y", "Y", "Y axis", 2), + ("Z", "Z", "Z axis", 3) + ] + + orient_axis_: EnumProperty( + name="Orientation axis", description="Which axis of object to put along path", + default="Z", items=axes, update=updateNode) + + def get_axis_idx(self, letter): + return 'XYZ'.index(letter) + + def get_orient_axis_idx(self): + return self.get_axis_idx(self.orient_axis_) + + orient_axis = property(get_orient_axis_idx) + + autoscale: BoolProperty( + name="Auto scale", description="Scale object along orientation axis automatically", + default=False, update=updateNode) + + flip: BoolProperty( + name="Flip surface", + description="Flip the surface orientation", + default=False, update=updateNode) + + u_min : FloatProperty( + name = "Src U Min", + default = -1.0, + update = updateNode) + + u_max : FloatProperty( + name = "Src U Max", + default = 1.0, + update = updateNode) + + v_min : FloatProperty( + name = "Src V Min", + default = -1.0, + update = updateNode) + + v_max : FloatProperty( + name = "Src V Max", + default = 1.0, + update = updateNode) + + def sv_init(self, context): + self.inputs.new('SvSurfaceSocket', "Surface") + self.inputs.new('SvStringsSocket', 'UMin').prop_name = 'u_min' + self.inputs.new('SvStringsSocket', 'UMax').prop_name = 'u_max' + self.inputs.new('SvStringsSocket', 'VMin').prop_name = 'v_min' + self.inputs.new('SvStringsSocket', 'VMax').prop_name = 'v_max' + self.outputs.new('SvVectorFieldSocket', 'Field') + + def draw_buttons(self, context, layout): + layout.label(text="Object vertical axis:") + layout.prop(self, "orient_axis_", expand=True) + layout.prop(self, "autoscale", toggle=True) + + def draw_buttons_ext(self, context, layout): + self.draw_buttons(context, layout) + layout.prop(self, 'flip') + + def process(self): + if not any(socket.is_linked for socket in self.outputs): + return + + surfaces_s = self.inputs['Surface'].sv_get() + u_min_s = self.inputs['UMin'].sv_get() + u_max_s = self.inputs['UMax'].sv_get() + v_min_s = self.inputs['VMin'].sv_get() + v_max_s = self.inputs['VMax'].sv_get() + + fields_out = [] + for surface, u_min, u_max, v_min, v_max in zip_long_repeat(surfaces_s, u_min_s, u_max_s, v_min_s, v_max_s): + if isinstance(u_min, (list, int)): + u_min = u_min[0] + if isinstance(u_max, (list, int)): + u_max = u_max[0] + if isinstance(v_min, (list, int)): + v_min = v_min[0] + if isinstance(v_max, (list, int)): + v_max = v_max[0] + + field = SvBendAlongSurfaceField(surface, self.orient_axis, + self.autoscale, self.flip) + field.u_bounds = (u_min, u_max) + field.v_bounds = (v_min, v_max) + fields_out.append(field) + + self.outputs['Field'].sv_set(fields_out) + +def register(): + bpy.utils.register_class(SvBendAlongSurfaceFieldNode) + +def unregister(): + bpy.utils.unregister_class(SvBendAlongSurfaceFieldNode) + diff --git a/nodes/field/compose_vector_field.py b/nodes/field/compose_vector_field.py new file mode 100644 index 0000000000..601a595787 --- /dev/null +++ b/nodes/field/compose_vector_field.py @@ -0,0 +1,82 @@ + +import numpy as np + +import bpy +from bpy.props import FloatProperty, EnumProperty, BoolProperty, IntProperty, StringProperty + +from sverchok.node_tree import SverchCustomTreeNode, throttled +from sverchok.data_structure import updateNode, zip_long_repeat, match_long_repeat +from sverchok.utils.logging import info, exception + +from sverchok.utils.math import coordinate_modes +from sverchok.utils.field.vector import SvComposedVectorField + +class SvComposeVectorFieldNode(bpy.types.Node, SverchCustomTreeNode): + """ + Triggers: Compose Vector Field + Tooltip: Generate vector field from three scalar fields + """ + bl_idname = 'SvExComposeVectorFieldNode' + bl_label = 'Compose Vector Field' + bl_icon = 'OUTLINER_OB_EMPTY' + sv_icon = 'SV_VFIELD_IN' + + @throttled + def update_sockets(self, context): + if self.input_mode == 'XYZ': + self.inputs[0].name = 'X' + self.inputs[1].name = 'Y' + self.inputs[2].name = 'Z' + elif self.input_mode == 'CYL': + self.inputs[0].name = 'Rho' + self.inputs[1].name = 'Phi' + self.inputs[2].name = 'Z' + else: # SPH + self.inputs[0].name = 'Rho' + self.inputs[1].name = 'Phi' + self.inputs[2].name = 'Theta' + + input_mode : EnumProperty( + name = "Coordinates", + items = coordinate_modes, + default = 'XYZ', + update = update_sockets) + + def sv_init(self, context): + self.inputs.new('SvScalarFieldSocket', "Field1") + self.inputs.new('SvScalarFieldSocket', "Field2") + self.inputs.new('SvScalarFieldSocket', "Field3") + self.outputs.new('SvVectorFieldSocket', "Field") + self.update_sockets(context) + + def draw_buttons(self, context, layout): + #layout.label(text="Input:") + layout.prop(self, "input_mode", expand=True) + + def process(self): + if not any(socket.is_linked for socket in self.outputs): + return + + fields_1_s = self.inputs[0].sv_get() + fields_2_s = self.inputs[1].sv_get() + fields_3_s = self.inputs[2].sv_get() + + fields_out = [] + for field1s, field2s, field3s in zip_long_repeat(fields_1_s, fields_2_s, fields_3_s): + if not isinstance(field1s, (list, tuple)): + field1s = [field1s] + if not isinstance(field2s, (list, tuple)): + field2s = [field2s] + if not isinstance(field3s, (list, tuple)): + field3s = [field3s] + for field1, field2, field3 in zip_long_repeat(field1s, field2s, field3s): + field = SvComposedVectorField(self.input_mode, field1, field2, field3) + fields_out.append(field) + self.outputs['Field'].sv_set(fields_out) + +def register(): + bpy.utils.register_class(SvComposeVectorFieldNode) + +def unregister(): + bpy.utils.unregister_class(SvComposeVectorFieldNode) + diff --git a/nodes/field/curve_bend_field.py b/nodes/field/curve_bend_field.py new file mode 100644 index 0000000000..f716117d74 --- /dev/null +++ b/nodes/field/curve_bend_field.py @@ -0,0 +1,158 @@ + +import numpy as np + +import bpy +from bpy.props import FloatProperty, EnumProperty, BoolProperty, IntProperty +from mathutils import Matrix + +import sverchok +from sverchok.node_tree import SverchCustomTreeNode, throttled +from sverchok.data_structure import updateNode, zip_long_repeat, ensure_nesting_level, get_data_nesting_level + +from sverchok.utils.field.vector import SvBendAlongCurveField + +T_MIN_SOCKET = 1 +T_MAX_SOCKET = 2 + +class SvBendAlongCurveFieldNode(bpy.types.Node, SverchCustomTreeNode): + """ + Triggers: Bend Along Curve + Tooltip: Generate a vector field which bends the space along the given curve. + """ + bl_idname = 'SvExBendAlongCurveFieldNode' + bl_label = 'Bend Along Curve Field' + bl_icon = 'OUTLINER_OB_EMPTY' + sv_icon = 'SV_BEND_CURVE_FIELD' + + algorithms = [ + ("householder", "Householder", "Use Householder reflection matrix", 1), + ("track", "Tracking", "Use quaternion-based tracking", 2), + ("diff", "Rotation difference", "Use rotational difference calculation", 3), + ('FRENET', "Frenet", "Use Frenet frames", 4), + ('ZERO', "Zero-Twist", "Use zero-twist frames", 5) + ] + + @throttled + def update_sockets(self, context): + self.inputs['Resolution'].hide_safe = not(self.algorithm == 'ZERO' or self.length_mode == 'L') + if self.algorithm in {'ZERO', 'FRENET'}: + self.orient_axis_ = 'Z' + #self.inputs[T_MIN_SOCKET].name = "Src {} Min".format(self.orient_axis) + #self.inputs[T_MAX_SOCKET].name = "Src {} Max".format(self.orient_axis) + + algorithm: EnumProperty( + name="Algorithm", description="Rotation calculation algorithm", + default="householder", items=algorithms, + update=update_sockets) + + axes = [ + ("X", "X", "X axis", 1), + ("Y", "Y", "Y axis", 2), + ("Z", "Z", "Z axis", 3) + ] + + orient_axis_: EnumProperty( + name="Orientation axis", description="Which axis of object to put along path", + default="Z", items=axes, update=update_sockets) + + t_min : FloatProperty( + name = "Src T Min", + default = -1.0, + update = updateNode) + + t_max : FloatProperty( + name = "Src T Max", + default = 1.0, + update = updateNode) + + orient_axis_: EnumProperty( + name="Orientation axis", description="Which axis of object to put along path", + default="Z", items=axes, update=updateNode) + + def get_axis_idx(self, letter): + return 'XYZ'.index(letter) + + def get_orient_axis_idx(self): + return self.get_axis_idx(self.orient_axis_) + + orient_axis = property(get_orient_axis_idx) + + up_axis: EnumProperty( + name="Up axis", description="Which axis of object should look up", + default='X', items=axes, update=updateNode) + + scale_all: BoolProperty( + name="Scale all axes", description="Scale objects along all axes or only along orientation axis", + default=True, update=updateNode) + + resolution : IntProperty( + name = "Resolution", + min = 10, default = 50, + update = updateNode) + + length_modes = [ + ('T', "Curve parameter", "Scaling along curve is depending on curve parametrization", 0), + ('L', "Curve length", "Scaling along curve is proportional to curve segment length", 1) + ] + + length_mode : EnumProperty( + name = "Scale along curve", + items = length_modes, + default = 'T', + update = update_sockets) + + def sv_init(self, context): + self.inputs.new('SvCurveSocket', 'Curve') #0 + self.inputs.new('SvStringsSocket', 'TMin').prop_name = 't_min' #1 + self.inputs.new('SvStringsSocket', 'TMax').prop_name = 't_max' #2 + self.inputs.new('SvStringsSocket', "Resolution").prop_name = 'resolution' + self.outputs.new('SvVectorFieldSocket', 'Field') + self.update_sockets(context) + + def draw_buttons(self, context, layout): + layout.label(text="Orientation:") + row = layout.row() + row.prop(self, "orient_axis_", expand=True) + row.enabled = self.algorithm not in {'ZERO', 'FRENET'} + + col = layout.column(align=True) + col.prop(self, "scale_all", toggle=True) + layout.prop(self, "algorithm") + if self.algorithm == 'track': + layout.prop(self, "up_axis") + layout.label(text="Scale along curve:") + layout.prop(self, 'length_mode', text='') + + def process(self): + if not any(socket.is_linked for socket in self.outputs): + return + + curves_s = self.inputs['Curve'].sv_get() + t_min_s = self.inputs[T_MIN_SOCKET].sv_get() + t_max_s = self.inputs[T_MAX_SOCKET].sv_get() + resolution_s = self.inputs['Resolution'].sv_get() + + fields_out = [] + for curve, t_min, t_max, resolution in zip_long_repeat(curves_s, t_min_s, t_max_s, resolution_s): + if isinstance(t_min, (list, int)): + t_min = t_min[0] + if isinstance(t_max, (list, int)): + t_max = t_max[0] + if isinstance(resolution, (list, int)): + resolution = resolution[0] + + field = SvBendAlongCurveField(curve, self.algorithm, self.scale_all, + self.orient_axis, t_min, t_max, + up_axis = self.up_axis, + resolution = resolution, + length_mode = self.length_mode) + fields_out.append(field) + + self.outputs['Field'].sv_set(fields_out) + +def register(): + bpy.utils.register_class(SvBendAlongCurveFieldNode) + +def unregister(): + bpy.utils.unregister_class(SvBendAlongCurveFieldNode) + diff --git a/nodes/field/decompose_vector_field.py b/nodes/field/decompose_vector_field.py new file mode 100644 index 0000000000..23b041eb6c --- /dev/null +++ b/nodes/field/decompose_vector_field.py @@ -0,0 +1,83 @@ + +import numpy as np + +import bpy +from bpy.props import FloatProperty, EnumProperty, BoolProperty, IntProperty, StringProperty + +from sverchok.node_tree import SverchCustomTreeNode, throttled +from sverchok.data_structure import updateNode, zip_long_repeat, match_long_repeat +from sverchok.utils.logging import info, exception + +from sverchok.utils.math import coordinate_modes +from sverchok.utils.field.scalar import SvVectorFieldDecomposed + +class SvDecomposeVectorFieldNode(bpy.types.Node, SverchCustomTreeNode): + """ + Triggers: Decompose Vector Field + Tooltip: Decompose vector field into three scalar fields + """ + bl_idname = 'SvExDecomposeVectorFieldNode' + bl_label = 'Decompose Vector Field' + bl_icon = 'OUTLINER_OB_EMPTY' + sv_icon = 'SV_VFIELD_OUT' + + @throttled + def update_sockets(self, context): + if self.output_mode == 'XYZ': + self.outputs[0].name = 'X' + self.outputs[1].name = 'Y' + self.outputs[2].name = 'Z' + elif self.output_mode == 'CYL': + self.outputs[0].name = 'Rho' + self.outputs[1].name = 'Phi' + self.outputs[2].name = 'Z' + else: # SPH + self.outputs[0].name = 'Rho' + self.outputs[1].name = 'Phi' + self.outputs[2].name = 'Theta' + + output_mode : EnumProperty( + name = "Coordinates", + items = coordinate_modes, + default = 'XYZ', + update = update_sockets) + + def sv_init(self, context): + self.inputs.new('SvVectorFieldSocket', "Field") + self.outputs.new('SvScalarFieldSocket', "Field1") + self.outputs.new('SvScalarFieldSocket', "Field2") + self.outputs.new('SvScalarFieldSocket', "Field3") + self.update_sockets(context) + + def draw_buttons(self, context, layout): + #layout.label(text="Output:") + layout.prop(self, "output_mode", expand=True) + + def process(self): + if not any(socket.is_linked for socket in self.outputs): + return + + fields_s = self.inputs['Field'].sv_get() + fields_1_out = [] + fields_2_out = [] + fields_3_out = [] + for fields in fields_s: + if not isinstance(fields, (list, tuple)): + fields = [fields] + for field in fields: + field1 = SvVectorFieldDecomposed(field, self.output_mode, 0) + field2 = SvVectorFieldDecomposed(field, self.output_mode, 1) + field3 = SvVectorFieldDecomposed(field, self.output_mode, 2) + fields_1_out.append(field1) + fields_2_out.append(field2) + fields_3_out.append(field3) + self.outputs[0].sv_set(fields_1_out) + self.outputs[1].sv_set(fields_2_out) + self.outputs[2].sv_set(fields_3_out) + +def register(): + bpy.utils.register_class(SvDecomposeVectorFieldNode) + +def unregister(): + bpy.utils.unregister_class(SvDecomposeVectorFieldNode) + diff --git a/nodes/field/differential_operations.py b/nodes/field/differential_operations.py new file mode 100644 index 0000000000..488c37f0c7 --- /dev/null +++ b/nodes/field/differential_operations.py @@ -0,0 +1,135 @@ + +import numpy as np + +import bpy +from bpy.props import FloatProperty, EnumProperty, BoolProperty, IntProperty, StringProperty + +from sverchok.node_tree import SverchCustomTreeNode, throttled +from sverchok.data_structure import updateNode, zip_long_repeat, match_long_repeat +from sverchok.utils.logging import info, exception + +from sverchok.utils.field.scalar import (SvScalarField, + SvVectorFieldDivergence, SvScalarFieldLaplacian) +from sverchok.utils.field.vector import ( SvVectorField, + SvScalarFieldGradient, SvVectorFieldRotor) +from sverchok.utils.modules.sockets import SvDynamicSocketsHandler, SocketInfo + +sockets_handler = SvDynamicSocketsHandler() + +S_FIELD_A, V_FIELD_A = sockets_handler.register_inputs( + SocketInfo("SvScalarFieldSocket", "SFieldA", "CIRCLE_DOT"), + SocketInfo("SvVectorFieldSocket", "VFieldA", "CIRCLE_DOT") + ) + +S_FIELD_B, V_FIELD_B = sockets_handler.register_outputs( + SocketInfo("SvScalarFieldSocket", "SFieldB", "CIRCLE_DOT"), + SocketInfo("SvVectorFieldSocket", "VFieldB", "CIRCLE_DOT") + ) + +operations = [ + ('GRAD', "Gradient", [("SFieldA", "SField")], [("VFieldB", "Gradient")]), + ('DIV', "Divergence", [("VFieldA", "VField")], [("SFieldB", "Divergence")]), + ('LAPLACE', "Laplacian", [("SFieldA", "SField")], [("SFieldB", "Laplacian")]), + ('ROTOR', "Rotor", [("VFieldA", "VField")], [("VFieldB", "Rotor")]) +] + +operation_modes = [ (id, name, name, i) for i, (id, name, _, _) in enumerate(operations) ] + +def get_sockets(op_id): + actual_inputs = None + actual_outputs = None + for id, _, inputs, outputs in operations: + if id == op_id: + return inputs, outputs + raise Exception("unsupported operation") + +class SvFieldDiffOpsNode(bpy.types.Node, SverchCustomTreeNode): + """ + Triggers: Field Differential Operations + Tooltip: Field differential operations + """ + bl_idname = 'SvExFieldDiffOpsNode' + bl_label = 'Field Differential Operation' + bl_icon = 'OUTLINER_OB_EMPTY' + sv_icon = 'SV_EX_NABLA' + + step : FloatProperty( + name = "Step", + default = 0.001, + precision = 4, + update = updateNode) + + def sv_init(self, context): + sockets_handler.init_sockets(self) + self.update_sockets(context) + + def draw_buttons(self, context, layout): + layout.prop(self, 'operation', text='') + layout.prop(self, 'step') + + @throttled + def update_sockets(self, context): + actual_inputs, actual_outputs = get_sockets(self.operation) + actual_inputs = dict(actual_inputs) + actual_outputs = dict(actual_outputs) + for socket in self.inputs: + registered = sockets_handler.get_input_by_idx(socket.index) + socket.hide_safe = registered.id not in actual_inputs + if not socket.hide_safe: + socket.name = actual_inputs[registered.id] + + for socket in self.outputs: + registered = sockets_handler.get_output_by_idx(socket.index) + socket.hide_safe = registered.id not in actual_outputs + if not socket.hide_safe: + socket.name = actual_outputs[registered.id] + + operation : EnumProperty( + name = "Operation", + items = operation_modes, + default = 'GRAD', + update = update_sockets) + + def process(self): + if not any(socket.is_linked for socket in self.outputs): + return + + sfields_s = self.inputs[S_FIELD_A.idx].sv_get(default=[[None]]) + vfields_s = self.inputs[V_FIELD_A.idx].sv_get(default=[[None]]) + if not isinstance(sfields_s, (list, tuple)): + sfields_s = [sfields_s] + if not isinstance(vfields_s, (list, tuple)): + vfields_s = [vfields_s] + + vfields_out = [] + sfields_out = [] + for sfields, vfields in zip_long_repeat(sfields_s, vfields_s): + if not isinstance(sfields, (list, tuple)): + sfields = [sfields] + if not isinstance(vfields, (list, tuple)): + vfields = [vfields] + for sfield, vfield in zip_long_repeat(sfields, vfields): + if self.operation == 'GRAD': + vfield = SvScalarFieldGradient(sfield, self.step) + vfields_out.append(vfield) + elif self.operation == 'DIV': + sfield = SvVectorFieldDivergence(vfield, self.step) + sfields_out.append(sfield) + elif self.operation == 'LAPLACE': + sfield = SvScalarFieldLaplacian(sfield, self.step) + sfields_out.append(sfield) + elif self.operation == 'ROTOR': + vfield = SvVectorFieldRotor(vfield, self.step) + vfields_out.append(vfield) + else: + raise Exception("Unsupported operation") + + self.outputs[V_FIELD_B.idx].sv_set(vfields_out) + self.outputs[S_FIELD_B.idx].sv_set(sfields_out) + +def register(): + bpy.utils.register_class(SvFieldDiffOpsNode) + +def unregister(): + bpy.utils.unregister_class(SvFieldDiffOpsNode) + diff --git a/nodes/field/image_field.py b/nodes/field/image_field.py new file mode 100644 index 0000000000..a5b926d04a --- /dev/null +++ b/nodes/field/image_field.py @@ -0,0 +1,90 @@ + +import numpy as np + +import bpy +from bpy.props import FloatProperty, EnumProperty, BoolProperty, IntProperty, StringProperty +from mathutils import kdtree +from mathutils import bvhtree + +from sverchok.node_tree import SverchCustomTreeNode, throttled +from sverchok.data_structure import updateNode, zip_long_repeat +from sverchok.utils.logging import info, exception + +from sverchok.utils.field.image import load_image, SvImageScalarField, SvImageVectorField + +class SvImageFieldNode(bpy.types.Node, SverchCustomTreeNode): + """ + Triggers: Image Field + Tooltip: Generate vector or scalar field from an Image data block + """ + bl_idname = 'SvExImageFieldNode' + bl_label = 'Image Field' + bl_icon = 'IMAGE' + + planes = [ + ('XY', "XY", "XOY plane", 0), + ('YZ', "YZ", "YOZ plane", 1), + ('XZ', "XZ", "XOZ plane", 2) + ] + + plane : EnumProperty( + name = "Plane", + items = planes, + default = 'XY', + update = updateNode) + + image_name : StringProperty( + name = "Image", + default = "image.png", + update = updateNode) + + def draw_buttons(self, context, layout): + layout.label(text="Image plane:") + layout.prop(self, 'plane', expand=True) + layout.prop_search(self, 'image_name', bpy.data, 'images', text='', icon='IMAGE') + + def sv_init(self, context): + self.outputs.new('SvVectorFieldSocket', "RGB") + self.outputs.new('SvVectorFieldSocket', "HSV") + self.outputs.new('SvScalarFieldSocket', "Red") + self.outputs.new('SvScalarFieldSocket', "Green") + self.outputs.new('SvScalarFieldSocket', "Blue") + self.outputs.new('SvScalarFieldSocket', "Hue") + self.outputs.new('SvScalarFieldSocket', "Saturation") + self.outputs.new('SvScalarFieldSocket', "Value") + self.outputs.new('SvScalarFieldSocket', "Alpha") + self.outputs.new('SvScalarFieldSocket', "RGB Average") + self.outputs.new('SvScalarFieldSocket', "Luminosity") + + def process(self): + if not any(socket.is_linked for socket in self.outputs): + return + + pixels = load_image(self.image_name) + + vfields_out = dict() + if self.outputs['RGB'].is_linked: + rgb = SvImageVectorField(pixels, 'RGB', plane=self.plane) + vfields_out['RGB'] = [rgb] + if self.outputs['HSV'].is_linked: + hsv = SvImageVectorField(pixels, 'HSV', plane=self.plane) + vfields_out['HSV'] = [hsv] + + sfields_out = dict() + for channel in ['Red', 'Green', 'Blue', 'Hue', 'Saturation', 'Value', 'Alpha', 'RGB Average', 'Luminosity']: + if self.outputs[channel].is_linked: + field = SvImageScalarField(pixels, channel, plane=self.plane) + sfields_out[channel] = [field] + + for space in vfields_out: + self.outputs[space].sv_set(vfields_out[space]) + + for channel in sfields_out: + self.outputs[channel].sv_set(sfields_out[channel]) + +def register(): + bpy.utils.register_class(SvImageFieldNode) + +def unregister(): + bpy.utils.unregister_class(SvImageFieldNode) + diff --git a/nodes/field/merge_scalar_fields.py b/nodes/field/merge_scalar_fields.py new file mode 100644 index 0000000000..959e693a81 --- /dev/null +++ b/nodes/field/merge_scalar_fields.py @@ -0,0 +1,63 @@ + +import numpy as np + +import bpy +from bpy.props import FloatProperty, EnumProperty, BoolProperty, IntProperty, StringProperty + +from sverchok.node_tree import SverchCustomTreeNode, throttled +from sverchok.data_structure import updateNode, zip_long_repeat, fullList, match_long_repeat, ensure_nesting_level +from sverchok.utils.logging import info, exception + +from sverchok.utils.field.scalar import SvMergedScalarField, SvScalarField + +class SvMergeScalarFieldsNode(bpy.types.Node, SverchCustomTreeNode): + """ + Triggers: Merge / Join Scalar Fields + Tooltip: Merge a list of scalar fields into one + """ + bl_idname = 'SvExMergeScalarFieldsNode' + bl_label = 'Join Scalar Fields' + bl_icon = 'OUTLINER_OB_EMPTY' + sv_icon = 'SV_JOIN_FIELDS' + + modes = [ + ('MIN', "Minimum", "Minimal value of all fields", 0), + ('MAX', "Maximum", "Maximal value of all fields", 1), + ('AVG', "Average", "Average value of all fields", 2), + ('SUM', "Sum", "Sum value of all fields", 3), + ('MINDIFF', "Voronoi", "Voronoi-like: difference between values of two minimal fields in each point", 4) + ] + + mode : EnumProperty( + name = "Mode", + items = modes, + default = 'AVG', + update = updateNode) + + def sv_init(self, context): + self.inputs.new('SvScalarFieldSocket', "Fields") + self.outputs.new('SvScalarFieldSocket', "Field") + + def draw_buttons(self, context, layout): + layout.prop(self, "mode", text="") + + def process(self): + if not any(socket.is_linked for socket in self.outputs): + return + + fields_s = self.inputs['Fields'].sv_get() + if isinstance(fields_s[0], SvScalarField): + fields_s = [fields_s] + + fields_out = [] + for fields in fields_s: + field = SvMergedScalarField(self.mode, fields) + fields_out.append(field) + self.outputs['Field'].sv_set(fields_out) + +def register(): + bpy.utils.register_class(SvMergeScalarFieldsNode) + +def unregister(): + bpy.utils.unregister_class(SvMergeScalarFieldsNode) + diff --git a/nodes/field/noise_vfield.py b/nodes/field/noise_vfield.py new file mode 100644 index 0000000000..a6e3835f75 --- /dev/null +++ b/nodes/field/noise_vfield.py @@ -0,0 +1,64 @@ + +import numpy as np + +import bpy +from bpy.props import FloatProperty, EnumProperty, BoolProperty, IntProperty, StringProperty + +from sverchok.node_tree import SverchCustomTreeNode, throttled +from sverchok.data_structure import updateNode, zip_long_repeat, fullList, match_long_repeat +from sverchok.utils.modules.eval_formula import get_variables, safe_eval +from sverchok.utils.logging import info, exception +from sverchok.utils.sv_noise_utils import noise_options, PERLIN_ORIGINAL + +from sverchok.utils.field.vector import SvNoiseVectorField + +avail_noise = [(t[0], t[0].title(), t[0].title(), '', t[1]) for t in noise_options] + +class SvNoiseVectorFieldNode(bpy.types.Node, SverchCustomTreeNode): + """ + Triggers: Noise Vector Field + Tooltip: Noise Vector Field + """ + bl_idname = 'SvExNoiseVectorFieldNode' + bl_label = 'Noise Vector Field' + bl_icon = 'OUTLINER_OB_FORCE_FIELD' + + noise_type: EnumProperty( + items=avail_noise, + default=PERLIN_ORIGINAL, + description="Noise type", + update=updateNode) + + seed: IntProperty(default=0, name='Seed', update=updateNode) + + def sv_init(self, context): + self.inputs.new('SvStringsSocket', 'Seed').prop_name = 'seed' + self.outputs.new('SvVectorFieldSocket', 'Noise') + + def draw_buttons(self, context, layout): + layout.prop(self, 'noise_type', text="Type") + + def process(self): + if not any(socket.is_linked for socket in self.outputs): + return + + seeds_s = self.inputs['Seed'].sv_get() + + fields_out = [] + for seed in seeds_s: + if isinstance(seed, (list, int)): + seed = seed[0] + + if seed == 0: + seed = 12345 + field = SvNoiseVectorField(self.noise_type, seed) + fields_out.append(field) + + self.outputs['Noise'].sv_set(fields_out) + +def register(): + bpy.utils.register_class(SvNoiseVectorFieldNode) + +def unregister(): + bpy.utils.unregister_class(SvNoiseVectorFieldNode) + diff --git a/nodes/field/scalar_field_eval.py b/nodes/field/scalar_field_eval.py new file mode 100644 index 0000000000..23c0790b3b --- /dev/null +++ b/nodes/field/scalar_field_eval.py @@ -0,0 +1,63 @@ + +import numpy as np + +import bpy +from bpy.props import FloatProperty, EnumProperty, BoolProperty, IntProperty, StringProperty + +from sverchok.node_tree import SverchCustomTreeNode, throttled +from sverchok.data_structure import updateNode, zip_long_repeat, fullList, match_long_repeat +from sverchok.utils.modules.eval_formula import get_variables, sv_compile, safe_eval_compiled +from sverchok.utils.logging import info, exception +from sverchok.utils.math import from_cylindrical, from_spherical, to_cylindrical, to_spherical + +from sverchok.utils.math import coordinate_modes +from sverchok.utils.field.scalar import SvScalarFieldLambda + +class SvScalarFieldEvaluateNode(bpy.types.Node, SverchCustomTreeNode): + """ + Triggers: Scalar Field Evaluate + Tooltip: Evaluate Scalar Field at specific point(s) + """ + bl_idname = 'SvExScalarFieldEvaluateNode' + bl_label = 'Evaluate Scalar Field' + bl_icon = 'OUTLINER_OB_EMPTY' + sv_icon = 'SV_EVAL_SCALAR_FIELD' + + def sv_init(self, context): + self.inputs.new('SvScalarFieldSocket', "Field") + d = self.inputs.new('SvVerticesSocket', "Vertices") + d.use_prop = True + d.prop = (0.0, 0.0, 0.0) + self.outputs.new('SvStringsSocket', 'Value') + + def process(self): + if not any(socket.is_linked for socket in self.outputs): + return + + vertices_s = self.inputs['Vertices'].sv_get() + fields_s = self.inputs['Field'].sv_get() + + values_out = [] + for field, vertices in zip_long_repeat(fields_s, vertices_s): + if len(vertices) == 0: + new_values = [] + elif len(vertices) == 1: + vertex = vertices[0] + value = field.evaluate(*vertex) + new_values = [value] + else: + XYZ = np.array(vertices) + xs = XYZ[:,0] + ys = XYZ[:,1] + zs = XYZ[:,2] + new_values = field.evaluate_grid(xs, ys, zs).tolist() + values_out.append(new_values) + + self.outputs['Value'].sv_set(values_out) + +def register(): + bpy.utils.register_class(SvScalarFieldEvaluateNode) + +def unregister(): + bpy.utils.unregister_class(SvScalarFieldEvaluateNode) + diff --git a/nodes/field/scalar_field_formula.py b/nodes/field/scalar_field_formula.py new file mode 100644 index 0000000000..2e8c858955 --- /dev/null +++ b/nodes/field/scalar_field_formula.py @@ -0,0 +1,147 @@ + +import numpy as np + +import bpy +from bpy.props import FloatProperty, EnumProperty, BoolProperty, IntProperty, StringProperty + +from sverchok.node_tree import SverchCustomTreeNode, throttled +from sverchok.data_structure import updateNode, zip_long_repeat, fullList, match_long_repeat +from sverchok.utils.modules.eval_formula import get_variables, sv_compile, safe_eval_compiled +from sverchok.utils.logging import info, exception +from sverchok.utils.math import from_cylindrical, from_spherical, to_cylindrical, to_spherical + +from sverchok.utils.math import coordinate_modes +from sverchok.utils.field.scalar import SvScalarFieldLambda + +class SvScalarFieldFormulaNode(bpy.types.Node, SverchCustomTreeNode): + """ + Triggers: Scalar Field Formula + Tooltip: Generate scalar field by formula + """ + bl_idname = 'SvExScalarFieldFormulaNode' + bl_label = 'Scalar Field Formula' + bl_icon = 'OUTLINER_OB_EMPTY' + sv_icon = 'SV_SCALAR_FIELD' + + @throttled + def on_update(self, context): + self.adjust_sockets() + + formula: StringProperty( + name = "Formula", + default = "x*x + y*y + z*z", + update = on_update) + + input_mode : EnumProperty( + name = "Coordinates", + items = coordinate_modes, + default = 'XYZ', + update = updateNode) + + def sv_init(self, context): + self.inputs.new('SvScalarFieldSocket', "Field") + self.outputs.new('SvScalarFieldSocket', "Field") + + def draw_buttons(self, context, layout): + layout.label(text="Input:") + layout.prop(self, "input_mode", expand=True) + layout.prop(self, "formula", text="") + + def make_function(self, variables): + compiled = sv_compile(self.formula) + + def carthesian(x, y, z, V): + variables.update(dict(x=x, y=y, z=z, V=V)) + return safe_eval_compiled(compiled, variables) + + def cylindrical(x, y, z, V): + rho, phi, z = to_cylindrical((x, y, z), mode='radians') + variables.update(dict(rho=rho, phi=phi, z=z, V=V)) + return safe_eval_compiled(compiled, variables) + + def spherical(x, y, z, V): + rho, phi, theta = to_spherical((x, y, z), mode='radians') + variables.update(dict(rho=rho, phi=phi, theta=theta, V=V)) + return safe_eval_compiled(compiled, variables) + + if self.input_mode == 'XYZ': + function = carthesian + elif self.input_mode == 'CYL': + function = cylindrical + else: # SPH + function = spherical + + return function + + def get_coordinate_variables(self): + if self.input_mode == 'XYZ': + return {'x', 'y', 'z', 'V'} + elif self.input_mode == 'CYL': + return {'rho', 'phi', 'z', 'V'} + else: # SPH + return {'rho', 'phi', 'theta', 'V'} + + def get_variables(self): + variables = get_variables(self.formula) + variables.difference_update(self.get_coordinate_variables()) + return list(sorted(list(variables))) + + def adjust_sockets(self): + variables = self.get_variables() + for key in self.inputs.keys(): + if key not in variables and key not in ['Field']: + self.debug("Input {} not in variables {}, remove it".format(key, str(variables))) + self.inputs.remove(self.inputs[key]) + for v in variables: + if v not in self.inputs: + self.debug("Variable {} not in inputs {}, add it".format(v, str(self.inputs.keys()))) + self.inputs.new('SvStringsSocket', v) + + def update(self): + if not self.formula: + return + self.adjust_sockets() + + def get_input(self): + variables = self.get_variables() + inputs = {} + + for var in variables: + if var in self.inputs and self.inputs[var].is_linked: + inputs[var] = self.inputs[var].sv_get() + return inputs + + def process(self): + if not any(socket.is_linked for socket in self.outputs): + return + + fields_s = self.inputs['Field'].sv_get(default = [None]) + + var_names = self.get_variables() + inputs = self.get_input() + input_values = [inputs.get(name, [[0]]) for name in var_names] + if var_names: + parameters = match_long_repeat([fields_s] + input_values) + else: + parameters = [fields_s] + + fields_out = [] + for field_in, *objects in zip(*parameters): + if var_names: + var_values_s = zip_long_repeat(*objects) + else: + var_values_s = [[]] + for var_values in var_values_s: + variables = dict(zip(var_names, var_values)) + function = self.make_function(variables.copy()) + new_field = SvScalarFieldLambda(function, variables, field_in) + fields_out.append(new_field) + + self.outputs['Field'].sv_set(fields_out) + +def register(): + bpy.utils.register_class(SvScalarFieldFormulaNode) + +def unregister(): + bpy.utils.unregister_class(SvScalarFieldFormulaNode) + diff --git a/nodes/field/scalar_field_math.py b/nodes/field/scalar_field_math.py new file mode 100644 index 0000000000..a42f94e7c7 --- /dev/null +++ b/nodes/field/scalar_field_math.py @@ -0,0 +1,89 @@ + +import numpy as np + +import bpy +from bpy.props import FloatProperty, EnumProperty, BoolProperty, IntProperty, StringProperty + +from sverchok.node_tree import SverchCustomTreeNode, throttled +from sverchok.data_structure import updateNode, zip_long_repeat, fullList, match_long_repeat +from sverchok.utils.modules.eval_formula import get_variables, safe_eval +from sverchok.utils.logging import info, exception + +from sverchok.utils.field.scalar import SvScalarFieldBinOp, SvScalarField, SvNegatedScalarField + +operations = [ + ('ADD', "Add", lambda x, y : x+y), + ('SUB', "Sub", lambda x, y : x-y), + ('MUL', "Multiply", lambda x, y : x * y), + ('MIN', "Minimum", lambda x, y : np.min([x,y],axis=0)), + ('MAX', "Maximum", lambda x, y : np.max([x,y],axis=0)), + ('AVG', "Average", lambda x, y : (x+y)/2), + ('NEG', "Negate", lambda x : -x) +] + +operation_modes = [ (id, name, name, i) for i, (id, name, fn) in enumerate(operations) ] + +def get_operation(op_id): + for id, _, function in operations: + if id == op_id: + return function + raise Exception("Unsupported operation: " + op_id) + +class SvScalarFieldMathNode(bpy.types.Node, SverchCustomTreeNode): + """ + Triggers: Scalar Field Math + Tooltip: Scalar Field Math + """ + bl_idname = 'SvExScalarFieldMathNode' + bl_label = 'Scalar Field Math' + bl_icon = 'OUTLINER_OB_EMPTY' + sv_icon = 'SV_SCALAR_FIELD_MATH' + + @throttled + def update_sockets(self, context): + self.inputs['FieldB'].hide_safe = self.operation == 'NEG' + + operation : EnumProperty( + name = "Operation", + items = operation_modes, + default = 'ADD', + update = update_sockets) + + def sv_init(self, context): + self.inputs.new('SvScalarFieldSocket', "FieldA") + self.inputs.new('SvScalarFieldSocket', "FieldB") + self.outputs.new('SvScalarFieldSocket', "FieldC") + self.update_sockets(context) + + def draw_buttons(self, context, layout): + layout.prop(self, 'operation', text='') + + def process(self): + if not any(socket.is_linked for socket in self.outputs): + return + + field_a_s = self.inputs['FieldA'].sv_get() + field_b_s = self.inputs['FieldB'].sv_get(default=[[None]]) + + fields_out = [] + for fields_a, fields_b in zip_long_repeat(field_a_s, field_b_s): + if isinstance(fields_a, SvScalarField): + fields_a = [fields_a] + if isinstance(fields_b, SvScalarField): + fields_b = [fields_b] + for field_a, field_b in zip_long_repeat(fields_a, fields_b): + operation = get_operation(self.operation) + if self.operation == 'NEG': + field_c = SvNegatedScalarField(field_a) + else: + field_c = SvScalarFieldBinOp(field_a, field_b, operation) + fields_out.append(field_c) + + self.outputs['FieldC'].sv_set(fields_out) + +def register(): + bpy.utils.register_class(SvScalarFieldMathNode) + +def unregister(): + bpy.utils.unregister_class(SvScalarFieldMathNode) + diff --git a/nodes/field/scalar_field_point.py b/nodes/field/scalar_field_point.py new file mode 100644 index 0000000000..4fd7b5b656 --- /dev/null +++ b/nodes/field/scalar_field_point.py @@ -0,0 +1,94 @@ + +import numpy as np + +import bpy +from bpy.props import FloatProperty, EnumProperty, BoolProperty, IntProperty, StringProperty + +from sverchok.node_tree import SverchCustomTreeNode, throttled +from sverchok.data_structure import updateNode, zip_long_repeat, fullList, match_long_repeat +from sverchok.utils.logging import info, exception + +from sverchok.utils.field.scalar import SvScalarFieldPointDistance +from sverchok.utils.math import falloff_types, falloff_array + +class SvScalarFieldPointNode(bpy.types.Node, SverchCustomTreeNode): + """ + Triggers: Scalar Field Point + Tooltip: Generate scalar field by distance from a point + """ + bl_idname = 'SvExScalarFieldPointNode' + bl_label = 'Distance from a point' + bl_icon = 'OUTLINER_OB_EMPTY' + sv_icon = 'SV_POINT_DISTANCE_FIELD' + + @throttled + def update_type(self, context): + self.inputs['Amplitude'].hide_safe = (self.falloff_type != 'NONE') + self.inputs['Coefficient'].hide_safe = (self.falloff_type not in ['NONE', 'inverse_exp', 'gauss']) + + falloff_type: EnumProperty( + name="Falloff type", items=falloff_types, default='NONE', update=update_type) + + amplitude: FloatProperty( + name="Amplitude", default=0.5, min=0.0, update=updateNode) + + coefficient: FloatProperty( + name="Coefficient", default=0.5, update=updateNode) + + clamp: BoolProperty( + name="Clamp", description="Restrict coefficient with R", default=False, update=updateNode) + + metrics = [ + ('EUCLIDEAN', "Euclidian", "Standard euclidian distance - sqrt(dx*dx + dy*dy + dz*dz)", 0), + ('CHEBYSHEV', "Chebyshev", "Chebyshev distance - abs(dx, dy, dz)", 1), + ('MANHATTAN', "Manhattan", "Manhattan distance - abs(dx) + abs(dy) + abs(dz)", 2) + ] + + metric : EnumProperty( + name = "Metric", + items = metrics, + default = 'EUCLIDEAN', + update = updateNode) + + def sv_init(self, context): + d = self.inputs.new('SvVerticesSocket', "Center") + d.use_prop = True + d.prop = (0.0, 0.0, 0.0) + + self.inputs.new('SvStringsSocket', 'Amplitude').prop_name = 'amplitude' + self.inputs.new('SvStringsSocket', 'Coefficient').prop_name = 'coefficient' + + self.outputs.new('SvScalarFieldSocket', "Field") + self.update_type(context) + + def draw_buttons(self, context, layout): + layout.prop(self, 'metric') + layout.prop(self, 'falloff_type') + layout.prop(self, 'clamp') + + def process(self): + if not any(socket.is_linked for socket in self.outputs): + return + + center_s = self.inputs['Center'].sv_get() + amplitudes_s = self.inputs['Amplitude'].sv_get(default=[0.5]) + coefficients_s = self.inputs['Coefficient'].sv_get(default=[0.5]) + + fields_out = [] + for centers, amplitudes, coefficients in zip_long_repeat(center_s, amplitudes_s, coefficients_s): + for center, amplitude, coefficient in zip_long_repeat(centers, amplitudes, coefficients): + if self.falloff_type == 'NONE': + falloff_func = None + else: + falloff_func = falloff_array(self.falloff_type, amplitude, coefficient, self.clamp) + field = SvScalarFieldPointDistance(np.array(center), metric=self.metric, falloff=falloff_func) + fields_out.append(field) + + self.outputs['Field'].sv_set(fields_out) + +def register(): + bpy.utils.register_class(SvScalarFieldPointNode) + +def unregister(): + bpy.utils.unregister_class(SvScalarFieldPointNode) + diff --git a/nodes/field/vector_field_apply.py b/nodes/field/vector_field_apply.py new file mode 100644 index 0000000000..5fdeca8dfa --- /dev/null +++ b/nodes/field/vector_field_apply.py @@ -0,0 +1,93 @@ + +import numpy as np + +import bpy +from bpy.props import FloatProperty, EnumProperty, BoolProperty, IntProperty, StringProperty + +from sverchok.node_tree import SverchCustomTreeNode, throttled +from sverchok.data_structure import updateNode, zip_long_repeat, repeat_last_for_length, match_long_repeat, ensure_nesting_level +from sverchok.utils.logging import info, exception + +class SvVectorFieldApplyNode(bpy.types.Node, SverchCustomTreeNode): + """ + Triggers: Vector Field Apply + Tooltip: Apply Vector Field to vertices + """ + bl_idname = 'SvExVectorFieldApplyNode' + bl_label = 'Apply Vector Field' + bl_icon = 'OUTLINER_OB_EMPTY' + sv_icon = 'SV_APPLY_VFIELD' + + coefficient : FloatProperty( + name = "Coefficient", + default = 1.0, + update = updateNode) + + iterations : IntProperty( + name = "Iterations", + default = 1, + min = 1, + update = updateNode) + + def sv_init(self, context): + self.inputs.new('SvVectorFieldSocket', "Field") + d = self.inputs.new('SvVerticesSocket', "Vertices") + d.use_prop = True + d.prop = (0.0, 0.0, 0.0) + self.inputs.new('SvStringsSocket', "Coefficient").prop_name = 'coefficient' + self.inputs.new('SvStringsSocket', "Iterations").prop_name = 'iterations' + self.outputs.new('SvVerticesSocket', 'Vertices') + + def process(self): + if not any(socket.is_linked for socket in self.outputs): + return + + vertices_s = self.inputs['Vertices'].sv_get() + coeffs_s = self.inputs['Coefficient'].sv_get() + fields_s = self.inputs['Field'].sv_get() + iterations_s = self.inputs['Iterations'].sv_get() + + vertices_s = ensure_nesting_level(vertices_s, 4) + coeffs_s = ensure_nesting_level(coeffs_s, 3) + + verts_out = [] + for fields, vertices_l, coeffs_l, iterations_l in zip_long_repeat(fields_s, vertices_s, coeffs_s, iterations_s): + if not isinstance(iterations_l, (list, tuple)): + iterations_l = [iterations_l] + if not isinstance(fields, (list, tuple)): + fields = [fields] + + for field, vertices, coeffs, iterations in zip_long_repeat(fields, vertices_l, coeffs_l, iterations_l): + + if len(vertices) == 0: + new_verts = [] + elif len(vertices) == 1: + vertex = vertices[0] + coeff = coeffs[0] + for i in range(iterations): + vector = field.evaluate(*vertex) + vertex = (np.array(vertex) + coeff * vector).tolist() + new_verts = [vertex] + else: + coeffs = repeat_last_for_length(coeffs, len(vertices)) + vertices = np.array(vertices) + for i in range(iterations): + xs = vertices[:,0] + ys = vertices[:,1] + zs = vertices[:,2] + new_xs, new_ys, new_zs = field.evaluate_grid(xs, ys, zs) + new_vectors = np.dstack((new_xs[:], new_ys[:], new_zs[:])) + new_vectors = np.array(coeffs)[np.newaxis].T * new_vectors[0] + vertices = vertices + new_vectors + new_verts = vertices.tolist() + + verts_out.append(new_verts) + + self.outputs['Vertices'].sv_set(verts_out) + +def register(): + bpy.utils.register_class(SvVectorFieldApplyNode) + +def unregister(): + bpy.utils.unregister_class(SvVectorFieldApplyNode) + diff --git a/nodes/field/vector_field_eval.py b/nodes/field/vector_field_eval.py new file mode 100644 index 0000000000..c2e04151ad --- /dev/null +++ b/nodes/field/vector_field_eval.py @@ -0,0 +1,61 @@ + +import numpy as np + +import bpy +from bpy.props import FloatProperty, EnumProperty, BoolProperty, IntProperty, StringProperty + +from sverchok.node_tree import SverchCustomTreeNode, throttled +from sverchok.data_structure import updateNode, zip_long_repeat, fullList, match_long_repeat +from sverchok.utils.logging import info, exception + +class SvVectorFieldEvaluateNode(bpy.types.Node, SverchCustomTreeNode): + """ + Triggers: Vector Field Evaluate + Tooltip: Evaluate Vector Field at specific point(s) + """ + bl_idname = 'SvExVectorFieldEvaluateNode' + bl_label = 'Evaluate Vector Field' + bl_icon = 'OUTLINER_OB_EMPTY' + sv_icon = 'SV_EVAL_VECTOR_FIELD' + + def sv_init(self, context): + self.inputs.new('SvVectorFieldSocket', "Field") + d = self.inputs.new('SvVerticesSocket', "Vertices") + d.use_prop = True + d.prop = (0.0, 0.0, 0.0) + self.outputs.new('SvVerticesSocket', 'Vectors') + + def process(self): + if not any(socket.is_linked for socket in self.outputs): + return + + vertices_s = self.inputs['Vertices'].sv_get() + fields_s = self.inputs['Field'].sv_get() + + values_out = [] + for field, vertices in zip_long_repeat(fields_s, vertices_s): + if len(vertices) == 0: + new_values = [] + elif len(vertices) == 1: + vertex = vertices[0] + value = field.evaluate(*vertex) + new_values = [tuple(value)] + else: + XYZ = np.array(vertices) + xs = XYZ[:,0] + ys = XYZ[:,1] + zs = XYZ[:,2] + new_xs, new_ys, new_zs = field.evaluate_grid(xs, ys, zs) + new_vectors = np.dstack((new_xs[:], new_ys[:], new_zs[:])) + new_values = new_vectors[0].tolist() + + values_out.append(new_values) + + self.outputs['Vectors'].sv_set(values_out) + +def register(): + bpy.utils.register_class(SvVectorFieldEvaluateNode) + +def unregister(): + bpy.utils.unregister_class(SvVectorFieldEvaluateNode) + diff --git a/nodes/field/vector_field_formula.py b/nodes/field/vector_field_formula.py new file mode 100644 index 0000000000..13040e1ee4 --- /dev/null +++ b/nodes/field/vector_field_formula.py @@ -0,0 +1,191 @@ + +import numpy as np + +import bpy +from bpy.props import FloatProperty, EnumProperty, BoolProperty, IntProperty, StringProperty + +from sverchok.node_tree import SverchCustomTreeNode, throttled +from sverchok.data_structure import updateNode, zip_long_repeat, fullList, match_long_repeat +from sverchok.utils.modules.eval_formula import get_variables, sv_compile, safe_eval_compiled +from sverchok.utils.logging import info, exception +from sverchok.utils.math import from_cylindrical, from_spherical, to_cylindrical, to_spherical + +from sverchok.utils.math import coordinate_modes +from sverchok.utils.field.vector import SvVectorFieldLambda + +class SvVectorFieldFormulaNode(bpy.types.Node, SverchCustomTreeNode): + """ + Triggers: Vector Field Formula + Tooltip: Generate vector field by formula + """ + bl_idname = 'SvExVectorFieldFormulaNode' + bl_label = 'Vector Field Formula' + bl_icon = 'OUTLINER_OB_EMPTY' + sv_icon = 'SV_VECTOR_FIELD' + + @throttled + def on_update(self, context): + self.adjust_sockets() + + formula1: StringProperty( + name = "Formula", + default = "-y", + update = on_update) + + formula2: StringProperty( + name = "Formula", + default = "x", + update = on_update) + + formula3: StringProperty( + name = "Formula", + default = "z", + update = on_update) + + input_mode : EnumProperty( + name = "Coordinates", + items = coordinate_modes, + default = 'XYZ', + update = on_update) + + output_mode : EnumProperty( + name = "Coordinates", + items = coordinate_modes, + default = 'XYZ', + update = updateNode) + + def sv_init(self, context): + self.inputs.new('SvVectorFieldSocket', "Field") + self.outputs.new('SvVectorFieldSocket', "Field") + + def draw_buttons(self, context, layout): + layout.label(text="Input:") + layout.prop(self, "input_mode", expand=True) + layout.prop(self, "formula1", text="") + layout.prop(self, "formula2", text="") + layout.prop(self, "formula3", text="") + layout.label(text="Output:") + layout.prop(self, "output_mode", expand=True) + + def make_function(self, variables): + compiled1 = sv_compile(self.formula1) + compiled2 = sv_compile(self.formula2) + compiled3 = sv_compile(self.formula3) + + if self.output_mode == 'XYZ': + def out_coordinates(x, y, z): + return x, y, z + elif self.output_mode == 'CYL': + def out_coordinates(rho, phi, z): + return from_cylindrical(rho, phi, z, mode='radians') + else: # SPH + def out_coordinates(rho, phi, theta): + return from_spherical(rho, phi, theta, mode='radians') + + def carthesian_in(x, y, z, V): + variables.update(dict(x=x, y=y, z=z, V=V)) + v1 = safe_eval_compiled(compiled1, variables) + v2 = safe_eval_compiled(compiled2, variables) + v3 = safe_eval_compiled(compiled3, variables) + return out_coordinates(v1, v2, v3) + + def cylindrical_in(x, y, z, V): + rho, phi, z = to_cylindrical((x, y, z), mode='radians') + variables.update(dict(rho=rho, phi=phi, z=z, V=V)) + v1 = safe_eval_compiled(compiled1, variables) + v2 = safe_eval_compiled(compiled2, variables) + v3 = safe_eval_compiled(compiled3, variables) + return out_coordinates(v1, v2, v3) + + def spherical_in(x, y, z, V): + rho, phi, theta = to_spherical((x, y, z), mode='radians') + variables.update(dict(rho=rho, phi=phi, theta=theta, V=V)) + v1 = safe_eval_compiled(compiled1, variables) + v2 = safe_eval_compiled(compiled2, variables) + v3 = safe_eval_compiled(compiled3, variables) + return out_coordinates(v1, v2, v3) + + if self.input_mode == 'XYZ': + function = carthesian_in + elif self.input_mode == 'CYL': + function = cylindrical_in + else: # SPH + function = spherical_in + + return function + + def get_coordinate_variables(self): + if self.input_mode == 'XYZ': + return {'x', 'y', 'z', 'V'} + elif self.input_mode == 'CYL': + return {'rho', 'phi', 'z', 'V'} + else: # SPH + return {'rho', 'phi', 'theta', 'V'} + + def get_variables(self): + variables = set() + for formula in [self.formula1, self.formula2, self.formula3]: + new_vars = get_variables(formula) + variables.update(new_vars) + variables.difference_update(self.get_coordinate_variables()) + return list(sorted(list(variables))) + + def adjust_sockets(self): + variables = self.get_variables() + for key in self.inputs.keys(): + if key not in variables and key not in ['Field']: + self.debug("Input {} not in variables {}, remove it".format(key, str(variables))) + self.inputs.remove(self.inputs[key]) + for v in variables: + if v not in self.inputs: + self.debug("Variable {} not in inputs {}, add it".format(v, str(self.inputs.keys()))) + self.inputs.new('SvStringsSocket', v) + + def update(self): + if not self.formula1 and not self.formula2 and not self.formula3: + return + self.adjust_sockets() + + def get_input(self): + variables = self.get_variables() + inputs = {} + + for var in variables: + if var in self.inputs and self.inputs[var].is_linked: + inputs[var] = self.inputs[var].sv_get() + return inputs + + def process(self): + if not any(socket.is_linked for socket in self.outputs): + return + + fields_s = self.inputs['Field'].sv_get(default = [None]) + + var_names = self.get_variables() + inputs = self.get_input() + input_values = [inputs.get(name, [[0]]) for name in var_names] + if var_names: + parameters = match_long_repeat([fields_s] + input_values) + else: + parameters = [fields_s] + + fields_out = [] + for field_in, *objects in zip(*parameters): + if var_names: + var_values_s = zip_long_repeat(*objects) + else: + var_values_s = [[]] + for var_values in var_values_s: + variables = dict(zip(var_names, var_values)) + function = self.make_function(variables.copy()) + new_field = SvVectorFieldLambda(function, variables, field_in) + fields_out.append(new_field) + + self.outputs['Field'].sv_set(fields_out) + +def register(): + bpy.utils.register_class(SvVectorFieldFormulaNode) + +def unregister(): + bpy.utils.unregister_class(SvVectorFieldFormulaNode) + diff --git a/nodes/field/vector_field_graph.py b/nodes/field/vector_field_graph.py new file mode 100644 index 0000000000..b376cfcfe8 --- /dev/null +++ b/nodes/field/vector_field_graph.py @@ -0,0 +1,169 @@ + +import numpy as np +import math + +import bpy +from bpy.props import FloatProperty, EnumProperty, BoolProperty, IntProperty, StringProperty +from mathutils import Vector + +from sverchok.node_tree import SverchCustomTreeNode, throttled +from sverchok.data_structure import updateNode, zip_long_repeat +from sverchok.utils.logging import info, exception +from sverchok.utils.sv_mesh_utils import mesh_join + +class SvVectorFieldGraphNode(bpy.types.Node, SverchCustomTreeNode): + """ + Triggers: Vector Field Graph + Tooltip: Generate a graphical representation of vector field + """ + bl_idname = 'SvExVectorFieldGraphNode' + bl_label = 'Vector Field Graph' + bl_icon = 'OUTLINER_OB_EMPTY' + sv_icon = 'SV_VECTOR_FIELD' + + samples_x : IntProperty( + name = "Samples X", + default = 10, + min = 2, + update = updateNode) + + samples_y : IntProperty( + name = "Samples Y", + default = 10, + min = 2, + update = updateNode) + + samples_z : IntProperty( + name = "Samples Z", + default = 10, + min = 2, + update = updateNode) + + scale : FloatProperty( + name = "Scale", + default = 1.0, + min = 0.0, + update = updateNode) + + join : BoolProperty( + name = "Join", + default = True, + update = updateNode) + + @throttled + def update_sockets(self, context): + pass + #self.inputs['Scale'].hide_safe = self.auto_scale + + auto_scale : BoolProperty( + name = "Auto Scale", + default = True, + update = update_sockets) + + def draw_buttons(self, context, layout): + layout.prop(self, 'auto_scale', toggle=True) + layout.prop(self, 'join', toggle=True) + + def sv_init(self, context): + self.inputs.new('SvVectorFieldSocket', "Field") + self.inputs.new('SvVerticesSocket', "Bounds") + self.inputs.new('SvStringsSocket', "Scale").prop_name = 'scale' + self.inputs.new('SvStringsSocket', "SamplesX").prop_name = 'samples_x' + self.inputs.new('SvStringsSocket', "SamplesY").prop_name = 'samples_y' + self.inputs.new('SvStringsSocket', "SamplesZ").prop_name = 'samples_z' + self.outputs.new('SvVerticesSocket', 'Vertices') + self.outputs.new('SvStringsSocket', 'Edges') + #self.outputs.new('SvStringsSocket', 'Faces') + self.update_sockets(context) + + def get_bounds(self, vertices): + vs = np.array(vertices) + min = vs.min(axis=0) + max = vs.max(axis=0) + return min.tolist(), max.tolist() + + def generate_one(self, v1, v2, dv): + dv = Vector(dv) + size = dv.length + dv = dv.normalized() + orth = dv.orthogonal() + arr1 = 0.1 * size * (orth - dv) + arr2 = 0.1 * size * (-orth - dv) + v3 = tuple(Vector(v2) + arr1) + v4 = tuple(Vector(v2) + arr2) + verts = [v1, v2, v3, v4] + edges = [[0, 1], [1, 2], [1, 3]] + return verts, edges + + def generate(self, points, vectors, scale): + new_verts = [] + new_edges = [] + vectors = scale * vectors + for v1, v2, dv in zip(points.tolist(), (points + vectors).tolist(), vectors.tolist()): + verts, edges = self.generate_one(v1, v2, dv) + new_verts.append(verts) + new_edges.append(edges) + return new_verts, new_edges + + def process(self): + if not any(socket.is_linked for socket in self.outputs): + return + + field_s = self.inputs['Field'].sv_get() + bounds_s = self.inputs['Bounds'].sv_get() + scale_s = self.inputs['Scale'].sv_get() + samples_x_s = self.inputs['SamplesX'].sv_get() + samples_y_s = self.inputs['SamplesY'].sv_get() + samples_z_s = self.inputs['SamplesZ'].sv_get() + + verts_out = [] + edges_out = [] + + inputs = zip_long_repeat(field_s, bounds_s, scale_s, samples_x_s, samples_y_s, samples_z_s) + for field, bounds, scale, samples_x, samples_y, samples_z in inputs: + if isinstance(samples_x, (list, tuple)): + samples_x = samples_x[0] + if isinstance(samples_y, (list, tuple)): + samples_y = samples_y[0] + if isinstance(samples_z, (list, tuple)): + samples_z = samples_z[0] + if isinstance(scale, (list, tuple)): + scale = scale[0] + + b1, b2 = self.get_bounds(bounds) + b1n, b2n = np.array(b1), np.array(b2) + self.debug("Bounds: %s - %s", b1, b2) + + x_range = np.linspace(b1[0], b2[0], num=samples_x) + y_range = np.linspace(b1[1], b2[1], num=samples_y) + z_range = np.linspace(b1[2], b2[2], num=samples_z) + xs, ys, zs = np.meshgrid(x_range, y_range, z_range, indexing='ij') + xs, ys, zs = xs.flatten(), ys.flatten(), zs.flatten() + points = np.stack((xs, ys, zs)).T + rxs, rys, rzs = field.evaluate_grid(xs, ys, zs) + vectors = np.stack((rxs, rys, rzs)).T + if self.auto_scale: + norms = np.linalg.norm(vectors, axis=1) + max_norm = norms.max() + size = b2n - b1n + size_x = size[0] / samples_x + size_y = size[1] / samples_y + size_z = size[2] / samples_z + size = math.pow(size_x * size_y * size_z, 1.0/3.0) + scale = scale * size / max_norm + + new_verts, new_edges = self.generate(points, vectors, scale) + if self.join: + new_verts, new_edges, _ = mesh_join(new_verts, new_edges, [[]] * len(new_verts)) + verts_out.append(new_verts) + edges_out.append(new_edges) + + self.outputs['Vertices'].sv_set(verts_out) + self.outputs['Edges'].sv_set(edges_out) + +def register(): + bpy.utils.register_class(SvVectorFieldGraphNode) + +def unregister(): + bpy.utils.unregister_class(SvVectorFieldGraphNode) + diff --git a/nodes/field/vector_field_lines.py b/nodes/field/vector_field_lines.py new file mode 100644 index 0000000000..7c342556cd --- /dev/null +++ b/nodes/field/vector_field_lines.py @@ -0,0 +1,126 @@ + +import numpy as np + +import bpy +from bpy.props import FloatProperty, EnumProperty, BoolProperty, IntProperty, StringProperty + +from sverchok.node_tree import SverchCustomTreeNode, throttled +from sverchok.data_structure import updateNode, zip_long_repeat, repeat_last_for_length, match_long_repeat, ensure_nesting_level +from sverchok.utils.logging import info, exception +from sverchok.utils.sv_mesh_utils import mesh_join + +class SvVectorFieldLinesNode(bpy.types.Node, SverchCustomTreeNode): + """ + Triggers: Vector Field Lines + Tooltip: Generate vector field lines + """ + bl_idname = 'SvExVectorFieldLinesNode' + bl_label = 'Vector Field Lines' + bl_icon = 'OUTLINER_OB_EMPTY' + sv_icon = 'SV_VECTOR_FIELD_LINES' + + step : FloatProperty( + name = "Step", + default = 0.1, + min = 0.0, + update = updateNode) + + iterations : IntProperty( + name = "Iterations", + default = 10, + min = 1, + update = updateNode) + + normalize : BoolProperty( + name = "Normalize", + default = True, + update = updateNode) + + join : BoolProperty( + name = "Join", + default = True, + update = updateNode) + + def draw_buttons(self, context, layout): + layout.prop(self, 'normalize', toggle=True) + layout.prop(self, 'join', toggle=True) + + def sv_init(self, context): + self.inputs.new('SvVectorFieldSocket', "Field") + self.inputs.new('SvVerticesSocket', "Vertices") + self.inputs.new('SvStringsSocket', "Step").prop_name = 'step' + self.inputs.new('SvStringsSocket', "Iterations").prop_name = 'iterations' + self.outputs.new('SvVerticesSocket', 'Vertices') + self.outputs.new('SvStringsSocket', 'Edges') + + def generate_all(self, field, vertices, step, iterations): + new_verts = np.empty((iterations, len(vertices), 3)) + for i in range(iterations): + xs = vertices[:,0] + ys = vertices[:,1] + zs = vertices[:,2] + new_xs, new_ys, new_zs = field.evaluate_grid(xs, ys, zs) + vectors = np.stack((new_xs, new_ys, new_zs)).T + if self.normalize: + norms = np.linalg.norm(vectors, axis=1)[np.newaxis].T + vertices = vertices + step * vectors / norms + else: + vertices = vertices + step * vectors + new_verts[i,:,:] = vertices + result = np.transpose(new_verts, axes=(1, 0, 2)) + return result.tolist() + + def process(self): + if not any(socket.is_linked for socket in self.outputs): + return + + vertices_s = self.inputs['Vertices'].sv_get() + step_s = self.inputs['Step'].sv_get() + fields_s = self.inputs['Field'].sv_get() + iterations_s = self.inputs['Iterations'].sv_get() + + vertices_s = ensure_nesting_level(vertices_s, 4) + step_s = ensure_nesting_level(step_s, 2) + + verts_out = [] + edges_out = [] + + for fields, vertices_l, steps_l, iterations_l in zip_long_repeat(fields_s, vertices_s, step_s, iterations_s): + if not isinstance(iterations_l, (list, tuple)): + iterations_l = [iterations_l] + if not isinstance(steps_l, (list, tuple)): + steps_l = [steps_l] + if not isinstance(fields, (list, tuple)): + fields = [fields] + + field_verts = [] + field_edges = [] + for field, vertices, step, iterations in zip_long_repeat(fields, vertices_l, steps_l, iterations_l): + + if len(vertices) == 0: + new_verts = [] + new_edges = [] + else: + new_verts = self.generate_all(field, np.array(vertices), step, iterations) + new_edges = [[(i,i+1) for i in range(iterations-1)]] * len(vertices) + if self.join: + new_verts, new_edges, _ = mesh_join(new_verts, new_edges, [[]] * len(new_verts)) + if self.join: + field_verts.append(new_verts) + field_edges.append(new_edges) + else: + field_verts.extend(new_verts) + field_edges.extend(new_edges) + + verts_out.extend(field_verts) + edges_out.extend(field_edges) + + self.outputs['Vertices'].sv_set(verts_out) + self.outputs['Edges'].sv_set(edges_out) + +def register(): + bpy.utils.register_class(SvVectorFieldLinesNode) + +def unregister(): + bpy.utils.unregister_class(SvVectorFieldLinesNode) + diff --git a/nodes/field/vector_field_math.py b/nodes/field/vector_field_math.py new file mode 100644 index 0000000000..434fa08191 --- /dev/null +++ b/nodes/field/vector_field_math.py @@ -0,0 +1,179 @@ + +import numpy as np + +import bpy +from bpy.props import FloatProperty, EnumProperty, BoolProperty, IntProperty, StringProperty + +from sverchok.node_tree import SverchCustomTreeNode, throttled +from sverchok.data_structure import updateNode, zip_long_repeat, match_long_repeat +from sverchok.utils.logging import info, exception + +from sverchok.utils.field.scalar import (SvScalarField, + SvVectorFieldsScalarProduct, + SvVectorFieldNorm, + SvVectorScalarFieldComposition) +from sverchok.utils.field.vector import (SvVectorField, + SvVectorFieldBinOp, SvVectorFieldMultipliedByScalar, + SvVectorFieldsLerp, SvVectorFieldCrossProduct, + SvVectorFieldTangent, SvVectorFieldCotangent, + SvAbsoluteVectorField, SvRelativeVectorField, + SvVectorFieldComposition) +from sverchok.utils.modules.sockets import SvDynamicSocketsHandler, SocketInfo + +sockets_handler = SvDynamicSocketsHandler() + +V_FIELD_A, V_FIELD_B, S_FIELD_B = sockets_handler.register_inputs( + SocketInfo("SvVectorFieldSocket", "VFieldA", "CIRCLE_DOT"), + SocketInfo("SvVectorFieldSocket", "VFieldB", "CIRCLE_DOT"), + SocketInfo("SvScalarFieldSocket", "SFieldB", "CIRCLE_DOT") + ) + +V_FIELD_C, S_FIELD_C, V_FIELD_D = sockets_handler.register_outputs( + SocketInfo("SvVectorFieldSocket", "VFieldC", "CIRCLE_DOT"), + SocketInfo("SvScalarFieldSocket", "SFieldC", "CIRCLE_DOT"), + SocketInfo("SvVectorFieldSocket", "VFieldD", "CIRCLE_DOT") + ) + +operations = [ + ('ADD', "Add", lambda x,y : x+y, [("VFieldA", "A"), ("VFieldB", "B")], [("VFieldC", "Sum")]), + ('SUB', "Sub", lambda x, y : x-y, [("VFieldA", "A"), ('VFieldB', "B")], [("VFieldC", "Difference")]), + ('AVG', "Average", lambda x, y : (x+y)/2, [("VFieldA", "A"), ("VFieldB", "B")], [("VFieldC", "Average")]), + ('DOT', "Scalar Product", None, [("VFieldA", "A"), ("VFieldB", "B")], [("SFieldC", "Product")]), + ('CROSS', "Vector Product", None, [("VFieldA", "A"), ("VFieldB","B")], [("VFieldC", "Product")]), + ('MUL', "Multiply Scalar", None, [("VFieldA", "VField"), ("SFieldB", "Scalar")], [("VFieldC", "Product")]), + ('TANG', "Projection decomposition", None, [("VFieldA", "VField"), ("VFieldB","Basis")], [("VFieldC", "Projection"), ("VFieldD", "Coprojection")]), + ('COMPOSE', "Composition VB(VA(x))", None, [("VFieldA", "VA"), ("VFieldB", "VB")], [("VFieldC", "VC")]), + ('COMPOSES', "Composition SB(VA(x))", None, [("VFieldA", "VA"), ("SFieldB", "SB")], [("SFieldC", "SC")]), + ('NORM', "Norm", None, [("VFieldA", "VField")], [("SFieldC", "Norm")]), + ('LERP', "Lerp A -> B", None, [("VFieldA", "A"), ("VFieldB", "B"), ("SFieldB", "Coefficient")], [("VFieldC", "VField")]), + ('ABS', "Relative -> Absolute", None, [("VFieldA", "Relative")], [("VFieldC", "Absolute")]), + ('REL', "Absolute -> Relative", None, [("VFieldA", "Absolute")], [("VFieldC", "Relative")]), +] + +operation_modes = [ (id, name, name, i) for i, (id, name, fn, _, _) in enumerate(operations) ] + +def get_operation(op_id): + for id, _, function, _, _ in operations: + if id == op_id: + return function + raise Exception("Unsupported operation: " + op_id) + +def get_sockets(op_id): + actual_inputs = None + actual_outputs = None + for id, _, _, inputs, outputs in operations: + if id == op_id: + return inputs, outputs + raise Exception("unsupported operation") + +class SvVectorFieldMathNode(bpy.types.Node, SverchCustomTreeNode): + """ + Triggers: Vector Field Math + Tooltip: Vector Field Math + """ + bl_idname = 'SvExVectorFieldMathNode' + bl_label = 'Vector Field Math' + bl_icon = 'OUTLINER_OB_EMPTY' + sv_icon = 'SV_VECTOR_FIELD_MATH' + + @throttled + def update_sockets(self, context): + actual_inputs, actual_outputs = get_sockets(self.operation) + actual_inputs = dict(actual_inputs) + actual_outputs = dict(actual_outputs) + for socket in self.inputs: + registered = sockets_handler.get_input_by_idx(socket.index) + socket.hide_safe = registered.id not in actual_inputs + if not socket.hide_safe: + socket.name = actual_inputs[registered.id] + + for socket in self.outputs: + registered = sockets_handler.get_output_by_idx(socket.index) + socket.hide_safe = registered.id not in actual_outputs + if not socket.hide_safe: + socket.name = actual_outputs[registered.id] + + operation : EnumProperty( + name = "Operation", + items = operation_modes, + default = 'ADD', + update = update_sockets) + + def sv_init(self, context): + sockets_handler.init_sockets(self) + self.update_sockets(context) + + def draw_buttons(self, context, layout): + layout.prop(self, 'operation', text='') + + def process(self): + if not any(socket.is_linked for socket in self.outputs): + return + + vfield_a_s = self.inputs[V_FIELD_A.idx].sv_get() + vfield_b_s = self.inputs[V_FIELD_B.idx].sv_get(default=[[None]]) + sfield_b_s = self.inputs[S_FIELD_B.idx].sv_get(default=[[None]]) + + vfields_c_out = [] + vfields_d_out = [] + sfields_out = [] + for vfields_a, vfields_b, sfields_b in zip_long_repeat(vfield_a_s, vfield_b_s, sfield_b_s): + + if not isinstance(vfields_a, (list, tuple)): + vfields_a = [vfields_a] + if not isinstance(vfields_b, (list, tuple)): + vfields_b = [vfields_b] + if not isinstance(sfields_b, (list, tuple)): + sfields_b = [sfields_b] + + for vfield_a, vfield_b, sfield_b in zip_long_repeat(vfields_a, vfields_b, sfields_b): + + inputs = dict(VFieldA = vfield_a, VFieldB = vfield_b, SFieldB = sfield_b) + + if self.operation == 'MUL': + field_c = SvVectorFieldMultipliedByScalar(vfield_a, sfield_b) + vfields_c_out.append(field_c) + elif self.operation == 'DOT': + field_c = SvVectorFieldsScalarProduct(vfield_a, vfield_b) + sfields_out.append(field_c) + elif self.operation == 'CROSS': + field_c = SvVectorFieldCrossProduct(vfield_a, vfield_b) + vfields_c_out.append(field_c) + elif self.operation == 'NORM': + field_c = SvVectorFieldNorm(vfield_a) + sfields_out.append(field_c) + elif self.operation == 'TANG': + field_c = SvVectorFieldTangent(vfield_a, vfield_b) + field_d = SvVectorFieldCotangent(vfield_a, vfield_b) + vfields_c_out.append(field_c) + vfields_d_out.append(field_d) + elif self.operation == 'COMPOSE': + field_c = SvVectorFieldComposition(vfield_a, vfield_b) + vfields_c_out.append(field_c) + elif self.operation == 'COMPOSES': + field_c = SvVectorScalarFieldComposition(vfield_a, sfield_b) + sfields_out.append(field_c) + elif self.operation == 'LERP': + field_c = SvVectorFieldsLerp(vfield_a, vfield_b, sfield_b) + vfields_c_out.append(field_c) + elif self.operation == 'ABS': + field_c = SvAbsoluteVectorField(vfield_a) + vfields_c_out.append(field_c) + elif self.operation == 'REL': + field_c = SvRelativeVectorField(vfield_a) + vfields_c_out.append(field_c) + else: + operation = get_operation(self.operation) + field_c = SvVectorFieldBinOp(vfield_a, vfield_b, operation) + vfields_c_out.append(field_c) + + self.outputs[V_FIELD_C.idx].sv_set(vfields_c_out) + self.outputs[V_FIELD_D.idx].sv_set(vfields_d_out) + self.outputs[S_FIELD_C.idx].sv_set(sfields_out) + +def register(): + bpy.utils.register_class(SvVectorFieldMathNode) + +def unregister(): + bpy.utils.unregister_class(SvVectorFieldMathNode) + diff --git a/nodes/field/voronoi_field.py b/nodes/field/voronoi_field.py new file mode 100644 index 0000000000..6df82ceced --- /dev/null +++ b/nodes/field/voronoi_field.py @@ -0,0 +1,53 @@ +import numpy as np + +import bpy +from bpy.props import FloatProperty, EnumProperty, BoolProperty, IntProperty, StringProperty +from mathutils import kdtree +from mathutils import bvhtree + +from sverchok.node_tree import SverchCustomTreeNode, throttled +from sverchok.data_structure import updateNode, zip_long_repeat, match_long_repeat +from sverchok.utils.logging import info, exception + +from sverchok.utils.field.scalar import SvVoronoiScalarField +from sverchok.utils.field.vector import SvVoronoiVectorField + +class SvVoronoiFieldNode(bpy.types.Node, SverchCustomTreeNode): + """ + Triggers: Voronoi Field + Tooltip: Generate Voronoi field + """ + bl_idname = 'SvExVoronoiFieldNode' + bl_label = 'Voronoi Field' + bl_icon = 'OUTLINER_OB_EMPTY' + sv_icon = 'SV_VORONOI' + + def sv_init(self, context): + self.inputs.new('SvVerticesSocket', "Vertices") + self.outputs.new('SvScalarFieldSocket', "SField") + self.outputs.new('SvVectorFieldSocket', "VField") + + def process(self): + + if not any(socket.is_linked for socket in self.outputs): + return + + vertices_s = self.inputs['Vertices'].sv_get() + + sfields_out = [] + vfields_out = [] + for vertices in vertices_s: + sfield = SvVoronoiScalarField(vertices) + vfield = SvVoronoiVectorField(vertices) + sfields_out.append(sfield) + vfields_out.append(vfield) + + self.outputs['SField'].sv_set(sfields_out) + self.outputs['VField'].sv_set(vfields_out) + +def register(): + bpy.utils.register_class(SvVoronoiFieldNode) + +def unregister(): + bpy.utils.unregister_class(SvVoronoiFieldNode) + diff --git a/nodes/surface/__init__.py b/nodes/surface/__init__.py new file mode 100644 index 0000000000..143f486c05 --- /dev/null +++ b/nodes/surface/__init__.py @@ -0,0 +1 @@ +# __init__.py diff --git a/nodes/surface/apply_field_to_surface.py b/nodes/surface/apply_field_to_surface.py new file mode 100644 index 0000000000..d52becaa05 --- /dev/null +++ b/nodes/surface/apply_field_to_surface.py @@ -0,0 +1,54 @@ + +import numpy as np + +import bpy +from bpy.props import FloatProperty, EnumProperty, BoolProperty, IntProperty + +from sverchok.node_tree import SverchCustomTreeNode, throttled +from sverchok.data_structure import updateNode, zip_long_repeat +from sverchok.utils.surface import SvDeformedByFieldSurface + +class SvApplyFieldToSurfaceNode(bpy.types.Node, SverchCustomTreeNode): + """ + Triggers: Apply field to surface + Tooltip: Apply vector field to surface + """ + bl_idname = 'SvExApplyFieldToSurfaceNode' + bl_label = 'Apply Field to Surface' + sv_icon = 'SV_SURFACE_VFIELD' + + coefficient : FloatProperty( + name = "Coefficient", + default = 1.0, + update=updateNode) + + def sv_init(self, context): + self.inputs.new('SvVectorFieldSocket', "Field") + self.inputs.new('SvSurfaceSocket', "Surface") + self.inputs.new('SvStringsSocket', "Coefficient").prop_name = 'coefficient' + self.outputs.new('SvSurfaceSocket', "Surface") + + def process(self): + if not any(socket.is_linked for socket in self.outputs): + return + + surface_s = self.inputs['Surface'].sv_get() + field_s = self.inputs['Field'].sv_get() + coeff_s = self.inputs['Coefficient'].sv_get() + + surface_out = [] + for surface, field, coeff in zip_long_repeat(surface_s, field_s, coeff_s): + if isinstance(coeff, (list, tuple)): + coeff = coeff[0] + + new_surface = SvDeformedByFieldSurface(surface, field, coeff) + surface_out.append(new_surface) + + self.outputs['Surface'].sv_set(surface_out) + +def register(): + bpy.utils.register_class(SvApplyFieldToSurfaceNode) + +def unregister(): + bpy.utils.unregister_class(SvApplyFieldToSurfaceNode) + diff --git a/nodes/surface/curve_lerp.py b/nodes/surface/curve_lerp.py new file mode 100644 index 0000000000..dd2adba28b --- /dev/null +++ b/nodes/surface/curve_lerp.py @@ -0,0 +1,69 @@ + +import numpy as np + +import bpy +from bpy.props import FloatProperty, EnumProperty, BoolProperty, IntProperty + +from sverchok.node_tree import SverchCustomTreeNode, throttled +from sverchok.data_structure import updateNode, zip_long_repeat, ensure_nesting_level +from sverchok.utils.logging import info, exception +from sverchok.utils.curve import SvCurve +from sverchok.utils.surface import SvCurveLerpSurface + +class SvCurveLerpNode(bpy.types.Node, SverchCustomTreeNode): + """ + Triggers: Curve Lerp Linear Surface + Tooltip: Generate a linear surface between two curves (curves linear interpolation) + """ + bl_idname = 'SvExCurveLerpNode' + bl_label = 'Linear Surface' + bl_icon = 'MOD_THICKNESS' + + v_min : FloatProperty( + name = "V Min", + default = 0.0, + update = updateNode) + + v_max : FloatProperty( + name = "V Max", + default = 1.0, + update = updateNode) + + def sv_init(self, context): + self.inputs.new('SvCurveSocket', "Curve1") + self.inputs.new('SvCurveSocket', "Curve2") + self.inputs.new('SvStringsSocket', "VMin").prop_name = 'v_min' + self.inputs.new('SvStringsSocket', "VMax").prop_name = 'v_max' + self.outputs.new('SvSurfaceSocket', "Surface") + + def process(self): + if not any(socket.is_linked for socket in self.outputs): + return + + curve1_s = self.inputs['Curve1'].sv_get() + curve2_s = self.inputs['Curve2'].sv_get() + vmin_s = self.inputs['VMin'].sv_get() + vmax_s = self.inputs['VMax'].sv_get() + + vmin_s = ensure_nesting_level(vmin_s, 2) + vmax_s = ensure_nesting_level(vmax_s, 2) + + if isinstance(curve1_s[0], SvCurve): + curve1_s = [curve1_s] + if isinstance(curve2_s[0], SvCurve): + curve2_s = [curve2_s] + + surface_out = [] + for curve1s, curve2s, vmins, vmaxs in zip_long_repeat(curve1_s, curve2_s, vmin_s, vmax_s): + for curve1, curve2, vmin, vmax in zip_long_repeat(curve1s, curve2s, vmins, vmaxs): + surface = SvCurveLerpSurface(curve1, curve2) + surface.v_bounds = (vmin, vmax) + surface_out.append(surface) + self.outputs['Surface'].sv_set(surface_out) + +def register(): + bpy.utils.register_class(SvCurveLerpNode) + +def unregister(): + bpy.utils.unregister_class(SvCurveLerpNode) + diff --git a/nodes/surface/evaluate_surface.py b/nodes/surface/evaluate_surface.py new file mode 100644 index 0000000000..c918106c03 --- /dev/null +++ b/nodes/surface/evaluate_surface.py @@ -0,0 +1,271 @@ + +import numpy as np + +import bpy +from bpy.props import EnumProperty, IntProperty, FloatProperty + +import sverchok +from sverchok.node_tree import SverchCustomTreeNode, throttled +from sverchok.data_structure import updateNode, zip_long_repeat, ensure_nesting_level, get_data_nesting_level +from sverchok.utils.logging import info, exception +from sverchok.utils.surface import SvSurface + +U_SOCKET = 1 +V_SOCKET = 2 + +class SvEvalSurfaceNode(bpy.types.Node, SverchCustomTreeNode): + """ + Triggers: Evaluate Surface + Tooltip: Evaluate Surface + """ + bl_idname = 'SvExEvalSurfaceNode' + bl_label = 'Evaluate Surface' + bl_icon = 'OUTLINER_OB_EMPTY' + sv_icon = 'SV_EVAL_SURFACE' + + @throttled + def update_sockets(self, context): + self.inputs[U_SOCKET].hide_safe = self.eval_mode == 'GRID' or self.input_mode == 'VERTICES' + self.inputs[V_SOCKET].hide_safe = self.eval_mode == 'GRID' or self.input_mode == 'VERTICES' + self.inputs['Vertices'].hide_safe = self.eval_mode == 'GRID' or self.input_mode == 'PAIRS' + + self.inputs['SamplesU'].hide_safe = self.eval_mode != 'GRID' + self.inputs['SamplesV'].hide_safe = self.eval_mode != 'GRID' + + self.outputs['Edges'].hide_safe = self.eval_mode == 'EXPLICIT' + self.outputs['Faces'].hide_safe = self.eval_mode == 'EXPLICIT' + + eval_modes = [ + ('GRID', "Grid", "Generate a default grid", 0), + ('EXPLICIT', "Explicit", "Evaluate the surface in the specified points", 1) + ] + + eval_mode : EnumProperty( + name = "Evaluation mode", + items = eval_modes, + default = 'GRID', + update = update_sockets) + + input_modes = [ + ('PAIRS', "Separate", "Separate U V (or X Y) sockets", 0), + ('VERTICES', "Vertices", "Single socket for vertices", 1) + ] + + input_mode : EnumProperty( + name = "Input mode", + items = input_modes, + default = 'PAIRS', + update = update_sockets) + + axes = [ + ('XY', "X Y", "XOY plane", 0), + ('YZ', "Y Z", "YOZ plane", 1), + ('XZ', "X Z", "XOZ plane", 2) + ] + + orientation : EnumProperty( + name = "Orientation", + items = axes, + default = 'XY', + update = updateNode) + + samples_u : IntProperty( + name = "Samples U", + description = "Number of samples along U direction", + default = 25, min = 3, + update = updateNode) + + samples_v : IntProperty( + name = "Samples V", + description = "Number of samples along V direction", + default = 25, min = 3, + update = updateNode) + + u_value : FloatProperty( + name = "U", + description = "Surface U parameter", + default = 0.5, + update = updateNode) + + v_value : FloatProperty( + name = "V", + description = "Surface V parameter", + default = 0.5, + update = updateNode) + + clamp_modes = [ + ('NO', "As is", "Do not clamp input values - try to process them as is (you will get either error or extrapolation on out-of-bounds values, depending on specific surface type", 0), + ('CLAMP', "Clamp", "Clamp input values into bounds - for example, turn -0.1 into 0", 1), + ('WRAP', "Wrap", "Wrap input values into bounds - for example, turn -0.1 into 0.9", 2) + ] + + clamp_mode : EnumProperty( + name = "Clamp", + items = clamp_modes, + default = 'NO', + update = updateNode) + + def draw_buttons(self, context, layout): + layout.label(text="Evaluate:") + layout.prop(self, "eval_mode", expand=True) + if self.eval_mode == 'EXPLICIT': + layout.label(text="Input mode:") + layout.prop(self, "input_mode", expand=True) + if self.input_mode == 'VERTICES': + layout.label(text="Input orientation:") + layout.prop(self, "orientation", expand=True) + layout.prop(self, 'clamp_mode', expand=True) + + def sv_init(self, context): + self.inputs.new('SvSurfaceSocket', "Surface") + self.inputs.new('SvStringsSocket', "U").prop_name = 'u_value' # 1 — U_SOCKET + self.inputs.new('SvStringsSocket', "V").prop_name = 'v_value' # 2 — V_SOCKET + self.inputs.new('SvVerticesSocket', "Vertices") # 3 + self.inputs.new('SvStringsSocket', "SamplesU").prop_name = 'samples_u' # 4 + self.inputs.new('SvStringsSocket', "SamplesV").prop_name = 'samples_v' # 5 + self.outputs.new('SvVerticesSocket', "Vertices") # 0 + self.outputs.new('SvStringsSocket', "Edges") + self.outputs.new('SvStringsSocket', "Faces") + self.update_sockets(context) + + def parse_input(self, verts): + verts = np.array(verts) + if self.orientation == 'XY': + us, vs = verts[:,0], verts[:,1] + elif self.orientation == 'YZ': + us, vs = verts[:,1], verts[:,2] + else: # XZ + us, vs = verts[:,0], verts[:,2] + return us, vs + + def _clamp(self, surface, us, vs): + u_min = surface.get_u_min() + u_max = surface.get_u_max() + v_min = surface.get_v_min() + v_max = surface.get_v_max() + us = np.clip(us, u_min, u_max) + vs = np.clip(vs, v_min, v_max) + return us, vs + + def _wrap(self, surface, us, vs): + u_min = surface.get_u_min() + u_max = surface.get_u_max() + v_min = surface.get_v_min() + v_max = surface.get_v_max() + u_len = u_max - u_min + v_len = v_max - u_min + us = (us - u_min) % u_len + u_min + vs = (vs - v_min) % v_len + v_min + return us, vs + + def build_output(self, surface, verts): + if surface.has_input_matrix: + orientation = surface.get_input_orientation() + if orientation == 'X': + reorder = np.array([2, 0, 1]) + verts = verts[:, reorder] + elif orientation == 'Y': + reorder = np.array([1, 2, 0]) + verts = verts[:, reorder] + else: # Z + pass + matrix = surface.get_input_matrix() + verts = verts - matrix.translation + np_matrix = np.array(matrix.to_3x3()) + verts = np.apply_along_axis(lambda v : np_matrix @ v, 1, verts) + return verts + + def make_grid_input(self, surface, samples_u, samples_v): + u_min = surface.get_u_min() + u_max = surface.get_u_max() + v_min = surface.get_v_min() + v_max = surface.get_v_max() + us = np.linspace(u_min, u_max, num=samples_u) + vs = np.linspace(v_min, v_max, num=samples_v) + us, vs = np.meshgrid(us, vs) + us = us.flatten() + vs = vs.flatten() + return us, vs + + def make_edges_xy(self, samples_u, samples_v): + edges = [] + for row in range(samples_v): + e_row = [(i + samples_u * row, (i+1) + samples_u * row) for i in range(samples_u-1)] + edges.extend(e_row) + if row < samples_v - 1: + e_col = [(i + samples_u * row, i + samples_u * (row+1)) for i in range(samples_u)] + edges.extend(e_col) + return edges + + def make_faces_xy(self, samples_u, samples_v): + faces = [] + for row in range(samples_v - 1): + for col in range(samples_u - 1): + i = row * samples_u + col + face = (i, i+samples_u, i+samples_u+1, i+1) + faces.append(face) + return faces + + def process(self): + if not any(socket.is_linked for socket in self.outputs): + return + + surfaces_s = self.inputs['Surface'].sv_get() + target_us_s = self.inputs[U_SOCKET].sv_get(default=[[]]) + target_vs_s = self.inputs[V_SOCKET].sv_get(default=[[]]) + target_verts_s = self.inputs['Vertices'].sv_get(default = [[]]) + samples_u_s = self.inputs['SamplesU'].sv_get() + samples_v_s = self.inputs['SamplesV'].sv_get() + + if isinstance(surfaces_s[0], SvSurface): + surfaces_s = [surfaces_s] + + samples_u_s = ensure_nesting_level(samples_u_s, 2) + samples_v_s = ensure_nesting_level(samples_v_s, 2) + target_us_s = ensure_nesting_level(target_us_s, 3) + target_vs_s = ensure_nesting_level(target_vs_s, 3) + target_verts_s = ensure_nesting_level(target_verts_s, 4) + + verts_out = [] + edges_out = [] + faces_out = [] + + inputs = zip_long_repeat(surfaces_s, target_us_s, target_vs_s, target_verts_s, samples_u_s, samples_v_s) + for surfaces, target_us_i, target_vs_i, target_verts_i, samples_u_i, samples_v_i in inputs: + objects = zip_long_repeat(surfaces, target_us_i, target_vs_i, target_verts_i, samples_u_i, samples_v_i) + for surface, target_us, target_vs, target_verts, samples_u, samples_v in objects: + + if self.eval_mode == 'GRID': + target_us, target_vs = self.make_grid_input(surface, samples_u, samples_v) + new_edges = self.make_edges_xy(samples_u, samples_v) + new_faces = self.make_faces_xy(samples_u, samples_v) + else: + if self.input_mode == 'VERTICES': + print(target_verts) + target_us, target_vs = self.parse_input(target_verts) + else: + target_us, target_vs = np.array(target_us), np.array(target_vs) + if self.clamp_mode == 'CLAMP': + target_us, target_vs = self._clamp(surface, target_us, target_vs) + elif self.clamp_mode == 'WRAP': + target_us, target_vs = self._wrap(surface, target_us, target_vs) + new_edges = [] + new_faces = [] + new_verts = surface.evaluate_array(target_us, target_vs) + + new_verts = self.build_output(surface, new_verts) + new_verts = new_verts.tolist() + verts_out.append(new_verts) + edges_out.append(new_edges) + faces_out.append(new_faces) + + self.outputs['Vertices'].sv_set(verts_out) + self.outputs['Edges'].sv_set(edges_out) + self.outputs['Faces'].sv_set(faces_out) + +def register(): + bpy.utils.register_class(SvEvalSurfaceNode) + +def unregister(): + bpy.utils.unregister_class(SvEvalSurfaceNode) + diff --git a/nodes/surface/extrude_curve.py b/nodes/surface/extrude_curve.py new file mode 100644 index 0000000000..bbe1c386e2 --- /dev/null +++ b/nodes/surface/extrude_curve.py @@ -0,0 +1,90 @@ + +import numpy as np + +import bpy +from bpy.props import FloatProperty, EnumProperty, BoolProperty, IntProperty + +from sverchok.node_tree import SverchCustomTreeNode, throttled +from sverchok.data_structure import updateNode, zip_long_repeat, ensure_nesting_level +from sverchok.utils.logging import info, exception + +from sverchok.utils.curve import SvCurve +from sverchok.utils.surface import SvExtrudeCurveCurveSurface, SvExtrudeCurveFrenetSurface, SvExtrudeCurveZeroTwistSurface + +class SvExtrudeCurveCurveSurfaceNode(bpy.types.Node, SverchCustomTreeNode): + """ + Triggers: Extrude Curve along Curve + Tooltip: Generate a surface by extruding a curve along another curve + """ + bl_idname = 'SvExExtrudeCurveCurveSurfaceNode' + bl_label = 'Extrude Curve Along Curve' + bl_icon = 'MOD_THICKNESS' + + modes = [ + ('NONE', "None", "No rotation", 0), + ('FRENET', "Frenet", "Frenet / native rotation", 1), + ('ZERO', "Zero-twist", "Zero-twist rotation", 2) + ] + + @throttled + def update_sockets(self, context): + self.inputs['Resolution'].hide_safe = self.algorithm != 'ZERO' + + algorithm : EnumProperty( + name = "Algorithm", + items = modes, + default = 'NONE', + update = update_sockets) + + resolution : IntProperty( + name = "Resolution", + min = 10, default = 50, + update = updateNode) + + def draw_buttons(self, context, layout): + layout.prop(self, "algorithm") + + def sv_init(self, context): + self.inputs.new('SvCurveSocket', "Profile") + self.inputs.new('SvCurveSocket', "Extrusion") + self.inputs.new('SvStringsSocket', "Resolution").prop_name = 'resolution' + self.outputs.new('SvSurfaceSocket', "Surface") + self.update_sockets(context) + + def process(self): + if not any(socket.is_linked for socket in self.outputs): + return + + profile_s = self.inputs['Profile'].sv_get() + extrusion_s = self.inputs['Extrusion'].sv_get() + resolution_s = self.inputs['Resolution'].sv_get() + + if isinstance(profile_s[0], SvCurve): + profile_s = [profile_s] + if isinstance(extrusion_s[0], SvCurve): + extrusion_s = [extrusion_s] + + surface_out = [] + for profiles, extrusions, resolution in zip_long_repeat(profile_s, extrusion_s, resolution_s): + if isinstance(resolution, (list, tuple)): + resolution = resolution[0] + + for profile, extrusion in zip_long_repeat(profiles, extrusions): + if self.algorithm == 'NONE': + surface = SvExtrudeCurveCurveSurface(profile, extrusion) + elif self.algorithm == 'FRENET': + surface = SvExtrudeCurveFrenetSurface(profile, extrusion) + elif self.algorithm == 'ZERO': + surface = SvExtrudeCurveZeroTwistSurface(profile, extrusion, resolution) + else: + raise Exception("Unsupported algorithm") + surface_out.append(surface) + + self.outputs['Surface'].sv_set(surface_out) + +def register(): + bpy.utils.register_class(SvExtrudeCurveCurveSurfaceNode) + +def unregister(): + bpy.utils.unregister_class(SvExtrudeCurveCurveSurfaceNode) + diff --git a/nodes/surface/extrude_point.py b/nodes/surface/extrude_point.py new file mode 100644 index 0000000000..970a5c9624 --- /dev/null +++ b/nodes/surface/extrude_point.py @@ -0,0 +1,54 @@ + +import numpy as np + +import bpy +from bpy.props import FloatProperty, EnumProperty, BoolProperty, IntProperty + +from sverchok.node_tree import SverchCustomTreeNode, throttled +from sverchok.data_structure import updateNode, zip_long_repeat, ensure_nesting_level + +from sverchok.utils.curve import SvCurve +from sverchok.utils.surface import SvExtrudeCurvePointSurface + +class SvExtrudeCurvePointNode(bpy.types.Node, SverchCustomTreeNode): + """ + Triggers: Extrude Curve to Point + Tooltip: Generate a (conic) surface by extruding a curve towards a point + """ + bl_idname = 'SvExExtrudeCurvePointNode' + bl_label = 'Extrude to Point' + bl_icon = 'OUTLINER_OB_EMPTY' + sv_icon = 'SV_EXTRUDE_CURVE_POINT' + + def sv_init(self, context): + self.inputs.new('SvCurveSocket', "Profile") + p = self.inputs.new('SvVerticesSocket', "Point") + p.use_prop = True + p.prop = (0.0, 0.0, 0.0) + self.outputs.new('SvSurfaceSocket', "Surface") + + def process(self): + if not any(socket.is_linked for socket in self.outputs): + return + + point_s = self.inputs['Point'].sv_get() + curve_s = self.inputs['Profile'].sv_get() + + if isinstance(curve_s[0], SvCurve): + curve_s = [curve_s] + point_s = ensure_nesting_level(point_s, 3) + + surface_out = [] + for curves, points in zip_long_repeat(curve_s, point_s): + for curve, point in zip_long_repeat(curves, points): + surface = SvExtrudeCurvePointSurface(curve, np.array(point)) + surface_out.append(surface) + + self.outputs['Surface'].sv_set(surface_out) + +def register(): + bpy.utils.register_class(SvExtrudeCurvePointNode) + +def unregister(): + bpy.utils.unregister_class(SvExtrudeCurvePointNode) + diff --git a/nodes/surface/extrude_vector.py b/nodes/surface/extrude_vector.py new file mode 100644 index 0000000000..eef87ac749 --- /dev/null +++ b/nodes/surface/extrude_vector.py @@ -0,0 +1,54 @@ + +import numpy as np + +import bpy +from bpy.props import FloatProperty, EnumProperty, BoolProperty, IntProperty + +from sverchok.node_tree import SverchCustomTreeNode, throttled +from sverchok.data_structure import updateNode, zip_long_repeat, ensure_nesting_level + +from sverchok.utils.curve import SvCurve +from sverchok.utils.surface import SvExtrudeCurveVectorSurface + +class SvExtrudeCurveVectorNode(bpy.types.Node, SverchCustomTreeNode): + """ + Triggers: Extrude Curve + Tooltip: Generate a surface by extruding a curve along a vector + """ + bl_idname = 'SvExExtrudeCurveVectorNode' + bl_label = 'Extrude Curve along Vector' + bl_icon = 'OUTLINER_OB_EMPTY' + sv_icon = 'SV_EXTRUDE_CURVE_VECTOR' + + def sv_init(self, context): + self.inputs.new('SvCurveSocket', "Profile") + p = self.inputs.new('SvVerticesSocket', "Vector") + p.use_prop = True + p.prop = (0.0, 0.0, 1.0) + self.outputs.new('SvSurfaceSocket', "Surface") + + def process(self): + if not any(socket.is_linked for socket in self.outputs): + return + + vector_s = self.inputs['Vector'].sv_get() + curve_s = self.inputs['Profile'].sv_get() + + if isinstance(curve_s[0], SvCurve): + curve_s = [curve_s] + vector_s = ensure_nesting_level(vector_s, 3) + + surface_out = [] + for curves, vectors in zip_long_repeat(curve_s, vector_s): + for curve, vector in zip_long_repeat(curves, vectors): + surface = SvExtrudeCurveVectorSurface(curve, vector) + surface_out.append(surface) + + self.outputs['Surface'].sv_set(surface_out) + +def register(): + bpy.utils.register_class(SvExtrudeCurveVectorNode) + +def unregister(): + bpy.utils.unregister_class(SvExtrudeCurveVectorNode) + diff --git a/nodes/surface/interpolating_surface.py b/nodes/surface/interpolating_surface.py new file mode 100644 index 0000000000..95f33915f5 --- /dev/null +++ b/nodes/surface/interpolating_surface.py @@ -0,0 +1,91 @@ + +import numpy as np + +import bpy +from bpy.props import FloatProperty, EnumProperty, BoolProperty, IntProperty +from mathutils import Matrix + +import sverchok +from sverchok.node_tree import SverchCustomTreeNode, throttled +from sverchok.data_structure import updateNode, zip_long_repeat, ensure_nesting_level, get_data_nesting_level +from sverchok.utils.geom import LinearSpline, CubicSpline +from sverchok.utils.surface import SvInterpolatingSurface +from sverchok.utils.curve import SvSplineCurve, make_euclidian_ts + +class SvInterpolatingSurfaceNode(bpy.types.Node, SverchCustomTreeNode): + """ + Triggers: Interpolating surface from curves + Tooltip: Generate interpolating surface across several curves + """ + bl_idname = 'SvInterpolatingSurfaceNode' + bl_label = 'Surface from Curves' + bl_icon = 'OUTLINER_OB_EMPTY' + sv_icon = 'SV_SURFACE_FROM_CURVES' + + def get_interp_modes(self, context): + modes = [ + ('LIN', "Linear", "Linear interpolation", 0), + ('CUBIC', "Cubic", "Cubic interpolation", 1) + ] + return modes + + interp_mode : EnumProperty( + name = "Interpolation mode", + items = get_interp_modes, + update = updateNode) + + is_cyclic : BoolProperty( + name = "Cyclic", + default = False, + update = updateNode) + + def get_u_spline_constructor(self): + if self.interp_mode == 'LIN': + def make(vertices): + spline = LinearSpline(vertices, metric='DISTANCE', is_cyclic=self.is_cyclic) + return SvSplineCurve(spline) + return make + elif self.interp_mode == 'CUBIC': + def make(vertices): + spline = CubicSpline(vertices, metric='DISTANCE', is_cyclic=self.is_cyclic) + return SvSplineCurve(spline) + return make + else: + raise Exception("Unsupported spline type: " + self.interp_mode) + + def draw_buttons(self, context, layout): + layout.label(text='Interpolation mode:') + layout.prop(self, 'interp_mode', text='') + layout.prop(self, 'is_cyclic', toggle=True) + + def sv_init(self, context): + self.inputs.new('SvCurveSocket', "Curves") + self.outputs.new('SvSurfaceSocket', "Surface") + self.update_sockets(context) + + def process(self): + if not any(socket.is_linked for socket in self.outputs): + return + + curves_s = self.inputs['Curves'].sv_get() + + if not isinstance(curves_s[0], (list, tuple)): + curves_s = [curves_s] + + surfaces_out = [] + for curves in curves_s: + + u_spline_constructor = self.get_u_spline_constructor() + v_bounds = (0.0, 1.0) + u_bounds = (0.0, 1.0) + surface = SvInterpolatingSurface(u_bounds, v_bounds, u_spline_constructor, curves) + surfaces_out.append(surface) + + self.outputs['Surface'].sv_set(surfaces_out) + +def register(): + bpy.utils.register_class(SvInterpolatingSurfaceNode) + +def unregister(): + bpy.utils.unregister_class(SvInterpolatingSurfaceNode) + diff --git a/nodes/surface/plane.py b/nodes/surface/plane.py new file mode 100644 index 0000000000..56c5652a16 --- /dev/null +++ b/nodes/surface/plane.py @@ -0,0 +1,136 @@ + +import numpy as np + +import bpy +from bpy.props import FloatProperty, EnumProperty, BoolProperty, IntProperty +from mathutils import Vector + +from sverchok.node_tree import SverchCustomTreeNode, throttled +from sverchok.data_structure import updateNode, zip_long_repeat, get_data_nesting_level, ensure_nesting_level +from sverchok.utils.surface import SvPlane + +class SvPlaneSurfaceNode(bpy.types.Node, SverchCustomTreeNode): + """ + Triggers: Plane + Tooltip: Generate planar surface + """ + bl_idname = 'SvExPlaneSurfaceNode' + bl_label = 'Plane (Surface)' + bl_icon = 'MESH_PLANE' + + modes = [ + ('3PT', "Three points", "Three points", 0), + ('NORM', "Point and normal", "Point and normal", 1) + ] + + @throttled + def update_sockets(self, context): + self.inputs['Point2'].hide_safe = self.mode != '3PT' + self.inputs['Point3'].hide_safe = self.mode != '3PT' + self.inputs['Normal'].hide_safe = self.mode != 'NORM' + + mode : EnumProperty( + name = "Mode", + items = modes, + default = '3PT', + update = update_sockets) + + u_min : FloatProperty( + name = "U Min", + default = 0.0, + update = updateNode) + + + u_max : FloatProperty( + name = "U Max", + default = 1.0, + update = updateNode) + + v_min : FloatProperty( + name = "V Min", + default = 0.0, + update = updateNode) + + + v_max : FloatProperty( + name = "V Max", + default = 1.0, + update = updateNode) + + def sv_init(self, context): + p = self.inputs.new('SvVerticesSocket', "Point1") + p.use_prop = True + p.prop = (0.0, 0.0, 0.0) + p = self.inputs.new('SvVerticesSocket', "Point2") + p.use_prop = True + p.prop = (1.0, 0.0, 0.0) + p = self.inputs.new('SvVerticesSocket', "Point3") + p.use_prop = True + p.prop = (0.0, 1.0, 0.0) + p = self.inputs.new('SvVerticesSocket', "Normal") + p.use_prop = True + p.prop = (0.0, 0.0, 1.0) + self.inputs.new('SvStringsSocket', "UMin").prop_name = 'u_min' + self.inputs.new('SvStringsSocket', "UMax").prop_name = 'u_max' + self.inputs.new('SvStringsSocket', "VMin").prop_name = 'v_min' + self.inputs.new('SvStringsSocket', "VMax").prop_name = 'v_max' + self.outputs.new('SvSurfaceSocket', "Surface") + self.update_sockets(context) + + def draw_buttons(self, context, layout): + layout.prop(self, "mode", text="") + + def process(self): + if not any(socket.is_linked for socket in self.outputs): + return + + point1_s = self.inputs['Point1'].sv_get() + point2_s = self.inputs['Point2'].sv_get() + point3_s = self.inputs['Point3'].sv_get() + normal_s = self.inputs['Normal'].sv_get() + u_min_s = self.inputs['UMin'].sv_get() + u_max_s = self.inputs['UMax'].sv_get() + v_min_s = self.inputs['VMin'].sv_get() + v_max_s = self.inputs['VMax'].sv_get() + + point1_s = ensure_nesting_level(point1_s, 3) + point2_s = ensure_nesting_level(point2_s, 3) + point3_s = ensure_nesting_level(point3_s, 3) + normal_s = ensure_nesting_level(normal_s, 3) + u_min_s = ensure_nesting_level(u_min_s, 2) + u_max_s = ensure_nesting_level(u_max_s, 2) + v_min_s = ensure_nesting_level(v_min_s, 2) + v_max_s = ensure_nesting_level(v_max_s, 2) + + surfaces_out = [] + inputs = zip_long_repeat(point1_s, point2_s, point3_s, normal_s, u_min_s, u_max_s, v_min_s, v_max_s) + for point1s, point2s, point3s, normals, u_mins, u_maxs, v_mins, v_maxs in inputs: + objects = zip_long_repeat(point1s, point2s, point3s, normals, u_mins, u_maxs, v_mins, v_maxs) + for point1, point2, point3, normal, u_min, u_max, v_min, v_max in objects: + + point1 = np.array(point1) + if self.mode == '3PT': + point2 = np.array(point2) + point3 = np.array(point3) + vec1 = point2 - point1 + vec2 = point3 - point1 + else: + vec1 = np.array( Vector(normal).orthogonal() ) + vec2 = np.cross(np.array(normal), vec1) + v1n = np.linalg.norm(vec1) + v2n = np.linalg.norm(vec2) + vec1, vec2 = vec1 / v1n, vec2 / v2n + + plane = SvPlane(point1, vec1, vec2) + plane.u_bounds = (u_min, u_max) + plane.v_bounds = (v_min, v_max) + surfaces_out.append(plane) + + self.outputs['Surface'].sv_set(surfaces_out) + +def register(): + bpy.utils.register_class(SvPlaneSurfaceNode) + +def unregister(): + bpy.utils.unregister_class(SvPlaneSurfaceNode) + diff --git a/nodes/surface/revolution_surface.py b/nodes/surface/revolution_surface.py new file mode 100644 index 0000000000..eff4253228 --- /dev/null +++ b/nodes/surface/revolution_surface.py @@ -0,0 +1,78 @@ + +import numpy as np +from math import pi + +import bpy +from bpy.props import FloatProperty, EnumProperty, BoolProperty, IntProperty + +from sverchok.node_tree import SverchCustomTreeNode, throttled +from sverchok.data_structure import updateNode, zip_long_repeat, ensure_nesting_level +from sverchok.utils.logging import info, exception +from sverchok.utils.curve import SvCurve +from sverchok.utils.surface import SvRevolutionSurface + +class SvRevolutionSurfaceNode(bpy.types.Node, SverchCustomTreeNode): + """ + Triggers: Revolution Surface + Tooltip: Generate a surface of revolution (similar to Spin / Lathe modifier) + """ + bl_idname = 'SvExRevolutionSurfaceNode' + bl_label = 'Revolution Surface' + bl_icon = 'MOD_SCREW' + + v_min : FloatProperty( + name = "Angle From", + description = "Minimal value of V surface parameter", + default = 0.0, + update = updateNode) + + v_max : FloatProperty( + name = "Angle To", + description = "Minimal value of V surface parameter", + default = 2*pi, + update = updateNode) + + def sv_init(self, context): + self.inputs.new('SvCurveSocket', "Profile") + p = self.inputs.new('SvVerticesSocket', "Point") + p.use_prop = True + p.prop = (0.0, 0.0, 0.0) + p = self.inputs.new('SvVerticesSocket', "Direction") + p.use_prop = True + p.prop = (0.0, 0.0, 1.0) + self.inputs.new('SvStringsSocket', 'AngleFrom').prop_name = 'v_min' + self.inputs.new('SvStringsSocket', 'AngleTo').prop_name = 'v_max' + self.outputs.new('SvSurfaceSocket', "Surface") + + def process(self): + if not any(socket.is_linked for socket in self.outputs): + return + + point_s = self.inputs['Point'].sv_get() + direction_s = self.inputs['Direction'].sv_get() + curve_s = self.inputs['Profile'].sv_get() + v_min_s = self.inputs['AngleFrom'].sv_get() + v_max_s = self.inputs['AngleTo'].sv_get() + + if isinstance(curve_s[0], SvCurve): + curve_s = [curve_s] + point_s = ensure_nesting_level(point_s, 3) + direction_s = ensure_nesting_level(direction_s, 3) + v_min_s = ensure_nesting_level(v_min_s, 2) + v_max_s = ensure_nesting_level(v_max_s, 2) + + surface_out = [] + for curves, points, directions, v_mins, v_maxs in zip_long_repeat(curve_s, point_s, direction_s, v_min_s, v_max_s): + for curve, point, direction, v_min, v_max in zip_long_repeat(curves, points, directions, v_mins, v_maxs): + surface = SvRevolutionSurface(curve, np.array(point), np.array(direction)) + surface.v_bounds = (v_min, v_max) + surface_out.append(surface) + + self.outputs['Surface'].sv_set(surface_out) + +def register(): + bpy.utils.register_class(SvRevolutionSurfaceNode) + +def unregister(): + bpy.utils.unregister_class(SvRevolutionSurfaceNode) + diff --git a/nodes/surface/sphere.py b/nodes/surface/sphere.py new file mode 100644 index 0000000000..dc36961d5e --- /dev/null +++ b/nodes/surface/sphere.py @@ -0,0 +1,93 @@ + +import numpy as np +from math import pi + +import bpy +from bpy.props import FloatProperty, EnumProperty, BoolProperty, IntProperty + +from sverchok.node_tree import SverchCustomTreeNode, throttled +from sverchok.data_structure import updateNode, zip_long_repeat, get_data_nesting_level +from sverchok.utils.surface import SvLambertSphere, SvEquirectSphere, SvGallSphere, SvDefaultSphere + +class SvSphereNode(bpy.types.Node, SverchCustomTreeNode): + """ + Triggers: Sphere + Tooltip: Generate spherical surface (different projections are available) + """ + bl_idname = 'SvExSphereNode' + bl_label = 'Sphere (Surface)' + bl_icon = 'MESH_UVSPHERE' + + projections = [ + ('DEFAULT', "Default", "Based on spherical coordinates", 0), + ('EQUIRECT', "Equirectangular", "Equirectangular (geographic) projection", 1), + ('LAMBERT', "Lambert", "Lambert cylindrical equal-area projection", 2), + ('GALL', "Gall Stereographic", "Gall stereographic projection", 3) + ] + + @throttled + def update_sockets(self, context): + self.inputs['Theta1'].hide_safe = self.projection != 'EQUIRECT' + + projection : EnumProperty( + name = "Projection", + items = projections, + default = 'DEFAULT', + update = update_sockets) + + radius : FloatProperty( + name = "Radius", + default = 1.0, + update = updateNode) + + theta1 : FloatProperty( + name = "Theta1", + description = "standard parallels (north and south of the equator) where the scale of the projection is true", + default = pi/4, + min = 0, max = pi/2, + update = updateNode) + + def sv_init(self, context): + p = self.inputs.new('SvVerticesSocket', "Center") + p.use_prop = True + p.prop = (0.0, 0.0, 0.0) + self.inputs.new('SvStringsSocket', "Radius").prop_name = 'radius' + self.inputs.new('SvStringsSocket', "Theta1").prop_name = 'theta1' + self.outputs.new('SvSurfaceSocket', "Surface") + self.update_sockets(context) + + def draw_buttons(self, context, layout): + layout.prop(self, "projection") + + def process(self): + if not any(socket.is_linked for socket in self.outputs): + return + + center_s = self.inputs['Center'].sv_get() + radius_s = self.inputs['Radius'].sv_get() + theta1_s = self.inputs['Theta1'].sv_get() + + surfaces_out = [] + for center, radius, theta1 in zip_long_repeat(center_s, radius_s, theta1_s): + if isinstance(radius, (list, tuple)): + radius = radius[0] + if isinstance(theta1, (list, tuple)): + theta1 = theta1[0] + + if self.projection == 'DEFAULT': + surface = SvDefaultSphere(np.array(center), radius) + elif self.projection == 'EQUIRECT': + surface = SvEquirectSphere(np.array(center), radius, theta1) + elif self.projection == 'LAMBERT': + surface = SvLambertSphere(np.array(center), radius) + else: + surface = SvGallSphere(np.array(center), radius) + surfaces_out.append(surface) + self.outputs['Surface'].sv_set(surfaces_out) + +def register(): + bpy.utils.register_class(SvSphereNode) + +def unregister(): + bpy.utils.unregister_class(SvSphereNode) + diff --git a/nodes/surface/subdomain.py b/nodes/surface/subdomain.py new file mode 100644 index 0000000000..1391b39d82 --- /dev/null +++ b/nodes/surface/subdomain.py @@ -0,0 +1,82 @@ + +import numpy as np + +import bpy +from bpy.props import FloatProperty, EnumProperty, BoolProperty, IntProperty + +from sverchok.node_tree import SverchCustomTreeNode, throttled +from sverchok.data_structure import updateNode, zip_long_repeat, ensure_nesting_level +from sverchok.utils.logging import info, exception +from sverchok.utils.surface import SvSurface, SvSurfaceSubdomain + +class SvSurfaceSubdomainNode(bpy.types.Node, SverchCustomTreeNode): + """ + Triggers: Surface Subdomain (iso trim) + Tooltip: Take a sub-domain of the surface - trim a surface along constant U/V curves. + """ + bl_idname = 'SvExSurfaceSubdomainNode' + bl_label = 'Surface Subdomain' + bl_icon = 'OUTLINER_OB_EMPTY' + sv_icon = 'SV_SURFACE_SUBDOMAIN' + + u_min : FloatProperty( + name = "U Min", + default = 0.0, + update = updateNode) + + v_min : FloatProperty( + name = "V Min", + default = 0.0, + update = updateNode) + + + u_max : FloatProperty( + name = "U Max", + default = 1.0, + update = updateNode) + + v_max : FloatProperty( + name = "V Max", + default = 1.0, + update = updateNode) + + def sv_init(self, context): + self.inputs.new('SvSurfaceSocket', "Surface") + self.inputs.new('SvStringsSocket', "UMin").prop_name = 'u_min' + self.inputs.new('SvStringsSocket', "UMax").prop_name = 'u_max' + self.inputs.new('SvStringsSocket', "VMin").prop_name = 'v_min' + self.inputs.new('SvStringsSocket', "VMax").prop_name = 'v_max' + self.outputs.new('SvSurfaceSocket', "Surface") + + def process(self): + if not any(socket.is_linked for socket in self.outputs): + return + + surface_s = self.inputs['Surface'].sv_get() + u_min_s = self.inputs['UMin'].sv_get() + u_max_s = self.inputs['UMax'].sv_get() + v_min_s = self.inputs['VMin'].sv_get() + v_max_s = self.inputs['VMax'].sv_get() + + u_min_s = ensure_nesting_level(u_min_s, 2) + u_max_s = ensure_nesting_level(u_max_s, 2) + v_min_s = ensure_nesting_level(v_min_s, 2) + v_max_s = ensure_nesting_level(v_max_s, 2) + + if isinstance(surface_s[0], SvSurface): + surface_s = [surface_s] + + surface_out = [] + for surfaces, u_mins, u_maxs, v_mins, v_maxs in zip_long_repeat(surface_s, u_min_s, u_max_s, v_min_s, v_max_s): + for surface, u_min, u_max, v_min, v_max in zip_long_repeat(surfaces, u_mins, u_maxs, v_mins, v_maxs): + new_surface = SvSurfaceSubdomain(surface, (u_min, u_max), (v_min, v_max)) + surface_out.append(new_surface) + + self.outputs['Surface'].sv_set(surface_out) + +def register(): + bpy.utils.register_class(SvSurfaceSubdomainNode) + +def unregister(): + bpy.utils.unregister_class(SvSurfaceSubdomainNode) + diff --git a/nodes/surface/surface_domain.py b/nodes/surface/surface_domain.py new file mode 100644 index 0000000000..9b92012c71 --- /dev/null +++ b/nodes/surface/surface_domain.py @@ -0,0 +1,79 @@ +import bpy +from bpy.props import FloatProperty, EnumProperty, BoolProperty, IntProperty + +from sverchok.node_tree import SverchCustomTreeNode, throttled +from sverchok.data_structure import updateNode, zip_long_repeat, ensure_nesting_level +from sverchok.utils.surface import SvSurface + +class SvSurfaceDomainNode(bpy.types.Node, SverchCustomTreeNode): + """ + Triggers: Surface Domain / Range + Tooltip: Output minimum and maximum values of U / V parameters allowed by the surface + """ + bl_idname = 'SvExSurfaceDomainNode' + bl_label = 'Surface Domain' + bl_icon = 'OUTLINER_OB_EMPTY' + sv_icon = 'SV_SURFACE_DOMAIN' + + def sv_init(self, context): + self.inputs.new('SvSurfaceSocket', "Surface") + self.outputs.new('SvStringsSocket', "UMin") + self.outputs.new('SvStringsSocket', "UMax") + self.outputs.new('SvStringsSocket', "URange") + self.outputs.new('SvStringsSocket', "VMin") + self.outputs.new('SvStringsSocket', "VMax") + self.outputs.new('SvStringsSocket', "VRange") + + def process(self): + if not any(socket.is_linked for socket in self.outputs): + return + + surface_s = self.inputs['Surface'].sv_get() + if isinstance(surface_s[0], SvSurface): + surface_s = [surface_s] + + u_min_out = [] + u_max_out = [] + u_range_out = [] + v_min_out = [] + v_max_out = [] + v_range_out = [] + for surfaces in surface_s: + new_u_min, new_u_max = [], [] + new_v_min, new_v_max = [], [] + new_u_range, new_v_range = [], [] + for surface in surfaces: + u_min, u_max = surface.get_u_min(), surface.get_u_max() + v_min, v_max = surface.get_v_min(), surface.get_v_max() + u_range = u_max - u_min + v_range = v_max - v_min + + new_u_min.append(u_min) + new_u_max.append(u_max) + new_u_range.append(u_range) + + new_v_min.append(v_min) + new_v_max.append(v_max) + new_v_range.append(v_range) + + u_min_out.append(new_u_min) + u_max_out.append(new_u_max) + u_range_out.append(new_u_range) + + v_min_out.append(new_v_min) + v_max_out.append(new_v_max) + v_range_out.append(new_v_range) + + self.outputs['UMin'].sv_set(u_min_out) + self.outputs['UMax'].sv_set(u_max_out) + self.outputs['URange'].sv_set(u_range_out) + self.outputs['VMin'].sv_set(v_min_out) + self.outputs['VMax'].sv_set(v_max_out) + self.outputs['VRange'].sv_set(v_range_out) + +def register(): + bpy.utils.register_class(SvSurfaceDomainNode) + +def unregister(): + bpy.utils.unregister_class(SvSurfaceDomainNode) + diff --git a/nodes/surface/surface_formula.py b/nodes/surface/surface_formula.py new file mode 100644 index 0000000000..aeab2dcbad --- /dev/null +++ b/nodes/surface/surface_formula.py @@ -0,0 +1,188 @@ + +import numpy as np +import math + +import bpy +from bpy.props import FloatProperty, EnumProperty, BoolProperty, IntProperty, StringProperty + +from sverchok.node_tree import SverchCustomTreeNode, throttled +from sverchok.data_structure import updateNode, zip_long_repeat, match_long_repeat, ensure_nesting_level +from sverchok.utils.modules.eval_formula import get_variables, sv_compile, safe_eval_compiled +from sverchok.utils.logging import info, exception +from sverchok.utils.math import from_cylindrical, from_spherical, to_cylindrical, to_spherical +from sverchok.utils.math import coordinate_modes +from sverchok.utils.surface import SvLambdaSurface + +class SvSurfaceFormulaNode(bpy.types.Node, SverchCustomTreeNode): + """ + Triggers: Surface Formula + Tooltip: Generate surface by formula + """ + bl_idname = 'SvExSurfaceFormulaNode' + bl_label = 'Surface Formula' + bl_icon = 'OUTLINER_OB_EMPTY' + sv_icon = 'SV_SURFACE_FORMULA' + + @throttled + def on_update(self, context): + self.adjust_sockets() + + formula1: StringProperty( + name = "Formula", + default = "(2 + 0.5*cos(u))*cos(v)", + update = on_update) + + formula2: StringProperty( + name = "Formula", + default = "(2 + 0.5*cos(u))*sin(v)", + update = on_update) + + formula3: StringProperty( + name = "Formula", + default = "0.5*sin(u)", + update = on_update) + + output_mode : EnumProperty( + name = "Coordinates", + items = coordinate_modes, + default = 'XYZ', + update = updateNode) + + u_min : FloatProperty( + name = "U Min", + default = 0, + update = updateNode) + + u_max : FloatProperty( + name = "U Max", + default = 2*math.pi, + update = updateNode) + + v_min : FloatProperty( + name = "V Min", + default = 0, + update = updateNode) + + v_max : FloatProperty( + name = "V Max", + default = 2*math.pi, + update = updateNode) + + def sv_init(self, context): + self.inputs.new('SvStringsSocket', 'UMin').prop_name = 'u_min' + self.inputs.new('SvStringsSocket', 'UMax').prop_name = 'u_max' + self.inputs.new('SvStringsSocket', 'VMin').prop_name = 'v_min' + self.inputs.new('SvStringsSocket', 'VMax').prop_name = 'v_max' + self.outputs.new('SvSurfaceSocket', 'Surface') + + def draw_buttons(self, context, layout): + layout.prop(self, "formula1", text="") + layout.prop(self, "formula2", text="") + layout.prop(self, "formula3", text="") + layout.label(text="Output:") + layout.prop(self, "output_mode", expand=True) + + def make_function(self, variables): + compiled1 = sv_compile(self.formula1) + compiled2 = sv_compile(self.formula2) + compiled3 = sv_compile(self.formula3) + + if self.output_mode == 'XYZ': + def out_coordinates(x, y, z): + return x, y, z + elif self.output_mode == 'CYL': + def out_coordinates(rho, phi, z): + return from_cylindrical(rho, phi, z, mode='radians') + else: # SPH + def out_coordinates(rho, phi, theta): + return from_spherical(rho, phi, theta, mode='radians') + + def function(u, v): + variables.update(dict(u=u, v=v)) + v1 = safe_eval_compiled(compiled1, variables) + v2 = safe_eval_compiled(compiled2, variables) + v3 = safe_eval_compiled(compiled3, variables) + return np.array(out_coordinates(v1, v2, v3)) + + return function + + def get_coordinate_variables(self): + return {'u', 'v'} + + def get_variables(self): + variables = set() + for formula in [self.formula1, self.formula2, self.formula3]: + new_vars = get_variables(formula) + variables.update(new_vars) + variables.difference_update(self.get_coordinate_variables()) + return list(sorted(list(variables))) + + def adjust_sockets(self): + variables = self.get_variables() + for key in self.inputs.keys(): + if key not in variables and key not in {'UMin', 'UMax', 'VMin', 'VMax'}: + self.debug("Input {} not in variables {}, remove it".format(key, str(variables))) + self.inputs.remove(self.inputs[key]) + for v in variables: + if v not in self.inputs: + self.debug("Variable {} not in inputs {}, add it".format(v, str(self.inputs.keys()))) + self.inputs.new('SvStringsSocket', v) + + def update(self): + if not self.formula1 and not self.formula2 and not self.formula3: + return + self.adjust_sockets() + + def get_input(self): + variables = self.get_variables() + inputs = {} + + for var in variables: + if var in self.inputs and self.inputs[var].is_linked: + inputs[var] = self.inputs[var].sv_get() + return inputs + + def process(self): + if not any(socket.is_linked for socket in self.outputs): + return + + u_min_s = self.inputs['UMin'].sv_get() + u_max_s = self.inputs['UMax'].sv_get() + u_min_s = ensure_nesting_level(u_min_s, 2) + u_max_s = ensure_nesting_level(u_max_s, 2) + + v_min_s = self.inputs['VMin'].sv_get() + v_max_s = self.inputs['VMax'].sv_get() + v_min_s = ensure_nesting_level(v_min_s, 2) + v_max_s = ensure_nesting_level(v_max_s, 2) + + var_names = self.get_variables() + inputs = self.get_input() + input_values = [inputs.get(name, [[0]]) for name in var_names] + if var_names: + parameters = match_long_repeat([u_min_s, u_max_s, v_min_s, v_max_s] + input_values) + else: + parameters = [u_min_s, u_max_s, v_min_s, v_max_s] + + surfaces_out = [] + for u_mins, u_maxs, v_mins, v_maxs, *objects in zip(*parameters): + if var_names: + var_values_s = zip_long_repeat(u_mins, u_maxs, v_mins, v_maxs, *objects) + else: + var_values_s = zip_long_repeat(u_mins, u_maxs, v_mins, v_maxs) + for u_min, u_max, v_min, v_max, *var_values in var_values_s: + variables = dict(zip(var_names, var_values)) + function = self.make_function(variables.copy()) + new_surface = SvLambdaSurface(function) + new_surface.u_bounds = (u_min, u_max) + new_surface.v_bounds = (v_min, v_max) + surfaces_out.append(new_surface) + + self.outputs['Surface'].sv_set(surfaces_out) + +def register(): + bpy.utils.register_class(SvSurfaceFormulaNode) + +def unregister(): + bpy.utils.unregister_class(SvSurfaceFormulaNode) + diff --git a/nodes/surface/surface_lerp.py b/nodes/surface/surface_lerp.py new file mode 100644 index 0000000000..91c9c24878 --- /dev/null +++ b/nodes/surface/surface_lerp.py @@ -0,0 +1,60 @@ + +import numpy as np + +import bpy +from bpy.props import FloatProperty, EnumProperty, BoolProperty, IntProperty + +from sverchok.node_tree import SverchCustomTreeNode, throttled +from sverchok.data_structure import updateNode, zip_long_repeat, ensure_nesting_level +from sverchok.utils.logging import info, exception +from sverchok.utils.surface import SvSurface, SvSurfaceLerpSurface + +class SvSurfaceLerpNode(bpy.types.Node, SverchCustomTreeNode): + """ + Triggers: Surface Lerp Linear + Tooltip: Linear interpolation of two surfaces + """ + bl_idname = 'SvExSurfaceLerpNode' + bl_label = 'Surface Lerp' + bl_icon = 'MOD_THICKNESS' + + coefficient : FloatProperty( + name = "Coefficient", + default = 0.5, + update = updateNode) + + def sv_init(self, context): + self.inputs.new('SvSurfaceSocket', "Surface1") + self.inputs.new('SvSurfaceSocket', "Surface2") + self.inputs.new('SvStringsSocket', "Coefficient").prop_name = 'coefficient' + self.outputs.new('SvSurfaceSocket', "Surface") + + def process(self): + if not any(socket.is_linked for socket in self.outputs): + return + + surface1_s = self.inputs['Surface1'].sv_get() + surface2_s = self.inputs['Surface2'].sv_get() + coeff_s = self.inputs['Coefficient'].sv_get() + + coeff_s = ensure_nesting_level(coeff_s, 2) + + if isinstance(surface1_s[0], SvSurface): + surface1_s = [surface1_s] + if isinstance(surface2_s[0], SvSurface): + surface2_s = [surface2_s] + + surface_out = [] + for surface1s, surface2s, coeffs in zip_long_repeat(surface1_s, surface2_s, coeff_s): + for surface1, surface2, coeff in zip_long_repeat(surface1s, surface2s, coeffs): + new_surface = SvSurfaceLerpSurface(surface1, surface2, coeff) + surface_out.append(new_surface) + + self.outputs['Surface'].sv_set(surface_out) + +def register(): + bpy.utils.register_class(SvSurfaceLerpNode) + +def unregister(): + bpy.utils.unregister_class(SvSurfaceLerpNode) + diff --git a/nodes/surface/taper_sweep.py b/nodes/surface/taper_sweep.py new file mode 100644 index 0000000000..479d89ae78 --- /dev/null +++ b/nodes/surface/taper_sweep.py @@ -0,0 +1,64 @@ + +import numpy as np + +import bpy +from bpy.props import FloatProperty, EnumProperty, BoolProperty, IntProperty + +from sverchok.node_tree import SverchCustomTreeNode, throttled +from sverchok.data_structure import updateNode, zip_long_repeat, ensure_nesting_level +from sverchok.utils.logging import info, exception +from sverchok.utils.curve import SvCurve +from sverchok.utils.surface import SvTaperSweepSurface + +class SvTaperSweepSurfaceNode(bpy.types.Node, SverchCustomTreeNode): + """ + Triggers: Taper Sweep Curve + Tooltip: Generate a taper surface along a line + """ + bl_idname = 'SvExTaperSweepSurfaceNode' + bl_label = 'Taper Sweep' + bl_icon = 'OUTLINER_OB_EMPTY' + sv_icon = 'SV_TAPER_SWEEP' + + def sv_init(self, context): + self.inputs.new('SvCurveSocket', "Profile") + self.inputs.new('SvCurveSocket', "Taper") + p = self.inputs.new('SvVerticesSocket', "Point") + p.use_prop = True + p.prop = (0.0, 0.0, 0.0) + p = self.inputs.new('SvVerticesSocket', "Direction") + p.use_prop = True + p.prop = (0.0, 0.0, 1.0) + self.outputs.new('SvSurfaceSocket', "Surface") + + def process(self): + if not any(socket.is_linked for socket in self.outputs): + return + + profile_s = self.inputs['Profile'].sv_get() + taper_s = self.inputs['Taper'].sv_get() + point_s = self.inputs['Point'].sv_get() + direction_s = self.inputs['Direction'].sv_get() + + if isinstance(profile_s[0], SvCurve): + profile_s = [profile_s] + if isinstance(taper_s[0], SvCurve): + taper_s = [taper_s] + + point_s = ensure_nesting_level(point_s, 3) + direction_s = ensure_nesting_level(direction_s, 3) + + surface_out = [] + for profiles, tapers, points, directions in zip_long_repeat(profile_s, taper_s, point_s, direction_s): + for profile, taper, point, direction in zip_long_repeat(profiles, tapers, points, directions): + surface = SvTaperSweepSurface(profile, taper, np.array(point), np.array(direction)) + surface_out.append(surface) + + self.outputs['Surface'].sv_set(surface_out) + +def register(): + bpy.utils.register_class(SvTaperSweepSurfaceNode) + +def unregister(): + bpy.utils.unregister_class(SvTaperSweepSurfaceNode) + diff --git a/nodes/surface/tessellate_trim.py b/nodes/surface/tessellate_trim.py new file mode 100644 index 0000000000..279306c751 --- /dev/null +++ b/nodes/surface/tessellate_trim.py @@ -0,0 +1,185 @@ + +import numpy as np + +import bpy +from bpy.props import EnumProperty, IntProperty + +import sverchok +from sverchok.node_tree import SverchCustomTreeNode, throttled +from sverchok.data_structure import updateNode, zip_long_repeat, ensure_nesting_level, get_data_nesting_level +from sverchok.utils.logging import info, exception +from sverchok.utils.geom_2d.merge_mesh import crop_mesh_delaunay + +from sverchok.utils.curve import SvCurve +from sverchok.utils.surface import SvSurface + +# This node requires delaunay_cdt function, which is available +# since Blender 2.81 only. So the node will not be available in +# Blender 2.80. + +class SvTessellateTrimSurfaceNode(bpy.types.Node, SverchCustomTreeNode): + """ + Triggers: Tessellate Trim Surface + Tooltip: Tessellate a surface with trimming curve + """ + bl_idname = 'SvExTessellateTrimSurfaceNode' + bl_label = 'Tessellate & Trim Surface' + bl_icon = 'OUTLINER_OB_EMPTY' + sv_icon = 'SV_TRIM_TESSELLATE' + + axes = [ + ('XY', "X Y", "XOY plane", 0), + ('YZ', "Y Z", "YOZ plane", 1), + ('XZ', "X Z", "XOZ plane", 2) + ] + + orientation : EnumProperty( + name = "Curve orientation", + items = axes, + default = 'XY', + update = updateNode) + + samples_u : IntProperty( + name = "Samples U", + default = 25, min = 3, + update = updateNode) + + samples_v : IntProperty( + name = "Samples V", + default = 25, min = 3, + update = updateNode) + + samples_t : IntProperty( + name = "Curve Samples", + default = 100, min = 3, + update = updateNode) + + mode_items = [('inner', 'Inner', 'Fit mesh', 'SELECT_INTERSECT', 0), + ('outer', 'Outer', 'Make hole', 'SELECT_SUBTRACT', 1)] + + crop_mode: bpy.props.EnumProperty( + items=mode_items, + default = 'inner', + name='Mode of cropping mesh', + update=updateNode, + description='Switch between creating holes and fitting mesh into another mesh') + + accuracy: bpy.props.IntProperty( + name='Accuracy', update=updateNode, default=5, min=3, max=12, + description='Some errors of the node can be fixed by changing this value') + + def draw_buttons(self, context, layout): + if crop_mesh_delaunay: + layout.label(text="Curve plane:") + layout.prop(self, "orientation", expand=True) + layout.prop(self, 'crop_mode', expand=True) + else: + layout.label("Unsupported Blender version") + + def draw_buttons_ext(self, context, layout): + self.draw_buttons(context, layout) + layout.prop(self, 'accuracy') + + def sv_init(self, context): + self.inputs.new('SvSurfaceSocket', "Surface") + self.inputs.new('SvCurveSocket', "TrimCurve") + self.inputs.new('SvStringsSocket', "SamplesU").prop_name = 'samples_u' + self.inputs.new('SvStringsSocket', "SamplesV").prop_name = 'samples_v' + self.inputs.new('SvStringsSocket', "CurveSamples").prop_name = 'samples_t' + self.outputs.new('SvVerticesSocket', "Vertices") + #self.outputs.new('SvStringsSocket', "Edges") + self.outputs.new('SvStringsSocket', "Faces") + + def make_grid(self, surface, samples_u, samples_v): + u_min = surface.get_u_min() + u_max = surface.get_u_max() + v_min = surface.get_v_min() + v_max = surface.get_v_max() + us = np.linspace(u_min, u_max, num=samples_u) + vs = np.linspace(v_min, v_max, num=samples_v) + us, vs = np.meshgrid(us, vs) + us = us.flatten() + vs = vs.flatten() + return [(u, v, 0.0) for u, v in zip(us, vs)] + + def make_edges_xy(self, samples_u, samples_v): + edges = [] + for row in range(samples_v): + e_row = [(i + samples_u * row, (i+1) + samples_u * row) for i in range(samples_u-1)] + edges.extend(e_row) + if row < samples_v - 1: + e_col = [(i + samples_u * row, i + samples_u * (row+1)) for i in range(samples_u)] + edges.extend(e_col) + return edges + + def make_faces_xy(self, samples_u, samples_v): + faces = [] + for row in range(samples_v - 1): + for col in range(samples_u - 1): + i = row * samples_u + col + face = (i, i+samples_u, i+samples_u+1, i+1) + faces.append(face) + return faces + + def make_curve(self, curve, samples_t): + t_min, t_max = curve.get_u_bounds() + ts = np.linspace(t_min, t_max, num=samples_t) + verts = curve.evaluate_array(ts).tolist() + n = len(ts) + edges = [[i, i + 1] for i in range(n - 1)] + edges.append([n-1, 0]) + faces = [list(range(n))] + return verts, edges, faces + + def process(self): + if not any(socket.is_linked for socket in self.outputs): + return + if not crop_mesh_delaunay: + raise Exception("Unsupported Blender version!") + + surfaces_s = self.inputs['Surface'].sv_get() + curves_s = self.inputs['TrimCurve'].sv_get() + samples_u_s = self.inputs['SamplesU'].sv_get() + samples_v_s = self.inputs['SamplesV'].sv_get() + samples_t_s = self.inputs['CurveSamples'].sv_get() + + if isinstance(surfaces_s[0], SvSurface): + surfaces_s = [surfaces_s] + if isinstance(curves_s[0], SvCurve): + curves_s = [curves_s] + + samples_u_s = ensure_nesting_level(samples_u_s, 2) + samples_v_s = ensure_nesting_level(samples_v_s, 2) + samples_t_s = ensure_nesting_level(samples_t_s, 2) + + verts_out = [] + faces_out = [] + inputs = zip_long_repeat(surfaces_s, curves_s, samples_u_s, samples_v_s, samples_t_s) + for surfaces, curves, samples_u_i, samples_v_i, samples_t_i in inputs: + objects = zip_long_repeat(surfaces, curves, samples_u_i, samples_v_i, samples_t_i) + for surface, curve, samples_u, samples_v, samples_t in objects: + + crop_verts, crop_edges, crop_faces = self.make_curve(curve, samples_t) + grid_verts = self.make_grid(surface, samples_u, samples_v) + #grid_edges = self.make_edges_xy(samples_u, samples_v) + grid_faces = self.make_faces_xy(samples_u, samples_v) + + epsilon = 1.0 / 10**self.accuracy + xy_verts, new_faces, _ = crop_mesh_delaunay(grid_verts, grid_faces, crop_verts, crop_faces, self.crop_mode, epsilon) + + us = np.array([vert[0] for vert in xy_verts]) + vs = np.array([vert[1] for vert in xy_verts]) + new_verts = surface.evaluate_array(us, vs).tolist() + + verts_out.append(new_verts) + faces_out.append(new_faces) + + self.outputs['Vertices'].sv_set(verts_out) + self.outputs['Faces'].sv_set(faces_out) + +def register(): + bpy.utils.register_class(SvTessellateTrimSurfaceNode) + +def unregister(): + bpy.utils.unregister_class(SvTessellateTrimSurfaceNode) + diff --git a/tests/docs_tests.py b/tests/docs_tests.py index a4b229996d..7ffdc7fc2b 100644 --- a/tests/docs_tests.py +++ b/tests/docs_tests.py @@ -159,7 +159,8 @@ def test_node_docs_existance(self): mesh_separate_mk2.py symmetrize.py vd_attr_node.py -bvh_nearest_new.py""".split() +scalar_field_point.py +bvh_nearest_new.py""".split("\n") def check_category(directory): dir_name = basename(directory) diff --git a/ui/icons/sv_apply_vfield.png b/ui/icons/sv_apply_vfield.png new file mode 100644 index 0000000000..7cea9deaca Binary files /dev/null and b/ui/icons/sv_apply_vfield.png differ diff --git a/ui/icons/sv_bend_curve_field.png b/ui/icons/sv_bend_curve_field.png new file mode 100644 index 0000000000..39819d84ed Binary files /dev/null and b/ui/icons/sv_bend_curve_field.png differ diff --git a/ui/icons/sv_bend_surface_field.png b/ui/icons/sv_bend_surface_field.png new file mode 100644 index 0000000000..43c5c13680 Binary files /dev/null and b/ui/icons/sv_bend_surface_field.png differ diff --git a/ui/icons/sv_cast_curve.png b/ui/icons/sv_cast_curve.png new file mode 100644 index 0000000000..39bda14a92 Binary files /dev/null and b/ui/icons/sv_cast_curve.png differ diff --git a/ui/icons/sv_concat_curves.png b/ui/icons/sv_concat_curves.png new file mode 100644 index 0000000000..e8d0a9b984 Binary files /dev/null and b/ui/icons/sv_concat_curves.png differ diff --git a/ui/icons/sv_curve_domain.png b/ui/icons/sv_curve_domain.png new file mode 100644 index 0000000000..a046329b4d Binary files /dev/null and b/ui/icons/sv_curve_domain.png differ diff --git a/ui/icons/sv_curve_endpoints.png b/ui/icons/sv_curve_endpoints.png new file mode 100644 index 0000000000..39308eca05 Binary files /dev/null and b/ui/icons/sv_curve_endpoints.png differ diff --git a/ui/icons/sv_curve_formula.png b/ui/icons/sv_curve_formula.png new file mode 100644 index 0000000000..fdb1894057 Binary files /dev/null and b/ui/icons/sv_curve_formula.png differ diff --git a/ui/icons/sv_curve_frame.png b/ui/icons/sv_curve_frame.png new file mode 100644 index 0000000000..5d74ff0172 Binary files /dev/null and b/ui/icons/sv_curve_frame.png differ diff --git a/ui/icons/sv_curve_length.png b/ui/icons/sv_curve_length.png new file mode 100644 index 0000000000..78e8fce0af Binary files /dev/null and b/ui/icons/sv_curve_length.png differ diff --git a/ui/icons/sv_curve_length_p.png b/ui/icons/sv_curve_length_p.png new file mode 100644 index 0000000000..f7a8e2e272 Binary files /dev/null and b/ui/icons/sv_curve_length_p.png differ diff --git a/ui/icons/sv_curve_on_surface.png b/ui/icons/sv_curve_on_surface.png new file mode 100644 index 0000000000..d29025deeb Binary files /dev/null and b/ui/icons/sv_curve_on_surface.png differ diff --git a/ui/icons/sv_curve_segment.png b/ui/icons/sv_curve_segment.png new file mode 100644 index 0000000000..f23b61b29a Binary files /dev/null and b/ui/icons/sv_curve_segment.png differ diff --git a/ui/icons/sv_curve_vfield.png b/ui/icons/sv_curve_vfield.png new file mode 100644 index 0000000000..ead3909195 Binary files /dev/null and b/ui/icons/sv_curve_vfield.png differ diff --git a/ui/icons/sv_eval_scalar_field.png b/ui/icons/sv_eval_scalar_field.png new file mode 100644 index 0000000000..7ca56ea58d Binary files /dev/null and b/ui/icons/sv_eval_scalar_field.png differ diff --git a/ui/icons/sv_eval_surface.png b/ui/icons/sv_eval_surface.png new file mode 100644 index 0000000000..436f1def54 Binary files /dev/null and b/ui/icons/sv_eval_surface.png differ diff --git a/ui/icons/sv_eval_vector_field.png b/ui/icons/sv_eval_vector_field.png new file mode 100644 index 0000000000..89da3b461d Binary files /dev/null and b/ui/icons/sv_eval_vector_field.png differ diff --git a/ui/icons/sv_extrude_curve_point.png b/ui/icons/sv_extrude_curve_point.png new file mode 100644 index 0000000000..b8cb7724b4 Binary files /dev/null and b/ui/icons/sv_extrude_curve_point.png differ diff --git a/ui/icons/sv_extrude_curve_vector.png b/ui/icons/sv_extrude_curve_vector.png new file mode 100644 index 0000000000..9947012fdf Binary files /dev/null and b/ui/icons/sv_extrude_curve_vector.png differ diff --git a/ui/icons/sv_fillet_polyline.png b/ui/icons/sv_fillet_polyline.png new file mode 100644 index 0000000000..7e3fc834e3 Binary files /dev/null and b/ui/icons/sv_fillet_polyline.png differ diff --git a/ui/icons/sv_flip_curve.png b/ui/icons/sv_flip_curve.png new file mode 100644 index 0000000000..30b0dd65ab Binary files /dev/null and b/ui/icons/sv_flip_curve.png differ diff --git a/ui/icons/sv_join_fields.png b/ui/icons/sv_join_fields.png new file mode 100644 index 0000000000..c3e3a74951 Binary files /dev/null and b/ui/icons/sv_join_fields.png differ diff --git a/ui/icons/sv_point_distance_field.png b/ui/icons/sv_point_distance_field.png new file mode 100644 index 0000000000..0e950f2d13 Binary files /dev/null and b/ui/icons/sv_point_distance_field.png differ diff --git a/ui/icons/sv_polyline.png b/ui/icons/sv_polyline.png new file mode 100644 index 0000000000..ce10c5e004 Binary files /dev/null and b/ui/icons/sv_polyline.png differ diff --git a/ui/icons/sv_scalar_field.png b/ui/icons/sv_scalar_field.png new file mode 100644 index 0000000000..6dbba6217a Binary files /dev/null and b/ui/icons/sv_scalar_field.png differ diff --git a/ui/icons/sv_scalar_field_math.png b/ui/icons/sv_scalar_field_math.png new file mode 100644 index 0000000000..766368d37c Binary files /dev/null and b/ui/icons/sv_scalar_field_math.png differ diff --git a/ui/icons/sv_surface_boundary.png b/ui/icons/sv_surface_boundary.png new file mode 100644 index 0000000000..b2b7a688d6 Binary files /dev/null and b/ui/icons/sv_surface_boundary.png differ diff --git a/ui/icons/sv_surface_domain.png b/ui/icons/sv_surface_domain.png new file mode 100644 index 0000000000..68123dfb5d Binary files /dev/null and b/ui/icons/sv_surface_domain.png differ diff --git a/ui/icons/sv_surface_formula.png b/ui/icons/sv_surface_formula.png new file mode 100644 index 0000000000..389b14a08e Binary files /dev/null and b/ui/icons/sv_surface_formula.png differ diff --git a/ui/icons/sv_surface_from_curves.png b/ui/icons/sv_surface_from_curves.png new file mode 100644 index 0000000000..ef1fae25df Binary files /dev/null and b/ui/icons/sv_surface_from_curves.png differ diff --git a/ui/icons/sv_surface_subdomain.png b/ui/icons/sv_surface_subdomain.png new file mode 100644 index 0000000000..0f642d4756 Binary files /dev/null and b/ui/icons/sv_surface_subdomain.png differ diff --git a/ui/icons/sv_surface_vfield.png b/ui/icons/sv_surface_vfield.png new file mode 100644 index 0000000000..aac5e69517 Binary files /dev/null and b/ui/icons/sv_surface_vfield.png differ diff --git a/ui/icons/sv_taper_sweep.png b/ui/icons/sv_taper_sweep.png new file mode 100644 index 0000000000..6326aa2997 Binary files /dev/null and b/ui/icons/sv_taper_sweep.png differ diff --git a/ui/icons/sv_trim_tessellate.png b/ui/icons/sv_trim_tessellate.png new file mode 100644 index 0000000000..a7b4888d9a Binary files /dev/null and b/ui/icons/sv_trim_tessellate.png differ diff --git a/ui/icons/sv_uv_iso_curve.png b/ui/icons/sv_uv_iso_curve.png new file mode 100644 index 0000000000..3fc28ad6f0 Binary files /dev/null and b/ui/icons/sv_uv_iso_curve.png differ diff --git a/ui/icons/sv_vector_field.png b/ui/icons/sv_vector_field.png new file mode 100644 index 0000000000..22a0a95ef7 Binary files /dev/null and b/ui/icons/sv_vector_field.png differ diff --git a/ui/icons/sv_vector_field_lines.png b/ui/icons/sv_vector_field_lines.png new file mode 100644 index 0000000000..d3598380e3 Binary files /dev/null and b/ui/icons/sv_vector_field_lines.png differ diff --git a/ui/icons/sv_vector_field_math.png b/ui/icons/sv_vector_field_math.png new file mode 100644 index 0000000000..85a98ac9d7 Binary files /dev/null and b/ui/icons/sv_vector_field_math.png differ diff --git a/ui/icons/sv_vfield_in.png b/ui/icons/sv_vfield_in.png new file mode 100644 index 0000000000..d9bf0e4bd9 Binary files /dev/null and b/ui/icons/sv_vfield_in.png differ diff --git a/ui/icons/sv_vfield_out.png b/ui/icons/sv_vfield_out.png new file mode 100644 index 0000000000..03268adc55 Binary files /dev/null and b/ui/icons/sv_vfield_out.png differ diff --git a/ui/icons/svg/sv_apply_vfield.svg b/ui/icons/svg/sv_apply_vfield.svg new file mode 100644 index 0000000000..34b0fb3449 --- /dev/null +++ b/ui/icons/svg/sv_apply_vfield.svg @@ -0,0 +1,241 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + diff --git a/ui/icons/svg/sv_bend_curve_field.svg b/ui/icons/svg/sv_bend_curve_field.svg new file mode 100644 index 0000000000..39906195a0 --- /dev/null +++ b/ui/icons/svg/sv_bend_curve_field.svg @@ -0,0 +1,234 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + diff --git a/ui/icons/svg/sv_bend_surface_field.svg b/ui/icons/svg/sv_bend_surface_field.svg new file mode 100644 index 0000000000..0ff574fec1 --- /dev/null +++ b/ui/icons/svg/sv_bend_surface_field.svg @@ -0,0 +1,192 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + diff --git a/ui/icons/svg/sv_cast_curve.svg b/ui/icons/svg/sv_cast_curve.svg new file mode 100644 index 0000000000..59c1e8a6c5 --- /dev/null +++ b/ui/icons/svg/sv_cast_curve.svg @@ -0,0 +1,252 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + diff --git a/ui/icons/svg/sv_concat_curves.svg b/ui/icons/svg/sv_concat_curves.svg new file mode 100644 index 0000000000..227b50798d --- /dev/null +++ b/ui/icons/svg/sv_concat_curves.svg @@ -0,0 +1,201 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + diff --git a/ui/icons/svg/sv_curve_domain.svg b/ui/icons/svg/sv_curve_domain.svg new file mode 100644 index 0000000000..c29e4f5be5 --- /dev/null +++ b/ui/icons/svg/sv_curve_domain.svg @@ -0,0 +1,223 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + t + + diff --git a/ui/icons/svg/sv_curve_endpoints.svg b/ui/icons/svg/sv_curve_endpoints.svg new file mode 100644 index 0000000000..2a85eb01f8 --- /dev/null +++ b/ui/icons/svg/sv_curve_endpoints.svg @@ -0,0 +1,209 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + diff --git a/ui/icons/svg/sv_curve_formula.svg b/ui/icons/svg/sv_curve_formula.svg new file mode 100644 index 0000000000..68cbd5cdd7 --- /dev/null +++ b/ui/icons/svg/sv_curve_formula.svg @@ -0,0 +1,180 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + f + + diff --git a/ui/icons/svg/sv_curve_frame.svg b/ui/icons/svg/sv_curve_frame.svg new file mode 100644 index 0000000000..1f379b1368 --- /dev/null +++ b/ui/icons/svg/sv_curve_frame.svg @@ -0,0 +1,207 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + diff --git a/ui/icons/svg/sv_curve_length.svg b/ui/icons/svg/sv_curve_length.svg new file mode 100644 index 0000000000..b093a17b14 --- /dev/null +++ b/ui/icons/svg/sv_curve_length.svg @@ -0,0 +1,214 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + L + + diff --git a/ui/icons/svg/sv_curve_length_p.svg b/ui/icons/svg/sv_curve_length_p.svg new file mode 100644 index 0000000000..4e2fbee4ba --- /dev/null +++ b/ui/icons/svg/sv_curve_length_p.svg @@ -0,0 +1,214 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + P + + diff --git a/ui/icons/svg/sv_curve_on_surface.svg b/ui/icons/svg/sv_curve_on_surface.svg new file mode 100644 index 0000000000..0666f5f41d --- /dev/null +++ b/ui/icons/svg/sv_curve_on_surface.svg @@ -0,0 +1,189 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + diff --git a/ui/icons/svg/sv_curve_segment.svg b/ui/icons/svg/sv_curve_segment.svg new file mode 100644 index 0000000000..f02d7c2bd9 --- /dev/null +++ b/ui/icons/svg/sv_curve_segment.svg @@ -0,0 +1,215 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + diff --git a/ui/icons/svg/sv_curve_vfield.svg b/ui/icons/svg/sv_curve_vfield.svg new file mode 100644 index 0000000000..84b730d6b8 --- /dev/null +++ b/ui/icons/svg/sv_curve_vfield.svg @@ -0,0 +1,183 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + diff --git a/ui/icons/svg/sv_eval_scalar_field.svg b/ui/icons/svg/sv_eval_scalar_field.svg new file mode 100644 index 0000000000..356523cd06 --- /dev/null +++ b/ui/icons/svg/sv_eval_scalar_field.svg @@ -0,0 +1,193 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + S(x) + + diff --git a/ui/icons/svg/sv_eval_surface.svg b/ui/icons/svg/sv_eval_surface.svg new file mode 100644 index 0000000000..ec69bdd5cd --- /dev/null +++ b/ui/icons/svg/sv_eval_surface.svg @@ -0,0 +1,211 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + diff --git a/ui/icons/svg/sv_eval_vector_field.svg b/ui/icons/svg/sv_eval_vector_field.svg new file mode 100644 index 0000000000..cbc46528da --- /dev/null +++ b/ui/icons/svg/sv_eval_vector_field.svg @@ -0,0 +1,193 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + V(x) + + diff --git a/ui/icons/svg/sv_extrude_curve_point.svg b/ui/icons/svg/sv_extrude_curve_point.svg new file mode 100644 index 0000000000..2c47b70247 --- /dev/null +++ b/ui/icons/svg/sv_extrude_curve_point.svg @@ -0,0 +1,211 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + diff --git a/ui/icons/svg/sv_extrude_curve_vector.svg b/ui/icons/svg/sv_extrude_curve_vector.svg new file mode 100644 index 0000000000..b40898d4ae --- /dev/null +++ b/ui/icons/svg/sv_extrude_curve_vector.svg @@ -0,0 +1,220 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + diff --git a/ui/icons/svg/sv_fillet_polyline.svg b/ui/icons/svg/sv_fillet_polyline.svg new file mode 100644 index 0000000000..910aeb061c --- /dev/null +++ b/ui/icons/svg/sv_fillet_polyline.svg @@ -0,0 +1,204 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + diff --git a/ui/icons/svg/sv_flip_curve.svg b/ui/icons/svg/sv_flip_curve.svg new file mode 100644 index 0000000000..af9d96fc6e --- /dev/null +++ b/ui/icons/svg/sv_flip_curve.svg @@ -0,0 +1,224 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + diff --git a/ui/icons/svg/sv_join_fields.svg b/ui/icons/svg/sv_join_fields.svg new file mode 100644 index 0000000000..302fd7edb7 --- /dev/null +++ b/ui/icons/svg/sv_join_fields.svg @@ -0,0 +1,286 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + diff --git a/ui/icons/svg/sv_point_distance_field.svg b/ui/icons/svg/sv_point_distance_field.svg new file mode 100644 index 0000000000..e52f7dd9b6 --- /dev/null +++ b/ui/icons/svg/sv_point_distance_field.svg @@ -0,0 +1,235 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + diff --git a/ui/icons/svg/sv_polyline.svg b/ui/icons/svg/sv_polyline.svg new file mode 100644 index 0000000000..08123c2bef --- /dev/null +++ b/ui/icons/svg/sv_polyline.svg @@ -0,0 +1,197 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + diff --git a/ui/icons/svg/sv_scalar_field.svg b/ui/icons/svg/sv_scalar_field.svg new file mode 100644 index 0000000000..3b98e3eab8 --- /dev/null +++ b/ui/icons/svg/sv_scalar_field.svg @@ -0,0 +1,214 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + diff --git a/ui/icons/svg/sv_scalar_field_math.svg b/ui/icons/svg/sv_scalar_field_math.svg new file mode 100644 index 0000000000..7e8d17d5d4 --- /dev/null +++ b/ui/icons/svg/sv_scalar_field_math.svg @@ -0,0 +1,203 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + S + f + + diff --git a/ui/icons/svg/sv_surface_boundary.svg b/ui/icons/svg/sv_surface_boundary.svg new file mode 100644 index 0000000000..7198897a6d --- /dev/null +++ b/ui/icons/svg/sv_surface_boundary.svg @@ -0,0 +1,183 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + diff --git a/ui/icons/svg/sv_surface_domain.svg b/ui/icons/svg/sv_surface_domain.svg new file mode 100644 index 0000000000..31e3aae9f6 --- /dev/null +++ b/ui/icons/svg/sv_surface_domain.svg @@ -0,0 +1,221 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + diff --git a/ui/icons/svg/sv_surface_formula.svg b/ui/icons/svg/sv_surface_formula.svg new file mode 100644 index 0000000000..cc545840be --- /dev/null +++ b/ui/icons/svg/sv_surface_formula.svg @@ -0,0 +1,194 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + f + + diff --git a/ui/icons/svg/sv_surface_from_curves.svg b/ui/icons/svg/sv_surface_from_curves.svg new file mode 100644 index 0000000000..761198c7c5 --- /dev/null +++ b/ui/icons/svg/sv_surface_from_curves.svg @@ -0,0 +1,207 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + diff --git a/ui/icons/svg/sv_surface_subdomain.svg b/ui/icons/svg/sv_surface_subdomain.svg new file mode 100644 index 0000000000..e734a511a3 --- /dev/null +++ b/ui/icons/svg/sv_surface_subdomain.svg @@ -0,0 +1,190 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + diff --git a/ui/icons/svg/sv_surface_vfield.svg b/ui/icons/svg/sv_surface_vfield.svg new file mode 100644 index 0000000000..f10e33f51a --- /dev/null +++ b/ui/icons/svg/sv_surface_vfield.svg @@ -0,0 +1,211 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + diff --git a/ui/icons/svg/sv_taper_sweep.svg b/ui/icons/svg/sv_taper_sweep.svg new file mode 100644 index 0000000000..9dc126e5df --- /dev/null +++ b/ui/icons/svg/sv_taper_sweep.svg @@ -0,0 +1,211 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + diff --git a/ui/icons/svg/sv_trim_tessellate.svg b/ui/icons/svg/sv_trim_tessellate.svg new file mode 100644 index 0000000000..e6d53ba52f --- /dev/null +++ b/ui/icons/svg/sv_trim_tessellate.svg @@ -0,0 +1,213 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + diff --git a/ui/icons/svg/sv_uv_iso_curve.svg b/ui/icons/svg/sv_uv_iso_curve.svg new file mode 100644 index 0000000000..cda5479b3b --- /dev/null +++ b/ui/icons/svg/sv_uv_iso_curve.svg @@ -0,0 +1,188 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + diff --git a/ui/icons/svg/sv_vector_field.svg b/ui/icons/svg/sv_vector_field.svg new file mode 100644 index 0000000000..8b3988953c --- /dev/null +++ b/ui/icons/svg/sv_vector_field.svg @@ -0,0 +1,246 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + diff --git a/ui/icons/svg/sv_vector_field_lines.svg b/ui/icons/svg/sv_vector_field_lines.svg new file mode 100644 index 0000000000..93c920f77a --- /dev/null +++ b/ui/icons/svg/sv_vector_field_lines.svg @@ -0,0 +1,229 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + diff --git a/ui/icons/svg/sv_vector_field_math.svg b/ui/icons/svg/sv_vector_field_math.svg new file mode 100644 index 0000000000..b5f1cf8617 --- /dev/null +++ b/ui/icons/svg/sv_vector_field_math.svg @@ -0,0 +1,204 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + V + f + + diff --git a/ui/icons/svg/sv_vfield_in.svg b/ui/icons/svg/sv_vfield_in.svg new file mode 100644 index 0000000000..bae6c99ed5 --- /dev/null +++ b/ui/icons/svg/sv_vfield_in.svg @@ -0,0 +1,238 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + diff --git a/ui/icons/svg/sv_vfield_out.svg b/ui/icons/svg/sv_vfield_out.svg new file mode 100644 index 0000000000..e3827bade2 --- /dev/null +++ b/ui/icons/svg/sv_vfield_out.svg @@ -0,0 +1,234 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + diff --git a/ui/nodeview_space_menu.py b/ui/nodeview_space_menu.py index 3af0297929..81d675952b 100644 --- a/ui/nodeview_space_menu.py +++ b/ui/nodeview_space_menu.py @@ -110,6 +110,9 @@ def draw(self, context): layout.separator() layout.menu("NODEVIEW_MT_AddGenerators", **icon('OBJECT_DATAMODE')) + layout.menu("NODEVIEW_MT_AddCurves", **icon('OUTLINER_OB_CURVE')) + layout.menu("NODEVIEW_MT_AddSurfaces", **icon('SURFACE_DATA')) + layout.menu("NODEVIEW_MT_AddFields", **icon('OUTLINER_OB_FORCE_FIELD')) layout.menu("NODEVIEW_MT_AddTransforms", **icon('ORIENTATION_LOCAL')) layout.menu("NODEVIEW_MT_AddAnalyzers", **icon('VIEWZOOM')) layout.menu("NODEVIEW_MT_AddModifiers", **icon('MODIFIER')) @@ -151,7 +154,6 @@ def draw(self, context): layout_draw_categories(self.layout, node_cats[self.bl_label]) layout.menu("NODEVIEW_MT_AddGeneratorsExt", **icon('PLUGIN')) - class NODEVIEW_MT_AddModifiers(bpy.types.Menu): bl_label = "Modifiers" @@ -237,6 +239,9 @@ def draw(self, context): # like magic. # make | NODEVIEW_MT_Add + class name , menu name make_class('GeneratorsExt', "Generators Extended"), + make_class('Curves', "Curves"), + make_class('Surfaces', "Surfaces"), + make_class('Fields', "Fields"), make_class('Transforms', "Transforms"), make_class('Analyzers', "Analyzers"), make_class('Viz', "Viz"), diff --git a/utils/curve.py b/utils/curve.py new file mode 100644 index 0000000000..54a1efeedf --- /dev/null +++ b/utils/curve.py @@ -0,0 +1,788 @@ +# This file is part of project Sverchok. It's copyrighted by the contributors +# recorded in the version control history of the file, available from +# its original location https://github.com/nortikin/sverchok/commit/master +# +# SPDX-License-Identifier: GPL3 +# License-Filename: LICENSE + +import numpy as np + +from math import sin, cos, pi + +from sverchok.utils.geom import PlaneEquation, LineEquation, LinearSpline, CubicSpline +from sverchok.utils.integrate import TrapezoidIntegral +from sverchok.utils.logging import error, exception + +################## +# # +# Curves # +# # +################## + +def make_euclidian_ts(pts): + tmp = np.linalg.norm(pts[:-1] - pts[1:], axis=1) + tknots = np.insert(tmp, 0, 0).cumsum() + tknots = tknots / tknots[-1] + return tknots + +class SvCurve(object): + def __repr__(self): + if hasattr(self, '__description__'): + description = self.__description__ + else: + description = self.__class__.__name__ + return "<{} curve>".format(description) + + def evaluate(self, t): + raise Exception("not implemented!") + + def evaluate_array(self, ts): + raise Exception("not implemented!") + + def calc_length(self, t_min, t_max, resolution = 50): + ts = np.linspace(t_min, t_max, num=resolution) + vectors = self.evaluate_array(ts) + dvs = vectors[1:] - vectors[:-1] + lengths = np.linalg.norm(dvs, axis=1) + return np.sum(lengths) + + def tangent(self, t): + v = self.evaluate(t) + h = self.tangent_delta + v_h = self.evaluate(t+h) + return (v_h - v) / h + + def tangent_array(self, ts): + vs = self.evaluate_array(ts) + h = self.tangent_delta + u_max = self.get_u_bounds()[1] + bad_idxs = (ts+h) > u_max + good_idxs = (ts+h) <= u_max + ts_h = ts + h + ts_h[bad_idxs] = (ts - h)[bad_idxs] + + vs_h = self.evaluate_array(ts_h) + tangents_plus = (vs_h - vs) / h + tangents_minus = (vs - vs_h) / h + tangents_x = np.where(good_idxs, tangents_plus[:,0], tangents_minus[:,0]) + tangents_y = np.where(good_idxs, tangents_plus[:,1], tangents_minus[:,1]) + tangents_z = np.where(good_idxs, tangents_plus[:,2], tangents_minus[:,2]) + tangents = np.stack((tangents_x, tangents_y, tangents_z)).T + return tangents + + def second_derivative(self, t): + if hasattr(self, 'tangent_delta'): + h = self.tangent_delta + else: + h = 0.001 + v0 = self.evaluate(t-h) + v1 = self.evaluate(t) + v2 = self.evaluate(t+h) + return (v2 - 2*v1 + v0) / (h * h) + + def second_derivative_array(self, ts): + h = 0.001 + v0s = self.evaluate_array(ts-h) + v1s = self.evaluate_array(ts) + v2s = self.evaluate_array(ts+h) + return (v2s - 2*v1s + v0s) / (h * h) + + def third_derivative_array(self, ts): + h = 0.001 + v0s = self.evaluate_array(ts) + v1s = self.evaluate_array(ts+h) + v2s = self.evaluate_array(ts+2*h) + v3s = self.evaluate_array(ts+3*h) + return (- v0s + 3*v1s - 3*v2s + v3s) / (h * h * h) + + def derivatives_array(self, n, ts): + result = [] + if n >= 1: + first = self.tangent_array(ts) + result.append(first) + + h = 0.001 + if n >= 2: + minus_h = self.evaluate_array(ts-h) + points = self.evaluate_array(ts) + plus_h = self.evaluate_array(ts+h) + second = (plus_h - 2*points + minus_h) / (h * h) + result.append(second) + + if n >= 3: + v0s = points + v1s = plus_h + v2s = self.evaluate_array(ts+2*h) + v3s = self.evaluate_array(ts+3*h) + third = (- v0s + 3*v1s - 3*v2s + v3s) / (h * h * h) + result.append(third) + + return result + + def main_normal(self, t, normalize=True): + tangent = self.tangent(t) + binormal = self.binormal(t, normalize) + v = np.cross(binormal, tangent) + if normalize: + v = v / np.linalg.norm(v) + return v + + def binormal(self, t, normalize=True): + tangent = self.tangent(t) + second = self.second_derivative(t) + v = np.cross(tangent, second) + if normalize: + v = v / np.linalg.norm(v) + return v + + def main_normal_array(self, ts, normalize=True): + tangents = self.tangent_array(ts) + binormals = self.binormal_array(ts, normalize) + v = np.cross(binormals, tangents) + if normalize: + norms = np.linalg.norm(v, axis=1, keepdims=True) + nonzero = (norms > 0)[:,0] + v[nonzero] = v[nonzero] / norms[nonzero][:,0][np.newaxis].T + return v + + def binormal_array(self, ts, normalize=True): + tangents, seconds = self.derivatives_array(2, ts) + v = np.cross(tangents, seconds) + if normalize: + norms = np.linalg.norm(v, axis=1, keepdims=True) + nonzero = (norms > 0)[:,0] + v[nonzero] = v[nonzero] / norms[nonzero][:,0][np.newaxis].T + return v + + def tangent_normal_binormal_array(self, ts, normalize=True): + tangents, seconds = self.derivatives_array(2, ts) + binormals = np.cross(tangents, seconds) + if normalize: + norms = np.linalg.norm(binormals, axis=1, keepdims=True) + nonzero = (norms > 0)[:,0] + binormals[nonzero] = binormals[nonzero] / norms[nonzero][:,0][np.newaxis].T + normals = np.cross(binormals, tangents) + if normalize: + norms = np.linalg.norm(normals, axis=1, keepdims=True) + nonzero = (norms > 0)[:,0] + normals[nonzero] = normals[nonzero] / norms[nonzero][:,0][np.newaxis].T + return tangents, normals, binormals + + def frame_array(self, ts): + tangents, normals, binormals = self.tangent_normal_binormal_array(ts) + tangents = tangents / np.linalg.norm(tangents, axis=1)[np.newaxis].T + matrices_np = np.dstack((normals, binormals, tangents)) + matrices_np = np.transpose(matrices_np, axes=(0,2,1)) + try: + matrices_np = np.linalg.inv(matrices_np) + return matrices_np, normals, binormals + except np.linalg.LinAlgError as e: + error("Some of matrices are singular:") + for m in matrices_np: + error("M:\n%s", m) + raise e + + def curvature_array(self, ts): + tangents, seconds = self.derivatives_array(2, ts) + numerator = np.linalg.norm(np.cross(tangents, seconds), axis=1) + tangents_norm = np.linalg.norm(tangents, axis=1) + denominator = tangents_norm * tangents_norm * tangents_norm + return numerator / denominator + + def torsion_array(self, ts): + tangents, seconds, thirds = self.derivatives_array(3, ts) + seconds_thirds = np.cross(seconds, thirds) + numerator = (tangents * seconds_thirds).sum(axis=1) + #numerator = np.apply_along_axis(lambda tangent: tangent.dot(seconds_thirds), 1, tangents) + first_second = np.cross(tangents, seconds) + denominator = np.linalg.norm(first_second, axis=1) + return numerator / (denominator * denominator) + + def pre_calc_torsion_integral(self, resolution): + t_min, t_max = self.get_u_bounds() + ts = np.linspace(t_min, t_max, resolution) + vectors = self.evaluate_array(ts) + dvs = vectors[1:] - vectors[:-1] + lengths = np.linalg.norm(dvs, axis=1) + xs = np.insert(np.cumsum(lengths), 0, 0) + ys = self.torsion_array(ts) + self._torsion_integral = TrapezoidIntegral(ts, xs, ys) + self._torsion_integral.calc() + + def torsion_integral(self, ts): + return self._torsion_integral.evaluate_cubic(ts) + + def get_u_bounds(self): + raise Exception("not implemented!") + +class SvCurveLengthSolver(object): + def __init__(self, curve): + self.curve = curve + self._spline = None + + def calc_length_segments(self, tknots): + vectors = self.curve.evaluate_array(tknots) + dvs = vectors[1:] - vectors[:-1] + lengths = np.linalg.norm(dvs, axis=1) + return lengths + + def get_total_length(self): + if self._spline is None: + raise Exception("You have to call solver.prepare() first") + return self._length_params[-1] + + def prepare(self, mode, resolution=50): + t_min, t_max = self.curve.get_u_bounds() + tknots = np.linspace(t_min, t_max, num=resolution) + lengths = self.calc_length_segments(tknots) + self._length_params = np.cumsum(np.insert(lengths, 0, 0)) + self._spline = self._make_spline(mode, tknots) + + def _make_spline(self, mode, tknots): + zeros = np.zeros(len(tknots)) + control_points = np.vstack((self._length_params, tknots, zeros)).T + if mode == 'LIN': + spline = LinearSpline(control_points, tknots = self._length_params, is_cyclic = False) + elif mode == 'SPL': + spline = CubicSpline(control_points, tknots = self._length_params, is_cyclic = False) + else: + raise Exception("Unsupported mode; supported are LIN and SPL.") + return spline + + def solve(self, input_lengths): + if self._spline is None: + raise Exception("You have to call solver.prepare() first") + spline_verts = self._spline.eval(input_lengths) + return spline_verts[:,1] + +class SvConcatCurve(SvCurve): + def __init__(self, curves, scale_to_unit = False): + self.curves = curves + self.scale_to_unit = scale_to_unit + bounds = [curve.get_u_bounds() for curve in curves] + self.src_min_bounds = np.array([bound[0] for bound in bounds]) + self.ranges = np.array([bound[1] - bound[0] for bound in bounds]) + if scale_to_unit: + self.u_max = float(len(curves)) + self.min_bounds = np.array(range(len(curves)), dtype=np.float64) + else: + self.u_max = self.ranges.sum() + self.min_bounds = np.insert(np.cumsum(self.ranges), 0, 0) + self.tangent_delta = 0.001 + self.__description__ = "Concat{}".format(curves) + + def get_u_bounds(self): + return (0.0, self.u_max) + + def _get_ts_grouped(self, ts): + index = self.min_bounds.searchsorted(ts, side='left') - 1 + index = index.clip(0, len(self.curves) - 1) + left_bounds = self.min_bounds[index] + curve_left_bounds = self.src_min_bounds[index] + dts = ts - left_bounds + curve_left_bounds + if self.scale_to_unit: + dts = dts * self.ranges[index] + #dts_grouped = np.split(dts, np.cumsum(np.unique(index, return_counts=True)[1])[:-1]) + # TODO: this should be vectorized somehow + dts_grouped = [] + prev_i = None + prev_dts = [] + + for i, dt in zip(index, dts): + if i == prev_i: + prev_dts.append(dt) + else: + if prev_dts: + dts_grouped.append((prev_i, prev_dts[:])) + prev_dts = [dt] + else: + prev_dts = [dt] + if prev_i is not None: + dts_grouped.append((prev_i, prev_dts[:])) + prev_i = i + + if prev_dts: + dts_grouped.append((i, prev_dts)) + + return dts_grouped + + def evaluate(self, t): + index = self.min_bounds.searchsorted(t, side='left') - 1 + index = index.clip(0, len(self.curves) - 1) + left_bound = self.min_bounds[index] + curve_left_bound = self.src_min_bounds[index] + dt = t - left_bound + curve_left_bound + return self.curves[index].evaluate(dt) + + def evaluate_array(self, ts): + dts_grouped = self._get_ts_grouped(ts) + points_grouped = [self.curves[i].evaluate_array(np.array(dts)) for i, dts in dts_grouped] + return np.concatenate(points_grouped) + + def tangent(self, t): + return self.tangent_array(np.array([t]))[0] + + def tangent_array(self, ts): + dts_grouped = self._get_ts_grouped(ts) + tangents_grouped = [self.curves[i].tangent_array(np.array(dts)) for i, dts in dts_grouped] + return np.concatenate(tangents_grouped) + + def second_derivative_array(self, ts): + dts_grouped = self._get_ts_grouped(ts) + vectors = [self.curves[i].second_derivative_array(np.array(dts)) for i, dts in dts_grouped] + return np.concatenate(vectors) + + def third_derivative_array(self, ts): + dts_grouped = self._get_ts_grouped(ts) + vectors = [self.curves[i].third_derivative_array(np.array(dts)) for i, dts in dts_grouped] + return np.concatenate(vectors) + + def derivatives_array(self, n, ts): + dts_grouped = self._get_ts_grouped(ts) + derivs = [self.curves[i].derivatives_array(n, np.array(dts)) for i, dts in dts_grouped] + result = [] + for i in range(n): + ith_derivs_grouped = [curve_derivs[i] for curve_derivs in derivs] + ith_derivs = np.concatenate(ith_derivs_grouped) + result.append(ith_derivs) + return result + +class SvFlipCurve(SvCurve): + def __init__(self, curve): + self.curve = curve + if hasattr(curve, 'tangent_delta'): + self.tangent_delta = curve.tangent_delta + else: + self.tangent_delta = 0.001 + self.__description__ = "Flip({})".format(curve) + + def get_u_bounds(self): + return self.curve.get_u_bounds() + + def evaluate(self, t): + m, M = self.curve.get_u_bounds() + t = M - t + m + return self.curve.evaluate(t) + + def evaluate_array(self, ts): + m, M = self.curve.get_u_bounds() + ts = M - ts + m + return self.curve.evaluate_array(ts) + + def tangent(self, t): + m, M = self.curve.get_u_bounds() + t = M - t + m + return self.curve.tangent(t) + + def tangent_array(self, ts): + m, M = self.curve.get_u_bounds() + ts = M - ts + m + return self.curve.tangent_array(ts) + + def second_derivative_array(self, ts): + m, M = self.curve.get_u_bounds() + ts = M - ts + m + return self.curve.second_derivative_array(ts) + + def third_derivative_array(self, ts): + m, M = self.curve.get_u_bounds() + ts = M - ts + m + return self.curve.third_derivative_array(ts) + + def derivatives_array(self, ts): + m, M = self.curve.get_u_bounds() + ts = M - ts + m + return self.curve.derivatives_array(ts) + +class SvCurveSegment(SvCurve): + def __init__(self, curve, u_min, u_max, rescale=False): + self.curve = curve + if hasattr(curve, 'tangent_delta'): + self.tangent_delta = curve.tangent_delta + else: + self.tangent_delta = 0.001 + self.rescale = rescale + if self.rescale: + self.u_bounds = (0.0, 1.0) + self.target_u_bounds = (u_min, u_max) + else: + self.u_bounds = (u_min, u_max) + self.target_u_bounds = (u_min, u_max) + self.__description__ = "{}[{} .. {}]".format(curve, u_min, u_max) + + def get_u_bounds(self): + return self.u_bounds + + def evaluate(self, t): + if self.rescale: + m,M = self.target_u_bounds + t = (M - m)*t + m + return self.curve.evaluate(t) + + def evaluate_array(self, ts): + if self.rescale: + m,M = self.target_u_bounds + ts = (M - m)*ts + m + return self.curve.evaluate_array(ts) + + def tangent(self, t): + if self.rescale: + m,M = self.target_u_bounds + t = (M - m)*t + m + return self.curve.tangent(t) + + def tangent_array(self, ts): + if self.rescale: + m,M = self.target_u_bounds + ts = (M - m)*ts + m + return self.curve.tangent_array(ts) + + def second_derivative_array(self, ts): + if self.rescale: + m,M = self.target_u_bounds + ts = (M - m)*ts + m + return self.curve.second_derivative_array(ts) + + def third_derivative_array(self, ts): + if self.rescale: + m,M = self.target_u_bounds + ts = (M - m)*ts + m + return self.curve.third_derivative_array(ts) + + def derivatives_array(self, ts): + if self.rescale: + m,M = self.target_u_bounds + ts = (M - m)*ts + m + return self.curve.derivatives_array(ts) + +class SvLine(SvCurve): + __description__ = "Line" + + def __init__(self, point, direction): + self.point = np.array(point) + self.direction = np.array(direction) + self.u_bounds = (0.0, 1.0) + + @classmethod + def from_two_points(cls, point1, point2): + direction = np.array(point2) - np.array(point1) + return SvLine(point1, direction) + + def get_u_bounds(self): + return self.u_bounds + + def evaluate(self, t): + return self.point + t * self.direction + + def evaluate_array(self, ts): + ts = ts[np.newaxis].T + return self.point + ts * self.direction + + def tangent(self, t): + tg = self.direction + n = np.linalg.norm(tg) + return tg / n + + def tangent_array(self, ts): + tg = self.direction + n = np.linalg.norm(tg) + tangent = tg / n + result = np.tile(tangent[np.newaxis].T, len(ts)).T + return result + +class SvCircle(SvCurve): + __description__ = "Circle" + + def __init__(self, matrix, radius): + self.matrix = np.array(matrix.to_3x3()) + self.center = np.array(matrix.translation) + self.radius = radius + self.u_bounds = (0.0, 2*pi) + + def get_u_bounds(self): + return self.u_bounds + + def evaluate(self, t): + r = self.radius + x = r * cos(t) + y = r * sin(t) + return self.matrix @ np.array([x, y, 0]) + self.center + + def evaluate_array(self, ts): + r = self.radius + xs = r * np.cos(ts) + ys = r * np.sin(ts) + zs = np.zeros_like(xs) + vertices = np.stack((xs, ys, zs)).T + return np.apply_along_axis(lambda v: self.matrix @ v, 1, vertices) + self.center + + def tangent(self, t): + x = - self.radius * sin(t) + y = self.radius * cos(t) + z = 0 + return self.matrix @ np.array([x, y, z]) + + def tangent_array(self, ts): + xs = - self.radius * np.sin(ts) + ys = self.radius * np.cos(ts) + zs = np.zeros_like(xs) + vectors = np.stack((xs, ys, zs)).T + result = np.apply_along_axis(lambda v: self.matrix @ v, 1, vectors) + return result + +# def second_derivative_array(self, ts): +# xs = - np.cos(ts) +# ys = - np.sin(ts) +# zs = np.zeros_like(xs) +# vectors = np.stack((xs, ys, zs)).T +# return np.apply_along_axis(lambda v: self.matrix @ v, 1, vectors) + +class SvLambdaCurve(SvCurve): + __description__ = "Formula" + + def __init__(self, function): + self.function = function + self.u_bounds = (0.0, 1.0) + self.tangent_delta = 0.001 + + def get_u_bounds(self): + return self.u_bounds + + def evaluate(self, t): + return self.function(t) + + def evaluate_array(self, ts): + return np.vectorize(self.function, signature='()->(3)')(ts) + + def tangent(self, t): + point = self.function(t) + point_h = self.function(t+self.tangent_delta) + return (point_h - point) / self.tangent_delta + + def tangent_array(self, ts): + points = np.vectorize(self.function, signature='()->(3)')(ts) + points_h = np.vectorize(self.function, signature='()->(3)')(ts+self.tangent_delta) + return (points_h - points) / self.tangent_delta + +class SvSplineCurve(SvCurve): + __description__ = "Spline" + + def __init__(self, spline): + self.spline = spline + + def evaluate(self, t): + v = self.spline.eval_at_point(t) + return np.array(v) + + def evaluate_array(self, ts): + vs = self.spline.eval(ts) + return np.array(vs) + + def tangent(self, t): + vs = self.spline.tangent(np.array([t])) + return vs[0] + + def tangent_array(self, ts): + return self.spline.tangent(ts) + + def get_u_bounds(self): + return (0.0, 1.0) + +class SvDeformedByFieldCurve(SvCurve): + def __init__(self, curve, field, coefficient=1.0): + self.curve = curve + self.field = field + self.coefficient = coefficient + self.tangent_delta = 0.001 + self.__description__ = "{}({})".format(field, curve) + + def get_u_bounds(self): + return self.curve.get_u_bounds() + + def evaluate(self, t): + v = self.curve.evaluate(t) + vec = self.field.evaluate(*tuple(v)) + return v + self.coefficient * vec + + def evaluate_array(self, ts): + vs = self.curve.evaluate_array(ts) + xs, ys, zs = vs[:,0], vs[:,1], vs[:,2] + vxs, vys, vzs = self.field.evaluate_grid(xs, ys, zs) + vecs = np.stack((vxs, vys, vzs)).T + return vs + self.coefficient * vecs + +class SvCastCurveToPlane(SvCurve): + def __init__(self, curve, point, normal, coefficient): + self.curve = curve + self.point = point + self.normal = normal + self.coefficient = coefficient + self.plane = PlaneEquation.from_normal_and_point(normal, point) + self.tangent_delta = 0.001 + self.__description__ = "{} casted to Plane".format(curve) + + def evaluate(self, t): + point = self.curve.evaluate(t) + target = np.array(self.plane.projection_of_point(point)) + k = self.coefficient + return (1 - k) * point + k * target + + def evaluate_array(self, ts): + points = self.curve.evaluate_array(ts) + targets = self.plane.projection_of_points(points) + k = self.coefficient + return (1 - k) * points + k * targets + + def get_u_bounds(self): + return self.curve.get_u_bounds() + +class SvCastCurveToSphere(SvCurve): + def __init__(self, curve, center, radius, coefficient): + self.curve = curve + self.center = center + self.radius = radius + self.coefficient = coefficient + self.tangent_delta = 0.001 + self.__description__ = "{} casted to Sphere".format(curve) + + def evaluate(self, t): + return self.evaluate_array(np.array([t]))[0] + + def evaluate_array(self, ts): + points = self.curve.evaluate_array(ts) + centered_points = points - self.center + norms = np.linalg.norm(centered_points, axis=1)[np.newaxis].T + normalized = centered_points / norms + targets = self.radius * normalized + self.center + k = self.coefficient + return (1 - k) * points + k * targets + + def get_u_bounds(self): + return self.curve.get_u_bounds() + +class SvCastCurveToCylinder(SvCurve): + def __init__(self, curve, center, direction, radius, coefficient): + self.curve = curve + self.center = center + self.direction = direction + self.radius = radius + self.coefficient = coefficient + self.line = LineEquation.from_direction_and_point(direction, center) + self.tangent_delta = 0.001 + self.__description__ = "{} casted to Cylinder".format(curve) + + def evaluate(self, t): + point = self.curve.evaluate(t) + projection_to_line = self.line.projection_of_point(point) + projection_to_line = np.array(projection_to_line) + radial = point - projection_to_line + radius = self.radius * radial / np.linalg.norm(radial) + projection = projection_to_line + radius + k = self.coefficient + return (1 - k) * point + k * projection + + def evaluate_array(self, ts): + points = self.curve.evaluate_array(ts) + projection_to_line = self.line.projection_of_points(points) + radial = points - projection_to_line + radius = self.radius * radial / np.linalg.norm(radial, axis=1, keepdims=True) + projections = projection_to_line + radius + k = self.coefficient + return (1 - k) * points + k * projections + + def get_u_bounds(self): + return self.curve.get_u_bounds() + +class SvCurveLerpCurve(SvCurve): + __description__ = "Lerp" + + def __init__(self, curve1, curve2, coefficient): + self.curve1 = curve1 + self.curve2 = curve2 + self.coefficient = coefficient + self.u_bounds = (0.0, 1.0) + self.c1_min, self.c1_max = curve1.get_u_bounds() + self.c2_min, self.c2_max = curve2.get_u_bounds() + self.tangent_delta = 0.001 + + def get_u_bounds(self): + return self.u_bounds + + def evaluate(self, t): + return self.evaluate_array(np.array([t]))[0] + + def evaluate_array(self, ts): + us1 = (self.c1_max - self.c1_min) * ts + self.c1_min + us2 = (self.c2_max - self.c2_min) * ts + self.c2_min + c1_points = self.curve1.evaluate_array(us1) + c2_points = self.curve2.evaluate_array(us2) + k = self.coefficient + return (1.0 - k) * c1_points + k * c2_points + +class SvCurveOnSurface(SvCurve): + def __init__(self, curve, surface, axis=0): + self.curve = curve + self.surface = surface + self.axis = axis + self.tangent_delta = 0.001 + self.__description__ = "{} on {}".format(curve, surface) + + def get_u_bounds(self): + return self.curve.get_u_bounds() + + def evaluate(self, t): + return self.evaluate_array(np.array([t]))[0] + + def evaluate_array(self, ts): + points = self.curve.evaluate_array(ts) + xs = points[:,0] + ys = points[:,1] + zs = points[:,2] + if self.axis == 0: + us = ys + vs = zs + elif self.axis == 1: + us = xs + vs = zs + elif self.axis == 2: + us = xs + vs = ys + else: + raise Exception("Unsupported orientation axis") + return self.surface.evaluate_array(us, vs) + +class SvIsoUvCurve(SvCurve): + def __init__(self, surface, fixed_axis, value, flip=False): + self.surface = surface + self.fixed_axis = fixed_axis + self.value = value + self.flip = flip + self.tangent_delta = 0.001 + self.__description__ = "{} at {} = {}".format(surface, fixed_axis, value) + + def get_u_bounds(self): + if self.fixed_axis == 'U': + return self.surface.get_v_min(), self.surface.get_v_max() + else: + return self.surface.get_u_min(), self.surface.get_u_max() + + def evaluate(self, t): + if self.fixed_axis == 'U': + if self.flip: + t = self.surface.get_v_max() - t + self.surface.get_v_min() + return self.surface.evaluate(self.value, t) + else: + if self.flip: + t = self.surface.get_u_max() - t + self.surface.get_u_min() + return self.surface.evaluate(t, self.value) + + def evaluate_array(self, ts): + if self.fixed_axis == 'U': + if self.flip: + ts = self.surface.get_v_max() - ts + self.surface.get_v_min() + return self.surface.evaluate_array(np.repeat(self.value, len(ts)), ts) + else: + if self.flip: + ts = self.surface.get_u_max() - ts + self.surface.get_u_min() + return self.surface.evaluate_array(ts, np.repeat(self.value, len(ts))) + diff --git a/utils/field/__init__.py b/utils/field/__init__.py new file mode 100644 index 0000000000..143f486c05 --- /dev/null +++ b/utils/field/__init__.py @@ -0,0 +1 @@ +# __init__.py diff --git a/utils/field/image.py b/utils/field/image.py new file mode 100644 index 0000000000..4bda008663 --- /dev/null +++ b/utils/field/image.py @@ -0,0 +1,101 @@ +# This file is part of project Sverchok. It's copyrighted by the contributors +# recorded in the version control history of the file, available from +# its original location https://github.com/nortikin/sverchok/commit/master +# +# SPDX-License-Identifier: GPL3 +# License-Filename: LICENSE + +import numpy as np + +import bpy +from mathutils import Color + +from sverchok.utils.modules.color_utils import color_channels + +from sverchok.utils.field.scalar import SvScalarField +from sverchok.utils.field.vector import SvVectorField + +def get_scalar(channel, x): + return color_channels[channel][1](x) + +def get_vector(space, x): + if space == 'RGB': + return np.array(x[:3]) + elif space == 'HSV': + return np.array(Color(x[:3]).hsv) + +def load_image(image_name): + image = bpy.data.images[image_name] + width, height = image.size + pixels = np.array(image.pixels).reshape((height, width, 4)) + return pixels + +class SvImageScalarField(SvScalarField): + def __init__(self, pixels, channel, plane='XY', fallback=0.0): + self.plane = plane + self.channel = channel + self.fallback = fallback + self.pixels = pixels + self.width, self.height, _ = pixels.shape + + def evaluate(self, x, y, z): + if self.plane == 'XY': + u,v = x,y + elif self.plane == 'YZ': + u,v = y,z + else: # XZ + u,v = x,z + return self._evaluate(u, v) + + def _evaluate(self, u, v): + if u < 0 or u >= self.height or v < 0 or v >= self.width: + return self.fallback + u, v = int(u), int(v) + color = self.pixels[v][u] + return get_scalar(self.channel, color) + + def evaluate_grid(self, xs, ys, zs): + if self.plane == 'XY': + us, vs = xs, ys + elif self.plane == 'YZ': + us, vs = ys, zs + else: # XZ + us, vs = xs, zs + return np.vectorize(self._evaluate)(us, vs) + +class SvImageVectorField(SvVectorField): + def __init__(self, pixels, space, plane='XY', fallback=None): + if fallback is None: + fallback = np.array([0, 0, 0]) + self.fallback = fallback + self.plane = plane + self.space = space + self.pixels = pixels + self.width, self.height, _ = pixels.shape + + def evaluate(self, x, y, z): + if self.plane == 'XY': + u,v = x,y + elif self.plane == 'YZ': + u,v = y,z + else: # XZ + u,v = x,z + return self._evaluate(u, v) + + def _evaluate(self, u, v): + if u < 0 or u >= self.height or v < 0 or v >= self.width: + return self.fallback + u, v = int(u), int(v) + color = self.pixels[v][u] + return get_vector(self.space, color) + + def evaluate_grid(self, xs, ys, zs): + if self.plane == 'XY': + us, vs = xs, ys + elif self.plane == 'YZ': + us, vs = ys, zs + else: # XZ + us, vs = xs, zs + vectors = np.vectorize(self._evaluate, signature='(),()->(3)')(us, vs).T + return vectors[0], vectors[1], vectors[2] + diff --git a/utils/field/scalar.py b/utils/field/scalar.py new file mode 100644 index 0000000000..6e846d7d56 --- /dev/null +++ b/utils/field/scalar.py @@ -0,0 +1,496 @@ +# This file is part of project Sverchok. It's copyrighted by the contributors +# recorded in the version control history of the file, available from +# its original location https://github.com/nortikin/sverchok/commit/master +# +# SPDX-License-Identifier: GPL3 +# License-Filename: LICENSE + +import numpy as np +from math import copysign + +from mathutils import Matrix, Vector +from mathutils import kdtree +from mathutils import bvhtree + +from sverchok.utils.math import from_cylindrical, from_spherical, to_cylindrical, to_spherical + +################## +# # +# Scalar Fields # +# # +################## + +class SvScalarField(object): + + def __repr__(self): + if hasattr(self, '__description__'): + description = self.__description__ + else: + description = self.__class__.__name__ + return "<{} scalar field>".format(description) + + def evaluate(self, point): + raise Exception("not implemented") + + def evaluate_grid(self, xs, ys, zs): + raise Exception("not implemented") + +class SvConstantScalarField(SvScalarField): + def __init__(self, value): + self.value = value + self.__description__ = "Constant = {}".format(value) + + def evaluate(self, x, y, z): + return self.value + + def evaluate_grid(self, xs, ys, zs): + result = np.full_like(xs, self.value, dtype=np.float64) + return result + +class SvVectorFieldDecomposed(SvScalarField): + def __init__(self, vfield, coords, axis): + self.vfield = vfield + self.coords = coords + self.axis = axis + self.__description__ = "{}.{}[{}]".format(vfield, coords, axis) + + def evaluate(self, x, y, z): + result = self.vfield.evaluate(x, y, z) + if self.coords == 'XYZ': + return result[self.axis] + elif self.coords == 'CYL': + rho, phi, z = to_cylindrical(tuple(result), mode='radians') + return [rho, phi, z][self.axis] + else: # SPH + rho, phi, theta = to_spherical(tuple(result), mode='radians') + return [rho, phi, theta][self.axis] + + def evaluate_grid(self, xs, ys, zs): + results = self.vfield.evaluate_grid(xs, ys, zs) + if self.coords == 'XYZ': + return results[self.axis] + elif self.coords == 'CYL': + vectors = np.stack(results).T + vectors = np.apply_along_axis(lambda v: np.array(to_cylindrical(tuple(v), mode='radians')), 1, vectors) + return vectors[:, self.axis] + else: # SPH + vectors = np.stack(results).T + vectors = np.apply_along_axis(lambda v: np.array(to_spherical(tuple(v), mode='radians')), 1, vectors) + return vectors[:, self.axis] + +class SvScalarFieldLambda(SvScalarField): + __description__ = "Formula" + + def __init__(self, function, variables, in_field): + self.function = function + self.variables = variables + self.in_field = in_field + + def evaluate_grid(self, xs, ys, zs): + if self.in_field is None: + Vs = np.zeros(xs.shape[0]) + else: + Vs = self.in_field.evaluate_grid(xs, ys, zs) + return np.vectorize(self.function)(xs, ys, zs, Vs) + + def evaluate(self, x, y, z): + if self.in_field is None: + V = None + else: + V = self.in_field.evaluate(x, y, z) + return self.function(x, y, z, V) + +class SvScalarFieldPointDistance(SvScalarField): + def __init__(self, center, metric='EUCLIDEAN', falloff=None): + self.center = center + self.falloff = falloff + self.metric = metric + self.__description__ = "Distance from {}".format(tuple(center)) + + def evaluate_grid(self, xs, ys, zs): + x0, y0, z0 = tuple(self.center) + xs = xs - x0 + ys = ys - y0 + zs = zs - z0 + points = np.stack((xs, ys, zs)) + if self.metric == 'EUCLIDEAN': + norms = np.linalg.norm(points, axis=0) + elif self.metric == 'CHEBYSHEV': + norms = np.max(np.abs(points), axis=0) + elif self.metric == 'MANHATTAN': + norms = np.sum(np.abs(points), axis=0) + else: + raise Exception('Unknown metric') + if self.falloff is not None: + result = self.falloff(norms) + return result + else: + return norms + + def evaluate(self, x, y, z): + point = np.array([x, y, z]) - self.center + if self.metric == 'EUCLIDEAN': + norm = np.linalg.norm(point) + elif self.metric == 'CHEBYSHEV': + norm = np.max(np.abs(point)) + elif self.metric == 'MANHATTAN': + norm = np.sum(np.abs(point)) + else: + raise Exception('Unknown metric') + if self.falloff is not None: + return self.falloff(np.array([norm]))[0] + else: + return norm + +class SvScalarFieldBinOp(SvScalarField): + def __init__(self, field1, field2, function): + self.function = function + self.field1 = field1 + self.field2 = field2 + + def evaluate(self, x, y, z): + return self.function(self.field1.evaluate(x, y, z), self.field2.evaluate(x, y, z)) + + def evaluate_grid(self, xs, ys, zs): + func = lambda xs, ys, zs : self.function(self.field1.evaluate_grid(xs, ys, zs), self.field2.evaluate_grid(xs, ys, zs)) + return np.vectorize(func, signature="(m),(m),(m)->(m)")(xs, ys, zs) + +class SvNegatedScalarField(SvScalarField): + def __init__(self, field): + self.field = field + self.__description__ = "Negate({})".format(field) + + def evaluate(self, x, y, z): + v = self.field.evaluate(x, y, z) + return -x + + def evaluate_grid(self, xs, ys, zs): + return (- self.field.evaluate_grid(xs, ys, zs)) + +class SvVectorFieldsScalarProduct(SvScalarField): + def __init__(self, field1, field2): + self.field1 = field1 + self.field2 = field2 + self.__description__ = "{} . {}".format(field1, field2) + + def evaluate(self, x, y, z): + v1 = self.field1.evaluate(x, y, z) + v2 = self.field2.evaluate(x, y, z) + return np.dot(v1, v2) + + def evaluate_grid(self, xs, ys, zs): + vx1, vy1, vz1 = self.field1.evaluate_grid(xs, ys, zs) + vx2, vy2, vz2 = self.field2.evaluate_grid(xs, ys, zs) + vectors1 = np.stack((vx1, vy1, vz1)).T + vectors2 = np.stack((vx2, vy2, vz2)).T + result = np.vectorize(np.dot, signature="(3),(3)->()")(vectors1, vectors2) + return result + +class SvVectorFieldNorm(SvScalarField): + def __init__(self, field): + self.field = field + self.__description__ = "Norm({})".format(field) + + def evaluate(self, x, y, z): + v = self.field.evaluate(x, y, z) + return np.linalg.norm(v) + + def evaluate_grid(self, xs, ys, zs): + vx, vy, vz = self.field.evaluate_grid(xs, ys, zs) + vectors = np.stack((vx, vy, vz)).T + result = np.linalg.norm(vectors, axis=1) + return result + +class SvMergedScalarField(SvScalarField): + def __init__(self, mode, fields): + self.mode = mode + self.fields = fields + self.__description__ = "{}{}".format(mode, fields) + + def _minimal_diff(self, array, **kwargs): + v1,v2 = np.partition(array, 1, **kwargs)[0:2] + return abs(v1 - v2) + + def evaluate(self, x, y, z): + values = np.array([field.evaluate(x, y, z) for field in self.fields]) + if self.mode == 'MIN': + value = np.min(values) + elif self.mode == 'MAX': + value = np.max(values) + elif self.mode == 'SUM': + value = np.sum(values) + elif self.mode == 'AVG': + value = np.mean(values) + elif self.mode == 'MINDIFF': + value = self._minimal_diff(values) + else: + raise Exception("unsupported operation") + return value + + def evaluate_grid(self, xs, ys, zs): + values = np.array([field.evaluate_grid(xs, ys, zs) for field in self.fields]) + if self.mode == 'MIN': + value = np.min(values, axis=0) + elif self.mode == 'MAX': + value = np.max(values, axis=0) + elif self.mode == 'SUM': + value = np.sum(values, axis=0) + elif self.mode == 'AVG': + value = np.mean(values, axis=0) + elif self.mode == 'MINDIFF': + value = self._minimal_diff(values, axis=0) + else: + raise Exception("unsupported operation") + return value + +class SvKdtScalarField(SvScalarField): + __description__ = "KDT" + + def __init__(self, vertices=None, kdt=None, falloff=None): + self.falloff = falloff + if kdt is not None: + self.kdt = kdt + elif vertices is not None: + self.kdt = kdtree.KDTree(len(vertices)) + for i, v in enumerate(vertices): + self.kdt.insert(v, i) + self.kdt.balance() + else: + raise Exception("Either kdt or vertices must be provided") + + def evaluate(self, x, y, z): + nearest, i, distance = self.kdt.find((x, y, z)) + if self.falloff is not None: + value = self.falloff(np.array([distance]))[0] + return value + else: + return distance + + def evaluate_grid(self, xs, ys, zs): + def find(v): + nearest, i, distance = self.kdt.find(v) + return distance + + points = np.stack((xs, ys, zs)).T + norms = np.vectorize(find, signature='(3)->()')(points) + if self.falloff is not None: + result = self.falloff(norms) + return result + else: + return norms + +class SvLineAttractorScalarField(SvScalarField): + __description__ = "Line Attractor" + + def __init__(self, center, direction, falloff=None): + self.center = center + self.direction = direction + self.falloff = falloff + + def evaluate(self, x, y, z): + vertex = np.array([x,y,z]) + direction = self.direction + to_center = self.center - vertex + projection = np.dot(to_center, direction) * direction / np.dot(direction, direction) + dv = to_center - projection + return np.linalg.norm(dv) + + def evaluate_grid(self, xs, ys, zs): + direction = self.direction + direction2 = np.dot(direction, direction) + + def func(vertex): + to_center = self.center - vertex + projection = np.dot(to_center, direction) * direction / direction2 + dv = to_center - projection + return np.linalg.norm(dv) + + points = np.stack((xs, ys, zs)).T + norms = np.vectorize(func, signature='(3)->()')(points) + if self.falloff is not None: + result = self.falloff(norms) + return result + else: + return norms + +class SvPlaneAttractorScalarField(SvScalarField): + __description__ = "Plane Attractor" + + def __init__(self, center, direction, falloff=None): + self.center = center + self.direction = direction + self.falloff = falloff + + def evaluate(self, x, y, z): + vertex = np.array([x,y,z]) + direction = self.direction + to_center = self.center - vertex + projection = np.dot(to_center, direction) * direction / np.dot(direction, direction) + return np.linalg.norm(projection) + + def evaluate_grid(self, xs, ys, zs): + direction = self.direction + direction2 = np.dot(direction, direction) + + def func(vertex): + to_center = self.center - vertex + projection = np.dot(to_center, direction) * direction / direction2 + return np.linalg.norm(projection) + + points = np.stack((xs, ys, zs)).T + norms = np.vectorize(func, signature='(3)->()')(points) + if self.falloff is not None: + result = self.falloff(norms) + return result + else: + return norms + +class SvBvhAttractorScalarField(SvScalarField): + __description__ = "BVH Attractor" + + def __init__(self, bvh=None, verts=None, faces=None, falloff=None, signed=False): + self.falloff = falloff + self.signed = signed + if bvh is not None: + self.bvh = bvh + elif verts is not None and faces is not None: + self.bvh = bvhtree.BVHTree.FromPolygons(verts, faces) + else: + raise Exception("Either bvh or verts and faces must be provided!") + + def evaluate(self, x, y, z): + nearest, normal, idx, distance = self.bvh.find_nearest((x,y,z)) + if self.signed: + sign = (Vector((x,y,z)) - nearest).dot(normal) + sign = copysign(1, sign) + else: + sign = 1 + return sign * distance + + def evaluate_grid(self, xs, ys, zs): + def find(v): + nearest, normal, idx, distance = self.bvh.find_nearest(v) + if nearest is None: + raise Exception("No nearest point on mesh found for vertex %s" % v) + if self.signed: + sign = (v - nearest).dot(normal) + sign = copysign(1, sign) + else: + sign = 1 + return sign * distance + + points = np.stack((xs, ys, zs)).T + norms = np.vectorize(find, signature='(3)->()')(points) + if self.falloff is not None: + result = self.falloff(norms) + return result + else: + return norms + +class SvVectorScalarFieldComposition(SvScalarField): + __description__ = "Composition" + + def __init__(self, vfield, sfield): + self.sfield = sfield + self.vfield = vfield + + def evaluate(self, x, y, z): + x1, y1, z1 = self.vfield.evaluate(x,y,z) + v2 = self.sfield.evaluate(x1,y1,z1) + return v2 + + def evaluate_grid(self, xs, ys, zs): + vx1, vy1, vz1 = self.vfield.evaluate_grid(xs, ys, zs) + return self.sfield.evaluate_grid(vx1, vy1, vz1) + +class SvVectorFieldDivergence(SvScalarField): + def __init__(self, field, step): + self.field = field + self.step = step + self.__description__ = "Div({})".format(field) + + def evaluate(self, x, y, z): + step = self.step + xs_dx_plus, _, _ = self.field.evaluate(x+step,y,z) + xs_dx_minus, _, _ = self.field.evaluate(x-step,y,z) + _, ys_dy_plus, _ = self.field.evaluate(x, y+step, z) + _, ys_dy_minus, _ = self.field.evaluate(x, y-step, z) + _, _, zs_dz_plus = self.field.evaluate(x, y, z+step) + _, _, zs_dz_minus = self.field.evaluate(x, y, z-step) + + dx_dx = (xs_dx_plus - xs_dx_minus) / (2*step) + dy_dy = (ys_dy_plus - ys_dy_minus) / (2*step) + dz_dz = (zs_dz_plus - zs_dz_minus) / (2*step) + + return dx_dx + dy_dy + dz_dz + + def evaluate_grid(self, xs, ys, zs): + step = self.step + xs_dx_plus, _, _ = self.field.evaluate_grid(xs+step, ys,zs) + xs_dx_minus, _, _ = self.field.evaluate_grid(xs-step,ys,zs) + _, ys_dy_plus, _ = self.field.evaluate_grid(xs, ys+step, zs) + _, ys_dy_minus, _ = self.field.evaluate_grid(xs, ys-step, zs) + _, _, zs_dz_plus = self.field.evaluate_grid(xs, ys, zs+step) + _, _, zs_dz_minus = self.field.evaluate_grid(xs, ys, zs-step) + + dx_dx = (xs_dx_plus - xs_dx_minus) / (2*step) + dy_dy = (ys_dy_plus - ys_dy_minus) / (2*step) + dz_dz = (zs_dz_plus - zs_dz_minus) / (2*step) + + return dx_dx + dy_dy + dz_dz + +class SvScalarFieldLaplacian(SvScalarField): + def __init__(self, field, step): + self.field = field + self.step = step + self.__description__ = "Laplace({})".format(field) + + def evaluate(self, x, y, z): + step = self.step + v_dx_plus = self.field.evaluate(x+step,y,z) + v_dx_minus = self.field.evaluate(x-step,y,z) + v_dy_plus = self.field.evaluate(x, y+step, z) + v_dy_minus = self.field.evaluate(x, y-step, z) + v_dz_plus = self.field.evaluate(x, y, z+step) + v_dz_minus = self.field.evaluate(x, y, z-step) + v0 = self.field.evaluate(x, y, z) + + sides = v_dx_plus + v_dx_minus + v_dy_plus + v_dy_minus + v_dz_plus + v_dz_minus + result = (sides - 6*v0) / (8 * step * step * step) + return result + + def evaluate_grid(self, xs, ys, zs): + step = self.step + v_dx_plus = self.field.evaluate_grid(xs+step, ys,zs) + v_dx_minus = self.field.evaluate_grid(xs-step,ys,zs) + v_dy_plus = self.field.evaluate_grid(xs, ys+step, zs) + v_dy_minus = self.field.evaluate_grid(xs, ys-step, zs) + v_dz_plus = self.field.evaluate_grid(xs, ys, zs+step) + v_dz_minus = self.field.evaluate_grid(xs, ys, zs-step) + v0 = self.field.evaluate_grid(xs, ys, zs) + + sides = v_dx_plus + v_dx_minus + v_dy_plus + v_dy_minus + v_dz_plus + v_dz_minus + result = (sides - 6*v0) / (8 * step * step * step) + return result + +class SvVoronoiScalarField(SvScalarField): + __description__ = "Voronoi" + + def __init__(self, vertices): + self.kdt = kdtree.KDTree(len(vertices)) + for i, v in enumerate(vertices): + self.kdt.insert(v, i) + self.kdt.balance() + + def evaluate(self, x, y, z): + vs = self.kdt.find_n((x,y,z), 2) + if len(vs) != 2: + raise Exception("Unexpected kdt result at (%s,%s,%s): %s" % (x, y, z, vs)) + t1, t2 = vs + distance1 = t1[2] + distance2 = t2[2] + return abs(distance1 - distance2) + + def evaluate_grid(self, xs, ys, zs): + return np.vectorize(self.evaluate, signature='(),(),()->()')(xs,ys,zs) + diff --git a/utils/field/vector.py b/utils/field/vector.py new file mode 100644 index 0000000000..71f3c68e4f --- /dev/null +++ b/utils/field/vector.py @@ -0,0 +1,907 @@ +# This file is part of project Sverchok. It's copyrighted by the contributors +# recorded in the version control history of the file, available from +# its original location https://github.com/nortikin/sverchok/commit/master +# +# SPDX-License-Identifier: GPL3 +# License-Filename: LICENSE + +import numpy as np +from math import sqrt, copysign +from mathutils import Matrix, Vector +from mathutils import noise +from mathutils import kdtree +from mathutils import bvhtree + +from sverchok.utils.geom import autorotate_householder, autorotate_track, autorotate_diff, diameter +from sverchok.utils.math import from_cylindrical, from_spherical + +from sverchok.utils.curve import SvCurveLengthSolver + +################## +# # +# Vector Fields # +# # +################## + +class SvVectorField(object): + def __repr__(self): + if hasattr(self, '__description__'): + description = self.__description__ + else: + description = self.__class__.__name__ + return "<{} vector field>".format(description) + + def evaluate(self, point): + raise Exception("not implemented") + + def evaluate_grid(self, xs, ys, zs): + raise Exception("not implemented") + +class SvMatrixVectorField(SvVectorField): + + def __init__(self, matrix): + self.matrix = matrix + self.__description__ = "Matrix" + + def evaluate(self, x, y, z): + v = Vector((x, y, z)) + v = (self.matrix @ v) - v + return np.array(v) + + def evaluate_grid(self, xs, ys, zs): + matrix = np.array(self.matrix.to_3x3()) + translation = np.array(self.matrix.translation) + points = np.stack((xs, ys, zs)).T + R = np.apply_along_axis(lambda v : matrix @ v + translation - v, 1, points).T + return R[0], R[1], R[2] + +class SvConstantVectorField(SvVectorField): + + def __init__(self, vector): + self.vector = vector + self.__description__ = "Constant = {}".format(vector) + + def evaluate(self, x, y, z): + return self.vector + + def evaluate_grid(self, xs, ys, zs): + x, y, z = self.vector + rx = np.full_like(xs, x) + ry = np.full_like(ys, y) + rz = np.full_like(zs, z) + return rx, ry, rz + +class SvComposedVectorField(SvVectorField): + def __init__(self, coords, sfield1, sfield2, sfield3): + self.coords = coords + self.sfield1 = sfield1 + self.sfield2 = sfield2 + self.sfield3 = sfield3 + self.__description__ = "{}({}, {}, {})".format(coords, sfield1, sfield2, sfield3) + + def evaluate(self, x, y, z): + v1 = self.sfield1.evaluate(x, y, z) + v2 = self.sfield2.evaluate(x, y, z) + v3 = self.sfield3.evaluate(x, y, z) + if self.coords == 'XYZ': + return np.array((v1, v2, v3)) + elif self.coords == 'CYL': + return np.array(from_cylindrical(v1, v2, v3, mode='radians')) + else: # SPH: + return np.array(from_spherical(v1, v2, v3, mode='radians')) + + def evaluate_grid(self, xs, ys, zs): + v1s = self.sfield1.evaluate_grid(xs, ys, zs) + v2s = self.sfield2.evaluate_grid(xs, ys, zs) + v3s = self.sfield3.evaluate_grid(xs, ys, zs) + if self.coords == 'XYZ': + return v1s, v2s, v3s + elif self.coords == 'CYL': + vectors = np.stack((v1s, v2s, v3s)).T + vectors = np.apply_along_axis(lambda v: np.array(from_cylindrical(*tuple(v), mode='radians')), 1, vectors).T + return vectors[0], vectors[1], vectors[2] + else: # SPH: + vectors = np.stack((v1s, v2s, v3s)).T + vectors = np.apply_along_axis(lambda v: np.array(from_spherical(*tuple(v), mode='radians')), 1, vectors).T + return vectors[0], vectors[1], vectors[2] + +class SvAbsoluteVectorField(SvVectorField): + def __init__(self, field): + self.field = field + self.__description__ = "Absolute({})".format(field) + + def evaluate(self, x, y, z): + r = self.field.evaluate(x, y, z) + return r + np.array([x, y, z]) + + def evaluate_grid(self, xs, ys, zs): + rxs, rys, rzs = self.field.evaluate_grid(xs, ys, zs) + return rxs + xs, rys + ys, rzs + zs + +class SvRelativeVectorField(SvVectorField): + def __init__(self, field): + self.field = field + self.__description__ = "Relative({})".format(field) + + def evaluate(self, x, y, z): + r = self.field.evaluate(x, y, z) + return r - np.array([x, y, z]) + + def evaluate_grid(self, xs, ys, zs): + rxs, rys, rzs = self.field.evaluate_grid(xs, ys, zs) + return rxs - xs, rys - ys, rzs - zs + +class SvVectorFieldLambda(SvVectorField): + + __description__ = "Formula" + + def __init__(self, function, variables, in_field): + self.function = function + self.variables = variables + self.in_field = in_field + + def evaluate_grid(self, xs, ys, zs): + if self.in_field is None: + Vs = np.zeros(xs.shape[0]) + else: + vx, vy, vz = self.in_field.evaluate_grid(xs, ys, zs) + Vs = np.stack((vx, vy, vz)).T + return np.vectorize(self.function, + signature = "(),(),(),(3)->(),(),()")(xs, ys, zs, Vs) + + def evaluate(self, x, y, z): + if self.in_field is None: + V = None + else: + V = self.in_field.evaluate(x, y, z) + return self.function(x, y, z, V) + +class SvVectorFieldBinOp(SvVectorField): + def __init__(self, field1, field2, function): + self.function = function + self.field1 = field1 + self.field2 = field2 + + def evaluate(self, x, y, z): + return self.function(self.field1.evaluate(x, y, z), self.field2.evaluate(x, y, z)) + + def evaluate_grid(self, xs, ys, zs): + def func(xs, ys, zs): + vx1, vy1, vz1 = self.field1.evaluate_grid(xs, ys, zs) + vx2, vy2, vz2 = self.field2.evaluate_grid(xs, ys, zs) + R = self.function(np.array([vx1, vy1, vz1]), np.array([vx2, vy2, vz2])) + return R[0], R[1], R[2] + return np.vectorize(func, signature="(m),(m),(m)->(m),(m),(m)")(xs, ys, zs) + +class SvAverageVectorField(SvVectorField): + + def __init__(self, fields): + self.fields = fields + self.__description__ = "Average" + + def evaluate(self, x, y, z): + vectors = np.array([field.evaluate(x, y, z) for field in self.fields]) + return np.mean(vectors, axis=0) + + def evaluate_grid(self, xs, ys, zs): + def func(xs, ys, zs): + data = [] + for field in self.fields: + vx, vy, vz = field.evaluate_grid(xs, ys, zs) + vectors = np.stack((vx, vy, vz)).T + data.append(vectors) + data = np.array(data) + mean = np.mean(data, axis=0).T + return mean[0], mean[1], mean[2] + return np.vectorize(func, signature="(m),(m),(m)->(m),(m),(m)")(xs, ys, zs) + +class SvVectorFieldCrossProduct(SvVectorField): + def __init__(self, field1, field2): + self.field1 = field1 + self.field2 = field2 + self.__description__ = "{} x {}".format(field1, field2) + + def evaluate(self, x, y, z): + v1 = self.field1.evaluate(x, y, z) + v2 = self.field2.evaluate(x, y, z) + return np.cross(v1, v2) + + def evaluate_grid(self, xs, ys, zs): + vx1, vy1, vz1 = self.field1.evaluate_grid(xs, ys, zs) + vx2, vy2, vz2 = self.field2.evaluate_grid(xs, ys, zs) + vectors1 = np.stack((vx1, vy1, vz1)).T + vectors2 = np.stack((vx2, vy2, vz2)).T + R = np.cross(vectors1, vectors2).T + return R[0], R[1], R[2] + +class SvVectorFieldMultipliedByScalar(SvVectorField): + def __init__(self, vector_field, scalar_field): + self.vector_field = vector_field + self.scalar_field = scalar_field + self.__description__ = "{} * {}".format(scalar_field, vector_field) + + def evaluate(self, x, y, z): + scalar = self.scalar_field.evaluate(x, y, z) + vector = self.vector_field.evaluate(x, y, z) + return scalar * vector + + def evaluate_grid(self, xs, ys, zs): + def product(xs, ys, zs): + scalars = self.scalar_field.evaluate_grid(xs, ys, zs) + vx, vy, vz = self.vector_field.evaluate_grid(xs, ys, zs) + vectors = np.stack((vx, vy, vz)) + R = (scalars * vectors) + return R[0], R[1], R[2] + return np.vectorize(product, signature="(m),(m),(m)->(m),(m),(m)")(xs, ys, zs) + +class SvVectorFieldsLerp(SvVectorField): + + def __init__(self, vfield1, vfield2, scalar_field): + self.vfield1 = vfield1 + self.vfield2 = vfield2 + self.scalar_field = scalar_field + self.__description__ = "Lerp" + + def evaluate(self, x, y, z): + scalar = self.scalar_field.evaluate(x, y, z) + vector1 = self.vfield1.evaluate(x, y, z) + vector2 = self.vfield2.evaluate(x, y, z) + return (1 - scalar) * vector1 + scalar * vector2 + + def evaluate_grid(self, xs, ys, zs): + scalars = self.scalar_field.evaluate_grid(xs, ys, zs) + vx1, vy1, vz1 = self.vfield1.evaluate_grid(xs, ys, zs) + vectors1 = np.stack((vx1, vy1, vz1)) + vx2, vy2, vz2 = self.vfield2.evaluate_grid(xs, ys, zs) + vectors2 = np.stack((vx2, vy2, vz2)) + R = (1 - scalars) * vectors1 + scalars * vectors2 + return R[0], R[1], R[2] + +class SvNoiseVectorField(SvVectorField): + def __init__(self, noise_type, seed): + self.noise_type = noise_type + self.seed = seed + self.__description__ = "{} noise".format(noise_type) + + def evaluate(self, x, y, z): + noise.seed_set(self.seed) + return noise.noise_vector((x, y, z), noise_basis=self.noise_type) + + def evaluate_grid(self, xs, ys, zs): + noise.seed_set(self.seed) + def mk_noise(v): + r = noise.noise_vector(v, noise_basis=self.noise_type) + return r[0], r[1], r[2] + vectors = np.stack((xs,ys,zs)).T + return np.vectorize(mk_noise, signature="(3)->(),(),()")(vectors) + +class SvKdtVectorField(SvVectorField): + + def __init__(self, vertices=None, kdt=None, falloff=None, negate=False): + self.falloff = falloff + self.negate = negate + if kdt is not None: + self.kdt = kdt + elif vertices is not None: + self.kdt = kdtree.KDTree(len(vertices)) + for i, v in enumerate(vertices): + self.kdt.insert(v, i) + self.kdt.balance() + else: + raise Exception("Either kdt or vertices must be provided") + self.__description__ = "KDT Attractor" + + def evaluate(self, x, y, z): + nearest, i, distance = self.kdt.find((x, y, z)) + vector = np.array(nearest) - np.array([x, y, z]) + if self.falloff is not None: + value = self.falloff(np.array([distance]))[0] + if self.negate: + value = - value + norm = np.linalg.norm(vector) + return value * vector / norm + else: + if self.negate: + return - vector + else: + return vector + + def evaluate_grid(self, xs, ys, zs): + def find(v): + nearest, i, distance = self.kdt.find(v) + dx, dy, dz = np.array(nearest) - np.array(v) + if self.negate: + return (-dx, -dy, -dz) + else: + return (dx, dy, dz) + + points = np.stack((xs, ys, zs)).T + vectors = np.vectorize(find, signature='(3)->(),(),()')(points) + if self.falloff is not None: + norms = np.linalg.norm(vectors, axis=0) + lens = self.falloff(norms) + R = (lens * vectors).T + return R[0], R[1], R[2] + else: + return vectors + +class SvVectorFieldPointDistance(SvVectorField): + def __init__(self, center, metric='EUCLIDEAN', falloff=None): + self.center = center + self.falloff = falloff + self.metric = metric + self.__description__ = "Distance from {}".format(tuple(center)) + + def evaluate_grid(self, xs, ys, zs): + x0, y0, z0 = tuple(self.center) + xs = x0 - xs + ys = y0 - ys + zs = z0 - zs + vectors = np.stack((xs, ys, zs)) + if self.metric == 'EUCLIDEAN': + norms = np.linalg.norm(vectors, axis=0) + elif self.metric == 'CHEBYSHEV': + norms = np.max(np.abs(vectors), axis=0) + elif self.metric == 'MANHATTAN': + norms = np.sum(np.abs(vectors), axis=0) + else: + raise Exception('Unknown metric') + if self.falloff is not None: + lens = self.falloff(norms) + R = lens * vectors / norms + else: + R = vectors + return R[0], R[1], R[2] + + def evaluate(self, x, y, z): + point = np.array([x, y, z]) - self.center + if self.metric == 'EUCLIDEAN': + norm = np.linalg.norm(point) + elif self.metric == 'CHEBYSHEV': + norm = np.max(point) + elif self.metric == 'MANHATTAN': + norm = np.sum(np.abs(point)) + else: + raise Exception('Unknown metric') + if self.falloff is not None: + value = self.falloff(np.array([norm]))[0] + return value * point / norm + else: + return point + +class SvLineAttractorVectorField(SvVectorField): + + def __init__(self, center, direction, falloff=None): + self.center = center + self.direction = direction + self.falloff = falloff + self.__description__ = "Line Attractor" + + def evaluate(self, x, y, z): + vertex = np.array([x,y,z]) + direction = self.direction + to_center = self.center - vertex + projection = np.dot(to_center, direction) * direction / np.dot(direction, direction) + dv = to_center - projection + if self.falloff is not None: + norm = np.linalg.norm(dv) + dv = self.falloff(norm) * dv / norm + return dv + + def evaluate_grid(self, xs, ys, zs): + direction = self.direction + direction2 = np.dot(direction, direction) + + def func(vertex): + to_center = self.center - vertex + projection = np.dot(to_center, direction) * direction / direction2 + dv = to_center - projection + return dv + + points = np.stack((xs, ys, zs)).T + vectors = np.vectorize(func, signature='(3)->(3)')(points) + if self.falloff is not None: + norms = np.linalg.norm(vectors, axis=1, keepdims=True) + nonzero = (norms > 0)[:,0] + lens = self.falloff(norms) + vectors[nonzero] = vectors[nonzero] / norms[nonzero][:,0][np.newaxis].T + R = (lens * vectors).T + return R[0], R[1], R[2] + else: + R = vectors.T + return R[0], R[1], R[2] + +class SvPlaneAttractorVectorField(SvVectorField): + + def __init__(self, center, direction, falloff=None): + self.center = center + self.direction = direction + self.falloff = falloff + self.__description__ = "Plane Attractor" + + def evaluate(self, x, y, z): + vertex = np.array([x,y,z]) + direction = self.direction + to_center = self.center - vertex + dv = np.dot(to_center, direction) * direction / np.dot(direction, direction) + if self.falloff is not None: + norm = np.linalg.norm(dv) + dv = self.falloff(norm) * dv / norm + return dv + + def evaluate_grid(self, xs, ys, zs): + direction = self.direction + direction2 = np.dot(direction, direction) + + def func(vertex): + to_center = self.center - vertex + projection = np.dot(to_center, direction) * direction / direction2 + return projection + + points = np.stack((xs, ys, zs)).T + vectors = np.vectorize(func, signature='(3)->(3)')(points) + if self.falloff is not None: + norms = np.linalg.norm(vectors, axis=1, keepdims=True) + lens = self.falloff(norms) + nonzero = (norms > 0)[:,0] + vectors[nonzero] = vectors[nonzero] / norms[nonzero][:,0][np.newaxis].T + R = (lens * vectors).T + return R[0], R[1], R[2] + else: + R = vectors.T + return R[0], R[1], R[2] + +class SvBvhAttractorVectorField(SvVectorField): + + def __init__(self, bvh=None, verts=None, faces=None, falloff=None, use_normal=False, signed_normal=False): + self.falloff = falloff + self.use_normal = use_normal + self.signed_normal = signed_normal + if bvh is not None: + self.bvh = bvh + elif verts is not None and faces is not None: + self.bvh = bvhtree.BVHTree.FromPolygons(verts, faces) + else: + raise Exception("Either bvh or verts and faces must be provided!") + self.__description__ = "BVH Attractor" + + def evaluate(self, x, y, z): + vertex = Vector((x,y,z)) + nearest, normal, idx, distance = self.bvh.find_nearest(vertex) + if self.use_normal: + if self.signed_normal: + sign = (v - nearest).dot(normal) + sign = copysign(1, sign) + else: + sign = 1 + return sign * np.array(normal) + else: + return np.array(nearest - vertex) + + def evaluate_grid(self, xs, ys, zs): + def find(v): + nearest, normal, idx, distance = self.bvh.find_nearest(v) + if nearest is None: + raise Exception("No nearest point on mesh found for vertex %s" % v) + if self.use_normal: + if self.signed_normal: + sign = (v - nearest).dot(normal) + sign = copysign(1, sign) + else: + sign = 1 + return sign * np.array(normal) + else: + return np.array(nearest) - v + + points = np.stack((xs, ys, zs)).T + vectors = np.vectorize(find, signature='(3)->(3)')(points) + if self.falloff is not None: + norms = np.linalg.norm(vectors, axis=0) + lens = self.falloff(norms) + R = (lens * vectors).T + return R[0], R[1], R[2] + else: + R = vectors.T + return R[0], R[1], R[2] + +class SvVectorFieldTangent(SvVectorField): + + def __init__(self, field1, field2): + self.field1 = field1 + self.field2 = field2 + self.__description__ = "Tangent" + + def evaluate(self, x, y, z): + v1 = self.field1.evaluate(x,y,z) + v2 = self.field2.evaluate(x,y,z) + projection = np.dot(v1, v2) * v2 / np.dot(v2, v2) + return projection + + def evaluate_grid(self, xs, ys, zs): + vx1, vy1, vz1 = self.field1.evaluate_grid(xs, ys, zs) + vx2, vy2, vz2 = self.field2.evaluate_grid(xs, ys, zs) + vectors1 = np.stack((vx1, vy1, vz1)).T + vectors2 = np.stack((vx2, vy2, vz2)).T + + def project(v1, v2): + projection = np.dot(v1, v2) * v2 / np.dot(v2, v2) + vx, vy, vz = projection + return vx, vy, vz + + return np.vectorize(project, signature="(3),(3)->(),(),()")(vectors1, vectors2) + +class SvVectorFieldCotangent(SvVectorField): + + def __init__(self, field1, field2): + self.field1 = field1 + self.field2 = field2 + self.__description__ = "Cotangent" + + def evaluate(self, x, y, z): + v1 = self.field1.evaluate(x,y,z) + v2 = self.field2.evaluate(x,y,z) + projection = np.dot(v1, v2) * v2 / np.dot(v2, v2) + return v1 - projection + + def evaluate_grid(self, xs, ys, zs): + vx1, vy1, vz1 = self.field1.evaluate_grid(xs, ys, zs) + vx2, vy2, vz2 = self.field2.evaluate_grid(xs, ys, zs) + vectors1 = np.stack((vx1, vy1, vz1)).T + vectors2 = np.stack((vx2, vy2, vz2)).T + + def project(v1, v2): + projection = np.dot(v1, v2) * v2 / np.dot(v2, v2) + coprojection = v1 - projection + vx, vy, vz = coprojection + return vx, vy, vz + + return np.vectorize(project, signature="(3),(3)->(),(),()")(vectors1, vectors2) + +class SvVectorFieldComposition(SvVectorField): + + def __init__(self, field1, field2): + self.field1 = field1 + self.field2 = field2 + self.__description__ = "Composition" + + def evaluate(self, x, y, z): + x1, y1, z1 = self.field1.evaluate(x,y,z) + v2 = self.field2.evaluate(x1,y1,z1) + return v2 + + def evaluate_grid(self, xs, ys, zs): + r = self.field1.evaluate_grid(xs, ys, zs) + vx1, vy1, vz1 = r + return self.field2.evaluate_grid(vx1, vy1, vz1) + +class SvScalarFieldGradient(SvVectorField): + def __init__(self, field, step): + self.field = field + self.step = step + self.__description__ = "Grad({})".format(field) + + def evaluate(self, x, y, z): + step = self.step + v_dx_plus = self.field.evaluate(x+step,y,z) + v_dx_minus = self.field.evaluate(x-step,y,z) + v_dy_plus = self.field.evaluate(x, y+step, z) + v_dy_minus = self.field.evaluate(x, y-step, z) + v_dz_plus = self.field.evaluate(x, y, z+step) + v_dz_minus = self.field.evaluate(x, y, z-step) + + dv_dx = (v_dx_plus - v_dx_minus) / (2*step) + dv_dy = (v_dy_plus - v_dy_minus) / (2*step) + dv_dz = (v_dz_plus - v_dz_minus) / (2*step) + return np.array([dv_dx, dv_dy, dv_dz]) + + def evaluate_grid(self, xs, ys, zs): + step = self.step + v_dx_plus = self.field.evaluate_grid(xs+step, ys,zs) + v_dx_minus = self.field.evaluate_grid(xs-step,ys,zs) + v_dy_plus = self.field.evaluate_grid(xs, ys+step, zs) + v_dy_minus = self.field.evaluate_grid(xs, ys-step, zs) + v_dz_plus = self.field.evaluate_grid(xs, ys, zs+step) + v_dz_minus = self.field.evaluate_grid(xs, ys, zs-step) + + dv_dx = (v_dx_plus - v_dx_minus) / (2*step) + dv_dy = (v_dy_plus - v_dy_minus) / (2*step) + dv_dz = (v_dz_plus - v_dz_minus) / (2*step) + + R = np.stack((dv_dx, dv_dy, dv_dz)) + return R[0], R[1], R[2] + +class SvVectorFieldRotor(SvVectorField): + def __init__(self, field, step): + self.field = field + self.step = step + self.__description__ = "Rot({})".format(field) + + def evaluate(self, x, y, z): + step = self.step + _, y_dx_plus, z_dx_plus = self.field.evaluate(x+step,y,z) + _, y_dx_minus, z_dx_minus = self.field.evaluate(x-step,y,z) + x_dy_plus, _, z_dy_plus = self.field.evaluate(x, y+step, z) + x_dy_minus, _, z_dy_minus = self.field.evaluate(x, y-step, z) + x_dz_plus, y_dz_plus, _ = self.field.evaluate(x, y, z+step) + x_dz_minus, y_dz_minus, _ = self.field.evaluate(x, y, z-step) + + dy_dx = (y_dx_plus - y_dx_minus) / (2*step) + dz_dx = (z_dx_plus - z_dx_minus) / (2*step) + dx_dy = (x_dy_plus - x_dy_minus) / (2*step) + dz_dy = (z_dy_plus - z_dy_minus) / (2*step) + dx_dz = (x_dz_plus - x_dz_minus) / (2*step) + dy_dz = (y_dz_plus - y_dz_minus) / (2*step) + + rx = dz_dy - dy_dz + ry = - (dz_dx - dx_dz) + rz = dy_dx - dx_dy + + return np.array([rx, ry, rz]) + + def evaluate_grid(self, xs, ys, zs): + step = self.step + _, y_dx_plus, z_dx_plus = self.field.evaluate_grid(xs+step,ys,zs) + _, y_dx_minus, z_dx_minus = self.field.evaluate_grid(xs-step,ys,zs) + x_dy_plus, _, z_dy_plus = self.field.evaluate_grid(xs, ys+step, zs) + x_dy_minus, _, z_dy_minus = self.field.evaluate_grid(xs, ys-step, zs) + x_dz_plus, y_dz_plus, _ = self.field.evaluate_grid(xs, ys, zs+step) + x_dz_minus, y_dz_minus, _ = self.field.evaluate_grid(xs, ys, zs-step) + + dy_dx = (y_dx_plus - y_dx_minus) / (2*step) + dz_dx = (z_dx_plus - z_dx_minus) / (2*step) + dx_dy = (x_dy_plus - x_dy_minus) / (2*step) + dz_dy = (z_dy_plus - z_dy_minus) / (2*step) + dx_dz = (x_dz_plus - x_dz_minus) / (2*step) + dy_dz = (y_dz_plus - y_dz_minus) / (2*step) + + rx = dz_dy - dy_dz + ry = - (dz_dx - dx_dz) + rz = dy_dx - dx_dy + R = np.stack((rx, ry, rz)) + return R[0], R[1], R[2] + +class SvBendAlongCurveField(SvVectorField): + def __init__(self, curve, algorithm, scale_all, axis, t_min, t_max, up_axis=None, resolution=50, length_mode='T'): + self.curve = curve + self.axis = axis + self.t_min = t_min + self.t_max = t_max + self.algorithm = algorithm + self.scale_all = scale_all + self.up_axis = up_axis + self.length_mode = length_mode + if algorithm == 'ZERO': + self.curve.pre_calc_torsion_integral(resolution) + if length_mode == 'L': + self.length_solver = SvCurveLengthSolver(curve) + self.length_solver.prepare('SPL', resolution) + self.__description__ = "Bend along {}".format(curve) + + def get_matrix(self, tangent, scale): + x = Vector((1.0, 0.0, 0.0)) + y = Vector((0.0, 1.0, 0.0)) + z = Vector((0.0, 0.0, 1.0)) + + if self.axis == 0: + ax1, ax2, ax3 = x, y, z + elif self.axis == 1: + ax1, ax2, ax3 = y, x, z + else: + ax1, ax2, ax3 = z, x, y + + if self.scale_all: + scale_matrix = Matrix.Scale(1/scale, 4, ax1) @ Matrix.Scale(scale, 4, ax2) @ Matrix.Scale(scale, 4, ax3) + else: + scale_matrix = Matrix.Scale(1/scale, 4, ax1) + scale_matrix = np.array(scale_matrix.to_3x3()) + + tangent = Vector(tangent) + if self.algorithm == 'householder': + rot = autorotate_householder(ax1, tangent).inverted() + elif self.algorithm == 'track': + axis = "XYZ"[self.axis] + rot = autorotate_track(axis, tangent, self.up_axis) + elif self.algorithm == 'diff': + rot = autorotate_diff(tangent, ax1) + else: + raise Exception("Unsupported algorithm") + rot = np.array(rot.to_3x3()) + + return np.matmul(rot, scale_matrix) + + def get_matrices(self, ts, scale): + frenet, _ , _ = self.curve.frame_array(ts) + if self.scale_all: + scale_matrix = np.array([ + [scale, 0, 0], + [0, scale, 0], + [0, 0, 1/scale] + ]) + else: + scale_matrix = np.array([ + [1, 0, 0], + [0, 1, 0], + [0, 0, 1/scale] + ]) + n = len(ts) + if self.algorithm == 'FRENET': + return frenet @ scale_matrix + elif self.algorithm == 'ZERO': + angles = - self.curve.torsion_integral(ts) + zeros = np.zeros((n,)) + ones = np.ones((n,)) + row1 = np.stack((np.cos(angles), np.sin(angles), zeros)).T # (n, 3) + row2 = np.stack((-np.sin(angles), np.cos(angles), zeros)).T # (n, 3) + row3 = np.stack((zeros, zeros, ones)).T # (n, 3) + rotation_matrices = np.dstack((row1, row2, row3)) + return frenet @ rotation_matrices @ scale_matrix + else: + raise Exception("Unsupported algorithm") + + def get_t_value(self, x, y, z): + curve_t_min, curve_t_max = self.curve.get_u_bounds() + t = [x, y, z][self.axis] + if self.length_mode == 'T': + t = (curve_t_max - curve_t_min) * (t - self.t_min) / (self.t_max - self.t_min) + curve_t_min + else: + t = (t - self.t_min) / (self.t_max - self.t_min) # 0 .. 1 + t = t * self.length_solver.get_total_length() + t = self.length_solver.solve(np.array([t]))[0] + return t + + def get_t_values(self, xs, ys, zs): + curve_t_min, curve_t_max = self.curve.get_u_bounds() + ts = [xs, ys, zs][self.axis] + if self.length_mode == 'T': + ts = (curve_t_max - curve_t_min) * (ts - self.t_min) / (self.t_max - self.t_min) + curve_t_min + else: + ts = (ts - self.t_min) / (self.t_max - self.t_min) # 0 .. 1 + ts = ts * self.length_solver.get_total_length() + ts = self.length_solver.solve(ts) + return ts + + def get_scale(self): + if self.length_mode == 'T': + curve_t_min, curve_t_max = self.curve.get_u_bounds() + t_range = curve_t_max - curve_t_min + else: + t_range = self.length_solver.get_total_length() + return (self.t_max - self.t_min) / t_range + + def evaluate(self, x, y, z): + t = self.get_t_value(x, y, z) + spline_tangent = self.curve.tangent(t) + spline_vertex = self.curve.evaluate(t) + scale = self.get_scale() + if self.algorithm in {'ZERO', 'FRENET'}: + matrix = self.get_matrices(np.array([t]), scale) + else: + matrix = self.get_matrix(spline_tangent, scale) + src_vector_projection = np.array([x, y, z]) + src_vector_projection[self.axis] = 0 + new_vertex = np.matmul(matrix, src_vector_projection) + spline_vertex + vector = new_vertex - np.array([x, y, z]) + return vector + + def evaluate_grid(self, xs, ys, zs): + ts = self.get_t_values(xs, ys, zs).flatten() + spline_tangents = self.curve.tangent_array(ts) + spline_vertices = self.curve.evaluate_array(ts) + scale = self.get_scale() + if self.algorithm in {'ZERO', 'FRENET'}: + matrices = self.get_matrices(ts, scale) + else: + matrices = np.vectorize(lambda t : self.get_matrix(t, scale), signature='(3)->(3,3)')(spline_tangents) + src_vectors = np.stack((xs, ys, zs)).T + src_vector_projections = src_vectors.copy() + src_vector_projections[:,self.axis] = 0 + #matrices = matrices[np.newaxis][np.newaxis] + multiply = np.vectorize(lambda m, v: m @ v, signature='(3,3),(3)->(3)') + new_vertices = multiply(matrices, src_vector_projections) + spline_vertices + R = (new_vertices - src_vectors).T + return R[0], R[1], R[2] + +class SvBendAlongSurfaceField(SvVectorField): + def __init__(self, surface, axis, autoscale=False, flip=False): + self.surface = surface + self.orient_axis = axis + self.autoscale = autoscale + self.flip = flip + self.u_bounds = (0, 1) + self.v_bounds = (0, 1) + self.__description__ = "Bend along {}".format(surface) + + def get_other_axes(self): + # Select U and V to be two axes except orient_axis + if self.orient_axis == 0: + u_index, v_index = 1,2 + elif self.orient_axis == 1: + u_index, v_index = 2,0 + else: + u_index, v_index = 1,0 + return u_index, v_index + + def get_uv(self, vertices): + """ + Translate source vertices to UV space of future spline. + vertices must be np.array of shape (n, 3). + """ + u_index, v_index = self.get_other_axes() + + # Rescale U and V coordinates to [0, 1], drop third coordinate + us = vertices[:,u_index].flatten() + vs = vertices[:,v_index].flatten() + min_u, max_u = self.u_bounds + min_v, max_v = self.v_bounds + size_u = max_u - min_u + size_v = max_v - min_v + + if size_u < 0.00001: + raise Exception("Object has too small size in U direction") + if size_v < 0.00001: + raise Exception("Object has too small size in V direction") + + us = self.surface.u_size * (us - min_u) / size_u + self.surface.get_u_min() + vs = self.surface.v_size * (vs - min_v) / size_v + self.surface.get_v_min() + + return size_u, size_v, us, vs + + def _evaluate(self, vertices): + src_size_u, src_size_v, us, vs = self.get_uv(vertices) + if self.autoscale: + u_index, v_index = self.get_other_axes() + scale_u = src_size_u / self.surface.u_size + scale_v = src_size_v / self.surface.v_size + scale_z = sqrt(scale_u * scale_v) + else: + if self.orient_axis == 2: + scale_z = -1.0 + else: + scale_z = 1.0 + if self.flip: + scale_z = - scale_z + + surf_vertices = self.surface.evaluate_array(us, vs) + spline_normals = self.surface.normal_array(us, vs) + zs = vertices[:,self.orient_axis].flatten() + zs = zs[np.newaxis].T + v1 = zs * spline_normals + v2 = scale_z * v1 + new_vertices = surf_vertices + v2 + return new_vertices + + def evaluate_grid(self, xs, ys, zs): + vertices = np.stack((xs, ys, zs)).T + new_vertices = self._evaluate(vertices) + R = (new_vertices - vertices).T + return R[0], R[1], R[2] + + def evaluate(self, x, y, z): + xs, ys, zs = self.evaluate_grid(np.array([[[x]]]), np.array([[[y]]]), np.array([[[z]]])) + return np.array([xs, ys, zs]) + +class SvVoronoiVectorField(SvVectorField): + + def __init__(self, vertices): + self.kdt = kdtree.KDTree(len(vertices)) + for i, v in enumerate(vertices): + self.kdt.insert(v, i) + self.kdt.balance() + self.__description__ = "Voronoi" + + def evaluate(self, x, y, z): + v = Vector((x,y,z)) + vs = self.kdt.find_n(v, 2) + if len(vs) != 2: + raise Exception("Unexpected kdt result at (%s,%s,%s): %s" % (x, y, z, vs)) + t1, t2 = vs + d1 = t1[2] + d2 = t2[2] + delta = abs(d1 - d2) + v1 = (t1[0] - v).normalized() + return delta * np.array(v1) + + def evaluate_grid(self, xs, ys, zs): + points = np.vectorize(self.evaluate, signature='(),(),()->(3)')(xs,ys,zs).T + return points[0], points[1], points[2] + diff --git a/utils/fillet.py b/utils/fillet.py new file mode 100644 index 0000000000..408a322461 --- /dev/null +++ b/utils/fillet.py @@ -0,0 +1,55 @@ + +import numpy as np +from math import tan, sin, pi + +from mathutils import Vector, Matrix + +from sverchok.utils.curve import SvCircle + +class ArcFilletData(object): + def __init__(self, center, matrix, normal, radius, p1, p2, angle): + self.center = center + self.normal = normal + self.radius = radius + self.p1 = p1 + self.p2 = p2 + self.angle = angle + self.matrix = matrix + + def get_curve(self): + circle = SvCircle(self.matrix, self.radius) + circle.u_bounds = (0.0, self.angle) + #circle.u_bounds = (-self.angle, 0.0) + return circle + +def calc_fillet(v1, v2, v3, radius): + if not isinstance(v1, Vector): + v1 = Vector(v1) + if not isinstance(v2, Vector): + v2 = Vector(v2) + if not isinstance(v3, Vector): + v3 = Vector(v3) + + dv1 = v1 - v2 + dv2 = v3 - v2 + dv1n, dv2n = dv1.normalized(), dv2.normalized() + angle = dv1.angle(dv2) + angle2 = angle / 2.0 + big_angle = pi - angle + + edge_len = radius / tan(angle2) + p1 = v2 + edge_len * dv1n + p2 = v2 + edge_len * dv2n + + center_len = radius / sin(angle2) + center = v2 + center_len * (dv1n + dv2n).normalized() + + normal = dv1.cross(dv2).normalized() + to_p1 = (p1 - center).normalized() + binormal = normal.cross(to_p1).normalized() + + matrix = Matrix([to_p1, -binormal, normal]).to_4x4().inverted() + matrix.translation = center + + return ArcFilletData(center, matrix, normal, radius, p1, p2, big_angle) + diff --git a/utils/integrate.py b/utils/integrate.py new file mode 100644 index 0000000000..7d5419939d --- /dev/null +++ b/utils/integrate.py @@ -0,0 +1,42 @@ +# This file is part of project Sverchok. It's copyrighted by the contributors +# recorded in the version control history of the file, available from +# its original location https://github.com/nortikin/sverchok/commit/master +# +# SPDX-License-Identifier: GPL3 +# License-Filename: LICENSE + +import numpy as np + +from sverchok.utils.geom import CubicSpline + +class TrapezoidIntegral(object): + """ + Calculate approximate indefinite integral from points + by trapezoid formula. + """ + def __init__(self, ts, xs, ys): + self.ts = ts + self.xs = xs + self.ys = ys + self.summands = None + + def calc(self): + dxs = self.xs[1:] - self.xs[:-1] + summands_left = self.ys[:-1] * dxs + summands_right = self.ys[1:] * dxs + + summands = (summands_left + summands_right) / 2.0 + self.summands = np.cumsum(summands) + self.summands = np.insert(self.summands, 0, 0) + + def evaluate_linear(self, ts): + return np.interp(ts, self.ts, self.summands) + + def evaluate_cubic(self, ts): + xs = self.ts + ys = self.summands + zs = np.zeros_like(xs, dtype=np.float64) + verts = np.stack((xs, ys, zs)).T + spline = CubicSpline(verts, tknots=xs, is_cyclic=False) + return spline.eval(ts)[:,1] + diff --git a/utils/math.py b/utils/math.py index f2bdeade30..ac02ccd1dd 100644 --- a/utils/math.py +++ b/utils/math.py @@ -16,9 +16,25 @@ # # ##### END GPL LICENSE BLOCK ##### +import numpy as np import math from math import sin, cos, radians, degrees, sqrt, asin, acos, atan2 +coordinate_modes = [ + ('XYZ', "Carthesian", "Carthesian coordinates - x, y, z", 0), + ('CYL', "Cylindrical", "Cylindrical coordinates - rho, phi, z", 1), + ('SPH', "Spherical", "Spherical coordinates - rho, phi, theta", 2) +] + +falloff_types = [ + ('NONE', "None - R", "Output distance", 0), + ("inverse", "Inverse - 1/R", "", 1), + ("inverse_square", "Inverse square - 1/R^2", "Similar to gravitation or electromagnetizm", 2), + ("inverse_cubic", "Inverse cubic - 1/R^3", "", 3), + ("inverse_exp", "Inverse exponent - Exp(-R)", "", 4), + ("gauss", "Gauss - Exp(-R^2/2)", "", 5) + ] + def smooth(x): return 3*x*x - 2*x*x*x @@ -55,6 +71,12 @@ def inverse_exp(c, x): def gauss(c, x): return math.exp(-c*x*x/2.0) +def inverse_exp_np(c, x): + return np.exp(-c*x) + +def gauss_np(c, x): + return np.exp(-c*x*x/2.0) + def falloff(type, radius, rho): if rho <= 0: return 1.0 @@ -63,6 +85,31 @@ def falloff(type, radius, rho): func = globals()[type] return 1.0 - func(rho / radius) +def falloff_array(falloff_type, amplitude, coefficient, clamp=False): + types = { + 'inverse': inverse, + 'inverse_square': inverse_square, + 'inverse_cubic': inverse_cubic, + 'inverse_exp': inverse_exp_np, + 'gauss': gauss_np + } + falloff_func = types[falloff_type] + + def function(rho_array): + zero_idxs = (rho_array == 0) + nonzero = (rho_array != 0) + result = np.empty_like(rho_array) + result[zero_idxs] = amplitude + result[nonzero] = amplitude * falloff_func(coefficient, rho_array[nonzero]) + negative = result <= 0 + result[negative] = 0.0 + + if clamp: + high = result >= rho_array + result[high] = rho_array[high] + return result + return function + # Standard functions which for some reasons are not in the math module def sign(x): if x < 0: diff --git a/utils/modules/sockets.py b/utils/modules/sockets.py new file mode 100644 index 0000000000..0d1e5ab157 --- /dev/null +++ b/utils/modules/sockets.py @@ -0,0 +1,47 @@ + +class SocketInfo(object): + def __init__(self, type, id, display_shape=None, idx=None): + self.type = type + self.id = id + self.idx = idx + self.display_shape = display_shape + +class SvDynamicSocketsHandler(object): + def __init__(self): + self.inputs_registry = dict() + self.outputs_registry = dict() + + def register_inputs(self, *sockets): + for idx, socket in enumerate(sockets): + socket.idx = idx + self.inputs_registry[socket.id] = socket + return sockets + + def register_outputs(self, *sockets): + for idx, socket in enumerate(sockets): + socket.idx = idx + self.outputs_registry[socket.id] = socket + return sockets + + def get_input_by_idx(self, idx): + for socket in self.inputs_registry.values(): + if socket.idx == idx: + return socket + raise Exception("unsupported input idx") + + def get_output_by_idx(self, idx): + for socket in self.outputs_registry.values(): + if socket.idx == idx: + return socket + raise Exception("unsupported output idx") + + def init_sockets(self, node): + for socket in self.inputs_registry.values(): + s = node.inputs.new(socket.type, socket.id) +# if socket.display_shape is not None: +# s.display_shape = socket.display_shape + for socket in self.outputs_registry.values(): + s = node.outputs.new(socket.type, socket.id) +# if socket.display_shape is not None: +# s.display_shape = socket.display_shape + diff --git a/utils/surface.py b/utils/surface.py new file mode 100644 index 0000000000..3a7010f209 --- /dev/null +++ b/utils/surface.py @@ -0,0 +1,1053 @@ +# This file is part of project Sverchok. It's copyrighted by the contributors +# recorded in the version control history of the file, available from +# its original location https://github.com/nortikin/sverchok/commit/master +# +# SPDX-License-Identifier: GPL3 +# License-Filename: LICENSE + +import numpy as np +from math import pi, cos, sin, atan, sqrt +from collections import defaultdict + +from mathutils import Matrix, Vector + +from sverchok.utils.logging import info, exception +from sverchok.utils.math import from_spherical +from sverchok.utils.geom import LineEquation + +def rotate_vector_around_vector_np(v, k, theta): + """ + Rotate vector v around vector k by theta angle. + input: v, k - np.array of shape (3,); theta - float, in radians. + output: np.array. + + This implements Rodrigues' formula: https://en.wikipedia.org/wiki/Rodrigues%27_rotation_formula + """ + if not isinstance(v, np.ndarray): + v = np.array(v) + if not isinstance(k, np.ndarray): + k = np.array(k) + if k.ndim == 1: + k = k[np.newaxis] + k = k / np.linalg.norm(k, axis=1) + + if isinstance(theta, np.ndarray): + ct, st = np.cos(theta)[np.newaxis].T, np.sin(theta)[np.newaxis].T + else: + ct, st = cos(theta), sin(theta) + + s1 = ct * v + s2 = st * np.cross(k, v) + p1 = 1.0 - ct + p2 = np.apply_along_axis(lambda v : k.dot(v), 1, v) + s3 = p1 * p2 * k + return s1 + s2 + s3 + +class SvSurface(object): + def __repr__(self): + if hasattr(self, '__description__'): + description = self.__description__ + else: + description = self.__class__.__name__ + return "<{} surface>".format(description) + + def evaluate(self, u, v): + raise Exception("not implemented!") + + def evaluate_array(self, us, vs): + raise Exception("not implemented!") + + def normal(self, u, v): + h = self.normal_delta + p = self.evaluate(u, v) + p_u = self.evaluate(u+h, v) + p_v = self.evaluate(u, v+h) + du = (p_u - p) / h + dv = (p_v - p) / h + normal = np.cross(du, dv) + n = np.linalg.norm(normal) + normal = normal / n + return normal + + def normal_array(self, us, vs): + surf_vertices = self.evaluate_array(us, vs) + u_plus = self.evaluate_array(us + self.normal_delta, vs) + v_plus = self.evaluate_array(us, vs + self.normal_delta) + du = u_plus - surf_vertices + dv = v_plus - surf_vertices + #self.info("Du: %s", du) + #self.info("Dv: %s", dv) + normal = np.cross(du, dv) + norm = np.linalg.norm(normal, axis=1)[np.newaxis].T + #if norm != 0: + normal = normal / norm + #self.info("Normals: %s", normal) + return normal + + def get_coord_mode(self): + return 'UV' + + @property + def has_input_matrix(self): + return False + + def get_input_matrix(self): + return None + + def get_input_orientation(self): + return None + + def get_u_min(self): + return 0.0 + + def get_u_max(self): + return 1.0 + + def get_v_min(self): + return 0.0 + + def get_v_max(self): + return 1.0 + + @property + def u_size(self): + m,M = self.get_u_min(), self.get_u_max() + return M - m + + @property + def v_size(self): + m,M = self.get_v_min(), self.get_v_max() + return M - m + +class SvSurfaceSubdomain(SvSurface): + def __init__(self, surface, u_bounds, v_bounds): + self.surface = surface + self.u_bounds = u_bounds + self.v_bounds = v_bounds + if hasattr(surface, "normal_delta"): + self.normal_delta = surface.normal_delta + else: + self.normal_delta = 0.001 + self.__description__ = "{}[{} .. {}][{} .. {}]".format(surface, u_bounds[0], u_bounds[1], v_bounds[0], v_bounds[1]) + + def evaluate(self, u, v): + return self.surface.evaluate(u, v) + + def evaluate_array(self, us, vs): + return self.surface.evaluate_array(us, vs) + + def normal(self, u, v): + return self.surface.normal(u, v) + + def normal_array(self, us, vs): + return self.surface.normal_array(us, vs) + + def get_u_min(self): + return self.u_bounds[0] + + def get_u_max(self): + return self.u_bounds[1] + + def get_v_min(self): + return self.v_bounds[0] + + def get_v_max(self): + return self.v_bounds[1] + +class SvPlane(SvSurface): + __description__ = "Plane" + + def __init__(self, point, vector1, vector2): + self.point = point + self.vector1 = vector1 + self.vector2 = vector2 + self._normal = np.cross(vector1, vector2) + n = np.linalg.norm(self._normal) + self._normal = self._normal / n + self.u_bounds = (0.0, 1.0) + self.v_bounds = (0.0, 1.0) + + def get_u_min(self): + return self.u_bounds[0] + + def get_u_max(self): + return self.u_bounds[1] + + def get_v_min(self): + return self.v_bounds[0] + + def get_v_max(self): + return self.v_bounds[1] + + @property + def u_size(self): + return self.u_bounds[1] - self.u_bounds[0] + + @property + def v_size(self): + return self.v_bounds[1] - self.v_bounds[0] + + def evaluate(self, u, v): + return self.point + u*self.vector1 + v*self.vector2 + + def evaluate_array(self, us, vs): + us = us[np.newaxis].T + vs = vs[np.newaxis].T + return self.point + us*self.vector1 + vs*self.vector2 + + def normal(self, u, v): + return self._normal + + def normal_array(self, us, vs): + normal = self.normal + n = np.linalg.norm(normal) + normal = normal / n + return np.tile(normal, len(us)) + +class SvEquirectSphere(SvSurface): + __description__ = "Equirectangular Sphere" + + def __init__(self, center, radius, theta1): + self.center = center + self.radius = radius + self.theta1 = theta1 + self.u_bounds = (0, radius * 2*pi * cos(theta1)) + self.v_bounds = (-radius * theta1, radius * (pi - theta1)) + + def get_u_min(self): + return self.u_bounds[0] + + def get_u_max(self): + return self.u_bounds[1] + + def get_v_min(self): + return self.v_bounds[0] + + def get_v_max(self): + return self.v_bounds[1] + + @property + def u_size(self): + return self.u_bounds[1] - self.u_bounds[0] + + @property + def v_size(self): + return self.v_bounds[1] - self.v_bounds[0] + + def evaluate(self, u, v): + rho = self.radius + phi = u / (rho * cos(self.theta1)) + theta = v / rho + self.theta1 + x, y, z = from_spherical(rho, phi, theta, mode="radians") + return np.array([x,y,z]) + self.center + + def evaluate_array(self, us, vs): + rho = self.radius + phis = us / (rho * cos(self.theta1)) + thetas = vs / rho + self.theta1 + xs = rho * np.sin(thetas) * np.cos(phis) + ys = rho * np.sin(thetas) * np.sin(phis) + zs = rho * np.cos(thetas) + return np.stack((xs, ys, zs)).T + self.center + + def normal(self, u, v): + rho = self.radius + phi = u / (rho * np.cos(self.theta1)) + theta = v / rho + self.theta1 + x, y, z = from_spherical(rho, phi, theta, mode="radians") + return np.array([x,y,z]) + + def normal_array(self, us, vs): + rho = self.radius + phis = us / (rho * cos(self.theta1)) + thetas = vs / rho + self.theta1 + xs = rho * np.sin(thetas) * np.cos(phis) + ys = rho * np.sin(thetas) * np.sin(phis) + zs = rho * np.cos(thetas) + return np.stack((xs, ys, zs)).T + +class SvLambertSphere(SvSurface): + __description__ = "Lambert Sphere" + + def __init__(self, center, radius): + self.center = center + self.radius = radius + self.u_bounds = (0, 2*pi) + self.v_bounds = (-1.0, 1.0) + + def get_u_min(self): + return self.u_bounds[0] + + def get_u_max(self): + return self.u_bounds[1] + + def get_v_min(self): + return self.v_bounds[0] + + def get_v_max(self): + return self.v_bounds[1] + + @property + def u_size(self): + return self.u_bounds[1] - self.u_bounds[0] + + @property + def v_size(self): + return self.v_bounds[1] - self.v_bounds[0] + + def evaluate(self, u, v): + rho = self.radius + phi = u + theta = np.arcsin(v) + x,y,z = from_spherical(rho, phi, theta, mode="radians") + return np.array([x,y,z]) + self.center + + def evaluate_array(self, us, vs): + rho = self.radius + phis = us + thetas = np.arcsin(vs) + pi/2 + xs = rho * np.sin(thetas) * np.cos(phis) + ys = rho * np.sin(thetas) * np.sin(phis) + zs = rho * np.cos(thetas) + return np.stack((xs, ys, zs)).T + self.center + + def normal(self, u, v): + rho = self.radius + phi = u + theta = np.arcsin(v) + pi/2 + x,y,z = from_spherical(rho, phi, theta, mode="radians") + return np.array([x,y,z]) + + def normal_array(self, us, vs): + rho = self.radius + phis = us + thetas = np.arcsin(vs) + pi/2 + xs = rho * np.sin(thetas) * np.cos(phis) + ys = rho * np.sin(thetas) * np.sin(phis) + zs = rho * np.cos(thetas) + return np.stack((xs, ys, zs)).T + +class SvGallSphere(SvSurface): + __description__ = "Gall Sphere" + + def __init__(self, center, radius): + self.center = center + self.radius = radius + self.u_bounds = (0, radius * 2*pi / sqrt(2)) + self.v_bounds = (- radius * (1 + sqrt(2)/2), radius * (1 + sqrt(2)/2)) + + def get_u_min(self): + return self.u_bounds[0] + + def get_u_max(self): + return self.u_bounds[1] + + def get_v_min(self): + return self.v_bounds[0] + + def get_v_max(self): + return self.v_bounds[1] + + @property + def u_size(self): + return self.u_bounds[1] - self.u_bounds[0] + + @property + def v_size(self): + return self.v_bounds[1] - self.v_bounds[0] + + def evaluate(self, u, v): + rho = self.radius + phi = u * sqrt(2) / rho + theta = 2 * atan(v / (rho * (1 + sqrt(2)/2))) + pi/2 + x,y,z = from_spherical(rho, phi, theta, mode="radians") + return np.array([x,y,z]) + self.center + + def evaluate_array(self, us, vs): + rho = self.radius + phis = us * sqrt(2) / rho + thetas = 2 * np.arctan(vs / (rho * (1 + sqrt(2)/2))) + pi/2 + xs = rho * np.sin(thetas) * np.cos(phis) + ys = rho * np.sin(thetas) * np.sin(phis) + zs = rho * np.cos(thetas) + return np.stack((xs, ys, zs)).T + self.center + + def normal(self, u, v): + rho = self.radius + phi = u * sqrt(2) / rho + theta = 2 * atan(v / (rho * (1 + sqrt(2)/2))) + pi/2 + x,y,z = from_spherical(rho, phi, theta, mode="radians") + return np.array([x,y,z]) + + def normal_array(self, us, vs): + rho = self.radius + phis = us * sqrt(2) / rho + thetas = 2 * np.arctan(vs / (rho * (1 + sqrt(2)/2))) + pi/2 + xs = rho * np.sin(thetas) * np.cos(phis) + ys = rho * np.sin(thetas) * np.sin(phis) + zs = rho * np.cos(thetas) + return np.stack((xs, ys, zs)).T + +class SvDefaultSphere(SvSurface): + __description__ = "Default Sphere" + + def __init__(self, center, radius): + self.center = center + self.radius = radius + self.u_bounds = (0, 2*pi) + self.v_bounds = (0, pi) + + def get_u_min(self): + return self.u_bounds[0] + + def get_u_max(self): + return self.u_bounds[1] + + def get_v_min(self): + return self.v_bounds[0] + + def get_v_max(self): + return self.v_bounds[1] + + def evaluate(self, u, v): + rho = self.radius + phi = u + theta = v + x,y,z = from_spherical(rho, phi, theta, mode="radians") + return np.array([x,y,z]) + self.center + + def evaluate_array(self, us, vs): + rho = self.radius + phis = us + thetas = vs + xs = rho * np.sin(thetas) * np.cos(phis) + ys = rho * np.sin(thetas) * np.sin(phis) + zs = rho * np.cos(thetas) + return np.stack((xs, ys, zs)).T + self.center + + def normal(self, u, v): + rho = self.radius + phi = u + theta = v + x,y,z = from_spherical(rho, phi, theta, mode="radians") + return np.array([x,y,z]) + + def normal_array(self, us, vs): + rho = self.radius + phis = us + thetas = vs + xs = rho * np.sin(thetas) * np.cos(phis) + ys = rho * np.sin(thetas) * np.sin(phis) + zs = rho * np.cos(thetas) + return np.stack((xs, ys, zs)).T + +class SvLambdaSurface(SvSurface): + __description__ = "Formula" + + def __init__(self, function): + self.function = function + self.u_bounds = (0.0, 1.0) + self.v_bounds = (0.0, 1.0) + self.normal_delta = 0.001 + + def get_u_min(self): + return self.u_bounds[0] + + def get_u_max(self): + return self.u_bounds[1] + + def get_v_min(self): + return self.v_bounds[0] + + def get_v_max(self): + return self.v_bounds[1] + + @property + def u_size(self): + return self.u_bounds[1] - self.u_bounds[0] + + @property + def v_size(self): + return self.v_bounds[1] - self.v_bounds[0] + + def evaluate(self, u, v): + return self.function(u, v) + + def evaluate_array(self, us, vs): + return np.vectorize(self.function, signature='(),()->(3)')(us, vs) + + def normal(self, u, v): + return self.normal_array(np.array([u]), np.array([v]))[0] + + def normal_array(self, us, vs): + surf_vertices = self.evaluate_array(us, vs) + u_plus = self.evaluate_array(us + self.normal_delta, vs) + v_plus = self.evaluate_array(us, vs + self.normal_delta) + du = u_plus - surf_vertices + dv = v_plus - surf_vertices + #self.info("Du: %s", du) + #self.info("Dv: %s", dv) + normal = np.cross(du, dv) + norm = np.linalg.norm(normal, axis=1)[np.newaxis].T + #if norm != 0: + normal = normal / norm + #self.info("Normals: %s", normal) + return normal + +class SvInterpolatingSurface(SvSurface): + __description__ = "Interpolating" + + def __init__(self, u_bounds, v_bounds, u_spline_constructor, v_splines): + self.v_splines = v_splines + self.u_spline_constructor = u_spline_constructor + self.u_bounds = u_bounds + self.v_bounds = v_bounds + + # Caches + # v -> Spline + self._u_splines = {} + # (u,v) -> vertex + self._eval_cache = {} + # (u,v) -> normal + self._normal_cache = {} + + @property + def u_size(self): + return self.u_bounds[1] - self.u_bounds[0] + #v = 0.0 + #verts = [spline.evaluate(v) for spline in self.v_splines] + #return self.get_u_spline(v, verts).u_size + + @property + def v_size(self): + return self.v_bounds[1] - self.v_bounds[0] + #return self.v_splines[0].v_size + + def get_u_spline(self, v, vertices): + """Get a spline along U direction for specified value of V coordinate""" + spline = self._u_splines.get(v, None) + if spline is not None: + return spline + else: + spline = self.u_spline_constructor(vertices) + self._u_splines[v] = spline + return spline + + def _evaluate(self, u, v): + spline_vertices = [] + for spline in self.v_splines: + v_min, v_max = spline.get_u_bounds() + vx = (v_max - v_min) * v + v_min + point = spline.evaluate(vx) + spline_vertices.append(point) + #spline_vertices = [spline.evaluate(v) for spline in self.v_splines] + u_spline = self.get_u_spline(v, spline_vertices) + result = u_spline.evaluate(u) + return result + + def evaluate(self, u, v): + result = self._eval_cache.get((u,v), None) + if result is not None: + return result + else: + result = self._evaluate(u, v) + self._eval_cache[(u,v)] = result + return result + + def evaluate_array(self, us, vs): + # FIXME: To be optimized! + normals = [self._evaluate(u, v) for u,v in zip(us, vs)] + return np.array(normals) + +# def evaluate_array(self, us, vs): +# result = np.empty((len(us), 3)) +# v_to_u = defaultdict(list) +# v_to_i = defaultdict(list) +# for i, (u, v) in enumerate(zip(us, vs)): +# v_to_u[v].append(u) +# v_to_i[v].append(i) +# for v, us_by_v in v_to_u.items(): +# is_by_v = v_to_i[v] +# spline_vertices = [spline.evaluate(v) for spline in self.v_splines] +# u_spline = self.get_u_spline(v, spline_vertices) +# points = u_spline.evaluate_array(np.array(us_by_v)) +# np.put(result, is_by_v, points) +# return result + + def _normal(self, u, v): + h = 0.001 + point = self.evaluate(u, v) + # we know this exists because it was filled in evaluate() + u_spline = self._u_splines[v] + u_tangent = u_spline.tangent(u) + point_v = self.evaluate(u, v+h) + dv = (point_v - point)/h + n = np.cross(u_tangent, dv) + norm = np.linalg.norm(n) + if norm != 0: + n = n / norm + return n + + def normal(self, u, v): + result = self._normal_cache.get((u,v), None) + if result is not None: + return result + else: + result = self._normal(u, v) + self._normal_cache[(u,v)] = result + return result + + def normal_array(self, us, vs): + # FIXME: To be optimized! + normals = [self._normal(u, v) for u,v in zip(us, vs)] + return np.array(normals) + +class SvDeformedByFieldSurface(SvSurface): + def __init__(self, surface, field, coefficient=1.0): + self.surface = surface + self.field = field + self.coefficient = coefficient + self.normal_delta = 0.001 + self.__description__ = "{}({})".format(field, surface) + + def get_coord_mode(self): + return self.surface.get_coord_mode() + + def get_u_min(self): + return self.surface.get_u_min() + + def get_u_max(self): + return self.surface.get_u_max() + + def get_v_min(self): + return self.surface.get_v_min() + + def get_v_max(self): + return self.surface.get_v_max() + + @property + def u_size(self): + return self.surface.u_size + + @property + def v_size(self): + return self.surface.v_size + + @property + def has_input_matrix(self): + return self.surface.has_input_matrix + + def get_input_matrix(self): + return self.surface.get_input_matrix() + + def evaluate(self, u, v): + p = self.surface.evaluate(u, v) + vec = self.field.evaluate(p) + return p + self.coefficient * vec + + def evaluate_array(self, us, vs): + ps = self.surface.evaluate_array(us, vs) + xs, ys, zs = ps[:,0], ps[:,1], ps[:,2] + vxs, vys, vzs = self.field.evaluate_grid(xs, ys, zs) + vecs = np.stack((vxs, vys, vzs)).T + return ps + self.coefficient * vecs + + def normal(self, u, v): + h = self.normal_delta + p = self.evaluate(u, v) + p_u = self.evaluate(u+h, v) + p_v = self.evaluate(u, v+h) + du = (p_u - p) / h + dv = (p_v - p) / h + normal = np.cross(du, dv) + n = np.linalg.norm(normal) + normal = normal / n + return normal + + def normal_array(self, us, vs): + surf_vertices = self.evaluate_array(us, vs) + u_plus = self.evaluate_array(us + self.normal_delta, vs) + v_plus = self.evaluate_array(us, vs + self.normal_delta) + du = u_plus - surf_vertices + dv = v_plus - surf_vertices + #self.info("Du: %s", du) + #self.info("Dv: %s", dv) + normal = np.cross(du, dv) + norm = np.linalg.norm(normal, axis=1)[np.newaxis].T + #if norm != 0: + normal = normal / norm + #self.info("Normals: %s", normal) + return normal + +class SvRevolutionSurface(SvSurface): + __description__ = "Revolution" + + def __init__(self, curve, point, direction): + self.curve = curve + self.point = point + self.direction = direction + self.normal_delta = 0.001 + self.v_bounds = (0.0, 2*pi) + + def evaluate(self, u, v): + point_on_curve = self.curve.evaluate(u) + dv = point_on_curve - self.point + return rotate_vector_around_vector_np(dv, self.direction, v) + + def evaluate_array(self, us, vs): + points_on_curve = self.curve.evaluate_array(us) + dvs = points_on_curve - self.point + return rotate_vector_around_vector_np(dvs, self.direction, vs) + + def get_u_min(self): + return self.curve.get_u_bounds()[0] + + def get_u_max(self): + return self.curve.get_u_bounds()[1] + + def get_v_min(self): + return self.v_bounds[0] + + def get_v_max(self): + return self.v_bounds[1] + +class SvExtrudeCurveVectorSurface(SvSurface): + def __init__(self, curve, vector): + self.curve = curve + self.vector = vector + self.normal_delta = 0.001 + self.__description__ = "Extrusion of {}".format(curve) + + def evaluate(self, u, v): + point_on_curve = self.curve.evaluate(u) + return point_on_curve + v * self.vector + + def evaluate_array(self, us, vs): + points_on_curve = self.curve.evaluate_array(us) + return points_on_curve + vs[np.newaxis].T * self.vector + + def get_u_min(self): + return self.curve.get_u_bounds()[0] + + def get_u_max(self): + return self.curve.get_u_bounds()[1] + + def get_v_min(self): + return 0.0 + + def get_v_max(self): + return 1.0 + + @property + def u_size(self): + m,M = self.curve.get_u_bounds() + return M - m + + @property + def v_size(self): + return 1.0 + +class SvExtrudeCurvePointSurface(SvSurface): + def __init__(self, curve, point): + self.curve = curve + self.point = point + self.normal_delta = 0.001 + self.__description__ = "Extrusion of {}".format(curve) + + def evaluate(self, u, v): + point_on_curve = self.curve.evaluate(u) + return (1.0 - v) * point_on_curve + v * self.point + + def evaluate_array(self, us, vs): + points_on_curve = self.curve.evaluate_array(us) + vs = vs[np.newaxis].T + return (1.0 - vs) * points_on_curve + vs * self.point + + def get_u_min(self): + return self.curve.get_u_bounds()[0] + + def get_u_max(self): + return self.curve.get_u_bounds()[1] + + def get_v_min(self): + return 0.0 + + def get_v_max(self): + return 1.0 + + @property + def u_size(self): + m,M = self.curve.get_u_bounds() + return M - m + + @property + def v_size(self): + return 1.0 + +class SvExtrudeCurveCurveSurface(SvSurface): + def __init__(self, u_curve, v_curve): + self.u_curve = u_curve + self.v_curve = v_curve + self.normal_delta = 0.001 + self.__description__ = "Extrusion of {}".format(u_curve) + + def evaluate(self, u, v): + u_point = self.u_curve.evaluate(u) + v_min, v_max = self.v_curve.get_u_bounds() + v0 = self.v_curve.evaluate(v_min) + v_point = self.v_curve.evaluate(v) + return u_point + (v_point - v0) + + def evaluate_array(self, us, vs): + u_points = self.u_curve.evaluate_array(us) + v_min, v_max = self.v_curve.get_u_bounds() + v0 = self.v_curve.evaluate(v_min) + v_points = self.v_curve.evaluate_array(vs) + return u_points + (v_points - v0) + + def get_u_min(self): + return self.u_curve.get_u_bounds()[0] + + def get_u_max(self): + return self.u_curve.get_u_bounds()[1] + + def get_v_min(self): + return self.v_curve.get_u_bounds()[0] + + def get_v_max(self): + return self.v_curve.get_u_bounds()[1] + + @property + def u_size(self): + m,M = self.u_curve.get_u_bounds() + return M - m + + @property + def v_size(self): + m,M = self.v_curve.get_u_bounds() + return M - m + +class SvExtrudeCurveFrenetSurface(SvSurface): + def __init__(self, profile, extrusion): + self.profile = profile + self.extrusion = extrusion + self.normal_delta = 0.001 + self.__description__ = "Extrusion of {}".format(profile) + + def evaluate(self, u, v): + return self.evaluate_array(np.array([u]), np.array([v]))[0] + + def evaluate_array(self, us, vs): + profile_points = self.profile.evaluate_array(us) + u_min, u_max = self.profile.get_u_bounds() + v_min, v_max = self.extrusion.get_u_bounds() + profile_start = self.extrusion.evaluate(u_min) + profile_vectors = profile_points # - profile_start + profile_vectors = np.transpose(profile_vectors[np.newaxis], axes=(1, 2, 0)) + extrusion_start = self.extrusion.evaluate(v_min) + extrusion_points = self.extrusion.evaluate_array(vs) + extrusion_vectors = extrusion_points - extrusion_start + frenet, _ , _ = self.extrusion.frame_array(vs) + profile_vectors = (frenet @ profile_vectors)[:,:,0] + return extrusion_vectors + profile_vectors + + def get_u_min(self): + return self.profile.get_u_bounds()[0] + + def get_u_max(self): + return self.profile.get_u_bounds()[1] + + def get_v_min(self): + return self.extrusion.get_u_bounds()[0] + + def get_v_max(self): + return self.extrusion.get_u_bounds()[1] + + @property + def u_size(self): + m,M = self.profile.get_u_bounds() + return M - m + + @property + def v_size(self): + m,M = self.extrusion.get_u_bounds() + return M - m + +class SvExtrudeCurveZeroTwistSurface(SvSurface): + def __init__(self, profile, extrusion, resolution): + self.profile = profile + self.extrusion = extrusion + self.normal_delta = 0.001 + self.extrusion.pre_calc_torsion_integral(resolution) + self.__description__ = "Extrusion of {}".format(profile) + + def evaluate(self, u, v): + return self.evaluate_array(np.array([u]), np.array([v]))[0] + + def evaluate_array(self, us, vs): + profile_points = self.profile.evaluate_array(us) + u_min, u_max = self.profile.get_u_bounds() + v_min, v_max = self.extrusion.get_u_bounds() + profile_start = self.extrusion.evaluate(u_min) + profile_vectors = profile_points # - profile_start + profile_vectors = np.transpose(profile_vectors[np.newaxis], axes=(1, 2, 0)) + extrusion_start = self.extrusion.evaluate(v_min) + extrusion_points = self.extrusion.evaluate_array(vs) + extrusion_vectors = extrusion_points - extrusion_start + + frenet, _ , _ = self.extrusion.frame_array(vs) + + angles = - self.extrusion.torsion_integral(vs) + n = len(us) + zeros = np.zeros((n,)) + ones = np.ones((n,)) + row1 = np.stack((np.cos(angles), np.sin(angles), zeros)).T # (n, 3) + row2 = np.stack((-np.sin(angles), np.cos(angles), zeros)).T # (n, 3) + row3 = np.stack((zeros, zeros, ones)).T # (n, 3) + rotation_matrices = np.dstack((row1, row2, row3)) + + profile_vectors = (frenet @ rotation_matrices @ profile_vectors)[:,:,0] + return extrusion_vectors + profile_vectors + + def get_u_min(self): + return self.profile.get_u_bounds()[0] + + def get_u_max(self): + return self.profile.get_u_bounds()[1] + + def get_v_min(self): + return self.extrusion.get_u_bounds()[0] + + def get_v_max(self): + return self.extrusion.get_u_bounds()[1] + +class SvCurveLerpSurface(SvSurface): + __description__ = "Lerp" + + def __init__(self, curve1, curve2): + self.curve1 = curve1 + self.curve2 = curve2 + self.normal_delta = 0.001 + self.v_bounds = (0.0, 1.0) + self.u_bounds = (0.0, 1.0) + self.c1_min, self.c1_max = curve1.get_u_bounds() + self.c2_min, self.c2_max = curve2.get_u_bounds() + + def evaluate(self, u, v): + return self.evaluate_array(np.array([u]), np.array([v]))[0] + + def evaluate_array(self, us, vs): + us1 = (self.c1_max - self.c1_min) * us + self.c1_min + us2 = (self.c2_max - self.c2_min) * us + self.c2_min + c1_points = self.curve1.evaluate_array(us1) + c2_points = self.curve2.evaluate_array(us2) + vs = vs[np.newaxis].T + points = (1.0 - vs)*c1_points + vs*c2_points + return points + + def get_u_min(self): + return self.u_bounds[0] + + def get_u_max(self): + return self.u_bounds[1] + + def get_v_min(self): + return self.v_bounds[0] + + def get_v_max(self): + return self.v_bounds[1] + + @property + def u_size(self): + return self.u_bounds[1] - self.u_bounds[0] + + @property + def v_size(self): + return self.v_bounds[1] - self.v_bounds[0] + +class SvSurfaceLerpSurface(SvSurface): + __description__ = "Lerp" + + def __init__(self, surface1, surface2, coefficient): + self.surface1 = surface1 + self.surface2 = surface2 + self.coefficient = coefficient + self.normal_delta = 0.001 + self.v_bounds = (0.0, 1.0) + self.u_bounds = (0.0, 1.0) + self.s1_u_min, self.s1_u_max = surface1.get_u_min(), surface1.get_u_max() + self.s1_v_min, self.s1_v_max = surface1.get_v_min(), surface1.get_v_max() + self.s2_u_min, self.s2_u_max = surface2.get_u_min(), surface2.get_u_max() + self.s2_v_min, self.s2_v_max = surface2.get_v_min(), surface2.get_v_max() + + def get_u_min(self): + return self.u_bounds[0] + + def get_u_max(self): + return self.u_bounds[1] + + def get_v_min(self): + return self.v_bounds[0] + + def get_v_max(self): + return self.v_bounds[1] + + @property + def u_size(self): + return self.u_bounds[1] - self.u_bounds[0] + + @property + def v_size(self): + return self.v_bounds[1] - self.v_bounds[0] + + def evaluate(self, u, v): + return self.evaluate_array(np.array([u]), np.array([v]))[0] + + def evaluate_array(self, us, vs): + us1 = (self.s1_u_max - self.s1_u_min) * us + self.s1_u_min + us2 = (self.s2_u_max - self.s2_u_min) * us + self.s2_u_min + vs1 = (self.s1_v_max - self.s1_v_min) * vs + self.s1_v_min + vs2 = (self.s2_v_max - self.s2_v_min) * vs + self.s2_v_min + s1_points = self.surface1.evaluate_array(us1, vs1) + s2_points = self.surface2.evaluate_array(us2, vs2) + k = self.coefficient + points = (1.0 - k) * s1_points + k * s2_points + return points + +class SvTaperSweepSurface(SvSurface): + __description__ = "Taper & Sweep" + + def __init__(self, profile, taper, point, direction): + self.profile = profile + self.taper = taper + self.direction = direction + self.point = point + self.line = LineEquation.from_direction_and_point(direction, point) + self.normal_delta = 0.001 + + def get_u_min(self): + return self.profile.get_u_bounds()[0] + + def get_u_max(self): + return self.profile.get_u_bounds()[1] + + def get_v_min(self): + return self.taper.get_u_bounds()[0] + + def get_v_max(self): + return self.taper.get_u_bounds()[1] + + def evaluate(self, u, v): + taper_point = self.taper.evaluate(v) + taper_projection = np.array( self.line.projection_of_point(taper_point) ) + scale = np.linalg.norm(taper_projection - taper_point) + profile_point = self.profile.evaluate(u) + return profile_point * scale + taper_projection + + def evaluate_array(self, us, vs): + taper_points = self.taper.evaluate_array(vs) + taper_projections = self.line.projection_of_points(taper_points) + scale = np.linalg.norm(taper_projections - taper_points, axis=1, keepdims=True) + profile_points = self.profile.evaluate_array(us) + return profile_points * scale + taper_projections +