nodebpy 0.1.1__py3-none-any.whl → 0.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
nodebpy/nodes/zone.py ADDED
@@ -0,0 +1,442 @@
1
+ from abc import ABC, abstractmethod
2
+ from typing import Literal
3
+
4
+ import bpy
5
+ from bpy.types import NodeSocket
6
+
7
+ from nodebpy.builder import NodeBuilder, SocketLinker
8
+
9
+ from .types import (
10
+ LINKABLE,
11
+ TYPE_INPUT_BOOLEAN,
12
+ TYPE_INPUT_GEOMETRY,
13
+ TYPE_INPUT_INT,
14
+ _AttributeDomains,
15
+ _BakeDataTypes,
16
+ )
17
+
18
+
19
+ class BaseZone(NodeBuilder, ABC):
20
+ _items_attribute: Literal["state_items", "repeat_items"]
21
+
22
+ @property
23
+ @abstractmethod
24
+ def _items_node(
25
+ self,
26
+ ) -> bpy.types.GeometryNodeRepeatOutput | bpy.types.GeometryNodeSimulationOutput:
27
+ """Return the items node (state_node, repeat_node, etc.)"""
28
+ pass
29
+
30
+ @property
31
+ @abstractmethod
32
+ def items(
33
+ self,
34
+ ) -> (
35
+ bpy.types.NodeGeometryRepeatOutputItems
36
+ | bpy.types.NodeGeometrySimulationOutputItems
37
+ ):
38
+ """Return the items collection"""
39
+ pass
40
+
41
+ @property
42
+ def outputs(self) -> dict[str, SocketLinker]:
43
+ """Get all output sockets based on items collection"""
44
+ return {
45
+ item.name: SocketLinker(self.node.outputs[item.name]) for item in self.items
46
+ }
47
+
48
+ @property
49
+ def inputs(self) -> dict[str, SocketLinker]:
50
+ """Get all input sockets based on items collection"""
51
+ return {
52
+ item.name: SocketLinker(self.node.inputs[item.name]) for item in self.items
53
+ }
54
+
55
+ def capture(
56
+ self, value: LINKABLE, domain: _AttributeDomains = "POINT"
57
+ ) -> SocketLinker:
58
+ """Capture something as an input to the simulation"""
59
+ item_dict = self._add_inputs(value)
60
+ self._establish_links(**item_dict)
61
+ return SocketLinker(self.node.outputs[-2])
62
+
63
+
64
+ class BaseZoneInput(BaseZone, NodeBuilder, ABC):
65
+ """Base class for zone input nodes"""
66
+
67
+ node: bpy.types.GeometryNodeSimulationInput | bpy.types.GeometryNodeRepeatInput
68
+
69
+ @property
70
+ def _items_node(self): # type: ignore
71
+ return self.node.paired_output
72
+
73
+ @property
74
+ def output(
75
+ self,
76
+ ) -> bpy.types.GeometryNodeSimulationOutput | bpy.types.GeometryNodeRepeatOutput:
77
+ return self.node.paired_output # type: ignore
78
+
79
+ def _add_socket(self, name: str, type: _BakeDataTypes):
80
+ """Add a socket to the zone"""
81
+ item = self.items.new(type, name)
82
+ return self.inputs[item.name]
83
+
84
+ def __rshift__(self, other):
85
+ """Custom zone input linking that creates sockets as needed"""
86
+ # Check if target is a zone output without inputs
87
+ if (
88
+ hasattr(other, "_default_input_socket")
89
+ and other._default_input_socket is None
90
+ ):
91
+ # Target zone needs a socket - create one based on our output
92
+ from ..builder import SOCKET_COMPATIBILITY
93
+
94
+ source_socket = self._default_output_socket
95
+ source_type = source_socket.type
96
+
97
+ compatible_types = SOCKET_COMPATIBILITY.get(source_type, [source_type])
98
+ best_type = compatible_types[0] if compatible_types else source_type
99
+
100
+ # Create socket on target zone
101
+ target_socket = other._add_socket(name=best_type.title(), type=best_type)
102
+ self.tree.link(source_socket, target_socket)
103
+ return other
104
+ else:
105
+ # Use the general smart linking approach
106
+ return self._smart_link_to(other)
107
+
108
+
109
+ class BaseZoneOutput(BaseZone, NodeBuilder, ABC):
110
+ """Base class for zone output nodes"""
111
+
112
+ node: bpy.types.GeometryNodeSimulationOutput | bpy.types.GeometryNodeRepeatOutput
113
+
114
+ @property
115
+ def _items_node(
116
+ self,
117
+ ) -> bpy.types.GeometryNodeRepeatOutput | bpy.types.GeometryNodeSimulationOutput:
118
+ return self.node
119
+
120
+ def _add_socket(self, name: str, type: _BakeDataTypes):
121
+ """Add a socket to the zone"""
122
+ item = self.items.new(type, name)
123
+ return self.node.inputs[item.name]
124
+
125
+ def __rshift__(self, other):
126
+ """Custom zone output linking that creates sockets as needed"""
127
+ from ..builder import SOCKET_COMPATIBILITY
128
+
129
+ # Get the source socket type
130
+ source_socket = self._default_output_socket
131
+ source_type = source_socket.type
132
+
133
+ # Check if target has compatible inputs
134
+ if hasattr(other, "_default_input_socket") and other._default_input_socket:
135
+ # Normal linking
136
+ return super().__rshift__(other)
137
+ elif hasattr(other, "_add_socket"):
138
+ # Target is also a zone - create compatible socket
139
+ compatible_types = SOCKET_COMPATIBILITY.get(source_type, [source_type])
140
+ best_type = compatible_types[0] if compatible_types else source_type
141
+
142
+ # Create socket on target zone
143
+ target_socket = other._add_socket(name=best_type.title(), type=best_type)
144
+ self.tree.link(source_socket, target_socket)
145
+ return other
146
+ else:
147
+ # Normal NodeBuilder
148
+ return super().__rshift__(other)
149
+
150
+ @property
151
+ def _default_input_socket(self) -> NodeSocket:
152
+ """Get default input socket, avoiding skip-type sockets"""
153
+ inputs = list(self.inputs.values())
154
+ if inputs:
155
+ return inputs[0].socket
156
+ else:
157
+ # No socket exists - this should be handled by zone-specific __rshift__ logic
158
+ # Return None to signal that a socket needs to be created
159
+ return None
160
+
161
+
162
+ class SimulationZone:
163
+ def __init__(self, *args: LINKABLE, **kwargs: LINKABLE):
164
+ self.input = SimulationInput()
165
+ self.output = SimulationOutput()
166
+ self.input.node.pair_with_output(self.output.node)
167
+
168
+ self.output.node.state_items.clear()
169
+ socket_lookup = self.output._add_inputs(*args, **kwargs)
170
+ for name, source in socket_lookup.items():
171
+ self.input._link_from(source, name)
172
+
173
+ def delta_time(self) -> SocketLinker:
174
+ return self.input.o_delta_time
175
+
176
+ def __getitem__(self, index: int):
177
+ match index:
178
+ case 0:
179
+ return self.input
180
+ case 1:
181
+ return self.output
182
+ case _:
183
+ raise IndexError("SimulationZone has only two items")
184
+
185
+
186
+ class BaseSimulationZone(BaseZone):
187
+ _items_attribute = "state_items"
188
+
189
+ @property
190
+ def items(self) -> bpy.types.NodeGeometrySimulationOutputItems:
191
+ return self._items_node.state_items # type: ignore
192
+
193
+
194
+ class SimulationInput(BaseSimulationZone, BaseZoneInput):
195
+ """Simulation Input node"""
196
+
197
+ name = "GeometryNodeSimulationInput"
198
+ node: bpy.types.GeometryNodeSimulationInput
199
+
200
+ @property
201
+ def o_delta_time(self) -> SocketLinker:
202
+ """Output socket: Delta Time"""
203
+ return self._output("Delta Time")
204
+
205
+
206
+ class SimulationOutput(BaseSimulationZone, BaseZoneOutput):
207
+ """Simulation Output node"""
208
+
209
+ name = "GeometryNodeSimulationOutput"
210
+ node: bpy.types.GeometryNodeSimulationOutput
211
+
212
+ @property
213
+ def i_skip(self) -> SocketLinker:
214
+ """Input socket: Skip simluation frame"""
215
+ return self._input("Skip")
216
+
217
+
218
+ class RepeatZone:
219
+ """Wrapper that supports both direct unpacking and iteration"""
220
+
221
+ def __init__(
222
+ self, iterations: TYPE_INPUT_INT = 1, *args: LINKABLE, **kwargs: LINKABLE
223
+ ):
224
+ self.input = RepeatInput(iterations)
225
+ self.output = RepeatOutput()
226
+ self.input.node.pair_with_output(self.output.node)
227
+
228
+ self.output.node.repeat_items.clear()
229
+ self.input._establish_links(**self.input._add_inputs(*args, **kwargs))
230
+
231
+ @property
232
+ def i(self) -> SocketLinker:
233
+ """Input socket: Skip simluation frame"""
234
+ return self.input.o_iteration
235
+
236
+ def __iter__(self):
237
+ """Support for loop: for i, input, output in RepeatZone(...)"""
238
+ self._index = 0
239
+ return self
240
+
241
+ def __next__(self):
242
+ """Support for iteration: next(RepeatZone)"""
243
+ if self._index > 0:
244
+ raise StopIteration
245
+ self._index += 1
246
+ return self.i, self.input, self.output
247
+
248
+
249
+ class BaseRepeatZone(BaseZone):
250
+ _items_attribute = "repeat_items"
251
+
252
+ @property
253
+ def items(self) -> bpy.types.NodeGeometryRepeatOutputItems:
254
+ return self._items_node.repeat_items # type: ignore
255
+
256
+
257
+ class RepeatInput(BaseRepeatZone, BaseZoneInput):
258
+ """Repeat Input node"""
259
+
260
+ name = "GeometryNodeRepeatInput"
261
+ node: bpy.types.GeometryNodeRepeatInput
262
+
263
+ def __init__(self, iterations: TYPE_INPUT_INT = 1):
264
+ super().__init__()
265
+ key_args = {"Iterations": iterations}
266
+ self._establish_links(**key_args)
267
+
268
+ @property
269
+ def o_iteration(self) -> SocketLinker:
270
+ """Output socket: Iteration"""
271
+ return self._output("Iteration")
272
+
273
+ @property
274
+ def output_node(self) -> bpy.types.GeometryNodeRepeatOutput:
275
+ zone_output = self.node.paired_output # type: ignore
276
+ assert zone_output is not None
277
+ return zone_output # type: ignore
278
+
279
+
280
+ class RepeatOutput(BaseRepeatZone, BaseZoneOutput):
281
+ """Repeat Output node"""
282
+
283
+ name = "GeometryNodeRepeatOutput"
284
+ node: bpy.types.GeometryNodeRepeatOutput
285
+
286
+
287
+ class ForEachGeometryElementInput(NodeBuilder):
288
+ """For Each Geometry Element Input node"""
289
+
290
+ name = "GeometryNodeForeachGeometryElementInput"
291
+ node: bpy.types.GeometryNodeForeachGeometryElementInput
292
+
293
+ def __init__(
294
+ self,
295
+ geometry: TYPE_INPUT_GEOMETRY = None,
296
+ selection: TYPE_INPUT_BOOLEAN = True,
297
+ extend: LINKABLE | None = None,
298
+ **kwargs,
299
+ ):
300
+ super().__init__()
301
+ key_args = {"Geometry": geometry, "Selection": selection, "__extend__": extend}
302
+ key_args.update(kwargs)
303
+
304
+ self._establish_links(**key_args)
305
+
306
+ @property
307
+ def i_geometry(self) -> SocketLinker:
308
+ """Input socket: Geometry"""
309
+ return self._input("Geometry")
310
+
311
+ @property
312
+ def i_selection(self) -> SocketLinker:
313
+ """Input socket: Selection"""
314
+ return self._input("Selection")
315
+
316
+ @property
317
+ def i_input_socket(self) -> SocketLinker:
318
+ """Input socket:"""
319
+ return self._input("__extend__")
320
+
321
+ @property
322
+ def o_index(self) -> SocketLinker:
323
+ """Output socket: Index"""
324
+ return self._output("Index")
325
+
326
+ @property
327
+ def o_input_socket(self) -> SocketLinker:
328
+ """Output socket:"""
329
+ return self._output("__extend__")
330
+
331
+
332
+ class ForEachGeometryElementOutput(NodeBuilder):
333
+ """For Each Geometry Element Output node"""
334
+
335
+ name = "GeometryNodeForeachGeometryElementOutput"
336
+ node: bpy.types.GeometryNodeForeachGeometryElementOutput
337
+
338
+ def __init__(
339
+ self,
340
+ extend_main: LINKABLE | None = None,
341
+ generation_0: LINKABLE = None,
342
+ extend_generation: LINKABLE | None = None,
343
+ active_input_index: int = 0,
344
+ active_generation_index: int = 0,
345
+ active_main_index: int = 0,
346
+ domain: _AttributeDomains = "POINT",
347
+ inspection_index: int = 0,
348
+ **kwargs,
349
+ ):
350
+ super().__init__()
351
+ key_args = {
352
+ "__extend__main": extend_main,
353
+ "Generation_0": generation_0,
354
+ "__extend__generation": extend_generation,
355
+ }
356
+ key_args.update(kwargs)
357
+ self.active_input_index = active_input_index
358
+ self.active_generation_index = active_generation_index
359
+ self.active_main_index = active_main_index
360
+ self.domain = domain
361
+ self.inspection_index = inspection_index
362
+ self._establish_links(**key_args)
363
+
364
+ @property
365
+ def i_input_socket(self) -> SocketLinker:
366
+ """Input socket:"""
367
+ return self._input("__extend__main")
368
+
369
+ @property
370
+ def i_geometry(self) -> SocketLinker:
371
+ """Input socket: Geometry"""
372
+ return self._input("Generation_0")
373
+
374
+ @property
375
+ def i_extend_generation(self) -> SocketLinker:
376
+ """Input socket:"""
377
+ return self._input("__extend__generation")
378
+
379
+ @property
380
+ def o_geometry(self) -> SocketLinker:
381
+ """Output socket: Geometry"""
382
+ return self._output("Geometry")
383
+
384
+ @property
385
+ def o_input_socket(self) -> SocketLinker:
386
+ """Output socket:"""
387
+ return self._output("__extend__main")
388
+
389
+ @property
390
+ def o_generation_0(self) -> SocketLinker:
391
+ """Output socket: Geometry"""
392
+ return self._output("Generation_0")
393
+
394
+ @property
395
+ def o_extend_generation(self) -> SocketLinker:
396
+ """Output socket:"""
397
+ return self._output("__extend__generation")
398
+
399
+ @property
400
+ def active_input_index(self) -> int:
401
+ return self.node.active_input_index
402
+
403
+ @active_input_index.setter
404
+ def active_input_index(self, value: int):
405
+ self.node.active_input_index = value
406
+
407
+ @property
408
+ def active_generation_index(self) -> int:
409
+ return self.node.active_generation_index
410
+
411
+ @active_generation_index.setter
412
+ def active_generation_index(self, value: int):
413
+ self.node.active_generation_index = value
414
+
415
+ @property
416
+ def active_main_index(self) -> int:
417
+ return self.node.active_main_index
418
+
419
+ @active_main_index.setter
420
+ def active_main_index(self, value: int):
421
+ self.node.active_main_index = value
422
+
423
+ @property
424
+ def domain(
425
+ self,
426
+ ) -> _AttributeDomains:
427
+ return self.node.domain
428
+
429
+ @domain.setter
430
+ def domain(
431
+ self,
432
+ value: _AttributeDomains,
433
+ ):
434
+ self.node.domain = value
435
+
436
+ @property
437
+ def inspection_index(self) -> int:
438
+ return self.node.inspection_index
439
+
440
+ @inspection_index.setter
441
+ def inspection_index(self, value: int):
442
+ self.node.inspection_index = value
nodebpy/screenshot.py CHANGED
@@ -438,7 +438,8 @@ def generate_mermaid_diagram(tree) -> str:
438
438
  ):
