621 lines
26 KiB
Python
621 lines
26 KiB
Python
|
|
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
|
|
)
|
|
pick_scene = getattr(self.controller, "pick_model", None) if self.controller else None
|
|
if pick_scene is None:
|
|
pick_scene = self.model
|
|
self.pick_cam.set_scene(pick_scene)
|
|
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
|