Загрузка данных


Сделай демонстрацию на pyside6 холста с инструментами карандаш и уголь, где можно будет выбирать инструмент, можно регулировать высоту пишущей части (конуса), регулировать наклон инструмента к плоскости бумаги (от 0 до 100, где 0 - касается остриём, а 100 - полное касание гранью) и регулировать угол поворота инструмента от 0 до 360 градусов.

import sys
import math
from PySide6.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout,
                               QHBoxLayout, QSlider, QLabel, QRadioButton,
                               QButtonGroup, QGroupBox, QSizePolicy)
from PySide6.QtCore import Qt, QPointF, QRectF
from PySide6.QtGui import QPainter, QPen, QColor, QBrush, QPolygonF, QFont

class Canvas(QWidget):
    def __init__(self):
        super().__init__()
        self.setMinimumSize(600, 500)
        self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
        self.tool_type = 0          # 0 - карандаш, 1 - уголь
        self.cone_height = 40       # высота конуса (мм)
        self.tilt = 20              # наклон 0..100 (0 - остриё, 100 - грань)
        self.rotation = 0           # поворот 0..360 градусов

        # Параметры инструментов
        self.R_pencil = 5.0         # радиус карандаша
        self.L_cyl = 80.0           # длина цилиндрической части
        self.R_charcoal = 7.0       # радиус описанной окр. угля (для расчёта касания)
        self.a_charcoal = 10.0      # сторона квадратного сечения угля

        self.scale = 2.5            # пикселей на мм
        self.iso_z = 0.4            # коэффициент глубины для изометрии

    def set_params(self, tool_type, cone_height, tilt, rotation):
        self.tool_type = tool_type
        self.cone_height = cone_height
        self.tilt = tilt
        self.rotation = rotation
        self.update()

    def project(self, x, y, z):
        # изометрическая проекция: Xscr = x - z*iso_z, Yscr = -y + z*iso_z (ось Y экрана вниз)
        cx = self.width() / 2
        cy = self.height() / 2 + 50   # смещаем центр вниз, чтобы бумага была ниже
        sx = cx + (x - z * 0.5) * self.scale
        sy = cy - y * self.scale + z * 0.3 * self.scale
        return QPointF(sx, sy)

    def paintEvent(self, event):
        painter = QPainter(self)
        painter.setRenderHint(QPainter.Antialiasing)

        # Фон
        painter.fillRect(self.rect(), QColor(240, 240, 240))

        # Параметры инструмента
        if self.tool_type == 0:
            R = self.R_pencil
            L_cone = self.cone_height
            color = QColor(220, 180, 100)  # дерево
            tip_color = QColor(60, 60, 60) # грифель
        else:
            R = self.R_charcoal
            L_cone = self.cone_height
            color = QColor(40, 40, 40)
            tip_color = QColor(20, 20, 20)

        L_total = L_cone + self.L_cyl

        # Угол наклона β к плоскости: 0 -> 90° (вертикально), 100 -> 0° (лежит)
        tilt_norm = self.tilt / 100.0
        beta = (1.0 - tilt_norm) * math.pi / 2   # радианы, 0..π/2
        phi = math.radians(self.rotation)

        # Направление оси инструмента в мировых координатах
        Z_tool = (math.cos(beta) * math.cos(phi),
                   math.sin(beta),
                   math.cos(beta) * math.sin(phi))
        Z_tool = (Z_tool[0], Z_tool[1], Z_tool[2])

        # Вспомогательный вектор "вперёд" в горизонтальной плоскости
        if beta < 0.001:  # почти горизонтально
            u = (math.cos(phi), 0.0, math.sin(phi))
        else:
            h_len = math.cos(beta)
            u = (math.cos(phi), 0.0, math.sin(phi))

        # Ортонормированный базис
        Z = Z_tool
        # Y_tool = Z × u (нормированный)
        Y_tool = (Z[1]*u[2] - Z[2]*u[1],
                   Z[2]*u[0] - Z[0]*u[2],
                   Z[0]*u[1] - Z[1]*u[0])
        norm_Y = math.hypot(*Y_tool)
        if norm_Y < 1e-6:
            Y_tool = (0.0, 0.0, 1.0)
            norm_Y = 1.0
        Y_tool = (Y_tool[0]/norm_Y, Y_tool[1]/norm_Y, Y_tool[2]/norm_Y)
        X_tool = (Y_tool[1]*Z[2] - Y_tool[2]*Z[1],
                   Y_tool[2]*Z[0] - Y_tool[0]*Z[2],
                   Y_tool[0]*Z[1] - Y_tool[1]*Z[0])

        # Вычисляем смещение O_y, чтобы нижняя точка инструмента касалась Y=0
        # минимум по z: 0 (остриё) или L_cone (основание конуса)
        val_tip = 0.0
        val_base = L_cone * math.sin(beta) - R * math.cos(beta)
        min_y = min(val_tip, val_base)
        O_y = -min_y   # поднимаем начало координат

        # Мировое смещение острия (центр на бумаге X=0, Z=0)
        O = (0.0, O_y, 0.0)

        # ----- Рисуем плоскость бумаги -----
        paper_size = 100
        paper_points = [(-paper_size, 0, -paper_size*0.6),
                        ( paper_size, 0, -paper_size*0.6),
                        ( paper_size, 0,  paper_size*0.8),
                        (-paper_size, 0,  paper_size*0.8)]
        poly = QPolygonF()
        for p in paper_points:
            poly.append(self.project(p[0], p[1], p[2]))
        painter.setBrush(QColor(255, 255, 255))
        painter.setPen(QPen(QColor(200, 200, 200), 1))
        painter.drawPolygon(poly)

        # ----- Функция перевода локальных координат в мировые -----
        def local_to_world(lx, ly, lz):
            wx = O[0] + lx * X_tool[0] + ly * Y_tool[0] + lz * Z_tool[0]
            wy = O[1] + lx * X_tool[1] + ly * Y_tool[1] + lz * Z_tool[1]
            wz = O[2] + lx * X_tool[2] + ly * Y_tool[2] + lz * Z_tool[2]
            return (wx, wy, wz)

        # ----- Генерация каркаса инструмента -----
        edges_3d = []   # список ребер: (p1_world, p2_world)
        edges_shadow = []

        if self.tool_type == 0:   # Карандаш (цилиндр + конус)
            N = 24  # число граней
            # точки на окружности основания конуса (z = L_cone)
            base_pts_local = []
            apex_local = (0.0, 0.0, 0.0)
            for i in range(N):
                angle = 2 * math.pi * i / N
                xl = R * math.cos(angle)
                yl = R * math.sin(angle)
                base_pts_local.append((xl, yl, L_cone))
            # точки на верхнем торце цилиндра (z = L_total)
            top_pts_local = []
            for i in range(N):
                angle = 2 * math.pi * i / N
                xl = R * math.cos(angle)
                yl = R * math.sin(angle)
                top_pts_local.append((xl, yl, L_total))

            # Ребра конуса (от острия к основанию)
            for pt in base_pts_local:
                p1 = local_to_world(*apex_local)
                p2 = local_to_world(*pt)
                edges_3d.append((p1, p2))
            # Ребра цилиндра
            for i in range(N):
                p1 = local_to_world(*base_pts_local[i])
                p2 = local_to_world(*top_pts_local[i])
                edges_3d.append((p1, p2))
                # поперечные ребра оснований
                p1_base = local_to_world(*base_pts_local[i])
                p2_base = local_to_world(*base_pts_local[(i+1)%N])
                edges_3d.append((p1_base, p2_base))
                p1_top = local_to_world(*top_pts_local[i])
                p2_top = local_to_world(*top_pts_local[(i+1)%N])
                edges_3d.append((p1_top, p2_top))

            # Для тени проецируем точки на Y=0
            def shadow(pt):
                return (pt[0], 0.0, pt[2])
            apex_sh = shadow(local_to_world(*apex_local))
            base_sh = [shadow(local_to_world(*pt)) for pt in base_pts_local]
            top_sh = [shadow(local_to_world(*pt)) for pt in top_pts_local]
            # Теневые ребра
            for pt in base_sh:
                edges_shadow.append((apex_sh, pt))
            for i in range(N):
                edges_shadow.append((base_sh[i], top_sh[i]))
                edges_shadow.append((base_sh[i], base_sh[(i+1)%N]))
                edges_shadow.append((top_sh[i], top_sh[(i+1)%N]))

        else:   # Уголь (призма с квадратным сечением + пирамида)
            a = self.a_charcoal
            half = a / 2.0
            # вершины квадрата в локальной плоскости XY (z = const)
            square = [(-half, -half), ( half, -half), ( half,  half), (-half,  half)]
            base_pts_local = [(x, y, L_cone) for x, y in square]
            top_pts_local = [(x, y, L_total) for x, y in square]
            apex_local = (0.0, 0.0, 0.0)

            # Рёбра пирамиды (остриё -> основание)
            for pt in base_pts_local:
                edges_3d.append((local_to_world(*apex_local), local_to_world(*pt)))
            # Рёбра призмы
            for i in range(4):
                p1 = local_to_world(*base_pts_local[i])
                p2 = local_to_world(*top_pts_local[i])
                edges_3d.append((p1, p2))
            # Основания
            for i in range(4):
                edges_3d.append((local_to_world(*base_pts_local[i]),
                                 local_to_world(*base_pts_local[(i+1)%4])))
                edges_3d.append((local_to_world(*top_pts_local[i]),
                                 local_to_world(*top_pts_local[(i+1)%4])))

            def shadow(pt):
                return (pt[0], 0.0, pt[2])
            apex_sh = shadow(local_to_world(*apex_local))
            base_sh = [shadow(local_to_world(*pt)) for pt in base_pts_local]
            top_sh = [shadow(local_to_world(*pt)) for pt in top_pts_local]
            for pt in base_sh:
                edges_shadow.append((apex_sh, pt))
            for i in range(4):
                edges_shadow.append((base_sh[i], top_sh[i]))
                edges_shadow.append((base_sh[i], base_sh[(i+1)%4]))
                edges_shadow.append((top_sh[i], top_sh[(i+1)%4]))

        # ----- Рисуем тень (пятно касания) -----
        pen_shadow = QPen(QColor(180, 180, 180), 1.5, Qt.DashLine)
        painter.setPen(pen_shadow)
        for p1, p2 in edges_shadow:
            painter.drawLine(self.project(*p1), self.project(*p2))

        # ----- Рисуем 3D каркас инструмента -----
        pen_3d = QPen(color.darker(120), 2.0)
        painter.setPen(pen_3d)
        for p1, p2 in edges_3d:
            painter.drawLine(self.project(*p1), self.project(*p2))

        # Подпись
        painter.setPen(Qt.black)
        painter.setFont(QFont("Arial", 10))
        painter.drawText(10, 20, f"Инструмент: {'Карандаш' if self.tool_type==0 else 'Уголь'} | "
                                  f"Конус: {self.cone_height:.0f} мм | "
                                  f"Наклон: {self.tilt} | "
                                  f"Поворот: {self.rotation}°")

class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("Демонстрация карандаша и угля")
        self.setGeometry(100, 100, 1100, 700)

        central = QWidget()
        self.setCentralWidget(central)
        main_layout = QHBoxLayout(central)

        # Холст
        self.canvas = Canvas()
        main_layout.addWidget(self.canvas, 1)

        # Панель управления
        panel = QWidget()
        panel.setFixedWidth(250)
        layout = QVBoxLayout(panel)

        # Выбор инструмента
        tool_group = QGroupBox("Инструмент")
        tool_layout = QVBoxLayout()
        self.radio_pencil = QRadioButton("Карандаш")
        self.radio_charcoal = QRadioButton("Уголь")
        self.radio_pencil.setChecked(True)
        tool_layout.addWidget(self.radio_pencil)
        tool_layout.addWidget(self.radio_charcoal)
        tool_group.setLayout(tool_layout)
        layout.addWidget(tool_group)

        # Группа радио
        self.tool_btn_group = QButtonGroup()
        self.tool_btn_group.addButton(self.radio_pencil, 0)
        self.tool_btn_group.addButton(self.radio_charcoal, 1)

        # Высота конуса
        layout.addWidget(QLabel("Высота конуса (мм):"))
        self.slider_cone = QSlider(Qt.Horizontal)
        self.slider_cone.setRange(10, 80)
        self.slider_cone.setValue(40)
        self.label_cone_val = QLabel("40")
        layout.addWidget(self.slider_cone)
        layout.addWidget(self.label_cone_val)

        # Наклон (0-100)
        layout.addWidget(QLabel("Наклон (0 - остриё, 100 - грань):"))
        self.slider_tilt = QSlider(Qt.Horizontal)
        self.slider_tilt.setRange(0, 100)
        self.slider_tilt.setValue(20)
        self.label_tilt_val = QLabel("20")
        layout.addWidget(self.slider_tilt)
        layout.addWidget(self.label_tilt_val)

        # Поворот (0-360)
        layout.addWidget(QLabel("Поворот (0-360°):"))
        self.slider_rot = QSlider(Qt.Horizontal)
        self.slider_rot.setRange(0, 360)
        self.slider_rot.setValue(0)
        self.label_rot_val = QLabel("0°")
        layout.addWidget(self.slider_rot)
        layout.addWidget(self.label_rot_val)

        layout.addStretch()
        main_layout.addWidget(panel)

        # Сигналы
        self.tool_btn_group.idClicked.connect(self.update_canvas)
        self.slider_cone.valueChanged.connect(self.on_cone_changed)
        self.slider_tilt.valueChanged.connect(self.on_tilt_changed)
        self.slider_rot.valueChanged.connect(self.on_rot_changed)

        self.update_canvas()

    def update_canvas(self):
        tool = self.tool_btn_group.checkedId()
        cone = self.slider_cone.value()
        tilt = self.slider_tilt.value()
        rot = self.slider_rot.value()
        self.canvas.set_params(tool, cone, tilt, rot)

    def on_cone_changed(self, val):
        self.label_cone_val.setText(str(val))
        self.update_canvas()

    def on_tilt_changed(self, val):
        self.label_tilt_val.setText(str(val))
        self.update_canvas()

    def on_rot_changed(self, val):
        self.label_rot_val.setText(f"{val}°")
        self.update_canvas()

if __name__ == "__main__":
    app = QApplication(sys.argv)
    window = MainWindow()
    window.show()
    sys.exit(app.exec())