Sverchok / SverchokRedux

A reimplementation of Sverchok
GNU General Public License v3.0
4 stars 2 forks source link

Simple properties in nodes #17

Closed ly29 closed 8 years ago

ly29 commented 8 years ago

Right now nodes are wonderfully simple. I would like to keep them that way, but at the same time I realize there are limits too how simple they can be. But as a general rule simplicity is a good starting point and then only adding complexity as needed.

Properties

First up are properties. It is easy enough to imagine something along the lines of the following for simple properties, like integer, float, bool and string.

The current:

import numpy as np

def linspace(start: float = 0.0, stop: float = 1.0, count: int = 10) -> [("linspace", np.ndarray)]:
    return np.linspace(start, stop, count)

linspace.label = "Linear space"
SvRxFunc = [linspace]

But if we look at the np.linspace documentation there are some additional parameters available, for now let us consider endpoint: bool which specifies if the generated linear space will be [start, ... ,stop] or [start, ..., stop-step] Like this.

>>> np.linspace(0, 10, 5, endpoint=True)
array([  0. ,   2.5,   5. ,   7.5,  10. ])
>>> np.linspace(0, 10, 5, endpoint=False)
array([ 0.,  2.,  4.,  6.,  8.])

For me this seems like perfect candidate for an input that does not need to be a socket.

import numpy as np

def linspace(start: float = 0.0, stop: float = 1.0, count: int = 10,
             endpoint: (bool, "property") = True) -> [("linspace", np.ndarray)]:
    return np.linspace(start, stop, count, endpoint=endpoint)

linspace.label = "Linear space"
SvRxFunc = [linspace]

From this a property could automatically be generated, as well as a draw_buttons that automatically draws the properties.

Resulting in class corresponding the following:

