Source code for session_py.xform
import uuid
import math
from typing import Optional, TYPE_CHECKING
if TYPE_CHECKING:
from .point import Point
from .vector import Vector
[docs]
class Xform:
[docs]
def __init__(self, m=None):
self.guid = str(uuid.uuid4())
self.name = "my_xform"
if m is None:
self.m = [
1.0,
0.0,
0.0,
0.0,
0.0,
1.0,
0.0,
0.0,
0.0,
0.0,
1.0,
0.0,
0.0,
0.0,
0.0,
1.0,
]
else:
self.m = list(m)
[docs]
@staticmethod
def translation(x, y, z):
xform = Xform()
xform.m[12] = x
xform.m[13] = y
xform.m[14] = z
return xform
[docs]
@staticmethod
def scaling(x, y, z):
xform = Xform()
xform.m[0] = x
xform.m[5] = y
xform.m[10] = z
return xform
[docs]
@staticmethod
def rotation_x(angle_radians):
xform = Xform()
cos_angle = math.cos(angle_radians)
sin_angle = math.sin(angle_radians)
xform.m[5] = cos_angle
xform.m[6] = sin_angle
xform.m[9] = -sin_angle
xform.m[10] = cos_angle
return xform
[docs]
@staticmethod
def rotation_y(angle_radians):
xform = Xform()
cos_angle = math.cos(angle_radians)
sin_angle = math.sin(angle_radians)
xform.m[0] = cos_angle
xform.m[2] = -sin_angle
xform.m[8] = sin_angle
xform.m[10] = cos_angle
return xform
[docs]
@staticmethod
def rotation_z(angle_radians):
xform = Xform()
cos_angle = math.cos(angle_radians)
sin_angle = math.sin(angle_radians)
xform.m[0] = cos_angle
xform.m[1] = sin_angle
xform.m[4] = -sin_angle
xform.m[5] = cos_angle
return xform
[docs]
@staticmethod
def rotation(axis, angle_radians):
xform = Xform()
axis = axis.normalize()
cos_angle = math.cos(angle_radians)
sin_angle = math.sin(angle_radians)
one_minus_cos = 1.0 - cos_angle
xx = axis.x * axis.x
xy = axis.x * axis.y
xz = axis.x * axis.z
yy = axis.y * axis.y
yz = axis.y * axis.z
zz = axis.z * axis.z
xform.m[0] = cos_angle + xx * one_minus_cos
xform.m[1] = xy * one_minus_cos + axis.z * sin_angle
xform.m[2] = xz * one_minus_cos - axis.y * sin_angle
xform.m[4] = xy * one_minus_cos - axis.z * sin_angle
xform.m[5] = cos_angle + yy * one_minus_cos
xform.m[6] = yz * one_minus_cos + axis.x * sin_angle
xform.m[8] = xz * one_minus_cos + axis.y * sin_angle
xform.m[9] = yz * one_minus_cos - axis.x * sin_angle
xform.m[10] = cos_angle + zz * one_minus_cos
return xform
[docs]
@staticmethod
def change_basis(origin, x_axis, y_axis, z_axis):
xform = Xform()
x_axis = x_axis.normalize()
y_axis = y_axis.normalize()
z_axis = z_axis.normalize()
xform.m[0] = x_axis.x
xform.m[1] = x_axis.y
xform.m[2] = x_axis.z
xform.m[4] = y_axis.x
xform.m[5] = y_axis.y
xform.m[6] = y_axis.z
xform.m[8] = z_axis.x
xform.m[9] = z_axis.y
xform.m[10] = z_axis.z
xform.m[12] = origin.x
xform.m[13] = origin.y
xform.m[14] = origin.z
return xform
[docs]
@staticmethod
def change_basis_alt(
origin_1, x_axis_1, y_axis_1, z_axis_1, origin_0, x_axis_0, y_axis_0, z_axis_0
):
a = x_axis_1.dot(y_axis_1)
b = x_axis_1.dot(z_axis_1)
c = y_axis_1.dot(z_axis_1)
r = [
[
x_axis_1.dot(x_axis_1),
a,
b,
x_axis_1.dot(x_axis_0),
x_axis_1.dot(y_axis_0),
x_axis_1.dot(z_axis_0),
],
[
a,
y_axis_1.dot(y_axis_1),
c,
y_axis_1.dot(x_axis_0),
y_axis_1.dot(y_axis_0),
y_axis_1.dot(z_axis_0),
],
[
b,
c,
z_axis_1.dot(z_axis_1),
z_axis_1.dot(x_axis_0),
z_axis_1.dot(y_axis_0),
z_axis_1.dot(z_axis_0),
],
]
i0 = 0 if r[0][0] >= r[1][1] else 1
if r[2][2] > r[i0][i0]:
i0 = 2
i1 = (i0 + 1) % 3
i2 = (i1 + 1) % 3
if r[i0][i0] == 0.0:
return Xform.identity()
d = 1.0 / r[i0][i0]
for j in range(6):
r[i0][j] *= d
r[i0][i0] = 1.0
if r[i1][i0] != 0.0:
d = -r[i1][i0]
for j in range(6):
r[i1][j] += d * r[i0][j]
r[i1][i0] = 0.0
if r[i2][i0] != 0.0:
d = -r[i2][i0]
for j in range(6):
r[i2][j] += d * r[i0][j]
r[i2][i0] = 0.0
if abs(r[i1][i1]) < abs(r[i2][i2]):
i1, i2 = i2, i1
if r[i1][i1] == 0.0:
return Xform.identity()
d = 1.0 / r[i1][i1]
for j in range(6):
r[i1][j] *= d
r[i1][i1] = 1.0
if r[i0][i1] != 0.0:
d = -r[i0][i1]
for j in range(6):
r[i0][j] += d * r[i1][j]
r[i0][i1] = 0.0
if r[i2][i1] != 0.0:
d = -r[i2][i1]
for j in range(6):
r[i2][j] += d * r[i1][j]
r[i2][i1] = 0.0
if r[i2][i2] == 0.0:
return Xform.identity()
d = 1.0 / r[i2][i2]
for j in range(6):
r[i2][j] *= d
r[i2][i2] = 1.0
if r[i0][i2] != 0.0:
d = -r[i0][i2]
for j in range(6):
r[i0][j] += d * r[i2][j]
r[i0][i2] = 0.0
if r[i1][i2] != 0.0:
d = -r[i1][i2]
for j in range(6):
r[i1][j] += d * r[i2][j]
r[i1][i2] = 0.0
m_xform = Xform()
m_xform.m[0] = r[0][3]
m_xform.m[4] = r[0][4]
m_xform.m[8] = r[0][5]
m_xform.m[1] = r[1][3]
m_xform.m[5] = r[1][4]
m_xform.m[9] = r[1][5]
m_xform.m[2] = r[2][3]
m_xform.m[6] = r[2][4]
m_xform.m[10] = r[2][5]
t0 = Xform.translation(-origin_1.x, -origin_1.y, -origin_1.z)
t2 = Xform.translation(origin_0.x, origin_0.y, origin_0.z)
return t2 * (m_xform * t0)
[docs]
@staticmethod
def plane_to_plane(
origin_0, x_axis_0, y_axis_0, z_axis_0, origin_1, x_axis_1, y_axis_1, z_axis_1
):
x0 = x_axis_0.normalize()
y0 = y_axis_0.normalize()
z0 = z_axis_0.normalize()
x1 = x_axis_1.normalize()
y1 = y_axis_1.normalize()
z1 = z_axis_1.normalize()
t0 = Xform.translation(-origin_0.x, -origin_0.y, -origin_0.z)
f0 = Xform()
f0.m[0] = x0.x
f0.m[1] = x0.y
f0.m[2] = x0.z
f0.m[4] = y0.x
f0.m[5] = y0.y
f0.m[6] = y0.z
f0.m[8] = z0.x
f0.m[9] = z0.y
f0.m[10] = z0.z
f1 = Xform()
f1.m[0] = x1.x
f1.m[4] = x1.y
f1.m[8] = x1.z
f1.m[1] = y1.x
f1.m[5] = y1.y
f1.m[9] = y1.z
f1.m[2] = z1.x
f1.m[6] = z1.y
f1.m[10] = z1.z
r = f1 * f0
t1 = Xform.translation(origin_1.x, origin_1.y, origin_1.z)
return t1 * (r * t0)
[docs]
@staticmethod
def plane_to_xy(origin, x_axis, y_axis, z_axis):
x = x_axis.normalize()
y = y_axis.normalize()
z = z_axis.normalize()
t = Xform.translation(-origin.x, -origin.y, -origin.z)
f = Xform()
f.m[0] = x.x
f.m[1] = x.y
f.m[2] = x.z
f.m[4] = y.x
f.m[5] = y.y
f.m[6] = y.z
f.m[8] = z.x
f.m[9] = z.y
f.m[10] = z.z
return f * t
[docs]
@staticmethod
def xy_to_plane(origin, x_axis, y_axis, z_axis):
x = x_axis.normalize()
y = y_axis.normalize()
z = z_axis.normalize()
f = Xform()
f.m[0] = x.x
f.m[4] = y.x
f.m[8] = z.x
f.m[1] = x.y
f.m[5] = y.y
f.m[9] = z.y
f.m[2] = x.z
f.m[6] = y.z
f.m[10] = z.z
t = Xform.translation(origin.x, origin.y, origin.z)
return t * f
[docs]
@staticmethod
def scale_xyz(scale_x, scale_y, scale_z):
xform = Xform()
xform.m[0] = scale_x
xform.m[5] = scale_y
xform.m[10] = scale_z
return xform
[docs]
@staticmethod
def scale_uniform(origin, scale_value):
t0 = Xform.translation(-origin.x, -origin.y, -origin.z)
t1 = Xform.scaling(scale_value, scale_value, scale_value)
t2 = Xform.translation(origin.x, origin.y, origin.z)
return t2 * (t1 * t0)
[docs]
@staticmethod
def scale_non_uniform(origin, scale_x, scale_y, scale_z):
t0 = Xform.translation(-origin.x, -origin.y, -origin.z)
t1 = Xform.scale_xyz(scale_x, scale_y, scale_z)
t2 = Xform.translation(origin.x, origin.y, origin.z)
return t2 * (t1 * t0)
[docs]
@staticmethod
def axis_rotation(angle, axis):
c = math.cos(angle)
s = math.sin(angle)
ux = axis.x
uy = axis.y
uz = axis.z
t = 1.0 - c
xform = Xform()
xform.m[0] = t * ux * ux + c
xform.m[4] = t * ux * uy - uz * s
xform.m[8] = t * ux * uz + uy * s
xform.m[1] = t * ux * uy + uz * s
xform.m[5] = t * uy * uy + c
xform.m[9] = t * uy * uz - ux * s
xform.m[2] = t * ux * uz - uy * s
xform.m[6] = t * uy * uz + ux * s
xform.m[10] = t * uz * uz + c
return xform
[docs]
@staticmethod
def look_at_rh(eye, target, up):
from .vector import Vector
f = (target - eye).normalize()
s = f.cross(up.normalize()).normalize()
u = s.cross(f)
xform = Xform()
xform.m[0] = s.x
xform.m[4] = s.y
xform.m[8] = s.z
xform.m[1] = u.x
xform.m[5] = u.y
xform.m[9] = u.z
xform.m[2] = -f.x
xform.m[6] = -f.y
xform.m[10] = -f.z
eye_vec = Vector(eye.x, eye.y, eye.z)
xform.m[12] = -s.dot(eye_vec)
xform.m[13] = -u.dot(eye_vec)
xform.m[14] = f.dot(eye_vec)
return xform
[docs]
def inverse(self) -> Optional["Xform"]:
a00 = self.m[0]
a01 = self.m[4]
a02 = self.m[8]
a10 = self.m[1]
a11 = self.m[5]
a12 = self.m[9]
a20 = self.m[2]
a21 = self.m[6]
a22 = self.m[10]
det = (
a00 * (a11 * a22 - a12 * a21)
- a01 * (a10 * a22 - a12 * a20)
+ a02 * (a10 * a21 - a11 * a20)
)
if abs(det) < 1e-12:
return None
inv_det = 1.0 / det
m00 = (a11 * a22 - a12 * a21) * inv_det
m01 = (a02 * a21 - a01 * a22) * inv_det
m02 = (a01 * a12 - a02 * a11) * inv_det
m10 = (a12 * a20 - a10 * a22) * inv_det
m11 = (a00 * a22 - a02 * a20) * inv_det
m12 = (a02 * a10 - a00 * a12) * inv_det
m20 = (a10 * a21 - a11 * a20) * inv_det
m21 = (a01 * a20 - a00 * a21) * inv_det
m22 = (a00 * a11 - a01 * a10) * inv_det
tx = self.m[12]
ty = self.m[13]
tz = self.m[14]
itx = -(m00 * tx + m01 * ty + m02 * tz)
ity = -(m10 * tx + m11 * ty + m12 * tz)
itz = -(m20 * tx + m21 * ty + m22 * tz)
res = Xform()
res.guid = ""
res.name = ""
res.m[0] = m00
res.m[4] = m01
res.m[8] = m02
res.m[12] = itx
res.m[1] = m10
res.m[5] = m11
res.m[9] = m12
res.m[13] = ity
res.m[2] = m20
res.m[6] = m21
res.m[10] = m22
res.m[14] = itz
return res
[docs]
def is_identity(self):
identity = Xform.identity()
for i in range(16):
if abs(self.m[i] - identity.m[i]) > 1e-10:
return False
return True
[docs]
def transformed_point(self, point):
from .point import Point
x = point.x
y = point.y
z = point.z
w = self.m[3] * x + self.m[7] * y + self.m[11] * z + self.m[15]
w_inv = 1.0 / w if abs(w) > 1e-10 else 1.0
return Point(
(self.m[0] * x + self.m[4] * y + self.m[8] * z + self.m[12]) * w_inv,
(self.m[1] * x + self.m[5] * y + self.m[9] * z + self.m[13]) * w_inv,
(self.m[2] * x + self.m[6] * y + self.m[10] * z + self.m[14]) * w_inv,
)
[docs]
def transformed_vector(self, vector):
x = vector.x
y = vector.y
z = vector.z
return Vector(
self.m[0] * x + self.m[4] * y + self.m[8] * z,
self.m[1] * x + self.m[5] * y + self.m[9] * z,
self.m[2] * x + self.m[6] * y + self.m[10] * z,
)
[docs]
def transform_point(self, point):
x = point.x
y = point.y
z = point.z
w = self.m[3] * x + self.m[7] * y + self.m[11] * z + self.m[15]
w_inv = 1.0 / w if abs(w) > 1e-10 else 1.0
point.x = (self.m[0] * x + self.m[4] * y + self.m[8] * z + self.m[12]) * w_inv
point.y = (self.m[1] * x + self.m[5] * y + self.m[9] * z + self.m[13]) * w_inv
point.z = (self.m[2] * x + self.m[6] * y + self.m[10] * z + self.m[14]) * w_inv
[docs]
def transform_vector(self, vector):
x = vector.x
y = vector.y
z = vector.z
vector.x = self.m[0] * x + self.m[4] * y + self.m[8] * z
vector.y = self.m[1] * x + self.m[5] * y + self.m[9] * z
vector.z = self.m[2] * x + self.m[6] * y + self.m[10] * z
def __mul__(self, other):
result = Xform()
result.m = [0.0] * 16
for i in range(4):
for j in range(4):
sum_val = 0.0
for k in range(4):
sum_val += self.m[k * 4 + i] * other.m[j * 4 + k]
result.m[j * 4 + i] = sum_val
return result
def __imul__(self, other):
temp = self * other
self.m = temp.m
return self
def __getitem__(self, idx):
if isinstance(idx, tuple) and len(idx) == 2:
row, col = idx
if not (0 <= row < 4 and 0 <= col < 4):
raise IndexError(f"Index out of bounds: ({row}, {col})")
return self.m[col * 4 + row]
raise TypeError("Index must be a tuple of (row, col)")
def __setitem__(self, idx, value):
if isinstance(idx, tuple) and len(idx) == 2:
row, col = idx
if not (0 <= row < 4 and 0 <= col < 4):
raise IndexError(f"Index out of bounds: ({row}, {col})")
self.m[col * 4 + row] = value
else:
raise TypeError("Index must be a tuple of (row, col)")
###########################################################################################
# Polymorphic JSON Serialization
###########################################################################################
def __jsondump__(self):
"""Serialize to polymorphic JSON format with type field.
Returns
-------
dict
Dictionary with 'type', 'guid', 'name', and object fields.
"""
return {
"type": f"{self.__class__.__name__}",
"guid": self.guid,
"name": self.name,
"m": self.m,
}
@classmethod
def __jsonload__(cls, data, guid=None, name=None):
"""Deserialize from polymorphic JSON format.
Parameters
----------
data : dict
Dictionary containing xform data.
guid : str, optional
GUID for the xform.
name : str, optional
Name for the xform.
Returns
-------
:class:`Xform`
Reconstructed xform instance.
"""
xform = cls.from_matrix(data["m"])
xform.guid = guid
xform.name = name
return xform