OverviewFilesCommitsIssuesDocsReleasesPeers

hardops commits / 58c47e5

fix: Blender 5.2 moved geometry nodes modifier inputs off IDProperties
Blender 5.2 dropped IDProperty support on NodesModifier entirely. Inputs now live on a typed RNA struct: <= 5.1 5.2 mod["Socket_1"] mod.properties.inputs.Socket_1.value mod["Socket_1_use_attribute"] .type (VALUE / ATTRIBUTE / LAYER) mod["Socket_1_attribute_name"] .attribute_name Every subscript of a nodes modifier now raises "TypeError: this type doesn't support IDProperties", which took out get_auto_smooth() and with it every boolean operator. Adds utility/geometry_nodes/compat.py: a ModifierInputs dict-like view plus attribute and UI-path helpers, feature-detected via 'properties' in bpy.types.NodesModifier.bl_rna.properties (false on 4.2-5.1, true from 5.2), so both storage layouts work from one call site. Call sites moved onto it: the NodeModifier wrappers (smooth by angle, boolean), RadArray, to_thread, ever_scroll, and the helper/bevel panels. Panels can no longer address an input with a string prop path, so prop_draw_string is replaced by prop_draw_data returning a (data, property) pair for layout.prop. HOPS_Boolean's Operation and Solver are menu sockets, which 5.2 stores as enum identifiers ('Difference') rather than the old integer index, so those two wrappers map both ways. Also fixes a hard crash: the helper panel passed a socket NAME to object.geometry_nodes_input_attribute_toggle, which resolves inputs by IDENTIFIER. On 5.0/5.1 that was a silent no-op; on 5.2 it segfaults Blender (reproduced on stock Blender with no addons loaded). Verified headless on 5.2.0: auto smooth get/set round trips, hops.bool_difference returns FINISHED on both CLASSIC and NODES workflows and evaluates to real cut geometry, all five operations and three solvers round trip, radial array builds its copies, to_thread new/backup/restore/get_modifier work including vector sockets, and attribute inputs and outputs toggle and set. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
58c47e53278dbab5d04676c03f9b43be7fee95ea
by plankatron <fleeky@gmail.com> · 2026-08-07T20:08:36.000Z

Changed files (10) +352 -146

