goodnavigate 0.1.0__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.
GoodNavigate.py
ADDED
goodnavigate/__init__.py
ADDED
|
@@ -0,0 +1,727 @@
|
|
|
1
|
+
"""Comfortable camera navigation helper for WorldViz Vizard 8.
|
|
2
|
+
|
|
3
|
+
Typical use from another Vizard script::
|
|
4
|
+
|
|
5
|
+
import viz
|
|
6
|
+
import goodnavigate
|
|
7
|
+
|
|
8
|
+
viz.go()
|
|
9
|
+
goodnavigate.enable()
|
|
10
|
+
|
|
11
|
+
Importing this module does not start Vizard or add scene objects. Call enable()
|
|
12
|
+
after viz.go() in the script that owns the scene.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
import ctypes
|
|
16
|
+
import math
|
|
17
|
+
import os
|
|
18
|
+
|
|
19
|
+
import viz
|
|
20
|
+
import vizact
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
DEFAULT_SPEED = 2.0
|
|
24
|
+
DEFAULT_FAST_MULT = 3.0
|
|
25
|
+
DEFAULT_SENSE = 0.2
|
|
26
|
+
DEFAULT_SPEED_MULT = 1.0
|
|
27
|
+
MIN_SPEED_MULT = 0.25
|
|
28
|
+
MAX_SPEED_MULT = 5.0
|
|
29
|
+
SPEED_MULT_STEP = 0.25
|
|
30
|
+
SPEED_OVERLAY_DURATION = 1.0
|
|
31
|
+
SPEED_OVERLAY_BG_FILE = 'goodnavigate_overlay_bg.png'
|
|
32
|
+
SPEED_OVERLAY_BG_SCALE_X = 3.25
|
|
33
|
+
SPEED_OVERLAY_BG_ASPECT = 512.0 / 192.0
|
|
34
|
+
SPEED_OVERLAY_TEXT_SCALE_X = 1.0
|
|
35
|
+
DEFAULT_HEIGHT_LOCK = True
|
|
36
|
+
DEFAULT_FORWARD_KEY = 'w'
|
|
37
|
+
DEFAULT_BACKWARD_KEY = 's'
|
|
38
|
+
DEFAULT_LEFT_KEY = 'a'
|
|
39
|
+
DEFAULT_RIGHT_KEY = 'd'
|
|
40
|
+
DEFAULT_UP_KEY = ' '
|
|
41
|
+
DEFAULT_DOWN_KEY = 'c'
|
|
42
|
+
DEFAULT_TOGGLE_HEIGHT_LOCK_KEY = 'f'
|
|
43
|
+
DEFAULT_CHECK_WINDOW_FOCUS = True
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class CameraNavigator(object):
|
|
47
|
+
"""WASD + mouse camera controller for a Vizard view."""
|
|
48
|
+
|
|
49
|
+
def __init__(self, view=None, speed=DEFAULT_SPEED, fast_mult=DEFAULT_FAST_MULT,
|
|
50
|
+
sense=DEFAULT_SENSE, speed_mult=DEFAULT_SPEED_MULT,
|
|
51
|
+
min_speed_mult=MIN_SPEED_MULT, max_speed_mult=MAX_SPEED_MULT,
|
|
52
|
+
speed_mult_step=SPEED_MULT_STEP, mouse_override=True,
|
|
53
|
+
show_speed_overlay=True,
|
|
54
|
+
speed_overlay_duration=SPEED_OVERLAY_DURATION,
|
|
55
|
+
height_lock=DEFAULT_HEIGHT_LOCK,
|
|
56
|
+
forward_key=DEFAULT_FORWARD_KEY,
|
|
57
|
+
backward_key=DEFAULT_BACKWARD_KEY,
|
|
58
|
+
left_key=DEFAULT_LEFT_KEY,
|
|
59
|
+
right_key=DEFAULT_RIGHT_KEY,
|
|
60
|
+
up_key=DEFAULT_UP_KEY,
|
|
61
|
+
down_key=DEFAULT_DOWN_KEY,
|
|
62
|
+
toggle_height_lock_key=DEFAULT_TOGGLE_HEIGHT_LOCK_KEY,
|
|
63
|
+
fast_keys=None,
|
|
64
|
+
check_window_focus=DEFAULT_CHECK_WINDOW_FOCUS):
|
|
65
|
+
self.view = view or viz.MainView
|
|
66
|
+
self.speed = speed
|
|
67
|
+
self.fast_mult = fast_mult
|
|
68
|
+
self.sense = sense
|
|
69
|
+
self.speed_mult = speed_mult
|
|
70
|
+
self.min_speed_mult = min_speed_mult
|
|
71
|
+
self.max_speed_mult = max_speed_mult
|
|
72
|
+
self.speed_mult_step = speed_mult_step
|
|
73
|
+
self.mouse_override = mouse_override
|
|
74
|
+
self.show_speed_overlay = show_speed_overlay
|
|
75
|
+
self.speed_overlay_duration = speed_overlay_duration
|
|
76
|
+
self.height_lock = bool(height_lock)
|
|
77
|
+
self.forward_key = forward_key
|
|
78
|
+
self.backward_key = backward_key
|
|
79
|
+
self.left_key = left_key
|
|
80
|
+
self.right_key = right_key
|
|
81
|
+
self.up_key = up_key
|
|
82
|
+
self.down_key = down_key
|
|
83
|
+
self.toggle_height_lock_key = toggle_height_lock_key
|
|
84
|
+
self.fast_keys = tuple(fast_keys) if fast_keys is not None else (viz.KEY_SHIFT_L, viz.KEY_SHIFT_R)
|
|
85
|
+
self.check_window_focus = check_window_focus
|
|
86
|
+
self.active_button = None
|
|
87
|
+
self.enabled = False
|
|
88
|
+
self._registered = False
|
|
89
|
+
self._uses_key_callbacks = False
|
|
90
|
+
self._pressed_keys = set()
|
|
91
|
+
self._window_handle = None
|
|
92
|
+
self._timer = None
|
|
93
|
+
self._speed_overlay = None
|
|
94
|
+
self._speed_overlay_bg = None
|
|
95
|
+
self._speed_overlay_remaining = 0.0
|
|
96
|
+
self._last_overlay_aspect = None
|
|
97
|
+
self._height_lock_toggle_down = False
|
|
98
|
+
|
|
99
|
+
def enable(self):
|
|
100
|
+
"""Enable mouse-look and keyboard movement callbacks."""
|
|
101
|
+
if self.mouse_override:
|
|
102
|
+
viz.mouse.setOverride(viz.ON)
|
|
103
|
+
|
|
104
|
+
if self.show_speed_overlay:
|
|
105
|
+
self._ensure_speed_overlay()
|
|
106
|
+
|
|
107
|
+
if not self._registered:
|
|
108
|
+
viz.callback(viz.MOUSEDOWN_EVENT, self.on_mouse_down)
|
|
109
|
+
viz.callback(viz.MOUSEUP_EVENT, self.on_mouse_up)
|
|
110
|
+
viz.callback(viz.MOUSE_MOVE_EVENT, self.on_mouse_move)
|
|
111
|
+
mouse_wheel_event = getattr(viz, 'MOUSEWHEEL_EVENT', None)
|
|
112
|
+
if mouse_wheel_event is not None:
|
|
113
|
+
viz.callback(mouse_wheel_event, self.on_mouse_wheel)
|
|
114
|
+
keydown_event = getattr(viz, 'KEYDOWN_EVENT', None)
|
|
115
|
+
keyup_event = getattr(viz, 'KEYUP_EVENT', None)
|
|
116
|
+
if keydown_event is not None and keyup_event is not None:
|
|
117
|
+
viz.callback(keydown_event, self.on_key_down)
|
|
118
|
+
viz.callback(keyup_event, self.on_key_up)
|
|
119
|
+
self._uses_key_callbacks = True
|
|
120
|
+
self._timer = vizact.ontimer(0, self.update_movement)
|
|
121
|
+
self._registered = True
|
|
122
|
+
|
|
123
|
+
self.enabled = True
|
|
124
|
+
return self
|
|
125
|
+
|
|
126
|
+
def disable(self):
|
|
127
|
+
"""Pause controls and return mouse handling to Vizard defaults."""
|
|
128
|
+
self.pause()
|
|
129
|
+
if self.mouse_override:
|
|
130
|
+
viz.mouse.setOverride(viz.OFF)
|
|
131
|
+
return self
|
|
132
|
+
|
|
133
|
+
def pause(self):
|
|
134
|
+
"""Pause GoodNavigate controls without changing Vizard mouse override state."""
|
|
135
|
+
self.enabled = False
|
|
136
|
+
self._clear_input_state()
|
|
137
|
+
self._speed_overlay_remaining = 0.0
|
|
138
|
+
self._last_overlay_aspect = None
|
|
139
|
+
self._set_speed_overlay_visible(False)
|
|
140
|
+
return self
|
|
141
|
+
|
|
142
|
+
def resume(self):
|
|
143
|
+
"""Resume GoodNavigate controls after pause()."""
|
|
144
|
+
if self.mouse_override:
|
|
145
|
+
viz.mouse.setOverride(viz.ON)
|
|
146
|
+
self.enabled = True
|
|
147
|
+
return self
|
|
148
|
+
|
|
149
|
+
def configure(self, speed=None, fast_mult=None, sense=None, view=None,
|
|
150
|
+
speed_mult=None, show_speed_overlay=None, height_lock=None,
|
|
151
|
+
forward_key=None, backward_key=None, left_key=None,
|
|
152
|
+
right_key=None, up_key=None, down_key=None,
|
|
153
|
+
toggle_height_lock_key=None, fast_keys=None,
|
|
154
|
+
check_window_focus=None):
|
|
155
|
+
"""Update controller settings without registering callbacks again."""
|
|
156
|
+
if speed is not None:
|
|
157
|
+
self.speed = speed
|
|
158
|
+
if fast_mult is not None:
|
|
159
|
+
self.fast_mult = fast_mult
|
|
160
|
+
if sense is not None:
|
|
161
|
+
self.sense = sense
|
|
162
|
+
if view is not None:
|
|
163
|
+
self.view = view
|
|
164
|
+
if show_speed_overlay is not None:
|
|
165
|
+
self.show_speed_overlay = show_speed_overlay
|
|
166
|
+
if not show_speed_overlay:
|
|
167
|
+
self._set_speed_overlay_visible(False)
|
|
168
|
+
if speed_mult is not None:
|
|
169
|
+
self.set_speed_mult(speed_mult, show_overlay=False)
|
|
170
|
+
if height_lock is not None:
|
|
171
|
+
self.set_height_lock(height_lock)
|
|
172
|
+
if check_window_focus is not None:
|
|
173
|
+
self.check_window_focus = check_window_focus
|
|
174
|
+
self.set_keys(
|
|
175
|
+
forward=forward_key,
|
|
176
|
+
backward=backward_key,
|
|
177
|
+
left=left_key,
|
|
178
|
+
right=right_key,
|
|
179
|
+
up=up_key,
|
|
180
|
+
down=down_key,
|
|
181
|
+
toggle_height_lock=toggle_height_lock_key,
|
|
182
|
+
fast=fast_keys,
|
|
183
|
+
)
|
|
184
|
+
return self
|
|
185
|
+
|
|
186
|
+
def set_speed(self, value):
|
|
187
|
+
"""Set the base movement speed."""
|
|
188
|
+
self.speed = value
|
|
189
|
+
return self.speed
|
|
190
|
+
|
|
191
|
+
def set_fast_mult(self, value):
|
|
192
|
+
"""Set the temporary fast movement multiplier."""
|
|
193
|
+
self.fast_mult = value
|
|
194
|
+
return self.fast_mult
|
|
195
|
+
|
|
196
|
+
def set_sense(self, value):
|
|
197
|
+
"""Set the mouse-look sensitivity."""
|
|
198
|
+
self.sense = value
|
|
199
|
+
return self.sense
|
|
200
|
+
|
|
201
|
+
def set_speed_mult(self, value, show_overlay=True):
|
|
202
|
+
"""Set the scroll-adjusted movement speed multiplier."""
|
|
203
|
+
self.speed_mult = max(self.min_speed_mult, min(self.max_speed_mult, value))
|
|
204
|
+
if show_overlay:
|
|
205
|
+
self._show_speed_overlay()
|
|
206
|
+
return self.speed_mult
|
|
207
|
+
|
|
208
|
+
def reset_speed_mult(self):
|
|
209
|
+
"""Reset the scroll-adjusted movement speed multiplier to x1.00."""
|
|
210
|
+
return self.set_speed_mult(DEFAULT_SPEED_MULT)
|
|
211
|
+
|
|
212
|
+
def get_speed_mult(self):
|
|
213
|
+
"""Return the current scroll-adjusted movement speed multiplier."""
|
|
214
|
+
return self.speed_mult
|
|
215
|
+
|
|
216
|
+
def set_keys(self, forward=None, backward=None, left=None, right=None,
|
|
217
|
+
up=None, down=None, toggle_height_lock=None, fast=None):
|
|
218
|
+
"""Update keyboard bindings. Pass None to leave a binding unchanged."""
|
|
219
|
+
if forward is not None:
|
|
220
|
+
self.forward_key = forward
|
|
221
|
+
if backward is not None:
|
|
222
|
+
self.backward_key = backward
|
|
223
|
+
if left is not None:
|
|
224
|
+
self.left_key = left
|
|
225
|
+
if right is not None:
|
|
226
|
+
self.right_key = right
|
|
227
|
+
if up is not None:
|
|
228
|
+
self.up_key = up
|
|
229
|
+
if down is not None:
|
|
230
|
+
self.down_key = down
|
|
231
|
+
if toggle_height_lock is not None:
|
|
232
|
+
self.toggle_height_lock_key = toggle_height_lock
|
|
233
|
+
if fast is not None:
|
|
234
|
+
self.fast_keys = tuple(fast)
|
|
235
|
+
return self
|
|
236
|
+
|
|
237
|
+
def get_status(self):
|
|
238
|
+
"""Return the current GoodNavigate state as a dictionary."""
|
|
239
|
+
return {
|
|
240
|
+
'enabled': self.enabled,
|
|
241
|
+
'height_lock': self.height_lock,
|
|
242
|
+
'speed': self.speed,
|
|
243
|
+
'fast_mult': self.fast_mult,
|
|
244
|
+
'sense': self.sense,
|
|
245
|
+
'speed_mult': self.speed_mult,
|
|
246
|
+
'mouse_override': self.mouse_override,
|
|
247
|
+
'show_speed_overlay': self.show_speed_overlay,
|
|
248
|
+
'window_active': self._is_window_active(),
|
|
249
|
+
'check_window_focus': self.check_window_focus,
|
|
250
|
+
'keys': {
|
|
251
|
+
'forward': self.forward_key,
|
|
252
|
+
'backward': self.backward_key,
|
|
253
|
+
'left': self.left_key,
|
|
254
|
+
'right': self.right_key,
|
|
255
|
+
'up': self.up_key,
|
|
256
|
+
'down': self.down_key,
|
|
257
|
+
'toggle_height_lock': self.toggle_height_lock_key,
|
|
258
|
+
'fast': self.fast_keys,
|
|
259
|
+
},
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
def set_height_lock(self, locked, show_overlay=False):
|
|
263
|
+
"""Set whether WASD movement ignores camera pitch."""
|
|
264
|
+
self.height_lock = bool(locked)
|
|
265
|
+
if show_overlay:
|
|
266
|
+
self._show_height_lock_overlay()
|
|
267
|
+
return self.height_lock
|
|
268
|
+
|
|
269
|
+
def toggle_height_lock(self, show_overlay=True):
|
|
270
|
+
"""Toggle whether WASD movement ignores camera pitch."""
|
|
271
|
+
return self.set_height_lock(not self.height_lock, show_overlay=show_overlay)
|
|
272
|
+
|
|
273
|
+
def is_height_locked(self):
|
|
274
|
+
"""Return True when WASD movement is constrained to horizontal motion."""
|
|
275
|
+
return self.height_lock
|
|
276
|
+
|
|
277
|
+
def on_mouse_down(self, button):
|
|
278
|
+
if (not self.enabled or self.active_button is not None or
|
|
279
|
+
button not in (viz.MOUSEBUTTON_LEFT, viz.MOUSEBUTTON_RIGHT)):
|
|
280
|
+
return
|
|
281
|
+
|
|
282
|
+
self.active_button = button
|
|
283
|
+
viz.mouse.setTrap(viz.ON)
|
|
284
|
+
viz.mouse.setVisible(viz.OFF)
|
|
285
|
+
|
|
286
|
+
if button == viz.MOUSEBUTTON_LEFT:
|
|
287
|
+
yaw, _pitch, roll = self.view.getEuler()
|
|
288
|
+
self.view.setEuler([yaw, 0, roll])
|
|
289
|
+
|
|
290
|
+
def on_mouse_up(self, button):
|
|
291
|
+
if not self.enabled or button != self.active_button:
|
|
292
|
+
return
|
|
293
|
+
|
|
294
|
+
viz.mouse.setTrap(viz.OFF)
|
|
295
|
+
viz.mouse.setVisible(viz.ON)
|
|
296
|
+
self.active_button = None
|
|
297
|
+
|
|
298
|
+
def on_mouse_move(self, event):
|
|
299
|
+
if not self.enabled or self.active_button is None:
|
|
300
|
+
return
|
|
301
|
+
|
|
302
|
+
yaw, pitch, roll = self.view.getEuler()
|
|
303
|
+
yaw += event.dx * self.sense
|
|
304
|
+
|
|
305
|
+
if self.active_button == viz.MOUSEBUTTON_RIGHT:
|
|
306
|
+
pitch = max(-89.0, min(89.0, pitch - event.dy * self.sense))
|
|
307
|
+
|
|
308
|
+
self.view.setEuler([yaw, pitch, roll])
|
|
309
|
+
|
|
310
|
+
def on_mouse_wheel(self, *args):
|
|
311
|
+
if not self.enabled:
|
|
312
|
+
return
|
|
313
|
+
|
|
314
|
+
delta = self._get_wheel_delta(args)
|
|
315
|
+
if delta == 0:
|
|
316
|
+
return
|
|
317
|
+
|
|
318
|
+
step = self.speed_mult_step if delta > 0 else -self.speed_mult_step
|
|
319
|
+
self.set_speed_mult(self.speed_mult + step)
|
|
320
|
+
|
|
321
|
+
def on_key_down(self, *args):
|
|
322
|
+
if not self.enabled or not self._is_window_active():
|
|
323
|
+
return
|
|
324
|
+
|
|
325
|
+
key = self._get_key_value(args)
|
|
326
|
+
if key is None:
|
|
327
|
+
return
|
|
328
|
+
|
|
329
|
+
normalized = self._normalize_key(key)
|
|
330
|
+
self._pressed_keys.add(normalized)
|
|
331
|
+
if (self._keys_match(key, self.toggle_height_lock_key) and
|
|
332
|
+
not self._height_lock_toggle_down):
|
|
333
|
+
self.toggle_height_lock(show_overlay=False)
|
|
334
|
+
self._height_lock_toggle_down = True
|
|
335
|
+
|
|
336
|
+
def on_key_up(self, *args):
|
|
337
|
+
key = self._get_key_value(args)
|
|
338
|
+
if key is None:
|
|
339
|
+
return
|
|
340
|
+
|
|
341
|
+
normalized = self._normalize_key(key)
|
|
342
|
+
self._pressed_keys.discard(normalized)
|
|
343
|
+
if self._keys_match(key, self.toggle_height_lock_key):
|
|
344
|
+
self._height_lock_toggle_down = False
|
|
345
|
+
|
|
346
|
+
def _get_key_value(self, args):
|
|
347
|
+
for value in args:
|
|
348
|
+
if isinstance(value, (str, int, float)):
|
|
349
|
+
return value
|
|
350
|
+
|
|
351
|
+
for attr in ('key', 'char', 'name'):
|
|
352
|
+
if hasattr(value, attr):
|
|
353
|
+
return getattr(value, attr)
|
|
354
|
+
|
|
355
|
+
return None
|
|
356
|
+
|
|
357
|
+
def _normalize_key(self, key):
|
|
358
|
+
if isinstance(key, str):
|
|
359
|
+
return key.lower()
|
|
360
|
+
return key
|
|
361
|
+
|
|
362
|
+
def _keys_match(self, actual, expected):
|
|
363
|
+
return self._normalize_key(actual) == self._normalize_key(expected)
|
|
364
|
+
|
|
365
|
+
def _is_key_down(self, key):
|
|
366
|
+
if not self._is_window_active():
|
|
367
|
+
return False
|
|
368
|
+
if self._uses_key_callbacks:
|
|
369
|
+
return self._normalize_key(key) in self._pressed_keys
|
|
370
|
+
return viz.key.isDown(key)
|
|
371
|
+
|
|
372
|
+
def _clear_input_state(self):
|
|
373
|
+
self._pressed_keys.clear()
|
|
374
|
+
self._height_lock_toggle_down = False
|
|
375
|
+
if self.active_button is not None:
|
|
376
|
+
self.active_button = None
|
|
377
|
+
viz.mouse.setTrap(viz.OFF)
|
|
378
|
+
viz.mouse.setVisible(viz.ON)
|
|
379
|
+
|
|
380
|
+
def _is_window_active(self):
|
|
381
|
+
if not self.check_window_focus:
|
|
382
|
+
return True
|
|
383
|
+
try:
|
|
384
|
+
return self._is_foreground_window()
|
|
385
|
+
except Exception:
|
|
386
|
+
return True
|
|
387
|
+
|
|
388
|
+
def _is_foreground_window(self):
|
|
389
|
+
if os.name != 'nt':
|
|
390
|
+
return True
|
|
391
|
+
|
|
392
|
+
handle = self._get_window_handle()
|
|
393
|
+
if not handle:
|
|
394
|
+
return True
|
|
395
|
+
|
|
396
|
+
user32 = ctypes.windll.user32
|
|
397
|
+
foreground = user32.GetForegroundWindow()
|
|
398
|
+
if not foreground:
|
|
399
|
+
return True
|
|
400
|
+
|
|
401
|
+
if foreground == handle:
|
|
402
|
+
return True
|
|
403
|
+
|
|
404
|
+
ga_root = 2
|
|
405
|
+
root = user32.GetAncestor(handle, ga_root)
|
|
406
|
+
foreground_root = user32.GetAncestor(foreground, ga_root)
|
|
407
|
+
return root != 0 and root == foreground_root
|
|
408
|
+
|
|
409
|
+
def _get_window_handle(self):
|
|
410
|
+
if self._window_handle:
|
|
411
|
+
return self._window_handle
|
|
412
|
+
|
|
413
|
+
get_handle = getattr(viz.window, 'getHandle', None)
|
|
414
|
+
if get_handle is None:
|
|
415
|
+
return None
|
|
416
|
+
|
|
417
|
+
try:
|
|
418
|
+
self._window_handle = int(get_handle())
|
|
419
|
+
except Exception:
|
|
420
|
+
self._window_handle = None
|
|
421
|
+
return self._window_handle
|
|
422
|
+
|
|
423
|
+
def _get_wheel_delta(self, args):
|
|
424
|
+
for value in args:
|
|
425
|
+
if isinstance(value, (int, float)):
|
|
426
|
+
return value
|
|
427
|
+
|
|
428
|
+
for attr in ('wheel', 'delta', 'direction', 'dir'):
|
|
429
|
+
if hasattr(value, attr):
|
|
430
|
+
attr_value = getattr(value, attr)
|
|
431
|
+
if isinstance(attr_value, (int, float)):
|
|
432
|
+
return attr_value
|
|
433
|
+
|
|
434
|
+
return 0
|
|
435
|
+
|
|
436
|
+
def _ensure_speed_overlay(self):
|
|
437
|
+
if self._speed_overlay is not None:
|
|
438
|
+
return self._speed_overlay
|
|
439
|
+
|
|
440
|
+
try:
|
|
441
|
+
self._speed_overlay_bg = viz.addTexQuad(parent=viz.SCREEN)
|
|
442
|
+
self._speed_overlay = self._create_speed_overlay_text()
|
|
443
|
+
except Exception:
|
|
444
|
+
self._speed_overlay = None
|
|
445
|
+
self._speed_overlay_bg = None
|
|
446
|
+
return None
|
|
447
|
+
|
|
448
|
+
self._configure_speed_overlay_bg()
|
|
449
|
+
self._configure_speed_overlay_text()
|
|
450
|
+
self._update_speed_overlay_layout(force=True)
|
|
451
|
+
self._set_speed_overlay_visible(False)
|
|
452
|
+
return self._speed_overlay
|
|
453
|
+
|
|
454
|
+
def _create_speed_overlay_text(self):
|
|
455
|
+
try:
|
|
456
|
+
return viz.addText('', parent=viz.SCREEN, font='Arial')
|
|
457
|
+
except Exception:
|
|
458
|
+
return viz.addText('', parent=viz.SCREEN)
|
|
459
|
+
|
|
460
|
+
def _configure_speed_overlay_bg(self):
|
|
461
|
+
self._call_overlay_bg_method('setPosition', [0.5, 0.5, 0])
|
|
462
|
+
self._call_overlay_bg_method('color', [0.22, 0.22, 0.22])
|
|
463
|
+
self._call_overlay_bg_method('alpha', 0.82)
|
|
464
|
+
self._apply_speed_overlay_bg_texture()
|
|
465
|
+
|
|
466
|
+
def _configure_speed_overlay_text(self):
|
|
467
|
+
self._call_overlay_method('setPosition', [0.5, 0.5, 0])
|
|
468
|
+
self._call_overlay_method('alignment', getattr(viz, 'ALIGN_CENTER_CENTER', None))
|
|
469
|
+
self._call_overlay_method('font', 'Arial')
|
|
470
|
+
self._call_overlay_method('fontSize', 84)
|
|
471
|
+
self._call_overlay_method('color', getattr(viz, 'WHITE', [1, 1, 1]))
|
|
472
|
+
|
|
473
|
+
def _update_speed_overlay_layout(self, force=False):
|
|
474
|
+
aspect = self._get_window_aspect()
|
|
475
|
+
if not force and self._last_overlay_aspect == aspect:
|
|
476
|
+
return
|
|
477
|
+
|
|
478
|
+
self._last_overlay_aspect = aspect
|
|
479
|
+
self._call_overlay_bg_method('setScale', [
|
|
480
|
+
SPEED_OVERLAY_BG_SCALE_X,
|
|
481
|
+
SPEED_OVERLAY_BG_SCALE_X * aspect / SPEED_OVERLAY_BG_ASPECT,
|
|
482
|
+
1.0,
|
|
483
|
+
])
|
|
484
|
+
self._call_overlay_method('setScale', [
|
|
485
|
+
SPEED_OVERLAY_TEXT_SCALE_X,
|
|
486
|
+
SPEED_OVERLAY_TEXT_SCALE_X * aspect,
|
|
487
|
+
1.0,
|
|
488
|
+
])
|
|
489
|
+
|
|
490
|
+
def _get_window_aspect(self):
|
|
491
|
+
try:
|
|
492
|
+
width, height = viz.window.getSize()
|
|
493
|
+
if height:
|
|
494
|
+
return float(width) / float(height)
|
|
495
|
+
except Exception:
|
|
496
|
+
pass
|
|
497
|
+
|
|
498
|
+
try:
|
|
499
|
+
width = viz.window.getWidth()
|
|
500
|
+
height = viz.window.getHeight()
|
|
501
|
+
if height:
|
|
502
|
+
return float(width) / float(height)
|
|
503
|
+
except Exception:
|
|
504
|
+
pass
|
|
505
|
+
|
|
506
|
+
return 1.0
|
|
507
|
+
|
|
508
|
+
def _apply_speed_overlay_bg_texture(self):
|
|
509
|
+
texture_path = os.path.join(os.path.dirname(__file__), SPEED_OVERLAY_BG_FILE)
|
|
510
|
+
if not os.path.exists(texture_path):
|
|
511
|
+
return
|
|
512
|
+
|
|
513
|
+
try:
|
|
514
|
+
texture = viz.addTexture(texture_path)
|
|
515
|
+
except Exception:
|
|
516
|
+
return
|
|
517
|
+
|
|
518
|
+
self._call_overlay_bg_method('texture', texture)
|
|
519
|
+
|
|
520
|
+
def _show_overlay_message(self, message):
|
|
521
|
+
if not self.show_speed_overlay:
|
|
522
|
+
return
|
|
523
|
+
|
|
524
|
+
overlay = self._ensure_speed_overlay()
|
|
525
|
+
if overlay is None:
|
|
526
|
+
return
|
|
527
|
+
|
|
528
|
+
if not self._call_overlay_method('message', message):
|
|
529
|
+
self._call_overlay_method('setText', message)
|
|
530
|
+
|
|
531
|
+
self._update_speed_overlay_layout(force=True)
|
|
532
|
+
self._speed_overlay_remaining = self.speed_overlay_duration
|
|
533
|
+
self._set_speed_overlay_visible(True)
|
|
534
|
+
|
|
535
|
+
def _show_speed_overlay(self):
|
|
536
|
+
self._show_overlay_message(u'\u00d7{:.2f}'.format(self.speed_mult))
|
|
537
|
+
|
|
538
|
+
def _show_height_lock_overlay(self):
|
|
539
|
+
state = 'ON' if self.height_lock else 'OFF'
|
|
540
|
+
self._show_overlay_message('Height Lock ' + state)
|
|
541
|
+
|
|
542
|
+
def _update_speed_overlay(self, dt):
|
|
543
|
+
if self._speed_overlay_remaining <= 0.0:
|
|
544
|
+
return
|
|
545
|
+
|
|
546
|
+
self._speed_overlay_remaining -= dt
|
|
547
|
+
if self._speed_overlay_remaining <= 0.0:
|
|
548
|
+
self._speed_overlay_remaining = 0.0
|
|
549
|
+
self._set_speed_overlay_visible(False)
|
|
550
|
+
|
|
551
|
+
def _set_speed_overlay_visible(self, visible):
|
|
552
|
+
state = viz.ON if visible else viz.OFF
|
|
553
|
+
if self._speed_overlay_bg is not None:
|
|
554
|
+
self._call_overlay_bg_method('visible', state)
|
|
555
|
+
if self._speed_overlay is not None:
|
|
556
|
+
self._call_overlay_method('visible', state)
|
|
557
|
+
|
|
558
|
+
def _call_overlay_method(self, name, *args):
|
|
559
|
+
return self._call_node_method(self._speed_overlay, name, *args)
|
|
560
|
+
|
|
561
|
+
def _call_overlay_bg_method(self, name, *args):
|
|
562
|
+
return self._call_node_method(self._speed_overlay_bg, name, *args)
|
|
563
|
+
|
|
564
|
+
def _call_node_method(self, node, name, *args):
|
|
565
|
+
if node is None:
|
|
566
|
+
return False
|
|
567
|
+
method = getattr(node, name, None)
|
|
568
|
+
if method is None:
|
|
569
|
+
return False
|
|
570
|
+
try:
|
|
571
|
+
method(*args)
|
|
572
|
+
return True
|
|
573
|
+
except Exception:
|
|
574
|
+
return False
|
|
575
|
+
|
|
576
|
+
def _update_height_lock_toggle(self):
|
|
577
|
+
is_down = self._is_key_down(self.toggle_height_lock_key)
|
|
578
|
+
if is_down and not self._height_lock_toggle_down:
|
|
579
|
+
self.toggle_height_lock(show_overlay=False)
|
|
580
|
+
self._height_lock_toggle_down = is_down
|
|
581
|
+
|
|
582
|
+
def update_movement(self):
|
|
583
|
+
if not self.enabled:
|
|
584
|
+
return
|
|
585
|
+
|
|
586
|
+
if not self._is_window_active():
|
|
587
|
+
self._clear_input_state()
|
|
588
|
+
return
|
|
589
|
+
|
|
590
|
+
if not self._uses_key_callbacks:
|
|
591
|
+
self._update_height_lock_toggle()
|
|
592
|
+
|
|
593
|
+
dt = None
|
|
594
|
+
if self._speed_overlay_remaining > 0.0:
|
|
595
|
+
dt = viz.elapsed()
|
|
596
|
+
self._update_speed_overlay_layout()
|
|
597
|
+
self._update_speed_overlay(dt)
|
|
598
|
+
|
|
599
|
+
mx = self._is_key_down(self.right_key) - self._is_key_down(self.left_key)
|
|
600
|
+
my = self._is_key_down(self.up_key) - self._is_key_down(self.down_key)
|
|
601
|
+
mz = self._is_key_down(self.forward_key) - self._is_key_down(self.backward_key)
|
|
602
|
+
|
|
603
|
+
if mx == 0 and my == 0 and mz == 0:
|
|
604
|
+
return
|
|
605
|
+
|
|
606
|
+
if dt is None:
|
|
607
|
+
dt = viz.elapsed()
|
|
608
|
+
is_shift = any(self._is_key_down(key) for key in self.fast_keys)
|
|
609
|
+
speed = self.speed * self.speed_mult * (self.fast_mult if is_shift else 1.0) * dt
|
|
610
|
+
|
|
611
|
+
length = math.sqrt(mx ** 2 + my ** 2 + mz ** 2)
|
|
612
|
+
mx, my, mz = (mx / length) * speed, (my / length) * speed, (mz / length) * speed
|
|
613
|
+
|
|
614
|
+
yaw, pitch, _roll = self.view.getEuler()
|
|
615
|
+
yaw_rad = math.radians(yaw)
|
|
616
|
+
sin_y, cos_y = math.sin(yaw_rad), math.cos(yaw_rad)
|
|
617
|
+
|
|
618
|
+
if self.height_lock:
|
|
619
|
+
forward_x, forward_y, forward_z = sin_y, 0.0, cos_y
|
|
620
|
+
else:
|
|
621
|
+
pitch_rad = math.radians(pitch)
|
|
622
|
+
cos_p = math.cos(pitch_rad)
|
|
623
|
+
forward_x = sin_y * cos_p
|
|
624
|
+
forward_y = -math.sin(pitch_rad)
|
|
625
|
+
forward_z = cos_y * cos_p
|
|
626
|
+
|
|
627
|
+
right_x, right_z = cos_y, -sin_y
|
|
628
|
+
pos = self.view.getPosition()
|
|
629
|
+
self.view.setPosition([
|
|
630
|
+
pos[0] + (mx * right_x + mz * forward_x),
|
|
631
|
+
pos[1] + my + mz * forward_y,
|
|
632
|
+
pos[2] + (mx * right_z + mz * forward_z),
|
|
633
|
+
])
|
|
634
|
+
|
|
635
|
+
|
|
636
|
+
_default_navigator = None
|
|
637
|
+
|
|
638
|
+
|
|
639
|
+
def enable(speed=DEFAULT_SPEED, fast_mult=DEFAULT_FAST_MULT, sense=DEFAULT_SENSE,
|
|
640
|
+
speed_mult=DEFAULT_SPEED_MULT, view=None, mouse_override=True,
|
|
641
|
+
show_speed_overlay=True, height_lock=DEFAULT_HEIGHT_LOCK,
|
|
642
|
+
forward_key=DEFAULT_FORWARD_KEY, backward_key=DEFAULT_BACKWARD_KEY,
|
|
643
|
+
left_key=DEFAULT_LEFT_KEY, right_key=DEFAULT_RIGHT_KEY,
|
|
644
|
+
up_key=DEFAULT_UP_KEY, down_key=DEFAULT_DOWN_KEY,
|
|
645
|
+
toggle_height_lock_key=DEFAULT_TOGGLE_HEIGHT_LOCK_KEY, fast_keys=None,
|
|
646
|
+
check_window_focus=DEFAULT_CHECK_WINDOW_FOCUS):
|
|
647
|
+
"""Enable the shared camera navigator and return it.
|
|
648
|
+
|
|
649
|
+
Call this once after viz.go() from your scene script. Repeated calls reuse the
|
|
650
|
+
same controller and only update its settings.
|
|
651
|
+
"""
|
|
652
|
+
global _default_navigator
|
|
653
|
+
|
|
654
|
+
if _default_navigator is None:
|
|
655
|
+
_default_navigator = CameraNavigator(
|
|
656
|
+
view=view,
|
|
657
|
+
speed=speed,
|
|
658
|
+
fast_mult=fast_mult,
|
|
659
|
+
sense=sense,
|
|
660
|
+
speed_mult=speed_mult,
|
|
661
|
+
mouse_override=mouse_override,
|
|
662
|
+
show_speed_overlay=show_speed_overlay,
|
|
663
|
+
height_lock=height_lock,
|
|
664
|
+
forward_key=forward_key,
|
|
665
|
+
backward_key=backward_key,
|
|
666
|
+
left_key=left_key,
|
|
667
|
+
right_key=right_key,
|
|
668
|
+
up_key=up_key,
|
|
669
|
+
down_key=down_key,
|
|
670
|
+
toggle_height_lock_key=toggle_height_lock_key,
|
|
671
|
+
fast_keys=fast_keys,
|
|
672
|
+
check_window_focus=check_window_focus,
|
|
673
|
+
)
|
|
674
|
+
else:
|
|
675
|
+
_default_navigator.configure(
|
|
676
|
+
speed=speed,
|
|
677
|
+
fast_mult=fast_mult,
|
|
678
|
+
sense=sense,
|
|
679
|
+
speed_mult=speed_mult,
|
|
680
|
+
view=view,
|
|
681
|
+
show_speed_overlay=show_speed_overlay,
|
|
682
|
+
height_lock=height_lock,
|
|
683
|
+
forward_key=forward_key,
|
|
684
|
+
backward_key=backward_key,
|
|
685
|
+
left_key=left_key,
|
|
686
|
+
right_key=right_key,
|
|
687
|
+
up_key=up_key,
|
|
688
|
+
down_key=down_key,
|
|
689
|
+
toggle_height_lock_key=toggle_height_lock_key,
|
|
690
|
+
fast_keys=fast_keys,
|
|
691
|
+
check_window_focus=check_window_focus,
|
|
692
|
+
)
|
|
693
|
+
_default_navigator.mouse_override = mouse_override
|
|
694
|
+
|
|
695
|
+
return _default_navigator.enable()
|
|
696
|
+
|
|
697
|
+
|
|
698
|
+
def disable():
|
|
699
|
+
"""Disable the shared navigator and return Vizard mouse handling to defaults."""
|
|
700
|
+
if _default_navigator is not None:
|
|
701
|
+
_default_navigator.disable()
|
|
702
|
+
return _default_navigator
|
|
703
|
+
|
|
704
|
+
|
|
705
|
+
def pause():
|
|
706
|
+
"""Pause GoodNavigate controls without changing Vizard mouse override state."""
|
|
707
|
+
if _default_navigator is not None:
|
|
708
|
+
_default_navigator.pause()
|
|
709
|
+
return _default_navigator
|
|
710
|
+
|
|
711
|
+
|
|
712
|
+
def resume():
|
|
713
|
+
"""Resume GoodNavigate controls after pause()."""
|
|
714
|
+
if _default_navigator is not None:
|
|
715
|
+
_default_navigator.resume()
|
|
716
|
+
return _default_navigator
|
|
717
|
+
|
|
718
|
+
|
|
719
|
+
def get_navigator():
|
|
720
|
+
"""Return the shared navigator, or None before enable() is called."""
|
|
721
|
+
return _default_navigator
|
|
722
|
+
|
|
723
|
+
|
|
724
|
+
if __name__ == '__main__':
|
|
725
|
+
viz.go()
|
|
726
|
+
viz.addChild('ground.osgb')
|
|
727
|
+
enable()
|
|
Binary file
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: goodnavigate
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Comfortable camera navigation helper for WorldViz Vizard 8
|
|
5
|
+
Author: cuber244
|
|
6
|
+
Project-URL: Homepage, https://github.com/cuber244/goodnavigate
|
|
7
|
+
Project-URL: Repository, https://github.com/cuber244/goodnavigate
|
|
8
|
+
Keywords: vizard,worldviz,camera,navigation,vr
|
|
9
|
+
Classifier: Development Status :: 3 - Alpha
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Topic :: Multimedia :: Graphics :: 3D Rendering
|
|
13
|
+
Requires-Python: >=3.7
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
|
|
16
|
+
# GoodNavigate
|
|
17
|
+
|
|
18
|
+
GoodNavigate is a camera navigation helper package for **WorldViz Vizard 8**.
|
|
19
|
+
It adds comfortable editor-style camera controls to a Vizard scene with one call.
|
|
20
|
+
|
|
21
|
+
This package depends on Vizard's `viz` and `vizact` modules, so it is intended to run inside a Vizard 8 Python environment. It is not a standalone Python camera library.
|
|
22
|
+
|
|
23
|
+
## Features
|
|
24
|
+
|
|
25
|
+
- Mouse-look camera rotation
|
|
26
|
+
- `WASD` camera movement
|
|
27
|
+
- Height-locked horizontal movement mode
|
|
28
|
+
- `F` key toggle for height lock
|
|
29
|
+
- Customizable movement keys
|
|
30
|
+
- Runtime speed and mouse sensitivity setters
|
|
31
|
+
- Current status dictionary
|
|
32
|
+
- Stops movement when the Vizard window loses focus
|
|
33
|
+
- `Space` to move up
|
|
34
|
+
- `C` to move down
|
|
35
|
+
- `Shift` for temporary fast movement
|
|
36
|
+
- Mouse wheel speed adjustment
|
|
37
|
+
- Center-screen speed multiplier overlay
|
|
38
|
+
- Rounded gray overlay background
|
|
39
|
+
- No automatic `viz.go()` when imported
|
|
40
|
+
- `pause()`, `resume()`, and `disable()` control helpers
|
|
41
|
+
|
|
42
|
+
## Installation
|
|
43
|
+
|
|
44
|
+
Install it into the Python environment used by Vizard 8.
|
|
45
|
+
|
|
46
|
+
```powershell
|
|
47
|
+
python -m pip install git+https://github.com/cuber244/goodnavigate.git
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
If `python` is not Vizard's Python, run pip through the Vizard Python executable instead. The exact path depends on your PC, but the command will look like this:
|
|
51
|
+
|
|
52
|
+
```powershell
|
|
53
|
+
"C:\Path\To\Vizard8\python.exe" -m pip install git+https://github.com/cuber244/goodnavigate.git
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
After the package is published to PyPI, installation becomes shorter:
|
|
57
|
+
|
|
58
|
+
```powershell
|
|
59
|
+
python -m pip install goodnavigate
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Basic Usage
|
|
63
|
+
|
|
64
|
+
Use the lowercase package name for new Vizard scripts.
|
|
65
|
+
|
|
66
|
+
```python
|
|
67
|
+
import viz
|
|
68
|
+
import goodnavigate
|
|
69
|
+
|
|
70
|
+
viz.go()
|
|
71
|
+
|
|
72
|
+
goodnavigate.enable()
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Importing `goodnavigate` does not start Vizard and does not add scene objects. Call `goodnavigate.enable()` after `viz.go()` in the script that owns the scene.
|
|
76
|
+
|
|
77
|
+
For compatibility, `import GoodNavigate` is also included after pip installation:
|
|
78
|
+
|
|
79
|
+
```python
|
|
80
|
+
import viz
|
|
81
|
+
import GoodNavigate
|
|
82
|
+
|
|
83
|
+
viz.go()
|
|
84
|
+
GoodNavigate.enable()
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## Controls
|
|
88
|
+
|
|
89
|
+
| Input | Action |
|
|
90
|
+
| --- | --- |
|
|
91
|
+
| Left mouse drag | Horizontal yaw rotation, pitch is leveled when drag starts |
|
|
92
|
+
| Right mouse drag | Free-look yaw and pitch rotation |
|
|
93
|
+
| `W` / `S` | Move forward / backward |
|
|
94
|
+
| `A` / `D` | Move left / right |
|
|
95
|
+
| `Space` | Move up |
|
|
96
|
+
| `C` | Move down |
|
|
97
|
+
| `Shift` | Temporary fast movement |
|
|
98
|
+
| Mouse wheel | Change movement speed multiplier |
|
|
99
|
+
| `F` | Toggle height lock |
|
|
100
|
+
|
|
101
|
+
`F` is handled through Vizard's key-down callback when available. If that callback is unavailable, GoodNavigate falls back to polling the key state each frame.
|
|
102
|
+
|
|
103
|
+
## Height Lock
|
|
104
|
+
|
|
105
|
+
Height lock is enabled by default.
|
|
106
|
+
|
|
107
|
+
When height lock is enabled, `WASD` movement stays horizontal even if the camera is looking up or down. This is useful for walking-style navigation.
|
|
108
|
+
|
|
109
|
+
When height lock is disabled, `W` and `S` move forward and backward along the camera's pitch direction. Looking up and pressing `W` moves upward and forward; looking down and pressing `W` moves downward and forward. `A` and `D` strafe relative to the current yaw.
|
|
110
|
+
|
|
111
|
+
Toggle height lock while running:
|
|
112
|
+
|
|
113
|
+
```python
|
|
114
|
+
# Press F in the Vizard window
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Configure the initial mode:
|
|
118
|
+
|
|
119
|
+
```python
|
|
120
|
+
goodnavigate.enable(height_lock=True) # default
|
|
121
|
+
goodnavigate.enable(height_lock=False) # free fly movement
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
Programmatic control:
|
|
125
|
+
|
|
126
|
+
```python
|
|
127
|
+
navigator = goodnavigate.enable()
|
|
128
|
+
|
|
129
|
+
navigator.set_height_lock(False)
|
|
130
|
+
navigator.toggle_height_lock()
|
|
131
|
+
locked = navigator.is_height_locked()
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
## Custom Keys
|
|
135
|
+
|
|
136
|
+
You can customize movement keys when enabling GoodNavigate.
|
|
137
|
+
|
|
138
|
+
```python
|
|
139
|
+
goodnavigate.enable(
|
|
140
|
+
forward_key='i',
|
|
141
|
+
backward_key='k',
|
|
142
|
+
left_key='j',
|
|
143
|
+
right_key='l',
|
|
144
|
+
up_key='u',
|
|
145
|
+
down_key='o',
|
|
146
|
+
toggle_height_lock_key='h',
|
|
147
|
+
)
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
Temporary fast movement keys can also be changed. Pass a tuple or list of Vizard key constants or key strings.
|
|
151
|
+
|
|
152
|
+
```python
|
|
153
|
+
goodnavigate.enable(fast_keys=(viz.KEY_SHIFT_L, viz.KEY_SHIFT_R))
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
You can change keys after enabling:
|
|
157
|
+
|
|
158
|
+
```python
|
|
159
|
+
navigator = goodnavigate.enable()
|
|
160
|
+
navigator.set_keys(forward='i', backward='k', toggle_height_lock='h')
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
## Speed Adjustment
|
|
164
|
+
|
|
165
|
+
The mouse wheel changes the movement speed multiplier.
|
|
166
|
+
|
|
167
|
+
- Default multiplier: `x1.00`
|
|
168
|
+
- Minimum multiplier: `x0.25`
|
|
169
|
+
- Maximum multiplier: `x5.00`
|
|
170
|
+
- Step per wheel operation: `0.25`
|
|
171
|
+
|
|
172
|
+
When the speed changes, or when the user scrolls at the minimum/maximum limit, the current multiplier is shown in the center of the screen for 1 second.
|
|
173
|
+
|
|
174
|
+
## Configuration
|
|
175
|
+
|
|
176
|
+
You can customize the base movement speed, fast movement multiplier, mouse sensitivity, initial speed multiplier, and initial height lock mode.
|
|
177
|
+
|
|
178
|
+
```python
|
|
179
|
+
goodnavigate.enable(
|
|
180
|
+
speed=2.0,
|
|
181
|
+
fast_mult=3.0,
|
|
182
|
+
sense=0.2,
|
|
183
|
+
speed_mult=1.0,
|
|
184
|
+
height_lock=True,
|
|
185
|
+
check_window_focus=True,
|
|
186
|
+
)
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
Disable the speed overlay if needed:
|
|
190
|
+
|
|
191
|
+
```python
|
|
192
|
+
goodnavigate.enable(show_speed_overlay=False)
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
Runtime setters:
|
|
196
|
+
|
|
197
|
+
```python
|
|
198
|
+
navigator = goodnavigate.enable()
|
|
199
|
+
|
|
200
|
+
navigator.set_speed(3.0)
|
|
201
|
+
navigator.set_fast_mult(4.0)
|
|
202
|
+
navigator.set_sense(0.15)
|
|
203
|
+
navigator.set_speed_mult(2.0)
|
|
204
|
+
navigator.reset_speed_mult()
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
## Status
|
|
208
|
+
|
|
209
|
+
Get the current controller state as a dictionary:
|
|
210
|
+
|
|
211
|
+
```python
|
|
212
|
+
navigator = goodnavigate.enable()
|
|
213
|
+
status = navigator.get_status()
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
The dictionary includes values such as `enabled`, `height_lock`, `speed`, `fast_mult`, `sense`, `speed_mult`, and current key bindings.
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
## Window Focus Safety
|
|
220
|
+
|
|
221
|
+
GoodNavigate stops movement when the Vizard window loses focus. This prevents held movement keys from remaining active after clicking another window.
|
|
222
|
+
|
|
223
|
+
This behavior is enabled by default:
|
|
224
|
+
|
|
225
|
+
```python
|
|
226
|
+
goodnavigate.enable(check_window_focus=True)
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
If you need to disable the focus check for a special setup, pass:
|
|
230
|
+
|
|
231
|
+
```python
|
|
232
|
+
goodnavigate.enable(check_window_focus=False)
|
|
233
|
+
```
|
|
234
|
+
## Pause, Resume, Disable
|
|
235
|
+
|
|
236
|
+
Pause GoodNavigate controls without returning Vizard mouse handling to its default behavior:
|
|
237
|
+
|
|
238
|
+
```python
|
|
239
|
+
goodnavigate.pause()
|
|
240
|
+
goodnavigate.resume()
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
Disable GoodNavigate controls and return mouse handling to Vizard defaults:
|
|
244
|
+
|
|
245
|
+
```python
|
|
246
|
+
goodnavigate.disable()
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
You can also call these on the navigator object:
|
|
250
|
+
|
|
251
|
+
```python
|
|
252
|
+
navigator = goodnavigate.enable()
|
|
253
|
+
navigator.pause()
|
|
254
|
+
navigator.resume()
|
|
255
|
+
navigator.disable()
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
## Using A Different View
|
|
259
|
+
|
|
260
|
+
By default, GoodNavigate controls `viz.MainView`. You can pass another Vizard view object.
|
|
261
|
+
|
|
262
|
+
```python
|
|
263
|
+
my_view = viz.MainView
|
|
264
|
+
goodnavigate.enable(view=my_view)
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
## Repository Layout
|
|
268
|
+
|
|
269
|
+
```text
|
|
270
|
+
pyproject.toml
|
|
271
|
+
README.md
|
|
272
|
+
src/
|
|
273
|
+
goodnavigate/
|
|
274
|
+
__init__.py
|
|
275
|
+
goodnavigate_overlay_bg.png
|
|
276
|
+
GoodNavigate.py
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
`src/goodnavigate/goodnavigate_overlay_bg.png` is packaged with the module and is used for the rounded gray speed overlay background.
|
|
280
|
+
|
|
281
|
+
The root-level `GoodNavigate.py` file can still be copied directly into a Vizard project if you want a no-pip workflow, but pip users should use the `src/` package.
|
|
282
|
+
|
|
283
|
+
## Requirements
|
|
284
|
+
|
|
285
|
+
- WorldViz Vizard 8
|
|
286
|
+
- Vizard Python environment with `viz` and `vizact`
|
|
287
|
+
- pip available in the Vizard Python environment
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
GoodNavigate.py,sha256=m1T-iieELFwHoIDKIfRopvqN9_y65YbRvDCXQTVH2Zo,239
|
|
2
|
+
goodnavigate/__init__.py,sha256=DVLDY1uHfkaI_3VonjYRhdKzp5IxpbMpvJXUPjtcuMw,25046
|
|
3
|
+
goodnavigate/goodnavigate_overlay_bg.png,sha256=o3DUW4QmBvlL0ghXMyN0wZZo8tUbZSEnPhESvKDa5Rw,3810
|
|
4
|
+
goodnavigate-0.1.0.dist-info/METADATA,sha256=AreixTn93RwYlFf2BsueLCE5aR7uUU76ImJH7U9jOOM,7790
|
|
5
|
+
goodnavigate-0.1.0.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
|
|
6
|
+
goodnavigate-0.1.0.dist-info/top_level.txt,sha256=vkijgywZ9-Gis5UTLj63RZjGhLk9yWclVfLsx6zt7Sg,26
|
|
7
|
+
goodnavigate-0.1.0.dist-info/RECORD,,
|