From dd2504a6f36e2b7c0718759410d466fc9393ed67 Mon Sep 17 00:00:00 2001 From: Falko Schindler Date: Fri, 22 Nov 2024 14:10:28 +0100 Subject: [PATCH] Allow ungrouping 3D objects (#4018) This PR implements feature request #4015 introducing an `ungroup` method for 3D objects: ```py import math import time from nicegui import ui with ui.scene() as scene: with scene.group() as group: a = scene.sphere().move(-2) b = scene.sphere().move(0) c = scene.sphere().move(2) ui.timer(0.1, lambda: group.move(y=math.sin(time.time()))) ui.button('Ungroup', on_click=a.ungroup) ui.run() ``` Open tasks: - [x] better name: "detach" - [x] introduce "attach" for symmetry - [ ] resolve pose so that the object doesn't move visually --- nicegui/elements/scene.js | 16 +++ nicegui/elements/scene_object3d.py | 132 ++++++++++++++++++ .../content/scene_documentation.py | 21 +++ 3 files changed, 169 insertions(+) diff --git a/nicegui/elements/scene.js b/nicegui/elements/scene.js index 659e9265b..c72469a19 100644 --- a/nicegui/elements/scene.js +++ b/nicegui/elements/scene.js @@ -413,6 +413,22 @@ export default { const geometry = this.objects.get(object_id).geometry; set_point_cloud_data(position, color, geometry); }, + attach(object_id, parent_id, x, y, z, R) { + if (!this.objects.has(object_id)) return; + const object = this.objects.get(object_id); + const parent = this.objects.get(parent_id); + parent.add(object); + this.move(object_id, x, y, z); + this.rotate(object_id, R); + }, + detach(object_id, x, y, z, R) { + if (!this.objects.has(object_id)) return; + const object = this.objects.get(object_id); + object.removeFromParent(); + this.scene.add(object); + this.move(object_id, x, y, z); + this.rotate(object_id, R); + }, move_camera(x, y, z, look_at_x, look_at_y, look_at_z, up_x, up_y, up_z, duration) { if (this.camera_tween) this.camera_tween.stop(); const camera_up_changed = up_x !== null || up_y !== null || up_z !== null; diff --git a/nicegui/elements/scene_object3d.py b/nicegui/elements/scene_object3d.py index 67a133edb..f30a608a5 100644 --- a/nicegui/elements/scene_object3d.py +++ b/nicegui/elements/scene_object3d.py @@ -196,6 +196,138 @@ def draggable(self, value: bool = True) -> Self: 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. + """ + 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: Union[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. + """ + 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: Union[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.""" diff --git a/website/documentation/content/scene_documentation.py b/website/documentation/content/scene_documentation.py index 8634c10ed..744be6ca3 100644 --- a/website/documentation/content/scene_documentation.py +++ b/website/documentation/content/scene_documentation.py @@ -268,4 +268,25 @@ def __init__(self, name: str, *, length: float = 1.0) -> None: CoordinateSystem('custom frame').move(-2, -2, 1).rotate(0.1, 0.2, 0.3) +@doc.demo('Attaching/detaching objects', ''' + To add or remove objects from groups you can use the `attach` and `detach` methods. + 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. +''') +def attach_detach() -> None: + import math + import time + + with ui.scene().classes('w-full h-64') as scene: + with scene.group() as group: + a = scene.box().move(-2) + b = scene.box().move(0) + c = scene.box().move(2) + + ui.timer(0.1, lambda: group.move(y=math.sin(time.time())).rotate(0, 0, time.time())) + ui.button('Detach', on_click=a.detach) + ui.button('Attach', on_click=lambda: a.attach(group)) + + doc.reference(ui.scene)