Skip to content

Commit

Permalink
Allow ungrouping 3D objects (#4018)
Browse files Browse the repository at this point in the history
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
  • Loading branch information
falkoschindler authored Nov 22, 2024
1 parent 1f6e3fb commit dd2504a
Show file tree
Hide file tree
Showing 3 changed files with 169 additions and 0 deletions.
16 changes: 16 additions & 0 deletions nicegui/elements/scene.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
132 changes: 132 additions & 0 deletions nicegui/elements/scene_object3d.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""
Expand Down
21 changes: 21 additions & 0 deletions website/documentation/content/scene_documentation.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

0 comments on commit dd2504a

Please sign in to comment.