Created
August 15, 2025 19:25
-
-
Save kwahoo2/5001f494d38ce3272dd926997b4bd05b to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # pySDL3 is required, install with: | |
| # pip install PySDL3 | |
| # Free Flight FreeCAD demo. Navigate scene with mouse and keyboard | |
| # Bindings: | |
| # mouse: yaw and pitch | |
| # Q/E - roll, W/S - forward/backward, A/D - sidestep left/right, Ctrl/Space - down/up | |
| # note: proper threading/concurrency not completed, adjust sleep in self._running loop if segfaults | |
| # note: background color and lights copying from main view not implemented yet | |
| from threading import Thread | |
| from OpenGL import GL | |
| import sdl3 | |
| import ctypes | |
| from pivy.coin import SoRenderManager | |
| from pivy.coin import SoSeparator | |
| from pivy.coin import SbViewportRegion, SbVec2s | |
| from pivy.coin import SbVec3f, SbRotation | |
| from pivy.coin import SoPerspectiveCamera | |
| from pivy.coin import SoDirectionalLight | |
| from pivy.coin import SoGetBoundingBoxAction | |
| import time, math | |
| from dataclasses import dataclass | |
| from PySide.QtGui import QGuiApplication | |
| @dataclass | |
| class MovementModifier: | |
| walk: float = 0.0 # forward - backward | |
| sidestep: float = 0.0 # left - right | |
| altitude: float = 0.0 # up - down | |
| pitch: float = 0.0 # pitch | |
| yaw: float = 0.0 # yaw | |
| roll: float = 0.0 # roll | |
| class SDLTest(object): | |
| "FreeCAD SDL testing script" | |
| def __init__(self): | |
| self._running = True | |
| # adjust to your preferences: | |
| self.ang_vel = 0.002 # angular velocity | |
| self.lin_vel = 0.001 # linear velocity | |
| self.mouse_sens = 0.05 # mouse sensivity | |
| self.w, self.h = 1280, 720 | |
| # end of adjustment | |
| self.r_rate_mod = 16 # frame duration-dependent velocity correction | |
| self.size_mod = 100 # scene size-dependent linear velocity correction | |
| self.near_plane = 100 # setting too small near_plane value may cause DEPTH_TEST issues, | |
| self.far_plane = 100000 # will be readjusted later based on the scene bounding box | |
| # Cursor locking position | |
| self.fixed_mouse_x, self.fixed_mouse_y = self.w // 2, self.h // 2 | |
| # Incrementing mouse position | |
| self.mouse_x, self.mouse_y = 0, 0 | |
| self.mouse_x_rel = ctypes.c_float(0) | |
| self.mouse_y_rel = ctypes.c_float(0) | |
| self.frame_duration = 0 | |
| self.mov_mod = MovementModifier() | |
| print('Initializing...') | |
| def draw(self): | |
| self.render_manager.render() | |
| sdl3.SDL_GL_SwapWindow(self.window) | |
| def process_events(self): | |
| event = sdl3.SDL_Event() | |
| while sdl3.SDL_PollEvent(ctypes.byref(event)): | |
| match event.type: | |
| case sdl3.SDL_EVENT_QUIT: | |
| self.terminate() | |
| case sdl3.SDL_EVENT_WINDOW_CLOSE_REQUESTED: | |
| self.terminate() | |
| case sdl3.SDL_EVENT_WINDOW_RESIZED: | |
| w, h = ctypes.c_int(), ctypes.c_int() | |
| sdl3.SDL_GetWindowSize(self.window, w, h) | |
| self.w = w.value | |
| self.h = h.value | |
| vp_reg = SbViewportRegion(self.w, self.h) | |
| self.render_manager.setViewportRegion(vp_reg) | |
| case sdl3.SDL_EVENT_KEY_DOWN: | |
| if event.key.key == sdl3.SDLK_ESCAPE: | |
| if self.mouse_is_grabbed: | |
| self.mouse_is_grabbed = False | |
| sdl3.SDL_SetWindowTitle(self.window, b"Click into window to grab the mouse") | |
| sdl3.SDL_SetWindowMouseGrab(self.window, self.mouse_is_grabbed) | |
| sdl3.SDL_ShowCursor() | |
| else: | |
| self.terminate() # terminate if esc hit second time | |
| else: | |
| self.check_key_down(event.key.key) | |
| case sdl3.SDL_EVENT_KEY_UP: | |
| self.check_key_up(event.key.key) | |
| case sdl3.SDL_EVENT_MOUSE_BUTTON_DOWN: | |
| if not self.mouse_is_grabbed: | |
| self.mouse_is_grabbed = True | |
| result = sdl3.SDL_SetWindowMouseGrab(self.window, self.mouse_is_grabbed) | |
| print ('Mouse grabbed', result) | |
| sdl3.SDL_SetWindowTitle(self.window, b"Press Esc to release the mouse") | |
| result = sdl3.SDL_HideCursor() | |
| sdl3.SDL_WarpMouseInWindow(self.window, self.fixed_mouse_x, self.fixed_mouse_y) | |
| x_rel = ctypes.c_float(0) | |
| y_rel = ctypes.c_float(0) | |
| # read after initial warp, to avoid sudden value change after grabbing mouse | |
| sdl3.SDL_GetRelativeMouseState(ctypes.byref(x_rel), ctypes.byref(y_rel)) | |
| if self.mouse_is_grabbed: | |
| sdl3.SDL_GetRelativeMouseState(ctypes.byref(self.mouse_x_rel), ctypes.byref(self.mouse_y_rel)) | |
| self.mouse_x += self.mouse_x_rel.value | |
| self.mouse_y += self.mouse_y_rel.value | |
| # print(f"Mouse position: ({self.mouse_x}, {self.mouse_y})") | |
| sdl3.SDL_WarpMouseInWindow(self.window, self.fixed_mouse_x, self.fixed_mouse_y) | |
| self.update_position() | |
| def check_key_down(self, key): | |
| print(f"pressed {key}") | |
| match key: | |
| case sdl3.SDLK_W: | |
| self.mov_mod.walk = -1 | |
| case sdl3.SDLK_S: | |
| self.mov_mod.walk = 1 | |
| case sdl3.SDLK_A: | |
| self.mov_mod.sidestep = -1 | |
| case sdl3.SDLK_D: | |
| self.mov_mod.sidestep = 1 | |
| case sdl3.SDLK_Q: | |
| self.mov_mod.roll = -1 | |
| case sdl3.SDLK_E: | |
| self.mov_mod.roll = 1 | |
| case sdl3.SDLK_LCTRL: | |
| self.mov_mod.altitude = -1 | |
| case sdl3.SDLK_SPACE: | |
| self.mov_mod.altitude = 1 | |
| def check_key_up(self, key): | |
| print(f"released {key}") | |
| match key: | |
| case sdl3.SDLK_W | sdl3.SDLK_S: | |
| self.mov_mod.walk = 0 | |
| case sdl3.SDLK_A | sdl3.SDLK_D: | |
| self.mov_mod.sidestep = 0 | |
| case sdl3.SDLK_Q | sdl3.SDLK_E: | |
| self.mov_mod.roll = 0 | |
| case sdl3.SDLK_LCTRL | sdl3.SDLK_SPACE: | |
| self.mov_mod.altitude = 0 | |
| def update_position(self): | |
| yaw = -self.mouse_x_rel.value * self.mouse_sens | |
| pitch = -self.mouse_y_rel.value * self.mouse_sens | |
| self.mouse_x_rel.value = 0 # zeroing values to avoid perpetual spinning | |
| self.mouse_y_rel.value = 0 | |
| rot_y = SbRotation(SbVec3f(0.0, 0.0, 1.0), yaw * self.ang_vel * self.r_rate_mod) | |
| rot_p = SbRotation(SbVec3f(1.0, 0.0, 0.0), pitch * self.ang_vel * self.r_rate_mod) | |
| rot_r = SbRotation(SbVec3f(0.0, 1.0, 0.0), self.mov_mod.roll * self.ang_vel * self.r_rate_mod) | |
| input_rot = rot_y * rot_p * rot_r | |
| qm = input_rot.getValue() | |
| mod_rot = FreeCAD.Rotation(qm[0], qm[1], qm[2], qm[3]) | |
| m_ypr = mod_rot.getYawPitchRoll() | |
| qc = self.camera.orientation.getValue().getValue() | |
| cam_rot = FreeCAD.Rotation(qc[0], qc[1], qc[2], qc[3]) # SbRotation to Base::Rotation | |
| c_ypr = cam_rot.getYawPitchRoll() | |
| c_r_f = FreeCAD.Rotation() | |
| c_r_f.setYawPitchRoll(c_ypr[0] + m_ypr[0], c_ypr[1] + m_ypr[1], c_ypr[2] + m_ypr[2]) | |
| c_sbr_f = SbRotation(c_r_f.Q[0], c_r_f.Q[1], c_r_f.Q[2], c_r_f.Q[3]) | |
| self.camera.orientation.setValue(c_sbr_f) | |
| mod_pos = SbVec3f(self.mov_mod.sidestep, self.mov_mod.altitude, self.mov_mod.walk) * self.lin_vel * self.r_rate_mod * self.size_mod | |
| mod_pos_f = c_sbr_f.multVec(mod_pos) | |
| self.camera.position.setValue(self.camera.position.getValue() + mod_pos_f) | |
| def run(self): | |
| sdl3.SDL_Init(sdl3.SDL_INIT_VIDEO) | |
| sdl3.SDL_GL_SetAttribute(sdl3.SDL_GL_CONTEXT_PROFILE_MASK, sdl3.SDL_GL_CONTEXT_PROFILE_COMPATIBILITY) | |
| # this is for FreeCAD X11 build running at top of Wayland | |
| if QGuiApplication.platformName() == 'xcb': | |
| sdl3.SDL_SetHint(sdl3.SDL_HINT_VIDEO_DRIVER, b'x11') | |
| self.window = sdl3.SDL_CreateWindow( | |
| b"Click into window to grab the mouse", | |
| self.w, self.h, | |
| sdl3.SDL_WINDOW_OPENGL | sdl3.SDL_WINDOW_RESIZABLE | |
| ) | |
| self.context = sdl3.SDL_GL_CreateContext(self.window) | |
| sdl3.SDL_GL_MakeCurrent(self.window, self.context) | |
| sdl3.SDL_GL_SetSwapInterval(1) | |
| GL.glBlendFunc(GL.GL_SRC_ALPHA, GL.GL_ONE_MINUS_SRC_ALPHA) | |
| GL.glEnable(GL.GL_BLEND) | |
| GL.glEnable(GL.GL_LIGHTING) | |
| GL.glEnable(GL.GL_DEPTH_TEST) | |
| self.mouse_is_grabbed = False | |
| viewer = FreeCADGui.ActiveDocument.ActiveView.getViewer() | |
| sg = viewer.getSceneGraph() | |
| old_camera = Gui.ActiveDocument.ActiveView.getCameraNode() | |
| self.camera = SoPerspectiveCamera() | |
| # use current viewport camera pose as initial pose | |
| self.camera.position.setValue(old_camera.position.getValue()) | |
| self.camera.orientation.setValue(old_camera.orientation.getValue()) | |
| self.camera.nearDistance.setValue(self.near_plane) | |
| self.camera.farDistance.setValue(self.far_plane) | |
| self.light = SoDirectionalLight() | |
| self.root = SoSeparator() | |
| self.root.ref() | |
| self.root.addChild(self.light) | |
| self.root.addChild(self.camera) | |
| self.root.addChild(sg) | |
| self.render_manager = SoRenderManager() | |
| vp_reg = SbViewportRegion(self.w, self.h) | |
| self.render_manager.setViewportRegion(vp_reg) | |
| self.render_manager.setSceneGraph(self.root) | |
| bbox_action = SoGetBoundingBoxAction(vp_reg) | |
| bbox_action.apply(self.root) | |
| bbox = bbox_action.getBoundingBox() | |
| bbox_vol = bbox.getVolume() | |
| if bbox_vol > 100: | |
| self.size_mod = bbox_vol ** (1/3) # movement velocity will be proportional to cube root of scene volume | |
| print (f"size mod: {self.size_mod}") | |
| self.near_plane = self.size_mod / 100 | |
| self.far_plane = self.size_mod * 10 | |
| self.camera.nearDistance.setValue(self.near_plane) | |
| self.camera.farDistance.setValue(self.far_plane) | |
| while self._running: | |
| frame_start = sdl3.SDL_GetTicks() | |
| if self.frame_duration > 0: | |
| self.r_rate_mod = self.frame_duration | |
| # fps = 1000 / self.frame_duration | |
| # print (f'Framerate is {fps:.2f} frames per second') | |
| self.process_events() | |
| time.sleep(0.02) # TODO do proper threading management, to avoid segfaults | |
| self.draw() | |
| self.frame_duration = sdl3.SDL_GetTicks() - frame_start | |
| def terminate(self): | |
| self._running = False | |
| self.root.unref() | |
| sdl3.SDL_GL_DestroyContext(self.context) | |
| sdl3.SDL_DestroyWindow(self.window) | |
| sdl3.SDL_Quit() | |
| print('Terminated...') | |
| if __name__ == "__main__": | |
| sdltest = SDLTest() | |
| t = Thread(target=sdltest.run) | |
| t.start() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment