From c8eb4cef4d23c43255a67e366bb1d803c0fd49d3 Mon Sep 17 00:00:00 2001 From: ayuan9957 <107920784+ayuan9957@users.noreply.github.com> Date: Sun, 1 Mar 2026 19:39:13 +0800 Subject: [PATCH] refactor(lui): complete batch split for interaction/editor/property flows --- PROJECT_OPTIMIZATION_ANALYSIS.md | 190 ++- PROJECT_TASK_CHECKLIST.md | 176 ++- ui/LUI/lui_function_properties.py | 2325 +++++++++++++++-------------- ui/LUI/lui_manager_editor.py | 376 ++--- ui/LUI/lui_manager_interaction.py | 418 ++++-- 5 files changed, 1976 insertions(+), 1509 deletions(-) diff --git a/PROJECT_OPTIMIZATION_ANALYSIS.md b/PROJECT_OPTIMIZATION_ANALYSIS.md index b27d604d..e8f0a1c7 100644 --- a/PROJECT_OPTIMIZATION_ANALYSIS.md +++ b/PROJECT_OPTIMIZATION_ANALYSIS.md @@ -62,13 +62,91 @@ - `ui/panels/editor_panels_left.py::_draw_resource_manager` 保留布局版拆分已完成: - 实测指标:`310 -> 23` 行(第一轮 `310 -> 163`,第二轮 `163 -> 23`) - 目录/文件项渲染、展开内容、右键菜单流程已拆到独立 helper +- `ui/LUI/lui_function_properties.py::_draw_component_properties` 第一轮编排拆分已完成: + - 实测指标:`1441 -> 3` 行(入口) + - 新增编排/子流程 helper: + - `_draw_component_properties_core`(`34`) + - `_draw_text_and_button_properties`(`216`) + - `_draw_layout_transform_properties`(`200`) + - `_draw_type_specific_properties`(`835`) + - `_draw_anchor_and_hierarchy_properties`(`168`) +- `ui/LUI/lui_manager_interaction.py::_handle_resize_drag` 第二轮拆分已完成: + - 实测指标:`257 -> 3` 行(入口) + - 新增编排/子流程 helper: + - `_handle_resize_drag_core`(`44`) + - `_finish_resize_drag_if_released`(`19`) + - `_compute_resize_drag_bounds`(`109`) + - `_apply_resize_drag_updates`(`110`) +- `ui/LUI/lui_manager_interaction.py::_compute_resize_drag_bounds` / `_apply_resize_drag_updates` 第三轮细拆已完成: + - 实测指标:`109 -> 27` / `110 -> 21` + - 新增细粒度 helper(边界计算与组件应用分层): + - `_get_resize_start_values`(`7`) + - `_apply_resize_handle_delta`(`36`) + - `_apply_resize_min_size_clamp`(`15`) + - `_apply_shift_keep_ratio_resize`(`17`) + - `_apply_alt_center_resize`(`22`) + - `_resolve_resize_parent_context`(`13`) + - `_apply_resize_component_size_by_type`(`24`) + - `_sync_input_field_layout_stretch`(`22`) +- `ui/LUI/lui_function_properties.py::_draw_type_specific_properties` 第二轮拆分已完成: + - 实测指标:`835 -> 25` 行(类型分发入口) + - 新增类型 helper: + - `_draw_type_props_input_field`(`91`) + - `_draw_type_props_slider`(`11`) + - `_draw_type_props_checkbox`(`19`) + - `_draw_type_props_plane_image`(`152`) + - `_draw_type_props_frame`(`14`) + - `_draw_type_props_selectbox`(`84`) + - `_draw_type_props_http_text`(`72`) + - `_draw_type_props_video`(`10`) + - `_draw_video_source_and_audio_controls`(`4`,第三轮后为编排入口) + - `_draw_video_playback_controls`(`147`) +- `ui/LUI/lui_function_properties.py::_draw_video_source_and_audio_controls` 第三轮拆分已完成: + - 实测指标:`231 -> 4` 行(入口) + - 新增子流程 helper: + - `_draw_video_audio_controls`(`26`) + - `_draw_video_source_controls`(`19`) + - `_process_pending_video_source`(`22`) + - `_load_video_texture_from_source`(`21`) + - `_apply_loaded_video_texture`(`14`) + - `_sync_video_size_and_sprite`(`28`) + - `_replace_video_sprite`(`23`) + - `_refresh_video_keep_alive_node`(`22`) + - `_load_audio_for_component`(`22`) + - `_start_video_texture_playback`(`7`) +- `ui/LUI/lui_function_properties.py::_draw_text_and_button_properties` 第四轮拆分已完成: + - 实测指标:`216 -> 14` 行(入口) + - 新增子流程 helper(文本编辑/颜色同步/按钮贴图): + - `_draw_text_content_editor`(`13`) + - `_draw_text_font_size_editor`(`18`) + - `_draw_text_color_editor`(`11`) + - `_sync_text_or_button_color`(`10`) + - `_draw_button_texture_controls`(`19`) + - `_apply_button_texture_variant`(`28`) +- `ui/LUI/lui_manager_editor.py::_set_parent_child_relationship` 第四轮拆分已完成: + - 实测指标:`234 -> 43` 行(入口) + - 新增子流程 helper(关系校验/索引同步/可见层级): + - `_is_valid_relationship_index`(`5`) + - `_prepare_parent_child_link`(`8`) + - `_remove_child_from_old_parent`(`8`) + - `_attach_child_to_new_parent`(`6`) + - `_compute_new_child_local_position`(`7`) + - `_ensure_child_visibility_and_layer`(`19`) +- `ui/LUI/lui_function_properties.py::_draw_layout_transform_properties` 第四轮拆分已完成: + - 实测指标:`200 -> 10` 行(入口) + - 新增子流程 helper(填充模式/尺寸联动/Layout Group/Z-Offset): + - `_draw_fill_layout_controls`(`37`) + - `_draw_position_controls`(`18`) + - `_draw_size_controls`(`17`) + - `_draw_layout_group_controls`(`53`) + - `_draw_z_offset_controls`(`27`) ## 1. 总体画像 - Python 文件: `147` -- 代码总行数: `58,341` -- `except Exception` / `except:` 总计: `949` -- 裸 `except:` 总计: `62` +- 代码总行数: `58,479` +- `except Exception` / `except:` 总计: `993` +- 裸 `except:` 总计: `52` - 旧上下文关键词引用总量: - `interface_manager`: `20` - `treeWidget`: `2` @@ -85,24 +163,27 @@ ### 2.1 超大文件 Top(非 VR) -1. `core/selection.py` (`2941` 行) -2. `core/InfoPanelManager.py` (`1725` 行) -3. `ui/LUI/lui_manager_editor.py` (`1724` 行) -4. `ui/panels/property_helpers.py` (`1711` 行) -5. `ui/LUI/lui_function_properties.py` (`1707` 行) -6. `TransformGizmo/rotate_gizmo.py` (`1587` 行) -7. `ui/panels/animation_tools.py` (`1579` 行) -8. `templates/main_template.py` (`1571` 行) +1. `core/selection.py` (`2933` 行) +2. `ui/LUI/lui_function_properties.py` (`1731` 行) +3. `core/InfoPanelManager.py` (`1726` 行) +4. `ui/LUI/lui_manager_editor.py` (`1725` 行) +5. `ui/panels/property_helpers.py` (`1712` 行) +6. `TransformGizmo/rotate_gizmo.py` (`1588` 行) +7. `templates/main_template.py` (`1572` 行) +8. `ui/panels/animation_tools.py` (`1563` 行) ### 2.2 长函数 Top(优先拆分,非 VR 当前状态) -1. `ui/LUI/lui_function_properties.py::_draw_component_properties` (`1441` 行) -2. `ui/LUI/lui_manager_interaction.py::_handle_resize_drag` (`257` 行) -3. `ui/LUI/lui_manager_editor.py::_set_parent_child_relationship` (`234` 行) -4. `ui/panels/editor_panels_top.py::draw_menu_bar` (`227` 行) -5. `scene/scene_manager_io_mixin.py::saveScene` (`222` 行) -6. `TransformGizmo/move_gizmo.py::_on_mouse_down` (`222` 行) -7. `ui/LUI/lui_manager_editor.py::draw_editor` (`217` 行) +1. `ui/panels/editor_panels_top.py::draw_menu_bar` (`227` 行) +2. `scene/scene_manager_io_mixin.py::saveScene` (`222` 行) +3. `TransformGizmo/move_gizmo.py::_on_mouse_down` (`222` 行) +4. `ui/LUI/lui_manager_editor.py::draw_editor` (`217` 行) +5. `ui/LUI/lui_manager_editor.py::draw_component_tree` (`217` 行) +6. `core/InfoPanelManager.py::add_methods_to_property_panel` (`209` 行) +7. `ui/panels/editor_panels_right.py::_draw_animation_properties` (`201` 行) +8. `ui/panels/editor_panels_right.py::_draw_gui_properties` (`191` 行) +9. `core/event_handler.py::mousePressEventLeft` (`186` 行) +10. `scene/scene_manager_model_mixin.py::importModel` (`177` 行) ### 2.3 异常处理密度高(可观测性风险) @@ -161,9 +242,9 @@ 建议拆分顺序(非 VR 当前阶段): -1. `ui/LUI/lui_function_properties.py::_draw_component_properties`(可按“变换/布局/视觉/交互/脚本”分区) -2. `ui/LUI/lui_manager_interaction.py::_handle_resize_drag` -3. `ui/panels/editor_panels_top.py::draw_menu_bar` +1. `ui/panels/editor_panels_top.py::draw_menu_bar` +2. `scene/scene_manager_io_mixin.py::saveScene` +3. `core/InfoPanelManager.py::add_methods_to_property_panel` 预期收益: @@ -249,12 +330,65 @@ - 主函数长度降到 `<= 140` 行(实际 `28` 行) - 拖拽与缩放交互行为保持一致 -### Task H(推荐下一步,2-3 天) +### Task H(已完成,第一轮) - 拆分 `ui/LUI/lui_function_properties.py::_draw_component_properties` - 验收: - - 主函数长度降到 `<= 260` 行 - - 按“变换/布局/视觉/交互/脚本”子区块拆分并保持渲染逻辑一致 + - 主函数长度降到 `<= 260` 行(实际 `3` 行,编排核心 `34` 行) + - 已完成第一轮分区拆分并保持渲染逻辑一致 + +### Task I(已完成,第二轮) + +- 拆分 `ui/LUI/lui_manager_interaction.py::_handle_resize_drag` +- 验收: + - 入口流程收敛到 `<= 120` 行(实际 `3` 行,编排核心 `44` 行) + - 缩放手柄行为与边界语义保持一致 +- 第三轮细拆补充(已完成): + - `_compute_resize_drag_bounds`: `109 -> 27` + - `_apply_resize_drag_updates`: `110 -> 21` + - 继续保持缩放语义一致 + +### Task J(已完成,第二轮) + +- 继续拆分 `ui/LUI/lui_function_properties.py::_draw_type_specific_properties` +- 验收: + - 单函数长度降到 `<= 320` 行(实际 `25` 行) + - 按组件类型拆分为独立 helper 并保持行为一致 + +### Task K(已完成,第三轮) + +- 继续拆分 `ui/LUI/lui_function_properties.py::_draw_video_source_and_audio_controls`(已完成) +- 验收: + - 单函数长度降到 `<= 160` 行(实际 `4` 行) + - 行为保持一致(本地视频/URL加载/音频联动) + +### Task L(已完成,第四轮) + +- 继续拆分 `ui/LUI/lui_function_properties.py::_draw_text_and_button_properties` +- 验收: + - 单函数长度降到 `<= 160` 行(实际 `14` 行) + - 文本/按钮属性编辑行为保持一致 + +### Task M(已完成,第四轮) + +- 继续拆分 `ui/LUI/lui_manager_editor.py::_set_parent_child_relationship` +- 验收: + - 单函数长度降到 `<= 180` 行(实际 `43` 行) + - 父子关系编辑行为保持一致 + +### Task N(推荐下一步,1-2 天) + +- 拆分 `ui/panels/editor_panels_top.py::draw_menu_bar` +- 验收: + - 单函数长度降到 `<= 170` 行 + - 菜单项顺序与行为保持一致 + +### Task O(已完成,第四轮) + +- 继续拆分 `ui/LUI/lui_function_properties.py::_draw_layout_transform_properties` +- 验收: + - 单函数长度降到 `<= 160` 行(实际 `10` 行) + - 布局与尺寸联动行为保持一致 ## 4.1 本轮深入分析(非 VR,P1 准备) @@ -281,7 +415,7 @@ - 失败重试逻辑保持语义一致。 - 当前状态(2026-03-01): - 第二轮已完成(`loadScene` 已达验收目标:`74` 行)。 - - 后续建议转入回归观察,优先推进 `ui/LUI/lui_function_properties.py::_draw_component_properties`。 + - 后续建议转入回归观察,优先推进 `ui/panels/editor_panels_top.py::draw_menu_bar`。 ### B) `main.py::__init__`(第二优先,已完成第一轮) @@ -345,8 +479,8 @@ 1. `loadScene` 已完成两轮拆分(当前建议冻结并转入回归观察)。 2. `main.__init__` 已完成第一轮拆分(建议转入回归观察)。 3. `_update_drag` 已完成第一轮拆分,建议转入回归观察。 -4. 下一步处理 `_draw_component_properties`(属性面板分区拆分)。 -5. 然后处理 `_handle_resize_drag`(拖拽缩放路径补充拆分)。 +4. `_draw_component_properties` 第一轮已完成,`_draw_type_specific_properties` 二轮与视频段三轮已完成。 +5. `_handle_resize_drag` 第三轮细拆已完成,`_draw_text_and_button_properties`、`_set_parent_child_relationship`、`_draw_layout_transform_properties` 第四轮已完成。 ## 5. 与现有文档关系 @@ -356,4 +490,4 @@ --- -如果按此路线继续,建议下一轮直接从 **Task H** 开始,先落地 `_draw_component_properties` 的分区拆分并保持行为不变。 +如果按此路线继续,建议下一轮直接从 **Task N** 开始,优先落地 `draw_menu_bar` 的流程拆分并保持行为不变。 diff --git a/PROJECT_TASK_CHECKLIST.md b/PROJECT_TASK_CHECKLIST.md index bc33865f..68d7411a 100644 --- a/PROJECT_TASK_CHECKLIST.md +++ b/PROJECT_TASK_CHECKLIST.md @@ -15,6 +15,12 @@ - 项目已进入“非 VR 结构优化阶段”。 - `_getActor`、`updateGizmoDrag`、`_update_drag` 第一轮拆分已完成。 - `资源管理器面板` 已完成“保留原布局/样式/交互”的第二轮拆分收敛。 +- `_draw_component_properties` 已完成第一轮“编排入口 + 子流程”拆分(入口 `1441 -> 3` 行)。 +- `_handle_resize_drag` 已完成第三轮“边界计算 / 更新应用”细拆(核心 `44` 行)。 +- `_draw_video_source_and_audio_controls` 已完成第三轮“音频/视频源/加载应用”拆分(`231 -> 4` 行)。 +- `_draw_text_and_button_properties` 已完成第四轮“文本/颜色/按钮贴图”拆分(`216 -> 14` 行)。 +- `_set_parent_child_relationship` 已完成第四轮“关系编排 + 可见层级”拆分(`234 -> 43` 行)。 +- `_draw_layout_transform_properties` 已完成第四轮“布局/尺寸/Z-Offset”拆分(`200 -> 10` 行)。 ## 已完成(Done) @@ -89,23 +95,173 @@ - 新增 helper:`_update_resource_manager_window_rect`、`_draw_resource_directory_entries`、`_draw_resource_directory_entry`、`_draw_resource_directory_children`、`_draw_resource_subdir_entry`、`_draw_resource_subfile_entry`、`_draw_resource_file_entries`、`_draw_resource_file_entry` - 保持目标:不改视觉布局,不改交互语义,仅做结构分解 +### H. LUI 属性面板拆分完成(第一轮编排) + +- [x] `ui/LUI/lui_function_properties.py::_draw_component_properties` 第一轮拆分完成: + - 入口函数行数:`1441 -> 3` + - 编排主流程函数:`_draw_component_properties_core = 34` 行 + - 新增 helper: + - `_draw_text_and_button_properties`(`216` 行) + - `_draw_layout_transform_properties`(`200` 行) + - `_draw_type_specific_properties`(`835` 行) + - `_draw_anchor_and_hierarchy_properties`(`168` 行) + - 保持目标:属性面板行为不变,先完成入口和流程层收敛 + +### I. LUI 缩放链路拆分完成(第二轮) + +- [x] `ui/LUI/lui_manager_interaction.py::_handle_resize_drag` 第二轮拆分完成: + - 入口函数行数:`257 -> 3` + - 编排主流程函数:`_handle_resize_drag_core = 44` 行 + - 新增 helper: + - `_finish_resize_drag_if_released`(`19` 行) + - `_compute_resize_drag_bounds`(`109` 行) + - `_apply_resize_drag_updates`(`110` 行) + - 保持目标:缩放手柄行为与边界语义保持一致 + +### J. LUI 属性类型分支拆分完成(第二轮) + +- [x] `ui/LUI/lui_function_properties.py::_draw_type_specific_properties` 第二轮拆分完成: + - 主函数行数:`835 -> 25`(类型分发入口) + - 新增类型 helper: + - `_draw_type_props_input_field`(`91` 行) + - `_draw_type_props_slider`(`11` 行) + - `_draw_type_props_checkbox`(`19` 行) + - `_draw_type_props_plane_image`(`152` 行) + - `_draw_type_props_frame`(`14` 行) + - `_draw_type_props_selectbox`(`84` 行) + - `_draw_type_props_http_text`(`72` 行) + - `_draw_type_props_video`(`10` 行) + - `_draw_video_source_and_audio_controls`(`231` 行) + - `_draw_video_playback_controls`(`147` 行) + - 保持目标:各组件类型交互语义保持一致,先完成类型分发层收敛 + +### K. LUI 视频属性段拆分完成(第三轮) + +- [x] `ui/LUI/lui_function_properties.py::_draw_video_source_and_audio_controls` 第三轮拆分完成: + - 主函数行数:`231 -> 4`(编排入口) + - 新增 helper: + - `_draw_video_audio_controls`(`26` 行) + - `_draw_video_source_controls`(`19` 行) + - `_process_pending_video_source`(`22` 行) + - `_load_video_texture_from_source`(`21` 行) + - `_apply_loaded_video_texture`(`14` 行) + - `_sync_video_size_and_sprite`(`28` 行) + - `_replace_video_sprite`(`23` 行) + - `_refresh_video_keep_alive_node`(`22` 行) + - `_load_audio_for_component`(`22` 行) + - `_start_video_texture_playback`(`7` 行) + - 保持目标:本地视频加载/URL加载回退/音频联动语义保持一致 + +### L. LUI 缩放链路细拆完成(第三轮) + +- [x] `ui/LUI/lui_manager_interaction.py::_compute_resize_drag_bounds` / `_apply_resize_drag_updates` 第三轮细拆完成: + - 主函数行数:`109 -> 27` / `110 -> 21` + - 新增 helper: + - `_get_resize_start_values`(`7` 行) + - `_apply_resize_handle_delta`(`36` 行) + - `_apply_resize_min_size_clamp`(`15` 行) + - `_apply_shift_keep_ratio_resize`(`17` 行) + - `_apply_alt_center_resize`(`22` 行) + - `_cache_resize_canvas_bounds`(`8` 行) + - `_sanitize_resize_dimensions`(`6` 行) + - `_resolve_resize_parent_context`(`13` 行) + - `_write_resize_component_data`(`5` 行) + - `_apply_resize_component_position`(`9` 行) + - `_apply_resize_component_size_by_type`(`24` 行) + - `_apply_button_resize_size`(`7` 行) + - `_apply_input_field_resize_size`(`10` 行) + - `_sync_input_field_layout_stretch`(`22` 行) + - `_post_resize_component_sync`(`7` 行) + - `_get_resize_modifier_info`(`7` 行) + - 保持目标:缩放手柄行为与边界语义保持一致 + +### M. LUI 文本按钮属性段拆分完成(第四轮) + +- [x] `ui/LUI/lui_function_properties.py::_draw_text_and_button_properties` 第四轮拆分完成: + - 主函数行数:`216 -> 14` + - 新增 helper: + - `_draw_text_content_editor`(`13` 行) + - `_draw_text_font_size_editor`(`18` 行) + - `_draw_text_color_editor`(`11` 行) + - `_sync_text_or_button_color`(`10` 行) + - `_apply_button_layout_color`(`12` 行) + - `_apply_text_color`(`3` 行) + - `_draw_button_texture_controls`(`19` 行) + - `_draw_button_texture_pick_button`(`22` 行) + - `_apply_button_texture_variant`(`28` 行) + - `_unpack_button_atlas_result`(`9` 行) + - `_apply_button_custom_textures`(`11` 行) + - `_fit_button_to_default_texture_size`(`22` 行) + - `_clear_button_custom_textures`(`13` 行) + - `_draw_button_texture_path_labels`(`7` 行) + - 保持目标:文本/按钮编辑行为与贴图切换语义保持一致 + +### N. LUI 层级编辑父子关系拆分完成(第四轮) + +- [x] `ui/LUI/lui_manager_editor.py::_set_parent_child_relationship` 第四轮拆分完成: + - 主函数行数:`234 -> 43` + - 新增 helper: + - `_is_valid_relationship_index`(`5` 行) + - `_prepare_parent_child_link`(`8` 行) + - `_remove_child_from_old_parent`(`8` 行) + - `_attach_child_to_new_parent`(`6` 行) + - `_compute_new_child_local_position`(`7` 行) + - `_resolve_parent_visual_object`(`10` 行) + - `_set_child_visual_parent_policy`(`12` 行) + - `_clamp_child_local_within_parent`(`27` 行) + - `_sync_child_canvas_index_with_parent`(`7` 行) + - `_apply_child_absolute_position`(`10` 行) + - `_ensure_child_visibility_and_layer`(`19` 行) + - `_ensure_button_child_visibility`(`15` 行) + - `_ensure_frame_child_visibility`(`9` 行) + - `_ensure_media_child_visibility`(`13` 行) + - `_ensure_child_sprite_visibility_and_layer`(`24` 行) + - `_finalize_parent_child_link`(`9` 行) + - 保持目标:逻辑父子关系/可见层级/画布索引同步语义保持一致 + +### O. LUI 布局变换属性段拆分完成(第四轮) + +- [x] `ui/LUI/lui_function_properties.py::_draw_layout_transform_properties` 第四轮拆分完成: + - 主函数行数:`200 -> 10` + - 新增 helper: + - `_draw_fill_layout_controls`(`37` 行) + - `_draw_position_controls`(`18` 行) + - `_draw_size_controls`(`17` 行) + - `_draw_width_control`(`23` 行) + - `_draw_height_control`(`19` 行) + - `_post_size_control_sync`(`9` 行) + - `_draw_layout_group_controls`(`53` 行) + - `_draw_z_offset_controls`(`27` 行) + - 保持目标:填充模式/尺寸联动/Layout Group/Z-Offset 行为一致 + ## 下一步(Next) -### N1 `属性面板超大函数` 拆分(最高优先级,非 VR) +### N1 `顶部菜单栏` 拆分(最高优先级,非 VR) -- 目标文件:`ui/LUI/lui_function_properties.py::_draw_component_properties` -- 当前规模:`1441` 行 +- 目标文件:`ui/panels/editor_panels_top.py::draw_menu_bar` +- 当前规模:`227` 行 - 验收目标: - - 按“变换/布局/视觉/交互/脚本”拆分为多个子绘制函数 - - 主流程压缩并保持属性面板行为一致 + - 拆分为“菜单组渲染 / 行为分发 / 状态同步”子流程 + - 单函数规模收敛到 `<= 170` 行 + - 保持现有菜单项顺序与功能一致 -### N2 `LUI 缩放链路` 拆分(高优先级,非 VR) +### N2 `场景保存链路` 拆分(高优先级,非 VR) -- 目标文件:`ui/LUI/lui_manager_interaction.py::_handle_resize_drag` -- 当前规模:`257` 行 +- 目标文件:`scene/scene_manager_io_mixin.py::saveScene` +- 当前规模:`222` 行 - 验收目标: - - 入口流程收敛到 `<= 120` 行 - - 保持缩放手柄行为与边界语义一致 + - 拆分为“保存前校验 / 节点序列化 / 后处理”子流程 + - 单函数规模收敛到 `<= 170` 行 + - 保持保存行为与输出格式一致 + +### N3 `属性面板方法注入` 拆分(中优先级,非 VR) + +- 目标文件:`core/InfoPanelManager.py::add_methods_to_property_panel` +- 当前规模:`209` 行 +- 验收目标: + - 拆分为“能力绑定 / 兼容兜底 / 日志分层”子流程 + - 单函数规模收敛到 `<= 170` 行 + - 保持属性面板行为一致 ## 验收标准(阶段) diff --git a/ui/LUI/lui_function_properties.py b/ui/LUI/lui_function_properties.py index 852c4e20..97c88882 100644 --- a/ui/LUI/lui_function_properties.py +++ b/ui/LUI/lui_function_properties.py @@ -265,6 +265,10 @@ class LUIFunctionPropertiesMixin: return None def _draw_component_properties(manager, index): + """Orchestrator entry for component property rendering.""" + return manager.luiFunction._draw_component_properties_core(manager, index) + + def _draw_component_properties_core(manager, index): """绘制组件属性编辑面板""" if index < 0 or index >= len(manager.components): return @@ -284,230 +288,266 @@ class LUIFunctionPropertiesMixin: # 文本属性 # Text content # Text content - if comp_type in ['Button', 'Text', 'Label']: - imgui.text("Text Content") - imgui.separator() - if 'text' in comp_data: - imgui.push_item_width(-1) - changed, new_text = imgui.input_text("##text_input", comp_data['text'], 256) - imgui.pop_item_width() - if changed: - comp_data['text'] = new_text - comp_obj.text = new_text - print(f"Text updated: {new_text}") + manager.luiFunction._draw_text_and_button_properties( + manager, comp_data, comp_obj, comp_type + ) - if comp_type in ['Text', 'Label']: - imgui.spacing() - imgui.text("Font Size") - font_size = int(comp_data.get('font_size', 14)) - changed, new_size = imgui.slider_int("##font_size", font_size, 8, 96) - if changed: - comp_data['font_size'] = int(new_size) - try: - if hasattr(comp_obj, '_text') and hasattr(comp_obj._text, 'font_size'): - comp_obj._text.font_size = int(new_size) - if hasattr(comp_obj, '_shadow_text') and hasattr(comp_obj._shadow_text, 'font_size'): - comp_obj._shadow_text.font_size = int(new_size) - if hasattr(comp_obj, 'text_handle') and hasattr(comp_obj.text_handle, 'font_size'): - comp_obj.text_handle.font_size = int(new_size) - except Exception as e: - print(f"Font size update failed: {e}") + manager.luiFunction._draw_layout_transform_properties( + manager, index, comp_data, comp_obj, comp_type + ) - imgui.spacing() - imgui.text("Color") - color = comp_data.get('color', (1.0, 1.0, 1.0, 1.0)) - color = list(color) if isinstance(color, tuple) else color - changed, new_color = imgui.color_edit4("##color_edit", color) - if changed: - comp_data['color'] = tuple(new_color) - if comp_type == 'Button': - if hasattr(comp_obj, '_layout'): - try: - if hasattr(comp_obj._layout, '_sprite_left'): - comp_obj._layout._sprite_left.color = tuple(new_color) - if hasattr(comp_obj._layout, '_sprite_mid'): - comp_obj._layout._sprite_mid.color = tuple(new_color) - if hasattr(comp_obj._layout, '_sprite_right'): - comp_obj._layout._sprite_right.color = tuple(new_color) - except Exception as e: - print(f"Button color set failed: {e}") - else: - if hasattr(comp_obj, '_text'): - comp_obj._text.color = tuple(new_color) - imgui.separator() + manager.luiFunction._draw_type_specific_properties( + manager, index, comp_data, comp_obj, comp_type + ) + manager.luiFunction._draw_anchor_and_hierarchy_properties( + manager, index, comp_data, comp_obj + ) - if comp_type == 'Button': - if hasattr(comp_obj, '_layout'): - try: - if hasattr(comp_obj._layout, '_sprite_left'): - comp_obj._layout._sprite_left.color = tuple(new_color) - if hasattr(comp_obj._layout, '_sprite_mid'): - comp_obj._layout._sprite_mid.color = tuple(new_color) - if hasattr(comp_obj._layout, '_sprite_right'): - comp_obj._layout._sprite_right.color = tuple(new_color) - except Exception as e: - print(f"\u8bbe\u7f6e\u6309\u94ae\u989c\u8272\u5931\u8d25: {e}") - else: - if hasattr(comp_obj, '_text'): - comp_obj._text.color = tuple(new_color) - imgui.separator() - imgui.spacing() - imgui.text("\u989c\u8272") - color = comp_data.get('color', (1.0, 1.0, 1.0, 1.0)) + def _draw_text_and_button_properties(manager, comp_data, comp_obj, comp_type): + if comp_type not in ['Button', 'Text', 'Label']: + return + # 编排入口:文本编辑/字号/颜色与按钮贴图拆分,降低单函数复杂度。 + manager.luiFunction._draw_text_content_editor(comp_data, comp_obj) + if comp_type in ['Text', 'Label']: + manager.luiFunction._draw_text_font_size_editor(comp_data, comp_obj) + + draw_color = manager.luiFunction._draw_text_color_editor(comp_data) + manager.luiFunction._sync_text_or_button_color(comp_obj, comp_type, draw_color) if comp_type == 'Button': - imgui.text("\u6309\u94ae\u56fe\u7247") + manager.luiFunction._draw_button_texture_controls(manager, comp_data, comp_obj) - def _apply_button_textures(): - normal_tex = comp_data.get('button_texture_ref_normal') or comp_data.get('button_texture_ref') - normal_uv = comp_data.get('button_atlas_uv_normal') or comp_data.get('button_atlas_uv') - hover_tex = comp_data.get('button_texture_ref_hover') - hover_uv = comp_data.get('button_atlas_uv_hover') - pressed_tex = comp_data.get('button_texture_ref_pressed') - pressed_uv = comp_data.get('button_atlas_uv_pressed') - if hasattr(comp_obj, 'set_custom_textures') and normal_tex is not None: - comp_obj.set_custom_textures(normal_tex, normal_uv, hover_tex, hover_uv, pressed_tex, pressed_uv) - elif hasattr(comp_obj, 'set_custom_texture') and normal_tex is not None: - comp_obj.set_custom_texture(normal_tex, normal_uv) + def _draw_text_content_editor(manager, comp_data, comp_obj): + imgui.text("Text Content") + imgui.separator() + if 'text' not in comp_data: + return - if imgui.button("\u66f4\u6539\u9ed8\u8ba4\u56fe##button_img_norm", (160, 20)): - selected_path = manager._change_image_texture( - title = "\u9009\u62e9\u56fe\u7247\u6587\u4ef6", - filetypes = [ - ("\u56fe\u7247\u6587\u4ef6", "*.png;*.jpg;*.jpeg;*.bmp;*.gif;*.tga"), - ("PNG", "*.png"), - ("JPEG", "*.jpg;*.jpeg"), - ("\u6240\u6709\u6587\u4ef6", "*.*") - ] - ) - if selected_path: - try: - atlas_result = manager.luiFunction._add_image_to_atlas(manager, selected_path, atlas_size=2048) - if atlas_result: - if len(atlas_result) >= 7: - tex, u0, v0, u1, v1, w, h = atlas_result[:7] - else: - tex, u0, v0, u1, v1 = atlas_result - w = int(max(1, round((u1 - u0) * tex.getXSize()))) - h = int(max(1, round((v1 - v0) * tex.getYSize()))) - comp_data['button_texture_path'] = selected_path - comp_data['button_texture_path_normal'] = selected_path - comp_data['button_atlas_uv'] = (u0, v0, u1, v1) - comp_data['button_atlas_uv_normal'] = (u0, v0, u1, v1) - comp_data['button_atlas_tex'] = tex - comp_data['button_texture_ref'] = tex - comp_data['button_texture_ref_normal'] = tex - comp_data['button_image_size'] = (int(w), int(h)) - comp_data['button_image_size_normal'] = (int(w), int(h)) - _apply_button_textures() - except Exception as e: - print(f"Button texture set failed: {e}") + imgui.push_item_width(-1) + changed, new_text = imgui.input_text("##text_input", comp_data['text'], 256) + imgui.pop_item_width() + if changed: + comp_data['text'] = new_text + comp_obj.text = new_text + print(f"Text updated: {new_text}") - if imgui.button("\u66f4\u6539\u60ac\u505c\u56fe##button_img_hover", (160, 20)): - selected_path = manager._change_image_texture( - title = "\u9009\u62e9\u56fe\u7247\u6587\u4ef6", - filetypes = [ - ("\u56fe\u7247\u6587\u4ef6", "*.png;*.jpg;*.jpeg;*.bmp;*.gif;*.tga"), - ("PNG", "*.png"), - ("JPEG", "*.jpg;*.jpeg"), - ("\u6240\u6709\u6587\u4ef6", "*.*") - ] - ) - if selected_path: - try: - atlas_result = manager.luiFunction._add_image_to_atlas(manager, selected_path, atlas_size=2048) - if atlas_result: - if len(atlas_result) >= 7: - tex, u0, v0, u1, v1, w, h = atlas_result[:7] - else: - tex, u0, v0, u1, v1 = atlas_result - w = int(max(1, round((u1 - u0) * tex.getXSize()))) - h = int(max(1, round((v1 - v0) * tex.getYSize()))) - comp_data['button_texture_path_hover'] = selected_path - comp_data['button_atlas_uv_hover'] = (u0, v0, u1, v1) - comp_data['button_texture_ref_hover'] = tex - comp_data['button_image_size_hover'] = (int(w), int(h)) - _apply_button_textures() - except Exception as e: - print(f"Button hover texture set failed: {e}") + def _draw_text_font_size_editor(manager, comp_data, comp_obj): + imgui.spacing() + imgui.text("Font Size") + font_size = int(comp_data.get('font_size', 14)) + changed, new_size = imgui.slider_int("##font_size", font_size, 8, 96) + if not changed: + return - if imgui.button("\u66f4\u6539\u6309\u4e0b\u56fe##button_img_pressed", (160, 20)): - selected_path = manager._change_image_texture( - title = "\u9009\u62e9\u56fe\u7247\u6587\u4ef6", - filetypes = [ - ("\u56fe\u7247\u6587\u4ef6", "*.png;*.jpg;*.jpeg;*.bmp;*.gif;*.tga"), - ("PNG", "*.png"), - ("JPEG", "*.jpg;*.jpeg"), - ("\u6240\u6709\u6587\u4ef6", "*.*") - ] - ) - if selected_path: - try: - atlas_result = manager.luiFunction._add_image_to_atlas(manager, selected_path, atlas_size=2048) - if atlas_result: - if len(atlas_result) >= 7: - tex, u0, v0, u1, v1, w, h = atlas_result[:7] - else: - tex, u0, v0, u1, v1 = atlas_result - w = int(max(1, round((u1 - u0) * tex.getXSize()))) - h = int(max(1, round((v1 - v0) * tex.getYSize()))) - comp_data['button_texture_path_pressed'] = selected_path - comp_data['button_atlas_uv_pressed'] = (u0, v0, u1, v1) - comp_data['button_texture_ref_pressed'] = tex - comp_data['button_image_size_pressed'] = (int(w), int(h)) - _apply_button_textures() - except Exception as e: - print(f"Button pressed texture set failed: {e}") + comp_data['font_size'] = int(new_size) + try: + if hasattr(comp_obj, '_text') and hasattr(comp_obj._text, 'font_size'): + comp_obj._text.font_size = int(new_size) + if hasattr(comp_obj, '_shadow_text') and hasattr(comp_obj._shadow_text, 'font_size'): + comp_obj._shadow_text.font_size = int(new_size) + if hasattr(comp_obj, 'text_handle') and hasattr(comp_obj.text_handle, 'font_size'): + comp_obj.text_handle.font_size = int(new_size) + except Exception as e: + print(f"Font size update failed: {e}") - if imgui.button("\u4f7f\u7528\u9ed8\u8ba4\u56fe\u5c3a\u5bf8##button_fit", (160, 20)): + def _draw_text_color_editor(manager, comp_data): + imgui.spacing() + imgui.text("Color") + color = comp_data.get('color', (1.0, 1.0, 1.0, 1.0)) + color = list(color) if isinstance(color, tuple) else color + changed, new_color = imgui.color_edit4("##color_edit", color) + draw_color = tuple(new_color) + if changed: + comp_data['color'] = draw_color + imgui.separator() + return draw_color + + def _sync_text_or_button_color(manager, comp_obj, comp_type, draw_color): + # 保留原行为:每帧同步当前编辑色,避免按钮状态贴图切换后颜色不同步。 + if comp_type == 'Button': + manager.luiFunction._apply_button_layout_color(comp_obj, draw_color) + else: + manager.luiFunction._apply_text_color(comp_obj, draw_color) + + imgui.separator() + imgui.spacing() + imgui.text("颜色") + + def _apply_button_layout_color(manager, comp_obj, color_value): + if not hasattr(comp_obj, '_layout'): + return + try: + if hasattr(comp_obj._layout, '_sprite_left'): + comp_obj._layout._sprite_left.color = color_value + if hasattr(comp_obj._layout, '_sprite_mid'): + comp_obj._layout._sprite_mid.color = color_value + if hasattr(comp_obj._layout, '_sprite_right'): + comp_obj._layout._sprite_right.color = color_value + except Exception as e: + print(f"Button color set failed: {e}") + + def _apply_text_color(manager, comp_obj, color_value): + if hasattr(comp_obj, '_text'): + comp_obj._text.color = color_value + + def _draw_button_texture_controls(manager, comp_data, comp_obj): + imgui.text("按钮图片") + manager.luiFunction._draw_button_texture_pick_button( + manager, comp_data, comp_obj, "更改默认图##button_img_norm", "normal" + ) + manager.luiFunction._draw_button_texture_pick_button( + manager, comp_data, comp_obj, "更改悬停图##button_img_hover", "hover" + ) + manager.luiFunction._draw_button_texture_pick_button( + manager, comp_data, comp_obj, "更改按下图##button_img_pressed", "pressed" + ) + + if imgui.button("使用默认图尺寸##button_fit", (160, 20)): + manager.luiFunction._fit_button_to_default_texture_size(comp_data, comp_obj) + + if imgui.button("恢复默认按钮##button_default", (160, 20)): + manager.luiFunction._clear_button_custom_textures(comp_data, comp_obj) + + manager.luiFunction._draw_button_texture_path_labels(comp_data) + + def _draw_button_texture_pick_button(manager, comp_data, comp_obj, button_label, variant): + if not imgui.button(button_label, (160, 20)): + return + + selected_path = manager._change_image_texture( + title="选择图片文件", + filetypes=[ + ("图片文件", "*.png;*.jpg;*.jpeg;*.bmp;*.gif;*.tga"), + ("PNG", "*.png"), + ("JPEG", "*.jpg;*.jpeg"), + ("所有文件", "*.*"), + ], + ) + if not selected_path: + return + + try: + manager.luiFunction._apply_button_texture_variant( + manager, comp_data, comp_obj, selected_path, variant + ) + except Exception as e: + print(f"Button {variant} texture set failed: {e}") + + def _apply_button_texture_variant(manager, comp_data, comp_obj, selected_path, variant): + atlas_result = manager.luiFunction._add_image_to_atlas(manager, selected_path, atlas_size=2048) + if not atlas_result: + return + + tex, u0, v0, u1, v1, w, h = manager.luiFunction._unpack_button_atlas_result(atlas_result) + if variant == "normal": + comp_data['button_texture_path'] = selected_path + comp_data['button_texture_path_normal'] = selected_path + comp_data['button_atlas_uv'] = (u0, v0, u1, v1) + comp_data['button_atlas_uv_normal'] = (u0, v0, u1, v1) + comp_data['button_atlas_tex'] = tex + comp_data['button_texture_ref'] = tex + comp_data['button_texture_ref_normal'] = tex + comp_data['button_image_size'] = (int(w), int(h)) + comp_data['button_image_size_normal'] = (int(w), int(h)) + elif variant == "hover": + comp_data['button_texture_path_hover'] = selected_path + comp_data['button_atlas_uv_hover'] = (u0, v0, u1, v1) + comp_data['button_texture_ref_hover'] = tex + comp_data['button_image_size_hover'] = (int(w), int(h)) + elif variant == "pressed": + comp_data['button_texture_path_pressed'] = selected_path + comp_data['button_atlas_uv_pressed'] = (u0, v0, u1, v1) + comp_data['button_texture_ref_pressed'] = tex + comp_data['button_image_size_pressed'] = (int(w), int(h)) + + manager.luiFunction._apply_button_custom_textures(comp_data, comp_obj) + + def _unpack_button_atlas_result(manager, atlas_result): + if len(atlas_result) >= 7: + tex, u0, v0, u1, v1, w, h = atlas_result[:7] + return tex, u0, v0, u1, v1, w, h + + tex, u0, v0, u1, v1 = atlas_result + w = int(max(1, round((u1 - u0) * tex.getXSize()))) + h = int(max(1, round((v1 - v0) * tex.getYSize()))) + return tex, u0, v0, u1, v1, w, h + + def _apply_button_custom_textures(manager, comp_data, comp_obj): + normal_tex = comp_data.get('button_texture_ref_normal') or comp_data.get('button_texture_ref') + normal_uv = comp_data.get('button_atlas_uv_normal') or comp_data.get('button_atlas_uv') + hover_tex = comp_data.get('button_texture_ref_hover') + hover_uv = comp_data.get('button_atlas_uv_hover') + pressed_tex = comp_data.get('button_texture_ref_pressed') + pressed_uv = comp_data.get('button_atlas_uv_pressed') + if hasattr(comp_obj, 'set_custom_textures') and normal_tex is not None: + comp_obj.set_custom_textures(normal_tex, normal_uv, hover_tex, hover_uv, pressed_tex, pressed_uv) + elif hasattr(comp_obj, 'set_custom_texture') and normal_tex is not None: + comp_obj.set_custom_texture(normal_tex, normal_uv) + + def _fit_button_to_default_texture_size(manager, comp_data, comp_obj): + w = h = None + if comp_data.get('button_image_size_normal'): + try: + w, h = comp_data['button_image_size_normal'] + except Exception: + w = h = None + elif comp_data.get('button_image_size'): + try: + w, h = comp_data['button_image_size'] + except Exception: w = h = None - if 'button_image_size_normal' in comp_data and comp_data['button_image_size_normal']: - try: - w, h = comp_data['button_image_size_normal'] - except Exception: - w = h = None - elif 'button_image_size' in comp_data and comp_data['button_image_size']: - try: - w, h = comp_data['button_image_size'] - except Exception: - w = h = None - if w is not None and h is not None and w > 0 and h > 0: - comp_data['width'] = float(w) - comp_data['height'] = float(h) - comp_obj.width = float(w) - comp_obj.height = float(h) - if hasattr(comp_obj, '_apply_stretch_sizes'): - comp_obj._apply_stretch_sizes() - if imgui.button("\u6062\u590d\u9ed8\u8ba4\u6309\u94ae##button_default", (160, 20)): - if hasattr(comp_obj, 'clear_custom_texture'): - comp_obj.clear_custom_texture() - for key in [ - 'button_texture_path', 'button_texture_path_normal', 'button_texture_path_hover', - 'button_texture_path_pressed', 'button_texture_ref', 'button_texture_ref_normal', - 'button_texture_ref_hover', 'button_texture_ref_pressed', 'button_atlas_uv', - 'button_atlas_uv_normal', 'button_atlas_uv_hover', 'button_atlas_uv_pressed', - 'button_image_size', 'button_image_size_normal', 'button_image_size_hover', 'button_image_size_pressed' - ]: - if key in comp_data: - comp_data[key] = None + if w is None or h is None or w <= 0 or h <= 0: + return - if comp_data.get('button_texture_path_normal'): - imgui.text(f"\u9ed8\u8ba4\u56fe: {comp_data['button_texture_path_normal']}") - if comp_data.get('button_texture_path_hover'): - imgui.text(f"\u60ac\u505c\u56fe: {comp_data['button_texture_path_hover']}") - if comp_data.get('button_texture_path_pressed'): - imgui.text(f"\u6309\u4e0b\u56fe: {comp_data['button_texture_path_pressed']}") + comp_data['width'] = float(w) + comp_data['height'] = float(h) + comp_obj.width = float(w) + comp_obj.height = float(h) + if hasattr(comp_obj, '_apply_stretch_sizes'): + comp_obj._apply_stretch_sizes() - # 布局模式 + def _clear_button_custom_textures(manager, comp_data, comp_obj): + if hasattr(comp_obj, 'clear_custom_texture'): + comp_obj.clear_custom_texture() + + for key in [ + 'button_texture_path', 'button_texture_path_normal', 'button_texture_path_hover', + 'button_texture_path_pressed', 'button_texture_ref', 'button_texture_ref_normal', + 'button_texture_ref_hover', 'button_texture_ref_pressed', 'button_atlas_uv', + 'button_atlas_uv_normal', 'button_atlas_uv_hover', 'button_atlas_uv_pressed', + 'button_image_size', 'button_image_size_normal', 'button_image_size_hover', 'button_image_size_pressed', + ]: + if key in comp_data: + comp_data[key] = None + + def _draw_button_texture_path_labels(manager, comp_data): + if comp_data.get('button_texture_path_normal'): + imgui.text(f"默认图: {comp_data['button_texture_path_normal']}") + if comp_data.get('button_texture_path_hover'): + imgui.text(f"悬停图: {comp_data['button_texture_path_hover']}") + if comp_data.get('button_texture_path_pressed'): + imgui.text(f"按下图: {comp_data['button_texture_path_pressed']}") + + def _draw_layout_transform_properties(manager, index, comp_data, comp_obj, comp_type): + # 编排入口:布局模式/坐标尺寸/布局组/Z-Offset 采用分步函数,降低维护成本。 + is_fill = manager.luiFunction._draw_fill_layout_controls(manager, index, comp_data) + imgui.separator() + manager.luiFunction._draw_position_controls(manager, comp_data, comp_obj, is_fill) + manager.luiFunction._draw_size_controls( + manager, index, comp_data, comp_obj, comp_type, is_fill + ) + manager.luiFunction._draw_layout_group_controls(manager, index, comp_data, comp_type) + manager.luiFunction._draw_z_offset_controls(manager, index, comp_data, comp_obj) + + def _draw_fill_layout_controls(manager, index, comp_data): layout_mode = comp_data.get('layout_mode', 'manual') is_fill = (layout_mode == 'fill') changed, new_fill = imgui.checkbox("填充父级", is_fill) if changed: comp_data['layout_mode'] = 'fill' if new_fill else 'manual' if new_fill: - # Fill overrides anchor positioning comp_data['anchored_to_parent'] = False comp_data.setdefault('fill_margin_left', 0.0) comp_data.setdefault('fill_margin_right', 0.0) @@ -515,172 +555,185 @@ class LUIFunctionPropertiesMixin: comp_data.setdefault('fill_margin_bottom', 0.0) if hasattr(manager, '_apply_fill_layout'): manager._apply_fill_layout(index) - is_fill = (comp_data.get('layout_mode') == 'fill') - if is_fill: - imgui.text_disabled("填充模式下,位置/尺寸由父级决定") - imgui.text("边距") - m_left = float(comp_data.get('fill_margin_left', 0.0)) - m_right = float(comp_data.get('fill_margin_right', 0.0)) - m_top = float(comp_data.get('fill_margin_top', 0.0)) - m_bottom = float(comp_data.get('fill_margin_bottom', 0.0)) - changed_l, new_l = imgui.input_float("左", m_left, 1.0, 10.0, "%.1f") - changed_r, new_r = imgui.input_float("右", m_right, 1.0, 10.0, "%.1f") - changed_t, new_t = imgui.input_float("上", m_top, 1.0, 10.0, "%.1f") - changed_b, new_b = imgui.input_float("下", m_bottom, 1.0, 10.0, "%.1f") - if changed_l or changed_r or changed_t or changed_b: - comp_data['fill_margin_left'] = new_l - comp_data['fill_margin_right'] = new_r - comp_data['fill_margin_top'] = new_t - comp_data['fill_margin_bottom'] = new_b - if hasattr(manager, '_apply_fill_layout'): - manager._apply_fill_layout(index) - imgui.separator() - # 位置属性 + is_fill = (comp_data.get('layout_mode') == 'fill') + if not is_fill: + return False + + imgui.text_disabled("填充模式下,位置/尺寸由父级决定") + imgui.text("边距") + m_left = float(comp_data.get('fill_margin_left', 0.0)) + m_right = float(comp_data.get('fill_margin_right', 0.0)) + m_top = float(comp_data.get('fill_margin_top', 0.0)) + m_bottom = float(comp_data.get('fill_margin_bottom', 0.0)) + changed_l, new_l = imgui.input_float("左", m_left, 1.0, 10.0, "%.1f") + changed_r, new_r = imgui.input_float("右", m_right, 1.0, 10.0, "%.1f") + changed_t, new_t = imgui.input_float("上", m_top, 1.0, 10.0, "%.1f") + changed_b, new_b = imgui.input_float("下", m_bottom, 1.0, 10.0, "%.1f") + if changed_l or changed_r or changed_t or changed_b: + comp_data['fill_margin_left'] = new_l + comp_data['fill_margin_right'] = new_r + comp_data['fill_margin_top'] = new_t + comp_data['fill_margin_bottom'] = new_b + if hasattr(manager, '_apply_fill_layout'): + manager._apply_fill_layout(index) + return True + + def _draw_position_controls(manager, comp_data, comp_obj, is_fill): imgui.text("位置") if is_fill: imgui.text(f"Left: {comp_data.get('left', 0.0):.1f}") imgui.text(f"Top: {comp_data.get('top', 0.0):.1f}") - else: - if 'left' in comp_data: - changed, new_left = imgui.input_float("Left", comp_data['left'], 1.0, 10.0, "%.1f") - if changed: - comp_data['left'] = new_left - comp_obj.left = new_left + return - if 'top' in comp_data: - changed, new_top = imgui.input_float("Top", comp_data['top'], 1.0, 10.0, "%.1f") - if changed: - comp_data['top'] = new_top - comp_obj.top = new_top + if 'left' in comp_data: + changed, new_left = imgui.input_float("Left", comp_data['left'], 1.0, 10.0, "%.1f") + if changed: + comp_data['left'] = new_left + comp_obj.left = new_left - # 尺寸属性 + if 'top' in comp_data: + changed, new_top = imgui.input_float("Top", comp_data['top'], 1.0, 10.0, "%.1f") + if changed: + comp_data['top'] = new_top + comp_obj.top = new_top + + def _draw_size_controls(manager, index, comp_data, comp_obj, comp_type, is_fill): imgui.spacing() imgui.text("尺寸") if is_fill: imgui.text(f"Width: {comp_data.get('width', 0.0):.1f}") imgui.text(f"Height: {comp_data.get('height', 0.0):.1f}") - else: - width_changed = False - height_changed = False - if 'width' in comp_data: - changed, new_width = imgui.input_float("Width", comp_data['width'], 1.0, 10.0, "%.1f") - if changed and new_width > 0: - comp_data['width'] = new_width - width_changed = True - if hasattr(comp_obj, 'width'): - comp_obj.width = new_width - # 同步更新内部 Sprite (针对 Image/Plane/Video) - if 'sprite' in comp_data: - comp_data['sprite'].width = new_width - if comp_type == 'Button' and hasattr(comp_obj, '_apply_stretch_sizes'): - comp_obj._apply_stretch_sizes() - if comp_type == 'Slider' and hasattr(comp_obj, 'set_height'): - comp_obj.set_height(float(comp_data.get('height', 16))) - if comp_type == 'InputField' and hasattr(comp_obj, 'set_height'): - comp_obj.set_height(float(comp_data.get('height', 24))) - if comp_type == 'Slider' and hasattr(comp_obj, 'set_width'): - comp_obj.set_width(new_width) - if comp_type == 'InputField' and hasattr(comp_obj, 'set_width'): - comp_obj.set_width(new_width) + return - if 'height' in comp_data: - changed, new_height = imgui.input_float("Height", comp_data['height'], 1.0, 10.0, "%.1f") - if changed and new_height > 0: - comp_data['height'] = new_height - height_changed = True - if hasattr(comp_obj, 'height'): - comp_obj.height = new_height - # 同步更新内部 Sprite (针对 Image/Plane/Video) - if 'sprite' in comp_data: - comp_data['sprite'].height = new_height - if comp_type == 'Button' and hasattr(comp_obj, '_apply_stretch_sizes'): - comp_obj._apply_stretch_sizes() - if comp_type == 'Slider' and hasattr(comp_obj, 'set_width'): - comp_obj.set_width(float(comp_data.get('width', 200))) - if comp_type == 'InputField' and hasattr(comp_obj, 'set_width'): - comp_obj.set_width(float(comp_data.get('width', 200))) + width_changed = manager.luiFunction._draw_width_control( + manager, comp_data, comp_obj, comp_type + ) + height_changed = manager.luiFunction._draw_height_control( + manager, comp_data, comp_obj, comp_type + ) + manager.luiFunction._post_size_control_sync( + manager, index, comp_data, comp_type, width_changed, height_changed + ) - if width_changed or height_changed: - if comp_type == 'HttpText' and hasattr(manager.luiFunction, 'sync_http_text_layout'): - manager.luiFunction.sync_http_text_layout(manager, comp_data) - if hasattr(manager, '_update_anchored_children'): - manager._update_anchored_children(index) + def _draw_width_control(manager, comp_data, comp_obj, comp_type): + if 'width' not in comp_data: + return False + changed, new_width = imgui.input_float("Width", comp_data['width'], 1.0, 10.0, "%.1f") + if not changed or new_width <= 0: + return False + comp_data['width'] = new_width + if hasattr(comp_obj, 'width'): + comp_obj.width = new_width + if 'sprite' in comp_data: + comp_data['sprite'].width = new_width + if comp_type == 'Button' and hasattr(comp_obj, '_apply_stretch_sizes'): + comp_obj._apply_stretch_sizes() + if comp_type == 'Slider' and hasattr(comp_obj, 'set_height'): + comp_obj.set_height(float(comp_data.get('height', 16))) + if comp_type == 'InputField' and hasattr(comp_obj, 'set_height'): + comp_obj.set_height(float(comp_data.get('height', 24))) + if comp_type == 'Slider' and hasattr(comp_obj, 'set_width'): + comp_obj.set_width(new_width) + if comp_type == 'InputField' and hasattr(comp_obj, 'set_width'): + comp_obj.set_width(new_width) + return True - if comp_type in ['VerticalLayout', 'HorizontalLayout'] and hasattr(manager, '_update_layout_inner'): + def _draw_height_control(manager, comp_data, comp_obj, comp_type): + if 'height' not in comp_data: + return False + changed, new_height = imgui.input_float("Height", comp_data['height'], 1.0, 10.0, "%.1f") + if not changed or new_height <= 0: + return False + + comp_data['height'] = new_height + if hasattr(comp_obj, 'height'): + comp_obj.height = new_height + if 'sprite' in comp_data: + comp_data['sprite'].height = new_height + if comp_type == 'Button' and hasattr(comp_obj, '_apply_stretch_sizes'): + comp_obj._apply_stretch_sizes() + if comp_type == 'Slider' and hasattr(comp_obj, 'set_width'): + comp_obj.set_width(float(comp_data.get('width', 200))) + if comp_type == 'InputField' and hasattr(comp_obj, 'set_width'): + comp_obj.set_width(float(comp_data.get('width', 200))) + return True + + def _post_size_control_sync(manager, index, comp_data, comp_type, width_changed, height_changed): + if width_changed or height_changed: + if comp_type == 'HttpText' and hasattr(manager.luiFunction, 'sync_http_text_layout'): + manager.luiFunction.sync_http_text_layout(manager, comp_data) + if hasattr(manager, '_update_anchored_children'): + manager._update_anchored_children(index) + + if comp_type in ['VerticalLayout', 'HorizontalLayout'] and hasattr(manager, '_update_layout_inner'): + manager._update_layout_inner(index) + + def _draw_layout_group_controls(manager, index, comp_data, comp_type): + if comp_type not in ['VerticalLayout', 'HorizontalLayout']: + return + + imgui.spacing() + imgui.text("Layout Group") + spacing = float(comp_data.get('layout_spacing', 0.0)) + changed, new_spacing = imgui.input_float("Spacing", spacing, 1.0, 10.0, "%.1f") + if changed: + comp_data['layout_spacing'] = new_spacing + if hasattr(manager, '_update_layout_inner'): manager._update_layout_inner(index) - # Layout group properties - if comp_type in ['VerticalLayout', 'HorizontalLayout']: - imgui.spacing() - imgui.text("Layout Group") + imgui.text("Padding") + pad_left = float(comp_data.get('layout_padding_left', 0.0)) + pad_right = float(comp_data.get('layout_padding_right', 0.0)) + pad_top = float(comp_data.get('layout_padding_top', 0.0)) + pad_bottom = float(comp_data.get('layout_padding_bottom', 0.0)) + changed_l, new_l = imgui.input_float("Pad Left", pad_left, 1.0, 10.0, "%.1f") + changed_r, new_r = imgui.input_float("Pad Right", pad_right, 1.0, 10.0, "%.1f") + changed_t, new_t = imgui.input_float("Pad Top", pad_top, 1.0, 10.0, "%.1f") + changed_b, new_b = imgui.input_float("Pad Bottom", pad_bottom, 1.0, 10.0, "%.1f") + if changed_l or changed_r or changed_t or changed_b: + comp_data['layout_padding_left'] = new_l + comp_data['layout_padding_right'] = new_r + comp_data['layout_padding_top'] = new_t + comp_data['layout_padding_bottom'] = new_b + if hasattr(manager, '_update_layout_inner'): + manager._update_layout_inner(index) - spacing = float(comp_data.get('layout_spacing', 0.0)) - changed, new_spacing = imgui.input_float("Spacing", spacing, 1.0, 10.0, "%.1f") - if changed: - comp_data['layout_spacing'] = new_spacing - if hasattr(manager, '_update_layout_inner'): - manager._update_layout_inner(index) + wrap_enabled = bool(comp_data.get('layout_wrap', True)) + changed_wrap, new_wrap = imgui.checkbox("Wrap", wrap_enabled) + if changed_wrap: + comp_data['layout_wrap'] = new_wrap + if hasattr(manager, '_update_layout_inner'): + manager._update_layout_inner(index) - imgui.text("Padding") - pad_left = float(comp_data.get('layout_padding_left', 0.0)) - pad_right = float(comp_data.get('layout_padding_right', 0.0)) - pad_top = float(comp_data.get('layout_padding_top', 0.0)) - pad_bottom = float(comp_data.get('layout_padding_bottom', 0.0)) + line_spacing = float(comp_data.get('layout_line_spacing', 0.0)) + changed_line, new_line_spacing = imgui.input_float("Line Spacing (Wrap)", line_spacing, 1.0, 10.0, "%.1f") + if changed_line: + comp_data['layout_line_spacing'] = new_line_spacing + if hasattr(manager, '_update_layout_inner'): + manager._update_layout_inner(index) - changed_l, new_l = imgui.input_float("Pad Left", pad_left, 1.0, 10.0, "%.1f") - changed_r, new_r = imgui.input_float("Pad Right", pad_right, 1.0, 10.0, "%.1f") - changed_t, new_t = imgui.input_float("Pad Top", pad_top, 1.0, 10.0, "%.1f") - changed_b, new_b = imgui.input_float("Pad Bottom", pad_bottom, 1.0, 10.0, "%.1f") - if changed_l or changed_r or changed_t or changed_b: - comp_data['layout_padding_left'] = new_l - comp_data['layout_padding_right'] = new_r - comp_data['layout_padding_top'] = new_t - comp_data['layout_padding_bottom'] = new_b - if hasattr(manager, '_update_layout_inner'): - manager._update_layout_inner(index) - - align_options = ["start", "center", "end", "stretch"] - - if comp_type in ['HorizontalLayout', 'VerticalLayout']: - wrap_enabled = bool(comp_data.get('layout_wrap', True)) - changed, new_wrap = imgui.checkbox("Wrap", wrap_enabled) - if changed: - comp_data['layout_wrap'] = new_wrap + align_options = ["start", "center", "end", "stretch"] + current_align = comp_data.get('layout_align', 'start') + if imgui.begin_combo("Align", current_align): + for opt in align_options: + if imgui.selectable(opt, current_align == opt)[0]: + comp_data['layout_align'] = opt if hasattr(manager, '_update_layout_inner'): manager._update_layout_inner(index) + imgui.end_combo() - line_spacing = float(comp_data.get('layout_line_spacing', 0.0)) - changed, new_line_spacing = imgui.input_float("Line Spacing (Wrap)", line_spacing, 1.0, 10.0, "%.1f") - if changed: - comp_data['layout_line_spacing'] = new_line_spacing - if hasattr(manager, '_update_layout_inner'): - manager._update_layout_inner(index) - - current_align = comp_data.get('layout_align', 'start') - if imgui.begin_combo("Align", current_align): - for opt in align_options: - if imgui.selectable(opt, current_align == opt)[0]: - comp_data['layout_align'] = opt - if hasattr(manager, '_update_layout_inner'): - manager._update_layout_inner(index) - imgui.end_combo() - - # --- 层级与显示顺序 (LUI 中组件层级由 Z-Offset 控制) --- + def _draw_z_offset_controls(manager, index, comp_data, comp_obj): imgui.spacing() imgui.text("层级与显示顺序") - - # 获取当前深度并确保其在 comp_data 中 current_z = comp_data.get('z_offset') if current_z is None: - # 兼容旧组件的 sort 属性,如果存在则作为初始 z_offset current_z = comp_data.get('sort', 0.0) if current_z == 0 and hasattr(comp_obj, 'z_offset'): - current_z = comp_obj.z_offset + current_z = comp_obj.z_offset comp_data['z_offset'] = float(current_z) - - # 1. 整数层级设置 (方便快速调整) + layer_val = int(current_z) changed_layer, new_layer = imgui.input_int("渲染层级 (Layer)", layer_val) if changed_layer: @@ -689,7 +742,6 @@ class LUIFunctionPropertiesMixin: comp_obj.set_z_offset(float(new_layer)) print(f"✓ 组件 #{index} 层级已映射到 Z-Offset: {new_layer}") - # 2. 深度微调 (Z-Offset 浮点数) changed_z, new_z = imgui.input_float("深度微调 (Z-Offset)", comp_data['z_offset'], 0.1, 1.0, "%.2f") if changed_z: comp_data['z_offset'] = new_z @@ -698,16 +750,242 @@ class LUIFunctionPropertiesMixin: elif hasattr(comp_obj, 'z_offset'): comp_obj.z_offset = new_z print(f"✓ 组件 #{index} Z-Offset 已微调为: {new_z}") - imgui.text_disabled("(注: LUI 内部组件通过 Z-Offset 决定遮挡关系)") # 特定类型的属性 - if comp_type == 'InputField': - imgui.spacing() - imgui.text("输入框属性") - imgui.text("输入框图片") - if imgui.button("更改图片##input_img", (120, 20)): + def _draw_type_specific_properties(manager, index, comp_data, comp_obj, comp_type): + if comp_type == 'InputField': + manager.luiFunction._draw_type_props_input_field(manager, index, comp_data, comp_obj, comp_type) + return + if comp_type == 'Slider': + manager.luiFunction._draw_type_props_slider(manager, index, comp_data, comp_obj, comp_type) + return + if comp_type == 'Checkbox': + manager.luiFunction._draw_type_props_checkbox(manager, index, comp_data, comp_obj, comp_type) + return + if comp_type in ['Plane', 'Image']: + manager.luiFunction._draw_type_props_plane_image(manager, index, comp_data, comp_obj, comp_type) + return + if comp_type == 'Frame': + manager.luiFunction._draw_type_props_frame(manager, index, comp_data, comp_obj, comp_type) + return + if comp_type == 'Selectbox': + manager.luiFunction._draw_type_props_selectbox(manager, index, comp_data, comp_obj, comp_type) + return + if comp_type == 'HttpText': + manager.luiFunction._draw_type_props_http_text(manager, index, comp_data, comp_obj, comp_type) + return + if comp_type == 'Video': + manager.luiFunction._draw_type_props_video(manager, index, comp_data, comp_obj, comp_type) + return + + def _draw_type_props_input_field(manager, index, comp_data, comp_obj, comp_type): + imgui.spacing() + imgui.text("输入框属性") + + imgui.text("输入框图片") + if imgui.button("更改图片##input_img", (120, 20)): + selected_path = manager._change_image_texture( + title = "选择图片文件", + filetypes = [ + ("图片文件", "*.png;*.jpg;*.jpeg;*.bmp;*.gif;*.tga"), + ("PNG", "*.png"), + ("JPEG", "*.jpg;*.jpeg"), + ("所有文件", "*.*") + ] + ) + if selected_path: + try: + atlas_result = manager.luiFunction._add_image_to_atlas(manager, selected_path, atlas_size=2048) + if atlas_result: + if len(atlas_result) >= 7: + tex, u0, v0, u1, v1, w, h = atlas_result[:7] + else: + tex, u0, v0, u1, v1 = atlas_result + w = int(max(1, round((u1 - u0) * tex.getXSize()))) + h = int(max(1, round((v1 - v0) * tex.getYSize()))) + comp_data['input_texture_path'] = selected_path + comp_data['input_atlas_uv'] = (u0, v0, u1, v1) + comp_data['input_texture_ref'] = tex + comp_data['input_image_size'] = (int(w), int(h)) + layout = getattr(comp_obj, '_layout', None) + if layout is not None: + for attr in ('_sprite_left', '_sprite_mid', '_sprite_right'): + spr = getattr(layout, attr, None) + if spr is not None and hasattr(spr, 'set_texture'): + spr.set_texture(tex, resize=False) + if hasattr(spr, 'set_uv_range'): + spr.set_uv_range(u0, v0, u1, v1) + except Exception as e: + print(f"InputField texture set failed: {e}") + + if imgui.button("使用图片尺寸##input_fit", (120, 20)): + w = h = None + if 'input_image_size' in comp_data and comp_data['input_image_size']: + try: + w, h = comp_data['input_image_size'] + except Exception: + w = h = None + elif 'input_atlas_uv' in comp_data and 'input_texture_ref' in comp_data and comp_data['input_texture_ref']: + try: + u0, v0, u1, v1 = comp_data['input_atlas_uv'] + tex = comp_data['input_texture_ref'] + tw = tex.getXSize() if hasattr(tex, 'getXSize') else 0 + th = tex.getYSize() if hasattr(tex, 'getYSize') else 0 + w = int(max(1, round((u1 - u0) * tw))) + h = int(max(1, round((v1 - v0) * th))) + except Exception: + w = h = None + if w is not None and h is not None and w > 0 and h > 0: + comp_data['width'] = float(w) + comp_data['height'] = float(h) + comp_obj.width = float(w) + comp_obj.height = float(h) + if hasattr(comp_obj, 'set_width'): + comp_obj.set_width(float(w)) + if hasattr(comp_obj, 'set_height'): + comp_obj.set_height(float(h)) + layout = getattr(comp_obj, '_layout', None) + if layout is not None: + if hasattr(layout, 'width'): + layout.width = "100%" + if hasattr(layout, 'height'): + layout.height = "100%" + + imgui.text("输入框颜色") + in_color = comp_data.get('input_color', (1.0, 1.0, 1.0, 1.0)) + in_color = list(in_color) if isinstance(in_color, tuple) else in_color + changed, new_in_color = imgui.color_edit4("##input_color", in_color) + if changed: + comp_data['input_color'] = tuple(new_in_color) + layout = getattr(comp_obj, '_layout', None) + if layout is not None: + for attr in ('_sprite_left', '_sprite_mid', '_sprite_right'): + spr = getattr(layout, attr, None) + if spr is not None: + spr.color = tuple(new_in_color) + + if 'value' in comp_data: + changed, new_value = imgui.input_text("当前值", comp_data['value'], 256) + if changed: + comp_data['value'] = new_value + comp_obj.value = new_value + + def _draw_type_props_slider(manager, index, comp_data, comp_obj, comp_type): + imgui.spacing() + imgui.text("滑块属性") + if 'value' in comp_data: + min_val = comp_data.get('min_value', 0.0) + max_val = comp_data.get('max_value', 100.0) + changed, new_value = imgui.slider_float("当前值", comp_data['value'], min_val, max_val, "%.1f") + if changed: + comp_data['value'] = new_value + if hasattr(comp_obj, 'set_value'): + comp_obj.set_value(new_value) + + def _draw_type_props_checkbox(manager, index, comp_data, comp_obj, comp_type): + imgui.spacing() + imgui.text("复选框属性") + if 'text' in comp_data: + changed, new_text = imgui.input_text("标签文本", comp_data['text'], 256) + if changed: + comp_data['text'] = new_text + # Checkbox uses a child label for text + if hasattr(comp_obj, 'label') and hasattr(comp_obj.label, 'text'): + comp_obj.label.text = new_text + elif hasattr(comp_obj, 'text'): + comp_obj.text = new_text + + if 'checked' in comp_data: + changed, new_checked = imgui.checkbox("选中状态", comp_data['checked']) + if changed: + comp_data['checked'] = new_checked + if hasattr(comp_obj, 'checked'): + comp_obj.checked = new_checked + + def _draw_type_props_plane_image(manager, index, comp_data, comp_obj, comp_type): + imgui.spacing() + imgui.text(f"{comp_type}属性") + + # 颜色属性 + if 'color' in comp_data: + color = comp_data.get('color', (1.0, 1.0, 1.0, 1.0)) + color = list(color) if isinstance(color, tuple) else color + changed, new_color = imgui.color_edit4("颜色", color) + if changed: + comp_data['color'] = tuple(new_color) + # 更新sprite颜色 + if 'sprite' in comp_data: + comp_data['sprite'].color = tuple(new_color) + + # 如果是Image,显示纹理路径 + if comp_type == 'Image' and 'texture_path' in comp_data: + # Fit size to image texture + if imgui.button("使用图片尺寸##fit_image"): + tex = None + # Prefer direct texture reference if available + if 'texture_ref' in comp_data and comp_data['texture_ref']: + tex = comp_data['texture_ref'] + elif 'texture' in comp_data and comp_data['texture']: + tex = comp_data['texture'] + else: + # Try to read from sprite python tag if present + try: + target = comp_data.get('sprite', comp_obj) + if target is not None and hasattr(target, 'get_python_tag'): + tex = target.get_python_tag('texture_ref') + except Exception: + tex = None + + w = h = None + if 'image_size' in comp_data and comp_data['image_size']: + try: + w, h = comp_data['image_size'] + except Exception: + w = h = None + elif 'atlas_uv' in comp_data and 'atlas_tex' in comp_data and comp_data['atlas_tex']: + try: + u0, v0, u1, v1 = comp_data['atlas_uv'] + atlas_tex = comp_data['atlas_tex'] + atlas_w = atlas_tex.getXSize() if hasattr(atlas_tex, 'getXSize') else 0 + atlas_h = atlas_tex.getYSize() if hasattr(atlas_tex, 'getYSize') else 0 + w = int(max(1, round((u1 - u0) * atlas_w))) + h = int(max(1, round((v1 - v0) * atlas_h))) + except Exception: + w = h = None + elif tex is not None: + if hasattr(tex, 'getOrigFileXSize') and hasattr(tex, 'getOrigFileYSize'): + ow = tex.getOrigFileXSize() + oh = tex.getOrigFileYSize() + if ow and oh: + w, h = ow, oh + if w is None or h is None: + if hasattr(tex, 'getXSize') and hasattr(tex, 'getYSize'): + w = tex.getXSize() + h = tex.getYSize() + + if w is not None and h is not None and w > 0 and h > 0: + comp_data['width'] = float(w) + comp_data['height'] = float(h) + comp_obj.width = float(w) + comp_obj.height = float(h) + # Sync sprite size (Image/Plane/Video) + if 'sprite' in comp_data and comp_data['sprite']: + spr = comp_data['sprite'] + if hasattr(spr, 'set_size'): + spr.set_size(float(w), float(h)) + else: + spr.width = float(w) + spr.height = float(h) + if hasattr(manager, '_hide_resize_handles'): + manager._hide_resize_handles() + else: + print("Image size not available: missing texture") + + imgui.text(f"纹理路径: {comp_data['texture_path']}") + if imgui.button("更改纹理", (100, 20)): + # 实现纹理更改功能 selected_path = manager._change_image_texture( title = "选择图片文件", filetypes = [ @@ -718,8 +996,10 @@ class LUIFunctionPropertiesMixin: ] ) if selected_path: + new_path = selected_path + comp_data['texture_path'] = new_path try: - atlas_result = manager.luiFunction._add_image_to_atlas(manager, selected_path, atlas_size=2048) + atlas_result = manager.luiFunction._add_image_to_atlas(manager, new_path, atlas_size=2048) if atlas_result: if len(atlas_result) >= 7: tex, u0, v0, u1, v1, w, h = atlas_result[:7] @@ -727,817 +1007,608 @@ class LUIFunctionPropertiesMixin: tex, u0, v0, u1, v1 = atlas_result w = int(max(1, round((u1 - u0) * tex.getXSize()))) h = int(max(1, round((v1 - v0) * tex.getYSize()))) - comp_data['input_texture_path'] = selected_path - comp_data['input_atlas_uv'] = (u0, v0, u1, v1) - comp_data['input_texture_ref'] = tex - comp_data['input_image_size'] = (int(w), int(h)) - layout = getattr(comp_obj, '_layout', None) - if layout is not None: - for attr in ('_sprite_left', '_sprite_mid', '_sprite_right'): - spr = getattr(layout, attr, None) - if spr is not None and hasattr(spr, 'set_texture'): - spr.set_texture(tex, resize=False) - if hasattr(spr, 'set_uv_range'): - spr.set_uv_range(u0, v0, u1, v1) - except Exception as e: - print(f"InputField texture set failed: {e}") - - if imgui.button("使用图片尺寸##input_fit", (120, 20)): - w = h = None - if 'input_image_size' in comp_data and comp_data['input_image_size']: - try: - w, h = comp_data['input_image_size'] - except Exception: - w = h = None - elif 'input_atlas_uv' in comp_data and 'input_texture_ref' in comp_data and comp_data['input_texture_ref']: - try: - u0, v0, u1, v1 = comp_data['input_atlas_uv'] - tex = comp_data['input_texture_ref'] - tw = tex.getXSize() if hasattr(tex, 'getXSize') else 0 - th = tex.getYSize() if hasattr(tex, 'getYSize') else 0 - w = int(max(1, round((u1 - u0) * tw))) - h = int(max(1, round((v1 - v0) * th))) - except Exception: - w = h = None - if w is not None and h is not None and w > 0 and h > 0: - comp_data['width'] = float(w) - comp_data['height'] = float(h) - comp_obj.width = float(w) - comp_obj.height = float(h) - if hasattr(comp_obj, 'set_width'): - comp_obj.set_width(float(w)) - if hasattr(comp_obj, 'set_height'): - comp_obj.set_height(float(h)) - layout = getattr(comp_obj, '_layout', None) - if layout is not None: - if hasattr(layout, 'width'): - layout.width = "100%" - if hasattr(layout, 'height'): - layout.height = "100%" - - imgui.text("输入框颜色") - in_color = comp_data.get('input_color', (1.0, 1.0, 1.0, 1.0)) - in_color = list(in_color) if isinstance(in_color, tuple) else in_color - changed, new_in_color = imgui.color_edit4("##input_color", in_color) - if changed: - comp_data['input_color'] = tuple(new_in_color) - layout = getattr(comp_obj, '_layout', None) - if layout is not None: - for attr in ('_sprite_left', '_sprite_mid', '_sprite_right'): - spr = getattr(layout, attr, None) - if spr is not None: - spr.color = tuple(new_in_color) - - if 'value' in comp_data: - changed, new_value = imgui.input_text("当前值", comp_data['value'], 256) - if changed: - comp_data['value'] = new_value - comp_obj.value = new_value - - elif comp_type == 'Slider': - imgui.spacing() - imgui.text("滑块属性") - if 'value' in comp_data: - min_val = comp_data.get('min_value', 0.0) - max_val = comp_data.get('max_value', 100.0) - changed, new_value = imgui.slider_float("当前值", comp_data['value'], min_val, max_val, "%.1f") - if changed: - comp_data['value'] = new_value - if hasattr(comp_obj, 'set_value'): - comp_obj.set_value(new_value) - - elif comp_type == 'Checkbox': - imgui.spacing() - imgui.text("复选框属性") - if 'text' in comp_data: - changed, new_text = imgui.input_text("标签文本", comp_data['text'], 256) - if changed: - comp_data['text'] = new_text - # Checkbox uses a child label for text - if hasattr(comp_obj, 'label') and hasattr(comp_obj.label, 'text'): - comp_obj.label.text = new_text - elif hasattr(comp_obj, 'text'): - comp_obj.text = new_text - - if 'checked' in comp_data: - changed, new_checked = imgui.checkbox("选中状态", comp_data['checked']) - if changed: - comp_data['checked'] = new_checked - if hasattr(comp_obj, 'checked'): - comp_obj.checked = new_checked - - elif comp_type in ['Plane', 'Image']: - imgui.spacing() - imgui.text(f"{comp_type}属性") - - # 颜色属性 - if 'color' in comp_data: - color = comp_data.get('color', (1.0, 1.0, 1.0, 1.0)) - color = list(color) if isinstance(color, tuple) else color - changed, new_color = imgui.color_edit4("颜色", color) - if changed: - comp_data['color'] = tuple(new_color) - # 更新sprite颜色 - if 'sprite' in comp_data: - comp_data['sprite'].color = tuple(new_color) - - # 如果是Image,显示纹理路径 - if comp_type == 'Image' and 'texture_path' in comp_data: - # Fit size to image texture - if imgui.button("使用图片尺寸##fit_image"): - tex = None - # Prefer direct texture reference if available - if 'texture_ref' in comp_data and comp_data['texture_ref']: - tex = comp_data['texture_ref'] - elif 'texture' in comp_data and comp_data['texture']: - tex = comp_data['texture'] - else: - # Try to read from sprite python tag if present - try: - target = comp_data.get('sprite', comp_obj) - if target is not None and hasattr(target, 'get_python_tag'): - tex = target.get_python_tag('texture_ref') - except Exception: - tex = None - - w = h = None - if 'image_size' in comp_data and comp_data['image_size']: - try: - w, h = comp_data['image_size'] - except Exception: - w = h = None - elif 'atlas_uv' in comp_data and 'atlas_tex' in comp_data and comp_data['atlas_tex']: - try: - u0, v0, u1, v1 = comp_data['atlas_uv'] - atlas_tex = comp_data['atlas_tex'] - atlas_w = atlas_tex.getXSize() if hasattr(atlas_tex, 'getXSize') else 0 - atlas_h = atlas_tex.getYSize() if hasattr(atlas_tex, 'getYSize') else 0 - w = int(max(1, round((u1 - u0) * atlas_w))) - h = int(max(1, round((v1 - v0) * atlas_h))) - except Exception: - w = h = None - elif tex is not None: - if hasattr(tex, 'getOrigFileXSize') and hasattr(tex, 'getOrigFileYSize'): - ow = tex.getOrigFileXSize() - oh = tex.getOrigFileYSize() - if ow and oh: - w, h = ow, oh - if w is None or h is None: - if hasattr(tex, 'getXSize') and hasattr(tex, 'getYSize'): - w = tex.getXSize() - h = tex.getYSize() - - if w is not None and h is not None and w > 0 and h > 0: - comp_data['width'] = float(w) - comp_data['height'] = float(h) - comp_obj.width = float(w) - comp_obj.height = float(h) - # Sync sprite size (Image/Plane/Video) - if 'sprite' in comp_data and comp_data['sprite']: - spr = comp_data['sprite'] - if hasattr(spr, 'set_size'): - spr.set_size(float(w), float(h)) - else: - spr.width = float(w) - spr.height = float(h) - if hasattr(manager, '_hide_resize_handles'): - manager._hide_resize_handles() - else: - print("Image size not available: missing texture") - - imgui.text(f"纹理路径: {comp_data['texture_path']}") - if imgui.button("更改纹理", (100, 20)): - # 实现纹理更改功能 - selected_path = manager._change_image_texture( - title = "选择图片文件", - filetypes = [ - ("图片文件", "*.png;*.jpg;*.jpeg;*.bmp;*.gif;*.tga"), - ("PNG", "*.png"), - ("JPEG", "*.jpg;*.jpeg"), - ("所有文件", "*.*") - ] - ) - if selected_path: - new_path = selected_path - comp_data['texture_path'] = new_path - try: - atlas_result = manager.luiFunction._add_image_to_atlas(manager, new_path, atlas_size=2048) - if atlas_result: - if len(atlas_result) >= 7: - tex, u0, v0, u1, v1, w, h = atlas_result[:7] - else: - tex, u0, v0, u1, v1 = atlas_result - w = int(max(1, round((u1 - u0) * tex.getXSize()))) - h = int(max(1, round((v1 - v0) * tex.getYSize()))) - print(f"? Texture loaded: {tex.getName()}, Size: {tex.getXSize()}x{tex.getYSize()}") - comp_data["current_texture"] = os.path.basename(new_path) - comp_data["atlas_uv"] = (u0, v0, u1, v1) - comp_data["atlas_tex"] = tex - comp_data["image_size"] = (int(w), int(h)) - old_spr = comp_data.get("sprite") - if old_spr: - try: - old_spr.parent = None - if hasattr(old_spr, "set_texture"): old_spr.set_texture(None) - if hasattr(old_spr, "destroy"): old_spr.destroy() - except: - pass + print(f"? Texture loaded: {tex.getName()}, Size: {tex.getXSize()}x{tex.getYSize()}") + comp_data["current_texture"] = os.path.basename(new_path) + comp_data["atlas_uv"] = (u0, v0, u1, v1) + comp_data["atlas_tex"] = tex + comp_data["image_size"] = (int(w), int(h)) + old_spr = comp_data.get("sprite") + if old_spr: try: - new_spr = LUISprite(comp_obj, tex) - if hasattr(new_spr, "set_texture"): - new_spr.set_texture(tex, resize=False) - if hasattr(new_spr, "set_uv_range"): - new_spr.set_uv_range(u0, v0, u1, v1) - new_spr.width = comp_data.get("width", 100) - new_spr.height = comp_data.get("height", 100) - new_spr.left = 0 - new_spr.top = 0 - new_spr.z_offset = comp_data.get("z_offset", 0) - new_spr.color = comp_data.get("color", (1,1,1,1)) - if hasattr(new_spr, "set_uv_range"): - new_spr.set_uv_range(u0, v0, u1, v1) - comp_data["sprite"] = new_spr - comp_data["texture_ref"] = tex - if not hasattr(manager, "texture_refs"): - manager.texture_refs = [] - manager.texture_refs.append(tex) - if hasattr(new_spr, "set_python_tag"): - new_spr.set_python_tag("texture_ref", tex) - if hasattr(comp_obj, "set_python_tag"): - comp_obj.set_python_tag("texture_ref", tex) - print(f"? Replaced sprite with atlas texture image: {tex.getName()}, Size: {tex.getXSize()}x{tex.getYSize()}") - except Exception as ex: - print(f"? Failed to create new sprite: {ex}") - target = comp_data.get("widget", comp_data.get("object")) - if hasattr(target, "set_texture"): - target.set_texture(tex) - else: - print(f"? Texture loading returned None for: {new_path}") - except Exception as e: - print(f"Image texture update failed: {e}") - if 'sprite' in comp_data: - sprite = comp_data['sprite'] - imgui.text(f"当前纹理: {comp_data.get('current_texture', 'blank')}") + old_spr.parent = None + if hasattr(old_spr, "set_texture"): old_spr.set_texture(None) + if hasattr(old_spr, "destroy"): old_spr.destroy() + except: + pass + try: + new_spr = LUISprite(comp_obj, tex) + if hasattr(new_spr, "set_texture"): + new_spr.set_texture(tex, resize=False) + if hasattr(new_spr, "set_uv_range"): + new_spr.set_uv_range(u0, v0, u1, v1) + new_spr.width = comp_data.get("width", 100) + new_spr.height = comp_data.get("height", 100) + new_spr.left = 0 + new_spr.top = 0 + new_spr.z_offset = comp_data.get("z_offset", 0) + new_spr.color = comp_data.get("color", (1,1,1,1)) + if hasattr(new_spr, "set_uv_range"): + new_spr.set_uv_range(u0, v0, u1, v1) + comp_data["sprite"] = new_spr + comp_data["texture_ref"] = tex + if not hasattr(manager, "texture_refs"): + manager.texture_refs = [] + manager.texture_refs.append(tex) + if hasattr(new_spr, "set_python_tag"): + new_spr.set_python_tag("texture_ref", tex) + if hasattr(comp_obj, "set_python_tag"): + comp_obj.set_python_tag("texture_ref", tex) + print(f"? Replaced sprite with atlas texture image: {tex.getName()}, Size: {tex.getXSize()}x{tex.getYSize()}") + except Exception as ex: + print(f"? Failed to create new sprite: {ex}") + target = comp_data.get("widget", comp_data.get("object")) + if hasattr(target, "set_texture"): + target.set_texture(tex) + else: + print(f"? Texture loading returned None for: {new_path}") + except Exception as e: + print(f"Image texture update failed: {e}") + if 'sprite' in comp_data: + sprite = comp_data['sprite'] + imgui.text(f"当前纹理: {comp_data.get('current_texture', 'blank')}") + + def _draw_type_props_frame(manager, index, comp_data, comp_obj, comp_type): + imgui.spacing() + imgui.text("框架属性") - elif comp_type == 'Frame': - imgui.spacing() - imgui.text("框架属性") - - # 框架特有的颜色属性 - if 'color' in comp_data: - color = comp_data.get('color', (0.7, 0.7, 0.7, 0.8)) - color = list(color) if isinstance(color, tuple) else color - changed, new_color = imgui.color_edit4("背景颜色", color) - if changed: - comp_data['color'] = tuple(new_color) - # 更新Frame颜色 - if hasattr(comp_obj, 'set_color'): - comp_obj.set_color(tuple(new_color)) - - elif comp_type == 'Selectbox': - imgui.spacing() - imgui.text("Selectbox Options") - imgui.separator() + # 框架特有的颜色属性 + if 'color' in comp_data: + color = comp_data.get('color', (0.7, 0.7, 0.7, 0.8)) + color = list(color) if isinstance(color, tuple) else color + changed, new_color = imgui.color_edit4("背景颜色", color) + if changed: + comp_data['color'] = tuple(new_color) + # 更新Frame颜色 + if hasattr(comp_obj, 'set_color'): + comp_obj.set_color(tuple(new_color)) + + def _draw_type_props_selectbox(manager, index, comp_data, comp_obj, comp_type): + imgui.spacing() + imgui.text("Selectbox Options") + imgui.separator() - options = comp_data.get('options', []) or [] - # normalize to list of (id,label) - normalized = [] - for i, opt in enumerate(options): - try: - opt_id, opt_label = opt - except Exception: - opt_id, opt_label = i, str(opt) - normalized.append((opt_id, str(opt_label))) - options = normalized - comp_data['options'] = options - - current_selected = comp_data.get('selected_option_id') - if hasattr(comp_obj, 'get_selected_option'): - try: - current_selected = comp_obj.get_selected_option() - except Exception: - pass - - opt_labels = [label for _, label in options] if options else ["(empty)"] - opt_ids = [oid for oid, _ in options] if options else [None] + options = comp_data.get('options', []) or [] + # normalize to list of (id,label) + normalized = [] + for i, opt in enumerate(options): try: - current_index = opt_ids.index(current_selected) + opt_id, opt_label = opt except Exception: - current_index = 0 + opt_id, opt_label = i, str(opt) + normalized.append((opt_id, str(opt_label))) + options = normalized + comp_data['options'] = options - changed_sel, new_index = imgui.combo("Selected", current_index, opt_labels) - if changed_sel and options: - sel_id = opt_ids[new_index] - comp_data['selected_option_id'] = sel_id - try: - if hasattr(comp_obj, '_select_option'): - comp_obj._select_option(sel_id) - except Exception: - pass + current_selected = comp_data.get('selected_option_id') + if hasattr(comp_obj, 'get_selected_option'): + try: + current_selected = comp_obj.get_selected_option() + except Exception: + pass - imgui.spacing() - imgui.text("Edit Options") - imgui.separator() + opt_labels = [label for _, label in options] if options else ["(empty)"] + opt_ids = [oid for oid, _ in options] if options else [None] + try: + current_index = opt_ids.index(current_selected) + except Exception: + current_index = 0 - new_labels = [] - dirty = False - for i, (opt_id, opt_label) in enumerate(options): - imgui.push_item_width(-40) - changed, new_label = imgui.input_text(f"##opt_label_{i}", opt_label, 128) - imgui.pop_item_width() - if changed: - opt_label = new_label - dirty = True - imgui.same_line() - if imgui.button(f"Delete##opt_del_{i}", (60, 20)): - dirty = True - continue - new_labels.append(opt_label) + changed_sel, new_index = imgui.combo("Selected", current_index, opt_labels) + if changed_sel and options: + sel_id = opt_ids[new_index] + comp_data['selected_option_id'] = sel_id + try: + if hasattr(comp_obj, '_select_option'): + comp_obj._select_option(sel_id) + except Exception: + pass - if imgui.button("Add Option", (100, 24)): - new_labels.append(f"Option {len(new_labels)+1}") + imgui.spacing() + imgui.text("Edit Options") + imgui.separator() + + new_labels = [] + dirty = False + for i, (opt_id, opt_label) in enumerate(options): + imgui.push_item_width(-40) + changed, new_label = imgui.input_text(f"##opt_label_{i}", opt_label, 128) + imgui.pop_item_width() + if changed: + opt_label = new_label dirty = True + imgui.same_line() + if imgui.button(f"Delete##opt_del_{i}", (60, 20)): + dirty = True + continue + new_labels.append(opt_label) - if dirty: - # rebuild options with sequential ids to avoid duplicates - options = [(i, label) for i, label in enumerate(new_labels)] - comp_data['options'] = options - # keep selection if possible - if options: - if current_selected not in [oid for oid, _ in options]: - current_selected = options[0][0] - comp_data['selected_option_id'] = current_selected - else: - comp_data['selected_option_id'] = None + if imgui.button("Add Option", (100, 24)): + new_labels.append(f"Option {len(new_labels)+1}") + dirty = True + + if dirty: + # rebuild options with sequential ids to avoid duplicates + options = [(i, label) for i, label in enumerate(new_labels)] + comp_data['options'] = options + # keep selection if possible + if options: + if current_selected not in [oid for oid, _ in options]: + current_selected = options[0][0] + comp_data['selected_option_id'] = current_selected + else: + comp_data['selected_option_id'] = None + try: + if hasattr(comp_obj, 'set_options'): + comp_obj.set_options(options) + elif hasattr(comp_obj, 'options'): + comp_obj.options = options + if options and hasattr(comp_obj, '_select_option'): + comp_obj._select_option(comp_data['selected_option_id']) + except Exception as e: + print(f"Selectbox options update failed: {e}") + + def _draw_type_props_http_text(manager, index, comp_data, comp_obj, comp_type): + imgui.spacing() + imgui.text("HTTP通信") + imgui.separator() + + http_url = str(comp_data.get('http_url', '')) + changed_url, new_url = imgui.input_text("URL", http_url, 512) + if changed_url: + comp_data['http_url'] = new_url + + method_options = ["GET", "POST"] + curr_method = str(comp_data.get('http_method', 'GET')).upper() + curr_method_idx = 1 if curr_method == "POST" else 0 + changed_method, new_method_idx = imgui.combo("Method", curr_method_idx, method_options) + if changed_method: + comp_data['http_method'] = method_options[new_method_idx] + + timeout_val = float(comp_data.get('http_timeout', 8.0)) + changed_timeout, new_timeout = imgui.input_float("Timeout(s)", timeout_val, 1.0, 5.0, "%.1f") + if changed_timeout: + comp_data['http_timeout'] = max(1.0, new_timeout) + + auto_refresh = bool(comp_data.get('auto_refresh', True)) + changed_auto, new_auto = imgui.checkbox("自动刷新", auto_refresh) + if changed_auto: + comp_data['auto_refresh'] = new_auto + + interval_val = float(comp_data.get('refresh_interval', 60.0)) + changed_interval, new_interval = imgui.input_float("刷新间隔(s)", interval_val, 1.0, 10.0, "%.1f") + if changed_interval: + comp_data['refresh_interval'] = max(1.0, new_interval) + + json_path = str(comp_data.get('http_json_path', '')) + changed_path, new_path = imgui.input_text("JSON路径(可选)", json_path, 256) + if changed_path: + comp_data['http_json_path'] = new_path + + headers_text = str(comp_data.get('http_headers', '{}')) + changed_headers, new_headers = imgui.input_text("Headers(JSON)", headers_text, 512) + if changed_headers: + comp_data['http_headers'] = new_headers + + body_text = str(comp_data.get('http_body', '')) + changed_body, new_body = imgui.input_text("Body", body_text, 512) + if changed_body: + comp_data['http_body'] = new_body + + max_chars = int(comp_data.get('max_chars', 300)) + changed_max, new_max = imgui.input_int("最大显示字符", max_chars) + if changed_max: + comp_data['max_chars'] = max(32, int(new_max)) + + imgui.text(f"状态: {comp_data.get('http_status', '未请求')}") + last_error = comp_data.get('last_error', '') + if last_error: + imgui.text_colored((1.0, 0.4, 0.4, 1.0), f"错误: {last_error}") + + if comp_data.get('_http_inflight'): + imgui.text("请求中...") + + if imgui.button("北京天气示例", (120, 24)): + comp_data['http_url'] = "https://api.open-meteo.com/v1/forecast?latitude=39.9042&longitude=116.4074¤t=temperature_2m,apparent_temperature,relative_humidity_2m,wind_speed_10m&timezone=Asia%2FShanghai" + comp_data['http_method'] = "GET" + comp_data['http_json_path'] = "current" + comp_data['http_headers'] = "{}" + comp_data['http_body'] = "" + comp_data['refresh_interval'] = 60.0 + comp_data['auto_refresh'] = True + + imgui.same_line() + if imgui.button("立即请求", (100, 24)): + manager.luiFunction.trigger_http_request(manager, comp_data, force=True) + + def _draw_type_props_video(manager, index, comp_data, comp_obj, comp_type): + imgui.spacing() + imgui.text("视频属性") + imgui.separator() + manager.luiFunction._draw_video_source_and_audio_controls( + manager, index, comp_data, comp_obj, comp_type + ) + manager.luiFunction._draw_video_playback_controls( + manager, index, comp_data, comp_obj, comp_type + ) + + def _draw_video_source_and_audio_controls(manager, index, comp_data, comp_obj, comp_type): + manager.luiFunction._draw_video_audio_controls(manager, comp_data) + manager.luiFunction._draw_video_source_controls(manager, comp_data) + manager.luiFunction._process_pending_video_source(manager, comp_data, comp_obj) + + def _draw_video_audio_controls(manager, comp_data): + imgui.spacing() + imgui.text("Audio") + imgui.separator() + + current_audio = comp_data.get("audio_path", "") + audio_display = os.path.basename(current_audio) if current_audio else "" + imgui.text(f"Current Audio: {audio_display if audio_display else 'None'}") + + if imgui.button("Select Audio", (110, 24)): + selected_audio = manager._change_image_texture( + title="Select Audio File", + filetypes=[("Audio", "*.mp3;*.wav;*.ogg;*.flac;*.m4a"), ("All Files", "*.*")], + ) + if selected_audio: + manager.luiFunction._load_audio_for_component( + manager, comp_data, selected_audio, from_video=False, stop_current=False + ) + + vol = comp_data.get("volume", 1.0) + changed, new_vol = imgui.slider_float("Volume", vol, 0.0, 1.0, "%.2f") + if changed: + comp_data["volume"] = new_vol + audio_sound = comp_data.get("audio") + if audio_sound and hasattr(audio_sound, "setVolume"): + audio_sound.setVolume(new_vol) + + def _draw_video_source_controls(manager, comp_data): + if "video_path" not in comp_data: + return + + current_source = comp_data.get("video_path", "") + is_url = "://" in current_source + source_display = os.path.basename(current_source) if (current_source and not is_url) else current_source + imgui.text(f"当前源: {source_display if source_display else '未设置'}") + + if imgui.button("选择本地视频", (110, 24)): + selected_path = manager._change_image_texture( + title="选择视频文件", + filetypes=[ + ("视频文件", "*.mp4;*.avi;*.mov;*.mkv;*.webm;*.ogv"), + ("所有文件", "*.*"), + ], + ) + if selected_path: + comp_data["_pending_video_source"] = selected_path + + def _process_pending_video_source(manager, comp_data, comp_obj): + if "_pending_video_source" not in comp_data: + return + + selected_path = comp_data.pop("_pending_video_source") + if not selected_path: + return + + selected_path = selected_path.strip() + comp_data["video_path"] = selected_path + comp_data["duration"] = 0.0 + print(f"✓ 尝试加载视频源: {selected_path}") + + try: + video_texture = manager.luiFunction._load_video_texture_from_source(manager, selected_path) + if not video_texture: + return + manager.luiFunction._apply_loaded_video_texture( + manager, comp_data, comp_obj, video_texture, selected_path + ) + except Exception as e: + print(f"⚠ 加载视频失败: {e}") + + def _load_video_texture_from_source(manager, selected_path): + from panda3d.core import Filename, MovieTexture, MovieVideo, loadPrcFileData + + loadPrcFileData("", "ffmpeg-protocol-whitelist http,https,tcp,tls,file") + loadPrcFileData("", "ffmpeg-show-error #t") + + if "://" in selected_path: + print(f"🔍 尝试加载 URL: [{selected_path}]") + try: + video_src = MovieVideo.get(Filename(selected_path)) + video_texture = MovieTexture("RemoteVideo") + if not video_texture.load(video_src): + print("⚠ MovieVideo 加载失败,回退到 Texture.read...") + if not video_texture.read(Filename(selected_path)): + raise Exception("MovieTexture 无法读取 URL") + return video_texture + except Exception as e: + print(f"⚠ 显式流加载失败: {e},尝试使用 Loader...") + return manager.world.loader.loadTexture(selected_path) + + return manager.world.loader.loadTexture(Filename.from_os_specific(selected_path)) + + def _apply_loaded_video_texture(manager, comp_data, comp_obj, video_texture, selected_path): + manager.luiFunction._sync_video_size_and_sprite( + manager, comp_data, comp_obj, video_texture + ) + comp_data["texture"] = video_texture + comp_obj.video_texture = video_texture + + if comp_data.get("audio_from_video"): + manager.luiFunction._load_audio_for_component( + manager, comp_data, selected_path, from_video=True, stop_current=True + ) + + manager.luiFunction._start_video_texture_playback(manager, video_texture, comp_data) + comp_data["is_playing"] = True + + def _sync_video_size_and_sprite(manager, comp_data, comp_obj, video_texture): + orig_w = video_texture.getOrigFileXSize() + orig_h = video_texture.getOrigFileYSize() + if orig_w <= 0 or orig_h <= 0: + return + + ratio = orig_w / orig_h + width = comp_data.get("width", 320) + new_height = width / ratio + print(f"✓Loaded Video Texture: {orig_w}x{orig_h}") + + from panda3d.core import ConfigVariableBool + if not ConfigVariableBool("support-threads").getValue(): + print("⚠ 警告: support-threads 为 False, 视频播放可能失败!") + + if "sprite" in comp_data: + spr = comp_data["sprite"] + if hasattr(spr, "set_uv_range"): + spr.set_uv_range(0, 0, 1, 1) + + comp_data["height"] = new_height + comp_obj.height = new_height + if "sprite" in comp_data: + comp_data["sprite"].height = new_height + + manager.luiFunction._replace_video_sprite( + manager, comp_data, comp_obj, video_texture, width, new_height + ) + + def _replace_video_sprite(manager, comp_data, comp_obj, video_texture, width, new_height): + if "sprite" in comp_data: + old_spr = comp_data["sprite"] + if old_spr: try: - if hasattr(comp_obj, 'set_options'): - comp_obj.set_options(options) - elif hasattr(comp_obj, 'options'): - comp_obj.options = options - if options and hasattr(comp_obj, '_select_option'): - comp_obj._select_option(comp_data['selected_option_id']) - except Exception as e: - print(f"Selectbox options update failed: {e}") + old_spr.parent = None + if hasattr(old_spr, "hide"): + old_spr.hide() + except Exception as ex: + print(f"⚠ Failed to detach old sprite: {ex}") - elif comp_type == 'HttpText': - imgui.spacing() - imgui.text("HTTP通信") - imgui.separator() + new_spr = LUISprite(comp_obj, video_texture) + if hasattr(new_spr, "set_texture"): + new_spr.set_texture(video_texture, resize=False) + new_spr.width = width + new_spr.height = new_height + new_spr.color = (1, 1, 1, 1) + new_spr.z_offset = 0 + if hasattr(new_spr, "set_uv_range"): + new_spr.set_uv_range(0, 0, 1, 1) - http_url = str(comp_data.get('http_url', '')) - changed_url, new_url = imgui.input_text("URL", http_url, 512) - if changed_url: - comp_data['http_url'] = new_url + comp_data["sprite"] = new_spr + manager.luiFunction._refresh_video_keep_alive_node(manager, comp_data, video_texture) - method_options = ["GET", "POST"] - curr_method = str(comp_data.get('http_method', 'GET')).upper() - curr_method_idx = 1 if curr_method == "POST" else 0 - changed_method, new_method_idx = imgui.combo("Method", curr_method_idx, method_options) - if changed_method: - comp_data['http_method'] = method_options[new_method_idx] + def _refresh_video_keep_alive_node(manager, comp_data, video_texture): + if "keep_alive" in comp_data and comp_data["keep_alive"]: + try: + comp_data["keep_alive"].destroy() + except Exception: + pass - timeout_val = float(comp_data.get('http_timeout', 8.0)) - changed_timeout, new_timeout = imgui.input_float("Timeout(s)", timeout_val, 1.0, 5.0, "%.1f") - if changed_timeout: - comp_data['http_timeout'] = max(1.0, new_timeout) + try: + from direct.gui.OnscreenImage import OnscreenImage + from panda3d.core import TransparencyAttrib - auto_refresh = bool(comp_data.get('auto_refresh', True)) - changed_auto, new_auto = imgui.checkbox("自动刷新", auto_refresh) - if changed_auto: - comp_data['auto_refresh'] = new_auto + dummy = OnscreenImage(image=video_texture, parent=manager.world.render2d) + dummy.setScale(0.1) + dummy.setPos(0, 0, 0) + dummy.setTransparency(TransparencyAttrib.MAlpha) + dummy.setColorScale(1, 1, 1, 0.01) + dummy.setBin("background", -10) - interval_val = float(comp_data.get('refresh_interval', 60.0)) - changed_interval, new_interval = imgui.input_float("刷新间隔(s)", interval_val, 1.0, 10.0, "%.1f") - if changed_interval: - comp_data['refresh_interval'] = max(1.0, new_interval) + comp_data["keep_alive"] = dummy + print("✓ Created keep-alive node for video") + except Exception as e: + print(f"⚠ Keep-alive creation failed: {e}") - json_path = str(comp_data.get('http_json_path', '')) - changed_path, new_path = imgui.input_text("JSON路径(可选)", json_path, 256) - if changed_path: - comp_data['http_json_path'] = new_path + def _load_audio_for_component(manager, comp_data, audio_path, from_video, stop_current): + if stop_current: + old_audio = comp_data.get("audio") + if old_audio and hasattr(old_audio, "stop"): + old_audio.stop() - headers_text = str(comp_data.get('http_headers', '{}')) - changed_headers, new_headers = imgui.input_text("Headers(JSON)", headers_text, 512) - if changed_headers: - comp_data['http_headers'] = new_headers + try: + audio_sound = manager.world.loader.loadSfx(p3d.Filename.from_os_specific(audio_path)) + if not audio_sound: + return - body_text = str(comp_data.get('http_body', '')) - changed_body, new_body = imgui.input_text("Body", body_text, 512) - if changed_body: - comp_data['http_body'] = new_body + comp_data["audio"] = audio_sound + comp_data["audio_path"] = audio_path + comp_data["audio_from_video"] = from_video + if hasattr(audio_sound, "setLoop"): + audio_sound.setLoop(comp_data.get("loop", True)) + if hasattr(audio_sound, "setVolume"): + audio_sound.setVolume(comp_data.get("volume", 1.0)) + if comp_data.get("is_playing") and hasattr(audio_sound, "play"): + audio_sound.play() + except Exception as e: + print(f"Audio load failed: {e}") - max_chars = int(comp_data.get('max_chars', 300)) - changed_max, new_max = imgui.input_int("最大显示字符", max_chars) - if changed_max: - comp_data['max_chars'] = max(32, int(new_max)) + def _start_video_texture_playback(manager, video_texture, comp_data): + if hasattr(video_texture, "play"): + if hasattr(video_texture, "stop"): + video_texture.stop() + video_texture.play() + if hasattr(video_texture, "setLoop"): + video_texture.setLoop(comp_data.get("loop", True)) - imgui.text(f"状态: {comp_data.get('http_status', '未请求')}") - last_error = comp_data.get('last_error', '') - if last_error: - imgui.text_colored((1.0, 0.4, 0.4, 1.0), f"错误: {last_error}") - - if comp_data.get('_http_inflight'): - imgui.text("请求中...") - - if imgui.button("北京天气示例", (120, 24)): - comp_data['http_url'] = "https://api.open-meteo.com/v1/forecast?latitude=39.9042&longitude=116.4074¤t=temperature_2m,apparent_temperature,relative_humidity_2m,wind_speed_10m&timezone=Asia%2FShanghai" - comp_data['http_method'] = "GET" - comp_data['http_json_path'] = "current" - comp_data['http_headers'] = "{}" - comp_data['http_body'] = "" - comp_data['refresh_interval'] = 60.0 - comp_data['auto_refresh'] = True + def _draw_video_playback_controls(manager, index, comp_data, comp_obj, comp_type): + # 播放控制 + imgui.spacing() + imgui.text("播放控制") + video_tex = comp_data.get('texture') + audio_sound = comp_data.get('audio') + if video_tex: + # Play/Pause Button + is_playing = comp_data.get('is_playing', False) + if imgui.button("暂停" if is_playing else "播放", (60, 24)): + if is_playing: + if hasattr(video_tex, 'stop'): video_tex.stop() + if audio_sound and hasattr(audio_sound, 'stop'): audio_sound.stop() + comp_data['is_playing'] = False + else: + if hasattr(video_tex, 'play'): video_tex.play() + if audio_sound and hasattr(audio_sound, 'play'): audio_sound.play() + comp_data['is_playing'] = True imgui.same_line() - if imgui.button("立即请求", (100, 24)): - manager.luiFunction.trigger_http_request(manager, comp_data, force=True) - - elif comp_type == 'Video': - imgui.spacing() - imgui.text("视频属性") - imgui.separator() - # Audio - imgui.spacing() - imgui.text("Audio") - imgui.separator() - current_audio = comp_data.get('audio_path', '') - audio_display = os.path.basename(current_audio) if current_audio else '' - imgui.text(f"Current Audio: {audio_display if audio_display else 'None'}") - if imgui.button("Select Audio", (110, 24)): - selected_audio = manager._change_image_texture( - title = "Select Audio File", - filetypes = [("Audio", "*.mp3;*.wav;*.ogg;*.flac;*.m4a"), ("All Files", "*.*")] - ) - if selected_audio: - try: - audio_sound = manager.world.loader.loadSfx(p3d.Filename.from_os_specific(selected_audio)) - if audio_sound: - comp_data["audio"] = audio_sound - comp_data["audio_path"] = selected_audio - comp_data["audio_from_video"] = False - if hasattr(audio_sound, "setLoop"): - audio_sound.setLoop(comp_data.get("loop", True)) - if hasattr(audio_sound, "setVolume"): - audio_sound.setVolume(comp_data.get("volume", 1.0)) - if comp_data.get("is_playing") and hasattr(audio_sound, "play"): - audio_sound.play() - except Exception as e: - print(f"Audio load failed: {e}") - # Volume - vol = comp_data.get("volume", 1.0) - changed, new_vol = imgui.slider_float("Volume", vol, 0.0, 1.0, "%.2f") + if imgui.button("重播", (60, 24)): + if hasattr(video_tex, 'stop'): video_tex.stop() + if audio_sound and hasattr(audio_sound, 'stop'): audio_sound.stop() + if hasattr(video_tex, 'play'): video_tex.play() + if audio_sound and hasattr(audio_sound, 'play'): audio_sound.play() + comp_data['is_playing'] = True + + # Loop Checkbox + loop = comp_data.get('loop', True) + changed, new_loop = imgui.checkbox("循环播放", loop) if changed: - comp_data["volume"] = new_vol - audio_sound = comp_data.get("audio") - if audio_sound and hasattr(audio_sound, "setVolume"): - audio_sound.setVolume(new_vol) + comp_data['loop'] = new_loop + if hasattr(video_tex, 'setLoop'): + video_tex.setLoop(new_loop) + if audio_sound and hasattr(audio_sound, 'setLoop'): audio_sound.setLoop(new_loop) + + # Time Display + if hasattr(video_tex, 'getTime'): + t = video_tex.getTime() + + # 检查是否需要循环播放 + duration = comp_data.get('duration', 0.0) + if duration > 0 and t >= duration: + # 播放时间超过总时长,重置到开头 + if hasattr(video_tex, 'setTime'): + video_tex.setTime(0.0) + t = 0.0 + print(f"✓ 视频播放完毕,重新开始循环播放") + + imgui.text(f"当前时间: {t:.2f}s") + + # Force refresh if stuck (hack) + # if t == 0.0 and comp_data.get('is_playing'): + # video_tex.play() + # imgui.separator() + # imgui.text("调试工具") + # if imgui.button("在Render2D中测试播放"): + # # Create a standard OnscreenImage to test playback capability + # from direct.gui.OnscreenImage import OnscreenImage + # from panda3d.core import TransparencyAttrib + + # # Remove previous test if any + # if hasattr(manager, '_debug_video_node') and manager._debug_video_node: + # manager._debug_video_node.destroy() + + # manager._debug_video_node = OnscreenImage(image=video_tex, pos=(0, 0, 0), scale=0.5, parent=manager.world.render2d) + # manager._debug_video_node.setTransparency(TransparencyAttrib.MAlpha) + # print(f"Debug: Created OnscreenImage with texture {video_tex}") + + # if imgui.button("清除Render2D测试"): + # if hasattr(manager, '_debug_video_node') and manager._debug_video_node: + # manager._debug_video_node.destroy() + # manager._debug_video_node = None + # print("Debug: Cleared OnscreenImage") - # 视频源显示与更改 (支持本地文件和 URL 流) - if 'video_path' in comp_data: - current_source = comp_data.get('video_path', '') - is_url = '://' in current_source + # Time Display and Control + if hasattr(video_tex, 'getTime'): + current_time = video_tex.getTime() - # 顶部状态显示 - source_display = os.path.basename(current_source) if (current_source and not is_url) else current_source - imgui.text(f"当前源: {source_display if source_display else '未设置'}") - - # -- 方式 1: 本地文件 -- - if imgui.button("选择本地视频", (110, 24)): - selected_path = manager._change_image_texture( - title = "选择视频文件", - filetypes = [ - ("视频文件", "*.mp4;*.avi;*.mov;*.mkv;*.webm;*.ogv"), - ("所有文件", "*.*") - ] - ) - if selected_path: - comp_data['_pending_video_source'] = selected_path - - # imgui.spacing() - # imgui.text("视频URL") - # imgui.separator() - - # # -- 方式 2: URL / Stream -- - # # 使用缓存的临时输入,避免每帧刷新导致光标丢失 - # url_input_key = f"video_url_input_{index}" - # if url_input_key not in comp_data: - # comp_data[url_input_key] = current_source if is_url else "" - - # changed, new_url = imgui.input_text("##vurl", comp_data[url_input_key], 1024) - # if changed: - # comp_data[url_input_key] = new_url - - # imgui.same_line() - # if imgui.button("加载 URL", (80, 24)): - # comp_data['_pending_video_source'] = comp_data[url_input_key] - - # 统一执行加载逻辑 - if '_pending_video_source' in comp_data: - selected_path = comp_data.pop('_pending_video_source') - if selected_path: - comp_data['video_path'] = selected_path - # 重置时长以强制新视频重新探测 - comp_data['duration'] = 0.0 - print(f"✓ 尝试加载视频源: {selected_path}") - - try: - # 重新加载视频纹理 - from panda3d.core import Filename, MovieTexture, MovieVideo, loadPrcFileData - - # Trim trailing spaces which are visible in error logs - selected_path = selected_path.strip() - - # Whitelist protocols for FFmpeg - loadPrcFileData("", "ffmpeg-protocol-whitelist http,https,tcp,tls,file") - loadPrcFileData("", "ffmpeg-show-error #t") - - if "://" in selected_path: - print(f"🔍 尝试加载 URL: [{selected_path}]") - try: - # Use MovieVideo for better protocol handling - video_src = MovieVideo.get(Filename(selected_path)) - video_texture = MovieTexture("RemoteVideo") - if not video_texture.load(video_src): - print("⚠ MovieVideo 加载失败,回退到 Texture.read...") - if not video_texture.read(Filename(selected_path)): - raise Exception("MovieTexture 无法读取 URL") - except Exception as e: - print(f"⚠ 显式流加载失败: {e},尝试使用 Loader...") - video_texture = manager.world.loader.loadTexture(selected_path) - else: - video_texture = manager.world.loader.loadTexture(Filename.from_os_specific(selected_path)) - if video_texture: - # 更新AspectRatio - orig_w = video_texture.getOrigFileXSize() - orig_h = video_texture.getOrigFileYSize() - if orig_w > 0 and orig_h > 0: - ratio = orig_w / orig_h - ratio = orig_w / orig_h - width = comp_data.get('width', 320) - new_height = width / ratio - - print(f"✓Loaded Video Texture: {orig_w}x{orig_h}") - - # Debug: check threading support - from panda3d.core import ConfigVariableBool - if not ConfigVariableBool("support-threads").getValue(): - print("⚠ 警告: support-threads 为 False, 视频播放可能失败!") - - if 'sprite' in comp_data: - spr = comp_data['sprite'] - # Ensure full UV coordinate usage - if hasattr(spr, 'set_uv_range'): - spr.set_uv_range(0, 0, 1, 1) - - # 更新组件高度 - comp_data['height'] = new_height - comp_obj.height = new_height - if 'sprite' in comp_data: - comp_data['sprite'].height = new_height - - # Re-create sprite to ensure no atlas conflict and clean up old one - if 'sprite' in comp_data: - # Detach old sprite to prevent "ghost" rectangles - old_spr = comp_data['sprite'] - if old_spr: - try: - # Try standard LUI detachment - old_spr.parent = None - if hasattr(old_spr, 'hide'): - old_spr.hide() - except Exception as ex: - print(f"⚠ Failed to detach old sprite: {ex}") - - # Create new sprite with direct texture - # Note: LUISprite(parent, texture) is valid if texture is a Texture object - new_spr = LUISprite(comp_obj, video_texture) - # Force set texture again with resize=False to ensure binding - if hasattr(new_spr, 'set_texture'): - new_spr.set_texture(video_texture, resize=False) - - new_spr.width = width - new_spr.height = new_height - # Video needs white color to modulate correctly with texture - new_spr.color = (1, 1, 1, 1) - new_spr.z_offset = 0 - - # Ensure UVs are full frame - if hasattr(new_spr, 'set_uv_range'): - new_spr.set_uv_range(0, 0, 1, 1) - - comp_data['sprite'] = new_spr - - # WORKAROUND: Force texture update by adding a dummy node to scene graph - # LUI alone might not trigger MovieTexture updates in some pipeline configs - if 'keep_alive' in comp_data and comp_data['keep_alive']: - try: - comp_data['keep_alive'].destroy() - except: - pass - - try: - from direct.gui.OnscreenImage import OnscreenImage - from panda3d.core import TransparencyAttrib - # Create a nearly invisible card (alpha=0.01) to force update - # We position it at (0,0,0) with a small but visible scale to prevent culling - dummy = OnscreenImage(image=video_texture, parent=manager.world.render2d) - dummy.setScale(0.1) - dummy.setPos(0, 0, 0) - dummy.setTransparency(TransparencyAttrib.MAlpha) - dummy.setColorScale(1, 1, 1, 0.01) # Nearly invisible - # Ensure it's behind other UI if possible, though in render2d everything is overlay - dummy.setBin("background", -10) - - comp_data['keep_alive'] = dummy - print("✓ Created keep-alive node for video") - except Exception as e: - print(f"⚠ Keep-alive creation failed: {e}") - - # 更新引用 - comp_data['texture'] = video_texture - comp_obj.video_texture = video_texture # Prevent GC - if comp_data.get("audio_from_video"): - old_audio = comp_data.get("audio") - if old_audio and hasattr(old_audio, "stop"): - old_audio.stop() - try: - audio_sound = manager.world.loader.loadSfx(p3d.Filename.from_os_specific(selected_path)) - if audio_sound: - comp_data["audio"] = audio_sound - comp_data["audio_path"] = selected_path - if hasattr(audio_sound, "setLoop"): - audio_sound.setLoop(comp_data.get("loop", True)) - if hasattr(audio_sound, "setVolume"): - audio_sound.setVolume(comp_data.get("volume", 1.0)) - if comp_data.get("is_playing") and hasattr(audio_sound, "play"): - audio_sound.play() - except Exception as e: - print(f"Audio load failed: {e}") - - if hasattr(video_texture, 'play'): - if hasattr(video_texture, 'stop'): - video_texture.stop() # Reset - video_texture.play() - if hasattr(video_texture, 'setLoop'): - video_texture.setLoop(comp_data.get("loop", True)) - - comp_data['is_playing'] = True - - except Exception as e: - print(f"⚠ 加载视频失败: {e}") - - # 播放控制 - imgui.spacing() - - - imgui.text("播放控制") - - video_tex = comp_data.get('texture') - audio_sound = comp_data.get('audio') - if video_tex: - # Play/Pause Button - is_playing = comp_data.get('is_playing', False) - if imgui.button("暂停" if is_playing else "播放", (60, 24)): - if is_playing: - if hasattr(video_tex, 'stop'): video_tex.stop() - if audio_sound and hasattr(audio_sound, 'stop'): audio_sound.stop() - comp_data['is_playing'] = False - else: - if hasattr(video_tex, 'play'): video_tex.play() - if audio_sound and hasattr(audio_sound, 'play'): audio_sound.play() - comp_data['is_playing'] = True - - imgui.same_line() - if imgui.button("重播", (60, 24)): - if hasattr(video_tex, 'stop'): video_tex.stop() - if audio_sound and hasattr(audio_sound, 'stop'): audio_sound.stop() - if hasattr(video_tex, 'play'): video_tex.play() - if audio_sound and hasattr(audio_sound, 'play'): audio_sound.play() - comp_data['is_playing'] = True - - # Loop Checkbox - loop = comp_data.get('loop', True) - changed, new_loop = imgui.checkbox("循环播放", loop) - if changed: - comp_data['loop'] = new_loop - if hasattr(video_tex, 'setLoop'): - video_tex.setLoop(new_loop) - if audio_sound and hasattr(audio_sound, 'setLoop'): audio_sound.setLoop(new_loop) - - # Time Display - if hasattr(video_tex, 'getTime'): - t = video_tex.getTime() - - # 检查是否需要循环播放 - duration = comp_data.get('duration', 0.0) - if duration > 0 and t >= duration: - # 播放时间超过总时长,重置到开头 - if hasattr(video_tex, 'setTime'): - video_tex.setTime(0.0) - t = 0.0 - print(f"✓ 视频播放完毕,重新开始循环播放") - - imgui.text(f"当前时间: {t:.2f}s") - - # Force refresh if stuck (hack) - # if t == 0.0 and comp_data.get('is_playing'): - # video_tex.play() - - # imgui.separator() - # imgui.text("调试工具") - # if imgui.button("在Render2D中测试播放"): - # # Create a standard OnscreenImage to test playback capability - # from direct.gui.OnscreenImage import OnscreenImage - # from panda3d.core import TransparencyAttrib - - # # Remove previous test if any - # if hasattr(manager, '_debug_video_node') and manager._debug_video_node: - # manager._debug_video_node.destroy() - - # manager._debug_video_node = OnscreenImage(image=video_tex, pos=(0, 0, 0), scale=0.5, parent=manager.world.render2d) - # manager._debug_video_node.setTransparency(TransparencyAttrib.MAlpha) - # print(f"Debug: Created OnscreenImage with texture {video_tex}") - - # if imgui.button("清除Render2D测试"): - # if hasattr(manager, '_debug_video_node') and manager._debug_video_node: - # manager._debug_video_node.destroy() - # manager._debug_video_node = None - # print("Debug: Cleared OnscreenImage") - - # Time Display and Control - if hasattr(video_tex, 'getTime'): - current_time = video_tex.getTime() - - if 'duration' not in comp_data or comp_data['duration'] <= 0.0: - # Attempt to detect duration automatically - detected_duration = 0.0 - # Method 1: standard MovieTexture interface (if available) - if hasattr(video_tex, 'getVideoLength'): - detected_duration = video_tex.getVideoLength() - # Method 2: check if it has a 'length' property - elif hasattr(video_tex, 'length'): - detected_duration = video_tex.length - - if detected_duration > 0: - comp_data['duration'] = detected_duration - print(f"✓ Automatically detected video duration: {detected_duration}s") - - # Manual Duration Input (Essential if auto-detection fails) - # We only show this if we really don't know the duration - current_dur = comp_data.get('duration', 0.0) - if current_dur <= 0.1: - imgui.text_colored((1, 1, 0, 1), "⚠未知时长,请手动设置:") - changed, new_dur = imgui.input_float("总时长(s)", current_dur, 1.0, 10.0, "%.1f") - if changed: - comp_data['duration'] = new_dur - - duration = comp_data.get('duration', 0.0) - - # 检查是否需要循环播放 - if duration > 0 and current_time >= duration: - # 播放时间超过总时长,重置到开头 - if hasattr(video_tex, 'setTime'): - video_tex.setTime(0.0) - current_time = 0.0 - print(f"✓ 视频播放完毕,重新开始循环播放") - - # If we still don't have a valid duration, use a fallback for display but warn user - # REVERT: Do not auto-extend duration, it causes confusion. Trust detection or user. - - display_max = duration if duration > 0 else max(current_time + 10.0, 60.0) - - # Format function for time - def format_time(seconds): - m = int(seconds // 60) - s = int(seconds % 60) - return f"{m:02d}:{s:02d}" - - # Progress Slider - imgui.text(f"进度: {format_time(current_time)} / {format_time(display_max)}") - imgui.same_line() - - # Use push_item_width to make it fit - imgui.push_item_width(-1) - # Use a custom format for the slider to show seconds, but maybe we can show MM:SS in the text - changed, seek_time = imgui.slider_float("##seek_slider", current_time, 0.0, display_max, "%.2fs") - imgui.pop_item_width() - + if 'duration' not in comp_data or comp_data['duration'] <= 0.0: + # Attempt to detect duration automatically + detected_duration = 0.0 + # Method 1: standard MovieTexture interface (if available) + if hasattr(video_tex, 'getVideoLength'): + detected_duration = video_tex.getVideoLength() + # Method 2: check if it has a 'length' property + elif hasattr(video_tex, 'length'): + detected_duration = video_tex.length + + if detected_duration > 0: + comp_data['duration'] = detected_duration + print(f"✓ Automatically detected video duration: {detected_duration}s") + + # Manual Duration Input (Essential if auto-detection fails) + # We only show this if we really don't know the duration + current_dur = comp_data.get('duration', 0.0) + if current_dur <= 0.1: + imgui.text_colored((1, 1, 0, 1), "⚠未知时长,请手动设置:") + changed, new_dur = imgui.input_float("总时长(s)", current_dur, 1.0, 10.0, "%.1f") if changed: - if hasattr(video_tex, 'setTime'): - video_tex.setTime(seek_time) - if audio_sound and hasattr(audio_sound, 'setTime'): audio_sound.setTime(seek_time) - # If paused, we might need to show the frame. - # Usually setTime works. - print(f"Seek to {seek_time}") - - # Update cached duration if we find a way, or if user inputs it? - # For now just show time - # imgui.text(f"时间: {current_time:.2f}s") # Already shown in slider - else: - imgui.text_colored((1, 0, 0, 1), "无有效视频纹理") - - # 锚点设置 + comp_data['duration'] = new_dur + + duration = comp_data.get('duration', 0.0) + + # 检查是否需要循环播放 + if duration > 0 and current_time >= duration: + # 播放时间超过总时长,重置到开头 + if hasattr(video_tex, 'setTime'): + video_tex.setTime(0.0) + current_time = 0.0 + print(f"✓ 视频播放完毕,重新开始循环播放") + + # If we still don't have a valid duration, use a fallback for display but warn user + # REVERT: Do not auto-extend duration, it causes confusion. Trust detection or user. + + display_max = duration if duration > 0 else max(current_time + 10.0, 60.0) + + # Format function for time + def format_time(seconds): + m = int(seconds // 60) + s = int(seconds % 60) + return f"{m:02d}:{s:02d}" + + # Progress Slider + imgui.text(f"进度: {format_time(current_time)} / {format_time(display_max)}") + imgui.same_line() + + # Use push_item_width to make it fit + imgui.push_item_width(-1) + # Use a custom format for the slider to show seconds, but maybe we can show MM:SS in the text + changed, seek_time = imgui.slider_float("##seek_slider", current_time, 0.0, display_max, "%.2fs") + imgui.pop_item_width() + + if changed: + if hasattr(video_tex, 'setTime'): + video_tex.setTime(seek_time) + if audio_sound and hasattr(audio_sound, 'setTime'): audio_sound.setTime(seek_time) + # If paused, we might need to show the frame. + # Usually setTime works. + print(f"Seek to {seek_time}") + + # Update cached duration if we find a way, or if user inputs it? + # For now just show time + # imgui.text(f"时间: {current_time:.2f}s") # Already shown in slider + else: + imgui.text_colored((1, 0, 0, 1), "无有效视频纹理") + + def _draw_anchor_and_hierarchy_properties(manager, index, comp_data, comp_obj): imgui.spacing() imgui.separator() imgui.text("锚点设置") diff --git a/ui/LUI/lui_manager_editor.py b/ui/LUI/lui_manager_editor.py index 5a3b7a2b..4d466cbd 100644 --- a/ui/LUI/lui_manager_editor.py +++ b/ui/LUI/lui_manager_editor.py @@ -28,231 +28,245 @@ class LUIManagerEditorMixin: comp_data['children_indices'] = [] def _set_parent_child_relationship(self, child_index, parent_index, anchor_position=None, keep_world=False): - # Set parent-child relationship - if (child_index < 0 or child_index >= len(self.components) or - parent_index < 0 or parent_index >= len(self.components)): + if not self._is_valid_relationship_index(child_index, parent_index): print("Error: invalid component index") return child_data = self.components[child_index] parent_data = self.components[parent_index] + if not self._prepare_parent_child_link(child_index, parent_index, child_data, parent_data): + return + # 编排入口:仅更新逻辑父子关系,不执行真实 reparent,避免破坏复杂组件内部结构。 + current_abs_left, current_abs_top = self._get_component_accumulated_pos(child_index) + print(f"[_set_parent_child_relationship] Child #{child_index} -> Parent #{parent_index}") + print(f" Current absolute position: ({current_abs_left:.1f}, {current_abs_top:.1f})") + + self._remove_child_from_old_parent(child_data, child_index) + self._attach_child_to_new_parent(child_data, parent_data, child_index, parent_index) + + new_local_left, new_local_top = self._compute_new_child_local_position( + child_index, parent_index, current_abs_left, current_abs_top + ) + child_data['left'] = new_local_left + child_data['top'] = new_local_top + + child_obj = child_data['object'] + parent_obj = self._resolve_parent_visual_object(parent_data) + + try: + self._set_child_visual_parent_policy(child_data, parent_data) + if not keep_world: + new_local_left, new_local_top = self._clamp_child_local_within_parent( + child_data, parent_data, child_obj, parent_obj, new_local_left, new_local_top + ) + child_data['left'] = new_local_left + child_data['top'] = new_local_top + + self._sync_child_canvas_index_with_parent(child_data, parent_data) + self._apply_child_absolute_position(child_obj, current_abs_left, current_abs_top, keep_world) + self._ensure_child_visibility_and_layer(child_data, parent_data, child_obj, parent_obj) + except Exception as e: + print(f"Reparenting error: {e}") + + self._finalize_parent_child_link(child_index, parent_index, parent_data) + + def _is_valid_relationship_index(self, child_index, parent_index): + return ( + 0 <= child_index < len(self.components) + and 0 <= parent_index < len(self.components) + ) + + def _prepare_parent_child_link(self, child_index, parent_index, child_data, parent_data): if parent_data.get('type') in ['VerticalLayout', 'HorizontalLayout']: child_data['draggable'] = False if child_data.get('parent_index') == parent_index: print(f"Component {child_index} already under {parent_index}") + return False + return True + + def _remove_child_from_old_parent(self, child_data, child_index): + old_parent_index = child_data.get('parent_index') + if old_parent_index is None or old_parent_index < 0: return - current_abs_left, current_abs_top = self._get_component_accumulated_pos(child_index) - - print(f"[_set_parent_child_relationship] Child #{child_index} -> Parent #{parent_index}") - print(f" Current absolute position: ({current_abs_left:.1f}, {current_abs_top:.1f})") - - old_parent_index = child_data.get('parent_index') - if old_parent_index is not None and old_parent_index >= 0: - old_parent_data = self.components[old_parent_index] - if child_index in old_parent_data.get('children_indices', []): - old_parent_data['children_indices'].remove(child_index) + old_parent_data = self.components[old_parent_index] + if child_index in old_parent_data.get('children_indices', []): + old_parent_data['children_indices'].remove(child_index) + def _attach_child_to_new_parent(self, child_data, parent_data, child_index, parent_index): child_data['parent_index'] = parent_index if 'children_indices' not in parent_data: parent_data['children_indices'] = [] if child_index not in parent_data['children_indices']: parent_data['children_indices'].append(child_index) + def _compute_new_child_local_position(self, child_index, parent_index, current_abs_left, current_abs_top): parent_abs_left, parent_abs_top = self._get_component_accumulated_pos(parent_index) new_local_left = current_abs_left - parent_abs_left new_local_top = current_abs_top - parent_abs_top - print(f" Parent absolute position: ({parent_abs_left:.1f}, {parent_abs_top:.1f})") print(f" Calculated local position: ({new_local_left:.1f}, {new_local_top:.1f})") + return new_local_left, new_local_top - # 在数据结构中存储局部坐标(相对于父组件) - child_data['left'] = new_local_left - child_data['top'] = new_local_top - - child_obj = child_data['object'] + def _resolve_parent_visual_object(self, parent_data): parent_obj = parent_data['object'] parent_type = parent_data.get('type') + if parent_type == 'ScrollableRegion': + return parent_data['object'].content_node + if parent_type in ['VerticalLayout', 'HorizontalLayout'] and parent_data.get('layout_obj'): + if parent_data.get('layout_wrap', True): + return parent_data.get('object') + return parent_data.get('layout_obj').cell() + return parent_obj + + def _set_child_visual_parent_policy(self, child_data, parent_data): visual_container_types = [ 'Frame', 'Plane', 'Video', 'HttpText', 'ScrollableRegion', 'TabbedFrame', - 'VerticalLayout', 'HorizontalLayout' + 'VerticalLayout', 'HorizontalLayout', ] - if parent_data.get('type') == 'ScrollableRegion': - parent_obj = parent_data['object'].content_node - elif parent_data.get('type') in ['VerticalLayout', 'HorizontalLayout'] and parent_data.get('layout_obj'): - if parent_data.get('layout_wrap', True): - parent_obj = parent_data.get('object') - else: - parent_obj = parent_data.get('layout_obj').cell() + if parent_data.get('type') in visual_container_types: + child_data['visual_parent_canvas'] = False + print("[_set_parent_child_relationship] Setting parent relationship (data only, no reparenting)") + else: + # 非视觉容器保持画布父级显示,避免被父组件裁剪。 + child_data['visual_parent_canvas'] = True + def _clamp_child_local_within_parent( + self, child_data, parent_data, child_obj, parent_obj, new_local_left, new_local_top + ): try: - if parent_type in visual_container_types: - child_data['visual_parent_canvas'] = False - # 关键修改:不要调用 reparent_to(),避免破坏内部结构 - # 只更新数据结构,保持原有的父对象关系 - print(f"[_set_parent_child_relationship] Setting parent relationship (data only, no reparenting)") - else: - # Keep visual parent on canvas to avoid clipping, but keep logical parent - child_data['visual_parent_canvas'] = True + parent_w = parent_data.get('width') + parent_h = parent_data.get('height') + if parent_w is None and hasattr(parent_obj, 'width'): + parent_w = parent_obj.width + if parent_h is None and hasattr(parent_obj, 'height'): + parent_h = parent_obj.height - # If not preserving world position, clamp into parent bounds for visibility - if not keep_world: - try: - parent_w = parent_data.get('width') - parent_h = parent_data.get('height') - if parent_w is None and hasattr(parent_obj, 'width'): - parent_w = parent_obj.width - if parent_h is None and hasattr(parent_obj, 'height'): - parent_h = parent_obj.height - child_w = child_data.get('width') - child_h = child_data.get('height') - if child_w is None and hasattr(child_obj, 'width'): - child_w = child_obj.width - if child_h is None and hasattr(child_obj, 'height'): - child_h = child_obj.height - if parent_w and parent_h and child_w and child_h: - max_x = max(0.0, float(parent_w) - float(child_w)) - max_y = max(0.0, float(parent_h) - float(child_h)) - new_local_left = max(0.0, min(float(new_local_left), max_x)) - new_local_top = max(0.0, min(float(new_local_top), max_y)) - child_data['left'] = new_local_left - child_data['top'] = new_local_top - except Exception: - pass + child_w = child_data.get('width') + child_h = child_data.get('height') + if child_w is None and hasattr(child_obj, 'width'): + child_w = child_obj.width + if child_h is None and hasattr(child_obj, 'height'): + child_h = child_obj.height - # 关键修改:由于我们不实际重新父化对象,所有组件都保持相对于Canvas的绝对位置 - # 只在数据结构中记录相对于父组件的局部坐标 - if child_data.get('visual_parent_canvas'): - # Keep world position - try: - child_data['canvas_index'] = parent_data.get('canvas_index', self.current_canvas_index) - except Exception: - pass - - # 根据keep_world参数决定是否保持世界位置 - if keep_world: - # 保持世界位置:使用绝对坐标 - if hasattr(child_obj, 'left'): - child_obj.left = current_abs_left - if hasattr(child_obj, 'top'): - child_obj.top = current_abs_top - print(f"[_set_parent_child_relationship] Child position kept at absolute: ({current_abs_left:.1f}, {current_abs_top:.1f})") - else: - # 不保持世界位置:使用局部坐标(但由于没有实际重新父化,这会导致位置跳变) - # 为了避免跳变,我们仍然使用绝对坐标 - if hasattr(child_obj, 'left'): - child_obj.left = current_abs_left - if hasattr(child_obj, 'top'): - child_obj.top = current_abs_top - print(f"[_set_parent_child_relationship] Child position set to absolute: ({current_abs_left:.1f}, {current_abs_top:.1f})") + if parent_w and parent_h and child_w and child_h: + max_x = max(0.0, float(parent_w) - float(child_w)) + max_y = max(0.0, float(parent_h) - float(child_h)) + new_local_left = max(0.0, min(float(new_local_left), max_x)) + new_local_top = max(0.0, min(float(new_local_top), max_y)) + except Exception: + pass - # Ensure child visuals are visible above parent - if hasattr(child_obj, 'z_offset'): - try: - parent_z = getattr(parent_obj, 'z_offset', 0) - child_obj.z_offset = float(parent_z) + 2.0 - except Exception: - pass + return new_local_left, new_local_top - # Special handling for Button: raise internal sprites/label above parent - if child_data.get('type') == 'Button': - try: - layout = getattr(child_obj, '_layout', None) - if layout is not None: - for attr in ('_sprite_left', '_sprite_mid', '_sprite_right'): - spr = getattr(layout, attr, None) - if spr is not None and hasattr(spr, 'z_offset'): - spr.z_offset = float(getattr(child_obj, 'z_offset', 0)) + 1.0 - lbl = getattr(child_obj, '_label', None) - if lbl is not None and hasattr(lbl, 'z_offset'): - lbl.z_offset = float(getattr(child_obj, 'z_offset', 0)) + 2.0 - # Ensure label visible - if lbl is not None and hasattr(lbl, 'show'): - lbl.show() - except Exception: - pass - - # Special handling for Frame: ensure visibility - elif child_data.get('type') == 'Frame': - try: - if hasattr(child_obj, 'show'): - child_obj.show() - if hasattr(child_obj, 'visible'): - child_obj.visible = True - print(f"[_set_parent_child_relationship] Frame visibility ensured") - except Exception as e: - print(f"[_set_parent_child_relationship] Error ensuring Frame visibility: {e}") - - # Special handling for Plane, Image, Video: ensure sprite visibility - elif child_data.get('type') in ['Plane', 'Image', 'Video', 'HttpText']: - try: - spr = child_data.get('sprite') - if spr is not None: - if hasattr(spr, 'show'): - spr.show() - if hasattr(spr, 'visible'): - spr.visible = True - - # Ensure sprite color is not transparent - if child_data.get('type') == 'Plane': - color = child_data.get('color', (1, 1, 1, 1)) - if hasattr(spr, 'color'): - spr.color = color - - print(f"[_set_parent_child_relationship] {child_data.get('type')} sprite visibility ensured") - except Exception as e: - print(f"[_set_parent_child_relationship] Error ensuring {child_data.get('type')} visibility: {e}") + def _sync_child_canvas_index_with_parent(self, child_data, parent_data): + if not child_data.get('visual_parent_canvas'): + return + try: + child_data['canvas_index'] = parent_data.get('canvas_index', self.current_canvas_index) + except Exception: + pass - child_sprite = child_data.get('sprite') - if child_sprite is not None: - try: - if hasattr(child_sprite, 'show'): - child_sprite.show() - # Ensure sprite is attached to the child object - if hasattr(child_sprite, 'parent') and child_sprite.parent != child_obj: - child_sprite.parent = child_obj - # Restore sprite color if stored - if hasattr(child_sprite, 'color') and 'color' in child_data: - child_sprite.color = child_data.get('color', child_sprite.color) - if hasattr(child_sprite, 'z_offset'): - parent_sprite = parent_data.get('sprite') - parent_sprite_z = 0.0 - if parent_sprite is not None and hasattr(parent_sprite, 'z_offset'): - parent_sprite_z = float(parent_sprite.z_offset) - child_sprite.z_offset = max(parent_sprite_z + 1.0, float(getattr(child_obj, 'z_offset', 0)) + 1.0) - except Exception: - pass - # Keep child visible above parent visuals - if hasattr(child_obj, 'z_offset'): + def _apply_child_absolute_position(self, child_obj, current_abs_left, current_abs_top, keep_world): + if hasattr(child_obj, 'left'): + child_obj.left = current_abs_left + if hasattr(child_obj, 'top'): + child_obj.top = current_abs_top + + if keep_world: + print(f"[_set_parent_child_relationship] Child position kept at absolute: ({current_abs_left:.1f}, {current_abs_top:.1f})") + else: + print(f"[_set_parent_child_relationship] Child position set to absolute: ({current_abs_left:.1f}, {current_abs_top:.1f})") + + def _ensure_child_visibility_and_layer(self, child_data, parent_data, child_obj, parent_obj): + if hasattr(child_obj, 'z_offset'): + try: parent_z = getattr(parent_obj, 'z_offset', 0) - try: - child_obj.z_offset = float(parent_z) + 2.0 - except Exception: - pass - # Ensure child sprite renders above parent visuals - child_sprite = child_data.get('sprite') - if child_sprite is not None and hasattr(child_sprite, 'z_offset'): - parent_sprite_z = 0 - try: - parent_sprite = parent_data.get('sprite') - if parent_sprite is not None and hasattr(parent_sprite, 'z_offset'): - parent_sprite_z = float(parent_sprite.z_offset) - except Exception: - parent_sprite_z = 0 - try: - child_sprite.z_offset = max(float(parent_sprite_z) + 1.0, float(getattr(child_obj, 'z_offset', 0)) + 1.0) - except Exception: - pass + child_obj.z_offset = float(parent_z) + 2.0 + except Exception: + pass + + comp_type = child_data.get('type') + if comp_type == 'Button': + self._ensure_button_child_visibility(child_obj) + elif comp_type == 'Frame': + self._ensure_frame_child_visibility(child_obj) + elif comp_type in ['Plane', 'Image', 'Video', 'HttpText']: + self._ensure_media_child_visibility(child_data) + + self._ensure_child_sprite_visibility_and_layer(child_data, parent_data, child_obj) + if hasattr(child_obj, 'show'): + child_obj.show() + + def _ensure_button_child_visibility(self, child_obj): + try: + layout = getattr(child_obj, '_layout', None) + if layout is not None: + for attr in ('_sprite_left', '_sprite_mid', '_sprite_right'): + spr = getattr(layout, attr, None) + if spr is not None and hasattr(spr, 'z_offset'): + spr.z_offset = float(getattr(child_obj, 'z_offset', 0)) + 1.0 + lbl = getattr(child_obj, '_label', None) + if lbl is not None and hasattr(lbl, 'z_offset'): + lbl.z_offset = float(getattr(child_obj, 'z_offset', 0)) + 2.0 + if lbl is not None and hasattr(lbl, 'show'): + lbl.show() + except Exception: + pass + + def _ensure_frame_child_visibility(self, child_obj): + try: if hasattr(child_obj, 'show'): child_obj.show() - + if hasattr(child_obj, 'visible'): + child_obj.visible = True + print("[_set_parent_child_relationship] Frame visibility ensured") except Exception as e: - print(f"Reparenting error: {e}") + print(f"[_set_parent_child_relationship] Error ensuring Frame visibility: {e}") - # Ensure canvas_index follows parent canvas (visibility) + def _ensure_media_child_visibility(self, child_data): + try: + spr = child_data.get('sprite') + if spr is not None: + if hasattr(spr, 'show'): + spr.show() + if hasattr(spr, 'visible'): + spr.visible = True + if child_data.get('type') == 'Plane' and hasattr(spr, 'color'): + spr.color = child_data.get('color', (1, 1, 1, 1)) + print(f"[_set_parent_child_relationship] {child_data.get('type')} sprite visibility ensured") + except Exception as e: + print(f"[_set_parent_child_relationship] Error ensuring {child_data.get('type')} visibility: {e}") + + def _ensure_child_sprite_visibility_and_layer(self, child_data, parent_data, child_obj): + child_sprite = child_data.get('sprite') + if child_sprite is None: + return + + try: + if hasattr(child_sprite, 'show'): + child_sprite.show() + if hasattr(child_sprite, 'parent') and child_sprite.parent != child_obj: + child_sprite.parent = child_obj + if hasattr(child_sprite, 'color') and 'color' in child_data: + child_sprite.color = child_data.get('color', child_sprite.color) + + if hasattr(child_sprite, 'z_offset'): + parent_sprite_z = 0.0 + parent_sprite = parent_data.get('sprite') + if parent_sprite is not None and hasattr(parent_sprite, 'z_offset'): + parent_sprite_z = float(parent_sprite.z_offset) + child_sprite.z_offset = max( + parent_sprite_z + 1.0, + float(getattr(child_obj, 'z_offset', 0)) + 1.0, + ) + except Exception: + pass + + def _finalize_parent_child_link(self, child_index, parent_index, parent_data): parent_canvas = parent_data.get('canvas_index') if parent_canvas is not None: self._update_canvas_index_recursive(child_index, parent_canvas) diff --git a/ui/LUI/lui_manager_interaction.py b/ui/LUI/lui_manager_interaction.py index f341f9b3..af85fec5 100644 --- a/ui/LUI/lui_manager_interaction.py +++ b/ui/LUI/lui_manager_interaction.py @@ -1213,68 +1213,140 @@ class LUIManagerInteractionMixin: print("✓ 重新创建了手柄,确保在最前面") def _handle_resize_drag(self): + """处理 resize 拖拽(编排入口)。""" + self._handle_resize_drag_core() + + def _handle_resize_drag_core(self): """处理resize拖拽 - 完整功能实现,支持Shift和Alt键操作""" if not hasattr(self.world, 'mouseWatcherNode') or not self.world.mouseWatcherNode.hasMouse(): return - - # 检查鼠标左键是否释放 + import panda3d.core as p3d - if not self.world.mouseWatcherNode.is_button_down(p3d.MouseButton.one()): - if self.resizing_handle: - comp_data = self.components[self.selected_index] - width_val = comp_data.get('width', 0) - height_val = comp_data.get('height', 0) - print(f"✓ 组件调整完成: width={width_val:.1f}, height={height_val:.1f}") - - # 更新锚点位置(如果有锚点) - if comp_data.get('anchored_to_parent'): - anchor_pos = comp_data.get('anchor_position') - if anchor_pos: - self._update_component_anchor_position(self.selected_index, anchor_pos) - - self.resizing_handle = None + if self._finish_resize_drag_if_released(p3d): return - + # 获取当前鼠标位置 mouse_x = self.world.mouseWatcherNode.getMouseX() mouse_y = self.world.mouseWatcherNode.getMouseY() - + win_x = self.world.win.getXSize() win_y = self.world.win.getYSize() pixel_x = (mouse_x + 1) * win_x / 2 pixel_y = (1 - mouse_y) * win_y / 2 - + # 计算鼠标移动距离 delta_x = pixel_x - self.resize_start_pos[0] delta_y = pixel_y - self.resize_start_pos[1] - + # 检查修饰键 shift_held = self.world.mouseWatcherNode.is_button_down(p3d.KeyboardButton.shift()) alt_held = self.world.mouseWatcherNode.is_button_down(p3d.KeyboardButton.alt()) - + # 获取组件数据 comp_data = self.components[self.selected_index] comp_obj = comp_data['object'] comp_type = comp_data['type'] - - # 获取初始边界(Canvas相对坐标) + + new_left, new_top, new_width, new_height = self._compute_resize_drag_bounds( + delta_x, delta_y, shift_held, alt_held + ) + + if self.selected_index < 0: + return + + comp_data = self.components[self.selected_index] + self._apply_resize_drag_updates( + comp_data, comp_obj, comp_type, + new_left, new_top, new_width, new_height, + shift_held, alt_held, + ) + + def _finish_resize_drag_if_released(self, p3d): + """鼠标释放时结束缩放并执行收尾同步。""" + if self.world.mouseWatcherNode.is_button_down(p3d.MouseButton.one()): + return False + + if self.resizing_handle and self.selected_index >= 0 and self.selected_index < len(self.components): + comp_data = self.components[self.selected_index] + width_val = comp_data.get('width', 0) + height_val = comp_data.get('height', 0) + print(f"✓ 组件调整完成: width={width_val:.1f}, height={height_val:.1f}") + + # 更新锚点位置(如果有锚点) + if comp_data.get('anchored_to_parent'): + anchor_pos = comp_data.get('anchor_position') + if anchor_pos: + self._update_component_anchor_position(self.selected_index, anchor_pos) + + self.resizing_handle = None + return True + + def _compute_resize_drag_bounds(self, delta_x, delta_y, shift_held, alt_held): + """根据当前手柄/修饰键计算新的 left/top/width/height。""" + start_left, start_top, start_width, start_height, aspect_ratio = self._get_resize_start_values() + handle_pos = self.resizing_handle._resize_position + + new_left, new_top, new_width, new_height = self._apply_resize_handle_delta( + handle_pos, start_left, start_top, start_width, start_height, delta_x, delta_y + ) + new_left, new_top, new_width, new_height = self._apply_resize_min_size_clamp( + handle_pos, start_left, start_top, start_width, start_height, + new_left, new_top, new_width, new_height, + ) + + if shift_held: + new_left, new_top, new_width, new_height = self._apply_shift_keep_ratio_resize( + handle_pos, start_left, start_top, start_width, start_height, + new_left, new_top, new_width, new_height, aspect_ratio, + ) + + if alt_held: + new_left, new_top, new_width, new_height = self._apply_alt_center_resize( + start_left, start_top, start_width, start_height, + new_left, new_top, new_width, new_height, + ) + + self._cache_resize_canvas_bounds() + return self._sanitize_resize_dimensions(new_left, new_top, new_width, new_height) + + def _apply_resize_drag_updates( + self, comp_data, comp_obj, comp_type, + new_left, new_top, new_width, new_height, + shift_held, alt_held, + ): + """将缩放结果写回组件数据与渲染对象。""" + parent_index, parent_offset_x, parent_offset_y, is_scene_parented = self._resolve_resize_parent_context( + comp_data, comp_obj + ) + _ = parent_index + + self._write_resize_component_data(comp_data, new_left, new_top, new_width, new_height) + self._apply_resize_component_position( + comp_obj, new_left, new_top, parent_offset_x, parent_offset_y, is_scene_parented + ) + + try: + self._apply_resize_component_size_by_type(comp_data, comp_obj, comp_type, new_width, new_height) + self._post_resize_component_sync(comp_type, shift_held, alt_held) + except Exception as e: + print(f"⚠ 设置组件尺寸失败 ({comp_type}): {e}") + + def _get_resize_start_values(self): start_left = self.resize_start_bounds['left'] start_top = self.resize_start_bounds['top'] start_width = self.resize_start_bounds['width'] start_height = self.resize_start_bounds['height'] - - # 计算初始宽高比 aspect_ratio = start_width / start_height if start_height > 0 else 1 - - # 根据手柄位置计算新的边界 - handle_pos = self.resizing_handle._resize_position - + return start_left, start_top, start_width, start_height, aspect_ratio + + def _apply_resize_handle_delta( + self, handle_pos, start_left, start_top, start_width, start_height, delta_x, delta_y + ): new_left = start_left new_top = start_top new_width = start_width new_height = start_height - - # 根据手柄位置调整尺寸 + if handle_pos == 'top-left': new_left = start_left + delta_x new_top = start_top + delta_y @@ -1301,171 +1373,191 @@ class LUIManagerInteractionMixin: elif handle_pos == 'left': new_left = start_left + delta_x new_width = start_width - delta_x - - # 应用最小尺寸限制 + + return new_left, new_top, new_width, new_height + + def _apply_resize_min_size_clamp( + self, handle_pos, start_left, start_top, start_width, start_height, + new_left, new_top, new_width, new_height, + ): if new_width < self.min_size: if handle_pos in ['top-left', 'left', 'bottom-left']: new_left = start_left + start_width - self.min_size new_width = self.min_size - + if new_height < self.min_size: if handle_pos in ['top-left', 'top', 'top-right']: new_top = start_top + start_height - self.min_size new_height = self.min_size - - # Shift键:保持宽高比 - if shift_held: - width_change = abs(new_width - start_width) - height_change = abs(new_height - start_height) - - if width_change > height_change: - new_height = new_width / aspect_ratio - if handle_pos in ['top-left', 'top', 'top-right']: - new_top = start_top + start_height - new_height - else: - new_width = new_height * aspect_ratio - if handle_pos in ['top-left', 'left', 'bottom-left']: - new_left = start_left + start_width - new_width - - # Alt键:从中心点缩放 - if alt_held: - center_x = start_left + start_width / 2 - center_y = start_top + start_height / 2 - - width_diff = new_width - start_width - height_diff = new_height - start_height - - new_width = start_width + width_diff * 2 - new_height = start_height + height_diff * 2 + + return new_left, new_top, new_width, new_height + + def _apply_shift_keep_ratio_resize( + self, handle_pos, start_left, start_top, start_width, start_height, + new_left, new_top, new_width, new_height, aspect_ratio, + ): + width_change = abs(new_width - start_width) + height_change = abs(new_height - start_height) + + if width_change > height_change: + new_height = new_width / aspect_ratio + if handle_pos in ['top-left', 'top', 'top-right']: + new_top = start_top + start_height - new_height + else: + new_width = new_height * aspect_ratio + if handle_pos in ['top-left', 'left', 'bottom-left']: + new_left = start_left + start_width - new_width + + return new_left, new_top, new_width, new_height + + def _apply_alt_center_resize( + self, start_left, start_top, start_width, start_height, + new_left, new_top, new_width, new_height, + ): + center_x = start_left + start_width / 2 + center_y = start_top + start_height / 2 + + width_diff = new_width - start_width + height_diff = new_height - start_height + new_width = start_width + width_diff * 2 + new_height = start_height + height_diff * 2 + new_left = center_x - new_width / 2 + new_top = center_y - new_height / 2 + + if new_width < self.min_size: + new_width = self.min_size new_left = center_x - new_width / 2 + if new_height < self.min_size: + new_height = self.min_size new_top = center_y - new_height / 2 - - # 再次应用最小尺寸限制 - if new_width < self.min_size: - new_width = self.min_size - new_left = center_x - new_width / 2 - if new_height < self.min_size: - new_height = self.min_size - new_top = center_y - new_height / 2 - - # 获取Canvas边界约束 - canvas_width = 800 # 默认Canvas宽度 - canvas_height = 600 # 默认Canvas高度 - + + return new_left, new_top, new_width, new_height + + def _cache_resize_canvas_bounds(self): + canvas_width = 800 + canvas_height = 600 if self.current_canvas_index >= 0 and self.current_canvas_index < len(self.canvases): canvas_panel = self.canvases[self.current_canvas_index]['panel'] canvas_width = canvas_panel.get_width() canvas_height = canvas_panel.get_height() - - - # 最后的安全检查,防止宽高变为负数 - if new_width < 1.0: new_width = 1.0 - if new_height < 1.0: new_height = 1.0 - - if self.selected_index < 0: - return + _ = (canvas_width, canvas_height) - comp_data = self.components[self.selected_index] + def _sanitize_resize_dimensions(self, new_left, new_top, new_width, new_height): + if new_width < 1.0: + new_width = 1.0 + if new_height < 1.0: + new_height = 1.0 + return new_left, new_top, new_width, new_height + + def _resolve_resize_parent_context(self, comp_data, comp_obj): parent_index = comp_data.get('parent_index') parent_offset_x = 0 parent_offset_y = 0 - if parent_index is not None and parent_index >= 0: - parent_offset_x, parent_offset_y = self._get_component_accumulated_pos(parent_index) + parent_offset_x, parent_offset_y = self._get_component_accumulated_pos(parent_index) - # Update component data + child_parent = getattr(comp_obj, 'parent', None) + parent_obj = None + if parent_index is not None and parent_index >= 0: + parent_obj = self.components[parent_index].get('object') + is_scene_parented = (child_parent is not None and child_parent == parent_obj) + return parent_index, parent_offset_x, parent_offset_y, is_scene_parented + + def _write_resize_component_data(self, comp_data, new_left, new_top, new_width, new_height): comp_data['left'] = new_left comp_data['top'] = new_top comp_data['width'] = new_width comp_data['height'] = new_height - # Update component position - child_parent = getattr(comp_obj, 'parent', None) - parent_obj = None - if parent_index is not None and parent_index >= 0: - parent_obj = self.components[parent_index].get('object') - - # Check if actually parented in LUI scene graph - is_scene_parented = (child_parent is not None and child_parent == parent_obj) - + def _apply_resize_component_position( + self, comp_obj, new_left, new_top, parent_offset_x, parent_offset_y, is_scene_parented + ): if is_scene_parented: - # Component is physically parented to its logical parent -> Use Relative coords comp_obj.left = new_left comp_obj.top = new_top else: - # Component is physically root/canvas -> Use Absolute coords (Parent Abs + Relative) comp_obj.left = parent_offset_x + new_left comp_obj.top = parent_offset_y + new_top - # Update component size + def _apply_resize_component_size_by_type(self, comp_data, comp_obj, comp_type, new_width, new_height): + if hasattr(comp_obj, 'width'): + comp_obj.width = new_width + if hasattr(comp_obj, 'height'): + comp_obj.height = new_height + + if comp_type == 'Frame': + pass + elif comp_type in ['Plane', 'Image', 'Video', 'HttpText']: + sprite = comp_data.get('sprite') + if sprite is not None: + sprite.width = new_width + sprite.height = new_height + if comp_type == 'HttpText': + self.luiFunction.sync_http_text_layout(self, comp_data) + elif comp_type == 'Button': + self._apply_button_resize_size(comp_obj, new_width, new_height) + elif comp_type == 'InputField': + self._apply_input_field_resize_size(comp_obj, new_width, new_height) + elif comp_type == 'Slider': + if hasattr(comp_obj, 'set_width'): + comp_obj.set_width(new_width) + if hasattr(comp_obj, 'set_height'): + comp_obj.set_height(new_height) + + def _apply_button_resize_size(self, comp_obj, new_width, new_height): + if hasattr(comp_obj, 'set_width'): + comp_obj.set_width(new_width) + if hasattr(comp_obj, 'set_height'): + comp_obj.set_height(new_height) + if hasattr(comp_obj, '_apply_stretch_sizes'): + comp_obj._apply_stretch_sizes() + + def _apply_input_field_resize_size(self, comp_obj, new_width, new_height): + if hasattr(comp_obj, 'set_width'): + comp_obj.set_width(new_width) + if hasattr(comp_obj, 'set_height'): + comp_obj.set_height(new_height) + if hasattr(comp_obj, 'width'): + comp_obj.width = new_width + if hasattr(comp_obj, 'height'): + comp_obj.height = new_height + self._sync_input_field_layout_stretch(comp_obj) + + def _sync_input_field_layout_stretch(self, comp_obj): try: - if hasattr(comp_obj, 'width'): - comp_obj.width = new_width - if hasattr(comp_obj, 'height'): - comp_obj.height = new_height + layout = getattr(comp_obj, '_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 and hasattr(spr, 'height'): + spr.height = '100%' + if spr is not None and attr == '_sprite_mid' and hasattr(spr, 'width'): + spr.width = '100%' + except Exception: + pass - if comp_type == 'Frame': - pass - elif comp_type in ['Plane', 'Image', 'Video', 'HttpText']: - sprite = comp_data.get('sprite') - if sprite is not None: - sprite.width = new_width - sprite.height = new_height - if comp_type == 'HttpText': - self.luiFunction.sync_http_text_layout(self, comp_data) - elif comp_type == 'Button': - if hasattr(comp_obj, 'set_width'): - comp_obj.set_width(new_width) - if hasattr(comp_obj, 'set_height'): - comp_obj.set_height(new_height) - if hasattr(comp_obj, '_apply_stretch_sizes'): - comp_obj._apply_stretch_sizes() - elif comp_type == 'InputField': - if hasattr(comp_obj, 'set_width'): - comp_obj.set_width(new_width) - if hasattr(comp_obj, 'set_height'): - comp_obj.set_height(new_height) - if hasattr(comp_obj, 'width'): - comp_obj.width = new_width - if hasattr(comp_obj, 'height'): - comp_obj.height = new_height - try: - layout = getattr(comp_obj, '_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 and hasattr(spr, 'height'): - spr.height = '100%' - if spr is not None and attr == '_sprite_mid' and hasattr(spr, 'width'): - spr.width = '100%' - except Exception: - pass - elif comp_type == 'Slider': - if hasattr(comp_obj, 'set_width'): - comp_obj.set_width(new_width) - if hasattr(comp_obj, 'set_height'): - comp_obj.set_height(new_height) + def _post_resize_component_sync(self, comp_type, shift_held, alt_held): + if comp_type in ['VerticalLayout', 'HorizontalLayout']: + self._update_layout_inner(self.selected_index) - if comp_type in ['VerticalLayout', 'HorizontalLayout']: - self._update_layout_inner(self.selected_index) + self._update_anchored_children(self.selected_index) + modifier_str = self._get_resize_modifier_info(shift_held, alt_held) + _ = modifier_str - self._update_anchored_children(self.selected_index) - - modifier_info = [] - if shift_held: - modifier_info.append('Shift(keep ratio)') - if alt_held: - modifier_info.append('Alt(center scale)') - modifier_str = ' + '.join(modifier_info) if modifier_info else 'Normal' - except Exception as e: - print(f"⚠ 设置组件尺寸失败 ({comp_type}): {e}") + def _get_resize_modifier_info(self, shift_held, alt_held): + modifier_info = [] + if shift_held: + modifier_info.append('Shift(keep ratio)') + if alt_held: + modifier_info.append('Alt(center scale)') + return ' + '.join(modifier_info) if modifier_info else 'Normal'