class SvRxLinspaceNode(...

     svrx_properties = [("svrx_endpoint", "Endpoint")]
     svrx_endpoint = BoolProperty(update=update_svrx, name="Endpoint", default=True)

     def draw_buttons(self, context, layout):
         for name, text in svrx_properties:
             layout.prop(self, name, text=text)

In the compile stage these could then be put into a kwargs dict and passed along to the function along these lines.

>>> kwargs = {"endpoint": True}
>>> np.linspace(0, 10, 5, **kwargs)
array([  0. ,   2.5,   5. ,   7.5,  10. ])

This has the, positive, side effect that all properties must be placed after the socket inputs, as well a giving and order for the drawing of them.

Additional parameters for which things to only draw in the draw_buttons_ext as well as passing along a dict for the property creation/drawing could be supported.

For more complex case supplying a custom draw_buttons could be supported.

Also, perhaps far out, making something along these lines makes the properties easily inspectable, it is easy enough to imagine a properties socket being made available and creating an additional node, as needed, for drawing the parameters there.

class SvRxLinspacePropertiesNode(...
     svrx_properties = [("svrx_endpoint", "Endpoint")]
     svrx_endpoint = BoolProperty(update=update_svrx, name="Endpoint", default=True)

     def draw_buttons(self, context, layout):
         for name, text in svrx_properties:
             layout.prop(self, name, text=text)    

The sole purpose of this node being to be able to control multiple linspace nodes and vary the arguments of the properties as needed. For linspace this might not the most useful thing but for other cases it could very well offer an extreme additional flexibility while keeping a simple default case. The parameter dicts would the be treated and matched as other parts of the data tree.

ly29 commented 8 years ago

The only real question here is the syntax, to make it sensible. For tomorrow.

ly29 commented 8 years ago

So perhaps the sane way is just to declare the bpy.props.FloatProperty directly.

Like so:

import numpy as np
from bpy.props import BoolProperty

def linspace(start: float = 0.0, stop: float = 1.0, count: int = 10,
             endpoint: BoolProperty() = True) -> [("linspace", np.ndarray)]:
    return np.linspace(start, stop, count, endpoint=endpoint)

What happens here:

>>> linspace.__annotations__["endpoint"]
(<built-in function BoolProperty>, {})

It creates a tuple with function and a keyword dict to pass along to blender when the class becomes registered as a proper blender class. This dict can be modified to include the name and default parameters which when know from introspection/annotations removing the need to auto supply them.

Where the name update and default can be auto filled.

ly29 commented 8 years ago

skarmavbild 2015-12-09 kl 10 11 27 Auto generated property in, now it just needs to get passed to the execution engine.

ly29 commented 8 years ago

skarmavbild 2015-12-09 kl 10 52 56

Working, also with multiple properties.

ly29 commented 8 years ago
import numpy as np
from bpy.props import BoolProperty, FloatProperty

def linspace(start: float = 0.0, stop: float = 1.0, count: int = 10,
             endpoint: BoolProperty() = True,
             test: FloatProperty(description="test", subtype="ANGLE") = 2.0
             ) -> [("linspace", np.ndarray)]:
    return np.linspace(start, stop, count, endpoint=endpoint)

linspace.label = "Linear space"

SvRxFunc = [linspace]
ly29 commented 8 years ago

@zeffii Is this sane?

zeffii commented 8 years ago

@ly29 I don't see why not. It's definitely still comprehensible.

I'm likely to declare nodes one param per line tho..I find it reads more pleasantly.

import numpy as np
from bpy.props import BoolProperty, FloatProperty

def linspace(
    start: float = 0.0,
    stop: float = 1.0,
    count: int = 10,
    endpoint: BoolProperty() = True,
    test: FloatProperty(description="test", subtype="ANGLE") = 2.0
    ) -> [("linspace", np.ndarray)]:

    return np.linspace(start, stop, count, endpoint=endpoint)

linspace.label = "Linear space"

SvRxFunc = [linspace]
zeffii commented 8 years ago

unless ofcourse the params fit on one line. (+/-100 chars)

ly29 commented 8 years ago

My only concern is the undocumented behavior of the property functions, but if they change a change to something like the following would be simple enough.

def linspace(start: float = 0.0,
             stop: float = 1.0,
             count: int = 10,
             endpoint: (BoolProperty, {}) = True,
             test: (FloatProperty, {description: "test", subtype: "ANGLE"}) = 2.0
             ) -> [("linspace", np.ndarray)]:
    return np.linspace(start, stop, count, endpoint=endpoint)
zeffii commented 8 years ago
         endpoint: (BoolProperty, {}) = True,
         test: (FloatProperty, {description: "test", subtype: "ANGLE"}) = 2.0

how is that supposed to work, are you doing preprocessing? The tuple notation?

ly29 commented 8 years ago

It that case the property would be created with func(dict) Right now this syntax works because bpy.props.FloatProperty etc. are returning (func, kwargs)

`>>> bpy.props.FloatProperty(name="test", default=10.0)
(<built-in function FloatProperty>, {'default': 10.0, 'name': 'test'})

Well, how it should work if things changes are problem for a later day. But there needs to be note about the expected behavior of the property functions.

zeffii commented 8 years ago

right - i follow now. I've seen that plenty of times in the REPL.

That part of the bpy api seems fairly cemented now, it's more likely that bpy gets new props than modifying existing ones to have a different signature.

ly29 commented 8 years ago

This went much quicker than I expected and the syntax ended up being nicer. Now onwards.

ly29 commented 8 years ago

Right now I store them in the node with a svrx_ prefix to avoid name collisions with the standard node properties.

The dict taken from the annotation is updated with the name and default taken from the inspection, also an update method is supplied.

        prop = annotations[name]
        prop[1].update({"name": name,
                        "default": parameter.default,
                        "update": execute_tree})
        properties["svrx_{}".format(name)] = prop
zeffii commented 8 years ago

Some props will need a custom update method, so they can do some action after or before the execute_tree is called.

ly29 commented 8 years ago

Yeah, good point. That function would have to chain the default. Hmm

zeffii commented 8 years ago

https://github.com/nortikin/sverchok/blob/master/nodes/basic_data/dupli_instances.py#L44-L53 for instance, because of the alternative update= https://github.com/nortikin/sverchok/blob/master/nodes/basic_data/dupli_instances.py#L59-L61

zeffii commented 8 years ago

I realize the execution model is different, but it might still be necessary to have that option..

ly29 commented 8 years ago

Yeah, also allowing get/set.

ly29 commented 8 years ago

Of course right now changing any property results in a total recompilation of everything, I would like to move this to a model where at least values of properties are updated somewhat more clever.

ly29 commented 8 years ago

Stopped the overriding, of user supplied data. Even though giving two different defaults should probably be considered an error I will leave that for now.

        func, kwargs = annotations[name]
        defaults = {"name": name,
                    "default": parameter.default,
                    "update": execute_tree}
        for key, value in defaults.items():
            if key not in kwargs:
                kwargs[key] = value
ly29 commented 8 years ago

if a property has ext=True it will be drawn in with draw_buttons_ext, instead of in the node. With this I close this, the node based property control I leave for later.