EG/ui/lui_manager.py
2026-02-25 14:42:22 +08:00

3378 lines
147 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import os
import sys
import struct
from pathlib import Path
import panda3d.core as p3d
from panda3d.core import NodePath, CardMaker
from ui.lui_function import luiFunction
# Ensure ui directory is in path and lui is correctly patched
UI_DIR = Path(__file__).resolve().parent
if str(UI_DIR) not in sys.path:
sys.path.insert(0, str(UI_DIR))
# Add Builtin to path
BUILTIN_DIR = UI_DIR / "Builtin"
if str(BUILTIN_DIR) not in sys.path:
sys.path.insert(0, str(BUILTIN_DIR))
# Handle DLL loading for lui.pyd
import panda3d
panda_dir = os.path.dirname(panda3d.__file__)
if hasattr(os, "add_dll_directory"):
try:
os.add_dll_directory(panda_dir)
os.add_dll_directory(str(UI_DIR))
except Exception as e:
print(f"Warning: Failed to add DLL directory: {e}")
try:
import lui
# Monkey patch for Builtin components
import panda3d
panda3d.lui = lui
sys.modules["panda3d.lui"] = lui
from Builtin.LUIRegion import LUIRegion
from Builtin.LUIInputHandler import LUIInputHandler
from Builtin.LUIButton import LUIButton
from Builtin.LUILabel import LUILabel
from Builtin.LUIInputField import LUIInputField
from Builtin.LUISlider import LUISlider
from Builtin.LUIFrame import LUIFrame
from Builtin.LUISkin import LUIDefaultSkin
from Builtin.LUISprite import LUISprite
from Builtin.LUIObject import LUIObject
except ImportError as e:
print(f"Error: Failed to import LUI: {e}")
lui = None
from imgui_bundle import imgui, imgui_ctx
class LUIManager:
"""Manager for LUI system integration"""
def __init__(self, world):
self.world = world
self.lui_enabled = (lui is not None)
# Always initialize selection fields, even when LUI is unavailable.
self.selected_index = -1
self._last_selected_index = -1
self.luiFunction = luiFunction
# Editor State
self.show_editor = True
self.play_mode = False
self._selected_type_index = 0
if not self.lui_enabled:
print("LUI is not available, LUIManager will be disabled.")
return
# 1. 优先注册中文字体 (必须在皮肤加载前)
self._register_chinese_font()
# 2. 初始化核心 LUI 区域
self._init_ui_regions()
# 3. 加载皮肤资源
self.lui_skin = LUIDefaultSkin()
self.lui_skin.load()
# 4. 状态管理 - 与engine保持一致
self.canvases = []
self.current_canvas_index = -1
self.canvas_counter = 0
self.components = []
self.root_order = []
self._tree_drag_src = None
self.selected_index = -1
self._last_selected_index = -1
# 拖拽和选择状态
self.dragging_comp = None
self.pending_drag_comp = None
self.pending_drag_index = -1
self.pending_drag_start_mouse = None
self.pending_drag_start_abs = None
self.pending_drag_start_parent = None
self.dragging_index = -1
self.drag_start_threshold = 6.0
self.drag_offset = (0, 0)
# 锚点功能相关变量
self.anchor_popup_open = False
self.pending_child_creation = False
self.pending_parent_index = -1
# 子父对齐功能
self.child_parent_alignment_enabled = True
# Resize handles 相关变量
self.resize_handles = []
self.selection_box = []
self.debug_outlines = {} # comp_index -> [border sprites]
self.resizing_handle = None
self.resize_start_pos = (0, 0)
self.resize_start_bounds = {}
self.min_size = 20
# 使用外部字典管理状态,避免修改 C++ 对象属性导致 AttributeError
self.drag_states = {}
self._last_interaction_frame = -1 # 记录最后一次交互的帧数,用于防止事件穿透
self._input_enabled = True
# 创建resize handles和selection box延迟创建确保在Canvas之后
if hasattr(self.world, 'taskMgr'):
self.world.taskMgr.doMethodLater(0.1, self._delayed_create_resize_handles, "delayed_create_resize_handles")
else:
self._create_resize_handles()
# 添加任务来处理拖动和resize handles更新
if hasattr(self.world, 'taskMgr'):
self.world.taskMgr.add(self._update_drag, "update_lui_drag")
self.world.taskMgr.add(self._update_resize_handles, "update_resize_handles")
self.world.taskMgr.add(self._update_component_outlines, "update_lui_outlines")
print("✓ LUIManager initialized with complete functionality")
def set_input_enabled(self, enabled):
"""Enable/disable LUI input handling (used to avoid ImGui click-through)."""
if not self.lui_enabled:
return
if enabled == self._input_enabled:
return
self._input_enabled = enabled
try:
if enabled:
if hasattr(self, "overlay_handler_node") and self.overlay_handler_node:
self.overlay_handler_node.reparent_to(self.world.mouseWatcher)
if hasattr(self, "overlay_region") and self.overlay_region:
self.overlay_region.set_input_handler(self.overlay_handler)
else:
if hasattr(self, "overlay_region") and self.overlay_region:
self.overlay_region.set_input_handler(None)
if hasattr(self, "overlay_handler_node") and self.overlay_handler_node:
self.overlay_handler_node.detach_node()
except Exception as e:
print(f"Warning: Failed to toggle LUI input state: {e}")
def _register_chinese_font(self):
"""注册支持中文的字体"""
from panda3d.lui import LUIFontPool
import os
# 尝试常见的系统路径,确保使用完整的 OS 路径
candidate_fonts = [
"C:/Windows/Fonts/msyh.ttc",
"C:/Windows/Fonts/simhei.ttf",
"C:/Windows/Fonts/arial.ttf"
]
font_registered = False
for fpath in candidate_fonts:
if os.path.exists(fpath):
try:
# 在 LUI 中,建议直接用 loader.loadFont 载入后获取动态库指针
# 或者直接传递路径字符串
font = self.world.loader.loadFont(fpath)
if font:
LUIFontPool.get_global_ptr().register_font("default", font)
LUIFontPool.get_global_ptr().register_font("label", font)
print(f"✓ LUI 成功注册字体: {fpath}")
font_registered = True
break
except Exception as e:
print(f"LUI 字体注册失败 {fpath}: {e}")
if not font_registered:
# 如果没有系统字体,至少注册一个默认的避免崩溃
print("⚠️ 警告: 未能找到合适的 LUI 中文字体")
def _init_ui_regions(self):
"""初始化 LUI 区域 (Overlay, Camera, WorldSpace)"""
# 渲染层级说明参考engine/main.py的设置
# - 3D场景render默认sort=0
# - LUI组件设置sort=5在3D场景之上
# - pixel2d (ImGui) 默认sort=20在LUI之上
# 这样3D场景 < LUI组件 < ImGui面板
self.overlay_region = LUIRegion.make("OverlayUI", self.world.win)
self.overlay_handler = LUIInputHandler()
self.overlay_handler_node = self.world.mouseWatcher.attach_new_node(self.overlay_handler)
self.overlay_region.set_input_handler(self.overlay_handler)
self.overlay_region.set_sort(5) # 在3D场景之上在ImGui之下与engine保持一致
self.overlay_root = self.overlay_region.root
# 调试输出:确认层级设置
actual_sort = self.overlay_region.getSort()
print(f"✓ LUI Overlay Region 层级设置: {actual_sort}")
# 显示所有DisplayRegion的层级信息
num_regions = self.world.win.getNumDisplayRegions()
print(f"✓ 窗口总共有 {num_regions} 个DisplayRegion:")
for i in range(num_regions):
dr = self.world.win.getDisplayRegion(i)
sort_value = dr.getSort()
camera = dr.getCamera()
camera_name = camera.getName() if not camera.isEmpty() else "无摄像机"
print(f" Region {i}: sort={sort_value}, camera={camera_name}")
# 当前操作的根节点
self.root = self.overlay_root
def _make_canvas_draggable(self, canvas_panel):
"""使Canvas可拖拽"""
def on_canvas_drag_start(event):
# 检查是否有组件或手柄在当前帧被点击
current_frame = 0
if hasattr(p3d, 'ClockObject'):
current_frame = p3d.ClockObject.getGlobalClock().getFrameCount()
else:
current_frame = self.world.taskMgr.globalClock.getFrameCount()
if self._last_interaction_frame == current_frame:
# 当前帧已经处理了组件或手柄的点击忽略Canvas点击事件
return
# 点击空白处:不自动取消选中,避免事件穿透导致闪退
# self.deselect_all()
# 如果启用填充模式,则自动禁用,以便用户可以自由拖动
canvas_index = -1
for i, c in enumerate(self.canvases):
if c['panel'] == canvas_panel:
canvas_index = i
break
if canvas_index >= 0:
canvas_data = self.canvases[canvas_index]
# 如果是固定模式,则禁止拖动初始化
if canvas_data.get('fixed', False):
return
if canvas_data.get('fill_mode', False):
canvas_data['fill_mode'] = False
print("✓ 自动禁用填充模式以允许自由拖动")
# 记录Canvas拖拽状态
if hasattr(self.world, 'mouseWatcherNode') and self.world.mouseWatcherNode.hasMouse():
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
pos = canvas_panel.get_pos()
self.canvas_drag_offset = (pixel_x - pos.x, pixel_y - pos.y)
self.dragging_canvas = canvas_panel
canvas_panel.bind("mousedown", on_canvas_drag_start)
def switch_canvas(self, index):
"""切换到指定Canvas并显示/隐藏相应的UI组件"""
if index < 0 or index >= len(self.canvases):
return
self.current_canvas_index = index
# 显示选中的Canvas隐藏其他Canvas
for i, canvas in enumerate(self.canvases):
if i == index:
canvas['panel'].show()
canvas['visible'] = True
else:
canvas['panel'].hide()
canvas['visible'] = False
# 显示/隐藏属于不同Canvas的组件包括子组件
visible_count = 0
hidden_count = 0
for comp in self.components:
comp_canvas_index = comp.get('canvas_index')
comp_obj = comp['object']
comp_name = comp.get('name', 'Unknown')
should_be_visible = (comp_canvas_index == index)
if should_be_visible:
if hasattr(comp_obj, 'show'):
comp_obj.show()
elif hasattr(comp_obj, 'visible'):
comp_obj.visible = True
visible_count += 1
else:
if hasattr(comp_obj, 'hide'):
comp_obj.hide()
elif hasattr(comp_obj, 'visible'):
comp_obj.visible = False
hidden_count += 1
# 更新当前根节点
if index >= 0:
self.root = self.canvases[index]['panel']
# 如果选中的组件不属于当前Canvas取消选中
if self.selected_index >= 0 and self.selected_index < len(self.components):
selected_comp = self.components[self.selected_index]
if selected_comp.get('canvas_index') != index:
if getattr(self, '_force_keep_selection_frames', 0) > 0:
pass
else:
self.selected_index = -1
self._hide_selection()
print(f"✓ Switched to Canvas: {self.canvases[index]['name']}")
print(f" 显示组件: {visible_count}, 隐藏组件: {hidden_count}")
def _update_canvas_geometry(self, canvas_data):
"""根据填充模式和边距更新Canvas几何信息"""
if not canvas_data.get('fill_mode', False):
return
panel = canvas_data['panel']
margins = canvas_data.get('margins', {'left':0,'right':0,'top':0,'bottom':0})
win_width = 800
win_height = 600
if hasattr(self.world, 'win'):
win_width = self.world.win.getXSize()
win_height = self.world.win.getYSize()
new_x = margins['left']
new_y = margins['top']
new_width = max(10, win_width - margins['left'] - margins['right'])
new_height = max(10, win_height - margins['top'] - margins['bottom'])
panel.set_pos(new_x, new_y)
panel.width = new_width
panel.height = new_height
if 'background' in canvas_data:
bg = canvas_data['background']
bg.width = new_width
bg.height = new_height
def _setup_component_drag(self, comp_data, comp_index):
"""为组件设置拖动功能 - 支持Canvas相对坐标系"""
comp_obj = comp_data['object']
def on_mouse_down(event):
comp_type = comp_data.get('type')
if getattr(self, 'play_mode', False):
# Let runtime widgets handle their own events (especially Slider)
if comp_type == 'Slider':
return
try:
if hasattr(comp_obj, 'on_mousedown'):
comp_obj.on_mousedown(event)
except Exception:
pass
return
# Prevent re-entrant drag
if self.dragging_comp is not None:
return
# Find current index
current_index = -1
for i, c in enumerate(self.components):
if c is comp_data:
current_index = i
break
if current_index == -1:
return
# Mark interaction frame
if hasattr(p3d, 'ClockObject'):
self._last_interaction_frame = p3d.ClockObject.getGlobalClock().getFrameCount()
else:
self._last_interaction_frame = self.world.taskMgr.globalClock.getFrameCount()
# Select component
self.selected_index = current_index
self._last_selected_index = current_index
self._force_keep_selection_frames = 3
# Force component to current canvas to avoid immediate deselect
try:
comp_data['canvas_index'] = self.current_canvas_index
except Exception:
pass
try:
if comp_data.get('canvas_index') is None:
comp_data['canvas_index'] = self.current_canvas_index
except Exception:
pass
try:
self._show_and_update_handles(comp_data)
except Exception:
pass
# Slider: allow knob/bar drag in edit mode without hijacking
if comp_type == 'Slider':
sender = getattr(event, 'sender', None)
knob = getattr(comp_obj, '_knob', None)
bg = getattr(comp_obj, '_slider_bg', None)
if sender is not None and (sender == knob or sender == bg):
# Let slider handle its own drag logic
try:
if hasattr(comp_obj, '_start_drag'):
comp_obj._start_drag(event)
except Exception:
pass
return
parent_idx = comp_data.get('parent_index')
if parent_idx is not None and parent_idx >= 0:
parent_type = self.components[parent_idx].get('type')
if parent_type in ['VerticalLayout', 'HorizontalLayout']:
return
if not comp_data.get('draggable', True):
return
self.pending_drag_comp = comp_data
self.pending_drag_index = current_index
self.pending_drag_start_abs = self._get_component_accumulated_pos(current_index)
self.pending_drag_start_parent = comp_obj.parent
self._is_drag_reparented = False
if hasattr(self.world, 'mouseWatcherNode') and self.world.mouseWatcherNode.hasMouse():
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
self.pending_drag_start_mouse = (pixel_x, pixel_y)
else:
self.pending_drag_start_mouse = None
# Bind mouse event
comp_obj.bind("mousedown", on_mouse_down)
# Slider selection bindings: allow clicking knob/bar to select in edit mode
try:
if comp_data.get('type') == 'Slider':
knob = getattr(comp_obj, '_knob', None)
bg = getattr(comp_obj, '_slider_bg', None)
except Exception:
pass
def on_mouse_up(event):
if getattr(self, 'play_mode', False):
try:
if hasattr(comp_obj, 'on_mouseup'):
comp_obj.on_mouseup(event)
except Exception:
pass
return
if comp_data.get('type') == 'Slider':
sender = getattr(event, 'sender', None)
knob = getattr(comp_obj, '_knob', None)
if sender is not None and sender == knob:
try:
if hasattr(comp_obj, '_stop_drag'):
comp_obj._stop_drag(event)
except Exception:
pass
comp_obj.bind("mouseup", on_mouse_up)
def _make_draggable(self, lui_obj):
"""启用 LUI 对象的鼠标拖拽功能 - 兼容旧接口"""
# 查找对应的组件索引
comp_index = -1
for i, comp in enumerate(self.components):
if comp.get('object') == lui_obj:
comp_index = i
break
if comp_index >= 0:
self._setup_component_drag(self.components[comp_index], comp_index)
def _get_component_accumulated_pos(self, comp_index):
"""递归计算组件相对于Canvas的累积位置"""
if comp_index < 0 or comp_index >= len(self.components):
return 0, 0
comp_data = self.components[comp_index]
comp_obj = comp_data['object']
# 优先使用递归计算(更可靠)
left = comp_data.get('left', 0)
top = comp_data.get('top', 0)
parent_index = comp_data.get('parent_index')
if parent_index is not None and parent_index >= 0:
parent_left, parent_top = self._get_component_accumulated_pos(parent_index)
result_x = parent_left + left
result_y = parent_top + top
print(f"[_get_component_accumulated_pos] Component #{comp_index} (has parent #{parent_index}): local=({left:.1f}, {top:.1f}), parent_offset=({parent_left:.1f}, {parent_top:.1f}), result=({result_x:.1f}, {result_y:.1f})")
return result_x, result_y
# 没有父组件直接返回局部坐标相对于Canvas
print(f"[_get_component_accumulated_pos] Component #{comp_index} (root): local=({left:.1f}, {top:.1f})")
return left, top
def _update_drag(self, task):
"""每帧更新拖动状态 - 支持Canvas边界约束和局部坐标"""
if getattr(self, 'play_mode', False):
# Disable editor drag logic while in play mode
return task.cont
# 1. 处理Canvas拖动
if hasattr(self, 'dragging_canvas') and self.dragging_canvas:
# 查找对应的Canvas数据
canvas_data = None
for c in self.canvases:
if c['panel'] == self.dragging_canvas:
canvas_data = c
break
# 检查Canvas是否锁定
if canvas_data and canvas_data.get('fixed', False):
return task.cont
if not hasattr(self.world, 'mouseWatcherNode') or not self.world.mouseWatcherNode.hasMouse():
return task.cont
import panda3d.core as p3d
if not self.world.mouseWatcherNode.is_button_down(p3d.MouseButton.one()):
self.dragging_canvas = None
return task.cont
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
new_x = pixel_x - self.canvas_drag_offset[0]
new_y = pixel_y - self.canvas_drag_offset[1]
self.dragging_canvas.set_pos(new_x, new_y)
return task.cont
# 2. 处理组件拖动
if self.dragging_comp is None:
# Pending drag: only start after threshold
if self.pending_drag_comp is None:
return task.cont
if not hasattr(self.world, 'mouseWatcherNode') or not self.world.mouseWatcherNode.hasMouse():
return task.cont
import panda3d.core as p3d
if not self.world.mouseWatcherNode.is_button_down(p3d.MouseButton.one()):
# click without drag
self.pending_drag_comp = None
self.pending_drag_index = -1
self.pending_drag_start_mouse = None
self.pending_drag_start_abs = None
self.pending_drag_start_parent = None
return task.cont
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
if self.pending_drag_start_mouse is None:
self.pending_drag_start_mouse = (pixel_x, pixel_y)
return task.cont
dx = pixel_x - self.pending_drag_start_mouse[0]
dy = pixel_y - self.pending_drag_start_mouse[1]
if (dx * dx + dy * dy) < (self.drag_start_threshold * self.drag_start_threshold):
return task.cont
# Start drag now
comp_data = self.pending_drag_comp
self.dragging_comp = comp_data
self.dragging_index = self.pending_drag_index
self._original_parent_obj = self.pending_drag_start_parent
self._is_drag_reparented = False
acc_left, acc_top = self.pending_drag_start_abs
if acc_left is None or acc_top is None:
acc_left, acc_top = self._get_component_accumulated_pos(self.pending_drag_index)
# If child, reparent to canvas for free drag
p_idx = comp_data.get('parent_index')
if p_idx is not None and p_idx >= 0 and self.current_canvas_index >= 0:
canvas_panel = self.canvases[self.current_canvas_index]['panel']
comp_obj = comp_data['object']
if not comp_data.get('visual_parent_canvas'):
try:
if getattr(comp_obj, 'parent', None) == canvas_panel:
pass
elif hasattr(comp_obj, 'reparent_to'):
comp_obj.reparent_to(canvas_panel)
if hasattr(comp_obj, 'set_pos'):
comp_obj.set_pos(acc_left, acc_top)
else:
comp_obj.left = acc_left
comp_obj.top = acc_top
self._is_drag_reparented = True
except Exception:
pass
# Compute drag offset
canvas_abs_left = 0
canvas_abs_top = 0
if self.current_canvas_index >= 0 and self.current_canvas_index < len(self.canvases):
canvas_panel = self.canvases[self.current_canvas_index]['panel']
canvas_abs_left = getattr(canvas_panel, 'left', 0)
canvas_abs_top = getattr(canvas_panel, 'top', 0)
if canvas_abs_left == 0 and canvas_abs_top == 0:
try:
canvas_pos = canvas_panel.get_pos()
canvas_abs_left = canvas_pos.x
canvas_abs_top = canvas_pos.y
except:
canvas_abs_left = 300
canvas_abs_top = 100
comp_abs_left = canvas_abs_left + acc_left
comp_abs_top = canvas_abs_top + acc_top
self.drag_offset = (pixel_x - comp_abs_left, pixel_y - comp_abs_top)
# Clear pending state
self.pending_drag_comp = None
self.pending_drag_index = -1
self.pending_drag_start_mouse = None
self.pending_drag_start_abs = None
self.pending_drag_start_parent = None
# Continue with drag in same frame
# 检查鼠标状态
if not hasattr(self.world, 'mouseWatcherNode') or not self.world.mouseWatcherNode.hasMouse():
return task.cont
# 检查鼠标左键是否释放
import panda3d.core as p3d
if not self.world.mouseWatcherNode.is_button_down(p3d.MouseButton.one()):
if self.dragging_comp:
comp_data = self.dragging_comp
print(f"组件移动到: left={comp_data.get('left', 0):.1f}, top={comp_data.get('top', 0):.1f} (局部坐标)")
# 拖动结束:检查是否需要接触父子关系 (Auto-unparent)
should_unparent = False
if getattr(self, '_is_drag_reparented', False):
comp_obj = comp_data['object']
# Check if center of component is outside parent bounds
p_idx = comp_data.get('parent_index')
if p_idx is not None and p_idx >= 0:
parent_data = self.components[p_idx]
p_w = parent_data.get('width', 100)
p_h = parent_data.get('height', 100)
c_w = comp_data.get('width', 0)
c_h = comp_data.get('height', 0)
# Current comp_data['left'] is LOCAL to parent
local_l = comp_data.get('left', 0)
local_t = comp_data.get('top', 0)
center_x = local_l + c_w / 2
center_y = local_t + c_h / 2
# Check bounds: center must be in 0..p_w, 0..p_h
# Allow some leniency or strict? Let's say if center is outside, unparent.
if center_x < 0 or center_x > p_w or center_y < 0 or center_y > p_h:
should_unparent = True
if should_unparent:
# Unparent!
# The component is currently visually attached to Canvas (from drag start)
# We just need to update the data to reflect this permanent change.
# Calculate the new global (canvas-relative) position
# current local + parent offset
p_off_x, p_off_y = self._get_component_accumulated_pos(p_idx)
new_canvas_left = local_l + p_off_x
new_canvas_top = local_t + p_off_y
comp_data['left'] = new_canvas_left
comp_data['top'] = new_canvas_top
# Remove parent linkage
if 'parent_index' in comp_data:
del comp_data['parent_index']
if 'anchored_to_parent' in comp_data:
del comp_data['anchored_to_parent']
print(f"✓ Auto-unparented component from #{p_idx} (Dragged out)")
elif hasattr(comp_obj, 'reparent_to') and hasattr(self, '_original_parent_obj'):
comp_obj.reparent_to(self._original_parent_obj)
# 重新应用计算出的局部坐标
comp_obj.left = comp_data.get('left', 0)
comp_obj.top = comp_data.get('top', 0)
print(f"✓ 组件归位到逻辑父级")
self.dragging_comp = None
return task.cont
# 获取鼠标位置
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
# 计算新的绝对位置(减去偏移量)
new_abs_left = pixel_x - self.drag_offset[0]
new_abs_top = pixel_y - self.drag_offset[1]
# 获取Canvas信息
canvas_abs_left = 0
canvas_abs_top = 0
canvas_width = 800
canvas_height = 600
if self.current_canvas_index >= 0 and self.current_canvas_index < len(self.canvases):
canvas_data = self.canvases[self.current_canvas_index]
canvas_panel = canvas_data['panel']
# 获取Canvas的绝对位置
canvas_abs_left = getattr(canvas_panel, 'left', 0)
canvas_abs_top = getattr(canvas_panel, 'top', 0)
if canvas_abs_left == 0 and canvas_abs_top == 0:
try:
pos = canvas_panel.get_pos()
canvas_abs_left = pos.x
canvas_abs_top = pos.y
except:
# 如果获取失败,使用默认值
canvas_abs_left = canvas_data.get('margins', {}).get('left', 240)
canvas_abs_top = canvas_data.get('margins', {}).get('top', 89)
# 获取Canvas的实际尺寸
try:
if hasattr(canvas_panel, 'width') and canvas_panel.width is not None:
canvas_width = float(canvas_panel.width)
elif hasattr(canvas_panel, 'get_width'):
canvas_width = float(canvas_panel.get_width())
if hasattr(canvas_panel, 'height') and canvas_panel.height is not None:
canvas_height = float(canvas_panel.height)
elif hasattr(canvas_panel, 'get_height'):
canvas_height = float(canvas_panel.get_height())
except Exception as e:
print(f"Warning: Failed to get canvas size: {e}")
# 使用窗口尺寸减去边距作为后备
margins = canvas_data.get('margins', {'left': 240, 'right': 480, 'top': 89, 'bottom': 220})
canvas_width = win_x - margins.get('left', 0) - margins.get('right', 0)
canvas_height = win_y - margins.get('top', 0) - margins.get('bottom', 0)
# 转换为Canvas相对坐标 (Global Canvas Pos)
canvas_relative_left = new_abs_left - canvas_abs_left
canvas_relative_top = new_abs_top - canvas_abs_top
# 获取当前拖拽的组件数据
comp_data = self.dragging_comp
comp_obj = comp_data['object']
# 获取组件宽高 (用于边界限制)
comp_width = comp_data.get('width', 0)
comp_height = comp_data.get('height', 0)
# 尝试从对象获取更准确的宽高
if hasattr(comp_obj, 'width') and comp_obj.width is not None and comp_obj.width > 0:
comp_width = comp_obj.width
if hasattr(comp_obj, 'height') and comp_obj.height is not None and comp_obj.height > 0:
comp_height = comp_obj.height
# 限制在Canvas范围内
# 确保左边界不小于0
if canvas_relative_left < 0:
canvas_relative_left = 0
# 确保上边界不小于0
if canvas_relative_top < 0:
canvas_relative_top = 0
# 确保右边界不超出Canvas宽度
if canvas_relative_left + comp_width > canvas_width:
canvas_relative_left = max(0, canvas_width - comp_width)
# 确保下边界不超出Canvas高度
if canvas_relative_top + comp_height > canvas_height:
canvas_relative_top = max(0, canvas_height - comp_height)
# 如果有父组件,需要转换为相对于父组件的局部坐标
comp_data = self.dragging_comp
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:
# 获取父组件相对于Canvas的累积位置
parent_offset_x, parent_offset_y = self._get_component_accumulated_pos(parent_index)
# 计算新的局部坐标
new_local_left = canvas_relative_left - parent_offset_x
new_local_top = canvas_relative_top - parent_offset_y
# 如果物理上已脱离父级挂在Canvas上则物理坐标就是Canvas相对坐标
comp_obj = comp_data['object']
if getattr(self, '_is_drag_reparented', False):
# 子组件被重定向到Canvas使其可以移动到负数局部坐标即父组件外侧
if hasattr(comp_obj, 'set_pos'):
comp_obj.set_pos(canvas_relative_left, canvas_relative_top)
else:
comp_obj.left = canvas_relative_left
comp_obj.top = canvas_relative_top
# print(f"Dragging Child (Canvas Rel): {canvas_relative_left:.1f}, {canvas_relative_top:.1f} | Local: {new_local_left:.1f}, {new_local_top:.1f}")
else:
# If visual parent is canvas, keep world position while storing local
if comp_data.get('visual_parent_canvas'):
abs_left = parent_offset_x + new_local_left
abs_top = parent_offset_y + new_local_top
if hasattr(comp_obj, 'set_pos'):
comp_obj.set_pos(abs_left, abs_top)
else:
comp_obj.left = abs_left
comp_obj.top = abs_top
else:
if hasattr(comp_obj, 'set_pos'):
comp_obj.set_pos(new_local_left, new_local_top)
else:
comp_obj.left = new_local_left
comp_obj.top = new_local_top
# 更新存储的逻辑坐标数据 (保持始终相对于逻辑父级)
comp_data['left'] = new_local_left
comp_data['top'] = new_local_top
# Sync visually-canvas children for non-container parents
if self.dragging_index is not None and self.dragging_index >= 0:
self._sync_canvas_children(self.dragging_index)
return task.cont
def _create_resize_handles(self):
"""创建选择框和8个resize手柄"""
from Builtin.LUISprite import LUISprite
from Builtin.LUIObject import LUIObject
print("开始创建resize handles...")
# 先创建4条选择框边线蓝色2px宽
for i in range(4):
border = LUISprite(self.overlay_root, "blank", "skin")
border.color = (0.2, 0.5, 1.0, 1.0) # 蓝色
border.visible = False
# 设置选择框层级在组件之上但在LUI内部
if hasattr(border, 'z_offset'):
border.z_offset = 40 # 选中框在普通边框(25)之上
elif hasattr(border, 'set_sort'):
border.set_sort(40)
# 确保边框在最后创建,这样在渲染顺序上会在前面
self.selection_box.append(border)
# 最后创建8个resize手柄白色圆形蓝色边框
handle_positions = [
'top-left', 'top', 'top-right', 'right',
'bottom-right', 'bottom', 'bottom-left', 'left'
]
for i, pos in enumerate(handle_positions):
# 创建容器对象用于接收鼠标事件
handle_container = LUIObject(parent=self.overlay_root)
handle_container.width = 10
handle_container.height = 10
handle_container.solid = True
handle_container.visible = False
# 设置手柄层级在选择框之上但在LUI内部
if hasattr(handle_container, 'z_offset'):
handle_container.z_offset = 50 # 在选择框之上
elif hasattr(handle_container, 'set_sort'):
handle_container.set_sort(50) # 在选择框之上
# 创建蓝色边框(外圈)
handle_border = LUISprite(handle_container, "blank", "skin")
handle_border.width = 10
handle_border.height = 10
handle_border.left = 0
handle_border.top = 0
handle_border.color = (0.2, 0.5, 1.0, 1.0) # 蓝色边框
# 创建白色圆形背景(内圈)
handle_bg = LUISprite(handle_container, "blank", "skin")
handle_bg.width = 8
handle_bg.height = 8
handle_bg.left = 1
handle_bg.top = 1
handle_bg.color = (1.0, 1.0, 1.0, 1.0) # 白色
# 存储手柄位置信息
handle_container._resize_position = pos
handle_container._resize_index = i
handle_container._bg = handle_bg
handle_container._border = handle_border
# 绑定鼠标事件
handle_container.bind("mousedown", lambda event, h=handle_container: self._on_handle_mousedown(event, h))
self.resize_handles.append(handle_container)
print(f"✓ 创建了{len(self.selection_box)}条边框和{len(self.resize_handles)}个手柄")
# 强制将手柄移到最前面在LUI中后创建的元素通常在前面
for handle in self.resize_handles:
# 尝试重新设置父节点来强制更新渲染顺序
if hasattr(handle, 'reparent_to'):
handle.reparent_to(self.overlay_root)
for border in self.selection_box:
if hasattr(border, 'reparent_to'):
border.reparent_to(self.overlay_root)
def _delayed_create_resize_handles(self, task):
"""延迟创建resize handles确保在Canvas之后创建"""
self._create_resize_handles()
return task.done
def _on_handle_mousedown(self, event, handle):
"""手柄鼠标按下事件"""
# 标记交互帧防止Canvas点击事件触发取消选中
if hasattr(p3d, 'ClockObject'):
self._last_interaction_frame = p3d.ClockObject.getGlobalClock().getFrameCount()
else:
# Fallback if specific import not avail in this scope (though p3d is imported)
self._last_interaction_frame = self.world.taskMgr.globalClock.getFrameCount()
if self.selected_index < 0:
return
comp_data = self.components[self.selected_index]
print(f"✓ 开始resize: 手柄位置={handle._resize_position}")
# 记录开始调整大小的状态
self.resizing_handle = handle
# 获取鼠标位置
if hasattr(self.world, 'mouseWatcherNode') and self.world.mouseWatcherNode.hasMouse():
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
self.resize_start_pos = (pixel_x, pixel_y)
# 记录组件的初始边界
self.resize_start_bounds = {
'left': comp_data.get('left', 0),
'top': comp_data.get('top', 0),
'width': comp_data.get('width', 100),
'height': comp_data.get('height', 30),
}
def _update_resize_handles(self, task):
# Restore selection if it was cleared immediately
if self.selected_index < 0 and getattr(self, '_force_keep_selection_frames', 0) > 0:
if getattr(self, '_last_selected_index', -1) >= 0:
self.selected_index = self._last_selected_index
if getattr(self, '_force_keep_selection_frames', 0) > 0:
self._force_keep_selection_frames -= 1
"""更新resize handles的位置和可见性"""
# 检查当前Canvas是否可见
if self.current_canvas_index >= 0 and self.current_canvas_index < len(self.canvases):
canvas = self.canvases[self.current_canvas_index]
if not canvas.get('visible', True):
self._hide_resize_handles()
return task.cont
if self.selected_index < 0 or self.selected_index >= len(self.components):
# 隐藏所有handles
self._hide_resize_handles()
return task.cont
comp_data = self.components[self.selected_index]
comp_type = comp_data['type']
# 只对Frame、Button、Slider、InputField、Plane、Image显示resize handles
if comp_type not in ['Frame', 'Button', 'Slider', 'InputField', 'Plane', 'Image', 'Checkbox', 'Text', 'Label', 'Video', 'Progressbar', 'Selectbox', 'ScrollableRegion', 'TabbedFrame', 'VerticalLayout', 'HorizontalLayout']:
self._hide_resize_handles()
return task.cont
# 显示并更新handles位置
self._show_and_update_handles(comp_data)
# 处理resize拖拽
if self.resizing_handle is not None:
self._handle_resize_drag()
return task.cont
def _hide_resize_handles(self):
"""隐藏所有resize handles和selection box"""
for border in self.selection_box:
border.visible = False
for handle in self.resize_handles:
handle.visible = False
def _ensure_component_outline(self, comp_index):
'Create a simple 4-edge outline for a component if not exists.'
if comp_index in self.debug_outlines:
return self.debug_outlines[comp_index]
from Builtin.LUISprite import LUISprite
borders = []
for _ in range(4):
border = LUISprite(self.overlay_root, "blank", "skin")
border.color = (0.2, 0.7, 1.0, 1.0)
border.visible = False
# Ensure outline is above components
if hasattr(border, 'z_offset'):
border.z_offset = 25
elif hasattr(border, 'set_sort'):
border.set_sort(25)
borders.append(border)
self.debug_outlines[comp_index] = borders
return borders
def _update_component_outlines(self, task):
'Show component outlines in edit mode, hide in play mode.'
if getattr(self, 'play_mode', False) or not self.lui_enabled or not self.show_editor:
for borders in self.debug_outlines.values():
for b in borders:
b.visible = False
return task.cont
for idx, comp_data in enumerate(self.components):
# If selected, let the resize handles/selection box handle the outline (Bright Blue)
# We hide the "debug" outline for the selected item to avoid double-drawing or z-fighting
if idx == self.selected_index:
borders = self.debug_outlines.get(idx)
if borders:
for b in borders:
b.visible = False
continue
comp_obj = comp_data.get('object')
if comp_obj is None:
continue
canvas_index = comp_data.get('canvas_index', self.current_canvas_index)
if canvas_index is None or canvas_index < 0 or canvas_index >= len(self.canvases):
# Component not assigned to a valid canvas
borders = self.debug_outlines.get(idx)
if borders:
for b in borders: b.visible = False
continue
canvas = self.canvases[canvas_index]
if not canvas.get('visible', True):
# Hide outlines for components on hidden canvases
borders = self.debug_outlines.get(idx)
if borders:
for b in borders:
b.visible = False
continue
# Determine size
width = comp_data.get('width', 0)
height = comp_data.get('height', 0)
try:
if hasattr(comp_obj, 'width') and comp_obj.width is not None and comp_obj.width > 0:
width = comp_obj.width
if hasattr(comp_obj, 'height') and comp_obj.height is not None and comp_obj.height > 0:
height = comp_obj.height
except Exception:
pass
if width <= 0 or height <= 0:
continue
# Determine Position (Absolute)
canvas_abs_left = 0
canvas_abs_top = 0
canvas_panel = self.canvases[canvas_index]['panel']
canvas_abs_left = getattr(canvas_panel, 'left', 0)
canvas_abs_top = getattr(canvas_panel, 'top', 0)
if canvas_abs_left == 0 and canvas_abs_top == 0:
try:
canvas_pos = canvas_panel.get_pos()
canvas_abs_left = canvas_pos.x
canvas_abs_top = canvas_pos.y
except Exception:
canvas_abs_left = 300
canvas_abs_top = 100
if hasattr(comp_obj, 'get_abs_pos'):
try:
abs_pos = comp_obj.get_abs_pos()
abs_left = abs_pos.x
abs_top = abs_pos.y
except Exception:
acc_left, acc_top = self._get_component_accumulated_pos(idx)
abs_left = canvas_abs_left + acc_left
abs_top = canvas_abs_top + acc_top
else:
acc_left, acc_top = self._get_component_accumulated_pos(idx)
abs_left = canvas_abs_left + acc_left
abs_top = canvas_abs_top + acc_top
# Draw Outline
borders = self._ensure_component_outline(idx)
# Style for unselected components: Gray, thinner or same thickness
for b in borders:
b.color = (0.7, 0.7, 0.7, 0.5) # Light gray, semi-transparent
if hasattr(b, 'z_offset'):
b.z_offset = 25
elif hasattr(b, 'set_sort'):
b.set_sort(25)
b.visible = True
t = 1.0 # Thickness
borders[0].left = abs_left
borders[0].top = abs_top - t
borders[0].width = width
borders[0].height = t
borders[1].left = abs_left
borders[1].top = abs_top + height
borders[1].width = width
borders[1].height = t
borders[2].left = abs_left - t
borders[2].top = abs_top
borders[2].width = t
borders[2].height = height
borders[3].left = abs_left + width
borders[3].top = abs_top
borders[3].width = t
borders[3].height = height
return task.cont
def _show_and_update_handles(self, comp_data):
"""显示并更新handles位置 - 修复坐标系不一致问题"""
# 每次显示时重新创建手柄,确保它们在最前面
if not hasattr(self, '_handles_recreated'):
self._recreate_handles_on_top()
self._handles_recreated = True
# 获取组件的Canvas相对位置和尺寸
comp_obj = comp_data['object']
# 获取Canvas的绝对位置用于坐标转换
canvas_abs_left = 0
canvas_abs_top = 0
if self.current_canvas_index >= 0 and self.current_canvas_index < len(self.canvases):
canvas_panel = self.canvases[self.current_canvas_index]['panel']
# 使用LUI对象的left和top属性而不是get_pos()方法
canvas_abs_left = getattr(canvas_panel, 'left', 0)
canvas_abs_top = getattr(canvas_panel, 'top', 0)
# 如果left/top属性不存在尝试使用get_pos()
if canvas_abs_left == 0 and canvas_abs_top == 0:
try:
canvas_pos = canvas_panel.get_pos()
canvas_abs_left = canvas_pos.x
canvas_abs_top = canvas_pos.y
except:
# 如果get_pos()也失败,使用默认值
canvas_abs_left = 300 # 默认Canvas位置
canvas_abs_top = 100
# 尝试直接获取组件的绝对屏幕坐标
if hasattr(comp_obj, 'get_abs_pos'):
try:
abs_pos = comp_obj.get_abs_pos()
abs_left = abs_pos.x
abs_top = abs_pos.y
# print(f"使用 get_abs_pos(): {abs_left}, {abs_top}")
except Exception as e:
print(f"get_abs_pos() 失败: {e}")
# Fallback to manual calculation
acc_left, acc_top = self._get_component_accumulated_pos(self.selected_index)
abs_left = canvas_abs_left + acc_left
abs_top = canvas_abs_top + acc_top
else:
# 递归计算组件的 Canvas 相对位置(累加所有父组件的相对位置)
# 使用 self.selected_index 似乎最安全,或者反查 comp_data
if self.selected_index >= 0 and self.components[self.selected_index] == comp_data:
acc_left, acc_top = self._get_component_accumulated_pos(self.selected_index)
else:
acc_left = comp_data.get('left', 0)
acc_top = comp_data.get('top', 0)
# 转换为绝对屏幕坐标(用于手柄和边框显示)
abs_left = canvas_abs_left + acc_left
abs_top = canvas_abs_top + acc_top
# 获取组件尺寸 - 优先从LUI对象获取实际尺寸
comp_type = comp_data['type']
width = comp_data.get('width', 100)
height = comp_data.get('height', 30)
# 尝试从LUI对象获取实际尺寸
try:
if hasattr(comp_obj, 'width') and comp_obj.width is not None:
actual_width = comp_obj.width
if actual_width > 0:
width = actual_width
if hasattr(comp_obj, 'height') and comp_obj.height is not None:
actual_height = comp_obj.height
if actual_height > 0:
height = actual_height
except:
pass
# 确保宽高是数值类型
try:
width = float(width)
height = float(height)
except (TypeError, ValueError):
width = 100.0
height = 30.0
# 更新comp_data中的尺寸
comp_data['width'] = width
comp_data['height'] = height
# 更新4条边框使用绝对位置因为边框在overlay_root下
# 确保 selection_box 存在
if not self.selection_box:
self._create_resize_handles()
if not self.selection_box: # Still empty?
return
# 上边框
self.selection_box[0].left = abs_left
self.selection_box[0].top = abs_top - 2
self.selection_box[0].width = width
self.selection_box[0].height = 2
self.selection_box[0].visible = True
# 下边框
self.selection_box[1].left = abs_left
self.selection_box[1].top = abs_top + height
self.selection_box[1].width = width
self.selection_box[1].height = 2
self.selection_box[1].visible = True
# 左边框
self.selection_box[2].left = abs_left - 2
self.selection_box[2].top = abs_top
self.selection_box[2].width = 2
self.selection_box[2].height = height
self.selection_box[2].visible = True
# 右边框
self.selection_box[3].left = abs_left + width
self.selection_box[3].top = abs_top
self.selection_box[3].width = 2
self.selection_box[3].height = height
self.selection_box[3].visible = True
# 更新8个手柄位置使用绝对位置因为手柄在overlay_root下
handle_positions = [
(abs_left - 5, abs_top - 5), # 左上
(abs_left + width/2 - 5, abs_top - 5), # 上
(abs_left + width - 5, abs_top - 5), # 右上
(abs_left + width - 5, abs_top + height/2 - 5), # 右
(abs_left + width - 5, abs_top + height - 5), # 右下
(abs_left + width/2 - 5, abs_top + height - 5), # 下
(abs_left - 5, abs_top + height - 5), # 左下
(abs_left - 5, abs_top + height/2 - 5), # 左
]
for i, (h_left, h_top) in enumerate(handle_positions):
if i < len(self.resize_handles):
handle = self.resize_handles[i]
handle.left = h_left
handle.top = h_top
handle.visible = True
def _recreate_handles_on_top(self):
"""重新创建手柄,确保它们在最前面"""
from Builtin.LUISprite import LUISprite
from Builtin.LUIObject import LUIObject
# 清除旧的手柄
for handle in self.resize_handles:
if hasattr(handle, 'remove'):
handle.remove()
for border in self.selection_box:
if hasattr(border, 'remove'):
border.remove()
self.resize_handles.clear()
self.selection_box.clear()
# 重新创建边框
for i in range(4):
border = LUISprite(self.overlay_root, "blank", "skin")
border.color = (0.2, 0.5, 1.0, 1.0) # 蓝色
border.visible = False
# 设置选择框层级在组件之上但在LUI内部
if hasattr(border, 'z_offset'):
border.z_offset = 40
elif hasattr(border, 'set_sort'):
border.set_sort(40)
self.selection_box.append(border)
# 重新创建手柄
handle_positions = [
'top-left', 'top', 'top-right', 'right',
'bottom-right', 'bottom', 'bottom-left', 'left'
]
for i, pos in enumerate(handle_positions):
handle_container = LUIObject(parent=self.overlay_root)
handle_container.width = 10
handle_container.height = 10
handle_container.solid = True
handle_container.visible = False
# 设置手柄层级在选择框之上但在LUI内部
if hasattr(handle_container, 'z_offset'):
handle_container.z_offset = 50
elif hasattr(handle_container, 'set_sort'):
handle_container.set_sort(50)
# 创建蓝色边框(外圈)
handle_border = LUISprite(handle_container, "blank", "skin")
handle_border.width = 10
handle_border.height = 10
handle_border.left = 0
handle_border.top = 0
handle_border.color = (0.2, 0.5, 1.0, 1.0) # 蓝色边框
# 创建白色圆形背景(内圈)
handle_bg = LUISprite(handle_container, "blank", "skin")
handle_bg.width = 8
handle_bg.height = 8
handle_bg.left = 1
handle_bg.top = 1
handle_bg.color = (1.0, 1.0, 1.0, 1.0) # 白色
# 存储手柄位置信息
handle_container._resize_position = pos
handle_container._resize_index = i
handle_container._bg = handle_bg
handle_container._border = handle_border
# 绑定鼠标事件
handle_container.bind("mousedown", lambda event, h=handle_container: self._on_handle_mousedown(event, h))
self.resize_handles.append(handle_container)
print("✓ 重新创建了手柄,确保在最前面")
def _handle_resize_drag(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
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相对坐标
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
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
new_width = start_width - delta_x
new_height = start_height - delta_y
elif handle_pos == 'top':
new_top = start_top + delta_y
new_height = start_height - delta_y
elif handle_pos == 'top-right':
new_top = start_top + delta_y
new_width = start_width + delta_x
new_height = start_height - delta_y
elif handle_pos == 'right':
new_width = start_width + delta_x
elif handle_pos == 'bottom-right':
new_width = start_width + delta_x
new_height = start_height + delta_y
elif handle_pos == 'bottom':
new_height = start_height + delta_y
elif handle_pos == 'bottom-left':
new_left = start_left + delta_x
new_width = start_width - delta_x
new_height = start_height + delta_y
elif handle_pos == 'left':
new_left = start_left + delta_x
new_width = start_width - delta_x
# 应用最小尺寸限制
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
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
# 获取Canvas边界约束
canvas_width = 800 # 默认Canvas宽度
canvas_height = 600 # 默认Canvas高度
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
comp_data = self.components[self.selected_index]
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)
# Update component data
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)
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
try:
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']:
sprite = comp_data.get('sprite')
if sprite is not None:
sprite.width = new_width
sprite.height = new_height
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)
if comp_type in ['VerticalLayout', 'HorizontalLayout']:
self._update_layout_inner(self.selected_index)
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 _add_to_scene_tree(self, comp_data):
"""Add a virtual node to represent the LUI component in the scene tree"""
from panda3d.core import NodePath
node_name = f"UI_{comp_data['type']}_{len(self.components)}"
ui_node = NodePath(node_name)
# Reparent to current canvas node if available
if self.current_canvas_index >= 0 and self.current_canvas_index < len(self.canvases):
canvas_node = self.canvases[self.current_canvas_index]['node']
ui_node.reparent_to(canvas_node)
comp_data['canvas_index'] = self.current_canvas_index
else:
ui_node.reparent_to(self.world.render)
comp_data['canvas_index'] = None
comp_data['ui_node'] = ui_node
comp_data['parent_index'] = None
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)):
print("Error: invalid component index")
return
child_data = self.components[child_index]
parent_data = self.components[parent_index]
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
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)
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)
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})")
# 在数据结构中存储局部坐标(相对于父组件)
child_data['left'] = new_local_left
child_data['top'] = new_local_top
child_obj = child_data['object']
parent_obj = parent_data['object']
parent_type = parent_data.get('type')
visual_container_types = [
'Frame', 'Plane', 'Video',
'ScrollableRegion', 'TabbedFrame',
'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()
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
# 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
# 关键修改由于我们不实际重新父化对象所有组件都保持相对于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})")
# 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
# 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']:
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}")
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'):
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
if hasattr(child_obj, 'show'):
child_obj.show()
except Exception as e:
print(f"Reparenting error: {e}")
# Ensure canvas_index follows parent canvas (visibility)
parent_canvas = parent_data.get('canvas_index')
if parent_canvas is not None:
self._update_canvas_index_recursive(child_index, parent_canvas)
if parent_data.get('type') in ['HorizontalLayout', 'VerticalLayout'] and parent_data.get('layout_wrap', True):
self._apply_wrap_layout(parent_index)
print(f"Set parent: child #{child_index} -> parent #{parent_index}")
def _set_parent_root(self, child_index, keep_world=False):
"""Detach a component from its parent and move it to root (canvas)."""
if child_index < 0 or child_index >= len(self.components):
print("Error: invalid component index")
return
child_data = self.components[child_index]
# 保存当前的累积位置相对于Canvas的绝对位置
current_abs_left, current_abs_top = self._get_component_accumulated_pos(child_index)
print(f"[_set_parent_root] Component #{child_index} current accumulated pos: ({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)
# 更新数据结构
child_data['parent_index'] = None
child_data['visual_parent_canvas'] = False
child_data['draggable'] = True
# 获取Canvas panel
parent_obj = None
canvas_index = self.current_canvas_index
if canvas_index is not None and canvas_index >= 0 and canvas_index < len(self.canvases):
parent_obj = self.canvases[canvas_index]['panel']
child_data['canvas_index'] = canvas_index
else:
parent_obj = getattr(self, 'overlay_root', None) or getattr(self.world, 'render', None)
child_obj = child_data.get('object')
# 关键修改:对于 Button 等复杂组件,不要重新父化!
# 只更新位置和数据结构
if child_obj is not None:
# 更新位置数据
if keep_world:
child_data['left'] = current_abs_left
child_data['top'] = current_abs_top
# 直接设置对象位置,不改变父对象
if hasattr(child_obj, 'set_pos'):
child_obj.set_pos(current_abs_left, current_abs_top)
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_root] Set position to: ({current_abs_left:.1f}, {current_abs_top:.1f}) without reparenting")
else:
# 不保持世界位置:使用原有的局部坐标
if hasattr(child_obj, 'left'):
child_obj.left = child_data.get('left', 0)
if hasattr(child_obj, 'top'):
child_obj.top = child_data.get('top', 0)
# 确保可见性
if child_obj is not None:
try:
if hasattr(child_obj, 'show'):
child_obj.show()
elif hasattr(child_obj, 'visible'):
child_obj.visible = True
except Exception:
pass
# 对于 Button确保内部组件可见
comp_type = child_data.get('type')
if comp_type == 'Button' and child_obj is not None:
try:
layout = getattr(child_obj, '_layout', None)
lbl = getattr(child_obj, '_label', None)
if layout is not None:
if hasattr(layout, 'show'):
layout.show()
if hasattr(layout, 'visible'):
layout.visible = True
if lbl is not None:
if hasattr(lbl, 'show'):
lbl.show()
if hasattr(lbl, 'visible'):
lbl.visible = True
# 重新设置文本
current_text = child_data.get('text', 'Button')
if hasattr(lbl, 'set_text'):
lbl.set_text(current_text)
elif hasattr(lbl, 'text'):
lbl.text = current_text
print(f"[_set_parent_root] Button components visibility ensured")
except Exception as e:
print(f"[_set_parent_root] Error ensuring button visibility: {e}")
# 对于 Plane, Image, Video - 确保内部 sprite 可见
elif comp_type in ['Plane', 'Image', 'Video'] and child_obj is not None:
try:
spr = child_data.get('sprite')
if spr is not None:
if hasattr(spr, 'show'):
spr.show()
if hasattr(spr, 'visible'):
spr.visible = True
# 确保 sprite 的颜色不是透明的
if comp_type == 'Plane':
color = child_data.get('color', (1, 1, 1, 1))
if hasattr(spr, 'color'):
spr.color = color
print(f"[_set_parent_root] {comp_type} sprite visibility ensured")
except Exception as e:
print(f"[_set_parent_root] Error ensuring {comp_type} visibility: {e}")
# 对于 Frame - 直接确保可见
elif comp_type == 'Frame' and child_obj is not None:
try:
if hasattr(child_obj, 'show'):
child_obj.show()
if hasattr(child_obj, 'visible'):
child_obj.visible = True
print(f"[_set_parent_root] Frame visibility ensured")
except Exception as e:
print(f"[_set_parent_root] Error ensuring Frame visibility: {e}")
# Update canvas index recursively
self._update_canvas_index_recursive(child_index, child_data.get('canvas_index'))
# 调试输出:确认组件状态
print(f"[_set_parent_root] Component #{child_index} ({child_data.get('name', 'Unknown')}) moved to root (data only)")
print(f" - Position: ({child_data.get('left', 0):.1f}, {child_data.get('top', 0):.1f})")
print(f" - Size: ({child_data.get('width', 0):.1f}, {child_data.get('height', 0):.1f})")
print(f" - Canvas index: {child_data.get('canvas_index')}")
print(f" - Visible: {getattr(child_obj, 'visible', 'N/A') if child_obj else 'N/A'}")
print(f" - Parent (unchanged): {getattr(child_obj, 'parent', 'N/A') if child_obj else 'N/A'}")
def create_child_component(self, parent_index, comp_type, anchor_position=None):
# Create child component under parent
print("\n=== Create child component ===")
print(f"Parent index: {parent_index}, Type: {comp_type}")
if parent_index < 0 or parent_index >= len(self.components):
print("Error: invalid parent index")
return None
parent_data = self.components[parent_index]
parent_obj = parent_data['object']
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()
parent_width = parent_data.get('width', 100)
parent_height = parent_data.get('height', 30)
comp_sizes = {
'Button': (100, 30),
'Text': (80, 20),
'Label': (80, 20),
'CheckBox': (120, 20),
'InputField': (200, 24),
'Slider': (200, 16),
'Frame': (300, 200),
'Plane': (100, 100),
'Image': (100, 100),
'Video': (320, 240),
'Progressbar': (200, 30),
'Selectbox': (200, 30),
'ScrollableRegion': (200, 200),
'TabbedFrame': (300, 200),
'VerticalLayout': (300, 200),
'HorizontalLayout': (300, 200),
}
child_w, child_h = comp_sizes.get(comp_type, (80, 20))
child_x = (parent_width - child_w) / 2
child_y = (parent_height - child_h) / 2
if parent_data.get('type') in ['VerticalLayout', 'HorizontalLayout']:
child_x = 0
child_y = 0
child_obj = None
try:
if comp_type == 'Button':
child_obj = self.luiFunction.create_button(self, text="ChildButton", x=child_x, y=child_y, parent=parent_obj)
elif comp_type in ['Text', 'Label']:
child_obj = self.luiFunction.create_label(self, text="ChildText", x=child_x, y=child_y, parent=parent_obj)
elif comp_type == 'CheckBox':
child_obj = self.luiFunction.create_checkbox(self, label="ChildCheckbox", x=child_x, y=child_y, parent=parent_obj)
elif comp_type == 'InputField':
child_obj = self.luiFunction.create_input_field(self, text="ChildInput", x=child_x, y=child_y, parent=parent_obj)
elif comp_type == 'Slider':
child_obj = self.luiFunction.create_slider(self, x=child_x, y=child_y, parent=parent_obj)
elif comp_type == 'Frame':
child_obj = self.luiFunction.create_frame(self, x=child_x, y=child_y, parent=parent_obj)
elif comp_type == 'Plane':
child_obj = self.luiFunction.create_plane(self, x=child_x, y=child_y, parent=parent_obj)
elif comp_type == 'Image':
child_obj = self.luiFunction.create_image(self, x=child_x, y=child_y, parent=parent_obj)
elif comp_type == 'Video':
child_obj = self.luiFunction.create_video(self, x=child_x, y=child_y, parent=parent_obj)
elif comp_type == 'Progressbar':
child_obj = self.luiFunction.create_progressbar(self, x=child_x, y=child_y, parent=parent_obj)
elif comp_type == 'Selectbox':
child_obj = self.luiFunction.create_selectbox(self, x=child_x, y=child_y, parent=parent_obj)
elif comp_type == 'ScrollableRegion':
child_obj = self.luiFunction.create_scrollable_region(self, x=child_x, y=child_y, parent=parent_obj)
elif comp_type == 'TabbedFrame':
child_obj = self.luiFunction.create_tabbed_frame(self, x=child_x, y=child_y, parent=parent_obj)
elif comp_type == 'VerticalLayout':
child_obj = self.luiFunction.create_vertical_layout(self, x=child_x, y=child_y, parent=parent_obj)
elif comp_type == 'HorizontalLayout':
child_obj = self.luiFunction.create_horizontal_layout(self, x=child_x, y=child_y, parent=parent_obj)
else:
print(f"Error: unsupported component type {comp_type}")
return None
except Exception as e:
print(f"Error: create component failed: {e}")
import traceback
traceback.print_exc()
return None
if child_obj:
child_index = -1
for i, comp in enumerate(self.components):
if comp.get('object') == child_obj:
child_index = i
break
if child_index >= 0:
child_data = self.components[child_index]
if parent_data.get('type') in ['HorizontalLayout', 'VerticalLayout'] and parent_data.get('layout_wrap', True):
self._apply_wrap_layout(parent_index)
if parent_data.get('type') in ['VerticalLayout', 'HorizontalLayout']:
child_data['draggable'] = False
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)
print(f"Linked child #{child_index} -> parent #{parent_index}")
return child_index
else:
print("Child creation failed")
return None
def _get_descendants(self, idx):
"""Return set of all descendant indices for a component."""
desc = set()
if idx < 0 or idx >= len(self.components):
return desc
comp = self.components[idx]
for c_idx in comp.get('children_indices', []):
if c_idx in desc:
continue
desc.add(c_idx)
desc.update(self._get_descendants(c_idx))
return desc
def _update_canvas_index_recursive(self, idx, canvas_index):
"""Update canvas_index for component and its descendants."""
if idx < 0 or idx >= len(self.components):
return
comp = self.components[idx]
comp['canvas_index'] = canvas_index
for c_idx in comp.get('children_indices', []):
self._update_canvas_index_recursive(c_idx, canvas_index)
def _reorder_list(self, items, src, target):
"""Move src before target within items list."""
if src == target:
return items
if src not in items or target not in items:
return items
items = list(items)
items.remove(src)
insert_at = items.index(target)
items.insert(insert_at, src)
return items
def _sync_canvas_children(self, parent_index):
"""Update positions for children that keep visual parent on canvas."""
if parent_index < 0 or parent_index >= len(self.components):
return
# Get parent's absolute position
parent_abs_left, parent_abs_top = self._get_component_accumulated_pos(parent_index)
parent_data = self.components[parent_index]
parent_obj = parent_data.get('object')
for child_idx in parent_data.get('children_indices', []):
if child_idx < 0 or child_idx >= len(self.components):
continue
child = self.components[child_idx]
child_obj = child.get('object')
if child_obj is None:
continue
# Check actual parenting
child_parent = getattr(child_obj, 'parent', None)
is_scene_parented = (child_parent is not None and child_parent == parent_obj)
if not is_scene_parented:
# If child is NOT physically parented to the parent object (e.g. it is on Canvas),
# we MUST manually update its absolute position to follow the parent.
abs_left = parent_abs_left + float(child.get('left', 0))
abs_top = parent_abs_top + float(child.get('top', 0))
if hasattr(child_obj, 'left'):
child_obj.left = abs_left
if hasattr(child_obj, 'top'):
child_obj.top = abs_top
if hasattr(child_obj, 'set_pos'):
child_obj.set_pos(abs_left, abs_top)
# Recurse for grandchildren
self._sync_canvas_children(child_idx)
def draw_editor(self):
"""Draw the LUI Editor Window"""
if not self.lui_enabled or not self.show_editor:
return
imgui.set_next_window_size((350, 600), imgui.Cond_.first_use_ever)
with imgui_ctx.begin("LUI编辑器", True) as (opened, show):
self.show_editor = opened
if not show:
return
# Play/Stop
if imgui.button("Play" if not self.play_mode else "Stop", (120, 25)):
self.play_mode = not self.play_mode
if self.play_mode:
# Clear editor-only state when entering play mode
self.selected_index = -1
self.dragging_comp = None
self.pending_drag_comp = None
self.pending_drag_index = -1
self.pending_drag_start_mouse = None
self.pending_drag_start_abs = None
self.pending_drag_start_parent = None
self._hide_resize_handles()
# Keep LUI input enabled for runtime UI interaction
self.set_input_enabled(True)
imgui.same_line()
imgui.text_colored((0.2, 1.0, 0.2, 1.0) if self.play_mode else (1.0, 0.8, 0.2, 1.0),
"Running" if self.play_mode else "Editing")
imgui.separator()
# In play mode, show toolbar only
if self.play_mode:
return
# Canvas Management
imgui.text("Canvas 管理")
imgui.separator()
if imgui.button("创建新Canvas", (150, 25)):
self.luiFunction.create_canvas(self)
imgui.same_line()
# 显示当前Canvas信息
if self.current_canvas_index >= 0 and self.current_canvas_index < len(self.canvases):
current_canvas_name = self.canvases[self.current_canvas_index]['name']
imgui.text_colored((0.0, 1.0, 0.0, 1.0), f"当前: {current_canvas_name}")
else:
imgui.text_colored((1.0, 0.0, 0.0, 1.0), "无Canvas")
if len(self.canvases) > 0:
canvas_names = [c['name'] for c in self.canvases]
if self.current_canvas_index >= len(canvas_names):
self.current_canvas_index = len(canvas_names) - 1
changed, new_index = imgui.combo("选择Canvas", self.current_canvas_index, canvas_names)
if changed:
self.switch_canvas(new_index)
# Canvas Properties
if self.current_canvas_index >= 0:
canvas = self.canvases[self.current_canvas_index]
panel = canvas['panel']
imgui.indent()
# Visibility
is_visible = canvas.get('visible', True)
changed, visible = imgui.checkbox("显示Canvas", is_visible)
if changed:
canvas['visible'] = visible
if visible:
panel.show()
else:
panel.hide()
# Fill Mode & Margins
is_fill_mode = canvas.get('fill_mode', False)
imgui.separator()
changed, fill_mode = imgui.checkbox("场景填充模式", is_fill_mode)
if changed:
canvas['fill_mode'] = fill_mode
if fill_mode:
# 刚切换到填充模式时,更新一次
self._update_canvas_geometry(canvas)
if fill_mode:
imgui.text("场景边距设置 (调整以匹配视口)")
margins = canvas.get('margins', {'left':0,'right':0,'top':0,'bottom':0})
geometry_changed = False
changed, margins['left'] = imgui.drag_float("左边距", margins['left'], 1.0, 0, 1000)
if changed: geometry_changed = True
changed, margins['right'] = imgui.drag_float("右边距", margins['right'], 1.0, 0, 1000)
if changed: geometry_changed = True
changed, margins['top'] = imgui.drag_float("顶边距", margins['top'], 1.0, 0, 500)
if changed: geometry_changed = True
changed, margins['bottom'] = imgui.drag_float("底边距", margins['bottom'], 1.0, 0, 500)
if changed: geometry_changed = True
if geometry_changed:
canvas['margins'] = margins
self._update_canvas_geometry(canvas)
else:
# Manual Position
curr_pos_val = panel.get_pos()
curr_pos = (curr_pos_val.x, curr_pos_val.y)
# 固定开关
is_fixed = canvas.get('fixed', False)
changed, fixed = imgui.checkbox("锁定位置 (禁止拖动)", is_fixed)
if changed:
canvas['fixed'] = fixed
if not is_fixed:
changed, pos = imgui.drag_float2("Canvas位置", curr_pos)
if changed:
panel.set_pos(pos[0], pos[1])
# Manual Size
curr_size = (panel.get_width(), panel.get_height())
changed, size = imgui.drag_float2("Canvas尺寸", curr_size, 1, 100, 4000)
if changed:
panel.width = size[0]
panel.height = size[1]
# 同时更新背景尺寸
if 'background' in canvas:
bg = canvas['background']
bg.width = size[0]
bg.height = size[1]
# Canvas Color
if 'background' in canvas:
imgui.separator()
bg = canvas['background']
# 获取当前颜色 (RGBA)
current_color = bg.color
# 转换为可编辑的格式 (list)
edit_color = [current_color[0], current_color[1], current_color[2], current_color[3]]
changed, new_color = imgui.color_edit4("背景颜色/透明度", edit_color)
if changed:
bg.color = (new_color[0], new_color[1], new_color[2], new_color[3])
# Title settings
if 'title_label' in canvas:
imgui.separator()
imgui.text("标题设置")
title = canvas['title_label']
# Visible
is_title_visible = title.visible
changed, title_visible = imgui.checkbox("显示标题", is_title_visible)
if changed:
try:
if title_visible:
title.show()
else:
title.hide()
except:
title.visible = title_visible
# Text content
curr_text = title.text
changed, new_text = imgui.input_text("标题文本", curr_text, 128)
if changed:
title.text = new_text
# Position
curr_pos_val = title.get_pos()
curr_pos = (curr_pos_val.x, curr_pos_val.y)
changed, new_pos = imgui.drag_float2("标题位置", curr_pos)
if changed:
title.set_pos(new_pos[0], new_pos[1])
imgui.unindent()
else:
imgui.text_colored((1, 1, 0, 1), "请先创建一个Canvas")
imgui.spacing()
imgui.text("创建组件")
imgui.separator()
if len(self.canvases) == 0:
imgui.begin_disabled()
# Component Type Selection
component_types = ["按钮 (Button)", "文本 (Text)", "复选框 (CheckBox)", "输入框 (InputField)",
"滑块 (Slider)", "框架 (Frame)","面板 (Plane)","图片 (Image)","视频 (Video)",
"进度条 (Progressbar)", "下拉框 (Selectbox)", "滚动区域 (ScrollableRegion)", "标签页 (TabbedFrame)",
"垂直布局组 (VerticalLayout)", "水平布局组 (HorizontalLayout)"]
changed, self._selected_type_index = imgui.combo("组件类型", self._selected_type_index, component_types)
if imgui.button("创建组件", (120, 25)):
self._create_component_by_type(self._selected_type_index)
if len(self.canvases) == 0:
imgui.end_disabled()
imgui.spacing()
imgui.text(f"已创建组件 ({len(self.components)})")
imgui.separator()
# Component Tree Helper
# self.draw_component_tree()
imgui.spacing()
imgui.separator()
# Anchor Popup Handling
self._handle_anchor_popup()
def draw_component_tree(self):
"""Draw the component tree (can be used in other windows)"""
# Track tree item rects for drop hit-testing (allows drop on earlier items)
self._tree_item_rects = {}
def _store_item_rect(item_key):
try:
rmin = imgui.get_item_rect_min()
rmax = imgui.get_item_rect_max()
self._tree_item_rects[item_key] = (rmin, rmax)
except Exception:
pass
def _hit_test_tree_item(mx, my):
for key, (rmin, rmax) in self._tree_item_rects.items():
try:
min_x = rmin.x
min_y = rmin.y
max_x = rmax.x
max_y = rmax.y
except Exception:
min_x = rmin[0]
min_y = rmin[1]
max_x = rmax[0]
max_y = rmax[1]
if min_x <= mx <= max_x and min_y <= my <= max_y:
return key
return None
# Root drop target
if imgui.selectable("ROOT", self.selected_index < 0):
# ????????
pass
_store_item_rect(-1)
def render_component_tree(idx):
if idx >= len(self.components): return True
comp = self.components[idx]
comp_type = comp.get('type', 'Unknown')
label = f"{comp.get('name', 'N/A')}"
# Determine flags
flags = imgui.TreeNodeFlags_.open_on_arrow | imgui.TreeNodeFlags_.open_on_double_click | imgui.TreeNodeFlags_.default_open
if self.selected_index == idx:
flags |= imgui.TreeNodeFlags_.selected
# Check for children
children = list(comp.get('children_indices', []))
if not children:
flags |= imgui.TreeNodeFlags_.leaf
else:
label += f" ({len(children)})"
# Draw tree node
is_open = imgui.tree_node_ex(f"##node_{idx}", flags, label)
_store_item_rect(idx)
# Custom drag/drop (avoid payload capsule issues)
if imgui.is_item_active() and imgui.is_mouse_dragging(0):
self._tree_drag_src = idx
imgui.begin_tooltip()
imgui.text(f"Move: {label}")
imgui.end_tooltip()
# Handle selection
if imgui.is_item_clicked():
self.selected_index = idx
# Clear 3D scene selection
if hasattr(self.world, 'selection') and self.world.selection:
self.world.selection.selectedNode = None
self.world.selection.clearSelectionBox()
self.world.selection.clearGizmo()
# Right-click menu
if imgui.begin_popup_context_item(f"##comp_ctx_{idx}"):
if imgui.menu_item("删除", "", False)[0]:
self.luiFunction.delete_component(self, idx)
imgui.end_popup()
if is_open: imgui.tree_pop()
return False
imgui.separator()
if imgui.begin_menu("创建子组件"):
child_types = ['Button', 'Text', 'CheckBox', 'InputField', 'Slider', 'Frame', 'Plane','Image', 'Video','Progressbar','Selectbox','ScrollableRegion','TabbedFrame', 'VerticalLayout', 'HorizontalLayout']
cn_names = ['按钮', '标签', '复选框', '输入框', '滑块', '框架', '面板', '图片', '视频','进度条','下拉框','滚动区域','标签页', '垂直布局组', '水平布局组']
for ct_idx, ct in enumerate(child_types):
if imgui.menu_item(f"{cn_names[ct_idx]}", "", False)[0]:
self.create_child_component(idx, ct)
imgui.end_menu()
imgui.end_popup()
# Draw children if open
if is_open:
for child_idx in children:
# After a deletion, the list might have shifted,
# so we check bounds again or just return False to stop this frame's recursion
if child_idx < len(self.components):
if not render_component_tree(child_idx):
# If a child (or sub-child) was deleted, indices shifted.
# Stopping current rendering frame's branch is safest.
imgui.tree_pop()
return False
imgui.tree_pop()
return True
# Find root components
roots = []
for i, comp in enumerate(self.components):
p_idx = comp.get('parent_index')
if p_idx is None or p_idx < 0:
roots.append(i)
# Maintain root order for sorting
if not self.root_order:
self.root_order = list(roots)
else:
self.root_order = [r for r in self.root_order if r in roots]
for r in roots:
if r not in self.root_order:
self.root_order.append(r)
# Render roots
for root_idx in self.root_order:
if not render_component_tree(root_idx):
break
# Handle tree drop after full render (allows drop on earlier items)
if self._tree_drag_src is not None and imgui.is_mouse_released(0):
src_idx = self._tree_drag_src
self._tree_drag_src = None
mx, my = imgui.get_mouse_pos()
target_idx = _hit_test_tree_item(mx, my)
if target_idx is None:
target_idx = -1
if 0 <= src_idx < len(self.components):
if target_idx == -1:
self._set_parent_root(src_idx, keep_world=True)
elif target_idx != src_idx:
if target_idx in self._get_descendants(src_idx):
print("Invalid drop: cannot parent to descendant")
else:
src_parent = self.components[src_idx].get('parent_index', -1)
tgt_parent = self.components[target_idx].get('parent_index', -1)
io = imgui.get_io()
want_reorder = bool(getattr(io, 'key_shift', False))
if want_reorder and src_parent == tgt_parent:
if src_parent is None or src_parent < 0:
if not self.root_order:
self.root_order = [i for i, c in enumerate(self.components) if c.get('parent_index') is None or c.get('parent_index') < 0]
self.root_order = self._reorder_list(self.root_order, src_idx, target_idx)
else:
parent_data = self.components[src_parent]
children = parent_data.get('children_indices', [])
parent_data['children_indices'] = self._reorder_list(children, src_idx, target_idx)
else:
self._set_parent_child_relationship(src_idx, target_idx, keep_world=True)
# Clear drag state if released outside any item
if self._tree_drag_src is not None and imgui.is_mouse_released(0):
self._tree_drag_src = None
imgui.spacing()
imgui.separator()
# Property Editing
if self.selected_index >= 0 and self.selected_index < len(self.components):
imgui.text("属性编辑")
imgui.separator()
self.luiFunction._draw_component_properties(self,self.selected_index)
else:
imgui.text_disabled("未选中任何组件")
# Anchor popup
if self.anchor_popup_open:
imgui.open_popup("选择锚点位置")
if imgui.begin_popup("选择锚点位置"):
imgui.text("选择锚点位置:")
# 中文显示名称
anchor_names = {
'top-left': '左上', 'top-center': '中上', 'top-right': '右上',
'middle-left': '左中', 'center': '中心', 'middle-right': '右中',
'bottom-left': '左下', 'bottom-center': '中下', 'bottom-right': '右下'
}
anchor_positions = [
['top-left', 'top-center', 'top-right'],
['middle-left', 'center', 'middle-right'],
['bottom-left', 'bottom-center', 'bottom-right']
]
for row in anchor_positions:
for i, pos in enumerate(row):
button_text = anchor_names.get(pos, pos)
if imgui.button(f"{button_text}##popup_{pos}", (50, 25)):
# 使用新的锚点更新方法
if hasattr(self, '_temp_selected_index_for_anchor'):
selected_idx = self._temp_selected_index_for_anchor
self._update_component_anchor_position(selected_idx, pos)
self.anchor_popup_open = False
if hasattr(self, '_temp_selected_index_for_anchor'):
delattr(self, '_temp_selected_index_for_anchor')
imgui.close_current_popup()
if i < len(row) - 1:
imgui.same_line()
imgui.spacing()
if imgui.button("取消"):
self.anchor_popup_open = False
if hasattr(self, '_temp_selected_index_for_anchor'):
delattr(self, '_temp_selected_index_for_anchor')
imgui.close_current_popup()
imgui.end_popup()
def _create_component_by_type(self, type_index):
"""Create LUI component by index - 默认生成在Canvas中心"""
# 1. 确定生成位置 (默认 Canvas 中心)
canvas_relative_x = 100
canvas_relative_y = 100
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()
# 根据组件类型预测尺寸,以便进行中心点对齐
# 注意:这些是 lui_function.py 中定义的默认值
comp_sizes = {
0: (100, 30), # Button
1: (80, 20), # Text
2: (120, 20), # Checkbox
3: (200, 24), # InputField
4: (200, 16), # Slider
5: (300, 200), # Frame
6: (100, 100), # Plane
7: (100, 100), # Image
8: (320, 240), # Video
9: (200, 30), # Progressbar
10: (200, 30), # Selectbox
11: (200, 200), # ScrollableRegion
12: (300, 200), # TabbedFrame
13: (300, 200), # VerticalLayout
14: (300, 200) # HorizontalLayout
}
w, h = comp_sizes.get(type_index, (100, 100))
# 计算中心位置
canvas_relative_x = (canvas_width - w) / 2
canvas_relative_y = (canvas_height - h) / 2
# 确保不超出合理范围 (防止 Canvas 太小时位置为负)
canvas_relative_x = max(10, canvas_relative_x)
canvas_relative_y = max(10, canvas_relative_y)
try:
if type_index == 0: # Button
self.luiFunction.create_button(self, text="New Button", x=canvas_relative_x, y=canvas_relative_y)
elif type_index == 1: # Text
self.luiFunction.create_label(self, text="New Text", x=canvas_relative_x, y=canvas_relative_y)
elif type_index == 2: # Checkbox
self.luiFunction.create_checkbox(self, label="Check box", x=canvas_relative_x, y=canvas_relative_y)
elif type_index == 3: # InputField
self.luiFunction.create_input_field(self, text="", x=canvas_relative_x, y=canvas_relative_y)
elif type_index == 4: # Slider
self.luiFunction.create_slider(self, x=canvas_relative_x, y=canvas_relative_y)
elif type_index == 5: # Frame
self.luiFunction.create_frame(self, x=canvas_relative_x, y=canvas_relative_y)
elif type_index == 6: # Plane
self.luiFunction.create_plane(self, x=canvas_relative_x, y=canvas_relative_y, color=(0.8, 0.8, 0.8, 1))
elif type_index == 7: # Image
self.luiFunction.create_image(self, x=canvas_relative_x, y=canvas_relative_y)
elif type_index == 8: # Video
self.luiFunction.create_video(self, x=canvas_relative_x, y=canvas_relative_y)
elif type_index == 9: # Progressbar
self.luiFunction.create_progressbar(self, x=canvas_relative_x, y=canvas_relative_y)
elif type_index == 10: # Selectbox
self.luiFunction.create_selectbox(self, x=canvas_relative_x, y=canvas_relative_y)
elif type_index == 11: # ScrollableRegion
self.luiFunction.create_scrollable_region(self, x=canvas_relative_x, y=canvas_relative_y)
elif type_index == 12: # TabbedFrame
self.luiFunction.create_tabbed_frame(self, x=canvas_relative_x, y=canvas_relative_y)
elif type_index == 13: # VerticalLayout
self.luiFunction.create_vertical_layout(self, x=canvas_relative_x, y=canvas_relative_y)
elif type_index == 14: # HorizontalLayout
self.luiFunction.create_horizontal_layout(self, x=canvas_relative_x, y=canvas_relative_y)
print(f"✓ 创建组件成功,已重定向至 Canvas 中心: ({canvas_relative_x:.1f}, {canvas_relative_y:.1f})")
if hasattr(self.world, 'updateSceneTree'):
self.world.updateSceneTree()
except Exception as e:
print(f"创建组件失败: {str(e)}")
def set_component_anchor(self, comp_index, anchor_position):
"""设置组件锚点 - 保持向后兼容"""
self._update_component_anchor_position(comp_index, anchor_position)
def _update_anchored_children(self, parent_index):
"""Recursively update anchored/fill children."""
if parent_index < 0 or parent_index >= len(self.components):
return
print(f"[_update_anchored_children] Updating children of #{parent_index}")
parent_data = self.components[parent_index]
children = parent_data.get('children_indices', [])
for child_idx in children:
if child_idx < len(self.components):
child_data = self.components[child_idx]
# Fill layout has priority over anchored positioning
if child_data.get('layout_mode') == 'fill':
print(f" -> Child #{child_idx} is FILL mode")
self._apply_fill_layout(child_idx)
elif child_data.get('anchored_to_parent'):
anchor_pos = child_data.get('anchor_position')
manual_offset = (
child_data.get('anchor_manual_offset_x', 0.0),
child_data.get('anchor_manual_offset_y', 0.0)
)
if anchor_pos:
self._update_component_anchor_position(child_idx, anchor_pos, manual_offset)
# Recurse into children
self._update_anchored_children(child_idx)
def _apply_fill_layout(self, comp_index):
"""Apply fill layout to component: match parent/canvas size with margins."""
if comp_index < 0 or comp_index >= len(self.components):
return
comp_data = self.components[comp_index]
comp_obj = comp_data.get('object')
comp_type = comp_data.get('type', '')
parent_width = None
parent_height = None
parent_index = comp_data.get('parent_index')
if parent_index is not None and parent_index >= 0 and parent_index < len(self.components):
parent_data = self.components[parent_index]
parent_width = parent_data.get('width')
parent_height = parent_data.get('height')
parent_obj = parent_data.get('object')
if parent_width is None and parent_obj is not None and hasattr(parent_obj, 'width'):
parent_width = parent_obj.width
if parent_height is None and parent_obj is not None and hasattr(parent_obj, 'height'):
parent_height = parent_obj.height
else:
canvas_index = comp_data.get('canvas_index', self.current_canvas_index)
if canvas_index is not None and canvas_index >= 0 and canvas_index < len(self.canvases):
canvas_panel = self.canvases[canvas_index].get('panel')
if canvas_panel is not None:
if hasattr(canvas_panel, 'width'):
parent_width = canvas_panel.width
elif hasattr(canvas_panel, 'get_width'):
parent_width = canvas_panel.get_width()
if hasattr(canvas_panel, 'height'):
parent_height = canvas_panel.height
elif hasattr(canvas_panel, 'get_height'):
parent_height = canvas_panel.get_height()
# DEBUG: Trace fill layout calculations
# print(f"[_apply_fill_layout] Comp #{comp_index} Parent #{parent_index}: PW={parent_width} PH={parent_height}")
if parent_width is None or parent_height is None:
# print(f"[_apply_fill_layout] Failed to get parent dimensions for Comp #{comp_index}")
return
margin_left = float(comp_data.get('fill_margin_left', 0.0))
margin_right = float(comp_data.get('fill_margin_right', 0.0))
margin_top = float(comp_data.get('fill_margin_top', 0.0))
margin_bottom = float(comp_data.get('fill_margin_bottom', 0.0))
new_left = margin_left
new_top = margin_top
new_width = float(parent_width) - (margin_left + margin_right)
new_height = float(parent_height) - (margin_top + margin_bottom)
if new_width < 1.0:
new_width = 1.0
if new_height < 1.0:
new_height = 1.0
comp_data['left'] = new_left
comp_data['top'] = new_top
comp_data['width'] = new_width
comp_data['height'] = new_height
if comp_obj is not None:
# Handle visual_parent_canvas case: convert local to absolute
abs_left = new_left
abs_top = new_top
if parent_index is not None and parent_index >= 0 and comp_data.get('visual_parent_canvas'):
p_abs_x, p_abs_y = self._get_component_accumulated_pos(parent_index)
abs_left = p_abs_x + new_left
abs_top = p_abs_y + new_top
if hasattr(comp_obj, 'set_pos'):
comp_obj.set_pos(abs_left, abs_top)
else:
if hasattr(comp_obj, 'left'):
comp_obj.left = abs_left
if hasattr(comp_obj, 'top'):
comp_obj.top = abs_top
if hasattr(comp_obj, 'width'):
comp_obj.width = new_width
if hasattr(comp_obj, 'height'):
comp_obj.height = new_height
if comp_type in ['Plane', 'Image', 'Video'] and 'sprite' in comp_data:
comp_data['sprite'].width = new_width
comp_data['sprite'].height = new_height
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)
# Force update internal layout for Button to ensure fill works
try:
layout = getattr(comp_obj, '_layout', None)
if layout:
if hasattr(layout, 'width'): layout.width = new_width
if hasattr(layout, 'height'): layout.height = new_height
# Inner layout
inner = getattr(layout, '_layout', None)
if inner:
if hasattr(inner, 'width'): inner.width = new_width
if hasattr(inner, 'height'): inner.height = new_height
# Mid sprite usually stretches
spr_mid = getattr(layout, '_sprite_mid', None)
if spr_mid:
spr_mid.width = new_width
spr_mid.height = new_height
except Exception:
pass
elif comp_type == 'InputField':
if hasattr(comp_obj, 'set_width'):
comp_obj.set_width(new_width)
elif comp_type == 'Slider':
if hasattr(comp_obj, 'set_width'):
comp_obj.set_width(new_width)
def _apply_layout_spacing(self, layout_obj, spacing):
try:
if hasattr(layout_obj, 'set_spacing'):
layout_obj.set_spacing(spacing)
elif hasattr(layout_obj, 'setSpacing'):
layout_obj.setSpacing(spacing)
elif hasattr(layout_obj, 'spacing'):
layout_obj.spacing = spacing
elif hasattr(layout_obj, '_spacing'):
layout_obj._spacing = spacing
except Exception:
pass
def _apply_layout_alignment(self, layout_obj, align):
try:
if hasattr(layout_obj, 'set_alignment'):
layout_obj.set_alignment(align)
elif hasattr(layout_obj, 'alignment'):
layout_obj.alignment = align
elif hasattr(layout_obj, 'align'):
layout_obj.align = align
except Exception:
pass
def _update_layout_inner(self, comp_index):
if comp_index < 0 or comp_index >= len(self.components):
return
comp_data = self.components[comp_index]
layout_obj = comp_data.get('layout_obj')
if layout_obj is None:
return
width = float(comp_data.get('width', 0.0))
height = float(comp_data.get('height', 0.0))
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))
inner_w = max(1.0, width - (pad_left + pad_right))
inner_h = max(1.0, height - (pad_top + pad_bottom))
try:
if hasattr(layout_obj, 'left'):
layout_obj.left = pad_left
if hasattr(layout_obj, 'top'):
layout_obj.top = pad_top
if hasattr(layout_obj, 'width'):
layout_obj.width = inner_w
if hasattr(layout_obj, 'height'):
layout_obj.height = inner_h
if hasattr(layout_obj, 'set_size'):
layout_obj.set_size(inner_w, inner_h)
except Exception:
pass
spacing = float(comp_data.get('layout_spacing', 0.0))
self._apply_layout_spacing(layout_obj, spacing)
align = comp_data.get('layout_align', None)
if align:
self._apply_layout_alignment(layout_obj, align)
if comp_data.get('type') in ['HorizontalLayout', 'VerticalLayout']:
self._apply_wrap_layout(comp_index)
try:
if hasattr(layout_obj, 'update'):
layout_obj.update()
except Exception:
pass
def _reparent_scrollable_children(self, parent_index):
if parent_index < 0 or parent_index >= len(self.components):
return
parent_data = self.components[parent_index]
if parent_data.get('type') != 'ScrollableRegion':
return
parent_obj = parent_data['object']
try:
content_node = parent_obj.content_node
except Exception:
return
for child_idx in parent_data.get('children_indices', []):
if child_idx < 0 or child_idx >= len(self.components):
continue
child_data = self.components[child_idx]
child_obj = child_data.get('object')
if child_obj is None:
continue
try:
if hasattr(child_obj, 'reparent_to'):
child_obj.reparent_to(content_node)
elif hasattr(child_obj, 'parent'):
child_obj.parent = content_node
except Exception:
pass
def _apply_wrap_layout(self, parent_index):
if parent_index < 0 or parent_index >= len(self.components):
return
parent_data = self.components[parent_index]
layout_type = parent_data.get('type')
if layout_type not in ['HorizontalLayout', 'VerticalLayout']:
return
if not parent_data.get('layout_wrap', True):
return
# Get parent absolute position for coordinate conversion
parent_abs_left, parent_abs_top = self._get_component_accumulated_pos(parent_index)
width = float(parent_data.get('width', 0.0))
height = float(parent_data.get('height', 0.0))
pad_left = float(parent_data.get('layout_padding_left', 0.0))
pad_right = float(parent_data.get('layout_padding_right', 0.0))
pad_top = float(parent_data.get('layout_padding_top', 0.0))
pad_bottom = float(parent_data.get('layout_padding_bottom', 0.0))
spacing = float(parent_data.get('layout_spacing', 0.0))
line_spacing = float(parent_data.get('layout_line_spacing', 0.0))
inner_w = max(1.0, width - (pad_left + pad_right))
inner_h = max(1.0, height - (pad_top + pad_bottom))
if layout_type == 'HorizontalLayout':
x = pad_left
y = pad_top
row_h = 0.0
for child_idx in parent_data.get('children_indices', []):
if child_idx < 0 or child_idx >= len(self.components):
continue
child = self.components[child_idx]
child_obj = child.get('object')
cw = float(child.get('width', 0.0))
ch = float(child.get('height', 0.0))
# Allow layout even if size is 0, but usually skip
# if cw <= 0 or ch <= 0: continue
if x > pad_left and (x + cw) > (pad_left + inner_w):
x = pad_left
y += row_h + line_spacing
row_h = 0.0
# Data stores relative position (to parent)
child['left'] = x
child['top'] = y
# Update visual object
try:
child_parent = getattr(child_obj, 'parent', None)
parent_obj = parent_data.get('object')
if child_parent is not None and child_parent == parent_obj:
# Child is parented to layout object: use relative coordinates
if hasattr(child_obj, 'left'):
child_obj.left = x
if hasattr(child_obj, 'top'):
child_obj.top = y
if hasattr(child_obj, 'set_pos'):
child_obj.set_pos(x, y)
else:
# Child is NOT parented to layout object (e.g. at root): use absolute coordinates
abs_x = parent_abs_left + x
abs_y = parent_abs_top + y
if hasattr(child_obj, 'left'):
child_obj.left = abs_x
if hasattr(child_obj, 'top'):
child_obj.top = abs_y
if hasattr(child_obj, 'set_pos'):
child_obj.set_pos(abs_x, abs_y)
except Exception:
pass
# Recursively update child layout/visuals
if child.get('type') in ['HorizontalLayout', 'VerticalLayout'] and child.get('layout_wrap', True):
self._apply_wrap_layout(child_idx)
elif child.get('children_indices'):
self._sync_canvas_children(child_idx)
row_h = max(row_h, ch)
x += cw + spacing
else:
# Vertical wrap: flow top->bottom, then next column
x = pad_left
y = pad_top
col_w = 0.0
for child_idx in parent_data.get('children_indices', []):
if child_idx < 0 or child_idx >= len(self.components):
continue
child = self.components[child_idx]
child_obj = child.get('object')
cw = float(child.get('width', 0.0))
ch = float(child.get('height', 0.0))
if y > pad_top and (y + ch) > (pad_top + inner_h):
y = pad_top
x += col_w + line_spacing
col_w = 0.0
child['left'] = x
child['top'] = y
try:
child_parent = getattr(child_obj, 'parent', None)
parent_obj = parent_data.get('object')
if child_parent is not None and child_parent == parent_obj:
# Child is parented to layout object: use relative coordinates
if hasattr(child_obj, 'left'):
child_obj.left = x
if hasattr(child_obj, 'top'):
child_obj.top = y
if hasattr(child_obj, 'set_pos'):
child_obj.set_pos(x, y)
else:
# Child is NOT parented to layout object (e.g. at root): use absolute coordinates
abs_x = parent_abs_left + x
abs_y = parent_abs_top + y
if hasattr(child_obj, 'left'):
child_obj.left = abs_x
if hasattr(child_obj, 'top'):
child_obj.top = abs_y
if hasattr(child_obj, 'set_pos'):
child_obj.set_pos(abs_x, abs_y)
except Exception:
pass
# If child is also a layout, recursively update it
if child.get('type') in ['HorizontalLayout', 'VerticalLayout'] and child.get('layout_wrap', True):
self._apply_wrap_layout(child_idx)
# If child is NOT a layout but has children (like a Frame with children), update their visual text
elif child.get('children_indices'):
self._sync_canvas_children(child_idx)
col_w = max(col_w, cw)
y += ch + spacing
def get_selected_component(self):
"""获取当前选中的组件"""
if self.selected_index >= 0 and self.selected_index < len(self.components):
return self.components[self.selected_index]
return None
def select_component(self, index):
"""选中指定索引的组件"""
if 0 <= index < len(self.components):
self.selected_index = index
print(f"✓ 选中组件 #{index}: {self.components[index]['type']}")
else:
self.selected_index = -1
def deselect_all(self):
"""取消选中所有组件"""
if getattr(self, '_force_keep_selection_frames', 0) > 0:
return
self.selected_index = -1
print("✓ 已取消选中所有组件")
def _change_image_texture(self, title = "选择文件",filetypes=None,initialdir=None):
"""更改图像组件的纹理"""
try:
import tkinter as tk
from tkinter import filedialog
root = tk.Tk()
root.withdraw()
root.attributes('-topmost',True)
if filetypes is None:
filetypes = [("所有文件","*.*")]
file_path = filedialog.askopenfilename(
title = title,
filetypes = filetypes,
initialdir = initialdir
)
root.destroy()
if file_path:
return file_path
return None
except Exception as e:
print(f"更改纹理失败: {e}")
def _update_component_anchor_position(self, comp_index, anchor_position, manual_offset=(0.0, 0.0)):
"""更新组件的锚点位置"""
if comp_index < 0 or comp_index >= len(self.components):
return
comp_data = self.components[comp_index]
comp_obj = comp_data['object']
# Update data
comp_data['anchor_position'] = anchor_position
comp_data['anchored_to_parent'] = True
comp_data['anchor_manual_offset_x'] = manual_offset[0]
comp_data['anchor_manual_offset_y'] = manual_offset[1]
# Determine strict parent reference
parent_index = comp_data.get('parent_index')
parent_width = 0
parent_height = 0
if parent_index is not None and parent_index >= 0:
parent_data = self.components[parent_index]
parent_width = parent_data.get('width', 100)
parent_height = parent_data.get('height', 30)
# Try to get actual object size if available
p_obj = parent_data.get('object')
if p_obj:
if hasattr(p_obj, 'width') and p_obj.width > 0: parent_width = p_obj.width
if hasattr(p_obj, 'height') and p_obj.height > 0: parent_height = p_obj.height
elif self.current_canvas_index >= 0:
# Canvas anchor
canvas = self.canvases[self.current_canvas_index]['panel']
if hasattr(canvas, 'get_width'):
parent_width = canvas.get_width()
parent_height = canvas.get_height()
else:
parent_width = canvas.width
parent_height = canvas.height
# Calculate anchor point in parent local space
anchor_ratios = {
'top-left': (0, 0), 'top-center': (0.5, 0), 'top-right': (1, 0),
'middle-left': (0, 0.5), 'center': (0.5, 0.5), 'middle-right': (1, 0.5),
'bottom-left': (0, 1), 'bottom-center': (0.5, 1), 'bottom-right': (1, 1)
}
rx, ry = anchor_ratios.get(anchor_position, (0, 0))
anchor_x = parent_width * rx
anchor_y = parent_height * ry
child_width = comp_data.get('width', 0)
child_height = comp_data.get('height', 0)
# Try to get actual object size
if hasattr(comp_obj, 'width') and comp_obj.width > 0: child_width = comp_obj.width
if hasattr(comp_obj, 'height') and comp_obj.height > 0: child_height = comp_obj.height
# Center child on anchor (pivot logic)
child_left = anchor_x - (child_width * rx) + manual_offset[0]
child_top = anchor_y - (child_height * ry) + manual_offset[1]
# Update logical position data
comp_data['left'] = child_left
comp_data['top'] = child_top
# Update visual position
if parent_index is not None and parent_index >= 0:
# Check if visually parented to canvas (absolute positioning needed)
if comp_data.get('visual_parent_canvas'):
p_abs_x, p_abs_y = self._get_component_accumulated_pos(parent_index)
abs_left = p_abs_x + child_left
abs_top = p_abs_y + child_top
# Prefer left/top properties for LUI objects
if hasattr(comp_obj, 'left') and hasattr(comp_obj, 'top'):
comp_obj.left = abs_left
comp_obj.top = abs_top
elif hasattr(comp_obj, 'set_pos'):
try:
comp_obj.set_pos(abs_left, abs_top)
except TypeError:
# Fallback for NodePath-like objects requiring 3 args
comp_obj.set_pos(abs_left, abs_top, 0)
else:
# Standard local parenting
if hasattr(comp_obj, 'left') and hasattr(comp_obj, 'top'):
comp_obj.left = child_left
comp_obj.top = child_top
elif hasattr(comp_obj, 'set_pos'):
try:
comp_obj.set_pos(child_left, child_top)
except TypeError:
comp_obj.set_pos(child_left, child_top, 0)
else:
# Canvas root
if hasattr(comp_obj, 'left') and hasattr(comp_obj, 'top'):
comp_obj.left = child_left
comp_obj.top = child_top
elif hasattr(comp_obj, 'set_pos'):
try:
comp_obj.set_pos(child_left, child_top)
except TypeError:
comp_obj.set_pos(child_left, child_top, 0)
print(f"✓ 已更新锚点位置: {anchor_position} -> ({child_left:.1f}, {child_top:.1f})")
# Update children positions (for visual_parent_canvas or anchored children)
self._sync_canvas_children(comp_index)
self._update_anchored_children(comp_index)
def _handle_anchor_popup(self):
"""Handle component anchor selection popup"""
if imgui.begin_popup("选择锚点位置"):
imgui.text("选择锚点位置:")
anchor_names = {
'top-left': '左上', 'top-center': '中上', 'top-right': '右上',
'middle-left': '左中', 'center': '中心', 'middle-right': '右中',
'bottom-left': '左下', 'bottom-center': '中下', 'bottom-right': '右下'
}
anchor_positions = [
['top-left', 'top-center', 'top-right'],
['middle-left', 'center', 'middle-right'],
['bottom-left', 'bottom-center', 'bottom-right']
]
for row in anchor_positions:
for i, pos in enumerate(row):
button_text = anchor_names.get(pos, pos)
if imgui.button(f"{button_text}##popup_{pos}", (50, 25)):
if hasattr(self, '_temp_selected_index_for_anchor'):
selected_idx = self._temp_selected_index_for_anchor
# Reset manual offset by passing (0,0) explicitly to match original behavior logic
self._update_component_anchor_position(selected_idx, pos, manual_offset=(0.0, 0.0))
# Clean up temp attribute
delattr(self, '_temp_selected_index_for_anchor')
# Reset flag if used elsewhere
self.anchor_popup_open = False
imgui.close_current_popup()
if i < len(row) - 1:
imgui.same_line()
imgui.spacing()
if imgui.button("取消", (160, 25)):
self.anchor_popup_open = False
if hasattr(self, '_temp_selected_index_for_anchor'):
delattr(self, '_temp_selected_index_for_anchor')
imgui.close_current_popup()
imgui.end_popup()