fix: GN update for custom geonodes
We also implement `BLField` support for Blender `IDStruct` types. There's a crash haunting us specifically with the cylinder array. Other primitives (esp. ring) work just fine, as does previews of nested-linked node groups. The crash triggers specifically when the file is saved and reloaded, whereafter all the `load_post` handlers run fine (like - extremely fine, we can perfectly access and dereference all of the node groups, seemingly all the objects, etc.). Then, crash. We're also discovering that `id_properties_ui` is completely useless for after-the-fact updating of custom properties within sockets. Which is a great surprise, and I have trouble thinking it's on purpose - the data is stored somewhere, after all. All the forced updates/redraws/etc. in the world don't seem to change this. We may have to go back to the drawing board with dynamically-updated min/max. The entire infrastructure with `SocketDef` altering sockets after creation is entirely, _violently_ unsuited to do a static modification. But the bare fact is, the dynamic modification methods are falling short. It's kind of important stuff, this stuff.main
parent
7ac6b615de
commit
785c6f764c
|
@ -149,6 +149,7 @@ def manage_pydeps(*_):
|
|||
# path_addon_pydeps='',
|
||||
# path_addon_reqs='',
|
||||
# )
|
||||
log.debug('PyDeps: Analyzing Post-File Load')
|
||||
ct.addon.prefs().on_addon_pydeps_changed(show_popup_if_deps_invalid=True)
|
||||
|
||||
|
||||
|
|
|
@ -220,7 +220,6 @@ class GeoNodes(enum.StrEnum):
|
|||
####################
|
||||
def import_geonodes(
|
||||
_geonodes: GeoNodes,
|
||||
force_append: bool = False,
|
||||
) -> bpy.types.GeometryNodeGroup:
|
||||
"""Given vendored GeoNodes tree link/append and return the local datablock.
|
||||
|
||||
|
@ -496,6 +495,7 @@ class GeoNodesToStructureNode(bpy.types.Operator):
|
|||
)
|
||||
bpy.ops.node.select_all(action='DESELECT')
|
||||
node = node_tree.nodes.new(geonodes.dedicated_node_type)
|
||||
node.sim_node_name = asset.name
|
||||
node.select = True
|
||||
node.location.x = node_location[0]
|
||||
node.location.y = node_location[1]
|
||||
|
@ -505,9 +505,9 @@ class GeoNodesToStructureNode(bpy.types.Operator):
|
|||
## Since the node doesn't itself handle the structure, we must.
|
||||
## We just import the GN tree, then attach the data block to the node.
|
||||
if geonodes.dedicated_node_type == 'GeoNodesStructureNodeType':
|
||||
## TODO: Is this too presumptuous? Or a fine little hack?
|
||||
geonodes_data = import_geonodes(asset.name)
|
||||
node.inputs['GeoNodes'].value = geonodes_data
|
||||
## TODO: Is this too presumptuous? Or a fine little hack?
|
||||
|
||||
# Restore the Pre-Modal Mouse Cursor Shape
|
||||
context.window.cursor_modal_restore()
|
||||
|
|
BIN
src/blender_maxwell/assets/internal/structure/_structure_primitive_cylinder.blend (Stored with Git LFS)
BIN
src/blender_maxwell/assets/internal/structure/_structure_primitive_cylinder.blend (Stored with Git LFS)
Binary file not shown.
BIN
src/blender_maxwell/assets/internal/structure/_structure_primitive_ring.blend (Stored with Git LFS)
BIN
src/blender_maxwell/assets/internal/structure/_structure_primitive_ring.blend (Stored with Git LFS)
Binary file not shown.
BIN
src/blender_maxwell/assets/structures/arrays/array_ring.blend (Stored with Git LFS)
BIN
src/blender_maxwell/assets/structures/arrays/array_ring.blend (Stored with Git LFS)
Binary file not shown.
|
@ -53,44 +53,44 @@ BLClass: typ.TypeAlias = (
|
|||
| bpy.types.FileHandler
|
||||
)
|
||||
BLIDStruct: typ.TypeAlias = (
|
||||
bpy.types.Action,
|
||||
bpy.types.Armature,
|
||||
bpy.types.Brush,
|
||||
bpy.types.CacheFile,
|
||||
bpy.types.Camera,
|
||||
bpy.types.Collection,
|
||||
bpy.types.Curve,
|
||||
bpy.types.Curves,
|
||||
bpy.types.FreestyleLineStyle,
|
||||
bpy.types.GreasePencil,
|
||||
bpy.types.Image,
|
||||
bpy.types.Key,
|
||||
bpy.types.Lattice,
|
||||
bpy.types.Library,
|
||||
bpy.types.Light,
|
||||
bpy.types.LightProbe,
|
||||
bpy.types.Mask,
|
||||
bpy.types.Material,
|
||||
bpy.types.Mesh,
|
||||
bpy.types.MetaBall,
|
||||
bpy.types.MovieClip,
|
||||
bpy.types.NodeTree,
|
||||
bpy.types.Object,
|
||||
bpy.types.PaintCurve,
|
||||
bpy.types.Palette,
|
||||
bpy.types.ParticleSettings,
|
||||
bpy.types.PointCloud,
|
||||
bpy.types.Scene,
|
||||
bpy.types.Screen,
|
||||
bpy.types.Sound,
|
||||
bpy.types.Speaker,
|
||||
bpy.types.Text,
|
||||
bpy.types.Texture,
|
||||
bpy.types.VectorFont,
|
||||
bpy.types.Volume,
|
||||
bpy.types.WindowManager,
|
||||
bpy.types.WorkSpace,
|
||||
bpy.types.World,
|
||||
bpy.types.Action
|
||||
| bpy.types.Armature
|
||||
| bpy.types.Brush
|
||||
| bpy.types.CacheFile
|
||||
| bpy.types.Camera
|
||||
| bpy.types.Collection
|
||||
| bpy.types.Curve
|
||||
| bpy.types.Curves
|
||||
| bpy.types.FreestyleLineStyle
|
||||
| bpy.types.GreasePencil
|
||||
| bpy.types.Image
|
||||
| bpy.types.Key
|
||||
| bpy.types.Lattice
|
||||
| bpy.types.Library
|
||||
| bpy.types.Light
|
||||
| bpy.types.LightProbe
|
||||
| bpy.types.Mask
|
||||
| bpy.types.Material
|
||||
| bpy.types.Mesh
|
||||
| bpy.types.MetaBall
|
||||
| bpy.types.MovieClip
|
||||
| bpy.types.NodeTree
|
||||
| bpy.types.Object
|
||||
| bpy.types.PaintCurve
|
||||
| bpy.types.Palette
|
||||
| bpy.types.ParticleSettings
|
||||
| bpy.types.PointCloud
|
||||
| bpy.types.Scene
|
||||
| bpy.types.Screen
|
||||
| bpy.types.Sound
|
||||
| bpy.types.Speaker
|
||||
| bpy.types.Text
|
||||
| bpy.types.Texture
|
||||
| bpy.types.VectorFont
|
||||
| bpy.types.Volume
|
||||
| bpy.types.WindowManager
|
||||
| bpy.types.WorkSpace
|
||||
| bpy.types.World
|
||||
)
|
||||
BLKeymapItem: typ.TypeAlias = typ.Any ## TODO: Better Type
|
||||
BLPropFlag: typ.TypeAlias = typ.Literal[
|
||||
|
|
|
@ -34,6 +34,9 @@ class OperatorType(enum.StrEnum):
|
|||
|
||||
GeoNodesToStructureNode = enum.auto()
|
||||
|
||||
# Socket: GeoNodesSocket
|
||||
SocketGeoNodesReset = enum.auto()
|
||||
|
||||
# Socket: Tidy3DCloudTask
|
||||
SocketCloudAuthenticate = enum.auto()
|
||||
SocketReloadCloudFolderList = enum.auto()
|
||||
|
|
|
@ -37,11 +37,41 @@ class BLSocketInfo:
|
|||
is_preview: bool
|
||||
socket_type: SocketType | None
|
||||
size: spux.NumberSize1D | None
|
||||
mathtype: spux.MathType | None
|
||||
physical_type: spux.PhysicalType | None
|
||||
default_value: spux.ScalarUnitlessRealExpr
|
||||
|
||||
bl_isocket_identifier: spux.ScalarUnitlessRealExpr
|
||||
|
||||
def encode(
|
||||
self, raw_value: typ.Any, unit_system: spux.UnitSystem | None
|
||||
) -> typ.Any:
|
||||
"""Encode a raw value, given a unit system, to be directly writable to a node socket.
|
||||
|
||||
This encoded form is also guaranteed to support writing to a node socket via a modifier interface.
|
||||
"""
|
||||
# Non-Numerical: Passthrough
|
||||
if unit_system is None or self.physical_type is None:
|
||||
return raw_value
|
||||
|
||||
# Numerical: Convert to Pure Python Type
|
||||
if (
|
||||
unit_system is not None
|
||||
and self.physical_type is not spux.PhysicalType.NonPhysical
|
||||
):
|
||||
unitless_value = spux.scale_to_unit_system(raw_value, unit_system)
|
||||
elif isinstance(raw_value, spux.SympyType):
|
||||
unitless_value = spux.sympy_to_python(raw_value)
|
||||
else:
|
||||
unitless_value = raw_value
|
||||
|
||||
# Coerce int -> float w/Target is Real
|
||||
## -> The value - modifier - GN path is more strict than properties.
|
||||
if self.mathtype is spux.MathType.Real and isinstance(unitless_value, int):
|
||||
return float(unitless_value)
|
||||
|
||||
return unitless_value
|
||||
|
||||
|
||||
class BLSocketType(enum.StrEnum):
|
||||
Virtual = 'NodeSocketVirtual'
|
||||
|
@ -85,12 +115,20 @@ class BLSocketType(enum.StrEnum):
|
|||
def from_bl_isocket(
|
||||
bl_isocket: bpy.types.NodeTreeInterfaceSocket,
|
||||
) -> typ.Self:
|
||||
"""Deduce the exact `BLSocketType` represented by an interface socket.
|
||||
|
||||
Interface sockets are an abstraction of what any instance of a particular node tree _will have of input sockets_ once constructed.
|
||||
"""
|
||||
return BLSocketType(bl_isocket.bl_socket_idname)
|
||||
|
||||
@staticmethod
|
||||
def info_from_bl_isocket(
|
||||
bl_isocket: bpy.types.NodeTreeInterfaceSocket,
|
||||
) -> typ.Self:
|
||||
"""Deduce all `BLSocketInfo` from an interface socket.
|
||||
|
||||
This is a high-level method providing a clean way to chain `BLSocketType.from_bl_isocket()` together with `self.parse()`.
|
||||
"""
|
||||
bl_socket_type = BLSocketType.from_bl_isocket(bl_isocket)
|
||||
if bl_socket_type.has_support:
|
||||
return bl_socket_type.parse(
|
||||
|
@ -103,13 +141,21 @@ class BLSocketType(enum.StrEnum):
|
|||
####################
|
||||
@property
|
||||
def has_support(self) -> bool:
|
||||
"""Decides whether the current `BLSocketType` is explicitly not supported.
|
||||
|
||||
Not all socket types make sense to represent in our node tree.
|
||||
In general, these should be skipped.
|
||||
"""
|
||||
BLST = BLSocketType
|
||||
return {
|
||||
# Won't Fix
|
||||
BLST.Virtual: False,
|
||||
BLST.Geometry: False,
|
||||
BLST.Shader: False,
|
||||
BLST.FloatUnsigned: False,
|
||||
BLST.IntUnsigned: False,
|
||||
## TODO
|
||||
BLST.Menu: False,
|
||||
}.get(self, True)
|
||||
|
||||
@property
|
||||
|
@ -132,12 +178,36 @@ class BLSocketType(enum.StrEnum):
|
|||
ST = SocketType
|
||||
return {
|
||||
# Blender
|
||||
BLST.Image: ST.BlenderImage,
|
||||
BLST.Material: ST.BlenderMaterial,
|
||||
BLST.Object: ST.BlenderObject,
|
||||
BLST.Collection: ST.BlenderCollection,
|
||||
# Basic
|
||||
BLST.Bool: ST.Bool,
|
||||
BLST.Bool: ST.BlenderCollection,
|
||||
BLST.String: ST.String,
|
||||
# Float
|
||||
# Array-Like
|
||||
BLST.Float: ST.Expr,
|
||||
BLST.FloatAngle: ST.Expr,
|
||||
BLST.FloatDistance: ST.Expr,
|
||||
BLST.FloatFactor: ST.Expr,
|
||||
BLST.FloatPercentage: ST.Expr,
|
||||
BLST.FloatTime: ST.Expr,
|
||||
BLST.FloatTimeAbsolute: ST.Expr,
|
||||
# Int
|
||||
BLST.Int: ST.Expr,
|
||||
BLST.IntFactor: ST.Expr,
|
||||
BLST.IntPercentage: ST.Expr,
|
||||
# Vector
|
||||
BLST.Color: ST.Color,
|
||||
}.get(self, ST.Expr)
|
||||
BLST.Rotation: ST.Expr,
|
||||
BLST.Vector: ST.Expr,
|
||||
BLST.VectorAcceleration: ST.Expr,
|
||||
BLST.VectorDirection: ST.Expr,
|
||||
BLST.VectorEuler: ST.Expr,
|
||||
BLST.VectorTranslation: ST.Expr,
|
||||
BLST.VectorVelocity: ST.Expr,
|
||||
BLST.VectorXYZ: ST.Expr,
|
||||
}[self]
|
||||
|
||||
@property
|
||||
def mathtype(self) -> spux.MathType | None:
|
||||
|
@ -240,7 +310,7 @@ class BLSocketType(enum.StrEnum):
|
|||
BLST.FloatAngle: P.Angle,
|
||||
BLST.FloatDistance: P.Length,
|
||||
BLST.FloatTime: P.Time,
|
||||
BLST.FloatTimeAbsolute: P.Time, ## What's the difference?
|
||||
BLST.FloatTimeAbsolute: P.Time, ## TODO: What's the difference?
|
||||
BLST.VectorAcceleration: P.Accel,
|
||||
## BLST.VectorDirection: Directions are unitless (within cartesian)
|
||||
BLST.VectorEuler: P.Angle,
|
||||
|
@ -291,8 +361,8 @@ class BLSocketType(enum.StrEnum):
|
|||
def parse(
|
||||
self, bl_default_value: typ.Any, description: str, bl_isocket_identifier: str
|
||||
) -> BLSocketInfo:
|
||||
# Parse the Description
|
||||
## TODO: Some kind of error on invalid parse if there is also no unambiguous physical type
|
||||
# Unpack Description
|
||||
## -> TODO: Raise an kind of error on invalid parse if there is also no unambiguous physical type
|
||||
descr_params = description.split(BL_SOCKET_DESCR_ANNOT_STRING)[0]
|
||||
directive = (
|
||||
_tokens[0]
|
||||
|
@ -300,33 +370,60 @@ class BLSocketType(enum.StrEnum):
|
|||
else _tokens[1]
|
||||
)
|
||||
|
||||
## Interpret the Description Parse
|
||||
# Parse PhysicalType
|
||||
## -> None if there is no appropriate MathType.
|
||||
## -> Otherwise, prefer unambiguous - description hint - NonPhysical
|
||||
has_physical_type = self.mathtype in [
|
||||
spux.MathType.Integer,
|
||||
spux.MathType.Rational,
|
||||
spux.MathType.Real,
|
||||
spux.MathType.Complex,
|
||||
]
|
||||
if has_physical_type:
|
||||
parsed_physical_type = getattr(spux.PhysicalType, directive, None)
|
||||
physical_type = (
|
||||
self.unambiguous_physical_type
|
||||
if self.unambiguous_physical_type is not None
|
||||
else parsed_physical_type
|
||||
else (
|
||||
parsed_physical_type
|
||||
if parsed_physical_type is not None
|
||||
else spux.PhysicalType.NonPhysical
|
||||
)
|
||||
)
|
||||
else:
|
||||
physical_type = None
|
||||
|
||||
# Parse the Default Value
|
||||
# Parse Default Value
|
||||
## -> Read the Blender socket's default value and convrt it
|
||||
if self.mathtype is not None and bl_default_value is not None:
|
||||
# Scalar: Convert to Pure Python TYpe
|
||||
if self.size == spux.NumberSize1D.Scalar:
|
||||
default_value = self.mathtype.pytype(bl_default_value)
|
||||
|
||||
# 2D (Description Hint): Sympy Matrix
|
||||
## -> The description hint "2D" is the trigger for this.
|
||||
## -> Ignore the last component to get the effect of "2D".
|
||||
elif description.startswith('2D'):
|
||||
default_value = sp.Matrix(tuple(bl_default_value)[:2])
|
||||
default_value = sp.ImmutableMatrix(tuple(bl_default_value)[:2])
|
||||
|
||||
# 3D/4D: Simple Parse to Sympy Matrix
|
||||
## -> We don't explicitly check the size.
|
||||
else:
|
||||
default_value = sp.Matrix(tuple(bl_default_value))
|
||||
default_value = sp.ImmutableMatrix(tuple(bl_default_value))
|
||||
|
||||
else:
|
||||
# Non-Mathematical: Passthrough
|
||||
default_value = bl_default_value
|
||||
|
||||
# Return Parsed Socket Information
|
||||
## -> Combining directly known and parsed knowledge.
|
||||
## -> Should contain everything needed to match the Blender socket.
|
||||
## -> Should contain everything needed to create a socket in our tree.
|
||||
return BLSocketInfo(
|
||||
has_support=self.has_support,
|
||||
is_preview=(directive == 'Preview'),
|
||||
socket_type=self.socket_type,
|
||||
size=self.size,
|
||||
mathtype=self.mathtype,
|
||||
physical_type=physical_type,
|
||||
default_value=default_value,
|
||||
bl_isocket_identifier=bl_isocket_identifier,
|
||||
|
|
|
@ -103,8 +103,8 @@ class ManagedBLMesh(base.ManagedObj):
|
|||
"""
|
||||
bl_object = bpy.data.objects.get(self.name)
|
||||
if bl_object is None:
|
||||
log.info('%s (ManagedBLMesh): Created BLObject for Preview', bl_object.name)
|
||||
bl_object = self.bl_object()
|
||||
log.info('%s (ManagedBLMesh): Created BLObject for Preview', bl_object.name)
|
||||
|
||||
if bl_object.name not in preview_collection().objects:
|
||||
log.info('Moving "%s" to Preview Collection', bl_object.name)
|
||||
|
|
|
@ -97,6 +97,7 @@ def write_modifier_geonodes(
|
|||
modifier_altered = False
|
||||
|
||||
# Alter GeoNodes Group
|
||||
## -> Check the existing node group, replace if it differs.
|
||||
if bl_modifier.node_group != modifier_attrs['node_group']:
|
||||
log.info(
|
||||
'Changing GeoNodes Modifier NodeTree from "%s" to "%s"',
|
||||
|
@ -106,30 +107,25 @@ def write_modifier_geonodes(
|
|||
bl_modifier.node_group = modifier_attrs['node_group']
|
||||
modifier_altered = True
|
||||
|
||||
# Alter GeoNodes Modifier Inputs
|
||||
# Parse GeoNodes Socket Info
|
||||
## -> TODO: Slow and hard to optimize, but very likely worth it.
|
||||
socket_infos = bl_socket_map.info_from_geonodes(bl_modifier.node_group)
|
||||
|
||||
for socket_name in modifier_attrs['inputs']:
|
||||
# Retrieve Modifier Interface ID
|
||||
## -> iface_id translates "modifier socket" to "GN input socket".
|
||||
iface_id = socket_infos[socket_name].bl_isocket_identifier
|
||||
input_value = modifier_attrs['inputs'][socket_name]
|
||||
|
||||
if modifier_attrs['unit_system'] is not None and not isinstance(
|
||||
input_value, bool
|
||||
):
|
||||
value_to_write = spux.scale_to_unit_system(
|
||||
input_value, modifier_attrs['unit_system']
|
||||
# Deduce Value to Write
|
||||
## -> This may involve a unit system conversion.
|
||||
## -> Special Case: Booleans do not go through unit conversion.
|
||||
## -> TODO: A special case isn't clean enough.
|
||||
bl_modifier[iface_id] = socket_infos[socket_name].encode(
|
||||
raw_value=modifier_attrs['inputs'][socket_name],
|
||||
unit_system=modifier_attrs['unit_system'],
|
||||
)
|
||||
else:
|
||||
value_to_write = input_value
|
||||
|
||||
# Edge Case: int -> float
|
||||
if isinstance(bl_modifier[iface_id], float) and isinstance(value_to_write, int):
|
||||
bl_modifier[iface_id] = float(value_to_write)
|
||||
else:
|
||||
bl_modifier[iface_id] = value_to_write
|
||||
|
||||
modifier_altered = True
|
||||
## TODO: More fine-grained alterations
|
||||
## TODO: More fine-grained alterations?
|
||||
|
||||
return modifier_altered # noqa: RET504
|
||||
|
||||
|
|
|
@ -435,6 +435,7 @@ def initialize_sim_tree_node_link_cache(_):
|
|||
"""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':
|
||||
log.debug('%s: Initializing NodeLinkCache for NodeTree', str(node_tree))
|
||||
node_tree.on_load()
|
||||
|
||||
|
||||
|
@ -450,13 +451,26 @@ def populate_missing_persistence(_) -> None:
|
|||
for _node_tree in bpy.data.node_groups
|
||||
if _node_tree.bl_idname == ct.TreeType.MaxwellSim.value and _node_tree.is_active
|
||||
]:
|
||||
log.debug(
|
||||
'%s: Regenerating Dynamic Field Persistance for NodeTree nodes/sockets',
|
||||
str(node_tree),
|
||||
)
|
||||
# Iterate over MaxwellSim Nodes
|
||||
# -> Excludes ex. frame and reroute nodes.
|
||||
for node in [_node for _node in node_tree.nodes if hasattr(_node, 'node_type')]:
|
||||
log.debug(
|
||||
'-> %s: Regenerating Dynamic Field Persistance for Node',
|
||||
str(node),
|
||||
)
|
||||
node.regenerate_dynamic_field_persistance()
|
||||
for bl_sockets in [node.inputs, node.outputs]:
|
||||
for bl_socket in bl_sockets:
|
||||
log.debug(
|
||||
'|-> %s: Regenerating Dynamic Field Persistance for Socket',
|
||||
str(bl_socket),
|
||||
)
|
||||
bl_socket.regenerate_dynamic_field_persistance()
|
||||
log.debug('Regenerated All Dynamic Field Persistance')
|
||||
|
||||
|
||||
####################
|
||||
|
|
|
@ -737,6 +737,14 @@ class MaxwellSimNode(bpy.types.Node, bl_instance.BLInstance):
|
|||
socket_name: The input socket that was altered, if any, in order to trigger this event.
|
||||
pop_name: The property that was altered, if any, in order to trigger this event.
|
||||
"""
|
||||
# log.debug(
|
||||
# '%s: Triggered Event %s (socket_name=%s, socket_kinds=%s, prop_name=%s)',
|
||||
# self.sim_node_name,
|
||||
# event,
|
||||
# str(socket_name),
|
||||
# str(socket_kinds),
|
||||
# str(prop_name),
|
||||
# )
|
||||
# Outflow Socket Kinds
|
||||
## -> Something has happened!
|
||||
## -> The effect is yet to be determined...
|
||||
|
@ -815,9 +823,9 @@ class MaxwellSimNode(bpy.types.Node, bl_instance.BLInstance):
|
|||
for event_method in triggered_event_methods:
|
||||
stop_propagation |= event_method.stop_propagation
|
||||
# log.critical(
|
||||
# '$[%s] [%s %s %s %s] Running: (%s)',
|
||||
# '%s: Running %s',
|
||||
# self.sim_node_name,
|
||||
# event_method.callback_info,
|
||||
# str(event_method.callback_info),
|
||||
# )
|
||||
event_method(self)
|
||||
|
||||
|
|
|
@ -43,8 +43,6 @@ class GeoNodesStructureNode(base.MaxwellSimNode):
|
|||
'Medium': sockets.MaxwellMediumSocketDef(),
|
||||
'Center': sockets.ExprSocketDef(
|
||||
size=spux.NumberSize1D.Vec3,
|
||||
mathtype=spux.MathType.Real,
|
||||
physical_type=spux.PhysicalType.Length,
|
||||
default_unit=spu.micrometer,
|
||||
default_value=sp.Matrix([0, 0, 0]),
|
||||
),
|
||||
|
@ -54,7 +52,6 @@ class GeoNodesStructureNode(base.MaxwellSimNode):
|
|||
}
|
||||
|
||||
managed_obj_types: typ.ClassVar = {
|
||||
'mesh': managed_objs.ManagedBLMesh,
|
||||
'modifier': managed_objs.ManagedBLModifier,
|
||||
}
|
||||
|
||||
|
@ -64,7 +61,7 @@ class GeoNodesStructureNode(base.MaxwellSimNode):
|
|||
@events.computes_output_socket(
|
||||
'Structure',
|
||||
input_sockets={'Medium'},
|
||||
managed_objs={'mesh'},
|
||||
managed_objs={'modifier'},
|
||||
)
|
||||
def compute_structure(
|
||||
self,
|
||||
|
@ -74,7 +71,7 @@ class GeoNodesStructureNode(base.MaxwellSimNode):
|
|||
"""Computes a triangle-mesh based Tidy3D structure, by manually copying mesh data from Blender to a `td.TriangleMesh`."""
|
||||
## TODO: mesh_as_arrays might not take the Center into account.
|
||||
## - Alternatively, Tidy3D might have a way to transform?
|
||||
mesh_as_arrays = managed_objs['mesh'].mesh_as_arrays
|
||||
mesh_as_arrays = managed_objs['modifier'].mesh_as_arrays
|
||||
return td.Structure(
|
||||
geometry=td.TriangleMesh.from_vertices_faces(
|
||||
mesh_as_arrays['verts'],
|
||||
|
@ -84,71 +81,28 @@ class GeoNodesStructureNode(base.MaxwellSimNode):
|
|||
)
|
||||
|
||||
####################
|
||||
# - Events: Preview Active Changed
|
||||
# - UI
|
||||
####################
|
||||
@events.on_value_changed(
|
||||
prop_name='preview_active',
|
||||
props={'preview_active'},
|
||||
managed_objs={'mesh'},
|
||||
)
|
||||
def on_preview_changed(self, props) -> None:
|
||||
"""Enables/disables previewing of the GeoNodes-driven mesh, regardless of whether a particular GeoNodes tree is chosen."""
|
||||
mesh = managed_objs['mesh']
|
||||
def draw_label(self) -> None:
|
||||
"""Show the extracted data (if any) in the node's header label.
|
||||
|
||||
# Push Preview State to Managed Mesh
|
||||
if props['preview_active']:
|
||||
mesh.show_preview()
|
||||
else:
|
||||
mesh.hide_preview()
|
||||
|
||||
####################
|
||||
# - Events: GN Input Changed
|
||||
####################
|
||||
@events.on_value_changed(
|
||||
socket_name={'Center'},
|
||||
any_loose_input_socket=True,
|
||||
# Pass Data
|
||||
managed_objs={'mesh', 'modifier'},
|
||||
input_sockets={'Center', 'GeoNodes'},
|
||||
all_loose_input_sockets=True,
|
||||
unit_systems={'BlenderUnits': ct.UNITS_BLENDER},
|
||||
scale_input_sockets={'Center': 'BlenderUnits'},
|
||||
)
|
||||
def on_input_socket_changed(
|
||||
self, input_sockets, loose_input_sockets, unit_systems
|
||||
) -> None:
|
||||
"""Pushes any change in GeoNodes-bound input sockets to the GeoNodes modifier.
|
||||
|
||||
Also pushes the `Center:Value` socket to govern the object's center in 3D space.
|
||||
Notes:
|
||||
Called by Blender to determine the text to place in the node's header.
|
||||
"""
|
||||
geonodes = input_sockets['GeoNodes']
|
||||
has_geonodes = not ct.FlowSignal.check(geonodes)
|
||||
geonodes = self._compute_input('GeoNodes')
|
||||
if geonodes is None:
|
||||
return self.bl_label
|
||||
|
||||
if has_geonodes:
|
||||
mesh = managed_objs['mesh']
|
||||
modifier = managed_objs['modifier']
|
||||
center = input_sockets['Center']
|
||||
unit_system = unit_systems['BlenderUnits']
|
||||
|
||||
# Push Loose Input Values to GeoNodes Modifier
|
||||
modifier.bl_modifier(
|
||||
mesh.bl_object(location=center),
|
||||
'NODES',
|
||||
{
|
||||
'node_group': geonodes,
|
||||
'inputs': loose_input_sockets,
|
||||
'unit_system': unit_system,
|
||||
},
|
||||
)
|
||||
return f'Structure: {self.sim_node_name}'
|
||||
|
||||
####################
|
||||
# - Events: GN Tree Changed
|
||||
# - Events: Swapped GN Node Tree
|
||||
####################
|
||||
@events.on_value_changed(
|
||||
socket_name={'GeoNodes'},
|
||||
# Pass Data
|
||||
managed_objs={'mesh', 'modifier'},
|
||||
input_sockets={'GeoNodes', 'Center'},
|
||||
# Loaded
|
||||
managed_objs={'modifier'},
|
||||
input_sockets={'GeoNodes'},
|
||||
)
|
||||
def on_input_changed(
|
||||
self,
|
||||
|
@ -157,12 +111,9 @@ class GeoNodesStructureNode(base.MaxwellSimNode):
|
|||
) -> None:
|
||||
"""Declares new loose input sockets in response to a new GeoNodes tree (if any)."""
|
||||
geonodes = input_sockets['GeoNodes']
|
||||
has_geonodes = not ct.FlowSignal.check(geonodes)
|
||||
has_geonodes = not ct.FlowSignal.check(geonodes) and geonodes is not None
|
||||
|
||||
if has_geonodes:
|
||||
mesh = managed_objs['mesh']
|
||||
modifier = managed_objs['modifier']
|
||||
|
||||
# Fill the Loose Input Sockets
|
||||
## -> The SocketDefs contain the default values from the interface.
|
||||
log.info(
|
||||
|
@ -176,9 +127,59 @@ class GeoNodesStructureNode(base.MaxwellSimNode):
|
|||
|
||||
elif self.loose_input_sockets:
|
||||
self.loose_input_sockets = {}
|
||||
managed_objs['modifier'].free()
|
||||
|
||||
if modifier.name in mesh.bl_object().modifiers.keys().copy():
|
||||
modifier.free_from_bl_object(mesh.bl_object())
|
||||
####################
|
||||
# - Events: Preview
|
||||
####################
|
||||
@events.on_value_changed(
|
||||
# Trigger
|
||||
prop_name='preview_active',
|
||||
# Loaded
|
||||
managed_objs={'modifier'},
|
||||
props={'preview_active'},
|
||||
)
|
||||
def on_preview_changed(self, managed_objs, props):
|
||||
if props['preview_active']:
|
||||
managed_objs['modifier'].show_preview()
|
||||
else:
|
||||
managed_objs['modifier'].hide_preview()
|
||||
|
||||
@events.on_value_changed(
|
||||
# Trigger
|
||||
socket_name={'Center', 'GeoNodes'}, ## MUST run after on_input_changed
|
||||
any_loose_input_socket=True,
|
||||
# Loaded
|
||||
managed_objs={'modifier'},
|
||||
input_sockets={'Center', 'GeoNodes'},
|
||||
all_loose_input_sockets=True,
|
||||
unit_systems={'BlenderUnits': ct.UNITS_BLENDER},
|
||||
scale_input_sockets={'Center': 'BlenderUnits'},
|
||||
)
|
||||
def on_input_socket_changed(
|
||||
self, managed_objs, input_sockets, loose_input_sockets, unit_systems
|
||||
) -> None:
|
||||
"""Pushes any change in GeoNodes-bound input sockets to the GeoNodes modifier.
|
||||
|
||||
Warnings:
|
||||
MUST be placed lower than `on_input_changed`, so it runs afterwards whenever the `GeoNodes` tree is changed.
|
||||
|
||||
Also pushes the `Center:Value` socket to govern the object's center in 3D space.
|
||||
"""
|
||||
geonodes = input_sockets['GeoNodes']
|
||||
has_geonodes = not ct.FlowSignal.check(geonodes) and geonodes is not None
|
||||
|
||||
if has_geonodes:
|
||||
# Push Loose Input Values to GeoNodes Modifier
|
||||
managed_objs['modifier'].bl_modifier(
|
||||
'NODES',
|
||||
{
|
||||
'node_group': geonodes,
|
||||
'inputs': loose_input_sockets,
|
||||
'unit_system': unit_systems['BlenderUnits'],
|
||||
},
|
||||
location=input_sockets['Center'],
|
||||
)
|
||||
|
||||
|
||||
####################
|
||||
|
|
|
@ -83,7 +83,7 @@ class BoxStructureNode(base.MaxwellSimNode):
|
|||
)
|
||||
|
||||
####################
|
||||
# - Preview
|
||||
# - Events: Preview
|
||||
####################
|
||||
@events.on_value_changed(
|
||||
# Trigger
|
||||
|
|
|
@ -14,30 +14,40 @@
|
|||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import typing as typ
|
||||
|
||||
import bpy
|
||||
import sympy as sp
|
||||
import sympy.physics.units as spu
|
||||
import tidy3d as td
|
||||
|
||||
from blender_maxwell.assets.geonodes import GeoNodes, import_geonodes
|
||||
from blender_maxwell.utils import bl_cache, logger
|
||||
from blender_maxwell.utils import extra_sympy_units as spux
|
||||
|
||||
from ... import contracts as ct
|
||||
from .. import base
|
||||
|
||||
log = logger.get(__name__)
|
||||
|
||||
|
||||
####################
|
||||
# - Operators
|
||||
####################
|
||||
class BlenderMaxwellResetGeoNodesSocket(bpy.types.Operator):
|
||||
bl_idname = 'blender_maxwell.reset_geo_nodes_socket'
|
||||
bl_label = 'Reset GeoNodes Socket'
|
||||
"""Simulate a change to the geometry nodes group of the attached `GeoNodes` socket.
|
||||
|
||||
node_tree_name: bpy.props.StringProperty(name='Node Tree Name')
|
||||
node_name: bpy.props.StringProperty(name='Node Name')
|
||||
socket_name: bpy.props.StringProperty(name='Socket Name')
|
||||
This causes updates to the GN group (ex. internal logic) to be immediately caught.
|
||||
"""
|
||||
|
||||
bl_idname = ct.OperatorType.SocketGeoNodesReset
|
||||
bl_label = 'Reset GeoNodes Group'
|
||||
|
||||
def execute(self, context):
|
||||
node_tree = bpy.data.node_groups[self.node_tree_name]
|
||||
node = node_tree.nodes[self.node_name]
|
||||
socket = node.inputs[self.socket_name]
|
||||
bl_socket = context.socket
|
||||
|
||||
# Report as though the GeoNodes Tree Changed
|
||||
socket.on_prop_changed('raw_value', context)
|
||||
bl_socket.on_prop_changed('raw_value', context)
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
|
@ -52,35 +62,18 @@ class BlenderGeoNodesBLSocket(base.MaxwellSimSocket):
|
|||
####################
|
||||
# - Properties
|
||||
####################
|
||||
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.on_prop_changed('raw_value', context)),
|
||||
raw_value: bpy.types.NodeTree = bl_cache.BLField(
|
||||
bltype_poll=lambda self, obj: self.filter_gn_trees(obj)
|
||||
)
|
||||
|
||||
####################
|
||||
# - UI
|
||||
####################
|
||||
# def draw_label_row(self, label_col_row, text):
|
||||
# label_col_row.label(text=text)
|
||||
# if not self.raw_value: return
|
||||
#
|
||||
# op = label_col_row.operator(
|
||||
# BlenderMaxwellResetGeoNodesSocket.bl_idname,
|
||||
# text="",
|
||||
# icon="FILE_REFRESH",
|
||||
# )
|
||||
# op.socket_name = self.name
|
||||
# op.node_name = self.node.name
|
||||
# op.node_tree_name = self.node.id_data.name
|
||||
def filter_gn_trees(self, obj: ct.BLIDStruct) -> bool:
|
||||
return obj.bl_idname == 'GeometryNodeTree' and not obj.name.startswith('_')
|
||||
|
||||
####################
|
||||
# - UI
|
||||
####################
|
||||
def draw_value(self, col: bpy.types.UILayout) -> None:
|
||||
col.prop(self, 'raw_value', text='')
|
||||
col.prop(self, self.blfields['raw_value'], text='')
|
||||
|
||||
####################
|
||||
# - Default Value
|
||||
|
|
|
@ -36,6 +36,7 @@ StringPropSubType: typ.TypeAlias = typ.Literal[
|
|||
'FILE_PATH', 'DIR_PATH', 'FILE_NAME', 'BYTE_STRING', 'PASSWORD', 'NONE'
|
||||
]
|
||||
|
||||
PollMethod: typ.TypeAlias = typ.Callable[[bl_instance.BLInstance, typ.Any], bool]
|
||||
StrMethod: typ.TypeAlias = typ.Callable[
|
||||
[bl_instance.BLInstance, bpy.types.Context, str], list[tuple[str, str]]
|
||||
]
|
||||
|
@ -72,8 +73,7 @@ class BLField:
|
|||
float_prec: int | None = None,
|
||||
str_secret: bool | None = None,
|
||||
path_type: typ.Literal['dir', 'file'] | None = None,
|
||||
# blptr_type: typ.Any | None = None, ## A Blender ID type
|
||||
## TODO: Test/Implement
|
||||
bltype_poll: PollMethod | None = None,
|
||||
## Dynamic
|
||||
str_cb: StrMethod | None = None,
|
||||
enum_cb: EnumMethod | None = None,
|
||||
|
@ -110,6 +110,10 @@ class BLField:
|
|||
Only meaningful for `pathlib.Path` properties.
|
||||
**NOTE**: No effort is made to make paths portable between operating systems.
|
||||
Use with care.
|
||||
bltype_poll: User-provided method for filtering selectable datablocks.
|
||||
Whenever the annotated type is `ct.BLIDStruct`, it may be desirable to constrain which datablocks the user should be able to select.
|
||||
This allows doing so.
|
||||
**NOTE**: The method will be run very often, and must therefore be cheap.
|
||||
str_cb: Method used to determine all valid strings, which presents to the user as a fuzzy-style search dropdown.
|
||||
Only meaningful for `str` properties.
|
||||
Results are not persisted, and must therefore re-run when reloading the file.
|
||||
|
@ -121,12 +125,6 @@ class BLField:
|
|||
cb_depends_on: Declares that `str_cb` / `enum_cb` should be regenerated whenever any of the given property names change.
|
||||
This allows fully automating the invocation of `Signal.ResetEnumItems` / `Signal.ResetStrSearch` in common cases.
|
||||
"""
|
||||
log.debug(
|
||||
'Initializing BLField (default_value=%s, use_prop_update=%s)',
|
||||
str(default_value),
|
||||
str(use_prop_update),
|
||||
)
|
||||
|
||||
self.use_dynamic_enum = enum_cb is not None
|
||||
self.use_str_search = str_cb is not None
|
||||
|
||||
|
@ -143,7 +141,7 @@ class BLField:
|
|||
'step': float_step,
|
||||
'precision': float_prec,
|
||||
# BLPointer: ID Type
|
||||
#'blptr_type': blptr_type,
|
||||
'bltype_poll': bltype_poll,
|
||||
# Str | Path | Enum: Flag Setters
|
||||
'str_secret': str_secret,
|
||||
'path_type': path_type,
|
||||
|
@ -222,6 +220,14 @@ class BLField:
|
|||
)
|
||||
self.bl_prop_str_search.init_bl_type(owner)
|
||||
|
||||
log.debug(
|
||||
'Initialized BLField "%s" (prop_type=%s, type=%s, default_value=%s)',
|
||||
str(self.bl_prop.name),
|
||||
str(self.bl_prop.bl_prop_type),
|
||||
str(self.bl_prop.prop_type),
|
||||
str(self.bl_prop.default_value),
|
||||
)
|
||||
|
||||
def __get__(
|
||||
self,
|
||||
bl_instance: bl_instance.BLInstance | None,
|
||||
|
@ -290,6 +296,9 @@ class BLField:
|
|||
|
||||
# Reset Enum Items
|
||||
elif value is Signal.ResetEnumItems:
|
||||
if self.bl_prop_enum_items is None:
|
||||
return
|
||||
|
||||
# Retrieve Old Items
|
||||
## -> This is verbatim what is being persisted, currently.
|
||||
## -> len(0): Manually replaced w/fallback to guarantee >=len(1)
|
||||
|
@ -356,6 +365,9 @@ class BLField:
|
|||
|
||||
# Reset Str Search
|
||||
elif value is Signal.ResetStrSearch:
|
||||
if self.bl_prop_str_search is None:
|
||||
return
|
||||
|
||||
self.bl_prop_str_search.invalidate_nonpersist(bl_instance)
|
||||
|
||||
# General __set__
|
||||
|
|
|
@ -173,6 +173,10 @@ class BLProp:
|
|||
def read_nonpersist(self, bl_instance: bl_instance.BLInstance | None) -> typ.Any:
|
||||
"""Read the non-persistent cache value for this property.
|
||||
|
||||
Notes:
|
||||
**Never reads cached BLPointers**; invokes `self.read()` instead.
|
||||
`BLPointer`s must align perfectly with Blender's internal logic, and as such, the cache cannot get involved, else we risk access-after-free crashes.
|
||||
|
||||
Returns:
|
||||
Generally, the cache value, with two exceptions.
|
||||
|
||||
|
@ -184,6 +188,8 @@ class BLProp:
|
|||
- `Signal.CacheEmpty`: When the cache has no entry.
|
||||
A good idea might be to fill it immediately with `self.write_nonpersist(bl_instance)`.
|
||||
"""
|
||||
if self.bl_prop_type is BLPropType.BLPointer:
|
||||
return self.read(bl_instance)
|
||||
return managed_cache.read(
|
||||
bl_instance,
|
||||
self.bl_name,
|
||||
|
@ -215,11 +221,17 @@ class BLProp:
|
|||
use_nonpersist=False,
|
||||
use_persist=True,
|
||||
)
|
||||
if self.bl_prop_type is BLPropType.BLPointer:
|
||||
return
|
||||
|
||||
self.write_nonpersist(bl_instance, value)
|
||||
|
||||
def write_nonpersist(
|
||||
self, bl_instance: bl_instance.BLInstance, value: typ.Any
|
||||
) -> None:
|
||||
if self.bl_prop_type is BLPropType.BLPointer:
|
||||
return ## We can't always write here, so we can only do nothing.
|
||||
|
||||
managed_cache.write(
|
||||
bl_instance,
|
||||
self.bl_name,
|
||||
|
@ -229,6 +241,9 @@ class BLProp:
|
|||
)
|
||||
|
||||
def invalidate_nonpersist(self, bl_instance: bl_instance.BLInstance | None) -> None:
|
||||
if self.bl_prop_type is BLPropType.BLPointer:
|
||||
return
|
||||
|
||||
managed_cache.invalidate_nonpersist(
|
||||
bl_instance,
|
||||
self.bl_name,
|
||||
|
|
|
@ -82,6 +82,10 @@ def _is_strenum(T: type) -> bool: # noqa: N803
|
|||
return inspect.isclass(T) and issubclass(T, enum.StrEnum)
|
||||
|
||||
|
||||
def _is_bl_id_struct(T: type) -> bool: # noqa: N803
|
||||
return T in BLIDStructs
|
||||
|
||||
|
||||
####################
|
||||
# - Blender Property Type
|
||||
####################
|
||||
|
@ -323,7 +327,7 @@ class BLPropType(enum.StrEnum):
|
|||
BPT.SingleDynEnum: ['enum_dynamic'],
|
||||
BPT.SetDynEnum: ['enum_dynamic'],
|
||||
# Special
|
||||
BPT.BLPointer: ['blptr_type'],
|
||||
BPT.BLPointer: [],
|
||||
BPT.Serialized: [],
|
||||
}[self]
|
||||
|
||||
|
@ -365,6 +369,12 @@ class BLPropType(enum.StrEnum):
|
|||
|
||||
# Define Information -> KWArg Getter
|
||||
def g_kwarg(name: str, force_key: str | None = None):
|
||||
"""Retrieve a dictionary mapping a name to its `prop_info[name]` value.
|
||||
|
||||
If `name` is not defined in `prop_info`, then return an empty dictionary.
|
||||
|
||||
If `force_key` is set, then use it as to override `name` as the key when returning the dictionary.
|
||||
"""
|
||||
key = force_key if force_key is not None else name
|
||||
return {key: prop_info[name]} if prop_info.get(name) is not None else {}
|
||||
|
||||
|
@ -482,10 +492,13 @@ class BLPropType(enum.StrEnum):
|
|||
|
||||
# BLPointer
|
||||
case BPT.BLPointer:
|
||||
kwargs |= encoded_default
|
||||
|
||||
# BLPointer: ID Type
|
||||
kwargs |= g_kwarg('blptr_type', force_key='type')
|
||||
## -> This is the type of datablock that will be selectable.
|
||||
kwargs |= {'type': obj_type}
|
||||
|
||||
# BLPointer: Poll Method
|
||||
## -> This allows the user filter selectable datablocks.
|
||||
kwargs |= g_kwarg('bltype_poll', force_key='poll')
|
||||
|
||||
# BLPointer
|
||||
case BPT.Serialized:
|
||||
|
@ -573,7 +586,7 @@ class BLPropType(enum.StrEnum):
|
|||
return {str(v) for v in value}
|
||||
|
||||
# BLPointer: Don't Alter
|
||||
case BPT.BLPointer if value in BLIDStructs or value is None:
|
||||
case BPT.BLPointer:
|
||||
return value
|
||||
|
||||
# Serialized: Serialize To UTF-8
|
||||
|
@ -662,7 +675,7 @@ class BLPropType(enum.StrEnum):
|
|||
|
||||
# BLPointer
|
||||
## -> None is always valid when it comes to BLPointers.
|
||||
case BPT.BLPointer if raw_value in BLIDStructs or raw_value is None:
|
||||
case BPT.BLPointer:
|
||||
return raw_value
|
||||
|
||||
# Serialized: Deserialize the Argument
|
||||
|
@ -748,7 +761,7 @@ class BLPropType(enum.StrEnum):
|
|||
return BPT.SetEnum
|
||||
|
||||
# Match BLPointers
|
||||
if obj_type in BLIDStructs:
|
||||
if _is_bl_id_struct(obj_type):
|
||||
return BPT.BLPointer
|
||||
|
||||
# Fallback: Serializable Object
|
||||
|
|
|
@ -1285,6 +1285,7 @@ UnitSystem: typ.TypeAlias = dict[PhysicalType, Unit]
|
|||
|
||||
_PT = PhysicalType
|
||||
UNITS_SI: UnitSystem = {
|
||||
_PT.NonPhysical: None,
|
||||
# Global
|
||||
_PT.Time: spu.second,
|
||||
_PT.Angle: spu.radian,
|
||||
|
@ -1396,7 +1397,7 @@ def strip_unit_system(sp_obj: SympyExpr, unit_system: UnitSystem) -> SympyExpr:
|
|||
Notes:
|
||||
You should probably use `scale_to_unit_system()` or `convert_to_unit_system()`.
|
||||
"""
|
||||
return sp_obj.subs({unit: 1 for unit in unit_system.values()})
|
||||
return sp_obj.subs({unit: 1 for unit in unit_system.values() if unit is not None})
|
||||
|
||||
|
||||
def scale_to_unit_system(
|
||||
|
|
Loading…
Reference in New Issue