Ejemplo n.º 1
0
    def __init__(self):
        HUDElement.__init__(self)
        self.dir = ani.model_dir / 'hud' / 'english'
        self.text_scale = 0.2
        self.text_color = (1, 1, 1, 1)

        self.circle = OnscreenImage(image=panda_path(self.dir / 'circle.png'),
                                    parent=self.dummy_right,
                                    scale=0.15)
        self.circle.setTransparency(TransparencyAttrib.MAlpha)
        autils.alignTo(self.circle, self.dummy_right, autils.CL, autils.C)
        self.circle.setZ(-0.65)

        self.crosshairs = OnscreenImage(image=panda_path(self.dir /
                                                         'crosshairs.png'),
                                        pos=(0, 0, 0),
                                        parent=self.circle,
                                        scale=0.14)
        self.crosshairs.setTransparency(TransparencyAttrib.MAlpha)

        self.text = OnscreenText(
            text="(0.00, 0.00)",
            pos=(0, -1.15),
            scale=self.text_scale,
            fg=self.text_color,
            align=TextNode.ACenter,
            mayChange=True,
            parent=self.circle,
        )
Ejemplo n.º 2
0
    def add_backbutton(self, item):
        """Add a back button"""

        func_name = item[0].text

        # This is the button you click. NOTE `command` is assigned ad hoc. See
        # Menus.populate_menus
        button = DirectButton(
            scale=BACKBUTTON_TEXT_SCALE,
            geom=(
                loadImageAsPlane(panda_path(MENU_ASSETS / 'backbutton.png')),
                loadImageAsPlane(panda_path(MENU_ASSETS / 'backbutton.png')),
                loadImageAsPlane(
                    panda_path(MENU_ASSETS / 'backbutton_hover.png')),
                loadImageAsPlane(panda_path(MENU_ASSETS / 'backbutton.png')),
            ),
            relief=None,
        )

        button_np = NodePath(button)
        # functional_button-<menu_name>-<button_text>
        button_id = f"functional_button-{self.name}-back"
        button_np.setName(button_id)
        button_np.reparentTo(self.area)

        button_np.setPos(-0.92, 0, 0.22)

        self.elements.append({
            'type': 'backbutton',
            'content': button_np,
            'object': button,
            'func_name': func_name,
        })

        return button_np
Ejemplo n.º 3
0
    def init_cloth(self):
        if not self.has_model or not ani.settings['graphics']['table']:
            node = render.find('scene').attachNewNode('cloth') 
            path = ani.model_dir / 'table' / 'custom' / 'custom.glb'

            model = loader.loadModel(panda_path(path))
            model.reparentTo(node)
            model.setScale(self.w, self.l, 1)
        else:
            path_dir = ani.model_dir / 'table' / self.name
            pbr_path = path_dir / (self.name + '_pbr.glb')
            standard_path = path_dir / (self.name + '.glb')
            if ani.settings['graphics']['physical_based_rendering']:
                path = pbr_path
                if not path.exists():
                    path = standard_path
            else:
                path = standard_path

            if not path.exists():
                raise ConfigError(f"Couldn't find table model at {standard_path} or {pbr_path}")

            node = loader.loadModel(panda_path(path))
            node.reparentTo(render.find('scene'))
            node.setName('cloth')

        self.nodes['cloth'] = node
        self.collision_nodes = {}
Ejemplo n.º 4
0
    def init_sphere(self):
        """Initialize the ball's nodes"""
        position = render.find('scene').find('cloth').attachNewNode(
            f"ball_{self.id}_position")
        ball = position.attachNewNode(f"ball_{self.id}")

        if self.rel_model_path is None:
            fallback_path = ani.model_dir / 'balls' / 'set_1' / '1.glb'
            expected_path = ani.model_dir / 'balls' / 'set_1' / f'{self.id}.glb'
            path = expected_path if expected_path.exists() else fallback_path

            sphere_node = base.loader.loadModel(panda_path(path))
            sphere_node.reparentTo(position)

            if path == fallback_path:
                tex = sphere_node.find_texture(Path(fallback_path).stem)
            else:
                tex = sphere_node.find_texture(self.id)

            # Here, we define self.rel_model_path based on path. Since rel_model_path is defined relative to
            # the directory, pooltool/models/balls, some work has to be done to define rel_model_path
            # relative to this directory. NOTE assumes no child directory is named balls
            parents = []
            parent = path.parent
            while True:
                if parent.stem == 'balls':
                    self.rel_model_path = Path('/'.join(
                        parents[::-1])) / path.name
                    break
                parents.append(parent.stem)
                parent = parent.parent
        else:
            sphere_node = base.loader.loadModel(
                panda_path(ani.model_dir / 'balls' / self.rel_model_path))
            sphere_node.reparentTo(position)
            tex = sphere_node.find_texture(Path(self.rel_model_path).stem)

        # https://discourse.panda3d.org/t/visual-artifact-at-poles-of-uv-sphere-gltf-format/27975/8
        tex.set_minfilter(SamplerState.FT_linear)

        sphere_node.setScale(self.get_scale_factor(sphere_node))
        position.setPos(*self.rvw[0, :])

        self.nodes['sphere'] = sphere_node
        self.nodes['ball'] = ball
        self.nodes['pos'] = position
        self.nodes['shadow'] = self.init_shadow()
        if ani.settings['graphics']['angular_vectors']:
            self.nodes['vector'] = self.init_angular_vector()

        if self.initial_orientation:
            # This ball already has a defined initial orientation, so load it up
            self.set_orientation(self.initial_orientation)
        else:
            self.randomize_orientation()
            self.initial_orientation = self.get_orientation()
Ejemplo n.º 5
0
    def load_room(self, path):
        self.room = loader.loadModel(panda_path(path))
        self.room.reparentTo(render.find('scene'))
        self.room.setPos(self.offset)
        self.room.setName('room')

        self.room_loaded = True

        return self.room
Ejemplo n.º 6
0
    def load_floor(self, path):
        self.floor = loader.loadModel(panda_path(path))
        self.floor.reparentTo(render.find('scene'))
        self.floor.setPos(self.offset)
        self.floor.setName('floor')

        self.floor_loaded = True

        return self.floor
Ejemplo n.º 7
0
 def add_transparent_ball(self):
     self.trans_ball = base.loader.loadModel(
         panda_path(ani.model_dir / 'balls' /
                    self.closest_ball.rel_model_path))
     self.trans_ball.reparentTo(render.find('scene').find('cloth'))
     self.trans_ball.setTransparency(TransparencyAttrib.MAlpha)
     self.trans_ball.setAlphaScale(0.4)
     self.trans_ball.setPos(self.closest_ball.get_node('pos').getPos())
     self.trans_ball.setHpr(self.closest_ball.get_node('sphere').getHpr())
Ejemplo n.º 8
0
    def __init__(self, xml):
        self.xml = xml
        self.name = self.xml.attrib['name']

        self.title_font = loader.loadFont(panda_path(TITLE_FONT))
        self.button_font = loader.loadFont(panda_path(BUTTON_FONT))

        # No idea why this conditional must exist
        if self.title_font.get_num_pages() == 0:
            self.title_font.setPixelsPerUnit(90)

        self.last_element = None
        self.num_elements = 0
        self.elements = []

        self.area_backdrop = DirectFrame(
            frameColor=FRAME_COLOR,
            frameSize=(-1, 1, -1, 1),
            parent=render2d,
        )

        self.area_backdrop.setImage(
            panda_path(MENU_ASSETS / 'menu_background.jpeg'))
        img = OnscreenImage(image=panda_path(ani.logo_paths['default']),
                            pos=(0, 0, 0.65),
                            parent=self.area_backdrop,
                            scale=(1.4 * 0.25, 1, 1.4 * 0.22))
        img.setTransparency(TransparencyAttrib.MAlpha)

        self.area = DirectScrolledFrame(
            frameColor=(1, 1, 1, 0.2),  # alpha == 0
            canvasSize=(-1, 1, -3, 1),
            frameSize=(-1, 1, -0.9, 0.3),
            scrollBarWidth=0.04,
            horizontalScroll_frameSize=(0, 0, 0, 0),
            parent=aspect2d,
        )
        self.area.setPos(0, 0, 0)
        self.area.setTransparency(TransparencyAttrib.MAlpha)

        # 0.05 means you scroll from top to bottom in 20 discrete steps
        self.area.verticalScroll['pageSize'] = 0.05

        self.hovered_entry = None
