feat: We did it, GeoNodes node w/live update!

We also implemented the TriMesh node, and established a strong
convention for updating nodes from sockets via. socket superclass
method. `trigger_updates`. It should be triggered as the
`update=` callback on **ALL PROPERTIES IN ALL SOCKETS**. This
method in turn calls the nodal `update()` function, which in turn
causes the node to chain-update all nodes linked to any output socket.

By default, `update()` is `pass`, so performance shouldn't be a concern,
but we should think about this deeper at some point.

Because update-chaining is done, we're ready for preview toggles on
node outputs. A lot of exciting things to do now!
blender-plugin-mvp
Sofus Albert Høgsbro Rose 2024-02-20 13:16:23 +01:00
parent 7344913c0e
commit 74d5a5daf8
Signed by: so-rose
GPG Key ID: AD901CB0F3701434
29 changed files with 396 additions and 28 deletions

View File

@ -450,6 +450,41 @@ SocketType_to_color = {
SocketType.MaxwellSimGridAxis: (0.4, 0.3, 0.25, 1.0), # Darkest Gold
}
BLNodeSocket_to_SocketType = {
"NodeSocketBool": SocketType.Bool,
"NodeSocketCollection": SocketType.BlenderCollection,
"NodeSocketColor": SocketType.Real3DVector,
"NodeSocketFloat": SocketType.RealNumber,
"NodeSocketFloatAngle": SocketType.RealNumber,
"NodeSocketFloatDistance": SocketType.RealNumber,
"NodeSocketFloatFactor": SocketType.RealNumber,
"NodeSocketFloatPercentage": SocketType.RealNumber,
"NodeSocketFloatTime": SocketType.RealNumber,
"NodeSocketFloatTimeAbsolute": SocketType.RealNumber,
"NodeSocketFloatUnsigned": SocketType.RealNumber,
"NodeSocketGeometry": SocketType.Any,
"NodeSocketImage": SocketType.BlenderImage,
"NodeSocketInt": SocketType.IntegerNumber,
"NodeSocketIntFactor": SocketType.IntegerNumber,
"NodeSocketIntPercentage": SocketType.IntegerNumber,
"NodeSocketIntUnsigned": SocketType.IntegerNumber,
"NodeSocketMaterial": SocketType.Any,
"NodeSocketObject": SocketType.BlenderObject,
"NodeSocketRotation": SocketType.Real3DVector,
"NodeSocketShader": SocketType.Any,
"NodeSocketStandard": SocketType.Any,
"NodeSocketString": SocketType.Text,
"NodeSocketTexture": SocketType.Any,
"NodeSocketVector": SocketType.Real3DVector,
"NodeSocketVectorAcceleration": SocketType.Real3DVector,
"NodeSocketVectorDirection": SocketType.Real3DVector,
"NodeSocketVectorEuler": SocketType.Real3DVector,
"NodeSocketVectorTranslation": SocketType.Real3DVector,
"NodeSocketVectorVelocity": SocketType.Real3DVector,
"NodeSocketVectorXYZ": SocketType.Real3DVector,
"NodeSocketVirtual": SocketType.Any,
}
####################
# - Node Types
####################

View File

