TexnoMagic Drawing is a set of 2D curves defined by points.
Specifically, Drawing is list of lists of 2D points (a list of separated
curves) as generated by pointing devices such as mouse, touch, and tablets.
This abstraction is roughly equivalent to using a pen & paper.
Individual drawing points indicate the trajectory of a
pen/pencil/marker/brush/chalk/etc when it was writing.
Drawing is a usually a representation of a
Symbol.
Drawings are stored as CSV files with individual curves separated
by empty lines (,
).
self.path
is a path of Drawing data CSV file.
This class provides convenient utilities for working with Drawings,
see individual methods.
Source code in texnomagic/drawing.py
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211 | class TexnoMagicDrawing:
"""TexnoMagic Drawing is a set of 2D curves defined by points.
Specifically, Drawing is list of lists of 2D points (a list of separated
curves) as generated by pointing devices such as mouse, touch, and tablets.
This abstraction is roughly equivalent to using a pen & paper.
Individual drawing points indicate the trajectory of a
pen/pencil/marker/brush/chalk/etc when it was writing.
Drawing is a usually a representation of a
[Symbol][texnomagic.symbol.TexnoMagicSymbol].
Drawings are stored as CSV files with individual curves separated
by empty lines (`,`).
`self.path` is a path of Drawing data CSV file.
This class provides convenient utilities for working with Drawings,
see individual methods.
"""
def __init__(self, path=None, curves=None, points_range=1000.0):
self.path = path
self.points_range = points_range
self._curves = None
self._points = None
self._file_size = None
if curves:
self.set_curves(curves)
@property
def curves(self) -> list[np.array]:
"""Individual curves (lists of points).
Lazy loaded on-demand."""
if self._curves is None:
self.load_curves()
return self._curves
@property
def points(self) -> np.array:
"""All points from all curves.
Lazy loaded on-demand."""
if self._points is None:
self.load_curves()
return self._points
@property
def name(self) -> str | None:
"""Drawing file name.
Derived from self.path."""
if self.path:
return self.path.name
return None
@property
def file_size(self) -> int:
"""Drawing file size.
Lazy loaded on-demand."""
if self._file_size is None:
self._file_size = self.path.stat().st_size
return self._file_size
def set_curves(self, curves):
"""Assign curves.
Converts to a single numpy.array points with curves being views
into the array for fast processing."""
# keep all points in single continuous numpy array
self._points = np.array(list(itertools.chain(*curves)), dtype=np.float64)
self._curves = []
i = 0
for curve in curves:
n = len(curve)
# curves are numpy views into main points array
cview = self._points[i:i+n]
self._curves.append(cview)
i += n
def load(self, path=None):
# this is only kept for consistence with symbol and abc
if path:
self.path = path
return self
def load_curves(self):
"""Load Drawing curves from file."""
curves = []
curve = []
with self.path.open('r') as f:
reader = csv.reader(f)
for row in reader:
if not row or '' in row:
# empty rows separate individual curves
curves.append(curve)
curve = []
continue
point = list(map(float, row[:2]))
curve.append(point)
curves.append(curve)
self.set_curves(curves)
def save(self):
"""Save drawing to CSV file specified by self.path."""
self.path.parent.mkdir(parents=True, exist_ok=True)
with self.path.open('w', newline='') as f:
writer = csv.writer(f)
first = True
for curve in self.curves:
if first:
first = False
else:
# curves separator
writer.writerow([None, None])
writer.writerows(curve.tolist())
def normalize(self):
"""
Normalize drawing points in-place into <0, self.points_range> range.
See also [curves_fit_area][texnomagic.drawing.TexnoMagicDrawing.curves_fit_area].
"""
if len(self.points) == 0:
return
# move to [0,0]
self._points -= np.min(self.points, axis=0)
# normalize
k = self.points_range / np.max((np.max(np.max(self._points, axis=0)), 0.2))
self._points *= k
# center
offset = (self.points_range - np.max(self._points, axis=0)) / 2
self._points += offset
def curves_fit_area(self, pos : tuple[float, float] | npt.ArrayLike, size : tuple[float, float] | npt.ArrayLike) -> list[npt.ArrayLike]:
"""
Return curves scaled to fit area.
Useful for drawing curves in UI
See also [normalize][texnomagic.drawing.TexnoMagicDrawing.normalize].
Args:
pos: position / offset (x, y) - relative <0, 1>
size: desired size (width, height) - relative <0, 1>
Returns:
Curves scaled to fit desired area.
"""
pos = np.array(pos)
size = np.array(size)
k = np.min(size) / self.points_range
max_range = self.points_range * k
offset = pos + (size - max_range) / 2
scurves = []
for curve in self.curves:
if len(curve) > 0:
scurve = curve * k + offset
else:
scurve = curve
scurves.append(scurve)
return scurves
def flip_y_axis(self):
"""
Flip Drawing along Y axis in-place.
Useful for compatibility with systems that use different Y axis sign.
"""
self._points[:,1] = self.points_range - self._points[:,1]
def delete(self):
"""Delete the Drawing file."""
if not self.path or not self.path.exists():
return
self.path.unlink()
def pretty(self, size=True) -> str:
"""Pretty Drawing string with colors in rich formatting."""
n_curves = len(self.curves)
n_points = len(self.points)
s = f"[white bold]{self.path.name}[/]: "
s += f"{n_points} points, {n_curves} curves"
if size:
fsize = math.ceil(self.file_size / 1024.0)
s += f", {fsize} kB"
return s
def __str__(self) -> str:
if self._curves is None:
info = "curves not loaded"
else:
info = f"{len(self._points)} points, {len(self._curves)} curves"
return f"{self.name}: {info}"
def __repr__(self) -> str:
return '<TexnoMagicSymbol %s>' % self.__str__()
|
curves: list[np.array]
property
Individual curves (lists of points).
Lazy loaded on-demand.
file_size: int
property
Drawing file size.
Lazy loaded on-demand.
name: str | None
property
Drawing file name.
Derived from self.path.
points: np.array
property
All points from all curves.
Lazy loaded on-demand.
curves_fit_area(pos, size)
Return curves scaled to fit area.
Useful for drawing curves in UI
See also normalize.
Parameters:
Name |
Type |
Description |
Default |
pos |
tuple[float, float] | ArrayLike
|
position / offset (x, y) - relative <0, 1>
|
required
|
size |
tuple[float, float] | ArrayLike
|
desired size (width, height) - relative <0, 1>
|
required
|
Returns:
Type |
Description |
list[ArrayLike]
|
Curves scaled to fit desired area.
|
Source code in texnomagic/drawing.py
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176 | def curves_fit_area(self, pos : tuple[float, float] | npt.ArrayLike, size : tuple[float, float] | npt.ArrayLike) -> list[npt.ArrayLike]:
"""
Return curves scaled to fit area.
Useful for drawing curves in UI
See also [normalize][texnomagic.drawing.TexnoMagicDrawing.normalize].
Args:
pos: position / offset (x, y) - relative <0, 1>
size: desired size (width, height) - relative <0, 1>
Returns:
Curves scaled to fit desired area.
"""
pos = np.array(pos)
size = np.array(size)
k = np.min(size) / self.points_range
max_range = self.points_range * k
offset = pos + (size - max_range) / 2
scurves = []
for curve in self.curves:
if len(curve) > 0:
scurve = curve * k + offset
else:
scurve = curve
scurves.append(scurve)
return scurves
|
delete()
Delete the Drawing file.
Source code in texnomagic/drawing.py
| def delete(self):
"""Delete the Drawing file."""
if not self.path or not self.path.exists():
return
self.path.unlink()
|
flip_y_axis()
Flip Drawing along Y axis in-place.
Useful for compatibility with systems that use different Y axis sign.
Source code in texnomagic/drawing.py
178
179
180
181
182
183
184 | def flip_y_axis(self):
"""
Flip Drawing along Y axis in-place.
Useful for compatibility with systems that use different Y axis sign.
"""
self._points[:,1] = self.points_range - self._points[:,1]
|
load_curves()
Load Drawing curves from file.
Source code in texnomagic/drawing.py
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112 | def load_curves(self):
"""Load Drawing curves from file."""
curves = []
curve = []
with self.path.open('r') as f:
reader = csv.reader(f)
for row in reader:
if not row or '' in row:
# empty rows separate individual curves
curves.append(curve)
curve = []
continue
point = list(map(float, row[:2]))
curve.append(point)
curves.append(curve)
self.set_curves(curves)
|
normalize()
Normalize drawing points in-place into <0, self.points_range> range.
See also curves_fit_area.
Source code in texnomagic/drawing.py
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144 | def normalize(self):
"""
Normalize drawing points in-place into <0, self.points_range> range.
See also [curves_fit_area][texnomagic.drawing.TexnoMagicDrawing.curves_fit_area].
"""
if len(self.points) == 0:
return
# move to [0,0]
self._points -= np.min(self.points, axis=0)
# normalize
k = self.points_range / np.max((np.max(np.max(self._points, axis=0)), 0.2))
self._points *= k
# center
offset = (self.points_range - np.max(self._points, axis=0)) / 2
self._points += offset
|
pretty(size=True)
Pretty Drawing string with colors in rich formatting.
Source code in texnomagic/drawing.py
192
193
194
195
196
197
198
199
200
201 | def pretty(self, size=True) -> str:
"""Pretty Drawing string with colors in rich formatting."""
n_curves = len(self.curves)
n_points = len(self.points)
s = f"[white bold]{self.path.name}[/]: "
s += f"{n_points} points, {n_curves} curves"
if size:
fsize = math.ceil(self.file_size / 1024.0)
s += f", {fsize} kB"
return s
|
save()
Save drawing to CSV file specified by self.path.
Source code in texnomagic/drawing.py
114
115
116
117
118
119
120
121
122
123
124
125
126 | def save(self):
"""Save drawing to CSV file specified by self.path."""
self.path.parent.mkdir(parents=True, exist_ok=True)
with self.path.open('w', newline='') as f:
writer = csv.writer(f)
first = True
for curve in self.curves:
if first:
first = False
else:
# curves separator
writer.writerow([None, None])
writer.writerows(curve.tolist())
|
set_curves(curves)
Assign curves.
Converts to a single numpy.array points with curves being views
into the array for fast processing.
Source code in texnomagic/drawing.py
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89 | def set_curves(self, curves):
"""Assign curves.
Converts to a single numpy.array points with curves being views
into the array for fast processing."""
# keep all points in single continuous numpy array
self._points = np.array(list(itertools.chain(*curves)), dtype=np.float64)
self._curves = []
i = 0
for curve in curves:
n = len(curve)
# curves are numpy views into main points array
cview = self._points[i:i+n]
self._curves.append(cview)
i += n
|