feat: Better link/append strategy for GN lookup
parent
18abfd4296
commit
619704c46e
|
@ -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:
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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),
|
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
|
||||||
|
|
||||||
|
|
||||||
####################
|
####################
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in New Issue