LoginSignup
4
0

More than 1 year has passed since last update.

<OpenSiv3D>簡単な図形描画アプリを作ってみた<Siv3D Advent Calendar 2021>

Last updated at Posted at 2021-12-09

はじめに

 *この記事はSiv3D Advent Calendar 2021参加用の記事です。

記事の目的

この記事の目的は、私が実際に開発した図形描画アプリの紹介、コードの公開、そして簡単な解説を行うことによって、OpenSiv3Dを多くの人に紹介することです。
OpenSiv3Dとは、@Reputelessさんによって開発されている、C++用のライブラリです。"クリエイターのための C++ ライブラリ"とされており、グラフィック描画、GUIアプリ開発、2D/3Dゲーム開発などに対応しています。そしてそのようなグラフィカルな機能だけではなく、オーディオ関係の機能、TCP通信など、「プログラミングをして何かを作る」ときに必要なあらゆる機能が様々に盛り込まれています。以下、公式サイトより引用します。

Siv3D を導入することで、次のような操作を組み合わせたアプリケーションを非常に短いコードで記述できます。

図形や画像、テキスト、動画、3Dモデルなど、グラフィックスの描画 (Direct3D 11 / OpenGL 4.1 / WebGL 2.0)
マウスやキーボード、Webカメラ、マイク、ゲームパッドなど、ヒューマンインタフェースデバイス (HID) の使用
ウィンドウ処理、ファイルシステム、ネットワーク
画像処理や音声処理
物理演算や経路探索、幾何などの計算
データ構造やアルゴリズム

また、現在も開発の真っ最中であるライブラリとして、C++20を含む最新のC++対応がなされており、このライブラリを使うことを通して最新のC++を身に着けていけるというのも売りの一つとなっています。
実際にこの記事で紹介するアプリでは、コンセプトと呼ばれるC++20の最新機能を、簡単にではありますが、使用してみています。
他にも、公式ドキュメントの充実さや、公式Slackコミュニティを通して、あるいは実装会と呼ばれるオンラインのイベントへの参加を通して、直接開発者さんご自身に質問でき、またその回答をいただけるというサポートの手厚さなど、入門・学習にとても易しいこと、またとても簡単なコードから目に見えて動くプログラムを書くことができるため、プログラミングの学習にも用いることができることなど、たくさんの魅力を備えたライブラリとなっています。

記事の注意点

私自身がプログラミングの初学者であるため、より熟達したプログラマーの方から見るとずさんであったり、未熟であったりする記述なども含まれているかもしれません。他の初学者の方には、私のコードを丸覚えするのではなく、あくまで参考として見ていただき、実際の開発ではご自身によって正しい使い方か、適切な設計か、を確認されることをおすすめします。私のコードの問題点を理解できる方は、もしよろしければ、私とそして今記事を読む他の初学者のために、指摘などをコメントでしていただけると幸いです。
また、時間と私の体力、気力・集中力の限界から、解説は簡素なものに限らせていただいております。本当はHands-on形式で、ゼロから徐々に作り方を解説していくような記事を書く予定でした。(残念ですが)今回の解説の目的は「アプリの仕組みの概要を把握してもらう」とさせていただきます。

この記事と、紹介させていただくアプリの背景

私はもともと、OpenSiv3Dを使って、初等幾何学の学習・研究補助アプリを作りたいというアイデアがありました。私は数学が好きなので、ぜひとも数学とプログラミングを組み合わせた事をやってみたいと思っており、その一環として、自分でもそのようなアプリがあれば使ってみたいと思い、構想しました。
私は、まだ技術力や経験が未熟であるためか、中々最初からちゃんと考えてコードを書き始める、ということができず、最初に「とっかかり」のような作業をすることが必要な人です。例えば、とりあえず適当な文字列を出してみよう、とりあえず適当な図形を出してみよう、次はその図形をとりあえず動かしてみよう、というような。そして、そうしているうちに自然とアイデアや実装が思いついてきて、少しずつ付け足していき、定期的にリファクタリングをしてコードや設計を整え、という形でアプリを作っています。
今回もその一環として、とりあえずマウス操作で直線を引いたり、円を描けたりするアプリを最初に作ってみました。
そのときに、開発者の方からこのアドベントカレンダーへの参加をお誘いいただきました。そこで、とりあえずこのアプリを「簡単な図形描画アプリ」にして、そのアプリを紹介・解説する記事を書こう、と思い至りました。
そしておよそ一週間ほどの期間を経て、無事にリファクタリングまで完了し、今この記事を書いている、というわけです。

