修复手柄选中以及移动异常,添加Imgui的web视图
This commit is contained in:
parent
b3d758a3e3
commit
c93ab3edac
@ -450,7 +450,8 @@ class EventHandler:
|
||||
print(f"点击的是碰撞节点: {hitNode.getName()}")
|
||||
# 碰撞节点的父节点应该是模型
|
||||
parent = hitNode.getParent()
|
||||
if parent in self.world.models:
|
||||
model_list = self.world.models if hasattr(self.world, 'models') else []
|
||||
if parent in model_list or parent.hasTag('is_model_root'):
|
||||
selectedModel = parent
|
||||
print(f"找到对应的模型: {selectedModel.getName()}")
|
||||
else:
|
||||
@ -460,13 +461,15 @@ class EventHandler:
|
||||
current = hitNode
|
||||
while current != self.world.render:
|
||||
# 检查是否是模型
|
||||
if current in self.world.models:
|
||||
model_list = self.world.models if hasattr(self.world, 'models') else []
|
||||
if current in model_list or current.hasTag('is_model_root'):
|
||||
selectedModel = current
|
||||
print(f"找到模型节点: {selectedModel.getName()}")
|
||||
break
|
||||
|
||||
# 检查是否是模型的子节点
|
||||
for model in self.world.models:
|
||||
model_list = self.world.models if hasattr(self.world, 'models') else []
|
||||
for model in model_list:
|
||||
if current.getParent() == model or current.isAncestorOf(model):
|
||||
selectedModel = model
|
||||
print(f"找到父模型: {selectedModel.getName()}")
|
||||
|
||||
225
core/imgui_webview.py
Normal file
225
core/imgui_webview.py
Normal file
@ -0,0 +1,225 @@
|
||||
"""
|
||||
imgui_webview.py
|
||||
后台 playwright 无头浏览器 + 截图 → Panda3D 纹理,供 ImGui 面板显示。
|
||||
"""
|
||||
from __future__ import annotations
|
||||
import threading
|
||||
import io
|
||||
import time
|
||||
|
||||
|
||||
class ImGuiWebView:
|
||||
"""
|
||||
后台线程运行 playwright Chromium,定期截图并转换为 Panda3D 纹理。
|
||||
ImGui 直接用 imgui.image() 显示纹理,鼠标/滚轮事件转发给浏览器。
|
||||
"""
|
||||
|
||||
def __init__(self, width: int = 1280, height: int = 720):
|
||||
self.view_width = width
|
||||
self.view_height = height
|
||||
|
||||
# 截图数据(bytes)
|
||||
self._screenshot: bytes | None = None
|
||||
self._screenshot_lock = threading.Lock()
|
||||
self.tex_dirty = False # 有新截图待上传 GPU
|
||||
|
||||
# 状态
|
||||
self.current_url = ""
|
||||
self.title = ""
|
||||
self.is_loading = False
|
||||
self.error: str | None = None
|
||||
|
||||
# 待处理指令(由 ImGui 线程写,浏览器线程读)
|
||||
self._cmd_navigate: str | None = None
|
||||
self._cmd_click: tuple | None = None # (x_ratio, y_ratio)
|
||||
self._cmd_scroll: float | None = None # pixels
|
||||
self._cmd_back = False
|
||||
self._cmd_forward = False
|
||||
self._cmd_reload = False
|
||||
self._lock = threading.Lock()
|
||||
|
||||
self._running = False
|
||||
self._thread: threading.Thread | None = None
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# 公开控制 API(由 ImGui 线程调用,线程安全)
|
||||
# ------------------------------------------------------------------ #
|
||||
|
||||
def start(self, url: str):
|
||||
if self._running:
|
||||
return
|
||||
self._running = True
|
||||
self._cmd_navigate = url
|
||||
self._thread = threading.Thread(target=self._run, daemon=True,
|
||||
name="imgui-webview")
|
||||
self._thread.start()
|
||||
|
||||
def stop(self):
|
||||
self._running = False
|
||||
|
||||
def navigate(self, url: str):
|
||||
if not url.startswith(('http://', 'https://', 'file://')):
|
||||
url = 'https://' + url
|
||||
with self._lock:
|
||||
self._cmd_navigate = url
|
||||
self.is_loading = True
|
||||
|
||||
def click(self, x_ratio: float, y_ratio: float):
|
||||
with self._lock:
|
||||
self._cmd_click = (x_ratio, y_ratio)
|
||||
|
||||
def scroll(self, delta_px: float):
|
||||
with self._lock:
|
||||
self._cmd_scroll = delta_px
|
||||
|
||||
def go_back(self):
|
||||
with self._lock:
|
||||
self._cmd_back = True
|
||||
|
||||
def go_forward(self):
|
||||
with self._lock:
|
||||
self._cmd_forward = True
|
||||
|
||||
def reload(self):
|
||||
with self._lock:
|
||||
self._cmd_reload = True
|
||||
|
||||
def get_screenshot_bytes(self) -> bytes | None:
|
||||
with self._screenshot_lock:
|
||||
return self._screenshot
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# 内部线程
|
||||
# ------------------------------------------------------------------ #
|
||||
|
||||
def _run(self):
|
||||
import asyncio
|
||||
loop = asyncio.new_event_loop()
|
||||
asyncio.set_event_loop(loop)
|
||||
try:
|
||||
loop.run_until_complete(self._async_run())
|
||||
except Exception as exc:
|
||||
self.error = f"WebView 线程异常: {exc}"
|
||||
import traceback; traceback.print_exc()
|
||||
finally:
|
||||
loop.close()
|
||||
|
||||
async def _async_run(self):
|
||||
try:
|
||||
from playwright.async_api import async_playwright
|
||||
except ImportError:
|
||||
self.error = (
|
||||
"playwright 未安装。\n"
|
||||
"请运行: pip install playwright\n"
|
||||
"然后运行: playwright install chromium"
|
||||
)
|
||||
self._running = False
|
||||
return
|
||||
|
||||
try:
|
||||
async with async_playwright() as pw:
|
||||
browser = await pw.chromium.launch(headless=True)
|
||||
ctx = await browser.new_context(
|
||||
viewport={"width": self.view_width,
|
||||
"height": self.view_height},
|
||||
user_agent=(
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
|
||||
"AppleWebKit/537.36 (KHTML, like Gecko) "
|
||||
"Chrome/121.0.0.0 Safari/537.36"
|
||||
),
|
||||
)
|
||||
page = await ctx.new_page()
|
||||
|
||||
# 初次导航
|
||||
start_url = self._cmd_navigate or "about:blank"
|
||||
self._cmd_navigate = None
|
||||
await self._goto(page, start_url)
|
||||
await self._snap(page)
|
||||
|
||||
# 主循环
|
||||
import asyncio
|
||||
while self._running:
|
||||
await asyncio.sleep(0.05)
|
||||
|
||||
with self._lock:
|
||||
nav_url = self._cmd_navigate; self._cmd_navigate = None
|
||||
clk = self._cmd_click; self._cmd_click = None
|
||||
scr = self._cmd_scroll; self._cmd_scroll = None
|
||||
do_back = self._cmd_back; self._cmd_back = False
|
||||
do_fwd = self._cmd_forward; self._cmd_forward = False
|
||||
do_reload = self._cmd_reload; self._cmd_reload = False
|
||||
|
||||
changed = False
|
||||
|
||||
if nav_url:
|
||||
self.is_loading = True
|
||||
await self._goto(page, nav_url)
|
||||
changed = True
|
||||
self.is_loading = False
|
||||
|
||||
if do_back:
|
||||
await page.go_back()
|
||||
await asyncio.sleep(0.5)
|
||||
changed = True
|
||||
|
||||
if do_fwd:
|
||||
await page.go_forward()
|
||||
await asyncio.sleep(0.5)
|
||||
changed = True
|
||||
|
||||
if do_reload:
|
||||
await page.reload(wait_until="domcontentloaded")
|
||||
changed = True
|
||||
|
||||
if clk is not None:
|
||||
xr, yr = clk
|
||||
x = int(xr * self.view_width)
|
||||
y = int(yr * self.view_height)
|
||||
await page.mouse.click(x, y)
|
||||
await asyncio.sleep(0.4)
|
||||
changed = True
|
||||
|
||||
if scr is not None:
|
||||
await page.evaluate(
|
||||
f"window.scrollBy(0, {int(scr)})"
|
||||
)
|
||||
await asyncio.sleep(0.15)
|
||||
changed = True
|
||||
|
||||
if changed:
|
||||
self.current_url = page.url
|
||||
try:
|
||||
self.title = await page.title()
|
||||
except Exception:
|
||||
pass
|
||||
await self._snap(page)
|
||||
|
||||
await browser.close()
|
||||
|
||||
except Exception as exc:
|
||||
self.error = str(exc)
|
||||
import traceback; traceback.print_exc()
|
||||
finally:
|
||||
self._running = False
|
||||
|
||||
async def _goto(self, page, url: str):
|
||||
import asyncio
|
||||
try:
|
||||
await page.goto(url, wait_until="domcontentloaded", timeout=20_000)
|
||||
self.current_url = page.url
|
||||
try:
|
||||
self.title = await page.title()
|
||||
except Exception:
|
||||
pass
|
||||
except Exception as exc:
|
||||
print(f"[WebView] 导航失败 {url}: {exc}")
|
||||
|
||||
async def _snap(self, page):
|
||||
"""截图并更新 self._screenshot"""
|
||||
try:
|
||||
data = await page.screenshot(type="png", full_page=False)
|
||||
with self._screenshot_lock:
|
||||
self._screenshot = data
|
||||
self.tex_dirty = True
|
||||
except Exception as exc:
|
||||
print(f"[WebView] 截图失败: {exc}")
|
||||
@ -1963,10 +1963,30 @@ class SelectionSystem:
|
||||
command = RotateNodeCommand(self.gizmoTarget, self.gizmoTargetStartHpr, current_hpr)
|
||||
self.world.command_manager.execute_command(command)
|
||||
print(f"创建旋转命令: {self.gizmoTargetStartHpr} -> {current_hpr}")
|
||||
print(f"创建旋转命令: {self.gizmoTargetStartHpr} -> {current_hpr}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"创建撤销命令时出错: {e}")
|
||||
|
||||
# 同步碰撞体
|
||||
try:
|
||||
target = self.gizmoTarget
|
||||
if target:
|
||||
# 寻找它的所属模型根节点
|
||||
root_model = target
|
||||
while root_model and root_model != self.world.render:
|
||||
model_list = self.world.models if hasattr(self.world, 'models') else []
|
||||
if root_model in model_list or root_model.hasTag('is_model_root'):
|
||||
break
|
||||
root_model = root_model.getParent()
|
||||
|
||||
# 如果这个节点属于某个模型,或者是模型自己,更新该模型的碰撞边界
|
||||
if root_model and hasattr(self.world, 'models') and root_model in self.world.models:
|
||||
if hasattr(self.world, 'scene_manager') and hasattr(self.world.scene_manager, 'refreshCollisionBounds'):
|
||||
self.world.scene_manager.refreshCollisionBounds(root_model)
|
||||
except Exception as e:
|
||||
print(f"同步模型碰撞体失败: {e}")
|
||||
|
||||
# 恢复所有轴的颜色
|
||||
for axis_name in ["x", "y", "z"]:
|
||||
self.setGizmoAxisColor(axis_name, self.gizmo_colors[axis_name])
|
||||
|
||||
28
imgui.ini
28
imgui.ini
@ -24,26 +24,26 @@ Size=832,45
|
||||
Collapsed=0
|
||||
|
||||
[Window][工具栏]
|
||||
Pos=241,20
|
||||
Size=908,74
|
||||
Pos=276,20
|
||||
Size=1413,74
|
||||
Collapsed=0
|
||||
DockId=0x0000000D,0
|
||||
|
||||
[Window][场景树]
|
||||
Pos=0,20
|
||||
Size=239,468
|
||||
Size=274,634
|
||||
Collapsed=0
|
||||
DockId=0x00000007,0
|
||||
|
||||
[Window][属性面板]
|
||||
Pos=1151,20
|
||||
Size=229,730
|
||||
Pos=1691,20
|
||||
Size=229,989
|
||||
Collapsed=0
|
||||
DockId=0x00000003,0
|
||||
|
||||
[Window][控制台]
|
||||
Pos=0,490
|
||||
Size=239,260
|
||||
Pos=0,656
|
||||
Size=274,353
|
||||
Collapsed=0
|
||||
DockId=0x00000008,0
|
||||
|
||||
@ -60,7 +60,7 @@ Collapsed=0
|
||||
|
||||
[Window][WindowOverViewport_11111111]
|
||||
Pos=0,20
|
||||
Size=1380,730
|
||||
Size=1920,989
|
||||
Collapsed=0
|
||||
|
||||
[Window][测试窗口1]
|
||||
@ -99,8 +99,8 @@ Size=600,500
|
||||
Collapsed=0
|
||||
|
||||
[Window][资源管理器]
|
||||
Pos=241,464
|
||||
Size=908,286
|
||||
Pos=276,723
|
||||
Size=1413,286
|
||||
Collapsed=0
|
||||
DockId=0x00000006,0
|
||||
|
||||
@ -201,17 +201,17 @@ Size=600,400
|
||||
Collapsed=0
|
||||
|
||||
[Window][Web面板]
|
||||
Pos=373,98
|
||||
Pos=474,122
|
||||
Size=942,580
|
||||
Collapsed=0
|
||||
|
||||
[Docking][Data]
|
||||
DockSpace ID=0x08BD597D Window=0x1BBC0F80 Pos=0,20 Size=1380,730 Split=X
|
||||
DockSpace ID=0x08BD597D Window=0x1BBC0F80 Pos=0,20 Size=1920,989 Split=X
|
||||
DockNode ID=0x00000001 Parent=0x08BD597D SizeRef=1689,989 Split=X
|
||||
DockNode ID=0x00000009 Parent=0x00000001 SizeRef=239,989 Split=Y Selected=0xE0015051
|
||||
DockNode ID=0x00000009 Parent=0x00000001 SizeRef=274,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=1448,989 Split=Y
|
||||
DockNode ID=0x0000000A Parent=0x00000001 SizeRef=1413,989 Split=Y
|
||||
DockNode ID=0x0000000D Parent=0x0000000A SizeRef=1318,74 HiddenTabBar=1 Selected=0x43A39006
|
||||
DockNode ID=0x0000000E Parent=0x0000000A SizeRef=1318,913 Split=Y
|
||||
DockNode ID=0x00000005 Parent=0x0000000E SizeRef=1341,625 CentralNode=1
|
||||
|
||||
@ -787,29 +787,14 @@ class SceneManager:
|
||||
minPoint = Point3()
|
||||
maxPoint = Point3()
|
||||
|
||||
# 使用与选择框相同的calcTightBounds方法获取边界
|
||||
if model.calcTightBounds(minPoint, maxPoint, self.world.render):
|
||||
# 使用与选择框相同的calcTightBounds方法获取边界,但是在局部坐标系中进行计算
|
||||
# 这样计算出的包围盒直接贴合几何体,并且无论模型自身受到什么平移/缩放/旋转都不会发生两次形变!
|
||||
if model.calcTightBounds(minPoint, maxPoint, model):
|
||||
# 检查边界框的有效性
|
||||
if (abs(minPoint.x) < 1e10 and abs(minPoint.y) < 1e10 and abs(minPoint.z) < 1e10 and
|
||||
abs(maxPoint.x) < 1e10 and abs(maxPoint.y) < 1e10 and abs(maxPoint.z) < 1e10):
|
||||
# 特殊处理FBX模型的碰撞体
|
||||
if model.hasTag("model_path") and model.getTag("model_path").lower().endswith('.fbx'):
|
||||
print("检测到FBX模型,调整碰撞体...")
|
||||
# 反向应用FBX的变换以匹配视觉表现
|
||||
# 缩放调整: 乘以100(因为模型被缩小了0.01倍)
|
||||
minPoint *= 100
|
||||
maxPoint *= 100
|
||||
|
||||
# 旋转调整: 绕P轴旋转-90度
|
||||
# 创建旋转矩阵
|
||||
from panda3d.core import Mat4, LRotation
|
||||
rotation = LRotation(0, -90, 0) # 绕P轴旋转-90度
|
||||
rot_matrix = Mat4()
|
||||
rotation.extractToMatrix(rot_matrix)
|
||||
|
||||
# 应用旋转变换到边界点
|
||||
minPoint = rot_matrix.xformPoint(minPoint)
|
||||
maxPoint = rot_matrix.xformPoint(maxPoint)
|
||||
|
||||
# 我们现在获取的是纯局部几何数据,因此不再需要手动乘以100或应用旋转来抵消FBX形变
|
||||
|
||||
# 创建与选择框完全一致的碰撞体
|
||||
cBox = CollisionBox(minPoint, maxPoint)
|
||||
@ -849,6 +834,31 @@ class SceneManager:
|
||||
traceback.print_exc()
|
||||
return None
|
||||
|
||||
def refreshCollisionBounds(self, model):
|
||||
"""重新计算并更新模型的碰撞框"""
|
||||
try:
|
||||
if not model or model.isEmpty():
|
||||
return
|
||||
|
||||
# 使用列表以便在遍历后安全删除
|
||||
children_to_remove = []
|
||||
for child in model.getChildren():
|
||||
name = child.getName() if hasattr(child, 'getName') else ""
|
||||
if name.startswith("modelCollision_"):
|
||||
children_to_remove.append(child)
|
||||
|
||||
# 如果存在旧碰撞节点,删除它并重新创建
|
||||
if children_to_remove:
|
||||
for child in children_to_remove:
|
||||
child.removeNode()
|
||||
|
||||
# 由于 calcTightBounds 被修改为使用局部坐标计算 (第三个参数传 model)
|
||||
# 无需再像之前一样为了解决偏移而将其重置回0点。
|
||||
self.setupCollision(model)
|
||||
|
||||
except Exception as e:
|
||||
print(f"刷新碰撞框失败: {e}")
|
||||
|
||||
# ==================== 场景树管理 ====================
|
||||
|
||||
def updateSceneTree(self):
|
||||
|
||||
@ -38,6 +38,7 @@ class ObjectController:
|
||||
self.local_to_global_id = {}
|
||||
self.local_transform_state = {}
|
||||
self.local_transform_base_positions = {}
|
||||
self.pick_vertex_index = {}
|
||||
self.virtual_tree = None
|
||||
self.virtual_tree_meta = None
|
||||
|
||||
@ -47,6 +48,105 @@ class ObjectController:
|
||||
self._source_model_name = ""
|
||||
self._source_model_stem = ""
|
||||
|
||||
def _get_model_world_mat(self):
|
||||
"""Return current model net transform matrix (to top/root)."""
|
||||
if not self.model:
|
||||
return LMatrix4f.ident_mat()
|
||||
try:
|
||||
if self.model.isEmpty():
|
||||
return LMatrix4f.ident_mat()
|
||||
except Exception:
|
||||
try:
|
||||
if self.model.is_empty():
|
||||
return LMatrix4f.ident_mat()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
try:
|
||||
return LMatrix4f(self.model.getNetTransform().getMat())
|
||||
except Exception:
|
||||
try:
|
||||
# snake_case fallback in newer Panda3D bindings
|
||||
return LMatrix4f(self.model.get_net_transform().get_mat())
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
try:
|
||||
top = self.model.getTop()
|
||||
if top and not top.isEmpty():
|
||||
return LMatrix4f(self.model.getMat(top))
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
try:
|
||||
return LMatrix4f(self.model.getMat())
|
||||
except Exception:
|
||||
return LMatrix4f.ident_mat()
|
||||
|
||||
def get_model_world_mat(self):
|
||||
"""Public accessor for current model net transform matrix."""
|
||||
return self._get_model_world_mat()
|
||||
|
||||
def _local_point_to_world(self, local_pos):
|
||||
"""Convert a local-space point to world-space based on model net transform."""
|
||||
mat = self._get_model_world_mat()
|
||||
p = Point3(float(local_pos.x), float(local_pos.y), float(local_pos.z))
|
||||
wp = mat.xformPoint(p)
|
||||
return Vec3(wp.x, wp.y, wp.z)
|
||||
|
||||
def _world_vec_to_local(self, world_vec):
|
||||
"""Convert a world-space vector to model-local space."""
|
||||
mat = self._get_model_world_mat()
|
||||
inv = LMatrix4f(mat)
|
||||
try:
|
||||
inv.invertInPlace()
|
||||
except Exception:
|
||||
try:
|
||||
inv.invert_in_place()
|
||||
except Exception:
|
||||
return Vec3(world_vec)
|
||||
v = Vec3(world_vec)
|
||||
lv = inv.xformVec(v)
|
||||
return Vec3(lv.x, lv.y, lv.z)
|
||||
|
||||
def world_vector_to_model_local(self, world_vec):
|
||||
"""Public converter from world delta vector to model-local delta vector."""
|
||||
return self._world_vec_to_local(world_vec)
|
||||
|
||||
def get_model_world_quat(self):
|
||||
"""Return current model world quaternion."""
|
||||
if not self.model:
|
||||
return Quat.identQuat()
|
||||
try:
|
||||
if self.model.isEmpty():
|
||||
return Quat.identQuat()
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
top = self.model.getTop()
|
||||
if top and not top.isEmpty():
|
||||
return Quat(self.model.getQuat(top))
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
return Quat(self.model.getQuat())
|
||||
except Exception:
|
||||
return Quat.identQuat()
|
||||
|
||||
def world_quat_delta_to_model_local(self, delta_quat_world):
|
||||
"""
|
||||
Convert world-space delta quaternion to model-local delta quaternion.
|
||||
local = inv(model_world_rot) * world_delta * model_world_rot
|
||||
"""
|
||||
if delta_quat_world is None:
|
||||
return Quat.identQuat()
|
||||
model_q = self.get_model_world_quat()
|
||||
inv_model_q = Quat(model_q)
|
||||
inv_model_q.invertInPlace()
|
||||
local_q = inv_model_q * Quat(delta_quat_world) * model_q
|
||||
local_q.normalize()
|
||||
return local_q
|
||||
|
||||
def _build_original_hierarchy_key(self, np, model_root):
|
||||
"""Capture hierarchy path before flatten/reparent."""
|
||||
parts = []
|
||||
@ -99,6 +199,7 @@ class ObjectController:
|
||||
self.local_to_global_id = {}
|
||||
self.local_transform_state = {}
|
||||
self.local_transform_base_positions = {}
|
||||
self.pick_vertex_index = {}
|
||||
self.virtual_tree = None
|
||||
self.virtual_tree_meta = None
|
||||
self.pick_model = None
|
||||
@ -192,6 +293,7 @@ class ObjectController:
|
||||
|
||||
# Keep ID colors only in picking clone to avoid affecting visible shading.
|
||||
self.pick_model = model.copy_to(NodePath("ssbo_pick_root"))
|
||||
self._build_pick_vertex_index(self.pick_model)
|
||||
self._set_uniform_vertex_color(model, 1.0, 1.0, 1.0, 1.0)
|
||||
|
||||
t2 = time.time()
|
||||
@ -403,6 +505,89 @@ class ObjectController:
|
||||
self.vertex_index[uid].append((gn_np, gi, rows))
|
||||
self.original_positions[uid].append(pos.copy())
|
||||
|
||||
def _build_pick_vertex_index(self, pick_root):
|
||||
"""
|
||||
Build local_id -> [(geom_node_np, geom_idx, row_indices_array)] for pick model.
|
||||
This keeps GPU-picking geometry writable in sync with visible geometry edits.
|
||||
"""
|
||||
import numpy as np
|
||||
|
||||
self.pick_vertex_index = {}
|
||||
if not pick_root:
|
||||
return
|
||||
|
||||
for gn_np in pick_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
|
||||
|
||||
fmt = vdata.get_format()
|
||||
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()
|
||||
color_array_format = fmt.get_array(color_array_idx)
|
||||
color_stride = color_array_format.get_stride()
|
||||
|
||||
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)
|
||||
|
||||
num_components = color_col.get_num_components()
|
||||
component_bytes = color_col.get_component_bytes()
|
||||
|
||||
if component_bytes == 4:
|
||||
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:
|
||||
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:
|
||||
continue
|
||||
|
||||
local_ids = r_vals + (g_vals << 8)
|
||||
sort_idx = np.argsort(local_ids)
|
||||
sorted_ids = local_ids[sort_idx]
|
||||
boundaries = np.where(np.diff(sorted_ids) != 0)[0] + 1
|
||||
id_groups = np.split(sort_idx, 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]
|
||||
if uid not in self.pick_vertex_index:
|
||||
self.pick_vertex_index[uid] = []
|
||||
self.pick_vertex_index[uid].append((gn_np, gi, rows))
|
||||
|
||||
def _apply_vertices_to_pick(self, local_idx, entry_idx, new_pos):
|
||||
"""Mirror one transformed vertex group to pick-model geometry."""
|
||||
pick_entries = self.pick_vertex_index.get(local_idx)
|
||||
if not pick_entries or entry_idx >= len(pick_entries):
|
||||
return
|
||||
|
||||
pick_gn_np, pick_gi, pick_rows = pick_entries[entry_idx]
|
||||
gnode = pick_gn_np.node()
|
||||
geom = gnode.modify_geom(pick_gi)
|
||||
vdata = geom.modify_vertex_data()
|
||||
writer = GeomVertexWriter(vdata, "vertex")
|
||||
|
||||
max_rows = min(len(pick_rows), len(new_pos))
|
||||
for j in range(max_rows):
|
||||
writer.set_row(int(pick_rows[j]))
|
||||
writer.set_data3f(float(new_pos[j, 0]), float(new_pos[j, 1]), float(new_pos[j, 2]))
|
||||
|
||||
def _init_local_transform_state(self):
|
||||
"""Initialize transform state for each local_idx after vertex index is ready."""
|
||||
self.local_transform_state = {}
|
||||
@ -437,7 +622,7 @@ class ObjectController:
|
||||
return local_indices
|
||||
|
||||
def get_local_pivot(self, local_idx):
|
||||
"""Get pivot for one local object (world-space center)."""
|
||||
"""Get pivot for one local object (model-local center)."""
|
||||
global_id = self.local_to_global_id.get(local_idx)
|
||||
if global_id is None:
|
||||
return Vec3(0, 0, 0)
|
||||
@ -457,7 +642,8 @@ class ObjectController:
|
||||
valid += 1
|
||||
if valid == 0:
|
||||
return Vec3(0, 0, 0)
|
||||
return acc / float(valid)
|
||||
center_local = acc / float(valid)
|
||||
return self._local_point_to_world(center_local)
|
||||
|
||||
def begin_transform_session(self, local_indices):
|
||||
"""Create immutable baseline snapshot for one gizmo drag session."""
|
||||
@ -543,6 +729,7 @@ class ObjectController:
|
||||
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]))
|
||||
self._apply_vertices_to_pick(local_idx, i, new_pos)
|
||||
|
||||
def _quat_to_np_mat3(self, quat):
|
||||
"""Convert Panda3D Quat to 3x3 numpy rotation matrix."""
|
||||
@ -611,6 +798,7 @@ class ObjectController:
|
||||
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]))
|
||||
self._apply_vertices_to_pick(local_idx, i, new_pos)
|
||||
|
||||
def get_world_pos(self, global_id):
|
||||
"""Get current world position of an object."""
|
||||
@ -621,8 +809,9 @@ class ObjectController:
|
||||
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
|
||||
|
||||
local_pos = Vec3(original_pos) + offset
|
||||
return self._local_point_to_world(local_pos)
|
||||
|
||||
def get_object_center(self, global_id):
|
||||
"""Get the original center position of an object (for rotation pivot)."""
|
||||
|
||||
@ -147,6 +147,8 @@ class SSBOEditor:
|
||||
|
||||
# Setup GPU Picking (uses simple vertex-color shader)
|
||||
self.setup_gpu_picking()
|
||||
# Keep pick clone aligned with source model transform.
|
||||
self._sync_pick_model_transform()
|
||||
|
||||
print(f"[SSBOEditor] Model loaded. Total objects: {count}")
|
||||
|
||||
@ -248,10 +250,58 @@ class SSBOEditor:
|
||||
self.pick_buffer.set_clear_color(Vec4(0, 0, 0, 0))
|
||||
self.pick_buffer.set_clear_color_active(True)
|
||||
|
||||
def _sync_pick_model_transform(self):
|
||||
"""Sync pick-scene clone to current source model world transform."""
|
||||
if not self.controller or not self.model:
|
||||
return
|
||||
pick_model = getattr(self.controller, "pick_model", None)
|
||||
if pick_model is None:
|
||||
return
|
||||
try:
|
||||
if pick_model.isEmpty():
|
||||
return
|
||||
except Exception:
|
||||
try:
|
||||
if pick_model.is_empty():
|
||||
return
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
if hasattr(self.controller, "get_model_world_mat"):
|
||||
world_mat = self.controller.get_model_world_mat()
|
||||
else:
|
||||
world_mat = LMatrix4f(self.model.getNetTransform().getMat())
|
||||
try:
|
||||
pick_model.set_mat(world_mat)
|
||||
except Exception:
|
||||
pick_model.setMat(world_mat)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def _refresh_ssbo_proxy_center(self):
|
||||
"""Update proxy center when source model transform changes."""
|
||||
if self._ssbo_transform_active:
|
||||
return
|
||||
if not self.controller or not self._ssbo_selected_local_indices:
|
||||
return
|
||||
if self._ssbo_gizmo_proxy is None:
|
||||
return
|
||||
try:
|
||||
if self._ssbo_gizmo_proxy.isEmpty():
|
||||
return
|
||||
except Exception:
|
||||
return
|
||||
try:
|
||||
center = self.controller.get_selection_center(self._ssbo_selected_local_indices)
|
||||
self._ssbo_gizmo_proxy.set_pos(center)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
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._sync_pick_model_transform()
|
||||
|
||||
self.pick_lens.set_fov(0.1)
|
||||
self.pick_lens.set_film_offset(0, 0)
|
||||
@ -294,6 +344,8 @@ class SSBOEditor:
|
||||
if io.want_capture_mouse:
|
||||
return
|
||||
if self.base.mouseWatcherNode.has_mouse():
|
||||
self._sync_pick_model_transform()
|
||||
self._refresh_ssbo_proxy_center()
|
||||
mpos = self.base.mouseWatcherNode.get_mouse()
|
||||
# If clicking gizmo, skip SSBO pick.
|
||||
if self._try_start_gizmo_drag(mpos.x, mpos.y):
|
||||
@ -457,16 +509,25 @@ class SSBOEditor:
|
||||
curr_quat = Quat(target.getQuat(self.base.render))
|
||||
curr_scale = Vec3(target.getScale())
|
||||
|
||||
delta_pos = curr_pos - start_pos
|
||||
delta_pos_world = curr_pos - start_pos
|
||||
inv_start_quat = Quat(start_quat)
|
||||
inv_start_quat.invertInPlace()
|
||||
delta_quat = curr_quat * inv_start_quat
|
||||
delta_quat_world = 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,
|
||||
)
|
||||
|
||||
# TransformGizmo drag deltas are in world space.
|
||||
# SSBO vertex transforms are applied in model-local space.
|
||||
delta_pos = delta_pos_world
|
||||
delta_quat = delta_quat_world
|
||||
if hasattr(self.controller, "world_vector_to_model_local"):
|
||||
delta_pos = self.controller.world_vector_to_model_local(delta_pos_world)
|
||||
if hasattr(self.controller, "world_quat_delta_to_model_local"):
|
||||
delta_quat = self.controller.world_quat_delta_to_model_local(delta_quat_world)
|
||||
|
||||
self.controller.apply_transform_session(
|
||||
self._ssbo_transform_snapshot,
|
||||
delta_pos,
|
||||
@ -600,6 +661,8 @@ class SSBOEditor:
|
||||
def update_task(self, task):
|
||||
dt = globalClock.getDt()
|
||||
io = imgui.get_io()
|
||||
self._sync_pick_model_transform()
|
||||
self._refresh_ssbo_proxy_center()
|
||||
|
||||
if io.want_capture_keyboard: return task.cont
|
||||
|
||||
|
||||
@ -40,7 +40,7 @@ class EditorPanels:
|
||||
|
||||
def _ensure_web_panel_state(self):
|
||||
if not hasattr(self.app, "web_panel_url_input") or not self.app.web_panel_url_input:
|
||||
self.app.web_panel_url_input = "https://www.example.com"
|
||||
self.app.web_panel_url_input = "https://www.baidu.com"
|
||||
if not hasattr(self.app, "_imgui_webview"):
|
||||
self.app._imgui_webview = None
|
||||
if not hasattr(self.app, "_imgui_webview_tex_id"):
|
||||
|
||||
Loading…
Reference in New Issue
Block a user