脚本修复
This commit is contained in:
parent
22ccfcd55d
commit
b4fd0c2e68
15
imgui.ini
15
imgui.ini
@ -37,9 +37,9 @@ DockId=0x00000007,0
|
||||
|
||||
[Window][属性面板]
|
||||
Pos=1504,20
|
||||
Size=346,996
|
||||
Size=346,498
|
||||
Collapsed=0
|
||||
DockId=0x00000002,0
|
||||
DockId=0x00000003,0
|
||||
|
||||
[Window][控制台]
|
||||
Pos=341,617
|
||||
@ -48,9 +48,10 @@ Collapsed=0
|
||||
DockId=0x00000006,1
|
||||
|
||||
[Window][脚本管理]
|
||||
Pos=1950,20
|
||||
Size=610,995
|
||||
Pos=1504,520
|
||||
Size=346,496
|
||||
Collapsed=0
|
||||
DockId=0x00000004,0
|
||||
|
||||
[Window][中文显示测试]
|
||||
Pos=60,60
|
||||
@ -152,7 +153,7 @@ Collapsed=0
|
||||
Pos=2214,20
|
||||
Size=346,1331
|
||||
Collapsed=0
|
||||
DockId=0x00000002,1
|
||||
DockId=0x00000003,1
|
||||
|
||||
[Window][LUI测试控制面板]
|
||||
Pos=6,10
|
||||
@ -233,5 +234,7 @@ DockSpace ID=0x08BD597D Window=0x1BBC0F80 Pos=0,20 Size=1850,996 Split=X
|
||||
DockNode ID=0x0000000D Parent=0x00000005 SizeRef=1318,383 HiddenTabBar=1 Selected=0x43A39006
|
||||
DockNode ID=0x0000000E Parent=0x00000005 SizeRef=1318,363 CentralNode=1 Selected=0xE0015051
|
||||
DockNode ID=0x00000006 Parent=0x00000008 SizeRef=2048,399 Selected=0x3A2E05C3
|
||||
DockNode ID=0x00000002 Parent=0x08BD597D SizeRef=346,1012 Selected=0x5DB6FF37
|
||||
DockNode ID=0x00000002 Parent=0x08BD597D SizeRef=346,1012 Split=Y Selected=0x5DB6FF37
|
||||
DockNode ID=0x00000003 Parent=0x00000002 SizeRef=346,498 Selected=0x5DB6FF37
|
||||
DockNode ID=0x00000004 Parent=0x00000002 SizeRef=346,496 Selected=0x3188AB8D
|
||||
|
||||
|
||||
2
main.py
2
main.py
@ -333,7 +333,7 @@ class MyWorld(PanelDelegates, CoreWorld):
|
||||
"scene_tree": True,
|
||||
"property": True,
|
||||
"console": True,
|
||||
"script": not self.use_ssbo_mouse_picking,
|
||||
"script": True,
|
||||
"toolbar": True,
|
||||
"resources": True,
|
||||
"web": False,
|
||||
|
||||
@ -1374,6 +1374,7 @@ class WebGLPackager:
|
||||
out: List[Dict[str, Any]] = []
|
||||
loader = getattr(script_manager, "loader", None)
|
||||
find_script_file = getattr(loader, "find_script_file", None) if loader else None
|
||||
build_script_reference = getattr(script_manager, "build_script_reference", None)
|
||||
|
||||
for component in script_components:
|
||||
script_instance = getattr(component, "script_instance", None)
|
||||
@ -1395,7 +1396,17 @@ class WebGLPackager:
|
||||
"name": script_name,
|
||||
"enabled": bool(getattr(component, "enabled", True)),
|
||||
}
|
||||
if script_file:
|
||||
if callable(build_script_reference):
|
||||
try:
|
||||
reference = build_script_reference(script_name, script_file) or {}
|
||||
except Exception:
|
||||
reference = {}
|
||||
if isinstance(reference, dict):
|
||||
for key in ("file", "project_relative_path", "script_guid", "relative_path", "path"):
|
||||
value = str(reference.get(key, "") or "").strip()
|
||||
if value:
|
||||
entry[key] = value
|
||||
elif script_file:
|
||||
entry["file"] = script_file
|
||||
|
||||
params = self._serialize_script_instance_params(script_instance)
|
||||
@ -1436,9 +1447,10 @@ class WebGLPackager:
|
||||
"name": script_name,
|
||||
"enabled": bool(item.get("enabled", True)),
|
||||
}
|
||||
script_file = str(item.get("file", "")).strip()
|
||||
if script_file:
|
||||
entry["file"] = script_file
|
||||
for key in ("file", "project_relative_path", "script_guid", "relative_path", "path"):
|
||||
value = str(item.get(key, "") or "").strip()
|
||||
if value:
|
||||
entry[key] = value
|
||||
params = item.get("params")
|
||||
if isinstance(params, dict) and params:
|
||||
entry["params"] = params
|
||||
|
||||
@ -1,160 +1,354 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
颜色变化脚本 - 让对象颜色产生循环变化
|
||||
"""
|
||||
|
||||
from core.script_system import ScriptBase
|
||||
from panda3d.core import Vec4
|
||||
import math
|
||||
|
||||
class ColorChangerScript(ScriptBase):
|
||||
"""颜色变化脚本类"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
# 颜色参数
|
||||
self.color_speed = 1.0 # 颜色变化速度 (周期/秒)
|
||||
self.color_mode = "rainbow" # 颜色模式: "rainbow", "pulse", "fade", "strobe"
|
||||
self.base_color = Vec4(1, 1, 1, 1) # 基础颜色
|
||||
self.intensity = 1.0 # 颜色强度
|
||||
|
||||
# 内部变量
|
||||
self.time_accumulator = 0.0 # 时间累积器
|
||||
self.original_color = None # 原始颜色
|
||||
self.is_changing = True # 是否正在变化
|
||||
self.strobe_state = False # 闪烁状态
|
||||
|
||||
def start(self):
|
||||
"""脚本开始时调用"""
|
||||
self.log("颜色变化脚本启动!")
|
||||
self.log(f"颜色参数: 速度={self.color_speed}, 模式={self.color_mode}, 强度={self.intensity}")
|
||||
|
||||
# 记录原始颜色
|
||||
self.original_color = self.gameObject.getColor()
|
||||
self.log(f"原始颜色: {self.original_color}")
|
||||
|
||||
def update(self, dt):
|
||||
"""每帧更新"""
|
||||
if not self.is_changing:
|
||||
return
|
||||
|
||||
# 累积时间
|
||||
self.time_accumulator += dt
|
||||
|
||||
# 根据模式计算新颜色
|
||||
if self.color_mode == "rainbow":
|
||||
new_color = self._calculate_rainbow_color()
|
||||
elif self.color_mode == "pulse":
|
||||
new_color = self._calculate_pulse_color()
|
||||
elif self.color_mode == "fade":
|
||||
new_color = self._calculate_fade_color()
|
||||
elif self.color_mode == "strobe":
|
||||
new_color = self._calculate_strobe_color()
|
||||
else:
|
||||
new_color = self.base_color
|
||||
|
||||
# 应用颜色
|
||||
self.gameObject.setColor(new_color)
|
||||
|
||||
def _calculate_rainbow_color(self):
|
||||
"""计算彩虹颜色"""
|
||||
# 使用HSV到RGB的转换创建彩虹效果
|
||||
hue = (self.time_accumulator * self.color_speed) % 1.0
|
||||
|
||||
# 简单的HSV到RGB转换
|
||||
i = int(hue * 6.0)
|
||||
f = (hue * 6.0) - i
|
||||
p = 0.0
|
||||
q = 1.0 - f
|
||||
t = f
|
||||
|
||||
if i % 6 == 0:
|
||||
r, g, b = 1.0, t, p
|
||||
elif i % 6 == 1:
|
||||
r, g, b = q, 1.0, p
|
||||
elif i % 6 == 2:
|
||||
r, g, b = p, 1.0, t
|
||||
elif i % 6 == 3:
|
||||
r, g, b = p, q, 1.0
|
||||
elif i % 6 == 4:
|
||||
r, g, b = t, p, 1.0
|
||||
else:
|
||||
r, g, b = 1.0, p, q
|
||||
|
||||
return Vec4(r * self.intensity, g * self.intensity, b * self.intensity, 1.0)
|
||||
|
||||
def _calculate_pulse_color(self):
|
||||
"""计算脉冲颜色"""
|
||||
pulse = (math.sin(self.time_accumulator * self.color_speed * 2 * math.pi) + 1.0) / 2.0
|
||||
multiplier = pulse * self.intensity
|
||||
return Vec4(
|
||||
self.base_color.getX() * multiplier,
|
||||
self.base_color.getY() * multiplier,
|
||||
self.base_color.getZ() * multiplier,
|
||||
self.base_color.getW()
|
||||
)
|
||||
|
||||
def _calculate_fade_color(self):
|
||||
"""计算淡入淡出颜色"""
|
||||
fade = (math.sin(self.time_accumulator * self.color_speed * 2 * math.pi) + 1.0) / 2.0
|
||||
alpha = fade * self.intensity
|
||||
return Vec4(
|
||||
self.base_color.getX(),
|
||||
self.base_color.getY(),
|
||||
self.base_color.getZ(),
|
||||
alpha
|
||||
)
|
||||
|
||||
def _calculate_strobe_color(self):
|
||||
"""计算闪烁颜色"""
|
||||
# 根据时间间隔切换状态
|
||||
interval = 1.0 / (self.color_speed * 2) # 闪烁间隔
|
||||
if int(self.time_accumulator / interval) % 2 == 0:
|
||||
return Vec4(
|
||||
self.base_color.getX() * self.intensity,
|
||||
self.base_color.getY() * self.intensity,
|
||||
self.base_color.getZ() * self.intensity,
|
||||
self.base_color.getW()
|
||||
)
|
||||
else:
|
||||
return Vec4(0.1, 0.1, 0.1, self.base_color.getW()) # 暗色状态
|
||||
|
||||
def set_color_parameters(self, speed=None, mode=None, base_color=None, intensity=None):
|
||||
"""设置颜色参数"""
|
||||
if speed is not None:
|
||||
self.color_speed = speed
|
||||
if mode is not None and mode in ["rainbow", "pulse", "fade", "strobe"]:
|
||||
self.color_mode = mode
|
||||
if base_color is not None:
|
||||
self.base_color = base_color
|
||||
if intensity is not None:
|
||||
self.intensity = intensity
|
||||
|
||||
self.log(f"颜色参数更新: 速度={self.color_speed}, 模式={self.color_mode}, 强度={self.intensity}")
|
||||
|
||||
def toggle_color_change(self):
|
||||
"""切换颜色变化状态"""
|
||||
self.is_changing = not self.is_changing
|
||||
status = "恢复" if self.is_changing else "暂停"
|
||||
self.log(f"颜色变化{status}")
|
||||
|
||||
def reset_color(self):
|
||||
"""重置到原始颜色"""
|
||||
if self.original_color:
|
||||
self.gameObject.setColor(self.original_color)
|
||||
self.time_accumulator = 0.0
|
||||
self.log("颜色已重置到原始值")
|
||||
|
||||
def set_solid_color(self, r=1.0, g=1.0, b=1.0, a=1.0):
|
||||
"""设置固定颜色"""
|
||||
color = Vec4(r, g, b, a)
|
||||
self.gameObject.setColor(color)
|
||||
self.base_color = color
|
||||
self.log(f"设置固定颜色: {color}")
|
||||
|
||||
def on_destroy(self):
|
||||
"""脚本销毁时调用"""
|
||||
self.log("颜色变化脚本停止")
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
颜色变化脚本 - 让对象颜色产生循环变化
|
||||
"""
|
||||
|
||||
from core.script_system import ScriptBase
|
||||
from panda3d.core import TransparencyAttrib, Vec4
|
||||
import math
|
||||
|
||||
|
||||
class ColorChangerScript(ScriptBase):
|
||||
"""颜色变化脚本类"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
# 颜色参数
|
||||
self.color_speed = 1.0 # 颜色变化速度 (周期/秒)
|
||||
self.color_mode = "rainbow" # 颜色模式: "rainbow", "pulse", "fade", "strobe"
|
||||
self.base_color = Vec4(1, 1, 1, 1) # 基础颜色
|
||||
self.intensity = 1.0 # 颜色强度
|
||||
|
||||
# 内部变量
|
||||
self.time_accumulator = 0.0 # 时间累积器
|
||||
self.original_color = None # 原始颜色
|
||||
self.is_changing = True # 是否正在变化
|
||||
self.strobe_state = False # 闪烁状态
|
||||
|
||||
# 渲染目标缓存(避免每帧重新遍历)
|
||||
self._target_nodes = []
|
||||
self._material_targets = []
|
||||
self._property_helpers = None
|
||||
|
||||
def _is_node_valid(self, node):
|
||||
if not node:
|
||||
return False
|
||||
try:
|
||||
return not node.isEmpty()
|
||||
except Exception:
|
||||
try:
|
||||
return not node.is_empty()
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
def _collect_target_nodes(self):
|
||||
"""收集需要同步颜色的节点(根 + GeomNode 后代)"""
|
||||
targets = []
|
||||
if self._is_node_valid(self.gameObject):
|
||||
targets.append(self.gameObject)
|
||||
try:
|
||||
for geom_np in self.gameObject.findAllMatches("**/+GeomNode"):
|
||||
if self._is_node_valid(geom_np):
|
||||
targets.append(geom_np)
|
||||
except Exception:
|
||||
pass
|
||||
return targets
|
||||
|
||||
@staticmethod
|
||||
def _clone_material(material):
|
||||
try:
|
||||
if hasattr(material, "makeCopy"):
|
||||
return material.makeCopy()
|
||||
except Exception:
|
||||
pass
|
||||
return material
|
||||
|
||||
@staticmethod
|
||||
def _set_material_base_color(material, color):
|
||||
color_vec = Vec4(color)
|
||||
try:
|
||||
if hasattr(material, "set_base_color"):
|
||||
material.set_base_color(color_vec)
|
||||
return
|
||||
if hasattr(material, "setBaseColor"):
|
||||
material.setBaseColor(color_vec)
|
||||
return
|
||||
if hasattr(material, "setDiffuse"):
|
||||
material.setDiffuse(color_vec)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def _prepare_material_targets(self):
|
||||
"""确保每个目标节点都有可编辑材质实例,避免改到共享材质。"""
|
||||
helper = self._property_helpers
|
||||
targets = []
|
||||
seen = set()
|
||||
for node in self._target_nodes:
|
||||
if not self._is_node_valid(node):
|
||||
continue
|
||||
try:
|
||||
materials = []
|
||||
# 优先走编辑器现有材质通路:可覆盖 GeomState 中的材质而不仅是 NodePath 材质。
|
||||
if helper:
|
||||
try:
|
||||
if hasattr(helper, "_ensure_unique_materials_for_node"):
|
||||
helper._ensure_unique_materials_for_node(node)
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
if hasattr(helper, "_get_node_materials"):
|
||||
materials = list(helper._get_node_materials(node) or [])
|
||||
except Exception:
|
||||
materials = []
|
||||
|
||||
if not materials:
|
||||
if node.hasMaterial():
|
||||
mat = node.getMaterial()
|
||||
if mat is not None:
|
||||
materials = [mat]
|
||||
|
||||
if not materials:
|
||||
continue
|
||||
|
||||
for mat in materials:
|
||||
if mat is None:
|
||||
continue
|
||||
material_key = id(mat)
|
||||
pair_key = (id(node), material_key)
|
||||
if pair_key in seen:
|
||||
continue
|
||||
seen.add(pair_key)
|
||||
targets.append((node, mat))
|
||||
except Exception:
|
||||
continue
|
||||
self._material_targets = targets
|
||||
|
||||
def _apply_color(self, color):
|
||||
"""同时更新 NodePath 颜色、ColorScale、材质颜色与透明度状态。"""
|
||||
if not self._is_node_valid(self.gameObject):
|
||||
return
|
||||
|
||||
color = Vec4(color)
|
||||
alpha = float(max(0.0, min(1.0, color.getW())))
|
||||
rgba_tuple = (float(color.getX()), float(color.getY()), float(color.getZ()), alpha)
|
||||
|
||||
for node in self._target_nodes:
|
||||
if not self._is_node_valid(node):
|
||||
continue
|
||||
try:
|
||||
node.setColor(*rgba_tuple)
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
node.setColorScale(*rgba_tuple)
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
node.setShaderInput("material_base_color", rgba_tuple)
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
node.setShaderInput("material_opacity", alpha)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if alpha < 0.999:
|
||||
try:
|
||||
node.setTransparency(TransparencyAttrib.M_alpha)
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
node.setAlphaScale(alpha)
|
||||
except Exception:
|
||||
pass
|
||||
else:
|
||||
try:
|
||||
node.setTransparency(TransparencyAttrib.M_none)
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
node.setAlphaScale(1.0)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
for node, material in self._material_targets:
|
||||
if not self._is_node_valid(node):
|
||||
continue
|
||||
try:
|
||||
if self._property_helpers and hasattr(self._property_helpers, "_set_material_base_color"):
|
||||
self._property_helpers._set_material_base_color(material, rgba_tuple)
|
||||
else:
|
||||
self._set_material_base_color(material, rgba_tuple)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
try:
|
||||
if self._property_helpers and hasattr(self._property_helpers, "_sync_material_node_runtime"):
|
||||
# 刷新 runtime 材质状态,但避免在每帧触发 source->runtime 全量重建。
|
||||
self._property_helpers._sync_material_node_runtime(
|
||||
node,
|
||||
material,
|
||||
refresh_ssbo_runtime=False,
|
||||
)
|
||||
else:
|
||||
node.setMaterial(material, 1)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
try:
|
||||
if self._property_helpers and hasattr(self._property_helpers, "_invalidate_material_render_cache"):
|
||||
self._property_helpers._invalidate_material_render_cache()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def start(self):
|
||||
"""脚本开始时调用"""
|
||||
self.log("颜色变化脚本启动!")
|
||||
self.log(f"颜色参数: 速度={self.color_speed}, 模式={self.color_mode}, 强度={self.intensity}")
|
||||
|
||||
if not self._is_node_valid(self.gameObject):
|
||||
self.log("警告: 挂载对象不可用,颜色变化不会生效")
|
||||
return
|
||||
|
||||
# 记录原始颜色
|
||||
try:
|
||||
self.original_color = self.gameObject.getColor()
|
||||
except Exception:
|
||||
self.original_color = Vec4(1, 1, 1, 1)
|
||||
self.log(f"原始颜色: {self.original_color}")
|
||||
|
||||
self._property_helpers = getattr(self.world, "property_helpers", None)
|
||||
self._target_nodes = self._collect_target_nodes()
|
||||
self._prepare_material_targets()
|
||||
self.log(
|
||||
f"颜色目标节点: {len(self._target_nodes)},材质目标: {len(self._material_targets)}"
|
||||
)
|
||||
|
||||
def update(self, dt):
|
||||
"""每帧更新"""
|
||||
if not self.is_changing:
|
||||
return
|
||||
if not self._is_node_valid(self.gameObject):
|
||||
return
|
||||
|
||||
# 累积时间
|
||||
self.time_accumulator += dt
|
||||
|
||||
# 根据模式计算新颜色
|
||||
if self.color_mode == "rainbow":
|
||||
new_color = self._calculate_rainbow_color()
|
||||
elif self.color_mode == "pulse":
|
||||
new_color = self._calculate_pulse_color()
|
||||
elif self.color_mode == "fade":
|
||||
new_color = self._calculate_fade_color()
|
||||
elif self.color_mode == "strobe":
|
||||
new_color = self._calculate_strobe_color()
|
||||
else:
|
||||
new_color = self.base_color
|
||||
|
||||
# 应用颜色
|
||||
self._apply_color(new_color)
|
||||
|
||||
def _calculate_rainbow_color(self):
|
||||
"""计算彩虹颜色"""
|
||||
# 使用HSV到RGB的转换创建彩虹效果
|
||||
hue = (self.time_accumulator * self.color_speed) % 1.0
|
||||
|
||||
# 简单的HSV到RGB转换
|
||||
i = int(hue * 6.0)
|
||||
f = (hue * 6.0) - i
|
||||
p = 0.0
|
||||
q = 1.0 - f
|
||||
t = f
|
||||
|
||||
if i % 6 == 0:
|
||||
r, g, b = 1.0, t, p
|
||||
elif i % 6 == 1:
|
||||
r, g, b = q, 1.0, p
|
||||
elif i % 6 == 2:
|
||||
r, g, b = p, 1.0, t
|
||||
elif i % 6 == 3:
|
||||
r, g, b = p, q, 1.0
|
||||
elif i % 6 == 4:
|
||||
r, g, b = t, p, 1.0
|
||||
else:
|
||||
r, g, b = 1.0, p, q
|
||||
|
||||
return Vec4(r * self.intensity, g * self.intensity, b * self.intensity, 1.0)
|
||||
|
||||
def _calculate_pulse_color(self):
|
||||
"""计算脉冲颜色"""
|
||||
pulse = (math.sin(self.time_accumulator * self.color_speed * 2 * math.pi) + 1.0) / 2.0
|
||||
multiplier = pulse * self.intensity
|
||||
return Vec4(
|
||||
self.base_color.getX() * multiplier,
|
||||
self.base_color.getY() * multiplier,
|
||||
self.base_color.getZ() * multiplier,
|
||||
self.base_color.getW(),
|
||||
)
|
||||
|
||||
def _calculate_fade_color(self):
|
||||
"""计算淡入淡出颜色"""
|
||||
fade = (math.sin(self.time_accumulator * self.color_speed * 2 * math.pi) + 1.0) / 2.0
|
||||
alpha = fade * self.intensity
|
||||
return Vec4(
|
||||
self.base_color.getX(),
|
||||
self.base_color.getY(),
|
||||
self.base_color.getZ(),
|
||||
alpha,
|
||||
)
|
||||
|
||||
def _calculate_strobe_color(self):
|
||||
"""计算闪烁颜色"""
|
||||
# 根据时间间隔切换状态
|
||||
speed = max(float(self.color_speed), 1e-4)
|
||||
interval = 1.0 / (speed * 2.0) # 闪烁间隔
|
||||
if int(self.time_accumulator / interval) % 2 == 0:
|
||||
return Vec4(
|
||||
self.base_color.getX() * self.intensity,
|
||||
self.base_color.getY() * self.intensity,
|
||||
self.base_color.getZ() * self.intensity,
|
||||
self.base_color.getW(),
|
||||
)
|
||||
return Vec4(0.1, 0.1, 0.1, self.base_color.getW()) # 暗色状态
|
||||
|
||||
def set_color_parameters(self, speed=None, mode=None, base_color=None, intensity=None):
|
||||
"""设置颜色参数"""
|
||||
if speed is not None:
|
||||
self.color_speed = speed
|
||||
if mode is not None and mode in ["rainbow", "pulse", "fade", "strobe"]:
|
||||
self.color_mode = mode
|
||||
if base_color is not None:
|
||||
self.base_color = Vec4(base_color)
|
||||
if intensity is not None:
|
||||
self.intensity = intensity
|
||||
|
||||
self.log(f"颜色参数更新: 速度={self.color_speed}, 模式={self.color_mode}, 强度={self.intensity}")
|
||||
|
||||
def toggle_color_change(self):
|
||||
"""切换颜色变化状态"""
|
||||
self.is_changing = not self.is_changing
|
||||
status = "恢复" if self.is_changing else "暂停"
|
||||
self.log(f"颜色变化{status}")
|
||||
|
||||
def reset_color(self):
|
||||
"""重置到原始颜色"""
|
||||
if self.original_color is None:
|
||||
return
|
||||
self._apply_color(self.original_color)
|
||||
self.time_accumulator = 0.0
|
||||
self.log("颜色已重置到原始值")
|
||||
|
||||
def set_solid_color(self, r=1.0, g=1.0, b=1.0, a=1.0):
|
||||
"""设置固定颜色"""
|
||||
color = Vec4(r, g, b, a)
|
||||
self.base_color = color
|
||||
self._apply_color(color)
|
||||
self.log(f"设置固定颜色: {color}")
|
||||
|
||||
def on_destroy(self):
|
||||
"""脚本销毁时调用"""
|
||||
self.log("颜色变化脚本停止")
|
||||
|
||||
@ -259,6 +259,26 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<div>
|
||||
<label for="script-select">Web Script</label>
|
||||
<select id="script-select">
|
||||
<option value="MoverScript">MoverScript</option>
|
||||
<option value="RotatorScript">RotatorScript</option>
|
||||
<option value="ScalerScript">ScalerScript</option>
|
||||
<option value="ColorChangerScript">ColorChangerScript</option>
|
||||
<option value="BouncerScript">BouncerScript</option>
|
||||
<option value="FollowerScript">FollowerScript</option>
|
||||
<option value="ComboAnimatorScript">ComboAnimatorScript</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="row two">
|
||||
<button id="attach-script-btn">Attach Script</button>
|
||||
<button id="detach-script-btn" class="secondary">Detach Script</button>
|
||||
</div>
|
||||
<div class="hint">这是 Web 端支持脚本示例。浏览器不会直接执行 Python 文件,而是按脚本名挂载对应的 Web 适配逻辑。</div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<label for="log-box">Message Log</label>
|
||||
<textarea id="log-box" readonly></textarea>
|
||||
@ -280,6 +300,7 @@
|
||||
const logBox = document.getElementById("log-box");
|
||||
const nodeSelect = document.getElementById("node-select");
|
||||
const clipInput = document.getElementById("clip-input");
|
||||
const scriptSelect = document.getElementById("script-select");
|
||||
|
||||
const pendingReplies = new Map();
|
||||
let sequence = 0;
|
||||
@ -389,6 +410,26 @@
|
||||
return nodes;
|
||||
}
|
||||
|
||||
async function refreshSupportedScripts() {
|
||||
const supported = await sendCommand("scripts.listSupported");
|
||||
if (!Array.isArray(supported) || supported.length === 0) {
|
||||
return supported;
|
||||
}
|
||||
|
||||
const currentValue = scriptSelect.value;
|
||||
scriptSelect.innerHTML = "";
|
||||
for (const script of supported) {
|
||||
const option = document.createElement("option");
|
||||
option.value = script.name || script.key || "";
|
||||
option.textContent = (script.name || script.key || "script") + (script.summary ? " - " + script.summary : "");
|
||||
scriptSelect.appendChild(option);
|
||||
}
|
||||
if (currentValue) {
|
||||
scriptSelect.value = currentValue;
|
||||
}
|
||||
return supported;
|
||||
}
|
||||
|
||||
window.addEventListener("message", (event) => {
|
||||
const data = event.data;
|
||||
if (!data || data.source !== "eg-webgl") {
|
||||
@ -437,6 +478,7 @@
|
||||
document.getElementById("handshake-btn").addEventListener("click", () => {
|
||||
runAction("handshake", async () => {
|
||||
const result = await sendHandshake();
|
||||
await refreshSupportedScripts();
|
||||
if (result.ready && !readyReceived) {
|
||||
await refreshNodes();
|
||||
}
|
||||
@ -571,6 +613,27 @@
|
||||
});
|
||||
});
|
||||
|
||||
document.getElementById("attach-script-btn").addEventListener("click", () => {
|
||||
runAction("scripts.attach", async () => {
|
||||
const scriptName = scriptSelect.value;
|
||||
return sendCommand("scripts.attach", {
|
||||
selector: getSelectedSelector(),
|
||||
script: {
|
||||
name: scriptName,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
document.getElementById("detach-script-btn").addEventListener("click", () => {
|
||||
runAction("scripts.detach", async () => {
|
||||
return sendCommand("scripts.detach", {
|
||||
selector: getSelectedSelector(),
|
||||
name: scriptSelect.value,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
viewerFrame.addEventListener("load", () => {
|
||||
setStatus("Viewer iframe loaded. Click Handshake to start.");
|
||||
});
|
||||
|
||||
@ -35,6 +35,18 @@ viewer.nodes.setTransform(
|
||||
{ coordSystem: "panda", space: "local" },
|
||||
);
|
||||
|
||||
const supportedScripts = viewer.scripts.listSupported();
|
||||
console.log("web supported scripts", supportedScripts);
|
||||
|
||||
viewer.scripts.attach({ publicId: hero.publicId }, {
|
||||
name: "MoverScript",
|
||||
params: {
|
||||
move_axis: "x",
|
||||
move_distance: 3,
|
||||
move_speed: 1.5,
|
||||
},
|
||||
});
|
||||
|
||||
viewer.events.on("animationFinished", (payload) => {
|
||||
console.log("animation finished", payload);
|
||||
});</code></pre>
|
||||
@ -56,14 +68,18 @@ iframe.contentWindow.postMessage({
|
||||
source: "eg-frontend",
|
||||
type: "command",
|
||||
id: "cmd-1",
|
||||
command: "animation.play",
|
||||
command: "scripts.attach",
|
||||
payload: {
|
||||
selector: { publicId: "model:asset-guid:root" },
|
||||
options: { clip: "Idle", loop: true },
|
||||
script: {
|
||||
name: "RotatorScript",
|
||||
params: { rotation_speed_y: 45 },
|
||||
},
|
||||
},
|
||||
}, iframe.src ? new URL(iframe.src).origin : "*");</code></pre>
|
||||
<p><strong>Message protocol:</strong> commands use <code>{ source: "eg-frontend", id, type: "command", command, payload }</code>. Responses/events use <code>{ source: "eg-webgl", type, replyTo?, event?, ok?, result?, error? }</code>.</p>
|
||||
<p><strong>Events:</strong> <code>ready</code>, <code>error</code>, <code>animationFinished</code>, <code>stateChanged</code>.</p>
|
||||
<p><strong>Script note:</strong> exported WebGL can read existing script metadata, but browser-side runtime only executes Web-supported script adapters, not arbitrary Python files.</p>
|
||||
<p><strong>Demo page:</strong> after export, open <code>frontend_demo.html</code> to see a working iframe host example.</p>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
@ -1597,6 +1597,142 @@ function normalizeScriptName(name) {
|
||||
return canonicalizeNameSegment(text);
|
||||
}
|
||||
|
||||
function buildSupportedScriptCatalog() {
|
||||
const descriptors = [
|
||||
{
|
||||
key: "moverscript",
|
||||
name: "MoverScript",
|
||||
aliases: ["mover"],
|
||||
summary: "Move the node back and forth along one Panda axis.",
|
||||
defaultParams: {
|
||||
move_axis: "x",
|
||||
move_distance: 5.0,
|
||||
move_speed: 2.0,
|
||||
current_direction: 1,
|
||||
current_distance: 0.0,
|
||||
is_moving: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "rotatorscript",
|
||||
name: "RotatorScript",
|
||||
aliases: ["rotator"],
|
||||
summary: "Rotate the node continuously around the Y axis in Web view.",
|
||||
defaultParams: {
|
||||
rotation_speed_y: 30.0,
|
||||
is_rotating: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "scalerscript",
|
||||
name: "ScalerScript",
|
||||
aliases: ["scaler"],
|
||||
summary: "Animate the node scale with a sine wave.",
|
||||
defaultParams: {
|
||||
base_scale: 1.0,
|
||||
scale_amplitude: 0.3,
|
||||
scale_speed: 2.0,
|
||||
uniform_scale: true,
|
||||
time_accumulator: 0.0,
|
||||
is_scaling: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "colorchangerscript",
|
||||
name: "ColorChangerScript",
|
||||
aliases: ["colorchanger"],
|
||||
summary: "Animate mesh material color or opacity over time.",
|
||||
defaultParams: {
|
||||
color_speed: 1.0,
|
||||
color_mode: "rainbow",
|
||||
base_color: [1, 1, 1, 1],
|
||||
intensity: 1.0,
|
||||
time_accumulator: 0.0,
|
||||
is_changing: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "bouncerscript",
|
||||
name: "BouncerScript",
|
||||
aliases: ["bouncer"],
|
||||
summary: "Bounce the node vertically using a simple waveform.",
|
||||
defaultParams: {
|
||||
jump_height: 2.0,
|
||||
jump_speed: 3.0,
|
||||
bounce_type: "sine",
|
||||
time_accumulator: 0.0,
|
||||
is_bouncing: true,
|
||||
bounce_direction: 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "followerscript",
|
||||
name: "FollowerScript",
|
||||
aliases: ["follower"],
|
||||
summary: "Follow another exported node selected by id/publicId/name reference.",
|
||||
defaultParams: {
|
||||
follow_speed: 5.0,
|
||||
follow_distance: 2.0,
|
||||
is_following: true,
|
||||
target_ref: null,
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "comboanimatorscript",
|
||||
name: "ComboAnimatorScript",
|
||||
aliases: ["comboanimator"],
|
||||
summary: "Rotate and bounce the node using a built-in Web combo animation.",
|
||||
defaultParams: {
|
||||
time: 0.0,
|
||||
is_active: true,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const catalog = new Map();
|
||||
for (const descriptor of descriptors) {
|
||||
const aliases = Array.from(new Set([
|
||||
descriptor.key,
|
||||
descriptor.name,
|
||||
...(Array.isArray(descriptor.aliases) ? descriptor.aliases : []),
|
||||
].map((value) => normalizeScriptName(value)).filter((value) => !!value)));
|
||||
const normalized = {
|
||||
key: normalizeScriptName(descriptor.key || descriptor.name || ""),
|
||||
name: String(descriptor.name || descriptor.key || ""),
|
||||
aliases,
|
||||
summary: String(descriptor.summary || ""),
|
||||
defaultParams: cloneJsonSafe(descriptor.defaultParams, {}),
|
||||
};
|
||||
for (const alias of aliases) {
|
||||
catalog.set(alias, normalized);
|
||||
}
|
||||
}
|
||||
return catalog;
|
||||
}
|
||||
|
||||
const WEB_SUPPORTED_SCRIPT_CATALOG = buildSupportedScriptCatalog();
|
||||
|
||||
function getWebSupportedScriptDescriptor(scriptName) {
|
||||
return WEB_SUPPORTED_SCRIPT_CATALOG.get(normalizeScriptName(scriptName)) || null;
|
||||
}
|
||||
|
||||
function listWebSupportedScripts() {
|
||||
const out = [];
|
||||
const seen = new Set();
|
||||
for (const descriptor of WEB_SUPPORTED_SCRIPT_CATALOG.values()) {
|
||||
if (!descriptor?.key || seen.has(descriptor.key)) continue;
|
||||
seen.add(descriptor.key);
|
||||
out.push({
|
||||
key: descriptor.key,
|
||||
name: descriptor.name,
|
||||
aliases: Array.isArray(descriptor.aliases) ? descriptor.aliases.slice() : [],
|
||||
summary: descriptor.summary || "",
|
||||
defaultParams: cloneJsonSafe(descriptor.defaultParams, {}),
|
||||
});
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function markObjectTransformDirty(obj) {
|
||||
if (!obj || !obj.isObject3D) return;
|
||||
obj.updateMatrix();
|
||||
@ -2006,6 +2142,7 @@ function createScriptState(THREE, scriptCfg, entry, nodeMap, nodeNameLookup, own
|
||||
const withOwnerState = (state) => ({
|
||||
ownerId: entry?.id || "",
|
||||
ownerPublicId: entry?.publicId || "",
|
||||
params: cloneJsonSafe(params, {}),
|
||||
...state,
|
||||
});
|
||||
|
||||
@ -2235,10 +2372,9 @@ function createScriptState(THREE, scriptCfg, entry, nodeMap, nodeNameLookup, own
|
||||
return null;
|
||||
}
|
||||
|
||||
function buildScriptRuntimeStates(THREE, nodes, nodeMap) {
|
||||
function buildScriptRuntimeStates(THREE, nodes, nodeMap, nodeNameLookup = buildNodeNameLookup(nodeMap)) {
|
||||
const runtimes = [];
|
||||
let unsupportedCount = 0;
|
||||
const nodeNameLookup = buildNodeNameLookup(nodeMap);
|
||||
|
||||
for (const node of nodes) {
|
||||
const entry = node;
|
||||
@ -2270,6 +2406,217 @@ function buildScriptRuntimeStates(THREE, nodes, nodeMap) {
|
||||
return { runtimes, unsupportedCount };
|
||||
}
|
||||
|
||||
function ensureDeclaredScripts(entry) {
|
||||
if (!entry?.nodeData || typeof entry.nodeData !== "object") {
|
||||
throw createViewerError("invalid_node_data", "Node data is unavailable for script operations", {
|
||||
node: buildNodeSelectorPayload(entry),
|
||||
});
|
||||
}
|
||||
if (!Array.isArray(entry.nodeData.scripts)) {
|
||||
entry.nodeData.scripts = [];
|
||||
}
|
||||
return entry.nodeData.scripts;
|
||||
}
|
||||
|
||||
function findDeclaredScriptIndex(entry, scriptNameOrKey) {
|
||||
const declared = Array.isArray(entry?.nodeData?.scripts) ? entry.nodeData.scripts : [];
|
||||
const wantedKey = normalizeScriptName(scriptNameOrKey);
|
||||
if (!wantedKey) return -1;
|
||||
for (let i = 0; i < declared.length; i += 1) {
|
||||
const key = normalizeScriptName(declared[i]?.name || declared[i]?.key || "");
|
||||
if (key === wantedKey) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
function normalizeScriptConfigInput(scriptInput, fallbackName = "") {
|
||||
if (typeof scriptInput === "string") {
|
||||
const trimmed = String(scriptInput).trim();
|
||||
if (!trimmed) {
|
||||
throw createViewerError("invalid_script_config", "Script name must not be empty");
|
||||
}
|
||||
return { name: trimmed, enabled: true, params: {} };
|
||||
}
|
||||
|
||||
if (!scriptInput || typeof scriptInput !== "object") {
|
||||
throw createViewerError("invalid_script_config", "Script config must be a string or object");
|
||||
}
|
||||
|
||||
const name = String(scriptInput.name || fallbackName || "").trim();
|
||||
if (!name) {
|
||||
throw createViewerError("invalid_script_config", "Script config must include a name");
|
||||
}
|
||||
|
||||
const params = (scriptInput.params && typeof scriptInput.params === "object")
|
||||
? cloneJsonSafe(scriptInput.params, {})
|
||||
: {};
|
||||
|
||||
const out = {
|
||||
name,
|
||||
params,
|
||||
};
|
||||
|
||||
if (Object.prototype.hasOwnProperty.call(scriptInput, "enabled")) {
|
||||
out.enabled = toBoolean(scriptInput.enabled, true);
|
||||
}
|
||||
|
||||
for (const key of ["file", "project_relative_path", "script_guid", "relative_path", "path"]) {
|
||||
const value = String(scriptInput[key] || "").trim();
|
||||
if (value) {
|
||||
out[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
function mergeScriptConfig(existingConfig, nextConfig) {
|
||||
const existing = (existingConfig && typeof existingConfig === "object") ? existingConfig : {};
|
||||
const next = normalizeScriptConfigInput(nextConfig, existing?.name || "");
|
||||
return {
|
||||
...cloneJsonSafe(existing, {}),
|
||||
...next,
|
||||
name: next.name || String(existing?.name || ""),
|
||||
enabled: Object.prototype.hasOwnProperty.call(next, "enabled")
|
||||
? !!next.enabled
|
||||
: toBoolean(existing?.enabled, true),
|
||||
params: {
|
||||
...(existing?.params && typeof existing.params === "object" ? cloneJsonSafe(existing.params, {}) : {}),
|
||||
...(next?.params && typeof next.params === "object" ? cloneJsonSafe(next.params, {}) : {}),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function refreshUnsupportedScriptStats(runtime) {
|
||||
let unsupportedCount = 0;
|
||||
for (const entry of runtime?.nodeOrder || []) {
|
||||
const declared = Array.isArray(entry?.nodeData?.scripts) ? entry.nodeData.scripts : [];
|
||||
entry.unsupportedScripts = [];
|
||||
for (const scriptCfg of declared) {
|
||||
if (getWebSupportedScriptDescriptor(scriptCfg?.name || scriptCfg?.key || "")) {
|
||||
continue;
|
||||
}
|
||||
unsupportedCount += 1;
|
||||
entry.unsupportedScripts.push(String(scriptCfg?.name || ""));
|
||||
}
|
||||
}
|
||||
runtime.unsupportedScriptCount = unsupportedCount;
|
||||
return unsupportedCount;
|
||||
}
|
||||
|
||||
function removeScriptRuntime(runtime, entry, scriptNameOrKey) {
|
||||
const wantedKey = normalizeScriptName(scriptNameOrKey);
|
||||
if (!wantedKey) return 0;
|
||||
let removed = 0;
|
||||
|
||||
if (Array.isArray(entry?.scriptStates)) {
|
||||
for (let i = entry.scriptStates.length - 1; i >= 0; i -= 1) {
|
||||
const state = entry.scriptStates[i];
|
||||
const stateKey = normalizeScriptName(state?.name || state?.key || "");
|
||||
if (stateKey === wantedKey) {
|
||||
entry.scriptStates.splice(i, 1);
|
||||
removed += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (Array.isArray(runtime?.scriptRuntimes)) {
|
||||
for (let i = runtime.scriptRuntimes.length - 1; i >= 0; i -= 1) {
|
||||
const state = runtime.scriptRuntimes[i];
|
||||
const stateKey = normalizeScriptName(state?.name || state?.key || "");
|
||||
const sameOwner = String(state?.ownerId || "") === String(entry?.id || "");
|
||||
if (sameOwner && stateKey === wantedKey) {
|
||||
runtime.scriptRuntimes.splice(i, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return removed;
|
||||
}
|
||||
|
||||
function instantiateScriptRuntime(THREE, runtime, entry, scriptCfg) {
|
||||
const state = createScriptState(
|
||||
THREE,
|
||||
scriptCfg,
|
||||
entry,
|
||||
runtime?.nodeMap || new Map(),
|
||||
runtime?.nodeNameLookup || buildNodeNameLookup(runtime?.nodeMap || new Map()),
|
||||
entry?.name || entry?.id || "",
|
||||
);
|
||||
if (!state) {
|
||||
throw createViewerError("script_not_supported", "Script is not supported in the Web viewer", {
|
||||
node: buildNodeSelectorPayload(entry),
|
||||
script: scriptCfg?.name || "",
|
||||
supportedScripts: listWebSupportedScripts(),
|
||||
});
|
||||
}
|
||||
entry.scriptStates.push(state);
|
||||
if (Array.isArray(runtime?.scriptRuntimes)) {
|
||||
runtime.scriptRuntimes.push(state);
|
||||
}
|
||||
refreshUnsupportedScriptStats(runtime);
|
||||
return state;
|
||||
}
|
||||
|
||||
function attachScriptToEntry(THREE, runtime, entry, scriptInput) {
|
||||
const nextConfig = normalizeScriptConfigInput(scriptInput);
|
||||
const descriptor = getWebSupportedScriptDescriptor(nextConfig.name);
|
||||
if (!descriptor) {
|
||||
throw createViewerError("script_not_supported", "Requested script cannot run in the Web viewer", {
|
||||
node: buildNodeSelectorPayload(entry),
|
||||
script: nextConfig.name,
|
||||
supportedScripts: listWebSupportedScripts(),
|
||||
});
|
||||
}
|
||||
|
||||
const declared = ensureDeclaredScripts(entry);
|
||||
const existingIndex = findDeclaredScriptIndex(entry, nextConfig.name);
|
||||
const mergedConfig = existingIndex >= 0
|
||||
? mergeScriptConfig(declared[existingIndex], { ...nextConfig, name: descriptor.name })
|
||||
: mergeScriptConfig({ name: descriptor.name, enabled: true, params: descriptor.defaultParams }, nextConfig);
|
||||
|
||||
if (existingIndex >= 0) {
|
||||
declared.splice(existingIndex, 1, mergedConfig);
|
||||
} else {
|
||||
declared.push(mergedConfig);
|
||||
}
|
||||
|
||||
removeScriptRuntime(runtime, entry, descriptor.key);
|
||||
instantiateScriptRuntime(THREE, runtime, entry, mergedConfig);
|
||||
return mergedConfig;
|
||||
}
|
||||
|
||||
function detachScriptFromEntry(runtime, entry, scriptNameOrKey) {
|
||||
const wantedKey = normalizeScriptName(scriptNameOrKey);
|
||||
if (!wantedKey) {
|
||||
throw createViewerError("invalid_script_config", "Script name must not be empty");
|
||||
}
|
||||
|
||||
const declared = ensureDeclaredScripts(entry);
|
||||
let removedDeclared = 0;
|
||||
for (let i = declared.length - 1; i >= 0; i -= 1) {
|
||||
const key = normalizeScriptName(declared[i]?.name || declared[i]?.key || "");
|
||||
if (key === wantedKey) {
|
||||
declared.splice(i, 1);
|
||||
removedDeclared += 1;
|
||||
}
|
||||
}
|
||||
|
||||
const removedRuntime = removeScriptRuntime(runtime, entry, wantedKey);
|
||||
refreshUnsupportedScriptStats(runtime);
|
||||
|
||||
if (removedDeclared === 0 && removedRuntime === 0) {
|
||||
throw createViewerError("script_not_found", "Requested script was not found on the node", {
|
||||
node: buildNodeSelectorPayload(entry),
|
||||
script: scriptNameOrKey,
|
||||
});
|
||||
}
|
||||
|
||||
return removedDeclared + removedRuntime;
|
||||
}
|
||||
|
||||
function makeRuntimeEntry(node, obj) {
|
||||
return {
|
||||
id: String(node?.id || ""),
|
||||
@ -2344,11 +2691,17 @@ function buildScriptStates(entry) {
|
||||
const name = String(scriptCfg?.name || "");
|
||||
const key = normalizeScriptName(name);
|
||||
const runtimeState = runtimeByKey.get(key);
|
||||
const descriptor = getWebSupportedScriptDescriptor(name || key);
|
||||
out.push({
|
||||
name,
|
||||
key,
|
||||
enabled: runtimeState ? !!runtimeState.enabled : toBoolean(scriptCfg?.enabled, true),
|
||||
supported: !!runtimeState,
|
||||
webSupported: !!descriptor,
|
||||
params: cloneJsonSafe(scriptCfg?.params ?? runtimeState?.params ?? {}, {}),
|
||||
file: String(scriptCfg?.file || ""),
|
||||
projectRelativePath: String(scriptCfg?.project_relative_path || ""),
|
||||
scriptGuid: String(scriptCfg?.script_guid || ""),
|
||||
});
|
||||
if (runtimeState) {
|
||||
runtimeByKey.delete(key);
|
||||
@ -2356,11 +2709,19 @@ function buildScriptStates(entry) {
|
||||
}
|
||||
|
||||
for (const runtimeState of runtimeByKey.values()) {
|
||||
const runtimeName = String(runtimeState?.name || "");
|
||||
const runtimeKey = normalizeScriptName(runtimeName || runtimeState?.key || "");
|
||||
const descriptor = getWebSupportedScriptDescriptor(runtimeName || runtimeKey);
|
||||
out.push({
|
||||
name: String(runtimeState?.name || ""),
|
||||
key: normalizeScriptName(runtimeState?.name || runtimeState?.key || ""),
|
||||
name: runtimeName,
|
||||
key: runtimeKey,
|
||||
enabled: !!runtimeState.enabled,
|
||||
supported: true,
|
||||
webSupported: !!descriptor,
|
||||
params: cloneJsonSafe(runtimeState?.params ?? {}, {}),
|
||||
file: "",
|
||||
projectRelativePath: "",
|
||||
scriptGuid: "",
|
||||
});
|
||||
}
|
||||
|
||||
@ -2410,6 +2771,7 @@ function buildSceneInfo(runtime) {
|
||||
environment: cloneJsonSafe(runtime?.manifest?.environment, {}),
|
||||
nodeCount: runtime?.nodeOrder?.length || 0,
|
||||
unsupportedScriptCount: runtime?.unsupportedScriptCount || 0,
|
||||
webSupportedScripts: listWebSupportedScripts(),
|
||||
nodes: (runtime?.nodeOrder || []).map((entry) => buildNodeSummary(entry)),
|
||||
};
|
||||
}
|
||||
@ -2692,6 +3054,39 @@ function createViewerApi(runtime, THREE) {
|
||||
});
|
||||
},
|
||||
},
|
||||
scripts: {
|
||||
listSupported() {
|
||||
return runApiAction(runtime, "scripts.listSupported", () => {
|
||||
return listWebSupportedScripts();
|
||||
});
|
||||
},
|
||||
attach(selector, script) {
|
||||
return runApiAction(runtime, "scripts.attach", () => {
|
||||
const entry = resolveRuntimeEntry(runtime, selector);
|
||||
attachScriptToEntry(THREE, runtime, entry, script);
|
||||
const result = buildNodeState(THREE, entry);
|
||||
emitRuntimeEvent(runtime, "stateChanged", {
|
||||
node: buildNodeSelectorPayload(entry),
|
||||
changeType: "script",
|
||||
state: result,
|
||||
});
|
||||
return result;
|
||||
});
|
||||
},
|
||||
detach(selector, scriptNameOrKey) {
|
||||
return runApiAction(runtime, "scripts.detach", () => {
|
||||
const entry = resolveRuntimeEntry(runtime, selector);
|
||||
detachScriptFromEntry(runtime, entry, scriptNameOrKey);
|
||||
const result = buildNodeState(THREE, entry);
|
||||
emitRuntimeEvent(runtime, "stateChanged", {
|
||||
node: buildNodeSelectorPayload(entry),
|
||||
changeType: "script",
|
||||
state: result,
|
||||
});
|
||||
return result;
|
||||
});
|
||||
},
|
||||
},
|
||||
events: {
|
||||
on(eventName, handler) {
|
||||
return runtime.eventHub.on(eventName, handler);
|
||||
@ -2720,6 +3115,9 @@ function createMessageBridge(runtime, api) {
|
||||
["animation.stop", (payload) => api.animation.stop(payload?.selector, payload?.options ?? {})],
|
||||
["animation.seek", (payload) => api.animation.seek(payload?.selector, payload?.time)],
|
||||
["animation.setSpeed", (payload) => api.animation.setSpeed(payload?.selector, payload?.speed)],
|
||||
["scripts.listSupported", () => api.scripts.listSupported()],
|
||||
["scripts.attach", (payload) => api.scripts.attach(payload?.selector, payload?.script ?? payload)],
|
||||
["scripts.detach", (payload) => api.scripts.detach(payload?.selector, payload?.name ?? payload?.key ?? payload?.script ?? "")],
|
||||
]);
|
||||
|
||||
let trustedOrigin = "";
|
||||
@ -2974,9 +3372,12 @@ async function bootstrap() {
|
||||
nodesById: new Map(),
|
||||
nodesByPublicId: new Map(),
|
||||
nodesByName: new Map(),
|
||||
nodeMap: new Map(),
|
||||
nodeNameLookup: new Map(),
|
||||
eventHub: createEventHub(),
|
||||
readyDeferred: makeDeferred(),
|
||||
animationControllers: [],
|
||||
scriptRuntimes: [],
|
||||
unsupportedScriptCount: 0,
|
||||
isReady: false,
|
||||
bridge: null,
|
||||
@ -3088,6 +3489,7 @@ async function bootstrap() {
|
||||
nodeMap.set(String(node.id || ""), obj);
|
||||
const entry = makeRuntimeEntry(node, obj);
|
||||
registerRuntimeEntry(runtime, entry);
|
||||
runtime.nodeMap.set(String(node.id || ""), obj);
|
||||
|
||||
if (node.kind === "model") {
|
||||
const modelUri = node.model?.uri;
|
||||
@ -3166,8 +3568,10 @@ async function bootstrap() {
|
||||
const composer = bloomSetup.composer;
|
||||
runtime.composer = composer;
|
||||
|
||||
const scriptRuntime = buildScriptRuntimeStates(THREE, runtime.nodeOrder, nodeMap);
|
||||
runtime.nodeNameLookup = buildNodeNameLookup(runtime.nodeMap);
|
||||
const scriptRuntime = buildScriptRuntimeStates(THREE, runtime.nodeOrder, runtime.nodeMap, runtime.nodeNameLookup);
|
||||
const scriptStates = scriptRuntime.runtimes;
|
||||
runtime.scriptRuntimes = scriptStates;
|
||||
runtime.unsupportedScriptCount = scriptRuntime.unsupportedCount;
|
||||
|
||||
const resize = () => {
|
||||
@ -3207,7 +3611,7 @@ async function bootstrap() {
|
||||
}
|
||||
}
|
||||
scene.updateMatrixWorld(true);
|
||||
for (const state of scriptStates) {
|
||||
for (const state of runtime.scriptRuntimes) {
|
||||
try {
|
||||
state.update(dt);
|
||||
} catch (err) {
|
||||
|
||||
@ -205,7 +205,7 @@ class AppActions:
|
||||
|
||||
def _mount_script_to_selected(self, script_name):
|
||||
"""挂载脚本到选中对象"""
|
||||
selected_node = self._get_selection_node()
|
||||
selected_node = self._get_script_target_node() if hasattr(self, "_get_script_target_node") else self._get_selection_node()
|
||||
|
||||
if not selected_node or selected_node.isEmpty():
|
||||
self.add_error_message("请先选择一个对象")
|
||||
@ -213,9 +213,22 @@ class AppActions:
|
||||
|
||||
try:
|
||||
if hasattr(self, 'script_manager') and self.script_manager:
|
||||
# 挂载前尝试重载该脚本,确保使用磁盘上的最新实现。
|
||||
try:
|
||||
self.script_manager.reload_script(script_name)
|
||||
except Exception:
|
||||
pass
|
||||
script_component = self.script_manager.add_script_to_object(selected_node, script_name)
|
||||
if script_component:
|
||||
script_info = {}
|
||||
try:
|
||||
script_info = self.script_manager.get_script_info(script_name) or {}
|
||||
except Exception:
|
||||
script_info = {}
|
||||
script_file = str(script_info.get("file", "") or "").strip()
|
||||
self.add_success_message(f"脚本 {script_name} 已挂载到 {selected_node.getName()}")
|
||||
if script_file:
|
||||
self.add_info_message(f"脚本文件: {script_file}")
|
||||
print(f"[脚本系统] 挂载脚本: {script_name} -> {selected_node.getName()}")
|
||||
else:
|
||||
self.add_error_message(f"挂载脚本 {script_name} 失败")
|
||||
@ -227,7 +240,7 @@ class AppActions:
|
||||
|
||||
def _unmount_script_from_selected(self, script_name):
|
||||
"""从选中对象卸载脚本"""
|
||||
selected_node = self._get_selection_node()
|
||||
selected_node = self._get_script_target_node() if hasattr(self, "_get_script_target_node") else self._get_selection_node()
|
||||
|
||||
if not selected_node or selected_node.isEmpty():
|
||||
self.add_error_message("请先选择一个对象")
|
||||
|
||||
@ -72,6 +72,49 @@ class PanelDelegates:
|
||||
return runtime_node
|
||||
return self._get_selection_source_node()
|
||||
|
||||
def _get_script_target_node(self):
|
||||
"""
|
||||
Return the runtime-visible node that scripts should mount to.
|
||||
|
||||
In SSBO mode, mounting to source tree nodes (e.g. ssbo_source_scene_root)
|
||||
won't affect the visible runtime scene. Prefer dynamic runtime object/proxy/model.
|
||||
"""
|
||||
ssbo_editor = getattr(self, "ssbo_editor", None)
|
||||
if ssbo_editor and hasattr(ssbo_editor, "has_active_selection") and ssbo_editor.has_active_selection():
|
||||
controller = getattr(ssbo_editor, "controller", None)
|
||||
selected_ids = list(getattr(ssbo_editor, "selected_ids", []) or [])
|
||||
|
||||
# Single selection: prefer the exact dynamic runtime object.
|
||||
if controller and len(selected_ids) == 1:
|
||||
try:
|
||||
obj_np = (getattr(controller, "id_to_object_np", {}) or {}).get(selected_ids[0])
|
||||
except Exception:
|
||||
obj_np = None
|
||||
if obj_np and self._node_is_valid(obj_np, require_attached=False):
|
||||
return obj_np
|
||||
|
||||
# Multi-selection proxy (if exists) should drive runtime edits.
|
||||
group_proxy = getattr(ssbo_editor, "_group_proxy", None)
|
||||
if group_proxy and self._node_is_valid(group_proxy, require_attached=False):
|
||||
return group_proxy
|
||||
|
||||
# Fallback scene node, but skip source-tree nodes.
|
||||
scene_node = getattr(ssbo_editor, "get_selection_scene_node", lambda: None)()
|
||||
if scene_node and self._node_is_valid(scene_node, require_attached=False):
|
||||
try:
|
||||
is_source = bool(getattr(ssbo_editor, "is_source_tree_node", lambda *_: False)(scene_node))
|
||||
except Exception:
|
||||
is_source = False
|
||||
if not is_source:
|
||||
return scene_node
|
||||
|
||||
# Last fallback: runtime root model (still visible), not source root.
|
||||
runtime_model = getattr(ssbo_editor, "model", None)
|
||||
if runtime_model and self._node_is_valid(runtime_model, require_attached=False):
|
||||
return runtime_model
|
||||
|
||||
return self._get_selection_node()
|
||||
|
||||
def _get_selection_key(self):
|
||||
ssbo_editor = getattr(self, "ssbo_editor", None)
|
||||
if ssbo_editor and hasattr(ssbo_editor, "has_active_selection") and ssbo_editor.has_active_selection():
|
||||
|
||||
@ -260,8 +260,10 @@ class ScriptPanels:
|
||||
def _draw_script_mounting_group(self):
|
||||
"""绘制脚本挂载组"""
|
||||
if imgui.collapsing_header("脚本挂载"):
|
||||
# 显示当前选中对象
|
||||
selected_node = self._get_selection_source_node()
|
||||
# 与挂载/卸载操作保持一致:统一使用脚本实际挂载目标节点。
|
||||
selected_node = self._get_script_target_node() if hasattr(self, "_get_script_target_node") else self._get_selection_node()
|
||||
if (not selected_node) or selected_node.isEmpty():
|
||||
selected_node = self._get_selection_source_node()
|
||||
|
||||
if selected_node and not selected_node.isEmpty():
|
||||
imgui.text("选中对象:")
|
||||
|
||||
Loading…
Reference in New Issue
Block a user