アプリのご紹介

かなり簡単なアプリであるため、上記の動画を見るだけで全てご理解いただけると思います。
基本的にマウス操作で、Line(直線を引く)、Circle(円を描く)、Rect(長方形を描く)、Triangle(三角形を描く)、Polygon(多角形を描く)の5種類の図形描画に対応しています。Eraserによる消しゴム機能と、Clearによる一斉消去、またカラーピッカーによって自由に色を決めることができ、Randomモードにすると図形を描画する度に色がランダムに選ばれるようになります。

コード(全文)

# include <Siv3D.hpp> // OpenSiv3D v0.6.3

// GUI領域全体のmarginサイズ
constexpr int32 gui_margin_size{ 8 };
// GUI領域のグリッドのサイズ
constexpr int32 gui_grid_size{ 40 };

// メニューの横幅
int32 menu_base_w(int32 w)
{
    return gui_margin_size * 2 + gui_grid_size * w;
}

// メニューの縦幅
int32 menu_base_h(int32 h)
{
    return gui_margin_size * 2 + gui_grid_size * h;
}

// メニューの横幅と縦幅をPoint型で返す
Point menu_base_w_h(int32 w, int32 h)
{
    return Point{ gui_margin_size * 2 + gui_grid_size * w, gui_margin_size * 2 + gui_grid_size * h };
}

// aからbへの線形補間。0 <= t <= 1の範囲。
double lerp(int32 a, int32 b, double t)
{
    int32 diff = abs(b - a);
    return a + t * diff;
}

// メニューの展開・縮小時のアニメーションのための関数。
// 展開完了時のy座標と、アニメーション管理用の変数(0<=t<=1)を入れてやれば、
// アニメーション時の各tごとの適切なy座標の値を返してくれる
int32 animation(int32 a, double t)
{
    return floor(lerp(a - menu_base_h(3), a, EaseInOutExpo(t)));
}

// GUIグリッド座標上のx座標とy座標とアニメーション管理用の変数(0<=t<=1)を入れてやれば、
// 各tごとの適切な座標をPoint型で返してくれる。
// ウィジェットの左上の角の位置を返すため、
// もしウィジェットの中心の座標が欲しい場合には、Point{ウィジェットの横幅の半分, ウィジェットの縦幅の半分}
// をこの関数の返り値に足してやる必要がある。
Point grid_layout(int32 x, int32 y, double t)
{
    return Point{ gui_margin_size + gui_grid_size * x, animation(gui_margin_size + gui_grid_size * y, t) };
}

// 図形の種類と描画モードを表している。上から順に、
// 線、円、長方形、三角形、多角形、消しゴムである。
// 消しゴム以外は図形の種類と描画モードを同時に意味しているが、
// 消しゴムだけは描画モードのみを意味していることに注意。
enum class ShapeType
{
    Line,
    Circle,
    Rect,
    Triangle,
    Polygon,
    Eraser,
};


// C++20で導入された構文。
// 見たままの意味なのでわかりやすい。
template <class T>
concept Drawable = requires(T shape)
{
    shape.draw();
};

// 描画される図形
// TにはSiv3Dでdraw関数が定義されている図形が入る。
template<Drawable T>
class Shape
{
public:
    // shapeは基本となるSiv3Dの図形、shape_typeは図形の種類、colorは図形の色である
    Shape(T shape, ShapeType shape_type, ColorF color) :shape_(shape), shape_type_(shape_type), color_(color) {};
    // 図形の描画
    void draw() const
    {
            shape_.draw(color_);
    }
    // shape_への参照を返す
    const T& get_shape() const { return shape_; }
    // shape_type_を返す
    ShapeType get_shapetype() const { return shape_type_; }
    // この図形の種類がstと同じならtrue、そうでないならfalseを返す
    bool comparing_shape_type(ShapeType st) const { return st == shape_type_; }
private:
    T shape_;
    ShapeType shape_type_;
    ColorF color_;
};

