2023-08-16 00:20:26 +10:00
|
|
|
# SPDX-FileCopyrightText: 2022-2023 Blender Authors
|
2023-06-15 13:09:04 +10:00
|
|
|
#
|
2022-09-26 12:36:13 -05:00
|
|
|
# SPDX-License-Identifier: GPL-2.0-or-later
|
2023-06-15 13:09:04 +10:00
|
|
|
|
2022-09-26 12:36:13 -05:00
|
|
|
import bpy
|
2023-09-01 20:46:12 +02:00
|
|
|
from bpy.types import Menu
|
|
|
|
|
from bl_ui import node_add_menu
|
2022-09-26 12:36:13 -05:00
|
|
|
from bpy.app.translations import (
|
|
|
|
|
pgettext_iface as iface_,
|
|
|
|
|
contexts as i18n_contexts,
|
|
|
|
|
)
|
|
|
|
|
|
2022-09-27 16:36:27 +10:00
|
|
|
|
I18n: Translate GN Add > Input > Import menu items
Geometry Nodes' Add > Input > Import menu includes file format items
such as "Standford PLY (.ply)", "STL (.stl)", "Text (.txt)". The
latter needs to be translated because "Text" is a generic format.
These items are declared using a custom function
`node_add_menu.add_node_type`, with a `label` argument. This commit
adds the `label` argument to the function arguments that can be
extracted from specific node declaration functions, and specifies the
argument position for each:
"add_node_type", "add_node_type_with_outputs", "add_simulation_zone",
"add_repeat_zone", "add_foreach_geometry_element_zone",
"add_closure_zone".
There is currently no facility to specify a translation context but it
could be easily added if the need arises.
Most of these functions do not actually declare new, unique messages,
but it could happen in the future. In addition, two messages were
extracted using manual `iface_()` calls, which are no longer needed
after this change.
Reported by Ye Gui in #43295.
2025-06-30 11:31:45 +02:00
|
|
|
def add_node_type(layout, node_type, *, label=None, poll=None, search_weight=0.0, translate=True):
|
2022-09-26 12:36:13 -05:00
|
|
|
"""Add a node type to a menu."""
|
|
|
|
|
bl_rna = bpy.types.Node.bl_rna_get_subclass(node_type)
|
|
|
|
|
if not label:
|
|
|
|
|
label = bl_rna.name if bl_rna else iface_("Unknown")
|
2023-09-01 20:46:12 +02:00
|
|
|
|
2023-09-03 15:48:30 +10:00
|
|
|
if poll is True or poll is None:
|
2023-09-01 20:46:12 +02:00
|
|
|
translation_context = bl_rna.translation_context if bl_rna else i18n_contexts.default
|
I18n: Translate GN Add > Input > Import menu items
Geometry Nodes' Add > Input > Import menu includes file format items
such as "Standford PLY (.ply)", "STL (.stl)", "Text (.txt)". The
latter needs to be translated because "Text" is a generic format.
These items are declared using a custom function
`node_add_menu.add_node_type`, with a `label` argument. This commit
adds the `label` argument to the function arguments that can be
extracted from specific node declaration functions, and specifies the
argument position for each:
"add_node_type", "add_node_type_with_outputs", "add_simulation_zone",
"add_repeat_zone", "add_foreach_geometry_element_zone",
"add_closure_zone".
There is currently no facility to specify a translation context but it
could be easily added if the need arises.
Most of these functions do not actually declare new, unique messages,
but it could happen in the future. In addition, two messages were
extracted using manual `iface_()` calls, which are no longer needed
after this change.
Reported by Ye Gui in #43295.
2025-06-30 11:31:45 +02:00
|
|
|
props = layout.operator(
|
|
|
|
|
"node.add_node",
|
|
|
|
|
text=label,
|
|
|
|
|
text_ctxt=translation_context,
|
|
|
|
|
translate=translate,
|
|
|
|
|
search_weight=search_weight)
|
2023-09-01 20:46:12 +02:00
|
|
|
props.type = node_type
|
|
|
|
|
props.use_transform = True
|
|
|
|
|
return props
|
2022-09-26 12:36:13 -05:00
|
|
|
|
2025-07-03 21:04:46 +10:00
|
|
|
return None
|
|
|
|
|
|
2022-09-26 12:36:13 -05:00
|
|
|
|
Nodes: support searching for outputs of various input nodes directly
Previously, one had to search for the name of an input node (Geometry, Light
Path, etc.) instead of for the actual desired values.
This patch makes it possible to search for the output names of various input
nodes directly. All other outputs of the input node are hidden automatically.
This was partially support for the Scene Time before.
Supported nodes:
* Compositor: Scene Time
* Geometry Nodes: Camera Info, Mouse Position, Scene Time, Viewport Transform
* Shader Nodes: Camera Data, Curves Info, Geometry, Volume Info, Light Path,
Object Info, Particle Info
Right now, the output names are hardcoded in the menu. We don't have a great way
to access those without an actual node instance currently. For that we'll need
to make the node declarations available in Python, which is a good project but
out of scope for this this feature. It also does not seem too bad to have more
explicit control over what's shown in the search.
Pull Request: https://projects.blender.org/blender/blender/pulls/139477
2025-05-28 05:41:37 +02:00
|
|
|
def add_node_type_with_outputs(context, layout, node_type, subnames, *, label=None, search_weight=0.0):
|
2025-05-15 20:56:33 +02:00
|
|
|
bl_rna = bpy.types.Node.bl_rna_get_subclass(node_type)
|
|
|
|
|
if not label:
|
|
|
|
|
label = bl_rna.name if bl_rna else "Unknown"
|
|
|
|
|
|
|
|
|
|
props = []
|
|
|
|
|
props.append(add_node_type(layout, node_type, label=label, search_weight=search_weight))
|
|
|
|
|
if getattr(context, "is_menu_search", False):
|
|
|
|
|
for subname in subnames:
|
|
|
|
|
sublabel = "{} ▸ {}".format(iface_(label), iface_(subname))
|
2025-07-09 12:00:36 +02:00
|
|
|
item_props = add_node_type(layout, node_type, label=sublabel, search_weight=search_weight, translate=False)
|
Nodes: support searching for outputs of various input nodes directly
Previously, one had to search for the name of an input node (Geometry, Light
Path, etc.) instead of for the actual desired values.
This patch makes it possible to search for the output names of various input
nodes directly. All other outputs of the input node are hidden automatically.
This was partially support for the Scene Time before.
Supported nodes:
* Compositor: Scene Time
* Geometry Nodes: Camera Info, Mouse Position, Scene Time, Viewport Transform
* Shader Nodes: Camera Data, Curves Info, Geometry, Volume Info, Light Path,
Object Info, Particle Info
Right now, the output names are hardcoded in the menu. We don't have a great way
to access those without an actual node instance currently. For that we'll need
to make the node declarations available in Python, which is a good project but
out of scope for this this feature. It also does not seem too bad to have more
explicit control over what's shown in the search.
Pull Request: https://projects.blender.org/blender/blender/pulls/139477
2025-05-28 05:41:37 +02:00
|
|
|
item_props.visible_output = subname
|
|
|
|
|
props.append(item_props)
|
2025-05-15 20:56:33 +02:00
|
|
|
return props
|
|
|
|
|
|
|
|
|
|
|
2022-09-26 12:36:13 -05:00
|
|
|
def draw_node_group_add_menu(context, layout):
|
|
|
|
|
"""Add items to the layout used for interacting with node groups."""
|
|
|
|
|
space_node = context.space_data
|
|
|
|
|
node_tree = space_node.edit_tree
|
|
|
|
|
all_node_groups = context.blend_data.node_groups
|
|
|
|
|
|
|
|
|
|
if node_tree in all_node_groups.values():
|
|
|
|
|
layout.separator()
|
|
|
|
|
add_node_type(layout, "NodeGroupInput")
|
|
|
|
|
add_node_type(layout, "NodeGroupOutput")
|
|
|
|
|
|
2025-05-08 10:17:56 +02:00
|
|
|
add_empty_group(layout)
|
|
|
|
|
|
2022-09-26 12:36:13 -05:00
|
|
|
if node_tree:
|
|
|
|
|
from nodeitems_builtins import node_tree_group_type
|
2022-09-27 16:36:27 +10:00
|
|
|
|
2025-05-14 17:41:47 +02:00
|
|
|
prefs = bpy.context.preferences
|
|
|
|
|
show_hidden = prefs.filepaths.show_hidden_files_datablocks
|
|
|
|
|
|
2022-09-27 16:36:27 +10:00
|
|
|
groups = [
|
|
|
|
|
group for group in context.blend_data.node_groups
|
|
|
|
|
if (group.bl_idname == node_tree.bl_idname and
|
2023-02-10 17:30:55 +01:00
|
|
|
not group.contains_tree(node_tree) and
|
2025-05-14 17:41:47 +02:00
|
|
|
(show_hidden or not group.name.startswith('.')))
|
2022-09-27 16:36:27 +10:00
|
|
|
]
|
2022-09-26 12:36:13 -05:00
|
|
|
if groups:
|
|
|
|
|
layout.separator()
|
|
|
|
|
for group in groups:
|
|
|
|
|
props = add_node_type(layout, node_tree_group_type[group.bl_idname], label=group.name)
|
|
|
|
|
ops = props.settings.add()
|
|
|
|
|
ops.name = "node_tree"
|
2024-04-27 16:02:36 +10:00
|
|
|
ops.value = "bpy.data.node_groups[{!r}]".format(group.name)
|
2024-08-11 19:25:53 +02:00
|
|
|
ops = props.settings.add()
|
|
|
|
|
ops.name = "width"
|
|
|
|
|
ops.value = repr(group.default_group_node_width)
|
2025-05-19 15:39:44 +02:00
|
|
|
ops = props.settings.add()
|
|
|
|
|
ops.name = "name"
|
|
|
|
|
ops.value = repr(group.name)
|
2022-09-26 12:36:13 -05:00
|
|
|
|
2022-11-02 10:18:40 +11:00
|
|
|
|
2022-11-01 16:09:49 +01:00
|
|
|
def draw_assets_for_catalog(layout, catalog_path):
|
|
|
|
|
layout.template_node_asset_menu_items(catalog_path=catalog_path)
|
|
|
|
|
|
2022-11-02 10:18:40 +11:00
|
|
|
|
2022-11-01 16:09:49 +01:00
|
|
|
def draw_root_assets(layout):
|
|
|
|
|
layout.menu_contents("NODE_MT_node_add_root_catalogs")
|
|
|
|
|
|
2022-09-26 12:36:13 -05:00
|
|
|
|
2025-05-10 06:28:59 +02:00
|
|
|
def add_node_type_with_searchable_enum(context, layout, node_idname, property_name, search_weight=0.0):
|
|
|
|
|
add_node_type(layout, node_idname, search_weight=search_weight)
|
2025-05-08 04:28:22 +02:00
|
|
|
if getattr(context, "is_menu_search", False):
|
|
|
|
|
node_type = getattr(bpy.types, node_idname)
|
2025-07-09 12:00:36 +02:00
|
|
|
translation_context = node_type.bl_rna.properties[property_name].translation_context
|
2025-05-08 04:28:22 +02:00
|
|
|
for item in node_type.bl_rna.properties[property_name].enum_items_static:
|
2025-07-09 12:00:36 +02:00
|
|
|
label = "{} ▸ {}".format(iface_(node_type.bl_rna.name), iface_(item.name, translation_context))
|
2025-05-10 06:28:59 +02:00
|
|
|
props = add_node_type(
|
|
|
|
|
layout,
|
|
|
|
|
node_idname,
|
I18n: Translate GN Add > Input > Import menu items
Geometry Nodes' Add > Input > Import menu includes file format items
such as "Standford PLY (.ply)", "STL (.stl)", "Text (.txt)". The
latter needs to be translated because "Text" is a generic format.
These items are declared using a custom function
`node_add_menu.add_node_type`, with a `label` argument. This commit
adds the `label` argument to the function arguments that can be
extracted from specific node declaration functions, and specifies the
argument position for each:
"add_node_type", "add_node_type_with_outputs", "add_simulation_zone",
"add_repeat_zone", "add_foreach_geometry_element_zone",
"add_closure_zone".
There is currently no facility to specify a translation context but it
could be easily added if the need arises.
Most of these functions do not actually declare new, unique messages,
but it could happen in the future. In addition, two messages were
extracted using manual `iface_()` calls, which are no longer needed
after this change.
Reported by Ye Gui in #43295.
2025-06-30 11:31:45 +02:00
|
|
|
label=label,
|
2025-07-09 12:00:36 +02:00
|
|
|
translate=False,
|
2025-05-10 06:28:59 +02:00
|
|
|
search_weight=search_weight)
|
2025-05-08 04:28:22 +02:00
|
|
|
prop = props.settings.add()
|
|
|
|
|
prop.name = property_name
|
|
|
|
|
prop.value = repr(item.identifier)
|
|
|
|
|
|
|
|
|
|
|
2025-08-28 08:45:23 +02:00
|
|
|
def add_node_type_with_searchable_enum_socket(
|
|
|
|
|
context,
|
|
|
|
|
layout,
|
|
|
|
|
node_idname,
|
|
|
|
|
socket_identifier,
|
|
|
|
|
enum_names,
|
|
|
|
|
search_weight=0.0):
|
|
|
|
|
add_node_type(layout, node_idname, search_weight=search_weight)
|
|
|
|
|
if getattr(context, "is_menu_search", False):
|
|
|
|
|
node_type = getattr(bpy.types, node_idname)
|
|
|
|
|
for enum_name in enum_names:
|
|
|
|
|
label = "{} ▸ {}".format(iface_(node_type.bl_rna.name), iface_(enum_name))
|
|
|
|
|
props = add_node_type(
|
|
|
|
|
layout,
|
|
|
|
|
node_idname,
|
|
|
|
|
label=label,
|
|
|
|
|
translate=False,
|
|
|
|
|
search_weight=search_weight)
|
|
|
|
|
prop = props.settings.add()
|
|
|
|
|
prop.name = f'inputs["{socket_identifier}"].default_value'
|
|
|
|
|
prop.value = repr(enum_name)
|
|
|
|
|
|
|
|
|
|
|
2025-05-09 04:02:50 +02:00
|
|
|
def add_color_mix_node(context, layout):
|
|
|
|
|
label = iface_("Mix Color")
|
2025-07-09 12:00:36 +02:00
|
|
|
props = node_add_menu.add_node_type(layout, "ShaderNodeMix", label=label, translate=False)
|
2025-05-09 04:02:50 +02:00
|
|
|
ops = props.settings.add()
|
|
|
|
|
ops.name = "data_type"
|
|
|
|
|
ops.value = "'RGBA'"
|
|
|
|
|
|
|
|
|
|
if getattr(context, "is_menu_search", False):
|
2025-07-09 12:00:36 +02:00
|
|
|
translation_context = bpy.types.ShaderNodeMix.bl_rna.properties["blend_type"].translation_context
|
2025-05-09 04:02:50 +02:00
|
|
|
for item in bpy.types.ShaderNodeMix.bl_rna.properties["blend_type"].enum_items_static:
|
2025-07-09 12:00:36 +02:00
|
|
|
sublabel = "{} ▸ {}".format(label, iface_(item.name, translation_context))
|
|
|
|
|
props = node_add_menu.add_node_type(layout, "ShaderNodeMix", label=sublabel, translate=False)
|
2025-05-09 04:02:50 +02:00
|
|
|
prop = props.settings.add()
|
|
|
|
|
prop.name = "data_type"
|
|
|
|
|
prop.value = "'RGBA'"
|
|
|
|
|
prop = props.settings.add()
|
|
|
|
|
prop.name = "blend_type"
|
|
|
|
|
prop.value = repr(item.identifier)
|
|
|
|
|
|
|
|
|
|
|
Geometry Nodes: add simulation support
This adds support for building simulations with geometry nodes. A new
`Simulation Input` and `Simulation Output` node allow maintaining a
simulation state across multiple frames. Together these two nodes form
a `simulation zone` which contains all the nodes that update the simulation
state from one frame to the next.
A new simulation zone can be added via the menu
(`Simulation > Simulation Zone`) or with the node add search.
The simulation state contains a geometry by default. However, it is possible
to add multiple geometry sockets as well as other socket types. Currently,
field inputs are evaluated and stored for the preceding geometry socket in
the order that the sockets are shown. Simulation state items can be added
by linking one of the empty sockets to something else. In the sidebar, there
is a new panel that allows adding, removing and reordering these sockets.
The simulation nodes behave as follows:
* On the first frame, the inputs of the `Simulation Input` node are evaluated
to initialize the simulation state. In later frames these sockets are not
evaluated anymore. The `Delta Time` at the first frame is zero, but the
simulation zone is still evaluated.
* On every next frame, the `Simulation Input` node outputs the simulation
state of the previous frame. Nodes in the simulation zone can edit that
data in arbitrary ways, also taking into account the `Delta Time`. The new
simulation state has to be passed to the `Simulation Output` node where it
is cached and forwarded.
* On a frame that is already cached or baked, the nodes in the simulation
zone are not evaluated, because the `Simulation Output` node can return
the previously cached data directly.
It is not allowed to connect sockets from inside the simulation zone to the
outside without going through the `Simulation Output` node. This is a necessary
restriction to make caching and sub-frame interpolation work. Links can go into
the simulation zone without problems though.
Anonymous attributes are not propagated by the simulation nodes unless they
are explicitly stored in the simulation state. This is unfortunate, but
currently there is no practical and reliable alternative. The core problem
is detecting which anonymous attributes will be required for the simulation
and afterwards. While we can detect this for the current evaluation, we can't
look into the future in time to see what data will be necessary. We intend to
make it easier to explicitly pass data through a simulation in the future,
even if the simulation is in a nested node group.
There is a new `Simulation Nodes` panel in the physics tab in the properties
editor. It allows baking all simulation zones on the selected objects. The
baking options are intentially kept at a minimum for this MVP. More features
for simulation baking as well as baking in general can be expected to be added
separately.
All baked data is stored on disk in a folder next to the .blend file. #106937
describes how baking is implemented in more detail. Volumes can not be baked
yet and materials are lost during baking for now. Packing the baked data into
the .blend file is not yet supported.
The timeline indicates which frames are currently cached, baked or cached but
invalidated by user-changes.
Simulation input and output nodes are internally linked together by their
`bNode.identifier` which stays the same even if the node name changes. They
are generally added and removed together. However, there are still cases where
"dangling" simulation nodes can be created currently. Those generally don't
cause harm, but would be nice to avoid this in more cases in the future.
Co-authored-by: Hans Goudey <h.goudey@me.com>
Co-authored-by: Lukas Tönne <lukas@blender.org>
Pull Request: https://projects.blender.org/blender/blender/pulls/104924
2023-05-03 13:18:51 +02:00
|
|
|
def add_simulation_zone(layout, label):
|
|
|
|
|
"""Add simulation zone to a menu."""
|
2023-07-31 03:38:17 +02:00
|
|
|
props = layout.operator("node.add_simulation_zone", text=label, text_ctxt=i18n_contexts.default)
|
Geometry Nodes: add simulation support
This adds support for building simulations with geometry nodes. A new
`Simulation Input` and `Simulation Output` node allow maintaining a
simulation state across multiple frames. Together these two nodes form
a `simulation zone` which contains all the nodes that update the simulation
state from one frame to the next.
A new simulation zone can be added via the menu
(`Simulation > Simulation Zone`) or with the node add search.
The simulation state contains a geometry by default. However, it is possible
to add multiple geometry sockets as well as other socket types. Currently,
field inputs are evaluated and stored for the preceding geometry socket in
the order that the sockets are shown. Simulation state items can be added
by linking one of the empty sockets to something else. In the sidebar, there
is a new panel that allows adding, removing and reordering these sockets.
The simulation nodes behave as follows:
* On the first frame, the inputs of the `Simulation Input` node are evaluated
to initialize the simulation state. In later frames these sockets are not
evaluated anymore. The `Delta Time` at the first frame is zero, but the
simulation zone is still evaluated.
* On every next frame, the `Simulation Input` node outputs the simulation
state of the previous frame. Nodes in the simulation zone can edit that
data in arbitrary ways, also taking into account the `Delta Time`. The new
simulation state has to be passed to the `Simulation Output` node where it
is cached and forwarded.
* On a frame that is already cached or baked, the nodes in the simulation
zone are not evaluated, because the `Simulation Output` node can return
the previously cached data directly.
It is not allowed to connect sockets from inside the simulation zone to the
outside without going through the `Simulation Output` node. This is a necessary
restriction to make caching and sub-frame interpolation work. Links can go into
the simulation zone without problems though.
Anonymous attributes are not propagated by the simulation nodes unless they
are explicitly stored in the simulation state. This is unfortunate, but
currently there is no practical and reliable alternative. The core problem
is detecting which anonymous attributes will be required for the simulation
and afterwards. While we can detect this for the current evaluation, we can't
look into the future in time to see what data will be necessary. We intend to
make it easier to explicitly pass data through a simulation in the future,
even if the simulation is in a nested node group.
There is a new `Simulation Nodes` panel in the physics tab in the properties
editor. It allows baking all simulation zones on the selected objects. The
baking options are intentially kept at a minimum for this MVP. More features
for simulation baking as well as baking in general can be expected to be added
separately.
All baked data is stored on disk in a folder next to the .blend file. #106937
describes how baking is implemented in more detail. Volumes can not be baked
yet and materials are lost during baking for now. Packing the baked data into
the .blend file is not yet supported.
The timeline indicates which frames are currently cached, baked or cached but
invalidated by user-changes.
Simulation input and output nodes are internally linked together by their
`bNode.identifier` which stays the same even if the node name changes. They
are generally added and removed together. However, there are still cases where
"dangling" simulation nodes can be created currently. Those generally don't
cause harm, but would be nice to avoid this in more cases in the future.
Co-authored-by: Hans Goudey <h.goudey@me.com>
Co-authored-by: Lukas Tönne <lukas@blender.org>
Pull Request: https://projects.blender.org/blender/blender/pulls/104924
2023-05-03 13:18:51 +02:00
|
|
|
props.use_transform = True
|
|
|
|
|
return props
|
|
|
|
|
|
|
|
|
|
|
2023-07-11 22:36:10 +02:00
|
|
|
def add_repeat_zone(layout, label):
|
2023-07-31 03:38:17 +02:00
|
|
|
props = layout.operator("node.add_repeat_zone", text=label, text_ctxt=i18n_contexts.default)
|
2023-07-11 22:36:10 +02:00
|
|
|
props.use_transform = True
|
|
|
|
|
return props
|
|
|
|
|
|
|
|
|
|
|
2024-09-24 11:52:02 +02:00
|
|
|
def add_foreach_geometry_element_zone(layout, label):
|
|
|
|
|
props = layout.operator(
|
2025-01-14 12:46:40 +11:00
|
|
|
"node.add_foreach_geometry_element_zone",
|
|
|
|
|
text=label,
|
|
|
|
|
text_ctxt=i18n_contexts.default,
|
|
|
|
|
)
|
2024-09-24 11:52:02 +02:00
|
|
|
props.use_transform = True
|
|
|
|
|
return props
|
|
|
|
|
|
|
|
|
|
|
2025-04-03 15:44:06 +02:00
|
|
|
def add_closure_zone(layout, label):
|
|
|
|
|
props = layout.operator(
|
|
|
|
|
"node.add_closure_zone", text=label, text_ctxt=i18n_contexts.default)
|
|
|
|
|
props.use_transform = True
|
|
|
|
|
return props
|
|
|
|
|
|
|
|
|
|
|
2025-05-08 10:17:56 +02:00
|
|
|
def add_empty_group(layout):
|
|
|
|
|
props = layout.operator("node.add_empty_group", text="New Group", text_ctxt=i18n_contexts.default)
|
|
|
|
|
props.use_transform = True
|
|
|
|
|
return props
|
|
|
|
|
|
|
|
|
|
|
2023-09-01 20:46:12 +02:00
|
|
|
class NODE_MT_category_layout(Menu):
|
|
|
|
|
bl_idname = "NODE_MT_category_layout"
|
|
|
|
|
bl_label = "Layout"
|
|
|
|
|
|
|
|
|
|
def draw(self, _context):
|
|
|
|
|
layout = self.layout
|
2025-06-02 13:49:17 +02:00
|
|
|
node_add_menu.add_node_type(layout, "NodeFrame", search_weight=-1)
|
2023-09-01 20:46:12 +02:00
|
|
|
node_add_menu.add_node_type(layout, "NodeReroute")
|
|
|
|
|
|
|
|
|
|
node_add_menu.draw_assets_for_catalog(layout, self.bl_label)
|
|
|
|
|
|
2023-09-03 15:48:30 +10:00
|
|
|
|
2022-09-26 12:36:13 -05:00
|
|
|
classes = (
|
2023-09-01 20:46:12 +02:00
|
|
|
NODE_MT_category_layout,
|
2022-09-26 12:36:13 -05:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
if __name__ == "__main__": # only for live edit.
|
|
|
|
|
from bpy.utils import register_class
|
|
|
|
|
for cls in classes:
|
|
|
|
|
register_class(cls)
|