diff --git a/Start_Run.py b/Start_Run.py index 3e90d01d..36decba8 100644 --- a/Start_Run.py +++ b/Start_Run.py @@ -1,9 +1,48 @@ import os +import shutil +import subprocess import sys + +def _maybe_relaunch_with_py311(): + """Use Python 3.11 to match ui/lui.pyd ABI on Windows.""" + if sys.version_info >= (3, 11): + return + if os.environ.get("EG_RELAUNCHED_PY311") == "1": + return + if os.name != "nt": + return + + py_launcher = shutil.which("py") + if not py_launcher: + print(f"⚠ 当前解释器是 Python {sys.version.split()[0]},未找到 py launcher,无法自动切换到 3.11。") + return + + try: + probe = subprocess.run( + [py_launcher, "-3.11", "-c", "import sys;print(sys.executable)"], + capture_output=True, + text=True, + check=False, + ) + if probe.returncode != 0: + print("⚠ 未检测到可用的 Python 3.11,LUI 可能不可用。") + return + except Exception as e: + print(f"⚠ 检测 Python 3.11 失败: {e}") + return + + os.environ["EG_RELAUNCHED_PY311"] = "1" + relaunch_cmd = [py_launcher, "-3.11", os.path.abspath(__file__), *sys.argv[1:]] + print(f"✓ 检测到 Python {sys.version.split()[0]},自动切换到 Python 3.11: {probe.stdout.strip()}") + os.execv(py_launcher, relaunch_cmd) + # 添加项目根目录到 Python 路径 project_root = os.path.dirname(os.path.abspath(__file__)) sys.path.insert(0, project_root) +third_party_path = os.path.join(project_root, "third_party") +if os.path.isdir(third_party_path): + sys.path.insert(0, third_party_path) # 设置工作目录为项目根目录 os.chdir(project_root) @@ -22,6 +61,7 @@ sys.path.insert(0, icons_path) # 现在可以导入并运行主程序 if __name__ == "__main__": + _maybe_relaunch_with_py311() args = sys.argv[1:] # args = "/home/tiger/桌面/Test1" # args = "C:/Users/29381/Desktop/1" @@ -46,4 +86,4 @@ if __name__ == "__main__": else: # print(f"[DEBUG] 无项目路径,正常启动") app = MyWorld() - app.run() \ No newline at end of file + app.run() diff --git a/main.py b/main.py index 0686ac0a..223f6ea1 100644 --- a/main.py +++ b/main.py @@ -1,3 +1,33 @@ +import os +import shutil +import subprocess +import sys +import warnings +from pathlib import Path + +if __name__ == "__main__" and os.name == "nt" and sys.version_info < (3, 11): + py_launcher = shutil.which("py") + if py_launcher and os.environ.get("EG_RELAUNCHED_PY311") != "1": + try: + probe = subprocess.run( + [py_launcher, "-3.11", "-c", "import sys;print(sys.executable)"], + capture_output=True, + text=True, + check=False, + ) + if probe.returncode == 0: + os.environ["EG_RELAUNCHED_PY311"] = "1" + os.execv(py_launcher, [py_launcher, "-3.11", os.path.abspath(__file__), *sys.argv[1:]]) + else: + print("⚠ 未检测到可用的 Python 3.11,LUI 可能不可用。") + except Exception as relaunch_error: + print(f"⚠ 自动切换 Python 3.11 失败: {relaunch_error}") + +PROJECT_ROOT = Path(__file__).resolve().parent +THIRD_PARTY_DIR = PROJECT_ROOT / "third_party" +if str(THIRD_PARTY_DIR) not in sys.path: + sys.path.insert(0, str(THIRD_PARTY_DIR)) + from panda3d.core import loadPrcFileData, WindowProperties, Point3 from math import pi, sin, cos @@ -11,11 +41,6 @@ import p3dimgui from imgui_bundle import imgui, imgui_ctx -import sys -import os -import warnings -from pathlib import Path - # 导入MyWorld类和必要的模块 from core.world import CoreWorld from core.selection import SelectionSystem diff --git a/third_party/p3dimgui/__init__.py b/third_party/p3dimgui/__init__.py new file mode 100644 index 00000000..ab913b9b --- /dev/null +++ b/third_party/p3dimgui/__init__.py @@ -0,0 +1,33 @@ +from .backend import ImGuiBackend + +from .utilities.PlaceManager import PlaceManager +from .utilities.PlacePanel import PlacePanel +from .utilities.ExplorerManager import ExplorerManager +from .utilities.SceneGraphExplorer import SceneGraphExplorer +from .utilities.TimeSliderManager import TimeSliderManager +from .utilities.IntervalTimeSlider import IntervalTimeSlider + +__all__ = ['init', + 'ImGuiBackend', + "PlacePanel", + 'PlaceManager', + 'ExplorerManager', + 'SceneGraphExplorer', + 'TimeSliderManager', + 'IntervalTimeSlider', + ] + +def init(window = None, parent = None, + style = 'dark', wantPlaceManager = True, + wantExplorerManager = True, wantTimeSliderManager = True): + try: + base.imgui + except AttributeError: + base.imgui = ImGuiBackend(window, parent, style) + + if wantPlaceManager: + base.placeManager = PlaceManager() + if wantExplorerManager: + base.explorerManager = ExplorerManager() + if wantTimeSliderManager: + base.timeSliderManager = TimeSliderManager() diff --git a/third_party/p3dimgui/backend.py b/third_party/p3dimgui/backend.py new file mode 100644 index 00000000..065beff2 --- /dev/null +++ b/third_party/p3dimgui/backend.py @@ -0,0 +1,565 @@ +from panda3d.core import ( + ButtonHandle, + ButtonRegistry, + ColorAttrib, + ColorBlendAttrib, + CullFaceAttrib, + DepthTestAttrib, + Geom, + GeomNode, + GeomTriangles, + GeomVertexData, + GeomVertexArrayFormat, + GeomVertexFormat, + InternalName, + KeyboardButton, + MouseButton, + NodePath, + RenderState, + SamplerState, + Shader, + ScissorAttrib, + Texture, + TextureAttrib +) + +from direct.directnotify import DirectNotifyGlobal +from direct.showbase.DirectObject import DirectObject + +try: + from shaders import * +except ModuleNotFoundError: + from .shaders import * + +from imgui_bundle import imgui +import ctypes + +import pyperclip +import sys + +__all__ = ['ImGuiBackend', 'ImGuiStyles'] + +KEYBOARD_BUTTON_TO_IMGUI_KEY = { + # Special keys + KeyboardButton.tab(): imgui.Key.tab.value, + KeyboardButton.left(): imgui.Key.left_arrow.value, + KeyboardButton.right(): imgui.Key.right_arrow.value, + KeyboardButton.up(): imgui.Key.up_arrow.value, + KeyboardButton.down(): imgui.Key.down_arrow.value, + KeyboardButton.enter(): imgui.Key.enter.value, + KeyboardButton.escape(): imgui.Key.escape.value, + KeyboardButton.backspace(): imgui.Key.backspace.value, + KeyboardButton.space(): imgui.Key.space.value, + KeyboardButton.lshift(): imgui.Key.left_shift.value, + KeyboardButton.rshift(): imgui.Key.right_shift.value, + KeyboardButton.shift(): imgui.Key.mod_shift.value, + KeyboardButton.control(): imgui.Key.mod_ctrl.value, + KeyboardButton.lcontrol(): imgui.Key.left_ctrl.value, + KeyboardButton.rcontrol(): imgui.Key.right_ctrl.value, + KeyboardButton.alt(): imgui.Key.mod_alt.value, + KeyboardButton.lalt(): imgui.Key.left_alt.value, + KeyboardButton.ralt(): imgui.Key.right_alt.value, + KeyboardButton._del(): imgui.Key.delete.value, + KeyboardButton.home(): imgui.Key.home.value, + KeyboardButton.end(): imgui.Key.end.value, + KeyboardButton.pageUp(): imgui.Key.page_up.value, + KeyboardButton.pageDown(): imgui.Key.page_down.value, + KeyboardButton.insert(): imgui.Key.insert.value, + KeyboardButton.capsLock(): imgui.Key.caps_lock.value, + KeyboardButton.numLock(): imgui.Key.num_lock.value, + KeyboardButton.scrollLock(): imgui.Key.scroll_lock.value, + KeyboardButton.menu(): imgui.Key.menu.value, + KeyboardButton.lmeta(): imgui.Key.left_super.value, + KeyboardButton.rmeta(): imgui.Key.right_super.value, + KeyboardButton.meta(): imgui.Key.mod_super.value, + + # Function keys (F1 to F16) + KeyboardButton.f1(): imgui.Key.f1.value, + KeyboardButton.f2(): imgui.Key.f2.value, + KeyboardButton.f3(): imgui.Key.f3.value, + KeyboardButton.f4(): imgui.Key.f4.value, + KeyboardButton.f5(): imgui.Key.f5.value, + KeyboardButton.f6(): imgui.Key.f6.value, + KeyboardButton.f7(): imgui.Key.f7.value, + KeyboardButton.f8(): imgui.Key.f8.value, + KeyboardButton.f9(): imgui.Key.f9.value, + KeyboardButton.f10(): imgui.Key.f10.value, + KeyboardButton.f11(): imgui.Key.f11.value, + KeyboardButton.f12(): imgui.Key.f12.value, + KeyboardButton.f13(): imgui.Key.f13.value, + KeyboardButton.f14(): imgui.Key.f14.value, + KeyboardButton.f15(): imgui.Key.f15.value, + KeyboardButton.f16(): imgui.Key.f16.value, + + # Numeric keys (0-9) on the main keyboard + KeyboardButton.asciiKey("0"): imgui.Key._0.value, + KeyboardButton.asciiKey("1"): imgui.Key._1.value, + KeyboardButton.asciiKey("2"): imgui.Key._2.value, + KeyboardButton.asciiKey("3"): imgui.Key._3.value, + KeyboardButton.asciiKey("4"): imgui.Key._4.value, + KeyboardButton.asciiKey("5"): imgui.Key._5.value, + KeyboardButton.asciiKey("6"): imgui.Key._6.value, + KeyboardButton.asciiKey("7"): imgui.Key._7.value, + KeyboardButton.asciiKey("8"): imgui.Key._8.value, + KeyboardButton.asciiKey("9"): imgui.Key._9.value, + + # Alphabetic keys + KeyboardButton.asciiKey("a"): imgui.Key.a.value, + KeyboardButton.asciiKey("b"): imgui.Key.b.value, + KeyboardButton.asciiKey("c"): imgui.Key.c.value, + KeyboardButton.asciiKey("d"): imgui.Key.d.value, + KeyboardButton.asciiKey("e"): imgui.Key.e.value, + KeyboardButton.asciiKey("f"): imgui.Key.f.value, + KeyboardButton.asciiKey("g"): imgui.Key.g.value, + KeyboardButton.asciiKey("h"): imgui.Key.h.value, + KeyboardButton.asciiKey("i"): imgui.Key.i.value, + KeyboardButton.asciiKey("j"): imgui.Key.j.value, + KeyboardButton.asciiKey("k"): imgui.Key.k.value, + KeyboardButton.asciiKey("l"): imgui.Key.l.value, + KeyboardButton.asciiKey("m"): imgui.Key.m.value, + KeyboardButton.asciiKey("n"): imgui.Key.n.value, + KeyboardButton.asciiKey("o"): imgui.Key.o.value, + KeyboardButton.asciiKey("p"): imgui.Key.p.value, + KeyboardButton.asciiKey("q"): imgui.Key.q.value, + KeyboardButton.asciiKey("r"): imgui.Key.r.value, + KeyboardButton.asciiKey("s"): imgui.Key.s.value, + KeyboardButton.asciiKey("t"): imgui.Key.t.value, + KeyboardButton.asciiKey("u"): imgui.Key.u.value, + KeyboardButton.asciiKey("v"): imgui.Key.v.value, + KeyboardButton.asciiKey("w"): imgui.Key.w.value, + KeyboardButton.asciiKey("x"): imgui.Key.x.value, + KeyboardButton.asciiKey("y"): imgui.Key.y.value, + KeyboardButton.asciiKey("z"): imgui.Key.z.value, + + # Punctuation keys + KeyboardButton.asciiKey("!"): imgui.Key._1.value, + KeyboardButton.asciiKey("@"): imgui.Key._2.value, + KeyboardButton.asciiKey("#"): imgui.Key._3.value, + KeyboardButton.asciiKey("$"): imgui.Key._4.value, + KeyboardButton.asciiKey("%"): imgui.Key._5.value, + KeyboardButton.asciiKey("^"): imgui.Key._6.value, + KeyboardButton.asciiKey("&"): imgui.Key._7.value, + KeyboardButton.asciiKey("*"): imgui.Key._8.value, + KeyboardButton.asciiKey("("): imgui.Key._9.value, + KeyboardButton.asciiKey(")"): imgui.Key._0.value, + KeyboardButton.asciiKey("-"): imgui.Key.minus.value, + KeyboardButton.asciiKey("="): imgui.Key.equal.value, + KeyboardButton.asciiKey("["): imgui.Key.left_bracket.value, + KeyboardButton.asciiKey("]"): imgui.Key.right_bracket.value, + KeyboardButton.asciiKey("\\"): imgui.Key.backslash.value, + KeyboardButton.asciiKey(";"): imgui.Key.semicolon.value, + KeyboardButton.asciiKey("'"): imgui.Key.apostrophe.value, + KeyboardButton.asciiKey(","): imgui.Key.comma.value, + KeyboardButton.asciiKey("."): imgui.Key.period.value, + KeyboardButton.asciiKey("/"): imgui.Key.slash.value, + + # Special characters + KeyboardButton.asciiKey("`"): imgui.Key.grave_accent.value, + KeyboardButton.asciiKey("~"): imgui.Key.grave_accent.value, + KeyboardButton.asciiKey("_"): imgui.Key.minus.value, + KeyboardButton.asciiKey("+"): imgui.Key.equal.value, + KeyboardButton.asciiKey("<"): imgui.Key.comma.value, + KeyboardButton.asciiKey(">"): imgui.Key.period.value, + KeyboardButton.asciiKey("?"): imgui.Key.slash.value, + + # Additional modifier keys + KeyboardButton.printScreen(): imgui.Key.print_screen.value, + KeyboardButton.pause(): imgui.Key.pause.value, +} + +class GeomList: + def __init__(self, vdata: GeomVertexData): + self.vdata: GeomVertexData = vdata # vertex data shared among the below GeomNodes + self.nodepaths: list[NodePath] = [] + +class ImGuiBackend(DirectObject): + """ + The main Dear ImGui backend code for Panda3D. + (Based on https://github.com/bluekyu/panda3d_imgui; ported to + Python using imgui-bundle and updated to support Dear ImGui version 1.92.3) + """ + + def __init__(self, window = None, parent = None, style = 'dark'): + DirectObject.__init__(self) + self.notify = DirectNotifyGlobal.directNotify.newCategory("imgui") + if not window: + window = base.win + if not parent: + parent = base.pixel2d + + self.window = window + self.root = parent.attachNewNode('imgui-root', 1000) + self.context = imgui.create_context() + self.io = imgui.get_io() + + # Add backend flag for rendering textures + self.io.backend_flags |= imgui.BackendFlags_.renderer_has_textures.value + + self.vformat = None + self.textureCounter = 0 + self.textures: dict[int, Texture] = {} + self.geomData: list[GeomList] = [] + + self.__setupStyle(style) + self.__setupGeom() + self.__setupShader() + self.__setupFront() + self.__setupEvent() + self.__windowEvent() + self.__setupButton() + self.__setupRender() + + # Set Clipboard functions + imgui.get_platform_io().platform_set_clipboard_text_fn = self.__setClipboardText + imgui.get_platform_io().platform_get_clipboard_text_fn = self.__getClipboardText + + def toggle(self): + if self.root.isHidden(): + self.root.show() + else: + self.root.hide() + + def hide(self): + self.root.hide() + + def show(self): + self.root.show() + + def isMouseCaptured(self) -> bool: + # This returns True if the mouse is over + # a Imgui window or any other reason. + return self.io.want_capture_mouse + + def isKeyboardCaptured(self) -> bool: + # This returns True when a Imgui text + # input widget is focused or any other reason. + return self.io.want_capture_keyboard + + def __setupStyle(self, style): + match style: + case 'dark': + imgui.style_colors_dark() + case 'classic': + imgui.style_colors_classic() + case 'light': + imgui.style_colors_light() + case _: + self.notify.warning(f"Unknown style: \"{style}\"") + + def __onButton(self, keyName: str, down: bool): + # Panda3D adds the prefix of the modifier keys to the key name + # if they are held down, so we have to strip them out. + if keyName.startswith(('control-', 'alt-', 'shift-', 'shift-control-', 'shift-alt-', 'shift-control-alt-', + 'meta-', 'control-meta-', 'alt-meta-', 'control-alt-meta-', 'shift-meta-', + 'shift-control-meta', 'shift-alt-meta-', 'shift-control-alt-meta-')): + keyName = keyName.split('-')[-1] + if keyName == '': + # must be minus. + keyName = '-' + + button = ButtonRegistry.ptr().getButton(keyName) + if button == ButtonHandle.none(): + return + if MouseButton.isMouseButton(button): + if button == MouseButton.one(): + self.io.add_mouse_button_event(0, down) + elif button == MouseButton.three(): + # NOTE: In Panda3D, MouseButton.three() is the right mouse button + # while MouseButton.two() is the middle (scroll wheel) button + self.io.add_mouse_button_event(1, down) + elif button == MouseButton.two(): + self.io.add_mouse_button_event(2, down) + elif button == MouseButton.four(): + self.io.add_mouse_button_event(3, down) + elif button == MouseButton.five(): + self.io.add_mouse_button_event(4, down) + elif button == MouseButton.wheelUp() and down: + self.io.add_mouse_wheel_event(0, .5) + elif button == MouseButton.wheelDown() and down: + self.io.add_mouse_wheel_event(0, -.5) + elif button == MouseButton.wheelLeft() and down: + self.io.add_mouse_wheel_event(.5, 0) + elif button == MouseButton.wheelRight() and down: + self.io.add_mouse_wheel_event(-.5, 0) + else: + imguiKey = KEYBOARD_BUTTON_TO_IMGUI_KEY.get(button, imgui.Key.none.value) + self.io.add_key_event(imguiKey, down) + + def __onKeystroke(self, keyName): + # NOTE: Panda3D for some reason doesn't recognize if + # the caps lock is on for macOS. You would have to + # hold down shift to input capital letters. + # Windows/Linux handles this just fine. + button = ButtonRegistry.ptr().getButton(keyName) + if keyName == ' ': + # There is no space button on the ButtonRegistry. + button = KeyboardButton.space() + if button.hasAsciiEquivalent(): + self.io.add_input_character(ord(button.getAsciiEquivalent())) + + def __setupGeom(self): + self.notify.debug("__setupGeom") + arrayFormat = GeomVertexArrayFormat( + InternalName.getVertex(), 4, Geom.NT_stdfloat, Geom.C_point, + InternalName.getColor(), 1, Geom.NT_packed_dabc, Geom.C_color + ) + + self.vformat = GeomVertexFormat.registerFormat(GeomVertexFormat(arrayFormat)) + + self.root.setState(RenderState.make( + ColorAttrib.makeVertex(), + ColorBlendAttrib.make(ColorBlendAttrib.M_add, ColorBlendAttrib.O_incoming_alpha, ColorBlendAttrib.O_one_minus_incoming_alpha), + DepthTestAttrib.make(DepthTestAttrib.M_none), + CullFaceAttrib.make(CullFaceAttrib.M_cull_none) + )) + + def __setupShader(self): + self.notify.debug("__setupShader") + shader = Shader.make( + Shader.SL_GLSL, + VERT_SHADER, + FRAG_SHADER, + ) + + self.root.setShader(shader) + + def __setupFront(self): + self.notify.debug("__setupFront") + self.io.fonts.add_font_default() + + def __setupEvent(self): + self.notify.debug("__setupEvent") + self.accept("window-event", self.__windowEvent) + + def __windowEvent(self, _ = None): + if self.window: + self.io.display_size = (self.window.getXSize(), self.window.getYSize()) + + def __setupButton(self): + self.notify.debug("__setupButton") + base.buttonThrowers[0].node().setButtonDownEvent('buttonDown') + base.buttonThrowers[0].node().setButtonUpEvent('buttonUp') + base.buttonThrowers[0].node().setKeystrokeEvent('keystroke') + + def __buttonDown(keyName): + self.__onButton(keyName, True) + + def __buttonUp(keyName): + self.__onButton(keyName, False) + + def __keyStroke(keyName): + self.__onKeystroke(keyName) + + def __handleOobe(): + if base.bboard.get('oobeEnabled'): + self.ignore('buttonDown') + self.ignore('buttonUp') + self.accept('oobe-down', __buttonDown) + self.accept('oobe-up', __buttonUp) + else: + self.ignore('oobe-down') + self.ignore('oobe-up') + + # Set the names to the button events back. + base.buttonThrowers[0].node().setButtonDownEvent('buttonDown') + base.buttonThrowers[0].node().setButtonUpEvent('buttonUp') + self.accept('buttonDown', __buttonDown) + self.accept('buttonUp', __buttonUp) + + self.accept('buttonDown', __buttonDown) + self.accept('buttonUp', __buttonUp) + self.accept('keystroke', __keyStroke) + + self.accept(base.bboard.getEvent('oobeEnabled'), __handleOobe) + + # self.addTask(__handleOobe, appendTask=True) + + def __setupRender(self): + self.notify.debug("__setupRender") + # NOTE: igLoop (frame rendering) has 50 sort. + base.taskMgr.add(self.__newFrame, "imgui-new-frame", 0) + base.taskMgr.add(self.__renderFrame, "imgui-render-frame", 40) + + def __newFrame(self, task): + if self.root.isHidden(): + return task.cont + + self.io.delta_time = base.clock.getDt() + if self.window: + mouse = self.window.getPointer(0) + if mouse.getInWindow(): + if self.io.want_set_mouse_pos: + self.window.movePointer(0, self.io.mouse_pos.x, self.io.mouse_pos.y) + else: + self.io.mouse_pos = mouse.getX(), mouse.getY() + else: + self.io.mouse_pos = (-imgui.FLT_MAX, -imgui.FLT_MAX) + + imgui.new_frame() + base.messenger.send("imgui-new-frame") + + return task.cont + + def __renderFrame(self, task): + if self.root.isHidden(): + return task.cont + + imgui.render() + + fbWidth = self.io.display_size.x * self.io.display_framebuffer_scale.x + fbHeight = self.io.display_size.y * self.io.display_framebuffer_scale.y + + self.__updateTextures() + + drawData = imgui.get_draw_data() + + for child in self.root.children: + child.detachNode() + + for i, cmdList in enumerate(drawData.cmd_lists): + if i > len(self.geomData) - 1: + self.geomData.append(GeomList(GeomVertexData(f"imgui-vertex-{i}", self.vformat, Geom.UH_stream))) + + geomList = self.geomData[i] + vertexHandle = geomList.vdata.modifyArrayHandle(0) + if vertexHandle.getNumRows() < cmdList.vtx_buffer.size(): + vertexHandle.uncleanSetNumRows(cmdList.vtx_buffer.size()) + vertexHandle.setData(ctypes.string_at(cmdList.vtx_buffer.data_address(), cmdList.vtx_buffer.size() * imgui.VERTEX_SIZE)) + + indexBuffer = cmdList.idx_buffer.data_address() + for k, drawCmd, in enumerate(cmdList.cmd_buffer): + if k > len(geomList.nodepaths) - 1: + geomList.nodepaths.append(self.__createGeomnode(geomList.vdata)) + + np = geomList.nodepaths[k] + np.reparentTo(self.root) + + node = np.node() + indexHandle = node.modifyGeom(0).modifyPrimitive(0).modifyVertices(drawCmd.elem_count).modifyHandle() + if indexHandle.getNumRows() < drawCmd.elem_count: + indexHandle.uncleanSetNumRows(drawCmd.elem_count) + + indexHandle.setData(ctypes.string_at(indexBuffer, drawCmd.elem_count * imgui.INDEX_SIZE)) + indexBuffer += (drawCmd.elem_count * imgui.INDEX_SIZE) + + # FIXME: This works, but it breaks Modal windows as the white + # covers the window (but still usable) instead of the background. + # (Not sure if this is to blame or the shader...) + state = RenderState.make(ScissorAttrib.make( + drawCmd.clip_rect.x / fbWidth, + drawCmd.clip_rect.z / fbWidth, + 1 - drawCmd.clip_rect.w / fbHeight, + 1 - drawCmd.clip_rect.y / fbHeight + )) + + if drawCmd.tex_ref.get_tex_id(): + texture = self.textures[drawCmd.tex_ref.get_tex_id()] + state = state.addAttrib(TextureAttrib.make(texture)) + + node.setGeomState(0, state) + + return task.cont + + def loadTexture(self, texture: Texture | str) -> imgui.ImTextureRef: + if type(texture) == str: + texture = base.loader.loadTexture(texture) + self.notify.debug(f"loadTexture ({self.textureCounter + 1})") + + self.textureCounter += 1 + self.textures[self.textureCounter] = texture + + return imgui.ImTextureRef(self.textureCounter) + + def removeTexture(self, tex: imgui.ImTextureRef, unload = True): + texture = self.textures.get(tex.get_tex_id(), None) + if texture: + self.notify.debug(f"removeTexture ({tex.get_tex_id()})") + + if unload: + texture.clear() + base.loader.unloadTexture(texture) + del texture + del self.textures[tex.get_tex_id()] + + def __updateTextures(self): + for tex in imgui.get_platform_io().textures: + if tex.status != imgui.ImTextureStatus.ok: + self.__updateTexture(tex) + + def __updateTexture(self, tex: imgui.ImTextureData): + def __setPixelsToTexture(texture: Texture): + # The easiest way to update a Panda texture + # is to recreate the ram image and rewrite + # all the pixels. + + # This clears the RamImage if one exists already. + textureData = texture.makeRamImage() + + pixels = tex.get_pixels_array() + textureData.setData(pixels.tobytes()) + + match tex.status: + case imgui.ImTextureStatus.want_create: + self.notify.debug(f"__updateTexture ({self.textureCounter + 1}): create ({tex.width}x{tex.height})") + assert tex.tex_id == 0 + assert tex.backend_user_data is None + assert tex.format == imgui.ImTextureFormat.rgba32 + + texture = Texture() + texture.setName(f"imgui-texture-{tex.unique_id}") + texture.setup2dTexture(tex.width, tex.height, Texture.T_unsigned_byte, Texture.F_rgba32) + texture.setMinfilter(SamplerState.FT_linear) + texture.setMagfilter(SamplerState.FT_linear) + + __setPixelsToTexture(texture) + + self.textureCounter += 1 + self.textures[self.textureCounter] = texture + + tex.tex_id = self.textureCounter + tex.status = imgui.ImTextureStatus.ok + case imgui.ImTextureStatus.want_updates: + self.notify.debug(f"__updateTexture ({tex.tex_id}): update ({tex.width}x{tex.height})") + + __setPixelsToTexture(self.textures[tex.tex_id]) + + tex.status = imgui.ImTextureStatus.ok + case imgui.ImTextureStatus.want_destroy: + texture = self.textures.get(tex.tex_id, None) + if texture: + texture.clear() + del texture + del self.textures[tex.tex_id] + + tex.tex_id = 0 + tex.status = imgui.ImTextureStatus.destroyed + + @staticmethod + def __createGeomnode(vdata: GeomVertexData) -> NodePath: + prim = GeomTriangles(Geom.UH_stream) + if imgui.INDEX_SIZE == 2: + prim.setIndexType(Geom.NT_uint16) + else: + prim.setIndexType(Geom.NT_uint32) + prim.closePrimitive() + + geom = Geom(vdata) + geom.addPrimitive(prim) + + node = GeomNode("imgui-geom") + node.addGeom(geom, RenderState.makeEmpty()) + + return NodePath(node) + + def cleanup(self): + if self.context: + imgui.destroy_context() + self.context = None + + @staticmethod + def __setClipboardText(_, str: str): + pyperclip.copy(str) + + @staticmethod + def __getClipboardText(_): + return pyperclip.paste() diff --git a/third_party/p3dimgui/shaders.py b/third_party/p3dimgui/shaders.py new file mode 100644 index 00000000..5f4f8bf7 --- /dev/null +++ b/third_party/p3dimgui/shaders.py @@ -0,0 +1,37 @@ +# These shaders is in GLSL version 1.20 is because so they can work +# right out of the box in macOS without having to force the OpenGL version +# to 3.2 (gl-version 3 2). + +FRAG_SHADER = """ +#version 120 + +varying vec2 texcoord; +varying vec4 color; + +uniform sampler2D p3d_Texture0; + +void main() +{ + gl_FragColor = color * texture2D(p3d_Texture0, texcoord); +} +""" + +VERT_SHADER = """ +#version 120 + +attribute vec4 p3d_Vertex; // { vec2 pos, vec2 uv } +attribute vec4 p3d_Color; + +varying vec2 texcoord; +varying vec4 color; + +uniform mat4 p3d_ModelViewProjectionMatrix; + +void main() +{ + texcoord = p3d_Vertex.zw; + color = p3d_Color.bgra; + gl_Position = p3d_ModelViewProjectionMatrix * vec4(p3d_Vertex.x, 0.0, -p3d_Vertex.y, 1.0); +} +""" + diff --git a/third_party/p3dimgui/utilities/ExplorerManager.py b/third_party/p3dimgui/utilities/ExplorerManager.py new file mode 100644 index 00000000..e91b306a --- /dev/null +++ b/third_party/p3dimgui/utilities/ExplorerManager.py @@ -0,0 +1,26 @@ +from direct.showbase.DirectObject import DirectObject +from panda3d.core import NodePath + +from p3dimgui.utilities.SceneGraphExplorer import SceneGraphExplorer + +from direct.extensions_native.extension_native_helpers import Dtool_funcToMethod + +class ExplorerManager(DirectObject): + def __init__(self): + DirectObject.__init__(self) + + self.nodesToExplorers: dict[NodePath, SceneGraphExplorer] = {} + + # Replace the explore() extension method for NodePath + def explore(nodePath): + self.nodesToExplorers[nodePath] = SceneGraphExplorer(nodePath) + + Dtool_funcToMethod(explore, NodePath) + self.accept('imgui-new-frame', self.__checkActive) + + def __checkActive(self): + for node in list(self.nodesToExplorers.keys()): + placer = self.nodesToExplorers[node] + if not placer.active: + del self.nodesToExplorers[node] + diff --git a/third_party/p3dimgui/utilities/IntervalTimeSlider.py b/third_party/p3dimgui/utilities/IntervalTimeSlider.py new file mode 100644 index 00000000..9910b661 --- /dev/null +++ b/third_party/p3dimgui/utilities/IntervalTimeSlider.py @@ -0,0 +1,88 @@ +from imgui_bundle import imgui, imgui_ctx + +from direct.showbase.DirectObject import DirectObject + +class IntervalTimeSlider(DirectObject): + def __init__(self, interval, active=True): + DirectObject.__init__(self) + self.interval = interval + self.__currentInterval = interval + + self.active = active + + self.windowPos = None + + self.__firstDraw = True + self.accept('imgui-new-frame', self.__draw) + + def __draw(self): + if not self.active: + return + curDuration = self.interval.getT() + maxDuration = self.interval.getDuration() + + if self.windowPos and (self.interval != self.__currentInterval): + imgui.set_next_window_pos(self.windowPos) + self.__currentInterval = self.interval + + if self.__firstDraw: + imgui.set_next_window_size((745,76)) + self.__firstDraw = False + with imgui_ctx.begin(f"Time Slider \"{self.interval.getName()}\"", True, + imgui.WindowFlags_.no_resize.value | imgui.WindowFlags_.no_scrollbar.value | imgui.WindowFlags_.no_scroll_with_mouse.value) as (_, windowOpen): + if not windowOpen or not self.interval: + self.active = False + return + + self.windowPos = imgui.get_window_pos() + + imgui.push_item_width(732) + changed, value = imgui.slider_float( + "##slider", curDuration, + v_min=0.0, v_max=self.interval.getDuration(), + format=f"{self.__calculateTimeFormat(curDuration)}/{self.__calculateTimeFormat(maxDuration)}", + flags=imgui.SliderFlags_.no_input.value + ) + imgui.pop_item_width() + + if changed: + self.interval.setT(value) + + imgui.spacing() + imgui.same_line(spacing=312) + if imgui.button("<<"): + self.interval.setT(0) + self.interval.pause() + + imgui.same_line() + + if imgui.button("Pause" if self.interval.isPlaying() else "Play"): + if self.interval.isPlaying(): + self.interval.pause() + else: + self.interval.resume() + + imgui.same_line() + + if imgui.button(">>"): + self.interval.setT(self.interval.getDuration()) + self.interval.pause() + + + def __calculateTimeFormat(self, duration): + if duration >= 60: + self.currentTime = "00:00" + totalSeconds = int(duration) % 60 + totalMinutes = int((duration - totalSeconds) / 60) + + if totalMinutes < 10: + totalMinutes = "%s%s" % (0, totalMinutes) + if totalSeconds < 10: + totalSeconds = "%s%s" % (0, totalSeconds) + + return "%s:%s" % (totalMinutes, totalSeconds) + else: + if duration < 10: + return "00:0%s" % (int(duration)) + else: + return "00:%s" % (int(duration)) diff --git a/third_party/p3dimgui/utilities/PlaceManager.py b/third_party/p3dimgui/utilities/PlaceManager.py new file mode 100644 index 00000000..b7c1ec1f --- /dev/null +++ b/third_party/p3dimgui/utilities/PlaceManager.py @@ -0,0 +1,27 @@ +from direct.showbase.DirectObject import DirectObject +from panda3d.core import NodePath + +from p3dimgui.utilities.PlacePanel import PlacePanel + +from direct.extensions_native.extension_native_helpers import Dtool_funcToMethod + +class PlaceManager(DirectObject): + def __init__(self): + DirectObject.__init__(self) + + self.nodesToPlacers: dict[NodePath, PlacePanel] = {} + + # Replace the place() and rgbPanel() extension methods for NodePath + def place(nodePath): + self.nodesToPlacers[nodePath] = PlacePanel(nodePath) + + Dtool_funcToMethod(place, NodePath) + Dtool_funcToMethod(place, NodePath, 'rgbPanel') + self.accept('imgui-new-frame', self.__checkActive) + + def __checkActive(self): + for node in list(self.nodesToPlacers.keys()): + placer = self.nodesToPlacers[node] + if not placer.active: + del self.nodesToPlacers[node] + diff --git a/third_party/p3dimgui/utilities/PlacePanel.py b/third_party/p3dimgui/utilities/PlacePanel.py new file mode 100644 index 00000000..5af71a46 --- /dev/null +++ b/third_party/p3dimgui/utilities/PlacePanel.py @@ -0,0 +1,224 @@ +from direct.showbase.DirectObject import DirectObject +from panda3d.core import NodePath + +from imgui_bundle import imgui, imgui_ctx + +class PlacePanel(DirectObject): + def __init__(self, nodePath: NodePath, active=True): + DirectObject.__init__(self) + + self.nodePath: NodePath = nodePath + self.__currentNodePath: NodePath = nodePath + + self.active = active + + self.scaleUniform = True + scale = self.nodePath.getScale() + if scale[0] != scale[1] or \ + scale[0] != scale[2] or \ + scale[1] != scale[2]: + self.scaleUniform = False + + self.windowPos = None + + self.accept('imgui-new-frame', self.__draw) + + def __del__(self): + self.ignoreAll() + + def __draw(self): + if not self.active: + return + + if not self.nodePath: + # Must've gotten deleted. + self.active = False + return + + pos = self.nodePath.getPos() + hpr = self.nodePath.getHpr() + scale = self.nodePath.getScale() + + # This is done to silence the "has no color set" warnings. + color = (1.0, 1.0, 1.0, 1.0) + if self.nodePath.hasColor(): + color = self.nodePath.getColor() + + colorScale = (1.0, 1.0, 1.0, 1.0) + if self.nodePath.hasColorScale(): + colorScale = self.nodePath.getColorScale() + + if self.windowPos and (self.nodePath != self.__currentNodePath): + imgui.set_next_window_pos(self.windowPos) + self.__currentNodePath = self.nodePath + + with imgui_ctx.begin(f"Node Placer \"{self.nodePath.getName()}\"", True) as (_, windowOpen): + if not windowOpen: + # Close has been clicked. + self.active = False + return + + self.windowPos = imgui.get_window_pos() + + with imgui_ctx.begin_group(): + imgui.text("Position") + + imgui.push_item_width(50) + xChanged, xValue = imgui.drag_float("X", pos[0], v_speed=0.05, format='%.3f') + yChanged, yValue = imgui.drag_float("Y", pos[1], v_speed=0.05, format='%.3f') + zChanged, zValue = imgui.drag_float("Z", pos[2], v_speed=0.05, format='%.3f') + imgui.pop_item_width() + + if xChanged: + self.nodePath.setX(xValue) + elif yChanged: + self.nodePath.setY(yValue) + elif zChanged: + self.nodePath.setZ(zValue) + + imgui.same_line(spacing=20) + + with imgui_ctx.begin_group(): + imgui.text("Orientation") + + imgui.push_item_width(50) + hChanged, hValue = imgui.drag_float("H", hpr[0], v_speed=0.05, format='%.3f') + pChanged, pValue = imgui.drag_float("P", hpr[1], v_speed=0.05, format='%.3f') + rChanged, rValue = imgui.drag_float("R", hpr[2], v_speed=0.05, format='%.3f') + imgui.pop_item_width() + + if hChanged: + self.nodePath.setH(hValue) + elif pChanged: + self.nodePath.setP(pValue) + elif rChanged: + self.nodePath.setR(rValue) + + imgui.same_line(spacing=20) + + with imgui_ctx.begin_group(): + imgui.text("Scaling") + + imgui.push_item_width(50) + sxChanged, sxValue = imgui.drag_float("X##scale", scale[0], v_speed=0.05, format='%.3f') + syChanged, syValue = imgui.drag_float("Y##scale", scale[1], v_speed=0.05, format='%.3f') + szChanged, szValue = imgui.drag_float("Z##scale", scale[2], v_speed=0.05, format='%.3f') + uniformBoxClicked, _ = imgui.checkbox("Uniform Scaling", self.scaleUniform) + imgui.pop_item_width() + + if uniformBoxClicked: + self.scaleUniform = not self.scaleUniform + + if sxChanged: + if self.scaleUniform: + self.nodePath.setScale(sxValue) + else: + self.nodePath.setSx(sxValue) + elif syChanged: + if self.scaleUniform: + self.nodePath.setScale(syValue) + else: + self.nodePath.setSy(syValue) + elif szChanged: + if self.scaleUniform: + self.nodePath.setScale(szValue) + else: + self.nodePath.setSz(szValue) + + # Colors + colorChanged, newColor = imgui.color_edit4('Color', color, imgui.ColorEditFlags_.float.value) + if colorChanged: + self.nodePath.setColor(*newColor) + colorScaleChanged, newColorScale = imgui.color_edit4('Color Scale', colorScale, imgui.ColorEditFlags_.float.value) + if colorScaleChanged: + self.nodePath.setColorScale(*newColorScale) + + imgui.separator() + + # Copy buttons + if imgui.button("Copy setPosHprScale()"): + imgui.open_popup("posHprScaleMenu") + + with imgui_ctx.begin_popup("posHprScaleMenu") as posHprScaleMenuOpened: + if posHprScaleMenuOpened: + for string in ( + f".setPosHpr(({pos[0]:.2f}, {pos[1]:.2f}, {pos[2]:.2f}), ({hpr[0]:.2f}, {hpr[1]:.2f}, {hpr[2]:.2f}))", + f".setPosHprScale(({pos[0]:.2f}, {pos[1]:.2f}, {pos[2]:.2f}), ({hpr[0]:.2f}, {hpr[1]:.2f}, {hpr[2]:.2f}), ({scale[0]:.2f}, {scale[1]:.2f}, {scale[2]:.2f}))", + "", + f".setPos(({pos[0]:.2f}, {pos[1]:.2f}, {pos[2]:.2f}))", + f".setHpr(({hpr[0]:.2f}, {hpr[1]:.2f}, {hpr[2]:.2f}))", + f".setScale(({scale[0]:.2f}, {scale[1]:.2f}, {scale[2]:.2f}))", + "", + f".setX({pos[0]:.2f})", + f".setY({pos[1]:.2f})", + f".setZ({pos[2]:.2f})", + "", + f".setH({hpr[0]:.2f})", + f".setP({hpr[1]:.2f})", + f".setR({hpr[2]:.2f})", + "", + f".setSx({scale[0]:.2f})", + f".setSy({scale[1]:.2f})", + f".setSz({scale[2]:.2f})", + "", + f".posHprInterval(1, ({pos[0]:.2f}, {pos[1]:.2f}, {pos[2]:.2f}), ({hpr[0]:.2f}, {hpr[1]:.2f}, {hpr[2]:.2f}), blendType='easeInOut'),", + f".scaleInterval(1, ({scale[0]:.2f}, {scale[1]:.2f}, {scale[2]:.2f}), blendType='easeInOut'),", + f".posHprScaleInterval(1, ({pos[0]:.2f}, {pos[1]:.2f}, {pos[2]:.2f}), ({hpr[0]:.2f}, {hpr[1]:.2f}, {hpr[2]:.2f}), ({scale[0]:.2f}, {scale[1]:.2f}, {scale[2]:.2f}), blendType='easeInOut'),", + "", + f"Func(node.setScale, ({scale[0]:.2f}, {scale[1]:.2f}, {scale[2]:.2f})),", + f"Func(node.setPosHpr, ({pos[0]:.2f}, {pos[1]:.2f}, {pos[2]:.2f}), ({hpr[0]:.2f}, {hpr[1]:.2f}, {hpr[2]:.2f})),", + f"Func(node.setPosHprScale, ({pos[0]:.2f}, {pos[1]:.2f}, {pos[2]:.2f}), ({hpr[0]:.2f}, {hpr[1]:.2f}, {hpr[2]:.2f}), ({scale[0]:.2f}, {scale[1]:.2f}, {scale[2]:.2f})),", + ): + if not string: + imgui.separator() + continue + clicked, _ = imgui.menu_item(string, "", False) + if clicked: + imgui.set_clipboard_text(string) + imgui.close_current_popup() + + imgui.same_line(spacing=20) + + if imgui.button("Copy setColor()"): + imgui.open_popup("colorMenu") + + with imgui_ctx.begin_popup("colorMenu") as colorMenuOpened: + if colorMenuOpened: + for string in ( + f".setColor(({colorScale[0]:.2f}, {color[1]:.2f}, {color[2]:.2f}))", + "", + f".colorInterval(1, ({color[0]:.2f}, {color[1]:.2f}, {color[2]:.2f}), blendType='easeInOut'),", + "", + f"Func(node.setColor, ({color[0]:.2f}, {color[1]:.2f}, {color[2]:.2f}))," + ): + if not string: + imgui.separator() + continue + clicked, _ = imgui.menu_item(string, "", False) + if clicked: + imgui.set_clipboard_text(string) + imgui.close_current_popup() + + imgui.same_line(spacing=20) + + if imgui.button("Copy setColorScale()"): + imgui.open_popup("colorScaleMenu") + + with imgui_ctx.begin_popup("colorScaleMenu") as colorScaleMenuOpened: + if colorScaleMenuOpened: + for string in ( + f".setColorScale(({colorScale[0]:.2f}, {colorScale[1]:.2f}, {colorScale[2]:.2f}))", + "", + f".colorScaleInterval(1, ({colorScale[0]:.2f}, {colorScale[1]:.2f}, {colorScale[2]:.2f}), blendType='easeInOut'),", + "", + f"Func(node.setColorScale, ({colorScale[0]:.2f}, {colorScale[1]:.2f}, {colorScale[2]:.2f}))," + ): + if not string: + imgui.separator() + continue + clicked, _ = imgui.menu_item(string, "", False) + if clicked: + imgui.set_clipboard_text(string) + imgui.close_current_popup() + + diff --git a/third_party/p3dimgui/utilities/SceneGraphExplorer.py b/third_party/p3dimgui/utilities/SceneGraphExplorer.py new file mode 100644 index 00000000..c611b71f --- /dev/null +++ b/third_party/p3dimgui/utilities/SceneGraphExplorer.py @@ -0,0 +1,171 @@ +from panda3d.core import NodePath +from direct.showbase.DirectObject import DirectObject +from direct.interval.IntervalGlobal import Sequence, Func, Wait + +from imgui_bundle import imgui, imgui_ctx + +class SceneGraphExplorer(DirectObject): + def __init__(self, nodePath = None, active = True): + DirectObject.__init__(self) + self.nodePath: NodePath = nodePath + self.active = active + if not nodePath: + self.nodePath = base.render + + self.__currentNodePath = self.nodePath + + self.windowPos = None + + self.reparentTarget: NodePath | None = None + + self.flashOnClick = False + self.highlightSeq: Sequence | None = None + + self.rename = '' + self.focusSetNameInput = False + + self.__firstDraw = True + self.accept('imgui-new-frame', self.__draw) + + def flash(self, nodePath): + if self.highlightSeq: + self.highlightSeq.finish() + colorScale = nodePath.getColorScale() + self.highlightSeq = Sequence( + Func(nodePath.setColorScale, (1.0, 0.0, 0.0, 0.0)), + Wait(1), + Func(nodePath.setColorScale, colorScale) + ) + self.highlightSeq.start() + + def __del__(self): + self.ignoreAll() + + def __draw(self): + if not self.active: + return + + id = 0 + def drawTreeForNode(nodePath): + nonlocal id + flags = 0 + if nodePath.getNumChildren() == 0: + flags = imgui.TreeNodeFlags_.leaf.value + elif nodePath == self.nodePath: + flags = imgui.TreeNodeFlags_.default_open.value + + typeName = nodePath.node().getType().getName() + name = nodePath.getName() + + tree = imgui.tree_node_ex(f"{self.nodePath.getName()}-{id}", flags, f"{typeName} {name}") + + if self.flashOnClick and imgui.is_item_clicked(): + self.flash(nodePath) + + namePopupId = imgui.get_id(f"{nodePath}-namePopup") + + if imgui.begin_popup_context_item(): + clickedFlash, _ = imgui.menu_item("Flash", "", False) + if clickedFlash: + self.flash(nodePath) + + imgui.separator() + + clickedSetName, _ = imgui.menu_item("Set Name", "", False) + if clickedSetName: + self.rename = nodePath.getName() + self.focusSetNameInput = True + imgui.open_popup(namePopupId) + + imgui.separator() + + clickedSetTarget, _ = imgui.menu_item("Set Reparent Target", "", False) + if clickedSetTarget: + self.reparentTarget = nodePath + + clickedReparent, _ = imgui.menu_item("Reparent to Target", "", False, self.reparentTarget is not None) + if clickedReparent: + nodePath.reparentTo(self.reparentTarget) + + clickedWrtReparent, _ = imgui.menu_item("WRT Reparent To Target", "", False, self.reparentTarget is not None) + if clickedWrtReparent: + nodePath.wrtReparentTo(self.reparentTarget) + + imgui.separator() + + clickedPlace, _ = imgui.menu_item("Place", "", False) + if clickedPlace: + nodePath.place() + + if nodePath != self.nodePath: + clickedExplore, _ = imgui.menu_item("Explore Seperately", "", False) + if clickedExplore: + nodePath.explore() + + clickedDelete, _ = imgui.menu_item("Delete", "", False) + if clickedDelete: + nodePath.removeNode() + + imgui.end_popup() + + with imgui_ctx.begin_popup(f"{nodePath}-namePopup") as namePopup: + if namePopup: + imgui.text("Set Name:") + imgui.same_line() + + if self.focusSetNameInput: + imgui.set_keyboard_focus_here() + self.focusSetNameInput = False + + nameChanged, newName = imgui.input_text("##input", self.rename, imgui.InputTextFlags_.chars_no_blank.value) + if nameChanged: + self.rename = newName + + if imgui.button("OK") or imgui.is_key_pressed(imgui.Key.enter): + nodePath.setName(self.rename) + self.rename = '' + imgui.close_current_popup() + + imgui.same_line() + + if imgui.button("Cancel") or imgui.is_key_pressed(imgui.Key.escape): + self.rename = '' + imgui.close_current_popup() + + if tree: + for child in nodePath.children: + id += 1 + drawTreeForNode(child) + imgui.tree_pop() + + if self.__firstDraw: + imgui.set_next_window_size((410,761)) + self.__firstDraw = False + + if self.windowPos and (self.nodePath != self.__currentNodePath): + imgui.set_next_window_pos(self.windowPos) + self.__currentNodePath = self.nodePath + + with imgui_ctx.begin(f"Explore: {self.nodePath.getName()}", True, imgui.WindowFlags_.menu_bar) as (_, windowOpen): + if not windowOpen or not self.nodePath: + self.active = False + return + + self.windowPos = imgui.get_window_pos() + + with imgui_ctx.begin_menu_bar() as menuBar: + if menuBar: + with imgui_ctx.begin_menu("Options") as optionsMenu: + if optionsMenu: + clickedFlashToggle, _ = imgui.menu_item("Flash on Click", "", self.flashOnClick) + if clickedFlashToggle: + self.flashOnClick = not self.flashOnClick + + imgui.text(f"Active Reparent Target: {self.reparentTarget}") + drawTreeForNode(self.nodePath) + + + + + + diff --git a/third_party/p3dimgui/utilities/TimeSliderManager.py b/third_party/p3dimgui/utilities/TimeSliderManager.py new file mode 100644 index 00000000..8eb74e74 --- /dev/null +++ b/third_party/p3dimgui/utilities/TimeSliderManager.py @@ -0,0 +1,26 @@ +from direct.showbase.DirectObject import DirectObject +from panda3d.direct import CInterval + +from p3dimgui.utilities.IntervalTimeSlider import IntervalTimeSlider + +from direct.extensions_native.extension_native_helpers import Dtool_funcToMethod + +class TimeSliderManager(DirectObject): + def __init__(self): + DirectObject.__init__(self) + + self.intervalToTimeSliders: dict[CInterval, IntervalTimeSlider] = {} + + # Replace the popupControls() extension method for CInterval + def popupControls(interval): + self.intervalToTimeSliders[interval] = IntervalTimeSlider(interval) + + Dtool_funcToMethod(popupControls, CInterval) + Dtool_funcToMethod(popupControls, CInterval, "slider") + self.accept('imgui-new-frame', self.__checkActive) + + def __checkActive(self): + for node in list(self.intervalToTimeSliders.keys()): + placer = self.intervalToTimeSliders[node] + if not placer.active: + del self.intervalToTimeSliders[node] diff --git a/ui/LUI/lui_manager_editor.py b/ui/LUI/lui_manager_editor.py index 4d466cbd..b9320cc3 100644 --- a/ui/LUI/lui_manager_editor.py +++ b/ui/LUI/lui_manager_editor.py @@ -614,12 +614,17 @@ class LUIManagerEditorMixin: def draw_editor(self): """Draw the LUI Editor Window""" + if hasattr(self, "world") and hasattr(self.world, "showLUIEditor"): + self.show_editor = bool(self.world.showLUIEditor) + if not self.lui_enabled or not self.show_editor: return imgui.set_next_window_size((350, 600), imgui.Cond_.first_use_ever) with imgui_ctx.begin("LUI编辑器", True) as (opened, show): self.show_editor = opened + if hasattr(self, "world") and hasattr(self.world, "showLUIEditor"): + self.world.showLUIEditor = opened if not show: return diff --git a/ui/panels/editor_panels_top.py b/ui/panels/editor_panels_top.py index bde4fb91..dd570e68 100644 --- a/ui/panels/editor_panels_top.py +++ b/ui/panels/editor_panels_top.py @@ -143,6 +143,8 @@ class EditorPanelsTopMixin: _, self.app.showConsole = imgui.menu_item("控制台", "", self.app.showConsole, True) _, self.app.showScriptPanel = imgui.menu_item("脚本管理", "", self.app.showScriptPanel, True) _, self.app.showLUIEditor = imgui.menu_item("LUI编辑器", "", self.app.showLUIEditor, True) + if hasattr(self.app, "lui_manager"): + self.app.lui_manager.show_editor = self.app.showLUIEditor prev_show_web_panel = self.app.showWebPanel _, self.app.showWebPanel = imgui.menu_item("Web面板", "", self.app.showWebPanel, True) if prev_show_web_panel and not self.app.showWebPanel: