はじめに
とあるdiscordのコミュニティで3DプリンタのGコードを解読したいって話が出たので、
pythonで試しに解読してみた。
Gコードの仕様について
Gコードについて調べてみたところ、下記のサイトが見つかった。
今回は単純に出力される形を見たいだけなので、G0とG1だけ見れば良さそうだ。
G0とG1はその後ろにX,Y,Zから始まる数値が出ていればその軸をその数値に合わせるようだ。
例えば、G1 X0 Z1 だったら、Y軸は触らずに(X, Z) = (0, 1)に移動する。移動量は前の座標次第となっている。
座標の位置を指定されていない限りはずっと同じ数値になるから一個以上前の情報を持っていなければ注意。
難易度的にはAtCoderのABCのC問題みたいな、「アルゴリズムとか考えなくていいから愚直に実装すればOK」のレベルだと感じた。
pythonコード
愚直に実装してみたのが次のコード。
他人のデータを使っていたので、出力結果は出せません。
長めなので折りたたみ
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
import re
from typing import Generator, Optional
from dataclasses import dataclass
import copy
@dataclass()
class Position:
x: float = 0.0
y: float = 0.0
z: float = 0.0
@dataclass()
class MoveCommand:
pos: Position = Position(0.0, 0.0, 0.0)
print: bool = False
use_x: bool = False
use_y: bool = False
use_z: bool = False
def set_x(self, value: str):
self.pos.x = float(value)
self.use_x = True
def set_y(self, value: str):
self.pos.y = float(value)
self.use_y = True
def set_z(self, value: str):
self.pos.z = float(value)
self.use_z = True
def move_from(self, last_pos: Position) -> Position:
result = copy.copy(last_pos)
if self.use_x:
result.x = self.pos.x
if self.use_y:
result.y = self.pos.y
if self.use_z:
result.z = self.pos.z
return result
MOVE_COMMAND_PATTERN = re.compile("G[01]")
def get_command(cells: list[str]) -> Optional[MoveCommand]:
if not(len(cells)):
return None
if not MOVE_COMMAND_PATTERN.match(cells[0]):
return None
command_result = MoveCommand()
command_result.print = cells[0][1] == "1"
for value in cells[1:]:
value_text = value[1:]
if value[0] == "X":
command_result.set_x(value_text)
elif value[0] == "Y":
command_result.set_y(value_text)
elif value[0] == "Z":
command_result.set_z(value_text)
return command_result
def get_lines(file_path: str) -> Generator[list[str], None, None]:
with open(file_path) as f:
for line in f:
yield line.rstrip().split()
def get_all_line(file_path: str) -> Generator[tuple[Position, Position], None, None]:
last_pos = Position(0, 0, 0)
for command in map(get_command, get_lines(file_path)):
if command is None:
continue
next_pos = command.move_from(last_pos)
if command.print:
yield last_pos, next_pos
last_pos = next_pos
def get_layer(file_path: str, min_z: float, max_z: Optional[float] = None)\
-> Generator[tuple[Position, Position], None, None]:
if max_z is None:
min_z = max_z
for start_pos, end_pos in get_all_line(file_path):
if min_z <= start_pos.y <= max_z and min_z <= end_pos.z <= max_z:
yield start_pos, end_pos
if __name__ == '__main__':
fig = plt.figure()
axis = Axes3D(fig)
for start, last in get_all_line("sample.txt"):
axis.plot([start.x, last.x], [start.y, last.y], [start.z, last.z], "g-")
plt.show()
使い方
if __name__ == '__main__':
fig = plt.figure()
axis = Axes3D(fig)
for start, last in get_all_line("sample.txt"):
axis.plot([start.x, last.x], [start.y, last.y], [start.z, last.z], "g-")
plt.show()
上記のところでmatplotlibで3次元のグラフを作って表示できるようにした。
かなり重かったから、全部のプリント用のGコードを線で表すのではなく、
連続して吐出していたらつなげるようにしたほうが良かったかも。
ポイント
座標の格納方法
座標の格納方法とGコードの解読をまとめたのが下記の部分。
get_command(line.rstlip().split())とでもすればGコードを1文解析する。
@dataclass()
class Position:
x: float = 0.0
y: float = 0.0
z: float = 0.0
@dataclass()
class MoveCommand:
pos: Position = Position(0.0, 0.0, 0.0)
print: bool = False
use_x: bool = False
use_y: bool = False
use_z: bool = False
def set_x(self, value: str):
self.pos.x = float(value)
self.use_x = True
def set_y(self, value: str):
self.pos.y = float(value)
self.use_y = True
def set_z(self, value: str):
self.pos.z = float(value)
self.use_z = True
def move_from(self, last_pos: Position) -> Position:
result = copy.copy(last_pos)
if self.use_x:
result.x = self.pos.x
if self.use_y:
result.y = self.pos.y
if self.use_z:
result.z = self.pos.z
return result
MOVE_COMMAND_PATTERN = re.compile("G[01]")
def get_command(cells: list[str]) -> Optional[MoveCommand]:
if not(len(cells)):
return None
if not MOVE_COMMAND_PATTERN.match(cells[0]):
return None
command_result = MoveCommand()
command_result.print = cells[0][1] == "1"
for value in cells[1:]:
value_text = value[1:]
if value[0] == "X":
command_result.set_x(value_text)
elif value[0] == "Y":
command_result.set_y(value_text)
elif value[0] == "Z":
command_result.set_z(value_text)
return command_result
ファイルの解析
テキストファイルを1文ごとに解析する場合、解析する関数を作ってからジェネレータを使って1文ごとに解析するのが楽。
ファイルを返すジェネレータ→コマンドで解析させるジェネレータ→制限をかけるジェネレータと三層構造にした。
def get_lines(file_path: str) -> Generator[list[str], None, None]:
with open(file_path) as f:
for line in f:
yield line.rstrip().split()
def get_all_line(file_path: str) -> Generator[tuple[Position, Position], None, None]:
last_pos = Position(0, 0, 0)
for command in map(get_command, get_lines(file_path)):
if command is None:
continue
next_pos = command.move_from(last_pos)
if command.print:
yield last_pos, next_pos
last_pos = next_pos
def get_layer(file_path: str, min_z: float, max_z: Optional[float] = None)\
-> Generator[tuple[Position, Position], None, None]:
if max_z is None:
min_z = max_z
for start_pos, end_pos in get_all_line(file_path):
if min_z <= start_pos.y <= max_z and min_z <= end_pos.z <= max_z:
yield start_pos, end_pos
なお、get_layerを作ったのはget_all_lineだけでは重かったから。