439
439
  formatted = ",".join(f"{v:.1g}" for v in value)
440
440
  key_params.append(f"({formatted})")
441
- except:
441
+ except Exception as e:
442
+ print(f"Error processing node: {e}")
442
443
  pass
443
444
 
444
445
  # Build minimal node label
nodebpy/sockets.py CHANGED
@@ -8,22 +8,22 @@ For example: SocketVector (interface socket) vs Vector (input node).
8
8
  """
9
9
 
10
10
  from .builder import (
11
- SocketGeometry,
12
11
  SocketBoolean,
12
+ SocketBundle,
13
+ SocketClosure,
14
+ SocketCollection,
15
+ SocketColor,
13
16
  SocketFloat,
14
- SocketVector,
17
+ SocketGeometry,
18
+ SocketImage,
15
19
  SocketInt,
16
- SocketColor,
17
- SocketRotation,
20
+ SocketMaterial,
18
21
  SocketMatrix,
19
- SocketString,
20
- MenuSocket,
22
+ SocketMenu,
21
23
  SocketObject,
22
- SocketCollection,
23
- SocketImage,
24
- SocketMaterial,
25
- SocketBundle,
26
- SocketClosure,
24
+ SocketRotation,
25
+ SocketString,
26
+ SocketVector,
27
27
  )
28
28
 
29
29
  __all__ = [
@@ -36,7 +36,7 @@ __all__ = [
36
36
  "SocketRotation",
37
37
  "SocketMatrix",
38
38
  "SocketString",
39
- "MenuSocket",
39
+ "SocketMenu",
40
40
  "SocketObject",
41
41
  "SocketCollection",
42
42
  "SocketImage",
@@ -1,20 +1,20 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: nodebpy
3
- Version: 0.1.1
3
+ Version: 0.2.0
4
4
  Summary: Build nodes in Blender with code
5
5
  Author: Brady Johnston
6
6
  Author-email: Brady Johnston <brady.johnston@me.com>
7
7
  Requires-Dist: arrangebpy>=0.1.0
8
- Requires-Dist: jsondiff>=2.2.1
9
- Requires-Dist: syrupy>=5.0.0
10
- Requires-Dist: tree-clipper>=0.1.1
11
8
  Requires-Dist: bpy>=5.0.0 ; extra == 'bpy'
12
9
  Requires-Dist: fake-bpy-module>=20260113 ; extra == 'dev'
10
+ Requires-Dist: jsondiff>=2.2.1 ; extra == 'dev'
13
11
  Requires-Dist: pytest>=9.0.2 ; extra == 'dev'
14
12
  Requires-Dist: pytest-cov>=7.0.0 ; extra == 'dev'
15
13
  Requires-Dist: quarto-cli>=1.8.26 ; extra == 'dev'
16
14
  Requires-Dist: quartodoc>=0.11.1 ; extra == 'dev'
17
15
  Requires-Dist: ruff>=0.14.11 ; extra == 'dev'
16
+ Requires-Dist: syrupy>=5.0.0 ; extra == 'dev'
17
+ Requires-Dist: tree-clipper>=0.1.1 ; extra == 'dev'
18
18
  Requires-Dist: ipython>=8.0.0 ; extra == 'jupyter'
19
19
  Requires-Python: >=3.11
20
20
  Provides-Extra: bpy
@@ -0,0 +1,25 @@
1
+ nodebpy/__init__.py,sha256=AcXaVEPnvX1BlraPSSQ-0NR-Ip6cI0WNRqz84xvrTok,285
2
+ nodebpy/arrange.py,sha256=xBHf-lYlvr-BCdI0gHhEn1ETWjEQyYmzgmhIZ1RYal8,11020
3
+ nodebpy/builder.py,sha256=Wfi4qvrOmdNbKNnOuYVhLMzf6rspvBQnY8eAk4QH3sU,52351
4
+ nodebpy/nodes/__init__.py,sha256=vRXLnYtOJK-nAsrNJDT5MGjWnLMawvGDn7HmjQx_YU0,12145
5
+ nodebpy/nodes/attribute.py,sha256=ACgOqgZ6PX14NmQVL3Tx67-T4fJUJLVQz9xxGcdSB2Q,13027
6
+ nodebpy/nodes/color.py,sha256=xZIbLvYquroHk3N9lTrVNaz8xv8MhE65s4zKTl10qXU,1842
7
+ nodebpy/nodes/converter.py,sha256=UEpcVe1APLnXu64CSUZzz2OXvpxgtnUEjov623DkFHU,132396
8
+ nodebpy/nodes/experimental.py,sha256=FWyVI6P-NP_vo8JUMBm-pS57naaE4kMqEQUzoOwAkM8,6739
9
+ nodebpy/nodes/geometry.py,sha256=81vXjoZdG2vq4cjrxv21nglJ3UUrLAxfN006Bk7uAp4,160537
10
+ nodebpy/nodes/grid.py,sha256=mgyEAutS7yfEv2K6zE48hUk7Yg1eBrUW4nGdqrhItfA,34164
11
+ nodebpy/nodes/group.py,sha256=HzSTRBZbtRUGKI3vTbRndsxhXVKYIsX0tQfVQ5690G4,512
12
+ nodebpy/nodes/input.py,sha256=kofvhhz2bH3cjnHPBjN3w2t-sMUqfmkKPzhXJZFv7vQ,57283
13
+ nodebpy/nodes/interface.py,sha256=xSIo8qb8vN58rs71b2sAKIK6jKujWPIaM_eXNgLnIPc,11117
14
+ nodebpy/nodes/mesh.py,sha256=B_cX264fFMax0M9xjGQiQcAuHoORj1cAjMmOQilapBw,524
15
+ nodebpy/nodes/texture.py,sha256=8Plz7nzZtKGcPyrRlIpyOl_LNwtz5jezdbA8h7IVBHk,2059
16
+ nodebpy/nodes/types.py,sha256=Y768eltOU5mMQt4tNT6sjnK2o9VEK0FXn7eeu3G6lb4,10508
17
+ nodebpy/nodes/vector.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
18
+ nodebpy/nodes/zone.py,sha256=P-xNY0v-8AwU9KgULE1EBvhI27ayFj7qqLWwb4TD8Ik,13506
19
+ nodebpy/screenshot.py,sha256=gXAwGfeK05YL9XIpghv5nQ5cXVgtR35wnLnTVDAh08g,18885
20
+ nodebpy/screenshot_subprocess.py,sha256=bmuyjedROCasEDI-JdjXWVsYoEX5I-asE3KxMVR30qM,15364
21
+ nodebpy/sockets.py,sha256=eU3p0IdCkYQeVRp8XkkDuOR38JTIzniTXKhsXQgx29Q,1011
22
+ nodebpy-0.2.0.dist-info/WHEEL,sha256=eh7sammvW2TypMMMGKgsM83HyA_3qQ5Lgg3ynoecH3M,79
23
+ nodebpy-0.2.0.dist-info/entry_points.txt,sha256=XvODbWiwSnneYWIYjxFJZmXdntQnapg59Ye2LtCKEN4,42
24
+ nodebpy-0.2.0.dist-info/METADATA,sha256=PS4WASNyB2eY3_iU9lkm72NqWeuBEBcO1eK2Yl5Iv14,6299
25
+ nodebpy-0.2.0.dist-info/RECORD,,