程序截图
操作方法
鼠标拖动。左键拖动及滚轮能看到不同角度下正方体的形状,右键拖动能将最近的正方体顶点挪到这个投影面的相应位置。
按键控制。wasd 控制投影面旋转,ws 关于 x 轴旋转,ad 关于 y 轴旋转。
个人思路
首先投影面的确立需要两个量,一个 x 轴方向的单位向量,一个 y 轴方向的单位向量,求原点与三维空间中的点的连线到这两个单位向量的投影就能得到三维空间中的点在二维投影面中的坐标。记 x 轴方向单位向量为 X,y 轴方向单位向量为 Y,X 与 Y 互相垂直,模都为 1。
正方体的八个顶点位置随意,X,Y 两个单位向量只需是互相垂直的非零向量即可。
鼠标横向拖动时,X 关于 Y 旋转,这是怎么做到的呢。要做到这一点,就需要一个新的向量,也就是投影面的法向量,投影面的法向量可以根据两向量叉乘得到。叉乘的推法用的是线性代数的方法,但是不好理解,我用我的方法推出来,希望能方便理解。
设投影面法向量为 Z(x2, y2, z2),X(x0, y0, z0) 和 Y(x1, y1, z1) 与 Z 的点乘为 0,这就能列出两个式子:
① x0x2+y0y2+z0z2 = 0
② x1x2+y1y2+z1z2 = 0
将①式转化为以 x2 和 y2 表示的 z2 得
③ z2 = - (x0x2 + y0y2) / z0
将②式和③式结合转化为以 x2 表示的 y2 得
④ y2 = (x0z1 - x1z0) / (y1z0 - y0z1) * x2
④式代回③式得
⑤ z2 = (x1y0 - x0y1) / (y1z0 - y0z1) * x2 (推这一步时负号别忘了看)
也就是 Z = [x2, (x0z1 - x1z0) / (y1z0 - y0z1) * x2, (x1y0 - x0y1) / (y1z0 - y0z1) * x2]
仔细观察 Z 只有一个变量 x2,不妨先放大(y1z0 - y0z1)倍,得到
Z = [(y1z0 - y0z1) * x2, (x0z1 - x1z0) * x2, (x1y0 - x0y1) * x2]
把 x2 提取出来,Z 就是 Z = x2 * [(y1z0 - y0z1), (x0z1 - x1z0), (x1y0 - x0y1)]
令 x2 = 1,Z[(y1z0 - y0z1), (x0z1 - x1z0), (x1y0 - x0y1)] 就是投影面的法向量。到这一步还没有结束,因为垂直于一个面的法向量有两个,一个符合右手坐标系,一个符合左手坐标系,将 X(1, 0, 0),Y(0, 1, 0) 代入得 Z(0, 0, -1),所以这个 Z 是符合左手坐标系的投影面法向量,要转换成右手坐标系只需乘个 -1 就行,也就是 Z 最终为 (y0z1 - y1z0, x1z0 - x0z1, x0y1 - x1y0)。
由于 X,Y 都是单位向量,这个 Z 还是投影面的单位法向量。
Z 求出来了,X 关于 Y 旋转可以看做 X 在 XOZ 平面上旋转,问题转化成了求平面中某个向量转过θ度后的向量,如下图,将 X 看做下图中的红色向量,Z 看做下图中的绿色向量,虚线为向量旋转后θ度后的向量,可以发现 cos(θ)X - sin(θ)Z,就能求出 X 顺时针转动θ度后的向量,而 cos(θ)Z + sin(θ)X 就能求出 Z 顺时针转动θ度后的向量。
其它的旋转方式皆可以此类推。
代码实现
TCW_GUI.h:
#pragma once
#include<graphics.h>
#include<string>
#include<list>
#include<functional>
#define TCW_GUI_BUTTON_MYSELF 0
namespace TCW_GUI
{
enum class State
{
general = 0,
touch = 1,
press = 2,
release = 3,
forbidden = 4
};
class Vec2
{
public:
double x, y;
Vec2() :x(0), y(0) {}
Vec2(double xx, double yy) :x(xx), y(yy) {};
Vec2 operator+(Vec2 num)
{
return Vec2(x + num.x, y + num.y);
}
Vec2 operator-(Vec2 num)
{
return Vec2(x - num.x, y - num.y);
}
Vec2 operator/(double num)
{
return Vec2(x / num, y / num);
}
Vec2 operator*(double num)
{
return Vec2(x * num, y * num);
}
};
class Rect
{
public:
Rect() :size(), position() {}
Rect(Vec2 position, Vec2 size) :size(size), position(position) {}
Vec2 size;
Vec2 position;
bool isInRect(Vec2 point)
{
Vec2 left_top = position - size / 2.0;
Vec2 right_buttom = position + size / 2.0;
if (point.x >= left_top.x && point.y >= left_top.y &&
point.x <= right_buttom.x && point.y <= right_buttom.y)return true;
return false;
}
};
class Button
{
private:
double textsize = 20;
double textareasize = 0.9;
Vec2 defaultsize = Vec2(textwidth(L"...") / textareasize, textheight(L"...") / textareasize);
Vec2 defaulttext = Vec2(textwidth(L"..."), textheight(L"..."));
State nowstate = State::general;
void DrawButton_General();
void DrawButton_Touch();
void DrawButton_Press();
void DrawButton_Forbidden();
bool isPress = false;
public:
Button() :boundingbox(), buttontext() {}
Button(Rect boundingbox, std::wstring buttontext, std::function<int(void*)> releaseFunc, void* releaseParam) :
boundingbox(boundingbox), buttontext(buttontext), releaseFunc(releaseFunc), releaseParam(releaseParam) {}
std::wstring buttontext;
Rect boundingbox;
std::function<int(void*)> releaseFunc = nullptr;
void* releaseParam = nullptr;
void DrawButton();
void receiver(ExMessage* msg);
void ForbidButton() { this->nowstate = State::forbidden; } // 禁用按钮
void RefreshButton() { this->nowstate = State::general; } // 恢复按钮
void SetTextSize(double size)
{
textsize = size;
defaultsize = Vec2(textsize * 1.5 / textareasize, textsize / textareasize);
defaulttext = Vec2(textsize * 1.5, textsize);
}
void SetTextAreaSize(double size)
{
textareasize = size;
defaultsize = Vec2(textsize * 1.5 / textareasize, textsize / textareasize);
defaulttext = Vec2(textsize * 1.5, textsize);
}
};
class ButtonManager
{
std::list<Button> buttonlist;
public:
Button* AddButton(Button button);
void ReceiveMessage(ExMessage* msg);
void DrawButton();
};
void Rectangle_TCW(Vec2 left_top, Vec2 right_buttom)
{
rectangle(left_top.x, left_top.y, right_buttom.x, right_buttom.y);
}
void Fillrectangle_TCW(Vec2 left_top, Vec2 right_buttom)
{
fillrectangle(left_top.x, left_top.y, right_buttom.x, right_buttom.y);
}
void Outtextxy_TCW(Vec2 position, const WCHAR* str)
{
outtextxy(position.x, position.y, str);
}
void Button::DrawButton_General()
{
LOGFONT log;
COLORREF textcol;
COLORREF linecol;
COLORREF fillcol;
int bkmode;
gettextstyle(&log);
bkmode = getbkmode();
textcol = gettextcolor();
linecol = getlinecolor();
fillcol = getfillcolor();
settextstyle(textsize, 0, TEXT("微软雅黑"));
settextcolor(BLACK);
setbkmode(TRANSPARENT);
setlinecolor(BLACK);
setfillcolor(WHITE);
Vec2 size_button = Vec2(this->boundingbox.size * textareasize);
Vec2 size_text = Vec2(textwidth(this->buttontext.c_str()), textsize);
if (boundingbox.size.x > defaultsize.x && boundingbox.size.y > defaultsize.y)
{
Rectangle_TCW(this->boundingbox.position - this->boundingbox.size / 2.0,
this->boundingbox.position + this->boundingbox.size / 2.0);
Fillrectangle_TCW(this->boundingbox.position - this->boundingbox.size / 2.0,
this->boundingbox.position + this->boundingbox.size / 2.0);
if (size_button.x >= size_text.x && size_button.y >= size_text.y)
{
Outtextxy_TCW(this->boundingbox.position - size_text / 2.0, buttontext.c_str());
}
else
{
int wordnum = size_button.x / textwidth(buttontext.c_str()) * buttontext.size() - 2;
std::wstring realstr = buttontext.substr(0, wordnum);
realstr += L"...";
size_text = Vec2(textwidth(realstr.c_str()), textsize);
Outtextxy_TCW(this->boundingbox.position - size_text / 2.0, realstr.c_s