From 619704c46ea1740d2ba3b75902b83298825f719f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sofus=20Albert=20H=C3=B8gsbro=20Rose?= Date: Mon, 8 Apr 2024 08:38:19 +0200 Subject: [PATCH] feat: Better link/append strategy for GN lookup --- src/blender_maxwell/assets/import_geonodes.py | 138 +++++- .../maxwell_sim_nodes/bl_socket_map.py | 5 +- .../managed_objs/managed_bl_modifier.py | 60 ++- .../managed_objs/managed_bl_object.py | 400 ------------------ .../maxwell_sim_nodes/nodes/outputs/viewer.py | 47 +- .../nodes/structures/geonodes_structure.py | 11 +- 6 files changed, 205 insertions(+), 456 deletions(-) delete mode 100644 src/blender_maxwell/node_trees/maxwell_sim_nodes/managed_objs/managed_bl_object.py diff --git a/src/blender_maxwell/assets/import_geonodes.py b/src/blender_maxwell/assets/import_geonodes.py index 586b1b5..d76474e 100644 --- a/src/blender_maxwell/assets/import_geonodes.py +++ b/src/blender_maxwell/assets/import_geonodes.py @@ -12,6 +12,7 @@ from ..utils import logger log = logger.get(__name__) +ImportMethod: typ.TypeAlias = typx.Literal['append', 'link'] BLOperatorStatus: typ.TypeAlias = set[ typx.Literal['RUNNING_MODAL', 'CANCELLED', 'FINISHED', 'PASS_THROUGH', 'INTERFACE'] ] @@ -25,40 +26,143 @@ class GeoNodes(enum.StrEnum): The value of this StrEnum is both the name of the .blend file containing the GeoNodes group, and of the GeoNodes group itself. """ + # Node Previews + ## Input + InputConstantPhysicalPol = '_input_constant_physical_pol' + ## Source + SourcePointDipole = '_source_point_dipole' + SourcePlaneWave = '_source_plane_wave' + SourceUniformCurrent = '_source_uniform_current' + SourceTFSF = '_source_tfsf' + SourceGaussianBeam = '_source_gaussian_beam' + SourceAstigmaticGaussianBeam = '_source_astigmatic_gaussian_beam' + SourceMode = '_source_mode' + SourceEHArray = '_source_eh_array' + SourceEHEquivArray = '_source_eh_equiv_array' + ## Structure + StructurePrimitivePlane = '_structure_primitive_plane' + StructurePrimitiveBox = '_structure_primitive_box' + StructurePrimitiveSphere = '_structure_primitive_sphere' + StructurePrimitiveCylinder = '_structure_primitive_cylinder' + StructurePrimitiveRing = '_structure_primitive_ring' + StructurePrimitiveCapsule = '_structure_primitive_capsule' + StructurePrimitiveCone = '_structure_primitive_cone' + ## Monitor + MonitorEHField = '_monitor_eh_field' + MonitorFieldPowerFlux = '_monitor_field_power_flux' + MonitorEpsTensor = '_monitor_eps_tensor' + MonitorDiffraction = '_monitor_diffraction' + MonitorProjCartEHField = '_monitor_proj_eh_field' + MonitorProjAngEHField = '_monitor_proj_ang_eh_field' + MonitorProjKSpaceEHField = '_monitor_proj_k_space_eh_field' + ## Simulation + SimulationSimDomain = '_simulation_sim_domain' + SimulationBoundConds = '_simulation_bound_conds' + SimulationBoundCondPML = '_simulation_bound_cond_pml' + SimulationBoundCondPEC = '_simulation_bound_cond_pec' + SimulationBoundCondPMC = '_simulation_bound_cond_pmc' + SimulationBoundCondBloch = '_simulation_bound_cond_bloch' + SimulationBoundCondPeriodic = '_simulation_bound_cond_periodic' + SimulationBoundCondAbsorbing = '_simulation_bound_cond_absorbing' + SimulationSimGrid = '_simulation_sim_grid' + SimulationSimGridAxisAuto = '_simulation_sim_grid_axis_auto' + SimulationSimGridAxisManual = '_simulation_sim_grid_axis_manual' + SimulationSimGridAxisUniform = '_simulation_sim_grid_axis_uniform' + SimulationSimGridAxisArray = '_simulation_sim_grid_axis_array' + # Structures + ## Primitives PrimitiveBox = 'box' PrimitiveRing = 'ring' PrimitiveSphere = 'sphere' -# GeoNodes Path Mapping -GN_PRIMITIVES_PATH = info.PATH_ASSETS / 'geonodes' / 'primitives' +# GeoNodes Paths +## Internal +GN_INTERNAL_PATH = info.PATH_ASSETS / 'internal' / 'primitives' +GN_INTERNAL_INPUTS_PATH = GN_INTERNAL_PATH / 'input' +GN_INTERNAL_SOURCES_PATH = GN_INTERNAL_PATH / 'source' +GN_INTERNAL_STRUCTURES_PATH = GN_INTERNAL_PATH / 'structure' +GN_INTERNAL_MONITORS_PATH = GN_INTERNAL_PATH / 'monitor' +GN_INTERNAL_SIMULATIONS_PATH = GN_INTERNAL_PATH / 'simulation' + +## Structures +GN_STRUCTURES_PATH = info.PATH_ASSETS / 'structures' +GN_STRUCTURES_PRIMITIVES_PATH = GN_STRUCTURES_PATH / 'primitives' + GN_PARENT_PATHS: dict[GeoNodes, Path] = { - GeoNodes.PrimitiveBox: GN_PRIMITIVES_PATH, - GeoNodes.PrimitiveRing: GN_PRIMITIVES_PATH, - GeoNodes.PrimitiveSphere: GN_PRIMITIVES_PATH, + # Node Previews + ## Input + GeoNodes.InputConstantPhysicalPol: GN_INTERNAL_INPUTS_PATH, + ## Source + GeoNodes.SourcePointDipole: GN_INTERNAL_SOURCES_PATH, + GeoNodes.SourcePlaneWave: GN_INTERNAL_SOURCES_PATH, + GeoNodes.SourceUniformCurrent: GN_INTERNAL_SOURCES_PATH, + GeoNodes.SourceTFSF: GN_INTERNAL_SOURCES_PATH, + GeoNodes.SourceGaussianBeam: GN_INTERNAL_SOURCES_PATH, + GeoNodes.SourceAstigmaticGaussianBeam: GN_INTERNAL_SOURCES_PATH, + GeoNodes.SourceMode: GN_INTERNAL_SOURCES_PATH, + GeoNodes.SourceEHArray: GN_INTERNAL_SOURCES_PATH, + GeoNodes.SourceEHEquivArray: GN_INTERNAL_SOURCES_PATH, + ## Structure + GeoNodes.StructurePrimitivePlane: GN_INTERNAL_STRUCTURES_PATH, + GeoNodes.StructurePrimitiveBox: GN_INTERNAL_STRUCTURES_PATH, + GeoNodes.StructurePrimitiveSphere: GN_INTERNAL_STRUCTURES_PATH, + GeoNodes.StructurePrimitiveCylinder: GN_INTERNAL_STRUCTURES_PATH, + GeoNodes.StructurePrimitiveRing: GN_INTERNAL_STRUCTURES_PATH, + GeoNodes.StructurePrimitiveCapsule: GN_INTERNAL_STRUCTURES_PATH, + GeoNodes.StructurePrimitiveCone: GN_INTERNAL_STRUCTURES_PATH, + ## Monitor + GeoNodes.MonitorEHField: GN_INTERNAL_STRUCTURES_PATH, + GeoNodes.MonitorFieldPowerFlux: GN_INTERNAL_STRUCTURES_PATH, + GeoNodes.MonitorEpsTensor: GN_INTERNAL_STRUCTURES_PATH, + GeoNodes.MonitorDiffraction: GN_INTERNAL_STRUCTURES_PATH, + GeoNodes.MonitorProjCartEHField: GN_INTERNAL_STRUCTURES_PATH, + GeoNodes.MonitorProjAngEHField: GN_INTERNAL_STRUCTURES_PATH, + GeoNodes.MonitorProjKSpaceEHField: GN_INTERNAL_STRUCTURES_PATH, + ## Simulation + GeoNodes.SimulationSimDomain: GN_INTERNAL_SIMULATIONS_PATH, + GeoNodes.SimulationBoundConds: GN_INTERNAL_SIMULATIONS_PATH, + GeoNodes.SimulationBoundCondPML: GN_INTERNAL_SIMULATIONS_PATH, + GeoNodes.SimulationBoundCondPEC: GN_INTERNAL_SIMULATIONS_PATH, + GeoNodes.SimulationBoundCondPMC: GN_INTERNAL_SIMULATIONS_PATH, + GeoNodes.SimulationBoundCondBloch: GN_INTERNAL_SIMULATIONS_PATH, + GeoNodes.SimulationBoundCondPeriodic: GN_INTERNAL_SIMULATIONS_PATH, + GeoNodes.SimulationBoundCondAbsorbing: GN_INTERNAL_SIMULATIONS_PATH, + GeoNodes.SimulationSimGrid: GN_INTERNAL_SIMULATIONS_PATH, + GeoNodes.SimulationSimGridAxisAuto: GN_INTERNAL_SIMULATIONS_PATH, + GeoNodes.SimulationSimGridAxisManual: GN_INTERNAL_SIMULATIONS_PATH, + GeoNodes.SimulationSimGridAxisUniform: GN_INTERNAL_SIMULATIONS_PATH, + GeoNodes.SimulationSimGridAxisArray: GN_INTERNAL_SIMULATIONS_PATH, + + # Structures + GeoNodes.PrimitiveBox: GN_STRUCTURES_PRIMITIVES_PATH, + GeoNodes.PrimitiveRing: GN_STRUCTURES_PRIMITIVES_PATH, + GeoNodes.PrimitiveSphere: GN_STRUCTURES_PRIMITIVES_PATH, } #################### # - Import GeoNodes (Link/Append) #################### -ImportMethod: typ.TypeAlias = typx.Literal['append', 'link'] - - def import_geonodes( geonodes: GeoNodes, import_method: ImportMethod, - force_import: bool = False, ) -> bpy.types.GeometryNodeGroup: - """Given a pre-defined GeoNodes group packaged with Blender Maxwell. + """Given a (name of a) GeoNodes group packaged with Blender Maxwell, link/append it to the current file, and return the node group. - The procedure is as follows: + Parameters: + geonodes: The (name of the) GeoNodes group, which ships with Blender Maxwell. + import_method: Whether to link or append the GeoNodes group. + When 'link', repeated calls will not link a new group; the existing group will simply be returned. - - Link it to the current .blend file. - - Retrieve the node group and return it. + Returns: + A GeoNodes group available in the current .blend file, which can ex. be attached to a 'GeoNodes Structure' node. """ - if geonodes in bpy.data.node_groups and not force_import: + if ( + import_method == 'link' + and geonodes in bpy.data.node_groups + ): return bpy.data.node_groups[geonodes] filename = geonodes @@ -144,8 +248,6 @@ class AppendGeoNodes(bpy.types.Operator): # - Properties #################### _asset: bpy.types.AssetRepresentation | None = None - _start_drag_x: bpy.props.IntProperty() - _start_drag_y: bpy.props.IntProperty() #################### # - UI @@ -168,9 +270,7 @@ class AppendGeoNodes(bpy.types.Operator): """ return context.asset is not None - def invoke(self, context, event): - self._start_drag_x = event.mouse_x - self._start_drag_y = event.mouse_y + def invoke(self, context: bpy.types.Context, _): return self.execute(context) def execute(self, context: bpy.types.Context) -> BLOperatorStatus: diff --git a/src/blender_maxwell/node_trees/maxwell_sim_nodes/bl_socket_map.py b/src/blender_maxwell/node_trees/maxwell_sim_nodes/bl_socket_map.py index ccd57c0..26a8202 100644 --- a/src/blender_maxwell/node_trees/maxwell_sim_nodes/bl_socket_map.py +++ b/src/blender_maxwell/node_trees/maxwell_sim_nodes/bl_socket_map.py @@ -43,10 +43,10 @@ def _size_from_bl_socket( description: str, bl_socket_type: BLSocketType, ): - """Parses the `size`, aka. number of elements, contained within the `default_value` of a Blender interface socket. + """Parses the number of elements contained in a Blender interface socket. Since there are no 2D sockets in Blender, the user can specify "2D" in the Blender socket's description to "promise" that only the first two values will be used. - When this is done, the third value is left entirely untouched by this entire system. + When this is done, the third value is just never altered by the addon. A hard-coded set of NodeSocket prefixes are used to determine which interface sockets are, in fact, 3D. - For 3D sockets, a hard-coded list of Blender node socket types is used. @@ -204,6 +204,7 @@ def _writable_bl_socket_value( unit_system: dict | None = None, allow_unit_not_in_unit_system: bool = False, ) -> typ.Any: + log.debug('Writing BL Socket Value (%s)', str(value)) socket_type = _socket_type_from_bl_socket(description, bl_socket_type) # Retrieve Unit-System Unit 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 e3f83d4..9346b54 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 @@ -1,3 +1,5 @@ +"""A managed Blender modifier, associated with some Blender object.""" + import typing as typ import bpy @@ -10,22 +12,35 @@ from .. import contracts as ct log = logger.get(__name__) ModifierType: typ.TypeAlias = typx.Literal['NODES', 'ARRAY'] - - NodeTreeInterfaceID: typ.TypeAlias = str +UnitSystem: typ.TypeAlias = typ.Any +#################### +# - Modifier Attributes +#################### class ModifierAttrsNODES(typ.TypedDict): + """Describes values set on an GeoNodes modifier. + + Attributes: + node_group: The GeoNodes group to use in the modifier. + unit_system: The unit system used by the GeoNodes output. + Generally, `ct.UNITS_BLENDER` is a good choice. + inputs: Values to associate with each GeoNodes interface socket. + Use `analyze_geonodes.interface(..., direc='INPUT')` to determine acceptable values. + """ + node_group: bpy.types.GeometryNodeTree - unit_system: bpy.types.GeometryNodeTree + unit_system: UnitSystem inputs: dict[NodeTreeInterfaceID, typ.Any] class ModifierAttrsARRAY(typ.TypedDict): - pass + """Describes values set on an Array modifier.""" ModifierAttrs: typ.TypeAlias = ModifierAttrsNODES | ModifierAttrsARRAY + MODIFIER_NAMES = { 'NODES': 'BLMaxwell_GeoNodes', 'ARRAY': 'BLMaxwell_Array', @@ -37,6 +52,7 @@ MODIFIER_NAMES = { #################### def read_modifier(bl_modifier: bpy.types.Modifier) -> ModifierAttrs: if bl_modifier.type == 'NODES': + ## TODO: Also get GeoNodes modifier values, if the nodegroup is not-None. return { 'node_group': bl_modifier.node_group, } @@ -50,9 +66,18 @@ def read_modifier(bl_modifier: bpy.types.Modifier) -> ModifierAttrs: # - Write Modifier Information #################### def write_modifier_geonodes( - bl_modifier: bpy.types.Modifier, + bl_modifier: bpy.types.NodesModifier, modifier_attrs: ModifierAttrsNODES, ) -> bool: + """Writes attributes to the GeoNodes modifier, changing only what's needed. + + Parameters: + bl_modifier: The GeoNodes modifier to write to. + modifier_attrs: The attributes to write to + + Returns: + True if the modifier was altered. + """ modifier_altered = False # Alter GeoNodes Group if bl_modifier.node_group != modifier_attrs['node_group']: @@ -163,7 +188,28 @@ class ManagedBLModifier(ct.schemas.ManagedObj): # - Deallocation #################### def free(self): - pass + """Not needed - when the object is removed, its modifiers are also removed.""" + + def free_from_bl_object( + self, + bl_object: bpy.types.Object, + ) -> None: + """Remove the managed BL modifier from the passed Blender object. + + Parameters: + bl_object: The Blender object to remove the modifier from. + """ + if (bl_modifier := bl_object.modifiers.get(self.name)) is not None: + log.info( + 'Removing (recreating) BLModifier "%s" on BLObject "%s" (existing modifier_type is "%s")', + bl_modifier.name, + bl_object.name, + bl_modifier.type, + ) + bl_modifier = bl_object.modifiers.remove(bl_modifier) + else: + msg = f'Tried to free bl_modifier "{self.name}", but bl_object "{bl_object.name}" has no modifier of that name' + raise ValueError(msg) #################### # - Modifiers @@ -190,7 +236,7 @@ class ManagedBLModifier(ct.schemas.ManagedObj): bl_modifier.type, modifier_type, ) - self.free() + self.free_from_bl_object(bl_object) modifier_was_removed = True # Create Modifier diff --git a/src/blender_maxwell/node_trees/maxwell_sim_nodes/managed_objs/managed_bl_object.py b/src/blender_maxwell/node_trees/maxwell_sim_nodes/managed_objs/managed_bl_object.py deleted file mode 100644 index d7dd684..0000000 --- a/src/blender_maxwell/node_trees/maxwell_sim_nodes/managed_objs/managed_bl_object.py +++ /dev/null @@ -1,400 +0,0 @@ -import contextlib - -import bmesh -import bpy -import numpy as np -import typing_extensions as typx - -from ....utils import logger -from .. import contracts as ct -from .managed_bl_collection import managed_collection, preview_collection - -log = logger.get(__name__) - -ModifierType = typx.Literal['NODES', 'ARRAY'] -MODIFIER_NAMES = { - 'NODES': 'BLMaxwell_GeoNodes', - 'ARRAY': 'BLMaxwell_Array', -} - - -#################### -# - BLObject -#################### -class ManagedBLObject(ct.schemas.ManagedObj): - managed_obj_type = ct.ManagedObjType.ManagedBLObject - _bl_object_name: str | None = None - - #################### - # - BL Object Name - #################### - @property - def name(self): - return self._bl_object_name - - @name.setter - def name(self, value: str) -> None: - log.info( - 'Changing BLObject w/Name "%s" to Name "%s"', self._bl_object_name, value - ) - - if not bpy.data.objects.get(value): - log.info( - 'Desired BLObject Name "%s" Not Taken', - value, - ) - - if self._bl_object_name is None: - log.info( - 'Set New BLObject Name to "%s"', - value, - ) - elif bl_object := bpy.data.objects.get(self._bl_object_name): - log.info( - 'Changed BLObject Name to "%s"', - value, - ) - bl_object.name = value - else: - msg = f'ManagedBLObject with name "{self._bl_object_name}" was deleted' - raise RuntimeError(msg) - - # Set Internal Name - self._bl_object_name = value - else: - log.info( - 'Desired BLObject Name "%s" is Taken. Using Blender Rename', - value, - ) - - # Set Name Anyway, but Respect Blender's Renaming - ## When a name already exists, Blender adds .### to prevent overlap. - ## `set_name` is allowed to change the name; nodes account for this. - bl_object.name = value - self._bl_object_name = bl_object.name - - log.info( - 'Changed BLObject Name to "%s"', - bl_object.name, - ) - - #################### - # - Allocation - #################### - def __init__(self, name: str): - self.name = name - - #################### - # - Deallocation - #################### - def free(self): - if (bl_object := bpy.data.objects.get(self.name)) is None: - return - - # Delete the Underlying Datablock - ## This automatically deletes the object too - log.info('Removing "%s" BLObject', bl_object.type) - if bl_object.type in {'MESH', 'EMPTY'}: - bpy.data.meshes.remove(bl_object.data) - elif bl_object.type == 'VOLUME': - bpy.data.volumes.remove(bl_object.data) - else: - msg = f'BLObject "{bl_object.name}" has invalid kind "{bl_object.type}"' - raise RuntimeError(msg) - - #################### - # - Actions - #################### - def show_preview( - self, - kind: typx.Literal['MESH', 'EMPTY', 'VOLUME'], - empty_display_type: typx.Literal[ - 'PLAIN_AXES', - 'ARROWS', - 'SINGLE_ARROW', - 'CIRCLE', - 'CUBE', - 'SPHERE', - 'CONE', - 'IMAGE', - ] - | None = None, - ) -> None: - """Moves the managed Blender object to the preview collection. - - If it's already included, do nothing. - """ - bl_object = self.bl_object(kind) - if bl_object.name not in preview_collection().objects: - log.info('Moving "%s" to Preview Collection', bl_object.name) - preview_collection().objects.link(bl_object) - - # Display Parameters - if kind == 'EMPTY' and empty_display_type is not None: - log.info( - 'Setting Empty Display Type "%s" for "%s"', - empty_display_type, - bl_object.name, - ) - bl_object.empty_display_type = empty_display_type - - def hide_preview( - self, - kind: typx.Literal['MESH', 'EMPTY', 'VOLUME'], - ) -> None: - """Removes the managed Blender object from the preview collection. - - If it's already removed, do nothing. - """ - bl_object = self.bl_object(kind) - if bl_object.name not in preview_collection().objects: - log.info('Removing "%s" from Preview Collection', bl_object.name) - preview_collection.objects.unlink(bl_object) - - def bl_select(self) -> None: - """Selects the managed Blender object globally, causing it to be ex. - outlined in the 3D viewport. - """ - if (bl_object := bpy.data.objects.get(self.name)) is not None: - bpy.ops.object.select_all(action='DESELECT') - bl_object.select_set(True) - - msg = 'Managed BLObject does not exist' - raise ValueError(msg) - - #################### - # - BLObject Management - #################### - def bl_object( - self, - kind: typx.Literal['MESH', 'EMPTY', 'VOLUME'], - ): - """Returns the managed blender object. - - If the requested object data kind is different, then delete the old - object and recreate. - """ - # Remove Object (if mismatch) - if (bl_object := bpy.data.objects.get(self.name)) and bl_object.type != kind: - log.info( - 'Removing (recreating) "%s" (existing kind is "%s", but "%s" is requested)', - bl_object.name, - bl_object.type, - kind, - ) - self.free() - - # Create Object w/Appropriate Data Block - if not (bl_object := bpy.data.objects.get(self.name)): - log.info( - 'Creating "%s" with kind "%s"', - self.name, - kind, - ) - if kind == 'MESH': - bl_data = bpy.data.meshes.new(self.name) - elif kind == 'EMPTY': - bl_data = None - elif kind == 'VOLUME': - raise NotImplementedError - else: - msg = f'Created BLObject w/invalid kind "{bl_object.type}" for "{self.name}"' - raise ValueError(msg) - - bl_object = bpy.data.objects.new(self.name, bl_data) - log.debug( - 'Linking "%s" to Base Collection', - bl_object.name, - ) - managed_collection().objects.link(bl_object) - - return bl_object - - #################### - # - Mesh Data Properties - #################### - @property - def mesh_data(self) -> bpy.types.Mesh: - """Directly loads the Blender mesh data. - - Raises: - ValueError: If the object has no mesh data. - """ - if (bl_object := bpy.data.objects.get(self.name)) and bl_object.type == 'MESH': - return bl_object.data - - msg = f'Requested mesh data from {self.name} of type {bl_object.type}' - raise ValueError(msg) - - @contextlib.contextmanager - def mesh_as_bmesh( - self, - evaluate: bool = True, - triangulate: bool = False, - ) -> bpy.types.Mesh: - if (bl_object := bpy.data.objects.get(self.name)) and bl_object.type == 'MESH': - bmesh_mesh = None - try: - bmesh_mesh = bmesh.new() - if evaluate: - bmesh_mesh.from_object( - bl_object, - bpy.context.evaluated_depsgraph_get(), - ) - else: - bmesh_mesh.from_object(bl_object) - - if triangulate: - bmesh.ops.triangulate(bmesh_mesh, faces=bmesh_mesh.faces) - - yield bmesh_mesh - - finally: - if bmesh_mesh: - bmesh_mesh.free() - - else: - msg = f'Requested BMesh from "{self.name}" of type "{bl_object.type}"' - raise ValueError(msg) - - @property - def mesh_as_arrays(self) -> dict: - ## TODO: Cached - - # Ensure Updated Geometry - log.debug('Updating View Layer') - bpy.context.view_layer.update() - - # Compute Evaluted + Triangulated Mesh - log.debug('Casting BMesh of "%s" to Temporary Mesh', self.name) - _mesh = bpy.data.meshes.new(name='TemporaryMesh') - with self.mesh_as_bmesh(evaluate=True, triangulate=True) as bmesh_mesh: - bmesh_mesh.to_mesh(_mesh) - - # Optimized Vertex Copy - ## See - log.debug('Copying Vertices from "%s"', self.name) - verts = np.zeros(3 * len(_mesh.vertices), dtype=np.float64) - _mesh.vertices.foreach_get('co', verts) - verts.shape = (-1, 3) - - # Optimized Triangle Copy - ## To understand, read it, **carefully**. - log.debug('Copying Faces from "%s"', self.name) - faces = np.zeros(3 * len(_mesh.polygons), dtype=np.uint64) - _mesh.polygons.foreach_get('vertices', faces) - faces.shape = (-1, 3) - - # Remove Temporary Mesh - log.debug('Removing Temporary Mesh') - bpy.data.meshes.remove(_mesh) - - return { - 'verts': verts, - 'faces': faces, - } - - #################### - # - Modifiers - #################### - def bl_modifier( - self, - modifier_type: ModifierType, - ): - """Creates a new modifier for the current `bl_object`. - - - Modifier Type Names: - """ - if not (bl_object := bpy.data.objects.get(self.name)): - msg = f'Tried to add modifier to "{self.name}", but it has no bl_object' - raise ValueError(msg) - - # (Create and) Return Modifier - bl_modifier_name = MODIFIER_NAMES[modifier_type] - if bl_modifier_name not in bl_object.modifiers: - return bl_object.modifiers.new( - name=bl_modifier_name, - type=modifier_type, - ) - return bl_object.modifiers[bl_modifier_name] - - def modifier_attrs(self, modifier_type: ModifierType) -> dict: - """Based on the modifier type, retrieve a representative dictionary of modifier attributes. - The attributes can then easily be set using `setattr`. - """ - bl_modifier = self.bl_modifier(modifier_type) - - if modifier_type == 'NODES': - return { - 'node_group': bl_modifier.node_group, - } - elif modifier_type == 'ARRAY': - raise NotImplementedError - - def s_modifier_attrs( - self, - modifier_type: ModifierType, - modifier_attrs: dict, - ): - bl_modifier = self.bl_modifier(modifier_type) - - if modifier_type == 'NODES': - if bl_modifier.node_group != modifier_attrs['node_group']: - bl_modifier.node_group = modifier_attrs['node_group'] - elif modifier_type == 'ARRAY': - raise NotImplementedError - - #################### - # - GeoNodes Modifier - #################### - def sync_geonodes_modifier( - self, - geonodes_node_group, - geonodes_identifier_to_value: dict, - ): - """Push the given GeoNodes Interface values to a GeoNodes modifier attached to a managed MESH object. - - The values must be compatible with the `default_value`s of the interface sockets. - - If there is no object, it is created. - If the object isn't a MESH object, it is made so. - If the GeoNodes modifier doesn't exist, it is created. - If the GeoNodes node group doesn't match, it is changed. - Only differing interface values are actually changed. - """ - bl_object = self.bl_object('MESH') - - # Get (/make) a GeoModes Modifier - bl_modifier = self.bl_modifier('NODES') - - # Set GeoNodes Modifier Attributes (specifically, the 'node_group') - self.s_modifier_attrs('NODES', {'node_group': geonodes_node_group}) - - # Set GeoNodes Values - modifier_altered = False - for ( - interface_identifier, - value, - ) in geonodes_identifier_to_value.items(): - if bl_modifier[interface_identifier] != value: - # Quickly Determine if IDPropertyArray is Equal - if ( - hasattr(bl_modifier[interface_identifier], 'to_list') - and tuple(bl_modifier[interface_identifier].to_list()) == value - ): - continue - - # Quickly Determine int/float Mismatch - if isinstance( - bl_modifier[interface_identifier], - float, - ) and isinstance(value, int): - value = float(value) - - bl_modifier[interface_identifier] = value - - modifier_altered = True - - # Update DepGraph (if anything changed) - if modifier_altered: - bl_object.data.update() 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 430a14b..73b84a3 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 @@ -71,9 +71,9 @@ class ViewerNode(base.MaxwellSimNode): update=lambda self, context: self.sync_prop('auto_3d_preview', context), ) - cache__data_was_unlinked: bpy.props.BoolProperty( - name='Data Was Unlinked', - description="Whether the Data input was unlinked last time it was checked.", + cache__data_socket_linked: bpy.props.BoolProperty( + name='Data Is Linked', + description='Whether the Data input was linked last time it was checked.', default=True, ) @@ -113,9 +113,12 @@ class ViewerNode(base.MaxwellSimNode): #################### def print_data_to_console(self): import sys + for module_name, module in sys.modules.copy().items(): if module_name == '__mp_main__': - print('Anything, even repr(), with this module just crashes:', module_name) + print( + 'Anything, even repr(), with this module just crashes:', module_name + ) print(module) ## Crash if not self.inputs['Data'].is_linked: @@ -141,27 +144,31 @@ class ViewerNode(base.MaxwellSimNode): if self.inputs['Data'].is_linked and props['auto_plot']: self.trigger_action('show_plot') + #################### + # - Event Methods: 3D Preview + #################### @events.on_value_changed( - socket_name='Data', + prop_name='auto_3d_preview', props={'auto_3d_preview'}, ) - def on_changed_3d_data(self, props): - # Data Not Attached - if not self.inputs['Data'].is_linked: - self.cache__data_was_unlinked = True + def on_changed_3d_preview(self, props): + # Unpreview Everything + node_tree = self.id_data + node_tree.unpreview_all() - # Data Just Attached - elif self.cache__data_was_unlinked: - node_tree = self.id_data + # Trigger Preview Action + if self.inputs['Data'].is_linked and props['auto_3d_preview']: + log.info('Enabling 3D Previews from "%s"', self.name) + self.trigger_action('show_preview') - # Unpreview Everything - node_tree.unpreview_all() - - # Enable Previews in Tree - if props['auto_3d_preview']: - log.info('Enabling 3D Previews from "%s"', self.name) - self.trigger_action('show_preview') - self.cache__data_was_unlinked = False + @events.on_value_changed( + socket_name='Data', + ) + def on_changed_3d_data(self): + # Just Linked / Just Unlinked: Preview/Unpreview + if self.inputs['Data'].is_linked ^ self.cache__data_socket_linked: + self.on_changed_3d_preview() + self.cache__data_socket_linked = self.inputs['Data'].is_linked #################### 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 aa89bd0..182029c 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 @@ -89,15 +89,10 @@ class GeoNodesStructureNode(base.MaxwellSimNode): if (geonodes := input_sockets['GeoNodes']) is None: if ( managed_objs['modifier'].name - in managed_objs['mesh'].bl_object().modifiers + in managed_objs['mesh'].bl_object().modifiers.keys().copy() ): - log.info( - 'Removing Modifier "%s" from BLObject "%s"', - managed_objs['modifier'].name, - managed_objs['mesh'].name, - ) - managed_objs['mesh'].bl_object().modifiers.remove( - managed_objs['modifier'].name + managed_objs['modifier'].free_from_bl_object( + managed_objs['mesh'].bl_object() ) # Reset Loose Input Sockets