IMGui
2
.idea/misc.xml
generated
@ -3,5 +3,5 @@
|
||||
<component name="Black">
|
||||
<option name="sdkName" value="Python 3.12 (PythonProject)" />
|
||||
</component>
|
||||
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.10 (EG)" project-jdk-type="Python SDK" />
|
||||
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.10" project-jdk-type="Python SDK" />
|
||||
</project>
|
||||
@ -9,14 +9,14 @@ enabled:
|
||||
- color_correction
|
||||
- env_probes
|
||||
- forward_shading
|
||||
- motion_blur
|
||||
# - motion_blur # disabled for editor performance
|
||||
- pssm
|
||||
- scattering
|
||||
- skin_shading
|
||||
- sky_ao
|
||||
- smaa
|
||||
- ssr
|
||||
- clouds
|
||||
# - ssr # disabled for editor performance
|
||||
#- clouds
|
||||
#- dof
|
||||
#- fxaa
|
||||
#- volumetrics
|
||||
@ -120,7 +120,7 @@ overrides:
|
||||
logarithmic_factor: 3.0
|
||||
sun_distance: 100.0
|
||||
split_count: 4
|
||||
resolution: 1024
|
||||
resolution: 512
|
||||
border_bias: 0.058
|
||||
use_pcf: True
|
||||
filter_sequence: halton_2D_32
|
||||
@ -133,7 +133,7 @@ overrides:
|
||||
pcss_penumbra_size: 2.38
|
||||
pcss_min_penumbra_size: 7.0
|
||||
use_distant_shadows: True
|
||||
dist_shadow_resolution: 4096
|
||||
dist_shadow_resolution: 1024
|
||||
dist_shadow_clipsize: 400.0
|
||||
dist_shadow_sundist: 300.0
|
||||
scene_shadow_resolution: 512
|
||||
@ -157,7 +157,7 @@ overrides:
|
||||
sky_ao:
|
||||
sample_radius: 17.17
|
||||
max_radius: 500.0
|
||||
resolution: 1024
|
||||
resolution: 512
|
||||
sample_sequence: poisson_2D_32
|
||||
ao_multiplier: 0.83
|
||||
ao_bias: 0.0
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
from direct.task.TaskManagerGlobal import taskMgr
|
||||
from panda3d.core import KeyboardButton
|
||||
|
||||
|
||||
class CustomMouseController:
|
||||
@ -23,18 +24,30 @@ class CustomMouseController:
|
||||
self.showbase.accept("mouse3-up", self.setKey, ["mouse3", False]) # 右键释放
|
||||
|
||||
self.showbase.accept("w", self.setKey, ["cam-forward", True])
|
||||
self.showbase.accept("W", self.setKey, ["cam-forward", True])
|
||||
self.showbase.accept("a", self.setKey, ["cam-left", True])
|
||||
self.showbase.accept("A", self.setKey, ["cam-left", True])
|
||||
self.showbase.accept("s", self.setKey, ["cam-backward", True])
|
||||
self.showbase.accept("S", self.setKey, ["cam-backward", True])
|
||||
self.showbase.accept("d", self.setKey, ["cam-right", True])
|
||||
self.showbase.accept("D", self.setKey, ["cam-right", True])
|
||||
self.showbase.accept("e", self.setKey, ["cam-up", True])
|
||||
self.showbase.accept("E", self.setKey, ["cam-up", True])
|
||||
self.showbase.accept("q", self.setKey, ["cam-down", True])
|
||||
self.showbase.accept("Q", self.setKey, ["cam-down", True])
|
||||
|
||||
self.showbase.accept("w-up", self.setKey, ["cam-forward", False])
|
||||
self.showbase.accept("W-up", self.setKey, ["cam-forward", False])
|
||||
self.showbase.accept("a-up", self.setKey, ["cam-left", False])
|
||||
self.showbase.accept("A-up", self.setKey, ["cam-left", False])
|
||||
self.showbase.accept("s-up", self.setKey, ["cam-backward", False])
|
||||
self.showbase.accept("S-up", self.setKey, ["cam-backward", False])
|
||||
self.showbase.accept("d-up", self.setKey, ["cam-right", False])
|
||||
self.showbase.accept("D-up", self.setKey, ["cam-right", False])
|
||||
self.showbase.accept("e-up", self.setKey, ["cam-up", False])
|
||||
self.showbase.accept("E-up", self.setKey, ["cam-up", False])
|
||||
self.showbase.accept("q-up", self.setKey, ["cam-down", False])
|
||||
self.showbase.accept("Q-up", self.setKey, ["cam-down", False])
|
||||
|
||||
self.last_mouse_x = 0
|
||||
self.last_mouse_y = 0
|
||||
@ -61,17 +74,25 @@ class CustomMouseController:
|
||||
|
||||
# 检查ImGui是否捕获了键盘输入
|
||||
if self._should_handle_keyboard():
|
||||
if self.keyMap["cam-left"]:
|
||||
watcher = self.showbase.mouseWatcherNode
|
||||
left_down = self.keyMap["cam-left"] or watcher.is_button_down(KeyboardButton.ascii_key("a"))
|
||||
right_down = self.keyMap["cam-right"] or watcher.is_button_down(KeyboardButton.ascii_key("d"))
|
||||
backward_down = self.keyMap["cam-backward"] or watcher.is_button_down(KeyboardButton.ascii_key("s"))
|
||||
forward_down = self.keyMap["cam-forward"] or watcher.is_button_down(KeyboardButton.ascii_key("w"))
|
||||
up_down = self.keyMap["cam-up"] or watcher.is_button_down(KeyboardButton.ascii_key("e"))
|
||||
down_down = self.keyMap["cam-down"] or watcher.is_button_down(KeyboardButton.ascii_key("q"))
|
||||
|
||||
if left_down:
|
||||
self.showbase.camera.setX(self.showbase.camera, -self.move_speed * dt)
|
||||
if self.keyMap["cam-right"]:
|
||||
if right_down:
|
||||
self.showbase.camera.setX(self.showbase.camera, +self.move_speed * dt)
|
||||
if self.keyMap["cam-backward"]:
|
||||
if backward_down:
|
||||
self.showbase.camera.setY(self.showbase.camera, -self.move_speed * dt)
|
||||
if self.keyMap["cam-forward"]:
|
||||
if forward_down:
|
||||
self.showbase.camera.setY(self.showbase.camera, +self.move_speed * dt)
|
||||
if self.keyMap["cam-up"]:
|
||||
if up_down:
|
||||
self.showbase.camera.setZ(self.showbase.camera, +self.move_speed * dt)
|
||||
if self.keyMap["cam-down"]:
|
||||
if down_down:
|
||||
self.showbase.camera.setZ(self.showbase.camera, -self.move_speed * dt)
|
||||
if self.keyMap["mouse3"]: # 只使用右键控制视角旋转
|
||||
try:
|
||||
|
||||
@ -254,7 +254,7 @@ class EventHandler:
|
||||
return
|
||||
|
||||
# 根据当前工具处理点击事件
|
||||
if self.world.currentTool == "选择":
|
||||
if self.world.currentTool in ("选择", "移动", "旋转", "缩放"):
|
||||
print("✓ 使用选择工具处理点击")
|
||||
try:
|
||||
self._handleSelectionClick(hitNode)
|
||||
|
||||
@ -402,7 +402,10 @@ class ImGuiStyleManager:
|
||||
if icon_path.exists():
|
||||
# 使用base.imgui.loadTexture方法
|
||||
if hasattr(base, 'imgui') and hasattr(base.imgui, 'loadTexture'):
|
||||
return base.imgui.loadTexture(str(icon_path))
|
||||
# 转换路径为Panda3D兼容格式 (Windows下: D:\... -> /d/...)
|
||||
# 注意: p3dimgui.loadTexture 仅支持 str 或 Texture,不支持 Filename 对象
|
||||
fn = Filename.fromOsSpecific(str(icon_path))
|
||||
return base.imgui.loadTexture(fn.getFullpath())
|
||||
else:
|
||||
print(f"⚠ ImGui后端未初始化")
|
||||
return None
|
||||
|
||||
@ -118,6 +118,30 @@ class SelectionSystem:
|
||||
def _resetCursor(self):
|
||||
self._setCursor("default")
|
||||
|
||||
def _has_new_transform_gizmo(self):
|
||||
tg = getattr(self.world, "newTransform", None)
|
||||
return tg is not None
|
||||
|
||||
def sync_transform_gizmo_mode(self):
|
||||
"""Sync TransformGizmo mode with current tool."""
|
||||
if not self._has_new_transform_gizmo():
|
||||
return
|
||||
try:
|
||||
from TransformGizmo.events import TransformGizmoMode
|
||||
tool_mgr = getattr(self.world, "tool_manager", None)
|
||||
if not tool_mgr:
|
||||
return
|
||||
if tool_mgr.isRotateTool():
|
||||
self.world.newTransform.set_mode(TransformGizmoMode.ROTATE)
|
||||
elif tool_mgr.isScaleTool():
|
||||
self.world.newTransform.set_mode(TransformGizmoMode.SCALE)
|
||||
elif tool_mgr.isMoveTool() or tool_mgr.isSelectionTool():
|
||||
self.world.newTransform.set_mode(TransformGizmoMode.MOVE)
|
||||
else:
|
||||
self.world.newTransform.set_mode(TransformGizmoMode.NONE)
|
||||
except Exception as e:
|
||||
print(f"sync transform gizmo mode failed: {e}")
|
||||
|
||||
# ==================== 选择框系统 ====================
|
||||
|
||||
def createSelectionBox(self, nodePath):
|
||||
@ -135,8 +159,8 @@ class SelectionSystem:
|
||||
self.selectionBox = self.world.render.attachNewNode("selectionBox")
|
||||
self.selectionBoxTarget = nodePath
|
||||
|
||||
taskMgr.add(self.updateSelectionBoxTask, "updateSelectionBox")
|
||||
self.updateSelectionBoxGeometry()
|
||||
#taskMgr.add(self.updateSelectionBoxTask, "updateSelectionBox")
|
||||
#self.updateSelectionBoxGeometry()
|
||||
|
||||
except Exception as e:
|
||||
print(f" ✗ 创建选择框失败: {str(e)}")
|
||||
@ -340,6 +364,17 @@ class SelectionSystem:
|
||||
|
||||
def createGizmo(self, nodePath):
|
||||
"""为选中的节点创建坐标轴工具 - 保留箭头版本"""
|
||||
if self._has_new_transform_gizmo():
|
||||
self.gizmo = None
|
||||
self.gizmoTarget = nodePath
|
||||
if not nodePath:
|
||||
return
|
||||
self.sync_transform_gizmo_mode()
|
||||
try:
|
||||
self.world.newTransform.attach(nodePath)
|
||||
except Exception as e:
|
||||
print(f"attach TransformGizmo failed: {e}")
|
||||
return
|
||||
try:
|
||||
#print(f" 开始创建坐标轴,目标节点: {nodePath.getName()}")
|
||||
|
||||
@ -818,15 +853,31 @@ class SelectionSystem:
|
||||
pass
|
||||
|
||||
def clearGizmo(self):
|
||||
"""清除坐标轴"""
|
||||
"""Clear transform gizmo."""
|
||||
if self._has_new_transform_gizmo():
|
||||
try:
|
||||
self.world.newTransform.detach()
|
||||
except Exception as e:
|
||||
print(f"detach TransformGizmo failed: {e}")
|
||||
self.gizmo = None
|
||||
self.gizmoTarget = None
|
||||
self.gizmoXAxis = None
|
||||
self.gizmoYAxis = None
|
||||
self.gizmoZAxis = None
|
||||
self.isDraggingGizmo = False
|
||||
self.dragGizmoAxis = None
|
||||
self.dragStartMousePos = None
|
||||
self.gizmoTargetStartPos = None
|
||||
self.gizmoStartPos = None
|
||||
self._resetCursor()
|
||||
return
|
||||
|
||||
if self.gizmo:
|
||||
self.gizmo.removeNode()
|
||||
self.gizmo = None
|
||||
|
||||
# 停止坐标轴更新任务
|
||||
taskMgr.remove("updateGizmo")
|
||||
|
||||
# 清除坐标轴相关引用
|
||||
self.gizmoTarget = None
|
||||
self.gizmoXAxis = None
|
||||
self.gizmoYAxis = None
|
||||
@ -838,39 +889,8 @@ class SelectionSystem:
|
||||
self.gizmoStartPos = None
|
||||
self._resetCursor()
|
||||
|
||||
|
||||
# def setGizmoAxisColor(self, axis, color):
|
||||
# """设置坐标轴颜色 - RenderPipeline 兼容版本"""
|
||||
# try:
|
||||
# from panda3d.core import AntialiasAttrib,TransparencyAttrib
|
||||
#
|
||||
# axis_nodes = {
|
||||
# "x": self.gizmoXAxis,
|
||||
# "y": self.gizmoYAxis,
|
||||
# "z": self.gizmoZAxis
|
||||
# }
|
||||
#
|
||||
# if axis in axis_nodes and axis_nodes[axis]:
|
||||
# axis_node = axis_nodes[axis]
|
||||
#
|
||||
# axis_node.setColor(color[0]*20.0,color[1]*20.0,color[2]*20.0,color[3])
|
||||
# axis_node.setColorScale(color[0]*10.0,color[1]*10.0,color[2]*10.0,color[3])
|
||||
# axis_node.setShaderOff(10000)
|
||||
# axis_node.setLightOff()
|
||||
# axis_node.setMaterialOff()
|
||||
# axis_node.setTextureOff()
|
||||
# axis_node.setFogOff()
|
||||
#
|
||||
# except Exception as e:
|
||||
# print(f"设置坐标轴颜色失败: {str(e)}")
|
||||
# # 回退到简单的颜色设置
|
||||
# try:
|
||||
# if axis in axis_nodes and axis_nodes[axis]:
|
||||
# axis_nodes[axis].setColor(*color)
|
||||
# except:
|
||||
# pass
|
||||
|
||||
|
||||
def setGizmoRotAxisColor(self, axis, color):
|
||||
"""使用材质设置坐标轴颜色 - RenderPipeline兼容版本"""
|
||||
try:
|
||||
|
||||
@ -37,6 +37,12 @@ class ToolManager:
|
||||
|
||||
# 坐标轴现在始终跟随选中状态,不再依赖工具类型
|
||||
|
||||
try:
|
||||
if hasattr(self.world, "selection") and self.world.selection:
|
||||
self.world.selection.sync_transform_gizmo_mode()
|
||||
except Exception as e:
|
||||
print(f"sync gizmo mode on tool change failed: {e}")
|
||||
|
||||
def getCurrentTool(self):
|
||||
"""获取当前工具"""
|
||||
return self.currentTool
|
||||
|
||||
@ -20,6 +20,7 @@ render_pipeline_path = os.path.join(project_root, "RenderPipelineFile")
|
||||
sys.path.insert(0, render_pipeline_path)
|
||||
|
||||
from RenderPipelineFile.rpcore import RenderPipeline
|
||||
from ssbo_component.ssbo_editor import SSBOEditor
|
||||
|
||||
# 从渲染管线工具模块导入全局函数
|
||||
from core.render_pipeline_utils import get_render_pipeline, set_render_pipeline
|
||||
@ -47,12 +48,19 @@ class CoreWorld(ShowBase):
|
||||
loadPrcFileData("", f"win-size {width} {height}")
|
||||
loadPrcFileData("", "win-fixed-size #f") # 允许窗口调整大小
|
||||
|
||||
# Performance preset to match the standalone smooth runner
|
||||
loadPrcFileData("", "threading-model /Draw")
|
||||
loadPrcFileData("", "pstats-gpu-timing #f")
|
||||
loadPrcFileData("", "gl-debug #f")
|
||||
loadPrcFileData("", "gl-debug-object-labels #f")
|
||||
|
||||
# VR性能优化配置
|
||||
loadPrcFileData("", "prefer-single-buffer true")
|
||||
loadPrcFileData("", "gl-force-flush false")
|
||||
loadPrcFileData("", "sync-video false")
|
||||
loadPrcFileData("", "support-stencil false")
|
||||
loadPrcFileData("", "clock-mode non-real-time")
|
||||
loadPrcFileData("", "support-threads false")
|
||||
|
||||
if is_fullscreen:
|
||||
loadPrcFileData("", "fullscreen #t")
|
||||
@ -61,6 +69,9 @@ class CoreWorld(ShowBase):
|
||||
self.render_pipeline = RenderPipeline()
|
||||
self.render_pipeline.pre_showbase_init()
|
||||
|
||||
# 强制开启多线程支持 (Video播放必需)
|
||||
loadPrcFileData("force_threads", "support-threads #f")
|
||||
|
||||
# 初始化 ShowBase
|
||||
ShowBase.__init__(self)
|
||||
|
||||
|
||||
117
imgui.ini
@ -1,5 +1,5 @@
|
||||
[Window][Debug##Default]
|
||||
Pos=28,731
|
||||
Pos=28,465
|
||||
Size=400,400
|
||||
Collapsed=0
|
||||
|
||||
@ -24,34 +24,34 @@ Size=832,45
|
||||
Collapsed=0
|
||||
|
||||
[Window][工具栏]
|
||||
Pos=287,20
|
||||
Size=1234,32
|
||||
Pos=325,20
|
||||
Size=1228,32
|
||||
Collapsed=0
|
||||
DockId=0x00000007,0
|
||||
DockId=0x0000000D,0
|
||||
|
||||
[Window][场景树]
|
||||
Pos=0,20
|
||||
Size=285,776
|
||||
Size=323,634
|
||||
Collapsed=0
|
||||
DockId=0x00000001,0
|
||||
DockId=0x00000007,0
|
||||
|
||||
[Window][属性面板]
|
||||
Pos=1523,20
|
||||
Size=327,498
|
||||
Pos=1555,20
|
||||
Size=365,989
|
||||
Collapsed=0
|
||||
DockId=0x00000005,0
|
||||
DockId=0x00000003,0
|
||||
|
||||
[Window][控制台]
|
||||
Pos=880,798
|
||||
Size=641,218
|
||||
Pos=0,656
|
||||
Size=323,353
|
||||
Collapsed=0
|
||||
DockId=0x0000000C,0
|
||||
DockId=0x00000008,0
|
||||
|
||||
[Window][脚本管理]
|
||||
Pos=1523,520
|
||||
Size=327,496
|
||||
Pos=1540,20
|
||||
Size=380,390
|
||||
Collapsed=0
|
||||
DockId=0x00000006,0
|
||||
DockId=0x00000003,1
|
||||
|
||||
[Window][中文显示测试]
|
||||
Pos=60,60
|
||||
@ -60,7 +60,7 @@ Collapsed=0
|
||||
|
||||
[Window][WindowOverViewport_11111111]
|
||||
Pos=0,20
|
||||
Size=1850,996
|
||||
Size=1920,989
|
||||
Collapsed=0
|
||||
|
||||
[Window][测试窗口1]
|
||||
@ -79,30 +79,30 @@ Size=93,65
|
||||
Collapsed=0
|
||||
|
||||
[Window][新建项目]
|
||||
Pos=725,358
|
||||
Pos=760,354
|
||||
Size=400,300
|
||||
Collapsed=0
|
||||
|
||||
[Window][选择路径]
|
||||
Pos=625,258
|
||||
Pos=660,254
|
||||
Size=600,500
|
||||
Collapsed=0
|
||||
|
||||
[Window][打开项目]
|
||||
Pos=675,308
|
||||
Pos=710,304
|
||||
Size=500,400
|
||||
Collapsed=0
|
||||
|
||||
[Window][导入模型]
|
||||
Pos=625,258
|
||||
Pos=660,254
|
||||
Size=600,500
|
||||
Collapsed=0
|
||||
|
||||
[Window][资源管理器]
|
||||
Pos=0,798
|
||||
Size=878,218
|
||||
Pos=325,827
|
||||
Size=1228,182
|
||||
Collapsed=0
|
||||
DockId=0x0000000B,0
|
||||
DockId=0x00000006,0
|
||||
|
||||
[Window][创建3D文本]
|
||||
Pos=60,60
|
||||
@ -135,27 +135,68 @@ Size=89,250
|
||||
Collapsed=0
|
||||
|
||||
[Window][颜色选择器]
|
||||
Pos=775,308
|
||||
Pos=810,304
|
||||
Size=300,400
|
||||
Collapsed=0
|
||||
|
||||
[Window][选择diffuse纹理文件##texture_dialog]
|
||||
Pos=625,308
|
||||
Pos=660,304
|
||||
Size=600,400
|
||||
Collapsed=0
|
||||
|
||||
[Window][创建GUI图片]
|
||||
Pos=60,60
|
||||
Size=101,226
|
||||
Collapsed=0
|
||||
|
||||
[Window][LUI编辑器]
|
||||
Pos=1540,412
|
||||
Size=380,597
|
||||
Collapsed=0
|
||||
DockId=0x00000004,0
|
||||
|
||||
[Window][LUI测试控制面板]
|
||||
Pos=6,10
|
||||
Size=300,200
|
||||
Collapsed=1
|
||||
|
||||
[Window][选择高度图文件]
|
||||
Pos=60,60
|
||||
Size=596,498
|
||||
Collapsed=0
|
||||
|
||||
[Window][创建平面地形]
|
||||
Pos=61,60
|
||||
Size=238,176
|
||||
Collapsed=0
|
||||
|
||||
[Window][选择normal纹理文件##texture_dialog]
|
||||
Pos=660,304
|
||||
Size=600,400
|
||||
Collapsed=0
|
||||
|
||||
[Window][选择metallic纹理文件##texture_dialog]
|
||||
Pos=660,304
|
||||
Size=600,400
|
||||
Collapsed=0
|
||||
|
||||
[Window][选择roughness纹理文件##texture_dialog]
|
||||
Pos=660,304
|
||||
Size=600,400
|
||||
Collapsed=0
|
||||
|
||||
[Docking][Data]
|
||||
DockSpace ID=0x08BD597D Window=0x1BBC0F80 Pos=0,20 Size=1850,996 Split=X
|
||||
DockNode ID=0x00000003 Parent=0x08BD597D SizeRef=1521,996 Split=Y
|
||||
DockNode ID=0x00000009 Parent=0x00000003 SizeRef=1380,776 Split=X
|
||||
DockNode ID=0x00000001 Parent=0x00000009 SizeRef=285,730 HiddenTabBar=1 Selected=0xE0015051
|
||||
DockNode ID=0x00000002 Parent=0x00000009 SizeRef=1234,730 Split=Y
|
||||
DockNode ID=0x00000007 Parent=0x00000002 SizeRef=1380,32 HiddenTabBar=1 Selected=0x43A39006
|
||||
DockNode ID=0x00000008 Parent=0x00000002 SizeRef=1380,742 CentralNode=1 Selected=0x5E5F7166
|
||||
DockNode ID=0x0000000A Parent=0x00000003 SizeRef=1380,218 Split=X Selected=0x5428E753
|
||||
DockNode ID=0x0000000B Parent=0x0000000A SizeRef=878,111 HiddenTabBar=1 Selected=0x3A2E05C3
|
||||
DockNode ID=0x0000000C Parent=0x0000000A SizeRef=641,111 HiddenTabBar=1 Selected=0x5428E753
|
||||
DockNode ID=0x00000004 Parent=0x08BD597D SizeRef=327,996 Split=Y Selected=0x5DB6FF37
|
||||
DockNode ID=0x00000005 Parent=0x00000004 SizeRef=304,498 HiddenTabBar=1 Selected=0x5DB6FF37
|
||||
DockNode ID=0x00000006 Parent=0x00000004 SizeRef=304,496 HiddenTabBar=1 Selected=0x3188AB8D
|
||||
DockSpace ID=0x08BD597D Window=0x1BBC0F80 Pos=0,20 Size=1920,989 Split=X
|
||||
DockNode ID=0x00000001 Parent=0x08BD597D SizeRef=1553,989 Split=X
|
||||
DockNode ID=0x00000009 Parent=0x00000001 SizeRef=323,989 Split=Y Selected=0xE0015051
|
||||
DockNode ID=0x00000007 Parent=0x00000009 SizeRef=271,634 Selected=0xE0015051
|
||||
DockNode ID=0x00000008 Parent=0x00000009 SizeRef=271,353 Selected=0x5428E753
|
||||
DockNode ID=0x0000000A Parent=0x00000001 SizeRef=1228,989 Split=Y
|
||||
DockNode ID=0x0000000D Parent=0x0000000A SizeRef=1318,32 HiddenTabBar=1 Selected=0x43A39006
|
||||
DockNode ID=0x0000000E Parent=0x0000000A SizeRef=1318,955 Split=Y
|
||||
DockNode ID=0x00000005 Parent=0x0000000E SizeRef=1341,771 CentralNode=1
|
||||
DockNode ID=0x00000006 Parent=0x0000000E SizeRef=1341,182 Selected=0x3A2E05C3
|
||||
DockNode ID=0x00000002 Parent=0x08BD597D SizeRef=365,989 Split=Y Selected=0x3188AB8D
|
||||
DockNode ID=0x00000003 Parent=0x00000002 SizeRef=351,390 Selected=0x5DB6FF37
|
||||
DockNode ID=0x00000004 Parent=0x00000002 SizeRef=351,597 Selected=0x1EB923B7
|
||||
|
||||
|
||||
@ -99,3 +99,4 @@ xdg==5
|
||||
xkit==0.0.0
|
||||
zipp==1.0.0
|
||||
openvr==2.2.0
|
||||
imgui-bundle
|
||||
|
||||
67
ssbo_component/README.md
Normal file
@ -0,0 +1,67 @@
|
||||
|
||||
# SSBO Editor Component Usage Guide
|
||||
|
||||
This directory contains a modular SSBO Scene Editor component for Panda3D RenderPipeline.
|
||||
|
||||
## 📁 File Structure
|
||||
|
||||
- `ssbo_editor.py`: Main component class `SSBOEditor`. Handles ImGui, RP integration, and input.
|
||||
- `ssbo_controller.py`: Core logic for object ID baking and SSBO matrix packing.
|
||||
- `effects/ssbo_instancing.yaml`: The custom shader effect for RenderPipeline (includes Normal Matrix fix).
|
||||
- `demo_component.py`: Example usage script.
|
||||
|
||||
## 🚀 How to Integrate
|
||||
|
||||
### 1. Prerequisites
|
||||
|
||||
Ensure your project has:
|
||||
- `RenderPipeline` setup
|
||||
- `imgui_bundle` and `p3dimgui` installed
|
||||
- `panda3d` (version 1.10.15+)
|
||||
|
||||
### 2. Basic Setup (See demo_component.py)
|
||||
|
||||
```python
|
||||
from direct.showbase.ShowBase import ShowBase
|
||||
from rpcore import RenderPipeline
|
||||
from ssbo_component.ssbo_editor import SSBOEditor
|
||||
|
||||
class MyApp(ShowBase):
|
||||
def __init__(self):
|
||||
# 1. Initialize RP
|
||||
self.rp = RenderPipeline()
|
||||
self.rp.pre_showbase_init()
|
||||
super().__init__()
|
||||
self.rp.create(self)
|
||||
|
||||
# 2. Add SSBO Component
|
||||
# Pass your ShowBase instance (self), the RP instance (self.rp), and your model path
|
||||
self.editor = SSBOEditor(
|
||||
base_app=self,
|
||||
render_pipeline=self.rp,
|
||||
model_path="path/to/your/model.glb",
|
||||
font_path="path/to/chinese/font.ttc" # Optional
|
||||
)
|
||||
|
||||
app = MyApp()
|
||||
app.run()
|
||||
```
|
||||
|
||||
### 3. Key Features
|
||||
|
||||
- **SSBO Instancing**: Efficiently renders thousands of objects using hardware instancing.
|
||||
- **Normal Correction**: Default shaders are patched to correctly handle non-uniform scaling (Inverse Transpose Normal Matrix).
|
||||
- **ImGui Editor**: Built-in scene tree browsing, search, and selection.
|
||||
- **Object Manipulation**: Select and move objects (Arrows + Z/X keys).
|
||||
- **Shadow Integration**: Correctly injects SSBO data into RP shadow passes.
|
||||
- **Motion Blur Support**: Computes per-object velocity for correct motion blur.
|
||||
|
||||
### 4. Notes
|
||||
|
||||
- **Model Preparation**: The input model should have a clear hierarchy. The component will flatten it but preserve logical objects for selection.
|
||||
- **Effects Path**: The `ssbo_instancing.yaml` is loaded relative to the `ssbo_editor.py` file, so the directory structure inside `ssbo_component` should be preserved.
|
||||
- **GPU Picking**: Basic GPU picking is implemented using shaders in `ssbo_component/shaders/`. These are loaded automatically relative to the component path.
|
||||
|
||||
## ⚠️ Important for RenderPipeline
|
||||
|
||||
If you encounter `ImportError: No module named 'rplibs.six.moves'`, ensure you apply the compatibility fix at the start of your main script (as seen in `demo_component.py`).
|
||||
77
ssbo_component/demo_component.py
Normal file
@ -0,0 +1,77 @@
|
||||
"""
|
||||
SSBO Editor Component Demo
|
||||
==========================
|
||||
Demonstrates how to usage the SSBOEditor component in a new project.
|
||||
"""
|
||||
import sys
|
||||
import os
|
||||
|
||||
# ============================================================
|
||||
# [CRITICAL] RenderPipeline Python Compatibility Fix
|
||||
# ============================================================
|
||||
if "d:/panda3d/RenderPipeline" not in sys.path:
|
||||
sys.path.insert(0, "d:/panda3d/RenderPipeline")
|
||||
|
||||
try:
|
||||
import rplibs.six
|
||||
import six
|
||||
if not hasattr(rplibs.six, 'moves'):
|
||||
rplibs.six.moves = six.moves
|
||||
sys.modules['rplibs.six.moves'] = six.moves
|
||||
except ImportError:
|
||||
import six
|
||||
import types
|
||||
if "rplibs" not in sys.modules:
|
||||
rplibs = types.ModuleType("rplibs")
|
||||
sys.modules["rplibs"] = rplibs
|
||||
if "rplibs.six" not in sys.modules:
|
||||
rplibs.six = six
|
||||
sys.modules["rplibs.six"] = six
|
||||
sys.modules["rplibs.six.moves"] = six.moves
|
||||
|
||||
# ============================================================
|
||||
# Main Application
|
||||
# ============================================================
|
||||
from direct.showbase.ShowBase import ShowBase
|
||||
from panda3d.core import loadPrcFileData, Filename
|
||||
from rpcore import RenderPipeline
|
||||
|
||||
# Import our new component
|
||||
# Assuming d:/panda3d is in python path, or running from d:/panda3d
|
||||
from ssbo_component.ssbo_editor import SSBOEditor
|
||||
|
||||
loadPrcFileData("", "show-frame-rate-meter true")
|
||||
loadPrcFileData("", "sync-video false")
|
||||
|
||||
class MyGame(ShowBase):
|
||||
def __init__(self):
|
||||
# 1. RP Init
|
||||
self.rp = RenderPipeline()
|
||||
self.rp.pre_showbase_init()
|
||||
super().__init__()
|
||||
|
||||
# Position camera before RP create to avoid huge motion blur jump
|
||||
self.cam.setPos(0, -500, 200)
|
||||
self.cam.lookAt(0, 0, 0)
|
||||
|
||||
self.rp.create(self)
|
||||
self.rp.daytime_mgr.time = 0.415
|
||||
|
||||
# 2. Use the Component
|
||||
# Point to your model
|
||||
model_path = "d:/panda3d/panda3d/jyc.glb"
|
||||
font_path = "d:/panda3d/font/msyh.ttc"
|
||||
|
||||
print("Initializing SSBO Editor Component...")
|
||||
self.editor = SSBOEditor(
|
||||
base_app=self,
|
||||
render_pipeline=self.rp,
|
||||
model_path=model_path,
|
||||
font_path=font_path
|
||||
)
|
||||
|
||||
print("Demo ready.")
|
||||
|
||||
if __name__ == "__main__":
|
||||
app = MyGame()
|
||||
app.run()
|
||||
36
ssbo_component/effects/ssbo_instancing.yaml
Normal file
@ -0,0 +1,36 @@
|
||||
vertex:
|
||||
inout: |
|
||||
// SSBO Layout - Default column_major matches Panda3D's internal layout
|
||||
layout(std430) buffer transforms {
|
||||
mat4 transform_data[];
|
||||
};
|
||||
|
||||
// Input from Vertex Color (Baked ID)
|
||||
in vec4 p3d_Color;
|
||||
|
||||
transform: |
|
||||
// Decode Local ID (p3d_Color stores Local ID within chunk)
|
||||
int low = int(p3d_Color.r * 255.0 + 0.5);
|
||||
int high = int(p3d_Color.g * 255.0 + 0.5);
|
||||
int id = high * 256 + low;
|
||||
id = clamp(id, 0, 65535);
|
||||
|
||||
// Fetch Matrix from per-chunk SSBO
|
||||
mat4 model = transform_data[id];
|
||||
|
||||
// Transform Position
|
||||
vOutput.position = (model * p3d_Vertex).xyz;
|
||||
|
||||
// Transform Normal
|
||||
vOutput.normal = normalize(mat3(model) * p3d_Normal);
|
||||
|
||||
post_transform: |
|
||||
// Motion blur disabled for performance
|
||||
|
||||
fragment:
|
||||
inout: |
|
||||
|
||||
|
||||
material: |
|
||||
vec4 tex_color = texture(p3d_Texture0, vOutput.texcoord);
|
||||
if (tex_color.a < 0.5) discard;
|
||||
21
ssbo_component/shaders/pick_id.frag
Normal file
@ -0,0 +1,21 @@
|
||||
#version 430
|
||||
|
||||
// Input from vertex shader
|
||||
flat in int v_instance_id;
|
||||
|
||||
// Output: Object ID encoded as RGB color
|
||||
out vec4 fragColor;
|
||||
|
||||
void main() {
|
||||
// Encode instance ID into RGB channels
|
||||
// R = low byte, G = high byte, B = 0, A = 1
|
||||
int low_byte = v_instance_id & 0xFF;
|
||||
int high_byte = (v_instance_id >> 8) & 0xFF;
|
||||
|
||||
fragColor = vec4(
|
||||
float(low_byte) / 255.0,
|
||||
float(high_byte) / 255.0,
|
||||
0.0,
|
||||
1.0
|
||||
);
|
||||
}
|
||||
21
ssbo_component/shaders/pick_id.vert
Normal file
@ -0,0 +1,21 @@
|
||||
#version 430
|
||||
|
||||
// No SSBO — vertices are already in world space.
|
||||
// Just read vertex color for object ID and output it.
|
||||
|
||||
in vec4 p3d_Vertex;
|
||||
in vec4 p3d_Color;
|
||||
|
||||
uniform mat4 p3d_ModelViewProjectionMatrix;
|
||||
|
||||
flat out int v_instance_id;
|
||||
|
||||
void main() {
|
||||
// Decode object ID from vertex color R/G channels
|
||||
int low_byte = int(p3d_Color.r * 255.0 + 0.5);
|
||||
int high_byte = int(p3d_Color.g * 255.0 + 0.5);
|
||||
v_instance_id = low_byte + (high_byte << 8);
|
||||
|
||||
// Standard transform — vertices already in world space
|
||||
gl_Position = p3d_ModelViewProjectionMatrix * p3d_Vertex;
|
||||
}
|
||||
550
ssbo_component/ssbo_controller.py
Normal file
@ -0,0 +1,550 @@
|
||||
|
||||
from panda3d.core import (
|
||||
GeomVertexFormat, GeomVertexWriter, GeomVertexReader, GeomVertexRewriter,
|
||||
InternalName, Vec3, Vec4, LMatrix4f, ShaderBuffer, GeomEnums,
|
||||
BoundingSphere, NodePath, GeomNode, Texture, SamplerState,
|
||||
Point3, BoundingBox, Quat
|
||||
)
|
||||
import struct
|
||||
import time
|
||||
|
||||
class ObjectController:
|
||||
"""
|
||||
物体控制器 (No Custom Shader Mode)
|
||||
====================================
|
||||
Uses RP's default rendering (no rp.set_effect) for maximum FPS.
|
||||
Vertex colors baked for picking. Movement modifies vertex data directly.
|
||||
Stores original vertex positions per object for rotation/translation.
|
||||
"""
|
||||
def __init__(self):
|
||||
self.name_to_ids = {}
|
||||
self.id_to_name = {}
|
||||
self.key_to_node = {}
|
||||
self.node_list = []
|
||||
self.display_names = {}
|
||||
self.global_transforms = [] # Original transforms (for center/position)
|
||||
|
||||
self.id_to_chunk = {} # global_id -> (chunk_key, local_idx)
|
||||
self.chunks = {} # chunk_key -> dict with 'node' key
|
||||
|
||||
# Vertex index: local_id -> list of (geom_node_np, geom_idx, [row_indices])
|
||||
self.vertex_index = {}
|
||||
|
||||
# Original vertex positions: local_id -> list of (Vec3,) matching row order
|
||||
self.original_positions = {}
|
||||
|
||||
# Current position offsets: local_id -> Vec3 delta
|
||||
self.position_offsets = {}
|
||||
self.local_to_global_id = {}
|
||||
self.local_transform_state = {}
|
||||
self.local_transform_base_positions = {}
|
||||
self.virtual_tree = None
|
||||
self.virtual_tree_meta = None
|
||||
|
||||
self.model = None
|
||||
self.chunk_node = None # Single chunk node
|
||||
|
||||
def bake_ids_and_collect(self, model):
|
||||
"""
|
||||
Bake IDs into vertex colors, flatten, then build vertex index.
|
||||
|
||||
NO transform reset — vertices keep world-space positions.
|
||||
NO SSBO — uses RP default rendering.
|
||||
"""
|
||||
t0 = time.time()
|
||||
|
||||
geom_nodes = list(model.find_all_matches("**/+GeomNode"))
|
||||
print(f"[控制器] 找到 {len(geom_nodes)} 个 GeomNode")
|
||||
|
||||
self.name_to_ids = {}
|
||||
self.id_to_name = {}
|
||||
self.key_to_node = {}
|
||||
self.node_list = []
|
||||
self.display_names = {}
|
||||
self.global_transforms = []
|
||||
self.id_to_chunk = {}
|
||||
self.chunks = {}
|
||||
self.vertex_index = {}
|
||||
self.original_positions = {}
|
||||
self.position_offsets = {}
|
||||
self.local_to_global_id = {}
|
||||
self.local_transform_state = {}
|
||||
self.local_transform_base_positions = {}
|
||||
self.virtual_tree = None
|
||||
self.virtual_tree_meta = None
|
||||
|
||||
global_id_counter = 0
|
||||
chunk_key = model.get_name() or "default"
|
||||
|
||||
# No chunk wrapper — flatten directly on model (same as load_jyc_flatten.py)
|
||||
self.chunk_node = model
|
||||
self.chunks[chunk_key] = {'node': model, 'base_id': 0}
|
||||
|
||||
# Flatten hierarchy
|
||||
for np in geom_nodes:
|
||||
np.wrt_reparent_to(model)
|
||||
|
||||
local_idx = 0
|
||||
|
||||
for np in geom_nodes:
|
||||
gnode = np.node()
|
||||
|
||||
if gnode.get_num_parents() > 1:
|
||||
parent = np.get_parent()
|
||||
if not parent.is_empty():
|
||||
new_np = np.copy_to(parent)
|
||||
np.detach_node()
|
||||
np = new_np
|
||||
gnode = np.node()
|
||||
|
||||
unique_key = str(np)
|
||||
display_name = np.get_name() or f"Object_{global_id_counter}"
|
||||
|
||||
if unique_key not in self.name_to_ids:
|
||||
self.name_to_ids[unique_key] = []
|
||||
self.key_to_node[unique_key] = np
|
||||
self.node_list.append(unique_key)
|
||||
self.display_names[unique_key] = display_name
|
||||
|
||||
# Save original transform
|
||||
mat_double = np.get_mat()
|
||||
original_transform = LMatrix4f(mat_double)
|
||||
|
||||
for i in range(gnode.get_num_geoms()):
|
||||
geom = gnode.modify_geom(i)
|
||||
vdata = geom.modify_vertex_data()
|
||||
|
||||
if not vdata.has_column("color"):
|
||||
new_format = vdata.get_format().get_union_format(GeomVertexFormat.get_v3c4())
|
||||
vdata.set_format(new_format)
|
||||
|
||||
# Encode Local ID in R/G
|
||||
low = local_idx % 256
|
||||
high = local_idx // 256
|
||||
r = low / 255.0
|
||||
g = high / 255.0
|
||||
|
||||
writer = GeomVertexWriter(vdata, InternalName.make("color"))
|
||||
for row in range(vdata.get_num_rows()):
|
||||
writer.set_row(row)
|
||||
writer.set_data4f(r, g, 0.0, 1.0)
|
||||
|
||||
self.global_transforms.append(original_transform)
|
||||
self.id_to_chunk[global_id_counter] = (chunk_key, local_idx)
|
||||
self.name_to_ids[unique_key].append(global_id_counter)
|
||||
self.id_to_name[global_id_counter] = unique_key
|
||||
self.local_to_global_id[local_idx] = global_id_counter
|
||||
self.position_offsets[local_idx] = Vec3(0, 0, 0)
|
||||
|
||||
global_id_counter += 1
|
||||
local_idx += 1
|
||||
|
||||
# DO NOT reset transform — keep world-space positions
|
||||
|
||||
# Flatten directly on model — NO set_final, allows per-geom frustum culling
|
||||
model.flatten_strong()
|
||||
|
||||
t1 = time.time()
|
||||
print(f"[控制器] Flatten took {(t1-t0)*1000:.0f}ms")
|
||||
|
||||
# Build vertex index AFTER flatten
|
||||
self._build_vertex_index(model)
|
||||
self._init_local_transform_state()
|
||||
self.build_virtual_hierarchy()
|
||||
|
||||
t2 = time.time()
|
||||
print(f"[控制器] Vertex index built in {(t2-t1)*1000:.0f}ms, "
|
||||
f"{len(self.vertex_index)} unique IDs indexed")
|
||||
|
||||
self.model = model
|
||||
self.node_list.sort()
|
||||
return global_id_counter
|
||||
|
||||
def build_virtual_hierarchy(self):
|
||||
"""Build a readonly virtual tree from node_list path keys."""
|
||||
root = {
|
||||
"name": "",
|
||||
"path": "",
|
||||
"children": {},
|
||||
"leaf_key": None,
|
||||
"display_name": "",
|
||||
}
|
||||
max_depth = 0
|
||||
leaf_count = 0
|
||||
|
||||
for key in self.node_list:
|
||||
if not key:
|
||||
continue
|
||||
parts = [p for p in str(key).split("/") if p]
|
||||
if not parts:
|
||||
continue
|
||||
max_depth = max(max_depth, len(parts))
|
||||
cursor = root
|
||||
path_acc = ""
|
||||
for i, part in enumerate(parts):
|
||||
path_acc = f"{path_acc}/{part}" if path_acc else part
|
||||
child = cursor["children"].get(part)
|
||||
if child is None:
|
||||
child = {
|
||||
"name": part,
|
||||
"path": path_acc,
|
||||
"children": {},
|
||||
"leaf_key": None,
|
||||
"display_name": part,
|
||||
}
|
||||
cursor["children"][part] = child
|
||||
cursor = child
|
||||
if i == len(parts) - 1:
|
||||
cursor["leaf_key"] = key
|
||||
cursor["display_name"] = self.display_names.get(key, part)
|
||||
leaf_count += 1
|
||||
|
||||
self.virtual_tree = root
|
||||
self.virtual_tree_meta = {"max_depth": max_depth, "leaf_count": leaf_count}
|
||||
return root
|
||||
|
||||
def get_virtual_hierarchy(self):
|
||||
"""Return cached virtual tree; build on demand."""
|
||||
if self.virtual_tree is None:
|
||||
return self.build_virtual_hierarchy()
|
||||
return self.virtual_tree
|
||||
|
||||
def _build_vertex_index(self, chunk_root):
|
||||
"""
|
||||
After flatten, batch-read all vertex data with numpy to build:
|
||||
local_id -> [(geom_node_np, geom_idx, row_indices_array)]
|
||||
Also stores original vertex positions per object (as numpy arrays).
|
||||
"""
|
||||
import numpy as np
|
||||
|
||||
for gn_np in chunk_root.find_all_matches("**/+GeomNode"):
|
||||
gnode = gn_np.node()
|
||||
for gi in range(gnode.get_num_geoms()):
|
||||
geom = gnode.get_geom(gi)
|
||||
vdata = geom.get_vertex_data()
|
||||
num_rows = vdata.get_num_rows()
|
||||
|
||||
if num_rows == 0:
|
||||
continue
|
||||
|
||||
# Find vertex and color column info
|
||||
fmt = vdata.get_format()
|
||||
|
||||
# Get position column
|
||||
pos_col = fmt.get_column(InternalName.get_vertex())
|
||||
if pos_col is None:
|
||||
continue
|
||||
pos_array_idx = fmt.get_array_with(InternalName.get_vertex())
|
||||
pos_start = pos_col.get_start()
|
||||
|
||||
# Get color column
|
||||
color_col = fmt.get_column(InternalName.make("color"))
|
||||
if color_col is None:
|
||||
continue
|
||||
color_array_idx = fmt.get_array_with(InternalName.make("color"))
|
||||
color_start = color_col.get_start()
|
||||
|
||||
# Read raw position array
|
||||
pos_array_format = fmt.get_array(pos_array_idx)
|
||||
pos_stride = pos_array_format.get_stride()
|
||||
pos_handle = vdata.get_array(pos_array_idx).get_handle()
|
||||
pos_raw = bytes(pos_handle.get_data())
|
||||
pos_buf = np.frombuffer(pos_raw, dtype=np.uint8).reshape(num_rows, pos_stride)
|
||||
|
||||
# Extract xyz positions (3 floats starting at pos_start)
|
||||
positions = np.ndarray((num_rows, 3), dtype=np.float32,
|
||||
buffer=pos_buf[:, pos_start:pos_start+12].tobytes())
|
||||
|
||||
# Read raw color array
|
||||
color_array_format = fmt.get_array(color_array_idx)
|
||||
color_stride = color_array_format.get_stride()
|
||||
|
||||
if color_array_idx == pos_array_idx:
|
||||
color_buf = pos_buf
|
||||
else:
|
||||
color_handle = vdata.get_array(color_array_idx).get_handle()
|
||||
color_raw = bytes(color_handle.get_data())
|
||||
color_buf = np.frombuffer(color_raw, dtype=np.uint8).reshape(num_rows, color_stride)
|
||||
|
||||
# Decode color format to get ID
|
||||
# Color can be stored as float32 RGBA or unorm8 RGBA
|
||||
num_components = color_col.get_num_components()
|
||||
component_bytes = color_col.get_component_bytes()
|
||||
|
||||
if component_bytes == 4: # float32 per component
|
||||
color_data = np.ndarray((num_rows, num_components), dtype=np.float32,
|
||||
buffer=color_buf[:, color_start:color_start+num_components*4].tobytes())
|
||||
r_vals = (color_data[:, 0] * 255.0 + 0.5).astype(np.int32)
|
||||
g_vals = (color_data[:, 1] * 255.0 + 0.5).astype(np.int32)
|
||||
elif component_bytes == 1: # uint8 per component
|
||||
color_bytes = color_buf[:, color_start:color_start+num_components].copy()
|
||||
r_vals = color_bytes[:, 0].astype(np.int32)
|
||||
g_vals = color_bytes[:, 1].astype(np.int32)
|
||||
else:
|
||||
# Fallback: skip this geom
|
||||
continue
|
||||
|
||||
local_ids = r_vals + (g_vals << 8)
|
||||
|
||||
# Group rows by local_id using argsort (O(N log N) instead of O(N×K))
|
||||
sort_idx = np.argsort(local_ids)
|
||||
sorted_ids = local_ids[sort_idx]
|
||||
sorted_positions = positions[sort_idx]
|
||||
|
||||
# Find group boundaries
|
||||
boundaries = np.where(np.diff(sorted_ids) != 0)[0] + 1
|
||||
|
||||
# Split into groups
|
||||
id_groups = np.split(sort_idx, boundaries)
|
||||
pos_groups = np.split(sorted_positions, boundaries)
|
||||
group_ids = sorted_ids[np.concatenate([[0], boundaries])]
|
||||
|
||||
for k in range(len(group_ids)):
|
||||
uid = int(group_ids[k])
|
||||
rows = id_groups[k]
|
||||
pos = pos_groups[k]
|
||||
|
||||
if uid not in self.vertex_index:
|
||||
self.vertex_index[uid] = []
|
||||
self.original_positions[uid] = []
|
||||
|
||||
self.vertex_index[uid].append((gn_np, gi, rows))
|
||||
self.original_positions[uid].append(pos.copy())
|
||||
|
||||
def _init_local_transform_state(self):
|
||||
"""Initialize transform state for each local_idx after vertex index is ready."""
|
||||
self.local_transform_state = {}
|
||||
self.local_transform_base_positions = {}
|
||||
|
||||
for local_idx in self.vertex_index.keys():
|
||||
self.local_transform_base_positions[local_idx] = self.original_positions.get(local_idx, [])
|
||||
self.local_transform_state[local_idx] = {
|
||||
"offset": Vec3(0, 0, 0),
|
||||
"quat": Quat.identQuat(),
|
||||
"scale": Vec3(1, 1, 1),
|
||||
"pivot": self.get_local_pivot(local_idx),
|
||||
}
|
||||
|
||||
def get_local_indices_from_global_ids(self, global_ids):
|
||||
"""Map global ids to unique local indices."""
|
||||
local_indices = []
|
||||
if not global_ids:
|
||||
return local_indices
|
||||
seen = set()
|
||||
for global_id in global_ids:
|
||||
mapping = self.id_to_chunk.get(global_id)
|
||||
if not mapping:
|
||||
continue
|
||||
_, local_idx = mapping
|
||||
if local_idx in seen:
|
||||
continue
|
||||
if local_idx not in self.vertex_index:
|
||||
continue
|
||||
seen.add(local_idx)
|
||||
local_indices.append(local_idx)
|
||||
return local_indices
|
||||
|
||||
def get_local_pivot(self, local_idx):
|
||||
"""Get pivot for one local object (world-space center)."""
|
||||
global_id = self.local_to_global_id.get(local_idx)
|
||||
if global_id is None:
|
||||
return Vec3(0, 0, 0)
|
||||
return self.get_object_center(global_id)
|
||||
|
||||
def get_selection_center(self, local_indices):
|
||||
"""Get center point for a multi-object selection."""
|
||||
if not local_indices:
|
||||
return Vec3(0, 0, 0)
|
||||
acc = Vec3(0, 0, 0)
|
||||
valid = 0
|
||||
for local_idx in local_indices:
|
||||
state = self.local_transform_state.get(local_idx)
|
||||
if not state:
|
||||
continue
|
||||
acc += state.get("pivot", Vec3(0, 0, 0)) + state.get("offset", Vec3(0, 0, 0))
|
||||
valid += 1
|
||||
if valid == 0:
|
||||
return Vec3(0, 0, 0)
|
||||
return acc / float(valid)
|
||||
|
||||
def begin_transform_session(self, local_indices):
|
||||
"""Create immutable baseline snapshot for one gizmo drag session."""
|
||||
if not local_indices:
|
||||
return {"locals": {}}
|
||||
|
||||
locals_snapshot = {}
|
||||
for local_idx in local_indices:
|
||||
base_state = self.local_transform_state.get(local_idx)
|
||||
if not base_state:
|
||||
continue
|
||||
entries = self.vertex_index.get(local_idx, [])
|
||||
base_positions = self.local_transform_base_positions.get(local_idx, [])
|
||||
locals_snapshot[local_idx] = {
|
||||
"offset": Vec3(base_state["offset"]),
|
||||
"quat": Quat(base_state["quat"]),
|
||||
"scale": Vec3(base_state["scale"]),
|
||||
"pivot": Vec3(base_state["pivot"]),
|
||||
"entries": entries,
|
||||
"base_positions": base_positions,
|
||||
}
|
||||
return {"locals": locals_snapshot}
|
||||
|
||||
def apply_transform_session(self, snapshot, delta_pos, delta_quat, delta_scale):
|
||||
"""Apply transform delta to all local indices in snapshot and rewrite vertices."""
|
||||
import numpy as np
|
||||
|
||||
if not snapshot or "locals" not in snapshot:
|
||||
return
|
||||
if delta_pos is None:
|
||||
delta_pos = Vec3(0, 0, 0)
|
||||
if delta_quat is None:
|
||||
delta_quat = Quat.identQuat()
|
||||
if delta_scale is None:
|
||||
delta_scale = Vec3(1, 1, 1)
|
||||
|
||||
dscale = np.array([delta_scale.x, delta_scale.y, delta_scale.z], dtype=np.float32)
|
||||
dpos = np.array([delta_pos.x, delta_pos.y, delta_pos.z], dtype=np.float32)
|
||||
|
||||
for local_idx, local_data in snapshot["locals"].items():
|
||||
base_offset = local_data["offset"]
|
||||
base_quat = local_data["quat"]
|
||||
base_scale = local_data["scale"]
|
||||
pivot = local_data["pivot"]
|
||||
|
||||
final_offset = Vec3(base_offset) + delta_pos
|
||||
final_quat = Quat(delta_quat * base_quat)
|
||||
final_scale = Vec3(
|
||||
base_scale.x * delta_scale.x,
|
||||
base_scale.y * delta_scale.y,
|
||||
base_scale.z * delta_scale.z,
|
||||
)
|
||||
rot_mat = self._quat_to_np_mat3(final_quat)
|
||||
|
||||
self.local_transform_state[local_idx]["offset"] = final_offset
|
||||
self.local_transform_state[local_idx]["quat"] = final_quat
|
||||
self.local_transform_state[local_idx]["scale"] = final_scale
|
||||
self.position_offsets[local_idx] = final_offset
|
||||
|
||||
pivot_np = np.array([pivot.x, pivot.y, pivot.z], dtype=np.float32)
|
||||
base_s = np.array([base_scale.x, base_scale.y, base_scale.z], dtype=np.float32)
|
||||
total_scale = base_s * dscale
|
||||
total_offset = np.array([base_offset.x, base_offset.y, base_offset.z], dtype=np.float32) + dpos
|
||||
|
||||
entries = local_data["entries"]
|
||||
base_positions = local_data["base_positions"]
|
||||
for i, (gn_np, gi, rows) in enumerate(entries):
|
||||
if i >= len(base_positions):
|
||||
continue
|
||||
orig_pos = base_positions[i]
|
||||
if orig_pos is None or len(orig_pos) == 0:
|
||||
continue
|
||||
centered = orig_pos - pivot_np
|
||||
scaled = centered * total_scale
|
||||
rotated = scaled @ rot_mat.T
|
||||
new_pos = rotated + pivot_np + total_offset
|
||||
|
||||
gnode = gn_np.node()
|
||||
geom = gnode.modify_geom(gi)
|
||||
vdata = geom.modify_vertex_data()
|
||||
writer = GeomVertexWriter(vdata, "vertex")
|
||||
|
||||
for j in range(len(rows)):
|
||||
writer.set_row(int(rows[j]))
|
||||
writer.set_data3f(float(new_pos[j, 0]), float(new_pos[j, 1]), float(new_pos[j, 2]))
|
||||
|
||||
def _quat_to_np_mat3(self, quat):
|
||||
"""Convert Panda3D Quat to 3x3 numpy rotation matrix."""
|
||||
import numpy as np
|
||||
q = Quat(quat)
|
||||
q.normalize()
|
||||
w = float(q.getR())
|
||||
x = float(q.getI())
|
||||
y = float(q.getJ())
|
||||
z = float(q.getK())
|
||||
|
||||
xx = x * x
|
||||
yy = y * y
|
||||
zz = z * z
|
||||
xy = x * y
|
||||
xz = x * z
|
||||
yz = y * z
|
||||
wx = w * x
|
||||
wy = w * y
|
||||
wz = w * z
|
||||
|
||||
return np.array([
|
||||
[1.0 - 2.0 * (yy + zz), 2.0 * (xy - wz), 2.0 * (xz + wy)],
|
||||
[2.0 * (xy + wz), 1.0 - 2.0 * (xx + zz), 2.0 * (yz - wx)],
|
||||
[2.0 * (xz - wy), 2.0 * (yz + wx), 1.0 - 2.0 * (xx + yy)],
|
||||
], dtype=np.float32)
|
||||
|
||||
def create_ssbo(self):
|
||||
"""No SSBO needed — using RP default rendering."""
|
||||
return None
|
||||
|
||||
def move_object(self, global_id, delta):
|
||||
"""
|
||||
Move an object by modifying vertex positions directly.
|
||||
delta: Vec3 translation to apply.
|
||||
Uses numpy for batch vertex updates.
|
||||
"""
|
||||
import numpy as np
|
||||
|
||||
if global_id not in self.id_to_chunk:
|
||||
return
|
||||
|
||||
_, local_idx = self.id_to_chunk[global_id]
|
||||
|
||||
if local_idx not in self.vertex_index:
|
||||
return
|
||||
|
||||
# Accumulate offset
|
||||
self.position_offsets[local_idx] = self.position_offsets.get(local_idx, Vec3(0)) + delta
|
||||
offset = self.position_offsets[local_idx]
|
||||
offset_arr = np.array([offset.x, offset.y, offset.z], dtype=np.float32)
|
||||
|
||||
# Update each (geom_node, geom_idx, rows) group
|
||||
entries = self.vertex_index[local_idx]
|
||||
originals = self.original_positions[local_idx]
|
||||
|
||||
for i, (gn_np, gi, rows) in enumerate(entries):
|
||||
orig_pos = originals[i] # numpy array (N, 3)
|
||||
new_pos = orig_pos + offset_arr # vectorized add
|
||||
|
||||
gnode = gn_np.node()
|
||||
geom = gnode.modify_geom(gi)
|
||||
vdata = geom.modify_vertex_data()
|
||||
writer = GeomVertexWriter(vdata, "vertex")
|
||||
|
||||
for j in range(len(rows)):
|
||||
writer.set_row(int(rows[j]))
|
||||
writer.set_data3f(float(new_pos[j, 0]), float(new_pos[j, 1]), float(new_pos[j, 2]))
|
||||
|
||||
def get_world_pos(self, global_id):
|
||||
"""Get current world position of an object."""
|
||||
if global_id not in self.id_to_chunk:
|
||||
return Vec3(0, 0, 0)
|
||||
_, local_idx = self.id_to_chunk[global_id]
|
||||
|
||||
original_mat = self.global_transforms[global_id]
|
||||
original_pos = original_mat.get_row3(3)
|
||||
offset = self.position_offsets.get(local_idx, Vec3(0))
|
||||
|
||||
return Vec3(original_pos) + offset
|
||||
|
||||
def get_object_center(self, global_id):
|
||||
"""Get the original center position of an object (for rotation pivot)."""
|
||||
if global_id >= len(self.global_transforms):
|
||||
return Vec3(0, 0, 0)
|
||||
mat = self.global_transforms[global_id]
|
||||
return Vec3(mat.get_row3(3))
|
||||
|
||||
def get_transform(self, global_id):
|
||||
"""Get original transform."""
|
||||
if global_id >= len(self.global_transforms):
|
||||
return LMatrix4f.ident_mat()
|
||||
return self.global_transforms[global_id]
|
||||
|
||||
@property
|
||||
def transforms(self):
|
||||
return self.global_transforms
|
||||
617
ssbo_component/ssbo_editor.py
Normal file
@ -0,0 +1,617 @@
|
||||
|
||||
import sys
|
||||
import os
|
||||
import struct
|
||||
import time
|
||||
from panda3d.core import (
|
||||
Filename, loadPrcFileData, GeomVertexFormat,
|
||||
GeomVertexWriter, InternalName, Shader, Texture, SamplerState,
|
||||
Vec3, Vec4, Point2, Point3, LMatrix4f, ShaderBuffer, GeomEnums, OmniBoundingVolume, Quat,
|
||||
TransparencyAttrib, BoundingSphere, NodePath,
|
||||
GraphicsEngine, WindowProperties, FrameBufferProperties,
|
||||
GraphicsPipe, GraphicsOutput, Camera, DisplayRegion, OrthographicLens,
|
||||
BoundingBox
|
||||
)
|
||||
|
||||
import p3dimgui.backend as p3dimgui_backend
|
||||
import p3dimgui.shaders as p3dimgui_shaders
|
||||
from imgui_bundle import imgui
|
||||
from rpcore.effect import Effect
|
||||
|
||||
# Work around p3dimgui import-order issue where backend may import an unrelated
|
||||
# top-level "shaders" module and miss these globals.
|
||||
if not hasattr(p3dimgui_backend, "VERT_SHADER"):
|
||||
p3dimgui_backend.VERT_SHADER = p3dimgui_shaders.VERT_SHADER
|
||||
if not hasattr(p3dimgui_backend, "FRAG_SHADER"):
|
||||
p3dimgui_backend.FRAG_SHADER = p3dimgui_shaders.FRAG_SHADER
|
||||
|
||||
ImGuiBackend = p3dimgui_backend.ImGuiBackend
|
||||
|
||||
from .ssbo_controller import ObjectController
|
||||
|
||||
class SSBOEditor:
|
||||
"""
|
||||
SSBO Editor Component
|
||||
====================
|
||||
Encapsulates the SSBO rendering, ImGui editor, and interaction logic.
|
||||
Can be integrated into any ShowBase application using RenderPipeline.
|
||||
"""
|
||||
|
||||
def __init__(self, base_app, render_pipeline, model_path=None, font_path=None):
|
||||
self.base = base_app
|
||||
self.rp = render_pipeline
|
||||
self.controller = None
|
||||
self.model = None
|
||||
self.ssbo = None
|
||||
self.font_path = font_path
|
||||
# Picking resources may be created later when a model is loaded.
|
||||
self.pick_buffer = None
|
||||
self.pick_texture = None
|
||||
self.pick_cam = None
|
||||
self.pick_cam_np = None
|
||||
self.pick_lens = None
|
||||
|
||||
# Internal State
|
||||
self.selected_name = None
|
||||
self.selected_ids = []
|
||||
self.search_text = ""
|
||||
self.last_search_text = None
|
||||
self.filtered_nodes = []
|
||||
self.debug_mode = False
|
||||
self.keys = {}
|
||||
self._ssbo_transform_active = False
|
||||
self._ssbo_selected_local_indices = []
|
||||
self._ssbo_transform_snapshot = None
|
||||
self._ssbo_gizmo_proxy = None
|
||||
self._ssbo_proxy_start = {"pos": None, "quat": None, "scale": None}
|
||||
self._bound_transform_gizmo = None
|
||||
|
||||
# Initialize ImGui Backend if not already present
|
||||
if not hasattr(self.base, 'imgui_backend'):
|
||||
print("[SSBOEditor] Initializing ImGui Backend...")
|
||||
self.base.imgui_backend = ImGuiBackend()
|
||||
|
||||
self.load_font()
|
||||
|
||||
# Register Events
|
||||
self.base.accept("imgui-new-frame", self.draw_imgui)
|
||||
self.base.accept("f", self.focus_on_selected)
|
||||
self.base.accept("d", self.toggle_debug)
|
||||
self.base.accept("mouse1", self.on_mouse_click)
|
||||
|
||||
# Register Input Tasks
|
||||
for key in ['arrow_up', 'arrow_down', 'arrow_left', 'arrow_right', 'z', 'x']:
|
||||
self.base.accept(key, self.keys.__setitem__, [key, True])
|
||||
self.base.accept(f"{key}-up", self.keys.__setitem__, [key, False])
|
||||
|
||||
# Add Tasks
|
||||
self.base.taskMgr.add(self.update_task, "update_task")
|
||||
|
||||
# Load Model if provided
|
||||
if model_path:
|
||||
self.load_model(model_path)
|
||||
|
||||
def load_font(self):
|
||||
"""Load custom font for ImGui"""
|
||||
io = imgui.get_io()
|
||||
|
||||
# Load Chinese Glyph Ranges
|
||||
glyph_ranges = None
|
||||
try:
|
||||
if hasattr(io.fonts, 'get_glyph_ranges_chinese_full'):
|
||||
glyph_ranges = io.fonts.get_glyph_ranges_chinese_full()
|
||||
elif hasattr(io.fonts, 'get_glyph_ranges_chinese_simplified_common'):
|
||||
glyph_ranges = io.fonts.get_glyph_ranges_chinese_simplified_common()
|
||||
except Exception as e:
|
||||
print(f"[SSBOEditor] Warning: Could not get Chinese glyph ranges: {e}")
|
||||
|
||||
try:
|
||||
if self.font_path and os.path.exists(self.font_path):
|
||||
io.fonts.clear()
|
||||
# If glyph_ranges is None, it uses default (Basic Latin)
|
||||
if glyph_ranges:
|
||||
io.fonts.add_font_from_file_ttf(self.font_path, 18.0, glyph_ranges=glyph_ranges)
|
||||
else:
|
||||
io.fonts.add_font_from_file_ttf(self.font_path, 18.0)
|
||||
else:
|
||||
# Fallback to default or common font
|
||||
default_font = os.path.join(os.path.dirname(os.path.dirname(__file__)), "font", "msyh.ttc")
|
||||
if os.path.exists(default_font):
|
||||
io.fonts.clear()
|
||||
io.fonts.add_font_from_file_ttf(default_font, 18.0, glyph_ranges=glyph_ranges)
|
||||
else:
|
||||
io.fonts.clear()
|
||||
io.fonts.add_font_default()
|
||||
except Exception as e:
|
||||
print(f"[SSBOEditor] Font load error: {e}")
|
||||
io.fonts.clear()
|
||||
io.fonts.add_font_default()
|
||||
|
||||
def load_model(self, model_path):
|
||||
"""Load and process a model — NO custom shader, uses RP default rendering."""
|
||||
print(f"[SSBOEditor] Loading model: {model_path}")
|
||||
fn = Filename.fromOsSpecific(model_path)
|
||||
self.model = self.base.loader.loadModel(fn)
|
||||
|
||||
self.controller = ObjectController()
|
||||
count = self.controller.bake_ids_and_collect(self.model)
|
||||
self._ssbo_transform_active = False
|
||||
self._ssbo_selected_local_indices = []
|
||||
self._ssbo_transform_snapshot = None
|
||||
self._cleanup_ssbo_proxy()
|
||||
|
||||
self.model.reparent_to(self.base.render)
|
||||
|
||||
# NO rp.set_effect() — use RP default rendering for max FPS
|
||||
# NO SSBO creation — vertex positions are baked
|
||||
|
||||
# Setup GPU Picking (uses simple vertex-color shader)
|
||||
self.setup_gpu_picking()
|
||||
|
||||
print(f"[SSBOEditor] Model loaded. Total objects: {count}")
|
||||
|
||||
# No custom effect needed — RP default rendering for maximum FPS
|
||||
|
||||
def _inject_ssbo_into_shadow_state(self, effect_path):
|
||||
"""Inject SSBO inputs into RP shadow tag state"""
|
||||
try:
|
||||
if not hasattr(self.rp.tag_mgr, 'containers'): return
|
||||
|
||||
shadow_container = self.rp.tag_mgr.containers.get("shadow")
|
||||
if not shadow_container: return
|
||||
|
||||
tag_value = self.model.get_tag(shadow_container.tag_name)
|
||||
if not tag_value: return
|
||||
|
||||
effect = Effect.load(effect_path, {})
|
||||
if effect is None: return
|
||||
|
||||
shadow_shader = effect.get_shader_obj("shadow")
|
||||
if shadow_shader is None: return
|
||||
|
||||
# Since inputs are now on Nodes (Chunks), we just need to ensure the shader is applied.
|
||||
# extra_inputs is no longer needed if the inputs are on the nodes themselves?
|
||||
# Wait, RP might override state.
|
||||
# But specific shader inputs on NodePath have priority over State inputs usually?
|
||||
# Let's try applying without extra inputs first.
|
||||
|
||||
self.rp.tag_mgr.apply_state(
|
||||
"shadow", self.model, shadow_shader,
|
||||
tag_value, 65)
|
||||
|
||||
print(f"[SSBO Shadow] Re-applied shadow state (tag='{tag_value}')")
|
||||
except Exception as e:
|
||||
print(f"[SSBO Shadow] Error injecting shadow state: {e}")
|
||||
|
||||
def setup_gpu_picking(self):
|
||||
"""Setup GPU Picking (Basic implementation)"""
|
||||
# ... (Buffer setup code remains same) ...
|
||||
win_props = WindowProperties()
|
||||
win_props.set_size(1, 1)
|
||||
fb_props = FrameBufferProperties()
|
||||
fb_props.set_rgba_bits(8, 8, 8, 8)
|
||||
fb_props.set_depth_bits(16)
|
||||
|
||||
self.pick_buffer = self.base.graphicsEngine.make_output(
|
||||
self.base.pipe, "pick_buffer", -100,
|
||||
fb_props, win_props,
|
||||
GraphicsPipe.BF_refuse_window,
|
||||
self.base.win.get_gsg(), self.base.win
|
||||
)
|
||||
|
||||
if not self.pick_buffer:
|
||||
print("[GPU Picking] Failed to create buffer!")
|
||||
return
|
||||
|
||||
self.pick_texture = Texture()
|
||||
self.pick_texture.set_minfilter(Texture.FT_nearest)
|
||||
self.pick_texture.set_magfilter(Texture.FT_nearest)
|
||||
self.pick_buffer.add_render_texture(self.pick_texture, GraphicsOutput.RTM_copy_ram)
|
||||
|
||||
self.pick_cam = Camera("pick_camera")
|
||||
self.pick_cam_np = self.base.cam.attach_new_node(self.pick_cam)
|
||||
self.pick_lens = self.base.camLens.make_copy()
|
||||
self.pick_cam.set_lens(self.pick_lens)
|
||||
|
||||
dr = self.pick_buffer.make_display_region()
|
||||
dr.set_camera(self.pick_cam_np)
|
||||
|
||||
# Load pick shader
|
||||
current_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
pick_vert = os.path.join(current_dir, "shaders", "pick_id.vert")
|
||||
pick_frag = os.path.join(current_dir, "shaders", "pick_id.frag")
|
||||
|
||||
pick_vert = Filename.fromOsSpecific(pick_vert).getFullpath()
|
||||
pick_frag = Filename.fromOsSpecific(pick_frag).getFullpath()
|
||||
|
||||
try:
|
||||
pick_shader = Shader.load(
|
||||
Shader.SL_GLSL,
|
||||
pick_vert,
|
||||
pick_frag
|
||||
)
|
||||
self.pick_cam.set_scene(self.model)
|
||||
initial_state = NodePath("initial")
|
||||
initial_state.set_shader(pick_shader, 100)
|
||||
# Remove global SSBO input, Chunks have their own inputs
|
||||
# initial_state.set_shader_input("transforms", ssbo)
|
||||
self.pick_cam.set_initial_state(initial_state.get_state())
|
||||
except Exception as e:
|
||||
print(f"[GPU Picking] Warning: pick shaders failed to load: {e}")
|
||||
print("Picking disabled.")
|
||||
return
|
||||
|
||||
self.pick_buffer.set_active(False)
|
||||
self.pick_buffer.set_clear_color(Vec4(0, 0, 0, 0))
|
||||
self.pick_buffer.set_clear_color_active(True)
|
||||
|
||||
def pick_object(self, mx, my):
|
||||
if (not self.pick_buffer or not self.pick_texture or not self.pick_lens or
|
||||
not self.controller or not self.model):
|
||||
return False
|
||||
|
||||
self.pick_lens.set_fov(0.1)
|
||||
self.pick_lens.set_film_offset(0, 0)
|
||||
self.pick_cam.set_lens(self.pick_lens)
|
||||
|
||||
near_point = Point3()
|
||||
far_point = Point3()
|
||||
self.base.camLens.extrude(Point2(mx, my), near_point, far_point)
|
||||
|
||||
self.pick_cam_np.set_pos(0, 0, 0)
|
||||
self.pick_cam_np.look_at(far_point)
|
||||
|
||||
self.pick_buffer.set_active(True)
|
||||
self.base.graphicsEngine.render_frame()
|
||||
self.pick_buffer.set_active(False)
|
||||
self.base.graphicsEngine.extract_texture_data(
|
||||
self.pick_texture, self.base.win.get_gsg()
|
||||
)
|
||||
|
||||
ram_image = self.pick_texture.get_ram_image_as("RGBA")
|
||||
if ram_image:
|
||||
data = memoryview(ram_image)
|
||||
if len(data) >= 4:
|
||||
r, g, b, a = data[0], data[1], data[2], data[3]
|
||||
if a > 0:
|
||||
hit_id = r + (g << 8)
|
||||
node_key = self.controller.id_to_name.get(hit_id)
|
||||
if node_key:
|
||||
print(f"[Pick] Hit: ID={hit_id} -> {node_key}")
|
||||
self.select_node(node_key)
|
||||
return True
|
||||
|
||||
self.selected_name = None
|
||||
self.selected_ids = []
|
||||
return False
|
||||
|
||||
|
||||
def on_mouse_click(self):
|
||||
io = imgui.get_io()
|
||||
if io.want_capture_mouse:
|
||||
return
|
||||
if self.base.mouseWatcherNode.has_mouse():
|
||||
mpos = self.base.mouseWatcherNode.get_mouse()
|
||||
# If clicking gizmo, skip SSBO pick.
|
||||
if self._try_start_gizmo_drag(mpos.x, mpos.y):
|
||||
return
|
||||
prev_selected = self.selected_name
|
||||
hit = self.pick_object(mpos.x, mpos.y)
|
||||
# SSBO miss must clear current selection.
|
||||
if not hit:
|
||||
self._sync_selection_none()
|
||||
# Always fallback to legacy ray pick when SSBO misses.
|
||||
# This keeps scene selection usable if SSBO ID mapping is incomplete.
|
||||
self._fallback_legacy_pick(mpos.x, mpos.y)
|
||||
elif prev_selected != self.selected_name:
|
||||
# Ensure selection visuals refresh when SSBO selection changes.
|
||||
self._sync_selection_from_key(self.selected_name)
|
||||
|
||||
def toggle_debug(self):
|
||||
self.debug_mode = not self.debug_mode
|
||||
|
||||
def clear_selection(self):
|
||||
pass # No selection mask texture needed without custom shader
|
||||
|
||||
def update_selection_mask(self):
|
||||
pass # No selection mask texture needed without custom shader
|
||||
|
||||
def select_node(self, key):
|
||||
if not self.controller or key not in self.controller.name_to_ids:
|
||||
return
|
||||
self.selected_name = key
|
||||
self.selected_ids = self.controller.name_to_ids.get(key, [])
|
||||
self._sync_selection_from_key(key)
|
||||
|
||||
def _sync_selection_from_key(self, key):
|
||||
"""Sync SSBO picked key to legacy SelectionSystem."""
|
||||
try:
|
||||
if hasattr(self.base, "selection") and self.base.selection:
|
||||
kind, target = self._resolve_ssbo_selection_target(key)
|
||||
if kind == "proxy":
|
||||
target_np = target
|
||||
else:
|
||||
target_np = target if target is not None else self.model
|
||||
if target_np is None or target_np.isEmpty():
|
||||
target_np = self.model
|
||||
self.base.selection.updateSelection(target_np)
|
||||
except Exception as e:
|
||||
print(f"[SSBOEditor] selection sync failed: {e}")
|
||||
|
||||
def _sync_selection_none(self):
|
||||
"""Clear legacy SelectionSystem selection."""
|
||||
try:
|
||||
self._ssbo_transform_active = False
|
||||
self._ssbo_selected_local_indices = []
|
||||
self._ssbo_transform_snapshot = None
|
||||
self._cleanup_ssbo_proxy()
|
||||
if hasattr(self.base, "selection") and self.base.selection:
|
||||
self.base.selection.updateSelection(None)
|
||||
except Exception as e:
|
||||
print(f"[SSBOEditor] clear selection sync failed: {e}")
|
||||
|
||||
def bind_transform_gizmo(self, transform_gizmo):
|
||||
"""Bind TransformGizmo drag hooks so SSBO sub-object transforms can follow gizmo."""
|
||||
self._bound_transform_gizmo = transform_gizmo
|
||||
if not transform_gizmo:
|
||||
return
|
||||
hooks = {
|
||||
"move": {
|
||||
"drag_start": [self._on_ssbo_gizmo_drag_start],
|
||||
"drag_move": [self._on_ssbo_gizmo_drag_move],
|
||||
"drag_end": [self._on_ssbo_gizmo_drag_end],
|
||||
},
|
||||
"rotate": {
|
||||
"drag_start": [self._on_ssbo_gizmo_drag_start],
|
||||
"drag_move": [self._on_ssbo_gizmo_drag_move],
|
||||
"drag_end": [self._on_ssbo_gizmo_drag_end],
|
||||
},
|
||||
"scale": {
|
||||
"drag_start": [self._on_ssbo_gizmo_drag_start],
|
||||
"drag_move": [self._on_ssbo_gizmo_drag_move],
|
||||
"drag_end": [self._on_ssbo_gizmo_drag_end],
|
||||
},
|
||||
}
|
||||
try:
|
||||
if hasattr(transform_gizmo, "set_event_hooks"):
|
||||
transform_gizmo.set_event_hooks(hooks, replace=False)
|
||||
print("[SSBOEditor] TransformGizmo hooks bound")
|
||||
except Exception as e:
|
||||
print(f"[SSBOEditor] bind transform gizmo failed: {e}")
|
||||
|
||||
def _resolve_ssbo_selection_target(self, key):
|
||||
"""Resolve selected SSBO key to proxy node (preferred) or regular node."""
|
||||
self._ssbo_transform_active = False
|
||||
self._ssbo_transform_snapshot = None
|
||||
self._ssbo_selected_local_indices = []
|
||||
|
||||
if not self.controller or not key:
|
||||
return "node", self.model
|
||||
global_ids = self.controller.name_to_ids.get(key, [])
|
||||
local_indices = self.controller.get_local_indices_from_global_ids(global_ids)
|
||||
self._ssbo_selected_local_indices = local_indices
|
||||
if local_indices:
|
||||
print(f"[SSBOEditor] selection locals={len(local_indices)} key={key}")
|
||||
center = self.controller.get_selection_center(local_indices)
|
||||
proxy = self._ensure_ssbo_proxy(center)
|
||||
return "proxy", proxy
|
||||
target_np = self.controller.key_to_node.get(key)
|
||||
if target_np is None or target_np.isEmpty():
|
||||
target_np = self.model
|
||||
return "node", target_np
|
||||
|
||||
def _ensure_ssbo_proxy(self, center):
|
||||
if self._ssbo_gizmo_proxy is None or self._ssbo_gizmo_proxy.isEmpty():
|
||||
self._ssbo_gizmo_proxy = self.base.render.attach_new_node("ssbo_transform_proxy")
|
||||
self._ssbo_gizmo_proxy.setTag("is_ssbo_proxy", "1")
|
||||
self._ssbo_gizmo_proxy.set_pos(center)
|
||||
self._ssbo_gizmo_proxy.set_hpr(0, 0, 0)
|
||||
self._ssbo_gizmo_proxy.set_scale(1, 1, 1)
|
||||
return self._ssbo_gizmo_proxy
|
||||
|
||||
def _cleanup_ssbo_proxy(self):
|
||||
if self._ssbo_gizmo_proxy and not self._ssbo_gizmo_proxy.isEmpty():
|
||||
self._ssbo_gizmo_proxy.removeNode()
|
||||
self._ssbo_gizmo_proxy = None
|
||||
|
||||
def _on_ssbo_gizmo_drag_start(self, payload):
|
||||
try:
|
||||
target = payload.get("target") if payload else None
|
||||
if not target or target != self._ssbo_gizmo_proxy:
|
||||
self._ssbo_transform_active = False
|
||||
return
|
||||
if not self.controller or not self._ssbo_selected_local_indices:
|
||||
self._ssbo_transform_active = False
|
||||
return
|
||||
self._ssbo_transform_snapshot = self.controller.begin_transform_session(
|
||||
self._ssbo_selected_local_indices
|
||||
)
|
||||
self._ssbo_proxy_start = {
|
||||
"pos": Vec3(target.getPos(self.base.render)),
|
||||
"quat": Quat(target.getQuat(self.base.render)),
|
||||
"scale": Vec3(target.getScale()),
|
||||
}
|
||||
self._ssbo_transform_active = True
|
||||
print(f"[SSBOEditor] drag_start locals={len(self._ssbo_selected_local_indices)}")
|
||||
except Exception as e:
|
||||
self._ssbo_transform_active = False
|
||||
print(f"[SSBOEditor] drag_start bridge failed: {e}")
|
||||
|
||||
def _on_ssbo_gizmo_drag_move(self, payload):
|
||||
try:
|
||||
if not self._ssbo_transform_active:
|
||||
return
|
||||
target = payload.get("target") if payload else None
|
||||
if not target or target != self._ssbo_gizmo_proxy:
|
||||
return
|
||||
start_pos = self._ssbo_proxy_start.get("pos")
|
||||
start_quat = self._ssbo_proxy_start.get("quat")
|
||||
start_scale = self._ssbo_proxy_start.get("scale")
|
||||
if start_pos is None or start_quat is None or start_scale is None:
|
||||
return
|
||||
|
||||
curr_pos = Vec3(target.getPos(self.base.render))
|
||||
curr_quat = Quat(target.getQuat(self.base.render))
|
||||
curr_scale = Vec3(target.getScale())
|
||||
|
||||
delta_pos = curr_pos - start_pos
|
||||
inv_start_quat = Quat(start_quat)
|
||||
inv_start_quat.invertInPlace()
|
||||
delta_quat = curr_quat * inv_start_quat
|
||||
delta_scale = Vec3(
|
||||
curr_scale.x / start_scale.x if abs(start_scale.x) > 1e-8 else 1.0,
|
||||
curr_scale.y / start_scale.y if abs(start_scale.y) > 1e-8 else 1.0,
|
||||
curr_scale.z / start_scale.z if abs(start_scale.z) > 1e-8 else 1.0,
|
||||
)
|
||||
|
||||
self.controller.apply_transform_session(
|
||||
self._ssbo_transform_snapshot,
|
||||
delta_pos,
|
||||
delta_quat,
|
||||
delta_scale,
|
||||
)
|
||||
except Exception as e:
|
||||
print(f"[SSBOEditor] drag_move bridge failed: {e}")
|
||||
|
||||
def _on_ssbo_gizmo_drag_end(self, payload):
|
||||
try:
|
||||
if self._ssbo_transform_active:
|
||||
print(f"[SSBOEditor] drag_end locals={len(self._ssbo_selected_local_indices)}")
|
||||
self._ssbo_transform_active = False
|
||||
self._ssbo_transform_snapshot = None
|
||||
except Exception as e:
|
||||
print(f"[SSBOEditor] drag_end bridge failed: {e}")
|
||||
|
||||
def _fallback_legacy_pick(self, mx, my):
|
||||
"""Fallback to legacy ray picking when SSBO misses."""
|
||||
try:
|
||||
if not hasattr(self.base, "event_handler") or not self.base.event_handler:
|
||||
return
|
||||
win_w, win_h = self.base.win.getSize()
|
||||
x = (mx + 1.0) * 0.5 * win_w
|
||||
y = (1.0 - my) * 0.5 * win_h
|
||||
self.base.event_handler.mousePressEventLeft({"x": x, "y": y})
|
||||
except Exception as e:
|
||||
print(f"[SSBOEditor] legacy fallback pick failed: {e}")
|
||||
|
||||
def _try_start_gizmo_drag(self, mouse_x=None, mouse_y=None):
|
||||
"""Try to start gizmo drag using the existing SelectionSystem pipeline."""
|
||||
try:
|
||||
new_transform = getattr(self.base, "newTransform", None)
|
||||
if (
|
||||
new_transform is not None and
|
||||
mouse_x is not None and
|
||||
mouse_y is not None and
|
||||
self._is_mouse_on_new_gizmo(new_transform, mouse_x, mouse_y)
|
||||
):
|
||||
return True
|
||||
selection = getattr(self.base, "selection", None)
|
||||
if not selection or not selection.gizmo:
|
||||
return False
|
||||
win_w, win_h = self.base.win.getSize()
|
||||
mpos = self.base.mouseWatcherNode.get_mouse()
|
||||
x = (mpos.x + 1.0) * 0.5 * win_w
|
||||
y = (1.0 - mpos.y) * 0.5 * win_h
|
||||
|
||||
axis = selection.gizmoHighlightAxis or selection.checkGizmoClick(x, y)
|
||||
if axis:
|
||||
selection.startGizmoDrag(axis, x, y)
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"[SSBOEditor] gizmo drag start failed: {e}")
|
||||
return False
|
||||
|
||||
def _is_mouse_on_new_gizmo(self, new_transform, mouse_x, mouse_y):
|
||||
"""Refresh and query hover state for TransformGizmo on current click position."""
|
||||
try:
|
||||
mouse_pos = Point3(mouse_x, mouse_y, 0.0)
|
||||
for gizmo_name in ("move_gizmo", "rotate_gizmo", "scale_gizmo"):
|
||||
gizmo = getattr(new_transform, gizmo_name, None)
|
||||
if not gizmo or not getattr(gizmo, "attached", False):
|
||||
continue
|
||||
hover_updater = getattr(gizmo, "_update_hover_highlight", None)
|
||||
if callable(hover_updater):
|
||||
hover_updater(mouse_pos)
|
||||
return bool(getattr(new_transform, "is_hovering", False))
|
||||
except Exception as e:
|
||||
print(f"[SSBOEditor] new gizmo hover check failed: {e}")
|
||||
return False
|
||||
|
||||
def focus_on_selected(self):
|
||||
if self.selected_name and self.selected_ids:
|
||||
first_id = self.selected_ids[0]
|
||||
pos = self.controller.get_world_pos(first_id)
|
||||
dist = 100
|
||||
self.base.camera.set_pos(pos.x, pos.y - dist, pos.z + dist * 0.5)
|
||||
self.base.camera.look_at(pos)
|
||||
|
||||
def draw_imgui(self):
|
||||
if not self.controller: return
|
||||
|
||||
imgui.set_next_window_pos((10, 10), imgui.Cond_.first_use_ever)
|
||||
imgui.set_next_window_size((350, 600), imgui.Cond_.first_use_ever)
|
||||
|
||||
expanded, opened = imgui.begin("Scene Tree (Component)")
|
||||
if expanded:
|
||||
imgui.text(f"FPS: {globalClock.getAverageFrameRate():.1f}")
|
||||
imgui.separator()
|
||||
|
||||
changed, self.search_text = imgui.input_text("Search", self.search_text, 256)
|
||||
|
||||
if imgui.begin_child("ObjectList", (0, 380), child_flags=imgui.ChildFlags_.borders):
|
||||
if self.search_text != self.last_search_text:
|
||||
self.last_search_text = self.search_text
|
||||
search_lower = self.search_text.lower()
|
||||
self.filtered_nodes = []
|
||||
for key in self.controller.node_list:
|
||||
display = self.controller.display_names.get(key, key.split('/')[-1])
|
||||
if not search_lower or (search_lower in display.lower() or search_lower in key.lower()):
|
||||
geom_count = len(self.controller.name_to_ids.get(key, []))
|
||||
self.filtered_nodes.append((key, display, geom_count))
|
||||
|
||||
# If list is empty initially (no search), show all
|
||||
if not self.search_text and not self.filtered_nodes:
|
||||
if len(self.filtered_nodes) != len(self.controller.node_list):
|
||||
self.filtered_nodes = [(k, self.controller.display_names.get(k, k), len(self.controller.name_to_ids.get(k,[]))) for k in self.controller.node_list]
|
||||
|
||||
count = len(self.filtered_nodes)
|
||||
clipper = imgui.ListClipper()
|
||||
clipper.begin(count)
|
||||
while clipper.step():
|
||||
for i in range(clipper.display_start, clipper.display_end):
|
||||
key, display, geom_count = self.filtered_nodes[i]
|
||||
label = f"{display} ({geom_count})"
|
||||
is_selected = (key == self.selected_name)
|
||||
if imgui.selectable(label, is_selected)[0]:
|
||||
self.select_node(key)
|
||||
imgui.end_child()
|
||||
|
||||
imgui.separator()
|
||||
if self.selected_name:
|
||||
imgui.text_colored((1, 0.8, 0.2, 1), f"Selected: {self.selected_name}")
|
||||
if imgui.button("Focus (F)"): self.focus_on_selected()
|
||||
imgui.end()
|
||||
|
||||
# swap_transforms_task removed - motion blur disabled for performance
|
||||
|
||||
def update_task(self, task):
|
||||
dt = globalClock.getDt()
|
||||
io = imgui.get_io()
|
||||
|
||||
if io.want_capture_keyboard: return task.cont
|
||||
|
||||
if self.selected_ids and self.controller:
|
||||
speed = 50 * dt
|
||||
acc = Vec3(0, 0, 0)
|
||||
if self.keys.get('arrow_up'): acc.z += speed
|
||||
if self.keys.get('arrow_down'): acc.z -= speed
|
||||
if self.keys.get('arrow_left'): acc.x -= speed
|
||||
if self.keys.get('arrow_right'): acc.x += speed
|
||||
if self.keys.get('z'): acc.y += speed
|
||||
if self.keys.get('x'): acc.y -= speed
|
||||
|
||||
if acc.length_squared() > 0:
|
||||
for idx in self.selected_ids:
|
||||
self.controller.move_object(idx, acc)
|
||||
|
||||
return task.cont
|
||||
339
ui/Builtin/Elements.py
Normal file
@ -0,0 +1,339 @@
|
||||
"""
|
||||
|
||||
|
||||
|
||||
OUTDATED
|
||||
|
||||
Do not use anymore.
|
||||
|
||||
|
||||
"""
|
||||
|
||||
|
||||
import colorsys
|
||||
|
||||
from LUIObject import LUIObject
|
||||
from LUISlider import LUISlider
|
||||
from LUISprite import LUISprite
|
||||
from LUIVerticalLayout import LUIVerticalLayout
|
||||
from LUICallback import LUICallback
|
||||
from LUILabel import LUILabel
|
||||
from LUIFrame import LUIFrame
|
||||
from LUIButton import LUIButton
|
||||
|
||||
class LUISliderWithLabel(LUIObject, LUICallback):
|
||||
|
||||
def __init__(self, parent=None, width=100.0, filled=False, min_value=0, max_value=1.0, precision=2, value=None):
|
||||
LUIObject.__init__(self, x=0, y=0, w=width, h=0)
|
||||
LUICallback.__init__(self)
|
||||
|
||||
max_numbers_before = max(len(str(int(max_value))), len(str(int(min_value))))
|
||||
number_space_required = max_numbers_before
|
||||
|
||||
if precision > 0:
|
||||
number_space_required += 1 + precision
|
||||
|
||||
pixels_per_number = 7
|
||||
self.precision = precision
|
||||
|
||||
self.slider = LUISlider(self, width=width - pixels_per_number * number_space_required - 5, filled=filled, min_value=min_value, max_value=max_value, value=value)
|
||||
self.label = LUILabel(parent=self, shadow=True, text=u"1.23")
|
||||
self.label.right = 0
|
||||
self.label.top = self.label.height - self.slider.height
|
||||
self.label.color = (1,1,1,0.5)
|
||||
|
||||
self.slider.add_change_callback(self._on_slider_changed)
|
||||
self.slider.add_change_callback(self._trigger_callback)
|
||||
self._on_slider_changed(self.slider, self.slider.get_value())
|
||||
|
||||
if parent is not None:
|
||||
self.parent = parent
|
||||
|
||||
self.fit_to_children()
|
||||
|
||||
def get_value(self):
|
||||
return self.slider.get_value()
|
||||
|
||||
def set_value(self, val):
|
||||
self.slider.set_value(val)
|
||||
|
||||
def _on_slider_changed(self, obj, value):
|
||||
self.label.text = ("{:." + str(self.precision) + "f}").format(value)
|
||||
|
||||
class LUIKeyMarker(LUIObject):
|
||||
|
||||
def __init__(self, parent=None, key=u"A"):
|
||||
LUIObject.__init__(self)
|
||||
self.bgLeft = LUISprite(self, "Keymarker_Left", "skin")
|
||||
self.bgMid = LUISprite(self, "Keymarker", "skin")
|
||||
self.bgRight = LUISprite(self, "Keymarker_Right", "skin")
|
||||
|
||||
self.label = LUILabel(parent=self, text=key, shadow=True)
|
||||
self.label.centered = (True, True)
|
||||
self.label.margin = (-3, 0, 0, -1)
|
||||
self.margin = (-1, 0, 0, -1)
|
||||
|
||||
self.set_key(key)
|
||||
|
||||
if parent is not None:
|
||||
self.parent = parent
|
||||
|
||||
self.fit_to_children()
|
||||
|
||||
def set_key(self, key):
|
||||
self.label.set_text(key)
|
||||
self.width = self.label.width + self.bgLeft.width + self.bgRight.width + 7
|
||||
self.bgMid.width = self.width - self.bgLeft.width - self.bgRight.width
|
||||
self.bgMid.left = self.bgLeft.width
|
||||
self.bgRight.left = self.bgMid.width + self.bgMid.left
|
||||
|
||||
self.fit_to_children()
|
||||
|
||||
class LUIKeyInstruction(LUIObject):
|
||||
|
||||
def __init__(self, parent=None, key=u"A", instruction=u"Instruction"):
|
||||
LUIObject.__init__(self)
|
||||
self.marker = LUIKeyMarker(parent=self, key=key)
|
||||
self.instructionLabel = LUILabel(parent=self, text=instruction, shadow=True)
|
||||
self.instructionLabel.centered = (False, True)
|
||||
self.instructionLabel.margin.top = -4
|
||||
self.set_key(key)
|
||||
|
||||
def set_key(self, key):
|
||||
self.marker.set_key(key)
|
||||
self.instructionLabel.left = self.marker.width + 5
|
||||
self.fit_to_children()
|
||||
|
||||
|
||||
class LUIColorpicker(LUIObject):
|
||||
|
||||
def __init__(self, parent=None, color=None):
|
||||
LUIObject.__init__(self, x=0, y=0, w=27, h=27)
|
||||
|
||||
self.previewBg = LUISprite(self, "ColorpickerPreviewBg", "skin")
|
||||
|
||||
self.filler = LUISprite(self, "blank", "skin")
|
||||
self.filler.width = 21
|
||||
self.filler.height = 21
|
||||
self.filler.pos = (5, 5)
|
||||
self.filler.color = (0.2,0.6,1.0,1.0)
|
||||
|
||||
self.overlay = LUISprite(self, "ColorpickerPreviewOverlay", "skin")
|
||||
self.overlay.pos = (2, 2)
|
||||
self.overlay.bind("click", self._open_dialog)
|
||||
|
||||
self.fit_to_children()
|
||||
|
||||
self.popup = LUIColorpickerPopup(self)
|
||||
self.popup.hide()
|
||||
|
||||
if color is not None:
|
||||
self.colorValue = color
|
||||
else:
|
||||
# My favourite color
|
||||
self.colorValue = (0.2, 0.6, 1.0)
|
||||
self.set_color_value(self.colorValue)
|
||||
|
||||
self.popup.add_change_callback(self._on_popup_color_changed)
|
||||
|
||||
if parent is not None:
|
||||
self.parent = parent
|
||||
|
||||
def _open_dialog(self, event):
|
||||
if self.has_focus():
|
||||
self.blur()
|
||||
else:
|
||||
self.request_focus()
|
||||
|
||||
def on_focus(self, event):
|
||||
self.popup._load_rgb(self.colorValue)
|
||||
self.popup.open_at(self, 14.0)
|
||||
|
||||
def set_color_value(self, rgb):
|
||||
self.colorValue = rgb
|
||||
self.filler.color = rgb
|
||||
|
||||
def get_color_value(self):
|
||||
return self.colorValue
|
||||
|
||||
def on_tick(self, event):
|
||||
self.popup._update(event)
|
||||
|
||||
def on_blur(self, event):
|
||||
self.popup.close()
|
||||
|
||||
def _on_popup_color_changed(self, popup, rgb):
|
||||
self.set_color_value(rgb)
|
||||
|
||||
def _on_popup_closed(self):
|
||||
self.blur()
|
||||
|
||||
|
||||
class LUIPopup(LUIFrame):
|
||||
|
||||
def __init__(self, parent=None, width=200, height=200):
|
||||
LUIFrame.__init__(self, parent=parent, width=width, height=height, padding=10, innerPadding=0)
|
||||
self.topmost = True
|
||||
self.borderSize = 33
|
||||
self.content.bind("click", self._on_content_click)
|
||||
|
||||
def open_at(self, targetElement, distance):
|
||||
self.show()
|
||||
|
||||
targetPos = targetElement.get_abs_pos()+ targetElement.get_size() / 2
|
||||
|
||||
showAbove = targetPos.y > self.height - self.borderSize
|
||||
showLeft = targetPos.x > self.width - self.borderSize
|
||||
|
||||
relative = self.get_relative_pos(targetPos)
|
||||
self.pos += relative
|
||||
|
||||
if showLeft:
|
||||
self.left -= self.width - self.borderSize
|
||||
self.left += 25
|
||||
else:
|
||||
self.left -= self.borderSize
|
||||
self.left -= 25
|
||||
|
||||
if showAbove:
|
||||
self.top -= distance
|
||||
self.top -= self.height - self.borderSize
|
||||
else:
|
||||
self.top += distance
|
||||
self.top -= self.borderSize
|
||||
|
||||
|
||||
def _on_content_click(self, event):
|
||||
pass
|
||||
|
||||
def close(self):
|
||||
self.hide()
|
||||
|
||||
class LUIColorpickerPopup(LUIPopup, LUICallback):
|
||||
def __init__(self, parent=None):
|
||||
LUIPopup.__init__(self, parent=parent, width=240, height=146)
|
||||
LUICallback.__init__(self)
|
||||
|
||||
self.field = LUIObject(self.content, x=0, y=0, w=128, h=128)
|
||||
|
||||
self.fieldBG = LUISprite(self.field, "blank", "skin")
|
||||
self.fieldBG.size = (128, 128)
|
||||
self.fieldBG.color = (0.2,0.6,1.0)
|
||||
self.fieldFG = LUISprite(self.field, "ColorpickerFieldOverlay", "skin")
|
||||
self.fieldFG.pos = (-2, 0)
|
||||
|
||||
self.fieldBG.bind("mousedown", self._start_field_dragging)
|
||||
self.fieldBG.bind("mouseup", self._stop_field_dragging)
|
||||
|
||||
self.fieldHandle = LUISprite(self.field, "ColorpickerFieldHandle", "skin")
|
||||
self.fieldHandle.bind("mousedown", self._start_field_dragging)
|
||||
self.fieldHandle.bind("mouseup", self._stop_field_dragging)
|
||||
|
||||
self.fieldDragging = False
|
||||
|
||||
self.hueSlider = LUIObject(self.content, x=140, y=0, w=40, h=128)
|
||||
self.hueSliderFG = LUISprite(self.hueSlider, "ColorpickerHueSlider", "skin")
|
||||
|
||||
self.hueHandle = LUISprite(self.hueSlider, "ColorpickerHueHandle", "skin")
|
||||
self.hueHandle.left = (self.hueSliderFG.width - self.hueHandle.width) / 2.0
|
||||
self.hueHandle.top = 50
|
||||
|
||||
self.hueDragging = False
|
||||
self.hueSlider.bind("mousedown", self._start_hue_dragging)
|
||||
self.hueSlider.bind("mouseup", self._stop_hue_dragging)
|
||||
|
||||
self.labels = LUIVerticalLayout(self.content, width=40)
|
||||
self.labels.pos = (177, 42)
|
||||
|
||||
colors = [u"R", u"G", u"B"]
|
||||
self.colorLabels = []
|
||||
|
||||
for color in colors:
|
||||
label = LUILabel(text=color, shadow=True)
|
||||
label.color = (1,1,1,0.3)
|
||||
|
||||
valueLabel = LUILabel(text=u"255", shadow=True)
|
||||
valueLabel.right = 0
|
||||
self.labels.add(label, valueLabel)
|
||||
self.colorLabels.append(valueLabel)
|
||||
|
||||
self.activeColor = LUIObject(self.content, x=177, y=0)
|
||||
self.activeColorBG = LUISprite(self.activeColor, "blank", "skin")
|
||||
self.activeColorFG = LUISprite(self.activeColor, "ColorpickerActiveColorOverlay", "skin")
|
||||
|
||||
self.activeColorBG.size = (40, 40)
|
||||
self.activeColorBG.pos = (2, 0)
|
||||
self.activeColorBG.color = (0.2,0.6,1.0,1.0)
|
||||
|
||||
self.closeButton = LUIButton(parent=self.content, text=u"Done", width=45, template="ButtonGreen")
|
||||
self.closeButton.left = 177
|
||||
self.closeButton.top = 98
|
||||
self.closeButton.bind("click", self._close_popup)
|
||||
|
||||
self._set_hue(0.5)
|
||||
self._set_sat_val(0.5, 0.5)
|
||||
|
||||
self.widget = parent
|
||||
|
||||
def _load_rgb(self, rgb):
|
||||
hsv = colorsys.rgb_to_hsv(*rgb)
|
||||
self._set_hue(hsv[0])
|
||||
self._set_sat_val(hsv[1], hsv[2])
|
||||
|
||||
def _close_popup(self, event):
|
||||
self.widget._on_popup_closed()
|
||||
self.close()
|
||||
|
||||
def _update(self, event):
|
||||
if self.hueDragging:
|
||||
offset = event.coordinates.y - self.hueSliderFG.abs_pos.y
|
||||
offset /= 128.0
|
||||
offset = 1.0 - max(0.0, min(1.0, offset))
|
||||
self._set_hue(offset)
|
||||
|
||||
if self.fieldDragging:
|
||||
offset = event.coordinates - self.fieldBG.abs_pos
|
||||
saturation = max(0.0, min(1.0, offset.x / 128.0))
|
||||
value = 1.0 - max(0.0, min(1.0, offset.y / 128.0))
|
||||
self._set_sat_val(saturation, value)
|
||||
|
||||
self._update_color()
|
||||
|
||||
def _set_sat_val(self, sat, val):
|
||||
self.saturation = sat
|
||||
self.valueValue = val
|
||||
|
||||
self.fieldHandle.top = (1.0 - self.valueValue) * 128.0 - self.fieldHandle.height / 2.0
|
||||
self.fieldHandle.left = self.saturation * 128.0 - self.fieldHandle.width / 2.0
|
||||
|
||||
def _set_hue(self, hue):
|
||||
self.hueValue = min(0.999, hue)
|
||||
self.hueHandle.top = (1.0-hue) * 128.0 - self.hueHandle.height / 2
|
||||
self.fieldBG.color = colorsys.hsv_to_rgb(self.hueValue, 1, 1)
|
||||
|
||||
def _update_color(self):
|
||||
rgb = colorsys.hsv_to_rgb(self.hueValue, self.saturation, self.valueValue)
|
||||
self.activeColorBG.color = rgb
|
||||
|
||||
self.colorLabels[0].set_text(str(int(rgb[0]*255.0)))
|
||||
self.colorLabels[1].set_text(str(int(rgb[1]*255.0)))
|
||||
self.colorLabels[2].set_text(str(int(rgb[2]*255.0)))
|
||||
|
||||
self._trigger_callback(rgb)
|
||||
|
||||
def _start_field_dragging(self, event):
|
||||
if not self.fieldDragging:
|
||||
self.fieldDragging = True
|
||||
|
||||
def _stop_field_dragging(self, event):
|
||||
if self.fieldDragging:
|
||||
self.fieldDragging = False
|
||||
|
||||
def _start_hue_dragging(self, event):
|
||||
if not self.hueDragging:
|
||||
self.hueDragging = True
|
||||
|
||||
def _stop_hue_dragging(self, event):
|
||||
if self.hueDragging:
|
||||
self.hueDragging = False
|
||||
|
||||
100
ui/Builtin/LUIBlockText.py
Normal file
@ -0,0 +1,100 @@
|
||||
|
||||
from panda3d.core import LVecBase2i
|
||||
from LUIObject import LUIObject
|
||||
from LUILabel import LUILabel
|
||||
from LUIInitialState import LUIInitialState
|
||||
|
||||
__all__ = ["LUIBlockText"]
|
||||
|
||||
|
||||
class LUIBlockText(LUIObject):
|
||||
|
||||
""" Small helper class to format labels into paragraphs.
|
||||
Uses LUILabels internally """
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
""" Creates a new block of text. """
|
||||
LUIObject.__init__(self)
|
||||
LUIInitialState.init(self, kwargs)
|
||||
self._cursor = LVecBase2i(0)
|
||||
self._last_size = 14
|
||||
|
||||
self.labels = []
|
||||
|
||||
|
||||
def clear(self):
|
||||
""" Removes all text from this label and resets it to the initial state.
|
||||
This will also detach the sub-labels from this label. """
|
||||
self._cursor.set(0, 0)
|
||||
self.labels = []
|
||||
self.remove_all_children()
|
||||
|
||||
|
||||
def newline(self, font_size=None):
|
||||
""" Moves the cursor to the next line. The font size controls how much
|
||||
the cursor will move. By default, the font size of the last added text
|
||||
is used, or if no text was added yet, a size of 14."""
|
||||
self._cursor.x = 0
|
||||
if font_size is None:
|
||||
font_size = self._last_size
|
||||
self._cursor.y += font_size + 2
|
||||
|
||||
|
||||
def add(self, *args, **kwargs):
|
||||
""" Appends a new text. The arguments are equal to the arguments of
|
||||
LUILabel. The arguments shouldn't contain information about the
|
||||
placement like top_left, or center_vertical, since the labels are
|
||||
placed at explicit positions. """
|
||||
self._last_size = kwargs.get("font_size", 14)
|
||||
label = LUILabel(parent=self, left=self._cursor.x, top=self._cursor.y, width=self.get_width(),
|
||||
*args, **kwargs)
|
||||
|
||||
self.labels.append(label)
|
||||
|
||||
# This is a bit of a hack, we should use a horizontal layout, but we
|
||||
# don't for performance reasons.
|
||||
self._cursor.y += label.text_handle.height
|
||||
|
||||
# After every paragraph, we add a new line.
|
||||
self.newline()
|
||||
|
||||
|
||||
def set_text(self, text):
|
||||
""" Replaces the text with new text """
|
||||
self.clear()
|
||||
self.add(text=text)
|
||||
|
||||
|
||||
def update_height(self):
|
||||
""" Updates the height of the element, adding a newline to the end of
|
||||
every paragraph """
|
||||
top = 0
|
||||
for child in self.labels:
|
||||
child.top = top
|
||||
top += child._text.height
|
||||
|
||||
# Newline
|
||||
top += self._last_size + 2
|
||||
|
||||
|
||||
def set_wrap(self, wrap):
|
||||
""" Sets text wrapping for the element. Wrapping breaks lines on
|
||||
spaces, and breaks words if the word is longer than the line
|
||||
length. """
|
||||
for child in self.children:
|
||||
for c in child.children:
|
||||
c.set_wordwrap(wrap)
|
||||
|
||||
self.update_height()
|
||||
|
||||
|
||||
def set_width(self, width):
|
||||
""" Sets the width of this element, and turns on wrapping. """
|
||||
for child in self.children:
|
||||
child.set_width(width)
|
||||
|
||||
# Need to force an update to the text when the width changes.
|
||||
for c in child.children:
|
||||
c.set_wordwrap(True)
|
||||
|
||||
self.update_height()
|
||||
192
ui/Builtin/LUIButton.py
Normal file
@ -0,0 +1,192 @@
|
||||
|
||||
from LUIObject import LUIObject
|
||||
from LUILayouts import LUIHorizontalStretchedLayout
|
||||
from LUILabel import LUILabel
|
||||
from LUIInitialState import LUIInitialState
|
||||
|
||||
__all__ = ["LUIButton"]
|
||||
|
||||
|
||||
class LUIButton(LUIObject):
|
||||
|
||||
""" Simple button, containing three sprites and a label. """
|
||||
|
||||
def __init__(self, text="Button", template="ButtonDefault", **kwargs):
|
||||
""" Constructs a new button. The template controls which sprites to use.
|
||||
If the template is "ButtonDefault" for example, the sprites
|
||||
"ButtonDefault_Left", "ButtonDefault" and "ButtonDefault_Right" will
|
||||
be used. The sprites used when the button is pressed should be named
|
||||
"ButtonDefaultFocus_Left" and so on then.
|
||||
|
||||
If an explicit width is set on the button, the button will stick to
|
||||
that width, otherwise it will automatically resize to fit the label """
|
||||
LUIObject.__init__(self, x=0, y=0, solid=True)
|
||||
self._template = template
|
||||
self._layout = LUIHorizontalStretchedLayout(
|
||||
parent=self, prefix=self._template, width="100%")
|
||||
self._label = LUILabel(parent=self, text=text)
|
||||
self._label.z_offset = 1
|
||||
self._label.center_vertical = True
|
||||
self._label.margin = 0, 20, 0, 20
|
||||
self.margin.left = -1
|
||||
self._hovered = False
|
||||
self._pressed = False
|
||||
self._use_custom_texture = False
|
||||
self._custom_texture = None
|
||||
self._custom_uv = None
|
||||
self._custom_texture_hover = None
|
||||
self._custom_uv_hover = None
|
||||
self._custom_texture_pressed = None
|
||||
self._custom_uv_pressed = None
|
||||
LUIInitialState.init(self, kwargs)
|
||||
|
||||
def _apply_stretch_sizes(self):
|
||||
"""Ensure internal sprites stretch to the button size."""
|
||||
try:
|
||||
layout = getattr(self, '_layout', None)
|
||||
if layout is not None:
|
||||
if hasattr(layout, 'width'):
|
||||
layout.width = "100%"
|
||||
if hasattr(layout, 'height'):
|
||||
layout.height = "100%"
|
||||
inner = getattr(layout, '_layout', None)
|
||||
if inner is not None:
|
||||
if hasattr(inner, 'width'):
|
||||
inner.width = "100%"
|
||||
if hasattr(inner, 'height'):
|
||||
inner.height = "100%"
|
||||
for attr in ('_sprite_left', '_sprite_mid', '_sprite_right'):
|
||||
spr = getattr(layout, attr, None)
|
||||
if spr is not None:
|
||||
if hasattr(spr, 'height'):
|
||||
spr.height = "100%"
|
||||
if attr == '_sprite_mid' and hasattr(spr, 'width'):
|
||||
spr.width = "100%"
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def _apply_custom_texture(self, state="normal"):
|
||||
"""Apply custom texture based on state: normal/hover/pressed."""
|
||||
if not self._use_custom_texture:
|
||||
return
|
||||
|
||||
tex = None
|
||||
uv = None
|
||||
if state == "pressed" and self._custom_texture_pressed is not None:
|
||||
tex = self._custom_texture_pressed
|
||||
uv = self._custom_uv_pressed
|
||||
elif state == "hover" and self._custom_texture_hover is not None:
|
||||
tex = self._custom_texture_hover
|
||||
uv = self._custom_uv_hover
|
||||
else:
|
||||
tex = self._custom_texture
|
||||
uv = self._custom_uv
|
||||
|
||||
if tex is None:
|
||||
return
|
||||
|
||||
layout = getattr(self, "_layout", None)
|
||||
if layout is None:
|
||||
return
|
||||
for attr in ("_sprite_left", "_sprite_mid", "_sprite_right"):
|
||||
spr = getattr(layout, attr, None)
|
||||
if spr is None:
|
||||
continue
|
||||
try:
|
||||
if hasattr(spr, "set_texture"):
|
||||
spr.set_texture(tex, resize=False)
|
||||
if uv and hasattr(spr, "set_uv_range"):
|
||||
u0, v0, u1, v1 = uv
|
||||
spr.set_uv_range(u0, v0, u1, v1)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Hide left/right caps for single-image mode
|
||||
try:
|
||||
if hasattr(layout, "_sprite_left") and layout._sprite_left is not None:
|
||||
layout._sprite_left.width = 0
|
||||
if hasattr(layout, "_sprite_right") and layout._sprite_right is not None:
|
||||
layout._sprite_right.width = 0
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
self._apply_stretch_sizes()
|
||||
|
||||
def set_custom_textures(self, normal_tex, normal_uv=None, hover_tex=None, hover_uv=None, pressed_tex=None, pressed_uv=None):
|
||||
"""Set custom textures for normal/hover/pressed states."""
|
||||
self._use_custom_texture = True
|
||||
self._custom_texture = normal_tex
|
||||
self._custom_uv = normal_uv
|
||||
self._custom_texture_hover = hover_tex
|
||||
self._custom_uv_hover = hover_uv
|
||||
self._custom_texture_pressed = pressed_tex
|
||||
self._custom_uv_pressed = pressed_uv
|
||||
if self._hovered:
|
||||
self._apply_custom_texture("hover")
|
||||
else:
|
||||
self._apply_custom_texture("normal")
|
||||
|
||||
def set_custom_texture(self, texture, uv=None):
|
||||
"""Use a single texture for the button background."""
|
||||
self.set_custom_textures(texture, uv, None, None, None, None)
|
||||
|
||||
def clear_custom_texture(self):
|
||||
"""Restore default template textures."""
|
||||
self._use_custom_texture = False
|
||||
self._custom_texture = None
|
||||
self._custom_uv = None
|
||||
self._custom_texture_hover = None
|
||||
self._custom_uv_hover = None
|
||||
self._custom_texture_pressed = None
|
||||
self._custom_uv_pressed = None
|
||||
try:
|
||||
self._layout.prefix = self._template
|
||||
self._apply_stretch_sizes()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
@property
|
||||
def text(self):
|
||||
""" Returns the current label text of the button """
|
||||
return self._label.text
|
||||
|
||||
@text.setter
|
||||
def text(self, text):
|
||||
""" Sets the label text of the button """
|
||||
self._label.text = text
|
||||
|
||||
def on_mousedown(self, event):
|
||||
""" Internal on_mousedown handler. Do not override """
|
||||
self._pressed = True
|
||||
if self._use_custom_texture:
|
||||
self._apply_custom_texture("pressed")
|
||||
else:
|
||||
self._layout.prefix = self._template + "Focus"
|
||||
self._apply_stretch_sizes()
|
||||
self._label.margin.top = 1
|
||||
|
||||
def on_mouseup(self, event):
|
||||
""" Internal on_mouseup handler. Do not override """
|
||||
self._pressed = False
|
||||
if self._use_custom_texture:
|
||||
if self._hovered:
|
||||
self._apply_custom_texture("hover")
|
||||
else:
|
||||
self._apply_custom_texture("normal")
|
||||
else:
|
||||
self._layout.prefix = self._template
|
||||
self._apply_stretch_sizes()
|
||||
self._label.margin.top = 0
|
||||
|
||||
def on_mouseover(self, event):
|
||||
""" Internal mouseover handler """
|
||||
self._hovered = True
|
||||
if self._use_custom_texture and not self._pressed:
|
||||
self._apply_custom_texture("hover")
|
||||
|
||||
def on_mouseout(self, event):
|
||||
""" Internal mouseout handler """
|
||||
self._hovered = False
|
||||
self._pressed = False
|
||||
if self._use_custom_texture:
|
||||
self._apply_custom_texture("normal")
|
||||
102
ui/Builtin/LUICanvas.py
Normal file
@ -0,0 +1,102 @@
|
||||
"""
|
||||
LUICanvas - 类似Unity的Canvas容器,带有可视化边框
|
||||
用于组织和管理UI元素,所有UI组件都应该是Canvas的子节点
|
||||
"""
|
||||
|
||||
from LUIObject import LUIObject
|
||||
from LUISprite import LUISprite
|
||||
from LUIInitialState import LUIInitialState
|
||||
|
||||
__all__ = ["LUICanvas"]
|
||||
|
||||
|
||||
class LUICanvas(LUIObject):
|
||||
"""
|
||||
Canvas容器,类似Unity的Canvas系统
|
||||
- 所有UI元素的父容器
|
||||
- 带有可视化的边框线框(类似Unity编辑器中的Canvas Gizmo)
|
||||
- 可以设置边框颜色和宽度
|
||||
"""
|
||||
|
||||
def __init__(self, border_color=(0.3, 0.7, 1.0, 0.3), border_width=2, **kwargs):
|
||||
"""
|
||||
创建一个新的Canvas
|
||||
|
||||
参数:
|
||||
border_color: 边框颜色 (r, g, b, a),默认为蓝色半透明
|
||||
border_width: 边框宽度,默认为2像素(使用平面模式时此参数无效)
|
||||
"""
|
||||
LUIObject.__init__(self)
|
||||
|
||||
self._border_width = border_width
|
||||
self._border_color = border_color
|
||||
|
||||
# 创建一个透明平面覆盖整个Canvas
|
||||
self._border_plane = LUISprite(self, "blank", "skin")
|
||||
self._border_plane.pos = (0, 0)
|
||||
self._border_plane.color = self._border_color
|
||||
self._border_plane.z_offset = 0
|
||||
|
||||
# 先设置一个固定尺寸,确保可见
|
||||
self._border_plane.width = 1280
|
||||
self._border_plane.height = 720
|
||||
|
||||
# 应用初始状态
|
||||
LUIInitialState.init(self, kwargs)
|
||||
|
||||
# 最后设置Canvas占满整个屏幕(这会触发setter更新边框)
|
||||
self.set_size("100%", "100%")
|
||||
|
||||
def _update_borders(self):
|
||||
"""更新边框平面的位置和大小"""
|
||||
# 直接获取width和height属性值
|
||||
try:
|
||||
width = self.width
|
||||
height = self.height
|
||||
|
||||
# 设置平面覆盖整个Canvas区域
|
||||
self._border_plane.pos = (0, 0)
|
||||
self._border_plane.width = width
|
||||
self._border_plane.height = height
|
||||
except:
|
||||
# 如果获取失败,使用默认值
|
||||
pass
|
||||
|
||||
def set_border_color(self, color):
|
||||
"""设置边框颜色 (r, g, b, a)"""
|
||||
self._border_color = color
|
||||
self._border_plane.color = color
|
||||
|
||||
def set_border_width(self, width):
|
||||
"""设置边框宽度(平面模式下无效)"""
|
||||
self._border_width = width
|
||||
|
||||
def show_border(self):
|
||||
"""显示边框"""
|
||||
self._border_plane.show()
|
||||
|
||||
def hide_border(self):
|
||||
"""隐藏边框"""
|
||||
self._border_plane.hide()
|
||||
|
||||
@property
|
||||
def width(self):
|
||||
"""获取宽度"""
|
||||
return LUIObject.width.fget(self)
|
||||
|
||||
@width.setter
|
||||
def width(self, value):
|
||||
"""设置宽度并更新边框"""
|
||||
LUIObject.width.fset(self, value)
|
||||
self._update_borders()
|
||||
|
||||
@property
|
||||
def height(self):
|
||||
"""获取高度"""
|
||||
return LUIObject.height.fget(self)
|
||||
|
||||
@height.setter
|
||||
def height(self, value):
|
||||
"""设置高度并更新边框"""
|
||||
LUIObject.height.fset(self, value)
|
||||
self._update_borders()
|
||||
83
ui/Builtin/LUICheckbox.py
Normal file
@ -0,0 +1,83 @@
|
||||
|
||||
from __future__ import division
|
||||
|
||||
from LUIObject import LUIObject
|
||||
from LUISprite import LUISprite
|
||||
from LUILabel import LUILabel
|
||||
from LUIInitialState import LUIInitialState
|
||||
|
||||
__all__ = ["LUICheckbox"]
|
||||
|
||||
|
||||
class LUICheckbox(LUIObject):
|
||||
|
||||
""" This is a simple checkbox, including a Label. The checkbox can either
|
||||
be checked or unchecked. """
|
||||
|
||||
def __init__(self, checked=False, label=u"Checkbox", **kwargs):
|
||||
""" Constructs a new checkbox with the given label and state. By default,
|
||||
the checkbox is not checked. """
|
||||
LUIObject.__init__(self, x=0, y=0, solid=True)
|
||||
self._checked = checked
|
||||
self._checkbox_sprite = LUISprite(self, "Checkbox_Default", "skin")
|
||||
self._label = LUILabel(parent=self, text=label, margin=(0, 0, 0, 25),
|
||||
center_vertical=True, alpha=0.4)
|
||||
self._hovered = False
|
||||
self._update_sprite()
|
||||
LUIInitialState.init(self, kwargs)
|
||||
|
||||
@property
|
||||
def checked(self):
|
||||
""" Returns True if the checkbox is currently checked """
|
||||
return self._checked
|
||||
|
||||
@checked.setter
|
||||
def checked(self, checked):
|
||||
""" Sets the checkbox state """
|
||||
self._checked = checked
|
||||
self._update_sprite()
|
||||
|
||||
def toggle(self):
|
||||
""" Toggles the checkbox state """
|
||||
self.checked = not self.checked
|
||||
|
||||
@property
|
||||
def label(self):
|
||||
""" Returns a handle to the label, so it can be modified """
|
||||
return self._label
|
||||
|
||||
@property
|
||||
def sprite(self):
|
||||
""" Returns a handle to the internal checkbox sprite """
|
||||
return self._checkbox_sprite
|
||||
|
||||
def on_click(self, event):
|
||||
""" Internal onclick handler. Do not override """
|
||||
self._checked = not self._checked
|
||||
self.trigger_event("changed")
|
||||
self._update_sprite()
|
||||
|
||||
def on_mousedown(self, event):
|
||||
""" Internal mousedown handler. """
|
||||
self._checkbox_sprite.color = (0.9, 0.9, 0.9, 1.0)
|
||||
|
||||
def on_mouseup(self, event):
|
||||
""" Internal on_mouseup handler. """
|
||||
self._checkbox_sprite.color = (1, 1, 1, 1)
|
||||
|
||||
def on_mouseover(self, event):
|
||||
""" Internal mouseover handler """
|
||||
self._hovered = True
|
||||
self._update_sprite()
|
||||
|
||||
def on_mouseout(self, event):
|
||||
""" Internal mouseout handler """
|
||||
self._hovered = False
|
||||
self._update_sprite()
|
||||
|
||||
def _update_sprite(self):
|
||||
""" Internal method to update the sprites """
|
||||
img = "Checkbox_Checked" if self._checked else "Checkbox_Default"
|
||||
if self._hovered:
|
||||
img += "Hover"
|
||||
self._checkbox_sprite.set_texture(img, "skin")
|
||||
47
ui/Builtin/LUIFormattedLabel.py
Normal file
@ -0,0 +1,47 @@
|
||||
|
||||
from panda3d.core import LVecBase2i
|
||||
from LUIObject import LUIObject
|
||||
from LUILabel import LUILabel
|
||||
from LUIInitialState import LUIInitialState
|
||||
|
||||
__all__ = ["LUIFormattedLabel"]
|
||||
|
||||
|
||||
class LUIFormattedLabel(LUIObject):
|
||||
|
||||
""" Small helper class to build a text consisting of different formatted
|
||||
parts of text. Uses LUILabels internally """
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
""" Creates a new formatted label. """
|
||||
LUIObject.__init__(self)
|
||||
LUIInitialState.init(self, kwargs)
|
||||
self._cursor = LVecBase2i(0)
|
||||
self._last_size = 14
|
||||
|
||||
def clear(self):
|
||||
""" Removes all text from this label and resets it to the initial state.
|
||||
This will also detach the sub-labels from this label. """
|
||||
self._cursor.set(0, 0)
|
||||
self.remove_all_children()
|
||||
|
||||
def newline(self, font_size=None):
|
||||
""" Moves the cursor to the next line. The font size controls how much
|
||||
the cursor will move. By default, the font size of the last added text
|
||||
is used, or if no text was added yet, a size of 14."""
|
||||
self._cursor.x = 0
|
||||
if font_size is None:
|
||||
font_size = self._last_size
|
||||
self._cursor.y += font_size + 2
|
||||
|
||||
def add(self, *args, **kwargs):
|
||||
""" Appends a new text. The arguments are equal to the arguments of
|
||||
LUILabel. The arguments shouldn't contain information about the
|
||||
placement like top_left, or center_vertical, since the labels are
|
||||
placed at explicit positions. """
|
||||
self._last_size = kwargs.get("font_size", 14)
|
||||
label = LUILabel(parent=self, left=self._cursor.x, top=self._cursor.y,
|
||||
*args, **kwargs)
|
||||
# This is a bit of a hack, we should use a horizontal layout, but we
|
||||
# don't for performance reasons.
|
||||
self._cursor.x += label.text_handle.width
|
||||
66
ui/Builtin/LUIFrame.py
Normal file
@ -0,0 +1,66 @@
|
||||
|
||||
from __future__ import print_function
|
||||
|
||||
from LUIObject import LUIObject
|
||||
from LUISprite import LUISprite
|
||||
from LUILayouts import LUICornerLayout
|
||||
from LUIInitialState import LUIInitialState
|
||||
from LUIScrollableRegion import LUIScrollableRegion
|
||||
|
||||
__all__ = ["LUIFrame"]
|
||||
|
||||
|
||||
class LUIFrame(LUIObject):
|
||||
|
||||
""" A container which can store multiple ui-elements. If you don't want a
|
||||
border/background, you should use an empty LUIObject as container instead.
|
||||
"""
|
||||
|
||||
FS_sunken = 1
|
||||
FS_raised = 2
|
||||
|
||||
def __init__(self, inner_padding=5, scrollable=False, style=FS_raised,
|
||||
**kwargs):
|
||||
""" Creates a new frame with the given options and style. If scrollable
|
||||
is True, the contents of the frame will scroll if they don't fit into
|
||||
the frame height. inner_padding only has effect if scrollable is True.
|
||||
You can call fit_to_children() to make the frame fit automatically to
|
||||
it's contents."""
|
||||
LUIObject.__init__(self)
|
||||
|
||||
# Each *style* has a different border size (size of the shadow). The
|
||||
# border size shouldn't get calculated to the actual framesize, so we
|
||||
# are determining it first and then substracting it.
|
||||
# TODO: We could do this automatically, determined by the sprite size
|
||||
# probably?
|
||||
self._border_size = 0
|
||||
self.padding = 10
|
||||
self.solid = True
|
||||
prefix = ""
|
||||
|
||||
if style == LUIFrame.FS_raised:
|
||||
temp = LUISprite(self, "Frame_Left", "skin")
|
||||
self._border_size = temp.width
|
||||
self.remove_child(temp)
|
||||
prefix = "Frame_"
|
||||
elif style == LUIFrame.FS_sunken:
|
||||
self._border_size = 0
|
||||
prefix = "SunkenFrame_"
|
||||
else:
|
||||
raise Exception("Unkown LUIFrame style: " + style)
|
||||
|
||||
self._scrollable = scrollable
|
||||
self._layout = LUICornerLayout(parent=self, image_prefix=prefix)
|
||||
self._layout.margin = -(self.padding.top + self._border_size)
|
||||
if self._scrollable:
|
||||
self._content = LUIObject(self)
|
||||
self._content.size = (self.width, self.height)
|
||||
self._content.pos = (self._border_size, self._border_size)
|
||||
self._scroll_content = LUIScrollableRegion(
|
||||
self._content,
|
||||
width=self.width - 2 * self.padding.left,
|
||||
height=self.height - 2 * self.padding.left,
|
||||
padding=inner_padding)
|
||||
self.content_node = self._scroll_content.content_node
|
||||
|
||||
LUIInitialState.init(self, kwargs)
|
||||
20
ui/Builtin/LUIHorizontalLayout.py
Normal file
@ -0,0 +1,20 @@
|
||||
"""
|
||||
|
||||
This is a wrapper file. It contains no actual implementation
|
||||
|
||||
"""
|
||||
|
||||
from panda3d.lui import LUIHorizontalLayout as _LUIHorizontalLayout
|
||||
from LUIInitialState import LUIInitialState
|
||||
|
||||
__all__ = ["LUIHorizontalLayout"]
|
||||
|
||||
|
||||
class LUIHorizontalLayout(_LUIHorizontalLayout):
|
||||
""" This is a wrapper class for the C++ LUIHorizontalLayout class, to be
|
||||
able to use it in a more convenient way. It leverages LUIInitialState
|
||||
to be able to pass arbitrary keyword arguments. """
|
||||
|
||||
def __init__(self, parent=None, spacing=0.0, **kwargs):
|
||||
_LUIHorizontalLayout.__init__(self, parent, spacing)
|
||||
LUIInitialState.init(self, kwargs)
|
||||
41
ui/Builtin/LUIInitialState.py
Normal file
@ -0,0 +1,41 @@
|
||||
|
||||
__all__ = ["LUIInitialState"]
|
||||
|
||||
|
||||
class LUIInitialState:
|
||||
|
||||
""" Small helper class to pass keyword arguments to the LUI-objects. It takes
|
||||
all keyword arguments of a given call, and calls obj.<kwarg> = <value> for
|
||||
each keyword. It usually is called at the end of the __init__ method. """
|
||||
|
||||
def __init__(self):
|
||||
raise Exception("LUIInitialState is a static class")
|
||||
|
||||
# Some properties have alternative names, under which they can be accessed.
|
||||
__MAPPINGS = {
|
||||
"x": "left",
|
||||
"y": "top",
|
||||
"w": "width",
|
||||
"h": "height"
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def init(cls, obj, kwargs):
|
||||
""" Applies all keyword arguments as properties. For example, passing
|
||||
dict({"left": 10, "top": 3, "color": (0.2, 0.6, 1.0)}) results in
|
||||
behaviour similar to:
|
||||
|
||||
element.left = 10
|
||||
element.top = 3
|
||||
element.color = 0.2, 0.6, 1.0
|
||||
|
||||
Calling this method allows setting arbitrary properties in
|
||||
constructors, without having to specify each possible keyword argument.
|
||||
"""
|
||||
for arg_name, arg_val in kwargs.items():
|
||||
arg_name = cls.__MAPPINGS.get(arg_name, arg_name)
|
||||
if hasattr(obj, arg_name):
|
||||
setattr(obj, arg_name, arg_val)
|
||||
else:
|
||||
raise AttributeError("{0} has no attribute {1}".format(
|
||||
obj.__class__.__name__, arg_name))
|
||||
219
ui/Builtin/LUIInputField.py
Normal file
@ -0,0 +1,219 @@
|
||||
import re
|
||||
|
||||
from LUIObject import LUIObject
|
||||
from LUISprite import LUISprite
|
||||
from LUILabel import LUILabel
|
||||
from LUIInitialState import LUIInitialState
|
||||
from LUILayouts import LUIHorizontalStretchedLayout
|
||||
|
||||
__all__ = ["LUIInputField"]
|
||||
|
||||
|
||||
class LUIInputField(LUIObject):
|
||||
|
||||
""" Simple input field, accepting text input. This input field supports
|
||||
entering text and navigating. Selecting text is (currently) not supported.
|
||||
|
||||
The input field also supports various keyboard shortcuts:
|
||||
|
||||
[pos1] Move to the beginning of the text
|
||||
[end] Move to the end of the text
|
||||
[arrow_left] Move one character to the left
|
||||
[arrow_right] Move one character to the right
|
||||
[ctrl] + [arrow_left] Move to the left, skipping over words
|
||||
[ctrl] + [arrow_right] Move to the right, skipping over words
|
||||
[escape] Un-focus input element
|
||||
|
||||
"""
|
||||
|
||||
re_skip = re.compile("\W*\w+\W")
|
||||
|
||||
def __init__(self, parent=None, width=200, placeholder=u"Enter some text ..", value=u"", **kwargs):
|
||||
""" Constructs a new input field. An input field always needs a width specified """
|
||||
LUIObject.__init__(self, x=0, y=0, solid=True)
|
||||
self.set_width(width)
|
||||
self._layout = LUIHorizontalStretchedLayout(parent=self, prefix="InputField", width="100%")
|
||||
|
||||
# Container for the text
|
||||
self._text_content = LUIObject(self)
|
||||
self._text_content.margin = (5, 7, 5, 7)
|
||||
self._text_content.clip_bounds = (0,0,0,0)
|
||||
self._text_content.set_size("100%", "100%")
|
||||
|
||||
# Scroller for the text, so we can move right and left
|
||||
self._text_scroller = LUIObject(parent=self._text_content)
|
||||
self._text_scroller.center_vertical = True
|
||||
self._text = LUILabel(parent=self._text_scroller, text="")
|
||||
|
||||
# Cursor for the current position
|
||||
self._cursor = LUISprite(self._text_scroller, "blank", "skin", x=0, y=0, w=2, h=15)
|
||||
self._cursor.color = (0.5, 0.5, 0.5)
|
||||
self._cursor.margin.top = 2
|
||||
self._cursor.z_offset = 20
|
||||
self._cursor_index = 0
|
||||
self._cursor.hide()
|
||||
self._value = value
|
||||
|
||||
# Placeholder text, shown when out of focus and no value exists
|
||||
self._placeholder = LUILabel(parent=self._text_content, text=placeholder, shadow=False,
|
||||
center_vertical=True, alpha=0.2)
|
||||
|
||||
# Various states
|
||||
self._tickrate = 1.0
|
||||
self._tickstart = 0.0
|
||||
|
||||
self._render_text()
|
||||
|
||||
if parent is not None:
|
||||
self.parent = parent
|
||||
|
||||
LUIInitialState.init(self, kwargs)
|
||||
|
||||
@property
|
||||
def value(self):
|
||||
""" Returns the value of the input field """
|
||||
return self._value
|
||||
|
||||
@value.setter
|
||||
def value(self, new_value):
|
||||
""" Sets the value of the input field """
|
||||
self._value = new_value
|
||||
self._render_text()
|
||||
self.trigger_event("changed", self._value)
|
||||
|
||||
def clear(self):
|
||||
""" Clears the input value """
|
||||
self.value = u""
|
||||
|
||||
@property
|
||||
def cursor_pos(self):
|
||||
""" Set the cursor position """
|
||||
return self._cursor_index
|
||||
|
||||
@cursor_pos.setter
|
||||
def cursor_pos(self, pos):
|
||||
""" Set the cursor position """
|
||||
if pos >= 0:
|
||||
self._cursor_index = max(0, min(len(self._value), pos))
|
||||
else:
|
||||
self._cursor_index = max(len(self._value) + pos + 1, 0)
|
||||
self._reset_cursor_tick()
|
||||
self._render_text()
|
||||
|
||||
def on_tick(self, event):
|
||||
""" Tick handler, gets executed every frame """
|
||||
frame_time = globalClock.get_frame_time() - self._tickstart
|
||||
show_cursor = frame_time % self._tickrate < 0.5 * self._tickrate
|
||||
if show_cursor:
|
||||
self._cursor.color = (0.5, 0.5, 0.5, 1)
|
||||
else:
|
||||
self._cursor.color = (1, 1, 1, 0)
|
||||
|
||||
def on_click(self, event):
|
||||
""" Internal on click handler """
|
||||
self.request_focus()
|
||||
|
||||
def on_mousedown(self, event):
|
||||
""" Internal mousedown handler """
|
||||
local_x_offset = self._text.text_handle.get_relative_pos(event.coordinates).x
|
||||
self.cursor_pos = self._text.text_handle.get_char_index(local_x_offset)
|
||||
|
||||
def _reset_cursor_tick(self):
|
||||
""" Internal method to reset the cursor tick """
|
||||
self._tickstart = globalClock.get_frame_time()
|
||||
|
||||
def on_focus(self, event):
|
||||
""" Internal focus handler """
|
||||
self._cursor.show()
|
||||
self._placeholder.hide()
|
||||
self._reset_cursor_tick()
|
||||
self._layout.color = (0.9, 0.9, 0.9, 1)
|
||||
|
||||
def on_keydown(self, event):
|
||||
""" Internal keydown handler. Processes the special keys, and if none are
|
||||
present, redirects the event """
|
||||
key_name = event.message
|
||||
if key_name == "backspace":
|
||||
self._value = self._value[:max(0, self._cursor_index - 1)] + self._value[self._cursor_index:]
|
||||
self.cursor_pos -= 1
|
||||
self.trigger_event("changed", self._value)
|
||||
elif key_name == "delete":
|
||||
post_value = self._value[min(len(self._value), self._cursor_index + 1):]
|
||||
self._value = self._value[:self._cursor_index] + post_value
|
||||
self.cursor_pos = self._cursor_index
|
||||
self.trigger_event("changed", self._value)
|
||||
elif key_name == "arrow_left":
|
||||
if event.get_modifier_state("alt") or event.get_modifier_state("ctrl"):
|
||||
self.cursor_skip_left()
|
||||
else:
|
||||
self.cursor_pos -= 1
|
||||
elif key_name == "arrow_right":
|
||||
if event.get_modifier_state("alt") or event.get_modifier_state("ctrl"):
|
||||
self.cursor_skip_right()
|
||||
else:
|
||||
self.cursor_pos += 1
|
||||
elif key_name == "escape":
|
||||
self.blur()
|
||||
elif key_name == "home":
|
||||
self.cursor_pos = 0
|
||||
elif key_name == "end":
|
||||
self.cursor_pos = len(self.value)
|
||||
|
||||
self.trigger_event(key_name, self._value)
|
||||
|
||||
def on_keyrepeat(self, event):
|
||||
""" Internal keyrepeat handler """
|
||||
self.on_keydown(event)
|
||||
|
||||
def on_textinput(self, event):
|
||||
""" Internal textinput handler """
|
||||
self._value = self._value[:self._cursor_index] + event.message + \
|
||||
self._value[self._cursor_index:]
|
||||
self.cursor_pos = self._cursor_index + len(event.message)
|
||||
self.trigger_event("changed", self._value)
|
||||
|
||||
def on_blur(self, event):
|
||||
""" Internal blur handler """
|
||||
self._cursor.hide()
|
||||
if len(self._value) < 1:
|
||||
self._placeholder.show()
|
||||
|
||||
self._layout.color = (1, 1, 1, 1)
|
||||
|
||||
def _render_text(self):
|
||||
""" Internal method to render the text """
|
||||
self._text.set_text(self._value)
|
||||
self._cursor.left = self._text.left + \
|
||||
self._text.text_handle.get_char_pos(self._cursor_index) + 1
|
||||
max_left = self.width - 15
|
||||
|
||||
if self._value:
|
||||
self._placeholder.hide()
|
||||
else:
|
||||
if not self.focused:
|
||||
self._placeholder.show()
|
||||
|
||||
# Scroll if the cursor is outside of the clip bounds
|
||||
rel_pos = self.get_relative_pos(self._cursor.get_abs_pos()).x
|
||||
if rel_pos >= max_left:
|
||||
self._text_scroller.left = min(0, max_left - self._cursor.left)
|
||||
if rel_pos <= 0:
|
||||
self._text_scroller.left = min(0, - self._cursor.left - rel_pos)
|
||||
|
||||
def cursor_skip_left(self):
|
||||
""" Moves the cursor to the left, skipping the previous word """
|
||||
left_hand_str = ''.join(reversed(self.value[0:self.cursor_pos]))
|
||||
match = self.re_skip.match(left_hand_str)
|
||||
if match is not None:
|
||||
self.cursor_pos -= match.end() - 1
|
||||
else:
|
||||
self.cursor_pos = 0
|
||||
|
||||
def cursor_skip_right(self):
|
||||
""" Moves the cursor to the right, skipping the next word """
|
||||
right_hand_str = self.value[self.cursor_pos:]
|
||||
match = self.re_skip.match(right_hand_str)
|
||||
if match is not None:
|
||||
self.cursor_pos += match.end() - 1
|
||||
else:
|
||||
self.cursor_pos = len(self.value)
|
||||
8
ui/Builtin/LUIInputHandler.py
Normal file
@ -0,0 +1,8 @@
|
||||
"""
|
||||
|
||||
This is a wrapper file. It contains no actual implementation
|
||||
|
||||
"""
|
||||
|
||||
from panda3d.lui import LUIInputHandler as __LUIInputHandler
|
||||
LUIInputHandler = __LUIInputHandler
|
||||
77
ui/Builtin/LUILabel.py
Normal file
@ -0,0 +1,77 @@
|
||||
|
||||
from panda3d.lui import LUIText
|
||||
from LUIObject import LUIObject
|
||||
from LUIInitialState import LUIInitialState
|
||||
|
||||
__all__ = ["LUILabel"]
|
||||
|
||||
class LUILabel(LUIObject):
|
||||
|
||||
""" A simple label, displaying text. """
|
||||
|
||||
# Default variables which can be overridden by skins
|
||||
DEFAULT_COLOR = (0.9, 0.9, 0.9, 1)
|
||||
DEFAULT_USE_SHADOW = True
|
||||
|
||||
def __init__(self, text="Label", shadow=None, font_size=14, font="label", color=None, wordwrap=False, **kwargs):
|
||||
""" Creates a new label. If shadow is True, a small text shadow will be
|
||||
rendered below the actual text. """
|
||||
LUIObject.__init__(self)
|
||||
LUIInitialState.init(self, kwargs)
|
||||
self._text = LUIText(
|
||||
self,
|
||||
text,
|
||||
font,
|
||||
font_size,
|
||||
0,
|
||||
0,
|
||||
wordwrap
|
||||
)
|
||||
self._text.z_offset = 1
|
||||
if color is None:
|
||||
self.color = LUILabel.DEFAULT_COLOR
|
||||
else:
|
||||
self.color = color
|
||||
if shadow is None:
|
||||
shadow = LUILabel.DEFAULT_USE_SHADOW
|
||||
self._have_shadow = shadow
|
||||
if self._have_shadow:
|
||||
self._shadow_text = LUIText(
|
||||
self,
|
||||
text,
|
||||
font,
|
||||
font_size,
|
||||
0,
|
||||
0,
|
||||
wordwrap
|
||||
)
|
||||
self._shadow_text.top = 1
|
||||
self._shadow_text.color = (0,0,0,0.6)
|
||||
|
||||
def get_text_handle(self):
|
||||
""" Returns a handle to the internal used LUIText object """
|
||||
return self._text
|
||||
|
||||
text_handle = property(get_text_handle)
|
||||
|
||||
def get_text(self):
|
||||
""" Returns the current text of the label """
|
||||
return self._text.text
|
||||
|
||||
def set_text(self, text):
|
||||
""" Sets the text of the label """
|
||||
self._text.text = text
|
||||
if self._have_shadow:
|
||||
self._shadow_text.text = text
|
||||
|
||||
text = property(get_text, set_text)
|
||||
|
||||
def get_color(self):
|
||||
""" Returns the current color of the label's text """
|
||||
return self._text.color
|
||||
|
||||
def set_color(self, color):
|
||||
""" Sets the color of the label's text """
|
||||
self._text.color = color
|
||||
|
||||
color = property(get_color, set_color)
|
||||
105
ui/Builtin/LUILayouts.py
Normal file
@ -0,0 +1,105 @@
|
||||
|
||||
from __future__ import print_function, division
|
||||
|
||||
from LUIObject import LUIObject
|
||||
from LUISprite import LUISprite
|
||||
from LUIHorizontalLayout import LUIHorizontalLayout
|
||||
|
||||
from LUIInitialState import LUIInitialState
|
||||
|
||||
__all__ = ["LUICornerLayout", "LUIHorizontalStretchedLayout"]
|
||||
|
||||
class LUICornerLayout(LUIObject):
|
||||
|
||||
""" This is a layout which is used to combine 9 sprites to a single sprite,
|
||||
e.g. used for box shadow or frames."""
|
||||
|
||||
# List of all sprite identifiers required for the layout
|
||||
_MODES = ["TR", "Top", "TL", "Right", "Mid", "Left", "BR", "Bottom", "BL"]
|
||||
|
||||
def __init__(self, image_prefix="", **kwargs):
|
||||
""" Creates a new layout, using the image_prefix as prefix. """
|
||||
LUIObject.__init__(self)
|
||||
self.set_size("100%", "100%")
|
||||
self._prefix = image_prefix
|
||||
self._parts = {}
|
||||
for i in self._MODES:
|
||||
self._parts[i] = LUISprite(self, "blank", "skin")
|
||||
self._update_layout()
|
||||
LUIInitialState.init(self, kwargs)
|
||||
|
||||
def _update_layout(self):
|
||||
""" Updates the layouts components. """
|
||||
for i in self._MODES:
|
||||
self._parts[i].set_texture(self._prefix + i, "skin", resize=True)
|
||||
|
||||
# Top and Left
|
||||
self._parts["Top"].width = "100%"
|
||||
self._parts["Top"].margin = (0, self._parts["TR"].width, 0, self._parts["TL"].width)
|
||||
|
||||
self._parts["Left"].height = "100%"
|
||||
self._parts["Left"].margin = (self._parts["TL"].height, 0, self._parts["BL"].height, 0)
|
||||
|
||||
# Mid
|
||||
self._parts["Mid"].set_size("100%", "100%")
|
||||
self._parts["Mid"].margin = (self._parts["Top"].height, self._parts["Right"].width,
|
||||
self._parts["Bottom"].height, self._parts["Left"].width)
|
||||
|
||||
# Bottom and Right
|
||||
self._parts["Bottom"].width = "100%"
|
||||
self._parts["Bottom"].margin = (0, self._parts["BR"].width, 0, self._parts["BL"].width)
|
||||
self._parts["Bottom"].bottom = 0
|
||||
|
||||
self._parts["Right"].height = "100%"
|
||||
self._parts["Right"].margin = (self._parts["TR"].height, 0, self._parts["BR"].width, 0)
|
||||
self._parts["Right"].right = 0
|
||||
|
||||
# Corners
|
||||
self._parts["TL"].top_left = 0, 0
|
||||
self._parts["TR"].top_right = 0, 0
|
||||
self._parts["BL"].bottom_left = 0, 0
|
||||
self._parts["BR"].bottom_right = 0, 0
|
||||
|
||||
def set_prefix(self, prefix):
|
||||
""" Changes the texture of the layout """
|
||||
self._prefix = prefix
|
||||
self._update_layout()
|
||||
|
||||
def get_prefix(self):
|
||||
""" Returns the layouts texture prefix """
|
||||
return self._prefix
|
||||
|
||||
prefix = property(get_prefix, set_prefix)
|
||||
|
||||
|
||||
class LUIHorizontalStretchedLayout(LUIObject):
|
||||
|
||||
""" A layout which takes 3 sprites, a left sprite, a right sprite, and a
|
||||
middle sprite. While the left and right sprites remain untouched, the middle
|
||||
one will be stretched to fit the layout """
|
||||
|
||||
def __init__(self, parent=None, prefix="ButtonDefault", **kwargs):
|
||||
LUIObject.__init__(self)
|
||||
self._layout = LUIHorizontalLayout(self, spacing=0)
|
||||
self._layout.width = "100%"
|
||||
self._sprite_left = LUISprite(self._layout.cell(), "blank", "skin")
|
||||
self._sprite_mid = LUISprite(self._layout.cell('*'), "blank", "skin")
|
||||
self._sprite_right = LUISprite(self._layout.cell(), "blank", "skin")
|
||||
if parent is not None:
|
||||
self.parent = parent
|
||||
self.prefix = prefix
|
||||
LUIInitialState.init(self, kwargs)
|
||||
|
||||
def set_prefix(self, prefix):
|
||||
""" Sets the layout prefix, this controls which sprites will be used """
|
||||
self._sprite_left.set_texture(prefix + "_Left", "skin")
|
||||
self._sprite_mid.set_texture(prefix, "skin")
|
||||
self._sprite_right.set_texture(prefix + "_Right", "skin")
|
||||
self._sprite_mid.width = "100%"
|
||||
self._prefix = prefix
|
||||
|
||||
def get_prefix(self):
|
||||
""" Returns the layout prefix """
|
||||
return self._prefix
|
||||
|
||||
prefix = property(get_prefix, set_prefix)
|
||||
18
ui/Builtin/LUIObject.py
Normal file
@ -0,0 +1,18 @@
|
||||
"""
|
||||
|
||||
This is a wrapper file. It contains no actual implementation
|
||||
|
||||
"""
|
||||
|
||||
from panda3d.lui import LUIObject as _LUIObject
|
||||
from LUIInitialState import LUIInitialState
|
||||
|
||||
__all__ = ["LUIObject"]
|
||||
|
||||
class LUIObject(_LUIObject):
|
||||
""" This is a wrapper class for the C++ LUIObject class, to be able to
|
||||
use it in a more convenient way """
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
_LUIObject.__init__(self, *args)
|
||||
LUIInitialState.init(self, kwargs)
|
||||
72
ui/Builtin/LUIProgressbar.py
Normal file
@ -0,0 +1,72 @@
|
||||
|
||||
from LUIObject import LUIObject
|
||||
from LUISprite import LUISprite
|
||||
|
||||
from LUILayouts import LUIHorizontalStretchedLayout
|
||||
from LUILabel import LUILabel
|
||||
|
||||
class LUIProgressbar(LUIObject):
|
||||
|
||||
""" A simple progress bar """
|
||||
|
||||
def __init__(self, parent=None, width=200, value=50, show_label=True):
|
||||
""" Constructs a new progress bar. If show_label is True, a label indicating
|
||||
the current progress is shown """
|
||||
LUIObject.__init__(self)
|
||||
self.set_width(width)
|
||||
|
||||
self._bg_layout = LUIHorizontalStretchedLayout(
|
||||
parent=self, prefix="ProgressbarBg", width="100%")
|
||||
|
||||
self._fg_left = LUISprite(self, "ProgressbarFg_Left", "skin")
|
||||
self._fg_mid = LUISprite(self, "ProgressbarFg", "skin")
|
||||
self._fg_right = LUISprite(self, "ProgressbarFg_Right", "skin")
|
||||
self._fg_finish = LUISprite(self, "ProgressbarFg_Finish", "skin")
|
||||
|
||||
self._show_label = show_label
|
||||
self._progress_pixel = 0
|
||||
self._fg_finish.right = 0
|
||||
|
||||
if self._show_label:
|
||||
self._progress_label = LUILabel(parent=self, text=u"33 %")
|
||||
self._progress_label.centered = (True, True)
|
||||
|
||||
self.set_value(value)
|
||||
self._update_progress()
|
||||
|
||||
if parent is not None:
|
||||
self.parent = parent
|
||||
|
||||
def get_value(self):
|
||||
""" Returns the current value of the progress bar """
|
||||
return (self._progress_pixel / self.width * 100.0)
|
||||
|
||||
def set_value(self, val):
|
||||
""" Sets the value of the progress bar """
|
||||
val = max(0, min(100, val))
|
||||
self._progress_pixel = int(val / 100.0 * self.width)
|
||||
self._update_progress()
|
||||
|
||||
value = property(get_value, set_value)
|
||||
|
||||
def _update_progress(self):
|
||||
""" Internal method to update the progressbar """
|
||||
self._fg_finish.hide()
|
||||
|
||||
if self._progress_pixel <= self._fg_left.width + self._fg_right.width:
|
||||
self._fg_mid.hide()
|
||||
self._fg_right.left = self._fg_left.width
|
||||
else:
|
||||
self._fg_mid.show()
|
||||
self._fg_mid.left = self._fg_left.width
|
||||
self._fg_mid.width = self._progress_pixel - self._fg_right.width - self._fg_left.width
|
||||
self._fg_right.left = self._fg_mid.left + self._fg_mid.width
|
||||
|
||||
if self._progress_pixel >= self.width - self._fg_right.width:
|
||||
self._fg_finish.show()
|
||||
self._fg_finish.right = - (self.width - self._progress_pixel)
|
||||
self._fg_finish.clip_bounds = (0, self.width - self._progress_pixel, 0, 0)
|
||||
|
||||
if self._show_label:
|
||||
percentage = self._progress_pixel / self.width * 100.0
|
||||
self._progress_label.set_text("{} %".format(int(percentage)))
|
||||
91
ui/Builtin/LUIRadiobox.py
Normal file
@ -0,0 +1,91 @@
|
||||
|
||||
from __future__ import division
|
||||
|
||||
from LUIObject import LUIObject
|
||||
from LUISprite import LUISprite
|
||||
from LUIInitialState import LUIInitialState
|
||||
from LUILabel import LUILabel
|
||||
|
||||
class LUIRadiobox(LUIObject):
|
||||
|
||||
""" A radiobox which can be used in combination with a LUIRadioboxGroup """
|
||||
|
||||
def __init__(self, parent=None, group=None, value=None, active=False, label=u"Radiobox", **kwargs):
|
||||
""" Constructs a new radiobox. group should be a handle to a LUIRadioboxGroup.
|
||||
value will be the value returned by group.value, in case the box was
|
||||
selected. By default, the radiobox is not active. """
|
||||
assert group is not None, "LUIRadiobox needs a LUIRadioboxGroup!"
|
||||
LUIObject.__init__(self, x=0, y=0, solid=True)
|
||||
self._sprite = LUISprite(self, "Radiobox_Default", "skin")
|
||||
self._label = LUILabel(parent=self, text=label, margin=(0, 0, 0, 23),
|
||||
center_vertical=True)
|
||||
self._value = value
|
||||
self._active = False
|
||||
self._hovered = False
|
||||
self._group = group
|
||||
self._group.register_box(self)
|
||||
if active:
|
||||
self.set_active()
|
||||
if parent:
|
||||
self.parent = parent
|
||||
LUIInitialState.init(self, kwargs)
|
||||
|
||||
def on_click(self, event):
|
||||
""" Internal onclick handler. Do not override. """
|
||||
self.set_active()
|
||||
|
||||
def on_mouseover(self, event):
|
||||
""" Internal mouseover handler """
|
||||
self._hovered = True
|
||||
self._update_sprite()
|
||||
|
||||
def on_mouseout(self, event):
|
||||
""" Internal mouseout handler """
|
||||
self._hovered = False
|
||||
self._update_sprite()
|
||||
|
||||
def set_active(self):
|
||||
""" Internal function to set the radiobox active """
|
||||
if self._group is not None:
|
||||
self._group.set_active_box(self)
|
||||
else:
|
||||
self._update_state(True)
|
||||
|
||||
def get_value(self):
|
||||
""" Returns the value of the radiobox """
|
||||
return self._value
|
||||
|
||||
def set_value(self, value):
|
||||
""" Sets the value of the radiobox """
|
||||
self._value = value
|
||||
|
||||
value = property(get_value, set_value)
|
||||
|
||||
def get_label(self):
|
||||
""" Returns a handle to the label, so it can be modified (e.g. change
|
||||
its text) """
|
||||
return self._label
|
||||
|
||||
label = property(get_label)
|
||||
|
||||
def _update_state(self, active):
|
||||
""" Internal method to update the state of the radiobox. Called by the
|
||||
LUIRadioboxGroup """
|
||||
self._active = active
|
||||
self.trigger_event("changed")
|
||||
self._update_sprite()
|
||||
|
||||
def on_mousedown(self, event):
|
||||
""" Internal onmousedown handler. Do not override. """
|
||||
self._sprite.color = (0.86,0.86,0.86,1.0)
|
||||
|
||||
def on_mouseup(self, event):
|
||||
""" Internal onmouseup handler. Do not override. """
|
||||
self._sprite.color = (1,1,1,1)
|
||||
|
||||
def _update_sprite(self):
|
||||
""" Internal function to update the sprite of the radiobox """
|
||||
img = "Radiobox_Active" if self._active else "Radiobox_Default"
|
||||
if self._hovered:
|
||||
img += "Hover"
|
||||
self._sprite.set_texture(img, "skin")
|
||||
41
ui/Builtin/LUIRadioboxGroup.py
Normal file
@ -0,0 +1,41 @@
|
||||
|
||||
from LUIObject import LUIObject
|
||||
|
||||
class LUIRadioboxGroup(LUIObject):
|
||||
|
||||
""" Simple helper to group a bunch of LUIRadiobox and ensure only one is
|
||||
checked at one timem """
|
||||
|
||||
def __init__(self):
|
||||
""" Constructs a new group without any radioboxes inside """
|
||||
self._boxes = []
|
||||
self._selected_box = None
|
||||
|
||||
def register_box(self, box):
|
||||
""" Registers a box to the collection """
|
||||
if box not in self._boxes:
|
||||
self._boxes.append(box)
|
||||
|
||||
def set_active_box(self, active_box):
|
||||
""" Internal function to set the active box """
|
||||
for box in self._boxes:
|
||||
if box is not active_box:
|
||||
box._update_state(False)
|
||||
else:
|
||||
box._update_state(True)
|
||||
self._selected_box = active_box
|
||||
|
||||
def get_active_box(self):
|
||||
""" Returns the current selected box """
|
||||
return self._selected_box
|
||||
|
||||
active_box = property(get_active_box, set_active_box)
|
||||
|
||||
def get_active_value(self):
|
||||
""" Returns the value of the current selected box (or None if none is
|
||||
selected) """
|
||||
if self._selected_box is None:
|
||||
return None
|
||||
return self._selected_box.get_value()
|
||||
|
||||
active_value = property(get_active_value)
|
||||
8
ui/Builtin/LUIRegion.py
Normal file
@ -0,0 +1,8 @@
|
||||
"""
|
||||
|
||||
This is a wrapper file. It contains no actual implementation
|
||||
|
||||
"""
|
||||
|
||||
from panda3d.lui import LUIRegion as __LUIRegion
|
||||
LUIRegion = __LUIRegion
|
||||
8
ui/Builtin/LUIRoot.py
Normal file
@ -0,0 +1,8 @@
|
||||
"""
|
||||
|
||||
This is a wrapper file. It contains no actual implementation
|
||||
|
||||
"""
|
||||
|
||||
from panda3d.lui import LUIRoot as __LUIRoot
|
||||
LUIRoot = __LUIRoot
|
||||
155
ui/Builtin/LUIScrollableRegion.py
Normal file
@ -0,0 +1,155 @@
|
||||
|
||||
from LUIObject import LUIObject
|
||||
from LUISprite import LUISprite
|
||||
from LUIInitialState import LUIInitialState
|
||||
from LUILayouts import LUIHorizontalStretchedLayout
|
||||
|
||||
class LUIScrollableRegion(LUIObject):
|
||||
|
||||
""" Scrollable region, reparent elements to the .content_node to make them
|
||||
scroll. """
|
||||
|
||||
def __init__(self, parent=None, width=100, height=100, padding=10, **kwargs):
|
||||
LUIObject.__init__(self)
|
||||
self.set_size(width, height)
|
||||
self._content_parent = LUIObject(self)
|
||||
self._content_parent.set_size("100%", "100%")
|
||||
self._content_parent.clip_bounds = (0,0,0,0)
|
||||
|
||||
self._content_clip = LUIObject(self._content_parent, x=padding, y=padding)
|
||||
self._content_clip.set_size("100%", "100%")
|
||||
|
||||
self._content_scroller = LUIObject(self._content_clip)
|
||||
self._content_scroller.width = "100%"
|
||||
|
||||
|
||||
self._scrollbar = LUIObject(self, x=0, y=0, w=20)
|
||||
self._scrollbar.height = "100%"
|
||||
self._scrollbar.right = -10
|
||||
|
||||
self._scrollbar_bg = LUISprite(self._scrollbar, "blank", "skin")
|
||||
self._scrollbar_bg.color = (1,1,1,0.05)
|
||||
self._scrollbar_bg.set_size(3, "100%")
|
||||
self._scrollbar_bg.center_horizontal = True
|
||||
|
||||
# Handle
|
||||
self._scrollbar_handle = LUIObject(self._scrollbar, x=5, y=0, w=10)
|
||||
self._scroll_handle_top = LUISprite(self._scrollbar_handle, "ScrollbarHandle_Top", "skin")
|
||||
self._scroll_handle_mid = LUISprite(self._scrollbar_handle, "ScrollbarHandle", "skin")
|
||||
self._scroll_handle_bottom = LUISprite(self._scrollbar_handle, "ScrollbarHandle_Bottom", "skin")
|
||||
|
||||
self._scrollbar_handle.solid = True
|
||||
self._scrollbar.solid = True
|
||||
|
||||
self._scrollbar_handle.bind("mousedown", self._start_scrolling)
|
||||
self._scrollbar_handle.bind("mouseup", self._stop_scrolling)
|
||||
self._scrollbar.bind("mousedown", self._on_bar_click)
|
||||
self._scrollbar.bind("mouseup", self._stop_scrolling)
|
||||
|
||||
self._handle_dragging = False
|
||||
self._drag_start_y = 0
|
||||
|
||||
self._scroll_top_position = 0
|
||||
self._content_height = 400
|
||||
|
||||
# Scroll shadow
|
||||
self._scroll_shadow_top = LUIHorizontalStretchedLayout(parent=self, prefix="ScrollShadowTop", width="100%")
|
||||
self._scroll_shadow_bottom = LUIHorizontalStretchedLayout(parent=self, prefix="ScrollShadowBottom", width="100%")
|
||||
self._scroll_shadow_bottom.bottom = 0
|
||||
|
||||
self._handle_height = 100
|
||||
|
||||
if parent is not None:
|
||||
self.parent = parent
|
||||
|
||||
LUIInitialState.init(self, kwargs)
|
||||
self.content_node = self._content_scroller
|
||||
taskMgr.doMethodLater(0.05, lambda task: self._update(), "update_scrollbar")
|
||||
|
||||
def _on_bar_click(self, event):
|
||||
""" Internal handler when the user clicks on the scroll bar """
|
||||
self._scroll_to_bar_pixels(event.coordinates.y - self._scrollbar.abs_pos.y - self._handle_height / 2.0)
|
||||
self._update()
|
||||
self._start_scrolling(event)
|
||||
|
||||
def _start_scrolling(self, event):
|
||||
""" Internal method when we start scrolling """
|
||||
self.request_focus()
|
||||
if not self._handle_dragging:
|
||||
self._drag_start_y = event.coordinates.y
|
||||
self._handle_dragging = True
|
||||
|
||||
def _stop_scrolling(self, event):
|
||||
""" Internal handler when we should stop scrolling """
|
||||
if self._handle_dragging:
|
||||
self._handle_dragging = False
|
||||
self.blur()
|
||||
|
||||
def _scroll_to_bar_pixels(self, pixels):
|
||||
""" Internal method to convert from pixels to a relative position """
|
||||
offset = pixels * self._content_height / self.height
|
||||
self._scroll_top_position = offset
|
||||
self._scroll_top_position = max(0, min(self._content_height - self._content_clip.height, self._scroll_top_position))
|
||||
|
||||
def on_tick(self, event):
|
||||
""" Internal on tick handler """
|
||||
if self._handle_dragging:
|
||||
scroll_abs_pos = self._scrollbar.abs_pos
|
||||
clamped_coord_y = max(scroll_abs_pos.y, min(scroll_abs_pos.y + self.height, event.coordinates.y))
|
||||
offset = clamped_coord_y - self._drag_start_y
|
||||
self._drag_start_y = clamped_coord_y
|
||||
self._scroll_to_bar_pixels(self._scroll_top_position/self._content_height*self.height + offset)
|
||||
self._update()
|
||||
|
||||
def _set_handle_height(self, height):
|
||||
""" Internal method to set the scrollbar height """
|
||||
self._scroll_handle_mid.top = float(self._scroll_handle_top.height)
|
||||
|
||||
self._scroll_handle_mid.height = max(0.0, height - self._scroll_handle_top.height - self._scroll_handle_bottom.height)
|
||||
self._scroll_handle_bottom.top = self._scroll_handle_mid.height + self._scroll_handle_mid.top
|
||||
self._handle_height = height
|
||||
|
||||
def _update(self):
|
||||
""" Internal method to update the scroll bar """
|
||||
self._content_height = max(1, self._content_scroller.get_height() + 20)
|
||||
self._content_scroller.top = -self._scroll_top_position
|
||||
scrollbar_height = max(0.1, min(1.0, self._content_clip.height / self._content_height))
|
||||
scrollbar_height_px = scrollbar_height * self.height
|
||||
|
||||
self._set_handle_height(scrollbar_height_px)
|
||||
self._scrollbar_handle.top = self._scroll_top_position / self._content_height * self.height
|
||||
|
||||
top_alpha = max(0.0, min(1.0, self._scroll_top_position / 50.0))
|
||||
bottom_alpha = max(0.0, min(1.0, (self._content_height - self._scroll_top_position - self._content_clip.height) / 50.0 ))
|
||||
self._scroll_shadow_top.color = (1,1,1,top_alpha)
|
||||
self._scroll_shadow_bottom.color = (1,1,1,bottom_alpha)
|
||||
|
||||
if self._content_height <= self.height:
|
||||
self._scrollbar_handle.hide()
|
||||
else:
|
||||
self._scrollbar_handle.show()
|
||||
|
||||
def on_element_added(self):
|
||||
taskMgr.doMethodLater(0.05, lambda task: self._update(), "update_layout")
|
||||
|
||||
def get_scroll_percentage(self):
|
||||
""" Returns the current scroll height in percentage from 0 to 1 """
|
||||
return self._scroll_top_position / max(1, self._content_height - self._content_clip.height)
|
||||
|
||||
def set_scroll_percentage(self, percentage):
|
||||
""" Sets the scroll position in percentage, 0 means top and 1 means bottom """
|
||||
percentage = max(0.0, min(1.0, percentage))
|
||||
pixels = max(0.0, self._content_height - self._content_clip.height) * percentage
|
||||
self._scroll_top_position = pixels
|
||||
self._update()
|
||||
|
||||
scroll_percentage = property(get_scroll_percentage, set_scroll_percentage)
|
||||
|
||||
def scroll_to_bottom(self):
|
||||
""" Scrolls to the bottom of the frame """
|
||||
taskMgr.doMethodLater(0.07, lambda task: self.set_scroll_percentage(1.0), "scroll_to_bottom")
|
||||
|
||||
def scroll_to_top(self):
|
||||
""" Scrolls to the top of the frame """
|
||||
taskMgr.doMethodLater(0.07, lambda task: self.set_scroll_percentage(0.0), "scroll_to_top")
|
||||
|
||||
207
ui/Builtin/LUISelectbox.py
Normal file
@ -0,0 +1,207 @@
|
||||
|
||||
from LUIObject import LUIObject
|
||||
from LUISprite import LUISprite
|
||||
from LUILabel import LUILabel
|
||||
from LUILayouts import LUICornerLayout, LUIHorizontalStretchedLayout
|
||||
from LUIInitialState import LUIInitialState
|
||||
|
||||
from functools import partial
|
||||
|
||||
__all__ = ["LUISelectbox"]
|
||||
|
||||
class LUISelectbox(LUIObject):
|
||||
|
||||
""" Selectbox widget, showing several options whereas the user can select
|
||||
only one. """
|
||||
|
||||
def __init__(self, width=200, options=None, selected_option=None, **kwargs):
|
||||
""" Constructs a new selectbox with a given width """
|
||||
LUIObject.__init__(self, x=0, y=0, w=width+4, solid=True)
|
||||
LUIInitialState.init(self, kwargs)
|
||||
|
||||
# The selectbox has a small border, to correct this we move it
|
||||
self.margin.left = -2
|
||||
|
||||
self._bg_layout = LUIHorizontalStretchedLayout(parent=self, prefix="Selectbox", width="100%")
|
||||
|
||||
self._label_container = LUIObject(self, x=10, y=0)
|
||||
self._label_container.set_size("100%", "100%")
|
||||
self._label_container.clip_bounds = (0,0,0,0)
|
||||
self._label = LUILabel(parent=self._label_container, text=u"Select an option ..")
|
||||
self._label.center_vertical = True
|
||||
|
||||
self._drop_menu = LUISelectdrop(parent=self, width=width)
|
||||
self._drop_menu.top = self._bg_layout._sprite_right.height - 7
|
||||
self._drop_menu.topmost = True
|
||||
|
||||
self._drop_open = False
|
||||
self._drop_menu.hide()
|
||||
|
||||
self._options = []
|
||||
self._current_option_id = None
|
||||
|
||||
if options is not None:
|
||||
self._options = options
|
||||
|
||||
self._select_option(selected_option)
|
||||
|
||||
def get_selected_option(self):
|
||||
""" Returns the selected option """
|
||||
return self._current_option_id
|
||||
|
||||
def set_selected_option(self, option_id):
|
||||
""" Sets the selected option """
|
||||
raise NotImplementedError()
|
||||
|
||||
selected_option = property(get_selected_option, set_selected_option)
|
||||
|
||||
def _render_options(self):
|
||||
""" Internal method to render all available options """
|
||||
self._drop_menu._render_options(self._options)
|
||||
|
||||
def get_options(self):
|
||||
""" Returns the list of options """
|
||||
return self._options
|
||||
|
||||
def set_options(self, options):
|
||||
""" Sets the list of options, options should be a list containing entries
|
||||
whereas each entry is a tuple in the format (option_id, option_label).
|
||||
The option ID can be an arbitrary object, and will not get modified. """
|
||||
self._options = options
|
||||
self._current_option_id = None
|
||||
self._render_options()
|
||||
|
||||
options = property(get_options, set_options)
|
||||
|
||||
def _select_option(self, opt_id):
|
||||
""" Internal method to select an option """
|
||||
self._label.alpha = 1.0
|
||||
for elem_opt_id, opt_val in self._options:
|
||||
if opt_id == elem_opt_id:
|
||||
self._label.text = opt_val
|
||||
self._current_option_id = opt_id
|
||||
return
|
||||
self._label.alpha = 0.3
|
||||
|
||||
# def on_mouseover(self, event):
|
||||
# """ Internal handle when the select-knob was hovered """
|
||||
# self._bg_layout.color = (0.9,0.9,0.9,1.0)
|
||||
|
||||
# def on_mouseout(self, event):
|
||||
# """ Internal handle when the select-knob was no longer hovered """
|
||||
# self._bg_layout.color = (1,1,1,1.0)
|
||||
|
||||
def on_click(self, event):
|
||||
""" On-Click handler """
|
||||
self.request_focus()
|
||||
if self._drop_open:
|
||||
self._close_drop()
|
||||
else:
|
||||
self._open_drop()
|
||||
|
||||
def on_mousedown(self, event):
|
||||
""" Mousedown handler """
|
||||
self._bg_layout.alpha = 0.9
|
||||
|
||||
def on_mouseup(self, event):
|
||||
""" Mouseup handler """
|
||||
self._bg_layout.alpha = 1
|
||||
|
||||
def on_blur(self, event):
|
||||
""" Internal handler when the selectbox lost focus """
|
||||
if not self._drop_menu.focused:
|
||||
self._close_drop()
|
||||
|
||||
def _open_drop(self):
|
||||
""" Internal method to show the dropdown menu """
|
||||
if not self._drop_open:
|
||||
self._render_options()
|
||||
self._drop_menu.show()
|
||||
self.request_focus()
|
||||
self._drop_open = True
|
||||
|
||||
def _close_drop(self):
|
||||
""" Internal method to close the dropdown menu """
|
||||
if self._drop_open:
|
||||
self._drop_menu.hide()
|
||||
self._drop_open = False
|
||||
|
||||
def _on_option_selected(self, opt_id):
|
||||
""" Internal method when an option got selected """
|
||||
self._select_option(opt_id)
|
||||
self._close_drop()
|
||||
|
||||
|
||||
class LUISelectdrop(LUIObject):
|
||||
|
||||
""" Internal class used by the selectbox, representing the dropdown menu """
|
||||
|
||||
def __init__(self, parent, width=200):
|
||||
LUIObject.__init__(self, x=0, y=0, w=width, h=1, solid=True)
|
||||
|
||||
self._layout = LUICornerLayout(parent=self, image_prefix="Selectdrop_",
|
||||
width=width + 10, height=100)
|
||||
self._layout.margin.left = -3
|
||||
|
||||
self._opener = LUISprite(self, "SelectboxOpen_Right", "skin")
|
||||
self._opener.right = -4
|
||||
self._opener.top = -25
|
||||
self._opener.z_offset = 3
|
||||
|
||||
self._container = LUIObject(self._layout, 0, 0, 0, 0)
|
||||
self._container.width = self.width
|
||||
self._container.clip_bounds = (0,0,0,0)
|
||||
self._container.left = 5
|
||||
self._container.solid = True
|
||||
self._container.bind("mousedown", lambda *args: self.request_focus())
|
||||
|
||||
self._selectbox = parent
|
||||
self._option_focus = False
|
||||
self.parent = self._selectbox
|
||||
|
||||
def _on_opt_over(self, event):
|
||||
""" Inernal handler when an option got hovered """
|
||||
event.sender.color = (0,0,0,0.1)
|
||||
|
||||
def _on_opt_out(self, event):
|
||||
""" Inernal handler when an option got no longer hovered """
|
||||
event.sender.color = (0,0,0,0)
|
||||
|
||||
def _on_opt_click(self, opt_id, event):
|
||||
""" Internal handler when an option got clicked """
|
||||
self._selectbox._on_option_selected(opt_id)
|
||||
|
||||
def _render_options(self, options):
|
||||
""" Internal method to update the options """
|
||||
num_visible_options = min(30, len(options))
|
||||
offset_top = 6
|
||||
self._layout.height = num_visible_options * 30 + offset_top + 11
|
||||
self._container.height = num_visible_options * 30 + offset_top + 1
|
||||
self._container.remove_all_children()
|
||||
|
||||
current_y = offset_top
|
||||
for opt_id, opt_val in options:
|
||||
opt_container = LUIObject(self._container, x=0, y=current_y, w=self._container.width - 30, h=30)
|
||||
|
||||
opt_bg = LUISprite(opt_container, "blank", "skin")
|
||||
opt_bg.width = self._container.width
|
||||
opt_bg.height = opt_container.height
|
||||
opt_bg.color = (0,0,0,0)
|
||||
opt_bg.bind("mouseover", self._on_opt_over)
|
||||
opt_bg.bind("mouseout", self._on_opt_out)
|
||||
opt_bg.bind("mousedown", lambda *args: self.request_focus())
|
||||
opt_bg.bind("click", partial(self._on_opt_click, opt_id))
|
||||
opt_bg.solid = True
|
||||
|
||||
opt_label = LUILabel(parent=opt_container, text=opt_val)
|
||||
opt_label.top = 8
|
||||
opt_label.left = 8
|
||||
|
||||
if opt_id == self._selectbox.selected_option:
|
||||
opt_label.color = (0.6, 0.9, 0.4, 1.0)
|
||||
|
||||
divider = LUISprite(opt_container, "SelectdropDivider", "skin")
|
||||
divider.top = 30 - divider.height / 2
|
||||
divider.width = self._container.width
|
||||
|
||||
current_y += 30
|
||||
53
ui/Builtin/LUISkin.py
Normal file
@ -0,0 +1,53 @@
|
||||
|
||||
import os
|
||||
from os.path import join
|
||||
|
||||
from panda3d.core import Filename
|
||||
from panda3d.lui import LUIFontPool, LUIAtlasPool
|
||||
|
||||
class LUISkin:
|
||||
|
||||
""" Abstract class, each skin derives from this class """
|
||||
|
||||
skin_location = ""
|
||||
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
def load(self):
|
||||
""" Skins should override this. Each skin should at least provide the fonts
|
||||
'default' and 'label', and at least one atlas named 'skin' """
|
||||
raise NotImplementedError()
|
||||
|
||||
def get_resource(self, pth):
|
||||
""" Turns a relative path into an absolute one, using the skin_location """
|
||||
return Filename.from_os_specific(join(self.skin_location, pth)).get_fullpath()
|
||||
|
||||
|
||||
class LUIDefaultSkin(LUISkin):
|
||||
|
||||
""" The default skin which comes with LUI """
|
||||
|
||||
skin_location = os.path.join(os.path.dirname(os.path.abspath(__file__)), "../Skins/Default/")
|
||||
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
def load(self):
|
||||
LUIFontPool.get_global_ptr().register_font(
|
||||
"default", loader.loadFont(self.get_resource("font/SourceSansPro-Semibold.ttf")))
|
||||
|
||||
labelFont = loader.loadFont(self.get_resource("font/SourceSansPro-Semibold.ttf"))
|
||||
labelFont.setPixelsPerUnit(32)
|
||||
|
||||
LUIFontPool.get_global_ptr().register_font(
|
||||
"label", labelFont)
|
||||
|
||||
headerFont = loader.loadFont(self.get_resource("font/SourceSansPro-Light.ttf"))
|
||||
headerFont.setPixelsPerUnit(80)
|
||||
|
||||
LUIFontPool.get_global_ptr().register_font("header", headerFont)
|
||||
|
||||
LUIAtlasPool.get_global_ptr().load_atlas("skin",
|
||||
self.get_resource("res/atlas.txt"),
|
||||
self.get_resource("res/atlas.png"))
|
||||
214
ui/Builtin/LUISlider.py
Normal file
@ -0,0 +1,214 @@
|
||||
|
||||
from LUIObject import LUIObject
|
||||
from LUISprite import LUISprite
|
||||
from LUIInitialState import LUIInitialState
|
||||
from LUILayouts import LUIHorizontalStretchedLayout
|
||||
|
||||
class LUISlider(LUIObject):
|
||||
|
||||
""" Slider which can be used to control values """
|
||||
KNOB_Y_OFFSET = -5.0
|
||||
|
||||
def __init__(self, parent=None, filled=True, min_value=0.0, max_value=1.0, width=100.0, value=None, **kwargs):
|
||||
""" Constructs a new slider. If filled is True, the part behind the knob
|
||||
will be solid """
|
||||
LUIObject.__init__(self, x=0, y=0, solid=True)
|
||||
self.set_width(width)
|
||||
self._knob = LUISprite(self, "SliderKnob", "skin")
|
||||
self._knob.z_offset = 2
|
||||
self._knob.solid = True
|
||||
|
||||
# Construct the background
|
||||
self._slider_bg = LUIHorizontalStretchedLayout(parent=self, prefix="SliderBg", center_vertical=True, width="100%", margin=(-1, 0, 0, 0))
|
||||
|
||||
self._filled = filled
|
||||
self._min_value = min_value
|
||||
self._max_value = max_value
|
||||
|
||||
self._side_margin = self._knob.width / 4
|
||||
self._effective_width = self.width - 2 * self._side_margin
|
||||
|
||||
if self._filled:
|
||||
self._slider_fill = LUIObject(self)
|
||||
self._fill_left = LUISprite(self._slider_fill, "SliderBgFill_Left", "skin")
|
||||
self._fill_mid = LUISprite(self._slider_fill, "SliderBgFill", "skin")
|
||||
self._fill_mid.left = self._fill_left.width
|
||||
self._slider_fill.z_offset = 1
|
||||
self._slider_fill.center_vertical = True
|
||||
self._slider_fill.width = "100%"
|
||||
self._slider_fill.height = "100%"
|
||||
|
||||
if parent is not None:
|
||||
self.parent = parent
|
||||
|
||||
# Handle various events
|
||||
self._knob.bind("mousedown", self._start_drag)
|
||||
self._knob.bind("mousemove", self._update_drag)
|
||||
self._knob.bind("mouseup", self._stop_drag)
|
||||
self._knob.bind("keydown", self._on_keydown)
|
||||
self._knob.bind("blur", self._stop_drag)
|
||||
self._knob.bind("keyrepeat", self._on_keydown)
|
||||
|
||||
self._drag_start_pos = None
|
||||
self._dragging = False
|
||||
self._drag_start_val = 0
|
||||
self.current_val = 10
|
||||
|
||||
# Set initial value
|
||||
if value is None:
|
||||
self.set_value( (self._min_value + self._max_value) / 2.0 )
|
||||
else:
|
||||
self.set_value(value)
|
||||
|
||||
self._update_knob()
|
||||
|
||||
LUIInitialState.init(self, kwargs)
|
||||
self._refresh_layout()
|
||||
|
||||
def on_click(self, event):
|
||||
""" Internal on click handler """
|
||||
# I don't like this behaviour
|
||||
# relative_pos = self.get_relative_pos(event.coordinates)
|
||||
# if not self._dragging:
|
||||
# self._set_current_val(relative_pos.x)
|
||||
|
||||
def _update_knob(self):
|
||||
""" Internal method to update the slider knob """
|
||||
self._knob.left = self.current_val - (self._knob.width / 2) + self._side_margin
|
||||
if self._filled:
|
||||
knob_center = self._knob.left + (self._knob.width / 2)
|
||||
fill_len = max(0, knob_center - self._fill_left.width)
|
||||
self._fill_mid.width = fill_len
|
||||
if hasattr(self._slider_fill, 'left'):
|
||||
self._slider_fill.left = 0
|
||||
|
||||
def _refresh_layout(self):
|
||||
"""Recalculate slider layout after size changes."""
|
||||
# Guard for early init before internal parts are created
|
||||
if not hasattr(self, "_knob"):
|
||||
return
|
||||
try:
|
||||
# Use half knob width so knob center aligns with bar ends
|
||||
self._side_margin = self._knob.width / 2
|
||||
except Exception:
|
||||
self._side_margin = 0
|
||||
try:
|
||||
self._effective_width = max(1, self.width - (self._knob.width if hasattr(self, "_knob") else 0))
|
||||
except Exception:
|
||||
self._effective_width = 1
|
||||
|
||||
if hasattr(self, "_slider_bg"):
|
||||
if hasattr(self._slider_bg, 'width'):
|
||||
self._slider_bg.width = "100%"
|
||||
if hasattr(self._slider_bg, 'height'):
|
||||
self._slider_bg.height = "100%"
|
||||
if hasattr(self._slider_bg, 'center_vertical'):
|
||||
self._slider_bg.center_vertical = True
|
||||
if hasattr(self._knob, 'center_vertical'):
|
||||
self._knob.center_vertical = True
|
||||
# Explicit vertical centering for knob to avoid mismatch after resize
|
||||
try:
|
||||
base_h = self.height
|
||||
base_top = 0
|
||||
if hasattr(self, "_slider_bg") and self._slider_bg is not None:
|
||||
bg_h = getattr(self._slider_bg, "height", None)
|
||||
if bg_h is not None and bg_h != "100%":
|
||||
base_h = bg_h
|
||||
base_top = getattr(self._slider_bg, "top", 0) or 0
|
||||
if hasattr(self._knob, "height"):
|
||||
# Visual offset compensates for the skin's knob pivot
|
||||
self._knob.top = (base_h - self._knob.height) / 2.0 + self.KNOB_Y_OFFSET
|
||||
except Exception:
|
||||
pass
|
||||
if self._filled and hasattr(self, '_slider_fill'):
|
||||
self._slider_fill.center_vertical = True
|
||||
try:
|
||||
base_h = self.height
|
||||
base_top = 0
|
||||
if hasattr(self, "_slider_bg") and self._slider_bg is not None:
|
||||
bg_h = getattr(self._slider_bg, "height", None)
|
||||
if bg_h is not None and bg_h != "100%":
|
||||
base_h = bg_h
|
||||
base_top = getattr(self._slider_bg, "top", 0) or 0
|
||||
if hasattr(self._slider_fill, 'height'):
|
||||
self._slider_fill.top = base_top + (base_h - self._slider_fill.height) / 2.0 + self.KNOB_Y_OFFSET
|
||||
except Exception:
|
||||
pass
|
||||
if hasattr(self._slider_fill, 'width'):
|
||||
self._slider_fill.width = "100%"
|
||||
if hasattr(self._slider_fill, 'height'):
|
||||
self._slider_fill.height = "100%"
|
||||
|
||||
try:
|
||||
self.current_val = max(0, min(self.current_val, self._effective_width))
|
||||
except Exception:
|
||||
pass
|
||||
self._update_knob()
|
||||
|
||||
def set_width(self, width):
|
||||
"""Set slider width and refresh layout."""
|
||||
self.width = width
|
||||
if hasattr(self, "_slider_bg"):
|
||||
self._refresh_layout()
|
||||
|
||||
def set_height(self, height):
|
||||
"""Set slider height and refresh layout."""
|
||||
self.height = height
|
||||
if hasattr(self, "_slider_bg"):
|
||||
self._refresh_layout()
|
||||
|
||||
def _set_current_val(self, pixels):
|
||||
""" Internal method to set the current value in pixels """
|
||||
pixels = max(0, min(self._effective_width, pixels))
|
||||
self.current_val = pixels
|
||||
self.trigger_event("changed")
|
||||
self._update_knob()
|
||||
|
||||
def _start_drag(self, event):
|
||||
""" Internal drag start handler """
|
||||
self._knob.request_focus()
|
||||
if not self._dragging:
|
||||
self._drag_start_pos = event.coordinates
|
||||
self._dragging = True
|
||||
self._drag_start_val = self.current_val
|
||||
self._knob.color = (0.8,0.8,0.8,1.0)
|
||||
|
||||
def set_value(self, value):
|
||||
""" Sets the value of the slider, should be between minimum and maximum. """
|
||||
scaled = (float(value) - float(self._min_value)) \
|
||||
/ (float(self._max_value) - float(self._min_value)) \
|
||||
* self._effective_width
|
||||
self._set_current_val(scaled)
|
||||
|
||||
def get_value(self):
|
||||
""" Returns the current value of the slider """
|
||||
return (self.current_val / float(self._effective_width)) \
|
||||
* (float(self._max_value) - float(self._min_value)) \
|
||||
+ self._min_value
|
||||
|
||||
value = property(get_value, set_value)
|
||||
|
||||
def _on_keydown(self, event):
|
||||
""" Internal keydown handler """
|
||||
if event.message == "arrow_right":
|
||||
self._set_current_val(self.current_val + 2)
|
||||
elif event.message == "arrow_left":
|
||||
self._set_current_val(self.current_val - 2)
|
||||
elif event.message == "escape":
|
||||
self.current_val = self._drag_start_val
|
||||
self._stop_drag(event)
|
||||
self._update_knob()
|
||||
|
||||
def _update_drag(self, event):
|
||||
""" Internal drag handler """
|
||||
if self._dragging:
|
||||
dragOffset = event.coordinates.x - self._drag_start_pos.x
|
||||
finalValue = self._drag_start_val + dragOffset
|
||||
self._set_current_val(finalValue)
|
||||
|
||||
def _stop_drag(self, event):
|
||||
""" Internal drag stop handelr """
|
||||
self._drag_start_pos = None
|
||||
self._dragging = False
|
||||
self._drag_start_val = self.current_val
|
||||
self._knob.color = (1,1,1,1)
|
||||
18
ui/Builtin/LUISprite.py
Normal file
@ -0,0 +1,18 @@
|
||||
"""
|
||||
|
||||
This is a wrapper file. It contains no actual implementation
|
||||
|
||||
"""
|
||||
|
||||
from panda3d.lui import LUISprite as _LUISprite
|
||||
from LUIInitialState import LUIInitialState
|
||||
|
||||
__all__ = ["LUISprite"]
|
||||
|
||||
class LUISprite(_LUISprite):
|
||||
""" This is a wrapper class for the C++ LUISprite class, to be able to
|
||||
use it in a more convenient way """
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
_LUISprite.__init__(self, *args)
|
||||
LUIInitialState.init(self, kwargs)
|
||||
30
ui/Builtin/LUISpriteButton.py
Normal file
@ -0,0 +1,30 @@
|
||||
|
||||
from LUIObject import LUIObject
|
||||
from LUISprite import LUISprite
|
||||
from LUIInitialState import LUIInitialState
|
||||
|
||||
class LUISpriteButton(LUIObject):
|
||||
|
||||
""" Simple button that uses only two images: Default and focus. """
|
||||
|
||||
def __init__(self, template="ButtonDefault", **kwargs):
|
||||
LUIObject.__init__(self, x=0, y=0, solid=True)
|
||||
self._template = template
|
||||
self._button_sprite = LUISprite(self, template, "skin")
|
||||
if 'width' in kwargs:
|
||||
self._button_sprite.width = kwargs['width']
|
||||
if 'height' in kwargs:
|
||||
self._button_sprite.height = kwargs['height']
|
||||
LUIInitialState.init(self, kwargs)
|
||||
|
||||
def on_mousedown(self, event):
|
||||
""" Internal on_mousedown handler. Do not override """
|
||||
self._button_sprite.set_texture(self.template + "Focus", "skin", resize=False)
|
||||
|
||||
def on_mouseup(self, event):
|
||||
""" Internal on_mouseup handler. Do not override """
|
||||
self._button_sprite.set_texture(self.template, "skin", resize=False)
|
||||
|
||||
def on_click(self, event):
|
||||
""" Internal onclick handler. Do not override """
|
||||
self.trigger_event("changed")
|
||||
87
ui/Builtin/LUITabbedFrame.py
Normal file
@ -0,0 +1,87 @@
|
||||
from LUIFrame import LUIFrame
|
||||
from LUILabel import LUILabel
|
||||
from LUIObject import LUIObject
|
||||
from LUIVerticalLayout import LUIVerticalLayout
|
||||
from LUIHorizontalLayout import LUIHorizontalLayout
|
||||
|
||||
class LUITabbedFrame(LUIFrame):
|
||||
def __init__(self, **kwargs):
|
||||
super(LUITabbedFrame, self).__init__(**kwargs)
|
||||
|
||||
# The main window layout
|
||||
bar_spacing = kwargs.get('bar_spacing', 3)
|
||||
self.root_layout = LUIVerticalLayout(parent = self,
|
||||
spacing = bar_spacing)
|
||||
self.root_layout.height = "100%"
|
||||
self.root_layout.width = "100%"
|
||||
self.root_layout.margin = 0
|
||||
|
||||
# The header bar
|
||||
header_spacing = kwargs.get('header_spacing', 3)
|
||||
self.header_bar = LUIHorizontalLayout(parent = self.root_layout.cell("?"),
|
||||
spacing = header_spacing)
|
||||
self.root_layout.add(self.header_bar, "?")
|
||||
self.header_to_frame = {}
|
||||
self.current_frame = None
|
||||
|
||||
# The main window contents
|
||||
self.main_frame = LUIObject()
|
||||
self.main_frame.height = "100%"
|
||||
self.main_frame.width = "100%"
|
||||
self.main_frame.margin = 0
|
||||
# self.main_frame.padding = 0
|
||||
self.root_layout.add(self.main_frame, "*")
|
||||
self.bind("expose", self.on_expose)
|
||||
self.bind("unexpose", self.on_unexpose)
|
||||
|
||||
def add(self, header, frame):
|
||||
# header
|
||||
if isinstance(header, str):
|
||||
header = LUILabel(text = header)
|
||||
self.header_bar.add(header, "?")
|
||||
self.header_to_frame[header] = frame
|
||||
header.solid = True
|
||||
header.bind("click", self._change_to_tab)
|
||||
# Frame
|
||||
frame.parent = self.main_frame
|
||||
frame.width = "100%"
|
||||
frame.height = "100%"
|
||||
# Put frame in front
|
||||
if self.current_frame is None:
|
||||
self.current_frame = frame
|
||||
self.current_frame.show()
|
||||
else:
|
||||
frame.hide()
|
||||
return header
|
||||
|
||||
def _find_header_index(self, header):
|
||||
for idx, child in enumerate(self.header_bar.children):
|
||||
if any([grandchild == header for grandchild in child.children]):
|
||||
break
|
||||
else:
|
||||
raise ValueError("Given object is not a header bar item.")
|
||||
return idx
|
||||
|
||||
def remove(self, header):
|
||||
idx = self._find_header_index(header)
|
||||
self.header_bar.remove_cell(idx)
|
||||
frame = self.header_to_frame[header]
|
||||
frame.parent = None
|
||||
del self.header_to_frame[header]
|
||||
if self.current_frame == frame:
|
||||
self.current_frame = None
|
||||
|
||||
def _change_to_tab(self, lui_event):
|
||||
header = lui_event.sender
|
||||
if self.current_frame is not None:
|
||||
self.current_frame.trigger_event("unexpose")
|
||||
self.current_frame.hide()
|
||||
self.current_frame = self.header_to_frame[header]
|
||||
self.current_frame.show()
|
||||
self.current_frame.trigger_event("expose")
|
||||
|
||||
def on_expose(self, event):
|
||||
self.current_frame.trigger_event("expose")
|
||||
|
||||
def on_unexpose(self, event):
|
||||
self.current_frame.trigger_event("unexpose")
|
||||
20
ui/Builtin/LUIVerticalLayout.py
Normal file
@ -0,0 +1,20 @@
|
||||
"""
|
||||
|
||||
This is a wrapper file. It contains no actual implementation
|
||||
|
||||
"""
|
||||
|
||||
from panda3d.lui import LUIVerticalLayout as _LUIVerticalLayout
|
||||
from LUIInitialState import LUIInitialState
|
||||
|
||||
__all__ = ["LUIVerticalLayout"]
|
||||
|
||||
|
||||
class LUIVerticalLayout(_LUIVerticalLayout):
|
||||
""" This is a wrapper class for the C++ LUIVerticalLayout class, to be
|
||||
able to use it in a more convenient way. It leverages LUIInitialState
|
||||
to be able to pass arbitrary keyword arguments. """
|
||||
|
||||
def __init__(self, parent=None, spacing=0.0, **kwargs):
|
||||
_LUIVerticalLayout.__init__(self, parent, spacing)
|
||||
LUIInitialState.init(self, kwargs)
|
||||
85
ui/Builtin/RectTransform.py
Normal file
@ -0,0 +1,85 @@
|
||||
"""RectTransform - 一个轻量级的 Unity-like RectTransform 实现(Python 层 MVP)
|
||||
|
||||
提供 anchors/pivot/anchored_position/size_delta 的数据结构和
|
||||
把相对锚点映射到像素矩形的计算方法。
|
||||
|
||||
此实现不修改底层 C++ LUI,而是在 Python 层计算并把结果应用到
|
||||
`LUIObject` 的 `left/top/width/height` 属性上。
|
||||
"""
|
||||
from typing import Tuple
|
||||
|
||||
class RectTransform(object):
|
||||
def __init__(self,
|
||||
anchor_min=(0.5, 0.5),
|
||||
anchor_max=(0.5, 0.5),
|
||||
pivot=(0.5, 0.5),
|
||||
anchored_position=(0.0, 0.0),
|
||||
size_delta=(100.0, 30.0)):
|
||||
# 值域: anchor/pivot 为 0..1,anchored_position/size_delta 为像素
|
||||
self.anchor_min = tuple(anchor_min)
|
||||
self.anchor_max = tuple(anchor_max)
|
||||
self.pivot = tuple(pivot)
|
||||
self.anchored_position = tuple(anchored_position)
|
||||
self.size_delta = tuple(size_delta)
|
||||
|
||||
def compute_pixel_rect(self, parent_rect: Tuple[float, float, float, float]):
|
||||
"""
|
||||
将 RectTransform 转换为相对于父节点的像素矩形 (left, top, width, height)
|
||||
|
||||
parent_rect: (parent_left, parent_top, parent_width, parent_height)
|
||||
"""
|
||||
p_left, p_top, p_w, p_h = parent_rect
|
||||
|
||||
a_min_x = self.anchor_min[0]
|
||||
a_min_y = self.anchor_min[1]
|
||||
a_max_x = self.anchor_max[0]
|
||||
a_max_y = self.anchor_max[1]
|
||||
|
||||
# 计算锚框的像素坐标(left/top 和 right/bottom)
|
||||
anchor_left = p_left + a_min_x * p_w
|
||||
anchor_right = p_left + a_max_x * p_w
|
||||
anchor_top = p_top + a_min_y * p_h
|
||||
anchor_bottom = p_top + a_max_y * p_h
|
||||
|
||||
# 宽高
|
||||
if abs(a_max_x - a_min_x) < 1e-6:
|
||||
# 固定锚点 -> 使用 size_delta.x
|
||||
width = float(self.size_delta[0])
|
||||
# anchored_position.x 表示相对于锚点框的位置(像素)
|
||||
left = anchor_left + float(self.anchored_position[0]) - self.pivot[0] * width
|
||||
else:
|
||||
# 拉伸到锚框再加上 size_delta.x
|
||||
width = (anchor_right - anchor_left) + float(self.size_delta[0])
|
||||
# anchored_position.x 表示锚框内的像素偏移
|
||||
left = anchor_left + float(self.anchored_position[0])
|
||||
|
||||
if abs(a_max_y - a_min_y) < 1e-6:
|
||||
height = float(self.size_delta[1])
|
||||
# 注意:engine 中 top 增大表示向下,所以 anchored_position.y 保持像素系
|
||||
top = anchor_top + float(self.anchored_position[1]) - self.pivot[1] * height
|
||||
else:
|
||||
height = (anchor_bottom - anchor_top) + float(self.size_delta[1])
|
||||
top = anchor_top + float(self.anchored_position[1])
|
||||
|
||||
return (left, top, width, height)
|
||||
|
||||
def apply_to_lui(self, lui_obj, parent_rect: Tuple[float, float, float, float]):
|
||||
"""计算并应用到给定的 LUIObject(设置 left/top/width/height)"""
|
||||
left, top, width, height = self.compute_pixel_rect(parent_rect)
|
||||
try:
|
||||
lui_obj.left = left
|
||||
lui_obj.top = top
|
||||
lui_obj.width = width
|
||||
lui_obj.height = height
|
||||
except Exception:
|
||||
# 忽略不能设置的属性,调用者应保证对象支持这些属性
|
||||
pass
|
||||
|
||||
def to_dict(self):
|
||||
return {
|
||||
'anchor_min': tuple(self.anchor_min),
|
||||
'anchor_max': tuple(self.anchor_max),
|
||||
'pivot': tuple(self.pivot),
|
||||
'anchored_position': tuple(self.anchored_position),
|
||||
'size_delta': tuple(self.size_delta),
|
||||
}
|
||||
0
ui/Builtin/__init__.py
Normal file
5
ui/Skins/Default/GenerateAtlas.bat
Normal file
@ -0,0 +1,5 @@
|
||||
@echo off
|
||||
|
||||
cd res
|
||||
ppython ../../../Misc/LUIAtlasGen.py
|
||||
pause
|
||||
0
ui/Skins/Default/__init__.py
Normal file
BIN
ui/Skins/Default/font/SourceSansPro-Black.ttf
Normal file
BIN
ui/Skins/Default/font/SourceSansPro-BlackIt.ttf
Normal file
BIN
ui/Skins/Default/font/SourceSansPro-Bold.ttf
Normal file
BIN
ui/Skins/Default/font/SourceSansPro-BoldIt.ttf
Normal file
BIN
ui/Skins/Default/font/SourceSansPro-ExtraLight.ttf
Normal file
BIN
ui/Skins/Default/font/SourceSansPro-ExtraLightIt.ttf
Normal file
BIN
ui/Skins/Default/font/SourceSansPro-It.ttf
Normal file
BIN
ui/Skins/Default/font/SourceSansPro-Light.ttf
Normal file
BIN
ui/Skins/Default/font/SourceSansPro-LightIt.ttf
Normal file
BIN
ui/Skins/Default/font/SourceSansPro-Regular.ttf
Normal file
BIN
ui/Skins/Default/font/SourceSansPro-Semibold.ttf
Normal file
BIN
ui/Skins/Default/font/SourceSansPro-SemiboldIt.ttf
Normal file
BIN
ui/Skins/Default/res/ButtonDefault.png
Normal file
|
After Width: | Height: | Size: 2.8 KiB |
BIN
ui/Skins/Default/res/ButtonDefaultFocus.png
Normal file
|
After Width: | Height: | Size: 2.8 KiB |
BIN
ui/Skins/Default/res/ButtonDefaultFocus_Left.png
Normal file
|
After Width: | Height: | Size: 2.9 KiB |
BIN
ui/Skins/Default/res/ButtonDefaultFocus_Right.png
Normal file
|
After Width: | Height: | Size: 2.9 KiB |
BIN
ui/Skins/Default/res/ButtonDefault_Left.png
Normal file
|
After Width: | Height: | Size: 2.9 KiB |
BIN
ui/Skins/Default/res/ButtonDefault_Right.png
Normal file
|
After Width: | Height: | Size: 2.9 KiB |
BIN
ui/Skins/Default/res/ButtonGreen.png
Normal file
|
After Width: | Height: | Size: 2.8 KiB |
BIN
ui/Skins/Default/res/ButtonGreenFocus.png
Normal file
|
After Width: | Height: | Size: 2.8 KiB |
BIN
ui/Skins/Default/res/ButtonGreenFocus_Left.png
Normal file
|
After Width: | Height: | Size: 3.0 KiB |
BIN
ui/Skins/Default/res/ButtonGreenFocus_Right.png
Normal file
|
After Width: | Height: | Size: 3.0 KiB |
BIN
ui/Skins/Default/res/ButtonGreen_Left.png
Normal file
|
After Width: | Height: | Size: 3.0 KiB |
BIN
ui/Skins/Default/res/ButtonGreen_Right.png
Normal file
|
After Width: | Height: | Size: 3.0 KiB |
BIN
ui/Skins/Default/res/Checkbox_Checked.png
Normal file
|
After Width: | Height: | Size: 3.2 KiB |
BIN
ui/Skins/Default/res/Checkbox_CheckedHover.png
Normal file
|
After Width: | Height: | Size: 3.2 KiB |
BIN
ui/Skins/Default/res/Checkbox_Default.png
Normal file
|
After Width: | Height: | Size: 2.8 KiB |
BIN
ui/Skins/Default/res/Checkbox_DefaultHover.png
Normal file
|
After Width: | Height: | Size: 2.8 KiB |
BIN
ui/Skins/Default/res/ColorpickerActiveColorOverlay.png
Normal file
|
After Width: | Height: | Size: 2.9 KiB |
BIN
ui/Skins/Default/res/ColorpickerFieldHandle.png
Normal file
|
After Width: | Height: | Size: 3.1 KiB |
BIN
ui/Skins/Default/res/ColorpickerFieldOverlay.png
Normal file
|
After Width: | Height: | Size: 8.7 KiB |
BIN
ui/Skins/Default/res/ColorpickerHueHandle.png
Normal file
|
After Width: | Height: | Size: 3.6 KiB |
BIN
ui/Skins/Default/res/ColorpickerHueSlider.png
Normal file
|
After Width: | Height: | Size: 3.1 KiB |
BIN
ui/Skins/Default/res/ColorpickerPreviewBg.png
Normal file
|
After Width: | Height: | Size: 2.8 KiB |
BIN
ui/Skins/Default/res/ColorpickerPreviewOverlay.png
Normal file
|
After Width: | Height: | Size: 2.9 KiB |
BIN
ui/Skins/Default/res/Frame_BL.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
ui/Skins/Default/res/Frame_BR.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
ui/Skins/Default/res/Frame_Bottom.png
Normal file
|
After Width: | Height: | Size: 1.5 KiB |
BIN
ui/Skins/Default/res/Frame_Left.png
Normal file
|
After Width: | Height: | Size: 1.0 KiB |
BIN
ui/Skins/Default/res/Frame_Mid.png
Normal file
|
After Width: | Height: | Size: 99 B |
BIN
ui/Skins/Default/res/Frame_Right.png
Normal file
|
After Width: | Height: | Size: 1.0 KiB |
BIN
ui/Skins/Default/res/Frame_TL.png
Normal file
|
After Width: | Height: | Size: 732 B |
BIN
ui/Skins/Default/res/Frame_TR.png
Normal file
|
After Width: | Height: | Size: 727 B |
BIN
ui/Skins/Default/res/Frame_Top.png
Normal file
|
After Width: | Height: | Size: 874 B |
BIN
ui/Skins/Default/res/HorizontalListDivider.png
Normal file
|
After Width: | Height: | Size: 2.7 KiB |
BIN
ui/Skins/Default/res/InputField.png
Normal file
|
After Width: | Height: | Size: 2.7 KiB |
BIN
ui/Skins/Default/res/InputField_Left.png
Normal file
|
After Width: | Height: | Size: 2.8 KiB |
BIN
ui/Skins/Default/res/InputField_Right.png
Normal file
|
After Width: | Height: | Size: 2.8 KiB |
BIN
ui/Skins/Default/res/Keymarker.png
Normal file
|
After Width: | Height: | Size: 2.8 KiB |
BIN
ui/Skins/Default/res/Keymarker_Left.png
Normal file
|
After Width: | Height: | Size: 2.9 KiB |
BIN
ui/Skins/Default/res/Keymarker_Right.png
Normal file
|
After Width: | Height: | Size: 3.0 KiB |