operators/editmode/to_thread.py +43 -36
……@@ -9,6 +9,7 @@ from ...ui_framework.master import Master
99from ...ui_framework.utils.mods_list import get_mods_list
1010from ...utility import addon, method_handler
1111from ...utility.base_modal_controls import Base_Modal_Controls
12from ...utility.geometry_nodes import compat
1213from ...utils.cursor_warp import mouse_warp
1314from ...utils.modal_frame_drawing import draw_modal_frame
1415from ...utils.toggle_view3d_panels import collapse_3D_view_panels
……@@ -45,8 +46,10 @@ class HOPS_OT_ToThread(bpy.types.Operator):
4546 else:
4647 self.modifier, self.table = self.new_modifier(object)
4748 self.backup = None
49
50 self.inputs = compat.ModifierInputs(self.modifier)
4851 for key, value in parameters.items():
49 self.modifier[self.table[key]] = value
52 self.inputs[self.table[key]] = value
5053
5154 # States
5255 self.mouse_mode = 'DEPTH'
……@@ -135,12 +138,12 @@ class HOPS_OT_ToThread(bpy.types.Operator):
135138
136139 # Direction
137140 if event.shift:
138 self.modifier[self.table['Direction']] += self.base_controls.scroll * tau / self.modifier[self.table['Resolution']]
141 self.inputs[self.table['Direction']] += self.base_controls.scroll * tau / self.inputs[self.table['Resolution']]
139142 self.refresh_modifier(self.modifier)
140143
141144 # Turns
142145 else:
143 self.modifier[self.table['Turns']] = clamp(self.modifier[self.table['Turns']] + self.base_controls.scroll, 1, 100)
146 self.inputs[self.table['Turns']] = clamp(self.inputs[self.table['Turns']] + self.base_controls.scroll, 1, 100)
144147 self.refresh_modifier(self.modifier)
145148
146149
……@@ -153,13 +156,13 @@ class HOPS_OT_ToThread(bpy.types.Operator):
153156
154157 if abs(self.snap_buffer) > self.snap_break:
155158 increment = 0.01 if event.shift else 0.05
156 value = snap(self.modifier[self.table['Depth']] + copysign(increment, self.snap_buffer), increment)
157 self.modifier[self.table['Depth']] = clamp(value, -100.0, 100.0)
159 value = snap(self.inputs[self.table['Depth']] + copysign(increment, self.snap_buffer), increment)
160 self.inputs[self.table['Depth']] = clamp(value, -100.0, 100.0)
158161 self.snap_buffer = 0.0
159162
160163 else:
161 value = self.modifier[self.table['Depth']] + self.base_controls.mouse
162 self.modifier[self.table['Depth']] = clamp(value, -100.0, 100.0)
164 value = self.inputs[self.table['Depth']] + self.base_controls.mouse
165 self.inputs[self.table['Depth']] = clamp(value, -100.0, 100.0)
163166
164167 self.refresh_modifier(self.modifier)
165168
……@@ -170,13 +173,13 @@ class HOPS_OT_ToThread(bpy.types.Operator):
170173
171174 if abs(self.snap_buffer) > self.snap_break:
172175 increment = 0.01 if event.shift else 0.05
173 value = snap(self.modifier[self.table['Root']] + copysign(increment, self.snap_buffer), increment)
174 self.modifier[self.table['Root']] = clamp(value, 0.0, 1.0)
176 value = snap(self.inputs[self.table['Root']] + copysign(increment, self.snap_buffer), increment)
177 self.inputs[self.table['Root']] = clamp(value, 0.0, 1.0)
175178 self.snap_buffer = 0.0
176179
177180 else:
178 value = self.modifier[self.table['Root']] + self.base_controls.mouse
179 self.modifier[self.table['Root']] = clamp(value, 0.0, 1.0)
181 value = self.inputs[self.table['Root']] + self.base_controls.mouse
182 self.inputs[self.table['Root']] = clamp(value, 0.0, 1.0)
180183
181184 self.refresh_modifier(self.modifier)
182185
……@@ -187,13 +190,13 @@ class HOPS_OT_ToThread(bpy.types.Operator):
187190
188191 if abs(self.snap_buffer) > self.snap_break:
189192 increment = 0.01 if event.shift else 0.05
190 value = snap(self.modifier[self.table['Crest']] + copysign(increment, self.snap_buffer), increment)
191 self.modifier[self.table['Crest']] = clamp(value, 0.0, 1.0)
193 value = snap(self.inputs[self.table['Crest']] + copysign(increment, self.snap_buffer), increment)
194 self.inputs[self.table['Crest']] = clamp(value, 0.0, 1.0)
192195 self.snap_buffer = 0.0
193196
194197 else:
195 value = self.modifier[self.table['Crest']] + self.base_controls.mouse
196 self.modifier[self.table['Crest']] = clamp(value, 0.0, 1.0)
198 value = self.inputs[self.table['Crest']] + self.base_controls.mouse
199 self.inputs[self.table['Crest']] = clamp(value, 0.0, 1.0)
197200
198201 self.refresh_modifier(self.modifier)
199202
……@@ -204,13 +207,13 @@ class HOPS_OT_ToThread(bpy.types.Operator):
204207
205208 if abs(self.snap_buffer) > self.snap_break:
206209 increment = radians(1.0) if event.shift else radians(5.0)
207 value = snap(self.modifier[self.table['Taper']] + copysign(increment, self.snap_buffer), increment)
208 self.modifier[self.table['Taper']] = clamp(value, radians(15.0), radians(180.0))
210 value = snap(self.inputs[self.table['Taper']] + copysign(increment, self.snap_buffer), increment)
211 self.inputs[self.table['Taper']] = clamp(value, radians(15.0), radians(180.0))
209212 self.snap_buffer = 0.0
210213
211214 else:
212 value = self.modifier[self.table['Taper']] + self.base_controls.mouse
213 self.modifier[self.table['Taper']] = clamp(value, radians(15.0), radians(180.0))
215 value = self.inputs[self.table['Taper']] + self.base_controls.mouse
216 self.inputs[self.table['Taper']] = clamp(value, radians(15.0), radians(180.0))
214217
215218 self.refresh_modifier(self.modifier)
216219
……@@ -223,20 +226,20 @@ class HOPS_OT_ToThread(bpy.types.Operator):
223226 # Main
224227 win_list = []
225228 if addon.preference().ui.Hops_modal_fast_ui_loc_options != 1:
226 win_list.append(f'{self.modifier[self.table["Turns"]]}')
227 win_list.append(f'{self.modifier[self.table["Depth"]]:.3f}')
228 win_list.append(f'{self.modifier[self.table["Root"]]:.3f}')
229 win_list.append(f'{self.modifier[self.table["Crest"]]:.3f}')
230 win_list.append(f'{degrees(self.modifier[self.table["Taper"]]):.1f}°')
231 win_list.append(f'{degrees(self.modifier[self.table["Direction"]]):.1f}°')
229 win_list.append(f'{self.inputs[self.table["Turns"]]}')
230 win_list.append(f'{self.inputs[self.table["Depth"]]:.3f}')
231 win_list.append(f'{self.inputs[self.table["Root"]]:.3f}')
232 win_list.append(f'{self.inputs[self.table["Crest"]]:.3f}')
233 win_list.append(f'{degrees(self.inputs[self.table["Taper"]]):.1f}°')
234 win_list.append(f'{degrees(self.inputs[self.table["Direction"]]):.1f}°')
232235 else:
233236 win_list.append(self.modifier.name)
234 win_list.append(f'Turns: {self.modifier[self.table["Turns"]]}')
235 win_list.append(f'Depth: {self.modifier[self.table["Depth"]]:.3f}')
236 win_list.append(f'Root: {self.modifier[self.table["Root"]]:.3f}')
237 win_list.append(f'Crest: {self.modifier[self.table["Crest"]]:.3f}')
238 win_list.append(f'Taper: {degrees(self.modifier[self.table["Taper"]]):.1f}°')
239 win_list.append(f'Direction: {degrees(self.modifier[self.table["Direction"]]):.1f}°')
237 win_list.append(f'Turns: {self.inputs[self.table["Turns"]]}')
238 win_list.append(f'Depth: {self.inputs[self.table["Depth"]]:.3f}')
239 win_list.append(f'Root: {self.inputs[self.table["Root"]]:.3f}')
240 win_list.append(f'Crest: {self.inputs[self.table["Crest"]]:.3f}')
241 win_list.append(f'Taper: {degrees(self.inputs[self.table["Taper"]]):.1f}°')
242 win_list.append(f'Direction: {degrees(self.inputs[self.table["Direction"]]):.1f}°')
240243
241244 # Help
242245 help_items = {}
……@@ -457,12 +460,13 @@ class HOPS_OT_ToThread(bpy.types.Operator):
457460 continue
458461
459462 # Vertex group
460 group = object.vertex_groups.find(modifier[table['Selection']])
463 inputs = compat.ModifierInputs(modifier)
464 group = object.vertex_groups.find(inputs[table['Selection']])
461465 if all(group in groups for groups in groups_per_vertex):
462466 return modifier, table
463467
464468 # Attribute
465 layer = bm.verts.layers.float.get(modifier[table['Selection']])
469 layer = bm.verts.layers.float.get(inputs[table['Selection']])
466470 if layer and all(vert[layer] for vert in verts):
467471 return modifier, table
468472
……@@ -480,7 +484,7 @@ class HOPS_OT_ToThread(bpy.types.Operator):
480484 modifier: bpy.types.NodesModifier = object.modifiers.new(group.name, 'NODES')
481485 object.modifiers.move(len(object.modifiers) - 1, 0)
482486 modifier.node_group, table = to_thread_nodes()
483 modifier[table['Selection']] = group.name
487 compat.ModifierInputs(modifier)[table['Selection']] = group.name
484488
485489 try:
486490 modifier.panels[0].is_open = False
……@@ -492,9 +496,10 @@ class HOPS_OT_ToThread(bpy.types.Operator):
492496
493497 def backup_modifier(self, modifier: bpy.types.NodesModifier, table: dict) -> dict:
494498 backup = {}
499 inputs = compat.ModifierInputs(modifier)
495500
496501 for key in REQUIRED_SOCKETS:
497 value = modifier[table[key]]
502 value = inputs[table[key]]
498503
499504 # Copy vector properties
500505 if hasattr(value, 'to_list'):
……@@ -506,8 +511,10 @@ class HOPS_OT_ToThread(bpy.types.Operator):
506511
507512
508513 def restore_modifier(self, modifier: bpy.types.NodesModifier, table: dict, backup: dict):
514 inputs = compat.ModifierInputs(modifier)
515
509516 for key, value in backup.items():
510 modifier[table[key]] = value
517 inputs[table[key]] = value
511518
512519 self.refresh_modifier(modifier)
513520
operators/modals/ever_scroll/__init__.py +1 -1
……@@ -170,7 +170,7 @@ def get_node_graph_objects(mod):
170170 link = obj_input.links[0]
171171 if link.from_node.type != 'GROUP_INPUT' or link.from_socket.type != 'OBJECT': continue
172172
173 val = mod[link.from_socket.identifier]
173 val = geometry_nodes.compat.ModifierInputs(mod)[link.from_socket.identifier]
174174 if val and val.type == 'MESH':
175175 objects.append(val)
176176
operators/modals/radial_array_nodes.py +26 -24
……@@ -6,6 +6,7 @@ from ... utility import addon, method_handler, operator_override, context_copy
66from ...ui_framework.master import Master
77from ...ui_framework.utils.mods_list import get_mods_list
88from ...utility.base_modal_controls import Base_Modal_Controls
9from ...utility.geometry_nodes import compat
910
1011# Cursor Warp imports
1112from ...utils.toggle_view3d_panels import collapse_3D_view_panels
……@@ -155,23 +156,23 @@ class RadArray():
155156
156157 @property
157158 def count(self):
158 return self.modifier[self.input_map[1]]
159 return self.inputs[self.input_map[1]]
159160
160161 @count.setter
161162 def count(self, value):
162163 if value < 1:
163164 value = 1
164165
165 self.modifier[self.input_map[1]] = value
166 self.inputs[self.input_map[1]] = value
166167 self.update()
167168
168169 @property
169170 def axis(self):
170 return self.modifier[self.input_map[2]]
171 return self.inputs[self.input_map[2]]
171172
172173 @axis.setter
173174 def axis(self, value):
174 prop = self.modifier[self.input_map[2]]
175 prop = self.inputs[self.input_map[2]]
175176 for i in range(3):
176177 val = value[i]
177178 if val > 1.0: val = 1.0
……@@ -183,7 +184,7 @@ class RadArray():
183184
184185 @property
185186 def rotation(self):
186 return self.modifier[self.input_map[3]]
187 return self.inputs[self.input_map[3]]
187188
188189 @rotation.setter
189190 def rotation(self, value):
……@@ -193,52 +194,52 @@ class RadArray():
193194 elif value > 360.0:
194195 val = 360.0
195196
196 self.modifier[self.input_map[3]] = val
197 self.inputs[self.input_map[3]] = val
197198 self.update()
198199
199200 @property
200201 def offset(self):
201 return self.modifier[self.input_map[4]]
202 return self.inputs[self.input_map[4]]
202203
203204 @offset.setter
204205 def offset(self, value):
205 self.modifier[self.input_map[4]] = value
206 self.inputs[self.input_map[4]] = value
206207 self.update()
207208
208209 @property
209210 def apply_scale(self):
210 return self.modifier[self.input_map[5]]
211 return self.inputs[self.input_map[5]]
211212
212213 @apply_scale.setter
213214 def apply_scale(self, value):
214 self.modifier[self.input_map[5]] = value
215 self.inputs[self.input_map[5]] = value
215216 self.update()
216217
217218 @property
218219 def center_pivot(self):
219 return self.modifier[self.input_map[6]]
220 return self.inputs[self.input_map[6]]
220221
221222 @center_pivot.setter
222223 def center_pivot(self, value):
223 self.modifier[self.input_map[6]] = value
224 self.inputs[self.input_map[6]] = value
224225 self.update()
225226
226227 @property
227228 def full_circle(self):
228 return self.modifier[self.input_map[7]]
229 return self.inputs[self.input_map[7]]
229230
230231 @full_circle.setter
231232 def full_circle(self, value):
232 self.modifier[self.input_map[7]] = value
233 self.inputs[self.input_map[7]] = value
233234 self.update()
234235
235236 @property
236237 def pivot_offset(self):
237 return self.modifier[self.input_map[8]]
238 return self.inputs[self.input_map[8]]
238239
239240 @pivot_offset.setter
240241 def pivot_offset(self, value):
241 input = self.modifier[self.input_map[8]]
242 input = self.inputs[self.input_map[8]]
242243
243244 input[0] = value[0]
244245 input[1] = value[1]
……@@ -247,11 +248,11 @@ class RadArray():
247248
248249 @property
249250 def rotation_offset(self):
250 return self.modifier[self.input_map[9]]
251 return self.inputs[self.input_map[9]]
251252
252253 @rotation_offset.setter
253254 def rotation_offset(self, value):
254 input = self.modifier[self.input_map[9]]
255 input = self.inputs[self.input_map[9]]
255256
256257 input[0] = value[0]
257258 input[1] = value[1]
……@@ -260,11 +261,11 @@ class RadArray():
260261
261262 @property
262263 def self(self):
263 return self.modifier[self.input_map[10]]
264 return self.inputs[self.input_map[10]]
264265
265266 @self.setter
266267 def self(self, value):
267 self.modifier[self.input_map[10]] = value
268 self.inputs[self.input_map[10]] = value
268269 self.update()
269270
270271 def __init__(self, modifier) -> None:
……@@ -275,6 +276,7 @@ class RadArray():
275276 self.input_map = [input.identifier for input in modifier.node_group.inputs]
276277
277278 self.modifier = modifier
279 self.inputs = compat.ModifierInputs(modifier)
278280 self.st_offset = self.offset
279281 self.index = self.start_index = modifier.id_data.modifiers.find(modifier.name)
280282
……@@ -302,19 +304,19 @@ class RadArray():
302304
303305 for input in self.modifier.node_group.inputs[1:]:
304306 if input.type in {'RGBA', 'VECTOR'}:
305 self.backup.append((input.identifier, self.modifier[input.identifier][:], True))
307 self.backup.append((input.identifier, self.inputs[input.identifier][:], True))
306308 else:
307 self.backup.append((input.identifier, self.modifier[input.identifier], False))
309 self.backup.append((input.identifier, self.inputs[input.identifier], False))
308310
309311 def restore_backup(self):
310312 for name, value, iterable in self.backup:
311313 if iterable:
312 prop = self.modifier[name]
314 prop = self.inputs[name]
313315 for i, value in enumerate(value):
314316 prop[i] = value
315317
316318 else:
317 self.modifier[name] = value
319 self.inputs[name] = value
318320
319321 self.update()
320322
ui/bevel_helper.py +4 -4
……@@ -108,7 +108,7 @@ class HOPS_OT_bevel_helper(bpy.types.Operator):
108108
109109 row = split.row()
110110 row.alignment = 'LEFT'
111 prop_string = geometry_nodes.NodeBoolean.prop_draw_string
111 prop_data = geometry_nodes.NodeBoolean.prop_draw_data
112112
113113 wrapper = geometry_nodes.NodeBoolean.new(mod)
114114
……@@ -134,15 +134,15 @@ class HOPS_OT_bevel_helper(bpy.types.Operator):
134134 swapoperator = row.operator('hops.bevel_helper_bool_swap', text='', icon_value=icon, emboss=False)
135135 swapoperator.obj_name = obj.name
136136 swapoperator.mod_name = mod.name
137 row.prop(mod, prop_string(mod, 'name'), text='', emboss=False)
137 row.prop(*prop_data(mod, 'name'), text='', emboss=False)
138138 row.separator()
139 row.prop(mod, prop_string(mod, 'show_viewport'), text='', emboss=False)
139 row.prop(*prop_data(mod, 'show_viewport'), text='', emboss=False)
140140
141141 if bpy.app.version > (2, 90, 0):
142142 split = split.row(align=True).split(factor=0.3)
143143 row = split.row(align=True)
144144 row.alignment = 'LEFT'
145 row.prop(mod, prop_string(mod, 'solver'), text='', emboss=True)
145 row.prop(*prop_data(mod, 'solver'), text='', emboss=True)
146146
147147 row = split.row(align=True)
148148 row.alignment = 'RIGHT'
ui/hops_helper/mods_data.py +28 -16
……@@ -1,6 +1,7 @@
11import bpy
22from bpy.types import Panel
33from ... utility import addon, version
4from ... utility.geometry_nodes import compat
45
56mods_dic = {
67 'ARMATURE': 'MOD_ARMATURE',
……@@ -813,32 +814,35 @@ class DATA_PT_modifiers(ModifierButtonsPanel, Panel):
813814 for input in inputs:
814815 if getattr(input, _attr) == _type: continue
815816
816 id = f'["{input.identifier}"]'
817 use_attr_id = f'{input.identifier}_use_attribute'
817 value_data, value_prop = compat.value_prop(md, input.identifier)
818 if not value_data: continue
818819
819820 row = layout.row(align=True)
820821 row.label(text=input.name)
821822
822 if use_attr_id in md:
823 if compat.supports_attribute(md, input.identifier):
823824
824825 op = row.operator("object.geometry_nodes_input_attribute_toggle", text='', icon='SPREADSHEET')
825826 if not use_interface:
826827 op.prop_path = f"[\"{input.identifier}_use_attribute\"]"
827828 else:
828 op.input_name=input.name
829 # the operator resolves the socket by identifier, passing the name
830 # is a no-op up to 5.1 and crashes 5.2
831 op.input_name=input.identifier
829832 op.modifier_name = md.name
830833
831 if md[use_attr_id]:
834 if compat.get_use_attribute(md, input.identifier):
832835 HOPS_MT_helper_node_attr_search.__class__.modifier_name = md.name
833 HOPS_MT_helper_node_attr_search.__class__.attribute_name = f'{input.identifier}_attribute_name'
836 HOPS_MT_helper_node_attr_search.__class__.attribute_name = input.identifier
837 HOPS_MT_helper_node_attr_search.__class__.is_output = False
834838
835839 row.menu(HOPS_MT_helper_node_attr_search.bl_idname, text='', icon='DOWNARROW_HLT')
836 row.prop(md, f'["{input.identifier}_attribute_name"]', text='')
840 row.prop(*compat.attribute_name_prop(md, input.identifier), text='')
837841 else:
838 row.prop(md, id, text='')
842 row.prop(value_data, value_prop, text='')
839843
840844 else:
841 row.prop(md, id, text='')
845 row.prop(value_data, value_prop, text='')
842846
843847 layout.separator()
844848
……@@ -856,18 +860,19 @@ class DATA_PT_modifiers(ModifierButtonsPanel, Panel):
856860 outputs = [s for s in md.node_group.interface.items_tree if s.item_type == 'SOCKET' and s.in_out == 'OUTPUT']
857861
858862 for output in outputs:
859 id_name = f'{output.identifier}_attribute_name'
863 attr_data, attr_prop = compat.output_attribute_name_prop(md, output.identifier)
860864
861 if id_name not in md: continue
865 if not attr_data: continue
862866
863867 row = layout.row(align=True)
864868 row.label(text=output.name)
865869
866870 HOPS_MT_helper_node_attr_search.__class__.modifier_name = md.name
867 HOPS_MT_helper_node_attr_search.__class__.attribute_name = id_name
871 HOPS_MT_helper_node_attr_search.__class__.attribute_name = output.identifier
872 HOPS_MT_helper_node_attr_search.__class__.is_output = True
868873 row.menu(HOPS_MT_helper_node_attr_search.bl_idname, text='', icon='DOWNARROW_HLT')
869874
870 row.prop(md, f'["{id_name}"]', text='')
875 row.prop(attr_data, attr_prop, text='')
871876
872877 def OCEAN(self, layout, _ob, md):
873878 if not bpy.app.build_options.mod_oceansim:
……@@ -1902,7 +1907,8 @@ class HOPS_MT_helper_node_attr_search(bpy.types.Menu):
19021907 bl_idname = "HOPS_MT_helper_attr_search"
19031908
19041909 modifier_name: str
1905 attribute_name: str
1910 attribute_name: str # socket identifier
1911 is_output: bool
19061912
19071913 def draw(self, context):
19081914 layout = self.layout
……@@ -1931,6 +1937,7 @@ class HOPS_MT_helper_node_attr_search(bpy.types.Menu):
19311937 op = layout.operator(HOPS_OT_helper_node_attr_set.bl_idname, text=attribute)
19321938 op.modifier = self.__class__.modifier_name
19331939 op.attribute = self.__class__.attribute_name
1940 op.is_output = getattr(self.__class__, 'is_output', False)
19341941 op.value = attribute
19351942
19361943class HOPS_OT_helper_node_attr_set(bpy.types.Operator):
……@@ -1938,14 +1945,19 @@ class HOPS_OT_helper_node_attr_set(bpy.types.Operator):
19381945 bl_idname = 'hops.helper_node_attr_set'
19391946
19401947 modifier: bpy.props.StringProperty()
1941 attribute: bpy.props.StringProperty()
1948 attribute: bpy.props.StringProperty() # socket identifier
19421949 value: bpy.props.StringProperty()
1950 is_output: bpy.props.BoolProperty()
19431951
19441952 def execute(self, context):
19451953 obj = bpy.context.object
19461954 mod = obj.modifiers[self.modifier]
19471955
1948 mod[self.attribute] = self.value
1956 if self.is_output:
1957 compat.set_output_attribute_name(mod, self.attribute, self.value)
1958
1959 else:
1960 compat.set_attribute_name(mod, self.attribute, self.value)
19491961
19501962 for area in context.screen.areas:
19511963 if area.type == 'PROPERTIES':
utility/geometry_nodes/__init__.py +1 -0
……@@ -1,3 +1,4 @@
1from . import compat
12from . node_modifier import NodeModifier
23from . smooth_by_angle import SmoothByAngle
34from .node_boolean import NodeBoolean
utility/geometry_nodes/compat.py +159 -0
……@@ -0,0 +1,159 @@
1import bpy
2
3
4'''Bridge for the geometry nodes modifier input storage change in Blender 5.2.\n
5Up to 5.1 inputs live in the modifier's IDProperties, keyed by socket identifier:\n
6 modifier['Socket_1']\n
7 modifier['Socket_1_use_attribute']\n
8 modifier['Socket_1_attribute_name']\n
95.2 dropped IDProperty support on NodesModifier entirely and moved inputs to typed RNA:\n
10 modifier.properties.inputs.Socket_1.value\n
11 modifier.properties.inputs.Socket_1.type # VALUE / ATTRIBUTE / LAYER\n
12 modifier.properties.inputs.Socket_1.attribute_name\n
13Subscripting a modifier on 5.2 raises TypeError: this type doesn't support IDProperties.
14'''
15
16RNA_INPUTS = 'properties' in bpy.types.NodesModifier.bl_rna.properties
17
18
19def socket(modifier, identifier):
20 '''RNA input struct for identifier, or None. 5.2+ only'''
21
22 return getattr(modifier.properties.inputs, identifier, None)
23
24
25class ModifierInputs:
26 '''Dict-like view of a geometry nodes modifier's inputs, keyed by socket identifier'''
27
28 __slots__ = ('modifier',)
29
30 def __init__(self, modifier):
31 self.modifier = modifier
32
33 def __contains__(self, identifier):
34 if not RNA_INPUTS:
35 return identifier in self.modifier
36
37 return hasattr(self.modifier.properties.inputs, identifier)
38
39 def __getitem__(self, identifier):
40 if not RNA_INPUTS:
41 return self.modifier[identifier]
42
43 input = socket(self.modifier, identifier)
44 if input is None:
45 raise KeyError(identifier)
46
47 try:
48 return input.value
49
50 except AttributeError: # geometry sockets carry no value
51 raise KeyError(identifier)
52
53 def __setitem__(self, identifier, value):
54 if not RNA_INPUTS:
55 self.modifier[identifier] = value
56 return
57
58 input = socket(self.modifier, identifier)
59 if input is None:
60 raise KeyError(identifier)
61
62 input.value = value
63
64 def get(self, identifier, default=None):
65 try:
66 return self[identifier]
67
68 except KeyError:
69 return default
70
71
72def supports_attribute(modifier, identifier) -> bool:
73 '''True if the input can be driven by an attribute'''
74
75 if not RNA_INPUTS:
76 return f'{identifier}_use_attribute' in modifier
77
78 input = socket(modifier, identifier)
79
80 return bool(input) and hasattr(input, 'attribute_name')
81
82
83def get_use_attribute(modifier, identifier) -> bool:
84 if not RNA_INPUTS:
85 return bool(modifier.get(f'{identifier}_use_attribute', False))
86
87 input = socket(modifier, identifier)
88
89 return bool(input) and input.type != 'VALUE'
90
91
92def get_attribute_name(modifier, identifier) -> str:
93 if not RNA_INPUTS:
94 return modifier.get(f'{identifier}_attribute_name', '')
95
96 input = socket(modifier, identifier)
97 if input is None:
98 return ''
99
100 return getattr(input, 'attribute_name', '')
101
102
103def set_attribute_name(modifier, identifier, name):
104 if not RNA_INPUTS:
105 modifier[f'{identifier}_attribute_name'] = name
106 return
107
108 input = socket(modifier, identifier)
109 if input is None:
110 return
111
112 input.attribute_name = name
113
114
115def value_prop(modifier, identifier):
116 '''(data, property) pair for drawing the input's value with layout.prop'''
117
118 if not RNA_INPUTS:
119 return modifier, f'["{identifier}"]'
120
121 return socket(modifier, identifier), 'value'
122
123
124def attribute_name_prop(modifier, identifier):
125 '''(data, property) pair for drawing the input's attribute name with layout.prop'''
126
127 if not RNA_INPUTS:
128 return modifier, f'["{identifier}_attribute_name"]'
129
130 return socket(modifier, identifier), 'attribute_name'
131
132
133def output_attribute_name_prop(modifier, identifier):
134 '''(data, property) pair for drawing an output's attribute name, or (None, '') if absent'''
135
136 if not RNA_INPUTS:
137 id_name = f'{identifier}_attribute_name'
138 if id_name not in modifier:
139 return None, ''
140
141 return modifier, f'["{id_name}"]'
142
143 output = getattr(modifier.properties.outputs, identifier, None)
144 if output is None or not hasattr(output, 'attribute_name'):
145 return None, ''
146
147 return output, 'attribute_name'
148
149
150def set_output_attribute_name(modifier, identifier, name):
151 if not RNA_INPUTS:
152 modifier[f'{identifier}_attribute_name'] = name
153 return
154
155 output = getattr(modifier.properties.outputs, identifier, None)
156 if output is None:
157 return
158
159 output.attribute_name = name
utility/geometry_nodes/node_boolean.py +69 -54
……@@ -1,5 +1,6 @@
11import bpy, pathlib
22from .node_modifier import NodeModifier
3from . import compat
34
45
56NODES_FOLDER = 'HOPS_Nodes'
……@@ -7,6 +8,11 @@ INPUTS = {'Tag Inset Slice Face Value', 'Clean Attribute 4', 'Tag Faces Name', '
78OPERATION_INPUT = {0: 'INTERSECT', 'INTERSECT': 0, 1: 'UNION', 'UNION': 1, 2: 'DIFFERENCE', 'DIFFERENCE': 2, 3: 'INSET', 'INSET': 3, 4: 'KNIFE', 'KNIFE': 4}
89SOLVER_INPUT = [itm.identifier for itm in bpy.types.BooleanModifier.bl_rna.properties['solver'].enum_items] # FLOAT/FAST, EXACT, MANIFOLD
910
11# 5.2 stores menu sockets as their enum identifier instead of an index
12OPERATION_MENU = {'INTERSECT': 'Intersect', 'UNION': 'Union', 'DIFFERENCE': 'Difference', 'INSET': 'Inset', 'KNIFE': 'Knife'}
13OPERATION_MENU.update({menu: hops for hops, menu in OPERATION_MENU.items()})
14SOLVER_MENU = ['Float', 'Exact', 'Manifold'] # ordered as SOLVER_INPUT
15
1016class NodeBoolean(NodeModifier):
1117 default_name = 'HOPS_Boolean'
1218 asset_name = 'HOPS_Boolean'
……@@ -15,247 +21,256 @@ class NodeBoolean(NodeModifier):
1521 # input wrappers
1622 @property
1723 def input_geometry(self):
18 return self.modifier[self.input_map["Input Geometry"]]
24 return self.inputs[self.input_map["Input Geometry"]]
1925
2026 @input_geometry.setter
2127 def input_geometry(self, val):
22 self.modifier[self.input_map["Input Geometry"]] = val
28 self.inputs[self.input_map["Input Geometry"]] = val
2329 self.update_tag()
2430
2531 @property
2632 def operation(self):
2733 '''DIFFERNCE, UNION, INTERSECT, INSET, KNIFE'''
28 return OPERATION_INPUT[self.modifier[self.input_map["Operation"]]]
34 value = self.inputs[self.input_map["Operation"]]
35
36 if compat.RNA_INPUTS: return OPERATION_MENU[value]
37
38 return OPERATION_INPUT[value]
2939
3040 @operation.setter
3141 def operation(self, val):
32 self.modifier[self.input_map["Operation"]] = OPERATION_INPUT[val]
42 self.inputs[self.input_map["Operation"]] = OPERATION_MENU[val] if compat.RNA_INPUTS else OPERATION_INPUT[val]
3343 self.update_tag()
3444
3545 @property
3646 def object(self):
37 return self.modifier[self.input_map["Object"]]
47 return self.inputs[self.input_map["Object"]]
3848
3949 @object.setter
4050 def object(self, val):
41 self.modifier[self.input_map["Object"]] = val
51 self.inputs[self.input_map["Object"]] = val
4252 self.update_tag()
4353
4454 @property
4555 def solver(self):
4656 '''FLOAT, EXACT, MANIFOLD'''
47 return SOLVER_INPUT[self.modifier[self.input_map["Solver"]]]
57 value = self.inputs[self.input_map["Solver"]]
58
59 if compat.RNA_INPUTS: return SOLVER_INPUT[SOLVER_MENU.index(value)]
60
61 return SOLVER_INPUT[value]
4862
4963 @solver.setter
5064 def solver(self, val):
51 self.modifier[self.input_map["Solver"]] = SOLVER_INPUT.index(val)
65 index = SOLVER_INPUT.index(val)
66 self.inputs[self.input_map["Solver"]] = SOLVER_MENU[index] if compat.RNA_INPUTS else index
5267 self.update_tag()
5368
5469 @property
5570 def inset_thickness(self):
56 return self.modifier[self.input_map["Inset Thickness"]]
71 return self.inputs[self.input_map["Inset Thickness"]]
5772
5873 @inset_thickness.setter
5974 def inset_thickness(self, val):
60 self.modifier[self.input_map["Inset Thickness"]] = float(val)
75 self.inputs[self.input_map["Inset Thickness"]] = float(val)
6176 self.update_tag()
6277
6378 @property
6479 def stitch_distance(self):
65 return self.modifier[self.input_map["Stitch Distance"]]
80 return self.inputs[self.input_map["Stitch Distance"]]
6681
6782 @stitch_distance.setter
6883 def stitch_distance(self, val):
69 self.modifier[self.input_map["Stitch Distance"]] = float(val)
84 self.inputs[self.input_map["Stitch Distance"]] = float(val)
7085 self.update_tag()
7186
7287 @property
7388 def non_manifold(self):
74 return self.modifier[self.input_map["Non Manifold"]]
89 return self.inputs[self.input_map["Non Manifold"]]
7590
7691 @non_manifold.setter
7792 def non_manifold(self, val):
78 self.modifier[self.input_map["Non Manifold"]] = bool(val)
93 self.inputs[self.input_map["Non Manifold"]] = bool(val)
7994 self.update_tag()
8095
8196 @property
8297 def inset_slice(self):
83 return self.modifier[self.input_map["Inset Slice"]]
98 return self.inputs[self.input_map["Inset Slice"]]
8499
85100 @inset_slice.setter
86101 def inset_slice(self, val):
87 self.modifier[self.input_map["Inset Slice"]] = bool(val)
102 self.inputs[self.input_map["Inset Slice"]] = bool(val)
88103 self.update_tag()
89104
90105 @property
91106 def inset_slice_offset(self):
92 return self.modifier[self.input_map["Inset Slice Offset"]]
107 return self.inputs[self.input_map["Inset Slice Offset"]]
93108
94109 @inset_slice_offset.setter
95110 def inset_slice_offset(self, val):
96 self.modifier[self.input_map["Inset Slice Offset"]] = float(val)
111 self.inputs[self.input_map["Inset Slice Offset"]] = float(val)
97112 self.update_tag()
98113
99114 @property
100115 def transfer_normals(self):
101 return self.modifier[self.input_map["Transfer Normals"]]
116 return self.inputs[self.input_map["Transfer Normals"]]
102117
103118 @transfer_normals.setter
104119 def transfer_normals(self, val):
105 self.modifier[self.input_map["Transfer Normals"]] = bool(val)
120 self.inputs[self.input_map["Transfer Normals"]] = bool(val)
106121 self.update_tag()
107122
108123 @property
109124 def material_cut(self):
110 return self.modifier[self.input_map["Material Cut"]]
125 return self.inputs[self.input_map["Material Cut"]]
111126
112127 @material_cut.setter
113128 def material_cut(self, val):
114 self.modifier[self.input_map["Material Cut"]] = bool(val)
129 self.inputs[self.input_map["Material Cut"]] = bool(val)
115130 self.update_tag()
116131
117132 @property
118133 def material(self):
119 return self.modifier[self.input_map["Material"]]
134 return self.inputs[self.input_map["Material"]]
120135
121136 @material.setter
122137 def material(self, val):
123 self.modifier[self.input_map["Material"]] = val
138 self.inputs[self.input_map["Material"]] = val
124139 self.update_tag()
125140
126141 @property
127142 def weld(self):
128 return self.modifier[self.input_map["Weld"]]
143 return self.inputs[self.input_map["Weld"]]
129144
130145 @weld.setter
131146 def weld(self, val):
132 self.modifier[self.input_map["Weld"]] = bool(val)
147 self.inputs[self.input_map["Weld"]] = bool(val)
133148 self.update_tag()
134149
135150 @property
136151 def merge_threshold(self):
137 return self.modifier[self.input_map["Merge Threshold"]]
152 return self.inputs[self.input_map["Merge Threshold"]]
138153
139154 @merge_threshold.setter
140155 def merge_threshold(self, val):
141 self.modifier[self.input_map["Merge Threshold"]] = float(val)
156 self.inputs[self.input_map["Merge Threshold"]] = float(val)
142157 self.update_tag()
143158
144159 @property
145160 def tag_faces_name(self):
146 return self.modifier[self.input_map["Tag Faces Name"]]
161 return self.inputs[self.input_map["Tag Faces Name"]]
147162
148163 @tag_faces_name.setter
149164 def tag_faces_name(self, val):
150 self.modifier[self.input_map["Tag Faces Name"]] = val
165 self.inputs[self.input_map["Tag Faces Name"]] = val
151166 self.update_tag()
152167
153168 @property
154169 def tag_faces_value(self):
155 return self.modifier[self.input_map["Tag Faces Value"]]
170 return self.inputs[self.input_map["Tag Faces Value"]]
156171
157172 @tag_faces_value.setter
158173 def tag_faces_value(self, val):
159 self.modifier[self.input_map["Tag Faces Value"]] = float(val)
174 self.inputs[self.input_map["Tag Faces Value"]] = float(val)
160175 self.update_tag()
161176
162177 @property
163178 def tag_edge_boundary_name(self):
164 return self.modifier[self.input_map["Tag Edge Boundary Name"]]
179 return self.inputs[self.input_map["Tag Edge Boundary Name"]]
165180
166181 @tag_edge_boundary_name.setter
167182 def tag_edge_boundary_name(self, val):
168 self.modifier[self.input_map["Tag Edge Boundary Name"]] = val
183 self.inputs[self.input_map["Tag Edge Boundary Name"]] = val
169184 self.update_tag()
170185
171186 @property
172187 def tag_edge_boundary_value(self):
173 return self.modifier[self.input_map["Tag Edge Boundary Value"]]
188 return self.inputs[self.input_map["Tag Edge Boundary Value"]]
174189
175190 @tag_edge_boundary_value.setter
176191 def tag_edge_boundary_value(self, val):
177 self.modifier[self.input_map["Tag Edge Boundary Value"]] = float(val)
192 self.inputs[self.input_map["Tag Edge Boundary Value"]] = float(val)
178193 self.update_tag()
179194
180195 @property
181196 def tag_inset_slice_face_name(self):
182 return self.modifier[self.input_map["Tag Inset Slice Face Name"]]
197 return self.inputs[self.input_map["Tag Inset Slice Face Name"]]
183198
184199 @tag_inset_slice_face_name.setter
185200 def tag_inset_slice_face_name(self, val):
186 self.modifier[self.input_map["Tag Inset Slice Face Name"]] = val
201 self.inputs[self.input_map["Tag Inset Slice Face Name"]] = val
187202 self.update_tag()
188203
189204 @property
190205 def tag_inset_slice_face_value(self):
191 return self.modifier[self.input_map["Tag Inset Slice Face Value"]]
206 return self.inputs[self.input_map["Tag Inset Slice Face Value"]]
192207
193208 @tag_inset_slice_face_value.setter
194209 def tag_inset_slice_face_value(self, val):
195 self.modifier[self.input_map["Tag Inset Slice Face Value"]] = float(val)
210 self.inputs[self.input_map["Tag Inset Slice Face Value"]] = float(val)
196211 self.update_tag()
197212
198213 @property
199214 def tag_inset_slice_edge_name(self):
200 return self.modifier[self.input_map["Tag Inset Slice Edge Name"]]
215 return self.inputs[self.input_map["Tag Inset Slice Edge Name"]]
201216
202217 @tag_inset_slice_edge_name.setter
203218 def tag_inset_slice_edge_name(self, val):
204 self.modifier[self.input_map["Tag Inset Slice Edge Name"]] = val
219 self.inputs[self.input_map["Tag Inset Slice Edge Name"]] = val
205220 self.update_tag()
206221
207222 @property
208223 def tag_inset_slice_edge_value(self):
209 return self.modifier[self.input_map["Tag Inset Slice Edge Value"]]
224 return self.inputs[self.input_map["Tag Inset Slice Edge Value"]]
210225
211226 @tag_inset_slice_edge_value.setter
212227 def tag_inset_slice_edge_value(self, val):
213 self.modifier[self.input_map["Tag Inset Slice Edge Value"]] = float(val)
228 self.inputs[self.input_map["Tag Inset Slice Edge Value"]] = float(val)
214229 self.update_tag()
215230
216231 @property
217232 def clean_attribute_1(self):
218 return self.modifier[self.input_map["Clean Attribute 1"]]
233 return self.inputs[self.input_map["Clean Attribute 1"]]
219234
220235 @clean_attribute_1.setter
221236 def clean_attribute_1(self, val):
222 self.modifier[self.input_map["Clean Attribute 1"]] = val
237 self.inputs[self.input_map["Clean Attribute 1"]] = val
223238 self.update_tag()
224239
225240 @property
226241 def clean_attribute_2(self):
227 return self.modifier[self.input_map["Clean Attribute 2"]]
242 return self.inputs[self.input_map["Clean Attribute 2"]]
228243
229244 @clean_attribute_2.setter
230245 def clean_attribute_2(self, val):
231 self.modifier[self.input_map["Clean Attribute 2"]] = val
246 self.inputs[self.input_map["Clean Attribute 2"]] = val
232247 self.update_tag()
233248
234249 @property
235250 def clean_attribute_3(self):
236 return self.modifier[self.input_map["Clean Attribute 3"]]
251 return self.inputs[self.input_map["Clean Attribute 3"]]
237252
238253 @clean_attribute_3.setter
239254 def clean_attribute_3(self, val):
240 self.modifier[self.input_map["Clean Attribute 3"]] = val
255 self.inputs[self.input_map["Clean Attribute 3"]] = val
241256 self.update_tag()
242257
243258 @property
244259 def clean_attribute_4(self):
245 return self.modifier[self.input_map["Clean Attribute 4"]]
260 return self.inputs[self.input_map["Clean Attribute 4"]]
246261
247262 @clean_attribute_4.setter
248263 def clean_attribute_4(self, val):
249 self.modifier[self.input_map["Clean Attribute 4"]] = val
264 self.inputs[self.input_map["Clean Attribute 4"]] = val
250265 self.update_tag()
251266
252267 @property
253268 def clean_attribute_5(self):
254 return self.modifier[self.input_map["Clean Attribute 5"]]
269 return self.inputs[self.input_map["Clean Attribute 5"]]
255270
256271 @clean_attribute_5.setter
257272 def clean_attribute_5(self, val):
258 self.modifier[self.input_map["Clean Attribute 5"]] = val
273 self.inputs[self.input_map["Clean Attribute 5"]] = val
259274 self.update_tag()
260275
261276 @classmethod
utility/geometry_nodes/node_modifier.py +17 -7
……@@ -1,4 +1,5 @@
11import bpy, pathlib, sys
2from . import compat
23
34
45class NodeModifier():
……@@ -13,6 +14,11 @@ class NodeModifier():
1314 _filepath = '' # used for cacheing string
1415 blend_filename = 'geometry_nodes_essentials.blend' #5.0 node lib
1516
17 # Input access, keyed by socket identifier. 5.2 moved inputs off IDProperties
18 @property
19 def inputs(self):
20 return compat.ModifierInputs(self.modifier)
21
1622 # Copy shared modifier interface
1723 @property
1824 def show_on_cage(self):
……@@ -199,20 +205,24 @@ class NodeModifier():
199205 return path / 'geometry_nodes'
200206
201207 @classmethod
202 def prop_draw_string(cls, modifier, string) -> str:
203 '''Get string for drawing modifier's prop \n
204 Returns input string if string can't be calculated e.g. input modifier of incompatible type'''
208 def prop_draw_data(cls, modifier, string) -> tuple:
209 '''Get (data, property) pair for drawing modifier's prop with layout.prop\n
210 Inputs live on the modifier's IDProperties up to 5.1 and on their own RNA struct from 5.2\n
211 Returns (modifier, string) if the pair can't be calculated e.g. input modifier of incompatible type'''
205212
206 if hasattr(modifier, string): return string
213 if hasattr(modifier, string): return modifier, string
207214
208215 if not cls.prop_draw_map:
209 if not cls.is_valid_modifier(modifier): return string
216 if not cls.is_valid_modifier(modifier): return modifier, string
210217 cls.prop_draw_map_build(modifier)
211218
212219 identifier = cls.prop_draw_map.get(string, None)
213 if not identifier: return string
220 if not identifier: return modifier, string
221
222 data, prop = compat.value_prop(modifier, identifier)
223 if data is None: return modifier, string
214224
215 return f'["{identifier}"]'
225 return data, prop
216226
217227
218228 def update_tag(self):
utility/geometry_nodes/smooth_by_angle.py +4 -4
……@@ -10,20 +10,20 @@ class SmoothByAngle(NodeModifier):
1010
1111 @property
1212 def angle (self):
13 return self.modifier[self.input_map['Angle']]
13 return self.inputs[self.input_map['Angle']]
1414
1515 @angle.setter
1616 def angle(self, value):
17 self.modifier[self.input_map['Angle']] = float(value)
17 self.inputs[self.input_map['Angle']] = float(value)
1818 self.update_tag()
1919
2020 @property
2121 def ignore_sharpness (self):
22 return self.modifier[self.input_map['Ignore Sharpness']]
22 return self.inputs[self.input_map['Ignore Sharpness']]
2323
2424 @ignore_sharpness.setter
2525 def ignore_sharpness(self, value):
26 self.modifier[self.input_map['Ignore Sharpness']] = bool(value)
26 self.inputs[self.input_map['Ignore Sharpness']] = bool(value)
2727 self.update_tag()
2828
2929 @classmethod