MagicaVoxelの.voxファイルを読む

https://github.com/ephtracy/voxel-model/blob/master/MagicaVoxel-file-format-vox.txt を参考にしてMagicaVoxelの.vox形式のファイルをスクリプトで読んだ。 少ないバイト数でreadを呼ぶのはシステムコールを多用していて遅そうだったり、PACKというチャンクの実装をスキップしていたり、改善の余地はまだまだあるがバイナリファイルにも手出し出来そうだと分かった。

from io import BufferedReader
from typing import Tuple

# reference: https://github.com/ephtracy/voxel-model/blob/master/MagicaVoxel-file-format-vox.txt

VOX_HEADER = b"VOX "
VOX_VERSION_NUMBER = 150


def main():
    file = "./blue.vox"
    with open(file, "rb") as f:
        header = f.read(4)
        assert header == VOX_HEADER
        version: int = int.from_bytes(f.read(4), byteorder="little")
        assert version == VOX_VERSION_NUMBER

        while True:
            chunk_id = f.read(4)
            if len(chunk_id) == 0:
                exit()
            if chunk_id == b"MAIN":
                read_chunk_header(f)
            elif chunk_id == b"SIZE":
                bytes_of_chunk_content, _ = read_chunk_header(f)
                size_bytes = f.read(bytes_of_chunk_content)
                assert len(size_bytes) == 12
                size_x = size_bytes[:4]
                size_y = size_bytes[4:8]
                size_z = size_bytes[8:12]
                print(size_x, size_y, size_z)
            elif chunk_id == b"XYZI":
                bytes_of_chunk_content, _ = read_chunk_header(f)
                num_voxels = int.from_bytes(f.read(4), byteorder="little")
                assert bytes_of_chunk_content == num_voxels * 4 + 4
                for _ in range(num_voxels):
                    x, y, z, i = f.read(4)
                    print(x, y, z, i)
            elif chunk_id == b"RGBA":
                read_chunk_header(f)
                for _ in range(256):
                    r, g, b, a = f.read(4)
                    print(r, g, b, a)

            elif chunk_id == b"PACK":
                raise NotImplementedError()
            else:
                print(f"unknown Chunk {chunk_id}")
                bytes_of_chunk_content, bytes_of_children_chunks = read_chunk_header(f)
                f.read(bytes_of_chunk_content)
                f.read(bytes_of_children_chunks)


def read_chunk_header(f: BufferedReader) -> Tuple[int, int]:
    bytes_of_chunk_content = int.from_bytes(f.read(4), byteorder="little")
    bytes_of_children_chunks = int.from_bytes(f.read(4), byteorder="little")
    return (bytes_of_chunk_content, bytes_of_children_chunks)


if __name__ == "__main__":
    main()

手元の自作pythonライブラリを競プロでも活用したいのでツールを作った

概要

競プロなど単一のファイル提出が求められるケースに対応するために、手元の複数ファイルにまたがるpythonコードを組み合わせて単一の提出ファイルを作成するコマンドラインツールsinglueを作成しています。 2022年9月21日時点のバージョン(v0.1.5)での実行方法について紹介します。

github.com

背景

手元のディレクトリ内で定義されている関数やクラスをimportしながら作成したプログラムをオンラインジャッジの環境に提出すると手元でしか定義されていないので当然エラーが出ます。 そのため提出するファイルに、コピペしたり、あらかじめ使うであろうものを記述しておいたり、スニペットとして展開できるようにしたりといった方法があるかと思います。 いずれにおいても手動のオペレーションでミスする、量の多いテンプレートの見た目に圧倒される、スニペットのメンテナンスをミスする(改良した部分がスニペットには反映されていない)、といった経験があるのではないでしょうか(私は3つともありました)。

そこで1ファイルが巨大にならないようにライブラリとして整備しつつ、コピペミスによるエラー、実行環境の差異によるエラーが起こらないようにsinglueというコマンドラインツールを作成しました。

イメージ

概念を図で表すと以下のようなイメージです。

手元のファイル群(青枠で囲った部分)から提出用ファイル(赤枠部分、output.py)を生成します。

使用方法

インストール方法、実行例について説明します。

インストール方法

astモジュールにpython3.9以降に追加されたunparseに依存しているためpython3.9以降の環境が必要になります。 この環境が既にあればpip install singlueで事足りますが、無い場合は以下の手順でインストール出来るかと思います。

  • pyenvでpython3.9を追加
  • pipxをインストール
  • pipx install singlue

実行例

前提

以下のようなディレクトリ構造を仮定します。

$ tree
.
├── library.py
└── main.py

また各ファイルの内容は以下のものとします。

main.py

from library import one, two, Three


assert one() + two() == Three().three()

library.py

def one() -> int:
    return 1


def two() -> int:
    return 2


