Skip to content

Commit bdf703c

Browse files
committed
Add built-in node arrange and documentation
1 parent dddf0d9 commit bdf703c

File tree

6 files changed

+153
-32
lines changed

6 files changed

+153
-32
lines changed

api/arrange.py

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import bpy
2+
import typing
3+
4+
def _arrange(node_tree, padding: typing.Tuple[float, float] = (50, 25)):
5+
# Organize the nodes into columns based on their links.
6+
columns: typing.List[typing.List[typing.Any]] = []
7+
def contains_link(node, column):
8+
return any(
9+
any(
10+
any(link.from_node == node for link in input.links)
11+
for input in n.inputs
12+
)
13+
for n in column
14+
)
15+
for node in reversed(node_tree.nodes):
16+
if (x := next(
17+
filter(
18+
lambda x: contains_link(node, x[1]),
19+
enumerate(columns)
20+
),
21+
None
22+
)) is not None:
23+
if x[0] > 0:
24+
columns[x[0] - 1].append(node)
25+
else:
26+
columns.insert(x[0], [node])
27+
else:
28+
if len(columns) == 0:
29+
columns.append([node])
30+
else:
31+
columns[len(columns) - 1].append(node)
32+
33+
# Arrange the columns, computing the size of the node manually so arrangement can be done without UI being visible.
34+
UI_SCALE = bpy.context.preferences.view.ui_scale
35+
NODE_HEADER_HEIGHT = 20
36+
NODE_LINK_HEIGHT = 28
37+
NODE_PROPERTY_HEIGHT = 28
38+
NODE_VECTOR_HEIGHT = 84
39+
x = 0
40+
for col in columns:
41+
largest_width = 0
42+
y = 0
43+
for node in col:
44+
node.update()
45+
input_count = len(list(filter(lambda i: i.enabled, node.inputs)))
46+
output_count = len(list(filter(lambda i: i.enabled, node.outputs)))
47+
parent_props = [prop.identifier for base in type(node).__bases__ for prop in base.bl_rna.properties]
48+
properties_count = len([prop for prop in node.bl_rna.properties if prop.identifier not in parent_props])
49+
unset_vector_count = len(list(filter(lambda i: i.enabled and i.type == 'VECTOR' and len(i.links) == 0, node.inputs)))
50+
node_height = (
51+
NODE_HEADER_HEIGHT \
52+
+ (output_count * NODE_LINK_HEIGHT) \
53+
+ (properties_count * NODE_PROPERTY_HEIGHT) \
54+
+ (input_count * NODE_LINK_HEIGHT) \
55+
+ (unset_vector_count * NODE_VECTOR_HEIGHT)
56+
) * UI_SCALE
57+
if node.width > largest_width:
58+
largest_width = node.width
59+
node.location = (x, y)
60+
y -= node_height + padding[1]
61+
x += largest_width + padding[0]

api/node_mapper.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,12 @@ class OutputsList(dict):
1515

1616
def build_node(node_type):
1717
def build(_primary_arg=None, **kwargs):
18-
for k, v in kwargs.items():
18+
for k, v in kwargs.copy().items():
1919
if isinstance(v, InputGroup):
2020
kwargs = { **kwargs, **v.__dict__ }
2121
del kwargs[k]
22+
if v is None:
23+
del kwargs[k]
2224
node = State.current_node_tree.nodes.new(node_type.__name__)
2325
if _primary_arg is not None:
2426
State.current_node_tree.links.new(_primary_arg._socket, node.inputs[0])
@@ -316,6 +318,7 @@ def tree(builder):
316318
Marks a function as a node tree.
317319
\"\"\"
318320
pass
321+
_SomeType = TypeVar('_SomeType', bound='Type')
319322
class Type:
320323
def __add__(self, other) -> Type: return self
321324
def __radd__(self, other) -> Type: return self
@@ -333,6 +336,11 @@ def __lt__(self, other) -> Type: return self
333336
def __le__(self, other) -> Type: return self
334337
def __gt__(self, other) -> Type: return self
335338
def __ge__(self, other) -> Type: return self
339+
def __invert__(self) -> Type: return self
340+
def __getitem__(
341+
self,
342+
subscript: _SomeType | slice | Tuple[_SomeType | slice, SampleMode]
343+
) -> Type: return self
336344
x = Type()
337345
y = Type()
338346
z = Type()

api/static/sample_mode.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import enum
2+
3+
class SampleMode(enum.IntEnum):
4+
INDEX = 0
5+
NEAREST_SURFACE = 1
6+
NEAREST = 2

api/tree.py

Lines changed: 5 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,12 @@
1010
from .static.attribute import *
1111
from .static.expression import *
1212
from .static.input_group import *
13+
from .static.sample_mode import *
14+
from .arrange import _arrange
1315

1416
def _as_iterable(x):
17+
if isinstance(x, Type):
18+
return [x,]
1519
try:
1620
return iter(x)
1721
except TypeError:
@@ -100,35 +104,7 @@ def validate_param(param):
100104
node_group.outputs.new(result.socket_type, 'Result')
101105
link = node_group.links.new(result._socket, group_output_node.inputs[i])
102106

