File: geometry_nodes.py

package info (click to toggle)
blender 4.3.2%2Bdfsg-2
  • links: PTS, VCS
  • area: main
  • in suites: sid, trixie
  • size: 309,564 kB
  • sloc: cpp: 2,385,210; python: 330,236; ansic: 280,972; xml: 2,446; sh: 972; javascript: 317; makefile: 170
file content (374 lines) | stat: -rw-r--r-- 13,010 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
# SPDX-FileCopyrightText: 2020-2023 Blender Authors
#
# SPDX-License-Identifier: GPL-2.0-or-later

import bpy
from bpy.types import Operator
from bpy.props import BoolProperty

from bpy.app.translations import pgettext_data as data_


def add_empty_geometry_node_group(name):
    group = bpy.data.node_groups.new(name, 'GeometryNodeTree')

    group.interface.new_socket(data_("Geometry"), in_out='INPUT', socket_type='NodeSocketGeometry')
    input_node = group.nodes.new('NodeGroupInput')
    input_node.select = False
    input_node.location.x = -200 - input_node.width

    group.interface.new_socket(data_("Geometry"), in_out='OUTPUT', socket_type='NodeSocketGeometry')
    output_node = group.nodes.new('NodeGroupOutput')
    output_node.is_active_output = True
    output_node.select = False
    output_node.location.x = 200

    return group


def geometry_node_group_empty_new(name):
    group = add_empty_geometry_node_group(name)
    group.links.new(group.nodes[data_("Group Input")].outputs[0], group.nodes[data_("Group Output")].inputs[0])
    return group


def geometry_node_group_empty_modifier_new(name):
    group = geometry_node_group_empty_new(data_("Geometry Nodes"))
    group.is_modifier = True
    return group


def geometry_node_group_empty_tool_new(context):
    group = geometry_node_group_empty_new(data_("Tool"))
    # Node tools have fake users by default, otherwise Blender will delete them since they have no users.
    group.use_fake_user = True
    group.is_tool = True

    ob = context.object
    ob_type = ob.type if ob else 'MESH'
    if ob_type == 'CURVES':
        group.is_type_curve = True
    elif ob_type == 'POINTCLOUD':
        group.is_type_point_cloud = True
    else:
        group.is_type_mesh = True

    mode = ob.mode if ob else 'OBJECT'
    if mode in {'SCULPT', 'SCULPT_CURVES'}:
        group.is_mode_sculpt = True
    elif mode == 'EDIT':
        group.is_mode_edit = True
    else:
        group.is_mode_object = True

    return group


def geometry_modifier_poll(context):
    ob = context.object

    # Test object support for geometry node modifier
    if not ob or ob.type not in {'MESH', 'POINTCLOUD', 'VOLUME', 'CURVE', 'FONT', 'CURVES', 'GREASEPENCIL'}:
        return False

    return True


def get_context_modifier(context):
    # Context only has a "modifier" attribute in the modifier extra operators drop-down.
    modifier = getattr(context, "modifier", ...)
    if modifier is ...:
        ob = context.object
        if ob is None:
            return False
        modifier = ob.modifiers.active
    if modifier is None or modifier.type != 'NODES':
        return None
    return modifier


def edit_geometry_nodes_modifier_poll(context):
    return get_context_modifier(context) is not None


def socket_idname_to_attribute_type(idname):
    if idname.startswith("NodeSocketInt"):
        return 'INT'
    elif idname.startswith("NodeSocketColor"):
        return 'FLOAT_COLOR'
    elif idname.startswith("NodeSocketVector"):
        return 'FLOAT_VECTOR'
    elif idname.startswith("NodeSocketBool"):
        return 'BOOLEAN'
    elif idname.startswith("NodeSocketFloat"):
        return 'FLOAT'
    raise ValueError("Unsupported socket type")


def modifier_attribute_name_get(modifier, identifier):
    try:
        return modifier[identifier + "_attribute_name"]
    except KeyError:
        return None


def modifier_input_use_attribute(modifier, identifier):
    try:
        return modifier[identifier + "_use_attribute"] != 0
    except KeyError:
        return False


def get_socket_with_identifier(sockets, identifier):
    for socket in sockets:
        if socket.identifier == identifier:
            return socket
    return None


