Skip to content

visualdynamics.gui.mesh_panel

mesh_panel

The pane beside the 3-D view where the solids are typed.

A solid is described by its center and its size, and its preview in the view is turned and slid by the same gizmo that turns a coordinate system. So the pane holds a center and the solid's dimensions in the display unit, and the window holds the frame the solid is drawn in: the rings turn it about its center, and the arrows slide the center along the frame's own axes onto the grid. The rotation is also three fields, degrees about the geometry's X, Y and Z applied in that order, for an angle the rings' whole degrees do not reach. The rings write the angles back, so the fields and the view always agree. Square Up turns the solid back to the geometry's axes and keeps its center.

The pane holds no rule of its own. The window turns the fields and its frame into the numbers the project's add_solid_* verbs take, asks the core what they would make, and draws it. Add runs the project verb, so the journal line is the call a script makes. A solid is a shape that is meshed later (core.solids), so it has a name and no element size.

  • A block is a center, three widths, three angles and a corner radius, which rounds its four edges along its own Z.
  • A cylinder is a center, a radius, a length along the frame's Z, and three angles.
  • A section is a shape (I-beam, channel, tee, angle, rectangular tube or round tube), its dimensions, named as core.solids.SECTIONS names them, and a length along the frame's Z.
  • A hole is a diameter, a depth or Through, and a counterbore's diameter and depth when one is asked for. Its center is where it enters, and it runs down the frame's Z.
  • A plate is a center, two widths in the frame's X-Y plane and three angles, and a disk a center, a radius and three angles. Neither has a thickness: they are meshed into shells, whose element group takes it.

A solid already made can be opened in the pane to change it, with Apply in place of Add. A boolean has no size of its own, so its pane (Move Part) only moves and turns it.

Classes:

Name Description
MeshPanel

The fields for one solid, and its Add (or Apply) button.

Classes

MeshPanel

MeshPanel(parent: QWidget | None = None)

Bases: QWidget

The fields for one solid, and its Add (or Apply) button.

changed is emitted when a field is edited; the window then reads values and redraws the preview. add_asked, close_asked and square_asked are the Add, Close and Square Up buttons.

Methods:

Name Description
open_for

Set the pane up for kind, with every field on its default.

values

The fields as typed, in the display unit, as a dict: 'center',

set_values

Fill fields by name, in the display unit: center=(x, y, z),

show_reading

Show what Add would make, as the window reads it from the core,

Source code in src/visualdynamics/gui/mesh_panel.py
def __init__(self, parent: QWidget | None = None) -> None:
    super().__init__(parent)
    self.kind: str = 'solid_block'
    self.editing: bool = False
    self.unit: str = ''
    self.title: QLabel = QLabel(TITLES['solid_block'])
    grid = panel_grid(self, self.title)
    # the solid's name: an editable box once offered the geometry's
    # element groups, while the pane also added meshed blocks
    self.group: QComboBox = QComboBox()
    self.group.setEditable(True)
    self.group.setToolTip('What to call it; a part meshed from it '
                          'becomes an element group of this name')
    self.group_label: QLabel = QLabel('Name')
    grid.addWidget(self.group_label, 1, 0)
    grid.addWidget(self.group, 1, 1)
    self.center_label: QLabel = QLabel('Center')
    self.center_label.setEnabled(False)
    grid.addWidget(self.center_label, 2, 0, 1, 2)
    self.center: list[DoubleSpinBox] = []
    for row, axis in enumerate('XYZ', start=3):
        box = _box(0.0)
        box.setToolTip(f'{axis} of the center, in the display unit; the '
                       'arrows in the view slide it onto the grid')
        grid.addWidget(QLabel(axis), row, 0)
        grid.addWidget(box, row, 1)
        self.center.append(box)
    self.width_label: QLabel = QLabel('Width')
    self.width_label.setEnabled(False)
    grid.addWidget(self.width_label, 6, 0, 1, 2)
    self.widths: list[DoubleSpinBox] = []
    self.width_axes: list[QLabel] = []
    for row, axis in enumerate('XYZ', start=7):
        box = _box(0.0, minimum=0.0)
        box.setToolTip(f'The width along the frame\'s {axis}')
        label = QLabel(axis)
        grid.addWidget(label, row, 0)
        grid.addWidget(box, row, 1)
        self.widths.append(box)
        self.width_axes.append(label)
    # a cylinder's two numbers, in the widths' rows: one set or the
    # other is shown, never both
    self.radius_label: QLabel = QLabel('Radius')
    self.radius: DoubleSpinBox = _box(0.25, minimum=0.0)
    self.radius.setToolTip("Round the frame's Z axis")
    self.length_label: QLabel = QLabel('Length')
    self.length: DoubleSpinBox = _box(1.0, minimum=0.0)
    self.length.setToolTip("Along the frame's Z axis, centered")
    grid.addWidget(self.radius_label, 7, 0)
    grid.addWidget(self.radius, 7, 1)
    # the length in the last of the rows a section's dimensions take,
    # so it is a cylinder's second number and a section's last
    grid.addWidget(self.length_label, 12, 0)
    grid.addWidget(self.length, 12, 1)
    # a cone's radius at its top, in a row only a section's or a
    # hole's other numbers take, never shown with them (2026-10-20)
    self.top_label: QLabel = QLabel('Top radius')
    self.top: DoubleSpinBox = _box(CONE_TOP, minimum=0.0)
    self.top.setToolTip("At the frame's +Z end; zero for a point")
    grid.addWidget(self.top_label, 8, 0)
    grid.addWidget(self.top, 8, 1)
    # a section: its shape in the width label's row, its dimensions
    # in the widths' rows and below
    self.shape_label: QLabel = QLabel('Shape')
    self.shape: QComboBox = QComboBox()
    self.shape.addItems(list(SECTIONS))
    self.shape.setToolTip('The section run along the frame\'s Z')
    grid.addWidget(self.shape_label, 6, 0)
    grid.addWidget(self.shape, 6, 1)
    self.section_labels: list[QLabel] = []
    self.section: list[DoubleSpinBox] = []
    for row in range(7, 11):
        label, box = QLabel(), _box(0.0, minimum=0.0)
        # named for its dimension once a shape is picked (_shape_rows)
        box.setToolTip('A dimension of the section, in the display unit')
        grid.addWidget(label, row, 0)
        grid.addWidget(box, row, 1)
        self.section_labels.append(label)
        self.section.append(box)
    # a hole: its diameter, Through or a depth, and a counterbore
    # when one is asked for
    self.hole_labels: list[QLabel] = []
    self.hole: list[DoubleSpinBox] = []
    for row in (7, 9, 11, 12):
        label, box = QLabel(), _box(0.0, minimum=0.0)
        grid.addWidget(label, row, 0)
        grid.addWidget(box, row, 1)
        self.hole_labels.append(label)
        self.hole.append(box)
    self.hole[0].setToolTip('Across the hole')
    self.hole[1].setToolTip('From where it enters, down the frame\'s Z')
    self.hole[2].setToolTip('Across the counterbore at the entry; '
                            'zero for none')
    self.hole[3].setToolTip('How deep the counterbore goes from the '
                            'entry')
    self.through: QCheckBox = QCheckBox('Through')
    self.through.setToolTip('Through the part it is cut from, however '
                            'thick that part is made')
    grid.addWidget(self.through, 8, 0, 1, 2)
    self.counterbore: QCheckBox = QCheckBox('Counterbore')
    self.counterbore.setToolTip('Wider at the entry, for a screw\'s head')
    grid.addWidget(self.counterbore, 10, 0, 1, 2)
    self.angle_label: QLabel = QLabel('Rotation [\u00b0]')
    self.angle_label.setEnabled(False)
    grid.addWidget(self.angle_label, 13, 0, 1, 2)
    self.angles: list[DoubleSpinBox] = []
    self.angle_axes: list[QLabel] = []
    for row, axis in enumerate('XYZ', start=14):
        box = _box(0.0, minimum=-360.0)
        box.setMaximum(360.0)
        box.setDecimals(2)
        box.setToolTip(f'Degrees about the geometry\'s {axis} axis, '
                       'applied X, then Y, then Z; the rings in the view '
                       'turn it too, a degree at a time')
        axis_label = QLabel(axis)
        self.angle_axes.append(axis_label)
        grid.addWidget(axis_label, row, 0)
        grid.addWidget(box, row, 1)
        self.angles.append(box)
    # a block's corner radius, the one kind with corners to round
    self.corner_label: QLabel = QLabel('Corner radius')
    self.corner: DoubleSpinBox = _box(0.0, minimum=0.0)
    self.corner.setToolTip("Round the block's four edges along its own "
                           'Z: a window cut with them leaves fillets')
    grid.addWidget(self.corner_label, 17, 0)
    grid.addWidget(self.corner, 17, 1)
    self.square_button: QPushButton = QPushButton('Square Up')
    self.square_button.setToolTip(
        "Turn the solid back to the geometry's axes, keeping its center")
    grid.addWidget(self.square_button, 18, 0, 1, 2)
    self.reading_label: QLabel = QLabel()
    self.reading_label.setWordWrap(True)
    grid.addWidget(self.reading_label, 20, 0, 1, 2)
    buttons = QHBoxLayout()
    self.add_button: QPushButton = QPushButton('Add')
    self.add_button.setToolTip('Add what the pane describes to the '
                               'geometry, as the preview shows it')
    self.add_button.setDefault(True)
    self.close_button: QPushButton = QPushButton('Close')
    self.close_button.setToolTip('Put the pane away without adding')
    buttons.addWidget(self.add_button)
    buttons.addWidget(self.close_button)
    grid.addLayout(buttons, 21, 0, 1, 2)
    grid.setRowStretch(22, 1)
    commit_on_enter(*self.center, *self.widths, *self.angles,
                    self.radius, self.length, self.corner, self.top, *self.section,
                    *self.hole)
    for box in (*self.center, *self.widths, *self.angles,
                self.radius, self.length, self.corner, self.top, *self.section,
                *self.hole):
        box.valueChanged.connect(lambda _value: self.changed.emit())
    self.shape.currentTextChanged.connect(self._shape_picked)
    self.through.toggled.connect(self._hole_toggled)
    self.counterbore.toggled.connect(self._hole_toggled)
    self.group.currentTextChanged.connect(lambda _text: self.changed.emit())
    self.add_button.clicked.connect(self.add_asked.emit)
    self.close_button.clicked.connect(self.close_asked.emit)
    self.square_button.clicked.connect(self.square_asked.emit)
Methods:
open_for
open_for(kind: str, unit: str, editing: bool = False) -> None

Set the pane up for kind, with every field on its default.

kind is 'solid_block', 'solid_cylinder', 'solid_cone', 'solid_sphere', 'solid_section', 'solid_hole', 'solid_plate' or 'solid_disk', or 'solid_part' for a boolean, which is only edited. unit is the display unit the fields are in. The name field opens empty, and only the fields the kind takes are shown. editing opens the pane on a solid already made, to change it, with Apply in place of Add; a section's shape cannot be changed then.

Source code in src/visualdynamics/gui/mesh_panel.py
def open_for(self, kind: str, unit: str, editing: bool = False) -> None:
    """Set the pane up for `kind`, with every field on its default.

    `kind` is 'solid_block', 'solid_cylinder', 'solid_cone',
    'solid_sphere', 'solid_section', 'solid_hole', 'solid_plate' or
    'solid_disk', or 'solid_part' for a boolean, which is only
    edited. `unit` is the display unit the fields are in. The name
    field opens empty, and only the fields the kind takes are shown.
    `editing` opens the pane on a solid already made, to change it,
    with Apply in place of Add; a section's shape cannot be changed
    then.
    """
    self.kind = kind
    self.editing = editing
    self.unit = unit
    cylinder = kind == 'solid_cylinder'
    part = kind == 'solid_part'
    section, hole = kind == 'solid_section', kind == 'solid_hole'
    cone, sphere = kind == 'solid_cone', kind == 'solid_sphere'
    plate, disk = kind == 'solid_plate', kind == 'solid_disk'
    self.title.setText((EDIT_TITLES if editing else TITLES)[kind])
    self.add_button.setText('Apply' if editing else 'Add')
    self.center_label.setText(f'Center [{unit}]')
    self.width_label.setText(f'Width [{unit}]')
    self.radius_label.setText(f'{"Base radius" if cone else "Radius"} [{unit}]')
    self.length_label.setText(f'{"Height" if cone else "Length"} [{unit}]')
    self.top_label.setText(f'Top radius [{unit}]')
    for widget in (self.width_label, *self.widths, *self.width_axes):
        widget.setVisible(not (cylinder or part or section or hole or cone or sphere
                               or disk))
    # a plate is flat in the frame's X-Y plane: no width along Z
    for widget in (self.widths[2], self.width_axes[2]):
        widget.setVisible(not (cylinder or part or section or hole or cone or sphere
                               or disk or plate))
    for widget in (self.radius_label, self.radius):
        widget.setVisible(cylinder or cone or sphere or disk)
    for widget in (self.length_label, self.length):
        widget.setVisible(cylinder or section or cone)
    for widget in (self.top_label, self.top):
        widget.setVisible(cone)
    # a sphere turned is the same sphere (principle 3)
    for widget in (self.angle_label, *self.angle_axes, *self.angles):
        widget.setVisible(not sphere)
    # a section's shape is what it is: picked when it is added
    for widget in (self.shape_label, self.shape):
        widget.setVisible(section and not editing)
    for widget in (*self.section_labels, *self.section):
        widget.setVisible(False)
    for widget in (*self.hole_labels, *self.hole, self.through,
                   self.counterbore):
        widget.setVisible(False)
    for widget in (self.corner_label, self.corner):
        widget.setVisible(kind == 'solid_block')
    self.corner_label.setText(f'Corner radius [{unit}]')
    self.group.blockSignals(True)
    self.group.setCurrentText('')
    self.group.blockSignals(False)
    defaults = DEFAULTS[kind]
    self.set_values(center=defaults['center'], widths=defaults['widths'],
                    angles=(0.0, 0.0, 0.0),
                    radius=0.5 if (cone or sphere or disk) else 0.25,
                    length=SECTION_LENGTH if section else 1.0,
                    corner=0.0, top=CONE_TOP, quiet=True)
    if section:
        self.set_values(shape='I-beam', quiet=True)
    if hole:
        for label, named in zip(self.hole_labels, HOLE_DIMENSIONS):
            label.setText(f'{named[:1].upper()}{named[1:]} [{unit}]')
        self.set_values(dimensions=(HOLE_DEFAULTS[0], 0.0, 0.0, 0.0), quiet=True)
        for box, value in zip(self.hole[1:], HOLE_DEFAULTS[1:]):
            box.blockSignals(True)
            box.setValue(value)
            box.blockSignals(False)
