Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/tree_clipper/src/tree_clipper/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@
PROP_TYPE_ENUM,
}
NODE_TREE = "node_tree"
PANEL_STATES = "panel_states"
DIMENSIONS = "dimensions"


Expand Down
21 changes: 21 additions & 0 deletions packages/tree_clipper/src/tree_clipper/export_nodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
MATERIAL_NAME,
NAME,
NODE_TREE,
PANEL_STATES,
PROP_TYPE_BOOLEAN,
PROP_TYPE_COLLECTION,
PROP_TYPE_ENUM,
Expand Down Expand Up @@ -482,6 +483,26 @@ def serializer(
from_root: FromRoot,
) -> dict[str, Any]:
data = specific_handler(exporter, obj, from_root)

# Blender 5.2 exposes the open/closed state of interface panels as
# a read-only collection on every node. It has to be requested
# explicitly because a node's specific handler owns all inherited
# Node properties. Avoid writing an empty collection for the many

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

An empty collection is fine.
We generally export "too much", but that puts us on the safe side for extending the import later.

(In fact, it would have been nice if we were already exporting the panel_states)

# nodes that do not have panels.
if isinstance(obj, bpy.types.Node) and hasattr(obj, PANEL_STATES):
panel_states = getattr(obj, PANEL_STATES)
if len(panel_states) > 0:
prop = obj.bl_rna.properties[PANEL_STATES]
no_clobber(
data,
prop.identifier,
exporter._export_property_collection(
obj=obj,
prop=cast(bpy.types.CollectionProperty, prop),
from_root=from_root.add_prop(prop),
),
)

Comment on lines +486 to +505

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not written anywhere explicitly, but the idea for export_nodes.py and import_nodes.py is to be as general as possible.

With that in mind, this code seems out of place and should be handled in the specific_handlers file instead.

It would also be more consistent to keep the comments shorter and link to the GitHub issue.

for prop in unhandled_properties:
from_root_prop = from_root.add_prop(prop)

Expand Down
15 changes: 14 additions & 1 deletion packages/tree_clipper/src/tree_clipper/import_nodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from operator import xor
from pathlib import Path
from types import NoneType
from typing import Any
from typing import Any, cast

import bpy

Expand All @@ -28,6 +28,7 @@
MAGIC_STRING,
MATERIAL_NAME,
NAME,
PANEL_STATES,
PROP_TYPE_COLLECTION,
PROP_TYPE_ENUM,
PROP_TYPE_POINTER,
Expand Down Expand Up @@ -411,6 +412,18 @@ def deserializer(
) -> None:
specific_handler(importer, getter, serialization, from_root)

# Panel declarations can depend on node-specific properties and
# sockets, so restore their UI state only after the specific node
# handler has finished rebuilding the node.
if isinstance(getter(), bpy.types.Node) and PANEL_STATES in serialization:
prop = getter().bl_rna.properties[PANEL_STATES]
self._import_property_collection(
getter=getter,
prop=cast(bpy.types.CollectionProperty, prop),
serialization=serialization[PANEL_STATES],
from_root=from_root.add_prop(prop),
)

Comment on lines +415 to +426

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similar to the export, it seems better in the specific_handlers file.

for identifier in unhandled_prop_ids:
prop = getter().bl_rna.properties[identifier]
prop_from_root = from_root.add_prop(prop)
Expand Down
37 changes: 37 additions & 0 deletions packages/tree_clipper/src/tree_clipper/specific_handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
NODE_TREE,
no_clobber,
)
from .export_nodes import Exporter
from .import_nodes import Importer
from .specific_abstract import (
_BUILT_IN_EXPORTER,
Expand Down Expand Up @@ -418,6 +419,42 @@ def deserialize(self):
_import_node_parent(self)


_node_panel_state_type: Any = getattr(bpy.types, "NodePanelState", None)
if _node_panel_state_type is not None:
Comment on lines +422 to +423

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We usually check the Blender version instead.


def _export_node_panel_state(
exporter: Exporter,
obj: bpy.types.bpy_struct,
from_root,
):
# `identifier` is intentionally omitted. It matches the interface
# panel's read-only persistent UID, which may change when an interface
# containing gaps in its UIDs is recreated on import. The collection
# order follows the interface panel order.
return exporter.export_all_simple_writable_properties(
obj=obj,
assumed_type=_node_panel_state_type,
from_root=from_root,
)

def _import_node_panel_state(
importer: Importer,
getter: GETTER,
serialization: dict[str, Any],
from_root,
):
importer.import_all_simple_writable_properties(
getter=getter,
serialization=serialization,
assumed_type=_node_panel_state_type,
forbidden=[],
from_root=from_root,
)

no_clobber(_BUILT_IN_EXPORTER, _node_panel_state_type, _export_node_panel_state)
no_clobber(_BUILT_IN_IMPORTER, _node_panel_state_type, _import_node_panel_state)
Comment on lines +425 to +455

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe this is unnecessary, as the default property handling should suffice.

EDIT: the identifier is unexpectetly exported, this is a separate bug in the default handling.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The wording I've choosen of the comment on the SpecificImporter/SpecificExporter is too vague.

The helper classes should always be used if possible.



class CompositorNodeGroupImporter(SpecificImporter[bpy.types.CompositorNodeGroup]):
def deserialize(self):
self.import_all_simple_writable_properties_and_list(
Expand Down
Git LFS file not shown
Git LFS file not shown
67 changes: 67 additions & 0 deletions packages/tree_clipper/tests/test_node_panel_states.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import json

import bpy

from .util import (
export_to_string,
make_test_node_tree,
round_trip_without_external,
save_failed,
)

if (bpy.app.version[0] == 5 and bpy.app.version[1] >= 2) or bpy.app.version[0] > 5:

def test_node_panel_states():
try:
inner = make_test_node_tree(name="panel state inner")

# Leave a gap in the persistent UIDs. Those identifiers are
# read-only and are therefore not guaranteed to survive import.
removed_panel = inner.interface.new_panel(name="removed")
inner.interface.remove(removed_panel)

open_panel = inner.interface.new_panel(name="open")
closed_panel = inner.interface.new_panel(name="closed")
inner.interface.new_socket(
name="Open Value",
in_out="INPUT",
socket_type="NodeSocketFloat",
parent=open_panel,
)
inner.interface.new_socket(
name="Closed Value",
in_out="INPUT",
socket_type="NodeSocketFloat",
parent=closed_panel,
)

outer = make_test_node_tree(name="panel state outer")
group_node = outer.nodes.new(type="GeometryNodeGroup")
group_node.node_tree = inner
Comment on lines +16 to +40

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems easier to use a built-in node that has a collapsible panel, like Mesh Bevel.


assert [state.identifier for state in group_node.panel_states] == [
open_panel.persistent_uid,
closed_panel.persistent_uid,
]
group_node.panel_states[0].is_collapsed = False
group_node.panel_states[1].is_collapsed = True

serialization = json.loads(export_to_string(outer.name))
serialized_outer = next(
tree
for tree in serialization["node_trees"]
if tree["data"]["name"] == outer.name
)
serialized_node = serialized_outer["data"]["nodes"]["data"]["items"][0]
serialized_states = serialized_node["data"]["panel_states"]["data"]["items"]

assert [state["data"]["is_collapsed"] for state in serialized_states] == [
False,
True,
]
assert all("identifier" not in state["data"] for state in serialized_states)

round_trip_without_external(outer.name)
except:
save_failed(test_node_panel_states.__name__)
raise
Loading