@ -155,11 +155,6 @@ class MaxwellSimTreeNode(bpy.types.Node):
# Declare Node Property: 'preset' EnumProperty
if hasattr(cls, "input_socket_sets") or hasattr(cls, "output_socket_sets"):
if not hasattr(cls, "input_socket_sets"):
cls.input_socket_sets = {}
if not hasattr(cls, "output_socket_sets"):
cls.output_socket_sets = {}
socket_set_keys = [
input_socket_set_key
for input_socket_set_key in cls.input_socket_sets.keys()
@ -188,6 +183,11 @@ class MaxwellSimTreeNode(bpy.types.Node):
default=socket_set_keys[0]
)
if not hasattr(cls, "input_socket_sets"):
cls.input_socket_sets = {}
if not hasattr(cls, "output_socket_sets"):
cls.output_socket_sets = {}
# Declare Node Property: 'preset' EnumProperty
if hasattr(cls, "presets"):
first_preset = list(cls.presets.keys())[0]
@ -292,6 +292,18 @@ class MaxwellSimTreeNode(bpy.types.Node):
return ntree.bl_idname == contracts.TreeType.MaxwellSim.value
def update(self) -> None:
"""Called when some node properties (ex. links) change,
and/or by custom code."""
if hasattr(self, "update_cb"):
self.update_cb()
for bl_socket in self.outputs:
if bl_socket.is_linked:
for node_link in bl_socket.links:
linked_node = node_link.to_node
linked_node.update()
def _update_socket(self):
if not hasattr(self, "socket_set"):
raise ValueError("no socket")
@ -379,7 +391,7 @@ class MaxwellSimTreeNode(bpy.types.Node):
return self.inputs[self.input_sockets[input_socket_name].label]
elif hasattr(self, "input_socket_sets"):
elif hasattr(self, "socket_set"):
# You're on your own, chump
return self.inputs[next(
@ -412,7 +424,7 @@ class MaxwellSimTreeNode(bpy.types.Node):
return self.outputs[self.output_sockets[output_socket_name].label]
elif hasattr(self, "input_socket_sets"):
elif hasattr(self, "socket_set"):
return self.outputs[next(
socket_def.label
for socket_set, socket_dict in self.input_socket_sets.items()
@ -424,7 +436,7 @@ class MaxwellSimTreeNode(bpy.types.Node):
self,
output_bl_socket_name: contracts.BLSocketName,
) -> contracts.SocketName:
if hasattr(self, "output_socket_sets"):
if hasattr(self, "socket_set"):
return next(
socket_name
for socket_set, socket_dict in self.output_socket_sets.items()

View File

@ -1,5 +1,177 @@
import tidy3d as td
import numpy as np
import sympy as sp
import sympy.physics.units as spu
import bpy
import bmesh
from ... import contracts
from ... import sockets
from .. import base
GEONODES_MODIFIER_NAME = "BLMaxwell_GeoNodes"
class GeoNodesStructureNode(base.MaxwellSimTreeNode):
node_type = contracts.NodeType.GeoNodesStructure
bl_label = "GeoNodes Structure"
#bl_icon = ...
####################
# - Sockets
####################
input_sockets = {
"medium": sockets.MaxwellMediumSocketDef(
label="Medium",
),
"object": sockets.BlenderObjectSocketDef(
label="Object",
),
"geo_nodes": sockets.BlenderGeoNodesSocketDef(
label="GeoNodes",
),
}
output_sockets = {
"structure": sockets.MaxwellStructureSocketDef(
label="Structure",
),
}
####################
# - Output Socket Computation
####################
@base.computes_output_socket("structure")
def compute_simulation(self: contracts.NodeTypeProtocol) -> td.TriangleMesh:
# Extract the Blender Object
bl_object = self.compute_input("object")
# Ensure Updated Geometry
bpy.context.view_layer.update()
# Triangulate Object Mesh
bmesh_mesh = bmesh.new()
bmesh_mesh.from_mesh(bl_object.data)
bmesh.ops.triangulate(bmesh_mesh, faces=bmesh_mesh.faces)
mesh = bpy.data.meshes.new(name="TriangulatedMesh")
bmesh_mesh.to_mesh(mesh)
bmesh_mesh.free()
# Extract Vertices and Faces
vertices = np.array([vert.co for vert in mesh.vertices])
faces = np.array([
[vert for vert in poly.vertices]
for poly in mesh.polygons
])
# Remove Temporary Mesh
bpy.data.meshes.remove(mesh)
return td.Structure(
geometry=td.TriangleMesh.from_vertices_faces(vertices, faces),
medium=self.compute_input("medium")
)
####################
# - Update Function
####################
def update_cb(self) -> None:
bl_object = self.compute_input("object")
if bl_object is None: return
geo_nodes = self.compute_input("geo_nodes")
if geo_nodes is None: return
bl_modifier = bl_object.modifiers.get(GEONODES_MODIFIER_NAME)
if bl_modifier is None: return
# Set GeoNodes Modifier Attributes
for idx, interface_item in enumerate(
geo_nodes.interface.items_tree.values()
):
if idx == 0: continue ## Always-on "Geometry" Input (from Object)
bl_socket = self.inputs[
interface_item.name
]
if bl_socket.is_linked:
linked_bl_socket = bl_socket.links[0].from_socket
linked_bl_node = bl_socket.links[0].from_node
val = linked_bl_node.compute_output(
linked_bl_node.g_output_socket_name(
linked_bl_socket.name
)
) ## What a bunch of spaghetti
else:
val = self.inputs[
interface_item.name
].default_value
# Conservatively Set Differing Values
if bl_modifier[interface_item.identifier] != val:
bl_modifier[interface_item.identifier] = val
# Update DepGraph
bl_object.data.update()
def update_sockets_from_geonodes(self) -> None:
# Remove All "Loose" Sockets
socket_labels = {
socket_def.label
for socket_def in self.input_sockets.values()
} | {
socket_def.label
for socket_set_name, socket_set in self.input_socket_sets.items()
for socket_name, socket_def in socket_set.items()
}
bl_sockets_to_remove = {
bl_socket
for bl_socket_name, bl_socket in self.inputs.items()
if bl_socket_name not in socket_labels
}
for bl_socket in bl_sockets_to_remove:
self.inputs.remove(bl_socket)
# Query for Blender Object / Geo Nodes
bl_object = self.compute_input("object")
if bl_object is None: return
## TODO: Make object? Gray out geonodes if object not defined?
geo_nodes = self.compute_input("geo_nodes")
if geo_nodes is None: return
# Add Non-Static Sockets from GeoNodes
for bl_socket_name, bl_socket in geo_nodes.interface.items_tree.items():
# For now, don't allow Geometry inputs.
if bl_socket.socket_type == "NodeSocketGeometry": continue
self.inputs.new(
contracts.BLNodeSocket_to_SocketType[bl_socket.socket_type],
bl_socket_name,
)
# Create New GeoNodes Modifier
if GEONODES_MODIFIER_NAME not in bl_object.modifiers:
modifier = bl_object.modifiers.new(
name=GEONODES_MODIFIER_NAME,
type="NODES",
)
modifier.node_group = geo_nodes
self.update()
####################
# - Blender Registration
####################
BL_REGISTER = []
BL_NODES = {}
BL_REGISTER = [
GeoNodesStructureNode,
]
BL_NODES = {
contracts.NodeType.GeoNodesStructure: (
contracts.NodeCategory.MAXWELLSIM_STRUCTURES
)
}

View File

@ -1,6 +1,83 @@
import tidy3d as td
import numpy as np
import sympy as sp
import sympy.physics.units as spu
import bpy
import bmesh
from ... import contracts
from ... import sockets
from .. import base
class ObjectStructureNode(base.MaxwellSimTreeNode):
node_type = contracts.NodeType.ObjectStructure
bl_label = "Object Structure"
#bl_icon = ...
####################
# - Sockets
####################
input_sockets = {
"medium": sockets.MaxwellMediumSocketDef(
label="Medium",
),
"object": sockets.BlenderObjectSocketDef(
label="Object",
),
}
output_sockets = {
"structure": sockets.MaxwellStructureSocketDef(
label="Structure",
),
}
####################
# - Output Socket Computation
####################
@base.computes_output_socket("structure")
def compute_structure(self: contracts.NodeTypeProtocol) -> td.Structure:
# Extract the Blender Object
bl_object = self.compute_input("object")
# Ensure Updated Geometry
bpy.context.view_layer.update()
# Triangulate Object Mesh
bmesh_mesh = bmesh.new()
bmesh_mesh.from_mesh(bl_object.data)
bmesh.ops.triangulate(bmesh_mesh, faces=bmesh_mesh.faces)
mesh = bpy.data.meshes.new(name="TriangulatedMesh")
bmesh_mesh.to_mesh(mesh)
bmesh_mesh.free()
# Extract Vertices and Faces
vertices = np.array([vert.co for vert in mesh.vertices])
faces = np.array([
[vert for vert in poly.vertices]
for poly in mesh.polygons
])
# Remove Temporary Mesh
bpy.data.meshes.remove(mesh)
print(vertices)
return td.Structure(
geometry=td.TriangleMesh.from_vertices_faces(vertices, faces),
medium=self.compute_input("medium")
)
####################
# - Blender Registration
####################
BL_REGISTER = []
BL_NODES = {}
BL_REGISTER = [
ObjectStructureNode,
]
BL_NODES = {
contracts.NodeType.ObjectStructure: (
contracts.NodeCategory.MAXWELLSIM_STRUCTURES
)
}

View File

@ -140,6 +140,13 @@ class BLSocket(bpy.types.NodeSocket):
self._unit_previous = self.unit
####################
# - Callback Dispatcher
####################
def trigger_updates(self) -> None:
if not self.is_output:
self.node.update()
####################
# - Methods
####################

View File

@ -25,6 +25,7 @@ class BoolBLSocket(base.BLSocket):
name="Boolean",
description="Represents a boolean",
default=False,
update=(lambda self, context: self.trigger_updates()),
)
####################

View File

@ -27,6 +27,7 @@ class FilePathBLSocket(base.BLSocket):
description="Represents the path to a file",
#default="",
subtype="FILE_PATH",
update=(lambda self, context: self.trigger_updates()),
)
####################

View File

@ -27,6 +27,7 @@ class TextBLSocket(base.BLSocket):
name="Text",
description="Represents some text",
default="",
update=(lambda self, context: self.trigger_updates()),
)
####################

View File

@ -1,5 +1,6 @@
import typing as typ
import bpy
import pydantic as pyd
from .. import base
@ -12,16 +13,26 @@ class BlenderCollectionBLSocket(base.BLSocket):
socket_type = contracts.SocketType.BlenderCollection
bl_label = "BlenderCollection"
####################
# - Properties
####################
raw_value: bpy.props.PointerProperty(
name="Blender Collection",
description="Represents a Blender collection",
type=bpy.types.Collection,
update=(lambda self, context: self.trigger_updates()),
)
####################
# - Default Value
####################
@property
def default_value(self) -> None:
pass
def default_value(self) -> bpy.types.Collection | None:
return self.raw_value
@default_value.setter
def default_value(self, value: typ.Any) -> None:
pass
def default_value(self, value: bpy.types.Collection) -> None:
self.raw_value = value
####################
# - Socket Configuration

View File

@ -1,5 +1,6 @@
import typing as typ
import bpy
import pydantic as pyd
from .. import base
@ -12,16 +13,36 @@ class BlenderGeoNodesBLSocket(base.BLSocket):
socket_type = contracts.SocketType.BlenderGeoNodes
bl_label = "BlenderGeoNodes"
####################
# - Properties
####################
def update_geonodes_node(self):
if hasattr(self.node, "update_sockets_from_geonodes"):
self.node.update_sockets_from_geonodes()
else:
raise ValueError("Node doesn't have GeoNodes socket update method.")
# Run the Usual Updates
self.trigger_updates()
raw_value: bpy.props.PointerProperty(
name="Blender GeoNodes Tree",
description="Represents a Blender GeoNodes Tree",
type=bpy.types.NodeTree,
poll=(lambda self, obj: obj.bl_idname == 'GeometryNodeTree'),
update=(lambda self, context: self.update_geonodes_node()),
)
####################
# - Default Value
####################
@property
def default_value(self) -> None:
pass
def default_value(self) -> bpy.types.Object | None:
return self.raw_value
@default_value.setter
def default_value(self, value: typ.Any) -> None:
pass
def default_value(self, value: bpy.types.Object) -> None:
self.raw_value = value
####################
# - Socket Configuration

View File

@ -1,5 +1,6 @@
import typing as typ
import bpy
import pydantic as pyd
from .. import base

View File

@ -1,5 +1,6 @@
import typing as typ
import bpy
import pydantic as pyd
from .. import base
@ -12,16 +13,26 @@ class BlenderObjectBLSocket(base.BLSocket):
socket_type = contracts.SocketType.BlenderObject
bl_label = "BlenderObject"
####################
# - Properties
####################
raw_value: bpy.props.PointerProperty(
name="Blender Object",
description="Represents a Blender object",
type=bpy.types.Object,
update=(lambda self, context: self.trigger_updates()),
)
####################
# - Default Value
####################
@property
def default_value(self) -> None:
pass
def default_value(self) -> bpy.types.Object | None:
return self.raw_value
@default_value.setter
def default_value(self, value: typ.Any) -> None:
pass
def default_value(self, value: bpy.types.Object) -> None:
self.raw_value = value
####################
# - Socket Configuration

View File

@ -1,5 +1,6 @@
import typing as typ
import bpy
import pydantic as pyd
from .. import base

View File

@ -1,5 +1,6 @@
import typing as typ
import bpy
import pydantic as pyd
from .. import base

View File

@ -23,6 +23,7 @@ class MaxwellMediumBLSocket(base.BLSocket):
description="Represents a simple, real permittivity.",
default=0.0,
precision=4,
update=(lambda self, context: self.trigger_updates()),
)
####################

