scene.py 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221
  1. from dataclasses import dataclass
  2. from typing import Any, Callable, Dict, List, Optional, Union
  3. from typing_extensions import Self
  4. from .. import binding, globals # pylint: disable=redefined-builtin
  5. from ..dataclasses import KWONLY_SLOTS
  6. from ..element import Element
  7. from ..events import (GenericEventArguments, SceneClickEventArguments, SceneClickHit, SceneDragEventArguments,
  8. handle_event)
  9. from .scene_object3d import Object3D
  10. @dataclass(**KWONLY_SLOTS)
  11. class SceneCamera:
  12. x: float = 0
  13. y: float = -3
  14. z: float = 5
  15. look_at_x: float = 0
  16. look_at_y: float = 0
  17. look_at_z: float = 0
  18. up_x: float = 0
  19. up_y: float = 0
  20. up_z: float = 1
  21. @dataclass(**KWONLY_SLOTS)
  22. class SceneObject:
  23. id: str = 'scene'
  24. class Scene(Element,
  25. component='scene.js',
  26. libraries=['lib/tween/tween.umd.js'],
  27. exposed_libraries=[
  28. 'lib/three/three.module.js',
  29. 'lib/three/modules/CSS2DRenderer.js',
  30. 'lib/three/modules/CSS3DRenderer.js',
  31. 'lib/three/modules/DragControls.js',
  32. 'lib/three/modules/OrbitControls.js',
  33. 'lib/three/modules/STLLoader.js',
  34. ]):
  35. # pylint: disable=import-outside-toplevel
  36. from .scene_objects import Box as box
  37. from .scene_objects import Curve as curve
  38. from .scene_objects import Cylinder as cylinder
  39. from .scene_objects import Extrusion as extrusion
  40. from .scene_objects import Group as group
  41. from .scene_objects import Line as line
  42. from .scene_objects import PointCloud as point_cloud
  43. from .scene_objects import QuadraticBezierTube as quadratic_bezier_tube
  44. from .scene_objects import Ring as ring
  45. from .scene_objects import Sphere as sphere
  46. from .scene_objects import SpotLight as spot_light
  47. from .scene_objects import Stl as stl
  48. from .scene_objects import Text as text
  49. from .scene_objects import Text3d as text3d
  50. from .scene_objects import Texture as texture
  51. def __init__(self,
  52. width: int = 400,
  53. height: int = 300,
  54. grid: bool = True,
  55. on_click: Optional[Callable[..., Any]] = None,
  56. on_drag_start: Optional[Callable[..., Any]] = None,
  57. on_drag_end: Optional[Callable[..., Any]] = None,
  58. drag_constraints: str = '',
  59. ) -> None:
  60. """3D Scene
  61. Display a 3D scene using `three.js <https://threejs.org/>`_.
  62. Currently NiceGUI supports boxes, spheres, cylinders/cones, extrusions, straight lines, curves and textured meshes.
  63. Objects can be translated, rotated and displayed with different color, opacity or as wireframes.
  64. They can also be grouped to apply joint movements.
  65. :param width: width of the canvas
  66. :param height: height of the canvas
  67. :param grid: whether to display a grid
  68. :param on_click: callback to execute when a 3D object is clicked
  69. :param on_drag_start: callback to execute when a 3D object is dragged
  70. :param on_drag_end: callback to execute when a 3D object is dropped
  71. :param drag_constraints: comma-separated JavaScript expression for constraining positions of dragged objects (e.g. ``'x = 0, z = y / 2'``)
  72. """
  73. super().__init__()
  74. self._props['width'] = width
  75. self._props['height'] = height
  76. self._props['grid'] = grid
  77. self.objects: Dict[str, Object3D] = {}
  78. self.stack: List[Union[Object3D, SceneObject]] = [SceneObject()]
  79. self.camera: SceneCamera = SceneCamera()
  80. self._click_handler = on_click
  81. self._drag_start_handler = on_drag_start
  82. self._drag_end_handler = on_drag_end
  83. self.is_initialized = False
  84. self.on('init', self._handle_init)
  85. self.on('click3d', self._handle_click)
  86. self.on('dragstart', self._handle_drag)
  87. self.on('dragend', self._handle_drag)
  88. self._props['drag_constraints'] = drag_constraints
  89. def __enter__(self) -> Self:
  90. Object3D.current_scene = self
  91. super().__enter__()
  92. return self
  93. def __getattribute__(self, name: str) -> Any:
  94. attribute = super().__getattribute__(name)
  95. if isinstance(attribute, type) and issubclass(attribute, Object3D):
  96. Object3D.current_scene = self
  97. return attribute
  98. def _handle_init(self, e: GenericEventArguments) -> None:
  99. self.is_initialized = True
  100. with globals.socket_id(e.args['socket_id']):
  101. self.move_camera(duration=0)
  102. for obj in self.objects.values():
  103. obj.send()
  104. def run_method(self, name: str, *args: Any) -> None:
  105. """Run a method on the client.
  106. :param name: name of the method
  107. :param args: arguments to pass to the method
  108. """
  109. if not self.is_initialized:
  110. return
  111. super().run_method(name, *args)
  112. def _handle_click(self, e: GenericEventArguments) -> None:
  113. arguments = SceneClickEventArguments(
  114. sender=self,
  115. client=self.client,
  116. click_type=e.args['click_type'],
  117. button=e.args['button'],
  118. alt=e.args['alt_key'],
  119. ctrl=e.args['ctrl_key'],
  120. meta=e.args['meta_key'],
  121. shift=e.args['shift_key'],
  122. hits=[SceneClickHit(
  123. object_id=hit['object_id'],
  124. object_name=hit['object_name'],
  125. x=hit['point']['x'],
  126. y=hit['point']['y'],
  127. z=hit['point']['z'],
  128. ) for hit in e.args['hits']],
  129. )
  130. handle_event(self._click_handler, arguments)
  131. def _handle_drag(self, e: GenericEventArguments) -> None:
  132. arguments = SceneDragEventArguments(
  133. sender=self,
  134. client=self.client,
  135. type=e.args['type'],
  136. object_id=e.args['object_id'],
  137. object_name=e.args['object_name'],
  138. x=e.args['x'],
  139. y=e.args['y'],
  140. z=e.args['z'],
  141. )
  142. if arguments.type == 'dragend':
  143. self.objects[arguments.object_id].move(arguments.x, arguments.y, arguments.z)
  144. handle_event(self._drag_start_handler if arguments.type == 'dragstart' else self._drag_end_handler, arguments)
  145. def __len__(self) -> int:
  146. return len(self.objects)
  147. def move_camera(self,
  148. x: Optional[float] = None,
  149. y: Optional[float] = None,
  150. z: Optional[float] = None,
  151. look_at_x: Optional[float] = None,
  152. look_at_y: Optional[float] = None,
  153. look_at_z: Optional[float] = None,
  154. up_x: Optional[float] = None,
  155. up_y: Optional[float] = None,
  156. up_z: Optional[float] = None,
  157. duration: float = 0.5) -> None:
  158. """Move the camera to a new position.
  159. :param x: camera x position
  160. :param y: camera y position
  161. :param z: camera z position
  162. :param look_at_x: camera look-at x position
  163. :param look_at_y: camera look-at y position
  164. :param look_at_z: camera look-at z position
  165. :param up_x: x component of the camera up vector
  166. :param up_y: y component of the camera up vector
  167. :param up_z: z component of the camera up vector
  168. :param duration: duration of the movement in seconds (default: `0.5`)
  169. """
  170. self.camera.x = self.camera.x if x is None else x
  171. self.camera.y = self.camera.y if y is None else y
  172. self.camera.z = self.camera.z if z is None else z
  173. self.camera.look_at_x = self.camera.look_at_x if look_at_x is None else look_at_x
  174. self.camera.look_at_y = self.camera.look_at_y if look_at_y is None else look_at_y
  175. self.camera.look_at_z = self.camera.look_at_z if look_at_z is None else look_at_z
  176. self.camera.up_x = self.camera.up_x if up_x is None else up_x
  177. self.camera.up_y = self.camera.up_y if up_y is None else up_y
  178. self.camera.up_z = self.camera.up_z if up_z is None else up_z
  179. self.run_method('move_camera',
  180. self.camera.x, self.camera.y, self.camera.z,
  181. self.camera.look_at_x, self.camera.look_at_y, self.camera.look_at_z,
  182. self.camera.up_x, self.camera.up_y, self.camera.up_z, duration)
  183. def _handle_delete(self) -> None:
  184. binding.remove(list(self.objects.values()), Object3D)
  185. super()._handle_delete()
  186. def delete_objects(self, predicate: Callable[[Object3D], bool] = lambda _: True) -> None:
  187. """Remove objects from the scene.
  188. :param predicate: function which returns `True` for objects which should be deleted
  189. """
  190. for obj in list(self.objects.values()):
  191. if predicate(obj):
  192. obj.delete()
  193. def clear(self) -> None:
  194. """Remove all objects from the scene."""
  195. super().clear()
  196. self.delete_objects()