yaplotlib 0.1.2__tar.gz → 1.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- yaplotlib-1.0.0/PKG-INFO +129 -0
- yaplotlib-1.0.0/README.md +111 -0
- yaplotlib-1.0.0/pyproject.toml +24 -0
- yaplotlib-1.0.0/yaplotlib/__init__.py +61 -0
- yaplotlib-1.0.0/yaplotlib/_writer.py +356 -0
- yaplotlib-1.0.0/yaplotlib/core.py +106 -0
- yaplotlib-1.0.0/yaplotlib/document.py +162 -0
- yaplotlib-1.0.0/yaplotlib/elements.py +366 -0
- yaplotlib-1.0.0/yaplotlib/frame.py +365 -0
- yaplotlib-1.0.0/yaplotlib/globals.py +62 -0
- yaplotlib-1.0.0/yaplotlib/palettes.py +87 -0
- yaplotlib-1.0.0/yaplotlib/spec.py +81 -0
- yaplotlib-0.1.2/PKG-INFO +0 -15
- yaplotlib-0.1.2/setup.cfg +0 -10
- yaplotlib-0.1.2/setup.py +0 -34
- yaplotlib-0.1.2/yaplotlib.egg-info/PKG-INFO +0 -15
- yaplotlib-0.1.2/yaplotlib.egg-info/SOURCES.txt +0 -7
- yaplotlib-0.1.2/yaplotlib.egg-info/dependency_links.txt +0 -1
- yaplotlib-0.1.2/yaplotlib.egg-info/top_level.txt +0 -1
- yaplotlib-0.1.2/yaplotlib.py +0 -75
yaplotlib-1.0.0/PKG-INFO
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: yaplotlib
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: A tiny library to generate Yaplot file.
|
|
5
|
+
License: MIT
|
|
6
|
+
Author: vitroid
|
|
7
|
+
Author-email: vitroid@gmail.com
|
|
8
|
+
Requires-Python: >=3.11,<4.0
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
15
|
+
Requires-Dist: numpy (>=1.24)
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
|
|
18
|
+
## yaplotlib
|
|
19
|
+
|
|
20
|
+
Yaplot 形式の描画データを Python から作るライブラリです。
|
|
21
|
+
|
|
22
|
+
描画コマンドは種類(`line` や `circle` など)と座標の列で表します。種類ごとの違いはライブラリ内部の表にまとめてあり、呼び出し側は同じ `add` を使います。複数フレームと、ファイル先頭の `global` ブロック、画面に固定する SVG は `YaplotDocument` が持ちます。
|
|
23
|
+
|
|
24
|
+
## インストール
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
pip install yaplotlib
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
numpy が必要です。
|
|
31
|
+
|
|
32
|
+
## クイックスタート
|
|
33
|
+
|
|
34
|
+
```python
|
|
35
|
+
import numpy as np
|
|
36
|
+
from yaplotlib import YaplotDocument
|
|
37
|
+
|
|
38
|
+
doc = YaplotDocument()
|
|
39
|
+
doc.globals.fov = 30
|
|
40
|
+
doc.globals.radius = 0.3
|
|
41
|
+
|
|
42
|
+
xyz = np.array([[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]])
|
|
43
|
+
path = np.array([[0.0, 0.0, 0.0], [0.5, 0.2, 0.0], [1.0, 0.0, 0.0]])
|
|
44
|
+
|
|
45
|
+
for t in range(len(path)):
|
|
46
|
+
frame = doc.new_frame()
|
|
47
|
+
frame.add_many("circle", xyz, color=3)
|
|
48
|
+
frame.add("stick", (0, 0, 0), (1, 0, 0), arrow=1, color=4)
|
|
49
|
+
if t >= 1:
|
|
50
|
+
# polyline は、Yaplot が受け付けるようになるまで線分に分解して出力する
|
|
51
|
+
frame.add("polyline", *path[: t + 1], color=5)
|
|
52
|
+
frame.add("text", (0, 0, 0), text=f"t = {t}")
|
|
53
|
+
|
|
54
|
+
doc.save("movie.yap")
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
`color`、`layer`、`radius`、`arrow` を省略すると、その項目は出力されません。直前の値を引き継ぐので、色を指定した要素の後ろに置いた色なしの要素は、その色で描かれます。
|
|
58
|
+
|
|
59
|
+
## global ブロック
|
|
60
|
+
|
|
61
|
+
`doc.globals` に置いた値は、すべてのフレームの初期状態になります。何も設定しなければ `global` ブロックは出力しません。
|
|
62
|
+
|
|
63
|
+
- 見た目: `layer`, `color`, `radius`, `arrow`, `palettes`
|
|
64
|
+
- 表示の初期状態: `reality`(0..4), `splat`(0..3), `fov`, `verbose`(0..3), `thickness`, `rotation`(1〜3 個の数値), `fps`, `hide`, `show`, `messages`
|
|
65
|
+
- 全フレーム共通の SVG: `add_svg`
|
|
66
|
+
|
|
67
|
+
```python
|
|
68
|
+
doc.globals.reality = 3
|
|
69
|
+
doc.globals.palettes[3] = (255, 100, 100)
|
|
70
|
+
doc.globals.messages.append("loaded")
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## SVG
|
|
74
|
+
|
|
75
|
+
画面に固定する 2D の情報(見出し、凡例、グラフ)は SVG のまま渡します。`global` に置くと全フレーム、フレームに置くとそのフレームだけに出ます。matplotlib の Figure も渡せます(背景は透明になります)。
|
|
76
|
+
|
|
77
|
+
```python
|
|
78
|
+
frame.add_svg('<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">'
|
|
79
|
+
'<text x="16" y="32" fill="white">t = 0</text></svg>')
|
|
80
|
+
frame.add_svg(figure) # matplotlib の Figure
|
|
81
|
+
doc.globals.add_svg("hud.svg") # ファイル
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## まとめて追加する
|
|
85
|
+
|
|
86
|
+
`add_many` は座標を ndarray のまま保持します。形は「個数 N × 1 個あたりの座標数 × 3」です。
|
|
87
|
+
|
|
88
|
+
| 種類 | 形 |
|
|
89
|
+
|---|---|
|
|
90
|
+
| `circle`, `ring`, `triangle`, `square`, `pentagon`, `hexagon`, `text` | `(N, 3)` |
|
|
91
|
+
| `line`, `stick` | `(N, 2, 3)` |
|
|
92
|
+
| `polygon`, `polyline`(頂点数がそろうとき) | `(N, m, 3)` |
|
|
93
|
+
|
|
94
|
+
`line` の始点と終点が別の配列なら `np.stack([starts, ends], axis=1)` で重ねます。`text` には長さ N の `texts` も渡します。
|
|
95
|
+
|
|
96
|
+
```python
|
|
97
|
+
starts = np.zeros((10, 3))
|
|
98
|
+
ends = np.ones((10, 3))
|
|
99
|
+
frame.add_many("line", np.stack([starts, ends], axis=1), color=3)
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## パレット
|
|
103
|
+
|
|
104
|
+
```python
|
|
105
|
+
from yaplotlib.palettes import rainbow_colors
|
|
106
|
+
|
|
107
|
+
doc.globals.palettes.update(enumerate(rainbow_colors(8), start=10))
|
|
108
|
+
frame.palette(3, (255, 96, 96))
|
|
109
|
+
frame.palettes(rainbow_colors(8), offset=10)
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
`random_colors` と `gradation_colors` も同じように RGB のリストを返します。
|
|
113
|
+
|
|
114
|
+
## 以前の API
|
|
115
|
+
|
|
116
|
+
モジュールの関数(`Line`, `Circle`, `Color`, `SetPalette` など)と、`Line` や `Color` のような大文字のメソッドは、`DeprecationWarning` を出して残してあります。`Yaplot` は 1 フレームだけの `YaplotFrame` の別名です。
|
|
117
|
+
|
|
118
|
+
```python
|
|
119
|
+
from yaplotlib import Yaplot
|
|
120
|
+
|
|
121
|
+
drawing = Yaplot()
|
|
122
|
+
drawing.Points([(0, 0, 0), (1, 0, 0)], color=2, size=0.3)
|
|
123
|
+
drawing.save("example.yaplot")
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
## ライセンス
|
|
127
|
+
|
|
128
|
+
本リポジトリの `LICENSE` ファイルを参照してください。
|
|
129
|
+
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
## yaplotlib
|
|
2
|
+
|
|
3
|
+
Yaplot 形式の描画データを Python から作るライブラリです。
|
|
4
|
+
|
|
5
|
+
描画コマンドは種類(`line` や `circle` など)と座標の列で表します。種類ごとの違いはライブラリ内部の表にまとめてあり、呼び出し側は同じ `add` を使います。複数フレームと、ファイル先頭の `global` ブロック、画面に固定する SVG は `YaplotDocument` が持ちます。
|
|
6
|
+
|
|
7
|
+
## インストール
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pip install yaplotlib
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
numpy が必要です。
|
|
14
|
+
|
|
15
|
+
## クイックスタート
|
|
16
|
+
|
|
17
|
+
```python
|
|
18
|
+
import numpy as np
|
|
19
|
+
from yaplotlib import YaplotDocument
|
|
20
|
+
|
|
21
|
+
doc = YaplotDocument()
|
|
22
|
+
doc.globals.fov = 30
|
|
23
|
+
doc.globals.radius = 0.3
|
|
24
|
+
|
|
25
|
+
xyz = np.array([[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]])
|
|
26
|
+
path = np.array([[0.0, 0.0, 0.0], [0.5, 0.2, 0.0], [1.0, 0.0, 0.0]])
|
|
27
|
+
|
|
28
|
+
for t in range(len(path)):
|
|
29
|
+
frame = doc.new_frame()
|
|
30
|
+
frame.add_many("circle", xyz, color=3)
|
|
31
|
+
frame.add("stick", (0, 0, 0), (1, 0, 0), arrow=1, color=4)
|
|
32
|
+
if t >= 1:
|
|
33
|
+
# polyline は、Yaplot が受け付けるようになるまで線分に分解して出力する
|
|
34
|
+
frame.add("polyline", *path[: t + 1], color=5)
|
|
35
|
+
frame.add("text", (0, 0, 0), text=f"t = {t}")
|
|
36
|
+
|
|
37
|
+
doc.save("movie.yap")
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
`color`、`layer`、`radius`、`arrow` を省略すると、その項目は出力されません。直前の値を引き継ぐので、色を指定した要素の後ろに置いた色なしの要素は、その色で描かれます。
|
|
41
|
+
|
|
42
|
+
## global ブロック
|
|
43
|
+
|
|
44
|
+
`doc.globals` に置いた値は、すべてのフレームの初期状態になります。何も設定しなければ `global` ブロックは出力しません。
|
|
45
|
+
|
|
46
|
+
- 見た目: `layer`, `color`, `radius`, `arrow`, `palettes`
|
|
47
|
+
- 表示の初期状態: `reality`(0..4), `splat`(0..3), `fov`, `verbose`(0..3), `thickness`, `rotation`(1〜3 個の数値), `fps`, `hide`, `show`, `messages`
|
|
48
|
+
- 全フレーム共通の SVG: `add_svg`
|
|
49
|
+
|
|
50
|
+
```python
|
|
51
|
+
doc.globals.reality = 3
|
|
52
|
+
doc.globals.palettes[3] = (255, 100, 100)
|
|
53
|
+
doc.globals.messages.append("loaded")
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## SVG
|
|
57
|
+
|
|
58
|
+
画面に固定する 2D の情報(見出し、凡例、グラフ)は SVG のまま渡します。`global` に置くと全フレーム、フレームに置くとそのフレームだけに出ます。matplotlib の Figure も渡せます(背景は透明になります)。
|
|
59
|
+
|
|
60
|
+
```python
|
|
61
|
+
frame.add_svg('<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">'
|
|
62
|
+
'<text x="16" y="32" fill="white">t = 0</text></svg>')
|
|
63
|
+
frame.add_svg(figure) # matplotlib の Figure
|
|
64
|
+
doc.globals.add_svg("hud.svg") # ファイル
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## まとめて追加する
|
|
68
|
+
|
|
69
|
+
`add_many` は座標を ndarray のまま保持します。形は「個数 N × 1 個あたりの座標数 × 3」です。
|
|
70
|
+
|
|
71
|
+
| 種類 | 形 |
|
|
72
|
+
|---|---|
|
|
73
|
+
| `circle`, `ring`, `triangle`, `square`, `pentagon`, `hexagon`, `text` | `(N, 3)` |
|
|
74
|
+
| `line`, `stick` | `(N, 2, 3)` |
|
|
75
|
+
| `polygon`, `polyline`(頂点数がそろうとき) | `(N, m, 3)` |
|
|
76
|
+
|
|
77
|
+
`line` の始点と終点が別の配列なら `np.stack([starts, ends], axis=1)` で重ねます。`text` には長さ N の `texts` も渡します。
|
|
78
|
+
|
|
79
|
+
```python
|
|
80
|
+
starts = np.zeros((10, 3))
|
|
81
|
+
ends = np.ones((10, 3))
|
|
82
|
+
frame.add_many("line", np.stack([starts, ends], axis=1), color=3)
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## パレット
|
|
86
|
+
|
|
87
|
+
```python
|
|
88
|
+
from yaplotlib.palettes import rainbow_colors
|
|
89
|
+
|
|
90
|
+
doc.globals.palettes.update(enumerate(rainbow_colors(8), start=10))
|
|
91
|
+
frame.palette(3, (255, 96, 96))
|
|
92
|
+
frame.palettes(rainbow_colors(8), offset=10)
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
`random_colors` と `gradation_colors` も同じように RGB のリストを返します。
|
|
96
|
+
|
|
97
|
+
## 以前の API
|
|
98
|
+
|
|
99
|
+
モジュールの関数(`Line`, `Circle`, `Color`, `SetPalette` など)と、`Line` や `Color` のような大文字のメソッドは、`DeprecationWarning` を出して残してあります。`Yaplot` は 1 フレームだけの `YaplotFrame` の別名です。
|
|
100
|
+
|
|
101
|
+
```python
|
|
102
|
+
from yaplotlib import Yaplot
|
|
103
|
+
|
|
104
|
+
drawing = Yaplot()
|
|
105
|
+
drawing.Points([(0, 0, 0), (1, 0, 0)], color=2, size=0.3)
|
|
106
|
+
drawing.save("example.yaplot")
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## ライセンス
|
|
110
|
+
|
|
111
|
+
本リポジトリの `LICENSE` ファイルを参照してください。
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
[tool.poetry]
|
|
2
|
+
name = "yaplotlib"
|
|
3
|
+
version = "1.0.0"
|
|
4
|
+
description = "A tiny library to generate Yaplot file."
|
|
5
|
+
authors = ["vitroid <vitroid@gmail.com>"]
|
|
6
|
+
license = "MIT"
|
|
7
|
+
readme = "README.md"
|
|
8
|
+
|
|
9
|
+
[tool.poetry.dependencies]
|
|
10
|
+
python = "^3.11"
|
|
11
|
+
numpy = ">=1.24"
|
|
12
|
+
|
|
13
|
+
[tool.poetry.group.dev.dependencies]
|
|
14
|
+
pdoc = "^15.0"
|
|
15
|
+
pytest = ">=8"
|
|
16
|
+
|
|
17
|
+
[tool.pytest.ini_options]
|
|
18
|
+
pythonpath = ["."]
|
|
19
|
+
testpaths = ["tests"]
|
|
20
|
+
filterwarnings = ["always::DeprecationWarning"]
|
|
21
|
+
|
|
22
|
+
[build-system]
|
|
23
|
+
requires = ["poetry-core"]
|
|
24
|
+
build-backend = "poetry.core.masonry.api"
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"""Yaplot 形式の描画データを作る。"""
|
|
2
|
+
|
|
3
|
+
# even: stable; odd: develop
|
|
4
|
+
__version__ = "1.0.0"
|
|
5
|
+
|
|
6
|
+
from .core import (
|
|
7
|
+
Arrow,
|
|
8
|
+
ArrowType,
|
|
9
|
+
Circle,
|
|
10
|
+
Color,
|
|
11
|
+
Layer,
|
|
12
|
+
Line,
|
|
13
|
+
NewPage,
|
|
14
|
+
Polygon,
|
|
15
|
+
SetPalette,
|
|
16
|
+
Size,
|
|
17
|
+
Text,
|
|
18
|
+
)
|
|
19
|
+
from .document import YaplotDocument
|
|
20
|
+
from .elements import Batch, PaletteDef, Primitive, Svg
|
|
21
|
+
from .frame import Yaplot, YaplotFrame
|
|
22
|
+
from .globals import Globals
|
|
23
|
+
from .palettes import (
|
|
24
|
+
GradationPalettes,
|
|
25
|
+
RainbowPalettes,
|
|
26
|
+
RandomPalettes,
|
|
27
|
+
gradation_colors,
|
|
28
|
+
rainbow_colors,
|
|
29
|
+
random_colors,
|
|
30
|
+
)
|
|
31
|
+
from .spec import Kind
|
|
32
|
+
|
|
33
|
+
__all__ = [
|
|
34
|
+
"__version__",
|
|
35
|
+
"Kind",
|
|
36
|
+
"Primitive",
|
|
37
|
+
"Batch",
|
|
38
|
+
"PaletteDef",
|
|
39
|
+
"Svg",
|
|
40
|
+
"Globals",
|
|
41
|
+
"YaplotFrame",
|
|
42
|
+
"Yaplot",
|
|
43
|
+
"YaplotDocument",
|
|
44
|
+
"random_colors",
|
|
45
|
+
"rainbow_colors",
|
|
46
|
+
"gradation_colors",
|
|
47
|
+
"Line",
|
|
48
|
+
"Text",
|
|
49
|
+
"Circle",
|
|
50
|
+
"Arrow",
|
|
51
|
+
"Polygon",
|
|
52
|
+
"Color",
|
|
53
|
+
"SetPalette",
|
|
54
|
+
"Size",
|
|
55
|
+
"Layer",
|
|
56
|
+
"ArrowType",
|
|
57
|
+
"NewPage",
|
|
58
|
+
"RandomPalettes",
|
|
59
|
+
"RainbowPalettes",
|
|
60
|
+
"GradationPalettes",
|
|
61
|
+
]
|
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
"""要素の列を Yaplot のテキストに変換する。
|
|
2
|
+
|
|
3
|
+
状態(レイヤ・色・半径・矢印)の差分はここでだけ計算する。
|
|
4
|
+
フレームは状態を持たない。
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import operator
|
|
8
|
+
from typing import Optional, Sequence, Union
|
|
9
|
+
|
|
10
|
+
import numpy as np
|
|
11
|
+
|
|
12
|
+
from .elements import (
|
|
13
|
+
Batch,
|
|
14
|
+
FrameBreak,
|
|
15
|
+
PaletteDef,
|
|
16
|
+
Primitive,
|
|
17
|
+
Svg,
|
|
18
|
+
_as_int,
|
|
19
|
+
_as_radius,
|
|
20
|
+
)
|
|
21
|
+
from .globals import Globals
|
|
22
|
+
from .spec import KINDS
|
|
23
|
+
|
|
24
|
+
# Yaplot 本体の初期値。色だけは線が 0、塗りが 2 と分かれているため未確定にする。
|
|
25
|
+
_DEFAULT_LAYER = 1
|
|
26
|
+
_DEFAULT_RADIUS = 1.0
|
|
27
|
+
_DEFAULT_ARROW = 0
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def quote_text(text: str) -> str:
|
|
31
|
+
"""空白や引用符を含む文字列を Yaplot の ``"..."`` にする。"""
|
|
32
|
+
if "\n" in text or "\r" in text:
|
|
33
|
+
raise ValueError("改行を含む文字列は出力できません")
|
|
34
|
+
if text == "" or any(char.isspace() or char in '"\\' for char in text):
|
|
35
|
+
escaped = text.replace("\\", "\\\\").replace('"', '\\"')
|
|
36
|
+
return f'"{escaped}"'
|
|
37
|
+
return text
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def format_command(
|
|
41
|
+
command: str,
|
|
42
|
+
points: Sequence[Sequence[float]],
|
|
43
|
+
*,
|
|
44
|
+
count: bool = False,
|
|
45
|
+
text: Optional[str] = None,
|
|
46
|
+
) -> str:
|
|
47
|
+
"""描画コマンド 1 行を作る。座標の末尾には従来どおり空白を付ける。"""
|
|
48
|
+
parts = [command]
|
|
49
|
+
if count:
|
|
50
|
+
parts.append(str(len(points)))
|
|
51
|
+
for point in points:
|
|
52
|
+
for coord in point:
|
|
53
|
+
parts.append(f"{float(coord):.4f}")
|
|
54
|
+
line = " ".join(parts) + " "
|
|
55
|
+
if text is not None:
|
|
56
|
+
line += " " + quote_text(text)
|
|
57
|
+
return line + "\n"
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def format_palette(index: int, rgb: Sequence[int]) -> str:
|
|
61
|
+
"""``@ n r g b`` の 1 行を作る。"""
|
|
62
|
+
red, green, blue = rgb
|
|
63
|
+
return f"@ {int(index)} {int(red)} {int(green)} {int(blue)}\n"
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class _State:
|
|
67
|
+
"""書き出し中の、Yaplot 側の現在の状態。"""
|
|
68
|
+
|
|
69
|
+
def __init__(self, defaults: Optional[Globals]) -> None:
|
|
70
|
+
self.layer = _DEFAULT_LAYER
|
|
71
|
+
self.radius = _DEFAULT_RADIUS
|
|
72
|
+
self.arrow = _DEFAULT_ARROW
|
|
73
|
+
self.color: Optional[int] = None
|
|
74
|
+
if defaults is not None:
|
|
75
|
+
if defaults.layer is not None:
|
|
76
|
+
self.layer = _as_int("layer", defaults.layer, 1)
|
|
77
|
+
if defaults.radius is not None:
|
|
78
|
+
self.radius = _as_radius(defaults.radius)
|
|
79
|
+
if defaults.arrow is not None:
|
|
80
|
+
self.arrow = _as_int("arrow", defaults.arrow, 0)
|
|
81
|
+
self.color = _opening_color(defaults)
|
|
82
|
+
self._initial = (self.layer, self.radius, self.arrow, self.color)
|
|
83
|
+
|
|
84
|
+
def reset(self) -> None:
|
|
85
|
+
self.layer, self.radius, self.arrow, self.color = self._initial
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _opening_color(defaults: Globals) -> Optional[int]:
|
|
89
|
+
"""global ブロックを読み終えた時点で選ばれている色。"""
|
|
90
|
+
if defaults.color is not None:
|
|
91
|
+
return _as_int("color", defaults.color, 0)
|
|
92
|
+
if not defaults.palettes:
|
|
93
|
+
return None
|
|
94
|
+
last = sorted(defaults.palettes)[-1]
|
|
95
|
+
return _as_int("palette", last, 0)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _lowering(kind: str, lower: bool) -> bool:
|
|
99
|
+
spec = KINDS[kind]
|
|
100
|
+
if lower and spec.lower_to:
|
|
101
|
+
return True
|
|
102
|
+
if spec.command is None:
|
|
103
|
+
raise ValueError(
|
|
104
|
+
f"{kind!r} にはまだ Yaplot のコマンドがありません。"
|
|
105
|
+
" dumps(lower=True) で線分に分解して出力できます。"
|
|
106
|
+
)
|
|
107
|
+
return False
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _emit_style(state: _State, item: Union[Primitive, Batch], out: list[str]) -> None:
|
|
111
|
+
if item.layer is not None and item.layer != state.layer:
|
|
112
|
+
state.layer = item.layer
|
|
113
|
+
out.append(f"y {state.layer}\n")
|
|
114
|
+
if item.color is not None and item.color != state.color:
|
|
115
|
+
state.color = item.color
|
|
116
|
+
out.append(f"@ {state.color}\n")
|
|
117
|
+
if item.radius is not None and item.radius != state.radius:
|
|
118
|
+
state.radius = item.radius
|
|
119
|
+
out.append(f"r {state.radius}\n")
|
|
120
|
+
if item.arrow is not None and item.arrow != state.arrow:
|
|
121
|
+
state.arrow = item.arrow
|
|
122
|
+
out.append(f"a {state.arrow}\n")
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _emit_primitive(state: _State, prim: Primitive, out: list[str], lower: bool) -> None:
|
|
126
|
+
spec = KINDS[prim.kind]
|
|
127
|
+
_emit_style(state, prim, out)
|
|
128
|
+
if _lowering(prim.kind, lower):
|
|
129
|
+
command = KINDS[spec.lower_to].command # type: ignore[index]
|
|
130
|
+
points = prim.points
|
|
131
|
+
for start, end in zip(points, points[1:]):
|
|
132
|
+
out.append(format_command(command or "", (start, end)))
|
|
133
|
+
return
|
|
134
|
+
out.append(
|
|
135
|
+
format_command(
|
|
136
|
+
spec.command or "",
|
|
137
|
+
prim.points,
|
|
138
|
+
count=spec.npoints is None,
|
|
139
|
+
text=prim.text,
|
|
140
|
+
)
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def _emit_batch(state: _State, batch: Batch, out: list[str], lower: bool) -> None:
|
|
145
|
+
spec = KINDS[batch.kind]
|
|
146
|
+
lowering = _lowering(batch.kind, lower)
|
|
147
|
+
_emit_style(state, batch, out)
|
|
148
|
+
if lowering:
|
|
149
|
+
command = KINDS[spec.lower_to].command or "" # type: ignore[union-attr]
|
|
150
|
+
if isinstance(batch.coords, np.ndarray):
|
|
151
|
+
out.append(_format_block(command, _chains_to_lines(batch.coords), None, False))
|
|
152
|
+
else:
|
|
153
|
+
for array in batch.coords:
|
|
154
|
+
for index in range(array.shape[0] - 1):
|
|
155
|
+
out.append(format_command(command, (array[index], array[index + 1])))
|
|
156
|
+
return
|
|
157
|
+
if isinstance(batch.coords, np.ndarray):
|
|
158
|
+
out.append(
|
|
159
|
+
_format_block(spec.command or "", batch.coords, batch.texts, spec.npoints is None)
|
|
160
|
+
)
|
|
161
|
+
return
|
|
162
|
+
for index, array in enumerate(batch.coords):
|
|
163
|
+
text = None if batch.texts is None else batch.texts[index]
|
|
164
|
+
out.append(format_command(spec.command or "", array, count=True, text=text))
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def _emit_palette(state: _State, item: PaletteDef, out: list[str]) -> None:
|
|
168
|
+
out.append(format_palette(item.index, item.rgb))
|
|
169
|
+
# @ n r g b はパレット n を現在の色としても選ぶ。
|
|
170
|
+
state.color = item.index
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def _chains_to_lines(coords: np.ndarray) -> np.ndarray:
|
|
174
|
+
"""(N, m, 3) の折れ線を、隣り合う頂点を結ぶ (N*(m-1), 2, 3) の線分にする。"""
|
|
175
|
+
if coords.shape[0] == 0 or coords.shape[1] < 2:
|
|
176
|
+
return np.zeros((0, 2, 3), dtype=float)
|
|
177
|
+
paired = np.stack((coords[:, :-1, :], coords[:, 1:, :]), axis=2)
|
|
178
|
+
return paired.reshape(-1, 2, 3)
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def _format_block(
|
|
182
|
+
command: str,
|
|
183
|
+
coords: np.ndarray,
|
|
184
|
+
texts: Optional[tuple[str, ...]],
|
|
185
|
+
with_count: bool,
|
|
186
|
+
) -> str:
|
|
187
|
+
"""(N, k, 3) をコマンドの行にまとめて整形する。"""
|
|
188
|
+
count = int(coords.shape[0])
|
|
189
|
+
if count == 0:
|
|
190
|
+
return ""
|
|
191
|
+
width = int(coords.shape[1])
|
|
192
|
+
formatted = np.char.mod("%.4f", coords.reshape(count, width * 3))
|
|
193
|
+
prefix = f"{command} {width} " if with_count else f"{command} "
|
|
194
|
+
lines: list[str] = []
|
|
195
|
+
for index in range(count):
|
|
196
|
+
line = prefix + " ".join(formatted[index].tolist()) + " "
|
|
197
|
+
if texts is not None:
|
|
198
|
+
line += " " + quote_text(texts[index])
|
|
199
|
+
lines.append(line + "\n")
|
|
200
|
+
return "".join(lines)
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def _svg_text(svg: Svg) -> str:
|
|
204
|
+
text = svg.text
|
|
205
|
+
if not text.endswith("\n"):
|
|
206
|
+
return text + "\n"
|
|
207
|
+
return text
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def render_frame(
|
|
211
|
+
items: Sequence[object],
|
|
212
|
+
svgs: Sequence[Svg],
|
|
213
|
+
defaults: Optional[Globals],
|
|
214
|
+
lower: bool = True,
|
|
215
|
+
) -> str:
|
|
216
|
+
"""1 フレーム分をテキストにする。末尾にフレーム区切りの空行は付けない。"""
|
|
217
|
+
state = _State(defaults)
|
|
218
|
+
out: list[str] = []
|
|
219
|
+
for item in items:
|
|
220
|
+
if isinstance(item, FrameBreak):
|
|
221
|
+
out.append("\n")
|
|
222
|
+
state.reset()
|
|
223
|
+
continue
|
|
224
|
+
if isinstance(item, PaletteDef):
|
|
225
|
+
_emit_palette(state, item, out)
|
|
226
|
+
elif isinstance(item, Batch):
|
|
227
|
+
_emit_batch(state, item, out, lower)
|
|
228
|
+
elif isinstance(item, Primitive):
|
|
229
|
+
_emit_primitive(state, item, out, lower)
|
|
230
|
+
else:
|
|
231
|
+
raise TypeError(f"フレームに置けない要素です: {type(item).__name__}")
|
|
232
|
+
for svg in svgs:
|
|
233
|
+
out.append(_svg_text(svg))
|
|
234
|
+
return "".join(out)
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def _join_frames(texts: Sequence[str]) -> str:
|
|
238
|
+
out: list[str] = []
|
|
239
|
+
last = len(texts) - 1
|
|
240
|
+
for index, text in enumerate(texts):
|
|
241
|
+
if text and not text.endswith("\n"):
|
|
242
|
+
text += "\n"
|
|
243
|
+
out.append(text)
|
|
244
|
+
if index != last:
|
|
245
|
+
out.append("\n")
|
|
246
|
+
return "".join(out)
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def render_document(defaults: Globals, frames: Sequence[object], lower: bool = True) -> str:
|
|
250
|
+
"""global ブロックと全フレームを 1 つのテキストにする。"""
|
|
251
|
+
parts: list[str] = []
|
|
252
|
+
if not defaults.is_empty():
|
|
253
|
+
parts.append(render_globals(defaults))
|
|
254
|
+
rendered = [
|
|
255
|
+
render_frame(frame.items, frame.svgs, defaults, lower) # type: ignore[attr-defined]
|
|
256
|
+
for frame in frames
|
|
257
|
+
]
|
|
258
|
+
parts.append(_join_frames(rendered))
|
|
259
|
+
return "".join(parts)
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def render_globals(defaults: Globals) -> str:
|
|
263
|
+
"""global ブロックを、終端の空行まで含めて作る。"""
|
|
264
|
+
lines = ["global\n"]
|
|
265
|
+
if defaults.reality is not None:
|
|
266
|
+
lines.append(f"reality {_int_between('reality', defaults.reality, 0, 4)}\n")
|
|
267
|
+
if defaults.splat is not None:
|
|
268
|
+
lines.append(f"splat {_int_between('splat', defaults.splat, 0, 3)}\n")
|
|
269
|
+
if defaults.fov is not None:
|
|
270
|
+
lines.append(f"fov {_float_token(_as_float('fov', defaults.fov))}\n")
|
|
271
|
+
if defaults.verbose is not None:
|
|
272
|
+
lines.append(f"verbose {_int_between('verbose', defaults.verbose, 0, 3)}\n")
|
|
273
|
+
if defaults.thickness is not None:
|
|
274
|
+
lines.append(f"thickness {_plain_int('thickness', defaults.thickness)}\n")
|
|
275
|
+
if defaults.rotation is not None:
|
|
276
|
+
lines.append("rotation " + _rotation(defaults.rotation) + "\n")
|
|
277
|
+
if defaults.fps is not None:
|
|
278
|
+
lines.append(f"fps {_float_token(_as_float('fps', defaults.fps))}\n")
|
|
279
|
+
if defaults.hide:
|
|
280
|
+
lines.append("hide " + _layers("hide", defaults.hide) + "\n")
|
|
281
|
+
if defaults.show:
|
|
282
|
+
lines.append("show " + _layers("show", defaults.show) + "\n")
|
|
283
|
+
for message in defaults.messages:
|
|
284
|
+
if not isinstance(message, str):
|
|
285
|
+
raise TypeError("message は文字列にしてください")
|
|
286
|
+
lines.append("message " + quote_text(message) + "\n")
|
|
287
|
+
if defaults.radius is not None:
|
|
288
|
+
lines.append(f"r {_as_radius(defaults.radius)}\n")
|
|
289
|
+
if defaults.layer is not None:
|
|
290
|
+
lines.append(f"y {_as_int('layer', defaults.layer, 1)}\n")
|
|
291
|
+
if defaults.arrow is not None:
|
|
292
|
+
lines.append(f"a {_as_int('arrow', defaults.arrow, 0)}\n")
|
|
293
|
+
for index in sorted(defaults.palettes):
|
|
294
|
+
item = PaletteDef(index, defaults.palettes[index])
|
|
295
|
+
lines.append(format_palette(item.index, item.rgb))
|
|
296
|
+
if defaults.color is not None:
|
|
297
|
+
color = _as_int("color", defaults.color, 0)
|
|
298
|
+
last: Optional[int] = None
|
|
299
|
+
if defaults.palettes:
|
|
300
|
+
last = _as_int("palette", sorted(defaults.palettes)[-1], 0)
|
|
301
|
+
# 直前の @ n r g b がすでにこの色を選んでいれば、繰り返し出力しない。
|
|
302
|
+
if last != color:
|
|
303
|
+
lines.append(f"@ {color}\n")
|
|
304
|
+
for svg in defaults.svgs:
|
|
305
|
+
lines.append(_svg_text(svg))
|
|
306
|
+
lines.append("\n")
|
|
307
|
+
return "".join(lines)
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
def _plain_int(name: str, value: object) -> int:
|
|
311
|
+
if isinstance(value, bool):
|
|
312
|
+
raise TypeError(f"{name} は整数で指定してください")
|
|
313
|
+
try:
|
|
314
|
+
return operator.index(value) # type: ignore[arg-type]
|
|
315
|
+
except TypeError:
|
|
316
|
+
raise TypeError(f"{name} は整数で指定してください") from None
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
def _int_between(name: str, value: object, lo: int, hi: int) -> int:
|
|
320
|
+
number = _plain_int(name, value)
|
|
321
|
+
if not lo <= number <= hi:
|
|
322
|
+
raise ValueError(f"{name} は {lo}..{hi} の範囲です: {number}")
|
|
323
|
+
return number
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
def _as_float(name: str, value: object) -> float:
|
|
327
|
+
if isinstance(value, bool):
|
|
328
|
+
raise TypeError(f"{name} は数値で指定してください")
|
|
329
|
+
try:
|
|
330
|
+
return float(value) # type: ignore[arg-type]
|
|
331
|
+
except (TypeError, ValueError):
|
|
332
|
+
raise TypeError(f"{name} は数値で指定してください") from None
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
def _float_token(value: float) -> str:
|
|
336
|
+
if value == int(value) and abs(value) < 1e15:
|
|
337
|
+
return str(int(value))
|
|
338
|
+
return format(value, ".6g")
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
def _rotation(value: object) -> str:
|
|
342
|
+
try:
|
|
343
|
+
numbers = tuple(_as_float("rotation", item) for item in value) # type: ignore[union-attr]
|
|
344
|
+
except TypeError:
|
|
345
|
+
raise TypeError("rotation は 1〜3 個の数値にしてください") from None
|
|
346
|
+
if not 1 <= len(numbers) <= 3:
|
|
347
|
+
raise ValueError(f"rotation は 1〜3 個の数値にしてください({len(numbers)} 個渡されました)")
|
|
348
|
+
return " ".join(_float_token(number) for number in numbers)
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
def _layers(name: str, values: object) -> str:
|
|
352
|
+
try:
|
|
353
|
+
numbers = [_as_int(name, item, 1) for item in values] # type: ignore[union-attr]
|
|
354
|
+
except TypeError:
|
|
355
|
+
raise TypeError(f"{name} はレイヤ番号の列にしてください") from None
|
|
356
|
+
return " ".join(str(number) for number in numbers)
|