---
title: Node API Design
---

The design approach for interfacing with the nodes takes several aspects into consideration.

```{python}
from nodebpy import geometry as g
```


## Sockets
### Inputs
Input sockets are exposed in two different ways, they are positional arguments in the class `__init__` signature and are available behind the `inputs / i` accessor on the nodes.

```py
class SetPosition(BaseNode):
    def __init__(
        self,
        geometry: InputGeometry = None,
        selection: InputBoolean = True,
        position: InputVector = None,
        offset: InputVector = (0.0, 0.0, 0.0),
    ):
```

We can either pass in nodes / sockets / values into the constructor, or link them after construction.

The `g.Cube()` is used as a positional argument to `geometry`, while we explicitly state the offset with a keyword argument.
On the second line we scale `Position()` by `0.5` and then link that to the `position` input of `SetPosition`.
```{python}
with g.tree() as tree:
    sp = g.SetPosition(g.Cube(), offset=g.RandomValue() * 0.1)
    _ = (g.Position() * 0.5) >> sp.i.position

tree
```

### Outputs

Selection of outputs is done automatically to best match the data types of the inputs.
You can be specific with the output though, with outputs available behind the `outputs` / `o` accessor.

```{python}
with g.tree() as tree:
    time = g.SceneTime()

    _ = (
        g.Cube()
        >> g.SetPosition(offset=g.RandomValue(min=-1) * time.o.seconds)
        >> tree.outputs.geometry()
    )

tree
```

### Slicing Inputs and Outputs

You can use slicing to access individual or multiple components of input and output sockets.

```{python}
with g.tree() as tree:
    sep = g.SeparateXYZ(g.Position())
    comb = g.CombineXYZ(*sep.o)
    comb2 = g.CombineXYZ()

    sep.o[1] >> comb2.i[2]

tree
```

We can replicate part of a PCA analysis, getting the mean difference of the position field, scaling and combining into a matrix.
```{python}
with g.tree() as tree:
    pos = g.Position()
    diff = g.FieldAverage.point.vector(pos).o.mean - pos
    matrix = g.CombineMatrix()

    for i, axis1 in enumerate(diff):
        sep = g.FieldAverage.point.vector(diff * axis1)
        for j, axis2 in enumerate(sep.o.mean):
            axis2 >> matrix.i[int(i * 4 + j)]

tree
```


#### Vector Outputs

Some output attributes have convenience methods for simpler chaining.
Vector outputs can access the `x/y/z/` components quickly, which internally adds the `SeparateXYZ` required.
The same SeparateXYZ node is re-used across different outputs.

```{python}
with g.tree() as tree:
    pos = g.Position().o.position

    _ = g.SetPosition(g.Cube(), position=pos.x, offset=pos.y)

tree
```

#### Other Accessors

Similar methods also exist for `SocketColor`, `SocketMatrix`

##### Matrix

Matrix sockets have access to the `translation`, `rotation` and `scale` from the transform.

```{python}
with g.tree() as tree:
    mat = g.CombineMatrix().o.matrix
    mat.translation * 0.5
    mat.rotation >> g.RotateRotation()
    mat.scale + 0.5

tree
```


##### Color
Color sockets have `r` `g` `b` `a` properties.
```{python}
with g.tree() as tree:
    col = g.CombineColor().o.color
    col.r + 10
    col.g + 0.5
    col.b * 0.3
    col.a - 0.3
tree
```

## Enum Options

Many options aren't available as sockets. These are exposed on the node class itself.
The non-socket options are always keyword arguments, requiring them to be explicitly stated.

```py
class EvaluateAtIndex(BaseNode):
    def __init__(
        self,
        value: InputFloat
        | InputInteger
        | InputBoolean
        | InputVector
        | InputRotation
        | InputMatrix = None,
        index: InputInteger = 0,
        *,
        domain: _AttributeDomains = "POINT",
        data_type: _EvaluateAtIndexDataTypes = "FLOAT",
    ):
```

They are set during the class construction, but can also be set and changed afterwards.

```{python}
with g.tree() as tree:
    eai = g.EvaluateAtIndex(data_type="FLOAT_VECTOR")
    eai.data_type = "QUATERNION"
    eai.domain = "FACE"
```

## Class Methods

For nodes that have `mode`, `domain`, `data_type` and `operation` as potential enum values, convenience class methods are provided.

> The order that these methods will appear are: `mode` > `domain` > `data_type` > `operation`, but should only ever be 1 or 2 deep.
> ```py
> g.EvaluateAtIndex.face.vector()   # .domain.data_type
> g.Compare.float.less_than()       # .data_type.operation
> ```

Because sockets are the only positional arguments for the node constructors, enum values like `data_type` have to be specified as keyword arguments to the constructor.
All enum options are type-hinted with `Literal[]` so IDE auto-complete and type hinting will work, but the convenience class methods enable a cleaner way of writing the nodes.
For the example below both methods do work, but the second is cleaner to write and flows better with what the node is doing; 'On the edge domain, evaluate a float attribute'.

```{python}
with g.tree():
    # domain and data_type require kwargs
    eod1 = g.EvaluateOnDomain(domain="EDGE", data_type="FLOAT")

    # better IDE type-hinting and auto-complete
    eod2 = g.EvaluateOnDomain.edge.float()

assert eod1.data_type == eod2.data_type
assert eod1.domain == eod2.domain
```

A similar approach is taken for the `Compare` node, with the first being data type and then secondarily the comparison operation.

If the items being compared are nodes or sockets, regular boolean comparison with operators will also work.
All 3 different approaches have the same result.

```py
a = g.Integer(0)
b = g.Integer(2)

g.Compare(A=a, B=b, data_type="INT", operation="EQUAL")

g.Compare.integer.equal(a, b)

a == b
```

::: {.callout-warning}

# The code will be evaluated at runtime.


### Comparing Node Objects
In this instance `comp` will be a `g.Compare` node, set with the `operation="GREATER_THAN"` and `data_type="INT"`.

As this is a node within the node graph, the inputs have the potential to change during node tree evaluation meaning the result of the comparison could change. During playback on an animation the output will be `Cube` at frames <=50 and `Cone` when above that value.

```{python}
with g.tree():
    a = g.SceneTime().o.frame
    b = g.Integer(50)

    comp = a > b
    geo = g.Switch.geometry(comp, g.Cube(), g.Cone())

comp
```


### Comparing Python Objects
If the comparison was of just regular python values, then the result at runtime will just be boolean `True` and _not_ a `Compare` node. This means the value _won't_ change during node tree evaluation, and will only be evaluated a single time in node tree construction.

The result below will _always_ output a `Cone` geometry because the input will always be `True`.

```{python}
with g.tree():
    a = 0
    b = 0

    comp = a == b
    geo = g.Switch.geometry(comp, g.Cube(), g.Cone())

comp
```



:::