values
values() -> dict

The fields as typed, in the display unit, as a dict: 'center', 'widths', 'shape', 'dimensions', 'radius', 'length', 'corner', 'top', 'angles' in degrees and 'group', the solid's name.

A cone's 'radius' is at its base, 'top' at its top and 'length' its height. A section's 'dimensions' are its shape's, in the order SECTIONS gives them. A hole's are its diameter, its depth (zero when it goes through) and its counterbore's diameter and depth (zeros when it has none), as edit_solid takes them. For a cylinder, a section and a hole, 'widths' is the size of the box around the solid, which the gizmo is sized by.

Source code in src/visualdynamics/gui/mesh_panel.py
def values(self) -> dict:
    """The fields as typed, in the display unit, as a dict: 'center',
    'widths', 'shape', 'dimensions', 'radius', 'length', 'corner',
    'top', 'angles' in degrees and 'group', the solid's name.

    A cone's 'radius' is at its base, 'top' at its top and 'length'
    its height. A section's 'dimensions' are its shape's, in the order
    `SECTIONS` gives them. A hole's are its diameter, its depth (zero when it
    goes through) and its counterbore's diameter and depth (zeros
    when it has none), as `edit_solid` takes them. For a cylinder, a
    section and a hole, 'widths' is the size of the box around the
    solid, which the gizmo is sized by.
    """
    shape = self.shape.currentText()
    section = tuple(box.value() for box in self.section[:len(SECTIONS[shape])])
    through, bored = self.through.isChecked(), self.counterbore.isChecked()
    hole = (self.hole[0].value(), 0.0 if through else self.hole[1].value(),
            self.hole[2].value() if bored else 0.0,
            self.hole[3].value() if bored else 0.0)
    # what the gizmo sizes itself by: a cylinder's box, two diameters
    # and its length, a section's, a hole's as wide as its widest
    if self.kind == 'solid_cylinder':
        widths = (2 * self.radius.value(), 2 * self.radius.value(),
                  self.length.value())
    elif self.kind == 'solid_cone':
        wide = 2 * max(self.radius.value(), self.top.value())
        widths = (wide, wide, self.length.value())
    elif self.kind == 'solid_sphere':
        widths = (2 * self.radius.value(),) * 3
    elif self.kind == 'solid_disk':
        widths = (2 * self.radius.value(), 2 * self.radius.value(), 0.0)
    elif self.kind == 'solid_plate':
        widths = (self.widths[0].value(), self.widths[1].value(), 0.0)
    elif self.kind == 'solid_section':
        across = {'round tube': (section[0], section[0]),
                  'angle': section[:2], 'rectangular tube': section[:2]}.get(
            shape, (section[1], section[0]))
        widths = (*across, self.length.value())
    elif self.kind == 'solid_hole':
        wide = max(hole[0], hole[2])
        widths = (wide, wide, hole[1] or hole[0])
    else:
        widths = tuple(box.value() for box in self.widths)
    return {'center': tuple(box.value() for box in self.center),
            'widths': widths, 'shape': shape,
            'dimensions': hole if self.kind == 'solid_hole' else section,
            'radius': self.radius.value(), 'length': self.length.value(),
            'corner': self.corner.value(), 'top': self.top.value(),
            'angles': tuple(box.value() for box in self.angles),
            'group': self.group.currentText().strip()}
set_values
set_values(quiet: bool = False, **values) -> None

Fill fields by name, in the display unit: center=(x, y, z), widths=(x, y, z), angles=(x, y, z) in degrees, radius, length, corner, top, shape, dimensions and group. A hole's dimensions are given as values gives them. Picking a shape opens its dimensions on that shape's defaults, except when editing.

Tests type with this, and the gizmo writes the center and the angles back with it. With quiet the fields are filled without emitting changed.

Source code in src/visualdynamics/gui/mesh_panel.py
def set_values(self, quiet: bool = False, **values) -> None:
    """Fill fields by name, in the display unit: center=(x, y, z),
    widths=(x, y, z), angles=(x, y, z) in degrees, radius, length,
    corner, top, shape, dimensions and group. A hole's
    dimensions are given as `values` gives them. Picking a shape
    opens its dimensions on that shape's defaults, except when
    editing.

    Tests type with this, and the gizmo writes the center and the
    angles back with it. With `quiet` the fields are filled without
    emitting `changed`.
    """
    boxes = [*self.center, *self.widths, *self.angles,
             self.radius, self.length, self.corner, self.top, *self.section, *self.hole]
    if quiet:
        for box in boxes:
            box.blockSignals(True)
        self.group.blockSignals(True)
    try:
        for key, value in values.items():
            if key == 'shape':
                self.shape.blockSignals(True)
                self.shape.setCurrentText(value)
                self.shape.blockSignals(False)
                if not self.editing:
                    for box, number in zip(self.section, SECTION_DEFAULTS[value]):
                        box.setValue(number)
                self._shape_rows()
            elif key == 'dimensions' and self.kind == 'solid_hole':
                diameter, depth, wide, deep = value
                self.hole[0].setValue(diameter)
                if depth:
                    self.hole[1].setValue(depth)
                if wide:
                    self.hole[2].setValue(wide)
                    self.hole[3].setValue(deep)
                for check, on in ((self.through, not depth),
                                  (self.counterbore, bool(wide))):
                    check.blockSignals(True)
                    check.setChecked(on)
                    check.blockSignals(False)
                self._hole_rows()
            elif key == 'dimensions':
                for box, number in zip(self.section, value):
                    box.setValue(number)
            elif key == 'group':
                self.group.setCurrentText(value)
            elif key in ('radius', 'length', 'corner', 'top'):
                getattr(self, key).setValue(value)
            else:
                for box, number in zip(getattr(self, key), value):
                    box.setValue(number)
    finally:
        if quiet:
            for box in boxes:
                box.blockSignals(False)
            self.group.blockSignals(False)
    if not quiet:
        self.changed.emit()
show_reading
show_reading(text: str, ok: bool) -> None

Show what Add would make, as the window reads it from the core, and enable Add only when ok.

Source code in src/visualdynamics/gui/mesh_panel.py
def show_reading(self, text: str, ok: bool) -> None:
    """Show what Add would make, as the window reads it from the core,
    and enable Add only when `ok`."""
    self.reading_label.setText(text)
    self.add_button.setEnabled(ok)

Functions: