Загрузка данных
Сделай демонстрацию на 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())