feat: Better link/append strategy for GN lookup
parent
18abfd4296
commit
619704c46e
|
@ -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:
|
||||
|
|
|
@ -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<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.
|
||||
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
|
@ -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
|
||||
|
||||
# Data Just Attached
|
||||
elif self.cache__data_was_unlinked:
|
||||
node_tree = self.id_data
|
||||
|
||||
def on_changed_3d_preview(self, props):
|
||||
# Unpreview Everything
|
||||
node_tree = self.id_data
|
||||
node_tree.unpreview_all()
|
||||
|
||||
# Enable Previews in Tree
|
||||
if props['auto_3d_preview']:
|
||||
# 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')
|
||||
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
|
||||
|
||||
|
||||
####################
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue