雑感等

音楽,数学,語学,その他に関するメモを記す.

Python+PyQt5で4次元図形の描画

HSP(Hot Soup Processor)で4次元図形を回転させて描画 - 雑感等

これの二番煎じだけどクラスの設計はMVCを意識したつもり.

4次元図形の回転は上の記事と同様

MVCについて参考になったサイト

動作画面

一番上の行にある3つのスライダーは,4D->3Dに射影するカメラの角度.

その下の行にある2つのスライダーは,3D->2Dに射影するカメラの角度.

その下には描画された図形.この描画範囲内でホイールをコロコロすると,図形が拡大縮小する.

f:id:kazmus:20210526102212p:plain

pythonソース

from PyQt5.QtWidgets import QWidget, QApplication, QSlider, QGraphicsView, QGraphicsScene, QVBoxLayout, QHBoxLayout, \
    QGraphicsItem
from PyQt5.QtGui import QPen
from PyQt5.QtCore import Qt, QRectF
import sys
import numpy as np


class Model(object):
    def __init__(self):
        self.lines = None
        self._init_lines()
        pass

    def _init_lines(self):
        """
        線(点座標の対)を初期化
        点座標は4次元
        :return:
        """
        self.angle4d = [0, 0, 0]
        self.angle3d = [0, 0]
        p_o = np.array([0, 0, 0, 0])
        p_x = np.array([1, 0, 0, 0])
        p_y = np.array([0, 1, 0, 0])
        p_z = np.array([0, 0, 1, 0])
        p_w = np.array([0, 0, 0, 1])
        p_any = np.array([1, 1, 1, 1])
        p_any = p_any / np.linalg.norm(p_any)
        self.lines = np.stack([
            np.vstack([p_o, p_x]),
            np.vstack([p_o, p_y]),
            np.vstack([p_o, p_z]),
            np.vstack([p_o, p_w]),
            np.vstack([p_o, p_any])
        ])
        pass

    def set_t(self, val):
        self.angle4d[0] = val
        pass

    def set_u(self, val):
        self.angle4d[1] = val
        pass

    def set_v(self, val):
        self.angle4d[2] = val
        pass

    def set_t3(self, val):
        self.angle3d[0] = val
        pass

    def set_u3(self, val):
        self.angle3d[1] = val
        pass

    def _calc_projection4d(self, point_4, angle4d_cam, angle3d_cam):
        """
        投影を計算
        :param point_4:
        :param angle:
        :return:
        """
        t, u, v = angle4d_cam
        t3, u3 = angle3d_cam

        st = np.sin(t)
        ct = np.cos(t)
        su = np.sin(u)
        cu = np.cos(u)
        sv = np.sin(v)
        cv = np.cos(v)

        st3 = np.sin(t3)
        ct3 = np.cos(t3)
        su3 = np.sin(u3)
        cu3 = np.cos(u3)

        proj_4to3 = np.array([
            [-st, ct, 0, 0],
            [-ct * su, -st * su, cu, 0],
            [-ct * cu * sv, -st * cu * sv, -su * sv, cv]
        ])
        proj_3to2 = np.array([
            [-st3, ct3, 0],
            [-ct3 * su3, -st3 * su3, cu3],
        ])
        return proj_3to2 @ proj_4to3 @ point_4
        pass

    def projection(self):
        """
        投影された座標データを返す
        :return:
        """
        angle4d = np.array(self.angle4d)
        angle3d = np.array(self.angle3d)
        # ↓でやりたいことは,行列の第2方向(第0,第1に次ぐ第2方向)の各要素(4要素のベクトル)に対して,_calc_projection4dを適用したい
        return np.apply_along_axis(lambda x: self._calc_projection4d(x, angle4d, angle3d), 2, self.lines)
        pass

    pass


class Plotter(QGraphicsItem):
    def __init__(self, width, height, model):
        super(Plotter, self).__init__()
        self.width = width
        self.height = height
        self.model = model
        self.zoom = 200

        pass

    def plottable_centre(self):
        return np.array([self.width / 2, self.height / 2, self.width / 2, self.height / 2])
        pass

    def do_plot(self):
        lines = self.model.projection()
        centre = self.plottable_centre()
        self.plottable_lines = list(map(lambda x: list(map(int, x.flatten() * self.zoom + centre)), lines))
        self.update()
        pass

    def paint(self, painter, option, widget):
        colours = [Qt.red, Qt.green, Qt.blue, Qt.black, Qt.darkCyan, Qt.darkYellow, Qt.darkYellow]
        line_colour = zip(self.plottable_lines, colours)
        for line, colour in line_colour:
            pen = QPen()
            pen.setWidth(3)
            pen.setBrush(colour)
            painter.setPen(pen)
            painter.drawLine(*line)
            pass
        pass

    def boundingRect(self):
        return QRectF(0, 0, self.width, self.height)
        pass

    def wheelEvent(self, event):
        d = event.delta()
        self.zoom += d / 10
        self.do_plot()
        pass

    pass


class View(QWidget):
    def __init__(self, model):
        super(View, self).__init__()
        self.model = model
        pass

    def register(self, controller):
        self.controller = controller
        self.init_ui()
        pass

    def init_ui(self):
        """
        ウィジェットの配置
        :return:
        """
        self.setWindowTitle("4D rotation")

        self.scene_size = 500
        self.plotter = Plotter(self.scene_size, self.scene_size, self.model)

        controllers_4d_layout = QHBoxLayout()

        controllers_4d = self.controller.controllers_4d
        for ix in range(len(controllers_4d)):
            sld = QSlider(Qt.Horizontal, self)
            sld.setValue(33)
            controllers_4d_layout.addWidget(sld)
            sld.valueChanged.connect(controllers_4d[ix])
            controllers_4d[ix](sld.value())
            pass

        controllers_3d_layout = QHBoxLayout()
        controllers_3d = self.controller.controllers_3d
        for ix in range(len(controllers_3d)):
            sld = QSlider(Qt.Horizontal, self)
            sld.setValue(33)
            controllers_3d_layout.addWidget(sld)
            sld.valueChanged.connect(controllers_3d[ix])
            controllers_3d[ix](sld.value())
            pass

        self.graphics_view = QGraphicsView()
        scene = QGraphicsScene(self.graphics_view)
        scene.setSceneRect(0, 0, self.scene_size, self.scene_size)
        self.graphics_view.setScene(scene)
        scene.addItem(self.plotter)

        # self.setLayout(grid)
        main_layout = QVBoxLayout()
        main_layout.setAlignment(Qt.AlignTop)
        main_layout.addLayout(controllers_4d_layout)
        main_layout.addLayout(controllers_3d_layout)
        main_layout.addWidget(self.graphics_view)
        self.setLayout(main_layout)

        pass

    pass


class Controller(object):
    def __init__(self, view, model):
        self.view = view
        self.model = model

        self.controllers_4d = [self.chg_sld_t, self.chg_sld_u, self.chg_sld_v]
        self.controllers_3d = [self.chg_sld_t3, self.chg_sld_u3]
        self.view.register(self)
        self.do_plot()
        pass

    def do_plot(self):
        self.view.plotter.do_plot()
        pass

    def chg_sld_t(self, value):
        self.model.set_t(value * 2 * np.pi / 100)
        self.do_plot()
        pass

    def chg_sld_u(self, value):
        self.model.set_u(value * 2 * np.pi / 100)
        self.do_plot()
        pass

    def chg_sld_v(self, value):
        self.model.set_v(value * 2 * np.pi / 100)
        self.do_plot()
        pass

    def chg_sld_t3(self, value):
        self.model.set_t3(value * 2 * np.pi / 100)
        self.do_plot()
        pass

    def chg_sld_u3(self, value):
        self.model.set_u3(value * 2 * np.pi / 100)
        self.do_plot()
        pass

    pass


def main():
    app = QApplication(sys.argv)
    model = Model()
    view = View(model)
    controller = Controller(view, model)

    view.show()
    sys.exit(app.exec_())
    pass


if __name__ == '__main__':
    main()
    pass

PC変えてからHSPインタプリタ入れてなくて,入れるのめんどくさかった.

pythonでのgui開発とオブジェクト指向っぽい書き方の勉強がてらpythonに移植?した.

PlotterクラスとViewクラスに分けて実装した(参考にしたサイト・ソース↓がQGraphicsItemのクラスとViewクラスに分けてた)がMVC的には良かったのかわからない.