103-
# Attempt to run the "Node Arrange" add-on on the tree.
104-
try:
105-
for area in bpy.context.screen.areas:
106-
for space in area.spaces:
107-
if space.type == 'NODE_EDITOR':
108-
space.node_tree = node_group
109-
with bpy.context.temp_override(area=area, space=space, space_data=space):
110-
ntree = node_group
111-
ntree.nodes[0].select = True
112-
ntree.nodes.active = ntree.nodes[0]
113-
n_groups = []
114-
for i in ntree.nodes:
115-
if i.type == 'GROUP':
116-
n_groups.append(i)
117-
118-
while n_groups:
119-
j = n_groups.pop(0)
120-
node_arrange.nodes_iterate(j.node_tree)
121-
for i in j.node_tree.nodes:
122-
if i.type == 'GROUP':
123-
n_groups.append(i)
124-
125-
node_arrange.nodes_iterate(ntree)
126-
127-
# arrange nodes + this center nodes together
128-
if bpy.context.scene.node_center:
129-
node_arrange.nodes_center(ntree)
130-
except:
131-
pass
107+
_arrange(node_group)
132108

133109
# Return a function that creates a NodeGroup node in the tree.
134110
# This lets @trees be used in other @trees via simple function calls.

api/types.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import bpy
22
from bpy.types import NodeSocketStandard
33
import nodeitems_utils
4+
import enum
45
from .state import State
6+
from .static.sample_mode import SampleMode
57
import geometry_script
68

79
def map_case_name(i):
@@ -178,6 +180,53 @@ def capture(self, value, **kwargs):
178180
def transfer(self, attribute, **kwargs):
179181
data_type = socket_type_to_data_type(attribute._socket.type)
180182
return self.transfer_attribute(data_type=data_type, attribute=attribute, **kwargs)
183+
184+
def __getitem__(self, subscript):
185+
if isinstance(subscript, tuple):
186+
accessor = subscript[0]
187+
args = subscript[1:]
188+
else:
189+
accessor = subscript
190+
args = []
191+
sample_mode = SampleMode.INDEX if len(args) < 1 else args[0]
192+
domain = 'POINT' if len(args) < 2 else (args[1].value if isinstance(args[1], enum.Enum) else args[1])
193+
sample_position = None
194+
sampling_index = None
195+
if isinstance(accessor, slice):
196+
data_type = socket_type_to_data_type(accessor.start._socket.type)
197+
value = accessor.start
198+
match sample_mode:
199+
case SampleMode.INDEX:
200+
sampling_index = accessor.stop
201+
case SampleMode.NEAREST_SURFACE:
202+
sample_position = accessor.stop
203+
case SampleMode.NEAREST:
204+
sample_position = accessor.stop
205+
if accessor.step is not None:
206+
domain = accessor.step.value if isinstance(accessor.step, enum.Enum) else accessor.step
207+
else:
208+
data_type = socket_type_to_data_type(accessor._socket.type)
209+
value = accessor
210+
match sample_mode:
211+
case SampleMode.INDEX:
212+
return self.sample_index(
213+
data_type=data_type,
214+
domain=domain,
215+
value=value,
216+
index=sampling_index or geometry_script.index()
217+
)
218+
case SampleMode.NEAREST_SURFACE:
219+
return self.sample_nearest_surface(
220+
data_type=data_type,
221+
value=value,
222+
sample_position=sample_position or geometry_script.position()
223+
)
224+
case SampleMode.NEAREST:
225+
return self.sample_index(
226+
data_type=data_type,
227+
value=value,
228+
index=self.sample_nearest(domain=domain, sample_position=sample_position or geometry_script.position())
229+
)
181230

182231
for standard_socket in list(filter(lambda x: 'NodeSocket' in x, dir(bpy.types))):
183232
name = standard_socket.replace('NodeSocket', '')

book/src/api/advanced-scripting/attributes.md

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Attributes
22

3-
An important concept in Geometry Nodes is attributes. Many trees capture attributes or transfer them from one geometry to another.
3+
An important concept in Geometry Nodes is attributes. Many trees capture attributes or transfer them from one domain to another.
44

55
When using these methods, the `data_type` argument must be correctly specified for the transfer to work as intended.
66

@@ -65,4 +65,25 @@ my_custom_attribute = Attribute(
6565
geometry = my_custom_attribute.store(geometry, 0.5)
6666
# Use the value by calling the attribute
6767
geometry = geometry.set_position(offset=my_custom_attribute())
68-
```
68+
```
69+
70+
## Attribute Sampling
71+
In Blender 3.4+, transfer attribute was replaced with a few separate nodes: *Sample Index*, *Sample Nearest*, and *Sample Nearest Surface*.
72+
73+
To avoid inputting data types and geometry manually, you can use the custom `Geometry` subscript.
74+
75+
The structure for these subscripts is:
76+
77+
```python
78+
geometry[value : index or sample position : domain, mode, domain]
79+
```
80+
81+
Only the value argument is required. Other arguments can be supplied as needed.
82+
83+
```python
84+
geometry[value]
85+
geometry[value : sample_position, SampleMode.NEAREST]
86+
geometry[value : index() + 1 : SampleIndex.Domain.EDGE]
87+
```
88+
89+
Try passing different arguments and see how the resulting nodes are created.

0 commit comments

Comments
 (0)