You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
199 lines
8.0 KiB
199 lines
8.0 KiB
from dataclasses import dataclass |
|
from typing import Any, Callable, Dict, List, Optional, Union |
|
|
|
from typing_extensions import Self |
|
|
|
from .. import binding, globals # pylint: disable=redefined-builtin |
|
from ..dataclasses import KWONLY_SLOTS |
|
from ..element import Element |
|
from ..events import (GenericEventArguments, SceneClickEventArguments, SceneClickHit, SceneDragEventArguments, |
|
handle_event) |
|
from .scene_object3d import Object3D |
|
|
|
|
|
@dataclass(**KWONLY_SLOTS) |
|
class SceneCamera: |
|
x: float = 0 |
|
y: float = -3 |
|
z: float = 5 |
|
look_at_x: float = 0 |
|
look_at_y: float = 0 |
|
look_at_z: float = 0 |
|
up_x: float = 0 |
|
up_y: float = 0 |
|
up_z: float = 1 |
|
|
|
|
|
@dataclass(**KWONLY_SLOTS) |
|
class SceneObject: |
|
id: str = 'scene' |
|
|
|
|
|
class Scene(Element, |
|
component='scene.js', |
|
libraries=['lib/tween/tween.umd.js'], |
|
exposed_libraries=[ |
|
'lib/three/three.module.js', |
|
'lib/three/modules/CSS2DRenderer.js', |
|
'lib/three/modules/CSS3DRenderer.js', |
|
'lib/three/modules/DragControls.js', |
|
'lib/three/modules/OrbitControls.js', |
|
'lib/three/modules/STLLoader.js', |
|
]): |
|
# pylint: disable=import-outside-toplevel |
|
from .scene_objects import Box as box |
|
from .scene_objects import Curve as curve |
|
from .scene_objects import Cylinder as cylinder |
|
from .scene_objects import Extrusion as extrusion |
|
from .scene_objects import Group as group |
|
from .scene_objects import Line as line |
|
from .scene_objects import PointCloud as point_cloud |
|
from .scene_objects import QuadraticBezierTube as quadratic_bezier_tube |
|
from .scene_objects import Ring as ring |
|
from .scene_objects import Sphere as sphere |
|
from .scene_objects import SpotLight as spot_light |
|
from .scene_objects import Stl as stl |
|
from .scene_objects import Text as text |
|
from .scene_objects import Text3d as text3d |
|
from .scene_objects import Texture as texture |
|
|
|
def __init__(self, |
|
width: int = 400, |
|
height: int = 300, |
|
grid: bool = True, |
|
on_click: Optional[Callable[..., Any]] = None, |
|
on_drag_start: Optional[Callable[..., Any]] = None, |
|
on_drag_end: Optional[Callable[..., Any]] = None, |
|
drag_constraints: str = '', |
|
) -> None: |
|
"""3D Scene |
|
|
|
Display a 3D scene using `three.js <https://threejs.org/>`_. |
|
Currently NiceGUI supports boxes, spheres, cylinders/cones, extrusions, straight lines, curves and textured meshes. |
|
Objects can be translated, rotated and displayed with different color, opacity or as wireframes. |
|
They can also be grouped to apply joint movements. |
|
|
|
:param width: width of the canvas |
|
:param height: height of the canvas |
|
:param grid: whether to display a grid |
|
:param on_click: callback to execute when a 3D object is clicked |
|
:param on_drag_start: callback to execute when a 3D object is dragged |
|
:param on_drag_end: callback to execute when a 3D object is dropped |
|
:param drag_constraints: comma-separated JavaScript expression for constraining positions of dragged objects (e.g. ``'x = 0, z = y / 2'``) |
|
""" |
|
super().__init__() |
|
self._props['width'] = width |
|
self._props['height'] = height |
|
self._props['grid'] = grid |
|
self.objects: Dict[str, Object3D] = {} |
|
self.stack: List[Union[Object3D, SceneObject]] = [SceneObject()] |
|
self.camera: SceneCamera = SceneCamera() |
|
self.on_click = on_click |
|
self.on_drag_start = on_drag_start |
|
self.on_drag_end = on_drag_end |
|
self.is_initialized = False |
|
self.on('init', self.handle_init) |
|
self.on('click3d', self.handle_click) |
|
self.on('dragstart', self.handle_drag) |
|
self.on('dragend', self.handle_drag) |
|
self._props['drag_constraints'] = drag_constraints |
|
|
|
def __enter__(self) -> Self: |
|
Object3D.current_scene = self |
|
super().__enter__() |
|
return self |
|
|
|
def __getattribute__(self, name: str) -> Any: |
|
attribute = super().__getattribute__(name) |
|
if isinstance(attribute, type) and issubclass(attribute, Object3D): |
|
Object3D.current_scene = self |
|
return attribute |
|
|
|
def handle_init(self, e: GenericEventArguments) -> None: |
|
self.is_initialized = True |
|
with globals.socket_id(e.args['socket_id']): |
|
self.move_camera(duration=0) |
|
for obj in self.objects.values(): |
|
obj.send() |
|
|
|
def run_method(self, name: str, *args: Any) -> None: |
|
if not self.is_initialized: |
|
return |
|
super().run_method(name, *args) |
|
|
|
def handle_click(self, e: GenericEventArguments) -> None: |
|
arguments = SceneClickEventArguments( |
|
sender=self, |
|
client=self.client, |
|
click_type=e.args['click_type'], |
|
button=e.args['button'], |
|
alt=e.args['alt_key'], |
|
ctrl=e.args['ctrl_key'], |
|
meta=e.args['meta_key'], |
|
shift=e.args['shift_key'], |
|
hits=[SceneClickHit( |
|
object_id=hit['object_id'], |
|
object_name=hit['object_name'], |
|
x=hit['point']['x'], |
|
y=hit['point']['y'], |
|
z=hit['point']['z'], |
|
) for hit in e.args['hits']], |
|
) |
|
handle_event(self.on_click, arguments) |
|
|
|
def handle_drag(self, e: GenericEventArguments) -> None: |
|
arguments = SceneDragEventArguments( |
|
sender=self, |
|
client=self.client, |
|
type=e.args['type'], |
|
object_id=e.args['object_id'], |
|
object_name=e.args['object_name'], |
|
x=e.args['x'], |
|
y=e.args['y'], |
|
z=e.args['z'], |
|
) |
|
if arguments.type == 'dragend': |
|
self.objects[arguments.object_id].move(arguments.x, arguments.y, arguments.z) |
|
handle_event(self.on_drag_start if arguments.type == 'dragstart' else self.on_drag_end, arguments) |
|
|
|
def __len__(self) -> int: |
|
return len(self.objects) |
|
|
|
def move_camera(self, |
|
x: Optional[float] = None, |
|
y: Optional[float] = None, |
|
z: Optional[float] = None, |
|
look_at_x: Optional[float] = None, |
|
look_at_y: Optional[float] = None, |
|
look_at_z: Optional[float] = None, |
|
up_x: Optional[float] = None, |
|
up_y: Optional[float] = None, |
|
up_z: Optional[float] = None, |
|
duration: float = 0.5) -> None: |
|
self.camera.x = self.camera.x if x is None else x |
|
self.camera.y = self.camera.y if y is None else y |
|
self.camera.z = self.camera.z if z is None else z |
|
self.camera.look_at_x = self.camera.look_at_x if look_at_x is None else look_at_x |
|
self.camera.look_at_y = self.camera.look_at_y if look_at_y is None else look_at_y |
|
self.camera.look_at_z = self.camera.look_at_z if look_at_z is None else look_at_z |
|
self.camera.up_x = self.camera.up_x if up_x is None else up_x |
|
self.camera.up_y = self.camera.up_y if up_y is None else up_y |
|
self.camera.up_z = self.camera.up_z if up_z is None else up_z |
|
self.run_method('move_camera', |
|
self.camera.x, self.camera.y, self.camera.z, |
|
self.camera.look_at_x, self.camera.look_at_y, self.camera.look_at_z, |
|
self.camera.up_x, self.camera.up_y, self.camera.up_z, duration) |
|
|
|
def _on_delete(self) -> None: |
|
binding.remove(list(self.objects.values()), Object3D) |
|
super()._on_delete() |
|
|
|
def delete_objects(self, predicate: Callable[[Object3D], bool] = lambda _: True) -> None: |
|
for obj in list(self.objects.values()): |
|
if predicate(obj): |
|
obj.delete() |
|
|
|
def clear(self) -> None: |
|
"""Remove all objects from the scene.""" |
|
super().clear() |
|
self.delete_objects()
|
|
|