HomeDashboard/.venv/lib/python3.12/site-packages/nicegui/elements/scene/scene_object3d.py
2026-01-03 14:54:18 +01:00

349 lines
12 KiB
Python

from __future__ import annotations
import math
import uuid
from typing import TYPE_CHECKING, Any, Literal
from typing_extensions import Self
if TYPE_CHECKING:
from .scene import Scene, SceneObject
class Object3D:
current_scene: Scene | None = None
def __init__(self, type_: str, *args: Any) -> None:
self.type = type_
self.id = str(uuid.uuid4())
self.name: str | None = None
assert self.current_scene is not None
self.scene: Scene = self.current_scene
self.scene.objects[self.id] = self
self.parent: Object3D | SceneObject = self.scene.stack[-1]
self.args: list = list(args)
self.color: str | None = '#ffffff'
self.opacity: float = 1.0
self.side_: str = 'front'
self.visible_: bool = True
self.draggable_: bool = False
self.x: float = 0
self.y: float = 0
self.z: float = 0
self.R: list[list[float]] = [[1, 0, 0], [0, 1, 0], [0, 0, 1]]
self.sx: float = 1
self.sy: float = 1
self.sz: float = 1
self._create()
def with_name(self, name: str) -> Self:
"""Set the name of the object."""
self.name = name
self._name()
return self
@property
def data(self) -> list[Any]:
"""Data to be sent to the frontend."""
return [
self.type, self.id, self.parent.id, self.args,
self.name,
self.color, self.opacity, self.side_,
self.x, self.y, self.z,
self.R,
self.sx, self.sy, self.sz,
self.visible_,
self.draggable_,
]
def __enter__(self) -> Self:
self.scene.stack.append(self)
return self
def __exit__(self, *_) -> None:
self.scene.stack.pop()
def _create(self) -> None:
self.scene.run_method('create', self.type, self.id, self.parent.id, *self.args)
def _name(self) -> None:
self.scene.run_method('name', self.id, self.name)
def _material(self) -> None:
self.scene.run_method('material', self.id, self.color, self.opacity, self.side_)
def _move(self) -> None:
self.scene.run_method('move', self.id, self.x, self.y, self.z)
def _rotate(self) -> None:
self.scene.run_method('rotate', self.id, self.R)
def _scale(self) -> None:
self.scene.run_method('scale', self.id, self.sx, self.sy, self.sz)
def _visible(self) -> None:
self.scene.run_method('visible', self.id, self.visible_)
def _draggable(self) -> None:
self.scene.run_method('draggable', self.id, self.draggable_)
def _delete(self) -> None:
self.scene.run_method('delete', self.id)
def material(self,
color: str | None = '#ffffff',
opacity: float = 1.0,
side: Literal['front', 'back', 'both'] = 'front',
) -> Self:
"""Set the color and opacity of the object.
:param color: CSS color string (default: '#ffffff')
:param opacity: opacity between 0.0 and 1.0 (default: 1.0)
:param side: 'front', 'back', or 'double' (default: 'front')
"""
if self.color != color or self.opacity != opacity or self.side_ != side:
self.color = color
self.opacity = opacity
self.side_ = side
self._material()
return self
def move(self, x: float = 0.0, y: float = 0.0, z: float = 0.0) -> Self:
"""Move the object.
:param x: x coordinate
:param y: y coordinate
:param z: z coordinate
"""
if self.x != x or self.y != y or self.z != z:
self.x = x
self.y = y
self.z = z
self._move()
return self
@staticmethod
def rotation_matrix_from_euler(r_x: float, r_y: float, r_z: float) -> list[list[float]]:
"""Create a rotation matrix from Euler angles.
:param r_x: rotation around the x axis in radians
:param r_y: rotation around the y axis in radians
:param r_z: rotation around the z axis in radians
"""
sx, cx = math.sin(r_x), math.cos(r_x)
sy, cy = math.sin(r_y), math.cos(r_y)
sz, cz = math.sin(r_z), math.cos(r_z)
return [
[cz * cy, -sz * cx + cz * sy * sx, sz * sx + cz * sy * cx],
[sz * cy, cz * cx + sz * sy * sx, -cz * sx + sz * sy * cx],
[-sy, cy * sx, cy * cx],
]
def rotate(self, r_x: float, r_y: float, r_z: float) -> Self:
"""Rotate the object.
:param r_x: rotation around the x axis in radians
:param r_y: rotation around the y axis in radians
:param r_z: rotation around the z axis in radians
"""
return self.rotate_R(self.rotation_matrix_from_euler(r_x, r_y, r_z))
def rotate_R(self, R: list[list[float]]) -> Self:
"""Rotate the object.
:param R: 3x3 rotation matrix
"""
if self.R != R:
self.R = R
self._rotate()
return self
def scale(self, sx: float = 1.0, sy: float | None = None, sz: float | None = None) -> Self:
"""Scale the object.
:param sx: scale factor for the x axis
:param sy: scale factor for the y axis (default: `sx`)
:param sz: scale factor for the z axis (default: `sx`)
"""
if sy is None:
sy = sx
if sz is None:
sz = sx
if self.sx != sx or self.sy != sy or self.sz != sz:
self.sx = sx
self.sy = sy
self.sz = sz
self._scale()
return self
def visible(self, value: bool = True) -> Self:
"""Set the visibility of the object.
:param value: whether the object should be visible (default: `True`)
"""
if self.visible_ != value:
self.visible_ = value
self._visible()
return self
def draggable(self, value: bool = True) -> Self:
"""Set whether the object should be draggable.
:param value: whether the object should be draggable (default: `True`)
"""
if self.draggable_ != value:
self.draggable_ = value
self._draggable()
return self
def attach(self, parent: Object3D) -> None:
"""Attach the object to a parent object.
The position and rotation of the object are preserved so that the object does not move in space.
But note that scaling is not preserved.
If either the parent or the object itself is scaled, the object shape and position can change.
*Added in version 2.7.0*
"""
self.detach()
self.parent = parent
self._move_into_parent(parent)
self.scene.run_method('attach', self.id, parent.id, self.x, self.y, self.z, self.R)
def _move_into_parent(self, parent: Object3D | SceneObject) -> None:
if not isinstance(parent, Object3D):
return
if isinstance(parent.parent, Object3D):
self._move_into_parent(parent.parent)
M1: list[list[float]] = [
[self.R[0][0], self.R[0][1], self.R[0][2], self.x],
[self.R[1][0], self.R[1][1], self.R[1][2], self.y],
[self.R[2][0], self.R[2][1], self.R[2][2], self.z],
[0, 0, 0, 1],
]
M2_inv: list[list[float]] = [
[parent.R[0][0], parent.R[1][0], parent.R[2][0],
- parent.R[0][0] * parent.x
- parent.R[1][0] * parent.y
- parent.R[2][0] * parent.z],
[parent.R[0][1], parent.R[1][1], parent.R[2][1],
- parent.R[0][1] * parent.x
- parent.R[1][1] * parent.y
- parent.R[2][1] * parent.z],
[parent.R[0][2], parent.R[1][2], parent.R[2][2],
- parent.R[0][2] * parent.x
- parent.R[1][2] * parent.y
- parent.R[2][2] * parent.z],
[0, 0, 0, 1],
]
M: list[list[float]] = [
[
M2_inv[0][0] * M1[0][0] + M2_inv[0][1] * M1[1][0] + M2_inv[0][2] * M1[2][0],
M2_inv[0][0] * M1[0][1] + M2_inv[0][1] * M1[1][1] + M2_inv[0][2] * M1[2][1],
M2_inv[0][0] * M1[0][2] + M2_inv[0][1] * M1[1][2] + M2_inv[0][2] * M1[2][2],
M2_inv[0][0] * M1[0][3] + M2_inv[0][1] * M1[1][3] + M2_inv[0][2] * M1[2][3] + M2_inv[0][3],
],
[
M2_inv[1][0] * M1[0][0] + M2_inv[1][1] * M1[1][0] + M2_inv[1][2] * M1[2][0],
M2_inv[1][0] * M1[0][1] + M2_inv[1][1] * M1[1][1] + M2_inv[1][2] * M1[2][1],
M2_inv[1][0] * M1[0][2] + M2_inv[1][1] * M1[1][2] + M2_inv[1][2] * M1[2][2],
M2_inv[1][0] * M1[0][3] + M2_inv[1][1] * M1[1][3] + M2_inv[1][2] * M1[2][3] + M2_inv[1][3],
],
[
M2_inv[2][0] * M1[0][0] + M2_inv[2][1] * M1[1][0] + M2_inv[2][2] * M1[2][0],
M2_inv[2][0] * M1[0][1] + M2_inv[2][1] * M1[1][1] + M2_inv[2][2] * M1[2][1],
M2_inv[2][0] * M1[0][2] + M2_inv[2][1] * M1[1][2] + M2_inv[2][2] * M1[2][2],
M2_inv[2][0] * M1[0][3] + M2_inv[2][1] * M1[1][3] + M2_inv[2][2] * M1[2][3] + M2_inv[2][3],
],
[
0, 0, 0, 1,
],
]
self.x = M[0][3]
self.y = M[1][3]
self.z = M[2][3]
self.R = [
[M[0][0], M[0][1], M[0][2]],
[M[1][0], M[1][1], M[1][2]],
[M[2][0], M[2][1], M[2][2]],
]
def detach(self) -> None:
"""Remove the object from its parent group object.
The position and rotation of the object are preserved so that the object does not move in space.
But note that scaling is not preserved.
If either the parent or the object itself is scaled, the object shape and position can change.
*Added in version 2.7.0*
"""
self._move_out_of_parent(self.parent)
self.parent = self.scene.stack[0]
self.scene.run_method('detach', self.id, self.x, self.y, self.z, self.R)
def _move_out_of_parent(self, parent: Object3D | SceneObject) -> None:
if not isinstance(parent, Object3D):
return
M1: list[list[float]] = [
[self.R[0][0], self.R[0][1], self.R[0][2], self.x],
[self.R[1][0], self.R[1][1], self.R[1][2], self.y],
[self.R[2][0], self.R[2][1], self.R[2][2], self.z],
[0, 0, 0, 1],
]
M2: list[list[float]] = [
[parent.R[0][0], parent.R[0][1], parent.R[0][2], parent.x],
[parent.R[1][0], parent.R[1][1], parent.R[1][2], parent.y],
[parent.R[2][0], parent.R[2][1], parent.R[2][2], parent.z],
[0, 0, 0, 1],
]
M: list[list[float]] = [
[
M2[0][0] * M1[0][0] + M2[0][1] * M1[1][0] + M2[0][2] * M1[2][0],
M2[0][0] * M1[0][1] + M2[0][1] * M1[1][1] + M2[0][2] * M1[2][1],
M2[0][0] * M1[0][2] + M2[0][1] * M1[1][2] + M2[0][2] * M1[2][2],
M2[0][0] * M1[0][3] + M2[0][1] * M1[1][3] + M2[0][2] * M1[2][3] + M2[0][3],
],
[
M2[1][0] * M1[0][0] + M2[1][1] * M1[1][0] + M2[1][2] * M1[2][0],
M2[1][0] * M1[0][1] + M2[1][1] * M1[1][1] + M2[1][2] * M1[2][1],
M2[1][0] * M1[0][2] + M2[1][1] * M1[1][2] + M2[1][2] * M1[2][2],
M2[1][0] * M1[0][3] + M2[1][1] * M1[1][3] + M2[1][2] * M1[2][3] + M2[1][3],
],
[
M2[2][0] * M1[0][0] + M2[2][1] * M1[1][0] + M2[2][2] * M1[2][0],
M2[2][0] * M1[0][1] + M2[2][1] * M1[1][1] + M2[2][2] * M1[2][1],
M2[2][0] * M1[0][2] + M2[2][1] * M1[1][2] + M2[2][2] * M1[2][2],
M2[2][0] * M1[0][3] + M2[2][1] * M1[1][3] + M2[2][2] * M1[2][3] + M2[2][3],
],
[
0, 0, 0, 1,
],
]
self.x = M[0][3]
self.y = M[1][3]
self.z = M[2][3]
self.R = [
[M[0][0], M[0][1], M[0][2]],
[M[1][0], M[1][1], M[1][2]],
[M[2][0], M[2][1], M[2][2]],
]
if isinstance(parent.parent, Object3D):
self._move_out_of_parent(parent.parent)
@property
def children(self) -> list[Object3D]:
"""List of children of the object.
*Added in version 2.4.0*
"""
return [object for object in self.scene.objects.values() if object.parent == self]
def delete(self) -> None:
"""Delete the object."""
for child in self.children:
child.delete()
del self.scene.objects[self.id]
self._delete()