// std::visitで使用するためのもの。
struct Visitor {
    Circle& y;
    bool operator()(Shape<Line>& x)
    {
        return x.get_shape().intersects(y);
    }
    bool operator()(Shape<Circle>& x)
    {
        return x.get_shape().intersects(y);
    }
    bool operator()(Shape<Rect>& x)
    {
        return x.get_shape().intersects(y);
    }
    bool operator()(Shape<Triangle>& x)
    {
        return x.get_shape().intersects(y);
    }
    bool operator()(Shape<Polygon>& x)
    {
        return x.get_shape().intersects(y);
    }
};

void Main()
{
    // 基本設定
    Window::Resize(1280, 720);
    Scene::SetBackground(Palette::Black);

    // 変数・定数定義
    constexpr int32 menubutton_p_x{ gui_margin_size };
    constexpr int32 menubutton_size{ gui_grid_size*2 };
    constexpr int32 gui_button_width{ gui_grid_size * 3 };
    constexpr int32 color_check_circle_radius{ gui_grid_size / 2 };
    constexpr int32 animation_speed{ 3 };
    const Font font{ gui_grid_size };
    Array<std::variant<Shape<Line>, Shape<Circle>, Shape<Rect>, Shape<Triangle>, Shape<Polygon>>> shapes{};
    Array<Point> tmp{};
    ShapeType mode_of_shape{ ShapeType::Line };
    HSV color{ Palette::White };
    HSV menu_button_middle_line_color{ Palette::Gray };
    bool is_opened_menu{ false };
    bool is_debug{ false };
    bool is_random{ false };
    bool is_on_menu_base{ false };
    // アニメーション用変数
    double t{ 0.0 };
    Camera2D camera{ Point{0, 0}, 1.0 };

    // メインループ
    while (System::Update())
    {

        auto menu_base = Rect{ Point{0, animation(0, t)}, menu_base_w_h(21, 3) };

        is_on_menu_base = menu_base.leftPressed();

        if (is_random and mode_of_shape != ShapeType::Eraser and tmp.isEmpty() and not is_on_menu_base and MouseL.down())
            color = RandomColorF();

        camera.update();
        {
            const auto tt = camera.createTransformer();

            // 図形の描画
            for (const auto& shape : shapes)
            {
                std::visit([](auto& x) {x.draw(); }, shape);
            }

            // 各モードごとの処理
            if (not is_on_menu_base)
            {
                switch (mode_of_shape)
                {
                case ShapeType::Line:
                    if (MouseL.down())
                        tmp << Cursor::Pos();
                    if (not MouseL.down() && MouseL.pressed() && not tmp.isEmpty())
                    {
                        Line{ tmp.front(), Cursor::Pos() }.draw(color);
                    }
                    if (MouseL.up() && not tmp.isEmpty())
                    {
                        shapes << Shape<Line>{Line{ tmp.front(), Cursor::Pos() }, mode_of_shape, color};
                        tmp.clear();
                    }
                    break;
                case ShapeType::Circle:
                    if (MouseL.down())
                        tmp << Cursor::Pos();
                    if (not MouseL.down() && MouseL.pressed() && not tmp.isEmpty())
                    {
                        Circle{ tmp.front(), tmp.front().distanceFrom(Cursor::Pos()) }.draw(color);
                    }
                    if (MouseL.up() && not tmp.isEmpty())
                    {
                        shapes << Shape<Circle>{Circle{ tmp.front(), tmp.front().distanceFrom(Cursor::Pos()) }, mode_of_shape, color};
                        tmp.clear();
                    }
                    break;
                case ShapeType::Rect:
                    if (MouseL.down())
                        tmp << Cursor::Pos();
                    if (not MouseL.down() && MouseL.pressed() && not tmp.isEmpty())
                    {
                        Rect{ tmp.front().x, tmp.front().y, Cursor::Pos().x - tmp.front().x, Cursor::Pos().y - tmp.front().y }.draw(color);
                    }
                    if (MouseL.up() && not tmp.isEmpty())
                    {
                        if (tmp.front().x < Cursor::Pos().x and tmp.front().y < Cursor::Pos().y)
                            shapes << Shape<Rect>{Rect{ tmp.front().x, tmp.front().y, Cursor::Pos().x - tmp.front().x, Cursor::Pos().y - tmp.front().y }, mode_of_shape, color};
                        else if (tmp.front().x > Cursor::Pos().x and tmp.front().y < Cursor::Pos().y)
                            shapes << Shape<Rect>{Rect{ Cursor::Pos().x, tmp.front().y, tmp.front().x - Cursor::Pos().x, Cursor::Pos().y - tmp.front().y }, mode_of_shape, color};
                        else if (tmp.front().x > Cursor::Pos().x and tmp.front().y > Cursor::Pos().y)
                            shapes << Shape<Rect>{Rect{ Cursor::Pos().x, Cursor::Pos().y, tmp.front().x - Cursor::Pos().x, tmp.front().y - Cursor::Pos().y }, mode_of_shape, color};
                        else if (tmp.front().x < Cursor::Pos().x and tmp.front().y > Cursor::Pos().y)
                            shapes << Shape<Rect>{Rect{ tmp.front().x, Cursor::Pos().y, Cursor::Pos().x - tmp.front().x, tmp.front().y - Cursor::Pos().y }, mode_of_shape, color};
                        tmp.clear();
                    }
                    break;
                case ShapeType::Triangle:
                    if (MouseL.down())
                    {
                        if (tmp.size() < 2)
                            tmp << Cursor::Pos();
                        else
                        {
                            shapes << Shape<Triangle>{Triangle{ tmp[0], tmp[1], Cursor::Pos() }, mode_of_shape, color};
                            tmp.clear();
                        }
                    }
                    if (not tmp.isEmpty())
                    {
                        for (size_t i = 0; i < tmp.size() - 1; i++)
                        {
                            Line{ tmp[i], tmp[i + 1] }.draw(color);
                        }
                        Line{ tmp.back(), Cursor::Pos() }.draw(color);
                    }
                    break;
                case ShapeType::Polygon:
                    if (MouseL.down())
                    {
                        if ((tmp.size() >= 3) and (tmp.front().distanceFrom(Cursor::Pos()) < 10))
                        {
                            Array<Vec2> tmp_vec2{};
                            for (const auto& v : tmp)
                            {
                                tmp_vec2.push_back(Vec2{ static_cast<double>(v.x), static_cast<double>(v.y) });
                            }
                            shapes << Shape<Polygon>{Polygon::CorrectOne(tmp_vec2), mode_of_shape, color};
                            tmp.clear();
                        }
                        else
                        {
                            tmp << Cursor::Pos();
                        }
                    }
                    if (not tmp.isEmpty())
                    {
                        for (size_t i = 0; i < tmp.size() - 1; i++)
                        {
                            Line{ tmp[i], tmp[i + 1] }.draw(color);
                        }
                        Line{ tmp.back(), Cursor::Pos() }.draw(color);
                    }
                    break;
                case ShapeType::Eraser:
                    if (MouseL.pressed() and not is_on_menu_base)
                    {
                        Circle eraser = Circle{ Cursor::Pos(), 10 };
                        eraser.draw(Palette::White);
                        for (auto it = shapes.begin(); it != shapes.end(); ++it)
                        {
                            if (std::visit(Visitor{ eraser }, *it))
                            {
                                shapes.erase(it);
                                break;
                            }
                        }
                    }
                    break;
                default:
                    Print << U"There is no such shpae type";
                    break;
                }
            }
        }

        // UI

        //// menu_base
        menu_base.draw(Palette::Darkgray);

        //// color_circle
        Circle{ grid_layout(3, 0, t) + Point{color_check_circle_radius, color_check_circle_radius}, color_check_circle_radius }.draw(color);
        if (is_random)
            font(U"R").drawAt(grid_layout(3, 0, t) + Point{ color_check_circle_radius, color_check_circle_radius }, Palette::Whitesmoke);

        //// Randomボタン
        if (SimpleGUI::Button(U"Random", grid_layout(2, 1, t), gui_button_width))
        {
            is_random = not is_random;
        }

        //// クリアボタン
        if (SimpleGUI::Button(U"Clear", grid_layout(2, 2, t), gui_button_width))
        {
            shapes.clear();
        }

        //// ColorPicker
        SimpleGUI::ColorPicker(color, grid_layout(5, 0, t));

        //// ボタン
        if (SimpleGUI::Button(U"Line", grid_layout(10, 0, t), gui_button_width))
        {
            mode_of_shape = ShapeType::Line;
        }
        if (SimpleGUI::Button(U"Circle", grid_layout(10, 2, t), gui_button_width))
        {
            mode_of_shape = ShapeType::Circle;
        }
        if (SimpleGUI::Button(U"Rect", grid_layout(14, 0, t), gui_button_width))
        {
            mode_of_shape = ShapeType::Rect;
        }
        if (SimpleGUI::Button(U"Triangle", grid_layout(14, 2, t), gui_button_width))
        {
            mode_of_shape = ShapeType::Triangle;
        }
        if (SimpleGUI::Button(U"Polygon", grid_layout(18, 0, t), gui_button_width))
        {
            mode_of_shape = ShapeType::Polygon;
        }
        if (SimpleGUI::Button(U"Eraser", grid_layout(18, 2, t), gui_button_width))
        {
            mode_of_shape = ShapeType::Eraser;
        }

        //// メニューボタン
        auto menu_button = RoundRect{ gui_margin_size, gui_margin_size, menubutton_size, menubutton_size, 4 };
        menu_button.draw(Palette::White);
        Line{ Point{gui_margin_size + menubutton_p_x, gui_margin_size + 20}, Point{gui_margin_size + (menubutton_size - menubutton_p_x), gui_margin_size + 20}.lerp(Point{gui_margin_size + (menubutton_size - menubutton_p_x), gui_margin_size + 60}, t) }.draw(4, Palette::Gray);
        menu_button_middle_line_color.a = 1.0 - t;
        Line{ Point{gui_margin_size + menubutton_p_x, gui_margin_size + 40}, Point{gui_margin_size + (menubutton_size - menubutton_p_x), gui_margin_size + 40} }.draw(4, menu_button_middle_line_color);
        Line{ Point{gui_margin_size + menubutton_p_x, gui_margin_size + 60}, Point{gui_margin_size + (menubutton_size - menubutton_p_x), gui_margin_size + 60}.lerp(Point{gui_margin_size + (menubutton_size - menubutton_p_x), gui_margin_size + 20}, t) }.draw(4, Palette::Gray);
        if (menu_button.leftClicked())
        {
            is_opened_menu = not is_opened_menu;
            tmp.clear();
        }

        // 移動操作のUI表示
        camera.draw(color);

        // アニメーション変数の処理
        if (not is_opened_menu)
        {
            if (t > 0)
            {
                t -= Scene::DeltaTime() * animation_speed;
                if (t < 0)
                    t = 0;
            }
        }
        else
        {
            if (t < 1.0)
            {
                t += Scene::DeltaTime() * animation_speed;
                if (t > 1.0)
                    t = 1.0;
            }
        }

        //debug グリッド
        if (is_debug)
        {

            // 最小(8)グリッド
            for (size_t i = 0; i < 128; i++)
            {
                Line{ Point{8 * i, 0}, Point{8 * i, 8 * 128} }.draw();
            }
            for (size_t i = 0; i < 128; i++)
            {
                Line{ Point{0, 8 * i}, Point{8 * 128, 8 * i} }.draw();
            }

            // 40グリッド
            for (size_t i = 0; i < 32; i++)
            {
                Line{ Point{40 * i + 8, 0 + 8}, Point{40 * i + 8, 40 * 32 + 8} }.draw(Palette::Red);
            }
            for (size_t i = 0; i < 32; i++)
            {
                Line{ Point{0 + 8, 40 * i + 8}, Point{40 * 32 + 8, 40 * i + 8} }.draw(Palette::Red);
            }
        }

        if (KeySpace.down())
        {
            is_debug = not is_debug;
        }

        if (is_on_menu_base)
        {
            tmp.clear();
        }
    }
}

コードの解説

このアプリを開発するにあたって、最も独自に工夫したのはGUIの構築でした。まず、そこから解説しようと思います。
開発者の方にお聞きしたところ、OpenSiv3Dに付属しているGUIウィジェットは、40*40を1セルとして設計されているようです(ボタンなど、左右幅を指定できるものはその限りではないが、高さはやはり40となる)
そこで私は、統一性を持たせるために、このアプリでもGUIウィジェットの配置は40*40単位のグリッドに沿わせる形で行おうと考えました。
image.png
まず、メニュー全体を画面の端から8px空白を開けました。そして8pxの余白の内側で、40*40のグリッドを考え、GUIを配置しました。
また、開発中に確認しやすいように、スペースキーを押すとグリッドが表示されるようにしました(上の画像の白と赤のグリッドがそれです。白が8*8、赤が40*40)。
次に、メニューを開いたり閉じたりした時の上から下へ、また下から上へのスライドアニメーションの仕組みも必要でした。
アニメーションの仕組みは単純です。まずtというdouble型の変数を一つ用意します。そして、メニューが閉じた状態のウィジェットの位置と、メニューが開いた状態のウィジェットの位置を割り出しておきます。それから、0~1と、閉じた状態の位置~開いた状態の位置を対応させます
かんたんなことです。0だったら閉じた位置。0.5だったらちょうど真ん中。0.9だったら9割開いた位置。1だったら開いた位置。というように対応させるわけです。方法は後述します。
このように対応させれたら、あとはアニメーションがスタートしたらtを0から1に好きな時間をかけて変化させ、あるいは閉じるアニメーションがスタートしたらtを1から0に変化させます。それだけで終わりです。

// アニメーション変数の処理
        if (not is_opened_menu)
        {
            if (t > 0)
            {
                t -= Scene::DeltaTime() * animation_speed;
                if (t < 0)
                    t = 0;
            }
        }
        else
        {
            if (t < 1.0)
            {
                t += Scene::DeltaTime() * animation_speed;
                if (t > 1.0)
                    t = 1.0;
            }
        }

上記が、そのtを0から1へ、また1から0へ変化させる実際のコードです。

// メニューの展開・縮小時のアニメーションのための関数。
// 展開完了時のy座標と、アニメーション管理用の変数(0<=t<=1)を入れてやれば、
// アニメーション時の各tごとの適切なy座標の値を返してくれる
int32 animation(int32 a, double t)
{
    return floor(lerp(a - menu_base_h(3), a, EaseInOutExpo(t)));
}

上記が、tとウィジェットの位置を対応させる関数です。aの部分には、開いたときにいて欲しい位置情報のy座標を指定します。すると、tの値によって、対応するウィジェットの座標(y座標)を返してくれます。(y座標のみなのは、このメニューのアニメーションが単純に上下方向に移動するだけのアニメーションであるため)
また、メニューの開閉のアニメーションに少し変化をつけるため(つまり、一定のテンポですらーっと降りてきて、すらーっと上がる、のではなく、ゆっくり~早く、早く~ゆっくりのような動きの変化をつけることで、「シュンッ」といった感じのより豊かな動作をさせる事)の細工がしてあります。それも詳細は省略させていただきます。
GUI周りについては、最後に、このコードを紹介して終わりにしようと思います。

//// ボタン
        if (SimpleGUI::Button(U"Line", grid_layout(10, 0, t), gui_button_width))
        {
            mode_of_shape = ShapeType::Line;
        }
        if (SimpleGUI::Button(U"Circle", grid_layout(10, 2, t), gui_button_width))
        {
            mode_of_shape = ShapeType::Circle;
        }
        if (SimpleGUI::Button(U"Rect", grid_layout(14, 0, t), gui_button_width))
        {
            mode_of_shape = ShapeType::Rect;
        }
        if (SimpleGUI::Button(U"Triangle", grid_layout(14, 2, t), gui_button_width))
        {
            mode_of_shape = ShapeType::Triangle;
        }
        if (SimpleGUI::Button(U"Polygon", grid_layout(18, 0, t), gui_button_width))
        {
            mode_of_shape = ShapeType::Polygon;
        }
        if (SimpleGUI::Button(U"Eraser", grid_layout(18, 2, t), gui_button_width))
        {
            mode_of_shape = ShapeType::Eraser;
        }

grid_layoutという部分に注目してください。これは、左から10番目、上から2番めのグリッドに配置する、という意味です。このように記述するだけで、自動的に適切な実際の位置を(アニメーションもしっかり考慮した上で)返してくれる関数を実装しました。これでGUIの設計が、単にグリッドのマス目を見ながら起きたい位置を指定するだけ、という簡単なものになりました。以下がその関数の定義です。

