修复手柄选中以及移动异常,添加Imgui的web视图

This commit is contained in:
Hector 2026-02-27 10:32:57 +08:00
parent b3d758a3e3
commit c93ab3edac
8 changed files with 554 additions and 44 deletions

View File

@ -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
View 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}")

View File

@ -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])

View File

@ -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

View File

@ -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):

View File

@ -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)."""

View File

@ -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

View File

@ -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"):