Skip to content

Commit

Permalink
Merge pull request #698 from threedworld-mit/set_ui_element_position
Browse files Browse the repository at this point in the history
Added set_ui_element_position and a mask example controller
  • Loading branch information
alters-mit authored Apr 26, 2024
2 parents aced809 + b329e6c commit 35a6b69
Show file tree
Hide file tree
Showing 8 changed files with 240 additions and 1 deletion.
26 changes: 26 additions & 0 deletions Documentation/Changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,32 @@

To upgrade from TDW v1.11 to v1.12, read [this guide](upgrade_guides/v1.11_to_v1.12.md).

## v1.12.25

### Command API

#### New Commands

| Command | Description |
| --- | --- |
| `set_ui_element_position` | Set the position of a UI element. |

### `tdw` module

- Added: `ui.set_position(id, position)`.

### Example Controllers

- Added: `ui/mask.py`

### Documentation

#### Modified Documentation

| Document | Modification |
| --- | --- |
| `lessons/ui/ui.md` | Added a section describing how to create an image "mask".<br>Added a section describing how to move an image. |

## v1.12.24

### Command API
Expand Down
22 changes: 22 additions & 0 deletions Documentation/api/command_api.md
Original file line number Diff line number Diff line change
Expand Up @@ -844,6 +844,7 @@
| --- | --- |
| [`destroy_ui_element`](#destroy_ui_element) | Destroy a UI element in the scene. |
| [`set_ui_color`](#set_ui_color) | Set the color of a UI image or text. |
| [`set_ui_element_position`](#set_ui_element_position) | Set the position of a UI element. |
| [`set_ui_element_size`](#set_ui_element_size) | Set the size of a UI element. |
| [`set_ui_text`](#set_ui_text) | Set the text of a Text object that is already on the screen. |

Expand Down Expand Up @@ -11067,6 +11068,27 @@ Set the color of a UI image or text.

***

## **`set_ui_element_position`**

Set the position of a UI element.


```python
{"$type": "set_ui_element_position", "id": 1}
```

```python
{"$type": "set_ui_element_position", "id": 1, "position": {"x": 0, "y": 0}, "canvas_id": 0}
```

| Parameter | Type | Description | Default |
| --- | --- | --- | --- |
| `"position"` | Vector2Int | The anchor position of the UI element in pixels. x is lateral, y is vertical. The anchor position is not the true pixel position. For example, if the anchor is {"x": 0, "y": 0} and the position is {"x": 0, "y": 0}, the UI element will be in the bottom-left of the screen. | {"x": 0, "y": 0} |
| `"id"` | int | The unique ID of the UI element. | |
| `"canvas_id"` | int | The unique ID of the UI canvas. | 0 |

***

## **`set_ui_element_size`**

Set the size of a UI element.
Expand Down
Binary file added Documentation/lessons/ui/images/mask.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
101 changes: 101 additions & 0 deletions Documentation/lessons/ui/ui.md
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,105 @@ Result:

![](images/image.jpg)

## Create a UI "mask"

Creating a UI mask is as simple as creating a new image and drawing a transparent shape:

```python
from io import BytesIO
from PIL import Image, ImageDraw

w = 512
h = 512
image = Image.new(mode="RGBA", size=(w, h), color=(0, 0, 0, 255))
# Draw a circle on the mask.
draw = ImageDraw.Draw(image)
diameter = 256
x = w // 2 - diameter // 2
y = h // 2 - diameter // 2
draw.ellipse([(x, y), (y + diameter, y + diameter)], fill=(0, 0, 0, 0))
# Convert the PIL image to bytes.
with BytesIO() as output:
image.save(output, "PNG")
mask = output.getvalue()
# `mask` can now be used by `ui.add_image()`
```

## Move a UI element

To move a UI image or text, call `ui.set_position(id, position)`, which sends [`set_ui_element_position`](../../api/command_api.md#set_ui_element_position).

In this example, an image with a "mask" is added to the scene. This image is larger than the screen size so that it can be moved while still covering the entire screen:

```python
from io import BytesIO
from PIL import Image, ImageDraw
from tdw.controller import Controller
from tdw.add_ons.third_person_camera import ThirdPersonCamera
from tdw.add_ons.ui import UI
from tdw.add_ons.image_capture import ImageCapture
from tdw.backend.paths import EXAMPLE_CONTROLLER_OUTPUT_PATH


c = Controller()
# Add the UI add-on and the camera.
camera = ThirdPersonCamera(position={"x": 0, "y": 0, "z": -1.2},
avatar_id="a")
ui = UI()
c.add_ons.extend([camera, ui])
ui.attach_canvas_to_avatar(avatar_id="a")
screen_size = 512
commands = [{"$type": "create_empty_environment"},
{"$type": "set_screen_size",
"width": screen_size,
"height": screen_size}]
# Add a cube slightly off-center.
commands.extend(Controller.get_add_physics_object(model_name="cube",
library="models_flex.json",
object_id=0,
position={"x": 0.25, "y": 0, "z": 1},
rotation={"x": 30, "y": 10, "z": 0},
kinematic=True))
c.communicate(commands)

# Enable image capture.
path = EXAMPLE_CONTROLLER_OUTPUT_PATH.joinpath("ui_mask")
print(f"Images will be saved to: {path}")
capture = ImageCapture(path=path, avatar_ids=["a"])
c.add_ons.append(capture)

# Create the UI image with PIL.
# The image is larger than the screen size so we can move it around.
image_size = screen_size * 3
image = Image.new(mode="RGBA", size=(image_size, image_size), color=(0, 0, 0, 255))
# Draw a circle on the mask.
draw = ImageDraw.Draw(image)
diameter = 256
d = image_size // 2 - diameter // 2
draw.ellipse([(d, d), (d + diameter, d + diameter)], fill=(0, 0, 0, 0))
# Convert the PIL image to bytes.
with BytesIO() as output:
image.save(output, "PNG")
mask = output.getvalue()
x = 0
y = 0
# Add the image.
mask_id = ui.add_image(image=mask, position={"x": x, "y": y}, size={"x": image_size, "y": image_size}, raycast_target=False)
c.communicate([])

# Move the image.
for i in range(100):
x += 4
y += 3
ui.set_position(ui_id=mask_id, position={"x": x, "y": y})
c.communicate([])
c.communicate({"$type": "terminate"})
```

Result:

![](images/mask.gif)

## Destroy UI elements

Destroy a specific UI element via `ui.destroy(ui_id)`, which sends [`destroy_ui_element`](../../api/command_api.md#destroy_ui_element).
Expand All @@ -253,6 +352,7 @@ Example controllers:
- [hello_world_ui.py](https://github.com/threedworld-mit/tdw/blob/master/Python/example_controllers/ui/hello_world_ui.py) Minimal UI example.
- [anchors_and_pivots.py](https://github.com/threedworld-mit/tdw/blob/master/Python/example_controllers/ui/anchors_and_pivots.py) Anchor text to the top-left corner of the screen.
- [image.py](https://github.com/threedworld-mit/tdw/blob/master/Python/example_controllers/ui/image.py) Add a UI image.
- [mask.py](https://github.com/threedworld-mit/tdw/blob/master/Python/example_controllers/ui/mask.py) Create black background with a circular "hole" in it and move the image around.

Python API:

Expand All @@ -265,6 +365,7 @@ Command API:
- [`add_ui_text`](../../api/command_api.md#add_ui_text)
- [`add_ui_image`](../../api/command_api.md#add_ui_image)
- [`set_ui_element_size`](../../api/command_api.md#set_ui_element_size)
- [`set_ui_element_position`](../../api/command_api.md#set_ui_element_position)
- [`set_target_framerate`](../../api/command_api.md#set_target_framerate)
- [`destroy_ui_element`](../../api/command_api.md#destroy_ui_element)
- [`destroy_ui_canvas`](../../api/command_api.md#destroy_ui_canvas)
11 changes: 11 additions & 0 deletions Documentation/python/add_ons/ui.md
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,17 @@ Attach the UI canvas to a VR rig.
| --- | --- | --- | --- |
| plane_distance | float | 0.25 | The distance from the camera to the UI canvas. |

#### set_position

**`self.set_position(ui_id, position)`**

Set the position of a UI element.

| Parameter | Type | Default | Description |
| --- | --- | --- | --- |
| ui_id | int | | The UI element's ID. |
| position | Dict[str, float] | | The screen (pixel) position as a Vector2. Values must be integers. |

#### destroy

**`self.destroy(ui_id)`**
Expand Down
67 changes: 67 additions & 0 deletions Python/example_controllers/ui/mask.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
from io import BytesIO
from PIL import Image, ImageDraw
from tdw.controller import Controller
from tdw.add_ons.third_person_camera import ThirdPersonCamera
from tdw.add_ons.ui import UI
from tdw.add_ons.image_capture import ImageCapture
from tdw.backend.paths import EXAMPLE_CONTROLLER_OUTPUT_PATH


"""
Create black background with a circular "hole" in it and move the image around.
"""


c = Controller()
# Add the UI add-on and the camera.
camera = ThirdPersonCamera(position={"x": 0, "y": 0, "z": -1.2},
avatar_id="a")
ui = UI()
c.add_ons.extend([camera, ui])
ui.attach_canvas_to_avatar(avatar_id="a")
screen_size = 512
commands = [{"$type": "create_empty_environment"},
{"$type": "set_screen_size",
"width": screen_size,
"height": screen_size}]
# Add a cube slightly off-center.
commands.extend(Controller.get_add_physics_object(model_name="cube",
library="models_flex.json",
object_id=0,
position={"x": 0.25, "y": 0, "z": 1},
rotation={"x": 30, "y": 10, "z": 0},
kinematic=True))
c.communicate(commands)

# Enable image capture.
path = EXAMPLE_CONTROLLER_OUTPUT_PATH.joinpath("ui_mask")
print(f"Images will be saved to: {path}")
capture = ImageCapture(path=path, avatar_ids=["a"])
c.add_ons.append(capture)

# Create the UI image with PIL.
# The image is larger than the screen size so we can move it around.
image_size = screen_size * 3
image = Image.new(mode="RGBA", size=(image_size, image_size), color=(0, 0, 0, 255))
# Draw a circle on the mask.
draw = ImageDraw.Draw(image)
diameter = 256
d = image_size // 2 - diameter // 2
draw.ellipse([(d, d), (d + diameter, d + diameter)], fill=(0, 0, 0, 0))
# Convert the PIL image to bytes.
with BytesIO() as output:
image.save(output, "PNG")
mask = output.getvalue()
x = 0
y = 0
# Add the image.
mask_id = ui.add_image(image=mask, position={"x": x, "y": y}, size={"x": image_size, "y": image_size}, raycast_target=False)
c.communicate([])

# Move the image.
for i in range(100):
x += 4
y += 3
ui.set_position(ui_id=mask_id, position={"x": x, "y": y})
c.communicate([])
c.communicate({"$type": "terminate"})
12 changes: 12 additions & 0 deletions Python/tdw/add_ons/ui.py
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,18 @@ def attach_canvas_to_vr_rig(self, plane_distance: float = 0.25) -> None:
self.commands.append({"$type": "attach_ui_canvas_to_vr_rig",
"plane_distance": plane_distance})

def set_position(self, ui_id: int, position: Dict[str, float]) -> None:
"""
Set the position of a UI element.

:param ui_id: The UI element's ID.
:param position: The screen (pixel) position as a Vector2. Values must be integers.
"""

self.commands.append({"$type": "set_ui_element_position",
"id": ui_id,
"position": position})

def destroy(self, ui_id: int) -> None:
"""
Destroy a UI element.
Expand Down
2 changes: 1 addition & 1 deletion Python/tdw/version.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = "1.12.24.2"
__version__ = "1.12.25.0"

0 comments on commit 35a6b69

Please sign in to comment.