Tkinter with .p3d, error on opening

Hello,

I have a simulation of Brownian motion in three dimensions. It works from IDLE (the one included in panda3d), and it has a Tkinter program that starts up, accepts user input, and uses the input in a panda3d main loop. Tkinter doesn’t run during the panda3d program.

I got packp3d to work using “packp3d -o 3DTree.p3d -r tk -d C:/Users/myfolder -m 3DTree_main.py”

I installed the most recent 32-bit 1.9.4 SDK and Runtime, fresh downloads today. I’m running a 64-bit Windows 10 machine. I tried the 64 bit version of the SDK first, but I had the same problem.

My import looks like this:
import Tkinter as tkinter
master = tkinter.Tk()

from math import pi, sin, cos, sqrt
from random import randint
from direct.showbase.ShowBase import ShowBase
from direct.task import Task
from panda3d.core import AmbientLight, DirectionalLight, Material
from panda3d.core import LVector3, Mat4
from pandac.PandaModules import VBase4

When I open the command shell and start the program (>panda3d.exe 3DTree.p3d), I get this error:
_tkinter.TclError: Can’t find a usable init.tcl in the following directories:
“list of directories”

I tried copying the lib/tcl8.5 folder into the first directory it says it searched in, but that threw up another directory error. So I stopped messing with the files. The manual makes it seem like the -r tk should solve these issues, but it doesn’t seem to. I couldn’t find a solution online. ‘-r morepy’ doesn’t change the error.

Any help would be greatly appreciated.

I’m sorry to say that this is due to a known bug that prevents tkinter from working well using the panda3d packaging system:
https://bugs.launchpad.net/panda3d/+bug/1523079

Because we are transitioning to the new deploy-ng system going forward, that is what our development efforts will be focused on. I don’t think it’s been tested with Tkinter yet, though, but I’m happy to help you out if you decide to pursue that path.

Thanks for the reply.

That’s a bit of a bummer. But overall, the program works well, so I can’t complain too much.

I am happy to try a different packaging system to include Tkinter.

Alternatively, is there another simple GUI for user inputs like Tkinter that works well with panda3d and/or packp3d? I only have two number entry inputs and a few buttons. I could learn and code up another one quickly. I saw in the manual a DirectDialogue, but I wanted a sequential user input–>output–>parameters for the p3d program. Tkinter is outside my main loop for that reason.

Thanks!

I was able to create the same user interface without Tkinter using the DirectGUI. It works as a stand-alone program and accepts and outputs all the right values. But I’m having a similar problem as here: Destroying and re-creating ShowBase

My code looks like this:

class input_variables(ShowBase):
    def __init__(self):
        ShowBase.__init__(self)

//three buttons and two number entry fields.       

      exit_button = DirectButton(text = "Make it so!",
                                   command = self.exit_button,
                                   scale = 0.1,
                                   pos = (0, 0, -0.75),
                                   pressEffect = 1)
        def exit_button(self):
        base.destroy()

//run
del input_variables

//more code
class main(ShowBase):
// etc
// run main

I get a blank p3d screen that doesn’t run my main(). No errors though.

Thanks again.

I don’t know if there are other toolkits. wxPython also has packaging difficulties. DirectGui does not, however.

You can’t have more than one instance of ShowBase active at present, although it should be possible to destroy one and then create another (but this is a very recent feature in the development branch). I think it may be easier to simply share the ShowBase instance, and instead reset the scene graph for the second part of your application to run.

Thanks for the help so far.

I have everything under main() now, including an additional task ‘input_variables’, which is the DirectGUI, still two text entries and radiobuttons. That part works. I have an exit_button to .destroy() all the buttons and .cleanup() the text.

After reading, I added a taskMgr.remove(‘input_variables’).

But when I push the button I get a blank screen again. The task.frame is stuck at 0 along with the task.time is stuck at 0.0. I can’t find a way to get it to move onto the next task in my stack.

The main() loop should refresh every frame. At least, that’s what it does with the other part of my program when it’s divorced from the DirectGUI part.

It’s hard to tell what’s going on from just your description. Would it be possible for you to show more code? In particular, seeing the general gist of your main() and the part where you destroy things would be helpful.

Sure thing.

params = [SPHERE_SIZE, 1, DIST_BTW, True, 1]

class main(ShowBase):
  
    def __init__(self):
        ShowBase.__init__(self)
 
        # set background color
        base.setBackgroundColor(1,1,1)
 
        #call methods
        self.run_input_variables()
        self.calculate()
        self.choose_and_run()
         
    def run_input_variables(self):
        self.taskMgr.add(self.input_variables, "input_variables",
                         extraArgs = [], appendTask = True)
 
    def input_variables(self, task):       
        # Calls to the text and button widgets.
        # Q0 sphere size
        button0_text = OnscreenText(text = "Sphere Size",
                                    scale = 0.1, pos = (0, 0.9),
                                    align=TextNode.ACenter)
        button0 = DirectEntry(text = "",
                              command = self.item0, initialText = "5",
                              scale = 0.1, width = 10,
                              numLines = 1, pos = (-0.4, 0, 0.75))
        # Q1 one point, two points, cerebellum simulation
        button1_text = OnscreenText(text = "What type of simulation?",
                                    scale = 0.1, pos = (0, 0.6),
                                    align=TextNode.ACenter)
        button11 = DirectRadioButton(text = "One Point",
                                     command = self.item1, variable = v1,
                                     value = [1], scale = 0.1,
                                     pos = (-0.56, 0, 0.45),
                                     pressEffect = 1, indicatorValue = 0)
        button12 = DirectRadioButton(text = "Two Point",
                                     command = self.item1, variable = v1,
                                     value = [2], scale = 0.1,
                                     pos = (0, 0, 0.45),
                                     pressEffect = 1, indicatorValue = 0)
        button13 = DirectRadioButton(text = "Cerebellum",
                                     command = self.item1, variable = v1,
                                     value = [3], scale = 0.1,
                                     pos = (0.6, 0, 0.45),
                                     pressEffect = 1, indicatorValue = 0)
 
        buttons1 = [button11, button12, button13]
        for button in buttons1:
            button.setOthers(buttons1)
         
        # Q2 How far apart will the two points be if you use two.
        button2_text = OnscreenText(text = "How far apart (if two points)",
                                    scale = 0.1, pos = (0, 0.3),
                                    align=TextNode.ACenter)
 
        button2 = DirectEntry(text = "",
                              command = self.item2, initialText = "30",
                              scale = 0.1, width = 10,
                              numLines = 1, pos = (-0.4, 0, 0.15))
 
        # Q3 How many points in the cerebellar simulation?
        button3_text = OnscreenText(text = "What type of simulation?",
                                    scale = 0.1, pos = (0, 0),
                                    align=TextNode.ACenter)
        button31 = DirectRadioButton(text = "Four",
                                     command = self.item3, variable = v3,
                                     value = [True], scale = 0.1,
                                     pos = (-0.2, 0, -0.15),
                                     pressEffect = 1, indicatorValue = 0)
        button32 = DirectRadioButton(text = "Five",
                                     command = self.item3, variable = v3,
                                     value = [False], scale = 0.1,
                                     pos = (0.2, 0, -0.15),
                                     pressEffect = 1, indicatorValue = 0)
 
        buttons3 = [button31, button32]
        for button in buttons3:
            button.setOthers(buttons3)
 
        # Q4 What color scheme. Gray, random, RGBY(W)?
        button4_text = OnscreenText(text = "What color scheme?",
                                    scale = 0.1, pos = (0, -0.3),
                                    align=TextNode.ACenter)
         
        button41 = DirectRadioButton(text = "Gray",
                                     command = self.item4, variable = v4,
                                     value = [1], scale = 0.1,
                                     pos = (-0.5, 0, -0.45),
                                     pressEffect = 1, indicatorValue = 0)
        button42 = DirectRadioButton(text = "Random",
                                     command = self.item4, variable = v4,
                                     value = [2], scale = 0.1,
                                     pos = (-0.05, 0, -0.45),
                                     pressEffect = 1, indicatorValue = 0)
 
        button43 = DirectRadioButton(text = "RGBY(W)",
                                     command = self.item4, variable = v4,
                                     value = [3], scale = 0.1,
                                     pos = (0.5, 0, -0.45),
                                     pressEffect = 1, indicatorValue = 0)
 
        buttons4 = [button41, button42, button43]
        for button in buttons4:
            button.setOthers(buttons4)
 
        # exit
        exit_button = DirectButton(text = "Make it so!",
                                   command = self.exit_button,
                                   scale = 0.1,
                                   pos = (0, 0, -0.75),
                                   pressEffect = 1)
             
        self.button0_text = button0_text
        self.button0 = button0
        self.button1_text = button1_text
        self.button11 = button11
        self.button12 = button12
        self.button13 = button13
        self.button2_text = button2_text
        self.button2 = button2
        self.button3_text = button3_text
        self.button31 = button31
        self.button32 = button32
        self.button4_text = button4_text
        self.button41 = button41
        self.button42 = button42
        self.button43 = button43
        self.exit_button = exit_button
        print(task.frame)
        return Task.done
         
    # sphere size
    def item0(self, param0):
        try:
            param0 = int(param0)
        except ValueError:
            param0 = 5
        params[0] = param0
 
    # One point, two points, cerebellum simulation
    def item1(self):
        params[1] = v1[0]
             
    # How far apart will the two points be if you use two.
    def item2(self, param2):
        try:
            param2 = int(param2)
        except ValueError:
            param2 = 30
        params[2] = param2
 
    # How many points in the cerebellar simulation, 4 or 5
    def item3(self):
        params[3] = v3[0]
 
    # What color scheme. Gray, random, RGBY(W)?
    def item4(self):
        params[4] = v4[0]
 
    def exit_button(self):
        self.button0_text.cleanup()
        self.button0.destroy()
        self.button1_text.cleanup()
        self.button11.destroy()
        self.button12.destroy()
        self.button13.destroy()
        self.button2_text.cleanup()
        self.button2.destroy()
        self.button3_text.cleanup()
        self.button31.destroy()
        self.button32.destroy()
        self.button4_text.cleanup()
        self.button41.destroy()
        self.button42.destroy()
        self.button43.destroy()
        self.exit_button.destroy()
        taskMgr.remove('input_variables')
        print(params)

I don’t have the whole picture here (since I don’t know what choose_and_run actually does, or what your other tasks look like), but I do get the impression that perhaps there might be a misunderstanding as to how tasks work, perhaps confusing it for a Finite State Machine. A task is just a method that is scheduled to run at a specified time or interval; in this case, you are adding input_variables as a task right during initialization, which then sets up the GUI and exits right away. That’s fine, except there seems to be no actual reason to put it as a task (you could just call self.input_variables() directly in the __init__) nor will taskMgr.remove('input_variables') actually do anything (since the task is already done by that time).

I could further help you if you showed me how “the next task in your stack” is set up, so that we can figure out why it’s not being run when you press the exit button.

Ah. I see. Here’s the whole program. Everything worked with Tkinter user inputs outside the main class. I thought the other parts should be fine, but perhaps not.

# Dependancies for math
from math import pi, sin, cos, sqrt
from random import randint
import sys

# Dependancies for Panda 3D
from direct.showbase.ShowBase import ShowBase
from direct.task import Task
from panda3d.core import AmbientLight, DirectionalLight, Material
from panda3d.core import LVector3, Mat4, TextNode
from pandac.PandaModules import VBase4
from direct.gui.DirectGui import *
from direct.gui.OnscreenText import OnscreenText
from direct.interval.IntervalGlobal import *

# globals
SPHERE_SIZE = 5
DIST_BTW = 35
COLOR = VBase4(0.4, 0.4, 0.4, 1)
v1 = [1]
v3 = [True]
v4 = [1]
params = [SPHERE_SIZE, 1, DIST_BTW, True, 1]
Next = False
"""
Output:
params[0] - sphere size
params[1] - One point = 1, Two points = 2, Cerebellum simulation = 3
params[2] - how far apart two points will be if you choose two (2 for params[1])
params[3] - How many points for the cerebellar simulation, 4 = True, 5 = False
params[4] - What color scheme. Gray = 1, random = 2, RGBY(W) = 3
"""

# Useful function to make the color of the sphere random.
# No input, just outputs a random VBase4 color.
def random_color():
    random_color = VBase4(float(randint(0, 100))/100,
                          float(randint(0, 100))/100,
                          float(randint(0, 100))/100,
                          1)
    return random_color

# Useful function for finding the Euclidean distance in 3D.
def euclidean(vector1, vector2):
    def square(x): return float(x)*float(x)
    return sqrt(square(vector1[0] - vector2[0])
                + square(vector1[1] - vector2[1])
                + square(vector1[2] - vector2[2]))



# Main class. 
class main(ShowBase):
 
    def __init__(self):
        ShowBase.__init__(self)

        # set background color
        base.setBackgroundColor(1,1,1)
        # set ambient and directional light on the sphere
        # for scoping reasons, make them class variables
        ambientLight = AmbientLight("ambientLight")
        ambientLight.setColor((0.3 , 0.4, 0.5, 1))
        directionalLight = DirectionalLight("directionalLight")
        directionalLight.setDirection(LVector3(0, 8, -2.5))
        directionalLight.setColor((0.9, 0.9, 0.9, 1))
        self.ambientLight = ambientLight
        self.directionalLight = directionalLight

        self.disableMouse()
        self.camera.setPos(0, -300, 0) # Sets the camera back.
        # Stock code from the Panda3D manual that reactivates the mouse standard.
        # Left click moves in the x and z planes, right click in the y plane.
        # Both together rotates around (0, 0, 0).
        mat=Mat4(camera.getMat())
        mat.invertInPlace()
        base.mouseInterfaceNode.setMat(mat)
        base.enableMouse()

        #call methods
        self.input_variables()
        self.calculate()
        self.choose_and_run()

    def input_variables(self):       
        # Calls to the text and button widgets.
        # Q0 sphere size
        button0_text = OnscreenText(text = "Sphere Size",
                                    scale = 0.1, pos = (0, 0.9),
                                    align=TextNode.ACenter)
        button0 = DirectEntry(text = "",
                              command = self.item0, initialText = "5",
                              scale = 0.1, width = 10,
                              numLines = 1, pos = (-0.4, 0, 0.75))
        # Q1 one point, two points, cerebellum simulation
        button1_text = OnscreenText(text = "What type of simulation?",
                                    scale = 0.1, pos = (0, 0.6),
                                    align=TextNode.ACenter)
        button11 = DirectRadioButton(text = "One Point",
                                     command = self.item1, variable = v1,
                                     value = [1], scale = 0.1,
                                     pos = (-0.56, 0, 0.45),
                                     pressEffect = 1, indicatorValue = 0)
        button12 = DirectRadioButton(text = "Two Point",
                                     command = self.item1, variable = v1,
                                     value = [2], scale = 0.1,
                                     pos = (0, 0, 0.45),
                                     pressEffect = 1, indicatorValue = 0)
        button13 = DirectRadioButton(text = "Cerebellum",
                                     command = self.item1, variable = v1,
                                     value = [3], scale = 0.1,
                                     pos = (0.6, 0, 0.45),
                                     pressEffect = 1, indicatorValue = 0)

        buttons1 = [button11, button12, button13]
        for button in buttons1:
            button.setOthers(buttons1)
        

        # Q2 How far apart will the two points be if you use two.
        button2_text = OnscreenText(text = "How far apart (if two points)",
                                    scale = 0.1, pos = (0, 0.3),
                                    align=TextNode.ACenter)

        button2 = DirectEntry(text = "",
                              command = self.item2, initialText = "30",
                              scale = 0.1, width = 10,
                              numLines = 1, pos = (-0.4, 0, 0.15))

        # Q3 How many points in the cerebellar simulation?
        button3_text = OnscreenText(text = "What type of simulation?",
                                    scale = 0.1, pos = (0, 0),
                                    align=TextNode.ACenter)
        button31 = DirectRadioButton(text = "Four",
                                     command = self.item3, variable = v3,
                                     value = [True], scale = 0.1,
                                     pos = (-0.2, 0, -0.15),
                                     pressEffect = 1, indicatorValue = 0)
        button32 = DirectRadioButton(text = "Five",
                                     command = self.item3, variable = v3,
                                     value = [False], scale = 0.1,
                                     pos = (0.2, 0, -0.15),
                                     pressEffect = 1, indicatorValue = 0)

        buttons3 = [button31, button32]
        for button in buttons3:
            button.setOthers(buttons3)

        # Q4 What color scheme. Gray, random, RGBY(W)?
        button4_text = OnscreenText(text = "What color scheme?",
                                    scale = 0.1, pos = (0, -0.3),
                                    align=TextNode.ACenter)
        
        button41 = DirectRadioButton(text = "Gray",
                                     command = self.item4, variable = v4,
                                     value = [1], scale = 0.1,
                                     pos = (-0.5, 0, -0.45),
                                     pressEffect = 1, indicatorValue = 0)
        button42 = DirectRadioButton(text = "Random",
                                     command = self.item4, variable = v4,
                                     value = [2], scale = 0.1,
                                     pos = (-0.05, 0, -0.45),
                                     pressEffect = 1, indicatorValue = 0)

        button43 = DirectRadioButton(text = "RGBY(W)",
                                     command = self.item4, variable = v4,
                                     value = [3], scale = 0.1,
                                     pos = (0.5, 0, -0.45),
                                     pressEffect = 1, indicatorValue = 0)

        buttons4 = [button41, button42, button43]
        for button in buttons4:
            button.setOthers(buttons4)

        # exit
        exit_button = DirectButton(text = "Make it so!",
                                   command = self.exit_button,
                                   scale = 0.1,
                                   pos = (0, 0, -0.75),
                                   pressEffect = 1)
            
        self.button0_text = button0_text
        self.button0 = button0
        self.button1_text = button1_text
        self.button11 = button11
        self.button12 = button12
        self.button13 = button13
        self.button2_text = button2_text
        self.button2 = button2
        self.button3_text = button3_text
        self.button31 = button31
        self.button32 = button32
        self.button4_text = button4_text
        self.button41 = button41
        self.button42 = button42
        self.button43 = button43
        self.exit_button = exit_button
        
    # sphere size
    def item0(self, param0):
        try:
            param0 = int(param0)
        except ValueError:
            param0 = 5
        params[0] = param0

    # One point, two points, cerebellum simulation
    def item1(self):
        params[1] = v1[0]
            
    # How far apart will the two points be if you use two.
    def item2(self, param2):
        try:
            param2 = int(param2)
        except ValueError:
            param2 = 30
        params[2] = param2

    # How many points in the cerebellar simulation, 4 or 5
    def item3(self):
        params[3] = v3[0]

    # What color scheme. Gray, random, RGBY(W)?
    def item4(self):
        params[4] = v4[0]

    def exit_button(self):
        self.button0_text.cleanup()
        self.button0.destroy()
        self.button1_text.cleanup()
        self.button11.destroy()
        self.button12.destroy()
        self.button13.destroy()
        self.button2_text.cleanup()
        self.button2.destroy()
        self.button3_text.cleanup()
        self.button31.destroy()
        self.button32.destroy()
        self.button4_text.cleanup()
        self.button41.destroy()
        self.button42.destroy()
        self.button43.destroy()
        self.exit_button.destroy()
        print(params)

    def calculate(self):
        # As outputs, 3D_tree_input tells you whether the user wanted
        # one or two points or the cerebellum simulation
        SPHERE_SIZE = params[0]
        DIST_BTW = params[1]
        radius = SPHERE_SIZE/1.5
        # This is the extra distance between the spheres not immediately adjacent.
        # Basically, it specifies how much crowding is allowed.
        extra_distance = int(round(1.8*radius))

        # Allowable vectors from a point.
        # Calculates number of possible vectors of a certain distance
        # That distance is proportional to radius.
        possible_vectors = []
        # turns degrees into radians 1 degree through 360 degrees.
        thetas = [(float(i)*pi)/180 for i in range(360)]
        phis = thetas

        for theta in thetas:
            for phi in phis:   
                x = radius * sin(theta) * cos(phi)
                y = radius * sin(theta) * sin(phi)
                z = radius * cos(theta)
                x = int(round(x))
                y = int(round(y))
                z = int(round(z))

                possible_vectors.append((x, y, z))

        possible_vectors = list(set(possible_vectors))

    def choose_and_run(self):
        # one sphere at (0, 0, 0)
        if params[2] == 1:
            sphere_list = [(0, 0, 0)]
            outside_sphere_list0 = []
            outside_sphere_list0.append(sphere_list[0])
            counter_list0 = [0]

            # choose random color
            if params[4] == 2:
                COLOR = random_color()
                

        # Two spheres DIST_BTW apart if user selects "Two Spheres"
        # Sets cerebellum to True if user selects "Cerebellum?"
        elif params[2] == 2:
            sphere_list = [(-DIST_BTW/2, 0, 0), (DIST_BTW/2, 0, 0)]
            if params[4] == 1:
                COLOR = [VBase4(0.4, 0.4, 0.4, 1), VBase4(0.6, 0.6, 0.6, 1)]
            elif params[4] == 2:
                COLOR = []
                COLOR.append(random_color())
                COLOR.append(random_color())
            elif params[4] == 3:
                COLOR = []
                for i in range(2):
                    g = randint(0, 4)
                    COLOR.append(
                        [VBase4(1, 0, 0, 1),
                         VBase4(0, 0.5, 0, 1),
                         VBase4(0, 0, 1, 1),
                         VBase4(1, 1, 0, 1),
                         VBase4(1, 1, 1, 1)][g]
                        )
                    
            # Will need these local variable lists. They define which
            # nodes are chosen each iteration to 'grow'
            # these lists all depend on the 'master' sphere_list.
            # will need one list for each time we want a new node (coudn't find a way around it)
            outside_sphere_list0 = []
            outside_sphere_list0.append(sphere_list[0])
            counter_list0 = [0]
            outside_sphere_list1 = []
            outside_sphere_list1.append(sphere_list[1])
            counter_list1 = [0]
            
        elif params[2] == 3:
            sphere_list = [(-25, 0, 0), (25, 0, 0), (0, 0, 13), (0, 0, -17), (-25, 0, -17)]

            # choose colors based on user input.
            if params[4] == 1:
                COLOR = [VBase4(float(i)/10, float(i)/10, float(i)/10, 1) for i in range(3, 8)]
            elif params[4] == 2:
                COLOR = [random_color() for i in range(5)]
            elif params[4] == 3:
                COLOR = [VBase4(1, 0, 0, 1),
                         VBase4(0, 0.5, 0, 1),
                         VBase4(0, 0, 1, 1),
                         VBase4(1, 1, 0, 1),
                         VBase4(1, 1, 1, 1)]

            # Similar to if the user chooses two points, but more points obviously
            outside_sphere_list0 = []
            outside_sphere_list0.append(sphere_list[0])
            counter_list0 = [0]

            outside_sphere_list1 = []
            outside_sphere_list1.append(sphere_list[1])
            counter_list1 = [0]

            outside_sphere_list2 = []
            outside_sphere_list2.append(sphere_list[2])
            counter_list2 = [0]

            outside_sphere_list3 = []
            outside_sphere_list3.append(sphere_list[3])
            counter_list3 = [0]

            if params[3] == False:
                outside_sphere_list4 = []
                outside_sphere_list4.append(sphere_list[4])
                counter_list4 = [0]
            
            # task manager for drawing the spheres with add_sphere below
            if params[2] == 1: 
                self.load_sphere(sphere_list[0], directionalLight, ambientLight, COLOR)
                self.taskMgr.add(self.add_sphere, "add_sphere",
                                 extraArgs = [COLOR, sphere_list[0], outside_sphere_list0, counter_list0],
                                 appendTask = True)

            if params[2] == 2:
                self.load_sphere(sphere_list[0], directionalLight, ambientLight, COLOR[0])
                self.load_sphere(sphere_list[1], directionalLight, ambientLight, COLOR[1])            
                self.taskMgr.add(self.add_sphere, "add_sphere",
                                 extraArgs = [COLOR[0], sphere_list[0], outside_sphere_list0, counter_list0],
                                 appendTask = True)
                self.taskMgr.add(self.add_sphere, "add_sphere",
                                 extraArgs = [COLOR[1], sphere_list[1], outside_sphere_list1, counter_list1],
                                 appendTask = True)

            # This is for all the spheres in the cerebellum simulation.
            if params[2] == 3:
                if params[3] == True:
                    four_or_five = 4
                elif params[3] == False:
                    four_or_five = 5

                for i in range(four_or_five):
                    self.load_sphere(sphere_list[i], directionalLight, ambientLight, COLOR[i])

                self.taskMgr.add(self.add_sphere, "add_sphere",
                                 extraArgs = [COLOR[0], sphere_list[0], outside_sphere_list0, counter_list0],
                                 appendTask = True)
                self.taskMgr.add(self.add_sphere, "add_sphere",
                                 extraArgs = [COLOR[1], sphere_list[1], outside_sphere_list1, counter_list1],
                                 appendTask = True)
                self.taskMgr.add(self.add_sphere, "add_sphere",
                                 extraArgs = [COLOR[2], sphere_list[2], outside_sphere_list2, counter_list2],
                                 appendTask = True)
                self.taskMgr.add(self.add_sphere, "add_sphere",
                                 extraArgs = [COLOR[3], sphere_list[3], outside_sphere_list3, counter_list3],
                                 appendTask = True)
                if params[3] == False:
                    self.taskMgr.add(self.add_sphere, "add_sphere",
                                     extraArgs = [COLOR[4], sphere_list[4], outside_sphere_list4, counter_list4],
                                     appendTask = True)
            
    # Loads a sphere. The inputs are self explanatory.
    # I set the directional and ambient light to class globals,
    # so it wouldn't creat a new light per sphere, which is possible
    # but apparently expensive.
    def load_sphere(self,
                    position,
                    directionalLight,
                    ambientLight,
                    color):
        
        # Load a sphere.
        pos = position
        self.sphere = self.loader.loadModel("models/misc/ball")
        self.sphere.setScale(SPHERE_SIZE,
                             SPHERE_SIZE,
                             SPHERE_SIZE)
        self.sphere.setPos(pos)
       
        # sets the sphere color
        self.sphere.setColor(color)
        self.sphere.reparentTo(render)

        # make the sphere a material (to behave with the light)
        material = Material()
        material.setShininess(10.0)
        self.sphere.setMaterial(material)
                
        # Set lighting on the sphere
        self.sphere.setLight(self.sphere.attachNewNode(directionalLight))
        self.sphere.setLight(self.sphere.attachNewNode(ambientLight))

    # adds spheres to the scene
    def add_sphere(self, color, starting_node, outside_sphere_list, counter_list, task):
        dLight = self.directionalLight
        aLight = self.ambientLight

        # select randomly from the list of spheres
        n = randint(0, len(outside_sphere_list) - 1)
        chosen_sphere = outside_sphere_list[n]
        # select randomly from the list of possible vectors
        n = randint(0, len(possible_vectors) - 1)
        chosen_vector = possible_vectors[n]

        # add the vectors together to get a new location
        # for a sphere
        x = chosen_sphere[0] + chosen_vector[0]
        y = chosen_sphere[1] + chosen_vector[1]
        z = chosen_sphere[2] + chosen_vector[2]

        # calculates the distance from where the new sphere will randomly be
        # drawn to the other spheres, removes reduntant numbers
        d = []
        f = []
        distances = []
        pop = list(sphere_list)
        if len(pop) > 1:
            pop.remove(chosen_sphere)
        d = [(x, y, z) for i in range(len(pop))]
        distances = [int(euclidean(d[i], pop[i]))  \
                     for i in range(len(pop))]
        distances = list(set(distances))

        # Recalculate distances for the chosen_sphere, so that in later code, we can remove
        # sheres that are too crowded.
        d = []
        chosen_distances = []
        d = [chosen_sphere for i in range(len(pop))]
        chosen_distances = [int(euclidean(d[i], pop[i])) for i in range(len(pop))]
        chosen_distances = list(set(chosen_distances))
        # Sorts the chosen_distances lowest to highest.
        chosen_distances.sort()
        
        # temp will be used to index the counter of chosen_sphere.
        temp = outside_sphere_list.index(chosen_sphere)

        # make the new sphere list and render the sphere
        # first if statement only allows the render if the sphere
        # is going to be a certain distance away from all the other spheres,
        # in this case, a 'radius' distance away
        if min(distances) >= int(radius):
            if counter_list[temp] <= 1:
                if len(chosen_distances) <= 5:

                    outside_sphere_list.append((x, y, z))
                    sphere_list.append((x, y, z))
                    counter_list.append(0)
                    # The chosen_sphere counter goes up by one.
                    counter_list[temp] += 1

                    # draw sphere
                    self.load_sphere((x, y, z),
                                     dLight,
                                     aLight,
                                     color)

                if len(chosen_distances) > 5 \
                   and chosen_distances[5] >= int(radius) + extra_distance:
                    
                    outside_sphere_list.append((x, y, z))
                    sphere_list.append((x, y, z))
                    counter_list.append(0)
                    # The chosen_sphere counter goes up by one.
                    counter_list[temp] += 1

                    # draw sphere
                    self.load_sphere((x, y, z),
                                     dLight,
                                     aLight,
                                     color)

            # The only available spheres for selection are the outside
            # ones in sphere_list. They 'burn out' after the number chosen
            # in the if statement.
            if counter_list[temp] > 1:
                outside_sphere_list.remove(chosen_sphere)
                del counter_list[temp]
                
        return Task.cont

Cerebellum = main()
Cerebellum.run()

So, am I getting it right that what you want to happen is that you see the GUI for changing settings, and then press the exit button, which will then start the simulation with the given parameters?

In that case, I think you should simply move the self.choose_and_run() call to the end of the exit_button function. Or am I missing something?

You got it. And now it works!

Actually, I had to fix some errors I accidentally introduced in this version too. But it works well now. The menu looks good.

Thank you for all the help.

And it is so close! I used packp3d to make a .p3d file. It opens normally and gives the opening screen. But with the .p3d file, when I push the exit button, it closes the window. No error if run from the command line. Proceeds normally when I open with IDLE.

Is there any information in the log file? This page talks about where the log files are stored:
https://www.panda3d.org/manual/index.php/The_runtime_Panda3D_directory

Thank you so much!!!

It was the direct path to ‘ball’ in my load_sphere function. It was self.loader.loadModel(“models/misc/ball”) before. After reading the manual, I just changed it to self.loader.loadModel(“ball”). It’s saved as ball.egg.pz in my directory.

It works! Again, thank you!