scene_object3d.py 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197
  1. from __future__ import annotations
  2. import math
  3. import uuid
  4. from typing import TYPE_CHECKING, Any, List, Literal, Optional, Union
  5. from typing_extensions import Self
  6. if TYPE_CHECKING:
  7. from .scene import Scene, SceneObject
  8. class Object3D:
  9. current_scene: Optional[Scene] = None
  10. def __init__(self, type_: str, *args: Any) -> None:
  11. self.type = type_
  12. self.id = str(uuid.uuid4())
  13. self.name: Optional[str] = None
  14. assert self.current_scene is not None
  15. self.scene: Scene = self.current_scene
  16. self.scene.objects[self.id] = self
  17. self.parent: Union[Object3D, SceneObject] = self.scene.stack[-1]
  18. self.args: List = list(args)
  19. self.color: str = '#ffffff'
  20. self.opacity: float = 1.0
  21. self.side_: str = 'front'
  22. self.visible_: bool = True
  23. self.draggable_: bool = False
  24. self.x: float = 0
  25. self.y: float = 0
  26. self.z: float = 0
  27. self.R: List[List[float]] = [[1, 0, 0], [0, 1, 0], [0, 0, 1]]
  28. self.sx: float = 1
  29. self.sy: float = 1
  30. self.sz: float = 1
  31. self._create()
  32. def with_name(self, name: str) -> Self:
  33. """Set the name of the object."""
  34. self.name = name
  35. self._name()
  36. return self
  37. def send(self) -> None:
  38. self._create()
  39. self._name()
  40. self._material()
  41. self._move()
  42. self._rotate()
  43. self._scale()
  44. self._visible()
  45. self._draggable()
  46. def __enter__(self) -> Self:
  47. self.scene.stack.append(self)
  48. return self
  49. def __exit__(self, *_) -> None:
  50. self.scene.stack.pop()
  51. def _create(self) -> None:
  52. self.scene.run_method('create', self.type, self.id, self.parent.id, *self.args)
  53. def _name(self) -> None:
  54. self.scene.run_method('name', self.id, self.name)
  55. def _material(self) -> None:
  56. self.scene.run_method('material', self.id, self.color, self.opacity, self.side_)
  57. def _move(self) -> None:
  58. self.scene.run_method('move', self.id, self.x, self.y, self.z)
  59. def _rotate(self) -> None:
  60. self.scene.run_method('rotate', self.id, self.R)
  61. def _scale(self) -> None:
  62. self.scene.run_method('scale', self.id, self.sx, self.sy, self.sz)
  63. def _visible(self) -> None:
  64. self.scene.run_method('visible', self.id, self.visible_)
  65. def _draggable(self) -> None:
  66. self.scene.run_method('draggable', self.id, self.draggable_)
  67. def _delete(self) -> None:
  68. self.scene.run_method('delete', self.id)
  69. def material(self, color: str = '#ffffff', opacity: float = 1.0, side: Literal['front', 'back', 'both'] = 'front') -> Self:
  70. """Set the color and opacity of the object.
  71. :param color: CSS color string (default: '#ffffff')
  72. :param opacity: opacity between 0.0 and 1.0 (default: 1.0)
  73. :param side: 'front', 'back', or 'double' (default: 'front')
  74. """
  75. if self.color != color or self.opacity != opacity or self.side_ != side:
  76. self.color = color
  77. self.opacity = opacity
  78. self.side_ = side
  79. self._material()
  80. return self
  81. def move(self, x: float = 0.0, y: float = 0.0, z: float = 0.0) -> Self:
  82. """Move the object.
  83. :param x: x coordinate
  84. :param y: y coordinate
  85. :param z: z coordinate
  86. """
  87. if self.x != x or self.y != y or self.z != z:
  88. self.x = x
  89. self.y = y
  90. self.z = z
  91. self._move()
  92. return self
  93. @staticmethod
  94. def rotation_matrix_from_euler(r_x: float, r_y: float, r_z: float) -> List[List[float]]:
  95. """Create a rotation matrix from Euler angles.
  96. :param r_x: rotation around the x axis in radians
  97. :param r_y: rotation around the y axis in radians
  98. :param r_z: rotation around the z axis in radians
  99. """
  100. sx, cx = math.sin(r_x), math.cos(r_x)
  101. sy, cy = math.sin(r_y), math.cos(r_y)
  102. sz, cz = math.sin(r_z), math.cos(r_z)
  103. return [
  104. [cz * cy, -sz * cx + cz * sy * sx, sz * sx + cz * sy * cx],
  105. [sz * cy, cz * cx + sz * sy * sx, -cz * sx + sz * sy * cx],
  106. [-sy, cy * sx, cy * cx],
  107. ]
  108. def rotate(self, r_x: float, r_y: float, r_z: float) -> Self:
  109. """Rotate the object.
  110. :param r_x: rotation around the x axis in radians
  111. :param r_y: rotation around the y axis in radians
  112. :param r_z: rotation around the z axis in radians
  113. """
  114. return self.rotate_R(self.rotation_matrix_from_euler(r_x, r_y, r_z))
  115. def rotate_R(self, R: List[List[float]]) -> Self:
  116. """Rotate the object.
  117. :param R: 3x3 rotation matrix
  118. """
  119. if self.R != R:
  120. self.R = R
  121. self._rotate()
  122. return self
  123. def scale(self, sx: float = 1.0, sy: Optional[float] = None, sz: Optional[float] = None) -> Self:
  124. """Scale the object.
  125. :param sx: scale factor for the x axis
  126. :param sy: scale factor for the y axis (default: `sx`)
  127. :param sz: scale factor for the z axis (default: `sx`)
  128. """
  129. if sy is None:
  130. sy = sx
  131. if sz is None:
  132. sz = sx
  133. if self.sx != sx or self.sy != sy or self.sz != sz:
  134. self.sx = sx
  135. self.sy = sy
  136. self.sz = sz
  137. self._scale()
  138. return self
  139. def visible(self, value: bool = True) -> Self:
  140. """Set the visibility of the object.
  141. :param value: whether the object should be visible (default: `True`)
  142. """
  143. if self.visible_ != value:
  144. self.visible_ = value
  145. self._visible()
  146. return self
  147. def draggable(self, value: bool = True) -> Self:
  148. """Set whether the object should be draggable.
  149. :param value: whether the object should be draggable (default: `True`)
  150. """
  151. if self.draggable_ != value:
  152. self.draggable_ = value
  153. self._draggable()
  154. return self
  155. def delete(self) -> None:
  156. """Delete the object."""
  157. children = [object for object in self.scene.objects.values() if object.parent == self]
  158. for child in children:
  159. child.delete()
  160. del self.scene.objects[self.id]
  161. self._delete()