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

161 lines
4.0 KiB
Python

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
socket_color = (0.6, 0.6, 0.6, 1.0)
bl_label = "Complex Number"
compatible_types = {
complex: {},
sp.Expr: {
lambda v: v.is_complex,
lambda 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,
]