Skip to content

Commit 35a6b69

Browse files
authored
Merge pull request #698 from threedworld-mit/set_ui_element_position
Added set_ui_element_position and a mask example controller
2 parents aced809 + b329e6c commit 35a6b69

File tree

8 files changed

+240
-1
lines changed

8 files changed

+240
-1
lines changed

Documentation/Changelog.md

+26
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,32 @@
44

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

7+
## v1.12.25
8+
9+
### Command API
10+
11+
#### New Commands
12+
13+
| Command | Description |
14+
| --- | --- |
15+
| `set_ui_element_position` | Set the position of a UI element. |
16+
17+
### `tdw` module
18+
19+
- Added: `ui.set_position(id, position)`.
20+
21+
### Example Controllers
22+
23+
- Added: `ui/mask.py`
24+
25+
### Documentation
26+
27+
#### Modified Documentation
28+
29+
| Document | Modification |
30+
| --- | --- |
31+
| `lessons/ui/ui.md` | Added a section describing how to create an image "mask".<br>Added a section describing how to move an image. |
32+
733
## v1.12.24
834

935
### Command API

Documentation/api/command_api.md

+22
Original file line numberDiff line numberDiff line change
@@ -844,6 +844,7 @@
844844
| --- | --- |
845845
| [`destroy_ui_element`](#destroy_ui_element) | Destroy a UI element in the scene. |
846846
| [`set_ui_color`](#set_ui_color) | Set the color of a UI image or text. |
847+
| [`set_ui_element_position`](#set_ui_element_position) | Set the position of a UI element. |
847848
| [`set_ui_element_size`](#set_ui_element_size) | Set the size of a UI element. |
848849
| [`set_ui_text`](#set_ui_text) | Set the text of a Text object that is already on the screen. |
849850

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

1106811069
***
1106911070

11071+
## **`set_ui_element_position`**
11072+
11073+
Set the position of a UI element.
11074+
11075+
11076+
```python
11077+
{"$type": "set_ui_element_position", "id": 1}
11078+
```
11079+
11080+
```python
11081+
{"$type": "set_ui_element_position", "id": 1, "position": {"x": 0, "y": 0}, "canvas_id": 0}
11082+
```
11083+
11084+
| Parameter | Type | Description | Default |
11085+
| --- | --- | --- | --- |
11086+
| `"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} |
11087+
| `"id"` | int | The unique ID of the UI element. | |
11088+
| `"canvas_id"` | int | The unique ID of the UI canvas. | 0 |
11089+
11090+
***
11091+
1107011092
## **`set_ui_element_size`**
1107111093

1107211094
Set the size of a UI element.
158 KB
Loading

Documentation/lessons/ui/ui.md

+101
Original file line numberDiff line numberDiff line change
@@ -232,6 +232,105 @@ Result:
232232

233233
![](images/image.jpg)
234234

235+
## Create a UI "mask"
236+
237+
Creating a UI mask is as simple as creating a new image and drawing a transparent shape:
238+
239+
```python
240+
from io import BytesIO
241+
from PIL import Image, ImageDraw
242+
243+
w = 512
244+
h = 512
245+
image = Image.new(mode="RGBA", size=(w, h), color=(0, 0, 0, 255))
246+
# Draw a circle on the mask.
247+
draw = ImageDraw.Draw(image)
248+
diameter = 256
249+
x = w // 2 - diameter // 2
250+
y = h // 2 - diameter // 2
251+
draw.ellipse([(x, y), (y + diameter, y + diameter)], fill=(0, 0, 0, 0))
252+
# Convert the PIL image to bytes.
253+
with BytesIO() as output:
254+
image.save(output, "PNG")
255+
mask = output.getvalue()
256+
# `mask` can now be used by `ui.add_image()`
257+
```
258+
259+
## Move a UI element
260+
261+
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).
262+
263+
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:
264+
265+
```python
266+
from io import BytesIO
267+
from PIL import Image, ImageDraw
268+
from tdw.controller import Controller
269+
from tdw.add_ons.third_person_camera import ThirdPersonCamera
270+
from tdw.add_ons.ui import UI
271+
from tdw.add_ons.image_capture import ImageCapture
272+
from tdw.backend.paths import EXAMPLE_CONTROLLER_OUTPUT_PATH
273+
274+
275+
c = Controller()
276+
# Add the UI add-on and the camera.
277+
camera = ThirdPersonCamera(position={"x": 0, "y": 0, "z": -1.2},
278+
avatar_id="a")
279+
ui = UI()
280+
c.add_ons.extend([camera, ui])
281+
ui.attach_canvas_to_avatar(avatar_id="a")
282+
screen_size = 512
283+
commands = [{"$type": "create_empty_environment"},
284+
{"$type": "set_screen_size",
285+
"width": screen_size,
286+
"height": screen_size}]
287+
# Add a cube slightly off-center.
288+
commands.extend(Controller.get_add_physics_object(model_name="cube",
289+
library="models_flex.json",
290+
object_id=0,
291+
position={"x": 0.25, "y": 0, "z": 1},
292+
rotation={"x": 30, "y": 10, "z": 0},
293+
kinematic=True))
294+
c.communicate(commands)
295+
296+
# Enable image capture.
297+
path = EXAMPLE_CONTROLLER_OUTPUT_PATH.joinpath("ui_mask")
298+
print(f"Images will be saved to: {path}")
299+
capture = ImageCapture(path=path, avatar_ids=["a"])
300+
c.add_ons.append(capture)
301+
302+
# Create the UI image with PIL.
303+
# The image is larger than the screen size so we can move it around.
304+
image_size = screen_size * 3
305+
image = Image.new(mode="RGBA", size=(image_size, image_size), color=(0, 0, 0, 255))
306+
# Draw a circle on the mask.
307+
draw = ImageDraw.Draw(image)
308+
diameter = 256
309+
d = image_size // 2 - diameter // 2
310+
draw.ellipse([(d, d), (d + diameter, d + diameter)], fill=(0, 0, 0, 0))
311+
# Convert the PIL image to bytes.
312+
with BytesIO() as output:
313+
image.save(output, "PNG")
314+
mask = output.getvalue()
315+
x = 0
316+
y = 0
317+
# Add the image.
318+
mask_id = ui.add_image(image=mask, position={"x": x, "y": y}, size={"x": image_size, "y": image_size}, raycast_target=False)
319+
c.communicate([])
320+
321+
# Move the image.
322+
for i in range(100):
323+
x += 4
324+
y += 3
325+
ui.set_position(ui_id=mask_id, position={"x": x, "y": y})
326+
c.communicate([])
327+
c.communicate({"$type": "terminate"})
328+
```
329+
330+
Result:
331+
332+
![](images/mask.gif)
333+
235334
## Destroy UI elements
236335

237336
Destroy a specific UI element via `ui.destroy(ui_id)`, which sends [`destroy_ui_element`](../../api/command_api.md#destroy_ui_element).
@@ -253,6 +352,7 @@ Example controllers:
253352
- [hello_world_ui.py](https://github.com/threedworld-mit/tdw/blob/master/Python/example_controllers/ui/hello_world_ui.py) Minimal UI example.
254353
- [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.
255354
- [image.py](https://github.com/threedworld-mit/tdw/blob/master/Python/example_controllers/ui/image.py) Add a UI image.
355+
- [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.
256356

257357
Python API:
258358

@@ -265,6 +365,7 @@ Command API:
265365
- [`add_ui_text`](../../api/command_api.md#add_ui_text)
266366
- [`add_ui_image`](../../api/command_api.md#add_ui_image)
267367
- [`set_ui_element_size`](../../api/command_api.md#set_ui_element_size)
368+
- [`set_ui_element_position`](../../api/command_api.md#set_ui_element_position)
268369
- [`set_target_framerate`](../../api/command_api.md#set_target_framerate)
269370
- [`destroy_ui_element`](../../api/command_api.md#destroy_ui_element)
270371
- [`destroy_ui_canvas`](../../api/command_api.md#destroy_ui_canvas)

Documentation/python/add_ons/ui.md

+11
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,17 @@ Attach the UI canvas to a VR rig.
169169
| --- | --- | --- | --- |
170170
| plane_distance | float | 0.25 | The distance from the camera to the UI canvas. |
171171

172+
#### set_position
173+
174+
**`self.set_position(ui_id, position)`**
175+
176+
Set the position of a UI element.
177+
178+
| Parameter | Type | Default | Description |
179+
| --- | --- | --- | --- |
180+
| ui_id | int | | The UI element's ID. |
181+
| position | Dict[str, float] | | The screen (pixel) position as a Vector2. Values must be integers. |
182+
172183
#### destroy
173184

174185
**`self.destroy(ui_id)`**

Python/example_controllers/ui/mask.py

+67
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
from io import BytesIO
2+
from PIL import Image, ImageDraw
3+
from tdw.controller import Controller
4+
from tdw.add_ons.third_person_camera import ThirdPersonCamera
5+
from tdw.add_ons.ui import UI
6+
from tdw.add_ons.image_capture import ImageCapture
7+
from tdw.backend.paths import EXAMPLE_CONTROLLER_OUTPUT_PATH
8+
9+
10+
"""
11+
Create black background with a circular "hole" in it and move the image around.
12+
"""
13+
14+
15+
c = Controller()
16+
# Add the UI add-on and the camera.
17+
camera = ThirdPersonCamera(position={"x": 0, "y": 0, "z": -1.2},
18+
avatar_id="a")
19+
ui = UI()
20+
c.add_ons.extend([camera, ui])
21+
ui.attach_canvas_to_avatar(avatar_id="a")
22+
screen_size = 512
23+
commands = [{"$type": "create_empty_environment"},
24+
{"$type": "set_screen_size",
25+
"width": screen_size,
26+
"height": screen_size}]
27+
# Add a cube slightly off-center.
28+
commands.extend(Controller.get_add_physics_object(model_name="cube",
29+
library="models_flex.json",
30+
object_id=0,
31+
position={"x": 0.25, "y": 0, "z": 1},
32+
rotation={"x": 30, "y": 10, "z": 0},
33+
kinematic=True))
34+
c.communicate(commands)
35+
36+
# Enable image capture.
37+
path = EXAMPLE_CONTROLLER_OUTPUT_PATH.joinpath("ui_mask")
38+
print(f"Images will be saved to: {path}")
39+
capture = ImageCapture(path=path, avatar_ids=["a"])
40+
c.add_ons.append(capture)
41+
42+
# Create the UI image with PIL.
43+
# The image is larger than the screen size so we can move it around.
44+
image_size = screen_size * 3
45+
image = Image.new(mode="RGBA", size=(image_size, image_size), color=(0, 0, 0, 255))
46+
# Draw a circle on the mask.
47+
draw = ImageDraw.Draw(image)
48+
diameter = 256
49+
d = image_size // 2 - diameter // 2
50+
draw.ellipse([(d, d), (d + diameter, d + diameter)], fill=(0, 0, 0, 0))
51+
# Convert the PIL image to bytes.
52+
with BytesIO() as output:
53+
image.save(output, "PNG")
54+
mask = output.getvalue()
55+
x = 0
56+
y = 0
57+
# Add the image.
58+
mask_id = ui.add_image(image=mask, position={"x": x, "y": y}, size={"x": image_size, "y": image_size}, raycast_target=False)
59+
c.communicate([])
60+
61+
# Move the image.
62+
for i in range(100):
63+
x += 4
64+
y += 3
65+
ui.set_position(ui_id=mask_id, position={"x": x, "y": y})
66+
c.communicate([])
67+
c.communicate({"$type": "terminate"})

Python/tdw/add_ons/ui.py

+12
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,18 @@ def attach_canvas_to_vr_rig(self, plane_distance: float = 0.25) -> None:
149149
self.commands.append({"$type": "attach_ui_canvas_to_vr_rig",
150150
"plane_distance": plane_distance})
151151

152+
def set_position(self, ui_id: int, position: Dict[str, float]) -> None:
153+
"""
154+
Set the position of a UI element.
155+
156+
:param ui_id: The UI element's ID.
157+
:param position: The screen (pixel) position as a Vector2. Values must be integers.
158+
"""
159+
160+
self.commands.append({"$type": "set_ui_element_position",
161+
"id": ui_id,
162+
"position": position})
163+
152164
def destroy(self, ui_id: int) -> None:
153165
"""
154166
Destroy a UI element.

Python/tdw/version.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
__version__ = "1.12.24.2"
1+
__version__ = "1.12.25.0"

0 commit comments

Comments
 (0)