Skip to content

Instantly share code, notes, and snippets.

@Geson-anko
Created December 12, 2025 14:51
Show Gist options
  • Select an option

  • Save Geson-anko/0788752947fa3718168b882a8dc6aeda to your computer and use it in GitHub Desktop.

Select an option

Save Geson-anko/0788752947fa3718168b882a8dc6aeda to your computer and use it in GitHub Desktop.
Rotary Encoderのスリット板svgファイルを作成するスクリプトです。
#!/usr/bin/env python3
"""ロータリーエンコーダのスリットパターンSVG生成(メッシュ化用)
スリット部分が透過(穴)になり、円盤の実体部分のみ描画。
3Dプリント用のメッシュ変換に適した出力。
"""
import math
from dataclasses import dataclass
from enum import Enum
class GrayCodeType(Enum):
STANDARD = "standard"
INVERSE = "inverse"
REFLECTED = "reflected"
BALANCED = "balanced"
class CodeType(Enum):
BINARY = "binary"
GRAY = "gray"
@dataclass
class EncoderConfig:
bits: int = 8
inner_radius: float = 25 # 最内周トラックの内側半径
track_width: float = 12
track_gap: float = 1
center_hole: float = 15
outer_ring_width: float = 3 # 外周リングの幅(実体として残る)
outer_margin: float = 2 # 外周リングとトラックの間隔
msb_outside: bool = True
clockwise: bool = True
start_angle: float = -90
def standard_gray(n: int) -> int:
return n ^ (n >> 1)
def inverse_gray(n: int) -> int:
return ~(n ^ (n >> 1))
def balanced_gray(n: int, bits: int) -> int:
g = n ^ (n >> 1)
shift = n % bits if bits > 0 else 0
mask = (1 << bits) - 1
return ((g << shift) | (g >> (bits - shift))) & mask
def generate_code_sequence(bits: int, code_type: CodeType,
gray_type: GrayCodeType = GrayCodeType.STANDARD) -> list[int]:
divisions = 2 ** bits
sequence = []
for i in range(divisions):
if code_type == CodeType.BINARY:
value = i
else:
if gray_type == GrayCodeType.STANDARD:
value = standard_gray(i)
elif gray_type == GrayCodeType.INVERSE:
value = inverse_gray(i) & ((1 << bits) - 1)
elif gray_type == GrayCodeType.REFLECTED:
value = standard_gray(divisions - 1 - i)
elif gray_type == GrayCodeType.BALANCED:
value = balanced_gray(i, bits)
else:
value = standard_gray(i)
sequence.append(value)
return sequence
def generate_wedge_path(cx: float, cy: float, inner_r: float, outer_r: float,
start_angle: float, end_angle: float) -> str:
"""扇形のSVGパスを生成"""
start_rad = math.radians(start_angle)
end_rad = math.radians(end_angle)
outer_start_x = cx + outer_r * math.cos(start_rad)
outer_start_y = cy + outer_r * math.sin(start_rad)
outer_end_x = cx + outer_r * math.cos(end_rad)
outer_end_y = cy + outer_r * math.sin(end_rad)
inner_start_x = cx + inner_r * math.cos(start_rad)
inner_start_y = cy + inner_r * math.sin(start_rad)
inner_end_x = cx + inner_r * math.cos(end_rad)
inner_end_y = cy + inner_r * math.sin(end_rad)
large_arc = 1 if (end_angle - start_angle) > 180 else 0
return (f"M {outer_start_x:.3f} {outer_start_y:.3f} "
f"A {outer_r:.3f} {outer_r:.3f} 0 {large_arc} 1 {outer_end_x:.3f} {outer_end_y:.3f} "
f"L {inner_end_x:.3f} {inner_end_y:.3f} "
f"A {inner_r:.3f} {inner_r:.3f} 0 {large_arc} 0 {inner_start_x:.3f} {inner_start_y:.3f} Z")
def generate_ring_path(cx: float, cy: float, inner_r: float, outer_r: float) -> str:
"""ドーナツ形状(リング)のパスを生成(evenoddルール用)"""
# 外側の円(時計回り)
outer = (f"M {cx + outer_r:.3f} {cy:.3f} "
f"A {outer_r:.3f} {outer_r:.3f} 0 1 1 {cx - outer_r:.3f} {cy:.3f} "
f"A {outer_r:.3f} {outer_r:.3f} 0 1 1 {cx + outer_r:.3f} {cy:.3f} ")
# 内側の円(反時計回り = 穴)
inner = (f"M {cx + inner_r:.3f} {cy:.3f} "
f"A {inner_r:.3f} {inner_r:.3f} 0 1 0 {cx - inner_r:.3f} {cy:.3f} "
f"A {inner_r:.3f} {inner_r:.3f} 0 1 0 {cx + inner_r:.3f} {cy:.3f} ")
return outer + inner
def generate_encoder_svg(config: EncoderConfig,
code_type: CodeType = CodeType.GRAY,
gray_type: GrayCodeType = GrayCodeType.STANDARD) -> str:
"""メッシュ化用エンコーダSVGを生成(スリット=穴)"""
# サイズ計算
track_total = config.bits * (config.track_width + config.track_gap) - config.track_gap
outer_track_radius = config.inner_radius + track_total
outer_ring_inner = outer_track_radius + config.outer_margin
outer_ring_outer = outer_ring_inner + config.outer_ring_width
size = outer_ring_outer * 2 + 10
cx, cy = size / 2, size / 2
sequence = generate_code_sequence(config.bits, code_type, gray_type)
divisions = len(sequence)
angle_step = 360 / divisions
paths = []
# 外周リング(実体として残る)
ring_path = generate_ring_path(cx, cy, outer_ring_inner, outer_ring_outer)
paths.append(f' <path d="{ring_path}" fill="black" fill-rule="evenodd"/>')
# 各トラック:ビットが0の部分を描画(=実体として残る)
# ビットが1の部分はスリット(穴)
for bit in range(config.bits):
if config.msb_outside:
track_idx = config.bits - 1 - bit
else:
track_idx = bit
inner_r = config.inner_radius + track_idx * (config.track_width + config.track_gap)
outer_r = inner_r + config.track_width
for i, value in enumerate(sequence):
if config.clockwise:
angle_offset = i * angle_step
else:
angle_offset = -i * angle_step
start_angle = config.start_angle + angle_offset
end_angle = start_angle + (angle_step if config.clockwise else -angle_step)
if not config.clockwise:
start_angle, end_angle = end_angle, start_angle
# ビットが0の部分を描画(実体)、1の部分は穴(描画しない)
if not ((value >> bit) & 1):
path = generate_wedge_path(cx, cy, inner_r, outer_r, start_angle, end_angle)
paths.append(f' <path d="{path}" fill="black"/>')
# トラック間のギャップを埋めるリング(オプション:構造強度用)
for track_idx in range(config.bits - 1):
gap_inner = config.inner_radius + (track_idx + 1) * (config.track_width + config.track_gap) - config.track_gap
gap_outer = gap_inner + config.track_gap
gap_ring = generate_ring_path(cx, cy, gap_inner, gap_outer)
paths.append(f' <path d="{gap_ring}" fill="black" fill-rule="evenodd"/>')
# 最内周とセンターホールの間のリング
if config.inner_radius > config.center_hole:
inner_ring = generate_ring_path(cx, cy, config.center_hole, config.inner_radius)
paths.append(f' <path d="{inner_ring}" fill="black" fill-rule="evenodd"/>')
# 外周トラックと外周リングの間のリング
if config.outer_margin > 0:
margin_ring = generate_ring_path(cx, cy, outer_track_radius, outer_ring_inner)
paths.append(f' <path d="{margin_ring}" fill="black" fill-rule="evenodd"/>')
title = f"{code_type.value}"
if code_type == CodeType.GRAY:
title += f" ({gray_type.value})"
# 背景なし、透過SVG
svg = f'''<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" width="{size:.0f}" height="{size:.0f}" viewBox="0 0 {size:.3f} {size:.3f}">
<!-- {title} - {config.bits} bits - for mesh conversion -->
{chr(10).join(paths)}
</svg>'''
return svg
def main():
config = EncoderConfig(
bits=6,
inner_radius=16,
track_width=12,
track_gap=1,
center_hole=15,
outer_ring_width=5,
outer_margin=1,
msb_outside=True,
clockwise=True,
)
variants = [
(CodeType.BINARY, None, "encoder_binary.svg"),
(CodeType.GRAY, GrayCodeType.STANDARD, "encoder_gray_standard.svg"),
(CodeType.GRAY, GrayCodeType.INVERSE, "encoder_gray_inverse.svg"),
(CodeType.GRAY, GrayCodeType.REFLECTED, "encoder_gray_reflected.svg"),
(CodeType.GRAY, GrayCodeType.BALANCED, "encoder_gray_balanced.svg"),
]
for code_type, gray_type, filename in variants:
svg = generate_encoder_svg(config, code_type, gray_type or GrayCodeType.STANDARD)
with open(filename, "w") as f:
f.write(svg)
print(f"Generated: {filename}")
if __name__ == "__main__":
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment