3Dプリンタで使われるGコードをpythonで再現してみた

# はじめに

とあるdiscordのコミュニティで3DプリンタのGコードを解読したいって話が出たので、
pythonで試しに解読してみた。

# Gコードの仕様について

Gコードについて調べてみたところ、下記のサイトが見つかった。

https://reprap.org/wiki/G-code

今回は単純に出力される形を見たいだけなので、G0とG1だけ見れば良さそうだ。
G0とG1はその後ろにX,Y,Zから始まる数値が出ていればその軸をその数値に合わせるようだ。

例えば、G1 X0 Z1 だったら、Y軸は触らずに(X, Z) = (0, 1)に移動する。移動量は前の座標次第となっている。
座標の位置を指定されていない限りはずっと同じ数値になるから一個以上前の情報を持っていなければ注意。

難易度的にはAtCoderのABCのC問題みたいな、「アルゴリズムとか考えなくていいから愚直に実装すればOK」のレベルだと感じた。

# pythonコード

愚直に実装してみたのが次のコード。
他人のデータを使っていたので、出力結果は出せません。

<details><summary>長めなので折りたたみ</summary>
<div>

```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()

```

</div>
</details>

## 使い方

```python
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文解析する。

```py

@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文ごとに解析するのが楽。
ファイルを返すジェネレータ→コマンドで解析させるジェネレータ→制限をかけるジェネレータと三層構造にした。

```python

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だけでは重かったから。