Ejemplo n.º 9
0
    def __init__(self):
        HUDElement.__init__(self)
        self.dir = ani.model_dir / 'hud' / 'jack'
        self.text_scale = 0.4
        self.text_color = (1, 1, 1, 1)

        self.arc = OnscreenImage(image=panda_path(self.dir / 'arc.png'),
                                 pos=(1.4, 0, -0.45),
                                 parent=aspect2d,
                                 scale=0.075)
        self.arc.setTransparency(TransparencyAttrib.MAlpha)

        self.cue_cartoon = OnscreenImage(
            image=panda_path(self.dir / 'cue.png'),
            parent=aspect2d,
            pos=(0, 0, 0),
            scale=(0.15, 1, 0.01),
        )
        self.cue_cartoon.setTransparency(TransparencyAttrib.MAlpha)
        autils.alignTo(self.cue_cartoon, self.dummy_right, autils.CL, autils.C)
        self.cue_cartoon.setZ(-0.40)

        autils.alignTo(self.arc, self.cue_cartoon, autils.LR, autils.CR)

        self.rotational_point = OnscreenImage(image=panda_path(
            ani.model_dir / 'hud' / 'english' / 'circle.png'),
                                              parent=self.arc,
                                              scale=0.15)
        self.rotational_point.setTransparency(TransparencyAttrib.MAlpha)
        autils.alignTo(self.rotational_point, self.arc, autils.C, autils.LR)

        self.cue_cartoon.wrtReparentTo(self.rotational_point)

        self.text = OnscreenText(
            text="0 deg",
            pos=(-1, -1.4),
            scale=self.text_scale,
            fg=self.text_color,
            align=TextNode.ACenter,
            mayChange=True,
            parent=self.arc,
        )
Ejemplo n.º 10
0
    def init_model(self, R=c.R):
        path = utils.panda_path(ani.model_dir / 'cue' / 'cue.glb')
        cue_stick_model = loader.loadModel(path)
        cue_stick_model.setName('cue_stick_model')

        cue_stick = render.find('scene').find('cloth').attachNewNode(
            'cue_stick')
        cue_stick_model.reparentTo(cue_stick)

        self.nodes['cue_stick'] = cue_stick
        self.nodes['cue_stick_model'] = cue_stick_model
Ejemplo n.º 11
0
    def add_image(self, path, pos, scale):
        """Add an image to the menu

        Notes
        =====
        - images are parented to self.titleMenuBackdrop (as opposed self.titleMenu) in order to
          preserve their aspect ratios.
        """

        img = OnscreenImage(image=panda_path(path),
                            pos=pos,
                            parent=self.titleMenuBackdrop,
                            scale=scale)
        img.setTransparency(TransparencyAttrib.MAlpha)

        self.elements.append({
            'type': 'image',
            'name': panda_path(path),
            'content': img,
            'convert_factor': None,
        })
Ejemplo n.º 12
0
    def init_shadow(self):
        N = 20
        start, stop = 0.5, 0.9  # fraction of ball radius
        z_offset = 0.0005
        scales = np.linspace(start, stop, N)

        shadow_path = ani.model_dir / 'balls' / 'set_1' / f'shadow.glb'
        shadow_node = render.find('scene').find('cloth').attachNewNode(
            f'shadow_{self.id}')
        shadow_node.setPos(self.rvw[0, 0], self.rvw[0, 1], 0)

        # allow transparency of shadow to change
        shadow_node.setTransparency(TransparencyAttrib.MAlpha)

        for i, scale in enumerate(scales):
            shadow_layer = base.loader.loadModel(panda_path(shadow_path))
            shadow_layer.reparentTo(shadow_node)
            shadow_layer.setScale(self.get_scale_factor(shadow_layer) * scale)
            shadow_layer.setZ(z_offset * (1 - i / N))

        return shadow_node
Ejemplo n.º 13
0
 def __init__(self, path):
     self.path = panda_path(path)
     self.tree = ET.parse(path)
     self.root = self.tree.getroot()
Ejemplo n.º 14
0
#! /usr/bin/env python

import ast
import pooltool as pt
import configparser

from pooltool.utils import panda_path
from pooltool.error import ConfigError, TableConfigError

from pathlib import Path
from panda3d.core import *

loadPrcFile(
    panda_path(Path(pt.__file__).parent / 'config' / 'config_panda3d.prc'))

# This is hard-coded. Change it and everything looks bad
aspect_ratio = 1.6

menu_text_scale = 0.07
menu_text_scale_small = 0.04
zoom_sensitivity = 0.3
min_player_cam = 2
max_english = 6 / 10
power_sensitivity = 2
elevate_sensitivity = 13
english_sensitivity = 0.1
rotate_sensitivity_x = 19
rotate_sensitivity_y = 5
rotate_fine_sensitivity_x = 2
rotate_fine_sensitivity_y = 0
move_sensitivity = 0.6
Ejemplo n.º 15
0
    def add_dropdown(self, item):
        name = self.search_child_tag(item, 'name').text
        desc = self.search_child_tag(item, 'description').text

        if item.attrib.get('from_yaml'):
            # Populate the options from a YAML
            path = Path(
                pooltool.__file__).parent / item.attrib.get('from_yaml')
            config_obj = configparser.ConfigParser()
            config_obj.read(path)
            options = [option for option in config_obj.sections()]
        else:
            # Read the options directly from the XML
            options = [
                subitem.text for subitem in item if subitem.tag == 'option'
            ]

        try:
            func_name = self.search_child_tag(item, 'func').text
        except ValueError:
            func_name = None

        title = DirectLabel(
            text=name + ":",
            scale=AUX_TEXT_SCALE,
            parent=self.area.getCanvas(),
            relief=None,
            text_fg=TEXT_COLOR,
            text_align=TextNode.ALeft,
            text_font=self.title_font,
        )
        title.reparentTo(self.area.getCanvas())
        title_np = NodePath(title)
        title_np.reparentTo(self.area.getCanvas())

        dropdown = DirectOptionMenu(
            scale=BUTTON_TEXT_SCALE * 0.8,
            items=options,
            highlightColor=(0.65, 0.65, 0.65, 1),
            textMayChange=1,
            text_align=TextNode.ALeft,
            #text_font = self.button_font,
            relief=DGG.RIDGE,
            popupMarker_scale=0.6,
            popupMarker_image=loadImageAsPlane(
                panda_path(MENU_ASSETS / 'dropdown_marker.png')),
            popupMarker_relief=None,
            item_pad=(0.2, 0.2),
        )
        dropdown['frameColor'] = (1, 1, 1, 0.3)
        dropdown.reparentTo(self.area.getCanvas())

        dropdown_np = NodePath(dropdown)
        # functional_dropdown-<menu_name>-<dropdown_text>
        dropdown_id = f"functional_dropdown-{self.name}-{name.replace(' ','_')}"
        dropdown_np.setName(dropdown_id)
        dropdown_np.reparentTo(self.area.getCanvas())

        if self.last_element:
            autils.alignTo(title_np, self.last_element, autils.CT, autils.CB)
        else:
            title_np.setPos(-0.63, 0, 0.8)
        title_np.setX(-0.63)
        title_np.setZ(title_np.getZ() - MOVE)

        # Align the dropdown next to the title that refers to it
        autils.alignTo(dropdown_np, title_np, autils.CL, autils.CR)
        # Then shift it over just a bit to give some space
        dropdown_np.setX(dropdown_np.getX() + 0.02)
        # Then shift it down a little to align the text
        dropdown_np.setZ(dropdown_np.getZ() - 0.005)

        # This is the info button you hover over
        info_button = DirectButton(
            text='',
            text_align=TextNode.ALeft,
            scale=INFO_SCALE,
            image=panda_path(MENU_ASSETS / 'info_button.png'),
            relief=None,
        )

        # Bind mouse hover to displaying button info
        info_button.bind(DGG.ENTER, self.display_button_info, extraArgs=[desc])
        info_button.bind(DGG.EXIT, self.destroy_button_info)

        info_button = NodePath(info_button)
        info_button.reparentTo(self.area.getCanvas())

        # Align the info button next to the button it refers to
        autils.alignTo(info_button, title_np, autils.CR, autils.CL)
        # Then shift it over just a bit to give some space
        info_button.setX(info_button.getX() - 0.02)

        # Create a parent for all the nodes
        dropdown_id = 'dropdown_' + item.text.replace(' ', '_')
        dropdown_obj = self.area.getCanvas().attachNewNode(dropdown_id)
        title_np.reparentTo(dropdown_obj)
        dropdown_np.reparentTo(dropdown_obj)
        info_button.reparentTo(dropdown_obj)

        self.last_element = dropdown_np

        self.elements.append({
            'type': 'dropdown',
            'name': name,
            'content': dropdown_obj,
            'object': dropdown,
            'convert_factor': None,
            'func_name': func_name,
        })
Ejemplo n.º 16
0
    def add_checkbox(self, item):
        name = self.search_child_tag(item, 'name').text
        desc = self.search_child_tag(item, 'description').text

        title = DirectLabel(
            text=name + ":",
            scale=AUX_TEXT_SCALE,
            parent=self.area.getCanvas(),
            relief=None,
            text_fg=TEXT_COLOR,
            text_align=TextNode.ALeft,
            text_font=self.title_font,
        )
        title.reparentTo(self.area.getCanvas())
        title_np = NodePath(title)
        title_np.reparentTo(self.area.getCanvas())

        checkbox = DirectCheckButton(
            scale=BUTTON_TEXT_SCALE * 0.5,
            boxImage=(
                panda_path(MENU_ASSETS / 'unchecked.png'),
                panda_path(MENU_ASSETS / 'checked.png'),
                None,
            ),
            text="",
            relief=None,
            boxRelief=None,
        )

        checkbox_np = NodePath(checkbox)
        # functional_checkbox-<menu_name>-<checkbox_text>
        checkbox_id = f"functional_checkbox-{self.name}-{name.replace(' ','_')}"
        checkbox_np.setName(checkbox_id)
        checkbox_np.reparentTo(self.area.getCanvas())

        if self.last_element:
            autils.alignTo(title_np, self.last_element, autils.CT, autils.CB)
        else:
            title_np.setPos(-0.63, 0, 0.8)
        title_np.setX(-0.63)
        title_np.setZ(title_np.getZ() - MOVE)

        # Align the checkbox next to the title that refers to it
        autils.alignTo(checkbox_np, title_np, autils.CL, autils.CR)
        # Then shift it over just a bit to give some space
        checkbox_np.setX(checkbox_np.getX() + 0.02)
        # Then shift it down a little to align the text
        checkbox_np.setZ(checkbox_np.getZ() - 0.005)

        # This is the info button you hover over
        info_button = DirectButton(
            text='',
            text_align=TextNode.ALeft,
            scale=INFO_SCALE,
            image=panda_path(MENU_ASSETS / 'info_button.png'),
            relief=None,
        )

        # Bind mouse hover to displaying button info
        info_button.bind(DGG.ENTER, self.display_button_info, extraArgs=[desc])
        info_button.bind(DGG.EXIT, self.destroy_button_info)

        info_button = NodePath(info_button)
        info_button.reparentTo(self.area.getCanvas())

        # Align the info button next to the button it refers to
        autils.alignTo(info_button, title_np, autils.CR, autils.CL)
        # Then shift it over just a bit to give some space
        info_button.setX(info_button.getX() - 0.02)

        # Create a parent for all the nodes
        checkbox_id = 'checkbox_' + item.text.replace(' ', '_')
        checkbox_obj = self.area.getCanvas().attachNewNode(checkbox_id)
        title_np.reparentTo(checkbox_obj)
        checkbox_np.reparentTo(checkbox_obj)
        info_button.reparentTo(checkbox_obj)

        self.last_element = checkbox_np

        self.elements.append({
            'type': 'checkbox',
            'name': name,
            'content': checkbox_obj,
            'object': checkbox,
            'convert_factor': None,
        })
