nodebpy 520.14.0__tar.gz → 520.15.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 (81) hide show
  1. {nodebpy-520.14.0 → nodebpy-520.15.0}/PKG-INFO +1 -1
  2. {nodebpy-520.14.0 → nodebpy-520.15.0}/pyproject.toml +1 -1
  3. {nodebpy-520.14.0 → nodebpy-520.15.0}/src/nodebpy/builder/__init__.py +2 -1
  4. {nodebpy-520.14.0 → nodebpy-520.15.0}/src/nodebpy/builder/items.py +25 -2
  5. {nodebpy-520.14.0 → nodebpy-520.15.0}/src/nodebpy/export/codegen.py +9 -1
  6. {nodebpy-520.14.0 → nodebpy-520.15.0}/src/nodebpy/nodes/compositor/manual.py +8 -8
  7. {nodebpy-520.14.0 → nodebpy-520.15.0}/src/nodebpy/nodes/geometry/manual.py +113 -48
  8. {nodebpy-520.14.0 → nodebpy-520.15.0}/src/nodebpy/nodes/shader/manual.py +10 -10
  9. {nodebpy-520.14.0 → nodebpy-520.15.0}/README.md +0 -0
  10. {nodebpy-520.14.0 → nodebpy-520.15.0}/src/nodebpy/__init__.py +0 -0
  11. {nodebpy-520.14.0 → nodebpy-520.15.0}/src/nodebpy/assets/__init__.py +0 -0
  12. {nodebpy-520.14.0 → nodebpy-520.15.0}/src/nodebpy/assets/__main__.py +0 -0
  13. {nodebpy-520.14.0 → nodebpy-520.15.0}/src/nodebpy/assets/_codegen.py +0 -0
  14. {nodebpy-520.14.0 → nodebpy-520.15.0}/src/nodebpy/builder/_registry.py +0 -0
  15. {nodebpy-520.14.0 → nodebpy-520.15.0}/src/nodebpy/builder/_utils.py +0 -0
  16. {nodebpy-520.14.0 → nodebpy-520.15.0}/src/nodebpy/builder/accessor.py +0 -0
  17. {nodebpy-520.14.0 → nodebpy-520.15.0}/src/nodebpy/builder/arrange.py +0 -0
  18. {nodebpy-520.14.0 → nodebpy-520.15.0}/src/nodebpy/builder/asset.py +0 -0
  19. {nodebpy-520.14.0 → nodebpy-520.15.0}/src/nodebpy/builder/mixins.py +0 -0
  20. {nodebpy-520.14.0 → nodebpy-520.15.0}/src/nodebpy/builder/node.py +0 -0
  21. {nodebpy-520.14.0 → nodebpy-520.15.0}/src/nodebpy/builder/socket.py +0 -0
  22. {nodebpy-520.14.0 → nodebpy-520.15.0}/src/nodebpy/builder/tree.py +0 -0
  23. {nodebpy-520.14.0 → nodebpy-520.15.0}/src/nodebpy/export/__init__.py +0 -0
  24. {nodebpy-520.14.0 → nodebpy-520.15.0}/src/nodebpy/export/diagram.py +0 -0
  25. {nodebpy-520.14.0 → nodebpy-520.15.0}/src/nodebpy/export/web_render.py +0 -0
  26. {nodebpy-520.14.0 → nodebpy-520.15.0}/src/nodebpy/lib/nodearrange/__init__.py +0 -0
  27. {nodebpy-520.14.0 → nodebpy-520.15.0}/src/nodebpy/lib/nodearrange/arrange/__init__.py +0 -0
  28. {nodebpy-520.14.0 → nodebpy-520.15.0}/src/nodebpy/lib/nodearrange/arrange/graph.py +0 -0
  29. {nodebpy-520.14.0 → nodebpy-520.15.0}/src/nodebpy/lib/nodearrange/arrange/ordering.py +0 -0
  30. {nodebpy-520.14.0 → nodebpy-520.15.0}/src/nodebpy/lib/nodearrange/arrange/ranking.py +0 -0
  31. {nodebpy-520.14.0 → nodebpy-520.15.0}/src/nodebpy/lib/nodearrange/arrange/realize.py +0 -0
  32. {nodebpy-520.14.0 → nodebpy-520.15.0}/src/nodebpy/lib/nodearrange/arrange/stacking.py +0 -0
  33. {nodebpy-520.14.0 → nodebpy-520.15.0}/src/nodebpy/lib/nodearrange/arrange/structs.py +0 -0
  34. {nodebpy-520.14.0 → nodebpy-520.15.0}/src/nodebpy/lib/nodearrange/arrange/sugiyama.py +0 -0
  35. {nodebpy-520.14.0 → nodebpy-520.15.0}/src/nodebpy/lib/nodearrange/arrange/x_coords.py +0 -0
  36. {nodebpy-520.14.0 → nodebpy-520.15.0}/src/nodebpy/lib/nodearrange/arrange/y_coords.py +0 -0
  37. {nodebpy-520.14.0 → nodebpy-520.15.0}/src/nodebpy/lib/nodearrange/config.py +0 -0
  38. {nodebpy-520.14.0 → nodebpy-520.15.0}/src/nodebpy/lib/nodearrange/utils.py +0 -0
  39. {nodebpy-520.14.0 → nodebpy-520.15.0}/src/nodebpy/nodes/__init__.py +0 -0
  40. {nodebpy-520.14.0 → nodebpy-520.15.0}/src/nodebpy/nodes/_mixins.py +0 -0
  41. {nodebpy-520.14.0 → nodebpy-520.15.0}/src/nodebpy/nodes/compositor/__init__.py +0 -0
  42. {nodebpy-520.14.0 → nodebpy-520.15.0}/src/nodebpy/nodes/compositor/assets.py +0 -0
  43. {nodebpy-520.14.0 → nodebpy-520.15.0}/src/nodebpy/nodes/compositor/color.py +0 -0
  44. {nodebpy-520.14.0 → nodebpy-520.15.0}/src/nodebpy/nodes/compositor/converter.py +0 -0
  45. {nodebpy-520.14.0 → nodebpy-520.15.0}/src/nodebpy/nodes/compositor/distort.py +0 -0
  46. {nodebpy-520.14.0 → nodebpy-520.15.0}/src/nodebpy/nodes/compositor/filter.py +0 -0
  47. {nodebpy-520.14.0 → nodebpy-520.15.0}/src/nodebpy/nodes/compositor/group.py +0 -0
  48. {nodebpy-520.14.0 → nodebpy-520.15.0}/src/nodebpy/nodes/compositor/input.py +0 -0
  49. {nodebpy-520.14.0 → nodebpy-520.15.0}/src/nodebpy/nodes/compositor/interface.py +0 -0
  50. {nodebpy-520.14.0 → nodebpy-520.15.0}/src/nodebpy/nodes/compositor/matte.py +0 -0
  51. {nodebpy-520.14.0 → nodebpy-520.15.0}/src/nodebpy/nodes/compositor/output.py +0 -0
  52. {nodebpy-520.14.0 → nodebpy-520.15.0}/src/nodebpy/nodes/compositor/vector.py +0 -0
  53. {nodebpy-520.14.0 → nodebpy-520.15.0}/src/nodebpy/nodes/geometry/__init__.py +0 -0
  54. {nodebpy-520.14.0 → nodebpy-520.15.0}/src/nodebpy/nodes/geometry/assets.py +0 -0
  55. {nodebpy-520.14.0 → nodebpy-520.15.0}/src/nodebpy/nodes/geometry/attribute.py +0 -0
  56. {nodebpy-520.14.0 → nodebpy-520.15.0}/src/nodebpy/nodes/geometry/color.py +0 -0
  57. {nodebpy-520.14.0 → nodebpy-520.15.0}/src/nodebpy/nodes/geometry/converter.py +0 -0
  58. {nodebpy-520.14.0 → nodebpy-520.15.0}/src/nodebpy/nodes/geometry/geometry.py +0 -0
  59. {nodebpy-520.14.0 → nodebpy-520.15.0}/src/nodebpy/nodes/geometry/grid.py +0 -0
  60. {nodebpy-520.14.0 → nodebpy-520.15.0}/src/nodebpy/nodes/geometry/group.py +0 -0
  61. {nodebpy-520.14.0 → nodebpy-520.15.0}/src/nodebpy/nodes/geometry/groups.py +0 -0
  62. {nodebpy-520.14.0 → nodebpy-520.15.0}/src/nodebpy/nodes/geometry/input.py +0 -0
  63. {nodebpy-520.14.0 → nodebpy-520.15.0}/src/nodebpy/nodes/geometry/interface.py +0 -0
  64. {nodebpy-520.14.0 → nodebpy-520.15.0}/src/nodebpy/nodes/geometry/output.py +0 -0
  65. {nodebpy-520.14.0 → nodebpy-520.15.0}/src/nodebpy/nodes/geometry/texture.py +0 -0
  66. {nodebpy-520.14.0 → nodebpy-520.15.0}/src/nodebpy/nodes/geometry/utilities.py +0 -0
  67. {nodebpy-520.14.0 → nodebpy-520.15.0}/src/nodebpy/nodes/geometry/vector.py +0 -0
  68. {nodebpy-520.14.0 → nodebpy-520.15.0}/src/nodebpy/nodes/geometry/zone.py +0 -0
  69. {nodebpy-520.14.0 → nodebpy-520.15.0}/src/nodebpy/nodes/shader/__init__.py +0 -0
  70. {nodebpy-520.14.0 → nodebpy-520.15.0}/src/nodebpy/nodes/shader/assets.py +0 -0
  71. {nodebpy-520.14.0 → nodebpy-520.15.0}/src/nodebpy/nodes/shader/color.py +0 -0
  72. {nodebpy-520.14.0 → nodebpy-520.15.0}/src/nodebpy/nodes/shader/converter.py +0 -0
  73. {nodebpy-520.14.0 → nodebpy-520.15.0}/src/nodebpy/nodes/shader/grid.py +0 -0
  74. {nodebpy-520.14.0 → nodebpy-520.15.0}/src/nodebpy/nodes/shader/group.py +0 -0
  75. {nodebpy-520.14.0 → nodebpy-520.15.0}/src/nodebpy/nodes/shader/input.py +0 -0
  76. {nodebpy-520.14.0 → nodebpy-520.15.0}/src/nodebpy/nodes/shader/output.py +0 -0
  77. {nodebpy-520.14.0 → nodebpy-520.15.0}/src/nodebpy/nodes/shader/script.py +0 -0
  78. {nodebpy-520.14.0 → nodebpy-520.15.0}/src/nodebpy/nodes/shader/shader.py +0 -0
  79. {nodebpy-520.14.0 → nodebpy-520.15.0}/src/nodebpy/nodes/shader/texture.py +0 -0
  80. {nodebpy-520.14.0 → nodebpy-520.15.0}/src/nodebpy/nodes/shader/vector.py +0 -0
  81. {nodebpy-520.14.0 → nodebpy-520.15.0}/src/nodebpy/types.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: nodebpy
3
- Version: 520.14.0
3
+ Version: 520.15.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>
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "nodebpy"
3
- version = "520.14.0"
3
+ version = "520.15.0"
4
4
  description = "Build nodes trees in Blender more elegantly with code"
5
5
  readme = "README.md"
6
6
  authors = [
@@ -9,7 +9,7 @@ from .asset import (
9
9
  PackageLibrary,
10
10
  asset_group_base,
11
11
  )
12
- from .items import Item, ItemsMixin
12
+ from .items import Item, ItemsMixin, MenuItem
13
13
  from .mixins import LinkingMixin, OperatorMixin
14
14
  from .node import (
15
15
  BaseNode,
@@ -118,6 +118,7 @@ __all__ = [
118
118
  "MaterialSocketList",
119
119
  "MatrixSocket",
120
120
  "MatrixSocketList",
121
+ "MenuItem",
121
122
  "MenuSocket",
122
123
  "MenuSocketList",
123
124
  "NodeGroupBuilder",
@@ -10,7 +10,7 @@ from ..types import _is_default_value
10
10
  from ._registry import _wrap_socket
11
11
  from ._utils import _SocketLike
12
12
  from .node import DynamicInputsMixin
13
- from .socket import Socket
13
+ from .socket import BaseSocket, Socket
14
14
 
15
15
  if TYPE_CHECKING:
16
16
  from ..types import (
@@ -96,7 +96,7 @@ def _infer_value_type(value: Any) -> str | None:
96
96
  return None
97
97
 
98
98
 
99
- _SocketT = TypeVar("_SocketT", bound=Socket, default=Socket)
99
+ _SocketT = TypeVar("_SocketT", bound=BaseSocket, default=Socket)
100
100
 
101
101
 
102
102
  class Item(Generic[_SocketT]):
@@ -152,6 +152,29 @@ class Item(Generic[_SocketT]):
152
152
  )
153
153
 
154
154
 
155
+ class MenuItem(Item[_SocketT]):
156
+ """Handle for one Menu Switch enum item.
157
+
158
+ ``input`` is the item's input socket (typed to the switch's data
159
+ type), ``is_selected`` the item's boolean output socket, and
160
+ ``description`` the tooltip Blender shows for the item in the menu.
161
+ """
162
+
163
+ @property
164
+ def is_selected(self) -> BooleanSocket:
165
+ """Boolean output socket — True when this item is selected."""
166
+ return cast("BooleanSocket", self.output)
167
+
168
+ @property
169
+ def description(self) -> str:
170
+ """Tooltip shown for this item in the drop-down menu."""
171
+ return self._item.description
172
+
173
+ @description.setter
174
+ def description(self, value: str) -> None:
175
+ self._item.description = value
176
+
177
+
155
178
  class ItemsMixin(DynamicInputsMixin):
156
179
  """Socket machinery for nodes whose sockets are driven by a bpy item
157
180
  collection (``capture_items``, ``bake_items``, ``format_items``, ...).
@@ -3904,6 +3904,7 @@ def _emit_menu_switch(node, ctx: EmitContext) -> Expr | _Val | None:
3904
3904
  """MenuSwitch emits the factory dict form
3905
3905
  ``g.MenuSwitch.geometry(menu, {"Name": value, ...})`` — the plain
3906
3906
  constructor's per-socket kwargs cannot recreate the enum item names.
3907
+ An item with a tooltip emits the ``(value, description)`` pair form.
3907
3908
  Each tree type has its own MenuSwitch class (with tree-specific factories
3908
3909
  such as ``shader``), so the alias follows the tree being exported."""
3909
3910
  factory = _SWITCH_FACTORY_NAMES.get(node.data_type)
@@ -3911,7 +3912,14 @@ def _emit_menu_switch(node, ctx: EmitContext) -> Expr | _Val | None:
3911
3912
  return None
3912
3913
  alias = _TREE_ALIAS.get(ctx.node_tree.bl_idname, "g")
3913
3914
  ctx.used_aliases.add(alias)
3914
- items = DictExpr(dict(_switch_item_exprs(node, ctx, "Menu")))
3915
+ item_exprs: dict[str, Expr] = {}
3916
+ for (name, expr), item in zip(
3917
+ _switch_item_exprs(node, ctx, "Menu"), node.enum_items
3918
+ ):
3919
+ if item.description:
3920
+ expr = TupleExpr([expr, Lit(item.description)])
3921
+ item_exprs[name] = expr
3922
+ items = DictExpr(item_exprs)
3915
3923
  menu_link = ctx.input_link(node, "Menu")
3916
3924
  if menu_link is not None:
3917
3925
  return Call(
@@ -30,7 +30,7 @@ from ...types import (
30
30
  InputString,
31
31
  InputVector,
32
32
  )
33
- from ..geometry.manual import Float, Frame, _MenuSwitchBase
33
+ from ..geometry.manual import Float, Frame, MenuItems, _MenuSwitchBase
34
34
 
35
35
  __all__ = ["Float", "Frame", "MenuSwitch", "tree"]
36
36
 
@@ -52,43 +52,43 @@ class MenuSwitch[T: BaseSocket](_MenuSwitchBase[T]):
52
52
 
53
53
  @classmethod
54
54
  def float(
55
- cls, menu: InputMenu = None, items: dict[str, InputFloat] | None = None
55
+ cls, menu: InputMenu = None, items: MenuItems[InputFloat] | None = None
56
56
  ) -> "MenuSwitch[FloatSocket]":
57
57
  return MenuSwitch(menu, items, data_type="FLOAT")
58
58
 
59
59
  @classmethod
60
60
  def integer(
61
- cls, menu: InputMenu = None, items: dict[str, InputInteger] | None = None
61
+ cls, menu: InputMenu = None, items: MenuItems[InputInteger] | None = None
62
62
  ) -> "MenuSwitch[IntegerSocket]":
63
63
  return MenuSwitch(menu, items, data_type="INT")
64
64
 
65
65
  @classmethod
66
66
  def boolean(
67
- cls, menu: InputMenu = None, items: dict[str, InputBoolean] | None = None
67
+ cls, menu: InputMenu = None, items: MenuItems[InputBoolean] | None = None
68
68
  ) -> "MenuSwitch[BooleanSocket]":
69
69
  return MenuSwitch(menu, items, data_type="BOOLEAN")
70
70
 
71
71
  @classmethod
72
72
  def vector(
73
- cls, menu: InputMenu = None, items: dict[str, InputVector] | None = None
73
+ cls, menu: InputMenu = None, items: MenuItems[InputVector] | None = None
74
74
  ) -> "MenuSwitch[VectorSocket]":
75
75
  return MenuSwitch(menu, items, data_type="VECTOR")
76
76
 
77
77
  @classmethod
78
78
  def color(
79
- cls, menu: InputMenu = None, items: dict[str, InputColor] | None = None
79
+ cls, menu: InputMenu = None, items: MenuItems[InputColor] | None = None
80
80
  ) -> "MenuSwitch[ColorSocket]":
81
81
  return MenuSwitch(menu, items, data_type="RGBA")
82
82
 
83
83
  @classmethod
84
84
  def string(
85
- cls, menu: InputMenu = None, items: dict[str, InputString] | None = None
85
+ cls, menu: InputMenu = None, items: MenuItems[InputString] | None = None
86
86
  ) -> "MenuSwitch[StringSocket]":
87
87
  return MenuSwitch(menu, items, data_type="STRING")
88
88
 
89
89
  @classmethod
90
90
  def menu(
91
- cls, menu: InputMenu = None, items: dict[str, InputMenu] | None = None
91
+ cls, menu: InputMenu = None, items: MenuItems[InputMenu] | None = None
92
92
  ) -> "MenuSwitch[MenuSocket]":
93
93
  return MenuSwitch(menu, items, data_type="MENU")
94
94
 
@@ -51,6 +51,7 @@ from ...builder import Socket as SocketLinker
51
51
  from ...builder._registry import _wrap_socket
52
52
  from ...builder.items import (
53
53
  GridItem,
54
+ MenuItem,
54
55
  _apply_item_value,
55
56
  _FieldItemFactory,
56
57
  _socket_for_item,
@@ -128,6 +129,9 @@ __all__ = (
128
129
  "IndexSwitch",
129
130
  "JoinGeometry",
130
131
  "JoinStrings",
132
+ # "MenuItems" is deliberately not exported: quartodoc cannot document a
133
+ # PEP 695 `type` alias (griffe Kind.TYPE_ALIAS); import it from this
134
+ # module directly for annotations.
131
135
  "MenuSwitch",
132
136
  "MeshBoolean",
133
137
  "RepeatInput",
@@ -1501,6 +1505,22 @@ class IndexSwitch[T: BaseSocket](ItemsMixin, BaseNode):
1501
1505
  self.node.data_type = value
1502
1506
 
1503
1507
 
1508
+ type MenuItems[V] = Mapping[str, None | V | tuple[V | None, str]]
1509
+ """Menu Switch ``items`` mapping: item name → value, where the value may be
1510
+ a ``(value, description)`` pair to also set the item's tooltip."""
1511
+
1512
+
1513
+ def _split_menu_item(
1514
+ value: InputAny | tuple[InputAny, str],
1515
+ ) -> tuple[InputAny, str | None]:
1516
+ """Split a ``(value, description)`` menu item pair. A 2-tuple whose
1517
+ second element is a string can never be a socket default value, so the
1518
+ form is unambiguous."""
1519
+ if isinstance(value, tuple) and len(value) == 2 and isinstance(value[1], str):
1520
+ return value[0], value[1]
1521
+ return cast("InputAny", value), None
1522
+
1523
+
1504
1524
  class _MenuSwitchBase[T: BaseSocket](ItemsMixin, BaseNode):
1505
1525
  """Base class for MenuSwitch nodes across all tree types."""
1506
1526
 
@@ -1525,30 +1545,42 @@ class _MenuSwitchBase[T: BaseSocket](ItemsMixin, BaseNode):
1525
1545
  def __init__(
1526
1546
  self,
1527
1547
  menu: InputMenu = None,
1528
- items: Mapping[str, InputAny] | None = None,
1548
+ items: MenuItems[InputAny] | None = None,
1529
1549
  *,
1530
1550
  data_type: SOCKET_TYPES = "FLOAT",
1531
1551
  ):
1532
1552
  super().__init__()
1533
1553
  self.data_type = data_type
1534
1554
  self.node.enum_items.clear()
1535
- key_args = {"Menu": menu}
1555
+ # a plain string `menu` is an explicit selection; otherwise the
1556
+ # selection defaults to the first item
1557
+ self._explicit_selection = isinstance(menu, str)
1536
1558
  self._link_args(**(items or {}))
1537
- self._establish_links(**key_args)
1538
- # a plain string `menu` is an explicit selection; otherwise default
1539
- # the selection to the first item
1540
-
1541
- if self.node.enum_items and not isinstance(menu, str):
1559
+ if isinstance(menu, str) and not self.node.enum_items:
1560
+ # a selection named before any items exist (they will be added via
1561
+ # item()) can't be set yet; defer it to context exit
1542
1562
  assert self.node.inputs is not None
1543
- try:
1544
- menu_socket = self.node.inputs["Menu"]
1545
- menu_socket.default_value = self.node.enum_items[0].name
1546
- except TypeError: # pragma: no cover - rare Blender enum-refresh quirk
1547
- # the socket is a NodeSocketMenu whose enum hasn't refreshed yet, so
1548
- # the default_value isn't settable here; defer it to context exit.
1549
- self.tree._menu_defaults.append(
1550
- _MenuDefault(self.node.inputs["Menu"], self.node.enum_items[0].name)
1551
- )
1563
+ self.tree._menu_defaults.append(
1564
+ _MenuDefault(self.node.inputs["Menu"], menu)
1565
+ )
1566
+ else:
1567
+ self._establish_links(Menu=menu)
1568
+
1569
+ if self.node.enum_items and not self._explicit_selection:
1570
+ self._default_selection_to_first()
1571
+
1572
+ def _default_selection_to_first(self) -> None:
1573
+ """Default the menu selection to the first enum item."""
1574
+ assert self.node.inputs is not None
1575
+ try:
1576
+ menu_socket = self.node.inputs["Menu"]
1577
+ menu_socket.default_value = self.node.enum_items[0].name
1578
+ except TypeError: # pragma: no cover - rare Blender enum-refresh quirk
1579
+ # the socket is a NodeSocketMenu whose enum hasn't refreshed yet, so
1580
+ # the default_value isn't settable here; defer it to context exit.
1581
+ self.tree._menu_defaults.append(
1582
+ _MenuDefault(self.node.inputs["Menu"], self.node.enum_items[0].name)
1583
+ )
1552
1584
 
1553
1585
  @property
1554
1586
  def _socket_data_types(self) -> tuple[str, ...]:
@@ -1560,24 +1592,57 @@ class _MenuSwitchBase[T: BaseSocket](ItemsMixin, BaseNode):
1560
1592
  # menu items are untyped; .new() takes only a name
1561
1593
  return self._items.new(name)
1562
1594
 
1563
- def _link_args(self, **kwargs: InputAny):
1595
+ def _link_args(self, **kwargs: InputAny | tuple[InputAny, str]):
1564
1596
  for key, value in kwargs.items():
1565
- socket = self._add_socket(name=key, type=self.data_type)
1566
- if value is None:
1567
- continue # item declared but left unlinked
1568
- if _is_default_value(value):
1569
- if isinstance(socket, bpy.types.NodeSocketMenu) and isinstance(
1570
- value, str
1571
- ):
1572
- # the socket is a NodeSocketMenu, but the default_value is not settable
1573
- # until the full tree is built and menu items are known. We need to defer
1574
- # the setting of the default values until after tree construction.
1575
- self.tree._menu_defaults.append(_MenuDefault(socket, value))
1576
- else:
1577
- socket.default_value = value # ty: ignore[unresolved-attribute]
1597
+ value, description = _split_menu_item(value)
1598
+ item = self._new_item(key, self.data_type)
1599
+ if description:
1600
+ item.description = description
1601
+ self._apply_item_input(self._item_socket(item), value)
1602
+
1603
+ def _apply_item_input(self, socket: NodeSocket, value: InputAny) -> None:
1604
+ if value is None:
1605
+ return # item declared but left unlinked
1606
+ if _is_default_value(value):
1607
+ if isinstance(socket, bpy.types.NodeSocketMenu) and isinstance(value, str):
1608
+ # the socket is a NodeSocketMenu, but the default_value is not settable
1609
+ # until the full tree is built and menu items are known. We need to defer
1610
+ # the setting of the default values until after tree construction.
1611
+ self.tree._menu_defaults.append(_MenuDefault(socket, value))
1578
1612
  else:
1579
- source = self._source_socket(value) # type: ignore
1580
- self._link(source, socket)
1613
+ socket.default_value = value # ty: ignore[unresolved-attribute]
1614
+ else:
1615
+ source = self._source_socket(value) # type: ignore
1616
+ self._link(source, socket)
1617
+
1618
+ def item(
1619
+ self,
1620
+ name: str,
1621
+ value: InputAny | tuple[InputAny, str] = None,
1622
+ *,
1623
+ description: str | None = None,
1624
+ ) -> MenuItem[T]:
1625
+ """Declare a menu item and return its handle.
1626
+
1627
+ ``value`` may be a linkable (linked into the item's input socket), a
1628
+ plain default value, or a ``(value, description)`` pair; omit it to
1629
+ declare the item unlinked. ``description`` sets the tooltip Blender
1630
+ shows for the item in the menu.
1631
+
1632
+ Unless the menu selection was set explicitly, declaring the first
1633
+ item also defaults the selection to it (as the constructor does).
1634
+ """
1635
+ value, pair_description = _split_menu_item(value)
1636
+ if description is None:
1637
+ description = pair_description
1638
+ bpy_item = self._new_item(name, self.data_type)
1639
+ if description:
1640
+ bpy_item.description = description
1641
+ handle = cast("MenuItem[T]", MenuItem(self, bpy_item))
1642
+ self._apply_item_input(self._item_socket(bpy_item), value)
1643
+ if len(self.node.enum_items) == 1 and not self._explicit_selection:
1644
+ self._default_selection_to_first()
1645
+ return handle
1581
1646
 
1582
1647
  def is_selected(self, name: str) -> BooleanSocket:
1583
1648
  """Gets the boolean output socket that is True when the named menu item is selected.
@@ -1616,7 +1681,7 @@ class MenuSwitch[T: BaseSocket](_MenuSwitchBase[T]):
1616
1681
  def float(
1617
1682
  cls,
1618
1683
  menu: InputMenu = None,
1619
- items: dict[str, InputFloat] | None = None,
1684
+ items: MenuItems[InputFloat] | None = None,
1620
1685
  ) -> "MenuSwitch[FloatSocket]":
1621
1686
  return MenuSwitch(menu, items, data_type="FLOAT")
1622
1687
 
@@ -1624,7 +1689,7 @@ class MenuSwitch[T: BaseSocket](_MenuSwitchBase[T]):
1624
1689
  def integer(
1625
1690
  cls,
1626
1691
  menu: InputMenu = None,
1627
- items: dict[str, InputInteger] | None = None,
1692
+ items: MenuItems[InputInteger] | None = None,
1628
1693
  ) -> "MenuSwitch[IntegerSocket]":
1629
1694
  return MenuSwitch(menu, items, data_type="INT")
1630
1695
 
@@ -1632,7 +1697,7 @@ class MenuSwitch[T: BaseSocket](_MenuSwitchBase[T]):
1632
1697
  def boolean(
1633
1698
  cls,
1634
1699
  menu: InputMenu = None,
1635
- items: dict[str, InputBoolean] | None = None,
1700
+ items: MenuItems[InputBoolean] | None = None,
1636
1701
  ) -> "MenuSwitch[BooleanSocket]":
1637
1702
  return MenuSwitch(menu, items, data_type="BOOLEAN")
1638
1703
 
@@ -1640,7 +1705,7 @@ class MenuSwitch[T: BaseSocket](_MenuSwitchBase[T]):
1640
1705
  def vector(
1641
1706
  cls,
1642
1707
  menu: InputMenu = None,
1643
- items: dict[str, InputVector] | None = None,
1708
+ items: MenuItems[InputVector] | None = None,
1644
1709
  ) -> "MenuSwitch[VectorSocket]":
1645
1710
  return MenuSwitch(menu, items, data_type="VECTOR")
1646
1711
 
@@ -1648,7 +1713,7 @@ class MenuSwitch[T: BaseSocket](_MenuSwitchBase[T]):
1648
1713
  def color(
1649
1714
  cls,
1650
1715
  menu: InputMenu = None,
1651
- items: dict[str, InputColor] | None = None,
1716
+ items: MenuItems[InputColor] | None = None,
1652
1717
  ) -> "MenuSwitch[ColorSocket]":
1653
1718
  return MenuSwitch(menu, items, data_type="RGBA")
1654
1719
 
@@ -1656,7 +1721,7 @@ class MenuSwitch[T: BaseSocket](_MenuSwitchBase[T]):
1656
1721
  def rotation(
1657
1722
  cls,
1658
1723
  menu: InputMenu = None,
1659
- items: dict[str, InputRotation] | None = None,
1724
+ items: MenuItems[InputRotation] | None = None,
1660
1725
  ) -> "MenuSwitch[RotationSocket]":
1661
1726
  return MenuSwitch(menu, items, data_type="ROTATION")
1662
1727
 
@@ -1664,7 +1729,7 @@ class MenuSwitch[T: BaseSocket](_MenuSwitchBase[T]):
1664
1729
  def matrix(
1665
1730
  cls,
1666
1731
  menu: InputMenu = None,
1667
- items: dict[str, InputMatrix] | None = None,
1732
+ items: MenuItems[InputMatrix] | None = None,
1668
1733
  ) -> "MenuSwitch[MatrixSocket]":
1669
1734
  return MenuSwitch(menu, items, data_type="MATRIX")
1670
1735
 
@@ -1672,7 +1737,7 @@ class MenuSwitch[T: BaseSocket](_MenuSwitchBase[T]):
1672
1737
  def string(
1673
1738
  cls,
1674
1739
  menu: InputMenu = None,
1675
- items: dict[str, InputString] | None = None,
1740
+ items: MenuItems[InputString] | None = None,
1676
1741
  ) -> "MenuSwitch[StringSocket]":
1677
1742
  return MenuSwitch(menu, items, data_type="STRING")
1678
1743
 
@@ -1680,7 +1745,7 @@ class MenuSwitch[T: BaseSocket](_MenuSwitchBase[T]):
1680
1745
  def menu(
1681
1746
  cls,
1682
1747
  menu: InputMenu = None,
1683
- items: dict[str, InputMenu] | None = None,
1748
+ items: MenuItems[InputMenu] | None = None,
1684
1749
  ) -> "MenuSwitch[MenuSocket]":
1685
1750
  return MenuSwitch(menu, items, data_type="MENU")
1686
1751
 
@@ -1688,7 +1753,7 @@ class MenuSwitch[T: BaseSocket](_MenuSwitchBase[T]):
1688
1753
  def object(
1689
1754
  cls,
1690
1755
  menu: InputMenu = None,
1691
- items: dict[str, InputObject] | None = None,
1756
+ items: MenuItems[InputObject] | None = None,
1692
1757
  ) -> "MenuSwitch[ObjectSocket]":
1693
1758
  return MenuSwitch(menu, items, data_type="OBJECT")
1694
1759
 
@@ -1696,7 +1761,7 @@ class MenuSwitch[T: BaseSocket](_MenuSwitchBase[T]):
1696
1761
  def geometry(
1697
1762
  cls,
1698
1763
  menu: InputMenu = None,
1699
- items: dict[str, InputGeometry] | None = None,
1764
+ items: MenuItems[InputGeometry] | None = None,
1700
1765
  ) -> "MenuSwitch[GeometrySocket]":
1701
1766
  return MenuSwitch(menu, items, data_type="GEOMETRY")
1702
1767
 
@@ -1704,7 +1769,7 @@ class MenuSwitch[T: BaseSocket](_MenuSwitchBase[T]):
1704
1769
  def collection(
1705
1770
  cls,
1706
1771
  menu: InputMenu = None,
1707
- items: dict[str, InputCollection] | None = None,
1772
+ items: MenuItems[InputCollection] | None = None,
1708
1773
  ) -> "MenuSwitch[CollectionSocket]":
1709
1774
  return MenuSwitch(menu, items, data_type="COLLECTION")
1710
1775
 
@@ -1712,7 +1777,7 @@ class MenuSwitch[T: BaseSocket](_MenuSwitchBase[T]):
1712
1777
  def image(
1713
1778
  cls,
1714
1779
  menu: InputMenu = None,
1715
- items: dict[str, InputImage] | None = None,
1780
+ items: MenuItems[InputImage] | None = None,
1716
1781
  ) -> "MenuSwitch[ImageSocket]":
1717
1782
  return MenuSwitch(menu, items, data_type="IMAGE")
1718
1783
 
@@ -1720,7 +1785,7 @@ class MenuSwitch[T: BaseSocket](_MenuSwitchBase[T]):
1720
1785
  def material(
1721
1786
  cls,
1722
1787
  menu: InputMenu = None,
1723
- items: dict[str, InputMaterial] | None = None,
1788
+ items: MenuItems[InputMaterial] | None = None,
1724
1789
  ) -> "MenuSwitch[MaterialSocket]":
1725
1790
  return MenuSwitch(menu, items, data_type="MATERIAL")
1726
1791
 
@@ -1728,7 +1793,7 @@ class MenuSwitch[T: BaseSocket](_MenuSwitchBase[T]):
1728
1793
  def bundle(
1729
1794
  cls,
1730
1795
  menu: InputMenu = None,
1731
- items: dict[str, InputBundle] | None = None,
1796
+ items: MenuItems[InputBundle] | None = None,
1732
1797
  ) -> "MenuSwitch[BundleSocket]":
1733
1798
  return MenuSwitch(menu, items, data_type="BUNDLE")
1734
1799
 
@@ -1736,7 +1801,7 @@ class MenuSwitch[T: BaseSocket](_MenuSwitchBase[T]):
1736
1801
  def closure(
1737
1802
  cls,
1738
1803
  menu: InputMenu = None,
1739
- items: dict[str, InputClosure] | None = None,
1804
+ items: MenuItems[InputClosure] | None = None,
1740
1805
  ) -> "MenuSwitch[ClosureSocket]":
1741
1806
  return MenuSwitch(menu, items, data_type="CLOSURE")
1742
1807
 
@@ -30,7 +30,7 @@ from ...types import (
30
30
  InputVector,
31
31
  )
32
32
  from ..geometry import Frame, RepeatInput, RepeatOutput, RepeatZone
33
- from ..geometry.manual import Float, _MenuSwitchBase
33
+ from ..geometry.manual import Float, MenuItems, _MenuSwitchBase
34
34
 
35
35
  __all__ = [
36
36
  "Attribute",
@@ -74,55 +74,55 @@ class MenuSwitch[T: BaseSocket](_MenuSwitchBase[T]):
74
74
 
75
75
  @classmethod
76
76
  def float(
77
- cls, menu: InputMenu = None, items: dict[str, InputFloat] | None = None
77
+ cls, menu: InputMenu = None, items: MenuItems[InputFloat] | None = None
78
78
  ) -> "MenuSwitch[FloatSocket]":
79
79
  return MenuSwitch(menu, items, data_type="FLOAT")
80
80
 
81
81
  @classmethod
82
82
  def integer(
83
- cls, menu: InputMenu = None, items: dict[str, InputInteger] | None = None
83
+ cls, menu: InputMenu = None, items: MenuItems[InputInteger] | None = None
84
84
  ) -> "MenuSwitch[IntegerSocket]":
85
85
  return MenuSwitch(menu, items, data_type="INT")
86
86
 
87
87
  @classmethod
88
88
  def boolean(
89
- cls, menu: InputMenu = None, items: dict[str, InputBoolean] | None = None
89
+ cls, menu: InputMenu = None, items: MenuItems[InputBoolean] | None = None
90
90
  ) -> "MenuSwitch[BooleanSocket]":
91
91
  return MenuSwitch(menu, items, data_type="BOOLEAN")
92
92
 
93
93
  @classmethod
94
94
  def vector(
95
- cls, menu: InputMenu = None, items: dict[str, InputVector] | None = None
95
+ cls, menu: InputMenu = None, items: MenuItems[InputVector] | None = None
96
96
  ) -> "MenuSwitch[VectorSocket]":
97
97
  return MenuSwitch(menu, items, data_type="VECTOR")
98
98
 
99
99
  @classmethod
100
100
  def color(
101
- cls, menu: InputMenu = None, items: dict[str, InputColor] | None = None
101
+ cls, menu: InputMenu = None, items: MenuItems[InputColor] | None = None
102
102
  ) -> "MenuSwitch[ColorSocket]":
103
103
  return MenuSwitch(menu, items, data_type="RGBA")
104
104
 
105
105
  @classmethod
106
106
  def menu(
107
- cls, menu: InputMenu = None, items: dict[str, InputMenu] | None = None
107
+ cls, menu: InputMenu = None, items: MenuItems[InputMenu] | None = None
108
108
  ) -> "MenuSwitch[MenuSocket]":
109
109
  return MenuSwitch(menu, items, data_type="MENU")
110
110
 
111
111
  @classmethod
112
112
  def closure(
113
- cls, menu: InputMenu = None, items: dict[str, InputClosure] | None = None
113
+ cls, menu: InputMenu = None, items: MenuItems[InputClosure] | None = None
114
114
  ) -> "MenuSwitch[ClosureSocket]":
115
115
  return MenuSwitch(menu, items, data_type="CLOSURE")
116
116
 
117
117
  @classmethod
118
118
  def bundle(
119
- cls, menu: InputMenu = None, items: dict[str, InputBundle] | None = None
119
+ cls, menu: InputMenu = None, items: MenuItems[InputBundle] | None = None
120
120
  ) -> "MenuSwitch[BundleSocket]":
121
121
  return MenuSwitch(menu, items, data_type="BUNDLE")
122
122
 
123
123
  @classmethod
124
124
  def shader(
125
- cls, menu: InputMenu = None, items: dict[str, InputShader] | None = None
125
+ cls, menu: InputMenu = None, items: MenuItems[InputShader] | None = None
126
126
  ) -> "MenuSwitch[ShaderSocket]":
127
127
  return MenuSwitch(menu, items, data_type="SHADER")
128
128
 
File without changes