---
title: "Comparing the API"
subtitle: How does the API compare to other existing solutions?
---

Several other notable projects have also attempted interfacing with node trees via code. They mostly fit into two categories: either storing & retrieving node trees via code (`.json` or the `bpy` API), or authoring node trees with a custom API and syntax. `nodebpy` mostly fits into the latter category.

### Storing node trees as code

Converting node trees to Python API calls or `.json` gives a robust storage method, but this approach falls down in human authorability / readability. These projects are great for storage but less useful when wanting to write node trees from scratch:

- [NodeToPython](https://github.com/BrendanParmer/NodeToPython)
- [TreeClipper](https://github.com/Algebraic-UG/tree_clipper) (used by this project for running some tests and web rendering)

### Authoring node trees with code

Two previous projects have made similar approaches to authoring node trees. [`geometry-script`](https://github.com/carson-katri/geometry-script) also auto-generated most of its type hinting, code and docs. It uses the approach of method chaining with the `.` operator, but obfuscates some of the non-linear way of building node trees.

[`geonodes`](https://github.com/al1brn/geonodes) uses a similar context system for creating and authoring node trees, but doesn't expose each individual node as its own class the way `nodebpy` does.

I personally found both of their APIs to _not quite_ fit how I wanted to work, leading to the creation of `nodebpy`. In comparison, this project is also the only one distributed on PyPI and installable via `pip` for easier use in other projects.

The sections below reproduce examples from each project's own documentation side-by-side with the `nodebpy` equivalent.

## `geonodes`

### 'Hello World'

The hello world example from `geonodes` readme: https://github.com/al1brn/geonodes

::: {.panel-tabset}

#### `nodebpy`

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

with g.tree("Hello World") as tree:
    height = tree.inputs.float("Height", 3.0)
    omega = tree.inputs.float("Omega", 2.0)

    with g.Frame("Computing the wave"):
        pos = g.Position().o.position
        distance = g.Math.square_root(pos.x**2 + pos.y**2)
        z = height * g.Math.sine(distance * omega) / distance

    with g.Frame("Point offset & smooth"):
        mesh = (
            g.Grid(20, 20, 200, 200)
            >> g.SetPosition(offset=g.CombineXYZ(z=z))
            >> g.SetShadeSmooth.face()
        )

    mesh >> tree.outputs.geometry("Mesh")
```

#### `geonodes`

```python
from geonodes import *

with GeoNodes("Hello World"):
    height = 3
    omega  = 2

    grid = Mesh.Grid(vertices_x=200, vertices_y=200, size_x=20, size_y=20)
    with Layout("Computing the wave"):
        pos = nd.position
        distance = gnmath.sqrt(pos.x**2 + pos.y**2)
        z = height*gnmath.sin(distance*omega)/distance

    with Layout("Point offset and smoothness"):
        grid.offset = (0, 0, z)
        grid.faces.smooth = True

    grid.out()
```

:::

```{python}
#| echo: false
tree
```

## `geometry-script`

### README Demo

::: {.panel-tabset}

#### `nodebpy`

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

with g.tree("Repeat Grid") as tree:
    geometry = tree.inputs.geometry("Geometry")
    width = tree.inputs.integer("Width")
    height = tree.inputs.integer("Height")

    (
        g.Grid(width, height, vertices_x=width, vertices_y=height)
        >> g.MeshToPoints()
        >> g.InstanceOnPoints(instance=geometry)
        >> tree.outputs.geometry("Instances")
    )
```

#### `geometry-script`

```py
from geometry_script import *

@tree("Repeat Grid")
def repeat_grid(geometry: Geometry, width: Int, height: Int):
    g = grid(
        size_x=width, size_y=height,
        vertices_x=width, vertices_y=height
    ).mesh.mesh_to_points()
    return g.instance_on_points(instance=geometry)
```


:::

```{python}
# | echo: false
tree
```

### Primitive Shapes

::: {.panel-tabset}

#### `nodebpy`

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

with g.tree("Primitive Shapes") as tree:
    join =  g.JoinGeometry([g.Cube(), g.UVSphere(), g.Cylinder()])
    join >> tree.outputs.geometry("Result")
```

#### `geometry-script`

```{python}
#| eval: false
@tree("Primitive Shapes")
def primitive_shapes():
    yield cube()
    yield uv_sphere()
    yield cylinder().mesh
```

:::


```{python}
# | echo: false
tree
```


### Voxelise

Example script found [here](https://carson-katri.github.io/geometry-script/tutorials/voxelize.html#final-script).

::: {.panel-tabset}

#### `nodebpy`

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

with g.tree("Voxelise") as tree:
    geo = tree.inputs.geometry("Geometry")
    resolution = tree.inputs.float("Resolution", 0.2)

    (
        geo
        >> g.MeshToVolume(interior_band_width=resolution)
        >> g.DistributePointsInVolume(mode="Grid", spacing=resolution)
        >> g.InstanceOnPoints(instance=g.Cube(size=resolution))
        >> tree.outputs.geometry("Result")
    )
```

#### `geometry-script`

```{python}
#| eval: false
from geometry_script import *

@tree("Voxelize")
def voxelize(geometry: Geometry, resolution: Float = 0.2):
    return geometry.mesh_to_volume(
        interior_band_width=resolution,
        fill_volume=False
    ).distribute_points_in_volume(
        mode=DistributePointsInVolume.Mode.DENSITY_GRID,
        spacing=resolution
    ).instance_on_points(
        instance=cube(size=resolution)
    )

```

:::


```{python}
# | echo: false
tree
```

### City Builder

Example script found [here](https://carson-katri.github.io/geometry-script/tutorials/city-builder.html#final-script).

::: {.panel-tabset}

#### `nodebpy`

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

with g.tree("Voxelise") as tree:
    geo = tree.inputs.geometry("Geometry")
    seed = tree.inputs.integer("Seed")
    road_width = tree.inputs.float("Road Width", 0.25)
    size_x = tree.inputs.float("Size X", 5.0)
    size_y = tree.inputs.float("Size Y", 5.0)
    density = tree.inputs.float("Density", 10.0)
    building_size_min = tree.inputs.vector("Building Size Min", (0.1, 0.1, 0.2))
    building_size_max = tree.inputs.vector("Building Size Max", (0.3, 0.3, 1.0))

    curve_mesh = geo >> g.CurveToMesh(
        profile_curve=g.CurveLine(
            start=g.CombineXYZ(x=road_width * -0.5),
            end=g.CombineXYZ(x=road_width * 0.5),
        ),
    )

    building_points = g.Grid(size_x, size_y) >> g.DistributePointsOnFaces(
        density=density, seed=seed
    )

    road_points = geo >> g.CurveToPoints(mode="EVALUATED")
    building_points = g.DeleteGeometry.point(
        building_points,
        selection=g.GeometryProximity(
            target_element="POINTS",
            target=road_points,
            source_position=g.Position().o.position,
        ).o.distance
        < road_width,
    )

    buildings = building_points >> g.InstanceOnPoints(
        instance=g.Cube() >> g.TransformGeometry(translation=(0, 0, 0.5)),
        scale=g.RandomValue.vector(
            min=building_size_min, max=building_size_max, seed=seed
        ),
    )

    g.JoinGeometry((curve_mesh, buildings)) >> tree.outputs.geometry("Result")
```

#### `geometry-script`

```{python}
# | eval: false
from geometry_script import *

@tree("City Builder")
def city_builder(
    geometry: Geometry,
    seed: Int,
    road_width: Float = 0.25,
    size_x: Float = 5,
    size_y: Float = 5,
    density: Float = 10,
    building_size_min: Vector = (0.1, 0.1, 0.2),
    building_size_max: Vector = (0.3, 0.3, 1),
):
    # Road mesh
    yield geometry.curve_to_mesh(
        profile_curve=curve_line(
            start=combine_xyz(x=road_width * -0.5), end=combine_xyz(x=road_width * 0.5)
        )
    )
    # Building points
    building_points = (
        grid(size_x=size_x, size_y=size_y)
        .distribute_points_on_faces(density=density, seed=seed)
        .points
    )
    road_points = geometry.curve_to_points(mode=CurveToPoints.Mode.EVALUATED).points
    # Delete points within the curve
    building_points = building_points.delete_geometry(
        domain=DeleteGeometry.Domain.POINT,
        selection=geometry_proximity(
            target_element=GeometryProximity.TargetElement.POINTS,
            target=road_points,
            source_position=position(),
        ).distance
        < road_width,
    )
    # Building instances
    yield building_points.instance_on_points(
        instance=cube().transform(translation=(0, 0, 0.5)),
        scale=random_value(
            data_type=RandomValue.DataType.FLOAT_VECTOR,
            min=building_size_min,
            max=building_size_max,
            seed=seed,
        ),
    )
```

:::


```{python}
# | echo: false
tree
```
