refactor(lui): complete batch split for interaction/editor/property flows

This commit is contained in:
ayuan9957 2026-03-01 19:39:13 +08:00
parent 22a9b5b64a
commit 718cf802be
3 changed files with 1648 additions and 1471 deletions

File diff suppressed because it is too large Load Diff

View File

@ -28,231 +28,245 @@ class LUIManagerEditorMixin:
comp_data['children_indices'] = []
def _set_parent_child_relationship(self, child_index, parent_index, anchor_position=None, keep_world=False):
# Set parent-child relationship
if (child_index < 0 or child_index >= len(self.components) or
parent_index < 0 or parent_index >= len(self.components)):
if not self._is_valid_relationship_index(child_index, parent_index):
print("Error: invalid component index")
return
child_data = self.components[child_index]
parent_data = self.components[parent_index]
if not self._prepare_parent_child_link(child_index, parent_index, child_data, parent_data):
return
# 编排入口:仅更新逻辑父子关系,不执行真实 reparent避免破坏复杂组件内部结构。
current_abs_left, current_abs_top = self._get_component_accumulated_pos(child_index)
print(f"[_set_parent_child_relationship] Child #{child_index} -> Parent #{parent_index}")
print(f" Current absolute position: ({current_abs_left:.1f}, {current_abs_top:.1f})")
self._remove_child_from_old_parent(child_data, child_index)
self._attach_child_to_new_parent(child_data, parent_data, child_index, parent_index)
new_local_left, new_local_top = self._compute_new_child_local_position(
child_index, parent_index, current_abs_left, current_abs_top
)
child_data['left'] = new_local_left
child_data['top'] = new_local_top
child_obj = child_data['object']
parent_obj = self._resolve_parent_visual_object(parent_data)
try:
self._set_child_visual_parent_policy(child_data, parent_data)
if not keep_world:
new_local_left, new_local_top = self._clamp_child_local_within_parent(
child_data, parent_data, child_obj, parent_obj, new_local_left, new_local_top
)
child_data['left'] = new_local_left
child_data['top'] = new_local_top
self._sync_child_canvas_index_with_parent(child_data, parent_data)
self._apply_child_absolute_position(child_obj, current_abs_left, current_abs_top, keep_world)
self._ensure_child_visibility_and_layer(child_data, parent_data, child_obj, parent_obj)
except Exception as e:
print(f"Reparenting error: {e}")
self._finalize_parent_child_link(child_index, parent_index, parent_data)
def _is_valid_relationship_index(self, child_index, parent_index):
return (
0 <= child_index < len(self.components)
and 0 <= parent_index < len(self.components)
)
def _prepare_parent_child_link(self, child_index, parent_index, child_data, parent_data):
if parent_data.get('type') in ['VerticalLayout', 'HorizontalLayout']:
child_data['draggable'] = False
if child_data.get('parent_index') == parent_index:
print(f"Component {child_index} already under {parent_index}")
return False
return True
def _remove_child_from_old_parent(self, child_data, child_index):
old_parent_index = child_data.get('parent_index')
if old_parent_index is None or old_parent_index < 0:
return
current_abs_left, current_abs_top = self._get_component_accumulated_pos(child_index)
print(f"[_set_parent_child_relationship] Child #{child_index} -> Parent #{parent_index}")
print(f" Current absolute position: ({current_abs_left:.1f}, {current_abs_top:.1f})")
old_parent_index = child_data.get('parent_index')
if old_parent_index is not None and old_parent_index >= 0:
old_parent_data = self.components[old_parent_index]
if child_index in old_parent_data.get('children_indices', []):
old_parent_data['children_indices'].remove(child_index)
old_parent_data = self.components[old_parent_index]
if child_index in old_parent_data.get('children_indices', []):
old_parent_data['children_indices'].remove(child_index)
def _attach_child_to_new_parent(self, child_data, parent_data, child_index, parent_index):
child_data['parent_index'] = parent_index
if 'children_indices' not in parent_data:
parent_data['children_indices'] = []
if child_index not in parent_data['children_indices']:
parent_data['children_indices'].append(child_index)
def _compute_new_child_local_position(self, child_index, parent_index, current_abs_left, current_abs_top):
parent_abs_left, parent_abs_top = self._get_component_accumulated_pos(parent_index)
new_local_left = current_abs_left - parent_abs_left
new_local_top = current_abs_top - parent_abs_top
print(f" Parent absolute position: ({parent_abs_left:.1f}, {parent_abs_top:.1f})")
print(f" Calculated local position: ({new_local_left:.1f}, {new_local_top:.1f})")
return new_local_left, new_local_top
# 在数据结构中存储局部坐标(相对于父组件)
child_data['left'] = new_local_left
child_data['top'] = new_local_top
child_obj = child_data['object']
def _resolve_parent_visual_object(self, parent_data):
parent_obj = parent_data['object']
parent_type = parent_data.get('type')
if parent_type == 'ScrollableRegion':
return parent_data['object'].content_node
if parent_type in ['VerticalLayout', 'HorizontalLayout'] and parent_data.get('layout_obj'):
if parent_data.get('layout_wrap', True):
return parent_data.get('object')
return parent_data.get('layout_obj').cell()
return parent_obj
def _set_child_visual_parent_policy(self, child_data, parent_data):
visual_container_types = [
'Frame', 'Plane', 'Video', 'HttpText',
'ScrollableRegion', 'TabbedFrame',
'VerticalLayout', 'HorizontalLayout'
'VerticalLayout', 'HorizontalLayout',
]
if parent_data.get('type') == 'ScrollableRegion':
parent_obj = parent_data['object'].content_node
elif parent_data.get('type') in ['VerticalLayout', 'HorizontalLayout'] and parent_data.get('layout_obj'):
if parent_data.get('layout_wrap', True):
parent_obj = parent_data.get('object')
else:
parent_obj = parent_data.get('layout_obj').cell()
if parent_data.get('type') in visual_container_types:
child_data['visual_parent_canvas'] = False
print("[_set_parent_child_relationship] Setting parent relationship (data only, no reparenting)")
else:
# 非视觉容器保持画布父级显示,避免被父组件裁剪。
child_data['visual_parent_canvas'] = True
def _clamp_child_local_within_parent(
self, child_data, parent_data, child_obj, parent_obj, new_local_left, new_local_top
):
try:
if parent_type in visual_container_types:
child_data['visual_parent_canvas'] = False
# 关键修改:不要调用 reparent_to(),避免破坏内部结构
# 只更新数据结构,保持原有的父对象关系
print(f"[_set_parent_child_relationship] Setting parent relationship (data only, no reparenting)")
else:
# Keep visual parent on canvas to avoid clipping, but keep logical parent
child_data['visual_parent_canvas'] = True
parent_w = parent_data.get('width')
parent_h = parent_data.get('height')
if parent_w is None and hasattr(parent_obj, 'width'):
parent_w = parent_obj.width
if parent_h is None and hasattr(parent_obj, 'height'):
parent_h = parent_obj.height
# If not preserving world position, clamp into parent bounds for visibility
if not keep_world:
try:
parent_w = parent_data.get('width')
parent_h = parent_data.get('height')
if parent_w is None and hasattr(parent_obj, 'width'):
parent_w = parent_obj.width
if parent_h is None and hasattr(parent_obj, 'height'):
parent_h = parent_obj.height
child_w = child_data.get('width')
child_h = child_data.get('height')
if child_w is None and hasattr(child_obj, 'width'):
child_w = child_obj.width
if child_h is None and hasattr(child_obj, 'height'):
child_h = child_obj.height
if parent_w and parent_h and child_w and child_h:
max_x = max(0.0, float(parent_w) - float(child_w))
max_y = max(0.0, float(parent_h) - float(child_h))
new_local_left = max(0.0, min(float(new_local_left), max_x))
new_local_top = max(0.0, min(float(new_local_top), max_y))
child_data['left'] = new_local_left
child_data['top'] = new_local_top
except Exception:
pass
child_w = child_data.get('width')
child_h = child_data.get('height')
if child_w is None and hasattr(child_obj, 'width'):
child_w = child_obj.width
if child_h is None and hasattr(child_obj, 'height'):
child_h = child_obj.height
# 关键修改由于我们不实际重新父化对象所有组件都保持相对于Canvas的绝对位置
# 只在数据结构中记录相对于父组件的局部坐标
if child_data.get('visual_parent_canvas'):
# Keep world position
try:
child_data['canvas_index'] = parent_data.get('canvas_index', self.current_canvas_index)
except Exception:
pass
# 根据keep_world参数决定是否保持世界位置
if keep_world:
# 保持世界位置:使用绝对坐标
if hasattr(child_obj, 'left'):
child_obj.left = current_abs_left
if hasattr(child_obj, 'top'):
child_obj.top = current_abs_top
print(f"[_set_parent_child_relationship] Child position kept at absolute: ({current_abs_left:.1f}, {current_abs_top:.1f})")
else:
# 不保持世界位置:使用局部坐标(但由于没有实际重新父化,这会导致位置跳变)
# 为了避免跳变,我们仍然使用绝对坐标
if hasattr(child_obj, 'left'):
child_obj.left = current_abs_left
if hasattr(child_obj, 'top'):
child_obj.top = current_abs_top
print(f"[_set_parent_child_relationship] Child position set to absolute: ({current_abs_left:.1f}, {current_abs_top:.1f})")
if parent_w and parent_h and child_w and child_h:
max_x = max(0.0, float(parent_w) - float(child_w))
max_y = max(0.0, float(parent_h) - float(child_h))
new_local_left = max(0.0, min(float(new_local_left), max_x))
new_local_top = max(0.0, min(float(new_local_top), max_y))
except Exception:
pass
# Ensure child visuals are visible above parent
if hasattr(child_obj, 'z_offset'):
try:
parent_z = getattr(parent_obj, 'z_offset', 0)
child_obj.z_offset = float(parent_z) + 2.0
except Exception:
pass
return new_local_left, new_local_top
# Special handling for Button: raise internal sprites/label above parent
if child_data.get('type') == 'Button':
try:
layout = getattr(child_obj, '_layout', None)
if layout is not None:
for attr in ('_sprite_left', '_sprite_mid', '_sprite_right'):
spr = getattr(layout, attr, None)
if spr is not None and hasattr(spr, 'z_offset'):
spr.z_offset = float(getattr(child_obj, 'z_offset', 0)) + 1.0
lbl = getattr(child_obj, '_label', None)
if lbl is not None and hasattr(lbl, 'z_offset'):
lbl.z_offset = float(getattr(child_obj, 'z_offset', 0)) + 2.0
# Ensure label visible
if lbl is not None and hasattr(lbl, 'show'):
lbl.show()
except Exception:
pass
# Special handling for Frame: ensure visibility
elif child_data.get('type') == 'Frame':
try:
if hasattr(child_obj, 'show'):
child_obj.show()
if hasattr(child_obj, 'visible'):
child_obj.visible = True
print(f"[_set_parent_child_relationship] Frame visibility ensured")
except Exception as e:
print(f"[_set_parent_child_relationship] Error ensuring Frame visibility: {e}")
# Special handling for Plane, Image, Video: ensure sprite visibility
elif child_data.get('type') in ['Plane', 'Image', 'Video', 'HttpText']:
try:
spr = child_data.get('sprite')
if spr is not None:
if hasattr(spr, 'show'):
spr.show()
if hasattr(spr, 'visible'):
spr.visible = True
# Ensure sprite color is not transparent
if child_data.get('type') == 'Plane':
color = child_data.get('color', (1, 1, 1, 1))
if hasattr(spr, 'color'):
spr.color = color
print(f"[_set_parent_child_relationship] {child_data.get('type')} sprite visibility ensured")
except Exception as e:
print(f"[_set_parent_child_relationship] Error ensuring {child_data.get('type')} visibility: {e}")
def _sync_child_canvas_index_with_parent(self, child_data, parent_data):
if not child_data.get('visual_parent_canvas'):
return
try:
child_data['canvas_index'] = parent_data.get('canvas_index', self.current_canvas_index)
except Exception:
pass
child_sprite = child_data.get('sprite')
if child_sprite is not None:
try:
if hasattr(child_sprite, 'show'):
child_sprite.show()
# Ensure sprite is attached to the child object
if hasattr(child_sprite, 'parent') and child_sprite.parent != child_obj:
child_sprite.parent = child_obj
# Restore sprite color if stored
if hasattr(child_sprite, 'color') and 'color' in child_data:
child_sprite.color = child_data.get('color', child_sprite.color)
if hasattr(child_sprite, 'z_offset'):
parent_sprite = parent_data.get('sprite')
parent_sprite_z = 0.0
if parent_sprite is not None and hasattr(parent_sprite, 'z_offset'):
parent_sprite_z = float(parent_sprite.z_offset)
child_sprite.z_offset = max(parent_sprite_z + 1.0, float(getattr(child_obj, 'z_offset', 0)) + 1.0)
except Exception:
pass
# Keep child visible above parent visuals
if hasattr(child_obj, 'z_offset'):
def _apply_child_absolute_position(self, child_obj, current_abs_left, current_abs_top, keep_world):
if hasattr(child_obj, 'left'):
child_obj.left = current_abs_left
if hasattr(child_obj, 'top'):
child_obj.top = current_abs_top
if keep_world:
print(f"[_set_parent_child_relationship] Child position kept at absolute: ({current_abs_left:.1f}, {current_abs_top:.1f})")
else:
print(f"[_set_parent_child_relationship] Child position set to absolute: ({current_abs_left:.1f}, {current_abs_top:.1f})")
def _ensure_child_visibility_and_layer(self, child_data, parent_data, child_obj, parent_obj):
if hasattr(child_obj, 'z_offset'):
try:
parent_z = getattr(parent_obj, 'z_offset', 0)
try:
child_obj.z_offset = float(parent_z) + 2.0
except Exception:
pass
# Ensure child sprite renders above parent visuals
child_sprite = child_data.get('sprite')
if child_sprite is not None and hasattr(child_sprite, 'z_offset'):
parent_sprite_z = 0
try:
parent_sprite = parent_data.get('sprite')
if parent_sprite is not None and hasattr(parent_sprite, 'z_offset'):
parent_sprite_z = float(parent_sprite.z_offset)
except Exception:
parent_sprite_z = 0
try:
child_sprite.z_offset = max(float(parent_sprite_z) + 1.0, float(getattr(child_obj, 'z_offset', 0)) + 1.0)
except Exception:
pass
child_obj.z_offset = float(parent_z) + 2.0
except Exception:
pass
comp_type = child_data.get('type')
if comp_type == 'Button':
self._ensure_button_child_visibility(child_obj)
elif comp_type == 'Frame':
self._ensure_frame_child_visibility(child_obj)
elif comp_type in ['Plane', 'Image', 'Video', 'HttpText']:
self._ensure_media_child_visibility(child_data)
self._ensure_child_sprite_visibility_and_layer(child_data, parent_data, child_obj)
if hasattr(child_obj, 'show'):
child_obj.show()
def _ensure_button_child_visibility(self, child_obj):
try:
layout = getattr(child_obj, '_layout', None)
if layout is not None:
for attr in ('_sprite_left', '_sprite_mid', '_sprite_right'):
spr = getattr(layout, attr, None)
if spr is not None and hasattr(spr, 'z_offset'):
spr.z_offset = float(getattr(child_obj, 'z_offset', 0)) + 1.0
lbl = getattr(child_obj, '_label', None)
if lbl is not None and hasattr(lbl, 'z_offset'):
lbl.z_offset = float(getattr(child_obj, 'z_offset', 0)) + 2.0
if lbl is not None and hasattr(lbl, 'show'):
lbl.show()
except Exception:
pass
def _ensure_frame_child_visibility(self, child_obj):
try:
if hasattr(child_obj, 'show'):
child_obj.show()
if hasattr(child_obj, 'visible'):
child_obj.visible = True
print("[_set_parent_child_relationship] Frame visibility ensured")
except Exception as e:
print(f"Reparenting error: {e}")
print(f"[_set_parent_child_relationship] Error ensuring Frame visibility: {e}")
# Ensure canvas_index follows parent canvas (visibility)
def _ensure_media_child_visibility(self, child_data):
try:
spr = child_data.get('sprite')
if spr is not None:
if hasattr(spr, 'show'):
spr.show()
if hasattr(spr, 'visible'):
spr.visible = True
if child_data.get('type') == 'Plane' and hasattr(spr, 'color'):
spr.color = child_data.get('color', (1, 1, 1, 1))
print(f"[_set_parent_child_relationship] {child_data.get('type')} sprite visibility ensured")
except Exception as e:
print(f"[_set_parent_child_relationship] Error ensuring {child_data.get('type')} visibility: {e}")
def _ensure_child_sprite_visibility_and_layer(self, child_data, parent_data, child_obj):
child_sprite = child_data.get('sprite')
if child_sprite is None:
return
try:
if hasattr(child_sprite, 'show'):
child_sprite.show()
if hasattr(child_sprite, 'parent') and child_sprite.parent != child_obj:
child_sprite.parent = child_obj
if hasattr(child_sprite, 'color') and 'color' in child_data:
child_sprite.color = child_data.get('color', child_sprite.color)
if hasattr(child_sprite, 'z_offset'):
parent_sprite_z = 0.0
parent_sprite = parent_data.get('sprite')
if parent_sprite is not None and hasattr(parent_sprite, 'z_offset'):
parent_sprite_z = float(parent_sprite.z_offset)
child_sprite.z_offset = max(
parent_sprite_z + 1.0,
float(getattr(child_obj, 'z_offset', 0)) + 1.0,
)
except Exception:
pass
def _finalize_parent_child_link(self, child_index, parent_index, parent_data):
parent_canvas = parent_data.get('canvas_index')
if parent_canvas is not None:
self._update_canvas_index_recursive(child_index, parent_canvas)

View File

@ -1213,68 +1213,140 @@ class LUIManagerInteractionMixin:
print("✓ 重新创建了手柄,确保在最前面")
def _handle_resize_drag(self):
"""处理 resize 拖拽(编排入口)。"""
self._handle_resize_drag_core()
def _handle_resize_drag_core(self):
"""处理resize拖拽 - 完整功能实现支持Shift和Alt键操作"""
if not hasattr(self.world, 'mouseWatcherNode') or not self.world.mouseWatcherNode.hasMouse():
return
# 检查鼠标左键是否释放
import panda3d.core as p3d
if not self.world.mouseWatcherNode.is_button_down(p3d.MouseButton.one()):
if self.resizing_handle:
comp_data = self.components[self.selected_index]
width_val = comp_data.get('width', 0)
height_val = comp_data.get('height', 0)
print(f"✓ 组件调整完成: width={width_val:.1f}, height={height_val:.1f}")
# 更新锚点位置(如果有锚点)
if comp_data.get('anchored_to_parent'):
anchor_pos = comp_data.get('anchor_position')
if anchor_pos:
self._update_component_anchor_position(self.selected_index, anchor_pos)
self.resizing_handle = None
if self._finish_resize_drag_if_released(p3d):
return
# 获取当前鼠标位置
mouse_x = self.world.mouseWatcherNode.getMouseX()
mouse_y = self.world.mouseWatcherNode.getMouseY()
win_x = self.world.win.getXSize()
win_y = self.world.win.getYSize()
pixel_x = (mouse_x + 1) * win_x / 2
pixel_y = (1 - mouse_y) * win_y / 2
# 计算鼠标移动距离
delta_x = pixel_x - self.resize_start_pos[0]
delta_y = pixel_y - self.resize_start_pos[1]
# 检查修饰键
shift_held = self.world.mouseWatcherNode.is_button_down(p3d.KeyboardButton.shift())
alt_held = self.world.mouseWatcherNode.is_button_down(p3d.KeyboardButton.alt())
# 获取组件数据
comp_data = self.components[self.selected_index]
comp_obj = comp_data['object']
comp_type = comp_data['type']
# 获取初始边界Canvas相对坐标
new_left, new_top, new_width, new_height = self._compute_resize_drag_bounds(
delta_x, delta_y, shift_held, alt_held
)
if self.selected_index < 0:
return
comp_data = self.components[self.selected_index]
self._apply_resize_drag_updates(
comp_data, comp_obj, comp_type,
new_left, new_top, new_width, new_height,
shift_held, alt_held,
)
def _finish_resize_drag_if_released(self, p3d):
"""鼠标释放时结束缩放并执行收尾同步。"""
if self.world.mouseWatcherNode.is_button_down(p3d.MouseButton.one()):
return False
if self.resizing_handle and self.selected_index >= 0 and self.selected_index < len(self.components):
comp_data = self.components[self.selected_index]
width_val = comp_data.get('width', 0)
height_val = comp_data.get('height', 0)
print(f"✓ 组件调整完成: width={width_val:.1f}, height={height_val:.1f}")
# 更新锚点位置(如果有锚点)
if comp_data.get('anchored_to_parent'):
anchor_pos = comp_data.get('anchor_position')
if anchor_pos:
self._update_component_anchor_position(self.selected_index, anchor_pos)
self.resizing_handle = None
return True
def _compute_resize_drag_bounds(self, delta_x, delta_y, shift_held, alt_held):
"""根据当前手柄/修饰键计算新的 left/top/width/height。"""
start_left, start_top, start_width, start_height, aspect_ratio = self._get_resize_start_values()
handle_pos = self.resizing_handle._resize_position
new_left, new_top, new_width, new_height = self._apply_resize_handle_delta(
handle_pos, start_left, start_top, start_width, start_height, delta_x, delta_y
)
new_left, new_top, new_width, new_height = self._apply_resize_min_size_clamp(
handle_pos, start_left, start_top, start_width, start_height,
new_left, new_top, new_width, new_height,
)
if shift_held:
new_left, new_top, new_width, new_height = self._apply_shift_keep_ratio_resize(
handle_pos, start_left, start_top, start_width, start_height,
new_left, new_top, new_width, new_height, aspect_ratio,
)
if alt_held:
new_left, new_top, new_width, new_height = self._apply_alt_center_resize(
start_left, start_top, start_width, start_height,
new_left, new_top, new_width, new_height,
)
self._cache_resize_canvas_bounds()
return self._sanitize_resize_dimensions(new_left, new_top, new_width, new_height)
def _apply_resize_drag_updates(
self, comp_data, comp_obj, comp_type,
new_left, new_top, new_width, new_height,
shift_held, alt_held,
):
"""将缩放结果写回组件数据与渲染对象。"""
parent_index, parent_offset_x, parent_offset_y, is_scene_parented = self._resolve_resize_parent_context(
comp_data, comp_obj
)
_ = parent_index
self._write_resize_component_data(comp_data, new_left, new_top, new_width, new_height)
self._apply_resize_component_position(
comp_obj, new_left, new_top, parent_offset_x, parent_offset_y, is_scene_parented
)
try:
self._apply_resize_component_size_by_type(comp_data, comp_obj, comp_type, new_width, new_height)
self._post_resize_component_sync(comp_type, shift_held, alt_held)
except Exception as e:
print(f"⚠ 设置组件尺寸失败 ({comp_type}): {e}")
def _get_resize_start_values(self):
start_left = self.resize_start_bounds['left']
start_top = self.resize_start_bounds['top']
start_width = self.resize_start_bounds['width']
start_height = self.resize_start_bounds['height']
# 计算初始宽高比
aspect_ratio = start_width / start_height if start_height > 0 else 1
# 根据手柄位置计算新的边界
handle_pos = self.resizing_handle._resize_position
return start_left, start_top, start_width, start_height, aspect_ratio
def _apply_resize_handle_delta(
self, handle_pos, start_left, start_top, start_width, start_height, delta_x, delta_y
):
new_left = start_left
new_top = start_top
new_width = start_width
new_height = start_height
# 根据手柄位置调整尺寸
if handle_pos == 'top-left':
new_left = start_left + delta_x
new_top = start_top + delta_y
@ -1301,171 +1373,191 @@ class LUIManagerInteractionMixin:
elif handle_pos == 'left':
new_left = start_left + delta_x
new_width = start_width - delta_x
# 应用最小尺寸限制
return new_left, new_top, new_width, new_height
def _apply_resize_min_size_clamp(
self, handle_pos, start_left, start_top, start_width, start_height,
new_left, new_top, new_width, new_height,
):
if new_width < self.min_size:
if handle_pos in ['top-left', 'left', 'bottom-left']:
new_left = start_left + start_width - self.min_size
new_width = self.min_size
if new_height < self.min_size:
if handle_pos in ['top-left', 'top', 'top-right']:
new_top = start_top + start_height - self.min_size
new_height = self.min_size
# Shift键保持宽高比
if shift_held:
width_change = abs(new_width - start_width)
height_change = abs(new_height - start_height)
if width_change > height_change:
new_height = new_width / aspect_ratio
if handle_pos in ['top-left', 'top', 'top-right']:
new_top = start_top + start_height - new_height
else:
new_width = new_height * aspect_ratio
if handle_pos in ['top-left', 'left', 'bottom-left']:
new_left = start_left + start_width - new_width
# Alt键从中心点缩放
if alt_held:
center_x = start_left + start_width / 2
center_y = start_top + start_height / 2
width_diff = new_width - start_width
height_diff = new_height - start_height
new_width = start_width + width_diff * 2
new_height = start_height + height_diff * 2
return new_left, new_top, new_width, new_height
def _apply_shift_keep_ratio_resize(
self, handle_pos, start_left, start_top, start_width, start_height,
new_left, new_top, new_width, new_height, aspect_ratio,
):
width_change = abs(new_width - start_width)
height_change = abs(new_height - start_height)
if width_change > height_change:
new_height = new_width / aspect_ratio
if handle_pos in ['top-left', 'top', 'top-right']:
new_top = start_top + start_height - new_height
else:
new_width = new_height * aspect_ratio
if handle_pos in ['top-left', 'left', 'bottom-left']:
new_left = start_left + start_width - new_width
return new_left, new_top, new_width, new_height
def _apply_alt_center_resize(
self, start_left, start_top, start_width, start_height,
new_left, new_top, new_width, new_height,
):
center_x = start_left + start_width / 2
center_y = start_top + start_height / 2
width_diff = new_width - start_width
height_diff = new_height - start_height
new_width = start_width + width_diff * 2
new_height = start_height + height_diff * 2
new_left = center_x - new_width / 2
new_top = center_y - new_height / 2
if new_width < self.min_size:
new_width = self.min_size
new_left = center_x - new_width / 2
if new_height < self.min_size:
new_height = self.min_size
new_top = center_y - new_height / 2
# 再次应用最小尺寸限制
if new_width < self.min_size:
new_width = self.min_size
new_left = center_x - new_width / 2
if new_height < self.min_size:
new_height = self.min_size
new_top = center_y - new_height / 2
# 获取Canvas边界约束
canvas_width = 800 # 默认Canvas宽度
canvas_height = 600 # 默认Canvas高度
return new_left, new_top, new_width, new_height
def _cache_resize_canvas_bounds(self):
canvas_width = 800
canvas_height = 600
if self.current_canvas_index >= 0 and self.current_canvas_index < len(self.canvases):
canvas_panel = self.canvases[self.current_canvas_index]['panel']
canvas_width = canvas_panel.get_width()
canvas_height = canvas_panel.get_height()
# 最后的安全检查,防止宽高变为负数
if new_width < 1.0: new_width = 1.0
if new_height < 1.0: new_height = 1.0
if self.selected_index < 0:
return
_ = (canvas_width, canvas_height)
comp_data = self.components[self.selected_index]
def _sanitize_resize_dimensions(self, new_left, new_top, new_width, new_height):
if new_width < 1.0:
new_width = 1.0
if new_height < 1.0:
new_height = 1.0
return new_left, new_top, new_width, new_height
def _resolve_resize_parent_context(self, comp_data, comp_obj):
parent_index = comp_data.get('parent_index')
parent_offset_x = 0
parent_offset_y = 0
if parent_index is not None and parent_index >= 0:
parent_offset_x, parent_offset_y = self._get_component_accumulated_pos(parent_index)
parent_offset_x, parent_offset_y = self._get_component_accumulated_pos(parent_index)
# Update component data
child_parent = getattr(comp_obj, 'parent', None)
parent_obj = None
if parent_index is not None and parent_index >= 0:
parent_obj = self.components[parent_index].get('object')
is_scene_parented = (child_parent is not None and child_parent == parent_obj)
return parent_index, parent_offset_x, parent_offset_y, is_scene_parented
def _write_resize_component_data(self, comp_data, new_left, new_top, new_width, new_height):
comp_data['left'] = new_left
comp_data['top'] = new_top
comp_data['width'] = new_width
comp_data['height'] = new_height
# Update component position
child_parent = getattr(comp_obj, 'parent', None)
parent_obj = None
if parent_index is not None and parent_index >= 0:
parent_obj = self.components[parent_index].get('object')
# Check if actually parented in LUI scene graph
is_scene_parented = (child_parent is not None and child_parent == parent_obj)
def _apply_resize_component_position(
self, comp_obj, new_left, new_top, parent_offset_x, parent_offset_y, is_scene_parented
):
if is_scene_parented:
# Component is physically parented to its logical parent -> Use Relative coords
comp_obj.left = new_left
comp_obj.top = new_top
else:
# Component is physically root/canvas -> Use Absolute coords (Parent Abs + Relative)
comp_obj.left = parent_offset_x + new_left
comp_obj.top = parent_offset_y + new_top
# Update component size
def _apply_resize_component_size_by_type(self, comp_data, comp_obj, comp_type, new_width, new_height):
if hasattr(comp_obj, 'width'):
comp_obj.width = new_width
if hasattr(comp_obj, 'height'):
comp_obj.height = new_height
if comp_type == 'Frame':
pass
elif comp_type in ['Plane', 'Image', 'Video', 'HttpText']:
sprite = comp_data.get('sprite')
if sprite is not None:
sprite.width = new_width
sprite.height = new_height
if comp_type == 'HttpText':
self.luiFunction.sync_http_text_layout(self, comp_data)
elif comp_type == 'Button':
self._apply_button_resize_size(comp_obj, new_width, new_height)
elif comp_type == 'InputField':
self._apply_input_field_resize_size(comp_obj, new_width, new_height)
elif comp_type == 'Slider':
if hasattr(comp_obj, 'set_width'):
comp_obj.set_width(new_width)
if hasattr(comp_obj, 'set_height'):
comp_obj.set_height(new_height)
def _apply_button_resize_size(self, comp_obj, new_width, new_height):
if hasattr(comp_obj, 'set_width'):
comp_obj.set_width(new_width)
if hasattr(comp_obj, 'set_height'):
comp_obj.set_height(new_height)
if hasattr(comp_obj, '_apply_stretch_sizes'):
comp_obj._apply_stretch_sizes()
def _apply_input_field_resize_size(self, comp_obj, new_width, new_height):
if hasattr(comp_obj, 'set_width'):
comp_obj.set_width(new_width)
if hasattr(comp_obj, 'set_height'):
comp_obj.set_height(new_height)
if hasattr(comp_obj, 'width'):
comp_obj.width = new_width
if hasattr(comp_obj, 'height'):
comp_obj.height = new_height
self._sync_input_field_layout_stretch(comp_obj)
def _sync_input_field_layout_stretch(self, comp_obj):
try:
if hasattr(comp_obj, 'width'):
comp_obj.width = new_width
if hasattr(comp_obj, 'height'):
comp_obj.height = new_height
layout = getattr(comp_obj, '_layout', None)
if layout is not None:
if hasattr(layout, 'width'):
layout.width = '100%'
if hasattr(layout, 'height'):
layout.height = '100%'
inner = getattr(layout, '_layout', None)
if inner is not None:
if hasattr(inner, 'width'):
inner.width = '100%'
if hasattr(inner, 'height'):
inner.height = '100%'
for attr in ('_sprite_left', '_sprite_mid', '_sprite_right'):
spr = getattr(layout, attr, None)
if spr is not None and hasattr(spr, 'height'):
spr.height = '100%'
if spr is not None and attr == '_sprite_mid' and hasattr(spr, 'width'):
spr.width = '100%'
except Exception:
pass
if comp_type == 'Frame':
pass
elif comp_type in ['Plane', 'Image', 'Video', 'HttpText']:
sprite = comp_data.get('sprite')
if sprite is not None:
sprite.width = new_width
sprite.height = new_height
if comp_type == 'HttpText':
self.luiFunction.sync_http_text_layout(self, comp_data)
elif comp_type == 'Button':
if hasattr(comp_obj, 'set_width'):
comp_obj.set_width(new_width)
if hasattr(comp_obj, 'set_height'):
comp_obj.set_height(new_height)
if hasattr(comp_obj, '_apply_stretch_sizes'):
comp_obj._apply_stretch_sizes()
elif comp_type == 'InputField':
if hasattr(comp_obj, 'set_width'):
comp_obj.set_width(new_width)
if hasattr(comp_obj, 'set_height'):
comp_obj.set_height(new_height)
if hasattr(comp_obj, 'width'):
comp_obj.width = new_width
if hasattr(comp_obj, 'height'):
comp_obj.height = new_height
try:
layout = getattr(comp_obj, '_layout', None)
if layout is not None:
if hasattr(layout, 'width'):
layout.width = '100%'
if hasattr(layout, 'height'):
layout.height = '100%'
inner = getattr(layout, '_layout', None)
if inner is not None:
if hasattr(inner, 'width'):
inner.width = '100%'
if hasattr(inner, 'height'):
inner.height = '100%'
for attr in ('_sprite_left', '_sprite_mid', '_sprite_right'):
spr = getattr(layout, attr, None)
if spr is not None and hasattr(spr, 'height'):
spr.height = '100%'
if spr is not None and attr == '_sprite_mid' and hasattr(spr, 'width'):
spr.width = '100%'
except Exception:
pass
elif comp_type == 'Slider':
if hasattr(comp_obj, 'set_width'):
comp_obj.set_width(new_width)
if hasattr(comp_obj, 'set_height'):
comp_obj.set_height(new_height)
def _post_resize_component_sync(self, comp_type, shift_held, alt_held):
if comp_type in ['VerticalLayout', 'HorizontalLayout']:
self._update_layout_inner(self.selected_index)
if comp_type in ['VerticalLayout', 'HorizontalLayout']:
self._update_layout_inner(self.selected_index)
self._update_anchored_children(self.selected_index)
modifier_str = self._get_resize_modifier_info(shift_held, alt_held)
_ = modifier_str
self._update_anchored_children(self.selected_index)
modifier_info = []
if shift_held:
modifier_info.append('Shift(keep ratio)')
if alt_held:
modifier_info.append('Alt(center scale)')
modifier_str = ' + '.join(modifier_info) if modifier_info else 'Normal'
except Exception as e:
print(f"⚠ 设置组件尺寸失败 ({comp_type}): {e}")
def _get_resize_modifier_info(self, shift_held, alt_held):
modifier_info = []
if shift_held:
modifier_info.append('Shift(keep ratio)')
if alt_held:
modifier_info.append('Alt(center scale)')
return ' + '.join(modifier_info) if modifier_info else 'Normal'