View File

@ -30,7 +30,8 @@ class ComplexNumberBLSocket(base.BLSocket):
description="Represents a complex number (real, imaginary)",
size=2,
default=(0.0, 0.0),
subtype='NONE'
subtype='NONE',
update=(lambda self, context: self.trigger_updates()),
)
coord_sys: bpy.props.EnumProperty(
name="Coordinate System",
@ -136,6 +137,8 @@ class ComplexNumberBLSocket(base.BLSocket):
sp.arg(cart_value) if y != 0 else 0,
)
self.trigger_updates()
####################
# - Socket Configuration
####################

View File

@ -30,6 +30,7 @@ class RealNumberBLSocket(base.BLSocket):
description="Represents a real number",
default=0.0,
precision=6,
update=(lambda self, context: self.trigger_updates()),
)
####################

View File

@ -22,6 +22,7 @@ class PhysicalAccelScalarBLSocket(base.BLSocket):
description="Represents the unitless part of the acceleration",
default=0.0,
precision=6,
update=(lambda self, context: self.trigger_updates()),
)
####################

View File

@ -22,6 +22,7 @@ class PhysicalAngleBLSocket(base.BLSocket):
description="Represents the unitless part of the acceleration",
default=0.0,
precision=4,
update=(lambda self, context: self.trigger_updates()),
)
####################