// GUIグリッド座標上のx座標とy座標とアニメーション管理用の変数(0<=t<=1)を入れてやれば、
// 各tごとの適切な座標をPoint型で返してくれる。
// ウィジェットの左上の角の位置を返すため、
// もしウィジェットの中心の座標が欲しい場合には、Point{ウィジェットの横幅の半分, ウィジェットの縦幅の半分}
// をこの関数の返り値に足してやる必要がある。
Point grid_layout(int32 x, int32 y, double t)
{
    return Point{ gui_margin_size + gui_grid_size * x, animation(gui_margin_size + gui_grid_size * y, t) };
}

次に、図形の描画の実装についても概説しましょう。
まず、以下のようにして”Shape”というものを定義しました。

// 描画される図形
// TにはSiv3Dでdraw関数が定義されている図形が入る。
template<Drawable T>
class Shape
{
public:
    // shapeは基本となるSiv3Dの図形、shape_typeは図形の種類、colorは図形の色である
    Shape(T shape, ShapeType shape_type, ColorF color) :shape_(shape), shape_type_(shape_type), color_(color) {};
    // 図形の描画
    void draw() const
    {
            shape_.draw(color_);
    }
    // shape_への参照を返す
    const T& get_shape() const { return shape_; }
    // shape_type_を返す
    ShapeType get_shapetype() const { return shape_type_; }
    // この図形の種類がstと同じならtrue、そうでないならfalseを返す
    bool comparing_shape_type(ShapeType st) const { return st == shape_type_; }
private:
    T shape_;
    ShapeType shape_type_;
    ColorF color_;
};

template<Drawable T>と書かれていますね。このDrawableの定義はこうです。

// C++20で導入された構文。
// 見たままの意味なのでわかりやすい。
template <class T>
concept Drawable = requires(T shape)
{
    shape.draw();
};

はい、C++20で導入された新しい文法、コンセプトです! C++20らしさを表現するためのほとんど演出に近い簡単な使用ですが、どうしても使ってみたかったので書きました。

// 図形の描画
            for (const auto& shape : shapes)
            {
                std::visit([](auto& x) {x.draw(); }, shape);
            }

これで、図形を描画しています。ご覧の通り、描画自体は、Shapeクラスのdraw関数を呼び出すことを経由して、OpenSiv3Dのdraw関数をよびだしているだけなので、ほとんど僕は何もしていません。
したことといえば、異なる図形を同じ配列にまとめるために、variantを使うように工夫したことくらいです。

Array<std::variant<Shape<Line>, Shape<Circle>, Shape<Rect>, Shape<Triangle>, Shape<Polygon>>> shapes{};

ただ、図形を作成して上の配列に格納する部分はしっかり書く必要がありました。それが以下のコードです。(長いため、Rectの処理の部分のみ抜粋しています)

if (MouseL.down())
    tmp << Cursor::Pos();
if (not MouseL.down() && MouseL.pressed() && not tmp.isEmpty())
{
    Rect{ tmp.front().x, tmp.front().y, Cursor::Pos().x - tmp.front().x, Cursor::Pos().y - tmp.front().y }.draw(color);
}
if (MouseL.up() && not tmp.isEmpty())
{
    if (tmp.front().x < Cursor::Pos().x and tmp.front().y < Cursor::Pos().y)
        shapes << Shape<Rect>{Rect{ tmp.front().x, tmp.front().y, Cursor::Pos().x - tmp.front().x, Cursor::Pos().y - tmp.front().y }, mode_of_shape, color};
    else if (tmp.front().x > Cursor::Pos().x and tmp.front().y < Cursor::Pos().y)
        shapes << Shape<Rect>{Rect{ Cursor::Pos().x, tmp.front().y, tmp.front().x - Cursor::Pos().x, Cursor::Pos().y - tmp.front().y }, mode_of_shape, color};
    else if (tmp.front().x > Cursor::Pos().x and tmp.front().y > Cursor::Pos().y)
        shapes << Shape<Rect>{Rect{ Cursor::Pos().x, Cursor::Pos().y, tmp.front().x - Cursor::Pos().x, tmp.front().y - Cursor::Pos().y }, mode_of_shape, color};
    else if (tmp.front().x < Cursor::Pos().x and tmp.front().y > Cursor::Pos().y)
        shapes << Shape<Rect>{Rect{ tmp.front().x, Cursor::Pos().y, Cursor::Pos().x - tmp.front().x, tmp.front().y - Cursor::Pos().y }, mode_of_shape, color};
    tmp.clear();
}

tmpというのは、一時的にマウスの押した位置を保管しておくための変数です。(例えば多角形ならわかりやすいですが、マウスでどんどんポチポチと頂点を押して指定していきますよね。それらはtmpに一時的に保存されて、最後に図形の形が確定した時点でtmpの中身を使って図形を生成して、そしてtmpを再び空にします)
解説は以上です。

最後に

このアプリを作る過程、それこそが本当は最も大事な学習リソースだったような気がします。例えば、僕は最初はボタンの位置なんて適当に並べておいて、とにかく図形を描画できるようにしたり、色を指定できるようにしたりしていきました。そして、ある程度アプリも機能が整い、頭も温まってきたところで、GUIの構築に取り組み、上述したようなシステムを考え、実装し、きれいにボタンなどを配置することができました。もし僕が最初からきれいにボタンを配置して~などと考えてたら、ここまで作れてた自信はありません。
人によっても違うと思いますが、特に慣れないうちは、ハリボテで、ツギハギで、とても人に見せられないような作業途中の荒れ果てた風景になったとしても、手を動かしてみる、とりあえず作ってみる、でも、余裕が出てきたら、しっかり最後は整えていく。すると、気がついたときには、びっくりするほどしっかりとした、いろいろな機能のついたアプリになっている……。そういうやり方が「効く」人もいるのではないかな、と思いました。
ただ一方で、あまりにも雑然としたまま進めてしまうと、最後にリファクタをするときにコードが複雑になりすぎて全く収集がつかない、という自体に陥るリスクも感じました。あくまで雑然としたやり方が通用するのはブートスタートのためだけで、極力早い段階から綺麗にできるならしておいたほうがいい、というのもまた感じました。
このアプリは、おそらく人生で二度目に作った「完成した、完結した、見た目も機能も整った、実用的な一つのアプリ」になると思います。(プログラミングを初めて2ヶ月とかで作った掲示板が一度目。あれからだいぶ長い間こういう「一つのまとまったなにか」を作れてなかった)
以上でこの記事は閉めさせていただきます。ぜひとも皆様のプログラミングライフに、少しなりともこの記事がお役に立ちますように。
(長い割にほぼなんの解説もしてない気がするから誰の役に立つんだ?とすごく疑問な記事になってしまったが)

4
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
0