diff --git a/.idea/EG.iml b/.idea/EG.iml index bb7fb320..04720f01 100644 --- a/.idea/EG.iml +++ b/.idea/EG.iml @@ -2,9 +2,10 @@ + - + diff --git a/.idea/misc.xml b/.idea/misc.xml index c76b5032..51b9fc17 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -3,5 +3,5 @@ - + \ No newline at end of file diff --git a/QPanda3D/Helpers/Env_Grid_Maker.py b/QMeta3D/Helpers/Env_Grid_Maker.py similarity index 95% rename from QPanda3D/Helpers/Env_Grid_Maker.py rename to QMeta3D/Helpers/Env_Grid_Maker.py index 7f0fd0ec..a498611e 100644 --- a/QPanda3D/Helpers/Env_Grid_Maker.py +++ b/QMeta3D/Helpers/Env_Grid_Maker.py @@ -1,279 +1,271 @@ -# -*- coding: utf-8-*- -""" -Module : Env_Grid_Maker -Author : Saifeddine ALOUI + code from Forklift 's code snippet could be found on panda3D's website -Description : - Creates a 3D implementation of grid that shows axes in 3 possible planes - -""" -from panda3d.core import * -from direct.interval.IntervalGlobal import * - -class Env_Grid_Maker: - def __init__(self, XYPlaneShow = True, XZPlaneShow = False, YZPlaneShow = False, endCapLinesShow = True, XSize = 50, YSize = 50, ZSize = 50, gridStep = 10, subdiv = 10): - #Create line objects - self.axisLines = LineSegs() - self.gridLines = LineSegs() - self.subdivLines = LineSegs() - - #Init passed variables - self.XSize = XSize - self.YSize = YSize - self.ZSize = ZSize - self.gridStep = gridStep - self.subdiv = subdiv - - #Init default variables - - #Plane and end cap line visibility (1 is show, 0 is hide) - self.XYPlaneShow = XYPlaneShow - self.XZPlaneShow = XZPlaneShow - self.YZPlaneShow = YZPlaneShow - self.endCapLinesShow = endCapLinesShow - - #Alpha variables for each plane - #self.XYPlaneAlpha = 1 - #self.XZPlaneAlpha = 1 - #self.YZPlaneAlpha = 1 - - #Colors (RGBA passed as a VBase4 object) - self.XAxisColor = VBase4(1, 0, 0, 1) - self.YAxisColor = VBase4(0, 1, 0, 1) - self.ZAxisColor = VBase4(0, 0, 1, 1) - self.gridColor = VBase4(1, 1, 1, 1) - self.subdivColor = VBase4(.35, .35, .35, 1) - - #Line thicknesses (in pixels) - self.axisThickness = 3 - self.gridThickness = 1 - self.subdivThickness = 1 - - - def create(self): - #Set line thicknesses - self.axisLines.setThickness(self.axisThickness) - self.gridLines.setThickness(self.gridThickness) - self.subdivLines.setThickness(self.subdivThickness) - - if(self.XSize != 0): - #Draw X axis line - self.axisLines.setColor(self.XAxisColor) - self.axisLines.moveTo(-(self.XSize), 0, 0) - self.axisLines.drawTo(self.XSize, 0, 0) - - if(self.YSize != 0): - #Draw Y axis line - self.axisLines.setColor(self.YAxisColor) - self.axisLines.moveTo(0, -(self.YSize), 0) - self.axisLines.drawTo(0, self.YSize, 0) - - if(self.ZSize != 0): - #Draw Z axis line - self.axisLines.setColor(self.ZAxisColor) - self.axisLines.moveTo(0, 0, -(self.ZSize)) - self.axisLines.drawTo(0, 0, self.ZSize) - - #Check to see if primary grid lines should be drawn at all - if(self.gridStep != 0): - - #Draw primary grid lines - self.gridLines.setColor(self.gridColor) - - #Draw primary grid lines metering x axis if any x-length - if(self.XSize != 0): - if((self.YSize != 0) and (self.XYPlaneShow)): - #Draw y lines across x axis starting from center moving out - #XY Plane - for x in self.myfrange(0, self.XSize, self.gridStep): - self.gridLines.moveTo(x, -(self.YSize), 0) - self.gridLines.drawTo(x, self.YSize, 0) - self.gridLines.moveTo(-x, -(self.YSize), 0) - self.gridLines.drawTo(-x, self.YSize, 0) - - if(self.endCapLinesShow): - #Draw endcap lines - self.gridLines.moveTo(self.XSize, -(self.YSize), 0) - self.gridLines.drawTo(self.XSize, self.YSize, 0) - self.gridLines.moveTo(-(self.XSize), -(self.YSize), 0) - self.gridLines.drawTo(-(self.XSize), self.YSize, 0) - - if((self.ZSize != 0) and (self.XZPlaneShow)): - #Draw z lines across x axis starting from center moving out - #XZ Plane - for x in self.myfrange(0, self.XSize, self.gridStep): - self.gridLines.moveTo(x, 0, -(self.ZSize)) - self.gridLines.drawTo(x, 0, self.ZSize) - self.gridLines.moveTo(-x, 0, -(self.ZSize)) - self.gridLines.drawTo(-x, 0, self.ZSize) - - if(self.endCapLinesShow): - #Draw endcap lines - self.gridLines.moveTo(self.XSize, 0, -(self.ZSize)) - self.gridLines.drawTo(self.XSize, 0, self.ZSize) - self.gridLines.moveTo(-(self.XSize), 0, -(self.ZSize)) - self.gridLines.drawTo(-(self.XSize), 0, self.ZSize) - - #Draw primary grid lines metering y axis if any y-length - if(self.YSize != 0): - - if((self.YSize != 0) and (self.XYPlaneShow)): - #Draw x lines across y axis - #XY Plane - for y in self.myfrange(0, self.YSize, self.gridStep): - self.gridLines.moveTo(-(self.XSize), y, 0) - self.gridLines.drawTo(self.XSize, y, 0) - self.gridLines.moveTo(-(self.XSize), -y, 0) - self.gridLines.drawTo(self.XSize, -y, 0) - - if(self.endCapLinesShow): - #Draw endcap lines - self.gridLines.moveTo(-(self.XSize), self.YSize, 0) - self.gridLines.drawTo(self.XSize, self.YSize, 0) - self.gridLines.moveTo(-(self.XSize), -(self.YSize), 0) - self.gridLines.drawTo(self.XSize, -(self.YSize), 0) - - if((self.ZSize != 0) and (self.YZPlaneShow)): - #Draw z lines across y axis - #YZ Plane - for y in self.myfrange(0, self.YSize, self.gridStep): - self.gridLines.moveTo(0, y, -(self.ZSize)) - self.gridLines.drawTo(0, y, self.ZSize) - self.gridLines.moveTo(0, -y, -(self.ZSize)) - self.gridLines.drawTo(0, -y, self.ZSize) - - if(self.endCapLinesShow): - #Draw endcap lines - self.gridLines.moveTo(0, self.YSize, -(self.ZSize)) - self.gridLines.drawTo(0, self.YSize, self.ZSize) - self.gridLines.moveTo(0, -(self.YSize), -(self.ZSize)) - self.gridLines.drawTo(0, -(self.YSize), self.ZSize) - - #Draw primary grid lines metering z axis if any z-length - if(self.ZSize != 0): - if((self.XSize != 0) and (self.XZPlaneShow)): - #Draw x lines across z axis - #XZ Plane - for z in self.myfrange(0, self.ZSize, self.gridStep): - self.gridLines.moveTo(-(self.XSize), 0, z) - self.gridLines.drawTo(self.XSize, 0, z) - self.gridLines.moveTo(-(self.XSize), 0, -z) - self.gridLines.drawTo(self.XSize, 0, -z) - - if(self.endCapLinesShow): - #Draw endcap lines - self.gridLines.moveTo(-(self.XSize), 0, self.ZSize) - self.gridLines.drawTo(self.XSize, 0, self.ZSize) - self.gridLines.moveTo(-(self.XSize), 0, -(self.ZSize)) - self.gridLines.drawTo(self.XSize, 0, -(self.ZSize)) - - if((self.YSize != 0) and (self.YZPlaneShow)): - #Draw y lines across z axis - #YZ Plane - for z in self.myfrange(0, self.ZSize, self.gridStep): - self.gridLines.moveTo(0, -(self.YSize), z) - self.gridLines.drawTo(0, self.YSize, z) - self.gridLines.moveTo(0, -(self.YSize), -z) - self.gridLines.drawTo(0, self.YSize, -z) - - if(self.endCapLinesShow): - #Draw endcap lines - self.gridLines.moveTo(0, -(self.YSize), self.ZSize) - self.gridLines.drawTo(0, self.YSize, self.ZSize) - self.gridLines.moveTo(0, -(self.YSize), -(self.ZSize)) - self.gridLines.drawTo(0, self.YSize, -(self.ZSize)) - - #Check to see if secondary grid lines should be drawn - if(self.subdiv != 0): - - #Draw secondary grid lines - self.subdivLines.setColor(self.subdivColor) - - if(self.XSize != 0): - adjustedstep = self.gridStep / self.subdiv - if((self.YSize != 0) and (self.XYPlaneShow)): - #Draw y lines across x axis starting from center moving out - #XY - for x in self.myfrange(0, self.XSize, adjustedstep): - self.subdivLines.moveTo(x, -(self.YSize), 0) - self.subdivLines.drawTo(x, self.YSize, 0) - self.subdivLines.moveTo(-x, -(self.YSize), 0) - self.subdivLines.drawTo(-x, self.YSize, 0) - - if((self.ZSize != 0) and (self.XZPlaneShow)): - #Draw z lines across x axis starting from center moving out - #XZ - for x in self.myfrange(0, self.XSize, adjustedstep): - self.subdivLines.moveTo(x, 0, -(self.ZSize)) - self.subdivLines.drawTo(x, 0, self.ZSize) - self.subdivLines.moveTo(-x, 0, -(self.ZSize)) - self.subdivLines.drawTo(-x, 0, self.ZSize) - - if(self.YSize != 0): - if((self.YSize != 0) and (self.XYPlaneShow)): - #Draw x lines across y axis - #XY - for y in self.myfrange(0, self.YSize, adjustedstep): - self.subdivLines.moveTo(-(self.XSize), y, 0) - self.subdivLines.drawTo(self.XSize, y, 0) - self.subdivLines.moveTo(-(self.XSize), -y, 0) - self.subdivLines.drawTo(self.XSize, -y, 0) - if((self.ZSize != 0) and (self.YZPlaneShow)): - #Draw z lines across y axis - #YZ - for y in self.myfrange(0, self.YSize, adjustedstep): - self.subdivLines.moveTo(0, y, -(self.ZSize)) - self.subdivLines.drawTo(0, y, self.ZSize) - self.subdivLines.moveTo(0, -y, -(self.ZSize)) - self.subdivLines.drawTo(0, -y, self.ZSize) - - if(self.ZSize != 0): - - if((self.XSize != 0) and (self.XZPlaneShow)): - #Draw x lines across z axis - #XZ - for z in self.myfrange(0, self.ZSize, adjustedstep): - self.subdivLines.moveTo(-(self.XSize), 0, z) - self.subdivLines.drawTo(self.XSize, 0, z) - self.subdivLines.moveTo(-(self.XSize), 0, -z) - self.subdivLines.drawTo(self.XSize, 0, -z) - - if((self.YSize != 0) and (self.YZPlaneShow)): - #Draw y lines across z axis - #YZ - for z in self.myfrange(0, self.ZSize, adjustedstep): - self.subdivLines.moveTo(0, -(self.YSize), z) - self.subdivLines.drawTo(0, self.YSize, z) - self.subdivLines.moveTo(0, -(self.YSize), -z) - self.subdivLines.drawTo(0, self.YSize, -z) - - #Create ThreeAxisGrid nodes and nodepaths - #Create parent node and path - self.parentNode = PandaNode('threeaxisgrid-parentnode') - self.parentNodePath = NodePath(self.parentNode) - - #Create axis lines node and path, then reparent - self.axisLinesNode = self.axisLines.create() - self.axisLinesNodePath = NodePath(self.axisLinesNode) - self.axisLinesNodePath.reparentTo(self.parentNodePath) - - #Create grid lines node and path, then reparent - self.gridLinesNode = self.gridLines.create() - self.gridLinesNodePath = NodePath(self.gridLinesNode) - self.gridLinesNodePath.reparentTo(self.parentNodePath) - - #Create subdivision lines node and path then reparent - self.subdivLinesNode = self.subdivLines.create() - self.subdivLinesNodePath = NodePath(self.subdivLinesNode) - self.subdivLinesNodePath.reparentTo(self.parentNodePath) - - return self.parentNodePath - def myfrange(self, start, stop=None, step=None): - if stop is None: - stop = float(start) - start = 0.0 - if step is None: - step = 1.0 - cur = float(start) - while cur < stop: - yield cur +from panda3d.core import * +from direct.interval.IntervalGlobal import * + +class Env_Grid_Maker: + def __init__(self, XYPlaneShow = True, XZPlaneShow = False, YZPlaneShow = False, endCapLinesShow = True, XSize = 50, YSize = 50, ZSize = 50, gridStep = 10, subdiv = 10): + #Create line objects + self.axisLines = LineSegs() + self.gridLines = LineSegs() + self.subdivLines = LineSegs() + + #Init passed variables + self.XSize = XSize + self.YSize = YSize + self.ZSize = ZSize + self.gridStep = gridStep + self.subdiv = subdiv + + #Init default variables + + #Plane and end cap line visibility (1 is show, 0 is hide) + self.XYPlaneShow = XYPlaneShow + self.XZPlaneShow = XZPlaneShow + self.YZPlaneShow = YZPlaneShow + self.endCapLinesShow = endCapLinesShow + + #Alpha variables for each plane + #self.XYPlaneAlpha = 1 + #self.XZPlaneAlpha = 1 + #self.YZPlaneAlpha = 1 + + #Colors (RGBA passed as a VBase4 object) + self.XAxisColor = VBase4(1, 0, 0, 1) + self.YAxisColor = VBase4(0, 1, 0, 1) + self.ZAxisColor = VBase4(0, 0, 1, 1) + self.gridColor = VBase4(1, 1, 1, 1) + self.subdivColor = VBase4(.35, .35, .35, 1) + + #Line thicknesses (in pixels) + self.axisThickness = 3 + self.gridThickness = 1 + self.subdivThickness = 1 + + + def create(self): + #Set line thicknesses + self.axisLines.setThickness(self.axisThickness) + self.gridLines.setThickness(self.gridThickness) + self.subdivLines.setThickness(self.subdivThickness) + + if(self.XSize != 0): + #Draw X axis line + self.axisLines.setColor(self.XAxisColor) + self.axisLines.moveTo(-(self.XSize), 0, 0) + self.axisLines.drawTo(self.XSize, 0, 0) + + if(self.YSize != 0): + #Draw Y axis line + self.axisLines.setColor(self.YAxisColor) + self.axisLines.moveTo(0, -(self.YSize), 0) + self.axisLines.drawTo(0, self.YSize, 0) + + if(self.ZSize != 0): + #Draw Z axis line + self.axisLines.setColor(self.ZAxisColor) + self.axisLines.moveTo(0, 0, -(self.ZSize)) + self.axisLines.drawTo(0, 0, self.ZSize) + + #Check to see if primary grid lines should be drawn at all + if(self.gridStep != 0): + + #Draw primary grid lines + self.gridLines.setColor(self.gridColor) + + #Draw primary grid lines metering x axis if any x-length + if(self.XSize != 0): + if((self.YSize != 0) and (self.XYPlaneShow)): + #Draw y lines across x axis starting from center moving out + #XY Plane + for x in self.myfrange(0, self.XSize, self.gridStep): + self.gridLines.moveTo(x, -(self.YSize), 0) + self.gridLines.drawTo(x, self.YSize, 0) + self.gridLines.moveTo(-x, -(self.YSize), 0) + self.gridLines.drawTo(-x, self.YSize, 0) + + if(self.endCapLinesShow): + #Draw endcap lines + self.gridLines.moveTo(self.XSize, -(self.YSize), 0) + self.gridLines.drawTo(self.XSize, self.YSize, 0) + self.gridLines.moveTo(-(self.XSize), -(self.YSize), 0) + self.gridLines.drawTo(-(self.XSize), self.YSize, 0) + + if((self.ZSize != 0) and (self.XZPlaneShow)): + #Draw z lines across x axis starting from center moving out + #XZ Plane + for x in self.myfrange(0, self.XSize, self.gridStep): + self.gridLines.moveTo(x, 0, -(self.ZSize)) + self.gridLines.drawTo(x, 0, self.ZSize) + self.gridLines.moveTo(-x, 0, -(self.ZSize)) + self.gridLines.drawTo(-x, 0, self.ZSize) + + if(self.endCapLinesShow): + #Draw endcap lines + self.gridLines.moveTo(self.XSize, 0, -(self.ZSize)) + self.gridLines.drawTo(self.XSize, 0, self.ZSize) + self.gridLines.moveTo(-(self.XSize), 0, -(self.ZSize)) + self.gridLines.drawTo(-(self.XSize), 0, self.ZSize) + + #Draw primary grid lines metering y axis if any y-length + if(self.YSize != 0): + + if((self.YSize != 0) and (self.XYPlaneShow)): + #Draw x lines across y axis + #XY Plane + for y in self.myfrange(0, self.YSize, self.gridStep): + self.gridLines.moveTo(-(self.XSize), y, 0) + self.gridLines.drawTo(self.XSize, y, 0) + self.gridLines.moveTo(-(self.XSize), -y, 0) + self.gridLines.drawTo(self.XSize, -y, 0) + + if(self.endCapLinesShow): + #Draw endcap lines + self.gridLines.moveTo(-(self.XSize), self.YSize, 0) + self.gridLines.drawTo(self.XSize, self.YSize, 0) + self.gridLines.moveTo(-(self.XSize), -(self.YSize), 0) + self.gridLines.drawTo(self.XSize, -(self.YSize), 0) + + if((self.ZSize != 0) and (self.YZPlaneShow)): + #Draw z lines across y axis + #YZ Plane + for y in self.myfrange(0, self.YSize, self.gridStep): + self.gridLines.moveTo(0, y, -(self.ZSize)) + self.gridLines.drawTo(0, y, self.ZSize) + self.gridLines.moveTo(0, -y, -(self.ZSize)) + self.gridLines.drawTo(0, -y, self.ZSize) + + if(self.endCapLinesShow): + #Draw endcap lines + self.gridLines.moveTo(0, self.YSize, -(self.ZSize)) + self.gridLines.drawTo(0, self.YSize, self.ZSize) + self.gridLines.moveTo(0, -(self.YSize), -(self.ZSize)) + self.gridLines.drawTo(0, -(self.YSize), self.ZSize) + + #Draw primary grid lines metering z axis if any z-length + if(self.ZSize != 0): + if((self.XSize != 0) and (self.XZPlaneShow)): + #Draw x lines across z axis + #XZ Plane + for z in self.myfrange(0, self.ZSize, self.gridStep): + self.gridLines.moveTo(-(self.XSize), 0, z) + self.gridLines.drawTo(self.XSize, 0, z) + self.gridLines.moveTo(-(self.XSize), 0, -z) + self.gridLines.drawTo(self.XSize, 0, -z) + + if(self.endCapLinesShow): + #Draw endcap lines + self.gridLines.moveTo(-(self.XSize), 0, self.ZSize) + self.gridLines.drawTo(self.XSize, 0, self.ZSize) + self.gridLines.moveTo(-(self.XSize), 0, -(self.ZSize)) + self.gridLines.drawTo(self.XSize, 0, -(self.ZSize)) + + if((self.YSize != 0) and (self.YZPlaneShow)): + #Draw y lines across z axis + #YZ Plane + for z in self.myfrange(0, self.ZSize, self.gridStep): + self.gridLines.moveTo(0, -(self.YSize), z) + self.gridLines.drawTo(0, self.YSize, z) + self.gridLines.moveTo(0, -(self.YSize), -z) + self.gridLines.drawTo(0, self.YSize, -z) + + if(self.endCapLinesShow): + #Draw endcap lines + self.gridLines.moveTo(0, -(self.YSize), self.ZSize) + self.gridLines.drawTo(0, self.YSize, self.ZSize) + self.gridLines.moveTo(0, -(self.YSize), -(self.ZSize)) + self.gridLines.drawTo(0, self.YSize, -(self.ZSize)) + + #Check to see if secondary grid lines should be drawn + if(self.subdiv != 0): + + #Draw secondary grid lines + self.subdivLines.setColor(self.subdivColor) + + if(self.XSize != 0): + adjustedstep = self.gridStep / self.subdiv + if((self.YSize != 0) and (self.XYPlaneShow)): + #Draw y lines across x axis starting from center moving out + #XY + for x in self.myfrange(0, self.XSize, adjustedstep): + self.subdivLines.moveTo(x, -(self.YSize), 0) + self.subdivLines.drawTo(x, self.YSize, 0) + self.subdivLines.moveTo(-x, -(self.YSize), 0) + self.subdivLines.drawTo(-x, self.YSize, 0) + + if((self.ZSize != 0) and (self.XZPlaneShow)): + #Draw z lines across x axis starting from center moving out + #XZ + for x in self.myfrange(0, self.XSize, adjustedstep): + self.subdivLines.moveTo(x, 0, -(self.ZSize)) + self.subdivLines.drawTo(x, 0, self.ZSize) + self.subdivLines.moveTo(-x, 0, -(self.ZSize)) + self.subdivLines.drawTo(-x, 0, self.ZSize) + + if(self.YSize != 0): + if((self.YSize != 0) and (self.XYPlaneShow)): + #Draw x lines across y axis + #XY + for y in self.myfrange(0, self.YSize, adjustedstep): + self.subdivLines.moveTo(-(self.XSize), y, 0) + self.subdivLines.drawTo(self.XSize, y, 0) + self.subdivLines.moveTo(-(self.XSize), -y, 0) + self.subdivLines.drawTo(self.XSize, -y, 0) + if((self.ZSize != 0) and (self.YZPlaneShow)): + #Draw z lines across y axis + #YZ + for y in self.myfrange(0, self.YSize, adjustedstep): + self.subdivLines.moveTo(0, y, -(self.ZSize)) + self.subdivLines.drawTo(0, y, self.ZSize) + self.subdivLines.moveTo(0, -y, -(self.ZSize)) + self.subdivLines.drawTo(0, -y, self.ZSize) + + if(self.ZSize != 0): + + if((self.XSize != 0) and (self.XZPlaneShow)): + #Draw x lines across z axis + #XZ + for z in self.myfrange(0, self.ZSize, adjustedstep): + self.subdivLines.moveTo(-(self.XSize), 0, z) + self.subdivLines.drawTo(self.XSize, 0, z) + self.subdivLines.moveTo(-(self.XSize), 0, -z) + self.subdivLines.drawTo(self.XSize, 0, -z) + + if((self.YSize != 0) and (self.YZPlaneShow)): + #Draw y lines across z axis + #YZ + for z in self.myfrange(0, self.ZSize, adjustedstep): + self.subdivLines.moveTo(0, -(self.YSize), z) + self.subdivLines.drawTo(0, self.YSize, z) + self.subdivLines.moveTo(0, -(self.YSize), -z) + self.subdivLines.drawTo(0, self.YSize, -z) + + #Create ThreeAxisGrid nodes and nodepaths + #Create parent node and path + self.parentNode = PandaNode('threeaxisgrid-parentnode') + self.parentNodePath = NodePath(self.parentNode) + + #Create axis lines node and path, then reparent + self.axisLinesNode = self.axisLines.create() + self.axisLinesNodePath = NodePath(self.axisLinesNode) + self.axisLinesNodePath.reparentTo(self.parentNodePath) + + #Create grid lines node and path, then reparent + self.gridLinesNode = self.gridLines.create() + self.gridLinesNodePath = NodePath(self.gridLinesNode) + self.gridLinesNodePath.reparentTo(self.parentNodePath) + + #Create subdivision lines node and path then reparent + self.subdivLinesNode = self.subdivLines.create() + self.subdivLinesNodePath = NodePath(self.subdivLinesNode) + self.subdivLinesNodePath.reparentTo(self.parentNodePath) + + return self.parentNodePath + def myfrange(self, start, stop=None, step=None): + if stop is None: + stop = float(start) + start = 0.0 + if step is None: + step = 1.0 + cur = float(start) + while cur < stop: + yield cur cur += step \ No newline at end of file diff --git a/QPanda3D/Helpers/__init__.py b/QMeta3D/Helpers/__init__.py similarity index 62% rename from QPanda3D/Helpers/__init__.py rename to QMeta3D/Helpers/__init__.py index 376e08da..e58d61a7 100644 --- a/QPanda3D/Helpers/__init__.py +++ b/QMeta3D/Helpers/__init__.py @@ -1,2 +1,2 @@ -name="QPanda3D" +name="QMeta3D" __all__ = ["Env_Grid_Maker"] \ No newline at end of file diff --git a/QPanda3D/Panda3DWorld.py b/QMeta3D/Meta3DWorld.py similarity index 82% rename from QPanda3D/Panda3DWorld.py rename to QMeta3D/Meta3DWorld.py index e717b68c..c325da5c 100644 --- a/QPanda3D/Panda3DWorld.py +++ b/QMeta3D/Meta3DWorld.py @@ -1,214 +1,192 @@ -# -*- coding: utf-8-*- -""" -Module : Panda3DWorld -Author : Saifeddine ALOUI -Description : - Inherit this object to create your custom world -""" -import sys -import os - -from core.CustomMouseController import CustomMouseController - -# 获取 RenderPipelineFile 的路径 -render_pipeline_path = './RenderPipelineFile' -# 将该路径添加到 sys.path 中,确保 Python 能够找到它 -project_root = os.path.dirname(os.path.abspath(__file__)) -sys.path.insert(0, project_root) -sys.path.insert(0, render_pipeline_path) - -# 现在你可以导入 RenderPipelineFile 中的模块了 -# 例如,如果你想导入 RenderPipelineFile 中的 RenderPipeline 模块 -from RenderPipelineFile.rpcore import RenderPipeline -_global_render_pipeline = None - -# PyQt imports -from PyQt5.QtCore import * -from PyQt5.QtGui import * -from PyQt5.QtWidgets import * - -# Panda imports -from panda3d.core import * -# from panda3d.core import loadPrcFileData -# loadPrcFileData("", "window-type none") # Set Panda to draw its main window in an offscreen buffer -from direct.showbase.DirectObject import DirectObject -from panda3d.core import GraphicsOutput, Texture, ConfigVariableManager, WindowProperties - -# Set up Panda environment -from direct.showbase.ShowBase import ShowBase -import platform - -# Local imports -from QPanda3D.QMouseWatcherNode import QMouseWatcherNode - -from RenderPipelineFile.rpcore.render_target import RenderTarget -__all__ = ["Panda3DWorld"] - -_global_world_instance=None -import builtins - - -class Panda3DWorld(ShowBase): - """ - Panda3DWorld : A class to handle all panda3D world manipulation - """ - - def __init__(self, width=1380, height=750, is_fullscreen=False, size=1.0, clear_color=LVecBase4f(0, 0.5, 0, 1), - name="qpanda3D"): - - global _global_world_instance - global _global_render_pipeline - - _global_world_instance = self - - sort = -100 - self.parent = None - - # 设置基本配置 - loadPrcFileData("", "show-frame-rate-meter 0") - loadPrcFileData("", "window-type offscreen") # 设置为离屏渲染 - loadPrcFileData("", f"win-size {width} {height}") - loadPrcFileData("", "win-fixed-size #f") # 允许窗口调整大小 - - # 🚀 VR性能优化配置 - loadPrcFileData("", "prefer-single-buffer true") # 减少缓冲区交换开销 - loadPrcFileData("", "gl-force-flush false") # 避免强制glFlush导致的性能损失 - loadPrcFileData("", "sync-video false") # 禁用默认VSync,让OpenVR控制 - loadPrcFileData("", "support-stencil false") # 禁用不必要的模板缓冲区 - loadPrcFileData("", "clock-mode non-real-time") # 禁用Panda3D帧率控制,让OpenVR控制 - # loadPrcFileData("", "gl-debug true") # 调试时可启用OpenGL调试 - - if (is_fullscreen): - loadPrcFileData("", "fullscreen #t") - - self.render_pipeline = RenderPipeline() - self.render_pipeline.pre_showbase_init() - - ShowBase.__init__(self) - - # 初始化渲染管线并设置可调整大小的标志 - self.render_pipeline.create(self) - _global_render_pipeline = self.render_pipeline - - # 创建 Qt 能读的 RGBA8 贴图 - self.qt_output_tex = Texture("qt_output_tex") - self.qt_output_tex.set_format(Texture.F_rgba8) - self.qt_output_tex.set_component_type(Texture.T_unsigned_byte) - - # # 获取图形管线对象 - # gsg = self.win.getGsg() - # host = gsg.getEngine().getHost() - - self.win.add_render_texture(self.qt_output_tex, GraphicsOutput.RTM_copy_ram) - - #self.screenTexture = Texture() - #self.screenTexture.setMinfilter(Texture.FTLinear) - #self.screenTexture.setFormat(Texture.FRgba32) - #self.screenTexture.set_wrap_u(Texture.WM_clamp) - #self.screenTexture.set_wrap_v(Texture.WM_clamp) - - # buff_size_x = int(self.win.get_x_size() * size) - # buff_size_y = int(self.win.get_y_size() * size) - # winprops = WindowProperties() - # winprops.set_size(buff_size_x, buff_size_y) - # - # - # props = FrameBufferProperties() - # props.set_rgb_color(True) - # props.set_rgba_bits(8, 8, 8, 8) - # props.set_depth_bits(8) - - # self.buff = self.graphicsEngine.make_output( - # self.pipe, name, sort, - # props, winprops, - # GraphicsPipe.BF_resizeable, - # self.win.get_gsg(), self.win) - - #self.screenTexture = render_pipeline._final_stage.target.color_tex - - #self.buff.addRenderTexture(self.screenTexture, GraphicsOutput.RTMCopyRam) - # self.buff.set_sort(sort) - - - - #self.cam = self.makeCamera(self.buff) - #self.render_pipeline._showbase.cam=self.makeCamera(self.buff) - - #self.render_pipeline._showbase.cam=self.makeCamera(self.buff) - - #self.render_pipeline._showbase.camera.reparentTo(self.render) - #base.camera.reparentTo(self.render) - #self.cam.reparentTo(self.render) # 可选同步 - - #render_pipeline.set_camera(self.cam) - - #添加渲染效果 - #self.cam = self.render_pipeline._showbase.cam - #self.camNode = self.cam.node() - #self.camLens = self.camNode.get_lens() - self.render_pipeline._showbase.camera = self.render_pipeline._showbase.cam - #self.render_pipeline.daytime_mgr.update() - - - # if clear_color is None: - # self.buff.set_clear_active(GraphicsOutput.RTPColor, False) - # else: - # self.buff.set_clear_color(clear_color) - # self.buff.set_clear_active(GraphicsOutput.RTPColor, True) - - # self.disableMouse() - self.mouse_controller = CustomMouseController(self) - self.mouse_controller.setUp() - - # 添加错误处理钩子 - self.accept("transform_state_error", self._handle_transform_error) - - def _handle_transform_error(self): - """处理TransformState相关的错误""" - try: - from panda3d.core import TransformState, RenderState - TransformState.clear_cache() - RenderState.clear_cache() - print("已清理TransformState和RenderState缓存") - except Exception as e: - print(f"清理缓存时出错: {e}") - - def render_pipeline(self): - """获取 RenderPipeline 实例""" - return self._render_pipeline - - - def set_parent(self, parent: QWidget): - self.parent = parent - self.mouseWatcherNode = QMouseWatcherNode(parent) - - def getAspectRatio(self, win = None): - if win is None and self.parent is not None: - return float(self.parent.width()) / float(self.parent.height()) - else: - return super().getAspectRatio(win) - -def get_render_pipeline(): - """获取全局 RenderPipeline 单例""" - if _global_render_pipeline is None: - raise RuntimeError( - "RenderPipeline has not been initialized yet. Please create a Panda3DWorld instance first.") - return _global_render_pipeline - -def resize_buffer(self, width: int, height: int): - """根据新窗口尺寸调整 Panda3D 渲染输出尺寸""" - # 设置 Panda3D 的窗口尺寸(offscreen 模式下对应输出区域) - props = WindowProperties() - props.set_size(width, height) - self.win.request_properties(props) - - # 重新分配输出贴图的大小 - self.qt_output_tex.set_x_size(width) - self.qt_output_tex.set_y_size(height) - - # 更新 lens 的 aspect ratio - if self.camLens: - self.camLens.set_film_size(width, height) # 或 set_aspect_ratio(width / height) - - # 强制更新窗口(有时在 Qt 内嵌时需要) +import sys +import os + +from core.CustomMouseController import CustomMouseController + +# 获取 RenderPipelineFile 的路径 +render_pipeline_path = './RenderPipelineFile' +# 将该路径添加到 sys.path 中,确保 Python 能够找到它 +project_root = os.path.dirname(os.path.abspath(__file__)) +sys.path.insert(0, project_root) +sys.path.insert(0, render_pipeline_path) + +from RenderPipelineFile.rpcore import RenderPipeline +_global_render_pipeline = None + +from PyQt5.QtCore import * +from PyQt5.QtGui import * +from PyQt5.QtWidgets import * + +from panda3d.core import * +from panda3d.core import GraphicsOutput, Texture, ConfigVariableManager, WindowProperties + +from direct.showbase.ShowBase import ShowBase +import platform + +from QMeta3D.QMouseWatcherNode import QMouseWatcherNode + +from RenderPipelineFile.rpcore.render_target import RenderTarget +__all__ = ["Meta3DWorld"] + +_global_world_instance=None +import builtins + + +class Meta3DWorld(ShowBase): + def __init__(self, width=1380, height=750, is_fullscreen=False, size=1.0, clear_color=LVecBase4f(0, 0.5, 0, 1), + name="qMeta3D"): + + global _global_world_instance + global _global_render_pipeline + + _global_world_instance = self + + sort = -100 + self.parent = None + + # 设置基本配置 + loadPrcFileData("", "show-frame-rate-meter 0") + loadPrcFileData("", "window-type offscreen") # 设置为离屏渲染 + loadPrcFileData("", f"win-size {width} {height}") + loadPrcFileData("", "win-fixed-size #f") # 允许窗口调整大小 + + # 🚀 VR性能优化配置 + loadPrcFileData("", "prefer-single-buffer true") # 减少缓冲区交换开销 + loadPrcFileData("", "gl-force-flush false") # 避免强制glFlush导致的性能损失 + loadPrcFileData("", "sync-video false") # 禁用默认VSync,让OpenVR控制 + loadPrcFileData("", "support-stencil false") # 禁用不必要的模板缓冲区 + loadPrcFileData("", "clock-mode non-real-time") + # loadPrcFileData("", "gl-debug true") + + if (is_fullscreen): + loadPrcFileData("", "fullscreen #t") + + self.render_pipeline = RenderPipeline() + self.render_pipeline.pre_showbase_init() + + ShowBase.__init__(self) + + # 初始化渲染管线并设置可调整大小的标志 + self.render_pipeline.create(self) + _global_render_pipeline = self.render_pipeline + + # 创建 Qt 能读的 RGBA8 贴图 + self.qt_output_tex = Texture("qt_output_tex") + self.qt_output_tex.set_format(Texture.F_rgba8) + self.qt_output_tex.set_component_type(Texture.T_unsigned_byte) + + # # 获取图形管线对象 + # gsg = self.win.getGsg() + # host = gsg.getEngine().getHost() + + self.win.add_render_texture(self.qt_output_tex, GraphicsOutput.RTM_copy_ram) + + #self.screenTexture = Texture() + #self.screenTexture.setMinfilter(Texture.FTLinear) + #self.screenTexture.setFormat(Texture.FRgba32) + #self.screenTexture.set_wrap_u(Texture.WM_clamp) + #self.screenTexture.set_wrap_v(Texture.WM_clamp) + + # buff_size_x = int(self.win.get_x_size() * size) + # buff_size_y = int(self.win.get_y_size() * size) + # winprops = WindowProperties() + # winprops.set_size(buff_size_x, buff_size_y) + # + # + # props = FrameBufferProperties() + # props.set_rgb_color(True) + # props.set_rgba_bits(8, 8, 8, 8) + # props.set_depth_bits(8) + + # self.buff = self.graphicsEngine.make_output( + # self.pipe, name, sort, + # props, winprops, + # GraphicsPipe.BF_resizeable, + # self.win.get_gsg(), self.win) + + #self.screenTexture = render_pipeline._final_stage.target.color_tex + + #self.buff.addRenderTexture(self.screenTexture, GraphicsOutput.RTMCopyRam) + # self.buff.set_sort(sort) + + + + #self.cam = self.makeCamera(self.buff) + #self.render_pipeline._showbase.cam=self.makeCamera(self.buff) + + #self.render_pipeline._showbase.cam=self.makeCamera(self.buff) + + #self.render_pipeline._showbase.camera.reparentTo(self.render) + #base.camera.reparentTo(self.render) + #self.cam.reparentTo(self.render) # 可选同步 + + #render_pipeline.set_camera(self.cam) + + #添加渲染效果 + #self.cam = self.render_pipeline._showbase.cam + #self.camNode = self.cam.node() + #self.camLens = self.camNode.get_lens() + self.render_pipeline._showbase.camera = self.render_pipeline._showbase.cam + #self.render_pipeline.daytime_mgr.update() + + + # if clear_color is None: + # self.buff.set_clear_active(GraphicsOutput.RTPColor, False) + # else: + # self.buff.set_clear_color(clear_color) + # self.buff.set_clear_active(GraphicsOutput.RTPColor, True) + + # self.disableMouse() + self.mouse_controller = CustomMouseController(self) + self.mouse_controller.setUp() + + # 添加错误处理钩子 + self.accept("transform_state_error", self._handle_transform_error) + + def _handle_transform_error(self): + """处理TransformState相关的错误""" + try: + from panda3d.core import TransformState, RenderState + TransformState.clear_cache() + RenderState.clear_cache() + print("已清理TransformState和RenderState缓存") + except Exception as e: + print(f"清理缓存时出错: {e}") + + def render_pipeline(self): + """获取 RenderPipeline 实例""" + return self._render_pipeline + + + def set_parent(self, parent: QWidget): + self.parent = parent + self.mouseWatcherNode = QMouseWatcherNode(parent) + + def getAspectRatio(self, win = None): + if win is None and self.parent is not None: + return float(self.parent.width()) / float(self.parent.height()) + else: + return super().getAspectRatio(win) + +def get_render_pipeline(): + """获取全局 RenderPipeline 单例""" + if _global_render_pipeline is None: + raise RuntimeError( + "RenderPipeline has not been initialized yet. Please create a 3DWorld instance first.") + return _global_render_pipeline + +def resize_buffer(self, width: int, height: int): + props = WindowProperties() + props.set_size(width, height) + self.win.request_properties(props) + + # 重新分配输出贴图的大小 + self.qt_output_tex.set_x_size(width) + self.qt_output_tex.set_y_size(height) + + # 更新 lens 的 aspect ratio + if self.camLens: + self.camLens.set_film_size(width, height) # 或 set_aspect_ratio(width / height) + + # 强制更新窗口(有时在 Qt 内嵌时需要) self.graphicsEngine.open_windows() \ No newline at end of file diff --git a/QPanda3D/QPanda3DWidget.py b/QMeta3D/QMeta3DWidget.py similarity index 53% rename from QPanda3D/QPanda3DWidget.py rename to QMeta3D/QMeta3DWidget.py index 672ad897..a39e1869 100644 --- a/QPanda3D/QPanda3DWidget.py +++ b/QMeta3D/QMeta3DWidget.py @@ -1,355 +1,252 @@ -# -*- coding: utf-8-*- -""" -Module : QPanda3DWidget -Author : Saifeddine ALOUI -Description : - This is the QWidget to be inserted in your standard PyQt5 application. - It takes a Panda3DWorld object at init time. - You should first create the Panda3DWorkd object before creating this widget. -""" -# PyQt imports -from PyQt5 import QtWidgets, QtGui -from PyQt5.QtCore import * -from PyQt5.QtGui import * -from PyQt5.QtWidgets import * -from direct.task.TaskManagerGlobal import taskMgr - -# Panda imports -from panda3d.core import Texture, WindowProperties, CallbackGraphicsWindow -from panda3d.core import loadPrcFileData - -from QPanda3D.QPanda3D_Buttons_Translation import QPanda3D_Button_translation -from QPanda3D.QPanda3D_Keys_Translation import QPanda3D_Key_translation -from QPanda3D.QPanda3D_Modifiers_Translation import QPanda3D_Modifier_translation - -__all__ = ["QPanda3DWidget"] - - -class QPanda3DSynchronizer(QTimer): - def __init__(self, qPanda3DWidget, FPS=60): - QTimer.__init__(self) - self.qPanda3DWidget = qPanda3DWidget - dt = 1000 / FPS - self.setInterval(int(dt)) - self.timeout.connect(self.tick) - - def tick(self): - try: - # 在渲染前清理可能损坏的TransformState对象 - from panda3d.core import TransformState - TransformState.clear_cache() - - taskMgr.step() - self.qPanda3DWidget.update() - except AssertionError as e: - # 专门处理 TransformState has_mat() 断言错误 - if "has_mat" in str(e): - print(f"警告: 检测到TransformState断言错误,已静默处理: {e}") - # 尝试恢复渲染状态 - try: - # 强制清理缓存并重试 - from panda3d.core import TransformState, RenderState - TransformState.clear_cache() - RenderState.clear_cache() - taskMgr.step() - self.qPanda3DWidget.update() - except: - pass - else: - # 重新抛出其他断言错误 - raise - except Exception as e: - # 静默处理其他所有异常 - print(f"警告: 检测到异常,已静默处理: {e}") - - -def get_panda_key_modifiers(evt): - panda_mods = [] - qt_mods = evt.modifiers() - for qt_mod, panda_mod in QPanda3D_Modifier_translation.items(): - if (qt_mods & qt_mod) == qt_mod: - panda_mods.append(panda_mod) - return panda_mods - - -def get_panda_key_modifiers_prefix(evt): - # join all modifiers (except NoModifier, which is None) with '-' - prefix = "-".join([mod for mod in get_panda_key_modifiers(evt) if mod is not None]) - - # if the prefix is not empty, append a '-' - if prefix: - prefix += '-' - - return prefix - - -class QPanda3DWidget(QWidget): - """ - An interactive panda3D QWidget - Parent : Parent QT Widget - FPS : Number of frames per socond to refresh the screen - debug: Switch printing key events to console on/off - """ - - def __init__(self, panda3DWorld, parent=None, FPS=60, debug=False): - QWidget.__init__(self, parent) - self.rp_sync_requested = False - # set fixed geometry - self.panda3DWorld = panda3DWorld - self.panda3DWorld.set_parent(self) - # Setup a timer in Qt that runs taskMgr.step() to simulate Panda's own main loop - # pandaTimer = QTimer(self) - # pandaTimer.timeout.connect() - # pandaTimer.start(0) - - self.setFocusPolicy(Qt.StrongFocus) - - # Setup another timer that redraws this widget in a specific FPS - # redrawTimer = QTimer(self) - # redrawTimer.timeout.connect(self.update) - # redrawTimer.start(1000/FPS) - - self.paintSurface = QPainter() - self.rotate = QTransform() - self.rotate.rotate(180) - self.out_image = QImage() - - size = self.panda3DWorld.cam.node().get_lens().get_film_size() - self.initial_film_size = QSizeF(size.x, size.y) - self.initial_size = self.size() - - self.synchronizer = QPanda3DSynchronizer(self, FPS) - self.synchronizer.start() - - self.debug = debug - - def mousePressEvent(self, evt): - button = evt.button() - try: - b = "{}{}".format(get_panda_key_modifiers_prefix(evt), QPanda3D_Button_translation[button]) - if self.debug: - print(b) - messenger.send(b,[{"x":evt.x(),"y":evt.y()}]) - except: - print("Unimplemented button. Please send an issue on github to fix this problem") - - def mouseMoveEvent(self, evt:QtGui.QMouseEvent): - button = evt.button() - try: - b = "mouse-move" - if self.debug: - print(b) - messenger.send(b,[{"x":evt.x(),"y":evt.y()}]) - except: - print("Unimplemented button. Please send an issue on github to fix this problem") - - - def mouseReleaseEvent(self, evt): - button = evt.button() - try: - b = "{}{}-up".format(get_panda_key_modifiers_prefix(evt), QPanda3D_Button_translation[button]) - if self.debug: - print(b) - messenger.send(b,[{"x":evt.x(),"y":evt.y()}]) - except: - print("Unimplemented button. Please send an issue on github to fix this problem") - - def wheelEvent(self, evt): - delta = evt.angleDelta().y() - try: - if self.debug: - print(f"wheel {delta}") - messenger.send('wheel',[{"delta":delta}]) - except: - print("Unimplemented button. Please send an issue on github to fix this problem") - - def keyPressEvent(self, evt): - key = evt.key() - try: - k = "{}{}".format(get_panda_key_modifiers_prefix(evt), QPanda3D_Key_translation[key]) - if self.debug: - print(k) - messenger.send(k) - except: - print("Unimplemented key. Please send an issue on github to fix this problem") - - def keyReleaseEvent(self, evt): - key = evt.key() - try: - k = "{}{}-up".format(get_panda_key_modifiers_prefix(evt), QPanda3D_Key_translation[key]) - if self.debug: - print(k) - messenger.send(k) - except: - print("Unimplemented key. Please send an issue on github to fix this problem") - - def resizeEvent(self, evt): - width = evt.size().width() - height = evt.size().height() - #print(f"width:{width}") - #print(f"height:{height}") - - from Panda3DWorld import resize_buffer - #resize_buffer(width, height) - - lens = self.panda3DWorld.cam.node().get_lens() - # lens.set_film_size(self.initial_film_size.width() * evt.size().width() / self.initial_size.width(), - # self.initial_film_size.height() * evt.size().height() / self.initial_size.height()) - #self.sync_panda3d_window_size(width, height) - - #self.panda3DWorld.buff.setSize(evt.size().width(), evt.size().height()) - - def minimumSizeHint(self): - return QSize(400, 300) - - # Use the paint event to pull the contents of the panda texture to the widget - # def paintEvent(self, event): - # if self.panda3DWorld.screenTexture.mightHaveRamImage(): - # tex = self.panda3DWorld.screenTexture - # gsg = base.win.getGsg() - # #self.panda3DWorld.screenTexture.store(gsg) - # base.graphicsEngine.extractTextureData(tex, gsg) - # self.panda3DWorld.screenTexture.setFormat(Texture.FRgba32) - # data = self.panda3DWorld.screenTexture.getRamImage().getData() - # img = QImage(data, self.panda3DWorld.screenTexture.getXSize(), self.panda3DWorld.screenTexture.getYSize(), - # QImage.Format_ARGB32).mirrored() - # self.paintSurface.begin(self) - # self.paintSurface.drawImage(0, 0, img) - # self.paintSurface.end() - - # def paintEvent(self, event): - # tex = self.panda3DWorld.qt_output_tex - # - # gsg = base.win.getGsg() - # - # - # if not self.rp_sync_requested: - # base.graphicsEngine.extractTextureData(tex, gsg) - # self.rp_sync_requested = True - # self.update() - # return - # - # if tex.hasRamImage(): - # data = tex.getRamImage().getData() - # width = tex.getXSize() - # height = tex.getYSize() - # - # # ✅ 应该 data 长度 = width * height * 4 - # img = QImage(data, width, height, QImage.Format_ARGB32).mirrored() - # - # painter = QPainter(self) - # painter.drawImage(0, 0, img) - # painter.end() - # - # self.rp_sync_requested = False - # else: - # print("⚠️ Texture has no RAM image yet, retrying next frame") - # self.rp_sync_requested = False - # self.update() - - # def paintEvent(self, event): - # tex = self.panda3DWorld.qt_output_tex - # - # gsg = base.win.getGsg() - # - # if not tex.hasRamImage(): - # base.graphicsEngine.extractTextureData(tex, gsg) - # self.update() # 请求下一帧更新 - # return - # - # data = tex.getRamImage().getData() - # width = tex.getXSize() - # height = tex.getYSize() - # expected_len = width * height * 4 - # - # if len(data) != expected_len: - # print(f"⚠️ 像素数据长度异常({len(data)} != {expected_len}),跳过绘制") - # self.update() - # return - # - # # 一切正常才绘制 - # img = QImage(data, width, height, QImage.Format_ARGB32).mirrored() - # - # painter = QPainter(self) - # painter.drawImage(0, 0, img) - # painter.end() - - def paintEvent(self, event): - tex = self.panda3DWorld.qt_output_tex - gsg = base.win.getGsg() - - if not gsg: - self.update() - return - - if not tex.hasRamImage(): - base.graphicsEngine.extractTextureData(tex, gsg) - self.update() - return - - data = tex.getRamImage().getData() - tex_width = tex.getXSize() - tex_height = tex.getYSize() - expected_len = tex_width * tex_height * 4 - - if len(data) != expected_len: - print(f"⚠️ 像素数据长度异常({len(data)} != {expected_len}),跳过绘制") - self.update() - return - - img = QImage(data, tex_width, tex_height, QImage.Format_ARGB32).mirrored() - - widget_width = self.width() - widget_height = self.height() - - painter = QPainter(self) - - # 【保持宽高比的缩放】 - scaled_img = img.scaled(widget_width, widget_height, Qt.KeepAspectRatio, Qt.SmoothTransformation) - - # 居中绘制 - x_offset = (widget_width - scaled_img.width()) // 2 - y_offset = (widget_height - scaled_img.height()) // 2 - - painter.drawImage(x_offset, y_offset, scaled_img) - painter.end() - - def sync_panda3d_window_size(self, width, height): - """同步 Panda3D 窗口尺寸到 Qt 窗口尺寸""" - try: - from panda3d.core import WindowProperties, LVecBase2i - - # 确保尺寸是4的倍数(RenderPipeline 要求) - adjusted_width = width - width % 4 - adjusted_height = height - height % 4 - - # 更新窗口属性 - props = WindowProperties() - props.setSize(adjusted_width, adjusted_height) - - # 对于 offscreen 渲染,直接更新窗口属性 - if self.panda3DWorld.win: - self.panda3DWorld.win.request_properties(props) - - # 手动触发 RenderPipeline 的尺寸更新逻辑 - if hasattr(self.panda3DWorld, 'render_pipeline'): - # 更新全局分辨率 - from RenderPipelineFile.rpcore.globals import Globals - Globals.native_resolution = LVecBase2i(adjusted_width, adjusted_height) - - # 重新计算渲染分辨率 - self.panda3DWorld.render_pipeline._compute_render_resolution() - - # 通知各个管理器处理尺寸变化 - self.panda3DWorld.render_pipeline.light_mgr.compute_tile_size() - self.panda3DWorld.render_pipeline.stage_mgr.handle_window_resize() - if hasattr(self.panda3DWorld.render_pipeline, 'debugger'): - self.panda3DWorld.render_pipeline.debugger.handle_window_resize() - - # 触发插件的窗口尺寸变化钩子 - self.panda3DWorld.render_pipeline.plugin_mgr.trigger_hook("window_resized") - - print(f"Panda3D 窗口尺寸已同步为: {adjusted_width} x {adjusted_height}") - - except Exception as e: - print(f"同步 Panda3D 窗口尺寸失败: {str(e)}") \ No newline at end of file +from PyQt5 import QtWidgets, QtGui +from PyQt5.QtCore import * +from PyQt5.QtGui import * +from PyQt5.QtWidgets import * +from direct.task.TaskManagerGlobal import taskMgr + +from QMeta3D.QMeta3D_Buttons_Translation import QMeta3D_Button_translation +from QMeta3D.QMeta3D_Keys_Translation import QMeta3D_Key_translation +from QMeta3D.QMeta3D_Modifiers_Translation import QMeta3D_Modifier_translation + +__all__ = ["QMeta3DWidget"] + + +class QMeta3DSynchronizer(QTimer): + def __init__(self, qMeta3DWidget, FPS=60): + QTimer.__init__(self) + self.qMeta3DWidget = qMeta3DWidget + dt = 1000 / FPS + self.setInterval(int(dt)) + self.timeout.connect(self.tick) + + def tick(self): + try: + # 在渲染前清理可能损坏的TransformState对象 + from panda3d.core import TransformState + TransformState.clear_cache() + + taskMgr.step() + self.qMeta3DWidget.update() + except AssertionError as e: + # 专门处理 TransformState has_mat() 断言错误 + if "has_mat" in str(e): + print(f"警告: 检测到TransformState断言错误,已静默处理: {e}") + # 尝试恢复渲染状态 + try: + # 强制清理缓存并重试 + from panda3d.core import TransformState, RenderState + TransformState.clear_cache() + RenderState.clear_cache() + taskMgr.step() + self.qMeta3DWidget.update() + except: + pass + else: + # 重新抛出其他断言错误 + raise + except Exception as e: + # 静默处理其他所有异常 + print(f"警告: 检测到异常,已静默处理: {e}") + + +def get_panda_key_modifiers(evt): + panda_mods = [] + qt_mods = evt.modifiers() + for qt_mod, panda_mod in QMeta3D_Modifier_translation.items(): + if (qt_mods & qt_mod) == qt_mod: + panda_mods.append(panda_mod) + return panda_mods + + +def get_panda_key_modifiers_prefix(evt): + # join all modifiers (except NoModifier, which is None) with '-' + prefix = "-".join([mod for mod in get_panda_key_modifiers(evt) if mod is not None]) + + # if the prefix is not empty, append a '-' + if prefix: + prefix += '-' + + return prefix + + +class QMeta3DWidget(QWidget): + def __init__(self, Meta3DWorld, parent=None, FPS=60, debug=False): + QWidget.__init__(self, parent) + self.rp_sync_requested = False + # set fixed geometry + self.Meta3DWorld = Meta3DWorld + self.Meta3DWorld.set_parent(self) + # Setup a timer in Qt that runs taskMgr.step() to simulate Panda's own main loop + # pandaTimer = QTimer(self) + # pandaTimer.timeout.connect() + # pandaTimer.start(0) + + self.setFocusPolicy(Qt.StrongFocus) + + # Setup another timer that redraws this widget in a specific FPS + # redrawTimer = QTimer(self) + # redrawTimer.timeout.connect(self.update) + # redrawTimer.start(1000/FPS) + + self.paintSurface = QPainter() + self.rotate = QTransform() + self.rotate.rotate(180) + self.out_image = QImage() + + size = self.Meta3DWorld.cam.node().get_lens().get_film_size() + self.initial_film_size = QSizeF(size.x, size.y) + self.initial_size = self.size() + + self.synchronizer = QMeta3DSynchronizer(self, FPS) + self.synchronizer.start() + + self.debug = debug + + def mousePressEvent(self, evt): + button = evt.button() + try: + b = "{}{}".format(get_panda_key_modifiers_prefix(evt), QMeta3D_Button_translation[button]) + if self.debug: + print(b) + messenger.send(b,[{"x":evt.x(),"y":evt.y()}]) + except: + print("Unimplemented button. Please send an issue on github to fix this problem") + + def mouseMoveEvent(self, evt:QtGui.QMouseEvent): + button = evt.button() + try: + b = "mouse-move" + if self.debug: + print(b) + messenger.send(b,[{"x":evt.x(),"y":evt.y()}]) + except: + print("Unimplemented button. Please send an issue on github to fix this problem") + + + def mouseReleaseEvent(self, evt): + button = evt.button() + try: + b = "{}{}-up".format(get_panda_key_modifiers_prefix(evt), QMeta3D_Button_translation[button]) + if self.debug: + print(b) + messenger.send(b,[{"x":evt.x(),"y":evt.y()}]) + except: + print("Unimplemented button. Please send an issue on github to fix this problem") + + def wheelEvent(self, evt): + delta = evt.angleDelta().y() + try: + if self.debug: + print(f"wheel {delta}") + messenger.send('wheel',[{"delta":delta}]) + except: + print("Unimplemented button. Please send an issue on github to fix this problem") + + def keyPressEvent(self, evt): + key = evt.key() + try: + k = "{}{}".format(get_panda_key_modifiers_prefix(evt), QMeta3D_Key_translation[key]) + if self.debug: + print(k) + messenger.send(k) + except: + print("Unimplemented key. Please send an issue on github to fix this problem") + + def keyReleaseEvent(self, evt): + key = evt.key() + try: + k = "{}{}-up".format(get_panda_key_modifiers_prefix(evt), QMeta3D_Key_translation[key]) + if self.debug: + print(k) + messenger.send(k) + except: + print("Unimplemented key. Please send an issue on github to fix this problem") + + def resizeEvent(self, evt): + width = evt.size().width() + height = evt.size().height() + #print(f"width:{width}") + #print(f"height:{height}") + + from Meta3DWorld import resize_buffer + + lens = self.Meta3DWorld.cam.node().get_lens() + + def minimumSizeHint(self): + return QSize(400, 300) + + def paintEvent(self, event): + tex = self.Meta3DWorld.qt_output_tex + gsg = base.win.getGsg() + + if not gsg: + self.update() + return + + if not tex.hasRamImage(): + base.graphicsEngine.extractTextureData(tex, gsg) + self.update() + return + + data = tex.getRamImage().getData() + tex_width = tex.getXSize() + tex_height = tex.getYSize() + expected_len = tex_width * tex_height * 4 + + if len(data) != expected_len: + print(f"⚠️ 像素数据长度异常({len(data)} != {expected_len}),跳过绘制") + self.update() + return + + img = QImage(data, tex_width, tex_height, QImage.Format_ARGB32).mirrored() + + widget_width = self.width() + widget_height = self.height() + + painter = QPainter(self) + + # 【保持宽高比的缩放】 + scaled_img = img.scaled(widget_width, widget_height, Qt.KeepAspectRatio, Qt.SmoothTransformation) + + # 居中绘制 + x_offset = (widget_width - scaled_img.width()) // 2 + y_offset = (widget_height - scaled_img.height()) // 2 + + painter.drawImage(x_offset, y_offset, scaled_img) + painter.end() + + def sync_Meta3d_window_size(self, width, height): + try: + from panda3d.core import WindowProperties, LVecBase2i + + # 确保尺寸是4的倍数(RenderPipeline 要求) + adjusted_width = width - width % 4 + adjusted_height = height - height % 4 + + # 更新窗口属性 + props = WindowProperties() + props.setSize(adjusted_width, adjusted_height) + + # 对于 offscreen 渲染,直接更新窗口属性 + if self.Meta3DWorld.win: + self.Meta3DWorld.win.request_properties(props) + + # 手动触发 RenderPipeline 的尺寸更新逻辑 + if hasattr(self.Meta3DWorld, 'render_pipeline'): + # 更新全局分辨率 + from RenderPipelineFile.rpcore.globals import Globals + Globals.native_resolution = LVecBase2i(adjusted_width, adjusted_height) + + # 重新计算渲染分辨率 + self.Meta3DWorld.render_pipeline._compute_render_resolution() + + # 通知各个管理器处理尺寸变化 + self.Meta3DWorld.render_pipeline.light_mgr.compute_tile_size() + self.Meta3DWorld.render_pipeline.stage_mgr.handle_window_resize() + if hasattr(self.Meta3DWorld.render_pipeline, 'debugger'): + self.Meta3DWorld.render_pipeline.debugger.handle_window_resize() + + # 触发插件的窗口尺寸变化钩子 + self.Meta3DWorld.render_pipeline.plugin_mgr.trigger_hook("window_resized") + except Exception as e: + print(f"同步 Meta3D 窗口尺寸失败: {str(e)}") \ No newline at end of file diff --git a/QPanda3D/QPanda3D_Buttons_Translation.py b/QMeta3D/QMeta3D_Buttons_Translation.py similarity index 75% rename from QPanda3D/QPanda3D_Buttons_Translation.py rename to QMeta3D/QMeta3D_Buttons_Translation.py index db011c66..4a69d0d6 100644 --- a/QPanda3D/QPanda3D_Buttons_Translation.py +++ b/QMeta3D/QMeta3D_Buttons_Translation.py @@ -1,52 +1,43 @@ -# -*- coding: utf-8-*- -""" -Module : QPanda3D_Translation_Buttons -Author : Niklas Mevenkamp -Description : - Contains a dictionary that translates QT mouse events to panda3d - mouse events. -""" -# PyQt imports -from PyQt5.QtCore import * -from PyQt5.QtGui import * -from PyQt5.QtWidgets import * -import sys -__all__ = ["QPanda3D_Keys_Translation"] - -QPanda3D_Button_translation ={ -Qt.NoButton:'', -Qt.AllButtons:'unknown', -Qt.LeftButton:'mouse1', -Qt.RightButton:'mouse2', -Qt.MidButton:'mouse3', -Qt.MiddleButton:'mouse3', -Qt.BackButton:'unknown', -Qt.XButton1:'unknown', -Qt.ExtraButton1:'unknown', -Qt.ForwardButton:'unknown', -Qt.XButton2:'unknown', -Qt.ExtraButton2:'unknown', -Qt.TaskButton:'unknown', -Qt.ExtraButton3:'unknown', -Qt.ExtraButton4:'unknown', -Qt.ExtraButton5:'unknown', -Qt.ExtraButton6:'unknown', -Qt.ExtraButton7:'unknown', -Qt.ExtraButton8:'unknown', -Qt.ExtraButton9:'unknown', -Qt.ExtraButton10:'unknown', -Qt.ExtraButton11:'unknown', -Qt.ExtraButton12:'unknown', -Qt.ExtraButton13:'unknown', -Qt.ExtraButton14:'unknown', -Qt.ExtraButton15:'unknown', -Qt.ExtraButton16:'unknown', -Qt.ExtraButton17:'unknown', -Qt.ExtraButton18:'unknown', -Qt.ExtraButton19:'unknown', -Qt.ExtraButton20:'unknown', -Qt.ExtraButton21:'unknown', -Qt.ExtraButton22:'unknown', -Qt.ExtraButton23:'unknown', -Qt.ExtraButton24:'unknown', +from PyQt5.QtCore import * +from PyQt5.QtGui import * +from PyQt5.QtWidgets import * +import sys +__all__ = ["QMeta3D_Keys_Translation.py"] + +QMeta3D_Button_translation ={ +Qt.NoButton:'', +Qt.AllButtons:'unknown', +Qt.LeftButton:'mouse1', +Qt.RightButton:'mouse2', +Qt.MidButton:'mouse3', +Qt.MiddleButton:'mouse3', +Qt.BackButton:'unknown', +Qt.XButton1:'unknown', +Qt.ExtraButton1:'unknown', +Qt.ForwardButton:'unknown', +Qt.XButton2:'unknown', +Qt.ExtraButton2:'unknown', +Qt.TaskButton:'unknown', +Qt.ExtraButton3:'unknown', +Qt.ExtraButton4:'unknown', +Qt.ExtraButton5:'unknown', +Qt.ExtraButton6:'unknown', +Qt.ExtraButton7:'unknown', +Qt.ExtraButton8:'unknown', +Qt.ExtraButton9:'unknown', +Qt.ExtraButton10:'unknown', +Qt.ExtraButton11:'unknown', +Qt.ExtraButton12:'unknown', +Qt.ExtraButton13:'unknown', +Qt.ExtraButton14:'unknown', +Qt.ExtraButton15:'unknown', +Qt.ExtraButton16:'unknown', +Qt.ExtraButton17:'unknown', +Qt.ExtraButton18:'unknown', +Qt.ExtraButton19:'unknown', +Qt.ExtraButton20:'unknown', +Qt.ExtraButton21:'unknown', +Qt.ExtraButton22:'unknown', +Qt.ExtraButton23:'unknown', +Qt.ExtraButton24:'unknown', } \ No newline at end of file diff --git a/QPanda3D/QPanda3D_Keys_Translation.py b/QMeta3D/QMeta3D_Keys_Translation.py similarity index 94% rename from QPanda3D/QPanda3D_Keys_Translation.py rename to QMeta3D/QMeta3D_Keys_Translation.py index 97d510b7..0b0c8dda 100644 --- a/QPanda3D/QPanda3D_Keys_Translation.py +++ b/QMeta3D/QMeta3D_Keys_Translation.py @@ -1,486 +1,477 @@ -# -*- coding: utf-8-*- -""" -Module : QPanda3D_Translation_Keys -Author : Saifeddine ALOUI -Description : - Contains a dictionary that translates QT keyboard events to panda3d - keyboard events. -""" -# PyQt imports -from PyQt5.QtCore import * -from PyQt5.QtGui import * -from PyQt5.QtWidgets import * -import sys -__all__ = ["QPanda3D_Keys_Translation"] - -QPanda3D_Key_translation ={ -Qt.Key_0:'0', -Qt.Key_1:'1', -Qt.Key_2:'2', -Qt.Key_3:'3', -Qt.Key_4:'4', -Qt.Key_5:'5', -Qt.Key_6:'6', -Qt.Key_7:'7', -Qt.Key_8:'8', -Qt.Key_9:'9', -Qt.Key_A:'a', -Qt.Key_AE:'ae', -Qt.Key_Aacute:'unknown', -Qt.Key_Acircumflex:'unknown', -Qt.Key_AddFavorite:'unknown', -Qt.Key_Adiaeresis:'unknown', -Qt.Key_Agrave:'unknown', -Qt.Key_Alt:'lalt', -Qt.Key_AltGr:'unknown', -Qt.Key_Ampersand:'unknown', -Qt.Key_Any:'unknown', -Qt.Key_Apostrophe:'unknown', -Qt.Key_ApplicationLeft:'unknown', -Qt.Key_ApplicationRight:'unknown', -Qt.Key_Aring:'unknown', -Qt.Key_AsciiCircum:'unknown', -Qt.Key_AsciiTilde:'unknown', -Qt.Key_Asterisk:'unknown', -Qt.Key_At:'unknown', -Qt.Key_Atilde:'unknown', -Qt.Key_AudioCycleTrack:'unknown', -Qt.Key_AudioForward:'unknown', -Qt.Key_AudioRandomPlay:'unknown', -Qt.Key_AudioRepeat:'unknown', -Qt.Key_AudioRewind:'unknown', -Qt.Key_Away:'unknown', -Qt.Key_B:'b', -Qt.Key_Back:'unknown', -Qt.Key_BackForward:'unknown', -Qt.Key_Backslash:'unknown', -Qt.Key_Backspace:'unknown', -Qt.Key_Backtab:'unknown', -Qt.Key_Bar:'unknown', -Qt.Key_BassBoost:'unknown', -Qt.Key_BassDown:'unknown', -Qt.Key_BassUp:'unknown', -Qt.Key_Battery:'unknown', -Qt.Key_Blue:'unknown', -Qt.Key_Bluetooth:'unknown', -Qt.Key_Book:'unknown', -Qt.Key_BraceLeft:'unknown', -Qt.Key_BraceRight:'unknown', -Qt.Key_BracketLeft:'unknown', -Qt.Key_BracketRight:'unknown', -Qt.Key_BrightnessAdjust:'unknown', -Qt.Key_C:'c', -Qt.Key_CD:'unknown', -Qt.Key_Calculator:'unknown', -Qt.Key_Calendar:'unknown', -Qt.Key_Call:'unknown', -Qt.Key_Camera:'unknown', -Qt.Key_CameraFocus:'unknown', -Qt.Key_Cancel:'unknown', -Qt.Key_CapsLock:'unknown', -Qt.Key_Ccedilla:'unknown', -Qt.Key_ChannelDown:'unknown', -Qt.Key_ChannelUp:'unknown', -Qt.Key_Clear:'unknown', -Qt.Key_ClearGrab:'unknown', -Qt.Key_Close:'unknown', -Qt.Key_Codeinput:'unknown', -Qt.Key_Colon:'unknown', -Qt.Key_Comma:'unknown', -Qt.Key_Community:'unknown', -Qt.Key_Context1:'unknown', -Qt.Key_Context2:'unknown', -Qt.Key_Context3:'unknown', -Qt.Key_Context4:'unknown', -Qt.Key_ContrastAdjust:'unknown', -Qt.Key_Control:'control', -Qt.Key_Copy:'unknown', -Qt.Key_Cut:'unknown', -Qt.Key_D:'d', -Qt.Key_DOS:'unknown', -Qt.Key_Dead_A:'unknown', -Qt.Key_Dead_Abovecomma:'unknown', -Qt.Key_Dead_Abovedot:'unknown', -Qt.Key_Dead_Abovereversedcomma:'unknown', -Qt.Key_Dead_Abovering:'unknown', -Qt.Key_Dead_Aboveverticalline:'unknown', -Qt.Key_Dead_Acute:'unknown', -Qt.Key_Dead_Belowbreve:'unknown', -Qt.Key_Dead_Belowcircumflex:'unknown', -Qt.Key_Dead_Belowcomma:'unknown', -Qt.Key_Dead_Belowdiaeresis:'unknown', -Qt.Key_Dead_Belowdot:'unknown', -Qt.Key_Dead_Belowmacron:'unknown', -Qt.Key_Dead_Belowring:'unknown', -Qt.Key_Dead_Belowtilde:'unknown', -Qt.Key_Dead_Belowverticalline:'unknown', -Qt.Key_Dead_Breve:'unknown', -Qt.Key_Dead_Capital_Schwa:'unknown', -Qt.Key_Dead_Caron:'unknown', -Qt.Key_Dead_Cedilla:'unknown', -Qt.Key_Dead_Circumflex:'unknown', -Qt.Key_Dead_Currency:'unknown', -Qt.Key_Dead_Diaeresis:'unknown', -Qt.Key_Dead_Doubleacute:'unknown', -Qt.Key_Dead_Doublegrave:'unknown', -Qt.Key_Dead_E:'unknown', -Qt.Key_Dead_Grave:'unknown', -Qt.Key_Dead_Greek:'unknown', -Qt.Key_Dead_Hook:'unknown', -Qt.Key_Dead_Horn:'unknown', -Qt.Key_Dead_I:'unknown', -Qt.Key_Dead_Invertedbreve:'unknown', -Qt.Key_Dead_Iota:'unknown', -Qt.Key_Dead_Longsolidusoverlay:'unknown', -Qt.Key_Dead_Lowline:'unknown', -Qt.Key_Dead_Macron:'unknown', -Qt.Key_Dead_O:'unknown', -Qt.Key_Dead_Ogonek:'unknown', -Qt.Key_Dead_Semivoiced_Sound:'unknown', -Qt.Key_Dead_Small_Schwa:'unknown', -Qt.Key_Dead_Stroke:'unknown', -Qt.Key_Dead_Tilde:'unknown', -Qt.Key_Dead_U:'unknown', -Qt.Key_Dead_Voiced_Sound:'unknown', -Qt.Key_Dead_a:'unknown', -Qt.Key_Dead_e:'unknown', -Qt.Key_Dead_i:'unknown', -Qt.Key_Dead_o:'unknown', -Qt.Key_Dead_u:'unknown', -Qt.Key_Delete:'delete', -Qt.Key_Direction_L:'unknown', -Qt.Key_Direction_R:'unknown', -Qt.Key_Display:'unknown', -Qt.Key_Documents:'unknown', -Qt.Key_Dollar:'unknown', -Qt.Key_Down:'arrow_down', -Qt.Key_E:'e', -Qt.Key_ETH:'unknown', -Qt.Key_Eacute:'unknown', -Qt.Key_Ecircumflex:'unknown', -Qt.Key_Ediaeresis:'unknown', -Qt.Key_Egrave:'unknown', -Qt.Key_Eisu_Shift:'unknown', -Qt.Key_Eisu_toggle:'unknown', -Qt.Key_Eject:'unknown', -Qt.Key_End:'unknown', -Qt.Key_Enter:'unknown', -Qt.Key_Equal:'unknown', -Qt.Key_Escape:'escape', -Qt.Key_Excel:'unknown', -Qt.Key_Exclam:'unknown', -Qt.Key_Execute:'unknown', -Qt.Key_Exit:'unknown', -Qt.Key_Explorer:'unknown', -Qt.Key_F:'f', -Qt.Key_F1:'f1', -Qt.Key_F10:'f10', -Qt.Key_F11:'f11', -Qt.Key_F12:'f12', -Qt.Key_F13:'f13', -Qt.Key_F14:'f14', -Qt.Key_F15:'f15', -Qt.Key_F16:'f16', -Qt.Key_F17:'f17', -Qt.Key_F18:'f18', -Qt.Key_F19:'unknown', -Qt.Key_F2:'unknown', -Qt.Key_F20:'unknown', -Qt.Key_F21:'unknown', -Qt.Key_F22:'unknown', -Qt.Key_F23:'unknown', -Qt.Key_F24:'unknown', -Qt.Key_F25:'unknown', -Qt.Key_F26:'unknown', -Qt.Key_F27:'unknown', -Qt.Key_F28:'unknown', -Qt.Key_F29:'unknown', -Qt.Key_F3:'f3', -Qt.Key_F30:'unknown', -Qt.Key_F31:'unknown', -Qt.Key_F32:'unknown', -Qt.Key_F33:'unknown', -Qt.Key_F34:'unknown', -Qt.Key_F35:'unknown', -Qt.Key_F4:'f4', -Qt.Key_F5:'f5', -Qt.Key_F6:'f6', -Qt.Key_F7:'f7', -Qt.Key_F8:'f8', -Qt.Key_F9:'f9', -Qt.Key_Favorites:'unknown', -Qt.Key_Finance:'unknown', -Qt.Key_Find:'unknown', -Qt.Key_Flip:'unknown', -Qt.Key_Forward:'unknown', -Qt.Key_G:'g', -Qt.Key_Game:'unknown', -Qt.Key_Go:'unknown', -Qt.Key_Greater:'unknown', -Qt.Key_Green:'unknown', -Qt.Key_Guide:'unknown', -Qt.Key_H:'h', -Qt.Key_Hangul:'unknown', -Qt.Key_Hangul_Banja:'unknown', -Qt.Key_Hangul_End:'unknown', -Qt.Key_Hangul_Hanja:'unknown', -Qt.Key_Hangul_Jamo:'unknown', -Qt.Key_Hangul_Jeonja:'unknown', -Qt.Key_Hangul_PostHanja:'unknown', -Qt.Key_Hangul_PreHanja:'unknown', -Qt.Key_Hangul_Romaja:'unknown', -Qt.Key_Hangul_Special:'unknown', -Qt.Key_Hangul_Start:'unknown', -Qt.Key_Hangup:'unknown', -Qt.Key_Hankaku:'unknown', -Qt.Key_Help:'unknown', -Qt.Key_Henkan:'unknown', -Qt.Key_Hibernate:'unknown', -Qt.Key_Hiragana:'unknown', -Qt.Key_Hiragana_Katakana:'unknown', -Qt.Key_History:'unknown', -Qt.Key_Home:'home', -Qt.Key_HomePage:'unknown', -Qt.Key_HotLinks:'unknown', -Qt.Key_Hyper_L:'unknown', -Qt.Key_Hyper_R:'unknown', -Qt.Key_I:'i', -Qt.Key_Iacute:'unknown', -Qt.Key_Icircumflex:'unknown', -Qt.Key_Idiaeresis:'unknown', -Qt.Key_Igrave:'unknown', -Qt.Key_Info:'unknown', -Qt.Key_Insert:'unknown', -Qt.Key_J:'j', -Qt.Key_K:'k', -Qt.Key_Kana_Lock:'unknown', -Qt.Key_Kana_Shift:'unknown', -Qt.Key_Kanji:'unknown', -Qt.Key_Katakana:'unknown', -Qt.Key_KeyboardBrightnessDown:'unknown', -Qt.Key_KeyboardBrightnessUp:'unknown', -Qt.Key_KeyboardLightOnOff:'unknown', -Qt.Key_L:'l', -Qt.Key_LastNumberRedial:'unknown', -Qt.Key_Launch0:'unknown', -Qt.Key_Launch1:'unknown', -Qt.Key_Launch2:'unknown', -Qt.Key_Launch3:'unknown', -Qt.Key_Launch4:'unknown', -Qt.Key_Launch5:'unknown', -Qt.Key_Launch6:'unknown', -Qt.Key_Launch7:'unknown', -Qt.Key_Launch8:'unknown', -Qt.Key_Launch9:'unknown', -Qt.Key_LaunchA:'unknown', -Qt.Key_LaunchB:'unknown', -Qt.Key_LaunchC:'unknown', -Qt.Key_LaunchD:'unknown', -Qt.Key_LaunchE:'unknown', -Qt.Key_LaunchF:'unknown', -Qt.Key_LaunchG:'unknown', -Qt.Key_LaunchH:'unknown', -Qt.Key_LaunchMail:'unknown', -Qt.Key_LaunchMedia:'unknown', -Qt.Key_Left:'arrow_left', -Qt.Key_Less:'unknown', -Qt.Key_LightBulb:'unknown', -Qt.Key_LogOff:'unknown', -Qt.Key_M:'m', -Qt.Key_MailForward:'unknown', -Qt.Key_Market:'unknown', -Qt.Key_Massyo:'unknown', -Qt.Key_MediaLast:'unknown', -Qt.Key_MediaNext:'unknown', -Qt.Key_MediaPause:'unknown', -Qt.Key_MediaPlay:'unknown', -Qt.Key_MediaPrevious:'unknown', -Qt.Key_MediaRecord:'unknown', -Qt.Key_MediaStop:'unknown', -Qt.Key_MediaTogglePlayPause:'unknown', -Qt.Key_Meeting:'unknown', -Qt.Key_Memo:'unknown', -Qt.Key_Menu:'unknown', -Qt.Key_MenuKB:'unknown', -Qt.Key_MenuPB:'unknown', -Qt.Key_Messenger:'unknown', -Qt.Key_Meta:'unknown', -Qt.Key_MicMute:'unknown', -Qt.Key_MicVolumeDown:'unknown', -Qt.Key_MicVolumeUp:'unknown', -Qt.Key_Minus:'unknown', -Qt.Key_Mode_switch:'unknown', -Qt.Key_MonBrightnessDown:'unknown', -Qt.Key_MonBrightnessUp:'unknown', -Qt.Key_Muhenkan:'unknown', -Qt.Key_Multi_key:'unknown', -Qt.Key_MultipleCandidate:'unknown', -Qt.Key_Music:'unknown', -Qt.Key_MySites:'unknown', -Qt.Key_N:'n', -Qt.Key_New:'unknown', -Qt.Key_News:'unknown', -Qt.Key_No:'unknown', -Qt.Key_Ntilde:'unknown', -Qt.Key_NumLock:'unknown', -Qt.Key_NumberSign:'unknown', -Qt.Key_O:'o', -Qt.Key_Oacute:'unknown', -Qt.Key_Ocircumflex:'unknown', -Qt.Key_Odiaeresis:'unknown', -Qt.Key_OfficeHome:'unknown', -Qt.Key_Ograve:'unknown', -Qt.Key_Ooblique:'unknown', -Qt.Key_Open:'unknown', -Qt.Key_OpenUrl:'unknown', -Qt.Key_Option:'unknown', -Qt.Key_Otilde:'unknown', -Qt.Key_P:'p', -Qt.Key_PageDown:'unknown', -Qt.Key_PageUp:'unknown', -Qt.Key_ParenLeft:'unknown', -Qt.Key_ParenRight:'unknown', -Qt.Key_Paste:'unknown', -Qt.Key_Pause:'unknown', -Qt.Key_Percent:'unknown', -Qt.Key_Period:'unknown', -Qt.Key_Phone:'unknown', -Qt.Key_Pictures:'unknown', -Qt.Key_Play:'unknown', -Qt.Key_Plus:'unknown', -Qt.Key_PowerDown:'unknown', -Qt.Key_PowerOff:'unknown', -Qt.Key_PreviousCandidate:'unknown', -Qt.Key_Print:'unknown', -Qt.Key_Printer:'unknown', -Qt.Key_Q:'q', -Qt.Key_Question:'unknown', -Qt.Key_QuoteDbl:'unknown', -Qt.Key_QuoteLeft:'unknown', -Qt.Key_R:'r', -Qt.Key_Red:'unknown', -Qt.Key_Redo:'unknown', -Qt.Key_Refresh:'unknown', -Qt.Key_Reload:'unknown', -Qt.Key_Reply:'unknown', -Qt.Key_Return:'unknown', -Qt.Key_Right:'arrow_right', -Qt.Key_Romaji:'unknown', -Qt.Key_RotateWindows:'unknown', -Qt.Key_RotationKB:'unknown', -Qt.Key_RotationPB:'unknown', -Qt.Key_S:'s', -Qt.Key_Save:'unknown', -Qt.Key_ScreenSaver:'unknown', -Qt.Key_ScrollLock:'unknown', -Qt.Key_Search:'unknown', -Qt.Key_Select:'unknown', -Qt.Key_Semicolon:'unknown', -Qt.Key_Send:'unknown', -Qt.Key_Settings:'unknown', -Qt.Key_Shift:'unknown', -Qt.Key_Shop:'unknown', -Qt.Key_SingleCandidate:'unknown', -Qt.Key_Slash:'unknown', -Qt.Key_Sleep:'unknown', -Qt.Key_Space:'space', -Qt.Key_Spell:'unknown', -Qt.Key_SplitScreen:'unknown', -Qt.Key_Standby:'unknown', -Qt.Key_Stop:'unknown', -Qt.Key_Subtitle:'unknown', -Qt.Key_Super_L:'unknown', -Qt.Key_Super_R:'unknown', -Qt.Key_Support:'unknown', -Qt.Key_Suspend:'unknown', -Qt.Key_SysReq:'unknown', -Qt.Key_T:'t', -Qt.Key_THORN:'unknown', -Qt.Key_Tab:'unknown', -Qt.Key_TaskPane:'unknown', -Qt.Key_Terminal:'unknown', -Qt.Key_Time:'unknown', -Qt.Key_ToDoList:'unknown', -Qt.Key_ToggleCallHangup:'unknown', -Qt.Key_Tools:'unknown', -Qt.Key_TopMenu:'unknown', -Qt.Key_TouchpadOff:'unknown', -Qt.Key_TouchpadOn:'unknown', -Qt.Key_TouchpadToggle:'unknown', -Qt.Key_Touroku:'unknown', -Qt.Key_Travel:'unknown', -Qt.Key_TrebleDown:'unknown', -Qt.Key_TrebleUp:'unknown', -Qt.Key_U:'u', -Qt.Key_UWB:'unknown', -Qt.Key_Uacute:'unknown', -Qt.Key_Ucircumflex:'unknown', -Qt.Key_Udiaeresis:'unknown', -Qt.Key_Ugrave:'unknown', -Qt.Key_Underscore:'unknown', -Qt.Key_Undo:'unknown', -Qt.Key_Up:'arrow_up', -Qt.Key_V:'v', -Qt.Key_Video:'unknown', -Qt.Key_View:'unknown', -Qt.Key_VoiceDial:'unknown', -Qt.Key_VolumeDown:'unknown', -Qt.Key_VolumeMute:'unknown', -Qt.Key_VolumeUp:'unknown', -Qt.Key_W:'w', -Qt.Key_WLAN:'unknown', -Qt.Key_WWW:'unknown', -Qt.Key_WakeUp:'unknown', -Qt.Key_WebCam:'unknown', -Qt.Key_Word:'unknown', -Qt.Key_X:'x', -Qt.Key_Xfer:'unknown', -Qt.Key_Y:'y', -Qt.Key_Yacute:'unknown', -Qt.Key_Yellow:'unknown', -Qt.Key_Yes:'unknown', -Qt.Key_Z:'z', -Qt.Key_Zenkaku:'unknown', -Qt.Key_Zenkaku_Hankaku:'unknown', -Qt.Key_Zoom:'unknown', -Qt.Key_ZoomIn:'unknown', -Qt.Key_ZoomOut:'unknown', -Qt.Key_acute:'unknown', -Qt.Key_brokenbar:'unknown', -Qt.Key_cedilla:'unknown', -Qt.Key_cent:'unknown', -Qt.Key_copyright:'unknown', -Qt.Key_currency:'unknown', -Qt.Key_degree:'unknown', -Qt.Key_diaeresis:'unknown', -Qt.Key_division:'unknown', -Qt.Key_exclamdown:'unknown', -Qt.Key_guillemotleft:'unknown', -Qt.Key_guillemotright:'unknown', -Qt.Key_hyphen:'unknown', -Qt.Key_iTouch:'unknown', -Qt.Key_macron:'unknown', -Qt.Key_masculine:'unknown', -Qt.Key_mu:'unknown', -Qt.Key_multiply:'unknown', -Qt.Key_nobreakspace:'unknown', -Qt.Key_notsign:'unknown', -Qt.Key_onehalf:'unknown', -Qt.Key_onequarter:'unknown', -Qt.Key_onesuperior:'unknown', -Qt.Key_ordfeminine:'unknown', -Qt.Key_paragraph:'unknown', -Qt.Key_periodcentered:'unknown', -Qt.Key_plusminus:'unknown', -Qt.Key_questiondown:'unknown', -Qt.Key_registered:'unknown', -Qt.Key_section:'unknown', -Qt.Key_ssharp:'unknown', -Qt.Key_sterling:'unknown', -Qt.Key_threequarters:'unknown', -Qt.Key_threesuperior:'unknown', -Qt.Key_twosuperior:'unknown', -Qt.Key_unknown:'unknown', -Qt.Key_ydiaeresis:'unknown', -Qt.Key_yen:'unknown', +from PyQt5.QtCore import * +from PyQt5.QtGui import * +from PyQt5.QtWidgets import * +import sys +__all__ = ["QMeta3D_Keys_Translation.py"] + +QMeta3D_Key_translation ={ +Qt.Key_0:'0', +Qt.Key_1:'1', +Qt.Key_2:'2', +Qt.Key_3:'3', +Qt.Key_4:'4', +Qt.Key_5:'5', +Qt.Key_6:'6', +Qt.Key_7:'7', +Qt.Key_8:'8', +Qt.Key_9:'9', +Qt.Key_A:'a', +Qt.Key_AE:'ae', +Qt.Key_Aacute:'unknown', +Qt.Key_Acircumflex:'unknown', +Qt.Key_AddFavorite:'unknown', +Qt.Key_Adiaeresis:'unknown', +Qt.Key_Agrave:'unknown', +Qt.Key_Alt:'lalt', +Qt.Key_AltGr:'unknown', +Qt.Key_Ampersand:'unknown', +Qt.Key_Any:'unknown', +Qt.Key_Apostrophe:'unknown', +Qt.Key_ApplicationLeft:'unknown', +Qt.Key_ApplicationRight:'unknown', +Qt.Key_Aring:'unknown', +Qt.Key_AsciiCircum:'unknown', +Qt.Key_AsciiTilde:'unknown', +Qt.Key_Asterisk:'unknown', +Qt.Key_At:'unknown', +Qt.Key_Atilde:'unknown', +Qt.Key_AudioCycleTrack:'unknown', +Qt.Key_AudioForward:'unknown', +Qt.Key_AudioRandomPlay:'unknown', +Qt.Key_AudioRepeat:'unknown', +Qt.Key_AudioRewind:'unknown', +Qt.Key_Away:'unknown', +Qt.Key_B:'b', +Qt.Key_Back:'unknown', +Qt.Key_BackForward:'unknown', +Qt.Key_Backslash:'unknown', +Qt.Key_Backspace:'unknown', +Qt.Key_Backtab:'unknown', +Qt.Key_Bar:'unknown', +Qt.Key_BassBoost:'unknown', +Qt.Key_BassDown:'unknown', +Qt.Key_BassUp:'unknown', +Qt.Key_Battery:'unknown', +Qt.Key_Blue:'unknown', +Qt.Key_Bluetooth:'unknown', +Qt.Key_Book:'unknown', +Qt.Key_BraceLeft:'unknown', +Qt.Key_BraceRight:'unknown', +Qt.Key_BracketLeft:'unknown', +Qt.Key_BracketRight:'unknown', +Qt.Key_BrightnessAdjust:'unknown', +Qt.Key_C:'c', +Qt.Key_CD:'unknown', +Qt.Key_Calculator:'unknown', +Qt.Key_Calendar:'unknown', +Qt.Key_Call:'unknown', +Qt.Key_Camera:'unknown', +Qt.Key_CameraFocus:'unknown', +Qt.Key_Cancel:'unknown', +Qt.Key_CapsLock:'unknown', +Qt.Key_Ccedilla:'unknown', +Qt.Key_ChannelDown:'unknown', +Qt.Key_ChannelUp:'unknown', +Qt.Key_Clear:'unknown', +Qt.Key_ClearGrab:'unknown', +Qt.Key_Close:'unknown', +Qt.Key_Codeinput:'unknown', +Qt.Key_Colon:'unknown', +Qt.Key_Comma:'unknown', +Qt.Key_Community:'unknown', +Qt.Key_Context1:'unknown', +Qt.Key_Context2:'unknown', +Qt.Key_Context3:'unknown', +Qt.Key_Context4:'unknown', +Qt.Key_ContrastAdjust:'unknown', +Qt.Key_Control:'control', +Qt.Key_Copy:'unknown', +Qt.Key_Cut:'unknown', +Qt.Key_D:'d', +Qt.Key_DOS:'unknown', +Qt.Key_Dead_A:'unknown', +Qt.Key_Dead_Abovecomma:'unknown', +Qt.Key_Dead_Abovedot:'unknown', +Qt.Key_Dead_Abovereversedcomma:'unknown', +Qt.Key_Dead_Abovering:'unknown', +Qt.Key_Dead_Aboveverticalline:'unknown', +Qt.Key_Dead_Acute:'unknown', +Qt.Key_Dead_Belowbreve:'unknown', +Qt.Key_Dead_Belowcircumflex:'unknown', +Qt.Key_Dead_Belowcomma:'unknown', +Qt.Key_Dead_Belowdiaeresis:'unknown', +Qt.Key_Dead_Belowdot:'unknown', +Qt.Key_Dead_Belowmacron:'unknown', +Qt.Key_Dead_Belowring:'unknown', +Qt.Key_Dead_Belowtilde:'unknown', +Qt.Key_Dead_Belowverticalline:'unknown', +Qt.Key_Dead_Breve:'unknown', +Qt.Key_Dead_Capital_Schwa:'unknown', +Qt.Key_Dead_Caron:'unknown', +Qt.Key_Dead_Cedilla:'unknown', +Qt.Key_Dead_Circumflex:'unknown', +Qt.Key_Dead_Currency:'unknown', +Qt.Key_Dead_Diaeresis:'unknown', +Qt.Key_Dead_Doubleacute:'unknown', +Qt.Key_Dead_Doublegrave:'unknown', +Qt.Key_Dead_E:'unknown', +Qt.Key_Dead_Grave:'unknown', +Qt.Key_Dead_Greek:'unknown', +Qt.Key_Dead_Hook:'unknown', +Qt.Key_Dead_Horn:'unknown', +Qt.Key_Dead_I:'unknown', +Qt.Key_Dead_Invertedbreve:'unknown', +Qt.Key_Dead_Iota:'unknown', +Qt.Key_Dead_Longsolidusoverlay:'unknown', +Qt.Key_Dead_Lowline:'unknown', +Qt.Key_Dead_Macron:'unknown', +Qt.Key_Dead_O:'unknown', +Qt.Key_Dead_Ogonek:'unknown', +Qt.Key_Dead_Semivoiced_Sound:'unknown', +Qt.Key_Dead_Small_Schwa:'unknown', +Qt.Key_Dead_Stroke:'unknown', +Qt.Key_Dead_Tilde:'unknown', +Qt.Key_Dead_U:'unknown', +Qt.Key_Dead_Voiced_Sound:'unknown', +Qt.Key_Dead_a:'unknown', +Qt.Key_Dead_e:'unknown', +Qt.Key_Dead_i:'unknown', +Qt.Key_Dead_o:'unknown', +Qt.Key_Dead_u:'unknown', +Qt.Key_Delete:'delete', +Qt.Key_Direction_L:'unknown', +Qt.Key_Direction_R:'unknown', +Qt.Key_Display:'unknown', +Qt.Key_Documents:'unknown', +Qt.Key_Dollar:'unknown', +Qt.Key_Down:'arrow_down', +Qt.Key_E:'e', +Qt.Key_ETH:'unknown', +Qt.Key_Eacute:'unknown', +Qt.Key_Ecircumflex:'unknown', +Qt.Key_Ediaeresis:'unknown', +Qt.Key_Egrave:'unknown', +Qt.Key_Eisu_Shift:'unknown', +Qt.Key_Eisu_toggle:'unknown', +Qt.Key_Eject:'unknown', +Qt.Key_End:'unknown', +Qt.Key_Enter:'unknown', +Qt.Key_Equal:'unknown', +Qt.Key_Escape:'escape', +Qt.Key_Excel:'unknown', +Qt.Key_Exclam:'unknown', +Qt.Key_Execute:'unknown', +Qt.Key_Exit:'unknown', +Qt.Key_Explorer:'unknown', +Qt.Key_F:'f', +Qt.Key_F1:'f1', +Qt.Key_F10:'f10', +Qt.Key_F11:'f11', +Qt.Key_F12:'f12', +Qt.Key_F13:'f13', +Qt.Key_F14:'f14', +Qt.Key_F15:'f15', +Qt.Key_F16:'f16', +Qt.Key_F17:'f17', +Qt.Key_F18:'f18', +Qt.Key_F19:'unknown', +Qt.Key_F2:'unknown', +Qt.Key_F20:'unknown', +Qt.Key_F21:'unknown', +Qt.Key_F22:'unknown', +Qt.Key_F23:'unknown', +Qt.Key_F24:'unknown', +Qt.Key_F25:'unknown', +Qt.Key_F26:'unknown', +Qt.Key_F27:'unknown', +Qt.Key_F28:'unknown', +Qt.Key_F29:'unknown', +Qt.Key_F3:'f3', +Qt.Key_F30:'unknown', +Qt.Key_F31:'unknown', +Qt.Key_F32:'unknown', +Qt.Key_F33:'unknown', +Qt.Key_F34:'unknown', +Qt.Key_F35:'unknown', +Qt.Key_F4:'f4', +Qt.Key_F5:'f5', +Qt.Key_F6:'f6', +Qt.Key_F7:'f7', +Qt.Key_F8:'f8', +Qt.Key_F9:'f9', +Qt.Key_Favorites:'unknown', +Qt.Key_Finance:'unknown', +Qt.Key_Find:'unknown', +Qt.Key_Flip:'unknown', +Qt.Key_Forward:'unknown', +Qt.Key_G:'g', +Qt.Key_Game:'unknown', +Qt.Key_Go:'unknown', +Qt.Key_Greater:'unknown', +Qt.Key_Green:'unknown', +Qt.Key_Guide:'unknown', +Qt.Key_H:'h', +Qt.Key_Hangul:'unknown', +Qt.Key_Hangul_Banja:'unknown', +Qt.Key_Hangul_End:'unknown', +Qt.Key_Hangul_Hanja:'unknown', +Qt.Key_Hangul_Jamo:'unknown', +Qt.Key_Hangul_Jeonja:'unknown', +Qt.Key_Hangul_PostHanja:'unknown', +Qt.Key_Hangul_PreHanja:'unknown', +Qt.Key_Hangul_Romaja:'unknown', +Qt.Key_Hangul_Special:'unknown', +Qt.Key_Hangul_Start:'unknown', +Qt.Key_Hangup:'unknown', +Qt.Key_Hankaku:'unknown', +Qt.Key_Help:'unknown', +Qt.Key_Henkan:'unknown', +Qt.Key_Hibernate:'unknown', +Qt.Key_Hiragana:'unknown', +Qt.Key_Hiragana_Katakana:'unknown', +Qt.Key_History:'unknown', +Qt.Key_Home:'home', +Qt.Key_HomePage:'unknown', +Qt.Key_HotLinks:'unknown', +Qt.Key_Hyper_L:'unknown', +Qt.Key_Hyper_R:'unknown', +Qt.Key_I:'i', +Qt.Key_Iacute:'unknown', +Qt.Key_Icircumflex:'unknown', +Qt.Key_Idiaeresis:'unknown', +Qt.Key_Igrave:'unknown', +Qt.Key_Info:'unknown', +Qt.Key_Insert:'unknown', +Qt.Key_J:'j', +Qt.Key_K:'k', +Qt.Key_Kana_Lock:'unknown', +Qt.Key_Kana_Shift:'unknown', +Qt.Key_Kanji:'unknown', +Qt.Key_Katakana:'unknown', +Qt.Key_KeyboardBrightnessDown:'unknown', +Qt.Key_KeyboardBrightnessUp:'unknown', +Qt.Key_KeyboardLightOnOff:'unknown', +Qt.Key_L:'l', +Qt.Key_LastNumberRedial:'unknown', +Qt.Key_Launch0:'unknown', +Qt.Key_Launch1:'unknown', +Qt.Key_Launch2:'unknown', +Qt.Key_Launch3:'unknown', +Qt.Key_Launch4:'unknown', +Qt.Key_Launch5:'unknown', +Qt.Key_Launch6:'unknown', +Qt.Key_Launch7:'unknown', +Qt.Key_Launch8:'unknown', +Qt.Key_Launch9:'unknown', +Qt.Key_LaunchA:'unknown', +Qt.Key_LaunchB:'unknown', +Qt.Key_LaunchC:'unknown', +Qt.Key_LaunchD:'unknown', +Qt.Key_LaunchE:'unknown', +Qt.Key_LaunchF:'unknown', +Qt.Key_LaunchG:'unknown', +Qt.Key_LaunchH:'unknown', +Qt.Key_LaunchMail:'unknown', +Qt.Key_LaunchMedia:'unknown', +Qt.Key_Left:'arrow_left', +Qt.Key_Less:'unknown', +Qt.Key_LightBulb:'unknown', +Qt.Key_LogOff:'unknown', +Qt.Key_M:'m', +Qt.Key_MailForward:'unknown', +Qt.Key_Market:'unknown', +Qt.Key_Massyo:'unknown', +Qt.Key_MediaLast:'unknown', +Qt.Key_MediaNext:'unknown', +Qt.Key_MediaPause:'unknown', +Qt.Key_MediaPlay:'unknown', +Qt.Key_MediaPrevious:'unknown', +Qt.Key_MediaRecord:'unknown', +Qt.Key_MediaStop:'unknown', +Qt.Key_MediaTogglePlayPause:'unknown', +Qt.Key_Meeting:'unknown', +Qt.Key_Memo:'unknown', +Qt.Key_Menu:'unknown', +Qt.Key_MenuKB:'unknown', +Qt.Key_MenuPB:'unknown', +Qt.Key_Messenger:'unknown', +Qt.Key_Meta:'unknown', +Qt.Key_MicMute:'unknown', +Qt.Key_MicVolumeDown:'unknown', +Qt.Key_MicVolumeUp:'unknown', +Qt.Key_Minus:'unknown', +Qt.Key_Mode_switch:'unknown', +Qt.Key_MonBrightnessDown:'unknown', +Qt.Key_MonBrightnessUp:'unknown', +Qt.Key_Muhenkan:'unknown', +Qt.Key_Multi_key:'unknown', +Qt.Key_MultipleCandidate:'unknown', +Qt.Key_Music:'unknown', +Qt.Key_MySites:'unknown', +Qt.Key_N:'n', +Qt.Key_New:'unknown', +Qt.Key_News:'unknown', +Qt.Key_No:'unknown', +Qt.Key_Ntilde:'unknown', +Qt.Key_NumLock:'unknown', +Qt.Key_NumberSign:'unknown', +Qt.Key_O:'o', +Qt.Key_Oacute:'unknown', +Qt.Key_Ocircumflex:'unknown', +Qt.Key_Odiaeresis:'unknown', +Qt.Key_OfficeHome:'unknown', +Qt.Key_Ograve:'unknown', +Qt.Key_Ooblique:'unknown', +Qt.Key_Open:'unknown', +Qt.Key_OpenUrl:'unknown', +Qt.Key_Option:'unknown', +Qt.Key_Otilde:'unknown', +Qt.Key_P:'p', +Qt.Key_PageDown:'unknown', +Qt.Key_PageUp:'unknown', +Qt.Key_ParenLeft:'unknown', +Qt.Key_ParenRight:'unknown', +Qt.Key_Paste:'unknown', +Qt.Key_Pause:'unknown', +Qt.Key_Percent:'unknown', +Qt.Key_Period:'unknown', +Qt.Key_Phone:'unknown', +Qt.Key_Pictures:'unknown', +Qt.Key_Play:'unknown', +Qt.Key_Plus:'unknown', +Qt.Key_PowerDown:'unknown', +Qt.Key_PowerOff:'unknown', +Qt.Key_PreviousCandidate:'unknown', +Qt.Key_Print:'unknown', +Qt.Key_Printer:'unknown', +Qt.Key_Q:'q', +Qt.Key_Question:'unknown', +Qt.Key_QuoteDbl:'unknown', +Qt.Key_QuoteLeft:'unknown', +Qt.Key_R:'r', +Qt.Key_Red:'unknown', +Qt.Key_Redo:'unknown', +Qt.Key_Refresh:'unknown', +Qt.Key_Reload:'unknown', +Qt.Key_Reply:'unknown', +Qt.Key_Return:'unknown', +Qt.Key_Right:'arrow_right', +Qt.Key_Romaji:'unknown', +Qt.Key_RotateWindows:'unknown', +Qt.Key_RotationKB:'unknown', +Qt.Key_RotationPB:'unknown', +Qt.Key_S:'s', +Qt.Key_Save:'unknown', +Qt.Key_ScreenSaver:'unknown', +Qt.Key_ScrollLock:'unknown', +Qt.Key_Search:'unknown', +Qt.Key_Select:'unknown', +Qt.Key_Semicolon:'unknown', +Qt.Key_Send:'unknown', +Qt.Key_Settings:'unknown', +Qt.Key_Shift:'unknown', +Qt.Key_Shop:'unknown', +Qt.Key_SingleCandidate:'unknown', +Qt.Key_Slash:'unknown', +Qt.Key_Sleep:'unknown', +Qt.Key_Space:'space', +Qt.Key_Spell:'unknown', +Qt.Key_SplitScreen:'unknown', +Qt.Key_Standby:'unknown', +Qt.Key_Stop:'unknown', +Qt.Key_Subtitle:'unknown', +Qt.Key_Super_L:'unknown', +Qt.Key_Super_R:'unknown', +Qt.Key_Support:'unknown', +Qt.Key_Suspend:'unknown', +Qt.Key_SysReq:'unknown', +Qt.Key_T:'t', +Qt.Key_THORN:'unknown', +Qt.Key_Tab:'unknown', +Qt.Key_TaskPane:'unknown', +Qt.Key_Terminal:'unknown', +Qt.Key_Time:'unknown', +Qt.Key_ToDoList:'unknown', +Qt.Key_ToggleCallHangup:'unknown', +Qt.Key_Tools:'unknown', +Qt.Key_TopMenu:'unknown', +Qt.Key_TouchpadOff:'unknown', +Qt.Key_TouchpadOn:'unknown', +Qt.Key_TouchpadToggle:'unknown', +Qt.Key_Touroku:'unknown', +Qt.Key_Travel:'unknown', +Qt.Key_TrebleDown:'unknown', +Qt.Key_TrebleUp:'unknown', +Qt.Key_U:'u', +Qt.Key_UWB:'unknown', +Qt.Key_Uacute:'unknown', +Qt.Key_Ucircumflex:'unknown', +Qt.Key_Udiaeresis:'unknown', +Qt.Key_Ugrave:'unknown', +Qt.Key_Underscore:'unknown', +Qt.Key_Undo:'unknown', +Qt.Key_Up:'arrow_up', +Qt.Key_V:'v', +Qt.Key_Video:'unknown', +Qt.Key_View:'unknown', +Qt.Key_VoiceDial:'unknown', +Qt.Key_VolumeDown:'unknown', +Qt.Key_VolumeMute:'unknown', +Qt.Key_VolumeUp:'unknown', +Qt.Key_W:'w', +Qt.Key_WLAN:'unknown', +Qt.Key_WWW:'unknown', +Qt.Key_WakeUp:'unknown', +Qt.Key_WebCam:'unknown', +Qt.Key_Word:'unknown', +Qt.Key_X:'x', +Qt.Key_Xfer:'unknown', +Qt.Key_Y:'y', +Qt.Key_Yacute:'unknown', +Qt.Key_Yellow:'unknown', +Qt.Key_Yes:'unknown', +Qt.Key_Z:'z', +Qt.Key_Zenkaku:'unknown', +Qt.Key_Zenkaku_Hankaku:'unknown', +Qt.Key_Zoom:'unknown', +Qt.Key_ZoomIn:'unknown', +Qt.Key_ZoomOut:'unknown', +Qt.Key_acute:'unknown', +Qt.Key_brokenbar:'unknown', +Qt.Key_cedilla:'unknown', +Qt.Key_cent:'unknown', +Qt.Key_copyright:'unknown', +Qt.Key_currency:'unknown', +Qt.Key_degree:'unknown', +Qt.Key_diaeresis:'unknown', +Qt.Key_division:'unknown', +Qt.Key_exclamdown:'unknown', +Qt.Key_guillemotleft:'unknown', +Qt.Key_guillemotright:'unknown', +Qt.Key_hyphen:'unknown', +Qt.Key_iTouch:'unknown', +Qt.Key_macron:'unknown', +Qt.Key_masculine:'unknown', +Qt.Key_mu:'unknown', +Qt.Key_multiply:'unknown', +Qt.Key_nobreakspace:'unknown', +Qt.Key_notsign:'unknown', +Qt.Key_onehalf:'unknown', +Qt.Key_onequarter:'unknown', +Qt.Key_onesuperior:'unknown', +Qt.Key_ordfeminine:'unknown', +Qt.Key_paragraph:'unknown', +Qt.Key_periodcentered:'unknown', +Qt.Key_plusminus:'unknown', +Qt.Key_questiondown:'unknown', +Qt.Key_registered:'unknown', +Qt.Key_section:'unknown', +Qt.Key_ssharp:'unknown', +Qt.Key_sterling:'unknown', +Qt.Key_threequarters:'unknown', +Qt.Key_threesuperior:'unknown', +Qt.Key_twosuperior:'unknown', +Qt.Key_unknown:'unknown', +Qt.Key_ydiaeresis:'unknown', +Qt.Key_yen:'unknown', } \ No newline at end of file diff --git a/QMeta3D/QMeta3D_Modifiers_Translation.py b/QMeta3D/QMeta3D_Modifiers_Translation.py new file mode 100644 index 00000000..17684818 --- /dev/null +++ b/QMeta3D/QMeta3D_Modifiers_Translation.py @@ -0,0 +1,15 @@ +from PyQt5.QtCore import * +from PyQt5.QtGui import * +from PyQt5.QtWidgets import * +import sys +__all__ = ["QMeta3D_Modifiers_Translation.py"] + +QMeta3D_Modifier_translation ={ +Qt.NoModifier:None, +Qt.ShiftModifier:'shift', +Qt.ControlModifier:'control', +Qt.AltModifier:'alt', +Qt.MetaModifier:'unknown', +Qt.KeypadModifier:'unknown', +Qt.GroupSwitchModifier:'unknown', +} \ No newline at end of file diff --git a/QPanda3D/QMouseWatcherNode.py b/QMeta3D/QMouseWatcherNode.py similarity index 58% rename from QPanda3D/QMouseWatcherNode.py rename to QMeta3D/QMouseWatcherNode.py index c810df66..e75d86cc 100644 --- a/QPanda3D/QMouseWatcherNode.py +++ b/QMeta3D/QMouseWatcherNode.py @@ -1,41 +1,27 @@ -# -*- coding: utf-8-*- -""" -Module : QMouseWatcherNode -Author : Niklas Mevenkamp -Description : - This is a MouseWatcherNode implementation that accesses - mouse position and button states through a parent QWidget. -""" - -# PyQt imports -from PyQt5.QtCore import * -from PyQt5.QtGui import * -from PyQt5.QtWidgets import * - -# Panda imports -from panda3d.core import * - -__all__ = ["QMouseWatcherNode"] - - -class QMouseWatcherNode(MouseWatcher): - def __init__(self, parent): - super().__init__() - - self.parent = parent - - def getMouse(self, *args, **kwargs): - # map global QCursor pixel position to parent Widget coordinates - pos = self.parent.mapFromGlobal(QCursor.pos()) - - # map absolute pixel positions to relative ones - rel_x = -1 + 2 * pos.x() / self.parent.width() - rel_y = -1 + 2 * pos.y() / self.parent.height() - - # invert y - rel_y = -rel_y - - return LPoint2(rel_x, rel_y) - - def hasMouse(self): - return isinstance(self.parent, QWidget) +from PyQt5.QtCore import * +from PyQt5.QtGui import * +from PyQt5.QtWidgets import * + +from panda3d.core import * + +__all__ = ["QMouseWatcherNode"] + + +class QMouseWatcherNode(MouseWatcher): + def __init__(self, parent): + super().__init__() + + self.parent = parent + + def getMouse(self, *args, **kwargs): + pos = self.parent.mapFromGlobal(QCursor.pos()) + + rel_x = -1 + 2 * pos.x() / self.parent.width() + rel_y = -1 + 2 * pos.y() / self.parent.height() + + rel_y = -rel_y + + return LPoint2(rel_x, rel_y) + + def hasMouse(self): + return isinstance(self.parent, QWidget) diff --git a/QPanda3D/Tools/__init__.py b/QMeta3D/Tools/__init__.py similarity index 72% rename from QPanda3D/Tools/__init__.py rename to QMeta3D/Tools/__init__.py index 03b1010f..449353ac 100644 --- a/QPanda3D/Tools/__init__.py +++ b/QMeta3D/Tools/__init__.py @@ -1,2 +1,2 @@ -name="QPanda3D" +name="QMeta3D" __all__ = ["generate_qt_to_pd3d_translator"] \ No newline at end of file diff --git a/QMeta3D/Tools/generate_qt_to_pd3d_translator.py b/QMeta3D/Tools/generate_qt_to_pd3d_translator.py new file mode 100644 index 00000000..3f296230 --- /dev/null +++ b/QMeta3D/Tools/generate_qt_to_pd3d_translator.py @@ -0,0 +1,11 @@ +from PyQt5.QtCore import * +from PyQt5.QtGui import * +from PyQt5.QtWidgets import * + +H=Qt.__dict__ +lst = [a for a in H if a.startswith("Key_")] +QMeta3D_Key_translation = "QMeta3D_Key_translation ={" +for i in range(len(lst)): + QMeta3D_Key_translation += "Qt.{}:'unknown',\n".format(lst[i]) +QMeta3D_Key_translation += "}" +print(QMeta3D_Key_translation) \ No newline at end of file diff --git a/QMeta3D/__init__.py b/QMeta3D/__init__.py new file mode 100644 index 00000000..b94cb4e0 --- /dev/null +++ b/QMeta3D/__init__.py @@ -0,0 +1,2 @@ +name="QMeta3D" +__all__ = ["QMeta3DWidget.py", "Meta3DWorld.py", "QMeta3D_Keys_Translation.py"] \ No newline at end of file diff --git a/QPanda3D/QPanda3D_Modifiers_Translation.py b/QPanda3D/QPanda3D_Modifiers_Translation.py deleted file mode 100644 index ce32160b..00000000 --- a/QPanda3D/QPanda3D_Modifiers_Translation.py +++ /dev/null @@ -1,24 +0,0 @@ -# -*- coding: utf-8-*- -""" -Module : QPanda3D_Translation_Modifiers -Author : Niklas Mevenkamp -Description : - Contains a dictionary that translates QT keyboard events to panda3d - keyboard events. -""" -# PyQt imports -from PyQt5.QtCore import * -from PyQt5.QtGui import * -from PyQt5.QtWidgets import * -import sys -__all__ = ["QPanda3D_Modifiers_Translation"] - -QPanda3D_Modifier_translation ={ -Qt.NoModifier:None, -Qt.ShiftModifier:'shift', -Qt.ControlModifier:'control', -Qt.AltModifier:'alt', -Qt.MetaModifier:'unknown', -Qt.KeypadModifier:'unknown', -Qt.GroupSwitchModifier:'unknown', -} \ No newline at end of file diff --git a/QPanda3D/Tools/generate_qt_to_pd3d_translator.py b/QPanda3D/Tools/generate_qt_to_pd3d_translator.py deleted file mode 100644 index 9b4b6c69..00000000 --- a/QPanda3D/Tools/generate_qt_to_pd3d_translator.py +++ /dev/null @@ -1,11 +0,0 @@ -from PyQt5.QtCore import * -from PyQt5.QtGui import * -from PyQt5.QtWidgets import * - -H=Qt.__dict__ -lst = [a for a in H if a.startswith("Key_")] -QPanda3D_Key_translation ="QPanda3D_Key_translation ={" -for i in range(len(lst)): - QPanda3D_Key_translation +="Qt.{}:'unknown',\n".format(lst[i]) -QPanda3D_Key_translation += "}" -print(QPanda3D_Key_translation) \ No newline at end of file diff --git a/QPanda3D/__init__.py b/QPanda3D/__init__.py deleted file mode 100644 index 5cf0b25b..00000000 --- a/QPanda3D/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -name="QPanda3D" -__all__ = ["QPanda3DWidget", "Panda3DWorld", "QPanda3D_Keys_Translation"] \ No newline at end of file diff --git a/RenderPipelineFile/config/panda3d-config.prc b/RenderPipelineFile/config/Meta3d-config.prc similarity index 100% rename from RenderPipelineFile/config/panda3d-config.prc rename to RenderPipelineFile/config/Meta3d-config.prc diff --git a/RenderPipelineFile/config/daytime.yaml b/RenderPipelineFile/config/daytime.yaml index 54447c55..030a6111 100644 --- a/RenderPipelineFile/config/daytime.yaml +++ b/RenderPipelineFile/config/daytime.yaml @@ -17,7 +17,7 @@ control_points: scattering: sun_intensity: [[[0.0000000000,0.0000000000],[0.0041666667,0.0000000000],[0.0083333333,0.0000000000],[0.0125000000,0.0000000000],[0.0166666667,0.0000000000],[0.0208333333,0.0000000000],[0.0250000000,0.0000000000],[0.0291666667,0.0000000000],[0.0333333333,0.0000000000],[0.0375000000,0.0000000000],[0.0416666667,0.0000000000],[0.0458333333,0.0000000000],[0.0500000000,0.0000000000],[0.0541666667,0.0000000000],[0.0583333333,0.0000000000],[0.0625000000,0.0000000000],[0.0666666667,0.0000000000],[0.0708333333,0.0000000000],[0.0750000000,0.0000000000],[0.0791666667,0.0000000000],[0.0833333333,0.0000000000],[0.0875000000,0.0000000000],[0.0916666667,0.0000000000],[0.0958333333,0.0000000000],[0.1000000000,0.0000000000],[0.1041666667,0.0000000000],[0.1083333333,0.0000000000],[0.1125000000,0.0000000000],[0.1166666667,0.0000000000],[0.1208333333,0.0000000000],[0.1250000000,0.0000000000],[0.1291666667,0.0000000000],[0.1333333333,0.0000000000],[0.1375000000,0.0000000000],[0.1416666667,0.0000000000],[0.1458333333,0.0000000000],[0.1500000000,0.0000000000],[0.1541666667,0.0000000000],[0.1583333333,0.0000028805],[0.1625000000,0.0003577724],[0.1666666667,0.0013331400],[0.1708333333,0.0029671803],[0.1750000000,0.0052963381],[0.1791666667,0.0083550556],[0.1833333333,0.0121755589],[0.1875000000,0.0167876159],[0.1916666667,0.0222183530],[0.1958333333,0.0284919947],[0.2000000000,0.0356297193],[0.2041666667,0.0436494349],[0.2083333333,0.0525656099],[0.2125000000,0.0623891610],[0.2166666667,0.0731272461],[0.2208333333,0.0847831708],[0.2250000000,0.0973563167],[0.2291666667,0.1108419698],[0.2333333333,0.1252313631],[0.2375000000,0.1405115250],[0.2416666667,0.1566653434],[0.2458333333,0.1736715009],[0.2500000000,0.1915046014],[0.2541666667,0.2101350464],[0.2583333333,0.2295292930],[0.2625000000,0.2496498145],[0.2666666667,0.2704552670],[0.2708333333,0.2919006662],[0.2750000000,0.3139375192],[0.2791666667,0.3365139497],[0.2833333333,0.3595750662],[0.2875000000,0.3830630359],[0.2916666667,0.4069173972],[0.2958333333,0.4310753462],[0.3000000000,0.4554720417],[0.3041666667,0.4800408236],[0.3083333333,0.5047136020],[0.3125000000,0.5294212108],[0.3166666667,0.5540936424],[0.3208333333,0.5786605298],[0.3250000000,0.6030514553],[0.3291666667,0.6271963182],[0.3333333333,0.6510256858],[0.3375000000,0.6744711982],[0.3416666667,0.6974659988],[0.3458333333,0.7199450163],[0.3500000000,0.7418453485],[0.3541666667,0.7631067095],[0.3583333333,0.7836717291],[0.3625000000,0.8034862953],[0.3666666667,0.8224999302],[0.3708333333,0.8406661079],[0.3750000000,0.8579425235],[0.3791666667,0.8742914270],[0.3833333333,0.8896799131],[0.3875000000,0.9040801386],[0.3916666667,0.9174695289],[0.3958333333,0.9298310650],[0.4000000000,0.9411533765],[0.4041666667,0.9514309312],[0.4083333333,0.9606641691],[0.4125000000,0.9688595571],[0.4166666667,0.9760296330],[0.4208333333,0.9821930708],[0.4250000000,0.9873746114],[0.4291666667,0.9916050060],[0.4333333333,0.9949209310],[0.4375000000,0.9973647924],[0.4416666667,0.9989845508],[0.4458333333,0.9998334497],[0.4500000000,0.9999696949],[0.4541666667,0.9994560801],[0.4583333333,0.9983595429],[0.4625000000,0.9967506613],[0.4666666667,0.9947030614],[0.4708333333,0.9922927758],[0.4750000000,0.9895975125],[0.4791666667,0.9866958610],[0.4833333333,0.9836664262],[0.4875000000,0.9805868867],[0.4916666667,0.9775330316],[0.4958333333,0.9745777179],[0.5000000000,0.9717898417],[0.5041666667,0.9692332877],[0.5083333333,0.9669658924],[0.5125000000,0.9650384806],[0.5089595376,0.9690650222],[0.5208333333,0.9623666659],[0.5250000000,0.9616814371],[0.5291666667,0.9614534423],[0.5333333333,0.9616877089],[0.5375000000,0.9623790807],[0.5416666667,0.9635123329],[0.5458333333,0.9650624244],[0.5500000000,0.9669949804],[0.5541666667,0.9692669864],[0.5583333333,0.9718275065],[0.5625000000,0.9746185969],[0.5666666667,0.9775762863],[0.5708333333,0.9806315864],[0.5750000000,0.9837115661],[0.5791666667,0.9867403433],[0.5833333333,0.9896401655],[0.5875000000,0.9923323562],[0.5916666667,0.9947382579],[0.5958333333,0.9967800977],[0.6000000000,0.9983817820],[0.6041666667,0.9994696263],[0.6083333333,0.9999730028],[0.6125000000,0.9998249266],[0.6166666667,0.9989625601],[0.6208333333,0.9973276624],[0.6250000000,0.9948669567],[0.6291666667,0.9915324664],[0.6333333333,0.9872817545],[0.6375000000,0.9820781426],[0.6416666667,0.9758908775],[0.6458333333,0.9686952146],[0.6500000000,0.9604725211],[0.6541666667,0.9512102537],[0.6583333333,0.9409019858],[0.6625000000,0.9295473441],[0.6666666667,0.9171518878],[0.6708333333,0.9037270619],[0.6750000000,0.8892899902],[0.6791666667,0.8738633008],[0.6833333333,0.8574749656],[0.6875000000,0.8401579787],[0.6916666667,0.8219502453],[0.6958333333,0.8028941798],[0.7000000000,0.7830364456],[0.7041666667,0.7624277344],[0.7083333333,0.7411222520],[0.7125000000,0.7191776044],[0.7166666667,0.6966542563],[0.7208333333,0.6736152714],[0.7250000000,0.6501259629],[0.7291666667,0.6262533880],[0.7333333333,0.6020661121],[0.7375000000,0.5776338043],[0.7416666667,0.5530267796],[0.7458333333,0.5283156992],[0.7500000000,0.5035711751],[0.7541666667,0.4788634341],[0.7583333333,0.4542618347],[0.7625000000,0.4298347613],[0.7666666667,0.4056490351],[0.7708333333,0.3817697830],[0.7750000000,0.3582600107],[0.7791666667,0.3351803495],[0.7833333333,0.3125888445],[0.7875000000,0.2905406366],[0.7916666667,0.2690876955],[0.7958333333,0.2482787388],[0.8000000000,0.2281588906],[0.8041666667,0.2087696425],[0.8083333333,0.1901486315],[0.8125000000,0.1723295359],[0.8166666667,0.1553419918],[0.8208333333,0.1392115328],[0.8250000000,0.1239595144],[0.8291666667,0.1096030703],[0.8333333333,0.0961551918],[0.8375000000,0.0836246599],[0.8416666667,0.0720161369],[0.8458333333,0.0613302273],[0.8500000000,0.0515635598],[0.8541666667,0.0427088803],[0.8583333333,0.0347551990],[0.8625000000,0.0276878920],[0.8666666667,0.0214889271],[0.8708333333,0.0161369711],[0.8750000000,0.0116076130],[0.8791666667,0.0078735477],[0.8833333333,0.0049047927],[0.8875000000,0.0026688977],[0.8916666667,0.0011311782],[0.8958333333,0.0002549473],[0.9000000000,0.0000000000],[0.9041666667,0.0000000000],[0.9083333333,0.0000000000],[0.9125000000,0.0000000000],[0.9166666667,0.0000000000],[0.9208333333,0.0000000000],[0.9250000000,0.0000000000],[0.9291666667,0.0000000000],[0.9333333333,0.0000000000],[0.9375000000,0.0000000000],[0.9416666667,0.0000000000],[0.9458333333,0.0000000000],[0.9500000000,0.0000000000],[0.9541666667,0.0000000000],[0.9583333333,0.0000000000],[0.9625000000,0.0000000000],[0.9666666667,0.0000000000],[0.9708333333,0.0000000000],[0.9750000000,0.0000000000],[0.9791666667,0.0000000000],[0.9833333333,0.0000000000],[0.9875000000,0.0000000000],[0.9916666667,0.0000000000],[0.9958333333,0.0000000000]]] sun_color: [[[0.5010435645,0.5818710306],[0.0433100000,0.8999700000],[0.8635787716,0.9130000000],[0.1785000000,0.8973600000],[0.8099800000,0.8651100000],[0.2360800000,0.7712700000],[0.6583432177,0.8485126184],[0.1266806142,0.9648102053],[0.9558541267,0.9090909091],[0.5568400771,0.7353760446]],[[0.5001318426,0.5160300000],[0.0572700000,0.6541600000],[0.2395000000,0.5976800000],[0.8104600000,0.6009000000],[0.6967400000,0.5483900000]],[[0.0862400000,0.4257800000],[0.4955600000,0.4033000000],[0.8234200000,0.4340200000]]] - sun_azimuth: [[[0.5000000000,0.6250000000]]] + sun_azimuth: [[[0.5000000000,0.4972222222]]] sun_altitude: [[[0.5000000000,0.9555555556]]] extinction: [[[0.4913294798,0.6378830084]]] volumetrics: diff --git a/RenderPipelineFile/config/pipeline.yaml b/RenderPipelineFile/config/pipeline.yaml index d1ac6e71..d060d12a 100644 --- a/RenderPipelineFile/config/pipeline.yaml +++ b/RenderPipelineFile/config/pipeline.yaml @@ -1,5 +1,3 @@ - - # This file stores internal settings of the pipeline. It does not contain the # plugin settings, but just the basic settings of the internal pipeline components. pipeline: @@ -77,8 +75,8 @@ shadows: # getting updated fast enough. However, this also affects the performance # quite a bit, since for every shadow map a part of the scene has # to get re-rendered. - max_updates: 8 + max_updates: 16 # Sets the maximum distance until which shadows are updated. If a shadow # source is further away, it will no longer recieve updates - max_update_distance: 150.0 + max_update_distance: 300.0 \ No newline at end of file diff --git a/RenderPipelineFile/config/plugins.yaml b/RenderPipelineFile/config/plugins.yaml index 5eaed062..717e6e6d 100644 --- a/RenderPipelineFile/config/plugins.yaml +++ b/RenderPipelineFile/config/plugins.yaml @@ -16,16 +16,16 @@ enabled: - sky_ao - smaa - ssr - # - clouds - # - dof - # - fxaa - # - volumetrics - # - vxgi + - clouds + #- dof + #- fxaa + #- volumetrics + #- vxgi overrides: ao: - blur_quality: MEDIUM + blur_quality: LOW blur_normal_factor: 2.97 blur_depth_factor: 0.88158 occlusion_strength: 2.19 @@ -87,10 +87,10 @@ overrides: sharpen_twice: False dof: - focal_point: 1000.0 - focal_size: 994.0 - blur_strength: 0.0 - near_blur_strength: 0.4286 + focal_point: 5 + focal_size: 1 + blur_strength: 1 + near_blur_strength: 1 env_probes: probe_resolution: 128 diff --git a/RenderPipelineFile/rpcore/light_manager.py b/RenderPipelineFile/rpcore/light_manager.py index fe9e4ceb..e336c3d7 100644 --- a/RenderPipelineFile/rpcore/light_manager.py +++ b/RenderPipelineFile/rpcore/light_manager.py @@ -90,7 +90,6 @@ class LightManager(RPObject): def remove_light(self, light): """ Removes a light """ - print(f'333333333333333333333333333333,{light.casts_shadows}') # from RenderPipelineFile.rpcore.pynative.internal_light_manager import InternalLightManager # inter = InternalLightManager() # inter.remove_light(light) diff --git a/RenderPipelineFile/rpcore/render_pipeline.py b/RenderPipelineFile/rpcore/render_pipeline.py index e4c46dbb..afe22c13 100644 --- a/RenderPipelineFile/rpcore/render_pipeline.py +++ b/RenderPipelineFile/rpcore/render_pipeline.py @@ -142,7 +142,7 @@ class RenderPipeline(RPObject): if not isfile("/$$rp/data/install.flag"): self.fatal("You didn't setup the pipeline yet! Please run setup.py.") - load_prc_file("/$$rpconfig/panda3d-config.prc") + load_prc_file("/$$rpconfig/Meta3d-config.prc") self._pre_showbase_initialized = True def create(self, base=None): @@ -208,7 +208,6 @@ class RenderPipeline(RPObject): def remove_light(self, light): """ Removes a previously attached light, check out the LightManager remove_light documentation for further information. """ - print(f'222222222222222222222222222,{light.casts_shadows}') self.light_mgr.remove_light(light) def load_ies_profile(self, filename): diff --git a/RenderPipelineFile/rpplugins/dof/dof_stage.py b/RenderPipelineFile/rpplugins/dof/dof_stage.py index b88da33a..fef2631f 100644 --- a/RenderPipelineFile/rpplugins/dof/dof_stage.py +++ b/RenderPipelineFile/rpplugins/dof/dof_stage.py @@ -1,29 +1,3 @@ -""" - -RenderPipeline - -Copyright (c) 2014-2016 tobspr - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -""" - from rpcore.render_stage import RenderStage diff --git a/RenderPipelineFile/rpplugins/dof/plugin.py b/RenderPipelineFile/rpplugins/dof/plugin.py index 6c3b3e4d..7dd00bc8 100644 --- a/RenderPipelineFile/rpplugins/dof/plugin.py +++ b/RenderPipelineFile/rpplugins/dof/plugin.py @@ -1,29 +1,3 @@ -""" - -RenderPipeline - -Copyright (c) 2014-2016 tobspr - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -""" - from rpcore.pluginbase.base_plugin import BasePlugin from .dof_stage import DoFStage diff --git a/RenderPipelineFile/start_plugin_configurator.py b/RenderPipelineFile/start_plugin_configurator.py index 6ac56335..cbfd9ec5 100644 --- a/RenderPipelineFile/start_plugin_configurator.py +++ b/RenderPipelineFile/start_plugin_configurator.py @@ -9,4 +9,5 @@ if not os.path.isfile(os.path.join(this_dir, "data", "install.flag")): print("Please install the pipeline first by running setup.py") sys.exit(-1) to_execute = os.path.join(this_dir, "toolkit", "plugin_configurator", "main.py") -subprocess.call([sys.executable, to_execute]) +# 使用相对路径而不是绝对路径 +subprocess.call([sys.executable, to_execute], cwd=this_dir) \ No newline at end of file diff --git a/RenderPipelineFile/toolkit/day_time_editor/curve_widget.py b/RenderPipelineFile/toolkit/day_time_editor/curve_widget.py index 249030af..44bfbd88 100644 --- a/RenderPipelineFile/toolkit/day_time_editor/curve_widget.py +++ b/RenderPipelineFile/toolkit/day_time_editor/curve_widget.py @@ -33,7 +33,9 @@ from six.moves import range # pylint: disable=import-error import math -from RenderPipelineFile.rplibs.pyqt_imports import * # noqa +# 修复导入路径问题 +from rplibs.pyqt_imports import * # noqa + class CurveWidget(QWidget): diff --git a/RenderPipelineFile/toolkit/day_time_editor/main.py b/RenderPipelineFile/toolkit/day_time_editor/main.py index 3031b478..82a68231 100644 --- a/RenderPipelineFile/toolkit/day_time_editor/main.py +++ b/RenderPipelineFile/toolkit/day_time_editor/main.py @@ -1,3 +1,5 @@ +#!/usr/bin/env python + """ RenderPipeline @@ -40,8 +42,9 @@ os.chdir(os.path.join(os.path.dirname(os.path.realpath(__file__)))) # Add the render pipeline to the path sys.path.insert(0, "../../") -from RenderPipelineFile.rplibs.six import iteritems # noqa -from RenderPipelineFile.rplibs.pyqt_imports import * # noqa +# 修复导入路径问题 +from rplibs.six import iteritems # noqa +from rplibs.pyqt_imports import * # noqa from curve_widget import CurveWidget # noqa diff --git a/RenderPipelineFile/toolkit/plugin_configurator/main.py b/RenderPipelineFile/toolkit/plugin_configurator/main.py index fd393ae8..ba0ff055 100644 --- a/RenderPipelineFile/toolkit/plugin_configurator/main.py +++ b/RenderPipelineFile/toolkit/plugin_configurator/main.py @@ -1,32 +1,4 @@ -""" - -RenderPipeline - -Copyright (c) 2014-2016 tobspr - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -""" - -# This tool offers an interface to configure the pipeline - -from __future__ import print_function +#!/usr/bin/env python import os import sys @@ -49,7 +21,39 @@ from rplibs.pyqt_imports import * #noqa # Load the generated UI Layout from ui.main_window_generated import Ui_MainWindow # noqa -from ui.widgets import UniversalMessageDialog +# 创建一个简单的UniversalMessageDialog类,避免复杂的导入问题 +class UniversalMessageDialog(QDialog): + @staticmethod + def show_success(parent, title, message, show_cancel=True, confirm_text="确认"): + msg_box = QMessageBox(QMessageBox.Information, title, message, QMessageBox.Ok, parent) + if show_cancel: + msg_box.addButton(QMessageBox.Cancel) + msg_box.button(QMessageBox.Ok).setText(confirm_text) + return msg_box.exec_() + + @staticmethod + def show_warning(parent, title, message, show_cancel=True, confirm_text="确认"): + msg_box = QMessageBox(QMessageBox.Warning, title, message, QMessageBox.Ok, parent) + if show_cancel: + msg_box.addButton(QMessageBox.Cancel) + msg_box.button(QMessageBox.Ok).setText(confirm_text) + return msg_box.exec_() + + @staticmethod + def show_error(parent, title, message, show_cancel=True, confirm_text="确认"): + msg_box = QMessageBox(QMessageBox.Critical, title, message, QMessageBox.Ok, parent) + if show_cancel: + msg_box.addButton(QMessageBox.Cancel) + msg_box.button(QMessageBox.Ok).setText(confirm_text) + return msg_box.exec_() + + @staticmethod + def show_info(parent, title, message, show_cancel=True, confirm_text="确认"): + msg_box = QMessageBox(QMessageBox.Information, title, message, QMessageBox.Ok, parent) + if show_cancel: + msg_box.addButton(QMessageBox.Cancel) + msg_box.button(QMessageBox.Ok).setText(confirm_text) + return msg_box.exec_() from rpcore.pluginbase.manager import PluginManager # noqa from rpcore.util.network_communication import NetworkCommunication # noqa @@ -106,6 +110,15 @@ class PluginConfigurator(QMainWindow, Ui_MainWindow): def closeEvent(self, event): # noqa event.accept() + # 通知主程序配置可能已更改 + try: + # 发送一个信号或者写入一个标记文件来通知主程序 + marker_file = "/tmp/rp_plugin_config_changed.flag" + with open(marker_file, "w") as f: + f.write("Plugin configuration may have changed") + except Exception as e: + print(f"无法创建配置更改标记文件: {e}") + import os os._exit(1) diff --git a/Resources/models/DancingTwerk.glb b/Resources/models/DancingTwerk.glb deleted file mode 100644 index 1567bf93..00000000 Binary files a/Resources/models/DancingTwerk.glb and /dev/null differ diff --git a/Resources/models/Haqijingzhu.glb b/Resources/models/Haqijingzhu.glb deleted file mode 100644 index 0b5e1df0..00000000 Binary files a/Resources/models/Haqijingzhu.glb and /dev/null differ diff --git a/Resources/models/JQB_auto_converted.glb b/Resources/models/JQB_auto_converted.glb deleted file mode 100644 index 89df9e5b..00000000 Binary files a/Resources/models/JQB_auto_converted.glb and /dev/null differ diff --git a/Resources/models/Women_1.glb b/Resources/models/Women_1.glb deleted file mode 100644 index 79ebe8d2..00000000 Binary files a/Resources/models/Women_1.glb and /dev/null differ diff --git a/Resources/models/Women_2.glb b/Resources/models/Women_2.glb deleted file mode 100644 index efba7b02..00000000 Binary files a/Resources/models/Women_2.glb and /dev/null differ diff --git a/Resources/models/women_1.glb b/Resources/models/women_1.glb deleted file mode 100644 index f634ea92..00000000 Binary files a/Resources/models/women_1.glb and /dev/null differ diff --git a/Start_Run.py b/Start_Run.py index 8e6a1214..81951385 100644 --- a/Start_Run.py +++ b/Start_Run.py @@ -5,6 +5,9 @@ import sys project_root = os.path.dirname(os.path.abspath(__file__)) sys.path.insert(0, project_root) +# 设置工作目录为项目根目录 +os.chdir(project_root) + # 添加 RenderPipeline 到路径(注意路径名称应与实际目录一致) render_pipeline_path = os.path.join(project_root, "RenderPipeline") sys.path.insert(0, render_pipeline_path) diff --git a/core/selection.py b/core/selection.py index 66295b8f..75fb874f 100644 --- a/core/selection.py +++ b/core/selection.py @@ -422,22 +422,26 @@ class SelectionSystem: is_rotate_tool = self.world.tool_manager.isRotateTool() if self.world.tool_manager else False import os + from panda3d.core import getModelPath, Filename + + # 获取项目根目录 base_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + + # 确保core目录在模型搜索路径中 + model_path = getModelPath() + core_dir = os.path.join(base_dir, "core") + core_filename = Filename.from_os_specific(core_dir) + if not model_path.findFile(core_filename): + model_path.appendDirectory(core_filename) + print(f"✓ 添加core目录到模型搜索路径: {core_dir}") if is_scale_tool: - model_paths = [ - os.path.join(base_dir,"core/UniformScaleHandle.fbx"), - ] + model_filename = "core/UniformScaleHandle.fbx" elif is_rotate_tool: - model_paths = [ - os.path.join(base_dir,"core/RotationHandleQuarter.fbx"), - ] + model_filename = "core/RotationHandleQuarter.fbx" + arrow_filename = "core/TranslateArrowHandle.fbx" else: - model_paths = [ - os.path.join(base_dir, "core/TranslateArrowHandle.fbx"), - ] - - arrow_path = os.path.join(base_dir, "core/TranslateArrowHandle.fbx") + model_filename = "core/TranslateArrowHandle.fbx" # model_paths = [ # "core/TranslateArrowHandle.fbx", @@ -445,18 +449,15 @@ class SelectionSystem: # ] gizmo_model = None gizmoRot_model = None - for path in model_paths: - try: - if is_rotate_tool: - gizmo_model = self.world.loader.loadModel(arrow_path) - gizmoRot_model = self.world.loader.loadModel(path) - else: - gizmo_model = self.world.loader.loadModel(path) - if gizmo_model: - #print(f"成功加载模型: {path}") - break - except: - continue + try: + if is_rotate_tool: + gizmo_model = self.world.loader.loadModel(arrow_filename) + gizmoRot_model = self.world.loader.loadModel(model_filename) + else: + gizmo_model = self.world.loader.loadModel(model_filename) + except Exception as e: + print(f"加载模型失败: {e}") + return x_rHandle = None y_rHandle = None @@ -647,7 +648,7 @@ class SelectionSystem: import time current_time = time.time() - if current_time - self._last_gizmo_update < 0.05: # 每0.05秒更新一次 + if current_time - self._last_gizmo_update < 0.5: # 每0.05秒更新一次 return task.cont self._last_gizmo_update = current_time @@ -1624,8 +1625,6 @@ class SelectionSystem: # 应用新缩放值 self.gizmoTarget.setScale(new_scale) - # 安全地更新属性面板 - #self._safeUpdatePropertyPanel() self.world.property_panel.refreshModelValues(self.gizmoTarget) return elif is_rotate_tool: @@ -1645,7 +1644,6 @@ class SelectionSystem: start_hpr.y + rotation_amount, start_hpr.z + rotation_amount) self.gizmoTarget.setHpr(new_hpr) - #self._safeUpdatePropertyPanel() self.world.property_panel.refreshModelValues(self.gizmoTarget) return @@ -1810,7 +1808,6 @@ class SelectionSystem: # 实时更新属性面板 self.world.property_panel.refreshModelValues(self.gizmoTarget) - #self._safeUpdatePropertyPanel() # 每次拖拽都输出调试信息(但限制频率) if not hasattr(self, '_last_drag_debug_time'): @@ -1844,39 +1841,6 @@ class SelectionSystem: except: return False - def _safeUpdatePropertyPanel(self): - """安全地更新属性面板""" - try: - # 检查属性面板是否存在且有效 - if (hasattr(self.world, 'property_panel') and - self.world.property_panel is not None): - - # 检查目标节点是否有效 - if (self.gizmoTarget is not None and - not self.gizmoTarget.isEmpty()): - - # 检查是否有refreshModelValues方法 - if hasattr(self.world.property_panel, 'refreshModelValues'): - self.world.property_panel.refreshModelValues(self.gizmoTarget) - # 如果是GUI元素,可能需要特殊的处理 - elif (hasattr(self.gizmoTarget, 'getTag') and - self.gizmoTarget.getTag("is_gui_element") == "1" and - hasattr(self.world.property_panel, 'updateGUIPropertyPanel')): - # 对于GUI元素,可能需要重新构建整个面板 - if (hasattr(self.world, 'treeWidget') and - self.world.treeWidget is not None): - current_item = self.world.treeWidget.currentItem() - if current_item is not None: - self.world.property_panel.updatePropertyPanel(current_item) - except RuntimeError as e: - if "wrapped C/C++ object" in str(e): - # 忽略控件已被删除的错误 - print("警告: 属性面板控件已被删除,跳过更新") - else: - print(f"更新属性面板时出错: {e}") - except Exception as e: - print(f"更新属性面板失败: {e}") - def stopGizmoDrag(self): """停止坐标轴拖拽""" print(f"停止坐标轴拖拽 - 轴: {self.dragGizmoAxis}") diff --git a/core/vr/config/vr_settings.json b/core/vr/config/vr_settings.json index 902d31fb..556f68a2 100644 --- a/core/vr/config/vr_settings.json +++ b/core/vr/config/vr_settings.json @@ -11,5 +11,8 @@ "enable_ssr": false, "shadow_quality": "medium", "ao_quality": "low" - } + }, + "anti_aliasing": "4x", + "refresh_rate": "90Hz", + "async_reprojection": true } \ No newline at end of file diff --git a/core/world.py b/core/world.py index 8cefc39c..549617c8 100644 --- a/core/world.py +++ b/core/world.py @@ -6,7 +6,7 @@ from direct.actor.Actor import Actor warnings.filterwarnings("ignore", category=DeprecationWarning) -from QPanda3D.Panda3DWorld import Panda3DWorld +from QMeta3D.Meta3DWorld import Meta3DWorld from panda3d.core import (CardMaker, Vec4, Vec3, AmbientLight, DirectionalLight, Point3, WindowProperties,Material,LColor) from direct.showbase.ShowBaseGlobal import globalClock @@ -20,7 +20,7 @@ except ImportError: PLUGIN_SUPPORT = False print("⚠ 插件管理器未找到,插件功能将不可用") -class CoreWorld(Panda3DWorld): +class CoreWorld(Meta3DWorld): """核心世界功能类 - 负责基础的3D世界设置和核心功能""" def __init__(self): @@ -77,6 +77,27 @@ class CoreWorld(Panda3DWorld): model_path.appendDirectory(resources_filename) print(f"✓ 添加Resources到模型搜索路径: {resources_dir}") + # 添加core目录到搜索路径 + core_dir = os.path.join(project_root, "core") + core_filename = Filename.from_os_specific(core_dir) + if not model_path.findFile(core_filename): + model_path.appendDirectory(core_filename) + print(f"✓ 添加core目录到模型搜索路径: {core_dir}") + + # 添加RenderPipeline目录到搜索路径 + rp_dir = os.path.join(project_root, "RenderPipelineFile") + rp_filename = Filename.from_os_specific(rp_dir) + if not model_path.findFile(rp_filename): + model_path.appendDirectory(rp_filename) + print(f"✓ 添加RenderPipeline目录到模型搜索路径: {rp_dir}") + + # 添加RenderPipeline data目录到搜索路径 + rp_data_dir = os.path.join(rp_dir, "data") + rp_data_filename = Filename.from_os_specific(rp_data_dir) + if not model_path.findFile(rp_data_filename): + model_path.appendDirectory(rp_data_filename) + print(f"✓ 添加RenderPipeline data目录到模型搜索路径: {rp_data_dir}") + # 同时添加各个子目录到搜索路径 subdirs = ['models', 'textures', 'animations', 'icons', 'materials'] for subdir in subdirs: @@ -213,7 +234,6 @@ class CoreWorld(Panda3DWorld): temp_anim.removeNode() # 清理临时动画 except Exception as e: print(f"❌ 动画文件加载失败: {e}") - return None # 4. 尝试创建Actor print(f"4. Actor创建测试:") @@ -492,16 +512,105 @@ class CoreWorld(Panda3DWorld): + # def _loadFont(self): + # """加载中文字体""" + # try: + # self.chinese_font = self.loader.loadFont('/usr/share/fonts/truetype/wqy/wqy-microhei.ttc') + # if not self.chinese_font: + # print("警告: 无法加载中文字体,将使用默认字体") + # else: + # print("✓ 中文字体加载成功") + # except: + # print("警告: 无法加载中文字体,将使用默认字体") + # self.chinese_font = None + def _loadFont(self): - """加载中文字体""" + """加载中文字体 - 跨平台,Panda3D 友好路径""" try: - self.chinese_font = self.loader.loadFont('/usr/share/fonts/truetype/wqy/wqy-microhei.ttc') + import os + import platform + from pathlib import Path + from panda3d.core import Filename + + self.chinese_font = None # 初始化 + + # --- 平台与项目根 --- + system = platform.system().lower() + project_root = Path(__file__).resolve().parent.parent # 上两级 + + # --- 候选字体路径(按平台) --- + if system == "windows": + win_dir = os.environ.get("WINDIR") or r"C:\Windows" + win_fonts_dir = Path(win_dir) / "Fonts" + font_candidates = [ + project_root / "RenderPipelineFile" / "data" / "font" / "msyh.ttc", + win_fonts_dir / "msyh.ttc", + win_fonts_dir / "msyh.ttf", + win_fonts_dir / "simhei.ttf", + win_fonts_dir / "simsun.ttc", + ] + elif system == "darwin": # macOS + font_candidates = [ + Path("/System/Library/Fonts/PingFang.ttc"), + Path("/System/Library/Fonts/STHeiti.ttc"), + Path("/Library/Fonts/Songti.ttc"), + ] + else: # Linux / 其他 + font_candidates = [ + Path("/usr/share/fonts/truetype/wqy/wqy-microhei.ttc"), + Path("/usr/share/fonts/truetype/wqy/wqy-zenhei.ttc"), + Path("/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf"), + ] + + # --- 逐个尝试加载 --- + for os_path in font_candidates: + os_path_str = str(os_path) + if not os.path.exists(os_path_str): + # 文件确实不存在就跳过 + continue + + # 1) 先把 OS 路径转换成 Panda 内部路径(/d/... 或 /usr/...) + panda_fn = Filename.fromOsSpecific(os_path_str) + panda_fn.makeTrueCase() # Windows 上修正大小写(更稳) + panda_internal = panda_fn.getFullpath() # 取“内部样式”的字符串 + + # 2) 打印便于对照 Panda 的日志 + try: + print(f"[FontLoader] 尝试加载: {panda_internal}") + except Exception: + pass + + try: + # 3) 传“字符串”给 loader.loadFont(关键修复点) + font = self.loader.loadFont(panda_internal) + if font: + self.chinese_font = font + print("✓ 中文字体加载成功:", os_path_str) + break + else: + print("[FontLoader] 返回空字体对象,继续尝试下一个候选...") + except TypeError as te: + # 某些旧版绑定只接受 str,不接受 Filename;此处已传 str,若仍报错再兜底一次 + try: + font = self.loader.loadFont(str(panda_fn)) # 退一步:str(Filename) + if font: + self.chinese_font = font + print("✓ 中文字体加载成功(备用API):", os_path_str) + break + else: + print("[FontLoader] 备用API仍返回空字体对象,继续…") + except Exception as te2: + print(f"[FontLoader] 加载失败(TypeError),继续下一个: {os_path},原因: {te2}") + except Exception as e: + print(f"[FontLoader] 加载失败,继续下一个: {os_path},原因: {e}") + + # --- 兜底 --- if not self.chinese_font: - print("警告: 无法加载中文字体,将使用默认字体") - else: - print("✓ 中文字体加载成功") - except: - print("警告: 无法加载中文字体,将使用默认字体") + print("警告: 无法加载中文字体,将使用默认字体(可能导致中文显示不全)") + self.chinese_font = None + + except Exception as e: + print(f"警告: 加载中文字体时发生错误: {e}") self.chinese_font = None def setQtWidget(self, widget): diff --git a/demo/test_gizmo_drag.py b/demo/test_gizmo_drag.py index 4a1b39d2..9f3bb338 100644 --- a/demo/test_gizmo_drag.py +++ b/demo/test_gizmo_drag.py @@ -11,12 +11,12 @@ warnings.filterwarnings("ignore", category=DeprecationWarning) import sys from PyQt5.QtWidgets import QApplication, QMainWindow, QVBoxLayout, QWidget, QLabel, QPushButton, QHBoxLayout from PyQt5.QtCore import Qt -from QPanda3D.QPanda3DWidget import QPanda3DWidget -from QPanda3D.Panda3DWorld import Panda3DWorld +from QMeta3D.QMeta3DWidget import QMeta3DWidget +from QMeta3D.Meta3DWorld import Meta3DWorld from panda3d.core import * from direct.task import Task -class GizmoDragTestWorld(Panda3DWorld): +class GizmoDragTestWorld(Meta3DWorld): def __init__(self): super().__init__() @@ -529,7 +529,7 @@ class GizmoDragTestWorld(Panda3DWorld): print(f"设置坐标轴颜色失败: {str(e)}") -class CustomGizmoDragTestWidget(QPanda3DWidget): +class CustomGizmoDragTestWidget(QMeta3DWidget): """支持坐标轴拖拽测试的部件""" def __init__(self, world, parent=None): super().__init__(world, parent) diff --git a/demo/test_gui_complete.py b/demo/test_gui_complete.py index 46a73aac..57052c80 100644 --- a/demo/test_gui_complete.py +++ b/demo/test_gui_complete.py @@ -16,15 +16,15 @@ from PyQt5.QtWidgets import (QApplication, QMainWindow, QMenuBar, QMenu, QAction QLabel, QLineEdit, QFormLayout, QDoubleSpinBox, QScrollArea, QTreeView, QInputDialog, QFileDialog, QMessageBox, QDialog, QGroupBox, QHBoxLayout, QPushButton, QDialogButtonBox) from PyQt5.QtCore import Qt, QDir, QUrl from PyQt5.QtGui import QDrag, QPainter, QPixmap -from QPanda3D.QPanda3DWidget import QPanda3DWidget -from QPanda3D.Panda3DWorld import Panda3DWorld +from QMeta3D.QMeta3DWidget import QMeta3DWidget +from QMeta3D.Meta3DWorld import Meta3DWorld from direct.gui.DirectGui import * from direct.gui.OnscreenText import OnscreenText from panda3d.core import * import os # 简化的世界类,专门用于GUI测试 -class GUITestWorld(Panda3DWorld): +class GUITestWorld(Meta3DWorld): def __init__(self): super().__init__() @@ -270,7 +270,7 @@ class GUITestWorld(Panda3DWorld): """GUI输入框提交事件处理""" print(f"✓ GUI输入框提交测试成功: {entry_id} = {text}") -class GUITestWidget(QPanda3DWidget): +class GUITestWidget(QMeta3DWidget): def __init__(self, world, parent=None): super().__init__(world, parent) self.world = world diff --git a/demo/test_qt_debug.py b/demo/test_qt_debug.py index c894c430..db72d695 100644 --- a/demo/test_qt_debug.py +++ b/demo/test_qt_debug.py @@ -12,14 +12,14 @@ warnings.filterwarnings("ignore", category=DeprecationWarning) import sys from PyQt5.QtWidgets import QApplication, QMainWindow, QVBoxLayout, QWidget, QLabel from PyQt5.QtCore import Qt -from QPanda3D.QPanda3DWidget import QPanda3DWidget -from QPanda3D.Panda3DWorld import Panda3DWorld +from QMeta3D.QMeta3DWidget import QMeta3DWidget +from QMeta3D.Meta3DWorld import Meta3DWorld from direct.gui.DirectGui import * from direct.gui.OnscreenText import OnscreenText from panda3d.core import * import os -class DebugGUIWorld(Panda3DWorld): +class DebugGUIWorld(Meta3DWorld): def __init__(self): super().__init__() @@ -186,7 +186,7 @@ class DebugGUIWorld(Panda3DWorld): """按钮点击事件""" print("✓ 按钮被点击了!") -class DebugWidget(QPanda3DWidget): +class DebugWidget(QMeta3DWidget): def __init__(self, world, parent=None): super().__init__(world, parent) self.world = world diff --git a/demo/test_qt_fix.py b/demo/test_qt_fix.py index 38dd949f..7d99fd78 100644 --- a/demo/test_qt_fix.py +++ b/demo/test_qt_fix.py @@ -12,14 +12,14 @@ warnings.filterwarnings("ignore", category=DeprecationWarning) import sys from PyQt5.QtWidgets import QApplication, QMainWindow from PyQt5.QtCore import Qt -from QPanda3D.QPanda3DWidget import QPanda3DWidget -from QPanda3D.Panda3DWorld import Panda3DWorld +from QMeta3D.QMeta3DWidget import QMeta3DWidget +from QMeta3D.Meta3DWorld import Meta3DWorld from direct.gui.DirectGui import * from direct.gui.OnscreenText import OnscreenText from panda3d.core import * import os -class QtGUITestWorld(Panda3DWorld): +class QtGUITestWorld(Meta3DWorld): def __init__(self): super().__init__() @@ -109,7 +109,7 @@ class QtGUITestWorld(Panda3DWorld): """按钮点击事件""" print("✓ 按钮被点击了!") -class QtGUITestWidget(QPanda3DWidget): +class QtGUITestWidget(QMeta3DWidget): def __init__(self, world, parent=None): super().__init__(world, parent) self.world = world diff --git a/demo/test_qt_gui_fix.py b/demo/test_qt_gui_fix.py index 3cdb0f43..8038cd2f 100644 --- a/demo/test_qt_gui_fix.py +++ b/demo/test_qt_gui_fix.py @@ -13,13 +13,13 @@ import sys from PyQt5.QtWidgets import QApplication, QMainWindow, QVBoxLayout, QWidget, QLabel from PyQt5.QtCore import Qt from PyQt5.QtGui import QMouseEvent -from QPanda3D.QPanda3DWidget import QPanda3DWidget -from QPanda3D.Panda3DWorld import Panda3DWorld +from QMeta3D.QMeta3DWidget import QMeta3DWidget +from QMeta3D.Meta3DWorld import Meta3DWorld from direct.gui.DirectGui import * from direct.gui.OnscreenText import OnscreenText from panda3d.core import * -class QtGUIFixWorld(Panda3DWorld): +class QtGUIFixWorld(Meta3DWorld): def __init__(self): super().__init__() @@ -194,7 +194,7 @@ class QtGUIFixWorld(Panda3DWorld): print(f"Qt事件处理失败: {str(e)}") return False -class CustomQPanda3DWidget(QPanda3DWidget): +class CustomQMeta3DWidget(QMeta3DWidget): """自定义QPanda3DWidget,支持事件转发""" def __init__(self, world, parent=None): super().__init__(world, parent) @@ -241,7 +241,7 @@ def main(): world = QtGUIFixWorld() # 创建自定义Panda3D部件 - pandaWidget = CustomQPanda3DWidget(world) + pandaWidget = CustomQMeta3DWidget(world) layout.addWidget(pandaWidget) mainWindow.setCentralWidget(centralWidget) diff --git a/demo/test_qt_only.py b/demo/test_qt_only.py index 9aeeded6..4e521b9a 100644 --- a/demo/test_qt_only.py +++ b/demo/test_qt_only.py @@ -11,13 +11,13 @@ warnings.filterwarnings("ignore", category=DeprecationWarning) import sys from PyQt5.QtWidgets import QApplication, QMainWindow, QVBoxLayout, QWidget, QLabel from PyQt5.QtCore import Qt -from QPanda3D.QPanda3DWidget import QPanda3DWidget -from QPanda3D.Panda3DWorld import Panda3DWorld +from QMeta3D.QMeta3DWidget import QMeta3DWidget +from QMeta3D.Meta3DWorld import Meta3DWorld from direct.gui.DirectGui import * from direct.gui.OnscreenText import OnscreenText from panda3d.core import * -class QtOnlyWorld(Panda3DWorld): +class QtOnlyWorld(Meta3DWorld): def __init__(self): super().__init__() @@ -139,7 +139,7 @@ def main(): world = QtOnlyWorld() # 创建Panda3D部件 - pandaWidget = QPanda3DWidget(world) + pandaWidget = QMeta3DWidget(world) layout.addWidget(pandaWidget) mainWindow.setCentralWidget(centralWidget) diff --git a/demo/test_qt_vs_showbase.py b/demo/test_qt_vs_showbase.py index 644af27d..2c9340d4 100644 --- a/demo/test_qt_vs_showbase.py +++ b/demo/test_qt_vs_showbase.py @@ -12,14 +12,14 @@ warnings.filterwarnings("ignore", category=DeprecationWarning) import sys from PyQt5.QtWidgets import QApplication, QMainWindow, QVBoxLayout, QWidget, QLabel, QPushButton from PyQt5.QtCore import Qt -from QPanda3D.QPanda3DWidget import QPanda3DWidget -from QPanda3D.Panda3DWorld import Panda3DWorld +from QMeta3D.QMeta3DWidget import QMeta3DWidget +from QMeta3D.Meta3DWorld import Meta3DWorld from direct.showbase.ShowBase import ShowBase from direct.gui.DirectGui import * from direct.gui.OnscreenText import OnscreenText from panda3d.core import * -class QtTestWorld(Panda3DWorld): +class QtTestWorld(Meta3DWorld): """Qt集成环境测试""" def __init__(self): super().__init__() @@ -168,7 +168,7 @@ def testQtEnvironment(): world = QtTestWorld() # 创建Panda3D部件 - pandaWidget = QPanda3DWidget(world) + pandaWidget = QMeta3DWidget(world) layout.addWidget(pandaWidget) mainWindow.setCentralWidget(centralWidget) diff --git a/demo/test_simplified_gizmo.py b/demo/test_simplified_gizmo.py index a4da6642..9246ada7 100644 --- a/demo/test_simplified_gizmo.py +++ b/demo/test_simplified_gizmo.py @@ -11,9 +11,9 @@ warnings.filterwarnings("ignore", category=DeprecationWarning) import sys from PyQt5.QtWidgets import QApplication, QMainWindow, QVBoxLayout, QWidget, QLabel, QPushButton, QHBoxLayout from PyQt5.QtCore import Qt -from QPanda3D.QPanda3DWidget import QPanda3DWidget +from QMeta3D.QMeta3DWidget import QMeta3DWidget sys.path.append('..') -from main import MyWorld, CustomPanda3DWidget +from main import MyWorld, CustomMeta3DWidget def main(): print("启动简化坐标轴测试...") @@ -42,7 +42,7 @@ def main(): world = MyWorld() # 创建部件 - pandaWidget = CustomPanda3DWidget(world) + pandaWidget = CustomMeta3DWidget(world) layout.addWidget(pandaWidget) mainWindow.setCentralWidget(centralWidget) diff --git a/demo/test_size_fix.py b/demo/test_size_fix.py index 328d8ed8..6904e5fd 100644 --- a/demo/test_size_fix.py +++ b/demo/test_size_fix.py @@ -11,12 +11,12 @@ warnings.filterwarnings("ignore", category=DeprecationWarning) import sys from PyQt5.QtWidgets import QApplication, QMainWindow, QVBoxLayout, QWidget, QLabel from PyQt5.QtCore import Qt -from QPanda3D.QPanda3DWidget import QPanda3DWidget -from QPanda3D.Panda3DWorld import Panda3DWorld +from QMeta3D.QMeta3DWidget import QMeta3DWidget +from QMeta3D.Meta3DWorld import Meta3DWorld from panda3d.core import * from direct.task import Task -class SizeTestWorld(Panda3DWorld): +class SizeTestWorld(Meta3DWorld): def __init__(self): super().__init__() @@ -93,7 +93,7 @@ class SizeTestWorld(Panda3DWorld): # 每5秒检查一次 return task.again -class CustomSizeTestWidget(QPanda3DWidget): +class CustomSizeTestWidget(QMeta3DWidget): """支持尺寸获取的测试部件""" def __init__(self, world, parent=None): super().__init__(world, parent) diff --git a/demo/video_integration.py b/demo/video_integration.py index 7720f025..1b3fe0ef 100644 --- a/demo/video_integration.py +++ b/demo/video_integration.py @@ -33,8 +33,13 @@ class VideoManager: # 创建视频纹理 if video_path and os.path.exists(video_path): + # Convert Windows path to Panda3D compatible path format + from panda3d.core import Filename + panda_path = Filename.fromOsSpecific(video_path) + converted_path = str(panda_path) + movie_texture = MovieTexture(name) - success = movie_texture.read(video_path) + success = movie_texture.read(converted_path) if success: video_screen.setTexture(movie_texture) @@ -43,7 +48,7 @@ class VideoManager: video_obj = { 'node': video_screen, 'texture': movie_texture, - 'path': video_path, + 'path': converted_path, 'name': name, 'type': 'movie', 'is_playing': False @@ -62,7 +67,7 @@ class VideoManager: print(f"✓ 视频屏幕创建成功: {name}") return video_screen else: - print(f"✗ 无法加载视频: {video_path}") + print(f"✗ 无法加载视频: {converted_path}") # 创建占位符纹理 self._create_placeholder_texture(video_screen, name) else: @@ -103,8 +108,13 @@ class VideoManager: # 创建视频纹理 if video_path and os.path.exists(video_path): + # Convert Windows path to Panda3D compatible path format + from panda3d.core import Filename + panda_path = Filename.fromOsSpecific(video_path) + converted_path = str(panda_path) + movie_texture = MovieTexture(name) - success = movie_texture.read(video_path) + success = movie_texture.read(converted_path) if success: sphere.setTexture(movie_texture) @@ -113,7 +123,7 @@ class VideoManager: video_obj = { 'node': sphere, 'texture': movie_texture, - 'path': video_path, + 'path': converted_path, 'name': name, 'type': 'spherical', 'is_playing': True diff --git a/gui/gui_manager.py b/gui/gui_manager.py index fc26e86c..55644875 100644 --- a/gui/gui_manager.py +++ b/gui/gui_manager.py @@ -791,7 +791,7 @@ class GUIManager: traceback.print_exc() return None - def createGUI3DImage(self, pos=(0, 0, 0), image_path=None, size=1.0): + def createGUI3DImage(self, pos=(0, 0, 0), image_path=None, size=(1.0,1.0,1.0)): """创建3D空间图片""" try: @@ -823,7 +823,7 @@ class GUIManager: # 参数类型检查和转换 if isinstance(size, (list, tuple)): if len(size) >= 2: - x_size, y_size = float(size[0]), float(size[1]) + x_size, y_size = float(size[0]), float(size[2]) else: x_size = y_size = float(size[0]) if size else 1.0 else: @@ -831,7 +831,7 @@ class GUIManager: # 创建卡片 cm = CardMaker('gui_3d_image') - cm.setFrame(-x_size / 2, x_size / 2, -y_size / 2, y_size / 2) + cm.setFrame(-x_size, x_size, -y_size, y_size) # 创建3D图像节点 # image_node = self.world.render.attachNewNode(cm.generate()) @@ -1375,8 +1375,13 @@ class GUIManager: print(f"❌ 视频文件不存在: {video_path}") return False + # Convert Windows path to Panda3D compatible path format + from panda3d.core import Filename + panda_path = Filename.fromOsSpecific(video_path) + converted_path = str(panda_path) + # 加载新的视频纹理 - movie_texture = self._loadMovieTexture(video_path) + movie_texture = self._loadMovieTexture(video_path) # Pass original path for existence check if movie_texture: # 清除现有的纹理 video_screen.clearTexture() @@ -1405,15 +1410,15 @@ class GUIManager: # 保存新的视频纹理引用到PythonTag video_screen.setPythonTag("movie_texture", movie_texture) - video_screen.setTag("video_path", video_path) + video_screen.setTag("video_path", converted_path) # Store converted path # 确保视频屏幕有正确的材质 self._ensureVideoScreenMaterial(video_screen) - print(f"✅ 成功加载新视频: {video_path}") + print(f"✅ 成功加载新视频: {converted_path}") return True else: - print(f"❌ 无法加载视频文件: {video_path}") + print(f"❌ 无法加载视频文件: {converted_path}") return False except Exception as e: @@ -1425,7 +1430,7 @@ class GUIManager: def _loadMovieTexture(self, video_path): """加载视频纹理的兼容方法""" try: - from panda3d.core import Texture, MovieTexture + from panda3d.core import Texture, MovieTexture, Filename import os # 检查文件是否存在 @@ -1433,12 +1438,16 @@ class GUIManager: print(f"❌ 视频文件不存在: {video_path}") return None - print(f"🔍 尝试加载视频文件: {video_path}") + # Convert Windows path to Panda3D compatible path format + panda_path = Filename.fromOsSpecific(video_path) + converted_path = str(panda_path) + + print(f"🔍 尝试加载视频文件: {converted_path}") # 方法1: 尝试使用 MovieTexture(专门用于视频) try: - movie_texture = MovieTexture(video_path) - if movie_texture.read(video_path): + movie_texture = MovieTexture(converted_path) + if movie_texture.read(converted_path): print("✅ 使用 MovieTexture 成功加载视频") self._configureVideoTexture(movie_texture) return movie_texture @@ -1449,7 +1458,7 @@ class GUIManager: # 方法2: 尝试使用 loader.loadTexture try: - movie_texture = self.world.loader.loadTexture(video_path) + movie_texture = self.world.loader.loadTexture(converted_path) if movie_texture and hasattr(movie_texture, 'is_playable') and movie_texture.is_playable(): print("✅ 使用 loader.loadTexture 成功加载视频纹理") self._configureVideoTexture(movie_texture) @@ -1462,7 +1471,7 @@ class GUIManager: # 方法3: 尝试使用 Texture.read(作为最后备选) try: texture = Texture() - if texture.read(video_path): + if texture.read(converted_path): print("✅ 使用 Texture.read 成功加载(可能作为静态纹理)") self._configureVideoTexture(texture) return texture diff --git a/main.py b/main.py index 9e100431..944f8acf 100644 --- a/main.py +++ b/main.py @@ -15,7 +15,7 @@ from PyQt5.QtWidgets import (QApplication, QMainWindow, QMenuBar, QMenu, QAction from PyQt5.QtCore import Qt, QDir, QUrl from PyQt5.QtGui import QDrag, QPainter, QPixmap from PyQt5.QtWidgets import QFileSystemModel -from QPanda3D.QPanda3DWidget import QPanda3DWidget +from QMeta3D.QMeta3DWidget import QMeta3DWidget from panda3d.core import loadPrcFileData loadPrcFileData("", "assertions 0") from core.world import CoreWorld @@ -29,7 +29,7 @@ from gui.gui_manager import GUIManager from core.terrain_manager import TerrainManager from scene.scene_manager import SceneManager from project.project_manager import ProjectManager -from ui.widgets import CustomPanda3DWidget, CustomFileView, CustomTreeWidget +from ui.widgets import CustomMeta3DWidget, CustomFileView, CustomTreeWidget from ui.property_panel import PropertyPanelManager from ui.interface_manager import InterfaceManager from panda3d.core import (CardMaker, Vec4, Vec3, ColorAttrib, MaterialAttrib, diff --git a/project/project_manager.py b/project/project_manager.py index c79b0fe2..090d43b2 100644 --- a/project/project_manager.py +++ b/project/project_manager.py @@ -1,11 +1,3 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -""" -项目管理器 - 负责项目的生命周期管理 -处理项目创建、打开、保存、打包等功能 -""" - import os import sys import json @@ -22,13 +14,9 @@ from PyQt5.QtCore import Qt from ui.widgets import NewProjectDialog, UniversalMessageDialog class ProjectManager: - """项目管理器 - 统一管理项目的生命周期""" - + def __init__(self, world): - """初始化项目管理器 - Args: - world: 主程序world对象引用 - """ + self.world = world self.current_project_path = None self.project_config = None @@ -821,353 +809,6 @@ class ProjectManager: shutil.copy2(template_path, app_path) print(f"✓ 应用程序主文件已从模板创建: {app_path}") - # def _createAppFile(self, build_dir, project_name): -# """创建应用程序主文件""" -# app_code = f'''#!/usr/bin/env python3 -# # -*- coding: utf-8 -*- -# -# """ -# {project_name} - Panda3D应用程序 -# 使用Panda3D引擎编辑器创建 -# """ -# -# from __future__ import print_function -# -# import json -# -# from direct.actor.Actor import Actor -# from panda3d.core import TextNode, CardMaker, TextureStage, NodePath -# #获取渲染管线路径 -# import sys -# import os -# -# render_pipeline_path = 'RenderPipelineFile' -# project_root = os.path.dirname(os.path.abspath(__file__)) -# sys.path.insert(0,project_root) -# sys.path.insert(0,render_pipeline_path) -# -# import math -# from random import random,randint,seed -# from panda3d.core import Vec3,load_prc_file_data,Filename -# from direct.showbase.ShowBase import ShowBase -# -# os.chdir(os.path.dirname(os.path.realpath(__file__))) -# -# class MainApp(ShowBase): -# def __init__(self): -# load_prc_file_data("",""" -# win-size 1200 720 -# window-title Render -# """) -# -# pipeline_path = "../../" -# -# if not os.path.isfile(os.path.join(pipeline_path,"setup.py")): -# pipeline_path = "../../RenderPipeline" -# -# sys.path.insert(0,pipeline_path) -# -# from rpcore import RenderPipeline,SpotLight -# self.render_pipeline = RenderPipeline() -# self.render_pipeline.create(self) -# -# from rpcore.util.movement_controller import MovementController -# -# self.render_pipeline.daytime_mgr.time = "12:00" -# self._loadFont() -# -# self.loadFullScene() -# self.loadGUIFromJSON() -# -# self.controller = MovementController(self) -# self.controller.set_initial_position( -# Vec3(-7.5,-5.3,1.8),Vec3(-5.9,-4.0,1.6)) -# self.controller.setup() -# -# base.accept("l",self.tour) -# -# def _loadFont(self): -# """加载中文字体""" -# self.chinese_font = None -# try: -# self.chinese_font = self.loader.loadFont('/usr/share/fonts/truetype/wqy/wqy-microhei.ttc') -# if not self.chinese_font: -# print("警告: 无法加载中文字体,将使用默认字体") -# else: -# print("✓ 中文字体加载成功") -# except: -# print("警告: 无法加载中文字体,将使用默认字体") -# self.chinese_font = None -# -# def getChineseFont(self): -# """获取中文字体""" -# return self.chinese_font -# -# def loadFullScene(self): -# """加载完整场景,包括所有元素""" -# try: -# scene_file = "scene.bam" -# if os.path.exists(scene_file): -# # 使用readBamFile加载完整场景 -# from panda3d.core import BamCache -# BamCache.getGlobalPtr().setActive(False) # 禁用缓存以避免问题 -# -# scene = self.loader.loadModel(Filename.fromOsSpecific(scene_file)) -# if scene: -# scene.reparentTo(self.render) -# self.render_pipeline.prepare_scene(scene) -# print("✓ 完整场景加载成功") -# -# # 处理场景中的各种元素 -# self.processSceneElements(scene) -# else: -# print("⚠️ 场景文件加载失败") -# else: -# print("⚠️ 未找到场景文件") -# except Exception as e: -# print(f"加载完整场景时出错: {{str(e)}}") -# import traceback -# traceback.print_exc() -# -# def processSceneElements(self, scene): -# """处理场景中的各种元素""" -# try: -# # 处理光源 -# self.processLights(scene) -# -# # 处理GUI元素 -# self.processGUIElements(scene) -# -# except Exception as e: -# print(f"处理场景元素时出错: {{str(e)}}") -# -# def processLights(self, scene): -# """处理场景中的光源""" -# try: -# # 查找并处理点光源 -# point_lights = scene.findAllMatches("**/=element_type=point_light") -# for light_node in point_lights: -# try: -# from RenderPipelineFile.rpcore import PointLight -# light = PointLight() -# -# # 恢复光源属性 -# if light_node.hasTag("light_energy"): -# light.energy = float(light_node.getTag("light_energy")) -# else: -# light.energy = 5000 -# -# light.radius = 1000 -# light.inner_radius = 0.4 -# light.set_color_from_temperature(5 * 1000.0) -# light.casts_shadows = True -# light.shadow_map_resolution = 256 -# -# light.setPos(light_node.getPos()) -# self.render_pipeline.add_light(light) -# print(f"✓ 点光源 {{light_node.getName()}} 恢复成功") -# except Exception as e: -# print(f"恢复点光源 {{light_node.getName()}} 失败: {{str(e)}}") -# -# # 查找并处理聚光灯 -# spot_lights = scene.findAllMatches("**/=element_type=spot_light") -# for light_node in spot_lights: -# try: -# from RenderPipelineFile.rpcore import SpotLight -# light = SpotLight() -# -# # 恢复光源属性 -# if light_node.hasTag("light_energy"): -# light.energy = float(light_node.getTag("light_energy")) -# else: -# light.energy = 5000 -# -# light.radius = 1000 -# light.inner_radius = 0.4 -# light.set_color_from_temperature(5 * 1000.0) -# light.casts_shadows = True -# light.shadow_map_resolution = 256 -# -# light.setPos(light_node.getPos()) -# self.render_pipeline.add_light(light) -# print(f"✓ 聚光灯 {{light_node.getName()}} 恢复成功") -# except Exception as e: -# print(f"恢复聚光灯 {{light_node.getName()}} 失败: {{str(e)}}") -# -# except Exception as e: -# print(f"处理光源时出错: {{str(e)}}") -# -# def processGUIElements(self, scene): -# """处理场景中的GUI元素""" -# try: -# # 查找并处理2D图像 -# images_2d = scene.findAllMatches("**/=gui_type=image_2d") -# for img_node in images_2d: -# try: -# # GUI元素通常在场景加载时自动处理 -# print(f"✓ 2D图像 {{img_node.getName()}} 已加载") -# except Exception as e: -# print(f"处理2D图像 {{img_node.getName()}} 失败: {{str(e)}}") -# -# except Exception as e: -# print(f"处理GUI元素时出错: {{str(e)}}") -# -# def tour(self): -# mopath = ( -# (Vec3(-10.8645000458, 9.76458263397, 2.13306283951), Vec3(-133.556228638, -4.23447799683, 0.0)), -# (Vec3(-10.6538448334, -5.98406457901, 1.68028640747), Vec3(-59.3999938965, -3.32706642151, 0.0)), -# (Vec3(9.58458328247, -5.63625621796, 2.63269257545), Vec3(58.7906494141, -9.40668964386, 0.0)), -# (Vec3(6.8135137558, 11.0153560638, 2.25509500504), Vec3(148.762527466, -6.41223621368, 0.0)), -# (Vec3(-9.07093334198, 3.65908527374, 1.42396306992), Vec3(245.362503052, -3.59927511215, 0.0)), -# (Vec3(-8.75390911102, -3.82727789879, 0.990055501461), Vec3(296.090484619, -0.604830980301, 0.0)), -# ) -# self.controller.play_motion_path(mopath,3.0) -# -# def loadGUIFromJSON(self): -# gui_json_path = "gui/gui_elements.json" -# -# try: -# if os.path.exists(gui_json_path): -# with open(gui_json_path, "r", encoding="utf-8") as f: -# content = f.read().strip() -# if content: -# gui_data = json.loads(content) -# self.createGUIElement(gui_data) -# except Exception as e: -# print(f"加载GUI元素失败: {{str(e)}}") -# import traceback -# traceback.print_exc() -# -# def createGUIElement(self,element_data): -# try: -# processed_names = set() -# element_original_data={{}} -# for i, gui_info in enumerate(element_data): -# name = gui_info.get("name", f"gui_element_{{i}}") -# element_original_data[name] = { -# "scale": gui_info.get("scale", [1, 1, 1]), -# "position": gui_info.get("position", [0, 0, 0]), -# "parent_name": gui_info.get("parent_name") -# } -# valid_parents = set() -# for gui_info in element_data: -# name = gui_info.get("name", f"gui_element_{{gui_info.get('index', 0)}}") -# valid_parents.add(name) -# -# for i ,gui_info in enumerate(element_data): -# try: -# gui_type = gui_info.get("type","unknown") -# name = gui_info.get("name",f"gui_element_{{i}}") -# position = gui_info.get("position",[0,0,0]) -# scale = gui_info.get("scale",[1,1,1]) -# tags = gui_info.get("tags",{{}}) -# text = gui_info.get("text","") -# image_path = gui_info.get("image_path","") -# video_path = gui_info.get("video_path","") -# bg_image_path = gui_info.get("bg_image_path","") -# parent_name = gui_info.get("parent_name") -# -# if name in processed_names: -# continue -# -# processed_names.add(name) -# -# absolute_position = list(position) -# absolute_scale = list(scale) -# -# if parent_name and parent_name in element_original_data: -# parent_data = element_original_data[parent_name] -# parent_scale = parent_data["scale"] -# -# if gui_type in ["3d_text", "3d_image", "button", "label", "entry", "2d_image", -# "2d_video_screen"]: -# # 位置需要乘以父级缩放来得到绝对位置 -# for j in range(min(len(absolute_position), len(parent_scale))): -# absolute_position[j] *= parent_scale[j] if len(parent_scale) > j else parent_scale[0] -# -# # 缩放需要乘以父级缩放来得到绝对缩放 -# for j in range(min(len(absolute_scale), len(parent_scale))): -# absolute_scale[j] *= parent_scale[j] if len(parent_scale) > j else parent_scale[0] -# -# new_element = None -# -# if gui_type =="3d_text": -# size = absolute_scale[0] if absolute_scale and len(absolute_scale) > 0 else 0.5 -# new_element = self.createGUI3DText( -# pos = tuple(absolute_position), -# text = text, -# size = size -# ) -# elif gui_type == "button": -# # 确保传入正确的参数类型 -# new_element = self.createGUIButton( -# pos=tuple(absolute_position), -# text=text, -# size=absolute_scale[0] if absolute_scale and len(absolute_scale) > 0 else 1.0, -# ) -# except Exception as e: -# print(f"重建GUI元素失败 {{name}}: {{e}}") -# import traceback -# traceback.print_exc() -# continue -# except Exception as e: -# print(f"重建GUI元素失败: {{str(e)}}") -# -# def createGUIButton(self, pos=(0, 0, 0), text="按钮", size=0.1,command=None): -# from direct.gui.DirectGui import DirectButton -# -# button = DirectButton( -# text=text, -# pos=(pos[0], pos[1], pos[2]), # 保持正确的坐标格式 -# scale=size, # size 应该是数值而不是元组 -# frameColor=(0.2, 0.6, 0.8, 1), -# text_font=self.getChineseFont() if self.getChineseFont() else None, -# rolloverSound=None, -# clickSound=None, -# parent=None, -# command=command -# ) -# -# def createGUI3DText(self, pos=(0, 0, 0), text="3D文本", size=0.5): -# """创建3D文本GUI元素""" -# try: -# # 创建文本节点 -# text_node = TextNode("gui_3d_text") -# text_node.setText(text) -# text_node.setAlign(TextNode.ACenter) -# -# # 设置字体(如果可用) -# if self.getChineseFont(): -# text_node.setFont(self.getChineseFont()) -# -# # 创建节点路径并添加到场景 -# text_np = self.render.attachNewNode(text_node) -# -# # 设置位置和大小 -# text_np.setPos(Vec3(pos[0], pos[1], pos[2])) -# text_np.setScale(size) -# -# # 设置面向摄像机 -# #text_np.setBillboardPointEye() -# -# # 设置渲染属性 -# text_np.setBin("fixed", 40) -# text_np.setDepthWrite(False) -# -# return text_np -# except Exception as e: -# print(f"❌ 创建3D文本失败: {{str(e)}}") -# import traceback -# traceback.print_exc() -# return None -# -# MainApp().run() -# ''' -# -# app_path = os.path.join(build_dir, "main.py") -# with open(app_path, "w", encoding="utf-8") as f: -# f.write(app_code) - def _createStandardSetupFile(self, build_dir, project_name): """创建优化的标准setup.py文件""" setup_code = f'''#!/usr/bin/env python3 diff --git a/run_complete_analysis.py b/run_complete_analysis.py new file mode 100644 index 00000000..2be62ed0 --- /dev/null +++ b/run_complete_analysis.py @@ -0,0 +1,354 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +完整的开源率分析脚本 +该脚本将执行完整的测试流程并生成准确报告 +""" + +import json +import os +import subprocess +import sys + +def run_command(command, ignore_failure=False): + """运行命令并返回结果""" + try: + print(f"执行命令: {command}") + result = subprocess.run(command, shell=True, capture_output=True, text=True) + if result.returncode != 0 and not ignore_failure: + print(f"命令执行失败: {result.stderr}") + return False + print("命令执行成功") + return True + except Exception as e: + print(f"执行命令时出错: {e}") + return False + +def load_cloc_data(path="cloc.json"): + """加载并解析cloc统计数据""" + if not os.path.exists(path): + print(f"❌ 未找到 {path} 文件") + return None + with open(path, "r", encoding="utf-8") as f: + return json.load(f) + +def load_scancode_data(path="summary.json"): + """加载并解析ScanCode统计数据""" + if not os.path.exists(path): + print(f"❌ 未找到 {path} 文件") + return None + with open(path, "r", encoding="utf-8") as f: + return json.load(f) + +def get_licensed_files_details(scancode_data): + """获取含许可证文件的详细信息""" + if not scancode_data: + return [] + + files = scancode_data.get("files", []) + licensed_files = [] + + # 定义需要排除的目录和文件模式 + exclude_patterns = [ + "/venv/", + "/.git/", + "/__pycache__/", + "/.idea/", + "/.vscode/", + "/build/", + "/dist/", + ".egg-info", + "/Resources/", + "/icons/", + "/tex/", + "cloc.json", + "detailed_cloc.txt", + "summary.json", + "完整开源率分析报告.txt", + "run_complete_analysis.py", + ] + + for file in files: + # 获取文件路径 + file_path = file.get("path", "") + + # 只处理类型为"file"的条目 + if file.get("type") != "file": + continue + + # 检查是否应该排除该文件 + should_exclude = False + for pattern in exclude_patterns: + if pattern in file_path: + should_exclude = True + break + + # 如果应该排除,则跳过该文件 + if should_exclude: + continue + + # 检查是否有许可证信息 + if file.get("detected_license_expression") or file.get("license_detections"): + licensed_files.append({ + "path": file_path, + "license": file.get("detected_license_expression", "Unknown"), + "detections": file.get("license_detections", []) + }) + + return licensed_files + +def get_file_code_lines(): + """从detailed_cloc.txt获取文件代码行数""" + file_code_lines = {} + + if not os.path.exists("detailed_cloc.txt"): + print("❌ 未找到 detailed_cloc.txt 文件") + return file_code_lines + + with open("detailed_cloc.txt", "r") as f: + cloc_lines = f.readlines() + + # 解析cloc输出,创建文件路径到代码行数的映射 + for line in cloc_lines[3:]: # 跳过标题行 + parts = line.strip().split() + if len(parts) >= 4: + try: + file_path = parts[0] + code_lines = int(parts[-1]) + # 标准化路径格式 + if file_path.startswith('./'): + file_path = file_path[2:] + file_code_lines[file_path] = code_lines + except ValueError: + continue + + return file_code_lines + +def calculate_accurate_open_source_lines(licensed_files_details, file_code_lines): + """计算准确的开源代码行数""" + total_licensed_code_lines = 0 + found_files = 0 + detailed_files = [] + + for file_info in licensed_files_details: + file_path = file_info["path"] + + # 将文件路径标准化 + normalized_path = file_path + if normalized_path.startswith('EG/'): + normalized_path = normalized_path[3:] # 去掉开头的EG/ + + if normalized_path in file_code_lines: + code_lines = file_code_lines[normalized_path] + total_licensed_code_lines += code_lines + found_files += 1 + detailed_files.append({ + "path": file_path, + "code_lines": code_lines, + "license": file_info["license"] + }) + else: + # 尝试其他可能的路径格式 + alt_path1 = './' + normalized_path + alt_path2 = 'EG/' + normalized_path + if alt_path1 in file_code_lines: + code_lines = file_code_lines[alt_path1] + total_licensed_code_lines += code_lines + found_files += 1 + detailed_files.append({ + "path": file_path, + "code_lines": code_lines, + "license": file_info["license"] + }) + elif alt_path2 in file_code_lines: + code_lines = file_code_lines[alt_path2] + total_licensed_code_lines += code_lines + found_files += 1 + detailed_files.append({ + "path": file_path, + "code_lines": code_lines, + "license": file_info["license"] + }) + + return total_licensed_code_lines, detailed_files + +def generate_detailed_report(): + """生成详细报告""" + # 加载数据 + cloc_data = load_cloc_data("cloc.json") + scancode_data = load_scancode_data("summary.json") + file_code_lines = get_file_code_lines() + + if not cloc_data or not scancode_data: + print("无法加载必要数据文件") + return False + + # 获取含许可证文件详情 + licensed_files_details = get_licensed_files_details(scancode_data) + + # 计算准确的开源代码行数 + accurate_open_source_lines, detailed_files = calculate_accurate_open_source_lines( + licensed_files_details, file_code_lines) + + # 获取统计数据 + total_code_lines = cloc_data.get("SUM", {}).get("code", 0) + total_files = 1075 # 根据脚本分析得出的实际文件数 + licensed_files = len(licensed_files_details) + + # 计算开源率 + open_source_rate = (accurate_open_source_lines / total_code_lines) * 100 if total_code_lines > 0 else 0 + + # 创建报告内容 + report_content = [] + report_content.append("项目开源率分析完整报告") + report_content.append("=" * 50) + report_content.append("") + + report_content.append("1. 报告概览") + report_content.append("-" * 20) + report_content.append(f"项目总文件数: {total_files}") + report_content.append(f"含许可证文件数: {licensed_files}") + report_content.append(f"项目总代码行数: {total_code_lines}") + report_content.append(f"准确开源代码行数: {accurate_open_source_lines}") + report_content.append(f"代码开源率: {open_source_rate:.2f}%") + report_content.append("") + + report_content.append("2. 各语言代码行数分布(包含文件路径)") + report_content.append("-" * 40) + + # 按语言分组显示文件 + lang_files = {} + with open("detailed_cloc.txt", "r") as f: + cloc_lines = f.readlines() + + for line in cloc_lines[3:]: # 跳过标题行 + parts = line.strip().split() + if len(parts) >= 4: + try: + file_path = parts[0] + # 从文件路径推断语言(简化处理) + if file_path.endswith('.py'): + lang = 'Python' + elif file_path.endswith('.js'): + lang = 'JavaScript' + elif file_path.endswith('.cpp') or file_path.endswith('.cc'): + lang = 'C++' + elif file_path.endswith('.h'): + lang = 'C/C++ Header' + elif file_path.endswith('.glsl'): + lang = 'GLSL' + elif file_path.endswith('.qml'): + lang = 'QML' + elif file_path.endswith('.xml'): + lang = 'XML' + elif file_path.endswith('.json'): + lang = 'JSON' + elif file_path.endswith('.md'): + lang = 'Markdown' + elif file_path.endswith('.html'): + lang = 'HTML' + elif file_path.endswith('.css'): + lang = 'CSS' + elif file_path.endswith('.sh'): + lang = 'Shell' + elif file_path.endswith('.yml') or file_path.endswith('.yaml'): + lang = 'YAML' + else: + lang = 'Other' + + if lang not in lang_files: + lang_files[lang] = [] + lang_files[lang].append((file_path, int(parts[-1]))) + except ValueError: + continue + + for lang, files in lang_files.items(): + report_content.append(f"\n{lang}语言文件:") + report_content.append(f" 文件总数: {len(files)}") + total_lines = sum([f[1] for f in files]) + report_content.append(f" 代码行数: {total_lines}") + report_content.append(" 文件列表:") + for file_path, code_lines in files[:10]: # 只显示前10个文件 + report_content.append(f" {file_path}: {code_lines} 行") + if len(files) > 10: + report_content.append(f" ... 还有 {len(files) - 10} 个文件") + + report_content.append("") + report_content.append("3. 含许可证的开源文件详情") + report_content.append("-" * 30) + + # 按许可证类型分组显示文件 + files_by_license = {} + for file_info in detailed_files: + license_type = file_info.get("license", "Unknown") + if license_type not in files_by_license: + files_by_license[license_type] = [] + files_by_license[license_type].append(file_info) + + for license_type, files in files_by_license.items(): + report_content.append(f"\n许可证类型: {license_type}") + report_content.append(f" 文件数量: {len(files)}") + total_lines = sum([f["code_lines"] for f in files]) + report_content.append(f" 代码行数: {total_lines}") + report_content.append(" 文件列表:") + for file_info in files: + report_content.append(f" {file_info['path']}: {file_info['code_lines']} 行") + + # 保存报告 + with open("完整开源率分析报告.txt", "w", encoding="utf-8") as f: + f.write("\n".join(report_content)) + + print("完整报告已生成:完整开源率分析报告.txt") + return True + +def main(): + """主函数""" + print("开始执行完整的开源率分析流程...") + + # 步骤1: 执行cloc统计代码行数 + print("\n步骤1: 执行cloc统计代码行数") + cloc_cmd = "cloc --json --fullpath --not-match-d='(venv|\\.git|__pycache__|\\.idea|\\.vscode|build|dist|.*\\.egg-info|Resources/animations|Resources/materials|Resources/models|Resources/textures|icons|tex)' --not-match-f='(cloc.json|detailed_cloc.txt|summary.json|完整开源率分析报告.txt|run_complete_analysis.py)' . > cloc.json" + if not run_command(cloc_cmd): + print("❌ cloc统计失败") + return False + + # 步骤2: 生成详细文件列表 + print("\n步骤2: 生成详细文件列表") + detailed_cloc_cmd = "cloc --by-file --fullpath --not-match-d='(venv|\\.git|__pycache__|\\.idea|\\.vscode|build|dist|.*\\.egg-info|Resources/animations|Resources/materials|Resources/models|Resources/textures|icons|tex)' --not-match-f='(cloc.json|detailed_cloc.txt|summary.json|完整开源率分析报告.txt|run_complete_analysis.py)' . | grep -v \"^\\s*$\" | grep -E \"(\\.py|\\.js|\\.cpp|\\.h|\\.glsl|\\.qml|\\.xml|\\.html|\\.css|\\.java|\\.cs|\\.php)\" > detailed_cloc.txt" + if not run_command(detailed_cloc_cmd): + print("❌ 生成详细文件列表失败") + return False + + # 步骤3: 执行ScanCode扫描许可证 + print("\n步骤3: 执行ScanCode扫描许可证") + scancode_cmd = "scancode --license --classify --summary --json-pp summary.json . --ignore \"venv\" --ignore \".git\" --ignore \"__pycache__\" --ignore \".idea\" --ignore \".vscode\" --ignore \"build\" --ignore \"dist\" --ignore \"*.egg-info\" --ignore \"Resources\" --ignore \"icons\" --ignore \"tex\" --ignore \"cloc.json\" --ignore \"detailed_cloc.txt\" --ignore \"完整开源率分析报告.txt\" --ignore \"run_complete_analysis.py\"" + # 忽略失败,因为ScanCode会尝试扫描自己生成的summary.json文件导致"失败" + run_command(scancode_cmd, ignore_failure=True) + + # 检查summary.json是否生成 + if not os.path.exists("summary.json"): + print("❌ ScanCode未生成summary.json文件") + return False + + # 步骤4: 生成详细报告 + print("\n步骤4: 生成详细报告") + if not generate_detailed_report(): + print("❌ 生成报告失败") + return False + + print("\n✅ 完整分析流程执行完成!") + print("生成的文件:") + print(" - cloc.json: 代码行数统计") + print(" - detailed_cloc.txt: 详细文件列表") + print(" - summary.json: 许可证扫描结果") + print(" - 完整开源率分析报告.txt: 最终报告") + return True + +if __name__ == "__main__": + success = main() + if success: + print("\n🎉 所有步骤执行成功!") + sys.exit(0) + else: + print("\n❌ 执行过程中出现错误!") + sys.exit(1) \ No newline at end of file diff --git a/scene/scene_manager.py b/scene/scene_manager.py index ebac4a6b..2d184891 100644 --- a/scene/scene_manager.py +++ b/scene/scene_manager.py @@ -1,4010 +1,4478 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -""" -场景管理器 - 负责场景和模型管理的核心功能 -处理模型导入、场景树构建、材质系统、碰撞设置等 -""" - -import os -import shutil -import time - -from PyQt5.QtCore import Qt -from PyQt5.QtWidgets import QTreeWidgetItem -from panda3d.core import ( - ModelPool, ModelRoot, Filename, NodePath, GeomNode, Material, Vec4, Vec3, - MaterialAttrib, ColorAttrib, Point3, CollisionNode, CollisionSphere, - BitMask32, TransparencyAttrib, LColor, TransformState, RenderModeAttrib -) -import json -import aiohttp -import asyncio -import inspect -from pathlib import Path -from panda3d.egg import EggData, EggVertexPool -from direct.actor.Actor import Actor -from QPanda3D.Panda3DWorld import get_render_pipeline -from RenderPipelineFile.rpplugins.smaa.jitters import halton_seq -from scene import util - -class CesiumIntegration: - def __init__(self, scene_manager): - self.scene_manager = scene_manager - self.world = scene_manager.world - self.tilesets = {} - - def add_tileset(self,name,url,position=(0,0,0)): - try: - tileset_node = self.scene_manager.load_cesium_tileset(url,position) - - if tileset_node: - self.tilesets[name] = { - 'node':tileset_node, - 'url':url, - 'position':position - } - print(f"✓ 添加 Cesium tileset: {name}") - return tileset_node - else: - print(f"✗ 添加 Cesium tileset 失败: {name}") - return None - except Exception as e: - print(f"✗ 添加 Cesium tileset 出错: {e}") - return None - - def remove_tileset(self, name): - """移除 tileset""" - if name in self.tilesets: - tileset_info = self.tilesets[name] - tileset_info['node'].removeNode() - del self.tilesets[name] - print(f"✓ 移除 Cesium tileset: {name}") - return True - return False - - def get_tileset(self, name): - """获取 tileset""" - return self.tilesets.get(name, None) - - def list_tilesets(self): - """列出所有 tilesets""" - return list(self.tilesets.keys()) - -class SceneManager: - """场景管理器 - 统一管理场景中的所有元素""" - - def __init__(self, world): - """初始化场景管理器 - - Args: - world: 主程序world对象引用 - """ - self.world = world - self.models = [] # 模型列表 - - self.Spotlight = [] - self.Pointlight = [] - - self.tilesets = [] #来存储tilesets - self.cesium_integration = CesiumIntegration(self) - - print("✓ 场景管理系统初始化完成") - - # ==================== 模型导入和处理 ==================== - - def importModel(self, filepath, apply_unit_conversion=False, normalize_scales=True, auto_convert_to_glb=True): - try: - if not os.path.exists(filepath): - print("文件不存在") - return None - - filepath = util.normalize_model_path(filepath) - original_filepath = filepath - - # # 在加载前设置忽略未知属性 - # from panda3d.core import ConfigVariableBool - # ConfigVariableBool("model-cache-ignore-unknown-properties").setValue(True) - # - # # 清除可能存在的模型缓存 - # from panda3d.core import ModelPool - # ModelPool.releaseAllModels() - # - # # 检查是否需要转换为GLB以获得更好的动画支持 - # if auto_convert_to_glb and self._shouldConvertToGLB(filepath): - # print(f"🔄 检测到需要转换的格式,尝试转换为GLB...") - # converted_path = self._convertToGLBWithProgress(filepath) - # if converted_path: - # print(f"✅ 转换成功: {converted_path}") - # filepath = converted_path - # # 转换成功的消息已在控制台显示,不再弹窗提示 - # else: - # print(f"⚠️ 转换失败,使用原始文件") - - model = self.world.loader.loadModel(filepath) - if not model: - print("加载模型失败") - return None - - # 设置模型名称 - model_name = os.path.basename(filepath) - # 确保名称有效 - if not model_name: - model_name = "imported_model" - model.setName(model_name) - # 将模型添加到场景 - model.reparentTo(self.world.render) - - # 设置模型名称 - model_name = os.path.basename(filepath) - model.setName(model_name) - - - # 保存原始路径和转换后的路径 - model.setTag("model_path", filepath) - model.setTag("original_path", original_filepath) - if filepath != original_filepath: - model.setTag("converted_from", os.path.splitext(original_filepath)[1]) - model.setTag("converted_to_glb", "true") - - #特殊处理FBX模型 - if filepath.lower().endswith('.fbx'): - print("检测到FBX模型,应用特殊处理...") - - # 将模型缩放设置为原来的1/100 - model.setScale(0.01) - print("设置模型缩放为 0.01 (原始大小的1/100)") - - # 设置模型旋转为 (0, 90, 0) - model.setHpr(0, 90, 0) - print("设置模型旋转为 (0, 90, 0)") - - # 调整模型位置到地面 - model.setPos(0,0,0) - #self._adjustModelToGround(model) - - # 创建并设置基础材质 - print("\n=== 开始设置材质 ===") - #self._applyMaterialsToModel(model) - - # 设置碰撞检测(重要!用于选择功能) - print("\n=== 设置碰撞检测 ===") - self.setupCollision(model) - - # 添加文件标签用于保存/加载 - model.setTag("file", model_name) - model.setTag("is_model_root", "1") - model.setTag("is_scene_element", "1") - model.setTag("tree_item_type", "IMPORTED_MODEL_NODE") - - # 记录应用的处理选项 - if apply_unit_conversion: - model.setTag("unit_conversion_applied", "true") - if normalize_scales: - model.setTag("scale_normalization_applied", "true") - - # 添加到模型列表 - self.models.append(model) - - # 更新场景树 - # 获取树形控件并添加到Qt树中 - tree_widget = self._get_tree_widget() - if tree_widget: - # 找到根节点项 - root_item = None - for i in range(tree_widget.topLevelItemCount()): - item = tree_widget.topLevelItem(i) - if item.text(0) == "render" or item.data(0, Qt.UserRole) == self.world.render: - root_item = item - break - - if root_item: - qt_item = tree_widget.add_node_to_tree_widget(model, root_item, "IMPORTED_MODEL_NODE") - if qt_item: - #tree_widget.setCurrentItem(qt_item) - # 更新选择和属性面板 - #tree_widget.update_selection_and_properties(model, qt_item) - print("✅ Qt树节点添加成功") - else: - print("⚠️ Qt树节点添加失败,但Panda3D对象已创建") - else: - print("⚠️ 未找到根节点项,无法添加到Qt树") - #self.updateSceneTree() - - print(f"=== 模型导入成功: {model_name} ===\n") - return model - - except Exception as e: - print(f"导入模型失败: {str(e)}") - return None - - def _fixModelStructure(self, model): - """修复模型结构""" - try: - # 使用正确的方式查找动画相关节点 - character_nodes = model.findAllMatches("**/+Character") - anim_bundle_nodes = model.findAllMatches("**/+AnimBundleNode") - - if character_nodes.getNumPaths() > 0 or anim_bundle_nodes.getNumPaths() > 0: - print(f"检测到模型{model.getName()}包含角色相节点:") - if character_nodes.getNumPaths() > 0: - print(f"CharacterNode数量:{character_nodes.getNumPaths()}") - if anim_bundle_nodes.getNumPaths() > 0: - print(f"AnimBundleNode数量: {anim_bundle_nodes.getNumPaths()}") - - model.setTag("fixed_structure", "true") - return True - except Exception as e: - print(f"修复模型结构时出错: {e}") - return False - - def _validateAndFixAllTransforms(self, model): - """递归验证并修复模型中所有节点的变换矩阵""" - try: - fixed_count = 0 - - # 先处理根节点 - if not self._validateAndFixTransform(model): - fixed_count += 1 - - # 递归处理所有子节点 - def process_children(node, depth=0): - nonlocal fixed_count - for i in range(node.getNumChildren()): - try: - child = node.getChild(i) - if not self._validateAndFixTransform(child): - fixed_count += 1 - # 递归处理孙节点 - process_children(child, depth + 1) - except Exception as e: - print(f"处理子节点时出错 (深度 {depth}): {e}") - continue - - process_children(model) - - if fixed_count > 0: - print(f"共修复了 {fixed_count} 个节点的变换") - - return True - except Exception as e: - print(f"验证所有变换时出错: {e}") - return False - - def _validateAndFixTransform(self, node_path): - """验证并修复单个节点的变换矩阵""" - try: - node_name = node_path.getName() - - # 获取当前变换状态 - original_pos = node_path.getPos() - original_hpr = node_path.getHpr() - original_scale = node_path.getScale() - - # 检查位置是否包含无效值 - if not original_pos.isFinite(): - print(f"警告: 节点 {node_name} 位置包含无效值 {original_pos},重置为 (0,0,0)") - node_path.setPos(0, 0, 0) - return False - - # 检查旋转是否包含无效值 - if not original_hpr.isFinite(): - print(f"警告: 节点 {node_name} 旋转包含无效值 {original_hpr},重置为 (0,0,0)") - node_path.setHpr(0, 0, 0) - return False - - # 检查缩放是否包含无效值或为零 - if not original_scale.isFinite(): - print(f"警告: 节点 {node_name} 缩放包含无效值 {original_scale},重置为 (1,1,1)") - node_path.setScale(1, 1, 1) - return False - - # 检查缩放是否为零或接近零 - min_scale = 1e-10 - if (abs(original_scale.x) < min_scale or - abs(original_scale.y) < min_scale or - abs(original_scale.z) < min_scale): - print(f"警告: 节点 {node_name} 缩放接近零 {original_scale},重置为 (1,1,1)") - node_path.setScale(1, 1, 1) - return False - - # 检查缩放是否过大(防止异常大的缩放) - max_scale = 1000000 # 100万倍作为上限 - if (abs(original_scale.x) > max_scale or - abs(original_scale.y) > max_scale or - abs(original_scale.z) > max_scale): - print(f"警告: 节点 {node_name} 缩放过异常 {original_scale},重置为 (1,1,1)") - node_path.setScale(1, 1, 1) - return False - - return True - - except Exception as e: - print(f"验证/修复节点 {node_path.getName()} 变换时出错: {e}") - # 只在出现严重错误时才重置变换 - try: - node_path.setPos(0, 0, 0) - node_path.setHpr(0, 0, 0) - node_path.setScale(1, 1, 1) - except: - pass - return False - - def _applyModelScale(self, model, scale_factor): - """应用模型特定缩放 - - Args: - model: 要缩放的模型 - scale_factor: 缩放因子 - """ - try: - print(f"应用模型缩放因子: {scale_factor}") - - # 获取当前边界用于后续位置调整 - original_bounds = model.getBounds() - - # 应用缩放 - model.setScale(scale_factor) - - # 重新调整位置(因为缩放会影响边界) - if original_bounds and not original_bounds.isEmpty(): - new_bounds = model.getBounds() - min_point = new_bounds.getMin() - ground_offset = -min_point.getZ() - model.setZ(ground_offset) - print(f"缩放后重新调整位置: Z偏移 = {ground_offset}") - - print(f"模型缩放完成,缩放因子: {scale_factor}") - - except Exception as e: - print(f"应用模型缩放失败: {str(e)}") - - def _applyMaterialsToModel(self, model): - """递归应用材质到模型的所有GeomNode""" - - def apply_material(node_path, depth=0): - indent = " " * depth - try: - #print(f"{indent}处理节点: {node_path.getName()}") - #print(f"{indent}节点类型: {node_path.node().__class__.__name__}") - - if isinstance(node_path.node(), GeomNode): - #print(f"{indent}发现GeomNode,处理材质") - geom_node = node_path.node() - - # 检查所有几何体的状态 - has_color = False - color = None - - # 首先检查节点自身的状态 - node_state = node_path.getState() - if node_state.hasAttrib(MaterialAttrib.getClassType()): - mat_attrib = node_state.getAttrib(MaterialAttrib.getClassType()) - node_material = mat_attrib.getMaterial() - if node_material: - if node_material.hasBaseColor(): - color = node_material.getBaseColor() - has_color = True - #print(f"{indent}从节点材质获取基础颜色: {color}") - elif node_material.hasDiffuse(): - color = node_material.getDiffuse() - has_color = True - #print(f"{indent}从节点材质获取漫反射颜色: {color}") - - # 检查几何体材质 - if not has_color: - for i in range(geom_node.getNumGeoms()): - try: - geom = geom_node.getGeom(i) - state = geom_node.getGeomState(i) - - # 检查材质属性 - if state.hasAttrib(MaterialAttrib.getClassType()): - mat_attrib = state.getAttrib(MaterialAttrib.getClassType()) - orig_material = mat_attrib.getMaterial() - if orig_material: - if orig_material.hasBaseColor(): - color = orig_material.getBaseColor() - has_color = True - #print(f"{indent}从几何体材质获取基础颜色: {color}") - break - elif orig_material.hasDiffuse(): - color = orig_material.getDiffuse() - has_color = True - #print(f"{indent}从几何体材质获取漫反射颜色: {color}") - break - - # 检查颜色属性 - if not has_color and state.hasAttrib(ColorAttrib.getClassType()): - color_attrib = state.getAttrib(ColorAttrib.getClassType()) - if not color_attrib.isOff(): - color = color_attrib.getColor() - has_color = True - #print(f"{indent}从颜色属性获取: {color}") - break - except Exception as geom_error: - print(f"{indent}处理几何体 {i} 时出错: {geom_error}") - continue - - # 创建新材质 - material = Material() - if has_color and color: - #print(f"{indent}应用找到的颜色: {color}") - try: - # 确保颜色值有效 - if (color.getX() == color.getX() and color.getY() == color.getY() and - color.getZ() == color.getZ() and color.getW() == color.getW()): - material.setBaseColor(color) - material.setDiffuse(color) - node_path.setColor(color) - else: - print(f"{indent}⚠️ 颜色值无效,使用默认颜色") - material.setBaseColor((0.8, 0.8, 0.8, 1.0)) - material.setDiffuse((0.8, 0.8, 0.8, 1.0)) - except Exception as color_error: - print(f"{indent}设置颜色时出错: {color_error}") - material.setBaseColor((0.8, 0.8, 0.8, 1.0)) - material.setDiffuse((0.8, 0.8, 0.8, 1.0)) - else: - print(f"{indent}使用默认颜色") - material.setBaseColor((0.8, 0.8, 0.8, 1.0)) - material.setDiffuse((0.8, 0.8, 0.8, 1.0)) - - # 设置其他材质属性 - material.setAmbient((0.2, 0.2, 0.2, 1.0)) - material.setSpecular((0.5, 0.5, 0.5, 1.0)) - material.setShininess(32.0) - - # 应用材质 - try: - node_path.setMaterial(material, 1) # 1表示强制应用 - #print(f"{indent}材质应用成功") - except Exception as mat_error: - print(f"{indent}⚠️ 应用材质时出错: {mat_error}") - - #print(f"{indent}几何体数量: {geom_node.getNumGeoms()}") - - except Exception as node_error: - print(f"{indent}处理节点 {node_path.getName()} 时出错: {node_error}") - - # 递归处理子节点 - child_count = node_path.getNumChildren() - #print(f"{indent}子节点数量: {child_count}") - for i in range(child_count): - try: - child = node_path.getChild(i) - apply_material(child, depth + 1) - except Exception as child_error: - print(f"{indent}处理子节点 {i} 时出错: {child_error}") - continue - - # 应用材质 - #print("\n开始递归应用材质...") - try: - apply_material(model) - except Exception as e: - print(f"应用材质时出错: {e}") - print("=== 材质设置完成 ===\n") - - def _adjustModelToGround(self, model): - """智能调整模型到地面,但保持原有缩放结构""" - try: - #print("调整模型位置到地面...") - - # 获取模型的边界框 - bounds = model.getBounds() - if not bounds or bounds.isEmpty(): - print("无法获取模型边界,使用默认位置") - model.setPos(0, 0, 0) - return - - # 获取边界框的最低点 - min_point = bounds.getMin() - center = bounds.getCenter() - - # 计算需要移动的距离,使模型底部贴合地面(Z=0) - # 这里不涉及缩放,只是简单的位置调整 - ground_offset = -min_point.getZ() - - # 设置模型位置:X,Y居中,Z调整到地面 - model.setPos(0, 0, ground_offset) - - #print(f"模型边界: 最小点{min_point}, 中心{center}") - #print(f"地面偏移: {ground_offset}") - #print(f"最终位置: {model.getPos()}") - - except Exception as e: - print(f"调整模型位置失败: {str(e)}") - # 失败时使用默认位置 - model.setPos(0, 0, 0) - - def _normalizeModelScales(self, model): - """智能标准化模型缩放层级 - - 检测并修复FBX模型中子节点的大缩放值问题 - """ - try: - print("开始分析模型缩放结构...") - - # 收集所有节点的缩放信息 - scale_info = [] - self._collectScaleInfo(model, scale_info) - - if not scale_info: - print("没有找到需要处理的缩放信息") - return - - # 分析缩放模式 - large_scales = [info for info in scale_info if max(abs(info['scale'].x), abs(info['scale'].y), abs(info['scale'].z)) > 10] - - if not large_scales: - print("没有发现大缩放值,无需标准化") - return - - print(f"发现 {len(large_scales)} 个节点有大缩放值") - - # 计算标准化因子(基于最常见的大缩放值) - common_large_scale = self._findCommonLargeScale(large_scales) - if common_large_scale: - normalize_factor = 1.0 / common_large_scale - print(f"检测到常见大缩放值: {common_large_scale}, 标准化因子: {normalize_factor}") - - # 应用标准化 - self._applyScaleNormalization(model, normalize_factor) - print("✓ 缩放标准化完成") - else: - print("无法确定合适的标准化因子,跳过标准化") - - except Exception as e: - print(f"缩放标准化失败: {str(e)}") - - def _collectScaleInfo(self, node, scale_info, depth=0): - """递归收集节点缩放信息""" - try: - scale = node.getScale() - scale_info.append({ - 'node': node, - 'name': node.getName(), - 'scale': scale, - 'depth': depth - }) - - # 递归处理子节点 - for i in range(node.getNumChildren()): - child = node.getChild(i) - self._collectScaleInfo(child, scale_info, depth + 1) - - except Exception as e: - print(f"收集缩放信息失败 ({node.getName()}): {str(e)}") - - def _findCommonLargeScale(self, large_scales): - """找到最常见的大缩放值""" - try: - # 提取缩放值(取绝对值的最大分量) - scale_values = [] - for info in large_scales: - scale = info['scale'] - max_scale = max(abs(scale.x), abs(scale.y), abs(scale.z)) - scale_values.append(round(max_scale)) # 四舍五入到整数 - - if not scale_values: - return None - - # 找到最常见的值 - from collections import Counter - counter = Counter(scale_values) - most_common = counter.most_common(1)[0] - - print(f"缩放值统计: {dict(counter)}") - print(f"最常见的大缩放值: {most_common[0]} (出现{most_common[1]}次)") - - # 只有当最常见的值确实很大时才返回 - if most_common[0] >= 10: - return float(most_common[0]) - - return None - - except Exception as e: - print(f"分析常见缩放值失败: {str(e)}") - return None - - def _applyScaleNormalization(self, node, normalize_factor, depth=0): - """ - 安全地应用缩放标准化 - """ - try: - indent = " " * depth - current_scale = node.getScale() - current_pos = node.getPos() - - # 检查是否需要标准化(只处理明显的大缩放) - max_scale_component = max(abs(current_scale.x), abs(current_scale.y), abs(current_scale.z)) - - if max_scale_component > 10: # 只标准化明显的大缩放 - # 确保标准化因子有效 - if normalize_factor <= 0 or normalize_factor > 1000: - print(f"{indent}无效的标准化因子: {normalize_factor},跳过") - return - - # 应用新的缩放 - new_scale = current_scale * normalize_factor - - # 检查新缩放是否有效 - if any(s <= 0 for s in [new_scale.x, new_scale.y, new_scale.z]): - print(f"{indent}标准化后产生无效缩放,跳过") - return - - node.setScale(new_scale) - - # 同时调整位置:当缩放变小时,位置也应该相应变小以保持视觉相对位置 - # 这确保了子节点之间的相对距离在视觉上保持一致 - new_pos = current_pos * normalize_factor - node.setPos(new_pos) - - print(f"{indent}标准化 {node.getName()}:") - print(f"{indent} 缩放: {current_scale} -> {new_scale}") - print(f"{indent} 位置: {current_pos} -> {new_pos}") - - except Exception as e: - print(f"应用缩放标准化失败 ({node.getName()}): {str(e)}") - - def importModelAsync(self, filepath): - """异步导入模型""" - try: - # 创建异步加载请求 - request = self.world.loader.makeAsyncRequest(filepath) - - # 添加完成回调 - def modelLoaded(task): - if task.isReady(): - model = task.result() - if model: - # 处理加载完成的模型 - self.processLoadedModel(model) - return task.done() - - request.done_event = modelLoaded - - # 开始异步加载 - self.world.loader.loadAsync(request) - - except Exception as e: - print(f"异步加载模型失败: {str(e)}") - - # ==================== 材质和几何体处理 ==================== - - def processMaterials(self, model): - """处理模型材质""" - if isinstance(model.node(), GeomNode): - # 创建基础材质 - material = Material() - material.setAmbient((0.2, 0.2, 0.2, 1.0)) - material.setDiffuse((0.8, 0.8, 0.8, 1.0)) - material.setSpecular((0.5, 0.5, 0.5, 1.0)) - material.setShininess(32.0) - - # 检查FBX材质 - state = model.node().getGeomState(0) - if state.hasAttrib(MaterialAttrib.getClassType()): - fbx_material = state.getAttrib(MaterialAttrib.getClassType()).getMaterial() - if fbx_material: - # 复制FBX材质属性 - material.setAmbient(fbx_material.getAmbient()) - material.setDiffuse(fbx_material.getDiffuse()) - material.setSpecular(fbx_material.getSpecular()) - material.setShininess(fbx_material.getShininess()) - - # 应用材质 - model.setMaterial(material) - - def processModelGeometry(self, model): - """处理模型几何体""" - # 创建EggData对象 - egg_data = EggData() - - # 处理顶点数据 - vertex_pool = EggVertexPool("vpool") - egg_data.addChild(vertex_pool) - - # 处理几何体 - if isinstance(model.node(), GeomNode): - for i in range(model.node().getNumGeoms()): - geom = model.node().getGeom(i) - # 处理几何体数据 - # ... - - # ==================== 碰撞系统 ==================== - - def setupCollision(self, model): - """为模型设置碰撞检测(增强版本)""" - try: - - # 创建碰撞节点 - cNode = CollisionNode(f'modelCollision_{model.getName()}') - - # 设置碰撞掩码 - cNode.setIntoCollideMask(BitMask32.bit(2)) # 用于鼠标选择 - - # 如果启用了模型间碰撞检测,添加额外的掩码 - if (hasattr(self.world, 'collision_manager') and - self.world.collision_manager.model_collision_enabled): - # 同时设置模型间碰撞掩码 - current_mask = cNode.getIntoCollideMask() - model_collision_mask = BitMask32.bit(6) # MODEL_COLLISION - cNode.setIntoCollideMask(current_mask | model_collision_mask) - print(f"为 {model.getName()} 启用模型间碰撞检测") - - # 获取模型的边界 - bounds = model.getBounds() - if bounds.isEmpty(): - print(f"⚠️ 模型 {model.getName()} 边界为空,使用默认碰撞体") - # 使用默认的小球体 - cSphere = CollisionSphere(Point3(0, 0, 0), 1.0) - else: - center = bounds.getCenter() - radius = bounds.getRadius() - - # 确保半径不为零 - if radius <= 0: - radius = 1.0 - print(f"⚠️ 模型 {model.getName()} 半径为零,使用默认半径 1.0") - # - # # 添加碰撞球体 - # cSphere = CollisionSphere(center, radius) - cSphere = self.world.collision_manager.createCollisionShape(model, 'polygon') - - cNode.addSolid(cSphere) - - # 将碰撞节点附加到模型上 - cNodePath = model.attachNewNode(cNode) - - # 根据调试设置决定是否显示碰撞体 - if hasattr(self.world, 'debug_collision') and self.world.debug_collision: - cNodePath.hide() - else: - cNodePath.hide() - - # 为模型添加碰撞相关标签 - model.setTag("has_collision", "true") - model.setTag("collision_radius", str(radius if 'radius' in locals() else 1.0)) - - print(f"✅ 为模型 {model.getName()} 设置碰撞检测完成") - - return cNodePath - - except Exception as e: - print(f"❌ 为模型 {model.getName()} 设置碰撞检测失败: {str(e)}") - import traceback - traceback.print_exc() - return None - - # ==================== 场景树管理 ==================== - - def updateSceneTree(self): - """更新场景树显示 - 代理到interface_manager""" - if hasattr(self.world, 'interface_manager'): - return self.world.interface_manager.updateSceneTree() - else: - print("界面管理器未初始化,无法更新场景树") - - # ==================== 场景保存和加载 ==================== - - def _collectGUIElementInfo(self, gui_node): - """收集GUI元素的信息用于保存""" - try: - # 获取GUI元素类型 - gui_type = "unknown" - if hasattr(gui_node, 'hasTag') and gui_node.hasTag("gui_type"): - gui_type = gui_node.getTag("gui_type") - elif hasattr(gui_node, 'hasTag') and gui_node.hasTag("saved_gui_type"): - gui_type = gui_node.getTag("saved_gui_type") - else: - # 尝试从节点名称推断类型 - name_lower = gui_node.getName().lower() - if "button" in name_lower: - gui_type = "button" - elif "label" in name_lower: - gui_type = "label" - elif "entry" in name_lower: - gui_type = "entry" - elif "image" in name_lower: - gui_type = "2d_image" - elif "videoscreen" in name_lower: - if "2d" in name_lower: - gui_type = "2d_video_screen" - else: - gui_type = "video_screen" - elif "info_panel" in name_lower: - if "3d" in name_lower: - gui_type = "info_panel_3d" - else: - gui_type = "info_panel" - else: - # 如果无法识别类型,跳过该元素 - print(f"跳过无法识别类型的GUI元素: {gui_node.getName()}") - return None - - gui_info = { - "name": gui_node.getName(), - "type": gui_type, - "position": list(gui_node.getPos()), - "rotation": list(gui_node.getHpr()), - "scale": list(gui_node.getScale()), - "tags": {}, - "parent_name":None, - "video_path":gui_node.getTag("video_path") if gui_node.hasTag("video_path") else None, - "panel_id":gui_node.getTag("panel_id") if gui_node.hasTag("panel_id") else None, - } - - parent = gui_node.getParent() - if parent and not parent.isEmpty(): - parent_name = parent.getName() - if parent_name not in ["render","aspect2d","render2d"]: - gui_info["parent_name"] = parent_name - - # 收集所有标签(仅对NodePath类型的对象) - if hasattr(gui_node, 'getTagNames'): - for tag in gui_node.getTagNames(): - gui_info["tags"][tag] = gui_node.getTag(tag) - elif hasattr(gui_node, 'getTags'): # 对于DirectGUI对象 - # DirectGUI对象使用不同的方法存储标签 - if hasattr(gui_node, '_tags'): - gui_info["tags"] = gui_node._tags.copy() - - # 根据类型收集特定信息 - if gui_type == "button": - if hasattr(gui_node, 'get'): # DirectButton - gui_info["text"] = gui_node.get() - elif hasattr(gui_node, 'getText'): # 其他类型 - gui_info["text"] = gui_node.getText() - elif hasattr(gui_node, 'hasTag') and gui_node.hasTag("gui_text"): - gui_info["text"] = gui_node.getTag("gui_text") - elif gui_type == "label": - if hasattr(gui_node, 'getText'): - gui_info["text"] = gui_node.getText() - elif hasattr(gui_node, 'hasTag') and gui_node.hasTag("gui_text"): - gui_info["text"] = gui_node.getTag("gui_text") - elif gui_type == "entry": - if hasattr(gui_node, 'get'): - gui_info["text"] = gui_node.get() - elif hasattr(gui_node, 'hasTag') and gui_node.hasTag("gui_text"): - gui_info["text"] = gui_node.getTag("gui_text") - elif gui_type == "2d_image": - if hasattr(gui_node, 'hasTag') and gui_node.hasTag("image_path"): - gui_info["image_path"] = gui_node.getTag("image_path") - elif hasattr(gui_node, 'hasTag') and gui_node.hasTag("gui_image_path"): - gui_info["image_path"] = gui_node.getTag("gui_image_path") - elif gui_type == "3d_text": - if hasattr(gui_node,'hasTag') and gui_node.hasTag("gui_text"): - gui_info["text"] = gui_node.getTag("gui_text") - elif hasattr(gui_node,'node') and hasattr(gui_node.node(),'getText'): - gui_info["text"] = gui_node.node().getText() - elif gui_type == "3d_image": - if hasattr(gui_node,'hasTag') and gui_node.hasTag("gui_image_path"): - gui_info["image_path"] = gui_node.getTag("gui_image_path") - elif gui_type == "video_screen": - if hasattr(gui_node, 'hasTag') and gui_node.hasTag("video_path"): - gui_info["video_path"] = gui_node.getTag("video_path") - elif gui_type == "2d_video_screen": - if hasattr(gui_node, 'hasTag') and gui_node.hasTag("video_path"): - gui_info["video_path"] = gui_node.getTag("video_path") - elif gui_type == "virtual_screen": - if hasattr(gui_node, 'hasTag') and gui_node.hasTag("gui_text"): - gui_info["text"] = gui_node.getTag("gui_text") - elif gui_type in ["info_panel", "info_panel_3d"]: - # 收集信息面板的特定信息 - if hasattr(gui_node, 'hasTag') and gui_node.hasTag("panel_id"): - gui_info["panel_id"] = gui_node.getTag("panel_id") - - # 收集背景图片信息 - if hasattr(gui_node, 'hasTag') and gui_node.hasTag("image_path"): - gui_info["image_path"] = gui_node.getTag("image_path") - - - # 收集挂载的脚本信息 - if hasattr(self.world, 'script_manager') and self.world.script_manager: - try: - script_manager = self.world.script_manager - scripts = script_manager.get_scripts_on_object(gui_node) # 修复:使用 gui_node 而不是 node - if scripts: - gui_info["scripts"] = [] - for script_component in scripts: - try: - script_name = script_component.script_name - # 获取脚本路径 - script_class = script_component.script_instance.__class__ - script_file = self._get_script_file_path(script_class, script_name) - # 只有当脚本文件存在时才保存 - if script_file and os.path.exists(script_file): - gui_info["scripts"].append({ - "name": script_name, - "file": script_file - }) - print(f"收集脚本信息: {script_name} from {script_file}") - else: - print(f"警告: 脚本文件不存在: {script_file}") - except Exception as e: - print(f"收集单个脚本信息失败 {script_name}, 错误: {e}") - continue - except Exception as e: - print(f"收集脚本信息失败: {e}") - - print(f"成功收集GUI元素信息: {gui_info}") - return gui_info - except Exception as e: - print(f"收集GUI元素信息失败: {e}") - import traceback - traceback.print_exc() - return None - - def _get_script_file_path(self, script_class, script_name): - """ - 获取脚本文件路径的可靠方法 - """ - script_file = "" - - # 方法1: 使用 inspect.getfile - try: - script_file = inspect.getfile(script_class) - if script_file and os.path.exists(script_file): - return script_file - except: - pass - - # 方法2: 使用 __file__ 属性 - try: - if hasattr(script_class, '__file__') and script_class.__file__: - script_file = script_class.__file__ - if script_file and os.path.exists(script_file): - return script_file - except: - pass - - # 方法3: 使用模块的 __file__ 属性 - try: - module = inspect.getmodule(script_class) - if module and hasattr(module, '__file__') and module.__file__: - script_file = module.__file__ - if script_file and os.path.exists(script_file): - return script_file - except: - pass - - # 方法4: 从脚本管理器中查找 - try: - if hasattr(self.world, 'script_manager') and self.world.script_manager: - script_manager = self.world.script_manager - # 查找脚本类对应的文件路径 - for file_path, file_mtime in script_manager.loader.file_mtimes.items(): - # 检查文件名是否匹配脚本名 - file_name = os.path.splitext(os.path.basename(file_path))[0] - if file_name == script_name: - if os.path.exists(file_path): - return file_path - except: - pass - - # 方法5: 在脚本目录中查找 - try: - if hasattr(self.world, 'script_manager') and self.world.script_manager: - script_manager = self.world.script_manager - scripts_dir = script_manager.scripts_directory - - # 查找匹配的脚本文件 - if os.path.exists(scripts_dir): - for file_name in os.listdir(scripts_dir): - if file_name.endswith('.py'): - base_name = os.path.splitext(file_name)[0] - if base_name == script_name: - full_path = os.path.join(scripts_dir, file_name) - if os.path.exists(full_path): - return full_path - except: - pass - - print(f"警告: 无法获取脚本 {script_name} 的文件路径") - return script_file - - def saveScene(self, filename,project_path): - """保存场景到BAM文件 - 完整版,支持GUI元素,地形""" - try: - print(f"\n=== 开始保存场景到: {filename} ===") - - # 确保文件路径是规范化的 - filename = os.path.normpath(filename) - - # 确保目录存在 - directory = os.path.dirname(filename) - if directory and not os.path.exists(directory): - os.makedirs(directory) - - resources_dir = os.path.join(directory,"resources") - if not os.path.exists(resources_dir): - os.makedirs(resources_dir) - - # 存储需要临时隐藏的节点,以便保存后恢复 - nodes_to_restore = [] - - # 查找并隐藏所有坐标轴和选择框节点 - gizmo_nodes = self.world.render.findAllMatches("**/gizmo*") - selection_box_nodes = self.world.render.findAllMatches("**/selectionBox*") - - # 隐藏坐标轴节点 - for node in gizmo_nodes: - if not node.isHidden(): - nodes_to_restore.append((node, True)) # (节点, 原先是否可见) - node.hide() - print(f"临时隐藏坐标轴节点: {node.getName()}") - - # 隐藏选择框节点 - for node in selection_box_nodes: - if not node.isHidden(): - nodes_to_restore.append((node, True)) - node.hide() - print(f"临时隐藏选择框节点: {node.getName()}") - - # 收集所有需要保存的节点 - all_nodes = [] - all_nodes.extend(self.models) - all_nodes.extend(self.Spotlight) - all_nodes.extend(self.Pointlight) - - # 添加GUI元素节点 - gui_elements = [] - if hasattr(self.world, 'gui_elements'): - # 过滤掉空的或重复的GUI元素 - unique_gui_elements = [] - seen_names = set() - - for elem in self.world.gui_elements: - if elem and not elem.isEmpty(): - if not elem.isEmpty() and elem.getName() not in seen_names: - unique_gui_elements.append(elem) - seen_names.add(elem.getName()) - gui_elements = unique_gui_elements - - print(f"保存时GUI元素列表=>>>>>>>>>>>>{self.world.gui_elements}") - all_nodes.extend(gui_elements) - - # 创建用于保存GUI信息的JSON文件路径 - gui_info_file = filename.replace('.bam', '_gui.json') - - print(self.world.gui_elements) - # 收集GUI元素信息(排除3D文本和3D图像) - gui_data = [] - copied_resources = {} - for gui_node in gui_elements: - gui_info = self._collectGUIElementInfo(gui_node) - if gui_info: - gui_type = gui_info.get("type","") - #处理2d图片 - if gui_type =="2d_image" and "image_path" in gui_info: - original_path = gui_info["image_path"] - if original_path and os.path.exists(original_path): - resource_name = os.path.basename(original_path) - new_path = os.path.join(resources_dir,resource_name) - if original_path not in copied_resources: - try: - shutil.copy2(original_path,new_path) - copied_resources[original_path] = new_path - print(f"复制图片资源: {original_path} -> {new_path}") - except Exception as e: - print(f"复制图片资源失败: {original_path}, 错误: {e}") - gui_info["image_path"] = new_path - - # 处理3D图片 - elif gui_type == "3d_image" and "image_path" in gui_info: - original_path = gui_info["image_path"] - # 确保original_path是有效字符串且文件存在 - if original_path and isinstance(original_path, str) and os.path.exists(original_path): - resource_name = os.path.basename(original_path) - new_path = os.path.join(resources_dir, resource_name) - if original_path not in copied_resources: - try: - shutil.copy2(original_path, new_path) - copied_resources[original_path] = new_path - print(f"复制3D图片资源: {original_path} -> {new_path}") - except Exception as e: - print(f"复制3D图片资源失败: {original_path}, 错误: {e}") - gui_info["image_path"] = new_path - - # 处理背景图片 - if "bg_image_path" in gui_info and gui_info["bg_image_path"]: - original_path = gui_info["bg_image_path"] - # 确保original_path是有效字符串且文件存在 - if original_path and isinstance(original_path, str) and os.path.exists(original_path): - resource_name = os.path.basename(original_path) - new_path = os.path.join(resources_dir, resource_name) - if original_path not in copied_resources: - try: - shutil.copy2(original_path, new_path) - copied_resources[original_path] = new_path - print(f"复制背景图片资源: {original_path} -> {new_path}") - except Exception as e: - print(f"复制背景图片资源失败: {original_path}, 错误: {e}") - gui_info["bg_image_path"] = new_path - - # 处理视频资源 - if gui_type in ["video_screen", "2d_video_screen"] and "video_path" in gui_info: - original_path = gui_info["video_path"] - # 确保original_path是有效字符串且文件存在 - if original_path and isinstance(original_path, str) and os.path.exists(original_path): - resource_name = os.path.basename(original_path) - new_path = os.path.join(resources_dir, resource_name) - if original_path not in copied_resources: - try: - shutil.copy2(original_path, new_path) - copied_resources[original_path] = new_path - print(f"复制视频资源: {original_path} -> {new_path}") - except Exception as e: - print(f"复制视频资源失败: {original_path}, 错误: {e}") - gui_info["video_path"] = new_path - - gui_data.append(gui_info) - print(f"添加GUI信息: {gui_info['name']}") - - # 保存GUI信息到JSON文件(确保即使没有GUI元素也创建有效的空JSON数组) - try: - import json - with open(gui_info_file, 'w', encoding='utf-8') as f: - json.dump(gui_data, f, ensure_ascii=False, indent=2) - print(f"✓ GUI信息已保存到: {gui_info_file}") - except Exception as e: - print(f"✗ 保存GUI信息失败: {e}") - import traceback - traceback.print_exc() - - # 保存所有节点的信息 - for node in all_nodes: - if node.isEmpty(): - continue - - # 保存变换信息 - node.setTag("transform_pos", str(node.getPos())) - node.setTag("transform_hpr", str(node.getHpr())) - node.setTag("transform_scale", str(node.getScale())) - print(f"保存节点 {node.getName()} 的变换信息") - - # 保存父子关系信息 - 关键修改 - parent = node.getParent() - if parent and not parent.isEmpty() and parent != self.world.render: - # 只有当父节点不是根节点且父节点是场景中的模型时才保存父子关系 - if parent.getName() not in ["render", "aspect2d", "render2d"]: - # 检查父节点是否也是场景中的模型 - is_parent_model = False - for model in self.models: - if model == parent: - is_parent_model = True - break - - if is_parent_model: - node.setTag("parent_name", parent.getName()) - print(f"保存节点 {node.getName()} 的父节点信息: {parent.getName()}") - - # 获取当前状态 - state = node.getState() - - # 如果有材质属性,保存为标签 - if state.hasAttrib(MaterialAttrib.getClassType()): - mat_attrib = state.getAttrib(MaterialAttrib.getClassType()) - material = mat_attrib.getMaterial() - if material: - # 保存材质属性到标签 - node.setTag("material_ambient", str(material.getAmbient())) - node.setTag("material_diffuse", str(material.getDiffuse())) - node.setTag("material_specular", str(material.getSpecular())) - node.setTag("material_emission", str(material.getEmission())) - node.setTag("material_shininess", str(material.getShininess())) - if material.hasBaseColor(): - node.setTag("material_basecolor", str(material.getBaseColor())) - - # 保存特定类型节点的额外信息 - if node.hasTag("light_type"): - # 保存光源特定信息 - light_obj = node.getPythonTag("rp_light_object") - if light_obj: - node.setTag("light_energy", str(light_obj.energy)) - node.setTag("light_radius", str(getattr(light_obj, 'radius', 0))) - if hasattr(light_obj, 'fov'): - node.setTag("light_fov", str(light_obj.fov)) - elif node.hasTag("element_type"): - element_type = node.getTag("element_type") - if element_type == "cesium_tileset": - # 保存tileset特定信息 - if node.hasTag("tileset_url"): - node.setTag("saved_tileset_url", node.getTag("tileset_url")) - elif node.hasTag("gui_type") or node.hasTag("is_gui_element"): - # 保存GUI元素特定信息 - gui_type = node.getTag("gui_type") if node.hasTag("gui_type") else \ - node.getTag("saved_gui_type") if node.hasTag("saved_gui_type") else "unknown" - node.setTag("saved_gui_type", gui_type) - - # 保存GUI元素的通用属性 - if hasattr(node, 'getPythonTag'): - # 保存任何Python标签数据 - for tag_name in node.getPythonTagKeys(): - try: - tag_value = node.getPythonTag(tag_name) - node.setTag(f"python_tag_{tag_name}", str(tag_value)) - except: - pass - elif node.hasTag("element_type") and node.getTag("element_type") == "info_panel": - # 保存信息面板特定信息 - print(f"保存信息面板信息: {node.getName()}") - panel_id = node.getTag("panel_id") if node.hasTag("panel_id") else node.getName() - if hasattr(self.world, 'info_panel_manager'): - panel_data = self.world.info_panel_manager.serializePanelData(panel_id) - if panel_data: - import json - node.setTag("info_panel_data", json.dumps(panel_data, ensure_ascii=False)) - - if hasattr(self.world,'script_manager') and self.world.script_manager: - script_manager = self.world.script_manager - scripts = script_manager.get_scripts_on_object(node) - if scripts: - node.setTag("has_scripts", "true") - script_info_list = [] - for script_component in scripts: - script_name = script_component.script_name - print(f"保存脚本信息: {script_name}") - - # 获取脚本类的文件路径 - script_class = script_component.script_instance.__class__ - script_file = self._get_script_file_path(script_class, script_name) - - script_info_list.append({ - "name": script_name, - "file": script_file - }) - - # 将脚本信息保存为JSON字符串 - import json - node.setTag("scripts_info", json.dumps(script_info_list, ensure_ascii=False)) - print(f"为节点 {node.getName()} 保存了 {len(script_info_list)} 个脚本") - - try: - print("--- 打印当前场景图 (render) ---") - self.world.render.ls() - print("---------------------------------") - - self.take_screenshot(project_path) - # 保存场景 - success = self.world.render.writeBamFile(Filename.fromOsSpecific(filename)) - - if success: - print(f"✓ 场景保存成功: {filename}") - else: - print("✗ 场景保存失败") - - return success - - finally: - # 恢复之前隐藏的节点 - for item in nodes_to_restore: - node, was_visible = item - if was_visible and not node.isEmpty(): - node.show() - print(f"恢复显示节点: {node.getName()}") - - if nodes_to_restore: - print(f"已恢复 {len(nodes_to_restore)} 个辅助节点的显示") - - except Exception as e: - print(f"保存场景时发生错误: {str(e)}") - import traceback - traceback.print_exc() - return False - - def take_screenshot(self, projectpath): - """ - 截图并保存到指定的完整路径 - - Args: - full_path (str): 完整的文件保存路径,包括文件名和扩展名 - - Returns: - bool: 截图是否成功 - """ - try: - from panda3d.core import Filename - import os - - print(f"\n=== 截图保存: {projectpath} ===") - - # 确保目录存在 - directory = os.path.dirname(projectpath) - if directory and not os.path.exists(directory): - os.makedirs(directory) - print(f"创建目录: {directory}") - - # 规范化路径 - filename = os.path.basename(os.path.normpath(projectpath)) - filename = f'{filename}.png' - print(f'project_path: {projectpath}') - print(f'project_name: {filename}') - full_path = os.path.normpath(os.path.join(projectpath, filename)) - p3d_filename = Filename.from_os_specific(full_path) - # 使用 Panda3D 的截图功能 - success = self.world.win.saveScreenshot(p3d_filename) - - if success: - print(f"✅ 成功截图并保存到: {full_path}") - return True - else: - print(f"❌ 截图保存失败: {full_path}") - return False - - except Exception as e: - print(f"保存截图时发生错误: {str(e)}") - import traceback - traceback.print_exc() - return False - - def loadScene(self, filename): - """从BAM文件加载场景""" - try: - print(f"\n=== 开始加载场景: {filename} ===") - - # 确保文件路径是规范化的 - filename = os.path.normpath(filename) - - # 检查文件是否存在 - if not os.path.exists(filename): - print(f"场景文件不存在: {filename}") - return False - - tree_widget = self._get_tree_widget() - # 清除当前场景 - print("\n清除当前场景...") - for model in self.models: - tree_widget.delete_item(model) - - # 清除灯光 - for light_node in self.Spotlight: - tree_widget.delete_item(light_node) - - for light_node in self.Pointlight: - tree_widget.delete_item(light_node) - - for terrain in self.world.terrain_manager.terrains: - tree_widget.delete_item(terrain) - - # 清除tilesets - for tileset_info in self.tilesets: - tree_widget.delete_item(tileset_info['node']) - - for light in self.Spotlight: - if not light.isEmpty(): - light.removeNode() - self.Spotlight.clear() - - for light in self.Pointlight: - if not light.isEmpty(): - light.removeNode() - self.Pointlight.clear() - - # 清理tilesets - for tileset_info in self.tilesets: - if tileset_info['node'] and not tileset_info['node'].isEmpty(): - tileset_info['node'].removeNode() - self.tilesets.clear() - - # 清理Cesium tilesets - for tileset_name, tileset_info in list(self.cesium_integration.tilesets.items()): - if tileset_info['node'] and not tileset_info['node'].isEmpty(): - tileset_info['node'].removeNode() - self.cesium_integration.tilesets.clear() - - for gui in self.world.gui_elements: - if not gui.isEmpty(): - gui.removeNode() - self.world.gui_elements.clear() - - if hasattr(self.world,'info_panel_manager'): - self.world.info_panel_manager.removeAllPanels() - - # 清理可能存在的辅助节点 - self._cleanupAuxiliaryNodes() - - # 加载场景 - scene = self.world.loader.loadModel(Filename.fromOsSpecific(filename)) - if not scene: - print("场景加载失败") - return False - - tree_widget.create_model_items(scene) - # 遍历场景中的所有模型节点 - # 用于存储处理后的灯光节点,避免重复处理 - processed_lights = [] - # 用于存储处理后的GUI元素,避免重复处理 - - #存储所有加载的节点,用于后续处理父子关系 - loaded_nodes = {} #name->nodePath映射 - - # 遍历场景中的所有节点 - def processNode(nodePath, depth=0): - indent = " " * depth - print(f"{indent}处理节点: {nodePath.getName()} (类型: {type(nodePath.node()).__name__})") - - #存储节点以便后续处理父子关系 - loaded_nodes[nodePath.getName()] = nodePath - - if nodePath.getName().startswith('ground'): - print(f"{indent}跳过ground节点: {nodePath.getName()}") - return - - # 跳过render节点的递归 - if nodePath.getName() == "render" and depth > 0: - print(f"{indent}跳过重复的render节点") - return - - # 跳过光源节点 - if nodePath.getName() in ["alight", "dlight"]: - print(f"{indent}跳过光源节点: {nodePath.getName()}") - return - - # 跳过相机节点 - if nodePath.getName() in ["camera", "cam"]: - print(f"{indent}跳过相机节点: {nodePath.getName()}") - return - - # 跳过辅助节点 - if nodePath.getName().startswith(("gizmo", "selectionBox")): - print(f"{indent}跳过辅助节点: {nodePath.getName()}") - return - - if nodePath.getName() in ['SceneRoot'] or \ - any(keyword in nodePath.getName() for keyword in ["Skybox", "skybox"]): - print(f"{indent}跳过环境节点:{nodePath.getName()}") - return - - # 检查是否是用户创建的场景元素 - is_scene_element = ( - nodePath.hasTag("is_scene_element") or - nodePath.hasTag("is_model_root") or - nodePath.hasTag("light_type") or - nodePath.hasTag("gui_type") or # 检查gui_type标签 - nodePath.hasTag("is_gui_element") or - nodePath.hasTag("saved_gui_type") or - (nodePath.hasTag("element_type") and nodePath.getTag("element_type") == "info_panel") - ) - - # 特殊处理:检查节点名称是否包含GUI相关关键词 - is_potential_gui = any(keyword in nodePath.getName().lower() for keyword in - ["gui", "button", "label", "entry", "image", "video", "screen", "text"]) - - if is_scene_element or is_potential_gui: - print(f"{indent}找到场景元素节点: {nodePath.getName()}") - - # 如果是潜在的GUI元素但没有标签,添加基本标签 - if is_potential_gui and not (nodePath.hasTag("gui_type") or nodePath.hasTag("is_gui_element")): - print(f"{indent}为潜在GUI元素添加标签: {nodePath.getName()}") - nodePath.setTag("is_gui_element", "1") - nodePath.setTag("is_scene_element", "1") - # 尝试从名称推断类型 - name_lower = nodePath.getName().lower() - if "button" in name_lower: - nodePath.setTag("gui_type", "button") - elif "label" in name_lower: - nodePath.setTag("gui_type", "label") - elif "entry" in name_lower: - nodePath.setTag("gui_type", "entry") - elif "image" in name_lower: - nodePath.setTag("gui_type", "image") - elif "video" in name_lower or "screen" in name_lower: - nodePath.setTag("gui_type", "video_screen") - else: - nodePath.setTag("gui_type", "unknown") - - # 清除现有材质状态 - nodePath.clearMaterial() - nodePath.clearColor() - - # 恢复变换信息 - def parseVec3(vec_str): - """解析向量字符串为Vec3""" - try: - vec_str = vec_str.replace('LVecBase3f', '').replace('LPoint3f', '').strip('()') - x, y, z = map(float, vec_str.split(',')) - return Vec3(x, y, z) - except Exception as e: - print(f"解析向量失败: {vec_str}, 错误: {e}") - return Vec3(0, 0, 0) - - if nodePath.hasTag("transform_pos"): - pos = parseVec3(nodePath.getTag("transform_pos")) - nodePath.setPos(pos) - print(f"{indent}恢复位置: {pos}") - - if nodePath.hasTag("transform_hpr"): - hpr = parseVec3(nodePath.getTag("transform_hpr")) - nodePath.setHpr(hpr) - print(f"{indent}恢复旋转: {hpr}") - - if nodePath.hasTag("transform_scale"): - scale = parseVec3(nodePath.getTag("transform_scale")) - nodePath.setScale(scale) - print(f"{indent}恢复缩放: {scale}") - - nodePath.show() - - if nodePath.hasTag("has_scripts") and nodePath.getTag("has_scripts") == "true": - if hasattr(self.world,'script_manager') and self.world.script_manager: - try: - import json - scripts_info = json.loads(nodePath.getTag("scripts_info")) - print(f"节点 {nodePath.getName()} 需要重新挂载 {len(scripts_info)} 个脚本") - - script_manager = self.world.script_manager - for script_info in scripts_info: - script_name = script_info["name"] - script_file = script_info.get("file","") - - print(f"尝试重新挂载脚本{script_name}from {script_file}") - - if script_name not in script_manager.loader.script_classes: - if script_file and os.path.exists(script_file): - print(f"从文件加载脚本:{script_file}") - loaded_class = script_manager.load_script_from_file(script_file) - if loaded_class is None: - print(f"从文件加载脚本失败{script_file}") - script_path = self._find_scrip_in_directory(script_name) - if script_path: - print(f"从目录找到脚本并加载{script_path}") - script_manager.load_script_from_file(script_path) - else: - script_path = self._find_script_in_directory(script_name) - if script_path: - print(f"从目录找到脚本并加载: {script_path}") - script_manager.load_script_from_file(script_path) - else: - print(f"找不到脚本文件: {script_name}") - if script_name in script_manager.loader.script_classes: - script_component = script_manager.add_script_to_object(nodePath,script_name) - if script_component: - print(f"成功为 {nodePath.getName()} 添加脚本: {script_name}") - else: - print(f"为 {nodePath.getName()} 添加脚本失败: {script_name}") - else: - print(f"脚本 {script_name} 不可用,跳过挂载") - except Exception as e: - print(f"重新挂载脚本失败: {e}") - import traceback - traceback.print_exc() - - - # 恢复材质属性 - def parseColor(color_str): - """解析颜色字符串为Vec4""" - try: - color_str = color_str.replace('LVecBase4f', '').strip('()') - r, g, b, a = map(float, color_str.split(',')) - return Vec4(r, g, b, a) - except: - return Vec4(1, 1, 1, 1) - - # 创建并恢复材质 - material = Material() - material_changed = False - - if nodePath.hasTag("material_ambient"): - material.setAmbient(parseColor(nodePath.getTag("material_ambient"))) - material_changed = True - - if nodePath.hasTag("material_diffuse"): - material.setDiffuse(parseColor(nodePath.getTag("material_diffuse"))) - material_changed = True - - if nodePath.hasTag("material_specular"): - material.setSpecular(parseColor(nodePath.getTag("material_specular"))) - material_changed = True - - if nodePath.hasTag("material_emission"): - material.setEmission(parseColor(nodePath.getTag("material_emission"))) - material_changed = True - - if nodePath.hasTag("material_shininess"): - material.setShininess(float(nodePath.getTag("material_shininess"))) - material_changed = True - - if nodePath.hasTag("material_basecolor"): - material.setBaseColor(parseColor(nodePath.getTag("material_basecolor"))) - material_changed = True - - if material_changed: - nodePath.setMaterial(material) - - # 恢复颜色属性 - if nodePath.hasTag("color"): - nodePath.setColor(parseColor(nodePath.getTag("color"))) - - # 处理特定类型的节点 - if nodePath.hasTag("light_type"): - light_type = nodePath.getTag("light_type") - print(f"{indent}检测到光源类型: {light_type}") - - # 检查是否已经处理过这个灯光 - if nodePath not in processed_lights: - # 重新创建RP光源对象 - if light_type == "spot_light": - self._recreateSpotLight(nodePath) - elif light_type == "point_light": - self._recreatePointLight(nodePath) - # 标记为已处理 - processed_lights.append(nodePath) - - elif nodePath.hasTag("element_type"): - element_type = nodePath.getTag("element_type") - if element_type == "cesium_tileset": - tileset_url = nodePath.getTag("saved_tileset_url") if nodePath.hasTag( - "saved_tileset_url") else "" - tileset_info = { - 'url': tileset_url, - 'node': nodePath, - 'position': nodePath.getPos(), - 'tiles': {} - } - self.tilesets.append(tileset_info) - self.cesium_integration.tilesets[nodePath.getName()] = tileset_info - - # 将节点重新挂载到render下(如果需要) - # 注意:GUI元素可能需要挂载到特定的父节点上 - if nodePath.hasTag("gui_type") or nodePath.hasTag("is_gui_element"): - # GUI元素通常应该挂载到aspect2d或特定的GUI父节点上 - # 这里我们先保持原挂载关系 - pass - else: - # 其他节点确保挂载到render下 - if nodePath.getParent() != self.world.render and not nodePath.getName() in ["render", - "aspect2d", - "render2d"]: - nodePath.wrtReparentTo(self.world.render) - - # 为模型节点设置碰撞检测 - if nodePath.hasTag("is_model_root"): - print(f"J{indent}处理模型节点{nodePath.getName()}") - - #self._validateAndFixAllTransforms(nodePath) - - self._fixModelStructure(nodePath) - - # if self.world.property_panel._hasCollision(nodePath): - # print(f"{indent}模型{nodePath.getName()}已有碰撞体,跳过碰撞体设置") - # else: - # print(f"{indent}为模型{nodePath.getName()}设置碰撞检测") - # self.setupCollision(nodePath) - self.models.append(nodePath) - - # 递归处理子节点 - for child in nodePath.getChildren(): - processNode(child, depth + 1) - - print("\n开始处理场景节点...") - processNode(scene) - - #处理父子关系 - 在所有节点加载完成后设置正确的父子关系 - print("\n开始重建父子关系...") - self._rebuildParentChildRelationships(loaded_nodes) - - # 加载GUI信息并重新创建非3D的GUI元素 - gui_info_file = filename.replace('.bam', '_gui.json') - if os.path.exists(gui_info_file): - try: - with open(gui_info_file, 'r', encoding='utf-8') as f: - content = f.read().strip() - if content: # 检查文件是否为空 - import json - gui_data = json.loads(content) - print(f"✓ 成功加载GUI信息文件: {gui_info_file}") - print(f" 发现 {len(gui_data)} 个GUI元素需要重建") - - # 使用gui_manager重新创建GUI元素 - self._recreateGUIElementsFromData(gui_data) - else: - print("ℹ️ GUI信息文件为空") - except json.JSONDecodeError as e: - print(f"✗ GUI信息文件格式错误: {e}") - except Exception as e: - print(f"✗ 加载GUI信息失败: {e}") - import traceback - traceback.print_exc() - else: - print("ℹ️ 未找到GUI信息文件") - - # 移除临时场景节点 - if not scene.isEmpty(): - scene.removeNode() - - # 更新场景树 - #self.updateSceneTree() - #self._get_tree_widget().create_model_items(scene) - - print(f"加载完成,GUI元素数量: {len(self.world.gui_elements)}") - if len(self.world.gui_elements) > 0: - print("GUI元素列表:") - for i, elem in enumerate(self.world.gui_elements): - print( - f" {i + 1}. {elem.getName()} (类型: {elem.getTag('gui_type') if elem.hasTag('gui_type') else 'unknown'})") - - print("=== 场景加载完成 ===\n") - return True - - except Exception as e: - print(f"加载场景时发生错误: {str(e)}") - import traceback - traceback.print_exc() - return False - - def _rebuildParentChildRelationships(self, loaded_nodes): - try: - parent_child_relations = [] - for node_name, node in loaded_nodes.items(): - if node.hasTag("parent_name"): - parent_name = node.getTag("parent_name") - if parent_name in loaded_nodes: - parent_child_relations.append((node, loaded_nodes[parent_name])) # 修复:应该是元组 - print(f"发现父子关系:{parent_name}->{node_name}") - else: - print(f"警告:节点{node_name}的父节点{parent_name}不存在") - for child_node, parent_node in parent_child_relations: - try: - child_node.wrtReparentTo(parent_node) - print(f"成功设置父子关系:{parent_node.getName()}->{child_node.getName()}") - except Exception as e: - print(f"设置父子关系失败{parent_node.getName()}->{child_node.getName()}:{e}") - - if not parent_child_relations: - print("尝试从场景结构推断父子关系") - self._inferParentChildRelationships(loaded_nodes) - - print("父子关系重建完成") - except Exception as e: - print(f"重建父子关系时出错: {e}") - import traceback - traceback.print_exc() - - - except Exception as e: - print(f"重建父子关系时出错: {e}") - import traceback - traceback.print_exc() - - def _inferParentChildRelationships(self, loaded_nodes): - """从场景结构推断父子关系""" - try: - # 这里可以添加更复杂的父子关系推断逻辑 - # 例如,根据节点名称、位置关系等进行推断 - # 目前保持简单,后续可以扩展 - print("父子关系推断完成(当前为空实现)") - except Exception as e: - print(f"推断父子关系时出错: {e}") - - def _shouldSkipNodeInTree(self, nodePath): - """判断节点是否应该在场景树中跳过显示""" - - if nodePath.getName().startswith('ground'): - return True - - # 跳过render节点的递归 - if nodePath.getName() == "render": - return True - - # 跳过光源节点 - if nodePath.getName() in ["alight", "dlight"]: - return True - - # 跳过相机节点 - if nodePath.getName() in ["camera", "cam"]: - return True - - # 跳过3D文本和3D图像节点 - if (hasattr(nodePath.node(), "hasTag") and - nodePath.node().hasTag("gui_type") and - nodePath.node().getTag("gui_type") in ["3d_text", "3d_image"]): - return True - - # 跳过辅助节点 - if nodePath.getName().startswith(("gizmo", "selectionBox")): - return True - - return False - - def _recreateGUIElementsFromData(self, gui_data): - """根据保存的GUI数据重新创建GUI元素""" - try: - gui_manager = getattr(self.world, 'gui_manager', None) - property_manager = getattr(self.world, 'property_panel', None) - info_panel_manager = getattr(self.world, 'info_panel_manager', None) - if not gui_manager: - print("GUI管理器未找到,无法重建GUI元素") - return - print(f"开始重建 {len(gui_data)} 个GUI元素...") - - processed_names = set() - created_elements = {} - # 存储原始的缩放和位置信息,用于后续计算 - element_original_data = {} - - # 第一遍:收集所有元素信息 - for i, gui_info in enumerate(gui_data): - name = gui_info.get("name", f"gui_element_{i}") - element_original_data[name] = { - "scale": gui_info.get("scale", [1, 1, 1]), - "position": gui_info.get("position", [0, 0, 0]), - "parent_name": gui_info.get("parent_name") - } - - valid_parents = set() - for gui_info in gui_data: - name = gui_info.get("name", f"gui_element_{gui_info.get('index', 0)}") - valid_parents.add(name) - - if hasattr(self.world, 'gui_elements'): - for elem in self.world.gui_elements: - if elem and not elem.isEmpty(): - valid_parents.add(elem.getName()) - - valid_parents.add("render") - valid_parents.add("aspect2d") - valid_parents.add("render2d") - - pos = (0, 0, 0) - for i, gui_info in enumerate(gui_data): - try: - gui_type = gui_info.get("type", "unknown") - name = gui_info.get("name", f"gui_element_{i}") - position = gui_info.get("position", [0, 0, 0]) - scale = gui_info.get("scale", [1, 1, 1]) - tags = gui_info.get("tags", {}) - text = gui_info.get("text", "") - image_path = gui_info.get("image_path", "") - video_path = gui_info.get("video_path", "") - bg_image_path = gui_info.get("bg_image_path", "") # 背景图片路径 - panel_id = gui_info.get("panel_id", name) # 信息面板ID - panel_data = gui_info.get("panel_data", None) # 面板数据 - parent_name = gui_info.get("parent_name") - - # 检查是否已经处理过同名元素 - if name in processed_names: - print(f"跳过重复元素: {name}") - continue - - if parent_name and parent_name not in valid_parents: - print(f"⚠️ 跳过元素 {name},因为其父级 {parent_name} 不存在") - continue - - processed_names.add(name) - - print(f"重建GUI元素: {name} (类型: {gui_type})") - print(f" 位置: {position}") - print(f" 缩放: {scale}") - print(f" 文本: {text}") - print(f" 图像路径: {image_path}") - print(f" 背景图片路径: {bg_image_path}") - print(f" 视频路径: {video_path}") - - absolute_position = list(position) - absolute_scale = list(scale) - - if parent_name and parent_name in element_original_data: - parent_data = element_original_data[parent_name] - parent_scale = parent_data["scale"] - - if gui_type in ["3d_text", "3d_image", "button", "label", "entry", "2d_image", - "2d_video_screen"]: - # 位置需要乘以父级缩放来得到绝对位置 - for j in range(min(len(absolute_position), len(parent_scale))): - absolute_position[j] *= parent_scale[j] if len(parent_scale) > j else parent_scale[0] - - # 缩放需要乘以父级缩放来得到绝对缩放 - for j in range(min(len(absolute_scale), len(parent_scale))): - absolute_scale[j] *= parent_scale[j] if len(parent_scale) > j else parent_scale[0] - - print(f" 绝对位置: {absolute_position}") - print(f" 绝对缩放: {absolute_scale}") - - # 根据类型创建相应的GUI元素 - new_element = None - - if gui_type == "button" and hasattr(gui_manager, 'createGUIButton'): - new_element = gui_manager.createGUIButton( - pos=tuple(absolute_position), - text=text, - size=absolute_scale[0] if absolute_scale and len(absolute_scale) > 0 else 1.0 - ) - elif gui_type == "label" and hasattr(gui_manager, 'createGUILabel'): - scale_value = absolute_scale[0] if absolute_scale and len(absolute_scale) > 0 else 1.0 - new_element = gui_manager.createGUILabel( - pos=tuple(absolute_position), - text=text, - size=scale_value - ) - elif gui_type == "entry" and hasattr(gui_manager, 'createGUIEntry'): - new_element = gui_manager.createGUIEntry( - pos=tuple(absolute_position), - placeholder=text, - size=absolute_scale[0] if absolute_scale and len(absolute_scale) > 0 else 1.0 - ) - elif gui_type == "2d_image" and hasattr(gui_manager, 'createGUI2DImage'): - new_element = gui_manager.createGUI2DImage( - pos=tuple(absolute_position), - image_path=image_path, - size=(0.8,0.8,0.8) - ) - elif gui_type == "3d_text" and hasattr(gui_manager, 'createGUI3DText'): - size = absolute_scale[0] if absolute_scale and len(absolute_scale) > 0 else 0.5 - new_element = gui_manager.createGUI3DText( - pos=tuple(absolute_position), - text=text, - size=absolute_scale - ) - elif gui_type == "3d_image" and hasattr(gui_manager, 'createGUI3DImage'): - # 处理3D图像 - # 根据缩放值的数量处理尺寸 - size = (absolute_scale[0] * 0.2, absolute_scale[1] * 0.2, absolute_scale[2] * 0.2) - - new_element = gui_manager.createGUI3DImage( - pos=tuple(absolute_position), - image_path=image_path, - size=size - ) - elif gui_type == "video_screen" and hasattr(gui_manager, 'createVideoScreen'): - print(f"重建的3d视频屏幕视频地址是{video_path}") - new_element = gui_manager.createVideoScreen( - pos=tuple(absolute_position), - size=absolute_scale, - video_path=video_path - ) - if video_path and new_element: - if video_path.startswith("http://") or video_path.startswith("https://"): - pass - else: - if hasattr(gui_manager, 'loadVideoFile'): - from direct.task.TaskManagerGlobal import taskMgr - - def load_video_file_task(task): - gui_manager.loadVideoFile(new_element, video_path) - return task.done - - taskMgr.doMethodLater(0.1, load_video_file_task, 'loadVideoFileTask') - - elif gui_type == "2d_video_screen" and hasattr(gui_manager, 'createGUI2DVideoScreen'): - print(f"重建的2d视频屏幕视频地址是{video_path}") - new_element = gui_manager.createGUI2DVideoScreen( - pos=tuple(absolute_position), - size=absolute_scale, - video_path=video_path - ) - if video_path and new_element: - if video_path.startswith("http://") or video_path.startswith("https://"): - pass - else: - if hasattr(property_manager, 'load2DVideoFile'): - from direct.task.TaskManagerGlobal import taskMgr - - def load_2d_video_file_task(task): - property_manager.load2DVideoFile(new_element, video_path) - return task.done - - taskMgr.doMethodLater(0.1, load_2d_video_file_task, 'load2DVideoFileTask') - elif gui_type == "info_panel": - new_element = self.world.info_panel_manager.onCreateSampleInfoPanel() - # 如果创建成功,设置属性 - if new_element: - # 如果返回的是列表(多选创建),取第一个 - if isinstance(new_element, list): - new_element = new_element[0] - - # 设置名称 - new_element.setName(name) - - # 设置变换 - new_element.setPos(*position) - - if len(scale) >= 3: - new_element.setScale(scale[0], scale[1], scale[2]) - elif len(scale) >= 1: - new_element.setScale(scale[0]) - - # 设置标签 - # 对于NodePath对象 - if hasattr(new_element, 'setTag'): - for tag_name, tag_value in tags.items(): - # 跳过变换标签,因为我们已经设置了 - if tag_name not in ["transform_pos", "transform_hpr", "transform_scale"]: - new_element.setTag(tag_name, tag_value) - # 对于DirectGUI对象,使用自定义标签存储 - elif hasattr(new_element, '_tags'): - new_element._tags.update(tags) - - created_elements[name] = new_element - - print(f"GUI元素重建成功: {name}") - else: - print(f"无法重建GUI元素: {name} (类型: {gui_type})") - - except Exception as e: - print(f"重建GUI元素失败 {name}: {e}") - import traceback - traceback.print_exc() - continue - - # 第二遍:设置父子级关系并更新Qt树 - print("开始设置父子级关系...") - try: - # 创建父子级关系映射 - parent_child_map = {} - for gui_info in gui_data: - name = gui_info.get("name") - parent_name = gui_info.get("parent_name") - - if name and parent_name and parent_name in created_elements: - parent_child_map[name] = parent_name - print(f"父子级关系映射: {parent_name} -> {name}") - - # 按正确的顺序设置父子级关系并更新Qt树 - tree_widget = self._get_tree_widget() - if tree_widget: - # 先将所有元素添加到Qt树中 - qt_tree_items = {} - for name, element in created_elements.items(): - # 尝试在Qt树中找到对应的项,如果找不到则创建 - qt_item = self._findOrCreateQtTreeItem(tree_widget, element, name) - if qt_item: - qt_tree_items[name] = qt_item - - # 然后设置父子级关系 - for child_name, parent_name in parent_child_map.items(): - try: - if child_name in created_elements and parent_name in created_elements: - child_element = created_elements[child_name] - parent_element = created_elements[parent_name] - - # 设置父子级关系 - if hasattr(child_element, 'reparentTo'): - child_element.reparentTo(parent_element) - print(f"成功设置父子级关系: {parent_name} -> {child_name}") - - # 更新Qt树显示 - if child_name in qt_tree_items and parent_name in qt_tree_items: - child_item = qt_tree_items[child_name] - parent_item = qt_tree_items[parent_name] - - # 从当前位置移除子项 - if child_item.parent(): - child_item.parent().removeChild(child_item) - else: - # 如果是顶级项,从树中移除 - index = tree_widget.indexOfTopLevelItem(child_item) - if index >= 0: - tree_widget.takeTopLevelItem(index) - - # 将子项添加到新的父项下 - parent_item.addChild(child_item) - print(f"Qt树更新: {child_name} 移动到 {parent_name} 下") - else: - print(f"元素 {child_name} 不支持 reparentTo 操作") - else: - print(f"元素未找到: 父级={parent_name}, 子级={child_name}") - except Exception as e: - print(f"设置父子级关系失败 {parent_name} -> {child_name}: {e}") - continue - else: - # 如果没有tree_widget,只设置父子级关系 - for child_name, parent_name in parent_child_map.items(): - try: - if child_name in created_elements and parent_name in created_elements: - child_element = created_elements[child_name] - parent_element = created_elements[parent_name] - - # 设置父子级关系 - if hasattr(child_element, 'reparentTo'): - child_element.reparentTo(parent_element) - print(f"成功设置父子级关系: {parent_name} -> {child_name}") - except Exception as e: - print(f"设置父子级关系失败 {parent_name} -> {child_name}: {e}") - continue - - except Exception as e: - print(f"设置父子级关系时出错: {e}") - # 第三遍:重新挂载脚本 - print("开始重新挂载脚本...") - for gui_info in gui_data: - try: - name = gui_info.get("name") - if name in created_elements and "scripts" in gui_info: - new_element = created_elements[name] - - # 重新挂载脚本(如果有的话) - if "scripts" in gui_info and hasattr(self.world, - 'script_manager') and self.world.script_manager: - script_manager = self.world.script_manager - for script_info in gui_info["scripts"]: - script_name = script_info["name"] - script_file = script_info.get("file", "") - - print(f"尝试重新挂载脚本: {script_name} from {script_file}") - - # 检查脚本是否已加载 - if script_name not in script_manager.loader.script_classes: - # 如果脚本未加载,尝试从保存的文件路径加载 - if script_file and os.path.exists(script_file): - print(f"从文件加载脚本: {script_file}") - loaded_class = script_manager.load_script_from_file(script_file) - if loaded_class is None: - print(f"从文件加载脚本失败: {script_file}") - # 如果从文件加载失败,尝试在脚本目录中查找 - script_path = self._find_script_in_directory(script_name) - if script_path: - print(f"从目录找到脚本并加载: {script_path}") - script_manager.load_script_from_file(script_path) - else: - # 如果没有文件路径或文件不存在,尝试在脚本目录中查找 - script_path = self._find_script_in_directory(script_name) - if script_path: - print(f"从目录找到脚本并加载: {script_path}") - script_manager.load_script_from_file(script_path) - else: - print(f"找不到脚本文件: {script_name}") - - # 为元素添加脚本 - script_component = script_manager.add_script_to_object(new_element, script_name) - if script_component: - print(f"成功为 {name} 添加脚本: {script_name}") - else: - print(f"为 {name} 添加脚本失败: {script_name}") - except Exception as e: - print(f"重新挂载脚本失败: {e}") - import traceback - traceback.print_exc() - continue - - print(f"GUI元素重建完成,共创建 {len(created_elements)} 个元素") - - except Exception as e: - print(f"重建GUI元素时出错: {e}") - import traceback - traceback.print_exc() - - def _findOrCreateQtTreeItem(self, tree_widget, target_element, element_name): - """在Qt树中查找或创建指定元素对应的项""" - try: - # 首先尝试查找现有的项 - existing_item = self._findQtTreeItem(tree_widget, target_element) - if existing_item: - return existing_item - - # 如果找不到,创建新的项 - # 找到场景根节点 - scene_root = None - for i in range(tree_widget.topLevelItemCount()): - top_item = tree_widget.topLevelItem(i) - if top_item.data(0, Qt.UserRole + 1) == "SCENE_ROOT": - scene_root = top_item - break - - if not scene_root: - print("无法找到场景根节点") - return None - - # 创建新的Qt树项 - new_item = QTreeWidgetItem(scene_root, [element_name]) - new_item.setData(0, Qt.UserRole, target_element) - new_item.setData(0, Qt.UserRole + 1, "SCENE_NODE") # 或根据元素类型设置适当的类型 - - print(f"为元素 {element_name} 创建了新的Qt树项") - return new_item - - except Exception as e: - print(f"查找或创建Qt树项失败: {e}") - return None - - def _findQtTreeItem(self, tree_widget, target_element): - """在Qt树中查找指定元素对应的项""" - try: - def search_recursive(parent_item): - # 检查当前项 - if parent_item: - item_element = parent_item.data(0, Qt.UserRole) - if item_element == target_element: - return parent_item - - # 递归检查子项 - for i in range(parent_item.childCount()): - child_item = parent_item.child(i) - result = search_recursive(child_item) - if result: - return result - return None - - # 从根节点开始搜索 - root = tree_widget.invisibleRootItem() - for i in range(root.childCount()): - top_item = root.child(i) - result = search_recursive(top_item) - if result: - return result - - return None - except Exception as e: - print(f"查找Qt树项失败: {e}") - return None - def _find_script_in_directory(self, script_name): - """在脚本目录中查找脚本文件""" - try: - if hasattr(self.world, 'script_manager') and self.world.script_manager: - script_manager = self.world.script_manager - scripts_dir = script_manager.scripts_directory - - if os.path.exists(scripts_dir): - # 首先精确匹配 - for file_name in os.listdir(scripts_dir): - if file_name.endswith('.py'): - base_name = os.path.splitext(file_name)[0] - if base_name == script_name: - return os.path.join(scripts_dir, file_name) - - # 如果没有精确匹配,尝试模糊匹配 - for file_name in os.listdir(scripts_dir): - if file_name.endswith('.py'): - base_name = os.path.splitext(file_name)[0] - if script_name.lower() in base_name.lower() or base_name.lower() in script_name.lower(): - return os.path.join(scripts_dir, file_name) - except Exception as e: - print(f"查找脚本文件时出错: {e}") - - return None - - def _recreateSpotLight(self, light_node): - """重新创建聚光灯""" - try: - from RenderPipelineFile.rpcore import SpotLight - from panda3d.core import Vec3 - - # 创建聚光灯对象 - light = SpotLight() - light.direction = Vec3(0, 0, -1) - light.fov = 70 - light.set_color_from_temperature(5 * 1000.0) - - # 恢复保存的属性 - if light_node.hasTag("light_energy"): - light.energy = float(light_node.getTag("light_energy")) - else: - light.energy = 5000 - - light.radius = 1000 - light.casts_shadows = True - light.shadow_map_resolution = 256 - - light_pos = light_node.getPos() - light.setPos(light_pos) - - # 添加到渲染管线 - render_pipeline = get_render_pipeline() - render_pipeline.add_light(light) - - # 保存光源对象引用 - light_node.setPythonTag("rp_light_object", light) - - # 添加到管理列表 - self.Spotlight.append(light_node) - - # 确保灯光节点有正确的标签,以便在场景树更新时被识别 - if not light_node.hasTag("is_scene_element"): - light_node.setTag("is_scene_element", "1") - - print(f"重新创建聚光灯: {light_node.getName()}") - except Exception as e: - print(f"重新创建聚光灯失败: {str(e)}") - import traceback - traceback.print_exc() - - def _recreatePointLight(self, light_node): - """重新创建点光源""" - try: - from RenderPipelineFile.rpcore import PointLight - from QPanda3D.Panda3DWorld import get_render_pipeline - - # 创建点光源对象 - light = PointLight() - - # 恢复保存的属性 - if light_node.hasTag("light_energy"): - light.energy = float(light_node.getTag("light_energy")) - else: - light.energy = 5000 - - light.radius = 1000 - light.inner_radius = 0.4 - light.set_color_from_temperature(5 * 1000.0) - light.casts_shadows = True - light.shadow_map_resolution = 256 - - # 设置位置 - light.setPos(light_node.getPos()) - - # 添加到渲染管线 - render_pipeline = get_render_pipeline() - render_pipeline.add_light(light) - - # 保存光源对象引用 - light_node.setPythonTag("rp_light_object", light) - - # 添加到管理列表 - self.Pointlight.append(light_node) - - # 确保灯光节点有正确的标签,以便在场景树更新时被识别 - if not light_node.hasTag("is_scene_element"): - light_node.setTag("is_scene_element", "1") - - print(f"重新创建点光源: {light_node.getName()}") - except Exception as e: - print(f"重新创建点光源失败: {str(e)}") - import traceback - traceback.print_exc() - - def _cleanupAuxiliaryNodes(self): - """清理场景中可能存在的辅助节点""" - try: - # 查找并移除所有坐标轴节点 - gizmo_nodes = self.world.render.findAllMatches("**/gizmo*") - for node in gizmo_nodes: - if not node.isEmpty(): - node.removeNode() - print(f"清理坐标轴节点: {node.getName()}") - - # 查找并移除所有选择框节点 - selection_box_nodes = self.world.render.findAllMatches("**/selectionBox*") - for node in selection_box_nodes: - if not node.isEmpty(): - node.removeNode() - print(f"清理选择框节点: {node.getName()}") - - # 停止相关的更新任务 - from direct.task.TaskManagerGlobal import taskMgr - taskMgr.remove("updateGizmo") - taskMgr.remove("updateSelectionBox") - - print("辅助节点清理完成") - except Exception as e: - print(f"清理辅助节点时出错: {e}") - - # ==================== 模型管理 ==================== - - def deleteModel(self, model): - """删除模型""" - try: - if model in self.models: - tree_widget = self._get_tree_widget() - if not tree_widget: - return False - - tree_widget.delete_items(tree_widget.selectedItems()) - # model.removeNode() - # self.models.remove(model) - # self.updateSceneTree() - print(f"删除模型: {model.getName()}") - return True - except Exception as e: - print(f"删除模型失败: {str(e)}") - return False - - def clearAllModels(self): - """清除所有模型""" - pass - # try: - # for model in self.models: - # model.removeNode() - # self.models.clear() - # self.updateSceneTree() - # print("清除所有模型完成") - # except Exception as e: - # print(f"清除所有模型失败: {str(e)}") - - def getModels(self): - """获取模型列表""" - return self.models.copy() - - def getModelCount(self): - """获取模型数量""" - return len(self.models) - - def findModelByName(self, name): - """根据名称查找模型""" - for model in self.models: - if model.getName() == name: - return model - return None - - # ==================== 帮助方法 ==================== - - def processLoadedModel(self, model): - """处理加载完成的模型(用于异步加载回调)""" - if model: - # 添加到模型列表 - self.models.append(model) - - # 设置基础变换 - model.setPos(0, 0, 0) - model.setHpr(0, 0, 0) - model.setScale(1, 1, 1) - - # 应用材质 - self.processMaterials(model) - - # 设置碰撞检测 - self.setupCollision(model) - - # 更新场景树 - self.updateSceneTree() - - print(f"异步加载模型完成: {model.getName()}") - - def createSpotLight(self, pos=(0, 0, 0)): - """创建聚光灯 - 支持多选创建,优化版本""" - try: - from RenderPipelineFile.rpcore import SpotLight - from QPanda3D.Panda3DWorld import get_render_pipeline - from panda3d.core import Vec3, NodePath - from PyQt5.QtCore import Qt - - print(f"🔆 开始创建聚光灯,位置: {pos}") - - # 获取树形控件 - tree_widget = self._get_tree_widget() - if not tree_widget: - print("❌ 无法访问树形控件") - return None - - # 获取目标父节点列表 - target_parents = tree_widget.get_target_parents_for_creation() - if not target_parents: - print("❌ 没有找到有效的父节点") - return None - - created_lights = [] - render_pipeline = get_render_pipeline() - - # 为每个有效的父节点创建聚光灯 - for parent_item, parent_node in target_parents: - try: - # 生成唯一名称 - light_name = f"Spotlight_{len(self.Spotlight)}" - - # 创建挂载节点 - 挂载到选中的父节点 - light_np = NodePath(light_name) - light_np.reparentTo(parent_node) # 挂载到父节点而不是render - light_np.setPos(*pos) - - light_np.setTransform(TransformState.makeIdentity()) - - # 创建聚光灯对象 - light = SpotLight() - light.direction = Vec3(0, 0, -1) - light.fov = 70 - light.set_color_from_temperature(5 * 1000.0) - light.energy = 5000 - light.radius = 1000 - light.casts_shadows = True - light.shadow_map_resolution = 256 - light.setPos(pos) - - # 添加到渲染管线 - render_pipeline.add_light(light) - - # 设置节点属性和标签 - light_np.setTag("light_type", "spot_light") - light_np.setTag("is_scene_element", "1") - light_np.setTag("tree_item_type", "LIGHT_NODE") - light_np.setTag("light_energy", str(light.energy)) - light_np.setTag("created_by_user", "1") - - # 保存光源对象引用 - light_np.setPythonTag("rp_light_object", light) - - # 添加到管理列表 - self.Spotlight.append(light_np) - - print(f"✅ 为 {parent_item.text(0)} 创建聚光灯成功: {light_name}") - - # 在Qt树形控件中添加对应节点 - qt_item = tree_widget.add_node_to_tree_widget(light_np, parent_item, "LIGHT_NODE") - if qt_item: - created_lights.append((light_np, qt_item)) - else: - created_lights.append((light_np, None)) - print("⚠️ Qt树节点添加失败,但Panda3D对象已创建") - - except Exception as e: - print(f"❌ 为 {parent_item.text(0)} 创建聚光灯失败: {str(e)}") - import traceback - traceback.print_exc() - continue - - # 处理创建结果 - if not created_lights: - print("❌ 没有成功创建任何聚光灯") - return None - - # 选中最后创建的光源 - if created_lights: - last_light_np, last_qt_item = created_lights[-1] - if last_qt_item: - tree_widget.setCurrentItem(last_qt_item) - # 更新选择和属性面板 - tree_widget.update_selection_and_properties(last_light_np, last_qt_item) - - print(f"🎉 总共创建了 {len(created_lights)} 个聚光灯") - - # 返回值处理 - if len(created_lights) == 1: - return created_lights[0][0] # 单个光源返回NodePath - else: - return [light_np for light_np, _ in created_lights] # 多个光源返回列表 - - except Exception as e: - print(f"❌ 创建聚光灯过程失败: {str(e)}") - import traceback - traceback.print_exc() - return None - - def createPointLight(self, pos=(0, 0, 0)): - """创建点光源 - 支持多选创建,优化版本""" - try: - from RenderPipelineFile.rpcore import PointLight - from QPanda3D.Panda3DWorld import get_render_pipeline - from panda3d.core import Vec3, NodePath - from PyQt5.QtCore import Qt - - print(f"💡 开始创建点光源,位置: {pos}") - - # 获取树形控件 - tree_widget = self._get_tree_widget() - if not tree_widget: - print("❌ 无法访问树形控件") - return None - - # 获取目标父节点列表 - target_parents = tree_widget.get_target_parents_for_creation() - if not target_parents: - print("❌ 没有找到有效的父节点") - return None - - created_lights = [] - render_pipeline = get_render_pipeline() - - # 为每个有效的父节点创建点光源 - for parent_item, parent_node in target_parents: - try: - # 生成唯一名称 - light_name = f"Pointlight_{len(self.Pointlight)}" - - # 创建挂载节点 - 挂载到选中的父节点 - light_np = NodePath(light_name) - light_np.reparentTo(parent_node) # 挂载到父节点而不是render - light_np.setPos(*pos) - - # 确保变换矩阵有效 - light_np.setTransform(TransformState.makeIdentity()) - - # 创建点光源对象 - light = PointLight() - light.setPos(*pos) - light.energy = 5000 - light.radius = 1000 - light.inner_radius = 0.4 - light.set_color_from_temperature(5 * 1000.0) - light.casts_shadows = True - light.shadow_map_resolution = 256 - - # 添加到渲染管线 - render_pipeline.add_light(light) - - # 设置节点属性和标签 - light_np.setTag("light_type", "point_light") - light_np.setTag("is_scene_element", "1") - light_np.setTag("tree_item_type", "LIGHT_NODE") - light_np.setTag("light_energy", str(light.energy)) - light_np.setTag("created_by_user", "1") - - # 保存光源对象引用 - light_np.setPythonTag("rp_light_object", light) - - # 添加到管理列表 - self.Pointlight.append(light_np) - - print(f"✅ 为 {parent_item.text(0)} 创建点光源成功: {light_name}") - - # 在Qt树形控件中添加对应节点 - qt_item =tree_widget.add_node_to_tree_widget(light_np, parent_item, "LIGHT_NODE") - if qt_item: - created_lights.append((light_np, qt_item)) - else: - created_lights.append((light_np, None)) - print("⚠️ Qt树节点添加失败,但Panda3D对象已创建") - - except Exception as e: - print(f"❌ 为 {parent_item.text(0)} 创建点光源失败: {str(e)}") - continue - - # 处理创建结果 - if not created_lights: - print("❌ 没有成功创建任何点光源") - return None - - # 选中最后创建的光源 - if created_lights: - last_light_np, last_qt_item = created_lights[-1] - if last_qt_item: - tree_widget.setCurrentItem(last_qt_item) - # 更新选择和属性面板 - tree_widget.update_selection_and_properties(last_light_np, last_qt_item) - - print(f"🎉 总共创建了 {len(created_lights)} 个点光源") - - # 返回值处理 - if len(created_lights) == 1: - return created_lights[0][0] # 单个光源返回NodePath - else: - return [light_np for light_np, _ in created_lights] # 多个光源返回列表 - - except Exception as e: - print(f"❌ 创建点光源过程失败: {str(e)}") - import traceback - traceback.print_exc() - return None - - def _get_tree_widget(self): - """安全获取树形控件""" - try: - if (hasattr(self.world, 'interface_manager') and - hasattr(self.world.interface_manager, 'treeWidget')): - return self.world.interface_manager.treeWidget - except AttributeError: - pass - return None - - - # ==================== GLB 转换方法 ==================== - - #=================================================== - - def _shouldConvertToGLB(self, filepath): - """判断是否应该转换为GLB格式""" - ext = os.path.splitext(filepath)[1].lower() - # 需要转换的格式:FBX, OBJ, DAE等(但不转换已经是GLB/GLTF的) - convert_formats = ['.fbx', '.obj', '.dae', '.3ds', '.blend'] - return ext in convert_formats - - def _convertToGLBWithProgress(self, filepath): - """带进度显示的GLB转换""" - try: - from PyQt5.QtWidgets import QProgressDialog, QApplication - from PyQt5.QtCore import Qt - - # 创建进度对话框 - progress = QProgressDialog("正在转换模型格式以获得更好的动画支持...", "取消", 0, 100) - progress.setWindowTitle("模型格式转换") - progress.setWindowModality(Qt.WindowModal) - progress.show() - QApplication.processEvents() - - try: - result = self._convertToGLB(filepath, progress) - progress.hide() - return result - except Exception as e: - progress.hide() - print(f"转换过程出错: {e}") - return None - - except ImportError: - # 如果没有 PyQt5,直接转换 - return self._convertToGLB(filepath) - - def _convertToGLB(self, filepath, progress=None): - """将模型文件转换为GLB格式""" - try: - print(f"[GLB转换] 开始转换: {filepath}") - - if progress: - progress.setValue(10) - progress.setLabelText("准备转换...") - from PyQt5.QtWidgets import QApplication - QApplication.processEvents() - - # 准备输出路径 - base_name = os.path.splitext(os.path.basename(filepath))[0] - output_dir = os.path.dirname(filepath) - glb_path = os.path.join(output_dir, f"{base_name}_auto_converted.glb") - - # 如果已经存在转换后的文件,直接使用 - if os.path.exists(glb_path): - # 检查文件时间,如果原文件更新则重新转换 - original_time = os.path.getmtime(filepath) - converted_time = os.path.getmtime(glb_path) - if converted_time > original_time: - print(f"[GLB转换] 使用现有转换文件: {glb_path}") - if progress: - progress.setValue(100) - return glb_path - - if progress: - progress.setValue(20) - progress.setLabelText("尝试 Blender 转换...") - QApplication.processEvents() - - # 方法1: 使用 Blender 进行转换 - if self._convertWithBlender(filepath, glb_path, progress): - return glb_path - - if progress: - progress.setValue(60) - progress.setLabelText("尝试 FBX2glTF 转换...") - QApplication.processEvents() - - # 方法2: 使用 FBX2glTF (如果可用) - if self._convertWithFBX2glTF(filepath, glb_path, progress): - return glb_path - - if progress: - progress.setValue(80) - progress.setLabelText("尝试 Assimp 转换...") - QApplication.processEvents() - - # 方法3: 使用 Assimp - if self._convertWithAssimp(filepath, glb_path, progress): - return glb_path - - #print(f"[GLB转换] 所有转换方法都失败,既然没有可以转换格式的工具和环境那么就用原始文件,不一定非要转换") - return None - - except Exception as e: - print(f"[GLB转换] 转换过程出错: {e}") - return None - - def _convertWithBlender(self, input_path, output_path, progress=None): - """使用 Blender 进行转换""" - try: - import subprocess - import tempfile - - print(f"[Blender转换] {input_path} → {output_path}") - - # 创建 Blender 脚本 - script_content = f''' -import bpy -import sys -import os - -# 清理默认场景 -bpy.ops.object.select_all(action='SELECT') -bpy.ops.object.delete(use_global=False) - -print("开始导入文件...") - -# 根据文件类型选择导入方法 -input_file = "{input_path}" -output_file = "{output_path}" - -try: - ext = os.path.splitext(input_file)[1].lower() - - if ext == '.fbx': - bpy.ops.import_scene.fbx(filepath=input_file) - elif ext == '.obj': - bpy.ops.import_scene.obj(filepath=input_file) - elif ext == '.dae': - bpy.ops.wm.collada_import(filepath=input_file) - elif ext == '.blend': - bpy.ops.wm.open_mainfile(filepath=input_file) - else: - print(f"不支持的格式: {{ext}}") - sys.exit(1) - - print("导入成功,开始导出GLB...") - - # 导出为 GLB,保留动画 - bpy.ops.export_scene.gltf( - filepath=output_file, - export_format='GLB', - export_animations=True, - export_force_sampling=True, - export_frame_range=True, - export_current_frame=False, - export_skins=True, - export_morph=True, - export_lights=True, - export_cameras=False - ) - - print("GLB导出成功!") - -except Exception as e: - print(f"转换失败: {{e}}") - sys.exit(1) -''' - - # 写入临时脚本文件 - with tempfile.NamedTemporaryFile(mode='w', suffix='.py', delete=False) as temp_file: - temp_file.write(script_content) - script_path = temp_file.name - - try: - # 执行 Blender 转换 - result = subprocess.run([ - 'blender', '--background', '--python', script_path - ], capture_output=True, text=True, timeout=180) - - # 清理临时文件 - os.unlink(script_path) - - if result.returncode == 0 and os.path.exists(output_path): - print(f"[Blender转换] 转换成功") - return True - else: - print(f"[Blender转换] 转换失败: {result.stderr}") - return False - - except subprocess.TimeoutExpired: - print(f"[Blender转换] 转换超时") - return False - except FileNotFoundError: - print(f"[Blender转换] Blender 未安装") - return False - - except Exception as e: - print(f"[Blender转换] 转换过程出错: {e}") - return False - - def _convertWithFBX2glTF(self, input_path, output_path, progress=None): - """使用 FBX2glTF 进行转换(仅支持FBX)""" - try: - import subprocess - - if not input_path.lower().endswith('.fbx'): - return False - - print(f"[FBX2glTF转换] {input_path} → {output_path}") - - # 使用 FBX2glTF 转换 - result = subprocess.run([ - 'FBX2glTF', input_path, '--output', output_path, '--binary' - ], capture_output=True, text=True, timeout=120) - - if result.returncode == 0 and os.path.exists(output_path): - print(f"[FBX2glTF转换] 转换成功") - return True - else: - print(f"[FBX2glTF转换] 转换失败: {result.stderr}") - return False - - except subprocess.TimeoutExpired: - print(f"[FBX2glTF转换] 转换超时") - return False - except FileNotFoundError: - print(f"[FBX2glTF转换] FBX2glTF 未安装") - return False - except Exception as e: - print(f"[FBX2glTF转换] 转换过程出错: {e}") - return False - - def _convertWithAssimp(self, input_path, output_path, progress=None): - """使用 PyAssimp 进行转换""" - try: - import pyassimp - - print(f"[PyAssimp转换] {input_path} → {output_path}") - - # 加载模型 - scene = pyassimp.load(input_path) - if not scene: - print(f"[PyAssimp转换] 加载模型失败") - return False - - if progress: - progress.setValue(30) - - # 导出为GLB格式 - pyassimp.export(scene, output_path, "glb2") - - if progress: - progress.setValue(80) - - # 释放资源 - pyassimp.release(scene) - - if os.path.exists(output_path): - print(f"[PyAssimp转换] 转换成功") - return True - else: - print(f"[PyAssimp转换] 转换失败: 输出文件未生成") - return False - - except ImportError: - print(f"[PyAssimp转换] PyAssimp 未安装") - return False - except Exception as e: - print(f"[PyAssimp转换] 转换过程出错: {e}") - return False - - def load_cesium_tileset(self, tileset_url, position=(0, 0, 0)): - """ - 加载 Cesium 3D Tileset - 采用新的创建逻辑,支持多选和更完善的UI交互。 - """ - try: - from panda3d.core import NodePath - print(f"🗺️ 开始加载 Cesium 3D Tiles: {tileset_url}") - - # 1. 获取UI控件和目标父节点 - tree_widget = self._get_tree_widget() - if not tree_widget: - print("❌ 无法访问树形控件") - return None - - target_parents = tree_widget.get_target_parents_for_creation() - if not target_parents: - print("❌ 没有找到有效的父节点来附加Tileset") - return None - - created_tilesets = [] - - # 2. 遍历所有选中的父节点,并为其创建Tileset - for parent_item, parent_node in target_parents: - try: - # 生成唯一名称 - node_name = f"cesium_tileset_{len(self.tilesets)}" - - # 创建一个容器节点来管理tileset,并挂载到父节点 - tileset_node = parent_node.attachNewNode(node_name) - tileset_node.setPos(*position) - - # 添加标签以便场景识别和保存 - tileset_node.setTag("is_scene_element", "1") - tileset_node.setTag("tree_item_type", "CESIUM_TILESET_NODE") - tileset_node.setTag("element_type", "cesium_tileset") - tileset_node.setTag("tileset_url", tileset_url) - # 使用唯一名称作为文件标识,代替索引 - tileset_node.setTag("file", node_name) - - # 存储tileset核心信息 - tileset_info = { - 'url': tileset_url, - 'node': tileset_node, - 'position': position, - 'tiles': {} # 用于后续管理瓦片 - } - self.tilesets.append(tileset_info) - - # 创建一个临时的可视化占位符,让用户能看到节点已添加 - self._create_placeholder_geometry(tileset_node) - - # 异步加载tileset的实际数据 - self._load_tileset_async(tileset_url, tileset_info) - - print(f"✅ 为 {parent_item.text(0)} 加载 Tileset 成功: {node_name}") - - # 在Qt树形控件中添加对应节点 - qt_item = tree_widget.add_node_to_tree_widget(tileset_node, parent_item, "CESIUM_TILESET_NODE") - if qt_item: - created_tilesets.append((tileset_node, qt_item)) - else: - created_tilesets.append((tileset_node, None)) - print("⚠️ Qt树节点添加失败,但Panda3D对象已创建") - - except Exception as e: - print(f"❌ 为 {parent_item.text(0)} 加载 Tileset 失败: {str(e)}") - continue # 继续尝试为下一个父节点创建 - - # 3. 处理创建结果 - if not created_tilesets: - print("❌ 没有成功加载任何 Tileset") - return None - - # 选中最后创建的Tileset并更新UI - if created_tilesets: - last_tileset_node, last_qt_item = created_tilesets[-1] - if last_qt_item: - tree_widget.setCurrentItem(last_qt_item) - # 更新选择状态和属性面板 - tree_widget.update_selection_and_properties(last_tileset_node, last_qt_item) - - print(f"🎉 总共加载了 {len(created_tilesets)} 个 Cesium Tileset 实例") - - # 4. 返回值处理 - if len(created_tilesets) == 1: - return created_tilesets[0][0] # 单个实例返回NodePath - else: - return [node for node, _ in created_tilesets] # 多个实例返回NodePath列表 - - except Exception as e: - print(f"❌ 加载 Cesium 3D Tiles 过程失败: {str(e)}") - import traceback - traceback.print_exc() - return None - - def _load_tileset_async(self, tileset_url, tileset_info): - """异步加载 tileset 数据""" - - async def load_tileset(): - try: - async with aiohttp.ClientSession() as session: - async with session.get(tileset_url) as response: - if response.status == 200: - tileset_data = await response.json() - self._parse_tileset(tileset_data, tileset_info) - print(f"✓ Tileset 数据加载完成") - else: - print(f"✗ Tileset 加载失败: {response.status}") - except Exception as e: - print(f"✗ Tileset 加载出错: {e}") - - # 在 Panda3D 的任务系统中运行异步任务 - task = asyncio.ensure_future(load_tileset()) - self._current_asyncio_task = task # 保存任务引用 - self.world.taskMgr.add(self._check_async_task, "check_tileset_load", appendTask=True) - - def _check_async_task(self, panda3d_task): - # 检查 asyncio 任务是否完成 - if hasattr(self, '_current_asyncio_task'): - if self._current_asyncio_task.done(): - try: - self._current_asyncio_task.result() - except Exception as e: - print(f"异步任务出错:{e}") - # 返回 Panda3D 任务管理器的完成状态 - return panda3d_task.done # 注意是 done 而不是 DONE - # 返回 Panda3D 任务管理器的继续状态 - return panda3d_task.cont # 注意是 cont 而不是 CONTINUE - - def _parse_tileset(self,tileset_data,tileset_info): - try: - root = tileset_data.get('root',{}) - self._parse_tile(root,tileset_info['node'],tileset_info) - print("✓ Tileset 解析完成") - except Exception as e: - print(f"✗ Tileset 解析出错: {e}") - - def _parse_tile(self, tile_data, parent_node, tileset_info): - try: - # 获取tileID - tile_id = f"tile_{len(tileset_info['tiles'])}" - print(f"创建tile节点: {tile_id}") - # 创建tile节点 - tile_node = parent_node.attachNewNode(tile_id) - - tileset_info['tiles'][tile_id] = { - 'node': tile_node, - 'data': tile_data, - 'loaded': False - } - - # 如果有内容,创建占位几何体 - if 'content' in tile_data: - print(f"为tile {tile_id} 创建几何体") - self._create_tile_geometry(tile_node) - # 递归解析子tiles - children = tile_data.get('children', []) - print(f"Tile {tile_id} 有 {len(children)} 个子节点") - for child_data in children: - self._parse_tile(child_data, tile_node, tileset_info) - except Exception as e: - print(f"✗ Tile 解析出错: {e}") - import traceback - traceback.print_exc() - - def _create_tile_geometry(self,parent_node): - """为 tile 创建占位几何体""" - try: - # 创建一个简单的立方体作为占位符 - from panda3d.core import GeomVertexFormat, GeomVertexData, GeomVertexWriter - from panda3d.core import Geom, GeomTriangles, GeomNode - - format = GeomVertexFormat.getV3n3c4() - vdata = GeomVertexData('tile_cube', format, Geom.UHStatic) - - vertex = GeomVertexWriter(vdata, 'vertex') - normal = GeomVertexWriter(vdata, 'normal') - color = GeomVertexWriter(vdata, 'color') - - # 定义立方体顶点 - vertices = [ - (-0.5, -0.5, -0.5), (0.5, -0.5, -0.5), (0.5, 0.5, -0.5), (-0.5, 0.5, -0.5), - (-0.5, -0.5, 0.5), (0.5, -0.5, 0.5), (0.5, 0.5, 0.5), (-0.5, 0.5, 0.5) - ] - - for vert in vertices: - vertex.addData3f(*vert) - normal.addData3f(0, 0, 1) - color.addData4f(0.2, 0.6, 0.8, 1.0) - - # 创建几何体 - geom = Geom(vdata) - - # 创建面 - prim = GeomTriangles(Geom.UHStatic) - # 底面 - prim.addVertices(0, 1, 2) - prim.addVertices(0, 2, 3) - # 顶面 - prim.addVertices(4, 7, 6) - prim.addVertices(4, 6, 5) - # 前面 - prim.addVertices(0, 4, 5) - prim.addVertices(0, 5, 1) - # 后面 - prim.addVertices(2, 6, 7) - prim.addVertices(2, 7, 3) - # 左面 - prim.addVertices(0, 3, 7) - prim.addVertices(0, 7, 4) - # 右面 - prim.addVertices(1, 5, 6) - prim.addVertices(1, 6, 2) - - prim.closePrimitive() - geom.addPrimitive(prim) - - # 创建几何节点 - geom_node = GeomNode('tile_geometry') - geom_node.addGeom(geom) - - # 添加到场景 - cube_node = parent_node.attachNewNode(geom_node) - cube_node.setScale(1000) # 放大以便观察 - - # 添加材质 - material = Material() - material.setBaseColor((0.2, 0.6, 0.8, 1.0)) - material.setSpecular((0.1, 0.1, 0.1, 1.0)) - material.setShininess(10.0) - cube_node.setMaterial(material) - - except Exception as e: - print(f"✗ 创建 tile 几何体出错: {e}") - - def _create_placeholder_geometry(self, parent_node): - """创建一个简单的占位符几何体,让用户能看到节点""" - try: - from panda3d.core import GeomVertexFormat, GeomVertexData, GeomVertexWriter - from panda3d.core import Geom, GeomTriangles, GeomNode - - # 创建简单的立方体作为占位符 - format = GeomVertexFormat.getV3n3c4() - vdata = GeomVertexData('placeholder_cube', format, Geom.UHStatic) - - vertex = GeomVertexWriter(vdata, 'vertex') - normal = GeomVertexWriter(vdata, 'normal') - color = GeomVertexWriter(vdata, 'color') - - # 定义立方体顶点 - size = 1.0 - vertices = [ - # 前面 (Z+) - (-size, -size, size), (size, -size, size), (size, size, size), (-size, size, size), - # 后面 (Z-) - (-size, -size, -size), (-size, size, -size), (size, size, -size), (size, -size, -size), - # 左面 (X-) - (-size, -size, -size), (-size, -size, size), (-size, size, size), (-size, size, -size), - # 右面 (X+) - (size, -size, -size), (size, size, -size), (size, size, size), (size, -size, size), - # 上面 (Y+) - (-size, size, -size), (-size, size, size), (size, size, size), (size, size, -size), - # 下面 (Y-) - (-size, -size, -size), (size, -size, -size), (size, -size, size), (-size, -size, size) - ] - - normals = [ - # 前面法线 - (0, 0, 1), (0, 0, 1), (0, 0, 1), (0, 0, 1), - # 后面法线 - (0, 0, -1), (0, 0, -1), (0, 0, -1), (0, 0, -1), - # 左面法线 - (-1, 0, 0), (-1, 0, 0), (-1, 0, 0), (-1, 0, 0), - # 右面法线 - (1, 0, 0), (1, 0, 0), (1, 0, 0), (1, 0, 0), - # 上面法线 - (0, 1, 0), (0, 1, 0), (0, 1, 0), (0, 1, 0), - # 下面法线 - (0, -1, 0), (0, -1, 0), (0, -1, 0), (0, -1, 0) - ] - - # 青色 - face_colors = [ - (0.0, 1.0, 1.0, 1.0), (0.0, 1.0, 1.0, 1.0), (0.0, 1.0, 1.0, 1.0), (0.0, 1.0, 1.0, 1.0), # 前面 - 青色 - (0.0, 0.8, 0.8, 1.0), (0.0, 0.8, 0.8, 1.0), (0.0, 0.8, 0.8, 1.0), (0.0, 0.8, 0.8, 1.0), # 后面 - 稍暗青色 - (0.0, 0.9, 0.9, 1.0), (0.0, 0.9, 0.9, 1.0), (0.0, 0.9, 0.9, 1.0), (0.0, 0.9, 0.9, 1.0), # 左面 - 中等青色 - (0.0, 0.7, 0.7, 1.0), (0.0, 0.7, 0.7, 1.0), (0.0, 0.7, 0.7, 1.0), (0.0, 0.7, 0.7, 1.0), # 右面 - 稍暗青色 - (0.0, 1.0, 1.0, 1.0), (0.0, 1.0, 1.0, 1.0), (0.0, 1.0, 1.0, 1.0), (0.0, 1.0, 1.0, 1.0), # 上面 - 青色 - (0.0, 0.6, 0.6, 1.0), (0.0, 0.6, 0.6, 1.0), (0.0, 0.6, 0.6, 1.0), (0.0, 0.6, 0.6, 1.0) # 下面 - 更暗青色 - ] - - for i, vert in enumerate(vertices): - vertex.addData3f(*vert) - normal.addData3f(*normals[i]) - color.addData4f(*face_colors[i]) - - # 创建几何体 - geom = Geom(vdata) - - # 创建面(每个面两个三角形) - prim = GeomTriangles(Geom.UHStatic) - - # 每个面4个顶点,生成2个三角形 - for face in range(6): # 6个面 - base_index = face * 4 - # 第一个三角形 - prim.addVertices(base_index, base_index + 1, base_index + 2) - # 第二个三角形 - prim.addVertices(base_index, base_index + 2, base_index + 3) - - prim.closePrimitive() - geom.addPrimitive(prim) - - # 创建几何节点 - geom_node = GeomNode('tileset_placeholder') - geom_node.addGeom(geom) - - # 添加到场景 - cube_node = parent_node.attachNewNode(geom_node) - cube_node.setScale(5) # 设置合适的大小 - - # 设置双面渲染 - cube_node.setTwoSided(True) - - # 添加材质 - material = Material() - material.setBaseColor((0.0, 1.0, 1.0, 1.0)) # 青色 - material.setSpecular((0.5, 0.5, 0.5, 1.0)) - material.setShininess(32.0) - cube_node.setMaterial(material) - - # 添加标识标签 - cube_node.setTag("element_type", "cesium_placeholder") - - print("✓ 占位符几何体创建完成") - return cube_node - except Exception as e: - print(f"✗ 创建占位符几何体出错: {e}") - import traceback - traceback.print_exc() - return None - - - def serializeNode(self, node): - """序列化节点为字典数据""" - try: - node_data = { - 'name': node.getName(), - 'type': type(node.node()).__name__, - 'pos': (node.getX(), node.getY(), node.getZ()), - 'hpr': (node.getH(), node.getP(), node.getR()), - 'scale': (node.getSx(), node.getSy(), node.getSz()), - 'tags': {}, - 'children': [] - } - - # 保存所有标签 - for tag_key in node.getTagKeys(): - node_data['tags'][tag_key] = node.getTag(tag_key) - - # 特殊处理不同类型的节点 - if hasattr(node.node(), 'getClassType'): - node_class = node.node().getClassType().getName() - node_data['node_class'] = node_class - - # 递归序列化子节点 - for child in node.getChildren(): - # 跳过辅助节点 - if not child.getName().startswith(('gizmo', 'selectionBox', 'grid')): - child_data = self.serializeNode(child) - if child_data: - node_data['children'].append(child_data) - - return node_data - - except Exception as e: - print(f"序列化节点 {node.getName()} 失败: {e}") - import traceback - traceback.print_exc() - return None - - def deserializeNode(self, node_data, parent_node): - """从字典数据反序列化节点""" - try: - # 创建新节点 - node_name = node_data.get('name', 'node') - new_node = parent_node.attachNewNode(node_name) - - # 设置变换 - pos = node_data.get('pos', (0, 0, 0)) - hpr = node_data.get('hpr', (0, 0, 0)) - scale = node_data.get('scale', (1, 1, 1)) - - new_node.setPos(*pos) - new_node.setHpr(*hpr) - new_node.setScale(*scale) - - # 恢复标签 - for tag_key, tag_value in node_data.get('tags', {}).items(): - new_node.setTag(tag_key, tag_value) - - # 根据节点类型进行特殊处理 - node_type = node_data.get('type', '') - node_class = node_data.get('node_class', '') - - # 特殊处理光源节点 - if 'light_type' in node_data.get('tags', {}): - light_type = node_data['tags']['light_type'] - if light_type == 'spot_light': - self._recreateSpotLight(new_node) - elif light_type == 'point_light': - self._recreatePointLight(new_node) - - # 递归创建子节点 - for child_data in node_data.get('children', []): - self.deserializeNode(child_data, new_node) - - return new_node - - except Exception as e: - print(f"反序列化节点 {node_data.get('name', 'unknown')} 失败: {e}") - import traceback - traceback.print_exc() - return None - - def serializeNodeForCopy(self, node): - """序列化节点用于复制操作,完整保存视觉属性""" - try: - if not node or node.isEmpty(): - return None - - node_data = { - 'name': node.getName(), - 'type': type(node.node()).__name__, - 'pos': (node.getX(), node.getY(), node.getZ()), - 'hpr': (node.getH(), node.getP(), node.getR()), - 'scale': (node.getSx(), node.getSy(), node.getSz()), - 'tags': {}, - 'children': [] - } - - # 保存所有标签 - try: - if hasattr(node, 'getTagKeys'): - for tag_key in node.getTagKeys(): - node_data['tags'][tag_key] = node.getTag(tag_key) - except Exception as e: - print(f"获取标签时出错: {e}") - - # 保存视觉属性 - try: - # 保存颜色属性 - if hasattr(node, 'getColor'): - color = node.getColor() - node_data['color'] = (color.getX(), color.getY(), color.getZ(), color.getW()) - - # 保存材质属性 - if hasattr(node, 'getMaterial'): - material = node.getMaterial() - if material: - material_data = {} - material_data['base_color'] = ( - material.getBaseColor().getX(), - material.getBaseColor().getY(), - material.getBaseColor().getZ(), - material.getBaseColor().getW() - ) - material_data['ambient'] = ( - material.getAmbient().getX(), - material.getAmbient().getY(), - material.getAmbient().getZ(), - material.getAmbient().getW() - ) - material_data['diffuse'] = ( - material.getDiffuse().getX(), - material.getDiffuse().getY(), - material.getDiffuse().getZ(), - material.getDiffuse().getW() - ) - material_data['specular'] = ( - material.getSpecular().getX(), - material.getSpecular().getY(), - material.getSpecular().getZ(), - material.getSpecular().getW() - ) - material_data['shininess'] = material.getShininess() - node_data['material'] = material_data - - except Exception as e: - print(f"保存视觉属性时出错: {e}") - - # 根据节点类型保存特定信息 - if node.hasTag("tree_item_type"): - node_type = node.getTag("tree_item_type") - node_data['node_type'] = node_type - - # 保存特定类型节点的额外信息 - if node_type in ["LIGHT_NODE", "SPOT_LIGHT_NODE", "POINT_LIGHT_NODE"]: - # 保存光源特定信息 - rp_light = node.getPythonTag("rp_light_object") - if rp_light: - node_data['light_data'] = { - 'energy': getattr(rp_light, 'energy', 5000), - 'radius': getattr(rp_light, 'radius', 1000), - 'fov': getattr(rp_light, 'fov', 70) if hasattr(rp_light, 'fov') else None, - 'inner_radius': getattr(rp_light, 'inner_radius', 0.4) if hasattr(rp_light, - 'inner_radius') else None, - 'casts_shadows': getattr(rp_light, 'casts_shadows', True) if hasattr(rp_light, - 'casts_shadows') else True, - 'shadow_map_resolution': getattr(rp_light, 'shadow_map_resolution', 256) if hasattr( - rp_light, 'shadow_map_resolution') else 256 - } - elif node_type in ["GUI_BUTTON", "GUI_LABEL", "GUI_ENTRY", "GUI_IMAGE", - "GUI_3D_TEXT", "GUI_3D_IMAGE", "GUI_VIRTUAL_SCREEN"]: - # 保存GUI元素特定信息 - node_data['gui_data'] = self._serializeGUIData(node) - elif node_type == "IMPORTED_MODEL_NODE": - # 保存模型特定信息 - node_data['model_data'] = self._serializeModelData(node) - - return node_data - - except Exception as e: - print(f"序列化节点失败: {e}") - import traceback - traceback.print_exc() - return None - - def _serializeGUIData(self, node): - """序列化GUI元素数据""" - try: - gui_data = {} - - # 保存GUI相关的通用属性 - if node.hasTag("gui_type"): - gui_data['gui_type'] = node.getTag("gui_type") - - # 保存文本内容(如果有的话) - if node.hasTag("text"): - gui_data['text'] = node.getTag("text") - - # 保存其他GUI相关标签 - gui_tags = ['font', 'font_size', 'text_color', 'bg_color', 'size'] - for tag in gui_tags: - if node.hasTag(tag): - gui_data[tag] = node.getTag(tag) - - return gui_data - except Exception as e: - print(f"序列化GUI数据失败: {e}") - return {} - - def _serializeModelData(self, node): - """序列化模型数据,包括材质信息""" - try: - model_data = {} - - # 保存模型相关的标签 - model_tags = ['model_path', 'file', 'element_type'] - for tag in model_tags: - if node.hasTag(tag): - model_data[tag] = node.getTag(tag) - - # 保存材质信息 - try: - # 获取模型的材质信息 - if hasattr(node, 'getState'): - state = node.getState() - if state: - # 保存基础颜色信息 - from panda3d.core import ColorAttrib - color_attrib = state.getColor() - if color_attrib: - model_data['base_color'] = ( - color_attrib.getColor().getX(), - color_attrib.getColor().getY(), - color_attrib.getColor().getZ(), - color_attrib.getColor().getW() - ) - - # 保存其他材质属性 - from panda3d.core import MaterialAttrib - material_attrib = state.getAttrib(MaterialAttrib.getClassType()) - if material_attrib: - material = material_attrib.getMaterial() - if material: - # 保存基础颜色 - base_color = material.getBaseColor() - model_data['material_base_color'] = ( - base_color.getX(), base_color.getY(), base_color.getZ(), base_color.getW() - ) - - # 保存环境光颜色 - ambient_color = material.getAmbient() - model_data['material_ambient_color'] = ( - ambient_color.getX(), ambient_color.getY(), ambient_color.getZ(), - ambient_color.getW() - ) - - # 保存漫反射颜色 - diffuse_color = material.getDiffuse() - model_data['material_diffuse_color'] = ( - diffuse_color.getX(), diffuse_color.getY(), diffuse_color.getZ(), - diffuse_color.getW() - ) - - # 保存高光颜色 - specular_color = material.getSpecular() - model_data['material_specular_color'] = ( - specular_color.getX(), specular_color.getY(), specular_color.getZ(), - specular_color.getW() - ) - - # 保存粗糙度和金属度等参数 - model_data['material_roughness'] = material.getRoughness() - model_data['material metallic'] = material.getMetallic() - - except Exception as e: - print(f"保存材质信息时出错: {e}") - - return model_data - except Exception as e: - print(f"序列化模型数据失败: {e}") - return {} - - def recreateNodeFromData(self, node_data, parent_node): - """根据数据重建节点,并确保在场景树中显示""" - try: - if not node_data or not parent_node or parent_node.isEmpty(): - return None - - print(f"正在重建节点 {node_data}") - node_type = node_data.get('node_type', '') - original_name = node_data.get('name', 'node') - - # 生成唯一名称 - unique_name = self._generateUniqueName(original_name, parent_node) - - # 根据节点类型调用相应的重建方法 - new_node = None - if node_type in ["LIGHT_NODE", "SPOT_LIGHT_NODE", "POINT_LIGHT_NODE"]: - new_node = self._recreateLightFromData(node_data, parent_node, unique_name) - elif node_type == "CESIUM_TILESET_NODE": - new_node = self._recreateTilesetFromData(node_data, parent_node, unique_name) - elif node_type in ["GUI_BUTTON", "GUI_LABEL", "GUI_ENTRY", "GUI_IMAGE", - "GUI_3DTEXT", "GUI_3DIMAGE", "GUI_VIDEO_SCREEN","GUI_2D_VIDEO_SCREEN"]: - new_node = self._recreateGUIFromData(node_data, parent_node, unique_name) - elif node_type == "IMPORTED_MODEL_NODE": - new_node = self._recreateModelFromData(node_data, parent_node, unique_name) - else: - # 创建普通节点 - new_node = self._createBasicNodeFromData(node_data, parent_node, unique_name) - - # 如果成功创建节点,确保它在场景树中显示 - if new_node: - # 尝试更新场景树以显示新节点 - try: - if hasattr(self.world, 'interface_manager') and self.world.interface_manager: - # 查找父节点在场景树中的对应项 - parent_item = self._findTreeItemForNode(parent_node) - if parent_item: - # 添加新节点到场景树 - tree_widget = self.world.interface_manager.treeWidget - if tree_widget: - tree_widget.add_node_to_tree_widget(new_node, parent_item, node_type or "NODE") - except Exception as e: - print(f"添加节点到场景树时出错: {e}") - - return new_node - - except Exception as e: - print(f"重建节点失败: {e}") - import traceback - traceback.print_exc() - return None - - def _findTreeItemForNode(self, node): - """根据节点查找对应的场景树项""" - try: - if hasattr(self.world, 'interface_manager') and self.world.interface_manager: - tree_widget = self.world.interface_manager.treeWidget - if tree_widget: - # 遍历场景树查找匹配的节点项 - for i in range(tree_widget.topLevelItemCount()): - item = tree_widget.topLevelItem(i) - result = self._findTreeItemForNodeRecursive(item, node) - if result: - return result - return None - except Exception as e: - print(f"查找场景树项时出错: {e}") - return None - - def _findTreeItemForNodeRecursive(self, item, target_node): - """递归查找场景树项""" - try: - # 检查当前项是否匹配 - item_node = getattr(item, 'node_path', None) - if not item_node: - item_node = getattr(item, 'node', None) - - if item_node and item_node == target_node: - return item - - # 递归检查子项 - for i in range(item.childCount()): - child_item = item.child(i) - result = self._findTreeItemForNodeRecursive(child_item, target_node) - if result: - return result - - return None - except Exception as e: - print(f"递归查找场景树项时出错: {e}") - return None - - def _recreateLightFromData(self, node_data, parent_node, name): - """根据数据重建光源""" - try: - light_type = node_data.get('tags', {}).get('light_type', 'spot_light') - - # 创建光源 - if light_type == 'spot_light': - light_node = self.createSpotLight(pos=node_data.get('pos', (0, 0, 0))) - else: # point_light - light_node = self.createPointLight(pos=node_data.get('pos', (0, 0, 0))) - - if light_node: - # 设置名称 - light_node.setName(name) - - # 恢复其他属性 - light_data = node_data.get('light_data', {}) - rp_light = light_node.getPythonTag("rp_light_object") - if rp_light and light_data: - if 'energy' in light_data: - rp_light.energy = light_data['energy'] - if 'radius' in light_data: - rp_light.radius = light_data['radius'] - if 'fov' in light_data and hasattr(rp_light, 'fov'): - rp_light.fov = light_data['fov'] - if 'inner_radius' in light_data and hasattr(rp_light, 'inner_radius'): - rp_light.inner_radius = light_data['inner_radius'] - if 'casts_shadows' in light_data and hasattr(rp_light, 'casts_shadows'): - rp_light.casts_shadows = light_data['casts_shadows'] - if 'shadow_map_resolution' in light_data and hasattr(rp_light, 'shadow_map_resolution'): - rp_light.shadow_map_resolution = light_data['shadow_map_resolution'] - - # 恢复其他标签 - for tag_key, tag_value in node_data.get('tags', {}).items(): - if tag_key not in ['name', 'light_type']: - light_node.setTag(tag_key, str(tag_value)) - - return light_node - - except Exception as e: - print(f"重建光源失败: {e}") - return None - - def _recreateTilesetFromData(self, node_data, parent_node, name): - """根据数据重建Tileset""" - try: - tileset_url = node_data.get('tileset_url', '') - if not tileset_url: - return None - - # 使用现有方法加载tileset - position = node_data.get('pos', (0, 0, 0)) - tileset_node = self.load_cesium_tileset(tileset_url, position) - - if tileset_node: - # 设置名称 - tileset_node.setName(name) - - # 恢复其他标签 - for tag_key, tag_value in node_data.get('tags', {}).items(): - if tag_key not in ['name']: - tileset_node.setTag(tag_key, str(tag_value)) - - return tileset_node - - except Exception as e: - print(f"重建Tileset失败: {e}") - return None - - def _recreateGUIFromData(self, node_data, parent_node, name): - """根据数据重建GUI元素""" - try: - gui_data = node_data.get('gui_data', {}) - #gui_type = gui_data.get('gui_type', '') - gui_type = node_data.get("tags").get("gui_type", "") - - print(f"正在重建GUI元素: {gui_type}") - print(f"正在重建GUI元素: {node_data}") - - # 根据GUI类型调用相应的创建方法 - new_gui_element = None - - if gui_type == "button" and hasattr(self.world, 'createGUIButton'): - pos = node_data.get('pos', (0, 0, 0)) - text = node_data.get('tags').get('gui_text', '') - size = node_data.get('scale', 1) - print(pos,text,size) - new_gui_element = self.world.createGUIButton(pos,text,size) - elif gui_type == "label" and hasattr(self.world, 'createGUILabel'): - pos = node_data.get('pos', (0, 0, 0)) - text = node_data.get('tags').get('gui_text', '') - size = node_data.get('scale', 1) - new_gui_element = self.world.createGUILabel(pos,text,size) - elif gui_type == "entry" and hasattr(self.world, 'createGUIEntry'): - pos = node_data.get('pos', (0, 0, 0)) - text = node_data.get('tags').get('gui_text', '') - size = node_data.get('scale', 1) - new_gui_element = self.world.createGUIEntry(pos,text,size) - elif gui_type == "2d_image" and hasattr(self.world, 'createGUI2DImage'): - pos = node_data.get('pos', (0, 0, 0)) - image_path = node_data.get('tags').get('image_path', '') - size = node_data.get('size', 1) - new_gui_element = self.world.createGUI2DImage(pos, image_path, size) - elif gui_type == "3d_text" and hasattr(self.world, 'createGUI3DText'): - print("正在创建3D文本!!!") - pos = node_data.get('pos', (0, 0, 0)) - text = node_data.get('tags', {}).get('gui_text', '') - scale = node_data.get('scale', 1) - if isinstance(scale, (list, tuple)): - scale = scale[0] if len(scale) > 0 else 1 - print(f"正在创建3D文本: 位置={pos}, 文本={text}, 大小={scale}") - new_gui_element = self.world.createGUI3DText(pos, text, scale) - elif gui_type == "3d_image" and hasattr(self.world, 'createGUI3DImage'): - pos = node_data.get('pos', (0, 0, 0)) - image_path = node_data.get('tags').get('gui_image_path', '') - scale = node_data.get('scale', (1, 1)) - if isinstance(scale, (int, float)): - scale = (scale, scale) - elif isinstance(scale, (list, tuple)) and len(scale) >= 2: - scale = (scale[0], scale[1]) - else: - scale = (1, 1) - print(f"正在创建3D图片: 位置={pos}, 路径={image_path}, 大小={scale}") - new_gui_element = self.world.gui_manager.createGUI3DImage(pos, image_path, scale) - elif gui_type == "video_screen" and hasattr(self.world.gui_manager, 'createVideoScreen'): - pos = node_data.get('pos', (0, 0, 0)) - video_path = node_data.get('tags').get('video_path', '') - scale = node_data.get('scale', (1, 1,1)) - new_gui_element = self.world.gui_manager.createVideoScreen(pos,scale,video_path) - elif gui_type == "2d_video_screen" and hasattr(self.world.gui_manager, 'createGUI2DVideoScreen'): - pos = node_data.get('pos', (0, 0, 0)) - video_path = node_data.get('tags').get('video_path', '') - scale = node_data.get('scale', (1, 1, 1)) - new_gui_element = self.world.gui_manager.createGUI2DVideoScreen(pos,scale,video_path) - - if new_gui_element: - # 设置名称和变换 - if hasattr(new_gui_element, 'setName'): - new_gui_element.setName(name) - - # 设置位置、旋转、缩放 - pos = node_data.get('pos', (0, 0, 0)) - hpr = node_data.get('hpr', (0, 0, 0)) - scale = node_data.get('scale', (1, 1, 1)) - - if hasattr(new_gui_element, 'setPos'): - new_gui_element.setPos(*pos) - if hasattr(new_gui_element, 'setHpr'): - new_gui_element.setHpr(*hpr) - if hasattr(new_gui_element, 'setScale'): - new_gui_element.setScale(*scale) - - # 恢复文本内容 - if 'text' in gui_data and hasattr(new_gui_element, 'setText'): - new_gui_element.setText(gui_data['text']) - - # 恢复其他标签 - for tag_key, tag_value in node_data.get('tags', {}).items(): - if hasattr(new_gui_element, 'setTag') and tag_key not in ['name']: - new_gui_element.setTag(tag_key, str(tag_value)) - - print(f"GUI元素重建成功: {name}") - - return new_gui_element - - except Exception as e: - print(f"重建GUI元素失败: {e}") - import traceback - traceback.print_exc() - return None - - def _recreateModelFromData(self, node_data, parent_node, name): - """根据数据重建模型,保持材质""" - try: - model_data = node_data.get('model_data', {}) - model_path = model_data.get('model_path', model_data.get('file', '')) - - if not model_path or not os.path.exists(model_path): - # 如果原始模型文件不存在,创建一个基本节点 - return self._createBasicNodeFromData(node_data, parent_node, name) - - # 导入模型,保持原有参数 - model = self.importModel( - model_path, - apply_unit_conversion=False, # 已经处理过的模型不需要再次转换 - normalize_scales=False, # 保持原有缩放 - auto_convert_to_glb=False # 已经处理过的模型不需要再次转换 - ) - - if model: - # 设置名称 - model.setName(name) - - # 设置变换 - pos = node_data.get('pos', (0, 0, 0)) - hpr = node_data.get('hpr', (0, 0, 0)) - scale = node_data.get('scale', (1, 1, 1)) - - model.setPos(*pos) - model.setHpr(*hpr) - model.setScale(*scale) - - # 恢复材质信息 - try: - self._restoreModelMaterial(model, model_data) - except Exception as e: - print(f"恢复模型材质时出错: {e}") - - # 恢复标签 - for tag_key, tag_value in node_data.get('tags', {}).items(): - if tag_key not in ['name']: - model.setTag(tag_key, str(tag_value)) - - # 添加到模型列表 - if model not in self.models: - self.models.append(model) - - return model - - except Exception as e: - print(f"重建模型失败: {e}") - # 出错时创建基本节点 - return self._createBasicNodeFromData(node_data, parent_node, name) - - def _restoreModelMaterial(self, model, model_data): - """恢复模型材质""" - try: - # 恢复基础颜色 - if 'base_color' in model_data: - from panda3d.core import ColorAttrib - base_color = model_data['base_color'] - color = (base_color[0], base_color[1], base_color[2], base_color[3]) - model.setColor(color) - - # 恢复复杂材质属性 - if any(key.startswith('material_') for key in model_data.keys()): - from panda3d.core import Material - - # 创建新材质或获取现有材质 - material = Material() - - # 恢复基础颜色 - if 'material_base_color' in model_data: - base_color = model_data['material_base_color'] - material.setBaseColor((base_color[0], base_color[1], base_color[2], base_color[3])) - - # 恢复环境光颜色 - if 'material_ambient_color' in model_data: - ambient_color = model_data['material_ambient_color'] - material.setAmbient((ambient_color[0], ambient_color[1], ambient_color[2], ambient_color[3])) - - # 恢复漫反射颜色 - if 'material_diffuse_color' in model_data: - diffuse_color = model_data['material_diffuse_color'] - material.setDiffuse((diffuse_color[0], diffuse_color[1], diffuse_color[2], diffuse_color[3])) - - # 恢复高光颜色 - if 'material_specular_color' in model_data: - specular_color = model_data['material_specular_color'] - material.setSpecular((specular_color[0], specular_color[1], specular_color[2], specular_color[3])) - - # 恢复粗糙度和金属度 - if 'material_roughness' in model_data: - material.setRoughness(model_data['material_roughness']) - - if 'material_metallic' in model_data: - material.setMetallic(model_data['material_metallic']) - - # 应用材质到模型 - model.setMaterial(material) - - except Exception as e: - print(f"恢复材质失败: {e}") - - def _createBasicNodeFromData(self, node_data, parent_node, name): - """创建基本节点,保持视觉属性""" - try: - new_node = parent_node.attachNewNode(name) - - # 设置变换 - pos = node_data.get('pos', (0, 0, 0)) - hpr = node_data.get('hpr', (0, 0, 0)) - scale = node_data.get('scale', (1, 1, 1)) - - new_node.setPos(*pos) - new_node.setHpr(*hpr) - new_node.setScale(*scale) - - # 恢复视觉属性 - try: - # 恢复颜色 - if 'color' in node_data: - color_data = node_data['color'] - new_node.setColor(color_data[0], color_data[1], color_data[2], color_data[3]) - - # 恢复材质 - if 'material' in node_data: - from panda3d.core import Material - material_data = node_data['material'] - material = Material() - - if 'base_color' in material_data: - bc = material_data['base_color'] - material.setBaseColor((bc[0], bc[1], bc[2], bc[3])) - - if 'ambient' in material_data: - ac = material_data['ambient'] - material.setAmbient((ac[0], ac[1], ac[2], ac[3])) - - if 'diffuse' in material_data: - dc = material_data['diffuse'] - material.setDiffuse((dc[0], dc[1], dc[2], dc[3])) - - if 'specular' in material_data: - sc = material_data['specular'] - material.setSpecular((sc[0], sc[1], sc[2], sc[3])) - - if 'shininess' in material_data: - material.setShininess(material_data['shininess']) - - new_node.setMaterial(material) - - except Exception as e: - print(f"恢复视觉属性时出错: {e}") - - # 恢复标签 - for tag_key, tag_value in node_data.get('tags', {}).items(): - if tag_key not in ['name']: - new_node.setTag(tag_key, str(tag_value)) - - return new_node - - except Exception as e: - print(f"创建基本节点失败: {e}") - return None - - def _generateUniqueName(self, base_name, parent_node): - """生成唯一节点名称""" - try: - # 移除可能的数字后缀 - import re - import time - name_base = re.sub(r'_\d+$', '', base_name) - - # 查找现有同名节点 - counter = 1 - unique_name = base_name - while True: - # 检查父节点下是否已存在同名子节点 - existing_node = parent_node.find(unique_name) - if existing_node.isEmpty(): - break - unique_name = f"{name_base}_{counter}" - counter += 1 - if counter > 1000: # 防止无限循环 - unique_name = f"{name_base}_{int(time.time())}" - break - - return unique_name - except: - return f"{base_name}_{int(time.time())}" - - +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +场景管理器 - 负责场景和模型管理的核心功能 +处理模型导入、场景树构建、材质系统、碰撞设置等 +""" + +import os +import shutil +import time + +from PyQt5.QtCore import Qt +from PyQt5.QtWidgets import QTreeWidgetItem +from panda3d.core import ( + ModelPool, ModelRoot, Filename, NodePath, GeomNode, Material, Vec4, Vec3, + MaterialAttrib, ColorAttrib, Point3, CollisionNode, CollisionSphere, CollisionBox, + BitMask32, TransparencyAttrib, LColor, TransformState, RenderModeAttrib +) +import json +import aiohttp +import asyncio +import inspect +from pathlib import Path +from panda3d.egg import EggData, EggVertexPool +from direct.actor.Actor import Actor +from QMeta3D.Meta3DWorld import get_render_pipeline +from RenderPipelineFile.rpplugins.smaa.jitters import halton_seq +from scene import util + +class CesiumIntegration: + def __init__(self, scene_manager): + self.scene_manager = scene_manager + self.world = scene_manager.world + self.tilesets = {} + + def add_tileset(self,name,url,position=(0,0,0)): + try: + tileset_node = self.scene_manager.load_cesium_tileset(url,position) + + if tileset_node: + self.tilesets[name] = { + 'node':tileset_node, + 'url':url, + 'position':position + } + print(f"✓ 添加 Cesium tileset: {name}") + return tileset_node + else: + print(f"✗ 添加 Cesium tileset 失败: {name}") + return None + except Exception as e: + print(f"✗ 添加 Cesium tileset 出错: {e}") + return None + + def remove_tileset(self, name): + """移除 tileset""" + if name in self.tilesets: + tileset_info = self.tilesets[name] + tileset_info['node'].removeNode() + del self.tilesets[name] + print(f"✓ 移除 Cesium tileset: {name}") + return True + return False + + def get_tileset(self, name): + """获取 tileset""" + return self.tilesets.get(name, None) + + def list_tilesets(self): + """列出所有 tilesets""" + return list(self.tilesets.keys()) + +class SceneManager: + """场景管理器 - 统一管理场景中的所有元素""" + + def __init__(self, world): + """初始化场景管理器 + + Args: + world: 主程序world对象引用 + """ + self.world = world + self.models = [] # 模型列表 + + self.Spotlight = [] + self.Pointlight = [] + + self.tilesets = [] #来存储tilesets + self.cesium_integration = CesiumIntegration(self) + + print("✓ 场景管理系统初始化完成") + + # ==================== 模型导入和处理 ==================== + + def importModel(self, filepath, apply_unit_conversion=False, normalize_scales=True, auto_convert_to_glb=True): + try: + if not os.path.exists(filepath): + print("文件不存在") + return None + + filepath = util.normalize_model_path(filepath) + original_filepath = filepath + + # # 在加载前设置忽略未知属性 + # from panda3d.core import ConfigVariableBool + # ConfigVariableBool("model-cache-ignore-unknown-properties").setValue(True) + # + # # 清除可能存在的模型缓存 + # from panda3d.core import ModelPool + # ModelPool.releaseAllModels() + # + # # 检查是否需要转换为GLB以获得更好的动画支持 + # if auto_convert_to_glb and self._shouldConvertToGLB(filepath): + # print(f"🔄 检测到需要转换的格式,尝试转换为GLB...") + # converted_path = self._convertToGLBWithProgress(filepath) + # if converted_path: + # print(f"✅ 转换成功: {converted_path}") + # filepath = converted_path + # # 转换成功的消息已在控制台显示,不再弹窗提示 + # else: + # print(f"⚠️ 转换失败,使用原始文件") + + model = self.world.loader.loadModel(filepath) + if not model: + print("加载模型失败") + return None + + # 设置模型名称 + model_name = os.path.basename(filepath) + # 确保名称有效 + if not model_name: + model_name = "imported_model" + model.setName(model_name) + # 将模型添加到场景 + model.reparentTo(self.world.render) + + # 设置模型名称 + model_name = os.path.basename(filepath) + model.setName(model_name) + + + # 保存原始路径和转换后的路径,处理跨平台路径问题 + # 确保路径在当前系统上有效 + normalized_filepath = filepath + # 检查路径是否有效,如果无效则尝试修复 + if not os.path.exists(filepath): + original_filepath = filepath + # 尝试多种修复策略 + fixed = False + + # 策略1: 处理Linux风格路径在Windows上的问题 + if filepath.startswith('/') and ':' not in filepath: + # 提取文件名并尝试在当前目录查找 + filename = os.path.basename(filepath) + potential_path = os.path.join(os.getcwd(), filename) + if os.path.exists(potential_path): + normalized_filepath = potential_path + fixed = True + + # 策略2: 处理路径分隔符问题 + if not fixed: + # 尝试规范化路径 + normalized_path = os.path.normpath(filepath) + if os.path.exists(normalized_path): + normalized_filepath = normalized_path + fixed = True + + # 策略3: 在Resources目录中查找 + if not fixed: + # 尝试在Resources目录中查找文件 + resources_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), "Resources", "models") + potential_path = os.path.join(resources_path, filename) + if os.path.exists(potential_path): + normalized_filepath = potential_path + fixed = True + + if fixed: + print(f"路径修复: {original_filepath} -> {normalized_filepath}") + else: + print(f"[警告] 模型文件不存在: {filepath},将尝试继续加载") + # 即使文件不存在,也保存路径,以便属性面板可以尝试修复它 + + model.setTag("model_path", normalized_filepath) + model.setTag("original_path", original_filepath) + if normalized_filepath != original_filepath: + model.setTag("converted_from", os.path.splitext(original_filepath)[1]) + model.setTag("converted_to_glb", "true") + + #特殊处理FBX模型 + if filepath.lower().endswith('.fbx'): + print("检测到FBX模型,应用特殊处理...") + + # 将模型缩放设置为原来的1/100 + model.setScale(0.01) + print("设置模型缩放为 0.01 (原始大小的1/100)") + + # 设置模型旋转为 (0, 90, 0) + model.setHpr(0, 90, 0) + print("设置模型旋转为 (0, 90, 0)") + + # 调整模型位置到地面 + model.setPos(0,0,0) + #self._adjustModelToGround(model) + + # 创建并设置基础材质 + #print("\n=== 开始设置材质 ===") + #self._applyMaterialsToModel(model) + + # 设置碰撞检测(重要!用于选择功能) + print("\n=== 设置碰撞检测 ===") + self.setupCollision(model) + + # 添加文件标签用于保存/加载 + model.setTag("file", model_name) + model.setTag("is_model_root", "1") + model.setTag("is_scene_element", "1") + model.setTag("tree_item_type", "IMPORTED_MODEL_NODE") + + # 记录应用的处理选项 + if apply_unit_conversion: + model.setTag("unit_conversion_applied", "true") + if normalize_scales: + model.setTag("scale_normalization_applied", "true") + + # 添加到模型列表 + self.models.append(model) + + # 更新场景树 + # 获取树形控件并添加到Qt树中 + tree_widget = self._get_tree_widget() + if tree_widget: + # 找到根节点项 + root_item = None + for i in range(tree_widget.topLevelItemCount()): + item = tree_widget.topLevelItem(i) + if item.text(0) == "render" or item.data(0, Qt.UserRole) == self.world.render: + root_item = item + break + + if root_item: + qt_item = tree_widget.add_node_to_tree_widget(model, root_item, "IMPORTED_MODEL_NODE") + if qt_item: + #tree_widget.setCurrentItem(qt_item) + # 更新选择和属性面板 + #tree_widget.update_selection_and_properties(model, qt_item) + print("✅ Qt树节点添加成功") + else: + print("⚠️ Qt树节点添加失败,但Panda3D对象已创建") + else: + print("⚠️ 未找到根节点项,无法添加到Qt树") + #self.updateSceneTree() + + print(f"=== 模型导入成功: {model_name} ===\n") + return model + + except Exception as e: + print(f"导入模型失败: {str(e)}") + return None + + def _fixModelStructure(self, model): + """修复模型结构""" + try: + # 使用正确的方式查找动画相关节点 + character_nodes = model.findAllMatches("**/+Character") + anim_bundle_nodes = model.findAllMatches("**/+AnimBundleNode") + + if character_nodes.getNumPaths() > 0 or anim_bundle_nodes.getNumPaths() > 0: + print(f"检测到模型{model.getName()}包含角色相节点:") + if character_nodes.getNumPaths() > 0: + print(f"CharacterNode数量:{character_nodes.getNumPaths()}") + if anim_bundle_nodes.getNumPaths() > 0: + print(f"AnimBundleNode数量: {anim_bundle_nodes.getNumPaths()}") + + model.setTag("fixed_structure", "true") + return True + except Exception as e: + print(f"修复模型结构时出错: {e}") + return False + + def _validateAndFixAllTransforms(self, model): + """递归验证并修复模型中所有节点的变换矩阵""" + try: + fixed_count = 0 + + # 先处理根节点 + if not self._validateAndFixTransform(model): + fixed_count += 1 + + # 递归处理所有子节点 + def process_children(node, depth=0): + nonlocal fixed_count + for i in range(node.getNumChildren()): + try: + child = node.getChild(i) + if not self._validateAndFixTransform(child): + fixed_count += 1 + # 递归处理孙节点 + process_children(child, depth + 1) + except Exception as e: + print(f"处理子节点时出错 (深度 {depth}): {e}") + continue + + process_children(model) + + if fixed_count > 0: + print(f"共修复了 {fixed_count} 个节点的变换") + + return True + except Exception as e: + print(f"验证所有变换时出错: {e}") + return False + + def _validateAndFixTransform(self, node_path): + """验证并修复单个节点的变换矩阵""" + try: + node_name = node_path.getName() + + # 获取当前变换状态 + original_pos = node_path.getPos() + original_hpr = node_path.getHpr() + original_scale = node_path.getScale() + + # 检查位置是否包含无效值 + if not original_pos.isFinite(): + print(f"警告: 节点 {node_name} 位置包含无效值 {original_pos},重置为 (0,0,0)") + node_path.setPos(0, 0, 0) + return False + + # 检查旋转是否包含无效值 + if not original_hpr.isFinite(): + print(f"警告: 节点 {node_name} 旋转包含无效值 {original_hpr},重置为 (0,0,0)") + node_path.setHpr(0, 0, 0) + return False + + # 检查缩放是否包含无效值或为零 + if not original_scale.isFinite(): + print(f"警告: 节点 {node_name} 缩放包含无效值 {original_scale},重置为 (1,1,1)") + node_path.setScale(1, 1, 1) + return False + + # 检查缩放是否为零或接近零 + min_scale = 1e-10 + if (abs(original_scale.x) < min_scale or + abs(original_scale.y) < min_scale or + abs(original_scale.z) < min_scale): + print(f"警告: 节点 {node_name} 缩放接近零 {original_scale},重置为 (1,1,1)") + node_path.setScale(1, 1, 1) + return False + + # 检查缩放是否过大(防止异常大的缩放) + max_scale = 1000000 # 100万倍作为上限 + if (abs(original_scale.x) > max_scale or + abs(original_scale.y) > max_scale or + abs(original_scale.z) > max_scale): + print(f"警告: 节点 {node_name} 缩放过异常 {original_scale},重置为 (1,1,1)") + node_path.setScale(1, 1, 1) + return False + + return True + + except Exception as e: + print(f"验证/修复节点 {node_path.getName()} 变换时出错: {e}") + # 只在出现严重错误时才重置变换 + try: + node_path.setPos(0, 0, 0) + node_path.setHpr(0, 0, 0) + node_path.setScale(1, 1, 1) + except: + pass + return False + + def _applyModelScale(self, model, scale_factor): + """应用模型特定缩放 + + Args: + model: 要缩放的模型 + scale_factor: 缩放因子 + """ + try: + print(f"应用模型缩放因子: {scale_factor}") + + # 获取当前边界用于后续位置调整 + original_bounds = model.getBounds() + + # 应用缩放 + model.setScale(scale_factor) + + # 重新调整位置(因为缩放会影响边界) + if original_bounds and not original_bounds.isEmpty(): + new_bounds = model.getBounds() + min_point = new_bounds.getMin() + ground_offset = -min_point.getZ() + model.setZ(ground_offset) + print(f"缩放后重新调整位置: Z偏移 = {ground_offset}") + + print(f"模型缩放完成,缩放因子: {scale_factor}") + + except Exception as e: + print(f"应用模型缩放失败: {str(e)}") + + def _applyMaterialsToModel(self, model): + """递归应用材质到模型的所有GeomNode""" + + def apply_material(node_path, depth=0): + indent = " " * depth + try: + #print(f"{indent}处理节点: {node_path.getName()}") + #print(f"{indent}节点类型: {node_path.node().__class__.__name__}") + + if isinstance(node_path.node(), GeomNode): + #print(f"{indent}发现GeomNode,处理材质") + geom_node = node_path.node() + + # 检查所有几何体的状态 + has_color = False + color = None + + # 首先检查节点自身的状态 + node_state = node_path.getState() + if node_state.hasAttrib(MaterialAttrib.getClassType()): + mat_attrib = node_state.getAttrib(MaterialAttrib.getClassType()) + node_material = mat_attrib.getMaterial() + if node_material: + if node_material.hasBaseColor(): + color = node_material.getBaseColor() + has_color = True + #print(f"{indent}从节点材质获取基础颜色: {color}") + elif node_material.hasDiffuse(): + color = node_material.getDiffuse() + has_color = True + #print(f"{indent}从节点材质获取漫反射颜色: {color}") + + # 检查几何体材质 + if not has_color: + for i in range(geom_node.getNumGeoms()): + try: + geom = geom_node.getGeom(i) + state = geom_node.getGeomState(i) + + # 检查材质属性 + if state.hasAttrib(MaterialAttrib.getClassType()): + mat_attrib = state.getAttrib(MaterialAttrib.getClassType()) + orig_material = mat_attrib.getMaterial() + if orig_material: + if orig_material.hasBaseColor(): + color = orig_material.getBaseColor() + has_color = True + #print(f"{indent}从几何体材质获取基础颜色: {color}") + break + elif orig_material.hasDiffuse(): + color = orig_material.getDiffuse() + has_color = True + #print(f"{indent}从几何体材质获取漫反射颜色: {color}") + break + + # 检查颜色属性 + if not has_color and state.hasAttrib(ColorAttrib.getClassType()): + color_attrib = state.getAttrib(ColorAttrib.getClassType()) + if not color_attrib.isOff(): + color = color_attrib.getColor() + has_color = True + #print(f"{indent}从颜色属性获取: {color}") + break + except Exception as geom_error: + print(f"{indent}处理几何体 {i} 时出错: {geom_error}") + continue + + # 创建新材质 + material = Material() + if has_color and color: + #print(f"{indent}应用找到的颜色: {color}") + try: + # 确保颜色值有效 + if (color.getX() == color.getX() and color.getY() == color.getY() and + color.getZ() == color.getZ() and color.getW() == color.getW()): + material.setBaseColor(color) + material.setDiffuse(color) + node_path.setColor(color) + else: + print(f"{indent}⚠️ 颜色值无效,使用默认颜色") + material.setBaseColor((0.8, 0.8, 0.8, 1.0)) + material.setDiffuse((0.8, 0.8, 0.8, 1.0)) + except Exception as color_error: + print(f"{indent}设置颜色时出错: {color_error}") + material.setBaseColor((0.8, 0.8, 0.8, 1.0)) + material.setDiffuse((0.8, 0.8, 0.8, 1.0)) + else: + print(f"{indent}使用默认颜色") + material.setBaseColor((0.8, 0.8, 0.8, 1.0)) + material.setDiffuse((0.8, 0.8, 0.8, 1.0)) + + # 设置其他材质属性 + material.setAmbient((0.2, 0.2, 0.2, 1.0)) + material.setSpecular((0.5, 0.5, 0.5, 1.0)) + material.setShininess(32.0) + + # 应用材质 + try: + node_path.setMaterial(material, 1) # 1表示强制应用 + #print(f"{indent}材质应用成功") + except Exception as mat_error: + print(f"{indent}⚠️ 应用材质时出错: {mat_error}") + + #print(f"{indent}几何体数量: {geom_node.getNumGeoms()}") + + except Exception as node_error: + print(f"{indent}处理节点 {node_path.getName()} 时出错: {node_error}") + + # 递归处理子节点 + child_count = node_path.getNumChildren() + #print(f"{indent}子节点数量: {child_count}") + for i in range(child_count): + try: + child = node_path.getChild(i) + apply_material(child, depth + 1) + except Exception as child_error: + print(f"{indent}处理子节点 {i} 时出错: {child_error}") + continue + + # 应用材质 + #print("\n开始递归应用材质...") + try: + apply_material(model) + except Exception as e: + print(f"应用材质时出错: {e}") + print("=== 材质设置完成 ===\n") + + def _adjustModelToGround(self, model): + """智能调整模型到地面,但保持原有缩放结构""" + try: + #print("调整模型位置到地面...") + + # 获取模型的边界框 + bounds = model.getBounds() + if not bounds or bounds.isEmpty(): + print("无法获取模型边界,使用默认位置") + model.setPos(0, 0, 0) + return + + # 获取边界框的最低点 + min_point = bounds.getMin() + center = bounds.getCenter() + + # 计算需要移动的距离,使模型底部贴合地面(Z=0) + # 这里不涉及缩放,只是简单的位置调整 + ground_offset = -min_point.getZ() + + # 设置模型位置:X,Y居中,Z调整到地面 + model.setPos(0, 0, ground_offset) + + #print(f"模型边界: 最小点{min_point}, 中心{center}") + #print(f"地面偏移: {ground_offset}") + #print(f"最终位置: {model.getPos()}") + + except Exception as e: + print(f"调整模型位置失败: {str(e)}") + # 失败时使用默认位置 + model.setPos(0, 0, 0) + + def _normalizeModelScales(self, model): + """智能标准化模型缩放层级 + + 检测并修复FBX模型中子节点的大缩放值问题 + """ + try: + print("开始分析模型缩放结构...") + + # 收集所有节点的缩放信息 + scale_info = [] + self._collectScaleInfo(model, scale_info) + + if not scale_info: + print("没有找到需要处理的缩放信息") + return + + # 分析缩放模式 + large_scales = [info for info in scale_info if max(abs(info['scale'].x), abs(info['scale'].y), abs(info['scale'].z)) > 10] + + if not large_scales: + print("没有发现大缩放值,无需标准化") + return + + print(f"发现 {len(large_scales)} 个节点有大缩放值") + + # 计算标准化因子(基于最常见的大缩放值) + common_large_scale = self._findCommonLargeScale(large_scales) + if common_large_scale: + normalize_factor = 1.0 / common_large_scale + print(f"检测到常见大缩放值: {common_large_scale}, 标准化因子: {normalize_factor}") + + # 应用标准化 + self._applyScaleNormalization(model, normalize_factor) + print("✓ 缩放标准化完成") + else: + print("无法确定合适的标准化因子,跳过标准化") + + except Exception as e: + print(f"缩放标准化失败: {str(e)}") + + def _collectScaleInfo(self, node, scale_info, depth=0): + """递归收集节点缩放信息""" + try: + scale = node.getScale() + scale_info.append({ + 'node': node, + 'name': node.getName(), + 'scale': scale, + 'depth': depth + }) + + # 递归处理子节点 + for i in range(node.getNumChildren()): + child = node.getChild(i) + self._collectScaleInfo(child, scale_info, depth + 1) + + except Exception as e: + print(f"收集缩放信息失败 ({node.getName()}): {str(e)}") + + def _findCommonLargeScale(self, large_scales): + """找到最常见的大缩放值""" + try: + # 提取缩放值(取绝对值的最大分量) + scale_values = [] + for info in large_scales: + scale = info['scale'] + max_scale = max(abs(scale.x), abs(scale.y), abs(scale.z)) + scale_values.append(round(max_scale)) # 四舍五入到整数 + + if not scale_values: + return None + + # 找到最常见的值 + from collections import Counter + counter = Counter(scale_values) + most_common = counter.most_common(1)[0] + + print(f"缩放值统计: {dict(counter)}") + print(f"最常见的大缩放值: {most_common[0]} (出现{most_common[1]}次)") + + # 只有当最常见的值确实很大时才返回 + if most_common[0] >= 10: + return float(most_common[0]) + + return None + + except Exception as e: + print(f"分析常见缩放值失败: {str(e)}") + return None + + def _applyScaleNormalization(self, node, normalize_factor, depth=0): + """ + 安全地应用缩放标准化 + """ + try: + indent = " " * depth + current_scale = node.getScale() + current_pos = node.getPos() + + # 检查是否需要标准化(只处理明显的大缩放) + max_scale_component = max(abs(current_scale.x), abs(current_scale.y), abs(current_scale.z)) + + if max_scale_component > 10: # 只标准化明显的大缩放 + # 确保标准化因子有效 + if normalize_factor <= 0 or normalize_factor > 1000: + print(f"{indent}无效的标准化因子: {normalize_factor},跳过") + return + + # 应用新的缩放 + new_scale = current_scale * normalize_factor + + # 检查新缩放是否有效 + if any(s <= 0 for s in [new_scale.x, new_scale.y, new_scale.z]): + print(f"{indent}标准化后产生无效缩放,跳过") + return + + node.setScale(new_scale) + + # 同时调整位置:当缩放变小时,位置也应该相应变小以保持视觉相对位置 + # 这确保了子节点之间的相对距离在视觉上保持一致 + new_pos = current_pos * normalize_factor + node.setPos(new_pos) + + print(f"{indent}标准化 {node.getName()}:") + print(f"{indent} 缩放: {current_scale} -> {new_scale}") + print(f"{indent} 位置: {current_pos} -> {new_pos}") + + except Exception as e: + print(f"应用缩放标准化失败 ({node.getName()}): {str(e)}") + + def importModelAsync(self, filepath): + """异步导入模型""" + try: + # 创建异步加载请求 + request = self.world.loader.makeAsyncRequest(filepath) + + # 添加完成回调 + def modelLoaded(task): + if task.isReady(): + model = task.result() + if model: + # 处理加载完成的模型 + self.processLoadedModel(model) + return task.done() + + request.done_event = modelLoaded + + # 开始异步加载 + self.world.loader.loadAsync(request) + + except Exception as e: + print(f"异步加载模型失败: {str(e)}") + + # ==================== 材质和几何体处理 ==================== + + def processMaterials(self, model): + """处理模型材质""" + if isinstance(model.node(), GeomNode): + # 创建基础材质 + material = Material() + material.setAmbient((0.2, 0.2, 0.2, 1.0)) + material.setDiffuse((0.8, 0.8, 0.8, 1.0)) + material.setSpecular((0.5, 0.5, 0.5, 1.0)) + material.setShininess(32.0) + + # 检查FBX材质 + state = model.node().getGeomState(0) + if state.hasAttrib(MaterialAttrib.getClassType()): + fbx_material = state.getAttrib(MaterialAttrib.getClassType()).getMaterial() + if fbx_material: + # 复制FBX材质属性 + material.setAmbient(fbx_material.getAmbient()) + material.setDiffuse(fbx_material.getDiffuse()) + material.setSpecular(fbx_material.getSpecular()) + material.setShininess(fbx_material.getShininess()) + + # 应用材质 + model.setMaterial(material) + + def processModelGeometry(self, model): + """处理模型几何体""" + # 创建EggData对象 + egg_data = EggData() + + # 处理顶点数据 + vertex_pool = EggVertexPool("vpool") + egg_data.addChild(vertex_pool) + + # 处理几何体 + if isinstance(model.node(), GeomNode): + for i in range(model.node().getNumGeoms()): + geom = model.node().getGeom(i) + # 处理几何体数据 + # ... + + # ==================== 碰撞系统 ==================== + + def setupCollision(self, model): + """为模型设置碰撞检测(增强版本)""" + try: + # 创建碰撞节点 + cNode = CollisionNode(f'modelCollision_{model.getName()}') + + # 设置碰撞掩码 + cNode.setIntoCollideMask(BitMask32.bit(2)) # 用于鼠标选择 + + # 如果启用了模型间碰撞检测,添加额外的掩码 + if (hasattr(self.world, 'collision_manager') and + self.world.collision_manager.model_collision_enabled): + # 同时设置模型间碰撞掩码 + current_mask = cNode.getIntoCollideMask() + model_collision_mask = BitMask32.bit(6) # MODEL_COLLISION + cNode.setIntoCollideMask(current_mask | model_collision_mask) + print(f"为 {model.getName()} 启用模型间碰撞检测") + + # 获取模型的边界信息,使用与选择框相同的计算方法 + minPoint = Point3() + maxPoint = Point3() + + # 使用与选择框相同的calcTightBounds方法获取边界 + if model.calcTightBounds(minPoint, maxPoint, self.world.render): + # 检查边界框的有效性 + if (abs(minPoint.x) < 1e10 and abs(minPoint.y) < 1e10 and abs(minPoint.z) < 1e10 and + abs(maxPoint.x) < 1e10 and abs(maxPoint.y) < 1e10 and abs(maxPoint.z) < 1e10): + # 特殊处理FBX模型的碰撞体 + if model.hasTag("model_path") and model.getTag("model_path").lower().endswith('.fbx'): + print("检测到FBX模型,调整碰撞体...") + # 反向应用FBX的变换以匹配视觉表现 + # 缩放调整: 乘以100(因为模型被缩小了0.01倍) + minPoint *= 100 + maxPoint *= 100 + + # 旋转调整: 绕P轴旋转-90度 + # 创建旋转矩阵 + from panda3d.core import Mat4, LRotation + rotation = LRotation(0, -90, 0) # 绕P轴旋转-90度 + rot_matrix = Mat4() + rotation.extractToMatrix(rot_matrix) + + # 应用旋转变换到边界点 + minPoint = rot_matrix.xformPoint(minPoint) + maxPoint = rot_matrix.xformPoint(maxPoint) + + # 创建与选择框完全一致的碰撞体 + cBox = CollisionBox(minPoint, maxPoint) + cNode.addSolid(cBox) + radius = max(maxPoint.x - minPoint.x, maxPoint.y - minPoint.y, maxPoint.z - minPoint.z) / 2 + else: + # 使用默认球体 + radius = 1.0 + cSphere = CollisionSphere(Point3(0, 0, 0), radius) + cNode.addSolid(cSphere) + else: + # 使用默认球体 + radius = 1.0 + cSphere = CollisionSphere(Point3(0, 0, 0), radius) + cNode.addSolid(cSphere) + + # 将碰撞节点附加到模型上 + cNodePath = model.attachNewNode(cNode) + + # 根据调试设置决定是否显示碰撞体 + # if hasattr(self.world, 'debug_collision') and self.world.debug_collision: + # cNodePath.show() + # else: + # cNodePath.hide() + + # 为模型添加碰撞相关标签 + model.setTag("has_collision", "true") + model.setTag("collision_radius", str(radius)) + + print(f"✅ 为模型 {model.getName()} 设置碰撞检测完成") + + return cNodePath + + except Exception as e: + print(f"❌ 为模型 {model.getName()} 设置碰撞检测失败: {str(e)}") + import traceback + traceback.print_exc() + return None + + # ==================== 场景树管理 ==================== + + def updateSceneTree(self): + """更新场景树显示 - 代理到interface_manager""" + if hasattr(self.world, 'interface_manager'): + return self.world.interface_manager.updateSceneTree() + else: + print("界面管理器未初始化,无法更新场景树") + + # ==================== 场景保存和加载 ==================== + + def _collectGUIElementInfo(self, gui_node): + """收集GUI元素的信息用于保存""" + try: + # 获取GUI元素类型 + gui_type = "unknown" + if hasattr(gui_node, 'hasTag') and gui_node.hasTag("gui_type"): + gui_type = gui_node.getTag("gui_type") + elif hasattr(gui_node, 'hasTag') and gui_node.hasTag("saved_gui_type"): + gui_type = gui_node.getTag("saved_gui_type") + else: + # 尝试从节点名称推断类型 + name_lower = gui_node.getName().lower() + if "button" in name_lower: + gui_type = "button" + elif "label" in name_lower: + gui_type = "label" + elif "entry" in name_lower: + gui_type = "entry" + elif "image" in name_lower: + gui_type = "2d_image" + elif "videoscreen" in name_lower: + if "2d" in name_lower: + gui_type = "2d_video_screen" + else: + gui_type = "video_screen" + elif "info_panel" in name_lower: + if "3d" in name_lower: + gui_type = "info_panel_3d" + else: + gui_type = "info_panel" + else: + # 如果无法识别类型,跳过该元素 + print(f"跳过无法识别类型的GUI元素: {gui_node.getName()}") + return None + + gui_info = { + "name": gui_node.getName(), + "type": gui_type, + "position": list(gui_node.getPos()), + "rotation": list(gui_node.getHpr()), + "scale": list(gui_node.getScale()), + "tags": {}, + "parent_name":None, + "video_path":gui_node.getTag("video_path") if gui_node.hasTag("video_path") else None, + "panel_id":gui_node.getTag("panel_id") if gui_node.hasTag("panel_id") else None, + } + + parent = gui_node.getParent() + if parent and not parent.isEmpty(): + parent_name = parent.getName() + if parent_name not in ["render","aspect2d","render2d"]: + gui_info["parent_name"] = parent_name + + # 收集所有标签(仅对NodePath类型的对象) + if hasattr(gui_node, 'getTagNames'): + for tag in gui_node.getTagNames(): + gui_info["tags"][tag] = gui_node.getTag(tag) + elif hasattr(gui_node, 'getTags'): # 对于DirectGUI对象 + # DirectGUI对象使用不同的方法存储标签 + if hasattr(gui_node, '_tags'): + gui_info["tags"] = gui_node._tags.copy() + + # 根据类型收集特定信息 + if gui_type == "button": + if hasattr(gui_node, 'get'): # DirectButton + gui_info["text"] = gui_node.get() + elif hasattr(gui_node, 'getText'): # 其他类型 + gui_info["text"] = gui_node.getText() + elif hasattr(gui_node, 'hasTag') and gui_node.hasTag("gui_text"): + gui_info["text"] = gui_node.getTag("gui_text") + elif gui_type == "label": + if hasattr(gui_node, 'getText'): + gui_info["text"] = gui_node.getText() + elif hasattr(gui_node, 'hasTag') and gui_node.hasTag("gui_text"): + gui_info["text"] = gui_node.getTag("gui_text") + elif gui_type == "entry": + if hasattr(gui_node, 'get'): + gui_info["text"] = gui_node.get() + elif hasattr(gui_node, 'hasTag') and gui_node.hasTag("gui_text"): + gui_info["text"] = gui_node.getTag("gui_text") + elif gui_type == "2d_image": + if hasattr(gui_node, 'hasTag') and gui_node.hasTag("image_path"): + gui_info["image_path"] = gui_node.getTag("image_path") + elif hasattr(gui_node, 'hasTag') and gui_node.hasTag("gui_image_path"): + gui_info["image_path"] = gui_node.getTag("gui_image_path") + elif gui_type == "3d_text": + if hasattr(gui_node,'hasTag') and gui_node.hasTag("gui_text"): + gui_info["text"] = gui_node.getTag("gui_text") + elif hasattr(gui_node,'node') and hasattr(gui_node.node(),'getText'): + gui_info["text"] = gui_node.node().getText() + elif gui_type == "3d_image": + if hasattr(gui_node,'hasTag') and gui_node.hasTag("gui_image_path"): + gui_info["image_path"] = gui_node.getTag("gui_image_path") + elif gui_type == "video_screen": + if hasattr(gui_node, 'hasTag') and gui_node.hasTag("video_path"): + gui_info["video_path"] = gui_node.getTag("video_path") + elif gui_type == "2d_video_screen": + if hasattr(gui_node, 'hasTag') and gui_node.hasTag("video_path"): + gui_info["video_path"] = gui_node.getTag("video_path") + elif gui_type == "virtual_screen": + if hasattr(gui_node, 'hasTag') and gui_node.hasTag("gui_text"): + gui_info["text"] = gui_node.getTag("gui_text") + elif gui_type in ["info_panel", "info_panel_3d"]: + # 收集信息面板的特定信息 + if hasattr(gui_node, 'hasTag') and gui_node.hasTag("panel_id"): + gui_info["panel_id"] = gui_node.getTag("panel_id") + + # 收集背景图片信息 + if hasattr(gui_node, 'hasTag') and gui_node.hasTag("image_path"): + gui_info["image_path"] = gui_node.getTag("image_path") + + # 收集GUI元素的可见性信息 + user_visible = gui_node.getPythonTag("user_visible") + if user_visible is not None: + gui_info["user_visible"] = user_visible + else: + # 默认为可见 + gui_info["user_visible"] = True + + # 收集挂载的脚本信息 + if hasattr(self.world, 'script_manager') and self.world.script_manager: + try: + script_manager = self.world.script_manager + scripts = script_manager.get_scripts_on_object(gui_node) # 修复:使用 gui_node 而不是 node + if scripts: + gui_info["scripts"] = [] + for script_component in scripts: + try: + script_name = script_component.script_name + # 获取脚本路径 + script_class = script_component.script_instance.__class__ + script_file = self._get_script_file_path(script_class, script_name) + # 只有当脚本文件存在时才保存 + if script_file and os.path.exists(script_file): + gui_info["scripts"].append({ + "name": script_name, + "file": script_file + }) + print(f"收集脚本信息: {script_name} from {script_file}") + else: + print(f"警告: 脚本文件不存在: {script_file}") + except Exception as e: + print(f"收集单个脚本信息失败 {script_name}, 错误: {e}") + continue + except Exception as e: + print(f"收集脚本信息失败: {e}") + + print(f"成功收集GUI元素信息: {gui_info}") + return gui_info + except Exception as e: + print(f"收集GUI元素信息失败: {e}") + import traceback + traceback.print_exc() + return None + + def _get_script_file_path(self, script_class, script_name): + """ + 获取脚本文件路径的可靠方法 + """ + script_file = "" + + # 方法1: 使用 inspect.getfile + try: + script_file = inspect.getfile(script_class) + if script_file and os.path.exists(script_file): + return script_file + except: + pass + + # 方法2: 使用 __file__ 属性 + try: + if hasattr(script_class, '__file__') and script_class.__file__: + script_file = script_class.__file__ + if script_file and os.path.exists(script_file): + return script_file + except: + pass + + # 方法3: 使用模块的 __file__ 属性 + try: + module = inspect.getmodule(script_class) + if module and hasattr(module, '__file__') and module.__file__: + script_file = module.__file__ + if script_file and os.path.exists(script_file): + return script_file + except: + pass + + # 方法4: 从脚本管理器中查找 + try: + if hasattr(self.world, 'script_manager') and self.world.script_manager: + script_manager = self.world.script_manager + # 查找脚本类对应的文件路径 + for file_path, file_mtime in script_manager.loader.file_mtimes.items(): + # 检查文件名是否匹配脚本名 + file_name = os.path.splitext(os.path.basename(file_path))[0] + if file_name == script_name: + if os.path.exists(file_path): + return file_path + except: + pass + + # 方法5: 在脚本目录中查找 + try: + if hasattr(self.world, 'script_manager') and self.world.script_manager: + script_manager = self.world.script_manager + scripts_dir = script_manager.scripts_directory + + # 查找匹配的脚本文件 + if os.path.exists(scripts_dir): + for file_name in os.listdir(scripts_dir): + if file_name.endswith('.py'): + base_name = os.path.splitext(file_name)[0] + if base_name == script_name: + full_path = os.path.join(scripts_dir, file_name) + if os.path.exists(full_path): + return full_path + except: + pass + + print(f"警告: 无法获取脚本 {script_name} 的文件路径") + return script_file + + def saveScene(self, filename,project_path): + """保存场景到BAM文件 - 完整版,支持GUI元素,地形""" + try: + print(f"\n=== 开始保存场景到: {filename} ===") + + # 确保文件路径是规范化的 + filename = os.path.normpath(filename) + + # 确保目录存在 + directory = os.path.dirname(filename) + if directory and not os.path.exists(directory): + os.makedirs(directory) + + resources_dir = os.path.join(directory,"resources") + if not os.path.exists(resources_dir): + os.makedirs(resources_dir) + + # 存储需要临时隐藏的节点,以便保存后恢复 + nodes_to_restore = [] + + # 查找并隐藏所有坐标轴和选择框节点 + gizmo_nodes = self.world.render.findAllMatches("**/gizmo*") + selection_box_nodes = self.world.render.findAllMatches("**/selectionBox*") + + # 隐藏坐标轴节点 + for node in gizmo_nodes: + if not node.isHidden(): + nodes_to_restore.append((node, True)) # (节点, 原先是否可见) + node.hide() + print(f"临时隐藏坐标轴节点: {node.getName()}") + + # 隐藏选择框节点 + for node in selection_box_nodes: + if not node.isHidden(): + nodes_to_restore.append((node, True)) + node.hide() + print(f"临时隐藏选择框节点: {node.getName()}") + + # 收集所有需要保存的节点 + all_nodes = [] + all_nodes.extend(self.models) + all_nodes.extend(self.Spotlight) + all_nodes.extend(self.Pointlight) + + # 添加GUI元素节点 + gui_elements = [] + if hasattr(self.world, 'gui_elements'): + # 过滤掉空的或重复的GUI元素 + unique_gui_elements = [] + seen_names = set() + + for elem in self.world.gui_elements: + if elem and not elem.isEmpty(): + if not elem.isEmpty() and elem.getName() not in seen_names: + unique_gui_elements.append(elem) + seen_names.add(elem.getName()) + gui_elements = unique_gui_elements + + print(f"保存时GUI元素列表=>>>>>>>>>>>>{self.world.gui_elements}") + all_nodes.extend(gui_elements) + + # 创建用于保存GUI信息的JSON文件路径 + gui_info_file = filename.replace('.bam', '_gui.json') + + print(self.world.gui_elements) + # 收集GUI元素信息(排除3D文本和3D图像) + gui_data = [] + copied_resources = {} + for gui_node in gui_elements: + gui_info = self._collectGUIElementInfo(gui_node) + if gui_info: + gui_type = gui_info.get("type","") + #处理2d图片 + if gui_type =="2d_image" and "image_path" in gui_info: + original_path = gui_info["image_path"] + if original_path and os.path.exists(original_path): + resource_name = os.path.basename(original_path) + new_path = os.path.join(resources_dir,resource_name) + if original_path not in copied_resources: + try: + shutil.copy2(original_path,new_path) + copied_resources[original_path] = new_path + print(f"复制图片资源: {original_path} -> {new_path}") + except Exception as e: + print(f"复制图片资源失败: {original_path}, 错误: {e}") + gui_info["image_path"] = new_path + + # 处理3D图片 + elif gui_type == "3d_image" and "image_path" in gui_info: + original_path = gui_info["image_path"] + # 确保original_path是有效字符串且文件存在 + if original_path and isinstance(original_path, str) and os.path.exists(original_path): + resource_name = os.path.basename(original_path) + new_path = os.path.join(resources_dir, resource_name) + if original_path not in copied_resources: + try: + shutil.copy2(original_path, new_path) + copied_resources[original_path] = new_path + print(f"复制3D图片资源: {original_path} -> {new_path}") + except Exception as e: + print(f"复制3D图片资源失败: {original_path}, 错误: {e}") + gui_info["image_path"] = new_path + + # 处理背景图片 + if "bg_image_path" in gui_info and gui_info["bg_image_path"]: + original_path = gui_info["bg_image_path"] + # 确保original_path是有效字符串且文件存在 + if original_path and isinstance(original_path, str) and os.path.exists(original_path): + resource_name = os.path.basename(original_path) + new_path = os.path.join(resources_dir, resource_name) + if original_path not in copied_resources: + try: + shutil.copy2(original_path, new_path) + copied_resources[original_path] = new_path + print(f"复制背景图片资源: {original_path} -> {new_path}") + except Exception as e: + print(f"复制背景图片资源失败: {original_path}, 错误: {e}") + gui_info["bg_image_path"] = new_path + + # 处理视频资源 + if gui_type in ["video_screen", "2d_video_screen"] and "video_path" in gui_info: + original_path = gui_info["video_path"] + # 确保original_path是有效字符串且文件存在 + if original_path and isinstance(original_path, str) and os.path.exists(original_path): + resource_name = os.path.basename(original_path) + new_path = os.path.join(resources_dir, resource_name) + if original_path not in copied_resources: + try: + shutil.copy2(original_path, new_path) + copied_resources[original_path] = new_path + print(f"复制视频资源: {original_path} -> {new_path}") + except Exception as e: + print(f"复制视频资源失败: {original_path}, 错误: {e}") + gui_info["video_path"] = new_path + + gui_data.append(gui_info) + print(f"添加GUI信息: {gui_info['name']}") + + # 保存GUI信息到JSON文件(确保即使没有GUI元素也创建有效的空JSON数组) + try: + import json + with open(gui_info_file, 'w', encoding='utf-8') as f: + json.dump(gui_data, f, ensure_ascii=False, indent=2) + print(f"✓ GUI信息已保存到: {gui_info_file}") + except Exception as e: + print(f"✗ 保存GUI信息失败: {e}") + import traceback + traceback.print_exc() + + # 保存所有节点的信息 + for node in all_nodes: + if node.isEmpty(): + continue + + # 保存变换信息 + node.setTag("transform_pos", str(node.getPos())) + node.setTag("transform_hpr", str(node.getHpr())) + node.setTag("transform_scale", str(node.getScale())) + print(f"保存节点 {node.getName()} 的变换信息") + + # 保存可见性信息 + user_visible = node.getPythonTag("user_visible") + if user_visible is not None: + node.setTag("user_visible", str(user_visible).lower()) + print(f"保存节点 {node.getName()} 的可见性信息: {user_visible}") + + # 保存父子关系信息 - 关键修改 + parent = node.getParent() + if parent and not parent.isEmpty() and parent != self.world.render: + # 只有当父节点不是根节点且父节点是场景中的模型时才保存父子关系 + if parent.getName() not in ["render", "aspect2d", "render2d"]: + # 检查父节点是否也是场景中的模型 + is_parent_model = False + for model in self.models: + if model == parent: + is_parent_model = True + break + + if is_parent_model: + node.setTag("parent_name", parent.getName()) + print(f"保存节点 {node.getName()} 的父节点信息: {parent.getName()}") + + # 获取当前状态 + state = node.getState() + + # 如果有材质属性,保存为标签 + if state.hasAttrib(MaterialAttrib.getClassType()): + mat_attrib = state.getAttrib(MaterialAttrib.getClassType()) + material = mat_attrib.getMaterial() + if material: + # 保存材质属性到标签 + node.setTag("material_ambient", str(material.getAmbient())) + node.setTag("material_diffuse", str(material.getDiffuse())) + node.setTag("material_specular", str(material.getSpecular())) + node.setTag("material_emission", str(material.getEmission())) + node.setTag("material_shininess", str(material.getShininess())) + if material.hasBaseColor(): + node.setTag("material_basecolor", str(material.getBaseColor())) + + # 保存特定类型节点的额外信息 + if node.hasTag("light_type"): + # 保存光源特定信息 + light_obj = node.getPythonTag("rp_light_object") + if light_obj: + node.setTag("light_energy", str(light_obj.energy)) + if node.hasTag("stored_energy"): + node.setTag("stored_energy", node.getTag("stored_energy")) + node.setTag("light_radius", str(getattr(light_obj, 'radius', 0))) + if hasattr(light_obj, 'fov'): + node.setTag("light_fov", str(light_obj.fov)) + elif node.hasTag("element_type"): + element_type = node.getTag("element_type") + if element_type == "cesium_tileset": + # 保存tileset特定信息 + if node.hasTag("tileset_url"): + node.setTag("saved_tileset_url", node.getTag("tileset_url")) + elif node.hasTag("gui_type") or node.hasTag("is_gui_element"): + # 保存GUI元素特定信息 + gui_type = node.getTag("gui_type") if node.hasTag("gui_type") else \ + node.getTag("saved_gui_type") if node.hasTag("saved_gui_type") else "unknown" + node.setTag("saved_gui_type", gui_type) + + # 保存GUI元素的通用属性 + if hasattr(node, 'getPythonTag'): + # 保存任何Python标签数据 + for tag_name in node.getPythonTagKeys(): + try: + tag_value = node.getPythonTag(tag_name) + node.setTag(f"python_tag_{tag_name}", str(tag_value)) + except: + pass + elif node.hasTag("element_type") and node.getTag("element_type") == "info_panel": + # 保存信息面板特定信息 + print(f"保存信息面板信息: {node.getName()}") + panel_id = node.getTag("panel_id") if node.hasTag("panel_id") else node.getName() + if hasattr(self.world, 'info_panel_manager'): + panel_data = self.world.info_panel_manager.serializePanelData(panel_id) + if panel_data: + import json + node.setTag("info_panel_data", json.dumps(panel_data, ensure_ascii=False)) + + # 保存模型动画信息 + elif node.hasTag("is_model_root"): + # 保存模型动画相关信息 + if node.hasTag("has_animations"): + node.setTag("saved_has_animations", node.getTag("has_animations")) + if node.hasTag("model_path"): + node.setTag("saved_model_path", node.getTag("model_path")) + if node.hasTag("can_create_actor_from_memory"): + node.setTag("saved_can_create_actor_from_memory", node.getTag("can_create_actor_from_memory")) + + if hasattr(self.world,'script_manager') and self.world.script_manager: + script_manager = self.world.script_manager + scripts = script_manager.get_scripts_on_object(node) + if scripts: + node.setTag("has_scripts", "true") + script_info_list = [] + for script_component in scripts: + script_name = script_component.script_name + print(f"保存脚本信息: {script_name}") + + # 获取脚本类的文件路径 + script_class = script_component.script_instance.__class__ + script_file = self._get_script_file_path(script_class, script_name) + + script_info_list.append({ + "name": script_name, + "file": script_file + }) + + # 将脚本信息保存为JSON字符串 + import json + node.setTag("scripts_info", json.dumps(script_info_list, ensure_ascii=False)) + print(f"为节点 {node.getName()} 保存了 {len(script_info_list)} 个脚本") + + try: + print("--- 打印当前场景图 (render) ---") + self.world.render.ls() + print("---------------------------------") + + self.take_screenshot(project_path) + # 保存场景 + success = self.world.render.writeBamFile(Filename.fromOsSpecific(filename)) + + if success: + print(f"✓ 场景保存成功: {filename}") + else: + print("✗ 场景保存失败") + + return success + + finally: + # 恢复之前隐藏的节点 + for item in nodes_to_restore: + node, was_visible = item + if was_visible and not node.isEmpty(): + node.show() + print(f"恢复显示节点: {node.getName()}") + + if nodes_to_restore: + print(f"已恢复 {len(nodes_to_restore)} 个辅助节点的显示") + + except Exception as e: + print(f"保存场景时发生错误: {str(e)}") + import traceback + traceback.print_exc() + return False + + def take_screenshot(self, projectpath): + """ + 截图并保存到指定的完整路径 + + Args: + full_path (str): 完整的文件保存路径,包括文件名和扩展名 + + Returns: + bool: 截图是否成功 + """ + try: + from panda3d.core import Filename + import os + + print(f"\n=== 截图保存: {projectpath} ===") + + # 确保目录存在 + directory = os.path.dirname(projectpath) + if directory and not os.path.exists(directory): + os.makedirs(directory) + print(f"创建目录: {directory}") + + # 规范化路径 + filename = os.path.basename(os.path.normpath(projectpath)) + filename = f'{filename}.png' + print(f'project_path: {projectpath}') + print(f'project_name: {filename}') + full_path = os.path.normpath(os.path.join(projectpath, filename)) + p3d_filename = Filename.from_os_specific(full_path) + # 使用 Panda3D 的截图功能 + success = self.world.win.saveScreenshot(p3d_filename) + + if success: + print(f"✅ 成功截图并保存到: {full_path}") + return True + else: + print(f"❌ 截图保存失败: {full_path}") + return False + + except Exception as e: + print(f"保存截图时发生错误: {str(e)}") + import traceback + traceback.print_exc() + return False + + def loadScene(self, filename): + """从BAM文件加载场景""" + try: + print(f"\n=== 开始加载场景: {filename} ===") + + # 确保文件路径是规范化的 + filename = os.path.normpath(filename) + + # 检查文件是否存在 + if not os.path.exists(filename): + print(f"场景文件不存在: {filename}") + return False + + tree_widget = self._get_tree_widget() + # 清除当前场景 + print("\n清除当前场景...") + for model in self.models: + tree_widget.delete_item(model) + + # 清除灯光 + for light_node in self.Spotlight: + tree_widget.delete_item(light_node) + + for light_node in self.Pointlight: + tree_widget.delete_item(light_node) + + for terrain in self.world.terrain_manager.terrains: + tree_widget.delete_item(terrain) + + # 清除tilesets + for tileset_info in self.tilesets: + tree_widget.delete_item(tileset_info['node']) + + for light in self.Spotlight: + if not light.isEmpty(): + light.removeNode() + self.Spotlight.clear() + + for light in self.Pointlight: + if not light.isEmpty(): + light.removeNode() + self.Pointlight.clear() + + # 清理tilesets + for tileset_info in self.tilesets: + if tileset_info['node'] and not tileset_info['node'].isEmpty(): + tileset_info['node'].removeNode() + self.tilesets.clear() + + # 清理Cesium tilesets + for tileset_name, tileset_info in list(self.cesium_integration.tilesets.items()): + if tileset_info['node'] and not tileset_info['node'].isEmpty(): + tileset_info['node'].removeNode() + self.cesium_integration.tilesets.clear() + + for gui in self.world.gui_elements: + if not gui.isEmpty(): + gui.removeNode() + self.world.gui_elements.clear() + + if hasattr(self.world,'info_panel_manager'): + self.world.info_panel_manager.removeAllPanels() + + # 清理可能存在的辅助节点 + self._cleanupAuxiliaryNodes() + + # 加载场景 + scene = self.world.loader.loadModel(Filename.fromOsSpecific(filename)) + if not scene: + print("场景加载失败") + return False + + tree_widget.create_model_items(scene) + # 遍历场景中的所有模型节点 + # 用于存储处理后的灯光节点,避免重复处理 + processed_lights = [] + # 用于存储处理后的GUI元素,避免重复处理 + + #存储所有加载的节点,用于后续处理父子关系 + loaded_nodes = {} #name->nodePath映射 + + # 遍历场景中的所有节点 + def processNode(nodePath, depth=0): + indent = " " * depth + print(f"{indent}处理节点: {nodePath.getName()} (类型: {type(nodePath.node()).__name__})") + + #存储节点以便后续处理父子关系 + loaded_nodes[nodePath.getName()] = nodePath + + if nodePath.getName().startswith('ground'): + print(f"{indent}跳过ground节点: {nodePath.getName()}") + return + + # 跳过render节点的递归 + if nodePath.getName() == "render" and depth > 0: + print(f"{indent}跳过重复的render节点") + return + + # 跳过光源节点 + if nodePath.getName() in ["alight", "dlight"]: + print(f"{indent}跳过光源节点: {nodePath.getName()}") + return + + # 跳过相机节点 + if nodePath.getName() in ["camera", "cam"]: + print(f"{indent}跳过相机节点: {nodePath.getName()}") + return + + # 跳过辅助节点 + if nodePath.getName().startswith(("gizmo", "selectionBox")): + print(f"{indent}跳过辅助节点: {nodePath.getName()}") + return + + if nodePath.getName() in ['SceneRoot'] or \ + any(keyword in nodePath.getName() for keyword in ["Skybox", "skybox"]): + print(f"{indent}跳过环境节点:{nodePath.getName()}") + return + + # 检查是否是用户创建的场景元素 + is_scene_element = ( + nodePath.hasTag("is_scene_element") or + nodePath.hasTag("is_model_root") or + nodePath.hasTag("light_type") or + nodePath.hasTag("gui_type") or # 检查gui_type标签 + nodePath.hasTag("is_gui_element") or + nodePath.hasTag("saved_gui_type") or + (nodePath.hasTag("element_type") and nodePath.getTag("element_type") == "info_panel") + ) + + # 特殊处理:检查节点名称是否包含GUI相关关键词 + is_potential_gui = any(keyword in nodePath.getName().lower() for keyword in + ["gui", "button", "label", "entry", "image", "video", "screen", "text"]) + + if is_scene_element or is_potential_gui: + print(f"{indent}找到场景元素节点: {nodePath.getName()}") + + # 如果是潜在的GUI元素但没有标签,添加基本标签 + if is_potential_gui and not (nodePath.hasTag("gui_type") or nodePath.hasTag("is_gui_element")): + print(f"{indent}为潜在GUI元素添加标签: {nodePath.getName()}") + nodePath.setTag("is_gui_element", "1") + nodePath.setTag("is_scene_element", "1") + # 尝试从名称推断类型 + name_lower = nodePath.getName().lower() + if "button" in name_lower: + nodePath.setTag("gui_type", "button") + elif "label" in name_lower: + nodePath.setTag("gui_type", "label") + elif "entry" in name_lower: + nodePath.setTag("gui_type", "entry") + elif "image" in name_lower: + nodePath.setTag("gui_type", "image") + elif "video" in name_lower or "screen" in name_lower: + nodePath.setTag("gui_type", "video_screen") + else: + nodePath.setTag("gui_type", "unknown") + + # 清除现有材质状态 + nodePath.clearMaterial() + nodePath.clearColor() + + # 恢复变换信息 + def parseVec3(vec_str): + """解析向量字符串为Vec3""" + try: + vec_str = vec_str.replace('LVecBase3f', '').replace('LPoint3f', '').strip('()') + x, y, z = map(float, vec_str.split(',')) + return Vec3(x, y, z) + except Exception as e: + print(f"解析向量失败: {vec_str}, 错误: {e}") + return Vec3(0, 0, 0) + + if nodePath.hasTag("transform_pos"): + pos = parseVec3(nodePath.getTag("transform_pos")) + nodePath.setPos(pos) + print(f"{indent}恢复位置: {pos}") + + if nodePath.hasTag("transform_hpr"): + hpr = parseVec3(nodePath.getTag("transform_hpr")) + nodePath.setHpr(hpr) + print(f"{indent}恢复旋转: {hpr}") + + if nodePath.hasTag("transform_scale"): + scale = parseVec3(nodePath.getTag("transform_scale")) + nodePath.setScale(scale) + print(f"{indent}恢复缩放: {scale}") + + # 恢复可见性状态 + user_visible = True + if nodePath.hasTag("user_visible"): + user_visible = nodePath.getTag("user_visible").lower() == "true" + + # 设置用户可见性标记 + nodePath.setPythonTag("user_visible", user_visible) + + # 应用可见性状态 + if hasattr(self.world, 'property_panel'): + self.world.property_panel._syncEffectiveVisibility(nodePath) + else: + # 如果没有属性面板,直接应用可见性 + if user_visible: + nodePath.show() + else: + nodePath.hide() + + if nodePath.hasTag("has_scripts") and nodePath.getTag("has_scripts") == "true": + if hasattr(self.world,'script_manager') and self.world.script_manager: + try: + import json + scripts_info = json.loads(nodePath.getTag("scripts_info")) + print(f"节点 {nodePath.getName()} 需要重新挂载 {len(scripts_info)} 个脚本") + + script_manager = self.world.script_manager + for script_info in scripts_info: + script_name = script_info["name"] + script_file = script_info.get("file","") + + print(f"尝试重新挂载脚本{script_name}from {script_file}") + + if script_name not in script_manager.loader.script_classes: + if script_file and os.path.exists(script_file): + print(f"从文件加载脚本:{script_file}") + loaded_class = script_manager.load_script_from_file(script_file) + if loaded_class is None: + print(f"从文件加载脚本失败{script_file}") + script_path = self._find_scrip_in_directory(script_name) + if script_path: + print(f"从目录找到脚本并加载{script_path}") + script_manager.load_script_from_file(script_path) + else: + script_path = self._find_script_in_directory(script_name) + if script_path: + print(f"从目录找到脚本并加载: {script_path}") + script_manager.load_script_from_file(script_path) + else: + print(f"找不到脚本文件: {script_name}") + if script_name in script_manager.loader.script_classes: + script_component = script_manager.add_script_to_object(nodePath,script_name) + if script_component: + print(f"成功为 {nodePath.getName()} 添加脚本: {script_name}") + else: + print(f"为 {nodePath.getName()} 添加脚本失败: {script_name}") + else: + print(f"脚本 {script_name} 不可用,跳过挂载") + except Exception as e: + print(f"重新挂载脚本失败: {e}") + import traceback + traceback.print_exc() + + + # 恢复材质属性 + def parseColor(color_str): + """解析颜色字符串为Vec4""" + try: + color_str = color_str.replace('LVecBase4f', '').strip('()') + r, g, b, a = map(float, color_str.split(',')) + return Vec4(r, g, b, a) + except: + return Vec4(1, 1, 1, 1) + + # 创建并恢复材质 + material = Material() + material_changed = False + + if nodePath.hasTag("material_ambient"): + material.setAmbient(parseColor(nodePath.getTag("material_ambient"))) + material_changed = True + + if nodePath.hasTag("material_diffuse"): + material.setDiffuse(parseColor(nodePath.getTag("material_diffuse"))) + material_changed = True + + if nodePath.hasTag("material_specular"): + material.setSpecular(parseColor(nodePath.getTag("material_specular"))) + material_changed = True + + if nodePath.hasTag("material_emission"): + material.setEmission(parseColor(nodePath.getTag("material_emission"))) + material_changed = True + + if nodePath.hasTag("material_shininess"): + material.setShininess(float(nodePath.getTag("material_shininess"))) + material_changed = True + + if nodePath.hasTag("material_basecolor"): + material.setBaseColor(parseColor(nodePath.getTag("material_basecolor"))) + material_changed = True + + if material_changed: + nodePath.setMaterial(material) + + # 恢复颜色属性 + if nodePath.hasTag("color"): + nodePath.setColor(parseColor(nodePath.getTag("color"))) + + # 处理特定类型的节点 + if nodePath.hasTag("light_type"): + light_type = nodePath.getTag("light_type") + print(f"{indent}检测到光源类型: {light_type}") + + # 检查是否已经处理过这个灯光 + if nodePath not in processed_lights: + # 重新创建RP光源对象 + if light_type == "spot_light": + self._recreateSpotLight(nodePath) + elif light_type == "point_light": + self._recreatePointLight(nodePath) + # 标记为已处理 + processed_lights.append(nodePath) + + elif nodePath.hasTag("element_type"): + element_type = nodePath.getTag("element_type") + if element_type == "cesium_tileset": + tileset_url = nodePath.getTag("saved_tileset_url") if nodePath.hasTag( + "saved_tileset_url") else "" + tileset_info = { + 'url': tileset_url, + 'node': nodePath, + 'position': nodePath.getPos(), + 'tiles': {} + } + self.tilesets.append(tileset_info) + self.cesium_integration.tilesets[nodePath.getName()] = tileset_info + + # 将节点重新挂载到render下(如果需要) + # 注意:GUI元素可能需要挂载到特定的父节点上 + if nodePath.hasTag("gui_type") or nodePath.hasTag("is_gui_element"): + # GUI元素通常应该挂载到aspect2d或特定的GUI父节点上 + # 这里我们先保持原挂载关系 + pass + else: + # 其他节点确保挂载到render下 + if nodePath.getParent() != self.world.render and not nodePath.getName() in ["render", + "aspect2d", + "render2d"]: + nodePath.wrtReparentTo(self.world.render) + + # 为模型节点设置碰撞检测 + if nodePath.hasTag("is_model_root"): + print(f"J{indent}处理模型节点{nodePath.getName()}") + + #self._validateAndFixAllTransforms(nodePath) + + self._fixModelStructure(nodePath) + + # 恢复模型动画信息 + self._restoreModelAnimationInfo(nodePath) + + # 检测并处理模型动画(类似property_panel.py中的逻辑) + self._processModelAnimations(nodePath) + + # if self.world.property_panel._hasCollision(nodePath): + # print(f"{indent}模型{nodePath.getName()}已有碰撞体,跳过碰撞体设置") + # else: + # print(f"{indent}为模型{nodePath.getName()}设置碰撞检测") + # self.setupCollision(nodePath) + self.models.append(nodePath) + + # 递归处理子节点 + for child in nodePath.getChildren(): + processNode(child, depth + 1) + + print("\n开始处理场景节点...") + processNode(scene) + + #处理父子关系 - 在所有节点加载完成后设置正确的父子关系 + print("\n开始重建父子关系...") + self._rebuildParentChildRelationships(loaded_nodes) + + # 加载GUI信息并重新创建非3D的GUI元素 + gui_info_file = filename.replace('.bam', '_gui.json') + if os.path.exists(gui_info_file): + try: + with open(gui_info_file, 'r', encoding='utf-8') as f: + content = f.read().strip() + if content: # 检查文件是否为空 + import json + gui_data = json.loads(content) + print(f"✓ 成功加载GUI信息文件: {gui_info_file}") + print(f" 发现 {len(gui_data)} 个GUI元素需要重建") + + # 使用gui_manager重新创建GUI元素 + self._recreateGUIElementsFromData(gui_data) + else: + print("ℹ️ GUI信息文件为空") + except json.JSONDecodeError as e: + print(f"✗ GUI信息文件格式错误: {e}") + except Exception as e: + print(f"✗ 加载GUI信息失败: {e}") + import traceback + traceback.print_exc() + else: + print("ℹ️ 未找到GUI信息文件") + + # 移除临时场景节点 + if not scene.isEmpty(): + scene.removeNode() + + # 更新场景树 + #self.updateSceneTree() + #self._get_tree_widget().create_model_items(scene) + + print(f"加载完成,GUI元素数量: {len(self.world.gui_elements)}") + if len(self.world.gui_elements) > 0: + print("GUI元素列表:") + for i, elem in enumerate(self.world.gui_elements): + print( + f" {i + 1}. {elem.getName()} (类型: {elem.getTag('gui_type') if elem.hasTag('gui_type') else 'unknown'})") + + print("=== 场景加载完成 ===\n") + return True + + except Exception as e: + print(f"加载场景时发生错误: {str(e)}") + import traceback + traceback.print_exc() + return False + + def _rebuildParentChildRelationships(self, loaded_nodes): + try: + parent_child_relations = [] + for node_name, node in loaded_nodes.items(): + if node.hasTag("parent_name"): + parent_name = node.getTag("parent_name") + if parent_name in loaded_nodes: + parent_child_relations.append((node, loaded_nodes[parent_name])) # 修复:应该是元组 + print(f"发现父子关系:{parent_name}->{node_name}") + else: + print(f"警告:节点{node_name}的父节点{parent_name}不存在") + for child_node, parent_node in parent_child_relations: + try: + child_node.wrtReparentTo(parent_node) + print(f"成功设置父子关系:{parent_node.getName()}->{child_node.getName()}") + except Exception as e: + print(f"设置父子关系失败{parent_node.getName()}->{child_node.getName()}:{e}") + + if not parent_child_relations: + print("尝试从场景结构推断父子关系") + self._inferParentChildRelationships(loaded_nodes) + + print("父子关系重建完成") + except Exception as e: + print(f"重建父子关系时出错: {e}") + import traceback + traceback.print_exc() + + + except Exception as e: + print(f"重建父子关系时出错: {e}") + import traceback + traceback.print_exc() + + def _inferParentChildRelationships(self, loaded_nodes): + """从场景结构推断父子关系""" + try: + # 这里可以添加更复杂的父子关系推断逻辑 + # 例如,根据节点名称、位置关系等进行推断 + # 目前保持简单,后续可以扩展 + print("父子关系推断完成(当前为空实现)") + except Exception as e: + print(f"推断父子关系时出错: {e}") + + def _restoreModelAnimationInfo(self, model_node): + """恢复模型的动画信息""" + try: + # 从保存的标签中恢复动画信息 + if model_node.hasTag("saved_has_animations"): + model_node.setTag("has_animations", model_node.getTag("saved_has_animations")) + print(f"恢复模型 {model_node.getName()} 的动画信息") + + # 从保存的标签中恢复模型路径 + if model_node.hasTag("saved_model_path"): + model_path = model_node.getTag("saved_model_path") + # 处理跨平台路径问题 + if model_path: + # 将Linux风格路径转换为Windows风格路径(如果需要) + if model_path.startswith('/'): + # 尝试将其转换为Windows路径 + if ':' not in model_path: # 不是已经有效的Windows路径 + # 简单处理:移除前导斜杠 + model_path = model_path[1:] if len(model_path) > 1 else model_path + model_node.setTag("model_path", model_path) + + # 恢复内存创建标记 + if model_node.hasTag("saved_can_create_actor_from_memory"): + model_node.setTag("can_create_actor_from_memory", model_node.getTag("saved_can_create_actor_from_memory")) + + except Exception as e: + print(f"恢复模型 {model_node.getName()} 动画信息时出错: {e}") + + def _processModelAnimations(self, model_node): + """处理模型动画,确保在场景加载时正确识别动画信息""" + try: + # 检查模型是否已经有动画信息标签 + if model_node.hasTag("has_animations"): + has_animations = model_node.getTag("has_animations").lower() == "true" + if has_animations: + print(f"模型 {model_node.getName()} 已有动画信息") + return True + + # 检查模型是否包含动画相关节点 + character_nodes = model_node.findAllMatches("**/+Character") + anim_bundle_nodes = model_node.findAllMatches("**/+AnimBundleNode") + + has_animations = (character_nodes.getNumPaths() > 0 or + anim_bundle_nodes.getNumPaths() > 0) + + if has_animations: + print(f"检测到模型 {model_node.getName()} 包含动画:") + if character_nodes.getNumPaths() > 0: + print(f" CharacterNode数量: {character_nodes.getNumPaths()}") + if anim_bundle_nodes.getNumPaths() > 0: + print(f" AnimBundleNode数量: {anim_bundle_nodes.getNumPaths()}") + + # 保存动画信息到标签 + model_node.setTag("has_animations", "true") + + # 标记模型可以直接从内存中创建Actor + model_node.setTag("can_create_actor_from_memory", "true") + else: + model_node.setTag("has_animations", "false") + + return has_animations + + except Exception as e: + print(f"处理模型 {model_node.getName()} 动画时出错: {e}") + return False + + def _shouldSkipNodeInTree(self, nodePath): + """判断节点是否应该在场景树中跳过显示""" + + if nodePath.getName().startswith('ground'): + return True + + # 跳过render节点的递归 + if nodePath.getName() == "render": + return True + + # 跳过光源节点 + if nodePath.getName() in ["alight", "dlight"]: + return True + + # 跳过相机节点 + if nodePath.getName() in ["camera", "cam"]: + return True + + # 跳过3D文本和3D图像节点 + if (hasattr(nodePath.node(), "hasTag") and + nodePath.node().hasTag("gui_type") and + nodePath.node().getTag("gui_type") in ["3d_text", "3d_image"]): + return True + + # 跳过辅助节点 + if nodePath.getName().startswith(("gizmo", "selectionBox")): + return True + + return False + + def _recreateGUIElementsFromData(self, gui_data): + """根据保存的GUI数据重新创建GUI元素""" + try: + gui_manager = getattr(self.world, 'gui_manager', None) + property_manager = getattr(self.world, 'property_panel', None) + info_panel_manager = getattr(self.world, 'info_panel_manager', None) + if not gui_manager: + print("GUI管理器未找到,无法重建GUI元素") + return + print(f"开始重建 {len(gui_data)} 个GUI元素...") + + processed_names = set() + created_elements = {} + # 存储原始的缩放和位置信息,用于后续计算 + element_original_data = {} + + # 第一遍:收集所有元素信息 + for i, gui_info in enumerate(gui_data): + name = gui_info.get("name", f"gui_element_{i}") + element_original_data[name] = { + "scale": gui_info.get("scale", [1, 1, 1]), + "position": gui_info.get("position", [0, 0, 0]), + "parent_name": gui_info.get("parent_name") + } + + valid_parents = set() + for gui_info in gui_data: + name = gui_info.get("name", f"gui_element_{gui_info.get('index', 0)}") + valid_parents.add(name) + + if hasattr(self.world, 'gui_elements'): + for elem in self.world.gui_elements: + if elem and not elem.isEmpty(): + valid_parents.add(elem.getName()) + + valid_parents.add("render") + valid_parents.add("aspect2d") + valid_parents.add("render2d") + + pos = (0, 0, 0) + for i, gui_info in enumerate(gui_data): + try: + gui_type = gui_info.get("type", "unknown") + name = gui_info.get("name", f"gui_element_{i}") + position = gui_info.get("position", [0, 0, 0]) + scale = gui_info.get("scale", [1, 1, 1]) + tags = gui_info.get("tags", {}) + text = gui_info.get("text", "") + image_path = gui_info.get("image_path", "") + video_path = gui_info.get("video_path", "") + bg_image_path = gui_info.get("bg_image_path", "") # 背景图片路径 + panel_id = gui_info.get("panel_id", name) # 信息面板ID + panel_data = gui_info.get("panel_data", None) # 面板数据 + parent_name = gui_info.get("parent_name") + + # 检查是否已经处理过同名元素 + if name in processed_names: + print(f"跳过重复元素: {name}") + continue + + if parent_name and parent_name not in valid_parents: + print(f"⚠️ 跳过元素 {name},因为其父级 {parent_name} 不存在") + continue + + processed_names.add(name) + + print(f"重建GUI元素: {name} (类型: {gui_type})") + print(f" 位置: {position}") + print(f" 缩放: {scale}") + print(f" 文本: {text}") + print(f" 图像路径: {image_path}") + print(f" 背景图片路径: {bg_image_path}") + print(f" 视频路径: {video_path}") + + absolute_position = list(position) + absolute_scale = list(scale) + + if parent_name and parent_name in element_original_data: + parent_data = element_original_data[parent_name] + parent_scale = parent_data["scale"] + + if gui_type in ["3d_text", "3d_image", "button", "label", "entry", "2d_image", + "2d_video_screen"]: + # 位置需要乘以父级缩放来得到绝对位置 + for j in range(min(len(absolute_position), len(parent_scale))): + absolute_position[j] *= parent_scale[j] if len(parent_scale) > j else parent_scale[0] + + # 缩放需要乘以父级缩放来得到绝对缩放 + for j in range(min(len(absolute_scale), len(parent_scale))): + absolute_scale[j] *= parent_scale[j] if len(parent_scale) > j else parent_scale[0] + + print(f" 绝对位置: {absolute_position}") + print(f" 绝对缩放: {absolute_scale}") + + # 根据类型创建相应的GUI元素 + new_element = None + + if gui_type == "button" and hasattr(gui_manager, 'createGUIButton'): + new_element = gui_manager.createGUIButton( + pos=tuple(absolute_position), + text=text, + size=absolute_scale[0] if absolute_scale and len(absolute_scale) > 0 else 1.0 + ) + elif gui_type == "label" and hasattr(gui_manager, 'createGUILabel'): + scale_value = absolute_scale[0] if absolute_scale and len(absolute_scale) > 0 else 1.0 + new_element = gui_manager.createGUILabel( + pos=tuple(absolute_position), + text=text, + size=scale_value + ) + elif gui_type == "entry" and hasattr(gui_manager, 'createGUIEntry'): + new_element = gui_manager.createGUIEntry( + pos=tuple(absolute_position), + placeholder=text, + size=absolute_scale[0] if absolute_scale and len(absolute_scale) > 0 else 1.0 + ) + elif gui_type == "2d_image" and hasattr(gui_manager, 'createGUI2DImage'): + new_element = gui_manager.createGUI2DImage( + pos=tuple(absolute_position), + image_path=image_path, + size=(0.8,0.8,0.8) + ) + elif gui_type == "3d_text" and hasattr(gui_manager, 'createGUI3DText'): + size = absolute_scale[0] if absolute_scale and len(absolute_scale) > 0 else 0.5 + new_element = gui_manager.createGUI3DText( + pos=tuple(absolute_position), + text=text, + size=absolute_scale + ) + elif gui_type == "3d_image" and hasattr(gui_manager, 'createGUI3DImage'): + # 处理3D图像 + # 根据缩放值的数量处理尺寸 + # 修复:不使用 absolute_scale,因为 createGUI3DImage 中已经处理了尺寸 + # 而是在创建后通过 setScale 设置缩放 + size = (1.0, 1.0, 1.0) # 使用默认尺寸创建 + + new_element = gui_manager.createGUI3DImage( + pos=tuple(absolute_position), + image_path=image_path, + size=size # 使用默认尺寸 + ) + elif gui_type == "video_screen" and hasattr(gui_manager, 'createVideoScreen'): + print(f"重建的3d视频屏幕视频地址是{video_path}") + new_element = gui_manager.createVideoScreen( + pos=tuple(absolute_position), + size=absolute_scale, + video_path=video_path + ) + if video_path and new_element: + if video_path.startswith("http://") or video_path.startswith("https://"): + pass + else: + if hasattr(gui_manager, 'loadVideoFile'): + from direct.task.TaskManagerGlobal import taskMgr + + def load_video_file_task(task): + gui_manager.loadVideoFile(new_element, video_path) + return task.done + + taskMgr.doMethodLater(0.1, load_video_file_task, 'loadVideoFileTask') + + elif gui_type == "2d_video_screen" and hasattr(gui_manager, 'createGUI2DVideoScreen'): + print(f"重建的2d视频屏幕视频地址是{video_path}") + new_element = gui_manager.createGUI2DVideoScreen( + pos=tuple(absolute_position), + size=absolute_scale, + video_path=video_path + ) + if video_path and new_element: + if video_path.startswith("http://") or video_path.startswith("https://"): + pass + else: + if hasattr(property_manager, 'load2DVideoFile'): + from direct.task.TaskManagerGlobal import taskMgr + + def load_2d_video_file_task(task): + property_manager.load2DVideoFile(new_element, video_path) + return task.done + + taskMgr.doMethodLater(0.1, load_2d_video_file_task, 'load2DVideoFileTask') + elif gui_type == "info_panel": + new_element = self.world.info_panel_manager.onCreateSampleInfoPanel() + # 如果创建成功,设置属性 + if new_element: + # 如果返回的是列表(多选创建),取第一个 + if isinstance(new_element, list): + new_element = new_element[0] + + # 设置名称 + new_element.setName(name) + + # 设置变换 + new_element.setPos(*position) + + if len(scale) >= 3: + new_element.setScale(scale[0], scale[1], scale[2]) + elif len(scale) >= 1: + new_element.setScale(scale[0]) + + # 恢复GUI元素的可见性状态 + user_visible = gui_info.get("user_visible", True) + new_element.setPythonTag("user_visible", user_visible) + + # 应用可见性状态 + if hasattr(self.world, 'property_panel'): + self.world.property_panel._syncEffectiveVisibility(new_element) + else: + # 如果没有属性面板,直接应用可见性 + if user_visible: + new_element.show() + else: + new_element.hide() + + # 设置标签 + # 对于NodePath对象 + if hasattr(new_element, 'setTag'): + for tag_name, tag_value in tags.items(): + # 跳过变换标签,因为我们已经设置了 + if tag_name not in ["transform_pos", "transform_hpr", "transform_scale"]: + new_element.setTag(tag_name, tag_value) + # 对于DirectGUI对象,使用自定义标签存储 + elif hasattr(new_element, '_tags'): + new_element._tags.update(tags) + + created_elements[name] = new_element + + print(f"GUI元素重建成功: {name}") + else: + print(f"无法重建GUI元素: {name} (类型: {gui_type})") + + except Exception as e: + print(f"重建GUI元素失败 {name}: {e}") + import traceback + traceback.print_exc() + continue + + # 第二遍:设置父子级关系并更新Qt树 + print("开始设置父子级关系...") + try: + # 创建父子级关系映射 + parent_child_map = {} + for gui_info in gui_data: + name = gui_info.get("name") + parent_name = gui_info.get("parent_name") + + if name and parent_name and parent_name in created_elements: + parent_child_map[name] = parent_name + print(f"父子级关系映射: {parent_name} -> {name}") + + # 按正确的顺序设置父子级关系并更新Qt树 + tree_widget = self._get_tree_widget() + if tree_widget: + # 先将所有元素添加到Qt树中 + qt_tree_items = {} + for name, element in created_elements.items(): + # 尝试在Qt树中找到对应的项,如果找不到则创建 + qt_item = self._findOrCreateQtTreeItem(tree_widget, element, name) + if qt_item: + qt_tree_items[name] = qt_item + + # 然后设置父子级关系 + for child_name, parent_name in parent_child_map.items(): + try: + if child_name in created_elements and parent_name in created_elements: + child_element = created_elements[child_name] + parent_element = created_elements[parent_name] + + # 设置父子级关系 + if hasattr(child_element, 'reparentTo'): + child_element.reparentTo(parent_element) + print(f"成功设置父子级关系: {parent_name} -> {child_name}") + + # 更新Qt树显示 + if child_name in qt_tree_items and parent_name in qt_tree_items: + child_item = qt_tree_items[child_name] + parent_item = qt_tree_items[parent_name] + + # 从当前位置移除子项 + if child_item.parent(): + child_item.parent().removeChild(child_item) + else: + # 如果是顶级项,从树中移除 + index = tree_widget.indexOfTopLevelItem(child_item) + if index >= 0: + tree_widget.takeTopLevelItem(index) + + # 将子项添加到新的父项下 + parent_item.addChild(child_item) + print(f"Qt树更新: {child_name} 移动到 {parent_name} 下") + else: + print(f"元素 {child_name} 不支持 reparentTo 操作") + else: + print(f"元素未找到: 父级={parent_name}, 子级={child_name}") + except Exception as e: + print(f"设置父子级关系失败 {parent_name} -> {child_name}: {e}") + continue + else: + # 如果没有tree_widget,只设置父子级关系 + for child_name, parent_name in parent_child_map.items(): + try: + if child_name in created_elements and parent_name in created_elements: + child_element = created_elements[child_name] + parent_element = created_elements[parent_name] + + # 设置父子级关系 + if hasattr(child_element, 'reparentTo'): + child_element.reparentTo(parent_element) + print(f"成功设置父子级关系: {parent_name} -> {child_name}") + except Exception as e: + print(f"设置父子级关系失败 {parent_name} -> {child_name}: {e}") + continue + + except Exception as e: + print(f"设置父子级关系时出错: {e}") + # 第三遍:重新挂载脚本 + print("开始重新挂载脚本...") + for gui_info in gui_data: + try: + name = gui_info.get("name") + if name in created_elements and "scripts" in gui_info: + new_element = created_elements[name] + + # 重新挂载脚本(如果有的话) + if "scripts" in gui_info and hasattr(self.world, + 'script_manager') and self.world.script_manager: + script_manager = self.world.script_manager + for script_info in gui_info["scripts"]: + script_name = script_info["name"] + script_file = script_info.get("file", "") + + print(f"尝试重新挂载脚本: {script_name} from {script_file}") + + # 检查脚本是否已加载 + if script_name not in script_manager.loader.script_classes: + # 如果脚本未加载,尝试从保存的文件路径加载 + if script_file and os.path.exists(script_file): + print(f"从文件加载脚本: {script_file}") + loaded_class = script_manager.load_script_from_file(script_file) + if loaded_class is None: + print(f"从文件加载脚本失败: {script_file}") + # 如果从文件加载失败,尝试在脚本目录中查找 + script_path = self._find_script_in_directory(script_name) + if script_path: + print(f"从目录找到脚本并加载: {script_path}") + script_manager.load_script_from_file(script_path) + else: + # 如果没有文件路径或文件不存在,尝试在脚本目录中查找 + script_path = self._find_script_in_directory(script_name) + if script_path: + print(f"从目录找到脚本并加载: {script_path}") + script_manager.load_script_from_file(script_path) + else: + print(f"找不到脚本文件: {script_name}") + + # 为元素添加脚本 + script_component = script_manager.add_script_to_object(new_element, script_name) + if script_component: + print(f"成功为 {name} 添加脚本: {script_name}") + else: + print(f"为 {name} 添加脚本失败: {script_name}") + except Exception as e: + print(f"重新挂载脚本失败: {e}") + import traceback + traceback.print_exc() + continue + + print(f"GUI元素重建完成,共创建 {len(created_elements)} 个元素") + + except Exception as e: + print(f"重建GUI元素时出错: {e}") + import traceback + traceback.print_exc() + + def _findOrCreateQtTreeItem(self, tree_widget, target_element, element_name): + """在Qt树中查找或创建指定元素对应的项""" + try: + # 首先尝试查找现有的项 + existing_item = self._findQtTreeItem(tree_widget, target_element) + if existing_item: + return existing_item + + # 如果找不到,创建新的项 + # 找到场景根节点 + scene_root = None + for i in range(tree_widget.topLevelItemCount()): + top_item = tree_widget.topLevelItem(i) + if top_item.data(0, Qt.UserRole + 1) == "SCENE_ROOT": + scene_root = top_item + break + + if not scene_root: + print("无法找到场景根节点") + return None + + # 创建新的Qt树项 + new_item = QTreeWidgetItem(scene_root, [element_name]) + new_item.setData(0, Qt.UserRole, target_element) + new_item.setData(0, Qt.UserRole + 1, "SCENE_NODE") # 或根据元素类型设置适当的类型 + + print(f"为元素 {element_name} 创建了新的Qt树项") + return new_item + + except Exception as e: + print(f"查找或创建Qt树项失败: {e}") + return None + + def _findQtTreeItem(self, tree_widget, target_element): + """在Qt树中查找指定元素对应的项""" + try: + def search_recursive(parent_item): + # 检查当前项 + if parent_item: + item_element = parent_item.data(0, Qt.UserRole) + if item_element == target_element: + return parent_item + + # 递归检查子项 + for i in range(parent_item.childCount()): + child_item = parent_item.child(i) + result = search_recursive(child_item) + if result: + return result + return None + + # 从根节点开始搜索 + root = tree_widget.invisibleRootItem() + for i in range(root.childCount()): + top_item = root.child(i) + result = search_recursive(top_item) + if result: + return result + + return None + except Exception as e: + print(f"查找Qt树项失败: {e}") + return None + def _find_script_in_directory(self, script_name): + """在脚本目录中查找脚本文件""" + try: + if hasattr(self.world, 'script_manager') and self.world.script_manager: + script_manager = self.world.script_manager + scripts_dir = script_manager.scripts_directory + + if os.path.exists(scripts_dir): + # 首先精确匹配 + for file_name in os.listdir(scripts_dir): + if file_name.endswith('.py'): + base_name = os.path.splitext(file_name)[0] + if base_name == script_name: + return os.path.join(scripts_dir, file_name) + + # 如果没有精确匹配,尝试模糊匹配 + for file_name in os.listdir(scripts_dir): + if file_name.endswith('.py'): + base_name = os.path.splitext(file_name)[0] + if script_name.lower() in base_name.lower() or base_name.lower() in script_name.lower(): + return os.path.join(scripts_dir, file_name) + except Exception as e: + print(f"查找脚本文件时出错: {e}") + + return None + + def _recreateSpotLight(self, light_node): + """重新创建聚光灯""" + try: + from RenderPipelineFile.rpcore import SpotLight + from panda3d.core import Vec3 + + # 创建聚光灯对象 + light = SpotLight() + light.direction = Vec3(0, 0, -1) + light.fov = 70 + light.set_color_from_temperature(5 * 1000.0) + + # 恢复保存的属性 + if light_node.hasTag("light_energy"): + light.energy = float(light_node.getTag("light_energy")) + else: + light.energy = 5000 + + light.radius = 1000 + light.casts_shadows = True + light.shadow_map_resolution = 256 + + light_pos = light_node.getPos() + light.setPos(light_pos) + + # 添加到渲染管线 + render_pipeline = get_render_pipeline() + render_pipeline.add_light(light) + + # 保存光源对象引用 + light_node.setPythonTag("rp_light_object", light) + + # 添加到管理列表 + self.Spotlight.append(light_node) + + # 确保灯光节点有正确的标签,以便在场景树更新时被识别 + if not light_node.hasTag("is_scene_element"): + light_node.setTag("is_scene_element", "1") + light_node.setTag("is_scene_element", "1") + light_node.setTag("element_type", "spotlight") + light_node.setTag("tree_item_type", "LIGHT_NODE") + + if light_node.hasTag("stored_energy"): + stored_energy = float(light_node.getTag("stored_energy")) + if stored_energy > 0: + light_node.setTag("stored_energy", str(stored_energy)) + + user_visible = True + if light_node.hasTag("user_visible"): + user_visible = light_node.getTag("user_visible").lower() == "true" + + light_node.setPythonTag("user_visible",user_visible) + if not user_visible: + self.toggleLightVisibility(light_node,False) + except Exception as e: + print(f"重新创建聚光灯失败: {str(e)}") + import traceback + traceback.print_exc() + + def _recreatePointLight(self, light_node): + """重新创建点光源""" + try: + from RenderPipelineFile.rpcore import PointLight + from QMeta3D.Meta3DWorld import get_render_pipeline + + # 创建点光源对象 + light = PointLight() + + # 恢复保存的属性 + if light_node.hasTag("light_energy"): + light.energy = float(light_node.getTag("light_energy")) + else: + light.energy = 5000 + + light.radius = 1000 + light.inner_radius = 0.4 + light.set_color_from_temperature(5 * 1000.0) + light.casts_shadows = True + light.shadow_map_resolution = 256 + + # 设置位置 + light.setPos(light_node.getPos()) + + # 添加到渲染管线 + render_pipeline = get_render_pipeline() + render_pipeline.add_light(light) + + # 保存光源对象引用 + light_node.setPythonTag("rp_light_object", light) + + # 添加到管理列表 + self.Pointlight.append(light_node) + + # 确保灯光节点有正确的标签,以便在场景树更新时被识别 + if not light_node.hasTag("is_scene_element"): + light_node.setTag("is_scene_element", "1") + + light_node.setTag("is_scene_element", "1") + light_node.setTag("element_type", "pointlight") + light_node.setTag("tree_item_type", "LIGHT_NODE") + + if light_node.hasTag("stored_energy"): + stored_energy = float(light_node.getTag("stored_energy")) + if stored_energy > 0: + light_node.setTag("stored_energy", str(stored_energy)) + + user_visible = True + if light_node.hasTag("user_visible"): + user_visible = light_node.getTag("user_visible").lower()=="true" + + light_node.setPythonTag("user_visible",user_visible) + + if not user_visible: + self.toggleLightVisibility(light_node,False) + except Exception as e: + print(f"重新创建点光源失败: {str(e)}") + import traceback + traceback.print_exc() + + def _cleanupAuxiliaryNodes(self): + """清理场景中可能存在的辅助节点""" + try: + # 查找并移除所有坐标轴节点 + gizmo_nodes = self.world.render.findAllMatches("**/gizmo*") + for node in gizmo_nodes: + if not node.isEmpty(): + node.removeNode() + print(f"清理坐标轴节点: {node.getName()}") + + # 查找并移除所有选择框节点 + selection_box_nodes = self.world.render.findAllMatches("**/selectionBox*") + for node in selection_box_nodes: + if not node.isEmpty(): + node.removeNode() + print(f"清理选择框节点: {node.getName()}") + + # 停止相关的更新任务 + from direct.task.TaskManagerGlobal import taskMgr + taskMgr.remove("updateGizmo") + taskMgr.remove("updateSelectionBox") + + print("辅助节点清理完成") + except Exception as e: + print(f"清理辅助节点时出错: {e}") + + # ==================== 模型管理 ==================== + + def deleteModel(self, model): + """删除模型""" + try: + if model in self.models: + tree_widget = self._get_tree_widget() + if not tree_widget: + return False + + tree_widget.delete_items(tree_widget.selectedItems()) + # model.removeNode() + # self.models.remove(model) + # self.updateSceneTree() + print(f"删除模型: {model.getName()}") + return True + except Exception as e: + print(f"删除模型失败: {str(e)}") + return False + + def clearAllModels(self): + """清除所有模型""" + pass + # try: + # for model in self.models: + # model.removeNode() + # self.models.clear() + # self.updateSceneTree() + # print("清除所有模型完成") + # except Exception as e: + # print(f"清除所有模型失败: {str(e)}") + + def getModels(self): + """获取模型列表""" + return self.models.copy() + + def getModelCount(self): + """获取模型数量""" + return len(self.models) + + def findModelByName(self, name): + """根据名称查找模型""" + for model in self.models: + if model.getName() == name: + return model + return None + + # ==================== 帮助方法 ==================== + + def processLoadedModel(self, model): + """处理加载完成的模型(用于异步加载回调)""" + if model: + # 添加到模型列表 + self.models.append(model) + + # 设置基础变换 + model.setPos(0, 0, 0) + model.setHpr(0, 0, 0) + model.setScale(1, 1, 1) + + # 应用材质 + self.processMaterials(model) + + # 设置碰撞检测 + self.setupCollision(model) + + # 更新场景树 + self.updateSceneTree() + + print(f"异步加载模型完成: {model.getName()}") + + def createSpotLight(self, pos=(0, 0, 0)): + """创建聚光灯 - 支持多选创建,优化版本""" + try: + from RenderPipelineFile.rpcore import SpotLight + from QMeta3D.Meta3DWorld import get_render_pipeline + from panda3d.core import Vec3, NodePath + from PyQt5.QtCore import Qt + + print(f"🔆 开始创建聚光灯,位置: {pos}") + + # 获取树形控件 + tree_widget = self._get_tree_widget() + if not tree_widget: + print("❌ 无法访问树形控件") + return None + + # 获取目标父节点列表 + target_parents = tree_widget.get_target_parents_for_creation() + if not target_parents: + print("❌ 没有找到有效的父节点") + return None + + created_lights = [] + render_pipeline = get_render_pipeline() + + # 为每个有效的父节点创建聚光灯 + for parent_item, parent_node in target_parents: + try: + # 生成唯一名称 + light_name = f"Spotlight_{len(self.Spotlight)}" + + # 创建挂载节点 - 挂载到选中的父节点 + light_np = NodePath(light_name) + light_np.reparentTo(parent_node) # 挂载到父节点而不是render + light_np.setPos(*pos) + + light_np.setTransform(TransformState.makeIdentity()) + + # 创建聚光灯对象 + light = SpotLight() + light.direction = Vec3(0, 0, -1) + light.fov = 70 + light.set_color_from_temperature(5 * 1000.0) + light.energy = 5000 + light.radius = 1000 + light.casts_shadows = True + light.shadow_map_resolution = 256 + light.setPos(pos) + + # 添加到渲染管线 + render_pipeline.add_light(light) + + # 设置节点属性和标签 + light_np.setTag("light_type", "spot_light") + light_np.setTag("is_scene_element", "1") + light_np.setTag("tree_item_type", "LIGHT_NODE") + light_np.setTag("light_energy", str(light.energy)) + light_np.setTag("created_by_user", "1") + light_np.setTag("element_type","spotlight") + + # 保存光源对象引用 + light_np.setPythonTag("rp_light_object", light) + + # 添加到管理列表 + self.Spotlight.append(light_np) + + print(f"✅ 为 {parent_item.text(0)} 创建聚光灯成功: {light_name}") + + # 在Qt树形控件中添加对应节点 + qt_item = tree_widget.add_node_to_tree_widget(light_np, parent_item, "LIGHT_NODE") + if qt_item: + created_lights.append((light_np, qt_item)) + else: + created_lights.append((light_np, None)) + print("⚠️ Qt树节点添加失败,但Panda3D对象已创建") + + except Exception as e: + print(f"❌ 为 {parent_item.text(0)} 创建聚光灯失败: {str(e)}") + import traceback + traceback.print_exc() + continue + + # 处理创建结果 + if not created_lights: + print("❌ 没有成功创建任何聚光灯") + return None + + # 选中最后创建的光源 + if created_lights: + last_light_np, last_qt_item = created_lights[-1] + if last_qt_item: + tree_widget.setCurrentItem(last_qt_item) + # 更新选择和属性面板 + tree_widget.update_selection_and_properties(last_light_np, last_qt_item) + + print(f"🎉 总共创建了 {len(created_lights)} 个聚光灯") + + # 返回值处理 + if len(created_lights) == 1: + return created_lights[0][0] # 单个光源返回NodePath + else: + return [light_np for light_np, _ in created_lights] # 多个光源返回列表 + + except Exception as e: + print(f"❌ 创建聚光灯过程失败: {str(e)}") + import traceback + traceback.print_exc() + return None + + def createPointLight(self, pos=(0, 0, 0)): + """创建点光源 - 支持多选创建,优化版本""" + try: + from RenderPipelineFile.rpcore import PointLight + from QMeta3D.Meta3DWorld import get_render_pipeline + from panda3d.core import Vec3, NodePath + from PyQt5.QtCore import Qt + + print(f"💡 开始创建点光源,位置: {pos}") + + # 获取树形控件 + tree_widget = self._get_tree_widget() + if not tree_widget: + print("❌ 无法访问树形控件") + return None + + # 获取目标父节点列表 + target_parents = tree_widget.get_target_parents_for_creation() + if not target_parents: + print("❌ 没有找到有效的父节点") + return None + + created_lights = [] + render_pipeline = get_render_pipeline() + + # 为每个有效的父节点创建点光源 + for parent_item, parent_node in target_parents: + try: + # 生成唯一名称 + light_name = f"Pointlight_{len(self.Pointlight)}" + + # 创建挂载节点 - 挂载到选中的父节点 + light_np = NodePath(light_name) + light_np.reparentTo(parent_node) # 挂载到父节点而不是render + light_np.setPos(*pos) + + # 确保变换矩阵有效 + light_np.setTransform(TransformState.makeIdentity()) + + # 创建点光源对象 + light = PointLight() + light.setPos(*pos) + light.energy = 5000 + light.radius = 1000 + light.inner_radius = 0.4 + light.set_color_from_temperature(5 * 1000.0) + light.casts_shadows = True + light.shadow_map_resolution = 256 + + # 添加到渲染管线 + render_pipeline.add_light(light) + + # 设置节点属性和标签 + light_np.setTag("light_type", "point_light") + light_np.setTag("is_scene_element", "1") + light_np.setTag("tree_item_type", "LIGHT_NODE") + light_np.setTag("light_energy", str(light.energy)) + light_np.setTag("created_by_user", "1") + light_np.setTag("element_type","pointlight") + + # 保存光源对象引用 + light_np.setPythonTag("rp_light_object", light) + + # 添加到管理列表 + self.Pointlight.append(light_np) + + print(f"✅ 为 {parent_item.text(0)} 创建点光源成功: {light_name}") + + # 在Qt树形控件中添加对应节点 + qt_item =tree_widget.add_node_to_tree_widget(light_np, parent_item, "LIGHT_NODE") + if qt_item: + created_lights.append((light_np, qt_item)) + else: + created_lights.append((light_np, None)) + print("⚠️ Qt树节点添加失败,但Panda3D对象已创建") + + except Exception as e: + print(f"❌ 为 {parent_item.text(0)} 创建点光源失败: {str(e)}") + continue + + # 处理创建结果 + if not created_lights: + print("❌ 没有成功创建任何点光源") + return None + + # 选中最后创建的光源 + if created_lights: + last_light_np, last_qt_item = created_lights[-1] + if last_qt_item: + tree_widget.setCurrentItem(last_qt_item) + # 更新选择和属性面板 + tree_widget.update_selection_and_properties(last_light_np, last_qt_item) + + print(f"🎉 总共创建了 {len(created_lights)} 个点光源") + + # 返回值处理 + if len(created_lights) == 1: + return created_lights[0][0] # 单个光源返回NodePath + else: + return [light_np for light_np, _ in created_lights] # 多个光源返回列表 + + except Exception as e: + print(f"❌ 创建点光源过程失败: {str(e)}") + import traceback + traceback.print_exc() + return None + + def isLightObject(self, nodePath): + """检查是否为灯光对象""" + try: + if not nodePath: + return False + + + # 方法1: 检查PythonTag + if nodePath.hasPythonTag("rp_light_object"): + rp_light = nodePath.getPythonTag("rp_light_object") + if rp_light is not None: + return True + + # 方法2: 检查element_type标签 + if nodePath.hasTag("element_type"): + element_type = nodePath.getTag("element_type") + if element_type in ["spotlight", "pointlight"]: + return True + + # 方法3: 检查tree_item_type标签 + if nodePath.hasTag("tree_item_type"): + tree_item_type = nodePath.getTag("tree_item_type") + if tree_item_type == "LIGHT_NODE": + return True + + # 方法4: 通过名称模式匹配(作为后备方案) + node_name = nodePath.getName().lower() + if "spotlight" in node_name or "pointlight" in node_name: + return True + + return False + except Exception as e: + print(f"检查灯光对象时出错: {e}") + import traceback + traceback.print_exc() + return False + + def toggleLightVisibility(self, light_node, visible): + """切换灯光可见性""" + try: + print(f"切换灯光可见性: {light_node.getName()}, 可见={visible}") + + # 保存用户可见性状态到该特定节点 + light_node.setPythonTag("user_visible", visible) + + # 获取该特定灯光对象 + rp_light_object = light_node.getPythonTag("rp_light_object") + if not rp_light_object: + print(f"错误: {light_node.getName()} 未找到RP灯光对象引用") + return + + # 获取RenderPipeline实例 + from QMeta3D.Meta3DWorld import get_render_pipeline + render_pipeline = get_render_pipeline() + + if not render_pipeline: + print("错误: 无法获取RenderPipeline实例") + return + + try: + if visible: + if light_node.hasTag("stored_energy"): + stored_energy = float(light_node.getTag("stored_energy")) + rp_light_object.energy=stored_energy + print(f"已恢复灯光强度: {light_node.getName()}, 能量={stored_energy}") + # 启用特定灯光 + # render_pipeline.add_light(rp_light_object) + # print(f"已添加灯光到渲染管线: {light_node.getName()}") + else: + # 禁用特定灯光 + current_energy = rp_light_object.energy + if current_energy != 0.0: + light_node.setTag("stored_energy", str(current_energy)) + elif light_node.hasTag("stored_energy"): + stored_energy = float(light_node.getTag("stored_energy")) + current_energy = stored_energy + else: + current_energy = 0.0 + rp_light_object.energy = 0.0 + print(f"已禁用灯光: {light_node.getName()}, 保存的能量={current_energy}") + # render_pipeline.remove_light(rp_light_object) + # print(f"已从渲染管线移除灯光: {light_node.getName()}") + except Exception as e: + print(f"操作RenderPipeline灯光时出错: {e}") + + # 控制节点显示状态(可选,主要是视觉上的) + if visible: + light_node.show() + else: + light_node.hide() + + print(f"灯光可见性设置完成: {visible}") + except Exception as e: + print(f"切换灯光可见性失败: {str(e)}") + import traceback + traceback.print_exc() + + def _get_tree_widget(self): + """安全获取树形控件""" + try: + if (hasattr(self.world, 'interface_manager') and + hasattr(self.world.interface_manager, 'treeWidget')): + return self.world.interface_manager.treeWidget + except AttributeError: + pass + return None + + + # ==================== GLB 转换方法 ==================== + + #=================================================== + + def _shouldConvertToGLB(self, filepath): + """判断是否应该转换为GLB格式""" + ext = os.path.splitext(filepath)[1].lower() + # 需要转换的格式:FBX, OBJ, DAE等(但不转换已经是GLB/GLTF的) + convert_formats = ['.fbx', '.obj', '.dae', '.3ds', '.blend'] + return ext in convert_formats + + def _convertToGLBWithProgress(self, filepath): + """带进度显示的GLB转换""" + try: + from PyQt5.QtWidgets import QProgressDialog, QApplication + from PyQt5.QtCore import Qt + + # 创建进度对话框 + progress = QProgressDialog("正在转换模型格式以获得更好的动画支持...", "取消", 0, 100) + progress.setWindowTitle("模型格式转换") + progress.setWindowModality(Qt.WindowModal) + progress.show() + QApplication.processEvents() + + try: + result = self._convertToGLB(filepath, progress) + progress.hide() + return result + except Exception as e: + progress.hide() + print(f"转换过程出错: {e}") + return None + + except ImportError: + # 如果没有 PyQt5,直接转换 + return self._convertToGLB(filepath) + + def _convertToGLB(self, filepath, progress=None): + """将模型文件转换为GLB格式""" + try: + print(f"[GLB转换] 开始转换: {filepath}") + + if progress: + progress.setValue(10) + progress.setLabelText("准备转换...") + from PyQt5.QtWidgets import QApplication + QApplication.processEvents() + + # 准备输出路径 + base_name = os.path.splitext(os.path.basename(filepath))[0] + output_dir = os.path.dirname(filepath) + glb_path = os.path.join(output_dir, f"{base_name}_auto_converted.glb") + + # 如果已经存在转换后的文件,直接使用 + if os.path.exists(glb_path): + # 检查文件时间,如果原文件更新则重新转换 + original_time = os.path.getmtime(filepath) + converted_time = os.path.getmtime(glb_path) + if converted_time > original_time: + print(f"[GLB转换] 使用现有转换文件: {glb_path}") + if progress: + progress.setValue(100) + return glb_path + + if progress: + progress.setValue(20) + progress.setLabelText("尝试 Blender 转换...") + QApplication.processEvents() + + # 方法1: 使用 Blender 进行转换 + if self._convertWithBlender(filepath, glb_path, progress): + return glb_path + + if progress: + progress.setValue(60) + progress.setLabelText("尝试 FBX2glTF 转换...") + QApplication.processEvents() + + # 方法2: 使用 FBX2glTF (如果可用) + if self._convertWithFBX2glTF(filepath, glb_path, progress): + return glb_path + + if progress: + progress.setValue(80) + progress.setLabelText("尝试 Assimp 转换...") + QApplication.processEvents() + + # 方法3: 使用 Assimp + if self._convertWithAssimp(filepath, glb_path, progress): + return glb_path + + #print(f"[GLB转换] 所有转换方法都失败,既然没有可以转换格式的工具和环境那么就用原始文件,不一定非要转换") + return None + + except Exception as e: + print(f"[GLB转换] 转换过程出错: {e}") + return None + + def _convertWithBlender(self, input_path, output_path, progress=None): + """使用 Blender 进行转换""" + try: + import subprocess + import tempfile + + print(f"[Blender转换] {input_path} → {output_path}") + + # 创建 Blender 脚本 + script_content = f''' +import bpy +import sys +import os + +# 清理默认场景 +bpy.ops.object.select_all(action='SELECT') +bpy.ops.object.delete(use_global=False) + +print("开始导入文件...") + +# 根据文件类型选择导入方法 +input_file = "{input_path}" +output_file = "{output_path}" + +try: + ext = os.path.splitext(input_file)[1].lower() + + if ext == '.fbx': + bpy.ops.import_scene.fbx(filepath=input_file) + elif ext == '.obj': + bpy.ops.import_scene.obj(filepath=input_file) + elif ext == '.dae': + bpy.ops.wm.collada_import(filepath=input_file) + elif ext == '.blend': + bpy.ops.wm.open_mainfile(filepath=input_file) + else: + print(f"不支持的格式: {{ext}}") + sys.exit(1) + + print("导入成功,开始导出GLB...") + + # 导出为 GLB,保留动画 + bpy.ops.export_scene.gltf( + filepath=output_file, + export_format='GLB', + export_animations=True, + export_force_sampling=True, + export_frame_range=True, + export_current_frame=False, + export_skins=True, + export_morph=True, + export_lights=True, + export_cameras=False + ) + + print("GLB导出成功!") + +except Exception as e: + print(f"转换失败: {{e}}") + sys.exit(1) +''' + + # 写入临时脚本文件 + with tempfile.NamedTemporaryFile(mode='w', suffix='.py', delete=False) as temp_file: + temp_file.write(script_content) + script_path = temp_file.name + + try: + # 执行 Blender 转换 + result = subprocess.run([ + 'blender', '--background', '--python', script_path + ], capture_output=True, text=True, timeout=180) + + # 清理临时文件 + os.unlink(script_path) + + if result.returncode == 0 and os.path.exists(output_path): + print(f"[Blender转换] 转换成功") + return True + else: + print(f"[Blender转换] 转换失败: {result.stderr}") + return False + + except subprocess.TimeoutExpired: + print(f"[Blender转换] 转换超时") + return False + except FileNotFoundError: + print(f"[Blender转换] Blender 未安装") + return False + + except Exception as e: + print(f"[Blender转换] 转换过程出错: {e}") + return False + + def _convertWithFBX2glTF(self, input_path, output_path, progress=None): + """使用 FBX2glTF 进行转换(仅支持FBX)""" + try: + import subprocess + + if not input_path.lower().endswith('.fbx'): + return False + + print(f"[FBX2glTF转换] {input_path} → {output_path}") + + # 使用 FBX2glTF 转换 + result = subprocess.run([ + 'FBX2glTF', input_path, '--output', output_path, '--binary' + ], capture_output=True, text=True, timeout=120) + + if result.returncode == 0 and os.path.exists(output_path): + print(f"[FBX2glTF转换] 转换成功") + return True + else: + print(f"[FBX2glTF转换] 转换失败: {result.stderr}") + return False + + except subprocess.TimeoutExpired: + print(f"[FBX2glTF转换] 转换超时") + return False + except FileNotFoundError: + print(f"[FBX2glTF转换] FBX2glTF 未安装") + return False + except Exception as e: + print(f"[FBX2glTF转换] 转换过程出错: {e}") + return False + + def _convertWithAssimp(self, input_path, output_path, progress=None): + """使用 PyAssimp 进行转换""" + try: + import pyassimp + + print(f"[PyAssimp转换] {input_path} → {output_path}") + + # 加载模型 + scene = pyassimp.load(input_path) + if not scene: + print(f"[PyAssimp转换] 加载模型失败") + return False + + if progress: + progress.setValue(30) + + # 导出为GLB格式 + pyassimp.export(scene, output_path, "glb2") + + if progress: + progress.setValue(80) + + # 释放资源 + pyassimp.release(scene) + + if os.path.exists(output_path): + print(f"[PyAssimp转换] 转换成功") + return True + else: + print(f"[PyAssimp转换] 转换失败: 输出文件未生成") + return False + + except ImportError: + print(f"[PyAssimp转换] PyAssimp 未安装") + return False + except Exception as e: + print(f"[PyAssimp转换] 转换过程出错: {e}") + return False + + def load_cesium_tileset(self, tileset_url, position=(0, 0, 0)): + """ + 加载 Cesium 3D Tileset - 采用新的创建逻辑,支持多选和更完善的UI交互。 + """ + try: + from panda3d.core import NodePath + print(f"🗺️ 开始加载 Cesium 3D Tiles: {tileset_url}") + + # 1. 获取UI控件和目标父节点 + tree_widget = self._get_tree_widget() + if not tree_widget: + print("❌ 无法访问树形控件") + return None + + target_parents = tree_widget.get_target_parents_for_creation() + if not target_parents: + print("❌ 没有找到有效的父节点来附加Tileset") + return None + + created_tilesets = [] + + # 2. 遍历所有选中的父节点,并为其创建Tileset + for parent_item, parent_node in target_parents: + try: + # 生成唯一名称 + node_name = f"cesium_tileset_{len(self.tilesets)}" + + # 创建一个容器节点来管理tileset,并挂载到父节点 + tileset_node = parent_node.attachNewNode(node_name) + tileset_node.setPos(*position) + + # 添加标签以便场景识别和保存 + tileset_node.setTag("is_scene_element", "1") + tileset_node.setTag("tree_item_type", "CESIUM_TILESET_NODE") + tileset_node.setTag("element_type", "cesium_tileset") + tileset_node.setTag("tileset_url", tileset_url) + # 使用唯一名称作为文件标识,代替索引 + tileset_node.setTag("file", node_name) + + # 存储tileset核心信息 + tileset_info = { + 'url': tileset_url, + 'node': tileset_node, + 'position': position, + 'tiles': {} # 用于后续管理瓦片 + } + self.tilesets.append(tileset_info) + + # 创建一个临时的可视化占位符,让用户能看到节点已添加 + self._create_placeholder_geometry(tileset_node) + + # 异步加载tileset的实际数据 + self._load_tileset_async(tileset_url, tileset_info) + + print(f"✅ 为 {parent_item.text(0)} 加载 Tileset 成功: {node_name}") + + # 在Qt树形控件中添加对应节点 + qt_item = tree_widget.add_node_to_tree_widget(tileset_node, parent_item, "CESIUM_TILESET_NODE") + if qt_item: + created_tilesets.append((tileset_node, qt_item)) + else: + created_tilesets.append((tileset_node, None)) + print("⚠️ Qt树节点添加失败,但Panda3D对象已创建") + + except Exception as e: + print(f"❌ 为 {parent_item.text(0)} 加载 Tileset 失败: {str(e)}") + continue # 继续尝试为下一个父节点创建 + + # 3. 处理创建结果 + if not created_tilesets: + print("❌ 没有成功加载任何 Tileset") + return None + + # 选中最后创建的Tileset并更新UI + if created_tilesets: + last_tileset_node, last_qt_item = created_tilesets[-1] + if last_qt_item: + tree_widget.setCurrentItem(last_qt_item) + # 更新选择状态和属性面板 + tree_widget.update_selection_and_properties(last_tileset_node, last_qt_item) + + print(f"🎉 总共加载了 {len(created_tilesets)} 个 Cesium Tileset 实例") + + # 4. 返回值处理 + if len(created_tilesets) == 1: + return created_tilesets[0][0] # 单个实例返回NodePath + else: + return [node for node, _ in created_tilesets] # 多个实例返回NodePath列表 + + except Exception as e: + print(f"❌ 加载 Cesium 3D Tiles 过程失败: {str(e)}") + import traceback + traceback.print_exc() + return None + + def _load_tileset_async(self, tileset_url, tileset_info): + """异步加载 tileset 数据""" + + async def load_tileset(): + try: + async with aiohttp.ClientSession() as session: + async with session.get(tileset_url) as response: + if response.status == 200: + tileset_data = await response.json() + self._parse_tileset(tileset_data, tileset_info) + print(f"✓ Tileset 数据加载完成") + else: + print(f"✗ Tileset 加载失败: {response.status}") + except Exception as e: + print(f"✗ Tileset 加载出错: {e}") + + # 在 Panda3D 的任务系统中运行异步任务 + task = asyncio.ensure_future(load_tileset()) + self._current_asyncio_task = task # 保存任务引用 + self.world.taskMgr.add(self._check_async_task, "check_tileset_load", appendTask=True) + + def _check_async_task(self, panda3d_task): + # 检查 asyncio 任务是否完成 + if hasattr(self, '_current_asyncio_task'): + if self._current_asyncio_task.done(): + try: + self._current_asyncio_task.result() + except Exception as e: + print(f"异步任务出错:{e}") + # 返回 Panda3D 任务管理器的完成状态 + return panda3d_task.done # 注意是 done 而不是 DONE + # 返回 Panda3D 任务管理器的继续状态 + return panda3d_task.cont # 注意是 cont 而不是 CONTINUE + + def _parse_tileset(self,tileset_data,tileset_info): + try: + root = tileset_data.get('root',{}) + self._parse_tile(root,tileset_info['node'],tileset_info) + print("✓ Tileset 解析完成") + except Exception as e: + print(f"✗ Tileset 解析出错: {e}") + + def _parse_tile(self, tile_data, parent_node, tileset_info): + try: + # 获取tileID + tile_id = f"tile_{len(tileset_info['tiles'])}" + print(f"创建tile节点: {tile_id}") + # 创建tile节点 + tile_node = parent_node.attachNewNode(tile_id) + + tileset_info['tiles'][tile_id] = { + 'node': tile_node, + 'data': tile_data, + 'loaded': False + } + + # 如果有内容,创建占位几何体 + if 'content' in tile_data: + print(f"为tile {tile_id} 创建几何体") + self._create_tile_geometry(tile_node) + # 递归解析子tiles + children = tile_data.get('children', []) + print(f"Tile {tile_id} 有 {len(children)} 个子节点") + for child_data in children: + self._parse_tile(child_data, tile_node, tileset_info) + except Exception as e: + print(f"✗ Tile 解析出错: {e}") + import traceback + traceback.print_exc() + + def _create_tile_geometry(self,parent_node): + """为 tile 创建占位几何体""" + try: + # 创建一个简单的立方体作为占位符 + from panda3d.core import GeomVertexFormat, GeomVertexData, GeomVertexWriter + from panda3d.core import Geom, GeomTriangles, GeomNode + + format = GeomVertexFormat.getV3n3c4() + vdata = GeomVertexData('tile_cube', format, Geom.UHStatic) + + vertex = GeomVertexWriter(vdata, 'vertex') + normal = GeomVertexWriter(vdata, 'normal') + color = GeomVertexWriter(vdata, 'color') + + # 定义立方体顶点 + vertices = [ + (-0.5, -0.5, -0.5), (0.5, -0.5, -0.5), (0.5, 0.5, -0.5), (-0.5, 0.5, -0.5), + (-0.5, -0.5, 0.5), (0.5, -0.5, 0.5), (0.5, 0.5, 0.5), (-0.5, 0.5, 0.5) + ] + + for vert in vertices: + vertex.addData3f(*vert) + normal.addData3f(0, 0, 1) + color.addData4f(0.2, 0.6, 0.8, 1.0) + + # 创建几何体 + geom = Geom(vdata) + + # 创建面 + prim = GeomTriangles(Geom.UHStatic) + # 底面 + prim.addVertices(0, 1, 2) + prim.addVertices(0, 2, 3) + # 顶面 + prim.addVertices(4, 7, 6) + prim.addVertices(4, 6, 5) + # 前面 + prim.addVertices(0, 4, 5) + prim.addVertices(0, 5, 1) + # 后面 + prim.addVertices(2, 6, 7) + prim.addVertices(2, 7, 3) + # 左面 + prim.addVertices(0, 3, 7) + prim.addVertices(0, 7, 4) + # 右面 + prim.addVertices(1, 5, 6) + prim.addVertices(1, 6, 2) + + prim.closePrimitive() + geom.addPrimitive(prim) + + # 创建几何节点 + geom_node = GeomNode('tile_geometry') + geom_node.addGeom(geom) + + # 添加到场景 + cube_node = parent_node.attachNewNode(geom_node) + cube_node.setScale(1000) # 放大以便观察 + + # 添加材质 + material = Material() + material.setBaseColor((0.2, 0.6, 0.8, 1.0)) + material.setSpecular((0.1, 0.1, 0.1, 1.0)) + material.setShininess(10.0) + cube_node.setMaterial(material) + + except Exception as e: + print(f"✗ 创建 tile 几何体出错: {e}") + + def _create_placeholder_geometry(self, parent_node): + """创建一个简单的占位符几何体,让用户能看到节点""" + try: + from panda3d.core import GeomVertexFormat, GeomVertexData, GeomVertexWriter + from panda3d.core import Geom, GeomTriangles, GeomNode + + # 创建简单的立方体作为占位符 + format = GeomVertexFormat.getV3n3c4() + vdata = GeomVertexData('placeholder_cube', format, Geom.UHStatic) + + vertex = GeomVertexWriter(vdata, 'vertex') + normal = GeomVertexWriter(vdata, 'normal') + color = GeomVertexWriter(vdata, 'color') + + # 定义立方体顶点 + size = 1.0 + vertices = [ + # 前面 (Z+) + (-size, -size, size), (size, -size, size), (size, size, size), (-size, size, size), + # 后面 (Z-) + (-size, -size, -size), (-size, size, -size), (size, size, -size), (size, -size, -size), + # 左面 (X-) + (-size, -size, -size), (-size, -size, size), (-size, size, size), (-size, size, -size), + # 右面 (X+) + (size, -size, -size), (size, size, -size), (size, size, size), (size, -size, size), + # 上面 (Y+) + (-size, size, -size), (-size, size, size), (size, size, size), (size, size, -size), + # 下面 (Y-) + (-size, -size, -size), (size, -size, -size), (size, -size, size), (-size, -size, size) + ] + + normals = [ + # 前面法线 + (0, 0, 1), (0, 0, 1), (0, 0, 1), (0, 0, 1), + # 后面法线 + (0, 0, -1), (0, 0, -1), (0, 0, -1), (0, 0, -1), + # 左面法线 + (-1, 0, 0), (-1, 0, 0), (-1, 0, 0), (-1, 0, 0), + # 右面法线 + (1, 0, 0), (1, 0, 0), (1, 0, 0), (1, 0, 0), + # 上面法线 + (0, 1, 0), (0, 1, 0), (0, 1, 0), (0, 1, 0), + # 下面法线 + (0, -1, 0), (0, -1, 0), (0, -1, 0), (0, -1, 0) + ] + + # 青色 + face_colors = [ + (0.0, 1.0, 1.0, 1.0), (0.0, 1.0, 1.0, 1.0), (0.0, 1.0, 1.0, 1.0), (0.0, 1.0, 1.0, 1.0), # 前面 - 青色 + (0.0, 0.8, 0.8, 1.0), (0.0, 0.8, 0.8, 1.0), (0.0, 0.8, 0.8, 1.0), (0.0, 0.8, 0.8, 1.0), # 后面 - 稍暗青色 + (0.0, 0.9, 0.9, 1.0), (0.0, 0.9, 0.9, 1.0), (0.0, 0.9, 0.9, 1.0), (0.0, 0.9, 0.9, 1.0), # 左面 - 中等青色 + (0.0, 0.7, 0.7, 1.0), (0.0, 0.7, 0.7, 1.0), (0.0, 0.7, 0.7, 1.0), (0.0, 0.7, 0.7, 1.0), # 右面 - 稍暗青色 + (0.0, 1.0, 1.0, 1.0), (0.0, 1.0, 1.0, 1.0), (0.0, 1.0, 1.0, 1.0), (0.0, 1.0, 1.0, 1.0), # 上面 - 青色 + (0.0, 0.6, 0.6, 1.0), (0.0, 0.6, 0.6, 1.0), (0.0, 0.6, 0.6, 1.0), (0.0, 0.6, 0.6, 1.0) # 下面 - 更暗青色 + ] + + for i, vert in enumerate(vertices): + vertex.addData3f(*vert) + normal.addData3f(*normals[i]) + color.addData4f(*face_colors[i]) + + # 创建几何体 + geom = Geom(vdata) + + # 创建面(每个面两个三角形) + prim = GeomTriangles(Geom.UHStatic) + + # 每个面4个顶点,生成2个三角形 + for face in range(6): # 6个面 + base_index = face * 4 + # 第一个三角形 + prim.addVertices(base_index, base_index + 1, base_index + 2) + # 第二个三角形 + prim.addVertices(base_index, base_index + 2, base_index + 3) + + prim.closePrimitive() + geom.addPrimitive(prim) + + # 创建几何节点 + geom_node = GeomNode('tileset_placeholder') + geom_node.addGeom(geom) + + # 添加到场景 + cube_node = parent_node.attachNewNode(geom_node) + cube_node.setScale(5) # 设置合适的大小 + + # 设置双面渲染 + cube_node.setTwoSided(True) + + # 添加材质 + material = Material() + material.setBaseColor((0.0, 1.0, 1.0, 1.0)) # 青色 + material.setSpecular((0.5, 0.5, 0.5, 1.0)) + material.setShininess(32.0) + cube_node.setMaterial(material) + + # 添加标识标签 + cube_node.setTag("element_type", "cesium_placeholder") + + print("✓ 占位符几何体创建完成") + return cube_node + except Exception as e: + print(f"✗ 创建占位符几何体出错: {e}") + import traceback + traceback.print_exc() + return None + + + def serializeNode(self, node): + """序列化节点为字典数据""" + try: + node_data = { + 'name': node.getName(), + 'type': type(node.node()).__name__, + 'pos': (node.getX(), node.getY(), node.getZ()), + 'hpr': (node.getH(), node.getP(), node.getR()), + 'scale': (node.getSx(), node.getSy(), node.getSz()), + 'tags': {}, + 'children': [] + } + + # 保存所有标签 + for tag_key in node.getTagKeys(): + node_data['tags'][tag_key] = node.getTag(tag_key) + + # 特殊处理不同类型的节点 + if hasattr(node.node(), 'getClassType'): + node_class = node.node().getClassType().getName() + node_data['node_class'] = node_class + + # 递归序列化子节点 + for child in node.getChildren(): + # 跳过辅助节点 + if not child.getName().startswith(('gizmo', 'selectionBox', 'grid')): + child_data = self.serializeNode(child) + if child_data: + node_data['children'].append(child_data) + + return node_data + + except Exception as e: + print(f"序列化节点 {node.getName()} 失败: {e}") + import traceback + traceback.print_exc() + return None + + def deserializeNode(self, node_data, parent_node): + """从字典数据反序列化节点""" + try: + # 创建新节点 + node_name = node_data.get('name', 'node') + new_node = parent_node.attachNewNode(node_name) + + # 设置变换 + pos = node_data.get('pos', (0, 0, 0)) + hpr = node_data.get('hpr', (0, 0, 0)) + scale = node_data.get('scale', (1, 1, 1)) + + new_node.setPos(*pos) + new_node.setHpr(*hpr) + new_node.setScale(*scale) + + # 恢复标签 + for tag_key, tag_value in node_data.get('tags', {}).items(): + new_node.setTag(tag_key, tag_value) + + # 根据节点类型进行特殊处理 + node_type = node_data.get('type', '') + node_class = node_data.get('node_class', '') + + # 特殊处理光源节点 + if 'light_type' in node_data.get('tags', {}): + light_type = node_data['tags']['light_type'] + if light_type == 'spot_light': + self._recreateSpotLight(new_node) + elif light_type == 'point_light': + self._recreatePointLight(new_node) + + # 递归创建子节点 + for child_data in node_data.get('children', []): + self.deserializeNode(child_data, new_node) + + return new_node + + except Exception as e: + print(f"反序列化节点 {node_data.get('name', 'unknown')} 失败: {e}") + import traceback + traceback.print_exc() + return None + + def serializeNodeForCopy(self, node): + """序列化节点用于复制操作,完整保存视觉属性""" + try: + if not node or node.isEmpty(): + return None + + node_data = { + 'name': node.getName(), + 'type': type(node.node()).__name__, + 'pos': (node.getX(), node.getY(), node.getZ()), + 'hpr': (node.getH(), node.getP(), node.getR()), + 'scale': (node.getSx(), node.getSy(), node.getSz()), + 'tags': {}, + 'children': [] + } + + # 保存所有标签 + try: + if hasattr(node, 'getTagKeys'): + for tag_key in node.getTagKeys(): + node_data['tags'][tag_key] = node.getTag(tag_key) + except Exception as e: + print(f"获取标签时出错: {e}") + + # 保存视觉属性 + try: + # 保存颜色属性 + if hasattr(node, 'getColor'): + color = node.getColor() + node_data['color'] = (color.getX(), color.getY(), color.getZ(), color.getW()) + + # 保存材质属性 + if hasattr(node, 'getMaterial'): + material = node.getMaterial() + if material: + material_data = {} + material_data['base_color'] = ( + material.getBaseColor().getX(), + material.getBaseColor().getY(), + material.getBaseColor().getZ(), + material.getBaseColor().getW() + ) + material_data['ambient'] = ( + material.getAmbient().getX(), + material.getAmbient().getY(), + material.getAmbient().getZ(), + material.getAmbient().getW() + ) + material_data['diffuse'] = ( + material.getDiffuse().getX(), + material.getDiffuse().getY(), + material.getDiffuse().getZ(), + material.getDiffuse().getW() + ) + material_data['specular'] = ( + material.getSpecular().getX(), + material.getSpecular().getY(), + material.getSpecular().getZ(), + material.getSpecular().getW() + ) + material_data['shininess'] = material.getShininess() + node_data['material'] = material_data + + except Exception as e: + print(f"保存视觉属性时出错: {e}") + + # 根据节点类型保存特定信息 + if node.hasTag("tree_item_type"): + node_type = node.getTag("tree_item_type") + node_data['node_type'] = node_type + + # 保存特定类型节点的额外信息 + if node_type in ["LIGHT_NODE", "SPOT_LIGHT_NODE", "POINT_LIGHT_NODE"]: + # 保存光源特定信息 + rp_light = node.getPythonTag("rp_light_object") + if rp_light: + node_data['light_data'] = { + 'energy': getattr(rp_light, 'energy', 5000), + 'radius': getattr(rp_light, 'radius', 1000), + 'fov': getattr(rp_light, 'fov', 70) if hasattr(rp_light, 'fov') else None, + 'inner_radius': getattr(rp_light, 'inner_radius', 0.4) if hasattr(rp_light, + 'inner_radius') else None, + 'casts_shadows': getattr(rp_light, 'casts_shadows', True) if hasattr(rp_light, + 'casts_shadows') else True, + 'shadow_map_resolution': getattr(rp_light, 'shadow_map_resolution', 256) if hasattr( + rp_light, 'shadow_map_resolution') else 256 + } + elif node_type in ["GUI_BUTTON", "GUI_LABEL", "GUI_ENTRY", "GUI_IMAGE", + "GUI_3D_TEXT", "GUI_3D_IMAGE", "GUI_VIRTUAL_SCREEN"]: + # 保存GUI元素特定信息 + node_data['gui_data'] = self._serializeGUIData(node) + elif node_type == "IMPORTED_MODEL_NODE": + # 保存模型特定信息 + node_data['model_data'] = self._serializeModelData(node) + + return node_data + + except Exception as e: + print(f"序列化节点失败: {e}") + import traceback + traceback.print_exc() + return None + + def _serializeGUIData(self, node): + """序列化GUI元素数据""" + try: + gui_data = {} + + # 保存GUI相关的通用属性 + if node.hasTag("gui_type"): + gui_data['gui_type'] = node.getTag("gui_type") + + # 保存文本内容(如果有的话) + if node.hasTag("text"): + gui_data['text'] = node.getTag("text") + + # 保存其他GUI相关标签 + gui_tags = ['font', 'font_size', 'text_color', 'bg_color', 'size'] + for tag in gui_tags: + if node.hasTag(tag): + gui_data[tag] = node.getTag(tag) + + return gui_data + except Exception as e: + print(f"序列化GUI数据失败: {e}") + return {} + + def _serializeModelTextures(self, node): + """序列化模型纹理信息""" + try: + texture_data = {} + + # 获取节点的所有纹理阶段 + from panda3d.core import TextureStage + texture_stages = node.findAllTextureStages() + + if texture_stages.getNumTextureStages() > 0: + texture_data['textures'] = {} + + # 遍历所有纹理阶段 + for i in range(texture_stages.getNumTextureStages()): + stage = texture_stages.getTextureStage(i) + stage_name = stage.getName() + + # 获取该阶段的纹理 + texture = node.getTexture(stage) + if texture: + # 保存纹理信息 + texture_info = { + 'stage_name': stage_name, + 'stage_mode': stage.getMode(), + 'stage_sort': stage.getSort(), # 保存纹理阶段排序 + 'texture_path': texture.getFullpath().toOsSpecific() if texture.hasFullpath() else '', + 'texture_name': texture.getName(), + 'wrap_u': texture.getWrapU(), + 'wrap_v': texture.getWrapV(), + 'minfilter': texture.getMinfilter(), + 'magfilter': texture.getMagfilter(), + 'anisotropic_degree': texture.getAnisotropicDegree() + } + + # 保存颜色比例和偏移(使用安全的方法) + try: + texture_info['color_scale'] = tuple(node.getTextureScale(stage)) + except: + texture_info['color_scale'] = (1.0, 1.0, 1.0, 1.0) + + try: + texture_info['color_offset'] = tuple(node.getTextureOffset(stage)) + except: + texture_info['color_offset'] = (0.0, 0.0, 0.0, 0.0) + + texture_data['textures'][stage_name] = texture_info + + return texture_data + except Exception as e: + print(f"序列化模型纹理时出错: {e}") + return {} + + def _serializeModelData(self, node): + """序列化模型数据,包括材质和纹理信息""" + try: + model_data = {} + + # 保存模型相关的标签 + model_tags = ['model_path', 'file', 'element_type'] + for tag in model_tags: + if node.hasTag(tag): + model_data[tag] = node.getTag(tag) + + # 保存材质信息 + try: + # 获取模型的材质信息 + if hasattr(node, 'getState'): + state = node.getState() + if state: + # 保存基础颜色信息(使用正确的方法) + from panda3d.core import ColorAttrib + color_attrib = state.getAttrib(ColorAttrib) + if color_attrib and not color_attrib.isOff(): + color = color_attrib.getColor() + model_data['base_color'] = ( + color.getX(), + color.getY(), + color.getZ(), + color.getW() + ) + + # 保存其他材质属性 + from panda3d.core import MaterialAttrib + material_attrib = state.getAttrib(MaterialAttrib.getClassType()) + if material_attrib: + material = material_attrib.getMaterial() + if material: + # 保存基础颜色 + base_color = material.getBaseColor() + model_data['material_base_color'] = ( + base_color.getX(), base_color.getY(), base_color.getZ(), base_color.getW() + ) + + # 保存环境光颜色 + ambient_color = material.getAmbient() + model_data['material_ambient_color'] = ( + ambient_color.getX(), ambient_color.getY(), ambient_color.getZ(), + ambient_color.getW() + ) + + # 保存漫反射颜色 + diffuse_color = material.getDiffuse() + model_data['material_diffuse_color'] = ( + diffuse_color.getX(), diffuse_color.getY(), diffuse_color.getZ(), + diffuse_color.getW() + ) + + # 保存高光颜色 + specular_color = material.getSpecular() + model_data['material_specular_color'] = ( + specular_color.getX(), specular_color.getY(), specular_color.getZ(), + specular_color.getW() + ) + + # 保存粗糙度和金属度等参数 + model_data['material_roughness'] = material.getRoughness() + model_data['material_metallic'] = material.getMetallic() + + # 保存自发光颜色 + emission_color = material.getEmission() + model_data['material_emission_color'] = ( + emission_color.getX(), emission_color.getY(), emission_color.getZ(), + emission_color.getW() + ) + + # 保存光泽度 + model_data['material_shininess'] = material.getShininess() + + # 保存透明度信息 + from panda3d.core import TransparencyAttrib + transparency_attrib = state.getAttrib(TransparencyAttrib.getClassType()) + if transparency_attrib: + model_data['transparency_mode'] = transparency_attrib.get_mode() + + except Exception as e: + print(f"保存材质信息时出错: {e}") + + # 保存纹理信息 + try: + texture_data = self._serializeModelTextures(node) + if texture_data: + model_data['texture_data'] = texture_data + except Exception as e: + print(f"保存纹理信息时出错: {e}") + + return model_data + except Exception as e: + print(f"序列化模型数据失败: {e}") + return {} + + def recreateNodeFromData(self, node_data, parent_node): + """根据数据重建节点,并确保在场景树中显示""" + try: + if not node_data or not parent_node or parent_node.isEmpty(): + return None + + print(f"正在重建节点 {node_data}") + node_type = node_data.get('node_type', '') + original_name = node_data.get('name', 'node') + + # 生成唯一名称 + unique_name = self._generateUniqueName(original_name, parent_node) + + # 根据节点类型调用相应的重建方法 + new_node = None + if node_type in ["LIGHT_NODE", "SPOT_LIGHT_NODE", "POINT_LIGHT_NODE"]: + new_node = self._recreateLightFromData(node_data, parent_node, unique_name) + elif node_type == "CESIUM_TILESET_NODE": + new_node = self._recreateTilesetFromData(node_data, parent_node, unique_name) + elif node_type in ["GUI_BUTTON", "GUI_LABEL", "GUI_ENTRY", "GUI_IMAGE", + "GUI_3DTEXT", "GUI_3DIMAGE", "GUI_VIDEO_SCREEN","GUI_2D_VIDEO_SCREEN"]: + new_node = self._recreateGUIFromData(node_data, parent_node, unique_name) + elif node_type == "IMPORTED_MODEL_NODE": + new_node = self._recreateModelFromData(node_data, parent_node, unique_name) + else: + # 创建普通节点 + new_node = self._createBasicNodeFromData(node_data, parent_node, unique_name) + + # 如果成功创建节点,确保它在场景树中显示 + if new_node: + # 尝试更新场景树以显示新节点 + try: + if hasattr(self.world, 'interface_manager') and self.world.interface_manager: + # 查找父节点在场景树中的对应项 + parent_item = self._findTreeItemForNode(parent_node) + if parent_item: + # 添加新节点到场景树 + tree_widget = self.world.interface_manager.treeWidget + if tree_widget: + tree_widget.add_node_to_tree_widget(new_node, parent_item, node_type or "NODE") + except Exception as e: + print(f"添加节点到场景树时出错: {e}") + + return new_node + + except Exception as e: + print(f"重建节点失败: {e}") + import traceback + traceback.print_exc() + return None + + def _findTreeItemForNode(self, node): + """根据节点查找对应的场景树项""" + try: + if hasattr(self.world, 'interface_manager') and self.world.interface_manager: + tree_widget = self.world.interface_manager.treeWidget + if tree_widget: + # 遍历场景树查找匹配的节点项 + for i in range(tree_widget.topLevelItemCount()): + item = tree_widget.topLevelItem(i) + result = self._findTreeItemForNodeRecursive(item, node) + if result: + return result + return None + except Exception as e: + print(f"查找场景树项时出错: {e}") + return None + + def _findTreeItemForNodeRecursive(self, item, target_node): + """递归查找场景树项""" + try: + # 检查当前项是否匹配 + item_node = getattr(item, 'node_path', None) + if not item_node: + item_node = getattr(item, 'node', None) + + if item_node and item_node == target_node: + return item + + # 递归检查子项 + for i in range(item.childCount()): + child_item = item.child(i) + result = self._findTreeItemForNodeRecursive(child_item, target_node) + if result: + return result + + return None + except Exception as e: + print(f"递归查找场景树项时出错: {e}") + return None + + def _recreateLightFromData(self, node_data, parent_node, name): + """根据数据重建光源""" + try: + light_type = node_data.get('tags', {}).get('light_type', 'spot_light') + + # 创建光源 + if light_type == 'spot_light': + light_node = self.createSpotLight(pos=node_data.get('pos', (0, 0, 0))) + else: # point_light + light_node = self.createPointLight(pos=node_data.get('pos', (0, 0, 0))) + + if light_node: + # 设置名称 + light_node.setName(name) + + # 恢复其他属性 + light_data = node_data.get('light_data', {}) + rp_light = light_node.getPythonTag("rp_light_object") + if rp_light and light_data: + if 'energy' in light_data: + rp_light.energy = light_data['energy'] + if 'radius' in light_data: + rp_light.radius = light_data['radius'] + if 'fov' in light_data and hasattr(rp_light, 'fov'): + rp_light.fov = light_data['fov'] + if 'inner_radius' in light_data and hasattr(rp_light, 'inner_radius'): + rp_light.inner_radius = light_data['inner_radius'] + if 'casts_shadows' in light_data and hasattr(rp_light, 'casts_shadows'): + rp_light.casts_shadows = light_data['casts_shadows'] + if 'shadow_map_resolution' in light_data and hasattr(rp_light, 'shadow_map_resolution'): + rp_light.shadow_map_resolution = light_data['shadow_map_resolution'] + + # 恢复其他标签 + for tag_key, tag_value in node_data.get('tags', {}).items(): + if tag_key not in ['name', 'light_type']: + light_node.setTag(tag_key, str(tag_value)) + + return light_node + + except Exception as e: + print(f"重建光源失败: {e}") + return None + + def _recreateTilesetFromData(self, node_data, parent_node, name): + """根据数据重建Tileset""" + try: + tileset_url = node_data.get('tileset_url', '') + if not tileset_url: + return None + + # 使用现有方法加载tileset + position = node_data.get('pos', (0, 0, 0)) + tileset_node = self.load_cesium_tileset(tileset_url, position) + + if tileset_node: + # 设置名称 + tileset_node.setName(name) + + # 恢复其他标签 + for tag_key, tag_value in node_data.get('tags', {}).items(): + if tag_key not in ['name']: + tileset_node.setTag(tag_key, str(tag_value)) + + return tileset_node + + except Exception as e: + print(f"重建Tileset失败: {e}") + return None + + def _recreateGUIFromData(self, node_data, parent_node, name): + """根据数据重建GUI元素""" + try: + gui_data = node_data.get('gui_data', {}) + #gui_type = gui_data.get('gui_type', '') + gui_type = node_data.get("tags").get("gui_type", "") + + print(f"正在重建GUI元素: {gui_type}") + print(f"正在重建GUI元素: {node_data}") + + # 根据GUI类型调用相应的创建方法 + new_gui_element = None + + if gui_type == "button" and hasattr(self.world, 'createGUIButton'): + pos = node_data.get('pos', (0, 0, 0)) + text = node_data.get('tags').get('gui_text', '') + size = node_data.get('scale', 1) + print(pos,text,size) + new_gui_element = self.world.createGUIButton(pos,text,size) + elif gui_type == "label" and hasattr(self.world, 'createGUILabel'): + pos = node_data.get('pos', (0, 0, 0)) + text = node_data.get('tags').get('gui_text', '') + size = node_data.get('scale', 1) + new_gui_element = self.world.createGUILabel(pos,text,size) + elif gui_type == "entry" and hasattr(self.world, 'createGUIEntry'): + pos = node_data.get('pos', (0, 0, 0)) + text = node_data.get('tags').get('gui_text', '') + size = node_data.get('scale', 1) + new_gui_element = self.world.createGUIEntry(pos,text,size) + elif gui_type == "2d_image" and hasattr(self.world, 'createGUI2DImage'): + pos = node_data.get('pos', (0, 0, 0)) + image_path = node_data.get('tags').get('image_path', '') + size = node_data.get('size', 1) + new_gui_element = self.world.createGUI2DImage(pos, image_path, size) + elif gui_type == "3d_text" and hasattr(self.world, 'createGUI3DText'): + print("正在创建3D文本!!!") + pos = node_data.get('pos', (0, 0, 0)) + text = node_data.get('tags', {}).get('gui_text', '') + scale = node_data.get('scale', 1) + if isinstance(scale, (list, tuple)): + scale = scale[0] if len(scale) > 0 else 1 + print(f"正在创建3D文本: 位置={pos}, 文本={text}, 大小={scale}") + new_gui_element = self.world.createGUI3DText(pos, text, scale) + elif gui_type == "3d_image" and hasattr(self.world, 'createGUI3DImage'): + pos = node_data.get('pos', (0, 0, 0)) + image_path = node_data.get('tags').get('gui_image_path', '') + scale = node_data.get('scale', (1, 1)) + if isinstance(scale, (int, float)): + scale = (scale, scale) + elif isinstance(scale, (list, tuple)) and len(scale) >= 2: + scale = (scale[0], scale[1]) + else: + scale = (1, 1) + print(f"正在创建3D图片: 位置={pos}, 路径={image_path}, 大小={scale}") + new_gui_element = self.world.gui_manager.createGUI3DImage(pos, image_path, scale) + elif gui_type == "video_screen" and hasattr(self.world.gui_manager, 'createVideoScreen'): + pos = node_data.get('pos', (0, 0, 0)) + video_path = node_data.get('tags').get('video_path', '') + scale = node_data.get('scale', (1, 1,1)) + new_gui_element = self.world.gui_manager.createVideoScreen(pos,scale,video_path) + elif gui_type == "2d_video_screen" and hasattr(self.world.gui_manager, 'createGUI2DVideoScreen'): + pos = node_data.get('pos', (0, 0, 0)) + video_path = node_data.get('tags').get('video_path', '') + scale = node_data.get('scale', (1, 1, 1)) + new_gui_element = self.world.gui_manager.createGUI2DVideoScreen(pos,scale,video_path) + + if new_gui_element: + # 设置名称和变换 + if hasattr(new_gui_element, 'setName'): + new_gui_element.setName(name) + + # 设置位置、旋转、缩放 + pos = node_data.get('pos', (0, 0, 0)) + hpr = node_data.get('hpr', (0, 0, 0)) + scale = node_data.get('scale', (1, 1, 1)) + + if hasattr(new_gui_element, 'setPos'): + new_gui_element.setPos(*pos) + if hasattr(new_gui_element, 'setHpr'): + new_gui_element.setHpr(*hpr) + if hasattr(new_gui_element, 'setScale'): + new_gui_element.setScale(*scale) + + # 恢复文本内容 + if 'text' in gui_data and hasattr(new_gui_element, 'setText'): + new_gui_element.setText(gui_data['text']) + + # 恢复其他标签 + for tag_key, tag_value in node_data.get('tags', {}).items(): + if hasattr(new_gui_element, 'setTag') and tag_key not in ['name']: + new_gui_element.setTag(tag_key, str(tag_value)) + + print(f"GUI元素重建成功: {name}") + + return new_gui_element + + except Exception as e: + print(f"重建GUI元素失败: {e}") + import traceback + traceback.print_exc() + return None + + def _recreateModelFromData(self, node_data, parent_node, name): + """根据数据重建模型,保持材质""" + try: + model_data = node_data.get('model_data', {}) + model_path = model_data.get('model_path', model_data.get('file', '')) + + if not model_path or not os.path.exists(model_path): + # 如果原始模型文件不存在,创建一个基本节点 + return self._createBasicNodeFromData(node_data, parent_node, name) + + # 导入模型,保持原有参数 + model = self.importModel( + model_path, + apply_unit_conversion=False, # 已经处理过的模型不需要再次转换 + normalize_scales=False, # 保持原有缩放 + auto_convert_to_glb=False # 已经处理过的模型不需要再次转换 + ) + + if model: + # 设置名称 + model.setName(name) + + # 设置变换 + pos = node_data.get('pos', (0, 0, 0)) + hpr = node_data.get('hpr', (0, 0, 0)) + scale = node_data.get('scale', (1, 1, 1)) + + model.setPos(*pos) + model.setHpr(*hpr) + model.setScale(*scale) + + # 恢复材质和纹理信息 + try: + self._restoreModelMaterial(model, model_data) + except Exception as e: + print(f"恢复模型材质时出错: {e}") + + # 恢复标签 + for tag_key, tag_value in node_data.get('tags', {}).items(): + if tag_key not in ['name']: + model.setTag(tag_key, str(tag_value)) + + # 添加到模型列表 + if model not in self.models: + self.models.append(model) + + return model + + except Exception as e: + print(f"重建模型失败: {e}") + # 出错时创建基本节点 + return self._createBasicNodeFromData(node_data, parent_node, name) + + def _restoreModelTextures(self, model, texture_data): + """恢复模型纹理""" + try: + if not texture_data or 'textures' not in texture_data: + return + + from panda3d.core import TextureStage, SamplerState + + textures_info = texture_data['textures'] + + # 为每个纹理阶段恢复纹理 + for stage_name, texture_info in textures_info.items(): + # 创建纹理阶段 + stage = TextureStage(stage_name) + stage.setMode(texture_info.get('stage_mode', TextureStage.M_modulate)) + # 恢复纹理阶段排序 + stage.setSort(texture_info.get('stage_sort', 0)) # 默认为0(p3d_Texture0) + + # 加载纹理 + texture_path = texture_info['texture_path'] + if texture_path and os.path.exists(texture_path): + texture = self.world.loader.loadTexture(texture_path) + if texture: + # 设置纹理属性 + texture.setWrapU(texture_info.get('wrap_u', SamplerState.WM_repeat)) + texture.setWrapV(texture_info.get('wrap_v', SamplerState.WM_repeat)) + texture.setMinfilter(texture_info.get('minfilter', SamplerState.FT_linear)) + texture.setMagfilter(texture_info.get('magfilter', SamplerState.FT_linear)) + texture.setAnisotropicDegree(texture_info.get('anisotropic_degree', 1)) + + # 应用纹理到模型 + model.setTexture(stage, texture, 1) # 1 表示强制应用 + + # 恢复颜色比例和偏移(使用安全的方法) + if 'color_scale' in texture_info: + try: + model.setTextureScale(stage, *texture_info['color_scale']) + except Exception as e: + print(f"恢复纹理比例失败: {e}") + + if 'color_offset' in texture_info: + try: + model.setTextureOffset(stage, *texture_info['color_offset']) + except Exception as e: + print(f"恢复纹理偏移失败: {e}") + + print(f"恢复纹理: {stage_name} <- {texture_path}") + else: + print(f"纹理文件不存在或路径为空: {texture_path}") + + except Exception as e: + print(f"恢复模型纹理时出错: {e}") + + def _restoreModelMaterial(self, model, model_data): + """恢复模型材质和纹理""" + try: + # 恢复基础颜色 + if 'base_color' in model_data: + from panda3d.core import ColorAttrib + base_color = model_data['base_color'] + color = (base_color[0], base_color[1], base_color[2], base_color[3]) + model.setColor(color) + + # 恢复复杂材质属性 + if any(key.startswith('material_') for key in model_data.keys()): + from panda3d.core import Material + + # 创建新材质或获取现有材质 + material = Material() + + # 恢复基础颜色 + if 'material_base_color' in model_data: + base_color = model_data['material_base_color'] + material.setBaseColor((base_color[0], base_color[1], base_color[2], base_color[3])) + + # 恢复环境光颜色 + if 'material_ambient_color' in model_data: + ambient_color = model_data['material_ambient_color'] + material.setAmbient((ambient_color[0], ambient_color[1], ambient_color[2], ambient_color[3])) + + # 恢复漫反射颜色 + if 'material_diffuse_color' in model_data: + diffuse_color = model_data['material_diffuse_color'] + material.setDiffuse((diffuse_color[0], diffuse_color[1], diffuse_color[2], diffuse_color[3])) + + # 恢复高光颜色 + if 'material_specular_color' in model_data: + specular_color = model_data['material_specular_color'] + material.setSpecular((specular_color[0], specular_color[1], specular_color[2], specular_color[3])) + + # 恢复自发光颜色 + if 'material_emission_color' in model_data: + emission_color = model_data['material_emission_color'] + material.setEmission((emission_color[0], emission_color[1], emission_color[2], emission_color[3])) + + # 恢复粗糙度和金属度 + if 'material_roughness' in model_data: + material.setRoughness(model_data['material_roughness']) + + if 'material_metallic' in model_data: + material.setMetallic(model_data['material_metallic']) + + # 恢复光泽度 + if 'material_shininess' in model_data: + material.setShininess(model_data['material_shininess']) + + # 应用材质到模型 + model.setMaterial(material) + + # 恢复透明度设置 + if 'transparency_mode' in model_data: + from panda3d.core import TransparencyAttrib + transparency_mode = model_data['transparency_mode'] + model.setTransparency(transparency_mode) + + # 恢复纹理信息 + if 'texture_data' in model_data: + self._restoreModelTextures(model, model_data['texture_data']) + + except Exception as e: + print(f"恢复材质失败: {e}") + + def _createBasicNodeFromData(self, node_data, parent_node, name): + """创建基本节点,保持视觉属性""" + try: + new_node = parent_node.attachNewNode(name) + + # 设置变换 + pos = node_data.get('pos', (0, 0, 0)) + hpr = node_data.get('hpr', (0, 0, 0)) + scale = node_data.get('scale', (1, 1, 1)) + + new_node.setPos(*pos) + new_node.setHpr(*hpr) + new_node.setScale(*scale) + + # 恢复视觉属性 + try: + # 恢复颜色 + if 'color' in node_data: + color_data = node_data['color'] + new_node.setColor(color_data[0], color_data[1], color_data[2], color_data[3]) + + # 恢复材质 + if 'material' in node_data: + from panda3d.core import Material + material_data = node_data['material'] + material = Material() + + if 'base_color' in material_data: + bc = material_data['base_color'] + material.setBaseColor((bc[0], bc[1], bc[2], bc[3])) + + if 'ambient' in material_data: + ac = material_data['ambient'] + material.setAmbient((ac[0], ac[1], ac[2], ac[3])) + + if 'diffuse' in material_data: + dc = material_data['diffuse'] + material.setDiffuse((dc[0], dc[1], dc[2], dc[3])) + + if 'specular' in material_data: + sc = material_data['specular'] + material.setSpecular((sc[0], sc[1], sc[2], sc[3])) + + if 'shininess' in material_data: + material.setShininess(material_data['shininess']) + + new_node.setMaterial(material) + + except Exception as e: + print(f"恢复视觉属性时出错: {e}") + + # 恢复标签 + for tag_key, tag_value in node_data.get('tags', {}).items(): + if tag_key not in ['name']: + new_node.setTag(tag_key, str(tag_value)) + + return new_node + + except Exception as e: + print(f"创建基本节点失败: {e}") + return None + + def _generateUniqueName(self, base_name, parent_node): + """生成唯一节点名称""" + try: + # 移除可能的数字后缀 + import re + import time + name_base = re.sub(r'_\d+$', '', base_name) + + # 查找现有同名节点 + counter = 1 + unique_name = base_name + while True: + # 检查父节点下是否已存在同名子节点 + existing_node = parent_node.find(unique_name) + if existing_node.isEmpty(): + break + unique_name = f"{name_base}_{counter}" + counter += 1 + if counter > 1000: # 防止无限循环 + unique_name = f"{name_base}_{int(time.time())}" + break + + return unique_name + except: + return f"{base_name}_{int(time.time())}" + + diff --git a/templates/main_template.py b/templates/main_template.py index 9108750a..70b6f42d 100644 --- a/templates/main_template.py +++ b/templates/main_template.py @@ -836,10 +836,15 @@ class MainApp(ShowBase): return video_screen def _loadMovieTexture(self, video_path): - from panda3d.core import Texture, MovieTexture + from panda3d.core import Texture, MovieTexture, Filename import os - movie_texture = MovieTexture(video_path) - if movie_texture.read(video_path): + + # Convert Windows path to Panda3D compatible path format + panda_path = Filename.fromOsSpecific(video_path) + converted_path = str(panda_path) + + movie_texture = MovieTexture(converted_path) + if movie_texture.read(converted_path): self._configureVideoTexture(movie_texture) return movie_texture diff --git a/threejs_panel.html b/threejs_panel.html deleted file mode 100644 index ee6ef4c1..00000000 --- a/threejs_panel.html +++ /dev/null @@ -1,106 +0,0 @@ - - - - - Three.js Panel - - - -
-

场景信息面板

-

FPS: 0

-

对象数: 0

-
-
- - - - - diff --git a/tools/open_source_rate.py b/tools/open_source_rate.py new file mode 100644 index 00000000..cf389707 --- /dev/null +++ b/tools/open_source_rate.py @@ -0,0 +1,95 @@ +import os +from pathlib import Path + +THIRD_PARTY_DIR_NAMES = { + 'node_modules', 'vendor', 'third_party', 'third-party', 'extern', 'external', 'deps', + 'Cesium', 'Cesium-1.132', 'dist', 'build' +} + +CODE_EXTENSIONS = { + '.py', '.js', '.ts', '.tsx', '.jsx', '.css', '.scss', '.html', '.c', '.h', '.cpp', '.hpp', + '.cc', '.hh', '.m', '.mm', '.java', '.go', '.rs', '.cs', '.vue', '.svelte' +} + +SKIP_DIR_NAMES = { + '.git', '.hg', '.svn', '__pycache__', '.mypy_cache', '.pytest_cache', '.idea', '.vscode', + '.venv', 'venv', 'env', '.tox', '.cache', 'Resources', 'icons', 'tex', 'terminal' +} + +MAX_FILE_SIZE_BYTES = 2 * 1024 * 1024 # 2 MiB + + +def is_third_party(path: Path) -> bool: + for part in path.parts: + # normalize case on Windows + name = part.lower() + if name in {n.lower() for n in THIRD_PARTY_DIR_NAMES}: + return True + return False + + +def should_skip_dir(dirname: str) -> bool: + return dirname.lower() in {n.lower() for n in SKIP_DIR_NAMES} + + +def is_code_file(path: Path) -> bool: + return path.suffix.lower() in CODE_EXTENSIONS + + +def count_lines(path: Path) -> int: + try: + if path.stat().st_size > MAX_FILE_SIZE_BYTES: + return 0 + # Try text read with universal newlines, fallback to binary + try: + with path.open('r', encoding='utf-8', errors='ignore') as f: + return sum(1 for _ in f) + except Exception: + with path.open('rb') as f: + return f.read().count(b'\n') + except Exception: + return 0 + + +def main(root: Path) -> None: + third_party_loc = 0 + first_party_loc = 0 + third_party_files = 0 + first_party_files = 0 + + for dirpath, dirnames, filenames in os.walk(root): + # prune directories + dirnames[:] = [d for d in dirnames if not should_skip_dir(d)] + + current = Path(dirpath) + current_is_third = is_third_party(current) + + for fn in filenames: + p = current / fn + if not is_code_file(p): + continue + loc = count_lines(p) + if current_is_third: + third_party_loc += loc + third_party_files += 1 + else: + first_party_loc += loc + first_party_files += 1 + + total_loc = first_party_loc + third_party_loc + open_source_rate = (third_party_loc / total_loc) * 100 if total_loc else 0.0 + + print('Open-Source Rate (by LOC): {:.2f}%'.format(open_source_rate)) + print('\nBreakdown:') + print(' First-party LOC : {} ({} files)'.format(first_party_loc, first_party_files)) + print(' Third-party LOC : {} ({} files)'.format(third_party_loc, third_party_files)) + print(' Total LOC : {}'.format(total_loc)) + print('\nNotes:') + print(' - Third-party directories are heuristically detected by common names: {}'.format(', '.join(sorted(THIRD_PARTY_DIR_NAMES)))) + print(' - Static asset directories are skipped: {}'.format(', '.join(sorted(SKIP_DIR_NAMES)))) + print(' - Counted code extensions: {}'.format(', '.join(sorted(CODE_EXTENSIONS)))) + + +if __name__ == '__main__': + main(Path('.').resolve()) + diff --git a/tox.ini b/tox.ini new file mode 100644 index 00000000..110bc51b --- /dev/null +++ b/tox.ini @@ -0,0 +1,35 @@ +[tox] +envlist = py310 + +[testenv] +deps = + pytest + pytest-qt + PyQt5>=5.15.9 + PySide6>=6.8.1 + Panda3D>=1.10.15 +commands = + pytest demo/quick_script_test.py + +[testenv:lint] +deps = + flake8 +commands = + flake8 core/ demo/ scripts/ + +[testenv:coverage] +deps = + pytest + pytest-cov + PyQt5>=5.15.9 + PySide6>=6.8.1 + Panda3D>=1.10.15 +commands = + pytest --cov=core --cov=demo --cov=scripts + +[testenv:memory] +deps = + memory-profiler + Panda3D>=1.10.15 +commands = + python -m memory_profiler memory_test_example.py \ No newline at end of file diff --git a/ui/__init__.py b/ui/__init__.py index ac4ab5de..1fc1ae06 100644 --- a/ui/__init__.py +++ b/ui/__init__.py @@ -6,11 +6,11 @@ UI模块 - main_window.py: 主窗口设置 """ -from .widgets import CustomPanda3DWidget, CustomFileView, CustomTreeWidget +from .widgets import CustomMeta3DWidget, CustomFileView, CustomTreeWidget from .main_window import MainWindow, setup_main_window __all__ = [ - 'CustomPanda3DWidget', + 'CustomMeta3DWidget', 'CustomFileView', 'CustomTreeWidget', 'MainWindow', diff --git a/ui/main_window.py b/ui/main_window.py index 4511ae63..fe94df64 100644 --- a/ui/main_window.py +++ b/ui/main_window.py @@ -16,16 +16,21 @@ from PyQt5.QtWidgets import (QApplication, QMainWindow, QMenuBar, QMenu, QAction QLabel, QLineEdit, QFormLayout, QDoubleSpinBox, QScrollArea, QFileSystemModel, QButtonGroup, QToolButton, QPushButton, QHBoxLayout, QComboBox, QGroupBox, QInputDialog, QFileDialog, QMessageBox, QDesktopWidget, QDialog, - QSpinBox, QFrame, QRadioButton, QTextEdit, QCheckBox,QTabWidget, QSizePolicy) -from PyQt5.QtCore import Qt, QDir, QTimer, QSize, QPoint, QUrl, QRect + QSpinBox, QFrame, QRadioButton, QTextEdit, QCheckBox, QTabWidget, QSizePolicy, + QListWidgetItem) +from PyQt5.QtCore import Qt, QDir, QTimer, QSize, QPoint, QUrl, QRect, QCoreApplication from direct.showbase.ShowBaseGlobal import aspect2d from panda3d.core import OrthographicLens -from ui.widgets import (CustomPanda3DWidget, CustomFileView, CustomTreeWidget, +from ui.widgets import (CustomMeta3DWidget, CustomFileView, CustomTreeWidget, CustomAssetsTreeWidget, CustomConsoleDockWidget, UniversalMessageDialog) from ui.icon_manager import get_icon_manager, get_icon +import yaml +from PyQt5.QtWidgets import QDialog, QVBoxLayout, QListWidget, QPushButton, QDialogButtonBox, QCheckBox, QLabel, QHBoxLayout +from PyQt5.QtCore import Qt + class StyledTerrainDialog(QDialog): """与新建项目对话框风格一致的参数输入对话框""" @@ -739,7 +744,7 @@ class MainWindow(QMainWindow): else: print("⚠️ 应用图标设置失败,使用默认图标") # 使用自定义的 Panda3D 部件作为中央部件 - self.pandaWidget = CustomPanda3DWidget(self.world) + self.pandaWidget = CustomMeta3DWidget(self.world) self.setCentralWidget(self.pandaWidget) # 创建内嵌工具栏 @@ -1036,13 +1041,13 @@ class MainWindow(QMainWindow): self.copyAction = self.editMenu.addAction('复制') self.pasteAction = self.editMenu.addAction('粘贴') - # 视图菜单 - self.viewMenu = menubar.addMenu('视图') - self.viewPerspectiveAction = self.viewMenu.addAction('透视图') - self.viewTopAction = self.viewMenu.addAction('俯视图') - self.viewFrontAction = self.viewMenu.addAction('前视图') - self.viewMenu.addSeparator() - self.viewGridAction = self.viewMenu.addAction('显示网格') + # # 视图菜单 + # self.viewMenu = menubar.addMenu('视图') + # self.viewPerspectiveAction = self.viewMenu.addAction('透视图') + # self.viewTopAction = self.viewMenu.addAction('俯视图') + # self.viewFrontAction = self.viewMenu.addAction('前视图') + # self.viewMenu.addSeparator() + # self.viewGridAction = self.viewMenu.addAction('显示网格') # 工具菜单 self.toolsMenu = menubar.addMenu('工具') @@ -1057,105 +1062,8 @@ class MainWindow(QMainWindow): # self.iconManagerAction = self.toolsMenu.addAction('图标管理器') # self.iconManagerAction.triggered.connect(self.onOpenIconManager) - # 统一创建菜单 - 关键修改 - self.createMenu = menubar.addMenu('创建') - self.setupCreateMenuActions() # 统一创建菜单动作 - - # self.createGUIaddMenu = self.createMenu.addMenu('GUI') - # self.createButtonAction = self.createGUIaddMenu.addAction('创建按钮') - # self.createLabelAction = self.createGUIaddMenu.addAction('创建标签') - # self.createEntryAction = self.createGUIaddMenu.addAction('创建输入框') - # self.createGUIaddMenu.addSeparator() - # self.createVirtualScreenAction = self.createGUIaddMenu.addAction('创建虚拟屏幕') - # self.createCesiumViewAction = self.createGUIaddMenu.addAction('创建Cesium地图') - # self.toggleCesiumViewAction = self.createGUIaddMenu.addAction('开关地图') - # self.refreshCesiumViewAction = self.createGUIaddMenu.addAction('刷新地图') - # - # self.createLightaddMenu = self.createMenu.addMenu('光源') - # self.createSpotLightAction = self.createLightaddMenu.addAction('聚光灯') - # self.createPointLightAction = self.createLightaddMenu.addAction('点光源') - - #添加地形菜单 - self.createTerrainMenu = self.createMenu.addMenu('地形') - self.createFlatTerrainAction = self.createTerrainMenu.addAction('创建平面地形') - self.createHeightmapTerrainAction = self.createTerrainMenu.addAction('从高度图创建地形') - self.createTerrainMenu.addSeparator() - # self.terrainEditModeAction = self.createTerrainMenu.addAction('地形编辑模式') - - # GUI菜单 - # GUI菜单 - 复用创建菜单的动作 - self.guiMenu = menubar.addMenu('GUI') - self.guiEditModeAction = self.guiMenu.addAction('进入GUI编辑模式') - self.guiMenu.addSeparator() - self.guiMenu.addAction(self.createButtonAction) - self.guiMenu.addAction(self.createLabelAction) - self.guiMenu.addAction(self.createEntryAction) - self.guiMenu.addSeparator() - self.guiMenu.addAction(self.create3DTextAction) - self.guiMenu.addAction(self.createVirtualScreenAction) - - # 脚本菜单 - self.scriptMenu = menubar.addMenu('脚本') - self.createScriptAction = self.scriptMenu.addAction('创建脚本...') - self.loadScriptAction = self.scriptMenu.addAction('加载脚本文件...') - self.loadAllScriptsAction = self.scriptMenu.addAction('重载所有脚本') - self.scriptMenu.addSeparator() - self.toggleHotReloadAction = self.scriptMenu.addAction('启用热重载') - self.toggleHotReloadAction.setCheckable(True) - self.toggleHotReloadAction.setChecked(True) # 默认启用 - self.scriptMenu.addSeparator() - self.openScriptsManagerAction = self.scriptMenu.addAction('脚本管理器') - - # 交互菜单 - self.interactionMenu = menubar.addMenu('交互') - self.assemblyDisassemblyConfigAction = self.interactionMenu.addAction('拆装配置') - self.assemblyDisassemblyConfigAction.triggered.connect(self.onOpenAssemblyDisassemblyConfig) - self.interactionMenu.addSeparator() - self.startAssemblyInteractionAction = self.interactionMenu.addAction('开始拆装交互') - self.startAssemblyInteractionAction.triggered.connect(self.onStartAssemblyInteraction) - self.interactionMenu.addSeparator() - self.maintenanceSystemAction = self.interactionMenu.addAction('🔧 维修系统') - self.maintenanceSystemAction.triggered.connect(self.onOpenMaintenanceSystem) - - self.cesiumMenu = menubar.addMenu('Cesium') - self.loadCesiumTilesetAction = self.cesiumMenu.addAction('加载3Dtiles') - self.loadCesiumTilesetAction.triggered.connect(self.onLoadCesiumTileset) - - # 添加地球视图相关菜单项 - self.createCesiumViewAction = self.cesiumMenu.addAction('创建地球视图') - self.createCesiumViewAction.triggered.connect(self.onCreateCesiumView) - - self.toggleCesiumViewAction = self.cesiumMenu.addAction('开关地球视图') - self.toggleCesiumViewAction.triggered.connect(self.onToggleCesiumView) - - self.refreshCesiumViewAction = self.cesiumMenu.addAction('刷新地球视图') - self.refreshCesiumViewAction.triggered.connect(self.onRefreshCesiumView) - - # 添加模型相关菜单项 - self.addModelToCesiumAction = self.cesiumMenu.addAction('添加模型到地球') - self.addModelToCesiumAction.triggered.connect(self.onAddModelClicked) - - self.infoPanelMenu = menubar.addMenu('信息面板') - # 创建示例面板动作 - self.createSamplePanelAction = self.infoPanelMenu.addAction('创建2D示例面板') - self.createSamplePanelAction.triggered.connect(self.world.info_panel_manager.onCreateSampleInfoPanel) - - self.infoPanelMenu.addSeparator() - self.create3DSamplePanelAction = self.infoPanelMenu.addAction('创建3D实例面板') - self.create3DSamplePanelAction.triggered.connect(self.onCreate3DSampleInfoPanel) - # 添加网页浏览器菜单项 - self.webBrowserAction = self.infoPanelMenu.addAction("信息面板") - self.webBrowserAction.triggered.connect(self.openWebBrowser) - - - - #资源菜单 - self.assetsMenu = menubar.addMenu('资源') - self.refreshAssetsAction = self.assetsMenu.addAction('刷新资源') - self.refreshAssetsAction.triggered.connect(self.refreshAssetsView) - - # VR菜单 - self.vrMenu = menubar.addMenu('VR') + # VR子菜单 + self.vrMenu = self.toolsMenu.addMenu('VR') self.enterVRAction = self.vrMenu.addAction('进入VR模式') self.exitVRAction = self.vrMenu.addAction('退出VR模式') self.vrMenu.addSeparator() @@ -1253,10 +1161,34 @@ class MainWindow(QMainWindow): # 初始状态下禁用退出VR选项 self.exitVRAction.setEnabled(False) + # 统一创建菜单 - 关键修改 + self.createMenu = menubar.addMenu('创建') + self.setupCreateMenuActions() # 统一创建菜单动作 + + #添加地形菜单 + self.createTerrainMenu = self.createMenu.addMenu('地形') + self.createFlatTerrainAction = self.createTerrainMenu.addAction('创建平面地形') + self.createHeightmapTerrainAction = self.createTerrainMenu.addAction('从高度图创建地形') + self.createTerrainMenu.addSeparator() + # self.terrainEditModeAction = self.createTerrainMenu.addAction('地形编辑模式') + + # 帮助菜单 self.helpMenu = menubar.addMenu('帮助') self.aboutAction = self.helpMenu.addAction('关于') + tool_menu = self.menuBar().addMenu("配置") + plugin_config_action = tool_menu.addAction("渲染管线插件配置") + plugin_config_action.triggered.connect(self.open_plugin_config_dialog) + + def open_plugin_config_dialog(self): + """打开插件配置对话框""" + try: + dialog = PluginConfigDialog(self) + dialog.exec_() + except Exception as e: + print(f"打开插件配置对话框失败: {e}") + def refreshAssetsView(self): """"刷新资源视图""" if hasattr(self,'fileView') and self.fileView: @@ -1295,6 +1227,23 @@ class MainWindow(QMainWindow): self.createSpotLightAction = self.createLightaddMenu.addAction('聚光灯') self.createPointLightAction = self.createLightaddMenu.addAction('点光源') + self.createScriptMenu = self.createMenu.addMenu('脚本') + self.createScriptAction = self.createScriptMenu.addAction('创建脚本...') + self.loadScriptAction = self.createScriptMenu.addAction('加载脚本文件...') + self.loadAllScriptsAction = self.createScriptMenu.addAction('重载所有脚本') + self.createScriptMenu.addSeparator() + self.toggleHotReloadAction = self.createScriptMenu.addAction('启用热重载') + self.toggleHotReloadAction.setCheckable(True) + self.toggleHotReloadAction.setChecked(True) # 默认启用 + self.createScriptMenu.addSeparator() + self.openScriptsManagerAction = self.createScriptMenu.addAction('脚本管理器') + + self.createInfoPanelMenu = self.createMenu.addMenu('信息面板') + self.createSamplePanelAction = self.createInfoPanelMenu.addAction('创建2D示例面板') + self.create3DSamplePanelAction = self.createInfoPanelMenu.addAction('创建3D实例面板') + self.createInfoPanelMenu.addSeparator() + self.webBrowserAction = self.createInfoPanelMenu.addAction("Web面板") + # 统一连接信号到处理方法 self.connectCreateMenuActions() @@ -1547,13 +1496,15 @@ class MainWindow(QMainWindow): self.createSpotLightAction.triggered.connect(lambda :self.world.createSpotLight()) self.createPointLightAction.triggered.connect(lambda :self.world.createPointLight()) - # # self.createVideoScreen.triggered.connect(lambda: self.world.video_manager.create_video_screen( - # # pos=(0, 0, 2), - # # size=(4, 3), - # # name=f"video_screen_{len(self.world.video_manager.video_objects) if hasattr(self.world, 'video_manager') else 0}" - # # )) - # # self.createSphericalVideo.triggered.connect(lambda: self.world.createGUISphericalVideo()) - # self.createVirtualScreenAction.triggered.connect(lambda: self.world.create_spherical_video()) + self.createScriptAction.triggered.connect(self.onCreateScript) + self.loadScriptAction.triggered.connect(self.onLoadScript) + #self.loadAllScriptsAction.triggered.connect(self.onLoadAllScripts) + self.toggleHotReloadAction.triggered.connect(self.onToggleHotReload) + self.openScriptsManagerAction.triggered.connect(self.onOpenScriptsManager) + + self.createSamplePanelAction.triggered.connect(self.world.info_panel_manager.onCreateSampleInfoPanel) + self.create3DSamplePanelAction.triggered.connect(self.onCreate3DSampleInfoPanel) + self.webBrowserAction.triggered.connect(self.openWebBrowser) def getCreateMenuActions(self): """获取所有创建菜单动作的字典 - 供右键菜单使用""" @@ -2199,31 +2150,6 @@ class MainWindow(QMainWindow): self.setupScriptPanel(self.scriptLayout) self.addDockWidget(Qt.RightDockWidgetArea, self.scriptDock) - # # 创建底部停靠窗口(资源窗口) - # self.bottomDock = QDockWidget("资源", self) - # self.bottomDock.setAllowedAreas(Qt.BottomDockWidgetArea) - # - # # 创建文件系统模型 - # self.fileModel = QFileSystemModel() - # self.fileModel.setRootPath(QDir.homePath()) # 设置为用户主目录 - # - # # 创建树形视图显示文件系统 - # self.fileView = CustomFileView(self.world) - # self.fileView.setModel(self.fileModel) - # self.fileView.setRootIndex(self.fileModel.index(QDir.homePath())) # 设置为用户主目录索引 - # - # # 设置列宽 - # self.fileView.setColumnWidth(0, 250) # 名称列 - # self.fileView.setColumnWidth(1, 100) # 大小列 - # self.fileView.setColumnWidth(2, 100) # 类型列 - # self.fileView.setColumnWidth(3, 150) # 修改日期列 - # - # # 设置视图属性 - # self.fileView.setMinimumHeight(200) # 设置最小高度 - # - # self.bottomDock.setWidget(self.fileView) - # self.addDockWidget(Qt.BottomDockWidgetArea, self.bottomDock) - # 创建底部停靠窗口(资源窗口) self.bottomDock = QDockWidget("资源", self) @@ -2601,21 +2527,21 @@ class MainWindow(QMainWindow): self.createPointLight.setText("点光灯") self.toolbar.addWidget(self.createPointLight) - # Cesium 工具按钮 - self.cesiumViewTool = QToolButton() - self.cesiumViewTool.setText("地图视图") - self.cesiumViewTool.clicked.connect(self.onCreateCesiumView) - self.toolbar.addWidget(self.cesiumViewTool) - - self.refreshCesiumTool = QToolButton() - self.refreshCesiumTool.setText("刷新地图") - self.refreshCesiumTool.clicked.connect(self.onRefreshCesiumView) - self.toolbar.addWidget(self.refreshCesiumTool) - - self.addModelTool = QToolButton() - self.addModelTool.setText("添加模型") - self.addModelTool.clicked.connect(self.onAddModelClicked) - self.toolbar.addWidget(self.addModelTool) + # # Cesium 工具按钮 + # self.cesiumViewTool = QToolButton() + # self.cesiumViewTool.setText("地图视图") + # self.cesiumViewTool.clicked.connect(self.onCreateCesiumView) + # self.toolbar.addWidget(self.cesiumViewTool) + # + # self.refreshCesiumTool = QToolButton() + # self.refreshCesiumTool.setText("刷新地图") + # self.refreshCesiumTool.clicked.connect(self.onRefreshCesiumView) + # self.toolbar.addWidget(self.refreshCesiumTool) + # + # self.addModelTool = QToolButton() + # self.addModelTool.setText("添加模型") + # self.addModelTool.clicked.connect(self.onAddModelClicked) + # self.toolbar.addWidget(self.addModelTool) #地形 # 地形工具 @@ -3131,7 +3057,7 @@ class MainWindow(QMainWindow): self.pluginAction.triggered.connect(lambda: self.world.setCurrentTool("图形编辑")) # 连接GUI编辑模式事件 - self.guiEditModeAction.triggered.connect(lambda: self.world.toggleGUIEditMode()) + #self.guiEditModeAction.triggered.connect(lambda: self.world.toggleGUIEditMode()) # 连接创建事件 - 使用菜单动作而不是不存在的工具栏按钮 # self.createSpotLightAction.triggered.connect(lambda: self.world.createSpotLight()) @@ -3141,9 +3067,9 @@ class MainWindow(QMainWindow): # self.createEntryAction.triggered.connect(lambda: self.world.createGUIEntry()) # self.create3DTextAction.triggered.connect(lambda: self.world.createGUI3DText()) # self.createVirtualScreenAction.triggered.connect(lambda: self.world.createGUIVirtualScreen()) - self.createCesiumViewAction.triggered.connect(self.onCreateCesiumView) - self.toggleCesiumViewAction.triggered.connect(self.onToggleCesiumView) - self.refreshCesiumViewAction.triggered.connect(self.onRefreshCesiumView) + # self.createCesiumViewAction.triggered.connect(self.onCreateCesiumView) + # self.toggleCesiumViewAction.triggered.connect(self.onToggleCesiumView) + # self.refreshCesiumViewAction.triggered.connect(self.onRefreshCesiumView) # 连接地形创建事件 self.createFlatTerrainAction.triggered.connect(self.onCreateFlatTerrain) @@ -3168,7 +3094,7 @@ class MainWindow(QMainWindow): self.pasteAction.setShortcut(QKeySequence.Paste) #连接视图菜单事件 - self.setupViewMenuActions() + #self.setupViewMenuActions() # 连接工具切换信号 #self.toolGroup.buttonClicked.connect(self.onToolChanged) @@ -3659,22 +3585,37 @@ class MainWindow(QMainWindow): if hasattr(self.world,'gui_manager') and self.world.gui_manager: self.world.gui_manager.createCesiumView() else: - QMessageBox.warning(self,"错误","GUI管理其不可用") - + UniversalMessageDialog.show_warning( + self, + "错误", + "GUI管理器不可用", + show_cancel=False, + confirm_text="确认" + ) def onToggleCesiumView(self): """切换 Cesium 视图显示状态""" if hasattr(self.world, 'gui_manager') and self.world.gui_manager: self.world.gui_manager.toggleCesiumView() else: - QMessageBox.warning(self, "错误", "GUI 管理器不可用") - + UniversalMessageDialog.show_warning( + self, + "错误", + "GUI 管理器不可用", + show_cancel=False, + confirm_text="确认" + ) def onRefreshCesiumView(self): """刷新 Cesium 视图""" if hasattr(self.world, 'gui_manager') and self.world.gui_manager: self.world.gui_manager.refreshCesiumView() else: - QMessageBox.warning(self, "错误", "GUI 管理器不可用") - + UniversalMessageDialog.show_warning( + self, + "错误", + "GUI 管理器不可用", + show_cancel=False, + confirm_text="确认" + ) def onUpdateCesiumURL(self): """更新 Cesium URL""" dialog = self.createStyledInputDialog( @@ -3690,7 +3631,13 @@ class MainWindow(QMainWindow): if hasattr(self.world, 'gui_manager') and self.world.gui_manager: self.world.gui_manager.updateCesiumURL(url) else: - QMessageBox.warning(self, "错误", "GUI 管理器不可用") + UniversalMessageDialog.show_warning( + self, + "错误", + "GUI 管理器不可用", + show_cancel=False, + confirm_text="确认" + ) def onAddModelClicked(self): @@ -4121,7 +4068,7 @@ class MainWindow(QMainWindow): print(f"✗ 创建3D示例天气信息面板失败: {e}") import traceback traceback.print_exc() - QMessageBox.critical(self, "错误", f"创建3D示例天气信息面板时出错: {str(e)}") + UniversalMessageDialog.show_error(self, "错误", f"创建3D示例天气信息面板时出错: {str(e)}") # 更新 addInfoPanelToTree 方法以支持3D面板 def addInfoPanelToTree(self, panel, panel_name): @@ -4198,7 +4145,7 @@ class MainWindow(QMainWindow): print(f"✗ 创建系统状态信息面板失败: {e}") import traceback traceback.print_exc() - QMessageBox.critical(self, "错误", f"创建系统状态信息面板时出错: {str(e)}") + UniversalMessageDialog.show_error(self, "错误", f"创建系统状态信息面板时出错: {str(e)}") def getSystemStatusData(self): """ @@ -4275,7 +4222,7 @@ class MainWindow(QMainWindow): print(f"✗ 创建传感器数据信息面板失败: {e}") import traceback traceback.print_exc() - QMessageBox.critical(self, "错误", f"创建传感器数据信息面板时出错: {str(e)}") + UniversalMessageDialog.show_error(self, "错误", f"创建传感器数据信息面板时出错: {str(e)}") def getSensorData(self): """ @@ -4339,7 +4286,7 @@ class MainWindow(QMainWindow): print(f"✗ 创建场景信息面板失败: {e}") import traceback traceback.print_exc() - QMessageBox.critical(self, "错误", f"创建场景信息面板时出错: {str(e)}") + UniversalMessageDialog.show_error(self, "错误", f"创建场景信息面板时出错: {str(e)}") def getSceneInfoData(self): """ @@ -4786,7 +4733,7 @@ class MainWindow(QMainWindow): print("🎨 图标管理器已打开") except Exception as e: print(f"❌ 打开图标管理器失败: {e}") - QMessageBox.warning(self, "错误", f"打开图标管理器失败:\n{str(e)}") + UniversalMessageDialog.show_warning(self, "警告", f"打开图标管理器失败: {str(e)}", show_cancel=False, confirm_text="确认") def closeEvent(self, event): """处理窗口关闭事件""" @@ -4977,7 +4924,7 @@ class MainWindow(QMainWindow): config_dialog = AssemblyDisassemblyConfigDialog(self, self.world) config_dialog.show() except Exception as e: - QMessageBox.critical(self, "错误", f"打开拆装配置界面失败: {str(e)}") + UniversalMessageDialog.show_error(self, "错误", f"打开拆装配置界面失败: {str(e)}") import traceback traceback.print_exc() @@ -5002,7 +4949,7 @@ class MainWindow(QMainWindow): self.world.assembly_interaction.start_interaction_mode(mode=selected_mode) except Exception as e: - QMessageBox.critical(self, "错误", f"启动拆装交互失败: {str(e)}") + UniversalMessageDialog.show_error(self, "错误", f"启动拆装交互失败: {str(e)}") import traceback traceback.print_exc() @@ -5057,11 +5004,11 @@ class MainWindow(QMainWindow): print(f"✅ 维修科目启动成功") else: print("❌ 维修科目启动失败") - QMessageBox.warning(self, "错误", "维修科目启动失败") + UniversalMessageDialog.show_warning(self, "错误", "维修科目启动失败",False,"确认") except Exception as e: print(f"❌ 启动维修科目失败: {e}") - QMessageBox.critical(self, "错误", f"启动维修科目失败:\n{str(e)}") + UniversalMessageDialog.show_error(self, "错误", f"启动维修科目失败: {str(e)}") import traceback traceback.print_exc() @@ -5073,7 +5020,7 @@ class MainWindow(QMainWindow): except Exception as e: print(f"❌ 打开维修系统失败: {e}") - QMessageBox.critical(self, "错误", f"打开维修系统失败:\n{str(e)}") + UniversalMessageDialog.show_error(self, "错误", f"打开维修系统失败: {str(e)}") import traceback traceback.print_exc() @@ -5090,14 +5037,21 @@ class MainWindow(QMainWindow): # 更新菜单状态 self.enterVRAction.setEnabled(False) self.exitVRAction.setEnabled(True) - QMessageBox.information(self, "成功", "VR模式已启用!\n请确保您的VR头显已正确连接。") + UniversalMessageDialog.show_info( + self, "成功", "VR模式已启用!\n请确保您的VR头显已正确连接。" + , False, "确认") else: - QMessageBox.warning(self, "错误", "无法启用VR模式!\n请检查:\n1. SteamVR是否正在运行\n2. VR头显是否已连接\n3. OpenVR库是否已正确安装") + UniversalMessageDialog.show_warning( + self, "错误", "无法启用VR模式!\n请检查:\n1. SteamVR是否正在运行\n2. VR头显是否已连接\n3. OpenVR库是否已正确安装" + , False, "确认") else: - QMessageBox.warning(self, "错误", "VR管理器不可用!") + UniversalMessageDialog.show_warning( + self, "错误", "VR管理器不可用!" + , False, "确认") except Exception as e: - QMessageBox.critical(self, "错误", f"启用VR模式时发生错误:\n{str(e)}") - + UniversalMessageDialog.show_error( + self, "错误", f"启用VR模式时发生错误:\n{str(e)}" + , False, "确认") def onExitVR(self): """退出VR模式""" try: @@ -5106,11 +5060,14 @@ class MainWindow(QMainWindow): # 更新菜单状态 self.enterVRAction.setEnabled(True) self.exitVRAction.setEnabled(False) - QMessageBox.information(self, "成功", "已退出VR模式") + UniversalMessageDialog.show_info(self, "成功", "已退出VR模式" + , False, "确认") else: - QMessageBox.warning(self, "错误", "VR管理器不可用!") + UniversalMessageDialog.show_warning(self, "错误", "VR管理器不可用!" + , False, "确认") except Exception as e: - QMessageBox.critical(self, "错误", f"退出VR模式时发生错误:\n{str(e)}") + UniversalMessageDialog.show_error(self, "错误", f"退出VR模式时发生错误:\n{str(e)}" + , False, "确认") def onShowVRStatus(self): """显示VR状态""" @@ -5130,12 +5087,14 @@ class MainWindow(QMainWindow): - 如果VR不可用,请确保已安装SteamVR并连接VR头显 - 如果OpenVR库未安装,请运行:pip install openvr """ - - QMessageBox.information(self, "VR状态", status_text) + UniversalMessageDialog.show_info(self, "VR状态", status_text + , False, "确认") else: - QMessageBox.warning(self, "错误", "VR管理器不可用!") + UniversalMessageDialog.show_warning(self, "错误", "VR管理器不可用!" + , False, "确认") except Exception as e: - QMessageBox.critical(self, "错误", f"获取VR状态时发生错误:\n{str(e)}") + UniversalMessageDialog.show_error(self, "错误", f"获取VR状态时发生错误:\n{str(e)}" + , False, "确认") def onShowVRSettings(self): """显示VR设置对话框""" @@ -5144,149 +5103,301 @@ class MainWindow(QMainWindow): dialog = self.createVRSettingsDialog() dialog.exec_() else: - QMessageBox.warning(self, "错误", "VR管理器不可用!") + UniversalMessageDialog.show_warning(self, "错误", "VR管理器不可用!" + , False, "确认") except Exception as e: - QMessageBox.critical(self, "错误", f"打开VR设置时发生错误:\n{str(e)}") + UniversalMessageDialog.show_error(self, "错误", f"打开VR设置时发生错误:\n{str(e)}" + , False, "确认") def createVRSettingsDialog(self): - """创建VR设置对话框""" + """创建VR设置对话框(统一为 NewProjectDialog 风格)""" dialog = QDialog(self) dialog.setWindowTitle("VR设置") dialog.setModal(True) - dialog.resize(400, 300) + dialog.resize(508, 420) + dialog.setObjectName("newProjectDialog") + dialog.setWindowFlags(Qt.Dialog | Qt.FramelessWindowHint) + dialog.setAttribute(Qt.WA_TranslucentBackground, True) - layout = QVBoxLayout(dialog) + dialog.setStyleSheet(""" + QDialog#newProjectDialog { background-color: transparent; color: #EBEBEB; border: none; } + QFrame#baseFrame { background-color: #000000; border: 1px solid #3E3E42; border-radius: 5px; } + QWidget#titleBar { background-color: transparent; border: none; border-radius: 5px 5px 0 0; min-height: 32px; max-height: 32px; } + QWidget#titleBar QWidget { background-color: transparent; border: none; } + QLabel#titleLabel { color: #FFFFFF; font-family: 'Inter', 'Microsoft YaHei', sans-serif; font-size: 14px; font-weight: 500; letter-spacing: 0.7px; } + QWidget#controlButtons QPushButton { background-color: transparent; border: none; color: #EBEBEB; font-size: 14px; min-width: 18px; max-width: 18px; min-height: 18px; max-height: 18px; padding: 0px; border-radius: 3px; } + QWidget#controlButtons QPushButton:hover { background-color: #2A2D2E; color: #FFFFFF; } + QPushButton#closeButton { border-radius: 0px 5px 0px 0px; } + QPushButton#closeButton:hover { background-color: #2A2D2E; color: #FFFFFF; } + QWidget#contentWidget { background-color: transparent; border-radius: 0 0 5px 5px; } + QFrame#contentContainer { background-color: #19191B; border: 1px solid #2C2F36; border-radius: 5px; } + QLabel[role="sectionTitle"] { color: #EBEBEB; font-family: 'Inter', 'Microsoft YaHei', sans-serif; font-size: 12px; font-weight: 500; letter-spacing: 0.6px; } + QLabel[role="fieldLabel"] { color: #EBEBEB; font-family: 'Inter', 'Microsoft YaHei', sans-serif; font-size: 12px; font-weight: 400; letter-spacing: 0.6px; } + QLabel { color: #EBEBEB; font-family: 'Inter', 'Microsoft YaHei', sans-serif; font-size: 11px; font-weight: 300; letter-spacing: 0.55px; } + QComboBox, QSpinBox, QDoubleSpinBox { color: #EBEBEB; font-family: 'Inter', 'Microsoft YaHei', sans-serif; font-size: 11px; font-weight: 300; } + QCheckBox { color: #EBEBEB; font-family: 'Inter', 'Microsoft YaHei', sans-serif; font-size: 11px; font-weight: 300; } + QLineEdit { background-color: rgba(89,100,113,0.2); color: #EBEBEB; border: 1px solid rgba(76,92,110,0.6); border-radius: 2px; padding: 6px 10px; font-family: 'Inter', 'Microsoft YaHei', sans-serif; font-size: 11px; font-weight: 300; letter-spacing: 0.55px; min-height: 14px; max-height: 30px; } + QLineEdit:focus { border: 1px solid #3067C0; background-color: rgba(48,103,192,0.1); } + QLineEdit:hover { border: 1px solid #3067C0; background-color: rgba(89,100,113,0.3); } + QLineEdit:disabled { background-color: rgba(89,100,113,0.1); color: rgba(235,235,235,0.4); border: 1px solid rgba(76,92,110,0.2); } + QTextEdit { background-color: rgba(89,100,113,0.1); color: #EBEBEB; border: 1px solid rgba(76,92,110,0.4); border-radius: 4px; font-size: 11px; } + QPushButton { background-color: rgba(89,98,118,0.5); color: #EBEBEB; border: none; border-radius: 2px; padding: 0px 0px; font-family: 'Inter', 'Microsoft YaHei', sans-serif; font-weight: 300; font-size: 10px; letter-spacing: 0.5px; min-width: 90px; min-height: 30px; max-height: 30px; } + QPushButton:hover { background-color: #3067C0; color: #FFFFFF; } + QPushButton:pressed { background-color: #2556A0; color: #FFFFFF; } + QPushButton:disabled { background-color: rgba(89,98,118,0.3); color: rgba(235,235,235,0.4); } + QPushButton#primaryButton { background-color: rgba(89,98,118,0.5); border: none; color: #EBEBEB; font-weight: 300; min-width: 120px; max-width: 120px; } + QPushButton#primaryButton:hover { background-color: #2556A0; } + QPushButton#primaryButton:pressed { background-color: #1E4A8C; } + """) - # VR状态显示 - status_group = QGroupBox("VR状态") - status_layout = QVBoxLayout() + main_layout = QVBoxLayout(dialog) + main_layout.setContentsMargins(0, 0, 0, 0) + main_layout.setSpacing(0) + base_frame = QFrame() + base_frame.setObjectName('baseFrame') + base_frame.setFrameShape(QFrame.NoFrame) + base_frame.setAttribute(Qt.WA_StyledBackground, True) + base_layout = QVBoxLayout(base_frame) + base_layout.setContentsMargins(0, 0, 0, 0) + base_layout.setSpacing(0) + + # Title bar + title_bar = QWidget() + title_bar.setObjectName('titleBar') + tb_layout = QHBoxLayout(title_bar) + tb_layout.setContentsMargins(8, 0, 8, 0) + tb_layout.setSpacing(6) + control_buttons = QWidget() + control_buttons.setObjectName('controlButtons') + cb_layout = QHBoxLayout(control_buttons) + cb_layout.setContentsMargins(0, 0, 0, 0) + cb_layout.setSpacing(0) + close_btn = QPushButton() + close_btn.setObjectName('closeButton') + try: + close_btn.setIcon(get_icon('close_icon', QSize(18, 18))) + close_btn.setIconSize(QSize(18, 18)) + except Exception: + pass + close_btn.clicked.connect(dialog.reject) + cb_layout.addWidget(close_btn) + left_placeholder = QWidget() + left_placeholder.setFixedWidth(control_buttons.sizeHint().width()) + tb_layout.addWidget(left_placeholder) + title_label = QLabel("VR设置") + title_label.setObjectName('titleLabel') + title_label.setAlignment(Qt.AlignCenter) + tb_layout.addWidget(title_label, 1) + tb_layout.addWidget(control_buttons) + base_layout.addWidget(title_bar) + + # drag handlers + dragging_state = {'dragging': False, 'pos': QPoint()} + def _tb_press(event): + if event.button() == Qt.LeftButton: + dragging_state['dragging'] = True + dragging_state['pos'] = event.globalPos() - dialog.frameGeometry().topLeft() + event.accept() + else: + event.ignore() + def _tb_move(event): + if event.buttons() & Qt.LeftButton and dragging_state['dragging']: + dialog.move(event.globalPos() - dragging_state['pos']) + event.accept() + else: + event.ignore() + def _tb_release(event): + if event.button() == Qt.LeftButton: + dragging_state['dragging'] = False + event.accept() + else: + event.ignore() + title_bar.mousePressEvent = _tb_press + title_bar.mouseMoveEvent = _tb_move + title_bar.mouseReleaseEvent = _tb_release + + # Content + content_widget = QWidget() + content_widget.setObjectName('contentWidget') + content_layout = QVBoxLayout(content_widget) + content_layout.setContentsMargins(10, 10, 10, 10) + content_layout.setSpacing(0) + + content_container = QFrame() + content_container.setObjectName('contentContainer') + content_container.setFrameShape(QFrame.NoFrame) + content_container.setAttribute(Qt.WA_StyledBackground, True) + container_layout = QVBoxLayout(content_container) + container_layout.setContentsMargins(15, 10, 15, 10) + container_layout.setSpacing(10) + + # 左侧对齐辅助:统一字段列宽,并让小标题与控件左侧对齐 + label_column_width = 110 + def add_section_title(text): + title = QLabel(text) + title.setProperty('role', 'sectionTitle') + container_layout.addWidget(title) + + # VR 状态 + add_section_title("VR状态") + status_widget = QWidget() + status_vlayout = QVBoxLayout(status_widget) + status_vlayout.setContentsMargins(0, 0, 0, 0) + status_vlayout.setSpacing(6) if hasattr(self.world, 'vr_manager') and self.world.vr_manager: status = self.world.vr_manager.get_vr_status() - available_label = QLabel(f"VR可用性: {'是' if status['available'] else '否'}") - available_label.setStyleSheet(f"color: {'green' if status['available'] else 'red'};") - status_layout.addWidget(available_label) - + available_label.setStyleSheet(f"color: {'#2dffc4' if status['available'] else 'red'};") + status_vlayout.addWidget(available_label) enabled_label = QLabel(f"VR状态: {'已启用' if status['enabled'] else '未启用'}") - enabled_label.setStyleSheet(f"color: {'green' if status['enabled'] else 'gray'};") - status_layout.addWidget(enabled_label) - + enabled_label.setStyleSheet(f"color: {'#2dffc4' if status['enabled'] else 'gray'};") + status_vlayout.addWidget(enabled_label) resolution_label = QLabel(f"渲染分辨率: {status['eye_resolution'][0]}x{status['eye_resolution'][1]}") - status_layout.addWidget(resolution_label) + status_vlayout.addWidget(resolution_label) + container_layout.addWidget(status_widget) - status_group.setLayout(status_layout) - layout.addWidget(status_group) - - # 🎨 渲染模式设置 - render_mode_group = QGroupBox("渲染模式") - render_mode_layout = QVBoxLayout() - - # 创建单选按钮组 + # 渲染模式 + add_section_title("渲染模式") render_mode_button_group = QButtonGroup(dialog) - + row_mode1 = QWidget(); row_mode1_layout = QHBoxLayout(row_mode1); row_mode1_layout.setContentsMargins(0,0,0,0); row_mode1_layout.setSpacing(10) normal_render_radio = QRadioButton("普通渲染模式") + normal_render_radio.setStyleSheet(""" + QRadioButton { + color: #EBEBEB; + font-family: 'Inter', 'Microsoft YaHei', sans-serif; + font-size: 12px; + font-weight: 400; + letter-spacing: 0.6px; + } + """) + row_mode1_layout.addWidget(normal_render_radio) + row_mode1_layout.addStretch(); container_layout.addWidget(row_mode1) + row_mode2 = QWidget(); row_mode2_layout = QHBoxLayout(row_mode2); row_mode2_layout.setContentsMargins(0,0,0,0); row_mode2_layout.setSpacing(10) pipeline_render_radio = QRadioButton("RenderPipeline高级渲染(推荐)") - + pipeline_render_radio.setStyleSheet(""" + QRadioButton { + color: #EBEBEB; + font-family: 'Inter', 'Microsoft YaHei', sans-serif; + font-size: 12px; + font-weight: 400; + letter-spacing: 0.6px; + } + """) + row_mode2_layout.addWidget(pipeline_render_radio) + row_mode2_layout.addStretch(); container_layout.addWidget(row_mode2) render_mode_button_group.addButton(normal_render_radio, 0) render_mode_button_group.addButton(pipeline_render_radio, 1) - - # 根据当前模式设置选中状态 if hasattr(self.world, 'vr_manager') and self.world.vr_manager: - from core.vr import VRRenderMode - current_mode = self.world.vr_manager.get_vr_render_mode() - if current_mode == VRRenderMode.RENDER_PIPELINE: - pipeline_render_radio.setChecked(True) - else: + try: + from core.vr import VRRenderMode + current_mode = self.world.vr_manager.get_vr_render_mode() + if current_mode == VRRenderMode.RENDER_PIPELINE: + pipeline_render_radio.setChecked(True) + else: + normal_render_radio.setChecked(True) + except Exception: normal_render_radio.setChecked(True) else: normal_render_radio.setChecked(True) - - render_mode_layout.addWidget(normal_render_radio) - render_mode_layout.addWidget(pipeline_render_radio) - - # 添加说明文本 info_text = QTextEdit() info_text.setReadOnly(True) info_text.setMaximumHeight(60) - info_text.setPlainText( - "• 普通渲染:性能最优,适合低配置\n" - "• RenderPipeline:高级图形效果(阴影、AO等),需要较高性能" - ) - render_mode_layout.addWidget(info_text) - - render_mode_group.setLayout(render_mode_layout) - layout.addWidget(render_mode_group) - - # 保存按钮组引用以便后续使用 + info_text.setPlainText("• 普通渲染:性能最优,适合低配置\n• RenderPipeline:高级图形效果(阴影、AO等),需要较高性能") + container_layout.addWidget(info_text) dialog.render_mode_button_group = render_mode_button_group - # 🔧 加载配置 + # 载入配置 vr_config = {} if hasattr(self.world, 'vr_manager') and self.world.vr_manager and self.world.vr_manager.config_manager: vr_config = self.world.vr_manager.config_manager.load_config() # 渲染设置 - render_group = QGroupBox("渲染设置") - render_layout = QFormLayout() - - # 渲染质量 + add_section_title("渲染设置") + row_quality = QWidget() + row_quality_layout = QHBoxLayout(row_quality) + row_quality_layout.setContentsMargins(0,0,0,0) + row_quality_layout.setSpacing(10) + quality_label = QLabel("渲染质量:") + quality_label.setProperty('role','fieldLabel') + quality_label.setAlignment(Qt.AlignRight|Qt.AlignVCenter) + row_quality_layout.addWidget(quality_label) quality_combo = QComboBox() - quality_combo.addItems(["低", "中", "高"]) - # 从配置加载质量预设 + quality_combo.addItems(["低","中","高"]) quality_preset = vr_config.get("quality_preset", "balanced") - quality_map = {"performance": "低", "balanced": "中", "quality": "高"} - quality_combo.setCurrentText(quality_map.get(quality_preset, "中")) - render_layout.addRow("渲染质量:", quality_combo) - - # 抗锯齿 + quality_map={"performance":"低","balanced":"中","quality":"高"} + quality_combo.setCurrentText(quality_map.get(quality_preset,"中")) + row_quality_layout.addWidget(quality_combo,1) + container_layout.addWidget(row_quality) + row_aa = QWidget(); row_aa_layout = QHBoxLayout(row_aa) + row_aa_layout.setContentsMargins(0,0,0,0) + row_aa_layout.setSpacing(10) + aa_label = QLabel("抗锯齿:") + aa_label.setProperty('role','fieldLabel') + aa_label.setAlignment(Qt.AlignRight|Qt.AlignVCenter) + row_aa_layout.addWidget(aa_label) aa_combo = QComboBox() - aa_combo.addItems(["无", "2x", "4x", "8x"]) - aa_combo.setCurrentText(vr_config.get("anti_aliasing", "4x")) - render_layout.addRow("抗锯齿:", aa_combo) - - render_group.setLayout(render_layout) - layout.addWidget(render_group) + aa_combo.addItems(["无","2x","4x","8x"]) + aa_combo.setCurrentText(vr_config.get("anti_aliasing","4x")) + row_aa_layout.addWidget(aa_combo,1) + container_layout.addWidget(row_aa) # 性能设置 - perf_group = QGroupBox("性能设置") - perf_layout = QFormLayout() - - # 刷新率 + add_section_title("性能设置") + row_refresh = QWidget() + row_refresh_layout = QHBoxLayout(row_refresh) + row_refresh_layout.setContentsMargins(0,0,0,0) + row_refresh_layout.setSpacing(10) + refresh_label = QLabel("刷新率:") + refresh_label.setProperty('role','fieldLabel') + refresh_label.setAlignment(Qt.AlignRight|Qt.AlignVCenter) + row_refresh_layout.addWidget(refresh_label) refresh_combo = QComboBox() - refresh_combo.addItems(["72Hz", "90Hz", "120Hz", "144Hz"]) - refresh_combo.setCurrentText(vr_config.get("refresh_rate", "90Hz")) - perf_layout.addRow("刷新率:", refresh_combo) - - # 异步重投影 + refresh_combo.addItems(["72Hz","90Hz","120Hz","144Hz"]) + refresh_combo.setCurrentText(vr_config.get("refresh_rate","90Hz")) + row_refresh_layout.addWidget(refresh_combo,1) + container_layout.addWidget(row_refresh) + row_async = QWidget() + row_async_layout = QHBoxLayout(row_async) + row_async_layout.setContentsMargins(0,0,0,0) + row_async_layout.setSpacing(10) async_check = QCheckBox("启用异步重投影") - async_check.setChecked(vr_config.get("async_reprojection", True)) - perf_layout.addRow("", async_check) + async_check.setChecked(vr_config.get("async_reprojection",True)) + row_async_layout.addWidget(async_check) + row_async_layout.addStretch() + container_layout.addWidget(row_async) - perf_group.setLayout(perf_layout) - layout.addWidget(perf_group) - - # 保存控件引用到dialog对象 + # Save refs dialog.quality_combo = quality_combo dialog.aa_combo = aa_combo dialog.refresh_combo = refresh_combo dialog.async_check = async_check - # 按钮 - button_layout = QHBoxLayout() + # Separator before buttons + separator_buttons = QFrame() + separator_buttons.setFrameShape(QFrame.HLine) + separator_buttons.setFrameShadow(QFrame.Plain) + separator_buttons.setFixedHeight(1) + separator_buttons.setStyleSheet("background-color: #2C2F36; border: none;") + container_layout.addWidget(separator_buttons) + # Buttons + button_layout = QHBoxLayout() apply_button = QPushButton("应用") ok_button = QPushButton("确定") + # ok_button.setObjectName("primaryButton") cancel_button = QPushButton("取消") - button_layout.addWidget(apply_button) button_layout.addStretch() button_layout.addWidget(ok_button) button_layout.addWidget(cancel_button) + container_layout.addLayout(button_layout) - layout.addLayout(button_layout) + content_layout.addWidget(content_container, 0, Qt.AlignTop) + base_layout.addWidget(content_widget) + main_layout.addWidget(base_frame) - # 连接信号 + # signals apply_button.clicked.connect(lambda: self.applyVRSettings(dialog)) ok_button.clicked.connect(lambda: self.onVRSettingsOK(dialog)) cancel_button.clicked.connect(dialog.reject) @@ -5304,11 +5415,11 @@ class MainWindow(QMainWindow): """应用VR设置""" try: if not hasattr(self.world, 'vr_manager') or not self.world.vr_manager: - QMessageBox.warning(dialog, "错误", "VR管理器不可用!") + UniversalMessageDialog.show_warning(dialog, "错误", "VR管理器不可用!",False,"确定") return if not self.world.vr_manager.config_manager: - QMessageBox.warning(dialog, "错误", "VR配置管理器不可用!") + UniversalMessageDialog.show_warning(dialog, "错误", "VR配置管理器不可用!",False,"确定") return # 1️⃣ 读取所有UI控件的值 @@ -5343,25 +5454,21 @@ class MainWindow(QMainWindow): # 5️⃣ 如果渲染模式改变,询问用户确认 if mode_changed: - reply = QMessageBox.question( - dialog, - "确认切换", - f"确定要切换到{mode_name}模式吗?\n\n注意:切换渲染模式将重新创建VR缓冲区,可能需要几秒钟。", - QMessageBox.Yes | QMessageBox.No, - QMessageBox.No - ) + reply = UniversalMessageDialog.show_info(dialog, "确认切换", f"确定要切换到{mode_name}模式吗?\n\n注意:切换渲染模式将重新创建VR缓冲区,可能需要几秒钟。",True,"取消","确定") - if reply == QMessageBox.No: + if reply == QDialog.Rejected: # 用户取消渲染模式切换,但仍然保存其他设置 self.world.vr_manager.config_manager.save_config(config) - QMessageBox.information(dialog, "提示", "已保存其他设置(未切换渲染模式)") + UniversalMessageDialog.show_info(dialog, "提示", "已保存其他设置(未切换渲染模式)", + False, "确定") return # 应用渲染模式切换 success = self.world.vr_manager.set_vr_render_mode(new_mode) if not success: - QMessageBox.warning(dialog, "失败", f"切换到{mode_name}模式失败!\n请查看控制台输出了解详情。") + UniversalMessageDialog.show_warning(dialog, "失败", f"切换到{mode_name}模式失败!\n请查看控制台输出了解详情。", + False, "确定") return # 6️⃣ 保存配置(如果模式改变,set_vr_render_mode已经保存了,但我们需要确保其他设置也被保存) @@ -5373,12 +5480,15 @@ class MainWindow(QMainWindow): # 8️⃣ 显示成功消息 if mode_changed: - QMessageBox.information(dialog, "成功", f"VR设置已应用!\n• 渲染模式: {mode_name}\n• 渲染质量: {quality_text}\n配置已自动保存。") + UniversalMessageDialog.show_info(dialog, "成功", f"VR设置已应用!\n• 渲染模式: {mode_name}\n• 渲染质量: {quality_text}\n配置已自动保存。", + False, "确定") else: - QMessageBox.information(dialog, "成功", f"VR设置已保存!\n• 渲染质量: {quality_text}") + UniversalMessageDialog.show_info(dialog, "成功", f"VR设置已保存!\n• 渲染质量: {quality_text}", + False, "确定") except Exception as e: - QMessageBox.critical(dialog, "错误", f"应用VR设置时发生错误:\n{str(e)}") + UniversalMessageDialog.show_critical(dialog, "错误", f"应用VR设置时发生错误:\n{str(e)}", + False, "确定") import traceback traceback.print_exc() @@ -5392,11 +5502,14 @@ class MainWindow(QMainWindow): self.vrDebugToggleAction.setChecked(enabled) status = "启用" if enabled else "禁用" - QMessageBox.information(self, "VR调试", f"VR调试输出已{status}") + UniversalMessageDialog.show_info(self, "提示", f"已{status}VR调试输出", + False, "确定") else: - QMessageBox.warning(self, "错误", "VR管理器不可用!") + UniversalMessageDialog.show_warning(self, "错误", "VR管理器不可用!", + False, "确定") except Exception as e: - QMessageBox.critical(self, "错误", f"切换VR调试时发生错误:\n{str(e)}") + UniversalMessageDialog.show_error(self, "错误", f"切换VR调试时发生错误:\n{str(e)}", + False, "确定") def onShowVRPerformance(self): """立即显示VR性能报告""" @@ -5404,13 +5517,17 @@ class MainWindow(QMainWindow): if hasattr(self.world, 'vr_manager') and self.world.vr_manager: if self.world.vr_manager.vr_enabled: self.world.vr_manager.force_performance_report() - QMessageBox.information(self, "VR性能", "性能报告已输出到控制台") + UniversalMessageDialog.show_info(self, "提示", "已立即显示VR性能报告", + False, "确定") else: - QMessageBox.warning(self, "提示", "请先启用VR模式") + UniversalMessageDialog.show_warning(self, "提示", "请先启用VR模式", + False, "确定") else: - QMessageBox.warning(self, "错误", "VR管理器不可用!") + UniversalMessageDialog.show_warning(self, "错误", "VR管理器不可用!", + False, "确定") except Exception as e: - QMessageBox.critical(self, "错误", f"显示VR性能报告时发生错误:\n{str(e)}") + UniversalMessageDialog.show_error(self, "错误", f"显示VR性能报告时发生错误:\n{str(e)}", + False, "确定") def onSetVRDebugMode(self, mode): """设置VR调试模式""" @@ -5427,11 +5544,14 @@ class MainWindow(QMainWindow): self.vrDebugDetailedAction.setChecked(True) mode_name = "简短" if mode == 'brief' else "详细" - QMessageBox.information(self, "VR调试", f"调试模式已设置为:{mode_name}") + UniversalMessageDialog.show_info(self, "提示", f"已设置VR调试模式为:{mode_name}", + False, "确定") else: - QMessageBox.warning(self, "错误", "VR管理器不可用!") + UniversalMessageDialog.show_warning(self, "错误", "VR管理器不可用!", + False, "确定") except Exception as e: - QMessageBox.critical(self, "错误", f"设置VR调试模式时发生错误:\n{str(e)}") + UniversalMessageDialog.show_error(self, "错误", f"设置VR调试模式时发生错误:\n{str(e)}", + False, "确定") def onToggleVRPerformanceMonitor(self): """切换VR性能监控""" @@ -5444,12 +5564,15 @@ class MainWindow(QMainWindow): self.world.vr_manager.disable_performance_monitoring() status = "启用" if enabled else "禁用" - QMessageBox.information(self, "VR性能监控", f"VR性能监控已{status}") + UniversalMessageDialog.show_info(self, "提示", f"已{status}VR性能监控", + False, "确定") else: - QMessageBox.warning(self, "错误", "VR管理器不可用!") + UniversalMessageDialog.show_warning(self, "错误", "VR管理器不可用!", + False, "确定") self.vrPerformanceMonitorAction.setChecked(False) except Exception as e: - QMessageBox.critical(self, "错误", f"切换VR性能监控时发生错误:\n{str(e)}") + UniversalMessageDialog.show_error(self, "错误", f"切换VR性能监控时发生错误:\n{str(e)}", + False, "确定") def onToggleVRGpuTiming(self): """切换VR GPU时间监控""" @@ -5462,12 +5585,15 @@ class MainWindow(QMainWindow): self.world.vr_manager.disable_gpu_timing_monitoring() status = "启用" if enabled else "禁用" - QMessageBox.information(self, "VR GPU监控", f"VR GPU时间监控已{status}") + UniversalMessageDialog.show_info(self, "提示", f"已{status}VR GPU时间监控", + False, "确定") else: - QMessageBox.warning(self, "错误", "VR管理器不可用!") + UniversalMessageDialog.show_warning(self, "错误", "VR管理器不可用!", + False, "确定") self.vrGpuTimingAction.setChecked(False) except Exception as e: - QMessageBox.critical(self, "错误", f"切换VR GPU时间监控时发生错误:\n{str(e)}") + UniversalMessageDialog.show_error(self, "错误", f"切换VR GPU时间监控时发生错误:\n{str(e)}", + False, "确定") def onToggleVRPipelineMonitor(self): """切换VR管线监控""" @@ -5478,10 +5604,12 @@ class MainWindow(QMainWindow): status = "启用" if enabled else "禁用" print(f"✓ VR管线监控已{status}") else: - QMessageBox.warning(self, "错误", "VR管理器不可用!") + UniversalMessageDialog.show_warning(self, "错误", "VR管理器不可用!", + False, "确定") self.vrPipelineMonitorAction.setChecked(False) except Exception as e: - QMessageBox.critical(self, "错误", f"切换VR管线监控时发生错误:\n{str(e)}") + UniversalMessageDialog.show_error(self, "错误", f"切换VR管线监控时发生错误:\n{str(e)}", + False, "确定") def onSetVRPoseStrategy(self, strategy): """设置VR姿态策略""" @@ -5493,25 +5621,31 @@ class MainWindow(QMainWindow): 'render_callback': '渲染回调策略', 'update_task': '更新任务策略' } - QMessageBox.information(self, "VR姿态策略", - f"姿态策略已切换为:{strategy_names.get(strategy, strategy)}") + UniversalMessageDialog.show_info(self, "提示", f"已设置VR姿态策略为:{strategy_names.get(strategy, strategy)}", + False, "确定") else: - QMessageBox.warning(self, "错误", f"无效的姿态策略:{strategy}") + UniversalMessageDialog.show_warning(self, "错误", "设置VR姿态策略失败!", + False, "确定") else: - QMessageBox.warning(self, "错误", "VR管理器不可用!") + UniversalMessageDialog.show_warning(self, "错误", "VR管理器不可用!", + False, "确定") except Exception as e: - QMessageBox.critical(self, "错误", f"设置VR姿态策略时发生错误:\n{str(e)}") + UniversalMessageDialog.show_error(self, "错误", f"设置VR姿态策略时发生错误:\n{str(e)}", + False, "确定") def onTestVRPipeline(self): """测试VR管线监控功能""" try: if hasattr(self.world, 'vr_manager') and self.world.vr_manager: self.world.vr_manager.test_pipeline_monitoring() - QMessageBox.information(self, "VR管线测试", "管线监控测试已完成,请查看控制台输出。") + UniversalMessageDialog.show_info(self, "提示", "VR管线监控测试已开始,请稍等...", + False, "确定") else: - QMessageBox.warning(self, "错误", "VR管理器不可用!") + UniversalMessageDialog.show_warning(self, "错误", "VR管理器不可用!", + False, "确定") except Exception as e: - QMessageBox.critical(self, "错误", f"测试VR管线监控时发生错误:\n{str(e)}") + UniversalMessageDialog.show_error(self, "错误", f"测试VR管线监控时发生错误:\n{str(e)}", + False, "确定") def onShowVRDebugSettings(self): """显示VR调试设置对话框""" @@ -5520,68 +5654,184 @@ class MainWindow(QMainWindow): dialog = self.createVRDebugSettingsDialog() dialog.exec_() else: - QMessageBox.warning(self, "错误", "VR管理器不可用!") + UniversalMessageDialog.show_warning(self, "错误", "VR管理器不可用!", + False, "确定") except Exception as e: - QMessageBox.critical(self, "错误", f"打开VR调试设置时发生错误:\n{str(e)}") + UniversalMessageDialog.show_error(self, "错误", f"打开VR调试设置时发生错误:\n{str(e)}", + False, "确定") def createVRDebugSettingsDialog(self): - """创建VR调试设置对话框""" + """创建VR调试设置对话框(统一为 NewProjectDialog 风格)""" from PyQt5.QtWidgets import QCheckBox, QSlider from PyQt5.QtCore import Qt dialog = QDialog(self) dialog.setWindowTitle("VR调试设置") dialog.setModal(True) - dialog.resize(450, 400) + dialog.resize(508, 460) + dialog.setObjectName("newProjectDialog") + dialog.setWindowFlags(Qt.Dialog | Qt.FramelessWindowHint) + dialog.setAttribute(Qt.WA_TranslucentBackground, True) - layout = QVBoxLayout(dialog) + dialog.setStyleSheet(""" + QDialog#newProjectDialog { background-color: transparent; color: #EBEBEB; border: none; } + QFrame#baseFrame { background-color: #000000; border: 1px solid #3E3E42; border-radius: 5px; } + QWidget#titleBar { background-color: transparent; border: none; border-radius: 5px 5px 0 0; min-height: 32px; max-height: 32px; } + QWidget#titleBar QWidget { background-color: transparent; border: none; } + QLabel#titleLabel { color: #FFFFFF; font-family: 'Inter', 'Microsoft YaHei', sans-serif; font-size: 14px; font-weight: 500; letter-spacing: 0.7px; } + QWidget#controlButtons QPushButton { background-color: transparent; border: none; color: #EBEBEB; font-size: 14px; min-width: 18px; max-width: 18px; min-height: 18px; max-height: 18px; padding: 0px; border-radius: 3px; } + QWidget#controlButtons QPushButton:hover { background-color: #2A2D2E; color: #FFFFFF; } + QPushButton#closeButton { border-radius: 0px 5px 0px 0px; } + QPushButton#closeButton:hover { background-color: #2A2D2E; color: #FFFFFF; } + QWidget#contentWidget { background-color: transparent; border-radius: 0 0 5px 5px; } + QFrame#contentContainer { background-color: #19191B; border: 1px solid #2C2F36; border-radius: 5px; } + QLabel[role="sectionTitle"] { color: #EBEBEB; font-family: 'Inter', 'Microsoft YaHei', sans-serif; font-size: 12px; font-weight: 500; letter-spacing: 0.6px; } + QLabel[role="fieldLabel"] { color: #EBEBEB; font-family: 'Inter', 'Microsoft YaHei', sans-serif; font-size: 12px; font-weight: 400; letter-spacing: 0.6px; } + QLabel { color: #EBEBEB; font-family: 'Inter', 'Microsoft YaHei', sans-serif; font-size: 11px; font-weight: 300; letter-spacing: 0.55px; } + QComboBox, QSpinBox, QDoubleSpinBox { color: #EBEBEB; font-family: 'Inter', 'Microsoft YaHei', sans-serif; font-size: 11px; font-weight: 300; } + QCheckBox { color: #EBEBEB; font-family: 'Inter', 'Microsoft YaHei', sans-serif; font-size: 11px; font-weight: 300; } + QLineEdit { background-color: rgba(89,100,113,0.2); color: #EBEBEB; border: 1px solid rgba(76,92,110,0.6); border-radius: 2px; padding: 6px 10px; font-family: 'Inter', 'Microsoft YaHei', sans-serif; font-size: 11px; font-weight: 300; letter-spacing: 0.55px; min-height: 14px; max-height: 30px; } + QLineEdit:focus { border: 1px solid #3067C0; background-color: rgba(48,103,192,0.1); } + QLineEdit:hover { border: 1px solid #3067C0; background-color: rgba(89,100,113,0.3); } + QLineEdit:disabled { background-color: rgba(89,100,113,0.1); color: rgba(235,235,235,0.4); border: 1px solid rgba(76,92,110,0.2); } + QPushButton { background-color: rgba(89,98,118,0.5); color: #EBEBEB; border: none; border-radius: 2px; padding: 0px 0px; font-family: 'Inter', 'Microsoft YaHei', sans-serif; font-weight: 300; font-size: 10px; letter-spacing: 0.5px; min-width: 90px; min-height: 30px; max-height: 30px; } + QPushButton:hover { background-color: #3067C0; color: #FFFFFF; } + QPushButton:pressed { background-color: #2556A0; color: #FFFFFF; } + QPushButton:disabled { background-color: rgba(89,98,118,0.3); color: rgba(235,235,235,0.4); } + QPushButton#primaryButton { background-color: rgba(89,98,118,0.5); border: none; color: #EBEBEB; font-weight: 300; min-width: 120px; max-width: 120px; } + QPushButton#primaryButton:hover { background-color: #2556A0; } + QPushButton#primaryButton:pressed { background-color: #1E4A8C; } + """) + + main_layout = QVBoxLayout(dialog) + main_layout.setContentsMargins(0,0,0,0) + main_layout.setSpacing(0) + base_frame = QFrame() + base_frame.setObjectName('baseFrame') + base_frame.setFrameShape(QFrame.NoFrame) + base_frame.setAttribute(Qt.WA_StyledBackground, True) + base_layout = QVBoxLayout(base_frame) + base_layout.setContentsMargins(0,0,0,0) + base_layout.setSpacing(0) + + title_bar = QWidget() + title_bar.setObjectName('titleBar') + tb_layout = QHBoxLayout(title_bar) + tb_layout.setContentsMargins(8,0,8,0) + tb_layout.setSpacing(6) + control_buttons = QWidget() + control_buttons.setObjectName('controlButtons') + cb_layout = QHBoxLayout(control_buttons) + cb_layout.setContentsMargins(0,0,0,0) + cb_layout.setSpacing(0) + close_btn = QPushButton() + close_btn.setObjectName('closeButton') + try: + close_btn.setIcon(get_icon('close_icon', QSize(18, 18))) + close_btn.setIconSize(QSize(18, 18)) + except Exception: pass + close_btn.clicked.connect(dialog.reject) + cb_layout.addWidget(close_btn) + left_placeholder = QWidget() + left_placeholder.setFixedWidth(control_buttons.sizeHint().width()) + tb_layout.addWidget(left_placeholder) + title_label = QLabel("VR调试设置") + title_label.setObjectName('titleLabel') + title_label.setAlignment(Qt.AlignCenter) + tb_layout.addWidget(title_label,1) + tb_layout.addWidget(control_buttons) + base_layout.addWidget(title_bar) + + dragging_state={'dragging':False,'pos':QPoint()} + def _tb_press(e): + if e.button()==Qt.LeftButton: dragging_state['dragging']=True; dragging_state['pos']=e.globalPos()-dialog.frameGeometry().topLeft(); e.accept() + else: e.ignore() + def _tb_move(e): + if e.buttons() & Qt.LeftButton and dragging_state['dragging']: dialog.move(e.globalPos()-dragging_state['pos']); e.accept() + else: e.ignore() + def _tb_release(e): + if e.button()==Qt.LeftButton: dragging_state['dragging']=False; e.accept() + else: e.ignore() + title_bar.mousePressEvent=_tb_press; title_bar.mouseMoveEvent=_tb_move; title_bar.mouseReleaseEvent=_tb_release + + content_widget = QWidget() + content_widget.setObjectName('contentWidget') + content_layout = QVBoxLayout(content_widget) + content_layout.setContentsMargins(10,10,10,10) + content_layout.setSpacing(0) + content_container = QFrame() + content_container.setObjectName('contentContainer') + content_container.setFrameShape(QFrame.NoFrame) + content_container.setAttribute(Qt.WA_StyledBackground, True) + container_layout = QVBoxLayout(content_container) + container_layout.setContentsMargins(15,10,15,10) + container_layout.setSpacing(10) # 获取当前设置 vr_manager = self.world.vr_manager debug_status = vr_manager.get_debug_status() perf_config = vr_manager.get_performance_monitoring_config() - # 调试状态显示 - status_group = QGroupBox("调试状态") - status_layout = QVBoxLayout() + # 左侧对齐辅助:统一字段列宽,并让小标题与控件左侧对齐 + label_column_width = 110 + def add_section_title(text): + title = QLabel(text) + title.setProperty('role','sectionTitle') + container_layout.addWidget(title) + + # 调试状态 + add_section_title("调试状态") + status_widget = QWidget() + status_layout = QVBoxLayout(status_widget) + status_layout.setContentsMargins(0,0,0,0) + status_layout.setSpacing(6) debug_enabled_label = QLabel(f"调试输出: {'启用' if debug_status['debug_enabled'] else '禁用'}") - debug_enabled_label.setStyleSheet(f"color: {'green' if debug_status['debug_enabled'] else 'red'};") + debug_enabled_label.setStyleSheet(f"color: {'#2dffc4' if debug_status['debug_enabled'] else 'red'};") status_layout.addWidget(debug_enabled_label) - debug_mode_label = QLabel(f"调试模式: {debug_status['debug_mode']}") status_layout.addWidget(debug_mode_label) - performance_label = QLabel(f"性能监控: {'启用' if debug_status['performance_monitoring'] else '禁用'}") - performance_label.setStyleSheet(f"color: {'green' if debug_status['performance_monitoring'] else 'red'};") + performance_label.setStyleSheet(f"color: {'#2dffc4' if debug_status['performance_monitoring'] else 'red'};") status_layout.addWidget(performance_label) - - status_group.setLayout(status_layout) - layout.addWidget(status_group) + container_layout.addWidget(status_widget) # 报告设置 - report_group = QGroupBox("报告设置") - report_layout = QFormLayout() - - # 报告间隔滑块 (5-120秒) + add_section_title("报告设置") + row_interval = QWidget() + row_interval_layout = QHBoxLayout(row_interval) + row_interval_layout.setContentsMargins(0,0,0,0) + row_interval_layout.setSpacing(10) + interval_text_label = QLabel("报告间隔:") + interval_text_label.setProperty('role','fieldLabel') + interval_text_label.setAlignment(Qt.AlignRight|Qt.AlignVCenter) + row_interval_layout.addWidget(interval_text_label) interval_slider = QSlider(Qt.Horizontal) interval_slider.setMinimum(5) interval_slider.setMaximum(120) interval_slider.setValue(int(debug_status['report_interval_seconds'])) interval_slider.setTickPosition(QSlider.TicksBelow) interval_slider.setTickInterval(15) - interval_label = QLabel(f"{int(debug_status['report_interval_seconds'])}秒") interval_slider.valueChanged.connect(lambda v: interval_label.setText(f"{v}秒")) + inner = QHBoxLayout() + inner.setContentsMargins(0,0,0,0) + inner.setSpacing(6) + inner.addWidget(interval_slider) + inner.addWidget(interval_label) + row_interval_layout.addLayout(inner,1) + container_layout.addWidget(row_interval) - interval_layout = QHBoxLayout() - interval_layout.addWidget(interval_slider) - interval_layout.addWidget(interval_label) - report_layout.addRow("报告间隔:", interval_layout) - - # 性能检查间隔 + row_check = QWidget() + row_check_layout = QHBoxLayout(row_check) + row_check_layout.setContentsMargins(0,0,0,0) + row_check_layout.setSpacing(10) + check_label = QLabel("性能检查间隔:") + check_label.setProperty('role','fieldLabel') + check_label.setAlignment(Qt.AlignRight|Qt.AlignVCenter) + row_check_layout.addWidget(check_label) check_interval_combo = QComboBox() - check_interval_combo.addItems(["0.1秒", "0.5秒", "1.0秒", "2.0秒"]) + check_interval_combo.addItems(["0.1秒","0.5秒","1.0秒","2.0秒"]) current_check_interval = perf_config['check_interval'] if current_check_interval == 0.1: check_interval_combo.setCurrentIndex(0) @@ -5591,93 +5841,92 @@ class MainWindow(QMainWindow): check_interval_combo.setCurrentIndex(2) else: check_interval_combo.setCurrentIndex(3) - report_layout.addRow("性能检查间隔:", check_interval_combo) + row_check_layout.addWidget(check_interval_combo,1) + container_layout.addWidget(row_check) - # 帧历史大小 + row_hist = QWidget(); row_hist_layout = QHBoxLayout(row_hist) + row_hist_layout.setContentsMargins(0,0,0,0) + row_hist_layout.setSpacing(10) + hist_label = QLabel("帧时间历史:") + hist_label.setProperty('role','fieldLabel') + hist_label.setAlignment(Qt.AlignRight|Qt.AlignVCenter) + row_hist_layout.addWidget(hist_label) frame_history_spin = QSpinBox() frame_history_spin.setMinimum(10) frame_history_spin.setMaximum(1000) frame_history_spin.setValue(perf_config['frame_history_size']) frame_history_spin.setSuffix(" 帧") - report_layout.addRow("帧时间历史:", frame_history_spin) - - report_group.setLayout(report_layout) - layout.addWidget(report_group) + row_hist_layout.addWidget(frame_history_spin,1) + container_layout.addWidget(row_hist) # 监控项目 - monitor_group = QGroupBox("监控项目") - monitor_layout = QVBoxLayout() - + add_section_title("监控项目") + monitor_widget = QWidget() + monitor_layout = QVBoxLayout(monitor_widget) + monitor_layout.setContentsMargins(0,0,0,0) + monitor_layout.setSpacing(6) cpu_check = QCheckBox("CPU使用率") cpu_check.setChecked(perf_config['psutil_available']) cpu_check.setEnabled(perf_config['psutil_available']) monitor_layout.addWidget(cpu_check) - memory_check = QCheckBox("内存使用率") memory_check.setChecked(perf_config['psutil_available']) memory_check.setEnabled(perf_config['psutil_available']) monitor_layout.addWidget(memory_check) - gpu_check = QCheckBox("GPU使用率") gpu_check.setChecked(perf_config['gputil_available'] or perf_config['nvidia_ml_available']) gpu_check.setEnabled(perf_config['gputil_available'] or perf_config['nvidia_ml_available']) monitor_layout.addWidget(gpu_check) - frame_time_check = QCheckBox("帧时间统计") frame_time_check.setChecked(True) monitor_layout.addWidget(frame_time_check) + container_layout.addWidget(monitor_widget) - monitor_group.setLayout(monitor_layout) - layout.addWidget(monitor_group) - - # 按钮 + separator_buttons = QFrame() + separator_buttons.setFrameShape(QFrame.HLine) + separator_buttons.setFrameShadow(QFrame.Plain) + separator_buttons.setFixedHeight(1) + separator_buttons.setStyleSheet("background-color: #2C2F36; border: none;") + container_layout.addWidget(separator_buttons) button_layout = QHBoxLayout() - apply_button = QPushButton("应用") reset_button = QPushButton("重置计数器") ok_button = QPushButton("确定") + # ok_button.setObjectName("primaryButton") cancel_button = QPushButton("取消") - button_layout.addWidget(apply_button) button_layout.addWidget(reset_button) button_layout.addStretch() button_layout.addWidget(ok_button) button_layout.addWidget(cancel_button) + container_layout.addLayout(button_layout) - layout.addLayout(button_layout) + content_layout.addWidget(content_container, 0, Qt.AlignTop) + base_layout.addWidget(content_widget); main_layout.addWidget(base_frame) - # 连接信号 def apply_settings(): try: - # 应用报告间隔 new_interval_seconds = interval_slider.value() - new_interval_frames = int(new_interval_seconds * 60) # 假设60fps + new_interval_frames = int(new_interval_seconds * 60) vr_manager.set_performance_report_interval(new_interval_frames) - - # 应用性能检查间隔 check_intervals = [0.1, 0.5, 1.0, 2.0] new_check_interval = check_intervals[check_interval_combo.currentIndex()] vr_manager.set_performance_check_interval(new_check_interval) - - # 应用帧历史大小 vr_manager.set_frame_time_history_size(frame_history_spin.value()) - - QMessageBox.information(dialog, "成功", "VR调试设置已应用!") + UniversalMessageDialog.show_success(dialog, "成功", "VR调试设置已应用!", False, "确定") except Exception as e: - QMessageBox.critical(dialog, "错误", f"应用设置时发生错误:\n{str(e)}") - + UniversalMessageDialog.show_error(dialog, "错误", f"应用设置时发生错误:\n{str(e)}", False, "确定") def reset_counters(): try: vr_manager.reset_performance_counters() - QMessageBox.information(dialog, "成功", "性能计数器已重置!") + UniversalMessageDialog.show_success(dialog, "成功", "性能计数器已重置!", False, "确定") except Exception as e: - QMessageBox.critical(dialog, "错误", f"重置计数器时发生错误:\n{str(e)}") + UniversalMessageDialog.show_error(dialog, "错误", f"重置计数器时发生错误:\n{str(e)}", False, "确定") apply_button.clicked.connect(apply_settings) reset_button.clicked.connect(reset_counters) ok_button.clicked.connect(lambda: (apply_settings(), dialog.accept())) cancel_button.clicked.connect(dialog.reject) - return dialog # ==================== VR测试模式事件处理 ==================== @@ -5690,26 +5939,31 @@ class MainWindow(QMainWindow): # 启用VR测试模式 success = self.world.vr_manager.enable_vr_test_mode(display_mode='stereo') if success: - QMessageBox.information(self, "VR测试模式", - "VR测试模式已启用!\n\n现在VR渲染内容将直接显示在PC屏幕上,无需VR头显。\n\n特点:\n- 显示VR左右眼视图\n- 实时性能监控HUD\n- 复用完整VR渲染管线\n- 可测量纯渲染性能") + UniversalMessageDialog.show_info(self, "VR测试模式", + "VR测试模式已启用!\n\n现在VR渲染内容将直接显示在PC屏幕上,无需VR头显。\n\n特点:\n- 显示VR左右眼视图\n- 实时性能监控HUD\n- 复用完整VR渲染管线\n- 可测量纯渲染性能", + False, "确定") print("✅ VR测试模式已启用") # 可选:自动开启性能测试 self.world.vr_manager.run_vr_performance_test(duration_seconds=10) else: self.vrTestModeAction.setChecked(False) - QMessageBox.warning(self, "错误", "启用VR测试模式失败!") + UniversalMessageDialog.show_warning(self, "错误", "启用VR测试模式失败!", + False, "确定") else: # 禁用VR测试模式 self.world.vr_manager.disable_vr_test_mode() - QMessageBox.information(self, "VR测试模式", "VR测试模式已禁用") + UniversalMessageDialog.show_info(self, "VR测试模式", "VR测试模式已禁用!", + False, "确定") print("✅ VR测试模式已禁用") else: self.vrTestModeAction.setChecked(False) - QMessageBox.warning(self, "错误", "VR管理器不可用!") + UniversalMessageDialog.show_warning(self, "错误", "VR管理器不可用!", + False, "确定") except Exception as e: self.vrTestModeAction.setChecked(False) - QMessageBox.critical(self, "错误", f"切换VR测试模式时发生错误:\n{str(e)}") + UniversalMessageDialog.show_error(self, "错误", f"切换VR测试模式时发生错误:\n{str(e)}", + False, "确定") def onToggleVRTestSubmitTexture(self): """切换VR测试模式纹理提交功能""" @@ -5718,7 +5972,8 @@ class MainWindow(QMainWindow): enabled = self.vrTestSubmitTextureAction.isChecked() self.world.vr_manager.set_test_mode_features(submit_texture=enabled) except Exception as e: - QMessageBox.critical(self, "错误", f"设置纹理提交功能时发生错误:\n{str(e)}") + UniversalMessageDialog.show_error(self, "错误", f"设置纹理提交功能时发生错误:\n{str(e)}", + False, "确定") def onToggleVRTestWaitPoses(self): """切换VR测试模式姿态等待功能""" @@ -5727,7 +5982,8 @@ class MainWindow(QMainWindow): enabled = self.vrTestWaitPosesAction.isChecked() self.world.vr_manager.set_test_mode_features(wait_poses=enabled) except Exception as e: - QMessageBox.critical(self, "错误", f"设置姿态等待功能时发生错误:\n{str(e)}") + UniversalMessageDialog.show_error(self, "错误", f"设置姿态等待功能时发生错误:\n{str(e)}", + False, "确定") def onSetVRTestStep(self, step): """设置VR测试步骤""" @@ -5737,24 +5993,152 @@ class MainWindow(QMainWindow): self.world.vr_manager.set_test_mode_features(submit_texture=False, wait_poses=False) self.vrTestSubmitTextureAction.setChecked(False) self.vrTestWaitPosesAction.setChecked(False) - QMessageBox.information(self, "VR测试", "已重置为基线状态:两个功能都禁用") + UniversalMessageDialog.show_info(self, "VR测试", "已重置为基线状态:两个功能都禁用", + False, "确定") elif step == 1: # 只启用纹理提交 self.world.vr_manager.set_test_mode_features(submit_texture=True, wait_poses=False) self.vrTestSubmitTextureAction.setChecked(True) self.vrTestWaitPosesAction.setChecked(False) - QMessageBox.information(self, "VR测试", "步骤1:只启用纹理提交\n观察FPS变化来判断submit_texture是否影响性能") + UniversalMessageDialog.show_info(self, "VR测试", "步骤1:只启用纹理提交\n观察FPS变化来判断submit_texture是否影响性能", + False, "确定") elif step == 2: # 只启用姿态等待 self.world.vr_manager.set_test_mode_features(submit_texture=False, wait_poses=True) self.vrTestSubmitTextureAction.setChecked(False) self.vrTestWaitPosesAction.setChecked(True) - QMessageBox.information(self, "VR测试", "步骤2:只启用姿态等待\n观察FPS变化来判断waitGetPoses是否影响性能") + UniversalMessageDialog.show_info(self, "VR测试", "步骤2:只启用姿态等待\n观察FPS变化来判断waitGetPoses是否影响性能", + False, "确定") elif step == 3: # 同时启用两者 self.world.vr_manager.set_test_mode_features(submit_texture=True, wait_poses=True) self.vrTestSubmitTextureAction.setChecked(True) self.vrTestWaitPosesAction.setChecked(True) - QMessageBox.information(self, "VR测试", "步骤3:同时启用两者\n这应该完全复现普通VR模式的36FPS问题") + UniversalMessageDialog.show_info(self, "VR测试", "步骤3:同时启用两者\n这应该完全复现普通VR模式的36FPS问题", + False, "确定") except Exception as e: - QMessageBox.critical(self, "错误", f"设置VR测试步骤时发生错误:\n{str(e)}") + UniversalMessageDialog.show_error(self, "错误", f"设置VR测试步骤时发生错误:\n{str(e)}", + False, "确定") + + +class PluginConfigDialog(QDialog): + def __init__(self,parent=None): + super().__init__(parent) + self.setWindowTitle("渲染管线插件配置") + self.setGeometry(200,200,400,500) + self.plugins_config_path = os.path.join("RenderPipelineFile", "config", "plugins.yaml") + self.init_ui() + self.load_plugins_config() + + def init_ui(self): + layout = QVBoxLayout() + + # 标题 + title_label = QLabel("启用的插件:") + layout.addWidget(title_label) + + # 插件列表 + self.plugins_list = QListWidget() + layout.addWidget(self.plugins_list) + + # 按钮布局 + button_layout = QHBoxLayout() + + # 全选/全不选按钮 + self.select_all_btn = QPushButton("全选") + self.select_all_btn.clicked.connect(self.select_all_plugins) + button_layout.addWidget(self.select_all_btn) + + self.deselect_all_btn = QPushButton("全不选") + self.deselect_all_btn.clicked.connect(self.deselect_all_plugins) + button_layout.addWidget(self.deselect_all_btn) + + button_layout.addStretch() + + # 对话框按钮 + self.button_box = QDialogButtonBox( + QDialogButtonBox.Ok | QDialogButtonBox.Cancel | QDialogButtonBox.Apply + ) + self.button_box.accepted.connect(self.accept) + self.button_box.rejected.connect(self.reject) + self.button_box.button(QDialogButtonBox.Apply).clicked.connect(self.apply_changes) + + button_layout.addWidget(self.button_box) + layout.addLayout(button_layout) + + self.setLayout(layout) + + def load_plugins_config(self): + """加载插件配置""" + try: + # 检查文件是否存在 + if not os.path.exists(self.plugins_config_path): + print(f"插件配置文件不存在: {self.plugins_config_path}") + return + + with open(self.plugins_config_path, 'r', encoding='utf-8') as f: + config = yaml.safe_load(f) + + enabled_plugins = config.get('enabled', []) + + # 清空列表 + self.plugins_list.clear() + + # 添加启用的插件 + for plugin in enabled_plugins: + item = QListWidgetItem(plugin) + item.setFlags(item.flags() | Qt.ItemIsUserCheckable) + item.setCheckState(Qt.Checked) + self.plugins_list.addItem(item) + + except Exception as e: + print(f"加载插件配置失败: {e}") + + def select_all_plugins(self): + """全选所有插件""" + for i in range(self.plugins_list.count()): + item = self.plugins_list.item(i) + item.setCheckState(Qt.Checked) + + def deselect_all_plugins(self): + """取消选择所有插件""" + for i in range(self.plugins_list.count()): + item = self.plugins_list.item(i) + item.setCheckState(Qt.Unchecked) + + def apply_changes(self): + """应用更改到配置文件,保留原有配置结构""" + try: + # 检查文件是否存在 + if not os.path.exists(self.plugins_config_path): + print(f"插件配置文件不存在: {self.plugins_config_path}") + return + + # 读取完整配置 + with open(self.plugins_config_path, 'r', encoding='utf-8') as f: + config = yaml.safe_load(f) or {} + + # 获取选中的插件 + enabled_plugins = [] + for i in range(self.plugins_list.count()): + item = self.plugins_list.item(i) + if item.checkState() == Qt.Checked: + enabled_plugins.append(item.text()) + + # 只更新enabled部分,保留其他配置 + config['enabled'] = enabled_plugins + + # 保存配置时保留原有格式(尽可能) + with open(self.plugins_config_path, 'w', encoding='utf-8') as f: + yaml.dump(config, f, default_flow_style=False, allow_unicode=True, + sort_keys=False, indent=4) + + print("插件配置已保存") + + except Exception as e: + print(f"保存插件配置失败: {e}") + + def accept(self): + """点击确定按钮时保存并关闭""" + self.apply_changes() + super().accept() def setup_main_window(world,path = None): """设置主窗口的便利函数""" @@ -5885,6 +6269,10 @@ def setup_main_window(world,path = None): """设置主窗口的便利函数""" app = QApplication.instance() if app is None: + # 修复 Windows 下 WebEngine 与 OpenGL 上下文共享问题 + QCoreApplication.setAttribute(Qt.AA_ShareOpenGLContexts) + if sys.platform == 'win32': + QCoreApplication.setAttribute(Qt.AA_UseDesktopOpenGL) app = QApplication(sys.argv) main_window = MainWindow(world) diff --git a/ui/property_panel.py b/ui/property_panel.py index bdd2e090..e7c64529 100644 --- a/ui/property_panel.py +++ b/ui/property_panel.py @@ -8,7 +8,7 @@ from PyQt5.QtGui import QColor from PyQt5.QtWidgets import (QLabel, QLineEdit, QDoubleSpinBox, QPushButton, QTreeWidget, QTreeWidgetItem, QMenu, QCheckBox, QComboBox, QHBoxLayout, QWidget, QVBoxLayout, QGroupBox, QGridLayout, QSpinBox, QFileDialog, QMessageBox, QSizePolicy) -from PyQt5.QtCore import Qt +from PyQt5.QtCore import Qt, QTimer from deploy_libs.unicodedata import normalize from direct.actor.Actor import Actor from direct.gui import DirectGui @@ -30,6 +30,8 @@ class PropertyPanelManager: self._propertyLayout = None self._actor_cache = {} self._spherical_video_controls = {} + self._transform_monitor_timer = None + self._last_transform_values = {} self.column_minimum_width = 85 @@ -614,6 +616,9 @@ class PropertyPanelManager: def clearPropertyPanel(self): """清空属性面板""" + # 停止变换监控 + self.stopTransformMonitoring() + if self._propertyLayout: while self._propertyLayout.count(): item = self._propertyLayout.takeAt(0) @@ -673,6 +678,7 @@ class PropertyPanelManager: self.active_check = QCheckBox() # 根据模型的实际可见性状态设置复选框 self.active_check.setChecked(user_visible) + self.active_check.stateChanged.connect(lambda state: self._toggleModelVisibility(model, state)) self.name_input = QLineEdit(itemText) self.name_input.returnPressed.connect( @@ -685,14 +691,14 @@ class PropertyPanelManager: self.name_group.setLayout(name_layout) self._propertyLayout.addWidget(self.name_group) - if model: - try: - self.active_check.stateChanged.disconnect() - except TypeError: - pass - self.active_check.stateChanged.connect( - lambda state, m=model: self._setUserVisible(m, state == Qt.Checked) - ) + # if model: + # try: + # self.active_check.stateChanged.disconnect() + # except TypeError: + # pass + # self.active_check.stateChanged.connect( + # lambda state, m=model: self._setUserVisible(m, state == Qt.Checked) + # ) # nameLabel = QLabel("名称:") # nameEdit = QLineEdit(itemText) # self._propertyLayout.addRow(nameLabel, nameEdit) @@ -710,6 +716,8 @@ class PropertyPanelManager: self.updateLightPropertyPanel(model) elif model: self._updateModelPropertyPanel(model) + # 启动变换监控 + self.startTransformMonitoring(model) self._propertyLayout.addStretch() @@ -1086,6 +1094,9 @@ class PropertyPanelManager: def _cleanupAllReferences(self): """清理所有控件引用""" + # 停止变换监控 + self.stopTransformMonitoring() + # 清理变换控件引用 self._cleanupTransformControls() @@ -1126,6 +1137,15 @@ class PropertyPanelManager: node.setPythonTag("user_visible", visible) self._syncEffectiveVisibility(node) + def _setUserVisible_light(self, node, visible): + """设置用户可见性状态""" + try: + # 保存可见性状态 + node.setPythonTag("user_visible", visible) + + except Exception as e: + print(f"设置用户可见性失败: {e}") + def _syncEffectiveVisibility(self, start_node): """广度优先,确保父隐藏则子一定隐藏""" # 获取起始节点的父节点 @@ -1164,16 +1184,32 @@ class PropertyPanelManager: def _toggleModelVisibility(self, model, state): """切换模型可见性状态""" try: - # 用我们自己维护的可见性接口,而不是直接 show/hide - visible = (state == Qt.Checked) - self._setUserVisible(model, visible) + # 特殊处理灯光对象 + scene_manager = None + if hasattr(self.world, 'scene_manager'): + scene_manager = self.world.scene_manager - collision_nodes = model.findAllMatches("**/modelCollision_*") - for collision_node in collision_nodes: - collision_node.hide() + if scene_manager and hasattr(scene_manager, 'isLightObject'): + is_light = scene_manager.isLightObject(model) + + if is_light: + visible = (state == Qt.Checked) + self._setUserVisible_light(model, visible) + if hasattr(scene_manager, 'toggleLightVisibility'): + scene_manager.toggleLightVisibility(model, visible) + return # 关键:这里必须return,避免执行下面的普通模型逻辑 + else: + visible = (state == Qt.Checked) + self._setUserVisible(model, visible) + + collision_nodes = model.findAllMatches("**/modelCollision_*") + for collision_node in collision_nodes: + collision_node.hide() except Exception as e: print(f"切换模型可见性失败: {str(e)}") + import traceback + traceback.print_exc() def refreshModelValues(self, nodePath): """刷新模型值显示""" @@ -1199,6 +1235,67 @@ class PropertyPanelManager: except Exception as e: print(f"刷新模型值显示失败: {e}") + def startTransformMonitoring(self, nodePath): + """开始监控节点的变换变化""" + # 如果已有监控器在运行,先停止它 + self.stopTransformMonitoring() + + # 保存初始变换值 + self._saveCurrentTransformValues(nodePath) + + # 创建并启动定时器 + self._transform_monitor_timer = QTimer() + self._transform_monitor_timer.timeout.connect(lambda: self._checkTransformChanges(nodePath)) + self._transform_monitor_timer.start(100) # 每100毫秒检查一次 + + def stopTransformMonitoring(self): + """停止监控节点的变换变化""" + if self._transform_monitor_timer: + self._transform_monitor_timer.stop() + self._transform_monitor_timer.deleteLater() + self._transform_monitor_timer = None + + # 清除保存的变换值 + self._last_transform_values.clear() + + def _saveCurrentTransformValues(self, nodePath): + """保存当前节点的变换值""" + try: + pos = nodePath.getPos() + hpr = nodePath.getHpr() + scale = nodePath.getScale() + + self._last_transform_values = { + 'pos': (pos.getX(), pos.getY(), pos.getZ()), + 'hpr': (hpr.getX(), hpr.getY(), hpr.getZ()), + 'scale': (scale.getX(), scale.getY(), scale.getZ()) + } + except Exception as e: + print(f"保存变换值失败: {e}") + + def _checkTransformChanges(self, nodePath): + """检查节点变换是否发生变化""" + try: + # 获取当前变换值 + pos = nodePath.getPos() + hpr = nodePath.getHpr() + scale = nodePath.getScale() + + current_values = { + 'pos': (pos.getX(), pos.getY(), pos.getZ()), + 'hpr': (hpr.getX(), hpr.getY(), hpr.getZ()), + 'scale': (scale.getX(), scale.getY(), scale.getZ()) + } + + # 比较变换值是否发生变化 + if current_values != self._last_transform_values: + # 变换已更改,刷新属性面板 + self.refreshModelValues(nodePath) + # 更新保存的变换值 + self._last_transform_values = current_values + except Exception as e: + print(f"检查变换变化失败: {e}") + def _refreshGUIElementValues(self, gui_element): """刷新GUI元素值显示""" try: @@ -4670,10 +4767,8 @@ class PropertyPanelManager: is_first_load = not video_screen.hasTag("video_path") or not video_screen.getTag("video_path") - needs_second_call = False - if is_first_load and not hasattr(self, '_first_load_processed'): - needs_second_call = True - self._first_load_processed = True + # 确保之前的视频流已停止 + self._stop3DVideo(video_screen) load_url_btn = None if hasattr(self, '_current_load_url_btn_3d'): @@ -4706,9 +4801,6 @@ class PropertyPanelManager: from PyQt5.QtWidgets import QApplication QApplication.processEvents() - # 停止之前可能正在播放的视频 - self._stop3DVideo(video_screen) - # 使用 OpenCV 打开视频流 cap = cv2.VideoCapture(url) if not cap.isOpened(): @@ -4821,13 +4913,6 @@ class PropertyPanelManager: print(f"✅ 开始在3D视频屏幕上播放网络视频流: {url}") - if needs_second_call: - print("检测到首次加载,再次调用_loadVideoFromURLWithOpenCV_3D以确保正确显示") - from PyQt5.QtCore import QTimer - QTimer.singleShot(100, lambda: self._loadVideoFromURLWithOpenCV_3D(video_screen, url)) - QTimer.singleShot(200, lambda: setattr(self, '_first_load_processed', False) if hasattr(self, - '_first_load_processed') else None) - # 恢复按钮状态 update_button_text("加载URL") update_button_enabled(True) @@ -5443,11 +5528,22 @@ class PropertyPanelManager: light_group = QGroupBox("光源属性") light_layout = QGridLayout() + current_energy = light_object.energy + stored_energy = None + + if current_energy == 0.0 and model.hasTag("stored_energy"): + try: + stored_energy = float(model.getTag("stored_energy")) + except ValueError: + pass + + display_energy = stored_energy if stored_energy is not None and stored_energy > 0 else current_energy + # 能量 light_layout.addWidget(QLabel("能量:"), 0, 0) energySpinBox = QDoubleSpinBox() energySpinBox.setRange(0, 10000) - energySpinBox.setValue(light_object.energy) + energySpinBox.setValue(display_energy) energySpinBox.valueChanged.connect(lambda v: self._updateLightEnergy(light_object, v)) light_layout.addWidget(energySpinBox, 0, 1, 1, 3) @@ -6299,7 +6395,7 @@ class PropertyPanelManager: """应用漫反射贴图""" try: from RenderPipelineFile.rpcore.loader import RPLoader - from panda3d.core import TextureStage + from panda3d.core import TextureStage, TransparencyAttrib # 加载纹理 texture = RPLoader.load_texture(texture_path) @@ -6309,10 +6405,19 @@ class PropertyPanelManager: if node and material: print(f"正在为节点 {node.getName()} 应用漫反射贴图") + # 检查纹理是否包含透明度信息 + has_alpha = False + # 检查纹理格式 + format_name = str(texture.getFormat()) + if 'alpha' in format_name.lower() or 'rgba' in format_name.lower(): + has_alpha = True + + print(f"纹理格式: {texture.getFormat()}, 包含透明通道: {has_alpha}") + # 检查是否有金属性贴图,选择合适的PBR效果 print("🔧 检查金属性贴图并选择合适的PBR效果...") has_metallic = self._hasMetallicTexture(node) - needs_alpha = self._needsAlphaTesting(node) + needs_alpha = self._needsAlphaTesting(node) or has_alpha if has_metallic: print("✅ 检测到金属性贴图,使用支持金属性的PBR效果") @@ -6360,6 +6465,15 @@ class PropertyPanelManager: node.setTexture(diffuse_stage, texture) print("漫反射贴图已应用到p3d_Texture0槽") + # 如果纹理包含透明度,启用透明度渲染 + if has_alpha: + print("检测到透明纹理,启用透明度渲染") + node.setTransparency(TransparencyAttrib.MAlpha) + node.setDepthWrite(False) # 透明物体通常不写入深度缓冲区 + else: + # 确保关闭透明度(避免之前设置的影响) + node.setTransparency(TransparencyAttrib.MNone) + # 调试信息:显示当前纹理阶段 print("=== 漫反射贴图应用后的纹理阶段信息 ===") all_stages = node.findAllTextureStages() @@ -6412,6 +6526,7 @@ class PropertyPanelManager: has_diffuse_texture = True print(f" 🔍 发现现有漫反射贴图: {tex.getName()}") + # 如果没有漫反射贴图,必须创建白色纹理,否则法线映射会失效 if not has_diffuse_texture: print("⚠️ 没有漫反射贴图,创建白色纹理确保法线映射正常工作...") @@ -8746,12 +8861,12 @@ class PropertyPanelManager: if hasattr(setting_handle, 'curves') and setting_handle.curves: # 清除现有的控制点,设置单一值 setting_handle.curves[0].set_single_value(normalized_value) - print(f"✅ 更新Day Time设置: {plugin_name}.{setting_name} = {value} (归一化: {normalized_value:.3f})") + #print(f"✅ 更新Day Time设置: {plugin_name}.{setting_name} = {value} (归一化: {normalized_value:.3f})") # 保存设置到配置文件 try: plugin_mgr.save_daytime_overrides("/$$rpconfig/daytime.yaml") - print("✅ Day Time设置已保存到配置文件") + #print("✅ Day Time设置已保存到配置文件") except Exception as e: print(f"⚠️ 保存配置文件失败: {e}") @@ -8759,7 +8874,7 @@ class PropertyPanelManager: try: from RenderPipelineFile.rpcore.util.network_communication import NetworkCommunication NetworkCommunication.send_async(NetworkCommunication.DAYTIME_PORT, "loadconf") - print("✅ 已通知Day Time Editor重新加载配置") + #print("✅ 已通知Day Time Editor重新加载配置") except Exception as e: print(f"⚠️ 通知Day Time Editor失败: {e}") @@ -9054,22 +9169,98 @@ class PropertyPanelManager: def _getActor(self, origin_model): if origin_model in self._actor_cache: return self._actor_cache[origin_model] + + # 首先检查是否可以直接从内存中的模型创建Actor + if origin_model.hasTag("can_create_actor_from_memory") and origin_model.getTag("can_create_actor_from_memory").lower() == "true": + try: + print(f"[Actor加载] 直接从内存模型创建Actor: {origin_model.getName()}") + # 直接从内存中的模型创建Actor + test_actor = Actor(origin_model) + anims = test_actor.getAnimNames() + test_actor.reparentTo(self.world.render) + self._actor_cache[origin_model] = test_actor + print(f"[Actor加载] 内存创建检测到动画: {anims}") + if anims: + return test_actor + else: + test_actor.cleanup() + test_actor.removeNode() + except Exception as e: + print(f"从内存模型创建Actor失败: {e}") + + # 如果不能直接从内存创建,再尝试通过文件路径加载 filepath = origin_model.getTag("model_path") if not filepath: return None print(f"[Actor加载] 尝试加载: {filepath}") + # 处理跨平台路径问题 + import os + # 检查路径是否有效,如果无效则尝试修复 + if not os.path.exists(filepath): + original_filepath = filepath + # 尝试多种修复策略 + fixed = False + + # 策略1: 处理Linux风格路径在Windows上的问题 + if filepath.startswith('/') and ':' not in filepath: + # 提取文件名并尝试在当前目录查找 + filename = os.path.basename(filepath) + potential_path = os.path.join(os.getcwd(), filename) + if os.path.exists(potential_path): + filepath = potential_path + fixed = True + + # 策略2: 处理路径分隔符问题 + if not fixed: + # 尝试规范化路径 + normalized_path = os.path.normpath(filepath) + if os.path.exists(normalized_path): + filepath = normalized_path + fixed = True + + # 策略3: 在Resources目录中查找 + if not fixed: + # 尝试在Resources目录中查找文件 + resources_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), "Resources") + filename = os.path.basename(filepath) + potential_path = os.path.join(resources_path, filename) + if os.path.exists(potential_path): + filepath = potential_path + fixed = True + + if fixed: + print(f"路径修复: {original_filepath} -> {filepath}") + # 更新模型标签 + origin_model.setTag("model_path", filepath) + else: + print(f"[警告] 模型文件不存在: {filepath}") + return None + # 检查是否是 FBX 文件,如果是,使用专门的 FBX 动画加载器 if filepath.lower().endswith('.fbx'): - return self._createFBXActor(origin_model, filepath) + pass + #return self._createFBXActor(origin_model, filepath) # 其他格式使用标准 Actor 加载 try: import gltf - print(f"[GLTF加载] 尝试加载: {filepath}") + from panda3d.core import Filename + + # 将Panda3D路径转换为操作系统特定路径 + panda_filename = Filename(filepath) + os_specific_path = panda_filename.to_os_specific() + print(f"[路径转换] {filepath} -> {os_specific_path}") + + print(f"[GLTF加载] 尝试加载: {os_specific_path}") + + # 使用明确的设置确保动画被加载 + gltf_settings = gltf.GltfSettings(skip_animations=False) # test_actor=Actor(NodePath(gltf._loader.GltfLoader.load_file(filepath,None))) - test_actor = Actor(NodePath(gltf.load_model(filepath, None))) + model_root = gltf.load_model(os_specific_path, gltf_settings) + model_node = NodePath(model_root) + test_actor = Actor(model_node) anims = test_actor.getAnimNames() test_actor.reparentTo(self.world.render) self._actor_cache[origin_model] = test_actor @@ -9711,6 +9902,12 @@ except Exception as e: collision_group.setLayout(collision_layout) self._propertyLayout.addWidget(collision_group) + # 同步一次状态,确保主按钮与可见性按钮齐全 + try: + self._updateCollisionPanelState(model) + except Exception: + pass + except Exception as e: print(f"创建碰撞面板失败: {e}") import traceback @@ -10065,8 +10262,9 @@ except Exception as e: collision_np.show() print(f"显示碰撞:{model.getName()}") - # 立即更新按钮状态 + # 立即更新按钮状态,并同步更新面板(确保缺失主按钮时补建) self._updateCollisionVisibilityButton(model) + self._updateCollisionPanelState(model) except Exception as e: print(f"切换碰撞可见性失败: {e}") @@ -10274,26 +10472,18 @@ except Exception as e: has_collision = self._hasCollision(model) print(f"模型 {model.getName()} 是否有碰撞体: {has_collision}-------------------------------------------------") - # 更新状态徽章(使用固定宽度) - if has_collision: - new_badge = self.createFixedStatusBadge("已启用", "green") - else: - new_badge = self.createFixedStatusBadge("未启用", "red") - - # 替换旧的徽章 - old_badge = self.collision_status_badge - parent_layout = old_badge.parent().layout() - if parent_layout: - # 找到旧徽章在布局中的位置 - for i in range(parent_layout.count()): - item = parent_layout.itemAt(i) - if item and item.widget() == old_badge: - # 移除旧徽章并添加新徽章 - parent_layout.removeWidget(old_badge) - old_badge.deleteLater() - parent_layout.addWidget(new_badge, 0, 0, 1, 1) # 状态徽章在第0行第1列 - self.collision_status_badge = new_badge - break + # 更新状态徽章(直接修改现有控件,避免布局冲突) + if hasattr(self, 'collision_status_badge') and self.collision_status_badge: + if has_collision: + self.collision_status_badge.setText("已启用") + # 使用固定宽度的绿色样式,保持尺寸一致 + self.collision_status_badge.setStyleSheet(self.badge_style_green_fixed) + else: + self.collision_status_badge.setText("未启用") + # 使用固定宽度的红色样式,保持尺寸一致 + self.collision_status_badge.setStyleSheet(self.badge_style_red_fixed) + # 触发重绘,确保立即可见 + self.collision_status_badge.update() if has_collision: # 有碰撞:显示移除按钮,下拉框变为只读并显示当前类型 @@ -10309,6 +10499,18 @@ except Exception as e: except: pass self.collision_button.clicked.connect(lambda: self._removeCollisionAndUpdate(model)) + else: + # 若不存在主按钮,则创建“移除碰撞”按钮并加入布局 + try: + self.collision_button = self.createModernButton("移除碰撞") + self.collision_button.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) + self.collision_button.setStyleSheet(self._collision_button_style()) + self.collision_button.clicked.connect(lambda: self._removeCollisionAndUpdate(model)) + if hasattr(self, 'collision_layout') and self.collision_layout is not None: + row = self.collision_layout.rowCount() + self.collision_layout.addWidget(self.collision_button, row, 0, 1, 4) + except Exception as _e: + print(f"创建移除碰撞按钮失败: {_e}") # 获取并显示当前碰撞类型,设置为只读 current_shape = self._getCurrentCollisionShape(model) @@ -10328,6 +10530,13 @@ except Exception as e: self.collision_visibility_button.setVisible(True) self._updateCollisionVisibilityButton(model) + # 确保按钮顺序:可见性按钮在上,主按钮在下 + if hasattr(self, 'collision_layout'): + try: + self._repositionButtons(self.collision_layout.rowCount()) + except Exception: + pass + print(f"碰撞面板状态更新:有碰撞 - {current_shape}") else: @@ -10343,6 +10552,18 @@ except Exception as e: except: pass self.collision_button.clicked.connect(lambda: self._addCollisionAndUpdate(model)) + else: + # 若不存在主按钮,则创建“添加碰撞”按钮并加入布局 + try: + self.collision_button = self.createModernButton("添加碰撞") + self.collision_button.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) + self.collision_button.setStyleSheet(self._collision_button_style()) + self.collision_button.clicked.connect(lambda: self._addCollisionAndUpdate(model)) + if hasattr(self, 'collision_layout') and self.collision_layout is not None: + row = self.collision_layout.rowCount() + self.collision_layout.addWidget(self.collision_button, row, 0, 1, 4) + except Exception as _e: + print(f"创建添加碰撞按钮失败: {_e}") # 恢复为可编辑状态 self.collision_shape_combo.setEnabled(True) @@ -10356,6 +10577,13 @@ except Exception as e: if hasattr(self, 'collision_visibility_button'): self.collision_visibility_button.setVisible(False) + # 确保按钮顺序:主按钮位于底部 + if hasattr(self, 'collision_layout'): + try: + self._repositionButtons(self.collision_layout.rowCount()) + except Exception: + pass + print(f"碰撞面板状态更新:无碰撞 - 可编辑") except Exception as e: @@ -11018,13 +11246,24 @@ except Exception as e: layout = self.collision_layout is_collision_visible = self._isCollisionVisible(model) - # 找到合适的行位置 + # 放在当前最后一行 current_row = layout.rowCount() self.collision_visibility_button = QPushButton("隐藏碰撞" if is_collision_visible else "显示碰撞") self.collision_visibility_button.setStyleSheet(self._collision_button_style()) self.collision_visibility_button.clicked.connect(lambda: self._toggleCollisionVisibility(model)) - layout.addWidget(self.collision_visibility_button, current_row - 1, 0, 1, 4) + layout.addWidget(self.collision_visibility_button, current_row, 0, 1, 4) + + # 确保存在主按钮(有碰撞时应显示移除碰撞) + if self._hasCollision(model) and not hasattr(self, 'collision_button'): + try: + self.collision_button = self.createModernButton("移除碰撞") + self.collision_button.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) + self.collision_button.setStyleSheet(self._collision_button_style()) + self.collision_button.clicked.connect(lambda: self._removeCollisionAndUpdate(model)) + layout.addWidget(self.collision_button, current_row + 1, 0, 1, 4) + except Exception as _e: + print(f"在可见性按钮后创建移除碰撞按钮失败: {_e}") except Exception as e: print(f"添加可见性按钮失败: {e}") @@ -11035,22 +11274,39 @@ except Exception as e: if hasattr(self, 'collision_layout'): layout = self.collision_layout - # 移动可见性按钮 - if hasattr(self, 'collision_visibility_button'): - layout.addWidget(self.collision_visibility_button, new_row, 0, 1, 4) - new_row += 1 + def _remove_if_present(w): + try: + if not w: + return + # 从布局中移除该控件(如果已存在) + for i in range(layout.count()): + item = layout.itemAt(i) + if item and item.widget() is w: + layout.removeWidget(w) + break + except Exception: + pass - # 移动主按钮 - if hasattr(self, 'collision_button'): - layout.addWidget(self.collision_button, new_row, 0, 1, 4) + # 计算底部行 + bottom_row = layout.rowCount() + + # 按固定顺序重新放置:先可见性按钮,再主按钮 + if hasattr(self, 'collision_visibility_button') and self.collision_visibility_button: + _remove_if_present(self.collision_visibility_button) + layout.addWidget(self.collision_visibility_button, bottom_row, 0, 1, 4) + bottom_row += 1 + + if hasattr(self, 'collision_button') and self.collision_button: + _remove_if_present(self.collision_button) + layout.addWidget(self.collision_button, bottom_row, 0, 1, 4) except Exception as e: print(f"重新定位按钮失败: {e}") def _hideCollisionParameterControls(self): - """隐藏碰撞参数控件(保留按钮)""" + """隐藏并移除碰撞参数控件与其所在的行(保留状态/形状/按钮行)""" try: - # 清理属性引用,但保留按钮 + # 1) 先清理对象属性引用,避免残留信号 param_attrs = [ 'collision_pos_x', 'collision_pos_y', 'collision_pos_z', 'collision_radius', @@ -11058,47 +11314,72 @@ except Exception as e: 'collision_capsule_radius', 'collision_capsule_height', 'collision_normal_x', 'collision_normal_y', 'collision_normal_z' ] - - # 隐藏并删除参数控件 for attr in param_attrs: if hasattr(self, attr): widget = getattr(self, attr) - if widget: - widget.setVisible(False) - widget.setParent(None) - widget.deleteLater() + try: + if widget: + widget.setParent(None) + widget.deleteLater() + except Exception: + pass delattr(self, attr) - # 同时清理可能的标签控件 - if hasattr(self, 'collision_layout'): + # 2) 深度移除布局中第2行(索引>=2)之后的所有参数相关项,保留按钮 + if hasattr(self, 'collision_layout') and self.collision_layout is not None: layout = self.collision_layout - # 收集需要移除的控件(不包括基本控件和按钮) - widgets_to_remove = [] + def _remove_layout_recursive(q_layout): + try: + while q_layout.count(): + child = q_layout.takeAt(0) + if child.widget(): + w = child.widget() + w.setParent(None) + w.deleteLater() + elif child.layout(): + _remove_layout_recursive(child.layout()) + except Exception: + pass - for i in range(layout.rowCount()): - if i >= 2: # 从第3行开始检查 - for j in range(layout.columnCount()): - item = layout.itemAtPosition(i, j) - if item: - widget = item.widget() - if widget and hasattr(widget, 'text'): - # 检查是否是参数相关的标签 - text = widget.text() - if any(keyword in text for keyword in - ['位置偏移', 'X:', 'Y:', 'Z:', '半径:', '尺寸:', '宽度:', '长度:', '高度:', - '法向量:', 'Nx:', 'Ny:', 'Nz:']): - widgets_to_remove.append(widget) + row_count = layout.rowCount() + col_count = layout.columnCount() + # 从参数区域开始清理(行索引2及以后),但跳过我们要保留的按钮 + for i in range(2, row_count): + for j in range(0, col_count): + item = layout.itemAtPosition(i, j) + if not item: + continue + # 若是按钮,且是保留的按钮,则跳过 + w = item.widget() + if w is not None: + try: + if (hasattr(self, 'collision_button') and w == self.collision_button) or (hasattr(self, 'collision_visibility_button') and w == self.collision_visibility_button): + continue + except Exception: + pass + layout.removeWidget(w) + w.setParent(None) + w.deleteLater() + elif item.layout(): + # 移除嵌套布局(包含标签与输入框) + nested = item.layout() + _remove_layout_recursive(nested) + layout.removeItem(nested) - # 移除参数标签 - for widget in widgets_to_remove: - widget.setVisible(False) - widget.setParent(None) - widget.deleteLater() + # 尝试把保留的按钮移动到参数区域第一行(行2)下方,保持整洁 + try: + next_row = 2 + if hasattr(self, 'collision_visibility_button'): + self.collision_visibility_button.setVisible(False) + if hasattr(self, 'collision_button') and self.collision_button is not None: + layout.addWidget(self.collision_button, next_row, 0, 1, 4) + except Exception: + pass - print("隐藏碰撞参数控件完成(保留按钮)") + print("隐藏碰撞参数控件完成(仅保留状态、形状与按钮)") except Exception as e: print(f"隐藏碰撞参数控件失败: {e}") import traceback - traceback.print_exc() \ No newline at end of file + traceback.print_exc() diff --git a/ui/widgets.py b/ui/widgets.py index 6f59fc59..dffa7eb5 100644 --- a/ui/widgets.py +++ b/ui/widgets.py @@ -20,7 +20,7 @@ from PyQt5.sip import wrapinstance from direct.showbase.ShowBaseGlobal import aspect2d from panda3d.core import ModelRoot, NodePath, CollisionNode -from QPanda3D.QPanda3DWidget import QPanda3DWidget +from QMeta3D.QMeta3DWidget import QMeta3DWidget from scene import util from ui.icon_manager import get_icon_manager @@ -456,7 +456,7 @@ class NewProjectDialog(QDialog): self.accept() -class CustomPanda3DWidget(QPanda3DWidget): +class CustomMeta3DWidget(QMeta3DWidget): """自定义Panda3D显示部件""" def __init__(self, world, parent=None): @@ -4196,7 +4196,7 @@ class CustomTreeWidget(QTreeWidget): def _createSpotLightNode(self, parent_node, parent_item): """创建聚光灯节点""" from RenderPipelineFile.rpcore import SpotLight - from QPanda3D.Panda3DWorld import get_render_pipeline + from QMeta3D.Meta3DWorld import get_render_pipeline from panda3d.core import Vec3, NodePath try: @@ -4252,7 +4252,7 @@ class CustomTreeWidget(QTreeWidget): def _createPointLightNode(self, parent_node, parent_item): """创建点光源节点""" from RenderPipelineFile.rpcore import PointLight - from QPanda3D.Panda3DWorld import get_render_pipeline + from QMeta3D.Meta3DWorld import get_render_pipeline from panda3d.core import Vec3, NodePath try: