Grease Pencil: Python: Add LayerGroup.children and root_nodes API

Adaptation of !138738 by @SietseB.

Adds two new python API endpoints:
* `GreasePencil.root_nodes`: Collection of all the top most nodes in the
  layer tree.
* `LayerGroup.children`: Collection of all the top most children of the layer
  group.

Both of these iterate the nodes in stack order, meaning bottom to top.
This matches the internal representation of the Grease Pencil data.

Pull Request: https://projects.blender.org/blender/blender/pulls/152913
This commit is contained in:
EvanLuo42 2026-01-29 09:55:52 +01:00 • committed by Falk David
parent 89de863679
commit 37d0902549
2 changed files with 194 additions and 4 deletions

View file

@ -13,9 +13,11 @@
#include "DNA_grease_pencil_types.h"
#include "DNA_scene_types.h"
#include "RNA_access.hh"
#include "RNA_define.hh"
#include "RNA_enum_types.hh"
#include "RNA_types.hh"
#include "rna_internal.hh"
#include "WM_api.hh"
@ -663,6 +665,49 @@ static void rna_GreasePencilLayerGroup_is_expanded_set(PointerRNA *ptr, const bo
group->wrap().set_expanded(value);
}
static void rna_GreasePencilLayerGroup_children_begin(CollectionPropertyIterator *iter,
PointerRNA *ptr)
{
using namespace blender::bke::greasepencil;
LayerGroup *layer_group = static_cast<LayerGroup *>(ptr->data);
ListBaseT<GreasePencilLayerTreeNode> *children = &layer_group->children;
rna_iterator_listbase_begin(iter, ptr, children, nullptr);
}
static void rna_GreasePencilLayerGroup_children_next(CollectionPropertyIterator *iter)
{
rna_iterator_listbase_next(iter);
}
static PointerRNA rna_GreasePencilLayerGroup_children_get(CollectionPropertyIterator *iter)
{
return RNA_pointer_create_discrete(
iter->parent.owner_id, RNA_GreasePencilTreeNode, rna_iterator_listbase_get(iter));
}
static int rna_GreasePencilLayerGroup_children_length(PointerRNA *ptr)
{
using namespace blender::bke::greasepencil;
TreeNode *node = static_cast<TreeNode *>(ptr->data);
BLI_assert(node->is_group());
return node->as_group().num_direct_nodes();
}
static bool rna_GreasePencilLayerGroup_children_lookup_int(PointerRNA *ptr,
int index,
PointerRNA *r_ptr)
{
using namespace blender::bke::greasepencil;
LayerGroup *layer_group = static_cast<LayerGroup *>(ptr->data);
ListBaseT<GreasePencilLayerTreeNode> *lb = &layer_group->children;
*r_ptr = rna_listbase_lookup_int(ptr, RNA_GreasePencilTreeNode, lb, index);
return true;
}
static void rna_iterator_grease_pencil_layer_groups_begin(CollectionPropertyIterator *iter,
PointerRNA *ptr)
{
@ -701,6 +746,36 @@ static int rna_iterator_grease_pencil_layer_groups_length(PointerRNA *ptr)
return grease_pencil->layer_groups().size();
}
static void rna_iterator_grease_pencil_root_nodes_begin(CollectionPropertyIterator *iter,
PointerRNA *ptr)
{
using namespace blender::bke::greasepencil;
GreasePencil *grease_pencil = rna_grease_pencil(ptr);
LayerGroup &root_group = grease_pencil->root_group();
ListBaseT<GreasePencilLayerTreeNode> *children = &root_group.children;
rna_iterator_listbase_begin(iter, ptr, children, nullptr);
}
static void rna_iterator_grease_pencil_root_nodes_next(CollectionPropertyIterator *iter)
{
rna_iterator_listbase_next(iter);
}
static PointerRNA rna_iterator_grease_pencil_root_nodes_get(CollectionPropertyIterator *iter)
{
return RNA_pointer_create_discrete(
iter->parent.owner_id, RNA_GreasePencilTreeNode, rna_iterator_listbase_get(iter));
}
static int rna_iterator_grease_pencil_root_nodes_length(PointerRNA *ptr)
{
using namespace blender::bke::greasepencil;
GreasePencil *grease_pencil = rna_grease_pencil(ptr);
return grease_pencil->root_group().num_direct_nodes();
}
static int rna_group_color_tag_get(PointerRNA *ptr)
{
using namespace bke::greasepencil;
@ -1262,6 +1337,23 @@ static void rna_def_grease_pencil_layer_group(BlenderRNA *brna)
prop = RNA_def_property(srna, "color_tag", PROP_ENUM, PROP_NONE);
RNA_def_property_enum_funcs(prop, "rna_group_color_tag_get", "rna_group_color_tag_set", nullptr);
RNA_def_property_enum_items(prop, enum_layergroup_color_items);
/* Children */
prop = RNA_def_property(srna, "children", PROP_COLLECTION, PROP_NONE);
RNA_def_property_struct_type(prop, "GreasePencilTreeNode");
RNA_def_property_collection_funcs(prop,
"rna_GreasePencilLayerGroup_children_begin",
"rna_GreasePencilLayerGroup_children_next",
nullptr,
"rna_GreasePencilLayerGroup_children_get",
"rna_GreasePencilLayerGroup_children_length",
"rna_GreasePencilLayerGroup_children_lookup_int",
nullptr,
nullptr);
RNA_def_property_ui_text(prop,
"Children",
"The direct children of this layer group. Ordered by stack order, "
"meaning the first child is the bottom most child in the layer tree.");
}
static void rna_def_grease_pencil_layer_groups(BlenderRNA *brna, PropertyRNA *cprop)
@ -1490,6 +1582,23 @@ static void rna_def_grease_pencil_data(BlenderRNA *brna)
RNA_def_property_ui_text(prop, "Layer Groups", "Grease Pencil layer groups");
rna_def_grease_pencil_layer_groups(brna, prop);
/* Root nodes. */
prop = RNA_def_property(srna, "root_nodes", PROP_COLLECTION, PROP_NONE);
RNA_def_property_struct_type(prop, "GreasePencilTreeNode");
RNA_def_property_collection_funcs(prop,
"rna_iterator_grease_pencil_root_nodes_begin",
"rna_iterator_grease_pencil_root_nodes_next",
nullptr,
"rna_iterator_grease_pencil_root_nodes_get",
"rna_iterator_grease_pencil_root_nodes_length",
nullptr,
nullptr,
nullptr);
RNA_def_property_ui_text(prop,
"Root Nodes",
"The root nodes of the layer tree. Ordered by stack order, meaning the "
"first node is the bottom most node in the layer tree.");
prop = RNA_def_property(srna, "use_autolock_layers", PROP_BOOLEAN, PROP_NONE);
RNA_def_property_boolean_sdna(prop, nullptr, "flag", GREASE_PENCIL_AUTOLOCK_LAYERS);
RNA_def_property_ui_text(

View file

@ -10,6 +10,7 @@ import unittest
# -----------------------------------------------------------------------------
# Tests
class TestGreasePencil(unittest.TestCase):
def setUp(self):
self.gp = bpy.data.grease_pencils.new("test_grease_pencil")
@ -62,14 +63,14 @@ class TestGreasePencilLayers(unittest.TestCase):
def test_grease_pencil_layers_move_down(self):
# Move the top most layer down
self.gp.layers.move(self.gp.layers[-1], 'DOWN')
self.gp.layers.move(self.gp.layers[-1], "DOWN")
self.assertEqual(self.gp.layers[0].name, "test_layer01")
self.assertEqual(self.gp.layers[1].name, "test_layer03")
self.assertEqual(self.gp.layers[2].name, "test_layer02")
def test_grease_pencil_layers_move_up(self):
# Move the bottom most layer up
self.gp.layers.move(self.gp.layers[0], 'UP')
self.gp.layers.move(self.gp.layers[0], "UP")
self.assertEqual(self.gp.layers[0].name, "test_layer02")
self.assertEqual(self.gp.layers[1].name, "test_layer01")
self.assertEqual(self.gp.layers[2].name, "test_layer03")
@ -299,7 +300,87 @@ class TestGreasePencilDrawing(unittest.TestCase):
self.assertEqual(len(strokes[0].points), 5)
if __name__ == '__main__':
class TestGreasePencilRootNodes(unittest.TestCase):
def setUp(self):
self.gp = bpy.data.grease_pencils.new("test_gp")
self.layer_a = self.gp.layers.new("LayerA")
self.group_g = self.gp.layer_groups.new("GroupG")
self.layer_b = self.gp.layers.new("LayerB")
self.layer_c = self.gp.layers.new("LayerC")
self.gp.layers.move_to_layer_group(self.layer_b, self.group_g)
self.gp.layers.move_to_layer_group(self.layer_c, self.group_g)
self.layer_d = self.gp.layers.new("LayerD")
def tearDown(self):
bpy.data.grease_pencils.remove(self.gp)
def test_root_nodes_len_and_order(self):
nodes = self.gp.root_nodes
self.assertEqual(len(nodes), 3)
self.assertEqual(nodes[0].name, "LayerA")
self.assertEqual(nodes[1].name, "GroupG")
self.assertEqual(nodes[2].name, "LayerD")
def test_root_nodes_types(self):
nodes = self.gp.root_nodes
self.assertIsInstance(nodes[0], bpy.types.GreasePencilLayer)
self.assertIsInstance(nodes[1], bpy.types.GreasePencilLayerGroup)
self.assertIsInstance(nodes[2], bpy.types.GreasePencilLayer)
class TestGreasePencilLayerGroupChildren(unittest.TestCase):
def setUp(self):
self.gp = bpy.data.grease_pencils.new("test_gp")
self.group = self.gp.layer_groups.new("Group")
self.layer1 = self.gp.layers.new("Layer1")
self.layer2 = self.gp.layers.new("Layer2")
self.gp.layers.move_to_layer_group(self.layer1, self.group)
self.gp.layers.move_to_layer_group(self.layer2, self.group)
self.subgroup = self.gp.layer_groups.new("SubGroup")
self.gp.layer_groups.move_to_layer_group(self.subgroup, self.group)
self.sublayer = self.gp.layers.new("SubLayer")
self.gp.layers.move_to_layer_group(self.sublayer, self.subgroup)
def tearDown(self):
bpy.data.grease_pencils.remove(self.gp)
def test_children_basic(self):
children = self.group.children
self.assertEqual(len(children), 3)
self.assertEqual(children[0].name, "Layer1")
self.assertEqual(children[1].name, "Layer2")
self.assertEqual(children[2].name, "SubGroup")
def test_children_not_recursive(self):
children = self.group.children
names = {child.name for child in children}
self.assertNotIn("SubLayer", names)
def test_children_iteration(self):
names = []
for node in self.group.children:
names.append(node.name)
self.assertEqual(names, ["Layer1", "Layer2", "SubGroup"])
if __name__ == "__main__":
import sys
sys.argv = [__file__] + (sys.argv[sys.argv.index("--") + 1:] if "--" in sys.argv else [])
sys.argv = [__file__] + (
sys.argv[sys.argv.index("--") + 1:] if "--" in sys.argv else []
)
unittest.main()