Created
December 12, 2025 14:51
-
-
Save Geson-anko/0788752947fa3718168b882a8dc6aeda to your computer and use it in GitHub Desktop.
Rotary Encoderのスリット板svgファイルを作成するスクリプトです。
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/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