feat: Better link/append strategy for GN lookup

main
Sofus Albert Høgsbro Rose 2024-04-08 08:38:19 +02:00
parent 18abfd4296
commit 619704c46e
Signed by: so-rose
GPG Key ID: AD901CB0F3701434
6 changed files with 205 additions and 456 deletions

View File

@ -12,6 +12,7 @@ from ..utils import logger
log = logger.get(__name__) log = logger.get(__name__)
ImportMethod: typ.TypeAlias = typx.Literal['append', 'link']
BLOperatorStatus: typ.TypeAlias = set[ BLOperatorStatus: typ.TypeAlias = set[
typx.Literal['RUNNING_MODAL', 'CANCELLED', 'FINISHED', 'PASS_THROUGH', 'INTERFACE'] 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. 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' PrimitiveBox = 'box'
PrimitiveRing = 'ring' PrimitiveRing = 'ring'
PrimitiveSphere = 'sphere' PrimitiveSphere = 'sphere'
# GeoNodes Path Mapping # GeoNodes Paths
GN_PRIMITIVES_PATH = info.PATH_ASSETS / 'geonodes' / 'primitives' ## 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] = { GN_PARENT_PATHS: dict[GeoNodes, Path] = {
GeoNodes.PrimitiveBox: GN_PRIMITIVES_PATH, # Node Previews
GeoNodes.PrimitiveRing: GN_PRIMITIVES_PATH, ## Input
GeoNodes.PrimitiveSphere: GN_PRIMITIVES_PATH, 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) # - Import GeoNodes (Link/Append)
#################### ####################
ImportMethod: typ.TypeAlias = typx.Literal['append', 'link']
def import_geonodes( def import_geonodes(
geonodes: GeoNodes, geonodes: GeoNodes,
import_method: ImportMethod, import_method: ImportMethod,
force_import: bool = False,
) -> bpy.types.GeometryNodeGroup: ) -> 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. Returns:
- Retrieve the node group and return it. 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] return bpy.data.node_groups[geonodes]
filename = geonodes filename = geonodes
@ -144,8 +248,6 @@ class AppendGeoNodes(bpy.types.Operator):
# - Properties # - Properties
#################### ####################
_asset: bpy.types.AssetRepresentation | None = None _asset: bpy.types.AssetRepresentation | None = None
_start_drag_x: bpy.props.IntProperty()
_start_drag_y: bpy.props.IntProperty()
#################### ####################
# - UI # - UI
@ -168,9 +270,7 @@ class AppendGeoNodes(bpy.types.Operator):
""" """
return context.asset is not None return context.asset is not None
def invoke(self, context, event): def invoke(self, context: bpy.types.Context, _):
self._start_drag_x = event.mouse_x
self._start_drag_y = event.mouse_y
return self.execute(context) return self.execute(context)
def execute(self, context: bpy.types.Context) -> BLOperatorStatus: def execute(self, context: bpy.types.Context) -> BLOperatorStatus:

View File

@ -43,10 +43,10 @@ def _size_from_bl_socket(
description: str, description: str,
bl_socket_type: BLSocketType, 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. 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<Type> prefixes are used to determine which interface sockets are, in fact, 3D. A hard-coded set of NodeSocket<Type> 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. - 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, unit_system: dict | None = None,
allow_unit_not_in_unit_system: bool = False, allow_unit_not_in_unit_system: bool = False,
) -> typ.Any: ) -> typ.Any:
log.debug('Writing BL Socket Value (%s)', str(value))
socket_type = _socket_type_from_bl_socket(description, bl_socket_type) socket_type = _socket_type_from_bl_socket(description, bl_socket_type)
# Retrieve Unit-System Unit # Retrieve Unit-System Unit

View File

@ -1,3 +1,5 @@
"""A managed Blender modifier, associated with some Blender object."""
import typing as typ import typing as typ
import bpy import bpy
@ -10,22 +12,35 @@ from .. import contracts as ct
log = logger.get(__name__) log = logger.get(__name__)
ModifierType: typ.TypeAlias = typx.Literal['NODES', 'ARRAY'] ModifierType: typ.TypeAlias = typx.Literal['NODES', 'ARRAY']
NodeTreeInterfaceID: typ.TypeAlias = str NodeTreeInterfaceID: typ.TypeAlias = str
UnitSystem: typ.TypeAlias = typ.Any
####################
# - Modifier Attributes
####################
class ModifierAttrsNODES(typ.TypedDict): 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 node_group: bpy.types.GeometryNodeTree
unit_system: bpy.types.GeometryNodeTree unit_system: UnitSystem
inputs: dict[NodeTreeInterfaceID, typ.Any] inputs: dict[NodeTreeInterfaceID, typ.Any]
class ModifierAttrsARRAY(typ.TypedDict): class ModifierAttrsARRAY(typ.TypedDict):
pass """Describes values set on an Array modifier."""
ModifierAttrs: typ.TypeAlias = ModifierAttrsNODES | ModifierAttrsARRAY ModifierAttrs: typ.TypeAlias = ModifierAttrsNODES | ModifierAttrsARRAY
MODIFIER_NAMES = { MODIFIER_NAMES = {
'NODES': 'BLMaxwell_GeoNodes', 'NODES': 'BLMaxwell_GeoNodes',
'ARRAY': 'BLMaxwell_Array', 'ARRAY': 'BLMaxwell_Array',
@ -37,6 +52,7 @@ MODIFIER_NAMES = {
#################### ####################
def read_modifier(bl_modifier: bpy.types.Modifier) -> ModifierAttrs: def read_modifier(bl_modifier: bpy.types.Modifier) -> ModifierAttrs:
if bl_modifier.type == 'NODES': if bl_modifier.type == 'NODES':
## TODO: Also get GeoNodes modifier values, if the nodegroup is not-None.
return { return {
'node_group': bl_modifier.node_group, 'node_group': bl_modifier.node_group,
} }
@ -50,9 +66,18 @@ def read_modifier(bl_modifier: bpy.types.Modifier) -> ModifierAttrs:
# - Write Modifier Information # - Write Modifier Information
#################### ####################
def write_modifier_geonodes( def write_modifier_geonodes(
bl_modifier: bpy.types.Modifier, bl_modifier: bpy.types.NodesModifier,
modifier_attrs: ModifierAttrsNODES, modifier_attrs: ModifierAttrsNODES,
) -> bool: ) -> 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 modifier_altered = False
# Alter GeoNodes Group # Alter GeoNodes Group
if bl_modifier.node_group != modifier_attrs['node_group']: if bl_modifier.node_group != modifier_attrs['node_group']:
@ -163,7 +188,28 @@ class ManagedBLModifier(ct.schemas.ManagedObj):
# - Deallocation # - Deallocation
#################### ####################
def free(self): 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 # - Modifiers
@ -190,7 +236,7 @@ class ManagedBLModifier(ct.schemas.ManagedObj):
bl_modifier.type, bl_modifier.type,
modifier_type, modifier_type,
) )
self.free() self.free_from_bl_object(bl_object)
modifier_was_removed = True modifier_was_removed = True
# Create Modifier # Create Modifier

View File

@ -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 <https://blog.michelanders.nl/2016/02/copying-vertices-to-numpy-arrays-in_4.html>
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: <https://docs.blender.org/api/current/bpy_types_enum_items/object_modifier_type_items.html#rna-enum-object-modifier-type-items>
"""
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()

View File

@ -71,9 +71,9 @@ class ViewerNode(base.MaxwellSimNode):
update=lambda self, context: self.sync_prop('auto_3d_preview', context), update=lambda self, context: self.sync_prop('auto_3d_preview', context),
) )
cache__data_was_unlinked: bpy.props.BoolProperty( cache__data_socket_linked: bpy.props.BoolProperty(
name='Data Was Unlinked', name='Data Is Linked',
description="Whether the Data input was unlinked last time it was checked.", description='Whether the Data input was linked last time it was checked.',
default=True, default=True,
) )
@ -113,9 +113,12 @@ class ViewerNode(base.MaxwellSimNode):
#################### ####################
def print_data_to_console(self): def print_data_to_console(self):
import sys import sys
for module_name, module in sys.modules.copy().items(): for module_name, module in sys.modules.copy().items():
if module_name == '__mp_main__': 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 print(module) ## Crash
if not self.inputs['Data'].is_linked: 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']: if self.inputs['Data'].is_linked and props['auto_plot']:
self.trigger_action('show_plot') self.trigger_action('show_plot')
####################
# - Event Methods: 3D Preview
####################
@events.on_value_changed( @events.on_value_changed(
socket_name='Data', prop_name='auto_3d_preview',
props={'auto_3d_preview'}, props={'auto_3d_preview'},
) )
def on_changed_3d_data(self, props): def on_changed_3d_preview(self, props):
# Data Not Attached # Unpreview Everything
if not self.inputs['Data'].is_linked: node_tree = self.id_data
self.cache__data_was_unlinked = True node_tree.unpreview_all()
# Data Just Attached # Trigger Preview Action
elif self.cache__data_was_unlinked: if self.inputs['Data'].is_linked and props['auto_3d_preview']:
node_tree = self.id_data log.info('Enabling 3D Previews from "%s"', self.name)
self.trigger_action('show_preview')
# Unpreview Everything @events.on_value_changed(
node_tree.unpreview_all() socket_name='Data',
)
# Enable Previews in Tree def on_changed_3d_data(self):
if props['auto_3d_preview']: # Just Linked / Just Unlinked: Preview/Unpreview
log.info('Enabling 3D Previews from "%s"', self.name) if self.inputs['Data'].is_linked ^ self.cache__data_socket_linked:
self.trigger_action('show_preview') self.on_changed_3d_preview()
self.cache__data_was_unlinked = False self.cache__data_socket_linked = self.inputs['Data'].is_linked
#################### ####################

View File

@ -89,15 +89,10 @@ class GeoNodesStructureNode(base.MaxwellSimNode):
if (geonodes := input_sockets['GeoNodes']) is None: if (geonodes := input_sockets['GeoNodes']) is None:
if ( if (
managed_objs['modifier'].name managed_objs['modifier'].name
in managed_objs['mesh'].bl_object().modifiers in managed_objs['mesh'].bl_object().modifiers.keys().copy()
): ):
log.info( managed_objs['modifier'].free_from_bl_object(
'Removing Modifier "%s" from BLObject "%s"', managed_objs['mesh'].bl_object()
managed_objs['modifier'].name,
managed_objs['mesh'].name,
)
managed_objs['mesh'].bl_object().modifiers.remove(
managed_objs['modifier'].name
) )
# Reset Loose Input Sockets # Reset Loose Input Sockets