diff --git a/src/blender_maxwell/__init__.py b/src/blender_maxwell/__init__.py index e00e96b..a0f6b7e 100644 --- a/src/blender_maxwell/__init__.py +++ b/src/blender_maxwell/__init__.py @@ -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) diff --git a/src/blender_maxwell/assets/geonodes.py b/src/blender_maxwell/assets/geonodes.py index 7a3dc69..420c845 100644 --- a/src/blender_maxwell/assets/geonodes.py +++ b/src/blender_maxwell/assets/geonodes.py @@ -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() diff --git a/src/blender_maxwell/assets/internal/structure/_structure_primitive_cylinder.blend b/src/blender_maxwell/assets/internal/structure/_structure_primitive_cylinder.blend index 19d9c81..d8ac9e6 100644 --- a/src/blender_maxwell/assets/internal/structure/_structure_primitive_cylinder.blend +++ b/src/blender_maxwell/assets/internal/structure/_structure_primitive_cylinder.blend @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:bd146c4c54a323f047d597034d3e3624ef47ce3334b055af4a526264c6880f16 +oid sha256:5dad07a8e5f15aa58639c0442ba808d7be3d4b16db2e0d4b85d10675a698f879 size 857695 diff --git a/src/blender_maxwell/assets/internal/structure/_structure_primitive_ring.blend b/src/blender_maxwell/assets/internal/structure/_structure_primitive_ring.blend index ed57c12..a2c4eec 100644 --- a/src/blender_maxwell/assets/internal/structure/_structure_primitive_ring.blend +++ b/src/blender_maxwell/assets/internal/structure/_structure_primitive_ring.blend @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:87051b6614bdb1adde169781c959b1eb2ffea14fbbc38536c9c4a74545665e39 +oid sha256:3d22d8da9ef5fb2642c5a737a016266ac2a974adf0282ef748d184a8900a7c97 size 883827 diff --git a/src/blender_maxwell/assets/structures/arrays/array_ring.blend b/src/blender_maxwell/assets/structures/arrays/array_ring.blend index 69d4699..a725b26 100644 --- a/src/blender_maxwell/assets/structures/arrays/array_ring.blend +++ b/src/blender_maxwell/assets/structures/arrays/array_ring.blend @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:025e4210b0bf4700d106945b19b8a13a09c56d1872509ca47397ccbaa3a6ae13 -size 911025 +oid sha256:7ed95730d700e3d4c7b0fe824a98702f0f7d05bc578b5697acf53dad182817d0 +size 915737 diff --git a/src/blender_maxwell/contracts/bl.py b/src/blender_maxwell/contracts/bl.py index 80f865b..1408ddb 100644 --- a/src/blender_maxwell/contracts/bl.py +++ b/src/blender_maxwell/contracts/bl.py @@ -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[ diff --git a/src/blender_maxwell/contracts/operator_types.py b/src/blender_maxwell/contracts/operator_types.py index a93dace..de80cb3 100644 --- a/src/blender_maxwell/contracts/operator_types.py +++ b/src/blender_maxwell/contracts/operator_types.py @@ -34,6 +34,9 @@ class OperatorType(enum.StrEnum): GeoNodesToStructureNode = enum.auto() + # Socket: GeoNodesSocket + SocketGeoNodesReset = enum.auto() + # Socket: Tidy3DCloudTask SocketCloudAuthenticate = enum.auto() SocketReloadCloudFolderList = enum.auto() diff --git a/src/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/bl_socket_types.py b/src/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/bl_socket_types.py index 4097443..46e62eb 100644 --- a/src/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/bl_socket_types.py +++ b/src/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/bl_socket_types.py @@ -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 - 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 - ) + # 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 + 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, diff --git a/src/blender_maxwell/node_trees/maxwell_sim_nodes/managed_objs/managed_bl_mesh.py b/src/blender_maxwell/node_trees/maxwell_sim_nodes/managed_objs/managed_bl_mesh.py index 12b8cb3..96caac7 100644 --- a/src/blender_maxwell/node_trees/maxwell_sim_nodes/managed_objs/managed_bl_mesh.py +++ b/src/blender_maxwell/node_trees/maxwell_sim_nodes/managed_objs/managed_bl_mesh.py @@ -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) diff --git a/src/blender_maxwell/node_trees/maxwell_sim_nodes/managed_objs/managed_bl_modifier.py b/src/blender_maxwell/node_trees/maxwell_sim_nodes/managed_objs/managed_bl_modifier.py index 5530c5a..4f16660 100644 --- a/src/blender_maxwell/node_trees/maxwell_sim_nodes/managed_objs/managed_bl_modifier.py +++ b/src/blender_maxwell/node_trees/maxwell_sim_nodes/managed_objs/managed_bl_modifier.py @@ -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'] - ) - 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 + # 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'], + ) + modifier_altered = True + ## TODO: More fine-grained alterations? return modifier_altered # noqa: RET504 diff --git a/src/blender_maxwell/node_trees/maxwell_sim_nodes/node_tree.py b/src/blender_maxwell/node_trees/maxwell_sim_nodes/node_tree.py index 797915c..5e64345 100644 --- a/src/blender_maxwell/node_trees/maxwell_sim_nodes/node_tree.py +++ b/src/blender_maxwell/node_trees/maxwell_sim_nodes/node_tree.py @@ -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') #################### diff --git a/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/base.py b/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/base.py index 071422a..4fca6f1 100644 --- a/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/base.py +++ b/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/base.py @@ -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) diff --git a/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/structures/geonodes_structure.py b/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/structures/geonodes_structure.py index e3a0ad2..367156e 100644 --- a/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/structures/geonodes_structure.py +++ b/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/structures/geonodes_structure.py @@ -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'], + ) #################### diff --git a/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/structures/primitives/box_structure.py b/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/structures/primitives/box_structure.py index b728465..6911682 100644 --- a/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/structures/primitives/box_structure.py +++ b/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/structures/primitives/box_structure.py @@ -83,7 +83,7 @@ class BoxStructureNode(base.MaxwellSimNode): ) #################### - # - Preview + # - Events: Preview #################### @events.on_value_changed( # Trigger diff --git a/src/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/blender/geonodes.py b/src/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/blender/geonodes.py index 3e615b4..c9edcfd 100644 --- a/src/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/blender/geonodes.py +++ b/src/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/blender/geonodes.py @@ -14,30 +14,40 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . +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 diff --git a/src/blender_maxwell/utils/bl_cache/bl_field.py b/src/blender_maxwell/utils/bl_cache/bl_field.py index 1735c89..284a291 100644 --- a/src/blender_maxwell/utils/bl_cache/bl_field.py +++ b/src/blender_maxwell/utils/bl_cache/bl_field.py @@ -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__ diff --git a/src/blender_maxwell/utils/bl_cache/bl_prop.py b/src/blender_maxwell/utils/bl_cache/bl_prop.py index 00c73c8..bc233b1 100644 --- a/src/blender_maxwell/utils/bl_cache/bl_prop.py +++ b/src/blender_maxwell/utils/bl_cache/bl_prop.py @@ -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, diff --git a/src/blender_maxwell/utils/bl_cache/bl_prop_type.py b/src/blender_maxwell/utils/bl_cache/bl_prop_type.py index 9f85a68..ca2961e 100644 --- a/src/blender_maxwell/utils/bl_cache/bl_prop_type.py +++ b/src/blender_maxwell/utils/bl_cache/bl_prop_type.py @@ -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 diff --git a/src/blender_maxwell/utils/extra_sympy_units.py b/src/blender_maxwell/utils/extra_sympy_units.py index 4d6eb38..871455a 100644 --- a/src/blender_maxwell/utils/extra_sympy_units.py +++ b/src/blender_maxwell/utils/extra_sympy_units.py @@ -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(