vantagekit 0.6.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- vantagekit/__init__.py +7 -0
- vantagekit/core/__init__.py +32 -0
- vantagekit/core/bindings.py +232 -0
- vantagekit/core/frame.py +123 -0
- vantagekit/core/rig_math.py +129 -0
- vantagekit/core/smoothing.py +126 -0
- vantagekit/core/targets.py +93 -0
- vantagekit/kit/__init__.py +20 -0
- vantagekit/kit/controls.py +202 -0
- vantagekit/kit/events.py +53 -0
- vantagekit/kit/input.py +152 -0
- vantagekit/kit/native_nav.py +84 -0
- vantagekit/kit/overlay.py +95 -0
- vantagekit/kit/rig.py +346 -0
- vantagekit-0.6.1.dist-info/METADATA +84 -0
- vantagekit-0.6.1.dist-info/RECORD +18 -0
- vantagekit-0.6.1.dist-info/WHEEL +4 -0
- vantagekit-0.6.1.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"""Targets the camera can attach to, and selection among them."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from typing import Callable, Hashable, List, Optional, Protocol, Sequence
|
|
7
|
+
|
|
8
|
+
from .frame import Quat, Vec3, dot, sub
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass(frozen=True)
|
|
12
|
+
class Pose:
|
|
13
|
+
position: Vec3 # stage units, world space
|
|
14
|
+
orientation: Quat = (1.0, 0.0, 0.0, 0.0) # (w, x, y, z)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class TargetProvider(Protocol):
|
|
18
|
+
def target_ids(self) -> Sequence[Hashable]:
|
|
19
|
+
"""Targets in cycling order."""
|
|
20
|
+
|
|
21
|
+
def target_pose(self, tid: Hashable) -> Optional[Pose]:
|
|
22
|
+
"""World pose of a target, or None if it is not present now."""
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class CallbackTargets:
|
|
26
|
+
"""A TargetProvider from two callables, for apps that keep poses in their own dicts."""
|
|
27
|
+
|
|
28
|
+
def __init__(
|
|
29
|
+
self,
|
|
30
|
+
ids: Callable[[], Sequence[Hashable]],
|
|
31
|
+
pose: Callable[[Hashable], Optional[Pose]],
|
|
32
|
+
):
|
|
33
|
+
self._ids = ids
|
|
34
|
+
self._pose = pose
|
|
35
|
+
|
|
36
|
+
def target_ids(self) -> Sequence[Hashable]:
|
|
37
|
+
return list(self._ids() or ())
|
|
38
|
+
|
|
39
|
+
def target_pose(self, tid: Hashable) -> Optional[Pose]:
|
|
40
|
+
return self._pose(tid)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class TargetCycler:
|
|
44
|
+
"""The selected target. The selection survives targets coming and going; previous and
|
|
45
|
+
next step through the provider's current list."""
|
|
46
|
+
|
|
47
|
+
def __init__(self, provider: TargetProvider, selected: Hashable = None):
|
|
48
|
+
self.provider = provider
|
|
49
|
+
self.selected = selected
|
|
50
|
+
|
|
51
|
+
def ids(self) -> List[Hashable]:
|
|
52
|
+
return list(self.provider.target_ids())
|
|
53
|
+
|
|
54
|
+
def pose(self) -> Optional[Pose]:
|
|
55
|
+
if self.selected is None:
|
|
56
|
+
return None
|
|
57
|
+
return self.provider.target_pose(self.selected)
|
|
58
|
+
|
|
59
|
+
def step(self, direction: int) -> Hashable:
|
|
60
|
+
ids = self.ids()
|
|
61
|
+
if not ids:
|
|
62
|
+
return self.selected
|
|
63
|
+
if self.selected in ids:
|
|
64
|
+
i = (ids.index(self.selected) + direction) % len(ids)
|
|
65
|
+
else:
|
|
66
|
+
i = 0 if direction >= 0 else len(ids) - 1
|
|
67
|
+
self.selected = ids[i]
|
|
68
|
+
return self.selected
|
|
69
|
+
|
|
70
|
+
def nearest(self, point: Sequence[float]) -> Hashable:
|
|
71
|
+
best, best_d = None, None
|
|
72
|
+
for tid in self.ids():
|
|
73
|
+
p = self.provider.target_pose(tid)
|
|
74
|
+
if p is None:
|
|
75
|
+
continue
|
|
76
|
+
d = sub(p.position, point)
|
|
77
|
+
d2 = dot(d, d)
|
|
78
|
+
if best_d is None or d2 < best_d:
|
|
79
|
+
best, best_d = tid, d2
|
|
80
|
+
return best
|
|
81
|
+
|
|
82
|
+
def ensure(self, near: Sequence[float] | None = None) -> Hashable:
|
|
83
|
+
"""Keep the selection if it is present; otherwise pick the target nearest to near
|
|
84
|
+
(or the first one)."""
|
|
85
|
+
if self.selected is not None and self.provider.target_pose(self.selected) is not None:
|
|
86
|
+
return self.selected
|
|
87
|
+
pick = self.nearest(near) if near is not None else None
|
|
88
|
+
if pick is None:
|
|
89
|
+
ids = self.ids()
|
|
90
|
+
pick = ids[0] if ids else None
|
|
91
|
+
if pick is not None:
|
|
92
|
+
self.selected = pick
|
|
93
|
+
return self.selected
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""Kit adapters for the vantagekit core: camera rig, input router, native navigation
|
|
2
|
+
handover and Events 2.0 subscriptions. Loaded by Kit as the python module of the
|
|
3
|
+
lhzn.vantagekit extension."""
|
|
4
|
+
|
|
5
|
+
from .controls import ViewerControls
|
|
6
|
+
from .events import on_stage_opened, on_update
|
|
7
|
+
from .input import InputRouter
|
|
8
|
+
from .native_nav import NativeNavigation
|
|
9
|
+
from .rig import MODES, CameraRig, RigSettings
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"CameraRig",
|
|
13
|
+
"InputRouter",
|
|
14
|
+
"MODES",
|
|
15
|
+
"NativeNavigation",
|
|
16
|
+
"RigSettings",
|
|
17
|
+
"ViewerControls",
|
|
18
|
+
"on_stage_opened",
|
|
19
|
+
"on_update",
|
|
20
|
+
]
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
"""ViewerControls: the rig, the input router and target selection wired with the default
|
|
2
|
+
actions. Apps add handlers for their own actions and their own binding layers.
|
|
3
|
+
|
|
4
|
+
controls = ViewerControls("/World/ViewerCam", CallbackTargets(ids_fn, pose_fn))
|
|
5
|
+
controls.on("pilot_toggle", my_toggle)
|
|
6
|
+
...
|
|
7
|
+
# once per frame, from the app's update observer:
|
|
8
|
+
frame = controls.update(dt)
|
|
9
|
+
...
|
|
10
|
+
controls.dispose()
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import math
|
|
16
|
+
import time
|
|
17
|
+
from typing import Callable, Dict, Iterable, List, Sequence
|
|
18
|
+
|
|
19
|
+
import carb
|
|
20
|
+
|
|
21
|
+
from ..core.bindings import DEFAULT_BINDINGS, Binding, BindingState, Frame
|
|
22
|
+
from ..core.targets import TargetCycler, TargetProvider
|
|
23
|
+
from .input import InputRouter
|
|
24
|
+
from .native_nav import NativeNavigation
|
|
25
|
+
from .overlay import NAMES, ViewOverlay
|
|
26
|
+
from .rig import CameraRig, RigSettings
|
|
27
|
+
|
|
28
|
+
# Orbit input scales: a drag across the whole window turns half a circle; a wheel step
|
|
29
|
+
# changes the range by about 14 percent; full stick deflection turns 90 degrees a second;
|
|
30
|
+
# a full trigger doubles or halves the range in about half a second.
|
|
31
|
+
DRAG_RAD_PER_WINDOW = math.pi
|
|
32
|
+
WHEEL_LOG_STEP = 0.15
|
|
33
|
+
STICK_RAD_PER_S = math.radians(90.0)
|
|
34
|
+
TRIGGER_LOG_PER_S = 1.5
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class ViewerControls:
|
|
38
|
+
def __init__(
|
|
39
|
+
self,
|
|
40
|
+
camera_path: str,
|
|
41
|
+
provider: TargetProvider,
|
|
42
|
+
bindings: Iterable[Binding] | None = None,
|
|
43
|
+
layers: Sequence[str] = ("camera", "orbit"),
|
|
44
|
+
settings: RigSettings | None = None,
|
|
45
|
+
free_gamepad: bool | None = None,
|
|
46
|
+
order: int = 0,
|
|
47
|
+
consume: bool = False,
|
|
48
|
+
name: str = "vantagekit",
|
|
49
|
+
overlay: bool = True,
|
|
50
|
+
overlay_extra: Callable[[], str] | None = None,
|
|
51
|
+
):
|
|
52
|
+
"""overlay: draw the view badge over the viewport (an app with its own HUD can turn
|
|
53
|
+
it off and draw view_badge() itself). overlay_extra: returns text the app appends
|
|
54
|
+
to the badge title (for example "PILOT" while piloting)."""
|
|
55
|
+
self.name = name
|
|
56
|
+
self._overlay_on = overlay
|
|
57
|
+
self._overlay = None
|
|
58
|
+
self._overlay_retry_t = 0.0
|
|
59
|
+
self._overlay_extra = overlay_extra
|
|
60
|
+
self.targets = TargetCycler(provider)
|
|
61
|
+
self.native = NativeNavigation(free_gamepad=free_gamepad)
|
|
62
|
+
self.rig = CameraRig(camera_path, self.targets, settings, self.native, name=name)
|
|
63
|
+
self.state = BindingState(list(bindings) if bindings is not None else DEFAULT_BINDINGS, layers)
|
|
64
|
+
self.router = InputRouter(self.state, order=order, consume=consume, name=name)
|
|
65
|
+
self._handlers: Dict[str, Callable[[], None]] = {
|
|
66
|
+
"attach": self.toggle_attach,
|
|
67
|
+
"view_next": self.next_view,
|
|
68
|
+
"target_prev": lambda: self.step_target(-1),
|
|
69
|
+
"target_next": lambda: self.step_target(1),
|
|
70
|
+
"home": self.rig.home,
|
|
71
|
+
}
|
|
72
|
+
self._mode_listeners: List[Callable[[str], None]] = []
|
|
73
|
+
self._target_listeners: List[Callable[[object], None]] = []
|
|
74
|
+
|
|
75
|
+
# wiring ---------------------------------------------------------------------
|
|
76
|
+
def on(self, action: str, fn: Callable[[], None]) -> None:
|
|
77
|
+
"""Handle a press action (replaces a default handler of the same name)."""
|
|
78
|
+
self._handlers[action] = fn
|
|
79
|
+
|
|
80
|
+
def on_mode_change(self, fn: Callable[[str], None]) -> None:
|
|
81
|
+
self._mode_listeners.append(fn)
|
|
82
|
+
|
|
83
|
+
def on_target_change(self, fn: Callable[[object], None]) -> None:
|
|
84
|
+
self._target_listeners.append(fn)
|
|
85
|
+
|
|
86
|
+
def set_layer(self, layer: str, on: bool) -> None:
|
|
87
|
+
self.state.set_layer(layer, on)
|
|
88
|
+
|
|
89
|
+
# actions ------------------------------------------------------------------
|
|
90
|
+
def set_mode(self, mode: str) -> str:
|
|
91
|
+
before = self.rig.mode
|
|
92
|
+
after = self.rig.set_mode(mode)
|
|
93
|
+
self.state.set_layer("orbit", after == "orbit")
|
|
94
|
+
if after != before:
|
|
95
|
+
for fn in self._mode_listeners:
|
|
96
|
+
fn(after)
|
|
97
|
+
return after
|
|
98
|
+
|
|
99
|
+
def toggle_attach(self) -> str:
|
|
100
|
+
return self.set_mode("free" if self.rig.attached else self.rig.view)
|
|
101
|
+
|
|
102
|
+
def next_view(self) -> str:
|
|
103
|
+
if not self.rig.attached:
|
|
104
|
+
return self.set_mode(self.rig.view)
|
|
105
|
+
views = list(self.rig.settings.views)
|
|
106
|
+
i = views.index(self.rig.mode) if self.rig.mode in views else -1
|
|
107
|
+
return self.set_mode(views[(i + 1) % len(views)])
|
|
108
|
+
|
|
109
|
+
def step_target(self, direction: int):
|
|
110
|
+
tid = self.rig.step_target(direction)
|
|
111
|
+
for fn in self._target_listeners:
|
|
112
|
+
fn(tid)
|
|
113
|
+
return tid
|
|
114
|
+
|
|
115
|
+
def select(self, tid) -> None:
|
|
116
|
+
self.targets.selected = tid
|
|
117
|
+
for fn in self._target_listeners:
|
|
118
|
+
fn(tid)
|
|
119
|
+
|
|
120
|
+
# per frame ------------------------------------------------------------------
|
|
121
|
+
def update(self, dt: float) -> Frame:
|
|
122
|
+
frame = self.router.poll(dt)
|
|
123
|
+
for action in frame.pressed:
|
|
124
|
+
fn = self._handlers.get(action)
|
|
125
|
+
if fn is not None:
|
|
126
|
+
try:
|
|
127
|
+
fn()
|
|
128
|
+
except Exception as e:
|
|
129
|
+
carb.log_error(f"[{self.name}] action {action} failed: {e}")
|
|
130
|
+
if self.rig.mode == "orbit":
|
|
131
|
+
d_yaw = -frame.value("orbit_yaw") * DRAG_RAD_PER_WINDOW
|
|
132
|
+
# The stick moves the camera the way it is pushed: left takes the camera left
|
|
133
|
+
# around the target (a positive yaw moves it right on screen), up raises it.
|
|
134
|
+
# The mouse drags the scene instead, so its signs are the opposite.
|
|
135
|
+
d_yaw += frame.amount("orbit_yaw_rate", STICK_RAD_PER_S)
|
|
136
|
+
d_elev = frame.value("orbit_pitch") * DRAG_RAD_PER_WINDOW
|
|
137
|
+
d_elev += frame.amount("orbit_pitch_rate", STICK_RAD_PER_S) # stick up: camera up
|
|
138
|
+
log_r = -frame.value("orbit_range") * WHEEL_LOG_STEP
|
|
139
|
+
log_r += frame.amount("orbit_range_rate", TRIGGER_LOG_PER_S)
|
|
140
|
+
if d_yaw or d_elev or log_r:
|
|
141
|
+
self.rig.orbit_by(d_yaw, d_elev, math.exp(log_r))
|
|
142
|
+
self.rig.update(dt)
|
|
143
|
+
self._update_overlay()
|
|
144
|
+
return frame
|
|
145
|
+
|
|
146
|
+
_HINTS = {
|
|
147
|
+
"free": "K / X: attach",
|
|
148
|
+
"chase": "T / D-pad down: next view Home / A: reset view",
|
|
149
|
+
"pov": "T / D-pad down: next view",
|
|
150
|
+
"orbit": "Right stick, drag: orbit Triggers, wheel: distance "
|
|
151
|
+
"T / D-pad down: chase from here Home / A: reset",
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
def view_badge(self) -> tuple:
|
|
155
|
+
"""(mode, title, hint) for the current view, e.g. ("orbit", "ORBIT 101", "Right
|
|
156
|
+
stick, drag: orbit ..."). The built-in overlay draws it; an app with its own HUD
|
|
157
|
+
can pass overlay=False and draw this at the top of its panel instead. COLORS in
|
|
158
|
+
vantagekit.kit.overlay holds the color per mode."""
|
|
159
|
+
mode = self.rig.mode
|
|
160
|
+
title = NAMES.get(mode, mode.upper())
|
|
161
|
+
if self.rig.attached and self.targets.selected is not None:
|
|
162
|
+
title += f" {self.targets.selected}"
|
|
163
|
+
if self._overlay_extra is not None:
|
|
164
|
+
try:
|
|
165
|
+
extra = self._overlay_extra()
|
|
166
|
+
except Exception:
|
|
167
|
+
extra = ""
|
|
168
|
+
if extra:
|
|
169
|
+
title += f" {extra}"
|
|
170
|
+
return mode, title, self._HINTS.get(mode, "")
|
|
171
|
+
|
|
172
|
+
def _update_overlay(self) -> None:
|
|
173
|
+
if not self._overlay_on:
|
|
174
|
+
return
|
|
175
|
+
if self._overlay is None:
|
|
176
|
+
# the viewport window may not exist yet at startup: try about once a second
|
|
177
|
+
now = time.monotonic()
|
|
178
|
+
if now < self._overlay_retry_t:
|
|
179
|
+
return
|
|
180
|
+
self._overlay_retry_t = now + 1.0
|
|
181
|
+
self._overlay = ViewOverlay(f"{self.name}.view")
|
|
182
|
+
if not self._overlay.ready:
|
|
183
|
+
self._overlay = None
|
|
184
|
+
return
|
|
185
|
+
self._overlay.show(*self.view_badge())
|
|
186
|
+
|
|
187
|
+
def on_stage_opened(self) -> None:
|
|
188
|
+
self.rig.reset_stage()
|
|
189
|
+
self.state.set_layer("orbit", False)
|
|
190
|
+
|
|
191
|
+
def help_rows(self) -> List[dict]:
|
|
192
|
+
return self.state.table()
|
|
193
|
+
|
|
194
|
+
def dispose(self) -> None:
|
|
195
|
+
if self._overlay is not None:
|
|
196
|
+
self._overlay.destroy()
|
|
197
|
+
self._overlay = None
|
|
198
|
+
self.router.dispose()
|
|
199
|
+
self.rig.dispose()
|
|
200
|
+
self._handlers.clear()
|
|
201
|
+
self._mode_listeners.clear()
|
|
202
|
+
self._target_listeners.clear()
|
vantagekit/kit/events.py
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""App update and stage-opened subscriptions through Events 2.0 (carb.eventdispatcher).
|
|
2
|
+
|
|
3
|
+
Kit 110 logs deprecation warnings for get_update_event_stream and get_stage_event_stream.
|
|
4
|
+
Both helpers return an observer that unsubscribes when released (set it to None), and fall
|
|
5
|
+
back to the older streams on a Kit without Events 2.0.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from typing import Any, Callable
|
|
11
|
+
|
|
12
|
+
import omni.kit.app
|
|
13
|
+
import omni.usd
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _dispatcher():
|
|
17
|
+
try:
|
|
18
|
+
import carb.eventdispatcher
|
|
19
|
+
|
|
20
|
+
return carb.eventdispatcher.get_eventdispatcher()
|
|
21
|
+
except Exception:
|
|
22
|
+
return None
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def on_update(fn: Callable[[float], None], name: str) -> Any:
|
|
26
|
+
"""Call fn(dt) once per app update."""
|
|
27
|
+
disp = _dispatcher()
|
|
28
|
+
if disp is not None and hasattr(omni.kit.app, "GLOBAL_EVENT_UPDATE"):
|
|
29
|
+
return disp.observe_event(
|
|
30
|
+
observer_name=name,
|
|
31
|
+
event_name=omni.kit.app.GLOBAL_EVENT_UPDATE,
|
|
32
|
+
on_event=lambda e: fn(float(e["dt"])),
|
|
33
|
+
)
|
|
34
|
+
stream = omni.kit.app.get_app().get_update_event_stream()
|
|
35
|
+
return stream.create_subscription_to_pop(lambda e: fn(float(e.payload["dt"])), name=name)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def on_stage_opened(fn: Callable[[], None], name: str, usd_context=None) -> Any:
|
|
39
|
+
"""Call fn() each time a stage finishes opening."""
|
|
40
|
+
ctx = usd_context or omni.usd.get_context()
|
|
41
|
+
disp = _dispatcher()
|
|
42
|
+
if disp is not None and hasattr(ctx, "stage_event_name"):
|
|
43
|
+
return disp.observe_event(
|
|
44
|
+
observer_name=name,
|
|
45
|
+
event_name=ctx.stage_event_name(omni.usd.StageEventType.OPENED),
|
|
46
|
+
on_event=lambda e: fn(),
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
def legacy(e):
|
|
50
|
+
if e.type == int(omni.usd.StageEventType.OPENED):
|
|
51
|
+
fn()
|
|
52
|
+
|
|
53
|
+
return ctx.get_stage_event_stream().create_subscription_to_pop(legacy, name=name)
|
vantagekit/kit/input.py
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
"""carb.input adapter: feeds a BindingState from keyboard, mouse and gamepad.
|
|
2
|
+
|
|
3
|
+
Kit 110 contract (carb/input/InputTypes.h, OnInputEventFn): the callback returns whether
|
|
4
|
+
the event should be processed by subsequent subscribers, so True passes it on and False
|
|
5
|
+
consumes it. The router passes everything on by default and leaves native navigation to
|
|
6
|
+
NativeNavigation; with consume=True it consumes the keys and mouse inputs that an active
|
|
7
|
+
layer binds.
|
|
8
|
+
|
|
9
|
+
The gamepad is polled once per frame (carb gamepads do not repeat, and polling gives the
|
|
10
|
+
analog levels); gamepad input events are ignored so nothing is counted twice.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import carb
|
|
16
|
+
import carb.input
|
|
17
|
+
|
|
18
|
+
from ..core.bindings import BindingState, Frame
|
|
19
|
+
|
|
20
|
+
_STICK_AXES = {
|
|
21
|
+
"LEFT_STICK_X": ("LEFT_STICK_RIGHT", "LEFT_STICK_LEFT"),
|
|
22
|
+
"LEFT_STICK_Y": ("LEFT_STICK_UP", "LEFT_STICK_DOWN"),
|
|
23
|
+
"RIGHT_STICK_X": ("RIGHT_STICK_RIGHT", "RIGHT_STICK_LEFT"),
|
|
24
|
+
"RIGHT_STICK_Y": ("RIGHT_STICK_UP", "RIGHT_STICK_DOWN"),
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _mods(flags: int) -> list:
|
|
29
|
+
out = []
|
|
30
|
+
ci = carb.input
|
|
31
|
+
if flags & int(getattr(ci, "KEYBOARD_MODIFIER_FLAG_CONTROL", 0)):
|
|
32
|
+
out.append("ctrl")
|
|
33
|
+
if flags & int(getattr(ci, "KEYBOARD_MODIFIER_FLAG_SHIFT", 0)):
|
|
34
|
+
out.append("shift")
|
|
35
|
+
if flags & int(getattr(ci, "KEYBOARD_MODIFIER_FLAG_ALT", 0)):
|
|
36
|
+
out.append("alt")
|
|
37
|
+
return out
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class InputRouter:
|
|
41
|
+
def __init__(self, state: BindingState, order: int = 0, consume: bool = False, name: str = "vantagekit"):
|
|
42
|
+
self.state = state
|
|
43
|
+
self.name = name
|
|
44
|
+
self._consume = consume
|
|
45
|
+
self._input = carb.input.acquire_input_interface()
|
|
46
|
+
self._keys = {}
|
|
47
|
+
for inp in state.inputs("key"):
|
|
48
|
+
enum = getattr(carb.input.KeyboardInput, inp.split(":", 1)[1], None)
|
|
49
|
+
if enum is None:
|
|
50
|
+
carb.log_warn(f"[{name}] unknown key in bindings: {inp}")
|
|
51
|
+
else:
|
|
52
|
+
self._keys[enum] = inp
|
|
53
|
+
self._pad_inputs = {}
|
|
54
|
+
for inp in state.inputs("pad"):
|
|
55
|
+
key = inp.split(":", 1)[1]
|
|
56
|
+
if key in _STICK_AXES:
|
|
57
|
+
pos, neg = _STICK_AXES[key]
|
|
58
|
+
self._pad_inputs[inp] = (
|
|
59
|
+
getattr(carb.input.GamepadInput, pos),
|
|
60
|
+
getattr(carb.input.GamepadInput, neg),
|
|
61
|
+
)
|
|
62
|
+
else:
|
|
63
|
+
enum = getattr(carb.input.GamepadInput, key, None)
|
|
64
|
+
if enum is None:
|
|
65
|
+
carb.log_warn(f"[{name}] unknown gamepad input in bindings: {inp}")
|
|
66
|
+
else:
|
|
67
|
+
self._pad_inputs[inp] = (enum, None)
|
|
68
|
+
self._pads = []
|
|
69
|
+
self._drag_from = None
|
|
70
|
+
# Optional callable: mouse events count only while it returns True. carb input is
|
|
71
|
+
# app-wide, so a viewer with its own pop-up (a second view with its own orbit) sets
|
|
72
|
+
# this to ignore drags and the wheel while the pointer is over that pop-up.
|
|
73
|
+
self.mouse_gate = None
|
|
74
|
+
self._sub = self._input.subscribe_to_input_events(self._on_event, order=order)
|
|
75
|
+
self._pad_sub = self._input.subscribe_to_gamepad_connection_events(self._on_pad)
|
|
76
|
+
|
|
77
|
+
# events -------------------------------------------------------------------
|
|
78
|
+
def _on_pad(self, e) -> None:
|
|
79
|
+
if e.type == carb.input.GamepadConnectionEventType.CREATED:
|
|
80
|
+
if e.gamepad not in self._pads:
|
|
81
|
+
self._pads.append(e.gamepad)
|
|
82
|
+
carb.log_info(f"[{self.name}] gamepad connected: {self._input.get_gamepad_name(e.gamepad)}")
|
|
83
|
+
elif e.type == carb.input.GamepadConnectionEventType.DESTROYED:
|
|
84
|
+
if e.gamepad in self._pads:
|
|
85
|
+
self._pads.remove(e.gamepad)
|
|
86
|
+
self.state.release_all()
|
|
87
|
+
carb.log_info(f"[{self.name}] gamepad disconnected")
|
|
88
|
+
|
|
89
|
+
def _on_event(self, event) -> bool:
|
|
90
|
+
dt = event.deviceType
|
|
91
|
+
if dt == carb.input.DeviceType.KEYBOARD:
|
|
92
|
+
return self._on_key(event.event)
|
|
93
|
+
if dt == carb.input.DeviceType.MOUSE:
|
|
94
|
+
return self._on_mouse(event.event)
|
|
95
|
+
return True # gamepad: polled in poll()
|
|
96
|
+
|
|
97
|
+
def _on_key(self, e) -> bool:
|
|
98
|
+
inp = self._keys.get(e.input)
|
|
99
|
+
if inp is None:
|
|
100
|
+
return True
|
|
101
|
+
t = e.type
|
|
102
|
+
if t == carb.input.KeyboardEventType.KEY_PRESS:
|
|
103
|
+
self.state.key_down(inp, _mods(int(e.modifiers)))
|
|
104
|
+
elif t == carb.input.KeyboardEventType.KEY_RELEASE:
|
|
105
|
+
self.state.key_up(inp)
|
|
106
|
+
# KEY_REPEAT and CHAR: ignored; a held key fires its press once
|
|
107
|
+
return not (self._consume and self.state.claims(inp))
|
|
108
|
+
|
|
109
|
+
def _on_mouse(self, e) -> bool:
|
|
110
|
+
if self.mouse_gate is not None and not self.mouse_gate():
|
|
111
|
+
self._drag_from = None # a drag that wanders into the pop-up ends there
|
|
112
|
+
return True
|
|
113
|
+
t = e.type
|
|
114
|
+
mt = carb.input.MouseEventType
|
|
115
|
+
claimed = False
|
|
116
|
+
if t == mt.LEFT_BUTTON_DOWN:
|
|
117
|
+
self._drag_from = (e.normalized_coords.x, e.normalized_coords.y)
|
|
118
|
+
claimed = self.state.claims("mouse:DRAG_X") or self.state.claims("mouse:DRAG_Y")
|
|
119
|
+
elif t == mt.LEFT_BUTTON_UP:
|
|
120
|
+
self._drag_from = None
|
|
121
|
+
elif t == mt.MOVE and self._drag_from is not None:
|
|
122
|
+
x, y = e.normalized_coords.x, e.normalized_coords.y
|
|
123
|
+
self.state.add_delta("mouse:DRAG_X", x - self._drag_from[0])
|
|
124
|
+
self.state.add_delta("mouse:DRAG_Y", y - self._drag_from[1])
|
|
125
|
+
self._drag_from = (x, y)
|
|
126
|
+
claimed = self.state.claims("mouse:DRAG_X") or self.state.claims("mouse:DRAG_Y")
|
|
127
|
+
elif t == mt.SCROLL:
|
|
128
|
+
self.state.add_delta("mouse:WHEEL", e.scrollDelta.y)
|
|
129
|
+
claimed = self.state.claims("mouse:WHEEL")
|
|
130
|
+
return not (self._consume and claimed)
|
|
131
|
+
|
|
132
|
+
# per frame ----------------------------------------------------------------
|
|
133
|
+
def poll(self, dt: float) -> Frame:
|
|
134
|
+
if self._pads:
|
|
135
|
+
gp = self._pads[0]
|
|
136
|
+
get = self._input.get_gamepad_value
|
|
137
|
+
for inp, (pos, neg) in self._pad_inputs.items():
|
|
138
|
+
v = float(get(gp, pos))
|
|
139
|
+
if neg is not None:
|
|
140
|
+
v -= float(get(gp, neg))
|
|
141
|
+
self.state.set_level(inp, v)
|
|
142
|
+
return self.state.step(dt)
|
|
143
|
+
|
|
144
|
+
def dispose(self) -> None:
|
|
145
|
+
if self._sub is not None:
|
|
146
|
+
self._input.unsubscribe_to_input_events(self._sub)
|
|
147
|
+
self._sub = None
|
|
148
|
+
if self._pad_sub is not None:
|
|
149
|
+
self._input.unsubscribe_to_gamepad_connection_events(self._pad_sub)
|
|
150
|
+
self._pad_sub = None
|
|
151
|
+
self._pads = []
|
|
152
|
+
self.state.release_all()
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"""Hand camera navigation between Kit and the rig, only when the mode changes.
|
|
2
|
+
|
|
3
|
+
Kit 110 has two independent native controllers:
|
|
4
|
+
|
|
5
|
+
- Mouse and keyboard navigation (omni.kit.manipulator.camera): at the start of every
|
|
6
|
+
gesture it reads the camera prim's `omni:kit:cameraLock` attribute and, when it is true,
|
|
7
|
+
disables tumble, look, pan, zoom and fly for that camera. Locking the rig's camera is a
|
|
8
|
+
per-camera switch that leaves other viewports and cameras alone.
|
|
9
|
+
- Gamepad navigation: enabled by the persistent setting
|
|
10
|
+
`/persistent/app/omniverse/gamepadCameraControl`, which the viewport window watches.
|
|
11
|
+
Isaac Sim's app file sets it false. It is persistent (saved in the user config), so the
|
|
12
|
+
value found at startup is restored on dispose.
|
|
13
|
+
|
|
14
|
+
Nothing here runs per frame.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import carb
|
|
20
|
+
import carb.settings
|
|
21
|
+
from pxr import Sdf
|
|
22
|
+
|
|
23
|
+
CAMERA_LOCK_ATTR = "omni:kit:cameraLock"
|
|
24
|
+
GAMEPAD_SETTING = "/persistent/app/omniverse/gamepadCameraControl"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class NativeNavigation:
|
|
28
|
+
"""free_gamepad: native gamepad navigation in free mode; None keeps the value found at
|
|
29
|
+
startup (the app may want it off so the sticks stay with its own controls)."""
|
|
30
|
+
|
|
31
|
+
def __init__(self, free_gamepad: bool | None = None):
|
|
32
|
+
self._settings = carb.settings.get_settings()
|
|
33
|
+
self._saved_gamepad = self._settings.get(GAMEPAD_SETTING)
|
|
34
|
+
self._free_gamepad = free_gamepad
|
|
35
|
+
self._locked: set = set() # (stage id, path) we locked
|
|
36
|
+
self._native: bool | None = None
|
|
37
|
+
|
|
38
|
+
def _gamepad_value(self, native: bool) -> bool:
|
|
39
|
+
if not native:
|
|
40
|
+
return False
|
|
41
|
+
if self._free_gamepad is not None:
|
|
42
|
+
return bool(self._free_gamepad)
|
|
43
|
+
return bool(self._saved_gamepad)
|
|
44
|
+
|
|
45
|
+
def set_native(self, native: bool, stage=None, camera_path: str | None = None) -> None:
|
|
46
|
+
"""native=True gives the camera to Kit (free mode); False gives it to the rig."""
|
|
47
|
+
if self._native == native:
|
|
48
|
+
return
|
|
49
|
+
self._native = native
|
|
50
|
+
want = self._gamepad_value(native)
|
|
51
|
+
if bool(self._settings.get(GAMEPAD_SETTING)) != want:
|
|
52
|
+
self._settings.set_bool(GAMEPAD_SETTING, want)
|
|
53
|
+
if stage is not None and camera_path:
|
|
54
|
+
self._lock(stage, camera_path, not native)
|
|
55
|
+
|
|
56
|
+
def _lock(self, stage, camera_path: str, on: bool) -> None:
|
|
57
|
+
prim = stage.GetPrimAtPath(camera_path)
|
|
58
|
+
if not prim or not prim.IsValid():
|
|
59
|
+
return
|
|
60
|
+
key = (id(stage), str(camera_path))
|
|
61
|
+
if on:
|
|
62
|
+
attr = prim.CreateAttribute(CAMERA_LOCK_ATTR, Sdf.ValueTypeNames.Bool, custom=True)
|
|
63
|
+
attr.Set(True)
|
|
64
|
+
self._locked.add(key)
|
|
65
|
+
elif key in self._locked or prim.GetAttribute(CAMERA_LOCK_ATTR):
|
|
66
|
+
attr = prim.GetAttribute(CAMERA_LOCK_ATTR)
|
|
67
|
+
if attr:
|
|
68
|
+
attr.Set(False)
|
|
69
|
+
self._locked.discard(key)
|
|
70
|
+
|
|
71
|
+
def reset(self) -> None:
|
|
72
|
+
"""Forget the applied state (a new stage was opened); the next set_native applies."""
|
|
73
|
+
self._native = None
|
|
74
|
+
self._locked.clear()
|
|
75
|
+
|
|
76
|
+
def dispose(self, stage=None, camera_path: str | None = None) -> None:
|
|
77
|
+
if stage is not None and camera_path:
|
|
78
|
+
try:
|
|
79
|
+
self._lock(stage, camera_path, False)
|
|
80
|
+
except Exception as e: # stage may be closing
|
|
81
|
+
carb.log_warn(f"[vantagekit] could not unlock {camera_path}: {e}")
|
|
82
|
+
if self._saved_gamepad is not None:
|
|
83
|
+
self._settings.set_bool(GAMEPAD_SETTING, bool(self._saved_gamepad))
|
|
84
|
+
self._native = None
|