2024-03-10 11:56:37 +01:00
|
|
|
import typing as typ
|
2024-02-14 12:33:40 +01:00
|
|
|
|
2024-03-10 11:56:37 +01:00
|
|
|
import bpy
|
2024-02-14 12:33:40 +01:00
|
|
|
|
2024-03-10 11:56:37 +01:00
|
|
|
from . import contracts as ct
|
2024-02-26 16:16:06 +01:00
|
|
|
|
2024-03-10 11:56:37 +01:00
|
|
|
####################
|
|
|
|
# - Cache Management
|
|
|
|
####################
|
|
|
|
MemAddr = int
|
2024-02-26 16:16:06 +01:00
|
|
|
|
2024-03-10 11:56:37 +01:00
|
|
|
class DeltaNodeLinkCache(typ.TypedDict):
|
|
|
|
added: set[MemAddr]
|
|
|
|
removed: set[MemAddr]
|
2024-02-26 16:16:06 +01:00
|
|
|
|
2024-03-10 11:56:37 +01:00
|
|
|
class NodeLinkCache:
|
|
|
|
def __init__(self, node_tree: bpy.types.NodeTree):
|
|
|
|
# Initialize Parameters
|
|
|
|
self._node_tree = node_tree
|
|
|
|
self.link_ptrs_to_links = {}
|
|
|
|
self.link_ptrs = set()
|
|
|
|
self.link_ptrs_from_sockets = {}
|
|
|
|
self.link_ptrs_to_sockets = {}
|
|
|
|
|
|
|
|
# Fill Cache
|
|
|
|
self.regenerate()
|
|
|
|
|
|
|
|
def remove(self, link_ptrs: set[MemAddr]) -> None:
|
|
|
|
for link_ptr in link_ptrs:
|
|
|
|
self.link_ptrs.remove(link_ptr)
|
|
|
|
self.link_ptrs_to_links.pop(link_ptr, None)
|
|
|
|
|
|
|
|
def regenerate(self) -> DeltaNodeLinkCache:
|
|
|
|
current_link_ptrs_to_links = {
|
|
|
|
link.as_pointer(): link for link in self._node_tree.links
|
|
|
|
}
|
|
|
|
current_link_ptrs = set(current_link_ptrs_to_links.keys())
|
|
|
|
|
|
|
|
# Compute Delta
|
|
|
|
added_link_ptrs = current_link_ptrs - self.link_ptrs
|
|
|
|
removed_link_ptrs = self.link_ptrs - current_link_ptrs
|
|
|
|
|
|
|
|
# Update Caches Incrementally
|
|
|
|
self.remove(removed_link_ptrs)
|
|
|
|
|
|
|
|
self.link_ptrs |= added_link_ptrs
|
|
|
|
for link_ptr in added_link_ptrs:
|
|
|
|
link = current_link_ptrs_to_links[link_ptr]
|
|
|
|
|
|
|
|
self.link_ptrs_to_links[link_ptr] = link
|
|
|
|
self.link_ptrs_from_sockets[link_ptr] = link.from_socket
|
|
|
|
self.link_ptrs_to_sockets[link_ptr] = link.to_socket
|
|
|
|
|
|
|
|
return {"added": added_link_ptrs, "removed": removed_link_ptrs}
|
2024-02-26 16:16:06 +01:00
|
|
|
|
2024-02-14 12:33:40 +01:00
|
|
|
####################
|
|
|
|
# - Node Tree Definition
|
|
|
|
####################
|
|
|
|
class MaxwellSimTree(bpy.types.NodeTree):
|
2024-03-10 11:56:37 +01:00
|
|
|
bl_idname = ct.TreeType.MaxwellSim.value
|
2024-02-14 12:33:40 +01:00
|
|
|
bl_label = "Maxwell Sim Editor"
|
2024-03-10 11:56:37 +01:00
|
|
|
bl_icon = ct.Icon.SimNodeEditor.value
|
2024-02-26 16:16:06 +01:00
|
|
|
|
2024-03-10 11:56:37 +01:00
|
|
|
managed_collection: bpy.props.PointerProperty(
|
|
|
|
name="Managed Collection",
|
|
|
|
description="Collection of Blender objects managed by this tree",
|
|
|
|
type=bpy.types.Collection,
|
|
|
|
)
|
2024-02-26 16:16:06 +01:00
|
|
|
preview_collection: bpy.props.PointerProperty(
|
|
|
|
name="Preview Collection",
|
|
|
|
description="Collection of Blender objects that will be previewed",
|
|
|
|
type=bpy.types.Collection,
|
|
|
|
)
|
|
|
|
|
2024-03-10 11:56:37 +01:00
|
|
|
####################
|
|
|
|
# - Lock Methods
|
|
|
|
####################
|
|
|
|
def unlock_all(self):
|
|
|
|
for node in self.nodes:
|
|
|
|
node.locked = False
|
|
|
|
for bl_socket in [*node.inputs, *node.outputs]:
|
|
|
|
bl_socket.locked = False
|
|
|
|
|
|
|
|
####################
|
|
|
|
# - Update Methods
|
|
|
|
####################
|
|
|
|
def sync_node_removed(self, node: bpy.types.Node):
|
|
|
|
"""Run by `Node.free()` when a node is being removed.
|
|
|
|
|
|
|
|
Removes node input links from the internal cache (so we don't attempt to update non-existant sockets).
|
|
|
|
"""
|
|
|
|
for bl_socket in node.inputs.values():
|
|
|
|
# Retrieve Socket Links (if any)
|
|
|
|
self._node_link_cache.remove({
|
|
|
|
link.as_pointer()
|
|
|
|
for link in bl_socket.links
|
|
|
|
})
|
|
|
|
## ONLY Input Socket Links are Removed from the NodeLink Cache
|
|
|
|
## - update() handles link-removal from still-existing node just fine.
|
|
|
|
## - update() does NOT handle link-removal of non-existant nodes.
|
|
|
|
|
|
|
|
def update(self):
|
|
|
|
"""Run by Blender when 'something changes' in the node tree.
|
|
|
|
|
|
|
|
Updates an internal node link cache, then updates sockets that just lost/gained an input link.
|
|
|
|
"""
|
|
|
|
if not hasattr(self, "_node_link_cache"):
|
|
|
|
self._node_link_cache = NodeLinkCache(self)
|
|
|
|
## We presume update() is run before the first link is altered.
|
|
|
|
## - Else, the first link of the session will not update caches.
|
|
|
|
## - We remain slightly unsure of the semantics.
|
|
|
|
## - More testing needed to prevent this 'first-link bug'.
|
|
|
|
return
|
|
|
|
|
|
|
|
# Compute Changes to NodeLink Cache
|
|
|
|
delta_links = self._node_link_cache.regenerate()
|
|
|
|
|
|
|
|
link_alterations = {
|
|
|
|
"to_remove": [],
|
|
|
|
"to_add": [],
|
|
|
|
}
|
|
|
|
for link_ptr in delta_links["removed"]:
|
|
|
|
from_socket = self._node_link_cache.link_ptrs_from_sockets[link_ptr]
|
|
|
|
to_socket = self._node_link_cache.link_ptrs_to_sockets[link_ptr]
|
|
|
|
|
|
|
|
# Update Socket Caches
|
|
|
|
self._node_link_cache.link_ptrs_from_sockets.pop(link_ptr, None)
|
|
|
|
self._node_link_cache.link_ptrs_to_sockets.pop(link_ptr, None)
|
|
|
|
|
|
|
|
# Trigger Report Chain on Socket that Just Lost a Link
|
|
|
|
## Aka. Forward-Refresh Caches Relying on Linkage
|
|
|
|
if not (
|
|
|
|
consent_removal := to_socket.sync_link_removed(from_socket)
|
|
|
|
):
|
|
|
|
# Did Not Consent to Removal: Queue Add Link
|
|
|
|
link_alterations["to_add"].append((from_socket, to_socket))
|
|
|
|
|
|
|
|
for link_ptr in delta_links["added"]:
|
|
|
|
link = self._node_link_cache.link_ptrs_to_links.get(link_ptr)
|
|
|
|
if link is None: continue
|
|
|
|
|
|
|
|
# Trigger Report Chain on Socket that Just Gained a Link
|
|
|
|
## Aka. Forward-Refresh Caches Relying on Linkage
|
|
|
|
|
|
|
|
if not (
|
|
|
|
consent_added := link.to_socket.sync_link_added(link)
|
|
|
|
):
|
|
|
|
# Did Not Consent to Addition: Queue Remove Link
|
|
|
|
link_alterations["to_remove"].append(link)
|
|
|
|
|
|
|
|
# Execute Queued Operations
|
|
|
|
## - Especially undoing undesirable link changes.
|
|
|
|
## - This is important for locked graphs, whose links must not change.
|
|
|
|
for link in link_alterations["to_remove"]:
|
|
|
|
self.links.remove(link)
|
|
|
|
for from_socket, to_socket in link_alterations["to_add"]:
|
|
|
|
self.links.new(from_socket, to_socket)
|
|
|
|
|
|
|
|
# If Queued Operations: Regenerate Cache
|
|
|
|
## - This prevents the next update() from picking up on alterations.
|
|
|
|
if link_alterations["to_remove"] or link_alterations["to_add"]:
|
|
|
|
self._node_link_cache.regenerate()
|
|
|
|
|
|
|
|
####################
|
|
|
|
# - Post-Load Handler
|
|
|
|
####################
|
|
|
|
def initialize_sim_tree_node_link_cache(scene: bpy.types.Scene):
|
|
|
|
"""Whenever a file is loaded, create/regenerate the NodeLinkCache in all trees.
|
|
|
|
"""
|
|
|
|
for node_tree in bpy.data.node_groups:
|
|
|
|
if node_tree.bl_idname == "MaxwellSimTree":
|
|
|
|
if not hasattr(node_tree, "_node_link_cache"):
|
|
|
|
node_tree._node_link_cache = NodeLinkCache(node_tree)
|
|
|
|
else:
|
|
|
|
node_tree._node_link_cache.regenerate()
|
2024-02-14 12:33:40 +01:00
|
|
|
|
|
|
|
####################
|
|
|
|
# - Blender Registration
|
|
|
|
####################
|
2024-03-10 11:56:37 +01:00
|
|
|
bpy.app.handlers.load_post.append(initialize_sim_tree_node_link_cache)
|
|
|
|
|
2024-02-14 12:33:40 +01:00
|
|
|
BL_REGISTER = [
|
|
|
|
MaxwellSimTree,
|
|
|
|
]
|