Merge pull request #11 from Nitwel/testing
Update main to latest progress
This commit is contained in:
commit
9e2f46784d
12
.editorconfig
Normal file
12
.editorconfig
Normal file
|
@ -0,0 +1,12 @@
|
|||
# EditorConfig is awesome: https://EditorConfig.org
|
||||
|
||||
# top-most EditorConfig file
|
||||
root = true
|
||||
|
||||
[*]
|
||||
indent_style = tab
|
||||
indent_size = 4
|
||||
end_of_line = crlf
|
||||
charset = utf-8
|
||||
trim_trailing_whitespace = false
|
||||
insert_final_newline = false
|
2
.gitattributes
vendored
2
.gitattributes
vendored
|
@ -1,3 +1,3 @@
|
|||
# Normalize EOL for all files that Git considers text files.
|
||||
* text=auto eol=lf
|
||||
Assets/** filter=lfs diff=lfs merge=lfs -text
|
||||
assets/** filter=lfs diff=lfs merge=lfs -text
|
||||
|
|
95
README.md
95
README.md
|
@ -1,3 +1,94 @@
|
|||
# Immersive Home
|
||||
![logo](assets/banner.png)
|
||||
|
||||
Using Godot 4.1.2
|
||||
# 🏠 Introduction
|
||||
|
||||
Immersive Home is project to bring Smart Home and Mixed Reality technologies together for an intuitive and immersive experience.
|
||||
|
||||
## Features
|
||||
|
||||
- **Fast and Intuitive control over IoT devices**
|
||||
- **Live overview over your smart home**
|
||||
- **Simple way for creating automations**
|
||||
- **Comfortable way of consuming virtual media unobstructed**
|
||||
- **Advanced automations based on position in room**
|
||||
|
||||
## Supported Devices
|
||||
|
||||
**Smart Home Platforms**
|
||||
- [Home Assistant](https://www.home-assistant.io/)
|
||||
|
||||
**Mixed Reality Headsets**
|
||||
- Meta Quest 2 / Pro / 3
|
||||
|
||||
# 🛠 Development
|
||||
|
||||
In order to contribute to this project, you need the following to be setup before you can start working:
|
||||
- Godot 4.1.3 installed
|
||||
|
||||
## Fundamentals
|
||||
|
||||
Communication with the Smart Home Environment is done using the `HomeAdapters` global. Each environment is made up of devices and entities.
|
||||
A device is a collection of different entities and entities can represent many different things in a smart home.
|
||||
For example, the entity of name `lights.smart_lamp_1` would control the kitchen lamps while `state.smart_lamp_1_temp` would show the current temperature of the lamp.
|
||||
|
||||
### Home Adapters
|
||||
|
||||
The `HomeAdapters` global allows to communicate with different backends and offers a set of fundamental functions allowing communication with the Smart Home.
|
||||
|
||||
```python
|
||||
Device {
|
||||
"id": String,
|
||||
"name": String,
|
||||
"entities": Array[Entity]
|
||||
}
|
||||
|
||||
Entity {
|
||||
"state": String
|
||||
"attributes": Dictionary
|
||||
}
|
||||
|
||||
# Get a list of all devices
|
||||
func get_devices() -> Signal[Array[Device]]
|
||||
|
||||
# Get a single device by id
|
||||
func get_device(id: String) -> Signal[Device]
|
||||
|
||||
# Returns the current state of an entity.
|
||||
func get_state(entity: String) -> Signal[Entity]
|
||||
|
||||
# Updates the state of the entity and returns the resulting state
|
||||
func set_state(entity: String, state: String, attributes: Dictionary) -> Signal[Entity]
|
||||
|
||||
# Watches the state and each time it changes, calls the callback with the changed state, returns a function to stop watching the state
|
||||
func watch_state(entity: String, callback: Callable[entity: Entity]) -> Callable
|
||||
```
|
||||
|
||||
### Interaction Events
|
||||
|
||||
Each time a button is pressed on the primary controller, a raycast is done to be able to interact with devices or the UI.
|
||||
|
||||
**InteractionEvent**
|
||||
```js
|
||||
{
|
||||
position: Vector3,
|
||||
rotation: Vector3
|
||||
}
|
||||
```
|
||||
|
||||
| Function called | Args | Description |
|
||||
| -- | -- | -- |
|
||||
| `_click` | `[event: InteractionEvent]` | The back trigger button has been pressed and released |
|
||||
| `_dbl_click` | `[event: InteractionEvent]` | The back trigger button has been pressed and released twice in a row |
|
||||
| `_long_click` | `[event: InteractionEvent]` | The back trigger button has been pressed, then hold still for a short period, then released |
|
||||
| `_press_down` | `[event: InteractionEvent]` | The back trigger button has been pressed down |
|
||||
| `_press_move` | `[event: InteractionEvent]` | The back trigger button has been moved while pressed down |
|
||||
| `_press_up` | `[event: InteractionEvent]` | The back trigger button has been released |
|
||||
| `_grab_down` | `[event: InteractionEvent]` | The side grap button been pressed down |
|
||||
| `_grab_move` | `[event: InteractionEvent]` | The side grap button been pressed down |
|
||||
| `_grab_up` | `[event: InteractionEvent]` | The side grap button been released |
|
||||
|
||||
### Testing without a VR Headset
|
||||
|
||||
In order to test without a headset, press the run project (F5) button in Godot and ignore the prompt that OpenXR failed to start.
|
||||
To simulate the headset and controller movement, we're using the [XR Input Simulator](https://godotengine.org/asset-library/asset/1775) asset.
|
||||
Click at the link to get a list of the supported controlls.
|
||||
|
|
|
@ -17,6 +17,8 @@ operator = 2
|
|||
|
||||
[sub_resource type="VisualShader" id="VisualShader_wb0u4"]
|
||||
code = "shader_type spatial;
|
||||
render_mode blend_mix, depth_draw_opaque, cull_back, diffuse_lambert, specular_schlick_ggx;
|
||||
|
||||
uniform vec4 Color : source_color;
|
||||
|
||||
|
||||
|
|
|
@ -102,7 +102,7 @@ function = 12
|
|||
|
||||
[resource]
|
||||
code = "shader_type spatial;
|
||||
render_mode unshaded;
|
||||
render_mode blend_mix, depth_draw_opaque, cull_back, diffuse_lambert, specular_schlick_ggx, unshaded;
|
||||
|
||||
uniform vec4 albedo : source_color;
|
||||
uniform float value;
|
||||
|
|
|
@ -63,7 +63,7 @@ condition = 1
|
|||
|
||||
[resource]
|
||||
code = "shader_type spatial;
|
||||
render_mode unshaded;
|
||||
render_mode blend_mix, depth_draw_opaque, cull_back, diffuse_lambert, specular_schlick_ggx, unshaded;
|
||||
|
||||
uniform vec4 bar_color : source_color;
|
||||
uniform sampler2D bar_texture : source_color;
|
||||
|
|
|
@ -43,9 +43,9 @@ var webxr_auto_primary := 0
|
|||
|
||||
# Called when the node enters the scene tree for the first time.
|
||||
func _ready():
|
||||
var webxr_interface = XRServer.find_interface("WebXR")
|
||||
if webxr_interface:
|
||||
XRServer.tracker_added.connect(self._on_webxr_tracker_added)
|
||||
# var webxr_interface = XRServer.find_interface("WebXR")
|
||||
# if webxr_interface:
|
||||
# XRServer.tracker_added.connect(self._on_webxr_tracker_added)
|
||||
|
||||
_load()
|
||||
|
||||
|
|
21
addons/promise/LICENSE
Normal file
21
addons/promise/LICENSE
Normal file
|
@ -0,0 +1,21 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2023 TheWalruzz
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
165
addons/promise/promise.gd
Normal file
165
addons/promise/promise.gd
Normal file
|
@ -0,0 +1,165 @@
|
|||
extends RefCounted
|
||||
class_name Promise
|
||||
|
||||
|
||||
enum Status {
|
||||
RESOLVED,
|
||||
REJECTED
|
||||
}
|
||||
|
||||
|
||||
signal settled(status: PromiseResult)
|
||||
signal resolved(value: Variant)
|
||||
signal rejected(reason: Rejection)
|
||||
|
||||
|
||||
## Generic rejection reason
|
||||
const PROMISE_REJECTED := "Promise rejected"
|
||||
|
||||
|
||||
var is_settled := false
|
||||
|
||||
|
||||
func _init(callable: Callable):
|
||||
resolved.connect(
|
||||
func(value: Variant):
|
||||
is_settled = true
|
||||
settled.emit(PromiseResult.new(Status.RESOLVED, value)),
|
||||
CONNECT_ONE_SHOT
|
||||
)
|
||||
rejected.connect(
|
||||
func(rejection: Rejection):
|
||||
is_settled = true
|
||||
settled.emit(PromiseResult.new(Status.REJECTED, rejection)),
|
||||
CONNECT_ONE_SHOT
|
||||
)
|
||||
|
||||
callable.call_deferred(
|
||||
func(value: Variant):
|
||||
if not is_settled:
|
||||
resolved.emit(value),
|
||||
func(rejection: Rejection):
|
||||
if not is_settled:
|
||||
rejected.emit(rejection)
|
||||
)
|
||||
|
||||
|
||||
func then(resolved_callback: Callable) -> Promise:
|
||||
resolved.connect(
|
||||
resolved_callback,
|
||||
CONNECT_ONE_SHOT
|
||||
)
|
||||
return self
|
||||
|
||||
|
||||
func catch(rejected_callback: Callable) -> Promise:
|
||||
rejected.connect(
|
||||
rejected_callback,
|
||||
CONNECT_ONE_SHOT
|
||||
)
|
||||
return self
|
||||
|
||||
|
||||
static func from(input_signal: Signal) -> Promise:
|
||||
return Promise.new(
|
||||
func(resolve: Callable, _reject: Callable):
|
||||
var number_of_args := input_signal.get_object().get_signal_list() \
|
||||
.filter(func(signal_info: Dictionary) -> bool: return signal_info["name"] == input_signal.get_name()) \
|
||||
.map(func(signal_info: Dictionary) -> int: return signal_info["args"].size()) \
|
||||
.front() as int
|
||||
|
||||
if number_of_args == 0:
|
||||
await input_signal
|
||||
resolve.call(null)
|
||||
else:
|
||||
# only one arg in signal is allowed for now
|
||||
var result = await input_signal
|
||||
resolve.call(result)
|
||||
)
|
||||
|
||||
|
||||
static func from_many(input_signals: Array[Signal]) -> Array[Promise]:
|
||||
return input_signals.map(
|
||||
func(input_signal: Signal):
|
||||
return Promise.from(input_signal)
|
||||
)
|
||||
|
||||
|
||||
static func all(promises: Array[Promise]) -> Promise:
|
||||
return Promise.new(
|
||||
func(resolve: Callable, reject: Callable):
|
||||
var resolved_promises: Array[bool] = []
|
||||
var results := []
|
||||
results.resize(promises.size())
|
||||
resolved_promises.resize(promises.size())
|
||||
resolved_promises.fill(false)
|
||||
|
||||
for i in promises.size():
|
||||
promises[i].then(
|
||||
func(value: Variant):
|
||||
results[i] = value
|
||||
resolved_promises[i] = true
|
||||
if resolved_promises.all(func(value: bool): return value):
|
||||
resolve.call(results)
|
||||
).catch(
|
||||
func(rejection: Rejection):
|
||||
reject.call(rejection)
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
static func any(promises: Array[Promise]) -> Promise:
|
||||
return Promise.new(
|
||||
func(resolve: Callable, reject: Callable):
|
||||
var rejected_promises: Array[bool] = []
|
||||
var rejections: Array[Rejection] = []
|
||||
rejections.resize(promises.size())
|
||||
rejected_promises.resize(promises.size())
|
||||
rejected_promises.fill(false)
|
||||
|
||||
for i in promises.size():
|
||||
promises[i].then(
|
||||
func(value: Variant):
|
||||
resolve.call(value)
|
||||
).catch(
|
||||
func(rejection: Rejection):
|
||||
rejections[i] = rejection
|
||||
rejected_promises[i] = true
|
||||
if rejected_promises.all(func(value: bool): return value):
|
||||
reject.call(PromiseAnyRejection.new(PROMISE_REJECTED, rejections))
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
class PromiseResult:
|
||||
var status: Status
|
||||
var payload: Variant
|
||||
|
||||
func _init(_status: Status, _payload: Variant):
|
||||
status = _status
|
||||
payload = _payload
|
||||
|
||||
|
||||
class Rejection:
|
||||
var reason: String
|
||||
var stack: Array
|
||||
|
||||
func _init(_reason: String):
|
||||
reason = _reason
|
||||
stack = get_stack() if OS.is_debug_build() else []
|
||||
|
||||
|
||||
func as_string() -> String:
|
||||
return ("%s\n" % reason) + "\n".join(
|
||||
stack.map(
|
||||
func(dict: Dictionary) -> String:
|
||||
return "At %s:%i:%s" % [dict["source"], dict["line"], dict["function"]]
|
||||
))
|
||||
|
||||
|
||||
class PromiseAnyRejection extends Rejection:
|
||||
var group: Array[Rejection]
|
||||
|
||||
func _init(_reason: String, _group: Array[Rejection]):
|
||||
super(_reason)
|
||||
group = _group
|
242
addons/xr-simulator/XRSimulator.gd
Normal file
242
addons/xr-simulator/XRSimulator.gd
Normal file
|
@ -0,0 +1,242 @@
|
|||
extends Node
|
||||
|
||||
enum ControllerSelectionMode {Hold, Toggle}
|
||||
|
||||
@export var enabled: bool
|
||||
@export var disable_xr_in_editor: bool = true
|
||||
@export var controller_selection_mode: ControllerSelectionMode = ControllerSelectionMode.Hold
|
||||
@export var device_x_sensitivity: float = 1
|
||||
@export var device_y_sensitivity: float = 1
|
||||
@export var scroll_sensitivity: float = 1
|
||||
@export var is_camera_height_limited: bool = true
|
||||
@export var min_camera_height: float = 0.5
|
||||
@export var max_camera_height: float = 2.0
|
||||
@export var xr_origin: NodePath
|
||||
|
||||
var origin: XROrigin3D
|
||||
var camera: XRCamera3D
|
||||
var left_controller: XRController3D
|
||||
var right_controller: XRController3D
|
||||
var left_tracker: XRPositionalTracker
|
||||
var right_tracker: XRPositionalTracker
|
||||
|
||||
var toggle_left_controller = false
|
||||
var toggle_right_controller = false
|
||||
var toggle_shift = false
|
||||
|
||||
var key_map = {
|
||||
KEY_1: "by_button",
|
||||
KEY_2: "ax_button",
|
||||
KEY_3: "by_touch",
|
||||
KEY_4: "ax_touch",
|
||||
KEY_5: "trigger_touch",
|
||||
KEY_6: "grip_touch",
|
||||
KEY_7: "secondary_click",
|
||||
KEY_8: "secondary_touch",
|
||||
KEY_9: "",
|
||||
KEY_0: "",
|
||||
KEY_MINUS: "primary_click",
|
||||
KEY_EQUAL: "primary_touch",
|
||||
KEY_BACKSPACE: "",
|
||||
KEY_ENTER: "menu_button"
|
||||
}
|
||||
|
||||
@onready var viewport: Viewport = get_viewport()
|
||||
|
||||
func _ready():
|
||||
if not enabled or not OS.has_feature("editor"):
|
||||
enabled = false
|
||||
return
|
||||
|
||||
Input.mouse_mode = Input.MOUSE_MODE_CAPTURED
|
||||
|
||||
origin = get_node(xr_origin)
|
||||
|
||||
camera = origin.get_node("XRCamera3D")
|
||||
|
||||
var left_hand = XRServer.get_tracker("left_hand")
|
||||
if left_hand == null:
|
||||
left_tracker = XRPositionalTracker.new()
|
||||
left_tracker.type = XRServer.TRACKER_CONTROLLER
|
||||
left_tracker.hand = XRPositionalTracker.TRACKER_HAND_LEFT
|
||||
left_tracker.name = "left_hand"
|
||||
else:
|
||||
left_tracker = left_hand
|
||||
|
||||
var right_hand = XRServer.get_tracker("right_hand")
|
||||
if right_hand == null:
|
||||
right_tracker = XRPositionalTracker.new()
|
||||
right_tracker.type = XRServer.TRACKER_CONTROLLER
|
||||
right_tracker.hand = XRPositionalTracker.TRACKER_HAND_RIGHT
|
||||
right_tracker.name = "right_hand"
|
||||
else:
|
||||
right_tracker = right_hand
|
||||
|
||||
for child in origin.get_children():
|
||||
if child.get("tracker"):
|
||||
var pose = child.pose
|
||||
if child.tracker == "left_hand":
|
||||
left_controller = child
|
||||
left_tracker.set_pose(pose, child.transform, Vector3.ZERO, Vector3.ZERO, XRPose.XR_TRACKING_CONFIDENCE_HIGH)
|
||||
XRServer.add_tracker(left_tracker)
|
||||
elif child.tracker == "right_hand":
|
||||
right_controller = child
|
||||
right_tracker.set_pose(pose, child.transform, Vector3.ZERO, Vector3.ZERO, XRPose.XR_TRACKING_CONFIDENCE_HIGH)
|
||||
XRServer.add_tracker(right_tracker)
|
||||
|
||||
|
||||
func _process(_delta):
|
||||
if enabled and disable_xr_in_editor and OS.has_feature("editor") and viewport.use_xr:
|
||||
viewport.use_xr = false
|
||||
|
||||
func _input(event):
|
||||
if not enabled or not origin.current or not OS.has_feature("editor"):
|
||||
return
|
||||
if Input.is_key_pressed(KEY_ESCAPE):
|
||||
Input.mouse_mode = Input.MOUSE_MODE_VISIBLE
|
||||
elif Input.mouse_mode != Input.MOUSE_MODE_CAPTURED and event is InputEventMouseButton:
|
||||
Input.mouse_mode = Input.MOUSE_MODE_CAPTURED
|
||||
|
||||
if Input.mouse_mode != Input.MOUSE_MODE_CAPTURED:
|
||||
return
|
||||
|
||||
simulate_joysticks()
|
||||
var is_any_controller_selected = false
|
||||
if event is InputEventMouseMotion:
|
||||
if Input.is_physical_key_pressed(KEY_Q) or toggle_left_controller:
|
||||
is_any_controller_selected = true
|
||||
if Input.is_key_pressed(KEY_SHIFT):
|
||||
rotate_device(event, left_controller)
|
||||
else:
|
||||
move_controller(event, left_controller)
|
||||
if Input.is_physical_key_pressed(KEY_E) or toggle_right_controller:
|
||||
is_any_controller_selected = true
|
||||
if Input.is_key_pressed(KEY_SHIFT):
|
||||
rotate_device(event, right_controller)
|
||||
else:
|
||||
move_controller(event, right_controller)
|
||||
if not is_any_controller_selected:
|
||||
rotate_device(event, camera)
|
||||
elif event is InputEventMouseButton:
|
||||
if Input.is_physical_key_pressed(KEY_Q) or toggle_left_controller:
|
||||
is_any_controller_selected = true
|
||||
attract_controller(event, left_controller)
|
||||
simulate_trigger(event, left_controller)
|
||||
simulate_grip(event, left_controller)
|
||||
if Input.is_physical_key_pressed(KEY_E) or toggle_right_controller:
|
||||
is_any_controller_selected = true
|
||||
attract_controller(event, right_controller)
|
||||
simulate_trigger(event, right_controller)
|
||||
simulate_grip(event, right_controller)
|
||||
if not is_any_controller_selected:
|
||||
camera_height(event)
|
||||
elif event is InputEventKey:
|
||||
if controller_selection_mode == ControllerSelectionMode.Toggle and event.pressed:
|
||||
if event.keycode == KEY_Q:
|
||||
toggle_left_controller = !toggle_left_controller
|
||||
elif event.keycode == KEY_E:
|
||||
toggle_right_controller = !toggle_right_controller
|
||||
|
||||
if Input.is_physical_key_pressed(KEY_Q) or toggle_left_controller:
|
||||
simulate_buttons(event, left_controller)
|
||||
if Input.is_physical_key_pressed(KEY_E) or toggle_right_controller:
|
||||
simulate_buttons(event, right_controller)
|
||||
|
||||
func camera_height(event: InputEventMouseButton):
|
||||
var direction = -1
|
||||
|
||||
if not event.pressed:
|
||||
return
|
||||
|
||||
if event.button_index == MOUSE_BUTTON_WHEEL_UP:
|
||||
direction = 1
|
||||
elif event.button_index != MOUSE_BUTTON_WHEEL_DOWN:
|
||||
return
|
||||
|
||||
var pos = camera.transform.origin
|
||||
var camera_y = pos.y + (scroll_sensitivity * direction)/20
|
||||
if (camera_y >= max_camera_height or camera_y <= min_camera_height) and is_camera_height_limited:
|
||||
camera_y = pos.y
|
||||
camera.transform.origin = Vector3(pos.x, camera_y , pos.z)
|
||||
|
||||
func simulate_joysticks():
|
||||
var vec_left = vector_key_mapping(KEY_D, KEY_A, KEY_W, KEY_S)
|
||||
left_tracker.set_input("primary", vec_left)
|
||||
|
||||
var vec_right = vector_key_mapping(KEY_RIGHT, KEY_LEFT, KEY_UP, KEY_DOWN)
|
||||
|
||||
right_tracker.set_input("primary", vec_right)
|
||||
|
||||
func simulate_trigger(event: InputEventMouseButton, controller: XRController3D):
|
||||
if event.button_index == MOUSE_BUTTON_LEFT:
|
||||
if controller.tracker == "left_hand":
|
||||
left_tracker.set_input("trigger", float(event.pressed))
|
||||
left_tracker.set_input("trigger_click", event.pressed)
|
||||
else:
|
||||
right_tracker.set_input("trigger", float(event.pressed))
|
||||
right_tracker.set_input("trigger_click", event.pressed)
|
||||
|
||||
func simulate_grip(event: InputEventMouseButton, controller: XRController3D):
|
||||
if event.button_index == MOUSE_BUTTON_RIGHT:
|
||||
if controller.tracker == "left_hand":
|
||||
left_tracker.set_input("grip", float(event.pressed))
|
||||
left_tracker.set_input("grip_click", event.pressed)
|
||||
else:
|
||||
right_tracker.set_input("grip", float(event.pressed))
|
||||
right_tracker.set_input("grip_click", event.pressed)
|
||||
|
||||
func simulate_buttons(event: InputEventKey, controller: XRController3D):
|
||||
if key_map.has(event.keycode):
|
||||
var button = key_map[event.keycode]
|
||||
if controller.tracker == "left_hand":
|
||||
left_tracker.set_input(button, event.pressed)
|
||||
else:
|
||||
right_tracker.set_input(button, event.pressed)
|
||||
|
||||
func move_controller(event: InputEventMouseMotion, controller: XRController3D):
|
||||
var movement = Vector3()
|
||||
movement += camera.global_transform.basis.x * event.relative.x * device_x_sensitivity/1000
|
||||
movement += camera.global_transform.basis.y * event.relative.y * -device_y_sensitivity/1000
|
||||
controller.global_translate(movement)
|
||||
|
||||
func attract_controller(event: InputEventMouseButton, controller: XRController3D):
|
||||
var direction = -1
|
||||
|
||||
if not event.pressed:
|
||||
return
|
||||
|
||||
if event.button_index == MOUSE_BUTTON_WHEEL_UP:
|
||||
direction = 1
|
||||
elif event.button_index != MOUSE_BUTTON_WHEEL_DOWN:
|
||||
return
|
||||
|
||||
var distance_vector = controller.global_transform.origin - camera.global_transform.origin
|
||||
var forward = distance_vector.normalized() * direction
|
||||
var movement = distance_vector + forward * (scroll_sensitivity/20)
|
||||
if distance_vector.length() > 0.1 and movement.length() > 0.1:
|
||||
controller.global_translate(forward * (scroll_sensitivity/20))
|
||||
|
||||
func rotate_device(event: InputEventMouseMotion, device: Node3D):
|
||||
var motion = event.relative
|
||||
device.rotate_y(motion.x * -device_x_sensitivity/1000)
|
||||
device.rotate(device.transform.basis.x, motion.y * -device_y_sensitivity/1000)
|
||||
|
||||
func vector_key_mapping(key_positive_x: int, key_negative_x: int, key_positive_y: int, key_negative_y: int):
|
||||
var x = 0
|
||||
var y = 0
|
||||
if Input.is_physical_key_pressed (key_positive_y):
|
||||
y = 1
|
||||
elif Input.is_physical_key_pressed (key_negative_y):
|
||||
y = -1
|
||||
|
||||
if Input.is_physical_key_pressed (key_positive_x):
|
||||
x = 1
|
||||
elif Input.is_physical_key_pressed (key_negative_x):
|
||||
x = -1
|
||||
|
||||
var vec = Vector2(x, y)
|
||||
|
||||
if vec:
|
||||
vec = vec.normalized()
|
||||
|
||||
return vec
|
7
addons/xr-simulator/XRSimulator.tscn
Normal file
7
addons/xr-simulator/XRSimulator.tscn
Normal file
|
@ -0,0 +1,7 @@
|
|||
[gd_scene load_steps=2 format=3 uid="uid://ctltchlf2j2r4"]
|
||||
|
||||
[ext_resource type="Script" path="res://addons/xr-simulator/XRSimulator.gd" id="1_uljju"]
|
||||
|
||||
[node name="XRSimulator" type="Node"]
|
||||
script = ExtResource("1_uljju")
|
||||
enabled = true
|
3
assets/banner.png
Normal file
3
assets/banner.png
Normal file
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:f70162100261f12060f1c82a27d6bc547a465c4f401c7914b3b2d0ed51d78f81
|
||||
size 431818
|
3
assets/banner.png.import
Normal file
3
assets/banner.png.import
Normal file
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:1d859d4114970ee061de56a0c9e108deddaac675613b45ea1c6ac09a6225d63e
|
||||
size 759
|
3
assets/design.afdesign
Normal file
3
assets/design.afdesign
Normal file
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:c48298302f3b19b61742398aa55b7e554074f72632cee50db3cde6696d60cdaa
|
||||
size 3305459
|
3
assets/logo.png
Normal file
3
assets/logo.png
Normal file
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:b5d79d4a0c78bcce53fb01fa0353ec971eca5f6f8f3fb1fa7322f18f87a4781b
|
||||
size 1122652
|
3
assets/logo.png.import
Normal file
3
assets/logo.png.import
Normal file
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:1daf27643292c3a222f2a9e60dfe38db5e62963cbfa2dd313588aa98f4e053b4
|
||||
size 753
|
3
assets/materials/interface.tres
Normal file
3
assets/materials/interface.tres
Normal file
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:55d5f30db336a8f8f7633743c23e41077d5c1a1b900c475a6d3811746eba3d1b
|
||||
size 141
|
3
assets/materials/sky.material
Normal file
3
assets/materials/sky.material
Normal file
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:25ef12c9534c8bbb08a816492562253c5c6fe0257f87427ce77b223231b07de5
|
||||
size 299
|
3
assets/materials/sky_passthrough.material
Normal file
3
assets/materials/sky_passthrough.material
Normal file
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:eead3733b7242e93c1739fcebd649efd1ce56ad1a3934e69d850c6ef3df66b9c
|
||||
size 351
|
3
assets/materials/swich_on.png
Normal file
3
assets/materials/swich_on.png
Normal file
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:06c7745de9d5236d8096979e16708530a3c61112b604a7603033ea8bc9097791
|
||||
size 258636
|
3
assets/materials/swich_on.png.import
Normal file
3
assets/materials/swich_on.png.import
Normal file
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:b936cb0718d6b36ba9070d10fe2a16b2dbff0a4f9649e80205e3218fdb4b9df9
|
||||
size 1007
|
3
assets/materials/switch_off.png
Normal file
3
assets/materials/switch_off.png
Normal file
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:3fa30c33f89997925bc7164245d51209879a912597a72b227b861f9a9841acec
|
||||
size 249715
|
3
assets/materials/switch_off.png.import
Normal file
3
assets/materials/switch_off.png.import
Normal file
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:71e68850377c2368a7433084c550cc7c5fa5110184cf59fc6d7a8f77f5f4af89
|
||||
size 1017
|
1
icon.svg
1
icon.svg
|
@ -1 +0,0 @@
|
|||
<svg height="128" width="128" xmlns="http://www.w3.org/2000/svg"><rect x="2" y="2" width="124" height="124" rx="14" fill="#363d52" stroke="#212532" stroke-width="4"/><g transform="scale(.101) translate(122 122)"><g fill="#fff"><path d="M105 673v33q407 354 814 0v-33z"/><path fill="#478cbf" d="m105 673 152 14q12 1 15 14l4 67 132 10 8-61q2-11 15-15h162q13 4 15 15l8 61 132-10 4-67q3-13 15-14l152-14V427q30-39 56-81-35-59-83-108-43 20-82 47-40-37-88-64 7-51 8-102-59-28-123-42-26 43-46 89-49-7-98 0-20-46-46-89-64 14-123 42 1 51 8 102-48 27-88 64-39-27-82-47-48 49-83 108 26 42 56 81zm0 33v39c0 276 813 276 813 0v-39l-134 12-5 69q-2 10-14 13l-162 11q-12 0-16-11l-10-65H447l-10 65q-4 11-16 11l-162-11q-12-3-14-13l-5-69z"/><path d="M483 600c3 34 55 34 58 0v-86c-3-34-55-34-58 0z"/><circle cx="725" cy="526" r="90"/><circle cx="299" cy="526" r="90"/></g><g fill="#414042"><circle cx="307" cy="532" r="60"/><circle cx="717" cy="532" r="60"/></g></g></svg>
|
Before Width: | Height: | Size: 950 B |
|
@ -1,37 +0,0 @@
|
|||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://bnc4gf261swvs"
|
||||
path="res://.godot/imported/icon.svg-218a8f2b3041327d8a5756f3a245f83b.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://icon.svg"
|
||||
dest_files=["res://.godot/imported/icon.svg-218a8f2b3041327d8a5756f3a245f83b.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=0
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
mipmaps/generate=false
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=0
|
||||
roughness/src_normal=""
|
||||
process/fix_alpha_border=true
|
||||
process/premult_alpha=false
|
||||
process/normal_map_invert_y=false
|
||||
process/hdr_as_srgb=false
|
||||
process/hdr_clamp_exposure=false
|
||||
process/size_limit=0
|
||||
detect_3d/compress_to=1
|
||||
svg/scale=1.0
|
||||
editor/scale_with_editor_scale=false
|
||||
editor/convert_colors_with_editor_theme=false
|
11
main.gd
Normal file
11
main.gd
Normal file
|
@ -0,0 +1,11 @@
|
|||
extends Node3D
|
||||
|
||||
var sky = preload("res://assets/materials/sky.material")
|
||||
var sky_passthrough = preload("res://assets/materials/sky_passthrough.material")
|
||||
|
||||
@onready var environment: WorldEnvironment = $WorldEnvironment
|
||||
|
||||
func _ready():
|
||||
# In case we're running on the headset, use the passthrough sky
|
||||
if OS.get_name() == "Android":
|
||||
environment.environment.sky.set_material(sky_passthrough)
|
73
main.tscn
73
main.tscn
|
@ -1,29 +1,78 @@
|
|||
[gd_scene load_steps=4 format=3 uid="uid://18sldbn0hij8"]
|
||||
[gd_scene load_steps=12 format=3 uid="uid://eecv28y6jxk4"]
|
||||
|
||||
[ext_resource type="PackedScene" uid="uid://b4kad2kuba1yn" path="res://addons/godot-xr-tools/hands/scenes/lowpoly/left_hand_low.tscn" id="1_66jmx"]
|
||||
[ext_resource type="PackedScene" uid="uid://l2n30mpbkdyw" path="res://addons/godot-xr-tools/hands/scenes/lowpoly/right_hand_low.tscn" id="2_3f5tl"]
|
||||
[ext_resource type="PackedScene" uid="uid://clc5dre31iskm" path="res://addons/godot-xr-tools/xr/start_xr.tscn" id="3_iaq1p"]
|
||||
[ext_resource type="PackedScene" uid="uid://clc5dre31iskm" path="res://addons/godot-xr-tools/xr/start_xr.tscn" id="1_i4c04"]
|
||||
[ext_resource type="Script" path="res://src/raycast.gd" id="1_tsqxc"]
|
||||
[ext_resource type="Script" path="res://main.gd" id="1_uvrd4"]
|
||||
[ext_resource type="Script" path="res://src/model.gd" id="2_7f1x4"]
|
||||
[ext_resource type="PackedScene" uid="uid://c3kdssrmv84kv" path="res://scenes/menu.tscn" id="3_1tbp3"]
|
||||
[ext_resource type="PackedScene" uid="uid://ctltchlf2j2r4" path="res://addons/xr-simulator/XRSimulator.tscn" id="5_3qc8g"]
|
||||
[ext_resource type="Material" uid="uid://bf5ina366dwm6" path="res://assets/materials/sky.material" id="5_wgwf8"]
|
||||
|
||||
[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_m58yb"]
|
||||
ao_enabled = true
|
||||
|
||||
[sub_resource type="BoxMesh" id="BoxMesh_ir3co"]
|
||||
material = SubResource("StandardMaterial3D_m58yb")
|
||||
size = Vector3(0.01, 0.01, 0.01)
|
||||
|
||||
[sub_resource type="Sky" id="Sky_vhymk"]
|
||||
sky_material = ExtResource("5_wgwf8")
|
||||
|
||||
[sub_resource type="Environment" id="Environment_7ghp0"]
|
||||
background_mode = 2
|
||||
background_color = Color(0.466667, 0.47451, 0.462745, 0)
|
||||
sky = SubResource("Sky_vhymk")
|
||||
ambient_light_color = Color(1, 1, 1, 1)
|
||||
ambient_light_sky_contribution = 0.72
|
||||
|
||||
[node name="Main" type="Node3D"]
|
||||
transform = Transform3D(1, -0.000296142, 0.000270963, 0.000296143, 1, -4.61078e-06, -0.000270962, 4.67014e-06, 1, 0, 0, 0)
|
||||
script = ExtResource("1_uvrd4")
|
||||
|
||||
[node name="XROrigin3D" type="XROrigin3D" parent="."]
|
||||
|
||||
[node name="XRCamera3D" type="XRCamera3D" parent="XROrigin3D"]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.6555, 0)
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.798091, 0.311748)
|
||||
|
||||
[node name="LeftHand" type="XRController3D" parent="XROrigin3D"]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -0.341606, 0.506389, 0)
|
||||
[node name="XRControllerLeft" type="XRController3D" parent="XROrigin3D"]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -0.469893, 0.597213, -0.251112)
|
||||
tracker = &"left_hand"
|
||||
pose = &"aim"
|
||||
|
||||
[node name="LeftHand" parent="XROrigin3D/LeftHand" instance=ExtResource("1_66jmx")]
|
||||
[node name="MeshInstance3D" type="MeshInstance3D" parent="XROrigin3D/XRControllerLeft"]
|
||||
mesh = SubResource("BoxMesh_ir3co")
|
||||
|
||||
[node name="RightHand" type="XRController3D" parent="XROrigin3D"]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0.408271, 0.538159, 0)
|
||||
[node name="Model" type="Node3D" parent="XROrigin3D/XRControllerLeft"]
|
||||
script = ExtResource("2_7f1x4")
|
||||
|
||||
[node name="Menu" parent="XROrigin3D/XRControllerLeft" instance=ExtResource("3_1tbp3")]
|
||||
transform = Transform3D(-4.37114e-08, 0, -1, -0.707107, 0.707107, 3.09086e-08, 0.707107, 0.707107, -3.09086e-08, 0.183517, 0, -0.0534939)
|
||||
|
||||
[node name="XRControllerRight" type="XRController3D" parent="XROrigin3D"]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0.488349, 0.559219, -0.2988)
|
||||
tracker = &"right_hand"
|
||||
pose = &"aim"
|
||||
|
||||
[node name="RightHand" parent="XROrigin3D/RightHand" instance=ExtResource("2_3f5tl")]
|
||||
[node name="MeshInstance3D" type="MeshInstance3D" parent="XROrigin3D/XRControllerRight"]
|
||||
mesh = SubResource("BoxMesh_ir3co")
|
||||
|
||||
[node name="StartXR" parent="." instance=ExtResource("3_iaq1p")]
|
||||
[node name="Raycast" type="Node3D" parent="XROrigin3D/XRControllerRight" node_paths=PackedStringArray("ray")]
|
||||
script = ExtResource("1_tsqxc")
|
||||
ray = NodePath("RayCast3D")
|
||||
|
||||
[node name="RayCast3D" type="RayCast3D" parent="XROrigin3D/XRControllerRight/Raycast"]
|
||||
transform = Transform3D(1.91069e-15, 4.37114e-08, 1, 1, -4.37114e-08, 0, 4.37114e-08, 1, -4.37114e-08, 0, 0, 0)
|
||||
target_position = Vector3(0, -5, 0)
|
||||
|
||||
[node name="StartXR" parent="." instance=ExtResource("1_i4c04")]
|
||||
enable_passthrough = true
|
||||
|
||||
[node name="WorldEnvironment" type="WorldEnvironment" parent="."]
|
||||
environment = SubResource("Environment_7ghp0")
|
||||
|
||||
[node name="DirectionalLight3D" type="DirectionalLight3D" parent="."]
|
||||
transform = Transform3D(0.834925, -0.386727, -0.39159, 0.550364, 0.586681, 0.594058, 0, -0.711511, 0.702675, 0, 7.21041, 2.06458)
|
||||
shadow_enabled = true
|
||||
|
||||
[node name="XRSimulator" parent="." instance=ExtResource("5_3qc8g")]
|
||||
xr_origin = NodePath("../XROrigin3D")
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -11,13 +11,25 @@ config_version=5
|
|||
[application]
|
||||
|
||||
config/name="ImmersiveHome"
|
||||
run/main_scene="res://main.tscn"
|
||||
config/features=PackedStringArray("4.1", "Mobile")
|
||||
config/icon="res://icon.svg"
|
||||
|
||||
[autoload]
|
||||
|
||||
XRToolsUserSettings="*res://addons/godot-xr-tools/user_settings/user_settings.gd"
|
||||
Request="*res://src/globals/request.gd"
|
||||
HomeAdapters="*res://src/globals/home_adapters.gd"
|
||||
|
||||
[editor_plugins]
|
||||
|
||||
enabled=PackedStringArray("res://addons/godot-xr-tools/plugin.cfg")
|
||||
|
||||
[rendering]
|
||||
|
||||
renderer/rendering_method="mobile"
|
||||
textures/vram_compression/import_etc2_astc=true
|
||||
lights_and_shadows/directional_shadow/soft_shadow_filter_quality=4
|
||||
lights_and_shadows/directional_shadow/soft_shadow_filter_quality.mobile=4
|
||||
|
||||
[xr]
|
||||
|
||||
|
|
23
scenes/device.tscn
Normal file
23
scenes/device.tscn
Normal file
|
@ -0,0 +1,23 @@
|
|||
[gd_scene load_steps=4 format=3 uid="uid://dbe8slnyhro2n"]
|
||||
|
||||
[ext_resource type="Script" path="res://src/ui/device.gd" id="1_rbo86"]
|
||||
|
||||
[sub_resource type="BoxMesh" id="BoxMesh_aa3i4"]
|
||||
size = Vector3(0.05, 0.01, 0.05)
|
||||
|
||||
[sub_resource type="BoxShape3D" id="BoxShape3D_28fjq"]
|
||||
size = Vector3(0.05, 0.01, 0.05)
|
||||
|
||||
[node name="Device" type="StaticBody3D"]
|
||||
script = ExtResource("1_rbo86")
|
||||
|
||||
[node name="MeshInstance3D" type="MeshInstance3D" parent="."]
|
||||
mesh = SubResource("BoxMesh_aa3i4")
|
||||
|
||||
[node name="CollisionShape3D" type="CollisionShape3D" parent="."]
|
||||
shape = SubResource("BoxShape3D_28fjq")
|
||||
|
||||
[node name="Label" type="Label3D" parent="."]
|
||||
transform = Transform3D(-2.18557e-09, -0.05, -2.18557e-09, 0, -2.18557e-09, 0.05, -0.05, 2.18557e-09, 9.55343e-17, 0, 0.00918245, 0)
|
||||
text = "Text"
|
||||
autowrap_mode = 3
|
33
scenes/entities/light.tscn
Normal file
33
scenes/entities/light.tscn
Normal file
|
@ -0,0 +1,33 @@
|
|||
[gd_scene load_steps=6 format=3 uid="uid://cw86rc42dv2d8"]
|
||||
|
||||
[ext_resource type="Script" path="res://src/entities/light.gd" id="1_ykxy3"]
|
||||
[ext_resource type="Texture2D" uid="uid://b72vsbcvqqxg7" path="res://assets/materials/swich_on.png" id="2_6gn2e"]
|
||||
[ext_resource type="Texture2D" uid="uid://cvc0o6dsktnvl" path="res://assets/materials/switch_off.png" id="3_qlm62"]
|
||||
|
||||
[sub_resource type="SphereShape3D" id="SphereShape3D_ukj14"]
|
||||
radius = 0.1
|
||||
|
||||
[sub_resource type="SpriteFrames" id="SpriteFrames_ldpuo"]
|
||||
animations = [{
|
||||
"frames": [{
|
||||
"duration": 1.0,
|
||||
"texture": ExtResource("2_6gn2e")
|
||||
}, {
|
||||
"duration": 1.0,
|
||||
"texture": ExtResource("3_qlm62")
|
||||
}],
|
||||
"loop": true,
|
||||
"name": &"default",
|
||||
"speed": 5.0
|
||||
}]
|
||||
|
||||
[node name="Light" type="StaticBody3D"]
|
||||
script = ExtResource("1_ykxy3")
|
||||
|
||||
[node name="CollisionShape3D" type="CollisionShape3D" parent="."]
|
||||
shape = SubResource("SphereShape3D_ukj14")
|
||||
|
||||
[node name="Icon" type="AnimatedSprite3D" parent="."]
|
||||
pixel_size = 0.0005
|
||||
billboard = 1
|
||||
sprite_frames = SubResource("SpriteFrames_ldpuo")
|
15
scenes/entities/sensor.tscn
Normal file
15
scenes/entities/sensor.tscn
Normal file
|
@ -0,0 +1,15 @@
|
|||
[gd_scene load_steps=3 format=3 uid="uid://xsiy71rsqulj"]
|
||||
|
||||
[ext_resource type="Script" path="res://src/entities/sensor.gd" id="1_57ac8"]
|
||||
|
||||
[sub_resource type="SphereShape3D" id="SphereShape3D_r20gc"]
|
||||
radius = 0.1
|
||||
|
||||
[node name="Sensor" type="StaticBody3D"]
|
||||
script = ExtResource("1_57ac8")
|
||||
|
||||
[node name="CollisionShape3D" type="CollisionShape3D" parent="."]
|
||||
shape = SubResource("SphereShape3D_r20gc")
|
||||
|
||||
[node name="Label" type="Label3D" parent="."]
|
||||
text = "some text"
|
33
scenes/entities/switch.tscn
Normal file
33
scenes/entities/switch.tscn
Normal file
|
@ -0,0 +1,33 @@
|
|||
[gd_scene load_steps=6 format=3 uid="uid://cscl5k7lhopj5"]
|
||||
|
||||
[ext_resource type="Script" path="res://src/entities/switch.gd" id="1_8ffhi"]
|
||||
[ext_resource type="Texture2D" uid="uid://b72vsbcvqqxg7" path="res://assets/materials/swich_on.png" id="1_w68gw"]
|
||||
[ext_resource type="Texture2D" uid="uid://cvc0o6dsktnvl" path="res://assets/materials/switch_off.png" id="2_86ba1"]
|
||||
|
||||
[sub_resource type="SphereShape3D" id="SphereShape3D_ukj14"]
|
||||
radius = 0.1
|
||||
|
||||
[sub_resource type="SpriteFrames" id="SpriteFrames_ldpuo"]
|
||||
animations = [{
|
||||
"frames": [{
|
||||
"duration": 1.0,
|
||||
"texture": ExtResource("1_w68gw")
|
||||
}, {
|
||||
"duration": 1.0,
|
||||
"texture": ExtResource("2_86ba1")
|
||||
}],
|
||||
"loop": true,
|
||||
"name": &"default",
|
||||
"speed": 5.0
|
||||
}]
|
||||
|
||||
[node name="Switch" type="StaticBody3D"]
|
||||
script = ExtResource("1_8ffhi")
|
||||
|
||||
[node name="CollisionShape3D" type="CollisionShape3D" parent="."]
|
||||
shape = SubResource("SphereShape3D_ukj14")
|
||||
|
||||
[node name="Icon" type="AnimatedSprite3D" parent="."]
|
||||
pixel_size = 0.0005
|
||||
billboard = 1
|
||||
sprite_frames = SubResource("SpriteFrames_ldpuo")
|
23
scenes/entity.tscn
Normal file
23
scenes/entity.tscn
Normal file
|
@ -0,0 +1,23 @@
|
|||
[gd_scene load_steps=4 format=3 uid="uid://xo0o5nrfjl23"]
|
||||
|
||||
[ext_resource type="Script" path="res://src/ui/entity.gd" id="1_825oj"]
|
||||
|
||||
[sub_resource type="BoxMesh" id="BoxMesh_aa3i4"]
|
||||
size = Vector3(0.05, 0.01, 0.05)
|
||||
|
||||
[sub_resource type="BoxShape3D" id="BoxShape3D_28fjq"]
|
||||
size = Vector3(0.05, 0.01, 0.05)
|
||||
|
||||
[node name="Entity" type="StaticBody3D"]
|
||||
script = ExtResource("1_825oj")
|
||||
|
||||
[node name="MeshInstance3D" type="MeshInstance3D" parent="."]
|
||||
mesh = SubResource("BoxMesh_aa3i4")
|
||||
|
||||
[node name="CollisionShape3D" type="CollisionShape3D" parent="."]
|
||||
shape = SubResource("BoxShape3D_28fjq")
|
||||
|
||||
[node name="Label" type="Label3D" parent="."]
|
||||
transform = Transform3D(-2.18557e-09, -0.05, -2.18557e-09, 0, -2.18557e-09, 0.05, -0.05, 2.18557e-09, 9.55343e-17, 0, 0.00918245, 0)
|
||||
text = "Text"
|
||||
autowrap_mode = 3
|
17
scenes/menu.tscn
Normal file
17
scenes/menu.tscn
Normal file
|
@ -0,0 +1,17 @@
|
|||
[gd_scene load_steps=4 format=3 uid="uid://c3kdssrmv84kv"]
|
||||
|
||||
[ext_resource type="Script" path="res://src/menu.gd" id="1_ng4u3"]
|
||||
[ext_resource type="Material" uid="uid://bertj8bp8b5l1" path="res://assets/materials/interface.tres" id="2_nsukb"]
|
||||
|
||||
[sub_resource type="PlaneMesh" id="PlaneMesh_6t3dn"]
|
||||
material = ExtResource("2_nsukb")
|
||||
size = Vector2(0.3, 0.3)
|
||||
|
||||
[node name="Menu" type="Node3D"]
|
||||
script = ExtResource("1_ng4u3")
|
||||
|
||||
[node name="Background" type="MeshInstance3D" parent="."]
|
||||
mesh = SubResource("PlaneMesh_6t3dn")
|
||||
|
||||
[node name="Devices" type="Node3D" parent="."]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -0.149223, 0, 0.150667)
|
23
src/entities/light.gd
Normal file
23
src/entities/light.gd
Normal file
|
@ -0,0 +1,23 @@
|
|||
extends StaticBody3D
|
||||
|
||||
@export var entity_id = "switch.plug_printer_2"
|
||||
@onready var sprite: AnimatedSprite3D = $Icon
|
||||
|
||||
# Called when the node enters the scene tree for the first time.
|
||||
func _ready():
|
||||
var stateInfo = await HomeAdapters.adapter_ws.get_state(entity_id)
|
||||
if stateInfo["state"] == "on":
|
||||
sprite.set_frame(0)
|
||||
else:
|
||||
sprite.set_frame(1)
|
||||
|
||||
|
||||
func _on_toggle():
|
||||
HomeAdapters.adapter_ws.set_state(entity_id, "off" if sprite.get_frame() == 0 else "on")
|
||||
if sprite.get_frame() == 0:
|
||||
sprite.set_frame(1)
|
||||
else:
|
||||
sprite.set_frame(0)
|
||||
|
||||
func _on_request_completed():
|
||||
pass
|
16
src/entities/sensor.gd
Normal file
16
src/entities/sensor.gd
Normal file
|
@ -0,0 +1,16 @@
|
|||
extends StaticBody3D
|
||||
|
||||
@export var entity_id = "sensor.sun_next_dawn"
|
||||
@onready var label: Label3D = $Label
|
||||
|
||||
# Called when the node enters the scene tree for the first time.
|
||||
func _ready():
|
||||
var stateInfo = await HomeAdapters.adapter_ws.get_state(entity_id)
|
||||
label.text = stateInfo["state"]
|
||||
|
||||
await HomeAdapters.adapter_ws.watch_state(entity_id, func(new_state):
|
||||
label.text = new_state["state"]
|
||||
)
|
||||
|
||||
func _on_toggle():
|
||||
pass
|
30
src/entities/switch.gd
Normal file
30
src/entities/switch.gd
Normal file
|
@ -0,0 +1,30 @@
|
|||
extends StaticBody3D
|
||||
|
||||
@export var entity_id = "switch.plug_printer_2"
|
||||
@onready var sprite: AnimatedSprite3D = $Icon
|
||||
|
||||
# Called when the node enters the scene tree for the first time.
|
||||
func _ready():
|
||||
var stateInfo = await HomeAdapters.adapter_ws.get_state(entity_id)
|
||||
if stateInfo["state"] == "on":
|
||||
sprite.set_frame(0)
|
||||
else:
|
||||
sprite.set_frame(1)
|
||||
|
||||
await HomeAdapters.adapter_ws.watch_state(entity_id, func(new_state):
|
||||
if new_state["state"] == "on":
|
||||
sprite.set_frame(0)
|
||||
else:
|
||||
sprite.set_frame(1)
|
||||
)
|
||||
|
||||
|
||||
func _on_toggle():
|
||||
HomeAdapters.adapter_ws.set_state(entity_id, "off" if sprite.get_frame() == 0 else "on")
|
||||
if sprite.get_frame() == 0:
|
||||
sprite.set_frame(1)
|
||||
else:
|
||||
sprite.set_frame(0)
|
||||
|
||||
func _on_request_completed():
|
||||
pass
|
11
src/globals/home_adapters.gd
Normal file
11
src/globals/home_adapters.gd
Normal file
|
@ -0,0 +1,11 @@
|
|||
extends Node
|
||||
|
||||
var Adapter = preload("res://src/home_adapters/adapter.gd")
|
||||
|
||||
var adapter = Adapter.new(Adapter.ADAPTER_TYPES.HASS)
|
||||
var adapter_ws = Adapter.new(Adapter.ADAPTER_TYPES.HASS_WS)
|
||||
|
||||
func _ready():
|
||||
add_child(adapter)
|
||||
add_child(adapter_ws)
|
||||
|
1
src/globals/request.gd
Normal file
1
src/globals/request.gd
Normal file
|
@ -0,0 +1 @@
|
|||
extends HTTPRequest
|
41
src/home_adapters/adapter.gd
Normal file
41
src/home_adapters/adapter.gd
Normal file
|
@ -0,0 +1,41 @@
|
|||
extends Node
|
||||
|
||||
const hass = preload("res://src/home_adapters/hass/hass.gd")
|
||||
const hass_ws = preload("res://src/home_adapters/hass_ws/hass.gd")
|
||||
|
||||
enum ADAPTER_TYPES {
|
||||
HASS,
|
||||
HASS_WS
|
||||
}
|
||||
|
||||
const adapters = {
|
||||
ADAPTER_TYPES.HASS: hass,
|
||||
ADAPTER_TYPES.HASS_WS: hass_ws
|
||||
}
|
||||
|
||||
const methods = [
|
||||
"load_devices",
|
||||
"get_state",
|
||||
"set_state"
|
||||
]
|
||||
|
||||
var adapter: Node
|
||||
|
||||
func _init(type: ADAPTER_TYPES):
|
||||
adapter = adapters[type].new()
|
||||
add_child(adapter)
|
||||
|
||||
for method in methods:
|
||||
assert(adapter.has_method(method), "Adapter does not implement method: " + method)
|
||||
|
||||
func load_devices():
|
||||
return await adapter.load_devices()
|
||||
|
||||
func get_state(entity: String):
|
||||
return await adapter.get_state(entity)
|
||||
|
||||
func set_state(entity: String, state: String, attributes: Dictionary = {}):
|
||||
return await adapter.set_state(entity, state, attributes)
|
||||
|
||||
func watch_state(entity: String, callback: Callable):
|
||||
return adapter.watch_state(entity, callback)
|
60
src/home_adapters/hass/hass.gd
Normal file
60
src/home_adapters/hass/hass.gd
Normal file
|
@ -0,0 +1,60 @@
|
|||
extends Node
|
||||
|
||||
var url: String = "http://192.168.33.33:8123"
|
||||
var token: String = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiIzZjQ0ZGM2N2Y3YzY0MDc1OGZlMWI2ZjJlNmIxZjRkNSIsImlhdCI6MTY5ODAxMDcyOCwiZXhwIjoyMDEzMzcwNzI4fQ.K6ydLUC-4Q7BNIRCU1nWlI2s6sg9UCiOu-Lpedw2zJc"
|
||||
var headers: PackedStringArray = PackedStringArray([])
|
||||
|
||||
var devices_template = FileAccess.get_file_as_string("res://src/home_adapters/hass/templates/devices.j2")
|
||||
|
||||
func _init(url := self.url, token := self.token):
|
||||
self.url = url
|
||||
self.token = token
|
||||
|
||||
headers = PackedStringArray(["Authorization: Bearer %s" % token, "Content-Type: application/json"])
|
||||
devices_template = devices_template.replace("\n", " ").replace("\t", "").replace("\r", " ").replace("\"", "\\\"")
|
||||
|
||||
func load_devices():
|
||||
Request.request("%s/api/template" % [url], headers, HTTPClient.METHOD_POST, "{\"template\": \"%s\"}" % [devices_template])
|
||||
var response = await Request.request_completed
|
||||
var data_string = response[3].get_string_from_utf8().replace("'", "\"")
|
||||
var json = JSON.parse_string(data_string).data
|
||||
|
||||
return json
|
||||
|
||||
func get_state(entity: String):
|
||||
var type = entity.split('.')[0]
|
||||
|
||||
Request.request("%s/api/states/%s" % [url, entity], headers, HTTPClient.METHOD_GET)
|
||||
var response = await Request.request_completed
|
||||
|
||||
var data_string = response[3].get_string_from_utf8().replace("'", "\"")
|
||||
var json = JSON.parse_string(data_string)
|
||||
|
||||
return json
|
||||
|
||||
|
||||
|
||||
func set_state(entity: String, state: String, attributes: Dictionary = {}):
|
||||
var type = entity.split('.')[0]
|
||||
var response
|
||||
|
||||
if type == 'switch':
|
||||
if state == 'on':
|
||||
Request.request("%s/api/services/switch/turn_on" % [url], headers, HTTPClient.METHOD_POST, "{\"entity_id\": \"%s\"}" % [entity])
|
||||
response = await Request.request_completed
|
||||
elif state == 'off':
|
||||
Request.request("%s/api/services/switch/turn_off" % [url], headers, HTTPClient.METHOD_POST, "{\"entity_id\": \"%s\"}" % [entity])
|
||||
response = await Request.request_completed
|
||||
elif type == 'light':
|
||||
if state == 'on':
|
||||
Request.request("%s/api/services/light/turn_on" % [url], headers, HTTPClient.METHOD_POST, "{\"entity_id\": \"%s\"}" % [entity])
|
||||
response = await Request.request_completed
|
||||
elif state == 'off':
|
||||
Request.request("%s/api/services/light/turn_off" % [url], headers, HTTPClient.METHOD_POST, "{\"entity_id\": \"%s\"}" % [entity])
|
||||
response = await Request.request_completed
|
||||
|
||||
var data_string = response[3].get_string_from_utf8().replace("'", "\"")
|
||||
var json = JSON.parse_string(data_string)
|
||||
|
||||
return json
|
||||
|
12
src/home_adapters/hass/templates/devices.j2
Normal file
12
src/home_adapters/hass/templates/devices.j2
Normal file
|
@ -0,0 +1,12 @@
|
|||
{% set devices = states | map(attribute='entity_id') | map('device_id') | unique | reject('eq',None) | list %}
|
||||
|
||||
{%- set ns = namespace(devices = []) %}
|
||||
{%- for device in devices %}
|
||||
{%- set entities = device_entities(device) | list %}
|
||||
{%- if entities %}
|
||||
{%- set ns.devices = ns.devices + [ {device: {"name": device_attr(device, "name"), "entities": entities }} ] %}
|
||||
{%- endif %}
|
||||
{%- endfor %}
|
||||
{
|
||||
"data": {{ ns.devices }}
|
||||
}
|
40
src/home_adapters/hass_ws/callback_map.gd
Normal file
40
src/home_adapters/hass_ws/callback_map.gd
Normal file
|
@ -0,0 +1,40 @@
|
|||
extends Node
|
||||
|
||||
class_name CallbackMap
|
||||
|
||||
var callbacks := {}
|
||||
|
||||
func add(key: Variant, callback: Callable) -> void:
|
||||
_validate_key(key)
|
||||
|
||||
if callbacks.has(key):
|
||||
callbacks[key].append(callback)
|
||||
else:
|
||||
callbacks[key] = [callback]
|
||||
|
||||
func add_once(key: Variant, callback: Callable) -> void:
|
||||
_validate_key(key)
|
||||
|
||||
var fn: Callable
|
||||
|
||||
fn = func(args: Array):
|
||||
remove(key, fn)
|
||||
callback.callv(args)
|
||||
|
||||
add(key, fn)
|
||||
|
||||
func remove(key: Variant, callback: Callable) -> void:
|
||||
_validate_key(key)
|
||||
|
||||
if callbacks.has(key):
|
||||
callbacks[key].erase(callback)
|
||||
|
||||
func call_key(key: Variant, args: Array) -> void:
|
||||
_validate_key(key)
|
||||
|
||||
if callbacks.has(key):
|
||||
for callback in callbacks[key]:
|
||||
callback.callv(args)
|
||||
|
||||
func _validate_key(key: Variant):
|
||||
assert(typeof(key) == TYPE_STRING || typeof(key) == TYPE_INT || typeof(key) == TYPE_FLOAT, "key must be a string or number")
|
222
src/home_adapters/hass_ws/hass.gd
Normal file
222
src/home_adapters/hass_ws/hass.gd
Normal file
|
@ -0,0 +1,222 @@
|
|||
extends Node
|
||||
|
||||
var devices_template := FileAccess.get_file_as_string("res://src/home_adapters/hass/templates/devices.j2")
|
||||
var socket := WebSocketPeer.new()
|
||||
# in seconds
|
||||
var request_timeout := 10.0
|
||||
|
||||
var url := "ws://192.168.33.33:8123/api/websocket"
|
||||
var token := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiIzZjQ0ZGM2N2Y3YzY0MDc1OGZlMWI2ZjJlNmIxZjRkNSIsImlhdCI6MTY5ODAxMDcyOCwiZXhwIjoyMDEzMzcwNzI4fQ.K6ydLUC-4Q7BNIRCU1nWlI2s6sg9UCiOu-Lpedw2zJc"
|
||||
var LOG_MESSAGES := false
|
||||
|
||||
var authenticated := false
|
||||
var id := 1
|
||||
var entities: Dictionary = {}
|
||||
|
||||
var entitiy_callbacks := CallbackMap.new()
|
||||
var packet_callbacks := CallbackMap.new()
|
||||
|
||||
signal on_connect()
|
||||
signal on_disconnect()
|
||||
|
||||
func _init(url := self.url, token := self.token):
|
||||
self.url = url
|
||||
self.token = token
|
||||
|
||||
devices_template = devices_template.replace("\n", " ").replace("\t", "").replace("\r", " ").replace("\"", "\\\"")
|
||||
connect_ws()
|
||||
|
||||
func connect_ws():
|
||||
print("Connecting to %s" % self.url)
|
||||
socket.connect_to_url(self.url)
|
||||
|
||||
# https://github.com/godotengine/godot/issues/84423
|
||||
# Otherwise the WebSocketPeer will crash when receiving large packets
|
||||
socket.set_inbound_buffer_size(65535 * 2)
|
||||
|
||||
func _process(delta):
|
||||
socket.poll()
|
||||
|
||||
var state = socket.get_ready_state()
|
||||
if state == WebSocketPeer.STATE_OPEN:
|
||||
while socket.get_available_packet_count():
|
||||
var packet = decode_packet(socket.get_packet())
|
||||
if typeof(packet) == TYPE_DICTIONARY:
|
||||
handle_packet(packet)
|
||||
elif typeof(packet) == TYPE_ARRAY:
|
||||
for p in packet:
|
||||
handle_packet(p)
|
||||
elif state == WebSocketPeer.STATE_CLOSING:
|
||||
pass
|
||||
elif state == WebSocketPeer.STATE_CLOSED:
|
||||
var code = socket.get_close_code()
|
||||
var reason = socket.get_close_reason()
|
||||
print("WS connection closed with code: %s, reason: %s" % [code, reason])
|
||||
handle_disconnect()
|
||||
|
||||
func handle_packet(packet: Dictionary):
|
||||
if LOG_MESSAGES: print("Received packet: %s" % packet)
|
||||
|
||||
if packet.type == "auth_required":
|
||||
send_packet({
|
||||
"type": "auth",
|
||||
"access_token": self.token
|
||||
})
|
||||
|
||||
elif packet.type == "auth_ok":
|
||||
authenticated = true
|
||||
start_subscriptions()
|
||||
|
||||
elif packet.type == "auth_invalid":
|
||||
handle_disconnect()
|
||||
else:
|
||||
packet_callbacks.call_key(int(packet.id), [packet])
|
||||
|
||||
func start_subscriptions():
|
||||
assert(authenticated, "Not authenticated")
|
||||
|
||||
# await send_request_packet({
|
||||
# "type": "supported_features",
|
||||
# "features": {
|
||||
# "coalesce_messages": 1
|
||||
# }
|
||||
# })
|
||||
|
||||
# await send_request_packet({
|
||||
# "type": "subscribe_events",
|
||||
# "event_type": "state_changed"
|
||||
# })
|
||||
|
||||
send_subscribe_packet({
|
||||
"type": "subscribe_entities"
|
||||
}, func(packet: Dictionary):
|
||||
if packet.type != "event":
|
||||
return
|
||||
|
||||
if packet.event.has("a"):
|
||||
for entity in packet.event.a.keys():
|
||||
entities[entity] = {
|
||||
"state": packet.event.a[entity]["s"],
|
||||
"attributes": packet.event.a[entity]["a"]
|
||||
}
|
||||
entitiy_callbacks.call_key(entity, [entities[entity]])
|
||||
on_connect.emit()
|
||||
|
||||
if packet.event.has("c"):
|
||||
for entity in packet.event.c.keys():
|
||||
if !entities.has(entity):
|
||||
continue
|
||||
|
||||
if packet.event.c[entity].has("+"):
|
||||
if packet.event.c[entity]["+"].has("s"):
|
||||
entities[entity]["state"] = packet.event.c[entity]["+"]["s"]
|
||||
if packet.event.c[entity]["+"].has("a"):
|
||||
entities[entity]["attributes"].merge(packet.event.c[entity]["+"]["a"])
|
||||
entitiy_callbacks.call_key(entity, [entities[entity]])
|
||||
)
|
||||
|
||||
func handle_disconnect():
|
||||
authenticated = false
|
||||
set_process(false)
|
||||
on_disconnect.emit()
|
||||
|
||||
func send_subscribe_packet(packet: Dictionary, callback: Callable):
|
||||
packet.id = id
|
||||
id += 1
|
||||
|
||||
packet_callbacks.add(packet.id, callback)
|
||||
send_packet(packet)
|
||||
|
||||
return func():
|
||||
packet_callbacks.remove(packet.id, callback)
|
||||
send_packet({
|
||||
id: id,
|
||||
"type": packet.type.replace("subscribe", "unsubscribe"),
|
||||
"subscription": packet.id
|
||||
})
|
||||
id += 1
|
||||
|
||||
|
||||
func send_request_packet(packet: Dictionary):
|
||||
packet.id = id
|
||||
id += 1
|
||||
|
||||
send_packet(packet)
|
||||
|
||||
var promise = Promise.new(func(resolve: Callable, reject: Callable):
|
||||
packet_callbacks.add_once(packet.id, resolve)
|
||||
|
||||
var timeout = Timer.new()
|
||||
timeout.set_wait_time(request_timeout)
|
||||
timeout.set_one_shot(true)
|
||||
timeout.timeout.connect(func():
|
||||
reject.call(Promise.Rejection.new("Request timed out"))
|
||||
packet_callbacks.remove(packet.id, resolve)
|
||||
)
|
||||
add_child(timeout)
|
||||
timeout.start()
|
||||
)
|
||||
|
||||
return await promise.settled
|
||||
|
||||
|
||||
func send_packet(packet: Dictionary):
|
||||
if LOG_MESSAGES || true: print("Sending packet: %s" % encode_packet(packet))
|
||||
socket.send_text(encode_packet(packet))
|
||||
|
||||
func decode_packet(packet: PackedByteArray):
|
||||
return JSON.parse_string(packet.get_string_from_utf8())
|
||||
|
||||
func encode_packet(packet: Dictionary):
|
||||
return JSON.stringify(packet)
|
||||
|
||||
func load_devices():
|
||||
if !authenticated:
|
||||
await on_connect
|
||||
|
||||
return entities
|
||||
|
||||
func get_state(entity: String):
|
||||
if !authenticated:
|
||||
await on_connect
|
||||
|
||||
if entities.has(entity):
|
||||
return entities[entity]
|
||||
return null
|
||||
|
||||
|
||||
func watch_state(entity: String, callback: Callable):
|
||||
if !authenticated:
|
||||
await on_connect
|
||||
|
||||
entitiy_callbacks.add(entity, callback)
|
||||
|
||||
|
||||
func set_state(entity: String, state: String, attributes: Dictionary = {}):
|
||||
assert(authenticated, "Not authenticated")
|
||||
|
||||
var domain = entity.split(".")[0]
|
||||
var service: String
|
||||
|
||||
if domain == 'switch':
|
||||
if state == 'on':
|
||||
service = 'turn_on'
|
||||
elif state == 'off':
|
||||
service = 'turn_off'
|
||||
elif domain == 'light':
|
||||
if state == 'on':
|
||||
service = 'turn_on'
|
||||
elif state == 'off':
|
||||
service = 'turn_off'
|
||||
|
||||
return await send_request_packet({
|
||||
"type": "call_service",
|
||||
"domain": domain,
|
||||
"service": service,
|
||||
"service_data": attributes,
|
||||
"target": {
|
||||
"entity_id": entity
|
||||
}
|
||||
})
|
||||
|
||||
|
12
src/home_adapters/hass_ws/templates/devices.j2
Normal file
12
src/home_adapters/hass_ws/templates/devices.j2
Normal file
|
@ -0,0 +1,12 @@
|
|||
{% set devices = states | map(attribute='entity_id') | map('device_id') | unique | reject('eq',None) | list %}
|
||||
|
||||
{%- set ns = namespace(devices = []) %}
|
||||
{%- for device in devices %}
|
||||
{%- set entities = device_entities(device) | list %}
|
||||
{%- if entities %}
|
||||
{%- set ns.devices = ns.devices + [ {device: {"name": device_attr(device, "name"), "entities": entities }} ] %}
|
||||
{%- endif %}
|
||||
{%- endfor %}
|
||||
{
|
||||
"data": {{ ns.devices }}
|
||||
}
|
111
src/menu.gd
Normal file
111
src/menu.gd
Normal file
|
@ -0,0 +1,111 @@
|
|||
extends Node3D
|
||||
|
||||
const Device = preload("res://scenes/device.tscn")
|
||||
const Entity = preload("res://scenes/entity.tscn")
|
||||
const Switch = preload("res://scenes/entities/switch.tscn")
|
||||
const Light = preload("res://scenes/entities/light.tscn")
|
||||
const Sensor = preload("res://scenes/entities/sensor.tscn")
|
||||
|
||||
@onready var devices_node = $Devices
|
||||
var devices
|
||||
|
||||
var selected_device = null
|
||||
# Called when the node enters the scene tree for the first time.
|
||||
func _ready():
|
||||
devices = await HomeAdapters.adapter.load_devices()
|
||||
render_devices()
|
||||
|
||||
func render_devices():
|
||||
var x = 0
|
||||
var y = 0
|
||||
|
||||
for device in devices:
|
||||
var info = device.values()[0]
|
||||
|
||||
var device_instance = Device.instantiate()
|
||||
device_instance.set_position(Vector3(y * 0.08, 0, -x * 0.08))
|
||||
device_instance.click.connect(_on_device_click)
|
||||
device_instance.id = device.keys()[0]
|
||||
|
||||
devices_node.add_child(device_instance)
|
||||
|
||||
device_instance.set_device_name(info["name"])
|
||||
|
||||
x += 1
|
||||
if x % 5 == 0:
|
||||
x = 0
|
||||
y += 1
|
||||
|
||||
func render_entities():
|
||||
var x = 0
|
||||
var y = 0
|
||||
|
||||
var info
|
||||
|
||||
for device in devices:
|
||||
if device.keys()[0] == selected_device:
|
||||
info = device.values()[0]
|
||||
break
|
||||
|
||||
if info == null:
|
||||
return
|
||||
|
||||
var entities = info["entities"]
|
||||
|
||||
for entity in entities:
|
||||
var entity_instance = Entity.instantiate()
|
||||
entity_instance.set_position(Vector3(y * 0.08, 0, -x * 0.08))
|
||||
entity_instance.click.connect(_on_entity_click)
|
||||
|
||||
devices_node.add_child(entity_instance)
|
||||
|
||||
entity_instance.set_entity_name(entity)
|
||||
|
||||
x += 1
|
||||
if x % 5 == 0:
|
||||
x = 0
|
||||
y += 1
|
||||
|
||||
func _on_device_click(device_id):
|
||||
selected_device = device_id
|
||||
print(selected_device)
|
||||
clear_menu()
|
||||
render_entities()
|
||||
|
||||
func _on_entity_click(entity_name):
|
||||
print(entity_name)
|
||||
selected_device = null
|
||||
clear_menu()
|
||||
render_devices()
|
||||
|
||||
var type = entity_name.split(".")[0]
|
||||
print(type)
|
||||
|
||||
if type == "switch":
|
||||
var switch = Switch.instantiate()
|
||||
switch.entity_id = entity_name
|
||||
|
||||
switch.set_position(global_position)
|
||||
get_node("/root").add_child(switch)
|
||||
|
||||
if type == "light":
|
||||
var light = Light.instantiate()
|
||||
light.entity_id = entity_name
|
||||
|
||||
light.set_position(global_position)
|
||||
get_node("/root").add_child(light)
|
||||
|
||||
if type == "sensor":
|
||||
var sensor = Sensor.instantiate()
|
||||
sensor.entity_id = entity_name
|
||||
|
||||
sensor.set_position(global_position)
|
||||
get_node("/root").add_child(sensor)
|
||||
|
||||
func clear_menu():
|
||||
for child in devices_node.get_children():
|
||||
devices_node.remove_child(child)
|
||||
|
||||
# Called every frame. 'delta' is the elapsed time since the previous frame.
|
||||
func _process(delta):
|
||||
pass
|
23
src/model.gd
Normal file
23
src/model.gd
Normal file
|
@ -0,0 +1,23 @@
|
|||
extends Node3D
|
||||
|
||||
@onready var _controller := XRHelpers.get_xr_controller(self)
|
||||
@export var light: StaticBody3D
|
||||
|
||||
# Called when the node enters the scene tree for the first time.
|
||||
func _ready():
|
||||
_controller.button_pressed.connect(self._on_button_pressed)
|
||||
|
||||
# Called every frame. 'delta' is the elapsed time since the previous frame.
|
||||
func _process(delta):
|
||||
pass
|
||||
|
||||
func _on_button_pressed(button):
|
||||
print("right: ", button)
|
||||
if button != "trigger_click":
|
||||
return
|
||||
|
||||
if light == null:
|
||||
return
|
||||
|
||||
# set light position to controller position
|
||||
light.transform.origin = _controller.transform.origin
|
27
src/raycast.gd
Normal file
27
src/raycast.gd
Normal file
|
@ -0,0 +1,27 @@
|
|||
extends Node3D
|
||||
|
||||
@onready var _controller := XRHelpers.get_xr_controller(self)
|
||||
@export var ray: RayCast3D
|
||||
|
||||
# Called when the node enters the scene tree for the first time.
|
||||
func _ready():
|
||||
_controller.button_pressed.connect(self._on_button_pressed)
|
||||
|
||||
# Called every frame. 'delta' is the elapsed time since the previous frame.
|
||||
func _process(delta):
|
||||
pass
|
||||
|
||||
func _on_button_pressed(button):
|
||||
print(button)
|
||||
if button != "trigger_click":
|
||||
return
|
||||
|
||||
var collider = ray.get_collider()
|
||||
|
||||
if collider == null:
|
||||
return
|
||||
|
||||
print(collider)
|
||||
|
||||
if collider.has_method("_on_toggle"):
|
||||
collider._on_toggle()
|
13
src/ui/device.gd
Normal file
13
src/ui/device.gd
Normal file
|
@ -0,0 +1,13 @@
|
|||
extends StaticBody3D
|
||||
|
||||
@onready var label: Label3D = $Label
|
||||
@export var id: String = "0"
|
||||
|
||||
signal click(id: String)
|
||||
|
||||
func _on_toggle():
|
||||
click.emit(id)
|
||||
|
||||
func set_device_name(text):
|
||||
assert(label != null, "Device has to be added to the scene tree")
|
||||
label.text = text
|
14
src/ui/entity.gd
Normal file
14
src/ui/entity.gd
Normal file
|
@ -0,0 +1,14 @@
|
|||
extends StaticBody3D
|
||||
|
||||
@onready var label: Label3D = $Label
|
||||
@export var text = "Default"
|
||||
|
||||
signal click(name: String)
|
||||
|
||||
func _on_toggle():
|
||||
click.emit(text)
|
||||
|
||||
func set_entity_name(text):
|
||||
assert(label != null, "Entity has to be added to the scene tree")
|
||||
label.text = text.replace(".", "\n")
|
||||
self.text = text
|
Loading…
Reference in New Issue
Block a user