Merge remote-tracking branch 'origin/geng' into IMgui_hu
This commit is contained in:
commit
5752559cdd
@ -13,6 +13,7 @@ enabled:
|
||||
- scattering
|
||||
- skin_shading
|
||||
- sky_ao
|
||||
- selection_outline
|
||||
- smaa
|
||||
- ssr
|
||||
# - clouds
|
||||
|
||||
@ -63,5 +63,6 @@ global_stage_order:
|
||||
|
||||
# Finishing stages, do not insert anything below
|
||||
- UpscaleStage
|
||||
- SelectionOutlineStage
|
||||
- FinalStage
|
||||
- UpdatePreviousPipesStage
|
||||
|
||||
@ -0,0 +1,2 @@
|
||||
"""Selection outline plugin package."""
|
||||
|
||||
@ -0,0 +1,3 @@
|
||||
settings:
|
||||
daytime_settings:
|
||||
|
||||
15
RenderPipelineFile/rpplugins/selection_outline/plugin.py
Normal file
15
RenderPipelineFile/rpplugins/selection_outline/plugin.py
Normal file
@ -0,0 +1,15 @@
|
||||
from rpcore.pluginbase.base_plugin import BasePlugin
|
||||
|
||||
from .selection_outline_stage import SelectionOutlineStage
|
||||
|
||||
|
||||
class Plugin(BasePlugin):
|
||||
|
||||
name = "Selection Outline"
|
||||
author = "EG Team"
|
||||
description = "Adds Unity-style selected-object outline as a post-process stage."
|
||||
version = "1.0"
|
||||
|
||||
def on_stage_setup(self):
|
||||
self.stage = self.create_stage(SelectionOutlineStage)
|
||||
|
||||
@ -0,0 +1,68 @@
|
||||
from panda3d.core import PNMImage, SamplerState, Texture, Vec4
|
||||
|
||||
from rpcore.render_stage import RenderStage
|
||||
|
||||
|
||||
class SelectionOutlineStage(RenderStage):
|
||||
|
||||
required_pipes = ["ShadedScene"]
|
||||
|
||||
def __init__(self, pipeline):
|
||||
RenderStage.__init__(self, pipeline)
|
||||
self._enabled = False
|
||||
self._outline_color = Vec4(1.0, 0.55, 0.0, 1.0)
|
||||
self._outline_width = 2.0
|
||||
self._fill_alpha = 0.0
|
||||
self._mask_tex = self._make_default_mask()
|
||||
|
||||
@property
|
||||
def produced_pipes(self):
|
||||
return {"ShadedScene": self.target.color_tex}
|
||||
|
||||
def create(self):
|
||||
self.target = self.create_target("SelectionOutline")
|
||||
self.target.add_color_attachment(bits=16)
|
||||
self.target.prepare_buffer()
|
||||
self._apply_inputs()
|
||||
|
||||
def reload_shaders(self):
|
||||
self.target.shader = self.load_plugin_shader("selection_outline.frag.glsl")
|
||||
self._apply_inputs()
|
||||
|
||||
def set_mask_texture(self, mask_texture):
|
||||
self._mask_tex = mask_texture if mask_texture else self._make_default_mask()
|
||||
self._apply_inputs()
|
||||
|
||||
def set_enabled_outline(self, enabled):
|
||||
self._enabled = bool(enabled)
|
||||
self._apply_inputs()
|
||||
|
||||
def set_outline_style(self, color=None, width_px=None, fill_alpha=None):
|
||||
if color is not None:
|
||||
self._outline_color = Vec4(color)
|
||||
if width_px is not None:
|
||||
self._outline_width = max(0.0, float(width_px))
|
||||
if fill_alpha is not None:
|
||||
self._fill_alpha = max(0.0, min(1.0, float(fill_alpha)))
|
||||
self._apply_inputs()
|
||||
|
||||
def _apply_inputs(self):
|
||||
enabled_val = 1.0 if self._enabled else 0.0
|
||||
self.target.set_shader_input("SelectionMaskTex", self._mask_tex)
|
||||
self.target.set_shader_input("SelectionOutlineEnabled", enabled_val)
|
||||
self.target.set_shader_input("SelectionOutlineColor", self._outline_color)
|
||||
self.target.set_shader_input("SelectionOutlineWidth", self._outline_width)
|
||||
self.target.set_shader_input("SelectionFillAlpha", self._fill_alpha)
|
||||
|
||||
def _make_default_mask(self):
|
||||
image = PNMImage(1, 1, 4)
|
||||
image.fill(0.0, 0.0, 0.0)
|
||||
image.alpha_fill(0.0)
|
||||
texture = Texture("selection_outline_default_mask")
|
||||
texture.load(image)
|
||||
texture.set_minfilter(SamplerState.FT_nearest)
|
||||
texture.set_magfilter(SamplerState.FT_nearest)
|
||||
texture.set_wrap_u(SamplerState.WM_clamp)
|
||||
texture.set_wrap_v(SamplerState.WM_clamp)
|
||||
return texture
|
||||
|
||||
@ -0,0 +1,59 @@
|
||||
/**
|
||||
* Selection outline post process stage.
|
||||
*/
|
||||
|
||||
#version 430
|
||||
|
||||
#pragma include "render_pipeline_base.inc.glsl"
|
||||
|
||||
uniform sampler2D ShadedScene;
|
||||
uniform sampler2D SelectionMaskTex;
|
||||
|
||||
uniform float SelectionOutlineEnabled;
|
||||
uniform vec4 SelectionOutlineColor;
|
||||
uniform float SelectionOutlineWidth; // pixels
|
||||
uniform float SelectionFillAlpha; // 0..1
|
||||
|
||||
out vec4 result;
|
||||
|
||||
float sample_mask(vec2 uv) {
|
||||
return textureLod(SelectionMaskTex, uv, 0).r;
|
||||
}
|
||||
|
||||
void main() {
|
||||
vec2 uv = get_texcoord();
|
||||
vec3 scene_col = textureLod(ShadedScene, uv, 0).rgb;
|
||||
|
||||
if (SelectionOutlineEnabled < 0.5) {
|
||||
result = vec4(scene_col, 1.0);
|
||||
return;
|
||||
}
|
||||
|
||||
vec2 texel = vec2(max(1.0, SelectionOutlineWidth)) / SCREEN_SIZE;
|
||||
float center = sample_mask(uv);
|
||||
|
||||
float max_nei = 0.0;
|
||||
max_nei = max(max_nei, sample_mask(uv + vec2( texel.x, 0.0)));
|
||||
max_nei = max(max_nei, sample_mask(uv + vec2(-texel.x, 0.0)));
|
||||
max_nei = max(max_nei, sample_mask(uv + vec2(0.0, texel.y)));
|
||||
max_nei = max(max_nei, sample_mask(uv + vec2(0.0, -texel.y)));
|
||||
max_nei = max(max_nei, sample_mask(uv + vec2( texel.x, texel.y)));
|
||||
max_nei = max(max_nei, sample_mask(uv + vec2( texel.x, -texel.y)));
|
||||
max_nei = max(max_nei, sample_mask(uv + vec2(-texel.x, texel.y)));
|
||||
max_nei = max(max_nei, sample_mask(uv + vec2(-texel.x, -texel.y)));
|
||||
|
||||
// Outer contour only.
|
||||
float edge = clamp(max_nei - center, 0.0, 1.0);
|
||||
float fill = center * SelectionFillAlpha;
|
||||
|
||||
vec3 col = scene_col;
|
||||
if (fill > 0.0) {
|
||||
col = mix(col, SelectionOutlineColor.rgb, fill * SelectionOutlineColor.a);
|
||||
}
|
||||
if (edge > 0.0) {
|
||||
col = mix(col, SelectionOutlineColor.rgb, edge * SelectionOutlineColor.a);
|
||||
}
|
||||
|
||||
result = vec4(col, 1.0);
|
||||
}
|
||||
|
||||
@ -14,6 +14,7 @@ from panda3d.core import (Vec3, Point3, Point2, LineSegs, ColorAttrib, RenderSta
|
||||
TransparencyAttrib, Vec4, CollisionCapsule)
|
||||
from direct.task.TaskManagerGlobal import taskMgr
|
||||
import math
|
||||
from core.selection_outline import SelectionOutlineManager
|
||||
|
||||
class SelectionSystem:
|
||||
"""选择和变换系统类"""
|
||||
@ -30,6 +31,17 @@ class SelectionSystem:
|
||||
self.selectedNode = None
|
||||
self.selectionBox = None # 选择框
|
||||
self.selectionBoxTarget = None # 选择框跟踪的目标节点
|
||||
self.show_selection_box = False
|
||||
self.enable_unity_outline = True
|
||||
self.outline_manager = getattr(self.world, "_selection_outline_manager", None)
|
||||
if not self.outline_manager:
|
||||
self.outline_manager = SelectionOutlineManager(
|
||||
self.world,
|
||||
enabled=self.enable_unity_outline,
|
||||
)
|
||||
setattr(self.world, "_selection_outline_manager", self.outline_manager)
|
||||
else:
|
||||
self.outline_manager.set_enabled(self.enable_unity_outline)
|
||||
|
||||
# 坐标轴工具(Gizmo)相关
|
||||
self.gizmo = None # 坐标轴
|
||||
@ -207,6 +219,21 @@ class SelectionSystem:
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
def _updateSelectionOutline(self, nodePath):
|
||||
"""Update Unity-like selection outline visuals."""
|
||||
try:
|
||||
if not getattr(self, "outline_manager", None):
|
||||
return
|
||||
if not self.enable_unity_outline:
|
||||
self.outline_manager.clear()
|
||||
return
|
||||
if nodePath and not nodePath.isEmpty():
|
||||
self.outline_manager.set_targets([nodePath])
|
||||
else:
|
||||
self.outline_manager.clear()
|
||||
except Exception as e:
|
||||
print(f"update selection outline failed: {e}")
|
||||
|
||||
def updateSelectionBoxGeometry(self):
|
||||
"""更新选择框的几何形状和位置"""
|
||||
try:
|
||||
@ -2065,8 +2092,12 @@ class SelectionSystem:
|
||||
|
||||
# 创建选择框
|
||||
#print("创建选择框...")
|
||||
self.createSelectionBox(nodePath)
|
||||
if self.selectionBox:
|
||||
if self.show_selection_box:
|
||||
self.createSelectionBox(nodePath)
|
||||
else:
|
||||
self.clearSelectionBox()
|
||||
|
||||
if (not self.show_selection_box) or self.selectionBox:
|
||||
box_name = "Unknown"
|
||||
if self.selectionBox and not self.selectionBox.isEmpty():
|
||||
box_name = self.selectionBox.getName()
|
||||
@ -2076,6 +2107,7 @@ class SelectionSystem:
|
||||
|
||||
# 创建坐标轴
|
||||
#print("创建坐标轴...")
|
||||
self._updateSelectionOutline(nodePath)
|
||||
self.createGizmo(nodePath)
|
||||
if self.gizmo:
|
||||
gizmo_name = "Unknown"
|
||||
@ -2088,6 +2120,7 @@ class SelectionSystem:
|
||||
else:
|
||||
print("清除选择...")
|
||||
self.clearSelectionBox()
|
||||
self._updateSelectionOutline(None)
|
||||
self.clearGizmo()
|
||||
print("✓ 取消选择")
|
||||
|
||||
@ -2176,6 +2209,9 @@ class SelectionSystem:
|
||||
if (self.selectionBoxTarget and self.selectionBoxTarget.isEmpty()):
|
||||
self.clearSelectionBox()
|
||||
|
||||
if self.selectedNode and self.selectedNode.isEmpty():
|
||||
self._updateSelectionOutline(None)
|
||||
|
||||
def setupGizmoCollision(self):
|
||||
if not self.gizmo or not self.gizmoXAxis or not self.gizmoYAxis or not self.gizmoZAxis:
|
||||
return
|
||||
@ -2895,6 +2931,7 @@ class SelectionSystem:
|
||||
|
||||
# 清理其他资源
|
||||
self.clearSelectionBox()
|
||||
self._updateSelectionOutline(None)
|
||||
self.clearGizmo()
|
||||
def clearSelection(self):
|
||||
"""清除当前选择"""
|
||||
@ -2902,6 +2939,7 @@ class SelectionSystem:
|
||||
self.selectedNode = None
|
||||
self.selectedObject = None
|
||||
self.clearSelectionBox()
|
||||
self._updateSelectionOutline(None)
|
||||
self.clearGizmo()
|
||||
|
||||
# 清除树形控件中的选择
|
||||
|
||||
303
core/selection_outline.py
Normal file
303
core/selection_outline.py
Normal file
@ -0,0 +1,303 @@
|
||||
from direct.task.TaskManagerGlobal import taskMgr
|
||||
from panda3d.core import (
|
||||
BitMask32,
|
||||
Camera,
|
||||
FrameBufferProperties,
|
||||
GraphicsOutput,
|
||||
GraphicsPipe,
|
||||
NodePath,
|
||||
Shader,
|
||||
Texture,
|
||||
Vec4,
|
||||
WindowProperties,
|
||||
)
|
||||
|
||||
|
||||
class SelectionOutlineManager:
|
||||
"""Selection mask manager feeding RenderPipeline SelectionOutlineStage."""
|
||||
|
||||
OUTLINE_PREFIX = "selectionOutline"
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
app,
|
||||
enabled=True,
|
||||
outline_color=Vec4(1.0, 0.55, 0.0, 1.0),
|
||||
outline_width_px=2.0,
|
||||
fill_alpha=0.0,
|
||||
max_targets=128,
|
||||
):
|
||||
self.app = app
|
||||
self.enabled = bool(enabled)
|
||||
self.outline_color = Vec4(outline_color)
|
||||
self.outline_width_px = max(0.0, float(outline_width_px))
|
||||
self.fill_alpha = max(0.0, min(1.0, float(fill_alpha)))
|
||||
self.max_targets = max(1, int(max_targets))
|
||||
|
||||
self._task_name = "selection_outline_mask_sync"
|
||||
self._tracked = [] # [(source_np, clone_np), ...]
|
||||
self._stage_missing_warned = False
|
||||
|
||||
self._mask_root = NodePath(f"{self.OUTLINE_PREFIX}_mask_root")
|
||||
self._mask_buffer = None
|
||||
self._mask_texture = None
|
||||
self._mask_cam = None
|
||||
self._mask_cam_np = None
|
||||
self._mask_shader = self._build_mask_shader()
|
||||
self._buffer_size = (0, 0)
|
||||
|
||||
@staticmethod
|
||||
def _is_empty(np):
|
||||
if not np:
|
||||
return True
|
||||
if hasattr(np, "isEmpty"):
|
||||
return np.isEmpty()
|
||||
if hasattr(np, "is_empty"):
|
||||
return np.is_empty()
|
||||
return False
|
||||
|
||||
@classmethod
|
||||
def is_outline_node(cls, node_path):
|
||||
if not node_path or cls._is_empty(node_path):
|
||||
return False
|
||||
name = node_path.getName() if hasattr(node_path, "getName") else ""
|
||||
if name.startswith(cls.OUTLINE_PREFIX):
|
||||
return True
|
||||
try:
|
||||
if node_path.hasPythonTag("selection_outline"):
|
||||
return True
|
||||
except Exception:
|
||||
pass
|
||||
return False
|
||||
|
||||
def set_enabled(self, enabled):
|
||||
self.enabled = bool(enabled)
|
||||
if not self.enabled:
|
||||
self.clear()
|
||||
self._apply_stage_inputs()
|
||||
|
||||
def set_targets(self, targets):
|
||||
if not self.enabled:
|
||||
self.clear()
|
||||
self._apply_stage_inputs()
|
||||
return
|
||||
|
||||
self._ensure_mask_resources()
|
||||
self.clear()
|
||||
|
||||
if not targets:
|
||||
self._apply_stage_inputs()
|
||||
return
|
||||
|
||||
seen = set()
|
||||
valid = []
|
||||
for target in targets:
|
||||
if self._is_empty(target) or self.is_outline_node(target):
|
||||
continue
|
||||
key = str(target)
|
||||
if key in seen:
|
||||
continue
|
||||
seen.add(key)
|
||||
valid.append(target)
|
||||
if len(valid) >= self.max_targets:
|
||||
break
|
||||
|
||||
for source in valid:
|
||||
self._clone_target(source)
|
||||
|
||||
if self._tracked:
|
||||
self._start_sync_task()
|
||||
self._sync_once()
|
||||
print(f"[SelectionOutline] targets={len(self._tracked)} active")
|
||||
else:
|
||||
print("[SelectionOutline] no valid targets for outline")
|
||||
|
||||
self._apply_stage_inputs()
|
||||
|
||||
def clear(self):
|
||||
self._stop_sync_task()
|
||||
for _, clone_np in self._tracked:
|
||||
if not self._is_empty(clone_np):
|
||||
clone_np.removeNode()
|
||||
self._tracked = []
|
||||
self._apply_stage_inputs()
|
||||
|
||||
def cleanup(self):
|
||||
self.clear()
|
||||
self._destroy_mask_resources()
|
||||
|
||||
def _build_mask_shader(self):
|
||||
return Shader.make(
|
||||
Shader.SL_GLSL,
|
||||
"""
|
||||
#version 430
|
||||
in vec4 p3d_Vertex;
|
||||
uniform mat4 p3d_ModelViewProjectionMatrix;
|
||||
void main() {
|
||||
gl_Position = p3d_ModelViewProjectionMatrix * p3d_Vertex;
|
||||
}
|
||||
""",
|
||||
"""
|
||||
#version 430
|
||||
out vec4 result;
|
||||
void main() {
|
||||
result = vec4(1.0, 1.0, 1.0, 1.0);
|
||||
}
|
||||
""",
|
||||
)
|
||||
|
||||
def _clone_target(self, source):
|
||||
try:
|
||||
clone_np = source.copyTo(self._mask_root)
|
||||
if self._is_empty(clone_np):
|
||||
return
|
||||
if not self._node_has_geom(clone_np):
|
||||
clone_np.removeNode()
|
||||
return
|
||||
clone_np.setName(f"{self.OUTLINE_PREFIX}_{source.getName()}")
|
||||
clone_np.setPythonTag("selection_outline", True)
|
||||
clone_np.setCollideMask(BitMask32.allOff())
|
||||
clone_np.setMat(self.app.render, source.getMat(self.app.render))
|
||||
self._tracked.append((source, clone_np))
|
||||
except Exception as exc:
|
||||
print(f"[SelectionOutline] clone failed: {exc}")
|
||||
|
||||
def _node_has_geom(self, np):
|
||||
if self._is_empty(np):
|
||||
return False
|
||||
try:
|
||||
node = np.node()
|
||||
if node and hasattr(node, "isGeomNode") and node.isGeomNode():
|
||||
return True
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
return not np.find("**/+GeomNode").isEmpty()
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
def _start_sync_task(self):
|
||||
taskMgr.remove(self._task_name)
|
||||
taskMgr.add(self._sync_task, self._task_name)
|
||||
|
||||
def _stop_sync_task(self):
|
||||
taskMgr.remove(self._task_name)
|
||||
|
||||
def _sync_task(self, task):
|
||||
self._sync_once()
|
||||
return task.cont
|
||||
|
||||
def _sync_once(self):
|
||||
if not self.enabled:
|
||||
self._apply_stage_inputs()
|
||||
return
|
||||
|
||||
self._ensure_mask_resources()
|
||||
alive = []
|
||||
for source, clone_np in self._tracked:
|
||||
if self._is_empty(source) or self._is_empty(clone_np):
|
||||
if not self._is_empty(clone_np):
|
||||
clone_np.removeNode()
|
||||
continue
|
||||
clone_np.setMat(self.app.render, source.getMat(self.app.render))
|
||||
alive.append((source, clone_np))
|
||||
self._tracked = alive
|
||||
self._apply_stage_inputs()
|
||||
|
||||
def _get_stage(self):
|
||||
rp = getattr(self.app, "render_pipeline", None)
|
||||
if not rp or not getattr(rp, "stage_mgr", None):
|
||||
return None
|
||||
return rp.stage_mgr.get_stage("SelectionOutlineStage")
|
||||
|
||||
def _apply_stage_inputs(self):
|
||||
stage = self._get_stage()
|
||||
if not stage:
|
||||
if not self._stage_missing_warned:
|
||||
print("[SelectionOutline] SelectionOutlineStage not found; plugin may be disabled.")
|
||||
self._stage_missing_warned = True
|
||||
return
|
||||
|
||||
self._stage_missing_warned = False
|
||||
stage.set_outline_style(
|
||||
color=self.outline_color,
|
||||
width_px=self.outline_width_px,
|
||||
fill_alpha=self.fill_alpha,
|
||||
)
|
||||
stage.set_mask_texture(self._mask_texture)
|
||||
stage.set_enabled_outline(self.enabled and bool(self._tracked))
|
||||
|
||||
def _get_window_size(self):
|
||||
if not getattr(self.app, "win", None):
|
||||
return 1, 1
|
||||
return max(1, self.app.win.getXSize()), max(1, self.app.win.getYSize())
|
||||
|
||||
def _ensure_mask_resources(self):
|
||||
size = self._get_window_size()
|
||||
if size != self._buffer_size:
|
||||
self._destroy_mask_resources()
|
||||
self._buffer_size = size
|
||||
if self._mask_buffer:
|
||||
return
|
||||
|
||||
if not getattr(self.app, "graphicsEngine", None) or not getattr(self.app, "win", None):
|
||||
return
|
||||
|
||||
w, h = self._buffer_size
|
||||
win_props = WindowProperties()
|
||||
win_props.setSize(w, h)
|
||||
fb_props = FrameBufferProperties()
|
||||
fb_props.setRgbaBits(8, 8, 8, 8)
|
||||
fb_props.setDepthBits(24)
|
||||
|
||||
self._mask_buffer = self.app.graphicsEngine.make_output(
|
||||
self.app.pipe,
|
||||
"selection_outline_mask",
|
||||
-80,
|
||||
fb_props,
|
||||
win_props,
|
||||
GraphicsPipe.BFRefuseWindow,
|
||||
self.app.win.getGsg(),
|
||||
self.app.win,
|
||||
)
|
||||
if not self._mask_buffer:
|
||||
print("[SelectionOutline] failed to create mask buffer")
|
||||
return
|
||||
|
||||
self._mask_texture = Texture("selection_outline_mask_tex")
|
||||
self._mask_texture.setMinfilter(Texture.FTNearest)
|
||||
self._mask_texture.setMagfilter(Texture.FTNearest)
|
||||
self._mask_buffer.addRenderTexture(self._mask_texture, GraphicsOutput.RTMBindOrCopy)
|
||||
self._mask_buffer.setClearColor(Vec4(0, 0, 0, 0))
|
||||
self._mask_buffer.setClearColorActive(True)
|
||||
self._mask_buffer.setActive(True)
|
||||
|
||||
self._mask_cam = Camera("selection_outline_mask_camera")
|
||||
self._mask_cam.setScene(self._mask_root)
|
||||
self._mask_cam.setLens(self.app.camLens)
|
||||
self._mask_cam_np = self.app.cam.attachNewNode(self._mask_cam)
|
||||
|
||||
dr = self._mask_buffer.makeDisplayRegion()
|
||||
dr.setCamera(self._mask_cam_np)
|
||||
|
||||
state_np = NodePath("selection_outline_mask_state")
|
||||
state_np.setShader(self._mask_shader, 10000)
|
||||
state_np.setLightOff(1)
|
||||
state_np.setMaterialOff(1)
|
||||
state_np.setTextureOff(1)
|
||||
state_np.setColorOff(1)
|
||||
self._mask_cam.setInitialState(state_np.getState())
|
||||
|
||||
def _destroy_mask_resources(self):
|
||||
if self._mask_cam_np and not self._is_empty(self._mask_cam_np):
|
||||
self._mask_cam_np.removeNode()
|
||||
self._mask_cam_np = None
|
||||
self._mask_cam = None
|
||||
|
||||
if self._mask_buffer and getattr(self.app, "graphicsEngine", None):
|
||||
try:
|
||||
self.app.graphicsEngine.removeWindow(self._mask_buffer)
|
||||
except Exception:
|
||||
pass
|
||||
self._mask_buffer = None
|
||||
self._mask_texture = None
|
||||
@ -49,6 +49,7 @@ from imgui_bundle import imgui
|
||||
from rpcore.effect import Effect
|
||||
|
||||
from .ssbo_controller import ObjectController
|
||||
from core.selection_outline import SelectionOutlineManager
|
||||
|
||||
class SSBOEditor:
|
||||
"""
|
||||
@ -91,6 +92,10 @@ class SSBOEditor:
|
||||
self.realtime_shadow_updates = False
|
||||
self._scheduler_tasks_original = None
|
||||
self._realtime_shadow_tasks_enabled = False
|
||||
self._outline_manager = getattr(self.base, "_selection_outline_manager", None)
|
||||
if self._outline_manager is None:
|
||||
self._outline_manager = SelectionOutlineManager(self.base)
|
||||
setattr(self.base, "_selection_outline_manager", self._outline_manager)
|
||||
|
||||
# Initialize ImGui Backend if not already present
|
||||
if not hasattr(self.base, 'imgui_backend'):
|
||||
@ -573,12 +578,40 @@ class SSBOEditor:
|
||||
if chunk_id is not None and chunk_id in self.controller.chunks:
|
||||
self.controller.chunks[chunk_id]["dirty"] = True
|
||||
|
||||
def _update_outline_for_selection(self):
|
||||
if not self._outline_manager:
|
||||
return
|
||||
if not self.controller or not self.selected_ids:
|
||||
self._outline_manager.clear()
|
||||
return
|
||||
|
||||
is_root_selection = (
|
||||
self.controller and
|
||||
self.selected_name == getattr(self.controller, "tree_root_key", None)
|
||||
)
|
||||
if is_root_selection:
|
||||
self._outline_manager.clear()
|
||||
return
|
||||
|
||||
targets = []
|
||||
target_limit = max(1, int(getattr(self._outline_manager, "max_targets", 64)))
|
||||
for gid in self.selected_ids:
|
||||
obj_np = self.controller.id_to_object_np.get(gid)
|
||||
if obj_np and not obj_np.is_empty():
|
||||
targets.append(obj_np)
|
||||
if len(targets) >= target_limit:
|
||||
break
|
||||
|
||||
self._outline_manager.set_targets(targets)
|
||||
|
||||
def clear_selection(self):
|
||||
self._stop_pick_sync_task()
|
||||
self._reset_pick_sync_cache()
|
||||
self._cleanup_group_proxy()
|
||||
self.selected_name = None
|
||||
self.selected_ids = []
|
||||
if self._outline_manager:
|
||||
self._outline_manager.clear()
|
||||
if self.controller:
|
||||
self.controller.set_active_ids([])
|
||||
if self._transform_gizmo:
|
||||
@ -637,6 +670,8 @@ class SSBOEditor:
|
||||
# keep static chunks active and transform the model root directly.
|
||||
if is_root_selection:
|
||||
self.controller.set_active_ids([])
|
||||
if self._outline_manager:
|
||||
self._outline_manager.clear()
|
||||
if self._transform_gizmo and self.model and not self.model.is_empty():
|
||||
self._transform_gizmo.attach(self.model)
|
||||
else:
|
||||
@ -646,6 +681,7 @@ class SSBOEditor:
|
||||
return
|
||||
|
||||
self.controller.set_active_ids(self.selected_ids)
|
||||
self._update_outline_for_selection()
|
||||
|
||||
if not self._transform_gizmo or not self.selected_ids:
|
||||
if self._transform_gizmo:
|
||||
|
||||
Loading…
Reference in New Issue
Block a user