glyphsynth 0.1.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.
- glyphsynth-0.1.0/LICENSE +21 -0
- glyphsynth-0.1.0/PKG-INFO +25 -0
- glyphsynth-0.1.0/README.md +3 -0
- glyphsynth-0.1.0/glyphsynth/__init__.py +9 -0
- glyphsynth-0.1.0/glyphsynth/cli/export.py +53 -0
- glyphsynth-0.1.0/glyphsynth/cli/main.py +51 -0
- glyphsynth-0.1.0/glyphsynth/core/__init__.py +9 -0
- glyphsynth-0.1.0/glyphsynth/core/__pycache__/__init__.cpython-312.pyc +0 -0
- glyphsynth-0.1.0/glyphsynth/core/__pycache__/glyph.cpython-312.pyc +0 -0
- glyphsynth-0.1.0/glyphsynth/core/glyph.py +242 -0
- glyphsynth-0.1.0/glyphsynth/core/graphics/__init__.py +15 -0
- glyphsynth-0.1.0/glyphsynth/core/graphics/__pycache__/__init__.cpython-312.pyc +0 -0
- glyphsynth-0.1.0/glyphsynth/core/graphics/__pycache__/container.cpython-312.pyc +0 -0
- glyphsynth-0.1.0/glyphsynth/core/graphics/__pycache__/draw.cpython-312.pyc +0 -0
- glyphsynth-0.1.0/glyphsynth/core/graphics/__pycache__/export.cpython-312.pyc +0 -0
- glyphsynth-0.1.0/glyphsynth/core/graphics/__pycache__/graphics.cpython-312.pyc +0 -0
- glyphsynth-0.1.0/glyphsynth/core/graphics/__pycache__/properties.cpython-312.pyc +0 -0
- glyphsynth-0.1.0/glyphsynth/core/graphics/container.py +239 -0
- glyphsynth-0.1.0/glyphsynth/core/graphics/draw.py +192 -0
- glyphsynth-0.1.0/glyphsynth/core/graphics/export.py +276 -0
- glyphsynth-0.1.0/glyphsynth/core/graphics/graphics.py +13 -0
- glyphsynth-0.1.0/glyphsynth/core/graphics/properties.py +80 -0
- glyphsynth-0.1.0/glyphsynth/lib/__init__.py +9 -0
- glyphsynth-0.1.0/glyphsynth/lib/__pycache__/__init__.cpython-312.pyc +0 -0
- glyphsynth-0.1.0/glyphsynth/lib/__pycache__/array.cpython-312.pyc +0 -0
- glyphsynth-0.1.0/glyphsynth/lib/__pycache__/util.cpython-312.pyc +0 -0
- glyphsynth-0.1.0/glyphsynth/lib/alphabet/minimal/__init__.py +7 -0
- glyphsynth-0.1.0/glyphsynth/lib/alphabet/minimal/__pycache__/__init__.cpython-312.pyc +0 -0
- glyphsynth-0.1.0/glyphsynth/lib/alphabet/minimal/__pycache__/letters.cpython-312.pyc +0 -0
- glyphsynth-0.1.0/glyphsynth/lib/alphabet/minimal/letters.py +233 -0
- glyphsynth-0.1.0/glyphsynth/lib/array.py +151 -0
- glyphsynth-0.1.0/glyphsynth/lib/util.py +87 -0
- glyphsynth-0.1.0/pyproject.toml +44 -0
glyphsynth-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Matt
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: glyphsynth
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Pythonic vector glyph synthesis toolkit
|
|
5
|
+
Author: mm21
|
|
6
|
+
Author-email: mm21.dev@gmail.com
|
|
7
|
+
Requires-Python: >=3.12,<4.0
|
|
8
|
+
Classifier: Development Status :: 3 - Alpha
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Classifier: Natural Language :: English
|
|
11
|
+
Classifier: Operating System :: OS Independent
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
14
|
+
Classifier: Programming Language :: Python :: Implementation :: CPython
|
|
15
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
16
|
+
Classifier: Typing :: Typed
|
|
17
|
+
Requires-Dist: pydantic (>=2.5.3,<3.0.0)
|
|
18
|
+
Requires-Dist: pyrollup (>=0.1.0,<0.2.0)
|
|
19
|
+
Requires-Dist: svgwrite (>=1.4.3,<2.0.0)
|
|
20
|
+
Requires-Dist: typer[all] (>=0.9.0,<0.10.0)
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
|
|
23
|
+
# GlyphSynth: Pythonic vector glyph synthesis toolkit
|
|
24
|
+
|
|
25
|
+
Work in progress; synthesis of basic shapes, compositions of shapes, and fractals is working.
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
from types import ModuleType
|
|
2
|
+
from typing import Any, Literal
|
|
3
|
+
|
|
4
|
+
from ..core.glyph import BaseGlyph
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def invoke_export(
|
|
8
|
+
path_cls: str, path_output: str, format: Literal["svg", "png"] | None
|
|
9
|
+
):
|
|
10
|
+
print(
|
|
11
|
+
f"Exporting: modpath={path_cls}, path_output={path_output}, format={format}"
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
# get class to be imported
|
|
15
|
+
glyph_cls: type[BaseGlyph] = _get_glyph_cls(path_cls)
|
|
16
|
+
|
|
17
|
+
print(f"--- got glyph_cls: {glyph_cls}")
|
|
18
|
+
|
|
19
|
+
# instantiate glyph
|
|
20
|
+
glyph: BaseGlyph = glyph_cls()
|
|
21
|
+
|
|
22
|
+
# export glyph
|
|
23
|
+
glyph.export(path_output, format)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _get_glyph_cls(path_cls: str) -> type[BaseGlyph]:
|
|
27
|
+
modpath: str
|
|
28
|
+
cls_name: str
|
|
29
|
+
|
|
30
|
+
modpath, cls_name = _parse_path_cls(path_cls)
|
|
31
|
+
|
|
32
|
+
print(f"--- importing {cls_name} from {modpath}")
|
|
33
|
+
|
|
34
|
+
mod: ModuleType = __import__(modpath, fromlist=[cls_name])
|
|
35
|
+
glyph_cls: Any = getattr(mod, cls_name)
|
|
36
|
+
|
|
37
|
+
assert issubclass(glyph_cls, BaseGlyph)
|
|
38
|
+
return glyph_cls
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _parse_path_cls(path_cls: str) -> tuple[str, str]:
|
|
42
|
+
"""
|
|
43
|
+
Return modpath and class name.
|
|
44
|
+
"""
|
|
45
|
+
modpath: str
|
|
46
|
+
cls_name: str
|
|
47
|
+
|
|
48
|
+
path_split: list[str] = path_cls.rsplit(".", maxsplit=1)
|
|
49
|
+
assert len(path_split) == 2, f"Invalid path_cls: {path_cls}"
|
|
50
|
+
|
|
51
|
+
modpath, cls_name = path_split
|
|
52
|
+
|
|
53
|
+
return (modpath, cls_name)
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
from typing_extensions import Annotated
|
|
2
|
+
from typing import Union, Literal
|
|
3
|
+
|
|
4
|
+
import typer
|
|
5
|
+
|
|
6
|
+
from .export import invoke_export
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
app = typer.Typer(
|
|
10
|
+
add_completion=False, invoke_without_command=False, no_args_is_help=True
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
# Dummy callback required in order to allow having a single subcommand
|
|
15
|
+
@app.callback()
|
|
16
|
+
def main():
|
|
17
|
+
pass
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@app.command(no_args_is_help=True)
|
|
21
|
+
def export(
|
|
22
|
+
path_cls: Annotated[
|
|
23
|
+
str,
|
|
24
|
+
typer.Argument(help="Path to Python class, e.g. my_pkg.my_mod.MyGlyph"),
|
|
25
|
+
],
|
|
26
|
+
path_output: Annotated[
|
|
27
|
+
str,
|
|
28
|
+
typer.Argument(
|
|
29
|
+
help="Path to output file or folder; if folder, filename generated by class name"
|
|
30
|
+
),
|
|
31
|
+
],
|
|
32
|
+
format: Annotated[
|
|
33
|
+
str,
|
|
34
|
+
typer.Argument(
|
|
35
|
+
help='"svg" or "png", or leave empty to determine from filename if possible, defaulting to "svg"'
|
|
36
|
+
),
|
|
37
|
+
] = None,
|
|
38
|
+
):
|
|
39
|
+
assert format in {"svg", "png", None}, f"Invalid format: {format}"
|
|
40
|
+
invoke_export(path_cls, path_output, format)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def run():
|
|
44
|
+
"""
|
|
45
|
+
Entry point of `glyphsynth` command.
|
|
46
|
+
"""
|
|
47
|
+
app()
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
if __name__ == "__main__":
|
|
51
|
+
run()
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from abc import ABC, abstractmethod
|
|
4
|
+
from typing import Self, TypeVar, get_args, cast
|
|
5
|
+
|
|
6
|
+
from pydantic import BaseModel, ConfigDict
|
|
7
|
+
|
|
8
|
+
from .graphics.graphics import (
|
|
9
|
+
GraphicsContainer,
|
|
10
|
+
BaseContainer,
|
|
11
|
+
)
|
|
12
|
+
from .graphics.properties import Properties
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
"BaseParams",
|
|
16
|
+
"BaseGlyph",
|
|
17
|
+
"EmptyParams",
|
|
18
|
+
"EmptyGlyph",
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class IdFactory:
|
|
23
|
+
_cls_map: dict[type[BaseGlyph], int]
|
|
24
|
+
|
|
25
|
+
def __init__(self):
|
|
26
|
+
self.reset()
|
|
27
|
+
|
|
28
|
+
def get_id(self, glyph_cls: type[BaseGlyph]) -> int:
|
|
29
|
+
if glyph_cls not in self._cls_map:
|
|
30
|
+
self._cls_map[glyph_cls] = 0
|
|
31
|
+
|
|
32
|
+
self._cls_map[glyph_cls] += 1
|
|
33
|
+
|
|
34
|
+
return self._cls_map[glyph_cls]
|
|
35
|
+
|
|
36
|
+
def reset(self):
|
|
37
|
+
self._cls_map = dict()
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
id_factory = IdFactory()
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class BaseParams(BaseModel):
|
|
44
|
+
"""
|
|
45
|
+
Subclass this class to create parameters for a Glyph.
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
model_config = ConfigDict(arbitrary_types_allowed=True)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
# def __init_subclass__(cls):
|
|
52
|
+
# dataclass(frozen=True, kw_only=True)(cls)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class EmptyParams(BaseParams):
|
|
56
|
+
pass
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class BaseGlyph[ParamsT: BaseParams](ABC, GraphicsContainer, BaseContainer):
|
|
60
|
+
"""
|
|
61
|
+
Base class for a standalone or reusable glyph, sized in abstract
|
|
62
|
+
(user) units.
|
|
63
|
+
"""
|
|
64
|
+
|
|
65
|
+
params: ParamsT
|
|
66
|
+
"""
|
|
67
|
+
Instance of params with type as specified by typevar.
|
|
68
|
+
"""
|
|
69
|
+
|
|
70
|
+
DefaultParams: type[ParamsT] | None = None
|
|
71
|
+
"""
|
|
72
|
+
Subclass of ParamsT to use as defaults for this particular Glyph,
|
|
73
|
+
if no parameters provided during instantiation.
|
|
74
|
+
"""
|
|
75
|
+
|
|
76
|
+
_params_cls: type[ParamsT]
|
|
77
|
+
"""
|
|
78
|
+
Params class: the type parameter provided as ParamsT, or EmptyParams
|
|
79
|
+
if no type parameter provided.
|
|
80
|
+
"""
|
|
81
|
+
|
|
82
|
+
def __init__(
|
|
83
|
+
self,
|
|
84
|
+
*,
|
|
85
|
+
glyph_id: str | None = None,
|
|
86
|
+
params: ParamsT | None = None,
|
|
87
|
+
properties: Properties | None = None,
|
|
88
|
+
size: tuple[float, float] | None = None,
|
|
89
|
+
parent: BaseGlyph | None = None,
|
|
90
|
+
insert: tuple[float, float] | None = None,
|
|
91
|
+
):
|
|
92
|
+
"""
|
|
93
|
+
:param parent: Parent glyph, or `None`{l=python} to create top-level glyph
|
|
94
|
+
:param glyph_id: Unique identifier, or `None`{l=python} to generate one
|
|
95
|
+
"""
|
|
96
|
+
|
|
97
|
+
# if parent not provided, ensure insert/size_inst not provided
|
|
98
|
+
if parent is None:
|
|
99
|
+
assert insert is None
|
|
100
|
+
|
|
101
|
+
glyph_id = glyph_id or self._get_id()
|
|
102
|
+
|
|
103
|
+
super().__init__(glyph_id, properties, size)
|
|
104
|
+
|
|
105
|
+
# set params
|
|
106
|
+
self.params = params or type(self).params_cls()
|
|
107
|
+
|
|
108
|
+
# invoke subclass's init (e.g. set properties based on params)
|
|
109
|
+
self.init()
|
|
110
|
+
|
|
111
|
+
# invoke post-init since size_canon may be set in init()
|
|
112
|
+
self._init_post()
|
|
113
|
+
|
|
114
|
+
# invoke subclass's drawing logic
|
|
115
|
+
self.draw()
|
|
116
|
+
|
|
117
|
+
if parent is not None:
|
|
118
|
+
parent.insert_glyph(self, insert)
|
|
119
|
+
|
|
120
|
+
def __init_subclass__(cls: type[BaseGlyph]):
|
|
121
|
+
"""
|
|
122
|
+
Populate _params_cls with the class representing the parameters for
|
|
123
|
+
this glyph. If not subscripted with a type arg by the subclass,
|
|
124
|
+
_params_cls is set to EmptyParams.
|
|
125
|
+
"""
|
|
126
|
+
|
|
127
|
+
# note: pyright is not yet aware that the new style
|
|
128
|
+
#
|
|
129
|
+
# class BaseGlyph[ParamsT: BaseParams](...): ...
|
|
130
|
+
#
|
|
131
|
+
# is equivalent to
|
|
132
|
+
#
|
|
133
|
+
# ParamsT = TypeVar("ParamsT", bound=BaseParams)
|
|
134
|
+
# class BaseGlyph(Generic[ParamsT], ...): ...
|
|
135
|
+
#
|
|
136
|
+
# since with the new style, Generic is not a base. Nonetheless
|
|
137
|
+
# __orig_bases__ is indeed defined here.
|
|
138
|
+
orig_bases = (
|
|
139
|
+
cls.__orig_bases__ # pyright: ignore[reportGeneralTypeIssues]
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
# get_args() returns tuple[Any, ...], but we know the type param
|
|
143
|
+
# should always be ParamsT
|
|
144
|
+
args: tuple[type[ParamsT]] = cast(
|
|
145
|
+
tuple[type[ParamsT]], get_args(orig_bases[0])
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
if len(args) > 0:
|
|
149
|
+
assert len(args) == 1
|
|
150
|
+
|
|
151
|
+
params_cls: type[ParamsT]
|
|
152
|
+
|
|
153
|
+
if isinstance(args[0], TypeVar):
|
|
154
|
+
# have a TypeVar, look up its bound
|
|
155
|
+
type_var = cast(TypeVar, args[0])
|
|
156
|
+
assert type_var.__bound__ is not None
|
|
157
|
+
params_cls = type_var.__bound__
|
|
158
|
+
else:
|
|
159
|
+
# already have a class
|
|
160
|
+
params_cls = args[0]
|
|
161
|
+
|
|
162
|
+
cls._params_cls = params_cls
|
|
163
|
+
else:
|
|
164
|
+
# set _params_cls with EmptyParams
|
|
165
|
+
cls._params_cls = EmptyParams
|
|
166
|
+
|
|
167
|
+
assert issubclass(cls._params_cls, BaseParams)
|
|
168
|
+
|
|
169
|
+
@property
|
|
170
|
+
def glyph_id(self) -> str:
|
|
171
|
+
"""
|
|
172
|
+
A meaningful identifier to associate with this glyph. Also used as
|
|
173
|
+
base name (without extension) of file to write when no filename is
|
|
174
|
+
provided.
|
|
175
|
+
"""
|
|
176
|
+
return self._id
|
|
177
|
+
|
|
178
|
+
@classmethod
|
|
179
|
+
@property
|
|
180
|
+
def params_cls(cls) -> type[ParamsT]:
|
|
181
|
+
"""
|
|
182
|
+
Returns the {obj}`BaseParams` subclass with which this class is
|
|
183
|
+
parameterized, accounting for any default values provided by
|
|
184
|
+
the subclass.
|
|
185
|
+
"""
|
|
186
|
+
return cls.DefaultParams or cls._params_cls
|
|
187
|
+
|
|
188
|
+
def insert_glyph(
|
|
189
|
+
self,
|
|
190
|
+
glyph: BaseGlyph,
|
|
191
|
+
insert: tuple[float, float] | None = None,
|
|
192
|
+
) -> Self:
|
|
193
|
+
glyph._bind(self, insert)
|
|
194
|
+
return self
|
|
195
|
+
|
|
196
|
+
def init(self):
|
|
197
|
+
...
|
|
198
|
+
|
|
199
|
+
@abstractmethod
|
|
200
|
+
def draw(self):
|
|
201
|
+
...
|
|
202
|
+
|
|
203
|
+
# TODO_NEXT 2
|
|
204
|
+
# - move to subclass of BaseClass
|
|
205
|
+
# - rename BaseClass -> _BaseClass?
|
|
206
|
+
# try:
|
|
207
|
+
# def draw_glyph[GlyphT: BaseGlyph, GlyphParamsT: BaseParams](self,
|
|
208
|
+
# glyph_cls: type[GlyphT[GlyphParamsT]], params: GlyphParamsT) -> GlyphT[GlyphParamsT]:
|
|
209
|
+
# return glyph_cls(parent=self, params=params)
|
|
210
|
+
|
|
211
|
+
def _get_id(self) -> str:
|
|
212
|
+
global id_factory
|
|
213
|
+
id_num: int = id_factory.get_id(type(self))
|
|
214
|
+
return f"{type(self).__name__}-{id_num}"
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
class EmptyGlyph(BaseGlyph[EmptyParams]):
|
|
218
|
+
"""
|
|
219
|
+
Glyph to use as an on-the-fly alternative to subclassing
|
|
220
|
+
{obj}`BaseGlyph`. It has an empty {obj}`BaseGlyph.draw`
|
|
221
|
+
implementation; the user can then add graphics objects
|
|
222
|
+
and other glyphs after creation.
|
|
223
|
+
|
|
224
|
+
Example:
|
|
225
|
+
|
|
226
|
+
```python
|
|
227
|
+
glyph1 = MyGlyph1()
|
|
228
|
+
glyph2 = MyGlyph2()
|
|
229
|
+
|
|
230
|
+
# create an empty glyph with unspecified size
|
|
231
|
+
glyph = EmptyGlyph()
|
|
232
|
+
|
|
233
|
+
# insert a glyph
|
|
234
|
+
glyph.insert_glyph(glyph1)
|
|
235
|
+
|
|
236
|
+
# insert another glyph in a different position
|
|
237
|
+
glyph.insert_glyph(glyph2, (100, 100))
|
|
238
|
+
```
|
|
239
|
+
"""
|
|
240
|
+
|
|
241
|
+
def draw(self):
|
|
242
|
+
pass
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from pyrollup import rollup
|
|
2
|
+
|
|
3
|
+
from . import graphics, draw, export, container, properties
|
|
4
|
+
|
|
5
|
+
from .graphics import *
|
|
6
|
+
from .draw import *
|
|
7
|
+
from .export import *
|
|
8
|
+
from .container import *
|
|
9
|
+
from .properties import *
|
|
10
|
+
|
|
11
|
+
__all__ = rollup(graphics, draw, export, container, properties)
|
|
12
|
+
|
|
13
|
+
# TODO: add types.py?
|
|
14
|
+
# - Coordinate
|
|
15
|
+
# - Box/Area
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
|
|
5
|
+
from svgwrite.drawing import Drawing
|
|
6
|
+
from svgwrite.container import SVG, Group
|
|
7
|
+
|
|
8
|
+
from .properties import Properties
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
__all__ = []
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class BaseContainer:
|
|
15
|
+
"""
|
|
16
|
+
Container for drawing and manipulation of low-level graphics objects.
|
|
17
|
+
|
|
18
|
+
There are 2 SVG trees maintained:
|
|
19
|
+
1. Standalone glyph
|
|
20
|
+
drawing (root <svg>)
|
|
21
|
+
wrapper svg (if size provided)
|
|
22
|
+
canonical svg
|
|
23
|
+
2. Placement in parent glyph
|
|
24
|
+
svg-parent
|
|
25
|
+
svg-wrapper-placement (insert only)
|
|
26
|
+
group (w/transforms, e.g. rotation)
|
|
27
|
+
svg-wrapper-scaling (width/height/viewbox only)
|
|
28
|
+
svg-canonical
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
DefaultProperties: type[Properties] = Properties
|
|
32
|
+
"""
|
|
33
|
+
Subclass of Properties with properties set as class attributes.
|
|
34
|
+
|
|
35
|
+
May be subclassed in-place or assigned to an existing subclass.
|
|
36
|
+
For example, subclassing in-place:
|
|
37
|
+
|
|
38
|
+
```python
|
|
39
|
+
class MyGlyph(BaseGlyph):
|
|
40
|
+
|
|
41
|
+
class DefaultProperties(Properties):
|
|
42
|
+
color: PropertyValue = "black"
|
|
43
|
+
stroke_width: PropertyValue = "10"
|
|
44
|
+
|
|
45
|
+
...
|
|
46
|
+
```
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
properties: Properties
|
|
50
|
+
"""
|
|
51
|
+
Properties automatically propagated to graphics objects in draw_*() APIs.
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
size_canon: tuple[float, float] | None = None
|
|
55
|
+
"""
|
|
56
|
+
Canonical size in user units, as provided by concrete class either as class
|
|
57
|
+
attribute or upon creation in {obj}`BaseGlyph.init`.
|
|
58
|
+
|
|
59
|
+
Used to set a consistent size for drawing, invariant of the size
|
|
60
|
+
passed upon instantiation.
|
|
61
|
+
|
|
62
|
+
May be `None`, in which case {obj}`BaseContainer.size`,
|
|
63
|
+
{obj}`BaseContainer.width`, and {obj}`BaseContainer.height`
|
|
64
|
+
will return `None`.
|
|
65
|
+
|
|
66
|
+
Note that some {obj}`BaseGlyph` subclasses may require that
|
|
67
|
+
this field is not `None`.
|
|
68
|
+
"""
|
|
69
|
+
|
|
70
|
+
viewbox_canon: tuple[tuple[float, float], tuple[float, float]] | None = None
|
|
71
|
+
|
|
72
|
+
_id: str
|
|
73
|
+
"""
|
|
74
|
+
Unique id for this container.
|
|
75
|
+
"""
|
|
76
|
+
|
|
77
|
+
_size: tuple[float, float] | None = None
|
|
78
|
+
"""
|
|
79
|
+
Instantiated size, set upon glyph creation. If `None`, not rescaled;
|
|
80
|
+
uses canonical size.
|
|
81
|
+
"""
|
|
82
|
+
|
|
83
|
+
_drawing: Drawing
|
|
84
|
+
"""
|
|
85
|
+
Drawing object, unique to each Glyph.
|
|
86
|
+
"""
|
|
87
|
+
|
|
88
|
+
_svg: SVG
|
|
89
|
+
"""
|
|
90
|
+
Canonical SVG container for all elements generated by this glyph.
|
|
91
|
+
"""
|
|
92
|
+
|
|
93
|
+
_group: Group
|
|
94
|
+
"""
|
|
95
|
+
Group to hold wrapper SVG container when added to a parent glyph.
|
|
96
|
+
Transformations are performed on this group rather than the <svg> itself
|
|
97
|
+
as <svg> does not support transformations in SVG 1.1.
|
|
98
|
+
"""
|
|
99
|
+
|
|
100
|
+
_parent: BaseContainer | None = None
|
|
101
|
+
"""
|
|
102
|
+
Parent container, or None if not inserted in another Glyph via
|
|
103
|
+
{obj}`Glyph.insert_glyph` or passing `parent` upon instantiation.
|
|
104
|
+
In this case it has its own standalone Drawing, but may still be
|
|
105
|
+
added as a child of another Glyph.
|
|
106
|
+
|
|
107
|
+
Only set when inserted as a child of another Glyph.
|
|
108
|
+
"""
|
|
109
|
+
|
|
110
|
+
_insert: tuple[float, float] | None = None
|
|
111
|
+
"""
|
|
112
|
+
Point at which this glyph is inserted in parent.
|
|
113
|
+
|
|
114
|
+
Only set when inserted as a child of another Glyph.
|
|
115
|
+
"""
|
|
116
|
+
|
|
117
|
+
def __init__(
|
|
118
|
+
self,
|
|
119
|
+
id_: str,
|
|
120
|
+
properties: Properties | None,
|
|
121
|
+
size: tuple[float, float] | None,
|
|
122
|
+
):
|
|
123
|
+
self._id = id_
|
|
124
|
+
self._size = size
|
|
125
|
+
self._drawing = Drawing()
|
|
126
|
+
self._group = self._drawing.g(id_=f"{self._id}-group")
|
|
127
|
+
|
|
128
|
+
self.properties = (
|
|
129
|
+
self.DefaultProperties() if properties is None else properties
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
@property
|
|
133
|
+
def has_size(self) -> bool:
|
|
134
|
+
try:
|
|
135
|
+
_ = self.size
|
|
136
|
+
except AssertionError:
|
|
137
|
+
return False
|
|
138
|
+
|
|
139
|
+
return True
|
|
140
|
+
|
|
141
|
+
# TODO: document: raises exception + add custom exception
|
|
142
|
+
@property
|
|
143
|
+
def size(self) -> tuple[float, float]:
|
|
144
|
+
s = self._size or self.size_canon
|
|
145
|
+
assert s is not None
|
|
146
|
+
return s
|
|
147
|
+
|
|
148
|
+
@property
|
|
149
|
+
def width(self) -> float:
|
|
150
|
+
return self.size[0]
|
|
151
|
+
|
|
152
|
+
@property
|
|
153
|
+
def height(self) -> float:
|
|
154
|
+
return self.size[1]
|
|
155
|
+
|
|
156
|
+
def _init_post(self):
|
|
157
|
+
# create canonical svg
|
|
158
|
+
self._svg: SVG = self._drawing.svg(
|
|
159
|
+
id_=self._id,
|
|
160
|
+
size=self.size_canon,
|
|
161
|
+
class_=type(self).__name__,
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
# set viewbox, if configured
|
|
165
|
+
if self.viewbox_canon is not None:
|
|
166
|
+
x, y = self.viewbox_canon[0]
|
|
167
|
+
w, h = self.viewbox_canon[1]
|
|
168
|
+
self._svg.viewbox(x, y, w, h)
|
|
169
|
+
self._svg.fit()
|
|
170
|
+
|
|
171
|
+
# add to drawing for standalone glyph
|
|
172
|
+
if self._size is None:
|
|
173
|
+
# no scaling needed, add svg directly
|
|
174
|
+
self._drawing.add(self._svg)
|
|
175
|
+
self._group.add(self._svg)
|
|
176
|
+
else:
|
|
177
|
+
# create wrapper svg and rescale
|
|
178
|
+
svg_wrapper: SVG = self._create_wrapper_scale()
|
|
179
|
+
self._drawing.add(svg_wrapper)
|
|
180
|
+
self._group.add(svg_wrapper)
|
|
181
|
+
|
|
182
|
+
# set top-level dimensions explicitly as larger SVGs
|
|
183
|
+
# are unexpectedly truncated when sized to 100% (default)
|
|
184
|
+
if self.has_size:
|
|
185
|
+
self._drawing["width"] = str(self.size[0])
|
|
186
|
+
self._drawing["height"] = str(self.size[1])
|
|
187
|
+
|
|
188
|
+
def _bind(
|
|
189
|
+
self,
|
|
190
|
+
parent: BaseContainer,
|
|
191
|
+
insert: tuple[float, float] | None,
|
|
192
|
+
):
|
|
193
|
+
self._parent = parent
|
|
194
|
+
self._insert = insert
|
|
195
|
+
|
|
196
|
+
# add group to parent, using wrapper svg for placement
|
|
197
|
+
wrapper_insert: SVG = self._drawing.svg(
|
|
198
|
+
insert=self._insert,
|
|
199
|
+
id_=f"{self._id}-wrapper-insert",
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
wrapper_insert.add(self._group)
|
|
203
|
+
parent._svg.add(wrapper_insert)
|
|
204
|
+
|
|
205
|
+
def _create_wrapper_scale(self) -> SVG:
|
|
206
|
+
"""
|
|
207
|
+
Create SVG wrapper for canonical SVG object to handle
|
|
208
|
+
placement/scaling.
|
|
209
|
+
"""
|
|
210
|
+
|
|
211
|
+
wrapper_scale: SVG = self._drawing.svg(
|
|
212
|
+
size=self._size,
|
|
213
|
+
id_=f"{self._id}-wrapper-scale",
|
|
214
|
+
)
|
|
215
|
+
self._rescale_svg(wrapper_scale, self._size)
|
|
216
|
+
wrapper_scale.add(self._svg)
|
|
217
|
+
|
|
218
|
+
return wrapper_scale
|
|
219
|
+
|
|
220
|
+
def _rescale_svg(
|
|
221
|
+
self,
|
|
222
|
+
svg: SVG,
|
|
223
|
+
size: tuple[float, float] | tuple[str, str] | None,
|
|
224
|
+
set_size: bool = False,
|
|
225
|
+
):
|
|
226
|
+
if size is not None:
|
|
227
|
+
if set_size:
|
|
228
|
+
svg["width"] = str(size[0])
|
|
229
|
+
svg["height"] = str(size[1])
|
|
230
|
+
|
|
231
|
+
if self.size_canon is None:
|
|
232
|
+
logging.warning(
|
|
233
|
+
f"Rescaling glyph {self} which has no canonical size; viewbox will not be set, so results may be unexpected"
|
|
234
|
+
)
|
|
235
|
+
else:
|
|
236
|
+
svg.viewbox(
|
|
237
|
+
0, 0, round(self.size_canon[0]), round(self.size_canon[1])
|
|
238
|
+
)
|
|
239
|
+
svg.fit()
|