io.SetClipboardTextFn = ImGui_ImplSdlGL3_SetClipboardText;
io.GetClipboardTextFn = ImGui_ImplSdlGL3_GetClipboardText;
void\*の代入
io.ImeWindowHandle = wmInfo.info.win.window;
TypeError: in method 'ImGuiIO_ImeWindowHandle_set', argument 2 of type 'void \*'
セッターを作った。
%extend ImGuiIO {
void SetImeWindowHandle(long long v)
{
ImGui::GetIO().ImeWindowHandle = (void\*)v;
}
}
NewFrame の移植
def CreateDeviceObjects():
pass
g_MousePressed=[False, False, False]
g_MouseWheel=None
def NewFrame(window):
if not g_FontTexture:
CreateDeviceObjects();
io = imgui.GetIO();
# Setup display size (every frame to accommodate for window resizing)
w, h=SDL_GetWindowSize(window);
display_w, display_h=SDL_GL_GetDrawableSize(window);
io.DisplaySize = imgui.ImVec2(w, h);
io.DisplayFramebufferScale = ImVec2(
(display_w / float(w)) if w > 0 else 0,
(display_h / float(h)) if h > 0 else 0);
# Setup time step
time = SDL_GetTicks();
current_time = time / 1000.0;
io.DeltaTime = (current_time - g_Time) if g_Time > 0.0 else (1.0 / 60.0);
g_Time = current_time;
# Setup inputs
# (we already got mouse wheel, keyboard keys & characters from SDL_PollEvent())
mouseMask, mx, my = SDL_GetMouseState();
if (SDL_GetWindowFlags(window) & SDL_WINDOW_MOUSE_FOCUS):
# Mouse position, in pixels (set to -1,-1 if no mouse / on another screen, etc.)
io.MousePos = ImVec2(mx, my);
else:
io.MousePos = ImVec2(-1, -1);
# If a mouse press event came, always pass it as "mouse held this frame", so we don't miss click-release events that are shorter than 1 frame.
io.MouseDown[0] = g_MousePressed[0] or (mouseMask & SDL_BUTTON(SDL_BUTTON_LEFT)) != 0;
io.MouseDown[1] = g_MousePressed[1] or (mouseMask & SDL_BUTTON(SDL_BUTTON_RIGHT)) != 0;
io.MouseDown[2] = g_MousePressed[2] or (mouseMask & SDL_BUTTON(SDL_BUTTON_MIDDLE)) != 0;
g_MousePressed[0] = g_MousePressed[1] = g_MousePressed[2] = False;
io.MouseWheel = g_MouseWheel;
g_MouseWheel = 0.0;
# Hide OS mouse cursor if ImGui is drawing it
SDL_ShowCursor(0 if io.MouseDrawCursor else 1);
# Start the frame
imgui.NewFrame();
io.MouseDown
セッター。
%extend ImGuiIO {
void SetMouseDown(int k, int v)
{
ImGui::GetIO().MouseDown[k]=v;
}
}
CreateDeviceObjects の移植 # Backup GL state
last_texture=glGetIntegerv(GL_TEXTURE_BINDING_2D);
last_array_buffer=glGetIntegerv(GL_ARRAY_BUFFER_BINDING);
last_vertex_array=glGetIntegerv(GL_VERTEX_ARRAY_BINDING);
vertex_shader =b'''#version 330
uniform mat4 ProjMtx;
in vec2 Position;
in vec2 UV;
in vec4 Color;
out vec2 Frag_UV;
out vec4 Frag_Color;
void main()
{
Frag_UV = UV;
Frag_Color = Color;
gl_Position = ProjMtx \* vec4(Position.xy,0,1);
};
'''
fragment_shader =b'''#version 330
uniform sampler2D Texture;
in vec2 Frag_UV;
in vec4 Frag_Color;
out vec4 Out_Color;
void main()
{
Out_Color = Frag_Color \* texture( Texture, Frag_UV.st);
};
'''
g_ShaderHandle = glCreateProgram();
g_VertHandle = glCreateShader(GL_VERTEX_SHADER);
g_FragHandle = glCreateShader(GL_FRAGMENT_SHADER);
glShaderSource(g_VertHandle, vertex_shader);
glShaderSource(g_FragHandle, fragment_shader);
glCompileShader(g_VertHandle);
glCompileShader(g_FragHandle);
glAttachShader(g_ShaderHandle, g_VertHandle);
glAttachShader(g_ShaderHandle, g_FragHandle);
glLinkProgram(g_ShaderHandle);
g_AttribLocationTex = glGetUniformLocation(g_ShaderHandle, "Texture");
g_AttribLocationProjMtx = glGetUniformLocation(g_ShaderHandle, "ProjMtx");
g_AttribLocationPosition = glGetAttribLocation(g_ShaderHandle, "Position");
g_AttribLocationUV = glGetAttribLocation(g_ShaderHandle, "UV");
g_AttribLocationColor = glGetAttribLocation(g_ShaderHandle, "Color");
g_VboHandle=glGenBuffers(1);
g_ElementsHandle=glGenBuffers(1);
g_VaoHandle=glGenVertexArrays(1);
glBindVertexArray(g_VaoHandle);
glBindBuffer(GL_ARRAY_BUFFER, g_VboHandle);
glEnableVertexAttribArray(g_AttribLocationPosition);
glEnableVertexAttribArray(g_AttribLocationUV);
glEnableVertexAttribArray(g_AttribLocationColor);
#define OFFSETOF(TYPE, ELEMENT) ((size_t)&(((TYPE \*)0)->ELEMENT))
SIZEOF_ImDrawVert=20
glVertexAttribPointer(g_AttribLocationPosition, 2, GL_FLOAT, GL_FALSE, SIZEOF_ImDrawVert, ctypes.c_void_p(0));
glVertexAttribPointer(g_AttribLocationUV, 2, GL_FLOAT, GL_FALSE, SIZEOF_ImDrawVert, ctypes.c_void_p(8));
glVertexAttribPointer(g_AttribLocationColor, 4, GL_UNSIGNED_BYTE, GL_TRUE, SIZEOF_ImDrawVert, ctypes.c_void_p(16));
#undef OFFSETOF
CreateFontsTexture();
# Restore modified GL state
glBindTexture(GL_TEXTURE_2D, last_texture);
glBindBuffer(GL_ARRAY_BUFFER, last_array_buffer);
glBindVertexArray(last_vertex_array);
return True;
sizeof
%constant int SIZEOF_ImDrawVert = sizeof(ImDrawVert);
%constant int SIZEOF_ImDrawIdx = sizeof(ImDrawIdx);
offset
%{
#define OFFSETOF(TYPE, ELEMENT) ((size_t)&(((TYPE \*)0)->ELEMENT))
const int OFFSETOF_ImDrawVert_pos = OFFSETOF(ImDrawVert, pos);
const int OFFSETOF_ImDrawVert_uv = OFFSETOF(ImDrawVert, uv);
const int OFFSETOF_ImDrawVert_col = OFFSETOF(ImDrawVert, col);
#undef OFFSETOF
%}
const int OFFSETOF_ImDrawVert_pos = OFFSETOF_ImDrawVert_pos;
const int OFFSETOF_ImDrawVert_uv = OFFSETOF_ImDrawVert_uv;
const int OFFSETOF_ImDrawVert_col = OFFSETOF_ImDrawVert_col;
CreateFontsTexture の移植
def CreateFontsTexture(): # Build texture atlas
io = imgui.GetIO(); # Load as RGBA 32-bits for OpenGL3 demo because it is more likely to be compatible with user's existing shader.
pixels, width, height=io.Fonts.GetTexDataAsRGBA32();
# Upload texture to graphics system
last_texture=glGetIntegerv(GL_TEXTURE_BINDING_2D);
g_FontTexture=glGenTextures(1);
glBindTexture(GL_TEXTURE_2D, g_FontTexture);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glPixelStorei(GL_UNPACK_ROW_LENGTH, 0);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, pixels);
# Store our identifier
io.Fonts.TexID = g_FontTexture;
# Restore state
glBindTexture(GL_TEXTURE_2D, last_texture);
値を返すためにポインタ引数を出力に使い(in + argout)、長さの分かっているバイト列へのポインタを bytes として返す
対象はこれ。
IMGUI_API void GetTexDataAsRGBA32(unsigned char\*_ out_pixels, int_ out_width, int* out_height, int* out_bytes_per_pixel = NULL);
typemap でやってみる。
imgui.i の%include "imgui/imgui.h"より前に記述。
%typemap(in, numinputs=0) (unsigned char** out_pixels, int* out_width, int* out_height, int* out_bytes_per_pixel) (unsigned char *tempP, int tempW, int tempH, int tempB) {
$1 = &tempP;
$2 = &tempW;
$3 = &tempH;
$4 = &tempB;
}
%typemap(argout)(unsigned char** out_pixels, int* out_width, int* out_height, int* out_bytes_per_pixel){
auto b = PyBytes_FromStringAndSize((const char *)_$1, (_$2) _ (_$3) _ (_$4));
auto w = PyLong_FromLong(_$2);
auto h = PyLong_FromLong(_$3);
if ((!$result) || ($result == Py_None)) {
// new tuple3
$result = PyTuple_New(3);
PyTuple_SetItem($result, 0, b);
PyTuple_SetItem($result, 1, w);
PyTuple_SetItem($result, 2, h);
}
else{
if (!PyTuple_Check($result)) {
// new tuple4
auto t= PyTuple_New(4);
PyTuple_SetItem(t, 0, $result);
PyTuple_SetItem(t, 1, b);
PyTuple_SetItem(t, 2, w);
PyTuple_SetItem(t, 3, h);
$result=t;
}
else {
// concat
auto head = $result;
auto tail = PyTuple_New(3);
PyTuple_SetItem($result, 0, b);
PyTuple_SetItem($result, 1, w);
PyTuple_SetItem($result, 2, h);
$result = PySequence_Concat(head, tail);
Py_DECREF(head);
Py_DECREF(tail);
}
}
}
typemap(in)で呼び出し時に出力変数を与える必要を無くして、typemap(argout)で呼び出し後の返り値を tuple 化して値を詰める。その際に、バイト列のサイズが計算できるので PyBytes_FromStringAndSize で bytes を作る。
TexID のセッター
int から void*への変換。
%extend ImFontAtlas {
void SetTexID(long long id) {
self->TexID=reinterpret_cast<void*>(id);
}
}
ここまで実装すると Texture の nullptr でプログラムがクラッシュしなくなる。
imgui の widget に対する inout な引数
float*
IMGUI_API bool SliderFloat(const char* label, float* v, float v_min, float v_max, const char* display_format = "%.3f", float power = 1.0f);
argout にすると返り値が(bool, float)になっていまいちな気がする。
swig で float\*型のヘルパークラスを作る。
%include "cpointer.i"
%pointer_functions(float, floatp);
f=imgui.new_floatp()
imgui.floatp_assign(f, 0.0)
imgui.SliderFloat("float", f, 0.0, 1.0);
bool*
float と同様に。
IMGUI_API void ShowTestWindow(bool* p_open = NULL);
%include "cpointer.i"
%pointer_functions(bool, boolp);
show_test_window = imgui.new_boolp();
imgui.boolp_assign(show_test_window, True)
if imgui.Button("Test Window"): imgui.boolp_assign(show_test_window, not boolp_assign.value))
if imgui.boolp_value(show_test_window):
imgui.SetNextWindowPos(imgui.ImVec2(650, 20), imgui.ImGuiSetCond_FirstUseEver);
imgui.ShowTestWindow(show_test_window);
動くとはいえこれはいかんな。ポインタクラスをもう少し便利にするか、別の方法を調べよう。
float[3]
IMGUI_API bool ColorEdit3(const char\* label, float col[3]);
typemap で list リストを受け取って、list に結果を格納するように記述する。
%typemap(in) float col[3] (float temp[3]) {
if (!PySequence_Check($input)) {
PyErr_SetString(PyExc_ValueError,"Expected a sequence");
return NULL;
}
if (PySequence_Length($input) < $1_dim0) {
PyErr_SetString(PyExc_ValueError,"Size mismatch. Expected more than $1_dim0 elements");
return NULL;
}
for (int i = 0; i < $1_dim0; i++) {
PyObject *o = PySequence_GetItem($input,i);
if (PyNumber_Check(o)) {
temp[i] = (float) PyFloat_AsDouble(o);
}
else {
PyErr_SetString(PyExc_ValueError,"Sequence elements must be numbers");
return NULL;
}
}
$1 = temp;
}
%typemap(argout) float col[3] {
for (int i = 0; i < $1_dim0; i++) {
PyObject *o = PyFloat_FromDouble((double) $1[i]);
PyList_SetItem($input, i, o);
}
}
clear_color = [114/255.0, 144/255.0, 154/255.0, 0];
imgui.ColorEdit3("clear color", clear_color);
RenderDrawLists の移植
def RenderDrawLists(draw_data):
global g_ShaderHandle
# Avoid rendering when minimized, scale coordinates for retina displays (screen coordinates != framebuffer coordinates)
io = imgui.GetIO();
fb_width = int(io.DisplaySize.x * io.DisplayFramebufferScale.x);
fb_height = int(io.DisplaySize.y * io.DisplayFramebufferScale.y);
if fb_width == 0 or fb_height == 0:
return;
draw_data.ScaleClipRects(io.DisplayFramebufferScale);
# Backup GL state
last_active_texture=glGetIntegerv(GL_ACTIVE_TEXTURE);
glActiveTexture(GL_TEXTURE0);
last_program=glGetIntegerv(GL_CURRENT_PROGRAM);
last_texture=glGetIntegerv(GL_TEXTURE_BINDING_2D);
last_array_buffer=glGetIntegerv(GL_ARRAY_BUFFER_BINDING);
last_element_array_buffer=glGetIntegerv(GL_ELEMENT_ARRAY_BUFFER_BINDING);
last_vertex_array=glGetIntegerv(GL_VERTEX_ARRAY_BINDING);
last_blend_src_rgb=glGetIntegerv(GL_BLEND_SRC_RGB);
last_blend_dst_rgb=glGetIntegerv(GL_BLEND_DST_RGB);
last_blend_src_alpha=glGetIntegerv(GL_BLEND_SRC_ALPHA);
last_blend_dst_alpha=glGetIntegerv(GL_BLEND_DST_ALPHA);
last_blend_equation_rgb=glGetIntegerv(GL_BLEND_EQUATION_RGB);
last_blend_equation_alpha=glGetIntegerv(GL_BLEND_EQUATION_ALPHA);
last_viewport=glGetIntegerv(GL_VIEWPORT);
last_scissor_box=glGetIntegerv(GL_SCISSOR_BOX);
last_enable_blend = glIsEnabled(GL_BLEND);
last_enable_cull_face = glIsEnabled(GL_CULL_FACE);
last_enable_depth_test = glIsEnabled(GL_DEPTH_TEST);
last_enable_scissor_test = glIsEnabled(GL_SCISSOR_TEST);
# Setup render state: alpha-blending enabled, no face culling, no depth testing, scissor enabled
glEnable(GL_BLEND);
glBlendEquation(GL_FUNC_ADD);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
glDisable(GL_CULL_FACE);
glDisable(GL_DEPTH_TEST);
glEnable(GL_SCISSOR_TEST);
# Setup viewport, orthographic projection matrix
glViewport(0, 0, fb_width, fb_height);
ortho_projection = (ctypes.c_float * 16)(
2.0/io.DisplaySize.x, 0.0, 0.0, 0.0,
0.0, 2.0/-io.DisplaySize.y, 0.0, 0.0,
0.0, 0.0, -1.0, 0.0,
-1.0, 1.0, 0.0, 1.0,
);
glUseProgram(g_ShaderHandle);
glUniform1i(g_AttribLocationTex, 0);
glUniformMatrix4fv(g_AttribLocationProjMtx, 1, GL_FALSE, ortho_projection);
glBindVertexArray(g_VaoHandle);
for n in range(draw_data.CmdListsCount):
cmd_list = draw_data.CmdLists[n];
idx_buffer_offset = 0;
glBindBuffer(GL_ARRAY_BUFFER, g_VboHandle);
glBufferData(GL_ARRAY_BUFFER, cmd_list.VtxBuffer.Size * SIZEOF_ImDrawVert, cmd_list.VtxBuffer.Data, GL_STREAM_DRAW);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, g_ElementsHandle);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, cmd_list.IdxBuffer.Size * SIZEOF_ImDrawVert, cmd_list.IdxBuffer.Data, GL_STREAM_DRAW);
for cmd_i in range(cmd_list.CmdBuffer.Size):
pcmd = cmd_list.CmdBuffer[cmd_i];
if pcmd.UserCallback:
pcmd.UserCallback(cmd_list, pcmd);
else:
glBindTexture(GL_TEXTURE_2D, pcmd.TextureId);
glScissor(pcmd.ClipRect.x, (fb_height - pcmd.ClipRect.w), (pcmd.ClipRect.z - pcmd.ClipRect.x), (pcmd.ClipRect.w - pcmd.ClipRect.y));
glDrawElements(GL_TRIANGLES, pcmd.ElemCount, GL_UNSIGNED_SHORT, idx_buffer_offset);
idx_buffer_offset += pcmd.ElemCount;
# Restore modified GL state
glUseProgram(last_program);
glBindTexture(GL_TEXTURE_2D, last_texture);
glActiveTexture(last_active_texture);
glBindVertexArray(last_vertex_array);
glBindBuffer(GL_ARRAY_BUFFER, last_array_buffer);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, last_element_array_buffer);
glBlendEquationSeparate(last_blend_equation_rgb, last_blend_equation_alpha);
glBlendFuncSeparate(last_blend_src_rgb, last_blend_dst_rgb, last_blend_src_alpha, last_blend_dst_alpha);
if (last_enable_blend):
glEnable(GL_BLEND);
else:
glDisable(GL_BLEND);
if (last_enable_cull_face):
glEnable(GL_CULL_FACE);
else:
glDisable(GL_CULL_FACE);
if (last_enable_depth_test):
glEnable(GL_DEPTH_TEST);
else:
glDisable(GL_DEPTH_TEST);
if (last_enable_scissor_test):
glEnable(GL_SCISSOR_TEST);
else:
glDisable(GL_SCISSOR_TEST);
glViewport(last_viewport[0], last_viewport[1], last_viewport[2], last_viewport[3]);
glScissor(last_scissor_box[0], last_scissor_box[1], last_scissor_box[2], last_scissor_box[3]);
getter
配列アクセスできない。
cmd_list = draw_data.CmdLists[n]
ゲッターを実装する
%extend ImDrawData {
ImDrawList\* GetCmdList(int n){
return self->CmdLists[n];
}
}
template の型定義が無い
実装する型を明示する。
%template(ImVectorDrawVert) ImVector<ImDrawVert>;
%template(ImVectorDrawIdx) ImVector<ImDrawIdx>;
%template(ImVectorDrawCmd) ImVector<ImDrawCmd>;
Byte 列を得る
cmd_list.VtxBuffer.Data
%typemap(in, numinputs=0) (unsigned char** out_bytes, int* out_size) (unsigned char *tempP, int tempSize) {
$1 = &tempP;
$2 = &tempSize;
}
%typemap(argout)(unsigned char** out_bytes, int* out_size){
auto b = PyBytes_FromStringAndSize((const char *)_$1, _$2);
if ((!$result) || ($result == Py_None)) {
$result = b;
}
else{
if (!PyTuple_Check($result)) {
// new tuple4
auto t= PyTuple_New(2);
PyTuple_SetItem(t, 0, $result);
PyTuple_SetItem(t, 1, b);
$result=t;
}
else {
// concat
auto head = $result;
auto tail = PyTuple_New(1);
PyTuple_SetItem($result, 0, b);
$result = PySequence_Concat(head, tail);
Py_DECREF(head);
Py_DECREF(tail);
}
}
}
// before
%include "imgui/imgui.h"
// after
%extend ImDrawList {
void GetVtxBufferData(unsigned char **out_bytes, int *out_size){
*out_bytes=(unsigned char *)self->VtxBuffer.Data;
*out_size=self->VtxBuffer.Size \* sizeof(self->VtxBuffer.Data[0]);
}
void GetIdxBufferData(unsigned char **out_bytes, int *out_size){
*out_bytes=(unsigned char *)self->IdxBuffer.Data;
*out_size=self->IdxBuffer.Size \* sizeof(self->IdxBuffer.Data[0]);
}
}
glBindBuffer(GL_ARRAY_BUFFER, g_VboHandle);
glBufferData(GL_ARRAY_BUFFER, cmd_list.VtxBuffer.Size \* SIZEOF_ImDrawVert, cmd_list.GetVtxBufferData(), GL_STREAM_DRAW);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, g_ElementsHandle);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, cmd_list.IdxBuffer.Size \* SIZEOF_Idx, cmd_list.GetIdxBufferData(), GL_STREAM_DRAW);
Shutdown の移植
def Shutdown():
InvalidateDeviceObjects();
imgui.Shutdown();
def InvalidateDeviceObjects():
global g_VaoHandle
global g_VboHandle
global g_ElementsHandle
global g_ShaderHandle
global g_VertHandle
global g_FragHandle
global g_FontTexture
if (g_VaoHandle): glDeleteVertexArrays(1, g_VaoHandle);
if (g_VboHandle): glDeleteBuffers(1, g_VboHandle);
if (g_ElementsHandle): glDeleteBuffers(1, g_ElementsHandle);
g_VaoHandle = g_VboHandle = g_ElementsHandle = 0;
if (g_ShaderHandle and g_VertHandle): glDetachShader(g_ShaderHandle, g_VertHandle);
if (g_VertHandle): glDeleteShader(g_VertHandle);
g_VertHandle = 0;
if (g_ShaderHandle and g_FragHandle): glDetachShader(g_ShaderHandle, g_FragHandle);
if (g_FragHandle): glDeleteShader(g_FragHandle);
g_FragHandle = 0;
if (g_ShaderHandle): glDeleteProgram(g_ShaderHandle);
g_ShaderHandle = 0;
if (g_FontTexture):
glDeleteTextures(1, g_FontTexture);
imgui.GetIO().Fonts.SetTexID(0);
g_FontTexture = 0;
ProcessEvent の移植
def ProcessEvent(event):
global g_MouseWheel
global g_MousePressed
io = imgui.GetIO();
if event.type==SDL_MOUSEWHEEL:
if (event.wheel.y > 0):
g_MouseWheel = 1;
if (event.wheel.y < 0):
g_MouseWheel = -1;
return True;
elif event.type==SDL_MOUSEBUTTONDOWN:
if (event.button.button == SDL_BUTTON_LEFT): g_MousePressed[0] = True;
if (event.button.button == SDL_BUTTON_RIGHT): g_MousePressed[1] = True;
if (event.button.button == SDL_BUTTON_MIDDLE): g_MousePressed[2] = True;
return True;
elif event.type==SDL_TEXTINPUT:
io.AddInputCharactersUTF8(event.text.text);
return True;
elif event.type==SDL_KEYDOWN or event.type==SDL_KEYUP:
key = event.key.keysym.sym & ~SDLK_SCANCODE_MASK;
io.SetKeysDown(key, event.type == SDL_KEYDOWN);
io.KeyShift = ((SDL_GetModState() & KMOD_SHIFT) != 0);
io.KeyCtrl = ((SDL_GetModState() & KMOD_CTRL) != 0);
io.KeyAlt = ((SDL_GetModState() & KMOD_ALT) != 0);
io.KeySuper = ((SDL_GetModState() & KMOD_GUI) != 0);
return True;
return False;
io.KeysDown
セッター
%extend ImGuiIO {
void SetKeysDown(int k, int v)
{
ImGui::GetIO().KeysDown[k]=v;
}
}
かくしてほぼ動くようになった。
実際には新しい Widget を Python から使用するたびに値をやりとりする部分を追加しなければならないが、そこはおいおいやっていく。
VisualStudio のセットアップ
こういうネイティブモジュールの開発ではないとクラッシュの原因を探すのが困難になるが、VisualStudio で普通にアタッチできるので便利。
ひとつの solution に python プロジェクトと、c++の dll プロジェクトを同居させてデバッグできる。
あとで書く