fix: GN update for custom geonodes
We also implement `BLField` support for Blender `IDStruct` types. There's a crash haunting us specifically with the cylinder array. Other primitives (esp. ring) work just fine, as does previews of nested-linked node groups. The crash triggers specifically when the file is saved and reloaded, whereafter all the `load_post` handlers run fine (like - extremely fine, we can perfectly access and dereference all of the node groups, seemingly all the objects, etc.). Then, crash. We're also discovering that `id_properties_ui` is completely useless for after-the-fact updating of custom properties within sockets. Which is a great surprise, and I have trouble thinking it's on purpose - the data is stored somewhere, after all. All the forced updates/redraws/etc. in the world don't seem to change this. We may have to go back to the drawing board with dynamically-updated min/max. The entire infrastructure with `SocketDef` altering sockets after creation is entirely, _violently_ unsuited to do a static modification. But the bare fact is, the dynamic modification methods are falling short. It's kind of important stuff, this stuff.main
parent
7ac6b615de
commit
785c6f764c
|
@ -149,6 +149,7 @@ def manage_pydeps(*_):
|
||||||
# path_addon_pydeps='',
|
# path_addon_pydeps='',
|
||||||
# path_addon_reqs='',
|
# path_addon_reqs='',
|
||||||
# )
|
# )
|
||||||
|
log.debug('PyDeps: Analyzing Post-File Load')
|
||||||
ct.addon.prefs().on_addon_pydeps_changed(show_popup_if_deps_invalid=True)
|
ct.addon.prefs().on_addon_pydeps_changed(show_popup_if_deps_invalid=True)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -220,7 +220,6 @@ class GeoNodes(enum.StrEnum):
|
||||||
####################
|
####################
|
||||||
def import_geonodes(
|
def import_geonodes(
|
||||||
_geonodes: GeoNodes,
|
_geonodes: GeoNodes,
|
||||||
force_append: bool = False,
|
|
||||||
) -> bpy.types.GeometryNodeGroup:
|
) -> bpy.types.GeometryNodeGroup:
|
||||||
"""Given vendored GeoNodes tree link/append and return the local datablock.
|
"""Given vendored GeoNodes tree link/append and return the local datablock.
|
||||||
|
|
||||||
|
@ -496,6 +495,7 @@ class GeoNodesToStructureNode(bpy.types.Operator):
|
||||||
)
|
)
|
||||||
bpy.ops.node.select_all(action='DESELECT')
|
bpy.ops.node.select_all(action='DESELECT')
|
||||||
node = node_tree.nodes.new(geonodes.dedicated_node_type)
|
node = node_tree.nodes.new(geonodes.dedicated_node_type)
|
||||||
|
node.sim_node_name = asset.name
|
||||||
node.select = True
|
node.select = True
|
||||||
node.location.x = node_location[0]
|
node.location.x = node_location[0]
|
||||||
node.location.y = node_location[1]
|
node.location.y = node_location[1]
|
||||||
|
@ -505,9 +505,9 @@ class GeoNodesToStructureNode(bpy.types.Operator):
|
||||||
## Since the node doesn't itself handle the structure, we must.
|
## Since the node doesn't itself handle the structure, we must.
|
||||||
## We just import the GN tree, then attach the data block to the node.
|
## We just import the GN tree, then attach the data block to the node.
|
||||||
if geonodes.dedicated_node_type == 'GeoNodesStructureNodeType':
|
if geonodes.dedicated_node_type == 'GeoNodesStructureNodeType':
|
||||||
|
## TODO: Is this too presumptuous? Or a fine little hack?
|
||||||
geonodes_data = import_geonodes(asset.name)
|
geonodes_data = import_geonodes(asset.name)
|
||||||
node.inputs['GeoNodes'].value = geonodes_data
|
node.inputs['GeoNodes'].value = geonodes_data
|
||||||
## TODO: Is this too presumptuous? Or a fine little hack?
|
|
||||||
|
|
||||||
# Restore the Pre-Modal Mouse Cursor Shape
|
# Restore the Pre-Modal Mouse Cursor Shape
|
||||||
context.window.cursor_modal_restore()
|
context.window.cursor_modal_restore()
|
||||||
|
|
BIN
src/blender_maxwell/assets/internal/structure/_structure_primitive_cylinder.blend (Stored with Git LFS)
BIN
src/blender_maxwell/assets/internal/structure/_structure_primitive_cylinder.blend (Stored with Git LFS)
Binary file not shown.
BIN
src/blender_maxwell/assets/internal/structure/_structure_primitive_ring.blend (Stored with Git LFS)
BIN
src/blender_maxwell/assets/internal/structure/_structure_primitive_ring.blend (Stored with Git LFS)
Binary file not shown.
BIN
src/blender_maxwell/assets/structures/arrays/array_ring.blend (Stored with Git LFS)
BIN
src/blender_maxwell/assets/structures/arrays/array_ring.blend (Stored with Git LFS)
Binary file not shown.
|
@ -53,44 +53,44 @@ BLClass: typ.TypeAlias = (
|
||||||
| bpy.types.FileHandler
|
| bpy.types.FileHandler
|
||||||
)
|
)
|
||||||
BLIDStruct: typ.TypeAlias = (
|
BLIDStruct: typ.TypeAlias = (
|
||||||
bpy.types.Action,
|
bpy.types.Action
|
||||||
bpy.types.Armature,
|
| bpy.types.Armature
|
||||||
bpy.types.Brush,
|
| bpy.types.Brush
|
||||||
bpy.types.CacheFile,
|
| bpy.types.CacheFile
|
||||||
bpy.types.Camera,
|
| bpy.types.Camera
|
||||||
bpy.types.Collection,
|
| bpy.types.Collection
|
||||||
bpy.types.Curve,
|
| bpy.types.Curve
|
||||||
bpy.types.Curves,
|
| bpy.types.Curves
|
||||||
bpy.types.FreestyleLineStyle,
|
| bpy.types.FreestyleLineStyle
|
||||||
bpy.types.GreasePencil,
|
| bpy.types.GreasePencil
|
||||||
bpy.types.Image,
|
| bpy.types.Image
|
||||||
bpy.types.Key,
|
| bpy.types.Key
|
||||||
bpy.types.Lattice,
|
| bpy.types.Lattice
|
||||||
bpy.types.Library,
|
| bpy.types.Library
|
||||||
bpy.types.Light,
|
| bpy.types.Light
|
||||||
bpy.types.LightProbe,
|
| bpy.types.LightProbe
|
||||||
bpy.types.Mask,
|
| bpy.types.Mask
|
||||||
bpy.types.Material,
|
| bpy.types.Material
|
||||||
bpy.types.Mesh,
|
| bpy.types.Mesh
|
||||||
bpy.types.MetaBall,
|
| bpy.types.MetaBall
|
||||||
bpy.types.MovieClip,
|
| bpy.types.MovieClip
|
||||||
bpy.types.NodeTree,
|
| bpy.types.NodeTree
|
||||||
bpy.types.Object,
|
| bpy.types.Object
|
||||||
bpy.types.PaintCurve,
|
| bpy.types.PaintCurve
|
||||||
bpy.types.Palette,
|
| bpy.types.Palette
|
||||||
bpy.types.ParticleSettings,
|
| bpy.types.ParticleSettings
|
||||||
bpy.types.PointCloud,
|
| bpy.types.PointCloud
|
||||||
bpy.types.Scene,
|
| bpy.types.Scene
|
||||||
bpy.types.Screen,
|
| bpy.types.Screen
|
||||||
bpy.types.Sound,
|
| bpy.types.Sound
|
||||||
bpy.types.Speaker,
|
| bpy.types.Speaker
|
||||||
bpy.types.Text,
|
| bpy.types.Text
|
||||||
bpy.types.Texture,
|
| bpy.types.Texture
|
||||||
bpy.types.VectorFont,
|
| bpy.types.VectorFont
|
||||||
bpy.types.Volume,
|
| bpy.types.Volume
|
||||||
bpy.types.WindowManager,
|
| bpy.types.WindowManager
|
||||||
bpy.types.WorkSpace,
|
| bpy.types.WorkSpace
|
||||||
bpy.types.World,
|
| bpy.types.World
|
||||||
)
|
)
|
||||||
BLKeymapItem: typ.TypeAlias = typ.Any ## TODO: Better Type
|
BLKeymapItem: typ.TypeAlias = typ.Any ## TODO: Better Type
|
||||||
BLPropFlag: typ.TypeAlias = typ.Literal[
|
BLPropFlag: typ.TypeAlias = typ.Literal[
|
||||||
|
|
|
@ -34,6 +34,9 @@ class OperatorType(enum.StrEnum):
|
||||||
|
|
||||||
GeoNodesToStructureNode = enum.auto()
|
GeoNodesToStructureNode = enum.auto()
|
||||||
|
|
||||||
|
# Socket: GeoNodesSocket
|
||||||
|
SocketGeoNodesReset = enum.auto()
|
||||||
|
|
||||||
# Socket: Tidy3DCloudTask
|
# Socket: Tidy3DCloudTask
|
||||||
SocketCloudAuthenticate = enum.auto()
|
SocketCloudAuthenticate = enum.auto()
|
||||||
SocketReloadCloudFolderList = enum.auto()
|
SocketReloadCloudFolderList = enum.auto()
|
||||||
|
|
|
@ -37,11 +37,41 @@ class BLSocketInfo:
|
||||||
is_preview: bool
|
is_preview: bool
|
||||||
socket_type: SocketType | None
|
socket_type: SocketType | None
|
||||||
size: spux.NumberSize1D | None
|
size: spux.NumberSize1D | None
|
||||||
|
mathtype: spux.MathType | None
|
||||||
physical_type: spux.PhysicalType | None
|
physical_type: spux.PhysicalType | None
|
||||||
default_value: spux.ScalarUnitlessRealExpr
|
default_value: spux.ScalarUnitlessRealExpr
|
||||||
|
|
||||||
bl_isocket_identifier: spux.ScalarUnitlessRealExpr
|
bl_isocket_identifier: spux.ScalarUnitlessRealExpr
|
||||||
|
|
||||||
|
def encode(
|
||||||
|
self, raw_value: typ.Any, unit_system: spux.UnitSystem | None
|
||||||
|
) -> typ.Any:
|
||||||
|
"""Encode a raw value, given a unit system, to be directly writable to a node socket.
|
||||||
|
|
||||||
|
This encoded form is also guaranteed to support writing to a node socket via a modifier interface.
|
||||||
|
"""
|
||||||
|
# Non-Numerical: Passthrough
|
||||||
|
if unit_system is None or self.physical_type is None:
|
||||||
|
return raw_value
|
||||||
|
|
||||||
|
# Numerical: Convert to Pure Python Type
|
||||||
|
if (
|
||||||
|
unit_system is not None
|
||||||
|
and self.physical_type is not spux.PhysicalType.NonPhysical
|
||||||
|
):
|
||||||
|
unitless_value = spux.scale_to_unit_system(raw_value, unit_system)
|
||||||
|
elif isinstance(raw_value, spux.SympyType):
|
||||||
|
unitless_value = spux.sympy_to_python(raw_value)
|
||||||
|
else:
|
||||||
|
unitless_value = raw_value
|
||||||
|
|
||||||
|
# Coerce int -> float w/Target is Real
|
||||||
|
## -> The value - modifier - GN path is more strict than properties.
|
||||||
|
if self.mathtype is spux.MathType.Real and isinstance(unitless_value, int):
|
||||||
|
return float(unitless_value)
|
||||||
|
|
||||||
|
return unitless_value
|
||||||
|
|
||||||
|
|
||||||
class BLSocketType(enum.StrEnum):
|
class BLSocketType(enum.StrEnum):
|
||||||
Virtual = 'NodeSocketVirtual'
|
Virtual = 'NodeSocketVirtual'
|
||||||
|
@ -85,12 +115,20 @@ class BLSocketType(enum.StrEnum):
|
||||||
def from_bl_isocket(
|
def from_bl_isocket(
|
||||||
bl_isocket: bpy.types.NodeTreeInterfaceSocket,
|
bl_isocket: bpy.types.NodeTreeInterfaceSocket,
|
||||||
) -> typ.Self:
|
) -> typ.Self:
|
||||||
|
"""Deduce the exact `BLSocketType` represented by an interface socket.
|
||||||
|
|
||||||
|
Interface sockets are an abstraction of what any instance of a particular node tree _will have of input sockets_ once constructed.
|
||||||
|
"""
|
||||||
return BLSocketType(bl_isocket.bl_socket_idname)
|
return BLSocketType(bl_isocket.bl_socket_idname)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def info_from_bl_isocket(
|
def info_from_bl_isocket(
|
||||||
bl_isocket: bpy.types.NodeTreeInterfaceSocket,
|
bl_isocket: bpy.types.NodeTreeInterfaceSocket,
|
||||||
) -> typ.Self:
|
) -> typ.Self:
|
||||||
|
"""Deduce all `BLSocketInfo` from an interface socket.
|
||||||
|
|
||||||
|
This is a high-level method providing a clean way to chain `BLSocketType.from_bl_isocket()` together with `self.parse()`.
|
||||||
|
"""
|
||||||
bl_socket_type = BLSocketType.from_bl_isocket(bl_isocket)
|
bl_socket_type = BLSocketType.from_bl_isocket(bl_isocket)
|
||||||
if bl_socket_type.has_support:
|
if bl_socket_type.has_support:
|
||||||
return bl_socket_type.parse(
|
return bl_socket_type.parse(
|
||||||
|
@ -103,13 +141,21 @@ class BLSocketType(enum.StrEnum):
|
||||||
####################
|
####################
|
||||||
@property
|
@property
|
||||||
def has_support(self) -> bool:
|
def has_support(self) -> bool:
|
||||||
|
"""Decides whether the current `BLSocketType` is explicitly not supported.
|
||||||
|
|
||||||
|
Not all socket types make sense to represent in our node tree.
|
||||||
|
In general, these should be skipped.
|
||||||
|
"""
|
||||||
BLST = BLSocketType
|
BLST = BLSocketType
|
||||||
return {
|
return {
|
||||||
|
# Won't Fix
|
||||||
BLST.Virtual: False,
|
BLST.Virtual: False,
|
||||||
BLST.Geometry: False,
|
BLST.Geometry: False,
|
||||||
BLST.Shader: False,
|
BLST.Shader: False,
|
||||||
BLST.FloatUnsigned: False,
|
BLST.FloatUnsigned: False,
|
||||||
BLST.IntUnsigned: False,
|
BLST.IntUnsigned: False,
|
||||||
|
## TODO
|
||||||
|
BLST.Menu: False,
|
||||||
}.get(self, True)
|
}.get(self, True)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@ -132,12 +178,36 @@ class BLSocketType(enum.StrEnum):
|
||||||
ST = SocketType
|
ST = SocketType
|
||||||
return {
|
return {
|
||||||
# Blender
|
# Blender
|
||||||
|
BLST.Image: ST.BlenderImage,
|
||||||
|
BLST.Material: ST.BlenderMaterial,
|
||||||
|
BLST.Object: ST.BlenderObject,
|
||||||
|
BLST.Collection: ST.BlenderCollection,
|
||||||
# Basic
|
# Basic
|
||||||
BLST.Bool: ST.Bool,
|
BLST.Bool: ST.BlenderCollection,
|
||||||
|
BLST.String: ST.String,
|
||||||
# Float
|
# Float
|
||||||
# Array-Like
|
BLST.Float: ST.Expr,
|
||||||
|
BLST.FloatAngle: ST.Expr,
|
||||||
|
BLST.FloatDistance: ST.Expr,
|
||||||
|
BLST.FloatFactor: ST.Expr,
|
||||||
|
BLST.FloatPercentage: ST.Expr,
|
||||||
|
BLST.FloatTime: ST.Expr,
|
||||||
|
BLST.FloatTimeAbsolute: ST.Expr,
|
||||||
|
# Int
|
||||||
|
BLST.Int: ST.Expr,
|
||||||
|
BLST.IntFactor: ST.Expr,
|
||||||
|
BLST.IntPercentage: ST.Expr,
|
||||||
|
# Vector
|
||||||
BLST.Color: ST.Color,
|
BLST.Color: ST.Color,
|
||||||
}.get(self, ST.Expr)
|
BLST.Rotation: ST.Expr,
|
||||||
|
BLST.Vector: ST.Expr,
|
||||||
|
BLST.VectorAcceleration: ST.Expr,
|
||||||
|
BLST.VectorDirection: ST.Expr,
|
||||||
|
BLST.VectorEuler: ST.Expr,
|
||||||
|
BLST.VectorTranslation: ST.Expr,
|
||||||
|
BLST.VectorVelocity: ST.Expr,
|
||||||
|
BLST.VectorXYZ: ST.Expr,
|
||||||
|
}[self]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def mathtype(self) -> spux.MathType | None:
|
def mathtype(self) -> spux.MathType | None:
|
||||||
|
@ -240,7 +310,7 @@ class BLSocketType(enum.StrEnum):
|
||||||
BLST.FloatAngle: P.Angle,
|
BLST.FloatAngle: P.Angle,
|
||||||
BLST.FloatDistance: P.Length,
|
BLST.FloatDistance: P.Length,
|
||||||
BLST.FloatTime: P.Time,
|
BLST.FloatTime: P.Time,
|
||||||
BLST.FloatTimeAbsolute: P.Time, ## What's the difference?
|
BLST.FloatTimeAbsolute: P.Time, ## TODO: What's the difference?
|
||||||
BLST.VectorAcceleration: P.Accel,
|
BLST.VectorAcceleration: P.Accel,
|
||||||
## BLST.VectorDirection: Directions are unitless (within cartesian)
|
## BLST.VectorDirection: Directions are unitless (within cartesian)
|
||||||
BLST.VectorEuler: P.Angle,
|
BLST.VectorEuler: P.Angle,
|
||||||
|
@ -291,8 +361,8 @@ class BLSocketType(enum.StrEnum):
|
||||||
def parse(
|
def parse(
|
||||||
self, bl_default_value: typ.Any, description: str, bl_isocket_identifier: str
|
self, bl_default_value: typ.Any, description: str, bl_isocket_identifier: str
|
||||||
) -> BLSocketInfo:
|
) -> BLSocketInfo:
|
||||||
# Parse the Description
|
# Unpack Description
|
||||||
## TODO: Some kind of error on invalid parse if there is also no unambiguous physical type
|
## -> TODO: Raise an kind of error on invalid parse if there is also no unambiguous physical type
|
||||||
descr_params = description.split(BL_SOCKET_DESCR_ANNOT_STRING)[0]
|
descr_params = description.split(BL_SOCKET_DESCR_ANNOT_STRING)[0]
|
||||||
directive = (
|
directive = (
|
||||||
_tokens[0]
|
_tokens[0]
|
||||||
|
@ -300,33 +370,60 @@ class BLSocketType(enum.StrEnum):
|
||||||
else _tokens[1]
|
else _tokens[1]
|
||||||
)
|
)
|
||||||
|
|
||||||
## Interpret the Description Parse
|
# Parse PhysicalType
|
||||||
|
## -> None if there is no appropriate MathType.
|
||||||
|
## -> Otherwise, prefer unambiguous - description hint - NonPhysical
|
||||||
|
has_physical_type = self.mathtype in [
|
||||||
|
spux.MathType.Integer,
|
||||||
|
spux.MathType.Rational,
|
||||||
|
spux.MathType.Real,
|
||||||
|
spux.MathType.Complex,
|
||||||
|
]
|
||||||
|
if has_physical_type:
|
||||||
parsed_physical_type = getattr(spux.PhysicalType, directive, None)
|
parsed_physical_type = getattr(spux.PhysicalType, directive, None)
|
||||||
physical_type = (
|
physical_type = (
|
||||||
self.unambiguous_physical_type
|
self.unambiguous_physical_type
|
||||||
if self.unambiguous_physical_type is not None
|
if self.unambiguous_physical_type is not None
|
||||||
else parsed_physical_type
|
else (
|
||||||
|
parsed_physical_type
|
||||||
|
if parsed_physical_type is not None
|
||||||
|
else spux.PhysicalType.NonPhysical
|
||||||
)
|
)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
physical_type = None
|
||||||
|
|
||||||
# Parse the Default Value
|
# Parse Default Value
|
||||||
|
## -> Read the Blender socket's default value and convrt it
|
||||||
if self.mathtype is not None and bl_default_value is not None:
|
if self.mathtype is not None and bl_default_value is not None:
|
||||||
|
# Scalar: Convert to Pure Python TYpe
|
||||||
if self.size == spux.NumberSize1D.Scalar:
|
if self.size == spux.NumberSize1D.Scalar:
|
||||||
default_value = self.mathtype.pytype(bl_default_value)
|
default_value = self.mathtype.pytype(bl_default_value)
|
||||||
|
|
||||||
|
# 2D (Description Hint): Sympy Matrix
|
||||||
|
## -> The description hint "2D" is the trigger for this.
|
||||||
|
## -> Ignore the last component to get the effect of "2D".
|
||||||
elif description.startswith('2D'):
|
elif description.startswith('2D'):
|
||||||
default_value = sp.Matrix(tuple(bl_default_value)[:2])
|
default_value = sp.ImmutableMatrix(tuple(bl_default_value)[:2])
|
||||||
|
|
||||||
|
# 3D/4D: Simple Parse to Sympy Matrix
|
||||||
|
## -> We don't explicitly check the size.
|
||||||
else:
|
else:
|
||||||
default_value = sp.Matrix(tuple(bl_default_value))
|
default_value = sp.ImmutableMatrix(tuple(bl_default_value))
|
||||||
|
|
||||||
else:
|
else:
|
||||||
|
# Non-Mathematical: Passthrough
|
||||||
default_value = bl_default_value
|
default_value = bl_default_value
|
||||||
|
|
||||||
# Return Parsed Socket Information
|
# Return Parsed Socket Information
|
||||||
## -> Combining directly known and parsed knowledge.
|
## -> Combining directly known and parsed knowledge.
|
||||||
## -> Should contain everything needed to match the Blender socket.
|
## -> Should contain everything needed to create a socket in our tree.
|
||||||
return BLSocketInfo(
|
return BLSocketInfo(
|
||||||
has_support=self.has_support,
|
has_support=self.has_support,
|
||||||
is_preview=(directive == 'Preview'),
|
is_preview=(directive == 'Preview'),
|
||||||
socket_type=self.socket_type,
|
socket_type=self.socket_type,
|
||||||
size=self.size,
|
size=self.size,
|
||||||
|
mathtype=self.mathtype,
|
||||||
physical_type=physical_type,
|
physical_type=physical_type,
|
||||||
default_value=default_value,
|
default_value=default_value,
|
||||||
bl_isocket_identifier=bl_isocket_identifier,
|
bl_isocket_identifier=bl_isocket_identifier,
|
||||||
|
|
|
@ -103,8 +103,8 @@ class ManagedBLMesh(base.ManagedObj):
|
||||||
"""
|
"""
|
||||||
bl_object = bpy.data.objects.get(self.name)
|
bl_object = bpy.data.objects.get(self.name)
|
||||||
if bl_object is None:
|
if bl_object is None:
|
||||||
log.info('%s (ManagedBLMesh): Created BLObject for Preview', bl_object.name)
|
|
||||||
bl_object = self.bl_object()
|
bl_object = self.bl_object()
|
||||||
|
log.info('%s (ManagedBLMesh): Created BLObject for Preview', bl_object.name)
|
||||||
|
|
||||||
if bl_object.name not in preview_collection().objects:
|
if bl_object.name not in preview_collection().objects:
|
||||||
log.info('Moving "%s" to Preview Collection', bl_object.name)
|
log.info('Moving "%s" to Preview Collection', bl_object.name)
|
||||||
|
|
|
@ -97,6 +97,7 @@ def write_modifier_geonodes(
|
||||||
modifier_altered = False
|
modifier_altered = False
|
||||||
|
|
||||||
# Alter GeoNodes Group
|
# Alter GeoNodes Group
|
||||||
|
## -> Check the existing node group, replace if it differs.
|
||||||
if bl_modifier.node_group != modifier_attrs['node_group']:
|
if bl_modifier.node_group != modifier_attrs['node_group']:
|
||||||
log.info(
|
log.info(
|
||||||
'Changing GeoNodes Modifier NodeTree from "%s" to "%s"',
|
'Changing GeoNodes Modifier NodeTree from "%s" to "%s"',
|
||||||
|
@ -106,30 +107,25 @@ def write_modifier_geonodes(
|
||||||
bl_modifier.node_group = modifier_attrs['node_group']
|
bl_modifier.node_group = modifier_attrs['node_group']
|
||||||
modifier_altered = True
|
modifier_altered = True
|
||||||
|
|
||||||
# Alter GeoNodes Modifier Inputs
|
# Parse GeoNodes Socket Info
|
||||||
|
## -> TODO: Slow and hard to optimize, but very likely worth it.
|
||||||
socket_infos = bl_socket_map.info_from_geonodes(bl_modifier.node_group)
|
socket_infos = bl_socket_map.info_from_geonodes(bl_modifier.node_group)
|
||||||
|
|
||||||
for socket_name in modifier_attrs['inputs']:
|
for socket_name in modifier_attrs['inputs']:
|
||||||
|
# Retrieve Modifier Interface ID
|
||||||
|
## -> iface_id translates "modifier socket" to "GN input socket".
|
||||||
iface_id = socket_infos[socket_name].bl_isocket_identifier
|
iface_id = socket_infos[socket_name].bl_isocket_identifier
|
||||||
input_value = modifier_attrs['inputs'][socket_name]
|
|
||||||
|
|
||||||
if modifier_attrs['unit_system'] is not None and not isinstance(
|
# Deduce Value to Write
|
||||||
input_value, bool
|
## -> This may involve a unit system conversion.
|
||||||
):
|
## -> Special Case: Booleans do not go through unit conversion.
|
||||||
value_to_write = spux.scale_to_unit_system(
|
## -> TODO: A special case isn't clean enough.
|
||||||
input_value, modifier_attrs['unit_system']
|
bl_modifier[iface_id] = socket_infos[socket_name].encode(
|
||||||
|
raw_value=modifier_attrs['inputs'][socket_name],
|
||||||
|
unit_system=modifier_attrs['unit_system'],
|
||||||
)
|
)
|
||||||
else:
|
|
||||||
value_to_write = input_value
|
|
||||||
|
|
||||||
# Edge Case: int -> float
|
|
||||||
if isinstance(bl_modifier[iface_id], float) and isinstance(value_to_write, int):
|
|
||||||
bl_modifier[iface_id] = float(value_to_write)
|
|
||||||
else:
|
|
||||||
bl_modifier[iface_id] = value_to_write
|
|
||||||
|
|
||||||
modifier_altered = True
|
modifier_altered = True
|
||||||
## TODO: More fine-grained alterations
|
## TODO: More fine-grained alterations?
|
||||||
|
|
||||||
return modifier_altered # noqa: RET504
|
return modifier_altered # noqa: RET504
|
||||||
|
|
||||||
|
|
|
@ -435,6 +435,7 @@ def initialize_sim_tree_node_link_cache(_):
|
||||||
"""Whenever a file is loaded, create/regenerate the NodeLinkCache in all trees."""
|
"""Whenever a file is loaded, create/regenerate the NodeLinkCache in all trees."""
|
||||||
for node_tree in bpy.data.node_groups:
|
for node_tree in bpy.data.node_groups:
|
||||||
if node_tree.bl_idname == 'MaxwellSimTree':
|
if node_tree.bl_idname == 'MaxwellSimTree':
|
||||||
|
log.debug('%s: Initializing NodeLinkCache for NodeTree', str(node_tree))
|
||||||
node_tree.on_load()
|
node_tree.on_load()
|
||||||
|
|
||||||
|
|
||||||
|
@ -450,13 +451,26 @@ def populate_missing_persistence(_) -> None:
|
||||||
for _node_tree in bpy.data.node_groups
|
for _node_tree in bpy.data.node_groups
|
||||||
if _node_tree.bl_idname == ct.TreeType.MaxwellSim.value and _node_tree.is_active
|
if _node_tree.bl_idname == ct.TreeType.MaxwellSim.value and _node_tree.is_active
|
||||||
]:
|
]:
|
||||||
|
log.debug(
|
||||||
|
'%s: Regenerating Dynamic Field Persistance for NodeTree nodes/sockets',
|
||||||
|
str(node_tree),
|
||||||
|
)
|
||||||
# Iterate over MaxwellSim Nodes
|
# Iterate over MaxwellSim Nodes
|
||||||
# -> Excludes ex. frame and reroute nodes.
|
# -> Excludes ex. frame and reroute nodes.
|
||||||
for node in [_node for _node in node_tree.nodes if hasattr(_node, 'node_type')]:
|
for node in [_node for _node in node_tree.nodes if hasattr(_node, 'node_type')]:
|
||||||
|
log.debug(
|
||||||
|
'-> %s: Regenerating Dynamic Field Persistance for Node',
|
||||||
|
str(node),
|
||||||
|
)
|
||||||
node.regenerate_dynamic_field_persistance()
|
node.regenerate_dynamic_field_persistance()
|
||||||
for bl_sockets in [node.inputs, node.outputs]:
|
for bl_sockets in [node.inputs, node.outputs]:
|
||||||
for bl_socket in bl_sockets:
|
for bl_socket in bl_sockets:
|
||||||
|
log.debug(
|
||||||
|
'|-> %s: Regenerating Dynamic Field Persistance for Socket',
|
||||||
|
str(bl_socket),
|
||||||
|
)
|
||||||
bl_socket.regenerate_dynamic_field_persistance()
|
bl_socket.regenerate_dynamic_field_persistance()
|
||||||
|
log.debug('Regenerated All Dynamic Field Persistance')
|
||||||
|
|
||||||
|
|
||||||
####################
|
####################
|
||||||
|
|
|
@ -737,6 +737,14 @@ class MaxwellSimNode(bpy.types.Node, bl_instance.BLInstance):
|
||||||
socket_name: The input socket that was altered, if any, in order to trigger this event.
|
socket_name: The input socket that was altered, if any, in order to trigger this event.
|
||||||
pop_name: The property that was altered, if any, in order to trigger this event.
|
pop_name: The property that was altered, if any, in order to trigger this event.
|
||||||
"""
|
"""
|
||||||
|
# log.debug(
|
||||||
|
# '%s: Triggered Event %s (socket_name=%s, socket_kinds=%s, prop_name=%s)',
|
||||||
|
# self.sim_node_name,
|
||||||
|
# event,
|
||||||
|
# str(socket_name),
|
||||||
|
# str(socket_kinds),
|
||||||
|
# str(prop_name),
|
||||||
|
# )
|
||||||
# Outflow Socket Kinds
|
# Outflow Socket Kinds
|
||||||
## -> Something has happened!
|
## -> Something has happened!
|
||||||
## -> The effect is yet to be determined...
|
## -> The effect is yet to be determined...
|
||||||
|
@ -815,9 +823,9 @@ class MaxwellSimNode(bpy.types.Node, bl_instance.BLInstance):
|
||||||
for event_method in triggered_event_methods:
|
for event_method in triggered_event_methods:
|
||||||
stop_propagation |= event_method.stop_propagation
|
stop_propagation |= event_method.stop_propagation
|
||||||
# log.critical(
|
# log.critical(
|
||||||
# '$[%s] [%s %s %s %s] Running: (%s)',
|
# '%s: Running %s',
|
||||||
# self.sim_node_name,
|
# self.sim_node_name,
|
||||||
# event_method.callback_info,
|
# str(event_method.callback_info),
|
||||||
# )
|
# )
|
||||||
event_method(self)
|
event_method(self)
|
||||||
|
|
||||||
|
|
|
@ -43,8 +43,6 @@ class GeoNodesStructureNode(base.MaxwellSimNode):
|
||||||
'Medium': sockets.MaxwellMediumSocketDef(),
|
'Medium': sockets.MaxwellMediumSocketDef(),
|
||||||
'Center': sockets.ExprSocketDef(
|
'Center': sockets.ExprSocketDef(
|
||||||
size=spux.NumberSize1D.Vec3,
|
size=spux.NumberSize1D.Vec3,
|
||||||
mathtype=spux.MathType.Real,
|
|
||||||
physical_type=spux.PhysicalType.Length,
|
|
||||||
default_unit=spu.micrometer,
|
default_unit=spu.micrometer,
|
||||||
default_value=sp.Matrix([0, 0, 0]),
|
default_value=sp.Matrix([0, 0, 0]),
|
||||||
),
|
),
|
||||||
|
@ -54,7 +52,6 @@ class GeoNodesStructureNode(base.MaxwellSimNode):
|
||||||
}
|
}
|
||||||
|
|
||||||
managed_obj_types: typ.ClassVar = {
|
managed_obj_types: typ.ClassVar = {
|
||||||
'mesh': managed_objs.ManagedBLMesh,
|
|
||||||
'modifier': managed_objs.ManagedBLModifier,
|
'modifier': managed_objs.ManagedBLModifier,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -64,7 +61,7 @@ class GeoNodesStructureNode(base.MaxwellSimNode):
|
||||||
@events.computes_output_socket(
|
@events.computes_output_socket(
|
||||||
'Structure',
|
'Structure',
|
||||||
input_sockets={'Medium'},
|
input_sockets={'Medium'},
|
||||||
managed_objs={'mesh'},
|
managed_objs={'modifier'},
|
||||||
)
|
)
|
||||||
def compute_structure(
|
def compute_structure(
|
||||||
self,
|
self,
|
||||||
|
@ -74,7 +71,7 @@ class GeoNodesStructureNode(base.MaxwellSimNode):
|
||||||
"""Computes a triangle-mesh based Tidy3D structure, by manually copying mesh data from Blender to a `td.TriangleMesh`."""
|
"""Computes a triangle-mesh based Tidy3D structure, by manually copying mesh data from Blender to a `td.TriangleMesh`."""
|
||||||
## TODO: mesh_as_arrays might not take the Center into account.
|
## TODO: mesh_as_arrays might not take the Center into account.
|
||||||
## - Alternatively, Tidy3D might have a way to transform?
|
## - Alternatively, Tidy3D might have a way to transform?
|
||||||
mesh_as_arrays = managed_objs['mesh'].mesh_as_arrays
|
mesh_as_arrays = managed_objs['modifier'].mesh_as_arrays
|
||||||
return td.Structure(
|
return td.Structure(
|
||||||
geometry=td.TriangleMesh.from_vertices_faces(
|
geometry=td.TriangleMesh.from_vertices_faces(
|
||||||
mesh_as_arrays['verts'],
|
mesh_as_arrays['verts'],
|
||||||
|
@ -84,71 +81,28 @@ class GeoNodesStructureNode(base.MaxwellSimNode):
|
||||||
)
|
)
|
||||||
|
|
||||||
####################
|
####################
|
||||||
# - Events: Preview Active Changed
|
# - UI
|
||||||
####################
|
####################
|
||||||
@events.on_value_changed(
|
def draw_label(self) -> None:
|
||||||
prop_name='preview_active',
|
"""Show the extracted data (if any) in the node's header label.
|
||||||
props={'preview_active'},
|
|
||||||
managed_objs={'mesh'},
|
|
||||||
)
|
|
||||||
def on_preview_changed(self, props) -> None:
|
|
||||||
"""Enables/disables previewing of the GeoNodes-driven mesh, regardless of whether a particular GeoNodes tree is chosen."""
|
|
||||||
mesh = managed_objs['mesh']
|
|
||||||
|
|
||||||
# Push Preview State to Managed Mesh
|
Notes:
|
||||||
if props['preview_active']:
|
Called by Blender to determine the text to place in the node's header.
|
||||||
mesh.show_preview()
|
|
||||||
else:
|
|
||||||
mesh.hide_preview()
|
|
||||||
|
|
||||||
####################
|
|
||||||
# - Events: GN Input Changed
|
|
||||||
####################
|
|
||||||
@events.on_value_changed(
|
|
||||||
socket_name={'Center'},
|
|
||||||
any_loose_input_socket=True,
|
|
||||||
# Pass Data
|
|
||||||
managed_objs={'mesh', 'modifier'},
|
|
||||||
input_sockets={'Center', 'GeoNodes'},
|
|
||||||
all_loose_input_sockets=True,
|
|
||||||
unit_systems={'BlenderUnits': ct.UNITS_BLENDER},
|
|
||||||
scale_input_sockets={'Center': 'BlenderUnits'},
|
|
||||||
)
|
|
||||||
def on_input_socket_changed(
|
|
||||||
self, input_sockets, loose_input_sockets, unit_systems
|
|
||||||
) -> None:
|
|
||||||
"""Pushes any change in GeoNodes-bound input sockets to the GeoNodes modifier.
|
|
||||||
|
|
||||||
Also pushes the `Center:Value` socket to govern the object's center in 3D space.
|
|
||||||
"""
|
"""
|
||||||
geonodes = input_sockets['GeoNodes']
|
geonodes = self._compute_input('GeoNodes')
|
||||||
has_geonodes = not ct.FlowSignal.check(geonodes)
|
if geonodes is None:
|
||||||
|
return self.bl_label
|
||||||
|
|
||||||
if has_geonodes:
|
return f'Structure: {self.sim_node_name}'
|
||||||
mesh = managed_objs['mesh']
|
|
||||||
modifier = managed_objs['modifier']
|
|
||||||
center = input_sockets['Center']
|
|
||||||
unit_system = unit_systems['BlenderUnits']
|
|
||||||
|
|
||||||
# Push Loose Input Values to GeoNodes Modifier
|
|
||||||
modifier.bl_modifier(
|
|
||||||
mesh.bl_object(location=center),
|
|
||||||
'NODES',
|
|
||||||
{
|
|
||||||
'node_group': geonodes,
|
|
||||||
'inputs': loose_input_sockets,
|
|
||||||
'unit_system': unit_system,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
####################
|
####################
|
||||||
# - Events: GN Tree Changed
|
# - Events: Swapped GN Node Tree
|
||||||
####################
|
####################
|
||||||
@events.on_value_changed(
|
@events.on_value_changed(
|
||||||
socket_name={'GeoNodes'},
|
socket_name={'GeoNodes'},
|
||||||
# Pass Data
|
# Loaded
|
||||||
managed_objs={'mesh', 'modifier'},
|
managed_objs={'modifier'},
|
||||||
input_sockets={'GeoNodes', 'Center'},
|
input_sockets={'GeoNodes'},
|
||||||
)
|
)
|
||||||
def on_input_changed(
|
def on_input_changed(
|
||||||
self,
|
self,
|
||||||
|
@ -157,12 +111,9 @@ class GeoNodesStructureNode(base.MaxwellSimNode):
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Declares new loose input sockets in response to a new GeoNodes tree (if any)."""
|
"""Declares new loose input sockets in response to a new GeoNodes tree (if any)."""
|
||||||
geonodes = input_sockets['GeoNodes']
|
geonodes = input_sockets['GeoNodes']
|
||||||
has_geonodes = not ct.FlowSignal.check(geonodes)
|
has_geonodes = not ct.FlowSignal.check(geonodes) and geonodes is not None
|
||||||
|
|
||||||
if has_geonodes:
|
if has_geonodes:
|
||||||
mesh = managed_objs['mesh']
|
|
||||||
modifier = managed_objs['modifier']
|
|
||||||
|
|
||||||
# Fill the Loose Input Sockets
|
# Fill the Loose Input Sockets
|
||||||
## -> The SocketDefs contain the default values from the interface.
|
## -> The SocketDefs contain the default values from the interface.
|
||||||
log.info(
|
log.info(
|
||||||
|
@ -176,9 +127,59 @@ class GeoNodesStructureNode(base.MaxwellSimNode):
|
||||||
|
|
||||||
elif self.loose_input_sockets:
|
elif self.loose_input_sockets:
|
||||||
self.loose_input_sockets = {}
|
self.loose_input_sockets = {}
|
||||||
|
managed_objs['modifier'].free()
|
||||||
|
|
||||||
if modifier.name in mesh.bl_object().modifiers.keys().copy():
|
####################
|
||||||
modifier.free_from_bl_object(mesh.bl_object())
|
# - Events: Preview
|
||||||
|
####################
|
||||||
|
@events.on_value_changed(
|
||||||
|
# Trigger
|
||||||
|
prop_name='preview_active',
|
||||||
|
# Loaded
|
||||||
|
managed_objs={'modifier'},
|
||||||
|
props={'preview_active'},
|
||||||
|
)
|
||||||
|
def on_preview_changed(self, managed_objs, props):
|
||||||
|
if props['preview_active']:
|
||||||
|
managed_objs['modifier'].show_preview()
|
||||||
|
else:
|
||||||
|
managed_objs['modifier'].hide_preview()
|
||||||
|
|
||||||
|
@events.on_value_changed(
|
||||||
|
# Trigger
|
||||||
|
socket_name={'Center', 'GeoNodes'}, ## MUST run after on_input_changed
|
||||||
|
any_loose_input_socket=True,
|
||||||
|
# Loaded
|
||||||
|
managed_objs={'modifier'},
|
||||||
|
input_sockets={'Center', 'GeoNodes'},
|
||||||
|
all_loose_input_sockets=True,
|
||||||
|
unit_systems={'BlenderUnits': ct.UNITS_BLENDER},
|
||||||
|
scale_input_sockets={'Center': 'BlenderUnits'},
|
||||||
|
)
|
||||||
|
def on_input_socket_changed(
|
||||||
|
self, managed_objs, input_sockets, loose_input_sockets, unit_systems
|
||||||
|
) -> None:
|
||||||
|
"""Pushes any change in GeoNodes-bound input sockets to the GeoNodes modifier.
|
||||||
|
|
||||||
|
Warnings:
|
||||||
|
MUST be placed lower than `on_input_changed`, so it runs afterwards whenever the `GeoNodes` tree is changed.
|
||||||
|
|
||||||
|
Also pushes the `Center:Value` socket to govern the object's center in 3D space.
|
||||||
|
"""
|
||||||
|
geonodes = input_sockets['GeoNodes']
|
||||||
|
has_geonodes = not ct.FlowSignal.check(geonodes) and geonodes is not None
|
||||||
|
|
||||||
|
if has_geonodes:
|
||||||
|
# Push Loose Input Values to GeoNodes Modifier
|
||||||
|
managed_objs['modifier'].bl_modifier(
|
||||||
|
'NODES',
|
||||||
|
{
|
||||||
|
'node_group': geonodes,
|
||||||
|
'inputs': loose_input_sockets,
|
||||||
|
'unit_system': unit_systems['BlenderUnits'],
|
||||||
|
},
|
||||||
|
location=input_sockets['Center'],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
####################
|
####################
|
||||||
|
|
|
@ -83,7 +83,7 @@ class BoxStructureNode(base.MaxwellSimNode):
|
||||||
)
|
)
|
||||||
|
|
||||||
####################
|
####################
|
||||||
# - Preview
|
# - Events: Preview
|
||||||
####################
|
####################
|
||||||
@events.on_value_changed(
|
@events.on_value_changed(
|
||||||
# Trigger
|
# Trigger
|
||||||
|
|
|
@ -14,30 +14,40 @@
|
||||||
# You should have received a copy of the GNU Affero General Public License
|
# You should have received a copy of the GNU Affero General Public License
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
import typing as typ
|
||||||
|
|
||||||
import bpy
|
import bpy
|
||||||
|
import sympy as sp
|
||||||
|
import sympy.physics.units as spu
|
||||||
|
import tidy3d as td
|
||||||
|
|
||||||
|
from blender_maxwell.assets.geonodes import GeoNodes, import_geonodes
|
||||||
|
from blender_maxwell.utils import bl_cache, logger
|
||||||
|
from blender_maxwell.utils import extra_sympy_units as spux
|
||||||
|
|
||||||
from ... import contracts as ct
|
from ... import contracts as ct
|
||||||
from .. import base
|
from .. import base
|
||||||
|
|
||||||
|
log = logger.get(__name__)
|
||||||
|
|
||||||
|
|
||||||
####################
|
####################
|
||||||
# - Operators
|
# - Operators
|
||||||
####################
|
####################
|
||||||
class BlenderMaxwellResetGeoNodesSocket(bpy.types.Operator):
|
class BlenderMaxwellResetGeoNodesSocket(bpy.types.Operator):
|
||||||
bl_idname = 'blender_maxwell.reset_geo_nodes_socket'
|
"""Simulate a change to the geometry nodes group of the attached `GeoNodes` socket.
|
||||||
bl_label = 'Reset GeoNodes Socket'
|
|
||||||
|
|
||||||
node_tree_name: bpy.props.StringProperty(name='Node Tree Name')
|
This causes updates to the GN group (ex. internal logic) to be immediately caught.
|
||||||
node_name: bpy.props.StringProperty(name='Node Name')
|
"""
|
||||||
socket_name: bpy.props.StringProperty(name='Socket Name')
|
|
||||||
|
bl_idname = ct.OperatorType.SocketGeoNodesReset
|
||||||
|
bl_label = 'Reset GeoNodes Group'
|
||||||
|
|
||||||
def execute(self, context):
|
def execute(self, context):
|
||||||
node_tree = bpy.data.node_groups[self.node_tree_name]
|
bl_socket = context.socket
|
||||||
node = node_tree.nodes[self.node_name]
|
|
||||||
socket = node.inputs[self.socket_name]
|
|
||||||
|
|
||||||
# Report as though the GeoNodes Tree Changed
|
# Report as though the GeoNodes Tree Changed
|
||||||
socket.on_prop_changed('raw_value', context)
|
bl_socket.on_prop_changed('raw_value', context)
|
||||||
|
|
||||||
return {'FINISHED'}
|
return {'FINISHED'}
|
||||||
|
|
||||||
|
@ -52,35 +62,18 @@ class BlenderGeoNodesBLSocket(base.MaxwellSimSocket):
|
||||||
####################
|
####################
|
||||||
# - Properties
|
# - Properties
|
||||||
####################
|
####################
|
||||||
raw_value: bpy.props.PointerProperty(
|
raw_value: bpy.types.NodeTree = bl_cache.BLField(
|
||||||
name='Blender GeoNodes Tree',
|
bltype_poll=lambda self, obj: self.filter_gn_trees(obj)
|
||||||
description='Represents a Blender GeoNodes Tree',
|
|
||||||
type=bpy.types.NodeTree,
|
|
||||||
poll=(lambda self, obj: obj.bl_idname == 'GeometryNodeTree'),
|
|
||||||
update=(lambda self, context: self.on_prop_changed('raw_value', context)),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
####################
|
def filter_gn_trees(self, obj: ct.BLIDStruct) -> bool:
|
||||||
# - UI
|
return obj.bl_idname == 'GeometryNodeTree' and not obj.name.startswith('_')
|
||||||
####################
|
|
||||||
# def draw_label_row(self, label_col_row, text):
|
|
||||||
# label_col_row.label(text=text)
|
|
||||||
# if not self.raw_value: return
|
|
||||||
#
|
|
||||||
# op = label_col_row.operator(
|
|
||||||
# BlenderMaxwellResetGeoNodesSocket.bl_idname,
|
|
||||||
# text="",
|
|
||||||
# icon="FILE_REFRESH",
|
|
||||||
# )
|
|
||||||
# op.socket_name = self.name
|
|
||||||
# op.node_name = self.node.name
|
|
||||||
# op.node_tree_name = self.node.id_data.name
|
|
||||||
|
|
||||||
####################
|
####################
|
||||||
# - UI
|
# - UI
|
||||||
####################
|
####################
|
||||||
def draw_value(self, col: bpy.types.UILayout) -> None:
|
def draw_value(self, col: bpy.types.UILayout) -> None:
|
||||||
col.prop(self, 'raw_value', text='')
|
col.prop(self, self.blfields['raw_value'], text='')
|
||||||
|
|
||||||
####################
|
####################
|
||||||
# - Default Value
|
# - Default Value
|
||||||
|
|
|
@ -36,6 +36,7 @@ StringPropSubType: typ.TypeAlias = typ.Literal[
|
||||||
'FILE_PATH', 'DIR_PATH', 'FILE_NAME', 'BYTE_STRING', 'PASSWORD', 'NONE'
|
'FILE_PATH', 'DIR_PATH', 'FILE_NAME', 'BYTE_STRING', 'PASSWORD', 'NONE'
|
||||||
]
|
]
|
||||||
|
|
||||||
|
PollMethod: typ.TypeAlias = typ.Callable[[bl_instance.BLInstance, typ.Any], bool]
|
||||||
StrMethod: typ.TypeAlias = typ.Callable[
|
StrMethod: typ.TypeAlias = typ.Callable[
|
||||||
[bl_instance.BLInstance, bpy.types.Context, str], list[tuple[str, str]]
|
[bl_instance.BLInstance, bpy.types.Context, str], list[tuple[str, str]]
|
||||||
]
|
]
|
||||||
|
@ -72,8 +73,7 @@ class BLField:
|
||||||
float_prec: int | None = None,
|
float_prec: int | None = None,
|
||||||
str_secret: bool | None = None,
|
str_secret: bool | None = None,
|
||||||
path_type: typ.Literal['dir', 'file'] | None = None,
|
path_type: typ.Literal['dir', 'file'] | None = None,
|
||||||
# blptr_type: typ.Any | None = None, ## A Blender ID type
|
bltype_poll: PollMethod | None = None,
|
||||||
## TODO: Test/Implement
|
|
||||||
## Dynamic
|
## Dynamic
|
||||||
str_cb: StrMethod | None = None,
|
str_cb: StrMethod | None = None,
|
||||||
enum_cb: EnumMethod | None = None,
|
enum_cb: EnumMethod | None = None,
|
||||||
|
@ -110,6 +110,10 @@ class BLField:
|
||||||
Only meaningful for `pathlib.Path` properties.
|
Only meaningful for `pathlib.Path` properties.
|
||||||
**NOTE**: No effort is made to make paths portable between operating systems.
|
**NOTE**: No effort is made to make paths portable between operating systems.
|
||||||
Use with care.
|
Use with care.
|
||||||
|
bltype_poll: User-provided method for filtering selectable datablocks.
|
||||||
|
Whenever the annotated type is `ct.BLIDStruct`, it may be desirable to constrain which datablocks the user should be able to select.
|
||||||
|
This allows doing so.
|
||||||
|
**NOTE**: The method will be run very often, and must therefore be cheap.
|
||||||
str_cb: Method used to determine all valid strings, which presents to the user as a fuzzy-style search dropdown.
|
str_cb: Method used to determine all valid strings, which presents to the user as a fuzzy-style search dropdown.
|
||||||
Only meaningful for `str` properties.
|
Only meaningful for `str` properties.
|
||||||
Results are not persisted, and must therefore re-run when reloading the file.
|
Results are not persisted, and must therefore re-run when reloading the file.
|
||||||
|
@ -121,12 +125,6 @@ class BLField:
|
||||||
cb_depends_on: Declares that `str_cb` / `enum_cb` should be regenerated whenever any of the given property names change.
|
cb_depends_on: Declares that `str_cb` / `enum_cb` should be regenerated whenever any of the given property names change.
|
||||||
This allows fully automating the invocation of `Signal.ResetEnumItems` / `Signal.ResetStrSearch` in common cases.
|
This allows fully automating the invocation of `Signal.ResetEnumItems` / `Signal.ResetStrSearch` in common cases.
|
||||||
"""
|
"""
|
||||||
log.debug(
|
|
||||||
'Initializing BLField (default_value=%s, use_prop_update=%s)',
|
|
||||||
str(default_value),
|
|
||||||
str(use_prop_update),
|
|
||||||
)
|
|
||||||
|
|
||||||
self.use_dynamic_enum = enum_cb is not None
|
self.use_dynamic_enum = enum_cb is not None
|
||||||
self.use_str_search = str_cb is not None
|
self.use_str_search = str_cb is not None
|
||||||
|
|
||||||
|
@ -143,7 +141,7 @@ class BLField:
|
||||||
'step': float_step,
|
'step': float_step,
|
||||||
'precision': float_prec,
|
'precision': float_prec,
|
||||||
# BLPointer: ID Type
|
# BLPointer: ID Type
|
||||||
#'blptr_type': blptr_type,
|
'bltype_poll': bltype_poll,
|
||||||
# Str | Path | Enum: Flag Setters
|
# Str | Path | Enum: Flag Setters
|
||||||
'str_secret': str_secret,
|
'str_secret': str_secret,
|
||||||
'path_type': path_type,
|
'path_type': path_type,
|
||||||
|
@ -222,6 +220,14 @@ class BLField:
|
||||||
)
|
)
|
||||||
self.bl_prop_str_search.init_bl_type(owner)
|
self.bl_prop_str_search.init_bl_type(owner)
|
||||||
|
|
||||||
|
log.debug(
|
||||||
|
'Initialized BLField "%s" (prop_type=%s, type=%s, default_value=%s)',
|
||||||
|
str(self.bl_prop.name),
|
||||||
|
str(self.bl_prop.bl_prop_type),
|
||||||
|
str(self.bl_prop.prop_type),
|
||||||
|
str(self.bl_prop.default_value),
|
||||||
|
)
|
||||||
|
|
||||||
def __get__(
|
def __get__(
|
||||||
self,
|
self,
|
||||||
bl_instance: bl_instance.BLInstance | None,
|
bl_instance: bl_instance.BLInstance | None,
|
||||||
|
@ -290,6 +296,9 @@ class BLField:
|
||||||
|
|
||||||
# Reset Enum Items
|
# Reset Enum Items
|
||||||
elif value is Signal.ResetEnumItems:
|
elif value is Signal.ResetEnumItems:
|
||||||
|
if self.bl_prop_enum_items is None:
|
||||||
|
return
|
||||||
|
|
||||||
# Retrieve Old Items
|
# Retrieve Old Items
|
||||||
## -> This is verbatim what is being persisted, currently.
|
## -> This is verbatim what is being persisted, currently.
|
||||||
## -> len(0): Manually replaced w/fallback to guarantee >=len(1)
|
## -> len(0): Manually replaced w/fallback to guarantee >=len(1)
|
||||||
|
@ -356,6 +365,9 @@ class BLField:
|
||||||
|
|
||||||
# Reset Str Search
|
# Reset Str Search
|
||||||
elif value is Signal.ResetStrSearch:
|
elif value is Signal.ResetStrSearch:
|
||||||
|
if self.bl_prop_str_search is None:
|
||||||
|
return
|
||||||
|
|
||||||
self.bl_prop_str_search.invalidate_nonpersist(bl_instance)
|
self.bl_prop_str_search.invalidate_nonpersist(bl_instance)
|
||||||
|
|
||||||
# General __set__
|
# General __set__
|
||||||
|
|
|
@ -173,6 +173,10 @@ class BLProp:
|
||||||
def read_nonpersist(self, bl_instance: bl_instance.BLInstance | None) -> typ.Any:
|
def read_nonpersist(self, bl_instance: bl_instance.BLInstance | None) -> typ.Any:
|
||||||
"""Read the non-persistent cache value for this property.
|
"""Read the non-persistent cache value for this property.
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
**Never reads cached BLPointers**; invokes `self.read()` instead.
|
||||||
|
`BLPointer`s must align perfectly with Blender's internal logic, and as such, the cache cannot get involved, else we risk access-after-free crashes.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Generally, the cache value, with two exceptions.
|
Generally, the cache value, with two exceptions.
|
||||||
|
|
||||||
|
@ -184,6 +188,8 @@ class BLProp:
|
||||||
- `Signal.CacheEmpty`: When the cache has no entry.
|
- `Signal.CacheEmpty`: When the cache has no entry.
|
||||||
A good idea might be to fill it immediately with `self.write_nonpersist(bl_instance)`.
|
A good idea might be to fill it immediately with `self.write_nonpersist(bl_instance)`.
|
||||||
"""
|
"""
|
||||||
|
if self.bl_prop_type is BLPropType.BLPointer:
|
||||||
|
return self.read(bl_instance)
|
||||||
return managed_cache.read(
|
return managed_cache.read(
|
||||||
bl_instance,
|
bl_instance,
|
||||||
self.bl_name,
|
self.bl_name,
|
||||||
|
@ -215,11 +221,17 @@ class BLProp:
|
||||||
use_nonpersist=False,
|
use_nonpersist=False,
|
||||||
use_persist=True,
|
use_persist=True,
|
||||||
)
|
)
|
||||||
|
if self.bl_prop_type is BLPropType.BLPointer:
|
||||||
|
return
|
||||||
|
|
||||||
self.write_nonpersist(bl_instance, value)
|
self.write_nonpersist(bl_instance, value)
|
||||||
|
|
||||||
def write_nonpersist(
|
def write_nonpersist(
|
||||||
self, bl_instance: bl_instance.BLInstance, value: typ.Any
|
self, bl_instance: bl_instance.BLInstance, value: typ.Any
|
||||||
) -> None:
|
) -> None:
|
||||||
|
if self.bl_prop_type is BLPropType.BLPointer:
|
||||||
|
return ## We can't always write here, so we can only do nothing.
|
||||||
|
|
||||||
managed_cache.write(
|
managed_cache.write(
|
||||||
bl_instance,
|
bl_instance,
|
||||||
self.bl_name,
|
self.bl_name,
|
||||||
|
@ -229,6 +241,9 @@ class BLProp:
|
||||||
)
|
)
|
||||||
|
|
||||||
def invalidate_nonpersist(self, bl_instance: bl_instance.BLInstance | None) -> None:
|
def invalidate_nonpersist(self, bl_instance: bl_instance.BLInstance | None) -> None:
|
||||||
|
if self.bl_prop_type is BLPropType.BLPointer:
|
||||||
|
return
|
||||||
|
|
||||||
managed_cache.invalidate_nonpersist(
|
managed_cache.invalidate_nonpersist(
|
||||||
bl_instance,
|
bl_instance,
|
||||||
self.bl_name,
|
self.bl_name,
|
||||||
|
|
|
@ -82,6 +82,10 @@ def _is_strenum(T: type) -> bool: # noqa: N803
|
||||||
return inspect.isclass(T) and issubclass(T, enum.StrEnum)
|
return inspect.isclass(T) and issubclass(T, enum.StrEnum)
|
||||||
|
|
||||||
|
|
||||||
|
def _is_bl_id_struct(T: type) -> bool: # noqa: N803
|
||||||
|
return T in BLIDStructs
|
||||||
|
|
||||||
|
|
||||||
####################
|
####################
|
||||||
# - Blender Property Type
|
# - Blender Property Type
|
||||||
####################
|
####################
|
||||||
|
@ -323,7 +327,7 @@ class BLPropType(enum.StrEnum):
|
||||||
BPT.SingleDynEnum: ['enum_dynamic'],
|
BPT.SingleDynEnum: ['enum_dynamic'],
|
||||||
BPT.SetDynEnum: ['enum_dynamic'],
|
BPT.SetDynEnum: ['enum_dynamic'],
|
||||||
# Special
|
# Special
|
||||||
BPT.BLPointer: ['blptr_type'],
|
BPT.BLPointer: [],
|
||||||
BPT.Serialized: [],
|
BPT.Serialized: [],
|
||||||
}[self]
|
}[self]
|
||||||
|
|
||||||
|
@ -365,6 +369,12 @@ class BLPropType(enum.StrEnum):
|
||||||
|
|
||||||
# Define Information -> KWArg Getter
|
# Define Information -> KWArg Getter
|
||||||
def g_kwarg(name: str, force_key: str | None = None):
|
def g_kwarg(name: str, force_key: str | None = None):
|
||||||
|
"""Retrieve a dictionary mapping a name to its `prop_info[name]` value.
|
||||||
|
|
||||||
|
If `name` is not defined in `prop_info`, then return an empty dictionary.
|
||||||
|
|
||||||
|
If `force_key` is set, then use it as to override `name` as the key when returning the dictionary.
|
||||||
|
"""
|
||||||
key = force_key if force_key is not None else name
|
key = force_key if force_key is not None else name
|
||||||
return {key: prop_info[name]} if prop_info.get(name) is not None else {}
|
return {key: prop_info[name]} if prop_info.get(name) is not None else {}
|
||||||
|
|
||||||
|
@ -482,10 +492,13 @@ class BLPropType(enum.StrEnum):
|
||||||
|
|
||||||
# BLPointer
|
# BLPointer
|
||||||
case BPT.BLPointer:
|
case BPT.BLPointer:
|
||||||
kwargs |= encoded_default
|
|
||||||
|
|
||||||
# BLPointer: ID Type
|
# BLPointer: ID Type
|
||||||
kwargs |= g_kwarg('blptr_type', force_key='type')
|
## -> This is the type of datablock that will be selectable.
|
||||||
|
kwargs |= {'type': obj_type}
|
||||||
|
|
||||||
|
# BLPointer: Poll Method
|
||||||
|
## -> This allows the user filter selectable datablocks.
|
||||||
|
kwargs |= g_kwarg('bltype_poll', force_key='poll')
|
||||||
|
|
||||||
# BLPointer
|
# BLPointer
|
||||||
case BPT.Serialized:
|
case BPT.Serialized:
|
||||||
|
@ -573,7 +586,7 @@ class BLPropType(enum.StrEnum):
|
||||||
return {str(v) for v in value}
|
return {str(v) for v in value}
|
||||||
|
|
||||||
# BLPointer: Don't Alter
|
# BLPointer: Don't Alter
|
||||||
case BPT.BLPointer if value in BLIDStructs or value is None:
|
case BPT.BLPointer:
|
||||||
return value
|
return value
|
||||||
|
|
||||||
# Serialized: Serialize To UTF-8
|
# Serialized: Serialize To UTF-8
|
||||||
|
@ -662,7 +675,7 @@ class BLPropType(enum.StrEnum):
|
||||||
|
|
||||||
# BLPointer
|
# BLPointer
|
||||||
## -> None is always valid when it comes to BLPointers.
|
## -> None is always valid when it comes to BLPointers.
|
||||||
case BPT.BLPointer if raw_value in BLIDStructs or raw_value is None:
|
case BPT.BLPointer:
|
||||||
return raw_value
|
return raw_value
|
||||||
|
|
||||||
# Serialized: Deserialize the Argument
|
# Serialized: Deserialize the Argument
|
||||||
|
@ -748,7 +761,7 @@ class BLPropType(enum.StrEnum):
|
||||||
return BPT.SetEnum
|
return BPT.SetEnum
|
||||||
|
|
||||||
# Match BLPointers
|
# Match BLPointers
|
||||||
if obj_type in BLIDStructs:
|
if _is_bl_id_struct(obj_type):
|
||||||
return BPT.BLPointer
|
return BPT.BLPointer
|
||||||
|
|
||||||
# Fallback: Serializable Object
|
# Fallback: Serializable Object
|
||||||
|
|
|
@ -1285,6 +1285,7 @@ UnitSystem: typ.TypeAlias = dict[PhysicalType, Unit]
|
||||||
|
|
||||||
_PT = PhysicalType
|
_PT = PhysicalType
|
||||||
UNITS_SI: UnitSystem = {
|
UNITS_SI: UnitSystem = {
|
||||||
|
_PT.NonPhysical: None,
|
||||||
# Global
|
# Global
|
||||||
_PT.Time: spu.second,
|
_PT.Time: spu.second,
|
||||||
_PT.Angle: spu.radian,
|
_PT.Angle: spu.radian,
|
||||||
|
@ -1396,7 +1397,7 @@ def strip_unit_system(sp_obj: SympyExpr, unit_system: UnitSystem) -> SympyExpr:
|
||||||
Notes:
|
Notes:
|
||||||
You should probably use `scale_to_unit_system()` or `convert_to_unit_system()`.
|
You should probably use `scale_to_unit_system()` or `convert_to_unit_system()`.
|
||||||
"""
|
"""
|
||||||
return sp_obj.subs({unit: 1 for unit in unit_system.values()})
|
return sp_obj.subs({unit: 1 for unit in unit_system.values() if unit is not None})
|
||||||
|
|
||||||
|
|
||||||
def scale_to_unit_system(
|
def scale_to_unit_system(
|
||||||
|
|
Loading…
Reference in New Issue