oscillode/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/number/complex_number_socket.py

159 lines
4.0 KiB
Python
Raw Normal View History

import typing as typ
import bpy
import sympy as sp
import pydantic as pyd
from .. import base
from ... import contracts
####################
# - Blender Socket
####################
class ComplexNumberBLSocket(base.BLSocket):
socket_type = contracts.SocketType.ComplexNumber
bl_label = "Complex Number"
compatible_types = {
complex: {},
sp.Expr: {
lambda self, v: v.is_complex,
lambda self, v: len(v.free_symbols) == 0,
},
}
####################
# - Properties
####################
raw_value: bpy.props.FloatVectorProperty(
name="Complex Number",
description="Represents a complex number (real, imaginary)",
size=2,
default=(0.0, 0.0),
subtype='NONE'
)
coord_sys: bpy.props.EnumProperty(
name="Coordinate System",
description="Choose between cartesian and polar form",
items=[
("CARTESIAN", "Cartesian", "Use Cartesian Coordinates", "EMPTY_AXIS", 0),
("POLAR", "Polar", "Use Polar Coordinates", "DRIVER_ROTATIONAL_DIFFERENCE", 1),
],
default="CARTESIAN",
update=lambda self, context: self._update_coord_sys(),
)
####################
# - Socket UI
####################
def draw_value(self, col: bpy.types.UILayout) -> None:
"""Draw the value of the complex number, including a toggle for
specifying the active coordinate system.
"""
col_row = col.row()
col_row.prop(self, "raw_value", text="")
col.prop(self, "coord_sys", text="")
def draw_preview(self, col_box: bpy.types.UILayout) -> None:
"""Draw a live-preview value for the complex number, into the
given preview box.
- Cartesian: a,b -> a + ib
- Polar: r,t -> re^(it)
Returns:
The sympy expression representing the complex number.
"""
if self.coord_sys == "CARTESIAN":
text = f"= {self.default_value.n(2)}"
elif self.coord_sys == "POLAR":
r = sp.Abs(self.default_value).n(2)
theta_rad = sp.arg(self.default_value).n(2)
text = f"= {r*sp.exp(sp.I*theta_rad)}"
else:
raise RuntimeError("Invalid coordinate system for complex number")
col_box.label(text=text)
####################
# - Computation of Default Value
####################
@property
def default_value(self) -> sp.Expr:
"""Return the complex number as a sympy expression, of a form
determined by the coordinate system.
- Cartesian: a,b -> a + ib
- Polar: r,t -> re^(it)
Returns:
The sympy expression representing the complex number.
"""
v1, v2 = self.raw_value
return {
"CARTESIAN": v1 + sp.I*v2,
"POLAR": v1 * sp.exp(sp.I*v2),
}[self.coord_sys]
@default_value.setter
def default_value(self, value: typ.Any) -> None:
"""Set the complex number from a sympy expression, using an internal
representation determined by the coordinate system.
- Cartesian: a,b -> a + ib
- Polar: r,t -> re^(it)
"""
# (Guard) Value Compatibility
if not self.is_compatible(value):
msg = f"Tried setting socket ({self}) to incompatible value ({value}) of type {type(value)}"
raise ValueError(msg)
self.raw_value = {
"CARTESIAN": (sp.re(value), sp.im(value)),
"POLAR": (sp.Abs(value), sp.arg(value)),
}[self.coord_sys]
####################
# - Internal Update Methods
####################
def _update_coord_sys(self):
if self.coord_sys == "CARTESIAN":
r, theta_rad = self.raw_value
self.raw_value = (
r * sp.cos(theta_rad),
r * sp.sin(theta_rad),
)
elif self.coord_sys == "POLAR":
x, y = self.raw_value
cart_value = x + sp.I*y
self.raw_value = (
sp.Abs(cart_value),
sp.arg(cart_value) if y != 0 else 0,
)
####################
# - Socket Configuration
####################
class ComplexNumberSocketDef(pyd.BaseModel):
socket_type: contracts.SocketType = contracts.SocketType.ComplexNumber
label: str
preview: bool = False
coord_sys: typ.Literal["CARTESIAN", "POLAR"] = "CARTESIAN"
def init(self, bl_socket: ComplexNumberBLSocket) -> None:
bl_socket.preview_active = self.preview
bl_socket.coord_sys = self.coord_sys
####################
# - Blender Registration
####################
BL_REGISTER = [
ComplexNumberBLSocket,
]