diff --git a/src/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/flow_events.py b/src/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/flow_events.py index 0d2f3eb..984451b 100644 --- a/src/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/flow_events.py +++ b/src/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/flow_events.py @@ -24,7 +24,7 @@ class FlowEvent(enum.StrEnum): ShowPlot: Indicates that the node/socket should enable its plotted preview. This should generally be used if the node is rendering to an image, for viewing through the Blender image editor. LinkChanged: Indicates that a link to a node/socket was added/removed. - In nodes, this is accompanied by a `socket_name` to indicate which socket it is that had its links altered. + Is translated to `DataChanged` on sockets before propagation. DataChanged: Indicates that data flowing through a node/socket was altered. In nodes, this event is accompanied by a `socket_name` or `prop_name`, to indicate which socket/property it is that was changed. **This event is essential**, as it invalidates all input/output socket caches along its path. diff --git a/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/analysis/extract_data.py b/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/analysis/extract_data.py index 7ef8579..b4daaf8 100644 --- a/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/analysis/extract_data.py +++ b/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/analysis/extract_data.py @@ -195,7 +195,7 @@ class ExtractDataNode(base.MaxwellSimNode): @events.computes_output_socket( 'Data', - kind=ct.FlowKind.Value, + kind=ct.FlowKind.Array, props={'extract_filter'}, input_sockets={'Monitor Data'}, ) @@ -210,7 +210,7 @@ class ExtractDataNode(base.MaxwellSimNode): 'Data', kind=ct.FlowKind.LazyValueFunc, output_sockets={'Data'}, - output_socket_kinds={'Data': ct.FlowKind.Value}, + output_socket_kinds={'Data': ct.FlowKind.Array}, ) def compute_extracted_data_lazy(self, output_sockets: dict) -> ct.LazyValueFuncFlow: return ct.LazyValueFuncFlow( diff --git a/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/analysis/math/filter_math.py b/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/analysis/math/filter_math.py index e73e588..2147fd6 100644 --- a/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/analysis/math/filter_math.py +++ b/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/analysis/math/filter_math.py @@ -1,3 +1,4 @@ +import enum import typing as typ import bpy @@ -14,6 +15,13 @@ log = logger.get(__name__) class FilterMathNode(base.MaxwellSimNode): + """Reduces the dimensionality of data. + + Attributes: + operation: Operation to apply to the input. + dim: Dims to use when filtering data + """ + node_type = ct.NodeType.FilterMath bl_label = 'Filter Math' @@ -31,32 +39,17 @@ class FilterMathNode(base.MaxwellSimNode): #################### # - Properties #################### - operation: bpy.props.EnumProperty( - name='Op', - description='Operation to filter with', - items=lambda self, _: self.search_operations(), - update=lambda self, context: self.on_prop_changed('operation', context), + operation: enum.Enum = bl_cache.BLField( + prop_ui=True, enum_cb=lambda self, _: self.search_operations() ) - dim: bpy.props.StringProperty( - name='Dim', - description='Dims to use when filtering data', - default='', - search=lambda self, _, edit_text: self.search_dims(edit_text), - update=lambda self, context: self.on_prop_changed('dim', context), + dim: str = bl_cache.BLField( + '', prop_ui=True, str_cb=lambda self, _, edit_text: self.search_dims(edit_text) ) dim_names: list[str] = bl_cache.BLField([]) dim_lens: dict[str, int] = bl_cache.BLField({}) - @property - def has_dim(self) -> bool: - return ( - self.active_socket_set in ['By Dim', 'By Dim Value'] - and self.inputs['Data'].is_linked - and self.dim_names - ) - #################### # - Operation Search #################### @@ -77,7 +70,7 @@ class FilterMathNode(base.MaxwellSimNode): # - Dim Search #################### def search_dims(self, edit_text: str) -> list[tuple[str, str, str]]: - if self.has_dim: + if self.dim_names: dims = [ (dim_name, dim_name) for dim_name in self.dim_names @@ -94,17 +87,23 @@ class FilterMathNode(base.MaxwellSimNode): # - UI #################### def draw_props(self, _: bpy.types.Context, layout: bpy.types.UILayout) -> None: - layout.prop(self, 'operation', text='') - if self.has_dim: - layout.prop(self, 'dim', text='') + layout.prop(self, self.blfields['operation'], text='') + if self.dim_names: + layout.prop(self, self.blfields['dim'], text='') #################### # - Events #################### + @events.on_value_changed( + prop_name='active_socket_set', + ) + def on_socket_set_changed(self): + self.operation = bl_cache.Signal.ResetEnumItems + @events.on_value_changed( socket_name={'Data'}, - prop_name={'active_socket_set', 'dim'}, - props={'active_socket_set', 'dim'}, + prop_name={'active_socket_set'}, + props={'active_socket_set'}, input_sockets={'Data'}, input_socket_kinds={'Data': ct.FlowKind.Info}, input_sockets_optional={'Data': True}, @@ -121,21 +120,32 @@ class FilterMathNode(base.MaxwellSimNode): self.dim_names = [] self.dim_lens = {} - # Add Input Value w/Unit from InfoFlow - ## Socket Type is determined from the Unit + # Reset String Searcher + self.dim = bl_cache.Signal.ResetStrSearch + + @events.on_value_changed( + prop_name='dim', + props={'active_socket_set', 'dim'}, + input_sockets={'Data'}, + input_socket_kinds={'Data': ct.FlowKind.Info}, + input_sockets_optional={'Data': True}, + ) + def on_dim_change(self, props: dict, input_sockets: dict): + # Add/Remove Input Socket "Value" if ( props['active_socket_set'] == 'By Dim Value' - and props['dim'] != '' and props['dim'] in input_sockets['Data'].dim_names ): - socket_def = sockets.SOCKET_DEFS[ + # Get Current and Wanted Socket Defs + current_socket_def = self.loose_input_sockets.get('Value') + wanted_socket_def = sockets.SOCKET_DEFS[ ct.unit_to_socket_type(input_sockets['Data'].dim_idx[props['dim']].unit) ] - if ( - _val_socket_def := self.loose_input_sockets.get('Value') - ) is None or _val_socket_def != socket_def: + + # Determine Whether to Declare New Loose Input SOcket + if current_socket_def is None or current_socket_def != wanted_socket_def: self.loose_input_sockets = { - 'Value': socket_def(), + 'Value': wanted_socket_def(), } elif self.loose_input_sockets: self.loose_input_sockets = {} @@ -151,18 +161,18 @@ class FilterMathNode(base.MaxwellSimNode): input_socket_kinds={'Data': {ct.FlowKind.LazyValueFunc, ct.FlowKind.Info}}, ) def compute_data(self, props: dict, input_sockets: dict): + # Retrieve Inputs lazy_value_func = input_sockets['Data'][ct.FlowKind.LazyValueFunc] info = input_sockets['Data'][ct.FlowKind.Info] - # Determine Bound/Free Parameters - if props['dim'] in info.dim_names: + # Compute Bound/Free Parameters + func_args = [int] if props['active_socket_set'] == 'By Dim Value' else [] + if props['dim']: axis = info.dim_names.index(props['dim']) else: - msg = 'Dimension invalid' + msg = 'Dimension cannot be empty' raise ValueError(msg) - func_args = [int] if props['active_socket_set'] == 'By Dim Value' else [] - # Select Function filter_func: typ.Callable[[jax.Array], jax.Array] = { 'By Dim': {'SQUEEZE': lambda data: jnp.squeeze(data, axis)}, @@ -189,8 +199,11 @@ class FilterMathNode(base.MaxwellSimNode): }, ) def compute_array(self, output_sockets: dict) -> ct.ArrayFlow: + # Retrieve Inputs lazy_value_func = output_sockets['Data'][ct.FlowKind.LazyValueFunc] params = output_sockets['Data'][ct.FlowKind.Params] + + # Compute Array return ct.ArrayFlow( values=lazy_value_func.func_jax(*params.func_args, **params.func_kwargs), unit=None, ## TODO: Unit Propagation @@ -207,14 +220,18 @@ class FilterMathNode(base.MaxwellSimNode): input_socket_kinds={'Data': ct.FlowKind.Info}, ) def compute_data_info(self, props: dict, input_sockets: dict) -> ct.InfoFlow: + # Retrieve Inputs info = input_sockets['Data'] - if props['dim'] in info.dim_names: + # Compute Bound/Free Parameters + ## Empty Dimension -> Empty InfoFlow + if props['dim']: axis = info.dim_names.index(props['dim']) else: return ct.InfoFlow() - # Compute Axis + # Compute Information + ## Compute Info w/By-Operation Change to Dimensions if (props['active_socket_set'], props['operation']) in [ ('By Dim', 'SQUEEZE'), ('By Dim Value', 'FIX'), @@ -228,6 +245,7 @@ class FilterMathNode(base.MaxwellSimNode): }, ) + # Fallback to Empty InfoFlow return ct.InfoFlow() @events.computes_output_socket( @@ -238,20 +256,30 @@ class FilterMathNode(base.MaxwellSimNode): input_socket_kinds={'Data': {ct.FlowKind.Info, ct.FlowKind.Params}}, input_sockets_optional={'Value': True}, ) - def compute_data_params(self, props: dict, input_sockets: dict) -> ct.ParamsFlow: + def compute_composed_params( + self, props: dict, input_sockets: dict + ) -> ct.ParamsFlow: + # Retrieve Inputs info = input_sockets['Data'][ct.FlowKind.Info] params = input_sockets['Data'][ct.FlowKind.Params] + # Compute Composed Parameters + ## -> Only operations that add parameters. + ## -> A dimension must be selected. + ## -> There must be an input value. if ( (props['active_socket_set'], props['operation']) in [ ('By Dim Value', 'FIX'), ] - and props['dim'] in info.dim_names + and props['dim'] and input_sockets['Value'] is not None ): - # Compute IDX Corresponding to Value - ## Aka. "indexing by a float" + # Compute IDX Corresponding to Coordinate Value + ## -> Each dimension declares a unit-aware real number at each index. + ## -> "Value" is a unit-aware real number from loose input socket. + ## -> This finds the dimensional index closest to "Value". + ## Total Effect: Indexing by a unit-aware real number. nearest_idx_to_value = info.dim_idx[props['dim']].nearest_idx_of( input_sockets['Value'], require_sorted=True ) @@ -269,6 +297,3 @@ BL_REGISTER = [ FilterMathNode, ] BL_NODES = {ct.NodeType.FilterMath: (ct.NodeCategory.MAXWELLSIM_ANALYSIS_MATH)} - - -## TODO TODO Okay so just like, Value needs to be a Loose socket, events needs to be able to handle sets of kinds, the invalidator needs to be able to handle sets of kinds too. Given all that, we only need to propagate the output array unit; given all all that, we are 100% goddamn ready to fix that goddamn coordinate. 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 d52113a..c5f2ac7 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 @@ -753,8 +753,8 @@ class MaxwellSimNode(bpy.types.Node): Notes: This can be an unpredictably heavy function, depending on the node graph topology. - Doesn't currently accept `LinkChanged` (->Output) events; rather, these propagate as `DataChanged` events. - **This may change** if it becomes important for the node to differentiate between "change in data" and "change in link". + Doesn't accept `LinkChanged` events; they are translated to `DataChanged` on the socket. + This is on purpose: It seems to be a bad idea to try and differentiate between "changes in data" and "changes in linkage". Parameters: event: The event to report forwards/backwards along the node tree. @@ -762,10 +762,12 @@ class MaxwellSimNode(bpy.types.Node): pop_name: The property that was altered, if any, in order to trigger this event. """ # Outflow Socket Kinds - ## Something has happened, that much is for sure. - ## Output methods might require invalidation of (outsck, FlowKind)s. - ## Whichever FlowKinds we do happen to invalidate, we should mark. - ## This way, each FlowKind gets its own invalidation chain. + ## -> Something has happened! + ## -> The effect is yet to be determined... + ## -> We will watch for which kinds actually invalidate. + ## -> ...Then ONLY propagate kinds that have an invalidated outsck. + ## -> This way, kinds get "their own" invalidation chains. + ## -> ...While still respecting "crossovers". altered_socket_kinds = set() # Invalidate Caches on DataChanged @@ -869,7 +871,6 @@ class MaxwellSimNode(bpy.types.Node): """ if hasattr(self, prop_name): # Invalidate UI BLField Caches - log.critical((prop_name, self.ui_blfields)) if prop_name in self.ui_blfields: setattr(self, prop_name, bl_cache.Signal.InvalidateCache) diff --git a/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/outputs/viewer.py b/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/outputs/viewer.py index 4665dad..e44f676 100644 --- a/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/outputs/viewer.py +++ b/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/outputs/viewer.py @@ -137,7 +137,7 @@ class ViewerNode(base.MaxwellSimNode): props={'auto_plot'}, ) def on_changed_plot_preview(self, props): - if self.inputs['Any'].is_linked and props['auto_plot']: + if props['auto_plot']: self.trigger_event(ct.FlowEvent.ShowPlot) @events.on_value_changed( @@ -150,7 +150,7 @@ class ViewerNode(base.MaxwellSimNode): # Remove Non-Repreviewed Previews on Close with node_tree.repreview_all(): - if self.inputs['Any'].is_linked and props['auto_3d_preview']: + if props['auto_3d_preview']: self.trigger_event(ct.FlowEvent.ShowPreview) diff --git a/src/blender_maxwell/utils/bl_cache.py b/src/blender_maxwell/utils/bl_cache.py index 42673b0..ed5ffaa 100644 --- a/src/blender_maxwell/utils/bl_cache.py +++ b/src/blender_maxwell/utils/bl_cache.py @@ -712,9 +712,11 @@ class BLField: kwargs_prop['options'].add('SKIP_SAVE') if self._str_cb is not None: - kwargs_prop |= lambda _self, context, edit_text: self._safe_str_cb( - _self, context, edit_text - ) + kwargs_prop |= { + 'search': lambda _self, context, edit_text: self._safe_str_cb( + _self, context, edit_text + ) + } ## Path elif AttrType is Path: @@ -845,6 +847,16 @@ class BLField: self._cached_bl_property.__set__(bl_instance, Signal.InvalidateCache) elif value == Signal.ResetStrSearch: + # Set String to '' + ## Prevents the presence of an invalid value not in the new search. + ## -> Infinite recursion if we don't check current value for ''. + ## -> May cause a hiccup (chains will trigger twice) + current_value = self._cached_bl_property.__get__( + bl_instance, bl_instance.__class__ + ) + if current_value != '': + self._cached_bl_property.__set__(bl_instance, '') + # Pop the Cached String Search Items ## The next time Blender does a str search, it'll update. self._str_cb_cache.pop(bl_instance.instance_id, None)