zatsu-plotter
from 20250525週fabnews
#IKeJIのプロジェクト #雑プロッター
CoreXY方式のシンプルなペンプロッター
https://github.com/ikeji/zatsu-plotter
@ikeji: 雨で外出できないので、CoreXY方式のペンプロッタを作った。
ペンプロッタ界のHello Worldは自己相似図形と聞いたのでヒルベルトしてみた。 #自作ペンプロッタを愛でる会 #CoreXY #28BYJ48 #RP2040 #MicroPython
https://pbs.twimg.com/media/GrwX23nW0AAgzKJ.jpg
設計情報
https://blog.ikejima.org/weekly/2025/05/26/2025w21.html
https://gyazo.com/55d113ea6e6e86d9af887d69f81cfcaa
HBOTに対してCoreXYのメリットは軸を直行に保持する必要がない事のはず。
なら、CoreXYなら軸の機構が不要になるはず。
V3からの改造inajob.icon
モーター側プーリーの幅を広げ、タコ糸を2巻きできるようにした
Xフロックにバインダーの長辺に引っ掛けるための段差をつけた
これによりX軸自体がX方向にブレるのを防ぐことができる
課題
ペンホルダーが回転したり浮いたりする
竹ひごに引っ掛けるなどできないか?
ペンホルダーが邪魔で描画している線が見えない
ペンのアップダウン機能が無い
これは個性か?
ペンアップがないのでハッチングで濃淡をつけるinajob.icon
https://gyazo.com/51d64f7d0c6c663a28817bc07cbc7a8a
こんなデータ
@ina_ani: ペンアップが無いのでハッチングで濃淡つけて誤魔化す作戦
https://pbs.twimg.com/media/G8sy467bQAARw_B.jpg
ハッチングはInkscapeのエクステンションを使用
https://inkscape.org/~Moini/%E2%98%85hatch-fill+1
Inkscapeの組み込みのパスエフェクトのハッチングは微妙だった
ドーナツ型のような図形の時に内側が塗りつぶされてしまう問題がある
g-code出力はInkscapeの標準のエクステンションを使った
inajob.iconが色々試したソースコード
code: plotter.py
from machine import Pin
from time import sleep_ms
import math
pin1 = Pin(0,Pin.OUT)
pin2 = Pin(1,Pin.OUT)
pin3 = Pin(2,Pin.OUT)
pin4 = Pin(3,Pin.OUT)
pin5 = Pin(4,Pin.OUT)
pin6 = Pin(5,Pin.OUT)
pin7 = Pin(6,Pin.OUT)
pin8 = Pin(7,Pin.OUT)
class BYJ:
def __init__(self, pins):
self.pins = pins
self.current_phase = 0
def step(self):
self.current_phase = (self.current_phase + 1) % 4
self._update()
def unstep(self):
self.current_phase = (self.current_phase - 1 + 4) % 4
self._update()
def _update(self):
for i in range(4):
self.pinsi.value(i==self.current_phase)
# 左のモータ
byj1 = BYJ(pin1, pin2, pin3, pin4)
# 右のモータ
byj2 = BYJ(pin5, pin6, pin7, pin8)
# ヘッドを左に動かす。
def left(steps):
for i in range(steps):
byj1.step()
byj2.step()
sleep_ms(4)
# ヘッドを右に動かす。
def right(steps):
for i in range(steps):
byj1.unstep()
byj2.unstep()
sleep_ms(4)
# ヘッドを上に動かす。
def up(steps):
for i in range(steps):
byj1.unstep()
byj2.step()
sleep_ms(4)
# ヘッドを下に動かす。
def down(steps):
for i in range(steps):
byj1.step()
byj2.unstep()
sleep_ms(4)
current_x = 0
current_y = 0
# ============================================
# 指定した座標へ線を引く関数 (ブレゼンハム法)
# ============================================
def goto(target_x, target_y):
"""
現在地から目標座標 (target_x, target_y) へ、
ブレゼンハムのアルゴリズムを用いて移動しながら線を描く。
"""
global current_x, current_y
# 終点までの距離の絶対値を計算
dx = abs(target_x - current_x)
dy = abs(target_y - current_y)
# 移動方向の決定 (正方向なら1, 負方向なら-1)
sx = 1 if current_x < target_x else -1
sy = 1 if current_y < target_y else -1
# 誤差項の初期値
err = dx - dy
while True:
# 現在地が目標に到達したら終了
if current_x == target_x and current_y == target_y:
break
e2 = 2 * err
# --- X軸方向への移動判定 ---
# 誤差項の2倍が -dy より大きい場合、X方向へ1単位進む
if e2 > -dy:
err -= dy
if sx > 0:
right(1)
current_x += 1
else:
left(1)
current_x -= 1
# X移動後に目標に到達したかチェック(行き過ぎ防止)
if current_x == target_x and current_y == target_y:
break
# --- Y軸方向への移動判定 ---
# 誤差項の2倍が dx より小さい場合、Y方向へ1単位進む
# (注: 斜め移動の場合はXとYの両方のif文が実行されます)
if e2 < dx:
err += dx
if sy > 0:
up(1)
current_y += 1
else:
down(1)
current_y -= 1
# 四角を描画
def square(l):
up(l)
left(l)
down(l)
right(l)
def draw_star(center_x, center_y, radius):
"""
中心 (center_x, center_y)、半径 radius の五芒星を描く。
"""
points = []
# 1. 五芒星の5つの頂点の座標を計算する
# 上の頂点から始めて、時計回りに72度ずつ角度を変えて計算します。
for i in range(5):
# 角度の計算: スタートは90度(真上)、そこから72度(360/5)ずつ引く
angle_deg = 90 - (i * 72)
angle_rad = math.radians(angle_deg) # ラジアンに変換
# 三角関数で座標を計算 (ブレゼンハム用に整数に丸める)
x = center_x + radius * math.cos(angle_rad)
y = center_y + radius * math.sin(angle_rad)
points.append((round(x), round(y)))
# 2. 一筆書きの順番で頂点を結ぶ
# 順番: 頂点1 → 3 → 5 → 2 → 4 → 1 (インデックスは0始まり)
draw_order = 0, 2, 4, 1, 3, 0
print(f"--- 星の描画開始 中心:({center_x},{center_y}), 半径:{radius} ---")
# 最初の頂点へ移動 (現在地から線が引かれます)
start_idx = draw_order0
goto(pointsstart_idx0, pointsstart_idx1)
# 残りの頂点を順番に巡る
for i in range(1, len(draw_order)):
idx = draw_orderi
# 次の頂点の絶対座標を指定してgoto
goto(pointsidx0, pointsidx1)
print("--- 星の描画終了 ---")
# アルファベット全26文字の一筆書きデータ
# 各文字の (0,0) を基準とした相対座標リスト
FONT_DATA = {
'A': (0,0), (2,5), (4,0), (3,2), (1,2), (3,2), (4,0), (6,0),
'B': (0,0), (0,5), (3,5), (4,4), (4,3), (3,2), (0,2), (3,2), (4,1), (4,0), (0,0), (6,0),
'C': (4,5), (0,5), (0,0), (4,0), (6,0),
'D': (0,0), (0,5), (3,5), (4,3), (4,2), (3,0), (0,0), (6,0),
'E': (4,5), (0,5), (0,2), (3,2), (0,2), (0,0), (4,0), (6,0),
'F': (4,5), (0,5), (0,2), (3,2), (0,2), (0,0), (6,0),
'G': (4,5), (0,5), (0,0), (4,0), (4,2), (2,2), (4,2), (4,0), (6,0),
'H': (0,0), (0,5), (0,2), (4,2), (4,5), (4,0), (6,0),
'I': (0,5), (4,5), (2,5), (2,0), (0,0), (4,0), (6,0),
'J': (0,1), (1,0), (3,0), (4,1), (4,5), (4,0), (6,0),
'K': (0,0), (0,5), (0,2), (4,5), (0,2), (4,0), (6,0),
'L': (0,5), (0,0), (4,0), (6,0),
'M': (0,0), (0,5), (2,3), (4,5), (4,0), (6,0),
'N': (0,0), (0,5), (4,0), (4,5), (4,0), (6,0),
'O': (0,0), (0,5), (4,5), (4,0), (0,0), (6,0),
'P': (0,0), (0,5), (4,5), (4,2), (0,2), (0,0), (6,0),
'Q': (0,0), (0,5), (4,5), (4,1), (3,1), (4,0), (3,1), (0,0), (6,0),
'R': (0,0), (0,5), (4,5), (4,2), (0,2), (4,0), (6,0),
'S': (4,5), (0,5), (0,2), (4,2), (4,0), (0,0), (6,0),
'T': (0,5), (4,5), (2,5), (2,0), (6,0),
'U': (0,5), (0,0), (4,0), (4,5), (4,0), (6,0),
'V': (0,5), (2,0), (4,5), (4,0), (6,0),
'W': (0,5), (0,0), (2,2), (4,0), (4,5), (4,0), (6,0),
'X': (0,5), (4,0), (2,2), (0,0), (2,2), (4,5), (4,0), (6,0),
'Y': (0,5), (2,2), (4,5), (2,2), (2,0), (6,0),
'Z': (0,5), (4,5), (0,0), (4,0), (6,0),
' ': (6,0) # スペース
}
def draw_text(text, start_x, start_y, scale=1):
"""
指定した文字列を一筆書きで描画する
"""
# 最初の文字の開始位置へ
goto(start_x, start_y)
current_base_x = start_x
current_base_y = start_y
for char in text.upper():
if char in FONT_DATA:
points = FONT_DATAchar
for dx, dy in points:
# 座標をスケールに合わせて計算
tx = current_base_x + int(dx * scale)
ty = current_base_y + int(dy * scale)
# ブレゼンハムのgotoで移動(描画)
goto(tx, ty)
# 1文字終わるごとに、次の文字の開始基準点を更新
# 各文字のデータ末尾が (6,0) なので、6単位分右へズレる
current_base_x += int(6 * scale)
def _get_value(line, char):
"""'X10.5' のような文字列から数値部分を抽出する"""
idx = line.find(char)
if idx == -1:
return None
# 記号の後の数字、小数点、マイナス記号を拾う
start = idx + 1
end = start
while end < len(line) and (lineend.isdigit() or lineend in '.-+'):
end += 1
try:
return float(linestart:end)
except:
return None
def run_gcode_file(filename, scale=1.0, offset_x=0, offset_y=0):
global current_x, current_y
try:
with open(filename, 'r') as f:
for line in f:
# 1. 前後の空白削除と大文字化、コメント(;)の除去
line = line.split(';')0.split('(')0.upper().strip()
if not line:
continue
# 2. X, Y 座標の抽出
new_x = _get_value(line, 'X')
new_y = _get_value(line, 'Y')
# 3. 座標が指定されている場合のみ移動
if new_x is not None or new_y is not None:
target_x = current_x
target_y = current_y
if new_x is not None:
target_x = int(new_x * scale) + offset_x
if new_y is not None:
target_y = int(new_y * scale) + offset_y
goto(target_x, target_y)
except OSError:
print("Error: File not found")
run_gcode_file("test.gcode",scale=20)
コア部分を別ファイルにして、サンプル集に変えた方がいいかな?ikeji.icon