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
@@ -0,0 +1,7 @@
1
+ """Compatibility wrapper for existing Vizard scripts.
2
+
3
+ Prefer `import goodnavigate` for new code. This module keeps older
4
+ `import GoodNavigate` scripts working after pip installation.
5
+ """
6
+
7
+ from goodnavigate import * # noqa: F401,F403
@@ -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,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (83.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ GoodNavigate
2
+ goodnavigate