View File

@ -32,6 +32,7 @@ class PhysicalAreaBLSocket(base.BLSocket):
description="Represents the unitless part of the area",
default=0.0,
precision=6,
update=(lambda self, context: self.trigger_updates()),
)
####################

View File

@ -22,6 +22,7 @@ class PhysicalForceScalarBLSocket(base.BLSocket):
description="Represents the unitless part of the force",
default=0.0,
precision=6,
update=(lambda self, context: self.trigger_updates()),
)
####################

View File

@ -22,6 +22,7 @@ class PhysicalFreqBLSocket(base.BLSocket):
description="Represents the unitless part of the frequency",
default=0.0,
precision=6,
update=(lambda self, context: self.trigger_updates()),
)
####################

View File

@ -22,6 +22,7 @@ class PhysicalLengthBLSocket(base.BLSocket):
description="Represents the unitless part of the force",
default=0.0,
precision=6,
update=(lambda self, context: self.trigger_updates()),
)
####################

View File

@ -33,6 +33,7 @@ class PhysicalPoint3DBLSocket(base.BLSocket):
size=3,
default=(0.0, 0.0, 0.0),
precision=4,
update=(lambda self, context: self.trigger_updates()),
)
####################

View File

@ -33,6 +33,7 @@ class PhysicalSize3DBLSocket(base.BLSocket):
size=3,
default=(1.0, 1.0, 1.0),
precision=4,
update=(lambda self, context: self.trigger_updates()),
)
####################

View File

@ -22,6 +22,7 @@ class PhysicalTimeBLSocket(base.BLSocket):
description="Represents the unitless part of the force",
default=0.0,
precision=6,
update=(lambda self, context: self.trigger_updates()),
)
####################

View File

@ -32,6 +32,7 @@ class PhysicalVolumeBLSocket(base.BLSocket):
description="Represents the unitless part of the area",
default=0.0,
precision=6,
update=(lambda self, context: self.trigger_updates()),
)
####################

View File

@ -2,3 +2,4 @@ tidy3d==2.5.2
pydantic==2.6.0
sympy==1.12
scipy==1.12.0
trimesh==4.1.4

BIN
code/demo.blend (Stored with Git LFS)

Binary file not shown.