class Three:
    def __init__(self):
        self.value = 3

    def three(self):
        return self.value

実行コマンド

singlue main.py > output.py

上のコマンドを実行すると以下の内容のoutput.pyというファイルが作成されます。 ジャッジ環境において解決できずエラーを起こすimport文が削除され、代わりにimportする対象のクラスや関数が埋め込まれているのが確認出来ます。

なお、説明の都合上ファイル名をmain.pylibrary.pyとしていますがこの命名である必要はないです。 またライブラリとしているファイルはmain.pyと同一のディレクトリ階層にあるという制約はありますがlibrary.pyに類するファイルが複数であっても動きます。

output.py

def one() -> int:
    return 1
def two() -> int:
    return 2
class Three:

    def __init__(self):
        self.value = 3

    def three(self):
        return self.value
assert one() + two() == Three().three()

課題

混み入った依存関係が必要になるケースは少ないのではないかと感じていますが、多段の依存関係の解決(ファイル内、ファイル間)が出来ないのが現在の課題です。

最後に

コンテストにぶっつけ本番で使用することはおすすめしません。日々の精進等で試していただけると嬉しいです。

課題等を報告する場合はGitHubのIssues(https://github.com/kawagh/singlue/issues)にてお願いします。

pipxでpython3.9依存のツールを利用できるようにした

背景

python3.8がシステムのデフォルトの環境だがpython3.9に依存する自作CLIツール(https://github.com/kawagh/singlue)をグローバルに扱いたかった。

前提の環境

  • OS
$ uname -sv
Linux #49~20.04.1-Ubuntu SMP Thu Aug 4 19:15:44 UTC 2022

方法

  • python3.9aptでインストール
  • pipxで依存するpythonのバージョンを指定してツールをインストール
sudo apt install python3.9
sudo apt install python3.9-venv
pipx install singlue --python python3.9

その他

ツールの開発時はpyenvpython3.9を指定していたのでそれをpipxの方で参照しても良かったのかもしれない。今ではaptpyenvそれぞれでインストールしたpython3.9が存在しているが、手軽さを優先した。

テストの存在するディレクトリを指定してpytestの実行を速める

多くのファイルを含むディレクトリが存在するとpytestがその中も探していてテストの完了が遅くなっていた。 poetryでプロジェクトを管理しているのでpyproject.tomlに以下のようにテストの存在するパスを明示すると遅くなっていたのが解決した。

[tool.pytest.ini_options]
testpaths= ["tests"]

他のconfigファイルの場合の記載例もドキュメントに記述されている。

参考

Configuration — pytest documentation

vimの選択範囲のテキストを外部コマンドに渡す

選択範囲のテキストをレジスタを経由してvimscriptの変数とし、その変数の出力を一時ファイルにリダイレクトして外部コマンドを実行することで実現できる。

その一例として、VisualモードでDOT言語のコードを選択した状態で以下の関数を呼び出して表現されるグラフの画像を取得できる。

function! MakeDotGraphFromSelected()
    let tmp = @@
    silent normal gvy
    redir! > tmp.dot
    let selected = @@
    silent echo selected
    redir END
    call system('dot -Tpng -o tmp.png tmp.dot')
    call delete('tmp.dot')
    let @@ = tmp
endfunction

参考

Androidアプリ内チュートリアル作成の取り組み

新規でアプリをインストールした際に下のように画面を表示できるようにしたいと思っています。

実装手順

  • LaunchedEffect()とdelay()でmutableState(チュートリアルを画像の集まりと考えてsceneと呼んでいる)を一定時間おきに進める
  • そのmutableStateを提供するcompostionLocalを作成する
  • ハイライトやテキストを付加させたいcomposableのmodifierをcompositionLocalの値に応じて渡す(以下にそのコード片を示す)
val textPaint = Paint().asFrameworkPaint().apply {
        textSize = 50.sp.value
        color = android.graphics.Color.BLACK
    }

val modifierForButton:Modifier =
    if (isTutorial && LocalScene.current == 0)
        Modifier.drawBehind {
            drawCircle(
                Color.LightGray, this.size.maxDimension,
                alpha = 0.5f
            )
            drawIntoCanvas { canvas: Canvas ->
                canvas.nativeCanvas.drawText(
                    "description1",
                    this.center.x,
                    this.center.y + 90,
                    textPaint
                )
            }
        }
    else Modifier

改善したい点

  • 条件分岐に応じてmodfierForButtonなどといった定義を要すること。

当初はModifierの拡張関数としてModifier.explainedBy(scene:Int):Modifierなどと定義して即座にcomposableに渡せたらと思っていたが、そのようにしてcompositionLocalの値を読もうとすると@Composable invocations can only happen from the context of a @Composable functionと言われてしまい出来なかった。