Ejemplo n.º 17
0
    def add_entry(self, item):
        name = self.search_child_tag(item, 'name').text
        desc = self.search_child_tag(item, 'description').text

        validator = item.attrib.get('validator')
        if validator is None:
            validator = lambda value: True
        else:
            try:
                validator = getattr(self, validator)
            except AttributeError:
                raise AttributeError(
                    f"Unknown validator string '{validator}' for element with name '{name}'"
                )

        try:
            initial = item.attrib['initial']
        except KeyError:
            initial = ''

        try:
            width = int(item.attrib['width'])
        except KeyError:
            width = 4

        title = DirectLabel(
            text=name + ":",
            scale=AUX_TEXT_SCALE,
            parent=self.area.getCanvas(),
            relief=None,
            text_fg=TEXT_COLOR,
            text_align=TextNode.ALeft,
            text_font=self.title_font,
        )
        title.reparentTo(self.area.getCanvas())
        title_np = NodePath(title)
        title_np.reparentTo(self.area.getCanvas())

        entry = DirectEntry(
            text="",
            scale=BUTTON_TEXT_SCALE * 0.7,
            initialText=initial,
            relief=DGG.RIDGE,
            numLines=1,
            width=width,
            focus=0,
            focusInCommand=self.entry_buildup,
            focusInExtraArgs=[True, name],
            focusOutCommand=self.entry_teardown,
            focusOutExtraArgs=[name, initial],
            suppressKeys=True,
        )
        entry['frameColor'] = (1, 1, 1, 0.3)

        # If the mouse hovers over a direct entry, update self.hovered_entry
        entry.bind(DGG.ENTER, self.update_hovered_entry, extraArgs=[name])
        entry.bind(DGG.EXIT, self.update_hovered_entry, extraArgs=[None])

        entry_np = NodePath(entry)
        # functional_entry-<menu_name>-<entry_text>
        entry_id = f"functional_entry-{self.name}-{name.replace(' ','_')}"
        entry_np.setName(entry_id)
        entry_np.reparentTo(self.area.getCanvas())

        if self.last_element:
            autils.alignTo(title_np, self.last_element, autils.CT, autils.CB)
        else:
            title_np.setPos(-0.63, 0, 0.8)
        title_np.setX(-0.63)
        title_np.setZ(title_np.getZ() - MOVE)

        # Align the entry next to the title that refers to it
        autils.alignTo(entry_np, title_np, autils.CL, autils.CR)
        # Then shift it over just a bit to give some space
        entry_np.setX(entry_np.getX() + 0.02)
        # Then shift it down a little to align the text
        entry_np.setZ(entry_np.getZ() - 0.005)

        # This is the info button you hover over
        info_button = DirectButton(
            text='',
            text_align=TextNode.ALeft,
            scale=INFO_SCALE,
            image=panda_path(MENU_ASSETS / 'info_button.png'),
            relief=None,
        )

        # Bind mouse hover to displaying button info
        info_button.bind(DGG.ENTER, self.display_button_info, extraArgs=[desc])
        info_button.bind(DGG.EXIT, self.destroy_button_info)

        info_button = NodePath(info_button)
        info_button.reparentTo(self.area.getCanvas())

        # Align the info button next to the button it refers to
        autils.alignTo(info_button, title_np, autils.CR, autils.CL)
        # Then shift it over just a bit to give some space
        info_button.setX(info_button.getX() - 0.02)

        # This text is shown if an error is detected in the user input
        error = DirectLabel(
            text="",
            textMayChange=1,
            text_fg=ERROR_COLOR,
            text_bg=(0, 0, 0, 0.3),
            scale=ERROR_TEXT_SCALE,
            parent=self.area.getCanvas(),
            relief=None,
            text_align=TextNode.ALeft,
        )
        error.reparentTo(self.area.getCanvas())
        error_np = NodePath(error)
        error_np.reparentTo(self.area.getCanvas())
        error_np.hide()

        # Align the error msg next to the entry it refers to
        autils.alignTo(error, entry_np, autils.CL, autils.CR)
        # Then shift it over just a bit to give some space
        error_np.setX(error_np.getX() + 0.02)
        # And shift it down a little too
        error_np.setZ(error_np.getZ() - 0.01)

        # Create a parent for all the nodes
        entry_id = 'entry_' + item.text.replace(' ', '_')
        entry_obj = self.area.getCanvas().attachNewNode(entry_id)
        title_np.reparentTo(entry_obj)
        entry_np.reparentTo(entry_obj)
        info_button.reparentTo(entry_obj)
        error_np.reparentTo(entry_obj)

        self.last_element = entry_np

        self.elements.append({
            'type': 'entry',
            'initial': initial,
            'name': name,
            'content': entry_obj,
            'object': entry,
            'error_msg': error,
            'validator': validator,
            'convert_factor': None,
        })
Ejemplo n.º 18
0
    def add_button(self, item):
        """Add a button"""

        name = self.search_child_tag(item, 'name').text
        func_name = self.search_child_tag(item, 'func').text
        desc = self.search_child_tag(item, 'description').text

        # This is the button you click. NOTE `command` is assigned ad hoc. See
        # Menus.populate_menus
        button = DirectButton(
            text=name,
            text_align=TextNode.ALeft,
            text_font=self.button_font,
            scale=BUTTON_TEXT_SCALE,
            geom=loadImageAsPlane(panda_path(MENU_ASSETS / 'button.png')),
            relief=None,
        )

        # Bind mouse hover to highlighting option
        button.bind(DGG.ENTER, self.highlight_button, extraArgs=[button])
        button.bind(DGG.EXIT, self.unhighlight_button)

        button_np = NodePath(button)
        # functional_button-<menu_name>-<button_text>
        button_id = f"functional_button-{self.name}-{name.replace(' ','_')}"
        button_np.setName(button_id)
        button_np.reparentTo(self.area.getCanvas())

        if self.last_element:
            autils.alignTo(button_np, self.last_element, autils.CT, autils.CB)
        else:
            button_np.setPos(-0.63, 0, 0.8)
        button_np.setX(-0.63)
        button_np.setZ(button_np.getZ() - MOVE)

        # This is the info button you hover over
        info_button = DirectButton(
            text='',
            text_align=TextNode.ALeft,
            scale=INFO_SCALE,
            image=panda_path(MENU_ASSETS / 'info_button.png'),
            relief=None,
        )

        # Bind mouse hover to displaying button info
        info_button.bind(DGG.ENTER, self.display_button_info, extraArgs=[desc])
        info_button.bind(DGG.EXIT, self.destroy_button_info)

        info_button = NodePath(info_button)
        info_button.reparentTo(self.area.getCanvas())

        # Align the info button next to the button it refers to
        autils.alignTo(info_button, button_np, autils.CR, autils.CL)
        # Then shift it over just a bit to give some space
        info_button.setX(info_button.getX() - 0.02)

        # Create a parent for all the nodes
        button_id = 'button_' + item.text.replace(' ', '_')
        button_obj = self.area.getCanvas().attachNewNode(button_id)
        button_np.reparentTo(button_obj)
        info_button.reparentTo(button_obj)

        self.last_element = button_np

        self.elements.append({
            'type': 'button',
            'name': name,
            'content': button_obj,
            'object': button,
            'convert_factor': None,
            'func_name': func_name,
        })

        return button_obj