def get_enabled_socket_with_name(sockets, name):
    for socket in sockets:
        if socket.name == name and socket.enabled:
            return socket
    return None


def create_wrapper_group(operator, modifier, old_group):
    wrapper_name = old_group.name + ".wrapper"
    group = bpy.data.node_groups.new(wrapper_name, 'GeometryNodeTree')
    group.interface.new_socket(data_("Geometry"), in_out='OUTPUT', socket_type='NodeSocketGeometry')
    group.is_modifier = True

    first_geometry_input = next(
        (
            item for item in old_group.interface.items_tree if item.item_type == 'SOCKET' and
            item.in_out == 'INPUT' and
            item.bl_socket_idname == 'NodeSocketGeometry'
        ),
        None,
    )
    if first_geometry_input:
        group.interface.new_socket(data_("Geometry"), in_out='INPUT', socket_type='NodeSocketGeometry')
        group_input_node = group.nodes.new('NodeGroupInput')
        group_input_node.location.x = -200 - group_input_node.width
        group_input_node.select = False

    group_output_node = group.nodes.new('NodeGroupOutput')
    group_output_node.is_active_output = True
    group_output_node.location.x = 200
    group_output_node.select = False

    group_node = group.nodes.new("GeometryNodeGroup")
    group_node.node_tree = old_group
    group_node.update()

    # Copy default values for inputs and create named attribute input nodes.
    input_nodes = []
    for input_socket in old_group.interface.items_tree:
        if input_socket.item_type != 'SOCKET' or (input_socket.in_out not in {'INPUT', 'BOTH'}):
            continue
        identifier = input_socket.identifier
        group_node_input = get_socket_with_identifier(group_node.inputs, identifier)
        if modifier_input_use_attribute(modifier, identifier):
            input_node = group.nodes.new("GeometryNodeInputNamedAttribute")
            input_nodes.append(input_node)
            input_node.data_type = socket_idname_to_attribute_type(input_socket.bl_socket_idname)
            attribute_name = modifier_attribute_name_get(modifier, identifier)
            input_node.inputs["Name"].default_value = attribute_name
            output_socket = get_enabled_socket_with_name(input_node.outputs, "Attribute")
            group.links.new(output_socket, group_node_input)
        elif hasattr(input_socket, "default_value"):
            group_node_input.default_value = modifier[identifier]

    if first_geometry_input:
        group.links.new(
            group_input_node.outputs[0],
            get_socket_with_identifier(group_node.inputs, first_geometry_input.identifier),
        )

        # Adjust locations of named attribute input nodes and group input node to make some space.
        if input_nodes:
            for i, node in enumerate(input_nodes):
                node.location.x = -175
                node.location.y = i * -50
            group_input_node.location.x = -350

    # Connect outputs to store named attribute nodes to replace modifier attribute outputs.
    store_nodes = []
    first_geometry_output = None
    for output_socket in old_group.interface.items_tree:
        if output_socket.item_type != 'SOCKET' or (output_socket.in_out not in {'OUTPUT', 'BOTH'}):
            continue
        identifier = output_socket.identifier
        group_node_output = get_socket_with_identifier(group_node.outputs, identifier)
        attribute_name = modifier_attribute_name_get(modifier, identifier)
        if attribute_name:
            store_node = group.nodes.new("GeometryNodeStoreNamedAttribute")
            store_nodes.append(store_node)
            store_node.data_type = socket_idname_to_attribute_type(output_socket.bl_socket_idname)
            store_node.domain = output_socket.attribute_domain
            store_node.inputs["Name"].default_value = attribute_name
            input_socket = get_enabled_socket_with_name(store_node.inputs, "Value")
            group.links.new(group_node_output, input_socket)
        elif output_socket.bl_socket_idname == 'NodeSocketGeometry':
            if not first_geometry_output:
                first_geometry_output = group_node_output

    # Adjust locations of store named attribute nodes and move group output.
    # Note that the node group has its sockets names translated, while the built-in nodes don't.
    if store_nodes:
        for i, node in enumerate(store_nodes):
            node.location.x = (i + 1) * 175
            node.location.y = 0
        group_output_node.location.x = (len(store_nodes) + 1) * 175

        group.links.new(first_geometry_output, store_nodes[0].inputs["Geometry"])
        for i in range(len(store_nodes) - 1):
            group.links.new(store_nodes[i].outputs["Geometry"], store_nodes[i + 1].inputs["Geometry"])

        group.links.new(store_nodes[-1].outputs["Geometry"], group_output_node.inputs[data_("Geometry")])
    else:
        if not first_geometry_output:
            operator.report({'WARNING'}, "Node group must have a geometry output")
            return None
        group.links.new(first_geometry_output, group_output_node.inputs[data_("Geometry")])

    return group


class MoveModifierToNodes(Operator):
    """Move inputs and outputs from in the modifier to a new node group"""

    bl_idname = "object.geometry_nodes_move_to_nodes"
    bl_label = "Move to Nodes"
    bl_options = {'REGISTER', 'UNDO'}

    use_selected_objects: BoolProperty(
        name="Selected Objects",
        description="Affect all selected objects instead of just the active object",
    )

    @classmethod
    def poll(cls, context):
        return edit_geometry_nodes_modifier_poll(context)

    def invoke(self, context, event):
        if event.alt:
            self.use_selected_objects = True
        return self.execute(context)

    def execute(self, context):
        active_modifier = get_context_modifier(context)
        if not active_modifier:
            return {'CANCELLED'}
        modifier_name = active_modifier.name

        objects = []
        if self.use_selected_objects:
            objects = context.selected_editable_objects
        else:
            objects = [context.object]

        for ob in objects:
            modifier = ob.modifiers[modifier_name]
            if not modifier:
                continue
            old_group = modifier.node_group
            if not old_group:
                continue
            new_group = create_wrapper_group(self, modifier, old_group)
            if new_group:
                modifier.node_group = new_group

        return {'FINISHED'}


class NewGeometryNodesModifier(Operator):
    """Create a new modifier with a new geometry node group"""

    bl_idname = "node.new_geometry_nodes_modifier"
    bl_label = "New Geometry Node Modifier"
    bl_options = {'REGISTER', 'UNDO'}

    @classmethod
    def poll(cls, context):
        return geometry_modifier_poll(context)

    def execute(self, context):
        ob = context.object
        modifier = ob.modifiers.new(data_("GeometryNodes"), 'NODES')
        if not modifier:
            return {'CANCELLED'}

        group = geometry_node_group_empty_modifier_new(data_("Geometry Nodes"))
        modifier.node_group = group

        return {'FINISHED'}


class NewGeometryNodeTreeAssign(Operator):
    """Create a new geometry node group and assign it to the active modifier"""

    bl_idname = "node.new_geometry_node_group_assign"
    bl_label = "Assign New Geometry Node Group"
    bl_options = {'REGISTER', 'UNDO'}

    @classmethod
    def poll(cls, context):
        return geometry_modifier_poll(context)

    def execute(self, context):
        modifier = get_context_modifier(context)
        if not modifier:
            return {'CANCELLED'}
        group = geometry_node_group_empty_modifier_new(data_("Geometry Nodes"))
        modifier.node_group = group

        return {'FINISHED'}


class NewGeometryNodeGroupTool(Operator):
    """Create a new geometry node group for a tool"""
    bl_idname = "node.new_geometry_node_group_tool"
    bl_label = "New Geometry Node Tool Group"
    bl_options = {'REGISTER', 'UNDO'}

    @classmethod
    def poll(cls, context):
        space = context.space_data
        return space and space.type == 'NODE_EDITOR' and space.geometry_nodes_type == 'TOOL'

    def execute(self, context):
        group = geometry_node_group_empty_tool_new(context)
        context.space_data.geometry_nodes_tool_tree = group
        return {'FINISHED'}


class ZoneOperator:
    @classmethod
    def get_node(cls, context):
        node = context.active_node
        if node is None:
            return None
        if node.bl_idname == cls.output_node_type:
            return node
        if node.bl_idname == cls.input_node_type:
            return node.paired_output

    @classmethod
    def poll(cls, context):
        space = context.space_data
        # Needs active node editor and a tree.
        if not space or space.type != 'NODE_EDITOR' or not space.edit_tree or not space.edit_tree.is_editable:
            return False
        if cls.get_node(context) is None:
            return False
        return True


classes = (
    NewGeometryNodesModifier,
    NewGeometryNodeTreeAssign,
    NewGeometryNodeGroupTool,
    MoveModifierToNodes,
)