nodebpy 0.9.1__tar.gz → 0.10.0__tar.gz

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.
Files changed (97) hide show
  1. {nodebpy-0.9.1 → nodebpy-0.10.0}/PKG-INFO +7 -11
  2. {nodebpy-0.9.1 → nodebpy-0.10.0}/README.md +6 -10
  3. {nodebpy-0.9.1 → nodebpy-0.10.0}/pyproject.toml +1 -1
  4. {nodebpy-0.9.1 → nodebpy-0.10.0}/src/nodebpy/__init__.py +2 -4
  5. nodebpy-0.10.0/src/nodebpy/builder/__init__.py +142 -0
  6. nodebpy-0.10.0/src/nodebpy/builder/_registry.py +19 -0
  7. nodebpy-0.10.0/src/nodebpy/builder/_utils.py +81 -0
  8. nodebpy-0.10.0/src/nodebpy/builder/accessor.py +188 -0
  9. nodebpy-0.10.0/src/nodebpy/builder/interface.py +571 -0
  10. nodebpy-0.10.0/src/nodebpy/builder/mixins.py +305 -0
  11. nodebpy-0.10.0/src/nodebpy/builder/node.py +233 -0
  12. nodebpy-0.10.0/src/nodebpy/builder/socket.py +508 -0
  13. nodebpy-0.10.0/src/nodebpy/builder/tree.py +851 -0
  14. {nodebpy-0.9.1 → nodebpy-0.10.0}/src/nodebpy/nodes/compositor/__init__.py +2 -0
  15. nodebpy-0.10.0/src/nodebpy/nodes/compositor/color.py +1396 -0
  16. nodebpy-0.10.0/src/nodebpy/nodes/compositor/converter.py +1451 -0
  17. nodebpy-0.10.0/src/nodebpy/nodes/compositor/distort.py +1287 -0
  18. nodebpy-0.10.0/src/nodebpy/nodes/compositor/filter.py +1966 -0
  19. {nodebpy-0.9.1 → nodebpy-0.10.0}/src/nodebpy/nodes/compositor/group.py +15 -1
  20. nodebpy-0.10.0/src/nodebpy/nodes/compositor/input.py +585 -0
  21. {nodebpy-0.9.1 → nodebpy-0.10.0}/src/nodebpy/nodes/compositor/interface.py +59 -33
  22. {nodebpy-0.9.1 → nodebpy-0.10.0}/src/nodebpy/nodes/compositor/manual.py +15 -0
  23. nodebpy-0.10.0/src/nodebpy/nodes/compositor/matte.py +1176 -0
  24. {nodebpy-0.9.1 → nodebpy-0.10.0}/src/nodebpy/nodes/compositor/output.py +41 -8
  25. nodebpy-0.10.0/src/nodebpy/nodes/compositor/vector.py +56 -0
  26. {nodebpy-0.9.1 → nodebpy-0.10.0}/src/nodebpy/nodes/geometry/__init__.py +3 -1
  27. {nodebpy-0.9.1 → nodebpy-0.10.0}/src/nodebpy/nodes/geometry/attribute.py +322 -232
  28. nodebpy-0.10.0/src/nodebpy/nodes/geometry/color.py +127 -0
  29. nodebpy-0.10.0/src/nodebpy/nodes/geometry/converter.py +5076 -0
  30. nodebpy-0.10.0/src/nodebpy/nodes/geometry/geometry.py +9316 -0
  31. nodebpy-0.10.0/src/nodebpy/nodes/geometry/grid.py +2504 -0
  32. nodebpy-0.10.0/src/nodebpy/nodes/geometry/group.py +35 -0
  33. nodebpy-0.10.0/src/nodebpy/nodes/geometry/groups.py +104 -0
  34. nodebpy-0.10.0/src/nodebpy/nodes/geometry/input.py +3567 -0
  35. {nodebpy-0.9.1 → nodebpy-0.10.0}/src/nodebpy/nodes/geometry/interface.py +307 -146
  36. {nodebpy-0.9.1 → nodebpy-0.10.0}/src/nodebpy/nodes/geometry/manual.py +779 -703
  37. {nodebpy-0.9.1 → nodebpy-0.10.0}/src/nodebpy/nodes/geometry/output.py +22 -9
  38. nodebpy-0.10.0/src/nodebpy/nodes/geometry/texture.py +1425 -0
  39. nodebpy-0.10.0/src/nodebpy/nodes/geometry/vector.py +716 -0
  40. {nodebpy-0.9.1 → nodebpy-0.10.0}/src/nodebpy/nodes/geometry/zone.py +85 -78
  41. {nodebpy-0.9.1 → nodebpy-0.10.0}/src/nodebpy/nodes/shader/__init__.py +9 -3
  42. nodebpy-0.10.0/src/nodebpy/nodes/shader/color.py +279 -0
  43. nodebpy-0.10.0/src/nodebpy/nodes/shader/converter.py +686 -0
  44. nodebpy-0.10.0/src/nodebpy/nodes/shader/grid.py +534 -0
  45. nodebpy-0.10.0/src/nodebpy/nodes/shader/group.py +35 -0
  46. nodebpy-0.10.0/src/nodebpy/nodes/shader/input.py +1077 -0
  47. {nodebpy-0.9.1 → nodebpy-0.10.0}/src/nodebpy/nodes/shader/interface.py +34 -7
  48. nodebpy-0.10.0/src/nodebpy/nodes/shader/manual.py +140 -0
  49. {nodebpy-0.9.1 → nodebpy-0.10.0}/src/nodebpy/nodes/shader/output.py +194 -84
  50. {nodebpy-0.9.1 → nodebpy-0.10.0}/src/nodebpy/nodes/shader/script.py +25 -2
  51. nodebpy-0.10.0/src/nodebpy/nodes/shader/shader.py +2136 -0
  52. {nodebpy-0.9.1 → nodebpy-0.10.0}/src/nodebpy/nodes/shader/texture.py +176 -56
  53. nodebpy-0.10.0/src/nodebpy/nodes/shader/vector.py +629 -0
  54. {nodebpy-0.9.1 → nodebpy-0.10.0}/src/nodebpy/screenshot.py +59 -27
  55. {nodebpy-0.9.1 → nodebpy-0.10.0}/src/nodebpy/sockets.py +2 -2
  56. nodebpy-0.10.0/src/nodebpy/types.py +245 -0
  57. nodebpy-0.9.1/src/nodebpy/builder.py +0 -1775
  58. nodebpy-0.9.1/src/nodebpy/nodes/compositor/color.py +0 -923
  59. nodebpy-0.9.1/src/nodebpy/nodes/compositor/converter.py +0 -1044
  60. nodebpy-0.9.1/src/nodebpy/nodes/compositor/distort.py +0 -844
  61. nodebpy-0.9.1/src/nodebpy/nodes/compositor/filter.py +0 -1102
  62. nodebpy-0.9.1/src/nodebpy/nodes/compositor/input.py +0 -476
  63. nodebpy-0.9.1/src/nodebpy/nodes/compositor/matte.py +0 -873
  64. nodebpy-0.9.1/src/nodebpy/nodes/compositor/texture.py +0 -886
  65. nodebpy-0.9.1/src/nodebpy/nodes/compositor/vector.py +0 -35
  66. nodebpy-0.9.1/src/nodebpy/nodes/geometry/color.py +0 -79
  67. nodebpy-0.9.1/src/nodebpy/nodes/geometry/converter.py +0 -3835
  68. nodebpy-0.9.1/src/nodebpy/nodes/geometry/geometry.py +0 -6617
  69. nodebpy-0.9.1/src/nodebpy/nodes/geometry/grid.py +0 -1815
  70. nodebpy-0.9.1/src/nodebpy/nodes/geometry/group.py +0 -21
  71. nodebpy-0.9.1/src/nodebpy/nodes/geometry/input.py +0 -2345
  72. nodebpy-0.9.1/src/nodebpy/nodes/geometry/texture.py +0 -953
  73. nodebpy-0.9.1/src/nodebpy/nodes/geometry/vector.py +0 -568
  74. nodebpy-0.9.1/src/nodebpy/nodes/shader/color.py +0 -187
  75. nodebpy-0.9.1/src/nodebpy/nodes/shader/converter.py +0 -503
  76. nodebpy-0.9.1/src/nodebpy/nodes/shader/grid.py +0 -406
  77. nodebpy-0.9.1/src/nodebpy/nodes/shader/group.py +0 -21
  78. nodebpy-0.9.1/src/nodebpy/nodes/shader/input.py +0 -898
  79. nodebpy-0.9.1/src/nodebpy/nodes/shader/manual.py +0 -15
  80. nodebpy-0.9.1/src/nodebpy/nodes/shader/shader.py +0 -1557
  81. nodebpy-0.9.1/src/nodebpy/nodes/shader/vector.py +0 -402
  82. nodebpy-0.9.1/src/nodebpy/nodes/shader/zone.py +0 -58
  83. nodebpy-0.9.1/src/nodebpy/types.py +0 -446
  84. {nodebpy-0.9.1 → nodebpy-0.10.0}/src/nodebpy/arrange.py +0 -0
  85. {nodebpy-0.9.1 → nodebpy-0.10.0}/src/nodebpy/lib/nodearrange/__init__.py +0 -0
  86. {nodebpy-0.9.1 → nodebpy-0.10.0}/src/nodebpy/lib/nodearrange/arrange/graph.py +0 -0
  87. {nodebpy-0.9.1 → nodebpy-0.10.0}/src/nodebpy/lib/nodearrange/arrange/ordering.py +0 -0
  88. {nodebpy-0.9.1 → nodebpy-0.10.0}/src/nodebpy/lib/nodearrange/arrange/ranking.py +0 -0
  89. {nodebpy-0.9.1 → nodebpy-0.10.0}/src/nodebpy/lib/nodearrange/arrange/realize.py +0 -0
  90. {nodebpy-0.9.1 → nodebpy-0.10.0}/src/nodebpy/lib/nodearrange/arrange/stacking.py +0 -0
  91. {nodebpy-0.9.1 → nodebpy-0.10.0}/src/nodebpy/lib/nodearrange/arrange/structs.py +0 -0
  92. {nodebpy-0.9.1 → nodebpy-0.10.0}/src/nodebpy/lib/nodearrange/arrange/sugiyama.py +0 -0
  93. {nodebpy-0.9.1 → nodebpy-0.10.0}/src/nodebpy/lib/nodearrange/arrange/x_coords.py +0 -0
  94. {nodebpy-0.9.1 → nodebpy-0.10.0}/src/nodebpy/lib/nodearrange/arrange/y_coords.py +0 -0
  95. {nodebpy-0.9.1 → nodebpy-0.10.0}/src/nodebpy/lib/nodearrange/config.py +0 -0
  96. {nodebpy-0.9.1 → nodebpy-0.10.0}/src/nodebpy/lib/nodearrange/utils.py +0 -0
  97. {nodebpy-0.9.1 → nodebpy-0.10.0}/src/nodebpy/nodes/__init__.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: nodebpy
3
- Version: 0.9.1
3
+ Version: 0.10.0
4
4
  Summary: Build nodes trees in Blender more elegantly with code
5
5
  Author: Brady Johnston
6
6
  Author-email: Brady Johnston <brady.johnston@me.com>
@@ -46,7 +46,6 @@ In `nodebpy` we use the `>>` operator to link from one node or socket into anoth
46
46
  This should feel and behave much like the <kbd>Alt</kbd> + <kbd>Right Click</kbd> drag between nodes in [Node Wrangler](https://docs.blender.org/manual/en/latest/addons/node/node_wrangler.html). It will use some smart logic to match the most compatible sockets between the nodes, but if you ever want to be explicit you do so. The input and output sockets of a node are accessible as properties via the `i_*` and `o_*` prefixes, or you can use the `...` placeholder to specify the particular input to be user, or pass in the previous node as a named argument.
47
47
 
48
48
  ```py
49
- g.Vector() >> g.SetPosition().i_offset
50
49
  g.Vector() >> g.SetPosition(offset=...)
51
50
  g.SetPosition(offset=g.Vector())
52
51
  ```
@@ -75,14 +74,9 @@ with TreeBuilder("MyTree") as tree:
75
74
  The node tree below creates a integer input and geometry output to the node group. We create a `rotation` variable that can be used later on as an argument, then construct a longer chain of nodes being created and linked together. The nodes are added and linked as each node is instantiated. After we exit the tree context, the nodes are automatically arranged.
76
75
 
77
76
  ``` python
78
- from nodebpy import TreeBuilder, geometry as g, sockets as s
79
-
80
- with TreeBuilder("AnotherTree", collapse=True) as tree:
81
- with tree.inputs:
82
- count = s.SocketInt("Count", 10)
83
- with tree.outputs:
84
- instances = s.SocketGeometry("Instances")
77
+ from nodebpy import geometry as g
85
78
 
79
+ with g.tree("AnotherTree", collapse=True) as tree:
86
80
  rotation = (
87
81
  g.RandomValue.vector(min=-1, seed=2)
88
82
  >> g.AlignRotationToVector()
@@ -90,7 +84,7 @@ with TreeBuilder("AnotherTree", collapse=True) as tree:
90
84
  )
91
85
 
92
86
  _ = (
93
- count
87
+ tree.inputs.integer("Count", 10)
94
88
  >> g.Points(position=g.RandomValue.vector(min=-1))
95
89
  >> g.InstanceOnPoints(instance=g.Cube(), rotation=rotation)
96
90
  >> g.SetPosition(
@@ -99,7 +93,7 @@ with TreeBuilder("AnotherTree", collapse=True) as tree:
99
93
  )
100
94
  >> g.RealizeInstances()
101
95
  >> g.InstanceOnPoints(g.Cube(), instance=...)
102
- >> instances
96
+ >> tree.outputs.geometry("Instances")
103
97
  )
104
98
  ```
105
99
 
@@ -120,7 +114,9 @@ math.operation = "SUBTRACT"
120
114
 
121
115
  # operation can be chose as a class method
122
116
  math = g.Math.subtract(1.0, 2.0)
117
+ math = g.Value(1.0) - 2.0
123
118
  math = g.Math.add(1.0, 2.0)
119
+ math = g.Value(1.0) + 2.0
124
120
 
125
121
  # these are equivalent, the g.Math.multiply is automatically added
126
122
  g.Value(1.0) * 2
@@ -33,7 +33,6 @@ In `nodebpy` we use the `>>` operator to link from one node or socket into anoth
33
33
  This should feel and behave much like the <kbd>Alt</kbd> + <kbd>Right Click</kbd> drag between nodes in [Node Wrangler](https://docs.blender.org/manual/en/latest/addons/node/node_wrangler.html). It will use some smart logic to match the most compatible sockets between the nodes, but if you ever want to be explicit you do so. The input and output sockets of a node are accessible as properties via the `i_*` and `o_*` prefixes, or you can use the `...` placeholder to specify the particular input to be user, or pass in the previous node as a named argument.
34
34
 
35
35
  ```py
36
- g.Vector() >> g.SetPosition().i_offset
37
36
  g.Vector() >> g.SetPosition(offset=...)
38
37
  g.SetPosition(offset=g.Vector())
39
38
  ```
@@ -62,14 +61,9 @@ with TreeBuilder("MyTree") as tree:
62
61
  The node tree below creates a integer input and geometry output to the node group. We create a `rotation` variable that can be used later on as an argument, then construct a longer chain of nodes being created and linked together. The nodes are added and linked as each node is instantiated. After we exit the tree context, the nodes are automatically arranged.
63
62
 
64
63
  ``` python
65
- from nodebpy import TreeBuilder, geometry as g, sockets as s
66
-
67
- with TreeBuilder("AnotherTree", collapse=True) as tree:
68
- with tree.inputs:
69
- count = s.SocketInt("Count", 10)
70
- with tree.outputs:
71
- instances = s.SocketGeometry("Instances")
64
+ from nodebpy import geometry as g
72
65
 
66
+ with g.tree("AnotherTree", collapse=True) as tree:
73
67
  rotation = (
74
68
  g.RandomValue.vector(min=-1, seed=2)
75
69
  >> g.AlignRotationToVector()
@@ -77,7 +71,7 @@ with TreeBuilder("AnotherTree", collapse=True) as tree:
77
71
  )
78
72
 
79
73
  _ = (
80
- count
74
+ tree.inputs.integer("Count", 10)
81
75
  >> g.Points(position=g.RandomValue.vector(min=-1))
82
76
  >> g.InstanceOnPoints(instance=g.Cube(), rotation=rotation)
83
77
  >> g.SetPosition(
@@ -86,7 +80,7 @@ with TreeBuilder("AnotherTree", collapse=True) as tree:
86
80
  )
87
81
  >> g.RealizeInstances()
88
82
  >> g.InstanceOnPoints(g.Cube(), instance=...)
89
- >> instances
83
+ >> tree.outputs.geometry("Instances")
90
84
  )
91
85
  ```
92
86
 
@@ -107,7 +101,9 @@ math.operation = "SUBTRACT"
107
101
 
108
102
  # operation can be chose as a class method
109
103
  math = g.Math.subtract(1.0, 2.0)
104
+ math = g.Value(1.0) - 2.0
110
105
  math = g.Math.add(1.0, 2.0)
106
+ math = g.Value(1.0) + 2.0
111
107
 
112
108
  # these are equivalent, the g.Math.multiply is automatically added
113
109
  g.Value(1.0) * 2
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "nodebpy"
3
- version = "0.9.1"
3
+ version = "0.10.0"
4
4
  description = "Build nodes trees in Blender more elegantly with code"
5
5
  readme = "README.md"
6
6
  authors = [
@@ -1,7 +1,6 @@
1
1
  from . import nodes, screenshot, sockets
2
- from .builder import TreeBuilder
2
+ from .builder import TreeBuilder, NodeGroupBuilder
3
3
  from .nodes import compositor, geometry, shader
4
- from .screenshot import generate_mermaid_diagram, save_mermaid_diagram
5
4
 
6
5
  __all__ = [
7
6
  "nodes",
@@ -11,6 +10,5 @@ __all__ = [
11
10
  "sockets",
12
11
  "screenshot",
13
12
  "TreeBuilder",
14
- "generate_mermaid_diagram",
15
- "save_mermaid_diagram",
13
+ "NodeGroupBuilder",
16
14
  ]
@@ -0,0 +1,142 @@
1
+ """nodebpy.builder — node tree construction API.
2
+
3
+ Public names are re-exported here. Old names (NodeBuilder, SocketLinker,
4
+ SocketBase) are kept as aliases for backward compatibility.
5
+ """
6
+
7
+ from ._utils import SocketError, denormalize_name, normalize_name
8
+ from .accessor import SocketAccessor
9
+ from .interface import (
10
+ InterfaceSocket,
11
+ SocketBoolean,
12
+ SocketBundle,
13
+ SocketClosure,
14
+ SocketCollection,
15
+ SocketColor,
16
+ SocketFloat,
17
+ SocketGeometry,
18
+ SocketImage,
19
+ SocketInteger,
20
+ SocketMaterial,
21
+ SocketMatrix,
22
+ SocketMenu,
23
+ SocketObject,
24
+ SocketRotation,
25
+ SocketShader,
26
+ SocketString,
27
+ SocketVector,
28
+ )
29
+ from .mixins import LinkingMixin, OperatorMixin
30
+ from .node import BaseNode, DynamicInputsMixin, NodeGroupBuilder
31
+ from .socket import (
32
+ BooleanSocket,
33
+ BundleSocket,
34
+ ClosureSocket,
35
+ CollectionSocket,
36
+ ColorSocket,
37
+ FloatSocket,
38
+ GeometrySocket,
39
+ ImageSocket,
40
+ IntegerSocket,
41
+ MaterialSocket,
42
+ MatrixSocket,
43
+ MenuSocket,
44
+ ObjectSocket,
45
+ RotationSocket,
46
+ ShaderSocket,
47
+ Socket,
48
+ StringSocket,
49
+ VectorSocket,
50
+ _BooleanMixin,
51
+ _ColorMixin,
52
+ _IntegerMixin,
53
+ _MatrixMixin,
54
+ _RotationMixin,
55
+ _VectorMixin,
56
+ )
57
+ from .tree import (
58
+ InputInterfaceContext,
59
+ MaterialBuilder,
60
+ OutputInterfaceContext,
61
+ PanelContext,
62
+ SocketContext,
63
+ TreeBuilder,
64
+ )
65
+
66
+ # Backward-compatible aliases for hand-written code that uses the old names.
67
+ NodeBuilder = BaseNode
68
+ SocketLinker = Socket
69
+ SocketBase = InterfaceSocket
70
+
71
+ __all__ = [
72
+ # Core
73
+ "TreeBuilder",
74
+ "MaterialBuilder",
75
+ "BaseNode",
76
+ "Socket",
77
+ "SocketAccessor",
78
+ # Mixins
79
+ "OperatorMixin",
80
+ "LinkingMixin",
81
+ "DynamicInputsMixin",
82
+ # Node groups
83
+ "NodeGroupBuilder",
84
+ # Type-specific socket classes (runtime)
85
+ "FloatSocket",
86
+ "VectorSocket",
87
+ "ColorSocket",
88
+ "IntegerSocket",
89
+ "BooleanSocket",
90
+ "RotationSocket",
91
+ "MatrixSocket",
92
+ "StringSocket",
93
+ "MenuSocket",
94
+ "GeometrySocket",
95
+ "ObjectSocket",
96
+ "MaterialSocket",
97
+ "ImageSocket",
98
+ "CollectionSocket",
99
+ "BundleSocket",
100
+ "ClosureSocket",
101
+ "ShaderSocket",
102
+ # Type-specific behaviour mixins
103
+ "_VectorMixin",
104
+ "_ColorMixin",
105
+ "_IntegerMixin",
106
+ "_BooleanMixin",
107
+ "_RotationMixin",
108
+ "_MatrixMixin",
109
+ # Interface socket base
110
+ "InterfaceSocket",
111
+ # Interface socket types
112
+ "SocketFloat",
113
+ "SocketInteger",
114
+ "SocketBoolean",
115
+ "SocketVector",
116
+ "SocketColor",
117
+ "SocketRotation",
118
+ "SocketMatrix",
119
+ "SocketString",
120
+ "SocketMenu",
121
+ "SocketObject",
122
+ "SocketGeometry",
123
+ "SocketCollection",
124
+ "SocketImage",
125
+ "SocketMaterial",
126
+ "SocketBundle",
127
+ "SocketClosure",
128
+ "SocketShader",
129
+ # Tree context helpers
130
+ "SocketContext",
131
+ "PanelContext",
132
+ "InputInterfaceContext",
133
+ "OutputInterfaceContext",
134
+ # Utilities
135
+ "SocketError",
136
+ "normalize_name",
137
+ "denormalize_name",
138
+ # Backward-compatible aliases
139
+ "NodeBuilder",
140
+ "SocketLinker",
141
+ "SocketBase",
142
+ ]
@@ -0,0 +1,19 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING
4
+
5
+ from bpy.types import NodeSocket
6
+
7
+ if TYPE_CHECKING:
8
+ from .socket import Socket
9
+
10
+ _SOCKET_LINKER_REGISTRY: dict[str, "type[Socket]"] = {}
11
+
12
+
13
+ def _get_socket_linker(socket: NodeSocket) -> "Socket":
14
+ for key, cls in _SOCKET_LINKER_REGISTRY.items():
15
+ if key in socket.bl_idname:
16
+ return cls(socket)
17
+ from .socket import Socket
18
+
19
+ return Socket(socket)
@@ -0,0 +1,81 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ import bpy
6
+ from bpy.types import NodeSocket
7
+
8
+
9
+ class SocketError(Exception):
10
+ """Raised when a socket operation fails."""
11
+
12
+
13
+ # Type precedence for mixed-type operator dispatch (higher = dominant).
14
+ _TYPE_PRECEDENCE: dict[str, int] = {
15
+ "INT": 0,
16
+ "VALUE": 1,
17
+ "FLOAT": 1,
18
+ "VECTOR": 2,
19
+ }
20
+
21
+ GEO_NODE_NAMES = (
22
+ f"GeometryNode{name}"
23
+ for name in (
24
+ "SetPosition",
25
+ "TransformGeometry",
26
+ "GroupInput",
27
+ "GroupOutput",
28
+ "MeshToPoints",
29
+ "PointsToVertices",
30
+ )
31
+ )
32
+
33
+
34
+ def normalize_name(name: str) -> str:
35
+ """Convert 'Geometry' or 'My Socket' to 'geometry' or 'my_socket'."""
36
+ return name.lower().replace(" ", "_").replace("é", "e")
37
+
38
+
39
+ def denormalize_name(attr_name: str) -> str:
40
+ """Convert 'geometry' or 'my_socket' to 'Geometry' or 'My Socket'."""
41
+ return attr_name.replace("_", " ").title()
42
+
43
+
44
+ def _allow_innactive_sockets(node: bpy.types.Node) -> bool:
45
+ """Returns True if we should allow inactive sockets to be linked for this node type"""
46
+ return node.bl_idname in (
47
+ "GeometryNodeIndexSwitch",
48
+ "GeometryNodeMenuSwitch",
49
+ "ShaderNodeMixShader",
50
+ "GeometryNodeSwitch",
51
+ )
52
+
53
+
54
+ def _resolve_promotion(
55
+ self_socket: NodeSocket, other: Any, reverse: bool
56
+ ) -> "tuple[NodeSocket, Any, bool]":
57
+ """Determine the dominant socket for operator dispatch.
58
+
59
+ When both operands have a socket type, the higher-precedence type wins.
60
+ If `other` is dominant, the operands are swapped and `reverse` is flipped.
61
+
62
+ Returns (dominant_socket, effective_other, effective_reverse).
63
+ """
64
+ other_type = getattr(other, "type", None)
65
+ self_prec = _TYPE_PRECEDENCE.get(self_socket.type, 1)
66
+ other_prec = _TYPE_PRECEDENCE.get(other_type, -1) # type: ignore[arg-type]
67
+
68
+ if other_prec > self_prec:
69
+ # Other side is dominant — swap so the linker wraps the vector/higher socket
70
+ other_socket = other._default_output_socket
71
+ return other_socket, self_socket, not reverse
72
+
73
+ return self_socket, other, reverse
74
+
75
+
76
+ class _NodeLike:
77
+ """Marker base for objects that wrap a Blender node (have .node, .inputs, .outputs)."""
78
+
79
+
80
+ class _SocketLike:
81
+ """Marker base for objects that wrap a single Blender NodeSocket (have .socket)."""
@@ -0,0 +1,188 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING, Literal
4
+
5
+ import bpy
6
+ from bpy.types import NodeSocket
7
+
8
+ from ._registry import _get_socket_linker
9
+ from ._utils import SocketError, _allow_innactive_sockets, denormalize_name
10
+
11
+ if TYPE_CHECKING:
12
+ from .socket import Socket
13
+
14
+
15
+ class SocketAccessor:
16
+ """Unified accessor for a node's input or output socket collection.
17
+
18
+ Supports identifier/name lookup, dict-style ``[]`` access, availability
19
+ filtering, and type-compatible matching — replacing the former pairs of
20
+ ``_input_idx``/``_output_idx``, ``_input``/``_output``,
21
+ ``_available_inputs``/``_available_outputs``, and ``_best_output_socket``.
22
+ """
23
+
24
+ def __init__(
25
+ self,
26
+ collection: bpy.types.NodeInputs | bpy.types.NodeOutputs,
27
+ direction: Literal["input", "output"],
28
+ ):
29
+ self._direction = direction
30
+ self._collection = collection
31
+
32
+ def _index(self, key: str | int) -> int:
33
+ """Find socket index by identifier, falling back to name.
34
+
35
+ Tries identifier match first. If no identifier matches, falls back to
36
+ name lookup — but raises if the name is duplicated (ambiguous).
37
+ Integer keys are returned directly.
38
+ """
39
+ if isinstance(key, int):
40
+ return key
41
+ ids = [s.identifier for s in self._collection]
42
+ denorm = denormalize_name(key)
43
+ for candidate in (key, denorm):
44
+ if candidate in ids:
45
+ return ids.index(candidate)
46
+ names = [s.name for s in self._collection]
47
+ for key in (key, denorm):
48
+ if key in names:
49
+ if names.count(key) > 1:
50
+ raise RuntimeError(
51
+ f"{self._direction.title()} name '{key}' is ambiguous on "
52
+ f"{self._node.bl_idname} (appears {names.count(key)} times). "
53
+ f"Use the socket identifier instead."
54
+ )
55
+ return names.index(key)
56
+ raise RuntimeError(
57
+ f"{self._direction.title()} '{key}' not found on "
58
+ f"{self._node.bl_idname}. Available sockets (id: name): {list(zip(ids, names))}"
59
+ )
60
+
61
+ def _get(self, key: str | int) -> "Socket":
62
+ """Get a Socket for a socket by identifier, name, or index."""
63
+ return _get_socket_linker(self._collection[self._index(key)])
64
+
65
+ def __getitem__(self, key: str | int) -> "Socket":
66
+ """Access by identifier, name, or integer index."""
67
+ return self._get(key)
68
+
69
+ @property
70
+ def _node(self) -> bpy.types.Node:
71
+ """The node this accessor is associated with."""
72
+ if isinstance(self._collection, list):
73
+ return self._collection[0].node
74
+ # bpy NodeInputs/NodeOutputs.id_data returns the NodeTree (top-level ID),
75
+ # not the Node. Retrieve the node via the first socket instead.
76
+ for s in self._collection:
77
+ return s.node
78
+ return self._collection.data # empty collection fallback
79
+
80
+ @property
81
+ def _ignore_visibility(self) -> bool:
82
+ """Whether to ignore socket visibility when selecting available sockets.
83
+
84
+ Only affects ``available`` / ``best_match`` (the auto-linking heuristics).
85
+ ``values()`` / ``items()`` always respect node-level visibility so that
86
+ iteration over a node's sockets stays predictable regardless of context.
87
+ Returns False when called outside a tree context (e.g. from a bare
88
+ Socket that was created outside a ``with tree:`` block).
89
+ """
90
+ from .tree import TreeBuilder
91
+
92
+ if not TreeBuilder._tree_contexts:
93
+ return False
94
+ return TreeBuilder._tree_contexts[-1].ignore_visibility
95
+
96
+ def _visible_sockets(self) -> list[NodeSocket]:
97
+ """Sockets that should appear in iteration (values/items/keys).
98
+
99
+ Uses the per-node allowlist (``_allow_innactive_sockets``) rather than
100
+ the tree-level ``ignore_visibility`` flag — enumeration should always
101
+ reflect what is meaningfully present on the node, not the linking context.
102
+ """
103
+ if self._direction == "input":
104
+ return [
105
+ s
106
+ for s in self._collection
107
+ if _allow_innactive_sockets(self._node)
108
+ or (not s.is_inactive and s.is_icon_visible)
109
+ ]
110
+ return [s for s in self._collection if s.is_icon_visible]
111
+
112
+ @property
113
+ def _available(self) -> list[NodeSocket]:
114
+ """Sockets eligible for automatic linking.
115
+
116
+ Respects ``ignore_visibility`` on the active ``TreeBuilder`` context, so
117
+ nodes with normally-hidden sockets can still be auto-linked when that flag
118
+ is set (e.g. during ``test_add_all_nodes``).
119
+ """
120
+ if self._direction == "input":
121
+ return [
122
+ s
123
+ for s in self._collection
124
+ if (
125
+ self._ignore_visibility or (not s.is_inactive and s.is_icon_visible)
126
+ )
127
+ and (not s.links or s.is_multi_input)
128
+ ]
129
+ return [
130
+ s for s in self._collection if self._ignore_visibility or s.is_icon_visible
131
+ ]
132
+
133
+ def _best_match(self, socket_type: str) -> NodeSocket:
134
+ """Find the best compatible socket for the given type."""
135
+ from ..types import SOCKET_COMPATIBILITY
136
+
137
+ compatible = SOCKET_COMPATIBILITY.get(socket_type, ())
138
+ possible = [s for s in self._available if s.type in compatible]
139
+ if possible:
140
+ possible.sort(key=lambda x: compatible.index(x.type))
141
+ return possible[0]
142
+ raise SocketError(
143
+ f"No compatible {self._direction} socket found for type "
144
+ f"{socket_type} on {self._node.name}"
145
+ )
146
+
147
+ def _values(self) -> "list[Socket]":
148
+ """All visible sockets as Sockets.
149
+
150
+ Uses node-level visibility rules regardless of ``ignore_visibility`` —
151
+ see ``_visible_sockets`` for rationale.
152
+ """
153
+ return [_get_socket_linker(s) for s in self._visible_sockets()]
154
+
155
+ def _items(self) -> "list[tuple[str, Socket]]":
156
+ """All visible sockets as (name, Socket) pairs.
157
+
158
+ Uses node-level visibility rules regardless of ``ignore_visibility`` —
159
+ see ``_visible_sockets`` for rationale.
160
+ """
161
+ return [(s.name, _get_socket_linker(s)) for s in self._visible_sockets()]
162
+
163
+ def _keys(self) -> list[str]:
164
+ """All visible socket names."""
165
+ return [name for name, _ in self._items()]
166
+
167
+ def __len__(self) -> int:
168
+ return len(self._items())
169
+
170
+ def __iter__(self):
171
+ """Iterate over socket names (enables ``**node.outputs`` unpacking)."""
172
+ return iter(self._keys())
173
+
174
+ def __getattr__(self, name: str) -> "Socket":
175
+ """Dynamic socket access by normalised attribute name.
176
+
177
+ Converts ``node.o.base_color`` to ``self._get("Base Color")``.
178
+ Raises ``AttributeError`` (not ``RuntimeError``) so that ``hasattr``
179
+ and ``getattr(..., default)`` behave correctly.
180
+ """
181
+ if name.startswith("_"):
182
+ raise AttributeError(name)
183
+ try:
184
+ return self._get(name)
185
+ except RuntimeError:
186
+ raise AttributeError(
187
+ f"Socket '{name}' not found on {self._direction} accessor"
188
+ )