From 62fe0628d8a5ef948a56acdbf10b98e53cac51bb Mon Sep 17 00:00:00 2001
From: Hector <145347438+hudomn@users.noreply.github.com>
Date: Wed, 25 Feb 2026 11:49:31 +0800
Subject: [PATCH] IMGui
---
.idea/misc.xml | 2 +-
RenderPipelineFile/config/plugins.yaml | 12 +-
core/CustomMouseController.py | 33 +-
core/event_handler.py | 4 +-
core/imgui_style_manager.py | 5 +-
core/selection.py | 92 +-
core/tool_manager.py | 6 +
core/world.py | 11 +
imgui.ini | 117 +-
main.py | 7763 ++---------------
requirements/requirements.txt | 1 +
ssbo_component/README.md | 67 +
ssbo_component/demo_component.py | 77 +
ssbo_component/effects/ssbo_instancing.yaml | 36 +
ssbo_component/shaders/pick_id.frag | 21 +
ssbo_component/shaders/pick_id.vert | 21 +
ssbo_component/ssbo_controller.py | 550 ++
ssbo_component/ssbo_editor.py | 617 ++
ui/Builtin/Elements.py | 339 +
ui/Builtin/LUIBlockText.py | 100 +
ui/Builtin/LUIButton.py | 192 +
ui/Builtin/LUICanvas.py | 102 +
ui/Builtin/LUICheckbox.py | 83 +
ui/Builtin/LUIFormattedLabel.py | 47 +
ui/Builtin/LUIFrame.py | 66 +
ui/Builtin/LUIHorizontalLayout.py | 20 +
ui/Builtin/LUIInitialState.py | 41 +
ui/Builtin/LUIInputField.py | 219 +
ui/Builtin/LUIInputHandler.py | 8 +
ui/Builtin/LUILabel.py | 77 +
ui/Builtin/LUILayouts.py | 105 +
ui/Builtin/LUIObject.py | 18 +
ui/Builtin/LUIProgressbar.py | 72 +
ui/Builtin/LUIRadiobox.py | 91 +
ui/Builtin/LUIRadioboxGroup.py | 41 +
ui/Builtin/LUIRegion.py | 8 +
ui/Builtin/LUIRoot.py | 8 +
ui/Builtin/LUIScrollableRegion.py | 155 +
ui/Builtin/LUISelectbox.py | 207 +
ui/Builtin/LUISkin.py | 53 +
ui/Builtin/LUISlider.py | 214 +
ui/Builtin/LUISprite.py | 18 +
ui/Builtin/LUISpriteButton.py | 30 +
ui/Builtin/LUITabbedFrame.py | 87 +
ui/Builtin/LUIVerticalLayout.py | 20 +
ui/Builtin/RectTransform.py | 85 +
ui/Builtin/__init__.py | 0
ui/Skins/Default/GenerateAtlas.bat | 5 +
ui/Skins/Default/__init__.py | 0
ui/Skins/Default/font/SourceSansPro-Black.ttf | Bin 0 -> 289528 bytes
.../Default/font/SourceSansPro-BlackIt.ttf | Bin 0 -> 102756 bytes
ui/Skins/Default/font/SourceSansPro-Bold.ttf | Bin 0 -> 291584 bytes
.../Default/font/SourceSansPro-BoldIt.ttf | Bin 0 -> 103008 bytes
.../Default/font/SourceSansPro-ExtraLight.ttf | Bin 0 -> 291648 bytes
.../font/SourceSansPro-ExtraLightIt.ttf | Bin 0 -> 104124 bytes
ui/Skins/Default/font/SourceSansPro-It.ttf | Bin 0 -> 103620 bytes
ui/Skins/Default/font/SourceSansPro-Light.ttf | Bin 0 -> 293304 bytes
.../Default/font/SourceSansPro-LightIt.ttf | Bin 0 -> 103996 bytes
.../Default/font/SourceSansPro-Regular.ttf | Bin 0 -> 293960 bytes
.../Default/font/SourceSansPro-Semibold.ttf | Bin 0 -> 292456 bytes
.../Default/font/SourceSansPro-SemiboldIt.ttf | Bin 0 -> 103336 bytes
ui/Skins/Default/res/ButtonDefault.png | Bin 0 -> 2858 bytes
ui/Skins/Default/res/ButtonDefaultFocus.png | Bin 0 -> 2858 bytes
.../Default/res/ButtonDefaultFocus_Left.png | Bin 0 -> 2993 bytes
.../Default/res/ButtonDefaultFocus_Right.png | Bin 0 -> 2995 bytes
ui/Skins/Default/res/ButtonDefault_Left.png | Bin 0 -> 3000 bytes
ui/Skins/Default/res/ButtonDefault_Right.png | Bin 0 -> 2999 bytes
ui/Skins/Default/res/ButtonGreen.png | Bin 0 -> 2899 bytes
ui/Skins/Default/res/ButtonGreenFocus.png | Bin 0 -> 2898 bytes
.../Default/res/ButtonGreenFocus_Left.png | Bin 0 -> 3051 bytes
.../Default/res/ButtonGreenFocus_Right.png | Bin 0 -> 3053 bytes
ui/Skins/Default/res/ButtonGreen_Left.png | Bin 0 -> 3051 bytes
ui/Skins/Default/res/ButtonGreen_Right.png | Bin 0 -> 3052 bytes
ui/Skins/Default/res/Checkbox_Checked.png | Bin 0 -> 3258 bytes
.../Default/res/Checkbox_CheckedHover.png | Bin 0 -> 3263 bytes
ui/Skins/Default/res/Checkbox_Default.png | Bin 0 -> 2911 bytes
.../Default/res/Checkbox_DefaultHover.png | Bin 0 -> 2913 bytes
.../res/ColorpickerActiveColorOverlay.png | Bin 0 -> 2961 bytes
.../Default/res/ColorpickerFieldHandle.png | Bin 0 -> 3185 bytes
.../Default/res/ColorpickerFieldOverlay.png | Bin 0 -> 8869 bytes
ui/Skins/Default/res/ColorpickerHueHandle.png | Bin 0 -> 3674 bytes
ui/Skins/Default/res/ColorpickerHueSlider.png | Bin 0 -> 3129 bytes
ui/Skins/Default/res/ColorpickerPreviewBg.png | Bin 0 -> 2902 bytes
.../Default/res/ColorpickerPreviewOverlay.png | Bin 0 -> 2942 bytes
ui/Skins/Default/res/Frame_BL.png | Bin 0 -> 1101 bytes
ui/Skins/Default/res/Frame_BR.png | Bin 0 -> 1105 bytes
ui/Skins/Default/res/Frame_Bottom.png | Bin 0 -> 1517 bytes
ui/Skins/Default/res/Frame_Left.png | Bin 0 -> 1043 bytes
ui/Skins/Default/res/Frame_Mid.png | Bin 0 -> 99 bytes
ui/Skins/Default/res/Frame_Right.png | Bin 0 -> 1043 bytes
ui/Skins/Default/res/Frame_TL.png | Bin 0 -> 732 bytes
ui/Skins/Default/res/Frame_TR.png | Bin 0 -> 727 bytes
ui/Skins/Default/res/Frame_Top.png | Bin 0 -> 874 bytes
.../Default/res/HorizontalListDivider.png | Bin 0 -> 2792 bytes
ui/Skins/Default/res/InputField.png | Bin 0 -> 2803 bytes
ui/Skins/Default/res/InputField_Left.png | Bin 0 -> 2870 bytes
ui/Skins/Default/res/InputField_Right.png | Bin 0 -> 2870 bytes
ui/Skins/Default/res/Keymarker.png | Bin 0 -> 2841 bytes
ui/Skins/Default/res/Keymarker_Left.png | Bin 0 -> 3001 bytes
ui/Skins/Default/res/Keymarker_Right.png | Bin 0 -> 3024 bytes
ui/Skins/Default/res/ListDivider.png | Bin 0 -> 2792 bytes
ui/Skins/Default/res/Popup_BL.png | Bin 0 -> 1101 bytes
ui/Skins/Default/res/Popup_BR.png | Bin 0 -> 1105 bytes
ui/Skins/Default/res/Popup_Bottom.png | Bin 0 -> 1517 bytes
ui/Skins/Default/res/Popup_Left.png | Bin 0 -> 1043 bytes
ui/Skins/Default/res/Popup_Mid.png | Bin 0 -> 99 bytes
ui/Skins/Default/res/Popup_Right.png | Bin 0 -> 1043 bytes
ui/Skins/Default/res/Popup_TL.png | Bin 0 -> 728 bytes
ui/Skins/Default/res/Popup_TR.png | Bin 0 -> 724 bytes
ui/Skins/Default/res/Popup_Top.png | Bin 0 -> 874 bytes
ui/Skins/Default/res/ProgressbarBg.png | Bin 0 -> 2809 bytes
ui/Skins/Default/res/ProgressbarBg_Left.png | Bin 0 -> 2850 bytes
ui/Skins/Default/res/ProgressbarBg_Right.png | Bin 0 -> 2852 bytes
ui/Skins/Default/res/ProgressbarFg.png | Bin 0 -> 2809 bytes
ui/Skins/Default/res/ProgressbarFg_Finish.png | Bin 0 -> 3086 bytes
ui/Skins/Default/res/ProgressbarFg_Left.png | Bin 0 -> 2881 bytes
ui/Skins/Default/res/ProgressbarFg_Right.png | Bin 0 -> 3070 bytes
ui/Skins/Default/res/Radiobox_Active.png | Bin 0 -> 3361 bytes
ui/Skins/Default/res/Radiobox_ActiveHover.png | Bin 0 -> 3372 bytes
ui/Skins/Default/res/Radiobox_Default.png | Bin 0 -> 3217 bytes
.../Default/res/Radiobox_DefaultHover.png | Bin 0 -> 3214 bytes
ui/Skins/Default/res/ScrollShadowBottom.png | Bin 0 -> 2814 bytes
.../Default/res/ScrollShadowBottom_Left.png | Bin 0 -> 2950 bytes
.../Default/res/ScrollShadowBottom_Right.png | Bin 0 -> 2995 bytes
ui/Skins/Default/res/ScrollShadowTop.png | Bin 0 -> 2820 bytes
ui/Skins/Default/res/ScrollShadowTop_Left.png | Bin 0 -> 2937 bytes
.../Default/res/ScrollShadowTop_Right.png | Bin 0 -> 2979 bytes
ui/Skins/Default/res/ScrollbarHandle.png | Bin 0 -> 2818 bytes
.../Default/res/ScrollbarHandle_Bottom.png | Bin 0 -> 2928 bytes
ui/Skins/Default/res/ScrollbarHandle_Top.png | Bin 0 -> 2975 bytes
ui/Skins/Default/res/Selectbox.png | Bin 0 -> 2822 bytes
ui/Skins/Default/res/SelectboxActive.png | Bin 0 -> 2831 bytes
ui/Skins/Default/res/SelectboxActive_Left.png | Bin 0 -> 2920 bytes
ui/Skins/Default/res/SelectboxOpen_Right.png | Bin 0 -> 3043 bytes
ui/Skins/Default/res/Selectbox_Left.png | Bin 0 -> 2910 bytes
ui/Skins/Default/res/Selectbox_Right.png | Bin 0 -> 3101 bytes
ui/Skins/Default/res/SelectdropDivider.png | Bin 0 -> 2797 bytes
ui/Skins/Default/res/Selectdrop_BL.png | Bin 0 -> 2922 bytes
ui/Skins/Default/res/Selectdrop_BR.png | Bin 0 -> 2925 bytes
ui/Skins/Default/res/Selectdrop_Bottom.png | Bin 0 -> 2812 bytes
ui/Skins/Default/res/Selectdrop_Left.png | Bin 0 -> 2812 bytes
ui/Skins/Default/res/Selectdrop_Mid.png | Bin 0 -> 2792 bytes
ui/Skins/Default/res/Selectdrop_Right.png | Bin 0 -> 2812 bytes
ui/Skins/Default/res/Selectdrop_TL.png | Bin 0 -> 2837 bytes
ui/Skins/Default/res/Selectdrop_TR.png | Bin 0 -> 2837 bytes
ui/Skins/Default/res/Selectdrop_Top.png | Bin 0 -> 2807 bytes
ui/Skins/Default/res/SliderBg.png | Bin 0 -> 2808 bytes
ui/Skins/Default/res/SliderBgFill.png | Bin 0 -> 2808 bytes
ui/Skins/Default/res/SliderBgFill_Left.png | Bin 0 -> 2889 bytes
ui/Skins/Default/res/SliderBg_Left.png | Bin 0 -> 2851 bytes
ui/Skins/Default/res/SliderBg_Right.png | Bin 0 -> 2857 bytes
ui/Skins/Default/res/SliderKnob.png | Bin 0 -> 3760 bytes
ui/Skins/Default/res/SunkenFrame_BL.png | Bin 0 -> 190 bytes
ui/Skins/Default/res/SunkenFrame_BR.png | Bin 0 -> 191 bytes
ui/Skins/Default/res/SunkenFrame_Bottom.png | Bin 0 -> 121 bytes
ui/Skins/Default/res/SunkenFrame_Left.png | Bin 0 -> 130 bytes
ui/Skins/Default/res/SunkenFrame_Mid.png | Bin 0 -> 112 bytes
ui/Skins/Default/res/SunkenFrame_Right.png | Bin 0 -> 132 bytes
ui/Skins/Default/res/SunkenFrame_TL.png | Bin 0 -> 197 bytes
ui/Skins/Default/res/SunkenFrame_TR.png | Bin 0 -> 200 bytes
ui/Skins/Default/res/SunkenFrame_Top.png | Bin 0 -> 126 bytes
ui/Skins/Default/res/atlas.png | Bin 0 -> 26117 bytes
ui/Skins/Default/res/atlas.txt | 101 +
ui/Skins/Default/res/blank.png | Bin 0 -> 2793 bytes
ui/Skins/Metro/GenerateAtlas.bat | 5 +
ui/Skins/Metro/LUIMetroSkin.py | 35 +
ui/Skins/Metro/__init__.py | 0
ui/Skins/Metro/border.png | Bin 0 -> 2792 bytes
ui/Skins/Metro/copy_frames.py | 41 +
ui/Skins/Metro/flat.png | Bin 0 -> 2792 bytes
ui/Skins/Metro/font/Roboto-Bold.ttf | Bin 0 -> 135820 bytes
ui/Skins/Metro/font/Roboto-LICENSE.txt | 178 +
ui/Skins/Metro/font/Roboto-Light.ttf | Bin 0 -> 140276 bytes
ui/Skins/Metro/font/Roboto-Medium.ttf | Bin 0 -> 160696 bytes
ui/Skins/Metro/font/Roboto-Thin.ttf | Bin 0 -> 130044 bytes
ui/Skins/Metro/res/ButtonDefault.png | Bin 0 -> 2797 bytes
ui/Skins/Metro/res/ButtonDefaultFocus.png | Bin 0 -> 2797 bytes
.../Metro/res/ButtonDefaultFocus_Left.png | Bin 0 -> 2797 bytes
.../Metro/res/ButtonDefaultFocus_Right.png | Bin 0 -> 2797 bytes
ui/Skins/Metro/res/ButtonDefault_Left.png | Bin 0 -> 2797 bytes
ui/Skins/Metro/res/ButtonDefault_Right.png | Bin 0 -> 2797 bytes
ui/Skins/Metro/res/ButtonGreen.png | Bin 0 -> 2797 bytes
ui/Skins/Metro/res/ButtonGreenFocus.png | Bin 0 -> 2797 bytes
ui/Skins/Metro/res/ButtonGreenFocus_Left.png | Bin 0 -> 2797 bytes
ui/Skins/Metro/res/ButtonGreenFocus_Right.png | Bin 0 -> 2797 bytes
ui/Skins/Metro/res/ButtonGreen_Left.png | Bin 0 -> 2797 bytes
ui/Skins/Metro/res/ButtonGreen_Right.png | Bin 0 -> 2797 bytes
ui/Skins/Metro/res/Checkbox_Checked.png | Bin 0 -> 2841 bytes
ui/Skins/Metro/res/Checkbox_CheckedHover.png | Bin 0 -> 2841 bytes
ui/Skins/Metro/res/Checkbox_Default.png | Bin 0 -> 2823 bytes
ui/Skins/Metro/res/Checkbox_DefaultHover.png | Bin 0 -> 2823 bytes
.../res/ColorpickerActiveColorOverlay.png | Bin 0 -> 2961 bytes
ui/Skins/Metro/res/ColorpickerFieldHandle.png | Bin 0 -> 3185 bytes
.../Metro/res/ColorpickerFieldOverlay.png | Bin 0 -> 8869 bytes
ui/Skins/Metro/res/ColorpickerHueHandle.png | Bin 0 -> 3674 bytes
ui/Skins/Metro/res/ColorpickerHueSlider.png | Bin 0 -> 3129 bytes
ui/Skins/Metro/res/ColorpickerPreviewBg.png | Bin 0 -> 2902 bytes
.../Metro/res/ColorpickerPreviewOverlay.png | Bin 0 -> 2942 bytes
ui/Skins/Metro/res/Draft3.psd | Bin 0 -> 13519832 bytes
ui/Skins/Metro/res/Frame_BL.png | Bin 0 -> 2792 bytes
ui/Skins/Metro/res/Frame_BR.png | Bin 0 -> 2792 bytes
ui/Skins/Metro/res/Frame_Bottom.png | Bin 0 -> 2792 bytes
ui/Skins/Metro/res/Frame_Left.png | Bin 0 -> 2792 bytes
ui/Skins/Metro/res/Frame_Mid.png | Bin 0 -> 2792 bytes
ui/Skins/Metro/res/Frame_Right.png | Bin 0 -> 2792 bytes
ui/Skins/Metro/res/Frame_TL.png | Bin 0 -> 2792 bytes
ui/Skins/Metro/res/Frame_TR.png | Bin 0 -> 2792 bytes
ui/Skins/Metro/res/Frame_Top.png | Bin 0 -> 2792 bytes
ui/Skins/Metro/res/HorizontalListDivider.png | Bin 0 -> 2792 bytes
ui/Skins/Metro/res/InputField.png | Bin 0 -> 2797 bytes
ui/Skins/Metro/res/InputField_Left.png | Bin 0 -> 2797 bytes
ui/Skins/Metro/res/InputField_Right.png | Bin 0 -> 2797 bytes
ui/Skins/Metro/res/Keymarker.png | Bin 0 -> 2841 bytes
ui/Skins/Metro/res/Keymarker_Left.png | Bin 0 -> 3001 bytes
ui/Skins/Metro/res/Keymarker_Right.png | Bin 0 -> 3024 bytes
ui/Skins/Metro/res/ListDivider.png | Bin 0 -> 2792 bytes
ui/Skins/Metro/res/Popup_BL.png | Bin 0 -> 2792 bytes
ui/Skins/Metro/res/Popup_BR.png | Bin 0 -> 2792 bytes
ui/Skins/Metro/res/Popup_Bottom.png | Bin 0 -> 2792 bytes
ui/Skins/Metro/res/Popup_Left.png | Bin 0 -> 2792 bytes
ui/Skins/Metro/res/Popup_Mid.png | Bin 0 -> 2792 bytes
ui/Skins/Metro/res/Popup_Right.png | Bin 0 -> 2792 bytes
ui/Skins/Metro/res/Popup_TL.png | Bin 0 -> 2792 bytes
ui/Skins/Metro/res/Popup_TR.png | Bin 0 -> 2792 bytes
ui/Skins/Metro/res/Popup_Top.png | Bin 0 -> 2792 bytes
ui/Skins/Metro/res/ProgressbarBg.png | Bin 0 -> 2797 bytes
ui/Skins/Metro/res/ProgressbarBg_Left.png | Bin 0 -> 2797 bytes
ui/Skins/Metro/res/ProgressbarBg_Right.png | Bin 0 -> 2797 bytes
ui/Skins/Metro/res/ProgressbarFg.png | Bin 0 -> 2799 bytes
ui/Skins/Metro/res/ProgressbarFg_Finish.png | Bin 0 -> 2799 bytes
ui/Skins/Metro/res/ProgressbarFg_Left.png | Bin 0 -> 2799 bytes
ui/Skins/Metro/res/ProgressbarFg_Right.png | Bin 0 -> 2799 bytes
ui/Skins/Metro/res/Radiobox_Active.png | Bin 0 -> 3112 bytes
ui/Skins/Metro/res/Radiobox_ActiveHover.png | Bin 0 -> 3112 bytes
ui/Skins/Metro/res/Radiobox_Default.png | Bin 0 -> 3065 bytes
ui/Skins/Metro/res/Radiobox_DefaultHover.png | Bin 0 -> 3065 bytes
ui/Skins/Metro/res/ScrollShadowBottom.png | Bin 0 -> 2814 bytes
.../Metro/res/ScrollShadowBottom_Left.png | Bin 0 -> 2950 bytes
.../Metro/res/ScrollShadowBottom_Right.png | Bin 0 -> 2995 bytes
ui/Skins/Metro/res/ScrollShadowTop.png | Bin 0 -> 2820 bytes
ui/Skins/Metro/res/ScrollShadowTop_Left.png | Bin 0 -> 2937 bytes
ui/Skins/Metro/res/ScrollShadowTop_Right.png | Bin 0 -> 2979 bytes
ui/Skins/Metro/res/ScrollbarHandle.png | Bin 0 -> 2818 bytes
ui/Skins/Metro/res/ScrollbarHandle_Bottom.png | Bin 0 -> 2928 bytes
ui/Skins/Metro/res/ScrollbarHandle_Top.png | Bin 0 -> 2975 bytes
ui/Skins/Metro/res/Selectbox.png | Bin 0 -> 2800 bytes
ui/Skins/Metro/res/SelectboxActive.png | Bin 0 -> 2800 bytes
ui/Skins/Metro/res/SelectboxActive_Left.png | Bin 0 -> 2800 bytes
ui/Skins/Metro/res/SelectboxOpen_Right.png | Bin 0 -> 2980 bytes
ui/Skins/Metro/res/Selectbox_Left.png | Bin 0 -> 2800 bytes
ui/Skins/Metro/res/Selectbox_Right.png | Bin 0 -> 2990 bytes
ui/Skins/Metro/res/SelectdropDivider.png | Bin 0 -> 2792 bytes
ui/Skins/Metro/res/Selectdrop_BL.png | Bin 0 -> 2791 bytes
ui/Skins/Metro/res/Selectdrop_BR.png | Bin 0 -> 2791 bytes
ui/Skins/Metro/res/Selectdrop_Bottom.png | Bin 0 -> 2791 bytes
ui/Skins/Metro/res/Selectdrop_Left.png | Bin 0 -> 2791 bytes
ui/Skins/Metro/res/Selectdrop_Mid.png | Bin 0 -> 2791 bytes
ui/Skins/Metro/res/Selectdrop_Right.png | Bin 0 -> 2791 bytes
ui/Skins/Metro/res/Selectdrop_TL.png | Bin 0 -> 2791 bytes
ui/Skins/Metro/res/Selectdrop_TR.png | Bin 0 -> 2791 bytes
ui/Skins/Metro/res/Selectdrop_Top.png | Bin 0 -> 2791 bytes
ui/Skins/Metro/res/SliderBg.png | Bin 0 -> 2797 bytes
ui/Skins/Metro/res/SliderBgFill.png | Bin 0 -> 2799 bytes
ui/Skins/Metro/res/SliderBgFill_Left.png | Bin 0 -> 2799 bytes
ui/Skins/Metro/res/SliderBg_Left.png | Bin 0 -> 2797 bytes
ui/Skins/Metro/res/SliderBg_Right.png | Bin 0 -> 2797 bytes
ui/Skins/Metro/res/SliderKnob.png | Bin 0 -> 2802 bytes
ui/Skins/Metro/res/SunkenFrame_BL.png | Bin 0 -> 2792 bytes
ui/Skins/Metro/res/SunkenFrame_BR.png | Bin 0 -> 2792 bytes
ui/Skins/Metro/res/SunkenFrame_Bottom.png | Bin 0 -> 2792 bytes
ui/Skins/Metro/res/SunkenFrame_Left.png | Bin 0 -> 2792 bytes
ui/Skins/Metro/res/SunkenFrame_Mid.png | Bin 0 -> 2792 bytes
ui/Skins/Metro/res/SunkenFrame_Right.png | Bin 0 -> 2792 bytes
ui/Skins/Metro/res/SunkenFrame_TL.png | Bin 0 -> 2792 bytes
ui/Skins/Metro/res/SunkenFrame_TR.png | Bin 0 -> 2792 bytes
ui/Skins/Metro/res/SunkenFrame_Top.png | Bin 0 -> 2792 bytes
ui/Skins/Metro/res/atlas.png | Bin 0 -> 13896 bytes
ui/Skins/Metro/res/atlas.txt | 101 +
ui/Skins/Metro/res/blank.png | Bin 0 -> 2793 bytes
ui/Skins/__init__.py | 0
ui/lui.pyd | Bin 0 -> 1079296 bytes
ui/lui_function.py | 2556 ++++++
ui/lui_manager.py | 3374 +++++++
ui/panels/__init__.py | 0
ui/panels/animation_tools.py | 434 +
ui/panels/app_actions.py | 1113 +++
ui/panels/create_actions.py | 215 +
ui/panels/dialog_panels.py | 1461 ++++
ui/panels/editor_panels.py | 2020 +++++
ui/panels/interaction_panels.py | 253 +
ui/panels/object_factory.py | 406 +
ui/panels/property_helpers.py | 1460 ++++
ui/panels/script_panels.py | 299 +
293 files changed, 18798 insertions(+), 7200 deletions(-)
create mode 100644 ssbo_component/README.md
create mode 100644 ssbo_component/demo_component.py
create mode 100644 ssbo_component/effects/ssbo_instancing.yaml
create mode 100644 ssbo_component/shaders/pick_id.frag
create mode 100644 ssbo_component/shaders/pick_id.vert
create mode 100644 ssbo_component/ssbo_controller.py
create mode 100644 ssbo_component/ssbo_editor.py
create mode 100644 ui/Builtin/Elements.py
create mode 100644 ui/Builtin/LUIBlockText.py
create mode 100644 ui/Builtin/LUIButton.py
create mode 100644 ui/Builtin/LUICanvas.py
create mode 100644 ui/Builtin/LUICheckbox.py
create mode 100644 ui/Builtin/LUIFormattedLabel.py
create mode 100644 ui/Builtin/LUIFrame.py
create mode 100644 ui/Builtin/LUIHorizontalLayout.py
create mode 100644 ui/Builtin/LUIInitialState.py
create mode 100644 ui/Builtin/LUIInputField.py
create mode 100644 ui/Builtin/LUIInputHandler.py
create mode 100644 ui/Builtin/LUILabel.py
create mode 100644 ui/Builtin/LUILayouts.py
create mode 100644 ui/Builtin/LUIObject.py
create mode 100644 ui/Builtin/LUIProgressbar.py
create mode 100644 ui/Builtin/LUIRadiobox.py
create mode 100644 ui/Builtin/LUIRadioboxGroup.py
create mode 100644 ui/Builtin/LUIRegion.py
create mode 100644 ui/Builtin/LUIRoot.py
create mode 100644 ui/Builtin/LUIScrollableRegion.py
create mode 100644 ui/Builtin/LUISelectbox.py
create mode 100644 ui/Builtin/LUISkin.py
create mode 100644 ui/Builtin/LUISlider.py
create mode 100644 ui/Builtin/LUISprite.py
create mode 100644 ui/Builtin/LUISpriteButton.py
create mode 100644 ui/Builtin/LUITabbedFrame.py
create mode 100644 ui/Builtin/LUIVerticalLayout.py
create mode 100644 ui/Builtin/RectTransform.py
create mode 100644 ui/Builtin/__init__.py
create mode 100644 ui/Skins/Default/GenerateAtlas.bat
create mode 100644 ui/Skins/Default/__init__.py
create mode 100644 ui/Skins/Default/font/SourceSansPro-Black.ttf
create mode 100644 ui/Skins/Default/font/SourceSansPro-BlackIt.ttf
create mode 100644 ui/Skins/Default/font/SourceSansPro-Bold.ttf
create mode 100644 ui/Skins/Default/font/SourceSansPro-BoldIt.ttf
create mode 100644 ui/Skins/Default/font/SourceSansPro-ExtraLight.ttf
create mode 100644 ui/Skins/Default/font/SourceSansPro-ExtraLightIt.ttf
create mode 100644 ui/Skins/Default/font/SourceSansPro-It.ttf
create mode 100644 ui/Skins/Default/font/SourceSansPro-Light.ttf
create mode 100644 ui/Skins/Default/font/SourceSansPro-LightIt.ttf
create mode 100644 ui/Skins/Default/font/SourceSansPro-Regular.ttf
create mode 100644 ui/Skins/Default/font/SourceSansPro-Semibold.ttf
create mode 100644 ui/Skins/Default/font/SourceSansPro-SemiboldIt.ttf
create mode 100644 ui/Skins/Default/res/ButtonDefault.png
create mode 100644 ui/Skins/Default/res/ButtonDefaultFocus.png
create mode 100644 ui/Skins/Default/res/ButtonDefaultFocus_Left.png
create mode 100644 ui/Skins/Default/res/ButtonDefaultFocus_Right.png
create mode 100644 ui/Skins/Default/res/ButtonDefault_Left.png
create mode 100644 ui/Skins/Default/res/ButtonDefault_Right.png
create mode 100644 ui/Skins/Default/res/ButtonGreen.png
create mode 100644 ui/Skins/Default/res/ButtonGreenFocus.png
create mode 100644 ui/Skins/Default/res/ButtonGreenFocus_Left.png
create mode 100644 ui/Skins/Default/res/ButtonGreenFocus_Right.png
create mode 100644 ui/Skins/Default/res/ButtonGreen_Left.png
create mode 100644 ui/Skins/Default/res/ButtonGreen_Right.png
create mode 100644 ui/Skins/Default/res/Checkbox_Checked.png
create mode 100644 ui/Skins/Default/res/Checkbox_CheckedHover.png
create mode 100644 ui/Skins/Default/res/Checkbox_Default.png
create mode 100644 ui/Skins/Default/res/Checkbox_DefaultHover.png
create mode 100644 ui/Skins/Default/res/ColorpickerActiveColorOverlay.png
create mode 100644 ui/Skins/Default/res/ColorpickerFieldHandle.png
create mode 100644 ui/Skins/Default/res/ColorpickerFieldOverlay.png
create mode 100644 ui/Skins/Default/res/ColorpickerHueHandle.png
create mode 100644 ui/Skins/Default/res/ColorpickerHueSlider.png
create mode 100644 ui/Skins/Default/res/ColorpickerPreviewBg.png
create mode 100644 ui/Skins/Default/res/ColorpickerPreviewOverlay.png
create mode 100644 ui/Skins/Default/res/Frame_BL.png
create mode 100644 ui/Skins/Default/res/Frame_BR.png
create mode 100644 ui/Skins/Default/res/Frame_Bottom.png
create mode 100644 ui/Skins/Default/res/Frame_Left.png
create mode 100644 ui/Skins/Default/res/Frame_Mid.png
create mode 100644 ui/Skins/Default/res/Frame_Right.png
create mode 100644 ui/Skins/Default/res/Frame_TL.png
create mode 100644 ui/Skins/Default/res/Frame_TR.png
create mode 100644 ui/Skins/Default/res/Frame_Top.png
create mode 100644 ui/Skins/Default/res/HorizontalListDivider.png
create mode 100644 ui/Skins/Default/res/InputField.png
create mode 100644 ui/Skins/Default/res/InputField_Left.png
create mode 100644 ui/Skins/Default/res/InputField_Right.png
create mode 100644 ui/Skins/Default/res/Keymarker.png
create mode 100644 ui/Skins/Default/res/Keymarker_Left.png
create mode 100644 ui/Skins/Default/res/Keymarker_Right.png
create mode 100644 ui/Skins/Default/res/ListDivider.png
create mode 100644 ui/Skins/Default/res/Popup_BL.png
create mode 100644 ui/Skins/Default/res/Popup_BR.png
create mode 100644 ui/Skins/Default/res/Popup_Bottom.png
create mode 100644 ui/Skins/Default/res/Popup_Left.png
create mode 100644 ui/Skins/Default/res/Popup_Mid.png
create mode 100644 ui/Skins/Default/res/Popup_Right.png
create mode 100644 ui/Skins/Default/res/Popup_TL.png
create mode 100644 ui/Skins/Default/res/Popup_TR.png
create mode 100644 ui/Skins/Default/res/Popup_Top.png
create mode 100644 ui/Skins/Default/res/ProgressbarBg.png
create mode 100644 ui/Skins/Default/res/ProgressbarBg_Left.png
create mode 100644 ui/Skins/Default/res/ProgressbarBg_Right.png
create mode 100644 ui/Skins/Default/res/ProgressbarFg.png
create mode 100644 ui/Skins/Default/res/ProgressbarFg_Finish.png
create mode 100644 ui/Skins/Default/res/ProgressbarFg_Left.png
create mode 100644 ui/Skins/Default/res/ProgressbarFg_Right.png
create mode 100644 ui/Skins/Default/res/Radiobox_Active.png
create mode 100644 ui/Skins/Default/res/Radiobox_ActiveHover.png
create mode 100644 ui/Skins/Default/res/Radiobox_Default.png
create mode 100644 ui/Skins/Default/res/Radiobox_DefaultHover.png
create mode 100644 ui/Skins/Default/res/ScrollShadowBottom.png
create mode 100644 ui/Skins/Default/res/ScrollShadowBottom_Left.png
create mode 100644 ui/Skins/Default/res/ScrollShadowBottom_Right.png
create mode 100644 ui/Skins/Default/res/ScrollShadowTop.png
create mode 100644 ui/Skins/Default/res/ScrollShadowTop_Left.png
create mode 100644 ui/Skins/Default/res/ScrollShadowTop_Right.png
create mode 100644 ui/Skins/Default/res/ScrollbarHandle.png
create mode 100644 ui/Skins/Default/res/ScrollbarHandle_Bottom.png
create mode 100644 ui/Skins/Default/res/ScrollbarHandle_Top.png
create mode 100644 ui/Skins/Default/res/Selectbox.png
create mode 100644 ui/Skins/Default/res/SelectboxActive.png
create mode 100644 ui/Skins/Default/res/SelectboxActive_Left.png
create mode 100644 ui/Skins/Default/res/SelectboxOpen_Right.png
create mode 100644 ui/Skins/Default/res/Selectbox_Left.png
create mode 100644 ui/Skins/Default/res/Selectbox_Right.png
create mode 100644 ui/Skins/Default/res/SelectdropDivider.png
create mode 100644 ui/Skins/Default/res/Selectdrop_BL.png
create mode 100644 ui/Skins/Default/res/Selectdrop_BR.png
create mode 100644 ui/Skins/Default/res/Selectdrop_Bottom.png
create mode 100644 ui/Skins/Default/res/Selectdrop_Left.png
create mode 100644 ui/Skins/Default/res/Selectdrop_Mid.png
create mode 100644 ui/Skins/Default/res/Selectdrop_Right.png
create mode 100644 ui/Skins/Default/res/Selectdrop_TL.png
create mode 100644 ui/Skins/Default/res/Selectdrop_TR.png
create mode 100644 ui/Skins/Default/res/Selectdrop_Top.png
create mode 100644 ui/Skins/Default/res/SliderBg.png
create mode 100644 ui/Skins/Default/res/SliderBgFill.png
create mode 100644 ui/Skins/Default/res/SliderBgFill_Left.png
create mode 100644 ui/Skins/Default/res/SliderBg_Left.png
create mode 100644 ui/Skins/Default/res/SliderBg_Right.png
create mode 100644 ui/Skins/Default/res/SliderKnob.png
create mode 100644 ui/Skins/Default/res/SunkenFrame_BL.png
create mode 100644 ui/Skins/Default/res/SunkenFrame_BR.png
create mode 100644 ui/Skins/Default/res/SunkenFrame_Bottom.png
create mode 100644 ui/Skins/Default/res/SunkenFrame_Left.png
create mode 100644 ui/Skins/Default/res/SunkenFrame_Mid.png
create mode 100644 ui/Skins/Default/res/SunkenFrame_Right.png
create mode 100644 ui/Skins/Default/res/SunkenFrame_TL.png
create mode 100644 ui/Skins/Default/res/SunkenFrame_TR.png
create mode 100644 ui/Skins/Default/res/SunkenFrame_Top.png
create mode 100644 ui/Skins/Default/res/atlas.png
create mode 100644 ui/Skins/Default/res/atlas.txt
create mode 100644 ui/Skins/Default/res/blank.png
create mode 100644 ui/Skins/Metro/GenerateAtlas.bat
create mode 100644 ui/Skins/Metro/LUIMetroSkin.py
create mode 100644 ui/Skins/Metro/__init__.py
create mode 100644 ui/Skins/Metro/border.png
create mode 100644 ui/Skins/Metro/copy_frames.py
create mode 100644 ui/Skins/Metro/flat.png
create mode 100644 ui/Skins/Metro/font/Roboto-Bold.ttf
create mode 100644 ui/Skins/Metro/font/Roboto-LICENSE.txt
create mode 100644 ui/Skins/Metro/font/Roboto-Light.ttf
create mode 100644 ui/Skins/Metro/font/Roboto-Medium.ttf
create mode 100644 ui/Skins/Metro/font/Roboto-Thin.ttf
create mode 100644 ui/Skins/Metro/res/ButtonDefault.png
create mode 100644 ui/Skins/Metro/res/ButtonDefaultFocus.png
create mode 100644 ui/Skins/Metro/res/ButtonDefaultFocus_Left.png
create mode 100644 ui/Skins/Metro/res/ButtonDefaultFocus_Right.png
create mode 100644 ui/Skins/Metro/res/ButtonDefault_Left.png
create mode 100644 ui/Skins/Metro/res/ButtonDefault_Right.png
create mode 100644 ui/Skins/Metro/res/ButtonGreen.png
create mode 100644 ui/Skins/Metro/res/ButtonGreenFocus.png
create mode 100644 ui/Skins/Metro/res/ButtonGreenFocus_Left.png
create mode 100644 ui/Skins/Metro/res/ButtonGreenFocus_Right.png
create mode 100644 ui/Skins/Metro/res/ButtonGreen_Left.png
create mode 100644 ui/Skins/Metro/res/ButtonGreen_Right.png
create mode 100644 ui/Skins/Metro/res/Checkbox_Checked.png
create mode 100644 ui/Skins/Metro/res/Checkbox_CheckedHover.png
create mode 100644 ui/Skins/Metro/res/Checkbox_Default.png
create mode 100644 ui/Skins/Metro/res/Checkbox_DefaultHover.png
create mode 100644 ui/Skins/Metro/res/ColorpickerActiveColorOverlay.png
create mode 100644 ui/Skins/Metro/res/ColorpickerFieldHandle.png
create mode 100644 ui/Skins/Metro/res/ColorpickerFieldOverlay.png
create mode 100644 ui/Skins/Metro/res/ColorpickerHueHandle.png
create mode 100644 ui/Skins/Metro/res/ColorpickerHueSlider.png
create mode 100644 ui/Skins/Metro/res/ColorpickerPreviewBg.png
create mode 100644 ui/Skins/Metro/res/ColorpickerPreviewOverlay.png
create mode 100644 ui/Skins/Metro/res/Draft3.psd
create mode 100644 ui/Skins/Metro/res/Frame_BL.png
create mode 100644 ui/Skins/Metro/res/Frame_BR.png
create mode 100644 ui/Skins/Metro/res/Frame_Bottom.png
create mode 100644 ui/Skins/Metro/res/Frame_Left.png
create mode 100644 ui/Skins/Metro/res/Frame_Mid.png
create mode 100644 ui/Skins/Metro/res/Frame_Right.png
create mode 100644 ui/Skins/Metro/res/Frame_TL.png
create mode 100644 ui/Skins/Metro/res/Frame_TR.png
create mode 100644 ui/Skins/Metro/res/Frame_Top.png
create mode 100644 ui/Skins/Metro/res/HorizontalListDivider.png
create mode 100644 ui/Skins/Metro/res/InputField.png
create mode 100644 ui/Skins/Metro/res/InputField_Left.png
create mode 100644 ui/Skins/Metro/res/InputField_Right.png
create mode 100644 ui/Skins/Metro/res/Keymarker.png
create mode 100644 ui/Skins/Metro/res/Keymarker_Left.png
create mode 100644 ui/Skins/Metro/res/Keymarker_Right.png
create mode 100644 ui/Skins/Metro/res/ListDivider.png
create mode 100644 ui/Skins/Metro/res/Popup_BL.png
create mode 100644 ui/Skins/Metro/res/Popup_BR.png
create mode 100644 ui/Skins/Metro/res/Popup_Bottom.png
create mode 100644 ui/Skins/Metro/res/Popup_Left.png
create mode 100644 ui/Skins/Metro/res/Popup_Mid.png
create mode 100644 ui/Skins/Metro/res/Popup_Right.png
create mode 100644 ui/Skins/Metro/res/Popup_TL.png
create mode 100644 ui/Skins/Metro/res/Popup_TR.png
create mode 100644 ui/Skins/Metro/res/Popup_Top.png
create mode 100644 ui/Skins/Metro/res/ProgressbarBg.png
create mode 100644 ui/Skins/Metro/res/ProgressbarBg_Left.png
create mode 100644 ui/Skins/Metro/res/ProgressbarBg_Right.png
create mode 100644 ui/Skins/Metro/res/ProgressbarFg.png
create mode 100644 ui/Skins/Metro/res/ProgressbarFg_Finish.png
create mode 100644 ui/Skins/Metro/res/ProgressbarFg_Left.png
create mode 100644 ui/Skins/Metro/res/ProgressbarFg_Right.png
create mode 100644 ui/Skins/Metro/res/Radiobox_Active.png
create mode 100644 ui/Skins/Metro/res/Radiobox_ActiveHover.png
create mode 100644 ui/Skins/Metro/res/Radiobox_Default.png
create mode 100644 ui/Skins/Metro/res/Radiobox_DefaultHover.png
create mode 100644 ui/Skins/Metro/res/ScrollShadowBottom.png
create mode 100644 ui/Skins/Metro/res/ScrollShadowBottom_Left.png
create mode 100644 ui/Skins/Metro/res/ScrollShadowBottom_Right.png
create mode 100644 ui/Skins/Metro/res/ScrollShadowTop.png
create mode 100644 ui/Skins/Metro/res/ScrollShadowTop_Left.png
create mode 100644 ui/Skins/Metro/res/ScrollShadowTop_Right.png
create mode 100644 ui/Skins/Metro/res/ScrollbarHandle.png
create mode 100644 ui/Skins/Metro/res/ScrollbarHandle_Bottom.png
create mode 100644 ui/Skins/Metro/res/ScrollbarHandle_Top.png
create mode 100644 ui/Skins/Metro/res/Selectbox.png
create mode 100644 ui/Skins/Metro/res/SelectboxActive.png
create mode 100644 ui/Skins/Metro/res/SelectboxActive_Left.png
create mode 100644 ui/Skins/Metro/res/SelectboxOpen_Right.png
create mode 100644 ui/Skins/Metro/res/Selectbox_Left.png
create mode 100644 ui/Skins/Metro/res/Selectbox_Right.png
create mode 100644 ui/Skins/Metro/res/SelectdropDivider.png
create mode 100644 ui/Skins/Metro/res/Selectdrop_BL.png
create mode 100644 ui/Skins/Metro/res/Selectdrop_BR.png
create mode 100644 ui/Skins/Metro/res/Selectdrop_Bottom.png
create mode 100644 ui/Skins/Metro/res/Selectdrop_Left.png
create mode 100644 ui/Skins/Metro/res/Selectdrop_Mid.png
create mode 100644 ui/Skins/Metro/res/Selectdrop_Right.png
create mode 100644 ui/Skins/Metro/res/Selectdrop_TL.png
create mode 100644 ui/Skins/Metro/res/Selectdrop_TR.png
create mode 100644 ui/Skins/Metro/res/Selectdrop_Top.png
create mode 100644 ui/Skins/Metro/res/SliderBg.png
create mode 100644 ui/Skins/Metro/res/SliderBgFill.png
create mode 100644 ui/Skins/Metro/res/SliderBgFill_Left.png
create mode 100644 ui/Skins/Metro/res/SliderBg_Left.png
create mode 100644 ui/Skins/Metro/res/SliderBg_Right.png
create mode 100644 ui/Skins/Metro/res/SliderKnob.png
create mode 100644 ui/Skins/Metro/res/SunkenFrame_BL.png
create mode 100644 ui/Skins/Metro/res/SunkenFrame_BR.png
create mode 100644 ui/Skins/Metro/res/SunkenFrame_Bottom.png
create mode 100644 ui/Skins/Metro/res/SunkenFrame_Left.png
create mode 100644 ui/Skins/Metro/res/SunkenFrame_Mid.png
create mode 100644 ui/Skins/Metro/res/SunkenFrame_Right.png
create mode 100644 ui/Skins/Metro/res/SunkenFrame_TL.png
create mode 100644 ui/Skins/Metro/res/SunkenFrame_TR.png
create mode 100644 ui/Skins/Metro/res/SunkenFrame_Top.png
create mode 100644 ui/Skins/Metro/res/atlas.png
create mode 100644 ui/Skins/Metro/res/atlas.txt
create mode 100644 ui/Skins/Metro/res/blank.png
create mode 100644 ui/Skins/__init__.py
create mode 100644 ui/lui.pyd
create mode 100644 ui/lui_function.py
create mode 100644 ui/lui_manager.py
create mode 100644 ui/panels/__init__.py
create mode 100644 ui/panels/animation_tools.py
create mode 100644 ui/panels/app_actions.py
create mode 100644 ui/panels/create_actions.py
create mode 100644 ui/panels/dialog_panels.py
create mode 100644 ui/panels/editor_panels.py
create mode 100644 ui/panels/interaction_panels.py
create mode 100644 ui/panels/object_factory.py
create mode 100644 ui/panels/property_helpers.py
create mode 100644 ui/panels/script_panels.py
diff --git a/.idea/misc.xml b/.idea/misc.xml
index 505c56b5..a86c38d2 100644
--- a/.idea/misc.xml
+++ b/.idea/misc.xml
@@ -3,5 +3,5 @@
-
+
\ No newline at end of file
diff --git a/RenderPipelineFile/config/plugins.yaml b/RenderPipelineFile/config/plugins.yaml
index 4b0f5d64..42c03f4c 100644
--- a/RenderPipelineFile/config/plugins.yaml
+++ b/RenderPipelineFile/config/plugins.yaml
@@ -9,14 +9,14 @@ enabled:
- color_correction
- env_probes
- forward_shading
- - motion_blur
+ # - motion_blur # disabled for editor performance
- pssm
- scattering
- skin_shading
- sky_ao
- smaa
- - ssr
- - clouds
+ # - ssr # disabled for editor performance
+ #- clouds
#- dof
#- fxaa
#- volumetrics
@@ -120,7 +120,7 @@ overrides:
logarithmic_factor: 3.0
sun_distance: 100.0
split_count: 4
- resolution: 1024
+ resolution: 512
border_bias: 0.058
use_pcf: True
filter_sequence: halton_2D_32
@@ -133,7 +133,7 @@ overrides:
pcss_penumbra_size: 2.38
pcss_min_penumbra_size: 7.0
use_distant_shadows: True
- dist_shadow_resolution: 4096
+ dist_shadow_resolution: 1024
dist_shadow_clipsize: 400.0
dist_shadow_sundist: 300.0
scene_shadow_resolution: 512
@@ -157,7 +157,7 @@ overrides:
sky_ao:
sample_radius: 17.17
max_radius: 500.0
- resolution: 1024
+ resolution: 512
sample_sequence: poisson_2D_32
ao_multiplier: 0.83
ao_bias: 0.0
diff --git a/core/CustomMouseController.py b/core/CustomMouseController.py
index ac4132e9..b03da669 100644
--- a/core/CustomMouseController.py
+++ b/core/CustomMouseController.py
@@ -1,4 +1,5 @@
from direct.task.TaskManagerGlobal import taskMgr
+from panda3d.core import KeyboardButton
class CustomMouseController:
@@ -23,18 +24,30 @@ class CustomMouseController:
self.showbase.accept("mouse3-up", self.setKey, ["mouse3", False]) # 右键释放
self.showbase.accept("w", self.setKey, ["cam-forward", True])
+ self.showbase.accept("W", self.setKey, ["cam-forward", True])
self.showbase.accept("a", self.setKey, ["cam-left", True])
+ self.showbase.accept("A", self.setKey, ["cam-left", True])
self.showbase.accept("s", self.setKey, ["cam-backward", True])
+ self.showbase.accept("S", self.setKey, ["cam-backward", True])
self.showbase.accept("d", self.setKey, ["cam-right", True])
+ self.showbase.accept("D", self.setKey, ["cam-right", True])
self.showbase.accept("e", self.setKey, ["cam-up", True])
+ self.showbase.accept("E", self.setKey, ["cam-up", True])
self.showbase.accept("q", self.setKey, ["cam-down", True])
+ self.showbase.accept("Q", self.setKey, ["cam-down", True])
self.showbase.accept("w-up", self.setKey, ["cam-forward", False])
+ self.showbase.accept("W-up", self.setKey, ["cam-forward", False])
self.showbase.accept("a-up", self.setKey, ["cam-left", False])
+ self.showbase.accept("A-up", self.setKey, ["cam-left", False])
self.showbase.accept("s-up", self.setKey, ["cam-backward", False])
+ self.showbase.accept("S-up", self.setKey, ["cam-backward", False])
self.showbase.accept("d-up", self.setKey, ["cam-right", False])
+ self.showbase.accept("D-up", self.setKey, ["cam-right", False])
self.showbase.accept("e-up", self.setKey, ["cam-up", False])
+ self.showbase.accept("E-up", self.setKey, ["cam-up", False])
self.showbase.accept("q-up", self.setKey, ["cam-down", False])
+ self.showbase.accept("Q-up", self.setKey, ["cam-down", False])
self.last_mouse_x = 0
self.last_mouse_y = 0
@@ -61,17 +74,25 @@ class CustomMouseController:
# 检查ImGui是否捕获了键盘输入
if self._should_handle_keyboard():
- if self.keyMap["cam-left"]:
+ watcher = self.showbase.mouseWatcherNode
+ left_down = self.keyMap["cam-left"] or watcher.is_button_down(KeyboardButton.ascii_key("a"))
+ right_down = self.keyMap["cam-right"] or watcher.is_button_down(KeyboardButton.ascii_key("d"))
+ backward_down = self.keyMap["cam-backward"] or watcher.is_button_down(KeyboardButton.ascii_key("s"))
+ forward_down = self.keyMap["cam-forward"] or watcher.is_button_down(KeyboardButton.ascii_key("w"))
+ up_down = self.keyMap["cam-up"] or watcher.is_button_down(KeyboardButton.ascii_key("e"))
+ down_down = self.keyMap["cam-down"] or watcher.is_button_down(KeyboardButton.ascii_key("q"))
+
+ if left_down:
self.showbase.camera.setX(self.showbase.camera, -self.move_speed * dt)
- if self.keyMap["cam-right"]:
+ if right_down:
self.showbase.camera.setX(self.showbase.camera, +self.move_speed * dt)
- if self.keyMap["cam-backward"]:
+ if backward_down:
self.showbase.camera.setY(self.showbase.camera, -self.move_speed * dt)
- if self.keyMap["cam-forward"]:
+ if forward_down:
self.showbase.camera.setY(self.showbase.camera, +self.move_speed * dt)
- if self.keyMap["cam-up"]:
+ if up_down:
self.showbase.camera.setZ(self.showbase.camera, +self.move_speed * dt)
- if self.keyMap["cam-down"]:
+ if down_down:
self.showbase.camera.setZ(self.showbase.camera, -self.move_speed * dt)
if self.keyMap["mouse3"]: # 只使用右键控制视角旋转
try:
diff --git a/core/event_handler.py b/core/event_handler.py
index a2425c97..9d4fdc64 100644
--- a/core/event_handler.py
+++ b/core/event_handler.py
@@ -254,7 +254,7 @@ class EventHandler:
return
# 根据当前工具处理点击事件
- if self.world.currentTool == "选择":
+ if self.world.currentTool in ("选择", "移动", "旋转", "缩放"):
print("✓ 使用选择工具处理点击")
try:
self._handleSelectionClick(hitNode)
@@ -589,4 +589,4 @@ class EventHandler:
self.world.selection.updateGizmoHighlight(x, y)
# 调用CoreWorld的父类方法处理基础的相机旋转
- super(type(self.world), self.world).mouseMoveEvent(evt)
\ No newline at end of file
+ super(type(self.world), self.world).mouseMoveEvent(evt)
diff --git a/core/imgui_style_manager.py b/core/imgui_style_manager.py
index e9aee657..39e3c04b 100644
--- a/core/imgui_style_manager.py
+++ b/core/imgui_style_manager.py
@@ -402,7 +402,10 @@ class ImGuiStyleManager:
if icon_path.exists():
# 使用base.imgui.loadTexture方法
if hasattr(base, 'imgui') and hasattr(base.imgui, 'loadTexture'):
- return base.imgui.loadTexture(str(icon_path))
+ # 转换路径为Panda3D兼容格式 (Windows下: D:\... -> /d/...)
+ # 注意: p3dimgui.loadTexture 仅支持 str 或 Texture,不支持 Filename 对象
+ fn = Filename.fromOsSpecific(str(icon_path))
+ return base.imgui.loadTexture(fn.getFullpath())
else:
print(f"⚠ ImGui后端未初始化")
return None
diff --git a/core/selection.py b/core/selection.py
index 53ccbb7b..a8565272 100644
--- a/core/selection.py
+++ b/core/selection.py
@@ -118,6 +118,30 @@ class SelectionSystem:
def _resetCursor(self):
self._setCursor("default")
+ def _has_new_transform_gizmo(self):
+ tg = getattr(self.world, "newTransform", None)
+ return tg is not None
+
+ def sync_transform_gizmo_mode(self):
+ """Sync TransformGizmo mode with current tool."""
+ if not self._has_new_transform_gizmo():
+ return
+ try:
+ from TransformGizmo.events import TransformGizmoMode
+ tool_mgr = getattr(self.world, "tool_manager", None)
+ if not tool_mgr:
+ return
+ if tool_mgr.isRotateTool():
+ self.world.newTransform.set_mode(TransformGizmoMode.ROTATE)
+ elif tool_mgr.isScaleTool():
+ self.world.newTransform.set_mode(TransformGizmoMode.SCALE)
+ elif tool_mgr.isMoveTool() or tool_mgr.isSelectionTool():
+ self.world.newTransform.set_mode(TransformGizmoMode.MOVE)
+ else:
+ self.world.newTransform.set_mode(TransformGizmoMode.NONE)
+ except Exception as e:
+ print(f"sync transform gizmo mode failed: {e}")
+
# ==================== 选择框系统 ====================
def createSelectionBox(self, nodePath):
@@ -135,8 +159,8 @@ class SelectionSystem:
self.selectionBox = self.world.render.attachNewNode("selectionBox")
self.selectionBoxTarget = nodePath
- taskMgr.add(self.updateSelectionBoxTask, "updateSelectionBox")
- self.updateSelectionBoxGeometry()
+ #taskMgr.add(self.updateSelectionBoxTask, "updateSelectionBox")
+ #self.updateSelectionBoxGeometry()
except Exception as e:
print(f" ✗ 创建选择框失败: {str(e)}")
@@ -340,6 +364,17 @@ class SelectionSystem:
def createGizmo(self, nodePath):
"""为选中的节点创建坐标轴工具 - 保留箭头版本"""
+ if self._has_new_transform_gizmo():
+ self.gizmo = None
+ self.gizmoTarget = nodePath
+ if not nodePath:
+ return
+ self.sync_transform_gizmo_mode()
+ try:
+ self.world.newTransform.attach(nodePath)
+ except Exception as e:
+ print(f"attach TransformGizmo failed: {e}")
+ return
try:
#print(f" 开始创建坐标轴,目标节点: {nodePath.getName()}")
@@ -818,15 +853,31 @@ class SelectionSystem:
pass
def clearGizmo(self):
- """清除坐标轴"""
+ """Clear transform gizmo."""
+ if self._has_new_transform_gizmo():
+ try:
+ self.world.newTransform.detach()
+ except Exception as e:
+ print(f"detach TransformGizmo failed: {e}")
+ self.gizmo = None
+ self.gizmoTarget = None
+ self.gizmoXAxis = None
+ self.gizmoYAxis = None
+ self.gizmoZAxis = None
+ self.isDraggingGizmo = False
+ self.dragGizmoAxis = None
+ self.dragStartMousePos = None
+ self.gizmoTargetStartPos = None
+ self.gizmoStartPos = None
+ self._resetCursor()
+ return
+
if self.gizmo:
self.gizmo.removeNode()
self.gizmo = None
- # 停止坐标轴更新任务
taskMgr.remove("updateGizmo")
- # 清除坐标轴相关引用
self.gizmoTarget = None
self.gizmoXAxis = None
self.gizmoYAxis = None
@@ -838,39 +889,8 @@ class SelectionSystem:
self.gizmoStartPos = None
self._resetCursor()
-
# def setGizmoAxisColor(self, axis, color):
# """设置坐标轴颜色 - RenderPipeline 兼容版本"""
- # try:
- # from panda3d.core import AntialiasAttrib,TransparencyAttrib
- #
- # axis_nodes = {
- # "x": self.gizmoXAxis,
- # "y": self.gizmoYAxis,
- # "z": self.gizmoZAxis
- # }
- #
- # if axis in axis_nodes and axis_nodes[axis]:
- # axis_node = axis_nodes[axis]
- #
- # axis_node.setColor(color[0]*20.0,color[1]*20.0,color[2]*20.0,color[3])
- # axis_node.setColorScale(color[0]*10.0,color[1]*10.0,color[2]*10.0,color[3])
- # axis_node.setShaderOff(10000)
- # axis_node.setLightOff()
- # axis_node.setMaterialOff()
- # axis_node.setTextureOff()
- # axis_node.setFogOff()
- #
- # except Exception as e:
- # print(f"设置坐标轴颜色失败: {str(e)}")
- # # 回退到简单的颜色设置
- # try:
- # if axis in axis_nodes and axis_nodes[axis]:
- # axis_nodes[axis].setColor(*color)
- # except:
- # pass
-
-
def setGizmoRotAxisColor(self, axis, color):
"""使用材质设置坐标轴颜色 - RenderPipeline兼容版本"""
try:
diff --git a/core/tool_manager.py b/core/tool_manager.py
index 87ffe17c..d86f4773 100644
--- a/core/tool_manager.py
+++ b/core/tool_manager.py
@@ -37,6 +37,12 @@ class ToolManager:
# 坐标轴现在始终跟随选中状态,不再依赖工具类型
+ try:
+ if hasattr(self.world, "selection") and self.world.selection:
+ self.world.selection.sync_transform_gizmo_mode()
+ except Exception as e:
+ print(f"sync gizmo mode on tool change failed: {e}")
+
def getCurrentTool(self):
"""获取当前工具"""
return self.currentTool
diff --git a/core/world.py b/core/world.py
index 77077142..2daf6cfc 100644
--- a/core/world.py
+++ b/core/world.py
@@ -20,6 +20,7 @@ render_pipeline_path = os.path.join(project_root, "RenderPipelineFile")
sys.path.insert(0, render_pipeline_path)
from RenderPipelineFile.rpcore import RenderPipeline
+from ssbo_component.ssbo_editor import SSBOEditor
# 从渲染管线工具模块导入全局函数
from core.render_pipeline_utils import get_render_pipeline, set_render_pipeline
@@ -47,12 +48,19 @@ class CoreWorld(ShowBase):
loadPrcFileData("", f"win-size {width} {height}")
loadPrcFileData("", "win-fixed-size #f") # 允许窗口调整大小
+ # Performance preset to match the standalone smooth runner
+ loadPrcFileData("", "threading-model /Draw")
+ loadPrcFileData("", "pstats-gpu-timing #f")
+ loadPrcFileData("", "gl-debug #f")
+ loadPrcFileData("", "gl-debug-object-labels #f")
+
# VR性能优化配置
loadPrcFileData("", "prefer-single-buffer true")
loadPrcFileData("", "gl-force-flush false")
loadPrcFileData("", "sync-video false")
loadPrcFileData("", "support-stencil false")
loadPrcFileData("", "clock-mode non-real-time")
+ loadPrcFileData("", "support-threads false")
if is_fullscreen:
loadPrcFileData("", "fullscreen #t")
@@ -60,6 +68,9 @@ class CoreWorld(ShowBase):
# 创建渲染管线
self.render_pipeline = RenderPipeline()
self.render_pipeline.pre_showbase_init()
+
+ # 强制开启多线程支持 (Video播放必需)
+ loadPrcFileData("force_threads", "support-threads #f")
# 初始化 ShowBase
ShowBase.__init__(self)
diff --git a/imgui.ini b/imgui.ini
index 1c34eff5..6556eb15 100644
--- a/imgui.ini
+++ b/imgui.ini
@@ -1,5 +1,5 @@
[Window][Debug##Default]
-Pos=28,731
+Pos=28,465
Size=400,400
Collapsed=0
@@ -24,34 +24,34 @@ Size=832,45
Collapsed=0
[Window][工具栏]
-Pos=287,20
-Size=1234,32
+Pos=325,20
+Size=1228,32
Collapsed=0
-DockId=0x00000007,0
+DockId=0x0000000D,0
[Window][场景树]
Pos=0,20
-Size=285,776
+Size=323,634
Collapsed=0
-DockId=0x00000001,0
+DockId=0x00000007,0
[Window][属性面板]
-Pos=1523,20
-Size=327,498
+Pos=1555,20
+Size=365,989
Collapsed=0
-DockId=0x00000005,0
+DockId=0x00000003,0
[Window][控制台]
-Pos=880,798
-Size=641,218
+Pos=0,656
+Size=323,353
Collapsed=0
-DockId=0x0000000C,0
+DockId=0x00000008,0
[Window][脚本管理]
-Pos=1523,520
-Size=327,496
+Pos=1540,20
+Size=380,390
Collapsed=0
-DockId=0x00000006,0
+DockId=0x00000003,1
[Window][中文显示测试]
Pos=60,60
@@ -60,7 +60,7 @@ Collapsed=0
[Window][WindowOverViewport_11111111]
Pos=0,20
-Size=1850,996
+Size=1920,989
Collapsed=0
[Window][测试窗口1]
@@ -79,30 +79,30 @@ Size=93,65
Collapsed=0
[Window][新建项目]
-Pos=725,358
+Pos=760,354
Size=400,300
Collapsed=0
[Window][选择路径]
-Pos=625,258
+Pos=660,254
Size=600,500
Collapsed=0
[Window][打开项目]
-Pos=675,308
+Pos=710,304
Size=500,400
Collapsed=0
[Window][导入模型]
-Pos=625,258
+Pos=660,254
Size=600,500
Collapsed=0
[Window][资源管理器]
-Pos=0,798
-Size=878,218
+Pos=325,827
+Size=1228,182
Collapsed=0
-DockId=0x0000000B,0
+DockId=0x00000006,0
[Window][创建3D文本]
Pos=60,60
@@ -135,27 +135,68 @@ Size=89,250
Collapsed=0
[Window][颜色选择器]
-Pos=775,308
+Pos=810,304
Size=300,400
Collapsed=0
[Window][选择diffuse纹理文件##texture_dialog]
-Pos=625,308
+Pos=660,304
+Size=600,400
+Collapsed=0
+
+[Window][创建GUI图片]
+Pos=60,60
+Size=101,226
+Collapsed=0
+
+[Window][LUI编辑器]
+Pos=1540,412
+Size=380,597
+Collapsed=0
+DockId=0x00000004,0
+
+[Window][LUI测试控制面板]
+Pos=6,10
+Size=300,200
+Collapsed=1
+
+[Window][选择高度图文件]
+Pos=60,60
+Size=596,498
+Collapsed=0
+
+[Window][创建平面地形]
+Pos=61,60
+Size=238,176
+Collapsed=0
+
+[Window][选择normal纹理文件##texture_dialog]
+Pos=660,304
+Size=600,400
+Collapsed=0
+
+[Window][选择metallic纹理文件##texture_dialog]
+Pos=660,304
+Size=600,400
+Collapsed=0
+
+[Window][选择roughness纹理文件##texture_dialog]
+Pos=660,304
Size=600,400
Collapsed=0
[Docking][Data]
-DockSpace ID=0x08BD597D Window=0x1BBC0F80 Pos=0,20 Size=1850,996 Split=X
- DockNode ID=0x00000003 Parent=0x08BD597D SizeRef=1521,996 Split=Y
- DockNode ID=0x00000009 Parent=0x00000003 SizeRef=1380,776 Split=X
- DockNode ID=0x00000001 Parent=0x00000009 SizeRef=285,730 HiddenTabBar=1 Selected=0xE0015051
- DockNode ID=0x00000002 Parent=0x00000009 SizeRef=1234,730 Split=Y
- DockNode ID=0x00000007 Parent=0x00000002 SizeRef=1380,32 HiddenTabBar=1 Selected=0x43A39006
- DockNode ID=0x00000008 Parent=0x00000002 SizeRef=1380,742 CentralNode=1 Selected=0x5E5F7166
- DockNode ID=0x0000000A Parent=0x00000003 SizeRef=1380,218 Split=X Selected=0x5428E753
- DockNode ID=0x0000000B Parent=0x0000000A SizeRef=878,111 HiddenTabBar=1 Selected=0x3A2E05C3
- DockNode ID=0x0000000C Parent=0x0000000A SizeRef=641,111 HiddenTabBar=1 Selected=0x5428E753
- DockNode ID=0x00000004 Parent=0x08BD597D SizeRef=327,996 Split=Y Selected=0x5DB6FF37
- DockNode ID=0x00000005 Parent=0x00000004 SizeRef=304,498 HiddenTabBar=1 Selected=0x5DB6FF37
- DockNode ID=0x00000006 Parent=0x00000004 SizeRef=304,496 HiddenTabBar=1 Selected=0x3188AB8D
+DockSpace ID=0x08BD597D Window=0x1BBC0F80 Pos=0,20 Size=1920,989 Split=X
+ DockNode ID=0x00000001 Parent=0x08BD597D SizeRef=1553,989 Split=X
+ DockNode ID=0x00000009 Parent=0x00000001 SizeRef=323,989 Split=Y Selected=0xE0015051
+ DockNode ID=0x00000007 Parent=0x00000009 SizeRef=271,634 Selected=0xE0015051
+ DockNode ID=0x00000008 Parent=0x00000009 SizeRef=271,353 Selected=0x5428E753
+ DockNode ID=0x0000000A Parent=0x00000001 SizeRef=1228,989 Split=Y
+ DockNode ID=0x0000000D Parent=0x0000000A SizeRef=1318,32 HiddenTabBar=1 Selected=0x43A39006
+ DockNode ID=0x0000000E Parent=0x0000000A SizeRef=1318,955 Split=Y
+ DockNode ID=0x00000005 Parent=0x0000000E SizeRef=1341,771 CentralNode=1
+ DockNode ID=0x00000006 Parent=0x0000000E SizeRef=1341,182 Selected=0x3A2E05C3
+ DockNode ID=0x00000002 Parent=0x08BD597D SizeRef=365,989 Split=Y Selected=0x3188AB8D
+ DockNode ID=0x00000003 Parent=0x00000002 SizeRef=351,390 Selected=0x5DB6FF37
+ DockNode ID=0x00000004 Parent=0x00000002 SizeRef=351,597 Selected=0x1EB923B7
diff --git a/main.py b/main.py
index 0c9a7c26..e15559bd 100644
--- a/main.py
+++ b/main.py
@@ -34,6 +34,18 @@ from core.InfoPanelManager import InfoPanelManager
from core.collision_manager import CollisionManager
from core.CustomMouseController import CustomMouseController
from core.resource_manager import ResourceManager
+from ui.lui_manager import LUIManager
+from ui.panels.editor_panels import EditorPanels
+from ui.panels.script_panels import ScriptPanels
+from ui.panels.dialog_panels import DialogPanels
+from ui.panels.interaction_panels import InteractionPanels
+from ui.panels.create_actions import CreateActions
+from ui.panels.object_factory import ObjectFactory
+from ui.panels.animation_tools import AnimationTools
+from ui.panels.property_helpers import PropertyHelpers
+from ui.panels.app_actions import AppActions
+from ssbo_component.ssbo_editor import SSBOEditor
+from TransformGizmo.transform_gizmo import TransformGizmo
# 拖拽监控类
class DragDropMonitor:
@@ -106,7 +118,7 @@ class MyWorld(CoreWorld):
# 设置窗口为最大化模式
props = WindowProperties()
- props.set_maximized(True)
+ #props.set_maximized(True)
self.win.request_properties(props)
print("✓ 窗口已设置为最大化模式")
@@ -117,15 +129,11 @@ class MyWorld(CoreWorld):
self.accept("f", self.onFocusKeyPressed)
self.accept("F", self.onFocusKeyPressed) # 大写F
- #初始化巡检系统
- self.patrol_system = PatrolSystem(self)
- self.accept("p",self.onPatrolKeyPressed)
- self.accept("P",self.onPatrolKeyPressed)
-
- # 绑定鼠标事件用于3D场景选择
- self.accept("mouse1", self.onMouseClick)
+ self.use_ssbo_mouse_picking = True
+ if not self.use_ssbo_mouse_picking:
+ self.accept("mouse1", self.onMouseClick)
+ # Keep release/move bindings even in SSBO mode so gizmo drag can work.
self.accept("mouse1-up", self.onMouseRelease)
- # 尝试多种鼠标移动事件绑定方式
self.accept("mouse-move", self.onMouseMove)
self.accept("drag", self.onMouseMove)
@@ -140,6 +148,12 @@ class MyWorld(CoreWorld):
# 初始化GUI管理系统
self.gui_manager = GUIManager(self)
+
+ # 初始化LUI管理系统
+ self.lui_manager = LUIManager(self)
+
+ # 新的坐标系
+ self.newTransform = TransformGizmo(self)
# 初始化视频管理
if VideoManager is not None:
@@ -194,7 +208,10 @@ class MyWorld(CoreWorld):
self.debug_collision = True # 是否显示碰撞体
# 默认启用模型间碰撞检测(可选)
- self.enableModelCollisionDetection(enable=True, frequency=0.1, threshold=0.5)
+ if self.use_ssbo_mouse_picking:
+ self.enableModelCollisionDetection(enable=False, frequency=0.1, threshold=0.5)
+ else:
+ self.enableModelCollisionDetection(enable=True, frequency=0.1, threshold=0.5)
# 碰撞检测UI相关变量
self._selected_collision_shape = "球形 (Sphere)" # 默认选择的碰撞形状
@@ -209,15 +226,41 @@ class MyWorld(CoreWorld):
self.terrain_edit_operation = "add"
# Install Dear ImGui
- p3dimgui.init()
+ p3dimgui.init(
+ wantPlaceManager=False,
+ wantExplorerManager=False,
+ wantTimeSliderManager=False,
+ )
+ # Let ssbo_component reuse the existing imgui backend instance.
+ self.imgui_backend = self.imgui
+ # Initialize SSBO editor and let it own mouse1 picking.
+ # Do not auto-load a default model here; models are loaded from import flow.
+ self.ssbo_editor = None
+ if self.use_ssbo_mouse_picking:
+ self.ssbo_editor = SSBOEditor(
+ base_app=self,
+ render_pipeline=self.render_pipeline,
+ model_path=None,
+ font_path=None,
+ )
+ self.ssbo_editor.bind_transform_gizmo(self.newTransform)
+ print("SSBOEditor mouse picking enabled (waiting for imported model)")
- # 启用ImGui Docking功能
imgui.get_io().config_flags |= imgui.ConfigFlags_.docking_enable
print("✓ ImGui Docking功能已启用")
# 初始化样式管理器
from core.imgui_style_manager import ImGuiStyleManager
self.style_manager = ImGuiStyleManager(self.imgui, self)
+ self.editor_panels = EditorPanels(self)
+ self.script_panels = ScriptPanels(self)
+ self.dialog_panels = DialogPanels(self)
+ self.interaction_panels = InteractionPanels(self)
+ self.create_actions = CreateActions(self)
+ self.object_factory = ObjectFactory(self)
+ self.animation_tools = AnimationTools(self)
+ self.property_helpers = PropertyHelpers(self)
+ self.app_actions = AppActions(self)
# 简化的初始化字体设置(只使用中文字体)
try:
@@ -226,8 +269,6 @@ class MyWorld(CoreWorld):
# 尝试加载中文字体
import platform
- from pathlib import Path
-
system = platform.system().lower()
if system == "linux":
font_path = "/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc"
@@ -269,7 +310,7 @@ class MyWorld(CoreWorld):
self.showSceneTree = True
self.showPropertyPanel = True
self.showConsole = True
- self.showScriptPanel = True
+ self.showScriptPanel = not self.use_ssbo_mouse_picking
self.showToolbar = True
self.showResourceManager = True
@@ -342,6 +383,7 @@ class MyWorld(CoreWorld):
self.is_dragging = False
self.show_drag_overlay = False
self.drag_drop_monitor = None
+ self.showLUIEditor = not self.use_ssbo_mouse_picking
# 导入功能状态
self.show_import_dialog = False
@@ -410,6 +452,7 @@ class MyWorld(CoreWorld):
self.accept('control-c', self._on_copy)
self.accept('control-v', self._on_paste)
self.accept('delete', self._on_delete_pressed)
+ self.accept('escape', self._on_escape_pressed)
# 滚轮事件
self.accept('wheel_up', self._on_wheel_up)
@@ -598,38 +641,6 @@ class MyWorld(CoreWorld):
except Exception as e:
print(f"处理鼠标移动事件失败: {e}")
- def onPatrolKeyPressed(self):
- """处理 P 键按下事件 - 控制巡检系统"""
- try:
- print("检测到 P 键按下")
- if not self.patrol_system.is_patrolling:
- if not self.patrol_system.patrol_points:
- self.createDefaultPatrolRoute()
- if self.patrol_system.start_patrol():
- print("✓ 巡检已开始")
- else:
- print("✗ 巡检启动失败")
- else:
- if self.patrol_system.stop_patrol():
- print("✓ 巡检已停止")
- else:
- print("✗ 巡检停止失败")
- except Exception as e:
- print(f"处理 P 键事件失败: {e}")
-
- def createDefaultPatrolRoute(self):
- """创建默认巡检路线"""
- try:
- self.patrol_system.clear_patrol_points()
- self.patrol_system.add_patrol_point((0, -10, 2), (0,0,0), 1.5)
- self.patrol_system.add_patrol_point((10, -10, 2), (0,0,0), 1.5)
- self.patrol_system.add_patrol_point((10, 5, 2), (0,0,0), 1.5)
- self.patrol_system.add_patrol_point((10, 0, 5), (0,0,0), 1.5)
- self.patrol_system.add_patrol_point((0, -10, 2), None, 2.5)
- print("✓ 默认自动朝向巡检路线已创建")
- self.patrol_system.list_patrol_points()
- except Exception as e:
- print(f"创建默认自动朝向巡检路线失败: {e}")
def enableModelCollisionDetection(self, enable=True, frequency=0.1, threshold=0.5):
"""启用模型间碰撞检测"""
@@ -766,6 +777,10 @@ class MyWorld(CoreWorld):
# 绘制拖拽界面
self._draw_drag_drop_interface()
+ # 绘制LUI编辑器
+ if self.showLUIEditor and hasattr(self, 'lui_manager'):
+ self.lui_manager.draw_editor()
+
# 更新变换监控
dt = imgui.get_io().delta_time
self.update_transform_monitoring(dt)
@@ -802,1126 +817,65 @@ class MyWorld(CoreWorld):
if self.showToolbar:
self._draw_toolbar()
-
-
def _draw_menu_bar(self):
- """绘制菜单栏"""
- with imgui_ctx.begin_main_menu_bar() as main_menu:
- if main_menu:
- # 文件菜单
- with imgui_ctx.begin_menu("文件") as file_menu:
- if file_menu:
- if imgui.menu_item("新建项目", "Ctrl+N", False, True)[1]:
- self._on_new_project()
- if imgui.menu_item("打开项目", "Ctrl+O", False, True)[1]:
- self._on_open_project()
- imgui.separator()
- if imgui.menu_item("保存", "Ctrl+S", False, True)[1]:
- self._on_save_project()
- if imgui.menu_item("另存为", "", False, True)[1]:
- self._on_save_as_project()
- imgui.separator()
- if imgui.menu_item("退出", "Alt+F4", False, True)[1]:
- self._on_exit()
-
- # 编辑菜单
- with imgui_ctx.begin_menu("编辑") as edit_menu:
- if edit_menu:
- if imgui.menu_item("撤销", "Ctrl+Z", False, True)[1]:
- self._on_undo()
- if imgui.menu_item("重做", "Ctrl+Y", False, True)[1]:
- self._on_redo()
- imgui.separator()
- if imgui.menu_item("剪切", "Ctrl+X", False, True)[1]:
- self._on_cut()
- if imgui.menu_item("复制", "Ctrl+C", False, True)[1]:
- self._on_copy()
- if imgui.menu_item("粘贴", "Ctrl+V", False, True)[1]:
- self._on_paste()
- imgui.separator()
- if imgui.menu_item("删除", "Del", False, True)[1]:
- self._on_delete()
-
- # 创建菜单
- with imgui_ctx.begin_menu("创建") as create_menu:
- if create_menu:
- if imgui.menu_item("导入模型", "", False, True)[1]:
- self._on_import_model()
-
- imgui.separator()
-
- if imgui.menu_item("空对象", "", False, True)[1]:
- self._on_create_empty_object()
-
- # 3D对象子菜单
- with imgui_ctx.begin_menu("3D对象") as three_d_menu:
- if three_d_menu:
- if imgui.menu_item("立方体", "", False, True)[1]:
- self._on_create_cube()
- if imgui.menu_item("球体", "", False, True)[1]:
- self._on_create_sphere()
- if imgui.menu_item("圆柱体", "", False, True)[1]:
- self._on_create_cylinder()
- if imgui.menu_item("平面", "", False, True)[1]:
- self._on_create_plane()
-
- # 3D GUI子菜单
- with imgui_ctx.begin_menu("3D GUI") as three_d_gui_menu:
- if three_d_gui_menu:
- if imgui.menu_item("3D文本", "", False, True)[1]:
- self._on_create_3d_text()
- if imgui.menu_item("3D图片", "", False, True)[1]:
- self._on_create_3d_image()
-
- # GUI子菜单
- with imgui_ctx.begin_menu("GUI") as gui_menu:
- if gui_menu:
- if imgui.menu_item("创建按钮", "", False, True)[1]:
- self._on_create_gui_button()
- if imgui.menu_item("创建标签", "", False, True)[1]:
- self._on_create_gui_label()
- if imgui.menu_item("创建输入框", "", False, True)[1]:
- self._on_create_gui_entry()
- if imgui.menu_item("创建图片", "", False, True)[1]:
- self._on_create_gui_image()
- imgui.separator()
- if imgui.menu_item("创建视频屏幕", "", False, True)[1]:
- self._on_create_video_screen()
- if imgui.menu_item("创建2D视频屏幕", "", False, True)[1]:
- self._on_create_2d_video_screen()
- if imgui.menu_item("创建球形视频", "", False, True)[1]:
- self._on_create_spherical_video()
- if imgui.menu_item("创建虚拟屏幕", "", False, True)[1]:
- self._on_create_virtual_screen()
-
- # 光源子菜单
- with imgui_ctx.begin_menu("光源") as light_menu:
- if light_menu:
- if imgui.menu_item("聚光灯", "", False, True)[1]:
- self._on_create_spot_light()
- if imgui.menu_item("点光源", "", False, True)[1]:
- self._on_create_point_light()
-
- # 地形子菜单
- with imgui_ctx.begin_menu("地形") as terrain_menu:
- if terrain_menu:
- if imgui.menu_item("创建平面地形", "", False, True)[1]:
- self._on_create_flat_terrain()
- if imgui.menu_item("从高度图创建地形", "", False, True)[1]:
- self._on_create_heightmap_terrain()
-
- # 脚本子菜单
- with imgui_ctx.begin_menu("脚本") as script_menu:
- if script_menu:
- if imgui.menu_item("创建脚本...", "", False, True)[1]:
- self._on_create_script()
- if imgui.menu_item("加载脚本文件...", "", False, True)[1]:
- self._on_load_script()
- imgui.separator()
- if imgui.menu_item("重载所有脚本", "", False, True)[1]:
- self._on_reload_all_scripts()
- _, self.hotReloadEnabled = imgui.menu_item("启用热重载", "", self.hotReloadEnabled, True)
- if imgui.menu_item("脚本管理器", "", False, True)[1]:
- self._on_open_scripts_manager()
-
- # 信息面板子菜单
- with imgui_ctx.begin_menu("信息面板") as info_panel_menu:
- if info_panel_menu:
- if imgui.menu_item("创建2D示例面板", "", False, True)[1]:
- self._on_create_2d_sample_panel()
- if imgui.menu_item("创建3D实例面板", "", False, True)[1]:
- self._on_create_3d_sample_panel()
- if imgui.menu_item("Web面板", "", False, True)[1]:
- self._on_create_web_panel()
-
- # 视图菜单
- with imgui_ctx.begin_menu("视图") as view_menu:
- if view_menu:
- _, self.showToolbar = imgui.menu_item("工具栏", "", self.showToolbar, True)
- _, self.showSceneTree = imgui.menu_item("场景树", "", self.showSceneTree, True)
- _, self.showResourceManager = imgui.menu_item("资源管理器", "", self.showResourceManager, True)
- _, self.showPropertyPanel = imgui.menu_item("属性面板", "", self.showPropertyPanel, True)
- _, self.showConsole = imgui.menu_item("控制台", "", self.showConsole, True)
- _, self.showScriptPanel = imgui.menu_item("脚本管理", "", self.showScriptPanel, True)
-
- # 工具菜单
- with imgui_ctx.begin_menu("工具") as tools_menu:
- if tools_menu:
- # 工具切换选项
- if imgui.menu_item("选择工具", "", False, True)[1]:
- self.tool_manager.setCurrentTool("选择")
- if imgui.menu_item("移动工具", "", False, True)[1]:
- self.tool_manager.setCurrentTool("移动")
- if imgui.menu_item("旋转工具", "", False, True)[1]:
- self.tool_manager.setCurrentTool("旋转")
- if imgui.menu_item("缩放工具", "", False, True)[1]:
- self.tool_manager.setCurrentTool("缩放")
-
- imgui.separator()
-
- # 编辑工具
- if imgui.menu_item("光照编辑", "", False, True)[1]:
- self.tool_manager.setCurrentTool("光照编辑")
- if imgui.menu_item("图形编辑", "", False, True)[1]:
- self.tool_manager.setCurrentTool("图形编辑")
-
- imgui.separator()
-
- # VR子菜单
- with imgui_ctx.begin_menu("VR") as vr_menu:
- if vr_menu:
- if imgui.menu_item("进入VR模式", "", False, True)[1]:
- self._toggle_vr_mode()
- if imgui.menu_item("退出VR模式", "", False, True)[1]:
- self._exit_vr_mode()
-
- imgui.separator()
-
- if imgui.menu_item("VR状态", "", False, True)[1]:
- self._show_vr_status()
- if imgui.menu_item("VR设置", "", False, True)[1]:
- self._show_vr_settings()
-
- imgui.separator()
-
- # VR调试子菜单
- with imgui_ctx.begin_menu("VR调试") as vr_debug_menu:
- if vr_debug_menu:
- _, self.vr_debug_enabled = imgui.menu_item("启用调试输出", "", self.vr_debug_enabled, True)
-
- if imgui.menu_item("立即显示性能报告", "", False, True)[1]:
- self._show_vr_performance_report()
-
- imgui.separator()
-
- # 输出模式
- with imgui_ctx.begin_menu("输出模式") as output_menu:
- if output_menu:
- if imgui.menu_item("简短模式", "", not self.vr_detailed_mode, True)[1]:
- self.vr_detailed_mode = False
- if imgui.menu_item("详细模式", "", self.vr_detailed_mode, True)[1]:
- self.vr_detailed_mode = True
-
- imgui.separator()
-
- _, self.vr_performance_monitor = imgui.menu_item("启用性能监控", "", self.vr_performance_monitor, True)
-
- # 窗口菜单 - 已隐藏
- # with imgui_ctx.begin_menu("窗口") as window_menu:
- # if window_menu:
- # _, self.showDemoWindow = imgui.menu_item("ImGui演示", "", self.showDemoWindow, True)
- # if self.testTexture:
- # imgui.menu_item("关闭纹理测试", "", False, True)
- # else:
- # imgui.menu_item("显示纹理测试", "", False, True)
-
- # 帮助菜单
- with imgui_ctx.begin_menu("帮助") as help_menu:
- if help_menu:
- imgui.menu_item("关于", "", False, True)
- imgui.menu_item("文档", "", False, True)
-
- # 右侧显示FPS
- imgui.set_cursor_pos_x(imgui.get_window_size().x - 140)
- imgui.text("%.2f FPS (%.2f ms)" % (imgui.get_io().framerate, 1000.0 / imgui.get_io().framerate))
-
+ self.editor_panels.draw_menu_bar()
+
def _draw_toolbar(self):
- """绘制工具栏"""
- # 工具栏可以保持无标题栏,但允许移动和调整大小
- flags = self.style_manager.get_window_flags("toolbar")
-
- with self.style_manager.begin_styled_window("工具栏", self.showToolbar, flags):
- self.showToolbar = True # 确保窗口保持打开
-
- # 选择工具按钮
- select_active = self.tool_manager.isSelectionTool()
- if self.icons.get('select'):
- tint_col = (1.0, 1.0, 0.0, 1.0) if select_active else (1.0, 1.0, 1.0, 1.0)
- if self.style_manager.image_button(self.icons['select'], (24, 24), tint_col=tint_col):
- self.tool_manager.setCurrentTool("选择")
- if imgui.is_item_hovered():
- imgui.set_tooltip("选择工具 (Q)")
- imgui.same_line()
- else:
- if imgui.button("选择##select_tool"):
- self.tool_manager.setCurrentTool("选择")
- if select_active:
- draw_list = imgui.get_window_draw_list()
- button_min = imgui.get_item_rect_min()
- button_max = imgui.get_item_rect_max()
- draw_list.add_rect_filled(button_min, button_max, imgui.get_color_u32((0.3, 0.6, 1.0, 0.3)))
- imgui.same_line()
-
- # 移动工具按钮
- move_active = self.tool_manager.isMoveTool()
- if self.icons.get('move'):
- # 使用不同颜色表示活动状态
- tint_col = (1.0, 1.0, 0.0, 1.0) if move_active else (1.0, 1.0, 1.0, 1.0) # 活动时显示黄色
- if self.style_manager.image_button(self.icons['move'], (24, 24), tint_col=tint_col):
- self.tool_manager.setCurrentTool("移动")
- if imgui.is_item_hovered():
- imgui.set_tooltip("移动工具 (W)")
- imgui.same_line()
- else:
- if imgui.button("移动##move_tool"):
- self.tool_manager.setCurrentTool("移动")
- if move_active:
- # 为活动按钮添加背景色
- draw_list = imgui.get_window_draw_list()
- button_min = imgui.get_item_rect_min()
- button_max = imgui.get_item_rect_max()
- draw_list.add_rect_filled(button_min, button_max, imgui.get_color_u32((0.3, 0.6, 1.0, 0.3)))
- imgui.same_line()
-
- # 旋转工具按钮
- rotate_active = self.tool_manager.isRotateTool()
- if self.icons.get('rotate'):
- tint_col = (1.0, 1.0, 0.0, 1.0) if rotate_active else (1.0, 1.0, 1.0, 1.0)
- if self.style_manager.image_button(self.icons['rotate'], (24, 24), tint_col=tint_col):
- self.tool_manager.setCurrentTool("旋转")
- if imgui.is_item_hovered():
- imgui.set_tooltip("旋转工具 (E)")
- imgui.same_line()
- else:
- if imgui.button("旋转##rotate_tool"):
- self.tool_manager.setCurrentTool("旋转")
- if rotate_active:
- draw_list = imgui.get_window_draw_list()
- button_min = imgui.get_item_rect_min()
- button_max = imgui.get_item_rect_max()
- draw_list.add_rect_filled(button_min, button_max, imgui.get_color_u32((0.3, 0.6, 1.0, 0.3)))
- imgui.same_line()
-
- # 缩放工具按钮
- scale_active = self.tool_manager.isScaleTool()
- if self.icons.get('scale'):
- tint_col = (1.0, 1.0, 0.0, 1.0) if scale_active else (1.0, 1.0, 1.0, 1.0)
- if self.style_manager.image_button(self.icons['scale'], (24, 24), tint_col=tint_col):
- self.tool_manager.setCurrentTool("缩放")
- if imgui.is_item_hovered():
- imgui.set_tooltip("缩放工具 (R)")
- else:
- if imgui.button("缩放##scale_tool"):
- self.tool_manager.setCurrentTool("缩放")
- if scale_active:
- draw_list = imgui.get_window_draw_list()
- button_min = imgui.get_item_rect_min()
- button_max = imgui.get_item_rect_max()
- draw_list.add_rect_filled(button_min, button_max, imgui.get_color_u32((0.3, 0.6, 1.0, 0.3)))
-
- imgui.same_line()
- imgui.separator()
- imgui.same_line()
-
- # 工具按钮已移除(导入、保存、播放)
-
- def _draw_scene_tree(self):
- """绘制场景树面板"""
- # 使用更少的限制性标志,允许docking
- flags = (imgui.WindowFlags_.no_collapse)
-
- with self.style_manager.begin_styled_window("场景树", self.showSceneTree, flags):
- self.showSceneTree = True # 确保窗口保持打开
-
- imgui.text("场景层级")
- imgui.separator()
-
- # 构建动态场景树
- self._build_scene_tree()
-
- def _build_scene_tree(self):
- """构建动态场景树"""
- # 渲染节点
- if imgui.tree_node("渲染"):
- # 环境光
- if hasattr(self, 'ambient_light') and self.ambient_light:
- self._draw_scene_node(self.ambient_light, "环境光", "light")
-
- # 聚光灯
- if hasattr(self, 'scene_manager') and self.scene_manager:
- if hasattr(self.scene_manager, 'Spotlight') and self.scene_manager.Spotlight:
- for i, spotlight in enumerate(self.scene_manager.Spotlight):
- self._draw_scene_node(spotlight, f"聚光灯_{i+1}", "light")
- if hasattr(self.scene_manager, 'Pointlight') and self.scene_manager.Pointlight:
- for i, pointlight in enumerate(self.scene_manager.Pointlight):
- self._draw_scene_node(pointlight, f"点光源_{i+1}", "light")
-
- # 地板
- if hasattr(self, 'ground') and self.ground:
- self._draw_scene_node(self.ground, "地板", "geometry")
-
- imgui.tree_pop()
-
- # 相机节点
- if imgui.tree_node("相机"):
- if hasattr(self, 'camera') and self.camera:
- self._draw_scene_node(self.camera, "主相机", "camera")
- imgui.tree_pop()
-
- # 3D模型节点
- if imgui.tree_node("模型"):
- if hasattr(self, 'scene_manager') and self.scene_manager and hasattr(self.scene_manager, 'models'):
- if self.scene_manager.models:
- for i, model in enumerate(self.scene_manager.models):
- if model and not model.isEmpty():
- self._draw_scene_node(model, model.getName() or f"模型_{i+1}", "model")
- else:
- imgui.text("(空)")
- else:
- imgui.text("(空)")
- imgui.tree_pop()
-
- # GUI元素节点
- if imgui.tree_node("GUI元素"):
- if hasattr(self, 'gui_manager') and self.gui_manager and hasattr(self.gui_manager, 'gui_elements'):
- if self.gui_manager.gui_elements:
- for gui_element in self.gui_manager.gui_elements:
- if gui_element and hasattr(gui_element, 'node'):
- gui_type = getattr(gui_element, 'gui_type', 'GUI_UNKNOWN')
- display_name = getattr(gui_element, 'name', gui_type)
- self._draw_scene_node(gui_element.node, display_name, "gui", gui_type)
- else:
- imgui.text("(空)")
- else:
- imgui.text("(空)")
- imgui.tree_pop()
-
- def _draw_scene_node(self, node, name, node_type, gui_subtype=None):
- """绘制单个场景节点"""
- if not node or node.isEmpty():
- return
-
- # 检查是否被选中
- is_selected = (hasattr(self, 'selection') and self.selection and
- hasattr(self.selection, 'selectedNode') and
- self.selection.selectedNode == node)
-
- # 节点可见性
- is_visible = node.is_hidden() == False
-
- # 设置选择颜色
- if is_selected:
- imgui.push_style_color(imgui.Col_.text, (0.2, 0.6, 1.0, 1.0))
-
- try:
- # 显示节点
- node_open = imgui.tree_node(name)
-
- # 处理节点选择
- if imgui.is_item_clicked():
- if hasattr(self, 'selection') and self.selection:
- self.selection.updateSelection(node)
-
- # 右键菜单
- if imgui.is_item_hovered() and imgui.is_mouse_clicked(1):
- self._show_node_context_menu(node, name, node_type)
-
- # 显示节点属性
- imgui.same_line()
- if is_visible:
- imgui.text_colored((0.5, 1.0, 0.5, 1.0), "可见")
- else:
- imgui.text_colored((0.5, 0.5, 0.5, 1.0), "隐藏")
-
- if node_open:
- # 如果有子节点,递归显示
- if node.getNumChildren() > 0:
- for i in range(node.getNumChildren()):
- child = node.getChild(i)
- if child and not child.isEmpty():
- child_name = child.getName() or f"子节点_{i+1}"
- self._draw_scene_node(child, child_name, node_type)
- imgui.tree_pop()
- except Exception as e:
- print(f"绘制场景节点时出错: {e}")
- finally:
- # 确保颜色状态被恢复
- if is_selected:
- imgui.pop_style_color()
-
- def _show_node_context_menu(self, node, name, node_type):
- """显示节点右键菜单"""
- self._context_menu_node = True
- self._context_menu_target = node
-
- def _draw_resource_manager(self):
- """绘制资源管理器面板"""
- # 使用面板类型的窗口标志,支持docking
- flags = self.style_manager.get_window_flags("panel")
-
- with self.style_manager.begin_styled_window("资源管理器", self.showResourceManager, flags):
- self.showResourceManager = True # 确保窗口保持打开
-
- # 获取资源管理器实例
- rm = self.resource_manager
-
- # 工具栏
- imgui.text("文件浏览器")
- imgui.separator()
-
- # 导航按钮
- if imgui.button("◀"):
- rm.navigate_back()
- imgui.same_line()
- if imgui.button("▶"):
- rm.navigate_forward()
- imgui.same_line()
- if imgui.button("▲"):
- rm.navigate_up()
- imgui.same_line()
- if imgui.button("主页"):
- rm.navigate_to(rm.project_root / "Resources")
- imgui.same_line()
- if imgui.button("刷新"):
- rm.force_refresh()
-
- # 自动刷新开关
- imgui.same_line()
- changed, rm.auto_refresh_enabled = imgui.checkbox("自动刷新", rm.auto_refresh_enabled)
- if changed:
- rm.set_auto_refresh(rm.auto_refresh_enabled)
-
- imgui.same_line()
- imgui.text(" ")
- imgui.same_line()
-
- # 路径输入框
- changed, new_path = imgui.input_text("路径", str(rm.current_path), 256)
- if changed:
- try:
- rm.navigate_to(Path(new_path))
- except:
- pass
-
- # 搜索框
- changed, rm.search_filter = imgui.input_text("搜索", rm.search_filter, 256)
-
- imgui.separator()
-
- # 检查自动刷新
- if rm.refresh_if_needed():
- # 目录内容发生变化,可以在这里添加通知逻辑
- pass
-
- # 获取目录内容
- dirs, files = rm.get_directory_contents(rm.current_path)
-
- # 显示目录
- for dir_path in dirs:
- if not rm.should_show_file(dir_path):
- continue
-
- # 目录图标和名称
- icon_name = rm.get_file_icon(dir_path.name, is_folder=True)
- node_open = False
-
- # 检查是否被选中
- is_selected = dir_path in rm.selected_files
-
- # 使用TreeNode来显示目录
- if is_selected:
- imgui.push_style_color(imgui.Col_.header, (100/255, 150/255, 200/255, 1.0))
-
- # 尝试加载PNG图标
- icon_texture = None
- try:
- # 直接使用图标名称,load_icon会自动添加.png
- icon_texture = self.style_manager.load_icon(f"file_types/{icon_name}")
- except:
- pass
-
- if icon_texture:
- # 使用PNG图标
- imgui.image(icon_texture, (16, 16))
- imgui.same_line()
- node_open = imgui.tree_node(f"{dir_path.name}")
- else:
- # 回退到文本标识符
- node_open = imgui.tree_node(f"[{icon_name.upper()}]{dir_path.name}")
-
- if is_selected:
- imgui.pop_style_color()
-
- # 处理选择
- if imgui.is_item_clicked():
- if imgui.get_io().key_ctrl:
- # 多选模式
- if is_selected:
- rm.selected_files.discard(dir_path)
- else:
- rm.selected_files.add(dir_path)
- else:
- # 单选模式
- rm.selected_files.clear()
- rm.selected_files.add(dir_path)
- rm.focused_file = dir_path
-
- # 双击导航
- if imgui.is_item_hovered() and imgui.is_mouse_double_clicked(0):
- rm.navigate_to(dir_path)
-
- # 右键菜单
- if imgui.is_item_hovered() and imgui.is_mouse_clicked(1):
- rm.show_context_menu = True
- rm.context_menu_file = dir_path
- rm.context_menu_position = (imgui.get_mouse_pos().x, imgui.get_mouse_pos().y)
-
- # 如果节点展开,显示子内容
- if node_open:
- # 获取子目录内容
- subdirs, subfiles = rm.get_directory_contents(dir_path)
-
- # 显示子目录
- for subdir in subdirs:
- if not rm.should_show_file(subdir):
- continue
-
- # 初始化变量
- subicon_name = "folder"
- sub_is_selected = False
-
- # 获取子目录图标名称
- subicon_name = rm.get_file_icon(subdir.name, is_folder=True)
- sub_is_selected = subdir in rm.selected_files
-
- # 尝试加载PNG图标
- subicon_texture = None
- try:
- subicon_texture = self.style_manager.load_icon(f"file_types/{subicon_name}")
- except:
- pass
-
- if subicon_texture:
- # 使用PNG图标
- imgui.image(subicon_texture, (16, 16))
- imgui.same_line()
- sub_node_open = imgui.tree_node(f" {subdir.name}")
- else:
- # 回退到文本标识符
- sub_node_open = imgui.tree_node(f" [{subicon_name.upper()}]{subdir.name}")
-
- if sub_is_selected:
- imgui.pop_style_color()
-
- # 处理子目录的选择
- if imgui.is_item_clicked():
- if imgui.get_io().key_ctrl:
- if sub_is_selected:
- rm.selected_files.discard(subdir)
- else:
- rm.selected_files.add(subdir)
- else:
- rm.selected_files.clear()
- rm.selected_files.add(subdir)
- rm.focused_file = subdir
-
- # 双击子目录导航
- if imgui.is_item_hovered() and imgui.is_mouse_double_clicked(0):
- rm.navigate_to(subdir)
-
- # 右键菜单
- if imgui.is_item_hovered() and imgui.is_mouse_clicked(1):
- rm.show_context_menu = True
- rm.context_menu_file = subdir
- rm.context_menu_position = (imgui.get_mouse_pos().x, imgui.get_mouse_pos().y)
-
- if sub_node_open:
- imgui.tree_pop()
-
- # 显示子文件
- for subfile in subfiles:
- if not rm.should_show_file(subfile):
- continue
-
- subicon_name = rm.get_file_icon(subfile.name)
- sub_is_selected = subfile in rm.selected_files
-
- # 尝试加载PNG图标
- subicon_texture = None
- try:
- subicon_texture = self.style_manager.load_icon(f"file_types/{subicon_name}")
- except:
- pass
-
- if subicon_texture:
- # 使用PNG图标
- imgui.image(subicon_texture, (16, 16))
- imgui.same_line()
- selected = imgui.selectable(f" {subfile.name}", sub_is_selected)
- else:
- # 回退到文本标识符
- selected = imgui.selectable(f" [{subicon_name.upper()}] {subfile.name}", sub_is_selected)
-
- # 处理子文件的选择
- if selected:
- if imgui.get_io().key_ctrl:
- if sub_is_selected:
- rm.selected_files.discard(subfile)
- else:
- rm.selected_files.add(subfile)
- else:
- rm.selected_files.clear()
- rm.selected_files.add(subfile)
- rm.focused_file = subfile
-
- # 双击子文件操作
- if imgui.is_item_hovered() and imgui.is_mouse_double_clicked(0):
- if subfile.suffix.lower() in ['.gltf', '.glb', '.fbx', '.bam', '.egg', '.obj']:
- self.scene_manager.importModel(str(subfile))
- self.add_info_message(f"正在导入模型: {subfile.name}")
- else:
- rm.open_file(subfile)
-
- # 右键菜单
- if imgui.is_item_hovered() and imgui.is_mouse_clicked(1):
- rm.show_context_menu = True
- rm.context_menu_file = subfile
- rm.context_menu_position = (imgui.get_mouse_pos().x, imgui.get_mouse_pos().y)
-
- # 只有在节点展开时才调用tree_pop
- if node_open:
- imgui.tree_pop()
-
-
-
- # 处理拖拽开始
- if imgui.is_item_active() and imgui.is_item_hovered():
- if imgui.is_mouse_dragging(0):
- # 开始拖拽
- drag_files = list(rm.selected_files) if rm.selected_files else [file_path]
- rm.start_drag(drag_files)
- self.is_dragging = True
- self.show_drag_overlay = True
-
- # 双击打开文件
- if imgui.is_item_hovered() and imgui.is_mouse_double_clicked(0):
- # 检查是否是支持的3D模型格式
- if file_path.suffix.lower() in ['.gltf', '.glb', '.fbx', '.bam', '.egg', '.obj']:
- # 导入3D模型
- self.add_info_message(f"正在导入模型: {file_path.name}")
- self.scene_manager.importModel(str(file_path))
- else:
- # 使用系统默认程序打开
- rm.open_file(file_path)
-
- # 右键菜单
- if imgui.is_item_hovered() and imgui.is_mouse_clicked(1):
- rm.show_context_menu = True
- rm.context_menu_file = file_path
- rm.context_menu_position = (imgui.get_mouse_pos().x, imgui.get_mouse_pos().y)
-
- # 右键菜单
- if rm.show_context_menu and rm.context_menu_file:
- imgui.set_next_window_pos((rm.context_menu_position[0], rm.context_menu_position[1]))
- with imgui_ctx.begin_popup("context_menu", imgui.WindowFlags_.no_title_bar |
- imgui.WindowFlags_.no_resize | imgui.WindowFlags_.always_auto_resize) as popup:
- if popup:
- if rm.context_menu_file.is_dir():
- if imgui.menu_item("打开"):
- rm.navigate_to(rm.context_menu_file)
- imgui.separator()
- if imgui.menu_item("重命名"):
- print(f"重命名文件夹: {rm.context_menu_file.name}")
- if imgui.menu_item("删除"):
- print(f"删除文件夹: {rm.context_menu_file.name}")
- else:
- if imgui.menu_item("打开"):
- rm.open_file(rm.context_menu_file)
- imgui.separator()
- if imgui.menu_item("导入到场景"):
- if rm.context_menu_file.suffix.lower() in ['.gltf', '.glb', '.fbx', '.bam', '.egg', '.obj']:
- self.add_info_message(f"正在导入模型: {rm.context_menu_file.name}")
- self.scene_manager.importModel(str(rm.context_menu_file))
- if imgui.menu_item("重命名"):
- print(f"重命名文件: {rm.context_menu_file.name}")
- if imgui.menu_item("删除"):
- print(f"删除文件: {rm.context_menu_file.name}")
-
- imgui.separator()
- if imgui.menu_item("复制路径"):
- imgui.set_clipboard_text(str(rm.context_menu_file))
- self.add_info_message("路径已复制到剪贴板")
- if imgui.menu_item("在文件管理器中显示"):
- import platform
- import subprocess
- if platform.system() == "Windows":
- subprocess.run(["explorer", "/select,", str(rm.context_menu_file)])
- elif platform.system() == "Darwin":
- subprocess.run(["open", "-R", str(rm.context_menu_file)])
- else:
- subprocess.run(["xdg-open", str(rm.context_menu_file.parent)])
-
- # 如果点击其他地方,关闭菜单
- if imgui.is_mouse_clicked(0) or imgui.is_mouse_clicked(1):
- if not imgui.is_window_hovered():
- rm.show_context_menu = False
- rm.context_menu_file = None
-
- def _draw_property_panel(self):
- """绘制属性面板"""
- # 使用面板类型的窗口标志,支持docking
- flags = self.style_manager.get_window_flags("panel")
-
- with self.style_manager.begin_styled_window("属性面板", self.showPropertyPanel, flags):
- self.showPropertyPanel = True # 确保窗口保持打开
-
- # 获取当前选中的节点
- selected_node = None
- if hasattr(self, 'selection') and self.selection and hasattr(self.selection, 'selectedNode'):
- selected_node = self.selection.selectedNode
-
- if selected_node and not selected_node.isEmpty():
- self._draw_node_properties(selected_node)
- else:
- # 无选中对象时显示提示(模仿Qt版本的空状态样式)
- imgui.spacing()
- imgui.spacing()
-
- # 居中显示提示信息
- window_width = imgui.get_window_width()
- text_width = 200 # 估算文本宽度
- text_pos_x = (window_width - text_width) / 2
-
- imgui.set_cursor_pos_x(text_pos_x)
- imgui.text_colored((0.5, 0.5, 0.5, 1.0), "🔍 未选择任何对象")
-
- imgui.set_cursor_pos_x(text_pos_x - 20)
- imgui.text("请从场景树中选择一个对象")
-
- imgui.set_cursor_pos_x(text_pos_x + 10)
- imgui.text("以查看其属性")
-
- imgui.spacing()
- imgui.spacing()
-
- # 添加一些分隔线和装饰
- imgui.separator()
-
- # 显示快速提示
- imgui.text("💡 快速提示:")
- imgui.bullet_text("单击场景树中的对象进行选择")
- imgui.bullet_text("使用 F 键快速聚焦到选中对象")
- imgui.bullet_text("使用 Delete 键删除选中对象")
-
- def _draw_node_properties(self, node):
- """绘制节点属性"""
- if not node or node.isEmpty():
- # 停止变换监控
- self.stop_transform_monitoring()
- return
-
- # 检查是否需要重新启动变换监控
- if self._monitored_node != node:
- self.stop_transform_monitoring()
- self.start_transform_monitoring(node)
-
- # 获取节点基本信息
- node_name = node.getName() or "未命名节点"
- node_type = self._get_node_type_from_node(node)
-
- # 添加一些间距,模仿Qt版本的布局
- imgui.spacing()
-
- # 物体名称组(使用Qt版本的样式)
- if imgui.collapsing_header("物体名称"):
- # 第一行:可见性复选框和名称输入
- user_visible = node.getPythonTag("user_visible")
- if user_visible is None:
- user_visible = True
- node.setPythonTag("user_visible", True)
-
- # 可见性复选框(模仿Qt版本的样式)
- changed, is_visible = imgui.checkbox("##visibility", user_visible)
- if changed:
- node.setPythonTag("user_visible", is_visible)
- if is_visible:
- node.show()
- else:
- node.hide()
-
- imgui.same_line()
- imgui.text("可见")
- imgui.same_line()
- imgui.spacing()
- imgui.same_line()
-
- # 名称输入框(模仿Qt版本的样式)
- imgui.text("名称:")
- imgui.same_line()
- changed, new_name = imgui.input_text("##name", node_name, 256)
- if changed and hasattr(self, 'selection'):
- # 更新场景树中的名称
- self._update_node_name(node, new_name)
-
- # 添加分隔线
- imgui.separator()
-
- # 状态徽章(模仿Qt版本的徽章样式)
- self._draw_status_badges(node)
-
- imgui.spacing()
-
- # 变换属性组
- if imgui.collapsing_header("变换 Transform"):
- self._draw_transform_properties(node)
-
- # 根据节点类型显示特定属性组
- if node_type == "GUI元素":
- if imgui.collapsing_header("GUI信息"):
- self._draw_gui_properties(node)
- elif node_type == "光源":
- if imgui.collapsing_header("光源属性"):
- self._draw_light_properties(node)
- elif node_type == "模型":
- if imgui.collapsing_header("模型属性"):
- self._draw_model_properties(node)
-
- # 动画控制组(只对模型显示)
- if imgui.collapsing_header("动画控制"):
- self._draw_animation_properties(node)
-
- # 外观属性组(通用)
- if imgui.collapsing_header("外观属性"):
- self._draw_appearance_properties(node)
-
- # 碰撞检测组
- if imgui.collapsing_header("碰撞检测"):
- self._draw_collision_properties(node)
-
- # 操作按钮组
- if imgui.collapsing_header("操作"):
- self._draw_property_actions(node)
-
- def _getActor(self, origin_model):
- """
- 获取或创建模型的Actor,用于动画控制
- 复用Qt版本经过验证的实现方式
- """
- # 检查缓存
- if origin_model in self._actor_cache:
- return self._actor_cache[origin_model]
-
- # 尝试直接从内存创建
- if origin_model.hasTag("can_create_actor_from_memory"):
- try:
- test_actor = Actor(origin_model)
- anims = test_actor.getAnimNames()
- self._actor_cache[origin_model] = test_actor
- print(f"[Actor加载] 内存创建检测到动画: {anims}")
- if anims:
- return test_actor
- else:
- test_actor.cleanup()
- test_actor.removeNode()
- except Exception as e:
- print(f"从内存模型创建Actor失败: {e}")
-
- # 如果不能直接从内存创建,再尝试通过文件路径加载
- filepath = origin_model.getTag("model_path")
- if not filepath:
- return None
+ self.editor_panels.draw_toolbar()
- print(f"[Actor加载] 尝试加载: {filepath}")
+ def _draw_scene_tree(self, *args, **kwargs):
+ return self.editor_panels._draw_scene_tree(*args, **kwargs)
- # 处理跨平台路径问题
- import os
- # 检查路径是否有效,如果无效则尝试修复
- if not os.path.exists(filepath):
- original_filepath = filepath
- # 尝试多种修复策略
- fixed = False
+ def _build_scene_tree(self, *args, **kwargs):
+ return self.editor_panels._build_scene_tree(*args, **kwargs)
- import platform
- # 策略1: 处理Linux风格路径在Windows上的问题
- if filepath.startswith('/') and platform.system() == "Windows":
- print("[路径转换] 尝试处理Linux风格路径:", filepath)
- path_parts = filepath.split('/')
- print(platform.system())
- if len(path_parts) > 1:
- drive_letter = path_parts[1].upper() + ':\\' # 添加反斜杠确保正确路径格式
- remaining_path = '\\'.join(path_parts[2:]) if len(path_parts) > 2 else ''
- potential_path = os.path.join(drive_letter, remaining_path)
- print(f"[路径转换] 构造的潜在路径: {potential_path}")
- if os.path.exists(potential_path):
- filepath = potential_path
- fixed = True
- print(f"[路径转换] 成功: {original_filepath} -> {filepath}")
- else:
- print(f"[路径转换] 文件不存在: {potential_path}")
-
-
- # 策略2: 处理路径分隔符问题
- if not fixed:
- # 尝试规范化路径
- normalized_path = os.path.normpath(filepath)
- print(f"[路径规范化] 尝试规范化路径: {filepath} -> {normalized_path}")
- if os.path.exists(normalized_path):
- filepath = normalized_path
- fixed = True
- print(f"[路径规范化] 成功: {filepath}")
- else:
- print(f"[路径规范化] 文件不存在: {normalized_path}")
-
- # 策略3: 在Resources目录中查找
- if not fixed:
- # 尝试在Resources目录中查找文件
- resources_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), "Resources")
- filename = os.path.basename(filepath)
- potential_path = os.path.join(resources_path, filename)
- print(f"[Resources查找] 尝试在Resources目录查找: {potential_path}")
- if os.path.exists(potential_path):
- filepath = potential_path
- fixed = True
- print(f"[Resources查找] 成功: {filepath}")
- else:
- print(f"[Resources查找] 文件不存在: {potential_path}")
-
- if fixed:
- print(f"路径修复: {original_filepath} -> {filepath}")
- # 更新模型标签
- origin_model.setTag("model_path", filepath)
- else:
- print(f"[警告] 模型文件不存在: {filepath}")
- return None
+ def _draw_scene_node(self, *args, **kwargs):
+ return self.editor_panels._draw_scene_node(*args, **kwargs)
- # 检查是否是 FBX 文件,如果是,使用专门的 FBX 动画加载器
- if filepath.lower().endswith('.fbx'):
- pass
- #return self._createFBXActor(origin_model, filepath)
+ def _show_node_context_menu(self, *args, **kwargs):
+ return self.editor_panels._show_node_context_menu(*args, **kwargs)
- # 其他格式使用标准 Actor 加载
- try:
- import gltf
- from panda3d.core import Filename
-
- # 将Panda3D路径转换为操作系统特定路径
- panda_filename = Filename(filepath)
- os_specific_path = panda_filename.to_os_specific()
- print(f"[路径转换] {filepath} -> {os_specific_path}")
-
- print(f"[GLTF加载] 尝试加载: {os_specific_path}")
-
- # 使用明确的设置确保动画被加载
- gltf_settings = gltf.GltfSettings(skip_animations=False)
- model_root = gltf.load_model(os_specific_path, gltf_settings)
- model_node = NodePath(model_root)
- test_actor = Actor(model_node)
- anims = test_actor.getAnimNames()
- test_actor.reparentTo(self.render)
- self._actor_cache[origin_model] = test_actor
- print(f"[Actor加载] 标准加载检测到动画: {anims}")
- if not anims:
- test_actor.cleanup()
- test_actor.removeNode()
- return None
- return test_actor
- except Exception as e:
- print(f"创建Actor失败: {e}")
- return None
-
- def _getModelFormat(self, origin_model):
- """获取模型格式信息"""
- filepath = origin_model.getTag("model_path")
- original_path = origin_model.getTag("original_path")
- converted_from = origin_model.getTag("converted_from")
+ def _draw_resource_manager(self, *args, **kwargs):
+ return self.editor_panels._draw_resource_manager(*args, **kwargs)
- if filepath:
- ext = filepath.lower().split('.')[-1]
- format_name = ext.upper()
+ def _draw_property_panel(self, *args, **kwargs):
+ return self.editor_panels._draw_property_panel(*args, **kwargs)
- # 如果是转换后的文件,显示转换信息
- if converted_from and original_path:
- original_ext = converted_from.upper()
- format_name = f"{format_name} (从{original_ext}转换)"
+ def _draw_node_properties(self, *args, **kwargs):
+ return self.editor_panels._draw_node_properties(*args, **kwargs)
- return format_name
- return "未知"
+ def _getActor(self, *args, **kwargs):
+ return self.animation_tools._getActor(*args, **kwargs)
- def _processAnimationNames(self, origin_model, anim_names):
- """处理和分析动画名称,返回 [(显示名称, 原始名称), ...]"""
- format_info = self._getModelFormat(origin_model)
- processed = []
+ def _getModelFormat(self, *args, **kwargs):
+ return self.animation_tools._getModelFormat(*args, **kwargs)
- print(f"[动画分析] 格式: {format_info}, 原始动画名称: {anim_names}")
+ def _processAnimationNames(self, *args, **kwargs):
+ return self.animation_tools._processAnimationNames(*args, **kwargs)
- for name in anim_names:
- display_name = name
- original_name = name
+ def _isLikelyBoneGroup(self, *args, **kwargs):
+ return self.animation_tools._isLikelyBoneGroup(*args, **kwargs)
- if format_info == "GLB":
- # GLB 格式通常有真实的动画名称
- if "|" in name:
- # 处理类似 'Armature|mixamo.com|Layer0' 的名称
- parts = name.split("|")
- if "mixamo" in name.lower():
- # Mixamo 动画
- display_name = f"Mixamo_{parts[-1]}" if len(parts) > 1 else name
- elif len(parts) > 2:
- # 其他复杂命名
- display_name = f"{parts[0]}_{parts[-1]}"
- else:
- display_name = parts[-1]
+ def _analyzeAnimationQuality(self, *args, **kwargs):
+ return self.animation_tools._analyzeAnimationQuality(*args, **kwargs)
- elif format_info == "FBX":
- # FBX 格式可能需要特殊处理
- if self._isLikelyBoneGroup(name):
- # 检查是否是骨骼组而非动画
- print(f"[警告] '{name}' 可能不是真正的动画序列,而是骨骼组")
- display_name = f"⚠️ {name} (可能非动画)"
- else:
- display_name = name
+ def _playAnimation(self, *args, **kwargs):
+ return self.animation_tools._playAnimation(*args, **kwargs)
- elif format_info in ["EGG", "BAM"]:
- # 原生格式通常命名规范
- display_name = name
+ def _pauseAnimation(self, *args, **kwargs):
+ return self.animation_tools._pauseAnimation(*args, **kwargs)
- processed.append((display_name, original_name))
- print(f"[动画分析] {original_name} → {display_name}")
+ def _stopAnimation(self, *args, **kwargs):
+ return self.animation_tools._stopAnimation(*args, **kwargs)
- return processed
+ def _loopAnimation(self, *args, **kwargs):
+ return self.animation_tools._loopAnimation(*args, **kwargs)
- def _isLikelyBoneGroup(self, name):
- """判断动画名称是否更像骨骼组而不是动画序列"""
- bone_indicators = ['joints', 'bones', 'skeleton', 'surface', 'mesh', 'beta', 'rig']
- name_lower = name.lower()
+ def _setAnimationSpeed(self, *args, **kwargs):
+ return self.animation_tools._setAnimationSpeed(*args, **kwargs)
- # 如果包含这些关键词,可能是骨骼组
- for indicator in bone_indicators:
- if indicator in name_lower:
- return True
-
- # 如果名称太简单(少于3个字符),可能不是动画
- if len(name) < 3:
- return True
-
- return False
-
- def _analyzeAnimationQuality(self, actor, anim_names, format_info):
- """分析动画质量和类型(优化版本,减少详细分析以提高性能)"""
- try:
- valid_anims = 0
-
- # 简化分析:只检查动画是否存在,不详细分析帧数
- for anim_name in anim_names:
- try:
- control = actor.getAnimControl(anim_name)
- if control and control.getNumFrames() > 1:
- valid_anims += 1
- except Exception:
- # 忽略单个动画的分析错误,继续处理其他动画
- continue
-
- if valid_anims == 0:
- return "⚠️ 无有效动画"
- elif valid_anims < len(anim_names):
- return f"⚠️ {valid_anims}/{len(anim_names)} 个有效"
- else:
- return f"✓ {valid_anims} 个动画"
-
- except Exception as e:
- # 简化错误处理
- return "分析异常"
-
+ def _clear_animation_cache(self, *args, **kwargs):
+ return self.animation_tools._clear_animation_cache(*args, **kwargs)
def _get_node_type_from_node(self, node):
"""从节点判断其类型"""
# 检查是否为GUI元素
@@ -1945,5209 +899,449 @@ class MyWorld(CoreWorld):
# 默认为几何体
return "几何体"
- def _draw_status_badges(self, node):
- """绘制状态徽章(模仿Qt版本的徽章样式)"""
- imgui.text("状态标签: ")
-
- # 可见性状态徽章
- is_visible = not node.is_hidden()
- visibility_color = (0.176, 1.0, 0.769, 1.0) if is_visible else (0.953, 0.616, 0.471, 1.0)
- visibility_text = "可见" if is_visible else "隐藏"
-
- imgui.same_line()
- imgui.text_colored(visibility_color, f"[{visibility_text}]")
-
- # 节点类型徽章
- node_type = self._get_node_type_from_node(node)
- type_colors = {
- "GUI元素": (0.188, 0.404, 0.753, 1.0), # 主题蓝色
- "光源": (1.0, 0.8, 0.2, 1.0), # 黄色
- "模型": (0.6, 0.8, 1.0, 1.0), # 浅蓝色
- "相机": (0.8, 0.8, 0.2, 1.0), # 橙色
- "几何体": (0.5, 0.5, 0.5, 1.0), # 灰色
- }
-
- if node_type in type_colors:
- imgui.same_line()
- imgui.text_colored(type_colors[node_type], f"[{node_type}]")
-
- # 功能性徽章
- badges = []
-
- # 碰撞体徽章
- has_collision = hasattr(node, 'getChild') and any('Collision' in child.getName() for child in node.getChildren() if child.getName())
- if has_collision:
- badges.append(("碰撞", (0.2, 0.4, 0.8, 1.0))) # 蓝色
-
- # 脚本徽章
- has_script = hasattr(node, 'getPythonTag') and node.getPythonTag('script')
- if has_script:
- badges.append(("脚本", (0.8, 0.4, 0.8, 1.0))) # 紫色
-
- # 动画徽章(优化检测逻辑,避免重复创建Actor)
- has_animation = False
- if node_type == "模型": # 只对模型类型进行动画检测
- # 首先检查是否已经缓存了检测结果
- cached_result = node.getPythonTag('animation')
- if cached_result is not None:
- has_animation = cached_result
- else:
- # 只有在未缓存时才进行检测
- try:
- # 使用轻量级检测:先检查文件扩展名
- model_path = node.getTag("model_path")
- if model_path and model_path.lower().endswith(('.glb', '.gltf', '.fbx')):
- # 对于可能包含动画的格式,才进行Actor检测
- actor = self._getActor(node)
- if actor and actor.getAnimNames():
- has_animation = True
- # 缓存检测结果
- node.setPythonTag('animation', has_animation)
- print(f"[动画检测] {node.getName()}: {'有动画' if has_animation else '无动画'}")
- else:
- # 对于不太可能有动画的格式,直接标记为无动画
- node.setPythonTag('animation', False)
- except Exception as e:
- print(f"动画检测失败: {e}")
- node.setPythonTag('animation', False)
- else:
- # 对于非模型类型,检查已有的动画标签
- has_animation = hasattr(node, 'getPythonTag') and node.getPythonTag('animation')
-
- if has_animation:
- badges.append(("动画", (0.4, 0.8, 0.4, 1.0))) # 绿色
-
- # 材质徽章
- has_material = hasattr(node, 'getMaterial') and node.getMaterial()
- if has_material:
- badges.append(("材质", (0.8, 0.6, 0.2, 1.0))) # 金色
-
- # 绘制功能性徽章
- for badge_text, badge_color in badges:
- imgui.same_line()
- imgui.text_colored(badge_color, f"[{badge_text}]")
-
- # 如果没有特殊徽章,显示默认状态
- if not badges:
- imgui.same_line()
- imgui.text_colored((0.5, 0.5, 0.5, 1.0), "[标准对象]")
-
- def _draw_transform_properties(self, node):
- """绘制变换属性"""
- # 位置组
- if imgui.collapsing_header("位置 Position"):
- # 相对位置
- imgui.text("相对位置")
- pos = node.getPos()
-
- # X坐标
- changed, new_x = imgui.input_float("X##pos_x", pos.x, 0.1, 1.0, "%.3f")
- if changed: node.setX(new_x)
-
- # Y坐标
- changed, new_y = imgui.input_float("Y##pos_y", pos.y, 0.1, 1.0, "%.3f")
- if changed: node.setY(new_y)
-
- # Z坐标
- changed, new_z = imgui.input_float("Z##pos_z", pos.z, 0.1, 1.0, "%.3f")
- if changed: node.setZ(new_z)
-
- # 世界位置
- imgui.text("世界位置")
- world_pos = node.getPos(self.render)
-
- imgui.text(f"世界 X: {world_pos.x:.3f}")
- imgui.text(f"世界 Y: {world_pos.y:.3f}")
- imgui.text(f"世界 Z: {world_pos.z:.3f}")
-
- # 位置操作按钮
- if imgui.button("重置位置##reset_pos"):
- node.setPos(0, 0, 0)
- imgui.same_line()
- if imgui.button("复制位置##copy_pos"):
- self._clipboard_pos = (pos.x, pos.y, pos.z)
- imgui.same_line()
- if imgui.button("粘贴位置##paste_pos") and hasattr(self, '_clipboard_pos'):
- node.setPos(self._clipboard_pos[0], self._clipboard_pos[1], self._clipboard_pos[2])
-
- # 旋转组
- if imgui.collapsing_header("旋转 Rotation"):
- hpr = node.getHpr()
-
- # HPR旋转
- imgui.text("HPR 旋转 (度)")
- changed, new_h = imgui.input_float("H##rot_h", hpr.x, 1.0, 10.0, "%.1f")
- if changed: node.setH(new_h)
-
- changed, new_p = imgui.input_float("P##rot_p", hpr.y, 1.0, 10.0, "%.1f")
- if changed: node.setP(new_p)
-
- changed, new_r = imgui.input_float("R##rot_r", hpr.z, 1.0, 10.0, "%.1f")
- if changed: node.setR(new_r)
-
- # 旋转操作按钮
- if imgui.button("重置旋转##reset_rot"):
- node.setHpr(0, 0, 0)
- imgui.same_line()
- if imgui.button("随机旋转##random_rot"):
- import random
- node.setHpr(random.randint(0, 360), random.randint(0, 360), random.randint(0, 360))
-
- # 缩放组
- if imgui.collapsing_header("缩放 Scale"):
- scale = node.getScale()
-
- # XYZ缩放
- imgui.text("XYZ 缩放")
- changed, new_sx = imgui.input_float("X##scale_x", scale.x, 0.1, 1.0, "%.3f")
- if changed: node.setSx(new_sx)
-
- changed, new_sy = imgui.input_float("Y##scale_y", scale.y, 0.1, 1.0, "%.3f")
- if changed: node.setSy(new_sy)
-
- changed, new_sz = imgui.input_float("Z##scale_z", scale.z, 0.1, 1.0, "%.3f")
- if changed: node.setSz(new_sz)
-
- # 统一缩放
- if imgui.button("统一缩放##uniform_scale"):
- uniform_scale = (scale.x + scale.y + scale.z) / 3.0
- node.setScale(uniform_scale, uniform_scale, uniform_scale)
- imgui.same_line()
- if imgui.button("重置缩放##reset_scale"):
- node.setScale(1, 1, 1)
- imgui.same_line()
- if imgui.button("翻倍##double_scale"):
- node.setScale(scale.x * 2, scale.y * 2, scale.z * 2)
-
- def _draw_gui_properties(self, node):
- """绘制GUI元素属性"""
- # 获取GUI元素
- gui_element = None
- if hasattr(node, 'getPythonTag'):
- gui_element = node.getPythonTag('gui_element')
-
- if not gui_element:
- imgui.text("无GUI元素数据")
- return
-
- # GUI类型信息
- gui_type = getattr(gui_element, 'gui_type', 'UNKNOWN')
- imgui.text(f"GUI类型: {gui_type}")
-
- # 基本属性
- if imgui.collapsing_header("基本属性"):
- # 文本内容 (适用于按钮、标签等)
- if hasattr(gui_element, 'text'):
- changed, new_text = imgui.input_text("文本内容", gui_element.text, 256)
- if changed and hasattr(self, 'gui_manager'):
- self.gui_manager.editGUIElement(gui_element, 'text', new_text)
-
- # GUI ID
- gui_id = getattr(gui_element, 'id', '')
- changed, new_id = imgui.input_text("GUI ID", gui_id, 64)
- if changed and hasattr(self, 'gui_manager'):
- gui_element.id = new_id
-
- # 变换属性
- if imgui.collapsing_header("变换属性"):
- # 位置
- pos = gui_element.getPos()
- imgui.text("位置")
-
- if gui_type in ["button", "label", "entry", "2d_image"]:
- # 2D GUI组件使用屏幕坐标
- imgui.text("屏幕坐标")
- logical_x = pos.getX() / 0.1
- logical_z = pos.getZ() / 0.1
-
- changed, new_x = imgui.input_float("X##gui_pos_x", logical_x, 1.0, 10.0, "%.1f")
- if changed and hasattr(self, 'gui_manager'):
- gui_element.setX(new_x * 0.1)
-
- changed, new_z = imgui.input_float("Y##gui_pos_y", logical_z, 1.0, 10.0, "%.1f")
- if changed and hasattr(self, 'gui_manager'):
- gui_element.setZ(new_z * 0.1)
- else:
- # 3D GUI组件使用世界坐标
- imgui.text("世界坐标")
- changed, new_x = imgui.input_float("X##gui_world_x", pos.getX(), 0.1, 1.0, "%.3f")
- if changed and hasattr(self, 'gui_manager'):
- gui_element.setX(new_x)
-
- changed, new_y = imgui.input_float("Y##gui_world_y", pos.getY(), 0.1, 1.0, "%.3f")
- if changed and hasattr(self, 'gui_manager'):
- gui_element.setY(new_y)
-
- changed, new_z = imgui.input_float("Z##gui_world_z", pos.getZ(), 0.1, 1.0, "%.3f")
- if changed and hasattr(self, 'gui_manager'):
- gui_element.setZ(new_z)
-
- # 缩放
- scale = gui_element.getScale()
- imgui.text("缩放")
- changed, new_sx = imgui.input_float("X##gui_scale_x", scale.getX(), 0.1, 1.0, "%.3f")
- if changed and hasattr(self, 'gui_manager'):
- gui_element.setSx(new_sx)
-
- changed, new_sy = imgui.input_float("Y##gui_scale_y", scale.getY(), 0.1, 1.0, "%.3f")
- if changed and hasattr(self, 'gui_manager'):
- gui_element.setSy(new_sy)
-
- changed, new_sz = imgui.input_float("Z##gui_scale_z", scale.getZ(), 0.1, 1.0, "%.3f")
- if changed and hasattr(self, 'gui_manager'):
- gui_element.setSz(new_sz)
-
- # 旋转
- hpr = gui_element.getHpr()
- imgui.text("旋转")
- changed, new_h = imgui.input_float("H##gui_rot_h", hpr.getX(), 1.0, 10.0, "%.1f")
- if changed and hasattr(self, 'gui_manager'):
- gui_element.setH(new_h)
-
- changed, new_p = imgui.input_float("P##gui_rot_p", hpr.getY(), 1.0, 10.0, "%.1f")
- if changed and hasattr(self, 'gui_manager'):
- gui_element.setP(new_p)
-
- changed, new_r = imgui.input_float("R##gui_rot_r", hpr.getZ(), 1.0, 10.0, "%.1f")
- if changed and hasattr(self, 'gui_manager'):
- gui_element.setR(new_r)
-
- # 外观属性
- if imgui.collapsing_header("外观属性"):
- # 大小
- if hasattr(gui_element, 'size'):
- size = gui_element.size
- imgui.text("大小")
- changed, new_w = imgui.input_float("宽度", size[0], 1.0, 10.0, "%.1f")
- if changed and hasattr(self, 'gui_manager'):
- new_size = (new_w, size[1])
- self.gui_manager.editGUIElement(gui_element, 'size', new_size)
-
- changed, new_h = imgui.input_float("高度", size[1], 1.0, 10.0, "%.1f")
- if changed and hasattr(self, 'gui_manager'):
- new_size = (size[0], new_h)
- self.gui_manager.editGUIElement(gui_element, 'size', new_size)
-
- # 颜色
- if hasattr(gui_element, 'getColor'):
- try:
- color = gui_element.getColor()
- # 确保颜色是有效的
- if not color or (hasattr(color, '__len__') and len(color) < 3):
- color = (1.0, 1.0, 1.0, 1.0) # 默认白色
- except:
- color = (1.0, 1.0, 1.0, 1.0) # 默认白色
-
- imgui.text("颜色")
- # 获取颜色值
- if hasattr(color, 'getX'):
- # 如果是Panda3D的Vec4对象
- r, g, b = color.getX(), color.getY(), color.getZ()
- else:
- # 如果是元组或其他格式
- r, g, b = color[0], color[1], color[2]
-
- changed, new_r = imgui.slider_float("R##gui_color_r", r, 0.0, 1.0)
- if changed: gui_element.setColor(new_r, g, b, 1.0)
-
- changed, new_g = imgui.slider_float("G##gui_color_g", g, 0.0, 1.0)
- if changed: gui_element.setColor(r, new_g, b, 1.0)
-
- changed, new_b = imgui.slider_float("B##gui_color_b", b, 0.0, 1.0)
- if changed: gui_element.setColor(r, g, new_b, 1.0)
- # 透明度
- imgui.text("透明度")
- current_alpha = getattr(gui_element, 'alpha', 1.0)
- changed, new_alpha = imgui.slider_float("Alpha", current_alpha, 0.0, 1.0)
- if changed:
- gui_element.alpha = new_alpha
- if hasattr(gui_element, 'setTransparency'):
- # 将0.0-1.0范围转换为Panda3D的透明度格式
- panda_transparency = int((1.0 - new_alpha) * 255)
- gui_element.setTransparency(panda_transparency)
-
- # 渲染顺序
- imgui.text("渲染顺序")
- current_sort = getattr(gui_element, 'sort', 0)
- changed, new_sort = imgui.input_int("Sort Order", current_sort)
- if changed:
- gui_element.sort = new_sort
- if hasattr(gui_element, 'setBin'):
- gui_element.setBin('fixed', new_sort)
-
- # 字体设置(适用于文本类型的GUI元素)
- if gui_type in ["button", "label", "entry"]:
- imgui.text("字体设置")
-
- # 字体选择
- current_font = getattr(gui_element, 'font_path', '')
- if imgui.button(f"字体: {Path(current_font).name if current_font else '默认'}##font_select"):
- self.show_font_selector(
- gui_element,
- 'font_path',
- current_font,
- lambda font_path: self._apply_gui_font(gui_element, font_path)
- )
-
- # 字体大小
- current_size = getattr(gui_element, 'font_size', 12)
- changed, new_size = imgui.slider_float("字体大小", current_size, 8.0, 72.0)
- if changed:
- gui_element.font_size = new_size
- self._apply_gui_font_size(gui_element, new_size)
-
- # 字体样式
- imgui.text("字体样式")
- is_bold = getattr(gui_element, 'font_bold', False)
- changed, new_bold = imgui.checkbox("粗体", is_bold)
- if changed:
- gui_element.font_bold = new_bold
- self._apply_gui_font_style(gui_element)
-
- imgui.same_line()
- is_italic = getattr(gui_element, 'font_italic', False)
- changed, new_italic = imgui.checkbox("斜体", is_italic)
- if changed:
- gui_element.font_italic = new_italic
- self._apply_gui_font_style(gui_element)
-
- def _apply_gui_font(self, gui_element, font_path):
- """应用GUI元素的字体"""
- try:
- if hasattr(gui_element, 'setFont') and font_path:
- gui_element.setFont(font_path)
- gui_element.font_path = font_path
- except Exception as e:
- print(f"应用GUI字体失败: {e}")
-
- def _apply_gui_font_size(self, gui_element, font_size):
- """应用GUI元素的字体大小"""
- try:
- if hasattr(gui_element, 'setFontSize'):
- gui_element.setFontSize(font_size)
- gui_element.font_size = font_size
- except Exception as e:
- print(f"应用GUI字体大小失败: {e}")
-
- def _apply_gui_font_style(self, gui_element):
- """应用GUI元素的字体样式"""
- try:
- if hasattr(gui_element, 'setFontStyle'):
- style = 0
- if getattr(gui_element, 'font_bold', False):
- style |= 1 # 粗体
- if getattr(gui_element, 'font_italic', False):
- style |= 2 # 斜体
- gui_element.setFontStyle(style)
- except Exception as e:
- print(f"应用GUI字体样式失败: {e}")
-
- # 特定类型的属性
- if gui_type == "button":
- if imgui.collapsing_header("按钮属性"):
- # 按钮状态
- is_pressed = getattr(gui_element, 'pressed', False)
- changed, new_pressed = imgui.checkbox("按下状态", is_pressed)
- if changed:
- gui_element.pressed = new_pressed
-
- # 按钮回调
- callback_name = getattr(gui_element, 'callback_name', '')
- changed, new_callback = imgui.input_text("回调函数", callback_name, 64)
- if changed:
- gui_element.callback_name = new_callback
-
- elif gui_type == "entry":
- if imgui.collapsing_header("输入框属性"):
- # 输入框内容
- entry_text = getattr(gui_element, 'entry_text', '')
- changed, new_text = imgui.input_text("输入内容", entry_text, 256)
- if changed:
- gui_element.entry_text = new_text
- if hasattr(gui_element, 'set'):
- gui_element.set(new_text)
-
- # 最大长度
- max_length = getattr(gui_element, 'max_length', 256)
- changed, new_max = imgui.input_int("最大长度", max_length)
- if changed:
- gui_element.max_length = max(max_length, 1)
-
- # 密码模式
- is_password = getattr(gui_element, 'is_password', False)
- changed, new_password = imgui.checkbox("密码模式", is_password)
- if changed:
- gui_element.is_password = new_password
- if hasattr(gui_element, 'obscure'):
- gui_element.obscure(new_password)
-
- elif gui_type in ["2d_image", "3d_image"]:
- if imgui.collapsing_header("图像属性"):
- # 图像路径
- image_path = getattr(gui_element, 'image_path', '')
- changed, new_path = imgui.input_text("图像路径", image_path, 256)
- if changed and hasattr(self, 'gui_manager'):
- gui_element.image_path = new_path
- # TODO: 重新加载图像
-
- # 图像缩放模式
- scale_mode = getattr(gui_element, 'scale_mode', 'stretch')
- if imgui.begin_combo("缩放模式", scale_mode):
- if imgui.selectable("拉伸##stretch"):
- gui_element.scale_mode = 'stretch'
- if imgui.selectable("适应##fit"):
- gui_element.scale_mode = 'fit'
- if imgui.selectable("填充##fill"):
- gui_element.scale_mode = 'fill'
- imgui.end_combo()
-
- def _draw_light_properties(self, node):
- """绘制光源属性"""
- imgui.text("光源属性")
-
- # 光源颜色
- if hasattr(node, 'getColor'):
- try:
- color = node.getColor()
- # 确保颜色是有效的
- if not color or len(color) < 3:
- color = (1.0, 1.0, 1.0, 1.0) # 默认白色
- except:
- color = (1.0, 1.0, 1.0, 1.0) # 默认白色
-
- changed, new_r = imgui.drag_float("颜色 R", color[0], 0.01, 0.0, 1.0)
- if changed: node.setColor(new_r, color[1], color[2], color[3] if len(color) > 3 else 1.0)
-
- changed, new_g = imgui.drag_float("颜色 G", color[1], 0.01, 0.0, 1.0)
- if changed: node.setColor(color[0], new_g, color[2], color[3] if len(color) > 3 else 1.0)
-
- changed, new_b = imgui.drag_float("颜色 B", color[2], 0.01, 0.0, 1.0)
- if changed: node.setColor(color[0], color[1], new_b, color[3] if len(color) > 3 else 1.0)
-
- # 光源强度
- imgui.text("光源强度: (暂不支持编辑)")
-
- def _draw_model_properties(self, node):
- """绘制模型属性"""
- # 获取模型信息
- model_path = node.getTag("model_path") if node.hasTag("model_path") else "未知"
-
- imgui.text("模型路径:")
- imgui.same_line()
- imgui.text_colored((0.7, 0.7, 0.7, 1.0), model_path)
-
- # 模型基本信息
- imgui.text("模型名称:")
- imgui.same_line()
- model_name = node.getName() or "未命名模型"
- imgui.text_colored((0.7, 0.7, 0.7, 1.0), model_name)
-
- # 模型位置信息
- imgui.text("位置:")
- imgui.same_line()
- pos = node.getPos()
- imgui.text_colored((0.7, 0.7, 0.7, 1.0), f"X:{pos.x:.2f} Y:{pos.y:.2f} Z:{pos.z:.2f}")
-
- # 模型缩放信息
- imgui.text("缩放:")
- imgui.same_line()
- scale = node.getScale()
- imgui.text_colored((0.7, 0.7, 0.7, 1.0), f"X:{scale.x:.2f} Y:{scale.y:.2f} Z:{scale.z:.2f}")
-
- # 模型旋转信息
- imgui.text("旋转:")
- imgui.same_line()
- hpr = node.getHpr()
- imgui.text_colored((0.7, 0.7, 0.7, 1.0), f"H:{hpr.x:.1f}° P:{hpr.y:.1f}° R:{hpr.z:.1f}°")
-
- def _draw_animation_properties(self, node):
- """绘制动画控制属性面板(优化版本,使用缓存避免重复计算)"""
- # 检查是否已经缓存了动画信息
- cached_anim_info = node.getPythonTag("cached_anim_info")
- cached_processed_names = node.getPythonTag("cached_processed_names")
-
- # 只有在没有缓存时才进行完整的动画检测和处理
- if cached_anim_info is None or cached_processed_names is None:
- # 获取Actor
- actor = self._getActor(node)
- if not actor:
- imgui.text_colored((0.7, 0.7, 0.7, 1.0), "此模型不包含动画")
- return
-
- # 获取和分析动画名称
- anim_names = actor.getAnimNames()
- processed_names = self._processAnimationNames(node, anim_names)
-
- if not processed_names:
- imgui.text_colored((0.7, 0.7, 0.7, 1.0), "未检测到动画序列")
- # 缓存空结果
- node.setPythonTag("cached_processed_names", [])
- node.setPythonTag("cached_anim_info", "无动画")
- return
-
- # 计算并缓存动画信息
- format_info = self._getModelFormat(node)
- animation_info = self._analyzeAnimationQuality(actor, anim_names, format_info)
- info_text = f"格式: {format_info} | 动画数量: {len(processed_names)}"
- if animation_info:
- info_text += f" | {animation_info}"
-
- # 缓存结果
- node.setPythonTag("cached_anim_info", info_text)
- node.setPythonTag("cached_processed_names", processed_names)
-
- else:
- # 使用缓存的数据
- info_text = cached_anim_info
- processed_names = cached_processed_names
-
- # 如果缓存的空结果,直接返回
- if not processed_names:
- imgui.text_colored((0.7, 0.7, 0.7, 1.0), "未检测到动画序列")
- return
-
- # 显示动画信息(使用缓存的数据)
- imgui.text("信息:")
- imgui.same_line()
- imgui.text_colored((0.7, 0.7, 0.7, 1.0), info_text)
-
- imgui.spacing()
-
- # 动画选择下拉框
- imgui.text("动画名称:")
- imgui.same_line()
-
- # 获取当前选中的动画
- current_anim = node.getPythonTag("selected_animation")
- if current_anim is None:
- current_anim = processed_names[0][1] if processed_names else ""
- node.setPythonTag("selected_animation", current_anim)
-
- # 查找当前动画的索引
- current_index = 0
- for i, (display_name, original_name) in enumerate(processed_names):
- if original_name == current_anim:
- current_index = i
- break
-
- # 创建下拉框选项
- animation_options = [display_name for display_name, _ in processed_names]
- changed, new_index = imgui.combo("##animation_combo", current_index, animation_options)
-
- if changed and new_index < len(processed_names):
- selected_display, selected_original = processed_names[new_index]
- node.setPythonTag("selected_animation", selected_original)
- print(f"选择动画: {selected_display} (原始名称: {selected_original})")
-
- imgui.spacing()
-
- # 控制按钮组
- imgui.text("控制:")
-
- # 播放按钮
- if imgui.button("播放##play_animation"):
- self._playAnimation(node)
- imgui.same_line()
-
- # 暂停按钮
- if imgui.button("暂停##pause_animation"):
- self._pauseAnimation(node)
- imgui.same_line()
-
- # 停止按钮
- if imgui.button("停止##stop_animation"):
- self._stopAnimation(node)
- imgui.same_line()
-
- # 循环按钮
- if imgui.button("循环##loop_animation"):
- self._loopAnimation(node)
-
- imgui.spacing()
-
- # 播放速度控制
- imgui.text("播放速度:")
- imgui.same_line()
-
- # 获取当前速度
- current_speed = node.getPythonTag("anim_speed")
- if current_speed is None:
- current_speed = 1.0
- node.setPythonTag("anim_speed", current_speed)
-
- # 速度滑块
- changed, new_speed = imgui.slider_float("##anim_speed", current_speed, 0.1, 5.0, "%.1f")
- if changed:
- node.setPythonTag("anim_speed", new_speed)
- self._setAnimationSpeed(node, new_speed)
-
- imgui.same_line()
- imgui.text_colored((0.7, 0.7, 0.7, 1.0), "倍速")
-
- def _playAnimation(self, origin_model):
- """播放动画"""
- actor = self._getActor(origin_model)
- if not actor:
- return
+ def _draw_status_badges(self, *args, **kwargs):
+ return self.editor_panels._draw_status_badges(*args, **kwargs)
- # 保存原始世界坐标
- original_world_pos = origin_model.getPos(self.render)
- original_world_hpr = origin_model.getHpr(self.render)
- original_world_scale = origin_model.getScale(self.render)
-
- # 设置Actor位置和姿态
- actor.setPos(origin_model.getPos())
- actor.setHpr(origin_model.getHpr())
- actor.setScale(origin_model.getScale())
+ def _draw_transform_properties(self, *args, **kwargs):
+ return self.editor_panels._draw_transform_properties(*args, **kwargs)
- # 隐藏原始模型,显示Actor
- origin_model.hide()
- actor.show()
+ def _draw_gui_properties(self, *args, **kwargs):
+ return self.editor_panels._draw_gui_properties(*args, **kwargs)
- # 创建任务来维持世界坐标不变
- def maintainWorldPosition(task):
- try:
- if not actor.isEmpty():
- actor.setPos(self.render, original_world_pos)
- actor.setHpr(self.render, original_world_hpr)
- actor.setScale(self.render, original_world_scale)
- return task.cont
- else:
- return task.done
- except:
- return task.done
+ def _draw_light_properties(self, *args, **kwargs):
+ return self.editor_panels._draw_light_properties(*args, **kwargs)
- # 添加维持位置的任务
- taskMgr.add(maintainWorldPosition, f"maintain_anim_pos_{id(actor)}")
+ def _draw_model_properties(self, *args, **kwargs):
+ return self.editor_panels._draw_model_properties(*args, **kwargs)
- # 获取当前选中的动画
- current_anim = origin_model.getPythonTag("selected_animation")
- if current_anim:
- actor.play(current_anim)
- print(f"『动画播放』:{current_anim}")
- else:
- # 兜底:使用第一个可用动画
- anim_names = actor.getAnimNames()
- if anim_names:
- actor.play(anim_names[0])
- print(f"『动画播放』:{anim_names[0]}")
+ def _draw_animation_properties(self, *args, **kwargs):
+ return self.editor_panels._draw_animation_properties(*args, **kwargs)
- def _pauseAnimation(self, origin_model):
- """暂停动画"""
- actor = self._getActor(origin_model)
- if not actor:
- return
-
- # 设置Actor位置和姿态
- actor.setPos(origin_model.getPos())
- actor.setHpr(origin_model.getHpr())
- actor.setScale(origin_model.getScale())
+ def _draw_collision_properties(self, *args, **kwargs):
+ return self.editor_panels._draw_collision_properties(*args, **kwargs)
- # 隐藏原始模型,显示Actor
- origin_model.hide()
- actor.show()
-
- # 停止动画(保持当前姿势)
- actor.stop()
- print("『动画』暂停")
+ def _draw_shape_specific_parameters(self, *args, **kwargs):
+ return self.editor_panels._draw_shape_specific_parameters(*args, **kwargs)
- def _stopAnimation(self, origin_model):
- """停止动画"""
- actor = self._getActor(origin_model)
- if not actor:
- return
-
- # 停止动画
- actor.stop()
+ def _draw_sphere_parameters(self, *args, **kwargs):
+ return self.editor_panels._draw_sphere_parameters(*args, **kwargs)
- # 获取当前选中的动画
- current_anim = origin_model.getPythonTag("selected_animation")
- if current_anim and actor.getAnimControl(current_anim):
- actor.getAnimControl(current_anim).pose(0)
-
- # 隐藏Actor,显示原始模型
- actor.hide()
- origin_model.show()
-
- # 移除维持位置的任务
- taskMgr.remove(f"maintain_anim_pos_{id(actor)}")
-
- print("『动画』停止切换至原始模型")
+ def _draw_box_parameters(self, *args, **kwargs):
+ return self.editor_panels._draw_box_parameters(*args, **kwargs)
- def _loopAnimation(self, origin_model):
- """循环播放动画"""
- actor = self._getActor(origin_model)
- if not actor:
- return
+ def _draw_capsule_parameters(self, *args, **kwargs):
+ return self.editor_panels._draw_capsule_parameters(*args, **kwargs)
- # 保存原始世界坐标
- original_world_pos = origin_model.getPos(self.render)
- original_world_hpr = origin_model.getHpr(self.render)
- original_world_scale = origin_model.getScale(self.render)
-
- # 设置Actor位置和姿态
- actor.setPos(origin_model.getPos())
- actor.setHpr(origin_model.getHpr())
- actor.setScale(origin_model.getScale())
+ def _draw_plane_parameters(self, *args, **kwargs):
+ return self.editor_panels._draw_plane_parameters(*args, **kwargs)
- # 隐藏原始模型,显示Actor
- origin_model.hide()
- actor.show()
+ def _draw_property_actions(self, *args, **kwargs):
+ return self.editor_panels._draw_property_actions(*args, **kwargs)
- # 创建任务来维持世界坐标不变
- def maintainWorldPosition(task):
- try:
- if not actor.isEmpty():
- actor.setPos(self.render, original_world_pos)
- actor.setHpr(self.render, original_world_hpr)
- actor.setScale(self.render, original_world_scale)
- return task.cont
- else:
- return task.done
- except:
- return task.done
+ def _draw_appearance_properties(self, *args, **kwargs):
+ return self.editor_panels._draw_appearance_properties(*args, **kwargs)
- # 添加维持位置的任务
- taskMgr.add(maintainWorldPosition, f"maintain_anim_pos_{id(actor)}")
+ def _draw_material_properties(self, *args, **kwargs):
+ return self.editor_panels._draw_material_properties(*args, **kwargs)
- # 获取当前选中的动画
- current_anim = origin_model.getPythonTag("selected_animation")
- if current_anim:
- actor.loop(current_anim)
- print(f"[动画] 循环: {current_anim}")
- else:
- # 兜底:使用第一个可用动画
- anim_names = actor.getAnimNames()
- if anim_names:
- actor.loop(anim_names[0])
- print(f"[动画] 循环: {anim_names[0]}")
+ def _draw_shading_model_panel(self, *args, **kwargs):
+ return self.editor_panels._draw_shading_model_panel(*args, **kwargs)
- def _setAnimationSpeed(self, origin_model, speed):
- """设置动画播放速度"""
- actor = self._getActor(origin_model)
- if not actor:
- return
-
- # 获取当前选中的动画
- current_anim = origin_model.getPythonTag("selected_animation")
- if current_anim:
- actor.setPlayRate(speed, current_anim)
- print(f"[动画] 速度设为: {speed} ({current_anim})")
- else:
- # 兜底:尝试所有动画
- anim_names = actor.getAnimNames()
- for anim_name in anim_names:
- actor.setPlayRate(speed, anim_name)
- print(f"[动画] 速度设为: {speed} (所有动画)")
-
- def _clear_animation_cache(self, node):
- """清除节点的动画缓存,当模型发生变化时调用"""
- node.setPythonTag("cached_anim_info", None)
- node.setPythonTag("cached_processed_names", None)
- node.setPythonTag("animation", None) # 同时清除动画检测结果
-
- # 如果Actor在缓存中,也需要清理
- if node in self._actor_cache:
- actor = self._actor_cache[node]
- try:
- # 清理相关任务
- taskMgr.remove(f"maintain_anim_pos_{id(actor)}")
- # 清理Actor
- if not actor.isEmpty():
- actor.cleanup()
- actor.removeNode()
- except Exception as e:
- print(f"清理Actor缓存失败: {e}")
- finally:
- del self._actor_cache[node]
- print(f"[缓存清理] 清除节点 {node.getName()} 的动画缓存")
-
- def _draw_collision_properties(self, node):
- """绘制碰撞检测属性"""
- if not node or node.isEmpty():
- return
-
- try:
- # 检查节点是否已有碰撞
- has_collision = self._has_collision(node)
-
- # 碰撞状态徽章
- imgui.text("状态:")
- imgui.same_line()
- if has_collision:
- imgui.text_colored((0.0, 0.8, 0.0, 1.0), "🟢 已启用")
- else:
- imgui.text_colored((0.8, 0.0, 0.0, 1.0), "🔴 未启用")
-
- imgui.separator()
-
- # 碰撞形状选择
- imgui.text("碰撞形状:")
- imgui.same_line()
-
- # 碰撞形状选项
- collision_shapes = ["球形 (Sphere)", "盒型 (Box)", "胶囊体 (Capsule)", "平面 (Plane)", "自动选择 (Auto)"]
-
- # 获取当前形状
- current_shape = self._get_current_collision_shape(node) if has_collision else "球形 (Sphere)"
-
- # 形状选择下拉框
- current_index = collision_shapes.index(current_shape) if current_shape in collision_shapes else 0
- changed, selected_index = imgui.combo("##collision_shape", current_index, collision_shapes)
- if changed:
- # 始终更新选择的形状
- selected_shape = collision_shapes[selected_index]
- self._selected_collision_shape = selected_shape
- # 如果已经有碰撞体,询问用户是否要重新创建
- if has_collision:
- print(f"形状已更改为 {selected_shape},点击'移除碰撞'后'添加碰撞'来应用新形状")
-
- imgui.separator()
-
- # 位置偏移控件
- imgui.text("位置偏移:")
-
- # 获取当前位置偏移
- pos_offset = self._get_collision_position_offset(node)
-
- # X位置
- changed, new_x = imgui.drag_float("X##collision_pos_x", pos_offset[0], 0.1, -100.0, 100.0, "%.2f")
- if changed and has_collision:
- self._update_collision_position(node, 'x', new_x)
-
- # Y位置
- changed, new_y = imgui.drag_float("Y##collision_pos_y", pos_offset[1], 0.1, -100.0, 100.0, "%.2f")
- if changed and has_collision:
- self._update_collision_position(node, 'y', new_y)
-
- # Z位置
- changed, new_z = imgui.drag_float("Z##collision_pos_z", pos_offset[2], 0.1, -100.0, 100.0, "%.2f")
- if changed and has_collision:
- self._update_collision_position(node, 'z', new_z)
-
- # 形状特定参数(始终显示,但根据状态启用/禁用)
- shape_type = self._get_current_collision_shape_type(node)
- self._draw_shape_specific_parameters(node, shape_type, has_collision)
-
- imgui.separator()
-
- # 操作按钮
- if has_collision:
- # 显示/隐藏碰撞体按钮
- is_visible = self._is_collision_visible(node)
- visibility_text = "隐藏碰撞体" if is_visible else "显示碰撞体"
- if imgui.button(visibility_text):
- self._toggle_collision_visibility(node)
-
- imgui.same_line()
-
- # 移除碰撞按钮
- if imgui.button("移除碰撞"):
- self._remove_collision_from_node(node)
- else:
- # 添加碰撞按钮
- if imgui.button("添加碰撞"):
- self._add_collision_to_node(node)
-
- imgui.separator()
-
- # 碰撞检测触发模式
- imgui.text("触发模式:")
-
- # 自动检测开关
- auto_enabled = self.collision_manager.model_collision_enabled if hasattr(self, 'collision_manager') else False
- changed, new_auto = imgui.checkbox("自动检测", auto_enabled)
- if changed and hasattr(self, 'collision_manager'):
- self.collision_manager.enableModelCollisionDetection(new_auto, 0.1, 0.5)
-
- imgui.same_line()
-
- # 手动检测按钮
- if imgui.button("立即检测"):
- if hasattr(self, 'collision_manager'):
- self._manual_collision_detection()
-
- except Exception as e:
- print(f"绘制碰撞属性失败: {e}")
- import traceback
- traceback.print_exc()
-
- def _has_collision(self, node):
- """检查节点是否有碰撞体"""
- try:
- if not node or node.isEmpty():
- return False
-
- # 检查是否有碰撞节点
- for child in node.getChildren():
- if hasattr(child, 'getName') and child.getName():
- name = child.getName()
- if 'collision' in name.lower() or 'Collision' in name:
- return True
-
- return False
- except Exception as e:
- print(f"检查碰撞状态失败: {e}")
- return False
-
- def _get_current_collision_shape(self, node):
- """获取当前碰撞形状"""
- try:
- if not self._has_collision(node):
- return "球形 (Sphere)"
-
- # 查找碰撞节点并判断形状
- for child in node.getChildren():
- if hasattr(child, 'getName') and child.getName():
- name = child.getName()
- if 'collision' in name.lower() or 'Collision' in name:
- # 根据碰撞节点名称判断形状
- if 'sphere' in name.lower():
- return "球形 (Sphere)"
- elif 'box' in name.lower():
- return "盒型 (Box)"
- elif 'capsule' in name.lower():
- return "胶囊体 (Capsule)"
- elif 'plane' in name.lower():
- return "平面 (Plane)"
-
- return "球形 (Sphere)" # 默认
- except Exception as e:
- print(f"获取碰撞形状失败: {e}")
- return "球形 (Sphere)"
-
- def _get_current_collision_shape_type(self, node):
- """获取当前碰撞形状类型(内部标识)"""
- try:
- shape_name = self._get_current_collision_shape(node)
- if "Sphere" in shape_name:
- return "sphere"
- elif "Box" in shape_name:
- return "box"
- elif "Capsule" in shape_name:
- return "capsule"
- elif "Plane" in shape_name:
- return "plane"
- else:
- return "sphere"
- except Exception as e:
- print(f"获取碰撞形状类型失败: {e}")
- return "sphere"
-
- def _get_collision_position_offset(self, node):
- """获取碰撞体位置偏移"""
- try:
- if not self._has_collision(node):
- return (0.0, 0.0, 0.0)
-
- # 查找碰撞节点并获取位置
- for child in node.getChildren():
- if hasattr(child, 'getName') and child.getName():
- name = child.getName()
- if 'collision' in name.lower() or 'Collision' in name:
- pos = child.getPos()
- return (pos.x, pos.y, pos.z)
-
- return (0.0, 0.0, 0.0)
- except Exception as e:
- print(f"获取碰撞位置失败: {e}")
- return (0.0, 0.0, 0.0)
-
- def _is_collision_visible(self, node):
- """检查碰撞体是否可见"""
- try:
- if not self._has_collision(node):
- return False
-
- # 查找碰撞节点并检查可见性
- for child in node.getChildren():
- if hasattr(child, 'getName') and child.getName():
- name = child.getName()
- if 'collision' in name.lower() or 'Collision' in name:
- return child.isHidden() == False
-
- return False
- except Exception as e:
- print(f"检查碰撞可见性失败: {e}")
- return False
-
- def _add_collision_to_node(self, node):
- """为节点添加碰撞体"""
- try:
- if not node or node.isEmpty():
- print("无效的节点")
- return
-
- if self._has_collision(node):
- print("节点已有碰撞体")
- return
-
- # 获取选择的形状
- shape_name = getattr(self, '_selected_collision_shape', '球形 (Sphere)')
-
- if hasattr(self, 'collision_manager'):
- # 使用碰撞管理器添加碰撞体
- shape_type = self._get_shape_type_from_name(shape_name)
- collision_node = self.collision_manager.setupAdvancedCollision(
- node,
- shape_type=shape_type,
- mask_type='MODEL_COLLISION'
- )
-
- if collision_node:
- print(f"成功为节点 {node.getName()} 添加 {shape_name} 碰撞体")
- else:
- print(f"添加碰撞体失败")
- else:
- print("碰撞管理器未初始化")
-
- except Exception as e:
- print(f"添加碰撞体失败: {e}")
- import traceback
- traceback.print_exc()
-
- def _remove_collision_from_node(self, node):
- """从节点移除碰撞体"""
- try:
- if not node or node.isEmpty():
- print("无效的节点")
- return
-
- if not self._has_collision(node):
- print("节点没有碰撞体")
- return
-
- # 查找并移除碰撞节点
- children_to_remove = []
- for child in node.getChildren():
- if hasattr(child, 'getName') and child.getName():
- name = child.getName()
- if 'collision' in name.lower() or 'Collision' in name:
- children_to_remove.append(child)
-
- # 移除找到的碰撞节点
- for child in children_to_remove:
- child.removeNode()
-
- if children_to_remove:
- print(f"成功移除节点 {node.getName()} 的碰撞体")
- else:
- print(f"未找到碰撞体")
-
- except Exception as e:
- print(f"移除碰撞体失败: {e}")
- import traceback
- traceback.print_exc()
-
- def _toggle_collision_visibility(self, node):
- """切换碰撞体可见性"""
- try:
- if not node or node.isEmpty():
- return
-
- # 查找碰撞节点并切换可见性
- for child in node.getChildren():
- if hasattr(child, 'getName') and child.getName():
- name = child.getName()
- if 'collision' in name.lower() or 'Collision' in name:
- if child.isHidden():
- child.show()
- else:
- child.hide()
- break
-
- except Exception as e:
- print(f"切换碰撞可见性失败: {e}")
-
- def _update_collision_position(self, node, axis, value):
- """更新碰撞体位置"""
- try:
- if not node or node.isEmpty():
- return
-
- # 查找碰撞节点并更新位置
- for child in node.getChildren():
- if hasattr(child, 'getName') and child.getName():
- name = child.getName()
- if 'collision' in name.lower() or 'Collision' in name:
- current_pos = child.getPos()
- if axis == 'x':
- child.setPos(value, current_pos.y, current_pos.z)
- elif axis == 'y':
- child.setPos(current_pos.x, value, current_pos.z)
- elif axis == 'z':
- child.setPos(current_pos.x, current_pos.y, value)
- break
-
- except Exception as e:
- print(f"更新碰撞位置失败: {e}")
-
- def _get_shape_type_from_name(self, shape_name):
- """从形状名称获取形状类型"""
- if "Sphere" in shape_name:
- return "sphere"
- elif "Box" in shape_name:
- return "box"
- elif "Capsule" in shape_name:
- return "capsule"
- elif "Plane" in shape_name:
- return "plane"
- else:
- return "sphere"
-
- def _draw_shape_specific_parameters(self, node, shape_type, has_collision=True):
- """绘制形状特定参数"""
- try:
- if shape_type == "sphere":
- self._draw_sphere_parameters(node, has_collision)
- elif shape_type == "box":
- self._draw_box_parameters(node, has_collision)
- elif shape_type == "capsule":
- self._draw_capsule_parameters(node, has_collision)
- elif shape_type == "plane":
- self._draw_plane_parameters(node, has_collision)
- except Exception as e:
- print(f"绘制形状参数失败: {e}")
-
- def _draw_sphere_parameters(self, node, has_collision=True):
- """绘制球形参数"""
- try:
- imgui.text("球形参数:")
- imgui.same_line()
-
- # 获取当前半径
- radius = self._get_sphere_radius(node)
-
- # 半径调整
- changed, new_radius = imgui.drag_float("半径##sphere_radius", radius, 0.1, 0.1, 100.0, "%.2f")
- if changed and has_collision:
- self._update_sphere_radius(node, new_radius)
-
- except Exception as e:
- print(f"绘制球形参数失败: {e}")
-
- def _draw_box_parameters(self, node, has_collision=True):
- """绘制盒型参数"""
- try:
- imgui.text("盒型参数:")
-
- # 获取当前尺寸
- size = self._get_box_size(node)
-
- # 尺寸调整
- changed, new_x = imgui.drag_float("长度##box_length", size[0], 0.1, 0.1, 100.0, "%.2f")
- if changed and has_collision:
- self._update_box_size(node, 'x', new_x)
-
- changed, new_y = imgui.drag_float("宽度##box_width", size[1], 0.1, 0.1, 100.0, "%.2f")
- if changed and has_collision:
- self._update_box_size(node, 'y', new_y)
-
- changed, new_z = imgui.drag_float("高度##box_height", size[2], 0.1, 0.1, 100.0, "%.2f")
- if changed and has_collision:
- self._update_box_size(node, 'z', new_z)
-
- except Exception as e:
- print(f"绘制盒型参数失败: {e}")
-
- def _draw_capsule_parameters(self, node, has_collision=True):
- """绘制胶囊体参数"""
- try:
- imgui.text("胶囊体参数:")
-
- # 获取当前参数
- radius = self._get_capsule_radius(node)
- height = self._get_capsule_height(node)
-
- # 半径调整
- changed, new_radius = imgui.drag_float("半径##capsule_radius", radius, 0.1, 0.1, 100.0, "%.2f")
- if changed and has_collision:
- self._update_capsule_radius(node, new_radius)
-
- # 高度调整
- changed, new_height = imgui.drag_float("高度##capsule_height", height, 0.1, 0.1, 100.0, "%.2f")
- if changed and has_collision:
- self._update_capsule_height(node, new_height)
-
- except Exception as e:
- print(f"绘制胶囊体参数失败: {e}")
-
- def _draw_plane_parameters(self, node, has_collision=True):
- """绘制平面参数"""
- try:
- imgui.text("平面参数:")
-
- # 获取当前法向量
- normal = self._get_plane_normal(node)
-
- # 法向量调整
- changed, new_x = imgui.drag_float("法向量 X##plane_normal_x", normal[0], 0.1, -1.0, 1.0, "%.2f")
- if changed and has_collision:
- self._update_plane_normal(node, 'x', new_x)
-
- changed, new_y = imgui.drag_float("法向量 Y##plane_normal_y", normal[1], 0.1, -1.0, 1.0, "%.2f")
- if changed and has_collision:
- self._update_plane_normal(node, 'y', new_y)
-
- changed, new_z = imgui.drag_float("法向量 Z##plane_normal_z", normal[2], 0.1, -1.0, 1.0, "%.2f")
- if changed and has_collision:
- self._update_plane_normal(node, 'z', new_z)
-
- except Exception as e:
- print(f"绘制平面参数失败: {e}")
-
- def _get_sphere_radius(self, node):
- """获取球形半径"""
- try:
- # 从碰撞节点获取半径信息
- for child in node.getChildren():
- if hasattr(child, 'getName') and child.getName():
- name = child.getName()
- if 'collision' in name.lower() or 'Collision' in name:
- if hasattr(child.node(), 'getSolids') and child.node().getNumSolids() > 0:
- solid = child.node().getSolid(0)
- from panda3d.core import CollisionSphere
- if isinstance(solid, CollisionSphere):
- return solid.getRadius()
- return 1.0
- except Exception as e:
- print(f"获取球形半径失败: {e}")
- return 1.0
-
- def _update_sphere_radius(self, node, radius):
- """更新球形半径"""
- try:
- # 重新创建碰撞体来更新参数
- if hasattr(self, 'collision_manager'):
- # 先移除旧的碰撞体
- self._remove_collision_from_node(node)
- # 重新创建带有新参数的碰撞体
- self.collision_manager.setupAdvancedCollision(
- node,
- shape_type='sphere',
- mask_type='MODEL_COLLISION',
- radius=radius
- )
- print(f"更新球形半径为: {radius}")
- except Exception as e:
- print(f"更新球形半径失败: {e}")
-
- def _get_box_size(self, node):
- """获取盒型尺寸"""
- try:
- # 从碰撞节点获取尺寸信息
- for child in node.getChildren():
- if hasattr(child, 'getName') and child.getName():
- name = child.getName()
- if 'collision' in name.lower() or 'Collision' in name:
- # 尝试从碰撞体获取尺寸
- if hasattr(child.node(), 'getSolids') and child.node().getNumSolids() > 0:
- solid = child.node().getSolid(0)
- from panda3d.core import CollisionBox
- if isinstance(solid, CollisionBox):
- min_p = solid.getMin()
- max_p = solid.getMax()
- return (
- max_p.x - min_p.x,
- max_p.y - min_p.y,
- max_p.z - min_p.z
- )
- return (1.0, 1.0, 1.0)
- except Exception as e:
- print(f"获取盒型尺寸失败: {e}")
- return (1.0, 1.0, 1.0)
-
- def _update_box_size(self, node, axis, value):
- """更新盒型尺寸"""
- try:
- # 获取当前尺寸
- current_size = self._get_box_size(node)
- new_size = list(current_size)
-
- # 更新指定轴的尺寸
- if axis == 'x':
- new_size[0] = value
- elif axis == 'y':
- new_size[1] = value
- elif axis == 'z':
- new_size[2] = value
-
- # 重新创建碰撞体
- if hasattr(self, 'collision_manager'):
- self._remove_collision_from_node(node)
- self.collision_manager.setupAdvancedCollision(
- node,
- shape_type='box',
- mask_type='MODEL_COLLISION',
- width=new_size[0],
- length=new_size[1],
- height=new_size[2]
- )
- print(f"更新盒型尺寸: {new_size}")
- except Exception as e:
- print(f"更新盒型尺寸失败: {e}")
-
- def _get_capsule_radius(self, node):
- """获取胶囊体半径"""
- try:
- # 从碰撞节点获取半径信息
- for child in node.getChildren():
- if hasattr(child, 'getName') and child.getName():
- name = child.getName()
- if 'collision' in name.lower() or 'Collision' in name:
- if hasattr(child.node(), 'getSolids') and child.node().getNumSolids() > 0:
- solid = child.node().getSolid(0)
- from panda3d.core import CollisionCapsule
- if isinstance(solid, CollisionCapsule):
- return solid.getRadius()
- return 1.0
- except Exception as e:
- print(f"获取胶囊体半径失败: {e}")
- return 1.0
-
- def _update_capsule_radius(self, node, radius):
- """更新胶囊体半径"""
- try:
- # 获取当前高度
- height = self._get_capsule_height(node)
-
- # 重新创建碰撞体
- if hasattr(self, 'collision_manager'):
- self._remove_collision_from_node(node)
- self.collision_manager.setupAdvancedCollision(
- node,
- shape_type='capsule',
- mask_type='MODEL_COLLISION',
- radius=radius,
- height=height
- )
- print(f"更新胶囊体半径为: {radius}")
- except Exception as e:
- print(f"更新胶囊体半径失败: {e}")
-
- def _get_capsule_height(self, node):
- """获取胶囊体高度"""
- try:
- # 从碰撞节点获取高度信息
- for child in node.getChildren():
- if hasattr(child, 'getName') and child.getName():
- name = child.getName()
- if 'collision' in name.lower() or 'Collision' in name:
- if hasattr(child.node(), 'getSolids') and child.node().getNumSolids() > 0:
- solid = child.node().getSolid(0)
- from panda3d.core import CollisionCapsule
- if isinstance(solid, CollisionCapsule):
- point_a = solid.getPointA()
- point_b = solid.getPointB()
- return (point_b - point_a).length() + 2 * solid.getRadius()
- return 2.0
- except Exception as e:
- print(f"获取胶囊体高度失败: {e}")
- return 2.0
-
- def _update_capsule_height(self, node, height):
- """更新胶囊体高度"""
- try:
- # 获取当前半径
- radius = self._get_capsule_radius(node)
-
- # 重新创建碰撞体
- if hasattr(self, 'collision_manager'):
- self._remove_collision_from_node(node)
- self.collision_manager.setupAdvancedCollision(
- node,
- shape_type='capsule',
- mask_type='MODEL_COLLISION',
- radius=radius,
- height=height
- )
- print(f"更新胶囊体高度为: {height}")
- except Exception as e:
- print(f"更新胶囊体高度失败: {e}")
-
- def _get_plane_normal(self, node):
- """获取平面法向量"""
- try:
- # 从碰撞节点获取法向量信息
- for child in node.getChildren():
- if hasattr(child, 'getName') and child.getName():
- name = child.getName()
- if 'collision' in name.lower() or 'Collision' in name:
- if hasattr(child.node(), 'getSolids') and child.node().getNumSolids() > 0:
- solid = child.node().getSolid(0)
- from panda3d.core import CollisionPlane
- if isinstance(solid, CollisionPlane):
- plane = solid.getPlane()
- normal = plane.getNormal()
- return (normal.x, normal.y, normal.z)
- return (0.0, 0.0, 1.0)
- except Exception as e:
- print(f"获取平面法向量失败: {e}")
- return (0.0, 0.0, 1.0)
-
- def _update_plane_normal(self, node, axis, value):
- """更新平面法向量"""
- try:
- # 获取当前法向量
- current_normal = self._get_plane_normal(node)
- new_normal = list(current_normal)
-
- # 更新指定轴的值
- if axis == 'x':
- new_normal[0] = value
- elif axis == 'y':
- new_normal[1] = value
- elif axis == 'z':
- new_normal[2] = value
-
- # 标准化法向量
- from panda3d.core import Vec3
- normal_vec = Vec3(*new_normal)
- normal_vec.normalize()
-
- # 重新创建碰撞体
- if hasattr(self, 'collision_manager'):
- self._remove_collision_from_node(node)
- self.collision_manager.setupAdvancedCollision(
- node,
- shape_type='plane',
- mask_type='MODEL_COLLISION',
- normal=normal_vec
- )
- print(f"更新平面法向量为: ({normal_vec.x:.2f}, {normal_vec.y:.2f}, {normal_vec.z:.2f})")
- except Exception as e:
- print(f"更新平面法向量失败: {e}")
-
- def _manual_collision_detection(self):
- """手动执行碰撞检测"""
- try:
- if hasattr(self, 'collision_manager'):
- results = self.collision_manager.detectModelCollisions(log_results=True)
- if results:
- print(f"手动碰撞检测完成,发现 {len(results)} 个碰撞")
- else:
- print("手动碰撞检测完成,未发现碰撞")
- except Exception as e:
- print(f"手动碰撞检测失败: {e}")
- def _draw_property_actions(self, node):
- """绘制属性操作按钮"""
- # 重置变换
- if imgui.button("重置变换"):
- node.setPos(0, 0, 0)
- node.setHpr(0, 0, 0)
- node.setScale(1, 1, 1)
-
- imgui.same_line()
-
- # 切换可见性
- is_visible = not node.is_hidden()
- visibility_text = "隐藏" if is_visible else "显示"
- if imgui.button(visibility_text):
- if is_visible:
- node.hide()
- else:
- node.show()
-
- imgui.same_line()
-
- # 聚焦到对象
- if imgui.button("聚焦"):
- if hasattr(self, 'selection') and self.selection:
- self.selection.focusCameraOnSelectedNodeAdvanced()
-
- # 删除对象
- imgui.same_line()
- if imgui.button("删除"):
- if hasattr(self, 'selection') and self.selection:
- self.selection.deleteSelectedNode()
-
- def _update_node_name(self, node, new_name):
- """更新节点名称"""
- if new_name and new_name != node.getName():
- node.setName(new_name)
- # 更新场景树显示
- if hasattr(self, 'scene_tree'):
- self.scene_tree.refresh()
-
- def _draw_appearance_properties(self, node):
- """绘制外观属性"""
- # 颜色属性
- if hasattr(node, 'getColor'):
- imgui.text("颜色")
- try:
- color = node.getColor()
- # 确保颜色是有效的
- if not color or len(color) < 3:
- color = (1.0, 1.0, 1.0, 1.0) # 默认白色
- except:
- color = (1.0, 1.0, 1.0, 1.0) # 默认白色
-
- # 颜色滑块
- changed, new_r = imgui.slider_float("R##color_r", color[0], 0.0, 1.0)
- if changed:
- new_color = (new_r, color[1], color[2], color[3] if len(color) > 3 else 1.0)
- node.setColor(new_color)
- color = new_color
-
- changed, new_g = imgui.slider_float("G##color_g", color[1], 0.0, 1.0)
- if changed:
- new_color = (color[0], new_g, color[2], color[3] if len(color) > 3 else 1.0)
- node.setColor(new_color)
- color = new_color
-
- changed, new_b = imgui.slider_float("B##color_b", color[2], 0.0, 1.0)
- if changed:
- new_color = (color[0], color[1], new_b, color[3] if len(color) > 3 else 1.0)
- node.setColor(new_color)
- color = new_color
-
- # 只有当颜色有alpha通道时才显示alpha滑块
- if len(color) > 3:
- changed, new_a = imgui.slider_float("A##color_a", color[3], 0.0, 1.0)
- if changed:
- new_color = (color[0], color[1], color[2], new_a)
- node.setColor(new_color)
- color = new_color
-
- # 颜色预览和选择器
- imgui.text("颜色预览")
- color_with_alpha = (color[0], color[1], color[2], color[3] if len(color) > 3 else 1.0)
- if imgui.color_button("颜色预览##preview", color_with_alpha, 0, (100, 30)):
- # 点击颜色按钮打开颜色选择器
- self.show_color_picker(node, 'color', color_with_alpha)
-
- imgui.same_line()
- if imgui.button("选择颜色##color_picker_btn"):
- self.show_color_picker(node, 'color', (color.x, color.y, color.z, color.w))
-
- # 透明度
- if hasattr(node, 'setTransparency') and hasattr(node, 'getTransparency'):
- imgui.text("透明度")
- current_transparency = node.getTransparency()
- # 将当前的透明度值转换为0.0-1.0范围用于显示
- display_transparency = 1.0 - current_transparency if current_transparency <= 1 else 0.0
- changed, new_transparency = imgui.slider_float("透明度", display_transparency, 0.0, 1.0)
- if changed:
- # 将0.0-1.0范围转换回Panda3D的透明度格式
- panda_transparency = int((1.0 - new_transparency) * 255)
- node.setTransparency(panda_transparency)
-
- # 材质属性
- self._draw_material_properties(node)
-
- # 渲染状态
- imgui.text("渲染状态")
- if imgui.button("应用材质"):
- self._apply_material_to_node(node)
-
- imgui.same_line()
- if imgui.button("重置材质"):
- self._reset_material(node)
-
- def _draw_material_properties(self, node):
- """绘制材质属性"""
- materials = node.find_all_materials()
-
- if not materials:
- imgui.text_colored((0.5, 0.5, 0.5, 1.0), "无材质")
- return
-
- for i, material in enumerate(materials):
- material_name = material.get_name() if hasattr(material, 'get_name') and material.get_name() else f"材质{i + 1}"
-
- if imgui.collapsing_header(f"材质: {material_name}"):
- # 材质基础颜色
- base_color = self._get_material_base_color(material)
- if base_color:
- imgui.text("基础颜色")
- changed, new_r = imgui.slider_float(f"R##mat_r_{i}", base_color[0], 0.0, 1.0)
- if changed:
- self._update_material_base_color(material, 'r', new_r)
- base_color = (new_r, base_color[1], base_color[2], base_color[3])
-
- changed, new_g = imgui.slider_float(f"G##mat_g_{i}", base_color[1], 0.0, 1.0)
- if changed:
- self._update_material_base_color(material, 'g', new_g)
- base_color = (base_color[0], new_g, base_color[2], base_color[3])
-
- changed, new_b = imgui.slider_float(f"B##mat_b_{i}", base_color[2], 0.0, 1.0)
- if changed:
- self._update_material_base_color(material, 'b', new_b)
- base_color = (base_color[0], base_color[1], new_b, base_color[3])
-
- changed, new_a = imgui.slider_float(f"A##mat_a_{i}", base_color[3], 0.0, 1.0)
- if changed:
- self._update_material_base_color(material, 'a', new_a)
- base_color = (base_color[0], base_color[1], base_color[2], new_a)
-
- # PBR属性
- if hasattr(material, 'roughness') and material.roughness is not None:
- imgui.text("PBR属性")
- try:
- roughness_value = float(material.roughness)
- changed, new_roughness = imgui.slider_float(f"粗糙度##rough_{i}", roughness_value, 0.0, 1.0)
- if changed:
- self._update_material_roughness(material, new_roughness)
- except:
- imgui.text_colored((0.7, 0.7, 0.7, 1.0), "粗糙度: 不可用")
-
- if hasattr(material, 'metallic') and material.metallic is not None:
- try:
- metallic_value = float(material.metallic)
- changed, new_metallic = imgui.slider_float(f"金属性##metal_{i}", metallic_value, 0.0, 1.0)
- if changed:
- self._update_material_metallic(material, new_metallic)
- except:
- imgui.text_colored((0.7, 0.7, 0.7, 1.0), "金属性: 不可用")
-
- if hasattr(material, 'refractive_index') and material.refractive_index is not None:
- try:
- ior_value = float(material.refractive_index)
- changed, new_ior = imgui.slider_float(f"折射率##ior_{i}", ior_value, 1.0, 3.0)
- if changed:
- self._update_material_ior(material, new_ior)
- except:
- imgui.text_colored((0.7, 0.7, 0.7, 1.0), "折射率: 不可用")
-
- # 材质预设
- imgui.text("材质预设")
- presets = ["默认", "金属", "塑料", "玻璃", "木材", "混凝土"]
- current_preset = 0 # 默认选择
-
- if imgui.begin_combo(f"预设##preset_{i}", presets[current_preset]):
- for j, preset_name in enumerate(presets):
- if imgui.selectable(preset_name, j == current_preset):
- self._apply_material_preset(material, preset_name)
- imgui.end_combo()
-
- # 纹理信息
- imgui.text("纹理贴图")
- if imgui.button(f"选择漫反射贴图##diffuse_{i}"):
- self._select_texture_for_material(node, material, "diffuse")
-
- imgui.same_line()
- if imgui.button(f"选择法线贴图##normal_{i}"):
- self._select_texture_for_material(node, material, "normal")
-
- imgui.same_line()
- if imgui.button(f"选择粗糙度贴图##roughness_{i}"):
- self._select_texture_for_material(node, material, "roughness")
-
- if imgui.button(f"选择金属性贴图##metallic_{i}"):
- self._select_texture_for_material(node, material, "metallic")
-
- imgui.same_line()
- if imgui.button(f"选择自发光贴图##emission_{i}"):
- self._select_texture_for_material(node, material, "emission")
-
- imgui.same_line()
- if imgui.button(f"清除所有贴图##clear_{i}"):
- self._clear_all_textures(node)
-
- # 着色模型选择
- self._draw_shading_model_panel(material, i)
-
- # 显示当前纹理信息
- self._display_current_textures(node, material)
-
- def _get_material_base_color(self, material):
- """获取材质基础颜色"""
- try:
- if hasattr(material, 'base_color') and material.base_color is not None:
- return (material.base_color.x, material.base_color.y, material.base_color.z, material.base_color.w)
- elif hasattr(material, 'get_base_color'):
- color = material.get_base_color()
- return (color.x, color.y, color.z, color.w)
- elif hasattr(material, 'getDiffuse'):
- color = material.getDiffuse()
- return (color.x, color.y, color.z, color.w if hasattr(color, 'w') else 1.0)
- else:
- return (1.0, 1.0, 1.0, 1.0) # 默认白色
- except:
- return (1.0, 1.0, 1.0, 1.0) # 默认白色
-
- def _update_material_base_color(self, material, component, value):
- """更新材质基础颜色"""
- try:
- base_color = self._get_material_base_color(material)
- new_color = list(base_color)
-
- if component == 'r':
- new_color[0] = value
- elif component == 'g':
- new_color[1] = value
- elif component == 'b':
- new_color[2] = value
- elif component == 'a':
- new_color[3] = value
-
- new_color_tuple = tuple(new_color)
-
- if hasattr(material, 'set_base_color'):
- from panda3d.core import Vec4
- material.set_base_color(Vec4(*new_color_tuple))
- elif hasattr(material, 'setDiffuse'):
- from panda3d.core import Vec4
- material.setDiffuse(Vec4(*new_color_tuple))
- except Exception as e:
- print(f"更新材质基础颜色失败: {e}")
-
- def _update_material_roughness(self, material, value):
- """更新材质粗糙度"""
- try:
- if hasattr(material, 'set_roughness'):
- material.set_roughness(value)
- except Exception as e:
- print(f"更新材质粗糙度失败: {e}")
-
- def _update_material_metallic(self, material, value):
- """更新材质金属性"""
- try:
- if hasattr(material, 'set_metallic'):
- material.set_metallic(value)
- except Exception as e:
- print(f"更新材质金属性失败: {e}")
-
- def _update_material_ior(self, material, value):
- """更新材质折射率"""
- try:
- if hasattr(material, 'set_refractive_index'):
- material.set_refractive_index(value)
- except Exception as e:
- print(f"更新材质折射率失败: {e}")
-
- def _apply_material_preset(self, material, preset_name):
- """应用材质预设"""
- try:
- from panda3d.core import Vec4, Material
-
- presets = {
- "默认": {
- "base_color": Vec4(0.8, 0.8, 0.8, 1.0),
- "roughness": 0.5,
- "metallic": 0.0,
- "ior": 1.5
- },
- "金属": {
- "base_color": Vec4(0.7, 0.7, 0.8, 1.0),
- "roughness": 0.2,
- "metallic": 1.0,
- "ior": 2.5
- },
- "塑料": {
- "base_color": Vec4(0.9, 0.9, 0.9, 1.0),
- "roughness": 0.8,
- "metallic": 0.0,
- "ior": 1.45
- },
- "玻璃": {
- "base_color": Vec4(0.9, 0.9, 1.0, 0.2),
- "roughness": 0.0,
- "metallic": 0.0,
- "ior": 1.5
- },
- "木材": {
- "base_color": Vec4(0.6, 0.4, 0.2, 1.0),
- "roughness": 0.9,
- "metallic": 0.0,
- "ior": 1.55
- },
- "混凝土": {
- "base_color": Vec4(0.5, 0.5, 0.5, 1.0),
- "roughness": 1.0,
- "metallic": 0.0,
- "ior": 1.5
- }
- }
-
- if preset_name in presets:
- preset = presets[preset_name]
-
- # 应用基础颜色
- if hasattr(material, 'set_base_color'):
- material.set_base_color(preset["base_color"])
- elif hasattr(material, 'setDiffuse'):
- material.setDiffuse(preset["base_color"])
-
- # 应用PBR属性
- if hasattr(material, 'set_roughness'):
- material.set_roughness(preset["roughness"])
- if hasattr(material, 'set_metallic'):
- material.set_metallic(preset["metallic"])
- if hasattr(material, 'set_refractive_index'):
- material.set_refractive_index(preset["ior"])
-
- print(f"已应用材质预设: {preset_name}")
- except Exception as e:
- print(f"应用材质预设失败: {e}")
-
- def _apply_material_to_node(self, node):
- """为节点应用材质"""
- try:
- from panda3d.core import Material, Vec4
-
- # 检查是否已有材质
- materials = node.find_all_materials()
-
- if not materials:
- # 创建新材质
- material = Material(f"default-material-{node.getName()}")
- material.setBaseColor(Vec4(0.8, 0.8, 0.8, 1.0))
- material.setDiffuse(Vec4(0.8, 0.8, 0.8, 1.0))
- material.setAmbient(Vec4(0.4, 0.4, 0.4, 1.0))
- material.setSpecular(Vec4(0.1, 0.1, 0.1, 1.0))
- material.setShininess(10.0)
- node.setMaterial(material, 1)
- print(f"已为新节点创建默认材质")
- else:
- print(f"节点已有 {len(materials)} 个材质")
- except Exception as e:
- print(f"应用材质失败: {e}")
-
- def _reset_material(self, node):
- """重置节点材质"""
- try:
- materials = node.find_all_materials()
-
- for material in materials:
- # 重置为默认材质属性
- from panda3d.core import Vec4
- default_color = Vec4(0.8, 0.8, 0.8, 1.0)
-
- if hasattr(material, 'set_base_color'):
- material.set_base_color(default_color)
- elif hasattr(material, 'setDiffuse'):
- material.setDiffuse(default_color)
-
- if hasattr(material, 'set_roughness'):
- material.set_roughness(0.5)
- if hasattr(material, 'set_metallic'):
- material.set_metallic(0.0)
- if hasattr(material, 'set_refractive_index'):
- material.set_refractive_index(1.5)
-
- print(f"已重置材质")
- except Exception as e:
- print(f"重置材质失败: {e}")
-
- def _select_texture_for_material(self, node, material, texture_type):
- """为材质选择纹理"""
- try:
- # 设置当前纹理对话框状态
- self._current_texture_dialog = {
- 'node': node,
- 'material': material,
- 'texture_type': texture_type
- }
-
- # 初始化路径
- if not hasattr(self, '_texture_dialog_path'):
- self._texture_dialog_path = '/home/hello/EG/Resources'
-
- # 设置文件类型过滤
- self._texture_dialog_filter = "*.png"
-
- except Exception as e:
- print(f"选择纹理失败: {e}")
-
- def _apply_texture_to_material(self, node, material, texture_type, texture_path):
- """应用纹理到材质"""
- try:
- from panda3d.core import TextureStage
- from direct.showbase import Loader
-
- # 加载纹理
- loader = Loader.Loader(self)
- texture = loader.loadTexture(texture_path)
-
- if not texture:
- print(f"无法加载纹理: {texture_path}")
- return
-
- # 设置纹理属性
- texture.setMagfilter(texture.FTLinear)
- texture.setMinfilter(texture.FTLinearMipmapLinear)
-
- # 纹理槽位映射
- texture_slots = {
- "diffuse": 0, # p3d_Texture0
- "ior": 2, # p3d_Texture2
- "normal": 1, # p3d_Texture1
- "roughness": 3, # p3d_Texture3
- "parallax": 4, # p3d_Texture4
- "metallic": 5, # p3d_Texture5
- "emission": 6, # p3d_Texture6
- "ao": 7, # p3d_Texture7
- "alpha": 8, # p3d_Texture8
- "detail": 9, # p3d_Texture9
- "gloss": 10 # p3d_Texture10
- }
-
- slot = texture_slots.get(texture_type, 0)
-
- # 创建纹理阶段
- texture_stage = TextureStage(f"{texture_type}_map")
- texture_stage.setSort(slot)
- texture_stage.setMode(TextureStage.MModulate)
-
- # 应用纹理到节点
- node.setTexture(texture_stage, texture)
-
- print(f"已应用{texture_type}纹理: {texture_path}")
-
- except Exception as e:
- print(f"应用纹理失败: {e}")
-
- def _clear_all_textures(self, node):
- """清除节点所有纹理"""
- try:
- # 清除所有纹理阶段
- node.clearTexture()
- node.clearTexture()
- print("已清除所有纹理")
- except Exception as e:
- print(f"清除纹理失败: {e}")
-
- def _display_current_textures(self, node, material):
- """显示当前纹理信息"""
- try:
- from panda3d.core import TextureStage
-
- # 获取所有纹理阶段
- texture_stages = node.findAllTextureStages()
-
- if not texture_stages:
- imgui.text_colored((0.7, 0.7, 0.7, 1.0), "当前无纹理")
- return
-
- imgui.text("当前纹理:")
- for stage in texture_stages:
- texture = node.getTexture(stage)
- if texture:
- texture_name = texture.getName() or "未命名"
- stage_name = stage.getName() or "未命名"
- imgui.text(f" {stage_name}: {texture_name}")
- except Exception as e:
- print(f"显示纹理信息失败: {e}")
-
- def _draw_shading_model_panel(self, material, material_index):
- """绘制着色模型选择面板"""
- try:
- imgui.text("着色模型")
-
- # RenderPipeline支持的着色模型
- shading_models = ["默认", "自发光", "透明"]
- current_model = 0 # 默认选择
-
- # 安全地获取当前着色模型
- try:
- if hasattr(material, 'emission') and material.emission is not None:
- current_model = int(material.emission.x)
- except:
- current_model = 0
-
- # 着色模型选择
- if imgui.begin_combo(f"着色模型##shading_{material_index}", shading_models[current_model]):
- for j, model_name in enumerate(shading_models):
- if imgui.selectable(model_name, j == current_model):
- self._update_shading_model(material, j)
- imgui.end_combo()
-
- # 如果是透明着色模型,添加透明度控制
- if current_model == 3: # 透明着色模型
- imgui.text("透明度设置")
- try:
- if hasattr(material, 'shading_model_param0'):
- current_opacity = material.shading_model_param0
- else:
- current_opacity = 1.0
-
- changed, new_opacity = imgui.slider_float(f"不透明度##opacity_{material_index}", current_opacity, 0.0, 1.0)
- if changed:
- self._update_transparency(material, new_opacity)
- except:
- imgui.text_colored((0.7, 0.7, 0.7, 1.0), "透明度控制不可用")
-
- except Exception as e:
- print(f"绘制着色模型面板失败: {e}")
-
- def _update_shading_model(self, material, model_index):
- """更新着色模型"""
- try:
- from panda3d.core import Vec4
-
- # 根据不同的着色模型设置相应的参数
- if model_index == 1: # 自发光着色模型
- print("设置自发光着色模型...")
- if hasattr(material, 'set_emission'):
- current_emission = material.emission or Vec4(0, 0, 0, 0)
- new_emission = Vec4(1.0, current_emission.y, current_emission.z, current_emission.w)
- material.set_emission(new_emission)
- print("自发光着色模型设置完成")
-
- elif model_index == 3: # 透明着色模型
- print("设置透明着色模型...")
- if hasattr(material, 'set_emission'):
- current_emission = material.emission or Vec4(0, 0, 0, 0)
- new_emission = Vec4(3.0, 0, 0, 0) # 3表示透明着色模型
- material.set_emission(new_emission)
-
- # 设置默认透明度
- if hasattr(material, 'shading_model_param0'):
- material.shading_model_param0 = 0.8 # 默认80%不透明度
-
- print("透明着色模型设置完成")
-
- else: # 默认着色模型
- print("设置默认着色模型...")
- if hasattr(material, 'set_emission'):
- current_emission = material.emission or Vec4(0, 0, 0, 0)
- new_emission = Vec4(0.0, current_emission.y, current_emission.z, current_emission.w)
- material.set_emission(new_emission)
- print("默认着色模型设置完成")
-
- print(f"着色模型已更新为: {model_index} ({'自发光' if model_index == 1 else '透明' if model_index == 3 else '默认'})")
-
- except Exception as e:
- print(f"更新着色模型失败: {e}")
-
- def _update_transparency(self, material, opacity_value):
- """更新透明度"""
- try:
- if hasattr(material, 'shading_model_param0'):
- material.shading_model_param0 = opacity_value
- print(f"透明度已更新: {opacity_value}")
- except Exception as e:
- print(f"更新透明度失败: {e}")
-
- def _draw_texture_file_dialog(self):
- """绘制纹理文件选择对话框"""
- if not hasattr(self, '_current_texture_dialog') or not self._current_texture_dialog:
- return
-
- try:
- dialog_data = self._current_texture_dialog
- node = dialog_data['node']
- material = dialog_data['material']
- texture_type = dialog_data['texture_type']
-
- # 设置对话框标志
- flags = (imgui.WindowFlags_.no_resize |
- imgui.WindowFlags_.no_collapse |
- imgui.WindowFlags_.modal)
-
- # 获取屏幕尺寸,居中显示对话框
- display_size = imgui.get_io().display_size
- dialog_width = 600
- dialog_height = 400
- imgui.set_next_window_size((dialog_width, dialog_height))
- imgui.set_next_window_pos(
- ((display_size.x - dialog_width) / 2, (display_size.y - dialog_height) / 2)
- )
-
- # 显示文件选择对话框
- opened, window_open = imgui.begin(f"选择{texture_type}纹理文件##texture_dialog", True, flags)
- if not window_open:
- self._current_texture_dialog = None
- imgui.end()
- return
-
- imgui.text(f"选择{texture_type}纹理文件")
- imgui.separator()
-
- # 当前路径显示
- current_path = getattr(self, '_texture_dialog_path', '/home/hello/EG/Resources')
- imgui.text(f"当前路径: {current_path}")
-
- imgui.separator()
-
- # 文件类型过滤
- imgui.text("支持的纹理格式:")
- file_types = ["*.png", "*.jpg", "*.jpeg", "*.bmp", "*.tga", "*.dds"]
-
- current_filter = getattr(self, '_texture_dialog_filter', "*.png")
- if imgui.begin_combo("文件类型##texture_filter", current_filter):
- for i, file_type in enumerate(file_types):
- if imgui.selectable(file_type, i == file_types.index(current_filter)):
- self._texture_dialog_filter = file_type
- imgui.end_combo()
-
- imgui.separator()
-
- # 路径导航按钮
- if imgui.button("上级目录##up_dir"):
- current_path = os.path.dirname(current_path)
- self._texture_dialog_path = current_path
-
- imgui.same_line()
- if imgui.button("主目录##home_dir"):
- self._texture_dialog_path = '/home/hello/EG/Resources'
-
- imgui.same_line()
- if imgui.button("当前目录##current_dir"):
- self._texture_dialog_path = '/home/hello/EG/Resources'
-
- imgui.same_line()
- if imgui.button("纹理目录##textures_dir"):
- self._texture_dialog_path = '/home/hello/EG/Resources/textures'
-
- imgui.separator()
-
- # 文件列表
- if imgui.begin_child("file_list##texture_files", (580, 200)):
- try:
- # 列出目录和文件
- items = []
- if os.path.exists(current_path):
- for item in os.listdir(current_path):
- item_path = os.path.join(current_path, item)
- if os.path.isdir(item_path):
- items.append(('dir', item, item_path))
- elif any(item.lower().endswith(ext[1:]) for ext in file_types):
- items.append(('file', item, item_path))
-
- # 排序:目录在前,文件在后
- items.sort(key=lambda x: (x[0], x[1].lower()))
-
- for item_type, item_name, item_path in items:
- if item_type == 'dir':
- if imgui.selectable(f"📁 {item_name}##dir_{item_name}", False)[0]:
- self._texture_dialog_path = item_path
- else:
- selected, _ = imgui.selectable(f"📄 {item_name}##file_{item_name}", False)
- if selected:
- # 应用选择的纹理
- self._apply_texture_to_material(node, material, texture_type, item_path)
- # 关闭对话框
- self._current_texture_dialog = None
- break
-
- except Exception as e:
- imgui.text_colored((1.0, 0.5, 0.5, 1.0), f"读取目录失败: {e}")
-
- imgui.end_child()
-
- imgui.separator()
-
- # 路径输入框
- changed, new_path = imgui.input_text("文件路径##texture_path", current_path, 512)
- if changed:
- self._texture_dialog_path = new_path
-
- imgui.same_line()
- if imgui.button("确认路径##confirm_path"):
- if os.path.isfile(new_path):
- # 检查文件扩展名
- file_ext = os.path.splitext(new_path)[1].lower()
- if file_ext in [ext[1:] for ext in file_types]:
- self._apply_texture_to_material(node, material, texture_type, new_path)
- self._current_texture_dialog = None
- else:
- print(f"不支持的文件格式: {file_ext}")
- else:
- print("请选择有效的文件")
-
- imgui.separator()
-
- # 按钮
- if imgui.button("取消##cancel_texture"):
- self._current_texture_dialog = None
-
- imgui.end()
-
- except Exception as e:
- print(f"绘制纹理对话框失败: {e}")
- # 确保在异常情况下也调用 imgui.end()
- try:
- imgui.end()
- except:
- pass
+ def _apply_gui_font(self, *args, **kwargs):
+ return self.property_helpers._apply_gui_font(*args, **kwargs)
- def start_transform_monitoring(self, node):
- """开始变换监控"""
- if node and not node.isEmpty():
- self._monitored_node = node
- self._transform_monitoring = True
- self._transform_update_timer = 0
-
- # 记录初始变换值
- self._update_last_transform_values()
-
- def stop_transform_monitoring(self):
- """停止变换监控"""
- self._transform_monitoring = False
- self._monitored_node = None
- self._last_transform_values = {}
-
- def _update_last_transform_values(self):
- """更新最后记录的变换值"""
- if self._monitored_node and not self._monitored_node.isEmpty():
- try:
- pos = self._monitored_node.getPos()
- hpr = self._monitored_node.getHpr()
- scale = self._monitored_node.getScale()
-
- self._last_transform_values = {
- 'pos': (pos.x, pos.y, pos.z),
- 'hpr': (hpr.x, hpr.y, hpr.z),
- 'scale': (scale.x, scale.y, scale.z)
- }
- except Exception as e:
- print(f"更新变换值失败: {e}")
-
- def _check_transform_changes(self):
- """检查变换变化"""
- if not self._transform_monitoring or not self._monitored_node:
- return
-
- try:
- pos = self._monitored_node.getPos()
- hpr = self._monitored_node.getHpr()
- scale = self._monitored_node.getScale()
-
- current_values = {
- 'pos': (pos.x, pos.y, pos.z),
- 'hpr': (hpr.x, hpr.y, hpr.z),
- 'scale': (scale.x, scale.y, scale.z)
- }
-
- # 检查是否有变化
- if current_values != self._last_transform_values:
- # 更新记录的值
- self._last_transform_values = current_values
- # 触发属性面板更新(通过设置更新标志)
- self.property_panel_update_timer = 0
-
- except Exception as e:
- print(f"检查变换变化失败: {e}")
-
- def update_transform_monitoring(self, dt):
- """更新变换监控(在主循环中调用)"""
- if not self._transform_monitoring:
- return
-
- self._transform_update_timer += dt
- if self._transform_update_timer >= self._transform_update_interval:
- self._transform_update_timer = 0
- self._check_transform_changes()
-
- def show_color_picker(self, target_object, property_name, initial_color, callback=None):
- """显示颜色选择器"""
- self._color_picker_active = True
- self._color_picker_target = (target_object, property_name)
- self._color_picker_current_color = initial_color
- self._color_picker_callback = callback
-
- def _draw_color_picker(self):
- """绘制颜色选择器对话框"""
- if not self._color_picker_active:
- return
-
- # 设置对话框标志
- flags = (imgui.WindowFlags_.no_resize |
- imgui.WindowFlags_.no_collapse |
- imgui.WindowFlags_.modal)
-
- # 获取屏幕尺寸,居中显示对话框
- display_size = imgui.get_io().display_size
- dialog_width = 300
- dialog_height = 400
- imgui.set_next_window_size((dialog_width, dialog_height))
- imgui.set_next_window_pos(
- ((display_size.x - dialog_width) / 2, (display_size.y - dialog_height) / 2)
- )
-
- with imgui_ctx.begin("颜色选择器", True, flags) as window:
- if not window.opened:
- self._color_picker_active = False
- self._color_picker_target = None
- return
-
- imgui.text("选择颜色")
- imgui.separator()
-
- # 颜色编辑器
- changed, new_color = imgui.color_edit4(
- "颜色##color_picker",
- self._color_picker_current_color
- )
-
- if changed:
- self._color_picker_current_color = new_color
-
- # 预设颜色
- imgui.text("预设颜色")
- preset_colors = [
- (1.0, 0.0, 0.0, 1.0), # 红色
- (0.0, 1.0, 0.0, 1.0), # 绿色
- (0.0, 0.0, 1.0, 1.0), # 蓝色
- (1.0, 1.0, 0.0, 1.0), # 黄色
- (1.0, 0.0, 1.0, 1.0), # 洋红
- (0.0, 1.0, 1.0, 1.0), # 青色
- (1.0, 1.0, 1.0, 1.0), # 白色
- (0.0, 0.0, 0.0, 1.0), # 黑色
- (0.5, 0.5, 0.5, 1.0), # 灰色
- (0.188, 0.404, 0.753, 1.0), # 主题蓝色
- (0.176, 1.0, 0.769, 1.0), # 主题绿色
- (0.953, 0.616, 0.471, 1.0), # 主题橙色
- ]
-
- # 绘制预设颜色按钮
- colors_per_row = 6
- for i, color in enumerate(preset_colors):
- if i % colors_per_row != 0:
- imgui.same_line()
-
- imgui.color_button(f"preset_{i}", color, 0, (30, 30))
- if imgui.is_item_clicked():
- self._color_picker_current_color = color
-
- imgui.separator()
-
- # 按钮区域
- if imgui.button("确定"):
- self._apply_color_selection()
- self._color_picker_active = False
- self._color_picker_target = None
-
- imgui.same_line()
- if imgui.button("取消"):
- self._color_picker_active = False
- self._color_picker_target = None
-
- def _apply_color_selection(self):
- """应用颜色选择"""
- if not self._color_picker_target:
- return
-
- target_object, property_name = self._color_picker_target
-
- try:
- # 应用颜色到目标对象
- if hasattr(target_object, 'setColor'):
- target_object.setColor(self._color_picker_current_color)
- elif hasattr(target_object, property_name):
- setattr(target_object, property_name, self._color_picker_current_color)
-
- # 调用回调函数
- if self._color_picker_callback:
- self._color_picker_callback(self._color_picker_current_color)
-
- except Exception as e:
- print(f"应用颜色失败: {e}")
-
- def _draw_color_button(self, label, color, size=(50, 20)):
- """绘制颜色按钮并支持点击打开颜色选择器"""
- imgui.color_button(label, color, 0, size)
- if imgui.is_item_clicked():
- # 打开颜色选择器
- self.show_color_picker(None, None, color)
-
- def _refresh_available_fonts(self):
- """刷新可用字体列表"""
- try:
- import platform
- from pathlib import Path
-
- system = platform.system().lower()
- font_paths = []
-
- if system == "linux":
- font_dirs = [
- "/usr/share/fonts/truetype/",
- "/usr/share/fonts/opentype/",
- "/usr/local/share/fonts/",
- "~/.fonts/"
- ]
- elif system == "windows":
- font_dirs = [
- "C:/Windows/Fonts/",
- ]
- elif system == "darwin":
- font_dirs = [
- "/System/Library/Fonts/",
- "/Library/Fonts/",
- "~/Library/Fonts/"
- ]
- else:
- font_dirs = []
-
- # 扫描字体目录
- common_fonts = []
- for font_dir in font_dirs:
- font_path = Path(font_dir).expanduser()
- if font_path.exists():
- for font_file in font_path.rglob("*.ttf"):
- common_fonts.append(str(font_file))
- for font_file in font_path.rglob("*.otf"):
- common_fonts.append(str(font_file))
- for font_file in font_path.rglob("*.ttc"):
- common_fonts.append(str(font_file))
-
- # 过滤常见字体
- font_keywords = [
- "arial", "helvetica", "times", "courier", "verdana", "georgia",
- "comic", "impact", "trebuchet", "palatino", "garamond",
- "noto", "dejavu", "liberation", "ubuntu", "roboto", "open",
- "droid", "source", "wenquanyi", "wqy", "pingfang", "stheiti",
- "microsoft", "msyh", "simsun", "simhei", "kaiti", "fangsong"
- ]
-
- self._available_fonts = []
- for font_path in common_fonts:
- font_name = Path(font_path).name.lower()
- if any(keyword in font_name for keyword in font_keywords):
- self._available_fonts.append(font_path)
-
- # 添加一些默认字体路径
- default_fonts = [
- "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf",
- "/usr/share/fonts/truetype/liberation/LiberationSans-Regular.ttf",
- "/usr/share/fonts/opentype/noto/NotoSans-Regular.ttf",
- "C:/Windows/Fonts/arial.ttf",
- "C:/Windows/Fonts/msyh.ttc",
- "/System/Library/Fonts/PingFang.ttc"
- ]
-
- for font_path in default_fonts:
- if Path(font_path).exists() and font_path not in self._available_fonts:
- self._available_fonts.append(font_path)
-
- print(f"✓ 找到 {len(self._available_fonts)} 个可用字体")
-
- except Exception as e:
- print(f"⚠ 字体扫描失败: {e}")
- self._available_fonts = []
-
- def show_font_selector(self, target_object, property_name, current_font, callback=None):
- """显示字体选择器"""
- self._font_selector_active = True
- self._font_selector_target = (target_object, property_name)
- self._font_selector_current_font = current_font or ""
- self._font_selector_callback = callback
-
- def _draw_font_selector(self):
- """绘制字体选择器对话框"""
- if not self._font_selector_active:
- return
-
- # 设置对话框标志
- flags = (imgui.WindowFlags_.no_resize |
- imgui.WindowFlags_.no_collapse |
- imgui.WindowFlags_.modal)
-
- # 获取屏幕尺寸,居中显示对话框
- display_size = imgui.get_io().display_size
- dialog_width = 400
- dialog_height = 500
- imgui.set_next_window_size((dialog_width, dialog_height))
- imgui.set_next_window_pos(
- ((display_size.x - dialog_width) / 2, (display_size.y - dialog_height) / 2)
- )
-
- with imgui_ctx.begin("字体选择器", True, flags) as window:
- if not window.opened:
- self._font_selector_active = False
- self._font_selector_target = None
- return
-
- imgui.text("选择字体")
- imgui.separator()
-
- # 当前字体显示
- imgui.text(f"当前字体: {self._font_selector_current_font or '默认'}")
-
- # 字体搜索框
- changed, search_text = imgui.input_text("搜索", "", 256)
- imgui.separator()
-
- # 字体列表
- if imgui.begin_child("font_list", (380, 300)):
- for font_path in self._available_fonts:
- font_name = Path(font_path).name
-
- # 搜索过滤
- if search_text and search_text.lower() not in font_name.lower():
- continue
-
- # 字体项
- if imgui.selectable(font_name, font_path == self._font_selector_current_font):
- self._font_selector_current_font = font_path
-
- # 显示字体路径作为工具提示
- if imgui.is_item_hovered():
- imgui.set_tooltip(font_path)
-
- imgui.end_child()
-
- imgui.separator()
-
- # 按钮区域
- if imgui.button("确定"):
- self._apply_font_selection()
- self._font_selector_active = False
- self._font_selector_target = None
-
- imgui.same_line()
- if imgui.button("取消"):
- self._font_selector_active = False
- self._font_selector_target = None
-
- imgui.same_line()
- if imgui.button("刷新字体"):
- self._refresh_available_fonts()
-
- def _apply_font_selection(self):
- """应用字体选择"""
- if not self._font_selector_target:
- return
-
- target_object, property_name = self._font_selector_target
-
- try:
- # 应用字体到目标对象
- if hasattr(target_object, property_name):
- setattr(target_object, property_name, self._font_selector_current_font)
-
- # 调用回调函数
- if self._font_selector_callback:
- self._font_selector_callback(self._font_selector_current_font)
-
- except Exception as e:
- print(f"应用字体失败: {e}")
-
- def _draw_font_selector_button(self, label, current_font):
- """绘制字体选择器按钮"""
- font_name = Path(current_font).name if current_font else "默认字体"
- display_text = f"{font_name[:20]}..." if len(font_name) > 20 else font_name
-
- if imgui.button(f"{label}: {display_text}##font_selector"):
- self.show_font_selector(None, None, current_font)
-
- def _draw_console(self):
- """绘制控制台面板"""
- # 使用面板类型的窗口标志,支持docking
- flags = self.style_manager.get_window_flags("panel")
-
- with self.style_manager.begin_styled_window("控制台", self.showConsole, flags):
- self.showConsole = True # 确保窗口保持打开
-
- imgui.text("控制台输出")
- imgui.separator()
-
- # 显示消息系统中的消息
- if hasattr(self, 'messages') and self.messages:
- for message in self.messages:
- # 显示时间戳
- imgui.text_colored((0.7, 0.7, 0.7, 1.0), f"[{message['timestamp']}]")
- imgui.same_line()
-
- # 根据消息类型显示图标
- if message['text'].startswith('✓'):
- if self.icons.get('success'):
- imgui.image(self.icons['success'], (12, 12))
- imgui.same_line()
- elif message['text'].startswith('✗'):
- if self.icons.get('delete_fail_icon'):
- imgui.image(self.icons['delete_fail_icon'], (12, 12))
- imgui.same_line()
- elif message['text'].startswith('⚠'):
- if self.icons.get('warning'):
- imgui.image(self.icons['warning'], (12, 12))
- imgui.same_line()
-
- # 显示消息文本
- imgui.text_colored(message['color'], message['text'])
- else:
- # 默认消息
- imgui.text_colored((0.157, 0.620, 1.0, 1.0), "[系统]")
- imgui.same_line()
- imgui.text("引擎已就绪")
-
- # 输入框
- imgui.separator()
- changed, command = imgui.input_text(">", "", 256)
- if changed and command:
- self.add_info_message(f"执行命令: {command}")
- # TODO: 实现命令执行逻辑
-
- imgui.separator()
-
- # 视角控制信息
- imgui.text("视角控制:")
- imgui.text(" WASD - 移动")
- imgui.text(" Q/E - 上下")
- imgui.text(" 右键拖拽 - 旋转视角")
- imgui.text(" 滚轮 - 前进/后退")
-
- # 相机位置信息
- cam_pos = self.camera.getPos()
- cam_hpr = self.camera.getHpr()
- imgui.text(f"位置: X={cam_pos.x:.1f}, Y={cam_pos.y:.1f}, Z={cam_pos.z:.1f}")
- imgui.text(f"旋转: H={cam_hpr.x:.1f}, P={cam_hpr.y:.1f}, R={cam_hpr.z:.1f}")
-
- # 控制状态
- imgui.checkbox("启用视角控制", self.camera_control_enabled)
-
- # 重置按钮
- if imgui.button("重置相机"):
- self.camera.setPos(0, -20, 5)
- self.camera.setHpr(0, 0, 0)
- self.add_info_message("相机位置已重置")
-
- def _draw_script_panel(self):
- """绘制脚本管理面板(与Qt版本功能一致)"""
- # 使用面板类型的窗口标志,支持docking
- flags = self.style_manager.get_window_flags("panel")
-
- with self.style_manager.begin_styled_window("脚本管理", self.showScriptPanel, flags):
- self.showScriptPanel = True # 确保窗口保持打开
-
- # 1. 脚本系统状态组
- self._draw_script_status_group()
-
- imgui.spacing()
-
- # 2. 创建脚本组
- self._draw_create_script_group()
-
- imgui.spacing()
-
- # 3. 可用脚本组
- self._draw_available_scripts_group()
-
- imgui.spacing()
-
- # 4. 脚本挂载组
- self._draw_script_mounting_group()
-
- def _draw_script_status_group(self):
- """绘制脚本系统状态组"""
- if imgui.collapsing_header("脚本系统状态", imgui.TreeNodeFlags_.default_open):
- # 脚本系统状态
- imgui.text("脚本引擎状态:")
- imgui.same_line()
-
- # 检查脚本管理器是否正常工作
- if hasattr(self, 'script_manager') and self.script_manager:
- if hasattr(self.script_manager, 'engine') and self.script_manager.engine:
- imgui.text_colored((0.0, 1.0, 0.0, 1.0), "✓ 已启动")
- else:
- imgui.text_colored((1.0, 0.5, 0.0, 1.0), "⚠ 引擎未初始化")
- else:
- imgui.text_colored((1.0, 0.0, 0.0, 1.0), "✗ 未启动")
-
- # 热重载状态
- imgui.text("热重载状态:")
- imgui.same_line()
-
- hot_reload_enabled = False
- if hasattr(self, 'script_manager') and self.script_manager:
- hot_reload_enabled = getattr(self.script_manager, 'hot_reload_enabled', False)
-
- if hot_reload_enabled:
- imgui.text_colored((0.0, 1.0, 0.0, 1.0), "✓ 已启用")
- else:
- imgui.text_colored((1.0, 0.5, 0.0, 1.0), "✗ 已禁用")
-
- imgui.same_line()
- if imgui.button("切换热重载##toggle_hot_reload"):
- self._toggle_hot_reload()
-
- def _draw_create_script_group(self):
- """绘制创建脚本组"""
- if imgui.collapsing_header("创建脚本"):
- # 脚本名称输入
- imgui.text("脚本名称:")
- imgui.same_line()
-
- # 获取当前脚本名称
- if not hasattr(self, '_new_script_name'):
- self._new_script_name = "new_script"
-
- changed, new_name = imgui.input_text("##script_name", self._new_script_name, 256)
- if changed:
- self._new_script_name = new_name
-
- # 模板选择
- imgui.text("脚本模板:")
- imgui.same_line()
-
- if not hasattr(self, '_selected_template'):
- self._selected_template = 0
-
- templates = ["基础脚本", "移动脚本", "旋转脚本", "缩放脚本", "动画脚本"]
- changed, selected = imgui.combo("##script_template", self._selected_template, templates)
- if changed:
- self._selected_template = selected
-
- # 创建按钮
- if imgui.button("创建脚本##create_script"):
- self._create_new_script()
-
- imgui.same_line()
- if imgui.button("从文件创建##create_from_file"):
- self._on_create_script()
-
- def _draw_available_scripts_group(self):
- """绘制可用脚本组"""
- if imgui.collapsing_header("可用脚本"):
- # 刷新脚本列表
- if imgui.button("刷新列表##refresh_scripts"):
- self._refresh_scripts_list()
-
- imgui.same_line()
- if imgui.button("重载全部##reload_all_scripts"):
- self._reload_all_scripts()
-
- imgui.separator()
-
- # 获取可用脚本列表
- available_scripts = []
- if hasattr(self, 'script_manager') and self.script_manager:
- try:
- available_scripts = self.script_manager.get_available_scripts()
- except Exception as e:
- print(f"获取脚本列表失败: {e}")
-
- # 显示脚本列表
- if available_scripts:
- for i, script_name in enumerate(available_scripts):
- selected, _ = imgui.selectable(f"{script_name}##script_{i}", False)
- if selected:
- self._on_script_selected(script_name)
-
- # 双击编辑
- if imgui.is_item_hovered() and imgui.is_mouse_double_clicked(0):
- self._edit_script(script_name)
- else:
- imgui.text_colored((0.7, 0.7, 0.7, 1.0), "无可用脚本")
-
- def _draw_script_mounting_group(self):
- """绘制脚本挂载组"""
- if imgui.collapsing_header("脚本挂载"):
- # 显示当前选中对象
- selected_node = None
- if hasattr(self, 'selection') and self.selection and hasattr(self.selection, 'selectedNode'):
- selected_node = self.selection.selectedNode
-
- if selected_node and not selected_node.isEmpty():
- imgui.text("选中对象:")
- imgui.same_line()
- imgui.text_colored((0.0, 1.0, 0.0, 1.0), selected_node.getName() or "未命名对象")
-
- imgui.spacing()
-
- # 脚本选择和挂载
- imgui.text("选择脚本:")
- imgui.same_line()
-
- # 获取可用脚本
- available_scripts = []
- if hasattr(self, 'script_manager') and self.script_manager:
- try:
- available_scripts = self.script_manager.get_available_scripts()
- except Exception as e:
- print(f"获取脚本列表失败: {e}")
-
- if available_scripts:
- if not hasattr(self, '_mount_script_index'):
- self._mount_script_index = 0
-
- changed, selected = imgui.combo("##mount_script", self._mount_script_index, available_scripts)
- if changed:
- self._mount_script_index = selected
-
- imgui.same_line()
- if imgui.button("挂载##mount_script"):
- if self._mount_script_index < len(available_scripts):
- script_name = available_scripts[self._mount_script_index]
- self._mount_script_to_selected(script_name)
- else:
- imgui.text_colored((0.7, 0.7, 0.7, 1.0), "无可用脚本")
-
- imgui.spacing()
-
- # 显示已挂载的脚本
- imgui.text("已挂载脚本:")
-
- mounted_scripts = []
- if hasattr(self, 'script_manager') and self.script_manager:
- try:
- mounted_scripts = self.script_manager.get_scripts_on_object(selected_node)
- except Exception as e:
- print(f"获取已挂载脚本失败: {e}")
-
- if mounted_scripts:
- for i, script_component in enumerate(mounted_scripts):
- # 从ScriptComponent获取脚本名称
- script_name = getattr(script_component, 'script_name', None)
- if not script_name and hasattr(script_component, '__class__'):
- script_name = script_component.__class__.__name__
-
- if not script_name:
- script_name = f"Script_{i}"
-
- imgui.text(f"• {script_name}")
- imgui.same_line()
- if imgui.button(f"卸载##unmount_{i}"):
- self._unmount_script_from_selected(script_name)
- imgui.same_line()
- if imgui.button(f"编辑##edit_mounted_{i}"):
- self._edit_script(script_name)
- else:
- imgui.text_colored((0.7, 0.7, 0.7, 1.0), "无挂载脚本")
- else:
- imgui.text_colored((0.7, 0.7, 0.7, 1.0), "请先选择一个对象")
-
- def _toggle_hot_reload(self):
- """切换热重载状态"""
- if hasattr(self, 'script_manager') and self.script_manager:
- try:
- current_state = getattr(self.script_manager, 'hot_reload_enabled', False)
- self.script_manager.hot_reload_enabled = not current_state
-
- new_state = "启用" if not current_state else "禁用"
- self.add_success_message(f"热重载已{new_state}")
- print(f"[脚本系统] 热重载已{new_state}")
- except Exception as e:
- self.add_error_message(f"切换热重载失败: {str(e)}")
- print(f"[脚本系统] 切换热重载失败: {e}")
-
- def _create_new_script(self):
- """创建新脚本"""
- if not hasattr(self, '_new_script_name') or not self._new_script_name.strip():
- self.add_error_message("请输入脚本名称")
- return
-
- script_name = self._new_script_name.strip()
- if not script_name.endswith('.py'):
- script_name += '.py'
-
- # 确定模板类型
- template_map = {
- 0: "basic",
- 1: "movement",
- 2: "rotation",
- 3: "scale",
- 4: "animation"
- }
- template_type = template_map.get(getattr(self, '_selected_template', 0), "basic")
-
- try:
- if hasattr(self, 'script_manager') and self.script_manager:
- result = self.script_manager.create_script_file(script_name, template_type)
- if result:
- self.add_success_message(f"脚本 {script_name} 创建成功")
- print(f"[脚本系统] 创建脚本成功: {script_name}")
- # 刷新脚本列表
- self._refresh_scripts_list()
- else:
- self.add_error_message(f"脚本 {script_name} 创建失败")
- else:
- self.add_error_message("脚本管理器未初始化")
- except Exception as e:
- self.add_error_message(f"创建脚本失败: {str(e)}")
- print(f"[脚本系统] 创建脚本失败: {e}")
-
- def _refresh_scripts_list(self):
- """刷新脚本列表"""
- try:
- if hasattr(self, 'script_manager') and self.script_manager:
- # 这里可以添加缓存逻辑,避免频繁刷新
- available_scripts = self.script_manager.get_available_scripts()
- print(f"[脚本系统] 刷新脚本列表: {len(available_scripts)} 个脚本")
- self.add_success_message(f"脚本列表已刷新,共 {len(available_scripts)} 个脚本")
- else:
- self.add_error_message("脚本管理器未初始化")
- except Exception as e:
- self.add_error_message(f"刷新脚本列表失败: {str(e)}")
- print(f"[脚本系统] 刷新脚本列表失败: {e}")
-
- def _reload_all_scripts(self):
- """重载所有脚本"""
- try:
- if hasattr(self, 'script_manager') and self.script_manager:
- # 获取所有可用脚本并逐个重载
- available_scripts = self.script_manager.get_available_scripts()
- success_count = 0
-
- for script_name in available_scripts:
- if self.script_manager.reload_script(script_name):
- success_count += 1
-
- self.add_success_message(f"重载完成: {success_count}/{len(available_scripts)} 个脚本成功")
- print(f"[脚本系统] 重载脚本: {success_count}/{len(available_scripts)} 成功")
- else:
- self.add_error_message("脚本管理器未初始化")
- except Exception as e:
- self.add_error_message(f"重载脚本失败: {str(e)}")
- print(f"[脚本系统] 重载脚本失败: {e}")
-
- def _on_script_selected(self, script_name):
- """处理脚本选择事件"""
- print(f"[脚本系统] 选择脚本: {script_name}")
- self.add_info_message(f"已选择脚本: {script_name}")
-
- def _edit_script(self, script_name):
- """编辑脚本"""
- try:
- if hasattr(self, 'script_manager') and self.script_manager:
- # 获取脚本信息
- script_info = self.script_manager.get_script_info(script_name)
- if script_info and script_info.get("file"):
- script_path = script_info["file"]
-
- # 打开系统默认编辑器
- import subprocess
- import platform
-
- system = platform.system()
- try:
- if system == "Windows":
- subprocess.run(['notepad', script_path])
- elif system == "Darwin": # macOS
- subprocess.run(['open', script_path])
- else: # Linux
- subprocess.run(['xdg-open', script_path])
-
- self.add_success_message(f"已打开脚本编辑器: {script_name}")
- print(f"[脚本系统] 编辑脚本: {script_path}")
- except Exception as e:
- self.add_error_message(f"打开编辑器失败: {str(e)}")
- else:
- self.add_error_message(f"找不到脚本文件: {script_name}")
- else:
- self.add_error_message("脚本管理器未初始化")
- except Exception as e:
- self.add_error_message(f"编辑脚本失败: {str(e)}")
- print(f"[脚本系统] 编辑脚本失败: {e}")
-
- def _mount_script_to_selected(self, script_name):
- """挂载脚本到选中对象"""
- selected_node = None
- if hasattr(self, 'selection') and self.selection and hasattr(self.selection, 'selectedNode'):
- selected_node = self.selection.selectedNode
-
- if not selected_node or selected_node.isEmpty():
- self.add_error_message("请先选择一个对象")
- return
-
- try:
- if hasattr(self, 'script_manager') and self.script_manager:
- script_component = self.script_manager.add_script_to_object(selected_node, script_name)
- if script_component:
- self.add_success_message(f"脚本 {script_name} 已挂载到 {selected_node.getName()}")
- print(f"[脚本系统] 挂载脚本: {script_name} -> {selected_node.getName()}")
- else:
- self.add_error_message(f"挂载脚本 {script_name} 失败")
- else:
- self.add_error_message("脚本管理器未初始化")
- except Exception as e:
- self.add_error_message(f"挂载脚本失败: {str(e)}")
- print(f"[脚本系统] 挂载脚本失败: {e}")
-
- def _unmount_script_from_selected(self, script_name):
- """从选中对象卸载脚本"""
- selected_node = None
- if hasattr(self, 'selection') and self.selection and hasattr(self.selection, 'selectedNode'):
- selected_node = self.selection.selectedNode
-
- if not selected_node or selected_node.isEmpty():
- self.add_error_message("请先选择一个对象")
- return
-
- try:
- if hasattr(self, 'script_manager') and self.script_manager:
- result = self.script_manager.remove_script_from_object(selected_node, script_name)
- if result:
- self.add_success_message(f"脚本 {script_name} 已从 {selected_node.getName()} 卸载")
- print(f"[脚本系统] 卸载脚本: {script_name} <- {selected_node.getName()}")
- else:
- self.add_error_message(f"卸载脚本 {script_name} 失败")
- else:
- self.add_error_message("脚本管理器未初始化")
- except Exception as e:
- self.add_error_message(f"卸载脚本失败: {str(e)}")
- print(f"[脚本系统] 卸载脚本失败: {e}")
-
- # ==================== 菜单处理函数 ====================
-
- def _on_new_project(self):
- """处理新建项目菜单项"""
- self.add_info_message("打开新建项目对话框")
- self.show_new_project_dialog = True
-
- def _on_open_project(self):
- """处理打开项目菜单项"""
- self.add_info_message("打开项目对话框")
- self.show_open_project_dialog = True
-
- def _on_save_project(self):
- """处理保存项目菜单项"""
- if hasattr(self, 'project_manager') and self.project_manager:
- try:
- # 检查是否有当前项目路径
- if not self.project_manager.current_project_path:
- self.add_warning_message("没有当前项目路径,请先创建或打开项目")
- self.show_save_as_dialog = True
- return
-
- # 直接调用保存逻辑,避免Qt依赖
- if self._save_project_impl():
- self.add_success_message("项目保存成功")
- else:
- self.add_error_message("项目保存失败")
- except Exception as e:
- self.add_error_message(f"项目保存失败: {e}")
- else:
- self.add_error_message("项目管理器未初始化")
-
- def _on_save_as_project(self):
- """处理另存为项目菜单项"""
- self.add_info_message("另存为项目(功能待实现)")
- # TODO: 实现另存为对话框
- # self.show_save_as_dialog = True
-
- def _on_exit(self):
- """处理退出菜单项"""
- self.add_info_message("退出应用程序")
- self.userExit()
-
- # ==================== 键盘事件处理函数 ====================
-
- def _on_ctrl_pressed(self):
- """Ctrl键按下"""
- self.ctrl_pressed = True
-
- def _on_ctrl_released(self):
- """Ctrl键释放"""
- self.ctrl_pressed = False
-
- def _on_alt_pressed(self):
- """Alt键按下"""
- self.alt_pressed = True
-
- def _on_alt_released(self):
- """Alt键释放"""
- self.alt_pressed = False
-
- def _on_n_pressed(self):
- """N键按下 - 检查Ctrl+N组合键"""
- if self.ctrl_pressed:
- self._on_new_project()
-
- def _on_o_pressed(self):
- """O键按下 - 检查Ctrl+O组合键"""
- if self.ctrl_pressed:
- self._on_open_project()
-
-
-
- def _on_f4_pressed(self):
- """F4键按下 - 检查Alt+F4组合键"""
- if self.alt_pressed:
- self._on_exit()
-
- # 移除了单独的按键处理方法,现在直接使用组合键事件
-
- def _on_delete_pressed(self):
- """Delete键按下 - 删除选中节点"""
- self._on_delete()
-
- def _on_wheel_up(self):
- """滚轮向上滚动 - 相机前进"""
- try:
- if not self.camera_control_enabled:
- return
-
- # 检查鼠标是否在ImGui窗口上
- if self._is_mouse_over_imgui():
- return
-
- # 沿相机前向向量移动
- forward = self.camera.getMat().getRow3(1)
- distance = 20.0 * globalClock.getDt()
- currentPos = self.camera.getPos()
- newPos = currentPos + forward * distance
- self.camera.setPos(newPos)
- except Exception as e:
- print(f"滚轮前进失败: {e}")
-
- def _on_wheel_down(self):
- """滚轮向下滚动 - 相机后退"""
- try:
- # 检查鼠标是否在ImGui窗口上
- if self._is_mouse_over_imgui():
- return
-
- # 沿相机前向向量移动
- forward = self.camera.getMat().getRow3(1)
- distance = -20.0 * globalClock.getDt()
- currentPos = self.camera.getPos()
- newPos = currentPos + forward * distance
- self.camera.setPos(newPos)
- except Exception as e:
- print(f"滚轮后退失败: {e}")
-
- def _is_mouse_over_imgui(self):
- """检测鼠标是否在ImGui窗口上"""
- try:
- # 检查是否有任何ImGui窗口想要捕获鼠标
- if hasattr(imgui, 'get_io') and imgui.get_io().want_capture_mouse:
- return True
-
- # 检查鼠标是否在任何ImGui窗口内
- mouse_pos = self.mouseWatcherNode.getMouse()
- if not mouse_pos:
- return False
-
- # 简单的边界检查(可以根据需要扩展)
- display_size = imgui.get_io().display_size
- mouse_x = mouse_pos.get_x() * display_size.x / 2 + display_size.x / 2
- mouse_y = display_size.y - (mouse_pos.get_y() * display_size.y / 2 + display_size.y / 2)
-
- # 检查是否在常见的ImGui界面区域内
- # 这里可以根据实际的ImGui窗口位置进行更精确的检测
- if mouse_x < 300 and mouse_y < 200: # 左上角区域(菜单栏)
- return True
- if mouse_x < 300 and mouse_y > display_size.y - 200: # 左下角区域(工具栏)
- return True
- if mouse_x > display_size.x - 300 and mouse_y < 200: # 右上角区域
- return True
-
- return False
- except Exception as e:
- print(f"ImGui界面检测失败: {e}")
- return False
-
- def processImGuiMouseClick(self, x, y):
- """处理ImGui鼠标点击事件,返回是否消费了该事件"""
- try:
- # ImGui优先策略:如果ImGui想要捕获鼠标,则由ImGui处理
- if hasattr(imgui, 'get_io') and imgui.get_io().want_capture_mouse:
- return True
-
- # 检查是否有任何ImGui窗口悬停
- try:
- if imgui.is_any_window_hovered():
- return True
- except AttributeError:
- # 如果方法不存在,跳过这个检查
- pass
-
- # 检查鼠标是否在ImGui界面区域内
- if self._is_mouse_over_imgui():
- return True
-
- # 如果以上条件都不满足,则让3D场景处理该事件
- return False
-
- except Exception as e:
- print(f"ImGui鼠标点击处理失败: {e}")
- return False
-
- # ==================== 消息系统 ====================
-
- def add_message(self, text, color=(1.0, 1.0, 1.0, 1.0)):
- """添加消息到消息列表,同时输出到终端"""
- import datetime
- timestamp = datetime.datetime.now().strftime("%H:%M:%S")
-
- # 输出到终端
- print(f"[{timestamp}] {text}")
-
- # 添加到GUI消息列表
- self.messages.append({
- 'text': text,
- 'color': color,
- 'timestamp': timestamp
- })
-
- # 限制消息数量
- if len(self.messages) > self.max_messages:
- self.messages = self.messages[-self.max_messages:]
-
- def add_success_message(self, text):
- """添加成功消息"""
- self.add_message(f"✓ {text}", (0.176, 1.0, 0.769, 1.0))
-
- def add_error_message(self, text):
- """添加错误消息"""
- self.add_message(f"✗ {text}", (1.0, 0.3, 0.3, 1.0))
-
- def add_warning_message(self, text):
- """添加警告消息"""
- self.add_message(f"⚠ {text}", (0.953, 0.616, 0.471, 1.0))
-
- def add_info_message(self, text):
- """添加信息消息"""
- self.add_message(f"ℹ {text}", (0.157, 0.620, 1.0, 1.0))
-
- # ==================== 编辑菜单功能实现 ====================
-
- def _on_undo(self):
- """处理撤销操作"""
- try:
- if hasattr(self, 'command_manager') and self.command_manager:
- if self.command_manager.can_undo():
- success = self.command_manager.undo()
- if success:
- self.add_success_message("撤销操作成功")
- else:
- self.add_error_message("撤销操作失败")
- else:
- self.add_warning_message("没有可撤销的操作")
- else:
- self.add_error_message("命令管理器未初始化")
- except Exception as e:
- self.add_error_message(f"撤销操作失败: {e}")
-
- def _on_redo(self):
- """处理重做操作"""
- try:
- if hasattr(self, 'command_manager') and self.command_manager:
- if self.command_manager.can_redo():
- success = self.command_manager.redo()
- if success:
- self.add_success_message("重做操作成功")
- else:
- self.add_error_message("重做操作失败")
- else:
- self.add_warning_message("没有可重做的操作")
- else:
- self.add_error_message("命令管理器未初始化")
- except Exception as e:
- self.add_error_message(f"重做操作失败: {e}")
-
- def _on_copy(self):
- """处理复制操作"""
- try:
- if not hasattr(self, 'selection') or not self.selection:
- self.add_error_message("选择系统未初始化")
- return
-
- # 获取当前选中的节点
- selected_node = self.selection.selectedNode
- if not selected_node:
- self.add_warning_message("没有选中的节点")
- return
-
- # 检查节点有效性(不能复制根节点)
- if selected_node.getName() == "render":
- self.add_warning_message("不能复制根节点")
- return
-
- # 序列化节点
- if hasattr(self, 'scene_manager') and self.scene_manager:
- node_data = self.scene_manager.serializeNodeForCopy(selected_node)
- if node_data:
- self.clipboard = [node_data]
- self.clipboard_mode = "copy"
- self.add_success_message(f"已复制节点: {selected_node.getName()}")
- else:
- self.add_error_message("节点序列化失败")
- else:
- self.add_error_message("场景管理器未初始化")
- except Exception as e:
- self.add_error_message(f"复制操作失败: {e}")
-
- def _on_cut(self):
- """处理剪切操作"""
- try:
- if not hasattr(self, 'selection') or not self.selection:
- self.add_error_message("选择系统未初始化")
- return
-
- # 获取当前选中的节点
- selected_node = self.selection.selectedNode
- if not selected_node:
- self.add_warning_message("没有选中的节点")
- return
-
- # 检查节点有效性(不能剪切根节点和系统节点)
- node_name = selected_node.getName()
- if node_name == "render":
- self.add_warning_message("不能剪切根节点")
- return
-
- # 序列化节点
- if hasattr(self, 'scene_manager') and self.scene_manager:
- node_data = self.scene_manager.serializeNodeForCopy(selected_node)
- if node_data:
- self.clipboard = [node_data]
- self.clipboard_mode = "cut"
-
- # 删除原节点
- self._delete_node(selected_node)
- self.selection.clearSelection()
-
- self.add_success_message(f"已剪切节点: {node_name}")
- else:
- self.add_error_message("节点序列化失败")
- else:
- self.add_error_message("场景管理器未初始化")
- except Exception as e:
- self.add_error_message(f"剪切操作失败: {e}")
-
- def _on_paste(self):
- """处理粘贴操作"""
- try:
- if not self.clipboard:
- self.add_warning_message("剪切板为空")
- return
-
- if not hasattr(self, 'scene_manager') or not self.scene_manager:
- self.add_error_message("场景管理器未初始化")
- return
-
- # 确定粘贴目标父节点
- parent_node = None
- if hasattr(self, 'selection') and self.selection:
- selected_node = self.selection.selectedNode
- if selected_node:
- parent_node = selected_node
-
- # 如果没有选中节点,使用渲染根节点
- if not parent_node:
- parent_node = self.render
-
- # 反序列化并添加节点
- for node_data in self.clipboard:
- new_node = self.scene_manager.deserializeNode(node_data, parent_node)
- if new_node:
- self.add_success_message(f"已粘贴节点: {new_node.getName()}")
-
- # 如果是剪切模式,清空剪切板
- if self.clipboard_mode == "cut":
- self.clipboard = []
- self.clipboard_mode = ""
- else:
- self.add_error_message("节点反序列化失败")
- except Exception as e:
- self.add_error_message(f"粘贴操作失败: {e}")
-
- def _on_delete(self):
- """处理删除操作"""
- try:
- if not hasattr(self, 'selection') or not self.selection:
- self.add_error_message("选择系统未初始化")
- return
-
- # 获取当前选中的节点
- selected_node = self.selection.selectedNode
- if not selected_node:
- self.add_warning_message("没有选中的节点")
- return
-
- # 检查节点有效性(不能删除根节点)
- node_name = selected_node.getName()
- if node_name == "render":
- self.add_warning_message("不能删除根节点")
- return
-
- # 删除节点
- if hasattr(self, 'scene_manager') and self.scene_manager:
- self._delete_node(selected_node)
- self.selection.clearSelection()
- self.add_success_message(f"已删除节点: {node_name}")
- else:
- self.add_error_message("场景管理器未初始化")
- except Exception as e:
- self.add_error_message(f"删除操作失败: {e}")
-
- def _delete_node(self, node):
- """删除节点的通用方法 - 使用命令系统"""
- try:
- if not node or node.isEmpty():
- return False
-
- node_name = node.getName() or "未命名节点"
- parent = node.getParent()
-
- # 创建删除命令
- if hasattr(self, 'command_manager') and self.command_manager:
- from core.Command_System import DeleteNodeCommand
- command = DeleteNodeCommand(node, parent, self)
- self.command_manager.execute_command(command)
- print(f"[命令系统] 创建删除命令: {node_name}")
- else:
- # 备用方案:直接删除并执行清理
- print(f"[删除] 命令管理器不可用,直接删除节点: {node_name}")
- self._perform_node_cleanup(node)
- node.removeNode()
-
- print(f"[删除] 成功删除节点: {node_name}")
- return True
-
- except Exception as e:
- print(f"[删除] 删除节点失败: {e}")
- return False
-
- def _perform_node_cleanup(self, node):
- """执行节点清理逻辑"""
- try:
- node_name = node.getName() or "未命名节点"
-
- # 从场景管理器的模型列表中移除(如果是模型)
- if hasattr(self, 'scene_manager') and self.scene_manager:
- if node in self.scene_manager.models:
- self.scene_manager.models.remove(node)
- print(f"[场景管理器] 从模型列表移除: {node_name}")
-
- # 停止所有与该节点相关的脚本
- if hasattr(self, 'script_manager') and self.script_manager:
- try:
- # 移除该节点上的所有脚本
- if node in self.script_manager.object_scripts:
- del self.script_manager.object_scripts[node]
- print(f"[脚本系统] 移除节点 {node_name} 的所有脚本")
- except Exception as e:
- print(f"[脚本系统] 移除脚本失败: {e}")
-
- # 清理碰撞体
- if hasattr(self, 'collision_manager') and self.collision_manager:
- try:
- self.collision_manager.remove_collision_for_node(node)
- print(f"[碰撞系统] 移除节点 {node_name} 的碰撞体")
- except Exception as e:
- print(f"[碰撞系统] 移除碰撞体失败: {e}")
-
- # 清理Actor缓存(如果有动画)
- if hasattr(self, '_actor_cache') and node in self._actor_cache:
- actor = self._actor_cache[node]
- try:
- # 清理相关任务
- taskMgr.remove(f"maintain_anim_pos_{id(actor)}")
- # 清理Actor
- if not actor.isEmpty():
- actor.cleanup()
- actor.removeNode()
- print(f"[动画系统] 清理节点 {node_name} 的Actor缓存")
- except Exception as e:
- print(f"[动画系统] 清理Actor缓存失败: {e}")
- finally:
- del self._actor_cache[node]
-
- except Exception as e:
- print(f"[清理] 节点清理失败: {e}")
-
- # ==================== 对话框绘制函数 ====================
-
- def _draw_new_project_dialog(self):
- """绘制新建项目对话框"""
- if not self.show_new_project_dialog:
- return
-
- # 初始化默认值
- if not hasattr(self, 'new_project_name'):
- self.new_project_name = "新项目"
- if not hasattr(self, 'new_project_path'):
- self.new_project_path = "./projects/"
-
- # 设置对话框标志
- flags = (imgui.WindowFlags_.no_resize |
- imgui.WindowFlags_.no_collapse |
- imgui.WindowFlags_.modal)
-
- # 获取屏幕尺寸,居中显示对话框
- display_size = imgui.get_io().display_size
- dialog_width = 400
- dialog_height = 300
- imgui.set_next_window_size((dialog_width, dialog_height))
- imgui.set_next_window_pos(
- ((display_size.x - dialog_width) / 2, (display_size.y - dialog_height) / 2)
- )
-
- with imgui_ctx.begin("新建项目", True, flags) as window:
- if not window.opened:
- self.show_new_project_dialog = False
- return
-
- imgui.text("创建新项目")
- imgui.separator()
-
- # 项目名称输入
- changed, project_name = imgui.input_text("项目名称", self.new_project_name, 256)
- if changed:
- self.new_project_name = project_name
-
- # 项目路径输入
- changed, project_path = imgui.input_text("项目路径", self.new_project_path, 256)
- if changed:
- self.new_project_path = project_path
-
- imgui.same_line()
- if imgui.button("浏览..."):
- self.path_browser_mode = "new_project"
- self.path_browser_current_path = os.path.dirname(self.new_project_path) if self.new_project_path else os.getcwd()
- self.show_path_browser = True
- self._refresh_path_browser()
-
- imgui.separator()
-
- # 按钮区域
- if imgui.button("创建"):
- if self.new_project_name and self.new_project_path:
- self._create_new_project(self.new_project_name, self.new_project_path)
- self.show_new_project_dialog = False
-
- imgui.same_line()
- if imgui.button("取消"):
- self.show_new_project_dialog = False
-
- def _draw_open_project_dialog(self):
- """绘制打开项目对话框"""
- if not self.show_open_project_dialog:
- return
-
- # 初始化默认值
- if not hasattr(self, 'open_project_path'):
- self.open_project_path = "./projects/"
-
- # 设置对话框标志
- flags = (imgui.WindowFlags_.no_resize |
- imgui.WindowFlags_.no_collapse |
- imgui.WindowFlags_.modal)
-
- # 获取屏幕尺寸,居中显示对话框
- display_size = imgui.get_io().display_size
- dialog_width = 500
- dialog_height = 400
- imgui.set_next_window_size((dialog_width, dialog_height))
- imgui.set_next_window_pos(
- ((display_size.x - dialog_width) / 2, (display_size.y - dialog_height) / 2)
- )
-
- with imgui_ctx.begin("打开项目", True, flags) as window:
- if not window.opened:
- self.show_open_project_dialog = False
- return
-
- imgui.text("选择项目")
- imgui.separator()
-
- imgui.text("项目路径:")
- changed, project_path = imgui.input_text("##project_path", self.open_project_path, 512)
- if changed:
- self.open_project_path = project_path
-
- imgui.same_line()
- if imgui.button("浏览..."):
- self.path_browser_mode = "open_project"
- self.path_browser_current_path = self.open_project_path if self.open_project_path else os.getcwd()
- self.show_path_browser = True
- self._refresh_path_browser()
-
- imgui.separator()
-
- # 按钮区域
- if imgui.button("打开"):
- if self.open_project_path:
- self._open_project_path(self.open_project_path)
- self.show_open_project_dialog = False
-
- imgui.same_line()
- if imgui.button("取消"):
- self.show_open_project_dialog = False
-
- def _draw_path_browser(self):
- """绘制路径选择对话框"""
- if not self.show_path_browser:
- return
-
- # 设置对话框标志
- flags = (imgui.WindowFlags_.no_resize |
- imgui.WindowFlags_.no_collapse |
- imgui.WindowFlags_.modal)
-
- # 获取屏幕尺寸,居中显示对话框
- display_size = imgui.get_io().display_size
- dialog_width = 600
- dialog_height = 500
- imgui.set_next_window_size((dialog_width, dialog_height))
- imgui.set_next_window_pos(
- ((display_size.x - dialog_width) / 2, (display_size.y - dialog_height) / 2)
- )
-
- with imgui_ctx.begin("选择路径", True, flags) as window:
- if not window.opened:
- self.show_path_browser = False
- return
-
- imgui.text("选择路径")
- imgui.separator()
-
- # 当前路径显示
- imgui.text("当前路径:")
- imgui.same_line()
- imgui.text_colored((0.7, 0.7, 0.7, 1.0), self.path_browser_current_path)
-
- imgui.separator()
-
- # 路径导航按钮
- if imgui.button("上级目录"):
- parent_path = os.path.dirname(self.path_browser_current_path)
- if parent_path != self.path_browser_current_path:
- self.path_browser_current_path = parent_path
- self._refresh_path_browser()
-
- imgui.same_line()
- if imgui.button("主目录"):
- self.path_browser_current_path = os.path.expanduser("~")
- self._refresh_path_browser()
-
- imgui.same_line()
- if imgui.button("当前目录"):
- self.path_browser_current_path = os.getcwd()
- self._refresh_path_browser()
-
- imgui.separator()
-
- # 文件和目录列表
- if self.path_browser_items:
- # 先显示目录
- for item in self.path_browser_items:
- if item['is_dir']:
- # 尝试使用图标或文本标识目录
- if self.icons.get('property_select_image'): # 使用现有图标作为文件夹图标
- imgui.image(self.icons['property_select_image'], (16, 16))
- imgui.same_line()
- else:
- imgui.text_colored((0.4, 0.6, 1.0, 1.0), ">")
- imgui.same_line()
-
- if imgui.selectable(item['name'], False)[0]:
- self.path_browser_current_path = item['path']
- self._refresh_path_browser()
- if imgui.is_item_hovered() and imgui.is_mouse_double_clicked(0):
- self.path_browser_current_path = item['path']
- self._refresh_path_browser()
-
-# 显示文件(根据模式显示不同类型的文件)
- if self.path_browser_mode == "open_project":
- for item in self.path_browser_items:
- if not item['is_dir'] and item['name'].endswith('.json'):
- imgui.text_colored((1.0, 1.0, 0.7, 1.0), "[FILE]")
- imgui.same_line()
- if imgui.selectable(item['name'], False)[0]:
- self.path_browser_selected_path = item['path']
- if imgui.is_item_hovered() and imgui.is_mouse_double_clicked(0):
- # 选择包含project.json的目录
- self.path_browser_current_path = os.path.dirname(item['path'])
- self._apply_selected_path()
- elif self.path_browser_mode == "import_model":
- for item in self.path_browser_items:
- if not item['is_dir']:
- file_ext = os.path.splitext(item['name'])[1].lower()
- # 根据文件类型显示不同颜色
- if file_ext in ['.gltf', '.glb']:
- color = (0.7, 1.0, 0.7, 1.0) # 绿色 - glTF
- elif file_ext == '.fbx':
- color = (1.0, 0.7, 0.7, 1.0) # 红色 - FBX
- elif file_ext in ['.bam', '.egg']:
- color = (0.7, 0.7, 1.0, 1.0) # 蓝色 - Panda3D
- elif file_ext == '.obj':
- color = (1.0, 1.0, 0.7, 1.0) # 黄色 - OBJ
- else:
- color = (0.8, 0.8, 0.8, 1.0) # 灰色 - 其他
-
- imgui.text_colored(color, f"[{file_ext[1:].upper()}]")
- imgui.same_line()
- if imgui.selectable(item['name'], False)[0]:
- self.path_browser_selected_path = item['path']
- if imgui.is_item_hovered() and imgui.is_mouse_double_clicked(0):
- self.path_browser_selected_path = item['path']
- self._apply_selected_path()
-
- imgui.separator()
-
- # 选中路径显示
- if self.path_browser_selected_path:
- imgui.text("选中路径:")
- imgui.same_line()
- imgui.text_colored((0.7, 0.7, 0.7, 1.0), self.path_browser_selected_path)
-
- # 按钮区域
- if imgui.button("确定"):
- self._apply_selected_path()
- self.show_path_browser = False
-
- imgui.same_line()
- if imgui.button("取消"):
- self.show_path_browser = False
-
- def _draw_import_dialog(self):
- """绘制导入模型对话框"""
- if not self.show_import_dialog:
- return
-
- # 设置对话框标志
- flags = (imgui.WindowFlags_.no_resize |
- imgui.WindowFlags_.no_collapse |
- imgui.WindowFlags_.modal)
-
- # 获取屏幕尺寸,居中显示对话框
- display_size = imgui.get_io().display_size
- dialog_width = 600
- dialog_height = 500
- imgui.set_next_window_size((dialog_width, dialog_height))
- imgui.set_next_window_pos(
- ((display_size.x - dialog_width) / 2, (display_size.y - dialog_height) / 2)
- )
-
- with imgui_ctx.begin("导入模型", True, flags) as window:
- if not window.opened:
- self.show_import_dialog = False
- return
-
- imgui.text("选择要导入的模型文件")
- imgui.separator()
-
- # 文件路径输入
- imgui.text("文件路径:")
- changed, file_path = imgui.input_text("##import_file_path", self.import_file_path, 512)
- if changed:
- self.import_file_path = file_path
-
- imgui.same_line()
- if imgui.button("浏览..."):
- self.path_browser_mode = "import_model"
- self.path_browser_current_path = os.path.dirname(self.import_file_path) if self.import_file_path else os.getcwd()
- self.show_path_browser = True
- self._refresh_path_browser()
-
- imgui.separator()
-
- # 支持的格式说明
- imgui.text("支持的文件格式:")
- formats_text = ", ".join(self.supported_formats)
- imgui.text_colored((0.7, 0.7, 0.7, 1.0), formats_text)
-
- imgui.separator()
-
- # 文件预览信息
- if self.import_file_path and os.path.exists(self.import_file_path):
- file_size = os.path.getsize(self.import_file_path)
- imgui.text(f"文件大小: {file_size / 1024:.2f} KB")
-
- file_ext = os.path.splitext(self.import_file_path)[1].lower()
- if file_ext in self.supported_formats:
- imgui.text_colored((0.176, 1.0, 0.769, 1.0), "✓ 文件格式支持")
- else:
- imgui.text_colored((1.0, 0.3, 0.3, 1.0), "✗ 不支持的文件格式")
- else:
- imgui.text_colored((0.7, 0.7, 0.7, 1.0), "请选择有效的文件路径")
-
- imgui.separator()
-
- # 按钮区域
- can_import = (self.import_file_path and
- os.path.exists(self.import_file_path) and
- os.path.splitext(self.import_file_path)[1].lower() in self.supported_formats)
-
- # 根据状态设置按钮颜色
- if can_import:
- if imgui.button("导入"):
- self._import_model()
- self.show_import_dialog = False
- else:
- # 禁用状态的按钮(灰色显示)
- imgui.push_style_color(imgui.Col_.button, (0.3, 0.3, 0.3, 1.0))
- imgui.button("导入")
- imgui.pop_style_color()
-
- imgui.same_line()
- if imgui.button("取消"):
- self.show_import_dialog = False
-
- def _create_new_project(self, name, path):
- """创建新项目的实际实现"""
- if not hasattr(self, 'project_manager') or not self.project_manager:
- print("✗ 项目管理器未初始化")
- return
-
- try:
- if self._create_new_project_impl(name, path):
- print(f"✓ 项目创建成功: {name}")
- else:
- print(f"✗ 项目创建失败: {name}")
- except Exception as e:
- print(f"✗ 项目创建失败: {e}")
-
- def _open_project_path(self, path):
- """打开项目的实际实现"""
- if not hasattr(self, 'project_manager') or not self.project_manager:
- print("✗ 项目管理器未初始化")
- return
-
- try:
- print(f"打开项目: {path}")
- if self._open_project_impl(path):
- print(f"✓ 项目打开成功: {path}")
- else:
- print(f"✗ 项目打开失败: {path}")
- except Exception as e:
- print(f"✗ 项目打开失败: {e}")
-
- # ==================== 项目管理具体实现 ====================
-
- def _save_project_impl(self):
- """保存项目的具体实现(不依赖Qt)"""
- import json
- import datetime
- import os
-
- project_path = self.project_manager.current_project_path
- scenes_path = os.path.join(project_path, "scenes")
-
- # 固定的场景文件名
- scene_file = os.path.join(scenes_path, "scene.bam")
-
- # 如果存在旧文件,先删除
- if os.path.exists(scene_file):
- try:
- os.remove(scene_file)
- print(f"已删除旧场景文件: {scene_file}")
- except Exception as e:
- print(f"删除旧场景文件失败: {str(e)}")
- return False
-
- # 保存场景
- if self.scene_manager.saveScene(scene_file, project_path):
- # 更新项目配置文件
- config_file = os.path.join(project_path, "project.json")
- if os.path.exists(config_file):
- with open(config_file, "r", encoding="utf-8") as f:
- project_config = json.load(f)
-
- # 更新最后修改时间
- project_config["last_modified"] = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
- # 记录场景文件路径
- project_config["scene_file"] = os.path.relpath(scene_file, project_path)
-
- with open(config_file, "w", encoding="utf-8") as f:
- json.dump(project_config, f, ensure_ascii=False, indent=4)
-
- # 更新项目配置
- self.project_manager.project_config = project_config
- return True
- return False
-
- def _open_project_impl(self, project_path):
- """打开项目的具体实现(不依赖Qt)"""
- import json
- import datetime
- import os
-
- try:
- # 检查项目管理器是否已初始化
- if not hasattr(self, 'project_manager') or not self.project_manager:
- print("✗ 项目管理器未初始化")
- self.add_error_message("项目管理器未初始化")
- return False
-
- # 检查场景管理器是否已初始化
- if not hasattr(self, 'scene_manager') or not self.scene_manager:
- print("✗ 场景管理器未初始化")
- self.add_error_message("场景管理器未初始化")
- return False
-
- # 检查是否是有效的项目文件夹
- config_file = os.path.join(project_path, "project.json")
- if not os.path.exists(config_file):
- print(f"⚠ 选择的不是有效的项目文件夹: {project_path}")
- self.add_warning_message(f"选择的不是有效的项目文件夹: {project_path}")
- return False
+ def _apply_gui_font_size(self, *args, **kwargs):
+ return self.property_helpers._apply_gui_font_size(*args, **kwargs)
- # 读取项目配置
- try:
- with open(config_file, "r", encoding="utf-8") as f:
- project_config = json.load(f)
- except Exception as e:
- print(f"✗ 读取项目配置文件失败: {e}")
- self.add_error_message(f"读取项目配置文件失败: {e}")
- return False
+ def _apply_gui_font_style(self, *args, **kwargs):
+ return self.property_helpers._apply_gui_font_style(*args, **kwargs)
- # 检查场景文件
- scene_file = os.path.join(project_path, "scenes", "scene.bam")
- if os.path.exists(scene_file):
- # 加载场景
- try:
- if self.scene_manager.loadScene(scene_file):
- # 更新项目配置
- project_config["scene_file"] = os.path.relpath(scene_file, project_path)
- print(f"✓ 场景加载成功: {scene_file}")
- else:
- print(f"⚠ 场景加载失败: {scene_file}")
- self.add_warning_message(f"场景加载失败: {scene_file}")
- except Exception as e:
- print(f"✗ 加载场景时发生错误: {e}")
- self.add_error_message(f"加载场景时发生错误: {e}")
- # 继续执行,不阻止项目打开
+ def _has_collision(self, *args, **kwargs):
+ return self.property_helpers._has_collision(*args, **kwargs)
- # 更新项目配置
- project_config["last_modified"] = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
- try:
- with open(config_file, "w", encoding="utf-8") as f:
- json.dump(project_config, f, ensure_ascii=False, indent=4)
- except Exception as e:
- print(f"✗ 保存项目配置失败: {e}")
- self.add_error_message(f"保存项目配置失败: {e}")
+ def _get_current_collision_shape(self, *args, **kwargs):
+ return self.property_helpers._get_current_collision_shape(*args, **kwargs)
- # 更新项目状态
- self.project_manager.current_project_path = project_path
- self.project_manager.project_config = project_config
-
- # 更新窗口标题
- project_name = os.path.basename(project_path)
- self._update_window_title(project_name)
-
- print(f"✓ 项目打开成功: {project_path}")
- self.add_success_message(f"项目打开成功: {project_name}")
- return True
-
- except Exception as e:
- print(f"✗ 打开项目时发生错误: {e}")
- self.add_error_message(f"打开项目时发生错误: {e}")
- return False
-
- def _create_new_project_impl(self, name, path):
- """创建新项目的具体实现(不依赖Qt)"""
- import json
- import datetime
- import os
-
- full_project_path = os.path.normpath(os.path.join(path, name))
- print(f"创建项目路径: {full_project_path}")
-
- try:
- # 创建项目文件夹结构
- os.makedirs(full_project_path)
- os.makedirs(os.path.join(full_project_path, "models")) # 模型文件夹
- os.makedirs(os.path.join(full_project_path, "textures")) # 贴图文件夹
- scenes_path = os.path.join(full_project_path, "scenes") # 场景文件夹
- os.makedirs(scenes_path)
-
- # 创建项目配置文件
- project_config = {
- "name": name,
- "path": full_project_path,
- "created": datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
- "last_modified": datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
- "version": "1.0",
- "scene_file": "scenes/scene.bam"
- }
-
- # 保存项目配置
- config_file = os.path.join(full_project_path, "project.json")
- with open(config_file, "w", encoding="utf-8") as f:
- json.dump(project_config, f, ensure_ascii=False, indent=4)
-
- # 保存初始场景
- scene_file = os.path.join(scenes_path, "scene.bam")
- self.scene_manager.saveScene(scene_file, full_project_path)
-
- # 更新项目管理器状态
- self.project_manager.current_project_path = full_project_path
- self.project_manager.project_config = project_config
-
- # 更新窗口标题
- self._update_window_title(name)
-
- return True
-
- except Exception as e:
- print(f"创建项目失败: {e}")
- return False
-
- def _update_window_title(self, project_name):
- """更新窗口标题"""
- try:
- props = WindowProperties()
- props.set_title(f"EG Engine - {project_name}")
- self.win.request_properties(props)
- print(f"窗口标题已更新: EG Engine - {project_name}")
- except Exception as e:
- print(f"更新窗口标题失败: {e}")
-
- # ==================== 路径浏览器辅助方法 ====================
-
- def _refresh_path_browser(self):
- """刷新路径浏览器内容"""
- try:
- self.path_browser_items = []
-
- if not os.path.exists(self.path_browser_current_path):
- self.add_error_message(f"路径不存在: {self.path_browser_current_path}")
- return
-
- # 获取目录中的所有项目
- items = []
- try:
- for item_name in os.listdir(self.path_browser_current_path):
- item_path = os.path.join(self.path_browser_current_path, item_name)
- is_dir = os.path.isdir(item_path)
-
- items.append({
- 'name': item_name,
- 'path': item_path,
- 'is_dir': is_dir
- })
- except PermissionError:
- self.add_error_message(f"无法访问路径: {self.path_browser_current_path}")
- return
-
- # 根据模式过滤文件
- if self.path_browser_mode == "import_model":
- # 只显示支持的模型文件
- filtered_items = []
- for item in items:
- if item['is_dir']:
- filtered_items.append(item)
- else:
- file_ext = os.path.splitext(item['name'])[1].lower()
- if file_ext in self.supported_formats:
- filtered_items.append(item)
- items = filtered_items
-
- # 排序:目录在前,文件在后,按名称排序
- items.sort(key=lambda x: (not x['is_dir'], x['name'].lower()))
- self.path_browser_items = items
-
- except Exception as e:
- self.add_error_message(f"刷新路径浏览器失败: {e}")
-
- def _apply_selected_path(self):
- """应用选择的路径"""
- try:
- if self.path_browser_mode == "new_project":
- # 新建项目模式:直接使用当前路径
- self.new_project_path = self.path_browser_current_path
- self.add_info_message(f"已选择项目路径: {self.new_project_path}")
- elif self.path_browser_mode == "open_project":
- # 打开项目模式:使用当前路径
- self.open_project_path = self.path_browser_current_path
- self.add_info_message(f"已选择项目路径: {self.open_project_path}")
- elif self.path_browser_mode == "import_model":
- # 导入模型模式:使用选择的文件路径
- self.import_file_path = self.path_browser_selected_path
- self.add_info_message(f"已选择文件: {self.import_file_path}")
- except Exception as e:
- self.add_error_message(f"应用路径失败: {e}")
-
- # ==================== 创建功能对话框实现 ====================
-
- def _draw_gui_button_dialog(self):
- """绘制GUI按钮创建对话框"""
- if not self.show_gui_button_dialog:
- return
-
- flags = (imgui.WindowFlags_.no_resize |
- imgui.WindowFlags_.no_collapse |
- imgui.WindowFlags_.modal)
-
- with imgui_ctx.begin("创建GUI按钮", self.show_gui_button_dialog, flags) as window:
- if not window:
- self.show_gui_button_dialog = False
- return
-
- # 初始化参数
- if 'button_text' not in self.dialog_params:
- self.dialog_params['button_text'] = "按钮"
- if 'button_pos' not in self.dialog_params:
- self.dialog_params['button_pos'] = [0.0, 0.0, 0.0]
- if 'button_size' not in self.dialog_params:
- self.dialog_params['button_size'] = [0.1, 0.1, 0.1]
-
- imgui.text("GUI按钮参数设置")
- imgui.separator()
-
- # 文本输入
- changed, self.dialog_params['button_text'] = imgui.input_text("按钮文本", self.dialog_params['button_text'], 256)
-
- # 位置输入
- changed, x = imgui.input_float("X坐标", self.dialog_params['button_pos'][0])
- if changed:
- self.dialog_params['button_pos'][0] = x
- changed, y = imgui.input_float("Y坐标", self.dialog_params['button_pos'][1])
- if changed:
- self.dialog_params['button_pos'][1] = y
- changed, z = imgui.input_float("Z坐标", self.dialog_params['button_pos'][2])
- if changed:
- self.dialog_params['button_pos'][2] = z
-
- # 大小输入
- changed, width = imgui.input_float("宽度", self.dialog_params['button_size'][0])
- if changed:
- self.dialog_params['button_size'][0] = width
- changed, height = imgui.input_float("高度", self.dialog_params['button_size'][1])
- if changed:
- self.dialog_params['button_size'][1] = height
-
- imgui.separator()
-
- # 按钮
- if imgui.button("创建"):
- try:
- pos = tuple(self.dialog_params['button_pos'])
- text = self.dialog_params['button_text']
- size = tuple(self.dialog_params['button_size'][:2])
-
- result = self.createGUIButton(pos, text, size)
- self.add_success_message(f"GUI按钮创建成功: {text}")
- self.show_gui_button_dialog = False
- except Exception as e:
- self.add_error_message(f"创建GUI按钮失败: {str(e)}")
-
- imgui.same_line()
- if imgui.button("取消"):
- self.show_gui_button_dialog = False
-
- def _draw_gui_label_dialog(self):
- """绘制GUI标签创建对话框"""
- if not self.show_gui_label_dialog:
- return
-
- flags = (imgui.WindowFlags_.no_resize |
- imgui.WindowFlags_.no_collapse |
- imgui.WindowFlags_.modal)
-
- with imgui_ctx.begin("创建GUI标签", self.show_gui_label_dialog, flags) as window:
- if not window:
- self.show_gui_label_dialog = False
- return
-
- # 初始化参数
- if 'label_text' not in self.dialog_params:
- self.dialog_params['label_text'] = "标签"
- if 'label_pos' not in self.dialog_params:
- self.dialog_params['label_pos'] = [0.0, 0.0, 0.0]
- if 'label_size' not in self.dialog_params:
- self.dialog_params['label_size'] = [0.1, 0.1, 0.1]
-
- imgui.text("GUI标签参数设置")
- imgui.separator()
-
- # 文本输入
- changed, self.dialog_params['label_text'] = imgui.input_text("标签文本", self.dialog_params['label_text'], 256)
-
- # 位置输入
- changed, x = imgui.input_float("X坐标", self.dialog_params['label_pos'][0])
- if changed:
- self.dialog_params['label_pos'][0] = x
- changed, y = imgui.input_float("Y坐标", self.dialog_params['label_pos'][1])
- if changed:
- self.dialog_params['label_pos'][1] = y
- changed, z = imgui.input_float("Z坐标", self.dialog_params['label_pos'][2])
- if changed:
- self.dialog_params['label_pos'][2] = z
-
- # 大小输入
- changed, width = imgui.input_float("宽度", self.dialog_params['label_size'][0])
- if changed:
- self.dialog_params['label_size'][0] = width
- changed, height = imgui.input_float("高度", self.dialog_params['label_size'][1])
- if changed:
- self.dialog_params['label_size'][1] = height
-
- imgui.separator()
-
- # 按钮
- if imgui.button("创建"):
- try:
- pos = tuple(self.dialog_params['label_pos'])
- text = self.dialog_params['label_text']
- size = tuple(self.dialog_params['label_size'][:2])
-
- result = self.createGUILabel(pos, text, size)
- self.add_success_message(f"GUI标签创建成功: {text}")
- self.show_gui_label_dialog = False
- except Exception as e:
- self.add_error_message(f"创建GUI标签失败: {str(e)}")
-
- imgui.same_line()
- if imgui.button("取消"):
- self.show_gui_label_dialog = False
-
- def _draw_gui_entry_dialog(self):
- """绘制GUI输入框创建对话框"""
- if not self.show_gui_entry_dialog:
- return
-
- flags = (imgui.WindowFlags_.no_resize |
- imgui.WindowFlags_.no_collapse |
- imgui.WindowFlags_.modal)
-
- with imgui_ctx.begin("创建GUI输入框", self.show_gui_entry_dialog, flags) as window:
- if not window:
- self.show_gui_entry_dialog = False
- return
-
- # 初始化参数
- if 'entry_pos' not in self.dialog_params:
- self.dialog_params['entry_pos'] = [0.0, 0.0, 0.0]
- if 'entry_size' not in self.dialog_params:
- self.dialog_params['entry_size'] = [0.2, 0.05, 0.1]
-
- imgui.text("GUI输入框参数设置")
- imgui.separator()
-
- # 位置输入
- changed, x = imgui.input_float("X坐标", self.dialog_params['entry_pos'][0])
- if changed:
- self.dialog_params['entry_pos'][0] = x
- changed, y = imgui.input_float("Y坐标", self.dialog_params['entry_pos'][1])
- if changed:
- self.dialog_params['entry_pos'][1] = y
- changed, z = imgui.input_float("Z坐标", self.dialog_params['entry_pos'][2])
- if changed:
- self.dialog_params['entry_pos'][2] = z
-
- # 大小输入
- changed, width = imgui.input_float("宽度", self.dialog_params['entry_size'][0])
- if changed:
- self.dialog_params['entry_size'][0] = width
- changed, height = imgui.input_float("高度", self.dialog_params['entry_size'][1])
- if changed:
- self.dialog_params['entry_size'][1] = height
-
- imgui.separator()
-
- # 按钮
- if imgui.button("创建"):
- try:
- pos = tuple(self.dialog_params['entry_pos'])
- size = tuple(self.dialog_params['entry_size'][:2])
-
- result = self.createGUIEntry(pos, size)
- self.add_success_message("GUI输入框创建成功")
- self.show_gui_entry_dialog = False
- except Exception as e:
- self.add_error_message(f"创建GUI输入框失败: {str(e)}")
-
- imgui.same_line()
- if imgui.button("取消"):
- self.show_gui_entry_dialog = False
-
- def _draw_gui_image_dialog(self):
- """绘制GUI图片创建对话框"""
- if not self.show_gui_image_dialog:
- return
-
- flags = (imgui.WindowFlags_.no_resize |
- imgui.WindowFlags_.no_collapse |
- imgui.WindowFlags_.modal)
-
- with imgui_ctx.begin("创建GUI图片", self.show_gui_image_dialog, flags) as window:
- if not window:
- self.show_gui_image_dialog = False
- return
-
- # 初始化参数
- if 'image_pos' not in self.dialog_params:
- self.dialog_params['image_pos'] = [0.0, 0.0, 0.0]
- if 'image_size' not in self.dialog_params:
- self.dialog_params['image_size'] = [0.2, 0.2, 0.1]
- if 'image_path' not in self.dialog_params:
- self.dialog_params['image_path'] = ""
-
- imgui.text("GUI图片参数设置")
- imgui.separator()
-
- # 位置输入
- changed, x = imgui.input_float("X坐标", self.dialog_params['image_pos'][0])
- if changed:
- self.dialog_params['image_pos'][0] = x
- changed, y = imgui.input_float("Y坐标", self.dialog_params['image_pos'][1])
- if changed:
- self.dialog_params['image_pos'][1] = y
- changed, z = imgui.input_float("Z坐标", self.dialog_params['image_pos'][2])
- if changed:
- self.dialog_params['image_pos'][2] = z
-
- # 大小输入
- changed, width = imgui.input_float("宽度", self.dialog_params['image_size'][0])
- if changed:
- self.dialog_params['image_size'][0] = width
- changed, height = imgui.input_float("高度", self.dialog_params['image_size'][1])
- if changed:
- self.dialog_params['image_size'][1] = height
-
- # 图片路径
- changed, self.dialog_params['image_path'] = imgui.input_text("图片路径", self.dialog_params['image_path'], 512)
-
- imgui.separator()
-
- # 按钮
- if imgui.button("创建"):
- try:
- pos = tuple(self.dialog_params['image_pos'])
- size = tuple(self.dialog_params['image_size'][:2])
- image_path = self.dialog_params['image_path']
-
- result = self.createGUIImage(pos, image_path, size)
- self.add_success_message("GUI图片创建成功")
- self.show_gui_image_dialog = False
- except Exception as e:
- self.add_error_message(f"创建GUI图片失败: {str(e)}")
-
- imgui.same_line()
- if imgui.button("取消"):
- self.show_gui_image_dialog = False
-
- def _draw_3d_text_dialog(self):
- """绘制3D文本创建对话框"""
- if not self.show_3d_text_dialog:
- return
-
- flags = (imgui.WindowFlags_.no_resize |
- imgui.WindowFlags_.no_collapse |
- imgui.WindowFlags_.modal)
-
- with imgui_ctx.begin("创建3D文本", self.show_3d_text_dialog, flags) as window:
- if not window:
- self.show_3d_text_dialog = False
- return
-
- # 初始化参数
- if 'text3d_text' not in self.dialog_params:
- self.dialog_params['text3d_text'] = "3D文本"
- if 'text3d_pos' not in self.dialog_params:
- self.dialog_params['text3d_pos'] = [0.0, 0.0, 0.0]
- if 'text3d_size' not in self.dialog_params:
- self.dialog_params['text3d_size'] = 1.0
-
- imgui.text("3D文本参数设置")
- imgui.separator()
-
- # 文本输入
- changed, self.dialog_params['text3d_text'] = imgui.input_text("文本内容", self.dialog_params['text3d_text'], 256)
-
- # 位置输入
- changed, x = imgui.input_float("X坐标", self.dialog_params['text3d_pos'][0])
- if changed:
- self.dialog_params['text3d_pos'][0] = x
- changed, y = imgui.input_float("Y坐标", self.dialog_params['text3d_pos'][1])
- if changed:
- self.dialog_params['text3d_pos'][1] = y
- changed, z = imgui.input_float("Z坐标", self.dialog_params['text3d_pos'][2])
- if changed:
- self.dialog_params['text3d_pos'][2] = z
-
- # 大小输入
- changed, self.dialog_params['text3d_size'] = imgui.input_float("文本大小", self.dialog_params['text3d_size'])
-
- imgui.separator()
-
- # 按钮
- if imgui.button("创建"):
- try:
- pos = tuple(self.dialog_params['text3d_pos'])
- text = self.dialog_params['text3d_text']
- size = self.dialog_params['text3d_size']
-
- result = self.create3DText(pos, text, size)
- self.add_success_message(f"3D文本创建成功: {text}")
- self.show_3d_text_dialog = False
- except Exception as e:
- self.add_error_message(f"创建3D文本失败: {str(e)}")
-
- imgui.same_line()
- if imgui.button("取消"):
- self.show_3d_text_dialog = False
-
- def _draw_3d_image_dialog(self):
- """绘制3D图片创建对话框"""
- if not self.show_3d_image_dialog:
- return
-
- flags = (imgui.WindowFlags_.no_resize |
- imgui.WindowFlags_.no_collapse |
- imgui.WindowFlags_.modal)
-
- with imgui_ctx.begin("创建3D图片", self.show_3d_image_dialog, flags) as window:
- if not window:
- self.show_3d_image_dialog = False
- return
-
- # 初始化参数
- if 'image3d_pos' not in self.dialog_params:
- self.dialog_params['image3d_pos'] = [0.0, 0.0, 0.0]
- if 'image3d_size' not in self.dialog_params:
- self.dialog_params['image3d_size'] = [1.0, 1.0, 1.0]
- if 'image3d_path' not in self.dialog_params:
- self.dialog_params['image3d_path'] = ""
-
- imgui.text("3D图片参数设置")
- imgui.separator()
-
- # 位置输入
- changed, x = imgui.input_float("X坐标", self.dialog_params['image3d_pos'][0])
- if changed:
- self.dialog_params['image3d_pos'][0] = x
- changed, y = imgui.input_float("Y坐标", self.dialog_params['image3d_pos'][1])
- if changed:
- self.dialog_params['image3d_pos'][1] = y
- changed, z = imgui.input_float("Z坐标", self.dialog_params['image3d_pos'][2])
- if changed:
- self.dialog_params['image3d_pos'][2] = z
-
- # 大小输入
- changed, width = imgui.input_float("宽度", self.dialog_params['image3d_size'][0])
- if changed:
- self.dialog_params['image3d_size'][0] = width
- changed, height = imgui.input_float("高度", self.dialog_params['image3d_size'][1])
- if changed:
- self.dialog_params['image3d_size'][1] = height
-
- # 图片路径
- changed, self.dialog_params['image3d_path'] = imgui.input_text("图片路径", self.dialog_params['image3d_path'], 512)
-
- imgui.separator()
-
- # 按钮
- if imgui.button("创建"):
- try:
- pos = tuple(self.dialog_params['image3d_pos'])
- size = tuple(self.dialog_params['image3d_size'][:2])
- image_path = self.dialog_params['image3d_path']
-
- result = self.create3DImage(pos, image_path, size)
- self.add_success_message("3D图片创建成功")
- self.show_3d_image_dialog = False
- except Exception as e:
- self.add_error_message(f"创建3D图片失败: {str(e)}")
-
- imgui.same_line()
- if imgui.button("取消"):
- self.show_3d_image_dialog = False
-
- # 添加其他创建对话框的占位符方法
- def _draw_video_screen_dialog(self):
- """绘制视频屏幕创建对话框"""
- if not self.show_video_screen_dialog:
- return
- self.show_video_screen_dialog = False
- self.add_info_message("视频屏幕创建功能开发中...")
-
- def _draw_2d_video_screen_dialog(self):
- """绘制2D视频屏幕创建对话框"""
- if not self.show_2d_video_screen_dialog:
- return
- self.show_2d_video_screen_dialog = False
- self.add_info_message("2D视频屏幕创建功能开发中...")
-
- def _draw_spherical_video_dialog(self):
- """绘制球形视频创建对话框"""
- if not self.show_spherical_video_dialog:
- return
- self.show_spherical_video_dialog = False
- self.add_info_message("球形视频创建功能开发中...")
-
- def _draw_virtual_screen_dialog(self):
- """绘制虚拟屏幕创建对话框"""
- if not self.show_virtual_screen_dialog:
- return
- self.show_virtual_screen_dialog = False
- self.add_info_message("虚拟屏幕创建功能开发中...")
-
- def _draw_spot_light_dialog(self):
- """绘制聚光灯创建对话框"""
- if not self.show_spot_light_dialog:
- return
-
- flags = (imgui.WindowFlags_.no_resize |
- imgui.WindowFlags_.no_collapse |
- imgui.WindowFlags_.modal)
-
- with imgui_ctx.begin("创建聚光灯", self.show_spot_light_dialog, flags) as window:
- if not window:
- self.show_spot_light_dialog = False
- return
-
- # 初始化参数
- if 'spotlight_pos' not in self.dialog_params:
- self.dialog_params['spotlight_pos'] = [0.0, 0.0, 5.0]
- if 'spotlight_color' not in self.dialog_params:
- self.dialog_params['spotlight_color'] = [1.0, 1.0, 1.0]
- if 'spotlight_intensity' not in self.dialog_params:
- self.dialog_params['spotlight_intensity'] = 1.0
-
- imgui.text("聚光灯参数设置")
- imgui.separator()
-
- # 位置输入
- changed, x = imgui.input_float("X坐标", self.dialog_params['spotlight_pos'][0])
- if changed:
- self.dialog_params['spotlight_pos'][0] = x
- changed, y = imgui.input_float("Y坐标", self.dialog_params['spotlight_pos'][1])
- if changed:
- self.dialog_params['spotlight_pos'][1] = y
- changed, z = imgui.input_float("Z坐标", self.dialog_params['spotlight_pos'][2])
- if changed:
- self.dialog_params['spotlight_pos'][2] = z
-
- # 颜色输入
- changed, r = imgui.input_float("红色 (R)", self.dialog_params['spotlight_color'][0], 0.0, 1.0)
- if changed:
- self.dialog_params['spotlight_color'][0] = max(0.0, min(1.0, r))
- changed, g = imgui.input_float("绿色 (G)", self.dialog_params['spotlight_color'][1], 0.0, 1.0)
- if changed:
- self.dialog_params['spotlight_color'][1] = max(0.0, min(1.0, g))
- changed, b = imgui.input_float("蓝色 (B)", self.dialog_params['spotlight_color'][2], 0.0, 1.0)
- if changed:
- self.dialog_params['spotlight_color'][2] = max(0.0, min(1.0, b))
-
- # 强度输入
- changed, self.dialog_params['spotlight_intensity'] = imgui.input_float("强度", self.dialog_params['spotlight_intensity'], 0.1, 2.0)
- if changed:
- self.dialog_params['spotlight_intensity'] = max(0.1, self.dialog_params['spotlight_intensity'])
-
- imgui.separator()
-
- # 按钮
- if imgui.button("创建"):
- try:
- pos = tuple(self.dialog_params['spotlight_pos'])
-
- result = self.createSpotLight(pos)
- if result:
- # 设置颜色和强度
- light = result.node()
- if hasattr(light, 'setColor'):
- color = tuple(self.dialog_params['spotlight_color'])
- light.setColor(color + (1.0,)) # 添加alpha通道
- if hasattr(light, 'setEnergy'):
- light.setEnergy(self.dialog_params['spotlight_intensity'])
-
- self.add_success_message("聚光灯创建成功")
- self.show_spot_light_dialog = False
- else:
- self.add_error_message("聚光灯创建失败")
- except Exception as e:
- self.add_error_message(f"创建聚光灯失败: {str(e)}")
-
- imgui.same_line()
- if imgui.button("取消"):
- self.show_spot_light_dialog = False
-
- def _draw_point_light_dialog(self):
- """绘制点光源创建对话框"""
- if not self.show_point_light_dialog:
- return
-
- flags = (imgui.WindowFlags_.no_resize |
- imgui.WindowFlags_.no_collapse |
- imgui.WindowFlags_.modal)
-
- with imgui_ctx.begin("创建点光源", self.show_point_light_dialog, flags) as window:
- if not window:
- self.show_point_light_dialog = False
- return
-
- # 初始化参数
- if 'pointlight_pos' not in self.dialog_params:
- self.dialog_params['pointlight_pos'] = [0.0, 0.0, 5.0]
- if 'pointlight_color' not in self.dialog_params:
- self.dialog_params['pointlight_color'] = [1.0, 1.0, 1.0]
- if 'pointlight_intensity' not in self.dialog_params:
- self.dialog_params['pointlight_intensity'] = 1.0
- if 'pointlight_radius' not in self.dialog_params:
- self.dialog_params['pointlight_radius'] = 10.0
-
- imgui.text("点光源参数设置")
- imgui.separator()
-
- # 位置输入
- changed, x = imgui.input_float("X坐标", self.dialog_params['pointlight_pos'][0])
- if changed:
- self.dialog_params['pointlight_pos'][0] = x
- changed, y = imgui.input_float("Y坐标", self.dialog_params['pointlight_pos'][1])
- if changed:
- self.dialog_params['pointlight_pos'][1] = y
- changed, z = imgui.input_float("Z坐标", self.dialog_params['pointlight_pos'][2])
- if changed:
- self.dialog_params['pointlight_pos'][2] = z
-
- # 颜色输入
- changed, r = imgui.input_float("红色 (R)", self.dialog_params['pointlight_color'][0], 0.0, 1.0)
- if changed:
- self.dialog_params['pointlight_color'][0] = max(0.0, min(1.0, r))
- changed, g = imgui.input_float("绿色 (G)", self.dialog_params['pointlight_color'][1], 0.0, 1.0)
- if changed:
- self.dialog_params['pointlight_color'][1] = max(0.0, min(1.0, g))
- changed, b = imgui.input_float("蓝色 (B)", self.dialog_params['pointlight_color'][2], 0.0, 1.0)
- if changed:
- self.dialog_params['pointlight_color'][2] = max(0.0, min(1.0, b))
-
- # 强度输入
- changed, self.dialog_params['pointlight_intensity'] = imgui.input_float("强度", self.dialog_params['pointlight_intensity'], 0.1, 2.0)
- if changed:
- self.dialog_params['pointlight_intensity'] = max(0.1, self.dialog_params['pointlight_intensity'])
-
- # 半径输入
- changed, self.dialog_params['pointlight_radius'] = imgui.input_float("影响半径", self.dialog_params['pointlight_radius'], 1.0, 50.0)
- if changed:
- self.dialog_params['pointlight_radius'] = max(1.0, self.dialog_params['pointlight_radius'])
-
- imgui.separator()
-
- # 按钮
- if imgui.button("创建"):
- try:
- pos = tuple(self.dialog_params['pointlight_pos'])
-
- result = self.createPointLight(pos)
- if result:
- # 设置颜色和强度
- light = result.node()
- if hasattr(light, 'setColor'):
- color = tuple(self.dialog_params['pointlight_color'])
- light.setColor(color + (1.0,)) # 添加alpha通道
- if hasattr(light, 'setEnergy'):
- light.setEnergy(self.dialog_params['pointlight_intensity'])
- if hasattr(light, 'setAttenuation'):
- # 设置衰减: (constant, linear, quadratic)
- radius = self.dialog_params['pointlight_radius']
- light.setAttenuation((1.0, 0.5/radius, 0.5/(radius*radius)))
-
- self.add_success_message("点光源创建成功")
- self.show_point_light_dialog = False
- else:
- self.add_error_message("点光源创建失败")
- except Exception as e:
- self.add_error_message(f"创建点光源失败: {str(e)}")
-
- imgui.same_line()
- if imgui.button("取消"):
- self.show_point_light_dialog = False
-
- def _draw_terrain_dialog(self):
- """绘制地形创建对话框"""
- if not self.show_terrain_dialog:
- return
-
- flags = (imgui.WindowFlags_.no_resize |
- imgui.WindowFlags_.no_collapse |
- imgui.WindowFlags_.modal)
-
- with imgui_ctx.begin("创建平面地形", self.show_terrain_dialog, flags) as window:
- if not window:
- self.show_terrain_dialog = False
- return
-
- # 初始化参数
- if 'terrain_width' not in self.dialog_params:
- self.dialog_params['terrain_width'] = 10.0
- if 'terrain_height' not in self.dialog_params:
- self.dialog_params['terrain_height'] = 10.0
- if 'terrain_resolution' not in self.dialog_params:
- self.dialog_params['terrain_resolution'] = 129
-
- imgui.text("平面地形参数设置")
- imgui.separator()
-
- # 尺寸输入
- changed, self.dialog_params['terrain_width'] = imgui.input_float("宽度", self.dialog_params['terrain_width'], 1.0, 100.0)
- if changed:
- self.dialog_params['terrain_width'] = max(1.0, self.dialog_params['terrain_width'])
-
- changed, self.dialog_params['terrain_height'] = imgui.input_float("高度", self.dialog_params['terrain_height'], 1.0, 100.0)
- if changed:
- self.dialog_params['terrain_height'] = max(1.0, self.dialog_params['terrain_height'])
-
- # 分辨率输入
- changed, self.dialog_params['terrain_resolution'] = imgui.input_int("分辨率", self.dialog_params['terrain_resolution'])
- if changed:
- # 确保分辨率是有效的 (2的幂次方 + 1)
- valid_resolutions = [17, 33, 65, 129, 257, 513, 1025]
- if self.dialog_params['terrain_resolution'] not in valid_resolutions:
- closest_res = min(valid_resolutions, key=lambda x: abs(x - self.dialog_params['terrain_resolution']))
- self.dialog_params['terrain_resolution'] = closest_res
-
- imgui.separator()
- imgui.text("有效分辨率值: 17, 33, 65, 129, 257, 513, 1025")
-
- imgui.separator()
-
- # 按钮
- if imgui.button("创建"):
- try:
- width = self.dialog_params['terrain_width']
- height = self.dialog_params['terrain_height']
- resolution = self.dialog_params['terrain_resolution']
-
- # 转换为地形管理器期望的格式
- size = (width, height)
-
- result = self.createFlatTerrain(size, resolution)
- if result:
- self.add_success_message("平面地形创建成功")
- self.show_terrain_dialog = False
- else:
- self.add_error_message("平面地形创建失败")
- except Exception as e:
- self.add_error_message(f"创建平面地形失败: {str(e)}")
-
- imgui.same_line()
- if imgui.button("取消"):
- self.show_terrain_dialog = False
-
- def _draw_script_dialog(self):
- """绘制脚本创建对话框"""
- if not self.show_script_dialog:
- return
-
- flags = (imgui.WindowFlags_.no_resize |
- imgui.WindowFlags_.no_collapse |
- imgui.WindowFlags_.modal)
-
- with imgui_ctx.begin("创建脚本", self.show_script_dialog, flags) as window:
- if not window:
- self.show_script_dialog = False
- return
-
- # 初始化参数
- if 'script_name' not in self.dialog_params:
- self.dialog_params['script_name'] = "new_script"
- if 'script_template' not in self.dialog_params:
- self.dialog_params['script_template'] = 0 # 0=basic, 1=movement
-
- imgui.text("脚本参数设置")
- imgui.separator()
-
- # 脚本名称输入
- changed, self.dialog_params['script_name'] = imgui.input_text("脚本名称", self.dialog_params['script_name'], 256)
-
- # 模板选择
- templates = ["基础模板", "移动模板"]
- changed, self.dialog_params['script_template'] = imgui.combo("模板类型", self.dialog_params['script_template'], templates)
-
- imgui.separator()
-
- # 模板说明
- if self.dialog_params['script_template'] == 0:
- imgui.text_colored((0.7, 0.7, 0.7, 1.0), "基础模板: 包含基本的脚本结构")
- else:
- imgui.text_colored((0.7, 0.7, 0.7, 1.0), "移动模板: 包含移动相关的基本功能")
-
- imgui.separator()
-
- # 按钮
- if imgui.button("创建"):
- try:
- script_name = self.dialog_params['script_name']
- if not script_name:
- self.add_error_message("请输入脚本名称")
- return
-
- template = "basic" if self.dialog_params['script_template'] == 0 else "movement"
-
- result = self.createScript(script_name, template)
- if result:
- self.add_success_message(f"脚本创建成功: {script_name}")
-
- # 如果启用了热重载,自动加载新脚本
- if self.hotReloadEnabled:
- try:
- self.loadScript(result)
- self.add_info_message(f"脚本已自动加载: {script_name}")
- except Exception as e:
- self.add_warning_message(f"脚本自动加载失败: {str(e)}")
-
- self.show_script_dialog = False
- else:
- self.add_error_message("脚本创建失败")
- except Exception as e:
- self.add_error_message(f"创建脚本失败: {str(e)}")
-
- imgui.same_line()
- if imgui.button("取消"):
- self.show_script_dialog = False
-
- def _draw_script_browser(self):
- """绘制脚本文件浏览器"""
- if not self.show_script_browser:
- return
-
- flags = (imgui.WindowFlags_.no_resize |
- imgui.WindowFlags_.no_collapse |
- imgui.WindowFlags_.modal)
-
- with imgui_ctx.begin("选择脚本文件", self.show_script_browser, flags) as window:
- if not window:
- self.show_script_browser = False
- return
-
- imgui.text("选择脚本文件")
- imgui.separator()
-
- # 导航按钮
- if imgui.button("上级目录"):
- parent_path = os.path.dirname(self.script_browser_current_path)
- if parent_path != self.script_browser_current_path:
- self.script_browser_current_path = parent_path
- self._refresh_script_browser()
-
- imgui.same_line()
- if imgui.button("脚本目录"):
- # 切换到脚本目录
- if hasattr(self, 'script_manager') and self.script_manager:
- scripts_dir = self.script_manager.scripts_directory
- if os.path.exists(scripts_dir):
- self.script_browser_current_path = scripts_dir
- self._refresh_script_browser()
-
- imgui.same_line()
- if imgui.button("当前目录"):
- self.script_browser_current_path = os.getcwd()
- self._refresh_script_browser()
-
- imgui.separator()
-
- # 当前路径显示
- imgui.text("当前路径:")
- imgui.same_line()
- imgui.text_colored((0.7, 0.7, 0.7, 1.0), self.script_browser_current_path)
-
- imgui.separator()
-
- # 文件列表
- if imgui.begin_child("script_file_list", (580, 300)):
- for item in self.script_browser_items:
- if item['is_dir']:
- # 目录
- imgui.text_colored((0.3, 0.8, 1.0, 1.0), f"📁 {item['name']}")
- if imgui.is_item_clicked():
- self.script_browser_current_path = item['path']
- self._refresh_script_browser()
- else:
- # Python文件
- imgui.text(f"📄 {item['name']}")
- if imgui.is_item_clicked():
- self.script_browser_selected_path = item['path']
- imgui.end_child()
-
- imgui.separator()
-
- # 选中的文件信息
- if self.script_browser_selected_path and os.path.exists(self.script_browser_selected_path):
- file_size = os.path.getsize(self.script_browser_selected_path)
- imgui.text(f"文件大小: {file_size / 1024:.2f} KB")
-
- file_ext = os.path.splitext(self.script_browser_selected_path)[1].lower()
- if file_ext == '.py':
- imgui.text_colored((0.176, 1.0, 0.769, 1.0), "✓ Python脚本文件")
- else:
- imgui.text_colored((1.0, 0.3, 0.3, 1.0), "✗ 不是Python脚本文件")
- else:
- imgui.text_colored((0.7, 0.7, 0.7, 1.0), "请选择有效的Python脚本文件")
-
- imgui.separator()
-
- # 按钮
- can_load = (self.script_browser_selected_path and
- os.path.exists(self.script_browser_selected_path) and
- os.path.splitext(self.script_browser_selected_path)[1].lower() == '.py')
-
- if can_load:
- if imgui.button("加载脚本"):
- try:
- result = self.loadScript(self.script_browser_selected_path)
- if result:
- script_name = os.path.basename(self.script_browser_selected_path)
- self.add_success_message(f"脚本加载成功: {script_name}")
- self.show_script_browser = False
- else:
- self.add_error_message("脚本加载失败")
- except Exception as e:
- self.add_error_message(f"加载脚本失败: {str(e)}")
- else:
- imgui.push_style_var(imgui.StyleVar_.alpha, 0.5)
- imgui.button("加载脚本")
- imgui.pop_style_var()
-
- imgui.same_line()
- if imgui.button("取消"):
- self.show_script_browser = False
- self.script_browser_selected_path = ""
-
- def _refresh_script_browser(self):
- """刷新脚本浏览器内容"""
- try:
- self.script_browser_items = []
-
- if not os.path.exists(self.script_browser_current_path):
- self.script_browser_current_path = os.getcwd()
-
- # 获取目录中的所有项目
- items = []
- try:
- for item_name in os.listdir(self.script_browser_current_path):
- item_path = os.path.join(self.script_browser_current_path, item_name)
-
- if os.path.isdir(item_path):
- items.append({
- 'name': item_name,
- 'path': item_path,
- 'is_dir': True
- })
- elif os.path.isfile(item_path):
- file_ext = os.path.splitext(item_name)[1].lower()
- if file_ext == '.py': # 只显示Python文件
- items.append({
- 'name': item_name,
- 'path': item_path,
- 'is_dir': False
- })
- except PermissionError:
- print(f"权限错误: 无法访问目录 {self.script_browser_current_path}")
-
- # 排序:目录在前,文件在后
- items.sort(key=lambda x: (not x['is_dir'], x['name'].lower()))
- self.script_browser_items = items
-
- except Exception as e:
- print(f"刷新脚本浏览器时出错: {e}")
- self.script_browser_items = []
-
- def _draw_heightmap_browser(self):
- """绘制高度图文件浏览器"""
- if not self.show_heightmap_browser:
- return
-
- flags = (imgui.WindowFlags_.no_resize |
- imgui.WindowFlags_.no_collapse |
- imgui.WindowFlags_.modal)
-
- with imgui_ctx.begin("选择高度图文件", self.show_heightmap_browser, flags) as window:
- if not window:
- self.show_heightmap_browser = False
- return
-
- imgui.text("选择高度图文件")
- imgui.separator()
-
- # 导航按钮
- if imgui.button("上级目录"):
- parent_path = os.path.dirname(self.heightmap_browser_current_path)
- if parent_path != self.heightmap_browser_current_path:
- self.heightmap_browser_current_path = parent_path
- self._refresh_heightmap_browser()
-
- imgui.same_line()
- if imgui.button("主目录"):
- self.heightmap_browser_current_path = os.getcwd()
- self._refresh_heightmap_browser()
-
- imgui.same_line()
- if imgui.button("当前目录"):
- self.heightmap_browser_current_path = os.getcwd()
- self._refresh_heightmap_browser()
-
- imgui.separator()
-
- # 当前路径显示
- imgui.text("当前路径:")
- imgui.same_line()
- imgui.text_colored((0.7, 0.7, 0.7, 1.0), self.heightmap_browser_current_path)
-
- imgui.separator()
-
- # 文件列表
- if imgui.begin_child("file_list", (580, 300)):
- for item in self.heightmap_browser_items:
- if item['is_dir']:
- # 目录
- imgui.text_colored((0.3, 0.8, 1.0, 1.0), f"📁 {item['name']}")
- if imgui.is_item_clicked():
- self.heightmap_browser_current_path = item['path']
- self._refresh_heightmap_browser()
- else:
- # 文件
- imgui.text(f"📄 {item['name']}")
- if imgui.is_item_clicked():
- self.heightmap_browser_selected_path = item['path']
- self.heightmap_file_path = item['path']
- imgui.end_child()
-
- imgui.separator()
-
- # 支持的格式说明
- imgui.text("支持的文件格式:")
- formats_text = ", ".join(self.supported_heightmap_formats)
- imgui.text_colored((0.7, 0.7, 0.7, 1.0), formats_text)
-
- imgui.separator()
-
- # 选中的文件信息
- if self.heightmap_file_path and os.path.exists(self.heightmap_file_path):
- file_size = os.path.getsize(self.heightmap_file_path)
- imgui.text(f"文件大小: {file_size / 1024:.2f} KB")
-
- file_ext = os.path.splitext(self.heightmap_file_path)[1].lower()
- if file_ext in self.supported_heightmap_formats:
- imgui.text_colored((0.176, 1.0, 0.769, 1.0), "✓ 文件格式支持")
- else:
- imgui.text_colored((1.0, 0.3, 0.3, 1.0), "✗ 不支持的文件格式")
- else:
- imgui.text_colored((0.7, 0.7, 0.7, 1.0), "请选择有效的高度图文件")
-
- imgui.separator()
-
- # 按钮
- can_import = (self.heightmap_file_path and
- os.path.exists(self.heightmap_file_path) and
- os.path.splitext(self.heightmap_file_path)[1].lower() in self.supported_heightmap_formats)
-
- if can_import:
- if imgui.button("创建地形"):
- try:
- # 使用默认缩放参数创建地形
- scale = (1.0, 1.0, 10.0) # X, Y, Z缩放
- result = self.createTerrainFromHeightMap(self.heightmap_file_path, scale)
- if result:
- self.add_success_message("高度图地形创建成功")
- self.show_heightmap_browser = False
- else:
- self.add_error_message("高度图地形创建失败")
- except Exception as e:
- self.add_error_message(f"创建高度图地形失败: {str(e)}")
- else:
- imgui.push_style_var(imgui.StyleVar_.alpha, 0.5)
- imgui.button("创建地形")
- imgui.pop_style_var()
-
- imgui.same_line()
- if imgui.button("取消"):
- self.show_heightmap_browser = False
- self.heightmap_file_path = ""
-
- def _refresh_heightmap_browser(self):
- """刷新高度图浏览器内容"""
- try:
- self.heightmap_browser_items = []
-
- if not os.path.exists(self.heightmap_browser_current_path):
- self.heightmap_browser_current_path = os.getcwd()
-
- # 获取目录中的所有项目
- items = []
- try:
- for item_name in os.listdir(self.heightmap_browser_current_path):
- item_path = os.path.join(self.heightmap_browser_current_path, item_name)
-
- if os.path.isdir(item_path):
- items.append({
- 'name': item_name,
- 'path': item_path,
- 'is_dir': True
- })
- elif os.path.isfile(item_path):
- file_ext = os.path.splitext(item_name)[1].lower()
- if file_ext in self.supported_heightmap_formats:
- items.append({
- 'name': item_name,
- 'path': item_path,
- 'is_dir': False
- })
- except PermissionError:
- print(f"权限错误: 无法访问目录 {self.heightmap_browser_current_path}")
-
- # 排序:目录在前,文件在后
- items.sort(key=lambda x: (not x['is_dir'], x['name'].lower()))
- self.heightmap_browser_items = items
-
- except Exception as e:
- print(f"刷新高度图浏览器时出错: {e}")
- self.heightmap_browser_items = []
-
- # ==================== 导入功能实现 ====================
-
- def _on_import_model(self):
- """处理导入模型菜单项"""
- self.add_info_message("打开导入模型对话框")
- self.show_import_dialog = True
-
- def _import_model(self):
- """导入模型的具体实现"""
- try:
- if not self.import_file_path:
- self.add_error_message("请选择要导入的文件")
- return
-
- if not os.path.exists(self.import_file_path):
- self.add_error_message(f"文件不存在: {self.import_file_path}")
- return
-
- # 检查文件格式
- file_ext = os.path.splitext(self.import_file_path)[1].lower()
- if file_ext not in self.supported_formats:
- self.add_error_message(f"不支持的文件格式: {file_ext}")
- return
-
- # 调用场景管理器导入模型
- if hasattr(self, 'scene_manager') and self.scene_manager:
- self.add_info_message(f"正在导入模型: {os.path.basename(self.import_file_path)}")
-
- # 导入模型
- model_node = self.scene_manager.importModel(self.import_file_path)
-
- if model_node:
- # 添加材质处理确保颜色正常
- if hasattr(self.scene_manager, 'processMaterials'):
- self.scene_manager.processMaterials(model_node)
- self.add_info_message("已应用默认材质")
-
- # 额外的材质处理,确保颜色正确显示
- try:
- # 强制刷新模型显示
- model_node.clearMaterial()
- model_node.clearTexture()
-
- # 重新应用材质
- if hasattr(self.scene_manager, 'processMaterials'):
- self.scene_manager.processMaterials(model_node)
-
- # 设置默认的基础颜色(如果模型没有颜色)
- try:
- color = model_node.getColor()
- if color and len(color) >= 4 and color == (1, 1, 1, 1): # 默认白色
- model_node.setColor(0.8, 0.8, 0.8, 1.0) # 设置为中性灰
- elif not color: # 如果没有颜色
- model_node.setColor(0.8, 0.8, 0.8, 1.0) # 设置为中性灰
- except:
- # 如果getColor失败,直接设置默认颜色
- model_node.setColor(0.8, 0.8, 0.8, 1.0) # 设置为中性灰
-
- except Exception as e:
- self.add_warning_message(f"材质处理警告: {e}")
-
- # 设置模型位置
- model_node.setPos(0, 0, 0)
-
- # 添加到场景管理器的模型列表
- if hasattr(self.scene_manager, 'models'):
- self.scene_manager.models.append(model_node)
-
- # 选中新导入的模型
- if hasattr(self, 'selection') and self.selection:
- self.selection.selectNode(model_node)
-
- self.add_success_message(f"模型导入成功: {os.path.basename(self.import_file_path)}")
- else:
- self.add_error_message("模型导入失败")
- else:
- self.add_error_message("场景管理器未初始化")
-
- except Exception as e:
- self.add_error_message(f"导入模型失败: {e}")
-
- # 清空导入路径
- self.import_file_path = ""
-
+ def _get_current_collision_shape_type(self, *args, **kwargs):
+ return self.property_helpers._get_current_collision_shape_type(*args, **kwargs)
+
+ def _get_collision_position_offset(self, *args, **kwargs):
+ return self.property_helpers._get_collision_position_offset(*args, **kwargs)
+
+ def _is_collision_visible(self, *args, **kwargs):
+ return self.property_helpers._is_collision_visible(*args, **kwargs)
+
+ def _add_collision_to_node(self, *args, **kwargs):
+ return self.property_helpers._add_collision_to_node(*args, **kwargs)
+
+ def _remove_collision_from_node(self, *args, **kwargs):
+ return self.property_helpers._remove_collision_from_node(*args, **kwargs)
+
+ def _toggle_collision_visibility(self, *args, **kwargs):
+ return self.property_helpers._toggle_collision_visibility(*args, **kwargs)
+
+ def _update_collision_position(self, *args, **kwargs):
+ return self.property_helpers._update_collision_position(*args, **kwargs)
+
+ def _get_shape_type_from_name(self, *args, **kwargs):
+ return self.property_helpers._get_shape_type_from_name(*args, **kwargs)
+
+ def _get_sphere_radius(self, *args, **kwargs):
+ return self.property_helpers._get_sphere_radius(*args, **kwargs)
+
+ def _update_sphere_radius(self, *args, **kwargs):
+ return self.property_helpers._update_sphere_radius(*args, **kwargs)
+
+ def _get_box_size(self, *args, **kwargs):
+ return self.property_helpers._get_box_size(*args, **kwargs)
+
+ def _update_box_size(self, *args, **kwargs):
+ return self.property_helpers._update_box_size(*args, **kwargs)
+
+ def _get_capsule_radius(self, *args, **kwargs):
+ return self.property_helpers._get_capsule_radius(*args, **kwargs)
+
+ def _update_capsule_radius(self, *args, **kwargs):
+ return self.property_helpers._update_capsule_radius(*args, **kwargs)
+
+ def _get_capsule_height(self, *args, **kwargs):
+ return self.property_helpers._get_capsule_height(*args, **kwargs)
+
+ def _update_capsule_height(self, *args, **kwargs):
+ return self.property_helpers._update_capsule_height(*args, **kwargs)
+
+ def _get_plane_normal(self, *args, **kwargs):
+ return self.property_helpers._get_plane_normal(*args, **kwargs)
+
+ def _update_plane_normal(self, *args, **kwargs):
+ return self.property_helpers._update_plane_normal(*args, **kwargs)
+
+ def _manual_collision_detection(self, *args, **kwargs):
+ return self.property_helpers._manual_collision_detection(*args, **kwargs)
+
+ def _update_node_name(self, *args, **kwargs):
+ return self.property_helpers._update_node_name(*args, **kwargs)
+
+ def _get_material_base_color(self, *args, **kwargs):
+ return self.property_helpers._get_material_base_color(*args, **kwargs)
+
+ def _update_material_base_color(self, *args, **kwargs):
+ return self.property_helpers._update_material_base_color(*args, **kwargs)
+
+ def _update_material_roughness(self, *args, **kwargs):
+ return self.property_helpers._update_material_roughness(*args, **kwargs)
+
+ def _update_material_metallic(self, *args, **kwargs):
+ return self.property_helpers._update_material_metallic(*args, **kwargs)
+
+ def _update_material_ior(self, *args, **kwargs):
+ return self.property_helpers._update_material_ior(*args, **kwargs)
+
+ def _apply_material_preset(self, *args, **kwargs):
+ return self.property_helpers._apply_material_preset(*args, **kwargs)
+
+ def _apply_material_to_node(self, *args, **kwargs):
+ return self.property_helpers._apply_material_to_node(*args, **kwargs)
+
+ def _reset_material(self, *args, **kwargs):
+ return self.property_helpers._reset_material(*args, **kwargs)
+
+ def _select_texture_for_material(self, *args, **kwargs):
+ return self.property_helpers._select_texture_for_material(*args, **kwargs)
+
+ def _apply_texture_to_material(self, *args, **kwargs):
+ return self.property_helpers._apply_texture_to_material(*args, **kwargs)
+
+ def _clear_all_textures(self, *args, **kwargs):
+ return self.property_helpers._clear_all_textures(*args, **kwargs)
+
+ def _display_current_textures(self, *args, **kwargs):
+ return self.property_helpers._display_current_textures(*args, **kwargs)
+
+ def _update_shading_model(self, *args, **kwargs):
+ return self.property_helpers._update_shading_model(*args, **kwargs)
+
+ def _update_transparency(self, *args, **kwargs):
+ return self.property_helpers._update_transparency(*args, **kwargs)
+
+ def _draw_texture_file_dialog(self, *args, **kwargs):
+ return self.property_helpers._draw_texture_file_dialog(*args, **kwargs)
+
+ def start_transform_monitoring(self, *args, **kwargs):
+ return self.property_helpers.start_transform_monitoring(*args, **kwargs)
+
+ def stop_transform_monitoring(self, *args, **kwargs):
+ return self.property_helpers.stop_transform_monitoring(*args, **kwargs)
+
+ def _update_last_transform_values(self, *args, **kwargs):
+ return self.property_helpers._update_last_transform_values(*args, **kwargs)
+
+ def _check_transform_changes(self, *args, **kwargs):
+ return self.property_helpers._check_transform_changes(*args, **kwargs)
+
+ def update_transform_monitoring(self, *args, **kwargs):
+ return self.property_helpers.update_transform_monitoring(*args, **kwargs)
+
+ def show_color_picker(self, *args, **kwargs):
+ return self.property_helpers.show_color_picker(*args, **kwargs)
+
+ def _draw_color_picker(self, *args, **kwargs):
+ return self.property_helpers._draw_color_picker(*args, **kwargs)
+
+ def _apply_color_selection(self, *args, **kwargs):
+ return self.property_helpers._apply_color_selection(*args, **kwargs)
+
+ def _draw_color_button(self, *args, **kwargs):
+ return self.property_helpers._draw_color_button(*args, **kwargs)
+
+ def _refresh_available_fonts(self, *args, **kwargs):
+ return self.property_helpers._refresh_available_fonts(*args, **kwargs)
+
+ def show_font_selector(self, *args, **kwargs):
+ return self.property_helpers.show_font_selector(*args, **kwargs)
+
+ def _draw_font_selector(self, *args, **kwargs):
+ return self.property_helpers._draw_font_selector(*args, **kwargs)
+
+ def _apply_font_selection(self, *args, **kwargs):
+ return self.property_helpers._apply_font_selection(*args, **kwargs)
+
+ def _draw_font_selector_button(self, *args, **kwargs):
+ return self.property_helpers._draw_font_selector_button(*args, **kwargs)
+ def _draw_console(self, *args, **kwargs):
+ return self.script_panels._draw_console(*args, **kwargs)
+
+ def _draw_script_panel(self, *args, **kwargs):
+ return self.script_panels._draw_script_panel(*args, **kwargs)
+
+ def _draw_script_status_group(self, *args, **kwargs):
+ return self.script_panels._draw_script_status_group(*args, **kwargs)
+
+ def _draw_create_script_group(self, *args, **kwargs):
+ return self.script_panels._draw_create_script_group(*args, **kwargs)
+
+ def _draw_available_scripts_group(self, *args, **kwargs):
+ return self.script_panels._draw_available_scripts_group(*args, **kwargs)
+
+ def _draw_script_mounting_group(self, *args, **kwargs):
+ return self.script_panels._draw_script_mounting_group(*args, **kwargs)
+
+
+ def _toggle_hot_reload(self, *args, **kwargs):
+ return self.app_actions._toggle_hot_reload(*args, **kwargs)
+
+ def _create_new_script(self, *args, **kwargs):
+ return self.app_actions._create_new_script(*args, **kwargs)
+
+ def _refresh_scripts_list(self, *args, **kwargs):
+ return self.app_actions._refresh_scripts_list(*args, **kwargs)
+
+ def _reload_all_scripts(self, *args, **kwargs):
+ return self.app_actions._reload_all_scripts(*args, **kwargs)
+
+ def _on_script_selected(self, *args, **kwargs):
+ return self.app_actions._on_script_selected(*args, **kwargs)
+
+ def _edit_script(self, *args, **kwargs):
+ return self.app_actions._edit_script(*args, **kwargs)
+
+ def _mount_script_to_selected(self, *args, **kwargs):
+ return self.app_actions._mount_script_to_selected(*args, **kwargs)
+
+ def _unmount_script_from_selected(self, *args, **kwargs):
+ return self.app_actions._unmount_script_from_selected(*args, **kwargs)
+
+ def _on_new_project(self, *args, **kwargs):
+ return self.app_actions._on_new_project(*args, **kwargs)
+
+ def _on_open_project(self, *args, **kwargs):
+ return self.app_actions._on_open_project(*args, **kwargs)
+
+ def _on_save_project(self, *args, **kwargs):
+ return self.app_actions._on_save_project(*args, **kwargs)
+
+ def _on_save_as_project(self, *args, **kwargs):
+ return self.app_actions._on_save_as_project(*args, **kwargs)
+
+ def _on_exit(self, *args, **kwargs):
+ return self.app_actions._on_exit(*args, **kwargs)
+
+ def _on_ctrl_pressed(self, *args, **kwargs):
+ return self.app_actions._on_ctrl_pressed(*args, **kwargs)
+
+ def _on_ctrl_released(self, *args, **kwargs):
+ return self.app_actions._on_ctrl_released(*args, **kwargs)
+
+ def _on_alt_pressed(self, *args, **kwargs):
+ return self.app_actions._on_alt_pressed(*args, **kwargs)
+
+ def _on_alt_released(self, *args, **kwargs):
+ return self.app_actions._on_alt_released(*args, **kwargs)
+
+ def _on_n_pressed(self, *args, **kwargs):
+ return self.app_actions._on_n_pressed(*args, **kwargs)
+
+ def _on_o_pressed(self, *args, **kwargs):
+ return self.app_actions._on_o_pressed(*args, **kwargs)
+
+ def _on_f4_pressed(self, *args, **kwargs):
+ return self.app_actions._on_f4_pressed(*args, **kwargs)
+
+ def _on_delete_pressed(self, *args, **kwargs):
+ return self.app_actions._on_delete_pressed(*args, **kwargs)
+
+ def _on_escape_pressed(self, *args, **kwargs):
+ return self.app_actions._on_escape_pressed(*args, **kwargs)
+
+ def _on_wheel_up(self, *args, **kwargs):
+ return self.app_actions._on_wheel_up(*args, **kwargs)
+
+ def _on_wheel_down(self, *args, **kwargs):
+ return self.app_actions._on_wheel_down(*args, **kwargs)
+
+ def _is_mouse_over_imgui(self, *args, **kwargs):
+ return self.app_actions._is_mouse_over_imgui(*args, **kwargs)
+
+ def processImGuiMouseClick(self, *args, **kwargs):
+ return self.app_actions.processImGuiMouseClick(*args, **kwargs)
+
+ def add_message(self, *args, **kwargs):
+ return self.app_actions.add_message(*args, **kwargs)
+
+ def add_success_message(self, *args, **kwargs):
+ return self.app_actions.add_success_message(*args, **kwargs)
+
+ def add_error_message(self, *args, **kwargs):
+ return self.app_actions.add_error_message(*args, **kwargs)
+
+ def add_warning_message(self, *args, **kwargs):
+ return self.app_actions.add_warning_message(*args, **kwargs)
+
+ def add_info_message(self, *args, **kwargs):
+ return self.app_actions.add_info_message(*args, **kwargs)
+
+ def _on_undo(self, *args, **kwargs):
+ return self.app_actions._on_undo(*args, **kwargs)
+
+ def _on_redo(self, *args, **kwargs):
+ return self.app_actions._on_redo(*args, **kwargs)
+
+ def _on_copy(self, *args, **kwargs):
+ return self.app_actions._on_copy(*args, **kwargs)
+
+ def _on_cut(self, *args, **kwargs):
+ return self.app_actions._on_cut(*args, **kwargs)
+
+ def _on_paste(self, *args, **kwargs):
+ return self.app_actions._on_paste(*args, **kwargs)
+
+ def _on_delete(self, *args, **kwargs):
+ return self.app_actions._on_delete(*args, **kwargs)
+
+ def _delete_node(self, *args, **kwargs):
+ return self.app_actions._delete_node(*args, **kwargs)
+
+ def _perform_node_cleanup(self, *args, **kwargs):
+ return self.app_actions._perform_node_cleanup(*args, **kwargs)
+
+ def _create_new_project(self, *args, **kwargs):
+ return self.app_actions._create_new_project(*args, **kwargs)
+
+ def _open_project_path(self, *args, **kwargs):
+ return self.app_actions._open_project_path(*args, **kwargs)
+
+ def _save_project_impl(self, *args, **kwargs):
+ return self.app_actions._save_project_impl(*args, **kwargs)
+
+ def _open_project_impl(self, *args, **kwargs):
+ return self.app_actions._open_project_impl(*args, **kwargs)
+
+ def _create_new_project_impl(self, *args, **kwargs):
+ return self.app_actions._create_new_project_impl(*args, **kwargs)
+
+ def _update_window_title(self, *args, **kwargs):
+ return self.app_actions._update_window_title(*args, **kwargs)
+
+ def _import_model_for_runtime(self, *args, **kwargs):
+ return self.app_actions._import_model_for_runtime(*args, **kwargs)
+
+ def _on_import_model(self, *args, **kwargs):
+ return self.app_actions._on_import_model(*args, **kwargs)
+
+ def _import_model(self, *args, **kwargs):
+ return self.app_actions._import_model(*args, **kwargs)
+ def _draw_new_project_dialog(self, *args, **kwargs):
+ return self.dialog_panels._draw_new_project_dialog(*args, **kwargs)
+
+ def _draw_open_project_dialog(self, *args, **kwargs):
+ return self.dialog_panels._draw_open_project_dialog(*args, **kwargs)
+
+ def _draw_path_browser(self, *args, **kwargs):
+ return self.dialog_panels._draw_path_browser(*args, **kwargs)
+
+ def _draw_import_dialog(self, *args, **kwargs):
+ return self.dialog_panels._draw_import_dialog(*args, **kwargs)
+
+ def _refresh_path_browser(self, *args, **kwargs):
+ return self.dialog_panels._refresh_path_browser(*args, **kwargs)
+
+ def _apply_selected_path(self, *args, **kwargs):
+ return self.dialog_panels._apply_selected_path(*args, **kwargs)
+
+ def _draw_gui_button_dialog(self, *args, **kwargs):
+ return self.dialog_panels._draw_gui_button_dialog(*args, **kwargs)
+
+ def _draw_gui_label_dialog(self, *args, **kwargs):
+ return self.dialog_panels._draw_gui_label_dialog(*args, **kwargs)
+
+ def _draw_gui_entry_dialog(self, *args, **kwargs):
+ return self.dialog_panels._draw_gui_entry_dialog(*args, **kwargs)
+
+ def _draw_gui_image_dialog(self, *args, **kwargs):
+ return self.dialog_panels._draw_gui_image_dialog(*args, **kwargs)
+
+ def _draw_3d_text_dialog(self, *args, **kwargs):
+ return self.dialog_panels._draw_3d_text_dialog(*args, **kwargs)
+
+ def _draw_3d_image_dialog(self, *args, **kwargs):
+ return self.dialog_panels._draw_3d_image_dialog(*args, **kwargs)
+
+ def _draw_video_screen_dialog(self, *args, **kwargs):
+ return self.dialog_panels._draw_video_screen_dialog(*args, **kwargs)
+
+ def _draw_2d_video_screen_dialog(self, *args, **kwargs):
+ return self.dialog_panels._draw_2d_video_screen_dialog(*args, **kwargs)
+
+ def _draw_spherical_video_dialog(self, *args, **kwargs):
+ return self.dialog_panels._draw_spherical_video_dialog(*args, **kwargs)
+
+ def _draw_virtual_screen_dialog(self, *args, **kwargs):
+ return self.dialog_panels._draw_virtual_screen_dialog(*args, **kwargs)
+
+ def _draw_spot_light_dialog(self, *args, **kwargs):
+ return self.dialog_panels._draw_spot_light_dialog(*args, **kwargs)
+
+ def _draw_point_light_dialog(self, *args, **kwargs):
+ return self.dialog_panels._draw_point_light_dialog(*args, **kwargs)
+
+ def _draw_terrain_dialog(self, *args, **kwargs):
+ return self.dialog_panels._draw_terrain_dialog(*args, **kwargs)
+
+ def _draw_script_dialog(self, *args, **kwargs):
+ return self.dialog_panels._draw_script_dialog(*args, **kwargs)
+
+ def _draw_script_browser(self, *args, **kwargs):
+ return self.dialog_panels._draw_script_browser(*args, **kwargs)
+
+ def _refresh_script_browser(self, *args, **kwargs):
+ return self.dialog_panels._refresh_script_browser(*args, **kwargs)
+
+ def _draw_heightmap_browser(self, *args, **kwargs):
+ return self.dialog_panels._draw_heightmap_browser(*args, **kwargs)
+
+ def _refresh_heightmap_browser(self, *args, **kwargs):
+ return self.dialog_panels._refresh_heightmap_browser(*args, **kwargs)
def setup_drag_drop_support(self):
"""设置拖拽支持"""
try:
@@ -7206,7 +1400,7 @@ class MyWorld(CoreWorld):
return False
# 导入模型
- model_node = self.scene_manager.importModel(file_path)
+ model_node = self._import_model_for_runtime(file_path)
if model_node:
# 应用材质确保颜色正常
@@ -7228,787 +1422,136 @@ class MyWorld(CoreWorld):
self.add_message("error", f"导入模型时发生错误: {str(e)}")
return False
- def _draw_drag_drop_interface(self):
- """绘制拖拽界面"""
- # 检查资源管理器的拖拽状态
- if self.resource_manager.is_dragging():
- self.is_dragging = True
- self.dragged_files = self.resource_manager.get_dragged_files()
- self.show_drag_overlay = True
-
- # 绘制拖拽覆盖层
- if self.show_drag_overlay:
- self._draw_drag_overlay()
-
- # 检查是否有拖拽的文件需要处理
- if self.is_dragging and self.dragged_files:
- # 显示拖拽状态
- self._draw_drag_status()
-
- # 检查是否释放鼠标(结束拖拽)
- if imgui.is_mouse_released(0):
- self._handle_drag_drop_completion()
-
- def _handle_drag_drop_completion(self):
- """处理拖拽完成"""
- # 检查是否在3D视图中释放
- mouse_pos = imgui.get_mouse_pos()
- viewport = imgui.get_main_viewport()
-
- # 简单检查:如果不在任何ImGui窗口上,则认为是在3D视图中
- if not imgui.is_any_window_hovered():
- # 导入支持的3D模型文件
- imported_count = 0
- for file_path in self.dragged_files:
- if file_path.suffix.lower() in ['.gltf', '.glb', '.fbx', '.bam', '.egg', '.obj']:
- try:
- self.scene_manager.importModel(str(file_path))
- self.add_success_message(f"成功导入模型: {file_path.name}")
- imported_count += 1
- except Exception as e:
- self.add_error_message(f"导入模型失败 {file_path.name}: {e}")
-
- if imported_count > 0:
- self.add_success_message(f"共导入 {imported_count} 个模型")
-
- # 清除拖拽状态
- self.is_dragging = False
- self.dragged_files.clear()
- self.show_drag_overlay = False
- self.resource_manager.clear_drag()
-
- def _draw_drag_overlay(self):
- """绘制拖拽覆盖层"""
- viewport = imgui.get_main_viewport()
- imgui.set_next_window_pos((0, 0))
- imgui.set_next_window_size(viewport.work_size)
-
- flags = (
- imgui.WindowFlags_.no_title_bar |
- imgui.WindowFlags_.no_resize |
- imgui.WindowFlags_.no_move |
- imgui.WindowFlags_.no_scrollbar |
- imgui.WindowFlags_.no_saved_settings |
- imgui.WindowFlags_.no_background |
- imgui.WindowFlags_.no_focus_on_appearing
- )
-
- imgui.begin("##DragOverlay", True, flags)
-
- # 绘制半透明背景
- draw_list = imgui.get_window_draw_list()
- draw_list.add_rect_filled(
- (0, 0), viewport.work_size,
- imgui.get_color_u32((0, 0, 0, 0.1))
- )
-
- # 绘制提示文本
- text_size = imgui.calc_text_size("释放以导入文件")
- text_pos = (
- (viewport.work_size.x - text_size.x) / 2,
- (viewport.work_size.y - text_size.y) / 2
- )
-
- draw_list.add_text(
- text_pos,
- imgui.get_color_u32((1, 1, 1, 1)),
- "释放以导入文件"
- )
-
- imgui.end()
-
- def _draw_drag_status(self):
- """绘制拖拽状态"""
- viewport = imgui.get_main_viewport()
-
- # 在右下角显示拖拽状态
- imgui.set_next_window_pos(
- (viewport.work_size.x - 300, viewport.work_size.y - 150),
- imgui.Cond_.first_use_ever
- )
-
- flags = (
- imgui.WindowFlags_.no_title_bar |
- imgui.WindowFlags_.no_resize |
- imgui.WindowFlags_.no_move |
- imgui.WindowFlags_.no_scrollbar |
- imgui.WindowFlags_.no_saved_settings
- )
-
- with imgui_ctx.begin("拖拽状态", True, flags):
- imgui.text("拖拽的文件:")
- for file_path in self.dragged_files:
- filename = os.path.basename(file_path)
- imgui.text(f" • {filename}")
-
- imgui.separator()
-
- if imgui.button("导入所有文件"):
- self.process_dragged_files()
-
- imgui.same_line()
- if imgui.button("取消"):
- self.clear_dragged_files()
-
- def _draw_context_menus(self):
- """绘制右键菜单"""
- # 节点右键菜单
- if hasattr(self, '_context_menu_node') and self._context_menu_node:
- imgui.open_popup("节点右键菜单")
- self._context_menu_node = None
-
- if imgui.begin_popup("节点右键菜单"):
- if imgui.menu_item("删除节点", "", False, True)[1]:
- if hasattr(self, '_context_menu_target') and self._context_menu_target:
- self._delete_node(self._context_menu_target)
- imgui.close_current_popup()
-
- if imgui.menu_item("重命名", "", False, True)[1]:
- self._renaming_node = True
- imgui.close_current_popup()
-
- imgui.separator()
-
- if imgui.menu_item("复制", "", False, True)[1]:
- if hasattr(self, '_context_menu_target') and self._context_menu_target:
- self._copy_node(self._context_menu_target)
- imgui.close_current_popup()
-
- if imgui.menu_item("聚焦", "", False, True)[1]:
- if hasattr(self, '_context_menu_target') and self._context_menu_target:
- if hasattr(self, 'selection') and self.selection:
- self.selection.updateSelection(self._context_menu_target)
- self.selection.focusCameraOnSelectedNodeAdvanced()
- imgui.close_current_popup()
-
- imgui.end_popup()
-
- # 重命名对话框
- if hasattr(self, '_renaming_node') and self._renaming_node:
- imgui.open_popup("重命名节点")
- if not hasattr(self, '_rename_buffer'):
- self._rename_buffer = ""
- if hasattr(self, '_context_menu_target') and self._context_menu_target:
- self._rename_buffer = self._context_menu_target.getName() or ""
-
- if imgui.begin_popup("重命名节点"):
- changed, new_name = imgui.input_text("新名称", self._rename_buffer, 256)
- if changed:
- self._rename_buffer = new_name
-
- if imgui.button("确定"):
- if hasattr(self, '_context_menu_target') and self._context_menu_target:
- self._context_menu_target.setName(self._rename_buffer)
- self._renaming_node = False
- imgui.close_current_popup()
-
- imgui.same_line()
- if imgui.button("取消"):
- self._renaming_node = False
- imgui.close_current_popup()
-
- imgui.end_popup()
-
- def _delete_node_simple(self, node):
- """删除节点 - 简化版本"""
- if not node or node.isEmpty():
- return
-
- # 从场景管理器中删除
- if hasattr(self, 'scene_manager') and self.scene_manager:
- if hasattr(self.scene_manager, 'models') and node in self.scene_manager.models:
- self.scene_manager.models.remove(node)
-
- # 从GUI管理器中删除
- if hasattr(self, 'gui_manager') and self.gui_manager:
- gui_element = None
- if hasattr(node, 'getPythonTag'):
- gui_element = node.getPythonTag('gui_element')
- if gui_element and hasattr(self.gui_manager, 'gui_elements'):
- if gui_element in self.gui_manager.gui_elements:
- self.gui_manager.gui_elements.remove(gui_element)
-
- # 使用主删除方法
- self._delete_node(node)
-
- # 获取节点名称(在删除之前)
- node_name = node.getName() if not node.isEmpty() else '未命名节点'
-
- # 删除节点本身
- node.removeNode()
-
- # 清除选择
- if hasattr(self, 'selection') and self.selection:
- if self.selection.selectedNode == node:
- self.selection.clearSelection()
-
- # 添加成功消息
- self.add_success_message(f"已删除节点: {node_name}")
-
- def _copy_node(self, node):
- """复制节点"""
- if not node or node.isEmpty():
- return
-
- # 这里可以实现节点复制逻辑
- # 暂时只显示消息
- self.add_info_message(f"复制功能暂未实现: {node.getName() or '未命名节点'}")
-
- # ==================== 创建功能实现 ====================
-
- def _on_create_empty_object(self):
- """创建空对象"""
- try:
- result = self.createEmptyObject()
- if result:
- self.add_success_message("空对象创建成功")
- else:
- self.add_error_message("空对象创建失败")
- return result
- except Exception as e:
- self.add_error_message(f"创建空对象失败: {str(e)}")
-
- def _on_create_cube(self):
- """创建立方体"""
- try:
- result = self.createCube()
- if result:
- self.add_success_message("立方体创建成功")
- else:
- self.add_error_message("立方体创建失败")
- return result
- except Exception as e:
- self.add_error_message(f"创建立方体失败: {str(e)}")
-
- def _on_create_sphere(self):
- """创建球体"""
- try:
- result = self.createSphere()
- if result:
- self.add_success_message("球体创建成功")
- else:
- self.add_error_message("球体创建失败")
- return result
- except Exception as e:
- self.add_error_message(f"创建球体失败: {str(e)}")
-
- def _on_create_cylinder(self):
- """创建圆柱体"""
- try:
- result = self.createCylinder()
- if result:
- self.add_success_message("圆柱体创建成功")
- else:
- self.add_error_message("圆柱体创建失败")
- return result
- except Exception as e:
- self.add_error_message(f"创建圆柱体失败: {str(e)}")
-
- def _on_create_plane(self):
- """创建平面"""
- try:
- result = self.createPlane()
- if result:
- self.add_success_message("平面创建成功")
- else:
- self.add_error_message("平面创建失败")
- return result
- except Exception as e:
- self.add_error_message(f"创建平面失败: {str(e)}")
-
- def _on_create_3d_text(self):
- """创建3D文本"""
- self.show_3d_text_dialog = True
-
- def _on_create_3d_image(self):
- """创建3D图片"""
- self.show_3d_image_dialog = True
-
- def _on_create_gui_button(self):
- """创建GUI按钮"""
- self.show_gui_button_dialog = True
-
- def _on_create_gui_label(self):
- """创建GUI标签"""
- self.show_gui_label_dialog = True
-
- def _on_create_gui_entry(self):
- """创建GUI输入框"""
- self.show_gui_entry_dialog = True
-
- def _on_create_gui_image(self):
- """创建GUI图片"""
- self.show_gui_image_dialog = True
-
- def _on_create_video_screen(self):
- """创建视频屏幕"""
- self.show_video_screen_dialog = True
-
- def _on_create_2d_video_screen(self):
- """创建2D视频屏幕"""
- self.show_2d_video_screen_dialog = True
-
- def _on_create_spherical_video(self):
- """创建球形视频"""
- self.show_spherical_video_dialog = True
-
- def _on_create_virtual_screen(self):
- """创建虚拟屏幕"""
- self.show_virtual_screen_dialog = True
-
- def _on_create_spot_light(self):
- """创建聚光灯"""
- self.show_spot_light_dialog = True
-
- def _on_create_point_light(self):
- """创建点光源"""
- self.show_point_light_dialog = True
-
- def _on_create_flat_terrain(self):
- """创建平面地形"""
- self.show_terrain_dialog = True
-
- def _on_create_heightmap_terrain(self):
- """从高度图创建地形"""
- self.show_heightmap_browser = True
-
- def _on_create_script(self):
- """创建脚本"""
- self.show_script_dialog = True
-
- def _on_load_script(self):
- """加载脚本文件"""
- self.show_script_browser = True
-
- def _on_reload_all_scripts(self):
- """重载所有脚本"""
- try:
- if hasattr(self, 'script_manager') and self.script_manager:
- self.script_manager.reloadAllScripts()
- self.add_success_message("所有脚本重载成功")
- else:
- self.add_error_message("脚本管理器未初始化")
- except Exception as e:
- self.add_error_message(f"重载脚本失败: {str(e)}")
-
- def _on_open_scripts_manager(self):
- """打开脚本管理器"""
- self.showScriptPanel = True
- self.add_info_message("脚本管理器已打开")
-
- def _on_create_2d_sample_panel(self):
- """创建2D示例面板"""
- try:
- result = self.create2DSamplePanel()
- if result:
- self.add_success_message("2D示例面板创建成功")
- else:
- self.add_error_message("2D示例面板创建失败")
- except Exception as e:
- self.add_error_message(f"创建2D示例面板失败: {str(e)}")
-
- def _on_create_3d_sample_panel(self):
- """创建3D实例面板"""
- try:
- result = self.create3DSamplePanel()
- if result:
- self.add_success_message("3D实例面板创建成功")
- else:
- self.add_error_message("3D实例面板创建失败")
- except Exception as e:
- self.add_error_message(f"创建3D实例面板失败: {str(e)}")
-
- def _on_create_web_panel(self):
- """创建Web面板"""
- try:
- result = self.createWebPanel()
- if result:
- self.add_success_message("Web面板创建成功")
- else:
- self.add_error_message("Web面板创建失败")
- except Exception as e:
- self.add_error_message(f"创建Web面板失败: {str(e)}")
-
- # ==================== 3D对象和GUI创建方法 ====================
-
- def createEmptyObject(self):
- """创建空对象"""
- try:
- from panda3d.core import NodePath
- # 创建一个空节点
- empty_node = NodePath("EmptyObject")
- empty_node.reparentTo(self.render)
- empty_node.setPos(0, 0, 0)
-
- # 添加到场景管理器
- if hasattr(self, 'scene_manager') and self.scene_manager:
- self.scene_manager.models.append(empty_node)
-
- # 更新场景树
- if hasattr(self, 'updateSceneTree'):
- self.updateSceneTree()
-
- print("✓ 空对象创建成功")
- return empty_node
- except Exception as e:
- print(f"✗ 创建空对象失败: {e}")
- return None
-
- def create3DText(self, pos=(0, 0, 0), text="3D Text", scale=1.0):
- """创建3D文本"""
- try:
- from panda3d.core import TextNode, NodePath
-
- # 创建文本节点
- text_node = TextNode("3DText")
- text_node.setText(text)
- text_node.setAlign(TextNode.ACenter)
- text_node.setTextColor(1, 1, 1, 1) # 白色
-
- # 设置中文字体
- chinese_font = self._get_chinese_font()
- if chinese_font:
- text_node.setFont(chinese_font)
-
- # 创建节点路径并设置位置
- text_np = NodePath(text_node)
- text_np.reparentTo(self.render)
- text_np.setPos(pos)
- text_np.setScale(scale)
-
- # 添加到场景管理器
- if hasattr(self, 'scene_manager') and self.scene_manager:
- self.scene_manager.models.append(text_np)
-
- # 更新场景树
- if hasattr(self, 'updateSceneTree'):
- self.updateSceneTree()
-
- print(f"✓ 3D文本创建成功: {text}")
- return text_np
- except Exception as e:
- print(f"✗ 创建3D文本失败: {e}")
- return None
-
- def create3DImage(self, pos=(0, 0, 0), image_path="", size=(1, 1)):
- """创建3D图片"""
- try:
- from panda3d.core import CardMaker, NodePath
-
- if not image_path or not os.path.exists(image_path):
- print("✗ 图片文件不存在")
- return None
-
- # 创建卡片几何体
- card_maker = CardMaker("3DImage")
- card_maker.setFrame(-size[0]/2, size[0]/2, -size[1]/2, size[1]/2)
- card = card_maker.generate()
-
- # 创建节点路径
- image_np = NodePath(card)
- image_np.reparentTo(self.render)
- image_np.setPos(pos)
-
- # 加载纹理
- from panda3d.core import Texture, Filename
- tex = self.loader.loadTexture(Filename.fromOsSpecific(image_path))
- if tex:
- image_np.setTexture(tex, 1)
- else:
- print("✗ 加载纹理失败")
- image_np.removeNode()
- return None
-
- # 添加到场景管理器
- if hasattr(self, 'scene_manager') and self.scene_manager:
- self.scene_manager.models.append(image_np)
-
- # 更新场景树
- if hasattr(self, 'updateSceneTree'):
- self.updateSceneTree()
-
- print(f"✓ 3D图片创建成功: {os.path.basename(image_path)}")
- return image_np
- except Exception as e:
- print(f"✗ 创建3D图片失败: {e}")
- return None
-
- def createCube(self, pos=(0, 0, 0), size=1.0):
- """创建立方体"""
- try:
- # 尝试使用Panda3D的内置几何体
- from panda3d.core import NodePath, GeomNode, Geom, GeomVertexFormat, GeomVertexData, GeomVertexWriter, GeomTriangles, GeomPoints
- from panda3d.core import Vec3, Vec4, RenderState, ShadeModelAttrib
-
- # 创建顶点数据格式
- format = GeomVertexFormat.getV3n3cpt2()
- vdata = GeomVertexData('cube', format, Geom.UHStatic)
- vdata.setNumRows(24)
-
- vertex = GeomVertexWriter(vdata, 'vertex')
- normal = GeomVertexWriter(vdata, 'normal')
- color = GeomVertexWriter(vdata, 'color')
-
- # 立方体的8个顶点
- s = size / 2
- vertices = [
- (-s, -s, -s), (s, -s, -s), (s, s, -s), (-s, s, -s), # 底面
- (-s, -s, s), (s, -s, s), (s, s, s), (-s, s, s) # 顶面
- ]
-
- # 立方体的6个面,每个面4个顶点
- faces = [
- (0, 1, 2, 3), # 底面
- (4, 7, 6, 5), # 顶面
- (0, 4, 5, 1), # 前面
- (2, 6, 7, 3), # 后面
- (0, 3, 7, 4), # 左面
- (1, 5, 6, 2) # 右面
- ]
-
- # 法线
- normals = [
- (0, 0, -1), (0, 0, 1), (0, -1, 0), (0, 1, 0), (-1, 0, 0), (1, 0, 0)
- ]
-
- # 添加顶点数据
- for face_idx, face in enumerate(faces):
- n = normals[face_idx]
- for vertex_idx in face:
- v = vertices[vertex_idx]
- vertex.addData3f(*v)
- normal.addData3f(*n)
- color.addData4f(0.8, 0.8, 0.8, 1.0) # 灰色
-
- # 创建几何体
- geom = Geom(vdata)
-
- # 添加三角形
- for face_idx in range(6):
- base = face_idx * 4
- # 每个面分成2个三角形
- tri = GeomTriangles(Geom.UHStatic)
- tri.addConsecutiveVertices(base, 3)
- tri.closePrimitive()
-
- tri2 = GeomTriangles(Geom.UHStatic)
- tri2.addVertices(base + 2, base + 3, base)
- tri2.closePrimitive()
-
- geom.addPrimitive(tri)
- geom.addPrimitive(tri2)
-
- # 创建节点
- geom_node = GeomNode('cube')
- geom_node.addGeom(geom)
-
- cube = NodePath(geom_node)
- cube.reparentTo(self.render)
- cube.setPos(pos)
-
- # 设置渲染状态
- state = RenderState.make(ShadeModelAttrib.make(ShadeModelAttrib.MSmooth))
- cube.set_state(state)
-
- # 添加到场景管理器
- if hasattr(self, 'scene_manager') and self.scene_manager:
- self.scene_manager.models.append(cube)
-
- # 更新场景树
- if hasattr(self, 'updateSceneTree'):
- self.updateSceneTree()
-
- print("✓ 立方体创建成功")
- return cube
- except Exception as e:
- print(f"✗ 创建立方体失败: {e}")
- return None
-
- def createSphere(self, pos=(0, 0, 0), radius=1.0):
- """创建球体"""
- try:
- from panda3d.core import CardMaker, NodePath
-
- # 创建一个简单的球体(使用多个卡片近似)
- sphere_root = NodePath("Sphere")
-
- # 创建球体的多个面来近似球形
- num_segments = 6
- for i in range(num_segments):
- angle1 = (i / num_segments) * 360
-
- # 创建球体片段
- segment_maker = CardMaker(f"SphereSegment{i}")
- segment_maker.setFrame(-radius, radius, -radius, radius)
- segment = NodePath(segment_maker.generate())
- segment.reparentTo(sphere_root)
-
- # 设置位置和旋转来形成球体
- segment.setPos(0, 0, 0)
- segment.setH(angle1)
- segment.setP(angle1/2) # 倾斜角度
- segment.setScale(radius)
-
- # 设置颜色
- from panda3d.core import Vec4
- sphere_root.setColor(Vec4(0.8, 0.6, 0.4, 1.0)) # 棕色
-
- sphere_root.reparentTo(self.render)
- sphere_root.setPos(pos)
-
- # 添加到场景管理器
- if hasattr(self, 'scene_manager') and self.scene_manager:
- self.scene_manager.models.append(sphere_root)
-
- # 更新场景树
- if hasattr(self, 'updateSceneTree'):
- self.updateSceneTree()
-
- print("✓ 球体创建成功")
- return sphere_root
- except Exception as e:
- print(f"✗ 创建球体失败: {e}")
- return None
-
- def createCylinder(self, pos=(0, 0, 0), radius=1.0, height=2.0):
- """创建圆柱体"""
- try:
- import math
- from panda3d.core import CardMaker, NodePath
-
- # 创建圆柱体
- cylinder_root = NodePath("Cylinder")
-
- # 创建圆柱体的多个侧面
- num_segments = 12
- for i in range(num_segments):
- angle1 = (i / num_segments) * 360
- angle2 = ((i + 1) / num_segments) * 360
-
- # 计算圆柱体侧面的位置
- x1 = radius * math.cos(math.radians(angle1))
- y1 = radius * math.sin(math.radians(angle1))
-
- # 创建圆柱体侧面
- segment_maker = CardMaker(f"CylinderSegment{i}")
- segment = NodePath(segment_maker.generate())
- segment.reparentTo(cylinder_root)
-
- # 设置位置和旋转
- segment.setPos(x1, y1, 0)
- segment.setH(angle1)
- segment.setScale(0.5, height, 1) # 调整宽度和高度
-
- # 设置颜色
- from panda3d.core import Vec4
- cylinder_root.setColor(Vec4(0.4, 0.8, 0.4, 1.0)) # 绿色
-
- cylinder_root.reparentTo(self.render)
- cylinder_root.setPos(pos)
-
- # 添加到场景管理器
- if hasattr(self, 'scene_manager') and self.scene_manager:
- self.scene_manager.models.append(cylinder_root)
-
- # 更新场景树
- if hasattr(self, 'updateSceneTree'):
- self.updateSceneTree()
-
- print("✓ 圆柱体创建成功")
- return cylinder_root
- except Exception as e:
- print(f"✗ 创建圆柱体失败: {e}")
- return None
-
- def createPlane(self, pos=(0, 0, 0), size=(1, 1)):
- """创建平面"""
- try:
- from panda3d.core import CardMaker, NodePath
-
- # 创建平面
- card_maker = CardMaker("Plane")
- card_maker.setFrame(-size[0]/2, size[0]/2, -size[1]/2, size[1]/2)
- plane = NodePath(card_maker.generate())
-
- plane.reparentTo(self.render)
- plane.setPos(pos)
-
- # 添加到场景管理器
- if hasattr(self, 'scene_manager') and self.scene_manager:
- self.scene_manager.models.append(plane)
-
- # 更新场景树
- if hasattr(self, 'updateSceneTree'):
- self.updateSceneTree()
-
- print("✓ 平面创建成功")
- return plane
- except Exception as e:
- print(f"✗ 创建平面失败: {e}")
- return None
-
- def create2DSamplePanel(self):
- """创建2D示例面板"""
- try:
- from core.InfoPanelManager import createSampleInfoPanel
- result = createSampleInfoPanel(self.render)
- return result
- except Exception as e:
- print(f"创建2D示例面板失败: {e}")
- return None
-
- def create3DSamplePanel(self):
- """创建3D实例面板"""
- try:
- if hasattr(self, 'info_panel_manager') and self.info_panel_manager:
- # 创建3D信息面板
- panel_id = f"3d_sample_{int(time.time())}"
- result = self.info_panel_manager.create3DInfoPanel(
- panel_id=panel_id,
- position=(0, 0, 2),
- size=(1.0, 0.6),
- bg_color=(0.15, 0.25, 0.35, 0.95),
- border_color=(0.3, 0.5, 0.7, 1.0),
- title_color=(0.7, 0.9, 1.0, 1.0),
- content_color=(0.95, 0.95, 0.95, 1.0)
- )
-
- # 添加示例内容
- if result:
- sample_data = {
- "标题": "3D信息面板",
- "状态": "运行中",
- "创建时间": time.strftime("%Y-%m-%d %H:%M:%S"),
- "位置": f"X:0, Y:0, Z:2"
- }
- self.info_panel_manager.updatePanelContent(panel_id, content=sample_data)
-
- return result
- return None
- except Exception as e:
- print(f"创建3D示例面板失败: {e}")
- return None
-
- def createWebPanel(self, url="https://www.example.com"):
- """创建Web面板"""
- try:
- if hasattr(self, 'info_panel_manager') and self.info_panel_manager:
- panel_id = f"web_panel_{int(time.time())}"
- result = self.info_panel_manager.createHTTPInfoPanel(
- panel_id=panel_id,
- url=url,
- position=(0.8, 0.0),
- size=(0.35, 0.4),
- update_interval=5.0 # 每5秒更新一次
- )
- return result
- return None
- except Exception as e:
- print(f"创建Web面板失败: {e}")
- return None
-
- # ==================== GUI创建方法 ====================
-
+ def _draw_drag_drop_interface(self, *args, **kwargs):
+ return self.interaction_panels._draw_drag_drop_interface(*args, **kwargs)
+
+ def _handle_drag_drop_completion(self, *args, **kwargs):
+ return self.interaction_panels._handle_drag_drop_completion(*args, **kwargs)
+
+ def _draw_drag_overlay(self, *args, **kwargs):
+ return self.interaction_panels._draw_drag_overlay(*args, **kwargs)
+
+ def _draw_drag_status(self, *args, **kwargs):
+ return self.interaction_panels._draw_drag_status(*args, **kwargs)
+
+ def _draw_context_menus(self, *args, **kwargs):
+ return self.interaction_panels._draw_context_menus(*args, **kwargs)
+
+ def _delete_node_simple(self, *args, **kwargs):
+ return self.interaction_panels._delete_node_simple(*args, **kwargs)
+
+ def _copy_node(self, *args, **kwargs):
+ return self.interaction_panels._copy_node(*args, **kwargs)
+
+
+ def _on_create_empty_object(self, *args, **kwargs):
+ return self.create_actions._on_create_empty_object(*args, **kwargs)
+
+ def _on_create_cube(self, *args, **kwargs):
+ return self.create_actions._on_create_cube(*args, **kwargs)
+
+ def _on_create_sphere(self, *args, **kwargs):
+ return self.create_actions._on_create_sphere(*args, **kwargs)
+
+ def _on_create_cylinder(self, *args, **kwargs):
+ return self.create_actions._on_create_cylinder(*args, **kwargs)
+
+ def _on_create_plane(self, *args, **kwargs):
+ return self.create_actions._on_create_plane(*args, **kwargs)
+
+ def _on_create_3d_text(self, *args, **kwargs):
+ return self.create_actions._on_create_3d_text(*args, **kwargs)
+
+ def _on_create_3d_image(self, *args, **kwargs):
+ return self.create_actions._on_create_3d_image(*args, **kwargs)
+
+ def _on_create_gui_button(self, *args, **kwargs):
+ return self.create_actions._on_create_gui_button(*args, **kwargs)
+
+ def _on_create_gui_label(self, *args, **kwargs):
+ return self.create_actions._on_create_gui_label(*args, **kwargs)
+
+ def _on_create_gui_entry(self, *args, **kwargs):
+ return self.create_actions._on_create_gui_entry(*args, **kwargs)
+
+ def _on_create_gui_image(self, *args, **kwargs):
+ return self.create_actions._on_create_gui_image(*args, **kwargs)
+
+ def _on_create_video_screen(self, *args, **kwargs):
+ return self.create_actions._on_create_video_screen(*args, **kwargs)
+
+ def _on_create_2d_video_screen(self, *args, **kwargs):
+ return self.create_actions._on_create_2d_video_screen(*args, **kwargs)
+
+ def _on_create_spherical_video(self, *args, **kwargs):
+ return self.create_actions._on_create_spherical_video(*args, **kwargs)
+
+ def _on_create_virtual_screen(self, *args, **kwargs):
+ return self.create_actions._on_create_virtual_screen(*args, **kwargs)
+
+ def _on_create_spot_light(self, *args, **kwargs):
+ return self.create_actions._on_create_spot_light(*args, **kwargs)
+
+ def _on_create_point_light(self, *args, **kwargs):
+ return self.create_actions._on_create_point_light(*args, **kwargs)
+
+ def _on_create_flat_terrain(self, *args, **kwargs):
+ return self.create_actions._on_create_flat_terrain(*args, **kwargs)
+
+ def _on_create_heightmap_terrain(self, *args, **kwargs):
+ return self.create_actions._on_create_heightmap_terrain(*args, **kwargs)
+
+ def _on_create_script(self, *args, **kwargs):
+ return self.create_actions._on_create_script(*args, **kwargs)
+
+ def _on_load_script(self, *args, **kwargs):
+ return self.create_actions._on_load_script(*args, **kwargs)
+
+ def _on_reload_all_scripts(self, *args, **kwargs):
+ return self.create_actions._on_reload_all_scripts(*args, **kwargs)
+
+ def _on_open_scripts_manager(self, *args, **kwargs):
+ return self.create_actions._on_open_scripts_manager(*args, **kwargs)
+
+ def _on_create_2d_sample_panel(self, *args, **kwargs):
+ return self.create_actions._on_create_2d_sample_panel(*args, **kwargs)
+
+ def _on_create_3d_sample_panel(self, *args, **kwargs):
+ return self.create_actions._on_create_3d_sample_panel(*args, **kwargs)
+
+ def _on_create_web_panel(self, *args, **kwargs):
+ return self.create_actions._on_create_web_panel(*args, **kwargs)
+
+
+ def createEmptyObject(self, *args, **kwargs):
+ return self.object_factory.createEmptyObject(*args, **kwargs)
+
+ def create3DText(self, *args, **kwargs):
+ return self.object_factory.create3DText(*args, **kwargs)
+
+ def create3DImage(self, *args, **kwargs):
+ return self.object_factory.create3DImage(*args, **kwargs)
+
+ def createCube(self, *args, **kwargs):
+ return self.object_factory.createCube(*args, **kwargs)
+
+ def createSphere(self, *args, **kwargs):
+ return self.object_factory.createSphere(*args, **kwargs)
+
+ def createCylinder(self, *args, **kwargs):
+ return self.object_factory.createCylinder(*args, **kwargs)
+
+ def createPlane(self, *args, **kwargs):
+ return self.object_factory.createPlane(*args, **kwargs)
+
+ def create2DSamplePanel(self, *args, **kwargs):
+ return self.object_factory.create2DSamplePanel(*args, **kwargs)
+
+ def create3DSamplePanel(self, *args, **kwargs):
+ return self.object_factory.create3DSamplePanel(*args, **kwargs)
+
+ def createWebPanel(self, *args, **kwargs):
+ return self.object_factory.createWebPanel(*args, **kwargs)
def createGUIButton(self, pos=(0, 0, 0), text="按钮", size=0.1):
"""创建2D GUI按钮"""
try:
diff --git a/requirements/requirements.txt b/requirements/requirements.txt
index 2bb40970..69fd9bb3 100644
--- a/requirements/requirements.txt
+++ b/requirements/requirements.txt
@@ -99,3 +99,4 @@ xdg==5
xkit==0.0.0
zipp==1.0.0
openvr==2.2.0
+imgui-bundle
diff --git a/ssbo_component/README.md b/ssbo_component/README.md
new file mode 100644
index 00000000..e820624d
--- /dev/null
+++ b/ssbo_component/README.md
@@ -0,0 +1,67 @@
+
+# SSBO Editor Component Usage Guide
+
+This directory contains a modular SSBO Scene Editor component for Panda3D RenderPipeline.
+
+## 📁 File Structure
+
+- `ssbo_editor.py`: Main component class `SSBOEditor`. Handles ImGui, RP integration, and input.
+- `ssbo_controller.py`: Core logic for object ID baking and SSBO matrix packing.
+- `effects/ssbo_instancing.yaml`: The custom shader effect for RenderPipeline (includes Normal Matrix fix).
+- `demo_component.py`: Example usage script.
+
+## 🚀 How to Integrate
+
+### 1. Prerequisites
+
+Ensure your project has:
+- `RenderPipeline` setup
+- `imgui_bundle` and `p3dimgui` installed
+- `panda3d` (version 1.10.15+)
+
+### 2. Basic Setup (See demo_component.py)
+
+```python
+from direct.showbase.ShowBase import ShowBase
+from rpcore import RenderPipeline
+from ssbo_component.ssbo_editor import SSBOEditor
+
+class MyApp(ShowBase):
+ def __init__(self):
+ # 1. Initialize RP
+ self.rp = RenderPipeline()
+ self.rp.pre_showbase_init()
+ super().__init__()
+ self.rp.create(self)
+
+ # 2. Add SSBO Component
+ # Pass your ShowBase instance (self), the RP instance (self.rp), and your model path
+ self.editor = SSBOEditor(
+ base_app=self,
+ render_pipeline=self.rp,
+ model_path="path/to/your/model.glb",
+ font_path="path/to/chinese/font.ttc" # Optional
+ )
+
+app = MyApp()
+app.run()
+```
+
+### 3. Key Features
+
+- **SSBO Instancing**: Efficiently renders thousands of objects using hardware instancing.
+- **Normal Correction**: Default shaders are patched to correctly handle non-uniform scaling (Inverse Transpose Normal Matrix).
+- **ImGui Editor**: Built-in scene tree browsing, search, and selection.
+- **Object Manipulation**: Select and move objects (Arrows + Z/X keys).
+- **Shadow Integration**: Correctly injects SSBO data into RP shadow passes.
+- **Motion Blur Support**: Computes per-object velocity for correct motion blur.
+
+### 4. Notes
+
+- **Model Preparation**: The input model should have a clear hierarchy. The component will flatten it but preserve logical objects for selection.
+- **Effects Path**: The `ssbo_instancing.yaml` is loaded relative to the `ssbo_editor.py` file, so the directory structure inside `ssbo_component` should be preserved.
+- **GPU Picking**: Basic GPU picking is implemented using shaders in `ssbo_component/shaders/`. These are loaded automatically relative to the component path.
+
+## ⚠️ Important for RenderPipeline
+
+If you encounter `ImportError: No module named 'rplibs.six.moves'`, ensure you apply the compatibility fix at the start of your main script (as seen in `demo_component.py`).
diff --git a/ssbo_component/demo_component.py b/ssbo_component/demo_component.py
new file mode 100644
index 00000000..09d8668f
--- /dev/null
+++ b/ssbo_component/demo_component.py
@@ -0,0 +1,77 @@
+"""
+SSBO Editor Component Demo
+==========================
+Demonstrates how to usage the SSBOEditor component in a new project.
+"""
+import sys
+import os
+
+# ============================================================
+# [CRITICAL] RenderPipeline Python Compatibility Fix
+# ============================================================
+if "d:/panda3d/RenderPipeline" not in sys.path:
+ sys.path.insert(0, "d:/panda3d/RenderPipeline")
+
+try:
+ import rplibs.six
+ import six
+ if not hasattr(rplibs.six, 'moves'):
+ rplibs.six.moves = six.moves
+ sys.modules['rplibs.six.moves'] = six.moves
+except ImportError:
+ import six
+ import types
+ if "rplibs" not in sys.modules:
+ rplibs = types.ModuleType("rplibs")
+ sys.modules["rplibs"] = rplibs
+ if "rplibs.six" not in sys.modules:
+ rplibs.six = six
+ sys.modules["rplibs.six"] = six
+ sys.modules["rplibs.six.moves"] = six.moves
+
+# ============================================================
+# Main Application
+# ============================================================
+from direct.showbase.ShowBase import ShowBase
+from panda3d.core import loadPrcFileData, Filename
+from rpcore import RenderPipeline
+
+# Import our new component
+# Assuming d:/panda3d is in python path, or running from d:/panda3d
+from ssbo_component.ssbo_editor import SSBOEditor
+
+loadPrcFileData("", "show-frame-rate-meter true")
+loadPrcFileData("", "sync-video false")
+
+class MyGame(ShowBase):
+ def __init__(self):
+ # 1. RP Init
+ self.rp = RenderPipeline()
+ self.rp.pre_showbase_init()
+ super().__init__()
+
+ # Position camera before RP create to avoid huge motion blur jump
+ self.cam.setPos(0, -500, 200)
+ self.cam.lookAt(0, 0, 0)
+
+ self.rp.create(self)
+ self.rp.daytime_mgr.time = 0.415
+
+ # 2. Use the Component
+ # Point to your model
+ model_path = "d:/panda3d/panda3d/jyc.glb"
+ font_path = "d:/panda3d/font/msyh.ttc"
+
+ print("Initializing SSBO Editor Component...")
+ self.editor = SSBOEditor(
+ base_app=self,
+ render_pipeline=self.rp,
+ model_path=model_path,
+ font_path=font_path
+ )
+
+ print("Demo ready.")
+
+if __name__ == "__main__":
+ app = MyGame()
+ app.run()
diff --git a/ssbo_component/effects/ssbo_instancing.yaml b/ssbo_component/effects/ssbo_instancing.yaml
new file mode 100644
index 00000000..74313aad
--- /dev/null
+++ b/ssbo_component/effects/ssbo_instancing.yaml
@@ -0,0 +1,36 @@
+vertex:
+ inout: |
+ // SSBO Layout - Default column_major matches Panda3D's internal layout
+ layout(std430) buffer transforms {
+ mat4 transform_data[];
+ };
+
+ // Input from Vertex Color (Baked ID)
+ in vec4 p3d_Color;
+
+ transform: |
+ // Decode Local ID (p3d_Color stores Local ID within chunk)
+ int low = int(p3d_Color.r * 255.0 + 0.5);
+ int high = int(p3d_Color.g * 255.0 + 0.5);
+ int id = high * 256 + low;
+ id = clamp(id, 0, 65535);
+
+ // Fetch Matrix from per-chunk SSBO
+ mat4 model = transform_data[id];
+
+ // Transform Position
+ vOutput.position = (model * p3d_Vertex).xyz;
+
+ // Transform Normal
+ vOutput.normal = normalize(mat3(model) * p3d_Normal);
+
+ post_transform: |
+ // Motion blur disabled for performance
+
+fragment:
+ inout: |
+
+
+ material: |
+ vec4 tex_color = texture(p3d_Texture0, vOutput.texcoord);
+ if (tex_color.a < 0.5) discard;
diff --git a/ssbo_component/shaders/pick_id.frag b/ssbo_component/shaders/pick_id.frag
new file mode 100644
index 00000000..efd297ab
--- /dev/null
+++ b/ssbo_component/shaders/pick_id.frag
@@ -0,0 +1,21 @@
+#version 430
+
+// Input from vertex shader
+flat in int v_instance_id;
+
+// Output: Object ID encoded as RGB color
+out vec4 fragColor;
+
+void main() {
+ // Encode instance ID into RGB channels
+ // R = low byte, G = high byte, B = 0, A = 1
+ int low_byte = v_instance_id & 0xFF;
+ int high_byte = (v_instance_id >> 8) & 0xFF;
+
+ fragColor = vec4(
+ float(low_byte) / 255.0,
+ float(high_byte) / 255.0,
+ 0.0,
+ 1.0
+ );
+}
diff --git a/ssbo_component/shaders/pick_id.vert b/ssbo_component/shaders/pick_id.vert
new file mode 100644
index 00000000..fa901fe7
--- /dev/null
+++ b/ssbo_component/shaders/pick_id.vert
@@ -0,0 +1,21 @@
+#version 430
+
+// No SSBO — vertices are already in world space.
+// Just read vertex color for object ID and output it.
+
+in vec4 p3d_Vertex;
+in vec4 p3d_Color;
+
+uniform mat4 p3d_ModelViewProjectionMatrix;
+
+flat out int v_instance_id;
+
+void main() {
+ // Decode object ID from vertex color R/G channels
+ int low_byte = int(p3d_Color.r * 255.0 + 0.5);
+ int high_byte = int(p3d_Color.g * 255.0 + 0.5);
+ v_instance_id = low_byte + (high_byte << 8);
+
+ // Standard transform — vertices already in world space
+ gl_Position = p3d_ModelViewProjectionMatrix * p3d_Vertex;
+}
diff --git a/ssbo_component/ssbo_controller.py b/ssbo_component/ssbo_controller.py
new file mode 100644
index 00000000..d6d97b4e
--- /dev/null
+++ b/ssbo_component/ssbo_controller.py
@@ -0,0 +1,550 @@
+
+from panda3d.core import (
+ GeomVertexFormat, GeomVertexWriter, GeomVertexReader, GeomVertexRewriter,
+ InternalName, Vec3, Vec4, LMatrix4f, ShaderBuffer, GeomEnums,
+ BoundingSphere, NodePath, GeomNode, Texture, SamplerState,
+ Point3, BoundingBox, Quat
+)
+import struct
+import time
+
+class ObjectController:
+ """
+ 物体控制器 (No Custom Shader Mode)
+ ====================================
+ Uses RP's default rendering (no rp.set_effect) for maximum FPS.
+ Vertex colors baked for picking. Movement modifies vertex data directly.
+ Stores original vertex positions per object for rotation/translation.
+ """
+ def __init__(self):
+ self.name_to_ids = {}
+ self.id_to_name = {}
+ self.key_to_node = {}
+ self.node_list = []
+ self.display_names = {}
+ self.global_transforms = [] # Original transforms (for center/position)
+
+ self.id_to_chunk = {} # global_id -> (chunk_key, local_idx)
+ self.chunks = {} # chunk_key -> dict with 'node' key
+
+ # Vertex index: local_id -> list of (geom_node_np, geom_idx, [row_indices])
+ self.vertex_index = {}
+
+ # Original vertex positions: local_id -> list of (Vec3,) matching row order
+ self.original_positions = {}
+
+ # Current position offsets: local_id -> Vec3 delta
+ self.position_offsets = {}
+ self.local_to_global_id = {}
+ self.local_transform_state = {}
+ self.local_transform_base_positions = {}
+ self.virtual_tree = None
+ self.virtual_tree_meta = None
+
+ self.model = None
+ self.chunk_node = None # Single chunk node
+
+ def bake_ids_and_collect(self, model):
+ """
+ Bake IDs into vertex colors, flatten, then build vertex index.
+
+ NO transform reset — vertices keep world-space positions.
+ NO SSBO — uses RP default rendering.
+ """
+ t0 = time.time()
+
+ geom_nodes = list(model.find_all_matches("**/+GeomNode"))
+ print(f"[控制器] 找到 {len(geom_nodes)} 个 GeomNode")
+
+ self.name_to_ids = {}
+ self.id_to_name = {}
+ self.key_to_node = {}
+ self.node_list = []
+ self.display_names = {}
+ self.global_transforms = []
+ self.id_to_chunk = {}
+ self.chunks = {}
+ self.vertex_index = {}
+ self.original_positions = {}
+ self.position_offsets = {}
+ self.local_to_global_id = {}
+ self.local_transform_state = {}
+ self.local_transform_base_positions = {}
+ self.virtual_tree = None
+ self.virtual_tree_meta = None
+
+ global_id_counter = 0
+ chunk_key = model.get_name() or "default"
+
+ # No chunk wrapper — flatten directly on model (same as load_jyc_flatten.py)
+ self.chunk_node = model
+ self.chunks[chunk_key] = {'node': model, 'base_id': 0}
+
+ # Flatten hierarchy
+ for np in geom_nodes:
+ np.wrt_reparent_to(model)
+
+ local_idx = 0
+
+ for np in geom_nodes:
+ gnode = np.node()
+
+ if gnode.get_num_parents() > 1:
+ parent = np.get_parent()
+ if not parent.is_empty():
+ new_np = np.copy_to(parent)
+ np.detach_node()
+ np = new_np
+ gnode = np.node()
+
+ unique_key = str(np)
+ display_name = np.get_name() or f"Object_{global_id_counter}"
+
+ if unique_key not in self.name_to_ids:
+ self.name_to_ids[unique_key] = []
+ self.key_to_node[unique_key] = np
+ self.node_list.append(unique_key)
+ self.display_names[unique_key] = display_name
+
+ # Save original transform
+ mat_double = np.get_mat()
+ original_transform = LMatrix4f(mat_double)
+
+ for i in range(gnode.get_num_geoms()):
+ geom = gnode.modify_geom(i)
+ vdata = geom.modify_vertex_data()
+
+ if not vdata.has_column("color"):
+ new_format = vdata.get_format().get_union_format(GeomVertexFormat.get_v3c4())
+ vdata.set_format(new_format)
+
+ # Encode Local ID in R/G
+ low = local_idx % 256
+ high = local_idx // 256
+ r = low / 255.0
+ g = high / 255.0
+
+ writer = GeomVertexWriter(vdata, InternalName.make("color"))
+ for row in range(vdata.get_num_rows()):
+ writer.set_row(row)
+ writer.set_data4f(r, g, 0.0, 1.0)
+
+ self.global_transforms.append(original_transform)
+ self.id_to_chunk[global_id_counter] = (chunk_key, local_idx)
+ self.name_to_ids[unique_key].append(global_id_counter)
+ self.id_to_name[global_id_counter] = unique_key
+ self.local_to_global_id[local_idx] = global_id_counter
+ self.position_offsets[local_idx] = Vec3(0, 0, 0)
+
+ global_id_counter += 1
+ local_idx += 1
+
+ # DO NOT reset transform — keep world-space positions
+
+ # Flatten directly on model — NO set_final, allows per-geom frustum culling
+ model.flatten_strong()
+
+ t1 = time.time()
+ print(f"[控制器] Flatten took {(t1-t0)*1000:.0f}ms")
+
+ # Build vertex index AFTER flatten
+ self._build_vertex_index(model)
+ self._init_local_transform_state()
+ self.build_virtual_hierarchy()
+
+ t2 = time.time()
+ print(f"[控制器] Vertex index built in {(t2-t1)*1000:.0f}ms, "
+ f"{len(self.vertex_index)} unique IDs indexed")
+
+ self.model = model
+ self.node_list.sort()
+ return global_id_counter
+
+ def build_virtual_hierarchy(self):
+ """Build a readonly virtual tree from node_list path keys."""
+ root = {
+ "name": "",
+ "path": "",
+ "children": {},
+ "leaf_key": None,
+ "display_name": "",
+ }
+ max_depth = 0
+ leaf_count = 0
+
+ for key in self.node_list:
+ if not key:
+ continue
+ parts = [p for p in str(key).split("/") if p]
+ if not parts:
+ continue
+ max_depth = max(max_depth, len(parts))
+ cursor = root
+ path_acc = ""
+ for i, part in enumerate(parts):
+ path_acc = f"{path_acc}/{part}" if path_acc else part
+ child = cursor["children"].get(part)
+ if child is None:
+ child = {
+ "name": part,
+ "path": path_acc,
+ "children": {},
+ "leaf_key": None,
+ "display_name": part,
+ }
+ cursor["children"][part] = child
+ cursor = child
+ if i == len(parts) - 1:
+ cursor["leaf_key"] = key
+ cursor["display_name"] = self.display_names.get(key, part)
+ leaf_count += 1
+
+ self.virtual_tree = root
+ self.virtual_tree_meta = {"max_depth": max_depth, "leaf_count": leaf_count}
+ return root
+
+ def get_virtual_hierarchy(self):
+ """Return cached virtual tree; build on demand."""
+ if self.virtual_tree is None:
+ return self.build_virtual_hierarchy()
+ return self.virtual_tree
+
+ def _build_vertex_index(self, chunk_root):
+ """
+ After flatten, batch-read all vertex data with numpy to build:
+ local_id -> [(geom_node_np, geom_idx, row_indices_array)]
+ Also stores original vertex positions per object (as numpy arrays).
+ """
+ import numpy as np
+
+ for gn_np in chunk_root.find_all_matches("**/+GeomNode"):
+ gnode = gn_np.node()
+ for gi in range(gnode.get_num_geoms()):
+ geom = gnode.get_geom(gi)
+ vdata = geom.get_vertex_data()
+ num_rows = vdata.get_num_rows()
+
+ if num_rows == 0:
+ continue
+
+ # Find vertex and color column info
+ fmt = vdata.get_format()
+
+ # Get position column
+ pos_col = fmt.get_column(InternalName.get_vertex())
+ if pos_col is None:
+ continue
+ pos_array_idx = fmt.get_array_with(InternalName.get_vertex())
+ pos_start = pos_col.get_start()
+
+ # Get color column
+ color_col = fmt.get_column(InternalName.make("color"))
+ if color_col is None:
+ continue
+ color_array_idx = fmt.get_array_with(InternalName.make("color"))
+ color_start = color_col.get_start()
+
+ # Read raw position array
+ pos_array_format = fmt.get_array(pos_array_idx)
+ pos_stride = pos_array_format.get_stride()
+ pos_handle = vdata.get_array(pos_array_idx).get_handle()
+ pos_raw = bytes(pos_handle.get_data())
+ pos_buf = np.frombuffer(pos_raw, dtype=np.uint8).reshape(num_rows, pos_stride)
+
+ # Extract xyz positions (3 floats starting at pos_start)
+ positions = np.ndarray((num_rows, 3), dtype=np.float32,
+ buffer=pos_buf[:, pos_start:pos_start+12].tobytes())
+
+ # Read raw color array
+ color_array_format = fmt.get_array(color_array_idx)
+ color_stride = color_array_format.get_stride()
+
+ if color_array_idx == pos_array_idx:
+ color_buf = pos_buf
+ else:
+ color_handle = vdata.get_array(color_array_idx).get_handle()
+ color_raw = bytes(color_handle.get_data())
+ color_buf = np.frombuffer(color_raw, dtype=np.uint8).reshape(num_rows, color_stride)
+
+ # Decode color format to get ID
+ # Color can be stored as float32 RGBA or unorm8 RGBA
+ num_components = color_col.get_num_components()
+ component_bytes = color_col.get_component_bytes()
+
+ if component_bytes == 4: # float32 per component
+ color_data = np.ndarray((num_rows, num_components), dtype=np.float32,
+ buffer=color_buf[:, color_start:color_start+num_components*4].tobytes())
+ r_vals = (color_data[:, 0] * 255.0 + 0.5).astype(np.int32)
+ g_vals = (color_data[:, 1] * 255.0 + 0.5).astype(np.int32)
+ elif component_bytes == 1: # uint8 per component
+ color_bytes = color_buf[:, color_start:color_start+num_components].copy()
+ r_vals = color_bytes[:, 0].astype(np.int32)
+ g_vals = color_bytes[:, 1].astype(np.int32)
+ else:
+ # Fallback: skip this geom
+ continue
+
+ local_ids = r_vals + (g_vals << 8)
+
+ # Group rows by local_id using argsort (O(N log N) instead of O(N×K))
+ sort_idx = np.argsort(local_ids)
+ sorted_ids = local_ids[sort_idx]
+ sorted_positions = positions[sort_idx]
+
+ # Find group boundaries
+ boundaries = np.where(np.diff(sorted_ids) != 0)[0] + 1
+
+ # Split into groups
+ id_groups = np.split(sort_idx, boundaries)
+ pos_groups = np.split(sorted_positions, boundaries)
+ group_ids = sorted_ids[np.concatenate([[0], boundaries])]
+
+ for k in range(len(group_ids)):
+ uid = int(group_ids[k])
+ rows = id_groups[k]
+ pos = pos_groups[k]
+
+ if uid not in self.vertex_index:
+ self.vertex_index[uid] = []
+ self.original_positions[uid] = []
+
+ self.vertex_index[uid].append((gn_np, gi, rows))
+ self.original_positions[uid].append(pos.copy())
+
+ def _init_local_transform_state(self):
+ """Initialize transform state for each local_idx after vertex index is ready."""
+ self.local_transform_state = {}
+ self.local_transform_base_positions = {}
+
+ for local_idx in self.vertex_index.keys():
+ self.local_transform_base_positions[local_idx] = self.original_positions.get(local_idx, [])
+ self.local_transform_state[local_idx] = {
+ "offset": Vec3(0, 0, 0),
+ "quat": Quat.identQuat(),
+ "scale": Vec3(1, 1, 1),
+ "pivot": self.get_local_pivot(local_idx),
+ }
+
+ def get_local_indices_from_global_ids(self, global_ids):
+ """Map global ids to unique local indices."""
+ local_indices = []
+ if not global_ids:
+ return local_indices
+ seen = set()
+ for global_id in global_ids:
+ mapping = self.id_to_chunk.get(global_id)
+ if not mapping:
+ continue
+ _, local_idx = mapping
+ if local_idx in seen:
+ continue
+ if local_idx not in self.vertex_index:
+ continue
+ seen.add(local_idx)
+ local_indices.append(local_idx)
+ return local_indices
+
+ def get_local_pivot(self, local_idx):
+ """Get pivot for one local object (world-space center)."""
+ global_id = self.local_to_global_id.get(local_idx)
+ if global_id is None:
+ return Vec3(0, 0, 0)
+ return self.get_object_center(global_id)
+
+ def get_selection_center(self, local_indices):
+ """Get center point for a multi-object selection."""
+ if not local_indices:
+ return Vec3(0, 0, 0)
+ acc = Vec3(0, 0, 0)
+ valid = 0
+ for local_idx in local_indices:
+ state = self.local_transform_state.get(local_idx)
+ if not state:
+ continue
+ acc += state.get("pivot", Vec3(0, 0, 0)) + state.get("offset", Vec3(0, 0, 0))
+ valid += 1
+ if valid == 0:
+ return Vec3(0, 0, 0)
+ return acc / float(valid)
+
+ def begin_transform_session(self, local_indices):
+ """Create immutable baseline snapshot for one gizmo drag session."""
+ if not local_indices:
+ return {"locals": {}}
+
+ locals_snapshot = {}
+ for local_idx in local_indices:
+ base_state = self.local_transform_state.get(local_idx)
+ if not base_state:
+ continue
+ entries = self.vertex_index.get(local_idx, [])
+ base_positions = self.local_transform_base_positions.get(local_idx, [])
+ locals_snapshot[local_idx] = {
+ "offset": Vec3(base_state["offset"]),
+ "quat": Quat(base_state["quat"]),
+ "scale": Vec3(base_state["scale"]),
+ "pivot": Vec3(base_state["pivot"]),
+ "entries": entries,
+ "base_positions": base_positions,
+ }
+ return {"locals": locals_snapshot}
+
+ def apply_transform_session(self, snapshot, delta_pos, delta_quat, delta_scale):
+ """Apply transform delta to all local indices in snapshot and rewrite vertices."""
+ import numpy as np
+
+ if not snapshot or "locals" not in snapshot:
+ return
+ if delta_pos is None:
+ delta_pos = Vec3(0, 0, 0)
+ if delta_quat is None:
+ delta_quat = Quat.identQuat()
+ if delta_scale is None:
+ delta_scale = Vec3(1, 1, 1)
+
+ dscale = np.array([delta_scale.x, delta_scale.y, delta_scale.z], dtype=np.float32)
+ dpos = np.array([delta_pos.x, delta_pos.y, delta_pos.z], dtype=np.float32)
+
+ for local_idx, local_data in snapshot["locals"].items():
+ base_offset = local_data["offset"]
+ base_quat = local_data["quat"]
+ base_scale = local_data["scale"]
+ pivot = local_data["pivot"]
+
+ final_offset = Vec3(base_offset) + delta_pos
+ final_quat = Quat(delta_quat * base_quat)
+ final_scale = Vec3(
+ base_scale.x * delta_scale.x,
+ base_scale.y * delta_scale.y,
+ base_scale.z * delta_scale.z,
+ )
+ rot_mat = self._quat_to_np_mat3(final_quat)
+
+ self.local_transform_state[local_idx]["offset"] = final_offset
+ self.local_transform_state[local_idx]["quat"] = final_quat
+ self.local_transform_state[local_idx]["scale"] = final_scale
+ self.position_offsets[local_idx] = final_offset
+
+ pivot_np = np.array([pivot.x, pivot.y, pivot.z], dtype=np.float32)
+ base_s = np.array([base_scale.x, base_scale.y, base_scale.z], dtype=np.float32)
+ total_scale = base_s * dscale
+ total_offset = np.array([base_offset.x, base_offset.y, base_offset.z], dtype=np.float32) + dpos
+
+ entries = local_data["entries"]
+ base_positions = local_data["base_positions"]
+ for i, (gn_np, gi, rows) in enumerate(entries):
+ if i >= len(base_positions):
+ continue
+ orig_pos = base_positions[i]
+ if orig_pos is None or len(orig_pos) == 0:
+ continue
+ centered = orig_pos - pivot_np
+ scaled = centered * total_scale
+ rotated = scaled @ rot_mat.T
+ new_pos = rotated + pivot_np + total_offset
+
+ gnode = gn_np.node()
+ geom = gnode.modify_geom(gi)
+ vdata = geom.modify_vertex_data()
+ writer = GeomVertexWriter(vdata, "vertex")
+
+ for j in range(len(rows)):
+ writer.set_row(int(rows[j]))
+ writer.set_data3f(float(new_pos[j, 0]), float(new_pos[j, 1]), float(new_pos[j, 2]))
+
+ def _quat_to_np_mat3(self, quat):
+ """Convert Panda3D Quat to 3x3 numpy rotation matrix."""
+ import numpy as np
+ q = Quat(quat)
+ q.normalize()
+ w = float(q.getR())
+ x = float(q.getI())
+ y = float(q.getJ())
+ z = float(q.getK())
+
+ xx = x * x
+ yy = y * y
+ zz = z * z
+ xy = x * y
+ xz = x * z
+ yz = y * z
+ wx = w * x
+ wy = w * y
+ wz = w * z
+
+ return np.array([
+ [1.0 - 2.0 * (yy + zz), 2.0 * (xy - wz), 2.0 * (xz + wy)],
+ [2.0 * (xy + wz), 1.0 - 2.0 * (xx + zz), 2.0 * (yz - wx)],
+ [2.0 * (xz - wy), 2.0 * (yz + wx), 1.0 - 2.0 * (xx + yy)],
+ ], dtype=np.float32)
+
+ def create_ssbo(self):
+ """No SSBO needed — using RP default rendering."""
+ return None
+
+ def move_object(self, global_id, delta):
+ """
+ Move an object by modifying vertex positions directly.
+ delta: Vec3 translation to apply.
+ Uses numpy for batch vertex updates.
+ """
+ import numpy as np
+
+ if global_id not in self.id_to_chunk:
+ return
+
+ _, local_idx = self.id_to_chunk[global_id]
+
+ if local_idx not in self.vertex_index:
+ return
+
+ # Accumulate offset
+ self.position_offsets[local_idx] = self.position_offsets.get(local_idx, Vec3(0)) + delta
+ offset = self.position_offsets[local_idx]
+ offset_arr = np.array([offset.x, offset.y, offset.z], dtype=np.float32)
+
+ # Update each (geom_node, geom_idx, rows) group
+ entries = self.vertex_index[local_idx]
+ originals = self.original_positions[local_idx]
+
+ for i, (gn_np, gi, rows) in enumerate(entries):
+ orig_pos = originals[i] # numpy array (N, 3)
+ new_pos = orig_pos + offset_arr # vectorized add
+
+ gnode = gn_np.node()
+ geom = gnode.modify_geom(gi)
+ vdata = geom.modify_vertex_data()
+ writer = GeomVertexWriter(vdata, "vertex")
+
+ for j in range(len(rows)):
+ writer.set_row(int(rows[j]))
+ writer.set_data3f(float(new_pos[j, 0]), float(new_pos[j, 1]), float(new_pos[j, 2]))
+
+ def get_world_pos(self, global_id):
+ """Get current world position of an object."""
+ if global_id not in self.id_to_chunk:
+ return Vec3(0, 0, 0)
+ _, local_idx = self.id_to_chunk[global_id]
+
+ original_mat = self.global_transforms[global_id]
+ original_pos = original_mat.get_row3(3)
+ offset = self.position_offsets.get(local_idx, Vec3(0))
+
+ return Vec3(original_pos) + offset
+
+ def get_object_center(self, global_id):
+ """Get the original center position of an object (for rotation pivot)."""
+ if global_id >= len(self.global_transforms):
+ return Vec3(0, 0, 0)
+ mat = self.global_transforms[global_id]
+ return Vec3(mat.get_row3(3))
+
+ def get_transform(self, global_id):
+ """Get original transform."""
+ if global_id >= len(self.global_transforms):
+ return LMatrix4f.ident_mat()
+ return self.global_transforms[global_id]
+
+ @property
+ def transforms(self):
+ return self.global_transforms
diff --git a/ssbo_component/ssbo_editor.py b/ssbo_component/ssbo_editor.py
new file mode 100644
index 00000000..e82948f2
--- /dev/null
+++ b/ssbo_component/ssbo_editor.py
@@ -0,0 +1,617 @@
+
+import sys
+import os
+import struct
+import time
+from panda3d.core import (
+ Filename, loadPrcFileData, GeomVertexFormat,
+ GeomVertexWriter, InternalName, Shader, Texture, SamplerState,
+ Vec3, Vec4, Point2, Point3, LMatrix4f, ShaderBuffer, GeomEnums, OmniBoundingVolume, Quat,
+ TransparencyAttrib, BoundingSphere, NodePath,
+ GraphicsEngine, WindowProperties, FrameBufferProperties,
+ GraphicsPipe, GraphicsOutput, Camera, DisplayRegion, OrthographicLens,
+ BoundingBox
+)
+
+import p3dimgui.backend as p3dimgui_backend
+import p3dimgui.shaders as p3dimgui_shaders
+from imgui_bundle import imgui
+from rpcore.effect import Effect
+
+# Work around p3dimgui import-order issue where backend may import an unrelated
+# top-level "shaders" module and miss these globals.
+if not hasattr(p3dimgui_backend, "VERT_SHADER"):
+ p3dimgui_backend.VERT_SHADER = p3dimgui_shaders.VERT_SHADER
+if not hasattr(p3dimgui_backend, "FRAG_SHADER"):
+ p3dimgui_backend.FRAG_SHADER = p3dimgui_shaders.FRAG_SHADER
+
+ImGuiBackend = p3dimgui_backend.ImGuiBackend
+
+from .ssbo_controller import ObjectController
+
+class SSBOEditor:
+ """
+ SSBO Editor Component
+ ====================
+ Encapsulates the SSBO rendering, ImGui editor, and interaction logic.
+ Can be integrated into any ShowBase application using RenderPipeline.
+ """
+
+ def __init__(self, base_app, render_pipeline, model_path=None, font_path=None):
+ self.base = base_app
+ self.rp = render_pipeline
+ self.controller = None
+ self.model = None
+ self.ssbo = None
+ self.font_path = font_path
+ # Picking resources may be created later when a model is loaded.
+ self.pick_buffer = None
+ self.pick_texture = None
+ self.pick_cam = None
+ self.pick_cam_np = None
+ self.pick_lens = None
+
+ # Internal State
+ self.selected_name = None
+ self.selected_ids = []
+ self.search_text = ""
+ self.last_search_text = None
+ self.filtered_nodes = []
+ self.debug_mode = False
+ self.keys = {}
+ self._ssbo_transform_active = False
+ self._ssbo_selected_local_indices = []
+ self._ssbo_transform_snapshot = None
+ self._ssbo_gizmo_proxy = None
+ self._ssbo_proxy_start = {"pos": None, "quat": None, "scale": None}
+ self._bound_transform_gizmo = None
+
+ # Initialize ImGui Backend if not already present
+ if not hasattr(self.base, 'imgui_backend'):
+ print("[SSBOEditor] Initializing ImGui Backend...")
+ self.base.imgui_backend = ImGuiBackend()
+
+ self.load_font()
+
+ # Register Events
+ self.base.accept("imgui-new-frame", self.draw_imgui)
+ self.base.accept("f", self.focus_on_selected)
+ self.base.accept("d", self.toggle_debug)
+ self.base.accept("mouse1", self.on_mouse_click)
+
+ # Register Input Tasks
+ for key in ['arrow_up', 'arrow_down', 'arrow_left', 'arrow_right', 'z', 'x']:
+ self.base.accept(key, self.keys.__setitem__, [key, True])
+ self.base.accept(f"{key}-up", self.keys.__setitem__, [key, False])
+
+ # Add Tasks
+ self.base.taskMgr.add(self.update_task, "update_task")
+
+ # Load Model if provided
+ if model_path:
+ self.load_model(model_path)
+
+ def load_font(self):
+ """Load custom font for ImGui"""
+ io = imgui.get_io()
+
+ # Load Chinese Glyph Ranges
+ glyph_ranges = None
+ try:
+ if hasattr(io.fonts, 'get_glyph_ranges_chinese_full'):
+ glyph_ranges = io.fonts.get_glyph_ranges_chinese_full()
+ elif hasattr(io.fonts, 'get_glyph_ranges_chinese_simplified_common'):
+ glyph_ranges = io.fonts.get_glyph_ranges_chinese_simplified_common()
+ except Exception as e:
+ print(f"[SSBOEditor] Warning: Could not get Chinese glyph ranges: {e}")
+
+ try:
+ if self.font_path and os.path.exists(self.font_path):
+ io.fonts.clear()
+ # If glyph_ranges is None, it uses default (Basic Latin)
+ if glyph_ranges:
+ io.fonts.add_font_from_file_ttf(self.font_path, 18.0, glyph_ranges=glyph_ranges)
+ else:
+ io.fonts.add_font_from_file_ttf(self.font_path, 18.0)
+ else:
+ # Fallback to default or common font
+ default_font = os.path.join(os.path.dirname(os.path.dirname(__file__)), "font", "msyh.ttc")
+ if os.path.exists(default_font):
+ io.fonts.clear()
+ io.fonts.add_font_from_file_ttf(default_font, 18.0, glyph_ranges=glyph_ranges)
+ else:
+ io.fonts.clear()
+ io.fonts.add_font_default()
+ except Exception as e:
+ print(f"[SSBOEditor] Font load error: {e}")
+ io.fonts.clear()
+ io.fonts.add_font_default()
+
+ def load_model(self, model_path):
+ """Load and process a model — NO custom shader, uses RP default rendering."""
+ print(f"[SSBOEditor] Loading model: {model_path}")
+ fn = Filename.fromOsSpecific(model_path)
+ self.model = self.base.loader.loadModel(fn)
+
+ self.controller = ObjectController()
+ count = self.controller.bake_ids_and_collect(self.model)
+ self._ssbo_transform_active = False
+ self._ssbo_selected_local_indices = []
+ self._ssbo_transform_snapshot = None
+ self._cleanup_ssbo_proxy()
+
+ self.model.reparent_to(self.base.render)
+
+ # NO rp.set_effect() — use RP default rendering for max FPS
+ # NO SSBO creation — vertex positions are baked
+
+ # Setup GPU Picking (uses simple vertex-color shader)
+ self.setup_gpu_picking()
+
+ print(f"[SSBOEditor] Model loaded. Total objects: {count}")
+
+ # No custom effect needed — RP default rendering for maximum FPS
+
+ def _inject_ssbo_into_shadow_state(self, effect_path):
+ """Inject SSBO inputs into RP shadow tag state"""
+ try:
+ if not hasattr(self.rp.tag_mgr, 'containers'): return
+
+ shadow_container = self.rp.tag_mgr.containers.get("shadow")
+ if not shadow_container: return
+
+ tag_value = self.model.get_tag(shadow_container.tag_name)
+ if not tag_value: return
+
+ effect = Effect.load(effect_path, {})
+ if effect is None: return
+
+ shadow_shader = effect.get_shader_obj("shadow")
+ if shadow_shader is None: return
+
+ # Since inputs are now on Nodes (Chunks), we just need to ensure the shader is applied.
+ # extra_inputs is no longer needed if the inputs are on the nodes themselves?
+ # Wait, RP might override state.
+ # But specific shader inputs on NodePath have priority over State inputs usually?
+ # Let's try applying without extra inputs first.
+
+ self.rp.tag_mgr.apply_state(
+ "shadow", self.model, shadow_shader,
+ tag_value, 65)
+
+ print(f"[SSBO Shadow] Re-applied shadow state (tag='{tag_value}')")
+ except Exception as e:
+ print(f"[SSBO Shadow] Error injecting shadow state: {e}")
+
+ def setup_gpu_picking(self):
+ """Setup GPU Picking (Basic implementation)"""
+ # ... (Buffer setup code remains same) ...
+ win_props = WindowProperties()
+ win_props.set_size(1, 1)
+ fb_props = FrameBufferProperties()
+ fb_props.set_rgba_bits(8, 8, 8, 8)
+ fb_props.set_depth_bits(16)
+
+ self.pick_buffer = self.base.graphicsEngine.make_output(
+ self.base.pipe, "pick_buffer", -100,
+ fb_props, win_props,
+ GraphicsPipe.BF_refuse_window,
+ self.base.win.get_gsg(), self.base.win
+ )
+
+ if not self.pick_buffer:
+ print("[GPU Picking] Failed to create buffer!")
+ return
+
+ self.pick_texture = Texture()
+ self.pick_texture.set_minfilter(Texture.FT_nearest)
+ self.pick_texture.set_magfilter(Texture.FT_nearest)
+ self.pick_buffer.add_render_texture(self.pick_texture, GraphicsOutput.RTM_copy_ram)
+
+ self.pick_cam = Camera("pick_camera")
+ self.pick_cam_np = self.base.cam.attach_new_node(self.pick_cam)
+ self.pick_lens = self.base.camLens.make_copy()
+ self.pick_cam.set_lens(self.pick_lens)
+
+ dr = self.pick_buffer.make_display_region()
+ dr.set_camera(self.pick_cam_np)
+
+ # Load pick shader
+ current_dir = os.path.dirname(os.path.abspath(__file__))
+ pick_vert = os.path.join(current_dir, "shaders", "pick_id.vert")
+ pick_frag = os.path.join(current_dir, "shaders", "pick_id.frag")
+
+ pick_vert = Filename.fromOsSpecific(pick_vert).getFullpath()
+ pick_frag = Filename.fromOsSpecific(pick_frag).getFullpath()
+
+ try:
+ pick_shader = Shader.load(
+ Shader.SL_GLSL,
+ pick_vert,
+ pick_frag
+ )
+ self.pick_cam.set_scene(self.model)
+ initial_state = NodePath("initial")
+ initial_state.set_shader(pick_shader, 100)
+ # Remove global SSBO input, Chunks have their own inputs
+ # initial_state.set_shader_input("transforms", ssbo)
+ self.pick_cam.set_initial_state(initial_state.get_state())
+ except Exception as e:
+ print(f"[GPU Picking] Warning: pick shaders failed to load: {e}")
+ print("Picking disabled.")
+ return
+
+ self.pick_buffer.set_active(False)
+ self.pick_buffer.set_clear_color(Vec4(0, 0, 0, 0))
+ self.pick_buffer.set_clear_color_active(True)
+
+ def pick_object(self, mx, my):
+ if (not self.pick_buffer or not self.pick_texture or not self.pick_lens or
+ not self.controller or not self.model):
+ return False
+
+ self.pick_lens.set_fov(0.1)
+ self.pick_lens.set_film_offset(0, 0)
+ self.pick_cam.set_lens(self.pick_lens)
+
+ near_point = Point3()
+ far_point = Point3()
+ self.base.camLens.extrude(Point2(mx, my), near_point, far_point)
+
+ self.pick_cam_np.set_pos(0, 0, 0)
+ self.pick_cam_np.look_at(far_point)
+
+ self.pick_buffer.set_active(True)
+ self.base.graphicsEngine.render_frame()
+ self.pick_buffer.set_active(False)
+ self.base.graphicsEngine.extract_texture_data(
+ self.pick_texture, self.base.win.get_gsg()
+ )
+
+ ram_image = self.pick_texture.get_ram_image_as("RGBA")
+ if ram_image:
+ data = memoryview(ram_image)
+ if len(data) >= 4:
+ r, g, b, a = data[0], data[1], data[2], data[3]
+ if a > 0:
+ hit_id = r + (g << 8)
+ node_key = self.controller.id_to_name.get(hit_id)
+ if node_key:
+ print(f"[Pick] Hit: ID={hit_id} -> {node_key}")
+ self.select_node(node_key)
+ return True
+
+ self.selected_name = None
+ self.selected_ids = []
+ return False
+
+
+ def on_mouse_click(self):
+ io = imgui.get_io()
+ if io.want_capture_mouse:
+ return
+ if self.base.mouseWatcherNode.has_mouse():
+ mpos = self.base.mouseWatcherNode.get_mouse()
+ # If clicking gizmo, skip SSBO pick.
+ if self._try_start_gizmo_drag(mpos.x, mpos.y):
+ return
+ prev_selected = self.selected_name
+ hit = self.pick_object(mpos.x, mpos.y)
+ # SSBO miss must clear current selection.
+ if not hit:
+ self._sync_selection_none()
+ # Always fallback to legacy ray pick when SSBO misses.
+ # This keeps scene selection usable if SSBO ID mapping is incomplete.
+ self._fallback_legacy_pick(mpos.x, mpos.y)
+ elif prev_selected != self.selected_name:
+ # Ensure selection visuals refresh when SSBO selection changes.
+ self._sync_selection_from_key(self.selected_name)
+
+ def toggle_debug(self):
+ self.debug_mode = not self.debug_mode
+
+ def clear_selection(self):
+ pass # No selection mask texture needed without custom shader
+
+ def update_selection_mask(self):
+ pass # No selection mask texture needed without custom shader
+
+ def select_node(self, key):
+ if not self.controller or key not in self.controller.name_to_ids:
+ return
+ self.selected_name = key
+ self.selected_ids = self.controller.name_to_ids.get(key, [])
+ self._sync_selection_from_key(key)
+
+ def _sync_selection_from_key(self, key):
+ """Sync SSBO picked key to legacy SelectionSystem."""
+ try:
+ if hasattr(self.base, "selection") and self.base.selection:
+ kind, target = self._resolve_ssbo_selection_target(key)
+ if kind == "proxy":
+ target_np = target
+ else:
+ target_np = target if target is not None else self.model
+ if target_np is None or target_np.isEmpty():
+ target_np = self.model
+ self.base.selection.updateSelection(target_np)
+ except Exception as e:
+ print(f"[SSBOEditor] selection sync failed: {e}")
+
+ def _sync_selection_none(self):
+ """Clear legacy SelectionSystem selection."""
+ try:
+ self._ssbo_transform_active = False
+ self._ssbo_selected_local_indices = []
+ self._ssbo_transform_snapshot = None
+ self._cleanup_ssbo_proxy()
+ if hasattr(self.base, "selection") and self.base.selection:
+ self.base.selection.updateSelection(None)
+ except Exception as e:
+ print(f"[SSBOEditor] clear selection sync failed: {e}")
+
+ def bind_transform_gizmo(self, transform_gizmo):
+ """Bind TransformGizmo drag hooks so SSBO sub-object transforms can follow gizmo."""
+ self._bound_transform_gizmo = transform_gizmo
+ if not transform_gizmo:
+ return
+ hooks = {
+ "move": {
+ "drag_start": [self._on_ssbo_gizmo_drag_start],
+ "drag_move": [self._on_ssbo_gizmo_drag_move],
+ "drag_end": [self._on_ssbo_gizmo_drag_end],
+ },
+ "rotate": {
+ "drag_start": [self._on_ssbo_gizmo_drag_start],
+ "drag_move": [self._on_ssbo_gizmo_drag_move],
+ "drag_end": [self._on_ssbo_gizmo_drag_end],
+ },
+ "scale": {
+ "drag_start": [self._on_ssbo_gizmo_drag_start],
+ "drag_move": [self._on_ssbo_gizmo_drag_move],
+ "drag_end": [self._on_ssbo_gizmo_drag_end],
+ },
+ }
+ try:
+ if hasattr(transform_gizmo, "set_event_hooks"):
+ transform_gizmo.set_event_hooks(hooks, replace=False)
+ print("[SSBOEditor] TransformGizmo hooks bound")
+ except Exception as e:
+ print(f"[SSBOEditor] bind transform gizmo failed: {e}")
+
+ def _resolve_ssbo_selection_target(self, key):
+ """Resolve selected SSBO key to proxy node (preferred) or regular node."""
+ self._ssbo_transform_active = False
+ self._ssbo_transform_snapshot = None
+ self._ssbo_selected_local_indices = []
+
+ if not self.controller or not key:
+ return "node", self.model
+ global_ids = self.controller.name_to_ids.get(key, [])
+ local_indices = self.controller.get_local_indices_from_global_ids(global_ids)
+ self._ssbo_selected_local_indices = local_indices
+ if local_indices:
+ print(f"[SSBOEditor] selection locals={len(local_indices)} key={key}")
+ center = self.controller.get_selection_center(local_indices)
+ proxy = self._ensure_ssbo_proxy(center)
+ return "proxy", proxy
+ target_np = self.controller.key_to_node.get(key)
+ if target_np is None or target_np.isEmpty():
+ target_np = self.model
+ return "node", target_np
+
+ def _ensure_ssbo_proxy(self, center):
+ if self._ssbo_gizmo_proxy is None or self._ssbo_gizmo_proxy.isEmpty():
+ self._ssbo_gizmo_proxy = self.base.render.attach_new_node("ssbo_transform_proxy")
+ self._ssbo_gizmo_proxy.setTag("is_ssbo_proxy", "1")
+ self._ssbo_gizmo_proxy.set_pos(center)
+ self._ssbo_gizmo_proxy.set_hpr(0, 0, 0)
+ self._ssbo_gizmo_proxy.set_scale(1, 1, 1)
+ return self._ssbo_gizmo_proxy
+
+ def _cleanup_ssbo_proxy(self):
+ if self._ssbo_gizmo_proxy and not self._ssbo_gizmo_proxy.isEmpty():
+ self._ssbo_gizmo_proxy.removeNode()
+ self._ssbo_gizmo_proxy = None
+
+ def _on_ssbo_gizmo_drag_start(self, payload):
+ try:
+ target = payload.get("target") if payload else None
+ if not target or target != self._ssbo_gizmo_proxy:
+ self._ssbo_transform_active = False
+ return
+ if not self.controller or not self._ssbo_selected_local_indices:
+ self._ssbo_transform_active = False
+ return
+ self._ssbo_transform_snapshot = self.controller.begin_transform_session(
+ self._ssbo_selected_local_indices
+ )
+ self._ssbo_proxy_start = {
+ "pos": Vec3(target.getPos(self.base.render)),
+ "quat": Quat(target.getQuat(self.base.render)),
+ "scale": Vec3(target.getScale()),
+ }
+ self._ssbo_transform_active = True
+ print(f"[SSBOEditor] drag_start locals={len(self._ssbo_selected_local_indices)}")
+ except Exception as e:
+ self._ssbo_transform_active = False
+ print(f"[SSBOEditor] drag_start bridge failed: {e}")
+
+ def _on_ssbo_gizmo_drag_move(self, payload):
+ try:
+ if not self._ssbo_transform_active:
+ return
+ target = payload.get("target") if payload else None
+ if not target or target != self._ssbo_gizmo_proxy:
+ return
+ start_pos = self._ssbo_proxy_start.get("pos")
+ start_quat = self._ssbo_proxy_start.get("quat")
+ start_scale = self._ssbo_proxy_start.get("scale")
+ if start_pos is None or start_quat is None or start_scale is None:
+ return
+
+ curr_pos = Vec3(target.getPos(self.base.render))
+ curr_quat = Quat(target.getQuat(self.base.render))
+ curr_scale = Vec3(target.getScale())
+
+ delta_pos = curr_pos - start_pos
+ inv_start_quat = Quat(start_quat)
+ inv_start_quat.invertInPlace()
+ delta_quat = curr_quat * inv_start_quat
+ delta_scale = Vec3(
+ curr_scale.x / start_scale.x if abs(start_scale.x) > 1e-8 else 1.0,
+ curr_scale.y / start_scale.y if abs(start_scale.y) > 1e-8 else 1.0,
+ curr_scale.z / start_scale.z if abs(start_scale.z) > 1e-8 else 1.0,
+ )
+
+ self.controller.apply_transform_session(
+ self._ssbo_transform_snapshot,
+ delta_pos,
+ delta_quat,
+ delta_scale,
+ )
+ except Exception as e:
+ print(f"[SSBOEditor] drag_move bridge failed: {e}")
+
+ def _on_ssbo_gizmo_drag_end(self, payload):
+ try:
+ if self._ssbo_transform_active:
+ print(f"[SSBOEditor] drag_end locals={len(self._ssbo_selected_local_indices)}")
+ self._ssbo_transform_active = False
+ self._ssbo_transform_snapshot = None
+ except Exception as e:
+ print(f"[SSBOEditor] drag_end bridge failed: {e}")
+
+ def _fallback_legacy_pick(self, mx, my):
+ """Fallback to legacy ray picking when SSBO misses."""
+ try:
+ if not hasattr(self.base, "event_handler") or not self.base.event_handler:
+ return
+ win_w, win_h = self.base.win.getSize()
+ x = (mx + 1.0) * 0.5 * win_w
+ y = (1.0 - my) * 0.5 * win_h
+ self.base.event_handler.mousePressEventLeft({"x": x, "y": y})
+ except Exception as e:
+ print(f"[SSBOEditor] legacy fallback pick failed: {e}")
+
+ def _try_start_gizmo_drag(self, mouse_x=None, mouse_y=None):
+ """Try to start gizmo drag using the existing SelectionSystem pipeline."""
+ try:
+ new_transform = getattr(self.base, "newTransform", None)
+ if (
+ new_transform is not None and
+ mouse_x is not None and
+ mouse_y is not None and
+ self._is_mouse_on_new_gizmo(new_transform, mouse_x, mouse_y)
+ ):
+ return True
+ selection = getattr(self.base, "selection", None)
+ if not selection or not selection.gizmo:
+ return False
+ win_w, win_h = self.base.win.getSize()
+ mpos = self.base.mouseWatcherNode.get_mouse()
+ x = (mpos.x + 1.0) * 0.5 * win_w
+ y = (1.0 - mpos.y) * 0.5 * win_h
+
+ axis = selection.gizmoHighlightAxis or selection.checkGizmoClick(x, y)
+ if axis:
+ selection.startGizmoDrag(axis, x, y)
+ return True
+ except Exception as e:
+ print(f"[SSBOEditor] gizmo drag start failed: {e}")
+ return False
+
+ def _is_mouse_on_new_gizmo(self, new_transform, mouse_x, mouse_y):
+ """Refresh and query hover state for TransformGizmo on current click position."""
+ try:
+ mouse_pos = Point3(mouse_x, mouse_y, 0.0)
+ for gizmo_name in ("move_gizmo", "rotate_gizmo", "scale_gizmo"):
+ gizmo = getattr(new_transform, gizmo_name, None)
+ if not gizmo or not getattr(gizmo, "attached", False):
+ continue
+ hover_updater = getattr(gizmo, "_update_hover_highlight", None)
+ if callable(hover_updater):
+ hover_updater(mouse_pos)
+ return bool(getattr(new_transform, "is_hovering", False))
+ except Exception as e:
+ print(f"[SSBOEditor] new gizmo hover check failed: {e}")
+ return False
+
+ def focus_on_selected(self):
+ if self.selected_name and self.selected_ids:
+ first_id = self.selected_ids[0]
+ pos = self.controller.get_world_pos(first_id)
+ dist = 100
+ self.base.camera.set_pos(pos.x, pos.y - dist, pos.z + dist * 0.5)
+ self.base.camera.look_at(pos)
+
+ def draw_imgui(self):
+ if not self.controller: return
+
+ imgui.set_next_window_pos((10, 10), imgui.Cond_.first_use_ever)
+ imgui.set_next_window_size((350, 600), imgui.Cond_.first_use_ever)
+
+ expanded, opened = imgui.begin("Scene Tree (Component)")
+ if expanded:
+ imgui.text(f"FPS: {globalClock.getAverageFrameRate():.1f}")
+ imgui.separator()
+
+ changed, self.search_text = imgui.input_text("Search", self.search_text, 256)
+
+ if imgui.begin_child("ObjectList", (0, 380), child_flags=imgui.ChildFlags_.borders):
+ if self.search_text != self.last_search_text:
+ self.last_search_text = self.search_text
+ search_lower = self.search_text.lower()
+ self.filtered_nodes = []
+ for key in self.controller.node_list:
+ display = self.controller.display_names.get(key, key.split('/')[-1])
+ if not search_lower or (search_lower in display.lower() or search_lower in key.lower()):
+ geom_count = len(self.controller.name_to_ids.get(key, []))
+ self.filtered_nodes.append((key, display, geom_count))
+
+ # If list is empty initially (no search), show all
+ if not self.search_text and not self.filtered_nodes:
+ if len(self.filtered_nodes) != len(self.controller.node_list):
+ self.filtered_nodes = [(k, self.controller.display_names.get(k, k), len(self.controller.name_to_ids.get(k,[]))) for k in self.controller.node_list]
+
+ count = len(self.filtered_nodes)
+ clipper = imgui.ListClipper()
+ clipper.begin(count)
+ while clipper.step():
+ for i in range(clipper.display_start, clipper.display_end):
+ key, display, geom_count = self.filtered_nodes[i]
+ label = f"{display} ({geom_count})"
+ is_selected = (key == self.selected_name)
+ if imgui.selectable(label, is_selected)[0]:
+ self.select_node(key)
+ imgui.end_child()
+
+ imgui.separator()
+ if self.selected_name:
+ imgui.text_colored((1, 0.8, 0.2, 1), f"Selected: {self.selected_name}")
+ if imgui.button("Focus (F)"): self.focus_on_selected()
+ imgui.end()
+
+ # swap_transforms_task removed - motion blur disabled for performance
+
+ def update_task(self, task):
+ dt = globalClock.getDt()
+ io = imgui.get_io()
+
+ if io.want_capture_keyboard: return task.cont
+
+ if self.selected_ids and self.controller:
+ speed = 50 * dt
+ acc = Vec3(0, 0, 0)
+ if self.keys.get('arrow_up'): acc.z += speed
+ if self.keys.get('arrow_down'): acc.z -= speed
+ if self.keys.get('arrow_left'): acc.x -= speed
+ if self.keys.get('arrow_right'): acc.x += speed
+ if self.keys.get('z'): acc.y += speed
+ if self.keys.get('x'): acc.y -= speed
+
+ if acc.length_squared() > 0:
+ for idx in self.selected_ids:
+ self.controller.move_object(idx, acc)
+
+ return task.cont
diff --git a/ui/Builtin/Elements.py b/ui/Builtin/Elements.py
new file mode 100644
index 00000000..031dc1d0
--- /dev/null
+++ b/ui/Builtin/Elements.py
@@ -0,0 +1,339 @@
+"""
+
+
+
+OUTDATED
+
+Do not use anymore.
+
+
+"""
+
+
+import colorsys
+
+from LUIObject import LUIObject
+from LUISlider import LUISlider
+from LUISprite import LUISprite
+from LUIVerticalLayout import LUIVerticalLayout
+from LUICallback import LUICallback
+from LUILabel import LUILabel
+from LUIFrame import LUIFrame
+from LUIButton import LUIButton
+
+class LUISliderWithLabel(LUIObject, LUICallback):
+
+ def __init__(self, parent=None, width=100.0, filled=False, min_value=0, max_value=1.0, precision=2, value=None):
+ LUIObject.__init__(self, x=0, y=0, w=width, h=0)
+ LUICallback.__init__(self)
+
+ max_numbers_before = max(len(str(int(max_value))), len(str(int(min_value))))
+ number_space_required = max_numbers_before
+
+ if precision > 0:
+ number_space_required += 1 + precision
+
+ pixels_per_number = 7
+ self.precision = precision
+
+ self.slider = LUISlider(self, width=width - pixels_per_number * number_space_required - 5, filled=filled, min_value=min_value, max_value=max_value, value=value)
+ self.label = LUILabel(parent=self, shadow=True, text=u"1.23")
+ self.label.right = 0
+ self.label.top = self.label.height - self.slider.height
+ self.label.color = (1,1,1,0.5)
+
+ self.slider.add_change_callback(self._on_slider_changed)
+ self.slider.add_change_callback(self._trigger_callback)
+ self._on_slider_changed(self.slider, self.slider.get_value())
+
+ if parent is not None:
+ self.parent = parent
+
+ self.fit_to_children()
+
+ def get_value(self):
+ return self.slider.get_value()
+
+ def set_value(self, val):
+ self.slider.set_value(val)
+
+ def _on_slider_changed(self, obj, value):
+ self.label.text = ("{:." + str(self.precision) + "f}").format(value)
+
+class LUIKeyMarker(LUIObject):
+
+ def __init__(self, parent=None, key=u"A"):
+ LUIObject.__init__(self)
+ self.bgLeft = LUISprite(self, "Keymarker_Left", "skin")
+ self.bgMid = LUISprite(self, "Keymarker", "skin")
+ self.bgRight = LUISprite(self, "Keymarker_Right", "skin")
+
+ self.label = LUILabel(parent=self, text=key, shadow=True)
+ self.label.centered = (True, True)
+ self.label.margin = (-3, 0, 0, -1)
+ self.margin = (-1, 0, 0, -1)
+
+ self.set_key(key)
+
+ if parent is not None:
+ self.parent = parent
+
+ self.fit_to_children()
+
+ def set_key(self, key):
+ self.label.set_text(key)
+ self.width = self.label.width + self.bgLeft.width + self.bgRight.width + 7
+ self.bgMid.width = self.width - self.bgLeft.width - self.bgRight.width
+ self.bgMid.left = self.bgLeft.width
+ self.bgRight.left = self.bgMid.width + self.bgMid.left
+
+ self.fit_to_children()
+
+class LUIKeyInstruction(LUIObject):
+
+ def __init__(self, parent=None, key=u"A", instruction=u"Instruction"):
+ LUIObject.__init__(self)
+ self.marker = LUIKeyMarker(parent=self, key=key)
+ self.instructionLabel = LUILabel(parent=self, text=instruction, shadow=True)
+ self.instructionLabel.centered = (False, True)
+ self.instructionLabel.margin.top = -4
+ self.set_key(key)
+
+ def set_key(self, key):
+ self.marker.set_key(key)
+ self.instructionLabel.left = self.marker.width + 5
+ self.fit_to_children()
+
+
+class LUIColorpicker(LUIObject):
+
+ def __init__(self, parent=None, color=None):
+ LUIObject.__init__(self, x=0, y=0, w=27, h=27)
+
+ self.previewBg = LUISprite(self, "ColorpickerPreviewBg", "skin")
+
+ self.filler = LUISprite(self, "blank", "skin")
+ self.filler.width = 21
+ self.filler.height = 21
+ self.filler.pos = (5, 5)
+ self.filler.color = (0.2,0.6,1.0,1.0)
+
+ self.overlay = LUISprite(self, "ColorpickerPreviewOverlay", "skin")
+ self.overlay.pos = (2, 2)
+ self.overlay.bind("click", self._open_dialog)
+
+ self.fit_to_children()
+
+ self.popup = LUIColorpickerPopup(self)
+ self.popup.hide()
+
+ if color is not None:
+ self.colorValue = color
+ else:
+ # My favourite color
+ self.colorValue = (0.2, 0.6, 1.0)
+ self.set_color_value(self.colorValue)
+
+ self.popup.add_change_callback(self._on_popup_color_changed)
+
+ if parent is not None:
+ self.parent = parent
+
+ def _open_dialog(self, event):
+ if self.has_focus():
+ self.blur()
+ else:
+ self.request_focus()
+
+ def on_focus(self, event):
+ self.popup._load_rgb(self.colorValue)
+ self.popup.open_at(self, 14.0)
+
+ def set_color_value(self, rgb):
+ self.colorValue = rgb
+ self.filler.color = rgb
+
+ def get_color_value(self):
+ return self.colorValue
+
+ def on_tick(self, event):
+ self.popup._update(event)
+
+ def on_blur(self, event):
+ self.popup.close()
+
+ def _on_popup_color_changed(self, popup, rgb):
+ self.set_color_value(rgb)
+
+ def _on_popup_closed(self):
+ self.blur()
+
+
+class LUIPopup(LUIFrame):
+
+ def __init__(self, parent=None, width=200, height=200):
+ LUIFrame.__init__(self, parent=parent, width=width, height=height, padding=10, innerPadding=0)
+ self.topmost = True
+ self.borderSize = 33
+ self.content.bind("click", self._on_content_click)
+
+ def open_at(self, targetElement, distance):
+ self.show()
+
+ targetPos = targetElement.get_abs_pos()+ targetElement.get_size() / 2
+
+ showAbove = targetPos.y > self.height - self.borderSize
+ showLeft = targetPos.x > self.width - self.borderSize
+
+ relative = self.get_relative_pos(targetPos)
+ self.pos += relative
+
+ if showLeft:
+ self.left -= self.width - self.borderSize
+ self.left += 25
+ else:
+ self.left -= self.borderSize
+ self.left -= 25
+
+ if showAbove:
+ self.top -= distance
+ self.top -= self.height - self.borderSize
+ else:
+ self.top += distance
+ self.top -= self.borderSize
+
+
+ def _on_content_click(self, event):
+ pass
+
+ def close(self):
+ self.hide()
+
+class LUIColorpickerPopup(LUIPopup, LUICallback):
+ def __init__(self, parent=None):
+ LUIPopup.__init__(self, parent=parent, width=240, height=146)
+ LUICallback.__init__(self)
+
+ self.field = LUIObject(self.content, x=0, y=0, w=128, h=128)
+
+ self.fieldBG = LUISprite(self.field, "blank", "skin")
+ self.fieldBG.size = (128, 128)
+ self.fieldBG.color = (0.2,0.6,1.0)
+ self.fieldFG = LUISprite(self.field, "ColorpickerFieldOverlay", "skin")
+ self.fieldFG.pos = (-2, 0)
+
+ self.fieldBG.bind("mousedown", self._start_field_dragging)
+ self.fieldBG.bind("mouseup", self._stop_field_dragging)
+
+ self.fieldHandle = LUISprite(self.field, "ColorpickerFieldHandle", "skin")
+ self.fieldHandle.bind("mousedown", self._start_field_dragging)
+ self.fieldHandle.bind("mouseup", self._stop_field_dragging)
+
+ self.fieldDragging = False
+
+ self.hueSlider = LUIObject(self.content, x=140, y=0, w=40, h=128)
+ self.hueSliderFG = LUISprite(self.hueSlider, "ColorpickerHueSlider", "skin")
+
+ self.hueHandle = LUISprite(self.hueSlider, "ColorpickerHueHandle", "skin")
+ self.hueHandle.left = (self.hueSliderFG.width - self.hueHandle.width) / 2.0
+ self.hueHandle.top = 50
+
+ self.hueDragging = False
+ self.hueSlider.bind("mousedown", self._start_hue_dragging)
+ self.hueSlider.bind("mouseup", self._stop_hue_dragging)
+
+ self.labels = LUIVerticalLayout(self.content, width=40)
+ self.labels.pos = (177, 42)
+
+ colors = [u"R", u"G", u"B"]
+ self.colorLabels = []
+
+ for color in colors:
+ label = LUILabel(text=color, shadow=True)
+ label.color = (1,1,1,0.3)
+
+ valueLabel = LUILabel(text=u"255", shadow=True)
+ valueLabel.right = 0
+ self.labels.add(label, valueLabel)
+ self.colorLabels.append(valueLabel)
+
+ self.activeColor = LUIObject(self.content, x=177, y=0)
+ self.activeColorBG = LUISprite(self.activeColor, "blank", "skin")
+ self.activeColorFG = LUISprite(self.activeColor, "ColorpickerActiveColorOverlay", "skin")
+
+ self.activeColorBG.size = (40, 40)
+ self.activeColorBG.pos = (2, 0)
+ self.activeColorBG.color = (0.2,0.6,1.0,1.0)
+
+ self.closeButton = LUIButton(parent=self.content, text=u"Done", width=45, template="ButtonGreen")
+ self.closeButton.left = 177
+ self.closeButton.top = 98
+ self.closeButton.bind("click", self._close_popup)
+
+ self._set_hue(0.5)
+ self._set_sat_val(0.5, 0.5)
+
+ self.widget = parent
+
+ def _load_rgb(self, rgb):
+ hsv = colorsys.rgb_to_hsv(*rgb)
+ self._set_hue(hsv[0])
+ self._set_sat_val(hsv[1], hsv[2])
+
+ def _close_popup(self, event):
+ self.widget._on_popup_closed()
+ self.close()
+
+ def _update(self, event):
+ if self.hueDragging:
+ offset = event.coordinates.y - self.hueSliderFG.abs_pos.y
+ offset /= 128.0
+ offset = 1.0 - max(0.0, min(1.0, offset))
+ self._set_hue(offset)
+
+ if self.fieldDragging:
+ offset = event.coordinates - self.fieldBG.abs_pos
+ saturation = max(0.0, min(1.0, offset.x / 128.0))
+ value = 1.0 - max(0.0, min(1.0, offset.y / 128.0))
+ self._set_sat_val(saturation, value)
+
+ self._update_color()
+
+ def _set_sat_val(self, sat, val):
+ self.saturation = sat
+ self.valueValue = val
+
+ self.fieldHandle.top = (1.0 - self.valueValue) * 128.0 - self.fieldHandle.height / 2.0
+ self.fieldHandle.left = self.saturation * 128.0 - self.fieldHandle.width / 2.0
+
+ def _set_hue(self, hue):
+ self.hueValue = min(0.999, hue)
+ self.hueHandle.top = (1.0-hue) * 128.0 - self.hueHandle.height / 2
+ self.fieldBG.color = colorsys.hsv_to_rgb(self.hueValue, 1, 1)
+
+ def _update_color(self):
+ rgb = colorsys.hsv_to_rgb(self.hueValue, self.saturation, self.valueValue)
+ self.activeColorBG.color = rgb
+
+ self.colorLabels[0].set_text(str(int(rgb[0]*255.0)))
+ self.colorLabels[1].set_text(str(int(rgb[1]*255.0)))
+ self.colorLabels[2].set_text(str(int(rgb[2]*255.0)))
+
+ self._trigger_callback(rgb)
+
+ def _start_field_dragging(self, event):
+ if not self.fieldDragging:
+ self.fieldDragging = True
+
+ def _stop_field_dragging(self, event):
+ if self.fieldDragging:
+ self.fieldDragging = False
+
+ def _start_hue_dragging(self, event):
+ if not self.hueDragging:
+ self.hueDragging = True
+
+ def _stop_hue_dragging(self, event):
+ if self.hueDragging:
+ self.hueDragging = False
+
diff --git a/ui/Builtin/LUIBlockText.py b/ui/Builtin/LUIBlockText.py
new file mode 100644
index 00000000..d74d3c8b
--- /dev/null
+++ b/ui/Builtin/LUIBlockText.py
@@ -0,0 +1,100 @@
+
+from panda3d.core import LVecBase2i
+from LUIObject import LUIObject
+from LUILabel import LUILabel
+from LUIInitialState import LUIInitialState
+
+__all__ = ["LUIBlockText"]
+
+
+class LUIBlockText(LUIObject):
+
+ """ Small helper class to format labels into paragraphs.
+ Uses LUILabels internally """
+
+ def __init__(self, **kwargs):
+ """ Creates a new block of text. """
+ LUIObject.__init__(self)
+ LUIInitialState.init(self, kwargs)
+ self._cursor = LVecBase2i(0)
+ self._last_size = 14
+
+ self.labels = []
+
+
+ def clear(self):
+ """ Removes all text from this label and resets it to the initial state.
+ This will also detach the sub-labels from this label. """
+ self._cursor.set(0, 0)
+ self.labels = []
+ self.remove_all_children()
+
+
+ def newline(self, font_size=None):
+ """ Moves the cursor to the next line. The font size controls how much
+ the cursor will move. By default, the font size of the last added text
+ is used, or if no text was added yet, a size of 14."""
+ self._cursor.x = 0
+ if font_size is None:
+ font_size = self._last_size
+ self._cursor.y += font_size + 2
+
+
+ def add(self, *args, **kwargs):
+ """ Appends a new text. The arguments are equal to the arguments of
+ LUILabel. The arguments shouldn't contain information about the
+ placement like top_left, or center_vertical, since the labels are
+ placed at explicit positions. """
+ self._last_size = kwargs.get("font_size", 14)
+ label = LUILabel(parent=self, left=self._cursor.x, top=self._cursor.y, width=self.get_width(),
+ *args, **kwargs)
+
+ self.labels.append(label)
+
+ # This is a bit of a hack, we should use a horizontal layout, but we
+ # don't for performance reasons.
+ self._cursor.y += label.text_handle.height
+
+ # After every paragraph, we add a new line.
+ self.newline()
+
+
+ def set_text(self, text):
+ """ Replaces the text with new text """
+ self.clear()
+ self.add(text=text)
+
+
+ def update_height(self):
+ """ Updates the height of the element, adding a newline to the end of
+ every paragraph """
+ top = 0
+ for child in self.labels:
+ child.top = top
+ top += child._text.height
+
+ # Newline
+ top += self._last_size + 2
+
+
+ def set_wrap(self, wrap):
+ """ Sets text wrapping for the element. Wrapping breaks lines on
+ spaces, and breaks words if the word is longer than the line
+ length. """
+ for child in self.children:
+ for c in child.children:
+ c.set_wordwrap(wrap)
+
+ self.update_height()
+
+
+ def set_width(self, width):
+ """ Sets the width of this element, and turns on wrapping. """
+ for child in self.children:
+ child.set_width(width)
+
+ # Need to force an update to the text when the width changes.
+ for c in child.children:
+ c.set_wordwrap(True)
+
+ self.update_height()
diff --git a/ui/Builtin/LUIButton.py b/ui/Builtin/LUIButton.py
new file mode 100644
index 00000000..6921efdc
--- /dev/null
+++ b/ui/Builtin/LUIButton.py
@@ -0,0 +1,192 @@
+
+from LUIObject import LUIObject
+from LUILayouts import LUIHorizontalStretchedLayout
+from LUILabel import LUILabel
+from LUIInitialState import LUIInitialState
+
+__all__ = ["LUIButton"]
+
+
+class LUIButton(LUIObject):
+
+ """ Simple button, containing three sprites and a label. """
+
+ def __init__(self, text="Button", template="ButtonDefault", **kwargs):
+ """ Constructs a new button. The template controls which sprites to use.
+ If the template is "ButtonDefault" for example, the sprites
+ "ButtonDefault_Left", "ButtonDefault" and "ButtonDefault_Right" will
+ be used. The sprites used when the button is pressed should be named
+ "ButtonDefaultFocus_Left" and so on then.
+
+ If an explicit width is set on the button, the button will stick to
+ that width, otherwise it will automatically resize to fit the label """
+ LUIObject.__init__(self, x=0, y=0, solid=True)
+ self._template = template
+ self._layout = LUIHorizontalStretchedLayout(
+ parent=self, prefix=self._template, width="100%")
+ self._label = LUILabel(parent=self, text=text)
+ self._label.z_offset = 1
+ self._label.center_vertical = True
+ self._label.margin = 0, 20, 0, 20
+ self.margin.left = -1
+ self._hovered = False
+ self._pressed = False
+ self._use_custom_texture = False
+ self._custom_texture = None
+ self._custom_uv = None
+ self._custom_texture_hover = None
+ self._custom_uv_hover = None
+ self._custom_texture_pressed = None
+ self._custom_uv_pressed = None
+ LUIInitialState.init(self, kwargs)
+
+ def _apply_stretch_sizes(self):
+ """Ensure internal sprites stretch to the button size."""
+ try:
+ layout = getattr(self, '_layout', None)
+ if layout is not None:
+ if hasattr(layout, 'width'):
+ layout.width = "100%"
+ if hasattr(layout, 'height'):
+ layout.height = "100%"
+ inner = getattr(layout, '_layout', None)
+ if inner is not None:
+ if hasattr(inner, 'width'):
+ inner.width = "100%"
+ if hasattr(inner, 'height'):
+ inner.height = "100%"
+ for attr in ('_sprite_left', '_sprite_mid', '_sprite_right'):
+ spr = getattr(layout, attr, None)
+ if spr is not None:
+ if hasattr(spr, 'height'):
+ spr.height = "100%"
+ if attr == '_sprite_mid' and hasattr(spr, 'width'):
+ spr.width = "100%"
+ except Exception:
+ pass
+
+ def _apply_custom_texture(self, state="normal"):
+ """Apply custom texture based on state: normal/hover/pressed."""
+ if not self._use_custom_texture:
+ return
+
+ tex = None
+ uv = None
+ if state == "pressed" and self._custom_texture_pressed is not None:
+ tex = self._custom_texture_pressed
+ uv = self._custom_uv_pressed
+ elif state == "hover" and self._custom_texture_hover is not None:
+ tex = self._custom_texture_hover
+ uv = self._custom_uv_hover
+ else:
+ tex = self._custom_texture
+ uv = self._custom_uv
+
+ if tex is None:
+ return
+
+ layout = getattr(self, "_layout", None)
+ if layout is None:
+ return
+ for attr in ("_sprite_left", "_sprite_mid", "_sprite_right"):
+ spr = getattr(layout, attr, None)
+ if spr is None:
+ continue
+ try:
+ if hasattr(spr, "set_texture"):
+ spr.set_texture(tex, resize=False)
+ if uv and hasattr(spr, "set_uv_range"):
+ u0, v0, u1, v1 = uv
+ spr.set_uv_range(u0, v0, u1, v1)
+ except Exception:
+ pass
+
+ # Hide left/right caps for single-image mode
+ try:
+ if hasattr(layout, "_sprite_left") and layout._sprite_left is not None:
+ layout._sprite_left.width = 0
+ if hasattr(layout, "_sprite_right") and layout._sprite_right is not None:
+ layout._sprite_right.width = 0
+ except Exception:
+ pass
+
+ self._apply_stretch_sizes()
+
+ def set_custom_textures(self, normal_tex, normal_uv=None, hover_tex=None, hover_uv=None, pressed_tex=None, pressed_uv=None):
+ """Set custom textures for normal/hover/pressed states."""
+ self._use_custom_texture = True
+ self._custom_texture = normal_tex
+ self._custom_uv = normal_uv
+ self._custom_texture_hover = hover_tex
+ self._custom_uv_hover = hover_uv
+ self._custom_texture_pressed = pressed_tex
+ self._custom_uv_pressed = pressed_uv
+ if self._hovered:
+ self._apply_custom_texture("hover")
+ else:
+ self._apply_custom_texture("normal")
+
+ def set_custom_texture(self, texture, uv=None):
+ """Use a single texture for the button background."""
+ self.set_custom_textures(texture, uv, None, None, None, None)
+
+ def clear_custom_texture(self):
+ """Restore default template textures."""
+ self._use_custom_texture = False
+ self._custom_texture = None
+ self._custom_uv = None
+ self._custom_texture_hover = None
+ self._custom_uv_hover = None
+ self._custom_texture_pressed = None
+ self._custom_uv_pressed = None
+ try:
+ self._layout.prefix = self._template
+ self._apply_stretch_sizes()
+ except Exception:
+ pass
+
+ @property
+ def text(self):
+ """ Returns the current label text of the button """
+ return self._label.text
+
+ @text.setter
+ def text(self, text):
+ """ Sets the label text of the button """
+ self._label.text = text
+
+ def on_mousedown(self, event):
+ """ Internal on_mousedown handler. Do not override """
+ self._pressed = True
+ if self._use_custom_texture:
+ self._apply_custom_texture("pressed")
+ else:
+ self._layout.prefix = self._template + "Focus"
+ self._apply_stretch_sizes()
+ self._label.margin.top = 1
+
+ def on_mouseup(self, event):
+ """ Internal on_mouseup handler. Do not override """
+ self._pressed = False
+ if self._use_custom_texture:
+ if self._hovered:
+ self._apply_custom_texture("hover")
+ else:
+ self._apply_custom_texture("normal")
+ else:
+ self._layout.prefix = self._template
+ self._apply_stretch_sizes()
+ self._label.margin.top = 0
+
+ def on_mouseover(self, event):
+ """ Internal mouseover handler """
+ self._hovered = True
+ if self._use_custom_texture and not self._pressed:
+ self._apply_custom_texture("hover")
+
+ def on_mouseout(self, event):
+ """ Internal mouseout handler """
+ self._hovered = False
+ self._pressed = False
+ if self._use_custom_texture:
+ self._apply_custom_texture("normal")
diff --git a/ui/Builtin/LUICanvas.py b/ui/Builtin/LUICanvas.py
new file mode 100644
index 00000000..b6593006
--- /dev/null
+++ b/ui/Builtin/LUICanvas.py
@@ -0,0 +1,102 @@
+"""
+LUICanvas - 类似Unity的Canvas容器,带有可视化边框
+用于组织和管理UI元素,所有UI组件都应该是Canvas的子节点
+"""
+
+from LUIObject import LUIObject
+from LUISprite import LUISprite
+from LUIInitialState import LUIInitialState
+
+__all__ = ["LUICanvas"]
+
+
+class LUICanvas(LUIObject):
+ """
+ Canvas容器,类似Unity的Canvas系统
+ - 所有UI元素的父容器
+ - 带有可视化的边框线框(类似Unity编辑器中的Canvas Gizmo)
+ - 可以设置边框颜色和宽度
+ """
+
+ def __init__(self, border_color=(0.3, 0.7, 1.0, 0.3), border_width=2, **kwargs):
+ """
+ 创建一个新的Canvas
+
+ 参数:
+ border_color: 边框颜色 (r, g, b, a),默认为蓝色半透明
+ border_width: 边框宽度,默认为2像素(使用平面模式时此参数无效)
+ """
+ LUIObject.__init__(self)
+
+ self._border_width = border_width
+ self._border_color = border_color
+
+ # 创建一个透明平面覆盖整个Canvas
+ self._border_plane = LUISprite(self, "blank", "skin")
+ self._border_plane.pos = (0, 0)
+ self._border_plane.color = self._border_color
+ self._border_plane.z_offset = 0
+
+ # 先设置一个固定尺寸,确保可见
+ self._border_plane.width = 1280
+ self._border_plane.height = 720
+
+ # 应用初始状态
+ LUIInitialState.init(self, kwargs)
+
+ # 最后设置Canvas占满整个屏幕(这会触发setter更新边框)
+ self.set_size("100%", "100%")
+
+ def _update_borders(self):
+ """更新边框平面的位置和大小"""
+ # 直接获取width和height属性值
+ try:
+ width = self.width
+ height = self.height
+
+ # 设置平面覆盖整个Canvas区域
+ self._border_plane.pos = (0, 0)
+ self._border_plane.width = width
+ self._border_plane.height = height
+ except:
+ # 如果获取失败,使用默认值
+ pass
+
+ def set_border_color(self, color):
+ """设置边框颜色 (r, g, b, a)"""
+ self._border_color = color
+ self._border_plane.color = color
+
+ def set_border_width(self, width):
+ """设置边框宽度(平面模式下无效)"""
+ self._border_width = width
+
+ def show_border(self):
+ """显示边框"""
+ self._border_plane.show()
+
+ def hide_border(self):
+ """隐藏边框"""
+ self._border_plane.hide()
+
+ @property
+ def width(self):
+ """获取宽度"""
+ return LUIObject.width.fget(self)
+
+ @width.setter
+ def width(self, value):
+ """设置宽度并更新边框"""
+ LUIObject.width.fset(self, value)
+ self._update_borders()
+
+ @property
+ def height(self):
+ """获取高度"""
+ return LUIObject.height.fget(self)
+
+ @height.setter
+ def height(self, value):
+ """设置高度并更新边框"""
+ LUIObject.height.fset(self, value)
+ self._update_borders()
diff --git a/ui/Builtin/LUICheckbox.py b/ui/Builtin/LUICheckbox.py
new file mode 100644
index 00000000..11fdbc7c
--- /dev/null
+++ b/ui/Builtin/LUICheckbox.py
@@ -0,0 +1,83 @@
+
+from __future__ import division
+
+from LUIObject import LUIObject
+from LUISprite import LUISprite
+from LUILabel import LUILabel
+from LUIInitialState import LUIInitialState
+
+__all__ = ["LUICheckbox"]
+
+
+class LUICheckbox(LUIObject):
+
+ """ This is a simple checkbox, including a Label. The checkbox can either
+ be checked or unchecked. """
+
+ def __init__(self, checked=False, label=u"Checkbox", **kwargs):
+ """ Constructs a new checkbox with the given label and state. By default,
+ the checkbox is not checked. """
+ LUIObject.__init__(self, x=0, y=0, solid=True)
+ self._checked = checked
+ self._checkbox_sprite = LUISprite(self, "Checkbox_Default", "skin")
+ self._label = LUILabel(parent=self, text=label, margin=(0, 0, 0, 25),
+ center_vertical=True, alpha=0.4)
+ self._hovered = False
+ self._update_sprite()
+ LUIInitialState.init(self, kwargs)
+
+ @property
+ def checked(self):
+ """ Returns True if the checkbox is currently checked """
+ return self._checked
+
+ @checked.setter
+ def checked(self, checked):
+ """ Sets the checkbox state """
+ self._checked = checked
+ self._update_sprite()
+
+ def toggle(self):
+ """ Toggles the checkbox state """
+ self.checked = not self.checked
+
+ @property
+ def label(self):
+ """ Returns a handle to the label, so it can be modified """
+ return self._label
+
+ @property
+ def sprite(self):
+ """ Returns a handle to the internal checkbox sprite """
+ return self._checkbox_sprite
+
+ def on_click(self, event):
+ """ Internal onclick handler. Do not override """
+ self._checked = not self._checked
+ self.trigger_event("changed")
+ self._update_sprite()
+
+ def on_mousedown(self, event):
+ """ Internal mousedown handler. """
+ self._checkbox_sprite.color = (0.9, 0.9, 0.9, 1.0)
+
+ def on_mouseup(self, event):
+ """ Internal on_mouseup handler. """
+ self._checkbox_sprite.color = (1, 1, 1, 1)
+
+ def on_mouseover(self, event):
+ """ Internal mouseover handler """
+ self._hovered = True
+ self._update_sprite()
+
+ def on_mouseout(self, event):
+ """ Internal mouseout handler """
+ self._hovered = False
+ self._update_sprite()
+
+ def _update_sprite(self):
+ """ Internal method to update the sprites """
+ img = "Checkbox_Checked" if self._checked else "Checkbox_Default"
+ if self._hovered:
+ img += "Hover"
+ self._checkbox_sprite.set_texture(img, "skin")
diff --git a/ui/Builtin/LUIFormattedLabel.py b/ui/Builtin/LUIFormattedLabel.py
new file mode 100644
index 00000000..fad7df3e
--- /dev/null
+++ b/ui/Builtin/LUIFormattedLabel.py
@@ -0,0 +1,47 @@
+
+from panda3d.core import LVecBase2i
+from LUIObject import LUIObject
+from LUILabel import LUILabel
+from LUIInitialState import LUIInitialState
+
+__all__ = ["LUIFormattedLabel"]
+
+
+class LUIFormattedLabel(LUIObject):
+
+ """ Small helper class to build a text consisting of different formatted
+ parts of text. Uses LUILabels internally """
+
+ def __init__(self, **kwargs):
+ """ Creates a new formatted label. """
+ LUIObject.__init__(self)
+ LUIInitialState.init(self, kwargs)
+ self._cursor = LVecBase2i(0)
+ self._last_size = 14
+
+ def clear(self):
+ """ Removes all text from this label and resets it to the initial state.
+ This will also detach the sub-labels from this label. """
+ self._cursor.set(0, 0)
+ self.remove_all_children()
+
+ def newline(self, font_size=None):
+ """ Moves the cursor to the next line. The font size controls how much
+ the cursor will move. By default, the font size of the last added text
+ is used, or if no text was added yet, a size of 14."""
+ self._cursor.x = 0
+ if font_size is None:
+ font_size = self._last_size
+ self._cursor.y += font_size + 2
+
+ def add(self, *args, **kwargs):
+ """ Appends a new text. The arguments are equal to the arguments of
+ LUILabel. The arguments shouldn't contain information about the
+ placement like top_left, or center_vertical, since the labels are
+ placed at explicit positions. """
+ self._last_size = kwargs.get("font_size", 14)
+ label = LUILabel(parent=self, left=self._cursor.x, top=self._cursor.y,
+ *args, **kwargs)
+ # This is a bit of a hack, we should use a horizontal layout, but we
+ # don't for performance reasons.
+ self._cursor.x += label.text_handle.width
diff --git a/ui/Builtin/LUIFrame.py b/ui/Builtin/LUIFrame.py
new file mode 100644
index 00000000..4408b5c7
--- /dev/null
+++ b/ui/Builtin/LUIFrame.py
@@ -0,0 +1,66 @@
+
+from __future__ import print_function
+
+from LUIObject import LUIObject
+from LUISprite import LUISprite
+from LUILayouts import LUICornerLayout
+from LUIInitialState import LUIInitialState
+from LUIScrollableRegion import LUIScrollableRegion
+
+__all__ = ["LUIFrame"]
+
+
+class LUIFrame(LUIObject):
+
+ """ A container which can store multiple ui-elements. If you don't want a
+ border/background, you should use an empty LUIObject as container instead.
+ """
+
+ FS_sunken = 1
+ FS_raised = 2
+
+ def __init__(self, inner_padding=5, scrollable=False, style=FS_raised,
+ **kwargs):
+ """ Creates a new frame with the given options and style. If scrollable
+ is True, the contents of the frame will scroll if they don't fit into
+ the frame height. inner_padding only has effect if scrollable is True.
+ You can call fit_to_children() to make the frame fit automatically to
+ it's contents."""
+ LUIObject.__init__(self)
+
+ # Each *style* has a different border size (size of the shadow). The
+ # border size shouldn't get calculated to the actual framesize, so we
+ # are determining it first and then substracting it.
+ # TODO: We could do this automatically, determined by the sprite size
+ # probably?
+ self._border_size = 0
+ self.padding = 10
+ self.solid = True
+ prefix = ""
+
+ if style == LUIFrame.FS_raised:
+ temp = LUISprite(self, "Frame_Left", "skin")
+ self._border_size = temp.width
+ self.remove_child(temp)
+ prefix = "Frame_"
+ elif style == LUIFrame.FS_sunken:
+ self._border_size = 0
+ prefix = "SunkenFrame_"
+ else:
+ raise Exception("Unkown LUIFrame style: " + style)
+
+ self._scrollable = scrollable
+ self._layout = LUICornerLayout(parent=self, image_prefix=prefix)
+ self._layout.margin = -(self.padding.top + self._border_size)
+ if self._scrollable:
+ self._content = LUIObject(self)
+ self._content.size = (self.width, self.height)
+ self._content.pos = (self._border_size, self._border_size)
+ self._scroll_content = LUIScrollableRegion(
+ self._content,
+ width=self.width - 2 * self.padding.left,
+ height=self.height - 2 * self.padding.left,
+ padding=inner_padding)
+ self.content_node = self._scroll_content.content_node
+
+ LUIInitialState.init(self, kwargs)
diff --git a/ui/Builtin/LUIHorizontalLayout.py b/ui/Builtin/LUIHorizontalLayout.py
new file mode 100644
index 00000000..3a887a2c
--- /dev/null
+++ b/ui/Builtin/LUIHorizontalLayout.py
@@ -0,0 +1,20 @@
+"""
+
+This is a wrapper file. It contains no actual implementation
+
+"""
+
+from panda3d.lui import LUIHorizontalLayout as _LUIHorizontalLayout
+from LUIInitialState import LUIInitialState
+
+__all__ = ["LUIHorizontalLayout"]
+
+
+class LUIHorizontalLayout(_LUIHorizontalLayout):
+ """ This is a wrapper class for the C++ LUIHorizontalLayout class, to be
+ able to use it in a more convenient way. It leverages LUIInitialState
+ to be able to pass arbitrary keyword arguments. """
+
+ def __init__(self, parent=None, spacing=0.0, **kwargs):
+ _LUIHorizontalLayout.__init__(self, parent, spacing)
+ LUIInitialState.init(self, kwargs)
diff --git a/ui/Builtin/LUIInitialState.py b/ui/Builtin/LUIInitialState.py
new file mode 100644
index 00000000..cf3eb590
--- /dev/null
+++ b/ui/Builtin/LUIInitialState.py
@@ -0,0 +1,41 @@
+
+__all__ = ["LUIInitialState"]
+
+
+class LUIInitialState:
+
+ """ Small helper class to pass keyword arguments to the LUI-objects. It takes
+ all keyword arguments of a given call, and calls obj. = for
+ each keyword. It usually is called at the end of the __init__ method. """
+
+ def __init__(self):
+ raise Exception("LUIInitialState is a static class")
+
+ # Some properties have alternative names, under which they can be accessed.
+ __MAPPINGS = {
+ "x": "left",
+ "y": "top",
+ "w": "width",
+ "h": "height"
+ }
+
+ @classmethod
+ def init(cls, obj, kwargs):
+ """ Applies all keyword arguments as properties. For example, passing
+ dict({"left": 10, "top": 3, "color": (0.2, 0.6, 1.0)}) results in
+ behaviour similar to:
+
+ element.left = 10
+ element.top = 3
+ element.color = 0.2, 0.6, 1.0
+
+ Calling this method allows setting arbitrary properties in
+ constructors, without having to specify each possible keyword argument.
+ """
+ for arg_name, arg_val in kwargs.items():
+ arg_name = cls.__MAPPINGS.get(arg_name, arg_name)
+ if hasattr(obj, arg_name):
+ setattr(obj, arg_name, arg_val)
+ else:
+ raise AttributeError("{0} has no attribute {1}".format(
+ obj.__class__.__name__, arg_name))
diff --git a/ui/Builtin/LUIInputField.py b/ui/Builtin/LUIInputField.py
new file mode 100644
index 00000000..b3863c2f
--- /dev/null
+++ b/ui/Builtin/LUIInputField.py
@@ -0,0 +1,219 @@
+import re
+
+from LUIObject import LUIObject
+from LUISprite import LUISprite
+from LUILabel import LUILabel
+from LUIInitialState import LUIInitialState
+from LUILayouts import LUIHorizontalStretchedLayout
+
+__all__ = ["LUIInputField"]
+
+
+class LUIInputField(LUIObject):
+
+ """ Simple input field, accepting text input. This input field supports
+ entering text and navigating. Selecting text is (currently) not supported.
+
+ The input field also supports various keyboard shortcuts:
+
+ [pos1] Move to the beginning of the text
+ [end] Move to the end of the text
+ [arrow_left] Move one character to the left
+ [arrow_right] Move one character to the right
+ [ctrl] + [arrow_left] Move to the left, skipping over words
+ [ctrl] + [arrow_right] Move to the right, skipping over words
+ [escape] Un-focus input element
+
+ """
+
+ re_skip = re.compile("\W*\w+\W")
+
+ def __init__(self, parent=None, width=200, placeholder=u"Enter some text ..", value=u"", **kwargs):
+ """ Constructs a new input field. An input field always needs a width specified """
+ LUIObject.__init__(self, x=0, y=0, solid=True)
+ self.set_width(width)
+ self._layout = LUIHorizontalStretchedLayout(parent=self, prefix="InputField", width="100%")
+
+ # Container for the text
+ self._text_content = LUIObject(self)
+ self._text_content.margin = (5, 7, 5, 7)
+ self._text_content.clip_bounds = (0,0,0,0)
+ self._text_content.set_size("100%", "100%")
+
+ # Scroller for the text, so we can move right and left
+ self._text_scroller = LUIObject(parent=self._text_content)
+ self._text_scroller.center_vertical = True
+ self._text = LUILabel(parent=self._text_scroller, text="")
+
+ # Cursor for the current position
+ self._cursor = LUISprite(self._text_scroller, "blank", "skin", x=0, y=0, w=2, h=15)
+ self._cursor.color = (0.5, 0.5, 0.5)
+ self._cursor.margin.top = 2
+ self._cursor.z_offset = 20
+ self._cursor_index = 0
+ self._cursor.hide()
+ self._value = value
+
+ # Placeholder text, shown when out of focus and no value exists
+ self._placeholder = LUILabel(parent=self._text_content, text=placeholder, shadow=False,
+ center_vertical=True, alpha=0.2)
+
+ # Various states
+ self._tickrate = 1.0
+ self._tickstart = 0.0
+
+ self._render_text()
+
+ if parent is not None:
+ self.parent = parent
+
+ LUIInitialState.init(self, kwargs)
+
+ @property
+ def value(self):
+ """ Returns the value of the input field """
+ return self._value
+
+ @value.setter
+ def value(self, new_value):
+ """ Sets the value of the input field """
+ self._value = new_value
+ self._render_text()
+ self.trigger_event("changed", self._value)
+
+ def clear(self):
+ """ Clears the input value """
+ self.value = u""
+
+ @property
+ def cursor_pos(self):
+ """ Set the cursor position """
+ return self._cursor_index
+
+ @cursor_pos.setter
+ def cursor_pos(self, pos):
+ """ Set the cursor position """
+ if pos >= 0:
+ self._cursor_index = max(0, min(len(self._value), pos))
+ else:
+ self._cursor_index = max(len(self._value) + pos + 1, 0)
+ self._reset_cursor_tick()
+ self._render_text()
+
+ def on_tick(self, event):
+ """ Tick handler, gets executed every frame """
+ frame_time = globalClock.get_frame_time() - self._tickstart
+ show_cursor = frame_time % self._tickrate < 0.5 * self._tickrate
+ if show_cursor:
+ self._cursor.color = (0.5, 0.5, 0.5, 1)
+ else:
+ self._cursor.color = (1, 1, 1, 0)
+
+ def on_click(self, event):
+ """ Internal on click handler """
+ self.request_focus()
+
+ def on_mousedown(self, event):
+ """ Internal mousedown handler """
+ local_x_offset = self._text.text_handle.get_relative_pos(event.coordinates).x
+ self.cursor_pos = self._text.text_handle.get_char_index(local_x_offset)
+
+ def _reset_cursor_tick(self):
+ """ Internal method to reset the cursor tick """
+ self._tickstart = globalClock.get_frame_time()
+
+ def on_focus(self, event):
+ """ Internal focus handler """
+ self._cursor.show()
+ self._placeholder.hide()
+ self._reset_cursor_tick()
+ self._layout.color = (0.9, 0.9, 0.9, 1)
+
+ def on_keydown(self, event):
+ """ Internal keydown handler. Processes the special keys, and if none are
+ present, redirects the event """
+ key_name = event.message
+ if key_name == "backspace":
+ self._value = self._value[:max(0, self._cursor_index - 1)] + self._value[self._cursor_index:]
+ self.cursor_pos -= 1
+ self.trigger_event("changed", self._value)
+ elif key_name == "delete":
+ post_value = self._value[min(len(self._value), self._cursor_index + 1):]
+ self._value = self._value[:self._cursor_index] + post_value
+ self.cursor_pos = self._cursor_index
+ self.trigger_event("changed", self._value)
+ elif key_name == "arrow_left":
+ if event.get_modifier_state("alt") or event.get_modifier_state("ctrl"):
+ self.cursor_skip_left()
+ else:
+ self.cursor_pos -= 1
+ elif key_name == "arrow_right":
+ if event.get_modifier_state("alt") or event.get_modifier_state("ctrl"):
+ self.cursor_skip_right()
+ else:
+ self.cursor_pos += 1
+ elif key_name == "escape":
+ self.blur()
+ elif key_name == "home":
+ self.cursor_pos = 0
+ elif key_name == "end":
+ self.cursor_pos = len(self.value)
+
+ self.trigger_event(key_name, self._value)
+
+ def on_keyrepeat(self, event):
+ """ Internal keyrepeat handler """
+ self.on_keydown(event)
+
+ def on_textinput(self, event):
+ """ Internal textinput handler """
+ self._value = self._value[:self._cursor_index] + event.message + \
+ self._value[self._cursor_index:]
+ self.cursor_pos = self._cursor_index + len(event.message)
+ self.trigger_event("changed", self._value)
+
+ def on_blur(self, event):
+ """ Internal blur handler """
+ self._cursor.hide()
+ if len(self._value) < 1:
+ self._placeholder.show()
+
+ self._layout.color = (1, 1, 1, 1)
+
+ def _render_text(self):
+ """ Internal method to render the text """
+ self._text.set_text(self._value)
+ self._cursor.left = self._text.left + \
+ self._text.text_handle.get_char_pos(self._cursor_index) + 1
+ max_left = self.width - 15
+
+ if self._value:
+ self._placeholder.hide()
+ else:
+ if not self.focused:
+ self._placeholder.show()
+
+ # Scroll if the cursor is outside of the clip bounds
+ rel_pos = self.get_relative_pos(self._cursor.get_abs_pos()).x
+ if rel_pos >= max_left:
+ self._text_scroller.left = min(0, max_left - self._cursor.left)
+ if rel_pos <= 0:
+ self._text_scroller.left = min(0, - self._cursor.left - rel_pos)
+
+ def cursor_skip_left(self):
+ """ Moves the cursor to the left, skipping the previous word """
+ left_hand_str = ''.join(reversed(self.value[0:self.cursor_pos]))
+ match = self.re_skip.match(left_hand_str)
+ if match is not None:
+ self.cursor_pos -= match.end() - 1
+ else:
+ self.cursor_pos = 0
+
+ def cursor_skip_right(self):
+ """ Moves the cursor to the right, skipping the next word """
+ right_hand_str = self.value[self.cursor_pos:]
+ match = self.re_skip.match(right_hand_str)
+ if match is not None:
+ self.cursor_pos += match.end() - 1
+ else:
+ self.cursor_pos = len(self.value)
diff --git a/ui/Builtin/LUIInputHandler.py b/ui/Builtin/LUIInputHandler.py
new file mode 100644
index 00000000..3b5e2baa
--- /dev/null
+++ b/ui/Builtin/LUIInputHandler.py
@@ -0,0 +1,8 @@
+"""
+
+This is a wrapper file. It contains no actual implementation
+
+"""
+
+from panda3d.lui import LUIInputHandler as __LUIInputHandler
+LUIInputHandler = __LUIInputHandler
diff --git a/ui/Builtin/LUILabel.py b/ui/Builtin/LUILabel.py
new file mode 100644
index 00000000..0eb25a1b
--- /dev/null
+++ b/ui/Builtin/LUILabel.py
@@ -0,0 +1,77 @@
+
+from panda3d.lui import LUIText
+from LUIObject import LUIObject
+from LUIInitialState import LUIInitialState
+
+__all__ = ["LUILabel"]
+
+class LUILabel(LUIObject):
+
+ """ A simple label, displaying text. """
+
+ # Default variables which can be overridden by skins
+ DEFAULT_COLOR = (0.9, 0.9, 0.9, 1)
+ DEFAULT_USE_SHADOW = True
+
+ def __init__(self, text="Label", shadow=None, font_size=14, font="label", color=None, wordwrap=False, **kwargs):
+ """ Creates a new label. If shadow is True, a small text shadow will be
+ rendered below the actual text. """
+ LUIObject.__init__(self)
+ LUIInitialState.init(self, kwargs)
+ self._text = LUIText(
+ self,
+ text,
+ font,
+ font_size,
+ 0,
+ 0,
+ wordwrap
+ )
+ self._text.z_offset = 1
+ if color is None:
+ self.color = LUILabel.DEFAULT_COLOR
+ else:
+ self.color = color
+ if shadow is None:
+ shadow = LUILabel.DEFAULT_USE_SHADOW
+ self._have_shadow = shadow
+ if self._have_shadow:
+ self._shadow_text = LUIText(
+ self,
+ text,
+ font,
+ font_size,
+ 0,
+ 0,
+ wordwrap
+ )
+ self._shadow_text.top = 1
+ self._shadow_text.color = (0,0,0,0.6)
+
+ def get_text_handle(self):
+ """ Returns a handle to the internal used LUIText object """
+ return self._text
+
+ text_handle = property(get_text_handle)
+
+ def get_text(self):
+ """ Returns the current text of the label """
+ return self._text.text
+
+ def set_text(self, text):
+ """ Sets the text of the label """
+ self._text.text = text
+ if self._have_shadow:
+ self._shadow_text.text = text
+
+ text = property(get_text, set_text)
+
+ def get_color(self):
+ """ Returns the current color of the label's text """
+ return self._text.color
+
+ def set_color(self, color):
+ """ Sets the color of the label's text """
+ self._text.color = color
+
+ color = property(get_color, set_color)
diff --git a/ui/Builtin/LUILayouts.py b/ui/Builtin/LUILayouts.py
new file mode 100644
index 00000000..2210450d
--- /dev/null
+++ b/ui/Builtin/LUILayouts.py
@@ -0,0 +1,105 @@
+
+from __future__ import print_function, division
+
+from LUIObject import LUIObject
+from LUISprite import LUISprite
+from LUIHorizontalLayout import LUIHorizontalLayout
+
+from LUIInitialState import LUIInitialState
+
+__all__ = ["LUICornerLayout", "LUIHorizontalStretchedLayout"]
+
+class LUICornerLayout(LUIObject):
+
+ """ This is a layout which is used to combine 9 sprites to a single sprite,
+ e.g. used for box shadow or frames."""
+
+ # List of all sprite identifiers required for the layout
+ _MODES = ["TR", "Top", "TL", "Right", "Mid", "Left", "BR", "Bottom", "BL"]
+
+ def __init__(self, image_prefix="", **kwargs):
+ """ Creates a new layout, using the image_prefix as prefix. """
+ LUIObject.__init__(self)
+ self.set_size("100%", "100%")
+ self._prefix = image_prefix
+ self._parts = {}
+ for i in self._MODES:
+ self._parts[i] = LUISprite(self, "blank", "skin")
+ self._update_layout()
+ LUIInitialState.init(self, kwargs)
+
+ def _update_layout(self):
+ """ Updates the layouts components. """
+ for i in self._MODES:
+ self._parts[i].set_texture(self._prefix + i, "skin", resize=True)
+
+ # Top and Left
+ self._parts["Top"].width = "100%"
+ self._parts["Top"].margin = (0, self._parts["TR"].width, 0, self._parts["TL"].width)
+
+ self._parts["Left"].height = "100%"
+ self._parts["Left"].margin = (self._parts["TL"].height, 0, self._parts["BL"].height, 0)
+
+ # Mid
+ self._parts["Mid"].set_size("100%", "100%")
+ self._parts["Mid"].margin = (self._parts["Top"].height, self._parts["Right"].width,
+ self._parts["Bottom"].height, self._parts["Left"].width)
+
+ # Bottom and Right
+ self._parts["Bottom"].width = "100%"
+ self._parts["Bottom"].margin = (0, self._parts["BR"].width, 0, self._parts["BL"].width)
+ self._parts["Bottom"].bottom = 0
+
+ self._parts["Right"].height = "100%"
+ self._parts["Right"].margin = (self._parts["TR"].height, 0, self._parts["BR"].width, 0)
+ self._parts["Right"].right = 0
+
+ # Corners
+ self._parts["TL"].top_left = 0, 0
+ self._parts["TR"].top_right = 0, 0
+ self._parts["BL"].bottom_left = 0, 0
+ self._parts["BR"].bottom_right = 0, 0
+
+ def set_prefix(self, prefix):
+ """ Changes the texture of the layout """
+ self._prefix = prefix
+ self._update_layout()
+
+ def get_prefix(self):
+ """ Returns the layouts texture prefix """
+ return self._prefix
+
+ prefix = property(get_prefix, set_prefix)
+
+
+class LUIHorizontalStretchedLayout(LUIObject):
+
+ """ A layout which takes 3 sprites, a left sprite, a right sprite, and a
+ middle sprite. While the left and right sprites remain untouched, the middle
+ one will be stretched to fit the layout """
+
+ def __init__(self, parent=None, prefix="ButtonDefault", **kwargs):
+ LUIObject.__init__(self)
+ self._layout = LUIHorizontalLayout(self, spacing=0)
+ self._layout.width = "100%"
+ self._sprite_left = LUISprite(self._layout.cell(), "blank", "skin")
+ self._sprite_mid = LUISprite(self._layout.cell('*'), "blank", "skin")
+ self._sprite_right = LUISprite(self._layout.cell(), "blank", "skin")
+ if parent is not None:
+ self.parent = parent
+ self.prefix = prefix
+ LUIInitialState.init(self, kwargs)
+
+ def set_prefix(self, prefix):
+ """ Sets the layout prefix, this controls which sprites will be used """
+ self._sprite_left.set_texture(prefix + "_Left", "skin")
+ self._sprite_mid.set_texture(prefix, "skin")
+ self._sprite_right.set_texture(prefix + "_Right", "skin")
+ self._sprite_mid.width = "100%"
+ self._prefix = prefix
+
+ def get_prefix(self):
+ """ Returns the layout prefix """
+ return self._prefix
+
+ prefix = property(get_prefix, set_prefix)
diff --git a/ui/Builtin/LUIObject.py b/ui/Builtin/LUIObject.py
new file mode 100644
index 00000000..c666efe2
--- /dev/null
+++ b/ui/Builtin/LUIObject.py
@@ -0,0 +1,18 @@
+"""
+
+This is a wrapper file. It contains no actual implementation
+
+"""
+
+from panda3d.lui import LUIObject as _LUIObject
+from LUIInitialState import LUIInitialState
+
+__all__ = ["LUIObject"]
+
+class LUIObject(_LUIObject):
+ """ This is a wrapper class for the C++ LUIObject class, to be able to
+ use it in a more convenient way """
+
+ def __init__(self, *args, **kwargs):
+ _LUIObject.__init__(self, *args)
+ LUIInitialState.init(self, kwargs)
diff --git a/ui/Builtin/LUIProgressbar.py b/ui/Builtin/LUIProgressbar.py
new file mode 100644
index 00000000..8f744eaf
--- /dev/null
+++ b/ui/Builtin/LUIProgressbar.py
@@ -0,0 +1,72 @@
+
+from LUIObject import LUIObject
+from LUISprite import LUISprite
+
+from LUILayouts import LUIHorizontalStretchedLayout
+from LUILabel import LUILabel
+
+class LUIProgressbar(LUIObject):
+
+ """ A simple progress bar """
+
+ def __init__(self, parent=None, width=200, value=50, show_label=True):
+ """ Constructs a new progress bar. If show_label is True, a label indicating
+ the current progress is shown """
+ LUIObject.__init__(self)
+ self.set_width(width)
+
+ self._bg_layout = LUIHorizontalStretchedLayout(
+ parent=self, prefix="ProgressbarBg", width="100%")
+
+ self._fg_left = LUISprite(self, "ProgressbarFg_Left", "skin")
+ self._fg_mid = LUISprite(self, "ProgressbarFg", "skin")
+ self._fg_right = LUISprite(self, "ProgressbarFg_Right", "skin")
+ self._fg_finish = LUISprite(self, "ProgressbarFg_Finish", "skin")
+
+ self._show_label = show_label
+ self._progress_pixel = 0
+ self._fg_finish.right = 0
+
+ if self._show_label:
+ self._progress_label = LUILabel(parent=self, text=u"33 %")
+ self._progress_label.centered = (True, True)
+
+ self.set_value(value)
+ self._update_progress()
+
+ if parent is not None:
+ self.parent = parent
+
+ def get_value(self):
+ """ Returns the current value of the progress bar """
+ return (self._progress_pixel / self.width * 100.0)
+
+ def set_value(self, val):
+ """ Sets the value of the progress bar """
+ val = max(0, min(100, val))
+ self._progress_pixel = int(val / 100.0 * self.width)
+ self._update_progress()
+
+ value = property(get_value, set_value)
+
+ def _update_progress(self):
+ """ Internal method to update the progressbar """
+ self._fg_finish.hide()
+
+ if self._progress_pixel <= self._fg_left.width + self._fg_right.width:
+ self._fg_mid.hide()
+ self._fg_right.left = self._fg_left.width
+ else:
+ self._fg_mid.show()
+ self._fg_mid.left = self._fg_left.width
+ self._fg_mid.width = self._progress_pixel - self._fg_right.width - self._fg_left.width
+ self._fg_right.left = self._fg_mid.left + self._fg_mid.width
+
+ if self._progress_pixel >= self.width - self._fg_right.width:
+ self._fg_finish.show()
+ self._fg_finish.right = - (self.width - self._progress_pixel)
+ self._fg_finish.clip_bounds = (0, self.width - self._progress_pixel, 0, 0)
+
+ if self._show_label:
+ percentage = self._progress_pixel / self.width * 100.0
+ self._progress_label.set_text("{} %".format(int(percentage)))
diff --git a/ui/Builtin/LUIRadiobox.py b/ui/Builtin/LUIRadiobox.py
new file mode 100644
index 00000000..9d7d8bf2
--- /dev/null
+++ b/ui/Builtin/LUIRadiobox.py
@@ -0,0 +1,91 @@
+
+from __future__ import division
+
+from LUIObject import LUIObject
+from LUISprite import LUISprite
+from LUIInitialState import LUIInitialState
+from LUILabel import LUILabel
+
+class LUIRadiobox(LUIObject):
+
+ """ A radiobox which can be used in combination with a LUIRadioboxGroup """
+
+ def __init__(self, parent=None, group=None, value=None, active=False, label=u"Radiobox", **kwargs):
+ """ Constructs a new radiobox. group should be a handle to a LUIRadioboxGroup.
+ value will be the value returned by group.value, in case the box was
+ selected. By default, the radiobox is not active. """
+ assert group is not None, "LUIRadiobox needs a LUIRadioboxGroup!"
+ LUIObject.__init__(self, x=0, y=0, solid=True)
+ self._sprite = LUISprite(self, "Radiobox_Default", "skin")
+ self._label = LUILabel(parent=self, text=label, margin=(0, 0, 0, 23),
+ center_vertical=True)
+ self._value = value
+ self._active = False
+ self._hovered = False
+ self._group = group
+ self._group.register_box(self)
+ if active:
+ self.set_active()
+ if parent:
+ self.parent = parent
+ LUIInitialState.init(self, kwargs)
+
+ def on_click(self, event):
+ """ Internal onclick handler. Do not override. """
+ self.set_active()
+
+ def on_mouseover(self, event):
+ """ Internal mouseover handler """
+ self._hovered = True
+ self._update_sprite()
+
+ def on_mouseout(self, event):
+ """ Internal mouseout handler """
+ self._hovered = False
+ self._update_sprite()
+
+ def set_active(self):
+ """ Internal function to set the radiobox active """
+ if self._group is not None:
+ self._group.set_active_box(self)
+ else:
+ self._update_state(True)
+
+ def get_value(self):
+ """ Returns the value of the radiobox """
+ return self._value
+
+ def set_value(self, value):
+ """ Sets the value of the radiobox """
+ self._value = value
+
+ value = property(get_value, set_value)
+
+ def get_label(self):
+ """ Returns a handle to the label, so it can be modified (e.g. change
+ its text) """
+ return self._label
+
+ label = property(get_label)
+
+ def _update_state(self, active):
+ """ Internal method to update the state of the radiobox. Called by the
+ LUIRadioboxGroup """
+ self._active = active
+ self.trigger_event("changed")
+ self._update_sprite()
+
+ def on_mousedown(self, event):
+ """ Internal onmousedown handler. Do not override. """
+ self._sprite.color = (0.86,0.86,0.86,1.0)
+
+ def on_mouseup(self, event):
+ """ Internal onmouseup handler. Do not override. """
+ self._sprite.color = (1,1,1,1)
+
+ def _update_sprite(self):
+ """ Internal function to update the sprite of the radiobox """
+ img = "Radiobox_Active" if self._active else "Radiobox_Default"
+ if self._hovered:
+ img += "Hover"
+ self._sprite.set_texture(img, "skin")
diff --git a/ui/Builtin/LUIRadioboxGroup.py b/ui/Builtin/LUIRadioboxGroup.py
new file mode 100644
index 00000000..94117c79
--- /dev/null
+++ b/ui/Builtin/LUIRadioboxGroup.py
@@ -0,0 +1,41 @@
+
+from LUIObject import LUIObject
+
+class LUIRadioboxGroup(LUIObject):
+
+ """ Simple helper to group a bunch of LUIRadiobox and ensure only one is
+ checked at one timem """
+
+ def __init__(self):
+ """ Constructs a new group without any radioboxes inside """
+ self._boxes = []
+ self._selected_box = None
+
+ def register_box(self, box):
+ """ Registers a box to the collection """
+ if box not in self._boxes:
+ self._boxes.append(box)
+
+ def set_active_box(self, active_box):
+ """ Internal function to set the active box """
+ for box in self._boxes:
+ if box is not active_box:
+ box._update_state(False)
+ else:
+ box._update_state(True)
+ self._selected_box = active_box
+
+ def get_active_box(self):
+ """ Returns the current selected box """
+ return self._selected_box
+
+ active_box = property(get_active_box, set_active_box)
+
+ def get_active_value(self):
+ """ Returns the value of the current selected box (or None if none is
+ selected) """
+ if self._selected_box is None:
+ return None
+ return self._selected_box.get_value()
+
+ active_value = property(get_active_value)
diff --git a/ui/Builtin/LUIRegion.py b/ui/Builtin/LUIRegion.py
new file mode 100644
index 00000000..b6bbb3d4
--- /dev/null
+++ b/ui/Builtin/LUIRegion.py
@@ -0,0 +1,8 @@
+"""
+
+This is a wrapper file. It contains no actual implementation
+
+"""
+
+from panda3d.lui import LUIRegion as __LUIRegion
+LUIRegion = __LUIRegion
diff --git a/ui/Builtin/LUIRoot.py b/ui/Builtin/LUIRoot.py
new file mode 100644
index 00000000..79f83f96
--- /dev/null
+++ b/ui/Builtin/LUIRoot.py
@@ -0,0 +1,8 @@
+"""
+
+This is a wrapper file. It contains no actual implementation
+
+"""
+
+from panda3d.lui import LUIRoot as __LUIRoot
+LUIRoot = __LUIRoot
diff --git a/ui/Builtin/LUIScrollableRegion.py b/ui/Builtin/LUIScrollableRegion.py
new file mode 100644
index 00000000..08105cae
--- /dev/null
+++ b/ui/Builtin/LUIScrollableRegion.py
@@ -0,0 +1,155 @@
+
+from LUIObject import LUIObject
+from LUISprite import LUISprite
+from LUIInitialState import LUIInitialState
+from LUILayouts import LUIHorizontalStretchedLayout
+
+class LUIScrollableRegion(LUIObject):
+
+ """ Scrollable region, reparent elements to the .content_node to make them
+ scroll. """
+
+ def __init__(self, parent=None, width=100, height=100, padding=10, **kwargs):
+ LUIObject.__init__(self)
+ self.set_size(width, height)
+ self._content_parent = LUIObject(self)
+ self._content_parent.set_size("100%", "100%")
+ self._content_parent.clip_bounds = (0,0,0,0)
+
+ self._content_clip = LUIObject(self._content_parent, x=padding, y=padding)
+ self._content_clip.set_size("100%", "100%")
+
+ self._content_scroller = LUIObject(self._content_clip)
+ self._content_scroller.width = "100%"
+
+
+ self._scrollbar = LUIObject(self, x=0, y=0, w=20)
+ self._scrollbar.height = "100%"
+ self._scrollbar.right = -10
+
+ self._scrollbar_bg = LUISprite(self._scrollbar, "blank", "skin")
+ self._scrollbar_bg.color = (1,1,1,0.05)
+ self._scrollbar_bg.set_size(3, "100%")
+ self._scrollbar_bg.center_horizontal = True
+
+ # Handle
+ self._scrollbar_handle = LUIObject(self._scrollbar, x=5, y=0, w=10)
+ self._scroll_handle_top = LUISprite(self._scrollbar_handle, "ScrollbarHandle_Top", "skin")
+ self._scroll_handle_mid = LUISprite(self._scrollbar_handle, "ScrollbarHandle", "skin")
+ self._scroll_handle_bottom = LUISprite(self._scrollbar_handle, "ScrollbarHandle_Bottom", "skin")
+
+ self._scrollbar_handle.solid = True
+ self._scrollbar.solid = True
+
+ self._scrollbar_handle.bind("mousedown", self._start_scrolling)
+ self._scrollbar_handle.bind("mouseup", self._stop_scrolling)
+ self._scrollbar.bind("mousedown", self._on_bar_click)
+ self._scrollbar.bind("mouseup", self._stop_scrolling)
+
+ self._handle_dragging = False
+ self._drag_start_y = 0
+
+ self._scroll_top_position = 0
+ self._content_height = 400
+
+ # Scroll shadow
+ self._scroll_shadow_top = LUIHorizontalStretchedLayout(parent=self, prefix="ScrollShadowTop", width="100%")
+ self._scroll_shadow_bottom = LUIHorizontalStretchedLayout(parent=self, prefix="ScrollShadowBottom", width="100%")
+ self._scroll_shadow_bottom.bottom = 0
+
+ self._handle_height = 100
+
+ if parent is not None:
+ self.parent = parent
+
+ LUIInitialState.init(self, kwargs)
+ self.content_node = self._content_scroller
+ taskMgr.doMethodLater(0.05, lambda task: self._update(), "update_scrollbar")
+
+ def _on_bar_click(self, event):
+ """ Internal handler when the user clicks on the scroll bar """
+ self._scroll_to_bar_pixels(event.coordinates.y - self._scrollbar.abs_pos.y - self._handle_height / 2.0)
+ self._update()
+ self._start_scrolling(event)
+
+ def _start_scrolling(self, event):
+ """ Internal method when we start scrolling """
+ self.request_focus()
+ if not self._handle_dragging:
+ self._drag_start_y = event.coordinates.y
+ self._handle_dragging = True
+
+ def _stop_scrolling(self, event):
+ """ Internal handler when we should stop scrolling """
+ if self._handle_dragging:
+ self._handle_dragging = False
+ self.blur()
+
+ def _scroll_to_bar_pixels(self, pixels):
+ """ Internal method to convert from pixels to a relative position """
+ offset = pixels * self._content_height / self.height
+ self._scroll_top_position = offset
+ self._scroll_top_position = max(0, min(self._content_height - self._content_clip.height, self._scroll_top_position))
+
+ def on_tick(self, event):
+ """ Internal on tick handler """
+ if self._handle_dragging:
+ scroll_abs_pos = self._scrollbar.abs_pos
+ clamped_coord_y = max(scroll_abs_pos.y, min(scroll_abs_pos.y + self.height, event.coordinates.y))
+ offset = clamped_coord_y - self._drag_start_y
+ self._drag_start_y = clamped_coord_y
+ self._scroll_to_bar_pixels(self._scroll_top_position/self._content_height*self.height + offset)
+ self._update()
+
+ def _set_handle_height(self, height):
+ """ Internal method to set the scrollbar height """
+ self._scroll_handle_mid.top = float(self._scroll_handle_top.height)
+
+ self._scroll_handle_mid.height = max(0.0, height - self._scroll_handle_top.height - self._scroll_handle_bottom.height)
+ self._scroll_handle_bottom.top = self._scroll_handle_mid.height + self._scroll_handle_mid.top
+ self._handle_height = height
+
+ def _update(self):
+ """ Internal method to update the scroll bar """
+ self._content_height = max(1, self._content_scroller.get_height() + 20)
+ self._content_scroller.top = -self._scroll_top_position
+ scrollbar_height = max(0.1, min(1.0, self._content_clip.height / self._content_height))
+ scrollbar_height_px = scrollbar_height * self.height
+
+ self._set_handle_height(scrollbar_height_px)
+ self._scrollbar_handle.top = self._scroll_top_position / self._content_height * self.height
+
+ top_alpha = max(0.0, min(1.0, self._scroll_top_position / 50.0))
+ bottom_alpha = max(0.0, min(1.0, (self._content_height - self._scroll_top_position - self._content_clip.height) / 50.0 ))
+ self._scroll_shadow_top.color = (1,1,1,top_alpha)
+ self._scroll_shadow_bottom.color = (1,1,1,bottom_alpha)
+
+ if self._content_height <= self.height:
+ self._scrollbar_handle.hide()
+ else:
+ self._scrollbar_handle.show()
+
+ def on_element_added(self):
+ taskMgr.doMethodLater(0.05, lambda task: self._update(), "update_layout")
+
+ def get_scroll_percentage(self):
+ """ Returns the current scroll height in percentage from 0 to 1 """
+ return self._scroll_top_position / max(1, self._content_height - self._content_clip.height)
+
+ def set_scroll_percentage(self, percentage):
+ """ Sets the scroll position in percentage, 0 means top and 1 means bottom """
+ percentage = max(0.0, min(1.0, percentage))
+ pixels = max(0.0, self._content_height - self._content_clip.height) * percentage
+ self._scroll_top_position = pixels
+ self._update()
+
+ scroll_percentage = property(get_scroll_percentage, set_scroll_percentage)
+
+ def scroll_to_bottom(self):
+ """ Scrolls to the bottom of the frame """
+ taskMgr.doMethodLater(0.07, lambda task: self.set_scroll_percentage(1.0), "scroll_to_bottom")
+
+ def scroll_to_top(self):
+ """ Scrolls to the top of the frame """
+ taskMgr.doMethodLater(0.07, lambda task: self.set_scroll_percentage(0.0), "scroll_to_top")
+
diff --git a/ui/Builtin/LUISelectbox.py b/ui/Builtin/LUISelectbox.py
new file mode 100644
index 00000000..f2fa4a77
--- /dev/null
+++ b/ui/Builtin/LUISelectbox.py
@@ -0,0 +1,207 @@
+
+from LUIObject import LUIObject
+from LUISprite import LUISprite
+from LUILabel import LUILabel
+from LUILayouts import LUICornerLayout, LUIHorizontalStretchedLayout
+from LUIInitialState import LUIInitialState
+
+from functools import partial
+
+__all__ = ["LUISelectbox"]
+
+class LUISelectbox(LUIObject):
+
+ """ Selectbox widget, showing several options whereas the user can select
+ only one. """
+
+ def __init__(self, width=200, options=None, selected_option=None, **kwargs):
+ """ Constructs a new selectbox with a given width """
+ LUIObject.__init__(self, x=0, y=0, w=width+4, solid=True)
+ LUIInitialState.init(self, kwargs)
+
+ # The selectbox has a small border, to correct this we move it
+ self.margin.left = -2
+
+ self._bg_layout = LUIHorizontalStretchedLayout(parent=self, prefix="Selectbox", width="100%")
+
+ self._label_container = LUIObject(self, x=10, y=0)
+ self._label_container.set_size("100%", "100%")
+ self._label_container.clip_bounds = (0,0,0,0)
+ self._label = LUILabel(parent=self._label_container, text=u"Select an option ..")
+ self._label.center_vertical = True
+
+ self._drop_menu = LUISelectdrop(parent=self, width=width)
+ self._drop_menu.top = self._bg_layout._sprite_right.height - 7
+ self._drop_menu.topmost = True
+
+ self._drop_open = False
+ self._drop_menu.hide()
+
+ self._options = []
+ self._current_option_id = None
+
+ if options is not None:
+ self._options = options
+
+ self._select_option(selected_option)
+
+ def get_selected_option(self):
+ """ Returns the selected option """
+ return self._current_option_id
+
+ def set_selected_option(self, option_id):
+ """ Sets the selected option """
+ raise NotImplementedError()
+
+ selected_option = property(get_selected_option, set_selected_option)
+
+ def _render_options(self):
+ """ Internal method to render all available options """
+ self._drop_menu._render_options(self._options)
+
+ def get_options(self):
+ """ Returns the list of options """
+ return self._options
+
+ def set_options(self, options):
+ """ Sets the list of options, options should be a list containing entries
+ whereas each entry is a tuple in the format (option_id, option_label).
+ The option ID can be an arbitrary object, and will not get modified. """
+ self._options = options
+ self._current_option_id = None
+ self._render_options()
+
+ options = property(get_options, set_options)
+
+ def _select_option(self, opt_id):
+ """ Internal method to select an option """
+ self._label.alpha = 1.0
+ for elem_opt_id, opt_val in self._options:
+ if opt_id == elem_opt_id:
+ self._label.text = opt_val
+ self._current_option_id = opt_id
+ return
+ self._label.alpha = 0.3
+
+ # def on_mouseover(self, event):
+ # """ Internal handle when the select-knob was hovered """
+ # self._bg_layout.color = (0.9,0.9,0.9,1.0)
+
+ # def on_mouseout(self, event):
+ # """ Internal handle when the select-knob was no longer hovered """
+ # self._bg_layout.color = (1,1,1,1.0)
+
+ def on_click(self, event):
+ """ On-Click handler """
+ self.request_focus()
+ if self._drop_open:
+ self._close_drop()
+ else:
+ self._open_drop()
+
+ def on_mousedown(self, event):
+ """ Mousedown handler """
+ self._bg_layout.alpha = 0.9
+
+ def on_mouseup(self, event):
+ """ Mouseup handler """
+ self._bg_layout.alpha = 1
+
+ def on_blur(self, event):
+ """ Internal handler when the selectbox lost focus """
+ if not self._drop_menu.focused:
+ self._close_drop()
+
+ def _open_drop(self):
+ """ Internal method to show the dropdown menu """
+ if not self._drop_open:
+ self._render_options()
+ self._drop_menu.show()
+ self.request_focus()
+ self._drop_open = True
+
+ def _close_drop(self):
+ """ Internal method to close the dropdown menu """
+ if self._drop_open:
+ self._drop_menu.hide()
+ self._drop_open = False
+
+ def _on_option_selected(self, opt_id):
+ """ Internal method when an option got selected """
+ self._select_option(opt_id)
+ self._close_drop()
+
+
+class LUISelectdrop(LUIObject):
+
+ """ Internal class used by the selectbox, representing the dropdown menu """
+
+ def __init__(self, parent, width=200):
+ LUIObject.__init__(self, x=0, y=0, w=width, h=1, solid=True)
+
+ self._layout = LUICornerLayout(parent=self, image_prefix="Selectdrop_",
+ width=width + 10, height=100)
+ self._layout.margin.left = -3
+
+ self._opener = LUISprite(self, "SelectboxOpen_Right", "skin")
+ self._opener.right = -4
+ self._opener.top = -25
+ self._opener.z_offset = 3
+
+ self._container = LUIObject(self._layout, 0, 0, 0, 0)
+ self._container.width = self.width
+ self._container.clip_bounds = (0,0,0,0)
+ self._container.left = 5
+ self._container.solid = True
+ self._container.bind("mousedown", lambda *args: self.request_focus())
+
+ self._selectbox = parent
+ self._option_focus = False
+ self.parent = self._selectbox
+
+ def _on_opt_over(self, event):
+ """ Inernal handler when an option got hovered """
+ event.sender.color = (0,0,0,0.1)
+
+ def _on_opt_out(self, event):
+ """ Inernal handler when an option got no longer hovered """
+ event.sender.color = (0,0,0,0)
+
+ def _on_opt_click(self, opt_id, event):
+ """ Internal handler when an option got clicked """
+ self._selectbox._on_option_selected(opt_id)
+
+ def _render_options(self, options):
+ """ Internal method to update the options """
+ num_visible_options = min(30, len(options))
+ offset_top = 6
+ self._layout.height = num_visible_options * 30 + offset_top + 11
+ self._container.height = num_visible_options * 30 + offset_top + 1
+ self._container.remove_all_children()
+
+ current_y = offset_top
+ for opt_id, opt_val in options:
+ opt_container = LUIObject(self._container, x=0, y=current_y, w=self._container.width - 30, h=30)
+
+ opt_bg = LUISprite(opt_container, "blank", "skin")
+ opt_bg.width = self._container.width
+ opt_bg.height = opt_container.height
+ opt_bg.color = (0,0,0,0)
+ opt_bg.bind("mouseover", self._on_opt_over)
+ opt_bg.bind("mouseout", self._on_opt_out)
+ opt_bg.bind("mousedown", lambda *args: self.request_focus())
+ opt_bg.bind("click", partial(self._on_opt_click, opt_id))
+ opt_bg.solid = True
+
+ opt_label = LUILabel(parent=opt_container, text=opt_val)
+ opt_label.top = 8
+ opt_label.left = 8
+
+ if opt_id == self._selectbox.selected_option:
+ opt_label.color = (0.6, 0.9, 0.4, 1.0)
+
+ divider = LUISprite(opt_container, "SelectdropDivider", "skin")
+ divider.top = 30 - divider.height / 2
+ divider.width = self._container.width
+
+ current_y += 30
diff --git a/ui/Builtin/LUISkin.py b/ui/Builtin/LUISkin.py
new file mode 100644
index 00000000..0f0e774b
--- /dev/null
+++ b/ui/Builtin/LUISkin.py
@@ -0,0 +1,53 @@
+
+import os
+from os.path import join
+
+from panda3d.core import Filename
+from panda3d.lui import LUIFontPool, LUIAtlasPool
+
+class LUISkin:
+
+ """ Abstract class, each skin derives from this class """
+
+ skin_location = ""
+
+ def __init__(self):
+ pass
+
+ def load(self):
+ """ Skins should override this. Each skin should at least provide the fonts
+ 'default' and 'label', and at least one atlas named 'skin' """
+ raise NotImplementedError()
+
+ def get_resource(self, pth):
+ """ Turns a relative path into an absolute one, using the skin_location """
+ return Filename.from_os_specific(join(self.skin_location, pth)).get_fullpath()
+
+
+class LUIDefaultSkin(LUISkin):
+
+ """ The default skin which comes with LUI """
+
+ skin_location = os.path.join(os.path.dirname(os.path.abspath(__file__)), "../Skins/Default/")
+
+ def __init__(self):
+ pass
+
+ def load(self):
+ LUIFontPool.get_global_ptr().register_font(
+ "default", loader.loadFont(self.get_resource("font/SourceSansPro-Semibold.ttf")))
+
+ labelFont = loader.loadFont(self.get_resource("font/SourceSansPro-Semibold.ttf"))
+ labelFont.setPixelsPerUnit(32)
+
+ LUIFontPool.get_global_ptr().register_font(
+ "label", labelFont)
+
+ headerFont = loader.loadFont(self.get_resource("font/SourceSansPro-Light.ttf"))
+ headerFont.setPixelsPerUnit(80)
+
+ LUIFontPool.get_global_ptr().register_font("header", headerFont)
+
+ LUIAtlasPool.get_global_ptr().load_atlas("skin",
+ self.get_resource("res/atlas.txt"),
+ self.get_resource("res/atlas.png"))
diff --git a/ui/Builtin/LUISlider.py b/ui/Builtin/LUISlider.py
new file mode 100644
index 00000000..ca534362
--- /dev/null
+++ b/ui/Builtin/LUISlider.py
@@ -0,0 +1,214 @@
+
+from LUIObject import LUIObject
+from LUISprite import LUISprite
+from LUIInitialState import LUIInitialState
+from LUILayouts import LUIHorizontalStretchedLayout
+
+class LUISlider(LUIObject):
+
+ """ Slider which can be used to control values """
+ KNOB_Y_OFFSET = -5.0
+
+ def __init__(self, parent=None, filled=True, min_value=0.0, max_value=1.0, width=100.0, value=None, **kwargs):
+ """ Constructs a new slider. If filled is True, the part behind the knob
+ will be solid """
+ LUIObject.__init__(self, x=0, y=0, solid=True)
+ self.set_width(width)
+ self._knob = LUISprite(self, "SliderKnob", "skin")
+ self._knob.z_offset = 2
+ self._knob.solid = True
+
+ # Construct the background
+ self._slider_bg = LUIHorizontalStretchedLayout(parent=self, prefix="SliderBg", center_vertical=True, width="100%", margin=(-1, 0, 0, 0))
+
+ self._filled = filled
+ self._min_value = min_value
+ self._max_value = max_value
+
+ self._side_margin = self._knob.width / 4
+ self._effective_width = self.width - 2 * self._side_margin
+
+ if self._filled:
+ self._slider_fill = LUIObject(self)
+ self._fill_left = LUISprite(self._slider_fill, "SliderBgFill_Left", "skin")
+ self._fill_mid = LUISprite(self._slider_fill, "SliderBgFill", "skin")
+ self._fill_mid.left = self._fill_left.width
+ self._slider_fill.z_offset = 1
+ self._slider_fill.center_vertical = True
+ self._slider_fill.width = "100%"
+ self._slider_fill.height = "100%"
+
+ if parent is not None:
+ self.parent = parent
+
+ # Handle various events
+ self._knob.bind("mousedown", self._start_drag)
+ self._knob.bind("mousemove", self._update_drag)
+ self._knob.bind("mouseup", self._stop_drag)
+ self._knob.bind("keydown", self._on_keydown)
+ self._knob.bind("blur", self._stop_drag)
+ self._knob.bind("keyrepeat", self._on_keydown)
+
+ self._drag_start_pos = None
+ self._dragging = False
+ self._drag_start_val = 0
+ self.current_val = 10
+
+ # Set initial value
+ if value is None:
+ self.set_value( (self._min_value + self._max_value) / 2.0 )
+ else:
+ self.set_value(value)
+
+ self._update_knob()
+
+ LUIInitialState.init(self, kwargs)
+ self._refresh_layout()
+
+ def on_click(self, event):
+ """ Internal on click handler """
+ # I don't like this behaviour
+ # relative_pos = self.get_relative_pos(event.coordinates)
+ # if not self._dragging:
+ # self._set_current_val(relative_pos.x)
+
+ def _update_knob(self):
+ """ Internal method to update the slider knob """
+ self._knob.left = self.current_val - (self._knob.width / 2) + self._side_margin
+ if self._filled:
+ knob_center = self._knob.left + (self._knob.width / 2)
+ fill_len = max(0, knob_center - self._fill_left.width)
+ self._fill_mid.width = fill_len
+ if hasattr(self._slider_fill, 'left'):
+ self._slider_fill.left = 0
+
+ def _refresh_layout(self):
+ """Recalculate slider layout after size changes."""
+ # Guard for early init before internal parts are created
+ if not hasattr(self, "_knob"):
+ return
+ try:
+ # Use half knob width so knob center aligns with bar ends
+ self._side_margin = self._knob.width / 2
+ except Exception:
+ self._side_margin = 0
+ try:
+ self._effective_width = max(1, self.width - (self._knob.width if hasattr(self, "_knob") else 0))
+ except Exception:
+ self._effective_width = 1
+
+ if hasattr(self, "_slider_bg"):
+ if hasattr(self._slider_bg, 'width'):
+ self._slider_bg.width = "100%"
+ if hasattr(self._slider_bg, 'height'):
+ self._slider_bg.height = "100%"
+ if hasattr(self._slider_bg, 'center_vertical'):
+ self._slider_bg.center_vertical = True
+ if hasattr(self._knob, 'center_vertical'):
+ self._knob.center_vertical = True
+ # Explicit vertical centering for knob to avoid mismatch after resize
+ try:
+ base_h = self.height
+ base_top = 0
+ if hasattr(self, "_slider_bg") and self._slider_bg is not None:
+ bg_h = getattr(self._slider_bg, "height", None)
+ if bg_h is not None and bg_h != "100%":
+ base_h = bg_h
+ base_top = getattr(self._slider_bg, "top", 0) or 0
+ if hasattr(self._knob, "height"):
+ # Visual offset compensates for the skin's knob pivot
+ self._knob.top = (base_h - self._knob.height) / 2.0 + self.KNOB_Y_OFFSET
+ except Exception:
+ pass
+ if self._filled and hasattr(self, '_slider_fill'):
+ self._slider_fill.center_vertical = True
+ try:
+ base_h = self.height
+ base_top = 0
+ if hasattr(self, "_slider_bg") and self._slider_bg is not None:
+ bg_h = getattr(self._slider_bg, "height", None)
+ if bg_h is not None and bg_h != "100%":
+ base_h = bg_h
+ base_top = getattr(self._slider_bg, "top", 0) or 0
+ if hasattr(self._slider_fill, 'height'):
+ self._slider_fill.top = base_top + (base_h - self._slider_fill.height) / 2.0 + self.KNOB_Y_OFFSET
+ except Exception:
+ pass
+ if hasattr(self._slider_fill, 'width'):
+ self._slider_fill.width = "100%"
+ if hasattr(self._slider_fill, 'height'):
+ self._slider_fill.height = "100%"
+
+ try:
+ self.current_val = max(0, min(self.current_val, self._effective_width))
+ except Exception:
+ pass
+ self._update_knob()
+
+ def set_width(self, width):
+ """Set slider width and refresh layout."""
+ self.width = width
+ if hasattr(self, "_slider_bg"):
+ self._refresh_layout()
+
+ def set_height(self, height):
+ """Set slider height and refresh layout."""
+ self.height = height
+ if hasattr(self, "_slider_bg"):
+ self._refresh_layout()
+
+ def _set_current_val(self, pixels):
+ """ Internal method to set the current value in pixels """
+ pixels = max(0, min(self._effective_width, pixels))
+ self.current_val = pixels
+ self.trigger_event("changed")
+ self._update_knob()
+
+ def _start_drag(self, event):
+ """ Internal drag start handler """
+ self._knob.request_focus()
+ if not self._dragging:
+ self._drag_start_pos = event.coordinates
+ self._dragging = True
+ self._drag_start_val = self.current_val
+ self._knob.color = (0.8,0.8,0.8,1.0)
+
+ def set_value(self, value):
+ """ Sets the value of the slider, should be between minimum and maximum. """
+ scaled = (float(value) - float(self._min_value)) \
+ / (float(self._max_value) - float(self._min_value)) \
+ * self._effective_width
+ self._set_current_val(scaled)
+
+ def get_value(self):
+ """ Returns the current value of the slider """
+ return (self.current_val / float(self._effective_width)) \
+ * (float(self._max_value) - float(self._min_value)) \
+ + self._min_value
+
+ value = property(get_value, set_value)
+
+ def _on_keydown(self, event):
+ """ Internal keydown handler """
+ if event.message == "arrow_right":
+ self._set_current_val(self.current_val + 2)
+ elif event.message == "arrow_left":
+ self._set_current_val(self.current_val - 2)
+ elif event.message == "escape":
+ self.current_val = self._drag_start_val
+ self._stop_drag(event)
+ self._update_knob()
+
+ def _update_drag(self, event):
+ """ Internal drag handler """
+ if self._dragging:
+ dragOffset = event.coordinates.x - self._drag_start_pos.x
+ finalValue = self._drag_start_val + dragOffset
+ self._set_current_val(finalValue)
+
+ def _stop_drag(self, event):
+ """ Internal drag stop handelr """
+ self._drag_start_pos = None
+ self._dragging = False
+ self._drag_start_val = self.current_val
+ self._knob.color = (1,1,1,1)
diff --git a/ui/Builtin/LUISprite.py b/ui/Builtin/LUISprite.py
new file mode 100644
index 00000000..b04b4229
--- /dev/null
+++ b/ui/Builtin/LUISprite.py
@@ -0,0 +1,18 @@
+"""
+
+This is a wrapper file. It contains no actual implementation
+
+"""
+
+from panda3d.lui import LUISprite as _LUISprite
+from LUIInitialState import LUIInitialState
+
+__all__ = ["LUISprite"]
+
+class LUISprite(_LUISprite):
+ """ This is a wrapper class for the C++ LUISprite class, to be able to
+ use it in a more convenient way """
+
+ def __init__(self, *args, **kwargs):
+ _LUISprite.__init__(self, *args)
+ LUIInitialState.init(self, kwargs)
diff --git a/ui/Builtin/LUISpriteButton.py b/ui/Builtin/LUISpriteButton.py
new file mode 100644
index 00000000..348a197a
--- /dev/null
+++ b/ui/Builtin/LUISpriteButton.py
@@ -0,0 +1,30 @@
+
+from LUIObject import LUIObject
+from LUISprite import LUISprite
+from LUIInitialState import LUIInitialState
+
+class LUISpriteButton(LUIObject):
+
+ """ Simple button that uses only two images: Default and focus. """
+
+ def __init__(self, template="ButtonDefault", **kwargs):
+ LUIObject.__init__(self, x=0, y=0, solid=True)
+ self._template = template
+ self._button_sprite = LUISprite(self, template, "skin")
+ if 'width' in kwargs:
+ self._button_sprite.width = kwargs['width']
+ if 'height' in kwargs:
+ self._button_sprite.height = kwargs['height']
+ LUIInitialState.init(self, kwargs)
+
+ def on_mousedown(self, event):
+ """ Internal on_mousedown handler. Do not override """
+ self._button_sprite.set_texture(self.template + "Focus", "skin", resize=False)
+
+ def on_mouseup(self, event):
+ """ Internal on_mouseup handler. Do not override """
+ self._button_sprite.set_texture(self.template, "skin", resize=False)
+
+ def on_click(self, event):
+ """ Internal onclick handler. Do not override """
+ self.trigger_event("changed")
diff --git a/ui/Builtin/LUITabbedFrame.py b/ui/Builtin/LUITabbedFrame.py
new file mode 100644
index 00000000..4e210f58
--- /dev/null
+++ b/ui/Builtin/LUITabbedFrame.py
@@ -0,0 +1,87 @@
+from LUIFrame import LUIFrame
+from LUILabel import LUILabel
+from LUIObject import LUIObject
+from LUIVerticalLayout import LUIVerticalLayout
+from LUIHorizontalLayout import LUIHorizontalLayout
+
+class LUITabbedFrame(LUIFrame):
+ def __init__(self, **kwargs):
+ super(LUITabbedFrame, self).__init__(**kwargs)
+
+ # The main window layout
+ bar_spacing = kwargs.get('bar_spacing', 3)
+ self.root_layout = LUIVerticalLayout(parent = self,
+ spacing = bar_spacing)
+ self.root_layout.height = "100%"
+ self.root_layout.width = "100%"
+ self.root_layout.margin = 0
+
+ # The header bar
+ header_spacing = kwargs.get('header_spacing', 3)
+ self.header_bar = LUIHorizontalLayout(parent = self.root_layout.cell("?"),
+ spacing = header_spacing)
+ self.root_layout.add(self.header_bar, "?")
+ self.header_to_frame = {}
+ self.current_frame = None
+
+ # The main window contents
+ self.main_frame = LUIObject()
+ self.main_frame.height = "100%"
+ self.main_frame.width = "100%"
+ self.main_frame.margin = 0
+ # self.main_frame.padding = 0
+ self.root_layout.add(self.main_frame, "*")
+ self.bind("expose", self.on_expose)
+ self.bind("unexpose", self.on_unexpose)
+
+ def add(self, header, frame):
+ # header
+ if isinstance(header, str):
+ header = LUILabel(text = header)
+ self.header_bar.add(header, "?")
+ self.header_to_frame[header] = frame
+ header.solid = True
+ header.bind("click", self._change_to_tab)
+ # Frame
+ frame.parent = self.main_frame
+ frame.width = "100%"
+ frame.height = "100%"
+ # Put frame in front
+ if self.current_frame is None:
+ self.current_frame = frame
+ self.current_frame.show()
+ else:
+ frame.hide()
+ return header
+
+ def _find_header_index(self, header):
+ for idx, child in enumerate(self.header_bar.children):
+ if any([grandchild == header for grandchild in child.children]):
+ break
+ else:
+ raise ValueError("Given object is not a header bar item.")
+ return idx
+
+ def remove(self, header):
+ idx = self._find_header_index(header)
+ self.header_bar.remove_cell(idx)
+ frame = self.header_to_frame[header]
+ frame.parent = None
+ del self.header_to_frame[header]
+ if self.current_frame == frame:
+ self.current_frame = None
+
+ def _change_to_tab(self, lui_event):
+ header = lui_event.sender
+ if self.current_frame is not None:
+ self.current_frame.trigger_event("unexpose")
+ self.current_frame.hide()
+ self.current_frame = self.header_to_frame[header]
+ self.current_frame.show()
+ self.current_frame.trigger_event("expose")
+
+ def on_expose(self, event):
+ self.current_frame.trigger_event("expose")
+
+ def on_unexpose(self, event):
+ self.current_frame.trigger_event("unexpose")
diff --git a/ui/Builtin/LUIVerticalLayout.py b/ui/Builtin/LUIVerticalLayout.py
new file mode 100644
index 00000000..fd15f81c
--- /dev/null
+++ b/ui/Builtin/LUIVerticalLayout.py
@@ -0,0 +1,20 @@
+"""
+
+This is a wrapper file. It contains no actual implementation
+
+"""
+
+from panda3d.lui import LUIVerticalLayout as _LUIVerticalLayout
+from LUIInitialState import LUIInitialState
+
+__all__ = ["LUIVerticalLayout"]
+
+
+class LUIVerticalLayout(_LUIVerticalLayout):
+ """ This is a wrapper class for the C++ LUIVerticalLayout class, to be
+ able to use it in a more convenient way. It leverages LUIInitialState
+ to be able to pass arbitrary keyword arguments. """
+
+ def __init__(self, parent=None, spacing=0.0, **kwargs):
+ _LUIVerticalLayout.__init__(self, parent, spacing)
+ LUIInitialState.init(self, kwargs)
diff --git a/ui/Builtin/RectTransform.py b/ui/Builtin/RectTransform.py
new file mode 100644
index 00000000..e1d98028
--- /dev/null
+++ b/ui/Builtin/RectTransform.py
@@ -0,0 +1,85 @@
+"""RectTransform - 一个轻量级的 Unity-like RectTransform 实现(Python 层 MVP)
+
+提供 anchors/pivot/anchored_position/size_delta 的数据结构和
+把相对锚点映射到像素矩形的计算方法。
+
+此实现不修改底层 C++ LUI,而是在 Python 层计算并把结果应用到
+`LUIObject` 的 `left/top/width/height` 属性上。
+"""
+from typing import Tuple
+
+class RectTransform(object):
+ def __init__(self,
+ anchor_min=(0.5, 0.5),
+ anchor_max=(0.5, 0.5),
+ pivot=(0.5, 0.5),
+ anchored_position=(0.0, 0.0),
+ size_delta=(100.0, 30.0)):
+ # 值域: anchor/pivot 为 0..1,anchored_position/size_delta 为像素
+ self.anchor_min = tuple(anchor_min)
+ self.anchor_max = tuple(anchor_max)
+ self.pivot = tuple(pivot)
+ self.anchored_position = tuple(anchored_position)
+ self.size_delta = tuple(size_delta)
+
+ def compute_pixel_rect(self, parent_rect: Tuple[float, float, float, float]):
+ """
+ 将 RectTransform 转换为相对于父节点的像素矩形 (left, top, width, height)
+
+ parent_rect: (parent_left, parent_top, parent_width, parent_height)
+ """
+ p_left, p_top, p_w, p_h = parent_rect
+
+ a_min_x = self.anchor_min[0]
+ a_min_y = self.anchor_min[1]
+ a_max_x = self.anchor_max[0]
+ a_max_y = self.anchor_max[1]
+
+ # 计算锚框的像素坐标(left/top 和 right/bottom)
+ anchor_left = p_left + a_min_x * p_w
+ anchor_right = p_left + a_max_x * p_w
+ anchor_top = p_top + a_min_y * p_h
+ anchor_bottom = p_top + a_max_y * p_h
+
+ # 宽高
+ if abs(a_max_x - a_min_x) < 1e-6:
+ # 固定锚点 -> 使用 size_delta.x
+ width = float(self.size_delta[0])
+ # anchored_position.x 表示相对于锚点框的位置(像素)
+ left = anchor_left + float(self.anchored_position[0]) - self.pivot[0] * width
+ else:
+ # 拉伸到锚框再加上 size_delta.x
+ width = (anchor_right - anchor_left) + float(self.size_delta[0])
+ # anchored_position.x 表示锚框内的像素偏移
+ left = anchor_left + float(self.anchored_position[0])
+
+ if abs(a_max_y - a_min_y) < 1e-6:
+ height = float(self.size_delta[1])
+ # 注意:engine 中 top 增大表示向下,所以 anchored_position.y 保持像素系
+ top = anchor_top + float(self.anchored_position[1]) - self.pivot[1] * height
+ else:
+ height = (anchor_bottom - anchor_top) + float(self.size_delta[1])
+ top = anchor_top + float(self.anchored_position[1])
+
+ return (left, top, width, height)
+
+ def apply_to_lui(self, lui_obj, parent_rect: Tuple[float, float, float, float]):
+ """计算并应用到给定的 LUIObject(设置 left/top/width/height)"""
+ left, top, width, height = self.compute_pixel_rect(parent_rect)
+ try:
+ lui_obj.left = left
+ lui_obj.top = top
+ lui_obj.width = width
+ lui_obj.height = height
+ except Exception:
+ # 忽略不能设置的属性,调用者应保证对象支持这些属性
+ pass
+
+ def to_dict(self):
+ return {
+ 'anchor_min': tuple(self.anchor_min),
+ 'anchor_max': tuple(self.anchor_max),
+ 'pivot': tuple(self.pivot),
+ 'anchored_position': tuple(self.anchored_position),
+ 'size_delta': tuple(self.size_delta),
+ }
diff --git a/ui/Builtin/__init__.py b/ui/Builtin/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/ui/Skins/Default/GenerateAtlas.bat b/ui/Skins/Default/GenerateAtlas.bat
new file mode 100644
index 00000000..3ae0bb00
--- /dev/null
+++ b/ui/Skins/Default/GenerateAtlas.bat
@@ -0,0 +1,5 @@
+@echo off
+
+cd res
+ppython ../../../Misc/LUIAtlasGen.py
+pause
diff --git a/ui/Skins/Default/__init__.py b/ui/Skins/Default/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/ui/Skins/Default/font/SourceSansPro-Black.ttf b/ui/Skins/Default/font/SourceSansPro-Black.ttf
new file mode 100644
index 0000000000000000000000000000000000000000..0243842fc8b5055b21729c895efaac6b4377c26a
GIT binary patch
literal 289528
zcmdSCe_+=0{{R1YzFx1ru2rj6ZMAA$*R>zjs#UA4T1i%|TIn#AERvNFCk#7;Foa5&
z3UR`uL&w=+2$L}Bc!v%l3?Z7t>Cj;aY2Vx9^}JU3b&m6We?EVFI_K8?c|Bjx=i~YN
z=k>ZGMnpRBmnS)+Mou1cZRh0=ig!#MzQYP9j~}=Gtg@ke-Y1f>Z(QM+u~%kZvsTnf|nuLqv|_qdD)indg-?HHk}l
zDR{)6Huu6)x8E`TNRbPsi*uyRIc4VTxKsW+>5s5MS+26|FnRxuHW%)>$XD9sZTHaChM&FTc99
z)N13F{e$;E5kJ?{k1fOrVvroQSHJ&rinM~~gX8DM{54eYC$vI+_`6aPJ0>eeKMaB4>hRJ6E9DkOEip`93M41&W+_Y3Y=s
z?WM?#N2_2h&r4%^KAI;{Na0zbGrZ|*H=j5YA&UJl&r+m^Qz*ksJ?=uublOPN%#n^x
zx`dsv_(vtjq&IDH@^SZ;VeV`hrv6S&5Pq1(F{^mCQQDhIY2%EN!A?{X+%RdT@;NLC
z=55Ie%A;laKa4xI>C1c*G
zeW>k+?uIS!o{lIOVf|z{?orU2&y5G#S=&0+-rD9r!3#cjY5Gw6LNh_1(0=i2Xn*czhLf(G6n(|Ga5EF7wLS|jLu7D66^wX
z2Q+%#Epa#ZQL8CDZ-d|4tT}Jcwhgq=2y{x*cBfc+2PELf`hze2l
z8Emd@dfVLG)TsT$SwVWW2m6Qi6UNWmK|Jka+J@UO1W;0{8J$?OCYxBq^q0)PY18c!-~dX{JXo?>E=uf2Rot>i^%xYp(Zyqjro_&&^{rDxL7l
zWt@K%FubY3xs7r*?|F1>TTVOc`mA%RuAQt=tcP~}(|t_yerE#ptLxVtJkxzoZ{q1X
z!8}cv9Np`6s-GY-a)UcP;*dX@gC`y6dLnx697!?x?1JVk7FDN}Fai?NeKZwl^Qr
zUx!IQ+EVi{jQ-n?ewX0Pz#VSdM?Xvm`pNb5xdQT2M4Y{}eJXjO-_Rd@^3ucEhrNw_
zo=!Y$1@V~*wVO)#u?u9T;Q&zudHA{uz)$3vbd#9d!f;tf=6Yq{6cggIs=`74rUKUop|k=_CllQqdxvOH|`-tc}nZHw+y{*cY011;k6Xx5mbgo6fW8gegXg2~$fL2>OQhkN<+8|Iog_ra`~b
zzV#cRZnckOQO{4s`br({W?sPkSq%1DZ=@Oe3%^I6cHS1=M>1>xX3$nnRekWeH;;M~;Fwv^~&A
zW7MEaLF>He5GLV1KgJYP<0`Y{(WbrfP*Wo|KUKSC4r6W(<4KhuZi&;;lGHn9zRXHd4GN`
z;g{1UocS~_LH#U<)erNdo*9FD>)u@Rpy?`_7e)8z`H*7w2hMcL%0Agm5f2honfFti
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®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