scene.py 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153
  1. import traceback
  2. from dataclasses import dataclass
  3. from typing import Callable, Optional
  4. import websockets
  5. from justpy import WebPage
  6. from ..auto_context import get_view_stack
  7. from ..events import handle_event
  8. from ..page import Page
  9. from ..routes import add_dependencies
  10. from ..task_logger import create_task
  11. from .custom_view import CustomView
  12. from .element import Element
  13. from .scene_object3d import Object3D
  14. add_dependencies(__file__, [
  15. 'three.min.js',
  16. 'CSS2DRenderer.js',
  17. 'CSS3DRenderer.js',
  18. 'OrbitControls.js',
  19. 'STLLoader.js',
  20. 'tween.umd.min.js',
  21. ])
  22. @dataclass
  23. class SceneCamera:
  24. x: float = 0
  25. y: float = -3
  26. z: float = 5
  27. look_at_x: float = 0
  28. look_at_y: float = 0
  29. look_at_z: float = 0
  30. up_x: float = 0
  31. up_y: float = 0
  32. up_z: float = 1
  33. def create_move_command(self, duration: float = 0) -> str:
  34. return 'move_camera(' \
  35. f'{self.x}, {self.y}, {self.z}, ' \
  36. f'{self.look_at_x}, {self.look_at_y}, {self.look_at_z}, ' \
  37. f'{self.up_x}, {self.up_y}, {self.up_z}, {duration})'
  38. class SceneView(CustomView):
  39. def __init__(self, *, width: int, height: int, on_click: Optional[Callable]):
  40. super().__init__('scene', width=width, height=height)
  41. self.on_click = on_click
  42. self.allowed_events = ['onConnect', 'onClick']
  43. self.initialize(temp=False, onConnect=self.handle_connect, onClick=self.handle_click)
  44. self.objects = {}
  45. self.camera: SceneCamera = SceneCamera()
  46. def handle_connect(self, msg):
  47. try:
  48. for object in self.objects.values():
  49. object.send_to(msg.websocket)
  50. create_task(self.run_method(self.camera.create_move_command(), msg.websocket), name='move camera (connect)')
  51. except:
  52. traceback.print_exc()
  53. def handle_click(self, msg) -> Optional[bool]:
  54. try:
  55. for hit in msg.hits:
  56. hit.object = self.objects.get(hit.object_id)
  57. return handle_event(self.on_click, msg)
  58. except:
  59. traceback.print_exc()
  60. async def run_method(self, command, websocket):
  61. try:
  62. await websocket.send_json({'type': 'run_method', 'data': command, 'id': self.id})
  63. except (websockets.exceptions.ConnectionClosedOK, RuntimeError):
  64. pass
  65. return True
  66. def __len__(self):
  67. return len(self.objects)
  68. class Scene(Element):
  69. from .scene_objects import Box as box
  70. from .scene_objects import Curve as curve
  71. from .scene_objects import Cylinder as cylinder
  72. from .scene_objects import Extrusion as extrusion
  73. from .scene_objects import Group as group
  74. from .scene_objects import Line as line
  75. from .scene_objects import QuadraticBezierTube as quadratic_bezier_tube
  76. from .scene_objects import Ring as ring
  77. from .scene_objects import Sphere as sphere
  78. from .scene_objects import SpotLight as spot_light
  79. from .scene_objects import Stl as stl
  80. from .scene_objects import Text as text
  81. from .scene_objects import Text3d as text3d
  82. from .scene_objects import Texture as texture
  83. def __init__(self, width: int = 400, height: int = 300, on_click: Optional[Callable] = None):
  84. """3D Scene
  85. Display a 3d scene using `three.js <https://threejs.org/>`_.
  86. Currently NiceGUI supports boxes, spheres, cylinders/cones, extrusions, straight lines, curves and textured meshes.
  87. Objects can be translated, rotated and displayed with different color, opacity or as wireframes.
  88. They can also be grouped to apply joint movements.
  89. :param width: width of the canvas
  90. :param height: height of the canvas
  91. :param on_click: callback to execute when a 3d object is clicked
  92. """
  93. super().__init__(SceneView(width=width, height=height, on_click=on_click))
  94. def __enter__(self):
  95. get_view_stack().append(self.view)
  96. scene = self.view.objects.get('scene', SceneObject(self.view, self.page))
  97. Object3D.stack.clear()
  98. Object3D.stack.append(scene)
  99. return self
  100. def __exit__(self, *_):
  101. get_view_stack().pop()
  102. def move_camera(self,
  103. x: Optional[float] = None,
  104. y: Optional[float] = None,
  105. z: Optional[float] = None,
  106. look_at_x: Optional[float] = None,
  107. look_at_y: Optional[float] = None,
  108. look_at_z: Optional[float] = None,
  109. up_x: Optional[float] = None,
  110. up_y: Optional[float] = None,
  111. up_z: Optional[float] = None,
  112. duration: float = 0.5):
  113. camera: SceneCamera = self.view.camera
  114. camera.x = camera.x if x is None else x
  115. camera.y = camera.y if y is None else y
  116. camera.z = camera.z if z is None else z
  117. camera.look_at_x = camera.look_at_x if look_at_x is None else look_at_x
  118. camera.look_at_y = camera.look_at_y if look_at_y is None else look_at_y
  119. camera.look_at_z = camera.look_at_z if look_at_z is None else look_at_z
  120. camera.up_x = camera.up_x if up_x is None else up_x
  121. camera.up_y = camera.up_y if up_y is None else up_y
  122. camera.up_z = camera.up_z if up_z is None else up_z
  123. for socket in WebPage.sockets.get(self.page.page_id, {}).values():
  124. create_task(self.view.run_method(camera.create_move_command(duration), socket), name='move camera')
  125. class SceneObject:
  126. def __init__(self, view: SceneView, page: Page):
  127. self.id = 'scene'
  128. self.view = view
  129. self.page = page